Compare commits

..
18 Commits
Author SHA1 Message Date
david-spacil 39dac12bad Merge pull request #61 from ARUP-CAS/fix/qt6-kompatibilita
fix: kompatibilita s Qt6 / QGIS 4 a úklid lintů
2026-09-01 23:22:56 +02:00
david-spacil b708930ca2 update metadata 2026-09-01 23:21:04 +02:00
david-spacil 3f7839c818 fix: znovu odstraněn BOM a zalomen dlouhý import
Commit 9aba283 vrátil UTF-8 BOM do amcr_dialog.py a amcr_tools.py –
patrně ho doplnil editor při uložení. Oficiální pyqgis4-checker na nich
kvůli tomu znovu padal na SyntaxError, tedy přesně to, co d398d2c
opravoval.

Zároveň zalomen sloučený import z qgis.core v amcr_dialog.py, který měl
87 znaků.
2026-09-01 23:15:28 +02:00
david-spacil 9aba28317a chore: pořadí importů 2026-09-01 23:13:18 +02:00
david-spacil 048ffe4e2a chore: úklid nálezů z lintů
Bez změny chování. Kontroly, které pouští plugins.qgis.org (flake8),
plus ruff.

flake8: odstraněny koncové bílé znaky, srovnány nadbytečné prázdné řádky
a zalomeno 12 řádků přes 79 znaků. Hlášku o HTTP chybě v login_to_api()
si nešlo jen zalomit – tělo odpovědi je nově v proměnné `telo`, výsledný
text je stejný.

ruff: odstraněny zbytečné prefixy u'' u překládaných řetězců (6×),
.format() nahrazen f-stringem, str() uvnitř f-stringu převedeno na !s,
`k in dict.keys()` na `k in dict`, prázdný komentář; nepoužitá rozbalená
proměnná `existing_cfg` nahrazena `_`.

Vědomě neřešeno:
- resources.py (5 zbylých nálezů flake8) je generovaný výstup pyrcc,
  nese hlavičku "All changes made in this file will be lost", není nikde
  importovaný a v repu k němu chybí zdrojový .qrc.
- ruff BLE001 (10×) hlásí `except Exception`. Ty jsou v pluginu záměrné:
  drží pád mimo QGIS a chybu ukážou v liště zpráv. Změna by byla zásah
  do chování, ne úklid.
- ruff SIM103 (2×) navrhuje sloučit strážní podmínky do jednoho výrazu.
  Obě místa mají ke každé větvi vysvětlující komentář, sloučením by se
  staly hůř čitelnými.
- ruff UP009 a I001 (coding hlavička a řazení importů) jsou celoprojektová
  konvence; jejich změna patří do samostatného rozhodnutí, ne sem.

Ověřeno v QGIS 3.44.13 (Qt5) i QGIS 4.2.1 (Qt6): importy, vytvoření tasku
i všechny tři filtrační dialogy fungují shodně. Bandit, detect-secrets
i pyqgis4-checker hlásí nula nálezů.
2026-09-01 23:10:33 +02:00
david-spacil d398d2cd1b fix: kompatibilita s Qt6 / QGIS 4
Oficiální kontrola pyqgis4-checker (skript pyqt5_to_pyqt6.py z QGISu)
hlásila u pluginu 13 nekompatibilit a tři soubory vůbec nepřečetla.

Odstraněn UTF-8 BOM z amcr_codelists.py, amcr_dialog.py a amcr_tools.py.
Checker čte zdroják jako UTF-8 bez utf-8-sig a na BOM padá na
SyntaxError: invalid non-printable character U+FEFF, takže se ty soubory
nezkontrolovaly vůbec. Pythonu při běhu BOM nevadí, proto to nikdy
nevyskočilo. heslar.csv si BOM ponechává, tam je kvůli Excelu.

Enumy převedeny na plně kvalifikované tvary (13 míst):
Qgis.{Info,Warning,Critical} -> Qgis.MessageLevel.*,
QgsTask.CanCancel -> QgsTask.Flag.CanCancel,
QgsWkbTypes.*Geometry -> QgsWkbTypes.GeometryType.*Geometry.
Zkrácené tvary v QGIS 4.2.1 zatím fungují, ale kontrola je vytýká
a aliasy do budoucna mizí. Zbytek kódu už scoped tvary používal.

Do AGENTS.md doplněna sekce o kompatibilitě s Qt6 / QGIS 4 se závaznými
pravidly a příkazy na ověření.

