Compare commits

...

29 Commits

Author SHA1 Message Date
david-spacil 2ab3ace310 feature (wip): první krůčky logiky pro parsování PAS záznamů
- dosavadní logika parsování platná pro akce a lokality je podmíněna typ_dat
- stub logiky parsování pro samostatné nálezy
2026-07-03 12:15:58 +02:00
david-spacil 27a0f4e9ea feature: dynamičtjší způsob interpretace typu dat
- archeologicky_zaznam nově čerpá human-readable název pro název vrstvy v typ_dat_vocab
- archeologicky_zaznam_l pro ověřování, jestli je současný typ_dat akce nebo lokalita
2026-07-03 12:13:40 +02:00
david-spacil f4ad402f52 fix: oprava typ_dat = "pas" na typ_dat = "samostatny_nalez" 2026-07-03 11:56:25 +02:00
david-spacil 2fdbb15001 feature: aktualizace hesláře 2026-06-29 13:50:53 +02:00
david-spacil 8248d1bcae feature: nové heslářové globals + cache v dialogu 2026-06-29 13:50:39 +02:00
david-spacil abb06b9858 feature: globals pro PAS
založeny nové heslářové globals, pak přidány do funkce refresh_globals
2026-06-29 13:49:12 +02:00
david-spacil e84ee8e543 feature: update funkcí pro stahování dat
funkce fetch_set a download_heslare byly upraveny pro stahování dat nejen z OAI-PMH API, ale nově i z API digiarchivu (= osoby se nově nestahují z hesláře osob, ale z facetek digiarchivu, kde mají osoby přidělené "role" nálezce/vedoucí)
2026-06-29 13:48:23 +02:00
david-spacil 66532c108b feature: rozšíření slovníčku
slovníček interních vs api klíčů byl rozšířen o nová pole (nálezce, okolnosti, ...) a zároveň byla změněna jeho struktura: nově obsahuje i base url vzhledem k tomu, že si skript pro data sahá do dvou různých API; přidáno base url pro digiarchiv
2026-06-29 13:44:54 +02:00
david-spacil 86da8b9cf4 feature: tlačítko v menu
v kontextové nabídce přibylo tlačítko pro filtrování PASových záznamů; ikona je zatím placeholder
2026-06-29 13:41:44 +02:00
david-spacil a95831f180 feature: dialog: přidána nová pole
přidána pole samostatných nálezů pro filtrovací dialog + upravena viditelnost některých spolčných polí (např. skrýt areál, zobrazit organizaci nejen pro akce, ale i pro PAS, ...)
2026-06-29 13:39:44 +02:00
david-spacil 7ea2a99ada fix/small-fixes: drobné úpravy stylu a čitelnosti kódu (#54)
* small fixes

* fix: refactoring
rozbaleny listy, doplněny trailing commas
2026-06-28 11:03:15 +02:00
david-spacil 6772a99ead feature/projektové akce: filtrování a sloupec Projekt v akcích (#53)
* feature/projektove-akce: do dialogu akce přidán checkbox pro filtrování projektových akcí + backend logika

* feature/projektove-akce: do atributové tabulky akcí přidáno pole Projekt (prázdné, pokud nejde o projektovou akci)
2026-06-28 10:35:25 +02:00
Claude 9e8863b879 chore: sladění governance se vzorem aiscr-management
- ekosystémová poznámka (sibling hubu aiscr-management)
- konvence větví feat/fix/docs/chore + agents/<jméno>/<téma>
- pravidla pro AI agenty (git delivery)
- bezpečnost/soukromí (žádné secrets/PII)
- deterministické datum pro changelog
- PR šablona: pole Zapojení AI + položka konvence větví

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C8nJvecSSf6vB3NYZsMaVt
2026-06-28 10:26:06 +02:00
Claude 0eb8008e27 chore: PR šablona + AGENTS.md (a CLAUDE.md odkaz) pro standardizaci repozitáře
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C8nJvecSSf6vB3NYZsMaVt
2026-06-28 10:24:00 +02:00
david-spacil bc799452a3 Update metadata.txt 2026-06-13 12:16:27 +02:00
david-spacil ee558aa718 Merge pull request #49 from ARUP-CAS/agents/claude/fix-optim
Opravy chyb a optimalizace napříč pluginem

Souhrn revize celého kódu pluginu:

- Opravy pádů: chybějící locale při startu QGIS; životní cyklus
  úlohy aktualizace heslářů (GC reference, zákaz souběhu, message
  boxy nad hlavním oknem).
- Datová korektnost: transformace fallback geometrií WGS-84 do
  S-JTSK, oprava nevalidních polygonů přes makeValid(), normalizace
  hodnot pian_presnost/pian_typ, heslář přístupnosti bez záznamů
  s kódem None, ošetření null hodnot u komponent.
- Přihlašování a stahování: ověření údajů před uložením do správce
  autentizace, zřetelné hlášení síťových chyb a neúplných výsledků,
  jedno parsování JSON odpovědí, úspornější stránkování, guard proti
  souběžným stahováním, logování do QgsMessageLog.
- Metadata a CI: oprava ikony pluginu, akce ve workflow povýšeny na
  aktuální verze a připnuty na commit hash, poznámka o závislosti
  na requests v README.

Otestováno ručně v QGIS dle checklistu v PR (včetně hraničních
případů: výpadek sítě, expirovaná session, špatné heslo, zrušení
úlohy, prázdné výsledky, limit záznamů).
2026-06-12 13:59:46 +02:00
Claude eebd7668a5 Workflow: připnutí GitHub Actions na commit hash + aktualizace verzí
Reakce na CodeQL upozornění 'Unpinned tag for a non-immutable Action'.
Tag (v4/v2) může vlastník akce kdykoli přepsat na jiný commit; připnutí
na hash chrání release pipeline před podvržením závislosti.

Zároveň povýšeno na nejnovější verze: actions/checkout v6.0.3
a softprops/action-gh-release v3.0.0 (jen přechod na Node 24 runtime,
bez změn API; hashe ověřeny přes git ls-remote).
2026-06-12 13:46:47 +02:00
Claude fd11bee274 Ověření přihlašovacích údajů před uložením do správce autentizace
Špatné heslo zadané v přihlašovacím dialogu se dosud uložilo do QGIS
Authentication Manageru a chyba se ukázala až po zavření dialogu.

- Dialog nyní před uložením zkusí přihlášení k API; při neplatných
  údajích se nic neuloží a uživatel může údaje rovnou opravit.
- Pokud je server nedostupný (údaje nelze ověřit), uživatel si může
  zvolit, zda je uložit neověřené – změna hesla na webu u dříve
  uložených údajů tím není dotčena.
- login_to_api nově rozlišuje důvod selhání (LAST_LOGIN_ERROR:
  'auth' × 'network'), aby dialog uměl oba případy odlišit.
- Ověřuje se i změna samotného e-mailu (proti uloženému heslu).
2026-06-12 12:53:28 +02:00
Claude 93ed0ca810 Zřetelné hlášení síťových chyb během stahování
Při výpadku sítě uprostřed stahování se dosud chyba jen zapsala do logu:
stránkování se tiše ukončilo s částečnými daty a smyčka PIAN zkoušela
marně všechny zbývající dávky, takže uživatel skončil u hlášky 'Žádná
data k zobrazení' bez vysvětlení.

- Síťové chyby (requests.RequestException) se v obou smyčkách chytají
  zvlášť a stahování PIAN se po první z nich ihned ukončí.
- Pokud selže už stahování metadat, zobrazí se červená lišta
  'Stahování selhalo: chyba sítě'.
- Pokud se stihla vykreslit část dat, zobrazí se varování, že výsledek
  je neúplný a je třeba stahování zopakovat.
2026-06-12 12:35:51 +02:00
Claude 64ec1ea7fd Metadata, CI a dokumentace
- metadata.txt: icon ukazoval na neexistující download.png – plugin se
  ve správci zásuvných modulů zobrazoval bez ikony; nastaveno na akce.png.
- Workflow: actions/checkout v2→v4 (v2 je deprecated, Node 12)
  a softprops/action-gh-release v1→v2.
- README: poznámka, že knihovna requests nemusí být na Linuxu součástí
  systémové instalace QGIS.
2026-06-12 10:50:04 +02:00
Claude 27e5fe02ac Heslář pristupnost: přeskočení záznamů bez platného kódu
Pokud žádný titulek záznamu neprojde filtrem na jednopísmenný kód,
next() vrátil None a do heslar.csv se zapsal prázdný/None kód, který
se mohl dostat do API filtru jako hodnota 'None:or'. Takové záznamy
se nyní přeskakují.
2026-06-12 10:50:04 +02:00
Claude 46c09c4a09 Robustnější síťová vrstva a stahování dat
- login_to_api: ošetřena výjimka při parsování ne-JSON odpovědi
  (např. HTML chybová stránka za proxy) – dříve propadla až do QGIS.
- _api_get přejmenováno na _api_get_json: tělo odpovědi se parsuje jen
  jednou (kontrola expirované session ho znovu využije), místo dvojího
  json() na každé dávce.
- Stránkování porovnává počet stažených (ne unikátních) záznamů proti
  numFound – stránky plné duplicit už nevyvolávají zbytečné requesty.
- Přidán re-entrancy guard: během běžícího stahování (processEvents
  pumpuje event loop) nelze spustit druhé stahování.
- print() nahrazeno QgsMessageLog (záložka AMČR v panelu Zprávy).
2026-06-12 10:50:03 +02:00
Claude 444d1c4826 Oprava geometrií a filtrování PIAN
- Fallback geometrie z geom_wkt (WGS-84) se nyní transformuje do S-JTSK;
  dříve se souřadnice ve stupních vkládaly přímo do vrstvy EPSG:5514
  a prvek se vykreslil úplně mimo.
- Nevalidní geometrie (např. samoprůniky polygonů) se před zahozením
  zkusí opravit přes makeValid().
- Filtr přesnosti PIAN normalizuje hodnotu pian_presnost (API ji může
  vracet jako jednoprvkový seznam) – dříve mohl tiše zahazovat záznamy.
  Stejná normalizace pro pian_typ kvůli překladu kódu.
- Přístupy ke komponenta_areal/komponenta_obdobi a az_chranene_udaje
  ošetřeny proti hodnotě None (klíč existuje, ale je null).
2026-06-12 10:50:03 +02:00
Claude d9f5d2ae6e Oprava pádů: chybějící locale při startu a životní cyklus úlohy aktualizace heslářů
- amcr_viewer.py: QSettings().value('locale/userLocale') může být None na
  čisté instalaci QGIS – ošetřen fallback na 'en', jinak plugin spadne
  na TypeError hned při načtení.
- amcr_viewer.py: odstraněn nepoužívaný hvězdičkový import resources.
- amcr_dialog.py: úloha aktualizace heslářů se drží v modulové referenci,
  aby GC neuklidil Python wrapper QgsTask před dokončením (známá příčina
  pádů QGIS).
- amcr_dialog.py: tlačítko aktualizace se po spuštění zakáže – nelze už
  spustit několik stahování paralelně přepisujících heslar.csv.
- amcr_dialog.py: závěrečné QMessageBoxy se parentují na hlavní okno QGIS
  místo dialogu, který může být v době dokončení úlohy už zavřený
  (smazaný C++ objekt → pád).
2026-06-12 10:50:03 +02:00
david-spacil d417a78b85 Update metadata.txt 2026-06-10 13:40:14 +02:00
david-spacil ca827321d8 Update README.md 2026-06-10 13:33:33 +02:00
david-spacil b2001c625c Hotfix: oprava minimální podporované QGIS verze (3.4→3.44) (#48) 2026-06-10 13:31:55 +02:00
david-spacil 88018aa432 Update README.md 2026-06-10 13:16:33 +02:00
david-spacil 45b6ab09b2 Update CITATION.cff 2026-06-05 14:12:54 +02:00
12 changed files with 17785 additions and 22277 deletions
+35
View File
@@ -0,0 +1,35 @@
<!--
Děkujeme za příspěvek! Vyplň prosím sekce níže.
Nepotřebné body můžeš smazat. Komentáře (<!-- ... -->) se v PR nezobrazují.
-->
## Souhrn
<!-- Stručně co a proč. 13 věty. -->
## Změny
<!-- Co se konkrétně mění, ideálně po souborech / oblastech. -->
-
## Testování
<!-- Jak ověřit, že to funguje (kroky v QGIS, scénáře). -->
-
## Zapojení AI
<!-- Byla u změny použita AI? Jak? Např. "text navržen AI, ručně zkontrolováno
a upraveno". Smaž, pokud AI použita nebyla. -->
-
## Kontrolní seznam
- [ ] Změny jsou v souladu se stylem projektu (viz `AGENTS.md`)
- [ ] Při změně funkcí povýšena verze v `amcr_viewer/metadata.txt` a doplněn `changelog`
- [ ] Otestováno v QGIS (min. podporovaná verze 3.44)
- [ ] PR míří do správné cílové větve
- [ ] Větev odpovídá konvenci (`feat/ fix/ docs/ chore/<téma>`, AI `agents/<jméno>/<téma>`)
## Související issue
<!-- Např. "Closes #123". Smaž, pokud se neváže k issue. -->
+2 -2
View File
@@ -11,7 +11,7 @@ jobs:
steps:
# 1. Stáhne kód z repozitáře
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
# 2. Vytvoří ZIP (předpokládá, že kód je ve složce 'amcr_viewer')
- name: Zip Plugin
@@ -22,7 +22,7 @@ jobs:
# 3. Nahraje ZIP k Releasu
- name: Upload Release Asset
uses: softprops/action-gh-release@v1
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
if: startsWith(github.ref, 'refs/tags/')
with:
files: amcr_viewer.zip
+122
View File
@@ -0,0 +1,122 @@
# AGENTS.md
Pokyny pro AI agenty i lidské přispěvatele pracující v tomto repozitáři.
Tento soubor je jediný zdroj pravdy; `CLAUDE.md` na něj pouze odkazuje.
## O projektu
**AMČR Viewer** je plugin do QGIS pro stahování a vizualizaci dat z Digitálního
archivu Archeologické mapy ČR (AMČR / AIS CR) akce (*Fieldwork events*),
lokality (*Sites*) a jejich komponenty. Podporuje anonymní i přihlášený přístup
přes AMČR účet.
Zdroj dat: https://digiarchiv.aiscr.cz/ · Nápověda: https://amcr-help.aiscr.cz/digiarchiv/qgis-viewer.html
## Zařazení v ekosystému AIS CR
Tento repozitář je jedním ze **sourozeneckých repozitářů** ekosystému AIS CR.
Centrální governance a AI konfigurace spravuje hub **`aiscr-management`**; konvence
v tomto souboru jsou s tímto vzorem sladěné a zjednodušené pro potřeby jednoho
QGIS pluginu. Těžkou mašinerii hubu (složka `.agents/`, OpenSpec, sync skripty,
multi-assistant generování) tento repozitář **záměrně nepřebírá**. Při širších
otázkách governance má přednost vzor z `aiscr-management`.
## Struktura repozitáře
```
amcr_viewer/ # vlastní kód pluginu (toto se balí do releasu)
amcr_viewer.py # vstupní bod pluginu, integrace do QGIS
amcr_dialog.py # dialogy a UI (filtry, přihlášení)
amcr_tools.py # stahování dat z API, sestavení vrstev a atributů
amcr_codelists.py # hesláře / číselníky (codelists)
codelists/heslar.csv # lokální kopie číselníků
metadata.txt # metadata pluginu + verze + changelog
i18n/ # překlady (.ts)
*.png # ikony
.github/workflows/ # CI release pluginu
README.md # uživatelská dokumentace (anglicky)
```
## Konvence
### Jazyk
- **Kód a identifikátory:** anglicky. Atributová pole vrstev musí být ASCII
kompatibilní (bez diakritiky) viz historie změn v `metadata.txt`.
- **README a uživatelská dokumentace:** anglicky.
- **Commity, PR a komentáře v issue:** česky.
### Commity
- Styl odpovídá historii: česky, věcně, popisně; jeden commit = jedna logická
změna.
- První řádek stručně a výstižně (ideálně v imperativu); podrobnosti do těla.
- Pokud je commit připraven s pomocí AI, uveď to v těle commitu nebo v popisu PR.
### Větve
Konvence názvů je sladěná s hubem `aiscr-management`:
- **lidé:** `feat/<téma>` (nová funkce), `fix/<téma>` (oprava), `docs/<téma>`
(dokumentace), `chore/<téma>` (údržba).
- **AI agenti:** `agents/<jméno-agenta>/<téma>` (např. `agents/claude/oprava-pian`).
Další pravidla:
- Cílová větev pro nový vývoj je aktuální `version/v2.x.y` (ne přímo do
výchozí větve bez PR).
- **Nikdy** nepushuj přímo do chráněných větví; vždy přes Pull Request.
- Standardizační / nefunkční změny drž v samostatné větvi, ať se nemíchají do
feature PR.
### Pravidla pro AI agenty (git)
- AI ve výchozím stavu zůstává u **lokální práce na aktuální větvi**.
- Bez **výslovného pokynu** uživatele AI nestageuje (`git add`), necommituje,
nepushuje ani samo nepřepíná/nezakládá větev pro vzdálené doručení.
- Při výslovném požadavku na push/PR použij větev `agents/<jméno-agenta>/<téma>`;
pokud aktuální větev tomuto vzoru neodpovídá, vyžádej si nejdřív potvrzení.
- Vytvoření větve, stage, commit, push ani draft PR AI běžně nenabízí; zmiňuj je
jen tehdy, když jsou pro splnění úkolu opravdu nutné.
### QGIS specifika
- Minimální podporovaná verze QGIS je **3.44** (`qgisMinimumVersion` v
`metadata.txt`); kód nesmí spoléhat na novější API.
- Vrstvy a atributy se sestavují přes `QgsField` / QGIS API v `amcr_tools.py`.
Při přidání atributu je potřeba doplnit ho konzistentně na všech místech:
definice pole (`QgsField`), naplnění hodnoty z dokumentu, překlad hlavičky
sloupce a export atributů.
## Verzování a release
- Verze pluginu žije v **`amcr_viewer/metadata.txt`** (`version=`).
- **Při každé změně chování / nové funkci** povyš verzi a doplň položku do
`changelog=` v `metadata.txt` (formát `vX.Y.Z (RRRR-MM-DD)` + odrážky).
- Datum v changelogu ber z **deterministického zdroje**, ne z paměti, např.
`python -c "import datetime; print(datetime.date.today().isoformat())"`.
- Release se vytváří publikací GitHub Release; workflow
`.github/workflows/release_plugin.yml` zabalí složku `amcr_viewer/` do
`amcr_viewer.zip` a přiloží ji k releasu. Do ZIPu se nesmí dostat `.git*`
soubory.
## Pull requesty
- Používej PR šablonu (`.github/pull_request_template.md`): Souhrn / Změny /
Testování / Kontrolní seznam.
- PR musí mířit do správné `version/v2.x.y` větve.
- Před požádáním o review projdi kontrolní seznam v šabloně (zejména bump verze
v `metadata.txt`, pokud měníš chování).
- V popisu PR uveď **podíl AI** (např. „text navržen AI, ručně zkontrolováno")
a odkaz na související issue, pokud existuje.
## Bezpečnost a soukromí
- Do promptů, příkladů ani commitů **nevkládej** ostrá produkční data, plné
log dumpy ani reálné osobní údaje (PII).
- **Rediguj** secrets, tokeny, API klíče a hesla z čehokoli, co posíláš AI;
nikdy je necommituj do repozitáře (ani přihlašovací údaje k AMČR účtu).
- Pro interní infrastrukturu (URL, hostname, prostředí) používej placeholdery,
pokud konkrétní hodnota není nutná a povolená.
## Lokální ověření
Plugin se testuje načtením do QGIS (Plugins → Manage and Install Plugins →
Install from ZIP, nebo nasazením složky `amcr_viewer/` do adresáře pluginů
QGIS). Automatizované testy zatím repozitář neobsahuje změny ověřuj ručně
v QGIS na podporované verzi.
+6 -7
View File
@@ -20,11 +20,10 @@ identifiers:
value: 10.5281/zenodo.18609813
repository-code: 'https://github.com/ARUP-CAS/aiscr-qgis-amcr-viewer'
abstract: >-
This QGIS plugin is intended for downloading the data
(Fieldwork events data only, at the time) from the
Digiarchive of the Archaeological Map of the Czech
Republic (AMCR). As of now, only publicly accessible data
can be downloaded.
This plugin is intended for downloading the data
(Fieldwork events, Sites, and their Components) from
the Digital archive of the Archaeological Map of the
Czech Republic (https://digiarchiv.aiscr.cz/).
license: GPL-3.0
version: '1.0.1'
date-released: '2026-02-11'
version: '2.0.0'
date-released: '2026-06-05'
+6
View File
@@ -0,0 +1,6 @@
# CLAUDE.md
Pokyny pro tento repozitář jsou v [`AGENTS.md`](./AGENTS.md).
Tento soubor je záměrně jen odkaz, aby existoval jeden zdroj pravdy a obsah se
nerozjel. Cokoli platí pro AI agenty v tomto projektu, najdeš v `AGENTS.md`.
+5 -3
View File
@@ -2,7 +2,7 @@
[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
**Platform:** QGIS 3.4.04.99.0
**Platform:** QGIS 3.44.04.99.0
**Module Type:** Data Acquisition & Visualization
@@ -24,9 +24,9 @@
## 2. Installation Guide
**Install the plugin from QGIS plugin repository.**
**Install the latest version of the plugin from the QGIS plugin repository.**
**OR**
**OR** (*in case you need older version*)
1. *Obtain the [plugin distribution package](https://github.com/ARUP-CAS/aiscr-qgis-amcr-viewer/releases) (ZIP archive containing the `amcr_viewer` directory).*
2. *Launch QGIS.*
@@ -131,6 +131,8 @@ Layers are only created if the query returns features of the corresponding geome
The plugin is developed in **Python 3** using the **PyQt6** framework for the GUI and the **Requests** library for HTTP communication.
> **Note:** The `requests` library is bundled with the QGIS installers for Windows and macOS. On Linux (distribution packages), it may need to be installed separately (e.g. `python3-requests`).
### 4.1 File Structure
* `amcr_viewer.py`: Entry point; handles GUI integration, toolbar/menu setup, and login flow.
+135 -78
View File
@@ -9,24 +9,30 @@ from qgis.core import QgsMessageLog, Qgis
# Define paths for the plugin and its codelists directory
PLUGIN_DIR = os.path.dirname(__file__)
CODELISTS_DIR = os.path.join(PLUGIN_DIR, 'codelists')
BASE_URL = "https://api.aiscr.cz/2.2/oai"
BASE_URL_AMCR = "https://api.aiscr.cz/2.2/oai"
BASE_URL_DA = "https://digiarchiv.aiscr.cz/api/search/query"
OUTPUT_FILE = os.path.join(CODELISTS_DIR, 'heslar.csv')
slovnicek = {
'obdobi': 'heslo:obdobi',
'typ_akce': 'heslo:akce_typ',
'areal': 'heslo:areal',
'kraj': 'ruian_kraj',
'organizace': 'organizace',
'okres': 'ruian_okres',
'katastr': 'ruian_katastr',
'vedouci': 'osoba',
'pian_presnost': 'heslo:pian_presnost',
'typ_lokality': 'heslo:lokalita_typ',
'druh_lokality': 'heslo:lokalita_druh',
'jistota': 'heslo:jistota_urceni',
'lokalita_zachovalost': 'heslo:stav_dochovani',
'pristupnost': 'heslo:pristupnost'
'obdobi': (BASE_URL_AMCR, 'heslo:obdobi'),
'typ_akce': (BASE_URL_AMCR, 'heslo:akce_typ'),
'areal': (BASE_URL_AMCR, 'heslo:areal'),
'kraj': (BASE_URL_AMCR, 'ruian_kraj'),
'organizace': (BASE_URL_AMCR, 'organizace'),
'okres': (BASE_URL_AMCR, 'ruian_okres'),
'katastr': (BASE_URL_AMCR, 'ruian_katastr'),
'pian_presnost': (BASE_URL_AMCR, 'heslo:pian_presnost'),
'typ_lokality': (BASE_URL_AMCR, 'heslo:lokalita_typ'),
'druh_lokality': (BASE_URL_AMCR, 'heslo:lokalita_druh'),
'jistota': (BASE_URL_AMCR, 'heslo:jistota_urceni'),
'lokalita_zachovalost': (BASE_URL_AMCR, 'heslo:stav_dochovani'),
'pristupnost': (BASE_URL_AMCR, 'heslo:pristupnost'),
'nalez_kategorie': (BASE_URL_AMCR, 'heslo:predmet_druh_kat'),
'druh_nalezu': (BASE_URL_AMCR, 'heslo:predmet_druh'),
'specifikace': (BASE_URL_AMCR, 'heslo:predmet_specifikace'),
'nalezove_okolnosti': (BASE_URL_AMCR, 'heslo:nalezove_okolnosti'),
'vedouci': (BASE_URL_DA, 'f_vedouci'),
'nalezce': (BASE_URL_DA, 'f_nalezce'),
}
NS = {
@@ -97,13 +103,19 @@ def load_all_data():
return categorized_data
def fetch_set(internal_name, api_set, task=None):
def fetch_set(base_url, internal_name, api_set, task=None):
dataset = []
params = {
params_amcr = {
"verb": "ListRecords",
"metadataPrefix": "oai_dc",
"set": api_set
}
params_da = {
"entity": "samostatny_nalez" if internal_name == "nalezce" else "akce",
"rows": 0,
"noFacets": "false",
"onlyFacets": "true"
}
while True:
# Check for cancellation at each iteration
@@ -111,70 +123,95 @@ def fetch_set(internal_name, api_set, task=None):
return None
try:
response = requests.get(BASE_URL, params=params, timeout=30)
response.raise_for_status()
root = ET.fromstring(response.content) # nosec
if "digiarchiv" not in base_url:
response = requests.get(base_url, params=params_amcr, timeout=30)
response.raise_for_status()
root = ET.fromstring(response.content) # nosec
records = root.findall('.//oai:record', NS)
for rec in records:
metadata = rec.find('.//oai_dc:dc', NS)
if metadata is not None:
# Code (identifier)
identifier_el = metadata.find('dc:identifier', NS)
kod = (
identifier_el.text
if identifier_el is not None
else ""
)
# Title filter out system labels "AMČR - ..."
titles = metadata.findall('dc:title', NS)
nazev = ""
for t in titles:
if (
t.text
and not t.text.startswith("AMČR -")
and not t.text.startswith(" AMČR -")
):
nazev = t.text
break
# If no title passed the filter, fall back
# to the first available one
if not nazev and titles:
nazev = titles[0].text
specialni_pripady = ['okres', 'katastr']
if internal_name in specialni_pripady:
kod = nazev
if internal_name == 'pristupnost':
kod = next(
(
t.text for t in titles
if t.text
and len(t.text) == 1
and t.text.isalpha()
),
None
records = root.findall('.//oai:record', NS)
for rec in records:
metadata = rec.find('.//oai_dc:dc', NS)
if metadata is not None:
# Code (identifier)
identifier_el = metadata.find('dc:identifier', NS)
kod = (
identifier_el.text
if identifier_el is not None
else ""
)
dataset.append({
'Název': nazev,
'Kód': kod,
'Kategorie': internal_name
})
# Title filter out system labels "AMČR - ..."
titles = metadata.findall('dc:title', NS)
nazev = ""
for t in titles:
if (
t.text
and not t.text.startswith("AMČR -")
and not t.text.startswith(" AMČR -")
):
nazev = t.text
break
# If no title passed the filter, fall back
# to the first available one
if not nazev and titles:
nazev = titles[0].text
# Pagination
token = root.find('.//oai:resumptionToken', NS)
if token is not None and token.text:
params = {
"verb": "ListRecords",
"resumptionToken": token.text
}
time.sleep(0.5)
specialni_pripady = ['okres', 'katastr']
if internal_name in specialni_pripady:
kod = nazev
if internal_name == 'pristupnost':
kod = next(
(
t.text for t in titles
if t.text
and len(t.text) == 1
and t.text.isalpha()
),
None
)
# Skip records without a valid one-letter code
# a None code would end up in the CSV and later
# in the API filter as the string "None"
if not kod:
continue
dataset.append({
'Název': nazev,
'Kód': kod,
'Kategorie': internal_name
})
# Pagination
token = root.find('.//oai:resumptionToken', NS)
if token is not None and token.text:
params_amcr = {
"verb": "ListRecords",
"resumptionToken": token.text
}
time.sleep(0.5)
else:
break
else:
break
response = requests.get(base_url, params=params_da, timeout=30)
response.raise_for_status()
data_json = response.json()
records = data_json['facet_counts']['facet_fields'][api_set]
for r in records:
nazev = r["name"]
dataset.append({
'Název': nazev,
'Kód': nazev,
'Kategorie': internal_name
})
break
except Exception as e:
QgsMessageLog.logMessage(
@@ -190,8 +227,13 @@ def download_heslare(task=None):
ensure_codelists_dir()
all_data = []
total_sets = len(slovnicek)
# index, (interni, api_nazev)
for index, (key, value) in enumerate(slovnicek.items()):
base_url = value[0]
interni = key
api_nazev = value[1]
for index, (interni, api_nazev) in enumerate(slovnicek.items()):
# Check if the user cancelled the task via the QGIS taskbar
if task and task.isCanceled():
return False
@@ -201,7 +243,7 @@ def download_heslare(task=None):
"AMČR", Qgis.Info)
# Pass the task correctly to the updated fetch function
data = fetch_set(interni, api_nazev, task=task)
data = fetch_set(base_url, interni, api_nazev, task=task)
if data is None:
return False # Cancelled mid-download
@@ -255,6 +297,16 @@ def refresh_globals():
LOKALITA_ZACHOVALOST.update(data.get('lokalita_zachovalost', {}))
PRISTUPNOST.clear()
PRISTUPNOST.update(data.get('pristupnost', {}))
NALEZ_KATEGORIE.clear()
NALEZ_KATEGORIE.update(data.get('nalez_kategorie', {}))
DRUH_NALEZU.clear()
DRUH_NALEZU.update(data.get('druh_nalezu', {}))
SPECIFIKACE.clear()
SPECIFIKACE.update(data.get('specifikace', {}))
NALEZOVE_OKOLNOSTI.clear()
NALEZOVE_OKOLNOSTI.update(data.get('nalezove_okolnosti', {}))
NALEZCE.clear()
NALEZCE.update(data.get('nalezce', {}))
# Initialize empty dicts that will be populated immediately below
@@ -272,5 +324,10 @@ DRUH_LOKALITY = {}
JISTOTA = {}
LOKALITA_ZACHOVALOST = {}
PRISTUPNOST = {}
NALEZ_KATEGORIE = {}
DRUH_NALEZU = {}
SPECIFIKACE = {}
NALEZOVE_OKOLNOSTI = {}
NALEZCE = {}
refresh_globals()
+175 -43
View File
@@ -7,13 +7,22 @@ from qgis.PyQt.QtWidgets import (QDialog, QVBoxLayout,
from qgis.PyQt.QtCore import Qt, QSettings
from qgis.core import (QgsTask, QgsApplication,
QgsMessageLog, Qgis, QgsAuthMethodConfig)
from qgis.utils import iface
from .amcr_codelists import (OBDOBI, TYP_AKCE, KRAJE, AREAL, ORGANIZACE,
OKRESY, KATASTRY, VEDOUCI, PIAN_PRESNOST,
TYP_LOKALITY, DRUH_LOKALITY, JISTOTA,
LOKALITA_ZACHOVALOST, PRISTUPNOST,
LOKALITA_ZACHOVALOST, PRISTUPNOST,
NALEZ_KATEGORIE, DRUH_NALEZU, SPECIFIKACE,
NALEZOVE_OKOLNOSTI, NALEZCE,
download_heslare, refresh_globals)
# Keep Python references to running tasks. QgsTaskManager only holds the
# C++ object; without a Python-side reference the wrapper can be garbage
# collected before the task finishes, which crashes QGIS.
_ACTIVE_TASKS = []
class UpdateCodelistsTask(QgsTask):
def __init__(self, description):
super().__init__(description, QgsTask.CanCancel)
@@ -37,16 +46,19 @@ class UpdateCodelistsTask(QgsTask):
refresh_globals()
QgsMessageLog.logMessage(
"Hesláře AMČR byly úspěšně aktualizovány.",
"AMČR", Qgis.Info)
"AMČR", Qgis.Info
)
else:
if self.isCanceled():
QgsMessageLog.logMessage(
"Aktualizace heslářů byla zrušena.",
"AMČR", Qgis.Warning)
"AMČR", Qgis.Warning
)
else:
QgsMessageLog.logMessage(
f"Chyba aktualizace: {self.exception}",
"AMČR", Qgis.Critical)
"AMČR", Qgis.Critical
)
class FilterableSelectionDialog(QDialog):
@@ -144,10 +156,25 @@ class AmcrFilterDialog(QDialog):
# Cache dictionary to store selected codes for each category
self.selection_cache = {
'organizace': [], 'kraj': [], 'obdobi': [], 'areal': [],
'typ_akce': [], 'okres': [], 'katastr': [], 'vedouci': [],
'pian_presnost': [], 'pristupnost': [], 'typ_lokality': [],
'druh_lokality': [], 'jistota': [], 'lokalita_zachovalost': []
'organizace': [],
'kraj': [],
'obdobi': [],
'areal': [],
'typ_akce': [],
'okres': [],
'katastr': [],
'vedouci': [],
'pian_presnost': [],
'pristupnost': [],
'typ_lokality': [],
'druh_lokality': [],
'jistota': [],
'lokalita_zachovalost': [],
'nalez_kategorie': [],
'druh_nalezu': [],
'specifikace': [],
'nalezove_okolnosti': [],
'nalezce': [],
}
layout = QVBoxLayout()
@@ -162,6 +189,8 @@ class AmcrFilterDialog(QDialog):
if self.typ_dat == "akce":
self.chk_posevidence = QCheckBox("Pouze pozitivní zjištění")
layout.addWidget(self.chk_posevidence)
self.chk_proj_akce = QCheckBox("Pouze projektové akce")
layout.addWidget(self.chk_proj_akce)
layout.addSpacing(10)
@@ -196,7 +225,7 @@ class AmcrFilterDialog(QDialog):
# Filters valid for Akce
if self.typ_dat == "akce":
if self.typ_dat in ["samostatny_nalez", "akce"]:
self.picker_org = self.setup_picker(
"Organizace",
'organizace',
@@ -204,6 +233,7 @@ class AmcrFilterDialog(QDialog):
)
layout.addWidget(self.picker_org)
if self.typ_dat == "akce":
self.picker_vedouci = self.setup_picker(
"Vedoucí výzkumu",
'vedouci',
@@ -256,12 +286,49 @@ class AmcrFilterDialog(QDialog):
self.picker_obdobi = self.setup_picker("Období", 'obdobi', OBDOBI)
layout.addWidget(self.picker_obdobi)
self.picker_areal = self.setup_picker("Areál", 'areal', AREAL)
layout.addWidget(self.picker_areal)
if self.typ_dat == "samostatny_nalez":
self.picker_nalez_kategorie = self.setup_picker(
"Kategorie nálezu",
'nalez_kategorie',
NALEZ_KATEGORIE
)
layout.addWidget(self.picker_nalez_kategorie)
# Option to download related components table
self.chk_komponenty = QCheckBox("Načíst komponenty")
layout.addWidget(self.chk_komponenty)
self.picker_druh_nalezu = self.setup_picker(
"Druh nálezu",
'druh_nalezu',
DRUH_NALEZU
)
layout.addWidget(self.picker_druh_nalezu)
self.picker_specifikace = self.setup_picker(
"Specifikace nálezu",
'specifikace',
SPECIFIKACE
)
layout.addWidget(self.picker_specifikace)
self.picker_nalezove_okolnosti = self.setup_picker(
"Okolnosti nálezu",
'nalezove_okolnosti',
NALEZOVE_OKOLNOSTI
)
layout.addWidget(self.picker_nalezove_okolnosti)
self.picker_nalezce = self.setup_picker(
"Nálezce",
'nalezce',
NALEZCE
)
layout.addWidget(self.picker_nalezce)
if self.typ_dat != "samostatny_nalez":
self.picker_areal = self.setup_picker("Areál", 'areal', AREAL)
layout.addWidget(self.picker_areal)
# Option to download related components table
self.chk_komponenty = QCheckBox("Načíst komponenty")
layout.addWidget(self.chk_komponenty)
# Warning label
self.lbl_komponenty_warning = QLabel(
@@ -277,9 +344,10 @@ class AmcrFilterDialog(QDialog):
self.lbl_komponenty_warning.setVisible(False)
layout.addWidget(self.lbl_komponenty_warning)
self.chk_komponenty.toggled.connect(
self.lbl_komponenty_warning.setVisible
)
if self.typ_dat != "samostatny_nalez":
self.chk_komponenty.toggled.connect(
self.lbl_komponenty_warning.setVisible
)
# Pushes everything above to the top
layout.addStretch(1)
@@ -354,7 +422,7 @@ class AmcrFilterDialog(QDialog):
self.selection_cache[cache_key] = [
'HES-000861',
'HES-000862',
'HES-000863'
'HES-000863',
]
btn.clicked.connect(open_dialog)
@@ -370,21 +438,38 @@ class AmcrFilterDialog(QDialog):
return row_widget
def action_update_heslare(self):
# Create the task instance
# Create the task instance and keep a reference so the Python
# wrapper survives until the task finishes
task = UpdateCodelistsTask("Aktualizace heslářů AMČR")
_ACTIVE_TASKS.append(task)
# Re-enable the button regardless of the outcome
task.taskCompleted.connect(lambda: self.btn_update.setEnabled(True))
task.taskTerminated.connect(lambda: self.btn_update.setEnabled(True))
# Prevent parallel downloads overwriting heslar.csv
self.btn_update.setEnabled(False)
task.taskCompleted.connect(lambda: QMessageBox.information(
self,
"Hotovo",
"Hesláře byly úspěšně aktualizovány."
))
# Message boxes are parented to the main window, not to this dialog
# the dialog may already be closed (and its C++ object deleted)
# by the time the minute-long task finishes.
parent_win = iface.mainWindow() if iface else None
def _cleanup():
if task in _ACTIVE_TASKS:
_ACTIVE_TASKS.remove(task)
try:
self.btn_update.setEnabled(True)
except RuntimeError:
pass # dialog already closed
def on_completed():
_cleanup()
QMessageBox.information(
parent_win,
"Hotovo",
"Hesláře byly úspěšně aktualizovány."
)
# Show the exact error if the task fails
def on_error():
_cleanup()
if task.exception:
# This will show exactly what went wrong (e.g. PermissionError)
msg = (
@@ -393,8 +478,9 @@ class AmcrFilterDialog(QDialog):
)
else:
msg = "Aktualizace byla zrušena uživatelem."
QMessageBox.warning(self, "Chyba / Zrušeno", msg)
QMessageBox.warning(parent_win, "Chyba / Zrušeno", msg)
task.taskCompleted.connect(on_completed)
task.taskTerminated.connect(on_error)
QgsApplication.taskManager().addTask(task)
@@ -428,22 +514,25 @@ class AmcrFilterDialog(QDialog):
if self.typ_dat == "akce":
if self.chk_posevidence.isChecked():
filters['posevidence'] = 'true'
if self.selection_cache['organizace']:
filters['f_organizace'] = self.selection_cache['organizace']
if self.selection_cache['typ_akce']:
filters['f_typ_vyzkumu'] = self.selection_cache['typ_akce']
if self.selection_cache['vedouci']:
filters['f_vedouci'] = self.selection_cache['vedouci']
if self.chk_proj_akce.isChecked():
filters['proj_akce'] = 'true'
if self.typ_dat == "lokalita":
if self.selection_cache['typ_lokality']:
filters['f_typ_lokality'] = self.selection_cache['typ_lokality']
if self.selection_cache['druh_lokality']:
filters['f_druh_lokality'] = self.selection_cache['druh_lokality']
if self.selection_cache['jistota']:
filters['f_jistota'] = self.selection_cache['jistota']
if self.selection_cache['lokalita_zachovalost']:
filters['f_lokalita_zachovalost'] = self.selection_cache['lokalita_zachovalost']
if self.selection_cache['typ_akce']:
filters['f_typ_vyzkumu'] = self.selection_cache['typ_akce']
if self.selection_cache['vedouci']:
filters['f_vedouci'] = self.selection_cache['vedouci']
if self.selection_cache['organizace']:
filters['f_organizace'] = self.selection_cache['organizace']
if self.selection_cache['typ_lokality']:
filters['f_typ_lokality'] = self.selection_cache['typ_lokality']
if self.selection_cache['druh_lokality']:
filters['f_druh_lokality'] = self.selection_cache['druh_lokality']
if self.selection_cache['jistota']:
filters['f_jistota'] = self.selection_cache['jistota']
if self.selection_cache['lokalita_zachovalost']:
filters['f_lokalita_zachovalost'] = self.selection_cache['lokalita_zachovalost']
return filters
@@ -602,6 +691,39 @@ class LoginDialog(QDialog):
return True
def _verify_credentials(self, username: str, password: str) -> bool:
"""
Verify the credentials against the API before saving them.
Returns True if they should be stored: either the login succeeded,
or the server was unreachable and the user chose to keep them
unverified. Wrong credentials are never stored.
"""
# Lazy import to avoid an import cycle
# (amcr_tools imports LoginDialog lazily as well)
from . import amcr_tools
if amcr_tools.login_to_api(username, password):
return True
if amcr_tools.LAST_LOGIN_ERROR == 'network':
answer = QMessageBox.question(
self,
"Server nedostupný",
"Přihlašovací údaje se nepodařilo ověřit server AMČR "
"je nedostupný.\nChcete je přesto uložit (neověřené)?",
QMessageBox.StandardButton.Yes
| QMessageBox.StandardButton.No
)
return answer == QMessageBox.StandardButton.Yes
QMessageBox.warning(
self,
"Neplatné přihlašovací údaje",
"Přihlášení se nezdařilo zkontrolujte e-mail a heslo.\n"
"Údaje nebyly uloženy."
)
return False
# ------------------------------------------------------------------
# Button actions
# ------------------------------------------------------------------
@@ -628,6 +750,11 @@ class LoginDialog(QDialog):
if ok:
if not self._ensure_master_password():
return
# Verify the new username against the stored password
if not self._verify_credentials(
username, cfg.config("password", "")
):
return
cfg.setConfig("username", username)
auth_mgr.updateAuthenticationConfig(cfg)
self.accept()
@@ -637,6 +764,11 @@ class LoginDialog(QDialog):
QMessageBox.warning(self, "Chybí údaje", "Vyplňte prosím heslo.")
return
# Verify before prompting for the master password wrong
# credentials must never reach the Authentication Manager
if not self._verify_credentials(username, password):
return
if not self._ensure_master_password():
return
+443 -265
View File
@@ -17,6 +17,26 @@ TRANSLATIONS = {}
# None = not logged in (anonymous access)
AMCR_SESSION: requests.Session | None = None
# Reason of the last failed login: 'auth' (wrong credentials),
# 'network' (server unreachable / invalid response) or None
LAST_LOGIN_ERROR: str | None = None
# Re-entrancy guard: the download runs in the main thread and pumps the
# event loop via processEvents(), so the user could otherwise start
# a second download while the first one is still running
_LOADING = False
archeologicky_zaznam_l = [
"akce",
"lokalita",
]
typ_dat_vocab = {
"akce": "Akce",
"lokalita": "Lokalita",
"samostatny_nalez": "PAS",
}
def _log(msg: str, level=Qgis.MessageLevel.Info):
"""
@@ -33,6 +53,9 @@ def login_to_api(username: str, password: str):
"""
login_url = "https://digiarchiv.aiscr.cz/api/user/login"
global LAST_LOGIN_ERROR
LAST_LOGIN_ERROR = None
_log(f"Přihlašuji uživatele: '{username}'")
if not username or not password:
@@ -40,6 +63,7 @@ def login_to_api(username: str, password: str):
"CHYBA: username nebo heslo je prázdné.",
Qgis.MessageLevel.Critical
)
LAST_LOGIN_ERROR = 'auth'
return None
session = requests.Session()
@@ -67,6 +91,7 @@ def login_to_api(username: str, password: str):
f"CHYBA přihlášení (API): {body['error']}",
Qgis.MessageLevel.Critical
)
LAST_LOGIN_ERROR = 'auth'
return None
_log("Přihlášení proběhlo úspěšně.")
@@ -75,12 +100,22 @@ def login_to_api(username: str, password: str):
return session
except requests.exceptions.HTTPError as e:
_log(f"CHYBA HTTP {e.response.status_code if e.response else '?'}: "
f"{e.response.text[:300] if e.response else 'žádná odpověď'}",
status = e.response.status_code if e.response is not None else None
_log(f"CHYBA HTTP {status if status else '?'}: "
f"{e.response.text[:300] if e.response is not None else 'žádná odpověď'}",
Qgis.MessageLevel.Critical)
LAST_LOGIN_ERROR = 'auth' if status in (401, 403) else 'network'
return None
except requests.exceptions.RequestException as e:
_log(f"CHYBA sítě: {e}", Qgis.MessageLevel.Critical)
LAST_LOGIN_ERROR = 'network'
return None
except ValueError:
# Server returned non-JSON (e.g. an HTML error page behind a proxy)
_log("CHYBA: server nevrátil platný JSON: "
f"{response.text[:300]}",
Qgis.MessageLevel.Critical)
LAST_LOGIN_ERROR = 'network'
return None
@@ -104,33 +139,40 @@ def _get_session() -> requests.Session | None:
return AMCR_SESSION
def _api_get(url, params, timeout=30) -> requests.Response:
def _api_get_json(url, params, timeout=30) -> dict:
"""
Performs a GET request. If the API signals an expired login,
re-authenticates once and retries.
Performs a GET request and returns the parsed JSON body.
If the API signals an expired login, re-authenticates once and retries.
The body is parsed exactly once (the auth check reuses it).
Raises ValueError if the server does not return valid JSON.
"""
global AMCR_SESSION
def _is_auth_error(resp: requests.Response) -> bool:
def _is_auth_error(resp: requests.Response, body) -> bool:
"""The API returns auth errors with status 200
the body must be checked."""
if resp.status_code == 401:
return True
try:
body = resp.json()
err = str(body.get("error", "")).lower()
return (
"unauthorized" in err
or "not logged" in err
or "session" in err
)
except Exception:
if not isinstance(body, dict):
return False
err = str(body.get("error", "")).lower()
return (
"unauthorized" in err
or "not logged" in err
or "session" in err
)
def _parse(resp):
try:
return resp.json()
except ValueError:
return None
session = _get_session()
resp = (session or requests).get(url, params=params, timeout=timeout)
body = _parse(resp)
if _is_auth_error(resp):
if _is_auth_error(resp, body):
_log("Session vypršela během stahování obnovuji přihlášení...",
Qgis.MessageLevel.Warning)
AMCR_SESSION = None # Invalidate the old session
@@ -140,6 +182,7 @@ def _api_get(url, params, timeout=30) -> requests.Response:
AMCR_SESSION = login_to_api(username, password)
if AMCR_SESSION:
resp = AMCR_SESSION.get(url, params=params, timeout=timeout)
body = _parse(resp)
else:
_log("Opakované přihlášení selhalo.",
Qgis.MessageLevel.Critical)
@@ -147,7 +190,11 @@ def _api_get(url, params, timeout=30) -> requests.Response:
_log("Přihlašovací údaje nejsou uloženy pokračuji anonymně.",
Qgis.MessageLevel.Warning)
return resp
if body is None:
raise ValueError(
f"API nevrátilo platný JSON (HTTP {resp.status_code})"
)
return body
def load_translations():
@@ -165,7 +212,10 @@ def load_translations():
if r.status_code == 200:
TRANSLATIONS = r.json()
except Exception as e:
print(f"Error downloading vocabulary: {e}")
QgsMessageLog.logMessage(
f"Error downloading vocabulary: {e}",
"AMČR", Qgis.MessageLevel.Warning
)
def tr_code(code):
@@ -179,11 +229,12 @@ def tr_code(code):
def komp_projde_filtrem(komp, filter_areal, filter_datace, filters):
areal_id = komp.get('komponenta_areal', {}).get('id', "")
# 'or {}' the key may be present with a None value
areal_id = (komp.get('komponenta_areal') or {}).get('id', "")
if filter_areal and areal_id not in filters.get('f_areal', []):
return False
obdobi_id = komp.get('komponenta_obdobi', {}).get('id', "")
obdobi_id = (komp.get('komponenta_obdobi') or {}).get('id', "")
if filter_datace and obdobi_id not in filters.get('f_obdobi', []):
return False
@@ -198,6 +249,16 @@ def load_amcr_data(canvas, bb, filters=None,
2. Fetches metadata and geometries from API
3. Creates QGIS memory layers and populates them with features
"""
global _LOADING
if _LOADING:
iface.messageBar().pushMessage(
"AMCR",
"Stahování již probíhá, počkejte na jeho dokončení.",
level=Qgis.MessageLevel.Warning
)
return
_LOADING = True
load_translations()
# --- 1. COORDINATE TRANSFORMATION ---
@@ -233,7 +294,7 @@ def load_amcr_data(canvas, bb, filters=None,
base_params = {
"mapa": "true",
"sort": "ident_cely asc",
"entity": typ_dat
"entity": typ_dat,
}
# Restrict search to map window if requested
@@ -257,6 +318,7 @@ def load_amcr_data(canvas, bb, filters=None,
MAX_LIMIT = 20000 # Safety limit to prevent QGIS from freezing
seen_ids = set()
fetched_total = 0 # All downloaded records incl. duplicates
target_pian_ids_count = 0
# Check if we should skip negative results based on filter
@@ -266,10 +328,20 @@ def load_amcr_data(canvas, bb, filters=None,
else False
)
only_projektove_akce = (
filters.get('proj_akce') == '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
# Set when a network error interrupts the download the user
# gets an explicit error/warning instead of a silent partial result
network_error = False
# --- API PAGINATION LOOP ---
while True:
base_params['rows'] = BATCH_DOCS
@@ -279,8 +351,7 @@ def load_amcr_data(canvas, bb, filters=None,
del base_params['page']
try:
resp_docs = _api_get(url, params=base_params, timeout=30)
resp_json = resp_docs.json()
resp_json = _api_get_json(url, params=base_params, timeout=30)
data = resp_json.get('response', {})
batch_docs = data.get('docs', [])
num_found = data.get('numFound', 0)
@@ -288,6 +359,8 @@ def load_amcr_data(canvas, bb, filters=None,
if not batch_docs:
break
fetched_total += len(batch_docs)
# Filter out duplicates and append to main list
new_docs = []
for d in batch_docs:
@@ -297,12 +370,16 @@ def load_amcr_data(canvas, bb, filters=None,
new_docs.append(d)
docs.extend(new_docs)
print(
QgsMessageLog.logMessage(
f"Strana {current_page} stažena. "
f"Celkem záznamů: {len(docs)} / {num_found}"
f"Celkem záznamů: {len(docs)} / {num_found}",
"AMČR", Qgis.MessageLevel.Info
)
if len(docs) >= num_found:
# Compare downloaded (not unique) records against numFound
# pages full of duplicates would otherwise trigger
# needless extra requests
if fetched_total >= num_found:
break
if len(docs) >= MAX_LIMIT:
iface.messageBar().pushMessage(
@@ -315,9 +392,29 @@ def load_amcr_data(canvas, bb, filters=None,
current_page += 1
QApplication.processEvents() # Keep UI responsive
except Exception as e:
print(f"Chyba při stránkování na straně {current_page}: {e}")
except requests.exceptions.RequestException as e:
network_error = True
QgsMessageLog.logMessage(
f"Chyba sítě při stránkování na straně "
f"{current_page}: {e}",
"AMČR", Qgis.MessageLevel.Critical
)
break
except Exception as e:
QgsMessageLog.logMessage(
f"Chyba při stránkování na straně {current_page}: {e}",
"AMČR", Qgis.MessageLevel.Warning
)
break
if network_error and not docs:
iface.messageBar().pushMessage(
"AMCR",
"Stahování selhalo: chyba sítě. "
"Zkontrolujte připojení k internetu.",
level=Qgis.MessageLevel.Critical
)
return
if not docs:
iface.messageBar().pushMessage(
@@ -354,255 +451,289 @@ def load_amcr_data(canvas, bb, filters=None,
return ", ".join([str(x) for x in val if x])
# Process each downloaded metadata record
for doc in docs:
piani = doc.get('az_dj_pian', [])
if not piani:
continue
actions_with_geom += 1
# Extract protected fields
az_chranene = doc.get('az_chranene_udaje', {})
chranene = (
doc.get('akce_chranene_udaje')
or doc.get('lokalita_chranene_udaje')
or {}
)
# Format additional cadastral areas from nested dicts
dalsi_kat = az_chranene.get('dalsi_katastr', [])
dalsi_kat_str = ""
if isinstance(dalsi_kat, list):
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])
lokalizace = chranene.get('lokalizace_okolnosti', "")
lokalita_nazev = chranene.get('nazev', "")
lokalita_popis = chranene.get('popis', "")
# Core metadata structure
meta = {
"ident_cely": doc.get('ident_cely', ''),
"az_okres": g(doc, 'az_okres'),
"katastr": g_list(doc, 'katastr'),
"dalsi_katastr": dalsi_kat_str,
"pristupnost": g(doc, 'pristupnost'),
"loc": g_list(doc, 'loc')
}
# Add entity-specific metadata
if typ_dat == "akce":
meta.update({
"akce_hlavni_vedouci": g(
doc,
'akce_hlavni_vedouci'
),
"akce_organizace": tr_code(g(
doc,
'akce_organizace'
)),
"akce_specifikace_data": tr_code(g(
doc,
'akce_specifikace_data'
)),
"akce_datum_zahajeni": g(
doc,
'akce_datum_zahajeni'
),
"akce_datum_ukonceni": g(
doc,
'akce_datum_ukonceni'
),
"akce_hlavni_typ": tr_code(g(
doc,
'akce_hlavni_typ'
)),
"akce_vedlejsi_typ": g_list(
doc,
'akce_vedlejsi_typ',
translate=True
),
"lokalizace_okolnosti": (
str(lokalizace)
if lokalizace
else ""
),
"akce_je_nz": (
"Ano"
if doc.get('akce_je_nz') is True
else "Ne"
),
})
elif typ_dat == "lokalita":
meta.update({
"lokalita_nazev": lokalita_nazev,
"lokalita_popis": lokalita_popis,
"lokalita_zachovalost": tr_code(g(
doc,
'lokalita_zachovalost'
)),
"lokalita_druh": tr_code(g(
doc,
'lokalita_druh'
)),
"lokalita_typ": tr_code(g(
doc,
'lokalita_typ_lokality'
)),
})
# Documentation units (DJ) within the record
djs = doc.get('az_dokumentacni_jednotka', [])
for dj in djs:
# Skip negative evidence units if requested
if skip_negativni and dj.get('dj_negativni_jednotka') is True:
if typ_dat in archeologicky_zaznam_l:
for doc in docs:
piani = doc.get('az_dj_pian', [])
if not piani:
continue
komps = dj.get('dj_komponenta', [])
if only_projektove_akce and not doc.get("akce_projekt", False):
continue
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
actions_with_geom += 1
dj_id = dj.get('ident_cely')
dj_typ = dj.get('dj_typ')
# Extract protected fields ('or {}' key may hold None)
az_chranene = doc.get('az_chranene_udaje') or {}
chranene = (
doc.get('akce_chranene_udaje')
or doc.get('lokalita_chranene_udaje')
or {}
)
# Merge shared metadata with documentation unit-specific fields
dj_meta = {
**meta,
'dj_id': dj_id,
'dj_typ_value': dj_typ.get('value') if dj_typ else "",
'dj_negativni': (
"Negativní"
if dj.get('dj_negativni_jednotka') is True
else "Pozitivní"
)
# Format additional cadastral areas from nested dicts
dalsi_kat = az_chranene.get('dalsi_katastr', [])
dalsi_kat_str = ""
if isinstance(dalsi_kat, list):
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])
lokalizace = chranene.get('lokalizace_okolnosti', "")
lokalita_nazev = chranene.get('nazev', "")
lokalita_popis = chranene.get('popis', "")
# Core metadata structure
meta = {
"ident_cely": doc.get('ident_cely', ''),
"az_okres": g(doc, 'az_okres'),
"katastr": g_list(doc, 'katastr'),
"dalsi_katastr": dalsi_kat_str,
"pristupnost": g(doc, 'pristupnost'),
"loc": g_list(doc, 'loc'),
}
# Link Documentation Unit to Geometry (PIAN)
dj_pian = dj.get('dj_pian')
if dj_pian:
dj_pian_value = dj_pian.get('id')
if dj_pian_value:
target_pian_ids.add(dj_pian_value)
if dj_pian_value not in pian_lookup:
pian_lookup[dj_pian_value] = []
# Add entity-specific metadata
if typ_dat == "akce":
meta.update({
"akce_hlavni_vedouci": g(
doc,
'akce_hlavni_vedouci'
),
"akce_organizace": tr_code(g(
doc,
'akce_organizace'
)),
"akce_specifikace_data": tr_code(g(
doc,
'akce_specifikace_data'
)),
"akce_datum_zahajeni": g(
doc,
'akce_datum_zahajeni'
),
"akce_datum_ukonceni": g(
doc,
'akce_datum_ukonceni'
),
"akce_hlavni_typ": tr_code(g(
doc,
'akce_hlavni_typ'
)),
"akce_vedlejsi_typ": g_list(
doc,
'akce_vedlejsi_typ',
translate=True
),
"lokalizace_okolnosti": (
str(lokalizace)
if lokalizace
else ""
),
"akce_je_nz": (
"Ano"
if doc.get('akce_je_nz') is True
else "Ne"
),
"projekt": g(
doc,
'akce_projekt',
""
),
})
if komponenty == "true":
# One feature per component
# all data on a single row, no relations needed
if komps:
for komp in komps:
if not komp_projde_filtrem(
komp, filter_areal,
filter_datace, filters
):
elif typ_dat == "lokalita":
meta.update({
"lokalita_nazev": lokalita_nazev,
"lokalita_popis": lokalita_popis,
"lokalita_zachovalost": tr_code(g(
doc,
'lokalita_zachovalost'
)),
"lokalita_druh": tr_code(g(
doc,
'lokalita_druh'
)),
"lokalita_typ": tr_code(g(
doc,
'lokalita_typ_lokality'
)),
})
# Documentation units (DJ) within the record
djs = doc.get('az_dokumentacni_jednotka', [])
for dj in djs:
# Skip negative evidence units if requested
if skip_negativni and dj.get('dj_negativni_jednotka') is True:
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_typ = dj.get('dj_typ')
# Merge shared metadata with documentation unit-specific fields
dj_meta = {
**meta,
'dj_id': dj_id,
'dj_typ_value': dj_typ.get('value') if dj_typ else "",
'dj_negativni': (
"Negativní"
if dj.get('dj_negativni_jednotka') is True
else "Pozitivní"
)
}
# Link Documentation Unit to Geometry (PIAN)
dj_pian = dj.get('dj_pian')
if dj_pian:
dj_pian_value = dj_pian.get('id')
if dj_pian_value:
target_pian_ids.add(dj_pian_value)
if dj_pian_value not in pian_lookup:
pian_lookup[dj_pian_value] = []
if komponenty == "true":
# One feature per component
# all data on a single row, no relations needed
if komps:
for komp in komps:
if not komp_projde_filtrem(
komp, filter_areal,
filter_datace, filters
):
continue
komp_meta = {
**dj_meta,
'komponenta_id': komp.get(
'ident_cely',
""
),
'komponenta_areal': (
komp.get('komponenta_areal')
or {}
).get('value', ""),
'komponenta_obdobi': (
komp.get('komponenta_obdobi')
or {}
).get('value', ""),
}
pian_lookup[dj_pian_value].append(komp_meta)
target_pian_ids_count += 1
else:
# DJ without components — still include
# with empty component fields
if filter_areal or filter_datace:
continue
komp_meta = {
empty_meta = {
**dj_meta,
'komponenta_id': komp.get(
'ident_cely',
""
),
'komponenta_areal': komp.get(
'komponenta_areal',
{}
).get('value', ""),
'komponenta_obdobi': komp.get(
'komponenta_obdobi',
{}
).get('value', ""),
'komponenta_id': "",
'komponenta_areal': "",
'komponenta_obdobi': "",
}
pian_lookup[dj_pian_value].append(komp_meta)
pian_lookup[dj_pian_value].append(empty_meta)
target_pian_ids_count += 1
else:
# DJ without components — still include
# with empty component fields
if filter_areal or filter_datace:
continue
empty_meta = {
**dj_meta,
'komponenta_id': "",
'komponenta_areal': "",
'komponenta_obdobi': "",
}
pian_lookup[dj_pian_value].append(empty_meta)
target_pian_ids_count += 1
else:
target_pian_ids_count += 1
pian_lookup[dj_pian_value].append(dj_meta)
pian_lookup[dj_pian_value].append(dj_meta)
if not target_pian_ids:
iface.messageBar().pushMessage(
"AMCR",
f"Nalezeno {len(docs)} záznamů, ale žádný nemá geometrii.",
level=Qgis.MessageLevel.Warning
)
return
# ==========================================
# C) GEOMETRY FETCHING (PIAN)
# ==========================================
ids_list = list(target_pian_ids)
total_pians = len(ids_list)
docs_pian = []
# Geometry requests are batch-processed
# to stay under URL length limits:
BATCH_PIAN = 200
if not target_pian_ids:
iface.messageBar().pushMessage(
"AMCR",
f"Nalezeno {len(docs)} záznamů, ale žádný nemá geometrii.",
level=Qgis.MessageLevel.Warning
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
)
return
# ==========================================
# C) GEOMETRY FETCHING (PIAN)
# ==========================================
ids_list = list(target_pian_ids)
total_pians = len(ids_list)
docs_pian = []
# Geometry requests are batch-processed
# to stay under URL length limits:
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
)
for i in range(0, total_pians, BATCH_PIAN):
batch = ids_list[i: i + BATCH_PIAN]
or_query = " OR ".join(batch)
fq_pian = f"ident_cely:({or_query})"
fl_pian = [
"ident_cely", "pian_typ",
"pian_chranene_udaje", "pian_presnost"
]
params_pian = {
"mapa": "true",
"entity": "pian",
"q": fq_pian,
"rows": len(batch),
"fl": ",".join(fl_pian),
}
try:
QApplication.processEvents()
r_json = _api_get_json(url, params=params_pian, timeout=15)
docs_pian.extend(r_json.get('response', {}).get('docs', []))
except requests.exceptions.RequestException as e:
# Network is down stop immediately instead of
# uselessly retrying every remaining batch
network_error = True
QgsMessageLog.logMessage(
f"Chyba sítě při stahování geometrií PIAN: {e}",
"AMČR", Qgis.MessageLevel.Critical
)
break
except Exception as e:
QgsMessageLog.logMessage(
f"Chyba PIAN: {e}",
"AMČR", Qgis.MessageLevel.Warning
)
for i in range(0, total_pians, BATCH_PIAN):
batch = ids_list[i: i + BATCH_PIAN]
or_query = " OR ".join(batch)
fq_pian = f"ident_cely:({or_query})"
elif typ_dat == "samostatny_nalez":
for doc in docs:
loc = g(doc, "loc", [])
if not loc:
continue
params_pian = {
"mapa": "true",
"entity": "pian",
"q": fq_pian,
"rows": len(batch),
"fl": ",".join(fl_pian)
}
try:
QApplication.processEvents()
r_pian = _api_get(url, params=params_pian, timeout=15)
batch_docs = r_pian.json().get('response', {}).get('docs', [])
docs_pian.extend(batch_docs)
except Exception as e:
print(f"Chyba PIAN: {e}")
actions_with_geom += 1
sn_chranene = doc.get("samostatny_nalez_chranene_udaje") or {}
# ==========================================
# D) LAYER CREATION (QGIS Memory Layers)
# ==========================================
archeologicky_zaznam = "Akce" if typ_dat == "akce" else "Lokalita"
archeologicky_zaznam = typ_dat_vocab[typ_dat]
# Initialize three layers for different geometry types (S-JTSK CRS)
vl_poly = QgsVectorLayer(
@@ -634,7 +765,7 @@ def load_amcr_data(canvas, bb, filters=None,
QgsField("odkaz_do_digiarchivu", QMetaType.Type.QString),
QgsField("okres", QMetaType.Type.QString),
QgsField("katastr", QMetaType.Type.QString),
QgsField("dalsi_katastry", QMetaType.Type.QString)
QgsField("dalsi_katastry", QMetaType.Type.QString),
]
# Extend table based on data type
@@ -650,6 +781,7 @@ def load_amcr_data(canvas, bb, filters=None,
QgsField("vedlejsi_typ", QMetaType.Type.QString),
QgsField("zjisteni", QMetaType.Type.QString),
QgsField("nahrazuje_NZ", QMetaType.Type.QString),
QgsField("projekt", QMetaType.Type.QString),
]
elif typ_dat == "lokalita":
cols += [
@@ -657,7 +789,7 @@ def load_amcr_data(canvas, bb, filters=None,
QgsField("popis_lokality", QMetaType.Type.QString),
QgsField("typ_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))
@@ -693,6 +825,7 @@ def load_amcr_data(canvas, bb, filters=None,
"komponenta": "Komponenta",
"komponenta_areal": "Areál",
"komponenta_obdobi": "Období",
"projekt": "Projekt",
}
if komponenty == "true":
@@ -713,6 +846,14 @@ def load_amcr_data(canvas, bb, filters=None,
# Lists to hold features before batch-adding to layers
feats_p, feats_l, feats_pt = [], [], []
# Transform for PIANs that only provide WGS-84 geometry (geom_wkt)
# the target layers are in S-JTSK (EPSG:5514)
xform_wgs_to_sjtsk = QgsCoordinateTransform(
QgsCoordinateReferenceSystem("EPSG:4326"),
QgsCoordinateReferenceSystem("EPSG:5514"),
QgsProject.instance()
)
# --- FEATURE POPULATION ---
for doc in docs_pian:
try:
@@ -733,25 +874,46 @@ def load_amcr_data(canvas, bb, filters=None,
)
wkt = None
wkt_is_wgs = False
if jdata.get('geom_sjtsk_wkt'):
wkt = jdata.get('geom_sjtsk_wkt', {}).get('value')
elif jdata.get('geom_wkt'):
# Fallback geometry is in WGS-84 and must be
# transformed to S-JTSK before use
wkt = jdata.get('geom_wkt', {}).get('value')
wkt_is_wgs = True
pian_presnost = tr_code(str(doc.get('pian_presnost', '')))
pian_typ = tr_code(str(doc.get('pian_typ', '')))
# The API may return the value as a single-item list
# normalize before comparing against filter codes
raw_presnost = doc.get('pian_presnost', '')
if isinstance(raw_presnost, list):
raw_presnost = raw_presnost[0] if raw_presnost else ''
raw_typ = doc.get('pian_typ', '')
if isinstance(raw_typ, list):
raw_typ = raw_typ[0] if raw_typ else ''
pian_presnost = tr_code(str(raw_presnost))
pian_typ = tr_code(str(raw_typ))
# Final precision filter check
if (
filters
and filters.get('f_pian_presnost')
and doc.get('pian_presnost')
and str(raw_presnost)
not in filters.get('f_pian_presnost')
):
continue
if wkt:
geom = QgsGeometry.fromWkt(wkt)
if geom.isNull():
continue
if wkt_is_wgs:
geom.transform(xform_wgs_to_sjtsk)
if not geom.isGeosValid():
# Try to repair (e.g. self-intersections)
# instead of silently dropping the feature
geom = geom.makeValid()
if geom.isGeosValid():
t = geom.type()
target_list = None
@@ -784,7 +946,7 @@ def load_amcr_data(canvas, bb, filters=None,
+ meta['ident_cely'],
meta['az_okres'],
meta['katastr'],
meta['dalsi_katastr']
meta['dalsi_katastr'],
]
if is_akce:
atributy.extend([
@@ -797,7 +959,8 @@ def load_amcr_data(canvas, bb, filters=None,
meta['akce_hlavni_typ'],
meta['akce_vedlejsi_typ'],
meta['dj_negativni'],
meta['akce_je_nz']
meta['akce_je_nz'],
meta['projekt'],
])
else:
atributy.extend([
@@ -805,7 +968,7 @@ def load_amcr_data(canvas, bb, filters=None,
meta['lokalita_popis'],
meta['lokalita_typ'],
meta['lokalita_druh'],
meta['lokalita_zachovalost']
meta['lokalita_zachovalost'],
])
atributy.append(meta['pristupnost'])
@@ -821,8 +984,10 @@ def load_amcr_data(canvas, bb, filters=None,
target_list.append(feat)
except Exception as ex:
print(f"Chyba při tvorbě feature: {ex}")
pass
QgsMessageLog.logMessage(
f"Chyba při tvorbě feature: {ex}",
"AMČR", Qgis.MessageLevel.Warning
)
# --- ADDING TO QGIS INTERFACE ---
proj = QgsProject.instance()
@@ -833,15 +998,27 @@ def load_amcr_data(canvas, bb, filters=None,
(feats_pt, vl_point, "Body"),
]
for f, l, n in layers_to_process:
for f, L, n in layers_to_process:
if f:
l.dataProvider().addFeatures(f)
l.updateExtents()
l.setName(f"AMCR_{archeologicky_zaznam}_{n}")
proj.addMapLayer(l)
L.dataProvider().addFeatures(f)
L.updateExtents()
L.setName(f"AMCR_{archeologicky_zaznam}_{n}")
proj.addMapLayer(L)
added += len(f)
if added > 0:
if network_error:
iface.messageBar().pushMessage(
"AMCR",
"Stahování bylo přerušeno chybou sítě "
f"výsledek je neúplný (vykresleno {added} prvků). "
"Zkontrolujte připojení a spusťte stahování znovu.",
level=(
Qgis.MessageLevel.Warning
if added > 0
else Qgis.MessageLevel.Critical
)
)
elif added > 0:
iface.messageBar().pushMessage(
"AMCR",
f"Hotovo. Záznamů: {len(docs)} (s geom: {actions_with_geom}). "
@@ -862,5 +1039,6 @@ def load_amcr_data(canvas, bb, filters=None,
level=Qgis.MessageLevel.Critical
)
finally:
# Always restore cursor, even after failure
# Always restore cursor and release the guard, even after failure
_LOADING = False
QApplication.restoreOverrideCursor()
+14 -3
View File
@@ -6,7 +6,6 @@ from qgis.core import Qgis
from .amcr_tools import load_amcr_data, login_to_api
from .amcr_dialog import AmcrFilterDialog, LoginDialog
from .resources import *
import os.path
@@ -24,8 +23,9 @@ class AmcrViewer:
self.iface = iface
self.plugin_dir = os.path.dirname(__file__)
# Determine the user's locale to load appropriate translation files
locale = QSettings().value('locale/userLocale')[0:2]
# Determine the user's locale to load appropriate translation files.
# The setting may be missing (None) on a fresh QGIS install.
locale = str(QSettings().value('locale/userLocale') or 'en')[0:2]
locale_path = os.path.join(
self.plugin_dir,
'i18n',
@@ -90,6 +90,7 @@ class AmcrViewer:
"""
# Define paths for action-specific icons
icon_akce_path = os.path.join(self.plugin_dir, 'akce.png')
icon_pas_path = os.path.join(self.plugin_dir, 'akce.png')
icon_lokality_path = os.path.join(self.plugin_dir, 'lokality.png')
icon_amcr_help_path = os.path.join(self.plugin_dir, 'amcr-help.png')
@@ -109,6 +110,16 @@ class AmcrViewer:
)
self.plugin_menu.addAction(self.action_download_akce)
self.action_download_pas = self.add_action(
icon_path=icon_pas_path,
text=self.tr(u'Stáhnout data samostatných nálezů | AMČR Viewer'),
callback=lambda checked=False: self.run_download('samostatny_nalez'),
parent=self.iface.mainWindow(),
add_to_menu=False,
add_to_toolbar=False
)
self.plugin_menu.addAction(self.action_download_pas)
self.action_download_lokality = self.add_action(
icon_path=icon_lokality_path,
text=self.tr(u'Stáhnout data lokalit | AMČR Viewer'),
+16835 -21873
View File
File diff suppressed because it is too large Load Diff
+7 -3
View File
@@ -5,10 +5,10 @@
[general]
name=AMČR Viewer
qgisMinimumVersion=3.4.0
qgisMinimumVersion=3.44.0
qgisMaximumVersion=4.99.0
description=Viewing and downloading the AMČR data.
version=2.0.0
version=2.0.2
author=David Spáčil
email=spacil@arub.cz
@@ -26,6 +26,10 @@ 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.2 (2026-06-13)
* Plugin-wide fixes and optimalizations (details https://github.com/ARUP-CAS/aiscr-qgis-amcr-viewer/pull/49)
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
@@ -45,7 +49,7 @@ tags=python,AMCR,AIS CR,archaeology,PIAN,AMČR,archeologie
homepage=https://amcr-help.aiscr.cz/digiarchiv/qgis-viewer.html
category=Vector
icon=download.png
icon=akce.png
# experimental flag
experimental=False