mirror of
https://github.com/ARUP-CAS/aiscr-qgis-amcr-viewer.git
synced 2026-06-17 11:22:53 +02:00
Compare commits
32 Commits
v1.3.2
...
ee558aa718
| Author | SHA1 | Date | |
|---|---|---|---|
| ee558aa718 | |||
| eebd7668a5 | |||
| fd11bee274 | |||
| 93ed0ca810 | |||
| 64ec1ea7fd | |||
| 27e5fe02ac | |||
| 46c09c4a09 | |||
| 444d1c4826 | |||
| d9f5d2ae6e | |||
| d417a78b85 | |||
| ca827321d8 | |||
| b2001c625c | |||
| 88018aa432 | |||
| 45b6ab09b2 | |||
| 89e596802f | |||
| 4ea679ec9e | |||
| 830537f1a4 | |||
| 785b83c9c5 | |||
| 493696c67b | |||
| b313fc6db0 | |||
| 9a935261e6 | |||
| a4e30bf334 | |||
| 56389e27d7 | |||
| c8d42e2459 | |||
| a6ebbce4cf | |||
| 88149fbb30 | |||
| c0d054d22a | |||
| ba41039468 | |||
| 499b3b3f0a | |||
| 54f154b264 | |||
| c679e776df | |||
| a5604dfaa8 |
@@ -11,7 +11,7 @@ jobs:
|
||||
steps:
|
||||
# 1. Stáhne kód z repozitáře
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
# 2. Vytvoří ZIP (předpokládá, že kód je ve složce 'amcr_viewer')
|
||||
- name: Zip Plugin
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
|
||||
# 3. Nahraje ZIP k Releasu
|
||||
- name: Upload Release Asset
|
||||
uses: softprops/action-gh-release@v1
|
||||
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
files: amcr_viewer.zip
|
||||
|
||||
@@ -211,3 +211,4 @@ __marimo__/
|
||||
README_files/
|
||||
README.html
|
||||
amcr_viewer.zip
|
||||
pyrefly.toml
|
||||
|
||||
+6
-7
@@ -20,11 +20,10 @@ identifiers:
|
||||
value: 10.5281/zenodo.18609813
|
||||
repository-code: 'https://github.com/ARUP-CAS/aiscr-qgis-amcr-viewer'
|
||||
abstract: >-
|
||||
This QGIS plugin is intended for downloading the data
|
||||
(Fieldwork events data only, at the time) from the
|
||||
Digiarchive of the Archaeological Map of the Czech
|
||||
Republic (AMCR). As of now, only publicly accessible data
|
||||
can be downloaded.
|
||||
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/).
|
||||
license: GPL-3.0
|
||||
version: '1.0.1'
|
||||
date-released: '2026-02-11'
|
||||
version: '2.0.0'
|
||||
date-released: '2026-06-05'
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[](https://www.gnu.org/licenses/gpl-3.0)
|
||||
|
||||
**Platform:** QGIS 3.4.x
|
||||
**Platform:** QGIS 3.44.0–4.99.0
|
||||
|
||||
**Module Type:** Data Acquisition & Visualization
|
||||
|
||||
@@ -12,8 +12,7 @@
|
||||
|
||||
## 1. Overview
|
||||
|
||||
**AMCR Viewer** is a QGIS plugin designed to facilitate direct access to the Digital Archive of the Archaeological Map of the Czech Republic (AMČR). It allows researchers to **query, retrieve, and visualize *Fieldwork events* and *Sites* data (metadata and geometry) directly within the GIS environment**, eliminating the need to manually export data from the web interface. Both *Fieldwork events* and *Sites* layers may be accompanied by a *Components* layer with additional information. **Only publicly accessible data are supported at the time** (accessibility = anonymous).
|
||||
|
||||
**AMCR Viewer** is a QGIS plugin designed to facilitate direct access to the Digital Archive of the Archaeological Map of the Czech Republic (AMČR). It allows researchers to **query, retrieve, and visualize *Fieldwork events* and *Sites* data (metadata and geometry) directly within the GIS environment**, eliminating the need to manually export data from the web interface. Both *Fieldwork events* and *Sites* layers may optionally include component-level data (period and activity area) embedded directly in the attribute table. The plugin supports both **anonymous (public) access** and **authenticated access** for users with an AMČR account.
|
||||
|
||||
### Key Features
|
||||
|
||||
@@ -21,103 +20,163 @@
|
||||
* **Advanced Attribute Filtering:** Supports multi-criteria filtering using controlled vocabularies.
|
||||
* **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.
|
||||
|
||||
---
|
||||
* **Authenticated Access:** Users with an AMČR account can log in to access non-public records.
|
||||
|
||||
## 2. Installation Guide
|
||||
|
||||
**Install the plugin from QGIS plugin repository.**
|
||||
**Install the latest version of the plugin from the QGIS plugin repository.**
|
||||
|
||||
**OR**
|
||||
**OR** (*in case you need older version*)
|
||||
|
||||
*1. Obtain the [plugin distribution package](https://github.com/ARUP-CAS/aiscr-qgis-amcr-viewer/releases) (ZIP archive containing the `amcr_viewer` directory).*
|
||||
*2. Launch QGIS.*
|
||||
*3. Navigate to Plugins → Manage and Install Plugins...*
|
||||
*4. Select the Install from ZIP tab.*
|
||||
*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.*
|
||||
|
||||
---
|
||||
1. *Obtain the [plugin distribution package](https://github.com/ARUP-CAS/aiscr-qgis-amcr-viewer/releases) (ZIP archive containing the `amcr_viewer` directory).*
|
||||
2. *Launch QGIS.*
|
||||
3. *Navigate to Plugins → Manage and Install Plugins...*
|
||||
4. *Select the Install from ZIP tab.*
|
||||
5. *Locate the source ZIP file and click Install Plugin.*
|
||||
6. *Upon successful installation, the AMCR Viewer button will appear in the toolbar.*
|
||||
|
||||
## 3. User Manual
|
||||
|
||||
### 3.1 Data Retrieval
|
||||
### 3.1 Authentication (Optional)
|
||||
|
||||
To initiate a search query, click either the **Stáhnout data akcí** or the **Stáhnout data lokalit** icon from the dropdown menu. The filter dialog provides the following options. Shown options vary based on the choosed tool.
|
||||
By default, the plugin accesses only publicly available records (accessibility = anonymous). To access non-public data, log in using your AMČR account:
|
||||
|
||||
* **Spatial Filter:** *Checkbox "Limit search to current map extent":* If checked, the query is restricted to the geographical area currently visible in the QGIS canvas. If unchecked, the query searches the entire database (use with caution regarding data volume).
|
||||
* It is possible to view only those Fieldwork events with positive outcome, if "Positive findings only" is checked. Only *PIANs* marked as (or rather *PIANs* belonging to Documentation units marked as) "Type of evidence" = "positive" are rendered.
|
||||
* Click the dropdown arrow on the AMCR Viewer toolbar button and select **Přihlásit se**.
|
||||
* Enter your e-mail and password. Credentials are encrypted and stored securely in the **QGIS Authentication Manager** (DPAPI on Windows, Keychain on macOS, encrypted SQLite on Linux).
|
||||
* Stored credentials are reused automatically across sessions. To update or remove them, open the login dialog again.
|
||||
|
||||
### 3.2 Data Retrieval
|
||||
|
||||
To initiate a search query, click either the **Stáhnout data akcí** or the **Stáhnout data lokalit** option from the dropdown menu. The filter dialog provides the following options. Shown options vary based on the chosen tool.
|
||||
|
||||
* **Spatial Filter:** *Checkbox "Omezit vyhledávání rozsahem okna":* If checked, the query is restricted to the geographical area currently visible in the QGIS canvas. If unchecked, the query searches the entire database (use with caution regarding data volume).
|
||||
* **Positive findings only:** If checked, only *PIANs* belonging to Documentation units marked as "Type of evidence" = "positive" are included. *(Fieldwork events only.)*
|
||||
|
||||
* **Attribute Filters:**
|
||||
* The dialog utilizes "Picker" widgets for controlled vocabularies (common: Region, District, Cadastral area, Period, Activity Area, *PIAN* accuracy; *events* related: Organisation, Researcher, Event type; *sites* related: Site type and class, Level of confidence, State of preservation).
|
||||
* Click **Select...** to open a searchable selection window. Multiple values can be selected simultaneously (Logic: OR).
|
||||
* The dialog uses "Picker" widgets for controlled vocabularies (common: Region, District, Cadastral area, Period, Activity Area, *PIAN* accuracy, Accessibility; *events* related: Organisation, Researcher, Event type; *sites* related: Site type and class, Level of confidence, State of preservation).
|
||||
* Click **Vybrat...** to open a searchable selection window. Multiple values can be selected simultaneously (Logic: OR).
|
||||
|
||||
* **Codelists (Hesláře):**
|
||||
* Controlled vocabularies are downloaded from the AMČR OAI-PMH API and cached locally in `codelists/heslar.csv`.
|
||||
* To refresh all codelists, click the **Aktualizovat hesláře 🔄** button in the filter dialog. This runs as a background task and may take a few minutes.
|
||||
|
||||
* **Fieldwork Manager (Dynamic List):**
|
||||
* Due to the dynamic nature of the persons database, the list of Fieldwork Managers is retrieved from the AIS CR servers and needs to be updated the first time (and subsequently, if there is need).
|
||||
* To refresh the list from the server, click the **Refresh (🔄)** button next to the selection field. This downloads the latest list of researchers from the API.
|
||||
* **Components:** Check **Načíst komponenty** to include period and activity area data directly in the output layers.
|
||||
> ⚠ When components are loaded, spatial features are duplicated — each feature corresponds to one component. Spatial analyses (areas, counts) may be inaccurate.
|
||||
|
||||
* **Components:** The *components* data are downloaded as well upon checking the corresponding check box. This enriches the main (*Events* and *Sites*) layers with additional information (period and activity area).
|
||||
|
||||
* If no filter is used, all accessible Fieldwork events/PIANs are returned (although the number of Fieldwork events to be loaded is capped at 20000 records; it is advisable to set at least one filter).
|
||||
* If no filter is used, all accessible Fieldwork events/PIANs are returned (the number of records is capped at 20 000; it is advisable to set at least one filter).
|
||||
|
||||
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.3 Layer Structure & Attributes
|
||||
|
||||
Upon successful retrieval, the plugin generates up to three temporary memory layers:
|
||||
|
||||
### 3.2 Layer Structure & Attributes
|
||||
1. **AMCR\_[Akce|Lokalita]\_Polygony**
|
||||
2. **AMCR\_[Akce|Lokalita]\_Linie**
|
||||
3. **AMCR\_[Akce|Lokalita]\_Body**
|
||||
|
||||
Upon successful retrieval, the plugin generates four temporary memory layers:
|
||||
Layers are only created if the query returns features of the corresponding geometry type. All layers share the same attribute schema.
|
||||
|
||||
1. **AMCR Plochy (Polygons)**
|
||||
2. **AMČR Linie (Lines)**
|
||||
3. **AMČR Body (Points)**
|
||||
4. **AMČR Komponenty (*Components*/no geometry)**
|
||||
#### 3.3.1 Common fields
|
||||
|
||||
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.
|
||||
| Field | Description |
|
||||
| --- | --- |
|
||||
| pian | PIAN (spatial identifier) ID |
|
||||
| presnost | Spatial deviation \[units/tens/hundreds of meters/defined by cadastre\] |
|
||||
| pian\_typ | \[point/line/polygon\] |
|
||||
| dj | Documentation unit ID |
|
||||
| typ\_dj | \[trench/event part/whole event/cadastral territory\] |
|
||||
| definicni\_body | Feature centroid in WGS-84 coordinate system |
|
||||
| akce / lokalita | Fieldwork event / Site ID |
|
||||
| odkaz\_do\_digiarchivu | Link to the record in the Digital Archive |
|
||||
| okres | District |
|
||||
| katastr | Main cadastral area |
|
||||
| dalsi\_katastry | Other cadastral areas, if the event extends beyond the main cadastre |
|
||||
| Přístupnost | Record accessibility \[A/B/C/D\] |
|
||||
|
||||
---
|
||||
#### 3.3.2 Fields related to *Fieldwork events*
|
||||
|
||||
| Field | Description |
|
||||
| --- | --- |
|
||||
| akce\_lokalizace | Verbal description of the event location |
|
||||
| vedouci | Main fieldwork manager |
|
||||
| organizace | Organisation conducting the research |
|
||||
| specifikace\_data | \[exact date/exact years/sometime in years\] |
|
||||
| zahajeni | Event start date |
|
||||
| ukonceni | Event end date |
|
||||
| hlavni\_typ | Primary research method |
|
||||
| vedlejsi\_typ | Secondary research method |
|
||||
| zjisteni | Did the research reveal archaeological contexts? \[positive/negative\] |
|
||||
| nahrazuje\_NZ | Replaces a fieldwork report? \[yes/no\] |
|
||||
|
||||
#### 3.3.3 Fields related to *Sites*
|
||||
|
||||
| Field | Description |
|
||||
| --- | --- |
|
||||
| nazev\_lokality | Site name |
|
||||
| popis\_lokality | Site description |
|
||||
| typ\_lokality | Site classification by definition method |
|
||||
| druh\_lokality | Site classification by the nature of identified field relics |
|
||||
| zachovalost | Site preservation state |
|
||||
|
||||
#### 3.3.4 Component fields (only when *Načíst komponenty* is checked)
|
||||
|
||||
| Field | Description |
|
||||
| --- | --- |
|
||||
| komponenta | Component ID |
|
||||
| komponenta\_areal | Activity area \[settlement/burial area/field/…\] |
|
||||
| komponenta\_obdobi | Period \[Neolithic/High Middle Ages–Modern Period/…\] |
|
||||
|
||||
## 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.
|
||||
|
||||
> **Note:** The `requests` library is bundled with the QGIS installers for Windows and macOS. On Linux (distribution packages), it may need to be installed separately (e.g. `python3-requests`).
|
||||
|
||||
### 4.1 File Structure
|
||||
|
||||
* `amcr_viewer.py`: Entry point; handles GUI integration and initialization.
|
||||
* `amcr_dialog.py`: Manages the UI logic, including the custom `FilterableSelectionDialog` for handling large vocabularies.
|
||||
* `amcr_tools.py`: Core logic module. Handles API requests, pagination, data parsing, and vector layer generation.
|
||||
* `amcr_codelists.py`: Manages local caching of controlled vocabularies (`codelists/*.csv`).
|
||||
* `amcr_viewer.py`: Entry point; handles GUI integration, toolbar/menu setup, and login flow.
|
||||
* `amcr_dialog.py`: Manages the UI logic, including `AmcrFilterDialog`, `FilterableSelectionDialog`, and `LoginDialog`.
|
||||
* `amcr_tools.py`: Core logic module. Handles authentication, API requests, pagination, data parsing, and vector layer generation.
|
||||
* `amcr_codelists.py`: Manages local caching of controlled vocabularies (`codelists/heslar.csv`) downloaded via OAI-PMH.
|
||||
|
||||
### 4.2 Data Flow & API Integration
|
||||
|
||||
The plugin interacts with three primary endpoints of the AIS CR infrastructure:
|
||||
The plugin interacts with the following endpoints:
|
||||
|
||||
1. **Search API (Solr):**
|
||||
* Endpoint: `https://digiarchiv.aiscr.cz/api/search/query`
|
||||
* Method: `GET`
|
||||
* 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.
|
||||
1. **Login API:**
|
||||
* Endpoint: `https://digiarchiv.aiscr.cz/api/user/login`
|
||||
* Method: `POST`
|
||||
* Returns a session cookie used for subsequent authenticated requests.
|
||||
* Credentials are stored in the QGIS Authentication Manager; the session is restored automatically if it expires mid-download.
|
||||
|
||||
2. **Search API (Solr):**
|
||||
* Endpoint: `https://digiarchiv.aiscr.cz/api/search/query`
|
||||
* Method: `GET`
|
||||
* Parameters: `entity=akce|lokalita|pian`, `rows/page` (pagination), `mapa=true`.
|
||||
* Logic: Paginated in batches of 500 records (metadata) and 200 records (geometries). A safety cap of 20 000 records is enforced.
|
||||
|
||||
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.
|
||||
3. **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. Cached in memory for the session.
|
||||
|
||||
4. **Codelists API (OAI-PMH):**
|
||||
* Endpoint: `https://api.aiscr.cz/2.2/oai`
|
||||
* Used for downloading controlled vocabularies (periods, regions, organisations, etc.) on demand.
|
||||
|
||||
### 4.3 Data Persistence
|
||||
|
||||
* **Vocabularies:** Static vocabularies (e.g., Periods, Regions) are stored in `codelists/heslar.csv`.
|
||||
* **Dynamic Data:** The list of researchers is downloaded on-demand and cached in `codelists/vedouci.csv`.
|
||||
* **Vocabularies:** Stored in `codelists/heslar.csv`; updated on user request via the background task.
|
||||
* **Layers:** Output layers are created as `memory` layers. They are non-persistent and will be lost if QGIS is closed without saving.
|
||||
|
||||
### 4.4 Constraints
|
||||
|
||||
* **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.
|
||||
* **Record Limit:** A safety cap of 20 000 records is enforced.
|
||||
* **Batch Processing:** Geometry fetching is batched (200 IDs per request) to comply with URL length limitations and server load balancing.
|
||||
* **Component duplication:** When components are loaded, each output feature corresponds to one component rather than one documentation unit. A single PIAN may therefore appear multiple times in the layer.
|
||||
|
||||
## 6. Links and resources
|
||||
## 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).
|
||||
* [Import/Export. Pluginy propojující QGIS s AMČR \[poster\]](https://zenodo.org/records/20504909) (only in Czech; valid for v1.3.2).
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 533 B |
+232
-97
@@ -2,35 +2,68 @@
|
||||
import os
|
||||
import csv
|
||||
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
|
||||
PLUGIN_DIR = os.path.dirname(__file__)
|
||||
CODELISTS_DIR = os.path.join(PLUGIN_DIR, 'codelists')
|
||||
BASE_URL = "https://api.aiscr.cz/2.2/oai"
|
||||
OUTPUT_FILE = os.path.join(CODELISTS_DIR, 'heslar.csv')
|
||||
|
||||
slovnicek = {
|
||||
'obdobi': 'heslo:obdobi',
|
||||
'typ_akce': 'heslo:akce_typ',
|
||||
'areal': 'heslo:areal',
|
||||
'kraj': 'ruian_kraj',
|
||||
'organizace': 'organizace',
|
||||
'okres': 'ruian_okres',
|
||||
'katastr': 'ruian_katastr',
|
||||
'vedouci': 'osoba',
|
||||
'pian_presnost': 'heslo:pian_presnost',
|
||||
'typ_lokality': 'heslo:lokalita_typ',
|
||||
'druh_lokality': 'heslo:lokalita_druh',
|
||||
'jistota': 'heslo:jistota_urceni',
|
||||
'lokalita_zachovalost': 'heslo:stav_dochovani',
|
||||
'pristupnost': 'heslo:pristupnost'
|
||||
}
|
||||
|
||||
NS = {
|
||||
'oai': 'http://www.openarchives.org/OAI/2.0/',
|
||||
'dc': 'http://purl.org/dc/elements/1.1/',
|
||||
'oai_dc': 'http://www.openarchives.org/OAI/2.0/oai_dc/'
|
||||
}
|
||||
|
||||
|
||||
def ensure_codelists_dir():
|
||||
"""Creates the codelists directory if it does not exist."""
|
||||
if not os.path.exists(CODELISTS_DIR):
|
||||
os.makedirs(CODELISTS_DIR)
|
||||
|
||||
|
||||
def parse_codelist_file(filename, target_dict=None):
|
||||
"""Reads a CSV codelist file and populates the target dictionary grouped by categories."""
|
||||
"""
|
||||
Reads a CSV codelist file and populates
|
||||
the target dictionary grouped by categories.
|
||||
"""
|
||||
if target_dict is None:
|
||||
target_dict = {}
|
||||
|
||||
|
||||
path = os.path.join(CODELISTS_DIR, filename)
|
||||
|
||||
|
||||
# Return early if the file doesn't exist to avoid missing file errors
|
||||
if not os.path.exists(path):
|
||||
if not os.path.exists(path):
|
||||
return target_dict
|
||||
|
||||
|
||||
try:
|
||||
# Open the file using standard UTF-8 encoding
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
reader = csv.reader(f, delimiter=';')
|
||||
|
||||
|
||||
# Skip the CSV header row
|
||||
next(reader, None)
|
||||
|
||||
next(reader, None)
|
||||
|
||||
# Iterate through rows and extract label, code, and category
|
||||
for row in reader:
|
||||
if len(row) >= 3:
|
||||
@@ -38,109 +71,211 @@ def parse_codelist_file(filename, target_dict=None):
|
||||
code = row[1].strip()
|
||||
cat = row[2].strip()
|
||||
clean = code if code else None
|
||||
|
||||
# Initialize a new dictionary for a category if encountered for the first time
|
||||
|
||||
# Initialize a new dictionary for a category if encountered
|
||||
# for the first time
|
||||
if cat not in target_dict:
|
||||
target_dict[cat] = {}
|
||||
|
||||
# Assign the extracted code to the corresponding label within the category
|
||||
|
||||
# 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:
|
||||
# Check for cancellation at each iteration
|
||||
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:
|
||||
# Code (identifier)
|
||||
identifier_el = metadata.find('dc:identifier', NS)
|
||||
kod = (
|
||||
identifier_el.text
|
||||
if identifier_el is not None
|
||||
else ""
|
||||
)
|
||||
|
||||
# Title – filter out system labels "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
|
||||
# If no title passed the filter, fall back
|
||||
# to the first available one
|
||||
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
|
||||
)
|
||||
# Skip records without a valid one-letter code –
|
||||
# a None code would end up in the CSV and later
|
||||
# in the API filter as the string "None"
|
||||
if not kod:
|
||||
continue
|
||||
|
||||
dataset.append({
|
||||
'Název': nazev,
|
||||
'Kód': kod,
|
||||
'Kategorie': internal_name
|
||||
})
|
||||
|
||||
# Pagination
|
||||
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()
|
||||
|
||||
# API endpoint for fetching facet data for leaders
|
||||
url = "https://digiarchiv.aiscr.cz/api/search/query?entity=akce&sort=datestamp%20desc&page=0&onlyFacets=True&rows=0"
|
||||
|
||||
try:
|
||||
# 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', [])
|
||||
all_data = []
|
||||
total_sets = len(slovnicek)
|
||||
|
||||
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)
|
||||
for index, (interni, api_nazev) in enumerate(slovnicek.items()):
|
||||
# Check if the user cancelled the task via the QGIS taskbar
|
||||
if task and task.isCanceled():
|
||||
return False
|
||||
|
||||
# Initialize global codelist data when the module is imported
|
||||
_DATA = load_all_data()
|
||||
QgsMessageLog.logMessage(
|
||||
f"Zpracovávám kategorii: {interni}...",
|
||||
"AMČR", Qgis.Info)
|
||||
|
||||
# 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', {})
|
||||
# Pass the task correctly to the updated fetch function
|
||||
data = fetch_set(interni, api_nazev, task=task)
|
||||
|
||||
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
|
||||
if data is None:
|
||||
return False # Cancelled mid-download
|
||||
|
||||
all_data.extend(data)
|
||||
|
||||
# Report progress (0-100)
|
||||
if task:
|
||||
progress = (index + 1) / total_sets * 100
|
||||
task.setProgress(progress)
|
||||
|
||||
# Save to 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():
|
||||
"""Reloads data from files into the global variables."""
|
||||
data = load_all_data()
|
||||
|
||||
OBDOBI.clear()
|
||||
OBDOBI.update(data.get('obdobi', {}))
|
||||
TYP_AKCE.clear()
|
||||
TYP_AKCE.update(data.get('typ_akce', {}))
|
||||
AREAL.clear()
|
||||
AREAL.update(data.get('areal', {}))
|
||||
KRAJE.clear()
|
||||
KRAJE.update(data.get('kraj', {}))
|
||||
ORGANIZACE.clear()
|
||||
ORGANIZACE.update(data.get('organizace', {}))
|
||||
OKRESY.clear()
|
||||
OKRESY.update(data.get('okres', {}))
|
||||
KATASTRY.clear()
|
||||
KATASTRY.update(data.get('katastr', {}))
|
||||
VEDOUCI.clear()
|
||||
VEDOUCI.update(new_vedouci)
|
||||
|
||||
return len(VEDOUCI)
|
||||
VEDOUCI.update(data.get('vedouci', {}))
|
||||
PIAN_PRESNOST.clear()
|
||||
PIAN_PRESNOST.update(data.get('pian_presnost', {}))
|
||||
TYP_LOKALITY.clear()
|
||||
TYP_LOKALITY.update(data.get('typ_lokality', {}))
|
||||
DRUH_LOKALITY.clear()
|
||||
DRUH_LOKALITY.update(data.get('druh_lokality', {}))
|
||||
JISTOTA.clear()
|
||||
JISTOTA.update(data.get('jistota', {}))
|
||||
LOKALITA_ZACHOVALOST.clear()
|
||||
LOKALITA_ZACHOVALOST.update(data.get('lokalita_zachovalost', {}))
|
||||
PRISTUPNOST.clear()
|
||||
PRISTUPNOST.update(data.get('pristupnost', {}))
|
||||
|
||||
|
||||
# Initialize empty dicts that will be populated immediately below
|
||||
OBDOBI = {}
|
||||
TYP_AKCE = {}
|
||||
AREAL = {}
|
||||
KRAJE = {}
|
||||
ORGANIZACE = {}
|
||||
OKRESY = {}
|
||||
KATASTRY = {}
|
||||
VEDOUCI = {}
|
||||
PIAN_PRESNOST = {}
|
||||
TYP_LOKALITY = {}
|
||||
DRUH_LOKALITY = {}
|
||||
JISTOTA = {}
|
||||
LOKALITA_ZACHOVALOST = {}
|
||||
PRISTUPNOST = {}
|
||||
|
||||
refresh_globals()
|
||||
|
||||
+573
-93
@@ -1,50 +1,98 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from qgis.PyQt.QtWidgets import (QDialog, QVBoxLayout, QFormLayout,
|
||||
QLineEdit, QDialogButtonBox,
|
||||
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
|
||||
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)
|
||||
QMessageBox, QLabel, QFormLayout)
|
||||
from qgis.PyQt.QtCore import Qt, QSettings
|
||||
from qgis.core import (QgsTask, QgsApplication,
|
||||
QgsMessageLog, Qgis, QgsAuthMethodConfig)
|
||||
from qgis.utils import iface
|
||||
from .amcr_codelists import (OBDOBI, TYP_AKCE, KRAJE, AREAL, ORGANIZACE,
|
||||
OKRESY, KATASTRY, VEDOUCI, PIAN_PRESNOST,
|
||||
TYP_LOKALITY, DRUH_LOKALITY, JISTOTA,
|
||||
LOKALITA_ZACHOVALOST, PRISTUPNOST,
|
||||
download_heslare, refresh_globals)
|
||||
|
||||
|
||||
# Keep Python references to running tasks. QgsTaskManager only holds the
|
||||
# C++ object; without a Python-side reference the wrapper can be garbage
|
||||
# collected before the task finishes, which crashes QGIS.
|
||||
_ACTIVE_TASKS = []
|
||||
|
||||
|
||||
class UpdateCodelistsTask(QgsTask):
|
||||
def __init__(self, description):
|
||||
super().__init__(description, QgsTask.CanCancel)
|
||||
self.success = False
|
||||
self.exception = None
|
||||
|
||||
def run(self):
|
||||
"""Runs in a background thread."""
|
||||
try:
|
||||
# Call the download function with the task reference
|
||||
self.success = download_heslare(task=self)
|
||||
return self.success
|
||||
except Exception as e:
|
||||
self.exception = e
|
||||
return False
|
||||
|
||||
def finished(self, result):
|
||||
"""Runs in the main thread after run() completes."""
|
||||
if result:
|
||||
# Safely update the global variables in the main thread
|
||||
refresh_globals()
|
||||
QgsMessageLog.logMessage(
|
||||
"Hesláře AMČR byly úspěšně aktualizovány.",
|
||||
"AMČR", Qgis.Info)
|
||||
else:
|
||||
if self.isCanceled():
|
||||
QgsMessageLog.logMessage(
|
||||
"Aktualizace heslářů byla zrušena.",
|
||||
"AMČR", Qgis.Warning)
|
||||
else:
|
||||
QgsMessageLog.logMessage(
|
||||
f"Chyba aktualizace: {self.exception}",
|
||||
"AMČR", Qgis.Critical)
|
||||
|
||||
|
||||
class FilterableSelectionDialog(QDialog):
|
||||
"""
|
||||
A custom dialog for selecting multiple items from a list with a search filter.
|
||||
A custom dialog for selecting multiple items from
|
||||
a list with a search filter.
|
||||
Updated for PyQt6/Qt6 compatibility.
|
||||
"""
|
||||
def __init__(self, title, data_dict, preselected_codes, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle(f"Výběr: {title}")
|
||||
self.resize(400, 500)
|
||||
|
||||
|
||||
# Store the source data and previously selected items
|
||||
self.data_dict = data_dict
|
||||
self.preselected = preselected_codes if preselected_codes else []
|
||||
|
||||
|
||||
layout = QVBoxLayout()
|
||||
|
||||
|
||||
# Setup search input for filtering items
|
||||
self.search_bar = QLineEdit()
|
||||
self.search_bar.setPlaceholderText("Hledat v seznamu...")
|
||||
self.search_bar.textChanged.connect(self.filter_list)
|
||||
layout.addWidget(self.search_bar)
|
||||
|
||||
|
||||
# Main list widget for displaying selectable items
|
||||
self.list_widget = QListWidget()
|
||||
self.populate_list()
|
||||
layout.addWidget(self.list_widget)
|
||||
|
||||
|
||||
# Standard OK/Cancel dialog buttons
|
||||
buttons = QDialogButtonBox(
|
||||
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
||||
QDialogButtonBox.StandardButton.Ok
|
||||
| QDialogButtonBox.StandardButton.Cancel
|
||||
)
|
||||
buttons.accepted.connect(self.accept)
|
||||
buttons.rejected.connect(self.reject)
|
||||
layout.addWidget(buttons)
|
||||
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
def populate_list(self):
|
||||
@@ -53,19 +101,19 @@ class FilterableSelectionDialog(QDialog):
|
||||
for name in sorted_names:
|
||||
code = self.data_dict[name]
|
||||
item = QListWidgetItem(name)
|
||||
|
||||
|
||||
# Store the actual code (ID) hidden in the UserRole
|
||||
item.setData(Qt.ItemDataRole.UserRole, code)
|
||||
|
||||
|
||||
# Make the item checkable (adds a checkbox)
|
||||
item.setFlags(item.flags() | Qt.ItemFlag.ItemIsUserCheckable)
|
||||
|
||||
|
||||
# Restore previous selection state
|
||||
if code in self.preselected:
|
||||
item.setCheckState(Qt.CheckState.Checked)
|
||||
else:
|
||||
item.setCheckState(Qt.CheckState.Unchecked)
|
||||
|
||||
|
||||
self.list_widget.addItem(item)
|
||||
|
||||
def filter_list(self, text):
|
||||
@@ -96,21 +144,21 @@ class AmcrFilterDialog(QDialog):
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("Filtr AMČR")
|
||||
self.resize(500, 750)
|
||||
|
||||
# Determines if we are fetching 'akce' (projects) or 'lokalita' (locations)
|
||||
|
||||
# Determines if we are fetching 'akce' (events)
|
||||
# or 'lokalita' (sites)
|
||||
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_lokality': [], 'druh_lokality': [], 'jistota': [], 'lokalita_zachovalost': []
|
||||
'organizace': [], 'kraj': [], 'obdobi': [], 'areal': [],
|
||||
'typ_akce': [], 'okres': [], 'katastr': [], 'vedouci': [],
|
||||
'pian_presnost': [], 'pristupnost': [], 'typ_lokality': [],
|
||||
'druh_lokality': [], 'jistota': [], 'lokalita_zachovalost': []
|
||||
}
|
||||
|
||||
|
||||
layout = QVBoxLayout()
|
||||
|
||||
|
||||
# Filter by current map canvas extent
|
||||
self.chk_bbox = QCheckBox("Omezit vyhledávání rozsahem okna")
|
||||
self.chk_bbox.setChecked(True)
|
||||
@@ -121,9 +169,9 @@ class AmcrFilterDialog(QDialog):
|
||||
if self.typ_dat == "akce":
|
||||
self.chk_posevidence = QCheckBox("Pouze pozitivní zjištění")
|
||||
layout.addWidget(self.chk_posevidence)
|
||||
|
||||
|
||||
layout.addSpacing(10)
|
||||
|
||||
|
||||
# Spatial information – valid for all
|
||||
|
||||
self.picker_kraj = self.setup_picker("Kraj", 'kraj', KRAJE)
|
||||
@@ -132,144 +180,259 @@ class AmcrFilterDialog(QDialog):
|
||||
self.picker_okres = self.setup_picker("Okres", 'okres', OKRESY)
|
||||
layout.addWidget(self.picker_okres)
|
||||
|
||||
self.picker_katastr = self.setup_picker("Katastr", 'katastr', KATASTRY)
|
||||
self.picker_katastr = self.setup_picker(
|
||||
"Katastr",
|
||||
'katastr',
|
||||
KATASTRY
|
||||
)
|
||||
layout.addWidget(self.picker_katastr)
|
||||
|
||||
self.picker_presnost = self.setup_picker("PIAN – přesnost", 'pian_presnost', PIAN_PRESNOST)
|
||||
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)
|
||||
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
|
||||
|
||||
self.picker_typ = self.setup_picker("Typ výzkumu", 'typ_akce', TYP_AKCE)
|
||||
self.picker_typ = self.setup_picker(
|
||||
"Typ výzkumu",
|
||||
'typ_akce',
|
||||
TYP_AKCE
|
||||
)
|
||||
layout.addWidget(self.picker_typ)
|
||||
|
||||
# Filters valid for Lokality
|
||||
|
||||
if self.typ_dat == "lokalita":
|
||||
self.picker_typ_lokality = self.setup_picker("Lokalita – typ", 'typ_lokality', TYP_LOKALITY)
|
||||
self.picker_typ_lokality = self.setup_picker(
|
||||
"Lokalita – typ",
|
||||
'typ_lokality',
|
||||
TYP_LOKALITY
|
||||
)
|
||||
layout.addWidget(self.picker_typ_lokality)
|
||||
|
||||
self.picker_druh_lokality = self.setup_picker("Lokalita – druh", 'druh_lokality', DRUH_LOKALITY)
|
||||
self.picker_druh_lokality = self.setup_picker(
|
||||
"Lokalita – druh",
|
||||
'druh_lokality',
|
||||
DRUH_LOKALITY
|
||||
)
|
||||
layout.addWidget(self.picker_druh_lokality)
|
||||
|
||||
self.picker_jistota = self.setup_picker("Lokalita – jistota určení", 'jistota', JISTOTA)
|
||||
self.picker_jistota = self.setup_picker(
|
||||
"Lokalita – jistota určení",
|
||||
'jistota',
|
||||
JISTOTA
|
||||
)
|
||||
layout.addWidget(self.picker_jistota)
|
||||
|
||||
self.picker_lokalita_zachovalost = self.setup_picker("Lokalita - stav dochování", 'lokalita_zachovalost', LOKALITA_ZACHOVALOST)
|
||||
|
||||
self.picker_lokalita_zachovalost = self.setup_picker(
|
||||
"Lokalita - stav dochování",
|
||||
'lokalita_zachovalost',
|
||||
LOKALITA_ZACHOVALOST
|
||||
)
|
||||
layout.addWidget(self.picker_lokalita_zachovalost)
|
||||
|
||||
# Contextual information
|
||||
|
||||
self.picker_obdobi = self.setup_picker("Období", 'obdobi', OBDOBI)
|
||||
layout.addWidget(self.picker_obdobi)
|
||||
|
||||
|
||||
self.picker_areal = self.setup_picker("Areál", 'areal', AREAL)
|
||||
layout.addWidget(self.picker_areal)
|
||||
|
||||
# Option to download related components table
|
||||
self.chk_komponenty = QCheckBox("Načíst komponenty")
|
||||
layout.addWidget(self.chk_komponenty)
|
||||
|
||||
|
||||
# Warning label
|
||||
self.lbl_komponenty_warning = QLabel(
|
||||
"⚠ Při načtení komponent jsou prostorové prvky duplikovány — "
|
||||
"každý prvek odpovídá jedné komponentě. "
|
||||
"Prostorové analýzy (plochy, počty) mohou být zkreslené."
|
||||
)
|
||||
self.lbl_komponenty_warning.setWordWrap(True)
|
||||
self.lbl_komponenty_warning.setStyleSheet(
|
||||
"color: #8a6d00; background-color: #fff8e1; "
|
||||
"border: 1px solid #ffe082; border-radius: 4px; padding: 6px;"
|
||||
)
|
||||
self.lbl_komponenty_warning.setVisible(False)
|
||||
layout.addWidget(self.lbl_komponenty_warning)
|
||||
|
||||
self.chk_komponenty.toggled.connect(
|
||||
self.lbl_komponenty_warning.setVisible
|
||||
)
|
||||
|
||||
# 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)
|
||||
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
def setup_picker(self, label_text, cache_key, data_source, extra_btn=None):
|
||||
"""
|
||||
Creates a reusable UI component consisting of a label, a read-only
|
||||
text field showing selected items, and a button to open the selection dialog.
|
||||
Creates a reusable UI component consisting of a label, a read-only
|
||||
text field showing selected items, and a button to open
|
||||
the selection dialog.
|
||||
"""
|
||||
row_widget = QGroupBox(label_text)
|
||||
row_widget = QGroupBox(label_text)
|
||||
row_layout = QHBoxLayout()
|
||||
row_layout.setContentsMargins(5, 5, 5, 5)
|
||||
|
||||
|
||||
# Read-only field displaying the names of selected items
|
||||
display_field = QLineEdit()
|
||||
display_field.setReadOnly(True)
|
||||
display_field.setPlaceholderText("Nic nevybráno (vše)")
|
||||
display_field.setStyleSheet("background-color: #f0f0f0; color: #333;")
|
||||
|
||||
|
||||
btn = QPushButton("Vybrat...")
|
||||
btn.setFixedWidth(80)
|
||||
|
||||
# Nested function that handles opening the dialog and saving results
|
||||
|
||||
# Nested handler: opens the selection dialog and saves the result
|
||||
def open_dialog():
|
||||
dlg = FilterableSelectionDialog(label_text, data_source, self.selection_cache[cache_key], self)
|
||||
if dlg.exec() == QDialog.DialogCode.Accepted: # PyQt6: DialogCode
|
||||
dlg = FilterableSelectionDialog(
|
||||
label_text,
|
||||
data_source,
|
||||
self.selection_cache[cache_key],
|
||||
self
|
||||
)
|
||||
if dlg.exec() == QDialog.DialogCode.Accepted:
|
||||
codes, labels = dlg.get_selected_codes()
|
||||
# Update local cache with selected IDs
|
||||
# Update the local cache with selected IDs
|
||||
self.selection_cache[cache_key] = codes
|
||||
# Update the UI text field with selected names
|
||||
# Update the display field with the selected item names
|
||||
if labels:
|
||||
display_field.setText(", ".join(labels))
|
||||
else:
|
||||
display_field.clear()
|
||||
|
||||
# Special case: Pre-fill specific accuracy levels by default
|
||||
|
||||
# Special case: pre-select default PIAN accuracy levels
|
||||
if cache_key == 'pian_presnost':
|
||||
display_field.setText("odchylka jednotky metrů, odchylka desítky metrů, odchylka stovky metrů")
|
||||
self.selection_cache[cache_key] = ['HES-000861', 'HES-000862', 'HES-000863']
|
||||
display_field.setText(
|
||||
"odchylka jednotky metrů, odchylka desítky metrů, "
|
||||
"odchylka stovky metrů"
|
||||
)
|
||||
self.selection_cache[cache_key] = [
|
||||
'HES-000861',
|
||||
'HES-000862',
|
||||
'HES-000863'
|
||||
]
|
||||
|
||||
btn.clicked.connect(open_dialog)
|
||||
|
||||
|
||||
row_layout.addWidget(display_field)
|
||||
row_layout.addWidget(btn)
|
||||
|
||||
# Add an optional extra button (e.g., the refresh button for leaders)
|
||||
|
||||
# Optionally append an extra button (e.g. a refresh button)
|
||||
if extra_btn:
|
||||
row_layout.addWidget(extra_btn)
|
||||
|
||||
|
||||
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):
|
||||
# Create the task instance and keep a reference so the Python
|
||||
# wrapper survives until the task finishes
|
||||
task = UpdateCodelistsTask("Aktualizace heslářů AMČR")
|
||||
_ACTIVE_TASKS.append(task)
|
||||
|
||||
# Prevent parallel downloads overwriting heslar.csv
|
||||
self.btn_update.setEnabled(False)
|
||||
|
||||
# Message boxes are parented to the main window, not to this dialog –
|
||||
# the dialog may already be closed (and its C++ object deleted)
|
||||
# by the time the minute-long task finishes.
|
||||
parent_win = iface.mainWindow() if iface else None
|
||||
|
||||
def _cleanup():
|
||||
if task in _ACTIVE_TASKS:
|
||||
_ACTIVE_TASKS.remove(task)
|
||||
try:
|
||||
self.btn_update.setEnabled(True)
|
||||
except RuntimeError:
|
||||
pass # dialog already closed
|
||||
|
||||
def on_completed():
|
||||
_cleanup()
|
||||
QMessageBox.information(
|
||||
parent_win,
|
||||
"Hotovo",
|
||||
"Hesláře byly úspěšně aktualizovány."
|
||||
)
|
||||
|
||||
# Show the exact error if the task fails
|
||||
def on_error():
|
||||
_cleanup()
|
||||
if task.exception:
|
||||
# This will show exactly what went wrong (e.g. PermissionError)
|
||||
msg = (
|
||||
"Aktualizace selhala z důvodu chyby:\n"
|
||||
f"{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(parent_win, "Chyba / Zrušeno", msg)
|
||||
|
||||
task.taskCompleted.connect(on_completed)
|
||||
task.taskTerminated.connect(on_error)
|
||||
|
||||
QgsApplication.taskManager().addTask(task)
|
||||
|
||||
def get_bbox(self):
|
||||
return "true" if self.chk_bbox.isChecked() else "false"
|
||||
|
||||
|
||||
def get_komponenty(self):
|
||||
return "true" if self.chk_komponenty.isChecked() else "false"
|
||||
|
||||
|
||||
def get_filters(self):
|
||||
"""Compiles the user selections from the cache into API-ready filter parameters."""
|
||||
"""Compiles the user selections from the cache into
|
||||
API-ready filter parameters."""
|
||||
filters = {}
|
||||
|
||||
if self.selection_cache['kraj']:
|
||||
@@ -283,8 +446,10 @@ class AmcrFilterDialog(QDialog):
|
||||
if self.selection_cache['areal']:
|
||||
filters['f_areal'] = self.selection_cache['areal']
|
||||
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.chk_posevidence.isChecked():
|
||||
filters['posevidence'] = 'true'
|
||||
@@ -304,5 +469,320 @@ class AmcrFilterDialog(QDialog):
|
||||
filters['f_jistota'] = self.selection_cache['jistota']
|
||||
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.
|
||||
- loadAuthenticationConfig() with full=False loads only metadata (name, method,
|
||||
id) but NOT the config() values like username/password. Use full=True to
|
||||
access those.
|
||||
"""
|
||||
|
||||
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).
|
||||
# The Auth Manager must be unlocked before we attempt to read from it;
|
||||
# otherwise loadAuthenticationConfig() returns ok=False even for valid
|
||||
# configs, causing _has_saved to be incorrectly set to False.
|
||||
existing_id = QSettings().value(self.SETTINGS_KEY, "")
|
||||
if existing_id:
|
||||
QgsApplication.authManager().setMasterPassword(True)
|
||||
username = self._load_username_from_config(existing_id)
|
||||
self._has_saved = bool(existing_id) and bool(username)
|
||||
|
||||
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 the username from a stored config.
|
||||
Requires full=True since config() values are only populated
|
||||
when the config is fully decrypted."""
|
||||
ok, cfg = self._load_config(config_id, full=True)
|
||||
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
|
||||
|
||||
def _verify_credentials(self, username: str, password: str) -> bool:
|
||||
"""
|
||||
Verify the credentials against the API before saving them.
|
||||
Returns True if they should be stored: either the login succeeded,
|
||||
or the server was unreachable and the user chose to keep them
|
||||
unverified. Wrong credentials are never stored.
|
||||
"""
|
||||
# Lazy import to avoid an import cycle
|
||||
# (amcr_tools imports LoginDialog lazily as well)
|
||||
from . import amcr_tools
|
||||
|
||||
if amcr_tools.login_to_api(username, password):
|
||||
return True
|
||||
|
||||
if amcr_tools.LAST_LOGIN_ERROR == 'network':
|
||||
answer = QMessageBox.question(
|
||||
self,
|
||||
"Server nedostupný",
|
||||
"Přihlašovací údaje se nepodařilo ověřit – server AMČR "
|
||||
"je nedostupný.\nChcete je přesto uložit (neověřené)?",
|
||||
QMessageBox.StandardButton.Yes
|
||||
| QMessageBox.StandardButton.No
|
||||
)
|
||||
return answer == QMessageBox.StandardButton.Yes
|
||||
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
"Neplatné přihlašovací údaje",
|
||||
"Přihlášení se nezdařilo – zkontrolujte e-mail a heslo.\n"
|
||||
"Údaje nebyly uloženy."
|
||||
)
|
||||
return False
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 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
|
||||
# Verify the new username against the stored password
|
||||
if not self._verify_credentials(
|
||||
username, cfg.config("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
|
||||
|
||||
# Verify before prompting for the master password – wrong
|
||||
# credentials must never reach the Authentication Manager
|
||||
if not self._verify_credentials(username, password):
|
||||
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 = 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
|
||||
|
||||
+684
-234
File diff suppressed because it is too large
Load Diff
+96
-33
@@ -1,35 +1,39 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from qgis.PyQt.QtCore import QSettings, QTranslator, QCoreApplication
|
||||
from qgis.PyQt.QtGui import QIcon
|
||||
from qgis.PyQt.QtCore import QSettings, QTranslator, QCoreApplication, QUrl
|
||||
from qgis.PyQt.QtGui import QIcon, QDesktopServices
|
||||
from qgis.PyQt.QtWidgets import QMenu, QAction, QToolButton, QDialog
|
||||
from qgis.core import Qgis
|
||||
|
||||
from .amcr_tools import load_amcr_data
|
||||
from .amcr_dialog import AmcrFilterDialog
|
||||
from .resources import *
|
||||
from .amcr_tools import load_amcr_data, login_to_api
|
||||
from .amcr_dialog import AmcrFilterDialog, LoginDialog
|
||||
import os.path
|
||||
|
||||
|
||||
class AmcrViewer:
|
||||
"""
|
||||
Main plugin class that manages the GUI elements, menu entries,
|
||||
Main plugin class that manages the GUI elements, menu entries,
|
||||
and coordinates the flow between user input and data processing.
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self, iface):
|
||||
"""
|
||||
Constructor initializes the connection to QGIS interface and sets up
|
||||
Constructor initializes the connection to QGIS interface and sets up
|
||||
internationalization (i18n).
|
||||
"""
|
||||
self.iface = iface
|
||||
self.plugin_dir = os.path.dirname(__file__)
|
||||
|
||||
# Determine the user's locale to load appropriate translation files
|
||||
locale = QSettings().value('locale/userLocale')[0:2]
|
||||
|
||||
# Determine the user's locale to load appropriate translation files.
|
||||
# The setting may be missing (None) on a fresh QGIS install.
|
||||
locale = str(QSettings().value('locale/userLocale') or 'en')[0:2]
|
||||
locale_path = os.path.join(
|
||||
self.plugin_dir,
|
||||
'i18n',
|
||||
'AmcrViewer_{}.qm'.format(locale))
|
||||
'AmcrViewer_{}.qm'.format(locale)
|
||||
)
|
||||
|
||||
# Install the translator if a translation file for the current locale exists
|
||||
# Install the translator if a translation file
|
||||
# for the current locale exists
|
||||
if os.path.exists(locale_path):
|
||||
self.translator = QTranslator()
|
||||
self.translator.load(locale_path)
|
||||
@@ -41,14 +45,17 @@ class AmcrViewer:
|
||||
self.first_start = None
|
||||
|
||||
def tr(self, message):
|
||||
"""Helper method for translating strings within the AmcrViewer context."""
|
||||
"""
|
||||
Helper method for translating strings within
|
||||
the AmcrViewer context.
|
||||
"""
|
||||
return QCoreApplication.translate('AmcrViewer', message)
|
||||
|
||||
def add_action(self, icon_path, text, callback, enabled_flag=True,
|
||||
add_to_menu=True, add_to_toolbar=True, status_tip=None,
|
||||
whats_this=None, parent=None):
|
||||
"""
|
||||
Helper method to create QActions and automatically register them
|
||||
Helper method to create QActions and automatically register them
|
||||
into the QGIS Menu and Toolbar.
|
||||
"""
|
||||
icon = QIcon(icon_path)
|
||||
@@ -69,26 +76,29 @@ class AmcrViewer:
|
||||
if add_to_menu:
|
||||
self.iface.addPluginToMenu(self.menu, action)
|
||||
|
||||
# Store only actions that are directly attached to the QGIS UI for later cleanup
|
||||
# Store only actions that are directly attached
|
||||
# to the QGIS UI for later cleanup
|
||||
if add_to_toolbar or add_to_menu:
|
||||
self.actions.append(action)
|
||||
|
||||
|
||||
return action
|
||||
|
||||
def initGui(self):
|
||||
"""
|
||||
Called when the plugin is loaded. Creates the menu structure,
|
||||
Called when the plugin is loaded. Creates the menu structure,
|
||||
sub-actions, and the dropdown tool button in the toolbar.
|
||||
"""
|
||||
# Define paths for action-specific icons
|
||||
icon_akce_path = os.path.join(self.plugin_dir, 'akce.png')
|
||||
icon_lokality_path = os.path.join(self.plugin_dir, 'lokality.png')
|
||||
icon_amcr_help_path = os.path.join(self.plugin_dir, 'amcr-help.png')
|
||||
|
||||
# 1. Create a container menu for the plugin
|
||||
self.plugin_menu = QMenu()
|
||||
|
||||
# 2. Create sub-actions (Download Projects / Download Sites)
|
||||
# add_to_menu/toolbar is False because these go into our custom dropdown menu
|
||||
# add_to_menu/toolbar is False because these go into our
|
||||
# custom dropdown menu
|
||||
self.action_download_akce = self.add_action(
|
||||
icon_path=icon_akce_path,
|
||||
text=self.tr(u'Stáhnout data akcí | AMČR Viewer'),
|
||||
@@ -102,16 +112,40 @@ class AmcrViewer:
|
||||
self.action_download_lokality = self.add_action(
|
||||
icon_path=icon_lokality_path,
|
||||
text=self.tr(u'Stáhnout data lokalit | AMČR Viewer'),
|
||||
callback=lambda checked=False: self.run_download('lokalita'),
|
||||
callback=lambda checked=False: self.run_download('lokalita'),
|
||||
parent=self.iface.mainWindow(),
|
||||
add_to_menu=False,
|
||||
add_to_toolbar=False
|
||||
)
|
||||
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)
|
||||
|
||||
self.action_amcr_help = self.add_action(
|
||||
icon_path=icon_amcr_help_path,
|
||||
text=self.tr(u'Nápověda AMČR Help | AMČR Viewer'),
|
||||
callback=lambda checked=False: self.open_help(),
|
||||
parent=self.iface.mainWindow(),
|
||||
add_to_menu=False,
|
||||
add_to_toolbar=False
|
||||
)
|
||||
self.plugin_menu.addAction(self.action_amcr_help)
|
||||
|
||||
# 3. Create the main project action and attach the menu to it
|
||||
main_icon = QIcon(icon_akce_path)
|
||||
self.main_action = QAction(main_icon, 'AMČR Viewer', self.iface.mainWindow())
|
||||
self.main_action = QAction(
|
||||
main_icon,
|
||||
'AMČR Viewer',
|
||||
self.iface.mainWindow()
|
||||
)
|
||||
self.main_action.setMenu(self.plugin_menu)
|
||||
self.iface.addPluginToMenu(self.menu, self.main_action)
|
||||
|
||||
@@ -120,16 +154,19 @@ class AmcrViewer:
|
||||
self.tool_button = QToolButton()
|
||||
self.tool_button.setMenu(self.plugin_menu)
|
||||
self.tool_button.setDefaultAction(self.action_download_akce)
|
||||
self.tool_button.setPopupMode(QToolButton.ToolButtonPopupMode.MenuButtonPopup)
|
||||
|
||||
# Add the widget directly to the toolbar and store the reference for cleanup
|
||||
self.tool_button.setPopupMode(
|
||||
QToolButton.ToolButtonPopupMode.MenuButtonPopup
|
||||
)
|
||||
|
||||
# Add the widget directly to the toolbar
|
||||
# and store the reference for cleanup
|
||||
self.toolbar_action = self.iface.addToolBarWidget(self.tool_button)
|
||||
|
||||
|
||||
self.first_start = True
|
||||
|
||||
def unload(self):
|
||||
"""
|
||||
Called when the plugin is disabled or removed.
|
||||
Called when the plugin is disabled or removed.
|
||||
Ensures all GUI elements are removed from QGIS to avoid ghost icons.
|
||||
"""
|
||||
# 1. Remove the custom entry from the main 'Plugins' menu
|
||||
@@ -145,7 +182,7 @@ class AmcrViewer:
|
||||
self.iface.removePluginMenu(self.menu, action)
|
||||
self.iface.removeToolBarIcon(action)
|
||||
self.actions.clear()
|
||||
|
||||
|
||||
# 4. Reset map tools if currently active
|
||||
if hasattr(self, 'tool'):
|
||||
self.iface.mapCanvas().unsetMapTool(self.tool)
|
||||
@@ -153,19 +190,45 @@ class AmcrViewer:
|
||||
# --- Data downloading ---
|
||||
def run_download(self, typ_dat):
|
||||
"""
|
||||
Triggered by menu/toolbar actions. Opens the filter dialog and
|
||||
Triggered by menu/toolbar actions. Opens the filter dialog and
|
||||
hands off the parameters to the data loader.
|
||||
"""
|
||||
# Open the specific filter dialog (Projects vs Sites)
|
||||
dlg = AmcrFilterDialog(typ_dat)
|
||||
result = dlg.exec()
|
||||
|
||||
# If user confirmed the dialog (OK button), gather filters and load data
|
||||
|
||||
# If user confirmed the dialog (OK button),
|
||||
# gather filters and load data
|
||||
if result == QDialog.DialogCode.Accepted:
|
||||
filters = dlg.get_filters()
|
||||
bbox = dlg.get_bbox()
|
||||
komponenty = dlg.get_komponenty()
|
||||
|
||||
# Access the map canvas and start the fetch/render process from amcr_tools
|
||||
|
||||
# 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
|
||||
)
|
||||
|
||||
def open_help(self):
|
||||
help_url = "https://amcr-help.aiscr.cz/digiarchiv/qgis-viewer.html"
|
||||
QDesktopServices.openUrl(QUrl(help_url))
|
||||
|
||||
+21832
-13606
File diff suppressed because it is too large
Load Diff
@@ -5,14 +5,14 @@
|
||||
|
||||
[general]
|
||||
name=AMČR Viewer
|
||||
qgisMinimumVersion=3.4.0
|
||||
qgisMinimumVersion=3.44.0
|
||||
qgisMaximumVersion=4.99.0
|
||||
description=Viewing and downloading the AMČR data.
|
||||
version=1.3.2
|
||||
version=2.0.1
|
||||
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
|
||||
@@ -22,14 +22,32 @@ repository=https://github.com/ARUP-CAS/aiscr-qgis-amcr-viewer
|
||||
|
||||
hasProcessingProvider=no
|
||||
# Uncomment the following line and add your changelog:
|
||||
# changelog=
|
||||
changelog=
|
||||
Aktualizace na verzi 2.0.0 výrazně mění chování pluginu. Před updatem je doporučeno přečíst si seznam změn níže.
|
||||
Version 2.0.0 changes the plugin behavior dramatically. It is advised to check the changelog below.
|
||||
Plný seznam změn v češtině je dostupný zde: https://github.com/ARUP-CAS/aiscr-qgis-amcr-viewer/releases/tag/v2.0.0
|
||||
v2.0.1 (2026-06-10)
|
||||
* Fixed QGIS minimum version
|
||||
v2.0.0 (2026-06-05)
|
||||
* Added warning regarding the feature duplication when loading components to the dialog
|
||||
* Added Help button to the plugin menu
|
||||
* Code clean-up
|
||||
v2.0.0-alpha.4 (2026-06-03)
|
||||
* Backend filtering of the results based on the component-related filters improvement (plugin not only loads the results from API, it filters them further)
|
||||
v2.0.0-alpha.2–3 (2026-05-19)
|
||||
* Security vulnerabilities fix
|
||||
v2.0.0-alpha.1 (2026-05-19):
|
||||
* Attribute fields renamed to be ASCII compliant
|
||||
* Codelist update; codelist can be recompiled from AMČR API
|
||||
* Base element changed from Documentation Unit to Component if user asks for components to simplify result filtering
|
||||
* Plugin now supports logging with an AMČR account and enables the downloading of Events and Sites available to the roles Researcher and higher
|
||||
|
||||
# Tags are comma separated with spaces allowed
|
||||
tags=python,AMCR,AIS CR,archaeology,PIAN,AMČR
|
||||
tags=python,AMCR,AIS CR,archaeology,PIAN,AMČR,archeologie
|
||||
|
||||
homepage=https://amcr-help.aiscr.cz/digiarchiv/qgis-viewer.html
|
||||
category=Vector
|
||||
icon=download.png
|
||||
icon=akce.png
|
||||
# experimental flag
|
||||
experimental=False
|
||||
|
||||
@@ -41,9 +59,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