From a829716810cda866ff9a9288d6b9ef816c87f11d Mon Sep 17 00:00:00 2001 From: daniel Date: Tue, 5 May 2026 21:17:19 +0200 Subject: [PATCH] =?UTF-8?q?Statisitik=20Umringsgrenzpunkte=20erg=C3=A4nzt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- functions/grenzpunkt_alkis.py | 27 +- functions/umringstatistik_grenzpunkte.py | 373 +++++++++++++++++++++++ ui/tabs/working_tab.py | 47 ++- 3 files changed, 443 insertions(+), 4 deletions(-) create mode 100644 functions/umringstatistik_grenzpunkte.py diff --git a/functions/grenzpunkt_alkis.py b/functions/grenzpunkt_alkis.py index 452e5f9..0885cd7 100644 --- a/functions/grenzpunkt_alkis.py +++ b/functions/grenzpunkt_alkis.py @@ -227,6 +227,7 @@ def _parse_alkis_gml_memory_layer( feature_local_name: str, layer_name: str, geom_prop: Optional[str] = "position", + extract_qualitaet: bool = False, ) -> Optional[object]: """Parst ALKIS-GML-Bytes und erstellt einen QGIS Memory-Layer. @@ -243,6 +244,11 @@ def _parse_alkis_gml_memory_layer( Anzeigename des resultierenden Memory-Layers. geom_prop : Lokaler Name des Geometrie-Attributs (``None`` = kein Geometrie-Layer). + extract_qualitaet : + Wenn ``True``, wird das verschachtelte Attribut + ``qualitaetsangaben_ax_dqpunktort_genauigkeitsstufe`` aus + ``adv:qualitaetsangaben/adv:genauigkeitsstufe`` extrahiert. + Nur für ``AX_PunktortTA`` sinnvoll. Returns ------- @@ -271,12 +277,18 @@ def _parse_alkis_gml_memory_layer( # Felder aus ALLEN Features ermitteln (2-Pass), damit optionale Felder # wie "istTeilVon" nicht verloren gehen, wenn sie im ersten Feature fehlen. + _QUALITAET_CONTAINER = "qualitaetsangaben" + _QUALITAET_FELD = "qualitaetsangaben_ax_dqpunktort_genauigkeitsstufe" feld_namen: List[str] = ["gml_id"] for feat_el in feat_elems: for child in feat_el: local = child.tag.split("}")[-1] if "}" in child.tag else child.tag + if extract_qualitaet and local == _QUALITAET_CONTAINER: + continue # Container wird durch verschachteltes Einzelfeld ersetzt if local not in feld_namen and local != geom_prop: feld_namen.append(local) + if extract_qualitaet and _QUALITAET_FELD not in feld_namen: + feld_namen.append(_QUALITAET_FELD) # Memory-Layer anlegen mem_uri = f"Point?crs=EPSG:25833" if geom_prop else "none" @@ -312,6 +324,15 @@ def _parse_alkis_gml_memory_layer( for child in feat_elem: local = child.tag.split("}")[-1] if "}" in child.tag else child.tag + if extract_qualitaet and local == _QUALITAET_CONTAINER: + # Verschachtelten Genauigkeitswert extrahieren + qual_el = child.find(f".//{{{_ADV}}}genauigkeitsstufe") + if qual_el is not None and qual_el.text: + try: + qgs_feat[_QUALITAET_FELD] = qual_el.text.strip() + except Exception: + pass + continue if local == geom_prop or local not in feld_namen: continue href = child.get(f"{{{_XLINK}}}href", "") @@ -781,7 +802,7 @@ def _lade_punktort_ta_batches_per_gemarkung( duration=4, ) continue - tmp = _parse_alkis_gml_memory_layer(_gml_bytes, "AX_PunktortTA", "_TmpPTA", geom_prop="position") + tmp = _parse_alkis_gml_memory_layer(_gml_bytes, "AX_PunktortTA", "_TmpPTA", geom_prop="position", extract_qualitaet=True) if not tmp: warning( "Grenzpunkte (Diagnose)", @@ -1010,7 +1031,7 @@ def lade_punktort_ta( continue tmp = _parse_alkis_gml_memory_layer( - _gml_bytes, "AX_PunktortTA", "_TmpPTA", geom_prop="position" + _gml_bytes, "AX_PunktortTA", "_TmpPTA", geom_prop="position", extract_qualitaet=True ) if not tmp: continue @@ -1401,7 +1422,7 @@ def grenzpunkte_gemarkungsweise( continue tmp = _parse_alkis_gml_memory_layer( - _gml_bytes, "AX_PunktortTA", "_TmpPTA", geom_prop="position" + _gml_bytes, "AX_PunktortTA", "_TmpPTA", geom_prop="position", extract_qualitaet=True ) if not tmp: continue diff --git a/functions/umringstatistik_grenzpunkte.py b/functions/umringstatistik_grenzpunkte.py new file mode 100644 index 0000000..3fe98e8 --- /dev/null +++ b/functions/umringstatistik_grenzpunkte.py @@ -0,0 +1,373 @@ +"""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.1 + +#: 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() diff --git a/ui/tabs/working_tab.py b/ui/tabs/working_tab.py index 914249e..46332c9 100644 --- a/ui/tabs/working_tab.py +++ b/ui/tabs/working_tab.py @@ -16,8 +16,9 @@ from qgis.utils import iface from sn_verfahrensgebiet.functions.verfahrensgebiet_alkis import LoadStatus from sn_verfahrensgebiet.functions.flurstueck_alkis import flurstueck_alkis from sn_verfahrensgebiet.functions.verfahrensgebiet_alkis_komplett import verfahrensgebiet_alkis_komplett -from sn_verfahrensgebiet.functions.grenzpunkt_alkis import grenzpunkte_gemarkungsweise +from sn_verfahrensgebiet.functions.grenzpunkt_alkis import grenzpunkte_gemarkungsweise, JOINED_LAYER_NAME from sn_verfahrensgebiet.functions.gemarkung_alkis import gemarkung_schluessel_und_namen_von_vg_geometrie +from sn_verfahrensgebiet.functions.umringstatistik_grenzpunkte import umringstatistik_grenzpunkte from sn_verfahrensgebiet.functions.verfahrensgebiet_edit_service import ( pruefe_flurstueck_layer, pruefe_auswahl, @@ -104,6 +105,11 @@ class WorkingTab(QWidget): row_grenz_buttons.addWidget(self.btn_grenz_alkis) layout.addLayout(row_grenz_buttons) + row_umring_buttons = QHBoxLayout() + self.btn_umring_statistik = QPushButton("Umringstatistik Grenzpunkte") + row_umring_buttons.addWidget(self.btn_umring_statistik) + layout.addLayout(row_umring_buttons) + # Abschnitt 3: Verfahrensgebiet erstellen row_verf_erstellen_header = QHBoxLayout() lbl_verf_erstellen = QLabel("Verfahrensgebiet erstellen") @@ -128,6 +134,7 @@ class WorkingTab(QWidget): self.btn_verf_alkis.clicked.connect(self.handle_verf_alkis) self.btn_flurst_alkis.clicked.connect(self.handle_flurst_alkis) self.btn_grenz_alkis.clicked.connect(self.handle_grenz_alkis) + self.btn_umring_statistik.clicked.connect(self.handle_umringstatistik) self.btn_verf_aus_auswahl.clicked.connect(self.handle_verf_aus_auswahl) self.btn_verf_aus_liste.clicked.connect(self.handle_verf_aus_liste) # self.btn_verf_shape.clicked.connect(self.handle_verf_shape) @@ -488,6 +495,44 @@ class WorkingTab(QWidget): finally: self._set_busy(self.btn_grenz_alkis, False, "Grenzpunkte aus ALKIS laden") + def handle_umringstatistik(self) -> None: + """Klassifiziert Grenzpunkte auf dem VG-Umring nach Lagegenauigkeit. + + Sucht den vorhandenen ``Grenzpunkte (ALKIS)``-Layer im Projekt und + kombiniert ihn mit dem gewählten Verfahrensgebiet-Layer, um die + Umringstatistik zu berechnen. + """ + # 1. Grenzpunkte-Layer aus dem Projekt holen + project = QgsProject.instance() + gp_layers = project.mapLayersByName(JOINED_LAYER_NAME) + if not gp_layers: + warning( + "Umringstatistik", + f"Kein Layer '{JOINED_LAYER_NAME}' im Projekt gefunden. " + "Bitte zuerst 'Grenzpunkte aus ALKIS laden' ausführen.", + duration=8, + ) + return + grenzpunkte_layer = gp_layers[0] + + # 2. Verfahrensgebiet-Layer aus Dropdown + vg_layer = self.verf_layer_combo.currentLayer() + if not vg_layer or not vg_layer.isValid(): + warning( + "Umringstatistik", + "Kein Verfahrensgebiet-Layer gewählt. Bitte zuerst ein Verfahrensgebiet laden.", + duration=6, + ) + return + + self._set_busy(self.btn_umring_statistik, True, "Umringstatistik Grenzpunkte") + try: + result_layer = umringstatistik_grenzpunkte(self, grenzpunkte_layer, vg_layer) + if result_layer and result_layer.isValid(): + iface.mapCanvas().refresh() + finally: + self._set_busy(self.btn_umring_statistik, False, "Umringstatistik Grenzpunkte") + # --------------------------------------------------------------------------- # Modul-Hilfsfunktionen