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:
2026-06-04 17:32:32 +02:00
committed by GitHub
parent b313fc6db0
commit 493696c67b
4 changed files with 732 additions and 360 deletions
+71 -38
View File
@@ -13,20 +13,20 @@ BASE_URL = "https://api.aiscr.cz/2.2/oai"
OUTPUT_FILE = os.path.join(CODELISTS_DIR, 'heslar.csv')
slovnicek = {
'obdobi' : 'heslo:obdobi',
'typ_akce' : 'heslo:akce_typ',
'areal' : 'heslo:areal',
'kraj' : 'ruian_kraj',
'organizace' : 'organizace',
'okres' : 'ruian_okres',
'katastr' : 'ruian_katastr',
'vedouci' : 'osoba',
'pian_presnost' : 'heslo:pian_presnost',
'typ_lokality' : 'heslo:lokalita_typ',
'druh_lokality' : 'heslo:lokalita_druh',
'jistota' : 'heslo:jistota_urceni',
'lokalita_zachovalost' : 'heslo:stav_dochovani',
'pristupnost' : 'heslo:pristupnost'
'obdobi': 'heslo:obdobi',
'typ_akce': 'heslo:akce_typ',
'areal': 'heslo:areal',
'kraj': 'ruian_kraj',
'organizace': 'organizace',
'okres': 'ruian_okres',
'katastr': 'ruian_katastr',
'vedouci': 'osoba',
'pian_presnost': 'heslo:pian_presnost',
'typ_lokality': 'heslo:lokalita_typ',
'druh_lokality': 'heslo:lokalita_druh',
'jistota': 'heslo:jistota_urceni',
'lokalita_zachovalost': 'heslo:stav_dochovani',
'pristupnost': 'heslo:pristupnost'
}
NS = {
@@ -35,13 +35,18 @@ NS = {
'oai_dc': 'http://www.openarchives.org/OAI/2.0/oai_dc/'
}
def ensure_codelists_dir():
"""Creates the codelists directory if it does not exist."""
if not os.path.exists(CODELISTS_DIR):
os.makedirs(CODELISTS_DIR)
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:
target_dict = {}
@@ -67,17 +72,23 @@ def parse_codelist_file(filename, target_dict=None):
cat = row[2].strip()
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:
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
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
def load_all_data():
"""Loads the codelist during plugin startup."""
ensure_codelists_dir()
@@ -85,6 +96,7 @@ def load_all_data():
parse_codelist_file('heslar.csv', categorized_data)
return categorized_data
def fetch_set(internal_name, api_set, task=None):
dataset = []
params = {
@@ -94,7 +106,7 @@ def fetch_set(internal_name, api_set, task=None):
}
while True:
# Kontrola zrušení v každém kroku
# Check for cancellation at each iteration
if task and task.isCanceled():
return None
@@ -107,17 +119,27 @@ def fetch_set(internal_name, api_set, task=None):
for rec in records:
metadata = rec.find('.//oai_dc:dc', NS)
if metadata is not None:
# Kód (identifier)
kod = metadata.find('dc:identifier', NS).text if metadata.find('dc:identifier', NS) is not None else ""
# Code (identifier)
identifier_el = metadata.find('dc:identifier', NS)
kod = (
identifier_el.text
if identifier_el is not None
else ""
)
# Název (title) - filtrujeme systémové popisky "AMČR - ..."
# Title filter out system labels "AMČR - ..."
titles = metadata.findall('dc:title', NS)
nazev = ""
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
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:
nazev = titles[0].text
@@ -127,7 +149,15 @@ def fetch_set(internal_name, api_set, task=None):
kod = nazev
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({
'Název': nazev,
@@ -135,7 +165,7 @@ def fetch_set(internal_name, api_set, task=None):
'Kategorie': internal_name
})
# Stránkování
# Pagination
token = root.find('.//oai:resumptionToken', NS)
if token is not None and token.text:
params = {
@@ -147,11 +177,14 @@ def fetch_set(internal_name, api_set, task=None):
break
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
return dataset
def download_heslare(task=None):
"""Fetches the codelists from the AMČR API and saves it to a CSV file."""
ensure_codelists_dir()
@@ -159,26 +192,28 @@ def download_heslare(task=None):
total_sets = len(slovnicek)
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():
return False
QgsMessageLog.logMessage(f"Zpracovávám kategorii: {interni}...", "AMČR", Qgis.Info)
QgsMessageLog.logMessage(
f"Zpracovávám kategorii: {interni}...",
"AMČR", Qgis.Info)
# Nyní předáváme task správně do upravené funkce
# Pass the task correctly to the updated fetch function
data = fetch_set(interni, api_nazev, task=task)
if data is None:
return False # Bylo zrušeno uprostřed stahování
return False # Cancelled mid-download
all_data.extend(data)
# Reportování postupu (0-100)
# Report progress (0-100)
if task:
progress = (index + 1) / total_sets * 100
task.setProgress(progress)
# Uložení do CSV
# Save to CSV
with open(OUTPUT_FILE, 'w', newline='', encoding='utf-8-sig') as f:
fieldnames = ['Název', 'Kód', 'Kategorie']
writer = csv.DictWriter(f, fieldnames=fieldnames, delimiter=';')
@@ -187,11 +222,9 @@ def download_heslare(task=None):
return True
def refresh_globals():
"""Znovu načte data ze souborů do globálních proměnných."""
global OBDOBI, TYP_AKCE, AREAL, KRAJE, ORGANIZACE, OKRESY, KATASTRY
global VEDOUCI, PIAN_PRESNOST, TYP_LOKALITY, DRUH_LOKALITY, JISTOTA, LOKALITA_ZACHOVALOST, PRISTUPNOST
def refresh_globals():
"""Reloads data from files into the global variables."""
data = load_all_data()
OBDOBI.clear()
@@ -224,7 +257,7 @@ def refresh_globals():
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 = {}
TYP_AKCE = {}
AREAL = {}
+188 -67
View File
@@ -5,12 +5,15 @@ from qgis.PyQt.QtWidgets import (QDialog, QVBoxLayout,
QListWidget, QListWidgetItem, QHBoxLayout,
QMessageBox, QLabel, QFormLayout)
from qgis.PyQt.QtCore import Qt, QSettings
from qgis.core import QgsTask, QgsApplication, QgsMessageLog, Qgis, QgsAuthMethodConfig
from qgis.core import (QgsTask, QgsApplication,
QgsMessageLog, Qgis, QgsAuthMethodConfig)
from .amcr_codelists import (OBDOBI, TYP_AKCE, KRAJE, AREAL, ORGANIZACE,
OKRESY, KATASTRY, VEDOUCI, PIAN_PRESNOST, TYP_LOKALITY,
DRUH_LOKALITY, JISTOTA, LOKALITA_ZACHOVALOST, PRISTUPNOST,
OKRESY, KATASTRY, VEDOUCI, PIAN_PRESNOST,
TYP_LOKALITY, DRUH_LOKALITY, JISTOTA,
LOKALITA_ZACHOVALOST, PRISTUPNOST,
download_heslare, refresh_globals)
class UpdateCodelistsTask(QgsTask):
def __init__(self, description):
super().__init__(description, QgsTask.CanCancel)
@@ -18,9 +21,9 @@ class UpdateCodelistsTask(QgsTask):
self.exception = None
def run(self):
"""Tato část běží ve vedlejším vlákně."""
"""Runs in a background thread."""
try:
# Voláme upravenou funkci
# Call the download function with the task reference
self.success = download_heslare(task=self)
return self.success
except Exception as e:
@@ -28,20 +31,28 @@ class UpdateCodelistsTask(QgsTask):
return False
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:
# Teď bezpečně aktualizujeme globální proměnné v hlavním vlákně
# Safely update the global variables in the main thread
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:
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:
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):
"""
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.
"""
def __init__(self, title, data_dict, preselected_codes, parent=None):
@@ -68,7 +79,8 @@ class FilterableSelectionDialog(QDialog):
# Standard OK/Cancel dialog buttons
buttons = QDialogButtonBox(
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
QDialogButtonBox.StandardButton.Ok
| QDialogButtonBox.StandardButton.Cancel
)
buttons.accepted.connect(self.accept)
buttons.rejected.connect(self.reject)
@@ -126,15 +138,16 @@ class AmcrFilterDialog(QDialog):
self.setWindowTitle("Filtr AMČR")
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
# Cache dictionary to store selected codes for each category
self.selection_cache = {
'organizace': [], 'kraj': [], 'obdobi': [], 'areal': [],
'typ_akce': [], 'okres': [], 'katastr': [], 'vedouci': [], 'pian_presnost': [], 'pristupnost': [],
'typ_lokality': [], 'druh_lokality': [], 'jistota': [], 'lokalita_zachovalost': []
'typ_akce': [], 'okres': [], 'katastr': [], 'vedouci': [],
'pian_presnost': [], 'pristupnost': [], 'typ_lokality': [],
'druh_lokality': [], 'jistota': [], 'lokalita_zachovalost': []
}
layout = QVBoxLayout()
@@ -160,42 +173,82 @@ class AmcrFilterDialog(QDialog):
self.picker_okres = self.setup_picker("Okres", 'okres', OKRESY)
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)
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)
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)
# Filters valid for 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)
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)
# 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)
# Filters valid for Lokality
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)
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)
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)
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)
# Contextual information
@@ -218,10 +271,16 @@ class AmcrFilterDialog(QDialog):
buttons = QDialogButtonBox()
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)
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.Cancel)
@@ -234,7 +293,8 @@ class AmcrFilterDialog(QDialog):
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
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_layout = QHBoxLayout()
@@ -249,30 +309,42 @@ class AmcrFilterDialog(QDialog):
btn = QPushButton("Vybrat...")
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():
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:
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
# Update the UI text field with selected names
# Update the display field with the selected item names
if labels:
display_field.setText(", ".join(labels))
else:
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':
display_field.setText("odchylka jednotky metrů, odchylka desítky metrů, odchylka stovky metrů")
self.selection_cache[cache_key] = ['HES-000861', 'HES-000862', 'HES-000863']
display_field.setText(
"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)
row_layout.addWidget(display_field)
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:
row_layout.addWidget(extra_btn)
@@ -280,20 +352,27 @@ class AmcrFilterDialog(QDialog):
return row_widget
def action_update_heslare(self):
# Vytvoření instance tasku
# Create the task instance
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.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,
"Hotovo",
"Hesláře byly úspěšně aktualizovány."
))
# Ošetření, aby se přesně ukázala případná chyba
# Show the exact error if the task fails
def on_error():
if task.exception:
# Tohle ti přesně řekne, na čem to teď padá (např. PermissionError)
msg = f"Aktualizace selhala z důvodu chyby:\n{str(task.exception)}"
# This will show exactly what went wrong (e.g. PermissionError)
msg = (
"Aktualizace selhala z důvodu chyby:\n"
f"{str(task.exception)}"
)
else:
msg = "Aktualizace byla zrušena uživatelem."
QMessageBox.warning(self, "Chyba / Zrušeno", msg)
@@ -309,7 +388,8 @@ class AmcrFilterDialog(QDialog):
return "true" if self.chk_komponenty.isChecked() else "false"
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 = {}
if self.selection_cache['kraj']:
@@ -349,9 +429,11 @@ class AmcrFilterDialog(QDialog):
return filters
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
(DPAPI on Windows, Keychain on macOS, encrypted SQLite on Linux).
@@ -365,6 +447,9 @@ class LoginDialog(QDialog):
- storeAuthenticationConfig() and loadAuthenticationConfig() both have
SIP_INOUT on their config parameter, so Python bindings return a tuple
(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"
@@ -379,17 +464,29 @@ class LoginDialog(QDialog):
# Check whether a config ID is already stored from a previous session.
# 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, "")
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:
info = QLabel("✔ 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 = QLabel(
"✔ 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;")
else:
info = QLabel("Zadejte přihlašovací údaje k Digitálnímu archivu AMČR.\n"
"Budou zašifrovaně uloženy ve správci autentizace QGIS.")
info = QLabel(
"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)
layout.addWidget(info)
layout.addSpacing(8)
@@ -406,7 +503,9 @@ class LoginDialog(QDialog):
self.txt_pass = QLineEdit()
self.txt_pass.setEchoMode(QLineEdit.EchoMode.Password)
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)
@@ -422,7 +521,8 @@ class LoginDialog(QDialog):
layout.addStretch(1)
buttons = QDialogButtonBox(
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
QDialogButtonBox.StandardButton.Ok
| QDialogButtonBox.StandardButton.Cancel
)
buttons.accepted.connect(self._save_and_accept)
buttons.rejected.connect(self.reject)
@@ -438,7 +538,8 @@ class LoginDialog(QDialog):
def _load_config(config_id: str, full: bool = False):
"""
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.
"""
try:
@@ -453,8 +554,10 @@ class LoginDialog(QDialog):
return False, QgsAuthMethodConfig()
def _load_username_from_config(self, config_id: str) -> str:
"""Load just the username from a stored config (no password decryption)."""
ok, cfg = self._load_config(config_id, full=False)
"""Load the username from a stored config.
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 ""
def _ensure_master_password(self) -> bool:
@@ -469,11 +572,13 @@ class LoginDialog(QDialog):
QMessageBox.critical(
self, "Správce autentizace nedostupný",
"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
# 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):
return False # User cancelled the master password dialog
@@ -488,7 +593,11 @@ class LoginDialog(QDialog):
password = self.txt_pass.text()
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
existing_id = QSettings().value(self.SETTINGS_KEY, "")
@@ -521,13 +630,19 @@ class LoginDialog(QDialog):
settings = QSettings()
# Try to update an existing config first; fall back to creating a new one.
# We skip hasConfigId() as it may return False despite the config existing
# Try to update an existing config first;
# 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).
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:
cfg.setId(existing_id)
ok, cfg = auth_mgr.updateAuthenticationConfig(cfg)
ok = auth_mgr.updateAuthenticationConfig(cfg)
else:
ok, cfg = auth_mgr.storeAuthenticationConfig(cfg)
@@ -536,7 +651,8 @@ class LoginDialog(QDialog):
if not ok or not config_id:
QMessageBox.critical(
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."
)
return
@@ -550,7 +666,11 @@ class LoginDialog(QDialog):
if existing_id:
QgsApplication.authManager().removeAuthenticationConfig(existing_id)
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()
# ------------------------------------------------------------------
@@ -565,8 +685,9 @@ class LoginDialog(QDialog):
Note: hasConfigId() is intentionally skipped here it checks an
in-memory cache that may lag behind the actual database contents,
causing false negatives (see class docstring). loadAuthenticationConfig()
is called directly and its return value is used as the authoritative result.
causing false negatives (see class docstring).
loadAuthenticationConfig() is called directly and its return value is
used as the authoritative result.
"""
settings = QSettings()
config_id = settings.value(LoginDialog.SETTINGS_KEY, "")
+294 -97
View File
@@ -1,7 +1,8 @@
# -*- coding: utf-8 -*-
from qgis.core import (QgsProject, QgsVectorLayer, QgsFeature, QgsGeometry,
QgsField, QgsCoordinateReferenceSystem, QgsCoordinateTransform,
QgsWkbTypes, Qgis, QgsApplication, QgsAuthMethodConfig, QgsMessageLog)
QgsField, QgsCoordinateReferenceSystem,
QgsCoordinateTransform, QgsWkbTypes, Qgis,
QgsMessageLog)
from qgis.utils import iface
from qgis.PyQt.QtCore import Qt, QMetaType
from qgis.PyQt.QtWidgets import QApplication
@@ -12,25 +13,33 @@ import json
# Global cache to store translated terms from the Digital Archive
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
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)
def login_to_api(username: str, password: str):
"""
Přihlásí se do Digiarchiv API pomocí username a hesla.
Vrátí requests.Session s nastavenou session cookie, nebo None při chybě.
Logs in to the Digiarchiv API using a username and password.
Returns a requests.Session with the session cookie set, or None on error.
"""
login_url = "https://digiarchiv.aiscr.cz/api/user/login"
_log(f"Přihlašuji uživatele: '{username}'")
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
session = requests.Session()
@@ -42,14 +51,22 @@ def login_to_api(username: str, password: str):
try:
_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}")
response.raise_for_status()
# API vrací chyby se status 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()
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
_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:
_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
except requests.exceptions.RequestException as e:
_log(f"CHYBA sítě: {e}", Qgis.MessageLevel.Critical)
return None
def _get_session() -> requests.Session | None:
"""
Vrátí aktiv session. Pokud žádná není (restart QGIS), pokusí se
automaticky přihlásit pomocí uložených přihlašovacích údajů.
Vrátí None pokud přihlašovací údaje nejsou uloženy.
Returns the active session. If none exists (e.g. after a QGIS restart),
attempts automatic login using stored credentials.
Returns None if no credentials are stored.
"""
global AMCR_SESSION
if AMCR_SESSION is not None:
return AMCR_SESSION
# Zkusit auto-login pomocí uložených údajů
# Attempt auto-login using stored credentials
from .amcr_dialog import LoginDialog
username, password = LoginDialog.get_credentials()
if username and password:
@@ -87,19 +106,24 @@ def _get_session() -> requests.Session | None:
def _api_get(url, params, timeout=30) -> requests.Response:
"""
Provede GET request. Pokud API signalizuje vypršení přihlášení,
provede jedno opakované přihlášení a zkusí znovu.
Performs a GET request. If the API signals an expired login,
re-authenticates once and retries.
"""
global AMCR_SESSION
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:
return True
try:
body = resp.json()
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:
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)
if _is_auth_error(resp):
_log("Session vypršela během stahování obnovuji přihlášení...", Qgis.MessageLevel.Warning)
AMCR_SESSION = None # Zrušit starou session
_log("Session vypršela během stahování obnovuji přihlášení...",
Qgis.MessageLevel.Warning)
AMCR_SESSION = None # Invalidate the old session
from .amcr_dialog import LoginDialog
username, password = LoginDialog.get_credentials()
if username and password:
@@ -116,14 +141,20 @@ def _api_get(url, params, timeout=30) -> requests.Response:
if AMCR_SESSION:
resp = AMCR_SESSION.get(url, params=params, timeout=timeout)
else:
_log("Opakované přihlášení selhalo.", Qgis.MessageLevel.Critical)
_log("Opakované přihlášení selhalo.",
Qgis.MessageLevel.Critical)
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
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
if TRANSLATIONS:
return
@@ -136,20 +167,31 @@ def load_translations():
except Exception as e:
print(f"Error downloading vocabulary: {e}")
def tr_code(code):
"""Translates a technical code into a human-readable string using the global cache."""
"""
Translates a technical code into a human-readable string
using the global cache.
"""
if not code:
return ""
return TRANSLATIONS.get(code, code)
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
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 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:
1. Determines search area (Bounding Box)
@@ -159,19 +201,28 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
load_translations()
# --- 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()
crs_src = canvas.mapSettings().destinationCrs()
crs_dest = QgsCoordinateReferenceSystem("EPSG:4326")
xform = QgsCoordinateTransform(crs_src, crs_dest, QgsProject.instance())
extent_wgs = xform.transformBoundingBox(extent)
# Format the bounding box string as required by the API: minLat,minLon,maxLat,maxLon
bbox_str = f"{extent_wgs.yMinimum()},{extent_wgs.xMinimum()},{extent_wgs.yMaximum()},{extent_wgs.xMaximum()}"
# Format the bounding box string as required by the API:
# 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"
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))
try:
@@ -189,7 +240,8 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
if bb == "true":
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:
for key, value in filters.items():
if not value:
@@ -204,12 +256,15 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
BATCH_DOCS = 500 # Records per API request
MAX_LIMIT = 20000 # Safety limit to prevent QGIS from freezing
seen_ids = set()
target_pian_ids_count = 0
# 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
filter_areal = "f_areal" in filters if filters else False
@@ -242,12 +297,19 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
new_docs.append(d)
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:
break
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
current_page += 1
@@ -258,26 +320,31 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
break
if not docs:
iface.messageBar().pushMessage("AMCR", "Žádné záznamy nenalezeny.", level=Qgis.MessageLevel.Warning)
iface.messageBar().pushMessage(
"AMCR",
"Žádné záznamy nenalezeny.",
level=Qgis.MessageLevel.Warning
)
return
# ==========================================
# 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 = {}
target_pian_ids = set()
actions_with_geom = 0
# Helper function for safe single-value extraction
# Helper: safely extract a single value
def g(doc, key, default=""):
val = doc.get(key)
if isinstance(val, list):
return str(val[0]) if val 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):
val = doc.get(key, [])
if not isinstance(val, list):
@@ -294,15 +361,22 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
actions_with_geom += 1
# Extract protected data (fields not available in public Solr index)
# Extract protected fields
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')
or doc.get('lokalita_chranene_udaje')
or {}
)
# Format additional cadastral areas from dictionaries
# Format additional cadastral areas from nested dicts
dalsi_kat = az_chranene.get('dalsi_katastr', [])
dalsi_kat_str = ""
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])
lokalizace = chranene.get('lokalizace_okolnosti', "")
@@ -322,31 +396,70 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
# Add entity-specific metadata
if typ_dat == "akce":
meta.update({
"akce_hlavni_vedouci": g(doc, 'akce_hlavni_vedouci'),
"akce_organizace": tr_code(g(doc, 'akce_organizace')),
"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",
"akce_hlavni_vedouci": g(
doc,
'akce_hlavni_vedouci'
),
"akce_organizace": tr_code(g(
doc,
'akce_organizace'
)),
"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":
meta.update({
"lokalita_nazev": lokalita_nazev,
"lokalita_popis": lokalita_popis,
"lokalita_zachovalost": tr_code(g(doc, 'lokalita_zachovalost')),
"lokalita_druh": tr_code(g(doc, 'lokalita_druh')),
"lokalita_typ": tr_code(g(doc, 'lokalita_typ_lokality')),
"lokalita_zachovalost": tr_code(g(
doc,
'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
djs = doc.get('az_dokumentacni_jednotka', [])
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:
continue
@@ -355,18 +468,28 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
if filter_areal or filter_datace:
if not komps:
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
dj_id = dj.get('ident_cely')
dj_typ = dj.get('dj_typ')
# Merge general meta with documentation unit specific data
# Merge shared metadata with documentation unit-specific fields
dj_meta = {
**meta,
'dj_id': dj_id,
'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)
@@ -379,22 +502,36 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
pian_lookup[dj_pian_value] = []
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:
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
komp_meta = {
**dj_meta,
'komponenta_id': komp.get('ident_cely', ""),
'komponenta_areal': komp.get('komponenta_areal', {}).get('value', ""),
'komponenta_obdobi': komp.get('komponenta_obdobi', {}).get('value', ""),
'komponenta_id': komp.get(
'ident_cely',
""
),
'komponenta_areal': komp.get(
'komponenta_areal',
{}
).get('value', ""),
'komponenta_obdobi': komp.get(
'komponenta_obdobi',
{}
).get('value', ""),
}
pian_lookup[dj_pian_value].append(komp_meta)
target_pian_ids_count += 1
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:
continue
@@ -410,26 +547,39 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
target_pian_ids_count += 1
pian_lookup[dj_pian_value].append(dj_meta)
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(
"AMCR",
f"Nalezeno {len(docs)} záznamů, ale žádný nemá geometrii.",
level=Qgis.MessageLevel.Warning
)
return
# ==========================================
# C) GEOMETRY FETCHING (PIAN)
# ==========================================
ids_list = list(target_pian_ids)
total_pians = len(ids_list)
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:
BATCH_PIAN = 200
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)
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"]
fl_pian = [
"ident_cely", "pian_typ",
"pian_chranene_udaje", "pian_presnost"
]
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)
fq_pian = f"ident_cely:({or_query})"
@@ -455,9 +605,21 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
archeologicky_zaznam = "Akce" if typ_dat == "akce" else "Lokalita"
# Initialize three layers for different geometry types (S-JTSK CRS)
vl_poly = QgsVectorLayer("Polygon?crs=epsg:5514", 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")
vl_poly = QgsVectorLayer(
"Polygon?crs=epsg:5514",
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]
# Define attribute table structure
@@ -564,7 +726,11 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
raw = doc.get('pian_chranene_udaje')
if isinstance(raw, list) and raw:
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
if jdata.get('geom_sjtsk_wkt'):
@@ -576,7 +742,12 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
pian_typ = tr_code(str(doc.get('pian_typ', '')))
# 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
if wkt:
@@ -586,41 +757,54 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
target_list = None
if t == QgsWkbTypes.PolygonGeometry:
target_list = feats_p
referenced_layer = vl_poly
elif t == QgsWkbTypes.LineGeometry:
target_list = feats_l
referenced_layer = vl_line
elif t == QgsWkbTypes.PointGeometry:
target_list = feats_pt
referenced_layer = vl_point
if target_list is None:
continue
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:
feat = QgsFeature()
feat.setGeometry(geom)
atributy = [
pid, pian_presnost, pian_typ, 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']
pid,
pian_presnost,
pian_typ,
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:
atributy.extend([
meta['lokalizace_okolnosti'], meta['akce_hlavni_vedouci'],
meta['akce_organizace'], meta['akce_specifikace_data'],
meta['akce_datum_zahajeni'], meta['akce_datum_ukonceni'],
meta['akce_hlavni_typ'], meta['akce_vedlejsi_typ'],
meta['dj_negativni'], meta['akce_je_nz']
meta['lokalizace_okolnosti'],
meta['akce_hlavni_vedouci'],
meta['akce_organizace'],
meta['akce_specifikace_data'],
meta['akce_datum_zahajeni'],
meta['akce_datum_ukonceni'],
meta['akce_hlavni_typ'],
meta['akce_vedlejsi_typ'],
meta['dj_negativni'],
meta['akce_je_nz']
])
else:
atributy.extend([
meta['lokalita_nazev'], meta['lokalita_popis'],
meta['lokalita_typ'], meta['lokalita_druh'],
meta['lokalita_nazev'],
meta['lokalita_popis'],
meta['lokalita_typ'],
meta['lokalita_druh'],
meta['lokalita_zachovalost']
])
@@ -658,12 +842,25 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
added += len(f)
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:
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:
iface.messageBar().pushMessage("Chyba", str(e), level=Qgis.MessageLevel.Critical)
iface.messageBar().pushMessage(
"Chyba",
str(e),
level=Qgis.MessageLevel.Critical
)
finally:
# Always restore cursor, even after failure
QApplication.restoreOverrideCursor()
+34 -13
View File
@@ -3,13 +3,13 @@ from qgis.PyQt.QtCore import QSettings, QTranslator, QCoreApplication
from qgis.PyQt.QtGui import QIcon
from qgis.PyQt.QtWidgets import QMenu, QAction, QToolButton, QDialog
from qgis.core import Qgis
from qgis.utils import iface
from .amcr_tools import load_amcr_data, login_to_api
from .amcr_dialog import AmcrFilterDialog, LoginDialog
from .resources import *
import os.path
class AmcrViewer:
"""
Main plugin class that manages the GUI elements, menu entries,
@@ -29,9 +29,11 @@ class AmcrViewer:
locale_path = os.path.join(
self.plugin_dir,
'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):
self.translator = QTranslator()
self.translator.load(locale_path)
@@ -43,7 +45,10 @@ class AmcrViewer:
self.first_start = None
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)
def add_action(self, icon_path, text, callback, enabled_flag=True,
@@ -71,7 +76,8 @@ class AmcrViewer:
if add_to_menu:
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:
self.actions.append(action)
@@ -90,7 +96,8 @@ class AmcrViewer:
self.plugin_menu = QMenu()
# 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(
icon_path=icon_akce_path,
text=self.tr(u'Stáhnout data akcí | AMČR Viewer'),
@@ -123,7 +130,11 @@ class AmcrViewer:
# 3. Create the main project action and attach the menu to it
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.iface.addPluginToMenu(self.menu, self.main_action)
@@ -132,9 +143,12 @@ class AmcrViewer:
self.tool_button = QToolButton()
self.tool_button.setMenu(self.plugin_menu)
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.first_start = True
@@ -172,13 +186,15 @@ class AmcrViewer:
dlg = AmcrFilterDialog(typ_dat)
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:
filters = dlg.get_filters()
bbox = dlg.get_bbox()
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()
load_amcr_data(canvas, bbox, filters, typ_dat, komponenty)
@@ -190,9 +206,14 @@ class AmcrViewer:
session = login_to_api(username, password)
if session:
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:
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
)