Files
Plugin_SN_Verfahrensgebiet/functions/umringspruefung.py
T

312 lines
10 KiB
Python
Raw Normal View History

2026-04-23 22:24:14 +02:00
"""sn_verfahrensgebiet/functions/umringspruefung.py – Umringsprüfung (Variante 1).
Vergleicht den Umring (Boundary) des BauRaumOderBodenordnungsrecht-Polygons
mit den Grenzen der angrenzenden Flurstücke und hebt Abweichungen hervor.
**Algorithmus (Variante 1 – linienbasierter Umringvergleich):**
1. BROR-Polygon → Umringslinie via :func:`sn_basis.functions.qgiscore_wrapper.polygon_zu_linie`
(QGIS-3/4-kompatibel).
2. Flurstücke, die den BROR-Umring berühren oder schneiden, werden gefiltert.
3. Gefilterte Flurstücke → Umringslinien.
4. Union aller Flurstücksumringe.
5. ``BROR-Umring.difference(Flurstücksumrings-Union)`` → Abweichungen (rot).
6. ``BROR-Umring.difference(rote_Segmente)`` → Übereinstimmungen (grün).
7. Ergebnislayer mit Kategoriesymbologie (grün/rot) und Legendenbeschriftung.
**Rückgabe:** ``(result_layer, conflict_count)``
- ``result_layer``: Temporärer Linienlayer mit Feld ``typ``
(``"identisch"`` = grün, ``"Abweichung"`` = rot).
- ``conflict_count``: Anzahl der roten Konfliktsegmente (Parts).
"""
from __future__ import annotations
from typing import List, Optional, Tuple
from sn_basis.functions.qgiscore_wrapper import (
QgsVectorLayer,
QgsFeature,
QgsField,
QgsGeometry,
polygon_zu_linie,
)
from sn_basis.functions.qt_wrapper import QVariant
# ---------------------------------------------------------------------------
# Konstanten
# ---------------------------------------------------------------------------
#: Geometrische Toleranz für den Linienvergleich (EPSG:25833 = Meter).
#: Der BROR-Umring wird gegen die um diesen Betrag gepufferten Flurstücksumringe
#: verglichen, um numerische Rundungsfehler aus ALKIS-Koordinaten abzufangen.
LINIE_TOLERANZ_M: float = 0.0001
ERGEBNIS_LAYER_NAME = "Umringsprüfung"
FELD_TYP = "typ"
WERT_IDENTISCH = "identisch"
WERT_ABWEICHUNG = "Abweichung"
# ---------------------------------------------------------------------------
# Interne Hilfsfunktionen
# ---------------------------------------------------------------------------
def _zerlege_in_teile(geom: object) -> List[object]:
"""Zerlegt eine (Multi-)LineString-Geometrie in einzelne Teile.
Parameters
----------
geom :
``QgsGeometry`` vom Typ LineString oder MultiLineString.
Returns
-------
List[QgsGeometry]
Liste einzelner Geometrieteile; leer wenn ``geom`` None oder leer ist.
"""
if geom is None:
return []
try:
if getattr(geom, "isEmpty", lambda: True)():
return []
# asGeometryCollection() gibt eigene Kopien zurück – kein Ownership-Problem.
# QgsGeometry(part) über geom.parts() ist in QGIS 4.x aus den Python-Bindings
# entfernt worden und führt zu Absturz beim Rendern.
parts = geom.asGeometryCollection()
if parts:
return [p for p in parts if p is not None and not p.isEmpty()]
except Exception:
pass
# Fallback: Geometrie nicht zerlegbar → als Ganzes zurückgeben
try:
if not geom.isEmpty():
return [geom]
except Exception:
pass
return []
def _erstelle_ergebnis_layer(crs_authid: str) -> object:
"""Erzeugt den leeren Ergebnislayer mit Feld ``typ``.
Parameters
----------
crs_authid :
CRS-Kennung des Quell-Layers (z. B. ``"EPSG:25833"``).
Returns
-------
QgsVectorLayer
Leerer Memory-Layer vom Typ LineString.
"""
layer = QgsVectorLayer(
f"LineString?crs={crs_authid}", ERGEBNIS_LAYER_NAME, "memory"
)
pr = layer.dataProvider()
pr.addAttributes([QgsField(FELD_TYP, QVariant.String)])
layer.updateFields()
return layer
def _setze_kategorien_symbologie(layer) -> None:
"""Wendet Kategoriesymbologie an: grün = identisch, rot = Abweichung.
Wird bei fehlendem QGIS-Kontext still ignoriert (Unit-Test-Robustheit).
"""
try:
from qgis.core import ( # type: ignore[import]
QgsCategorizedSymbolRenderer,
QgsRendererCategory,
QgsLineSymbol,
)
green_sym = QgsLineSymbol.createSimple(
{"color": "0,180,0,255", "line_width": "0.5"}
)
red_sym = QgsLineSymbol.createSimple(
{"color": "220,0,0,255", "line_width": "0.8"}
)
categories = [
QgsRendererCategory(
WERT_IDENTISCH,
green_sym,
"identisch (übereinstimmend)",
),
QgsRendererCategory(
WERT_ABWEICHUNG,
red_sym,
"Abweichung (nicht in Flurstücken)",
),
]
renderer = QgsCategorizedSymbolRenderer(FELD_TYP, categories)
layer.setRenderer(renderer)
except Exception:
pass # Kein QGIS-Kontext (Tests) → Standarddarstellung behalten
# ---------------------------------------------------------------------------
# Öffentliche API
# ---------------------------------------------------------------------------
def umringspruefung(
bror_layer,
flst_layer,
progress=None,
linie_toleranz_m: float = LINIE_TOLERANZ_M,
) -> Tuple[Optional[object], int]:
"""Variante 1: Linienbasierter Umringvergleich.
Ermittelt, welche Teile des BROR-Umrings mit den Flurstücksgrenzen
übereinstimmen (grün) und welche keine Entsprechung haben (rot).
Parameters
----------
bror_layer :
BauRaumOderBodenordnungsrecht-Memory-Layer (Polygon).
flst_layer :
Flurstücke-Layer (Polygon, WFS oder Memory).
progress :
Optionaler ``ProgressDialog`` aus
:mod:`sn_basis.functions.dialog_wrapper` für Fortschrittsanzeige
und Abbrucherkennung.
linie_toleranz_m : float
Geometrische Puffertoleranz für den Linienvergleich in
CRS-Einheiten (Standardwert :data:`LINIE_TOLERANZ_M` = 0.001 m).
Returns
-------
Tuple[Optional[QgsVectorLayer], int]
``(result_layer, conflict_count)``
- ``result_layer``: Linienlayer mit grünen/roten Segmenten,
oder ``None`` bei Abbruch oder fehlgeschlagener Verarbeitung.
- ``conflict_count``: Anzahl der roten Konfliktsegmente (BROR-Teile
ohne Entsprechung in den Flurstücksgrenzen).
"""
def _canceled() -> bool:
return progress is not None and progress.is_canceled()
def _schritt(label: str, wert: int) -> None:
if progress is not None:
progress.set_label(label)
progress.set_value(wert)
# ------------------------------------------------------------------
# 1. BROR-Geometrie und Umringslinie ermitteln
# ------------------------------------------------------------------
_schritt("Umringsprüfung: BROR-Umring ermitteln …", 0)
bror_feats = [
f for f in bror_layer.getFeatures()
if f.geometry() and not f.geometry().isEmpty()
]
if not bror_feats:
return None, 0
bror_union = QgsGeometry.unaryUnion([f.geometry() for f in bror_feats])
if bror_union is None or bror_union.isEmpty():
return None, 0
bror_boundary = polygon_zu_linie(bror_union)
if bror_boundary is None or bror_boundary.isEmpty():
return None, 0
crs_authid = bror_layer.crs().authid()
if _canceled():
return None, 0
# ------------------------------------------------------------------
# 2. Relevante Flurstücke: nur jene, deren Geometrie den BROR-Umring
# berührt oder schneidet
# ------------------------------------------------------------------
_schritt("Umringsprüfung: Relevante Flurstücke filtern …", 1)
relevante_boundaries: List[object] = []
for feat in flst_layer.getFeatures():
if _canceled():
return None, 0
geom = feat.geometry()
if geom is None or geom.isEmpty():
continue
try:
if geom.intersects(bror_boundary):
boundary = polygon_zu_linie(geom)
if boundary is not None and not boundary.isEmpty():
relevante_boundaries.append(boundary)
except Exception:
continue
if relevante_boundaries:
flst_boundary_union = QgsGeometry.unaryUnion(relevante_boundaries)
else:
flst_boundary_union = None
if _canceled():
return None, 0
# ------------------------------------------------------------------
# 3. Segmentvergleich: Differenz und Übereinstimmung berechnen
# ------------------------------------------------------------------
_schritt("Umringsprüfung: Segmentvergleich …", 2)
ergebnis_layer = _erstelle_ergebnis_layer(crs_authid)
pr = ergebnis_layer.dataProvider()
feats_to_add: List[object] = []
conflict_count = 0
if flst_boundary_union is None or flst_boundary_union.isEmpty():
# Kein Flurstück berührt den BROR-Umring → alles ist Abweichung
for part in _zerlege_in_teile(bror_boundary):
feat = QgsFeature()
feat.setGeometry(part)
feat.setAttributes([WERT_ABWEICHUNG])
feats_to_add.append(feat)
conflict_count += 1
else:
# Flurstücksumringe leicht puffern für Toleranz-Matching
if linie_toleranz_m > 0:
flst_fuer_diff = flst_boundary_union.buffer(linie_toleranz_m, 4)
else:
flst_fuer_diff = flst_boundary_union
# Rote Segmente: BROR-Teile ohne Entsprechung in Flurstücksgrenzen
try:
red_geom = bror_boundary.difference(flst_fuer_diff)
except Exception:
red_geom = None
# Grüne Segmente: BROR-Teile, die nicht rot sind
try:
if red_geom and not red_geom.isEmpty():
green_geom = bror_boundary.difference(red_geom)
else:
green_geom = bror_boundary
except Exception:
green_geom = None
for part in _zerlege_in_teile(green_geom):
feat = QgsFeature()
feat.setGeometry(part)
feat.setAttributes([WERT_IDENTISCH])
feats_to_add.append(feat)
for part in _zerlege_in_teile(red_geom):
feat = QgsFeature()
feat.setGeometry(part)
feat.setAttributes([WERT_ABWEICHUNG])
feats_to_add.append(feat)
conflict_count += 1
if _canceled():
return None, 0
pr.addFeatures(feats_to_add)
ergebnis_layer.updateExtents()
_setze_kategorien_symbologie(ergebnis_layer)
_schritt("Umringsprüfung abgeschlossen.", 3)
return ergebnis_layer, conflict_count