Ověřeno spuštěním v QGIS 3.44.13 (Qt 5.15.18) i QGIS 4.2.1 (Qt 6.10.3):
importy, vytvoření tasku i všechny tři filtrační dialogy fungují shodně.
Log pyqgis4-checkeru je nově prázdný.
2026-09-01 23:05:02 +02:00
david-spacil 2218719f98 metadata update 2026-09-01 22:31:28 +02:00
david-spacil 6300919c36 update workflow kvůli nové podmínce s immutable releases 2026-09-01 22:27:00 +02:00
david-spacil 7cffcdb235 Merge pull request #60 from ARUP-CAS/version/v2.1.0
release: AMČR Viewer 2.1.0
2026-09-01 22:11:36 +02:00
david-spacil 0af4a0cd92 chore: verze 2.1.0 a changelog
Shrnuje změny sloučené do této větve oproti main: projektové akce (#53), úklid kódu (#54), stahování samostatných nálezů (#57), diagnostiku prázdných výsledků z API (#58) a filtrování podle data se scrollovatelným dialogem (#59).
2026-09-01 21:59:34 +02:00
david-spacil 9b25863031 chore: update přibaleného hesláře 2026-09-01 21:58:08 +02:00
david-spacil 9ec71c7ed3 feat: filtrování záznamů podle data (#59)
* fix: scrollovatelný filtrační dialog

Obsah dialogu přesahoval okno u všech tří typů dat – u akcí zabíral
přes 1280 px v okně vysokém 750 px, takže spodní filtry i tlačítka
byly nedostupné.

Filtry se nově scrollují. Tlačítka OK / Aktualizovat hesláře / Cancel
zůstávají mimo scrollovanou oblast, aby se kvůli potvrzení dialogu
nemuselo scrollovat na konec seznamu.

* feat: filtrování záznamů podle data

Přidává filtr na datum zahájení a ukončení u akcí a na datum nálezu
u samostatných nálezů (#56). Lokalita filtr nedostává – v jejím indexu
žádné datumové pole není, takže by šlo o ovládací prvek bez účinku.

API vyžaduje obě meze intervalu: jednostranný rozsah shodí server na
ArrayIndexOutOfBoundsException a hvězdičku nepřijímá. Prázdné pole se
proto nahrazuje zarážkou 0001-01-01 / 9999-12-31, což jsou shodou
okolností i výchozí meze QgsDateEdit. Blok s oběma poli prázdnými
neposílá do dotazu nic.

Obrácené rozmezí vrací API jako nula záznamů bez chybové hlášky, což
je k nerozeznání od skutečně prázdného výsledku – dialog ho proto
odmítne už při potvrzení a vypíše, kterých filtrů se to týká.
2026-09-01 21:50:55 +02:00
david-spacil 4ed99d73d9 fix: diagnostika prázdných výsledků z API (#58)
- logování odeslané URL a chyb z těla odpovědi v _api_get_json
- výpis filtrů z dialogu před zahájením stahování
- rozlišení chyby API od prázdného výsledku (api_error)
- dávky geometrií PIAN hlásí chybějící blok 'response'

API vrací chyby s HTTP 200 a klíčem 'error' místo bloku 'response'; resp_json.get('response', {}) z toho udělal prázdný výsledek, takže neplatný parametr, prázdné okno mapy i chyba serveru končily stejnou hláškou "Žádné záznamy nenalezeny".
2026-09-01 20:39:55 +02:00
david-spacil 4699dd9c95 feat: stahování samostatných nálezů (PAS) (#57)
* 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, ...)

* 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

* 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

* 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í)

* feature: globals pro PAS
založeny nové heslářové globals, pak přidány do funkce refresh_globals

* feature: nové heslářové globals + cache v dialogu

* feature: aktualizace hesláře

* fix: oprava typ_dat = "pas" na typ_dat = "samostatny_nalez"

* 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

* 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

* feature: čtení dat z JSON payload
získávání sn-specific dat, jako je nálezce, hloubka nálezu, ale i vlastní wkt, které není závislé na PIANu

* feature: párovací slovník metadat pro samostatné nálezy

* fix: Přístupnost přidána do hesláře aliasů

* fix: oprava chyb z dialogu znemožňujících stahování SN

* fix: předávání filtrů k samostatným nálezům z dialogu do "tools" skriptu

* feat: drobné změny
- escapování názvů vrstev (podtržítko místo mezery)
- `actions_with_geom` -> `entries_with_geom`
- čitelný `typ_dat` pro PAS: `PAS` -> `Samostatný nález`

* feat: dokončení smyčky na ukládání metadat k SN z docs

* feat: plnění vrstvy daty SN

* feat: update ikon pro samostatné nálezy a login
2026-09-01 20:28:31 +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
13 changed files with 4315 additions and 8391 deletions

No files matched your search

+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č. 1–3 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. -->
+20 -6
View File
@@ -1,15 +1,26 @@
name: Release QGIS Plugin
# Spouští se na push tagu, ne na publikaci releasu. Organizace má zapnuté
# immutable releases: k už publikovanému releasu nelze nic přiložit, příloha
# tedy musí vzniknout dřív, než se release zveřejní. Workflow proto založí
# koncept releasu i se ZIPem; text se dopisuje a publikuje ručně.
#
# Pozor: workflow se čte z commitu, na který tag ukazuje. Tag musí být
# založen až na commitu, který tento soubor obsahuje.
on:
release:
types: [published]
push:
tags:
- 'v*'
permissions:
contents: write
jobs:
build-and-release:
runs-on: ubuntu-latest
steps:
# 1. Stáhne kód z repozitáře
# 1. Stáhne kód z tagu, který běh spustil
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
@@ -20,11 +31,14 @@ jobs:
# -r = rekurzivně, -x = ignorovat skryté git soubory
zip -r amcr_viewer.zip amcr_viewer -x "*.git*"
# 3. Nahraje ZIP k Releasu
- name: Upload Release Asset
# 3. Založí koncept releasu i s přílohou
# Tagy s pomlčkou (v2.0.0-alpha.1) se označí jako pre-release.
- name: Create draft release with asset
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
if: startsWith(github.ref, 'refs/tags/')
with:
files: amcr_viewer.zip
draft: true
name: AMCR Viewer ${{ github.ref_name }}
prerelease: ${{ contains(github.ref_name, '-') }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+180
View File
@@ -0,0 +1,180 @@
# 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ů.
### Kompatibilita s Qt6 / QGIS 4
Plugin cílí na QGIS 3.44 i na QGIS 4 (`qgisMaximumVersion=4.99.0`), tedy na
Qt5 i Qt6 zároveň. **Tohle se drží rigorózně** – ne až před releasem, ale při
každé změně kódu. Chování obou větví se liší tiše: pod Qt5 projde i to, co
QGIS 4 odmítne, takže lokální „funguje mi to“ nic nedokazuje.
Závazná pravidla:
- **Nikdy neimportuj přímo z `PyQt5` ani z `PyQt6`.** Vždy přes shim
`qgis.PyQt.*`. Ten mimo jiné pod Qt6 přetahuje `QAction`, `QActionGroup`
a `QShortcut` z `QtGui`, takže import z `qgis.PyQt.QtWidgets` je správně.
- **Enumy vždy plně kvalifikované (scoped).** `Qgis.MessageLevel.Info`, ne
`Qgis.Info`; `QgsTask.Flag.CanCancel`, ne `QgsTask.CanCancel`;
`QgsWkbTypes.GeometryType.PointGeometry`, ne `QgsWkbTypes.PointGeometry`.
Totéž pro Qt: `Qt.CheckState.Checked`, `QDialogButtonBox.StandardButton.Ok`.
Zkrácené tvary sice v QGIS 4.2 zatím fungují, ale oficiální kontrola je
hlásí a do budoucna mizí.
- **Zdrojové `.py` soubory ukládej bez BOM.** Kontrolní skript čte soubor
jako UTF-8 bez `utf-8-sig` a na BOM spadne s
`SyntaxError: invalid non-printable character U+FEFF`, takže se takový
soubor **vůbec nezkontroluje**. (`codelists/heslar.csv` BOM mít smí, tam je
kvůli Excelu.)
- Nepoužívej API zrušená v Qt6: `exec_()`, `QRegExp`, `QDesktopWidget`,
`QApplication.desktop()`, `Qt.MidButton`, `QFontMetrics.width()`,
`setResizeMode`, atributy `AA_EnableHighDpiScaling` / `AA_UseHighDpiPixmaps`.
- `supportsQt6=True` v `metadata.txt` **nepatří** – bylo zrušeno; o zařazení
mezi „QGIS 4 Ready“ rozhoduje rozsah `qgisMinimumVersion` až
`qgisMaximumVersion`.
Ověření před PR, který mění Python kód:
```sh
# oficiální kontrola, kterou pouští i plugins.qgis.org (pyqgis4-checker)
docker run --rm --pull always --user $(id -u):$(id -g) \
--workdir /workspace/ -v "$(pwd):/workspace/" \
ghcr.io/qgis/pyqgis4-checker:main-ubuntu \
pyqt5_to_pyqt6.py --dry_run --logfile /workspace/pyqt6_checker.log .
```
Prázdný log = čisté. Kontrola je na plugins.qgis.org informativní
(neblokuje schválení), ale nález znamená, že plugin v QGIS 4 dříve nebo
později přestane fungovat.
Když je po ruce QGIS 4 (např. flatpak `org.qgis.qgis`), ověř navíc, že se
plugin pod Qt6 opravdu načte:
```sh
flatpak run --command=sh org.qgis.qgis -c \
'PYTHONPATH=/app/share/qgis/python python3 -c "import qgis.core"'
```
## 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 spouští **pushnutím tagu `vX.Y.Z`**, ne publikací releasu
v UI. Workflow `.github/workflows/release_plugin.yml` zabalí složku
`amcr_viewer/` do `amcr_viewer.zip` a založí **koncept** releasu i s touto
přílohou; text se dopíše a release zveřejní ručně. Do ZIPu se nesmí dostat
`.git*` soubory.
- Organizace má zapnuté **immutable releases** – k publikovanému releasu už
nelze nic přiložit. Proto příloha vzniká na konceptu, ještě před
zveřejněním; workflow spouštěný na `release: published` by vždy selhal.
- Workflow se čte z commitu, na který **tag ukazuje**. Tag proto zakládej až
na commitu, který obsahuje aktuální podobu workflow – jinak se nespustí nic.
## 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
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`.
-1
View File
@@ -31,6 +31,5 @@ def classFactory(iface): # pylint: disable=invalid-name
:param iface: A QGIS interface instance.
:type iface: QgsInterface
"""
#
from .amcr_viewer import AmcrViewer
return AmcrViewer(iface)
+85 -30
View File
@@ -1,32 +1,39 @@
# -*- coding: utf-8 -*-
import os
# -*- coding: utf-8 -*-
import csv
import requests
import xml.etree.ElementTree as ET # nosec
import os
import time
from qgis.core import QgsMessageLog, Qgis
import xml.etree.ElementTree as ET # nosec
import requests
from qgis.core import Qgis, QgsMessageLog
# 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 = {
@@ -84,7 +91,7 @@ def parse_codelist_file(filename, target_dict=None):
except Exception as e:
QgsMessageLog.logMessage(
f"AMČR Codelist Read Error for {filename}: {e}",
"AMČR", Qgis.Critical)
"AMČR", Qgis.MessageLevel.Critical)
return target_dict
@@ -92,18 +99,24 @@ def parse_codelist_file(filename, target_dict=None):
def load_all_data():
"""Loads the codelist during plugin startup."""
ensure_codelists_dir()
categorized_data = {k: {} for k in slovnicek.keys()}
categorized_data = {k: {} for k in slovnicek}
parse_codelist_file('heslar.csv', categorized_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,7 +124,10 @@ def fetch_set(internal_name, api_set, task=None):
return None
try:
response = requests.get(BASE_URL, params=params, timeout=30)
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
@@ -173,7 +189,7 @@ def fetch_set(internal_name, api_set, task=None):
# Pagination
token = root.find('.//oai:resumptionToken', NS)
if token is not None and token.text:
params = {
params_amcr = {
"verb": "ListRecords",
"resumptionToken": token.text
}
@@ -181,10 +197,29 @@ def fetch_set(internal_name, api_set, task=None):
else:
break
else:
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(
f"Chyba u setu {api_set}: {e}",
"AMČR", Qgis.Warning)
"AMČR", Qgis.MessageLevel.Warning)
break
return dataset
@@ -195,18 +230,23 @@ 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
QgsMessageLog.logMessage(
f"Zpracovávám kategorii: {interni}...",
"AMČR", Qgis.Info)
"AMČR", Qgis.MessageLevel.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
@@ -260,6 +300,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
@@ -277,5 +327,10 @@ DRUH_LOKALITY = {}
JISTOTA = {}
LOKALITA_ZACHOVALOST = {}
PRISTUPNOST = {}
NALEZ_KATEGORIE = {}
DRUH_NALEZU = {}
SPECIFIKACE = {}
NALEZOVE_OKOLNOSTI = {}
NALEZCE = {}
refresh_globals()
+312 -42
View File
@@ -1,18 +1,67 @@
# -*- coding: utf-8 -*-
from qgis.PyQt.QtWidgets import (QDialog, QVBoxLayout,
QLineEdit, QDialogButtonBox,
QCheckBox, QGroupBox, QPushButton,
QListWidget, QListWidgetItem, QHBoxLayout,
QMessageBox, QLabel, QFormLayout)
from qgis.PyQt.QtCore import Qt, QSettings
from qgis.core import (QgsTask, QgsApplication,
QgsMessageLog, Qgis, QgsAuthMethodConfig)
# -*- coding: utf-8 -*-
from qgis.core import (
Qgis,
QgsApplication,
QgsAuthMethodConfig,
QgsMessageLog,
QgsTask,
)
from qgis.gui import QgsDateEdit
from qgis.PyQt.QtCore import QSettings, Qt
from qgis.PyQt.QtWidgets import (
QCheckBox,
QDialog,
QDialogButtonBox,
QFormLayout,
QFrame,
QGridLayout,
QGroupBox,
QHBoxLayout,
QLabel,
QLineEdit,
QListWidget,
QListWidgetItem,
QMessageBox,
QPushButton,
QScrollArea,
QVBoxLayout,
QWidget,
)
from qgis.utils import iface
from .amcr_codelists import (OBDOBI, TYP_AKCE, KRAJE, AREAL, ORGANIZACE,
OKRESY, KATASTRY, VEDOUCI, PIAN_PRESNOST,
TYP_LOKALITY, DRUH_LOKALITY, JISTOTA,
LOKALITA_ZACHOVALOST, PRISTUPNOST,
download_heslare, refresh_globals)
from .amcr_codelists import (
AREAL,
DRUH_LOKALITY,
DRUH_NALEZU,
JISTOTA,
KATASTRY,
KRAJE,
LOKALITA_ZACHOVALOST,
NALEZ_KATEGORIE,
NALEZCE,
NALEZOVE_OKOLNOSTI,
OBDOBI,
OKRESY,
ORGANIZACE,
PIAN_PRESNOST,
PRISTUPNOST,
SPECIFIKACE,
TYP_AKCE,
TYP_LOKALITY,
VEDOUCI,
download_heslare,
refresh_globals,
)
# The date filter of the API requires both bounds; a one-sided range makes
# the server fail with an ArrayIndexOutOfBoundsException and '*' is not
# accepted either. An empty picker is therefore replaced by these sentinels,
# which are also the default limits of QgsDateEdit.
DATE_OPEN_FROM = "0001-01-01"
DATE_OPEN_TO = "9999-12-31"
# Shown by a date picker that is left empty
DATE_NULL_TEXT = "neomezeno"
# Keep Python references to running tasks. QgsTaskManager only holds the
@@ -23,7 +72,7 @@ _ACTIVE_TASKS = []
class UpdateCodelistsTask(QgsTask):
def __init__(self, description):
super().__init__(description, QgsTask.CanCancel)
super().__init__(description, QgsTask.Flag.CanCancel)
self.success = False
self.exception = None
@@ -44,16 +93,19 @@ class UpdateCodelistsTask(QgsTask):
refresh_globals()
QgsMessageLog.logMessage(
"Hesláře AMČR byly úspěšně aktualizovány.",
"AMČR", Qgis.Info)
"AMČR", Qgis.MessageLevel.Info
)
else:
if self.isCanceled():
QgsMessageLog.logMessage(
"Aktualizace heslářů byla zrušena.",
"AMČR", Qgis.Warning)
"AMČR", Qgis.MessageLevel.Warning
)
else:
QgsMessageLog.logMessage(
f"Chyba aktualizace: {self.exception}",
"AMČR", Qgis.Critical)
"AMČR", Qgis.MessageLevel.Critical
)
class FilterableSelectionDialog(QDialog):
@@ -151,12 +203,31 @@ 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': [],
}
# Date range pickers, filled by setup_date_range():
# (API field, label for messages, 'from' widget, 'to' widget)
self.date_ranges = []
layout = QVBoxLayout()
# Filter by current map canvas extent
@@ -169,6 +240,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)
@@ -187,13 +260,6 @@ class AmcrFilterDialog(QDialog):
)
layout.addWidget(self.picker_katastr)
self.picker_presnost = self.setup_picker(
"PIAN – přesnost",
'pian_presnost',
PIAN_PRESNOST
)
layout.addWidget(self.picker_presnost)
self.picker_pristupnost = self.setup_picker(
"Přístupnost",
'pristupnost',
@@ -203,7 +269,15 @@ class AmcrFilterDialog(QDialog):
# Filters valid for Akce
if self.typ_dat == "akce":
if self.typ_dat in ["lokalita", "akce"]:
self.picker_presnost = self.setup_picker(
"PIAN – přesnost",
'pian_presnost',
PIAN_PRESNOST
)
layout.addWidget(self.picker_presnost)
if self.typ_dat in ["samostatny_nalez", "akce"]:
self.picker_org = self.setup_picker(
"Organizace",
'organizace',
@@ -211,6 +285,7 @@ class AmcrFilterDialog(QDialog):
)
layout.addWidget(self.picker_org)
if self.typ_dat == "akce":
self.picker_vedouci = self.setup_picker(
"Vedoucí výzkumu",
'vedouci',
@@ -227,6 +302,12 @@ class AmcrFilterDialog(QDialog):
)
layout.addWidget(self.picker_typ)
self.box_datum = self.setup_date_range("Datum", [
("akce_datum_zahajeni", "Zahájení", "Datum zahájení"),
("akce_datum_ukonceni", "Ukončení", "Datum ukončení"),
])
layout.addWidget(self.box_datum)
# Filters valid for Lokality
if self.typ_dat == "lokalita":
@@ -263,6 +344,50 @@ class AmcrFilterDialog(QDialog):
self.picker_obdobi = self.setup_picker("Období", 'obdobi', OBDOBI)
layout.addWidget(self.picker_obdobi)
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)
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)
# Lokalita has no date field in the index at all, so the block
# is built only for the two entities that do
self.box_datum = self.setup_date_range("Datum nálezu", [
("samostatny_nalez_datum_nalezu", "", "Datum nálezu"),
])
layout.addWidget(self.box_datum)
if self.typ_dat != "samostatny_nalez":
self.picker_areal = self.setup_picker("Areál", 'areal', AREAL)
layout.addWidget(self.picker_areal)
@@ -284,6 +409,7 @@ class AmcrFilterDialog(QDialog):
self.lbl_komponenty_warning.setVisible(False)
layout.addWidget(self.lbl_komponenty_warning)
if self.typ_dat != "samostatny_nalez":
self.chk_komponenty.toggled.connect(
self.lbl_komponenty_warning.setVisible
)
@@ -291,6 +417,21 @@ class AmcrFilterDialog(QDialog):
# Pushes everything above to the top
layout.addStretch(1)
# The filter stack is taller than the window on every entity
# (over 1000 px for 'akce'), so it scrolls. The buttons stay
# outside the scroll area, otherwise the user would have to
# scroll to the bottom just to confirm the dialog.
content = QWidget()
content.setLayout(layout)
scroll = QScrollArea()
scroll.setWidgetResizable(True)
scroll.setFrameShape(QFrame.Shape.NoFrame)
scroll.setWidget(content)
outer = QVBoxLayout()
outer.addWidget(scroll)
# Main dialog OK/Cancel/Update buttons
buttons = QDialogButtonBox()
@@ -311,9 +452,9 @@ class AmcrFilterDialog(QDialog):
buttons.accepted.connect(self.accept)
buttons.rejected.connect(self.reject)
layout.addWidget(buttons)
outer.addWidget(buttons)
self.setLayout(layout)
self.setLayout(outer)
def setup_picker(self, label_text, cache_key, data_source, extra_btn=None):
"""
@@ -361,7 +502,7 @@ class AmcrFilterDialog(QDialog):
self.selection_cache[cache_key] = [
'HES-000861',
'HES-000862',
'HES-000863'
'HES-000863',
]
btn.clicked.connect(open_dialog)
@@ -376,6 +517,94 @@ class AmcrFilterDialog(QDialog):
row_widget.setLayout(row_layout)
return row_widget
def setup_date_range(self, title, rows):
"""
Creates a compact date range block: one row per API date field,
each with a 'from' and a 'to' picker.
rows is a list of (api_field, row_label, name_for_messages).
An empty row_label is used when the group box title already names
the field, which keeps the single-row variant from repeating itself.
A picker left empty means an open bound; the sentinel is
substituted in get_filters(), not here, so that an untouched
block adds no filter at all.
"""
row_widget = QGroupBox(title)
grid = QGridLayout()
grid.setContentsMargins(5, 5, 5, 5)
grid.setVerticalSpacing(3)
grid.setHorizontalSpacing(6)
for row, (api_field, row_label, name) in enumerate(rows):
if row_label:
grid.addWidget(QLabel(row_label), row, 0)
date_from = self._date_edit(
f"{name} – od (prázdné = bez dolní meze)"
)
date_to = self._date_edit(
f"{name} – do (prázdné = bez horní meze)"
)
separator = QLabel("–")
separator.setAlignment(Qt.AlignmentFlag.AlignCenter)
grid.addWidget(date_from, row, 1)
grid.addWidget(separator, row, 2)
grid.addWidget(date_to, row, 3)
self.date_ranges.append((api_field, name, date_from, date_to))
grid.setColumnStretch(1, 1)
grid.setColumnStretch(3, 1)
row_widget.setLayout(grid)
return row_widget
@staticmethod
def _date_edit(tooltip):
"""
A date picker that may stay empty.
clear() is essential here – setEmpty() looks empty but leaves
isNull() False with today's date, which would silently apply
a filter the user never set.
"""
widget = QgsDateEdit()
widget.setAllowNull(True)
widget.setNullRepresentation(DATE_NULL_TEXT)
widget.setDisplayFormat("d. M. yyyy")
widget.setCalendarPopup(True)
widget.clear()
widget.setToolTip(tooltip)
return widget
def accept(self):
"""
Blocks the dialog on a reversed date range. The API answers such
a query with zero records and no error, which is indistinguishable
from a genuinely empty result.
"""
reversed_ranges = [
name for _, name, date_from, date_to in self.date_ranges
if not date_from.isNull() and not date_to.isNull()
and date_from.date() > date_to.date()
]
if reversed_ranges:
QMessageBox.warning(
self,
"Neplatné rozmezí",
"U těchto filtrů je počáteční datum novější než koncové:\n"
+ "\n".join(f"• {name}" for name in reversed_ranges)
+ "\n\nDotaz by nevrátil žádný záznam. Opravte rozmezí, "
"nebo jedno z polí vyprázdněte."
)
return
super().accept()
def action_update_heslare(self):
# Create the task instance and keep a reference so the Python
# wrapper survives until the task finishes
@@ -413,7 +642,7 @@ class AmcrFilterDialog(QDialog):
# This will show exactly what went wrong (e.g. PermissionError)
msg = (
"Aktualizace selhala z důvodu chyby:\n"
f"{str(task.exception)}"
f"{task.exception!s}"
)
else:
msg = "Aktualizace byla zrušena uživatelem."
@@ -428,7 +657,10 @@ class AmcrFilterDialog(QDialog):
return "true" if self.chk_bbox.isChecked() else "false"
def get_komponenty(self):
if self.typ_dat in ["akce", "lokalita"]:
return "true" if self.chk_komponenty.isChecked() else "false"
else:
return "false"
def get_filters(self):
"""Compiles the user selections from the cache into
@@ -453,14 +685,17 @@ 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.chk_proj_akce.isChecked():
filters['proj_akce'] = 'true'
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.typ_dat == "lokalita":
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']:
@@ -468,7 +703,38 @@ class AmcrFilterDialog(QDialog):
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']
filters['f_lokalita_zachovalost'] = (
self.selection_cache['lokalita_zachovalost']
)
# Samostatné nálezy
if self.selection_cache['nalez_kategorie']:
filters['f_kategorie'] = self.selection_cache['nalez_kategorie']
if self.selection_cache['druh_nalezu']:
filters['f_druh_nalezu'] = self.selection_cache['druh_nalezu']
if self.selection_cache['specifikace']:
filters['f_specifikace'] = self.selection_cache['specifikace']
if self.selection_cache['nalezove_okolnosti']:
filters['f_nalezove_okolnosti'] = (
self.selection_cache['nalezove_okolnosti']
)
if self.selection_cache['nalezce']:
filters['f_nalezce'] = self.selection_cache['nalezce']
# Date ranges – the API needs both bounds, so an empty picker is
# replaced by a sentinel. A block with both pickers empty adds no
# filter at all; sending the full 0001–9999 range would only
# clutter the log without narrowing anything.
for api_field, _, date_from, date_to in self.date_ranges:
if date_from.isNull() and date_to.isNull():
continue
od = (DATE_OPEN_FROM if date_from.isNull()
else date_from.date().toString("yyyy-MM-dd"))
do = (DATE_OPEN_TO if date_to.isNull()
else date_to.date().toString("yyyy-MM-dd"))
filters[api_field] = f"{od},{do}"
return filters
@@ -490,7 +756,8 @@ class LoginDialog(QDialog):
- storeAuthenticationConfig() and loadAuthenticationConfig() both have
SIP_INOUT on their config parameter, so Python bindings return a tuple
(bool, QgsAuthMethodConfig) rather than just bool. Always unpack both.
- loadAuthenticationConfig() with full=False loads only metadata (name, method,
- loadAuthenticationConfig() with full=False loads only metadata
(name, method,
id) but NOT the config() values like username/password. Use full=True to
access those.
"""
@@ -721,7 +988,7 @@ class LoginDialog(QDialog):
# We skip hasConfigId() as it may return False
# despite the config existing
# (in-memory cache may not be populated yet in QGIS 4).
ok_load, existing_cfg = (
ok_load, _ = (
self._load_config(existing_id, full=False)
if existing_id
else (False, None)
@@ -750,7 +1017,9 @@ class LoginDialog(QDialog):
settings = QSettings()
existing_id = settings.value(self.SETTINGS_KEY, "")
if existing_id:
QgsApplication.authManager().removeAuthenticationConfig(existing_id)
QgsApplication.authManager().removeAuthenticationConfig(
existing_id
)
settings.remove(self.SETTINGS_KEY)
QMessageBox.information(
self,
@@ -785,4 +1054,5 @@ class LoginDialog(QDialog):
if not ok:
return "", ""
return cfg.config("username", ""), cfg.config("password", "") # nosec B106
return (cfg.config("username", ""),
cfg.config("password", "")) # nosec B106
+383 -47
View File
@@ -1,15 +1,24 @@
# -*- coding: utf-8 -*-
from qgis.core import (QgsProject, QgsVectorLayer, QgsFeature, QgsGeometry,
QgsField, QgsCoordinateReferenceSystem,
QgsCoordinateTransform, QgsWkbTypes, Qgis,
QgsMessageLog)
from qgis.utils import iface
from qgis.PyQt.QtCore import Qt, QMetaType
from qgis.PyQt.QtWidgets import QApplication
from qgis.PyQt.QtGui import QCursor
import requests
# -*- coding: utf-8 -*-
import json
import requests
from qgis.core import (
Qgis,
QgsCoordinateReferenceSystem,
QgsCoordinateTransform,
QgsFeature,
QgsField,
QgsGeometry,
QgsMessageLog,
QgsProject,
QgsVectorLayer,
QgsWkbTypes,
)
from qgis.PyQt.QtCore import QMetaType, Qt
from qgis.PyQt.QtGui import QCursor
from qgis.PyQt.QtWidgets import QApplication
from qgis.utils import iface
# Global cache to store translated terms from the Digital Archive
TRANSLATIONS = {}
@@ -26,6 +35,17 @@ LAST_LOGIN_ERROR: str | None = None
# 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": "Samostatný nález",
}
def _log(msg: str, level=Qgis.MessageLevel.Info):
"""
@@ -35,6 +55,14 @@ def _log(msg: str, level=Qgis.MessageLevel.Info):
QgsMessageLog.logMessage(msg, "AMČR login", level)
def _log_data(msg: str, level=Qgis.MessageLevel.Info):
"""
Shortcut: writes a message to the QGIS log used by the data download
(Messages panel → AMČR tab).
"""
QgsMessageLog.logMessage(msg, "AMČR", level)
def login_to_api(username: str, password: str):
"""
Logs in to the Digiarchiv API using a username and password.
@@ -90,8 +118,9 @@ def login_to_api(username: str, password: str):
except requests.exceptions.HTTPError as e:
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ěď'}",
telo = (e.response.text[:300] if e.response is not None
else "žádná odpověď")
_log(f"CHYBA HTTP {status if status else '?'}: {telo}",
Qgis.MessageLevel.Critical)
LAST_LOGIN_ERROR = 'auth' if status in (401, 403) else 'network'
return None
@@ -179,6 +208,21 @@ def _api_get_json(url, params, timeout=30) -> dict:
_log("Přihlašovací údaje nejsou uloženy – pokračuji anonymně.",
Qgis.MessageLevel.Warning)
# Log the URL that was actually sent, so a query returning nothing can
# be replayed in a browser instead of being reconstructed from the code
url_log = resp.url
if len(url_log) > 500:
url_log = f"{url_log[:500]}… (celkem {len(resp.url)} znaků)"
_log_data(f"GET {url_log}")
# The API reports errors with HTTP 200 and an 'error' key instead of
# a 'response' block – without this the caller sees only empty results
if isinstance(body, dict) and body.get("error"):
_log_data(
f"API vrátilo chybu: {body['error']}",
Qgis.MessageLevel.Critical
)
if body is None:
raise ValueError(
f"API nevrátilo platný JSON (HTTP {resp.status_code})"
@@ -283,7 +327,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
@@ -301,6 +345,14 @@ def load_amcr_data(canvas, bb, filters=None,
else:
base_params[key] = str(value).strip()
# Filters exactly as they arrived from the dialog – an empty result
# caused by a stray filter is otherwise indistinguishable from an
# empty map window
_log_data(
f"Stahuji '{typ_dat}' (bbox={bb}, komponenty={komponenty}), "
f"filtry: {filters if filters else 'žádné'}"
)
docs = []
current_page = 0
BATCH_DOCS = 500 # Records per API request
@@ -317,6 +369,12 @@ 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
@@ -325,6 +383,10 @@ def load_amcr_data(canvas, bb, filters=None,
# gets an explicit error/warning instead of a silent partial result
network_error = False
# Set when the API answers with an error instead of a result set –
# keeps an invalid parameter distinguishable from an empty result
api_error = False
# --- API PAGINATION LOOP ---
while True:
base_params['rows'] = BATCH_DOCS
@@ -335,11 +397,24 @@ def load_amcr_data(canvas, bb, filters=None,
try:
resp_json = _api_get_json(url, params=base_params, timeout=30)
data = resp_json.get('response', {})
# Errors arrive with HTTP 200 and no 'response' block;
# _api_get_json has already logged the reason
if 'response' not in resp_json:
api_error = True
break
data = resp_json['response']
batch_docs = data.get('docs', [])
num_found = data.get('numFound', 0)
if not batch_docs:
if current_page == 0:
_log_data(
"Dotaz proběhl v pořádku, ale nevrátil žádný "
f"záznam (numFound={num_found}).",
Qgis.MessageLevel.Warning
)
break
fetched_total += len(batch_docs)
@@ -399,6 +474,15 @@ def load_amcr_data(canvas, bb, filters=None,
)
return
if api_error and not docs:
iface.messageBar().pushMessage(
"AMCR",
"Dotaz na API selhal – podrobnosti v panelu Zprávy, "
"záložka AMČR.",
level=Qgis.MessageLevel.Critical
)
return
if not docs:
iface.messageBar().pushMessage(
"AMCR",
@@ -415,7 +499,7 @@ def load_amcr_data(canvas, bb, filters=None,
# to a list of its associated metadata
pian_lookup = {}
target_pian_ids = set()
actions_with_geom = 0
entries_with_geom = 0
# Helper: safely extract a single value
def g(doc, key, default=""):
@@ -434,12 +518,16 @@ def load_amcr_data(canvas, bb, filters=None,
return ", ".join([str(x) for x in val if x])
# Process each downloaded metadata record
if typ_dat in archeologicky_zaznam_l:
for doc in docs:
piani = doc.get('az_dj_pian', [])
if not piani:
continue
actions_with_geom += 1
if only_projektove_akce and not doc.get("akce_projekt", False):
continue
entries_with_geom += 1
# Extract protected fields ('or {}' – key may hold None)
az_chranene = doc.get('az_chranene_udaje') or {}
@@ -470,7 +558,7 @@ def load_amcr_data(canvas, bb, filters=None,
"katastr": g_list(doc, 'katastr'),
"dalsi_katastr": dalsi_kat_str,
"pristupnost": g(doc, 'pristupnost'),
"loc": g_list(doc, 'loc')
"loc": g_list(doc, 'loc'),
}
# Add entity-specific metadata
@@ -515,6 +603,11 @@ def load_amcr_data(canvas, bb, filters=None,
if doc.get('akce_je_nz') is True
else "Ne"
),
"projekt": g(
doc,
'akce_projekt',
""
),
})
elif typ_dat == "lokalita":
@@ -540,7 +633,8 @@ def load_amcr_data(canvas, bb, filters=None,
for dj in djs:
# 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
komps = dj.get('dj_komponenta', [])
@@ -560,7 +654,8 @@ def load_amcr_data(canvas, bb, filters=None,
dj_id = dj.get('ident_cely')
dj_typ = dj.get('dj_typ')
# Merge shared metadata with documentation unit-specific fields
# Merge shared metadata with documentation
# unit-specific fields
dj_meta = {
**meta,
'dj_id': dj_id,
@@ -607,7 +702,8 @@ def load_amcr_data(canvas, bb, filters=None,
or {}
).get('value', ""),
}
pian_lookup[dj_pian_value].append(komp_meta)
pian_lookup[dj_pian_value].append(
komp_meta)
target_pian_ids_count += 1
else:
# DJ without components — still include
@@ -621,7 +717,8 @@ def load_amcr_data(canvas, bb, filters=None,
'komponenta_areal': "",
'komponenta_obdobi': "",
}
pian_lookup[dj_pian_value].append(empty_meta)
pian_lookup[dj_pian_value].append(
empty_meta)
target_pian_ids_count += 1
else:
target_pian_ids_count += 1
@@ -647,15 +744,17 @@ def load_amcr_data(canvas, bb, filters=None,
iface.messageBar().pushMessage(
"AMCR",
f"Záznamů: {len(docs)} (z toho {actions_with_geom} s mapou). "
f"Záznamů: {len(docs)} (z toho {entries_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"
"ident_cely",
"pian_typ",
"pian_chranene_udaje",
"pian_presnost",
]
for i in range(0, total_pians, BATCH_PIAN):
@@ -668,12 +767,19 @@ def load_amcr_data(canvas, bb, filters=None,
"entity": "pian",
"q": fq_pian,
"rows": len(batch),
"fl": ",".join(fl_pian)
"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', []))
if 'response' not in r_json:
QgsMessageLog.logMessage(
"API nevrátilo geometrie PIAN pro dávku "
f"{i // BATCH_PIAN + 1} – viz chyba výše.",
"AMČR", Qgis.MessageLevel.Warning
)
continue
docs_pian.extend(r_json['response'].get('docs', []))
except requests.exceptions.RequestException as e:
# Network is down – stop immediately instead of
# uselessly retrying every remaining batch
@@ -689,43 +795,176 @@ def load_amcr_data(canvas, bb, filters=None,
"AMČR", Qgis.MessageLevel.Warning
)
elif typ_dat == "samostatny_nalez":
nalezy = {}
for doc in docs:
sn_loc = g_list(doc, "loc")
if not sn_loc:
continue
entries_with_geom += 1
sn_id = g(
doc,
"ident_cely"
)
sn_nalezce = g(
doc,
"samostatny_nalez_nalezce"
)
sn_hloubka = g(
doc,
"samostatny_nalez_hloubka"
)
sn_pristupnost = g(
doc,
"pristupnost"
)
sn_datum = g(
doc,
"samostatny_nalez_datum_nalezu"
)
sn_poznamka = g(
doc,
"samostatny_nalez_poznamka"
)
sn_spec = tr_code(g(
doc,
"samostatny_nalez_specifikace"
))
sn_evidencni = g(
doc,
"samostatny_nalez_evidencni_cislo"
)
sn_obdobi = tr_code(g(
doc,
"samostatny_nalez_obdobi"
))
sn_presna = g(
doc,
"samostatny_nalez_presna_datace"
)
sn_okres = g(
doc,
"samostatny_nalez_okres"
)
sn_pocet = g(
doc,
"samostatny_nalez_pocet"
)
sn_druh = tr_code(g(
doc,
"samostatny_nalez_druh_nalezu"
))
sn_predano_org = tr_code(g(
doc,
"samostatny_nalez_predano_organizace"
))
sn_okolnosti = tr_code(g(
doc,
"samostatny_nalez_okolnosti"
))
sn_projekt = g(
doc,
"samostatny_nalez_projekt"
)
sn_chranene = doc.get("samostatny_nalez_chranene_udaje") or {}
sn_lokalizace = g(
sn_chranene,
"lokalizace"
)
sn_katastr = sn_chranene.get('katastr', {}).get('value')
wkt = None
wkt_is_wgs = False
if sn_chranene.get('geom_sjtsk_wkt'):
wkt = sn_chranene.get('geom_sjtsk_wkt', {}).get('value')
elif sn_chranene.get('geom_wkt'):
# Fallback geometry is in WGS-84 and must be
# transformed to S-JTSK before use
wkt = sn_chranene.get('geom_wkt', {}).get('value')
wkt_is_wgs = True
meta = {
"ident_cely": sn_id,
"projekt": sn_projekt,
"okres": sn_okres,
"katastr": sn_katastr,
"nalezce": sn_nalezce,
"datum": sn_datum,
"okolnosti": sn_okolnosti,
"hloubka_cm": sn_hloubka,
"loc": sn_loc,
"lokalizace": sn_lokalizace,
"obdobi": sn_obdobi,
"presna_datace": sn_presna,
"nalez": sn_druh,
"material": sn_spec,
"pocet": sn_pocet,
"poznamka": sn_poznamka,
"pred_org": sn_predano_org,
"evidencni": sn_evidencni,
"pristupnost": sn_pristupnost,
"wkt": wkt,
"wkt_is_wgs": wkt_is_wgs,
}
nalezy[sn_id] = meta
if not entries_with_geom:
iface.messageBar().pushMessage(
"AMCR",
f"Nalezeno {len(docs)} záznamů, ale žádný nemá geometrii.",
level=Qgis.MessageLevel.Warning
)
return
# ==========================================
# 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)
archeologicky_zaznam_esc = archeologicky_zaznam.replace(" ", "_")
vl_poly = QgsVectorLayer(
"Polygon?crs=epsg:5514",
f"AMCR_{archeologicky_zaznam}_Polygony",
f"AMCR_{archeologicky_zaznam_esc}_Polygony",
"memory"
)
vl_line = QgsVectorLayer(
"LineString?crs=epsg:5514",
f"AMCR_{archeologicky_zaznam}_Linie",
f"AMCR_{archeologicky_zaznam_esc}_Linie",
"memory"
)
vl_point = QgsVectorLayer(
"Point?crs=epsg:5514",
f"AMCR_{archeologicky_zaznam}_Body",
f"AMCR_{archeologicky_zaznam_esc}_Body",
"memory"
)
layers = [vl_poly, vl_line, vl_point]
# Define attribute table structure
cols = []
if typ_dat in ["akce", "lokalita"]:
cols = [
QgsField("pian", QMetaType.Type.QString),
QgsField("presnost", QMetaType.Type.QString),
QgsField("pian_typ", QMetaType.Type.QString),
QgsField("dj", QMetaType.Type.QString),
QgsField("typ_dj", QMetaType.Type.QString),
QgsField("definicni_body", QMetaType.Type.QString),
]
cols += [
QgsField(typ_dat, QMetaType.Type.QString),
QgsField("definicni_body", QMetaType.Type.QString),
QgsField("odkaz_do_digiarchivu", QMetaType.Type.QString),
QgsField("okres", QMetaType.Type.QString),
QgsField("katastr", QMetaType.Type.QString),
QgsField("dalsi_katastry", QMetaType.Type.QString)
QgsField("dalsi_katastry", QMetaType.Type.QString),
]
# Extend table based on data type
@@ -741,6 +980,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 += [
@@ -748,10 +988,27 @@ 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),
]
elif typ_dat == "samostatny_nalez":
cols += [
QgsField("projekt", QMetaType.Type.QString),
QgsField("nalezce", QMetaType.Type.QString),
QgsField("datum", QMetaType.Type.QString),
QgsField("okolnosti", QMetaType.Type.QString),
QgsField("hloubka_cm", QMetaType.Type.QString),
QgsField("lokalizace", QMetaType.Type.QString),
QgsField("obdobi", QMetaType.Type.QString),
QgsField("presna_datace", QMetaType.Type.QString),
QgsField("nalez", QMetaType.Type.QString),
QgsField("material", QMetaType.Type.QString),
QgsField("pocet", QMetaType.Type.QString),
QgsField("poznamka", QMetaType.Type.QString),
QgsField("pred_org", QMetaType.Type.QString),
QgsField("evidencni", QMetaType.Type.QString),
]
cols.append(QgsField("Přístupnost", QMetaType.Type.QString))
cols.append(QgsField("pristupnost", QMetaType.Type.QString))
# Use aliases for technical field names
alias_map = {
@@ -784,6 +1041,21 @@ def load_amcr_data(canvas, bb, filters=None,
"komponenta": "Komponenta",
"komponenta_areal": "Areál",
"komponenta_obdobi": "Období",
"projekt": "Projekt",
"pristupnost": "Přístupnost",
"nalezce": "Nálezce",
"datum": "Datum nálezu",
"okolnosti": "Nálezové okolnosti",
"hloubka_cm": "Hloubka (cm)",
"lokalizace": "Lokalizace",
"obdobi": "Období",
"presna_datace": "Přesná datace",
"nalez": "Nález",
"material": "Materiál",
"pocet": "Počet předmětů",
"poznamka": "Poznámka/bližší popis",
"pred_org": "Předáno organizaci",
"evidencni": "Evidenční číslo",
}
if komponenty == "true":
@@ -813,6 +1085,7 @@ def load_amcr_data(canvas, bb, filters=None,
)
# --- FEATURE POPULATION ---
if typ_dat in ["akce", "lokalita"]:
for doc in docs_pian:
try:
pid = doc.get('ident_cely', '')
@@ -875,11 +1148,11 @@ def load_amcr_data(canvas, bb, filters=None,
if geom.isGeosValid():
t = geom.type()
target_list = None
if t == QgsWkbTypes.PolygonGeometry:
if t == QgsWkbTypes.GeometryType.PolygonGeometry:
target_list = feats_p
elif t == QgsWkbTypes.LineGeometry:
elif t == QgsWkbTypes.GeometryType.LineGeometry:
target_list = feats_l
elif t == QgsWkbTypes.PointGeometry:
elif t == QgsWkbTypes.GeometryType.PointGeometry:
target_list = feats_pt
if target_list is None:
@@ -898,13 +1171,13 @@ def load_amcr_data(canvas, bb, filters=None,
pian_typ,
meta['dj_id'],
meta['dj_typ_value'],
meta['loc'],
meta['ident_cely'],
meta['loc'],
"https://digiarchiv.aiscr.cz/id/"
+ meta['ident_cely'],
meta['az_okres'],
meta['katastr'],
meta['dalsi_katastr']
meta['dalsi_katastr'],
]
if is_akce:
atributy.extend([
@@ -917,7 +1190,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([
@@ -925,7 +1199,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'])
@@ -946,6 +1220,68 @@ def load_amcr_data(canvas, bb, filters=None,
"AMČR", Qgis.MessageLevel.Warning
)
elif typ_dat == "samostatny_nalez":
for n in nalezy.values():
try:
wkt = n["wkt"]
wkt_is_wgs = n["wkt_is_wgs"]
if wkt:
geom = QgsGeometry.fromWkt(wkt)
if geom.isNull():
continue
if wkt_is_wgs:
geom.transform(xform_wgs_to_sjtsk)
if not geom.isGeosValid():
geom = geom.makeValid()
if geom.isGeosValid():
t = geom.type()
target_list = None
if t == QgsWkbTypes.GeometryType.PolygonGeometry:
target_list = feats_p
elif t == QgsWkbTypes.GeometryType.LineGeometry:
target_list = feats_l
elif t == QgsWkbTypes.GeometryType.PointGeometry:
target_list = feats_pt
if target_list is None:
continue
feat = QgsFeature()
feat.setGeometry(geom)
atributy = [
n["ident_cely"],
n["loc"],
"https://digiarchiv.aiscr.cz/id/"
+ n['ident_cely'],
n["okres"],
n["katastr"],
"",
n["projekt"],
n["nalezce"],
n["datum"],
n["okolnosti"],
n["hloubka_cm"],
n["lokalizace"],
n["obdobi"],
n["presna_datace"],
n["nalez"],
n["material"],
n["pocet"],
n["poznamka"],
n["pred_org"],
n["evidencni"],
n["pristupnost"],
]
feat.setAttributes(atributy)
target_list.append(feat)
except Exception as ex:
QgsMessageLog.logMessage(
f"Chyba při tvorbě feature: {ex}",
"AMČR", Qgis.MessageLevel.Warning
)
# --- ADDING TO QGIS INTERFACE ---
proj = QgsProject.instance()
added = 0
@@ -955,12 +1291,12 @@ 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_esc}_{n}")
proj.addMapLayer(L)
added += len(f)
if network_error:
@@ -978,7 +1314,7 @@ def load_amcr_data(canvas, bb, filters=None,
elif added > 0:
iface.messageBar().pushMessage(
"AMCR",
f"Hotovo. Záznamů: {len(docs)} (s geom: {actions_with_geom}). "
f"Hotovo. Záznamů: {len(docs)} (s geom: {entries_with_geom}). "
f"Vykresleno: {added} prvků.",
level=Qgis.MessageLevel.Success
)
+27 -14
View File
@@ -1,13 +1,14 @@
# -*- coding: utf-8 -*-
from qgis.PyQt.QtCore import QSettings, QTranslator, QCoreApplication, QUrl
from qgis.PyQt.QtGui import QIcon, QDesktopServices
from qgis.PyQt.QtWidgets import QMenu, QAction, QToolButton, QDialog
from qgis.core import Qgis
from .amcr_tools import load_amcr_data, login_to_api
from .amcr_dialog import AmcrFilterDialog, LoginDialog
import os.path
from qgis.core import Qgis
from qgis.PyQt.QtCore import QCoreApplication, QSettings, QTranslator, QUrl
from qgis.PyQt.QtGui import QDesktopServices, QIcon
from qgis.PyQt.QtWidgets import QAction, QDialog, QMenu, QToolButton
from .amcr_dialog import AmcrFilterDialog, LoginDialog
from .amcr_tools import load_amcr_data, login_to_api
class AmcrViewer:
"""
@@ -29,7 +30,7 @@ class AmcrViewer:
locale_path = os.path.join(
self.plugin_dir,
'i18n',
'AmcrViewer_{}.qm'.format(locale)
f'AmcrViewer_{locale}.qm'
)
# Install the translator if a translation file
@@ -41,7 +42,7 @@ class AmcrViewer:
# Initialize internal state
self.actions = []
self.menu = self.tr(u'&AMČR Viewer')
self.menu = self.tr('&AMČR Viewer')
self.first_start = None
def tr(self, message):
@@ -90,6 +91,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, 'sn.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')
@@ -101,7 +103,7 @@ class AmcrViewer:
# custom dropdown menu
self.action_download_akce = self.add_action(
icon_path=icon_akce_path,
text=self.tr(u'Stáhnout data akcí | AMČR Viewer'),
text=self.tr('Stáhnout data akcí | AMČR Viewer'),
callback=lambda checked=False: self.run_download('akce'),
parent=self.iface.mainWindow(),
add_to_menu=False,
@@ -109,9 +111,20 @@ 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('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'),
text=self.tr('Stáhnout data lokalit | AMČR Viewer'),
callback=lambda checked=False: self.run_download('lokalita'),
parent=self.iface.mainWindow(),
add_to_menu=False,
@@ -120,8 +133,8 @@ class AmcrViewer:
self.plugin_menu.addAction(self.action_download_lokality)
self.action_login_dialog = self.add_action(
icon_path=icon_akce_path,
text=self.tr(u'Přihlásit se | AMČR Viewer'),
icon_path=icon_pas_path,
text=self.tr('Přihlásit se | AMČR Viewer'),
callback=lambda checked=False: self.login(),
parent=self.iface.mainWindow(),
add_to_menu=False,
@@ -131,7 +144,7 @@ class AmcrViewer:
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'),
text=self.tr('Nápověda AMČR Help | AMČR Viewer'),
callback=lambda checked=False: self.open_help(),
parent=self.iface.mainWindow(),
add_to_menu=False,
File diff suppressed because it is too large. Load diff
+14 -4
View File
@@ -8,7 +8,7 @@ name=AMČR Viewer
qgisMinimumVersion=3.44.0
qgisMaximumVersion=4.99.0
description=Viewing and downloading the AMČR data.
version=2.0.2
version=2.1.2
author=David Spáčil
email=spacil@arub.cz
@@ -23,9 +23,19 @@ repository=https://github.com/ARUP-CAS/aiscr-qgis-amcr-viewer
hasProcessingProvider=no
# Uncomment the following line and add your 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
Plný seznam změn v češtině je dostupný zde: https://github.com/ARUP-CAS/aiscr-qgis-amcr-viewer/releases/tag/v2.1.1
v2.1.2 (2026-09-01)
* Qt6 compatibility
* Code clean-up
v2.1.1 (2026-09-01)
* Added download of Individual finds (PAS), including a dedicated menu entry
* Added filtering by date
* Added filtering of project fieldwork events and a Project column in the attribute table
* Codelists are now built from the Digiarchiv API as well, not only from OAI-PMH; persons come from its facets by role (finder, fieldwork leader)
* Updated the bundled codelist file
* Filter dialog is now scrollable
* Empty API results are now diagnosable from the Messages panel: the query URL and API errors are logged
* Code clean-up
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)
Binary file not shown.

After

Width:  |  Height:  |  Size: 871 B

+23 -7
View File
@@ -9,7 +9,7 @@
id="svg1"
xml:space="preserve"
sodipodi:docname="icon.svg"
inkscape:version="1.4.3 (0d15f75, 2025-12-25)"
inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
@@ -24,12 +24,12 @@
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="22.627417"
inkscape:cx="6.8059028"
inkscape:cy="14.606174"
inkscape:cx="0.57452426"
inkscape:cy="15.556349"
inkscape:window-width="1920"
inkscape:window-height="1009"
inkscape:window-x="1912"
inkscape:window-y="-8"
inkscape:window-height="1131"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" /><defs
id="defs1"><clipPath
@@ -96,4 +96,20 @@
d="M 3.3486328 2.4970052 L 3.3486328 3.8323242 L 2.7424683 3.8323242 L 4.2085286 5.738151 L 5.674589 3.8323242 L 5.0684245 3.8323242 L 5.0684245 2.4970052 L 3.3486328 2.4970052 z "
inkscape:export-filename="path32.png"
inkscape:export-xdpi="3000"
inkscape:export-ydpi="3000" /></g></svg>
inkscape:export-ydpi="3000" /><rect
style="opacity:1;fill:#85140e;fill-opacity:1;stroke:none;stroke-width:0.414999;stroke-linecap:butt;stroke-linejoin:round;stroke-dasharray:none;paint-order:stroke fill markers"
id="rect1"
width="1.672105"
height="1.2511554"
x="-4.501821"
y="4.2814822" /><text
xml:space="preserve"
style="font-size:0.705556px;font-family:'Open Sans';-inkscape-font-specification:'Open Sans';text-align:start;writing-mode:lr-tb;direction:ltr;text-anchor:start;opacity:1;fill:#85140e;fill-opacity:1;stroke:none;stroke-width:0.414999;stroke-linecap:butt;stroke-linejoin:round;stroke-dasharray:none;paint-order:stroke fill markers"
x="-2.6360023"
y="5.1136947"
id="text1"><tspan
sodipodi:role="line"
id="tspan1"
style="font-size:0.705556px;fill:#85140e;fill-opacity:1;stroke-width:0.415"
x="-2.6360023"
y="5.1136947">lokality</tspan></text></g></svg>

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB