Umringsprüfung-Segmentbasiert
This commit is contained in:
@@ -0,0 +1,311 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user