Optimalizace výkonu a refaktorování GUI komponent (#22)

Comprehensive update to improve plugin efficiency and code quality:

- Performance: Increased BATCH_PIAN to 200 and optimized attribute parsing loops.
- Performance: Optimized codelist caching to reload only necessary data.
- UI/UX: Fixed plugin unloading (toolbar icon duplication) and added safe cursor handling.
- Refactoring: Moved GUI helper methods to class level for better OOP structure.
- Modernization: Updated dialog execution syntax to modern PyQt5/6 standards.
- Documentation: Added full inline English documentation across all modules.
This commit is contained in:
2026-03-11 11:37:15 +01:00
committed by GitHub
parent 158f623574
commit 3957b87a2b
4 changed files with 415 additions and 316 deletions
+66 -98
View File
@@ -1,64 +1,60 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import os import os
import csv import csv
import codecs
import requests import requests
import json
# Cesta k adresáři pluginu # Define paths for the plugin and its codelists directory
PLUGIN_DIR = os.path.dirname(__file__) PLUGIN_DIR = os.path.dirname(__file__)
CODELISTS_DIR = os.path.join(PLUGIN_DIR, 'codelists') CODELISTS_DIR = os.path.join(PLUGIN_DIR, 'codelists')
def ensure_codelists_dir(): def ensure_codelists_dir():
"""Creates the codelists directory if it does not exist."""
if not os.path.exists(CODELISTS_DIR): if not os.path.exists(CODELISTS_DIR):
os.makedirs(CODELISTS_DIR) os.makedirs(CODELISTS_DIR)
# --- 1. NAČÍTÁNÍ DAT --- def parse_codelist_file(filename, target_dict=None):
"""Reads a CSV codelist file and populates the target dictionary grouped by categories."""
def load_csv_data(filename): if target_dict is None:
"""Obecná funkce pro načtení CSV souboru do slovníku""" target_dict = {}
data = {}
path = os.path.join(CODELISTS_DIR, filename) path = os.path.join(CODELISTS_DIR, filename)
if not os.path.exists(path):
return data # Return early if the file doesn't exist to avoid missing file errors
if not os.path.exists(path):
return target_dict
try: try:
with codecs.open(path, 'r', 'utf-8') as f: # Open the file using standard UTF-8 encoding
with open(path, 'r', encoding='utf-8') as f:
reader = csv.reader(f, delimiter=';') reader = csv.reader(f, delimiter=';')
# Zkusíme přeskočit hlavičku, pokud tam je
first_row = next(reader, None)
# Pokud soubor není prázdný, zpracujeme ho # Skip the CSV header row
if first_row: next(reader, None)
# Pokud první řádek vypadá jako data (neobsahuje slovo "Název"), vrátíme ho do hry
# Ale my budeme generovat soubory s hlavičkou, takže OK. # Iterate through rows and extract label, code, and category
pass
for row in reader: for row in reader:
if len(row) >= 3: if len(row) >= 3:
label = row[0].strip() label = row[0].strip()
code = row[1].strip() code = row[1].strip()
category = row[2].strip() cat = row[2].strip()
clean = code if code else None
# Tady můžeme filtrovat podle kategorie, # Initialize a new dictionary for a category if encountered for the first time
# nebo prostě vrátit všechno jako {label: code} if cat not in target_dict:
# Pro jednoduchost vracíme {label: code} target_dict[cat] = {}
clean_code = code if code else None
data[label] = clean_code # Assign the extracted code to the corresponding label within the category
target_dict[cat][label] = clean
except Exception as e: except Exception as e:
print(f"AMČR Chyba čtení {filename}: {e}") print(f"AMČR Codelist Read Error for {filename}: {e}")
return data return target_dict
def load_all_data(): def load_all_data():
""" """Loads all static and dynamic codelists during plugin startup."""
Načte statický heslář I dynamický heslář vedoucích.
Vrací slovník slovníků.
"""
ensure_codelists_dir() ensure_codelists_dir()
# 1. Načteme hlavní statický heslář # Initialize the base structure with empty dictionaries for all expected categories
# Musíme ho rozparsovat podle kategorií, tak jak to bylo předtím
categorized_data = { categorized_data = {
'obdobi': {}, 'typ_akce': {}, 'areal': {}, 'obdobi': {}, 'typ_akce': {}, 'areal': {},
'kraj': {}, 'organizace': {}, 'okres': {}, 'katastr': {}, 'kraj': {}, 'organizace': {}, 'okres': {}, 'katastr': {},
@@ -66,77 +62,50 @@ def load_all_data():
'jistota': {}, 'lokalita_zachovalost': {} 'jistota': {}, 'lokalita_zachovalost': {}
} }
# Funkce pro roztřídění načteného slovníku (tohle je trochu redundance, ale pro zachování logiky) # Parse the default static codelist and the dynamically generated leaders codelist
def parse_file(filename): parse_codelist_file('heslar.csv', categorized_data)
path = os.path.join(CODELISTS_DIR, filename) parse_codelist_file('vedouci.csv', categorized_data)
if not os.path.exists(path): return
try:
with codecs.open(path, 'r', 'utf-8') as f:
reader = csv.reader(f, delimiter=';')
next(reader, None) # Skip header
for row in reader:
if len(row) >= 3:
label = row[0].strip()
code = row[1].strip()
cat = row[2].strip()
clean = code if code else None
if cat in categorized_data:
categorized_data[cat][label] = clean
except: pass
# Načteme soubory
parse_file('heslar.csv') # Statické
parse_file('vedouci.csv') # Dynamické (pokud existuje)
return categorized_data return categorized_data
# --- 2. AKTUALIZACE DAT (DOWNLOAD) ---
def download_vedouci(): def download_vedouci():
""" """Fetches the list of leaders from the AMČR API and saves it to a CSV file."""
Stáhne seznam vedoucích z API (pomocí onlyFacets) a uloží do codelists/vedouci.csv.
"""
ensure_codelists_dir() ensure_codelists_dir()
# Tvá URL + pojistka, abychom dostali všechny záznamy (limit -1) # API endpoint for fetching facet data for leaders
url = "https://digiarchiv.aiscr.cz/api/search/query?entity=akce&sort=datestamp%20desc&page=0&onlyFacets=True&rows=0" url = "https://digiarchiv.aiscr.cz/api/search/query?entity=akce&sort=datestamp%20desc&page=0&onlyFacets=True&rows=0"
try: try:
r = requests.get(url, timeout=20) # Raději delší timeout pro velký seznam # Execute the GET request with a 20-second timeout
r = requests.get(url, timeout=20)
r.raise_for_status() r.raise_for_status()
data = r.json() data = r.json()
# Cesta k datům dle tvého JSONu: # Extract the leaders list from the JSON response using safe dict getters
# {"facet_counts": { "f_vedouci": [ {"name": "Novák", ...}, ... ] }}
vedouci_list = data.get('facet_counts', {}).get('f_vedouci', []) vedouci_list = data.get('facet_counts', {}).get('f_vedouci', [])
if not vedouci_list: if not vedouci_list:
# Zkusíme ještě alternativní cestu, kdyby API vrátilo standardní Solr strukturu
# (facet_counts -> facet_fields -> f_vedouci)
vedouci_list = data.get('facet_counts', {}).get('facet_fields', {}).get('f_vedouci', []) vedouci_list = data.get('facet_counts', {}).get('facet_fields', {}).get('f_vedouci', [])
csv_path = os.path.join(CODELISTS_DIR, 'vedouci.csv') csv_path = os.path.join(CODELISTS_DIR, 'vedouci.csv')
count = 0 count = 0
with codecs.open(csv_path, 'w', 'utf-8') as f:
# Open the target CSV file for writing without extra blank lines
with open(csv_path, 'w', encoding='utf-8', newline='') as f:
writer = csv.writer(f, delimiter=';') writer = csv.writer(f, delimiter=';')
# Write the standard header required by the parser function
writer.writerow(['Název', 'Kód', 'Kategorie']) writer.writerow(['Název', 'Kód', 'Kategorie'])
# NOVÁ LOGIKA PARSOVÁNÍ # Iterate through the API results and format them for the CSV
for item in vedouci_list: for item in vedouci_list:
name = None name = None
# Varianta A: Položka je slovník {"name": "Jan Novák", "value": 10}
if isinstance(item, dict): if isinstance(item, dict):
name = item.get('name') name = item.get('name')
# Varianta B: Položka je jen string (kdyby se API vrátilo k plochému seznamu)
elif isinstance(item, str): elif isinstance(item, str):
name = item name = item
# Pokud máme jméno a není to číslo (count), zapíšeme # Ignore pure numbers (which are usually counts) and write valid names
if name and not str(name).isdigit(): if name and not str(name).isdigit():
writer.writerow([name, name, 'vedouci']) writer.writerow([name, name, 'vedouci'])
count += 1 count += 1
@@ -146,33 +115,32 @@ def download_vedouci():
except Exception as e: except Exception as e:
return False, str(e) return False, str(e)
# --- GLOBAL DATA --- # Initialize global codelist data when the module is imported
# Toto se načte při startu QGISu
_DATA = load_all_data() _DATA = load_all_data()
OBDOBI = _DATA['obdobi'] # Safely extract individual categories into global variables for easy access across the plugin
TYP_AKCE = _DATA['typ_akce'] OBDOBI = _DATA.get('obdobi', {})
AREAL = _DATA['areal'] TYP_AKCE = _DATA.get('typ_akce', {})
KRAJE = _DATA['kraj'] AREAL = _DATA.get('areal', {})
ORGANIZACE = _DATA['organizace'] KRAJE = _DATA.get('kraj', {})
OKRESY = _DATA['okres'] ORGANIZACE = _DATA.get('organizace', {})
KATASTRY = _DATA['katastr'] OKRESY = _DATA.get('okres', {})
VEDOUCI = _DATA['vedouci'] KATASTRY = _DATA.get('katastr', {})
PIAN_PRESNOST = _DATA['pian_presnost'] VEDOUCI = _DATA.get('vedouci', {})
TYP_LOKALITY = _DATA['typ_lokality'] PIAN_PRESNOST = _DATA.get('pian_presnost', {})
DRUH_LOKALITY = _DATA['druh_lokality'] TYP_LOKALITY = _DATA.get('typ_lokality', {})
JISTOTA = _DATA['jistota'] DRUH_LOKALITY = _DATA.get('druh_lokality', {})
LOKALITA_ZACHOVALOST = _DATA['lokalita_zachovalost'] JISTOTA = _DATA.get('jistota', {})
LOKALITA_ZACHOVALOST = _DATA.get('lokalita_zachovalost', {})
def refresh_vedouci_cache(): def refresh_vedouci_cache():
""" """Reloads only the 'vedouci.csv' file to quickly update the cache without full initialization."""
Znovu načte soubor vedouci.csv a aktualizuje globální proměnnou VEDOUCI. # Parse only the targeted file containing the updated leaders
Použijeme 'update', aby se zachovala reference na objekt (pokud ho dialog už používá). temp_data = parse_codelist_file('vedouci.csv')
""" new_vedouci = temp_data.get('vedouci', {})
temp_data = load_all_data()
new_vedouci = temp_data['vedouci']
# Vyčistíme a naplníme existující slovník (in-place update) # Clear the existing global dictionary and update it with the fresh data
VEDOUCI.clear() VEDOUCI.clear()
VEDOUCI.update(new_vedouci) VEDOUCI.update(new_vedouci)
return len(VEDOUCI) return len(VEDOUCI)
+121 -74
View File
@@ -11,40 +11,62 @@ from .amcr_codelists import (OBDOBI, TYP_AKCE, KRAJE, AREAL, ORGANIZACE,
download_vedouci, refresh_vedouci_cache) download_vedouci, refresh_vedouci_cache)
class FilterableSelectionDialog(QDialog): class FilterableSelectionDialog(QDialog):
"""
A custom dialog for selecting multiple items from a list with a search filter.
"""
def __init__(self, title, data_dict, preselected_codes, parent=None): def __init__(self, title, data_dict, preselected_codes, parent=None):
super().__init__(parent) super().__init__(parent)
self.setWindowTitle(f"Výběr: {title}") self.setWindowTitle(f"Výběr: {title}")
self.resize(400, 500) self.resize(400, 500)
# Store the source data and previously selected items
self.data_dict = data_dict self.data_dict = data_dict
self.preselected = preselected_codes if preselected_codes else [] self.preselected = preselected_codes if preselected_codes else []
layout = QVBoxLayout() layout = QVBoxLayout()
# Setup search input for filtering items
self.search_bar = QLineEdit() self.search_bar = QLineEdit()
self.search_bar.setPlaceholderText("Hledat v seznamu...") self.search_bar.setPlaceholderText("Hledat v seznamu...")
self.search_bar.textChanged.connect(self.filter_list) self.search_bar.textChanged.connect(self.filter_list)
layout.addWidget(self.search_bar) layout.addWidget(self.search_bar)
# Main list widget for displaying selectable items
self.list_widget = QListWidget() self.list_widget = QListWidget()
self.populate_list() self.populate_list()
layout.addWidget(self.list_widget) layout.addWidget(self.list_widget)
# Standard OK/Cancel dialog buttons
buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
buttons.accepted.connect(self.accept) buttons.accepted.connect(self.accept)
buttons.rejected.connect(self.reject) buttons.rejected.connect(self.reject)
layout.addWidget(buttons) layout.addWidget(buttons)
self.setLayout(layout) self.setLayout(layout)
def populate_list(self): def populate_list(self):
# Sort items alphabetically by their display name
sorted_names = sorted(self.data_dict.keys()) sorted_names = sorted(self.data_dict.keys())
for name in sorted_names: for name in sorted_names:
code = self.data_dict[name] code = self.data_dict[name]
item = QListWidgetItem(name) item = QListWidgetItem(name)
# Store the actual code (ID) hidden in the UserRole
item.setData(Qt.UserRole, code) item.setData(Qt.UserRole, code)
# Make the item checkable (adds a checkbox)
item.setFlags(item.flags() | Qt.ItemIsUserCheckable) item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
# Restore previous selection state
if code in self.preselected: if code in self.preselected:
item.setCheckState(Qt.Checked) item.setCheckState(Qt.Checked)
else: else:
item.setCheckState(Qt.Unchecked) item.setCheckState(Qt.Unchecked)
self.list_widget.addItem(item) self.list_widget.addItem(item)
def filter_list(self, text): def filter_list(self, text):
# Hide items that don't match the search text (case-insensitive)
search_text = text.lower() search_text = text.lower()
for i in range(self.list_widget.count()): for i in range(self.list_widget.count()):
item = self.list_widget.item(i) item = self.list_widget.item(i)
@@ -54,6 +76,7 @@ class FilterableSelectionDialog(QDialog):
item.setHidden(False) item.setHidden(False)
def get_selected_codes(self): def get_selected_codes(self):
"""Returns the hidden codes and display labels of all checked items."""
codes = [] codes = []
labels = [] labels = []
for i in range(self.list_widget.count()): for i in range(self.list_widget.count()):
@@ -66,15 +89,20 @@ class FilterableSelectionDialog(QDialog):
# --- Main window --- # --- Main window ---
class AmcrFilterDialog(QDialog): class AmcrFilterDialog(QDialog):
"""
The main filtering UI where users set criteria before downloading data.
"""
def __init__(self, typ_dat, parent=None): def __init__(self, typ_dat, parent=None):
super(AmcrFilterDialog, self).__init__(parent) super(AmcrFilterDialog, self).__init__(parent)
self.setWindowTitle("Filtr AMČR") self.setWindowTitle("Filtr AMČR")
self.resize(500, 750) self.resize(500, 750)
# Determines if we are fetching 'akce' (projects) or 'lokalita' (locations)
self.typ_dat = typ_dat self.typ_dat = typ_dat
# Cache for filtering # Cache dictionary to store selected codes for each category
self.selection_cache = { self.selection_cache = {
'organizace': [], 'kraj': [], 'obdobi': [], 'areal': [], 'organizace': [], 'kraj': [], 'obdobi': [], 'areal': [],
'typ_akce': [], 'okres': [], 'katastr': [], 'vedouci': [], 'pian_presnost': [], 'typ_akce': [], 'okres': [], 'katastr': [], 'vedouci': [], 'pian_presnost': [],
@@ -83,6 +111,7 @@ class AmcrFilterDialog(QDialog):
layout = QVBoxLayout() layout = QVBoxLayout()
# Filter by current map canvas extent
self.chk_bbox = QCheckBox("Omezit vyhledávání rozsahem okna") self.chk_bbox = QCheckBox("Omezit vyhledávání rozsahem okna")
self.chk_bbox.setChecked(True) self.chk_bbox.setChecked(True)
layout.addWidget(self.chk_bbox) layout.addWidget(self.chk_bbox)
@@ -94,14 +123,91 @@ class AmcrFilterDialog(QDialog):
layout.addWidget(self.chk_posevidence) layout.addWidget(self.chk_posevidence)
layout.addSpacing(10) layout.addSpacing(10)
# Spatial information valid for all
def setup_picker(label_text, cache_key, data_source, extra_btn=None): self.picker_kraj = self.setup_picker("Kraj", 'kraj', KRAJE)
layout.addWidget(self.picker_kraj)
self.picker_okres = self.setup_picker("Okres", 'okres', OKRESY)
layout.addWidget(self.picker_okres)
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)
layout.addWidget(self.picker_presnost)
# Filters valid for Akce
if self.typ_dat == "akce":
self.picker_org = self.setup_picker("Organizace", 'organizace', ORGANIZACE)
layout.addWidget(self.picker_org)
# Button to fetch fresh project leaders from the API
self.btn_update_vedouci = QPushButton("🔄")
self.btn_update_vedouci.setToolTip("Aktualizovat seznam vedoucích z API")
self.btn_update_vedouci.setFixedWidth(30)
self.btn_update_vedouci.clicked.connect(self.action_update_vedouci)
self.picker_vedouci = self.setup_picker("Vedoucí výzkumu", 'vedouci', VEDOUCI, extra_btn=self.btn_update_vedouci)
layout.addWidget(self.picker_vedouci)
# Type of event
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)
layout.addWidget(self.picker_typ_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)
layout.addWidget(self.picker_jistota)
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 buttons
buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.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.
"""
row_widget = QGroupBox(label_text) row_widget = QGroupBox(label_text)
# row_widget.setFlat(True) # row_widget.setFlat(True)
row_layout = QHBoxLayout() row_layout = QHBoxLayout()
row_layout.setContentsMargins(5, 5, 5, 5) row_layout.setContentsMargins(5, 5, 5, 5)
# Read-only field displaying the names of selected items
display_field = QLineEdit() display_field = QLineEdit()
display_field.setReadOnly(True) display_field.setReadOnly(True)
display_field.setPlaceholderText("Nic nevybráno (vše)") display_field.setPlaceholderText("Nic nevybráno (vše)")
@@ -110,16 +216,22 @@ class AmcrFilterDialog(QDialog):
btn = QPushButton("Vybrat...") btn = QPushButton("Vybrat...")
btn.setFixedWidth(80) btn.setFixedWidth(80)
# Nested function that handles opening the dialog and saving results
def open_dialog(): def open_dialog():
dlg = FilterableSelectionDialog(label_text, data_source, self.selection_cache[cache_key], self) dlg = FilterableSelectionDialog(label_text, data_source, self.selection_cache[cache_key], self)
if dlg.exec_() == QDialog.Accepted: if dlg.exec() == QDialog.Accepted:
codes, labels = dlg.get_selected_codes() codes, labels = dlg.get_selected_codes()
# Update local cache with selected IDs
self.selection_cache[cache_key] = codes self.selection_cache[cache_key] = codes
# Update the UI text field with selected names
if labels: if labels:
display_field.setText(", ".join(labels)) display_field.setText(", ".join(labels))
else: else:
display_field.clear() display_field.clear()
# Special case: Pre-fill specific accuracy levels by default
if cache_key == 'pian_presnost': if cache_key == 'pian_presnost':
display_field.setText("odchylka jednotky metrů, odchylka desítky metrů, odchylka stovky metrů") display_field.setText("odchylka jednotky metrů, odchylka desítky metrů, odchylka stovky metrů")
self.selection_cache[cache_key] = ['HES-000861', 'HES-000862', 'HES-000863'] self.selection_cache[cache_key] = ['HES-000861', 'HES-000862', 'HES-000863']
@@ -129,94 +241,28 @@ class AmcrFilterDialog(QDialog):
row_layout.addWidget(display_field) row_layout.addWidget(display_field)
row_layout.addWidget(btn) row_layout.addWidget(btn)
# Add an optional extra button (e.g., the refresh button for leaders)
if extra_btn: if extra_btn:
row_layout.addWidget(extra_btn) row_layout.addWidget(extra_btn)
row_widget.setLayout(row_layout) row_widget.setLayout(row_layout)
return row_widget return row_widget
# Spatial information valid for all
self.picker_kraj = setup_picker("Kraj", 'kraj', KRAJE)
layout.addWidget(self.picker_kraj)
self.picker_okres = setup_picker("Okres", 'okres', OKRESY)
layout.addWidget(self.picker_okres)
self.picker_katastr = setup_picker("Katastr", 'katastr', KATASTRY)
layout.addWidget(self.picker_katastr)
self.picker_presnost = setup_picker("PIAN přesnost", 'pian_presnost', PIAN_PRESNOST)
layout.addWidget(self.picker_presnost)
# Filters valid for Akce
if self.typ_dat == "akce":
self.picker_org = setup_picker("Organizace", 'organizace', ORGANIZACE)
layout.addWidget(self.picker_org)
self.btn_update_vedouci = QPushButton("🔄")
self.btn_update_vedouci.setToolTip("Aktualizovat seznam vedoucích z API")
self.btn_update_vedouci.setFixedWidth(30)
self.btn_update_vedouci.clicked.connect(self.action_update_vedouci)
self.picker_vedouci = setup_picker("Vedoucí výzkumu", 'vedouci', VEDOUCI, extra_btn=self.btn_update_vedouci)
layout.addWidget(self.picker_vedouci)
# Type of event
self.picker_typ = 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 = setup_picker("Lokalita typ", 'typ_lokality', TYP_LOKALITY)
layout.addWidget(self.picker_typ_lokality)
self.picker_druh_lokality = setup_picker("Lokalita druh", 'druh_lokality', DRUH_LOKALITY)
layout.addWidget(self.picker_druh_lokality)
self.picker_jistota = setup_picker("Lokalita jistota určení", 'jistota', JISTOTA)
layout.addWidget(self.picker_jistota)
self.picker_lokalita_zachovalost = setup_picker("Lokalita - stav dochování", 'lokalita_zachovalost', LOKALITA_ZACHOVALOST)
layout.addWidget(self.picker_lokalita_zachovalost)
# Contextual information
self.picker_obdobi = setup_picker("Období", 'obdobi', OBDOBI)
layout.addWidget(self.picker_obdobi)
self.picker_areal = setup_picker("Areál", 'areal', AREAL)
layout.addWidget(self.picker_areal)
self.chk_komponenty = QCheckBox("Načíst komponenty")
layout.addWidget(self.chk_komponenty)
layout.addStretch(1)
buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
buttons.accepted.connect(self.accept)
buttons.rejected.connect(self.reject)
layout.addWidget(buttons)
self.setLayout(layout)
def action_update_vedouci(self): def action_update_vedouci(self):
# Change cursor to loading state to indicate background task
QApplication.setOverrideCursor(Qt.WaitCursor) QApplication.setOverrideCursor(Qt.WaitCursor)
try: try:
success, msg = download_vedouci() success, msg = download_vedouci()
if success: if success:
count = refresh_vedouci_cache() count = refresh_vedouci_cache()
QApplication.restoreOverrideCursor()
QMessageBox.information(self, "Úspěch", f"{msg}\nNyní je v paměti {count} osob.") QMessageBox.information(self, "Úspěch", f"{msg}\nNyní je v paměti {count} osob.")
else: else:
QApplication.restoreOverrideCursor()
QMessageBox.warning(self, "Chyba", f"Nepodařilo se stáhnout data:\n{msg}") QMessageBox.warning(self, "Chyba", f"Nepodařilo se stáhnout data:\n{msg}")
except Exception as e: except Exception as e:
QApplication.restoreOverrideCursor()
QMessageBox.critical(self, "Chyba", str(e)) QMessageBox.critical(self, "Chyba", str(e))
finally:
# Safely restore the normal cursor even if an error occurs
QApplication.restoreOverrideCursor()
def get_bbox(self): def get_bbox(self):
return "true" if self.chk_bbox.isChecked() else "false" return "true" if self.chk_bbox.isChecked() else "false"
@@ -225,6 +271,7 @@ class AmcrFilterDialog(QDialog):
return "true" if self.chk_komponenty.isChecked() else "false" return "true" if self.chk_komponenty.isChecked() else "false"
def get_filters(self): def get_filters(self):
"""Compiles the user selections from the cache into API-ready filter parameters."""
filters = {} filters = {}
if self.selection_cache['kraj']: if self.selection_cache['kraj']:
+166 -128
View File
@@ -11,11 +11,11 @@ import json
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
import re import re
# Global translations cache # Global cache to store translated terms from the Digital Archive
TRANSLATIONS = {} TRANSLATIONS = {}
# Download Digiarchive's vocabulary
def load_translations(): def load_translations():
"""Fetches the official Czech translation dictionary from the AISCR API."""
global TRANSLATIONS global TRANSLATIONS
if TRANSLATIONS: if TRANSLATIONS:
return return
@@ -26,22 +26,32 @@ def load_translations():
if r.status_code == 200: if r.status_code == 200:
TRANSLATIONS = r.json() TRANSLATIONS = r.json()
except Exception as e: except Exception as e:
print(f"Chyba při stahování hesláře: {e}") print(f"Error downloading vocabulary: {e}")
def tr_code(code): def tr_code(code):
"""Translates a technical code into a human-readable string using the global cache."""
if not code: if not code:
return "" return ""
return TRANSLATIONS.get(code, code) return TRANSLATIONS.get(code, code)
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)
2. Fetches metadata and geometries from API
3. Creates QGIS memory layers and populates them with features
"""
load_translations() load_translations()
# 1. Bounding box # --- 1. COORDINATE TRANSFORMATION ---
# Get current map extent and transform it from project CRS (usually S-JTSK) to WGS-84 for the API
extent = canvas.extent() extent = canvas.extent()
crs_src = canvas.mapSettings().destinationCrs() crs_src = canvas.mapSettings().destinationCrs()
crs_dest = QgsCoordinateReferenceSystem("EPSG:4326") crs_dest = QgsCoordinateReferenceSystem("EPSG:4326")
xform = QgsCoordinateTransform(crs_src, crs_dest, QgsProject.instance()) xform = QgsCoordinateTransform(crs_src, crs_dest, QgsProject.instance())
extent_wgs = xform.transformBoundingBox(extent) extent_wgs = xform.transformBoundingBox(extent)
# Format the bounding box string as required by the API: minLat,minLon,maxLat,maxLon
bbox_str = f"{extent_wgs.yMinimum()},{extent_wgs.xMinimum()},{extent_wgs.yMaximum()},{extent_wgs.xMaximum()}" bbox_str = f"{extent_wgs.yMinimum()},{extent_wgs.xMinimum()},{extent_wgs.yMaximum()},{extent_wgs.xMaximum()}"
url = "https://digiarchiv.aiscr.cz/api/search/query" url = "https://digiarchiv.aiscr.cz/api/search/query"
@@ -50,21 +60,21 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
QApplication.setOverrideCursor(Qt.WaitCursor) QApplication.setOverrideCursor(Qt.WaitCursor)
try: try:
# =================== # ==========================================
# A) METADATA (Fieldwork event/Site) # A) METADATA FETCHING (Fieldwork/Site)
# =================== # ==========================================
base_params = { base_params = {
"mapa": "true", "mapa": "true",
"sort": "ident_cely asc" "sort": "ident_cely asc",
"entity": typ_dat
} }
base_params["entity"] = typ_dat # Restrict search to map window if requested
if bb == "true": if bb == "true":
base_params["loc_rpt"] = bbox_str base_params["loc_rpt"] = bbox_str
# Apply filters # Apply multi-select filters from the dialog using the ':or' syntax required by the API
if filters: if filters:
for key, value in filters.items(): for key, value in filters.items():
if not value: if not value:
@@ -76,13 +86,17 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
docs = [] docs = []
current_page = 0 current_page = 0
BATCH_DOCS = 500 BATCH_DOCS = 500 # Records per API request
MAX_LIMIT = 20000 MAX_LIMIT = 20000 # Safety limit to prevent QGIS from freezing
feats_k = [] feats_k = [] # List for component features (non-spatial)
seen_ids = set() seen_ids = set()
target_pian_ids_count = 0 target_pian_ids_count = 0
# Check if we should skip negative results based on filter
skip_negativni = filters.get('posevidence') == 'true' if filters else False
# --- API PAGINATION LOOP ---
while True: while True:
base_params['rows'] = BATCH_DOCS base_params['rows'] = BATCH_DOCS
if current_page > 0: if current_page > 0:
@@ -100,6 +114,7 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
if not batch_docs: if not batch_docs:
break break
# Filter out duplicates and append to main list
new_docs = [] new_docs = []
for d in batch_docs: for d in batch_docs:
ident = d.get('ident_cely') ident = d.get('ident_cely')
@@ -117,7 +132,7 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
break break
current_page += 1 current_page += 1
QApplication.processEvents() QApplication.processEvents() # Keep UI responsive
except Exception as e: except Exception as e:
print(f"Chyba při stránkování na straně {current_page}: {e}") print(f"Chyba při stránkování na straně {current_page}: {e}")
@@ -128,12 +143,31 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
return return
# ========================================== # ==========================================
# Attribute parsing # B) ATTRIBUTE PARSING
# ========================================== # ==========================================
# pian_lookup maps a Geometry ID (PIAN) to a list of its associated metadata
pian_lookup = {} pian_lookup = {}
target_pian_ids = set() target_pian_ids = set()
actions_with_geom = 0 actions_with_geom = 0
# Helper function for safe single-value extraction
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
def g_list(doc, key, translate=False):
val = doc.get(key, [])
if not isinstance(val, list):
val = [val] if val else []
if translate:
return ", ".join([tr_code(str(x)) for x in val if x])
return ", ".join([str(x) for x in val if x])
# Process each downloaded metadata record
for doc in docs: for doc in docs:
piani = doc.get('az_dj_pian', []) piani = doc.get('az_dj_pian', [])
if not piani: if not piani:
@@ -141,23 +175,11 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
actions_with_geom += 1 actions_with_geom += 1
def g(key, default=""): # Extract protected data (fields not available in public Solr index)
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
def g_list(key, translate=False):
val = doc.get(key, [])
if not isinstance(val, list):
val = [val] if val else []
if translate:
return ", ".join([tr_code(str(x)) for x in val if x])
return ", ".join([str(x) for x in val if x])
az_chranene = doc.get('az_chranene_udaje', {}) az_chranene = doc.get('az_chranene_udaje', {})
chranene = doc.get('akce_chranene_udaje') or doc.get('lokalita_chranene_udaje') or {} chranene = doc.get('akce_chranene_udaje') or doc.get('lokalita_chranene_udaje') or {}
# Format additional cadastral areas from dictionaries
dalsi_kat = az_chranene.get('dalsi_katastr', []) dalsi_kat = az_chranene.get('dalsi_katastr', [])
dalsi_kat_str = "" dalsi_kat_str = ""
if isinstance(dalsi_kat, list): if isinstance(dalsi_kat, list):
@@ -168,25 +190,26 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
lokalita_nazev = chranene.get('nazev', "") lokalita_nazev = chranene.get('nazev', "")
lokalita_popis = chranene.get('popis', "") lokalita_popis = chranene.get('popis', "")
# Prepate common metadata # Core metadata structure
meta = { meta = {
"ident_cely": doc.get('ident_cely', ''), "ident_cely": doc.get('ident_cely', ''),
"az_okres": g('az_okres'), "az_okres": g(doc, 'az_okres'),
"katastr": g_list('katastr'), "katastr": g_list(doc, 'katastr'),
"dalsi_katastr": dalsi_kat_str, "dalsi_katastr": dalsi_kat_str,
"pristupnost": g('pristupnost'), "pristupnost": g(doc, 'pristupnost'),
"loc": g_list('loc') "loc": g_list(doc, 'loc')
} }
# Add entity-specific metadata
if typ_dat == "akce": if typ_dat == "akce":
meta.update({ meta.update({
"akce_hlavni_vedouci": g('akce_hlavni_vedouci'), "akce_hlavni_vedouci": g(doc, 'akce_hlavni_vedouci'),
"akce_organizace": tr_code(g('akce_organizace')), "akce_organizace": tr_code(g(doc, 'akce_organizace')),
"akce_specifikace_data": tr_code(g('akce_specifikace_data')), "akce_specifikace_data": tr_code(g(doc, 'akce_specifikace_data')),
"akce_datum_zahajeni": g('akce_datum_zahajeni'), "akce_datum_zahajeni": g(doc, 'akce_datum_zahajeni'),
"akce_datum_ukonceni": g('akce_datum_ukonceni'), "akce_datum_ukonceni": g(doc, 'akce_datum_ukonceni'),
"akce_hlavni_typ": tr_code(g('akce_hlavni_typ')), "akce_hlavni_typ": tr_code(g(doc, 'akce_hlavni_typ')),
"akce_vedlejsi_typ": g_list('akce_vedlejsi_typ', translate=True), "akce_vedlejsi_typ": g_list(doc, 'akce_vedlejsi_typ', translate=True),
"lokalizace_okolnosti": str(lokalizace) if lokalizace else "", "lokalizace_okolnosti": str(lokalizace) if lokalizace else "",
"akce_je_nz": "Ano" if doc.get('akce_je_nz') is True else "Ne", "akce_je_nz": "Ano" if doc.get('akce_je_nz') is True else "Ne",
}) })
@@ -195,33 +218,42 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
meta.update({ meta.update({
"lokalita_nazev": lokalita_nazev, "lokalita_nazev": lokalita_nazev,
"lokalita_popis": lokalita_popis, "lokalita_popis": lokalita_popis,
"lokalita_zachovalost": tr_code(g('lokalita_zachovalost')), "lokalita_zachovalost": tr_code(g(doc, 'lokalita_zachovalost')),
"lokalita_druh": tr_code(g('lokalita_druh')), "lokalita_druh": tr_code(g(doc, 'lokalita_druh')),
"lokalita_typ": tr_code(g('lokalita_typ_lokality')), "lokalita_typ": tr_code(g(doc, 'lokalita_typ_lokality')),
}) })
# Documentation units (DJ) within the record
djs = doc.get('az_dokumentacni_jednotka', []) djs = doc.get('az_dokumentacni_jednotka', [])
for dj in djs: for dj in djs:
if filters and filters.get('posevidence') == 'true' and dj.get('dj_negativni_jednotka') is True: # Filter out negative evidence units if requested
if skip_negativni and dj.get('dj_negativni_jednotka') is True:
continue continue
dj_meta = meta.copy()
dj_id = dj.get('ident_cely') dj_id = dj.get('ident_cely')
dj_meta['dj_id'] = dj_id
dj_typ = dj.get('dj_typ') dj_typ = dj.get('dj_typ')
dj_meta['dj_typ_value'] = dj_typ.get('value') if dj_typ else ""
dj_meta['dj_negativni'] = "Negativní" if dj.get('dj_negativni_jednotka') is True else "Pozitivní" # Merge general meta with documentation unit specific data
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í"
}
# Link Documentation Unit to Geometry (PIAN)
dj_pian = dj.get('dj_pian') dj_pian = dj.get('dj_pian')
if dj_pian: if dj_pian:
dj_pian_value = dj_pian.get('id') dj_pian_value = dj_pian.get('id')
if dj_pian_value: if dj_pian_value:
target_pian_ids.add(dj_pian_value) target_pian_ids.add(dj_pian_value)
target_pian_ids_count = target_pian_ids_count+1 target_pian_ids_count += 1
if dj_pian_value not in pian_lookup: if dj_pian_value not in pian_lookup:
pian_lookup[dj_pian_value] = [] pian_lookup[dj_pian_value] = []
pian_lookup[dj_pian_value].append(dj_meta) pian_lookup[dj_pian_value].append(dj_meta)
# Parse non-spatial components if requested (for relational tables)
if komponenty == "true": if komponenty == "true":
komps = dj.get('dj_komponenta', []) komps = dj.get('dj_komponenta', [])
for komp in komps: for komp in komps:
@@ -229,7 +261,6 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
atributy = [ atributy = [
komp.get('ident_cely', ""), komp.get('ident_cely', ""),
dj_id, dj_id,
# komponenta_aktivita ..?,
komp.get('komponenta_areal', {}).get('value', ""), komp.get('komponenta_areal', {}).get('value', ""),
komp.get('komponenta_obdobi', {}).get('value', "") komp.get('komponenta_obdobi', {}).get('value', "")
] ]
@@ -242,16 +273,15 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
# ========================================== # ==========================================
# B) Geometry (PIAN) # C) GEOMETRY FETCHING (PIAN)
# ========================================== # ==========================================
ids_list = list(target_pian_ids) ids_list = list(target_pian_ids)
total_pians = len(ids_list) total_pians = len(ids_list)
docs_pian = [] docs_pian = []
BATCH_PIAN = 50 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=1) 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=1)
# Seznam polí pro PIAN
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): for i in range(0, total_pians, BATCH_PIAN):
@@ -275,19 +305,18 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
print(f"Chyba PIAN: {e}") print(f"Chyba PIAN: {e}")
# ========================================== # ==========================================
# C) TVORBA VRSTEV # D) LAYER CREATION (QGIS Memory Layers)
# ========================================== # ==========================================
vl_poly = QgsVectorLayer("Polygon?crs=epsg:5514", "AMCR Plochy", "memory")
vl_line = QgsVectorLayer("LineString?crs=epsg:5514", "AMCR Linie", "memory")
vl_point = QgsVectorLayer("Point?crs=epsg:5514", "AMCR Body", "memory")
layers = [vl_poly, vl_line, vl_point]
if typ_dat == "akce": archeologicky_zaznam = "Akce" if typ_dat == "akce" else "Lokalita"
archeologicky_zaznam = "Akce"
elif typ_dat == "lokalita":
archeologicky_zaznam = "Lokalita"
# Definice sloupců atributové tabulky # 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")
layers = [vl_poly, vl_line, vl_point]
# Define attribute table structure
cols = [ cols = [
QgsField("PIAN", QVariant.String), QgsField("PIAN", QVariant.String),
QgsField("Přesnost", QVariant.String), QgsField("Přesnost", QVariant.String),
@@ -302,8 +331,10 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
QgsField("Další katastry", QVariant.String) QgsField("Další katastry", QVariant.String)
] ]
# Extend table based on data type
if typ_dat == "akce": if typ_dat == "akce":
cols += [ cols += [
QgsField("Akce lokalizace", QVariant.String),
QgsField("Vedoucí akce", QVariant.String), QgsField("Vedoucí akce", QVariant.String),
QgsField("Organizace", QVariant.String), QgsField("Organizace", QVariant.String),
QgsField("Specifikace data", QVariant.String), QgsField("Specifikace data", QVariant.String),
@@ -311,28 +342,36 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
QgsField("Datum ukončení", QVariant.String), QgsField("Datum ukončení", QVariant.String),
QgsField("Hlavní typ", QVariant.String), QgsField("Hlavní typ", QVariant.String),
QgsField("Vedlejší typ", QVariant.String), QgsField("Vedlejší typ", QVariant.String),
QgsField("Zjištění", QVariant.String), QgsField("Zjištění", QVariant.String),
QgsField("Akce lokalizace", QVariant.String),
QgsField("Akce nahrazuje NZ", QVariant.String), QgsField("Akce nahrazuje NZ", QVariant.String),
] ]
elif typ_dat == "lokalita": elif typ_dat == "lokalita":
cols += [ cols += [
QgsField("Název lokality", QVariant.String), QgsField("nazev_lokality", QVariant.String),
QgsField("Popis lokality", QVariant.String), QgsField("popis_lokality", QVariant.String),
QgsField("Typ lokality", QVariant.String), QgsField("typ_lokality", QVariant.String),
QgsField("Druh lokality", QVariant.String), QgsField("druh_lokality", QVariant.String),
QgsField("Zachovalost", QVariant.String) QgsField("zachovalost", QVariant.String)
] ]
cols.append(QgsField("Přístupnost", QVariant.String)) cols.append(QgsField("Přístupnost", QVariant.String))
# Use aliases for technical field names
alias_map = {
"nazev_lokality": "Název lokality",
"popis_lokality": "Popis lokality",
"typ_lokality": "Typ lokality",
"druh_lokality": "Druh lokality",
"zachovalost": "Zachovalost"
}
# Create a non-spatial table for components if requested
if komponenty == "true": if komponenty == "true":
vl_komponenty = QgsVectorLayer("None", "AMCR Komponenty", "memory") vl_komponenty = QgsVectorLayer("None", "AMCR Komponenty", "memory")
pr = vl_komponenty.dataProvider() pr = vl_komponenty.dataProvider()
komponenty_cols = [ komponenty_cols = [
QgsField("komponenta", QVariant.String), # ident_cely QgsField("komponenta", QVariant.String),
QgsField("dj_id", QVariant.String), QgsField("dj_id", QVariant.String),
# potenciálně QgsField("komponenta_aktivita", QVariant.String),
QgsField("komponenta_areal", QVariant.String), QgsField("komponenta_areal", QVariant.String),
QgsField("komponenta_obdobi", QVariant.String) QgsField("komponenta_obdobi", QVariant.String)
] ]
@@ -346,98 +385,97 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
for vl in layers: for vl in layers:
vl.dataProvider().addAttributes(cols) vl.dataProvider().addAttributes(cols)
vl.updateFields() vl.updateFields()
for tech_name, alias in alias_map.items():
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 = [], [], [] feats_p, feats_l, feats_pt = [], [], []
# --- FEATURE POPULATION ---
for doc in docs_pian: for doc in docs_pian:
try: try:
pid = doc.get('ident_cely', '') pid = doc.get('ident_cely', '')
if pid not in pian_lookup: if pid not in pian_lookup:
continue continue
metas = pian_lookup[pid] metas = pian_lookup[pid]
# Geometry processing # Extract WKT geometry from protected JSON data
raw = doc.get('pian_chranene_udaje') raw = doc.get('pian_chranene_udaje')
if isinstance(raw, list) and raw: if isinstance(raw, list) and raw:
raw = raw[0] raw = raw[0]
jdata = json.loads(raw) if isinstance(raw, str) else (raw if isinstance(raw, dict) else {}) jdata = json.loads(raw) if isinstance(raw, str) else (raw or {})
wkt = None wkt = None
if jdata.get('geom_sjtsk_wkt'): if jdata.get('geom_sjtsk_wkt'):
wkt = jdata['geom_sjtsk_wkt'].get('value') wkt = jdata.get('geom_sjtsk_wkt', {}).get('value')
elif jdata.get('geom_wkt'): elif jdata.get('geom_wkt'):
wkt = jdata['geom_wkt'].get('value') wkt = jdata.get('geom_wkt', {}).get('value')
# PIAN attributes
pian_presnost = tr_code(str(doc.get('pian_presnost', ''))) pian_presnost = tr_code(str(doc.get('pian_presnost', '')))
pian_typ = tr_code(str(doc.get('pian_typ', ''))) pian_typ = tr_code(str(doc.get('pian_typ', '')))
# Final precision filter check
if filters and filters.get('f_pian_presnost') and doc.get('pian_presnost') not in filters.get('f_pian_presnost'): if filters and filters.get('f_pian_presnost') and doc.get('pian_presnost') not in filters.get('f_pian_presnost'):
continue continue
if wkt: if wkt:
geom = QgsGeometry.fromWkt(wkt) geom = QgsGeometry.fromWkt(wkt)
if geom.isGeosValid(): if geom.isGeosValid():
t = geom.type()
target_list = None
if t == QgsWkbTypes.PolygonGeometry:
target_list = feats_p
elif t == QgsWkbTypes.LineGeometry:
target_list = feats_l
elif t == QgsWkbTypes.PointGeometry:
target_list = feats_pt
if target_list is None:
continue
is_akce = (typ_dat == "akce")
# Create a QGIS feature for each documentation unit associated with this geometry
for meta in metas: for meta in metas:
feat = QgsFeature() feat = QgsFeature()
feat.setGeometry(geom) feat.setGeometry(geom)
atributy = [ atributy = [
pid, pid, pian_presnost, pian_typ, meta['dj_id'],
pian_presnost, meta['dj_typ_value'], meta['loc'], meta['ident_cely'],
pian_typ,
meta['dj_id'],
meta['dj_typ_value'],
meta['loc'],
meta['ident_cely'],
"https://digiarchiv.aiscr.cz/id/" + meta['ident_cely'], "https://digiarchiv.aiscr.cz/id/" + meta['ident_cely'],
meta['az_okres'], meta['az_okres'], meta['katastr'], meta['dalsi_katastr']
meta['katastr'],
meta['dalsi_katastr']
] ]
if typ_dat == "akce": if is_akce:
atributy += [ atributy.extend([
meta['akce_hlavni_vedouci'], meta['lokalizace_okolnosti'], meta['akce_hlavni_vedouci'],
meta['akce_organizace'], meta['akce_organizace'], meta['akce_specifikace_data'],
meta['akce_specifikace_data'], meta['akce_datum_zahajeni'], meta['akce_datum_ukonceni'],
meta['akce_datum_zahajeni'], meta['akce_hlavni_typ'], meta['akce_vedlejsi_typ'],
meta['akce_datum_ukonceni'], meta['dj_negativni'], meta['akce_je_nz']
meta['akce_hlavni_typ'], ])
meta['akce_vedlejsi_typ'], else:
meta['dj_negativni'], atributy.extend([
meta['lokalizace_okolnosti'], meta['lokalita_nazev'], meta['lokalita_popis'],
meta['akce_je_nz'] meta['lokalita_typ'], meta['lokalita_druh'],
]
elif typ_dat == "lokalita":
atributy += [
meta['lokalita_nazev'],
meta['lokalita_popis'],
meta['lokalita_typ'],
meta['lokalita_druh'],
meta['lokalita_zachovalost'] meta['lokalita_zachovalost']
] ])
atributy.append(meta['pristupnost']) atributy.append(meta['pristupnost'])
feat.setAttributes(atributy) feat.setAttributes(atributy)
target_list.append(feat)
t = geom.type()
if t == QgsWkbTypes.PolygonGeometry:
feats_p.append(feat)
elif t == QgsWkbTypes.LineGeometry:
feats_l.append(feat)
elif t == QgsWkbTypes.PointGeometry:
feats_pt.append(feat)
except Exception as ex: except Exception as ex:
print(f"Chyba při tvorbě feature: {ex}") print(f"Chyba při tvorbě feature: {ex}")
pass pass
# --- ADDING TO QGIS INTERFACE ---
proj = QgsProject.instance() proj = QgsProject.instance()
added = 0 added = 0
layers_to_process = [ layers_to_process = [
(feats_p, vl_poly, "Plochy"), (feats_p, vl_poly, "Polygony"),
(feats_l, vl_line, "Linie"), (feats_l, vl_line, "Linie"),
(feats_pt, vl_point, "Body"), (feats_pt, vl_point, "Body"),
] ]
@@ -449,7 +487,7 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
if f: if f:
l.dataProvider().addFeatures(f) l.dataProvider().addFeatures(f)
l.updateExtents() l.updateExtents()
l.setName(f"AMČR {n} (Filtrováno)") l.setName(f"AMCR_{archeologicky_zaznam}_{n}")
proj.addMapLayer(l) proj.addMapLayer(l)
if n != "Komponenty": if n != "Komponenty":
added += len(f) added += len(f)
@@ -457,23 +495,22 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
if added > 0: if added > 0:
iface.messageBar().pushMessage("AMCR", f"Hotovo. Záznamů: {len(docs)} (s geom: {actions_with_geom}). Vykresleno: {added} prvků.", level=0) iface.messageBar().pushMessage("AMCR", f"Hotovo. Záznamů: {len(docs)} (s geom: {actions_with_geom}). Vykresleno: {added} prvků.", level=0)
# Relation # --- RELATIONSHIP MANAGEMENT ---
# Set up automatic links between spatial layers and the component table
if komponenty == "true": if komponenty == "true":
parent_layers = [ parent_layers = [
(vl_poly, "Plochy"), (vl_poly, "Polygony"),
(vl_line, "Linie"), (vl_line, "Linie"),
(vl_point, "Body") (vl_point, "Body")
] ]
rel_manager = proj.relationManager() rel_manager = proj.relationManager()
for parent_layer, label in parent_layers: for parent_layer, label in parent_layers:
rel = QgsRelation() rel = QgsRelation()
#rel_id = f"rel_{parent_layer.id()}_komponenty"
rel_name = f"Komponenty pro {label}" rel_name = f"Komponenty pro {label}"
#rel.setId(rel_id)
rel.setName(rel_name) rel.setName(rel_name)
rel.setReferencingLayer(vl_komponenty.id()) rel.setReferencingLayer(vl_komponenty.id())
rel.setReferencedLayer(parent_layer.id()) rel.setReferencedLayer(parent_layer.id())
rel.addFieldPair("dj_id", "Dokumentační jednotka") # Upravit název parent sloupce po změně názvů sloupců u vrstev akcí/lokalit rel.addFieldPair("dj_id", "Dokumentační jednotka")
rel.generateId() rel.generateId()
if rel.isValid(): if rel.isValid():
rel_manager.addRelation(rel) rel_manager.addRelation(rel)
@@ -486,4 +523,5 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
except Exception as e: except Exception as e:
iface.messageBar().pushMessage("Chyba", str(e), level=2) iface.messageBar().pushMessage("Chyba", str(e), level=2)
finally: finally:
QApplication.restoreOverrideCursor() # Always restore cursor, even after failure
QApplication.restoreOverrideCursor()
+62 -16
View File
@@ -9,31 +9,48 @@ from .resources import *
import os.path import os.path
class AmcrViewer: class AmcrViewer:
"""
Main plugin class that manages the GUI elements, menu entries,
and coordinates the flow between user input and data processing.
"""
def __init__(self, iface): def __init__(self, iface):
"""
Constructor initializes the connection to QGIS interface and sets up
internationalization (i18n).
"""
self.iface = iface self.iface = iface
self.plugin_dir = os.path.dirname(__file__) self.plugin_dir = os.path.dirname(__file__)
# Determine the user's locale to load appropriate translation files
locale = QSettings().value('locale/userLocale')[0:2] locale = QSettings().value('locale/userLocale')[0:2]
locale_path = os.path.join( locale_path = os.path.join(
self.plugin_dir, self.plugin_dir,
'i18n', 'i18n',
'AmcrViewer_{}.qm'.format(locale)) 'AmcrViewer_{}.qm'.format(locale))
# Install the translator if a translation file for the current locale exists
if os.path.exists(locale_path): if os.path.exists(locale_path):
self.translator = QTranslator() self.translator = QTranslator()
self.translator.load(locale_path) self.translator.load(locale_path)
QCoreApplication.installTranslator(self.translator) QCoreApplication.installTranslator(self.translator)
# Initialize internal state
self.actions = [] self.actions = []
self.menu = self.tr(u'&AMČR Viewer') self.menu = self.tr(u'&AMČR Viewer')
self.first_start = None self.first_start = None
def tr(self, message): def tr(self, message):
"""Helper method for translating strings within the AmcrViewer context."""
return QCoreApplication.translate('AmcrViewer', message) return QCoreApplication.translate('AmcrViewer', message)
def add_action(self, icon_path, text, callback, enabled_flag=True, def add_action(self, icon_path, text, callback, enabled_flag=True,
add_to_menu=True, add_to_toolbar=True, status_tip=None, add_to_menu=True, add_to_toolbar=True, status_tip=None,
whats_this=None, parent=None): whats_this=None, parent=None):
"""
Helper method to create QActions and automatically register them
into the QGIS Menu and Toolbar.
"""
icon = QIcon(icon_path) icon = QIcon(icon_path)
action = QAction(icon, text, parent) action = QAction(icon, text, parent)
action.triggered.connect(callback) action.triggered.connect(callback)
@@ -45,26 +62,33 @@ class AmcrViewer:
if whats_this is not None: if whats_this is not None:
action.setWhatsThis(whats_this) action.setWhatsThis(whats_this)
# Standard QGIS API for adding icons and menu items
if add_to_toolbar: if add_to_toolbar:
self.iface.addToolBarIcon(action) self.iface.addToolBarIcon(action)
if add_to_menu: if add_to_menu:
self.iface.addPluginToMenu(self.menu, action) self.iface.addPluginToMenu(self.menu, action)
self.actions.append(action) # 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 return action
def initGui(self): def initGui(self):
"""
import os Called when the plugin is loaded. Creates the menu structure,
plugin_dir = os.path.dirname(__file__) sub-actions, and the dropdown tool button in the toolbar.
icon_akce_path = os.path.join(plugin_dir, 'akce.png') """
icon_lokality_path = os.path.join(plugin_dir, 'lokality.png') # Define paths for action-specific icons
icon_akce_path = os.path.join(self.plugin_dir, 'akce.png')
icon_lokality_path = os.path.join(self.plugin_dir, 'lokality.png')
# 1. Vytvoření společného menu # 1. Create a container menu for the plugin
self.plugin_menu = QMenu() self.plugin_menu = QMenu()
# 2. Vytvoření akcí (bez automatického přidání do lišty a menu) # 2. Create sub-actions (Download Projects / Download Sites)
# add_to_menu/toolbar is False because these go into our custom dropdown menu
self.action_download_akce = self.add_action( self.action_download_akce = self.add_action(
icon_path=icon_akce_path, icon_path=icon_akce_path,
text=self.tr(u'Stáhnout data akcí | AMČR Viewer'), text=self.tr(u'Stáhnout data akcí | AMČR Viewer'),
@@ -85,41 +109,63 @@ class AmcrViewer:
) )
self.plugin_menu.addAction(self.action_download_lokality) self.plugin_menu.addAction(self.action_download_lokality)
# 3. Přidání rozbalovacího menu do hlavního menu QGIS # 3. Create the main project action and attach the menu to it
main_icon = QIcon(icon_akce_path) main_icon = QIcon(icon_akce_path)
self.main_action = QAction(main_icon, 'AMČR Viewer', self.iface.mainWindow()) self.main_action = QAction(main_icon, 'AMČR Viewer', self.iface.mainWindow())
self.main_action.setMenu(self.plugin_menu) self.main_action.setMenu(self.plugin_menu)
self.iface.addPluginToMenu(self.menu, self.main_action) self.iface.addPluginToMenu(self.menu, self.main_action)
# 4. Přidání rozevíracího tlačítka do nástrojové lišty (Toolbar) # 4. Create and configure a QToolButton for the QGIS Toolbar
# This button acts as a dropdown menu button (MenuButtonPopup)
self.tool_button = QToolButton() self.tool_button = QToolButton()
self.tool_button.setMenu(self.plugin_menu) self.tool_button.setMenu(self.plugin_menu)
self.tool_button.setDefaultAction(self.action_download_akce) self.tool_button.setDefaultAction(self.action_download_akce)
self.tool_button.setPopupMode(QToolButton.MenuButtonPopup) self.tool_button.setPopupMode(QToolButton.MenuButtonPopup)
# Vložení vytvořeného tlačítka do QGIS rozhraní # Add the widget directly to the toolbar and store the reference for cleanup
self.iface.addToolBarWidget(self.tool_button) self.toolbar_action = self.iface.addToolBarWidget(self.tool_button)
self.first_start = True self.first_start = True
def unload(self): def unload(self):
"""
Called when the plugin is disabled or removed.
Ensures all GUI elements are removed from QGIS to avoid ghost icons.
"""
# 1. Remove the custom entry from the main 'Plugins' menu
if hasattr(self, 'main_action'):
self.iface.removePluginMenu(self.menu, self.main_action)
# 2. Remove the custom QToolButton from the toolbar
if hasattr(self, 'toolbar_action'):
self.iface.removeToolBarIcon(self.toolbar_action)
# 3. Clean up any remaining actions registered in self.actions
for action in self.actions: for action in self.actions:
self.iface.removePluginMenu(self.tr(u'&AMČR Viewer'), action) self.iface.removePluginMenu(self.menu, action)
self.iface.removeToolBarIcon(action) self.iface.removeToolBarIcon(action)
self.actions.clear()
# 4. Reset map tools if currently active
if hasattr(self, 'tool'): if hasattr(self, 'tool'):
self.iface.mapCanvas().unsetMapTool(self.tool) self.iface.mapCanvas().unsetMapTool(self.tool)
# --- Data downloading --- # --- Data downloading ---
def run_download(self, typ_dat): def run_download(self, typ_dat):
"""
Triggered by menu/toolbar actions. Opens the filter dialog and
hands off the parameters to the data loader.
"""
# Open the specific filter dialog (Projects vs Sites)
dlg = AmcrFilterDialog(typ_dat) dlg = AmcrFilterDialog(typ_dat)
result = dlg.exec_() result = dlg.exec()
# If user confirmed the dialog (OK button), gather filters and load data
if result == 1: if result == 1:
filters = dlg.get_filters() filters = dlg.get_filters()
bbox = dlg.get_bbox() bbox = dlg.get_bbox()
komponenty = dlg.get_komponenty() komponenty = dlg.get_komponenty()
# Access the map canvas and start the fetch/render process from amcr_tools
canvas = self.iface.mapCanvas() canvas = self.iface.mapCanvas()
load_amcr_data(canvas, bbox, filters, typ_dat, komponenty) load_amcr_data(canvas, bbox, filters, typ_dat, komponenty)