mirror of
https://github.com/ARUP-CAS/aiscr-qgis-amcr-viewer.git
synced 2026-06-17 11:22:53 +02:00
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:
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||||
Reference in New Issue
Block a user