"""sn_verfahrensgebiet/functions/umringstatistik_grenzpunkte.py – Umringstatistik Grenzpunkte. Extrahiert aus einem vorhandenen ``Grenzpunkte (ALKIS)``-Layer alle Punkte, die auf dem Umring des Verfahrensgebiets liegen, und klassifiziert sie in drei Kategorien: 1. **FGP** (grün): ``gp_festgestellterGrenzpunkt == "true"`` 2. **<3cm** (gelb): ``gp_festgestellterGrenzpunkt != "true"`` UND ``qualitaetsangaben_ax_dqpunktort_genauigkeitsstufe == 2100`` 3. **>3cm** (rot): ``gp_festgestellterGrenzpunkt != "true"`` UND ``qualitaetsangaben_ax_dqpunktort_genauigkeitsstufe > 2100`` 4. **unbekannt** (grau): keiner der obigen Fälle Der Umring-Test verwendet einen Abstandsschwellwert von :data:`UMRING_TOLERANZ_M`. **Rückgabe:** Memory-Layer ``Umringstatistik Grenzpunkte`` oder ``None`` bei Fehler. """ from __future__ import annotations from typing import Optional from sn_basis.functions import QgsProject from sn_basis.functions.qt_wrapper import QMessageBox from sn_basis.functions.dialog_wrapper import create_progress_dialog from sn_basis.functions.message_wrapper import warning from sn_basis.functions.qgiscore_wrapper import ( QgsVectorLayer, QgsFeature, QgsField, QgsGeometry, polygon_zu_linie, ) from sn_basis.functions.qt_wrapper import QVariant # --------------------------------------------------------------------------- # Konstanten # --------------------------------------------------------------------------- #: Anzeigename des Ergebnis-Layers. ERGEBNIS_LAYER_NAME = "Umringstatistik Grenzpunkte" #: Feldname für die Klassifikationskategorie im Ergebnis-Layer. FELD_KATEGORIE = "kategorie" #: Feldname für ``gp_festgestellterGrenzpunkt`` im Grenzpunkte(ALKIS)-Layer. FELD_FESTGESTELLT = "gp_festgestellterGrenzpunkt" #: Feldname für das Genauigkeitsattribut im Grenzpunkte(ALKIS)-Layer. FELD_GENAUIGKEIT = "qualitaetsangaben_ax_dqpunktort_genauigkeitsstufe" #: Feldname für die GML-ID im Grenzpunkte(ALKIS)-Layer. FELD_GML_ID = "gml_id" #: Kategoriewert: festgestellter Grenzpunkt. KATEGORIE_FGP = "FGP" #: Kategoriewert: Lagegenauigkeit ≤ 3 cm (Gütestufe 2100). KATEGORIE_KLEIN = "<3cm" #: Kategoriewert: Lagegenauigkeit > 3 cm (schlechtere Gütestufe). KATEGORIE_GROSS = ">3cm" #: Kategoriewert: Kategorie konnte nicht bestimmt werden. KATEGORIE_UNBEKANNT = "unbekannt" #: Maximaler Abstand (m) eines Punktes vom Umring, um als „auf dem Umring" zu gelten. UMRING_TOLERANZ_M: float = 0.01 #: Grenzwert für ``qualitaetsangaben_ax_dqpunktort_genauigkeitsstufe`` (3-cm-Schwelle). GENAUIGKEIT_GRENZWERT: int = 2100 # --------------------------------------------------------------------------- # Interne Hilfsfunktionen # --------------------------------------------------------------------------- def _setze_kategorien_symbologie(layer) -> None: """Wendet Kategoriesymbologie an: grün / gelb / rot / grau. Wird bei fehlendem QGIS-Kontext still ignoriert. """ try: from qgis.core import ( # type: ignore[import] QgsCategorizedSymbolRenderer, QgsRendererCategory, QgsMarkerSymbol, ) def _marker(color: str, size: str = "2.5") -> object: return QgsMarkerSymbol.createSimple( {"color": color, "size": size, "outline_style": "no"} ) categories = [ QgsRendererCategory( KATEGORIE_FGP, _marker("0,180,0,255"), f"{KATEGORIE_FGP} – festgestellter Grenzpunkt", ), QgsRendererCategory( KATEGORIE_KLEIN, _marker("255,220,0,255"), f"{KATEGORIE_KLEIN} – Gütestufe {GENAUIGKEIT_GRENZWERT}", ), QgsRendererCategory( KATEGORIE_GROSS, _marker("220,0,0,255"), f"{KATEGORIE_GROSS} – Gütestufe > {GENAUIGKEIT_GRENZWERT}", ), QgsRendererCategory( KATEGORIE_UNBEKANNT, _marker("128,128,128,255"), f"{KATEGORIE_UNBEKANNT} – Kategorie nicht bestimmbar", ), ] renderer = QgsCategorizedSymbolRenderer(FELD_KATEGORIE, categories) layer.setRenderer(renderer) except Exception: pass # Kein QGIS-Kontext (Tests) → Standarddarstellung behalten def _klassifiziere(feat) -> str: """Bestimmt die Kategorie eines Grenzpunkts anhand seiner Attribute. Parameters ---------- feat : Feature aus dem Grenzpunkte(ALKIS)-Layer. Returns ------- str Einer der Kategorienwerte :data:`KATEGORIE_FGP`, :data:`KATEGORIE_KLEIN`, :data:`KATEGORIE_GROSS` oder :data:`KATEGORIE_UNBEKANNT`. """ # festgestellterGrenzpunkt try: festgestellt_raw = feat[FELD_FESTGESTELLT] festgestellt = str(festgestellt_raw).lower().strip() == "true" if festgestellt_raw else False except Exception: festgestellt = False if festgestellt: return KATEGORIE_FGP # Genauigkeitsstufe auslesen try: gen_raw = feat[FELD_GENAUIGKEIT] genauigkeit = int(str(gen_raw).strip()) if gen_raw else None except (ValueError, TypeError, Exception): genauigkeit = None if genauigkeit is None: return KATEGORIE_UNBEKANNT if genauigkeit == GENAUIGKEIT_GRENZWERT: return KATEGORIE_KLEIN if genauigkeit > GENAUIGKEIT_GRENZWERT: return KATEGORIE_GROSS # Genauigkeit < 2100 (besser als 3 cm, aber nicht festgestellt) return KATEGORIE_UNBEKANNT # --------------------------------------------------------------------------- # Öffentliche API # --------------------------------------------------------------------------- def umringstatistik_grenzpunkte( tab_widget, grenzpunkte_layer, vg_layer, ) -> Optional[object]: """Klassifiziert Grenzpunkte auf dem VG-Umring nach Lagegenauigkeit. Ablauf: 1. VG-Geometrie aus ``vg_layer`` berechnen → Umringslinie. 2. Alle Features aus ``grenzpunkte_layer`` auf Abstand ≤ :data:`UMRING_TOLERANZ_M` zum Umring prüfen. 3. Treffer klassifizieren (FGP / <3cm / >3cm / unbekannt). 4. Ergebnis-Memory-Layer aufbauen und Kategoriesymbologie setzen. 5. Layer ins Projekt einfügen, Zusammenfassungs-MessageBox anzeigen. Parameters ---------- tab_widget : Eltern-Widget für Dialoge. grenzpunkte_layer : Geladener ``Grenzpunkte (ALKIS)``-Layer mit Attributen ``gp_festgestellterGrenzpunkt`` und ``qualitaetsangaben_ax_dqpunktort_genauigkeitsstufe``. vg_layer : Verfahrensgebiet-Layer (Polygon, EPSG:25833). Returns ------- QgsVectorLayer | None Ergebnis-Memory-Layer oder ``None`` bei Fehler/Abbruch. """ project = QgsProject.instance() prog = create_progress_dialog(5, "Umringstatistik Grenzpunkte", "Schritt 1/5: VG-Umring berechnen …") abgeschlossen = False try: prog.set_value(0) if prog.is_canceled(): return None # ------------------------------------------------------------------ # Schritt 1: VG-Umring berechnen # ------------------------------------------------------------------ feats_vg = [f for f in vg_layer.getFeatures() if f.hasGeometry() and not f.geometry().isEmpty()] if not feats_vg: prog.close() warning("Umringstatistik", "Verfahrensgebiet-Layer enthält keine Geometrien.", duration=6) return None vg_geom = QgsGeometry.unaryUnion([f.geometry() for f in feats_vg]) if vg_geom is None or vg_geom.isEmpty(): prog.close() warning("Umringstatistik", "Verfahrensgebiet-Geometrie konnte nicht vereint werden.", duration=6) return None umring_geom = polygon_zu_linie(vg_geom) if umring_geom is None or umring_geom.isEmpty(): prog.close() warning("Umringstatistik", "Umringslinie konnte aus der VG-Geometrie nicht extrahiert werden.", duration=6) return None prog.set_value(1) if prog.is_canceled(): return None # ------------------------------------------------------------------ # Schritt 2: Grenzpunkte auf dem Umring filtern # ------------------------------------------------------------------ prog.set_label("Schritt 2/5: Grenzpunkte auf Umring prüfen …") n_gesamt = grenzpunkte_layer.featureCount() treffer: list = [] for i, feat in enumerate(grenzpunkte_layer.getFeatures()): if i % 200 == 0: prog.set_label( f"Schritt 2/5: Grenzpunkte prüfen … ({i}/{n_gesamt})" ) if prog.is_canceled(): return None if not feat.hasGeometry() or feat.geometry().isEmpty(): continue try: dist = feat.geometry().distance(umring_geom) except Exception: continue if dist <= UMRING_TOLERANZ_M: treffer.append(feat) prog.set_value(2) if prog.is_canceled(): return None if not treffer: prog.close() QMessageBox.information( tab_widget, "Umringstatistik Grenzpunkte", "Es wurden keine Grenzpunkte auf dem Umring des Verfahrensgebiets gefunden.\n\n" f"Geprüft: {n_gesamt} Grenzpunkte\n" f"Toleranz: {UMRING_TOLERANZ_M} m", ) return None # ------------------------------------------------------------------ # Schritt 3: Klassifizieren # ------------------------------------------------------------------ prog.set_label(f"Schritt 3/5: {len(treffer)} Treffer klassifizieren …") n_fgp = 0 n_klein = 0 n_gross = 0 n_unbekannt = 0 klassifiziert: list = [] # [(feat, kategorie), ...] for feat in treffer: kat = _klassifiziere(feat) klassifiziert.append((feat, kat)) if kat == KATEGORIE_FGP: n_fgp += 1 elif kat == KATEGORIE_KLEIN: n_klein += 1 elif kat == KATEGORIE_GROSS: n_gross += 1 else: n_unbekannt += 1 prog.set_value(3) if prog.is_canceled(): return None # ------------------------------------------------------------------ # Schritt 4: Ergebnis-Layer aufbauen # ------------------------------------------------------------------ prog.set_label("Schritt 4/5: Ergebnis-Layer aufbauen …") crs_authid = vg_layer.crs().authid() or "EPSG:25833" result_layer = QgsVectorLayer( f"Point?crs={crs_authid}", ERGEBNIS_LAYER_NAME, "memory" ) pr = result_layer.dataProvider() felder = [ QgsField(FELD_KATEGORIE, QVariant.String), QgsField(FELD_GML_ID, QVariant.String), QgsField(FELD_FESTGESTELLT, QVariant.String), QgsField(FELD_GENAUIGKEIT, QVariant.String), ] pr.addAttributes(felder) result_layer.updateFields() neue_features = [] for feat, kat in klassifiziert: new_f = QgsFeature(result_layer.fields()) new_f.setGeometry(feat.geometry()) new_f[FELD_KATEGORIE] = kat for src_feld in (FELD_GML_ID, FELD_FESTGESTELLT, FELD_GENAUIGKEIT): try: new_f[src_feld] = feat[src_feld] except Exception: pass neue_features.append(new_f) pr.addFeatures(neue_features) result_layer.updateExtents() _setze_kategorien_symbologie(result_layer) prog.set_value(4) if prog.is_canceled(): return None # ------------------------------------------------------------------ # Schritt 5: Layer ins Projekt + Ergebnis-MessageBox # ------------------------------------------------------------------ prog.set_label("Schritt 5/5: Layer ins Projekt einfügen …") # Bestehenden Ergebnis-Layer ggf. ersetzen vorhandene = project.mapLayersByName(ERGEBNIS_LAYER_NAME) for lyr in vorhandene: project.removeMapLayer(lyr.id()) project.addMapLayer(result_layer) prog.set_value(5) abgeschlossen = True prog.close() n_umring = len(treffer) QMessageBox.information( tab_widget, "Umringstatistik Grenzpunkte", f"Grenzpunkte auf dem Umring: {n_umring}\n" f"\n" f" FGP (festgestellt): {n_fgp}\n" f" < 3 cm (Gütestufe {GENAUIGKEIT_GRENZWERT}): {n_klein}\n" f" > 3 cm (schlechtere Güte): {n_gross}\n" f" Unbekannt: {n_unbekannt}\n" f"\n" f"(Geprüft: {n_gesamt} Grenzpunkte, Toleranz: {UMRING_TOLERANZ_M} m)", ) return result_layer finally: if not abgeschlossen: prog.close()