Files
Plugin_SN_Verfahrensgebiet/functions/umringstatistik_grenzpunkte.py
T

374 lines
13 KiB
Python
Raw Normal View History

2026-05-05 21:17:19 +02:00
"""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.
2026-05-06 11:57:50 +02:00
UMRING_TOLERANZ_M: float = 0.01
2026-05-05 21:17:19 +02:00
#: 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()