Umringsprüfung-Segmentbasiert

This commit is contained in:
2026-04-23 22:24:14 +02:00
parent 7948131d5a
commit b25b4c83ac
5 changed files with 1047 additions and 274 deletions
+274 -103
View File
@@ -1,26 +1,34 @@
"""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:
aus ALKIS mit anschließender Flurstücks-Verschneidung, Flächenvergleich und
optionaler Umringsprüfung:
1. Verfahrensgebiet aus ``AX_BauRaumOderBodenordnungsrecht`` per VKZ laden.
2. BBox des VG ableiten.
3. Gemarkungsschlüssel per BBox aus ``AX_Gemarkung`` vorschlagen.
4. Flurstücke per Dialog (mit Vorschlag) laden.
5. Flurstücke räumlich auf das VG einschränken (``selectByLocation``).
6. Aus der Auswahl den enriched Verfahrensgebiet-Memory-Layer erzeugen.
7. ALKIS-VG-Hilfs-Layer entfernen, enriched Layer zurückgeben.
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.
Der enriched Layer enthält die Felder ``flurstueckskennzeichen`` und
``gemarkung_ax_gemarkung_schluessel_gemarkungsnummer`` des
:mod:`~sn_verfahrensgebiet.functions.verfahrensgebiet_edit_service`-Schemas
und ist damit vollständig kompatibel mit ``ergaenze_verfahrensgebiet`` und
``entferne_aus_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. ALKIS-Hilfs-Layer werden
dabei gesäubert, damit das Projekt nicht in einem inkonsistenten Zustand
verbleibt.
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
@@ -29,8 +37,13 @@ from typing import Optional, Tuple
from sn_basis.functions import QgsProject
from sn_basis.functions import QMessageBox
from sn_basis.functions.message_wrapper import info, warning
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,
@@ -42,12 +55,17 @@ from sn_verfahrensgebiet.functions.gemarkung_alkis import (
from sn_verfahrensgebiet.functions.flurstueck_alkis import flurstueck_alkis
from sn_verfahrensgebiet.functions.verfahrensgebiet_edit_service import (
erstelle_verfahrensgebiet_aus_auswahl,
FELD_KENNZEICHEN,
VERF_LAYER_NAME,
)
from sn_verfahrensgebiet.functions.umringspruefung import umringspruefung
# Feld für das Verfahrenskennzeichen (VKZ) im enriched Layer
# 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"
# ---------------------------------------------------------------------------
@@ -57,7 +75,7 @@ FELD_VKZ = "verfahrensnummer"
def _entferne_hilfs_layer(project, layer_name: str) -> None:
"""Entfernt alle Layer mit dem angegebenen Namen aus dem Projekt.
Wird verwendet, um den temporären ALKIS-VG-Layer nach der Verschneidung
Wird verwendet, um temporäre WFS-Hilfs-Layer nach der Verschneidung
aus dem Projekt zu bereinigen.
Parameters
@@ -72,24 +90,22 @@ def _entferne_hilfs_layer(project, layer_name: str) -> None:
def _select_by_location(flst_layer, vg_layer) -> int:
"""Selektiert Flurstücke, die das Verfahrensgebiet überlappen oder darin liegen.
"""Selektiert Flurstücke, die innerhalb des Verfahrensgebiets liegen.
Verwendet ``processing.run("native:selectbylocation")`` mit den Prädikaten
``overlaps`` (5) und ``within`` (6). Flurstücke, die das VG nur berühren
(gemeinsame Grenzlinie oder -punkt, aber kein gemeinsames Inneres), werden
damit ausgeschlossen.
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-WFS-Layer (Quelle der Selektion).
Flurstücks-Layer (Quelle der Selektion).
vg_layer :
Verfahrensgebiet-Memory-Layer (Schnittmaske).
Verfahrensgebiet-Layer (Schnittmaske).
Returns
-------
int
Anzahl der selektierten Features.
Anzahl der selektierten Features; 0 bei Fehler oder leerem Ergebnis.
"""
try:
import processing # type: ignore[import]
@@ -98,15 +114,14 @@ def _select_by_location(flst_layer, vg_layer) -> int:
"native:selectbylocation",
{
"INPUT": flst_layer,
"PREDICATE": [6], # 5 = overlaps, 6 = within; schließt reines Berühren aus
"PREDICATE": [6], # 6 = within; schließt reines Berühren aus
"INTERSECT": vg_layer,
"METHOD": 0, # 0 = neue Selektion
"METHOD": 0, # 0 = neue Selektion
},
)
return flst_layer.selectedFeatureCount()
except Exception:
# Fallback: keine Selektion (z. B. im Unit-Test-Modus)
return 0
@@ -114,9 +129,6 @@ 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.
Das Feld wird nur hinzugefügt, wenn ``vkz`` nicht leer ist, damit der
Layer ohne Projektvariable weiterhin funktioniert.
Parameters
----------
layer :
@@ -149,83 +161,161 @@ def _fuege_vkz_feld_hinzu(layer, vkz: str) -> None:
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], LoadStatus]:
"""Vollständiger VG-Lade-Workflow: ALKIS-VG → BBox → Gemarkungen →
Flurstücke → Verschneidung → enriched Memory-Layer.
) -> 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)`` – Verfahrensgebiet temporär laden
2. ``vg_layer.extent()`` – BBox ableiten
3. ``gemarkung_schluessel_und_namen_by_bbox(extent)`` – Schlüssel vorschlagen
4. ``flurstueck_alkis(tab_widget, vorgabe_schluessel, vorgabe_namen, bbox)`` – Flurstücke laden
5. ``_select_by_location(flst_layer, vg_layer)`` – räumliche Selektion
6. ``erstelle_verfahrensgebiet_aus_auswahl(flst_layer)`` – enriched Layer erzeugen
7. Ggf. VKZ-Feld setzen, ALKIS-VG-Hilfs-Layer entfernen
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 (weitergereicht an Unteraufrufe).
Eltern-Widget für alle Dialoge.
Returns
-------
Tuple[Optional[enriched_layer], Optional[flst_layer], LoadStatus]
- ``enriched_layer``: Memory-Layer „Verfahrensgebiet" mit
``flurstueckskennzeichen``- und ``gemarkung``-Feldern, oder ``None``
bei Abbruch/Fehler.
- ``flst_layer``: Geladener WFS-Flurstück-Layer (bleibt im Projekt),
oder ``None``.
- ``LoadStatus``: Status des VG-Lade-Vorgangs (``FIRST``, ``RELOAD``
oder ``NONE`` bei Abbruch).
Notes
-----
Der temporäre ALKIS-VG-Layer (aus ``AX_BauRaumOderBodenordnungsrecht``)
wird nach der Verschneidung aus dem Projekt entfernt. Im Projekt verbleiben
nur der enriched Memory-Layer und der Flurstücks-WFS-Layer.
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 (Zwischenobjekt)
# Schritt 1: VG aus ALKIS laden (BROR-Objekt → Memory-Layer)
# ------------------------------------------------------------------
info("VG laden", "Schritt 1/6: Verfahrensgebiet aus ALKIS laden …", duration=3)
alkis_vg_layer, vg_status, vg_features = verfahrensgebiet_alkis(tab_widget)
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, LoadStatus.NONE
return None, None, None, LoadStatus.NONE
# BROR-Layer umbenennen, um Namenskonflikt mit enriched VG-Layer zu vermeiden
alkis_vg_layer.setName(BROR_LAYER_NAME)
# Temporär ins Projekt hinzufügen, damit selectByLocation ihn findet
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 (entfernt): BBox-Ableitung war hier – WFS unterstützt kein
# BBox-Filter, daher wird der Extent nicht mehr benötigt.
# 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
# ------------------------------------------------------------------
# Schritt 3: Gemarkungsschlüssel per Geometrie-Spatial-Join ermitteln
# ------------------------------------------------------------------
info("VG laden", "Schritt 2/6: Gemarkungsschlüssel ermitteln \u2026", duration=3)
# VG-Geometrie aus den Roh-WFS-Features bilden (Union, damit
# alle Teilflächen berücksichtigt werden)
from sn_basis.functions import QgsGeometry as _QgsGeometry
if vg_features:
vg_geom = _QgsGeometry.unaryUnion([f.geometry() for f in vg_features])
else:
# Bei KEEP-Status keine WFS-Features vorhanden; Fallback auf Layergeometrie
vg_geom = next(alkis_vg_layer.getFeatures()).geometry()
schluessel_namen = gemarkung_schluessel_und_namen_von_vg_geometrie(vg_geom)
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]
@@ -236,25 +326,34 @@ def verfahrensgebiet_alkis_komplett(
duration=5,
)
progress.set_value(2)
# ------------------------------------------------------------------
# Schritt 4: Flurstücke laden (mit Nutzer-Dialog, Vorschlag befüllt)
# Schritt 3: Flurstücke laden (mit Nutzer-Dialog, Vorschlag befüllt)
# ------------------------------------------------------------------
info("VG laden", "Schritt 3/6: Flurstücke aus ALKIS laden …", duration=3)
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,
# bbox wird nicht übergeben – WFS unterstützt keinen BBox-Filter
ohne_dialog=True, # kein Nutzer-Dialog in der Pipeline
ohne_dialog=True,
)
if flst_status == LoadStatus.NONE or not flst_layer:
return None, None, LoadStatus.NONE
return None, None, None, LoadStatus.NONE
progress.set_value(3)
# ------------------------------------------------------------------
# Schritt 5: Räumliche Selektion
# Schritt 4: Räumliche Selektion
# ------------------------------------------------------------------
info("VG laden", "Schritt 4/6: Flurstücke räumlich selektieren …", duration=3)
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:
@@ -264,12 +363,17 @@ def verfahrensgebiet_alkis_komplett(
"Es wurden keine Flurstücke gefunden, die das Verfahrensgebiet schneiden.\n"
"Bitte Gemarkungsschlüssel und Ausdehnung prüfen.",
)
return None, flst_layer, LoadStatus.NONE
return None, flst_layer, None, LoadStatus.NONE
progress.set_value(4)
# ------------------------------------------------------------------
# Schritt 6: Enriched Memory-Layer aus Auswahl erzeugen
# Schritt 5: Enriched Memory-Layer aus Auswahl erzeugen
# ------------------------------------------------------------------
info("VG laden", "Schritt 5/6: Verfahrensgebiet-Layer erzeugen …", duration=3)
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
)
@@ -280,30 +384,97 @@ def verfahrensgebiet_alkis_komplett(
"Fehler",
"Verfahrensgebiet-Layer konnte nicht erstellt werden.",
)
return None, flst_layer, LoadStatus.NONE
return None, flst_layer, None, LoadStatus.NONE
# ------------------------------------------------------------------
# Schritt 7: VKZ-Feld setzen (optional, wenn Projektvariable vorhanden)
# ------------------------------------------------------------------
vkz = get_variable("verfahrensnummer") or ""
_fuege_vkz_feld_hinzu(enriched_layer, vkz)
info(
"VG laden",
f"Schritt 6/6: Fertig. {erfolgreich} Flurstücke verarbeitet, "
f"{fehlgeschlagen} fehlgeschlagen.",
duration=5,
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
)
return enriched_layer, flst_layer, vg_status
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:
# Hilfs-Layer (ALKIS-VG) stets aus dem Projekt entfernen
if alkis_vg_layer is not None:
_entferne_hilfs_layer(project, VERF_LAYER_NAME)
# Auch nach Layername des ALKIS-Temp-Layers bereinigen
_entferne_hilfs_layer(project, "TempVerfahrensgebiet")
# 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