Files
Plugin_SN_Verfahrensgebiet/functions/verfahrensgebiet_shape_komplett.py
T

978 lines
37 KiB
Python
Raw Normal View History

2026-07-15 15:29:48 +02:00
"""sn_verfahrensgebiet/functions/verfahrensgebiet_shape_komplett.py – VG-Lade-Workflow aus Shape (ZIP).
Orchestriert den vollständigen Arbeitsablauf zum Laden eines Verfahrensgebiets
aus einem ZIP-Archiv mit Shapefile-Set:
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.
2026-07-15 15:29:48 +02:00
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.
5. Gemarkungsschlüssel per Geometrie aus ``AX_Gemarkung`` vorschlagen.
6. Flurstücke per Dialog (mit Vorschlag) laden.
7. Flurstücke räumlich auf das Shape-VG einschränken (``selectByLocation``).
8. Temporären enriched Layer aus Auswahl für den Flächenvergleich erzeugen
(wird **nicht** ins Projekt eingefügt und **nicht** zurückgegeben).
9. Flächenvergleich zwischen Shape-VG und temporärem enriched Layer.
- **Übereinstimmung** (≤ Toleranz): modale OK-Meldung, Ablauf beendet.
- **Abweichung**: 2-Optionen-Dialog (Detailprüfung / Abbrechen).
10. Bei Wahl „Detailprüfung" (knickpunktbasiert):
- Grenzpunkte (AX_PunktortTA) per BBox laden.
- Knickpunkte des Shape-VG mit PunktortTA vergleichen (grün/rot/orange).
- Korrigiertes Polygon aus identischen + gefundenen PunktortTA ableiten.
- Ergebnislayer (Punkte), Verbindungslinien und Polygon ins Projekt einfügen.
- Abschlussmeldung mit Zählung.
Das Shape-VG (``Verfahrensgebiet``) ist das finale Rückgabe-Layer. Ein
``enriched Layer aus Flurstücken`` wird nur temporär für den Flächenvergleich
erzeugt, aber weder ins Projekt eingefügt noch zurückgegeben.
**Rückgabe:**
``Tuple[shape_vg_layer, flst_layer, None, LoadStatus]``
**Abbruchverhalten:** Jeder Schritt, der :attr:`LoadStatus.NONE` zurückgibt
oder kein Ergebnis liefert, bricht den Workflow ab.
"""
from __future__ import annotations
import os
import shutil
import tempfile
2026-07-15 15:29:48 +02:00
import zipfile
from typing import Optional, Tuple
2026-07-15 15:39:21 +02:00
from sn_basis.functions import QgsProject, QgsVectorLayer, QgsFeature, QgsField
2026-07-15 15:29:48 +02:00
from sn_basis.functions import QMessageBox
from sn_basis.functions.message_wrapper import warning
from sn_basis.functions.variable_wrapper import get_variable
2026-07-15 15:39:21 +02:00
from sn_basis.functions.qt_wrapper import QFileDialog, QVariant
2026-07-15 15:29:48 +02:00
from sn_basis.functions.qgiscore_wrapper import QgsGeometry
from sn_basis.functions.dialog_wrapper import (
create_progress_dialog,
show_info_dialog,
ask_detailpruefung_oder_abbrechen,
ask_yes_no,
2026-07-15 15:29:48 +02:00
)
from sn_verfahrensgebiet.functions.verfahrensgebiet_alkis import LoadStatus, verfahrensgebiet_alkis
from sn_verfahrensgebiet.functions.gemarkung_alkis import (
gemarkung_schluessel_und_namen_von_vg_geometrie,
)
from sn_verfahrensgebiet.functions.flurstueck_alkis import flurstueck_alkis
from sn_verfahrensgebiet.functions.verfahrensgebiet_edit_service import (
erstelle_verfahrensgebiet_aus_auswahl,
VERF_LAYER_NAME,
)
from sn_verfahrensgebiet.functions.grenzpunkt_alkis import lade_punktort_ta
from sn_verfahrensgebiet.functions.knickpunkt_pruefung import (
knickpunkt_pruefung,
_extrahiere_knickpunkte,
erstelle_korrigiertes_polygon,
)
from sn_verfahrensgebiet.functions.verfahrensgebiet_alkis_komplett import (
_select_by_location,
_vergleiche_flaechen,
_lese_flaechen_toleranz,
_entferne_hilfs_layer,
MINDEST_FLAECHEN_TOLERANZ_M2,
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,
}
2026-07-15 15:29:48 +02:00
# ---------------------------------------------------------------------------
# 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}')."
)
2026-07-15 15:39:21 +02:00
def _wende_vergleich_stil_an(layer) -> None:
"""Wendet einen kategorisierten Renderer auf den Vergleich-Layer an.
Farben:
- ``identisch`` (Schnittmenge): Gr\u00fcn
- ``nur_bror`` (nur im BROR): Gelb
- ``nur_vg`` (nur im Verfahrensgebiet): Blau
Schl\u00e4gt der Import der QGIS-Rendering-Klassen fehl (z.\u00a0B. im
Test-Modus), wird der Fehler still ignoriert.
"""
try:
from qgis.core import ( # type: ignore[import]
QgsCategorizedSymbolRenderer,
QgsRendererCategory,
QgsFillSymbol,
)
_FARBEN = [
("identisch", "#00aa00", 0.5), # gr\u00fcn
("nur_bror", "#ffcc00", 0.5), # gelb
("nur_vg", "#3366ff", 0.5), # blau
]
kategorien = []
for wert, farbe, opazitaet in _FARBEN:
symbol = QgsFillSymbol.createSimple({
"color": farbe,
"outline_color": "#555555",
"outline_width": "0.26",
})
symbol.setOpacity(opazitaet)
kategorien.append(QgsRendererCategory(wert, symbol, wert))
renderer = QgsCategorizedSymbolRenderer("typ", kategorien)
layer.setRenderer(renderer)
except Exception:
pass
def _erstelle_vergleich_layer(vg_layer, bror_layer) -> Optional[object]:
"""Erzeugt einen Vergleich-Layer aus geometrischen Differenzoperationen.
F\u00fcr jeden Fl\u00e4chentyp wird ein Feature angelegt:
- ``identisch``: Schnittmenge aus VG und BROR (gr\u00fcn).
- ``nur_bror``: Fl\u00e4che, die nur im BROR-Objekt liegt (gelb).
- ``nur_vg``: Fl\u00e4che, die nur im Verfahrensgebiet liegt (blau).
Parameters
----------
vg_layer :
Verfahrensgebiet-Layer (Shape).
bror_layer :
BauRaumOderBodenordnungsrecht-Layer (ALKIS).
Returns
-------
Optional[QgsVectorLayer]
Memory-Layer ``Vergleich`` mit Stil oder ``None`` bei Fehler.
"""
try:
vg_geom = None
for feat in vg_layer.getFeatures():
g = feat.geometry()
if g and not g.isEmpty():
vg_geom = g
break
bror_geom = None
for feat in bror_layer.getFeatures():
g = feat.geometry()
if g and not g.isEmpty():
bror_geom = g
break
if not vg_geom or not bror_geom:
return None
geom_identisch = vg_geom.intersection(bror_geom)
geom_nur_bror = bror_geom.difference(vg_geom)
geom_nur_vg = vg_geom.difference(bror_geom)
crs = vg_layer.crs().authid()
mem_layer = QgsVectorLayer(
f"MultiPolygon?crs={crs}", "Vergleich", "memory"
)
pr = mem_layer.dataProvider()
pr.addAttributes([QgsField("typ", QVariant.String)])
mem_layer.updateFields()
feats = []
for geom, typ in [
(geom_identisch, "identisch"),
(geom_nur_bror, "nur_bror"),
(geom_nur_vg, "nur_vg"),
]:
if geom and not geom.isEmpty():
f = QgsFeature()
f.setGeometry(geom)
f.setAttributes([typ])
feats.append(f)
pr.addFeatures(feats)
mem_layer.updateExtents()
_wende_vergleich_stil_an(mem_layer)
return mem_layer if mem_layer.isValid() else None
except Exception:
return None
2026-07-15 15:29:48 +02:00
def _finde_shp(verzeichnis: str) -> Optional[str]:
"""Sucht rekursiv nach der ersten ``.shp``-Datei im Verzeichnis.
Parameters
----------
verzeichnis :
Startverzeichnis für die Suche.
Returns
-------
Optional[str]
Absoluter Pfad zur ersten gefundenen ``.shp``-Datei, oder ``None``.
"""
for root, _dirs, files in os.walk(verzeichnis):
for fname in files:
if fname.lower().endswith(".shp"):
return os.path.join(root, fname)
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)
2026-07-15 15:29:48 +02:00
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``.
Alle Felder des Shapefiles werden in den Memory-Layer übernommen.
Die Attributwerte des ersten Features mit gültiger Geometrie werden
auf das Union-Feature gesetzt.
Parameters
----------
shp_layer :
Geladener ``QgsVectorLayer`` aus dem Shapefile.
Returns
-------
Optional[QgsVectorLayer]
Single-Feature-Memory-Layer mit Union-Geometrie und Shapefile-Attributen
oder ``None`` bei Fehler.
"""
geom_list = []
first_feat = None
for feat in shp_layer.getFeatures():
try:
geom = feat.geometry()
if geom and not geom.isEmpty():
geom_list.append(geom)
if first_feat is None:
first_feat = feat
except Exception:
pass
if not geom_list:
return None
union_geom = QgsGeometry.unaryUnion(geom_list)
if not union_geom or union_geom.isEmpty():
return None
crs_authid = shp_layer.crs().authid()
mem_layer = QgsVectorLayer(
f"MultiPolygon?crs={crs_authid}", VERF_LAYER_NAME, "memory"
)
pr = mem_layer.dataProvider()
# Felder des Shapefiles übernehmen
try:
pr.addAttributes(list(shp_layer.fields()))
mem_layer.updateFields()
except Exception:
pass
feat_out = QgsFeature(mem_layer.fields())
feat_out.setGeometry(union_geom)
# Attributwerte vom ersten Feature übernehmen
if first_feat is not None:
try:
feat_out.setAttributes(first_feat.attributes())
except Exception:
pass
pr.addFeatures([feat_out])
mem_layer.updateExtents()
if not mem_layer.isValid() or mem_layer.featureCount() == 0:
return None
return mem_layer
# ---------------------------------------------------------------------------
# Öffentliche Lade-Hilfsfunktion
# ---------------------------------------------------------------------------
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.
2026-07-15 15:29:48 +02:00
Schrittfolge:
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.
2026-07-15 15:29:48 +02:00
Parameters
----------
parent_widget :
Eltern-Widget für Dateidialog und Fehlermeldungen.
Returns
-------
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.
2026-07-15 15:29:48 +02:00
"""
pfade, _ = QFileDialog.getOpenFileNames(
2026-07-15 15:29:48 +02:00
parent_widget,
"Shapefile-Set(s) (ZIP oder .shp) auswählen",
2026-07-15 15:29:48 +02:00
"",
"Shapefile-Set (*.zip *.shp);;ZIP-Dateien (*.zip);;Shapefile (*.shp)",
2026-07-15 15:29:48 +02:00
)
if not pfade:
2026-07-15 15:29:48 +02:00
return None # Nutzerabbruch
# 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 = []
2026-07-15 15:29:48 +02:00
try:
# (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 = []
2026-07-15 15:29:48 +02:00
for pfad in pfade:
dateiname = os.path.basename(pfad)
2026-07-15 15:29:48 +02:00
if pfad.lower().endswith(".shp"):
shp_eintraege.append((pfad, dateiname))
continue
2026-07-15 15:29:48 +02:00
extract_dir = tempfile.mkdtemp(prefix="sn_vg_shape_")
temp_dirs.append(extract_dir)
2026-07-15 15:29:48 +02:00
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)
2026-07-15 15:29:48 +02:00
# ---------------------------------------------------------------------------
# Öffentliche API
# ---------------------------------------------------------------------------
def verfahrensgebiet_shape_komplett(
tab_widget,
) -> Tuple[Optional[object], Optional[object], Optional[object], LoadStatus]:
"""Vollständiger VG-Lade-Workflow aus Shape (ZIP): ZIP → BROR → Gemarkungen →
Flurstücke → Verschneidung → Flächenvergleich → opt. Umringsprüfung.
Das aus der ZIP geladene Shape-VG ist das finale Verfahrensgebiet-Layer.
Ist die Projektvariable ``verfahrensnummer`` gesetzt, wird das zugehörige
BROR-Objekt (``AX_BauRaumOderBodenordnungsrecht``) aus ALKIS geladen und
als Vergleichsobjekt im Flächenvergleich verwendet. Andernfalls wird ein
temporärer enriched Layer aus Flurstücken für den Vergleich herangezogen.
Schrittfolge:
1. ``lade_vg_aus_zip(tab_widget)`` – ZIP öffnen, Shapefile validieren,
Union-Memory-Layer „Verfahrensgebiet" erzeugen.
2. ``verfahrensgebiet_alkis(tab_widget, skip_existing_check=True)`` –
BROR-Objekt als Memory-Layer (sofern VKZ gesetzt).
Der BROR wird **vor** dem Shape-VG ins Projekt eingefügt, um den
Namenskonflikt mit ``_check_existing_layer`` zu vermeiden.
3. Shape-VG ins Projekt einfügen; BROR umbenennen zu
``BauRaumOderBodenordnungsrecht (ALKIS)`` und zum Projekt hinzufügen.
4. ``gemarkung_schluessel_und_namen_von_vg_geometrie(geom)`` – Schlüssel.
5. ``flurstueck_alkis(...)`` – Flurstücke laden.
6. ``_select_by_location(...)`` – räumliche Selektion.
7. Flächenvergleich:
- Mit BROR: ``_vergleiche_flaechen(bror_layer, shape_vg_layer, ...)``.
- Ohne BROR: temp. enriched Layer erzeugen und mit Shape-VG vergleichen.
8. Ggf. Detailprüfung (knickpunktbasiert) auf BROR bzw. Shape-VG.
Parameters
----------
tab_widget :
Eltern-Widget für alle Dialoge.
Returns
-------
Tuple[Optional[shape_vg_layer], Optional[flst_layer], Optional[bror_layer], LoadStatus]
- ``shape_vg_layer``: Memory-Layer „Verfahrensgebiet" (aus Shape).
- ``flst_layer``: Flurstücke-Layer (bleibt im Projekt).
- ``bror_layer``: BROR-Layer ``BauRaumOderBodenordnungsrecht (ALKIS)``
(wenn geladen, bleibt im Projekt); ``None`` wenn VKZ nicht gesetzt.
- :class:`LoadStatus`: ``FIRST`` oder ``NONE``.
"""
project = QgsProject.instance()
shape_vg_layer = None
zusatz_shape_layer: list = []
2026-07-15 15:29:48 +02:00
bror_layer = None
shape_was_added_by_us = False
bror_was_added_by_us = False
abgeschlossen = False
progress = None
2026-07-15 15:29:48 +02:00
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.
2026-07-15 15:29:48 +02:00
# ------------------------------------------------------------------
shape_vg_layers = lade_vg_aus_zip(tab_widget)
if not shape_vg_layers:
2026-07-15 15:29:48 +02:00
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:]
2026-07-15 15:29:48 +02:00
# 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
2026-07-15 15:29:48 +02:00
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 …"
)
2026-07-15 15:29:48 +02:00
progress.set_value(1)
# ------------------------------------------------------------------
# Schritt 2: BROR aus ALKIS laden (vor shape_vg, um Namenskonflikt
# in _check_existing_layer zu vermeiden)
# ------------------------------------------------------------------
if progress.is_canceled():
return None, None, None, LoadStatus.NONE
vkz = get_variable("verfahrensnummer") or ""
if str(vkz).strip():
bror_result, bror_status, _ = verfahrensgebiet_alkis(
tab_widget, skip_existing_check=True
)
progress.raise_to_front()
if bror_status != LoadStatus.NONE and bror_result:
bror_result.setName(BROR_LAYER_NAME)
project.addMapLayer(bror_result)
bror_layer = bror_result
bror_was_added_by_us = True
progress.set_value(2)
# Shape-VG erst jetzt ins Projekt einfügen (nach BROR-Ladung) – außer
# der TEMPORÄRE STOPP oben hat ihn schon eingefügt.
if not project.mapLayer(shape_vg_layer.id()):
project.addMapLayer(shape_vg_layer)
shape_was_added_by_us = True
2026-07-15 15:29:48 +02:00
# ------------------------------------------------------------------
# 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
2026-07-15 15:29:48 +02:00
# ------------------------------------------------------------------
# Schritt 3: Gemarkungsschlüssel per Geometrie-Spatial-Join ermitteln
# ------------------------------------------------------------------
progress.set_label("Schritt 3/6: Gemarkungsschlüssel ermitteln …")
if progress.is_canceled():
return None, None, None, LoadStatus.NONE
shape_feats = list(shape_vg_layer.getFeatures())
vg_geom = shape_feats[0].geometry() if shape_feats else None
schluessel_namen = (
gemarkung_schluessel_und_namen_von_vg_geometrie(vg_geom)
if vg_geom else []
)
vorgabe_schluessel = [nr for nr, _ in schluessel_namen]
vorgabe_namen = [nm for _, nm in schluessel_namen if nm]
if not vorgabe_schluessel:
warning(
"VG laden",
"Keine Gemarkungen in der BBox gefunden. Bitte Schlüssel manuell eingeben.",
duration=5,
)
progress.set_value(3)
# ------------------------------------------------------------------
# Schritt 4: Flurstücke laden (mit Nutzer-Dialog, Vorschlag befüllt)
# ------------------------------------------------------------------
progress.set_label("Schritt 4/6: Flurstücke aus ALKIS laden …")
if progress.is_canceled():
return None, None, None, LoadStatus.NONE
flst_layer, flst_status = flurstueck_alkis(
tab_widget,
vorgabe_schluessel=vorgabe_schluessel,
vorgabe_namen=vorgabe_namen if vorgabe_namen else None,
ohne_dialog=True,
)
if flst_status == LoadStatus.NONE or not flst_layer:
return None, None, None, LoadStatus.NONE
progress.set_value(4)
# ------------------------------------------------------------------
# Schritt 5: Räumliche Selektion
# ------------------------------------------------------------------
progress.set_label("Schritt 5/6: Flurstücke räumlich selektieren …")
if progress.is_canceled():
return None, None, None, LoadStatus.NONE
anzahl = _select_by_location(flst_layer, shape_vg_layer)
if anzahl == 0:
QMessageBox.warning(
tab_widget,
"Keine Flurstücke im VG",
"Es wurden keine Flurstücke gefunden, die das Verfahrensgebiet schneiden.\n"
"Bitte Gemarkungsschlüssel und Ausdehnung prüfen.",
)
return None, flst_layer, None, LoadStatus.NONE
progress.set_value(5)
# ------------------------------------------------------------------
# Schritt 6: Flächenvergleich als Gate
# ------------------------------------------------------------------
progress.set_label("Schritt 6/6: Flächenvergleich …")
if progress.is_canceled():
return None, None, None, LoadStatus.NONE
toleranz = _lese_flaechen_toleranz()
if bror_layer and bror_layer.isValid():
# BROR verfügbar → als Referenz-Objekt verwenden
ref_flaeche, shape_flaeche, stimmt_ueberein = _vergleiche_flaechen(
bror_layer, shape_vg_layer, toleranz
)
dialog_ref_label = "BauRaumOderBodenordnungsrecht"
dialog_vg_label = "Shape-Verfahrensgebiet "
pruef_layer = bror_layer
2026-07-15 15:39:21 +02:00
# Vergleich-Layer (grün/gelb/blau) ins Projekt einfügen
vergleich_layer = _erstelle_vergleich_layer(shape_vg_layer, bror_layer)
if vergleich_layer:
project.addMapLayer(vergleich_layer)
2026-07-15 15:29:48 +02:00
else:
# Kein BROR → temp. enriched Layer aus Flurstücken erzeugen
temp_enriched, _, _ = erstelle_verfahrensgebiet_aus_auswahl(flst_layer)
ref_flaeche, shape_flaeche, stimmt_ueberein = _vergleiche_flaechen(
shape_vg_layer, temp_enriched, toleranz
)
dialog_ref_label = "Shape-Verfahrensgebiet "
dialog_vg_label = "Verfahrensgebiet (Flurstücke)"
pruef_layer = shape_vg_layer
progress.set_value(6)
if stimmt_ueberein:
progress.set_value(7)
progress.close()
show_info_dialog(
"Flächenvergleich",
"Verfahrensgebiet mit erwarteter Geometrie erzeugt",
parent=tab_widget,
)
abgeschlossen = True
return shape_vg_layer, flst_layer, bror_layer, vg_status
# ------------------------------------------------------------------
# Flächenabweichung → 2-Optionen-Dialog
# ------------------------------------------------------------------
progress.close()
diff = abs(ref_flaeche - shape_flaeche)
entscheidung = ask_detailpruefung_oder_abbrechen(
tab_widget,
"Abweichung erkannt",
f"Abweichung erkannt.\n\n"
f"{dialog_ref_label}: {ref_flaeche:.2f} m²\n"
f"{dialog_vg_label}: {shape_flaeche:.2f} m²\n"
f"Differenz: {diff:.2f} m²\n\n"
f"Wie möchten Sie fortfahren?",
)
if entscheidung == "abbrechen":
abgeschlossen = True
return shape_vg_layer, flst_layer, bror_layer, vg_status
# ------------------------------------------------------------------
# Option: Detailprüfung (knickpunktbasiert) auf pruef_layer
# (= BROR wenn vorhanden, sonst Shape-VG)
# ------------------------------------------------------------------
knickpunkte = _extrahiere_knickpunkte(pruef_layer)
n_knick = len(knickpunkte)
gesamt_schritte = n_knick + 5
detail_progress = create_progress_dialog(
gesamt_schritte,
"Detailprüfung (Knickpunkte)",
"Schritt 1: Grenzpunkte laden …",
)
detail_progress.set_value(0)
punktort_layer = lade_punktort_ta(
vorgabe_schluessel, vg_geom, progress=detail_progress
)
if detail_progress.is_canceled():
detail_progress.close()
abgeschlossen = True
return shape_vg_layer, flst_layer, bror_layer, vg_status
detail_progress.set_value(1)
if punktort_layer is None:
detail_progress.close()
abgeschlossen = True
if not detail_progress.is_canceled():
QMessageBox.critical(
tab_widget,
"WFS-Fehler",
"Die Grenzpunktdaten (PunktortTA) konnten nicht geladen werden.\n\n"
"Mögliche Ursache: Netzwerk- oder Proxy-Fehler.\n"
"Bitte prüfen Sie die QGIS-Proxy-Einstellungen "
"(Einstellungen -> Optionen -> Netzwerk) und den Log-Eintrag "
"unter Ansicht -> Bedienfelder -> Protokollnachrichten "
"(Kategorie 'sn_verfahrensgebiet').",
)
return shape_vg_layer, flst_layer, bror_layer, vg_status
ergebnis_layer, identisch, kein_pta, fehlender_knick = knickpunkt_pruefung(
pruef_layer,
punktort_layer,
flst_layer,
progress=detail_progress,
knickpunkte=knickpunkte,
)
if detail_progress.is_canceled():
detail_progress.close()
abgeschlossen = True
return shape_vg_layer, flst_layer, bror_layer, vg_status
detail_progress.set_value(gesamt_schritte)
detail_progress.close()
# ------------------------------------------------------------------
# Korrigiertes Polygon aus identischen + fehlenden Knickpunkten
# (nur für Visualisierung – shape_vg_layer bleibt das finale VG)
# ------------------------------------------------------------------
if ergebnis_layer and ergebnis_layer.isValid():
linien_layer_korr, polygon_layer_korr = erstelle_korrigiertes_polygon(
pruef_layer, ergebnis_layer, pruef_layer.crs().authid()
)
if polygon_layer_korr is not None and polygon_layer_korr.isValid():
if linien_layer_korr is not None and linien_layer_korr.isValid():
project.addMapLayer(linien_layer_korr)
project.addMapLayer(polygon_layer_korr)
if ergebnis_layer and ergebnis_layer.isValid():
project.addMapLayer(ergebnis_layer)
show_info_dialog(
"Detailprüfung abgeschlossen",
f"Knickpunkte geprüft: {n_knick}\n"
f" Identisch (grün): {identisch}\n"
f" Kein PunktortTA (rot): {kein_pta}\n"
f" Fehlende Knickpunkte (orange): {fehlender_knick}",
parent=tab_widget,
)
abgeschlossen = True
return shape_vg_layer, flst_layer, bror_layer, vg_status
finally:
_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
2026-07-15 15:29:48 +02:00
if shape_was_added_by_us and shape_vg_layer is not None:
try:
project.removeMapLayer(shape_vg_layer.id())
except Exception:
pass
if bror_was_added_by_us and bror_layer is not None:
try:
project.removeMapLayer(bror_layer.id())
except Exception:
pass