312 lines
10 KiB
Python
312 lines
10 KiB
Python
"""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
|