Files
Plugin_SN_Verfahrensgebiet/functions/verfahrensgebiet_alkis_komplett.py
T
2026-04-23 22:24:14 +02:00

481 lines
17 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""sn_verfahrensgebiet/functions/verfahrensgebiet_alkis_komplett.py – Vollständiger VG-Lade-Workflow.
Orchestriert den vollständigen Arbeitsablauf zum Laden eines Verfahrensgebiets
aus ALKIS mit anschließender Flurstücks-Verschneidung, Flächenvergleich und
optionaler Umringsprüfung:
1. Verfahrensgebiet aus ``AX_BauRaumOderBodenordnungsrecht`` per VKZ laden.
2. Gemarkungsschlüssel per Geometrie aus ``AX_Gemarkung`` vorschlagen.
3. Flurstücke per Dialog (mit Vorschlag) laden.
4. Flurstücke räumlich auf das VG einschränken (``selectByLocation``).
5. Aus der Auswahl den enriched Verfahrensgebiet-Memory-Layer erzeugen.
6. Flächenvergleich zwischen BROR-Objekt und abgeleitetem Verfahrensgebiet.
- **Übereinstimmung** (≤ Toleranz): modale OK-Meldung, Ablauf beendet.
- **Abweichung**: Ja/Nein-Abfrage für Detailprüfung.
7. Optionale Umringsprüfung (Variante 1):
- BROR-Umring gegen Flurstücksgrenzen vergleichen.
- Ergebnislayer (grün/rot) ins Projekt einfügen.
- Abschlussmeldung mit Konfliktanzahl.
Das BROR-Objekt (``AX_BauRaumOderBodenordnungsrecht``) bleibt bei Erfolg als
temporärer Layer ``BauRaumOrdnungsrecht (ALKIS)`` im Projekt erhalten.
**Rückgabe:**
``Tuple[enriched_layer, flst_layer, bror_layer, LoadStatus]``
**Abbruchverhalten:** Jeder Schritt, der :attr:`LoadStatus.NONE` zurückgibt
oder kein Ergebnis liefert, bricht den Workflow ab. Der BROR-Layer wird nur
bei Fehler/Abbruch entfernt (wenn er in diesem Lauf hinzugefügt wurde).
"""
from __future__ import annotations
from typing import Optional, Tuple
from sn_basis.functions import QgsProject
from sn_basis.functions import QMessageBox
from sn_basis.functions.message_wrapper import warning
from sn_basis.functions.variable_wrapper import get_variable
from sn_basis.functions.dialog_wrapper import (
create_progress_dialog,
show_info_dialog,
ask_yes_no,
)
from sn_verfahrensgebiet.functions.verfahrensgebiet_alkis import (
verfahrensgebiet_alkis,
LoadStatus,
)
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.umringspruefung import umringspruefung
# Feldname für das Verfahrenskennzeichen im enriched Layer
FELD_VKZ = "verfahrensnummer"
#: Anzeigename des BROR-Layers im Projekt
BROR_LAYER_NAME = "BauRaumOrdnungsrecht (ALKIS)"
#: Projektvariable für die Flächentoleranz (in m²); Default 0
FLAECHEN_TOLERANZ_VARIABLE = "flaechen_toleranz_m2"
# ---------------------------------------------------------------------------
# Interne Hilfsfunktionen
# ---------------------------------------------------------------------------
def _entferne_hilfs_layer(project, layer_name: str) -> None:
"""Entfernt alle Layer mit dem angegebenen Namen aus dem Projekt.
Wird verwendet, um temporäre WFS-Hilfs-Layer nach der Verschneidung
aus dem Projekt zu bereinigen.
Parameters
----------
project :
Aktuelles QGIS-Projekt.
layer_name :
Anzeigename des zu entfernenden Layers.
"""
for lyr in project.mapLayersByName(layer_name):
project.removeMapLayer(lyr.id())
def _select_by_location(flst_layer, vg_layer) -> int:
"""Selektiert Flurstücke, die innerhalb des Verfahrensgebiets liegen.
Verwendet ``processing.run("native:selectbylocation")`` mit Prädikat 6
(within). Flurstücke, die das VG nur berühren, werden ausgeschlossen.
Parameters
----------
flst_layer :
Flurstücks-Layer (Quelle der Selektion).
vg_layer :
Verfahrensgebiet-Layer (Schnittmaske).
Returns
-------
int
Anzahl der selektierten Features; 0 bei Fehler oder leerem Ergebnis.
"""
try:
import processing # type: ignore[import]
processing.run(
"native:selectbylocation",
{
"INPUT": flst_layer,
"PREDICATE": [6], # 6 = within; schließt reines Berühren aus
"INTERSECT": vg_layer,
"METHOD": 0, # 0 = neue Selektion
},
)
return flst_layer.selectedFeatureCount()
except Exception:
return 0
def _fuege_vkz_feld_hinzu(layer, vkz: str) -> None:
"""Fügt das Feld ``verfahrensnummer`` zum enriched Layer hinzu und befüllt
es mit dem Verfahrenskennzeichen, sofern dieses nicht leer ist.
Parameters
----------
layer :
Enriched Verfahrensgebiet-Memory-Layer (in-place geändert).
vkz :
Verfahrenskennzeichen aus der Projektvariable ``verfahrensnummer``.
"""
if not vkz:
return
try:
from sn_basis.functions import QgsField
from sn_basis.functions.qt_wrapper import QVariant
pr = layer.dataProvider()
pr.addAttributes([QgsField(FELD_VKZ, QVariant.String)])
layer.updateFields()
idx = layer.fields().indexFromName(FELD_VKZ)
if idx < 0:
return
changes = {}
for feat in layer.getFeatures():
changes[feat.id()] = {idx: vkz}
pr.changeAttributeValues(changes)
layer.updateFields()
except Exception:
pass
def _lese_flaechen_toleranz() -> float:
"""Liest die Flächentoleranz aus der Projektvariable (Default: 0.0 m²).
Die Projektvariable :data:`FLAECHEN_TOLERANZ_VARIABLE` kann in den
Projekteigenschaften gesetzt werden. Fehlt sie oder ist sie ungültig,
wird 0.0 zurückgegeben (exakter Vergleich).
Returns
-------
float
Toleranz in m² (bei EPSG:25833 identisch mit den Flächen-Einheiten).
"""
try:
raw = get_variable(FLAECHEN_TOLERANZ_VARIABLE)
if raw is not None and str(raw).strip():
return float(str(raw).strip())
except Exception:
pass
return 0.0
def _flaeche_des_layers(layer) -> float:
"""Summiert die Flächen aller Features eines Layers (in CRS-Einheiten²).
Parameters
----------
layer :
Beliebiger Polygon-Layer.
Returns
-------
float
Summe aller Feature-Flächen; 0.0 wenn keine Features vorhanden.
"""
total = 0.0
for feat in layer.getFeatures():
try:
geom = feat.geometry()
if geom and not geom.isEmpty():
total += geom.area()
except Exception:
pass
return total
def _vergleiche_flaechen(
bror_layer, vg_layer, toleranz_m2: float = 0.0
) -> Tuple[float, float, bool]:
"""Vergleicht Flächen von BROR- und Verfahrensgebiet-Layer.
Parameters
----------
bror_layer :
BauRaumOrdnungsrecht-Layer.
vg_layer :
Enriched Verfahrensgebiet-Layer.
toleranz_m2 :
Maximale zulässige Flächendifferenz in m².
Returns
-------
Tuple[float, float, bool]
``(bror_flaeche, vg_flaeche, stimmt_ueberein)``
"""
bror_flaeche = _flaeche_des_layers(bror_layer)
vg_flaeche = _flaeche_des_layers(vg_layer)
stimmt_ueberein = abs(bror_flaeche - vg_flaeche) <= toleranz_m2
return bror_flaeche, vg_flaeche, stimmt_ueberein
# ---------------------------------------------------------------------------
# Öffentliche API
# ---------------------------------------------------------------------------
def verfahrensgebiet_alkis_komplett(
tab_widget,
) -> Tuple[Optional[object], Optional[object], Optional[object], LoadStatus]:
"""Vollständiger VG-Lade-Workflow: ALKIS-VG → Gemarkungen →
Flurstücke → Verschneidung → Flächenvergleich → opt. Umringsprüfung.
Schrittfolge:
1. ``verfahrensgebiet_alkis(tab_widget)`` – BROR-Objekt als Memory-Layer.
2. ``gemarkung_schluessel_und_namen_von_vg_geometrie(geom)`` – Schlüssel.
3. ``flurstueck_alkis(...)`` – Flurstücke laden.
4. ``_select_by_location(...)`` – räumliche Selektion.
5. ``erstelle_verfahrensgebiet_aus_auswahl(...)`` – enriched Layer.
6. ``_vergleiche_flaechen(...)`` – Flächenvergleich als Gate.
7. Ggf. ``umringspruefung(...)`` nach Nutzerbestätigung.
Der BROR-Layer (``BauRaumOrdnungsrecht (ALKIS)``) bleibt bei Erfolg
im Projekt. Bei Abbruch/Fehler wird er entfernt, falls er in diesem
Lauf hinzugefügt wurde.
Parameters
----------
tab_widget :
Eltern-Widget für alle Dialoge.
Returns
-------
Tuple[Optional[enriched_layer], Optional[flst_layer], Optional[bror_layer], LoadStatus]
- ``enriched_layer``: Memory-Layer „Verfahrensgebiet".
- ``flst_layer``: Flurstücke-Layer (bleibt im Projekt).
- ``bror_layer``: BROR-Layer ``BauRaumOrdnungsrecht (ALKIS)``
(bleibt bei Erfolg im Projekt).
- :class:`LoadStatus`: ``FIRST``, ``RELOAD`` oder ``NONE``.
"""
project = QgsProject.instance()
alkis_vg_layer = None
bror_was_added_by_us = False
abgeschlossen = False
# Fortschrittsdialog für den Basisworkflow (7 Schritte)
progress = create_progress_dialog(
7, "Verfahrensgebiet laden", "Initialisierung …"
)
try:
# ------------------------------------------------------------------
# Schritt 1: VG aus ALKIS laden (BROR-Objekt → Memory-Layer)
# ------------------------------------------------------------------
progress.set_label("Schritt 1/6: Verfahrensgebiet aus ALKIS laden …")
progress.set_value(0)
if progress.is_canceled():
return None, None, None, LoadStatus.NONE
alkis_vg_layer, vg_status, _ = verfahrensgebiet_alkis(tab_widget)
if vg_status == LoadStatus.NONE or not alkis_vg_layer:
return None, None, None, LoadStatus.NONE
# BROR-Layer umbenennen, um Namenskonflikt mit enriched VG-Layer zu vermeiden
alkis_vg_layer.setName(BROR_LAYER_NAME)
if vg_status in (LoadStatus.FIRST, LoadStatus.RELOAD):
project.addMapLayer(alkis_vg_layer)
bror_was_added_by_us = True
progress.set_value(1)
# ------------------------------------------------------------------
# Schritt 2: Gemarkungsschlüssel per Geometrie-Spatial-Join ermitteln
# ------------------------------------------------------------------
progress.set_label("Schritt 2/6: Gemarkungsschlüssel ermitteln …")
if progress.is_canceled():
return None, None, None, LoadStatus.NONE
bror_feats = list(alkis_vg_layer.getFeatures())
vg_geom = bror_feats[0].geometry() if bror_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(2)
# ------------------------------------------------------------------
# Schritt 3: Flurstücke laden (mit Nutzer-Dialog, Vorschlag befüllt)
# ------------------------------------------------------------------
progress.set_label("Schritt 3/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(3)
# ------------------------------------------------------------------
# Schritt 4: Räumliche Selektion
# ------------------------------------------------------------------
progress.set_label("Schritt 4/6: Flurstücke räumlich selektieren …")
if progress.is_canceled():
return None, None, None, LoadStatus.NONE
anzahl = _select_by_location(flst_layer, alkis_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(4)
# ------------------------------------------------------------------
# Schritt 5: Enriched Memory-Layer aus Auswahl erzeugen
# ------------------------------------------------------------------
progress.set_label("Schritt 5/6: Verfahrensgebiet-Layer erzeugen …")
if progress.is_canceled():
return None, None, None, LoadStatus.NONE
enriched_layer, erfolgreich, fehlgeschlagen = erstelle_verfahrensgebiet_aus_auswahl(
flst_layer
)
if not enriched_layer or not enriched_layer.isValid():
QMessageBox.critical(
tab_widget,
"Fehler",
"Verfahrensgebiet-Layer konnte nicht erstellt werden.",
)
return None, flst_layer, None, LoadStatus.NONE
vkz = get_variable("verfahrensnummer") or ""
_fuege_vkz_feld_hinzu(enriched_layer, vkz)
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()
bror_flaeche, vg_flaeche, stimmt_ueberein = _vergleiche_flaechen(
alkis_vg_layer, enriched_layer, toleranz
)
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 enriched_layer, flst_layer, alkis_vg_layer, vg_status
# ------------------------------------------------------------------
# Flächenabweichung → Nutzerabfrage für Detailprüfung
# ------------------------------------------------------------------
progress.close()
diff = abs(bror_flaeche - vg_flaeche)
starten = ask_yes_no(
"Abweichung erkannt",
f"Abweichung zwischen BauRaumOrdnungsrecht-Objekt und abgeleitetem "
f"Verfahrensgebiet erkannt. Detailprüfung starten?\n\n"
f"BauRaumOrdnungsrecht: {bror_flaeche:.2f} m²\n"
f"Verfahrensgebiet: {vg_flaeche:.2f} m²\n"
f"Differenz: {diff:.2f} m²",
default=True,
parent=tab_widget,
)
if not starten:
abgeschlossen = True
return enriched_layer, flst_layer, alkis_vg_layer, vg_status
# ------------------------------------------------------------------
# Umringsprüfung (Variante 1: linienbasierter Umringvergleich)
# ------------------------------------------------------------------
umring_progress = create_progress_dialog(
3, "Detailprüfung (Umringe)", "Starte Umringsprüfung …"
)
ergebnis_layer, konflikt_anzahl = umringspruefung(
alkis_vg_layer, flst_layer, progress=umring_progress
)
if umring_progress.is_canceled():
umring_progress.close()
abgeschlossen = True
return enriched_layer, flst_layer, alkis_vg_layer, vg_status
umring_progress.set_value(3)
umring_progress.close()
if ergebnis_layer and ergebnis_layer.isValid():
project.addMapLayer(ergebnis_layer)
show_info_dialog(
"Detailprüfung",
f"Detailprüfung abgeschlossen. "
f"Es wurden {konflikt_anzahl} Konfliktstellen identifiziert.",
parent=tab_widget,
)
abgeschlossen = True
return enriched_layer, flst_layer, alkis_vg_layer, vg_status
finally:
# WFS-Temp-Layer stets aus dem Projekt entfernen
_entferne_hilfs_layer(project, "TempVerfahrensgebiet")
# BROR-Layer bei Fehler/Abbruch entfernen (nur wenn in diesem Lauf hinzugefügt)
if not abgeschlossen and bror_was_added_by_us and alkis_vg_layer is not None:
try:
project.removeMapLayer(alkis_vg_layer.id())
except Exception:
pass