Compare commits

...
12 Commits
Author SHA1 Message Date
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
12 changed files with 4142 additions and 8329 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 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: on:
release: push:
types: [published] tags:
- 'v*'
permissions:
contents: write
jobs: jobs:
build-and-release: build-and-release:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
# 1. Stáhne kód z repozitáře # 1. Stáhne kód z tagu, který běh spustil
- name: Checkout code - name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
@@ -20,11 +31,14 @@ jobs:
# -r = rekurzivně, -x = ignorovat skryté git soubory # -r = rekurzivně, -x = ignorovat skryté git soubory
zip -r amcr_viewer.zip amcr_viewer -x "*.git*" zip -r amcr_viewer.zip amcr_viewer -x "*.git*"
# 3. Nahraje ZIP k Releasu # 3. Založí koncept releasu i s přílohou
- name: Upload Release Asset # 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 uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
if: startsWith(github.ref, 'refs/tags/')
with: with:
files: amcr_viewer.zip files: amcr_viewer.zip
draft: true
name: AMCR Viewer ${{ github.ref_name }}
prerelease: ${{ contains(github.ref_name, '-') }}
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+128
View File
@@ -0,0 +1,128 @@
# 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 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`.
+73 -21
View File
@@ -9,24 +9,30 @@ from qgis.core import QgsMessageLog, Qgis
# Define paths for the plugin and its codelists directory # Define paths for the plugin and its codelists directory
PLUGIN_DIR = os.path.dirname(__file__) PLUGIN_DIR = os.path.dirname(__file__)
CODELISTS_DIR = os.path.join(PLUGIN_DIR, 'codelists') CODELISTS_DIR = os.path.join(PLUGIN_DIR, 'codelists')
BASE_URL = "https://api.aiscr.cz/2.2/oai" 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') OUTPUT_FILE = os.path.join(CODELISTS_DIR, 'heslar.csv')
slovnicek = { slovnicek = {
'obdobi': 'heslo:obdobi', 'obdobi': (BASE_URL_AMCR, 'heslo:obdobi'),
'typ_akce': 'heslo:akce_typ', 'typ_akce': (BASE_URL_AMCR, 'heslo:akce_typ'),
'areal': 'heslo:areal', 'areal': (BASE_URL_AMCR, 'heslo:areal'),
'kraj': 'ruian_kraj', 'kraj': (BASE_URL_AMCR, 'ruian_kraj'),
'organizace': 'organizace', 'organizace': (BASE_URL_AMCR, 'organizace'),
'okres': 'ruian_okres', 'okres': (BASE_URL_AMCR, 'ruian_okres'),
'katastr': 'ruian_katastr', 'katastr': (BASE_URL_AMCR, 'ruian_katastr'),
'vedouci': 'osoba', 'pian_presnost': (BASE_URL_AMCR, 'heslo:pian_presnost'),
'pian_presnost': 'heslo:pian_presnost', 'typ_lokality': (BASE_URL_AMCR, 'heslo:lokalita_typ'),
'typ_lokality': 'heslo:lokalita_typ', 'druh_lokality': (BASE_URL_AMCR, 'heslo:lokalita_druh'),
'druh_lokality': 'heslo:lokalita_druh', 'jistota': (BASE_URL_AMCR, 'heslo:jistota_urceni'),
'jistota': 'heslo:jistota_urceni', 'lokalita_zachovalost': (BASE_URL_AMCR, 'heslo:stav_dochovani'),
'lokalita_zachovalost': 'heslo:stav_dochovani', 'pristupnost': (BASE_URL_AMCR, 'heslo:pristupnost'),
'pristupnost': '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 = { NS = {
@@ -97,13 +103,19 @@ def load_all_data():
return 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 = [] dataset = []
params = { params_amcr = {
"verb": "ListRecords", "verb": "ListRecords",
"metadataPrefix": "oai_dc", "metadataPrefix": "oai_dc",
"set": api_set "set": api_set
} }
params_da = {
"entity": "samostatny_nalez" if internal_name == "nalezce" else "akce",
"rows": 0,
"noFacets": "false",
"onlyFacets": "true"
}
while True: while True:
# Check for cancellation at each iteration # Check for cancellation at each iteration
@@ -111,7 +123,8 @@ def fetch_set(internal_name, api_set, task=None):
return None return None
try: 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() response.raise_for_status()
root = ET.fromstring(response.content) # nosec root = ET.fromstring(response.content) # nosec
@@ -173,7 +186,7 @@ def fetch_set(internal_name, api_set, task=None):
# Pagination # Pagination
token = root.find('.//oai:resumptionToken', NS) token = root.find('.//oai:resumptionToken', NS)
if token is not None and token.text: if token is not None and token.text:
params = { params_amcr = {
"verb": "ListRecords", "verb": "ListRecords",
"resumptionToken": token.text "resumptionToken": token.text
} }
@@ -181,6 +194,25 @@ def fetch_set(internal_name, api_set, task=None):
else: else:
break 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: except Exception as e:
QgsMessageLog.logMessage( QgsMessageLog.logMessage(
f"Chyba u setu {api_set}: {e}", f"Chyba u setu {api_set}: {e}",
@@ -195,8 +227,13 @@ def download_heslare(task=None):
ensure_codelists_dir() ensure_codelists_dir()
all_data = [] all_data = []
total_sets = len(slovnicek) 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 # Check if the user cancelled the task via the QGIS taskbar
if task and task.isCanceled(): if task and task.isCanceled():
return False return False
@@ -206,7 +243,7 @@ def download_heslare(task=None):
"AMČR", Qgis.Info) "AMČR", Qgis.Info)
# Pass the task correctly to the updated fetch function # 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: if data is None:
return False # Cancelled mid-download return False # Cancelled mid-download
@@ -260,6 +297,16 @@ def refresh_globals():
LOKALITA_ZACHOVALOST.update(data.get('lokalita_zachovalost', {})) LOKALITA_ZACHOVALOST.update(data.get('lokalita_zachovalost', {}))
PRISTUPNOST.clear() PRISTUPNOST.clear()
PRISTUPNOST.update(data.get('pristupnost', {})) 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 # Initialize empty dicts that will be populated immediately below
@@ -277,5 +324,10 @@ DRUH_LOKALITY = {}
JISTOTA = {} JISTOTA = {}
LOKALITA_ZACHOVALOST = {} LOKALITA_ZACHOVALOST = {}
PRISTUPNOST = {} PRISTUPNOST = {}
NALEZ_KATEGORIE = {}
DRUH_NALEZU = {}
SPECIFIKACE = {}
NALEZOVE_OKOLNOSTI = {}
NALEZCE = {}
refresh_globals() refresh_globals()
+250 -22
View File
@@ -3,18 +3,33 @@ from qgis.PyQt.QtWidgets import (QDialog, QVBoxLayout,
QLineEdit, QDialogButtonBox, QLineEdit, QDialogButtonBox,
QCheckBox, QGroupBox, QPushButton, QCheckBox, QGroupBox, QPushButton,
QListWidget, QListWidgetItem, QHBoxLayout, QListWidget, QListWidgetItem, QHBoxLayout,
QMessageBox, QLabel, QFormLayout) QMessageBox, QLabel, QFormLayout,
QGridLayout, QScrollArea, QFrame, QWidget)
from qgis.PyQt.QtCore import Qt, QSettings from qgis.PyQt.QtCore import Qt, QSettings
from qgis.core import (QgsTask, QgsApplication, from qgis.core import (QgsTask, QgsApplication,
QgsMessageLog, Qgis, QgsAuthMethodConfig) QgsMessageLog, Qgis, QgsAuthMethodConfig)
from qgis.gui import QgsDateEdit
from qgis.utils import iface from qgis.utils import iface
from .amcr_codelists import (OBDOBI, TYP_AKCE, KRAJE, AREAL, ORGANIZACE, from .amcr_codelists import (OBDOBI, TYP_AKCE, KRAJE, AREAL, ORGANIZACE,
OKRESY, KATASTRY, VEDOUCI, PIAN_PRESNOST, OKRESY, KATASTRY, VEDOUCI, PIAN_PRESNOST,
TYP_LOKALITY, DRUH_LOKALITY, JISTOTA, TYP_LOKALITY, DRUH_LOKALITY, JISTOTA,
LOKALITA_ZACHOVALOST, PRISTUPNOST, LOKALITA_ZACHOVALOST, PRISTUPNOST,
NALEZ_KATEGORIE, DRUH_NALEZU, SPECIFIKACE,
NALEZOVE_OKOLNOSTI, NALEZCE,
download_heslare, refresh_globals) 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 # Keep Python references to running tasks. QgsTaskManager only holds the
# C++ object; without a Python-side reference the wrapper can be garbage # C++ object; without a Python-side reference the wrapper can be garbage
# collected before the task finishes, which crashes QGIS. # collected before the task finishes, which crashes QGIS.
@@ -44,16 +59,19 @@ class UpdateCodelistsTask(QgsTask):
refresh_globals() refresh_globals()
QgsMessageLog.logMessage( QgsMessageLog.logMessage(
"Hesláře AMČR byly úspěšně aktualizovány.", "Hesláře AMČR byly úspěšně aktualizovány.",
"AMČR", Qgis.Info) "AMČR", Qgis.Info
)
else: else:
if self.isCanceled(): if self.isCanceled():
QgsMessageLog.logMessage( QgsMessageLog.logMessage(
"Aktualizace heslářů byla zrušena.", "Aktualizace heslářů byla zrušena.",
"AMČR", Qgis.Warning) "AMČR", Qgis.Warning
)
else: else:
QgsMessageLog.logMessage( QgsMessageLog.logMessage(
f"Chyba aktualizace: {self.exception}", f"Chyba aktualizace: {self.exception}",
"AMČR", Qgis.Critical) "AMČR", Qgis.Critical
)
class FilterableSelectionDialog(QDialog): class FilterableSelectionDialog(QDialog):
@@ -151,12 +169,31 @@ class AmcrFilterDialog(QDialog):
# Cache dictionary to store selected codes for each category # Cache dictionary to store selected codes for each category
self.selection_cache = { self.selection_cache = {
'organizace': [], 'kraj': [], 'obdobi': [], 'areal': [], 'organizace': [],
'typ_akce': [], 'okres': [], 'katastr': [], 'vedouci': [], 'kraj': [],
'pian_presnost': [], 'pristupnost': [], 'typ_lokality': [], 'obdobi': [],
'druh_lokality': [], 'jistota': [], 'lokalita_zachovalost': [] '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() layout = QVBoxLayout()
# Filter by current map canvas extent # Filter by current map canvas extent
@@ -169,6 +206,8 @@ class AmcrFilterDialog(QDialog):
if self.typ_dat == "akce": if self.typ_dat == "akce":
self.chk_posevidence = QCheckBox("Pouze pozitivní zjištění") self.chk_posevidence = QCheckBox("Pouze pozitivní zjištění")
layout.addWidget(self.chk_posevidence) layout.addWidget(self.chk_posevidence)
self.chk_proj_akce = QCheckBox("Pouze projektové akce")
layout.addWidget(self.chk_proj_akce)
layout.addSpacing(10) layout.addSpacing(10)
@@ -187,13 +226,6 @@ class AmcrFilterDialog(QDialog):
) )
layout.addWidget(self.picker_katastr) 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( self.picker_pristupnost = self.setup_picker(
"Přístupnost", "Přístupnost",
'pristupnost', 'pristupnost',
@@ -203,7 +235,15 @@ class AmcrFilterDialog(QDialog):
# Filters valid for Akce # 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( self.picker_org = self.setup_picker(
"Organizace", "Organizace",
'organizace', 'organizace',
@@ -211,6 +251,7 @@ class AmcrFilterDialog(QDialog):
) )
layout.addWidget(self.picker_org) layout.addWidget(self.picker_org)
if self.typ_dat == "akce":
self.picker_vedouci = self.setup_picker( self.picker_vedouci = self.setup_picker(
"Vedoucí výzkumu", "Vedoucí výzkumu",
'vedouci', 'vedouci',
@@ -227,6 +268,12 @@ class AmcrFilterDialog(QDialog):
) )
layout.addWidget(self.picker_typ) 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 # Filters valid for Lokality
if self.typ_dat == "lokalita": if self.typ_dat == "lokalita":
@@ -263,6 +310,50 @@ class AmcrFilterDialog(QDialog):
self.picker_obdobi = self.setup_picker("Období", 'obdobi', OBDOBI) self.picker_obdobi = self.setup_picker("Období", 'obdobi', OBDOBI)
layout.addWidget(self.picker_obdobi) layout.addWidget(self.picker_obdobi)
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) self.picker_areal = self.setup_picker("Areál", 'areal', AREAL)
layout.addWidget(self.picker_areal) layout.addWidget(self.picker_areal)
@@ -284,6 +375,7 @@ class AmcrFilterDialog(QDialog):
self.lbl_komponenty_warning.setVisible(False) self.lbl_komponenty_warning.setVisible(False)
layout.addWidget(self.lbl_komponenty_warning) layout.addWidget(self.lbl_komponenty_warning)
if self.typ_dat != "samostatny_nalez":
self.chk_komponenty.toggled.connect( self.chk_komponenty.toggled.connect(
self.lbl_komponenty_warning.setVisible self.lbl_komponenty_warning.setVisible
) )
@@ -291,6 +383,21 @@ class AmcrFilterDialog(QDialog):
# Pushes everything above to the top # Pushes everything above to the top
layout.addStretch(1) 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 # Main dialog OK/Cancel/Update buttons
buttons = QDialogButtonBox() buttons = QDialogButtonBox()
@@ -311,9 +418,9 @@ class AmcrFilterDialog(QDialog):
buttons.accepted.connect(self.accept) buttons.accepted.connect(self.accept)
buttons.rejected.connect(self.reject) 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): def setup_picker(self, label_text, cache_key, data_source, extra_btn=None):
""" """
@@ -361,7 +468,7 @@ class AmcrFilterDialog(QDialog):
self.selection_cache[cache_key] = [ self.selection_cache[cache_key] = [
'HES-000861', 'HES-000861',
'HES-000862', 'HES-000862',
'HES-000863' 'HES-000863',
] ]
btn.clicked.connect(open_dialog) btn.clicked.connect(open_dialog)
@@ -376,6 +483,94 @@ class AmcrFilterDialog(QDialog):
row_widget.setLayout(row_layout) row_widget.setLayout(row_layout)
return row_widget 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): def action_update_heslare(self):
# Create the task instance and keep a reference so the Python # Create the task instance and keep a reference so the Python
# wrapper survives until the task finishes # wrapper survives until the task finishes
@@ -428,7 +623,10 @@ class AmcrFilterDialog(QDialog):
return "true" if self.chk_bbox.isChecked() else "false" return "true" if self.chk_bbox.isChecked() else "false"
def get_komponenty(self): def get_komponenty(self):
if self.typ_dat in ["akce", "lokalita"]:
return "true" if self.chk_komponenty.isChecked() else "false" return "true" if self.chk_komponenty.isChecked() else "false"
else:
return "false"
def get_filters(self): def get_filters(self):
"""Compiles the user selections from the cache into """Compiles the user selections from the cache into
@@ -453,14 +651,17 @@ class AmcrFilterDialog(QDialog):
if self.typ_dat == "akce": if self.typ_dat == "akce":
if self.chk_posevidence.isChecked(): if self.chk_posevidence.isChecked():
filters['posevidence'] = 'true' filters['posevidence'] = 'true'
if self.selection_cache['organizace']: if self.chk_proj_akce.isChecked():
filters['f_organizace'] = self.selection_cache['organizace'] filters['proj_akce'] = 'true'
if self.selection_cache['typ_akce']: if self.selection_cache['typ_akce']:
filters['f_typ_vyzkumu'] = self.selection_cache['typ_akce'] filters['f_typ_vyzkumu'] = self.selection_cache['typ_akce']
if self.selection_cache['vedouci']: if self.selection_cache['vedouci']:
filters['f_vedouci'] = 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']: if self.selection_cache['typ_lokality']:
filters['f_typ_lokality'] = self.selection_cache['typ_lokality'] filters['f_typ_lokality'] = self.selection_cache['typ_lokality']
if self.selection_cache['druh_lokality']: if self.selection_cache['druh_lokality']:
@@ -470,6 +671,33 @@ class AmcrFilterDialog(QDialog):
if self.selection_cache['lokalita_zachovalost']: if self.selection_cache['lokalita_zachovalost']:
filters['f_lokalita_zachovalost'] = self.selection_cache['lokalita_zachovalost'] filters['f_lokalita_zachovalost'] = self.selection_cache['lokalita_zachovalost']
# 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 return filters
+354 -28
View File
@@ -26,6 +26,17 @@ LAST_LOGIN_ERROR: str | None = None
# a second download while the first one is still running # a second download while the first one is still running
_LOADING = False _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): def _log(msg: str, level=Qgis.MessageLevel.Info):
""" """
@@ -35,6 +46,14 @@ def _log(msg: str, level=Qgis.MessageLevel.Info):
QgsMessageLog.logMessage(msg, "AMČR login", level) 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): def login_to_api(username: str, password: str):
""" """
Logs in to the Digiarchiv API using a username and password. Logs in to the Digiarchiv API using a username and password.
@@ -179,6 +198,21 @@ def _api_get_json(url, params, timeout=30) -> dict:
_log("Přihlašovací údaje nejsou uloženy – pokračuji anonymně.", _log("Přihlašovací údaje nejsou uloženy – pokračuji anonymně.",
Qgis.MessageLevel.Warning) 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: if body is None:
raise ValueError( raise ValueError(
f"API nevrátilo platný JSON (HTTP {resp.status_code})" f"API nevrátilo platný JSON (HTTP {resp.status_code})"
@@ -283,7 +317,7 @@ def load_amcr_data(canvas, bb, filters=None,
base_params = { base_params = {
"mapa": "true", "mapa": "true",
"sort": "ident_cely asc", "sort": "ident_cely asc",
"entity": typ_dat "entity": typ_dat,
} }
# Restrict search to map window if requested # Restrict search to map window if requested
@@ -301,6 +335,14 @@ def load_amcr_data(canvas, bb, filters=None,
else: else:
base_params[key] = str(value).strip() 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 = [] docs = []
current_page = 0 current_page = 0
BATCH_DOCS = 500 # Records per API request BATCH_DOCS = 500 # Records per API request
@@ -317,6 +359,12 @@ def load_amcr_data(canvas, bb, filters=None,
else False else False
) )
only_projektove_akce = (
filters.get('proj_akce') == 'true'
if filters
else False
)
# Check whether we should filter results based on component filters # Check whether we should filter results based on component filters
filter_areal = "f_areal" in filters if filters else False filter_areal = "f_areal" in filters if filters else False
filter_datace = "f_obdobi" in filters if filters else False filter_datace = "f_obdobi" in filters if filters else False
@@ -325,6 +373,10 @@ def load_amcr_data(canvas, bb, filters=None,
# gets an explicit error/warning instead of a silent partial result # gets an explicit error/warning instead of a silent partial result
network_error = False 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 --- # --- API PAGINATION LOOP ---
while True: while True:
base_params['rows'] = BATCH_DOCS base_params['rows'] = BATCH_DOCS
@@ -335,11 +387,24 @@ def load_amcr_data(canvas, bb, filters=None,
try: try:
resp_json = _api_get_json(url, params=base_params, timeout=30) 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', []) batch_docs = data.get('docs', [])
num_found = data.get('numFound', 0) num_found = data.get('numFound', 0)
if not batch_docs: if not batch_docs:
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 break
fetched_total += len(batch_docs) fetched_total += len(batch_docs)
@@ -399,6 +464,15 @@ def load_amcr_data(canvas, bb, filters=None,
) )
return 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: if not docs:
iface.messageBar().pushMessage( iface.messageBar().pushMessage(
"AMCR", "AMCR",
@@ -415,7 +489,7 @@ def load_amcr_data(canvas, bb, filters=None,
# to a list of its associated metadata # to a list of its associated metadata
pian_lookup = {} pian_lookup = {}
target_pian_ids = set() target_pian_ids = set()
actions_with_geom = 0 entries_with_geom = 0
# Helper: safely extract a single value # Helper: safely extract a single value
def g(doc, key, default=""): def g(doc, key, default=""):
@@ -434,12 +508,16 @@ def load_amcr_data(canvas, bb, filters=None,
return ", ".join([str(x) for x in val if x]) return ", ".join([str(x) for x in val if x])
# Process each downloaded metadata record # Process each downloaded metadata record
if typ_dat in archeologicky_zaznam_l:
for doc in docs: for doc in docs:
piani = doc.get('az_dj_pian', []) piani = doc.get('az_dj_pian', [])
if not piani: if not piani:
continue 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) # Extract protected fields ('or {}' – key may hold None)
az_chranene = doc.get('az_chranene_udaje') or {} az_chranene = doc.get('az_chranene_udaje') or {}
@@ -470,7 +548,7 @@ def load_amcr_data(canvas, bb, filters=None,
"katastr": g_list(doc, 'katastr'), "katastr": g_list(doc, 'katastr'),
"dalsi_katastr": dalsi_kat_str, "dalsi_katastr": dalsi_kat_str,
"pristupnost": g(doc, 'pristupnost'), "pristupnost": g(doc, 'pristupnost'),
"loc": g_list(doc, 'loc') "loc": g_list(doc, 'loc'),
} }
# Add entity-specific metadata # Add entity-specific metadata
@@ -515,6 +593,11 @@ def load_amcr_data(canvas, bb, filters=None,
if doc.get('akce_je_nz') is True if doc.get('akce_je_nz') is True
else "Ne" else "Ne"
), ),
"projekt": g(
doc,
'akce_projekt',
""
),
}) })
elif typ_dat == "lokalita": elif typ_dat == "lokalita":
@@ -647,15 +730,17 @@ def load_amcr_data(canvas, bb, filters=None,
iface.messageBar().pushMessage( iface.messageBar().pushMessage(
"AMCR", "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"Stahuji {total_pians} unikátních geometrií, "
f"vykresluji {target_pian_ids_count} geometrií...", f"vykresluji {target_pian_ids_count} geometrií...",
level=Qgis.MessageLevel.Info level=Qgis.MessageLevel.Info
) )
fl_pian = [ fl_pian = [
"ident_cely", "pian_typ", "ident_cely",
"pian_chranene_udaje", "pian_presnost" "pian_typ",
"pian_chranene_udaje",
"pian_presnost",
] ]
for i in range(0, total_pians, BATCH_PIAN): for i in range(0, total_pians, BATCH_PIAN):
@@ -668,12 +753,19 @@ def load_amcr_data(canvas, bb, filters=None,
"entity": "pian", "entity": "pian",
"q": fq_pian, "q": fq_pian,
"rows": len(batch), "rows": len(batch),
"fl": ",".join(fl_pian) "fl": ",".join(fl_pian),
} }
try: try:
QApplication.processEvents() QApplication.processEvents()
r_json = _api_get_json(url, params=params_pian, timeout=15) 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: except requests.exceptions.RequestException as e:
# Network is down – stop immediately instead of # Network is down – stop immediately instead of
# uselessly retrying every remaining batch # uselessly retrying every remaining batch
@@ -689,43 +781,180 @@ def load_amcr_data(canvas, bb, filters=None,
"AMČR", Qgis.MessageLevel.Warning "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) # 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) # Initialize three layers for different geometry types (S-JTSK CRS)
archeologicky_zaznam_esc = archeologicky_zaznam.replace(" ", "_")
vl_poly = QgsVectorLayer( vl_poly = QgsVectorLayer(
"Polygon?crs=epsg:5514", "Polygon?crs=epsg:5514",
f"AMCR_{archeologicky_zaznam}_Polygony", f"AMCR_{archeologicky_zaznam_esc}_Polygony",
"memory" "memory"
) )
vl_line = QgsVectorLayer( vl_line = QgsVectorLayer(
"LineString?crs=epsg:5514", "LineString?crs=epsg:5514",
f"AMCR_{archeologicky_zaznam}_Linie", f"AMCR_{archeologicky_zaznam_esc}_Linie",
"memory" "memory"
) )
vl_point = QgsVectorLayer( vl_point = QgsVectorLayer(
"Point?crs=epsg:5514", "Point?crs=epsg:5514",
f"AMCR_{archeologicky_zaznam}_Body", f"AMCR_{archeologicky_zaznam_esc}_Body",
"memory" "memory"
) )
layers = [vl_poly, vl_line, vl_point] layers = [vl_poly, vl_line, vl_point]
# Define attribute table structure # Define attribute table structure
cols = []
if typ_dat in ["akce", "lokalita"]:
cols = [ cols = [
QgsField("pian", QMetaType.Type.QString), QgsField("pian", QMetaType.Type.QString),
QgsField("presnost", QMetaType.Type.QString), QgsField("presnost", QMetaType.Type.QString),
QgsField("pian_typ", QMetaType.Type.QString), QgsField("pian_typ", QMetaType.Type.QString),
QgsField("dj", QMetaType.Type.QString), QgsField("dj", QMetaType.Type.QString),
QgsField("typ_dj", QMetaType.Type.QString), QgsField("typ_dj", QMetaType.Type.QString),
QgsField("definicni_body", QMetaType.Type.QString), ]
cols += [
QgsField(typ_dat, QMetaType.Type.QString), QgsField(typ_dat, QMetaType.Type.QString),
QgsField("definicni_body", QMetaType.Type.QString),
QgsField("odkaz_do_digiarchivu", QMetaType.Type.QString), QgsField("odkaz_do_digiarchivu", QMetaType.Type.QString),
QgsField("okres", QMetaType.Type.QString), QgsField("okres", QMetaType.Type.QString),
QgsField("katastr", QMetaType.Type.QString), QgsField("katastr", QMetaType.Type.QString),
QgsField("dalsi_katastry", QMetaType.Type.QString) QgsField("dalsi_katastry", QMetaType.Type.QString),
] ]
# Extend table based on data type # Extend table based on data type
@@ -741,6 +970,7 @@ def load_amcr_data(canvas, bb, filters=None,
QgsField("vedlejsi_typ", QMetaType.Type.QString), QgsField("vedlejsi_typ", QMetaType.Type.QString),
QgsField("zjisteni", QMetaType.Type.QString), QgsField("zjisteni", QMetaType.Type.QString),
QgsField("nahrazuje_NZ", QMetaType.Type.QString), QgsField("nahrazuje_NZ", QMetaType.Type.QString),
QgsField("projekt", QMetaType.Type.QString),
] ]
elif typ_dat == "lokalita": elif typ_dat == "lokalita":
cols += [ cols += [
@@ -748,10 +978,27 @@ def load_amcr_data(canvas, bb, filters=None,
QgsField("popis_lokality", QMetaType.Type.QString), QgsField("popis_lokality", QMetaType.Type.QString),
QgsField("typ_lokality", QMetaType.Type.QString), QgsField("typ_lokality", QMetaType.Type.QString),
QgsField("druh_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 # Use aliases for technical field names
alias_map = { alias_map = {
@@ -784,6 +1031,21 @@ def load_amcr_data(canvas, bb, filters=None,
"komponenta": "Komponenta", "komponenta": "Komponenta",
"komponenta_areal": "Areál", "komponenta_areal": "Areál",
"komponenta_obdobi": "Období", "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": if komponenty == "true":
@@ -813,6 +1075,7 @@ def load_amcr_data(canvas, bb, filters=None,
) )
# --- FEATURE POPULATION --- # --- FEATURE POPULATION ---
if typ_dat in ["akce", "lokalita"]:
for doc in docs_pian: for doc in docs_pian:
try: try:
pid = doc.get('ident_cely', '') pid = doc.get('ident_cely', '')
@@ -898,13 +1161,13 @@ def load_amcr_data(canvas, bb, filters=None,
pian_typ, pian_typ,
meta['dj_id'], meta['dj_id'],
meta['dj_typ_value'], meta['dj_typ_value'],
meta['loc'],
meta['ident_cely'], meta['ident_cely'],
meta['loc'],
"https://digiarchiv.aiscr.cz/id/" "https://digiarchiv.aiscr.cz/id/"
+ meta['ident_cely'], + meta['ident_cely'],
meta['az_okres'], meta['az_okres'],
meta['katastr'], meta['katastr'],
meta['dalsi_katastr'] meta['dalsi_katastr'],
] ]
if is_akce: if is_akce:
atributy.extend([ atributy.extend([
@@ -917,7 +1180,8 @@ def load_amcr_data(canvas, bb, filters=None,
meta['akce_hlavni_typ'], meta['akce_hlavni_typ'],
meta['akce_vedlejsi_typ'], meta['akce_vedlejsi_typ'],
meta['dj_negativni'], meta['dj_negativni'],
meta['akce_je_nz'] meta['akce_je_nz'],
meta['projekt'],
]) ])
else: else:
atributy.extend([ atributy.extend([
@@ -925,7 +1189,7 @@ def load_amcr_data(canvas, bb, filters=None,
meta['lokalita_popis'], meta['lokalita_popis'],
meta['lokalita_typ'], meta['lokalita_typ'],
meta['lokalita_druh'], meta['lokalita_druh'],
meta['lokalita_zachovalost'] meta['lokalita_zachovalost'],
]) ])
atributy.append(meta['pristupnost']) atributy.append(meta['pristupnost'])
@@ -946,6 +1210,68 @@ def load_amcr_data(canvas, bb, filters=None,
"AMČR", Qgis.MessageLevel.Warning "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.PolygonGeometry:
target_list = feats_p
elif t == QgsWkbTypes.LineGeometry:
target_list = feats_l
elif t == QgsWkbTypes.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 --- # --- ADDING TO QGIS INTERFACE ---
proj = QgsProject.instance() proj = QgsProject.instance()
added = 0 added = 0
@@ -955,12 +1281,12 @@ def load_amcr_data(canvas, bb, filters=None,
(feats_pt, vl_point, "Body"), (feats_pt, vl_point, "Body"),
] ]
for f, l, n in layers_to_process: for f, L, n in layers_to_process:
if f: if f:
l.dataProvider().addFeatures(f) L.dataProvider().addFeatures(f)
l.updateExtents() L.updateExtents()
l.setName(f"AMCR_{archeologicky_zaznam}_{n}") L.setName(f"AMCR_{archeologicky_zaznam_esc}_{n}")
proj.addMapLayer(l) proj.addMapLayer(L)
added += len(f) added += len(f)
if network_error: if network_error:
@@ -978,7 +1304,7 @@ def load_amcr_data(canvas, bb, filters=None,
elif added > 0: elif added > 0:
iface.messageBar().pushMessage( iface.messageBar().pushMessage(
"AMCR", "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ů.", f"Vykresleno: {added} prvků.",
level=Qgis.MessageLevel.Success level=Qgis.MessageLevel.Success
) )
+12 -1
View File
@@ -90,6 +90,7 @@ class AmcrViewer:
""" """
# Define paths for action-specific icons # Define paths for action-specific icons
icon_akce_path = os.path.join(self.plugin_dir, 'akce.png') icon_akce_path = os.path.join(self.plugin_dir, 'akce.png')
icon_pas_path = os.path.join(self.plugin_dir, 'sn.png')
icon_lokality_path = os.path.join(self.plugin_dir, 'lokality.png') icon_lokality_path = os.path.join(self.plugin_dir, 'lokality.png')
icon_amcr_help_path = os.path.join(self.plugin_dir, 'amcr-help.png') 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.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( self.action_download_lokality = self.add_action(
icon_path=icon_lokality_path, icon_path=icon_lokality_path,
text=self.tr(u'Stáhnout data lokalit | AMČR Viewer'), text=self.tr(u'Stáhnout data lokalit | AMČR Viewer'),
@@ -120,7 +131,7 @@ class AmcrViewer:
self.plugin_menu.addAction(self.action_download_lokality) self.plugin_menu.addAction(self.action_download_lokality)
self.action_login_dialog = self.add_action( self.action_login_dialog = self.add_action(
icon_path=icon_akce_path, icon_path=icon_pas_path,
text=self.tr(u'Přihlásit se | AMČR Viewer'), text=self.tr(u'Přihlásit se | AMČR Viewer'),
callback=lambda checked=False: self.login(), callback=lambda checked=False: self.login(),
parent=self.iface.mainWindow(), parent=self.iface.mainWindow(),
File diff suppressed because it is too large. Load diff
+11 -4
View File
@@ -8,7 +8,7 @@ name=AMČR Viewer
qgisMinimumVersion=3.44.0 qgisMinimumVersion=3.44.0
qgisMaximumVersion=4.99.0 qgisMaximumVersion=4.99.0
description=Viewing and downloading the AMČR data. description=Viewing and downloading the AMČR data.
version=2.0.2 version=2.1.1
author=David Spáčil author=David Spáčil
email=spacil@arub.cz email=spacil@arub.cz
@@ -23,9 +23,16 @@ repository=https://github.com/ARUP-CAS/aiscr-qgis-amcr-viewer
hasProcessingProvider=no hasProcessingProvider=no
# Uncomment the following line and add your changelog: # Uncomment the following line and add your changelog:
changelog= changelog=
Aktualizace na verzi 2.0.0 výrazně mění chování pluginu. Před updatem je doporučeno přečíst si seznam změn níže. Plný seznam změn v češtině je dostupný zde: https://github.com/ARUP-CAS/aiscr-qgis-amcr-viewer/releases/tag/v2.1.1
Version 2.0.0 changes the plugin behavior dramatically. It is advised to check the changelog below. v2.1.1 (2026-09-01)
Plný seznam změn v češtině je dostupný zde: https://github.com/ARUP-CAS/aiscr-qgis-amcr-viewer/releases/tag/v2.0.0 * 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) v2.0.2 (2026-06-13)
* Plugin-wide fixes and optimalizations (details https://github.com/ARUP-CAS/aiscr-qgis-amcr-viewer/pull/49) * Plugin-wide fixes and optimalizations (details https://github.com/ARUP-CAS/aiscr-qgis-amcr-viewer/pull/49)
v2.0.1 (2026-06-10) 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" id="svg1"
xml:space="preserve" xml:space="preserve"
sodipodi:docname="icon.svg" 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:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@@ -24,12 +24,12 @@
inkscape:deskcolor="#d1d1d1" inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm" inkscape:document-units="mm"
inkscape:zoom="22.627417" inkscape:zoom="22.627417"
inkscape:cx="6.8059028" inkscape:cx="0.57452426"
inkscape:cy="14.606174" inkscape:cy="15.556349"
inkscape:window-width="1920" inkscape:window-width="1920"
inkscape:window-height="1009" inkscape:window-height="1131"
inkscape:window-x="1912" inkscape:window-x="0"
inkscape:window-y="-8" inkscape:window-y="0"
inkscape:window-maximized="1" inkscape:window-maximized="1"
inkscape:current-layer="layer1" /><defs inkscape:current-layer="layer1" /><defs
id="defs1"><clipPath 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 " 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-filename="path32.png"
inkscape:export-xdpi="3000" 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