Files
aiscr-qgis-amcr-viewer/amcr_viewer/amcr_dialog.py
T
david 3957b87a2b Optimalizace výkonu a refaktorování GUI komponent (#22)
Comprehensive update to improve plugin efficiency and code quality:

- Performance: Increased BATCH_PIAN to 200 and optimized attribute parsing loops.
- Performance: Optimized codelist caching to reload only necessary data.
- UI/UX: Fixed plugin unloading (toolbar icon duplication) and added safe cursor handling.
- Refactoring: Moved GUI helper methods to class level for better OOP structure.
- Modernization: Updated dialog execution syntax to modern PyQt5/6 standards.
- Documentation: Added full inline English documentation across all modules.
2026-03-11 11:37:15 +01:00

310 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
from qgis.PyQt.QtWidgets import (QDialog, QVBoxLayout, QFormLayout,
QLineEdit, QDialogButtonBox,
QCheckBox, QGroupBox, QPushButton,
QListWidget, QListWidgetItem, QHBoxLayout,
QLabel, QMessageBox, QApplication, QWidget)
from qgis.PyQt.QtCore import Qt
from .amcr_codelists import (OBDOBI, TYP_AKCE, KRAJE, AREAL, ORGANIZACE,
OKRESY, KATASTRY, VEDOUCI, PIAN_PRESNOST, TYP_LOKALITY,
DRUH_LOKALITY, JISTOTA, LOKALITA_ZACHOVALOST,
download_vedouci, refresh_vedouci_cache)
class FilterableSelectionDialog(QDialog):
"""
A custom dialog for selecting multiple items from a list with a search filter.
"""
def __init__(self, title, data_dict, preselected_codes, parent=None):
super().__init__(parent)
self.setWindowTitle(f"Výběr: {title}")
self.resize(400, 500)
# Store the source data and previously selected items
self.data_dict = data_dict
self.preselected = preselected_codes if preselected_codes else []
layout = QVBoxLayout()
# Setup search input for filtering items
self.search_bar = QLineEdit()
self.search_bar.setPlaceholderText("Hledat v seznamu...")
self.search_bar.textChanged.connect(self.filter_list)
layout.addWidget(self.search_bar)
# Main list widget for displaying selectable items
self.list_widget = QListWidget()
self.populate_list()
layout.addWidget(self.list_widget)
# Standard OK/Cancel dialog buttons
buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
buttons.accepted.connect(self.accept)
buttons.rejected.connect(self.reject)
layout.addWidget(buttons)
self.setLayout(layout)
def populate_list(self):
# Sort items alphabetically by their display name
sorted_names = sorted(self.data_dict.keys())
for name in sorted_names:
code = self.data_dict[name]
item = QListWidgetItem(name)
# Store the actual code (ID) hidden in the UserRole
item.setData(Qt.UserRole, code)
# Make the item checkable (adds a checkbox)
item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
# Restore previous selection state
if code in self.preselected:
item.setCheckState(Qt.Checked)
else:
item.setCheckState(Qt.Unchecked)
self.list_widget.addItem(item)
def filter_list(self, text):
# Hide items that don't match the search text (case-insensitive)
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)
def get_selected_codes(self):
"""Returns the hidden codes and display labels of all checked items."""
codes = []
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))
labels.append(item.text())
return codes, labels
# --- Main window ---
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)
self.setWindowTitle("Filtr AMČR")
self.resize(500, 750)
# Determines if we are fetching 'akce' (projects) or 'lokalita' (locations)
self.typ_dat = typ_dat
# Cache dictionary to store selected codes for each category
self.selection_cache = {
'organizace': [], 'kraj': [], 'obdobi': [], 'areal': [],
'typ_akce': [], 'okres': [], 'katastr': [], 'vedouci': [], 'pian_presnost': [],
'typ_lokality': [], 'druh_lokality': [], 'jistota': [], 'lokalita_zachovalost': []
}
layout = QVBoxLayout()
# Filter by current map canvas extent
self.chk_bbox = QCheckBox("Omezit vyhledávání rozsahem okna")
self.chk_bbox.setChecked(True)
layout.addWidget(self.chk_bbox)
# Positive/negative evidence valid for Akce
if self.typ_dat == "akce":
self.chk_posevidence = QCheckBox("Pouze pozitivní zjištění")
layout.addWidget(self.chk_posevidence)
layout.addSpacing(10)
# Spatial information valid for all
self.picker_kraj = self.setup_picker("Kraj", 'kraj', KRAJE)
layout.addWidget(self.picker_kraj)
self.picker_okres = self.setup_picker("Okres", 'okres', OKRESY)
layout.addWidget(self.picker_okres)
self.picker_katastr = self.setup_picker("Katastr", 'katastr', KATASTRY)
layout.addWidget(self.picker_katastr)
self.picker_presnost = self.setup_picker("PIAN přesnost", 'pian_presnost', PIAN_PRESNOST)
layout.addWidget(self.picker_presnost)
# Filters valid for Akce
if self.typ_dat == "akce":
self.picker_org = self.setup_picker("Organizace", 'organizace', ORGANIZACE)
layout.addWidget(self.picker_org)
# Button to fetch fresh project leaders from the API
self.btn_update_vedouci = QPushButton("🔄")
self.btn_update_vedouci.setToolTip("Aktualizovat seznam vedoucích z API")
self.btn_update_vedouci.setFixedWidth(30)
self.btn_update_vedouci.clicked.connect(self.action_update_vedouci)
self.picker_vedouci = self.setup_picker("Vedoucí výzkumu", 'vedouci', VEDOUCI, extra_btn=self.btn_update_vedouci)
layout.addWidget(self.picker_vedouci)
# Type of event
self.picker_typ = self.setup_picker("Typ výzkumu", 'typ_akce', TYP_AKCE)
layout.addWidget(self.picker_typ)
# Filters valid for Lokality
if self.typ_dat == "lokalita":
self.picker_typ_lokality = self.setup_picker("Lokalita typ", 'typ_lokality', TYP_LOKALITY)
layout.addWidget(self.picker_typ_lokality)
self.picker_druh_lokality = self.setup_picker("Lokalita druh", 'druh_lokality', DRUH_LOKALITY)
layout.addWidget(self.picker_druh_lokality)
self.picker_jistota = self.setup_picker("Lokalita jistota určení", 'jistota', JISTOTA)
layout.addWidget(self.picker_jistota)
self.picker_lokalita_zachovalost = self.setup_picker("Lokalita - stav dochování", 'lokalita_zachovalost', LOKALITA_ZACHOVALOST)
layout.addWidget(self.picker_lokalita_zachovalost)
# Contextual information
self.picker_obdobi = self.setup_picker("Období", 'obdobi', OBDOBI)
layout.addWidget(self.picker_obdobi)
self.picker_areal = self.setup_picker("Areál", 'areal', AREAL)
layout.addWidget(self.picker_areal)
# Option to download related components table
self.chk_komponenty = QCheckBox("Načíst komponenty")
layout.addWidget(self.chk_komponenty)
# Pushes everything above to the top
layout.addStretch(1)
# Main dialog OK/Cancel buttons
buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
buttons.accepted.connect(self.accept)
buttons.rejected.connect(self.reject)
layout.addWidget(buttons)
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']
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_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)
try:
success, msg = download_vedouci()
if success:
count = refresh_vedouci_cache()
QMessageBox.information(self, "Úspěch", f"{msg}\nNyní je v paměti {count} osob.")
else:
QMessageBox.warning(self, "Chyba", f"Nepodařilo se stáhnout data:\n{msg}")
except Exception as e:
QMessageBox.critical(self, "Chyba", str(e))
finally:
# Safely restore the normal cursor even if an error occurs
QApplication.restoreOverrideCursor()
def get_bbox(self):
return "true" if self.chk_bbox.isChecked() else "false"
def get_komponenty(self):
return "true" if self.chk_komponenty.isChecked() else "false"
def get_filters(self):
"""Compiles the user selections from the cache into API-ready filter parameters."""
filters = {}
if self.selection_cache['kraj']:
filters['f_kraj'] = self.selection_cache['kraj']
if self.selection_cache['okres']:
filters['f_okres'] = self.selection_cache['okres']
if self.selection_cache['katastr']:
filters['f_katastr'] = self.selection_cache['katastr']
if self.selection_cache['obdobi']:
filters['f_obdobi'] = self.selection_cache['obdobi']
if self.selection_cache['areal']:
filters['f_areal'] = self.selection_cache['areal']
if self.selection_cache['pian_presnost']:
filters['f_pian_presnost'] = self.selection_cache['pian_presnost']
if self.typ_dat == "akce":
if self.chk_posevidence.isChecked():
filters['posevidence'] = 'true'
if self.selection_cache['organizace']:
filters['f_organizace'] = self.selection_cache['organizace']
if self.selection_cache['typ_akce']:
filters['f_typ_vyzkumu'] = self.selection_cache['typ_akce']
if self.selection_cache['vedouci']:
filters['f_vedouci'] = self.selection_cache['vedouci']
if self.typ_dat == "lokalita":
if self.selection_cache['typ_lokality']:
filters['f_typ_lokality'] = self.selection_cache['typ_lokality']
if self.selection_cache['druh_lokality']:
filters['f_druh_lokality'] = self.selection_cache['druh_lokality']
if self.selection_cache['jistota']:
filters['f_jistota'] = self.selection_cache['jistota']
if self.selection_cache['lokalita_zachovalost']:
filters['f_lokalita_zachovalost'] = self.selection_cache['lokalita_zachovalost']
return filters