mirror of
https://github.com/ARUP-CAS/aiscr-qgis-amcr-viewer.git
synced 2026-06-17 11:22:53 +02:00
Compare commits
8 Commits
v1.3.2
...
v2.0.0-alpha.2
| Author | SHA1 | Date | |
|---|---|---|---|
| a6ebbce4cf | |||
| 88149fbb30 | |||
| c0d054d22a | |||
| ba41039468 | |||
| 499b3b3f0a | |||
| 54f154b264 | |||
| c679e776df | |||
| a5604dfaa8 |
@@ -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).
|
||||
|
||||
+182
-85
@@ -2,10 +2,38 @@
|
||||
import os
|
||||
import csv
|
||||
import requests
|
||||
import xml.etree.ElementTree as ET # nosec B314
|
||||
import time
|
||||
from qgis.core import QgsMessageLog, Qgis
|
||||
|
||||
# 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."""
|
||||
@@ -46,101 +74,170 @@ def parse_codelist_file(filename, target_dict=None):
|
||||
# Assign the extracted code to the corresponding label within the category
|
||||
target_dict[cat][label] = clean
|
||||
except Exception as e:
|
||||
print(f"AMČR Codelist Read Error for {filename}: {e}")
|
||||
QgsMessageLog.logMessage(f"AMČR Codelist Read Error for {filename}: {e}", "AMČR", Qgis.Critical)
|
||||
|
||||
return target_dict
|
||||
|
||||
def load_all_data():
|
||||
"""Loads all static and dynamic codelists during plugin startup."""
|
||||
"""Loads the codelist during plugin startup."""
|
||||
ensure_codelists_dir()
|
||||
|
||||
# Initialize the base structure with empty dictionaries for all expected categories
|
||||
categorized_data = {
|
||||
'obdobi': {}, 'typ_akce': {}, 'areal': {},
|
||||
'kraj': {}, 'organizace': {}, 'okres': {}, 'katastr': {},
|
||||
'vedouci': {}, 'pian_presnost': {}, 'typ_lokality': {}, 'druh_lokality': {},
|
||||
'jistota': {}, 'lokalita_zachovalost': {}
|
||||
}
|
||||
|
||||
# Parse the default static codelist and the dynamically generated leaders codelist
|
||||
categorized_data = {k: {} for k in slovnicek.keys()}
|
||||
parse_codelist_file('heslar.csv', categorized_data)
|
||||
parse_codelist_file('vedouci.csv', categorized_data)
|
||||
|
||||
return categorized_data
|
||||
|
||||
def download_vedouci():
|
||||
"""Fetches the list of leaders from the AMČR API and saves it to a CSV file."""
|
||||
def fetch_set(internal_name, api_set, task=None):
|
||||
dataset = []
|
||||
params = {
|
||||
"verb": "ListRecords",
|
||||
"metadataPrefix": "oai_dc",
|
||||
"set": api_set
|
||||
}
|
||||
|
||||
while True:
|
||||
# Kontrola zrušení v každém kroku
|
||||
if task and task.isCanceled():
|
||||
return None
|
||||
|
||||
try:
|
||||
response = requests.get(BASE_URL, params=params, timeout=30)
|
||||
response.raise_for_status()
|
||||
root = ET.fromstring(response.content)
|
||||
|
||||
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']
|
||||
|
||||
if internal_name in specialni_pripady:
|
||||
kod = nazev
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
# API endpoint for fetching facet data for leaders
|
||||
url = "https://digiarchiv.aiscr.cz/api/search/query?entity=akce&sort=datestamp%20desc&page=0&onlyFacets=True&rows=0"
|
||||
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
|
||||
|
||||
try:
|
||||
# Execute the GET request with a 20-second timeout
|
||||
r = requests.get(url, timeout=20)
|
||||
r.raise_for_status()
|
||||
data = r.json()
|
||||
|
||||
# Extract the leaders list from the JSON response using safe dict getters
|
||||
vedouci_list = data.get('facet_counts', {}).get('f_vedouci', [])
|
||||
if not vedouci_list:
|
||||
vedouci_list = data.get('facet_counts', {}).get('facet_fields', {}).get('f_vedouci', [])
|
||||
|
||||
csv_path = os.path.join(CODELISTS_DIR, 'vedouci.csv')
|
||||
|
||||
count = 0
|
||||
|
||||
# Open the target CSV file for writing without extra blank lines
|
||||
with open(csv_path, 'w', encoding='utf-8', newline='') as f:
|
||||
writer = csv.writer(f, delimiter=';')
|
||||
|
||||
# Write the standard header required by the parser function
|
||||
writer.writerow(['Název', 'Kód', 'Kategorie'])
|
||||
|
||||
# Iterate through the API results and format them for the CSV
|
||||
for item in vedouci_list:
|
||||
name = None
|
||||
if isinstance(item, dict):
|
||||
name = item.get('name')
|
||||
elif isinstance(item, str):
|
||||
name = item
|
||||
|
||||
# Ignore pure numbers (which are usually counts) and write valid names
|
||||
if name and not str(name).isdigit():
|
||||
writer.writerow([name, name, 'vedouci'])
|
||||
count += 1
|
||||
|
||||
return True, f"Staženo {count} jmen."
|
||||
|
||||
except Exception as e:
|
||||
return False, str(e)
|
||||
|
||||
# Initialize global codelist data when the module is imported
|
||||
_DATA = load_all_data()
|
||||
|
||||
# Safely extract individual categories into global variables for easy access across the plugin
|
||||
OBDOBI = _DATA.get('obdobi', {})
|
||||
TYP_AKCE = _DATA.get('typ_akce', {})
|
||||
AREAL = _DATA.get('areal', {})
|
||||
KRAJE = _DATA.get('kraj', {})
|
||||
ORGANIZACE = _DATA.get('organizace', {})
|
||||
OKRESY = _DATA.get('okres', {})
|
||||
KATASTRY = _DATA.get('katastr', {})
|
||||
VEDOUCI = _DATA.get('vedouci', {})
|
||||
PIAN_PRESNOST = _DATA.get('pian_presnost', {})
|
||||
TYP_LOKALITY = _DATA.get('typ_lokality', {})
|
||||
DRUH_LOKALITY = _DATA.get('druh_lokality', {})
|
||||
JISTOTA = _DATA.get('jistota', {})
|
||||
LOKALITA_ZACHOVALOST = _DATA.get('lokalita_zachovalost', {})
|
||||
|
||||
def refresh_vedouci_cache():
|
||||
"""Reloads only the 'vedouci.csv' file to quickly update the cache without full initialization."""
|
||||
# Parse only the targeted file containing the updated leaders
|
||||
temp_data = parse_codelist_file('vedouci.csv')
|
||||
new_vedouci = temp_data.get('vedouci', {})
|
||||
data = load_all_data()
|
||||
|
||||
# Clear the existing global dictionary and update it with the fresh 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()
|
||||
+308
-35
@@ -1,14 +1,43 @@
|
||||
# -*- 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):
|
||||
"""
|
||||
@@ -99,13 +128,12 @@ class AmcrFilterDialog(QDialog):
|
||||
|
||||
# Determines if we are fetching 'akce' (projects) or 'lokalita' (locations)
|
||||
self.typ_dat = typ_dat
|
||||
|
||||
|
||||
|
||||
|
||||
# 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': []
|
||||
}
|
||||
|
||||
@@ -137,20 +165,17 @@ class AmcrFilterDialog(QDialog):
|
||||
|
||||
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)
|
||||
|
||||
# Button to fetch fresh project leaders from the API
|
||||
self.btn_update_vedouci = QPushButton("🔄")
|
||||
self.btn_update_vedouci.setToolTip("Aktualizovat seznam vedoucích z API")
|
||||
self.btn_update_vedouci.setFixedWidth(30)
|
||||
self.btn_update_vedouci.clicked.connect(self.action_update_vedouci)
|
||||
|
||||
self.picker_vedouci = self.setup_picker("Vedoucí výzkumu", 'vedouci', VEDOUCI, extra_btn=self.btn_update_vedouci)
|
||||
self.picker_vedouci = self.setup_picker("Vedoucí výzkumu", 'vedouci', VEDOUCI)
|
||||
layout.addWidget(self.picker_vedouci)
|
||||
|
||||
# Type of event
|
||||
@@ -188,10 +213,18 @@ class AmcrFilterDialog(QDialog):
|
||||
# Pushes everything above to the top
|
||||
layout.addStretch(1)
|
||||
|
||||
# Main dialog OK/Cancel buttons
|
||||
buttons = QDialogButtonBox(
|
||||
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.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)
|
||||
@@ -219,7 +252,7 @@ class AmcrFilterDialog(QDialog):
|
||||
# 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: # PyQt6: DialogCode
|
||||
if dlg.exec() == QDialog.DialogCode.Accepted:
|
||||
codes, labels = dlg.get_selected_codes()
|
||||
# Update local cache with selected IDs
|
||||
self.selection_cache[cache_key] = codes
|
||||
@@ -246,21 +279,28 @@ class AmcrFilterDialog(QDialog):
|
||||
row_widget.setLayout(row_layout)
|
||||
return row_widget
|
||||
|
||||
def action_update_vedouci(self):
|
||||
# Change cursor to loading state to indicate background task
|
||||
QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor)
|
||||
try:
|
||||
success, msg = download_vedouci()
|
||||
if success:
|
||||
count = refresh_vedouci_cache()
|
||||
QMessageBox.information(self, "Úspěch", f"{msg}\nNyní je v paměti {count} osob.")
|
||||
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:
|
||||
QMessageBox.warning(self, "Chyba", f"Nepodařilo se stáhnout data:\n{msg}")
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "Chyba", str(e))
|
||||
finally:
|
||||
# Safely restore the normal cursor even if an error occurs
|
||||
QApplication.restoreOverrideCursor()
|
||||
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"
|
||||
@@ -284,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():
|
||||
@@ -305,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
|
||||
+196
-102
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from qgis.core import (QgsProject, QgsVectorLayer, QgsFeature, QgsGeometry,
|
||||
QgsField, QgsCoordinateReferenceSystem, QgsCoordinateTransform,
|
||||
QgsWkbTypes, QgsPolymorphicRelation, QgsEditorWidgetSetup, Qgis)
|
||||
QgsWkbTypes, Qgis, QgsApplication, QgsAuthMethodConfig, QgsMessageLog)
|
||||
from qgis.utils import iface
|
||||
from qgis.PyQt.QtCore import Qt, QMetaType
|
||||
from qgis.PyQt.QtWidgets import QApplication
|
||||
@@ -12,6 +12,116 @@ import json
|
||||
# Global cache to store translated terms from the Digital Archive
|
||||
TRANSLATIONS = {}
|
||||
|
||||
# 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
|
||||
@@ -86,7 +196,7 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
|
||||
current_page = 0
|
||||
BATCH_DOCS = 500 # Records per API request
|
||||
MAX_LIMIT = 20000 # Safety limit to prevent QGIS from freezing
|
||||
feats_k = [] # List for component features (non-spatial)
|
||||
|
||||
|
||||
seen_ids = set()
|
||||
target_pian_ids_count = 0
|
||||
@@ -103,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', [])
|
||||
@@ -146,7 +256,6 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
|
||||
|
||||
# pian_lookup maps a Geometry ID (PIAN) to a list of its associated metadata
|
||||
pian_lookup = {}
|
||||
komponenty_lookup = {}
|
||||
target_pian_ids = set()
|
||||
actions_with_geom = 0
|
||||
|
||||
@@ -247,23 +356,35 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
|
||||
dj_pian_value = dj_pian.get('id')
|
||||
if dj_pian_value:
|
||||
target_pian_ids.add(dj_pian_value)
|
||||
target_pian_ids_count += 1
|
||||
if dj_pian_value not in pian_lookup:
|
||||
pian_lookup[dj_pian_value] = []
|
||||
pian_lookup[dj_pian_value].append(dj_meta)
|
||||
|
||||
# Parse non-spatial components if requested (for relational tables)
|
||||
if komponenty == "true":
|
||||
# One feature per component — all data on a single row, no relations needed
|
||||
komps = dj.get('dj_komponenta', [])
|
||||
for komp in komps:
|
||||
komp_temp = [
|
||||
komp.get('ident_cely', ""),
|
||||
komp.get('komponenta_areal', {}).get('value', ""),
|
||||
komp.get('komponenta_obdobi', {}).get('value', "")
|
||||
]
|
||||
if dj_id not in komponenty_lookup:
|
||||
komponenty_lookup[dj_id] = []
|
||||
komponenty_lookup[dj_id].append(komp_temp)
|
||||
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:
|
||||
@@ -297,7 +418,7 @@ 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:
|
||||
@@ -317,32 +438,32 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
|
||||
|
||||
# Define attribute table structure
|
||||
cols = [
|
||||
QgsField("PIAN", QMetaType.Type.QString),
|
||||
QgsField("Přesnost", QMetaType.Type.QString),
|
||||
QgsField("PIAN – typ", QMetaType.Type.QString),
|
||||
QgsField("Dokumentační jednotka", QMetaType.Type.QString),
|
||||
QgsField("Typ dokumentační jednotky", QMetaType.Type.QString),
|
||||
QgsField("Definiční bod(y) (WGS-84)", QMetaType.Type.QString),
|
||||
QgsField(archeologicky_zaznam, QMetaType.Type.QString),
|
||||
QgsField("Odkaz do Digitálního archivu AMČR", QMetaType.Type.QString),
|
||||
QgsField("Okres", QMetaType.Type.QString),
|
||||
QgsField("Katastr", QMetaType.Type.QString),
|
||||
QgsField("Další katastry", QMetaType.Type.QString)
|
||||
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("Akce – lokalizace", QMetaType.Type.QString),
|
||||
QgsField("Vedoucí akce", QMetaType.Type.QString),
|
||||
QgsField("Organizace", QMetaType.Type.QString),
|
||||
QgsField("Specifikace data", QMetaType.Type.QString),
|
||||
QgsField("Datum zahájeni", QMetaType.Type.QString),
|
||||
QgsField("Datum ukončení", QMetaType.Type.QString),
|
||||
QgsField("Hlavní typ", QMetaType.Type.QString),
|
||||
QgsField("Vedlejší typ", QMetaType.Type.QString),
|
||||
QgsField("Zjištění", QMetaType.Type.QString),
|
||||
QgsField("Akce – nahrazuje NZ", QMetaType.Type.QString),
|
||||
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 += [
|
||||
@@ -357,29 +478,43 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
|
||||
|
||||
# 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"
|
||||
"zachovalost": "Zachovalost",
|
||||
"komponenta": "Komponenta",
|
||||
"komponenta_areal": "Areál",
|
||||
"komponenta_obdobi": "Období",
|
||||
}
|
||||
|
||||
# Create a non-spatial table for components if requested
|
||||
if komponenty == "true":
|
||||
vl_komponenty = QgsVectorLayer("None", "AMCR Komponenty", "memory")
|
||||
pr = vl_komponenty.dataProvider()
|
||||
komponenty_cols = [
|
||||
cols += [
|
||||
QgsField("komponenta", QMetaType.Type.QString),
|
||||
QgsField("dj_id", QMetaType.Type.QString),
|
||||
QgsField("komponenta_areal", QMetaType.Type.QString),
|
||||
QgsField("komponenta_obdobi", QMetaType.Type.QString),
|
||||
QgsField("vrstva", QMetaType.Type.QString)
|
||||
]
|
||||
pr.addAttributes(komponenty_cols)
|
||||
vl_komponenty.updateFields()
|
||||
|
||||
idx_vrstva = vl_komponenty.fields().indexOf("vrstva")
|
||||
vl_komponenty.setEditorWidgetSetup(idx_vrstva, QgsEditorWidgetSetup("Hidden", {}))
|
||||
|
||||
for vl in layers:
|
||||
vl.dataProvider().addAttributes(cols)
|
||||
@@ -464,13 +599,16 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
|
||||
meta['lokalita_typ'], meta['lokalita_druh'],
|
||||
meta['lokalita_zachovalost']
|
||||
])
|
||||
|
||||
if komponenty == "true" and meta['dj_id'] in komponenty_lookup:
|
||||
for k in komponenty_lookup[meta['dj_id']]:
|
||||
if len(k) == 3:
|
||||
k.append(referenced_layer.id())
|
||||
|
||||
|
||||
atributy.append(meta['pristupnost'])
|
||||
|
||||
if komponenty == "true":
|
||||
atributy.extend([
|
||||
meta.get('komponenta_id', ""),
|
||||
meta.get('komponenta_areal', ""),
|
||||
meta.get('komponenta_obdobi', ""),
|
||||
])
|
||||
|
||||
feat.setAttributes(atributy)
|
||||
target_list.append(feat)
|
||||
|
||||
@@ -478,18 +616,7 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
|
||||
print(f"Chyba při tvorbě feature: {ex}")
|
||||
pass
|
||||
|
||||
if komponenty == "true":
|
||||
for k in komponenty_lookup:
|
||||
for komp in komponenty_lookup[k]:
|
||||
if len(komp) == 4:
|
||||
feat = QgsFeature()
|
||||
atributy = [
|
||||
komp[0], k, komp[1], komp[2], komp[3]
|
||||
]
|
||||
feat.setAttributes(atributy)
|
||||
feats_k.append(feat)
|
||||
|
||||
# --- ADDING TO QGIS INTERFACE ---
|
||||
# --- ADDING TO QGIS INTERFACE ---
|
||||
proj = QgsProject.instance()
|
||||
added = 0
|
||||
layers_to_process = [
|
||||
@@ -498,49 +625,16 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
|
||||
(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"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=Qgis.MessageLevel.Success)
|
||||
|
||||
# --- RELATIONSHIP MANAGEMENT ---
|
||||
# Set up automatic links between spatial layers and the component table
|
||||
if komponenty == "true":
|
||||
parent_layers_ids = []
|
||||
if feats_p:
|
||||
parent_layers_ids.append(vl_poly.id())
|
||||
if feats_l:
|
||||
parent_layers_ids.append(vl_line.id())
|
||||
if feats_pt:
|
||||
parent_layers_ids.append(vl_point.id())
|
||||
|
||||
rel_manager = proj.relationManager()
|
||||
|
||||
rel = QgsPolymorphicRelation()
|
||||
# rel.setId(f"rel_komponenty_{archeologicky_zaznam}")
|
||||
rel.setName("Komponenty")
|
||||
rel.setReferencingLayer(vl_komponenty.id())
|
||||
rel.setReferencedLayerExpression("@layer_id")
|
||||
rel.setReferencedLayerField("vrstva")
|
||||
rel.setReferencedLayerIds(parent_layers_ids)
|
||||
rel.addFieldPair("dj_id", "Dokumentační jednotka")
|
||||
rel.generateId()
|
||||
|
||||
if rel.isValid():
|
||||
rel_manager.addPolymorphicRelation(rel)
|
||||
else:
|
||||
print("Relace Komponenty není validní!")
|
||||
|
||||
else:
|
||||
iface.messageBar().pushMessage("AMCR", "Žádná data k zobrazení.", level=Qgis.MessageLevel.Info)
|
||||
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
from qgis.PyQt.QtCore import QSettings, QTranslator, QCoreApplication
|
||||
from qgis.PyQt.QtGui import QIcon
|
||||
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
|
||||
|
||||
@@ -109,6 +111,16 @@ class AmcrViewer:
|
||||
)
|
||||
self.plugin_menu.addAction(self.action_download_lokality)
|
||||
|
||||
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())
|
||||
@@ -168,4 +180,19 @@ class AmcrViewer:
|
||||
|
||||
# Access the map canvas and start the fetch/render process from amcr_tools
|
||||
canvas = self.iface.mapCanvas()
|
||||
load_amcr_data(canvas, bbox, filters, typ_dat, komponenty)
|
||||
load_amcr_data(canvas, bbox, filters, typ_dat, komponenty)
|
||||
|
||||
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
@@ -8,11 +8,11 @@ name=AMČR Viewer
|
||||
qgisMinimumVersion=3.4.0
|
||||
qgisMaximumVersion=4.99.0
|
||||
description=Viewing and downloading the AMČR data.
|
||||
version=1.3.2
|
||||
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
|
||||
@@ -31,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
|
||||
@@ -41,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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user