# -*- coding: utf-8 -*- from qgis.PyQt.QtWidgets import (QDialog, QVBoxLayout, QLineEdit, QDialogButtonBox, QCheckBox, QGroupBox, QPushButton, QListWidget, QListWidgetItem, QHBoxLayout, QMessageBox) from qgis.PyQt.QtCore import Qt from qgis.core import QgsTask, QgsApplication, QgsMessageLog, Qgis from .amcr_codelists import (OBDOBI, TYP_AKCE, KRAJE, AREAL, ORGANIZACE, OKRESY, KATASTRY, VEDOUCI, PIAN_PRESNOST, TYP_LOKALITY, DRUH_LOKALITY, JISTOTA, LOKALITA_ZACHOVALOST, download_heslare, refresh_globals) class UpdateCodelistsTask(QgsTask): def __init__(self, description): super().__init__(description, QgsTask.CanCancel) self.success = False self.exception = None def run(self): """Tato část běží ve vedlejším vlákně.""" try: # Voláme upravenou funkci self.success = download_heslare(task=self) return self.success except Exception as e: self.exception = e return False def finished(self, result): """Tato část běží v hlavním vlákně po skončení run().""" if result: # Teď bezpečně aktualizujeme globální proměnné v hlavním vlákně refresh_globals() QgsMessageLog.logMessage("Hesláře AMČR byly úspěšně aktualizovány.", "AMČR", Qgis.Info) else: if self.isCanceled(): QgsMessageLog.logMessage("Aktualizace heslářů byla zrušena.", "AMČR", Qgis.Warning) else: QgsMessageLog.logMessage(f"Chyba aktualizace: {self.exception}", "AMČR", Qgis.Critical) 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) 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.StandardButton.Ok | QDialogButtonBox.StandardButton.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.ItemDataRole.UserRole, code) # Make the item checkable (adds a checkbox) item.setFlags(item.flags() | Qt.ItemFlag.ItemIsUserCheckable) # Restore previous selection state if code in self.preselected: item.setCheckState(Qt.CheckState.Checked) else: item.setCheckState(Qt.CheckState.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) 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.""" codes = [] labels = [] for i in range(self.list_widget.count()): item = self.list_widget.item(i) if item.checkState() == Qt.CheckState.Checked: codes.append(item.data(Qt.ItemDataRole.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().__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) self.picker_vedouci = self.setup_picker("Vedoucí výzkumu", 'vedouci', 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/Update buttons buttons = QDialogButtonBox() self.btn_update = QPushButton("Aktualizovat hesláře 🔄") self.btn_update.setToolTip("Provede kompletní aktualizaci heslářů AMČR. Toto bude trvat pár minut.") self.btn_update.clicked.connect(self.action_update_heslare) buttons.addButton(self.btn_update, QDialogButtonBox.ButtonRole.ActionRole) buttons.addButton(QDialogButtonBox.StandardButton.Ok) buttons.addButton(QDialogButtonBox.StandardButton.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_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: 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_heslare(self): # Vytvoření instance tasku task = UpdateCodelistsTask("Aktualizace heslářů AMČR") # Povolíme tlačítko zpět bez ohledu na výsledek task.taskCompleted.connect(lambda: self.btn_update.setEnabled(True)) task.taskTerminated.connect(lambda: self.btn_update.setEnabled(True)) task.taskCompleted.connect(lambda: QMessageBox.information(self, "Hotovo", "Hesláře byly úspěšně aktualizovány.")) # Ošetření, aby se přesně ukázala případná chyba def on_error(): if task.exception: # Tohle ti přesně řekne, na čem to teď padá (např. PermissionError) msg = f"Aktualizace selhala z důvodu chyby:\n{str(task.exception)}" else: msg = "Aktualizace byla zrušena uživatelem." QMessageBox.warning(self, "Chyba / Zrušeno", msg) task.taskTerminated.connect(on_error) QgsApplication.taskManager().addTask(task) 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