diff --git a/docs/ablauf_verfahrensgebiet_laden.md b/docs/ablauf_verfahrensgebiet_laden.md deleted file mode 100644 index 0eb0b3b..0000000 --- a/docs/ablauf_verfahrensgebiet_laden.md +++ /dev/null @@ -1,27 +0,0 @@ -# Ablauf: Verfahrensgebiet laden – frühzeitige Stopps - -Zwei Stellen, an denen der jeweilige Ablauf vorzeitig endet: - -- **Aus ALKIS laden**, Option "Nur Objekte laden": bricht direkt nach dem - Laden des BROR-Objekts ab, kein weiterer Workflow. -- **Aus Shape laden**, "Nein" beim (aktuell temporären) Weiter?-Dialog nach - dem ZIP-Import: bricht ab, bevor das BROR-Objekt überhaupt geladen wird. - -Quellen: `functions/verfahrensgebiet_alkis_komplett.py`, -`functions/verfahrensgebiet_shape_komplett.py`. - -```mermaid -flowchart TD - Start(["Verfahrensgebiet"]) - - subgraph ALKIS[" "] - A1["Aus ALKIS laden"] --> AE[("Layer: BauRaumOderBodenordnungsrecht (ALKIS)")] - end - - subgraph SHAPE[" "] - S1["Aus Shape laden"] --> SE[("Layer: Verfahrensgebiet (aus ZIP)")] - end - - Start --> A1 - Start --> S1 -``` diff --git a/docs/aus_alkis_laden.md b/docs/aus_alkis_laden.md new file mode 100644 index 0000000..43bfd35 --- /dev/null +++ b/docs/aus_alkis_laden.md @@ -0,0 +1,69 @@ +# Ablauf: Aus ALKIS laden + +Betrachteter Teilablauf: nur bis zum Erzeugen des Layers +"BauRaumOderBodenordnungsrecht (ALKIS)". Die anschließenden Schritte des +vollständigen Workflows (Gemarkungen, Flurstücke, Flächenvergleich, +Detailprüfung) sind hier bewusst nicht dargestellt. + +```mermaid +flowchart TD + Start(["Aus ALKIS laden"]) + B0{"Verfahrensnummer oder Landkreis in Projekteigenschaften gesetzt?"} + LKAUSW["Landkreis auswählen (Dialog)"] + Abbruch(["Abbruch"]) + MODUS{"Verfahrensnummer gesetzt?"} + EXIST1{"Layer 'Verfahrensgebiet' bereits vorhanden?"} + EXIST2{"Layer 'Verfahrensgebiet' bereits vorhanden?"} + Keep[("Verfahrensgebiet (bestehender Layer)")] + WFS1["WFS-Suche per Verfahrensnummer"] + WFS2["WFS-Suche per Landkreis"] + C1{"Mehrere Objekt-Kombinationen gefunden?"} + AUSW1["Objektauswahl"] + AUSW2["Objektauswahl"] + BUILD["Layer erzeugen"] + Ende[("BauRaumOderBodenordnungsrecht (ALKIS)")] + + Start --> B0 + B0 -- Nein --> LKAUSW + LKAUSW -- Abbruch --> Abbruch + LKAUSW -- OK --> MODUS + B0 -- Ja --> MODUS + + MODUS -- Ja --> EXIST1 + MODUS -- Nein --> EXIST2 + + EXIST1 -- "Ja, behalten" --> Keep + EXIST2 -- "Ja, behalten" --> Keep + + EXIST1 -- "Nein / neu laden" --> WFS1 + EXIST2 -- "Nein / neu laden" --> WFS2 + + WFS1 --> C1 + C1 -- Ja --> AUSW1 + C1 -- Nein --> BUILD + AUSW1 --> BUILD + + WFS2 --> AUSW2 + AUSW2 --> BUILD + + BUILD --> Ende +``` + +Hinweise zu den Entscheidungsknoten: + +- **B0**: Es werden die Projektvariablen `verfahrensnummer` und + `landkreis_kennzahl` gemeinsam geprüft. Nur wenn **beide** fehlen, wird der + Landkreis per Dialog abgefragt. +- **MODUS**: Ist eine Verfahrensnummer gesetzt, wird über die Verfahrensnummer + gesucht (auch wenn zusätzlich ein Landkreis gesetzt ist); andernfalls über + den Landkreis. +- **EXIST1/EXIST2**: Existiert bereits ein Layer "Verfahrensgebiet" und lehnt + der Nutzer das Neuladen ab, wird direkt dieser vorhandene Layer verwendet – + ohne erneute WFS-Suche oder Objektauswahl. +- **C1 (nur im Verfahrensnummer-Zweig)**: Die Objektauswahl erscheint nur, + wenn der WFS mehrere unterschiedliche (adv_name, bezeichnung)-Kombinationen + liefert. Bei genau einem Treffer wird der Layer ohne Nutzerinteraktion + erzeugt. +- **Landkreis-Zweig**: Hier erscheint die Objektauswahl immer (inkl. + "Alle auswählen"-Option), da hier potenziell deutlich mehr Treffer als bei + der Verfahrensnummer-Suche auftreten. diff --git a/docs/aus_shape_laden.md b/docs/aus_shape_laden.md new file mode 100644 index 0000000..65f9f05 --- /dev/null +++ b/docs/aus_shape_laden.md @@ -0,0 +1,20 @@ +# Ablauf: Aus Shape laden + +```mermaid +flowchart TD + Start(["Aus Shape laden"]) + A0{"Datei öffnen?"} + A2["entpacken"] + A3["Inhalt prüfen"] + + Ende1[("Layer:
Neuordnungsgebiet (NoBIS)")] + Ende2[("Layer:
Neuvermessungsgebiet (NoBIS)")] + + Start --> A0 + A0 -- Shape --> A3 + A0 -- ZIP --> A2 + A2 --> A3 + A3 -- Neuordnungsgebiet --> Ende1 + A3 -- Neuvermessungsgebiet --> Ende2 + +``` \ No newline at end of file diff --git a/docs/aus_shape_laden_technisch.md b/docs/aus_shape_laden_technisch.md new file mode 100644 index 0000000..3dcbb23 --- /dev/null +++ b/docs/aus_shape_laden_technisch.md @@ -0,0 +1,127 @@ +# Technischer Ablauf: Aus Shape laden (bis zum Stop) + +Ergänzung zu [aus_shape_laden.md](aus_shape_laden.md), die den fachlichen +Ablauf in Kurzform beschreibt. Dieses Dokument zeigt den tatsächlichen +technischen Ablauf in `lade_vg_aus_zip()` und dem anschließenden Stop in +`verfahrensgebiet_shape_komplett()` (Datei +`functions/verfahrensgebiet_shape_komplett.py`). + +Betrachteter Teilablauf: nur bis zum Stop nach dem Einfügen der Layer ins +Projekt. Die anschließenden Schritte (Gemarkung, Flurstücke, +Flächenvergleich, Detailprüfung) sind hier nicht dargestellt. + +```mermaid +flowchart TD + Start(["Aus Shape laden"]) + DLG["Dateidialog (Mehrfachauswahl):
.zip und/oder .shp"] + ABBRUCH1(["Abbruch (keine Auswahl)"]) + + SAMMEL["Für jede ausgewählte Datei:"] + ISZIP{"Endet auf .shp?"} + DIREKT["Direkt als Shapefile-Eintrag übernehmen"] + ENTPACK["In temporäres Verzeichnis entpacken"] + ENTPACKFEHLER{"Entpacken erfolgreich?"} + FEHLER_ZIP["Fehler notieren:
'ZIP konnte nicht entpackt werden'"] + FINDEALL["Alle .shp-Dateien im
entpackten Verzeichnis suchen"] + KEINSHP{"Mind. eine .shp gefunden?"} + FEHLER_KEIN_SHP["Fehler notieren:
'kein gültiges Shape-Dateiset'"] + ALS_EINTRAG["Jede gefundene .shp als
eigenen Eintrag übernehmen"] + + PROJEDEN["Für jeden Shapefile-Eintrag:"] + LOAD["Als QgsVectorLayer laden"] + VALID{"isValid()?"} + FEHLER_INVALID["Fehler notieren:
'kein gültiges Shape-Dateiset'"] + FBSCHL{"Feld 'FBSchl' vorhanden
und Wert bekannt?"} + FEHLER_FBSCHL["Fehler notieren:
Feld fehlt / unbekannter Wert"] + TYP{"FBSchl-Wert"} + ALS_NEUORDNUNG["Geometrien zu
'Neuordnungsgebiet' sammeln"] + ALS_NEUVERMESSUNG["Geometrien zu
'Neuvermessungsgebiet' sammeln"] + + NACHSCHLEIFE["Nach Verarbeitung aller Einträge"] + FEHLERVORHANDEN{"Fehler gesammelt?"} + SAMMELMELDUNG["Sammelmeldung anzeigen:
alle fehlerhaften Dateien"] + NICHTSVERWERTBAR{"Mind. ein Typ mit
gültigen Geometrien?"} + ABBRUCH2(["Abbruch (None)"]) + + MERGE["Je Gebietstyp:
unaryUnion aller gesammelten Geometrien
→ ein Memory-Layer je Typ"] + BENENNEN["Layer benennen:
'Neuordnungsgebiet (NoBIS)' /
'Neuvermessungsgebiet (NoBIS)'"] + RETURN["Liste der Layer zurückgeben
(1 oder 2 Layer)"] + + EINFUEGEN["Alle Layer ins Projekt einfügen"] + STOP{{"STOP:
'Layer ... wurde(n) geladen. Weiter?'"}} + WEITER(["weiter mit Gemarkung/Flurstücke/…"]) + ABBRUCH3(["Abbruch, Layer bleiben im Projekt"]) + + Start --> DLG + DLG -- keine Auswahl --> ABBRUCH1 + DLG -- Auswahl --> SAMMEL + SAMMEL --> ISZIP + ISZIP -- Ja --> DIREKT + ISZIP -- Nein --> ENTPACK + ENTPACK --> ENTPACKFEHLER + ENTPACKFEHLER -- Nein --> FEHLER_ZIP + ENTPACKFEHLER -- Ja --> FINDEALL + FINDEALL --> KEINSHP + KEINSHP -- Nein --> FEHLER_KEIN_SHP + KEINSHP -- Ja --> ALS_EINTRAG + + DIREKT --> PROJEDEN + ALS_EINTRAG --> PROJEDEN + FEHLER_ZIP -.->|nächste Datei| SAMMEL + FEHLER_KEIN_SHP -.->|nächste Datei| SAMMEL + + PROJEDEN --> LOAD + LOAD --> VALID + VALID -- Nein --> FEHLER_INVALID + VALID -- Ja --> FBSCHL + FBSCHL -- Nein --> FEHLER_FBSCHL + FBSCHL -- Ja --> TYP + TYP -- "901/10110F" --> ALS_NEUORDNUNG + TYP -- "901/10120F" --> ALS_NEUVERMESSUNG + FEHLER_INVALID -.->|nächstes Shapefile| PROJEDEN + FEHLER_FBSCHL -.->|nächstes Shapefile| PROJEDEN + ALS_NEUORDNUNG -.->|nächstes Shapefile| PROJEDEN + ALS_NEUVERMESSUNG -.->|nächstes Shapefile| PROJEDEN + + PROJEDEN --> NACHSCHLEIFE + NACHSCHLEIFE --> FEHLERVORHANDEN + FEHLERVORHANDEN -- Ja --> SAMMELMELDUNG + FEHLERVORHANDEN -- Nein --> NICHTSVERWERTBAR + SAMMELMELDUNG --> NICHTSVERWERTBAR + NICHTSVERWERTBAR -- Nein --> ABBRUCH2 + NICHTSVERWERTBAR -- Ja --> MERGE + MERGE --> BENENNEN + BENENNEN --> RETURN + + RETURN --> EINFUEGEN + EINFUEGEN --> STOP + STOP -- Ja --> WEITER + STOP -- Nein --> ABBRUCH3 +``` + +## Hinweise + +- **Mehrfachauswahl**: Es können mehrere Dateien gleichzeitig gewählt werden, + auch gemischt `.zip` und `.shp`. +- **ZIP mit mehreren Shapefile-Sets**: Enthält ein ZIP mehrere `.shp`-Dateien, + wird jede einzeln geprüft und verarbeitet – wie ein eigener Eintrag in der + Auswahl. +- **Fehler pro Datei, nicht pro Vorgang**: Schlägt die Prüfung einer + einzelnen Datei/eines einzelnen Shapefiles fehl (ZIP nicht entpackbar, + kein Shapefile enthalten, ungültiger Layer, Feld `FBSchl` fehlt oder hat + einen unbekannten Wert), wird nur dieser Eintrag übersprungen. Alle + anderen werden weiterverarbeitet. Am Ende erscheint eine **gesammelte** + Fehlermeldung mit allen fehlerhaften Einträgen – Abbruch (`None`) nur, + wenn *kein* Eintrag verwertbare Geometrien geliefert hat. +- **Zusammenführen gleicher Gebietstypen**: Alle Geometrien mit demselben + `FBSchl`-Typ (über alle Dateien/Shapefiles hinweg) werden per + `QgsGeometry.unaryUnion` zu **einem** Memory-Layer verschmolzen – nicht + ein Layer pro Datei. +- **Temporäre Verzeichnisse**: Jedes entpackte ZIP landet in einem eigenen + `tempfile.mkdtemp()`-Verzeichnis, das nach der Verarbeitung (auch im + Fehlerfall) wieder gelöscht wird. +- **Stop-Punkt**: Nach dem Einfügen aller erzeugten Layer ins Projekt fragt + ein Ja/Nein-Dialog, ob mit dem restlichen Workflow (Gemarkung, + Flurstücke, Flächenvergleich, Detailprüfung) fortgefahren werden soll. + Bei "Nein" bleiben die geladenen Layer im Projekt erhalten, der weitere + Ablauf wird aber nicht gestartet. diff --git a/functions/verfahrensgebiet_shape_komplett.py b/functions/verfahrensgebiet_shape_komplett.py index 9a320f0..1a88860 100644 --- a/functions/verfahrensgebiet_shape_komplett.py +++ b/functions/verfahrensgebiet_shape_komplett.py @@ -3,8 +3,10 @@ Orchestriert den vollständigen Arbeitsablauf zum Laden eines Verfahrensgebiets aus einem ZIP-Archiv mit Shapefile-Set: -1. Dateidialog öffnen (.zip-Filter); Nutzerabbruch beendet den Vorgang. -2. ZIP neben der ZIP-Datei entpacken und auf ein gültiges Shapefile-Set prüfen. +1. Dateidialog öffnen (.zip/.shp-Filter, Mehrfachauswahl); Nutzerabbruch + beendet den Vorgang. +2. ZIP in ein temporäres Verzeichnis entpacken (danach wieder gelöscht) und + auf ein gültiges Shapefile-Set prüfen. 3. Shapefile als QGIS-Vektorlayer laden und validieren (Standard-QGIS-Routinen). 4. Alle Features per ``QgsGeometry.unaryUnion`` zu einem Single-Feature- Memory-Layer „Verfahrensgebiet" verschmelzen. @@ -40,6 +42,8 @@ oder kein Ergebnis liefert, bricht den Workflow ab. from __future__ import annotations import os +import shutil +import tempfile import zipfile from typing import Optional, Tuple @@ -80,11 +84,74 @@ from sn_verfahrensgebiet.functions.verfahrensgebiet_alkis_komplett import ( BROR_LAYER_NAME, ) +#: Feldname im Shapefile, über den der Gebietstyp bestimmt wird. +FBSCHL_FELD = "FBSchl" +#: FBSchl-Wert für ein Neuordnungsgebiet. +FBSCHL_NEUORDNUNGSGEBIET = "901/10110F" +#: FBSchl-Wert für ein Neuvermessungsgebiet. +FBSCHL_NEUVERMESSUNGSGEBIET = "901/10120F" + +#: Anzeigename des Layers für ein Neuordnungsgebiet (NoBIS). +NEUORDNUNGSGEBIET_LAYER_NAME = "Neuordnungsgebiet (NoBIS)" +#: Anzeigename des Layers für ein Neuvermessungsgebiet (NoBIS). +NEUVERMESSUNGSGEBIET_LAYER_NAME = "Neuvermessungsgebiet (NoBIS)" +#: Zuordnung Gebietstyp → Layername. +_GEBIETSTYP_LAYER_NAMEN = { + "Neuordnungsgebiet": NEUORDNUNGSGEBIET_LAYER_NAME, + "Neuvermessungsgebiet": NEUVERMESSUNGSGEBIET_LAYER_NAME, +} + # --------------------------------------------------------------------------- # Interne Hilfsfunktionen # --------------------------------------------------------------------------- +def _ermittle_gebietstyp_aus_fbschl(raw_layer) -> Tuple[Optional[str], Optional[str]]: + """Prüft das Feld ``FBSchl`` und ermittelt den Gebietstyp. + + Zeigt selbst keine Fehlermeldung an, sondern gibt einen Fehlertext + zurück, damit der Aufrufer Fehler mehrerer Dateien sammeln und am Ende + gemeinsam anzeigen kann. + + Parameters + ---------- + raw_layer : + Dateibasierter Shapefile-Layer (vor dem Verschmelzen zum Memory-Layer). + + Returns + ------- + Tuple[Optional[str], Optional[str]] + ``(gebietstyp, fehlertext)``. Bei Erfolg ist ``gebietstyp`` + ``"Neuordnungsgebiet"`` oder ``"Neuvermessungsgebiet"`` und + ``fehlertext`` ``None``. Bei Fehler ist ``gebietstyp`` ``None`` und + ``fehlertext`` beschreibt die Ursache (fehlendes Feld oder + unbekannter Wert). + """ + idx = raw_layer.fields().indexFromName(FBSCHL_FELD) + if idx < 0: + return None, f"Feld '{FBSCHL_FELD}' fehlt (Shape entspricht nicht dem erwarteten Inhalt)." + + wert = None + for feat in raw_layer.getFeatures(): + try: + roh = feat.attribute(idx) + except Exception: + roh = None + if roh not in (None, ""): + wert = str(roh).strip() + break + + if wert == FBSCHL_NEUORDNUNGSGEBIET: + return "Neuordnungsgebiet", None + if wert == FBSCHL_NEUVERMESSUNGSGEBIET: + return "Neuvermessungsgebiet", None + + return None, ( + f"Unbekannter Wert im Feld '{FBSCHL_FELD}': {wert!r} " + f"(erwartet: '{FBSCHL_NEUORDNUNGSGEBIET}' oder " + f"'{FBSCHL_NEUVERMESSUNGSGEBIET}')." + ) + def _wende_vergleich_stil_an(layer) -> None: """Wendet einen kategorisierten Renderer auf den Vergleich-Layer an. @@ -216,6 +283,31 @@ def _finde_shp(verzeichnis: str) -> Optional[str]: return None +def _finde_alle_shp(verzeichnis: str) -> list: + """Sucht rekursiv nach allen ``.shp``-Dateien im Verzeichnis. + + Ein ZIP-Archiv kann mehrere Shapefile-Sets enthalten (z.B. je eines für + Neuordnungs- und Neuvermessungsgebiet) – im Unterschied zu + :func:`_finde_shp` werden hier alle gefunden, nicht nur die erste. + + Parameters + ---------- + verzeichnis : + Startverzeichnis für die Suche. + + Returns + ------- + list + Sortierte Liste der absoluten Pfade aller gefundenen ``.shp``-Dateien. + """ + treffer = [] + for root, _dirs, files in os.walk(verzeichnis): + for fname in files: + if fname.lower().endswith(".shp"): + treffer.append(os.path.join(root, fname)) + return sorted(treffer) + + def _erstelle_memory_layer_aus_shp(shp_layer) -> Optional[object]: """Vereint alle Features des Shapefile-Layers zu einem Single-Feature- Memory-Layer „Verfahrensgebiet" via ``QgsGeometry.unaryUnion``. @@ -291,17 +383,38 @@ def _erstelle_memory_layer_aus_shp(shp_layer) -> Optional[object]: # Öffentliche Lade-Hilfsfunktion # --------------------------------------------------------------------------- -def lade_vg_aus_zip(parent_widget) -> Optional[object]: - """Öffnet Dateidialog, entpackt ZIP und gibt Union-Memory-Layer zurück. +def lade_vg_aus_zip(parent_widget) -> Optional[list]: + """Öffnet Dateidialog (Mehrfachauswahl), entpackt ZIPs (bzw. nutzt direkt + gewählte ``.shp``-Dateien) und liefert je Gebietstyp einen Union-Memory-Layer. Schrittfolge: - 1. ``QFileDialog.getOpenFileName`` mit Filter ``ZIP-Dateien (*.zip)``. - 2. ZIP neben der Quelldatei in ein Unterverzeichnis entpacken. - 3. Erste ``.shp``-Datei im Verzeichnis finden. - 4. Als ``QgsVectorLayer`` laden und ``isValid()`` prüfen. - 5. Alle Features per ``QgsGeometry.unaryUnion`` verschmelzen. - 6. Single-Feature-Memory-Layer „Verfahrensgebiet" zurückgeben. + 1. ``QFileDialog.getOpenFileNames`` mit Filter für ``.zip`` und ``.shp`` + (Mehrfachauswahl möglich). + 2. Für jede ausgewählte Datei: bei ``.zip`` in ein temporäres Verzeichnis + entpacken (danach wieder gelöscht) und darin **alle** enthaltenen + ``.shp``-Dateien finden (ein ZIP kann mehrere Shapefile-Sets + bündeln, z.B. je eines für Neuordnungs- und Neuvermessungsgebiet); + bei ``.shp`` die Datei direkt verwenden. + 3. Jede gefundene Shapefile-Datei einzeln als ``QgsVectorLayer`` laden + und ``isValid()`` prüfen. + 4. Je Shapefile-Datei das Feld ``FBSchl`` prüfen und den Gebietstyp + ermitteln (Neuordnungs- bzw. Neuvermessungsgebiet). + 5. Die Features aller Dateien **desselben Gebietstyps** werden gemeinsam + per ``QgsGeometry.unaryUnion`` zu einem Single-Feature-Memory-Layer + verschmolzen – d.h. mehrere Neuordnungsgebiet-Dateien ergeben einen + Layer, mehrere Neuvermessungsgebiet-Dateien einen zweiten. + + Fehlerverhalten: Schlägt die Prüfung eines einzelnen Shapefiles fehl + (ZIP nicht entpackbar, kein Shapefile enthalten, ungültiger Layer, Feld + ``FBSchl`` fehlt oder hat einen unbekannten Wert), wird **nur dieses + Shapefile** übersprungen und sein Fehler gesammelt – das gilt auch, + wenn mehrere Shapefiles in derselben ZIP-Datei liegen: eine fehlerhafte + Datei darin bricht nicht die Verarbeitung der übrigen ab. Alle + verwertbaren Dateien werden gemäß Vorgabe eingelesen. Am Ende erscheint + eine gesammelte Fehlermeldung mit allen fehlerhaften Dateien. Erst wenn + **keine** Datei verwertbare Geometrien liefert, wird ``None`` + zurückgegeben. Parameters ---------- @@ -310,62 +423,173 @@ def lade_vg_aus_zip(parent_widget) -> Optional[object]: Returns ------- - Optional[QgsVectorLayer] - Valider Memory-Layer „Verfahrensgebiet" oder ``None`` bei Abbruch/Fehler. + Optional[list] + Liste der erzeugten Memory-Layer (ein Layer je in der Auswahl + vorkommendem, erfolgreich geprüftem Gebietstyp, max. 2), oder + ``None`` wenn keine Datei verwertet werden konnte oder der Dialog + abgebrochen wurde. """ - pfad, _ = QFileDialog.getOpenFileName( + pfade, _ = QFileDialog.getOpenFileNames( parent_widget, - "ZIP-Datei mit Shapefile-Set auswählen", + "Shapefile-Set(s) (ZIP oder .shp) auswählen", "", - "ZIP-Dateien (*.zip)", + "Shapefile-Set (*.zip *.shp);;ZIP-Dateien (*.zip);;Shapefile (*.shp)", ) - if not pfad: + if not pfade: return None # Nutzerabbruch - zip_dir = os.path.dirname(pfad) - zip_base = os.path.splitext(os.path.basename(pfad))[0] - extract_dir = os.path.join(zip_dir, zip_base) + # Je Gebietstyp gesammelte Geometrien/Felder/Attribute über alle + # ausgewählten Dateien hinweg (Reihenfolge der ersten Sichtung bleibt + # über dict-Einfügereihenfolge erhalten). + geometrien_je_typ: dict = {} + felder_je_typ: dict = {} + attribute_je_typ: dict = {} + crs_je_typ: dict = {} + temp_dirs: list = [] + fehler_liste: list = [] try: - with zipfile.ZipFile(pfad, "r") as zf: - zf.extractall(extract_dir) - except Exception: - QMessageBox.warning( - parent_widget, - "Fehler", - "Die ZIP-Datei konnte nicht entpackt werden.\n" - "Bitte eine gültige ZIP-Datei auswählen.", - ) - return None + # (shp_path, anzeige_name) für jede tatsächlich zu prüfende + # Shapefile-Datei sammeln – ein ZIP kann mehrere Shapefile-Sets + # enthalten, jedes wird einzeln behandelt. + shp_eintraege: list = [] - shp_path = _finde_shp(extract_dir) - if not shp_path: - QMessageBox.warning( - parent_widget, - "Fehler", - "gültiges Shape-Dateiset angeben", - ) - return None + for pfad in pfade: + dateiname = os.path.basename(pfad) - raw_layer = QgsVectorLayer(shp_path, "tmp_shape_vg", "ogr") - if not raw_layer or not raw_layer.isValid(): - QMessageBox.warning( - parent_widget, - "Fehler", - "gültiges Shape-Dateiset angeben", - ) - return None + if pfad.lower().endswith(".shp"): + shp_eintraege.append((pfad, dateiname)) + continue - mem_layer = _erstelle_memory_layer_aus_shp(raw_layer) - if mem_layer is None: - QMessageBox.warning( - parent_widget, - "Fehler", - "gültiges Shape-Dateiset angeben", - ) - return None + extract_dir = tempfile.mkdtemp(prefix="sn_vg_shape_") + temp_dirs.append(extract_dir) - return mem_layer + try: + with zipfile.ZipFile(pfad, "r") as zf: + zf.extractall(extract_dir) + except Exception: + fehler_liste.append( + f"{dateiname}: ZIP-Datei konnte nicht entpackt werden." + ) + continue + + shp_pfade_im_zip = _finde_alle_shp(extract_dir) + if not shp_pfade_im_zip: + fehler_liste.append( + f"{dateiname}: kein gültiges Shape-Dateiset enthalten." + ) + continue + + for shp_path in shp_pfade_im_zip: + anzeige_name = ( + f"{dateiname} ({os.path.basename(shp_path)})" + if len(shp_pfade_im_zip) > 1 + else dateiname + ) + shp_eintraege.append((shp_path, anzeige_name)) + + # ------------------------------------------------------------------ + # Jede gefundene Shapefile-Datei einzeln prüfen und einlesen. + # ------------------------------------------------------------------ + for shp_path, anzeige_name in shp_eintraege: + raw_layer = QgsVectorLayer(shp_path, "tmp_shape_vg", "ogr") + if not raw_layer or not raw_layer.isValid(): + fehler_liste.append( + f"{anzeige_name}: kein gültiges Shape-Dateiset." + ) + continue + + gebietstyp, fehlertext = _ermittle_gebietstyp_aus_fbschl(raw_layer) + if gebietstyp is None: + fehler_liste.append(f"{anzeige_name}: {fehlertext}") + del raw_layer + continue + + geom_list = [] + first_feat = None + for feat in raw_layer.getFeatures(): + try: + geom = feat.geometry() + except Exception: + geom = None + if geom and not geom.isEmpty(): + geom_list.append(geom) + if first_feat is None: + first_feat = feat + + if geom_list: + geometrien_je_typ.setdefault(gebietstyp, []).extend(geom_list) + if gebietstyp not in felder_je_typ: + felder_je_typ[gebietstyp] = list(raw_layer.fields()) + attribute_je_typ[gebietstyp] = ( + first_feat.attributes() if first_feat is not None else [] + ) + crs_je_typ[gebietstyp] = raw_layer.crs().authid() + else: + fehler_liste.append(f"{anzeige_name}: keine gültigen Geometrien enthalten.") + + # Referenz auf den dateibasierten Layer freigeben, bevor das + # temporäre Verzeichnis gelöscht wird (Windows hält sonst ggf. + # noch einen Dateihandle auf das Shapefile). + del raw_layer + + if fehler_liste: + meldung = ( + "Folgende Dateien konnten nicht eingelesen werden:\n\n" + + "\n".join(fehler_liste) + ) + if geometrien_je_typ: + meldung += "\n\nDie übrigen Dateien wurden gemäß Vorgabe eingelesen." + QMessageBox.warning(parent_widget, "Fehler in ausgewählten Dateien", meldung) + + if not geometrien_je_typ: + return None + + ergebnis_layer = [] + for typ, geometrien in geometrien_je_typ.items(): + union_geom = QgsGeometry.unaryUnion(geometrien) + if not union_geom or union_geom.isEmpty(): + continue + + mem_layer = QgsVectorLayer( + f"MultiPolygon?crs={crs_je_typ[typ]}", + _GEBIETSTYP_LAYER_NAMEN[typ], + "memory", + ) + pr = mem_layer.dataProvider() + try: + pr.addAttributes(felder_je_typ[typ]) + mem_layer.updateFields() + except Exception: + pass + + feat_out = QgsFeature(mem_layer.fields()) + feat_out.setGeometry(union_geom) + try: + feat_out.setAttributes(attribute_je_typ[typ]) + except Exception: + pass + + pr.addFeatures([feat_out]) + mem_layer.updateExtents() + + if mem_layer.isValid() and mem_layer.featureCount() > 0: + mem_layer.setCustomProperty("fb_gebietstyp", typ) + ergebnis_layer.append(mem_layer) + + if not ergebnis_layer: + QMessageBox.warning( + parent_widget, + "Fehler", + "gültiges Shape-Dateiset angeben", + ) + return None + + return ergebnis_layer + + finally: + for d in temp_dirs: + shutil.rmtree(d, ignore_errors=True) # --------------------------------------------------------------------------- @@ -420,38 +644,45 @@ def verfahrensgebiet_shape_komplett( """ project = QgsProject.instance() shape_vg_layer = None + zusatz_shape_layer: list = [] bror_layer = None shape_was_added_by_us = False bror_was_added_by_us = False abgeschlossen = False - progress = create_progress_dialog( - 7, "Verfahrensgebiet aus Shape laden", "Initialisierung …" - ) + progress = None try: # ------------------------------------------------------------------ # Schritt 1: Shape-VG aus ZIP laden (noch NICHT ins Projekt einfügen) + # Läuft VOR dem Fortschrittsdialog, damit dieser nicht schon während + # des Dateidialogs sichtbar ist. # ------------------------------------------------------------------ - progress.set_label("Schritt 1/6: Verfahrensgebiet aus ZIP laden …") - progress.set_value(0) - if progress.is_canceled(): + shape_vg_layers = lade_vg_aus_zip(tab_widget) + + if not shape_vg_layers: return None, None, None, LoadStatus.NONE - shape_vg_layer = lade_vg_aus_zip(tab_widget) - progress.raise_to_front() - - if not shape_vg_layer: - return None, None, None, LoadStatus.NONE + # Je Gebietstyp ein Layer (max. 2). Der erste ist das für den + # weiteren Ablauf maßgebliche Shape-VG; etwaige weitere Layer + # (zweiter Gebietstyp) werden vorerst nur mit ins Projekt geladen – + # ihre Einbindung in den weiteren Workflow folgt in einem späteren + # Schritt. + shape_vg_layer = shape_vg_layers[0] + zusatz_shape_layer = shape_vg_layers[1:] # TEMPORÄRER STOPP (wieder entfernen): Pause nach "Verfahrensgebiet # aus ZIP laden" zur Zwischenkontrolle – Layer dafür schon jetzt # sichtbar ins Projekt einfügen (normalerweise erst nach Schritt 2). project.addMapLayer(shape_vg_layer) shape_was_added_by_us = True + for lyr in zusatz_shape_layer: + project.addMapLayer(lyr) + + geladene_namen = ", ".join(lyr.name() for lyr in shape_vg_layers) if not ask_yes_no( "Verfahrensgebiet aus ZIP laden", - "Verfahrensgebiet aus ZIP wurde geladen. Weiter?", + f"Layer '{geladene_namen}' wurde(n) geladen. Weiter?", default=True, parent=tab_widget, ): @@ -461,13 +692,18 @@ def verfahrensgebiet_shape_komplett( return None, None, None, LoadStatus.NONE vg_status = LoadStatus.FIRST + + # Fortschrittsdialog erst jetzt erzeugen (nach dem Stop) – er darf + # nicht schon während Dateidialog/Stop-Abfrage sichtbar sein. + progress = create_progress_dialog( + 7, "Verfahrensgebiet aus Shape laden", "Schritt 2/6: BROR aus ALKIS laden …" + ) progress.set_value(1) # ------------------------------------------------------------------ # Schritt 2: BROR aus ALKIS laden (vor shape_vg, um Namenskonflikt # in _check_existing_layer zu vermeiden) # ------------------------------------------------------------------ - progress.set_label("Schritt 2/6: BROR aus ALKIS laden …") if progress.is_canceled(): return None, None, None, LoadStatus.NONE @@ -491,6 +727,20 @@ def verfahrensgebiet_shape_komplett( project.addMapLayer(shape_vg_layer) shape_was_added_by_us = True + # ------------------------------------------------------------------ + # Zwischenabfrage: Nutzer kann vor dem Gemarkungsschlüssel-Schritt + # abbrechen (Shape-VG und ggf. BROR bleiben im Projekt erhalten). + # ------------------------------------------------------------------ + progress.raise_to_front() + if not ask_yes_no( + "Verfahrensgebiet aus Shape laden", + "Weiter mit Gemarkungsschlüssel ermitteln?", + default=True, + parent=tab_widget, + ): + abgeschlossen = True + return shape_vg_layer, None, bror_layer, vg_status + # ------------------------------------------------------------------ # Schritt 3: Gemarkungsschlüssel per Geometrie-Spatial-Join ermitteln # ------------------------------------------------------------------ @@ -710,6 +960,11 @@ def verfahrensgebiet_shape_komplett( _entferne_hilfs_layer(project, "TempVerfahrensgebiet") # Shape-VG und BROR bei Fehler/Abbruch entfernen if not abgeschlossen: + for lyr in zusatz_shape_layer: + try: + project.removeMapLayer(lyr.id()) + except Exception: + pass if shape_was_added_by_us and shape_vg_layer is not None: try: project.removeMapLayer(shape_vg_layer.id()) diff --git a/ui/tabs/working_tab.py b/ui/tabs/working_tab.py index 348bce8..3f19417 100644 --- a/ui/tabs/working_tab.py +++ b/ui/tabs/working_tab.py @@ -251,15 +251,15 @@ class WorkingTab(QWidget): Styles angewendet und die UI-Elemente aktualisiert. """ # Vorprüfung: Verfahrensnummer muss gesetzt sein - vkz = get_variable("verfahrensnummer") or "" - if not str(vkz).strip(): - if not ask_yes_no( - "Verfahrensnummer nicht gesetzt", - "Verfahrensnummer nicht gesetzt. Trotzdem fortfahren?", - default=False, - parent=self, - ): - return + # vkz = get_variable("verfahrensnummer") or "" + # if not str(vkz).strip(): + # if not ask_yes_no( + # "Verfahrensnummer nicht gesetzt", + # "Verfahrensnummer nicht gesetzt. Trotzdem fortfahren?", + # default=False, + # parent=self, + # ): + # return self._set_busy(self.btn_verf_shape, True, "Aus Shape laden") try: