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')
|
||||
|
||||
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,30 +35,35 @@ 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 = {}
|
||||
|
||||
|
||||
path = os.path.join(CODELISTS_DIR, filename)
|
||||
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
try:
|
||||
# Open the file using standard UTF-8 encoding
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
reader = csv.reader(f, delimiter=';')
|
||||
|
||||
|
||||
# Skip the CSV header row
|
||||
next(reader, None)
|
||||
|
||||
next(reader, None)
|
||||
|
||||
# Iterate through rows and extract label, code, and category
|
||||
for row in reader:
|
||||
if len(row) >= 3:
|
||||
@@ -66,18 +71,24 @@ def parse_codelist_file(filename, target_dict=None):
|
||||
code = row[1].strip()
|
||||
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 = {
|
||||
@@ -92,42 +104,60 @@ def fetch_set(internal_name, api_set, task=None):
|
||||
"metadataPrefix": "oai_dc",
|
||||
"set": api_set
|
||||
}
|
||||
|
||||
|
||||
while True:
|
||||
# Kontrola zrušení v každém kroku
|
||||
# Check for cancellation at each iteration
|
||||
if task and task.isCanceled():
|
||||
return None
|
||||
|
||||
try:
|
||||
response = requests.get(BASE_URL, params=params, timeout=30)
|
||||
response.raise_for_status()
|
||||
root = ET.fromstring(response.content) # nosec
|
||||
|
||||
root = ET.fromstring(response.content) # nosec
|
||||
|
||||
records = root.findall('.//oai:record', NS)
|
||||
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 ""
|
||||
|
||||
# Název (title) - filtrujeme systémové popisky "AMČR - ..."
|
||||
# Code (identifier)
|
||||
identifier_el = metadata.find('dc:identifier', NS)
|
||||
kod = (
|
||||
identifier_el.text
|
||||
if identifier_el is not None
|
||||
else ""
|
||||
)
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
specialni_pripady = ['okres', 'katastr']
|
||||
|
||||
if internal_name in specialni_pripady:
|
||||
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 = {
|
||||
@@ -145,40 +175,45 @@ def fetch_set(internal_name, api_set, task=None):
|
||||
time.sleep(0.5)
|
||||
else:
|
||||
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()
|
||||
all_data = []
|
||||
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)
|
||||
|
||||
# Nyní předáváme task správně do upravené funkce
|
||||
QgsMessageLog.logMessage(
|
||||
f"Zpracovávám kategorii: {interni}...",
|
||||
"AMČR", Qgis.Info)
|
||||
|
||||
# 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,13 +222,11 @@ 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
|
||||
|
||||
"""Reloads data from files into the global variables."""
|
||||
data = load_all_data()
|
||||
|
||||
|
||||
OBDOBI.clear()
|
||||
OBDOBI.update(data.get('obdobi', {}))
|
||||
TYP_AKCE.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 = {}
|
||||
@@ -240,4 +273,4 @@ JISTOTA = {}
|
||||
LOKALITA_ZACHOVALOST = {}
|
||||
PRISTUPNOST = {}
|
||||
|
||||
refresh_globals()
|
||||
refresh_globals()
|
||||
|
||||
+237
-116
@@ -1,16 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from qgis.PyQt.QtWidgets import (QDialog, QVBoxLayout,
|
||||
QLineEdit, QDialogButtonBox,
|
||||
from qgis.PyQt.QtWidgets import (QDialog, QVBoxLayout,
|
||||
QLineEdit, QDialogButtonBox,
|
||||
QCheckBox, QGroupBox, QPushButton,
|
||||
QListWidget, QListWidgetItem, QHBoxLayout,
|
||||
QMessageBox, QLabel, QFormLayout)
|
||||
from qgis.PyQt.QtCore import Qt, QSettings
|
||||
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,
|
||||
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,
|
||||
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,52 +31,61 @@ 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):
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle(f"Výběr: {title}")
|
||||
self.resize(400, 500)
|
||||
|
||||
|
||||
# Store the source data and previously selected items
|
||||
self.data_dict = data_dict
|
||||
self.preselected = preselected_codes if preselected_codes else []
|
||||
|
||||
|
||||
layout = QVBoxLayout()
|
||||
|
||||
|
||||
# Setup search input for filtering items
|
||||
self.search_bar = QLineEdit()
|
||||
self.search_bar.setPlaceholderText("Hledat v seznamu...")
|
||||
self.search_bar.textChanged.connect(self.filter_list)
|
||||
layout.addWidget(self.search_bar)
|
||||
|
||||
|
||||
# Main list widget for displaying selectable items
|
||||
self.list_widget = QListWidget()
|
||||
self.populate_list()
|
||||
layout.addWidget(self.list_widget)
|
||||
|
||||
|
||||
# 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)
|
||||
layout.addWidget(buttons)
|
||||
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
def populate_list(self):
|
||||
@@ -82,19 +94,19 @@ class FilterableSelectionDialog(QDialog):
|
||||
for name in sorted_names:
|
||||
code = self.data_dict[name]
|
||||
item = QListWidgetItem(name)
|
||||
|
||||
|
||||
# Store the actual code (ID) hidden in the UserRole
|
||||
item.setData(Qt.ItemDataRole.UserRole, code)
|
||||
|
||||
|
||||
# Make the item checkable (adds a checkbox)
|
||||
item.setFlags(item.flags() | Qt.ItemFlag.ItemIsUserCheckable)
|
||||
|
||||
|
||||
# Restore previous selection state
|
||||
if code in self.preselected:
|
||||
item.setCheckState(Qt.CheckState.Checked)
|
||||
else:
|
||||
item.setCheckState(Qt.CheckState.Unchecked)
|
||||
|
||||
|
||||
self.list_widget.addItem(item)
|
||||
|
||||
def filter_list(self, text):
|
||||
@@ -125,20 +137,21 @@ class AmcrFilterDialog(QDialog):
|
||||
super().__init__(parent)
|
||||
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': []
|
||||
'organizace': [], 'kraj': [], 'obdobi': [], 'areal': [],
|
||||
'typ_akce': [], 'okres': [], 'katastr': [], 'vedouci': [],
|
||||
'pian_presnost': [], 'pristupnost': [], 'typ_lokality': [],
|
||||
'druh_lokality': [], 'jistota': [], 'lokalita_zachovalost': []
|
||||
}
|
||||
|
||||
|
||||
layout = QVBoxLayout()
|
||||
|
||||
|
||||
# Filter by current map canvas extent
|
||||
self.chk_bbox = QCheckBox("Omezit vyhledávání rozsahem okna")
|
||||
self.chk_bbox.setChecked(True)
|
||||
@@ -149,9 +162,9 @@ class AmcrFilterDialog(QDialog):
|
||||
if self.typ_dat == "akce":
|
||||
self.chk_posevidence = QCheckBox("Pouze pozitivní zjištění")
|
||||
layout.addWidget(self.chk_posevidence)
|
||||
|
||||
|
||||
layout.addSpacing(10)
|
||||
|
||||
|
||||
# Spatial information – valid for all
|
||||
|
||||
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)
|
||||
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
|
||||
|
||||
self.picker_obdobi = self.setup_picker("Období", 'obdobi', OBDOBI)
|
||||
layout.addWidget(self.picker_obdobi)
|
||||
|
||||
|
||||
self.picker_areal = self.setup_picker("Areál", 'areal', AREAL)
|
||||
layout.addWidget(self.picker_areal)
|
||||
|
||||
# Option to download related components table
|
||||
self.chk_komponenty = QCheckBox("Načíst komponenty")
|
||||
layout.addWidget(self.chk_komponenty)
|
||||
|
||||
|
||||
# Pushes everything above to the top
|
||||
layout.addStretch(1)
|
||||
|
||||
# Main dialog OK/Cancel/Update buttons
|
||||
|
||||
|
||||
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)
|
||||
|
||||
buttons.accepted.connect(self.accept)
|
||||
buttons.rejected.connect(self.reject)
|
||||
layout.addWidget(buttons)
|
||||
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
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.
|
||||
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.
|
||||
"""
|
||||
row_widget = QGroupBox(label_text)
|
||||
row_widget = QGroupBox(label_text)
|
||||
row_layout = QHBoxLayout()
|
||||
row_layout.setContentsMargins(5, 5, 5, 5)
|
||||
|
||||
|
||||
# Read-only field displaying the names of selected items
|
||||
display_field = QLineEdit()
|
||||
display_field.setReadOnly(True)
|
||||
display_field.setPlaceholderText("Nic nevybráno (vše)")
|
||||
display_field.setStyleSheet("background-color: #f0f0f0; color: #333;")
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
row_widget.setLayout(row_layout)
|
||||
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."))
|
||||
|
||||
# Ošetření, aby se přesně ukázala případná chyba
|
||||
|
||||
task.taskCompleted.connect(lambda: QMessageBox.information(
|
||||
self,
|
||||
"Hotovo",
|
||||
"Hesláře byly úspěšně aktualizovány."
|
||||
))
|
||||
|
||||
# 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)
|
||||
|
||||
|
||||
task.taskTerminated.connect(on_error)
|
||||
|
||||
|
||||
QgsApplication.taskManager().addTask(task)
|
||||
|
||||
def get_bbox(self):
|
||||
return "true" if self.chk_bbox.isChecked() else "false"
|
||||
|
||||
|
||||
def get_komponenty(self):
|
||||
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']:
|
||||
@@ -323,10 +403,10 @@ class AmcrFilterDialog(QDialog):
|
||||
if self.selection_cache['areal']:
|
||||
filters['f_areal'] = self.selection_cache['areal']
|
||||
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']:
|
||||
filters['pristupnost'] = self.selection_cache['pristupnost']
|
||||
|
||||
filters['pristupnost'] = self.selection_cache['pristupnost']
|
||||
|
||||
if self.typ_dat == "akce":
|
||||
if self.chk_posevidence.isChecked():
|
||||
filters['posevidence'] = 'true'
|
||||
@@ -346,12 +426,14 @@ class AmcrFilterDialog(QDialog):
|
||||
filters['f_jistota'] = self.selection_cache['jistota']
|
||||
if self.selection_cache['lokalita_zachovalost']:
|
||||
filters['f_lokalita_zachovalost'] = self.selection_cache['lokalita_zachovalost']
|
||||
|
||||
|
||||
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,10 +447,13 @@ 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"
|
||||
CONFIG_NAME = "AMČR Viewer"
|
||||
CONFIG_NAME = "AMČR Viewer"
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
@@ -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, "")
|
||||
@@ -578,4 +699,4 @@ class LoginDialog(QDialog):
|
||||
if not ok:
|
||||
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 -*-
|
||||
from qgis.core import (QgsProject, QgsVectorLayer, QgsFeature, QgsGeometry,
|
||||
QgsField, QgsCoordinateReferenceSystem, QgsCoordinateTransform,
|
||||
QgsWkbTypes, Qgis, QgsApplication, QgsAuthMethodConfig, QgsMessageLog)
|
||||
from qgis.core import (QgsProject, QgsVectorLayer, QgsFeature, QgsGeometry,
|
||||
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 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()
|
||||
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í aktivní 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,18 +141,24 @@ 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
|
||||
|
||||
return
|
||||
|
||||
url = "https://digiarchiv.aiscr.cz/api/assets/i18n/cs.json"
|
||||
try:
|
||||
r = requests.get(url, timeout=10)
|
||||
@@ -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."""
|
||||
if not code:
|
||||
"""
|
||||
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,26 +201,35 @@ 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:
|
||||
# ==========================================
|
||||
# A) METADATA FETCHING (Fieldwork/Site)
|
||||
# ==========================================
|
||||
|
||||
|
||||
base_params = {
|
||||
"mapa": "true",
|
||||
"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
|
||||
if bb == "true":
|
||||
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:
|
||||
@@ -200,21 +252,24 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
|
||||
base_params[key] = str(value).strip()
|
||||
|
||||
docs = []
|
||||
current_page = 0
|
||||
current_page = 0
|
||||
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
|
||||
filter_datace = "f_obdobi" in filters if filters else False
|
||||
|
||||
|
||||
# --- API PAGINATION LOOP ---
|
||||
while True:
|
||||
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
|
||||
elif 'page' in base_params:
|
||||
del base_params['page']
|
||||
|
||||
|
||||
try:
|
||||
resp_docs = _api_get(url, params=base_params, timeout=30)
|
||||
resp_json = resp_docs.json()
|
||||
data = resp_json.get('response', {})
|
||||
batch_docs = data.get('docs', [])
|
||||
num_found = data.get('numFound', 0)
|
||||
|
||||
num_found = data.get('numFound', 0)
|
||||
|
||||
if not batch_docs:
|
||||
break
|
||||
|
||||
|
||||
# Filter out duplicates and append to main list
|
||||
new_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:
|
||||
seen_ids.add(ident)
|
||||
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
|
||||
QApplication.processEvents() # Keep UI responsive
|
||||
|
||||
QApplication.processEvents() # Keep UI responsive
|
||||
|
||||
except Exception as e:
|
||||
print(f"Chyba při stránkování na straně {current_page}: {e}")
|
||||
break
|
||||
|
||||
if not docs:
|
||||
iface.messageBar().pushMessage("AMCR", "Žádné záznamy nenalezeny.", level=Qgis.MessageLevel.Warning)
|
||||
return
|
||||
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
|
||||
def g(doc, key, default=""):
|
||||
|
||||
# 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):
|
||||
@@ -293,16 +360,23 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
|
||||
continue
|
||||
|
||||
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 {}
|
||||
|
||||
# Format additional cadastral areas from dictionaries
|
||||
chranene = (
|
||||
doc.get('akce_chranene_udaje')
|
||||
or doc.get('lokalita_chranene_udaje')
|
||||
or {}
|
||||
)
|
||||
|
||||
# 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', "")
|
||||
@@ -314,7 +388,7 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
|
||||
"ident_cely": doc.get('ident_cely', ''),
|
||||
"az_okres": g(doc, 'az_okres'),
|
||||
"katastr": g_list(doc, 'katastr'),
|
||||
"dalsi_katastr": dalsi_kat_str,
|
||||
"dalsi_katastr": dalsi_kat_str,
|
||||
"pristupnost": g(doc, 'pristupnost'),
|
||||
"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
|
||||
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
|
||||
|
||||
komps = dj.get('dj_komponenta', [])
|
||||
|
||||
|
||||
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)
|
||||
dj_pian = dj.get('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] = []
|
||||
|
||||
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
|
||||
|
||||
|
||||
empty_meta = {
|
||||
**dj_meta,
|
||||
'komponenta_id': "",
|
||||
@@ -410,11 +547,13 @@ 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)
|
||||
return
|
||||
|
||||
iface.messageBar().pushMessage(
|
||||
"AMCR",
|
||||
f"Nalezeno {len(docs)} záznamů, ale žádný nemá geometrii.",
|
||||
level=Qgis.MessageLevel.Warning
|
||||
)
|
||||
return
|
||||
|
||||
# ==========================================
|
||||
# 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)
|
||||
total_pians = len(ids_list)
|
||||
docs_pian = []
|
||||
BATCH_PIAN = 200 # 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)
|
||||
|
||||
fl_pian = ["ident_cely", "pian_typ", "pian_chranene_udaje", "pian_presnost"]
|
||||
# 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). "
|
||||
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):
|
||||
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})"
|
||||
|
||||
|
||||
params_pian = {
|
||||
"mapa": "true",
|
||||
"entity": "pian",
|
||||
@@ -441,7 +591,7 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
|
||||
"fl": ",".join(fl_pian)
|
||||
}
|
||||
try:
|
||||
QApplication.processEvents()
|
||||
QApplication.processEvents()
|
||||
r_pian = _api_get(url, params=params_pian, timeout=15)
|
||||
batch_docs = r_pian.json().get('response', {}).get('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)
|
||||
# ==========================================
|
||||
|
||||
|
||||
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
|
||||
@@ -486,7 +648,7 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
|
||||
QgsField("ukonceni", QMetaType.Type.QString),
|
||||
QgsField("hlavni_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),
|
||||
]
|
||||
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("zachovalost", QMetaType.Type.QString)
|
||||
]
|
||||
|
||||
|
||||
cols.append(QgsField("Přístupnost", QMetaType.Type.QString))
|
||||
|
||||
# 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_areal": "Areál",
|
||||
"komponenta_obdobi": "Období",
|
||||
}
|
||||
}
|
||||
|
||||
if komponenty == "true":
|
||||
cols += [
|
||||
@@ -547,36 +709,45 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
|
||||
idx = vl.fields().lookupField(tech_name)
|
||||
if idx != -1:
|
||||
vl.setFieldAlias(idx, alias)
|
||||
|
||||
|
||||
# Lists to hold features before batch-adding to layers
|
||||
feats_p, feats_l, feats_pt = [], [], []
|
||||
|
||||
|
||||
# --- FEATURE POPULATION ---
|
||||
for doc in docs_pian:
|
||||
try:
|
||||
pid = doc.get('ident_cely', '')
|
||||
if pid not in pian_lookup:
|
||||
continue
|
||||
|
||||
continue
|
||||
|
||||
metas = pian_lookup[pid]
|
||||
|
||||
|
||||
# Extract WKT geometry from protected JSON data
|
||||
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'):
|
||||
wkt = jdata.get('geom_sjtsk_wkt', {}).get('value')
|
||||
elif jdata.get('geom_wkt'):
|
||||
wkt = jdata.get('geom_wkt', {}).get('value')
|
||||
|
||||
|
||||
pian_presnost = tr_code(str(doc.get('pian_presnost', '')))
|
||||
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']
|
||||
])
|
||||
|
||||
@@ -631,21 +815,21 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
|
||||
meta.get('komponenta_id', ""),
|
||||
meta.get('komponenta_areal', ""),
|
||||
meta.get('komponenta_obdobi', ""),
|
||||
])
|
||||
|
||||
])
|
||||
|
||||
feat.setAttributes(atributy)
|
||||
target_list.append(feat)
|
||||
|
||||
|
||||
except Exception as ex:
|
||||
print(f"Chyba při tvorbě feature: {ex}")
|
||||
pass
|
||||
|
||||
|
||||
# --- ADDING TO QGIS INTERFACE ---
|
||||
proj = QgsProject.instance()
|
||||
added = 0
|
||||
layers_to_process = [
|
||||
(feats_p, vl_poly, "Polygony"),
|
||||
(feats_l, vl_line, "Linie"),
|
||||
(feats_p, vl_poly, "Polygony"),
|
||||
(feats_l, vl_line, "Linie"),
|
||||
(feats_pt, vl_point, "Body"),
|
||||
]
|
||||
|
||||
@@ -653,17 +837,30 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
|
||||
if f:
|
||||
l.dataProvider().addFeatures(f)
|
||||
l.updateExtents()
|
||||
l.setName(f"AMCR_{archeologicky_zaznam}_{n}")
|
||||
l.setName(f"AMCR_{archeologicky_zaznam}_{n}")
|
||||
proj.addMapLayer(l)
|
||||
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()
|
||||
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.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,
|
||||
Main plugin class that manages the GUI elements, menu entries,
|
||||
and coordinates the flow between user input and data processing.
|
||||
"""
|
||||
|
||||
|
||||
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).
|
||||
"""
|
||||
self.iface = iface
|
||||
self.plugin_dir = os.path.dirname(__file__)
|
||||
|
||||
|
||||
# Determine the user's locale to load appropriate translation files
|
||||
locale = QSettings().value('locale/userLocale')[0:2]
|
||||
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,14 +45,17 @@ 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,
|
||||
add_to_menu=True, add_to_toolbar=True, status_tip=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.
|
||||
"""
|
||||
icon = QIcon(icon_path)
|
||||
@@ -71,15 +76,16 @@ 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)
|
||||
|
||||
|
||||
return action
|
||||
|
||||
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.
|
||||
"""
|
||||
# Define paths for action-specific icons
|
||||
@@ -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'),
|
||||
@@ -104,7 +111,7 @@ class AmcrViewer:
|
||||
self.action_download_lokality = self.add_action(
|
||||
icon_path=icon_lokality_path,
|
||||
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(),
|
||||
add_to_menu=False,
|
||||
add_to_toolbar=False
|
||||
@@ -114,7 +121,7 @@ class AmcrViewer:
|
||||
self.action_login_dialog = self.add_action(
|
||||
icon_path=icon_akce_path,
|
||||
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(),
|
||||
add_to_menu=False,
|
||||
add_to_toolbar=False
|
||||
@@ -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,16 +143,19 @@ 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)
|
||||
|
||||
# Add the widget directly to the toolbar and store the reference for cleanup
|
||||
self.tool_button.setPopupMode(
|
||||
QToolButton.ToolButtonPopupMode.MenuButtonPopup
|
||||
)
|
||||
|
||||
# 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
|
||||
|
||||
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.
|
||||
"""
|
||||
# 1. Remove the custom entry from the main 'Plugins' menu
|
||||
@@ -157,7 +171,7 @@ class AmcrViewer:
|
||||
self.iface.removePluginMenu(self.menu, action)
|
||||
self.iface.removeToolBarIcon(action)
|
||||
self.actions.clear()
|
||||
|
||||
|
||||
# 4. Reset map tools if currently active
|
||||
if hasattr(self, 'tool'):
|
||||
self.iface.mapCanvas().unsetMapTool(self.tool)
|
||||
@@ -165,20 +179,22 @@ class AmcrViewer:
|
||||
# --- Data downloading ---
|
||||
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.
|
||||
"""
|
||||
# Open the specific filter dialog (Projects vs Sites)
|
||||
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
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user