Compare commits

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

No files matched your search

+6 -20
View File
@@ -1,26 +1,15 @@
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:
push:
tags:
- 'v*'
permissions:
contents: write
release:
types: [published]
jobs:
build-and-release:
runs-on: ubuntu-latest
steps:
# 1. Stáhne kód z tagu, který běh spustil
# 1. Stáhne kód z repozitáře
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
@@ -31,14 +20,11 @@ jobs:
# -r = rekurzivně, -x = ignorovat skryté git soubory
zip -r amcr_viewer.zip amcr_viewer -x "*.git*"
# 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
# 3. Nahraje ZIP k Releasu
- name: Upload Release 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 }}
+4 -10
View File
@@ -90,16 +90,10 @@ Další pravidla:
`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.
- Release se vytváří publikací GitHub Release; workflow
`.github/workflows/release_plugin.yml` zabalí složku `amcr_viewer/` do
`amcr_viewer.zip` a přiloží ji k releasu. Do ZIPu se nesmí dostat `.git*`
soubory.
## Pull requesty
+10 -174
View File
@@ -3,12 +3,10 @@ from qgis.PyQt.QtWidgets import (QDialog, QVBoxLayout,
QLineEdit, QDialogButtonBox,
QCheckBox, QGroupBox, QPushButton,
QListWidget, QListWidgetItem, QHBoxLayout,
QMessageBox, QLabel, QFormLayout,
QGridLayout, QScrollArea, QFrame, QWidget)
QMessageBox, QLabel, QFormLayout)
from qgis.PyQt.QtCore import Qt, QSettings
from qgis.core import (QgsTask, QgsApplication,
QgsMessageLog, Qgis, QgsAuthMethodConfig)
from qgis.gui import QgsDateEdit
from qgis.utils import iface
from .amcr_codelists import (OBDOBI, TYP_AKCE, KRAJE, AREAL, ORGANIZACE,
OKRESY, KATASTRY, VEDOUCI, PIAN_PRESNOST,
@@ -19,17 +17,6 @@ from .amcr_codelists import (OBDOBI, TYP_AKCE, KRAJE, AREAL, ORGANIZACE,
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
# C++ object; without a Python-side reference the wrapper can be garbage
# collected before the task finishes, which crashes QGIS.
@@ -190,10 +177,6 @@ class AmcrFilterDialog(QDialog):
'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
@@ -226,6 +209,13 @@ 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',
@@ -235,14 +225,6 @@ class AmcrFilterDialog(QDialog):
# Filters valid for 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",
@@ -268,12 +250,6 @@ 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":
@@ -346,13 +322,6 @@ class AmcrFilterDialog(QDialog):
)
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)
@@ -383,21 +352,6 @@ 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()
@@ -418,9 +372,9 @@ class AmcrFilterDialog(QDialog):
buttons.accepted.connect(self.accept)
buttons.rejected.connect(self.reject)
outer.addWidget(buttons)
layout.addWidget(buttons)
self.setLayout(outer)
self.setLayout(layout)
def setup_picker(self, label_text, cache_key, data_source, extra_btn=None):
"""
@@ -483,94 +437,6 @@ 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
@@ -623,10 +489,7 @@ 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
@@ -671,33 +534,6 @@ class AmcrFilterDialog(QDialog):
if 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
+17 -300
View File
@@ -34,7 +34,7 @@ archeologicky_zaznam_l = [
typ_dat_vocab = {
"akce": "Akce",
"lokalita": "Lokalita",
"samostatny_nalez": "Samostatný nález",
"samostatny_nalez": "PAS",
}
@@ -46,14 +46,6 @@ 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.
@@ -198,21 +190,6 @@ 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})"
@@ -335,14 +312,6 @@ 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
@@ -373,10 +342,6 @@ 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
@@ -387,24 +352,11 @@ def load_amcr_data(canvas, bb, filters=None,
try:
resp_json = _api_get_json(url, params=base_params, timeout=30)
# 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']
data = resp_json.get('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)
@@ -464,15 +416,6 @@ 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",
@@ -489,7 +432,7 @@ def load_amcr_data(canvas, bb, filters=None,
# to a list of its associated metadata
pian_lookup = {}
target_pian_ids = set()
entries_with_geom = 0
actions_with_geom = 0
# Helper: safely extract a single value
def g(doc, key, default=""):
@@ -517,7 +460,7 @@ def load_amcr_data(canvas, bb, filters=None,
if only_projektove_akce and not doc.get("akce_projekt", False):
continue
entries_with_geom += 1
actions_with_geom += 1
# Extract protected fields ('or {}' – key may hold None)
az_chranene = doc.get('az_chranene_udaje') or {}
@@ -730,7 +673,7 @@ def load_amcr_data(canvas, bb, filters=None,
iface.messageBar().pushMessage(
"AMCR",
f"Záznamů: {len(docs)} (z toho {entries_with_geom} s mapou). "
f"Záznamů: {len(docs)} (z toho {actions_with_geom} s mapou). "
f"Stahuji {total_pians} unikátních geometrií, "
f"vykresluji {target_pian_ids_count} geometrií...",
level=Qgis.MessageLevel.Info
@@ -758,14 +701,7 @@ def load_amcr_data(canvas, bb, filters=None,
try:
QApplication.processEvents()
r_json = _api_get_json(url, params=params_pian, timeout=15)
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', []))
docs_pian.extend(r_json.get('response', {}).get('docs', []))
except requests.exceptions.RequestException as e:
# Network is down – stop immediately instead of
# uselessly retrying every remaining batch
@@ -782,132 +718,15 @@ def load_amcr_data(canvas, bb, filters=None,
)
elif typ_dat == "samostatny_nalez":
nalezy = {}
for doc in docs:
sn_loc = g_list(doc, "loc")
if not sn_loc:
loc = g(doc, "loc", [])
if not 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"
)
actions_with_geom += 1
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
# ==========================================
@@ -917,40 +736,32 @@ def load_amcr_data(canvas, bb, filters=None,
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_esc}_Polygony",
f"AMCR_{archeologicky_zaznam}_Polygony",
"memory"
)
vl_line = QgsVectorLayer(
"LineString?crs=epsg:5514",
f"AMCR_{archeologicky_zaznam_esc}_Linie",
f"AMCR_{archeologicky_zaznam}_Linie",
"memory"
)
vl_point = QgsVectorLayer(
"Point?crs=epsg:5514",
f"AMCR_{archeologicky_zaznam_esc}_Body",
f"AMCR_{archeologicky_zaznam}_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),
]
cols += [
QgsField(typ_dat, QMetaType.Type.QString),
QgsField("definicni_body", QMetaType.Type.QString),
QgsField(typ_dat, QMetaType.Type.QString),
QgsField("odkaz_do_digiarchivu", QMetaType.Type.QString),
QgsField("okres", QMetaType.Type.QString),
QgsField("katastr", QMetaType.Type.QString),
@@ -980,25 +791,8 @@ def load_amcr_data(canvas, bb, filters=None,
QgsField("druh_lokality", 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("pristupnost", QMetaType.Type.QString))
cols.append(QgsField("Přístupnost", QMetaType.Type.QString))
# Use aliases for technical field names
alias_map = {
@@ -1032,20 +826,6 @@ def load_amcr_data(canvas, bb, filters=None,
"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":
@@ -1075,7 +855,6 @@ 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', '')
@@ -1161,8 +940,8 @@ def load_amcr_data(canvas, bb, filters=None,
pian_typ,
meta['dj_id'],
meta['dj_typ_value'],
meta['ident_cely'],
meta['loc'],
meta['ident_cely'],
"https://digiarchiv.aiscr.cz/id/"
+ meta['ident_cely'],
meta['az_okres'],
@@ -1210,68 +989,6 @@ 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.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 ---
proj = QgsProject.instance()
added = 0
@@ -1285,7 +1002,7 @@ def load_amcr_data(canvas, bb, filters=None,
if f:
L.dataProvider().addFeatures(f)
L.updateExtents()
L.setName(f"AMCR_{archeologicky_zaznam_esc}_{n}")
L.setName(f"AMCR_{archeologicky_zaznam}_{n}")
proj.addMapLayer(L)
added += len(f)
@@ -1304,7 +1021,7 @@ def load_amcr_data(canvas, bb, filters=None,
elif added > 0:
iface.messageBar().pushMessage(
"AMCR",
f"Hotovo. Záznamů: {len(docs)} (s geom: {entries_with_geom}). "
f"Hotovo. Záznamů: {len(docs)} (s geom: {actions_with_geom}). "
f"Vykresleno: {added} prvků.",
level=Qgis.MessageLevel.Success
)
+2 -2
View File
@@ -90,7 +90,7 @@ class AmcrViewer:
"""
# Define paths for action-specific icons
icon_akce_path = os.path.join(self.plugin_dir, 'akce.png')
icon_pas_path = os.path.join(self.plugin_dir, 'sn.png')
icon_pas_path = os.path.join(self.plugin_dir, 'akce.png')
icon_lokality_path = os.path.join(self.plugin_dir, 'lokality.png')
icon_amcr_help_path = os.path.join(self.plugin_dir, 'amcr-help.png')
@@ -131,7 +131,7 @@ class AmcrViewer:
self.plugin_menu.addAction(self.action_download_lokality)
self.action_login_dialog = self.add_action(
icon_path=icon_pas_path,
icon_path=icon_akce_path,
text=self.tr(u'Přihlásit se | AMČR Viewer'),
callback=lambda checked=False: self.login(),
parent=self.iface.mainWindow(),
File diff suppressed because it is too large. Load diff
+4 -11
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.1.1
version=2.0.2
author=David Spáčil
email=spacil@arub.cz
@@ -23,16 +23,9 @@ repository=https://github.com/ARUP-CAS/aiscr-qgis-amcr-viewer
hasProcessingProvider=no
# Uncomment the following line and add your changelog:
changelog=
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.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
Aktualizace na verzi 2.0.0 výrazně mění chování pluginu. Před updatem je doporučeno přečíst si seznam změn níže.
Version 2.0.0 changes the plugin behavior dramatically. It is advised to check the changelog below.
Plný seznam změn v češtině je dostupný zde: https://github.com/ARUP-CAS/aiscr-qgis-amcr-viewer/releases/tag/v2.0.0
v2.0.2 (2026-06-13)
* Plugin-wide fixes and optimalizations (details https://github.com/ARUP-CAS/aiscr-qgis-amcr-viewer/pull/49)
v2.0.1 (2026-06-10)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 871 B

+7 -23
View File
@@ -9,7 +9,7 @@
id="svg1"
xml:space="preserve"
sodipodi:docname="icon.svg"
inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)"
inkscape:version="1.4.3 (0d15f75, 2025-12-25)"
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="0.57452426"
inkscape:cy="15.556349"
inkscape:cx="6.8059028"
inkscape:cy="14.606174"
inkscape:window-width="1920"
inkscape:window-height="1131"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-height="1009"
inkscape:window-x="1912"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" /><defs
id="defs1"><clipPath
@@ -96,20 +96,4 @@
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" /><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>
inkscape:export-ydpi="3000" /></g></svg>

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB