mirror of
https://github.com/ARUP-CAS/aiscr-qgis-amcr-viewer.git
synced 2026-06-17 11:22:53 +02:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a6ebbce4cf | |||
| 88149fbb30 | |||
| c0d054d22a | |||
| ba41039468 | |||
| 499b3b3f0a | |||
| 54f154b264 | |||
| c679e776df | |||
| a5604dfaa8 | |||
| 11f44d025b | |||
| 7f3b2b46fb | |||
| be53edefa5 | |||
| 3be7832b40 | |||
| 8c0c540fa4 | |||
| 8088b32661 | |||
| c17275ef66 | |||
| 9ec866f1d2 | |||
| 5a951edec7 | |||
| 8825ac3272 | |||
| 3957b87a2b |
@@ -210,3 +210,4 @@ __marimo__/
|
||||
|
||||
README_files/
|
||||
README.html
|
||||
amcr_viewer.zip
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[](https://www.gnu.org/licenses/gpl-3.0)
|
||||
|
||||
**Platform:** QGIS 3.4.x
|
||||
**Platform:** QGIS 3.4.0–4.99.0
|
||||
|
||||
**Module Type:** Data Acquisition & Visualization
|
||||
|
||||
@@ -22,8 +22,6 @@
|
||||
* **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.
|
||||
|
||||
---
|
||||
|
||||
## 2. Installation Guide
|
||||
|
||||
**Install the plugin from QGIS plugin repository.**
|
||||
@@ -37,8 +35,6 @@
|
||||
*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.*
|
||||
|
||||
---
|
||||
|
||||
## 3. User Manual
|
||||
|
||||
### 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).
|
||||
|
||||
|
||||
|
||||
### 3.2 Layer Structure & Attributes
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
#### 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 Ages–Modern Period/Middle La Tène (LtB–C1)/…] |
|
||||
| vrstva | system value linking to a specific geometry table with the corresponding documentation unit |
|
||||
|
||||
## 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
|
||||
|
||||
@@ -100,12 +146,10 @@ The plugin interacts with three primary endpoints of the AIS CR infrastructure:
|
||||
* 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.
|
||||
|
||||
|
||||
2. **Translation API:**
|
||||
* 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.
|
||||
|
||||
|
||||
### 4.3 Data Persistence
|
||||
|
||||
* **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.
|
||||
* **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 Viewer tutorial](https://amcr-help.aiscr.cz/digiarchiv/qgis-viewer.html) (only in Czech).
|
||||
|
||||
+207
-142
@@ -1,178 +1,243 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import os
|
||||
import csv
|
||||
import codecs
|
||||
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__)
|
||||
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():
|
||||
"""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}")
|
||||
QgsMessageLog.logMessage(f"AMČR Codelist Read Error for {filename}: {e}", "AMČR", Qgis.Critical)
|
||||
|
||||
return data
|
||||
return target_dict
|
||||
|
||||
def load_all_data():
|
||||
"""
|
||||
Načte statický heslář I dynamický heslář vedoucích.
|
||||
Vrací slovník slovníků.
|
||||
"""
|
||||
"""Loads the codelist 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
|
||||
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:
|
||||
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)
|
||||
|
||||
categorized_data = {k: {} for k in slovnicek.keys()}
|
||||
parse_codelist_file('heslar.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.
|
||||
"""
|
||||
ensure_codelists_dir()
|
||||
def fetch_set(internal_name, api_set, task=None):
|
||||
dataset = []
|
||||
params = {
|
||||
"verb": "ListRecords",
|
||||
"metadataPrefix": "oai_dc",
|
||||
"set": api_set
|
||||
}
|
||||
|
||||
# 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:
|
||||
r = requests.get(url, timeout=20) # Raději delší timeout pro velký seznam
|
||||
r.raise_for_status()
|
||||
data = r.json()
|
||||
|
||||
# Cesta k datům dle tvého JSONu:
|
||||
# {"facet_counts": { "f_vedouci": [ {"name": "Novák", ...}, ... ] }}
|
||||
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', [])
|
||||
while True:
|
||||
# Kontrola zrušení v každém kroku
|
||||
if task and task.isCanceled():
|
||||
return None
|
||||
|
||||
csv_path = os.path.join(CODELISTS_DIR, 'vedouci.csv')
|
||||
|
||||
count = 0
|
||||
with codecs.open(csv_path, 'w', 'utf-8') as f:
|
||||
writer = csv.writer(f, delimiter=';')
|
||||
writer.writerow(['Název', 'Kód', 'Kategorie'])
|
||||
try:
|
||||
response = requests.get(BASE_URL, params=params, timeout=30)
|
||||
response.raise_for_status()
|
||||
root = ET.fromstring(response.content)
|
||||
|
||||
# NOVÁ LOGIKA PARSOVÁNÍ
|
||||
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
|
||||
if name and not str(name).isdigit():
|
||||
writer.writerow([name, name, 'vedouci'])
|
||||
count += 1
|
||||
|
||||
return True, f"Staženo {count} jmen."
|
||||
|
||||
except Exception as e:
|
||||
return False, str(e)
|
||||
records = root.findall('.//oai:record', NS)
|
||||
for rec in records:
|
||||
metadata = rec.find('.//oai_dc:dc', NS)
|
||||
if metadata is not None:
|
||||
# Kód (identifier)
|
||||
kod = metadata.find('dc:identifier', NS).text if metadata.find('dc:identifier', NS) is not None else ""
|
||||
|
||||
# Název (title) - filtrujeme systémové popisky "AMČR - ..."
|
||||
titles = metadata.findall('dc:title', NS)
|
||||
nazev = ""
|
||||
for t in titles:
|
||||
if t.text and not t.text.startswith("AMČR -") and not t.text.startswith(" AMČR -"):
|
||||
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
|
||||
|
||||
specialni_pripady = ['okres', 'katastr']
|
||||
|
||||
# --- GLOBAL DATA ---
|
||||
# Toto se načte při startu QGISu
|
||||
_DATA = load_all_data()
|
||||
if internal_name in specialni_pripady:
|
||||
kod = nazev
|
||||
|
||||
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']
|
||||
if internal_name == 'pristupnost':
|
||||
kod = next((t.text for t in titles if t.text and len(t.text) == 1 and t.text.isalpha()), None)
|
||||
|
||||
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']
|
||||
dataset.append({
|
||||
'Název': nazev,
|
||||
'Kód': kod,
|
||||
'Kategorie': internal_name
|
||||
})
|
||||
|
||||
# Stránkování
|
||||
token = root.find('.//oai:resumptionToken', NS)
|
||||
if token is not None and token.text:
|
||||
params = {
|
||||
"verb": "ListRecords",
|
||||
"resumptionToken": token.text
|
||||
}
|
||||
time.sleep(0.5)
|
||||
else:
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
QgsMessageLog.logMessage(f"Chyba u setu {api_set}: {e}", "AMČR", Qgis.Warning)
|
||||
break
|
||||
|
||||
return dataset
|
||||
|
||||
def download_heslare(task=None):
|
||||
"""Fetches the codelists from the AMČR API and saves it to a CSV file."""
|
||||
ensure_codelists_dir()
|
||||
all_data = []
|
||||
total_sets = len(slovnicek)
|
||||
|
||||
# Vyčistíme a naplníme existující slovník (in-place update)
|
||||
for index, (interni, api_nazev) in enumerate(slovnicek.items()):
|
||||
# Pokud uživatel task zrušil v liště QGISu
|
||||
if task and task.isCanceled():
|
||||
return False
|
||||
|
||||
QgsMessageLog.logMessage(f"Zpracovávám kategorii: {interni}...", "AMČR", Qgis.Info)
|
||||
|
||||
# Nyní předáváme task správně do upravené funkce
|
||||
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.update(new_vedouci)
|
||||
return len(VEDOUCI)
|
||||
VEDOUCI.update(data.get('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()
|
||||
+413
-95
@@ -1,88 +1,145 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from qgis.PyQt.QtWidgets import (QDialog, QVBoxLayout, QFormLayout,
|
||||
from qgis.PyQt.QtWidgets import (QDialog, QVBoxLayout,
|
||||
QLineEdit, QDialogButtonBox,
|
||||
QCheckBox, QGroupBox, QPushButton,
|
||||
QListWidget, QListWidgetItem, QHBoxLayout,
|
||||
QLabel, QMessageBox, QApplication, QWidget)
|
||||
from qgis.PyQt.QtCore import Qt
|
||||
QMessageBox, QLabel, QFormLayout)
|
||||
from qgis.PyQt.QtCore import Qt, QSettings
|
||||
from qgis.core import QgsTask, QgsApplication, QgsMessageLog, Qgis, QgsAuthMethodConfig
|
||||
from .amcr_codelists import (OBDOBI, TYP_AKCE, KRAJE, AREAL, ORGANIZACE,
|
||||
OKRESY, KATASTRY, VEDOUCI, PIAN_PRESNOST, TYP_LOKALITY,
|
||||
DRUH_LOKALITY, JISTOTA, LOKALITA_ZACHOVALOST,
|
||||
download_vedouci, refresh_vedouci_cache)
|
||||
DRUH_LOKALITY, JISTOTA, LOKALITA_ZACHOVALOST, PRISTUPNOST,
|
||||
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):
|
||||
"""
|
||||
A custom dialog for selecting multiple items from a list with a search filter.
|
||||
Updated for PyQt6/Qt6 compatibility.
|
||||
"""
|
||||
def __init__(self, title, data_dict, preselected_codes, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle(f"Výběr: {title}")
|
||||
self.resize(400, 500)
|
||||
|
||||
# Store the source data and previously selected items
|
||||
self.data_dict = data_dict
|
||||
self.preselected = preselected_codes if preselected_codes else []
|
||||
|
||||
layout = QVBoxLayout()
|
||||
|
||||
# Setup search input for filtering items
|
||||
self.search_bar = QLineEdit()
|
||||
self.search_bar.setPlaceholderText("Hledat v seznamu...")
|
||||
self.search_bar.textChanged.connect(self.filter_list)
|
||||
layout.addWidget(self.search_bar)
|
||||
|
||||
# Main list widget for displaying selectable items
|
||||
self.list_widget = QListWidget()
|
||||
self.populate_list()
|
||||
layout.addWidget(self.list_widget)
|
||||
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.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)
|
||||
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:
|
||||
item.setCheckState(Qt.Checked)
|
||||
item.setCheckState(Qt.CheckState.Checked)
|
||||
else:
|
||||
item.setCheckState(Qt.Unchecked)
|
||||
item.setCheckState(Qt.CheckState.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)
|
||||
if search_text not in item.text().lower():
|
||||
item.setHidden(True)
|
||||
else:
|
||||
item.setHidden(False)
|
||||
item.setHidden(search_text not in item.text().lower())
|
||||
|
||||
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()):
|
||||
item = self.list_widget.item(i)
|
||||
if item.checkState() == Qt.Checked:
|
||||
codes.append(item.data(Qt.UserRole))
|
||||
if item.checkState() == Qt.CheckState.Checked:
|
||||
codes.append(item.data(Qt.ItemDataRole.UserRole))
|
||||
labels.append(item.text())
|
||||
return codes, labels
|
||||
|
||||
|
||||
# --- 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)
|
||||
super().__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': [],
|
||||
'typ_akce': [], 'okres': [], 'katastr': [], 'vedouci': [], 'pian_presnost': [], 'pristupnost': [],
|
||||
'typ_lokality': [], 'druh_lokality': [], 'jistota': [], 'lokalita_zachovalost': []
|
||||
}
|
||||
|
||||
layout = QVBoxLayout()
|
||||
|
||||
# Filter by current map canvas extent
|
||||
self.chk_bbox = QCheckBox("Omezit vyhledávání rozsahem okna")
|
||||
self.chk_bbox.setChecked(True)
|
||||
layout.addWidget(self.chk_bbox)
|
||||
@@ -94,129 +151,156 @@ class AmcrFilterDialog(QDialog):
|
||||
layout.addWidget(self.chk_posevidence)
|
||||
|
||||
layout.addSpacing(10)
|
||||
|
||||
def setup_picker(label_text, cache_key, data_source, extra_btn=None):
|
||||
row_widget = QGroupBox(label_text)
|
||||
# row_widget.setFlat(True)
|
||||
|
||||
row_layout = QHBoxLayout()
|
||||
row_layout.setContentsMargins(5, 5, 5, 5)
|
||||
|
||||
display_field = QLineEdit()
|
||||
display_field.setReadOnly(True)
|
||||
display_field.setPlaceholderText("Nic nevybráno (vše)")
|
||||
display_field.setStyleSheet("background-color: #f0f0f0; color: #333;")
|
||||
|
||||
btn = QPushButton("Vybrat...")
|
||||
btn.setFixedWidth(80)
|
||||
|
||||
def open_dialog():
|
||||
dlg = FilterableSelectionDialog(label_text, data_source, self.selection_cache[cache_key], self)
|
||||
if dlg.exec_() == QDialog.Accepted:
|
||||
codes, labels = dlg.get_selected_codes()
|
||||
self.selection_cache[cache_key] = codes
|
||||
if labels:
|
||||
display_field.setText(", ".join(labels))
|
||||
else:
|
||||
display_field.clear()
|
||||
|
||||
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']
|
||||
|
||||
btn.clicked.connect(open_dialog)
|
||||
|
||||
row_layout.addWidget(display_field)
|
||||
row_layout.addWidget(btn)
|
||||
|
||||
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)
|
||||
self.picker_kraj = self.setup_picker("Kraj", 'kraj', KRAJE)
|
||||
layout.addWidget(self.picker_kraj)
|
||||
|
||||
self.picker_okres = setup_picker("Okres", 'okres', OKRESY)
|
||||
self.picker_okres = self.setup_picker("Okres", 'okres', OKRESY)
|
||||
layout.addWidget(self.picker_okres)
|
||||
|
||||
self.picker_katastr = setup_picker("Katastr", 'katastr', KATASTRY)
|
||||
self.picker_katastr = self.setup_picker("Katastr", 'katastr', KATASTRY)
|
||||
layout.addWidget(self.picker_katastr)
|
||||
|
||||
self.picker_presnost = setup_picker("PIAN – přesnost", 'pian_presnost', PIAN_PRESNOST)
|
||||
self.picker_presnost = self.setup_picker("PIAN – přesnost", 'pian_presnost', PIAN_PRESNOST)
|
||||
layout.addWidget(self.picker_presnost)
|
||||
|
||||
self.picker_pristupnost = self.setup_picker("Přístupnost", 'pristupnost', PRISTUPNOST)
|
||||
layout.addWidget(self.picker_pristupnost)
|
||||
|
||||
# Filters valid for Akce
|
||||
|
||||
if self.typ_dat == "akce":
|
||||
self.picker_org = setup_picker("Organizace", 'organizace', ORGANIZACE)
|
||||
self.picker_org = self.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)
|
||||
self.picker_vedouci = self.setup_picker("Vedoucí výzkumu", 'vedouci', VEDOUCI)
|
||||
layout.addWidget(self.picker_vedouci)
|
||||
|
||||
# Type of event
|
||||
|
||||
self.picker_typ = setup_picker("Typ výzkumu", 'typ_akce', TYP_AKCE)
|
||||
self.picker_typ = self.setup_picker("Typ výzkumu", 'typ_akce', TYP_AKCE)
|
||||
layout.addWidget(self.picker_typ)
|
||||
|
||||
# Filters valid for Lokality
|
||||
|
||||
if self.typ_dat == "lokalita":
|
||||
self.picker_typ_lokality = setup_picker("Lokalita – typ", 'typ_lokality', TYP_LOKALITY)
|
||||
self.picker_typ_lokality = self.setup_picker("Lokalita – typ", 'typ_lokality', TYP_LOKALITY)
|
||||
layout.addWidget(self.picker_typ_lokality)
|
||||
|
||||
self.picker_druh_lokality = setup_picker("Lokalita – druh", 'druh_lokality', DRUH_LOKALITY)
|
||||
self.picker_druh_lokality = self.setup_picker("Lokalita – druh", 'druh_lokality', DRUH_LOKALITY)
|
||||
layout.addWidget(self.picker_druh_lokality)
|
||||
|
||||
self.picker_jistota = setup_picker("Lokalita – jistota určení", 'jistota', JISTOTA)
|
||||
self.picker_jistota = self.setup_picker("Lokalita – jistota určení", 'jistota', JISTOTA)
|
||||
layout.addWidget(self.picker_jistota)
|
||||
|
||||
self.picker_lokalita_zachovalost = setup_picker("Lokalita - stav dochování", 'lokalita_zachovalost', LOKALITA_ZACHOVALOST)
|
||||
self.picker_lokalita_zachovalost = self.setup_picker("Lokalita - stav dochování", 'lokalita_zachovalost', LOKALITA_ZACHOVALOST)
|
||||
layout.addWidget(self.picker_lokalita_zachovalost)
|
||||
|
||||
# Contextual information
|
||||
|
||||
self.picker_obdobi = setup_picker("Období", 'obdobi', OBDOBI)
|
||||
self.picker_obdobi = self.setup_picker("Období", 'obdobi', OBDOBI)
|
||||
layout.addWidget(self.picker_obdobi)
|
||||
|
||||
self.picker_areal = setup_picker("Areál", 'areal', AREAL)
|
||||
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)
|
||||
|
||||
buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
||||
# 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 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.")
|
||||
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.setContentsMargins(5, 5, 5, 5)
|
||||
|
||||
# Read-only field displaying the names of selected items
|
||||
display_field = QLineEdit()
|
||||
display_field.setReadOnly(True)
|
||||
display_field.setPlaceholderText("Nic nevybráno (vše)")
|
||||
display_field.setStyleSheet("background-color: #f0f0f0; color: #333;")
|
||||
|
||||
btn = QPushButton("Vybrat...")
|
||||
btn.setFixedWidth(80)
|
||||
|
||||
# Nested function that handles opening the dialog and saving results
|
||||
def open_dialog():
|
||||
dlg = FilterableSelectionDialog(label_text, data_source, self.selection_cache[cache_key], self)
|
||||
if dlg.exec() == QDialog.DialogCode.Accepted:
|
||||
codes, labels = dlg.get_selected_codes()
|
||||
# Update local cache with selected IDs
|
||||
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']
|
||||
|
||||
btn.clicked.connect(open_dialog)
|
||||
|
||||
row_layout.addWidget(display_field)
|
||||
row_layout.addWidget(btn)
|
||||
|
||||
# Add an optional extra button (e.g., the refresh button for leaders)
|
||||
if extra_btn:
|
||||
row_layout.addWidget(extra_btn)
|
||||
|
||||
row_widget.setLayout(row_layout)
|
||||
return row_widget
|
||||
|
||||
def action_update_heslare(self):
|
||||
# Vytvoření instance tasku
|
||||
task = UpdateCodelistsTask("Aktualizace heslářů AMČR")
|
||||
|
||||
# Povolíme tlačítko zpět bez ohledu na výsledek
|
||||
task.taskCompleted.connect(lambda: self.btn_update.setEnabled(True))
|
||||
task.taskTerminated.connect(lambda: self.btn_update.setEnabled(True))
|
||||
|
||||
task.taskCompleted.connect(lambda: QMessageBox.information(self, "Hotovo", "Hesláře byly úspěšně aktualizovány."))
|
||||
|
||||
# Ošetření, aby se přesně ukázala případná chyba
|
||||
def on_error():
|
||||
if task.exception:
|
||||
# Tohle ti přesně řekne, na čem to teď padá (např. PermissionError)
|
||||
msg = f"Aktualizace selhala z důvodu chyby:\n{str(task.exception)}"
|
||||
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))
|
||||
msg = "Aktualizace byla zrušena uživatelem."
|
||||
QMessageBox.warning(self, "Chyba / Zrušeno", msg)
|
||||
|
||||
task.taskTerminated.connect(on_error)
|
||||
|
||||
QgsApplication.taskManager().addTask(task)
|
||||
|
||||
def get_bbox(self):
|
||||
return "true" if self.chk_bbox.isChecked() else "false"
|
||||
@@ -225,6 +309,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']:
|
||||
@@ -239,6 +324,8 @@ class AmcrFilterDialog(QDialog):
|
||||
filters['f_areal'] = self.selection_cache['areal']
|
||||
if 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.chk_posevidence.isChecked():
|
||||
@@ -260,4 +347,235 @@ class AmcrFilterDialog(QDialog):
|
||||
if 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
|
||||
+368
-212
@@ -1,21 +1,129 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from qgis.gui import QgsMapToolIdentifyFeature
|
||||
from qgis.core import (QgsProject, QgsVectorLayer, QgsFeature, QgsGeometry,
|
||||
QgsField, QgsCoordinateReferenceSystem, QgsCoordinateTransform,
|
||||
QgsWkbTypes, QgsRelation, QgsEditorWidgetSetup)
|
||||
QgsWkbTypes, Qgis, QgsApplication, QgsAuthMethodConfig, QgsMessageLog)
|
||||
from qgis.utils import iface
|
||||
from qgis.PyQt.QtCore import QVariant, Qt
|
||||
from qgis.PyQt.QtWidgets import QMessageBox, QApplication
|
||||
from qgis.PyQt.QtCore import Qt, QMetaType
|
||||
from qgis.PyQt.QtWidgets import QApplication
|
||||
from qgis.PyQt.QtGui import QCursor
|
||||
import requests
|
||||
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
|
||||
# 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():
|
||||
"""Fetches the official Czech translation dictionary from the AISCR API."""
|
||||
global TRANSLATIONS
|
||||
if TRANSLATIONS:
|
||||
return
|
||||
@@ -26,45 +134,55 @@ 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"
|
||||
|
||||
iface.messageBar().pushMessage("AMCR", "Hledám záznamy...", level=1)
|
||||
QApplication.setOverrideCursor(Qt.WaitCursor)
|
||||
iface.messageBar().pushMessage("AMCR", "Hledám záznamy...", level=Qgis.MessageLevel.Info)
|
||||
QApplication.setOverrideCursor(QCursor(Qt.CursorShape.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 +194,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
|
||||
|
||||
|
||||
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:
|
||||
@@ -91,7 +213,7 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
|
||||
del base_params['page']
|
||||
|
||||
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()
|
||||
data = resp_json.get('response', {})
|
||||
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:
|
||||
break
|
||||
|
||||
# Filter out duplicates and append to main list
|
||||
new_docs = []
|
||||
for d in batch_docs:
|
||||
ident = d.get('ident_cely')
|
||||
@@ -113,27 +236,46 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
|
||||
if len(docs) >= num_found:
|
||||
break
|
||||
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
|
||||
|
||||
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}")
|
||||
break
|
||||
|
||||
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
|
||||
|
||||
# ==========================================
|
||||
# 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 +283,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 +298,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,63 +326,82 @@ 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
|
||||
if dj_pian_value not in pian_lookup:
|
||||
pian_lookup[dj_pian_value] = []
|
||||
pian_lookup[dj_pian_value].append(dj_meta)
|
||||
|
||||
if komponenty == "true":
|
||||
# One feature per component — all data on a single row, no relations needed
|
||||
komps = dj.get('dj_komponenta', [])
|
||||
for komp in komps:
|
||||
feat = QgsFeature()
|
||||
atributy = [
|
||||
komp.get('ident_cely', ""),
|
||||
dj_id,
|
||||
# komponenta_aktivita ..?,
|
||||
komp.get('komponenta_areal', {}).get('value', ""),
|
||||
komp.get('komponenta_obdobi', {}).get('value', "")
|
||||
]
|
||||
feat.setAttributes(atributy)
|
||||
feats_k.append(feat)
|
||||
if komps:
|
||||
for komp in komps:
|
||||
komp_meta = {
|
||||
**dj_meta,
|
||||
'komponenta_id': komp.get('ident_cely', ""),
|
||||
'komponenta_areal': komp.get('komponenta_areal', {}).get('value', ""),
|
||||
'komponenta_obdobi': komp.get('komponenta_obdobi', {}).get('value', ""),
|
||||
}
|
||||
pian_lookup[dj_pian_value].append(komp_meta)
|
||||
target_pian_ids_count += 1
|
||||
else:
|
||||
# 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:
|
||||
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
|
||||
|
||||
|
||||
# ==========================================
|
||||
# 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)
|
||||
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"]
|
||||
|
||||
for i in range(0, total_pians, BATCH_PIAN):
|
||||
@@ -268,222 +418,228 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
|
||||
}
|
||||
try:
|
||||
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', [])
|
||||
docs_pian.extend(batch_docs)
|
||||
except Exception as e:
|
||||
print(f"Chyba PIAN: {e}")
|
||||
|
||||
# ==========================================
|
||||
# C) TVORBA VRSTEV
|
||||
# D) LAYER CREATION (QGIS Memory Layers)
|
||||
# ==========================================
|
||||
vl_poly = QgsVectorLayer("Polygon?crs=epsg:5514", "AMCR Plochy", "memory")
|
||||
vl_line = QgsVectorLayer("LineString?crs=epsg:5514", "AMCR Linie", "memory")
|
||||
vl_point = QgsVectorLayer("Point?crs=epsg:5514", "AMCR Body", "memory")
|
||||
layers = [vl_poly, vl_line, vl_point]
|
||||
|
||||
if typ_dat == "akce":
|
||||
archeologicky_zaznam = "Akce"
|
||||
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),
|
||||
QgsField("PIAN – typ", QVariant.String),
|
||||
QgsField("Dokumentační jednotka", QVariant.String),
|
||||
QgsField("Typ dokumentační jednotky", QVariant.String),
|
||||
QgsField("Definiční bod(y) (WGS-84)", QVariant.String),
|
||||
QgsField(archeologicky_zaznam, QVariant.String),
|
||||
QgsField("Odkaz do Digitálního archivu AMČR", QVariant.String),
|
||||
QgsField("Okres", QVariant.String),
|
||||
QgsField("Katastr", QVariant.String),
|
||||
QgsField("Další katastry", QVariant.String)
|
||||
QgsField("pian", QMetaType.Type.QString),
|
||||
QgsField("presnost", QMetaType.Type.QString),
|
||||
QgsField("pian_typ", QMetaType.Type.QString),
|
||||
QgsField("dj", QMetaType.Type.QString),
|
||||
QgsField("typ_dj", QMetaType.Type.QString),
|
||||
QgsField("definicni_body", QMetaType.Type.QString),
|
||||
QgsField(typ_dat, QMetaType.Type.QString),
|
||||
QgsField("odkaz_do_digiarchivu", QMetaType.Type.QString),
|
||||
QgsField("okres", QMetaType.Type.QString),
|
||||
QgsField("katastr", QMetaType.Type.QString),
|
||||
QgsField("dalsi_katastry", QMetaType.Type.QString)
|
||||
]
|
||||
|
||||
# Extend table based on data type
|
||||
if typ_dat == "akce":
|
||||
cols += [
|
||||
QgsField("Vedoucí akce", QVariant.String),
|
||||
QgsField("Organizace", QVariant.String),
|
||||
QgsField("Specifikace data", QVariant.String),
|
||||
QgsField("Datum zahájeni", QVariant.String),
|
||||
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("Akce – nahrazuje NZ", QVariant.String),
|
||||
QgsField("akce_lokalizace", QMetaType.Type.QString),
|
||||
QgsField("vedouci", QMetaType.Type.QString),
|
||||
QgsField("organizace", QMetaType.Type.QString),
|
||||
QgsField("specifikace_data", QMetaType.Type.QString),
|
||||
QgsField("zahajeni", QMetaType.Type.QString),
|
||||
QgsField("ukonceni", QMetaType.Type.QString),
|
||||
QgsField("hlavni_typ", QMetaType.Type.QString),
|
||||
QgsField("vedlejsi_typ", QMetaType.Type.QString),
|
||||
QgsField("zjisteni", QMetaType.Type.QString),
|
||||
QgsField("nahrazuje_NZ", QMetaType.Type.QString),
|
||||
]
|
||||
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", QMetaType.Type.QString),
|
||||
QgsField("popis_lokality", QMetaType.Type.QString),
|
||||
QgsField("typ_lokality", QMetaType.Type.QString),
|
||||
QgsField("druh_lokality", QMetaType.Type.QString),
|
||||
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":
|
||||
vl_komponenty = QgsVectorLayer("None", "AMCR Komponenty", "memory")
|
||||
pr = vl_komponenty.dataProvider()
|
||||
komponenty_cols = [
|
||||
QgsField("komponenta", QVariant.String), # ident_cely
|
||||
QgsField("dj_id", QVariant.String),
|
||||
# potenciálně QgsField("komponenta_aktivita", QVariant.String),
|
||||
QgsField("komponenta_areal", QVariant.String),
|
||||
QgsField("komponenta_obdobi", QVariant.String)
|
||||
cols += [
|
||||
QgsField("komponenta", QMetaType.Type.QString),
|
||||
QgsField("komponenta_areal", QMetaType.Type.QString),
|
||||
QgsField("komponenta_obdobi", QMetaType.Type.QString),
|
||||
]
|
||||
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:
|
||||
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
|
||||
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:
|
||||
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'])
|
||||
|
||||
if komponenty == "true":
|
||||
atributy.extend([
|
||||
meta.get('komponenta_id', ""),
|
||||
meta.get('komponenta_areal', ""),
|
||||
meta.get('komponenta_obdobi', ""),
|
||||
])
|
||||
|
||||
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"),
|
||||
]
|
||||
|
||||
if komponenty == "true":
|
||||
layers_to_process.append((feats_k, vl_komponenty, "Komponenty"))
|
||||
|
||||
for f, l, n in layers_to_process:
|
||||
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)
|
||||
added += len(f)
|
||||
|
||||
if added > 0:
|
||||
iface.messageBar().pushMessage("AMCR", f"Hotovo. Záznamů: {len(docs)} (s geom: {actions_with_geom}). Vykresleno: {added} prvků.", level=0)
|
||||
|
||||
# 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:
|
||||
print(f"Relace pro {label} není validní!")
|
||||
|
||||
iface.messageBar().pushMessage("AMCR", f"Hotovo. Záznamů: {len(docs)} (s geom: {actions_with_geom}). Vykresleno: {added} prvků.", level=Qgis.MessageLevel.Success)
|
||||
else:
|
||||
iface.messageBar().pushMessage("AMCR", "Žádná data k zobrazení.", level=1)
|
||||
iface.messageBar().pushMessage("AMCR", "Žádná data k zobrazení.", level=Qgis.MessageLevel.Info)
|
||||
|
||||
except Exception as e:
|
||||
iface.messageBar().pushMessage("Chyba", str(e), level=2)
|
||||
iface.messageBar().pushMessage("Chyba", str(e), level=Qgis.MessageLevel.Critical)
|
||||
finally:
|
||||
QApplication.restoreOverrideCursor()
|
||||
# Always restore cursor, even after failure
|
||||
QApplication.restoreOverrideCursor()
|
||||
+93
-20
@@ -1,39 +1,58 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from qgis.PyQt.QtCore import QSettings, QTranslator, QCoreApplication
|
||||
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_dialog import AmcrFilterDialog
|
||||
from .amcr_tools import load_amcr_data, login_to_api
|
||||
from .amcr_dialog import AmcrFilterDialog, LoginDialog
|
||||
from .resources import *
|
||||
import os.path
|
||||
|
||||
class AmcrViewer:
|
||||
"""
|
||||
Main plugin class that manages the GUI elements, menu entries,
|
||||
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 +64,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 +111,88 @@ class AmcrViewer:
|
||||
)
|
||||
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)
|
||||
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)
|
||||
self.tool_button.setPopupMode(QToolButton.ToolButtonPopupMode.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 result == 1:
|
||||
# If user confirmed the dialog (OK button), gather filters and load data
|
||||
if result == QDialog.DialogCode.Accepted:
|
||||
filters = dlg.get_filters()
|
||||
bbox = dlg.get_bbox()
|
||||
komponenty = dlg.get_komponenty()
|
||||
|
||||
# Access the map canvas and start the fetch/render process from amcr_tools
|
||||
canvas = self.iface.mapCanvas()
|
||||
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
File diff suppressed because it is too large
Load Diff
@@ -5,13 +5,14 @@
|
||||
|
||||
[general]
|
||||
name=AMČR Viewer
|
||||
qgisMinimumVersion=3.4
|
||||
qgisMinimumVersion=3.4.0
|
||||
qgisMaximumVersion=4.99.0
|
||||
description=Viewing and downloading the AMČR data.
|
||||
version=1.2.0-rc.1
|
||||
version=2.0.0-alpha.2
|
||||
author=David Spáčil
|
||||
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
|
||||
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
|
||||
icon=download.png
|
||||
# experimental flag
|
||||
experimental=False
|
||||
experimental=True
|
||||
|
||||
# deprecated flag (applies to the whole plugin, not just a single version)
|
||||
deprecated=False
|
||||
@@ -40,9 +41,6 @@ deprecated=False
|
||||
# Check the documentation for more information.
|
||||
# plugin_dependencies=
|
||||
|
||||
# Category of the plugin: Raster, Vector, Database or Web
|
||||
# category=
|
||||
|
||||
# If the plugin can run on QGIS Server.
|
||||
server=False
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
|
||||
from PyQt5 import QtCore
|
||||
from qgis.PyQt import QtCore
|
||||
|
||||
qt_resource_data = b"\
|
||||
\x00\x00\x04\x0a\
|
||||
|
||||
Reference in New Issue
Block a user