Compare commits

..
Author SHA1 Message Date
22ottomi 0a906d3ac7 Merge pull request 'FBSchl-Feldsuche tolerant gegenueber abweichenden Feldnamen' (#45) from fix-fbschl-feldname into unstable
Release Plugin / release (push) Successful in 3s
Reviewed-on: #45
2026-09-02 11:44:47 +02:00
22ottomi 39f8209b9d Merge pull request 'Nur-laden-Option bei ALKIS-Laden durch Stop-Abfrage ersetzt' (#43) from alkis-nur-laden-stop into unstable
Reviewed-on: #43
2026-09-02 11:44:36 +02:00
Michael OttoandClaude Sonnet 5 3c0e6e5a19 FBSchl-Feldsuche tolerant gegenüber abweichenden Feldnamen
Manche Shape-Exporte liefern das Feld "FBSchl" mit angehängtem Punkt
(z.B. "FBSchl.") oder abweichender Groß-/Kleinschreibung. Der bisherige
exakte Namensvergleich (fields().indexFromName) erkannte das Feld dann
nicht und meldete fälschlich "Feld fehlt", die Datei wurde nicht
eingelesen.

- _normalisiere_feldname(): entfernt alle Nicht-alphanumerischen Zeichen
  und normalisiert auf Kleinbuchstaben
- _finde_fbschl_index(): exakter Namensabgleich zuerst, sonst Fallback auf
  normalisierten Vergleich über alle Felder

Fixes #44

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-02 11:43:34 +02:00
Michael OttoandClaude Sonnet 5 c93776d19e Nur-laden-Option bei ALKIS-Laden durch Stop-Abfrage ersetzt
- Checkbox "Nur Objekte laden (ohne weitere Prüfungen)" im
  Landkreis-Auswahldialog entfernt
- Stattdessen nach dem Laden/Übernehmen des BROR-Objekts eine einheitliche
  Ja/Nein-Stop-Abfrage ("Weiter?") vor Dialog 2 (Gemarkung/Flurstücke/
  Flächenvergleich) - analog zum Ablauf bei "Aus Shape laden"
- Gilt jetzt für Verfahrensnummer- und Landkreis-Zweig gleichermaßen,
  vorher gab es die Option nur im Landkreis-Zweig

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-02 09:21:58 +02:00
22ottomi 70c14783c5 Merge pull request 'Shape-Ladevorgang: Mehrfachauswahl, ZIP-Temp-Entpacken, FBSchl-Pruefung' (#42) from aus-shape-laden into unstable
Release Plugin / release (push) Successful in 4s
Reviewed-on: #42
2026-09-02 08:40:02 +02:00
Michael OttoandClaude Sonnet 5 b4f2222b50 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>
2026-09-02 08:36:13 +02:00
Michael Otto 8447a2615c Temporärer Weiter?-Stopp nach 'Verfahrensgebiet aus ZIP laden' + Ablauf-Doku
- verfahrensgebiet_shape_komplett.py: TEMPORÄRER STOPP (wieder entfernen)
  zur Zwischenkontrolle nach dem ZIP-Import. Layer wird dafür schon an
  dieser Stelle sichtbar ins Projekt eingefügt statt erst nach Schritt 2;
  bei "Nein" bleibt der Layer zur Inspektion erhalten (abgeschlossen=True),
  der spätere reguläre addMapLayer-Aufruf prüft auf Duplikat.
- docs/ablauf_verfahrensgebiet_laden.md: Mermaid-Diagramm der Lade-Abläufe
  (ALKIS/Shape) bis zu den jeweiligen frühen Stopps.

Lokaler Debug-Branch, bewusst nicht für unstable vorgesehen.
2026-08-31 09:20:50 +02:00
8 changed files with 664 additions and 110 deletions
+69
View File
@@ -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.
+20
View File
@@ -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
```
+127
View File
@@ -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.
+11 -12
View File
@@ -261,12 +261,12 @@ def _baue_layer_aus_landkreis_auswahl(tab_widget, kombinationen: dict, crs: str,
"""Auswahl + Bau für den Landkreis-Fallback. """Auswahl + Bau für den Landkreis-Fallback.
Zeigt :func:`~sn_verfahrensgebiet.functions.verfahrensgebiet_dialoge.ask_landkreis_objekte` Zeigt :func:`~sn_verfahrensgebiet.functions.verfahrensgebiet_dialoge.ask_landkreis_objekte`
(Mehrfachauswahl inkl. "Alle auswählen" und "Nur laden"-Checkbox). Die (Mehrfachauswahl inkl. "Alle auswählen"). Die ausgewählten Kombinationen
ausgewählten Kombinationen werden anhand der ersten 5 Zeichen von werden anhand der ersten 5 Zeichen von ``bezeichnung`` (= die eigentliche
``bezeichnung`` (= die eigentliche VKZ) gruppiert; je Gruppe wird EIN VKZ) gruppiert; je Gruppe wird EIN Feature erzeugt (Geometrien der
Feature erzeugt (Geometrien der Gruppe verschmolzen). Damit entsteht bei Gruppe verschmolzen). Damit entsteht bei Auswahl mehrerer/aller Objekte
Auswahl mehrerer/aller Objekte eines Landkreises ein Layer mit mehreren eines Landkreises ein Layer mit mehreren Features – einem je
Features – einem je tatsächlichem Verfahren, nicht ein Gesamt-Blob. tatsächlichem Verfahren, nicht ein Gesamt-Blob.
Args: Args:
tab_widget: Parent-Widget für Dialoge und Meldungen tab_widget: Parent-Widget für Dialoge und Meldungen
@@ -275,13 +275,12 @@ def _baue_layer_aus_landkreis_auswahl(tab_widget, kombinationen: dict, crs: str,
status: Lade-Status aus Phase 1 (FIRST/RELOAD), wird durchgereicht status: Lade-Status aus Phase 1 (FIRST/RELOAD), wird durchgereicht
Returns: Returns:
Tuple[Optional[QgsVectorLayer], LoadStatus, bool]: Tuple[Optional[QgsVectorLayer], LoadStatus]: ``(layer, status)``.
``(layer, status, nur_laden)``.
""" """
key_list = sorted(kombinationen.keys()) key_list = sorted(kombinationen.keys())
selected_keys, nur_laden = ask_landkreis_objekte(tab_widget, key_list) selected_keys = ask_landkreis_objekte(tab_widget, key_list)
if not selected_keys: if not selected_keys:
return None, LoadStatus.NONE, False return None, LoadStatus.NONE
# Nach VKZ (erste 5 Zeichen der bezeichnung) gruppieren # Nach VKZ (erste 5 Zeichen der bezeichnung) gruppieren
vkz_gruppen: dict[str, list] = {} vkz_gruppen: dict[str, list] = {}
@@ -322,12 +321,12 @@ def _baue_layer_aus_landkreis_auswahl(tab_widget, kombinationen: dict, crs: str,
if not neue_features: if not neue_features:
QMessageBox.critical(tab_widget, "Fehler", "Keine Geometrien zum Verschmelzen gefunden.") QMessageBox.critical(tab_widget, "Fehler", "Keine Geometrien zum Verschmelzen gefunden.")
return None, LoadStatus.NONE, False return None, LoadStatus.NONE
pr.addFeatures(neue_features) pr.addFeatures(neue_features)
memory_layer.updateExtents() memory_layer.updateExtents()
return memory_layer, status, nur_laden return memory_layer, status
def verfahrensgebiet_alkis(tab_widget, skip_existing_check: bool = False): def verfahrensgebiet_alkis(tab_widget, skip_existing_check: bool = False):
+21 -9
View File
@@ -51,6 +51,7 @@ from sn_basis.functions.dialog_wrapper import (
create_progress_dialog, create_progress_dialog,
show_info_dialog, show_info_dialog,
ask_detailpruefung_oder_vg_laden, ask_detailpruefung_oder_vg_laden,
ask_yes_no,
) )
from sn_basis.functions.landkreise import LANDKREISE, ALLE_LANDKREISE from sn_basis.functions.landkreise import LANDKREISE, ALLE_LANDKREISE
from sn_basis.functions.ly_style_wrapper import apply_style from sn_basis.functions.ly_style_wrapper import apply_style
@@ -273,9 +274,13 @@ def verfahrensgebiet_alkis_komplett(
durchsuchen) und wird danach immer geschlossen – auch wenn anschließend durchsuchen) und wird danach immer geschlossen – auch wenn anschließend
der BROR-Auswahldialog erscheint (kein Fortschrittsdialog läuft der BROR-Auswahldialog erscheint (kein Fortschrittsdialog läuft
währenddessen parallel zum Auswahldialog). währenddessen parallel zum Auswahldialog).
- **STOP** (Zwischenabfrage "Weiter?"): erscheint nach dem Laden/Übernehmen
des BROR-Objekts, bevor Dialog 2 startet. Bei "Nein" bricht der Ablauf
ab, das BROR-Objekt bleibt aber im Projekt (ersetzt die frühere
"Nur Objekte laden"-Option des Landkreis-Auswahldialogs).
- **Dialog 2** (eigentlicher Workflow, 6 Schritte): startet erst, nachdem - **Dialog 2** (eigentlicher Workflow, 6 Schritte): startet erst, nachdem
das Verfahrensgebiet feststeht (direkt nach Dialog 1 bei eindeutigem das Verfahrensgebiet feststeht und die Stop-Abfrage mit "Ja" bestätigt
Treffer/KEEP, oder erst nach Bestätigung des Auswahldialogs mit OK). wurde.
Schrittfolge in Dialog 2: Schrittfolge in Dialog 2:
@@ -351,8 +356,6 @@ def verfahrensgebiet_alkis_komplett(
# Dialog 1 schließt hier immer – egal ob eine Auswahl folgt oder nicht. # Dialog 1 schließt hier immer – egal ob eine Auswahl folgt oder nicht.
progress_suche.close() progress_suche.close()
nur_laden = False
try: try:
if vorab_layer is not None or not kombinationen: if vorab_layer is not None or not kombinationen:
# KEEP oder Fehler/Abbruch bereits in Phase 1 entschieden # KEEP oder Fehler/Abbruch bereits in Phase 1 entschieden
@@ -364,7 +367,7 @@ def verfahrensgebiet_alkis_komplett(
# Objekt(e) des Landkreises auswählen – kein eigener Fortschritts- # Objekt(e) des Landkreises auswählen – kein eigener Fortschritts-
# dialog währenddessen, nur der Auswahldialog selbst. # dialog währenddessen, nur der Auswahldialog selbst.
# ------------------------------------------------------------------ # ------------------------------------------------------------------
alkis_vg_layer, vg_status, nur_laden = _baue_layer_aus_landkreis_auswahl( alkis_vg_layer, vg_status = _baue_layer_aus_landkreis_auswahl(
tab_widget, kombinationen, crs, vg_status tab_widget, kombinationen, crs, vg_status
) )
if vg_status == LoadStatus.NONE or not alkis_vg_layer: if vg_status == LoadStatus.NONE or not alkis_vg_layer:
@@ -387,12 +390,21 @@ def verfahrensgebiet_alkis_komplett(
project.addMapLayer(alkis_vg_layer) project.addMapLayer(alkis_vg_layer)
bror_was_added_by_us = True bror_was_added_by_us = True
# Style direkt nach dem Laden anwenden – unabhängig vom weiteren # Style direkt nach dem Laden anwenden – unabhängig vom weiteren
# Ablauf, damit auch "Nur Objekte laden" den Style erhält. # Ablauf, damit auch bei "Nein" auf die Stop-Abfrage der Style
# gesetzt ist.
apply_style(alkis_vg_layer, BROR_STYLE_QML) apply_style(alkis_vg_layer, BROR_STYLE_QML)
if nur_laden: # ------------------------------------------------------------------
# Nutzer hat "Nur laden" angehakt – kein Dialog 2, kein weiterer # STOP: Zwischenabfrage nach dem Laden des BROR-Objekts, bevor mit
# Workflow (Gemarkung/Flurstücke/Vergleich). # Gemarkung/Flurstücke/Flächenvergleich fortgefahren wird (ersetzt
# die frühere "Nur Objekte laden"-Option).
# ------------------------------------------------------------------
if not ask_yes_no(
"Verfahrensgebiet laden",
f"Layer '{alkis_vg_layer.name()}' wurde geladen. Weiter?",
default=True,
parent=tab_widget,
):
abgeschlossen = True abgeschlossen = True
return alkis_vg_layer, None, None, vg_status return alkis_vg_layer, None, None, vg_status
+10 -13
View File
@@ -101,15 +101,15 @@ def ask_bror_kombinationen(
def ask_landkreis_objekte( def ask_landkreis_objekte(
parent: Any, parent: Any,
kombinationen: list[tuple[str, str]], kombinationen: list[tuple[str, str]],
) -> tuple[list[tuple[str, str]], bool]: ) -> list[tuple[str, str]]:
"""Zeigt Dialog zur Auswahl eines, mehrerer oder aller BROR-Objekte eines """Zeigt Dialog zur Auswahl eines, mehrerer oder aller BROR-Objekte eines
Landkreises (Fallback, wenn keine Verfahrensnummer gesetzt ist). Landkreises (Fallback, wenn keine Verfahrensnummer gesetzt ist).
Wie :func:`ask_bror_kombinationen`, zusätzlich mit einer "Alle Wie :func:`ask_bror_kombinationen`, zusätzlich mit einer "Alle
auswählen"-Checkbox (da hier potenziell deutlich mehr Treffer als bei der auswählen"-Checkbox (da hier potenziell deutlich mehr Treffer als bei der
VKZ-Suche auftreten) und einer "Nur laden"-Checkbox, mit der gesteuert VKZ-Suche auftreten). Ob nach dem Laden der weitere Workflow
wird, ob nach dem Laden der weitere Workflow (Gemarkung/Flurstücke/ (Gemarkung/Flurstücke/Flächenvergleich) fortgesetzt wird, klärt eine
Flächenvergleich) fortgesetzt werden soll. separate Stop-Abfrage nach dem Laden – nicht mehr dieser Dialog.
Parameters Parameters
---------- ----------
@@ -120,16 +120,16 @@ def ask_landkreis_objekte(
Returns Returns
------- -------
tuple[list[tuple[str, str]], bool] list[tuple[str, str]]
``(ausgewaehlte_kombinationen, nur_laden)``. ``([], False)`` bei Die gewählten ``(adv_name, bezeichnung)``-Tupel. Leere Liste bei
Abbruch oder wenn nichts ausgewählt wurde. Abbruch oder wenn nichts ausgewählt wurde.
""" """
if not kombinationen: if not kombinationen:
return [], False return []
if QT_VERSION == 0: # Mock-Modus if QT_VERSION == 0: # Mock-Modus
print(f"Mock-Modus: ask_landkreis_objekte → {kombinationen[:1]}") print(f"Mock-Modus: ask_landkreis_objekte → {kombinationen[:1]}")
return kombinationen[:1], False return kombinationen[:1]
dialog = QDialog(parent) dialog = QDialog(parent)
dialog.setWindowTitle("Verfahrensgebiete aus Landkreis laden") dialog.setWindowTitle("Verfahrensgebiete aus Landkreis laden")
@@ -204,9 +204,6 @@ def ask_landkreis_objekte(
mehrfachauswahl_hinweis = QLabel("<small>Mehrfachauswahl mit Strg + Umschalt</small>") mehrfachauswahl_hinweis = QLabel("<small>Mehrfachauswahl mit Strg + Umschalt</small>")
layout.addWidget(mehrfachauswahl_hinweis) layout.addWidget(mehrfachauswahl_hinweis)
nur_laden_checkbox = QCheckBox("Nur Objekte laden (ohne weitere Prüfungen)")
layout.addWidget(nur_laden_checkbox)
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel) buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
buttons.accepted.connect(dialog.accept) buttons.accepted.connect(dialog.accept)
buttons.rejected.connect(dialog.reject) buttons.rejected.connect(dialog.reject)
@@ -235,11 +232,11 @@ def ask_landkreis_objekte(
dialog.resize(breite, dialog.sizeHint().height()) dialog.resize(breite, dialog.sizeHint().height())
if exec_dialog(dialog) != DialogAccepted: if exec_dialog(dialog) != DialogAccepted:
return [], False return []
ausgewaehlte_zeilen = sorted(set(item.row() for item in table.selectedItems())) ausgewaehlte_zeilen = sorted(set(item.row() for item in table.selectedItems()))
ausgewaehlt = [(table.item(row, 1).text(), table.item(row, 0).text()) for row in ausgewaehlte_zeilen] ausgewaehlt = [(table.item(row, 1).text(), table.item(row, 0).text()) for row in ausgewaehlte_zeilen]
return ausgewaehlt, nur_laden_checkbox.isChecked() return ausgewaehlt
def ask_landkreis_auswahl( def ask_landkreis_auswahl(
+397 -67
View File
@@ -3,8 +3,10 @@
Orchestriert den vollständigen Arbeitsablauf zum Laden eines Verfahrensgebiets Orchestriert den vollständigen Arbeitsablauf zum Laden eines Verfahrensgebiets
aus einem ZIP-Archiv mit Shapefile-Set: aus einem ZIP-Archiv mit Shapefile-Set:
1. Dateidialog öffnen (.zip-Filter); Nutzerabbruch beendet den Vorgang. 1. Dateidialog öffnen (.zip/.shp-Filter, Mehrfachauswahl); Nutzerabbruch
2. ZIP neben der ZIP-Datei entpacken und auf ein gültiges Shapefile-Set prüfen. 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). 3. Shapefile als QGIS-Vektorlayer laden und validieren (Standard-QGIS-Routinen).
4. Alle Features per ``QgsGeometry.unaryUnion`` zu einem Single-Feature- 4. Alle Features per ``QgsGeometry.unaryUnion`` zu einem Single-Feature-
Memory-Layer „Verfahrensgebiet" verschmelzen. Memory-Layer „Verfahrensgebiet" verschmelzen.
@@ -40,6 +42,9 @@ oder kein Ergebnis liefert, bricht den Workflow ab.
from __future__ import annotations from __future__ import annotations
import os import os
import re
import shutil
import tempfile
import zipfile import zipfile
from typing import Optional, Tuple from typing import Optional, Tuple
@@ -53,6 +58,7 @@ from sn_basis.functions.dialog_wrapper import (
create_progress_dialog, create_progress_dialog,
show_info_dialog, show_info_dialog,
ask_detailpruefung_oder_abbrechen, ask_detailpruefung_oder_abbrechen,
ask_yes_no,
) )
from sn_verfahrensgebiet.functions.verfahrensgebiet_alkis import LoadStatus, verfahrensgebiet_alkis from sn_verfahrensgebiet.functions.verfahrensgebiet_alkis import LoadStatus, verfahrensgebiet_alkis
@@ -79,11 +85,129 @@ from sn_verfahrensgebiet.functions.verfahrensgebiet_alkis_komplett import (
BROR_LAYER_NAME, 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 # Interne Hilfsfunktionen
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def _normalisiere_feldname(name: str) -> str:
"""Normalisiert einen Feldnamen für den toleranten Vergleich.
Entfernt alle Zeichen außer Buchstaben/Ziffern (z.B. Punkte, Leerzeichen,
Unterstriche) und wandelt in Kleinbuchstaben um. Hintergrund: manche
Shape-Exporte liefern den Feldnamen ``FBSchl`` mit einem angehängten
Punkt (``FBSchl.``) oder abweichender Groß-/Kleinschreibung, wodurch ein
exakter Namensvergleich das Feld fälschlich als fehlend meldet (#44).
Parameters
----------
name :
Roher Feldname.
Returns
-------
str
Normalisierter Feldname (nur Kleinbuchstaben/Ziffern).
"""
return re.sub(r"[^a-zA-Z0-9]", "", name).lower()
def _finde_fbschl_index(raw_layer) -> int:
"""Sucht den Feldindex von ``FBSchl`` tolerant gegenüber Abweichungen im
Namen (Groß-/Kleinschreibung, angehängte Sonderzeichen wie ein Punkt).
Versucht zuerst den exakten Namen; schlägt das fehl, wird über alle
Felder mit normalisiertem Namensvergleich gesucht (siehe
:func:`_normalisiere_feldname`).
Parameters
----------
raw_layer :
Dateibasierter Shapefile-Layer.
Returns
-------
int
Feldindex, oder ``-1`` wenn kein passendes Feld gefunden wurde.
"""
idx = raw_layer.fields().indexFromName(FBSCHL_FELD)
if idx >= 0:
return idx
ziel = _normalisiere_feldname(FBSCHL_FELD)
for feld_idx, feld in enumerate(raw_layer.fields()):
if _normalisiere_feldname(feld.name()) == ziel:
return feld_idx
return -1
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.
Die Feldsuche ist tolerant gegenüber Abweichungen im Namen (z.B. ein
angehängter Punkt ``FBSchl.`` in manchen Shape-Exporten, siehe #44) –
siehe :func:`_finde_fbschl_index`.
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 = _finde_fbschl_index(raw_layer)
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: def _wende_vergleich_stil_an(layer) -> None:
"""Wendet einen kategorisierten Renderer auf den Vergleich-Layer an. """Wendet einen kategorisierten Renderer auf den Vergleich-Layer an.
@@ -215,6 +339,31 @@ def _finde_shp(verzeichnis: str) -> Optional[str]:
return None 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]: def _erstelle_memory_layer_aus_shp(shp_layer) -> Optional[object]:
"""Vereint alle Features des Shapefile-Layers zu einem Single-Feature- """Vereint alle Features des Shapefile-Layers zu einem Single-Feature-
Memory-Layer „Verfahrensgebiet" via ``QgsGeometry.unaryUnion``. Memory-Layer „Verfahrensgebiet" via ``QgsGeometry.unaryUnion``.
@@ -290,17 +439,38 @@ def _erstelle_memory_layer_aus_shp(shp_layer) -> Optional[object]:
# Öffentliche Lade-Hilfsfunktion # Öffentliche Lade-Hilfsfunktion
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def lade_vg_aus_zip(parent_widget) -> Optional[object]: def lade_vg_aus_zip(parent_widget) -> Optional[list]:
"""Öffnet Dateidialog, entpackt ZIP und gibt Union-Memory-Layer zurück. """Öffnet Dateidialog (Mehrfachauswahl), entpackt ZIPs (bzw. nutzt direkt
gewählte ``.shp``-Dateien) und liefert je Gebietstyp einen Union-Memory-Layer.
Schrittfolge: Schrittfolge:
1. ``QFileDialog.getOpenFileName`` mit Filter ``ZIP-Dateien (*.zip)``. 1. ``QFileDialog.getOpenFileNames`` mit Filter für ``.zip`` und ``.shp``
2. ZIP neben der Quelldatei in ein Unterverzeichnis entpacken. (Mehrfachauswahl möglich).
3. Erste ``.shp``-Datei im Verzeichnis finden. 2. Für jede ausgewählte Datei: bei ``.zip`` in ein temporäres Verzeichnis
4. Als ``QgsVectorLayer`` laden und ``isValid()`` prüfen. entpacken (danach wieder gelöscht) und darin **alle** enthaltenen
5. Alle Features per ``QgsGeometry.unaryUnion`` verschmelzen. ``.shp``-Dateien finden (ein ZIP kann mehrere Shapefile-Sets
6. Single-Feature-Memory-Layer „Verfahrensgebiet" zurückgeben. 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 Parameters
---------- ----------
@@ -309,62 +479,173 @@ def lade_vg_aus_zip(parent_widget) -> Optional[object]:
Returns Returns
------- -------
Optional[QgsVectorLayer] Optional[list]
Valider Memory-Layer „Verfahrensgebiet" oder ``None`` bei Abbruch/Fehler. 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, 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 return None # Nutzerabbruch
zip_dir = os.path.dirname(pfad) # Je Gebietstyp gesammelte Geometrien/Felder/Attribute über alle
zip_base = os.path.splitext(os.path.basename(pfad))[0] # ausgewählten Dateien hinweg (Reihenfolge der ersten Sichtung bleibt
extract_dir = os.path.join(zip_dir, zip_base) # ü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: try:
with zipfile.ZipFile(pfad, "r") as zf: # (shp_path, anzeige_name) für jede tatsächlich zu prüfende
zf.extractall(extract_dir) # Shapefile-Datei sammeln – ein ZIP kann mehrere Shapefile-Sets
except Exception: # enthalten, jedes wird einzeln behandelt.
QMessageBox.warning( shp_eintraege: list = []
parent_widget,
"Fehler",
"Die ZIP-Datei konnte nicht entpackt werden.\n"
"Bitte eine gültige ZIP-Datei auswählen.",
)
return None
shp_path = _finde_shp(extract_dir) for pfad in pfade:
if not shp_path: dateiname = os.path.basename(pfad)
QMessageBox.warning(
parent_widget,
"Fehler",
"gültiges Shape-Dateiset angeben",
)
return None
raw_layer = QgsVectorLayer(shp_path, "tmp_shape_vg", "ogr") if pfad.lower().endswith(".shp"):
if not raw_layer or not raw_layer.isValid(): shp_eintraege.append((pfad, dateiname))
QMessageBox.warning( continue
parent_widget,
"Fehler",
"gültiges Shape-Dateiset angeben",
)
return None
mem_layer = _erstelle_memory_layer_aus_shp(raw_layer) extract_dir = tempfile.mkdtemp(prefix="sn_vg_shape_")
if mem_layer is None: temp_dirs.append(extract_dir)
QMessageBox.warning(
parent_widget,
"Fehler",
"gültiges Shape-Dateiset angeben",
)
return None
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)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -419,38 +700,66 @@ def verfahrensgebiet_shape_komplett(
""" """
project = QgsProject.instance() project = QgsProject.instance()
shape_vg_layer = None shape_vg_layer = None
zusatz_shape_layer: list = []
bror_layer = None bror_layer = None
shape_was_added_by_us = False shape_was_added_by_us = False
bror_was_added_by_us = False bror_was_added_by_us = False
abgeschlossen = False abgeschlossen = False
progress = create_progress_dialog( progress = None
7, "Verfahrensgebiet aus Shape laden", "Initialisierung …"
)
try: try:
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# Schritt 1: Shape-VG aus ZIP laden (noch NICHT ins Projekt einfügen) # 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 …") shape_vg_layers = lade_vg_aus_zip(tab_widget)
progress.set_value(0)
if progress.is_canceled(): if not shape_vg_layers:
return None, None, None, LoadStatus.NONE return None, None, None, LoadStatus.NONE
shape_vg_layer = lade_vg_aus_zip(tab_widget) # Je Gebietstyp ein Layer (max. 2). Der erste ist das für den
progress.raise_to_front() # 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:]
if not shape_vg_layer: # 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",
f"Layer '{geladene_namen}' wurde(n) geladen. Weiter?",
default=True,
parent=tab_widget,
):
# abgeschlossen=True, damit der finally-Block den zur Kontrolle
# eingefügten Layer NICHT wieder entfernt (kein Fehlerfall).
abgeschlossen = True
return None, None, None, LoadStatus.NONE return None, None, None, LoadStatus.NONE
vg_status = LoadStatus.FIRST 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) progress.set_value(1)
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# Schritt 2: BROR aus ALKIS laden (vor shape_vg, um Namenskonflikt # Schritt 2: BROR aus ALKIS laden (vor shape_vg, um Namenskonflikt
# in _check_existing_layer zu vermeiden) # in _check_existing_layer zu vermeiden)
# ------------------------------------------------------------------ # ------------------------------------------------------------------
progress.set_label("Schritt 2/6: BROR aus ALKIS laden …")
if progress.is_canceled(): if progress.is_canceled():
return None, None, None, LoadStatus.NONE return None, None, None, LoadStatus.NONE
@@ -468,9 +777,25 @@ def verfahrensgebiet_shape_komplett(
progress.set_value(2) progress.set_value(2)
# Shape-VG erst jetzt ins Projekt einfügen (nach BROR-Ladung) # Shape-VG erst jetzt ins Projekt einfügen (nach BROR-Ladung) – außer
project.addMapLayer(shape_vg_layer) # der TEMPORÄRE STOPP oben hat ihn schon eingefügt.
shape_was_added_by_us = True if not project.mapLayer(shape_vg_layer.id()):
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 # Schritt 3: Gemarkungsschlüssel per Geometrie-Spatial-Join ermitteln
@@ -691,6 +1016,11 @@ def verfahrensgebiet_shape_komplett(
_entferne_hilfs_layer(project, "TempVerfahrensgebiet") _entferne_hilfs_layer(project, "TempVerfahrensgebiet")
# Shape-VG und BROR bei Fehler/Abbruch entfernen # Shape-VG und BROR bei Fehler/Abbruch entfernen
if not abgeschlossen: 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: if shape_was_added_by_us and shape_vg_layer is not None:
try: try:
project.removeMapLayer(shape_vg_layer.id()) project.removeMapLayer(shape_vg_layer.id())
+9 -9
View File
@@ -251,15 +251,15 @@ class WorkingTab(QWidget):
Styles angewendet und die UI-Elemente aktualisiert. Styles angewendet und die UI-Elemente aktualisiert.
""" """
# Vorprüfung: Verfahrensnummer muss gesetzt sein # Vorprüfung: Verfahrensnummer muss gesetzt sein
vkz = get_variable("verfahrensnummer") or "" # vkz = get_variable("verfahrensnummer") or ""
if not str(vkz).strip(): # if not str(vkz).strip():
if not ask_yes_no( # if not ask_yes_no(
"Verfahrensnummer nicht gesetzt", # "Verfahrensnummer nicht gesetzt",
"Verfahrensnummer nicht gesetzt. Trotzdem fortfahren?", # "Verfahrensnummer nicht gesetzt. Trotzdem fortfahren?",
default=False, # default=False,
parent=self, # parent=self,
): # ):
return # return
self._set_busy(self.btn_verf_shape, True, "Aus Shape laden") self._set_busy(self.btn_verf_shape, True, "Aus Shape laden")
try: try: