9 Commits

Author SHA1 Message Date
david 11f44d025b Update metadata.txt 2026-03-19 09:38:32 +01:00
david 7f3b2b46fb Merge pull request #28 from ARUP-CAS/relation-fix
Fix polymorphic relations not creating in some cases
2026-03-19 09:36:37 +01:00
david be53edefa5 relation fix 2026-03-19 09:30:13 +01:00
david 3be7832b40 Update metadata.txt 2026-03-16 14:30:12 +01:00
david 8c0c540fa4 Merge pull request #26 from ARUP-CAS/polymorphic-relation
Polymorphic relation instead of the standard ones; code cleanup
2026-03-16 14:13:50 +01:00
David Spáčil 8088b32661 removed unnecessary imports 2026-03-16 14:01:32 +01:00
David Spáčil c17275ef66 changed from three 1:N relations to a polymorphic relation 2026-03-16 14:00:37 +01:00
david 9ec866f1d2 Update metadata.txt 2026-03-13 10:56:09 +01:00
david 5a951edec7 Fixed compatibility issues with QGIS 4 (#24)
* used PyQt6 compatible objects

* metadata update
2026-03-13 10:48:12 +01:00
6 changed files with 165 additions and 141 deletions
+1
View File
@@ -210,3 +210,4 @@ __marimo__/
README_files/
README.html
amcr_viewer.zip
+60 -62
View File
@@ -13,6 +13,7 @@ from .amcr_codelists import (OBDOBI, TYP_AKCE, KRAJE, AREAL, ORGANIZACE,
class FilterableSelectionDialog(QDialog):
"""
A custom dialog for selecting multiple items from a list with a search filter.
Updated for PyQt6/Qt6 compatibility.
"""
def __init__(self, title, data_dict, preselected_codes, parent=None):
super().__init__(parent)
@@ -37,7 +38,9 @@ class FilterableSelectionDialog(QDialog):
layout.addWidget(self.list_widget)
# Standard OK/Cancel dialog buttons
buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
buttons = QDialogButtonBox(
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
)
buttons.accepted.connect(self.accept)
buttons.rejected.connect(self.reject)
layout.addWidget(buttons)
@@ -52,16 +55,16 @@ class FilterableSelectionDialog(QDialog):
item = QListWidgetItem(name)
# Store the actual code (ID) hidden in the UserRole
item.setData(Qt.UserRole, code)
item.setData(Qt.ItemDataRole.UserRole, code)
# Make the item checkable (adds a checkbox)
item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
item.setFlags(item.flags() | Qt.ItemFlag.ItemIsUserCheckable)
# Restore previous selection state
if code in self.preselected:
item.setCheckState(Qt.Checked)
item.setCheckState(Qt.CheckState.Checked)
else:
item.setCheckState(Qt.Unchecked)
item.setCheckState(Qt.CheckState.Unchecked)
self.list_widget.addItem(item)
@@ -70,10 +73,7 @@ class FilterableSelectionDialog(QDialog):
search_text = text.lower()
for i in range(self.list_widget.count()):
item = self.list_widget.item(i)
if search_text not in item.text().lower():
item.setHidden(True)
else:
item.setHidden(False)
item.setHidden(search_text not in item.text().lower())
def get_selected_codes(self):
"""Returns the hidden codes and display labels of all checked items."""
@@ -81,8 +81,8 @@ class FilterableSelectionDialog(QDialog):
labels = []
for i in range(self.list_widget.count()):
item = self.list_widget.item(i)
if item.checkState() == Qt.Checked:
codes.append(item.data(Qt.UserRole))
if item.checkState() == Qt.CheckState.Checked:
codes.append(item.data(Qt.ItemDataRole.UserRole))
labels.append(item.text())
return codes, labels
@@ -93,7 +93,7 @@ class AmcrFilterDialog(QDialog):
The main filtering UI where users set criteria before downloading data.
"""
def __init__(self, typ_dat, parent=None):
super(AmcrFilterDialog, self).__init__(parent)
super().__init__(parent)
self.setWindowTitle("Filtr AMČR")
self.resize(500, 750)
@@ -189,7 +189,9 @@ class AmcrFilterDialog(QDialog):
layout.addStretch(1)
# Main dialog OK/Cancel buttons
buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
buttons = QDialogButtonBox(
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
)
buttons.accepted.connect(self.accept)
buttons.rejected.connect(self.reject)
layout.addWidget(buttons)
@@ -197,60 +199,56 @@ class AmcrFilterDialog(QDialog):
self.setLayout(layout)
def setup_picker(self, label_text, cache_key, data_source, extra_btn=None):
"""
Creates a reusable UI component consisting of a label, a read-only
text field showing selected items, and a button to open the selection dialog.
"""
row_widget = QGroupBox(label_text)
# row_widget.setFlat(True)
row_layout = QHBoxLayout()
row_layout.setContentsMargins(5, 5, 5, 5)
# Read-only field displaying the names of selected items
display_field = QLineEdit()
display_field.setReadOnly(True)
display_field.setPlaceholderText("Nic nevybráno (vše)")
display_field.setStyleSheet("background-color: #f0f0f0; color: #333;")
btn = QPushButton("Vybrat...")
btn.setFixedWidth(80)
# Nested function that handles opening the dialog and saving results
def open_dialog():
dlg = FilterableSelectionDialog(label_text, data_source, self.selection_cache[cache_key], self)
if dlg.exec() == QDialog.Accepted:
codes, labels = dlg.get_selected_codes()
# Update local cache with selected IDs
self.selection_cache[cache_key] = codes
# Update the UI text field with selected names
if labels:
display_field.setText(", ".join(labels))
else:
display_field.clear()
# Special case: Pre-fill specific accuracy levels by default
if cache_key == 'pian_presnost':
display_field.setText("odchylka jednotky metrů, odchylka desítky metrů, odchylka stovky metrů")
self.selection_cache[cache_key] = ['HES-000861', 'HES-000862', 'HES-000863']
"""
Creates a reusable UI component consisting of a label, a read-only
text field showing selected items, and a button to open the selection dialog.
"""
row_widget = QGroupBox(label_text)
row_layout = QHBoxLayout()
row_layout.setContentsMargins(5, 5, 5, 5)
# Read-only field displaying the names of selected items
display_field = QLineEdit()
display_field.setReadOnly(True)
display_field.setPlaceholderText("Nic nevybráno (vše)")
display_field.setStyleSheet("background-color: #f0f0f0; color: #333;")
btn = QPushButton("Vybrat...")
btn.setFixedWidth(80)
# Nested function that handles opening the dialog and saving results
def open_dialog():
dlg = FilterableSelectionDialog(label_text, data_source, self.selection_cache[cache_key], self)
if dlg.exec() == QDialog.DialogCode.Accepted: # PyQt6: DialogCode
codes, labels = dlg.get_selected_codes()
# Update local cache with selected IDs
self.selection_cache[cache_key] = codes
# Update the UI text field with selected names
if labels:
display_field.setText(", ".join(labels))
else:
display_field.clear()
# Special case: Pre-fill specific accuracy levels by default
if cache_key == 'pian_presnost':
display_field.setText("odchylka jednotky metrů, odchylka desítky metrů, odchylka stovky metrů")
self.selection_cache[cache_key] = ['HES-000861', 'HES-000862', 'HES-000863']
btn.clicked.connect(open_dialog)
btn.clicked.connect(open_dialog)
row_layout.addWidget(display_field)
row_layout.addWidget(btn)
# Add an optional extra button (e.g., the refresh button for leaders)
if extra_btn:
row_layout.addWidget(extra_btn)
row_layout.addWidget(display_field)
row_layout.addWidget(btn)
# Add an optional extra button (e.g., the refresh button for leaders)
if extra_btn:
row_layout.addWidget(extra_btn)
row_widget.setLayout(row_layout)
return row_widget
row_widget.setLayout(row_layout)
return row_widget
def action_update_vedouci(self):
# Change cursor to loading state to indicate background task
QApplication.setOverrideCursor(Qt.WaitCursor)
QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor)
try:
success, msg = download_vedouci()
if success:
+97 -73
View File
@@ -1,15 +1,13 @@
# -*- coding: utf-8 -*-
from qgis.gui import QgsMapToolIdentifyFeature
from qgis.core import (QgsProject, QgsVectorLayer, QgsFeature, QgsGeometry,
QgsField, QgsCoordinateReferenceSystem, QgsCoordinateTransform,
QgsWkbTypes, QgsRelation, QgsEditorWidgetSetup)
QgsWkbTypes, QgsPolymorphicRelation, QgsEditorWidgetSetup, Qgis)
from qgis.utils import iface
from qgis.PyQt.QtCore import QVariant, Qt
from qgis.PyQt.QtWidgets import QMessageBox, QApplication
from qgis.PyQt.QtCore import Qt, QMetaType
from qgis.PyQt.QtWidgets import QApplication
from qgis.PyQt.QtGui import QCursor
import requests
import json
import xml.etree.ElementTree as ET
import re
# Global cache to store translated terms from the Digital Archive
TRANSLATIONS = {}
@@ -56,8 +54,8 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
url = "https://digiarchiv.aiscr.cz/api/search/query"
iface.messageBar().pushMessage("AMCR", "Hledám záznamy...", level=1)
QApplication.setOverrideCursor(Qt.WaitCursor)
iface.messageBar().pushMessage("AMCR", "Hledám záznamy...", level=Qgis.MessageLevel.Info)
QApplication.setOverrideCursor(QCursor(Qt.CursorShape.WaitCursor))
try:
# ==========================================
@@ -128,7 +126,7 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
if len(docs) >= num_found:
break
if len(docs) >= MAX_LIMIT:
iface.messageBar().pushMessage("AMCR", f"Limit {MAX_LIMIT} záznamů dosažen.", level=1)
iface.messageBar().pushMessage("AMCR", f"Limit {MAX_LIMIT} záznamů dosažen.", level=Qgis.MessageLevel.Warning)
break
current_page += 1
@@ -139,7 +137,7 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
break
if not docs:
iface.messageBar().pushMessage("AMCR", "Žádné záznamy nenalezeny.", level=1)
iface.messageBar().pushMessage("AMCR", "Žádné záznamy nenalezeny.", level=Qgis.MessageLevel.Warning)
return
# ==========================================
@@ -148,6 +146,7 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
# pian_lookup maps a Geometry ID (PIAN) to a list of its associated metadata
pian_lookup = {}
komponenty_lookup = {}
target_pian_ids = set()
actions_with_geom = 0
@@ -257,18 +256,18 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
if komponenty == "true":
komps = dj.get('dj_komponenta', [])
for komp in komps:
feat = QgsFeature()
atributy = [
komp_temp = [
komp.get('ident_cely', ""),
dj_id,
komp.get('komponenta_areal', {}).get('value', ""),
komp.get('komponenta_obdobi', {}).get('value', "")
]
feat.setAttributes(atributy)
feats_k.append(feat)
if dj_id not in komponenty_lookup:
komponenty_lookup[dj_id] = []
komponenty_lookup[dj_id].append(komp_temp)
if not target_pian_ids:
iface.messageBar().pushMessage("AMCR", f"Nalezeno {len(docs)} záznamů, ale žádný nemá geometrii.", level=1)
iface.messageBar().pushMessage("AMCR", f"Nalezeno {len(docs)} záznamů, ale žádný nemá geometrii.", level=Qgis.MessageLevel.Warning)
return
@@ -280,7 +279,7 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
docs_pian = []
BATCH_PIAN = 200 # Geometry requests are batch-processed to stay under URL length limits
iface.messageBar().pushMessage("AMCR", f"Záznamů: {len(docs)} (z toho {actions_with_geom} s mapou). Stahuji {total_pians} unikátních geometrií, vykresluji {target_pian_ids_count} geometrií...", level=1)
iface.messageBar().pushMessage("AMCR", f"Záznamů: {len(docs)} (z toho {actions_with_geom} s mapou). Stahuji {total_pians} unikátních geometrií, vykresluji {target_pian_ids_count} geometrií...", level=Qgis.MessageLevel.Info)
fl_pian = ["ident_cely", "pian_typ", "pian_chranene_udaje", "pian_presnost"]
@@ -318,43 +317,43 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
# Define attribute table structure
cols = [
QgsField("PIAN", QVariant.String),
QgsField("Přesnost", QVariant.String),
QgsField("PIAN typ", QVariant.String),
QgsField("Dokumentační jednotka", QVariant.String),
QgsField("Typ dokumentační jednotky", QVariant.String),
QgsField("Definiční bod(y) (WGS-84)", QVariant.String),
QgsField(archeologicky_zaznam, QVariant.String),
QgsField("Odkaz do Digitálního archivu AMČR", QVariant.String),
QgsField("Okres", QVariant.String),
QgsField("Katastr", QVariant.String),
QgsField("Další katastry", QVariant.String)
QgsField("PIAN", QMetaType.Type.QString),
QgsField("Přesnost", QMetaType.Type.QString),
QgsField("PIAN typ", QMetaType.Type.QString),
QgsField("Dokumentační jednotka", QMetaType.Type.QString),
QgsField("Typ dokumentační jednotky", QMetaType.Type.QString),
QgsField("Definiční bod(y) (WGS-84)", QMetaType.Type.QString),
QgsField(archeologicky_zaznam, QMetaType.Type.QString),
QgsField("Odkaz do Digitálního archivu AMČR", QMetaType.Type.QString),
QgsField("Okres", QMetaType.Type.QString),
QgsField("Katastr", QMetaType.Type.QString),
QgsField("Další katastry", QMetaType.Type.QString)
]
# Extend table based on data type
if typ_dat == "akce":
cols += [
QgsField("Akce lokalizace", QVariant.String),
QgsField("Vedoucí akce", QVariant.String),
QgsField("Organizace", QVariant.String),
QgsField("Specifikace data", QVariant.String),
QgsField("Datum zahájeni", QVariant.String),
QgsField("Datum ukončení", QVariant.String),
QgsField("Hlavní typ", QVariant.String),
QgsField("Vedlejší typ", QVariant.String),
QgsField("Zjištění", QVariant.String),
QgsField("Akce nahrazuje NZ", QVariant.String),
QgsField("Akce lokalizace", QMetaType.Type.QString),
QgsField("Vedoucí akce", QMetaType.Type.QString),
QgsField("Organizace", QMetaType.Type.QString),
QgsField("Specifikace data", QMetaType.Type.QString),
QgsField("Datum zahájeni", QMetaType.Type.QString),
QgsField("Datum ukončení", QMetaType.Type.QString),
QgsField("Hlavní typ", QMetaType.Type.QString),
QgsField("Vedlejší typ", QMetaType.Type.QString),
QgsField("Zjištění", QMetaType.Type.QString),
QgsField("Akce nahrazuje NZ", QMetaType.Type.QString),
]
elif typ_dat == "lokalita":
cols += [
QgsField("nazev_lokality", QVariant.String),
QgsField("popis_lokality", QVariant.String),
QgsField("typ_lokality", QVariant.String),
QgsField("druh_lokality", QVariant.String),
QgsField("zachovalost", QVariant.String)
QgsField("nazev_lokality", QMetaType.Type.QString),
QgsField("popis_lokality", QMetaType.Type.QString),
QgsField("typ_lokality", QMetaType.Type.QString),
QgsField("druh_lokality", QMetaType.Type.QString),
QgsField("zachovalost", QMetaType.Type.QString)
]
cols.append(QgsField("Přístupnost", QVariant.String))
cols.append(QgsField("Přístupnost", QMetaType.Type.QString))
# Use aliases for technical field names
alias_map = {
@@ -370,17 +369,17 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
vl_komponenty = QgsVectorLayer("None", "AMCR Komponenty", "memory")
pr = vl_komponenty.dataProvider()
komponenty_cols = [
QgsField("komponenta", QVariant.String),
QgsField("dj_id", QVariant.String),
QgsField("komponenta_areal", QVariant.String),
QgsField("komponenta_obdobi", QVariant.String)
QgsField("komponenta", QMetaType.Type.QString),
QgsField("dj_id", QMetaType.Type.QString),
QgsField("komponenta_areal", QMetaType.Type.QString),
QgsField("komponenta_obdobi", QMetaType.Type.QString),
QgsField("vrstva", QMetaType.Type.QString)
]
pr.addAttributes(komponenty_cols)
vl_komponenty.updateFields()
idx_dj_id = vl_komponenty.fields().indexOf("dj_id")
text_setup = QgsEditorWidgetSetup("TextEdit", {})
vl_komponenty.setEditorWidgetSetup(idx_dj_id, text_setup)
idx_vrstva = vl_komponenty.fields().indexOf("vrstva")
vl_komponenty.setEditorWidgetSetup(idx_vrstva, QgsEditorWidgetSetup("Hidden", {}))
for vl in layers:
vl.dataProvider().addAttributes(cols)
@@ -428,11 +427,14 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
target_list = None
if t == QgsWkbTypes.PolygonGeometry:
target_list = feats_p
referenced_layer = vl_poly
elif t == QgsWkbTypes.LineGeometry:
target_list = feats_l
referenced_layer = vl_line
elif t == QgsWkbTypes.PointGeometry:
target_list = feats_pt
referenced_layer = vl_point
if target_list is None:
continue
@@ -462,7 +464,12 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
meta['lokalita_typ'], meta['lokalita_druh'],
meta['lokalita_zachovalost']
])
if komponenty == "true" and meta['dj_id'] in komponenty_lookup:
for k in komponenty_lookup[meta['dj_id']]:
if len(k) == 3:
k.append(referenced_layer.id())
atributy.append(meta['pristupnost'])
feat.setAttributes(atributy)
target_list.append(feat)
@@ -470,6 +477,17 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
except Exception as ex:
print(f"Chyba při tvorbě feature: {ex}")
pass
if komponenty == "true":
for k in komponenty_lookup:
for komp in komponenty_lookup[k]:
if len(komp) == 4:
feat = QgsFeature()
atributy = [
komp[0], k, komp[1], komp[2], komp[3]
]
feat.setAttributes(atributy)
feats_k.append(feat)
# --- ADDING TO QGIS INTERFACE ---
proj = QgsProject.instance()
@@ -493,35 +511,41 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
added += len(f)
if added > 0:
iface.messageBar().pushMessage("AMCR", f"Hotovo. Záznamů: {len(docs)} (s geom: {actions_with_geom}). Vykresleno: {added} prvků.", level=0)
iface.messageBar().pushMessage("AMCR", f"Hotovo. Záznamů: {len(docs)} (s geom: {actions_with_geom}). Vykresleno: {added} prvků.", level=Qgis.MessageLevel.Success)
# --- RELATIONSHIP MANAGEMENT ---
# Set up automatic links between spatial layers and the component table
if komponenty == "true":
parent_layers = [
(vl_poly, "Polygony"),
(vl_line, "Linie"),
(vl_point, "Body")
]
parent_layers_ids = []
if feats_p:
parent_layers_ids.append(vl_poly.id())
if feats_l:
parent_layers_ids.append(vl_line.id())
if feats_pt:
parent_layers_ids.append(vl_point.id())
rel_manager = proj.relationManager()
for parent_layer, label in parent_layers:
rel = QgsRelation()
rel_name = f"Komponenty pro {label}"
rel.setName(rel_name)
rel.setReferencingLayer(vl_komponenty.id())
rel.setReferencedLayer(parent_layer.id())
rel.addFieldPair("dj_id", "Dokumentační jednotka")
rel.generateId()
if rel.isValid():
rel_manager.addRelation(rel)
else:
print(f"Relace pro {label} není validní!")
rel = QgsPolymorphicRelation()
# rel.setId(f"rel_komponenty_{archeologicky_zaznam}")
rel.setName("Komponenty")
rel.setReferencingLayer(vl_komponenty.id())
rel.setReferencedLayerExpression("@layer_id")
rel.setReferencedLayerField("vrstva")
rel.setReferencedLayerIds(parent_layers_ids)
rel.addFieldPair("dj_id", "Dokumentační jednotka")
rel.generateId()
if rel.isValid():
rel_manager.addPolymorphicRelation(rel)
else:
print("Relace Komponenty není validní!")
else:
iface.messageBar().pushMessage("AMCR", "Žádná data k zobrazení.", level=1)
iface.messageBar().pushMessage("AMCR", "Žádná data k zobrazení.", level=Qgis.MessageLevel.Info)
except Exception as e:
iface.messageBar().pushMessage("Chyba", str(e), level=2)
iface.messageBar().pushMessage("Chyba", str(e), level=Qgis.MessageLevel.Critical)
finally:
# Always restore cursor, even after failure
QApplication.restoreOverrideCursor()
+3 -3
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
from qgis.PyQt.QtCore import QSettings, QTranslator, QCoreApplication
from qgis.PyQt.QtGui import QIcon
from qgis.PyQt.QtWidgets import QMenu, QAction, QToolButton
from qgis.PyQt.QtWidgets import QMenu, QAction, QToolButton, QDialog
from .amcr_tools import load_amcr_data
from .amcr_dialog import AmcrFilterDialog
@@ -120,7 +120,7 @@ class AmcrViewer:
self.tool_button = QToolButton()
self.tool_button.setMenu(self.plugin_menu)
self.tool_button.setDefaultAction(self.action_download_akce)
self.tool_button.setPopupMode(QToolButton.MenuButtonPopup)
self.tool_button.setPopupMode(QToolButton.ToolButtonPopupMode.MenuButtonPopup)
# Add the widget directly to the toolbar and store the reference for cleanup
self.toolbar_action = self.iface.addToolBarWidget(self.tool_button)
@@ -161,7 +161,7 @@ class AmcrViewer:
result = dlg.exec()
# If user confirmed the dialog (OK button), gather filters and load data
if result == 1:
if result == QDialog.DialogCode.Accepted:
filters = dlg.get_filters()
bbox = dlg.get_bbox()
komponenty = dlg.get_komponenty()
+3 -2
View File
@@ -5,9 +5,10 @@
[general]
name=AMČR Viewer
qgisMinimumVersion=3.4
qgisMinimumVersion=3.4.0
qgisMaximumVersion=4.99.0
description=Viewing and downloading the AMČR data.
version=1.2.0
version=1.3.2
author=David Spáčil
email=spacil@arub.cz
+1 -1
View File
@@ -6,7 +6,7 @@
#
# WARNING! All changes made in this file will be lost!
from PyQt5 import QtCore
from qgis.PyQt import QtCore
qt_resource_data = b"\
\x00\x00\x04\x0a\