mirror of
https://github.com/ARUP-CAS/aiscr-qgis-amcr-viewer.git
synced 2026-06-17 11:22:53 +02:00
Chore/code cleanup (#43)
* čištění kódu podle flake8 * Update .gitignore * Update .gitignore (#41) * oprava komentářů a překlad do angličtiny * oprava přihlašování
This commit is contained in:
@@ -13,20 +13,20 @@ BASE_URL = "https://api.aiscr.cz/2.2/oai"
|
|||||||
OUTPUT_FILE = os.path.join(CODELISTS_DIR, 'heslar.csv')
|
OUTPUT_FILE = os.path.join(CODELISTS_DIR, 'heslar.csv')
|
||||||
|
|
||||||
slovnicek = {
|
slovnicek = {
|
||||||
'obdobi' : 'heslo:obdobi',
|
'obdobi': 'heslo:obdobi',
|
||||||
'typ_akce' : 'heslo:akce_typ',
|
'typ_akce': 'heslo:akce_typ',
|
||||||
'areal' : 'heslo:areal',
|
'areal': 'heslo:areal',
|
||||||
'kraj' : 'ruian_kraj',
|
'kraj': 'ruian_kraj',
|
||||||
'organizace' : 'organizace',
|
'organizace': 'organizace',
|
||||||
'okres' : 'ruian_okres',
|
'okres': 'ruian_okres',
|
||||||
'katastr' : 'ruian_katastr',
|
'katastr': 'ruian_katastr',
|
||||||
'vedouci' : 'osoba',
|
'vedouci': 'osoba',
|
||||||
'pian_presnost' : 'heslo:pian_presnost',
|
'pian_presnost': 'heslo:pian_presnost',
|
||||||
'typ_lokality' : 'heslo:lokalita_typ',
|
'typ_lokality': 'heslo:lokalita_typ',
|
||||||
'druh_lokality' : 'heslo:lokalita_druh',
|
'druh_lokality': 'heslo:lokalita_druh',
|
||||||
'jistota' : 'heslo:jistota_urceni',
|
'jistota': 'heslo:jistota_urceni',
|
||||||
'lokalita_zachovalost' : 'heslo:stav_dochovani',
|
'lokalita_zachovalost': 'heslo:stav_dochovani',
|
||||||
'pristupnost' : 'heslo:pristupnost'
|
'pristupnost': 'heslo:pristupnost'
|
||||||
}
|
}
|
||||||
|
|
||||||
NS = {
|
NS = {
|
||||||
@@ -35,30 +35,35 @@ NS = {
|
|||||||
'oai_dc': 'http://www.openarchives.org/OAI/2.0/oai_dc/'
|
'oai_dc': 'http://www.openarchives.org/OAI/2.0/oai_dc/'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def ensure_codelists_dir():
|
def ensure_codelists_dir():
|
||||||
"""Creates the codelists directory if it does not exist."""
|
"""Creates the codelists directory if it does not exist."""
|
||||||
if not os.path.exists(CODELISTS_DIR):
|
if not os.path.exists(CODELISTS_DIR):
|
||||||
os.makedirs(CODELISTS_DIR)
|
os.makedirs(CODELISTS_DIR)
|
||||||
|
|
||||||
|
|
||||||
def parse_codelist_file(filename, target_dict=None):
|
def parse_codelist_file(filename, target_dict=None):
|
||||||
"""Reads a CSV codelist file and populates the target dictionary grouped by categories."""
|
"""
|
||||||
|
Reads a CSV codelist file and populates
|
||||||
|
the target dictionary grouped by categories.
|
||||||
|
"""
|
||||||
if target_dict is None:
|
if target_dict is None:
|
||||||
target_dict = {}
|
target_dict = {}
|
||||||
|
|
||||||
path = os.path.join(CODELISTS_DIR, filename)
|
path = os.path.join(CODELISTS_DIR, filename)
|
||||||
|
|
||||||
# Return early if the file doesn't exist to avoid missing file errors
|
# Return early if the file doesn't exist to avoid missing file errors
|
||||||
if not os.path.exists(path):
|
if not os.path.exists(path):
|
||||||
return target_dict
|
return target_dict
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Open the file using standard UTF-8 encoding
|
# Open the file using standard UTF-8 encoding
|
||||||
with open(path, 'r', encoding='utf-8') as f:
|
with open(path, 'r', encoding='utf-8') as f:
|
||||||
reader = csv.reader(f, delimiter=';')
|
reader = csv.reader(f, delimiter=';')
|
||||||
|
|
||||||
# Skip the CSV header row
|
# Skip the CSV header row
|
||||||
next(reader, None)
|
next(reader, None)
|
||||||
|
|
||||||
# Iterate through rows and extract label, code, and category
|
# Iterate through rows and extract label, code, and category
|
||||||
for row in reader:
|
for row in reader:
|
||||||
if len(row) >= 3:
|
if len(row) >= 3:
|
||||||
@@ -66,18 +71,24 @@ def parse_codelist_file(filename, target_dict=None):
|
|||||||
code = row[1].strip()
|
code = row[1].strip()
|
||||||
cat = row[2].strip()
|
cat = row[2].strip()
|
||||||
clean = code if code else None
|
clean = code if code else None
|
||||||
|
|
||||||
# Initialize a new dictionary for a category if encountered for the first time
|
# Initialize a new dictionary for a category if encountered
|
||||||
|
# for the first time
|
||||||
if cat not in target_dict:
|
if cat not in target_dict:
|
||||||
target_dict[cat] = {}
|
target_dict[cat] = {}
|
||||||
|
|
||||||
# Assign the extracted code to the corresponding label within the category
|
# Assign the extracted code to the corresponding label
|
||||||
|
# within the category
|
||||||
target_dict[cat][label] = clean
|
target_dict[cat][label] = clean
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
QgsMessageLog.logMessage(f"AMČR Codelist Read Error for {filename}: {e}", "AMČR", Qgis.Critical)
|
QgsMessageLog.logMessage(
|
||||||
|
f"AMČR Codelist Read Error for {filename}: {e}",
|
||||||
|
"AMČR", Qgis.Critical)
|
||||||
|
|
||||||
return target_dict
|
return target_dict
|
||||||
|
|
||||||
|
|
||||||
def load_all_data():
|
def load_all_data():
|
||||||
"""Loads the codelist during plugin startup."""
|
"""Loads the codelist during plugin startup."""
|
||||||
ensure_codelists_dir()
|
ensure_codelists_dir()
|
||||||
@@ -85,6 +96,7 @@ def load_all_data():
|
|||||||
parse_codelist_file('heslar.csv', categorized_data)
|
parse_codelist_file('heslar.csv', categorized_data)
|
||||||
return categorized_data
|
return categorized_data
|
||||||
|
|
||||||
|
|
||||||
def fetch_set(internal_name, api_set, task=None):
|
def fetch_set(internal_name, api_set, task=None):
|
||||||
dataset = []
|
dataset = []
|
||||||
params = {
|
params = {
|
||||||
@@ -92,42 +104,60 @@ def fetch_set(internal_name, api_set, task=None):
|
|||||||
"metadataPrefix": "oai_dc",
|
"metadataPrefix": "oai_dc",
|
||||||
"set": api_set
|
"set": api_set
|
||||||
}
|
}
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
# Kontrola zrušení v každém kroku
|
# Check for cancellation at each iteration
|
||||||
if task and task.isCanceled():
|
if task and task.isCanceled():
|
||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = requests.get(BASE_URL, params=params, timeout=30)
|
response = requests.get(BASE_URL, params=params, timeout=30)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
root = ET.fromstring(response.content) # nosec
|
root = ET.fromstring(response.content) # nosec
|
||||||
|
|
||||||
records = root.findall('.//oai:record', NS)
|
records = root.findall('.//oai:record', NS)
|
||||||
for rec in records:
|
for rec in records:
|
||||||
metadata = rec.find('.//oai_dc:dc', NS)
|
metadata = rec.find('.//oai_dc:dc', NS)
|
||||||
if metadata is not None:
|
if metadata is not None:
|
||||||
# Kód (identifier)
|
# Code (identifier)
|
||||||
kod = metadata.find('dc:identifier', NS).text if metadata.find('dc:identifier', NS) is not None else ""
|
identifier_el = metadata.find('dc:identifier', NS)
|
||||||
|
kod = (
|
||||||
# Název (title) - filtrujeme systémové popisky "AMČR - ..."
|
identifier_el.text
|
||||||
|
if identifier_el is not None
|
||||||
|
else ""
|
||||||
|
)
|
||||||
|
|
||||||
|
# Title – filter out system labels "AMČR - ..."
|
||||||
titles = metadata.findall('dc:title', NS)
|
titles = metadata.findall('dc:title', NS)
|
||||||
nazev = ""
|
nazev = ""
|
||||||
for t in titles:
|
for t in titles:
|
||||||
if t.text and not t.text.startswith("AMČR -") and not t.text.startswith(" AMČR -"):
|
if (
|
||||||
|
t.text
|
||||||
|
and not t.text.startswith("AMČR -")
|
||||||
|
and not t.text.startswith(" AMČR -")
|
||||||
|
):
|
||||||
nazev = t.text
|
nazev = t.text
|
||||||
break
|
break
|
||||||
# Pokud by náhodou žádný title neprošel filtrem, vezmeme první dostupný
|
# If no title passed the filter, fall back
|
||||||
|
# to the first available one
|
||||||
if not nazev and titles:
|
if not nazev and titles:
|
||||||
nazev = titles[0].text
|
nazev = titles[0].text
|
||||||
|
|
||||||
specialni_pripady = ['okres', 'katastr']
|
specialni_pripady = ['okres', 'katastr']
|
||||||
|
|
||||||
if internal_name in specialni_pripady:
|
if internal_name in specialni_pripady:
|
||||||
kod = nazev
|
kod = nazev
|
||||||
|
|
||||||
if internal_name == 'pristupnost':
|
if internal_name == 'pristupnost':
|
||||||
kod = next((t.text for t in titles if t.text and len(t.text) == 1 and t.text.isalpha()), None)
|
kod = next(
|
||||||
|
(
|
||||||
|
t.text for t in titles
|
||||||
|
if t.text
|
||||||
|
and len(t.text) == 1
|
||||||
|
and t.text.isalpha()
|
||||||
|
),
|
||||||
|
None
|
||||||
|
)
|
||||||
|
|
||||||
dataset.append({
|
dataset.append({
|
||||||
'Název': nazev,
|
'Název': nazev,
|
||||||
@@ -135,7 +165,7 @@ def fetch_set(internal_name, api_set, task=None):
|
|||||||
'Kategorie': internal_name
|
'Kategorie': internal_name
|
||||||
})
|
})
|
||||||
|
|
||||||
# Stránkování
|
# Pagination
|
||||||
token = root.find('.//oai:resumptionToken', NS)
|
token = root.find('.//oai:resumptionToken', NS)
|
||||||
if token is not None and token.text:
|
if token is not None and token.text:
|
||||||
params = {
|
params = {
|
||||||
@@ -145,40 +175,45 @@ def fetch_set(internal_name, api_set, task=None):
|
|||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
QgsMessageLog.logMessage(f"Chyba u setu {api_set}: {e}", "AMČR", Qgis.Warning)
|
QgsMessageLog.logMessage(
|
||||||
|
f"Chyba u setu {api_set}: {e}",
|
||||||
|
"AMČR", Qgis.Warning)
|
||||||
break
|
break
|
||||||
|
|
||||||
return dataset
|
return dataset
|
||||||
|
|
||||||
|
|
||||||
def download_heslare(task=None):
|
def download_heslare(task=None):
|
||||||
"""Fetches the codelists from the AMČR API and saves it to a CSV file."""
|
"""Fetches the codelists from the AMČR API and saves it to a CSV file."""
|
||||||
ensure_codelists_dir()
|
ensure_codelists_dir()
|
||||||
all_data = []
|
all_data = []
|
||||||
total_sets = len(slovnicek)
|
total_sets = len(slovnicek)
|
||||||
|
|
||||||
for index, (interni, api_nazev) in enumerate(slovnicek.items()):
|
for index, (interni, api_nazev) in enumerate(slovnicek.items()):
|
||||||
# Pokud uživatel task zrušil v liště QGISu
|
# Check if the user cancelled the task via the QGIS taskbar
|
||||||
if task and task.isCanceled():
|
if task and task.isCanceled():
|
||||||
return False
|
return False
|
||||||
|
|
||||||
QgsMessageLog.logMessage(f"Zpracovávám kategorii: {interni}...", "AMČR", Qgis.Info)
|
QgsMessageLog.logMessage(
|
||||||
|
f"Zpracovávám kategorii: {interni}...",
|
||||||
# Nyní předáváme task správně do upravené funkce
|
"AMČR", Qgis.Info)
|
||||||
|
|
||||||
|
# Pass the task correctly to the updated fetch function
|
||||||
data = fetch_set(interni, api_nazev, task=task)
|
data = fetch_set(interni, api_nazev, task=task)
|
||||||
|
|
||||||
if data is None:
|
if data is None:
|
||||||
return False # Bylo zrušeno uprostřed stahování
|
return False # Cancelled mid-download
|
||||||
|
|
||||||
all_data.extend(data)
|
all_data.extend(data)
|
||||||
|
|
||||||
# Reportování postupu (0-100)
|
# Report progress (0-100)
|
||||||
if task:
|
if task:
|
||||||
progress = (index + 1) / total_sets * 100
|
progress = (index + 1) / total_sets * 100
|
||||||
task.setProgress(progress)
|
task.setProgress(progress)
|
||||||
|
|
||||||
# Uložení do CSV
|
# Save to CSV
|
||||||
with open(OUTPUT_FILE, 'w', newline='', encoding='utf-8-sig') as f:
|
with open(OUTPUT_FILE, 'w', newline='', encoding='utf-8-sig') as f:
|
||||||
fieldnames = ['Název', 'Kód', 'Kategorie']
|
fieldnames = ['Název', 'Kód', 'Kategorie']
|
||||||
writer = csv.DictWriter(f, fieldnames=fieldnames, delimiter=';')
|
writer = csv.DictWriter(f, fieldnames=fieldnames, delimiter=';')
|
||||||
@@ -187,13 +222,11 @@ def download_heslare(task=None):
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def refresh_globals():
|
def refresh_globals():
|
||||||
"""Znovu načte data ze souborů do globálních proměnných."""
|
"""Reloads data from files into the global variables."""
|
||||||
global OBDOBI, TYP_AKCE, AREAL, KRAJE, ORGANIZACE, OKRESY, KATASTRY
|
|
||||||
global VEDOUCI, PIAN_PRESNOST, TYP_LOKALITY, DRUH_LOKALITY, JISTOTA, LOKALITA_ZACHOVALOST, PRISTUPNOST
|
|
||||||
|
|
||||||
data = load_all_data()
|
data = load_all_data()
|
||||||
|
|
||||||
OBDOBI.clear()
|
OBDOBI.clear()
|
||||||
OBDOBI.update(data.get('obdobi', {}))
|
OBDOBI.update(data.get('obdobi', {}))
|
||||||
TYP_AKCE.clear()
|
TYP_AKCE.clear()
|
||||||
@@ -224,7 +257,7 @@ def refresh_globals():
|
|||||||
PRISTUPNOST.update(data.get('pristupnost', {}))
|
PRISTUPNOST.update(data.get('pristupnost', {}))
|
||||||
|
|
||||||
|
|
||||||
# Inicializace prázdných diktů, které se naplní hned pod tím
|
# Initialize empty dicts that will be populated immediately below
|
||||||
OBDOBI = {}
|
OBDOBI = {}
|
||||||
TYP_AKCE = {}
|
TYP_AKCE = {}
|
||||||
AREAL = {}
|
AREAL = {}
|
||||||
@@ -240,4 +273,4 @@ JISTOTA = {}
|
|||||||
LOKALITA_ZACHOVALOST = {}
|
LOKALITA_ZACHOVALOST = {}
|
||||||
PRISTUPNOST = {}
|
PRISTUPNOST = {}
|
||||||
|
|
||||||
refresh_globals()
|
refresh_globals()
|
||||||
|
|||||||
+237
-116
@@ -1,16 +1,19 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from qgis.PyQt.QtWidgets import (QDialog, QVBoxLayout,
|
from qgis.PyQt.QtWidgets import (QDialog, QVBoxLayout,
|
||||||
QLineEdit, QDialogButtonBox,
|
QLineEdit, QDialogButtonBox,
|
||||||
QCheckBox, QGroupBox, QPushButton,
|
QCheckBox, QGroupBox, QPushButton,
|
||||||
QListWidget, QListWidgetItem, QHBoxLayout,
|
QListWidget, QListWidgetItem, QHBoxLayout,
|
||||||
QMessageBox, QLabel, QFormLayout)
|
QMessageBox, QLabel, QFormLayout)
|
||||||
from qgis.PyQt.QtCore import Qt, QSettings
|
from qgis.PyQt.QtCore import Qt, QSettings
|
||||||
from qgis.core import QgsTask, QgsApplication, QgsMessageLog, Qgis, QgsAuthMethodConfig
|
from qgis.core import (QgsTask, QgsApplication,
|
||||||
from .amcr_codelists import (OBDOBI, TYP_AKCE, KRAJE, AREAL, ORGANIZACE,
|
QgsMessageLog, Qgis, QgsAuthMethodConfig)
|
||||||
OKRESY, KATASTRY, VEDOUCI, PIAN_PRESNOST, TYP_LOKALITY,
|
from .amcr_codelists import (OBDOBI, TYP_AKCE, KRAJE, AREAL, ORGANIZACE,
|
||||||
DRUH_LOKALITY, JISTOTA, LOKALITA_ZACHOVALOST, PRISTUPNOST,
|
OKRESY, KATASTRY, VEDOUCI, PIAN_PRESNOST,
|
||||||
|
TYP_LOKALITY, DRUH_LOKALITY, JISTOTA,
|
||||||
|
LOKALITA_ZACHOVALOST, PRISTUPNOST,
|
||||||
download_heslare, refresh_globals)
|
download_heslare, refresh_globals)
|
||||||
|
|
||||||
|
|
||||||
class UpdateCodelistsTask(QgsTask):
|
class UpdateCodelistsTask(QgsTask):
|
||||||
def __init__(self, description):
|
def __init__(self, description):
|
||||||
super().__init__(description, QgsTask.CanCancel)
|
super().__init__(description, QgsTask.CanCancel)
|
||||||
@@ -18,9 +21,9 @@ class UpdateCodelistsTask(QgsTask):
|
|||||||
self.exception = None
|
self.exception = None
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
"""Tato část běží ve vedlejším vlákně."""
|
"""Runs in a background thread."""
|
||||||
try:
|
try:
|
||||||
# Voláme upravenou funkci
|
# Call the download function with the task reference
|
||||||
self.success = download_heslare(task=self)
|
self.success = download_heslare(task=self)
|
||||||
return self.success
|
return self.success
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -28,52 +31,61 @@ class UpdateCodelistsTask(QgsTask):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def finished(self, result):
|
def finished(self, result):
|
||||||
"""Tato část běží v hlavním vlákně po skončení run()."""
|
"""Runs in the main thread after run() completes."""
|
||||||
if result:
|
if result:
|
||||||
# Teď bezpečně aktualizujeme globální proměnné v hlavním vlákně
|
# Safely update the global variables in the main thread
|
||||||
refresh_globals()
|
refresh_globals()
|
||||||
QgsMessageLog.logMessage("Hesláře AMČR byly úspěšně aktualizovány.", "AMČR", Qgis.Info)
|
QgsMessageLog.logMessage(
|
||||||
|
"Hesláře AMČR byly úspěšně aktualizovány.",
|
||||||
|
"AMČR", Qgis.Info)
|
||||||
else:
|
else:
|
||||||
if self.isCanceled():
|
if self.isCanceled():
|
||||||
QgsMessageLog.logMessage("Aktualizace heslářů byla zrušena.", "AMČR", Qgis.Warning)
|
QgsMessageLog.logMessage(
|
||||||
|
"Aktualizace heslářů byla zrušena.",
|
||||||
|
"AMČR", Qgis.Warning)
|
||||||
else:
|
else:
|
||||||
QgsMessageLog.logMessage(f"Chyba aktualizace: {self.exception}", "AMČR", Qgis.Critical)
|
QgsMessageLog.logMessage(
|
||||||
|
f"Chyba aktualizace: {self.exception}",
|
||||||
|
"AMČR", Qgis.Critical)
|
||||||
|
|
||||||
|
|
||||||
class FilterableSelectionDialog(QDialog):
|
class FilterableSelectionDialog(QDialog):
|
||||||
"""
|
"""
|
||||||
A custom dialog for selecting multiple items from a list with a search filter.
|
A custom dialog for selecting multiple items from
|
||||||
|
a list with a search filter.
|
||||||
Updated for PyQt6/Qt6 compatibility.
|
Updated for PyQt6/Qt6 compatibility.
|
||||||
"""
|
"""
|
||||||
def __init__(self, title, data_dict, preselected_codes, parent=None):
|
def __init__(self, title, data_dict, preselected_codes, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.setWindowTitle(f"Výběr: {title}")
|
self.setWindowTitle(f"Výběr: {title}")
|
||||||
self.resize(400, 500)
|
self.resize(400, 500)
|
||||||
|
|
||||||
# Store the source data and previously selected items
|
# Store the source data and previously selected items
|
||||||
self.data_dict = data_dict
|
self.data_dict = data_dict
|
||||||
self.preselected = preselected_codes if preselected_codes else []
|
self.preselected = preselected_codes if preselected_codes else []
|
||||||
|
|
||||||
layout = QVBoxLayout()
|
layout = QVBoxLayout()
|
||||||
|
|
||||||
# Setup search input for filtering items
|
# Setup search input for filtering items
|
||||||
self.search_bar = QLineEdit()
|
self.search_bar = QLineEdit()
|
||||||
self.search_bar.setPlaceholderText("Hledat v seznamu...")
|
self.search_bar.setPlaceholderText("Hledat v seznamu...")
|
||||||
self.search_bar.textChanged.connect(self.filter_list)
|
self.search_bar.textChanged.connect(self.filter_list)
|
||||||
layout.addWidget(self.search_bar)
|
layout.addWidget(self.search_bar)
|
||||||
|
|
||||||
# Main list widget for displaying selectable items
|
# Main list widget for displaying selectable items
|
||||||
self.list_widget = QListWidget()
|
self.list_widget = QListWidget()
|
||||||
self.populate_list()
|
self.populate_list()
|
||||||
layout.addWidget(self.list_widget)
|
layout.addWidget(self.list_widget)
|
||||||
|
|
||||||
# Standard OK/Cancel dialog buttons
|
# Standard OK/Cancel dialog buttons
|
||||||
buttons = QDialogButtonBox(
|
buttons = QDialogButtonBox(
|
||||||
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
QDialogButtonBox.StandardButton.Ok
|
||||||
|
| QDialogButtonBox.StandardButton.Cancel
|
||||||
)
|
)
|
||||||
buttons.accepted.connect(self.accept)
|
buttons.accepted.connect(self.accept)
|
||||||
buttons.rejected.connect(self.reject)
|
buttons.rejected.connect(self.reject)
|
||||||
layout.addWidget(buttons)
|
layout.addWidget(buttons)
|
||||||
|
|
||||||
self.setLayout(layout)
|
self.setLayout(layout)
|
||||||
|
|
||||||
def populate_list(self):
|
def populate_list(self):
|
||||||
@@ -82,19 +94,19 @@ class FilterableSelectionDialog(QDialog):
|
|||||||
for name in sorted_names:
|
for name in sorted_names:
|
||||||
code = self.data_dict[name]
|
code = self.data_dict[name]
|
||||||
item = QListWidgetItem(name)
|
item = QListWidgetItem(name)
|
||||||
|
|
||||||
# Store the actual code (ID) hidden in the UserRole
|
# Store the actual code (ID) hidden in the UserRole
|
||||||
item.setData(Qt.ItemDataRole.UserRole, code)
|
item.setData(Qt.ItemDataRole.UserRole, code)
|
||||||
|
|
||||||
# Make the item checkable (adds a checkbox)
|
# Make the item checkable (adds a checkbox)
|
||||||
item.setFlags(item.flags() | Qt.ItemFlag.ItemIsUserCheckable)
|
item.setFlags(item.flags() | Qt.ItemFlag.ItemIsUserCheckable)
|
||||||
|
|
||||||
# Restore previous selection state
|
# Restore previous selection state
|
||||||
if code in self.preselected:
|
if code in self.preselected:
|
||||||
item.setCheckState(Qt.CheckState.Checked)
|
item.setCheckState(Qt.CheckState.Checked)
|
||||||
else:
|
else:
|
||||||
item.setCheckState(Qt.CheckState.Unchecked)
|
item.setCheckState(Qt.CheckState.Unchecked)
|
||||||
|
|
||||||
self.list_widget.addItem(item)
|
self.list_widget.addItem(item)
|
||||||
|
|
||||||
def filter_list(self, text):
|
def filter_list(self, text):
|
||||||
@@ -125,20 +137,21 @@ class AmcrFilterDialog(QDialog):
|
|||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.setWindowTitle("Filtr AMČR")
|
self.setWindowTitle("Filtr AMČR")
|
||||||
self.resize(500, 750)
|
self.resize(500, 750)
|
||||||
|
|
||||||
# Determines if we are fetching 'akce' (projects) or 'lokalita' (locations)
|
# Determines if we are fetching 'akce' (events)
|
||||||
|
# or 'lokalita' (sites)
|
||||||
self.typ_dat = typ_dat
|
self.typ_dat = typ_dat
|
||||||
|
|
||||||
|
|
||||||
# Cache dictionary to store selected codes for each category
|
# Cache dictionary to store selected codes for each category
|
||||||
self.selection_cache = {
|
self.selection_cache = {
|
||||||
'organizace': [], 'kraj': [], 'obdobi': [], 'areal': [],
|
'organizace': [], 'kraj': [], 'obdobi': [], 'areal': [],
|
||||||
'typ_akce': [], 'okres': [], 'katastr': [], 'vedouci': [], 'pian_presnost': [], 'pristupnost': [],
|
'typ_akce': [], 'okres': [], 'katastr': [], 'vedouci': [],
|
||||||
'typ_lokality': [], 'druh_lokality': [], 'jistota': [], 'lokalita_zachovalost': []
|
'pian_presnost': [], 'pristupnost': [], 'typ_lokality': [],
|
||||||
|
'druh_lokality': [], 'jistota': [], 'lokalita_zachovalost': []
|
||||||
}
|
}
|
||||||
|
|
||||||
layout = QVBoxLayout()
|
layout = QVBoxLayout()
|
||||||
|
|
||||||
# Filter by current map canvas extent
|
# Filter by current map canvas extent
|
||||||
self.chk_bbox = QCheckBox("Omezit vyhledávání rozsahem okna")
|
self.chk_bbox = QCheckBox("Omezit vyhledávání rozsahem okna")
|
||||||
self.chk_bbox.setChecked(True)
|
self.chk_bbox.setChecked(True)
|
||||||
@@ -149,9 +162,9 @@ class AmcrFilterDialog(QDialog):
|
|||||||
if self.typ_dat == "akce":
|
if self.typ_dat == "akce":
|
||||||
self.chk_posevidence = QCheckBox("Pouze pozitivní zjištění")
|
self.chk_posevidence = QCheckBox("Pouze pozitivní zjištění")
|
||||||
layout.addWidget(self.chk_posevidence)
|
layout.addWidget(self.chk_posevidence)
|
||||||
|
|
||||||
layout.addSpacing(10)
|
layout.addSpacing(10)
|
||||||
|
|
||||||
# Spatial information – valid for all
|
# Spatial information – valid for all
|
||||||
|
|
||||||
self.picker_kraj = self.setup_picker("Kraj", 'kraj', KRAJE)
|
self.picker_kraj = self.setup_picker("Kraj", 'kraj', KRAJE)
|
||||||
@@ -160,156 +173,223 @@ class AmcrFilterDialog(QDialog):
|
|||||||
self.picker_okres = self.setup_picker("Okres", 'okres', OKRESY)
|
self.picker_okres = self.setup_picker("Okres", 'okres', OKRESY)
|
||||||
layout.addWidget(self.picker_okres)
|
layout.addWidget(self.picker_okres)
|
||||||
|
|
||||||
self.picker_katastr = self.setup_picker("Katastr", 'katastr', KATASTRY)
|
self.picker_katastr = self.setup_picker(
|
||||||
|
"Katastr",
|
||||||
|
'katastr',
|
||||||
|
KATASTRY
|
||||||
|
)
|
||||||
layout.addWidget(self.picker_katastr)
|
layout.addWidget(self.picker_katastr)
|
||||||
|
|
||||||
self.picker_presnost = self.setup_picker("PIAN – přesnost", 'pian_presnost', PIAN_PRESNOST)
|
self.picker_presnost = self.setup_picker(
|
||||||
|
"PIAN – přesnost",
|
||||||
|
'pian_presnost',
|
||||||
|
PIAN_PRESNOST
|
||||||
|
)
|
||||||
layout.addWidget(self.picker_presnost)
|
layout.addWidget(self.picker_presnost)
|
||||||
|
|
||||||
self.picker_pristupnost = self.setup_picker("Přístupnost", 'pristupnost', PRISTUPNOST)
|
self.picker_pristupnost = self.setup_picker(
|
||||||
|
"Přístupnost",
|
||||||
|
'pristupnost',
|
||||||
|
PRISTUPNOST
|
||||||
|
)
|
||||||
layout.addWidget(self.picker_pristupnost)
|
layout.addWidget(self.picker_pristupnost)
|
||||||
|
|
||||||
# Filters valid for Akce
|
# Filters valid for Akce
|
||||||
|
|
||||||
if self.typ_dat == "akce":
|
if self.typ_dat == "akce":
|
||||||
self.picker_org = self.setup_picker("Organizace", 'organizace', ORGANIZACE)
|
self.picker_org = self.setup_picker(
|
||||||
|
"Organizace",
|
||||||
|
'organizace',
|
||||||
|
ORGANIZACE
|
||||||
|
)
|
||||||
layout.addWidget(self.picker_org)
|
layout.addWidget(self.picker_org)
|
||||||
|
|
||||||
self.picker_vedouci = self.setup_picker("Vedoucí výzkumu", 'vedouci', VEDOUCI)
|
self.picker_vedouci = self.setup_picker(
|
||||||
|
"Vedoucí výzkumu",
|
||||||
|
'vedouci',
|
||||||
|
VEDOUCI
|
||||||
|
)
|
||||||
layout.addWidget(self.picker_vedouci)
|
layout.addWidget(self.picker_vedouci)
|
||||||
|
|
||||||
# Type of event
|
# Type of event
|
||||||
|
|
||||||
self.picker_typ = self.setup_picker("Typ výzkumu", 'typ_akce', TYP_AKCE)
|
self.picker_typ = self.setup_picker(
|
||||||
|
"Typ výzkumu",
|
||||||
|
'typ_akce',
|
||||||
|
TYP_AKCE
|
||||||
|
)
|
||||||
layout.addWidget(self.picker_typ)
|
layout.addWidget(self.picker_typ)
|
||||||
|
|
||||||
# Filters valid for Lokality
|
# Filters valid for Lokality
|
||||||
|
|
||||||
if self.typ_dat == "lokalita":
|
if self.typ_dat == "lokalita":
|
||||||
self.picker_typ_lokality = self.setup_picker("Lokalita – typ", 'typ_lokality', TYP_LOKALITY)
|
self.picker_typ_lokality = self.setup_picker(
|
||||||
|
"Lokalita – typ",
|
||||||
|
'typ_lokality',
|
||||||
|
TYP_LOKALITY
|
||||||
|
)
|
||||||
layout.addWidget(self.picker_typ_lokality)
|
layout.addWidget(self.picker_typ_lokality)
|
||||||
|
|
||||||
self.picker_druh_lokality = self.setup_picker("Lokalita – druh", 'druh_lokality', DRUH_LOKALITY)
|
self.picker_druh_lokality = self.setup_picker(
|
||||||
|
"Lokalita – druh",
|
||||||
|
'druh_lokality',
|
||||||
|
DRUH_LOKALITY
|
||||||
|
)
|
||||||
layout.addWidget(self.picker_druh_lokality)
|
layout.addWidget(self.picker_druh_lokality)
|
||||||
|
|
||||||
self.picker_jistota = self.setup_picker("Lokalita – jistota určení", 'jistota', JISTOTA)
|
self.picker_jistota = self.setup_picker(
|
||||||
|
"Lokalita – jistota určení",
|
||||||
|
'jistota',
|
||||||
|
JISTOTA
|
||||||
|
)
|
||||||
layout.addWidget(self.picker_jistota)
|
layout.addWidget(self.picker_jistota)
|
||||||
|
|
||||||
self.picker_lokalita_zachovalost = self.setup_picker("Lokalita - stav dochování", 'lokalita_zachovalost', LOKALITA_ZACHOVALOST)
|
self.picker_lokalita_zachovalost = self.setup_picker(
|
||||||
|
"Lokalita - stav dochování",
|
||||||
|
'lokalita_zachovalost',
|
||||||
|
LOKALITA_ZACHOVALOST
|
||||||
|
)
|
||||||
layout.addWidget(self.picker_lokalita_zachovalost)
|
layout.addWidget(self.picker_lokalita_zachovalost)
|
||||||
|
|
||||||
# Contextual information
|
# Contextual information
|
||||||
|
|
||||||
self.picker_obdobi = self.setup_picker("Období", 'obdobi', OBDOBI)
|
self.picker_obdobi = self.setup_picker("Období", 'obdobi', OBDOBI)
|
||||||
layout.addWidget(self.picker_obdobi)
|
layout.addWidget(self.picker_obdobi)
|
||||||
|
|
||||||
self.picker_areal = self.setup_picker("Areál", 'areal', AREAL)
|
self.picker_areal = self.setup_picker("Areál", 'areal', AREAL)
|
||||||
layout.addWidget(self.picker_areal)
|
layout.addWidget(self.picker_areal)
|
||||||
|
|
||||||
# Option to download related components table
|
# Option to download related components table
|
||||||
self.chk_komponenty = QCheckBox("Načíst komponenty")
|
self.chk_komponenty = QCheckBox("Načíst komponenty")
|
||||||
layout.addWidget(self.chk_komponenty)
|
layout.addWidget(self.chk_komponenty)
|
||||||
|
|
||||||
# Pushes everything above to the top
|
# Pushes everything above to the top
|
||||||
layout.addStretch(1)
|
layout.addStretch(1)
|
||||||
|
|
||||||
# Main dialog OK/Cancel/Update buttons
|
# Main dialog OK/Cancel/Update buttons
|
||||||
|
|
||||||
buttons = QDialogButtonBox()
|
buttons = QDialogButtonBox()
|
||||||
|
|
||||||
self.btn_update = QPushButton("Aktualizovat hesláře 🔄")
|
self.btn_update = QPushButton("Aktualizovat hesláře 🔄")
|
||||||
self.btn_update.setToolTip("Provede kompletní aktualizaci heslářů AMČR. Toto bude trvat pár minut.")
|
self.btn_update.setToolTip(
|
||||||
|
"Provede kompletní aktualizaci heslářů AMČR. "
|
||||||
|
"Toto bude trvat pár minut."
|
||||||
|
)
|
||||||
self.btn_update.clicked.connect(self.action_update_heslare)
|
self.btn_update.clicked.connect(self.action_update_heslare)
|
||||||
|
|
||||||
buttons.addButton(self.btn_update, QDialogButtonBox.ButtonRole.ActionRole)
|
buttons.addButton(
|
||||||
|
self.btn_update,
|
||||||
|
QDialogButtonBox.ButtonRole.ActionRole
|
||||||
|
)
|
||||||
buttons.addButton(QDialogButtonBox.StandardButton.Ok)
|
buttons.addButton(QDialogButtonBox.StandardButton.Ok)
|
||||||
buttons.addButton(QDialogButtonBox.StandardButton.Cancel)
|
buttons.addButton(QDialogButtonBox.StandardButton.Cancel)
|
||||||
|
|
||||||
buttons.accepted.connect(self.accept)
|
buttons.accepted.connect(self.accept)
|
||||||
buttons.rejected.connect(self.reject)
|
buttons.rejected.connect(self.reject)
|
||||||
layout.addWidget(buttons)
|
layout.addWidget(buttons)
|
||||||
|
|
||||||
self.setLayout(layout)
|
self.setLayout(layout)
|
||||||
|
|
||||||
def setup_picker(self, label_text, cache_key, data_source, extra_btn=None):
|
def setup_picker(self, label_text, cache_key, data_source, extra_btn=None):
|
||||||
"""
|
"""
|
||||||
Creates a reusable UI component consisting of a label, a read-only
|
Creates a reusable UI component consisting of a label, a read-only
|
||||||
text field showing selected items, and a button to open the selection dialog.
|
text field showing selected items, and a button to open
|
||||||
|
the selection dialog.
|
||||||
"""
|
"""
|
||||||
row_widget = QGroupBox(label_text)
|
row_widget = QGroupBox(label_text)
|
||||||
row_layout = QHBoxLayout()
|
row_layout = QHBoxLayout()
|
||||||
row_layout.setContentsMargins(5, 5, 5, 5)
|
row_layout.setContentsMargins(5, 5, 5, 5)
|
||||||
|
|
||||||
# Read-only field displaying the names of selected items
|
# Read-only field displaying the names of selected items
|
||||||
display_field = QLineEdit()
|
display_field = QLineEdit()
|
||||||
display_field.setReadOnly(True)
|
display_field.setReadOnly(True)
|
||||||
display_field.setPlaceholderText("Nic nevybráno (vše)")
|
display_field.setPlaceholderText("Nic nevybráno (vše)")
|
||||||
display_field.setStyleSheet("background-color: #f0f0f0; color: #333;")
|
display_field.setStyleSheet("background-color: #f0f0f0; color: #333;")
|
||||||
|
|
||||||
btn = QPushButton("Vybrat...")
|
btn = QPushButton("Vybrat...")
|
||||||
btn.setFixedWidth(80)
|
btn.setFixedWidth(80)
|
||||||
|
|
||||||
# Nested function that handles opening the dialog and saving results
|
# Nested handler: opens the selection dialog and saves the result
|
||||||
def open_dialog():
|
def open_dialog():
|
||||||
dlg = FilterableSelectionDialog(label_text, data_source, self.selection_cache[cache_key], self)
|
dlg = FilterableSelectionDialog(
|
||||||
|
label_text,
|
||||||
|
data_source,
|
||||||
|
self.selection_cache[cache_key],
|
||||||
|
self
|
||||||
|
)
|
||||||
if dlg.exec() == QDialog.DialogCode.Accepted:
|
if dlg.exec() == QDialog.DialogCode.Accepted:
|
||||||
codes, labels = dlg.get_selected_codes()
|
codes, labels = dlg.get_selected_codes()
|
||||||
# Update local cache with selected IDs
|
# Update the local cache with selected IDs
|
||||||
self.selection_cache[cache_key] = codes
|
self.selection_cache[cache_key] = codes
|
||||||
# Update the UI text field with selected names
|
# Update the display field with the selected item names
|
||||||
if labels:
|
if labels:
|
||||||
display_field.setText(", ".join(labels))
|
display_field.setText(", ".join(labels))
|
||||||
else:
|
else:
|
||||||
display_field.clear()
|
display_field.clear()
|
||||||
|
|
||||||
# Special case: Pre-fill specific accuracy levels by default
|
# Special case: pre-select default PIAN accuracy levels
|
||||||
if cache_key == 'pian_presnost':
|
if cache_key == 'pian_presnost':
|
||||||
display_field.setText("odchylka jednotky metrů, odchylka desítky metrů, odchylka stovky metrů")
|
display_field.setText(
|
||||||
self.selection_cache[cache_key] = ['HES-000861', 'HES-000862', 'HES-000863']
|
"odchylka jednotky metrů, odchylka desítky metrů, "
|
||||||
|
"odchylka stovky metrů"
|
||||||
|
)
|
||||||
|
self.selection_cache[cache_key] = [
|
||||||
|
'HES-000861',
|
||||||
|
'HES-000862',
|
||||||
|
'HES-000863'
|
||||||
|
]
|
||||||
|
|
||||||
btn.clicked.connect(open_dialog)
|
btn.clicked.connect(open_dialog)
|
||||||
|
|
||||||
row_layout.addWidget(display_field)
|
row_layout.addWidget(display_field)
|
||||||
row_layout.addWidget(btn)
|
row_layout.addWidget(btn)
|
||||||
|
|
||||||
# Add an optional extra button (e.g., the refresh button for leaders)
|
# Optionally append an extra button (e.g. a refresh button)
|
||||||
if extra_btn:
|
if extra_btn:
|
||||||
row_layout.addWidget(extra_btn)
|
row_layout.addWidget(extra_btn)
|
||||||
|
|
||||||
row_widget.setLayout(row_layout)
|
row_widget.setLayout(row_layout)
|
||||||
return row_widget
|
return row_widget
|
||||||
|
|
||||||
def action_update_heslare(self):
|
def action_update_heslare(self):
|
||||||
# Vytvoření instance tasku
|
# Create the task instance
|
||||||
task = UpdateCodelistsTask("Aktualizace heslářů AMČR")
|
task = UpdateCodelistsTask("Aktualizace heslářů AMČR")
|
||||||
|
|
||||||
# Povolíme tlačítko zpět bez ohledu na výsledek
|
# Re-enable the button regardless of the outcome
|
||||||
task.taskCompleted.connect(lambda: self.btn_update.setEnabled(True))
|
task.taskCompleted.connect(lambda: self.btn_update.setEnabled(True))
|
||||||
task.taskTerminated.connect(lambda: self.btn_update.setEnabled(True))
|
task.taskTerminated.connect(lambda: self.btn_update.setEnabled(True))
|
||||||
|
|
||||||
task.taskCompleted.connect(lambda: QMessageBox.information(self, "Hotovo", "Hesláře byly úspěšně aktualizovány."))
|
task.taskCompleted.connect(lambda: QMessageBox.information(
|
||||||
|
self,
|
||||||
# Ošetření, aby se přesně ukázala případná chyba
|
"Hotovo",
|
||||||
|
"Hesláře byly úspěšně aktualizovány."
|
||||||
|
))
|
||||||
|
|
||||||
|
# Show the exact error if the task fails
|
||||||
def on_error():
|
def on_error():
|
||||||
if task.exception:
|
if task.exception:
|
||||||
# Tohle ti přesně řekne, na čem to teď padá (např. PermissionError)
|
# This will show exactly what went wrong (e.g. PermissionError)
|
||||||
msg = f"Aktualizace selhala z důvodu chyby:\n{str(task.exception)}"
|
msg = (
|
||||||
|
"Aktualizace selhala z důvodu chyby:\n"
|
||||||
|
f"{str(task.exception)}"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
msg = "Aktualizace byla zrušena uživatelem."
|
msg = "Aktualizace byla zrušena uživatelem."
|
||||||
QMessageBox.warning(self, "Chyba / Zrušeno", msg)
|
QMessageBox.warning(self, "Chyba / Zrušeno", msg)
|
||||||
|
|
||||||
task.taskTerminated.connect(on_error)
|
task.taskTerminated.connect(on_error)
|
||||||
|
|
||||||
QgsApplication.taskManager().addTask(task)
|
QgsApplication.taskManager().addTask(task)
|
||||||
|
|
||||||
def get_bbox(self):
|
def get_bbox(self):
|
||||||
return "true" if self.chk_bbox.isChecked() else "false"
|
return "true" if self.chk_bbox.isChecked() else "false"
|
||||||
|
|
||||||
def get_komponenty(self):
|
def get_komponenty(self):
|
||||||
return "true" if self.chk_komponenty.isChecked() else "false"
|
return "true" if self.chk_komponenty.isChecked() else "false"
|
||||||
|
|
||||||
def get_filters(self):
|
def get_filters(self):
|
||||||
"""Compiles the user selections from the cache into API-ready filter parameters."""
|
"""Compiles the user selections from the cache into
|
||||||
|
API-ready filter parameters."""
|
||||||
filters = {}
|
filters = {}
|
||||||
|
|
||||||
if self.selection_cache['kraj']:
|
if self.selection_cache['kraj']:
|
||||||
@@ -323,10 +403,10 @@ class AmcrFilterDialog(QDialog):
|
|||||||
if self.selection_cache['areal']:
|
if self.selection_cache['areal']:
|
||||||
filters['f_areal'] = self.selection_cache['areal']
|
filters['f_areal'] = self.selection_cache['areal']
|
||||||
if self.selection_cache['pian_presnost']:
|
if self.selection_cache['pian_presnost']:
|
||||||
filters['f_pian_presnost'] = self.selection_cache['pian_presnost']
|
filters['f_pian_presnost'] = self.selection_cache['pian_presnost']
|
||||||
if self.selection_cache['pristupnost']:
|
if self.selection_cache['pristupnost']:
|
||||||
filters['pristupnost'] = self.selection_cache['pristupnost']
|
filters['pristupnost'] = self.selection_cache['pristupnost']
|
||||||
|
|
||||||
if self.typ_dat == "akce":
|
if self.typ_dat == "akce":
|
||||||
if self.chk_posevidence.isChecked():
|
if self.chk_posevidence.isChecked():
|
||||||
filters['posevidence'] = 'true'
|
filters['posevidence'] = 'true'
|
||||||
@@ -346,12 +426,14 @@ class AmcrFilterDialog(QDialog):
|
|||||||
filters['f_jistota'] = self.selection_cache['jistota']
|
filters['f_jistota'] = self.selection_cache['jistota']
|
||||||
if self.selection_cache['lokalita_zachovalost']:
|
if self.selection_cache['lokalita_zachovalost']:
|
||||||
filters['f_lokalita_zachovalost'] = self.selection_cache['lokalita_zachovalost']
|
filters['f_lokalita_zachovalost'] = self.selection_cache['lokalita_zachovalost']
|
||||||
|
|
||||||
return filters
|
return filters
|
||||||
|
|
||||||
|
|
||||||
class LoginDialog(QDialog):
|
class LoginDialog(QDialog):
|
||||||
"""
|
"""
|
||||||
Dialog for saving AMČR login credentials securely in the QGIS Authentication Manager.
|
Dialog for saving AMČR login credentials securely in the
|
||||||
|
QGIS Authentication Manager.
|
||||||
|
|
||||||
Credentials are encrypted by the platform's native secret storage
|
Credentials are encrypted by the platform's native secret storage
|
||||||
(DPAPI on Windows, Keychain on macOS, encrypted SQLite on Linux).
|
(DPAPI on Windows, Keychain on macOS, encrypted SQLite on Linux).
|
||||||
@@ -365,10 +447,13 @@ class LoginDialog(QDialog):
|
|||||||
- storeAuthenticationConfig() and loadAuthenticationConfig() both have
|
- storeAuthenticationConfig() and loadAuthenticationConfig() both have
|
||||||
SIP_INOUT on their config parameter, so Python bindings return a tuple
|
SIP_INOUT on their config parameter, so Python bindings return a tuple
|
||||||
(bool, QgsAuthMethodConfig) rather than just bool. Always unpack both.
|
(bool, QgsAuthMethodConfig) rather than just bool. Always unpack both.
|
||||||
|
- loadAuthenticationConfig() with full=False loads only metadata (name, method,
|
||||||
|
id) but NOT the config() values like username/password. Use full=True to
|
||||||
|
access those.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
SETTINGS_KEY = "amcr_viewer/auth_config_id"
|
SETTINGS_KEY = "amcr_viewer/auth_config_id"
|
||||||
CONFIG_NAME = "AMČR Viewer"
|
CONFIG_NAME = "AMČR Viewer"
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
@@ -379,17 +464,29 @@ class LoginDialog(QDialog):
|
|||||||
|
|
||||||
# Check whether a config ID is already stored from a previous session.
|
# Check whether a config ID is already stored from a previous session.
|
||||||
# We attempt a lightweight load (full=False) to confirm it is readable,
|
# We attempt a lightweight load (full=False) to confirm it is readable,
|
||||||
# since hasConfigId() may return False even for valid configs (cache lag).
|
# since hasConfigId() may return False even for valid configs
|
||||||
|
# (cache lag).
|
||||||
|
# The Auth Manager must be unlocked before we attempt to read from it;
|
||||||
|
# otherwise loadAuthenticationConfig() returns ok=False even for valid
|
||||||
|
# configs, causing _has_saved to be incorrectly set to False.
|
||||||
existing_id = QSettings().value(self.SETTINGS_KEY, "")
|
existing_id = QSettings().value(self.SETTINGS_KEY, "")
|
||||||
self._has_saved = bool(existing_id) and bool(self._load_username_from_config(existing_id))
|
if existing_id:
|
||||||
|
QgsApplication.authManager().setMasterPassword(True)
|
||||||
|
username = self._load_username_from_config(existing_id)
|
||||||
|
self._has_saved = bool(existing_id) and bool(username)
|
||||||
|
|
||||||
if self._has_saved:
|
if self._has_saved:
|
||||||
info = QLabel("✔ Přihlašovací údaje jsou bezpečně uloženy ve správci autentizace QGIS.\n"
|
info = QLabel(
|
||||||
"Vyplňte pole níže pouze pokud je chcete změnit.")
|
"✔ Přihlašovací údaje jsou bezpečně uloženy "
|
||||||
|
"ve správci autentizace QGIS.\n"
|
||||||
|
"Vyplňte pole níže pouze pokud je chcete změnit."
|
||||||
|
)
|
||||||
info.setStyleSheet("color: green; font-style: italic;")
|
info.setStyleSheet("color: green; font-style: italic;")
|
||||||
else:
|
else:
|
||||||
info = QLabel("Zadejte přihlašovací údaje k Digitálnímu archivu AMČR.\n"
|
info = QLabel(
|
||||||
"Budou zašifrovaně uloženy ve správci autentizace QGIS.")
|
"Zadejte přihlašovací údaje k Digitálnímu archivu AMČR.\n"
|
||||||
|
"Budou zašifrovaně uloženy ve správci autentizace QGIS."
|
||||||
|
)
|
||||||
info.setWordWrap(True)
|
info.setWordWrap(True)
|
||||||
layout.addWidget(info)
|
layout.addWidget(info)
|
||||||
layout.addSpacing(8)
|
layout.addSpacing(8)
|
||||||
@@ -406,7 +503,9 @@ class LoginDialog(QDialog):
|
|||||||
self.txt_pass = QLineEdit()
|
self.txt_pass = QLineEdit()
|
||||||
self.txt_pass.setEchoMode(QLineEdit.EchoMode.Password)
|
self.txt_pass.setEchoMode(QLineEdit.EchoMode.Password)
|
||||||
self.txt_pass.setPlaceholderText(
|
self.txt_pass.setPlaceholderText(
|
||||||
"ponechte prázdné pro zachování stávajícího hesla" if self._has_saved else "heslo"
|
"ponechte prázdné pro zachování stávajícího hesla"
|
||||||
|
if self._has_saved
|
||||||
|
else "heslo"
|
||||||
)
|
)
|
||||||
form.addRow("Heslo:", self.txt_pass)
|
form.addRow("Heslo:", self.txt_pass)
|
||||||
|
|
||||||
@@ -422,7 +521,8 @@ class LoginDialog(QDialog):
|
|||||||
layout.addStretch(1)
|
layout.addStretch(1)
|
||||||
|
|
||||||
buttons = QDialogButtonBox(
|
buttons = QDialogButtonBox(
|
||||||
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
QDialogButtonBox.StandardButton.Ok
|
||||||
|
| QDialogButtonBox.StandardButton.Cancel
|
||||||
)
|
)
|
||||||
buttons.accepted.connect(self._save_and_accept)
|
buttons.accepted.connect(self._save_and_accept)
|
||||||
buttons.rejected.connect(self.reject)
|
buttons.rejected.connect(self.reject)
|
||||||
@@ -438,7 +538,8 @@ class LoginDialog(QDialog):
|
|||||||
def _load_config(config_id: str, full: bool = False):
|
def _load_config(config_id: str, full: bool = False):
|
||||||
"""
|
"""
|
||||||
Attempt to load a QgsAuthMethodConfig by ID.
|
Attempt to load a QgsAuthMethodConfig by ID.
|
||||||
Returns (ok, cfg). Never raises; returns (False, empty cfg) on any error.
|
Returns (ok, cfg). Never raises; returns (False, empty cfg)
|
||||||
|
on any error.
|
||||||
full=True decrypts and includes the password.
|
full=True decrypts and includes the password.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
@@ -453,8 +554,10 @@ class LoginDialog(QDialog):
|
|||||||
return False, QgsAuthMethodConfig()
|
return False, QgsAuthMethodConfig()
|
||||||
|
|
||||||
def _load_username_from_config(self, config_id: str) -> str:
|
def _load_username_from_config(self, config_id: str) -> str:
|
||||||
"""Load just the username from a stored config (no password decryption)."""
|
"""Load the username from a stored config.
|
||||||
ok, cfg = self._load_config(config_id, full=False)
|
Requires full=True since config() values are only populated
|
||||||
|
when the config is fully decrypted."""
|
||||||
|
ok, cfg = self._load_config(config_id, full=True)
|
||||||
return cfg.config("username", "") if ok else ""
|
return cfg.config("username", "") if ok else ""
|
||||||
|
|
||||||
def _ensure_master_password(self) -> bool:
|
def _ensure_master_password(self) -> bool:
|
||||||
@@ -469,11 +572,13 @@ class LoginDialog(QDialog):
|
|||||||
QMessageBox.critical(
|
QMessageBox.critical(
|
||||||
self, "Správce autentizace nedostupný",
|
self, "Správce autentizace nedostupný",
|
||||||
"Správce autentizace QGIS je zakázán nebo poškozený.\n"
|
"Správce autentizace QGIS je zakázán nebo poškozený.\n"
|
||||||
"Zkuste obnovit databázi: Nastavení → Možnosti → Autentizace → Pomůcky."
|
"Zkuste obnovit databázi: "
|
||||||
|
"Nastavení → Možnosti → Autentizace → Pomůcky."
|
||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# setMasterPassword(True) shows the QGIS master password dialog if needed
|
# setMasterPassword(True) shows the QGIS
|
||||||
|
# master password dialog if needed
|
||||||
if not auth_mgr.setMasterPassword(True):
|
if not auth_mgr.setMasterPassword(True):
|
||||||
return False # User cancelled the master password dialog
|
return False # User cancelled the master password dialog
|
||||||
|
|
||||||
@@ -488,7 +593,11 @@ class LoginDialog(QDialog):
|
|||||||
password = self.txt_pass.text()
|
password = self.txt_pass.text()
|
||||||
|
|
||||||
if not username:
|
if not username:
|
||||||
QMessageBox.warning(self, "Chybí údaje", "Vyplňte prosím e-mailovou adresu.")
|
QMessageBox.warning(
|
||||||
|
self,
|
||||||
|
"Chybí údaje",
|
||||||
|
"Vyplňte prosím e-mailovou adresu."
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
existing_id = QSettings().value(self.SETTINGS_KEY, "")
|
existing_id = QSettings().value(self.SETTINGS_KEY, "")
|
||||||
@@ -521,13 +630,19 @@ class LoginDialog(QDialog):
|
|||||||
|
|
||||||
settings = QSettings()
|
settings = QSettings()
|
||||||
|
|
||||||
# Try to update an existing config first; fall back to creating a new one.
|
# Try to update an existing config first;
|
||||||
# We skip hasConfigId() as it may return False despite the config existing
|
# fall back to creating a new one.
|
||||||
|
# We skip hasConfigId() as it may return False
|
||||||
|
# despite the config existing
|
||||||
# (in-memory cache may not be populated yet in QGIS 4).
|
# (in-memory cache may not be populated yet in QGIS 4).
|
||||||
ok_load, existing_cfg = self._load_config(existing_id, full=False) if existing_id else (False, None)
|
ok_load, existing_cfg = (
|
||||||
|
self._load_config(existing_id, full=False)
|
||||||
|
if existing_id
|
||||||
|
else (False, None)
|
||||||
|
)
|
||||||
if ok_load:
|
if ok_load:
|
||||||
cfg.setId(existing_id)
|
cfg.setId(existing_id)
|
||||||
ok, cfg = auth_mgr.updateAuthenticationConfig(cfg)
|
ok = auth_mgr.updateAuthenticationConfig(cfg)
|
||||||
else:
|
else:
|
||||||
ok, cfg = auth_mgr.storeAuthenticationConfig(cfg)
|
ok, cfg = auth_mgr.storeAuthenticationConfig(cfg)
|
||||||
|
|
||||||
@@ -536,7 +651,8 @@ class LoginDialog(QDialog):
|
|||||||
if not ok or not config_id:
|
if not ok or not config_id:
|
||||||
QMessageBox.critical(
|
QMessageBox.critical(
|
||||||
self, "Chyba uložení",
|
self, "Chyba uložení",
|
||||||
"Přihlašovací údaje se nepodařilo uložit do správce autentizace QGIS.\n"
|
"Přihlašovací údaje se nepodařilo "
|
||||||
|
"uložit do správce autentizace QGIS.\n"
|
||||||
"Zkuste restartovat QGIS a přihlásit se znovu."
|
"Zkuste restartovat QGIS a přihlásit se znovu."
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
@@ -550,7 +666,11 @@ class LoginDialog(QDialog):
|
|||||||
if existing_id:
|
if existing_id:
|
||||||
QgsApplication.authManager().removeAuthenticationConfig(existing_id)
|
QgsApplication.authManager().removeAuthenticationConfig(existing_id)
|
||||||
settings.remove(self.SETTINGS_KEY)
|
settings.remove(self.SETTINGS_KEY)
|
||||||
QMessageBox.information(self, "Hotovo", "Uložené přihlašovací údaje byly odebrány.")
|
QMessageBox.information(
|
||||||
|
self,
|
||||||
|
"Hotovo",
|
||||||
|
"Uložené přihlašovací údaje byly odebrány."
|
||||||
|
)
|
||||||
self.reject()
|
self.reject()
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@@ -565,8 +685,9 @@ class LoginDialog(QDialog):
|
|||||||
|
|
||||||
Note: hasConfigId() is intentionally skipped here – it checks an
|
Note: hasConfigId() is intentionally skipped here – it checks an
|
||||||
in-memory cache that may lag behind the actual database contents,
|
in-memory cache that may lag behind the actual database contents,
|
||||||
causing false negatives (see class docstring). loadAuthenticationConfig()
|
causing false negatives (see class docstring).
|
||||||
is called directly and its return value is used as the authoritative result.
|
loadAuthenticationConfig() is called directly and its return value is
|
||||||
|
used as the authoritative result.
|
||||||
"""
|
"""
|
||||||
settings = QSettings()
|
settings = QSettings()
|
||||||
config_id = settings.value(LoginDialog.SETTINGS_KEY, "")
|
config_id = settings.value(LoginDialog.SETTINGS_KEY, "")
|
||||||
@@ -578,4 +699,4 @@ class LoginDialog(QDialog):
|
|||||||
if not ok:
|
if not ok:
|
||||||
return "", ""
|
return "", ""
|
||||||
|
|
||||||
return cfg.config("username", ""), cfg.config("password", "") # nosec B106
|
return cfg.config("username", ""), cfg.config("password", "") # nosec B106
|
||||||
|
|||||||
+351
-154
@@ -1,7 +1,8 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from qgis.core import (QgsProject, QgsVectorLayer, QgsFeature, QgsGeometry,
|
from qgis.core import (QgsProject, QgsVectorLayer, QgsFeature, QgsGeometry,
|
||||||
QgsField, QgsCoordinateReferenceSystem, QgsCoordinateTransform,
|
QgsField, QgsCoordinateReferenceSystem,
|
||||||
QgsWkbTypes, Qgis, QgsApplication, QgsAuthMethodConfig, QgsMessageLog)
|
QgsCoordinateTransform, QgsWkbTypes, Qgis,
|
||||||
|
QgsMessageLog)
|
||||||
from qgis.utils import iface
|
from qgis.utils import iface
|
||||||
from qgis.PyQt.QtCore import Qt, QMetaType
|
from qgis.PyQt.QtCore import Qt, QMetaType
|
||||||
from qgis.PyQt.QtWidgets import QApplication
|
from qgis.PyQt.QtWidgets import QApplication
|
||||||
@@ -12,25 +13,33 @@ import json
|
|||||||
# Global cache to store translated terms from the Digital Archive
|
# Global cache to store translated terms from the Digital Archive
|
||||||
TRANSLATIONS = {}
|
TRANSLATIONS = {}
|
||||||
|
|
||||||
# Session s autentizační cookie po přihlášení; None = nepřihlášen (anonymní přístup)
|
# Session with authentication cookie after login;
|
||||||
|
# None = not logged in (anonymous access)
|
||||||
AMCR_SESSION: requests.Session | None = None
|
AMCR_SESSION: requests.Session | None = None
|
||||||
|
|
||||||
|
|
||||||
def _log(msg: str, level=Qgis.MessageLevel.Info):
|
def _log(msg: str, level=Qgis.MessageLevel.Info):
|
||||||
"""Shortcut: zapíše zprávu do QGIS logu (panel Zprávy → záložka AMČR)."""
|
"""
|
||||||
|
Shortcut: writes a message to the QGIS log
|
||||||
|
(Messages panel → AMČR tab).
|
||||||
|
"""
|
||||||
QgsMessageLog.logMessage(msg, "AMČR login", level)
|
QgsMessageLog.logMessage(msg, "AMČR login", level)
|
||||||
|
|
||||||
|
|
||||||
def login_to_api(username: str, password: str):
|
def login_to_api(username: str, password: str):
|
||||||
"""
|
"""
|
||||||
Přihlásí se do Digiarchiv API pomocí username a hesla.
|
Logs in to the Digiarchiv API using a username and password.
|
||||||
Vrátí requests.Session s nastavenou session cookie, nebo None při chybě.
|
Returns a requests.Session with the session cookie set, or None on error.
|
||||||
"""
|
"""
|
||||||
login_url = "https://digiarchiv.aiscr.cz/api/user/login"
|
login_url = "https://digiarchiv.aiscr.cz/api/user/login"
|
||||||
|
|
||||||
_log(f"Přihlašuji uživatele: '{username}'")
|
_log(f"Přihlašuji uživatele: '{username}'")
|
||||||
|
|
||||||
if not username or not password:
|
if not username or not password:
|
||||||
_log("CHYBA: username nebo heslo je prázdné.", Qgis.MessageLevel.Critical)
|
_log(
|
||||||
|
"CHYBA: username nebo heslo je prázdné.",
|
||||||
|
Qgis.MessageLevel.Critical
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
session = requests.Session()
|
session = requests.Session()
|
||||||
@@ -42,14 +51,22 @@ def login_to_api(username: str, password: str):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
_log(f"Odesílám POST na {login_url} ...")
|
_log(f"Odesílám POST na {login_url} ...")
|
||||||
response = session.post(login_url, json={"user": username, "pwd": password}, timeout=10)
|
response = session.post(
|
||||||
|
login_url,
|
||||||
|
json={"user": username, "pwd": password},
|
||||||
|
timeout=10
|
||||||
|
)
|
||||||
_log(f"HTTP status: {response.status_code}")
|
_log(f"HTTP status: {response.status_code}")
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
|
||||||
# API vrací chyby se status kódem 200 – je nutné zkontrolovat tělo odpovědi
|
# The API returns errors with status code 200 –
|
||||||
|
# the response body must be checked
|
||||||
body = response.json()
|
body = response.json()
|
||||||
if "error" in body:
|
if "error" in body:
|
||||||
_log(f"CHYBA přihlášení (API): {body['error']}", Qgis.MessageLevel.Critical)
|
_log(
|
||||||
|
f"CHYBA přihlášení (API): {body['error']}",
|
||||||
|
Qgis.MessageLevel.Critical
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
_log("Přihlášení proběhlo úspěšně.")
|
_log("Přihlášení proběhlo úspěšně.")
|
||||||
@@ -59,23 +76,25 @@ def login_to_api(username: str, password: str):
|
|||||||
|
|
||||||
except requests.exceptions.HTTPError as e:
|
except requests.exceptions.HTTPError as e:
|
||||||
_log(f"CHYBA HTTP {e.response.status_code if e.response else '?'}: "
|
_log(f"CHYBA HTTP {e.response.status_code if e.response else '?'}: "
|
||||||
f"{e.response.text[:300] if e.response else 'žádná odpověď'}", Qgis.MessageLevel.Critical)
|
f"{e.response.text[:300] if e.response else 'žádná odpověď'}",
|
||||||
|
Qgis.MessageLevel.Critical)
|
||||||
return None
|
return None
|
||||||
except requests.exceptions.RequestException as e:
|
except requests.exceptions.RequestException as e:
|
||||||
_log(f"CHYBA sítě: {e}", Qgis.MessageLevel.Critical)
|
_log(f"CHYBA sítě: {e}", Qgis.MessageLevel.Critical)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _get_session() -> requests.Session | None:
|
def _get_session() -> requests.Session | None:
|
||||||
"""
|
"""
|
||||||
Vrátí aktivní session. Pokud žádná není (restart QGIS), pokusí se
|
Returns the active session. If none exists (e.g. after a QGIS restart),
|
||||||
automaticky přihlásit pomocí uložených přihlašovacích údajů.
|
attempts automatic login using stored credentials.
|
||||||
Vrátí None pokud přihlašovací údaje nejsou uloženy.
|
Returns None if no credentials are stored.
|
||||||
"""
|
"""
|
||||||
global AMCR_SESSION
|
global AMCR_SESSION
|
||||||
if AMCR_SESSION is not None:
|
if AMCR_SESSION is not None:
|
||||||
return AMCR_SESSION
|
return AMCR_SESSION
|
||||||
|
|
||||||
# Zkusit auto-login pomocí uložených údajů
|
# Attempt auto-login using stored credentials
|
||||||
from .amcr_dialog import LoginDialog
|
from .amcr_dialog import LoginDialog
|
||||||
username, password = LoginDialog.get_credentials()
|
username, password = LoginDialog.get_credentials()
|
||||||
if username and password:
|
if username and password:
|
||||||
@@ -87,19 +106,24 @@ def _get_session() -> requests.Session | None:
|
|||||||
|
|
||||||
def _api_get(url, params, timeout=30) -> requests.Response:
|
def _api_get(url, params, timeout=30) -> requests.Response:
|
||||||
"""
|
"""
|
||||||
Provede GET request. Pokud API signalizuje vypršení přihlášení,
|
Performs a GET request. If the API signals an expired login,
|
||||||
provede jedno opakované přihlášení a zkusí znovu.
|
re-authenticates once and retries.
|
||||||
"""
|
"""
|
||||||
global AMCR_SESSION
|
global AMCR_SESSION
|
||||||
|
|
||||||
def _is_auth_error(resp: requests.Response) -> bool:
|
def _is_auth_error(resp: requests.Response) -> bool:
|
||||||
"""API vrací auth chyby se status 200 – je nutné zkontrolovat tělo."""
|
"""The API returns auth errors with status 200 –
|
||||||
|
the body must be checked."""
|
||||||
if resp.status_code == 401:
|
if resp.status_code == 401:
|
||||||
return True
|
return True
|
||||||
try:
|
try:
|
||||||
body = resp.json()
|
body = resp.json()
|
||||||
err = str(body.get("error", "")).lower()
|
err = str(body.get("error", "")).lower()
|
||||||
return "unauthorized" in err or "not logged" in err or "session" in err
|
return (
|
||||||
|
"unauthorized" in err
|
||||||
|
or "not logged" in err
|
||||||
|
or "session" in err
|
||||||
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -107,8 +131,9 @@ def _api_get(url, params, timeout=30) -> requests.Response:
|
|||||||
resp = (session or requests).get(url, params=params, timeout=timeout)
|
resp = (session or requests).get(url, params=params, timeout=timeout)
|
||||||
|
|
||||||
if _is_auth_error(resp):
|
if _is_auth_error(resp):
|
||||||
_log("Session vypršela během stahování – obnovuji přihlášení...", Qgis.MessageLevel.Warning)
|
_log("Session vypršela během stahování – obnovuji přihlášení...",
|
||||||
AMCR_SESSION = None # Zrušit starou session
|
Qgis.MessageLevel.Warning)
|
||||||
|
AMCR_SESSION = None # Invalidate the old session
|
||||||
from .amcr_dialog import LoginDialog
|
from .amcr_dialog import LoginDialog
|
||||||
username, password = LoginDialog.get_credentials()
|
username, password = LoginDialog.get_credentials()
|
||||||
if username and password:
|
if username and password:
|
||||||
@@ -116,18 +141,24 @@ def _api_get(url, params, timeout=30) -> requests.Response:
|
|||||||
if AMCR_SESSION:
|
if AMCR_SESSION:
|
||||||
resp = AMCR_SESSION.get(url, params=params, timeout=timeout)
|
resp = AMCR_SESSION.get(url, params=params, timeout=timeout)
|
||||||
else:
|
else:
|
||||||
_log("Opakované přihlášení selhalo.", Qgis.MessageLevel.Critical)
|
_log("Opakované přihlášení selhalo.",
|
||||||
|
Qgis.MessageLevel.Critical)
|
||||||
else:
|
else:
|
||||||
_log("Přihlašovací údaje nejsou uloženy – pokračuji anonymně.", Qgis.MessageLevel.Warning)
|
_log("Přihlašovací údaje nejsou uloženy – pokračuji anonymně.",
|
||||||
|
Qgis.MessageLevel.Warning)
|
||||||
|
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
|
||||||
def load_translations():
|
def load_translations():
|
||||||
"""Fetches the official Czech translation dictionary from the AISCR API."""
|
"""
|
||||||
|
Fetches the official Czech translation dictionary
|
||||||
|
from the Digiarchive API.
|
||||||
|
"""
|
||||||
global TRANSLATIONS
|
global TRANSLATIONS
|
||||||
if TRANSLATIONS:
|
if TRANSLATIONS:
|
||||||
return
|
return
|
||||||
|
|
||||||
url = "https://digiarchiv.aiscr.cz/api/assets/i18n/cs.json"
|
url = "https://digiarchiv.aiscr.cz/api/assets/i18n/cs.json"
|
||||||
try:
|
try:
|
||||||
r = requests.get(url, timeout=10)
|
r = requests.get(url, timeout=10)
|
||||||
@@ -136,20 +167,31 @@ def load_translations():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error downloading vocabulary: {e}")
|
print(f"Error downloading vocabulary: {e}")
|
||||||
|
|
||||||
|
|
||||||
def tr_code(code):
|
def tr_code(code):
|
||||||
"""Translates a technical code into a human-readable string using the global cache."""
|
"""
|
||||||
if not code:
|
Translates a technical code into a human-readable string
|
||||||
|
using the global cache.
|
||||||
|
"""
|
||||||
|
if not code:
|
||||||
return ""
|
return ""
|
||||||
return TRANSLATIONS.get(code, code)
|
return TRANSLATIONS.get(code, code)
|
||||||
|
|
||||||
|
|
||||||
def komp_projde_filtrem(komp, filter_areal, filter_datace, filters):
|
def komp_projde_filtrem(komp, filter_areal, filter_datace, filters):
|
||||||
if filter_areal and komp.get('komponenta_areal', {}).get('id', "") not in filters.get('f_areal', []):
|
areal_id = komp.get('komponenta_areal', {}).get('id', "")
|
||||||
|
if filter_areal and areal_id not in filters.get('f_areal', []):
|
||||||
return False
|
return False
|
||||||
if filter_datace and komp.get('komponenta_obdobi', {}).get('id', "") not in filters.get('f_obdobi', []):
|
|
||||||
|
obdobi_id = komp.get('komponenta_obdobi', {}).get('id', "")
|
||||||
|
if filter_datace and obdobi_id not in filters.get('f_obdobi', []):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false"):
|
|
||||||
|
def load_amcr_data(canvas, bb, filters=None,
|
||||||
|
typ_dat="akce", komponenty="false"):
|
||||||
"""
|
"""
|
||||||
Main processing function:
|
Main processing function:
|
||||||
1. Determines search area (Bounding Box)
|
1. Determines search area (Bounding Box)
|
||||||
@@ -159,26 +201,35 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
|
|||||||
load_translations()
|
load_translations()
|
||||||
|
|
||||||
# --- 1. COORDINATE TRANSFORMATION ---
|
# --- 1. COORDINATE TRANSFORMATION ---
|
||||||
# Get current map extent and transform it from project CRS (usually S-JTSK) to WGS-84 for the API
|
# Get current map extent and transform it
|
||||||
|
# from project CRS (usually S-JTSK) to WGS-84 for the API
|
||||||
extent = canvas.extent()
|
extent = canvas.extent()
|
||||||
crs_src = canvas.mapSettings().destinationCrs()
|
crs_src = canvas.mapSettings().destinationCrs()
|
||||||
crs_dest = QgsCoordinateReferenceSystem("EPSG:4326")
|
crs_dest = QgsCoordinateReferenceSystem("EPSG:4326")
|
||||||
xform = QgsCoordinateTransform(crs_src, crs_dest, QgsProject.instance())
|
xform = QgsCoordinateTransform(crs_src, crs_dest, QgsProject.instance())
|
||||||
extent_wgs = xform.transformBoundingBox(extent)
|
extent_wgs = xform.transformBoundingBox(extent)
|
||||||
|
|
||||||
# Format the bounding box string as required by the API: minLat,minLon,maxLat,maxLon
|
# Format the bounding box string as required by the API:
|
||||||
bbox_str = f"{extent_wgs.yMinimum()},{extent_wgs.xMinimum()},{extent_wgs.yMaximum()},{extent_wgs.xMaximum()}"
|
# minLat,minLon,maxLat,maxLon
|
||||||
|
bbox_str = (
|
||||||
|
f"{extent_wgs.yMinimum()},{extent_wgs.xMinimum()},"
|
||||||
|
f"{extent_wgs.yMaximum()},{extent_wgs.xMaximum()}"
|
||||||
|
)
|
||||||
|
|
||||||
url = "https://digiarchiv.aiscr.cz/api/search/query"
|
url = "https://digiarchiv.aiscr.cz/api/search/query"
|
||||||
|
|
||||||
iface.messageBar().pushMessage("AMCR", "Hledám záznamy...", level=Qgis.MessageLevel.Info)
|
iface.messageBar().pushMessage(
|
||||||
|
"AMCR",
|
||||||
|
"Hledám záznamy...",
|
||||||
|
level=Qgis.MessageLevel.Info
|
||||||
|
)
|
||||||
QApplication.setOverrideCursor(QCursor(Qt.CursorShape.WaitCursor))
|
QApplication.setOverrideCursor(QCursor(Qt.CursorShape.WaitCursor))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# A) METADATA FETCHING (Fieldwork/Site)
|
# A) METADATA FETCHING (Fieldwork/Site)
|
||||||
# ==========================================
|
# ==========================================
|
||||||
|
|
||||||
base_params = {
|
base_params = {
|
||||||
"mapa": "true",
|
"mapa": "true",
|
||||||
"sort": "ident_cely asc",
|
"sort": "ident_cely asc",
|
||||||
@@ -186,10 +237,11 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Restrict search to map window if requested
|
# Restrict search to map window if requested
|
||||||
if bb == "true":
|
if bb == "true":
|
||||||
base_params["loc_rpt"] = bbox_str
|
base_params["loc_rpt"] = bbox_str
|
||||||
|
|
||||||
# Apply multi-select filters from the dialog using the ':or' syntax required by the API
|
# Apply multi-select filters from the dialog using
|
||||||
|
# the ':or' syntax required by the API
|
||||||
if filters:
|
if filters:
|
||||||
for key, value in filters.items():
|
for key, value in filters.items():
|
||||||
if not value:
|
if not value:
|
||||||
@@ -200,21 +252,24 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
|
|||||||
base_params[key] = str(value).strip()
|
base_params[key] = str(value).strip()
|
||||||
|
|
||||||
docs = []
|
docs = []
|
||||||
current_page = 0
|
current_page = 0
|
||||||
BATCH_DOCS = 500 # Records per API request
|
BATCH_DOCS = 500 # Records per API request
|
||||||
MAX_LIMIT = 20000 # Safety limit to prevent QGIS from freezing
|
MAX_LIMIT = 20000 # Safety limit to prevent QGIS from freezing
|
||||||
|
|
||||||
|
|
||||||
seen_ids = set()
|
seen_ids = set()
|
||||||
target_pian_ids_count = 0
|
target_pian_ids_count = 0
|
||||||
|
|
||||||
# Check if we should skip negative results based on filter
|
# Check if we should skip negative results based on filter
|
||||||
skip_negativni = filters.get('posevidence') == 'true' if filters else False
|
skip_negativni = (
|
||||||
|
filters.get('posevidence') == 'true'
|
||||||
|
if filters
|
||||||
|
else False
|
||||||
|
)
|
||||||
|
|
||||||
# Check whether we should filter results based on component filters
|
# Check whether we should filter results based on component filters
|
||||||
filter_areal = "f_areal" in filters if filters else False
|
filter_areal = "f_areal" in filters if filters else False
|
||||||
filter_datace = "f_obdobi" in filters if filters else False
|
filter_datace = "f_obdobi" in filters if filters else False
|
||||||
|
|
||||||
# --- API PAGINATION LOOP ---
|
# --- API PAGINATION LOOP ---
|
||||||
while True:
|
while True:
|
||||||
base_params['rows'] = BATCH_DOCS
|
base_params['rows'] = BATCH_DOCS
|
||||||
@@ -222,17 +277,17 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
|
|||||||
base_params['page'] = current_page
|
base_params['page'] = current_page
|
||||||
elif 'page' in base_params:
|
elif 'page' in base_params:
|
||||||
del base_params['page']
|
del base_params['page']
|
||||||
|
|
||||||
try:
|
try:
|
||||||
resp_docs = _api_get(url, params=base_params, timeout=30)
|
resp_docs = _api_get(url, params=base_params, timeout=30)
|
||||||
resp_json = resp_docs.json()
|
resp_json = resp_docs.json()
|
||||||
data = resp_json.get('response', {})
|
data = resp_json.get('response', {})
|
||||||
batch_docs = data.get('docs', [])
|
batch_docs = data.get('docs', [])
|
||||||
num_found = data.get('numFound', 0)
|
num_found = data.get('numFound', 0)
|
||||||
|
|
||||||
if not batch_docs:
|
if not batch_docs:
|
||||||
break
|
break
|
||||||
|
|
||||||
# Filter out duplicates and append to main list
|
# Filter out duplicates and append to main list
|
||||||
new_docs = []
|
new_docs = []
|
||||||
for d in batch_docs:
|
for d in batch_docs:
|
||||||
@@ -240,44 +295,56 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
|
|||||||
if ident and ident not in seen_ids:
|
if ident and ident not in seen_ids:
|
||||||
seen_ids.add(ident)
|
seen_ids.add(ident)
|
||||||
new_docs.append(d)
|
new_docs.append(d)
|
||||||
|
|
||||||
docs.extend(new_docs)
|
docs.extend(new_docs)
|
||||||
print(f"Strana {current_page} stažena. Celkem záznamů: {len(docs)} / {num_found}")
|
print(
|
||||||
|
f"Strana {current_page} stažena. "
|
||||||
|
f"Celkem záznamů: {len(docs)} / {num_found}"
|
||||||
|
)
|
||||||
|
|
||||||
if len(docs) >= num_found:
|
if len(docs) >= num_found:
|
||||||
break
|
break
|
||||||
if len(docs) >= MAX_LIMIT:
|
if len(docs) >= MAX_LIMIT:
|
||||||
iface.messageBar().pushMessage("AMCR", f"Limit {MAX_LIMIT} záznamů dosažen.", level=Qgis.MessageLevel.Warning)
|
iface.messageBar().pushMessage(
|
||||||
|
"AMCR",
|
||||||
|
f"Limit {MAX_LIMIT} záznamů dosažen.",
|
||||||
|
level=Qgis.MessageLevel.Warning
|
||||||
|
)
|
||||||
break
|
break
|
||||||
|
|
||||||
current_page += 1
|
current_page += 1
|
||||||
QApplication.processEvents() # Keep UI responsive
|
QApplication.processEvents() # Keep UI responsive
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Chyba při stránkování na straně {current_page}: {e}")
|
print(f"Chyba při stránkování na straně {current_page}: {e}")
|
||||||
break
|
break
|
||||||
|
|
||||||
if not docs:
|
if not docs:
|
||||||
iface.messageBar().pushMessage("AMCR", "Žádné záznamy nenalezeny.", level=Qgis.MessageLevel.Warning)
|
iface.messageBar().pushMessage(
|
||||||
return
|
"AMCR",
|
||||||
|
"Žádné záznamy nenalezeny.",
|
||||||
|
level=Qgis.MessageLevel.Warning
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# B) ATTRIBUTE PARSING
|
# B) ATTRIBUTE PARSING
|
||||||
# ==========================================
|
# ==========================================
|
||||||
|
|
||||||
# pian_lookup maps a Geometry ID (PIAN) to a list of its associated metadata
|
# pian_lookup maps a Geometry ID (PIAN)
|
||||||
|
# to a list of its associated metadata
|
||||||
pian_lookup = {}
|
pian_lookup = {}
|
||||||
target_pian_ids = set()
|
target_pian_ids = set()
|
||||||
actions_with_geom = 0
|
actions_with_geom = 0
|
||||||
|
|
||||||
# Helper function for safe single-value extraction
|
# Helper: safely extract a single value
|
||||||
def g(doc, key, default=""):
|
def g(doc, key, default=""):
|
||||||
val = doc.get(key)
|
val = doc.get(key)
|
||||||
if isinstance(val, list):
|
if isinstance(val, list):
|
||||||
return str(val[0]) if val else default
|
return str(val[0]) if val else default
|
||||||
return str(val) if val is not None else default
|
return str(val) if val is not None else default
|
||||||
|
|
||||||
# Helper function for safe list-value extraction and joining
|
# Helper: safely extract and join a list of values
|
||||||
def g_list(doc, key, translate=False):
|
def g_list(doc, key, translate=False):
|
||||||
val = doc.get(key, [])
|
val = doc.get(key, [])
|
||||||
if not isinstance(val, list):
|
if not isinstance(val, list):
|
||||||
@@ -293,16 +360,23 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
actions_with_geom += 1
|
actions_with_geom += 1
|
||||||
|
|
||||||
# Extract protected data (fields not available in public Solr index)
|
# Extract protected fields
|
||||||
az_chranene = doc.get('az_chranene_udaje', {})
|
az_chranene = doc.get('az_chranene_udaje', {})
|
||||||
chranene = doc.get('akce_chranene_udaje') or doc.get('lokalita_chranene_udaje') or {}
|
chranene = (
|
||||||
|
doc.get('akce_chranene_udaje')
|
||||||
# Format additional cadastral areas from dictionaries
|
or doc.get('lokalita_chranene_udaje')
|
||||||
|
or {}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Format additional cadastral areas from nested dicts
|
||||||
dalsi_kat = az_chranene.get('dalsi_katastr', [])
|
dalsi_kat = az_chranene.get('dalsi_katastr', [])
|
||||||
dalsi_kat_str = ""
|
dalsi_kat_str = ""
|
||||||
if isinstance(dalsi_kat, list):
|
if isinstance(dalsi_kat, list):
|
||||||
items = [x.get('value', '') if isinstance(x, dict) else str(x) for x in dalsi_kat]
|
items = [
|
||||||
|
x.get('value', '') if isinstance(x, dict) else str(x)
|
||||||
|
for x in dalsi_kat
|
||||||
|
]
|
||||||
dalsi_kat_str = ", ".join([i for i in items if i])
|
dalsi_kat_str = ", ".join([i for i in items if i])
|
||||||
|
|
||||||
lokalizace = chranene.get('lokalizace_okolnosti', "")
|
lokalizace = chranene.get('lokalizace_okolnosti', "")
|
||||||
@@ -314,7 +388,7 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
|
|||||||
"ident_cely": doc.get('ident_cely', ''),
|
"ident_cely": doc.get('ident_cely', ''),
|
||||||
"az_okres": g(doc, 'az_okres'),
|
"az_okres": g(doc, 'az_okres'),
|
||||||
"katastr": g_list(doc, 'katastr'),
|
"katastr": g_list(doc, 'katastr'),
|
||||||
"dalsi_katastr": dalsi_kat_str,
|
"dalsi_katastr": dalsi_kat_str,
|
||||||
"pristupnost": g(doc, 'pristupnost'),
|
"pristupnost": g(doc, 'pristupnost'),
|
||||||
"loc": g_list(doc, 'loc')
|
"loc": g_list(doc, 'loc')
|
||||||
}
|
}
|
||||||
@@ -322,53 +396,102 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
|
|||||||
# Add entity-specific metadata
|
# Add entity-specific metadata
|
||||||
if typ_dat == "akce":
|
if typ_dat == "akce":
|
||||||
meta.update({
|
meta.update({
|
||||||
"akce_hlavni_vedouci": g(doc, 'akce_hlavni_vedouci'),
|
"akce_hlavni_vedouci": g(
|
||||||
"akce_organizace": tr_code(g(doc, 'akce_organizace')),
|
doc,
|
||||||
"akce_specifikace_data": tr_code(g(doc, 'akce_specifikace_data')),
|
'akce_hlavni_vedouci'
|
||||||
"akce_datum_zahajeni": g(doc, 'akce_datum_zahajeni'),
|
),
|
||||||
"akce_datum_ukonceni": g(doc, 'akce_datum_ukonceni'),
|
"akce_organizace": tr_code(g(
|
||||||
"akce_hlavni_typ": tr_code(g(doc, 'akce_hlavni_typ')),
|
doc,
|
||||||
"akce_vedlejsi_typ": g_list(doc, 'akce_vedlejsi_typ', translate=True),
|
'akce_organizace'
|
||||||
"lokalizace_okolnosti": str(lokalizace) if lokalizace else "",
|
)),
|
||||||
"akce_je_nz": "Ano" if doc.get('akce_je_nz') is True else "Ne",
|
"akce_specifikace_data": tr_code(g(
|
||||||
|
doc,
|
||||||
|
'akce_specifikace_data'
|
||||||
|
)),
|
||||||
|
"akce_datum_zahajeni": g(
|
||||||
|
doc,
|
||||||
|
'akce_datum_zahajeni'
|
||||||
|
),
|
||||||
|
"akce_datum_ukonceni": g(
|
||||||
|
doc,
|
||||||
|
'akce_datum_ukonceni'
|
||||||
|
),
|
||||||
|
"akce_hlavni_typ": tr_code(g(
|
||||||
|
doc,
|
||||||
|
'akce_hlavni_typ'
|
||||||
|
)),
|
||||||
|
"akce_vedlejsi_typ": g_list(
|
||||||
|
doc,
|
||||||
|
'akce_vedlejsi_typ',
|
||||||
|
translate=True
|
||||||
|
),
|
||||||
|
"lokalizace_okolnosti": (
|
||||||
|
str(lokalizace)
|
||||||
|
if lokalizace
|
||||||
|
else ""
|
||||||
|
),
|
||||||
|
"akce_je_nz": (
|
||||||
|
"Ano"
|
||||||
|
if doc.get('akce_je_nz') is True
|
||||||
|
else "Ne"
|
||||||
|
),
|
||||||
})
|
})
|
||||||
|
|
||||||
elif typ_dat == "lokalita":
|
elif typ_dat == "lokalita":
|
||||||
meta.update({
|
meta.update({
|
||||||
"lokalita_nazev": lokalita_nazev,
|
"lokalita_nazev": lokalita_nazev,
|
||||||
"lokalita_popis": lokalita_popis,
|
"lokalita_popis": lokalita_popis,
|
||||||
"lokalita_zachovalost": tr_code(g(doc, 'lokalita_zachovalost')),
|
"lokalita_zachovalost": tr_code(g(
|
||||||
"lokalita_druh": tr_code(g(doc, 'lokalita_druh')),
|
doc,
|
||||||
"lokalita_typ": tr_code(g(doc, 'lokalita_typ_lokality')),
|
'lokalita_zachovalost'
|
||||||
|
)),
|
||||||
|
"lokalita_druh": tr_code(g(
|
||||||
|
doc,
|
||||||
|
'lokalita_druh'
|
||||||
|
)),
|
||||||
|
"lokalita_typ": tr_code(g(
|
||||||
|
doc,
|
||||||
|
'lokalita_typ_lokality'
|
||||||
|
)),
|
||||||
})
|
})
|
||||||
|
|
||||||
# Documentation units (DJ) within the record
|
# Documentation units (DJ) within the record
|
||||||
djs = doc.get('az_dokumentacni_jednotka', [])
|
djs = doc.get('az_dokumentacni_jednotka', [])
|
||||||
|
|
||||||
for dj in djs:
|
for dj in djs:
|
||||||
# Filter out negative evidence units if requested
|
# Skip negative evidence units if requested
|
||||||
if skip_negativni and dj.get('dj_negativni_jednotka') is True:
|
if skip_negativni and dj.get('dj_negativni_jednotka') is True:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
komps = dj.get('dj_komponenta', [])
|
komps = dj.get('dj_komponenta', [])
|
||||||
|
|
||||||
if filter_areal or filter_datace:
|
if filter_areal or filter_datace:
|
||||||
if not komps:
|
if not komps:
|
||||||
continue
|
continue
|
||||||
if not any(komp_projde_filtrem(komp, filter_areal, filter_datace, filters) for komp in komps):
|
if not any(
|
||||||
|
komp_projde_filtrem(
|
||||||
|
komp, filter_areal,
|
||||||
|
filter_datace, filters
|
||||||
|
)
|
||||||
|
for komp in komps
|
||||||
|
):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
dj_id = dj.get('ident_cely')
|
dj_id = dj.get('ident_cely')
|
||||||
dj_typ = dj.get('dj_typ')
|
dj_typ = dj.get('dj_typ')
|
||||||
|
|
||||||
# Merge general meta with documentation unit specific data
|
# Merge shared metadata with documentation unit-specific fields
|
||||||
dj_meta = {
|
dj_meta = {
|
||||||
**meta,
|
**meta,
|
||||||
'dj_id': dj_id,
|
'dj_id': dj_id,
|
||||||
'dj_typ_value': dj_typ.get('value') if dj_typ else "",
|
'dj_typ_value': dj_typ.get('value') if dj_typ else "",
|
||||||
'dj_negativni': "Negativní" if dj.get('dj_negativni_jednotka') is True else "Pozitivní"
|
'dj_negativni': (
|
||||||
|
"Negativní"
|
||||||
|
if dj.get('dj_negativni_jednotka') is True
|
||||||
|
else "Pozitivní"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
# Link Documentation Unit to Geometry (PIAN)
|
# Link Documentation Unit to Geometry (PIAN)
|
||||||
dj_pian = dj.get('dj_pian')
|
dj_pian = dj.get('dj_pian')
|
||||||
if dj_pian:
|
if dj_pian:
|
||||||
@@ -379,25 +502,39 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
|
|||||||
pian_lookup[dj_pian_value] = []
|
pian_lookup[dj_pian_value] = []
|
||||||
|
|
||||||
if komponenty == "true":
|
if komponenty == "true":
|
||||||
# One feature per component — all data on a single row, no relations needed
|
# One feature per component –
|
||||||
|
# all data on a single row, no relations needed
|
||||||
if komps:
|
if komps:
|
||||||
for komp in komps:
|
for komp in komps:
|
||||||
if not komp_projde_filtrem(komp, filter_areal, filter_datace, filters):
|
if not komp_projde_filtrem(
|
||||||
|
komp, filter_areal,
|
||||||
|
filter_datace, filters
|
||||||
|
):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
komp_meta = {
|
komp_meta = {
|
||||||
**dj_meta,
|
**dj_meta,
|
||||||
'komponenta_id': komp.get('ident_cely', ""),
|
'komponenta_id': komp.get(
|
||||||
'komponenta_areal': komp.get('komponenta_areal', {}).get('value', ""),
|
'ident_cely',
|
||||||
'komponenta_obdobi': komp.get('komponenta_obdobi', {}).get('value', ""),
|
""
|
||||||
|
),
|
||||||
|
'komponenta_areal': komp.get(
|
||||||
|
'komponenta_areal',
|
||||||
|
{}
|
||||||
|
).get('value', ""),
|
||||||
|
'komponenta_obdobi': komp.get(
|
||||||
|
'komponenta_obdobi',
|
||||||
|
{}
|
||||||
|
).get('value', ""),
|
||||||
}
|
}
|
||||||
pian_lookup[dj_pian_value].append(komp_meta)
|
pian_lookup[dj_pian_value].append(komp_meta)
|
||||||
target_pian_ids_count += 1
|
target_pian_ids_count += 1
|
||||||
else:
|
else:
|
||||||
# DJ without components — still include with empty component fields
|
# DJ without components — still include
|
||||||
|
# with empty component fields
|
||||||
if filter_areal or filter_datace:
|
if filter_areal or filter_datace:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
empty_meta = {
|
empty_meta = {
|
||||||
**dj_meta,
|
**dj_meta,
|
||||||
'komponenta_id': "",
|
'komponenta_id': "",
|
||||||
@@ -410,11 +547,13 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
|
|||||||
target_pian_ids_count += 1
|
target_pian_ids_count += 1
|
||||||
pian_lookup[dj_pian_value].append(dj_meta)
|
pian_lookup[dj_pian_value].append(dj_meta)
|
||||||
|
|
||||||
|
|
||||||
if not target_pian_ids:
|
if not target_pian_ids:
|
||||||
iface.messageBar().pushMessage("AMCR", f"Nalezeno {len(docs)} záznamů, ale žádný nemá geometrii.", level=Qgis.MessageLevel.Warning)
|
iface.messageBar().pushMessage(
|
||||||
return
|
"AMCR",
|
||||||
|
f"Nalezeno {len(docs)} záznamů, ale žádný nemá geometrii.",
|
||||||
|
level=Qgis.MessageLevel.Warning
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# C) GEOMETRY FETCHING (PIAN)
|
# C) GEOMETRY FETCHING (PIAN)
|
||||||
@@ -422,17 +561,28 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
|
|||||||
ids_list = list(target_pian_ids)
|
ids_list = list(target_pian_ids)
|
||||||
total_pians = len(ids_list)
|
total_pians = len(ids_list)
|
||||||
docs_pian = []
|
docs_pian = []
|
||||||
BATCH_PIAN = 200 # Geometry requests are batch-processed to stay under URL length limits
|
# Geometry requests are batch-processed
|
||||||
|
# to stay under URL length limits:
|
||||||
iface.messageBar().pushMessage("AMCR", f"Záznamů: {len(docs)} (z toho {actions_with_geom} s mapou). Stahuji {total_pians} unikátních geometrií, vykresluji {target_pian_ids_count} geometrií...", level=Qgis.MessageLevel.Info)
|
BATCH_PIAN = 200
|
||||||
|
|
||||||
fl_pian = ["ident_cely", "pian_typ", "pian_chranene_udaje", "pian_presnost"]
|
iface.messageBar().pushMessage(
|
||||||
|
"AMCR",
|
||||||
|
f"Záznamů: {len(docs)} (z toho {actions_with_geom} s mapou). "
|
||||||
|
f"Stahuji {total_pians} unikátních geometrií, "
|
||||||
|
f"vykresluji {target_pian_ids_count} geometrií...",
|
||||||
|
level=Qgis.MessageLevel.Info
|
||||||
|
)
|
||||||
|
|
||||||
|
fl_pian = [
|
||||||
|
"ident_cely", "pian_typ",
|
||||||
|
"pian_chranene_udaje", "pian_presnost"
|
||||||
|
]
|
||||||
|
|
||||||
for i in range(0, total_pians, BATCH_PIAN):
|
for i in range(0, total_pians, BATCH_PIAN):
|
||||||
batch = ids_list[i : i + BATCH_PIAN]
|
batch = ids_list[i: i + BATCH_PIAN]
|
||||||
or_query = " OR ".join(batch)
|
or_query = " OR ".join(batch)
|
||||||
fq_pian = f"ident_cely:({or_query})"
|
fq_pian = f"ident_cely:({or_query})"
|
||||||
|
|
||||||
params_pian = {
|
params_pian = {
|
||||||
"mapa": "true",
|
"mapa": "true",
|
||||||
"entity": "pian",
|
"entity": "pian",
|
||||||
@@ -441,7 +591,7 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
|
|||||||
"fl": ",".join(fl_pian)
|
"fl": ",".join(fl_pian)
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
QApplication.processEvents()
|
QApplication.processEvents()
|
||||||
r_pian = _api_get(url, params=params_pian, timeout=15)
|
r_pian = _api_get(url, params=params_pian, timeout=15)
|
||||||
batch_docs = r_pian.json().get('response', {}).get('docs', [])
|
batch_docs = r_pian.json().get('response', {}).get('docs', [])
|
||||||
docs_pian.extend(batch_docs)
|
docs_pian.extend(batch_docs)
|
||||||
@@ -451,13 +601,25 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
|
|||||||
# ==========================================
|
# ==========================================
|
||||||
# D) LAYER CREATION (QGIS Memory Layers)
|
# D) LAYER CREATION (QGIS Memory Layers)
|
||||||
# ==========================================
|
# ==========================================
|
||||||
|
|
||||||
archeologicky_zaznam = "Akce" if typ_dat == "akce" else "Lokalita"
|
archeologicky_zaznam = "Akce" if typ_dat == "akce" else "Lokalita"
|
||||||
|
|
||||||
# Initialize three layers for different geometry types (S-JTSK CRS)
|
# Initialize three layers for different geometry types (S-JTSK CRS)
|
||||||
vl_poly = QgsVectorLayer("Polygon?crs=epsg:5514", f"AMCR_{archeologicky_zaznam}_Polygony", "memory")
|
vl_poly = QgsVectorLayer(
|
||||||
vl_line = QgsVectorLayer("LineString?crs=epsg:5514", f"AMCR_{archeologicky_zaznam}_Linie", "memory")
|
"Polygon?crs=epsg:5514",
|
||||||
vl_point = QgsVectorLayer("Point?crs=epsg:5514", f"AMCR_{archeologicky_zaznam}_Body", "memory")
|
f"AMCR_{archeologicky_zaznam}_Polygony",
|
||||||
|
"memory"
|
||||||
|
)
|
||||||
|
vl_line = QgsVectorLayer(
|
||||||
|
"LineString?crs=epsg:5514",
|
||||||
|
f"AMCR_{archeologicky_zaznam}_Linie",
|
||||||
|
"memory"
|
||||||
|
)
|
||||||
|
vl_point = QgsVectorLayer(
|
||||||
|
"Point?crs=epsg:5514",
|
||||||
|
f"AMCR_{archeologicky_zaznam}_Body",
|
||||||
|
"memory"
|
||||||
|
)
|
||||||
layers = [vl_poly, vl_line, vl_point]
|
layers = [vl_poly, vl_line, vl_point]
|
||||||
|
|
||||||
# Define attribute table structure
|
# Define attribute table structure
|
||||||
@@ -486,7 +648,7 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
|
|||||||
QgsField("ukonceni", QMetaType.Type.QString),
|
QgsField("ukonceni", QMetaType.Type.QString),
|
||||||
QgsField("hlavni_typ", QMetaType.Type.QString),
|
QgsField("hlavni_typ", QMetaType.Type.QString),
|
||||||
QgsField("vedlejsi_typ", QMetaType.Type.QString),
|
QgsField("vedlejsi_typ", QMetaType.Type.QString),
|
||||||
QgsField("zjisteni", QMetaType.Type.QString),
|
QgsField("zjisteni", QMetaType.Type.QString),
|
||||||
QgsField("nahrazuje_NZ", QMetaType.Type.QString),
|
QgsField("nahrazuje_NZ", QMetaType.Type.QString),
|
||||||
]
|
]
|
||||||
elif typ_dat == "lokalita":
|
elif typ_dat == "lokalita":
|
||||||
@@ -497,7 +659,7 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
|
|||||||
QgsField("druh_lokality", QMetaType.Type.QString),
|
QgsField("druh_lokality", QMetaType.Type.QString),
|
||||||
QgsField("zachovalost", QMetaType.Type.QString)
|
QgsField("zachovalost", QMetaType.Type.QString)
|
||||||
]
|
]
|
||||||
|
|
||||||
cols.append(QgsField("Přístupnost", QMetaType.Type.QString))
|
cols.append(QgsField("Přístupnost", QMetaType.Type.QString))
|
||||||
|
|
||||||
# Use aliases for technical field names
|
# Use aliases for technical field names
|
||||||
@@ -531,7 +693,7 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
|
|||||||
"komponenta": "Komponenta",
|
"komponenta": "Komponenta",
|
||||||
"komponenta_areal": "Areál",
|
"komponenta_areal": "Areál",
|
||||||
"komponenta_obdobi": "Období",
|
"komponenta_obdobi": "Období",
|
||||||
}
|
}
|
||||||
|
|
||||||
if komponenty == "true":
|
if komponenty == "true":
|
||||||
cols += [
|
cols += [
|
||||||
@@ -547,36 +709,45 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
|
|||||||
idx = vl.fields().lookupField(tech_name)
|
idx = vl.fields().lookupField(tech_name)
|
||||||
if idx != -1:
|
if idx != -1:
|
||||||
vl.setFieldAlias(idx, alias)
|
vl.setFieldAlias(idx, alias)
|
||||||
|
|
||||||
# Lists to hold features before batch-adding to layers
|
# Lists to hold features before batch-adding to layers
|
||||||
feats_p, feats_l, feats_pt = [], [], []
|
feats_p, feats_l, feats_pt = [], [], []
|
||||||
|
|
||||||
# --- FEATURE POPULATION ---
|
# --- FEATURE POPULATION ---
|
||||||
for doc in docs_pian:
|
for doc in docs_pian:
|
||||||
try:
|
try:
|
||||||
pid = doc.get('ident_cely', '')
|
pid = doc.get('ident_cely', '')
|
||||||
if pid not in pian_lookup:
|
if pid not in pian_lookup:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
metas = pian_lookup[pid]
|
metas = pian_lookup[pid]
|
||||||
|
|
||||||
# Extract WKT geometry from protected JSON data
|
# Extract WKT geometry from protected JSON data
|
||||||
raw = doc.get('pian_chranene_udaje')
|
raw = doc.get('pian_chranene_udaje')
|
||||||
if isinstance(raw, list) and raw:
|
if isinstance(raw, list) and raw:
|
||||||
raw = raw[0]
|
raw = raw[0]
|
||||||
jdata = json.loads(raw) if isinstance(raw, str) else (raw or {})
|
jdata = (
|
||||||
|
json.loads(raw)
|
||||||
|
if isinstance(raw, str)
|
||||||
|
else (raw or {})
|
||||||
|
)
|
||||||
|
|
||||||
wkt = None
|
wkt = None
|
||||||
if jdata.get('geom_sjtsk_wkt'):
|
if jdata.get('geom_sjtsk_wkt'):
|
||||||
wkt = jdata.get('geom_sjtsk_wkt', {}).get('value')
|
wkt = jdata.get('geom_sjtsk_wkt', {}).get('value')
|
||||||
elif jdata.get('geom_wkt'):
|
elif jdata.get('geom_wkt'):
|
||||||
wkt = jdata.get('geom_wkt', {}).get('value')
|
wkt = jdata.get('geom_wkt', {}).get('value')
|
||||||
|
|
||||||
pian_presnost = tr_code(str(doc.get('pian_presnost', '')))
|
pian_presnost = tr_code(str(doc.get('pian_presnost', '')))
|
||||||
pian_typ = tr_code(str(doc.get('pian_typ', '')))
|
pian_typ = tr_code(str(doc.get('pian_typ', '')))
|
||||||
|
|
||||||
# Final precision filter check
|
# Final precision filter check
|
||||||
if filters and filters.get('f_pian_presnost') and doc.get('pian_presnost') not in filters.get('f_pian_presnost'):
|
if (
|
||||||
|
filters
|
||||||
|
and filters.get('f_pian_presnost')
|
||||||
|
and doc.get('pian_presnost')
|
||||||
|
not in filters.get('f_pian_presnost')
|
||||||
|
):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if wkt:
|
if wkt:
|
||||||
@@ -586,41 +757,54 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
|
|||||||
target_list = None
|
target_list = None
|
||||||
if t == QgsWkbTypes.PolygonGeometry:
|
if t == QgsWkbTypes.PolygonGeometry:
|
||||||
target_list = feats_p
|
target_list = feats_p
|
||||||
referenced_layer = vl_poly
|
|
||||||
elif t == QgsWkbTypes.LineGeometry:
|
elif t == QgsWkbTypes.LineGeometry:
|
||||||
target_list = feats_l
|
target_list = feats_l
|
||||||
referenced_layer = vl_line
|
|
||||||
elif t == QgsWkbTypes.PointGeometry:
|
elif t == QgsWkbTypes.PointGeometry:
|
||||||
target_list = feats_pt
|
target_list = feats_pt
|
||||||
referenced_layer = vl_point
|
|
||||||
|
|
||||||
if target_list is None:
|
if target_list is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
is_akce = (typ_dat == "akce")
|
is_akce = (typ_dat == "akce")
|
||||||
|
|
||||||
# Create a QGIS feature for each documentation unit associated with this geometry
|
# Create a QGIS feature for each documentation unit
|
||||||
|
# associated with this geometry
|
||||||
for meta in metas:
|
for meta in metas:
|
||||||
feat = QgsFeature()
|
feat = QgsFeature()
|
||||||
feat.setGeometry(geom)
|
feat.setGeometry(geom)
|
||||||
atributy = [
|
atributy = [
|
||||||
pid, pian_presnost, pian_typ, meta['dj_id'],
|
pid,
|
||||||
meta['dj_typ_value'], meta['loc'], meta['ident_cely'],
|
pian_presnost,
|
||||||
"https://digiarchiv.aiscr.cz/id/" + meta['ident_cely'],
|
pian_typ,
|
||||||
meta['az_okres'], meta['katastr'], meta['dalsi_katastr']
|
meta['dj_id'],
|
||||||
|
meta['dj_typ_value'],
|
||||||
|
meta['loc'],
|
||||||
|
meta['ident_cely'],
|
||||||
|
"https://digiarchiv.aiscr.cz/id/"
|
||||||
|
+ meta['ident_cely'],
|
||||||
|
meta['az_okres'],
|
||||||
|
meta['katastr'],
|
||||||
|
meta['dalsi_katastr']
|
||||||
]
|
]
|
||||||
if is_akce:
|
if is_akce:
|
||||||
atributy.extend([
|
atributy.extend([
|
||||||
meta['lokalizace_okolnosti'], meta['akce_hlavni_vedouci'],
|
meta['lokalizace_okolnosti'],
|
||||||
meta['akce_organizace'], meta['akce_specifikace_data'],
|
meta['akce_hlavni_vedouci'],
|
||||||
meta['akce_datum_zahajeni'], meta['akce_datum_ukonceni'],
|
meta['akce_organizace'],
|
||||||
meta['akce_hlavni_typ'], meta['akce_vedlejsi_typ'],
|
meta['akce_specifikace_data'],
|
||||||
meta['dj_negativni'], meta['akce_je_nz']
|
meta['akce_datum_zahajeni'],
|
||||||
|
meta['akce_datum_ukonceni'],
|
||||||
|
meta['akce_hlavni_typ'],
|
||||||
|
meta['akce_vedlejsi_typ'],
|
||||||
|
meta['dj_negativni'],
|
||||||
|
meta['akce_je_nz']
|
||||||
])
|
])
|
||||||
else:
|
else:
|
||||||
atributy.extend([
|
atributy.extend([
|
||||||
meta['lokalita_nazev'], meta['lokalita_popis'],
|
meta['lokalita_nazev'],
|
||||||
meta['lokalita_typ'], meta['lokalita_druh'],
|
meta['lokalita_popis'],
|
||||||
|
meta['lokalita_typ'],
|
||||||
|
meta['lokalita_druh'],
|
||||||
meta['lokalita_zachovalost']
|
meta['lokalita_zachovalost']
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -631,21 +815,21 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
|
|||||||
meta.get('komponenta_id', ""),
|
meta.get('komponenta_id', ""),
|
||||||
meta.get('komponenta_areal', ""),
|
meta.get('komponenta_areal', ""),
|
||||||
meta.get('komponenta_obdobi', ""),
|
meta.get('komponenta_obdobi', ""),
|
||||||
])
|
])
|
||||||
|
|
||||||
feat.setAttributes(atributy)
|
feat.setAttributes(atributy)
|
||||||
target_list.append(feat)
|
target_list.append(feat)
|
||||||
|
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print(f"Chyba při tvorbě feature: {ex}")
|
print(f"Chyba při tvorbě feature: {ex}")
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# --- ADDING TO QGIS INTERFACE ---
|
# --- ADDING TO QGIS INTERFACE ---
|
||||||
proj = QgsProject.instance()
|
proj = QgsProject.instance()
|
||||||
added = 0
|
added = 0
|
||||||
layers_to_process = [
|
layers_to_process = [
|
||||||
(feats_p, vl_poly, "Polygony"),
|
(feats_p, vl_poly, "Polygony"),
|
||||||
(feats_l, vl_line, "Linie"),
|
(feats_l, vl_line, "Linie"),
|
||||||
(feats_pt, vl_point, "Body"),
|
(feats_pt, vl_point, "Body"),
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -653,17 +837,30 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
|
|||||||
if f:
|
if f:
|
||||||
l.dataProvider().addFeatures(f)
|
l.dataProvider().addFeatures(f)
|
||||||
l.updateExtents()
|
l.updateExtents()
|
||||||
l.setName(f"AMCR_{archeologicky_zaznam}_{n}")
|
l.setName(f"AMCR_{archeologicky_zaznam}_{n}")
|
||||||
proj.addMapLayer(l)
|
proj.addMapLayer(l)
|
||||||
added += len(f)
|
added += len(f)
|
||||||
|
|
||||||
if added > 0:
|
if added > 0:
|
||||||
iface.messageBar().pushMessage("AMCR", f"Hotovo. Záznamů: {len(docs)} (s geom: {actions_with_geom}). Vykresleno: {added} prvků.", level=Qgis.MessageLevel.Success)
|
iface.messageBar().pushMessage(
|
||||||
|
"AMCR",
|
||||||
|
f"Hotovo. Záznamů: {len(docs)} (s geom: {actions_with_geom}). "
|
||||||
|
f"Vykresleno: {added} prvků.",
|
||||||
|
level=Qgis.MessageLevel.Success
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
iface.messageBar().pushMessage("AMCR", "Žádná data k zobrazení.", level=Qgis.MessageLevel.Info)
|
iface.messageBar().pushMessage(
|
||||||
|
"AMCR",
|
||||||
|
"Žádná data k zobrazení.",
|
||||||
|
level=Qgis.MessageLevel.Info
|
||||||
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
iface.messageBar().pushMessage("Chyba", str(e), level=Qgis.MessageLevel.Critical)
|
iface.messageBar().pushMessage(
|
||||||
|
"Chyba",
|
||||||
|
str(e),
|
||||||
|
level=Qgis.MessageLevel.Critical
|
||||||
|
)
|
||||||
finally:
|
finally:
|
||||||
# Always restore cursor, even after failure
|
# Always restore cursor, even after failure
|
||||||
QApplication.restoreOverrideCursor()
|
QApplication.restoreOverrideCursor()
|
||||||
|
|||||||
+51
-30
@@ -3,35 +3,37 @@ from qgis.PyQt.QtCore import QSettings, QTranslator, QCoreApplication
|
|||||||
from qgis.PyQt.QtGui import QIcon
|
from qgis.PyQt.QtGui import QIcon
|
||||||
from qgis.PyQt.QtWidgets import QMenu, QAction, QToolButton, QDialog
|
from qgis.PyQt.QtWidgets import QMenu, QAction, QToolButton, QDialog
|
||||||
from qgis.core import Qgis
|
from qgis.core import Qgis
|
||||||
from qgis.utils import iface
|
|
||||||
|
|
||||||
from .amcr_tools import load_amcr_data, login_to_api
|
from .amcr_tools import load_amcr_data, login_to_api
|
||||||
from .amcr_dialog import AmcrFilterDialog, LoginDialog
|
from .amcr_dialog import AmcrFilterDialog, LoginDialog
|
||||||
from .resources import *
|
from .resources import *
|
||||||
import os.path
|
import os.path
|
||||||
|
|
||||||
|
|
||||||
class AmcrViewer:
|
class AmcrViewer:
|
||||||
"""
|
"""
|
||||||
Main plugin class that manages the GUI elements, menu entries,
|
Main plugin class that manages the GUI elements, menu entries,
|
||||||
and coordinates the flow between user input and data processing.
|
and coordinates the flow between user input and data processing.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, iface):
|
def __init__(self, iface):
|
||||||
"""
|
"""
|
||||||
Constructor initializes the connection to QGIS interface and sets up
|
Constructor initializes the connection to QGIS interface and sets up
|
||||||
internationalization (i18n).
|
internationalization (i18n).
|
||||||
"""
|
"""
|
||||||
self.iface = iface
|
self.iface = iface
|
||||||
self.plugin_dir = os.path.dirname(__file__)
|
self.plugin_dir = os.path.dirname(__file__)
|
||||||
|
|
||||||
# Determine the user's locale to load appropriate translation files
|
# Determine the user's locale to load appropriate translation files
|
||||||
locale = QSettings().value('locale/userLocale')[0:2]
|
locale = QSettings().value('locale/userLocale')[0:2]
|
||||||
locale_path = os.path.join(
|
locale_path = os.path.join(
|
||||||
self.plugin_dir,
|
self.plugin_dir,
|
||||||
'i18n',
|
'i18n',
|
||||||
'AmcrViewer_{}.qm'.format(locale))
|
'AmcrViewer_{}.qm'.format(locale)
|
||||||
|
)
|
||||||
|
|
||||||
# Install the translator if a translation file for the current locale exists
|
# Install the translator if a translation file
|
||||||
|
# for the current locale exists
|
||||||
if os.path.exists(locale_path):
|
if os.path.exists(locale_path):
|
||||||
self.translator = QTranslator()
|
self.translator = QTranslator()
|
||||||
self.translator.load(locale_path)
|
self.translator.load(locale_path)
|
||||||
@@ -43,14 +45,17 @@ class AmcrViewer:
|
|||||||
self.first_start = None
|
self.first_start = None
|
||||||
|
|
||||||
def tr(self, message):
|
def tr(self, message):
|
||||||
"""Helper method for translating strings within the AmcrViewer context."""
|
"""
|
||||||
|
Helper method for translating strings within
|
||||||
|
the AmcrViewer context.
|
||||||
|
"""
|
||||||
return QCoreApplication.translate('AmcrViewer', message)
|
return QCoreApplication.translate('AmcrViewer', message)
|
||||||
|
|
||||||
def add_action(self, icon_path, text, callback, enabled_flag=True,
|
def add_action(self, icon_path, text, callback, enabled_flag=True,
|
||||||
add_to_menu=True, add_to_toolbar=True, status_tip=None,
|
add_to_menu=True, add_to_toolbar=True, status_tip=None,
|
||||||
whats_this=None, parent=None):
|
whats_this=None, parent=None):
|
||||||
"""
|
"""
|
||||||
Helper method to create QActions and automatically register them
|
Helper method to create QActions and automatically register them
|
||||||
into the QGIS Menu and Toolbar.
|
into the QGIS Menu and Toolbar.
|
||||||
"""
|
"""
|
||||||
icon = QIcon(icon_path)
|
icon = QIcon(icon_path)
|
||||||
@@ -71,15 +76,16 @@ class AmcrViewer:
|
|||||||
if add_to_menu:
|
if add_to_menu:
|
||||||
self.iface.addPluginToMenu(self.menu, action)
|
self.iface.addPluginToMenu(self.menu, action)
|
||||||
|
|
||||||
# Store only actions that are directly attached to the QGIS UI for later cleanup
|
# Store only actions that are directly attached
|
||||||
|
# to the QGIS UI for later cleanup
|
||||||
if add_to_toolbar or add_to_menu:
|
if add_to_toolbar or add_to_menu:
|
||||||
self.actions.append(action)
|
self.actions.append(action)
|
||||||
|
|
||||||
return action
|
return action
|
||||||
|
|
||||||
def initGui(self):
|
def initGui(self):
|
||||||
"""
|
"""
|
||||||
Called when the plugin is loaded. Creates the menu structure,
|
Called when the plugin is loaded. Creates the menu structure,
|
||||||
sub-actions, and the dropdown tool button in the toolbar.
|
sub-actions, and the dropdown tool button in the toolbar.
|
||||||
"""
|
"""
|
||||||
# Define paths for action-specific icons
|
# Define paths for action-specific icons
|
||||||
@@ -90,7 +96,8 @@ class AmcrViewer:
|
|||||||
self.plugin_menu = QMenu()
|
self.plugin_menu = QMenu()
|
||||||
|
|
||||||
# 2. Create sub-actions (Download Projects / Download Sites)
|
# 2. Create sub-actions (Download Projects / Download Sites)
|
||||||
# add_to_menu/toolbar is False because these go into our custom dropdown menu
|
# add_to_menu/toolbar is False because these go into our
|
||||||
|
# custom dropdown menu
|
||||||
self.action_download_akce = self.add_action(
|
self.action_download_akce = self.add_action(
|
||||||
icon_path=icon_akce_path,
|
icon_path=icon_akce_path,
|
||||||
text=self.tr(u'Stáhnout data akcí | AMČR Viewer'),
|
text=self.tr(u'Stáhnout data akcí | AMČR Viewer'),
|
||||||
@@ -104,7 +111,7 @@ class AmcrViewer:
|
|||||||
self.action_download_lokality = self.add_action(
|
self.action_download_lokality = self.add_action(
|
||||||
icon_path=icon_lokality_path,
|
icon_path=icon_lokality_path,
|
||||||
text=self.tr(u'Stáhnout data lokalit | AMČR Viewer'),
|
text=self.tr(u'Stáhnout data lokalit | AMČR Viewer'),
|
||||||
callback=lambda checked=False: self.run_download('lokalita'),
|
callback=lambda checked=False: self.run_download('lokalita'),
|
||||||
parent=self.iface.mainWindow(),
|
parent=self.iface.mainWindow(),
|
||||||
add_to_menu=False,
|
add_to_menu=False,
|
||||||
add_to_toolbar=False
|
add_to_toolbar=False
|
||||||
@@ -114,7 +121,7 @@ class AmcrViewer:
|
|||||||
self.action_login_dialog = self.add_action(
|
self.action_login_dialog = self.add_action(
|
||||||
icon_path=icon_akce_path,
|
icon_path=icon_akce_path,
|
||||||
text=self.tr(u'Přihlásit se | AMČR Viewer'),
|
text=self.tr(u'Přihlásit se | AMČR Viewer'),
|
||||||
callback=lambda checked=False: self.login(),
|
callback=lambda checked=False: self.login(),
|
||||||
parent=self.iface.mainWindow(),
|
parent=self.iface.mainWindow(),
|
||||||
add_to_menu=False,
|
add_to_menu=False,
|
||||||
add_to_toolbar=False
|
add_to_toolbar=False
|
||||||
@@ -123,7 +130,11 @@ class AmcrViewer:
|
|||||||
|
|
||||||
# 3. Create the main project action and attach the menu to it
|
# 3. Create the main project action and attach the menu to it
|
||||||
main_icon = QIcon(icon_akce_path)
|
main_icon = QIcon(icon_akce_path)
|
||||||
self.main_action = QAction(main_icon, 'AMČR Viewer', self.iface.mainWindow())
|
self.main_action = QAction(
|
||||||
|
main_icon,
|
||||||
|
'AMČR Viewer',
|
||||||
|
self.iface.mainWindow()
|
||||||
|
)
|
||||||
self.main_action.setMenu(self.plugin_menu)
|
self.main_action.setMenu(self.plugin_menu)
|
||||||
self.iface.addPluginToMenu(self.menu, self.main_action)
|
self.iface.addPluginToMenu(self.menu, self.main_action)
|
||||||
|
|
||||||
@@ -132,16 +143,19 @@ class AmcrViewer:
|
|||||||
self.tool_button = QToolButton()
|
self.tool_button = QToolButton()
|
||||||
self.tool_button.setMenu(self.plugin_menu)
|
self.tool_button.setMenu(self.plugin_menu)
|
||||||
self.tool_button.setDefaultAction(self.action_download_akce)
|
self.tool_button.setDefaultAction(self.action_download_akce)
|
||||||
self.tool_button.setPopupMode(QToolButton.ToolButtonPopupMode.MenuButtonPopup)
|
self.tool_button.setPopupMode(
|
||||||
|
QToolButton.ToolButtonPopupMode.MenuButtonPopup
|
||||||
# Add the widget directly to the toolbar and store the reference for cleanup
|
)
|
||||||
|
|
||||||
|
# Add the widget directly to the toolbar
|
||||||
|
# and store the reference for cleanup
|
||||||
self.toolbar_action = self.iface.addToolBarWidget(self.tool_button)
|
self.toolbar_action = self.iface.addToolBarWidget(self.tool_button)
|
||||||
|
|
||||||
self.first_start = True
|
self.first_start = True
|
||||||
|
|
||||||
def unload(self):
|
def unload(self):
|
||||||
"""
|
"""
|
||||||
Called when the plugin is disabled or removed.
|
Called when the plugin is disabled or removed.
|
||||||
Ensures all GUI elements are removed from QGIS to avoid ghost icons.
|
Ensures all GUI elements are removed from QGIS to avoid ghost icons.
|
||||||
"""
|
"""
|
||||||
# 1. Remove the custom entry from the main 'Plugins' menu
|
# 1. Remove the custom entry from the main 'Plugins' menu
|
||||||
@@ -157,7 +171,7 @@ class AmcrViewer:
|
|||||||
self.iface.removePluginMenu(self.menu, action)
|
self.iface.removePluginMenu(self.menu, action)
|
||||||
self.iface.removeToolBarIcon(action)
|
self.iface.removeToolBarIcon(action)
|
||||||
self.actions.clear()
|
self.actions.clear()
|
||||||
|
|
||||||
# 4. Reset map tools if currently active
|
# 4. Reset map tools if currently active
|
||||||
if hasattr(self, 'tool'):
|
if hasattr(self, 'tool'):
|
||||||
self.iface.mapCanvas().unsetMapTool(self.tool)
|
self.iface.mapCanvas().unsetMapTool(self.tool)
|
||||||
@@ -165,20 +179,22 @@ class AmcrViewer:
|
|||||||
# --- Data downloading ---
|
# --- Data downloading ---
|
||||||
def run_download(self, typ_dat):
|
def run_download(self, typ_dat):
|
||||||
"""
|
"""
|
||||||
Triggered by menu/toolbar actions. Opens the filter dialog and
|
Triggered by menu/toolbar actions. Opens the filter dialog and
|
||||||
hands off the parameters to the data loader.
|
hands off the parameters to the data loader.
|
||||||
"""
|
"""
|
||||||
# Open the specific filter dialog (Projects vs Sites)
|
# Open the specific filter dialog (Projects vs Sites)
|
||||||
dlg = AmcrFilterDialog(typ_dat)
|
dlg = AmcrFilterDialog(typ_dat)
|
||||||
result = dlg.exec()
|
result = dlg.exec()
|
||||||
|
|
||||||
# If user confirmed the dialog (OK button), gather filters and load data
|
# If user confirmed the dialog (OK button),
|
||||||
|
# gather filters and load data
|
||||||
if result == QDialog.DialogCode.Accepted:
|
if result == QDialog.DialogCode.Accepted:
|
||||||
filters = dlg.get_filters()
|
filters = dlg.get_filters()
|
||||||
bbox = dlg.get_bbox()
|
bbox = dlg.get_bbox()
|
||||||
komponenty = dlg.get_komponenty()
|
komponenty = dlg.get_komponenty()
|
||||||
|
|
||||||
# Access the map canvas and start the fetch/render process from amcr_tools
|
# Access the map canvas and start
|
||||||
|
# the fetch/render process from amcr_tools
|
||||||
canvas = self.iface.mapCanvas()
|
canvas = self.iface.mapCanvas()
|
||||||
load_amcr_data(canvas, bbox, filters, typ_dat, komponenty)
|
load_amcr_data(canvas, bbox, filters, typ_dat, komponenty)
|
||||||
|
|
||||||
@@ -190,9 +206,14 @@ class AmcrViewer:
|
|||||||
session = login_to_api(username, password)
|
session = login_to_api(username, password)
|
||||||
if session:
|
if session:
|
||||||
self.iface.messageBar().pushMessage(
|
self.iface.messageBar().pushMessage(
|
||||||
"AMČR", "Přihlášení proběhlo úspěšně.", level=Qgis.MessageLevel.Success
|
"AMČR",
|
||||||
|
"Přihlášení proběhlo úspěšně.",
|
||||||
|
level=Qgis.MessageLevel.Success
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.iface.messageBar().pushMessage(
|
self.iface.messageBar().pushMessage(
|
||||||
"AMČR", "Přihlášení se nezdařilo – viz záložka AMČR login v panelu Zprávy.", level=Qgis.MessageLevel.Critical
|
"AMČR",
|
||||||
)
|
"Přihlášení se nezdařilo – viz záložka AMČR login "
|
||||||
|
"v panelu Zprávy.",
|
||||||
|
level=Qgis.MessageLevel.Critical
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user