19 Commits

Author SHA1 Message Date
david a6ebbce4cf Update metadata.txt 2026-05-19 15:25:38 +02:00
david 88149fbb30 Hotfix/security (#37)
* vyměnit xml.etree.ElementTree za defusedxml.ElementTree

* zpátky xml.etree.ElementTree, ale přidáno # nosec; jde o false positive

* switch z ukládání hesel v registrech na používání QGIS Auth Manager
2026-05-19 15:22:35 +02:00
david c0d054d22a Update metadata.txt
v2.0.0-alpha.1
experimental flag true
2026-05-19 13:12:32 +02:00
david ba41039468 Feature/login (#36)
* přidáno nové tlačítko do toolbaru

* přihlašovací dialog

* funkce pro přihlašování

* aplikace získávání dat z API přes session

* zastřešující akce v main skriptu

* ošetření prodlužování session/opětovné přihlášení

* přidání přístupnosti do hesláře

* aktualizace přibaleného hesláře

* oprava stahování hesláře (pristupnost vyžaduje písmeno [A/B/C/D], ne heslo; aktualizace hesláře

* aplikace přístupnosti do filtračního dialogu

* úpravy přihlašovacího dialogu
2026-05-19 13:05:07 +02:00
david 499b3b3f0a Fix/komponenta jako zaklad (#35)
* čištění amcr_tools.py od zbytečných prvků souvisejících se standalone tabulkou komponent

* aplikace nové logiky přípravy dat v případě požadavku na komponenty

* nové aliasy + přidání dat komponent do finálních tabulek

* oprava pořadí polí v tabulce
2026-05-16 14:18:17 +02:00
david 54f154b264 Feature/aktualizace heslaru (#32)
* přechod od statického hesláře k dynamickému načítání z OAI-PMH API AMČR

* aplikace načítání heslářů a task management (backend)

* frontend + debugging

* aktualizace přibaleného hesláře

* kosmetické drobnosti

* ošetření speciální případů při stahování hesláře (katastr, okres) + s tím spojená aktualizace přiloženého hesláře
2026-05-14 14:02:18 +02:00
david c679e776df Přejmenování názvů atributových polí (#31)
* changed attribute field names

* added aliases
2026-04-16 09:39:49 +02:00
david a5604dfaa8 Update README.md (#30)
* update README.md (v1.3.2)
2026-04-13 12:49:13 +02:00
david 11f44d025b Update metadata.txt 2026-03-19 09:38:32 +01:00
david 7f3b2b46fb Merge pull request #28 from ARUP-CAS/relation-fix
Fix polymorphic relations not creating in some cases
2026-03-19 09:36:37 +01:00
david be53edefa5 relation fix 2026-03-19 09:30:13 +01:00
david 3be7832b40 Update metadata.txt 2026-03-16 14:30:12 +01:00
david 8c0c540fa4 Merge pull request #26 from ARUP-CAS/polymorphic-relation
Polymorphic relation instead of the standard ones; code cleanup
2026-03-16 14:13:50 +01:00
David Spáčil 8088b32661 removed unnecessary imports 2026-03-16 14:01:32 +01:00
David Spáčil c17275ef66 changed from three 1:N relations to a polymorphic relation 2026-03-16 14:00:37 +01:00
david 9ec866f1d2 Update metadata.txt 2026-03-13 10:56:09 +01:00
david 5a951edec7 Fixed compatibility issues with QGIS 4 (#24)
* used PyQt6 compatible objects

* metadata update
2026-03-13 10:48:12 +01:00
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
9 changed files with 22979 additions and 14095 deletions
+1
View File
@@ -210,3 +210,4 @@ __marimo__/
README_files/ README_files/
README.html README.html
amcr_viewer.zip
+59 -12
View File
@@ -2,7 +2,7 @@
[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
**Platform:** QGIS 3.4.x **Platform:** QGIS 3.4.04.99.0
**Module Type:** Data Acquisition & Visualization **Module Type:** Data Acquisition & Visualization
@@ -22,8 +22,6 @@
* **Dynamic Geometry Retrieval:** Automatically downloads and categorizes spatial data into Point, Line, and Polygon layers. * **Dynamic Geometry Retrieval:** Automatically downloads and categorizes spatial data into Point, Line, and Polygon layers.
* **Semantic Interoperability:** Automatically translates internal system codes into human-readable labels using the AIS CR API. * **Semantic Interoperability:** Automatically translates internal system codes into human-readable labels using the AIS CR API.
---
## 2. Installation Guide ## 2. Installation Guide
**Install the plugin from QGIS plugin repository.** **Install the plugin from QGIS plugin repository.**
@@ -37,8 +35,6 @@
*5. Locate the source ZIP file and click Install Plugin.* *5. Locate the source ZIP file and click Install Plugin.*
*6. Upon successful installation, the AMCR download button (load AMCR data) will appear in the interface.* *6. Upon successful installation, the AMCR download button (load AMCR data) will appear in the interface.*
---
## 3. User Manual ## 3. User Manual
### 3.1 Data Retrieval ### 3.1 Data Retrieval
@@ -64,8 +60,6 @@ To initiate a search query, click either the **Stáhnout data akcí** or the **S
For a more in-depth tutorial refer to the [AMČR Documentation](https://amcr-help.aiscr.cz/digiarchiv/qgis-viewer.html) (only in Czech). For a more in-depth tutorial refer to the [AMČR Documentation](https://amcr-help.aiscr.cz/digiarchiv/qgis-viewer.html) (only in Czech).
### 3.2 Layer Structure & Attributes ### 3.2 Layer Structure & Attributes
Upon successful retrieval, the plugin generates four temporary memory layers: Upon successful retrieval, the plugin generates four temporary memory layers:
@@ -77,11 +71,63 @@ Upon successful retrieval, the plugin generates four temporary memory layers:
The Attribute Table includes standardized fields with important metadata. The components layer has no geometry on its own and depend solely on a relation with the other three layers. The Attribute Table includes standardized fields with important metadata. The components layer has no geometry on its own and depend solely on a relation with the other three layers.
--- #### 3.2.1 Fields of the layers with geometry
| Field | Description |
| --- | --- |
| PIAN | PIAN (spatial identifier) ID |
| Přesnost | spatial deviation [in units/tens/hundreds of meters/defined by cadastre] |
| PIAN typ | [point/line/polygon] |
| Dokumentační jednotka | Documentation unit ID |
| Typ dokumentační jednotky | [trench/event part/whole event/cadastral territory] |
| Definiční bod(y) (WGS-84) | feature centroid in WGS-84 coordinate system |
| Akce/Lokalita | Event/Site ID |
| Odkaz do Digitálního archivu AMČR | link to the Event/Site record in the Digital Archive |
| Okres | district |
| Katastr | main cadastre |
| Další katastr | other cadastres, if the event extends beyond the main cadastre |
| Přístupnost | record accessibility [A/B/C/D] |
> Common fields
| Field | Description |
| --- | --- |
| Vedoucí akce | main fieldwork manager |
| Organizace | organization conducting the research |
| Specifikace data | [exact date/exact years/sometime in years] |
| Datum zahájení | Event start date |
| Datum ukončení | Event end date |
| Hlavní typ | primary research method [total excavation/pit trench/surface collection survey/…] |
| Vedlejší typ | secondary research method [same options as in Hlavní typ] |
| Zjištění | did the research reveal archaeological contexts? [positive/negative] |
| Akce lokalizace | verbal description of the event location |
| Akce nahrazuje NZ | replaces a fieldwork report? [yes/no] |
> Fields related to *Fieldwork events*
| Field | Description |
| --- | --- |
| nazev_lokality | site name |
| popis_lokality | site description |
| typ_lokality | site classification by definition method [survey polygon/heritage site/landscape] |
| druh_lokality | site classification by the nature of identified field relics [aerial survey polygon/landscape/remains of settlement/…] |
| zachovalost | site preservation state [buried site/ruin/aboveground remains/…] |
> Fields related to *Sites*
#### 3.2.2 Fields of the *Components* layer
| Field | Description |
| --- | --- |
| komponenta | Component ID |
| dj_id | parent Documentation unit ID |
| komponenta_areal | Activity area [settlement/burial area/field/…] |
| komponenta_obdobi | Period [Neolithic/High Middle AgesModern Period/Middle La Tène (LtBC1)/…] |
| vrstva | system value linking to a specific geometry table with the corresponding documentation unit |
## 4. Technical Architecture ## 4. Technical Architecture
The plugin is developed in **Python 3** using the **PyQt5** framework for the GUI and the **Requests** library for HTTP communication. The plugin is developed in **Python 3** using the **PyQt6** framework for the GUI and the **Requests** library for HTTP communication.
### 4.1 File Structure ### 4.1 File Structure
@@ -100,12 +146,10 @@ The plugin interacts with three primary endpoints of the AIS CR infrastructure:
* Parameters: `entity=akce`, `rows/page` (pagination). * Parameters: `entity=akce`, `rows/page` (pagination).
* Logic: The plugin implements a `while True` loop to handle pagination, processing data in batches of 500 records to ensure stability. * Logic: The plugin implements a `while True` loop to handle pagination, processing data in batches of 500 records to ensure stability.
2. **Translation API:** 2. **Translation API:**
* Endpoint: `https://digiarchiv.aiscr.cz/api/assets/i18n/cs.json` * Endpoint: `https://digiarchiv.aiscr.cz/api/assets/i18n/cs.json`
* Function: Retrieves the mapping between system codes (e.g., `HES-xxxx`) and Czech labels. This dictionary is cached in memory during the session. * Function: Retrieves the mapping between system codes (e.g., `HES-xxxx`) and Czech labels. This dictionary is cached in memory during the session.
### 4.3 Data Persistence ### 4.3 Data Persistence
* **Vocabularies:** Static vocabularies (e.g., Periods, Regions) are stored in `codelists/heslar.csv`. * **Vocabularies:** Static vocabularies (e.g., Periods, Regions) are stored in `codelists/heslar.csv`.
@@ -117,7 +161,10 @@ The plugin interacts with three primary endpoints of the AIS CR infrastructure:
* **Record Limit:** A safety cap of 20,000 records is enforced. * **Record Limit:** A safety cap of 20,000 records is enforced.
* **Batch Processing:** Geometry fetching is batched (50 IDs per request) to comply with URL length limitations and server load balancing. * **Batch Processing:** Geometry fetching is batched (50 IDs per request) to comply with URL length limitations and server load balancing.
## 6. Links and resources ### 4.5 Relational Data Linking
The plugin automatically utilizes advanced QGIS features for data relationship management, specifically Polymorphic Relations. The *Components* layer is dynamically linked within the project to the spatial layers of events and sites via the documentation unit identifier (dj_id). This allows users to immediately see all *components* belonging to a given geometry (point, line, or polygon) directly in the attribute form or the identify features tool, without the need to manually filter or join the data.
## 5. Links and resources
* [AMCR/Digiarchive Documentation](https://amcr-help.aiscr.cz/) (only in Czech). * [AMCR/Digiarchive Documentation](https://amcr-help.aiscr.cz/) (only in Czech).
* [AMCR Viewer tutorial](https://amcr-help.aiscr.cz/digiarchiv/qgis-viewer.html) (only in Czech). * [AMCR Viewer tutorial](https://amcr-help.aiscr.cz/digiarchiv/qgis-viewer.html) (only in Czech).
+195 -130
View File
@@ -1,80 +1,65 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import os import os
import csv import csv
import codecs
import requests import requests
import json import xml.etree.ElementTree as ET # nosec B314
import time
from qgis.core import QgsMessageLog, Qgis
# 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')
BASE_URL = "https://api.aiscr.cz/2.2/oai"
OUTPUT_FILE = os.path.join(CODELISTS_DIR, 'heslar.csv')
slovnicek = {
'obdobi' : 'heslo:obdobi',
'typ_akce' : 'heslo:akce_typ',
'areal' : 'heslo:areal',
'kraj' : 'ruian_kraj',
'organizace' : 'organizace',
'okres' : 'ruian_okres',
'katastr' : 'ruian_katastr',
'vedouci' : 'osoba',
'pian_presnost' : 'heslo:pian_presnost',
'typ_lokality' : 'heslo:lokalita_typ',
'druh_lokality' : 'heslo:lokalita_druh',
'jistota' : 'heslo:jistota_urceni',
'lokalita_zachovalost' : 'heslo:stav_dochovani',
'pristupnost' : 'heslo:pristupnost'
}
NS = {
'oai': 'http://www.openarchives.org/OAI/2.0/',
'dc': 'http://purl.org/dc/elements/1.1/',
'oai_dc': 'http://www.openarchives.org/OAI/2.0/oai_dc/'
}
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."""
if target_dict is None:
target_dict = {}
def load_csv_data(filename):
"""Obecná funkce pro načtení CSV souboru do slovníku"""
data = {}
path = os.path.join(CODELISTS_DIR, filename) path = os.path.join(CODELISTS_DIR, filename)
# Return early if the file doesn't exist to avoid missing file errors
if not os.path.exists(path): if not os.path.exists(path):
return data 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.
pass
for row in reader: # Iterate through rows and extract label, code, and category
if len(row) >= 3:
label = row[0].strip()
code = row[1].strip()
category = row[2].strip()
# 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
except Exception as e:
print(f"AMČR Chyba čtení {filename}: {e}")
return data
def load_all_data():
"""
Načte statický heslář I dynamický heslář vedoucích.
Vrací slovník slovníků.
"""
ensure_codelists_dir()
# 1. Načteme hlavní statický heslář
# Musíme ho rozparsovat podle kategorií, tak jak to bylo předtím
categorized_data = {
'obdobi': {}, 'typ_akce': {}, 'areal': {},
'kraj': {}, 'organizace': {}, 'okres': {}, 'katastr': {},
'vedouci': {}, 'pian_presnost': {}, 'typ_lokality': {}, 'druh_lokality': {},
'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: for row in reader:
if len(row) >= 3: if len(row) >= 3:
label = row[0].strip() label = row[0].strip()
@@ -82,97 +67,177 @@ def load_all_data():
cat = row[2].strip() cat = row[2].strip()
clean = code if code else None clean = code if code else None
if cat in categorized_data: # Initialize a new dictionary for a category if encountered for the first time
categorized_data[cat][label] = clean if cat not in target_dict:
except: pass target_dict[cat] = {}
# Načteme soubory # Assign the extracted code to the corresponding label within the category
parse_file('heslar.csv') # Statické target_dict[cat][label] = clean
parse_file('vedouci.csv') # Dynamické (pokud existuje) except Exception as e:
QgsMessageLog.logMessage(f"AMČR Codelist Read Error for {filename}: {e}", "AMČR", Qgis.Critical)
return target_dict
def load_all_data():
"""Loads the codelist during plugin startup."""
ensure_codelists_dir()
categorized_data = {k: {} for k in slovnicek.keys()}
parse_codelist_file('heslar.csv', categorized_data)
return categorized_data return categorized_data
# --- 2. AKTUALIZACE DAT (DOWNLOAD) --- def fetch_set(internal_name, api_set, task=None):
dataset = []
params = {
"verb": "ListRecords",
"metadataPrefix": "oai_dc",
"set": api_set
}
def download_vedouci(): while True:
""" # Kontrola zrušení v každém kroku
Stáhne seznam vedoucích z API (pomocí onlyFacets) a uloží do codelists/vedouci.csv. if task and task.isCanceled():
""" return None
ensure_codelists_dir()
# Tvá URL + pojistka, abychom dostali všechny záznamy (limit -1)
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 response = requests.get(BASE_URL, params=params, timeout=30)
r.raise_for_status() response.raise_for_status()
data = r.json() root = ET.fromstring(response.content)
# Cesta k datům dle tvého JSONu: records = root.findall('.//oai:record', NS)
# {"facet_counts": { "f_vedouci": [ {"name": "Novák", ...}, ... ] }} for rec in records:
vedouci_list = data.get('facet_counts', {}).get('f_vedouci', []) metadata = rec.find('.//oai_dc:dc', NS)
if metadata is not None:
# Kód (identifier)
kod = metadata.find('dc:identifier', NS).text if metadata.find('dc:identifier', NS) is not None else ""
if not vedouci_list: # Název (title) - filtrujeme systémové popisky "AMČR - ..."
# Zkusíme ještě alternativní cestu, kdyby API vrátilo standardní Solr strukturu titles = metadata.findall('dc:title', NS)
# (facet_counts -> facet_fields -> f_vedouci) nazev = ""
vedouci_list = data.get('facet_counts', {}).get('facet_fields', {}).get('f_vedouci', []) for t in titles:
if t.text and not t.text.startswith("AMČR -") and not t.text.startswith(" AMČR -"):
nazev = t.text
break
# Pokud by náhodou žádný title neprošel filtrem, vezmeme první dostupný
if not nazev and titles:
nazev = titles[0].text
csv_path = os.path.join(CODELISTS_DIR, 'vedouci.csv') specialni_pripady = ['okres', 'katastr']
count = 0 if internal_name in specialni_pripady:
with codecs.open(csv_path, 'w', 'utf-8') as f: kod = nazev
writer = csv.writer(f, delimiter=';')
writer.writerow(['Název', 'Kód', 'Kategorie'])
# NOVÁ LOGIKA PARSOVÁNÍ if internal_name == 'pristupnost':
for item in vedouci_list: kod = next((t.text for t in titles if t.text and len(t.text) == 1 and t.text.isalpha()), None)
name = None
# Varianta A: Položka je slovník {"name": "Jan Novák", "value": 10} dataset.append({
if isinstance(item, dict): 'Název': nazev,
name = item.get('name') 'Kód': kod,
'Kategorie': internal_name
})
# Varianta B: Položka je jen string (kdyby se API vrátilo k plochému seznamu) # Stránkování
elif isinstance(item, str): token = root.find('.//oai:resumptionToken', NS)
name = item if token is not None and token.text:
params = {
# Pokud máme jméno a není to číslo (count), zapíšeme "verb": "ListRecords",
if name and not str(name).isdigit(): "resumptionToken": token.text
writer.writerow([name, name, 'vedouci']) }
count += 1 time.sleep(0.5)
else:
return True, f"Staženo {count} jmen." break
except Exception as e: except Exception as e:
return False, str(e) QgsMessageLog.logMessage(f"Chyba u setu {api_set}: {e}", "AMČR", Qgis.Warning)
break
# --- GLOBAL DATA --- return dataset
# Toto se načte při startu QGISu
_DATA = load_all_data()
OBDOBI = _DATA['obdobi'] def download_heslare(task=None):
TYP_AKCE = _DATA['typ_akce'] """Fetches the codelists from the AMČR API and saves it to a CSV file."""
AREAL = _DATA['areal'] ensure_codelists_dir()
KRAJE = _DATA['kraj'] all_data = []
ORGANIZACE = _DATA['organizace'] total_sets = len(slovnicek)
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']
def refresh_vedouci_cache(): for index, (interni, api_nazev) in enumerate(slovnicek.items()):
""" # Pokud uživatel task zrušil v liště QGISu
Znovu načte soubor vedouci.csv a aktualizuje globální proměnnou VEDOUCI. if task and task.isCanceled():
Použijeme 'update', aby se zachovala reference na objekt (pokud ho dialog už používá). return False
"""
temp_data = load_all_data()
new_vedouci = temp_data['vedouci']
# Vyčistíme a naplníme existující slovník (in-place update) QgsMessageLog.logMessage(f"Zpracovávám kategorii: {interni}...", "AMČR", Qgis.Info)
# Nyní předáváme task správně do upravené funkce
data = fetch_set(interni, api_nazev, task=task)
if data is None:
return False # Bylo zrušeno uprostřed stahování
all_data.extend(data)
# Reportování postupu (0-100)
if task:
progress = (index + 1) / total_sets * 100
task.setProgress(progress)
# Uložení do CSV
with open(OUTPUT_FILE, 'w', newline='', encoding='utf-8-sig') as f:
fieldnames = ['Název', 'Kód', 'Kategorie']
writer = csv.DictWriter(f, fieldnames=fieldnames, delimiter=';')
writer.writeheader()
writer.writerows(all_data)
return True
def refresh_globals():
"""Znovu načte data ze souborů do globálních proměnných."""
global OBDOBI, TYP_AKCE, AREAL, KRAJE, ORGANIZACE, OKRESY, KATASTRY
global VEDOUCI, PIAN_PRESNOST, TYP_LOKALITY, DRUH_LOKALITY, JISTOTA, LOKALITA_ZACHOVALOST, PRISTUPNOST
data = load_all_data()
OBDOBI.clear()
OBDOBI.update(data.get('obdobi', {}))
TYP_AKCE.clear()
TYP_AKCE.update(data.get('typ_akce', {}))
AREAL.clear()
AREAL.update(data.get('areal', {}))
KRAJE.clear()
KRAJE.update(data.get('kraj', {}))
ORGANIZACE.clear()
ORGANIZACE.update(data.get('organizace', {}))
OKRESY.clear()
OKRESY.update(data.get('okres', {}))
KATASTRY.clear()
KATASTRY.update(data.get('katastr', {}))
VEDOUCI.clear() VEDOUCI.clear()
VEDOUCI.update(new_vedouci) VEDOUCI.update(data.get('vedouci', {}))
return len(VEDOUCI) PIAN_PRESNOST.clear()
PIAN_PRESNOST.update(data.get('pian_presnost', {}))
TYP_LOKALITY.clear()
TYP_LOKALITY.update(data.get('typ_lokality', {}))
DRUH_LOKALITY.clear()
DRUH_LOKALITY.update(data.get('druh_lokality', {}))
JISTOTA.clear()
JISTOTA.update(data.get('jistota', {}))
LOKALITA_ZACHOVALOST.clear()
LOKALITA_ZACHOVALOST.update(data.get('lokalita_zachovalost', {}))
PRISTUPNOST.clear()
PRISTUPNOST.update(data.get('pristupnost', {}))
# Inicializace prázdných diktů, které se naplní hned pod tím
OBDOBI = {}
TYP_AKCE = {}
AREAL = {}
KRAJE = {}
ORGANIZACE = {}
OKRESY = {}
KATASTRY = {}
VEDOUCI = {}
PIAN_PRESNOST = {}
TYP_LOKALITY = {}
DRUH_LOKALITY = {}
JISTOTA = {}
LOKALITA_ZACHOVALOST = {}
PRISTUPNOST = {}
refresh_globals()
+420 -102
View File
@@ -1,88 +1,145 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from qgis.PyQt.QtWidgets import (QDialog, QVBoxLayout, QFormLayout, from qgis.PyQt.QtWidgets import (QDialog, QVBoxLayout,
QLineEdit, QDialogButtonBox, QLineEdit, QDialogButtonBox,
QCheckBox, QGroupBox, QPushButton, QCheckBox, QGroupBox, QPushButton,
QListWidget, QListWidgetItem, QHBoxLayout, QListWidget, QListWidgetItem, QHBoxLayout,
QLabel, QMessageBox, QApplication, QWidget) QMessageBox, QLabel, QFormLayout)
from qgis.PyQt.QtCore import Qt from qgis.PyQt.QtCore import Qt, QSettings
from qgis.core import QgsTask, QgsApplication, QgsMessageLog, Qgis, QgsAuthMethodConfig
from .amcr_codelists import (OBDOBI, TYP_AKCE, KRAJE, AREAL, ORGANIZACE, from .amcr_codelists import (OBDOBI, TYP_AKCE, KRAJE, AREAL, ORGANIZACE,
OKRESY, KATASTRY, VEDOUCI, PIAN_PRESNOST, TYP_LOKALITY, OKRESY, KATASTRY, VEDOUCI, PIAN_PRESNOST, TYP_LOKALITY,
DRUH_LOKALITY, JISTOTA, LOKALITA_ZACHOVALOST, DRUH_LOKALITY, JISTOTA, LOKALITA_ZACHOVALOST, PRISTUPNOST,
download_vedouci, refresh_vedouci_cache) download_heslare, refresh_globals)
class UpdateCodelistsTask(QgsTask):
def __init__(self, description):
super().__init__(description, QgsTask.CanCancel)
self.success = False
self.exception = None
def run(self):
"""Tato část běží ve vedlejším vlákně."""
try:
# Voláme upravenou funkci
self.success = download_heslare(task=self)
return self.success
except Exception as e:
self.exception = e
return False
def finished(self, result):
"""Tato část běží v hlavním vlákně po skončení run()."""
if result:
# Teď bezpečně aktualizujeme globální proměnné v hlavním vlákně
refresh_globals()
QgsMessageLog.logMessage("Hesláře AMČR byly úspěšně aktualizovány.", "AMČR", Qgis.Info)
else:
if self.isCanceled():
QgsMessageLog.logMessage("Aktualizace heslářů byla zrušena.", "AMČR", Qgis.Warning)
else:
QgsMessageLog.logMessage(f"Chyba aktualizace: {self.exception}", "AMČR", Qgis.Critical)
class FilterableSelectionDialog(QDialog): class FilterableSelectionDialog(QDialog):
"""
A custom dialog for selecting multiple items from a list with a search filter.
Updated for PyQt6/Qt6 compatibility.
"""
def __init__(self, title, data_dict, preselected_codes, parent=None): 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)
buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
# Standard OK/Cancel dialog buttons
buttons = QDialogButtonBox(
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.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)
item.setData(Qt.UserRole, code)
item.setFlags(item.flags() | Qt.ItemIsUserCheckable) # Store the actual code (ID) hidden in the UserRole
item.setData(Qt.ItemDataRole.UserRole, code)
# Make the item checkable (adds a checkbox)
item.setFlags(item.flags() | Qt.ItemFlag.ItemIsUserCheckable)
# Restore previous selection state
if code in self.preselected: if code in self.preselected:
item.setCheckState(Qt.Checked) item.setCheckState(Qt.CheckState.Checked)
else: else:
item.setCheckState(Qt.Unchecked) item.setCheckState(Qt.CheckState.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)
if search_text not in item.text().lower(): item.setHidden(search_text not in item.text().lower())
item.setHidden(True)
else:
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()):
item = self.list_widget.item(i) item = self.list_widget.item(i)
if item.checkState() == Qt.Checked: if item.checkState() == Qt.CheckState.Checked:
codes.append(item.data(Qt.UserRole)) codes.append(item.data(Qt.ItemDataRole.UserRole))
labels.append(item.text()) labels.append(item.text())
return codes, labels return codes, labels
# --- 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().__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 dictionary to store selected codes for each category
# Cache for filtering
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': [], 'pristupnost': [],
'typ_lokality': [], 'druh_lokality': [], 'jistota': [], 'lokalita_zachovalost': [] 'typ_lokality': [], 'druh_lokality': [], 'jistota': [], 'lokalita_zachovalost': []
} }
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)
@@ -95,13 +152,95 @@ class AmcrFilterDialog(QDialog):
layout.addSpacing(10) layout.addSpacing(10)
def setup_picker(label_text, cache_key, data_source, extra_btn=None): # Spatial information valid for all
row_widget = QGroupBox(label_text)
# row_widget.setFlat(True)
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)
self.picker_pristupnost = self.setup_picker("Přístupnost", 'pristupnost', PRISTUPNOST)
layout.addWidget(self.picker_pristupnost)
# Filters valid for Akce
if self.typ_dat == "akce":
self.picker_org = self.setup_picker("Organizace", 'organizace', ORGANIZACE)
layout.addWidget(self.picker_org)
self.picker_vedouci = self.setup_picker("Vedoucí výzkumu", 'vedouci', VEDOUCI)
layout.addWidget(self.picker_vedouci)
# Type of event
self.picker_typ = self.setup_picker("Typ výzkumu", 'typ_akce', TYP_AKCE)
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/Update buttons
buttons = QDialogButtonBox()
self.btn_update = QPushButton("Aktualizovat hesláře 🔄")
self.btn_update.setToolTip("Provede kompletní aktualizaci heslářů AMČR. Toto bude trvat pár minut.")
self.btn_update.clicked.connect(self.action_update_heslare)
buttons.addButton(self.btn_update, QDialogButtonBox.ButtonRole.ActionRole)
buttons.addButton(QDialogButtonBox.StandardButton.Ok)
buttons.addButton(QDialogButtonBox.StandardButton.Cancel)
buttons.accepted.connect(self.accept)
buttons.rejected.connect(self.reject)
layout.addWidget(buttons)
self.setLayout(layout)
def setup_picker(self, label_text, cache_key, data_source, extra_btn=None):
"""
Creates a reusable UI component consisting of a label, a read-only
text field showing selected items, and a button to open the selection dialog.
"""
row_widget = QGroupBox(label_text)
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 +249,20 @@ 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.DialogCode.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 +272,35 @@ 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 def action_update_heslare(self):
# Vytvoření instance tasku
task = UpdateCodelistsTask("Aktualizace heslářů AMČR")
self.picker_kraj = setup_picker("Kraj", 'kraj', KRAJE) # Povolíme tlačítko zpět bez ohledu na výsledek
layout.addWidget(self.picker_kraj) task.taskCompleted.connect(lambda: self.btn_update.setEnabled(True))
task.taskTerminated.connect(lambda: self.btn_update.setEnabled(True))
self.picker_okres = setup_picker("Okres", 'okres', OKRESY) task.taskCompleted.connect(lambda: QMessageBox.information(self, "Hotovo", "Hesláře byly úspěšně aktualizovány."))
layout.addWidget(self.picker_okres)
self.picker_katastr = setup_picker("Katastr", 'katastr', KATASTRY) # Ošetření, aby se přesně ukázala případná chyba
layout.addWidget(self.picker_katastr) def on_error():
if task.exception:
self.picker_presnost = setup_picker("PIAN přesnost", 'pian_presnost', PIAN_PRESNOST) # Tohle ti přesně řekne, na čem to teď padá (např. PermissionError)
layout.addWidget(self.picker_presnost) msg = f"Aktualizace selhala z důvodu chyby:\n{str(task.exception)}"
# 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):
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: else:
QApplication.restoreOverrideCursor() msg = "Aktualizace byla zrušena uživatelem."
QMessageBox.warning(self, "Chyba", f"Nepodařilo se stáhnout data:\n{msg}") QMessageBox.warning(self, "Chyba / Zrušeno", msg)
except Exception as e:
QApplication.restoreOverrideCursor() task.taskTerminated.connect(on_error)
QMessageBox.critical(self, "Chyba", str(e))
QgsApplication.taskManager().addTask(task)
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 +309,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']:
@@ -239,6 +324,8 @@ class AmcrFilterDialog(QDialog):
filters['f_areal'] = self.selection_cache['areal'] filters['f_areal'] = self.selection_cache['areal']
if self.selection_cache['pian_presnost']: if self.selection_cache['pian_presnost']:
filters['f_pian_presnost'] = self.selection_cache['pian_presnost'] filters['f_pian_presnost'] = self.selection_cache['pian_presnost']
if self.selection_cache['pristupnost']:
filters['pristupnost'] = self.selection_cache['pristupnost']
if self.typ_dat == "akce": if self.typ_dat == "akce":
if self.chk_posevidence.isChecked(): if self.chk_posevidence.isChecked():
@@ -261,3 +348,234 @@ class AmcrFilterDialog(QDialog):
filters['f_lokalita_zachovalost'] = self.selection_cache['lokalita_zachovalost'] filters['f_lokalita_zachovalost'] = self.selection_cache['lokalita_zachovalost']
return filters return filters
class LoginDialog(QDialog):
"""
Dialog for saving AMČR login credentials securely in the QGIS Authentication Manager.
Credentials are encrypted by the platform's native secret storage
(DPAPI on Windows, Keychain on macOS, encrypted SQLite on Linux).
The auth config ID is persisted in QSettings so the session can be
restored automatically after a QGIS restart.
Note on QgsAuthManager quirks (QGIS 4 / Python bindings):
- hasConfigId() is unreliable it checks an in-memory cache that may not
be populated yet. We never use it as a hard gate; we skip it and call
loadAuthenticationConfig() directly instead.
- storeAuthenticationConfig() and loadAuthenticationConfig() both have
SIP_INOUT on their config parameter, so Python bindings return a tuple
(bool, QgsAuthMethodConfig) rather than just bool. Always unpack both.
"""
SETTINGS_KEY = "amcr_viewer/auth_config_id"
CONFIG_NAME = "AMČR Viewer"
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Přihlášení do AMČR")
self.setMinimumWidth(360)
layout = QVBoxLayout()
# Check whether a config ID is already stored from a previous session.
# We attempt a lightweight load (full=False) to confirm it is readable,
# since hasConfigId() may return False even for valid configs (cache lag).
existing_id = QSettings().value(self.SETTINGS_KEY, "")
self._has_saved = bool(existing_id) and bool(self._load_username_from_config(existing_id))
if self._has_saved:
info = QLabel("✔ Přihlašovací údaje jsou bezpečně uloženy ve správci autentizace QGIS.\n"
"Vyplňte pole níže pouze pokud je chcete změnit.")
info.setStyleSheet("color: green; font-style: italic;")
else:
info = QLabel("Zadejte přihlašovací údaje k Digitálnímu archivu AMČR.\n"
"Budou zašifrovaně uloženy ve správci autentizace QGIS.")
info.setWordWrap(True)
layout.addWidget(info)
layout.addSpacing(8)
form = QFormLayout()
self.txt_user = QLineEdit()
self.txt_user.setPlaceholderText("např. jan.novak@email.cz")
# Pre-fill the stored username (not sensitive) for convenience
if self._has_saved:
self.txt_user.setText(self._load_username_from_config(existing_id))
form.addRow("E-mail:", self.txt_user)
self.txt_pass = QLineEdit()
self.txt_pass.setEchoMode(QLineEdit.EchoMode.Password)
self.txt_pass.setPlaceholderText(
"ponechte prázdné pro zachování stávajícího hesla" if self._has_saved else "heslo"
)
form.addRow("Heslo:", self.txt_pass)
layout.addLayout(form)
layout.addSpacing(8)
if self._has_saved:
btn_forget = QPushButton("Odebrat uložené přihlašovací údaje")
btn_forget.setStyleSheet("color: #c0392b;")
btn_forget.clicked.connect(self._forget_credentials)
layout.addWidget(btn_forget)
layout.addStretch(1)
buttons = QDialogButtonBox(
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
)
buttons.accepted.connect(self._save_and_accept)
buttons.rejected.connect(self.reject)
layout.addWidget(buttons)
self.setLayout(layout)
# ------------------------------------------------------------------
# Private helpers
# ------------------------------------------------------------------
@staticmethod
def _load_config(config_id: str, full: bool = False):
"""
Attempt to load a QgsAuthMethodConfig by ID.
Returns (ok, cfg). Never raises; returns (False, empty cfg) on any error.
full=True decrypts and includes the password.
"""
try:
auth_mgr = QgsApplication.authManager()
cfg = QgsAuthMethodConfig()
result = auth_mgr.loadAuthenticationConfig(config_id, cfg, full)
# Python bindings return (bool, cfg) due to SIP_INOUT parameter
if isinstance(result, tuple):
return result
return result, cfg
except Exception:
return False, QgsAuthMethodConfig()
def _load_username_from_config(self, config_id: str) -> str:
"""Load just the username from a stored config (no password decryption)."""
ok, cfg = self._load_config(config_id, full=False)
return cfg.config("username", "") if ok else ""
def _ensure_master_password(self) -> bool:
"""
Ensure the Auth Manager is unlocked before writing.
Prompts the user to set or enter the master password if needed.
Returns True if the manager is ready, False if the user cancelled.
"""
auth_mgr = QgsApplication.authManager()
if auth_mgr.isDisabled():
QMessageBox.critical(
self, "Správce autentizace nedostupný",
"Správce autentizace QGIS je zakázán nebo poškozený.\n"
"Zkuste obnovit databázi: Nastavení → Možnosti → Autentizace → Pomůcky."
)
return False
# setMasterPassword(True) shows the QGIS master password dialog if needed
if not auth_mgr.setMasterPassword(True):
return False # User cancelled the master password dialog
return True
# ------------------------------------------------------------------
# Button actions
# ------------------------------------------------------------------
def _save_and_accept(self):
username = self.txt_user.text().strip()
password = self.txt_pass.text()
if not username:
QMessageBox.warning(self, "Chybí údaje", "Vyplňte prosím e-mailovou adresu.")
return
existing_id = QSettings().value(self.SETTINGS_KEY, "")
auth_mgr = QgsApplication.authManager()
# If a config already exists and the password field is blank,
# update only the username and keep the existing encrypted password.
if not password and existing_id:
ok, cfg = self._load_config(existing_id, full=True)
if ok:
if not self._ensure_master_password():
return
cfg.setConfig("username", username)
auth_mgr.updateAuthenticationConfig(cfg)
self.accept()
return
if not password:
QMessageBox.warning(self, "Chybí údaje", "Vyplňte prosím heslo.")
return
if not self._ensure_master_password():
return
cfg = QgsAuthMethodConfig()
cfg.setName(self.CONFIG_NAME)
cfg.setMethod("Basic")
cfg.setConfig("username", username)
cfg.setConfig("password", password) # nosec B106
settings = QSettings()
# Try to update an existing config first; fall back to creating a new one.
# We skip hasConfigId() as it may return False despite the config existing
# (in-memory cache may not be populated yet in QGIS 4).
ok_load, existing_cfg = self._load_config(existing_id, full=False) if existing_id else (False, None)
if ok_load:
cfg.setId(existing_id)
ok, cfg = auth_mgr.updateAuthenticationConfig(cfg)
else:
ok, cfg = auth_mgr.storeAuthenticationConfig(cfg)
config_id = cfg.id() if cfg else ""
if not ok or not config_id:
QMessageBox.critical(
self, "Chyba uložení",
"Přihlašovací údaje se nepodařilo uložit do správce autentizace QGIS.\n"
"Zkuste restartovat QGIS a přihlásit se znovu."
)
return
settings.setValue(self.SETTINGS_KEY, config_id)
self.accept()
def _forget_credentials(self):
settings = QSettings()
existing_id = settings.value(self.SETTINGS_KEY, "")
if existing_id:
QgsApplication.authManager().removeAuthenticationConfig(existing_id)
settings.remove(self.SETTINGS_KEY)
QMessageBox.information(self, "Hotovo", "Uložené přihlašovací údaje byly odebrány.")
self.reject()
# ------------------------------------------------------------------
# Public static API call this anywhere in the plugin to get credentials
# ------------------------------------------------------------------
@staticmethod
def get_credentials() -> tuple[str, str]:
"""
Retrieve (username, password) from the QGIS Authentication Manager.
Returns ('', '') if no credentials are stored or the manager is locked.
Note: hasConfigId() is intentionally skipped here it checks an
in-memory cache that may lag behind the actual database contents,
causing false negatives (see class docstring). loadAuthenticationConfig()
is called directly and its return value is used as the authoritative result.
"""
settings = QSettings()
config_id = settings.value(LoginDialog.SETTINGS_KEY, "")
if not config_id:
return "", ""
ok, cfg = LoginDialog._load_config(config_id, full=True)
if not ok:
return "", ""
return cfg.config("username", ""), cfg.config("password", "") # nosec B106
+358 -202
View File
@@ -1,21 +1,129 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from qgis.gui import QgsMapToolIdentifyFeature
from qgis.core import (QgsProject, QgsVectorLayer, QgsFeature, QgsGeometry, from qgis.core import (QgsProject, QgsVectorLayer, QgsFeature, QgsGeometry,
QgsField, QgsCoordinateReferenceSystem, QgsCoordinateTransform, QgsField, QgsCoordinateReferenceSystem, QgsCoordinateTransform,
QgsWkbTypes, QgsRelation, QgsEditorWidgetSetup) QgsWkbTypes, Qgis, QgsApplication, QgsAuthMethodConfig, QgsMessageLog)
from qgis.utils import iface from qgis.utils import iface
from qgis.PyQt.QtCore import QVariant, Qt from qgis.PyQt.QtCore import Qt, QMetaType
from qgis.PyQt.QtWidgets import QMessageBox, QApplication from qgis.PyQt.QtWidgets import QApplication
from qgis.PyQt.QtGui import QCursor
import requests import requests
import json import json
import xml.etree.ElementTree as ET
import re
# Global translations cache # Global cache to store translated terms from the Digital Archive
TRANSLATIONS = {} TRANSLATIONS = {}
# Download Digiarchive's vocabulary # Session s autentizační cookie po přihlášení; None = nepřihlášen (anonymní přístup)
AMCR_SESSION: requests.Session | None = None
def _log(msg: str, level=Qgis.MessageLevel.Info):
"""Shortcut: zapíše zprávu do QGIS logu (panel Zprávy → záložka AMČR)."""
QgsMessageLog.logMessage(msg, "AMČR login", level)
def login_to_api(username: str, password: str):
"""
Přihlásí se do Digiarchiv API pomocí username a hesla.
Vrátí requests.Session s nastavenou session cookie, nebo None při chybě.
"""
login_url = "https://digiarchiv.aiscr.cz/api/user/login"
_log(f"Přihlašuji uživatele: '{username}'")
if not username or not password:
_log("CHYBA: username nebo heslo je prázdné.", Qgis.MessageLevel.Critical)
return None
session = requests.Session()
session.headers.update({
"Accept": "application/json, text/plain, */*",
"Content-Type": "application/json",
"User-Agent": "QGIS-Plugin/1.0 (AISCR Data Fetcher)"
})
try:
_log(f"Odesílám POST na {login_url} ...")
response = session.post(login_url, json={"user": username, "pwd": password}, timeout=10)
_log(f"HTTP status: {response.status_code}")
response.raise_for_status()
# API vrací chyby se status kódem 200 je nutné zkontrolovat tělo odpovědi
body = response.json()
if "error" in body:
_log(f"CHYBA přihlášení (API): {body['error']}", Qgis.MessageLevel.Critical)
return None
_log("Přihlášení proběhlo úspěšně.")
global AMCR_SESSION
AMCR_SESSION = session
return session
except requests.exceptions.HTTPError as e:
_log(f"CHYBA HTTP {e.response.status_code if e.response else '?'}: "
f"{e.response.text[:300] if e.response else 'žádná odpověď'}", Qgis.MessageLevel.Critical)
return None
except requests.exceptions.RequestException as e:
_log(f"CHYBA sítě: {e}", Qgis.MessageLevel.Critical)
return None
def _get_session() -> requests.Session | None:
"""
Vrátí aktivní session. Pokud žádná není (restart QGIS), pokusí se
automaticky přihlásit pomocí uložených přihlašovacích údajů.
Vrátí None pokud přihlašovací údaje nejsou uloženy.
"""
global AMCR_SESSION
if AMCR_SESSION is not None:
return AMCR_SESSION
# Zkusit auto-login pomocí uložených údajů
from .amcr_dialog import LoginDialog
username, password = LoginDialog.get_credentials()
if username and password:
_log("Session vypršela nebo chybí automatické přihlášení...")
AMCR_SESSION = login_to_api(username, password)
return AMCR_SESSION
def _api_get(url, params, timeout=30) -> requests.Response:
"""
Provede GET request. Pokud API signalizuje vypršení přihlášení,
provede jedno opakované přihlášení a zkusí znovu.
"""
global AMCR_SESSION
def _is_auth_error(resp: requests.Response) -> bool:
"""API vrací auth chyby se status 200 je nutné zkontrolovat tělo."""
if resp.status_code == 401:
return True
try:
body = resp.json()
err = str(body.get("error", "")).lower()
return "unauthorized" in err or "not logged" in err or "session" in err
except Exception:
return False
session = _get_session()
resp = (session or requests).get(url, params=params, timeout=timeout)
if _is_auth_error(resp):
_log("Session vypršela během stahování obnovuji přihlášení...", Qgis.MessageLevel.Warning)
AMCR_SESSION = None # Zrušit starou session
from .amcr_dialog import LoginDialog
username, password = LoginDialog.get_credentials()
if username and password:
AMCR_SESSION = login_to_api(username, password)
if AMCR_SESSION:
resp = AMCR_SESSION.get(url, params=params, timeout=timeout)
else:
_log("Opakované přihlášení selhalo.", Qgis.MessageLevel.Critical)
else:
_log("Přihlašovací údaje nejsou uloženy pokračuji anonymně.", Qgis.MessageLevel.Warning)
return resp
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,45 +134,55 @@ 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"
iface.messageBar().pushMessage("AMCR", "Hledám záznamy...", level=1) iface.messageBar().pushMessage("AMCR", "Hledám záznamy...", level=Qgis.MessageLevel.Info)
QApplication.setOverrideCursor(Qt.WaitCursor) QApplication.setOverrideCursor(QCursor(Qt.CursorShape.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 +194,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 = []
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:
@@ -91,7 +213,7 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
del base_params['page'] del base_params['page']
try: try:
resp_docs = requests.get(url, params=base_params, timeout=30) resp_docs = _api_get(url, params=base_params, timeout=30)
resp_json = resp_docs.json() resp_json = resp_docs.json()
data = resp_json.get('response', {}) data = resp_json.get('response', {})
batch_docs = data.get('docs', []) batch_docs = data.get('docs', [])
@@ -100,6 +222,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')
@@ -113,41 +236,38 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
if len(docs) >= num_found: if len(docs) >= num_found:
break break
if len(docs) >= MAX_LIMIT: if len(docs) >= MAX_LIMIT:
iface.messageBar().pushMessage("AMCR", f"Limit {MAX_LIMIT} záznamů dosažen.", level=1) iface.messageBar().pushMessage("AMCR", f"Limit {MAX_LIMIT} záznamů dosažen.", level=Qgis.MessageLevel.Warning)
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}")
break break
if not docs: if not docs:
iface.messageBar().pushMessage("AMCR", "Žádné záznamy nenalezeny.", level=1) iface.messageBar().pushMessage("AMCR", "Žádné záznamy nenalezeny.", level=Qgis.MessageLevel.Warning)
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
for doc in docs: # Helper function for safe single-value extraction
piani = doc.get('az_dj_pian', []) def g(doc, key, default=""):
if not piani:
continue
actions_with_geom += 1
def g(key, default=""):
val = doc.get(key) val = doc.get(key)
if isinstance(val, list): if isinstance(val, list):
return str(val[0]) if val else default return str(val[0]) if val else default
return str(val) if val is not None else default return str(val) if val is not None else default
def g_list(key, translate=False): # Helper function for safe list-value extraction and joining
def g_list(doc, key, translate=False):
val = doc.get(key, []) val = doc.get(key, [])
if not isinstance(val, list): if not isinstance(val, list):
val = [val] if val else [] val = [val] if val else []
@@ -155,9 +275,19 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
return ", ".join([tr_code(str(x)) for x in val if x]) return ", ".join([tr_code(str(x)) for x in val if x])
return ", ".join([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:
continue
actions_with_geom += 1
# Extract protected data (fields not available in public Solr index)
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 +298,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,63 +326,82 @@ 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
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)
if komponenty == "true": if komponenty == "true":
# One feature per component — all data on a single row, no relations needed
komps = dj.get('dj_komponenta', []) komps = dj.get('dj_komponenta', [])
if komps:
for komp in komps: for komp in komps:
feat = QgsFeature() komp_meta = {
atributy = [ **dj_meta,
komp.get('ident_cely', ""), 'komponenta_id': komp.get('ident_cely', ""),
dj_id, 'komponenta_areal': komp.get('komponenta_areal', {}).get('value', ""),
# komponenta_aktivita ..?, 'komponenta_obdobi': komp.get('komponenta_obdobi', {}).get('value', ""),
komp.get('komponenta_areal', {}).get('value', ""), }
komp.get('komponenta_obdobi', {}).get('value', "") pian_lookup[dj_pian_value].append(komp_meta)
] target_pian_ids_count += 1
feat.setAttributes(atributy) else:
feats_k.append(feat) # DJ without components — still include with empty component fields
empty_meta = {
**dj_meta,
'komponenta_id': "",
'komponenta_areal': "",
'komponenta_obdobi': "",
}
pian_lookup[dj_pian_value].append(empty_meta)
target_pian_ids_count += 1
else:
target_pian_ids_count += 1
pian_lookup[dj_pian_value].append(dj_meta)
if not target_pian_ids: if not target_pian_ids:
iface.messageBar().pushMessage("AMCR", f"Nalezeno {len(docs)} záznamů, ale žádný nemá geometrii.", level=1) iface.messageBar().pushMessage("AMCR", f"Nalezeno {len(docs)} záznamů, ale žádný nemá geometrii.", level=Qgis.MessageLevel.Warning)
return return
# ========================================== # ==========================================
# 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=Qgis.MessageLevel.Info)
# 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):
@@ -268,87 +418,116 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
} }
try: try:
QApplication.processEvents() QApplication.processEvents()
r_pian = requests.get(url, params=params_pian, timeout=15) r_pian = _api_get(url, params=params_pian, timeout=15)
batch_docs = r_pian.json().get('response', {}).get('docs', []) batch_docs = r_pian.json().get('response', {}).get('docs', [])
docs_pian.extend(batch_docs) docs_pian.extend(batch_docs)
except Exception as e: except Exception as e:
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") archeologicky_zaznam = "Akce" if typ_dat == "akce" else "Lokalita"
vl_point = QgsVectorLayer("Point?crs=epsg:5514", "AMCR Body", "memory")
# 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] layers = [vl_poly, vl_line, vl_point]
if typ_dat == "akce": # Define attribute table structure
archeologicky_zaznam = "Akce"
elif typ_dat == "lokalita":
archeologicky_zaznam = "Lokalita"
# Definice sloupců atributové tabulky
cols = [ cols = [
QgsField("PIAN", QVariant.String), QgsField("pian", QMetaType.Type.QString),
QgsField("Přesnost", QVariant.String), QgsField("presnost", QMetaType.Type.QString),
QgsField("PIAN typ", QVariant.String), QgsField("pian_typ", QMetaType.Type.QString),
QgsField("Dokumentační jednotka", QVariant.String), QgsField("dj", QMetaType.Type.QString),
QgsField("Typ dokumentační jednotky", QVariant.String), QgsField("typ_dj", QMetaType.Type.QString),
QgsField("Definiční bod(y) (WGS-84)", QVariant.String), QgsField("definicni_body", QMetaType.Type.QString),
QgsField(archeologicky_zaznam, QVariant.String), QgsField(typ_dat, QMetaType.Type.QString),
QgsField("Odkaz do Digitálního archivu AMČR", QVariant.String), QgsField("odkaz_do_digiarchivu", QMetaType.Type.QString),
QgsField("Okres", QVariant.String), QgsField("okres", QMetaType.Type.QString),
QgsField("Katastr", QVariant.String), QgsField("katastr", QMetaType.Type.QString),
QgsField("Další katastry", QVariant.String) QgsField("dalsi_katastry", QMetaType.Type.QString)
] ]
# Extend table based on data type
if typ_dat == "akce": if typ_dat == "akce":
cols += [ cols += [
QgsField("Vedoucí akce", QVariant.String), QgsField("akce_lokalizace", QMetaType.Type.QString),
QgsField("Organizace", QVariant.String), QgsField("vedouci", QMetaType.Type.QString),
QgsField("Specifikace data", QVariant.String), QgsField("organizace", QMetaType.Type.QString),
QgsField("Datum zahájeni", QVariant.String), QgsField("specifikace_data", QMetaType.Type.QString),
QgsField("Datum ukončení", QVariant.String), QgsField("zahajeni", QMetaType.Type.QString),
QgsField("Hlavní typ", QVariant.String), QgsField("ukonceni", QMetaType.Type.QString),
QgsField("Vedlejší typ", QVariant.String), QgsField("hlavni_typ", QMetaType.Type.QString),
QgsField("Zjištění", QVariant.String), QgsField("vedlejsi_typ", QMetaType.Type.QString),
QgsField("Akce lokalizace", QVariant.String), QgsField("zjisteni", QMetaType.Type.QString),
QgsField("Akce nahrazuje NZ", QVariant.String), QgsField("nahrazuje_NZ", QMetaType.Type.QString),
] ]
elif typ_dat == "lokalita": elif typ_dat == "lokalita":
cols += [ cols += [
QgsField("Název lokality", QVariant.String), QgsField("nazev_lokality", QMetaType.Type.QString),
QgsField("Popis lokality", QVariant.String), QgsField("popis_lokality", QMetaType.Type.QString),
QgsField("Typ lokality", QVariant.String), QgsField("typ_lokality", QMetaType.Type.QString),
QgsField("Druh lokality", QVariant.String), QgsField("druh_lokality", QMetaType.Type.QString),
QgsField("Zachovalost", QVariant.String) QgsField("zachovalost", QMetaType.Type.QString)
] ]
cols.append(QgsField("Přístupnost", QVariant.String)) cols.append(QgsField("Přístupnost", QMetaType.Type.QString))
# Use aliases for technical field names
alias_map = {
"pian": "PIAN",
"presnost": "Přesnost",
"pian_typ": "PIAN typ",
"dj": "Dokumentační jednotka",
"typ_dj": "Typ dokumentační jednotky",
"definicni_body": "Definiční bod(y) (WGS-84)",
typ_dat: archeologicky_zaznam,
"odkaz_do_digiarchivu": "Odkaz do Digitálního archivu AMČR",
"okres": "Okres",
"katastr": "Katastr",
"dalsi_katastry": "Další katastry",
"akce_lokalizace": "Akce lokalizace",
"vedouci": "Vedoucí akce",
"organizace": "Organizace",
"specifikace_data": "Specifikace data",
"zahajeni": "Datum zahájeni",
"ukonceni": "Datum ukončení",
"hlavni_typ": "Hlavní typ",
"vedlejsi_typ": "Vedlejší typ",
"zjisteni": "Zjištění",
"nahrazuje_NZ": "Akce nahrazuje NZ",
"nazev_lokality": "Název lokality",
"popis_lokality": "Popis lokality",
"typ_lokality": "Typ lokality",
"druh_lokality": "Druh lokality",
"zachovalost": "Zachovalost",
"komponenta": "Komponenta",
"komponenta_areal": "Areál",
"komponenta_obdobi": "Období",
}
if komponenty == "true": if komponenty == "true":
vl_komponenty = QgsVectorLayer("None", "AMCR Komponenty", "memory") cols += [
pr = vl_komponenty.dataProvider() QgsField("komponenta", QMetaType.Type.QString),
komponenty_cols = [ QgsField("komponenta_areal", QMetaType.Type.QString),
QgsField("komponenta", QVariant.String), # ident_cely QgsField("komponenta_obdobi", QMetaType.Type.QString),
QgsField("dj_id", QVariant.String),
# potenciálně QgsField("komponenta_aktivita", QVariant.String),
QgsField("komponenta_areal", QVariant.String),
QgsField("komponenta_obdobi", QVariant.String)
] ]
pr.addAttributes(komponenty_cols)
vl_komponenty.updateFields()
idx_dj_id = vl_komponenty.fields().indexOf("dj_id")
text_setup = QgsEditorWidgetSetup("TextEdit", {})
vl_komponenty.setEditorWidgetSetup(idx_dj_id, text_setup)
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', '')
@@ -357,133 +536,110 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
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
referenced_layer = vl_poly
elif t == QgsWkbTypes.LineGeometry:
target_list = feats_l
referenced_layer = vl_line
elif t == QgsWkbTypes.PointGeometry:
target_list = feats_pt
referenced_layer = vl_point
if target_list is None:
continue
is_akce = (typ_dat == "akce")
# Create a QGIS feature for each documentation unit associated with this geometry
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) if komponenty == "true":
atributy.extend([
meta.get('komponenta_id', ""),
meta.get('komponenta_areal', ""),
meta.get('komponenta_obdobi', ""),
])
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"),
] ]
if komponenty == "true":
layers_to_process.append((feats_k, vl_komponenty, "Komponenty"))
for f, l, n in layers_to_process: for f, l, n in layers_to_process:
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":
added += len(f) added += len(f)
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=Qgis.MessageLevel.Success)
# Relation
if komponenty == "true":
parent_layers = [
(vl_poly, "Plochy"),
(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.generateId()
if rel.isValid():
rel_manager.addRelation(rel)
else: else:
print(f"Relace pro {label} není valid!") iface.messageBar().pushMessage("AMCR", "Žádná data k zobraze.", level=Qgis.MessageLevel.Info)
else:
iface.messageBar().pushMessage("AMCR", "Žádná data k zobrazení.", level=1)
except Exception as e: except Exception as e:
iface.messageBar().pushMessage("Chyba", str(e), level=2) iface.messageBar().pushMessage("Chyba", str(e), level=Qgis.MessageLevel.Critical)
finally: finally:
# Always restore cursor, even after failure
QApplication.restoreOverrideCursor() QApplication.restoreOverrideCursor()
+94 -21
View File
@@ -1,39 +1,58 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from qgis.PyQt.QtCore import QSettings, QTranslator, QCoreApplication from qgis.PyQt.QtCore import QSettings, QTranslator, QCoreApplication
from qgis.PyQt.QtGui import QIcon from qgis.PyQt.QtGui import QIcon
from qgis.PyQt.QtWidgets import QMenu, QAction, QToolButton from qgis.PyQt.QtWidgets import QMenu, QAction, QToolButton, QDialog
from qgis.core import Qgis
from qgis.utils import iface
from .amcr_tools import load_amcr_data from .amcr_tools import load_amcr_data, login_to_api
from .amcr_dialog import AmcrFilterDialog from .amcr_dialog import AmcrFilterDialog, LoginDialog
from .resources import * 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 +64,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)
# 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) self.actions.append(action)
return action return action
def initGui(self): def initGui(self):
"""
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')
import os # 1. Create a container menu for the plugin
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')
# 1. Vytvoření společného menu
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 +111,88 @@ 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 self.action_login_dialog = self.add_action(
icon_path=icon_akce_path,
text=self.tr(u'Přihlásit se | AMČR Viewer'),
callback=lambda checked=False: self.login(),
parent=self.iface.mainWindow(),
add_to_menu=False,
add_to_toolbar=False
)
self.plugin_menu.addAction(self.action_login_dialog)
# 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.ToolButtonPopupMode.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):
for action in self.actions: """
self.iface.removePluginMenu(self.tr(u'&AMČR Viewer'), action) Called when the plugin is disabled or removed.
self.iface.removeToolBarIcon(action) 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.menu, 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 result == 1: # If user confirmed the dialog (OK button), gather filters and load data
if result == QDialog.DialogCode.Accepted:
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)
def login(self):
dlg = LoginDialog(parent=self.iface.mainWindow())
result = dlg.exec()
if result == QDialog.DialogCode.Accepted:
username, password = LoginDialog.get_credentials()
session = login_to_api(username, password)
if session:
self.iface.messageBar().pushMessage(
"AMČR", "Přihlášení proběhlo úspěšně.", level=Qgis.MessageLevel.Success
)
else:
self.iface.messageBar().pushMessage(
"AMČR", "Přihlášení se nezdařilo viz záložka AMČR login v panelu Zprávy.", level=Qgis.MessageLevel.Critical
)
+21832 -13606
View File
File diff suppressed because it is too large Load Diff
+5 -7
View File
@@ -5,13 +5,14 @@
[general] [general]
name=AMČR Viewer name=AMČR Viewer
qgisMinimumVersion=3.4 qgisMinimumVersion=3.4.0
qgisMaximumVersion=4.99.0
description=Viewing and downloading the AMČR data. description=Viewing and downloading the AMČR data.
version=1.2.0-rc.1 version=2.0.0-alpha.2
author=David Spáčil author=David Spáčil
email=spacil@arub.cz email=spacil@arub.cz
about=This plugin is intended for downloading the data (Fieldwork events, Sites and their Components) from the Digiarchive of the Archaeological Map of the Czech Republic (https://digiarchiv.aiscr.cz/). As of now, only publicly accessible data can be downloaded. about=This plugin is intended for downloading the data (Fieldwork events, Sites, and their Components) from the Digital archive of the Archaeological Map of the Czech Republic (https://digiarchiv.aiscr.cz/).
tracker=https://github.com/ARUP-CAS/aiscr-qgis-amcr-viewer/issues tracker=https://github.com/ARUP-CAS/aiscr-qgis-amcr-viewer/issues
repository=https://github.com/ARUP-CAS/aiscr-qgis-amcr-viewer repository=https://github.com/ARUP-CAS/aiscr-qgis-amcr-viewer
@@ -30,7 +31,7 @@ homepage=https://amcr-help.aiscr.cz/digiarchiv/qgis-viewer.html
category=Vector category=Vector
icon=download.png icon=download.png
# experimental flag # experimental flag
experimental=False experimental=True
# deprecated flag (applies to the whole plugin, not just a single version) # deprecated flag (applies to the whole plugin, not just a single version)
deprecated=False deprecated=False
@@ -40,9 +41,6 @@ deprecated=False
# Check the documentation for more information. # Check the documentation for more information.
# plugin_dependencies= # plugin_dependencies=
# Category of the plugin: Raster, Vector, Database or Web
# category=
# If the plugin can run on QGIS Server. # If the plugin can run on QGIS Server.
server=False server=False
+1 -1
View File
@@ -6,7 +6,7 @@
# #
# WARNING! All changes made in this file will be lost! # WARNING! All changes made in this file will be lost!
from PyQt5 import QtCore from qgis.PyQt import QtCore
qt_resource_data = b"\ qt_resource_data = b"\
\x00\x00\x04\x0a\ \x00\x00\x04\x0a\