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: