2 Commits

Author SHA1 Message Date
david 8825ac3272 Update metadata.txt 2026-03-11 16:35:39 +01:00
david 3957b87a2b 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.
2026-03-11 11:37:15 +01:00
5 changed files with 416 additions and 317 deletions
+66 -98
View File
@@ -1,64 +1,60 @@
# -*- coding: utf-8 -*-
import os
import csv
import codecs
import requests
import json
# Cesta k adresáři pluginu
# Define paths for the plugin and its codelists directory
PLUGIN_DIR = os.path.dirname(__file__)
CODELISTS_DIR = os.path.join(PLUGIN_DIR, 'codelists')
def ensure_codelists_dir():
"""Creates the codelists directory if it does not exist."""
if not os.path.exists(CODELISTS_DIR):
os.makedirs(CODELISTS_DIR)
# --- 1. NAČÍTÁNÍ DAT ---
def load_csv_data(filename):
"""Obecná funkce pro načtení CSV souboru do slovníku"""
data = {}
def parse_codelist_file(filename, target_dict=None):
"""Reads a CSV codelist file and populates the target dictionary grouped by categories."""
if target_dict is None:
target_dict = {}
path = os.path.join(CODELISTS_DIR, filename)
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:
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=';')
# Zkusíme přeskočit hlavičku, pokud tam je
first_row = next(reader, None)
# Pokud soubor není prázdný, zpracujeme ho
if first_row:
# 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.
pass
# Skip the CSV header row
next(reader, None)
# Iterate through rows and extract label, code, and category
for row in reader:
if len(row) >= 3:
label = row[0].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,
# nebo prostě vrátit všechno jako {label: code}
# Pro jednoduchost vracíme {label: code}
clean_code = code if code else None
data[label] = clean_code
# Initialize a new dictionary for a category if encountered for the first time
if cat not in target_dict:
target_dict[cat] = {}
# Assign the extracted code to the corresponding label within the category
target_dict[cat][label] = clean
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():
"""
Načte statický heslář I dynamický heslář vedoucích.
Vrací slovník slovníků.
"""
"""Loads all static and dynamic codelists during plugin startup."""
ensure_codelists_dir()
# 1. Načteme hlavní statický heslář
# Musíme ho rozparsovat podle kategorií, tak jak to bylo předtím
# Initialize the base structure with empty dictionaries for all expected categories
categorized_data = {
'obdobi': {}, 'typ_akce': {}, 'areal': {},
'kraj': {}, 'organizace': {}, 'okres': {}, 'katastr': {},
@@ -66,77 +62,50 @@ def load_all_data():
'jistota': {}, 'lokalita_zachovalost': {}
}
# Funkce pro roztřídění načteného slovníku (tohle je trochu redundance, ale pro zachování logiky)
def parse_file(filename):
path = os.path.join(CODELISTS_DIR, filename)
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)
# Parse the default static codelist and the dynamically generated leaders codelist
parse_codelist_file('heslar.csv', categorized_data)
parse_codelist_file('vedouci.csv', categorized_data)
return categorized_data
# --- 2. AKTUALIZACE DAT (DOWNLOAD) ---
def download_vedouci():
"""
Stáhne seznam vedoucích z API (pomocí onlyFacets) a uloží do codelists/vedouci.csv.
"""
"""Fetches the list of leaders from the AMČR API and saves it to a CSV file."""
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"
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()
data = r.json()
# Cesta k datům dle tvého JSONu:
# {"facet_counts": { "f_vedouci": [ {"name": "Novák", ...}, ... ] }}
# Extract the leaders list from the JSON response using safe dict getters
vedouci_list = data.get('facet_counts', {}).get('f_vedouci', [])
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', [])
csv_path = os.path.join(CODELISTS_DIR, 'vedouci.csv')
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=';')
# Write the standard header required by the parser function
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:
name = None
# Varianta A: Položka je slovník {"name": "Jan Novák", "value": 10}
if isinstance(item, dict):
name = item.get('name')
# Varianta B: Položka je jen string (kdyby se API vrátilo k plochému seznamu)
elif isinstance(item, str):
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():
writer.writerow([name, name, 'vedouci'])
count += 1
@@ -146,33 +115,32 @@ def download_vedouci():
except Exception as e:
return False, str(e)
# --- GLOBAL DATA ---
# Toto se načte při startu QGISu
# Initialize global codelist data when the module is imported
_DATA = load_all_data()
OBDOBI = _DATA['obdobi']
TYP_AKCE = _DATA['typ_akce']
AREAL = _DATA['areal']
KRAJE = _DATA['kraj']
ORGANIZACE = _DATA['organizace']
OKRESY = _DATA['okres']
KATASTRY = _DATA['katastr']
VEDOUCI = _DATA['vedouci']
PIAN_PRESNOST = _DATA['pian_presnost']
TYP_LOKALITY = _DATA['typ_lokality']
DRUH_LOKALITY = _DATA['druh_lokality']
JISTOTA = _DATA['jistota']
LOKALITA_ZACHOVALOST = _DATA['lokalita_zachovalost']
# Safely extract individual categories into global variables for easy access across the plugin
OBDOBI = _DATA.get('obdobi', {})
TYP_AKCE = _DATA.get('typ_akce', {})
AREAL = _DATA.get('areal', {})
KRAJE = _DATA.get('kraj', {})
ORGANIZACE = _DATA.get('organizace', {})
OKRESY = _DATA.get('okres', {})
KATASTRY = _DATA.get('katastr', {})
VEDOUCI = _DATA.get('vedouci', {})
PIAN_PRESNOST = _DATA.get('pian_presnost', {})
TYP_LOKALITY = _DATA.get('typ_lokality', {})
DRUH_LOKALITY = _DATA.get('druh_lokality', {})
JISTOTA = _DATA.get('jistota', {})
LOKALITA_ZACHOVALOST = _DATA.get('lokalita_zachovalost', {})
def refresh_vedouci_cache():
"""
Znovu načte soubor vedouci.csv a aktualizuje globální proměnnou VEDOUCI.
Použijeme 'update', aby se zachovala reference na objekt (pokud ho dialog už používá).
"""
temp_data = load_all_data()
new_vedouci = temp_data['vedouci']
"""Reloads only the 'vedouci.csv' file to quickly update the cache without full initialization."""
# Parse only the targeted file containing the updated leaders
temp_data = parse_codelist_file('vedouci.csv')
new_vedouci = temp_data.get('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.update(new_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)
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):
super().__init__(parent)
self.setWindowTitle(f"Výběr: {title}")
self.resize(400, 500)
# Store the source data and previously selected items
self.data_dict = data_dict
self.preselected = preselected_codes if preselected_codes else []
layout = QVBoxLayout()
# Setup search input for filtering items
self.search_bar = QLineEdit()
self.search_bar.setPlaceholderText("Hledat v seznamu...")
self.search_bar.textChanged.connect(self.filter_list)
layout.addWidget(self.search_bar)
# Main list widget for displaying selectable items
self.list_widget = QListWidget()
self.populate_list()
layout.addWidget(self.list_widget)
# Standard OK/Cancel dialog buttons
buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
buttons.accepted.connect(self.accept)
buttons.rejected.connect(self.reject)
layout.addWidget(buttons)
self.setLayout(layout)
def populate_list(self):
# Sort items alphabetically by their display name
sorted_names = sorted(self.data_dict.keys())
for name in sorted_names:
code = self.data_dict[name]
item = QListWidgetItem(name)
# Store the actual code (ID) hidden in the UserRole
item.setData(Qt.UserRole, code)
# Make the item checkable (adds a checkbox)
item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
# Restore previous selection state
if code in self.preselected:
item.setCheckState(Qt.Checked)
else:
item.setCheckState(Qt.Unchecked)
self.list_widget.addItem(item)
def filter_list(self, text):
# Hide items that don't match the search text (case-insensitive)
search_text = text.lower()
for i in range(self.list_widget.count()):
item = self.list_widget.item(i)
@@ -54,6 +76,7 @@ class FilterableSelectionDialog(QDialog):
item.setHidden(False)
def get_selected_codes(self):
"""Returns the hidden codes and display labels of all checked items."""
codes = []
labels = []
for i in range(self.list_widget.count()):
@@ -66,15 +89,20 @@ class FilterableSelectionDialog(QDialog):
# --- Main window ---
class AmcrFilterDialog(QDialog):
"""
The main filtering UI where users set criteria before downloading data.
"""
def __init__(self, typ_dat, parent=None):
super(AmcrFilterDialog, self).__init__(parent)
self.setWindowTitle("Filtr AMČR")
self.resize(500, 750)
# Determines if we are fetching 'akce' (projects) or 'lokalita' (locations)
self.typ_dat = typ_dat
# Cache for filtering
# Cache dictionary to store selected codes for each category
self.selection_cache = {
'organizace': [], 'kraj': [], 'obdobi': [], 'areal': [],
'typ_akce': [], 'okres': [], 'katastr': [], 'vedouci': [], 'pian_presnost': [],
@@ -83,6 +111,7 @@ class AmcrFilterDialog(QDialog):
layout = QVBoxLayout()
# Filter by current map canvas extent
self.chk_bbox = QCheckBox("Omezit vyhledávání rozsahem okna")
self.chk_bbox.setChecked(True)
layout.addWidget(self.chk_bbox)
@@ -94,14 +123,91 @@ class AmcrFilterDialog(QDialog):
layout.addWidget(self.chk_posevidence)
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.setFlat(True)
row_layout = QHBoxLayout()
row_layout.setContentsMargins(5, 5, 5, 5)
# Read-only field displaying the names of selected items
display_field = QLineEdit()
display_field.setReadOnly(True)
display_field.setPlaceholderText("Nic nevybráno (vše)")
@@ -110,16 +216,22 @@ class AmcrFilterDialog(QDialog):
btn = QPushButton("Vybrat...")
btn.setFixedWidth(80)
# Nested function that handles opening the dialog and saving results
def open_dialog():
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()
# Update local cache with selected IDs
self.selection_cache[cache_key] = codes
# Update the UI text field with selected names
if labels:
display_field.setText(", ".join(labels))
else:
display_field.clear()
# Special case: Pre-fill specific accuracy levels by default
if cache_key == 'pian_presnost':
display_field.setText("odchylka jednotky metrů, odchylka desítky metrů, odchylka stovky metrů")
self.selection_cache[cache_key] = ['HES-000861', 'HES-000862', 'HES-000863']
@@ -129,94 +241,28 @@ class AmcrFilterDialog(QDialog):
row_layout.addWidget(display_field)
row_layout.addWidget(btn)
# Add an optional extra button (e.g., the refresh button for leaders)
if extra_btn:
row_layout.addWidget(extra_btn)
row_widget.setLayout(row_layout)
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):
# Change cursor to loading state to indicate background task
QApplication.setOverrideCursor(Qt.WaitCursor)
try:
success, msg = download_vedouci()
if success:
count = refresh_vedouci_cache()
QApplication.restoreOverrideCursor()
QMessageBox.information(self, "Úspěch", f"{msg}\nNyní je v paměti {count} osob.")
else:
QApplication.restoreOverrideCursor()
QMessageBox.warning(self, "Chyba", f"Nepodařilo se stáhnout data:\n{msg}")
except Exception as e:
QApplication.restoreOverrideCursor()
QMessageBox.critical(self, "Chyba", str(e))
finally:
# Safely restore the normal cursor even if an error occurs
QApplication.restoreOverrideCursor()
def get_bbox(self):
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"
def get_filters(self):
"""Compiles the user selections from the cache into API-ready filter parameters."""
filters = {}
if self.selection_cache['kraj']:
+166 -128
View File
@@ -11,11 +11,11 @@ import json
import xml.etree.ElementTree as ET
import re
# Global translations cache
# Global cache to store translated terms from the Digital Archive
TRANSLATIONS = {}
# Download Digiarchive's vocabulary
def load_translations():
"""Fetches the official Czech translation dictionary from the AISCR API."""
global TRANSLATIONS
if TRANSLATIONS:
return
@@ -26,22 +26,32 @@ def load_translations():
if r.status_code == 200:
TRANSLATIONS = r.json()
except Exception as e:
print(f"Chyba při stahování hesláře: {e}")
print(f"Error downloading vocabulary: {e}")
def tr_code(code):
"""Translates a technical code into a human-readable string using the global cache."""
if not code:
return ""
return TRANSLATIONS.get(code, code)
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()
# 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()
crs_src = canvas.mapSettings().destinationCrs()
crs_dest = QgsCoordinateReferenceSystem("EPSG:4326")
xform = QgsCoordinateTransform(crs_src, crs_dest, QgsProject.instance())
extent_wgs = xform.transformBoundingBox(extent)
# Format the bounding box string as required by the API: minLat,minLon,maxLat,maxLon
bbox_str = f"{extent_wgs.yMinimum()},{extent_wgs.xMinimum()},{extent_wgs.yMaximum()},{extent_wgs.xMaximum()}"
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)
try:
# ===================
# A) METADATA (Fieldwork event/Site)
# ===================
# ==========================================
# A) METADATA FETCHING (Fieldwork/Site)
# ==========================================
base_params = {
"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":
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:
for key, value in filters.items():
if not value:
@@ -76,13 +86,17 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
docs = []
current_page = 0
BATCH_DOCS = 500
MAX_LIMIT = 20000
feats_k = []
BATCH_DOCS = 500 # Records per API request
MAX_LIMIT = 20000 # Safety limit to prevent QGIS from freezing
feats_k = [] # List for component features (non-spatial)
seen_ids = set()
target_pian_ids_count = 0
# Check if we should skip negative results based on filter
skip_negativni = filters.get('posevidence') == 'true' if filters else False
# --- API PAGINATION LOOP ---
while True:
base_params['rows'] = BATCH_DOCS
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:
break
# Filter out duplicates and append to main list
new_docs = []
for d in batch_docs:
ident = d.get('ident_cely')
@@ -117,7 +132,7 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
break
current_page += 1
QApplication.processEvents()
QApplication.processEvents() # Keep UI responsive
except Exception as 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
# ==========================================
# Attribute parsing
# B) ATTRIBUTE PARSING
# ==========================================
# pian_lookup maps a Geometry ID (PIAN) to a list of its associated metadata
pian_lookup = {}
target_pian_ids = set()
actions_with_geom = 0
# Helper function for safe single-value extraction
def g(doc, key, default=""):
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:
piani = doc.get('az_dj_pian', [])
if not piani:
@@ -141,23 +175,11 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
actions_with_geom += 1
def g(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
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])
# Extract protected data (fields not available in public Solr index)
az_chranene = doc.get('az_chranene_udaje', {})
chranene = doc.get('akce_chranene_udaje') or doc.get('lokalita_chranene_udaje') or {}
# Format additional cadastral areas from dictionaries
dalsi_kat = az_chranene.get('dalsi_katastr', [])
dalsi_kat_str = ""
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_popis = chranene.get('popis', "")
# Prepate common metadata
# Core metadata structure
meta = {
"ident_cely": doc.get('ident_cely', ''),
"az_okres": g('az_okres'),
"katastr": g_list('katastr'),
"az_okres": g(doc, 'az_okres'),
"katastr": g_list(doc, 'katastr'),
"dalsi_katastr": dalsi_kat_str,
"pristupnost": g('pristupnost'),
"loc": g_list('loc')
"pristupnost": g(doc, 'pristupnost'),
"loc": g_list(doc, 'loc')
}
# Add entity-specific metadata
if typ_dat == "akce":
meta.update({
"akce_hlavni_vedouci": g('akce_hlavni_vedouci'),
"akce_organizace": tr_code(g('akce_organizace')),
"akce_specifikace_data": tr_code(g('akce_specifikace_data')),
"akce_datum_zahajeni": g('akce_datum_zahajeni'),
"akce_datum_ukonceni": g('akce_datum_ukonceni'),
"akce_hlavni_typ": tr_code(g('akce_hlavni_typ')),
"akce_vedlejsi_typ": g_list('akce_vedlejsi_typ', translate=True),
"akce_hlavni_vedouci": g(doc, 'akce_hlavni_vedouci'),
"akce_organizace": tr_code(g(doc, 'akce_organizace')),
"akce_specifikace_data": tr_code(g(doc, 'akce_specifikace_data')),
"akce_datum_zahajeni": g(doc, 'akce_datum_zahajeni'),
"akce_datum_ukonceni": g(doc, 'akce_datum_ukonceni'),
"akce_hlavni_typ": tr_code(g(doc, 'akce_hlavni_typ')),
"akce_vedlejsi_typ": g_list(doc, 'akce_vedlejsi_typ', translate=True),
"lokalizace_okolnosti": str(lokalizace) if lokalizace else "",
"akce_je_nz": "Ano" if doc.get('akce_je_nz') is True else "Ne",
})
@@ -195,33 +218,42 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
meta.update({
"lokalita_nazev": lokalita_nazev,
"lokalita_popis": lokalita_popis,
"lokalita_zachovalost": tr_code(g('lokalita_zachovalost')),
"lokalita_druh": tr_code(g('lokalita_druh')),
"lokalita_typ": tr_code(g('lokalita_typ_lokality')),
"lokalita_zachovalost": tr_code(g(doc, 'lokalita_zachovalost')),
"lokalita_druh": tr_code(g(doc, 'lokalita_druh')),
"lokalita_typ": tr_code(g(doc, 'lokalita_typ_lokality')),
})
# Documentation units (DJ) within the record
djs = doc.get('az_dokumentacni_jednotka', [])
for dj in djs:
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
dj_meta = meta.copy()
dj_id = dj.get('ident_cely')
dj_meta['dj_id'] = dj_id
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')
if dj_pian:
dj_pian_value = dj_pian.get('id')
if 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:
pian_lookup[dj_pian_value] = []
pian_lookup[dj_pian_value].append(dj_meta)
# Parse non-spatial components if requested (for relational tables)
if komponenty == "true":
komps = dj.get('dj_komponenta', [])
for komp in komps:
@@ -229,7 +261,6 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
atributy = [
komp.get('ident_cely', ""),
dj_id,
# komponenta_aktivita ..?,
komp.get('komponenta_areal', {}).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)
total_pians = len(ids_list)
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)
# Seznam polí pro PIAN
fl_pian = ["ident_cely", "pian_typ", "pian_chranene_udaje", "pian_presnost"]
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}")
# ==========================================
# 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"
elif typ_dat == "lokalita":
archeologicky_zaznam = "Lokalita"
archeologicky_zaznam = "Akce" if typ_dat == "akce" else "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 = [
QgsField("PIAN", 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)
]
# Extend table based on data type
if typ_dat == "akce":
cols += [
QgsField("Akce lokalizace", QVariant.String),
QgsField("Vedoucí akce", QVariant.String),
QgsField("Organizace", 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("Hlavní typ", QVariant.String),
QgsField("Vedlejší typ", QVariant.String),
QgsField("Zjištění", QVariant.String),
QgsField("Akce lokalizace", QVariant.String),
QgsField("Zjištění", QVariant.String),
QgsField("Akce nahrazuje NZ", QVariant.String),
]
elif typ_dat == "lokalita":
cols += [
QgsField("Název lokality", QVariant.String),
QgsField("Popis lokality", QVariant.String),
QgsField("Typ lokality", QVariant.String),
QgsField("Druh lokality", QVariant.String),
QgsField("Zachovalost", QVariant.String)
QgsField("nazev_lokality", QVariant.String),
QgsField("popis_lokality", QVariant.String),
QgsField("typ_lokality", QVariant.String),
QgsField("druh_lokality", QVariant.String),
QgsField("zachovalost", 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":
vl_komponenty = QgsVectorLayer("None", "AMCR Komponenty", "memory")
pr = vl_komponenty.dataProvider()
komponenty_cols = [
QgsField("komponenta", QVariant.String), # ident_cely
QgsField("komponenta", QVariant.String),
QgsField("dj_id", QVariant.String),
# potenciálně QgsField("komponenta_aktivita", QVariant.String),
QgsField("komponenta_areal", 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:
vl.dataProvider().addAttributes(cols)
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 = [], [], []
# --- FEATURE POPULATION ---
for doc in docs_pian:
try:
pid = doc.get('ident_cely', '')
if pid not in pian_lookup:
continue
metas = pian_lookup[pid]
# Geometry processing
# Extract WKT geometry from protected JSON data
raw = doc.get('pian_chranene_udaje')
if isinstance(raw, list) and raw:
raw = raw[0]
jdata = json.loads(raw) if isinstance(raw, str) else (raw if isinstance(raw, dict) else {})
jdata = json.loads(raw) if isinstance(raw, str) else (raw or {})
wkt = None
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'):
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_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'):
continue
if wkt:
geom = QgsGeometry.fromWkt(wkt)
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:
feat = QgsFeature()
feat.setGeometry(geom)
atributy = [
pid,
pian_presnost,
pian_typ,
meta['dj_id'],
meta['dj_typ_value'],
meta['loc'],
meta['ident_cely'],
pid, pian_presnost, pian_typ, meta['dj_id'],
meta['dj_typ_value'], meta['loc'], meta['ident_cely'],
"https://digiarchiv.aiscr.cz/id/" + meta['ident_cely'],
meta['az_okres'],
meta['katastr'],
meta['dalsi_katastr']
meta['az_okres'], meta['katastr'], meta['dalsi_katastr']
]
if typ_dat == "akce":
atributy += [
meta['akce_hlavni_vedouci'],
meta['akce_organizace'],
meta['akce_specifikace_data'],
meta['akce_datum_zahajeni'],
meta['akce_datum_ukonceni'],
meta['akce_hlavni_typ'],
meta['akce_vedlejsi_typ'],
meta['dj_negativni'],
meta['lokalizace_okolnosti'],
meta['akce_je_nz']
]
elif typ_dat == "lokalita":
atributy += [
meta['lokalita_nazev'],
meta['lokalita_popis'],
meta['lokalita_typ'],
meta['lokalita_druh'],
if is_akce:
atributy.extend([
meta['lokalizace_okolnosti'], meta['akce_hlavni_vedouci'],
meta['akce_organizace'], meta['akce_specifikace_data'],
meta['akce_datum_zahajeni'], meta['akce_datum_ukonceni'],
meta['akce_hlavni_typ'], meta['akce_vedlejsi_typ'],
meta['dj_negativni'], meta['akce_je_nz']
])
else:
atributy.extend([
meta['lokalita_nazev'], meta['lokalita_popis'],
meta['lokalita_typ'], meta['lokalita_druh'],
meta['lokalita_zachovalost']
]
])
atributy.append(meta['pristupnost'])
feat.setAttributes(atributy)
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)
target_list.append(feat)
except Exception as ex:
print(f"Chyba při tvorbě feature: {ex}")
pass
# --- ADDING TO QGIS INTERFACE ---
proj = QgsProject.instance()
added = 0
layers_to_process = [
(feats_p, vl_poly, "Plochy"),
(feats_p, vl_poly, "Polygony"),
(feats_l, vl_line, "Linie"),
(feats_pt, vl_point, "Body"),
]
@@ -449,7 +487,7 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
if f:
l.dataProvider().addFeatures(f)
l.updateExtents()
l.setName(f"AMČR {n} (Filtrováno)")
l.setName(f"AMCR_{archeologicky_zaznam}_{n}")
proj.addMapLayer(l)
if n != "Komponenty":
added += len(f)
@@ -457,23 +495,22 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
if added > 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":
parent_layers = [
(vl_poly, "Plochy"),
(vl_poly, "Polygony"),
(vl_line, "Linie"),
(vl_point, "Body")
]
rel_manager = proj.relationManager()
for parent_layer, label in parent_layers:
rel = QgsRelation()
#rel_id = f"rel_{parent_layer.id()}_komponenty"
rel_name = f"Komponenty pro {label}"
#rel.setId(rel_id)
rel.setName(rel_name)
rel.setReferencingLayer(vl_komponenty.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()
if rel.isValid():
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:
iface.messageBar().pushMessage("Chyba", str(e), level=2)
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
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):
"""
Constructor initializes the connection to QGIS interface and sets up
internationalization (i18n).
"""
self.iface = iface
self.plugin_dir = os.path.dirname(__file__)
# Determine the user's locale to load appropriate translation files
locale = QSettings().value('locale/userLocale')[0:2]
locale_path = os.path.join(
self.plugin_dir,
'i18n',
'AmcrViewer_{}.qm'.format(locale))
# Install the translator if a translation file for the current locale exists
if os.path.exists(locale_path):
self.translator = QTranslator()
self.translator.load(locale_path)
QCoreApplication.installTranslator(self.translator)
# Initialize internal state
self.actions = []
self.menu = self.tr(u'&AMČR Viewer')
self.first_start = None
def tr(self, message):
"""Helper method for translating strings within the AmcrViewer context."""
return QCoreApplication.translate('AmcrViewer', message)
def add_action(self, icon_path, text, callback, enabled_flag=True,
add_to_menu=True, add_to_toolbar=True, status_tip=None,
whats_this=None, parent=None):
"""
Helper method to create QActions and automatically register them
into the QGIS Menu and Toolbar.
"""
icon = QIcon(icon_path)
action = QAction(icon, text, parent)
action.triggered.connect(callback)
@@ -45,26 +62,33 @@ class AmcrViewer:
if whats_this is not None:
action.setWhatsThis(whats_this)
# Standard QGIS API for adding icons and menu items
if add_to_toolbar:
self.iface.addToolBarIcon(action)
if add_to_menu:
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
def initGui(self):
import os
plugin_dir = os.path.dirname(__file__)
icon_akce_path = os.path.join(plugin_dir, 'akce.png')
icon_lokality_path = os.path.join(plugin_dir, 'lokality.png')
"""
Called when the plugin is loaded. Creates the menu structure,
sub-actions, and the dropdown tool button in the toolbar.
"""
# Define paths for action-specific icons
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()
# 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(
icon_path=icon_akce_path,
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)
# 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)
self.main_action = QAction(main_icon, 'AMČR Viewer', self.iface.mainWindow())
self.main_action.setMenu(self.plugin_menu)
self.iface.addPluginToMenu(self.menu, self.main_action)
# 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.setMenu(self.plugin_menu)
self.tool_button.setDefaultAction(self.action_download_akce)
self.tool_button.setPopupMode(QToolButton.MenuButtonPopup)
# Vložení vytvořeného tlačítka do QGIS rozhraní
self.iface.addToolBarWidget(self.tool_button)
# Add the widget directly to the toolbar and store the reference for cleanup
self.toolbar_action = self.iface.addToolBarWidget(self.tool_button)
self.first_start = True
def unload(self):
"""
Called when the plugin is disabled or removed.
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:
self.iface.removePluginMenu(self.tr(u'&AMČR Viewer'), action)
self.iface.removePluginMenu(self.menu, action)
self.iface.removeToolBarIcon(action)
self.actions.clear()
# 4. Reset map tools if currently active
if hasattr(self, 'tool'):
self.iface.mapCanvas().unsetMapTool(self.tool)
# --- Data downloading ---
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)
result = dlg.exec_()
result = dlg.exec()
# If user confirmed the dialog (OK button), gather filters and load data
if result == 1:
filters = dlg.get_filters()
bbox = dlg.get_bbox()
komponenty = dlg.get_komponenty()
# Access the map canvas and start the fetch/render process from amcr_tools
canvas = self.iface.mapCanvas()
load_amcr_data(canvas, bbox, filters, typ_dat, komponenty)
load_amcr_data(canvas, bbox, filters, typ_dat, komponenty)
+1 -1
View File
@@ -7,7 +7,7 @@
name=AMČR Viewer
qgisMinimumVersion=3.4
description=Viewing and downloading the AMČR data.
version=1.2.0-rc.1
version=1.2.0
author=David Spáčil
email=spacil@arub.cz