mirror of
https://github.com/ARUP-CAS/aiscr-qgis-amcr-viewer.git
synced 2026-06-19 04:12:55 +02:00
Compare commits
15 Commits
v2.0.0-alpha.2
...
v2.0.1
| Author | SHA1 | Date | |
|---|---|---|---|
| d417a78b85 | |||
| ca827321d8 | |||
| b2001c625c | |||
| 88018aa432 | |||
| 45b6ab09b2 | |||
| 89e596802f | |||
| 4ea679ec9e | |||
| 830537f1a4 | |||
| 785b83c9c5 | |||
| 493696c67b | |||
| b313fc6db0 | |||
| 9a935261e6 | |||
| a4e30bf334 | |||
| 56389e27d7 | |||
| c8d42e2459 |
@@ -211,3 +211,4 @@ __marimo__/
|
|||||||
README_files/
|
README_files/
|
||||||
README.html
|
README.html
|
||||||
amcr_viewer.zip
|
amcr_viewer.zip
|
||||||
|
pyrefly.toml
|
||||||
|
|||||||
+6
-7
@@ -20,11 +20,10 @@ identifiers:
|
|||||||
value: 10.5281/zenodo.18609813
|
value: 10.5281/zenodo.18609813
|
||||||
repository-code: 'https://github.com/ARUP-CAS/aiscr-qgis-amcr-viewer'
|
repository-code: 'https://github.com/ARUP-CAS/aiscr-qgis-amcr-viewer'
|
||||||
abstract: >-
|
abstract: >-
|
||||||
This QGIS plugin is intended for downloading the data
|
This plugin is intended for downloading the data
|
||||||
(Fieldwork events data only, at the time) from the
|
(Fieldwork events, Sites, and their Components) from
|
||||||
Digiarchive of the Archaeological Map of the Czech
|
the Digital archive of the Archaeological Map of the
|
||||||
Republic (AMCR). As of now, only publicly accessible data
|
Czech Republic (https://digiarchiv.aiscr.cz/).
|
||||||
can be downloaded.
|
|
||||||
license: GPL-3.0
|
license: GPL-3.0
|
||||||
version: '1.0.1'
|
version: '2.0.0'
|
||||||
date-released: '2026-02-11'
|
date-released: '2026-06-05'
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
[](https://www.gnu.org/licenses/gpl-3.0)
|
[](https://www.gnu.org/licenses/gpl-3.0)
|
||||||
|
|
||||||
**Platform:** QGIS 3.4.0–4.99.0
|
**Platform:** QGIS 3.44.0–4.99.0
|
||||||
|
|
||||||
**Module Type:** Data Acquisition & Visualization
|
**Module Type:** Data Acquisition & Visualization
|
||||||
|
|
||||||
@@ -12,8 +12,7 @@
|
|||||||
|
|
||||||
## 1. Overview
|
## 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
|
### Key Features
|
||||||
|
|
||||||
@@ -21,109 +20,112 @@
|
|||||||
* **Advanced Attribute Filtering:** Supports multi-criteria filtering using controlled vocabularies.
|
* **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.
|
* **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.
|
||||||
|
* **Authenticated Access:** Users with an AMČR account can log in to access non-public records.
|
||||||
|
|
||||||
## 2. Installation Guide
|
## 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).*
|
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.*
|
2. *Launch QGIS.*
|
||||||
*3. Navigate to Plugins → Manage and Install Plugins...*
|
3. *Navigate to Plugins → Manage and Install Plugins...*
|
||||||
*4. Select the Install from ZIP tab.*
|
4. *Select the Install from ZIP tab.*
|
||||||
*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 Viewer button will appear in the toolbar.*
|
||||||
|
|
||||||
## 3. User Manual
|
## 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).
|
* Click the dropdown arrow on the AMCR Viewer toolbar button and select **Přihlásit se**.
|
||||||
* 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.
|
* 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:**
|
* **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).
|
* 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 **Select...** to open a searchable selection window. Multiple values can be selected simultaneously (Logic: OR).
|
* 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):**
|
* **Components:** Check **Načíst komponenty** to include period and activity area data directly in the output layers.
|
||||||
* 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).
|
> ⚠ When components are loaded, spatial features are duplicated — each feature corresponds to one component. Spatial analyses (areas, counts) may be inaccurate.
|
||||||
* 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:** 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 (the number of records is capped at 20 000; it is advisable to set at least one filter).
|
||||||
|
|
||||||
* 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).
|
|
||||||
|
|
||||||
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.3 Layer Structure & Attributes
|
||||||
|
|
||||||
Upon successful retrieval, the plugin generates four temporary memory layers:
|
Upon successful retrieval, the plugin generates up to three temporary memory layers:
|
||||||
|
|
||||||
1. **AMCR Plochy (Polygons)**
|
1. **AMCR\_[Akce|Lokalita]\_Polygony**
|
||||||
2. **AMČR Linie (Lines)**
|
2. **AMCR\_[Akce|Lokalita]\_Linie**
|
||||||
3. **AMČR Body (Points)**
|
3. **AMCR\_[Akce|Lokalita]\_Body**
|
||||||
4. **AMČR Komponenty (*Components*/no geometry)**
|
|
||||||
|
|
||||||
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.
|
Layers are only created if the query returns features of the corresponding geometry type. All layers share the same attribute schema.
|
||||||
|
|
||||||
#### 3.2.1 Fields of the layers with geometry
|
#### 3.3.1 Common fields
|
||||||
|
|
||||||
| Field | Description |
|
| Field | Description |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| PIAN | PIAN (spatial identifier) ID |
|
| pian | PIAN (spatial identifier) ID |
|
||||||
| Přesnost | spatial deviation [in units/tens/hundreds of meters/defined by cadastre] |
|
| presnost | Spatial deviation \[units/tens/hundreds of meters/defined by cadastre\] |
|
||||||
| PIAN – typ | [point/line/polygon] |
|
| pian\_typ | \[point/line/polygon\] |
|
||||||
| Dokumentační jednotka | Documentation unit ID |
|
| dj | Documentation unit ID |
|
||||||
| Typ dokumentační jednotky | [trench/event part/whole event/cadastral territory] |
|
| typ\_dj | \[trench/event part/whole event/cadastral territory\] |
|
||||||
| Definiční bod(y) (WGS-84) | feature centroid in WGS-84 coordinate system |
|
| definicni\_body | Feature centroid in WGS-84 coordinate system |
|
||||||
| Akce/Lokalita | Event/Site ID |
|
| akce / lokalita | Fieldwork event / Site ID |
|
||||||
| Odkaz do Digitálního archivu AMČR | link to the Event/Site record in the Digital Archive |
|
| odkaz\_do\_digiarchivu | Link to the record in the Digital Archive |
|
||||||
| Okres | district |
|
| okres | District |
|
||||||
| Katastr | main cadastre |
|
| katastr | Main cadastral area |
|
||||||
| Další katastr | other cadastres, if the event extends beyond the main cadastre |
|
| dalsi\_katastry | Other cadastral areas, if the event extends beyond the main cadastre |
|
||||||
| Přístupnost | record accessibility [A/B/C/D] |
|
| Přístupnost | Record accessibility \[A/B/C/D\] |
|
||||||
|
|
||||||
> Common fields
|
#### 3.3.2 Fields related to *Fieldwork events*
|
||||||
|
|
||||||
| Field | Description |
|
| Field | Description |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| Vedoucí akce | main fieldwork manager |
|
| akce\_lokalizace | Verbal description of the event location |
|
||||||
| Organizace | organization conducting the research |
|
| vedouci | Main fieldwork manager |
|
||||||
| Specifikace data | [exact date/exact years/sometime in years] |
|
| organizace | Organisation conducting the research |
|
||||||
| Datum zahájení | Event start date |
|
| specifikace\_data | \[exact date/exact years/sometime in years\] |
|
||||||
| Datum ukončení | Event end date |
|
| zahajeni | Event start date |
|
||||||
| Hlavní typ | primary research method [total excavation/pit trench/surface collection survey/…] |
|
| ukonceni | Event end date |
|
||||||
| Vedlejší typ | secondary research method [same options as in Hlavní typ] |
|
| hlavni\_typ | Primary research method |
|
||||||
| Zjištění | did the research reveal archaeological contexts? [positive/negative] |
|
| vedlejsi\_typ | Secondary research method |
|
||||||
| Akce – lokalizace | verbal description of the event location |
|
| zjisteni | Did the research reveal archaeological contexts? \[positive/negative\] |
|
||||||
| Akce – nahrazuje NZ | replaces a fieldwork report? [yes/no] |
|
| nahrazuje\_NZ | Replaces a fieldwork report? \[yes/no\] |
|
||||||
|
|
||||||
> Fields related to *Fieldwork events*
|
#### 3.3.3 Fields related to *Sites*
|
||||||
|
|
||||||
| Field | Description |
|
| Field | Description |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| nazev_lokality | site name |
|
| nazev\_lokality | Site name |
|
||||||
| popis_lokality | site description |
|
| popis\_lokality | Site description |
|
||||||
| typ_lokality | site classification by definition method [survey polygon/heritage site/landscape] |
|
| typ\_lokality | Site classification by definition method |
|
||||||
| druh_lokality | site classification by the nature of identified field relics [aerial survey polygon/landscape/remains of settlement/…] |
|
| druh\_lokality | Site classification by the nature of identified field relics |
|
||||||
| zachovalost | site preservation state [buried site/ruin/aboveground remains/…] |
|
| zachovalost | Site preservation state |
|
||||||
|
|
||||||
> Fields related to *Sites*
|
#### 3.3.4 Component fields (only when *Načíst komponenty* is checked)
|
||||||
|
|
||||||
#### 3.2.2 Fields of the *Components* layer
|
|
||||||
|
|
||||||
| Field | Description |
|
| Field | Description |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| komponenta | Component ID |
|
| komponenta | Component ID |
|
||||||
| dj_id | parent Documentation unit ID |
|
| komponenta\_areal | Activity area \[settlement/burial area/field/…\] |
|
||||||
| komponenta_areal | Activity area [settlement/burial area/field/…] |
|
| komponenta\_obdobi | Period \[Neolithic/High Middle Ages–Modern Period/…\] |
|
||||||
| 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
|
||||||
|
|
||||||
@@ -131,40 +133,48 @@ The plugin is developed in **Python 3** using the **PyQt6** framework for the GU
|
|||||||
|
|
||||||
### 4.1 File Structure
|
### 4.1 File Structure
|
||||||
|
|
||||||
* `amcr_viewer.py`: Entry point; handles GUI integration and initialization.
|
* `amcr_viewer.py`: Entry point; handles GUI integration, toolbar/menu setup, and login flow.
|
||||||
* `amcr_dialog.py`: Manages the UI logic, including the custom `FilterableSelectionDialog` for handling large vocabularies.
|
* `amcr_dialog.py`: Manages the UI logic, including `AmcrFilterDialog`, `FilterableSelectionDialog`, and `LoginDialog`.
|
||||||
* `amcr_tools.py`: Core logic module. Handles API requests, pagination, data parsing, and vector layer generation.
|
* `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/*.csv`).
|
* `amcr_codelists.py`: Manages local caching of controlled vocabularies (`codelists/heslar.csv`) downloaded via OAI-PMH.
|
||||||
|
|
||||||
### 4.2 Data Flow & API Integration
|
### 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):**
|
1. **Login API:**
|
||||||
* Endpoint: `https://digiarchiv.aiscr.cz/api/search/query`
|
* Endpoint: `https://digiarchiv.aiscr.cz/api/user/login`
|
||||||
* Method: `GET`
|
* Method: `POST`
|
||||||
* Parameters: `entity=akce`, `rows/page` (pagination).
|
* Returns a session cookie used for subsequent authenticated requests.
|
||||||
* Logic: The plugin implements a `while True` loop to handle pagination, processing data in batches of 500 records to ensure stability.
|
* Credentials are stored in the QGIS Authentication Manager; the session is restored automatically if it expires mid-download.
|
||||||
|
|
||||||
2. **Translation API:**
|
2. **Search API (Solr):**
|
||||||
* Endpoint: `https://digiarchiv.aiscr.cz/api/assets/i18n/cs.json`
|
* Endpoint: `https://digiarchiv.aiscr.cz/api/search/query`
|
||||||
* Function: Retrieves the mapping between system codes (e.g., `HES-xxxx`) and Czech labels. This dictionary is cached in memory during the session.
|
* 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.
|
||||||
|
|
||||||
|
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
|
### 4.3 Data Persistence
|
||||||
|
|
||||||
* **Vocabularies:** Static vocabularies (e.g., Periods, Regions) are stored in `codelists/heslar.csv`.
|
* **Vocabularies:** Stored in `codelists/heslar.csv`; updated on user request via the background task.
|
||||||
* **Dynamic Data:** The list of researchers is downloaded on-demand and cached in `codelists/vedouci.csv`.
|
|
||||||
* **Layers:** Output layers are created as `memory` layers. They are non-persistent and will be lost if QGIS is closed without saving.
|
* **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
|
### 4.4 Constraints
|
||||||
|
|
||||||
* **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 (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.
|
||||||
### 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
|
## 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).
|
||||||
|
* [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 |
@@ -2,7 +2,7 @@
|
|||||||
import os
|
import os
|
||||||
import csv
|
import csv
|
||||||
import requests
|
import requests
|
||||||
import xml.etree.ElementTree as ET # nosec B314
|
import xml.etree.ElementTree as ET # nosec
|
||||||
import time
|
import time
|
||||||
from qgis.core import QgsMessageLog, Qgis
|
from qgis.core import QgsMessageLog, Qgis
|
||||||
|
|
||||||
@@ -13,20 +13,20 @@ BASE_URL = "https://api.aiscr.cz/2.2/oai"
|
|||||||
OUTPUT_FILE = os.path.join(CODELISTS_DIR, 'heslar.csv')
|
OUTPUT_FILE = os.path.join(CODELISTS_DIR, 'heslar.csv')
|
||||||
|
|
||||||
slovnicek = {
|
slovnicek = {
|
||||||
'obdobi' : 'heslo:obdobi',
|
'obdobi': 'heslo:obdobi',
|
||||||
'typ_akce' : 'heslo:akce_typ',
|
'typ_akce': 'heslo:akce_typ',
|
||||||
'areal' : 'heslo:areal',
|
'areal': 'heslo:areal',
|
||||||
'kraj' : 'ruian_kraj',
|
'kraj': 'ruian_kraj',
|
||||||
'organizace' : 'organizace',
|
'organizace': 'organizace',
|
||||||
'okres' : 'ruian_okres',
|
'okres': 'ruian_okres',
|
||||||
'katastr' : 'ruian_katastr',
|
'katastr': 'ruian_katastr',
|
||||||
'vedouci' : 'osoba',
|
'vedouci': 'osoba',
|
||||||
'pian_presnost' : 'heslo:pian_presnost',
|
'pian_presnost': 'heslo:pian_presnost',
|
||||||
'typ_lokality' : 'heslo:lokalita_typ',
|
'typ_lokality': 'heslo:lokalita_typ',
|
||||||
'druh_lokality' : 'heslo:lokalita_druh',
|
'druh_lokality': 'heslo:lokalita_druh',
|
||||||
'jistota' : 'heslo:jistota_urceni',
|
'jistota': 'heslo:jistota_urceni',
|
||||||
'lokalita_zachovalost' : 'heslo:stav_dochovani',
|
'lokalita_zachovalost': 'heslo:stav_dochovani',
|
||||||
'pristupnost' : 'heslo:pristupnost'
|
'pristupnost': 'heslo:pristupnost'
|
||||||
}
|
}
|
||||||
|
|
||||||
NS = {
|
NS = {
|
||||||
@@ -35,30 +35,35 @@ NS = {
|
|||||||
'oai_dc': 'http://www.openarchives.org/OAI/2.0/oai_dc/'
|
'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."""
|
||||||
if not os.path.exists(CODELISTS_DIR):
|
if not os.path.exists(CODELISTS_DIR):
|
||||||
os.makedirs(CODELISTS_DIR)
|
os.makedirs(CODELISTS_DIR)
|
||||||
|
|
||||||
|
|
||||||
def parse_codelist_file(filename, target_dict=None):
|
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:
|
if target_dict is None:
|
||||||
target_dict = {}
|
target_dict = {}
|
||||||
|
|
||||||
path = os.path.join(CODELISTS_DIR, filename)
|
path = os.path.join(CODELISTS_DIR, filename)
|
||||||
|
|
||||||
# Return early if the file doesn't exist to avoid missing file errors
|
# 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
|
return target_dict
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Open the file using standard UTF-8 encoding
|
# Open the file using standard UTF-8 encoding
|
||||||
with open(path, 'r', encoding='utf-8') as f:
|
with open(path, 'r', encoding='utf-8') as f:
|
||||||
reader = csv.reader(f, delimiter=';')
|
reader = csv.reader(f, delimiter=';')
|
||||||
|
|
||||||
# Skip the CSV header row
|
# Skip the CSV header row
|
||||||
next(reader, None)
|
next(reader, None)
|
||||||
|
|
||||||
# Iterate through rows and extract label, code, and category
|
# Iterate through rows and extract label, code, and category
|
||||||
for row in reader:
|
for row in reader:
|
||||||
if len(row) >= 3:
|
if len(row) >= 3:
|
||||||
@@ -66,18 +71,24 @@ def parse_codelist_file(filename, target_dict=None):
|
|||||||
code = row[1].strip()
|
code = row[1].strip()
|
||||||
cat = row[2].strip()
|
cat = row[2].strip()
|
||||||
clean = code if code else None
|
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:
|
if cat not in target_dict:
|
||||||
target_dict[cat] = {}
|
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
|
target_dict[cat][label] = clean
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
QgsMessageLog.logMessage(f"AMČR Codelist Read Error for {filename}: {e}", "AMČR", Qgis.Critical)
|
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 the codelist during plugin startup."""
|
"""Loads the codelist during plugin startup."""
|
||||||
ensure_codelists_dir()
|
ensure_codelists_dir()
|
||||||
@@ -85,6 +96,7 @@ def load_all_data():
|
|||||||
parse_codelist_file('heslar.csv', categorized_data)
|
parse_codelist_file('heslar.csv', categorized_data)
|
||||||
return categorized_data
|
return categorized_data
|
||||||
|
|
||||||
|
|
||||||
def fetch_set(internal_name, api_set, task=None):
|
def fetch_set(internal_name, api_set, task=None):
|
||||||
dataset = []
|
dataset = []
|
||||||
params = {
|
params = {
|
||||||
@@ -92,42 +104,60 @@ def fetch_set(internal_name, api_set, task=None):
|
|||||||
"metadataPrefix": "oai_dc",
|
"metadataPrefix": "oai_dc",
|
||||||
"set": api_set
|
"set": api_set
|
||||||
}
|
}
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
# Kontrola zrušení v každém kroku
|
# Check for cancellation at each iteration
|
||||||
if task and task.isCanceled():
|
if task and task.isCanceled():
|
||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = requests.get(BASE_URL, params=params, timeout=30)
|
response = requests.get(BASE_URL, params=params, timeout=30)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
root = ET.fromstring(response.content)
|
root = ET.fromstring(response.content) # nosec
|
||||||
|
|
||||||
records = root.findall('.//oai:record', NS)
|
records = root.findall('.//oai:record', NS)
|
||||||
for rec in records:
|
for rec in records:
|
||||||
metadata = rec.find('.//oai_dc:dc', NS)
|
metadata = rec.find('.//oai_dc:dc', NS)
|
||||||
if metadata is not None:
|
if metadata is not None:
|
||||||
# Kód (identifier)
|
# Code (identifier)
|
||||||
kod = metadata.find('dc:identifier', NS).text if metadata.find('dc:identifier', NS) is not None else ""
|
identifier_el = metadata.find('dc:identifier', NS)
|
||||||
|
kod = (
|
||||||
# Název (title) - filtrujeme systémové popisky "AMČR - ..."
|
identifier_el.text
|
||||||
|
if identifier_el is not None
|
||||||
|
else ""
|
||||||
|
)
|
||||||
|
|
||||||
|
# Title – filter out system labels "AMČR - ..."
|
||||||
titles = metadata.findall('dc:title', NS)
|
titles = metadata.findall('dc:title', NS)
|
||||||
nazev = ""
|
nazev = ""
|
||||||
for t in titles:
|
for t in titles:
|
||||||
if t.text and not t.text.startswith("AMČR -") and not t.text.startswith(" AMČR -"):
|
if (
|
||||||
|
t.text
|
||||||
|
and not t.text.startswith("AMČR -")
|
||||||
|
and not t.text.startswith(" AMČR -")
|
||||||
|
):
|
||||||
nazev = t.text
|
nazev = t.text
|
||||||
break
|
break
|
||||||
# Pokud by náhodou žádný title neprošel filtrem, vezmeme první dostupný
|
# If no title passed the filter, fall back
|
||||||
|
# to the first available one
|
||||||
if not nazev and titles:
|
if not nazev and titles:
|
||||||
nazev = titles[0].text
|
nazev = titles[0].text
|
||||||
|
|
||||||
specialni_pripady = ['okres', 'katastr']
|
specialni_pripady = ['okres', 'katastr']
|
||||||
|
|
||||||
if internal_name in specialni_pripady:
|
if internal_name in specialni_pripady:
|
||||||
kod = nazev
|
kod = nazev
|
||||||
|
|
||||||
if internal_name == 'pristupnost':
|
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)
|
kod = next(
|
||||||
|
(
|
||||||
|
t.text for t in titles
|
||||||
|
if t.text
|
||||||
|
and len(t.text) == 1
|
||||||
|
and t.text.isalpha()
|
||||||
|
),
|
||||||
|
None
|
||||||
|
)
|
||||||
|
|
||||||
dataset.append({
|
dataset.append({
|
||||||
'Název': nazev,
|
'Název': nazev,
|
||||||
@@ -135,7 +165,7 @@ def fetch_set(internal_name, api_set, task=None):
|
|||||||
'Kategorie': internal_name
|
'Kategorie': internal_name
|
||||||
})
|
})
|
||||||
|
|
||||||
# Stránkování
|
# Pagination
|
||||||
token = root.find('.//oai:resumptionToken', NS)
|
token = root.find('.//oai:resumptionToken', NS)
|
||||||
if token is not None and token.text:
|
if token is not None and token.text:
|
||||||
params = {
|
params = {
|
||||||
@@ -145,40 +175,45 @@ def fetch_set(internal_name, api_set, task=None):
|
|||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
QgsMessageLog.logMessage(f"Chyba u setu {api_set}: {e}", "AMČR", Qgis.Warning)
|
QgsMessageLog.logMessage(
|
||||||
|
f"Chyba u setu {api_set}: {e}",
|
||||||
|
"AMČR", Qgis.Warning)
|
||||||
break
|
break
|
||||||
|
|
||||||
return dataset
|
return dataset
|
||||||
|
|
||||||
|
|
||||||
def download_heslare(task=None):
|
def download_heslare(task=None):
|
||||||
"""Fetches the codelists from the AMČR API and saves it to a CSV file."""
|
"""Fetches the codelists from the AMČR API and saves it to a CSV file."""
|
||||||
ensure_codelists_dir()
|
ensure_codelists_dir()
|
||||||
all_data = []
|
all_data = []
|
||||||
total_sets = len(slovnicek)
|
total_sets = len(slovnicek)
|
||||||
|
|
||||||
for index, (interni, api_nazev) in enumerate(slovnicek.items()):
|
for index, (interni, api_nazev) in enumerate(slovnicek.items()):
|
||||||
# Pokud uživatel task zrušil v liště QGISu
|
# Check if the user cancelled the task via the QGIS taskbar
|
||||||
if task and task.isCanceled():
|
if task and task.isCanceled():
|
||||||
return False
|
return False
|
||||||
|
|
||||||
QgsMessageLog.logMessage(f"Zpracovávám kategorii: {interni}...", "AMČR", Qgis.Info)
|
QgsMessageLog.logMessage(
|
||||||
|
f"Zpracovávám kategorii: {interni}...",
|
||||||
# Nyní předáváme task správně do upravené funkce
|
"AMČR", Qgis.Info)
|
||||||
|
|
||||||
|
# Pass the task correctly to the updated fetch function
|
||||||
data = fetch_set(interni, api_nazev, task=task)
|
data = fetch_set(interni, api_nazev, task=task)
|
||||||
|
|
||||||
if data is None:
|
if data is None:
|
||||||
return False # Bylo zrušeno uprostřed stahování
|
return False # Cancelled mid-download
|
||||||
|
|
||||||
all_data.extend(data)
|
all_data.extend(data)
|
||||||
|
|
||||||
# Reportování postupu (0-100)
|
# Report progress (0-100)
|
||||||
if task:
|
if task:
|
||||||
progress = (index + 1) / total_sets * 100
|
progress = (index + 1) / total_sets * 100
|
||||||
task.setProgress(progress)
|
task.setProgress(progress)
|
||||||
|
|
||||||
# Uložení do CSV
|
# Save to CSV
|
||||||
with open(OUTPUT_FILE, 'w', newline='', encoding='utf-8-sig') as f:
|
with open(OUTPUT_FILE, 'w', newline='', encoding='utf-8-sig') as f:
|
||||||
fieldnames = ['Název', 'Kód', 'Kategorie']
|
fieldnames = ['Název', 'Kód', 'Kategorie']
|
||||||
writer = csv.DictWriter(f, fieldnames=fieldnames, delimiter=';')
|
writer = csv.DictWriter(f, fieldnames=fieldnames, delimiter=';')
|
||||||
@@ -187,13 +222,11 @@ def download_heslare(task=None):
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def refresh_globals():
|
def refresh_globals():
|
||||||
"""Znovu načte data ze souborů do globálních proměnných."""
|
"""Reloads data from files into the global variables."""
|
||||||
global OBDOBI, TYP_AKCE, AREAL, KRAJE, ORGANIZACE, OKRESY, KATASTRY
|
|
||||||
global VEDOUCI, PIAN_PRESNOST, TYP_LOKALITY, DRUH_LOKALITY, JISTOTA, LOKALITA_ZACHOVALOST, PRISTUPNOST
|
|
||||||
|
|
||||||
data = load_all_data()
|
data = load_all_data()
|
||||||
|
|
||||||
OBDOBI.clear()
|
OBDOBI.clear()
|
||||||
OBDOBI.update(data.get('obdobi', {}))
|
OBDOBI.update(data.get('obdobi', {}))
|
||||||
TYP_AKCE.clear()
|
TYP_AKCE.clear()
|
||||||
@@ -224,7 +257,7 @@ def refresh_globals():
|
|||||||
PRISTUPNOST.update(data.get('pristupnost', {}))
|
PRISTUPNOST.update(data.get('pristupnost', {}))
|
||||||
|
|
||||||
|
|
||||||
# Inicializace prázdných diktů, které se naplní hned pod tím
|
# Initialize empty dicts that will be populated immediately below
|
||||||
OBDOBI = {}
|
OBDOBI = {}
|
||||||
TYP_AKCE = {}
|
TYP_AKCE = {}
|
||||||
AREAL = {}
|
AREAL = {}
|
||||||
@@ -240,4 +273,4 @@ JISTOTA = {}
|
|||||||
LOKALITA_ZACHOVALOST = {}
|
LOKALITA_ZACHOVALOST = {}
|
||||||
PRISTUPNOST = {}
|
PRISTUPNOST = {}
|
||||||
|
|
||||||
refresh_globals()
|
refresh_globals()
|
||||||
|
|||||||
+255
-116
@@ -1,16 +1,19 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from qgis.PyQt.QtWidgets import (QDialog, QVBoxLayout,
|
from qgis.PyQt.QtWidgets import (QDialog, QVBoxLayout,
|
||||||
QLineEdit, QDialogButtonBox,
|
QLineEdit, QDialogButtonBox,
|
||||||
QCheckBox, QGroupBox, QPushButton,
|
QCheckBox, QGroupBox, QPushButton,
|
||||||
QListWidget, QListWidgetItem, QHBoxLayout,
|
QListWidget, QListWidgetItem, QHBoxLayout,
|
||||||
QMessageBox, QLabel, QFormLayout)
|
QMessageBox, QLabel, QFormLayout)
|
||||||
from qgis.PyQt.QtCore import Qt, QSettings
|
from qgis.PyQt.QtCore import Qt, QSettings
|
||||||
from qgis.core import QgsTask, QgsApplication, QgsMessageLog, Qgis, QgsAuthMethodConfig
|
from qgis.core import (QgsTask, QgsApplication,
|
||||||
from .amcr_codelists import (OBDOBI, TYP_AKCE, KRAJE, AREAL, ORGANIZACE,
|
QgsMessageLog, Qgis, QgsAuthMethodConfig)
|
||||||
OKRESY, KATASTRY, VEDOUCI, PIAN_PRESNOST, TYP_LOKALITY,
|
from .amcr_codelists import (OBDOBI, TYP_AKCE, KRAJE, AREAL, ORGANIZACE,
|
||||||
DRUH_LOKALITY, JISTOTA, LOKALITA_ZACHOVALOST, PRISTUPNOST,
|
OKRESY, KATASTRY, VEDOUCI, PIAN_PRESNOST,
|
||||||
|
TYP_LOKALITY, DRUH_LOKALITY, JISTOTA,
|
||||||
|
LOKALITA_ZACHOVALOST, PRISTUPNOST,
|
||||||
download_heslare, refresh_globals)
|
download_heslare, refresh_globals)
|
||||||
|
|
||||||
|
|
||||||
class UpdateCodelistsTask(QgsTask):
|
class UpdateCodelistsTask(QgsTask):
|
||||||
def __init__(self, description):
|
def __init__(self, description):
|
||||||
super().__init__(description, QgsTask.CanCancel)
|
super().__init__(description, QgsTask.CanCancel)
|
||||||
@@ -18,9 +21,9 @@ class UpdateCodelistsTask(QgsTask):
|
|||||||
self.exception = None
|
self.exception = None
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
"""Tato část běží ve vedlejším vlákně."""
|
"""Runs in a background thread."""
|
||||||
try:
|
try:
|
||||||
# Voláme upravenou funkci
|
# Call the download function with the task reference
|
||||||
self.success = download_heslare(task=self)
|
self.success = download_heslare(task=self)
|
||||||
return self.success
|
return self.success
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -28,52 +31,61 @@ class UpdateCodelistsTask(QgsTask):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def finished(self, result):
|
def finished(self, result):
|
||||||
"""Tato část běží v hlavním vlákně po skončení run()."""
|
"""Runs in the main thread after run() completes."""
|
||||||
if result:
|
if result:
|
||||||
# Teď bezpečně aktualizujeme globální proměnné v hlavním vlákně
|
# Safely update the global variables in the main thread
|
||||||
refresh_globals()
|
refresh_globals()
|
||||||
QgsMessageLog.logMessage("Hesláře AMČR byly úspěšně aktualizovány.", "AMČR", Qgis.Info)
|
QgsMessageLog.logMessage(
|
||||||
|
"Hesláře AMČR byly úspěšně aktualizovány.",
|
||||||
|
"AMČR", Qgis.Info)
|
||||||
else:
|
else:
|
||||||
if self.isCanceled():
|
if self.isCanceled():
|
||||||
QgsMessageLog.logMessage("Aktualizace heslářů byla zrušena.", "AMČR", Qgis.Warning)
|
QgsMessageLog.logMessage(
|
||||||
|
"Aktualizace heslářů byla zrušena.",
|
||||||
|
"AMČR", Qgis.Warning)
|
||||||
else:
|
else:
|
||||||
QgsMessageLog.logMessage(f"Chyba aktualizace: {self.exception}", "AMČR", Qgis.Critical)
|
QgsMessageLog.logMessage(
|
||||||
|
f"Chyba aktualizace: {self.exception}",
|
||||||
|
"AMČR", Qgis.Critical)
|
||||||
|
|
||||||
|
|
||||||
class FilterableSelectionDialog(QDialog):
|
class FilterableSelectionDialog(QDialog):
|
||||||
"""
|
"""
|
||||||
A custom dialog for selecting multiple items from a list with a search filter.
|
A custom dialog for selecting multiple items from
|
||||||
|
a list with a search filter.
|
||||||
Updated for PyQt6/Qt6 compatibility.
|
Updated for PyQt6/Qt6 compatibility.
|
||||||
"""
|
"""
|
||||||
def __init__(self, title, data_dict, preselected_codes, parent=None):
|
def __init__(self, title, data_dict, preselected_codes, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.setWindowTitle(f"Výběr: {title}")
|
self.setWindowTitle(f"Výběr: {title}")
|
||||||
self.resize(400, 500)
|
self.resize(400, 500)
|
||||||
|
|
||||||
# Store the source data and previously selected items
|
# Store the source data and previously selected items
|
||||||
self.data_dict = data_dict
|
self.data_dict = data_dict
|
||||||
self.preselected = preselected_codes if preselected_codes else []
|
self.preselected = preselected_codes if preselected_codes else []
|
||||||
|
|
||||||
layout = QVBoxLayout()
|
layout = QVBoxLayout()
|
||||||
|
|
||||||
# Setup search input for filtering items
|
# Setup search input for filtering items
|
||||||
self.search_bar = QLineEdit()
|
self.search_bar = QLineEdit()
|
||||||
self.search_bar.setPlaceholderText("Hledat v seznamu...")
|
self.search_bar.setPlaceholderText("Hledat v seznamu...")
|
||||||
self.search_bar.textChanged.connect(self.filter_list)
|
self.search_bar.textChanged.connect(self.filter_list)
|
||||||
layout.addWidget(self.search_bar)
|
layout.addWidget(self.search_bar)
|
||||||
|
|
||||||
# Main list widget for displaying selectable items
|
# Main list widget for displaying selectable items
|
||||||
self.list_widget = QListWidget()
|
self.list_widget = QListWidget()
|
||||||
self.populate_list()
|
self.populate_list()
|
||||||
layout.addWidget(self.list_widget)
|
layout.addWidget(self.list_widget)
|
||||||
|
|
||||||
# Standard OK/Cancel dialog buttons
|
# Standard OK/Cancel dialog buttons
|
||||||
buttons = QDialogButtonBox(
|
buttons = QDialogButtonBox(
|
||||||
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
QDialogButtonBox.StandardButton.Ok
|
||||||
|
| QDialogButtonBox.StandardButton.Cancel
|
||||||
)
|
)
|
||||||
buttons.accepted.connect(self.accept)
|
buttons.accepted.connect(self.accept)
|
||||||
buttons.rejected.connect(self.reject)
|
buttons.rejected.connect(self.reject)
|
||||||
layout.addWidget(buttons)
|
layout.addWidget(buttons)
|
||||||
|
|
||||||
self.setLayout(layout)
|
self.setLayout(layout)
|
||||||
|
|
||||||
def populate_list(self):
|
def populate_list(self):
|
||||||
@@ -82,19 +94,19 @@ class FilterableSelectionDialog(QDialog):
|
|||||||
for name in sorted_names:
|
for name in sorted_names:
|
||||||
code = self.data_dict[name]
|
code = self.data_dict[name]
|
||||||
item = QListWidgetItem(name)
|
item = QListWidgetItem(name)
|
||||||
|
|
||||||
# Store the actual code (ID) hidden in the UserRole
|
# Store the actual code (ID) hidden in the UserRole
|
||||||
item.setData(Qt.ItemDataRole.UserRole, code)
|
item.setData(Qt.ItemDataRole.UserRole, code)
|
||||||
|
|
||||||
# Make the item checkable (adds a checkbox)
|
# Make the item checkable (adds a checkbox)
|
||||||
item.setFlags(item.flags() | Qt.ItemFlag.ItemIsUserCheckable)
|
item.setFlags(item.flags() | Qt.ItemFlag.ItemIsUserCheckable)
|
||||||
|
|
||||||
# Restore previous selection state
|
# Restore previous selection state
|
||||||
if code in self.preselected:
|
if code in self.preselected:
|
||||||
item.setCheckState(Qt.CheckState.Checked)
|
item.setCheckState(Qt.CheckState.Checked)
|
||||||
else:
|
else:
|
||||||
item.setCheckState(Qt.CheckState.Unchecked)
|
item.setCheckState(Qt.CheckState.Unchecked)
|
||||||
|
|
||||||
self.list_widget.addItem(item)
|
self.list_widget.addItem(item)
|
||||||
|
|
||||||
def filter_list(self, text):
|
def filter_list(self, text):
|
||||||
@@ -125,20 +137,21 @@ class AmcrFilterDialog(QDialog):
|
|||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.setWindowTitle("Filtr AMČR")
|
self.setWindowTitle("Filtr AMČR")
|
||||||
self.resize(500, 750)
|
self.resize(500, 750)
|
||||||
|
|
||||||
# Determines if we are fetching 'akce' (projects) or 'lokalita' (locations)
|
# Determines if we are fetching 'akce' (events)
|
||||||
|
# or 'lokalita' (sites)
|
||||||
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': [], 'pristupnost': [],
|
'typ_akce': [], 'okres': [], 'katastr': [], 'vedouci': [],
|
||||||
'typ_lokality': [], 'druh_lokality': [], 'jistota': [], 'lokalita_zachovalost': []
|
'pian_presnost': [], 'pristupnost': [], 'typ_lokality': [],
|
||||||
|
'druh_lokality': [], 'jistota': [], 'lokalita_zachovalost': []
|
||||||
}
|
}
|
||||||
|
|
||||||
layout = QVBoxLayout()
|
layout = QVBoxLayout()
|
||||||
|
|
||||||
# Filter by current map canvas extent
|
# Filter by current map canvas extent
|
||||||
self.chk_bbox = QCheckBox("Omezit vyhledávání rozsahem okna")
|
self.chk_bbox = QCheckBox("Omezit vyhledávání rozsahem okna")
|
||||||
self.chk_bbox.setChecked(True)
|
self.chk_bbox.setChecked(True)
|
||||||
@@ -149,9 +162,9 @@ class AmcrFilterDialog(QDialog):
|
|||||||
if self.typ_dat == "akce":
|
if self.typ_dat == "akce":
|
||||||
self.chk_posevidence = QCheckBox("Pouze pozitivní zjištění")
|
self.chk_posevidence = QCheckBox("Pouze pozitivní zjištění")
|
||||||
layout.addWidget(self.chk_posevidence)
|
layout.addWidget(self.chk_posevidence)
|
||||||
|
|
||||||
layout.addSpacing(10)
|
layout.addSpacing(10)
|
||||||
|
|
||||||
# Spatial information – valid for all
|
# Spatial information – valid for all
|
||||||
|
|
||||||
self.picker_kraj = self.setup_picker("Kraj", 'kraj', KRAJE)
|
self.picker_kraj = self.setup_picker("Kraj", 'kraj', KRAJE)
|
||||||
@@ -160,156 +173,241 @@ class AmcrFilterDialog(QDialog):
|
|||||||
self.picker_okres = self.setup_picker("Okres", 'okres', OKRESY)
|
self.picker_okres = self.setup_picker("Okres", 'okres', OKRESY)
|
||||||
layout.addWidget(self.picker_okres)
|
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)
|
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)
|
layout.addWidget(self.picker_presnost)
|
||||||
|
|
||||||
self.picker_pristupnost = self.setup_picker("Přístupnost", 'pristupnost', PRISTUPNOST)
|
self.picker_pristupnost = self.setup_picker(
|
||||||
|
"Přístupnost",
|
||||||
|
'pristupnost',
|
||||||
|
PRISTUPNOST
|
||||||
|
)
|
||||||
layout.addWidget(self.picker_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)
|
||||||
|
|
||||||
self.picker_vedouci = self.setup_picker("Vedoucí výzkumu", 'vedouci', 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
|
||||||
|
|
||||||
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)
|
layout.addWidget(self.picker_typ)
|
||||||
|
|
||||||
# Filters valid for Lokality
|
# Filters valid for Lokality
|
||||||
|
|
||||||
if self.typ_dat == "lokalita":
|
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)
|
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)
|
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)
|
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)
|
layout.addWidget(self.picker_lokalita_zachovalost)
|
||||||
|
|
||||||
# Contextual information
|
# Contextual information
|
||||||
|
|
||||||
self.picker_obdobi = self.setup_picker("Období", 'obdobi', OBDOBI)
|
self.picker_obdobi = self.setup_picker("Období", 'obdobi', OBDOBI)
|
||||||
layout.addWidget(self.picker_obdobi)
|
layout.addWidget(self.picker_obdobi)
|
||||||
|
|
||||||
self.picker_areal = self.setup_picker("Areál", 'areal', AREAL)
|
self.picker_areal = self.setup_picker("Areál", 'areal', AREAL)
|
||||||
layout.addWidget(self.picker_areal)
|
layout.addWidget(self.picker_areal)
|
||||||
|
|
||||||
# Option to download related components table
|
# Option to download related components table
|
||||||
self.chk_komponenty = QCheckBox("Načíst komponenty")
|
self.chk_komponenty = QCheckBox("Načíst komponenty")
|
||||||
layout.addWidget(self.chk_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
|
# Pushes everything above to the top
|
||||||
layout.addStretch(1)
|
layout.addStretch(1)
|
||||||
|
|
||||||
# Main dialog OK/Cancel/Update buttons
|
# Main dialog OK/Cancel/Update buttons
|
||||||
|
|
||||||
buttons = QDialogButtonBox()
|
buttons = QDialogButtonBox()
|
||||||
|
|
||||||
self.btn_update = QPushButton("Aktualizovat hesláře 🔄")
|
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.setToolTip(
|
||||||
|
"Provede kompletní aktualizaci heslářů AMČR. "
|
||||||
|
"Toto bude trvat pár minut."
|
||||||
|
)
|
||||||
self.btn_update.clicked.connect(self.action_update_heslare)
|
self.btn_update.clicked.connect(self.action_update_heslare)
|
||||||
|
|
||||||
buttons.addButton(self.btn_update, QDialogButtonBox.ButtonRole.ActionRole)
|
buttons.addButton(
|
||||||
|
self.btn_update,
|
||||||
|
QDialogButtonBox.ButtonRole.ActionRole
|
||||||
|
)
|
||||||
buttons.addButton(QDialogButtonBox.StandardButton.Ok)
|
buttons.addButton(QDialogButtonBox.StandardButton.Ok)
|
||||||
buttons.addButton(QDialogButtonBox.StandardButton.Cancel)
|
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)
|
||||||
|
|
||||||
self.setLayout(layout)
|
self.setLayout(layout)
|
||||||
|
|
||||||
def setup_picker(self, label_text, cache_key, data_source, extra_btn=None):
|
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
|
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.
|
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 = QHBoxLayout()
|
||||||
row_layout.setContentsMargins(5, 5, 5, 5)
|
row_layout.setContentsMargins(5, 5, 5, 5)
|
||||||
|
|
||||||
# Read-only field displaying the names of selected items
|
# Read-only field displaying the names of selected items
|
||||||
display_field = QLineEdit()
|
display_field = QLineEdit()
|
||||||
display_field.setReadOnly(True)
|
display_field.setReadOnly(True)
|
||||||
display_field.setPlaceholderText("Nic nevybráno (vše)")
|
display_field.setPlaceholderText("Nic nevybráno (vše)")
|
||||||
display_field.setStyleSheet("background-color: #f0f0f0; color: #333;")
|
display_field.setStyleSheet("background-color: #f0f0f0; color: #333;")
|
||||||
|
|
||||||
btn = QPushButton("Vybrat...")
|
btn = QPushButton("Vybrat...")
|
||||||
btn.setFixedWidth(80)
|
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():
|
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:
|
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 the local cache with selected IDs
|
||||||
self.selection_cache[cache_key] = codes
|
self.selection_cache[cache_key] = codes
|
||||||
# Update the UI text field with selected names
|
# Update the display field with the selected item names
|
||||||
if labels:
|
if labels:
|
||||||
display_field.setText(", ".join(labels))
|
display_field.setText(", ".join(labels))
|
||||||
else:
|
else:
|
||||||
display_field.clear()
|
display_field.clear()
|
||||||
|
|
||||||
# Special case: Pre-fill specific accuracy levels by default
|
# Special case: pre-select default PIAN accuracy levels
|
||||||
if cache_key == 'pian_presnost':
|
if cache_key == 'pian_presnost':
|
||||||
display_field.setText("odchylka jednotky metrů, odchylka desítky metrů, odchylka stovky metrů")
|
display_field.setText(
|
||||||
self.selection_cache[cache_key] = ['HES-000861', 'HES-000862', 'HES-000863']
|
"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)
|
btn.clicked.connect(open_dialog)
|
||||||
|
|
||||||
row_layout.addWidget(display_field)
|
row_layout.addWidget(display_field)
|
||||||
row_layout.addWidget(btn)
|
row_layout.addWidget(btn)
|
||||||
|
|
||||||
# Add an optional extra button (e.g., the refresh button for leaders)
|
# Optionally append an extra button (e.g. a refresh button)
|
||||||
if extra_btn:
|
if extra_btn:
|
||||||
row_layout.addWidget(extra_btn)
|
row_layout.addWidget(extra_btn)
|
||||||
|
|
||||||
row_widget.setLayout(row_layout)
|
row_widget.setLayout(row_layout)
|
||||||
return row_widget
|
return row_widget
|
||||||
|
|
||||||
def action_update_heslare(self):
|
def action_update_heslare(self):
|
||||||
# Vytvoření instance tasku
|
# Create the task instance
|
||||||
task = UpdateCodelistsTask("Aktualizace heslářů AMČR")
|
task = UpdateCodelistsTask("Aktualizace heslářů AMČR")
|
||||||
|
|
||||||
# Povolíme tlačítko zpět bez ohledu na výsledek
|
# Re-enable the button regardless of the outcome
|
||||||
task.taskCompleted.connect(lambda: self.btn_update.setEnabled(True))
|
task.taskCompleted.connect(lambda: self.btn_update.setEnabled(True))
|
||||||
task.taskTerminated.connect(lambda: self.btn_update.setEnabled(True))
|
task.taskTerminated.connect(lambda: self.btn_update.setEnabled(True))
|
||||||
|
|
||||||
task.taskCompleted.connect(lambda: QMessageBox.information(self, "Hotovo", "Hesláře byly úspěšně aktualizovány."))
|
task.taskCompleted.connect(lambda: QMessageBox.information(
|
||||||
|
self,
|
||||||
# Ošetření, aby se přesně ukázala případná chyba
|
"Hotovo",
|
||||||
|
"Hesláře byly úspěšně aktualizovány."
|
||||||
|
))
|
||||||
|
|
||||||
|
# Show the exact error if the task fails
|
||||||
def on_error():
|
def on_error():
|
||||||
if task.exception:
|
if task.exception:
|
||||||
# Tohle ti přesně řekne, na čem to teď padá (např. PermissionError)
|
# This will show exactly what went wrong (e.g. PermissionError)
|
||||||
msg = f"Aktualizace selhala z důvodu chyby:\n{str(task.exception)}"
|
msg = (
|
||||||
|
"Aktualizace selhala z důvodu chyby:\n"
|
||||||
|
f"{str(task.exception)}"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
msg = "Aktualizace byla zrušena uživatelem."
|
msg = "Aktualizace byla zrušena uživatelem."
|
||||||
QMessageBox.warning(self, "Chyba / Zrušeno", msg)
|
QMessageBox.warning(self, "Chyba / Zrušeno", msg)
|
||||||
|
|
||||||
task.taskTerminated.connect(on_error)
|
task.taskTerminated.connect(on_error)
|
||||||
|
|
||||||
QgsApplication.taskManager().addTask(task)
|
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"
|
||||||
|
|
||||||
def get_komponenty(self):
|
def get_komponenty(self):
|
||||||
return "true" if self.chk_komponenty.isChecked() else "false"
|
return "true" if self.chk_komponenty.isChecked() else "false"
|
||||||
|
|
||||||
def get_filters(self):
|
def get_filters(self):
|
||||||
"""Compiles the user selections from the cache into API-ready filter parameters."""
|
"""Compiles the user selections from the cache into
|
||||||
|
API-ready filter parameters."""
|
||||||
filters = {}
|
filters = {}
|
||||||
|
|
||||||
if self.selection_cache['kraj']:
|
if self.selection_cache['kraj']:
|
||||||
@@ -323,10 +421,10 @@ class AmcrFilterDialog(QDialog):
|
|||||||
if self.selection_cache['areal']:
|
if self.selection_cache['areal']:
|
||||||
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']:
|
if self.selection_cache['pristupnost']:
|
||||||
filters['pristupnost'] = 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():
|
||||||
filters['posevidence'] = 'true'
|
filters['posevidence'] = 'true'
|
||||||
@@ -346,12 +444,14 @@ class AmcrFilterDialog(QDialog):
|
|||||||
filters['f_jistota'] = self.selection_cache['jistota']
|
filters['f_jistota'] = self.selection_cache['jistota']
|
||||||
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):
|
class LoginDialog(QDialog):
|
||||||
"""
|
"""
|
||||||
Dialog for saving AMČR login credentials securely in the QGIS Authentication Manager.
|
Dialog for saving AMČR login credentials securely in the
|
||||||
|
QGIS Authentication Manager.
|
||||||
|
|
||||||
Credentials are encrypted by the platform's native secret storage
|
Credentials are encrypted by the platform's native secret storage
|
||||||
(DPAPI on Windows, Keychain on macOS, encrypted SQLite on Linux).
|
(DPAPI on Windows, Keychain on macOS, encrypted SQLite on Linux).
|
||||||
@@ -365,10 +465,13 @@ class LoginDialog(QDialog):
|
|||||||
- storeAuthenticationConfig() and loadAuthenticationConfig() both have
|
- storeAuthenticationConfig() and loadAuthenticationConfig() both have
|
||||||
SIP_INOUT on their config parameter, so Python bindings return a tuple
|
SIP_INOUT on their config parameter, so Python bindings return a tuple
|
||||||
(bool, QgsAuthMethodConfig) rather than just bool. Always unpack both.
|
(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"
|
SETTINGS_KEY = "amcr_viewer/auth_config_id"
|
||||||
CONFIG_NAME = "AMČR Viewer"
|
CONFIG_NAME = "AMČR Viewer"
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
@@ -379,17 +482,29 @@ class LoginDialog(QDialog):
|
|||||||
|
|
||||||
# Check whether a config ID is already stored from a previous session.
|
# Check whether a config ID is already stored from a previous session.
|
||||||
# We attempt a lightweight load (full=False) to confirm it is readable,
|
# We attempt a lightweight load (full=False) to confirm it is readable,
|
||||||
# since hasConfigId() may return False even for valid configs (cache lag).
|
# 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, "")
|
existing_id = QSettings().value(self.SETTINGS_KEY, "")
|
||||||
self._has_saved = bool(existing_id) and bool(self._load_username_from_config(existing_id))
|
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:
|
if self._has_saved:
|
||||||
info = QLabel("✔ Přihlašovací údaje jsou bezpečně uloženy ve správci autentizace QGIS.\n"
|
info = QLabel(
|
||||||
"Vyplňte pole níže pouze pokud je chcete změnit.")
|
"✔ 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;")
|
info.setStyleSheet("color: green; font-style: italic;")
|
||||||
else:
|
else:
|
||||||
info = QLabel("Zadejte přihlašovací údaje k Digitálnímu archivu AMČR.\n"
|
info = QLabel(
|
||||||
"Budou zašifrovaně uloženy ve správci autentizace QGIS.")
|
"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)
|
info.setWordWrap(True)
|
||||||
layout.addWidget(info)
|
layout.addWidget(info)
|
||||||
layout.addSpacing(8)
|
layout.addSpacing(8)
|
||||||
@@ -406,7 +521,9 @@ class LoginDialog(QDialog):
|
|||||||
self.txt_pass = QLineEdit()
|
self.txt_pass = QLineEdit()
|
||||||
self.txt_pass.setEchoMode(QLineEdit.EchoMode.Password)
|
self.txt_pass.setEchoMode(QLineEdit.EchoMode.Password)
|
||||||
self.txt_pass.setPlaceholderText(
|
self.txt_pass.setPlaceholderText(
|
||||||
"ponechte prázdné pro zachování stávajícího hesla" if self._has_saved else "heslo"
|
"ponechte prázdné pro zachování stávajícího hesla"
|
||||||
|
if self._has_saved
|
||||||
|
else "heslo"
|
||||||
)
|
)
|
||||||
form.addRow("Heslo:", self.txt_pass)
|
form.addRow("Heslo:", self.txt_pass)
|
||||||
|
|
||||||
@@ -422,7 +539,8 @@ class LoginDialog(QDialog):
|
|||||||
layout.addStretch(1)
|
layout.addStretch(1)
|
||||||
|
|
||||||
buttons = QDialogButtonBox(
|
buttons = QDialogButtonBox(
|
||||||
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
QDialogButtonBox.StandardButton.Ok
|
||||||
|
| QDialogButtonBox.StandardButton.Cancel
|
||||||
)
|
)
|
||||||
buttons.accepted.connect(self._save_and_accept)
|
buttons.accepted.connect(self._save_and_accept)
|
||||||
buttons.rejected.connect(self.reject)
|
buttons.rejected.connect(self.reject)
|
||||||
@@ -438,7 +556,8 @@ class LoginDialog(QDialog):
|
|||||||
def _load_config(config_id: str, full: bool = False):
|
def _load_config(config_id: str, full: bool = False):
|
||||||
"""
|
"""
|
||||||
Attempt to load a QgsAuthMethodConfig by ID.
|
Attempt to load a QgsAuthMethodConfig by ID.
|
||||||
Returns (ok, cfg). Never raises; returns (False, empty cfg) on any error.
|
Returns (ok, cfg). Never raises; returns (False, empty cfg)
|
||||||
|
on any error.
|
||||||
full=True decrypts and includes the password.
|
full=True decrypts and includes the password.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
@@ -453,8 +572,10 @@ class LoginDialog(QDialog):
|
|||||||
return False, QgsAuthMethodConfig()
|
return False, QgsAuthMethodConfig()
|
||||||
|
|
||||||
def _load_username_from_config(self, config_id: str) -> str:
|
def _load_username_from_config(self, config_id: str) -> str:
|
||||||
"""Load just the username from a stored config (no password decryption)."""
|
"""Load the username from a stored config.
|
||||||
ok, cfg = self._load_config(config_id, full=False)
|
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 ""
|
return cfg.config("username", "") if ok else ""
|
||||||
|
|
||||||
def _ensure_master_password(self) -> bool:
|
def _ensure_master_password(self) -> bool:
|
||||||
@@ -469,11 +590,13 @@ class LoginDialog(QDialog):
|
|||||||
QMessageBox.critical(
|
QMessageBox.critical(
|
||||||
self, "Správce autentizace nedostupný",
|
self, "Správce autentizace nedostupný",
|
||||||
"Správce autentizace QGIS je zakázán nebo poškozený.\n"
|
"Správce autentizace QGIS je zakázán nebo poškozený.\n"
|
||||||
"Zkuste obnovit databázi: Nastavení → Možnosti → Autentizace → Pomůcky."
|
"Zkuste obnovit databázi: "
|
||||||
|
"Nastavení → Možnosti → Autentizace → Pomůcky."
|
||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# setMasterPassword(True) shows the QGIS master password dialog if needed
|
# setMasterPassword(True) shows the QGIS
|
||||||
|
# master password dialog if needed
|
||||||
if not auth_mgr.setMasterPassword(True):
|
if not auth_mgr.setMasterPassword(True):
|
||||||
return False # User cancelled the master password dialog
|
return False # User cancelled the master password dialog
|
||||||
|
|
||||||
@@ -488,7 +611,11 @@ class LoginDialog(QDialog):
|
|||||||
password = self.txt_pass.text()
|
password = self.txt_pass.text()
|
||||||
|
|
||||||
if not username:
|
if not username:
|
||||||
QMessageBox.warning(self, "Chybí údaje", "Vyplňte prosím e-mailovou adresu.")
|
QMessageBox.warning(
|
||||||
|
self,
|
||||||
|
"Chybí údaje",
|
||||||
|
"Vyplňte prosím e-mailovou adresu."
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
existing_id = QSettings().value(self.SETTINGS_KEY, "")
|
existing_id = QSettings().value(self.SETTINGS_KEY, "")
|
||||||
@@ -521,13 +648,19 @@ class LoginDialog(QDialog):
|
|||||||
|
|
||||||
settings = QSettings()
|
settings = QSettings()
|
||||||
|
|
||||||
# Try to update an existing config first; fall back to creating a new one.
|
# Try to update an existing config first;
|
||||||
# We skip hasConfigId() as it may return False despite the config existing
|
# 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).
|
# (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)
|
ok_load, existing_cfg = (
|
||||||
|
self._load_config(existing_id, full=False)
|
||||||
|
if existing_id
|
||||||
|
else (False, None)
|
||||||
|
)
|
||||||
if ok_load:
|
if ok_load:
|
||||||
cfg.setId(existing_id)
|
cfg.setId(existing_id)
|
||||||
ok, cfg = auth_mgr.updateAuthenticationConfig(cfg)
|
ok = auth_mgr.updateAuthenticationConfig(cfg)
|
||||||
else:
|
else:
|
||||||
ok, cfg = auth_mgr.storeAuthenticationConfig(cfg)
|
ok, cfg = auth_mgr.storeAuthenticationConfig(cfg)
|
||||||
|
|
||||||
@@ -536,7 +669,8 @@ class LoginDialog(QDialog):
|
|||||||
if not ok or not config_id:
|
if not ok or not config_id:
|
||||||
QMessageBox.critical(
|
QMessageBox.critical(
|
||||||
self, "Chyba uložení",
|
self, "Chyba uložení",
|
||||||
"Přihlašovací údaje se nepodařilo uložit do správce autentizace QGIS.\n"
|
"Přihlašovací údaje se nepodařilo "
|
||||||
|
"uložit do správce autentizace QGIS.\n"
|
||||||
"Zkuste restartovat QGIS a přihlásit se znovu."
|
"Zkuste restartovat QGIS a přihlásit se znovu."
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
@@ -550,7 +684,11 @@ class LoginDialog(QDialog):
|
|||||||
if existing_id:
|
if existing_id:
|
||||||
QgsApplication.authManager().removeAuthenticationConfig(existing_id)
|
QgsApplication.authManager().removeAuthenticationConfig(existing_id)
|
||||||
settings.remove(self.SETTINGS_KEY)
|
settings.remove(self.SETTINGS_KEY)
|
||||||
QMessageBox.information(self, "Hotovo", "Uložené přihlašovací údaje byly odebrány.")
|
QMessageBox.information(
|
||||||
|
self,
|
||||||
|
"Hotovo",
|
||||||
|
"Uložené přihlašovací údaje byly odebrány."
|
||||||
|
)
|
||||||
self.reject()
|
self.reject()
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@@ -565,8 +703,9 @@ class LoginDialog(QDialog):
|
|||||||
|
|
||||||
Note: hasConfigId() is intentionally skipped here – it checks an
|
Note: hasConfigId() is intentionally skipped here – it checks an
|
||||||
in-memory cache that may lag behind the actual database contents,
|
in-memory cache that may lag behind the actual database contents,
|
||||||
causing false negatives (see class docstring). loadAuthenticationConfig()
|
causing false negatives (see class docstring).
|
||||||
is called directly and its return value is used as the authoritative result.
|
loadAuthenticationConfig() is called directly and its return value is
|
||||||
|
used as the authoritative result.
|
||||||
"""
|
"""
|
||||||
settings = QSettings()
|
settings = QSettings()
|
||||||
config_id = settings.value(LoginDialog.SETTINGS_KEY, "")
|
config_id = settings.value(LoginDialog.SETTINGS_KEY, "")
|
||||||
@@ -578,4 +717,4 @@ class LoginDialog(QDialog):
|
|||||||
if not ok:
|
if not ok:
|
||||||
return "", ""
|
return "", ""
|
||||||
|
|
||||||
return cfg.config("username", ""), cfg.config("password", "") # nosec B106
|
return cfg.config("username", ""), cfg.config("password", "") # nosec B106
|
||||||
|
|||||||
+370
-149
@@ -1,7 +1,8 @@
|
|||||||
# -*- 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,
|
||||||
QgsWkbTypes, Qgis, QgsApplication, QgsAuthMethodConfig, QgsMessageLog)
|
QgsCoordinateTransform, QgsWkbTypes, Qgis,
|
||||||
|
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,25 +13,33 @@ 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)
|
# Session with authentication cookie after login;
|
||||||
|
# None = not logged in (anonymous access)
|
||||||
AMCR_SESSION: requests.Session | None = None
|
AMCR_SESSION: requests.Session | None = None
|
||||||
|
|
||||||
|
|
||||||
def _log(msg: str, level=Qgis.MessageLevel.Info):
|
def _log(msg: str, level=Qgis.MessageLevel.Info):
|
||||||
"""Shortcut: zapíše zprávu do QGIS logu (panel Zprávy → záložka AMČR)."""
|
"""
|
||||||
|
Shortcut: writes a message to the QGIS log
|
||||||
|
(Messages panel → AMČR tab).
|
||||||
|
"""
|
||||||
QgsMessageLog.logMessage(msg, "AMČR login", level)
|
QgsMessageLog.logMessage(msg, "AMČR login", level)
|
||||||
|
|
||||||
|
|
||||||
def login_to_api(username: str, password: str):
|
def login_to_api(username: str, password: str):
|
||||||
"""
|
"""
|
||||||
Přihlásí se do Digiarchiv API pomocí username a hesla.
|
Logs in to the Digiarchiv API using a username and password.
|
||||||
Vrátí requests.Session s nastavenou session cookie, nebo None při chybě.
|
Returns a requests.Session with the session cookie set, or None on error.
|
||||||
"""
|
"""
|
||||||
login_url = "https://digiarchiv.aiscr.cz/api/user/login"
|
login_url = "https://digiarchiv.aiscr.cz/api/user/login"
|
||||||
|
|
||||||
_log(f"Přihlašuji uživatele: '{username}'")
|
_log(f"Přihlašuji uživatele: '{username}'")
|
||||||
|
|
||||||
if not username or not password:
|
if not username or not password:
|
||||||
_log("CHYBA: username nebo heslo je prázdné.", Qgis.MessageLevel.Critical)
|
_log(
|
||||||
|
"CHYBA: username nebo heslo je prázdné.",
|
||||||
|
Qgis.MessageLevel.Critical
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
session = requests.Session()
|
session = requests.Session()
|
||||||
@@ -42,14 +51,22 @@ def login_to_api(username: str, password: str):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
_log(f"Odesílám POST na {login_url} ...")
|
_log(f"Odesílám POST na {login_url} ...")
|
||||||
response = session.post(login_url, json={"user": username, "pwd": password}, timeout=10)
|
response = session.post(
|
||||||
|
login_url,
|
||||||
|
json={"user": username, "pwd": password},
|
||||||
|
timeout=10
|
||||||
|
)
|
||||||
_log(f"HTTP status: {response.status_code}")
|
_log(f"HTTP status: {response.status_code}")
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
|
||||||
# API vrací chyby se status kódem 200 – je nutné zkontrolovat tělo odpovědi
|
# The API returns errors with status code 200 –
|
||||||
|
# the response body must be checked
|
||||||
body = response.json()
|
body = response.json()
|
||||||
if "error" in body:
|
if "error" in body:
|
||||||
_log(f"CHYBA přihlášení (API): {body['error']}", Qgis.MessageLevel.Critical)
|
_log(
|
||||||
|
f"CHYBA přihlášení (API): {body['error']}",
|
||||||
|
Qgis.MessageLevel.Critical
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
_log("Přihlášení proběhlo úspěšně.")
|
_log("Přihlášení proběhlo úspěšně.")
|
||||||
@@ -59,23 +76,25 @@ def login_to_api(username: str, password: str):
|
|||||||
|
|
||||||
except requests.exceptions.HTTPError as e:
|
except requests.exceptions.HTTPError as e:
|
||||||
_log(f"CHYBA HTTP {e.response.status_code if e.response else '?'}: "
|
_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)
|
f"{e.response.text[:300] if e.response else 'žádná odpověď'}",
|
||||||
|
Qgis.MessageLevel.Critical)
|
||||||
return None
|
return None
|
||||||
except requests.exceptions.RequestException as e:
|
except requests.exceptions.RequestException as e:
|
||||||
_log(f"CHYBA sítě: {e}", Qgis.MessageLevel.Critical)
|
_log(f"CHYBA sítě: {e}", Qgis.MessageLevel.Critical)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _get_session() -> requests.Session | None:
|
def _get_session() -> requests.Session | None:
|
||||||
"""
|
"""
|
||||||
Vrátí aktivní session. Pokud žádná není (restart QGIS), pokusí se
|
Returns the active session. If none exists (e.g. after a QGIS restart),
|
||||||
automaticky přihlásit pomocí uložených přihlašovacích údajů.
|
attempts automatic login using stored credentials.
|
||||||
Vrátí None pokud přihlašovací údaje nejsou uloženy.
|
Returns None if no credentials are stored.
|
||||||
"""
|
"""
|
||||||
global AMCR_SESSION
|
global AMCR_SESSION
|
||||||
if AMCR_SESSION is not None:
|
if AMCR_SESSION is not None:
|
||||||
return AMCR_SESSION
|
return AMCR_SESSION
|
||||||
|
|
||||||
# Zkusit auto-login pomocí uložených údajů
|
# Attempt auto-login using stored credentials
|
||||||
from .amcr_dialog import LoginDialog
|
from .amcr_dialog import LoginDialog
|
||||||
username, password = LoginDialog.get_credentials()
|
username, password = LoginDialog.get_credentials()
|
||||||
if username and password:
|
if username and password:
|
||||||
@@ -87,19 +106,24 @@ def _get_session() -> requests.Session | None:
|
|||||||
|
|
||||||
def _api_get(url, params, timeout=30) -> requests.Response:
|
def _api_get(url, params, timeout=30) -> requests.Response:
|
||||||
"""
|
"""
|
||||||
Provede GET request. Pokud API signalizuje vypršení přihlášení,
|
Performs a GET request. If the API signals an expired login,
|
||||||
provede jedno opakované přihlášení a zkusí znovu.
|
re-authenticates once and retries.
|
||||||
"""
|
"""
|
||||||
global AMCR_SESSION
|
global AMCR_SESSION
|
||||||
|
|
||||||
def _is_auth_error(resp: requests.Response) -> bool:
|
def _is_auth_error(resp: requests.Response) -> bool:
|
||||||
"""API vrací auth chyby se status 200 – je nutné zkontrolovat tělo."""
|
"""The API returns auth errors with status 200 –
|
||||||
|
the body must be checked."""
|
||||||
if resp.status_code == 401:
|
if resp.status_code == 401:
|
||||||
return True
|
return True
|
||||||
try:
|
try:
|
||||||
body = resp.json()
|
body = resp.json()
|
||||||
err = str(body.get("error", "")).lower()
|
err = str(body.get("error", "")).lower()
|
||||||
return "unauthorized" in err or "not logged" in err or "session" in err
|
return (
|
||||||
|
"unauthorized" in err
|
||||||
|
or "not logged" in err
|
||||||
|
or "session" in err
|
||||||
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -107,8 +131,9 @@ def _api_get(url, params, timeout=30) -> requests.Response:
|
|||||||
resp = (session or requests).get(url, params=params, timeout=timeout)
|
resp = (session or requests).get(url, params=params, timeout=timeout)
|
||||||
|
|
||||||
if _is_auth_error(resp):
|
if _is_auth_error(resp):
|
||||||
_log("Session vypršela během stahování – obnovuji přihlášení...", Qgis.MessageLevel.Warning)
|
_log("Session vypršela během stahování – obnovuji přihlášení...",
|
||||||
AMCR_SESSION = None # Zrušit starou session
|
Qgis.MessageLevel.Warning)
|
||||||
|
AMCR_SESSION = None # Invalidate the old session
|
||||||
from .amcr_dialog import LoginDialog
|
from .amcr_dialog import LoginDialog
|
||||||
username, password = LoginDialog.get_credentials()
|
username, password = LoginDialog.get_credentials()
|
||||||
if username and password:
|
if username and password:
|
||||||
@@ -116,18 +141,24 @@ def _api_get(url, params, timeout=30) -> requests.Response:
|
|||||||
if AMCR_SESSION:
|
if AMCR_SESSION:
|
||||||
resp = AMCR_SESSION.get(url, params=params, timeout=timeout)
|
resp = AMCR_SESSION.get(url, params=params, timeout=timeout)
|
||||||
else:
|
else:
|
||||||
_log("Opakované přihlášení selhalo.", Qgis.MessageLevel.Critical)
|
_log("Opakované přihlášení selhalo.",
|
||||||
|
Qgis.MessageLevel.Critical)
|
||||||
else:
|
else:
|
||||||
_log("Přihlašovací údaje nejsou uloženy – pokračuji anonymně.", Qgis.MessageLevel.Warning)
|
_log("Přihlašovací údaje nejsou uloženy – pokračuji anonymně.",
|
||||||
|
Qgis.MessageLevel.Warning)
|
||||||
|
|
||||||
return resp
|
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 Digiarchive API.
|
||||||
|
"""
|
||||||
global TRANSLATIONS
|
global TRANSLATIONS
|
||||||
if TRANSLATIONS:
|
if TRANSLATIONS:
|
||||||
return
|
return
|
||||||
|
|
||||||
url = "https://digiarchiv.aiscr.cz/api/assets/i18n/cs.json"
|
url = "https://digiarchiv.aiscr.cz/api/assets/i18n/cs.json"
|
||||||
try:
|
try:
|
||||||
r = requests.get(url, timeout=10)
|
r = requests.get(url, timeout=10)
|
||||||
@@ -136,13 +167,31 @@ def load_translations():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error downloading vocabulary: {e}")
|
print(f"Error downloading vocabulary: {e}")
|
||||||
|
|
||||||
|
|
||||||
def tr_code(code):
|
def tr_code(code):
|
||||||
"""Translates a technical code into a human-readable string using the global cache."""
|
"""
|
||||||
if not code:
|
Translates a technical code into a human-readable string
|
||||||
|
using the global cache.
|
||||||
|
"""
|
||||||
|
if not code:
|
||||||
return ""
|
return ""
|
||||||
return TRANSLATIONS.get(code, code)
|
return TRANSLATIONS.get(code, code)
|
||||||
|
|
||||||
def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false"):
|
|
||||||
|
def komp_projde_filtrem(komp, filter_areal, filter_datace, filters):
|
||||||
|
areal_id = komp.get('komponenta_areal', {}).get('id', "")
|
||||||
|
if filter_areal and areal_id not in filters.get('f_areal', []):
|
||||||
|
return False
|
||||||
|
|
||||||
|
obdobi_id = komp.get('komponenta_obdobi', {}).get('id', "")
|
||||||
|
if filter_datace and obdobi_id not in filters.get('f_obdobi', []):
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def load_amcr_data(canvas, bb, filters=None,
|
||||||
|
typ_dat="akce", komponenty="false"):
|
||||||
"""
|
"""
|
||||||
Main processing function:
|
Main processing function:
|
||||||
1. Determines search area (Bounding Box)
|
1. Determines search area (Bounding Box)
|
||||||
@@ -152,26 +201,35 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
|
|||||||
load_translations()
|
load_translations()
|
||||||
|
|
||||||
# --- 1. COORDINATE TRANSFORMATION ---
|
# --- 1. COORDINATE TRANSFORMATION ---
|
||||||
# Get current map extent and transform it from project CRS (usually S-JTSK) to WGS-84 for the API
|
# Get current map extent and transform it
|
||||||
|
# from project CRS (usually S-JTSK) to WGS-84 for the API
|
||||||
extent = canvas.extent()
|
extent = canvas.extent()
|
||||||
crs_src = canvas.mapSettings().destinationCrs()
|
crs_src = canvas.mapSettings().destinationCrs()
|
||||||
crs_dest = QgsCoordinateReferenceSystem("EPSG:4326")
|
crs_dest = QgsCoordinateReferenceSystem("EPSG:4326")
|
||||||
xform = QgsCoordinateTransform(crs_src, crs_dest, QgsProject.instance())
|
xform = QgsCoordinateTransform(crs_src, crs_dest, QgsProject.instance())
|
||||||
extent_wgs = xform.transformBoundingBox(extent)
|
extent_wgs = xform.transformBoundingBox(extent)
|
||||||
|
|
||||||
# Format the bounding box string as required by the API: minLat,minLon,maxLat,maxLon
|
# Format the bounding box string as required by the API:
|
||||||
bbox_str = f"{extent_wgs.yMinimum()},{extent_wgs.xMinimum()},{extent_wgs.yMaximum()},{extent_wgs.xMaximum()}"
|
# minLat,minLon,maxLat,maxLon
|
||||||
|
bbox_str = (
|
||||||
|
f"{extent_wgs.yMinimum()},{extent_wgs.xMinimum()},"
|
||||||
|
f"{extent_wgs.yMaximum()},{extent_wgs.xMaximum()}"
|
||||||
|
)
|
||||||
|
|
||||||
url = "https://digiarchiv.aiscr.cz/api/search/query"
|
url = "https://digiarchiv.aiscr.cz/api/search/query"
|
||||||
|
|
||||||
iface.messageBar().pushMessage("AMCR", "Hledám záznamy...", level=Qgis.MessageLevel.Info)
|
iface.messageBar().pushMessage(
|
||||||
|
"AMCR",
|
||||||
|
"Hledám záznamy...",
|
||||||
|
level=Qgis.MessageLevel.Info
|
||||||
|
)
|
||||||
QApplication.setOverrideCursor(QCursor(Qt.CursorShape.WaitCursor))
|
QApplication.setOverrideCursor(QCursor(Qt.CursorShape.WaitCursor))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# A) METADATA FETCHING (Fieldwork/Site)
|
# A) METADATA FETCHING (Fieldwork/Site)
|
||||||
# ==========================================
|
# ==========================================
|
||||||
|
|
||||||
base_params = {
|
base_params = {
|
||||||
"mapa": "true",
|
"mapa": "true",
|
||||||
"sort": "ident_cely asc",
|
"sort": "ident_cely asc",
|
||||||
@@ -179,10 +237,11 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Restrict search to map window if requested
|
# Restrict search to map window if requested
|
||||||
if bb == "true":
|
if bb == "true":
|
||||||
base_params["loc_rpt"] = bbox_str
|
base_params["loc_rpt"] = bbox_str
|
||||||
|
|
||||||
# Apply multi-select filters from the dialog using the ':or' syntax required by the API
|
# Apply multi-select filters from the dialog using
|
||||||
|
# the ':or' syntax required by the API
|
||||||
if filters:
|
if filters:
|
||||||
for key, value in filters.items():
|
for key, value in filters.items():
|
||||||
if not value:
|
if not value:
|
||||||
@@ -193,17 +252,24 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
|
|||||||
base_params[key] = str(value).strip()
|
base_params[key] = str(value).strip()
|
||||||
|
|
||||||
docs = []
|
docs = []
|
||||||
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
|
||||||
|
|
||||||
|
|
||||||
seen_ids = set()
|
seen_ids = set()
|
||||||
target_pian_ids_count = 0
|
target_pian_ids_count = 0
|
||||||
|
|
||||||
# Check if we should skip negative results based on filter
|
# Check if we should skip negative results based on filter
|
||||||
skip_negativni = filters.get('posevidence') == 'true' if filters else False
|
skip_negativni = (
|
||||||
|
filters.get('posevidence') == 'true'
|
||||||
|
if filters
|
||||||
|
else False
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check whether we should filter results based on component filters
|
||||||
|
filter_areal = "f_areal" in filters if filters else False
|
||||||
|
filter_datace = "f_obdobi" in filters if filters else False
|
||||||
|
|
||||||
# --- API PAGINATION LOOP ---
|
# --- API PAGINATION LOOP ---
|
||||||
while True:
|
while True:
|
||||||
base_params['rows'] = BATCH_DOCS
|
base_params['rows'] = BATCH_DOCS
|
||||||
@@ -211,17 +277,17 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
|
|||||||
base_params['page'] = current_page
|
base_params['page'] = current_page
|
||||||
elif 'page' in base_params:
|
elif 'page' in base_params:
|
||||||
del base_params['page']
|
del base_params['page']
|
||||||
|
|
||||||
try:
|
try:
|
||||||
resp_docs = _api_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', [])
|
||||||
num_found = data.get('numFound', 0)
|
num_found = data.get('numFound', 0)
|
||||||
|
|
||||||
if not batch_docs:
|
if not batch_docs:
|
||||||
break
|
break
|
||||||
|
|
||||||
# Filter out duplicates and append to main list
|
# Filter out duplicates and append to main list
|
||||||
new_docs = []
|
new_docs = []
|
||||||
for d in batch_docs:
|
for d in batch_docs:
|
||||||
@@ -229,44 +295,56 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
|
|||||||
if ident and ident not in seen_ids:
|
if ident and ident not in seen_ids:
|
||||||
seen_ids.add(ident)
|
seen_ids.add(ident)
|
||||||
new_docs.append(d)
|
new_docs.append(d)
|
||||||
|
|
||||||
docs.extend(new_docs)
|
docs.extend(new_docs)
|
||||||
print(f"Strana {current_page} stažena. Celkem záznamů: {len(docs)} / {num_found}")
|
print(
|
||||||
|
f"Strana {current_page} stažena. "
|
||||||
|
f"Celkem záznamů: {len(docs)} / {num_found}"
|
||||||
|
)
|
||||||
|
|
||||||
if len(docs) >= num_found:
|
if len(docs) >= num_found:
|
||||||
break
|
break
|
||||||
if len(docs) >= MAX_LIMIT:
|
if len(docs) >= MAX_LIMIT:
|
||||||
iface.messageBar().pushMessage("AMCR", f"Limit {MAX_LIMIT} záznamů dosažen.", level=Qgis.MessageLevel.Warning)
|
iface.messageBar().pushMessage(
|
||||||
|
"AMCR",
|
||||||
|
f"Limit {MAX_LIMIT} záznamů dosažen.",
|
||||||
|
level=Qgis.MessageLevel.Warning
|
||||||
|
)
|
||||||
break
|
break
|
||||||
|
|
||||||
current_page += 1
|
current_page += 1
|
||||||
QApplication.processEvents() # Keep UI responsive
|
QApplication.processEvents() # Keep UI responsive
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Chyba při stránkování na straně {current_page}: {e}")
|
print(f"Chyba při stránkování na straně {current_page}: {e}")
|
||||||
break
|
break
|
||||||
|
|
||||||
if not docs:
|
if not docs:
|
||||||
iface.messageBar().pushMessage("AMCR", "Žádné záznamy nenalezeny.", level=Qgis.MessageLevel.Warning)
|
iface.messageBar().pushMessage(
|
||||||
return
|
"AMCR",
|
||||||
|
"Žádné záznamy nenalezeny.",
|
||||||
|
level=Qgis.MessageLevel.Warning
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# B) ATTRIBUTE PARSING
|
# B) ATTRIBUTE PARSING
|
||||||
# ==========================================
|
# ==========================================
|
||||||
|
|
||||||
# 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 = {}
|
||||||
target_pian_ids = set()
|
target_pian_ids = set()
|
||||||
actions_with_geom = 0
|
actions_with_geom = 0
|
||||||
|
|
||||||
# Helper function for safe single-value extraction
|
# Helper: safely extract a single value
|
||||||
def g(doc, key, default=""):
|
def g(doc, key, default=""):
|
||||||
val = doc.get(key)
|
val = doc.get(key)
|
||||||
if isinstance(val, list):
|
if isinstance(val, list):
|
||||||
return str(val[0]) if val else default
|
return str(val[0]) if val else default
|
||||||
return str(val) if val is not None else default
|
return str(val) if val is not None else default
|
||||||
|
|
||||||
# Helper function for safe list-value extraction and joining
|
# Helper: safely extract and join a list of values
|
||||||
def g_list(doc, key, translate=False):
|
def g_list(doc, key, translate=False):
|
||||||
val = doc.get(key, [])
|
val = doc.get(key, [])
|
||||||
if not isinstance(val, list):
|
if not isinstance(val, list):
|
||||||
@@ -282,16 +360,23 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
actions_with_geom += 1
|
actions_with_geom += 1
|
||||||
|
|
||||||
# Extract protected data (fields not available in public Solr index)
|
# Extract protected fields
|
||||||
az_chranene = doc.get('az_chranene_udaje', {})
|
az_chranene = doc.get('az_chranene_udaje', {})
|
||||||
chranene = doc.get('akce_chranene_udaje') or doc.get('lokalita_chranene_udaje') or {}
|
chranene = (
|
||||||
|
doc.get('akce_chranene_udaje')
|
||||||
# Format additional cadastral areas from dictionaries
|
or doc.get('lokalita_chranene_udaje')
|
||||||
|
or {}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Format additional cadastral areas from nested dicts
|
||||||
dalsi_kat = az_chranene.get('dalsi_katastr', [])
|
dalsi_kat = az_chranene.get('dalsi_katastr', [])
|
||||||
dalsi_kat_str = ""
|
dalsi_kat_str = ""
|
||||||
if isinstance(dalsi_kat, list):
|
if isinstance(dalsi_kat, list):
|
||||||
items = [x.get('value', '') if isinstance(x, dict) else str(x) for x in dalsi_kat]
|
items = [
|
||||||
|
x.get('value', '') if isinstance(x, dict) else str(x)
|
||||||
|
for x in dalsi_kat
|
||||||
|
]
|
||||||
dalsi_kat_str = ", ".join([i for i in items if i])
|
dalsi_kat_str = ", ".join([i for i in items if i])
|
||||||
|
|
||||||
lokalizace = chranene.get('lokalizace_okolnosti', "")
|
lokalizace = chranene.get('lokalizace_okolnosti', "")
|
||||||
@@ -303,7 +388,7 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
|
|||||||
"ident_cely": doc.get('ident_cely', ''),
|
"ident_cely": doc.get('ident_cely', ''),
|
||||||
"az_okres": g(doc, 'az_okres'),
|
"az_okres": g(doc, 'az_okres'),
|
||||||
"katastr": g_list(doc, 'katastr'),
|
"katastr": g_list(doc, 'katastr'),
|
||||||
"dalsi_katastr": dalsi_kat_str,
|
"dalsi_katastr": dalsi_kat_str,
|
||||||
"pristupnost": g(doc, 'pristupnost'),
|
"pristupnost": g(doc, 'pristupnost'),
|
||||||
"loc": g_list(doc, 'loc')
|
"loc": g_list(doc, 'loc')
|
||||||
}
|
}
|
||||||
@@ -311,45 +396,102 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
|
|||||||
# Add entity-specific metadata
|
# Add entity-specific metadata
|
||||||
if typ_dat == "akce":
|
if typ_dat == "akce":
|
||||||
meta.update({
|
meta.update({
|
||||||
"akce_hlavni_vedouci": g(doc, 'akce_hlavni_vedouci'),
|
"akce_hlavni_vedouci": g(
|
||||||
"akce_organizace": tr_code(g(doc, 'akce_organizace')),
|
doc,
|
||||||
"akce_specifikace_data": tr_code(g(doc, 'akce_specifikace_data')),
|
'akce_hlavni_vedouci'
|
||||||
"akce_datum_zahajeni": g(doc, 'akce_datum_zahajeni'),
|
),
|
||||||
"akce_datum_ukonceni": g(doc, 'akce_datum_ukonceni'),
|
"akce_organizace": tr_code(g(
|
||||||
"akce_hlavni_typ": tr_code(g(doc, 'akce_hlavni_typ')),
|
doc,
|
||||||
"akce_vedlejsi_typ": g_list(doc, 'akce_vedlejsi_typ', translate=True),
|
'akce_organizace'
|
||||||
"lokalizace_okolnosti": str(lokalizace) if lokalizace else "",
|
)),
|
||||||
"akce_je_nz": "Ano" if doc.get('akce_je_nz') is True else "Ne",
|
"akce_specifikace_data": tr_code(g(
|
||||||
|
doc,
|
||||||
|
'akce_specifikace_data'
|
||||||
|
)),
|
||||||
|
"akce_datum_zahajeni": g(
|
||||||
|
doc,
|
||||||
|
'akce_datum_zahajeni'
|
||||||
|
),
|
||||||
|
"akce_datum_ukonceni": g(
|
||||||
|
doc,
|
||||||
|
'akce_datum_ukonceni'
|
||||||
|
),
|
||||||
|
"akce_hlavni_typ": tr_code(g(
|
||||||
|
doc,
|
||||||
|
'akce_hlavni_typ'
|
||||||
|
)),
|
||||||
|
"akce_vedlejsi_typ": g_list(
|
||||||
|
doc,
|
||||||
|
'akce_vedlejsi_typ',
|
||||||
|
translate=True
|
||||||
|
),
|
||||||
|
"lokalizace_okolnosti": (
|
||||||
|
str(lokalizace)
|
||||||
|
if lokalizace
|
||||||
|
else ""
|
||||||
|
),
|
||||||
|
"akce_je_nz": (
|
||||||
|
"Ano"
|
||||||
|
if doc.get('akce_je_nz') is True
|
||||||
|
else "Ne"
|
||||||
|
),
|
||||||
})
|
})
|
||||||
|
|
||||||
elif typ_dat == "lokalita":
|
elif typ_dat == "lokalita":
|
||||||
meta.update({
|
meta.update({
|
||||||
"lokalita_nazev": lokalita_nazev,
|
"lokalita_nazev": lokalita_nazev,
|
||||||
"lokalita_popis": lokalita_popis,
|
"lokalita_popis": lokalita_popis,
|
||||||
"lokalita_zachovalost": tr_code(g(doc, 'lokalita_zachovalost')),
|
"lokalita_zachovalost": tr_code(g(
|
||||||
"lokalita_druh": tr_code(g(doc, 'lokalita_druh')),
|
doc,
|
||||||
"lokalita_typ": tr_code(g(doc, 'lokalita_typ_lokality')),
|
'lokalita_zachovalost'
|
||||||
|
)),
|
||||||
|
"lokalita_druh": tr_code(g(
|
||||||
|
doc,
|
||||||
|
'lokalita_druh'
|
||||||
|
)),
|
||||||
|
"lokalita_typ": tr_code(g(
|
||||||
|
doc,
|
||||||
|
'lokalita_typ_lokality'
|
||||||
|
)),
|
||||||
})
|
})
|
||||||
|
|
||||||
# Documentation units (DJ) within the record
|
# Documentation units (DJ) within the record
|
||||||
djs = doc.get('az_dokumentacni_jednotka', [])
|
djs = doc.get('az_dokumentacni_jednotka', [])
|
||||||
|
|
||||||
for dj in djs:
|
for dj in djs:
|
||||||
# Filter out negative evidence units if requested
|
# Skip negative evidence units if requested
|
||||||
if skip_negativni and dj.get('dj_negativni_jednotka') is True:
|
if skip_negativni and dj.get('dj_negativni_jednotka') is True:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
komps = dj.get('dj_komponenta', [])
|
||||||
|
|
||||||
|
if filter_areal or filter_datace:
|
||||||
|
if not komps:
|
||||||
|
continue
|
||||||
|
if not any(
|
||||||
|
komp_projde_filtrem(
|
||||||
|
komp, filter_areal,
|
||||||
|
filter_datace, filters
|
||||||
|
)
|
||||||
|
for komp in komps
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
|
||||||
dj_id = dj.get('ident_cely')
|
dj_id = dj.get('ident_cely')
|
||||||
dj_typ = dj.get('dj_typ')
|
dj_typ = dj.get('dj_typ')
|
||||||
|
|
||||||
# Merge general meta with documentation unit specific data
|
# Merge shared metadata with documentation unit-specific fields
|
||||||
dj_meta = {
|
dj_meta = {
|
||||||
**meta,
|
**meta,
|
||||||
'dj_id': dj_id,
|
'dj_id': dj_id,
|
||||||
'dj_typ_value': dj_typ.get('value') if dj_typ else "",
|
'dj_typ_value': dj_typ.get('value') if dj_typ else "",
|
||||||
'dj_negativni': "Negativní" if dj.get('dj_negativni_jednotka') is True else "Pozitivní"
|
'dj_negativni': (
|
||||||
|
"Negativní"
|
||||||
|
if dj.get('dj_negativni_jednotka') is True
|
||||||
|
else "Pozitivní"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
# Link Documentation Unit to Geometry (PIAN)
|
# Link Documentation Unit to Geometry (PIAN)
|
||||||
dj_pian = dj.get('dj_pian')
|
dj_pian = dj.get('dj_pian')
|
||||||
if dj_pian:
|
if dj_pian:
|
||||||
@@ -360,20 +502,39 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
|
|||||||
pian_lookup[dj_pian_value] = []
|
pian_lookup[dj_pian_value] = []
|
||||||
|
|
||||||
if komponenty == "true":
|
if komponenty == "true":
|
||||||
# One feature per component — all data on a single row, no relations needed
|
# One feature per component –
|
||||||
komps = dj.get('dj_komponenta', [])
|
# all data on a single row, no relations needed
|
||||||
if komps:
|
if komps:
|
||||||
for komp in komps:
|
for komp in komps:
|
||||||
|
if not komp_projde_filtrem(
|
||||||
|
komp, filter_areal,
|
||||||
|
filter_datace, filters
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
|
||||||
komp_meta = {
|
komp_meta = {
|
||||||
**dj_meta,
|
**dj_meta,
|
||||||
'komponenta_id': komp.get('ident_cely', ""),
|
'komponenta_id': komp.get(
|
||||||
'komponenta_areal': komp.get('komponenta_areal', {}).get('value', ""),
|
'ident_cely',
|
||||||
'komponenta_obdobi': komp.get('komponenta_obdobi', {}).get('value', ""),
|
""
|
||||||
|
),
|
||||||
|
'komponenta_areal': komp.get(
|
||||||
|
'komponenta_areal',
|
||||||
|
{}
|
||||||
|
).get('value', ""),
|
||||||
|
'komponenta_obdobi': komp.get(
|
||||||
|
'komponenta_obdobi',
|
||||||
|
{}
|
||||||
|
).get('value', ""),
|
||||||
}
|
}
|
||||||
pian_lookup[dj_pian_value].append(komp_meta)
|
pian_lookup[dj_pian_value].append(komp_meta)
|
||||||
target_pian_ids_count += 1
|
target_pian_ids_count += 1
|
||||||
else:
|
else:
|
||||||
# DJ without components — still include with empty component fields
|
# DJ without components — still include
|
||||||
|
# with empty component fields
|
||||||
|
if filter_areal or filter_datace:
|
||||||
|
continue
|
||||||
|
|
||||||
empty_meta = {
|
empty_meta = {
|
||||||
**dj_meta,
|
**dj_meta,
|
||||||
'komponenta_id': "",
|
'komponenta_id': "",
|
||||||
@@ -386,11 +547,13 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
|
|||||||
target_pian_ids_count += 1
|
target_pian_ids_count += 1
|
||||||
pian_lookup[dj_pian_value].append(dj_meta)
|
pian_lookup[dj_pian_value].append(dj_meta)
|
||||||
|
|
||||||
|
|
||||||
if not target_pian_ids:
|
if not target_pian_ids:
|
||||||
iface.messageBar().pushMessage("AMCR", f"Nalezeno {len(docs)} záznamů, ale žádný nemá geometrii.", level=Qgis.MessageLevel.Warning)
|
iface.messageBar().pushMessage(
|
||||||
return
|
"AMCR",
|
||||||
|
f"Nalezeno {len(docs)} záznamů, ale žádný nemá geometrii.",
|
||||||
|
level=Qgis.MessageLevel.Warning
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# C) GEOMETRY FETCHING (PIAN)
|
# C) GEOMETRY FETCHING (PIAN)
|
||||||
@@ -398,17 +561,28 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
|
|||||||
ids_list = list(target_pian_ids)
|
ids_list = list(target_pian_ids)
|
||||||
total_pians = len(ids_list)
|
total_pians = len(ids_list)
|
||||||
docs_pian = []
|
docs_pian = []
|
||||||
BATCH_PIAN = 200 # Geometry requests are batch-processed to stay under URL length limits
|
# Geometry requests are batch-processed
|
||||||
|
# to stay under URL length limits:
|
||||||
iface.messageBar().pushMessage("AMCR", f"Záznamů: {len(docs)} (z toho {actions_with_geom} s mapou). Stahuji {total_pians} unikátních geometrií, vykresluji {target_pian_ids_count} geometrií...", level=Qgis.MessageLevel.Info)
|
BATCH_PIAN = 200
|
||||||
|
|
||||||
fl_pian = ["ident_cely", "pian_typ", "pian_chranene_udaje", "pian_presnost"]
|
iface.messageBar().pushMessage(
|
||||||
|
"AMCR",
|
||||||
|
f"Záznamů: {len(docs)} (z toho {actions_with_geom} s mapou). "
|
||||||
|
f"Stahuji {total_pians} unikátních geometrií, "
|
||||||
|
f"vykresluji {target_pian_ids_count} geometrií...",
|
||||||
|
level=Qgis.MessageLevel.Info
|
||||||
|
)
|
||||||
|
|
||||||
|
fl_pian = [
|
||||||
|
"ident_cely", "pian_typ",
|
||||||
|
"pian_chranene_udaje", "pian_presnost"
|
||||||
|
]
|
||||||
|
|
||||||
for i in range(0, total_pians, BATCH_PIAN):
|
for i in range(0, total_pians, BATCH_PIAN):
|
||||||
batch = ids_list[i : i + BATCH_PIAN]
|
batch = ids_list[i: i + BATCH_PIAN]
|
||||||
or_query = " OR ".join(batch)
|
or_query = " OR ".join(batch)
|
||||||
fq_pian = f"ident_cely:({or_query})"
|
fq_pian = f"ident_cely:({or_query})"
|
||||||
|
|
||||||
params_pian = {
|
params_pian = {
|
||||||
"mapa": "true",
|
"mapa": "true",
|
||||||
"entity": "pian",
|
"entity": "pian",
|
||||||
@@ -417,7 +591,7 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
|
|||||||
"fl": ",".join(fl_pian)
|
"fl": ",".join(fl_pian)
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
QApplication.processEvents()
|
QApplication.processEvents()
|
||||||
r_pian = _api_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)
|
||||||
@@ -427,13 +601,25 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
|
|||||||
# ==========================================
|
# ==========================================
|
||||||
# D) LAYER CREATION (QGIS Memory Layers)
|
# D) LAYER CREATION (QGIS Memory Layers)
|
||||||
# ==========================================
|
# ==========================================
|
||||||
|
|
||||||
archeologicky_zaznam = "Akce" if typ_dat == "akce" else "Lokalita"
|
archeologicky_zaznam = "Akce" if typ_dat == "akce" else "Lokalita"
|
||||||
|
|
||||||
# Initialize three layers for different geometry types (S-JTSK CRS)
|
# Initialize three layers for different geometry types (S-JTSK CRS)
|
||||||
vl_poly = QgsVectorLayer("Polygon?crs=epsg:5514", f"AMCR_{archeologicky_zaznam}_Polygony", "memory")
|
vl_poly = QgsVectorLayer(
|
||||||
vl_line = QgsVectorLayer("LineString?crs=epsg:5514", f"AMCR_{archeologicky_zaznam}_Linie", "memory")
|
"Polygon?crs=epsg:5514",
|
||||||
vl_point = QgsVectorLayer("Point?crs=epsg:5514", f"AMCR_{archeologicky_zaznam}_Body", "memory")
|
f"AMCR_{archeologicky_zaznam}_Polygony",
|
||||||
|
"memory"
|
||||||
|
)
|
||||||
|
vl_line = QgsVectorLayer(
|
||||||
|
"LineString?crs=epsg:5514",
|
||||||
|
f"AMCR_{archeologicky_zaznam}_Linie",
|
||||||
|
"memory"
|
||||||
|
)
|
||||||
|
vl_point = QgsVectorLayer(
|
||||||
|
"Point?crs=epsg:5514",
|
||||||
|
f"AMCR_{archeologicky_zaznam}_Body",
|
||||||
|
"memory"
|
||||||
|
)
|
||||||
layers = [vl_poly, vl_line, vl_point]
|
layers = [vl_poly, vl_line, vl_point]
|
||||||
|
|
||||||
# Define attribute table structure
|
# Define attribute table structure
|
||||||
@@ -462,7 +648,7 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
|
|||||||
QgsField("ukonceni", QMetaType.Type.QString),
|
QgsField("ukonceni", QMetaType.Type.QString),
|
||||||
QgsField("hlavni_typ", QMetaType.Type.QString),
|
QgsField("hlavni_typ", QMetaType.Type.QString),
|
||||||
QgsField("vedlejsi_typ", QMetaType.Type.QString),
|
QgsField("vedlejsi_typ", QMetaType.Type.QString),
|
||||||
QgsField("zjisteni", QMetaType.Type.QString),
|
QgsField("zjisteni", QMetaType.Type.QString),
|
||||||
QgsField("nahrazuje_NZ", QMetaType.Type.QString),
|
QgsField("nahrazuje_NZ", QMetaType.Type.QString),
|
||||||
]
|
]
|
||||||
elif typ_dat == "lokalita":
|
elif typ_dat == "lokalita":
|
||||||
@@ -473,7 +659,7 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
|
|||||||
QgsField("druh_lokality", QMetaType.Type.QString),
|
QgsField("druh_lokality", QMetaType.Type.QString),
|
||||||
QgsField("zachovalost", QMetaType.Type.QString)
|
QgsField("zachovalost", QMetaType.Type.QString)
|
||||||
]
|
]
|
||||||
|
|
||||||
cols.append(QgsField("Přístupnost", QMetaType.Type.QString))
|
cols.append(QgsField("Přístupnost", QMetaType.Type.QString))
|
||||||
|
|
||||||
# Use aliases for technical field names
|
# Use aliases for technical field names
|
||||||
@@ -507,7 +693,7 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
|
|||||||
"komponenta": "Komponenta",
|
"komponenta": "Komponenta",
|
||||||
"komponenta_areal": "Areál",
|
"komponenta_areal": "Areál",
|
||||||
"komponenta_obdobi": "Období",
|
"komponenta_obdobi": "Období",
|
||||||
}
|
}
|
||||||
|
|
||||||
if komponenty == "true":
|
if komponenty == "true":
|
||||||
cols += [
|
cols += [
|
||||||
@@ -523,36 +709,45 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
|
|||||||
idx = vl.fields().lookupField(tech_name)
|
idx = vl.fields().lookupField(tech_name)
|
||||||
if idx != -1:
|
if idx != -1:
|
||||||
vl.setFieldAlias(idx, alias)
|
vl.setFieldAlias(idx, alias)
|
||||||
|
|
||||||
# Lists to hold features before batch-adding to layers
|
# Lists to hold features before batch-adding to layers
|
||||||
feats_p, feats_l, feats_pt = [], [], []
|
feats_p, feats_l, feats_pt = [], [], []
|
||||||
|
|
||||||
# --- FEATURE POPULATION ---
|
# --- FEATURE POPULATION ---
|
||||||
for doc in docs_pian:
|
for doc in docs_pian:
|
||||||
try:
|
try:
|
||||||
pid = doc.get('ident_cely', '')
|
pid = doc.get('ident_cely', '')
|
||||||
if pid not in pian_lookup:
|
if pid not in pian_lookup:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
metas = pian_lookup[pid]
|
metas = pian_lookup[pid]
|
||||||
|
|
||||||
# Extract WKT geometry from protected JSON data
|
# Extract WKT geometry from protected JSON data
|
||||||
raw = doc.get('pian_chranene_udaje')
|
raw = doc.get('pian_chranene_udaje')
|
||||||
if isinstance(raw, list) and raw:
|
if isinstance(raw, list) and raw:
|
||||||
raw = raw[0]
|
raw = raw[0]
|
||||||
jdata = json.loads(raw) if isinstance(raw, str) else (raw or {})
|
jdata = (
|
||||||
|
json.loads(raw)
|
||||||
|
if isinstance(raw, str)
|
||||||
|
else (raw or {})
|
||||||
|
)
|
||||||
|
|
||||||
wkt = None
|
wkt = None
|
||||||
if jdata.get('geom_sjtsk_wkt'):
|
if jdata.get('geom_sjtsk_wkt'):
|
||||||
wkt = jdata.get('geom_sjtsk_wkt', {}).get('value')
|
wkt = jdata.get('geom_sjtsk_wkt', {}).get('value')
|
||||||
elif jdata.get('geom_wkt'):
|
elif jdata.get('geom_wkt'):
|
||||||
wkt = jdata.get('geom_wkt', {}).get('value')
|
wkt = jdata.get('geom_wkt', {}).get('value')
|
||||||
|
|
||||||
pian_presnost = tr_code(str(doc.get('pian_presnost', '')))
|
pian_presnost = tr_code(str(doc.get('pian_presnost', '')))
|
||||||
pian_typ = tr_code(str(doc.get('pian_typ', '')))
|
pian_typ = tr_code(str(doc.get('pian_typ', '')))
|
||||||
|
|
||||||
# Final precision filter check
|
# Final precision filter check
|
||||||
if filters and filters.get('f_pian_presnost') and doc.get('pian_presnost') not in filters.get('f_pian_presnost'):
|
if (
|
||||||
|
filters
|
||||||
|
and filters.get('f_pian_presnost')
|
||||||
|
and doc.get('pian_presnost')
|
||||||
|
not in filters.get('f_pian_presnost')
|
||||||
|
):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if wkt:
|
if wkt:
|
||||||
@@ -562,41 +757,54 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
|
|||||||
target_list = None
|
target_list = None
|
||||||
if t == QgsWkbTypes.PolygonGeometry:
|
if t == QgsWkbTypes.PolygonGeometry:
|
||||||
target_list = feats_p
|
target_list = feats_p
|
||||||
referenced_layer = vl_poly
|
|
||||||
elif t == QgsWkbTypes.LineGeometry:
|
elif t == QgsWkbTypes.LineGeometry:
|
||||||
target_list = feats_l
|
target_list = feats_l
|
||||||
referenced_layer = vl_line
|
|
||||||
elif t == QgsWkbTypes.PointGeometry:
|
elif t == QgsWkbTypes.PointGeometry:
|
||||||
target_list = feats_pt
|
target_list = feats_pt
|
||||||
referenced_layer = vl_point
|
|
||||||
|
|
||||||
if target_list is None:
|
if target_list is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
is_akce = (typ_dat == "akce")
|
is_akce = (typ_dat == "akce")
|
||||||
|
|
||||||
# Create a QGIS feature for each documentation unit associated with this geometry
|
# Create a QGIS feature for each documentation unit
|
||||||
|
# associated with this geometry
|
||||||
for meta in metas:
|
for meta in metas:
|
||||||
feat = QgsFeature()
|
feat = QgsFeature()
|
||||||
feat.setGeometry(geom)
|
feat.setGeometry(geom)
|
||||||
atributy = [
|
atributy = [
|
||||||
pid, pian_presnost, pian_typ, meta['dj_id'],
|
pid,
|
||||||
meta['dj_typ_value'], meta['loc'], meta['ident_cely'],
|
pian_presnost,
|
||||||
"https://digiarchiv.aiscr.cz/id/" + meta['ident_cely'],
|
pian_typ,
|
||||||
meta['az_okres'], meta['katastr'], meta['dalsi_katastr']
|
meta['dj_id'],
|
||||||
|
meta['dj_typ_value'],
|
||||||
|
meta['loc'],
|
||||||
|
meta['ident_cely'],
|
||||||
|
"https://digiarchiv.aiscr.cz/id/"
|
||||||
|
+ meta['ident_cely'],
|
||||||
|
meta['az_okres'],
|
||||||
|
meta['katastr'],
|
||||||
|
meta['dalsi_katastr']
|
||||||
]
|
]
|
||||||
if is_akce:
|
if is_akce:
|
||||||
atributy.extend([
|
atributy.extend([
|
||||||
meta['lokalizace_okolnosti'], meta['akce_hlavni_vedouci'],
|
meta['lokalizace_okolnosti'],
|
||||||
meta['akce_organizace'], meta['akce_specifikace_data'],
|
meta['akce_hlavni_vedouci'],
|
||||||
meta['akce_datum_zahajeni'], meta['akce_datum_ukonceni'],
|
meta['akce_organizace'],
|
||||||
meta['akce_hlavni_typ'], meta['akce_vedlejsi_typ'],
|
meta['akce_specifikace_data'],
|
||||||
meta['dj_negativni'], meta['akce_je_nz']
|
meta['akce_datum_zahajeni'],
|
||||||
|
meta['akce_datum_ukonceni'],
|
||||||
|
meta['akce_hlavni_typ'],
|
||||||
|
meta['akce_vedlejsi_typ'],
|
||||||
|
meta['dj_negativni'],
|
||||||
|
meta['akce_je_nz']
|
||||||
])
|
])
|
||||||
else:
|
else:
|
||||||
atributy.extend([
|
atributy.extend([
|
||||||
meta['lokalita_nazev'], meta['lokalita_popis'],
|
meta['lokalita_nazev'],
|
||||||
meta['lokalita_typ'], meta['lokalita_druh'],
|
meta['lokalita_popis'],
|
||||||
|
meta['lokalita_typ'],
|
||||||
|
meta['lokalita_druh'],
|
||||||
meta['lokalita_zachovalost']
|
meta['lokalita_zachovalost']
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -607,21 +815,21 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
|
|||||||
meta.get('komponenta_id', ""),
|
meta.get('komponenta_id', ""),
|
||||||
meta.get('komponenta_areal', ""),
|
meta.get('komponenta_areal', ""),
|
||||||
meta.get('komponenta_obdobi', ""),
|
meta.get('komponenta_obdobi', ""),
|
||||||
])
|
])
|
||||||
|
|
||||||
feat.setAttributes(atributy)
|
feat.setAttributes(atributy)
|
||||||
target_list.append(feat)
|
target_list.append(feat)
|
||||||
|
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print(f"Chyba při tvorbě feature: {ex}")
|
print(f"Chyba při tvorbě feature: {ex}")
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# --- ADDING TO QGIS INTERFACE ---
|
# --- ADDING TO QGIS INTERFACE ---
|
||||||
proj = QgsProject.instance()
|
proj = QgsProject.instance()
|
||||||
added = 0
|
added = 0
|
||||||
layers_to_process = [
|
layers_to_process = [
|
||||||
(feats_p, vl_poly, "Polygony"),
|
(feats_p, vl_poly, "Polygony"),
|
||||||
(feats_l, vl_line, "Linie"),
|
(feats_l, vl_line, "Linie"),
|
||||||
(feats_pt, vl_point, "Body"),
|
(feats_pt, vl_point, "Body"),
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -629,17 +837,30 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
|
|||||||
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)
|
||||||
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}). "
|
||||||
|
f"Vykresleno: {added} prvků.",
|
||||||
|
level=Qgis.MessageLevel.Success
|
||||||
|
)
|
||||||
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
|
||||||
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
iface.messageBar().pushMessage("Chyba", str(e), level=Qgis.MessageLevel.Critical)
|
iface.messageBar().pushMessage(
|
||||||
|
"Chyba",
|
||||||
|
str(e),
|
||||||
|
level=Qgis.MessageLevel.Critical
|
||||||
|
)
|
||||||
finally:
|
finally:
|
||||||
# Always restore cursor, even after failure
|
# Always restore cursor, even after failure
|
||||||
QApplication.restoreOverrideCursor()
|
QApplication.restoreOverrideCursor()
|
||||||
|
|||||||
+68
-32
@@ -1,37 +1,39 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from qgis.PyQt.QtCore import QSettings, QTranslator, QCoreApplication
|
from qgis.PyQt.QtCore import QSettings, QTranslator, QCoreApplication, QUrl
|
||||||
from qgis.PyQt.QtGui import QIcon
|
from qgis.PyQt.QtGui import QIcon, QDesktopServices
|
||||||
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.core import Qgis
|
||||||
from qgis.utils import iface
|
|
||||||
|
|
||||||
from .amcr_tools import load_amcr_data, login_to_api
|
from .amcr_tools import load_amcr_data, login_to_api
|
||||||
from .amcr_dialog import AmcrFilterDialog, LoginDialog
|
from .amcr_dialog import AmcrFilterDialog, LoginDialog
|
||||||
from .resources import *
|
from .resources import *
|
||||||
import os.path
|
import os.path
|
||||||
|
|
||||||
|
|
||||||
class AmcrViewer:
|
class AmcrViewer:
|
||||||
"""
|
"""
|
||||||
Main plugin class that manages the GUI elements, menu entries,
|
Main plugin class that manages the GUI elements, menu entries,
|
||||||
and coordinates the flow between user input and data processing.
|
and coordinates the flow between user input and data processing.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, iface):
|
def __init__(self, iface):
|
||||||
"""
|
"""
|
||||||
Constructor initializes the connection to QGIS interface and sets up
|
Constructor initializes the connection to QGIS interface and sets up
|
||||||
internationalization (i18n).
|
internationalization (i18n).
|
||||||
"""
|
"""
|
||||||
self.iface = iface
|
self.iface = iface
|
||||||
self.plugin_dir = os.path.dirname(__file__)
|
self.plugin_dir = os.path.dirname(__file__)
|
||||||
|
|
||||||
# Determine the user's locale to load appropriate translation files
|
# Determine the user's locale to load appropriate translation files
|
||||||
locale = QSettings().value('locale/userLocale')[0:2]
|
locale = QSettings().value('locale/userLocale')[0:2]
|
||||||
locale_path = os.path.join(
|
locale_path = os.path.join(
|
||||||
self.plugin_dir,
|
self.plugin_dir,
|
||||||
'i18n',
|
'i18n',
|
||||||
'AmcrViewer_{}.qm'.format(locale))
|
'AmcrViewer_{}.qm'.format(locale)
|
||||||
|
)
|
||||||
|
|
||||||
# Install the translator if a translation file for the current locale exists
|
# Install the translator if a translation file
|
||||||
|
# for the current locale exists
|
||||||
if os.path.exists(locale_path):
|
if os.path.exists(locale_path):
|
||||||
self.translator = QTranslator()
|
self.translator = QTranslator()
|
||||||
self.translator.load(locale_path)
|
self.translator.load(locale_path)
|
||||||
@@ -43,14 +45,17 @@ class AmcrViewer:
|
|||||||
self.first_start = None
|
self.first_start = None
|
||||||
|
|
||||||
def tr(self, message):
|
def tr(self, message):
|
||||||
"""Helper method for translating strings within the AmcrViewer context."""
|
"""
|
||||||
|
Helper method for translating strings within
|
||||||
|
the AmcrViewer context.
|
||||||
|
"""
|
||||||
return QCoreApplication.translate('AmcrViewer', message)
|
return QCoreApplication.translate('AmcrViewer', message)
|
||||||
|
|
||||||
def add_action(self, icon_path, text, callback, enabled_flag=True,
|
def add_action(self, icon_path, text, callback, enabled_flag=True,
|
||||||
add_to_menu=True, add_to_toolbar=True, status_tip=None,
|
add_to_menu=True, add_to_toolbar=True, status_tip=None,
|
||||||
whats_this=None, parent=None):
|
whats_this=None, parent=None):
|
||||||
"""
|
"""
|
||||||
Helper method to create QActions and automatically register them
|
Helper method to create QActions and automatically register them
|
||||||
into the QGIS Menu and Toolbar.
|
into the QGIS Menu and Toolbar.
|
||||||
"""
|
"""
|
||||||
icon = QIcon(icon_path)
|
icon = QIcon(icon_path)
|
||||||
@@ -71,26 +76,29 @@ class AmcrViewer:
|
|||||||
if add_to_menu:
|
if add_to_menu:
|
||||||
self.iface.addPluginToMenu(self.menu, action)
|
self.iface.addPluginToMenu(self.menu, action)
|
||||||
|
|
||||||
# Store only actions that are directly attached to the QGIS UI for later cleanup
|
# Store only actions that are directly attached
|
||||||
|
# to the QGIS UI for later cleanup
|
||||||
if add_to_toolbar or add_to_menu:
|
if add_to_toolbar or add_to_menu:
|
||||||
self.actions.append(action)
|
self.actions.append(action)
|
||||||
|
|
||||||
return action
|
return action
|
||||||
|
|
||||||
def initGui(self):
|
def initGui(self):
|
||||||
"""
|
"""
|
||||||
Called when the plugin is loaded. Creates the menu structure,
|
Called when the plugin is loaded. Creates the menu structure,
|
||||||
sub-actions, and the dropdown tool button in the toolbar.
|
sub-actions, and the dropdown tool button in the toolbar.
|
||||||
"""
|
"""
|
||||||
# Define paths for action-specific icons
|
# Define paths for action-specific icons
|
||||||
icon_akce_path = os.path.join(self.plugin_dir, 'akce.png')
|
icon_akce_path = os.path.join(self.plugin_dir, 'akce.png')
|
||||||
icon_lokality_path = os.path.join(self.plugin_dir, 'lokality.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
|
# 1. Create a container menu for the plugin
|
||||||
self.plugin_menu = QMenu()
|
self.plugin_menu = QMenu()
|
||||||
|
|
||||||
# 2. Create sub-actions (Download Projects / Download Sites)
|
# 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(
|
self.action_download_akce = self.add_action(
|
||||||
icon_path=icon_akce_path,
|
icon_path=icon_akce_path,
|
||||||
text=self.tr(u'Stáhnout data akcí | AMČR Viewer'),
|
text=self.tr(u'Stáhnout data akcí | AMČR Viewer'),
|
||||||
@@ -104,7 +112,7 @@ class AmcrViewer:
|
|||||||
self.action_download_lokality = self.add_action(
|
self.action_download_lokality = self.add_action(
|
||||||
icon_path=icon_lokality_path,
|
icon_path=icon_lokality_path,
|
||||||
text=self.tr(u'Stáhnout data lokalit | AMČR Viewer'),
|
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(),
|
parent=self.iface.mainWindow(),
|
||||||
add_to_menu=False,
|
add_to_menu=False,
|
||||||
add_to_toolbar=False
|
add_to_toolbar=False
|
||||||
@@ -114,16 +122,30 @@ class AmcrViewer:
|
|||||||
self.action_login_dialog = self.add_action(
|
self.action_login_dialog = self.add_action(
|
||||||
icon_path=icon_akce_path,
|
icon_path=icon_akce_path,
|
||||||
text=self.tr(u'Přihlásit se | AMČR Viewer'),
|
text=self.tr(u'Přihlásit se | AMČR Viewer'),
|
||||||
callback=lambda checked=False: self.login(),
|
callback=lambda checked=False: self.login(),
|
||||||
parent=self.iface.mainWindow(),
|
parent=self.iface.mainWindow(),
|
||||||
add_to_menu=False,
|
add_to_menu=False,
|
||||||
add_to_toolbar=False
|
add_to_toolbar=False
|
||||||
)
|
)
|
||||||
self.plugin_menu.addAction(self.action_login_dialog)
|
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
|
# 3. Create the main project action and attach the menu to it
|
||||||
main_icon = QIcon(icon_akce_path)
|
main_icon = QIcon(icon_akce_path)
|
||||||
self.main_action = QAction(main_icon, 'AMČR Viewer', self.iface.mainWindow())
|
self.main_action = QAction(
|
||||||
|
main_icon,
|
||||||
|
'AMČR Viewer',
|
||||||
|
self.iface.mainWindow()
|
||||||
|
)
|
||||||
self.main_action.setMenu(self.plugin_menu)
|
self.main_action.setMenu(self.plugin_menu)
|
||||||
self.iface.addPluginToMenu(self.menu, self.main_action)
|
self.iface.addPluginToMenu(self.menu, self.main_action)
|
||||||
|
|
||||||
@@ -132,16 +154,19 @@ class AmcrViewer:
|
|||||||
self.tool_button = QToolButton()
|
self.tool_button = QToolButton()
|
||||||
self.tool_button.setMenu(self.plugin_menu)
|
self.tool_button.setMenu(self.plugin_menu)
|
||||||
self.tool_button.setDefaultAction(self.action_download_akce)
|
self.tool_button.setDefaultAction(self.action_download_akce)
|
||||||
self.tool_button.setPopupMode(QToolButton.ToolButtonPopupMode.MenuButtonPopup)
|
self.tool_button.setPopupMode(
|
||||||
|
QToolButton.ToolButtonPopupMode.MenuButtonPopup
|
||||||
# Add the widget directly to the toolbar and store the reference for cleanup
|
)
|
||||||
|
|
||||||
|
# Add the widget directly to the toolbar
|
||||||
|
# and store the reference for cleanup
|
||||||
self.toolbar_action = self.iface.addToolBarWidget(self.tool_button)
|
self.toolbar_action = self.iface.addToolBarWidget(self.tool_button)
|
||||||
|
|
||||||
self.first_start = True
|
self.first_start = True
|
||||||
|
|
||||||
def unload(self):
|
def unload(self):
|
||||||
"""
|
"""
|
||||||
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.
|
Ensures all GUI elements are removed from QGIS to avoid ghost icons.
|
||||||
"""
|
"""
|
||||||
# 1. Remove the custom entry from the main 'Plugins' menu
|
# 1. Remove the custom entry from the main 'Plugins' menu
|
||||||
@@ -157,7 +182,7 @@ class AmcrViewer:
|
|||||||
self.iface.removePluginMenu(self.menu, action)
|
self.iface.removePluginMenu(self.menu, action)
|
||||||
self.iface.removeToolBarIcon(action)
|
self.iface.removeToolBarIcon(action)
|
||||||
self.actions.clear()
|
self.actions.clear()
|
||||||
|
|
||||||
# 4. Reset map tools if currently active
|
# 4. Reset map tools if currently active
|
||||||
if hasattr(self, 'tool'):
|
if hasattr(self, 'tool'):
|
||||||
self.iface.mapCanvas().unsetMapTool(self.tool)
|
self.iface.mapCanvas().unsetMapTool(self.tool)
|
||||||
@@ -165,20 +190,22 @@ class AmcrViewer:
|
|||||||
# --- Data downloading ---
|
# --- Data downloading ---
|
||||||
def run_download(self, typ_dat):
|
def run_download(self, typ_dat):
|
||||||
"""
|
"""
|
||||||
Triggered by menu/toolbar actions. Opens the filter dialog and
|
Triggered by menu/toolbar actions. Opens the filter dialog and
|
||||||
hands off the parameters to the data loader.
|
hands off the parameters to the data loader.
|
||||||
"""
|
"""
|
||||||
# Open the specific filter dialog (Projects vs Sites)
|
# Open the specific filter dialog (Projects vs Sites)
|
||||||
dlg = AmcrFilterDialog(typ_dat)
|
dlg = AmcrFilterDialog(typ_dat)
|
||||||
result = dlg.exec()
|
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:
|
if result == QDialog.DialogCode.Accepted:
|
||||||
filters = dlg.get_filters()
|
filters = dlg.get_filters()
|
||||||
bbox = dlg.get_bbox()
|
bbox = dlg.get_bbox()
|
||||||
komponenty = dlg.get_komponenty()
|
komponenty = dlg.get_komponenty()
|
||||||
|
|
||||||
# Access the map canvas and start the fetch/render process from amcr_tools
|
# 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)
|
||||||
|
|
||||||
@@ -190,9 +217,18 @@ class AmcrViewer:
|
|||||||
session = login_to_api(username, password)
|
session = login_to_api(username, password)
|
||||||
if session:
|
if session:
|
||||||
self.iface.messageBar().pushMessage(
|
self.iface.messageBar().pushMessage(
|
||||||
"AMČR", "Přihlášení proběhlo úspěšně.", level=Qgis.MessageLevel.Success
|
"AMČR",
|
||||||
|
"Přihlášení proběhlo úspěšně.",
|
||||||
|
level=Qgis.MessageLevel.Success
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.iface.messageBar().pushMessage(
|
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
|
"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))
|
||||||
|
|||||||
@@ -5,10 +5,10 @@
|
|||||||
|
|
||||||
[general]
|
[general]
|
||||||
name=AMČR Viewer
|
name=AMČR Viewer
|
||||||
qgisMinimumVersion=3.4.0
|
qgisMinimumVersion=3.44.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=2.0.0-alpha.2
|
version=2.0.1
|
||||||
author=David Spáčil
|
author=David Spáčil
|
||||||
email=spacil@arub.cz
|
email=spacil@arub.cz
|
||||||
|
|
||||||
@@ -22,16 +22,34 @@ repository=https://github.com/ARUP-CAS/aiscr-qgis-amcr-viewer
|
|||||||
|
|
||||||
hasProcessingProvider=no
|
hasProcessingProvider=no
|
||||||
# Uncomment the following line and add your changelog:
|
# 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 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
|
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=True
|
experimental=False
|
||||||
|
|
||||||
# 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
|
||||||
|
|||||||
Reference in New Issue
Block a user