mirror of
https://github.com/ARUP-CAS/aiscr-qgis-amcr-viewer.git
synced 2026-06-19 04:12:55 +02:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 56389e27d7 | |||
| c8d42e2459 | |||
| a6ebbce4cf | |||
| 88149fbb30 | |||
| c0d054d22a | |||
| ba41039468 | |||
| 499b3b3f0a | |||
| 54f154b264 | |||
| c679e776df | |||
| a5604dfaa8 | |||
| 11f44d025b | |||
| 7f3b2b46fb | |||
| be53edefa5 | |||
| 3be7832b40 |
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
[](https://www.gnu.org/licenses/gpl-3.0)
|
[](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
|
**Module Type:** Data Acquisition & Visualization
|
||||||
|
|
||||||
@@ -22,8 +22,6 @@
|
|||||||
* **Dynamic Geometry Retrieval:** Automatically downloads and categorizes spatial data into Point, Line, and Polygon layers.
|
* **Dynamic Geometry Retrieval:** Automatically downloads and categorizes spatial data into Point, Line, and Polygon layers.
|
||||||
* **Semantic Interoperability:** Automatically translates internal system codes into human-readable labels using the AIS CR API.
|
* **Semantic Interoperability:** Automatically translates internal system codes into human-readable labels using the AIS CR API.
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Installation Guide
|
## 2. Installation Guide
|
||||||
|
|
||||||
**Install the plugin from QGIS plugin repository.**
|
**Install the plugin from QGIS plugin repository.**
|
||||||
@@ -37,8 +35,6 @@
|
|||||||
*5. Locate the source ZIP file and click Install Plugin.*
|
*5. Locate the source ZIP file and click Install Plugin.*
|
||||||
*6. Upon successful installation, the AMCR download button (load AMCR data) will appear in the interface.*
|
*6. Upon successful installation, the AMCR download button (load AMCR data) will appear in the interface.*
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. User Manual
|
## 3. User Manual
|
||||||
|
|
||||||
### 3.1 Data Retrieval
|
### 3.1 Data Retrieval
|
||||||
@@ -64,8 +60,6 @@ To initiate a search query, click either the **Stáhnout data akcí** or the **S
|
|||||||
|
|
||||||
For a more in-depth tutorial refer to the [AMČR Documentation](https://amcr-help.aiscr.cz/digiarchiv/qgis-viewer.html) (only in Czech).
|
For a more in-depth tutorial refer to the [AMČR Documentation](https://amcr-help.aiscr.cz/digiarchiv/qgis-viewer.html) (only in Czech).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### 3.2 Layer Structure & Attributes
|
### 3.2 Layer Structure & Attributes
|
||||||
|
|
||||||
Upon successful retrieval, the plugin generates four temporary memory layers:
|
Upon successful retrieval, the plugin generates four temporary memory layers:
|
||||||
@@ -77,11 +71,63 @@ Upon successful retrieval, the plugin generates four temporary memory layers:
|
|||||||
|
|
||||||
The Attribute Table includes standardized fields with important metadata. The components layer has no geometry on its own and depend solely on a relation with the other three layers.
|
The Attribute Table includes standardized fields with important metadata. The components layer has no geometry on its own and depend solely on a relation with the other three layers.
|
||||||
|
|
||||||
---
|
#### 3.2.1 Fields of the layers with geometry
|
||||||
|
|
||||||
|
| Field | Description |
|
||||||
|
| --- | --- |
|
||||||
|
| PIAN | PIAN (spatial identifier) ID |
|
||||||
|
| Přesnost | spatial deviation [in units/tens/hundreds of meters/defined by cadastre] |
|
||||||
|
| PIAN – typ | [point/line/polygon] |
|
||||||
|
| Dokumentační jednotka | Documentation unit ID |
|
||||||
|
| Typ dokumentační jednotky | [trench/event part/whole event/cadastral territory] |
|
||||||
|
| Definiční bod(y) (WGS-84) | feature centroid in WGS-84 coordinate system |
|
||||||
|
| Akce/Lokalita | Event/Site ID |
|
||||||
|
| Odkaz do Digitálního archivu AMČR | link to the Event/Site record in the Digital Archive |
|
||||||
|
| Okres | district |
|
||||||
|
| Katastr | main cadastre |
|
||||||
|
| Další katastr | other cadastres, if the event extends beyond the main cadastre |
|
||||||
|
| Přístupnost | record accessibility [A/B/C/D] |
|
||||||
|
|
||||||
|
> Common fields
|
||||||
|
|
||||||
|
| Field | Description |
|
||||||
|
| --- | --- |
|
||||||
|
| Vedoucí akce | main fieldwork manager |
|
||||||
|
| Organizace | organization conducting the research |
|
||||||
|
| Specifikace data | [exact date/exact years/sometime in years] |
|
||||||
|
| Datum zahájení | Event start date |
|
||||||
|
| Datum ukončení | Event end date |
|
||||||
|
| Hlavní typ | primary research method [total excavation/pit trench/surface collection survey/…] |
|
||||||
|
| Vedlejší typ | secondary research method [same options as in Hlavní typ] |
|
||||||
|
| Zjištění | did the research reveal archaeological contexts? [positive/negative] |
|
||||||
|
| Akce – lokalizace | verbal description of the event location |
|
||||||
|
| Akce – nahrazuje NZ | replaces a fieldwork report? [yes/no] |
|
||||||
|
|
||||||
|
> Fields related to *Fieldwork events*
|
||||||
|
|
||||||
|
| Field | Description |
|
||||||
|
| --- | --- |
|
||||||
|
| nazev_lokality | site name |
|
||||||
|
| popis_lokality | site description |
|
||||||
|
| typ_lokality | site classification by definition method [survey polygon/heritage site/landscape] |
|
||||||
|
| druh_lokality | site classification by the nature of identified field relics [aerial survey polygon/landscape/remains of settlement/…] |
|
||||||
|
| zachovalost | site preservation state [buried site/ruin/aboveground remains/…] |
|
||||||
|
|
||||||
|
> Fields related to *Sites*
|
||||||
|
|
||||||
|
#### 3.2.2 Fields of the *Components* layer
|
||||||
|
|
||||||
|
| Field | Description |
|
||||||
|
| --- | --- |
|
||||||
|
| komponenta | Component ID |
|
||||||
|
| dj_id | parent Documentation unit ID |
|
||||||
|
| komponenta_areal | Activity area [settlement/burial area/field/…] |
|
||||||
|
| komponenta_obdobi | Period [Neolithic/High Middle 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
|
## 4. Technical Architecture
|
||||||
|
|
||||||
The plugin is developed in **Python 3** using the **PyQt5** framework for the GUI and the **Requests** library for HTTP communication.
|
The plugin is developed in **Python 3** using the **PyQt6** framework for the GUI and the **Requests** library for HTTP communication.
|
||||||
|
|
||||||
### 4.1 File Structure
|
### 4.1 File Structure
|
||||||
|
|
||||||
@@ -100,12 +146,10 @@ The plugin interacts with three primary endpoints of the AIS CR infrastructure:
|
|||||||
* Parameters: `entity=akce`, `rows/page` (pagination).
|
* Parameters: `entity=akce`, `rows/page` (pagination).
|
||||||
* Logic: The plugin implements a `while True` loop to handle pagination, processing data in batches of 500 records to ensure stability.
|
* Logic: The plugin implements a `while True` loop to handle pagination, processing data in batches of 500 records to ensure stability.
|
||||||
|
|
||||||
|
|
||||||
2. **Translation API:**
|
2. **Translation API:**
|
||||||
* Endpoint: `https://digiarchiv.aiscr.cz/api/assets/i18n/cs.json`
|
* Endpoint: `https://digiarchiv.aiscr.cz/api/assets/i18n/cs.json`
|
||||||
* Function: Retrieves the mapping between system codes (e.g., `HES-xxxx`) and Czech labels. This dictionary is cached in memory during the session.
|
* Function: Retrieves the mapping between system codes (e.g., `HES-xxxx`) and Czech labels. This dictionary is cached in memory during the session.
|
||||||
|
|
||||||
|
|
||||||
### 4.3 Data Persistence
|
### 4.3 Data Persistence
|
||||||
|
|
||||||
* **Vocabularies:** Static vocabularies (e.g., Periods, Regions) are stored in `codelists/heslar.csv`.
|
* **Vocabularies:** Static vocabularies (e.g., Periods, Regions) are stored in `codelists/heslar.csv`.
|
||||||
@@ -117,7 +161,10 @@ The plugin interacts with three primary endpoints of the AIS CR infrastructure:
|
|||||||
* **Record Limit:** A safety cap of 20,000 records is enforced.
|
* **Record Limit:** A safety cap of 20,000 records is enforced.
|
||||||
* **Batch Processing:** Geometry fetching is batched (50 IDs per request) to comply with URL length limitations and server load balancing.
|
* **Batch Processing:** Geometry fetching is batched (50 IDs per request) to comply with URL length limitations and server load balancing.
|
||||||
|
|
||||||
## 6. Links and resources
|
### 4.5 Relational Data Linking
|
||||||
|
The plugin automatically utilizes advanced QGIS features for data relationship management, specifically Polymorphic Relations. The *Components* layer is dynamically linked within the project to the spatial layers of events and sites via the documentation unit identifier (dj_id). This allows users to immediately see all *components* belonging to a given geometry (point, line, or polygon) directly in the attribute form or the identify features tool, without the need to manually filter or join the data.
|
||||||
|
|
||||||
|
## 5. Links and resources
|
||||||
|
|
||||||
* [AMCR/Digiarchive Documentation](https://amcr-help.aiscr.cz/) (only in Czech).
|
* [AMCR/Digiarchive Documentation](https://amcr-help.aiscr.cz/) (only in Czech).
|
||||||
* [AMCR Viewer tutorial](https://amcr-help.aiscr.cz/digiarchiv/qgis-viewer.html) (only in Czech).
|
* [AMCR Viewer tutorial](https://amcr-help.aiscr.cz/digiarchiv/qgis-viewer.html) (only in Czech).
|
||||||
|
|||||||
+182
-85
@@ -2,10 +2,38 @@
|
|||||||
import os
|
import os
|
||||||
import csv
|
import csv
|
||||||
import requests
|
import requests
|
||||||
|
import xml.etree.ElementTree as ET # nosec
|
||||||
|
import time
|
||||||
|
from qgis.core import QgsMessageLog, Qgis
|
||||||
|
|
||||||
# Define paths for the plugin and its codelists directory
|
# Define paths for the plugin and its codelists directory
|
||||||
PLUGIN_DIR = os.path.dirname(__file__)
|
PLUGIN_DIR = os.path.dirname(__file__)
|
||||||
CODELISTS_DIR = os.path.join(PLUGIN_DIR, 'codelists')
|
CODELISTS_DIR = os.path.join(PLUGIN_DIR, 'codelists')
|
||||||
|
BASE_URL = "https://api.aiscr.cz/2.2/oai"
|
||||||
|
OUTPUT_FILE = os.path.join(CODELISTS_DIR, 'heslar.csv')
|
||||||
|
|
||||||
|
slovnicek = {
|
||||||
|
'obdobi' : 'heslo:obdobi',
|
||||||
|
'typ_akce' : 'heslo:akce_typ',
|
||||||
|
'areal' : 'heslo:areal',
|
||||||
|
'kraj' : 'ruian_kraj',
|
||||||
|
'organizace' : 'organizace',
|
||||||
|
'okres' : 'ruian_okres',
|
||||||
|
'katastr' : 'ruian_katastr',
|
||||||
|
'vedouci' : 'osoba',
|
||||||
|
'pian_presnost' : 'heslo:pian_presnost',
|
||||||
|
'typ_lokality' : 'heslo:lokalita_typ',
|
||||||
|
'druh_lokality' : 'heslo:lokalita_druh',
|
||||||
|
'jistota' : 'heslo:jistota_urceni',
|
||||||
|
'lokalita_zachovalost' : 'heslo:stav_dochovani',
|
||||||
|
'pristupnost' : 'heslo:pristupnost'
|
||||||
|
}
|
||||||
|
|
||||||
|
NS = {
|
||||||
|
'oai': 'http://www.openarchives.org/OAI/2.0/',
|
||||||
|
'dc': 'http://purl.org/dc/elements/1.1/',
|
||||||
|
'oai_dc': 'http://www.openarchives.org/OAI/2.0/oai_dc/'
|
||||||
|
}
|
||||||
|
|
||||||
def ensure_codelists_dir():
|
def ensure_codelists_dir():
|
||||||
"""Creates the codelists directory if it does not exist."""
|
"""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
|
# Assign the extracted code to the corresponding label within the category
|
||||||
target_dict[cat][label] = clean
|
target_dict[cat][label] = clean
|
||||||
except Exception as e:
|
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
|
return target_dict
|
||||||
|
|
||||||
def load_all_data():
|
def load_all_data():
|
||||||
"""Loads all static and dynamic codelists during plugin startup."""
|
"""Loads the codelist during plugin startup."""
|
||||||
ensure_codelists_dir()
|
ensure_codelists_dir()
|
||||||
|
categorized_data = {k: {} for k in slovnicek.keys()}
|
||||||
# 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
|
|
||||||
parse_codelist_file('heslar.csv', categorized_data)
|
parse_codelist_file('heslar.csv', categorized_data)
|
||||||
parse_codelist_file('vedouci.csv', categorized_data)
|
|
||||||
|
|
||||||
return categorized_data
|
return categorized_data
|
||||||
|
|
||||||
def download_vedouci():
|
def fetch_set(internal_name, api_set, task=None):
|
||||||
"""Fetches the list of leaders from the AMČR API and saves it to a CSV file."""
|
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) # nosec
|
||||||
|
|
||||||
|
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()
|
ensure_codelists_dir()
|
||||||
|
all_data = []
|
||||||
|
total_sets = len(slovnicek)
|
||||||
|
|
||||||
# API endpoint for fetching facet data for leaders
|
for index, (interni, api_nazev) in enumerate(slovnicek.items()):
|
||||||
url = "https://digiarchiv.aiscr.cz/api/search/query?entity=akce&sort=datestamp%20desc&page=0&onlyFacets=True&rows=0"
|
# 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:
|
data = load_all_data()
|
||||||
# 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', {})
|
|
||||||
|
|
||||||
# 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.clear()
|
||||||
VEDOUCI.update(new_vedouci)
|
VEDOUCI.update(data.get('vedouci', {}))
|
||||||
|
PIAN_PRESNOST.clear()
|
||||||
return len(VEDOUCI)
|
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 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from qgis.PyQt.QtWidgets import (QDialog, QVBoxLayout, QFormLayout,
|
from qgis.PyQt.QtWidgets import (QDialog, QVBoxLayout,
|
||||||
QLineEdit, QDialogButtonBox,
|
QLineEdit, QDialogButtonBox,
|
||||||
QCheckBox, QGroupBox, QPushButton,
|
QCheckBox, QGroupBox, QPushButton,
|
||||||
QListWidget, QListWidgetItem, QHBoxLayout,
|
QListWidget, QListWidgetItem, QHBoxLayout,
|
||||||
QLabel, QMessageBox, QApplication, QWidget)
|
QMessageBox, QLabel, QFormLayout)
|
||||||
from qgis.PyQt.QtCore import Qt
|
from qgis.PyQt.QtCore import Qt, QSettings
|
||||||
|
from qgis.core import QgsTask, QgsApplication, QgsMessageLog, Qgis, QgsAuthMethodConfig
|
||||||
from .amcr_codelists import (OBDOBI, TYP_AKCE, KRAJE, AREAL, ORGANIZACE,
|
from .amcr_codelists import (OBDOBI, TYP_AKCE, KRAJE, AREAL, ORGANIZACE,
|
||||||
OKRESY, KATASTRY, VEDOUCI, PIAN_PRESNOST, TYP_LOKALITY,
|
OKRESY, KATASTRY, VEDOUCI, PIAN_PRESNOST, TYP_LOKALITY,
|
||||||
DRUH_LOKALITY, JISTOTA, LOKALITA_ZACHOVALOST,
|
DRUH_LOKALITY, JISTOTA, LOKALITA_ZACHOVALOST, PRISTUPNOST,
|
||||||
download_vedouci, refresh_vedouci_cache)
|
download_heslare, refresh_globals)
|
||||||
|
|
||||||
|
class UpdateCodelistsTask(QgsTask):
|
||||||
|
def __init__(self, description):
|
||||||
|
super().__init__(description, QgsTask.CanCancel)
|
||||||
|
self.success = False
|
||||||
|
self.exception = None
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""Tato část běží ve vedlejším vlákně."""
|
||||||
|
try:
|
||||||
|
# Voláme upravenou funkci
|
||||||
|
self.success = download_heslare(task=self)
|
||||||
|
return self.success
|
||||||
|
except Exception as e:
|
||||||
|
self.exception = e
|
||||||
|
return False
|
||||||
|
|
||||||
|
def finished(self, result):
|
||||||
|
"""Tato část běží v hlavním vlákně po skončení run()."""
|
||||||
|
if result:
|
||||||
|
# Teď bezpečně aktualizujeme globální proměnné v hlavním vlákně
|
||||||
|
refresh_globals()
|
||||||
|
QgsMessageLog.logMessage("Hesláře AMČR byly úspěšně aktualizovány.", "AMČR", Qgis.Info)
|
||||||
|
else:
|
||||||
|
if self.isCanceled():
|
||||||
|
QgsMessageLog.logMessage("Aktualizace heslářů byla zrušena.", "AMČR", Qgis.Warning)
|
||||||
|
else:
|
||||||
|
QgsMessageLog.logMessage(f"Chyba aktualizace: {self.exception}", "AMČR", Qgis.Critical)
|
||||||
|
|
||||||
class FilterableSelectionDialog(QDialog):
|
class FilterableSelectionDialog(QDialog):
|
||||||
"""
|
"""
|
||||||
@@ -99,13 +128,12 @@ class AmcrFilterDialog(QDialog):
|
|||||||
|
|
||||||
# Determines if we are fetching 'akce' (projects) or 'lokalita' (locations)
|
# Determines if we are fetching 'akce' (projects) or 'lokalita' (locations)
|
||||||
self.typ_dat = typ_dat
|
self.typ_dat = typ_dat
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Cache dictionary to store selected codes for each category
|
# Cache dictionary to store selected codes for each category
|
||||||
self.selection_cache = {
|
self.selection_cache = {
|
||||||
'organizace': [], 'kraj': [], 'obdobi': [], 'areal': [],
|
'organizace': [], 'kraj': [], 'obdobi': [], 'areal': [],
|
||||||
'typ_akce': [], 'okres': [], 'katastr': [], 'vedouci': [], 'pian_presnost': [],
|
'typ_akce': [], 'okres': [], 'katastr': [], 'vedouci': [], 'pian_presnost': [], 'pristupnost': [],
|
||||||
'typ_lokality': [], 'druh_lokality': [], 'jistota': [], 'lokalita_zachovalost': []
|
'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)
|
self.picker_presnost = self.setup_picker("PIAN – přesnost", 'pian_presnost', PIAN_PRESNOST)
|
||||||
layout.addWidget(self.picker_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
|
# Filters valid for Akce
|
||||||
|
|
||||||
if self.typ_dat == "akce":
|
if self.typ_dat == "akce":
|
||||||
self.picker_org = self.setup_picker("Organizace", 'organizace', ORGANIZACE)
|
self.picker_org = self.setup_picker("Organizace", 'organizace', ORGANIZACE)
|
||||||
layout.addWidget(self.picker_org)
|
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)
|
layout.addWidget(self.picker_vedouci)
|
||||||
|
|
||||||
# Type of event
|
# Type of event
|
||||||
@@ -188,10 +213,18 @@ class AmcrFilterDialog(QDialog):
|
|||||||
# Pushes everything above to the top
|
# Pushes everything above to the top
|
||||||
layout.addStretch(1)
|
layout.addStretch(1)
|
||||||
|
|
||||||
# Main dialog OK/Cancel buttons
|
# Main dialog OK/Cancel/Update buttons
|
||||||
buttons = QDialogButtonBox(
|
|
||||||
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
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.accepted.connect(self.accept)
|
||||||
buttons.rejected.connect(self.reject)
|
buttons.rejected.connect(self.reject)
|
||||||
layout.addWidget(buttons)
|
layout.addWidget(buttons)
|
||||||
@@ -219,7 +252,7 @@ class AmcrFilterDialog(QDialog):
|
|||||||
# Nested function that handles opening the dialog and saving results
|
# Nested function that handles opening the dialog and saving results
|
||||||
def open_dialog():
|
def open_dialog():
|
||||||
dlg = FilterableSelectionDialog(label_text, data_source, self.selection_cache[cache_key], self)
|
dlg = FilterableSelectionDialog(label_text, data_source, self.selection_cache[cache_key], self)
|
||||||
if dlg.exec() == QDialog.DialogCode.Accepted: # PyQt6: DialogCode
|
if dlg.exec() == QDialog.DialogCode.Accepted:
|
||||||
codes, labels = dlg.get_selected_codes()
|
codes, labels = dlg.get_selected_codes()
|
||||||
# Update local cache with selected IDs
|
# Update local cache with selected IDs
|
||||||
self.selection_cache[cache_key] = codes
|
self.selection_cache[cache_key] = codes
|
||||||
@@ -246,21 +279,28 @@ class AmcrFilterDialog(QDialog):
|
|||||||
row_widget.setLayout(row_layout)
|
row_widget.setLayout(row_layout)
|
||||||
return row_widget
|
return row_widget
|
||||||
|
|
||||||
def action_update_vedouci(self):
|
def action_update_heslare(self):
|
||||||
# Change cursor to loading state to indicate background task
|
# Vytvoření instance tasku
|
||||||
QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor)
|
task = UpdateCodelistsTask("Aktualizace heslářů AMČR")
|
||||||
try:
|
|
||||||
success, msg = download_vedouci()
|
# Povolíme tlačítko zpět bez ohledu na výsledek
|
||||||
if success:
|
task.taskCompleted.connect(lambda: self.btn_update.setEnabled(True))
|
||||||
count = refresh_vedouci_cache()
|
task.taskTerminated.connect(lambda: self.btn_update.setEnabled(True))
|
||||||
QMessageBox.information(self, "Úspěch", f"{msg}\nNyní je v paměti {count} osob.")
|
|
||||||
|
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:
|
else:
|
||||||
QMessageBox.warning(self, "Chyba", f"Nepodařilo se stáhnout data:\n{msg}")
|
msg = "Aktualizace byla zrušena uživatelem."
|
||||||
except Exception as e:
|
QMessageBox.warning(self, "Chyba / Zrušeno", msg)
|
||||||
QMessageBox.critical(self, "Chyba", str(e))
|
|
||||||
finally:
|
task.taskTerminated.connect(on_error)
|
||||||
# Safely restore the normal cursor even if an error occurs
|
|
||||||
QApplication.restoreOverrideCursor()
|
QgsApplication.taskManager().addTask(task)
|
||||||
|
|
||||||
def get_bbox(self):
|
def get_bbox(self):
|
||||||
return "true" if self.chk_bbox.isChecked() else "false"
|
return "true" if self.chk_bbox.isChecked() else "false"
|
||||||
@@ -284,6 +324,8 @@ class AmcrFilterDialog(QDialog):
|
|||||||
filters['f_areal'] = self.selection_cache['areal']
|
filters['f_areal'] = self.selection_cache['areal']
|
||||||
if self.selection_cache['pian_presnost']:
|
if self.selection_cache['pian_presnost']:
|
||||||
filters['f_pian_presnost'] = self.selection_cache['pian_presnost']
|
filters['f_pian_presnost'] = self.selection_cache['pian_presnost']
|
||||||
|
if self.selection_cache['pristupnost']:
|
||||||
|
filters['pristupnost'] = self.selection_cache['pristupnost']
|
||||||
|
|
||||||
if self.typ_dat == "akce":
|
if self.typ_dat == "akce":
|
||||||
if self.chk_posevidence.isChecked():
|
if self.chk_posevidence.isChecked():
|
||||||
@@ -305,4 +347,235 @@ class AmcrFilterDialog(QDialog):
|
|||||||
if self.selection_cache['lokalita_zachovalost']:
|
if self.selection_cache['lokalita_zachovalost']:
|
||||||
filters['f_lokalita_zachovalost'] = self.selection_cache['lokalita_zachovalost']
|
filters['f_lokalita_zachovalost'] = self.selection_cache['lokalita_zachovalost']
|
||||||
|
|
||||||
return filters
|
return filters
|
||||||
|
|
||||||
|
class LoginDialog(QDialog):
|
||||||
|
"""
|
||||||
|
Dialog for saving AMČR login credentials securely in the QGIS Authentication Manager.
|
||||||
|
|
||||||
|
Credentials are encrypted by the platform's native secret storage
|
||||||
|
(DPAPI on Windows, Keychain on macOS, encrypted SQLite on Linux).
|
||||||
|
The auth config ID is persisted in QSettings so the session can be
|
||||||
|
restored automatically after a QGIS restart.
|
||||||
|
|
||||||
|
Note on QgsAuthManager quirks (QGIS 4 / Python bindings):
|
||||||
|
- hasConfigId() is unreliable – it checks an in-memory cache that may not
|
||||||
|
be populated yet. We never use it as a hard gate; we skip it and call
|
||||||
|
loadAuthenticationConfig() directly instead.
|
||||||
|
- storeAuthenticationConfig() and loadAuthenticationConfig() both have
|
||||||
|
SIP_INOUT on their config parameter, so Python bindings return a tuple
|
||||||
|
(bool, QgsAuthMethodConfig) rather than just bool. Always unpack both.
|
||||||
|
"""
|
||||||
|
|
||||||
|
SETTINGS_KEY = "amcr_viewer/auth_config_id"
|
||||||
|
CONFIG_NAME = "AMČR Viewer"
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.setWindowTitle("Přihlášení do AMČR")
|
||||||
|
self.setMinimumWidth(360)
|
||||||
|
|
||||||
|
layout = QVBoxLayout()
|
||||||
|
|
||||||
|
# Check whether a config ID is already stored from a previous session.
|
||||||
|
# We attempt a lightweight load (full=False) to confirm it is readable,
|
||||||
|
# since hasConfigId() may return False even for valid configs (cache lag).
|
||||||
|
existing_id = QSettings().value(self.SETTINGS_KEY, "")
|
||||||
|
self._has_saved = bool(existing_id) and bool(self._load_username_from_config(existing_id))
|
||||||
|
|
||||||
|
if self._has_saved:
|
||||||
|
info = QLabel("✔ Přihlašovací údaje jsou bezpečně uloženy ve správci autentizace QGIS.\n"
|
||||||
|
"Vyplňte pole níže pouze pokud je chcete změnit.")
|
||||||
|
info.setStyleSheet("color: green; font-style: italic;")
|
||||||
|
else:
|
||||||
|
info = QLabel("Zadejte přihlašovací údaje k Digitálnímu archivu AMČR.\n"
|
||||||
|
"Budou zašifrovaně uloženy ve správci autentizace QGIS.")
|
||||||
|
info.setWordWrap(True)
|
||||||
|
layout.addWidget(info)
|
||||||
|
layout.addSpacing(8)
|
||||||
|
|
||||||
|
form = QFormLayout()
|
||||||
|
|
||||||
|
self.txt_user = QLineEdit()
|
||||||
|
self.txt_user.setPlaceholderText("např. jan.novak@email.cz")
|
||||||
|
# Pre-fill the stored username (not sensitive) for convenience
|
||||||
|
if self._has_saved:
|
||||||
|
self.txt_user.setText(self._load_username_from_config(existing_id))
|
||||||
|
form.addRow("E-mail:", self.txt_user)
|
||||||
|
|
||||||
|
self.txt_pass = QLineEdit()
|
||||||
|
self.txt_pass.setEchoMode(QLineEdit.EchoMode.Password)
|
||||||
|
self.txt_pass.setPlaceholderText(
|
||||||
|
"ponechte prázdné pro zachování stávajícího hesla" if self._has_saved else "heslo"
|
||||||
|
)
|
||||||
|
form.addRow("Heslo:", self.txt_pass)
|
||||||
|
|
||||||
|
layout.addLayout(form)
|
||||||
|
layout.addSpacing(8)
|
||||||
|
|
||||||
|
if self._has_saved:
|
||||||
|
btn_forget = QPushButton("Odebrat uložené přihlašovací údaje")
|
||||||
|
btn_forget.setStyleSheet("color: #c0392b;")
|
||||||
|
btn_forget.clicked.connect(self._forget_credentials)
|
||||||
|
layout.addWidget(btn_forget)
|
||||||
|
|
||||||
|
layout.addStretch(1)
|
||||||
|
|
||||||
|
buttons = QDialogButtonBox(
|
||||||
|
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
||||||
|
)
|
||||||
|
buttons.accepted.connect(self._save_and_accept)
|
||||||
|
buttons.rejected.connect(self.reject)
|
||||||
|
layout.addWidget(buttons)
|
||||||
|
|
||||||
|
self.setLayout(layout)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Private helpers
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _load_config(config_id: str, full: bool = False):
|
||||||
|
"""
|
||||||
|
Attempt to load a QgsAuthMethodConfig by ID.
|
||||||
|
Returns (ok, cfg). Never raises; returns (False, empty cfg) on any error.
|
||||||
|
full=True decrypts and includes the password.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
auth_mgr = QgsApplication.authManager()
|
||||||
|
cfg = QgsAuthMethodConfig()
|
||||||
|
result = auth_mgr.loadAuthenticationConfig(config_id, cfg, full)
|
||||||
|
# Python bindings return (bool, cfg) due to SIP_INOUT parameter
|
||||||
|
if isinstance(result, tuple):
|
||||||
|
return result
|
||||||
|
return result, cfg
|
||||||
|
except Exception:
|
||||||
|
return False, QgsAuthMethodConfig()
|
||||||
|
|
||||||
|
def _load_username_from_config(self, config_id: str) -> str:
|
||||||
|
"""Load just the username from a stored config (no password decryption)."""
|
||||||
|
ok, cfg = self._load_config(config_id, full=False)
|
||||||
|
return cfg.config("username", "") if ok else ""
|
||||||
|
|
||||||
|
def _ensure_master_password(self) -> bool:
|
||||||
|
"""
|
||||||
|
Ensure the Auth Manager is unlocked before writing.
|
||||||
|
Prompts the user to set or enter the master password if needed.
|
||||||
|
Returns True if the manager is ready, False if the user cancelled.
|
||||||
|
"""
|
||||||
|
auth_mgr = QgsApplication.authManager()
|
||||||
|
|
||||||
|
if auth_mgr.isDisabled():
|
||||||
|
QMessageBox.critical(
|
||||||
|
self, "Správce autentizace nedostupný",
|
||||||
|
"Správce autentizace QGIS je zakázán nebo poškozený.\n"
|
||||||
|
"Zkuste obnovit databázi: Nastavení → Možnosti → Autentizace → Pomůcky."
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
# setMasterPassword(True) shows the QGIS master password dialog if needed
|
||||||
|
if not auth_mgr.setMasterPassword(True):
|
||||||
|
return False # User cancelled the master password dialog
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Button actions
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _save_and_accept(self):
|
||||||
|
username = self.txt_user.text().strip()
|
||||||
|
password = self.txt_pass.text()
|
||||||
|
|
||||||
|
if not username:
|
||||||
|
QMessageBox.warning(self, "Chybí údaje", "Vyplňte prosím e-mailovou adresu.")
|
||||||
|
return
|
||||||
|
|
||||||
|
existing_id = QSettings().value(self.SETTINGS_KEY, "")
|
||||||
|
auth_mgr = QgsApplication.authManager()
|
||||||
|
|
||||||
|
# If a config already exists and the password field is blank,
|
||||||
|
# update only the username and keep the existing encrypted password.
|
||||||
|
if not password and existing_id:
|
||||||
|
ok, cfg = self._load_config(existing_id, full=True)
|
||||||
|
if ok:
|
||||||
|
if not self._ensure_master_password():
|
||||||
|
return
|
||||||
|
cfg.setConfig("username", username)
|
||||||
|
auth_mgr.updateAuthenticationConfig(cfg)
|
||||||
|
self.accept()
|
||||||
|
return
|
||||||
|
|
||||||
|
if not password:
|
||||||
|
QMessageBox.warning(self, "Chybí údaje", "Vyplňte prosím heslo.")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not self._ensure_master_password():
|
||||||
|
return
|
||||||
|
|
||||||
|
cfg = QgsAuthMethodConfig()
|
||||||
|
cfg.setName(self.CONFIG_NAME)
|
||||||
|
cfg.setMethod("Basic")
|
||||||
|
cfg.setConfig("username", username)
|
||||||
|
cfg.setConfig("password", password) # nosec B106
|
||||||
|
|
||||||
|
settings = QSettings()
|
||||||
|
|
||||||
|
# Try to update an existing config first; fall back to creating a new one.
|
||||||
|
# We skip hasConfigId() as it may return False despite the config existing
|
||||||
|
# (in-memory cache may not be populated yet in QGIS 4).
|
||||||
|
ok_load, existing_cfg = self._load_config(existing_id, full=False) if existing_id else (False, None)
|
||||||
|
if ok_load:
|
||||||
|
cfg.setId(existing_id)
|
||||||
|
ok, cfg = auth_mgr.updateAuthenticationConfig(cfg)
|
||||||
|
else:
|
||||||
|
ok, cfg = auth_mgr.storeAuthenticationConfig(cfg)
|
||||||
|
|
||||||
|
config_id = cfg.id() if cfg else ""
|
||||||
|
|
||||||
|
if not ok or not config_id:
|
||||||
|
QMessageBox.critical(
|
||||||
|
self, "Chyba uložení",
|
||||||
|
"Přihlašovací údaje se nepodařilo uložit do správce autentizace QGIS.\n"
|
||||||
|
"Zkuste restartovat QGIS a přihlásit se znovu."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
settings.setValue(self.SETTINGS_KEY, config_id)
|
||||||
|
self.accept()
|
||||||
|
|
||||||
|
def _forget_credentials(self):
|
||||||
|
settings = QSettings()
|
||||||
|
existing_id = settings.value(self.SETTINGS_KEY, "")
|
||||||
|
if existing_id:
|
||||||
|
QgsApplication.authManager().removeAuthenticationConfig(existing_id)
|
||||||
|
settings.remove(self.SETTINGS_KEY)
|
||||||
|
QMessageBox.information(self, "Hotovo", "Uložené přihlašovací údaje byly odebrány.")
|
||||||
|
self.reject()
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Public static API – call this anywhere in the plugin to get credentials
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_credentials() -> tuple[str, str]:
|
||||||
|
"""
|
||||||
|
Retrieve (username, password) from the QGIS Authentication Manager.
|
||||||
|
Returns ('', '') if no credentials are stored or the manager is locked.
|
||||||
|
|
||||||
|
Note: hasConfigId() is intentionally skipped here – it checks an
|
||||||
|
in-memory cache that may lag behind the actual database contents,
|
||||||
|
causing false negatives (see class docstring). loadAuthenticationConfig()
|
||||||
|
is called directly and its return value is used as the authoritative result.
|
||||||
|
"""
|
||||||
|
settings = QSettings()
|
||||||
|
config_id = settings.value(LoginDialog.SETTINGS_KEY, "")
|
||||||
|
|
||||||
|
if not config_id:
|
||||||
|
return "", ""
|
||||||
|
|
||||||
|
ok, cfg = LoginDialog._load_config(config_id, full=True)
|
||||||
|
if not ok:
|
||||||
|
return "", ""
|
||||||
|
|
||||||
|
return cfg.config("username", ""), cfg.config("password", "") # nosec B106
|
||||||
+196
-94
@@ -1,7 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from qgis.core import (QgsProject, QgsVectorLayer, QgsFeature, QgsGeometry,
|
from qgis.core import (QgsProject, QgsVectorLayer, QgsFeature, QgsGeometry,
|
||||||
QgsField, QgsCoordinateReferenceSystem, QgsCoordinateTransform,
|
QgsField, QgsCoordinateReferenceSystem, QgsCoordinateTransform,
|
||||||
QgsWkbTypes, QgsPolymorphicRelation, QgsEditorWidgetSetup, Qgis)
|
QgsWkbTypes, Qgis, QgsApplication, QgsAuthMethodConfig, QgsMessageLog)
|
||||||
from qgis.utils import iface
|
from qgis.utils import iface
|
||||||
from qgis.PyQt.QtCore import Qt, QMetaType
|
from qgis.PyQt.QtCore import Qt, QMetaType
|
||||||
from qgis.PyQt.QtWidgets import QApplication
|
from qgis.PyQt.QtWidgets import QApplication
|
||||||
@@ -12,6 +12,116 @@ import json
|
|||||||
# Global cache to store translated terms from the Digital Archive
|
# Global cache to store translated terms from the Digital Archive
|
||||||
TRANSLATIONS = {}
|
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():
|
def load_translations():
|
||||||
"""Fetches the official Czech translation dictionary from the AISCR API."""
|
"""Fetches the official Czech translation dictionary from the AISCR API."""
|
||||||
global TRANSLATIONS
|
global TRANSLATIONS
|
||||||
@@ -86,7 +196,7 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
|
|||||||
current_page = 0
|
current_page = 0
|
||||||
BATCH_DOCS = 500 # Records per API request
|
BATCH_DOCS = 500 # Records per API request
|
||||||
MAX_LIMIT = 20000 # Safety limit to prevent QGIS from freezing
|
MAX_LIMIT = 20000 # Safety limit to prevent QGIS from freezing
|
||||||
feats_k = [] # List for component features (non-spatial)
|
|
||||||
|
|
||||||
seen_ids = set()
|
seen_ids = set()
|
||||||
target_pian_ids_count = 0
|
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']
|
del base_params['page']
|
||||||
|
|
||||||
try:
|
try:
|
||||||
resp_docs = requests.get(url, params=base_params, timeout=30)
|
resp_docs = _api_get(url, params=base_params, timeout=30)
|
||||||
resp_json = resp_docs.json()
|
resp_json = resp_docs.json()
|
||||||
data = resp_json.get('response', {})
|
data = resp_json.get('response', {})
|
||||||
batch_docs = data.get('docs', [])
|
batch_docs = data.get('docs', [])
|
||||||
@@ -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 maps a Geometry ID (PIAN) to a list of its associated metadata
|
||||||
pian_lookup = {}
|
pian_lookup = {}
|
||||||
komponenty_lookup = {}
|
|
||||||
target_pian_ids = set()
|
target_pian_ids = set()
|
||||||
actions_with_geom = 0
|
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')
|
dj_pian_value = dj_pian.get('id')
|
||||||
if dj_pian_value:
|
if dj_pian_value:
|
||||||
target_pian_ids.add(dj_pian_value)
|
target_pian_ids.add(dj_pian_value)
|
||||||
target_pian_ids_count += 1
|
|
||||||
if dj_pian_value not in pian_lookup:
|
if dj_pian_value not in pian_lookup:
|
||||||
pian_lookup[dj_pian_value] = []
|
pian_lookup[dj_pian_value] = []
|
||||||
pian_lookup[dj_pian_value].append(dj_meta)
|
|
||||||
|
|
||||||
# Parse non-spatial components if requested (for relational tables)
|
|
||||||
if komponenty == "true":
|
if komponenty == "true":
|
||||||
|
# One feature per component — all data on a single row, no relations needed
|
||||||
komps = dj.get('dj_komponenta', [])
|
komps = dj.get('dj_komponenta', [])
|
||||||
for komp in komps:
|
if komps:
|
||||||
komp_temp = [
|
for komp in komps:
|
||||||
komp.get('ident_cely', ""),
|
komp_meta = {
|
||||||
komp.get('komponenta_areal', {}).get('value', ""),
|
**dj_meta,
|
||||||
komp.get('komponenta_obdobi', {}).get('value', "")
|
'komponenta_id': komp.get('ident_cely', ""),
|
||||||
]
|
'komponenta_areal': komp.get('komponenta_areal', {}).get('value', ""),
|
||||||
if dj_id not in komponenty_lookup:
|
'komponenta_obdobi': komp.get('komponenta_obdobi', {}).get('value', ""),
|
||||||
komponenty_lookup[dj_id] = []
|
}
|
||||||
komponenty_lookup[dj_id].append(komp_temp)
|
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:
|
if not target_pian_ids:
|
||||||
@@ -297,7 +418,7 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
|
|||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
QApplication.processEvents()
|
QApplication.processEvents()
|
||||||
r_pian = requests.get(url, params=params_pian, timeout=15)
|
r_pian = _api_get(url, params=params_pian, timeout=15)
|
||||||
batch_docs = r_pian.json().get('response', {}).get('docs', [])
|
batch_docs = r_pian.json().get('response', {}).get('docs', [])
|
||||||
docs_pian.extend(batch_docs)
|
docs_pian.extend(batch_docs)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -317,32 +438,32 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
|
|||||||
|
|
||||||
# Define attribute table structure
|
# Define attribute table structure
|
||||||
cols = [
|
cols = [
|
||||||
QgsField("PIAN", QMetaType.Type.QString),
|
QgsField("pian", QMetaType.Type.QString),
|
||||||
QgsField("Přesnost", QMetaType.Type.QString),
|
QgsField("presnost", QMetaType.Type.QString),
|
||||||
QgsField("PIAN – typ", QMetaType.Type.QString),
|
QgsField("pian_typ", QMetaType.Type.QString),
|
||||||
QgsField("Dokumentační jednotka", QMetaType.Type.QString),
|
QgsField("dj", QMetaType.Type.QString),
|
||||||
QgsField("Typ dokumentační jednotky", QMetaType.Type.QString),
|
QgsField("typ_dj", QMetaType.Type.QString),
|
||||||
QgsField("Definiční bod(y) (WGS-84)", QMetaType.Type.QString),
|
QgsField("definicni_body", QMetaType.Type.QString),
|
||||||
QgsField(archeologicky_zaznam, QMetaType.Type.QString),
|
QgsField(typ_dat, QMetaType.Type.QString),
|
||||||
QgsField("Odkaz do Digitálního archivu AMČR", QMetaType.Type.QString),
|
QgsField("odkaz_do_digiarchivu", QMetaType.Type.QString),
|
||||||
QgsField("Okres", QMetaType.Type.QString),
|
QgsField("okres", QMetaType.Type.QString),
|
||||||
QgsField("Katastr", QMetaType.Type.QString),
|
QgsField("katastr", QMetaType.Type.QString),
|
||||||
QgsField("Další katastry", QMetaType.Type.QString)
|
QgsField("dalsi_katastry", QMetaType.Type.QString)
|
||||||
]
|
]
|
||||||
|
|
||||||
# Extend table based on data type
|
# Extend table based on data type
|
||||||
if typ_dat == "akce":
|
if typ_dat == "akce":
|
||||||
cols += [
|
cols += [
|
||||||
QgsField("Akce – lokalizace", QMetaType.Type.QString),
|
QgsField("akce_lokalizace", QMetaType.Type.QString),
|
||||||
QgsField("Vedoucí akce", QMetaType.Type.QString),
|
QgsField("vedouci", QMetaType.Type.QString),
|
||||||
QgsField("Organizace", QMetaType.Type.QString),
|
QgsField("organizace", QMetaType.Type.QString),
|
||||||
QgsField("Specifikace data", QMetaType.Type.QString),
|
QgsField("specifikace_data", QMetaType.Type.QString),
|
||||||
QgsField("Datum zahájeni", QMetaType.Type.QString),
|
QgsField("zahajeni", QMetaType.Type.QString),
|
||||||
QgsField("Datum ukončení", QMetaType.Type.QString),
|
QgsField("ukonceni", QMetaType.Type.QString),
|
||||||
QgsField("Hlavní typ", QMetaType.Type.QString),
|
QgsField("hlavni_typ", QMetaType.Type.QString),
|
||||||
QgsField("Vedlejší typ", QMetaType.Type.QString),
|
QgsField("vedlejsi_typ", QMetaType.Type.QString),
|
||||||
QgsField("Zjištění", QMetaType.Type.QString),
|
QgsField("zjisteni", QMetaType.Type.QString),
|
||||||
QgsField("Akce – nahrazuje NZ", QMetaType.Type.QString),
|
QgsField("nahrazuje_NZ", QMetaType.Type.QString),
|
||||||
]
|
]
|
||||||
elif typ_dat == "lokalita":
|
elif typ_dat == "lokalita":
|
||||||
cols += [
|
cols += [
|
||||||
@@ -357,29 +478,43 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
|
|||||||
|
|
||||||
# Use aliases for technical field names
|
# Use aliases for technical field names
|
||||||
alias_map = {
|
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",
|
"nazev_lokality": "Název lokality",
|
||||||
"popis_lokality": "Popis lokality",
|
"popis_lokality": "Popis lokality",
|
||||||
"typ_lokality": "Typ lokality",
|
"typ_lokality": "Typ lokality",
|
||||||
"druh_lokality": "Druh 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":
|
if komponenty == "true":
|
||||||
vl_komponenty = QgsVectorLayer("None", "AMCR Komponenty", "memory")
|
cols += [
|
||||||
pr = vl_komponenty.dataProvider()
|
|
||||||
komponenty_cols = [
|
|
||||||
QgsField("komponenta", QMetaType.Type.QString),
|
QgsField("komponenta", QMetaType.Type.QString),
|
||||||
QgsField("dj_id", QMetaType.Type.QString),
|
|
||||||
QgsField("komponenta_areal", QMetaType.Type.QString),
|
QgsField("komponenta_areal", QMetaType.Type.QString),
|
||||||
QgsField("komponenta_obdobi", 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:
|
for vl in layers:
|
||||||
vl.dataProvider().addAttributes(cols)
|
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_typ'], meta['lokalita_druh'],
|
||||||
meta['lokalita_zachovalost']
|
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'])
|
atributy.append(meta['pristupnost'])
|
||||||
|
|
||||||
|
if komponenty == "true":
|
||||||
|
atributy.extend([
|
||||||
|
meta.get('komponenta_id', ""),
|
||||||
|
meta.get('komponenta_areal', ""),
|
||||||
|
meta.get('komponenta_obdobi', ""),
|
||||||
|
])
|
||||||
|
|
||||||
feat.setAttributes(atributy)
|
feat.setAttributes(atributy)
|
||||||
target_list.append(feat)
|
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}")
|
print(f"Chyba při tvorbě feature: {ex}")
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if komponenty == "true":
|
# --- ADDING TO QGIS INTERFACE ---
|
||||||
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 ---
|
|
||||||
proj = QgsProject.instance()
|
proj = QgsProject.instance()
|
||||||
added = 0
|
added = 0
|
||||||
layers_to_process = [
|
layers_to_process = [
|
||||||
@@ -498,41 +625,16 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
|
|||||||
(feats_pt, vl_point, "Body"),
|
(feats_pt, vl_point, "Body"),
|
||||||
]
|
]
|
||||||
|
|
||||||
if komponenty == "true":
|
|
||||||
layers_to_process.append((feats_k, vl_komponenty, "Komponenty"))
|
|
||||||
|
|
||||||
for f, l, n in layers_to_process:
|
for f, l, n in layers_to_process:
|
||||||
if f:
|
if f:
|
||||||
l.dataProvider().addFeatures(f)
|
l.dataProvider().addFeatures(f)
|
||||||
l.updateExtents()
|
l.updateExtents()
|
||||||
l.setName(f"AMCR_{archeologicky_zaznam}_{n}")
|
l.setName(f"AMCR_{archeologicky_zaznam}_{n}")
|
||||||
proj.addMapLayer(l)
|
proj.addMapLayer(l)
|
||||||
if n != "Komponenty":
|
added += len(f)
|
||||||
added += len(f)
|
|
||||||
|
|
||||||
if added > 0:
|
if added > 0:
|
||||||
iface.messageBar().pushMessage("AMCR", f"Hotovo. Záznamů: {len(docs)} (s geom: {actions_with_geom}). Vykresleno: {added} prvků.", level=Qgis.MessageLevel.Success)
|
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 = [vl_poly.id(), vl_line.id(), 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")
|
|
||||||
|
|
||||||
if rel.isValid():
|
|
||||||
rel_manager.addPolymorphicRelation(rel)
|
|
||||||
else:
|
|
||||||
print("Relace Komponenty není validní!")
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
iface.messageBar().pushMessage("AMCR", "Žádná data k zobrazení.", level=Qgis.MessageLevel.Info)
|
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.QtCore import QSettings, QTranslator, QCoreApplication
|
||||||
from qgis.PyQt.QtGui import QIcon
|
from qgis.PyQt.QtGui import QIcon
|
||||||
from qgis.PyQt.QtWidgets import QMenu, QAction, QToolButton, QDialog
|
from qgis.PyQt.QtWidgets import QMenu, QAction, QToolButton, QDialog
|
||||||
|
from qgis.core import Qgis
|
||||||
|
from qgis.utils import iface
|
||||||
|
|
||||||
from .amcr_tools import load_amcr_data
|
from .amcr_tools import load_amcr_data, login_to_api
|
||||||
from .amcr_dialog import AmcrFilterDialog
|
from .amcr_dialog import AmcrFilterDialog, LoginDialog
|
||||||
from .resources import *
|
from .resources import *
|
||||||
import os.path
|
import os.path
|
||||||
|
|
||||||
@@ -109,6 +111,16 @@ class AmcrViewer:
|
|||||||
)
|
)
|
||||||
self.plugin_menu.addAction(self.action_download_lokality)
|
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
|
# 3. Create the main project action and attach the menu to it
|
||||||
main_icon = QIcon(icon_akce_path)
|
main_icon = QIcon(icon_akce_path)
|
||||||
self.main_action = QAction(main_icon, 'AMČR Viewer', self.iface.mainWindow())
|
self.main_action = QAction(main_icon, 'AMČR Viewer', self.iface.mainWindow())
|
||||||
@@ -168,4 +180,19 @@ class AmcrViewer:
|
|||||||
|
|
||||||
# Access the map canvas and start the fetch/render process from amcr_tools
|
# Access the map canvas and start the fetch/render process from amcr_tools
|
||||||
canvas = self.iface.mapCanvas()
|
canvas = self.iface.mapCanvas()
|
||||||
load_amcr_data(canvas, bbox, filters, typ_dat, komponenty)
|
load_amcr_data(canvas, bbox, filters, typ_dat, komponenty)
|
||||||
|
|
||||||
|
def login(self):
|
||||||
|
dlg = LoginDialog(parent=self.iface.mainWindow())
|
||||||
|
result = dlg.exec()
|
||||||
|
if result == QDialog.DialogCode.Accepted:
|
||||||
|
username, password = LoginDialog.get_credentials()
|
||||||
|
session = login_to_api(username, password)
|
||||||
|
if session:
|
||||||
|
self.iface.messageBar().pushMessage(
|
||||||
|
"AMČR", "Přihlášení proběhlo úspěšně.", level=Qgis.MessageLevel.Success
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.iface.messageBar().pushMessage(
|
||||||
|
"AMČR", "Přihlášení se nezdařilo – viz záložka AMČR login v panelu Zprávy.", level=Qgis.MessageLevel.Critical
|
||||||
|
)
|
||||||
+21832
-13606
File diff suppressed because it is too large
Load Diff
@@ -8,11 +8,11 @@ name=AMČR Viewer
|
|||||||
qgisMinimumVersion=3.4.0
|
qgisMinimumVersion=3.4.0
|
||||||
qgisMaximumVersion=4.99.0
|
qgisMaximumVersion=4.99.0
|
||||||
description=Viewing and downloading the AMČR data.
|
description=Viewing and downloading the AMČR data.
|
||||||
version=1.3.0
|
version=2.0.0-alpha.3
|
||||||
author=David Spáčil
|
author=David Spáčil
|
||||||
email=spacil@arub.cz
|
email=spacil@arub.cz
|
||||||
|
|
||||||
about=This plugin is intended for downloading the data (Fieldwork events, Sites and their Components) from the Digiarchive of the Archaeological Map of the Czech Republic (https://digiarchiv.aiscr.cz/). As of now, only publicly accessible data can be downloaded.
|
about=This plugin is intended for downloading the data (Fieldwork events, Sites, and their Components) from the Digital archive of the Archaeological Map of the Czech Republic (https://digiarchiv.aiscr.cz/).
|
||||||
|
|
||||||
tracker=https://github.com/ARUP-CAS/aiscr-qgis-amcr-viewer/issues
|
tracker=https://github.com/ARUP-CAS/aiscr-qgis-amcr-viewer/issues
|
||||||
repository=https://github.com/ARUP-CAS/aiscr-qgis-amcr-viewer
|
repository=https://github.com/ARUP-CAS/aiscr-qgis-amcr-viewer
|
||||||
@@ -31,7 +31,7 @@ homepage=https://amcr-help.aiscr.cz/digiarchiv/qgis-viewer.html
|
|||||||
category=Vector
|
category=Vector
|
||||||
icon=download.png
|
icon=download.png
|
||||||
# experimental flag
|
# experimental flag
|
||||||
experimental=False
|
experimental=True
|
||||||
|
|
||||||
# deprecated flag (applies to the whole plugin, not just a single version)
|
# deprecated flag (applies to the whole plugin, not just a single version)
|
||||||
deprecated=False
|
deprecated=False
|
||||||
@@ -41,9 +41,6 @@ deprecated=False
|
|||||||
# Check the documentation for more information.
|
# Check the documentation for more information.
|
||||||
# plugin_dependencies=
|
# plugin_dependencies=
|
||||||
|
|
||||||
# Category of the plugin: Raster, Vector, Database or Web
|
|
||||||
# category=
|
|
||||||
|
|
||||||
# If the plugin can run on QGIS Server.
|
# If the plugin can run on QGIS Server.
|
||||||
server=False
|
server=False
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user