diff --git a/assets/Biotope_Offenland.qml b/assets/Biotope_Offenland.qml new file mode 100644 index 0000000..a6c2753 --- /dev/null +++ b/assets/Biotope_Offenland.qml @@ -0,0 +1,808 @@ + + + + 1 + 1 + 1 + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 0 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + + + 0 + generatedlayout + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + "BIOTOPNAME" + + 2 + diff --git a/assets/Fliessgewaesser.qml b/assets/Fliessgewaesser.qml new file mode 100644 index 0000000..ddf2ab3 --- /dev/null +++ b/assets/Fliessgewaesser.qml @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 0 + 1 + diff --git a/assets/GIS_63000F_Objekt_Denkmalschutz.qml b/assets/GIS_63000F_Objekt_Denkmalschutz.qml new file mode 100644 index 0000000..06bb9e5 --- /dev/null +++ b/assets/GIS_63000F_Objekt_Denkmalschutz.qml @@ -0,0 +1,609 @@ + + + + 1 + 1 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + "gml_id" + + + + + + 0 + 0 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + + + 0 + generatedlayout + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + "gml_id" + + 2 + diff --git a/assets/GIS_Flst_Beschriftung_ALKIS_NAS.qml b/assets/GIS_Flst_Beschriftung_ALKIS_NAS.qml new file mode 100644 index 0000000..5e40734 --- /dev/null +++ b/assets/GIS_Flst_Beschriftung_ALKIS_NAS.qml @@ -0,0 +1,349 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 0 + 2 + diff --git a/assets/GIS_LfULG_LSG.qml b/assets/GIS_LfULG_LSG.qml new file mode 100644 index 0000000..28082ba --- /dev/null +++ b/assets/GIS_LfULG_LSG.qml @@ -0,0 +1,371 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 0 + 2 + diff --git a/assets/GIS_P41_60100A_FFH.qml b/assets/GIS_P41_60100A_FFH.qml new file mode 100644 index 0000000..b096d20 --- /dev/null +++ b/assets/GIS_P41_60100A_FFH.qml @@ -0,0 +1,575 @@ + + + + 1 + 1 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 0 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + + + 0 + generatedlayout + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + "OBJECTID" + + 2 + diff --git a/assets/GIS_P41_60100A_SPA.qml b/assets/GIS_P41_60100A_SPA.qml new file mode 100644 index 0000000..465fddc --- /dev/null +++ b/assets/GIS_P41_60100A_SPA.qml @@ -0,0 +1,536 @@ + + + + 1 + 1 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + "OBJECTID_1" + + + + + + 0 + 0 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + + + 0 + generatedlayout + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + "OBJECTID_1" + + 2 + diff --git a/assets/Gebietskulisse_pvfvo.qml b/assets/Gebietskulisse_pvfvo.qml new file mode 100644 index 0000000..ddf4d2f --- /dev/null +++ b/assets/Gebietskulisse_pvfvo.qml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 0 + 2 + diff --git a/assets/Haupteinzugsgebiete.qml b/assets/Haupteinzugsgebiete.qml new file mode 100644 index 0000000..14945a8 --- /dev/null +++ b/assets/Haupteinzugsgebiete.qml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 0 + 2 + diff --git a/assets/Linkliste.xlsx b/assets/Linkliste.xlsx index bb18ee5..3a22bc8 100644 Binary files a/assets/Linkliste.xlsx and b/assets/Linkliste.xlsx differ diff --git a/assets/Standgewässer.qml b/assets/Standgewässer.qml new file mode 100644 index 0000000..519ad5f --- /dev/null +++ b/assets/Standgewässer.qml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 0 + 2 + diff --git a/assets/Teileinzugsgebiete.qml b/assets/Teileinzugsgebiete.qml new file mode 100644 index 0000000..0bba48a --- /dev/null +++ b/assets/Teileinzugsgebiete.qml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 0 + 2 + diff --git a/assets/Verfahrensgebiet.qml b/assets/Verfahrensgebiet.qml new file mode 100644 index 0000000..474e368 --- /dev/null +++ b/assets/Verfahrensgebiet.qml @@ -0,0 +1,140 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 0 + 2 + diff --git a/assets/WEA_wald.qml b/assets/WEA_wald.qml new file mode 100644 index 0000000..d5611d4 --- /dev/null +++ b/assets/WEA_wald.qml @@ -0,0 +1,189 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 0 + 2 + diff --git a/metadata.txt b/metadata.txt index eb8ba90..1467e0e 100644 --- a/metadata.txt +++ b/metadata.txt @@ -1,8 +1,9 @@ [general] name=LNO Sachsen | Plan41 qgisMinimumVersion=3.0 +qgisMaximumVersion=4.99 description=Plugin zum Erzeugen der Pläne nach §38 und §41 -version=25.11.3 +version=26.3.11 author=Michael Otto email=michael.otto@landkreis-mittelsachsen.de about=Plugin zum Erzeugen der Pläne nach §38 und §41 @@ -10,4 +11,5 @@ category=Plugins homepage=https://entwicklung.vln-sn.de/AG_QGIS/Plugin_SN_Plan41 repository=https://entwicklung.vln-sn.de/AG_QGIS/Repository supportsQt6=true -experimental=true \ No newline at end of file +experimental=true + diff --git a/modules/listenauswerter.py b/modules/listenauswerter.py index cf9a654..4602aa5 100644 --- a/modules/listenauswerter.py +++ b/modules/listenauswerter.py @@ -91,7 +91,7 @@ class Listenauswerter: ident = raw.get("ident") inhalt = raw.get("Inhalt") link = raw.get("Link") - stildatei = raw.get("Stildatei") + stildatei = raw.get("stildatei") or raw.get("Stildatei") provider = raw.get("Provider") # Pflichtfelder prüfen @@ -127,6 +127,7 @@ class Listenauswerter: "ident": ident, "Inhalt": inhalt, "Link": link, + "stildatei": stildatei_value, "Stildatei": stildatei_value, "Provider": provider_norm, } diff --git a/ui/tab_a_logic.py b/ui/tab_a_logic.py index f94aa75..d226c82 100644 --- a/ui/tab_a_logic.py +++ b/ui/tab_a_logic.py @@ -1,17 +1,27 @@ + """ sn_plan41/ui/tab_a_logic.py – Fachlogik für Tab A (Daten) """ from __future__ import annotations +from sn_basis.functions.sys_wrapper import get_plugin_root, join_path, file_exists from typing import Any, Dict, List, Optional from collections.abc import Mapping as _Mapping import os +import datetime +import json +import tempfile from sn_basis.functions.qgiscore_wrapper import ( QgsVectorFileWriter, QgsVectorLayer, QgsProject, + QgsGeometry, + QgsFeature, + QgsField, + QgsFeatureRequest, + QgsCoordinateReferenceSystem, ) @@ -19,14 +29,25 @@ from sn_basis.functions.variable_wrapper import ( get_variable, set_variable, ) -from sn_basis.functions.sys_wrapper import file_exists from sn_basis.functions.ly_existence_wrapper import layer_exists from sn_basis.functions.ly_metadata_wrapper import get_layer_type +from sn_basis.functions.qt_wrapper import QVariant +from sn_basis.functions.dialog_wrapper import create_progress_dialog +from sn_basis.functions.message_wrapper import info, warning, error + # Prüfer-Typen from sn_basis.modules.Pruefmanager import Pruefmanager from sn_basis.modules.linkpruefer import Linkpruefer from sn_basis.modules.stilpruefer import Stilpruefer +from sn_basis.modules.Dateipruefer import Dateipruefer +from sn_basis.modules.layerpruefer import Layerpruefer +from sn_basis.modules.LayerLoader import LayerLoader +from sn_basis.modules.Datenschreiber import Datenschreiber + +from sn_basis.modules.pruef_ergebnis import pruef_ergebnis +from sn_basis.modules.DataGrabber import DataGrabber, SourceType, SourceDict +from sn_basis.modules.Datenabruf import Datenabruf Row = Dict[str, Any] DataDict = Dict[str, List[Row]] @@ -34,13 +55,16 @@ DataDict = Dict[str, List[Row]] class TabALogic: """ Kapselt die Fachlogik von Tab A. Verfahrens-DB wird **nicht** bei Pfad-Auswahl, - sondern erst beim ersten Layer-Schreiben angelegt (alte Logik). + sondern erst beim ersten Layer-Schreiben angelegt. """ def __init__(self, pruefmanager: Pruefmanager, link_pruefer: Linkpruefer, stil_pruefer: Stilpruefer) -> None: self.pruefmanager = pruefmanager self.link_pruefer = link_pruefer self.stil_pruefer = stil_pruefer + self.data_grabber: Optional[DataGrabber] = None + def _log(self, msg: str) -> None: + print(f"[TabALogic] {msg}") # ------------------------------- # Verfahrens-Datenbank (Pfad-Management) @@ -132,26 +156,10 @@ class TabALogic: # Verfahrensgebiet-Layer # ------------------------------- - def save_verfahrensgebiet_layer(self, layer) -> None: - if layer is None: - set_variable("verfahrensgebiet_layer", "", scope="project") - return - - if not hasattr(layer, "id") or not callable(layer.id): - set_variable("verfahrensgebiet_layer", "", scope="project") - return - - try: - layer_id = layer.id() - except Exception: - set_variable("verfahrensgebiet_layer", "", scope="project") - return - - if not layer_id: - set_variable("verfahrensgebiet_layer", "", scope="project") - return - - set_variable("verfahrensgebiet_layer", layer_id, scope="project") + def save_verfahrensgebiet_layer(self, layer: QgsVectorLayer) -> None: + """Speichert die Verfahrensgebiet-Layer-ID, unter Annahme, dass der Layer prevalidiert ist.""" + layer_id = layer.id() if layer is not None else "" + set_variable("verfahrensgebiet_layer", layer_id or "", scope="project") def load_verfahrensgebiet_layer_id(self) -> Optional[str]: value = get_variable("verfahrensgebiet_layer", scope="project") @@ -163,3 +171,900 @@ class TabALogic: layer_type = get_layer_type(layer) return layer_type == "vector" + + # === PIPELINE === + def _on_run_pipeline( + self, + source: str, + linkliste: str | None, + raumfilter: str, + progress: Optional[Any] = None, + ) -> Optional[Dict[str, Any]]: + """Pipeline starten; Linkliste wird ausgelesen und geprüft, dann Datenabruf ausgeführt.""" + self._log("Pipeline startet") + + if not self.pruefmanager or not self.data_grabber: + self._log("Fehler: Pruefmanager oder DataGrabber fehlt") + return None + + # 1) Verfahrens-DB prüfen und als aktive DB setzen + datei_ergebnis = Dateipruefer( + source, + basis_pfad="", + leereingabe_erlaubt=False, + standarddatei=None, + temporaer_erlaubt=True, + verfahrens_db_modus=True, + ).pruefe() + + datei_ergebnis = self.pruefmanager.verarbeite(datei_ergebnis) + if not datei_ergebnis.ok: + self._log("Verfahrens-DB-Pruefung fehlgeschlagen") + return None + + final_pfad = str(datei_ergebnis.kontext or source) + self.set_verfahrens_db(final_pfad) + + # Nach bestätigter Entscheidung: sofort Fortschrittsdialog zeigen + if progress is None: + progress = create_progress_dialog(1, "Fachdaten laden", "Prüfe Eingaben...") + else: + progress.set_total(1) + progress.set_value(0) + progress.set_label("Prüfe Eingaben...") + + # 2) Linkliste auflösen, falls leer Standardlinkliste verwenden + linkliste_final = self._resolve_linkliste(linkliste) + if linkliste_final is None: + self._log("Linkliste kann nicht aufgelöst werden") + return None + else: + self._log(f"Linkliste final: '{linkliste_final}'") + # 3) Raumfilter prüfen + raumfilter_layer = self._resolve_raumfilter(raumfilter, final_pfad) + if raumfilter in ("Verfahrensgebiet", "Pufferlayer") and raumfilter_layer is None: + self._log(f"Raumfilter '{raumfilter}' nicht verfügbar") + return None + + # 4) Lade-Status initialisieren (funktioniert ab Bestätigung überschreiben/anhängen) + if progress is None: + # placeholder mit 1; tatsächliche Gesamtzahl kennt DataGrabber später + progress = create_progress_dialog(1, "Fachdaten laden", "Prüfe Eingaben...") + else: + progress.set_total(1) + progress.set_value(0) + progress.set_label("Prüfe Eingaben...") + + # 5) Daten aus Linkliste laden und prüfen + source_dict, grabber_summary = self.data_grabber.run(linkliste_final) + self._log(f"DataGrabber: {grabber_summary.meldung} [{grabber_summary.aktion}]") + + # DEBUG: detaillierter Status + print("[TabALogic] ... Debug: source_dict keys:", list(source_dict.keys())) + print("[TabALogic] ... Debug: rows count:", len(source_dict.get("rows", []))) + if source_dict.get("rows"): + for i, row in enumerate(source_dict.get("rows", []), start=1): + print(f"[TabALogic] ... Debug: row {i}: {row}") + + if not source_dict.get("rows"): + self._log("Keine validen Linkliste-Einträge für Datenabruf") + print("[TabALogic] ... STOP: rows:", len(source_dict.get("rows", []))) + return None + + total_rows = len(source_dict.get("rows", [])) + if progress is not None: + if hasattr(progress, "set_total"): + progress.set_total(max(total_rows, 1)) + elif hasattr(progress, "setMaximum"): + progress.setMaximum(max(total_rows, 1)) + else: + progress.total = max(total_rows, 1) + progress.set_value(0) + progress.set_label("Lade Daten...") + + if not grabber_summary.ok: + self._log("Warnung: DataGrabber meldet fehlerhafte Zeilen, fahre mit Validierungsdaten fort") + + # 5) Datenabruf (aus validierten Zeilen) + datenabruf = Datenabruf(self.pruefmanager) + result_dict, datenabruf_results = datenabruf.datenabruf( + result_dict=source_dict, + raumfilter=raumfilter, + verfahrensgebiet_layer=raumfilter_layer, + speicherort=final_pfad, + pruef_ergebnisse=[grabber_summary], + progress=progress, + ) + + self._log("Datenabruf abgeschlossen") + + pipeline_context = { + "source": final_pfad, + "linkliste": linkliste_final, + "raumfilter": raumfilter_layer, + "raumfilter_name": raumfilter, + "source_dict": source_dict, + "result_dict": result_dict, + "datenabruf_results": datenabruf_results, + } + + # 6) Lade Dienste in das Projekt aus result_dict + load_summary = self._load_dienste_aus_result_dict(source_dict, pipeline_context, progress=progress) + + if progress is not None: + progress.set_value(total_rows) + progress.set_label("Pipeline abgeschlossen. Bitte OK klicken, um den Dialog zu schließen.") + + # 7) Log-Datei schreiben + self._write_markdown_log(final_pfad, source_dict, pipeline_context, load_summary) + + print("=" * 60 + "\n") + return pipeline_context + + def _load_dienste_aus_result_dict(self, source_dict: DataDict, pipeline_context: Dict[str, Any], progress: Optional[Any] = None) -> Dict[str, Any]: + """Lädt Dienste (aus Linkliste) ins Projekt und persistiert optional mit Datenschreiber.""" + rows = source_dict.get("rows", []) + total = len(rows) + loaded_count = 0 + skipped_outside = 0 + aborted = False + if not rows: + self._log("Keine Dienste zum Laden") + return + + final_pfad = pipeline_context.get("source") or "" + use_datenschreiber = bool(final_pfad) + + datenschreiber = None + if use_datenschreiber: + datenschreiber = Datenschreiber(self.pruefmanager, gpkg_path=final_pfad) + + daten_dict: Dict[str, Any] = {"daten": {}} + + raumfilter_layer = pipeline_context.get("raumfilter") + raumfilter_name = pipeline_context.get("raumfilter_name", "unbekannt") + raumfilter_crs_authid = None + if raumfilter_layer is not None and hasattr(raumfilter_layer, "crs") and callable(getattr(raumfilter_layer, "crs")): + try: + crs = raumfilter_layer.crs() + if crs is not None and hasattr(crs, "authid") and callable(getattr(crs, "authid")): + raumfilter_crs_authid = crs.authid() + except Exception: + raumfilter_crs_authid = None + # Für den späteren Filter benötigen wir entweder die reine Ausdehnung + # (Pufferlayer) oder – im Falle eines echten Verfahrensgebiets – die + # vollständige Geometrie. Die Filtermethode wird im Schleifenrumpf + # ausgewählt. + raumfilter_extent = None + if raumfilter_layer is not None and getattr(raumfilter_layer, 'extent', None) is not None: + raumfilter_extent = raumfilter_layer.extent() + + temp_layers: List[Any] = [] + layer_loader = LayerLoader(self.pruefmanager, stil_pruefer=self.stil_pruefer, layer_pruefer=self.link_pruefer) + + # Statistiken für Log: Raumfilter-Info pro Dienst + row_stats: List[Dict[str, Any]] = [] + layer_call_status: Dict[str, str] = {} + + + for idx, row in enumerate(rows, start=1): + ident = str(row.get("ident") or "") + provider = str(row.get("Provider", "")).lower() + link = str(row.get("Link", "")) + thema = str(row.get("Inhalt") or row.get("ident") or "Dienst") + style = row.get("stildatei") + + daten_map = (pipeline_context.get("result_dict") or {}).get("daten", {}) + fetched_features = daten_map.get(ident, []) if isinstance(daten_map, dict) else [] + fetched_count = len(fetched_features) if isinstance(fetched_features, list) else None + + if progress is not None: + progress.set_label(f"Lade Dienst {idx}/{total}: {thema}") + progress.set_value(idx) + if progress.is_canceled(): + aborted = True + layer_call_status[thema] = "abbruch_vor_layeraufruf" + self._log("Nutzerabbruch: Pipeline gestoppt") + self.pruefmanager.verarbeite( + pruef_ergebnis( + ok=False, + meldung="Pipeline durch Benutzer abgebrochen", + aktion="abbruch", + kontext={"dienst": thema, "schritt": idx}, + ) + ) + break + + + self._log(f"Lade Dienst '{thema}' ({provider})") + self._log(f"[DEBUG] Layeraufruf startet: thema='{thema}', provider='{provider}', link='{link}'") + layer_call_status[thema] = "layeraufruf_start" + + layer = layer_loader.create_layer(provider, link, thema) + + if not layer: + layer_call_status[thema] = "layer_nicht_ladbar" + self._log(f"[DEBUG] Layeraufruf fehlgeschlagen: thema='{thema}'") + row_stats.append({ + "dienst": thema, + "provider": provider, + "link": link, + "style": style or "", + "datenabruf_features": fetched_count, + "total_features": None, + "filtered_features": None, + "status": "layer_nicht_ladbar", + "raumfilter": raumfilter_name, + }) + continue + + layer_call_status[thema] = "layeraufruf_ok" + self._log(f"[DEBUG] Layeraufruf erfolgreich: thema='{thema}'") + + if progress is not None and progress.is_canceled(): + aborted = True + layer_call_status[thema] = "abbruch_nach_layeraufruf" + self._log("Nutzerabbruch nach Layer-Erzeugung") + break + + # Je nach Typ des Filters einen geeigneten Filter anwenden. + if raumfilter_layer and raumfilter_name == "Verfahrensgebiet": + # echte Geometrie-Schnittmenge, nicht nur BBox + layer_for_write = layer_loader.filter_by_layer( + layer, + raumfilter_layer, + cancel_callback=(progress.is_canceled if progress is not None else None), + ) + else: + layer_for_write = layer_loader.filter_by_extent( + layer, + raumfilter_extent, + cancel_callback=(progress.is_canceled if progress is not None else None), + source_layer=raumfilter_layer, + ) + + if progress is not None and progress.is_canceled(): + aborted = True + layer_call_status[thema] = "abbruch_nach_raumfilter" + self._log("Nutzerabbruch nach Raumfilter") + break + + # Zähle Features vor/nach Raumfilter + total_features = None + filtered_features = None + try: + if layer is not None and hasattr(layer, "featureCount"): + total_features = int(layer.featureCount()) + except Exception: + total_features = None + + if layer_for_write is not None and hasattr(layer_for_write, "featureCount"): + try: + filtered_features = int(layer_for_write.featureCount()) + except Exception: + filtered_features = None + + if not layer_for_write: + layer_call_status[thema] = "raumfilter_ausserhalb" + self._log(f"Dienst {thema} ist außerhalb des Raumfilters") + skipped_outside += 1 + row_stats.append({ + "dienst": thema, + "provider": provider, + "link": link, + "style": style or "", + "datenabruf_features": fetched_count, + "total_features": total_features, + "filtered_features": 0, + "status": "außerhalb", + "raumfilter": raumfilter_name, + }) + continue + + if style: + layer_loader.apply_style(layer_for_write, style) + + row_stats.append({ + "dienst": thema, + "provider": provider, + "link": link, + "style": style or "", + "datenabruf_features": fetched_count, + "total_features": total_features, + "filtered_features": filtered_features, + "status": "geladen", + "raumfilter": raumfilter_name, + }) + layer_call_status[thema] = "geladen" + self._log(f"[DEBUG] Dienst geladen: thema='{thema}', provider='{provider}', filtered_features={filtered_features}") + + + if provider == "wms": + # WMS ist Raster und wird nicht in GPKG geschrieben. + # Im temporären Modus wird er trotzdem direkt geladen. + loaded_count += 1 + if use_datenschreiber: + self._log(f"WMS-Layer {thema} wird nicht in GPKG gespeichert, nur in Projekt (temporär)") + # Während Datenbankmodus: wir speichern nicht in daten_dict, + # aber für gute Sichtbarkeit laden wir nach erfolgreichem Schreibprozess. + temp_layers.append(layer) + else: + temp_layers.append(layer) + continue + + if use_datenschreiber and datenschreiber: + daten_dict["daten"][thema] = { + "layer": layer_for_write, + "style_path": style, + } + else: + temp_layers.append(layer_for_write) + + loaded_count += 1 + + if use_datenschreiber and datenschreiber and daten_dict["daten"]: + self._log(f"Schreibe {len(daten_dict['daten'])} Layer in {final_pfad}") + results = datenschreiber.schreibe_Daten( + daten_dict, + processed_results=pipeline_context.get("datenabruf_results", []), + speicherort=final_pfad, + ) + datenschreiber.lade_Layer(results) + self._log("Datenschreiber abgeschlossen") + elif temp_layers: + self._log(f"Temporärmodus: Lade {len(temp_layers)} Layer ins Projekt") + for layer in temp_layers: + QgsProject.instance().addMapLayer(layer) + self._log("Temporärmodus: Layer im Projekt geladen") + else: + self._log("Keine Layer zum Laden (kein persistierter GPkg-Write).") + + self._log(f"Dienst-Laden fertig ({len(rows)} Zeilen)") + + return { + "row_count": len(rows), + "loaded_count": loaded_count, + "skipped_outside": skipped_outside, + "aborted": aborted, + "row_stats": row_stats, + "layer_call_status": layer_call_status, + "raumfilter_name": raumfilter_name, + } + + def _create_local_layer_from_fetched_features( + self, + thema: str, + features: List[Any], + crs_authid: Optional[str] = None, + ) -> Optional[QgsVectorLayer]: + """Erzeugt aus bereits geholten GeoJSON-Features einen lokalen OGR-Layer. + + Verhindert einen zweiten potentiell blockierenden Remote-Aufruf (WFS/REST). + """ + if not features: + return None + + normalized_features: List[Dict[str, Any]] = [] + detected_crs_authid: Optional[str] = None + for feature in features: + if not isinstance(feature, dict): + continue + + # Fall 1: bereits GeoJSON-Feature + if feature.get("type") == "Feature" and isinstance(feature.get("geometry"), dict): + normalized_features.append(feature) + continue + + # Fall 2: ArcGIS Feature-JSON -> GeoJSON konvertieren + attributes = feature.get("attributes") + geometry = feature.get("geometry") + if not isinstance(attributes, dict) or not isinstance(geometry, dict): + continue + + if detected_crs_authid is None: + try: + sr = geometry.get("spatialReference") + if isinstance(sr, dict): + wkid = sr.get("latestWkid") or sr.get("wkid") + if wkid: + detected_crs_authid = f"EPSG:{int(wkid)}" + except Exception: + detected_crs_authid = None + + geojson_geometry: Dict[str, Any] | None = None + + # Point + if "x" in geometry and "y" in geometry: + geojson_geometry = { + "type": "Point", + "coordinates": [geometry.get("x"), geometry.get("y")], + } + # MultiPoint + elif isinstance(geometry.get("points"), list): + geojson_geometry = { + "type": "MultiPoint", + "coordinates": geometry.get("points", []), + } + # LineString / MultiLineString + elif isinstance(geometry.get("paths"), list): + paths = geometry.get("paths", []) + if len(paths) == 1: + geojson_geometry = { + "type": "LineString", + "coordinates": paths[0], + } + else: + geojson_geometry = { + "type": "MultiLineString", + "coordinates": paths, + } + # Polygon / MultiPolygon + elif isinstance(geometry.get("rings"), list): + rings = geometry.get("rings", []) + cleaned_rings = [ + ring for ring in rings + if isinstance(ring, list) and len(ring) >= 4 + ] + if len(cleaned_rings) == 1: + geojson_geometry = { + "type": "Polygon", + "coordinates": cleaned_rings, + } + elif len(cleaned_rings) > 1: + # Robuster Fallback für ArcGIS-Ringe: + # Mehrere Ringe werden als MultiPolygon behandelt, + # damit nicht versehentlich alle Ringe als Löcher eines + # einzigen Polygons interpretiert werden. + geojson_geometry = { + "type": "MultiPolygon", + "coordinates": [[ring] for ring in cleaned_rings], + } + + if geojson_geometry is None: + continue + + normalized_features.append( + { + "type": "Feature", + "geometry": geojson_geometry, + "properties": attributes, + } + ) + + if not normalized_features: + self._log(f"[DEBUG] Keine konvertierbaren Features für lokalen Layer: thema='{thema}'") + return None + + try: + payload = { + "type": "FeatureCollection", + "features": normalized_features, + } + + with tempfile.NamedTemporaryFile( + suffix=".geojson", + delete=False, + mode="w", + encoding="utf-8", + ) as fh: + json.dump(payload, fh, ensure_ascii=False) + tmp_path = fh.name + + layer = QgsVectorLayer(tmp_path, thema, "ogr") + if layer and layer.isValid(): + target_crs = detected_crs_authid or crs_authid + if target_crs and QgsCoordinateReferenceSystem is not None and hasattr(layer, "setCrs"): + try: + layer.setCrs(QgsCoordinateReferenceSystem(target_crs)) + except Exception: + pass + self._log( + f"[DEBUG] Lokaler Layer gültig: thema='{thema}', " + f"input_features={len(features)}, geojson_features={len(normalized_features)}, " + f"layer_features={layer.featureCount()}, crs='{target_crs or 'unbekannt'}'" + ) + return layer + + self._log(f"[DEBUG] Lokaler Layer aus Datenabruf ungültig: thema='{thema}', pfad='{tmp_path}'") + return None + except Exception as exc: + self._log(f"[DEBUG] Fehler beim Erzeugen lokaler Featureschicht für {thema}: {exc}") + return None + + def _write_markdown_log( + self, + final_pfad: str, + source_dict: DataDict, + pipeline_context: Dict[str, Any], + load_summary: Dict[str, Any], + ) -> None: + """Schreibt den Pipeline-Log (Markdown).""" + lines = [ + "# Plan41 Fachdaten-Ladevorgang", + "", + f"**Datum**: {datetime.datetime.now().isoformat()}", + f"**Verfahrens-DB**: {final_pfad or 'temporär'}", + f"**Linkliste**: {pipeline_context.get('linkliste')}", + "", + "## Zusammenfassung", + "", + f"- **Zeilen gesamt**: {load_summary.get('row_count', 0)}", + f"- **Geladene Dienste**: {load_summary.get('loaded_count', 0)}", + f"- **Außerhalb Raumfilter**: {load_summary.get('skipped_outside', 0)}", + f"- **Abgebrochen**: {load_summary.get('aborted', False)}", + f"- **Raumfilter**: {load_summary.get('raumfilter_name', 'unbekannt')}", + f"- **Raumfilter-Typ**: {pipeline_context.get('raumfilter_name', 'unbekannt')}", + "", + "## Dienstliste", + "", + "| Dienst | Provider | Linkadresse | Aufrufstatus | Ergebnisstatus |", + "|---|---|---|---|---|", + ] + + status_by_dienst = { + str(stat.get("dienst", "")): str(stat.get("status", "n/a")) + for stat in load_summary.get("row_stats", []) + } + aufrufstatus_by_dienst = { + str(key): str(value) + for key, value in (load_summary.get("layer_call_status", {}) or {}).items() + } + + for row in source_dict.get('rows', []): + dienst = row.get('Inhalt') or row.get('ident') or '' + provider = row.get('Provider') or '' + link = row.get('Link') or '' + aufrufstatus = aufrufstatus_by_dienst.get(str(dienst), "nicht_aufgerufen") + ergebnisstatus = status_by_dienst.get(str(dienst), "n/a") + lines.append(f"| {dienst} | {provider} | {link} | {aufrufstatus} | {ergebnisstatus} |") + + lines.extend([ + "", + "## Raumfilter-Statistik", + "", + "| Dienst | Provider | Linkadresse | Datenabruf-Objekte | Gesamt-Objekte | Gefilterte Objekte | Raumfilter | Status |", + "|---|---|---|---|---|---|---|---|", + ]) + + for stat in load_summary.get('row_stats', []): + lines.append( + f"| {stat.get('dienst', '')} | {stat.get('provider', '')} | {stat.get('link', '')} | {stat.get('datenabruf_features', 'n/a')} | {stat.get('total_features', 'n/a')} | {stat.get('filtered_features', 'n/a')} | {stat.get('raumfilter', '')} | {stat.get('status', 'n/a')} |" + ) + + markdown = "\n".join(lines) + + if final_pfad: + log_dir = os.path.dirname(final_pfad) + os.makedirs(log_dir, exist_ok=True) + log_file = os.path.join(log_dir, "plan41_lade_log.md") + try: + with open(log_file, "w", encoding="utf-8") as fh: + fh.write(markdown) + + self.pruefmanager.verarbeite( + pruef_ergebnis( + ok=True, + meldung=f"Lade-Log gespeichert: {log_file}", + aktion="log_geschrieben", + kontext={"log_file": log_file}, + ) + ) + info("Lade-Log", f"Lade-Protokoll gespeichert: {log_file}", duration=10) + except Exception as exc: + self.pruefmanager.verarbeite( + pruef_ergebnis( + ok=False, + meldung=f"Fehler beim Schreiben des Logle (md): {exc}", + aktion="log_schreiben_fehlgeschlagen", + kontext={"error": str(exc)}, + ) + ) + warning("Lade-Log", f"Konnte Datei nicht schreiben: {exc}", duration=10) + else: + # temporärer Modus: nur anzeigen + info("Lade-Log (temporär)", markdown, duration=20) + + + def _clone_layer_with_extent(self, layer: QgsVectorLayer, extent, thema: str) -> QgsVectorLayer | None: + """Erstellt eine Memory-Kopie von mit Geometrien im BBOX-Raumfilter.""" + try: + request = QgsFeatureRequest().setFilterRect(extent) + features = list(layer.getFeatures(request)) + if not features: + return None + + geom_type_map = {0: "Point", 1: "LineString", 2: "Polygon"} + geom_type = geom_type_map.get(layer.geometryType(), "Polygon") + uri = f"{geom_type}?crs={layer.crs().authid()}" + + filtered_layer = QgsVectorLayer(uri, f"{thema}_BBOX", "memory") + if not filtered_layer or not filtered_layer.isValid(): + self._log(f"Fehler beim Erzeugen des temporären Filterlayers für {thema}") + return None + + provider = filtered_layer.dataProvider() + provider.addAttributes(layer.fields()) + filtered_layer.updateFields() + provider.addFeatures(features) + filtered_layer.updateExtents() + + return filtered_layer + except Exception as e: + self._log(f"Fehler beim Filtern von {thema} nach Raumfilter: {e}") + return None + + + def _resolve_linkliste(self, linkliste: str | None) -> str | None: + """ + Prüft und normalisiert den Linklisten-Pfad. + + Rückgabe: + - gültiger Pfad zur Linkliste (str) + - None → Pipeline abbrechen + """ + + # -------------------------------------------------- + # Standard-Linkliste (plattformneutral) + # -------------------------------------------------- + plugin_root = get_plugin_root() + standard_linkliste = join_path(plugin_root, "sn_plan41","assets", "Linkliste.xlsx") + + # -------------------------------------------------- + # 🔹 LEERE EINGABE → AUTOMATISCH STANDARDDATEI + # -------------------------------------------------- + if not linkliste: + linkliste_final = str(standard_linkliste) + self.set_linkliste(linkliste_final) + return linkliste_final + + # -------------------------------------------------- + # Dateiprüfung nur bei expliziter Eingabe + # -------------------------------------------------- + + pruefer = Dateipruefer( + pfad=linkliste, + leereingabe_erlaubt=True, + standarddatei=str(standard_linkliste), + ) + + ergebnis = pruefer.pruefe() + + # -------------------------------------------------- + # Entscheidung über Pruefmanager + # -------------------------------------------------- + ergebnis = self.pruefmanager.verarbeite(ergebnis) + + if not ergebnis.ok: + # Nutzer hat abgebrochen oder Fehler nicht bestätigt + return None + + # -------------------------------------------------- + # Erfolgsfall → geprüften Pfad übernehmen + # -------------------------------------------------- + linkliste_final = str(ergebnis.kontext) + + # Optional: Projektvariable aktualisieren + self.set_linkliste(linkliste_final) + + return linkliste_final + + def _resolve_raumfilter(self, raumfilter: str, source: str) -> QgsVectorLayer | None: + self._log(f"Raumfilter-Auswahl: '{raumfilter}'") + self._log(f"Source: '{source}'") + + if raumfilter == "Verfahrensgebiet": + layer = self._get_verfahrensgebiet_layer() + self._log( + "Verfahrensgebiet gefunden" + if layer else + "❌ Kein Verfahrensgebiet im Projekt" + ) + return layer + + if raumfilter == "Pufferlayer": + self._log("Pufferlayer-Modus aktiv") + return self._handle_pufferlayer(source) + + self._log("Kein Raumfilter gewählt") + return None + + + def _get_verfahrensgebiet_layer(self) -> QgsVectorLayer | None: + layer_id = self.load_verfahrensgebiet_layer_id() + self._log(f"Verfahrensgebiet-Layer-ID: {layer_id}") + + if not layer_id: + self._log("❌ Keine Layer-ID gespeichert") + return None + + layer = QgsProject.instance().mapLayer(layer_id) + if not layer: + self._log("❌ Layer-ID existiert nicht im Projekt") + return None + + if not self.is_valid_verfahrensgebiet_layer(layer): + self._log("❌ Layer ist kein gültiger Vektorlayer") + return None + + self._log(f"Verfahrensgebiet-Layer OK: '{layer.name()}'") + return layer + + + def _handle_pufferlayer(self, source: str) -> QgsVectorLayer | None: + self._log("Prüfe vorhandenen Pufferlayer im Projekt") + + existing = self._load_existing_pufferlayer() + if existing: + self._log("✔ Pufferlayer bereits im Projekt vorhanden") + return existing + + self._log("Kein Pufferlayer im Projekt") + + if source: + self._log("Prüfe Pufferlayer im Source") + exists = self._pufferlayer_exists_in_source(source) + self._log(f"Pufferlayer im Source vorhanden: {exists}") + + if exists: + return self._load_existing_pufferlayer() or self._create_pufferlayer() + + self._log("Erzeuge neuen Pufferlayer") + return self._create_pufferlayer() + + + def _load_existing_pufferlayer(self) -> QgsVectorLayer | None: + """ + Liefert einen vorhandenen Pufferlayer aus dem Projekt. + """ + layers = QgsProject.instance().mapLayersByName("Pufferlayer") + return layers[0] if layers else None + + + def _create_pufferlayer(self) -> QgsVectorLayer | None: + self._log("Starte Pufferlayer-Erstellung") + + basis_layer = self._get_verfahrensgebiet_layer() + if not basis_layer: + self._log("❌ Kein Verfahrensgebiet → kein Puffer möglich") + return None + source = self.load_verfahrens_db() + self._log(f"Basislayer: '{basis_layer.name()}'") + + layer = self.Pufferlayer_erstellen( + basis_layer=basis_layer, + distance=1000, + name="Pufferlayer", + source=source, + ) + + self._log( + "✔ Pufferlayer erfolgreich erzeugt" + if layer else + "❌ Pufferlayer-Erstellung fehlgeschlagen" + ) + return layer + + + from sn_basis.functions.qgiscore_wrapper import QgsVectorLayer + + def _pufferlayer_exists_in_source(self, source: str) -> bool: + """ + Prüft, ob im Source (z.B. GPKG) ein Layer namens 'Pufferlayer' existiert. + """ + if not source: + return False + + uri = f"{source}|layername=Pufferlayer" + layer = QgsVectorLayer(uri, "Pufferlayer", "ogr") + + return layer.isValid() + + + + def Pufferlayer_erstellen( + self, + basis_layer: QgsVectorLayer, + distance: float, + name: str, + source: str | None = None, + ) -> QgsVectorLayer | None: + """ + Erzeugt einen rechteckigen Pufferlayer (BBOX + Abstand) + um das Verfahrensgebiet. + + - Ohne Source → temporärer Memory-Layer + - Mit Source → Schreiben über Datenschreiber + + Parameters + ---------- + basis_layer : QgsVectorLayer + Verfahrensgebiet-Layer. + distance : float + Pufferabstand in Metern. + name : str + Name des Ziel-Layers. + source : str | None + Zielquelle (z.B. Verfahrens-DB) oder None für temporär. + + Returns + ------- + QgsVectorLayer | None + Neuer Pufferlayer oder None bei Fehler. + """ + if not basis_layer or not basis_layer.isValid(): + self._log("❌ Basislayer ungültig – kein Puffer möglich") + return None + + # -------------------------------------------------- + # 1. Rechteck-Geometrie (Extent + Puffer) + # -------------------------------------------------- + extent = basis_layer.extent().buffered(distance) + bbox_geom = QgsGeometry.fromRect(extent) + + # -------------------------------------------------- + # 2. CRS übernehmen + # -------------------------------------------------- + crs_auth = basis_layer.crs().authid() + uri = f"Polygon?crs={crs_auth}" + + mem_layer = QgsVectorLayer(uri, name, "memory") + provider = mem_layer.dataProvider() + provider.addAttributes([ + QgsField("id", QVariant.Int) + ]) + mem_layer.updateFields() + + + + # -------------------------------------------------- + # 4. Feature erzeugen + # -------------------------------------------------- + feat = QgsFeature(mem_layer.fields()) + feat.setGeometry(bbox_geom) + feat["id"] = 1 + provider.addFeature(feat) + mem_layer.updateExtents() + + # -------------------------------------------------- + # 5. Temporärer Modus → direkt ins Projekt + # -------------------------------------------------- + if not source: + QgsProject.instance().addMapLayer(mem_layer) + self._log("✔ Temporärer rechteckiger Pufferlayer erzeugt") + return mem_layer + + # -------------------------------------------------- + # 6. Persistenter Modus → Datenschreiber + # -------------------------------------------------- + writer = Datenschreiber( + pruefmanager=self.pruefmanager, + gpkg_path=source, + ) + + daten_dict = { + "daten": { + name: { + "layer": mem_layer + } + } + } + + + results = writer.schreibe_Daten( + daten_dict=daten_dict, + processed_results=[], + speicherort=source, + ) + + if not results: + self._log("❌ Schreiben des Pufferlayers fehlgeschlagen") + return None + + writer.lade_Layer(results) + + layers = QgsProject.instance().mapLayersByName(name) + if layers: + self._log("✔ Persistenter rechteckiger Pufferlayer geladen") + return layers[0] + + return None \ No newline at end of file diff --git a/ui/tab_a_ui.py b/ui/tab_a_ui.py index d771cc0..1da6369 100644 --- a/ui/tab_a_ui.py +++ b/ui/tab_a_ui.py @@ -31,6 +31,8 @@ from sn_basis.modules.Pruefmanager import Pruefmanager from sn_basis.modules.DataGrabber import DataGrabber from sn_basis.modules.Dateipruefer import Dateipruefer from sn_plan41.ui.tab_a_logic import TabALogic +from sn_basis.modules.linkpruefer import Linkpruefer +from sn_basis.modules.stilpruefer import Stilpruefer # Konstanten RAUMFILTER_VAR = "Raumfilter" @@ -65,7 +67,7 @@ class TabA(QWidget): # Services (werden von DockWidget gesetzt) self.pruefmanager: Optional[Pruefmanager] = None - self.data_grabber: Optional[DataGrabber] = None + self.logic: Optional[TabALogic] = None # UI-State @@ -89,6 +91,14 @@ class TabA(QWidget): """ self.pruefmanager = pruefmanager self.data_grabber = data_grabber + self.logic = TabALogic( + pruefmanager=self.pruefmanager, + link_pruefer=Linkpruefer(), + stil_pruefer=Stilpruefer(), + ) + + # DataGrabber in die Logik injizieren + self.logic.data_grabber = self.data_grabber def _build_ui(self) -> None: """Erstellt die komplette UI-Hierarchie mit allen Gruppen.""" @@ -168,6 +178,7 @@ class TabA(QWidget): main_layout.addWidget(QLabel("Raumfilter")) self._raumfilter_combo = QComboBox(self) + self._raumfilter_combo.setToolTip("Wählt die räumliche Bezugsfläche für die Datenextraktion.") self._raumfilter_combo.addItems(RAUMFILTER_OPTIONS) self._raumfilter_combo.currentTextChanged.connect(self._on_raumfilter_changed) main_layout.addWidget(self._raumfilter_combo) @@ -175,13 +186,10 @@ class TabA(QWidget): # === PIPELINE-STEUERUNG === 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) + self.btn_pipeline.clicked.connect(self._on_load_fachdaten) main_layout.addWidget(self.btn_pipeline) - 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) @@ -242,10 +250,19 @@ class TabA(QWidget): set_variable("tab_a_linkliste", path, scope="project") def _on_layer_changed(self, layer) -> None: - """Persistieret Layer-Auswahl.""" + """Persistiert Layer-Auswahl und registriert Verfahrensgebiet.""" self._pufferlayer = layer - if layer: - set_variable("tab_a_layer_id", layer.id(), scope="project") + + if not layer: + return + + # UI-State speichern + set_variable("tab_a_layer_id", layer.id(), scope="project") + + # 🔹 NEU: Verfahrensgebiet explizit registrieren + if self.logic: + self.logic.save_verfahrensgebiet_layer(layer) + def _on_raumfilter_changed(self, value: str) -> None: """Persistieret Raumfilter-Auswahl.""" @@ -267,92 +284,34 @@ class TabA(QWidget): def _update_group_color(self) -> None: """Visuelles Feedback für Verfahrens-DB-Status.""" if self.verfahrens_db: - self.group_button.setStyleSheet("font-weight: bold; background-color: #e0f7e0;") + self.group_button.setStyleSheet("font-weight: bold; background-color: ##d7a8ff;") else: self.group_button.setStyleSheet("font-weight: bold;") - # === PIPELINE === - def _on_run_pipeline(self) -> None: - """DEBUG: Pipeline mit maximaler Ausgabe.""" - print("\n" + "="*60) - print("🚀 _on_run_pipeline GESTARTET") - print("="*60) - # 🔥 DEBUG QT STATUS - from sn_basis.functions import qt_wrapper - qt_wrapper.debug_qt_status() # ← Zeigt EXAKT was läuft! - - - - # 1. Services prüfen - print(f"pruefmanager: {self.pruefmanager is not None}") - print(f"data_grabber: {self.data_grabber is not None}") - print(f"logic: {hasattr(self, 'logic')}") - - if not self.pruefmanager: - print("❌ FEHLER: self.pruefmanager fehlt!") - return - if not self.data_grabber: - print("❌ FEHLER: self.data_grabber fehlt!") - return - - print("✅ Services OK") - - # 2. FileWidget - source = self.file_widget.filePath() - print(f"📁 Eingabe: '{source}' (len={len(source or '')})") - - # 3. Dateipruefer - print("🔍 Dateipruefer starte...") - try: - pruefer = Dateipruefer( - source, - basis_pfad="", - leereingabe_erlaubt=False, - standarddatei=None, - temporaer_erlaubt=True, # ✅ Explizit True - verfahrens_db_modus=True # ✅ Keyword-only - ) - ergebnis1 = pruefer.pruefe() - print(f" → ok={ergebnis1.ok}, aktion='{ergebnis1.aktion}', kontext={ergebnis1.kontext}") - except Exception as e: - print(f"💥 Dateipruefer FEHLER: {e}") - import traceback - traceback.print_exc() - return - - # 4. Pruefmanager - print("🤖 Pruefmanager starte...") - try: - ergebnis2 = self.pruefmanager.verarbeite(ergebnis1) - - print(f" → ok={ergebnis2.ok}, aktion='{ergebnis2.aktion}', kontext={ergebnis2.kontext}") - except Exception as e: - print(f"💥 Pruefmanager FEHLER: {e}") - import traceback - traceback.print_exc() - return - - # 5. Entscheidung - weiter = ergebnis2.ok - print(f"➡️ Weiter? {weiter} (aktion='{ergebnis2.aktion}')") - - if weiter: - final_pfad = ergebnis2.kontext if ergebnis2.kontext else source - print(f"🚀 DataGrabber mit: '{final_pfad}'") - try: - self.data_grabber.run(final_pfad) - print("✅ DataGrabber aufgerufen!") - except Exception as e: - print(f"💥 DataGrabber FEHLER: {e}") - import traceback - traceback.print_exc() - else: - print("⏹️ Pipeline gestoppt (erwartet bei leerem Pfad)") - - print("="*60 + "\n") - + def _on_load_fachdaten(self) -> None: """Kompatibilitäts-Handler → neue Pipeline.""" - self._on_run_pipeline() + source = self.file_widget.filePath() + raumfilter = self._raumfilter_combo.currentText() + linkliste = self.linkliste_widget.filePath() + + if self.logic and self.layer_combo: + layer = self.layer_combo.currentLayer() + else: + layer = None + + if layer and layer.name() == "Verfahrensgebiet": + self.logic.save_verfahrensgebiet_layer(layer) + + if self.logic: + try: + self.logic._on_run_pipeline(source, linkliste, raumfilter) + except Exception as exc: + QMessageBox.warning( + self, + "Fehler beim Laden", + f"Fehler beim Ausführen der Pipeline: {exc}", + QMessageBox.StandardButton.Ok, + )