Shape-Ladevorgang: Mehrfachauswahl, ZIP-Temp-Entpacken, FBSchl-Prüfung
- Dateidialog erlaubt Mehrfachauswahl von .zip/.shp; ZIPs werden in ein
temporäres Verzeichnis entpackt statt dauerhaft neben der ZIP-Datei
(Cleanup nach dem Einlesen)
- Je Shapefile wird das Feld FBSchl geprüft und in Neuordnungs- bzw.
Neuvermessungsgebiet unterschieden; Geometrien gleichen Typs werden über
alle Dateien hinweg zu je einem Layer verschmolzen und entsprechend
benannt ("Neuordnungsgebiet (NoBIS)" / "Neuvermessungsgebiet (NoBIS)")
- Fehler einzelner Dateien/Shapefiles brechen den Vorgang nicht mehr ab,
sondern werden gesammelt und am Ende in einer Meldung ausgegeben
- Fortschrittsdialog erscheint erst nach dem Stop nach dem Laden, nicht
mehr schon vor dem Dateidialog
- Vorprüfung auf gesetzte Verfahrensnummer vorerst auskommentiert
- Ablauf-Dokumentation aufgeteilt in aus_alkis_laden.md und
aus_shape_laden.md (+ technische Ergänzung aus_shape_laden_technisch.md)
Löst #36, #37, #38
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
8447a2615c
commit
b4f2222b50
@@ -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
|
||||
```
|
||||
@@ -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.
|
||||
@@ -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:</br>Neuordnungsgebiet (NoBIS)")]
|
||||
Ende2[("Layer:</br>Neuvermessungsgebiet (NoBIS)")]
|
||||
|
||||
Start --> A0
|
||||
A0 -- Shape --> A3
|
||||
A0 -- ZIP --> A2
|
||||
A2 --> A3
|
||||
A3 -- Neuordnungsgebiet --> Ende1
|
||||
A3 -- Neuvermessungsgebiet --> Ende2
|
||||
|
||||
```
|
||||
@@ -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):<br/>.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:<br/>'ZIP konnte nicht entpackt werden'"]
|
||||
FINDEALL["Alle .shp-Dateien im<br/>entpackten Verzeichnis suchen"]
|
||||
KEINSHP{"Mind. eine .shp gefunden?"}
|
||||
FEHLER_KEIN_SHP["Fehler notieren:<br/>'kein gültiges Shape-Dateiset'"]
|
||||
ALS_EINTRAG["Jede gefundene .shp als<br/>eigenen Eintrag übernehmen"]
|
||||
|
||||
PROJEDEN["Für jeden Shapefile-Eintrag:"]
|
||||
LOAD["Als QgsVectorLayer laden"]
|
||||
VALID{"isValid()?"}
|
||||
FEHLER_INVALID["Fehler notieren:<br/>'kein gültiges Shape-Dateiset'"]
|
||||
FBSCHL{"Feld 'FBSchl' vorhanden<br/>und Wert bekannt?"}
|
||||
FEHLER_FBSCHL["Fehler notieren:<br/>Feld fehlt / unbekannter Wert"]
|
||||
TYP{"FBSchl-Wert"}
|
||||
ALS_NEUORDNUNG["Geometrien zu<br/>'Neuordnungsgebiet' sammeln"]
|
||||
ALS_NEUVERMESSUNG["Geometrien zu<br/>'Neuvermessungsgebiet' sammeln"]
|
||||
|
||||
NACHSCHLEIFE["Nach Verarbeitung aller Einträge"]
|
||||
FEHLERVORHANDEN{"Fehler gesammelt?"}
|
||||
SAMMELMELDUNG["Sammelmeldung anzeigen:<br/>alle fehlerhaften Dateien"]
|
||||
NICHTSVERWERTBAR{"Mind. ein Typ mit<br/>gültigen Geometrien?"}
|
||||
ABBRUCH2(["Abbruch (None)"])
|
||||
|
||||
MERGE["Je Gebietstyp:<br/>unaryUnion aller gesammelten Geometrien<br/>→ ein Memory-Layer je Typ"]
|
||||
BENENNEN["Layer benennen:<br/>'Neuordnungsgebiet (NoBIS)' /<br/>'Neuvermessungsgebiet (NoBIS)'"]
|
||||
RETURN["Liste der Layer zurückgeben<br/>(1 oder 2 Layer)"]
|
||||
|
||||
EINFUEGEN["Alle Layer ins Projekt einfügen"]
|
||||
STOP{{"STOP:<br/>'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.
|
||||
@@ -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())
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user