# sn_plan41/ui/tab_a_ui.py – UI für Tab A (Daten) from __future__ import annotations from typing import Optional from sn_basis.functions.qt_wrapper import ( QWidget, QVBoxLayout, QLabel, QPushButton, QToolButton, QFileDialog, QMessageBox, ToolButtonTextBesideIcon, ArrowDown, ArrowRight, SizePolicyPreferred, SizePolicyMaximum, QComboBox, ) from sn_basis.functions.qgisui_wrapper import QgsFileWidget, QgsMapLayerComboBox from sn_basis.functions.qgiscore_wrapper import QgsProject, QgsMapLayerProxyModel from sn_basis.functions.variable_wrapper import get_variable, set_variable from sn_plan41.ui.tab_a_logic import TabALogic # Prüf‑Workflow / DataGrabber (werden zur Laufzeit vom Pruefmanager/Pruefern verwendet) from sn_basis.modules.Dateipruefer import Dateipruefer from sn_basis.modules.Pruefmanager import Pruefmanager from sn_basis.modules.DataGrabber import DataGrabber from sn_basis.modules.linkpruefer import Linkpruefer from sn_basis.modules.stilpruefer import Stilpruefer from sn_basis.modules.Datenschreiber import Datenschreiber # Raumfilter-Optionen RAUMFILTER_VAR = "Raumfilter" RAUMFILTER_OPTIONS = ("Verfahrensgebiet", "Pufferlayer", "ohne") RAUMFILTER_DEFAULT = "Pufferlayer" pm = Pruefmanager(ui_modus="qgis") lp = Linkpruefer() sp = Stilpruefer() class TabA(QWidget): """ UI-Klasse für Tab A (Daten). Diese bereinigte Version enthält ausschließlich UI-Elemente und einfache, nicht-validierende Callback-Handler. Alle fachlichen Prüfungen und Fehlerbehandlungen werden zur Laufzeit vom Pruefmanager und den Prüfern übernommen. """ tab_title = "Daten" def __init__(self, parent=None, pruefmanager=None, link_pruefer=None, stil_pruefer=None, build_ui=True): super().__init__(parent) self.parent = parent self.tab_title = "Daten" # Logik-Adapter (TabALogic verwaltet persistente Projektvariablen) self.logic = TabALogic(pruefmanager=pruefmanager, link_pruefer=link_pruefer, stil_pruefer=stil_pruefer) # Prüfmanager-Instanz (UI-Modus wird zur Laufzeit vom Pruefmanager gehandhabt) self.pruefmanager = Pruefmanager(ui_modus="qgis") # DataGrabber-Instanz (synchroner Aufruf; Prüfungen übernimmt Pruefmanager/Pruefer) # Hinweis: DataGrabber erwartet ggf. Prüfer-Objekte; hier werden sie nicht übergeben, # da TabALogic / Pruefmanager diese zur Laufzeit bereitstellen können. self.data_grabber = DataGrabber(pruefmanager=self.pruefmanager) # Platzhalter, die vom Plugin oder Nutzer gesetzt werden können self._attributes_list = [] # optionale Attributliste (z. B. Excel-Import) self._pufferlayer = None # optionaler Layer (Verfahrensgebiet) self.verfahrens_db: Optional[str] = None self.lokale_linkliste: Optional[str] = None # UI-Widget-Referenz für Raumfilter self._raumfilter_combo: Optional[QComboBox] = None if build_ui: self._build_ui() self._restore_state() # --------------------------------------------------------- # UI-Aufbau # --------------------------------------------------------- def _build_ui(self) -> None: main_layout = QVBoxLayout() main_layout.setSpacing(4) main_layout.setContentsMargins(4, 4, 4, 4) # Verfahrens-Datenbank Gruppe self.group_button = QToolButton() self.group_button.setText("Verfahrens-Datenbank") self.group_button.setCheckable(True) self.group_button.setChecked(True) self.group_button.setToolButtonStyle(ToolButtonTextBesideIcon) self.group_button.setArrowType(ArrowDown) self.group_button.setStyleSheet("font-weight: bold;") self.group_button.toggled.connect(self._toggle_group) main_layout.addWidget(self.group_button) self.group_content = QWidget() self.group_content.setSizePolicy(SizePolicyPreferred, SizePolicyMaximum) group_layout = QVBoxLayout() group_layout.setSpacing(2) group_layout.setContentsMargins(10, 4, 4, 4) group_layout.addWidget(QLabel("bestehende Datei auswählen")) self.file_widget = QgsFileWidget() self.file_widget.setStorageMode(QgsFileWidget.GetFile) self.file_widget.setFilter("Geopackage (*.gpkg)") self.file_widget.fileChanged.connect(self._on_verfahrens_db_changed) group_layout.addWidget(self.file_widget) group_layout.addWidget(QLabel("-oder-")) self.btn_new = QPushButton("Neue Verfahrens-DB anlegen") self.btn_new.clicked.connect(self._create_new_gpkg) group_layout.addWidget(self.btn_new) self.group_content.setLayout(group_layout) main_layout.addWidget(self.group_content) # Optionale Linkliste self.optional_button = QToolButton() self.optional_button.setText("Optional: Lokale Linkliste") self.optional_button.setCheckable(True) self.optional_button.setChecked(False) self.optional_button.setToolButtonStyle(ToolButtonTextBesideIcon) self.optional_button.setArrowType(ArrowRight) self.optional_button.setStyleSheet("font-weight: bold; margin-top: 6px;") self.optional_button.toggled.connect(self._toggle_optional) main_layout.addWidget(self.optional_button) self.optional_content = QWidget() self.optional_content.setSizePolicy(SizePolicyPreferred, SizePolicyMaximum) optional_layout = QVBoxLayout() optional_layout.setSpacing(2) optional_layout.setContentsMargins(10, 4, 4, 20) optional_layout.addWidget(QLabel("(frei lassen für globale Linkliste)")) self.linkliste_widget = QgsFileWidget() self.linkliste_widget.setStorageMode(QgsFileWidget.GetFile) self.linkliste_widget.setFilter("Excelliste (*.xlsx)") self.linkliste_widget.fileChanged.connect(self._on_linkliste_changed) optional_layout.addWidget(self.linkliste_widget) self.optional_content.setLayout(optional_layout) self.optional_content.setVisible(False) main_layout.addWidget(self.optional_content) # Layer-Auswahl layer_label = QLabel("Verfahrensgebiet-Layer auswählen") layer_label.setStyleSheet("font-weight: bold; margin-top: 6px;") main_layout.addWidget(layer_label) self.layer_combo = QgsMapLayerComboBox() self.layer_combo.setSizePolicy(SizePolicyPreferred, SizePolicyMaximum) self.layer_combo.setFilters(QgsMapLayerProxyModel.VectorLayer) self.layer_combo.layerChanged.connect(self._on_layer_changed) main_layout.addWidget(self.layer_combo) # Raumfilter-Label + ComboBox (unterhalb der Layer-Auswahl) main_layout.addWidget(QLabel("Raumfilter")) self._raumfilter_combo = QComboBox(self) # Fülle Optionen (Wrapper stellt addItems bereit) try: self._raumfilter_combo.addItems(list(RAUMFILTER_OPTIONS)) except Exception: # fallback: iterativ hinzufügen, falls Wrapper andere API hat for opt in RAUMFILTER_OPTIONS: if hasattr(self._raumfilter_combo, "addItem"): self._raumfilter_combo.addItem(opt) # Initialisiere Auswahl aus Projekt-Variable oder Default stored = get_variable(RAUMFILTER_VAR, scope="project") if isinstance(stored, str) and stored in RAUMFILTER_OPTIONS: try: self._raumfilter_combo.setCurrentText(stored) except Exception: try: idx = self._raumfilter_combo.findText(stored) if idx is not None and idx >= 0: self._raumfilter_combo.setCurrentIndex(idx) except Exception: pass else: try: self._raumfilter_combo.setCurrentText(RAUMFILTER_DEFAULT) except Exception: try: idx = self._raumfilter_combo.findText(RAUMFILTER_DEFAULT) if idx is not None and idx >= 0: self._raumfilter_combo.setCurrentIndex(idx) except Exception: pass # persistiere Default, falls noch kein Wert gesetzt if not stored: set_variable(RAUMFILTER_VAR, RAUMFILTER_DEFAULT, scope="project") # Signal: bei Änderung Variable setzen try: self._raumfilter_combo.currentTextChanged.connect(self._on_raumfilter_changed) except Exception: try: self._raumfilter_combo.current_text_changed.connect(self._on_raumfilter_changed) except Exception: pass main_layout.addWidget(self._raumfilter_combo) # Neuer Button direkt unterhalb der Raumfilter-Combo: "Fachdaten laden" self.btn_pipeline = QPushButton("Fachdaten laden") self.btn_pipeline.setToolTip("Starte Pipeline: Linkliste → DataGrabber → Datenschreiber → Log") self.btn_pipeline.clicked.connect(self._on_run_pipeline) main_layout.addWidget(self.btn_pipeline) # (Optional) bestehender Button weiter unten für alternative Platzierung self.btn_load = QPushButton("Fachdaten laden (alt)") self.btn_load.clicked.connect(self._on_load_fachdaten) main_layout.addWidget(self.btn_load) main_layout.addStretch(1) self.setLayout(main_layout) # --------------------------------------------------------- # State Restore (UI-Wiederherstellung ohne Prüfungen) # --------------------------------------------------------- def _restore_state(self) -> None: db = self.logic.load_verfahrens_db() if db: self.verfahrens_db = db try: self.file_widget.setFilePath(db) except Exception: pass self._update_group_color() link = self.logic.load_linkliste() if link: self.lokale_linkliste = link try: self.linkliste_widget.setFilePath(link) except Exception: pass layer_id = self.logic.load_verfahrensgebiet_layer_id() if layer_id: layer = QgsProject.instance().mapLayer(layer_id) if layer: self.layer_combo.setLayer(layer) # Raumfilter aus Variable wiederherstellen (falls Combo existiert) try: stored = get_variable(RAUMFILTER_VAR, scope="project") if stored and self._raumfilter_combo is not None: try: self._raumfilter_combo.setCurrentText(stored) except Exception: idx = self._raumfilter_combo.findText(stored) if idx is not None and idx >= 0: self._raumfilter_combo.setCurrentIndex(idx) except Exception: pass # --------------------------------------------------------- # UI-Callbacks (ohne Prüfungen / Exceptions) # --------------------------------------------------------- def _toggle_group(self, checked: bool) -> None: self.group_button.setArrowType(ArrowDown if checked else ArrowRight) self.group_content.setVisible(checked) def _toggle_optional(self, checked: bool) -> None: self.optional_button.setArrowType(ArrowDown if checked else ArrowRight) self.optional_content.setVisible(checked) def _on_verfahrens_db_changed(self, path: str) -> None: self.verfahrens_db = path self.logic.set_verfahrens_db(path) self._update_group_color() def _on_linkliste_changed(self, path: str) -> None: self.lokale_linkliste = path self.logic.set_linkliste(path) def _on_layer_changed(self, layer) -> None: self.logic.save_verfahrensgebiet_layer(layer) self._pufferlayer = layer def _create_new_gpkg(self) -> None: file_path, _ = QFileDialog.getSaveFileName( self, "Neue Verfahrens-Datenbank anlegen", "", "Geopackage (*.gpkg)", ) if not file_path: return if not file_path.lower().endswith(".gpkg"): file_path += ".gpkg" # Delegation an TabALogic; TabALogic / Pruefmanager übernehmen Prüfungen self.logic.create_new_verfahrens_db(file_path) self.verfahrens_db = file_path try: self.file_widget.setFilePath(file_path) except Exception: pass self._update_group_color() def _on_load_fachdaten(self) -> None: """ Bestehender, kompakter Handler für 'Fachdaten laden'. Führt Dateiprüfung und DataGrabber.run aus (wie zuvor). """ pfad = self.file_widget.filePath() # Dateipruefer wird zur Laufzeit verwendet; hier nur der Aufruf pruefer = Dateipruefer(pfad=pfad, temporaer_erlaubt=True) ergebnis = pruefer.pruefe() ergebnis = self.pruefmanager.verarbeite(ergebnis) zielpfad = None if ergebnis.kontext is not None: try: zielpfad = str(ergebnis.kontext) except Exception: zielpfad = ergebnis.kontext # DataGrabber.run wird wie bisher aufgerufen; Signatur kann variieren. # Wir übergeben die bekannten Parameter; DataGrabber ist verantwortlich, # die Linkliste intern zu verwenden (z. B. aus TabALogic oder über Argumente). try: self.data_grabber.run( attributes_list=self._attributes_list, pufferlayer=self._pufferlayer, zielpfad=zielpfad, temporaer=(ergebnis.aktion == "temporaer_erzeugen"), temporaer_erlaubt=True, ) except Exception: # Fehler werden vom Pruefmanager / DataGrabber protokolliert pass def _on_run_pipeline(self) -> None: """ Neuer, vollständiger Pipeline-Handler, der: - Dateiprüfung (Verfahrens-DB) - DataGrabber-Ausführung (mit Linkliste) - Datenschreiber (schreiben, laden) - Logschreiber (Log-Datei) ausführt und Ergebnisse über den Pruefmanager protokolliert. """ # 1) Verfahrens-DB prüfen / ermitteln pfad = self.file_widget.filePath() pruefer = Dateipruefer(pfad=pfad, temporaer_erlaubt=True) ergebnis = pruefer.pruefe() ergebnis = self.pruefmanager.verarbeite(ergebnis) zielpfad = None if ergebnis.kontext is not None: try: zielpfad = str(ergebnis.kontext) except Exception: zielpfad = ergebnis.kontext if not zielpfad: # Falls kein Zielpfad ermittelt werden konnte, protokollieren und abbrechen pe_err = pruef_ergebnis( ok=False, meldung="Kein gültiger Speicherort für Verfahrens-DB ermittelt; Pipeline abgebrochen.", aktion="kein_dateipfad", kontext={}, ) self.pruefmanager.verarbeite(pe_err) return # 2) DataGrabber ausführen # Erwartung: DataGrabber.run gibt (daten_dict, processed_results) zurück. # Falls die konkrete Implementierung anders ist, passt dieser Aufruf entsprechend an. try: run_result = self.data_grabber.run( attributes_list=self._attributes_list, pufferlayer=self._pufferlayer, zielpfad=zielpfad, temporaer=(ergebnis.aktion == "temporaer_erzeugen"), temporaer_erlaubt=True, ) except Exception as exc: pe_err = pruef_ergebnis( ok=False, meldung=f"DataGrabber-Fehler: {exc}", aktion="datenabruf", kontext={}, ) self.pruefmanager.verarbeite(pe_err) return # Normalisiere Rückgabe: unterstütze sowohl None, einzelnes dict oder Tuple daten_dict = {} processed_results = [] if isinstance(run_result, tuple) and len(run_result) >= 2: daten_dict, processed_results = run_result[0], run_result[1] elif isinstance(run_result, dict) and "daten" in run_result: daten_dict = run_result # processed_results bleiben leer oder werden vom DataGrabber intern protokolliert else: # Wenn run() nichts zurückgibt, versuchen wir, auf DataGrabber intern gespeicherte Ergebnisse zuzugreifen daten_dict = getattr(self.data_grabber, "last_daten_dict", {}) or {} processed_results = getattr(self.data_grabber, "last_processed_results", []) or [] # 3) Datenschreiber: Daten in GPKG schreiben try: ds = Datenschreiber(pruefmanager=self.pruefmanager, gpkg_path=zielpfad) layer_infos = ds.schreibe_Daten(daten_dict=daten_dict, processed_results=processed_results, speicherort=zielpfad) except Exception as exc: pe_err = pruef_ergebnis( ok=False, meldung=f"Fehler beim Schreiben der Daten: {exc}", aktion="save_exception", kontext={}, ) self.pruefmanager.verarbeite(pe_err) return # 4) Layer laden und Stile anwenden try: ds.lade_Layer(layer_infos) except Exception as exc: pe_warn = pruef_ergebnis( ok=True, meldung=f"Fehler beim Laden der Layer: {exc}", aktion="layer_nicht_gefunden", kontext={}, ) self.pruefmanager.verarbeite(pe_warn) # 5) Log schreiben try: log_path = ds.schreibe_log(processed_results=processed_results, speicherort=zielpfad) # Optional: zeige Erfolgsmeldung try: QMessageBox.information(self, "Pipeline abgeschlossen", f"Pipeline erfolgreich abgeschlossen.\nLog: {log_path}") except Exception: pass except Exception as exc: pe_warn = pruef_ergebnis( ok=True, meldung=f"Log konnte nicht geschrieben werden: {exc}", aktion="standarddatei_vorschlagen", kontext={}, ) self.pruefmanager.verarbeite(pe_warn) # --------------------------------------------------------- # Raumfilter Callback # --------------------------------------------------------- def _on_raumfilter_changed(self, value: str) -> None: # Persistiere Auswahl in Projekt-Variable; Prüfungen übernimmt die Laufzeitlogik set_variable(RAUMFILTER_VAR, value, scope="project") # --------------------------------------------------------- # UI-Helfer # --------------------------------------------------------- def _prompt_user_to_select_file(self) -> None: fname, _ = QFileDialog.getOpenFileName( self, "Verfahrens-DB auswählen", "", "Geopackage (*.gpkg)", ) if fname: try: self.file_widget.setFilePath(fname) except Exception: try: self.file_widget.setFileName(fname) except Exception: self.file_widget.setProperty("filePath", fname) self.verfahrens_db = fname self.logic.set_verfahrens_db(fname) self._update_group_color() def _update_group_color(self) -> None: if self.verfahrens_db: self.group_button.setStyleSheet("font-weight: bold;") else: self.group_button.setStyleSheet("")