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
|