From b25b4c83ac8fc921055e8896c83b81e5a586b39f Mon Sep 17 00:00:00 2001 From: daniel Date: Thu, 23 Apr 2026 22:24:14 +0200 Subject: [PATCH] =?UTF-8?q?Umringspr=C3=BCfung-Segmentbasiert?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- functions/umringspruefung.py | 311 ++++++++++ functions/verfahrensgebiet_alkis_komplett.py | 377 ++++++++---- tests/test_verfahrensgebiet_alkis_komplett.py | 574 +++++++++++++----- tests/test_working_tab.py | 12 +- ui/tabs/working_tab.py | 47 +- 5 files changed, 1047 insertions(+), 274 deletions(-) create mode 100644 functions/umringspruefung.py diff --git a/functions/umringspruefung.py b/functions/umringspruefung.py new file mode 100644 index 0000000..e498909 --- /dev/null +++ b/functions/umringspruefung.py @@ -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 diff --git a/functions/verfahrensgebiet_alkis_komplett.py b/functions/verfahrensgebiet_alkis_komplett.py index 23e62d1..25ee5ea 100644 --- a/functions/verfahrensgebiet_alkis_komplett.py +++ b/functions/verfahrensgebiet_alkis_komplett.py @@ -1,26 +1,34 @@ """sn_verfahrensgebiet/functions/verfahrensgebiet_alkis_komplett.py – Vollständiger VG-Lade-Workflow. Orchestriert den vollständigen Arbeitsablauf zum Laden eines Verfahrensgebiets -aus ALKIS mit anschließender Flurstücks-Verschneidung: +aus ALKIS mit anschließender Flurstücks-Verschneidung, Flächenvergleich und +optionaler Umringsprüfung: 1. Verfahrensgebiet aus ``AX_BauRaumOderBodenordnungsrecht`` per VKZ laden. -2. BBox des VG ableiten. -3. Gemarkungsschlüssel per BBox aus ``AX_Gemarkung`` vorschlagen. -4. Flurstücke per Dialog (mit Vorschlag) laden. -5. Flurstücke räumlich auf das VG einschränken (``selectByLocation``). -6. Aus der Auswahl den enriched Verfahrensgebiet-Memory-Layer erzeugen. -7. ALKIS-VG-Hilfs-Layer entfernen, enriched Layer zurückgeben. +2. Gemarkungsschlüssel per Geometrie aus ``AX_Gemarkung`` vorschlagen. +3. Flurstücke per Dialog (mit Vorschlag) laden. +4. Flurstücke räumlich auf das VG einschränken (``selectByLocation``). +5. Aus der Auswahl den enriched Verfahrensgebiet-Memory-Layer erzeugen. +6. Flächenvergleich zwischen BROR-Objekt und abgeleitetem Verfahrensgebiet. -Der enriched Layer enthält die Felder ``flurstueckskennzeichen`` und -``gemarkung_ax_gemarkung_schluessel_gemarkungsnummer`` des -:mod:`~sn_verfahrensgebiet.functions.verfahrensgebiet_edit_service`-Schemas -und ist damit vollständig kompatibel mit ``ergaenze_verfahrensgebiet`` und -``entferne_aus_verfahrensgebiet``. + - **Übereinstimmung** (≤ Toleranz): modale OK-Meldung, Ablauf beendet. + - **Abweichung**: Ja/Nein-Abfrage für Detailprüfung. + +7. Optionale Umringsprüfung (Variante 1): + + - BROR-Umring gegen Flurstücksgrenzen vergleichen. + - Ergebnislayer (grün/rot) ins Projekt einfügen. + - Abschlussmeldung mit Konfliktanzahl. + +Das BROR-Objekt (``AX_BauRaumOderBodenordnungsrecht``) bleibt bei Erfolg als +temporärer Layer ``BauRaumOrdnungsrecht (ALKIS)`` im Projekt erhalten. + +**Rückgabe:** +``Tuple[enriched_layer, flst_layer, bror_layer, LoadStatus]`` **Abbruchverhalten:** Jeder Schritt, der :attr:`LoadStatus.NONE` zurückgibt -oder kein Ergebnis liefert, bricht den Workflow ab. ALKIS-Hilfs-Layer werden -dabei gesäubert, damit das Projekt nicht in einem inkonsistenten Zustand -verbleibt. +oder kein Ergebnis liefert, bricht den Workflow ab. Der BROR-Layer wird nur +bei Fehler/Abbruch entfernt (wenn er in diesem Lauf hinzugefügt wurde). """ from __future__ import annotations @@ -29,8 +37,13 @@ from typing import Optional, Tuple from sn_basis.functions import QgsProject from sn_basis.functions import QMessageBox -from sn_basis.functions.message_wrapper import info, warning +from sn_basis.functions.message_wrapper import warning from sn_basis.functions.variable_wrapper import get_variable +from sn_basis.functions.dialog_wrapper import ( + create_progress_dialog, + show_info_dialog, + ask_yes_no, +) from sn_verfahrensgebiet.functions.verfahrensgebiet_alkis import ( verfahrensgebiet_alkis, @@ -42,12 +55,17 @@ from sn_verfahrensgebiet.functions.gemarkung_alkis import ( from sn_verfahrensgebiet.functions.flurstueck_alkis import flurstueck_alkis from sn_verfahrensgebiet.functions.verfahrensgebiet_edit_service import ( erstelle_verfahrensgebiet_aus_auswahl, - FELD_KENNZEICHEN, VERF_LAYER_NAME, ) +from sn_verfahrensgebiet.functions.umringspruefung import umringspruefung -# Feld für das Verfahrenskennzeichen (VKZ) im enriched Layer +# Feldname für das Verfahrenskennzeichen im enriched Layer FELD_VKZ = "verfahrensnummer" +#: Anzeigename des BROR-Layers im Projekt +BROR_LAYER_NAME = "BauRaumOrdnungsrecht (ALKIS)" +#: Projektvariable für die Flächentoleranz (in m²); Default 0 +FLAECHEN_TOLERANZ_VARIABLE = "flaechen_toleranz_m2" + # --------------------------------------------------------------------------- @@ -57,7 +75,7 @@ FELD_VKZ = "verfahrensnummer" def _entferne_hilfs_layer(project, layer_name: str) -> None: """Entfernt alle Layer mit dem angegebenen Namen aus dem Projekt. - Wird verwendet, um den temporären ALKIS-VG-Layer nach der Verschneidung + Wird verwendet, um temporäre WFS-Hilfs-Layer nach der Verschneidung aus dem Projekt zu bereinigen. Parameters @@ -72,24 +90,22 @@ def _entferne_hilfs_layer(project, layer_name: str) -> None: def _select_by_location(flst_layer, vg_layer) -> int: - """Selektiert Flurstücke, die das Verfahrensgebiet überlappen oder darin liegen. + """Selektiert Flurstücke, die innerhalb des Verfahrensgebiets liegen. - Verwendet ``processing.run("native:selectbylocation")`` mit den Prädikaten - ``overlaps`` (5) und ``within`` (6). Flurstücke, die das VG nur berühren - (gemeinsame Grenzlinie oder -punkt, aber kein gemeinsames Inneres), werden - damit ausgeschlossen. + Verwendet ``processing.run("native:selectbylocation")`` mit Prädikat 6 + (within). Flurstücke, die das VG nur berühren, werden ausgeschlossen. Parameters ---------- flst_layer : - Flurstücks-WFS-Layer (Quelle der Selektion). + Flurstücks-Layer (Quelle der Selektion). vg_layer : - Verfahrensgebiet-Memory-Layer (Schnittmaske). + Verfahrensgebiet-Layer (Schnittmaske). Returns ------- int - Anzahl der selektierten Features. + Anzahl der selektierten Features; 0 bei Fehler oder leerem Ergebnis. """ try: import processing # type: ignore[import] @@ -98,15 +114,14 @@ def _select_by_location(flst_layer, vg_layer) -> int: "native:selectbylocation", { "INPUT": flst_layer, - "PREDICATE": [6], # 5 = overlaps, 6 = within; schließt reines Berühren aus + "PREDICATE": [6], # 6 = within; schließt reines Berühren aus "INTERSECT": vg_layer, - "METHOD": 0, # 0 = neue Selektion + "METHOD": 0, # 0 = neue Selektion }, ) return flst_layer.selectedFeatureCount() except Exception: - # Fallback: keine Selektion (z. B. im Unit-Test-Modus) return 0 @@ -114,9 +129,6 @@ def _fuege_vkz_feld_hinzu(layer, vkz: str) -> None: """Fügt das Feld ``verfahrensnummer`` zum enriched Layer hinzu und befüllt es mit dem Verfahrenskennzeichen, sofern dieses nicht leer ist. - Das Feld wird nur hinzugefügt, wenn ``vkz`` nicht leer ist, damit der - Layer ohne Projektvariable weiterhin funktioniert. - Parameters ---------- layer : @@ -149,83 +161,161 @@ def _fuege_vkz_feld_hinzu(layer, vkz: str) -> None: pass +def _lese_flaechen_toleranz() -> float: + """Liest die Flächentoleranz aus der Projektvariable (Default: 0.0 m²). + + Die Projektvariable :data:`FLAECHEN_TOLERANZ_VARIABLE` kann in den + Projekteigenschaften gesetzt werden. Fehlt sie oder ist sie ungültig, + wird 0.0 zurückgegeben (exakter Vergleich). + + Returns + ------- + float + Toleranz in m² (bei EPSG:25833 identisch mit den Flächen-Einheiten). + """ + try: + raw = get_variable(FLAECHEN_TOLERANZ_VARIABLE) + if raw is not None and str(raw).strip(): + return float(str(raw).strip()) + except Exception: + pass + return 0.0 + + +def _flaeche_des_layers(layer) -> float: + """Summiert die Flächen aller Features eines Layers (in CRS-Einheiten²). + + Parameters + ---------- + layer : + Beliebiger Polygon-Layer. + + Returns + ------- + float + Summe aller Feature-Flächen; 0.0 wenn keine Features vorhanden. + """ + total = 0.0 + for feat in layer.getFeatures(): + try: + geom = feat.geometry() + if geom and not geom.isEmpty(): + total += geom.area() + except Exception: + pass + return total + + +def _vergleiche_flaechen( + bror_layer, vg_layer, toleranz_m2: float = 0.0 +) -> Tuple[float, float, bool]: + """Vergleicht Flächen von BROR- und Verfahrensgebiet-Layer. + + Parameters + ---------- + bror_layer : + BauRaumOrdnungsrecht-Layer. + vg_layer : + Enriched Verfahrensgebiet-Layer. + toleranz_m2 : + Maximale zulässige Flächendifferenz in m². + + Returns + ------- + Tuple[float, float, bool] + ``(bror_flaeche, vg_flaeche, stimmt_ueberein)`` + """ + bror_flaeche = _flaeche_des_layers(bror_layer) + vg_flaeche = _flaeche_des_layers(vg_layer) + stimmt_ueberein = abs(bror_flaeche - vg_flaeche) <= toleranz_m2 + return bror_flaeche, vg_flaeche, stimmt_ueberein + + # --------------------------------------------------------------------------- # Öffentliche API # --------------------------------------------------------------------------- def verfahrensgebiet_alkis_komplett( tab_widget, -) -> Tuple[Optional[object], Optional[object], LoadStatus]: - """Vollständiger VG-Lade-Workflow: ALKIS-VG → BBox → Gemarkungen → - Flurstücke → Verschneidung → enriched Memory-Layer. +) -> Tuple[Optional[object], Optional[object], Optional[object], LoadStatus]: + """Vollständiger VG-Lade-Workflow: ALKIS-VG → Gemarkungen → + Flurstücke → Verschneidung → Flächenvergleich → opt. Umringsprüfung. Schrittfolge: - 1. ``verfahrensgebiet_alkis(tab_widget)`` – Verfahrensgebiet temporär laden - 2. ``vg_layer.extent()`` – BBox ableiten - 3. ``gemarkung_schluessel_und_namen_by_bbox(extent)`` – Schlüssel vorschlagen - 4. ``flurstueck_alkis(tab_widget, vorgabe_schluessel, vorgabe_namen, bbox)`` – Flurstücke laden - 5. ``_select_by_location(flst_layer, vg_layer)`` – räumliche Selektion - 6. ``erstelle_verfahrensgebiet_aus_auswahl(flst_layer)`` – enriched Layer erzeugen - 7. Ggf. VKZ-Feld setzen, ALKIS-VG-Hilfs-Layer entfernen + 1. ``verfahrensgebiet_alkis(tab_widget)`` – BROR-Objekt als Memory-Layer. + 2. ``gemarkung_schluessel_und_namen_von_vg_geometrie(geom)`` – Schlüssel. + 3. ``flurstueck_alkis(...)`` – Flurstücke laden. + 4. ``_select_by_location(...)`` – räumliche Selektion. + 5. ``erstelle_verfahrensgebiet_aus_auswahl(...)`` – enriched Layer. + 6. ``_vergleiche_flaechen(...)`` – Flächenvergleich als Gate. + 7. Ggf. ``umringspruefung(...)`` nach Nutzerbestätigung. + + Der BROR-Layer (``BauRaumOrdnungsrecht (ALKIS)``) bleibt bei Erfolg + im Projekt. Bei Abbruch/Fehler wird er entfernt, falls er in diesem + Lauf hinzugefügt wurde. Parameters ---------- tab_widget : - Eltern-Widget für alle Dialoge (weitergereicht an Unteraufrufe). + Eltern-Widget für alle Dialoge. Returns ------- - Tuple[Optional[enriched_layer], Optional[flst_layer], LoadStatus] - - ``enriched_layer``: Memory-Layer „Verfahrensgebiet" mit - ``flurstueckskennzeichen``- und ``gemarkung``-Feldern, oder ``None`` - bei Abbruch/Fehler. - - ``flst_layer``: Geladener WFS-Flurstück-Layer (bleibt im Projekt), - oder ``None``. - - ``LoadStatus``: Status des VG-Lade-Vorgangs (``FIRST``, ``RELOAD`` - oder ``NONE`` bei Abbruch). - - Notes - ----- - Der temporäre ALKIS-VG-Layer (aus ``AX_BauRaumOderBodenordnungsrecht``) - wird nach der Verschneidung aus dem Projekt entfernt. Im Projekt verbleiben - nur der enriched Memory-Layer und der Flurstücks-WFS-Layer. + Tuple[Optional[enriched_layer], Optional[flst_layer], Optional[bror_layer], LoadStatus] + - ``enriched_layer``: Memory-Layer „Verfahrensgebiet". + - ``flst_layer``: Flurstücke-Layer (bleibt im Projekt). + - ``bror_layer``: BROR-Layer ``BauRaumOrdnungsrecht (ALKIS)`` + (bleibt bei Erfolg im Projekt). + - :class:`LoadStatus`: ``FIRST``, ``RELOAD`` oder ``NONE``. """ project = QgsProject.instance() alkis_vg_layer = None + bror_was_added_by_us = False + abgeschlossen = False + + # Fortschrittsdialog für den Basisworkflow (7 Schritte) + progress = create_progress_dialog( + 7, "Verfahrensgebiet laden", "Initialisierung …" + ) try: # ------------------------------------------------------------------ - # Schritt 1: VG aus ALKIS laden (Zwischenobjekt) + # Schritt 1: VG aus ALKIS laden (BROR-Objekt → Memory-Layer) # ------------------------------------------------------------------ - info("VG laden", "Schritt 1/6: Verfahrensgebiet aus ALKIS laden …", duration=3) - alkis_vg_layer, vg_status, vg_features = verfahrensgebiet_alkis(tab_widget) + progress.set_label("Schritt 1/6: Verfahrensgebiet aus ALKIS laden …") + progress.set_value(0) + if progress.is_canceled(): + return None, None, None, LoadStatus.NONE + + alkis_vg_layer, vg_status, _ = verfahrensgebiet_alkis(tab_widget) if vg_status == LoadStatus.NONE or not alkis_vg_layer: - return None, None, LoadStatus.NONE + return None, None, None, LoadStatus.NONE + + # BROR-Layer umbenennen, um Namenskonflikt mit enriched VG-Layer zu vermeiden + alkis_vg_layer.setName(BROR_LAYER_NAME) - # Temporär ins Projekt hinzufügen, damit selectByLocation ihn findet if vg_status in (LoadStatus.FIRST, LoadStatus.RELOAD): project.addMapLayer(alkis_vg_layer) + bror_was_added_by_us = True + + progress.set_value(1) # ------------------------------------------------------------------ - # Schritt 2 (entfernt): BBox-Ableitung war hier – WFS unterstützt kein - # BBox-Filter, daher wird der Extent nicht mehr benötigt. + # Schritt 2: Gemarkungsschlüssel per Geometrie-Spatial-Join ermitteln # ------------------------------------------------------------------ + progress.set_label("Schritt 2/6: Gemarkungsschlüssel ermitteln …") + if progress.is_canceled(): + return None, None, None, LoadStatus.NONE - # ------------------------------------------------------------------ - # Schritt 3: Gemarkungsschlüssel per Geometrie-Spatial-Join ermitteln - # ------------------------------------------------------------------ - info("VG laden", "Schritt 2/6: Gemarkungsschlüssel ermitteln \u2026", duration=3) - # VG-Geometrie aus den Roh-WFS-Features bilden (Union, damit - # alle Teilflächen berücksichtigt werden) - from sn_basis.functions import QgsGeometry as _QgsGeometry - if vg_features: - vg_geom = _QgsGeometry.unaryUnion([f.geometry() for f in vg_features]) - else: - # Bei KEEP-Status keine WFS-Features vorhanden; Fallback auf Layergeometrie - vg_geom = next(alkis_vg_layer.getFeatures()).geometry() - schluessel_namen = gemarkung_schluessel_und_namen_von_vg_geometrie(vg_geom) + bror_feats = list(alkis_vg_layer.getFeatures()) + vg_geom = bror_feats[0].geometry() if bror_feats else None + + schluessel_namen = ( + gemarkung_schluessel_und_namen_von_vg_geometrie(vg_geom) + if vg_geom else [] + ) vorgabe_schluessel = [nr for nr, _ in schluessel_namen] vorgabe_namen = [nm for _, nm in schluessel_namen if nm] @@ -236,25 +326,34 @@ def verfahrensgebiet_alkis_komplett( duration=5, ) + progress.set_value(2) + # ------------------------------------------------------------------ - # Schritt 4: Flurstücke laden (mit Nutzer-Dialog, Vorschlag befüllt) + # Schritt 3: Flurstücke laden (mit Nutzer-Dialog, Vorschlag befüllt) # ------------------------------------------------------------------ - info("VG laden", "Schritt 3/6: Flurstücke aus ALKIS laden …", duration=3) + progress.set_label("Schritt 3/6: Flurstücke aus ALKIS laden …") + if progress.is_canceled(): + return None, None, None, LoadStatus.NONE + flst_layer, flst_status = flurstueck_alkis( tab_widget, vorgabe_schluessel=vorgabe_schluessel, vorgabe_namen=vorgabe_namen if vorgabe_namen else None, - # bbox wird nicht übergeben – WFS unterstützt keinen BBox-Filter - ohne_dialog=True, # kein Nutzer-Dialog in der Pipeline + ohne_dialog=True, ) if flst_status == LoadStatus.NONE or not flst_layer: - return None, None, LoadStatus.NONE + return None, None, None, LoadStatus.NONE + + progress.set_value(3) # ------------------------------------------------------------------ - # Schritt 5: Räumliche Selektion + # Schritt 4: Räumliche Selektion # ------------------------------------------------------------------ - info("VG laden", "Schritt 4/6: Flurstücke räumlich selektieren …", duration=3) + progress.set_label("Schritt 4/6: Flurstücke räumlich selektieren …") + if progress.is_canceled(): + return None, None, None, LoadStatus.NONE + anzahl = _select_by_location(flst_layer, alkis_vg_layer) if anzahl == 0: @@ -264,12 +363,17 @@ def verfahrensgebiet_alkis_komplett( "Es wurden keine Flurstücke gefunden, die das Verfahrensgebiet schneiden.\n" "Bitte Gemarkungsschlüssel und Ausdehnung prüfen.", ) - return None, flst_layer, LoadStatus.NONE + return None, flst_layer, None, LoadStatus.NONE + + progress.set_value(4) # ------------------------------------------------------------------ - # Schritt 6: Enriched Memory-Layer aus Auswahl erzeugen + # Schritt 5: Enriched Memory-Layer aus Auswahl erzeugen # ------------------------------------------------------------------ - info("VG laden", "Schritt 5/6: Verfahrensgebiet-Layer erzeugen …", duration=3) + progress.set_label("Schritt 5/6: Verfahrensgebiet-Layer erzeugen …") + if progress.is_canceled(): + return None, None, None, LoadStatus.NONE + enriched_layer, erfolgreich, fehlgeschlagen = erstelle_verfahrensgebiet_aus_auswahl( flst_layer ) @@ -280,30 +384,97 @@ def verfahrensgebiet_alkis_komplett( "Fehler", "Verfahrensgebiet-Layer konnte nicht erstellt werden.", ) - return None, flst_layer, LoadStatus.NONE + return None, flst_layer, None, LoadStatus.NONE - # ------------------------------------------------------------------ - # Schritt 7: VKZ-Feld setzen (optional, wenn Projektvariable vorhanden) - # ------------------------------------------------------------------ vkz = get_variable("verfahrensnummer") or "" _fuege_vkz_feld_hinzu(enriched_layer, vkz) - info( - "VG laden", - f"Schritt 6/6: Fertig. {erfolgreich} Flurstücke verarbeitet, " - f"{fehlgeschlagen} fehlgeschlagen.", - duration=5, + progress.set_value(5) + + # ------------------------------------------------------------------ + # Schritt 6: Flächenvergleich als Gate + # ------------------------------------------------------------------ + progress.set_label("Schritt 6/6: Flächenvergleich …") + if progress.is_canceled(): + return None, None, None, LoadStatus.NONE + + toleranz = _lese_flaechen_toleranz() + bror_flaeche, vg_flaeche, stimmt_ueberein = _vergleiche_flaechen( + alkis_vg_layer, enriched_layer, toleranz ) - return enriched_layer, flst_layer, vg_status + progress.set_value(6) + + if stimmt_ueberein: + progress.set_value(7) + progress.close() + show_info_dialog( + "Flächenvergleich", + "Verfahrensgebiet mit erwarteter Geometrie erzeugt", + parent=tab_widget, + ) + abgeschlossen = True + return enriched_layer, flst_layer, alkis_vg_layer, vg_status + + # ------------------------------------------------------------------ + # Flächenabweichung → Nutzerabfrage für Detailprüfung + # ------------------------------------------------------------------ + progress.close() + diff = abs(bror_flaeche - vg_flaeche) + starten = ask_yes_no( + "Abweichung erkannt", + f"Abweichung zwischen BauRaumOrdnungsrecht-Objekt und abgeleitetem " + f"Verfahrensgebiet erkannt. Detailprüfung starten?\n\n" + f"BauRaumOrdnungsrecht: {bror_flaeche:.2f} m²\n" + f"Verfahrensgebiet: {vg_flaeche:.2f} m²\n" + f"Differenz: {diff:.2f} m²", + default=True, + parent=tab_widget, + ) + + if not starten: + abgeschlossen = True + return enriched_layer, flst_layer, alkis_vg_layer, vg_status + + # ------------------------------------------------------------------ + # Umringsprüfung (Variante 1: linienbasierter Umringvergleich) + # ------------------------------------------------------------------ + umring_progress = create_progress_dialog( + 3, "Detailprüfung (Umringe)", "Starte Umringsprüfung …" + ) + + ergebnis_layer, konflikt_anzahl = umringspruefung( + alkis_vg_layer, flst_layer, progress=umring_progress + ) + + if umring_progress.is_canceled(): + umring_progress.close() + abgeschlossen = True + return enriched_layer, flst_layer, alkis_vg_layer, vg_status + + umring_progress.set_value(3) + umring_progress.close() + + if ergebnis_layer and ergebnis_layer.isValid(): + project.addMapLayer(ergebnis_layer) + + show_info_dialog( + "Detailprüfung", + f"Detailprüfung abgeschlossen. " + f"Es wurden {konflikt_anzahl} Konfliktstellen identifiziert.", + parent=tab_widget, + ) + + abgeschlossen = True + return enriched_layer, flst_layer, alkis_vg_layer, vg_status finally: - # Hilfs-Layer (ALKIS-VG) stets aus dem Projekt entfernen - if alkis_vg_layer is not None: - _entferne_hilfs_layer(project, VERF_LAYER_NAME) - # Auch nach Layername des ALKIS-Temp-Layers bereinigen - _entferne_hilfs_layer(project, "TempVerfahrensgebiet") + # WFS-Temp-Layer stets aus dem Projekt entfernen + _entferne_hilfs_layer(project, "TempVerfahrensgebiet") + # BROR-Layer bei Fehler/Abbruch entfernen (nur wenn in diesem Lauf hinzugefügt) + if not abgeschlossen and bror_was_added_by_us and alkis_vg_layer is not None: try: project.removeMapLayer(alkis_vg_layer.id()) except Exception: pass + diff --git a/tests/test_verfahrensgebiet_alkis_komplett.py b/tests/test_verfahrensgebiet_alkis_komplett.py index 2e2100a..e19d134 100644 --- a/tests/test_verfahrensgebiet_alkis_komplett.py +++ b/tests/test_verfahrensgebiet_alkis_komplett.py @@ -4,27 +4,39 @@ Unit-Tests für :mod:`sn_verfahrensgebiet.functions.verfahrensgebiet_alkis_kompl Getestete Szenarien: -Erfolgsfall: -- Vollständiger Workflow → enriched Layer + flst_layer + LoadStatus.FIRST +Erfolgsfall (Flächenübereinstimmung): +- Vollständiger Workflow → enriched Layer + flst_layer + bror_layer + LoadStatus.FIRST - VKZ aus Projektvariable wird als Feld gesetzt (wenn vorhanden) - Kein VKZ in Projektvariable → Feld wird nicht gesetzt (kein Fehler) Abbruchpfade: -- verfahrensgebiet_alkis gibt LoadStatus.NONE → (None, None, NONE) -- flurstueck_alkis gibt LoadStatus.NONE → (None, None, NONE) -- Keine Flurstücke selektiert → (None, flst_layer, NONE) + Warnung -- erstelle_verfahrensgebiet_aus_auswahl liefert ungültigen Layer → (None, flst_layer, NONE) -- Cleanup: ALKIS-VG-Hilfs-Layer wird in allen Fällen aus Projekt entfernt +- verfahrensgebiet_alkis gibt LoadStatus.NONE → (None, None, None, NONE) +- flurstueck_alkis gibt LoadStatus.NONE → (None, None, None, NONE) +- Keine Flurstücke selektiert → (None, flst_layer, None, NONE) + Warnung +- erstelle_verfahrensgebiet_aus_auswahl liefert ungültigen Layer → (None, flst_layer, None, NONE) +- Progress-Abbruch nach Schritt 1 → sofortiger sauberer Rückgabestatus + +Flächenvergleich: +- Übereinstimmung → show_info_dialog, keine Umringsprüfung +- Abweichung + Nein → kein Umringsprüfungs-Aufruf +- Abweichung + Ja → umringspruefung aufgerufen, Ergebnislayer ins Projekt + +Cleanup: +- BROR-Layer wird bei Erfolg NICHT entfernt +- BROR-Layer wird bei Fehler entfernt (wenn in diesem Lauf hinzugefügt) +- TempVerfahrensgebiet wird stets entfernt Hilfsfunktionen: - _select_by_location: processing-Fallback liefert 0 → keine Exception - _fuege_vkz_feld_hinzu: leeres VKZ wird ignoriert +- _vergleiche_flaechen: korrekte Toleranzlogik +- _lese_flaechen_toleranz: Default 0.0, parst Projektvariable """ import sys import types import unittest -from unittest.mock import patch, MagicMock, call +from unittest.mock import patch, MagicMock # --------------------------------------------------------------------------- # Minimales QGIS-Mock-Setup @@ -46,6 +58,8 @@ from sn_verfahrensgebiet.functions.verfahrensgebiet_alkis_komplett import ( verfahrensgebiet_alkis_komplett, _select_by_location, _fuege_vkz_feld_hinzu, + _vergleiche_flaechen, + _lese_flaechen_toleranz, ) @@ -60,7 +74,8 @@ class _DummyRect: def yMaximum(self): return 5658000.0 -def _make_layer(valid=True, name="Layer", lid="L1", feature_count=1): +def _make_layer(valid=True, name="Layer", lid="L1", feature_count=1, area=1000.0): + """Erzeugt einen konfigurierten Mock-Layer mit getFeatures-Support.""" layer = MagicMock() layer.isValid.return_value = valid layer.id.return_value = lid @@ -68,6 +83,18 @@ def _make_layer(valid=True, name="Layer", lid="L1", feature_count=1): layer.extent.return_value = _DummyRect() layer.featureCount.return_value = feature_count layer.selectedFeatureCount.return_value = feature_count + + mock_geom = MagicMock() + mock_geom.isEmpty.return_value = False + mock_geom.area.return_value = area + mock_geom.boundary.return_value = MagicMock(isEmpty=lambda: True) + mock_feat = MagicMock() + mock_feat.geometry.return_value = mock_geom + layer.getFeatures.return_value = [mock_feat] + + mock_crs = MagicMock() + mock_crs.authid.return_value = "EPSG:25833" + layer.crs.return_value = mock_crs return layer @@ -90,6 +117,34 @@ class _DummyProject: return self +def _make_progress_mock(canceled=False): + prog = MagicMock() + prog.is_canceled.return_value = canceled + return prog + + +# --------------------------------------------------------------------------- +# Patch-Konstanten +# --------------------------------------------------------------------------- + +_MOD = "sn_verfahrensgebiet.functions.verfahrensgebiet_alkis_komplett" +_PATCH_VG_ALKIS = f"{_MOD}.verfahrensgebiet_alkis" +_PATCH_GEM = f"{_MOD}.gemarkung_schluessel_und_namen_von_vg_geometrie" +_PATCH_FLST = f"{_MOD}.flurstueck_alkis" +_PATCH_SELECT = f"{_MOD}._select_by_location" +_PATCH_ERSTELLE = f"{_MOD}.erstelle_verfahrensgebiet_aus_auswahl" +_PATCH_VKZ = f"{_MOD}._fuege_vkz_feld_hinzu" +_PATCH_PROJECT = f"{_MOD}.QgsProject" +_PATCH_MSGBOX = f"{_MOD}.QMessageBox" +_PATCH_WARNING = f"{_MOD}.warning" +_PATCH_GET_VAR = f"{_MOD}.get_variable" +_PATCH_PROGRESS = f"{_MOD}.create_progress_dialog" +_PATCH_VERGLEICHE = f"{_MOD}._vergleiche_flaechen" +_PATCH_SHOW_INFO = f"{_MOD}.show_info_dialog" +_PATCH_ASK_YES_NO = f"{_MOD}.ask_yes_no" +_PATCH_UMRING = f"{_MOD}.umringspruefung" + + # --------------------------------------------------------------------------- # Tests: _select_by_location # --------------------------------------------------------------------------- @@ -97,35 +152,26 @@ class _DummyProject: class TestSelectByLocation(unittest.TestCase): def test_processing_not_available_returns_zero(self): - """Wenn processing nicht importierbar ist → 0 ohne Exception.""" - flst = MagicMock() - vg = MagicMock() + flst, vg = MagicMock(), MagicMock() with patch.dict("sys.modules", {"processing": None}): result = _select_by_location(flst, vg) self.assertEqual(result, 0) def test_processing_success_returns_count(self): - """processing.run erfolgreich → selectedFeatureCount des Layers.""" flst = MagicMock() flst.selectedFeatureCount.return_value = 7 vg = MagicMock() - - mock_processing = MagicMock() - with patch.dict("sys.modules", {"processing": mock_processing}): + mock_proc = MagicMock() + with patch.dict("sys.modules", {"processing": mock_proc}): result = _select_by_location(flst, vg) - self.assertEqual(result, 7) def test_processing_exception_returns_zero(self): - """Exception in processing.run → 0 ohne Propagation.""" - flst = MagicMock() - vg = MagicMock() - - mock_processing = MagicMock() - mock_processing.run.side_effect = RuntimeError("Kein QGIS") - with patch.dict("sys.modules", {"processing": mock_processing}): + flst, vg = MagicMock(), MagicMock() + mock_proc = MagicMock() + mock_proc.run.side_effect = RuntimeError("Kein QGIS") + with patch.dict("sys.modules", {"processing": mock_proc}): result = _select_by_location(flst, vg) - self.assertEqual(result, 0) @@ -136,225 +182,427 @@ class TestSelectByLocation(unittest.TestCase): class TestFuegeVkzFeldHinzu(unittest.TestCase): def test_empty_vkz_does_nothing(self): - """Leeres VKZ → kein Zugriff auf Layer.""" layer = MagicMock() _fuege_vkz_feld_hinzu(layer, "") layer.dataProvider.assert_not_called() def test_none_vkz_does_nothing(self): - """None-VKZ → kein Zugriff auf Layer.""" layer = MagicMock() _fuege_vkz_feld_hinzu(layer, None) layer.dataProvider.assert_not_called() def test_vkz_field_added(self): - """Nicht-leeres VKZ → Provider.addAttributes wird aufgerufen.""" pr = MagicMock() pr.addAttributes.return_value = True pr.changeAttributeValues.return_value = True - layer = MagicMock() layer.dataProvider.return_value = pr layer.fields.return_value.indexFromName.return_value = 2 layer.getFeatures.return_value = [] - _fuege_vkz_feld_hinzu(layer, "VF-2024-001") - pr.addAttributes.assert_called_once() +# --------------------------------------------------------------------------- +# Tests: _vergleiche_flaechen / _lese_flaechen_toleranz +# --------------------------------------------------------------------------- + +class TestFlaechenvergleich(unittest.TestCase): + + def _layer(self, area: float): + layer = MagicMock() + geom = MagicMock() + geom.isEmpty.return_value = False + geom.area.return_value = area + feat = MagicMock() + feat.geometry.return_value = geom + layer.getFeatures.return_value = [feat] + return layer + + def test_gleiche_flaeche_stimmt_ueberein(self): + self.assertTrue(_vergleiche_flaechen(self._layer(1234.5), self._layer(1234.5), 0.0)[2]) + + def test_verschiedene_flaeche_stimmt_nicht_ueberein(self): + self.assertFalse(_vergleiche_flaechen(self._layer(1234.5), self._layer(1200.0), 0.0)[2]) + + def test_differenz_innerhalb_toleranz(self): + self.assertTrue(_vergleiche_flaechen(self._layer(1234.5), self._layer(1234.8), 1.0)[2]) + + def test_differenz_ausserhalb_toleranz(self): + self.assertFalse(_vergleiche_flaechen(self._layer(1234.5), self._layer(1236.0), 1.0)[2]) + + def test_flaechen_werte_korrekt(self): + bror_f, vg_f, _ = _vergleiche_flaechen(self._layer(800.0), self._layer(750.0)) + self.assertAlmostEqual(bror_f, 800.0) + self.assertAlmostEqual(vg_f, 750.0) + + @patch(f"{_MOD}.get_variable", return_value=None) + def test_lese_toleranz_default_ist_null(self, _): + self.assertEqual(_lese_flaechen_toleranz(), 0.0) + + @patch(f"{_MOD}.get_variable", return_value="2.5") + def test_lese_toleranz_parst_string(self, _): + self.assertAlmostEqual(_lese_flaechen_toleranz(), 2.5) + + @patch(f"{_MOD}.get_variable", return_value="ungueltig") + def test_lese_toleranz_ungueltig_fallback_null(self, _): + self.assertEqual(_lese_flaechen_toleranz(), 0.0) + + # --------------------------------------------------------------------------- # Tests: verfahrensgebiet_alkis_komplett (Hauptworkflow) # --------------------------------------------------------------------------- -_PATCH_VG_ALKIS = "sn_verfahrensgebiet.functions.verfahrensgebiet_alkis_komplett.verfahrensgebiet_alkis" -_PATCH_GEM_BY_BBOX = "sn_verfahrensgebiet.functions.verfahrensgebiet_alkis_komplett.gemarkung_schluessel_und_namen_by_bbox" -_PATCH_FLST = "sn_verfahrensgebiet.functions.verfahrensgebiet_alkis_komplett.flurstueck_alkis" -_PATCH_SELECT = "sn_verfahrensgebiet.functions.verfahrensgebiet_alkis_komplett._select_by_location" -_PATCH_ERSTELLE = "sn_verfahrensgebiet.functions.verfahrensgebiet_alkis_komplett.erstelle_verfahrensgebiet_aus_auswahl" -_PATCH_VKZ = "sn_verfahrensgebiet.functions.verfahrensgebiet_alkis_komplett._fuege_vkz_feld_hinzu" -_PATCH_PROJECT = "sn_verfahrensgebiet.functions.verfahrensgebiet_alkis_komplett.QgsProject" -_PATCH_MSGBOX = "sn_verfahrensgebiet.functions.verfahrensgebiet_alkis_komplett.QMessageBox" -_PATCH_INFO = "sn_verfahrensgebiet.functions.verfahrensgebiet_alkis_komplett.info" -_PATCH_WARNING = "sn_verfahrensgebiet.functions.verfahrensgebiet_alkis_komplett.warning" -_PATCH_GET_VAR = "sn_verfahrensgebiet.functions.verfahrensgebiet_alkis_komplett.get_variable" - - class TestVerfahrensgebietAlkisKomplett(unittest.TestCase): - def _make_project(self): - proj = _DummyProject() - mock = MagicMock() - mock.instance.return_value = proj - # Leere mapLayersByName für Cleanup - proj._layers_by_name = {"Verfahrensgebiet": [], "TempVerfahrensgebiet": []} - return mock, proj + # ------------------------------------------------------------------ # + # Abbruchpfade + # ------------------------------------------------------------------ # - # ------------------------------------------------------------------ # - # 1. VG-ALKIS liefert NONE → sofortiger Abbruch # - # ------------------------------------------------------------------ # + @patch(_PATCH_PROGRESS) @patch(_PATCH_PROJECT) @patch(_PATCH_VG_ALKIS) - def test_vg_alkis_none_aborts(self, mock_vg, mock_proj): + def test_vg_alkis_none_aborts(self, mock_vg, mock_proj, mock_prog): mock_proj.instance.return_value = _DummyProject() - mock_vg.return_value = (None, LoadStatus.NONE) + mock_vg.return_value = (None, LoadStatus.NONE, []) + mock_prog.return_value = _make_progress_mock() - enriched, flst, status = verfahrensgebiet_alkis_komplett(None) + enriched, flst, bror, status = verfahrensgebiet_alkis_komplett(None) self.assertIsNone(enriched) self.assertIsNone(flst) + self.assertIsNone(bror) self.assertEqual(status, LoadStatus.NONE) - # ------------------------------------------------------------------ # - # 2. flurstueck_alkis liefert NONE → Abbruch nach Gemarkungsabfrage # - # ------------------------------------------------------------------ # + @patch(_PATCH_PROGRESS) @patch(_PATCH_PROJECT) - @patch(_PATCH_FLST) - @patch(_PATCH_GEM_BY_BBOX) + @patch(_PATCH_GEM) @patch(_PATCH_VG_ALKIS) - def test_flst_none_aborts(self, mock_vg, mock_gem, mock_flst, mock_proj): + def test_flst_none_aborts(self, mock_vg, mock_gem, mock_proj, mock_prog): mock_proj.instance.return_value = _DummyProject() - vg_layer = _make_layer(name="Verfahrensgebiet") - mock_vg.return_value = (vg_layer, LoadStatus.FIRST) + vg_layer = _make_layer(name="BauRaumOrdnungsrecht (ALKIS)") + mock_vg.return_value = (vg_layer, LoadStatus.FIRST, []) mock_gem.return_value = [("1234", "Muster")] - mock_flst.return_value = (None, LoadStatus.NONE) + mock_prog.return_value = _make_progress_mock() - enriched, flst, status = verfahrensgebiet_alkis_komplett(None) + with patch(_PATCH_FLST, return_value=(None, LoadStatus.NONE)): + enriched, flst, bror, status = verfahrensgebiet_alkis_komplett(None) self.assertIsNone(enriched) self.assertIsNone(flst) self.assertEqual(status, LoadStatus.NONE) - # ------------------------------------------------------------------ # - # 3. Keine Selektion → Warnung + (None, flst_layer, NONE) # - # ------------------------------------------------------------------ # + @patch(_PATCH_PROGRESS) @patch(_PATCH_MSGBOX) @patch(_PATCH_SELECT) @patch(_PATCH_PROJECT) + @patch(_PATCH_GEM) @patch(_PATCH_FLST) - @patch(_PATCH_GEM_BY_BBOX) @patch(_PATCH_VG_ALKIS) def test_no_selection_returns_none_enriched( - self, mock_vg, mock_gem, mock_flst, mock_proj, mock_select, mock_msgbox + self, mock_vg, mock_flst, mock_gem, mock_proj, mock_select, mock_msgbox, mock_prog ): mock_proj.instance.return_value = _DummyProject() - vg_layer = _make_layer(name="Verfahrensgebiet") + vg_layer = _make_layer(name="BauRaumOrdnungsrecht (ALKIS)") flst_layer = _make_layer(name="Flurstücke") - mock_vg.return_value = (vg_layer, LoadStatus.FIRST) - mock_gem.return_value = [] - mock_flst.return_value = (flst_layer, LoadStatus.FIRST) - mock_select.return_value = 0 # keine Selektion + mock_vg.return_value = (vg_layer, LoadStatus.FIRST, []) + mock_gem.return_value = [] + mock_flst.return_value = (flst_layer, LoadStatus.FIRST) + mock_select.return_value = 0 + mock_prog.return_value = _make_progress_mock() - enriched, flst, status = verfahrensgebiet_alkis_komplett(None) + enriched, flst, bror, status = verfahrensgebiet_alkis_komplett(None) self.assertIsNone(enriched) self.assertIs(flst, flst_layer) + self.assertIsNone(bror) self.assertEqual(status, LoadStatus.NONE) mock_msgbox.warning.assert_called_once() - # ------------------------------------------------------------------ # - # 4. Vollständiger Erfolgsfall # - # ------------------------------------------------------------------ # - @patch(_PATCH_VKZ) - @patch(_PATCH_GET_VAR) - @patch(_PATCH_ERSTELLE) - @patch(_PATCH_SELECT) - @patch(_PATCH_PROJECT) - @patch(_PATCH_FLST) - @patch(_PATCH_GEM_BY_BBOX) - @patch(_PATCH_VG_ALKIS) - def test_success_full_workflow( - self, - mock_vg, mock_gem, mock_flst, mock_proj, - mock_select, mock_erstelle, mock_get_var, mock_vkz_fn, - ): - """Vollständiger Workflow → enriched Layer + flst Layer + FIRST.""" - proj_instance = _DummyProject() - mock_proj.instance.return_value = proj_instance - - vg_layer = _make_layer(name="Verfahrensgebiet") - flst_layer = _make_layer(name="Flurstücke", lid="FLST1") - enriched = _make_layer(name="Verfahrensgebiet", lid="VG_ENRICHED", valid=True) - - mock_vg.return_value = (vg_layer, LoadStatus.FIRST) - mock_gem.return_value = [("1234", "Muster")] - mock_flst.return_value = (flst_layer, LoadStatus.FIRST) - mock_select.return_value = 3 - mock_erstelle.return_value = (enriched, 3, 0) - mock_get_var.return_value = "VF-2024-001" - - result_enriched, result_flst, status = verfahrensgebiet_alkis_komplett(None) - - self.assertIs(result_enriched, enriched) - self.assertIs(result_flst, flst_layer) - self.assertEqual(status, LoadStatus.FIRST) - mock_vkz_fn.assert_called_once_with(enriched, "VF-2024-001") - - # ------------------------------------------------------------------ # - # 5. Kein VKZ → _fuege_vkz_feld_hinzu mit leerem String # - # ------------------------------------------------------------------ # - @patch(_PATCH_VKZ) - @patch(_PATCH_GET_VAR) - @patch(_PATCH_ERSTELLE) - @patch(_PATCH_SELECT) - @patch(_PATCH_PROJECT) - @patch(_PATCH_FLST) - @patch(_PATCH_GEM_BY_BBOX) - @patch(_PATCH_VG_ALKIS) - def test_no_vkz_variable_passes_empty_string( - self, - mock_vg, mock_gem, mock_flst, mock_proj, - mock_select, mock_erstelle, mock_get_var, mock_vkz_fn, - ): - proj_instance = _DummyProject() - mock_proj.instance.return_value = proj_instance - - vg_layer = _make_layer(name="Verfahrensgebiet") - flst_layer = _make_layer(name="Flurstücke") - enriched = _make_layer(name="Verfahrensgebiet", valid=True) - - mock_vg.return_value = (vg_layer, LoadStatus.FIRST) - mock_gem.return_value = [] - mock_flst.return_value = (flst_layer, LoadStatus.FIRST) - mock_select.return_value = 2 - mock_erstelle.return_value = (enriched, 2, 0) - mock_get_var.return_value = "" # keine Projektvariable - - verfahrensgebiet_alkis_komplett(None) - - mock_vkz_fn.assert_called_once_with(enriched, "") - - # ------------------------------------------------------------------ # - # 6. erstelle_verfahrensgebiet liefert ungültigen Layer # - # ------------------------------------------------------------------ # + @patch(_PATCH_PROGRESS) @patch(_PATCH_MSGBOX) @patch(_PATCH_ERSTELLE) @patch(_PATCH_SELECT) @patch(_PATCH_PROJECT) + @patch(_PATCH_GEM) @patch(_PATCH_FLST) - @patch(_PATCH_GEM_BY_BBOX) @patch(_PATCH_VG_ALKIS) def test_invalid_enriched_layer_returns_none( - self, mock_vg, mock_gem, mock_flst, mock_proj, mock_select, mock_erstelle, mock_msgbox + self, mock_vg, mock_flst, mock_gem, mock_proj, mock_select, mock_erstelle, mock_msgbox, mock_prog ): mock_proj.instance.return_value = _DummyProject() - vg_layer = _make_layer() - flst_layer = _make_layer(name="Flurstücke") + vg_layer = _make_layer(name="BauRaumOrdnungsrecht (ALKIS)") + flst_layer = _make_layer(name="Flurstücke") bad_enriched = _make_layer(valid=False) - mock_vg.return_value = (vg_layer, LoadStatus.FIRST) - mock_gem.return_value = [] - mock_flst.return_value = (flst_layer, LoadStatus.FIRST) - mock_select.return_value = 5 - mock_erstelle.return_value = (bad_enriched, 0, 5) + mock_vg.return_value = (vg_layer, LoadStatus.FIRST, []) + mock_gem.return_value = [] + mock_flst.return_value = (flst_layer, LoadStatus.FIRST) + mock_select.return_value = 5 + mock_erstelle.return_value = (bad_enriched, 0, 5) + mock_prog.return_value = _make_progress_mock() - enriched, flst, status = verfahrensgebiet_alkis_komplett(None) + enriched, flst, bror, status = verfahrensgebiet_alkis_komplett(None) self.assertIsNone(enriched) self.assertIs(flst, flst_layer) + self.assertIsNone(bror) self.assertEqual(status, LoadStatus.NONE) mock_msgbox.critical.assert_called_once() + @patch(_PATCH_PROGRESS) + @patch(_PATCH_PROJECT) + @patch(_PATCH_VG_ALKIS) + def test_progress_cancel_after_step1_returns_none(self, mock_vg, mock_proj, mock_prog): + """Abbruch nach Schritt 1 → sofortiger sauberer Rückgabestatus.""" + mock_proj.instance.return_value = _DummyProject() + vg_layer = _make_layer(name="BauRaumOrdnungsrecht (ALKIS)") + mock_vg.return_value = (vg_layer, LoadStatus.FIRST, []) + + prog = MagicMock() + prog.is_canceled.side_effect = [False, True] # 1. Aufruf False, 2. True + mock_prog.return_value = prog + + enriched, flst, bror, status = verfahrensgebiet_alkis_komplett(None) + + self.assertIsNone(enriched) + self.assertEqual(status, LoadStatus.NONE) + + # ------------------------------------------------------------------ # + # Flächenvergleich – Übereinstimmung + # ------------------------------------------------------------------ # + + @patch(_PATCH_PROGRESS) + @patch(_PATCH_SHOW_INFO) + @patch(_PATCH_VERGLEICHE) + @patch(_PATCH_VKZ) + @patch(_PATCH_GET_VAR) + @patch(_PATCH_ERSTELLE) + @patch(_PATCH_SELECT) + @patch(_PATCH_PROJECT) + @patch(_PATCH_GEM) + @patch(_PATCH_FLST) + @patch(_PATCH_VG_ALKIS) + def test_success_flaechen_match_zeigt_info( + self, mock_vg, mock_flst, mock_gem, mock_proj, mock_select, + mock_erstelle, mock_get_var, mock_vkz, mock_vergl, mock_info, mock_prog, + ): + """Flächenübereinstimmung → show_info_dialog mit erwartetem Text.""" + proj_inst = _DummyProject() + mock_proj.instance.return_value = proj_inst + + vg_layer = _make_layer(name="BauRaumOrdnungsrecht (ALKIS)") + flst_layer = _make_layer(name="Flurstücke", lid="FLST1") + enriched = _make_layer(name="Verfahrensgebiet", lid="VG_ENRICHED", valid=True) + + mock_vg.return_value = (vg_layer, LoadStatus.FIRST, []) + mock_gem.return_value = [("1234", "Muster")] + mock_flst.return_value = (flst_layer, LoadStatus.FIRST) + mock_select.return_value = 3 + mock_erstelle.return_value = (enriched, 3, 0) + mock_get_var.return_value = "VF-2024-001" + mock_vergl.return_value = (1234.5, 1234.5, True) + mock_prog.return_value = _make_progress_mock() + + result_enriched, result_flst, result_bror, status = verfahrensgebiet_alkis_komplett(None) + + self.assertIs(result_enriched, enriched) + self.assertIs(result_flst, flst_layer) + self.assertIs(result_bror, vg_layer) + self.assertEqual(status, LoadStatus.FIRST) + mock_vkz.assert_called_once_with(enriched, "VF-2024-001") + mock_info.assert_called_once() + self.assertIn("erwarteter Geometrie", mock_info.call_args[0][1]) + + @patch(_PATCH_PROGRESS) + @patch(_PATCH_SHOW_INFO) + @patch(_PATCH_VERGLEICHE) + @patch(_PATCH_VKZ) + @patch(_PATCH_GET_VAR) + @patch(_PATCH_ERSTELLE) + @patch(_PATCH_SELECT) + @patch(_PATCH_PROJECT) + @patch(_PATCH_GEM) + @patch(_PATCH_FLST) + @patch(_PATCH_VG_ALKIS) + def test_no_vkz_variable_passes_empty_string( + self, mock_vg, mock_flst, mock_gem, mock_proj, mock_select, + mock_erstelle, mock_get_var, mock_vkz, mock_vergl, mock_info, mock_prog, + ): + mock_proj.instance.return_value = _DummyProject() + vg_layer = _make_layer(name="BauRaumOrdnungsrecht (ALKIS)") + flst_layer = _make_layer(name="Flurstücke") + enriched = _make_layer(name="Verfahrensgebiet", valid=True) + + mock_vg.return_value = (vg_layer, LoadStatus.FIRST, []) + mock_gem.return_value = [] + mock_flst.return_value = (flst_layer, LoadStatus.FIRST) + mock_select.return_value = 2 + mock_erstelle.return_value = (enriched, 2, 0) + mock_get_var.return_value = "" + mock_vergl.return_value = (1000.0, 1000.0, True) + mock_prog.return_value = _make_progress_mock() + + verfahrensgebiet_alkis_komplett(None) + + mock_vkz.assert_called_once_with(enriched, "") + + # ------------------------------------------------------------------ # + # Flächenvergleich – Abweichung + Nein + # ------------------------------------------------------------------ # + + @patch(_PATCH_PROGRESS) + @patch(_PATCH_UMRING) + @patch(_PATCH_ASK_YES_NO) + @patch(_PATCH_VERGLEICHE) + @patch(_PATCH_VKZ) + @patch(_PATCH_GET_VAR) + @patch(_PATCH_ERSTELLE) + @patch(_PATCH_SELECT) + @patch(_PATCH_PROJECT) + @patch(_PATCH_GEM) + @patch(_PATCH_FLST) + @patch(_PATCH_VG_ALKIS) + def test_flaechen_abweichung_nein_kein_umring( + self, mock_vg, mock_flst, mock_gem, mock_proj, mock_select, + mock_erstelle, mock_get_var, mock_vkz, mock_vergl, mock_ask, mock_umring, mock_prog, + ): + """Abweichung + Nein → umringspruefung wird NICHT aufgerufen.""" + mock_proj.instance.return_value = _DummyProject() + vg_layer = _make_layer(name="BauRaumOrdnungsrecht (ALKIS)") + flst_layer = _make_layer(name="Flurstücke") + enriched = _make_layer(name="Verfahrensgebiet", valid=True) + + mock_vg.return_value = (vg_layer, LoadStatus.FIRST, []) + mock_gem.return_value = [] + mock_flst.return_value = (flst_layer, LoadStatus.FIRST) + mock_select.return_value = 3 + mock_erstelle.return_value = (enriched, 3, 0) + mock_get_var.return_value = "" + mock_vergl.return_value = (1000.0, 800.0, False) + mock_ask.return_value = False + mock_prog.return_value = _make_progress_mock() + + result_enriched, _, _, status = verfahrensgebiet_alkis_komplett(None) + + mock_umring.assert_not_called() + self.assertIs(result_enriched, enriched) + self.assertEqual(status, LoadStatus.FIRST) + + # ------------------------------------------------------------------ # + # Flächenvergleich – Abweichung + Ja → Umringsprüfung + # ------------------------------------------------------------------ # + + @patch(_PATCH_PROGRESS) + @patch(_PATCH_SHOW_INFO) + @patch(_PATCH_UMRING) + @patch(_PATCH_ASK_YES_NO) + @patch(_PATCH_VERGLEICHE) + @patch(_PATCH_VKZ) + @patch(_PATCH_GET_VAR) + @patch(_PATCH_ERSTELLE) + @patch(_PATCH_SELECT) + @patch(_PATCH_PROJECT) + @patch(_PATCH_GEM) + @patch(_PATCH_FLST) + @patch(_PATCH_VG_ALKIS) + def test_flaechen_abweichung_ja_startet_umringspruefung( + self, mock_vg, mock_flst, mock_gem, mock_proj, mock_select, + mock_erstelle, mock_get_var, mock_vkz, mock_vergl, mock_ask, + mock_umring, mock_info, mock_prog, + ): + """Abweichung + Ja → umringspruefung aufgerufen, Ergebnislayer ins Projekt, + Abschlussmeldung mit Konfliktanzahl.""" + proj_inst = _DummyProject() + mock_proj.instance.return_value = proj_inst + + vg_layer = _make_layer(name="BauRaumOrdnungsrecht (ALKIS)") + flst_layer = _make_layer(name="Flurstücke") + enriched = _make_layer(name="Verfahrensgebiet", valid=True) + ergebnis_layer = _make_layer(name="Umringsprüfung", lid="UMRING_L") + + mock_vg.return_value = (vg_layer, LoadStatus.FIRST, []) + mock_gem.return_value = [] + mock_flst.return_value = (flst_layer, LoadStatus.FIRST) + mock_select.return_value = 3 + mock_erstelle.return_value = (enriched, 3, 0) + mock_get_var.return_value = "" + mock_vergl.return_value = (1000.0, 800.0, False) + mock_ask.return_value = True + mock_umring.return_value = (ergebnis_layer, 4) + mock_prog.return_value = _make_progress_mock() + + result_enriched, _, _, status = verfahrensgebiet_alkis_komplett(None) + + mock_umring.assert_called_once() + self.assertIn(ergebnis_layer, proj_inst.added) + mock_info.assert_called_once() + self.assertIn("4", mock_info.call_args[0][1]) + self.assertIn("Konfliktstellen", mock_info.call_args[0][1]) + self.assertEqual(status, LoadStatus.FIRST) + + # ------------------------------------------------------------------ # + # Cleanup-Verhalten + # ------------------------------------------------------------------ # + + @patch(_PATCH_PROGRESS) + @patch(_PATCH_SHOW_INFO) + @patch(_PATCH_VERGLEICHE) + @patch(_PATCH_VKZ) + @patch(_PATCH_GET_VAR) + @patch(_PATCH_ERSTELLE) + @patch(_PATCH_SELECT) + @patch(_PATCH_PROJECT) + @patch(_PATCH_GEM) + @patch(_PATCH_FLST) + @patch(_PATCH_VG_ALKIS) + def test_bror_layer_bleibt_bei_erfolg_im_projekt( + self, mock_vg, mock_flst, mock_gem, mock_proj, mock_select, + mock_erstelle, mock_get_var, mock_vkz, mock_vergl, mock_info, mock_prog, + ): + """Bei Erfolg darf der BROR-Layer NICHT entfernt werden.""" + proj_inst = _DummyProject() + mock_proj.instance.return_value = proj_inst + + vg_layer = _make_layer(name="BauRaumOrdnungsrecht (ALKIS)", lid="BROR_ID") + flst_layer = _make_layer(name="Flurstücke") + enriched = _make_layer(name="Verfahrensgebiet", valid=True) + + mock_vg.return_value = (vg_layer, LoadStatus.FIRST, []) + mock_gem.return_value = [] + mock_flst.return_value = (flst_layer, LoadStatus.FIRST) + mock_select.return_value = 3 + mock_erstelle.return_value = (enriched, 3, 0) + mock_get_var.return_value = "" + mock_vergl.return_value = (1000.0, 1000.0, True) + mock_prog.return_value = _make_progress_mock() + + verfahrensgebiet_alkis_komplett(None) + + self.assertNotIn("BROR_ID", proj_inst.removed) + + @patch(_PATCH_PROGRESS) + @patch(_PATCH_PROJECT) + @patch(_PATCH_GEM) + @patch(_PATCH_VG_ALKIS) + def test_bror_layer_wird_bei_fehler_entfernt( + self, mock_vg, mock_gem, mock_proj, mock_prog + ): + """Bei Fehler (flst_alkis NONE) wird der BROR-Layer aus dem Projekt entfernt.""" + proj_inst = _DummyProject() + mock_proj.instance.return_value = proj_inst + + vg_layer = _make_layer(name="BauRaumOrdnungsrecht (ALKIS)", lid="BROR_ERR") + mock_vg.return_value = (vg_layer, LoadStatus.FIRST, []) + mock_gem.return_value = [] + mock_prog.return_value = _make_progress_mock() + + with patch(_PATCH_FLST, return_value=(None, LoadStatus.NONE)): + verfahrensgebiet_alkis_komplett(None) + + self.assertIn("BROR_ERR", proj_inst.removed) + if __name__ == "__main__": unittest.main() + diff --git a/tests/test_working_tab.py b/tests/test_working_tab.py index 5af3cd7..f2fc707 100644 --- a/tests/test_working_tab.py +++ b/tests/test_working_tab.py @@ -18,9 +18,10 @@ from sn_verfahrensgebiet.ui.tabs.working_tab import WorkingTab, LoadStatus class _DummyLayer: - def __init__(self, valid=True, layer_id="L1"): + def __init__(self, valid=True, layer_id="L1", layer_name="DummyLayer"): self._valid = valid self._id = layer_id + self._name = layer_name def isValid(self): return self._valid @@ -28,6 +29,9 @@ class _DummyLayer: def id(self): return self._id + def name(self): + return self._name + def extent(self): return object() @@ -140,11 +144,11 @@ class TestWorkingTab(unittest.TestCase): mock_apply_style, mock_success, ): - """verfahrensgebiet_alkis_komplett → 3-Tupel (enriched, flst, FIRST).""" + """verfahrensgebiet_alkis_komplett → 4-Tupel (enriched, flst, bror, FIRST).""" tab = self._make_tab() enriched = _DummyLayer(valid=True, layer_id="enriched_ok") flst = _DummyLayer(valid=True, layer_id="flst_ok") - mock_alkis_komplett.return_value = (enriched, flst, LoadStatus.FIRST) + mock_alkis_komplett.return_value = (enriched, flst, None, LoadStatus.FIRST) proj = _DummyProject() proj.mapLayersByName = lambda _n: [] # kein bestehender Flst-Layer mock_project_instance.return_value = proj @@ -159,7 +163,7 @@ class TestWorkingTab(unittest.TestCase): @patch("sn_verfahrensgebiet.ui.tabs.working_tab.set_verfahrensgebiet_layer_id") @patch( "sn_verfahrensgebiet.ui.tabs.working_tab.verfahrensgebiet_alkis_komplett", - return_value=(None, None, LoadStatus.NONE), + return_value=(None, None, None, LoadStatus.NONE), ) def test_handle_verf_alkis_failure_clears_layer_var(self, _mock_alkis, mock_set_layer_id): """Status NONE → Layer-Variable wird auf '' gesetzt.""" diff --git a/ui/tabs/working_tab.py b/ui/tabs/working_tab.py index 110f001..645f38f 100644 --- a/ui/tabs/working_tab.py +++ b/ui/tabs/working_tab.py @@ -138,15 +138,16 @@ class WorkingTab(QWidget): self._restore_verf_layer_selection() def handle_verf_alkis(self) -> None: - """Startet den vollständigen VG-Lade-Workflow (ALKIS → Gemarkungen → Flurstücke → Verschneidung). + """Startet den vollständigen VG-Lade-Workflow (ALKIS → Gemarkungen → Flurstücke → + Verschneidung → Flächenvergleich → opt. Umringsprüfung). Delegiert an :func:`~sn_verfahrensgebiet.functions.verfahrensgebiet_alkis_komplett.verfahrensgebiet_alkis_komplett`. - Nach Erfolg werden enriched VG-Layer und Flurstücks-Layer ins Projekt eingefügt, - Styles angewendet und die UI-Elemente aktualisiert. + Nach Erfolg werden enriched VG-Layer, Flurstücks-Layer und BROR-Layer + ins Projekt eingefügt, Styles angewendet und die UI-Elemente aktualisiert. """ self._set_busy(self.btn_verf_alkis, True, "Aus ALKIS laden") try: - enriched_layer, flst_layer, status = verfahrensgebiet_alkis_komplett(self) + enriched_layer, flst_layer, bror_layer, status = verfahrensgebiet_alkis_komplett(self) if status == LoadStatus.NONE or not enriched_layer: set_verfahrensgebiet_layer_id("") @@ -162,6 +163,10 @@ class WorkingTab(QWidget): apply_style(flst_layer, "GIS_Flst_Beschriftung_ALKIS_NAS.qml") self.setze_haken(self.haken_flurst, True) + # BROR-Layer: einfache Darstellung setzen (roter Umring, transparent) + if bror_layer and bror_layer.isValid(): + _setze_bror_stil(bror_layer) + # Enriched VG-Layer ins Projekt project.addMapLayer(enriched_layer) apply_style(enriched_layer, "verfahrensgebiet.qml") @@ -430,3 +435,37 @@ class WorkingTab(QWidget): # self._set_busy(self.btn_grenz_alkis, False, "Grenzpunkte aus ALKIS laden") from sn_basis.functions.message_wrapper import warning as _warn _warn("Grenzpunkte", "Grenzpunkt-Pipeline ist noch nicht implementiert.", duration=5) + + +# --------------------------------------------------------------------------- +# Modul-Hilfsfunktionen +# --------------------------------------------------------------------------- + +def _setze_bror_stil(layer) -> None: + """Setzt eine einfache Darstellung für den BauRaumOrdnungsrecht-Layer. + + Roter Umring, halbtransparente Füllung; kein QGIS-Kontext → stilles Ignorieren. + + Parameters + ---------- + layer : + BauRaumOrdnungsrecht-Memory-Layer (Polygon). + """ + try: + from qgis.core import ( # type: ignore[import] + QgsSimpleFillSymbolLayer, + QgsFillSymbol, + QgsSingleSymbolRenderer, + ) + from qgis.PyQt.QtGui import QColor # type: ignore[import] + from qgis.PyQt.QtCore import Qt # type: ignore[import] + + fill = QgsSimpleFillSymbolLayer() + fill.setColor(QColor(220, 50, 50, 40)) # halbtransparent rot + fill.setStrokeColor(QColor(220, 50, 50)) + fill.setStrokeWidth(0.8) + symbol = QgsFillSymbol([fill]) + layer.setRenderer(QgsSingleSymbolRenderer(symbol)) + layer.triggerRepaint() + except Exception: + pass # Kein QGIS-Kontext (Tests, Mock-Modus) → Standarddarstellung