"""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