Files
Plugin_SN_Verfahrensgebiet/functions/gemarkung_alkis.py
T
2026-04-22 14:53:45 +02:00

208 lines
7.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""sn_verfahrensgebiet/functions/gemarkung_alkis.py – ALKIS-Gemarkungsabfrage.
Stellt Funktionen bereit, um Katasterbezirk-Objekte aus dem sächsischen
ALKIS-vereinfachten WFS zu laden und daraus Gemarkungsschlüssel zu extrahieren,
die als Vorschlag für die Flurstücksabfrage (Phase 2) dienen.
**Dienst:** ``https://geodienste.sachsen.de/aaa/public_alkis/vereinf/wfs``
**Typename:** ``Katasterbezirk``
Dieser Dienst hat – im Gegensatz zu ``adv:AX_Gemarkung`` aus dem NAS-Dienst –
tatsächliche Geometrien. Die client-seitige Verschneidung mit der VG-Geometrie
ist damit möglich.
**Attributkonvention:**
- ``schluessel``: vollständiger Katasterbezirksschlüssel (z.B. ``140640001``),
die letzten 4 Stellen entsprechen der
``gemarkung_ax_gemarkung_schluessel_gemarkungsnummer`` im NAS-Flurstücksdienst.
- ``ave_name``: Gemarkungsbezeichnung.
**Wichtig:** Diese Funktion stellt keine Nutzerabfragen. Sie gibt im
Fehlerfall eine leere Liste zurück.
"""
from __future__ import annotations
from typing import List, Optional, Tuple
from sn_basis.functions import QgsVectorLayer, QgsGeometry
from sn_basis.functions.message_wrapper import warning as _warn, info as _info
ALKIS_VEREINF_URL = "https://geodienste.sachsen.de/aaa/public_alkis/vereinf/wfs"
KATASTERBEZIRK_TYPENAME = "ave:KatasterBezirk"
# Attributnamen im vereinfachten Dienst
_ATTR_SCHLUESSEL = "schluessel" # vollständiger Schlüssel, letzte 4 = Gemarkungsnummer
_ATTR_NAME = "ave_name"
# ---------------------------------------------------------------------------
# Interne Hilfsfunktionen
# ---------------------------------------------------------------------------
def _extract_schluessel_und_name(feature) -> Tuple[str, str]:
"""Extrahiert Gemarkungsnummer (letzte 4 Stellen von ``schluessel``)
und Namen (``ave_name``) aus einem Katasterbezirk-Feature.
Returns
-------
Tuple[str, str]
``(gemarkungsnummer_4stellig, name)`` – leer wenn nicht ermittelbar.
"""
def _get(field: str) -> str:
try:
val = feature[field]
return str(val).strip() if val else ""
except (KeyError, Exception):
return ""
schluessel = _get(_ATTR_SCHLUESSEL)
name = _get(_ATTR_NAME)
# Letzte 4 Stellen des Schlüssels = Gemarkungsnummer
nummer = schluessel[-4:] if len(schluessel) >= 4 else schluessel
return nummer, name
def _load_all_gemarkung_layer():
"""Lädt alle Katasterbezirk-Objekte aus dem vereinfachten ALKIS-WFS.
Sachsen hat ≈ 2 000 Gemarkungen. Der vereinfachte Dienst liefert
tatsächliche Geometrien, sodass client-seitige Verschneidung möglich ist.
Returns
-------
QgsVectorLayer or None
Geladener Layer oder ``None`` bei Ungültigkeit.
"""
uri = (
f"url='{ALKIS_VEREINF_URL}' typename='{KATASTERBEZIRK_TYPENAME}' "
f"srsname='EPSG:25833'"
)
layer = QgsVectorLayer(uri, "_TempKatasterbezirk", "WFS")
return layer if (layer and layer.isValid()) else None
# ---------------------------------------------------------------------------
# Öffentliche API
# ---------------------------------------------------------------------------
def gemarkung_schluessel_und_namen_von_vg_geometrie(
vg_geometrie,
) -> List[Tuple[str, str]]:
"""Gibt Gemarkungsschlüssel und -namen aller Gemarkungen zurück, die die
VG-Geometrie schneiden.
Da der WFS-Dienst keine serverseitigen BBox- oder Geometriefilter
unterstützt, werden alle ``AX_Gemarkung``-Objekte geladen und client-seitig
per ``QgsGeometry.intersects`` gefiltert.
Der Aufruf ist *ohne Nutzerinteraktion* – im Fehlerfall wird eine leere
Liste zurückgegeben, damit der aufrufende Workflow manuellen Fallback
ermöglicht.
Parameters
----------
vg_geometrie :
``QgsGeometry`` der Verfahrensgebiets-Union (aus den Roh-WFS-Features
von ``AX_BauRaumOderBodenordnungsrecht``). In EPSG:25833.
Returns
-------
List[Tuple[str, str]]
Liste von ``(gemarkungsnummer, bezeichnung)``-Tupeln, aufsteigend
sortiert nach Nummer. Enthält keine Duplikate. Leer bei Fehler.
Examples
--------
>>> from qgis.core import QgsGeometry
>>> vg_geom = QgsGeometry.fromWkt("POLYGON((414000 5657000, 415000 5657000, ...))")
>>> result = gemarkung_schluessel_und_namen_von_vg_geometrie(vg_geom)
>>> # result: [('1234', 'Beispielgemarkung'), ('5678', 'Andere Gemarkung')]
"""
if vg_geometrie is None or vg_geometrie.isEmpty():
_warn("Gemarkung", "[DIAG] VG-Geometrie ist None oder leer – Gemarkungssuche abgebrochen.", duration=8)
return []
try:
layer = _load_all_gemarkung_layer()
except Exception as exc:
_warn("Gemarkung", f"[DIAG] Fehler beim Laden des Gemarkung-Layers: {exc}", duration=8)
return []
if not layer:
_warn("Gemarkung", "[DIAG] AX_Gemarkung-Layer ungültig oder nicht ladbar.", duration=8)
return []
# Feature-Anzahl ermitteln (0 = WFS gibt nichts zurück)
alle_features = list(layer.getFeatures())
if not alle_features:
_warn(
"Gemarkung",
"[DIAG] AX_Gemarkung-Layer ist gültig, aber enthält 0 Features. "
"WFS-Dienst liefert ohne Filter möglicherweise kein Ergebnis.",
duration=10,
)
return []
# Feldnamen der ersten Features loggen
feldnamen = [alle_features[0].fields().field(i).name() for i in range(alle_features[0].fields().count())]
hat_geom = sum(1 for f in alle_features[:20] if f.hasGeometry() and not f.geometry().isEmpty())
_info(
"Gemarkung",
f"[DIAG] {len(alle_features)} Gemarkungen geladen. "
f"Von ersten 20 haben {hat_geom} Geometrie. "
f"Alle Felder ({len(feldnamen)}): {feldnamen}",
duration=15,
)
result: dict[str, str] = {}
kein_schnitt = 0
for feat in alle_features:
try:
gem_geom = feat.geometry()
if gem_geom is None or gem_geom.isEmpty():
continue
if not gem_geom.intersects(vg_geometrie):
kein_schnitt += 1
continue
except Exception as exc:
_warn("Gemarkung", f"[DIAG] intersects() Fehler: {exc}", duration=8)
continue
nummer, name = _extract_schluessel_und_name(feat)
if nummer and nummer not in result:
result[nummer] = name
if not result:
_warn(
"Gemarkung",
f"[DIAG] 0 Gemarkungen nach Verschneidung. {kein_schnitt} Features ohne Schnitt. "
f"VG-Geometrie gültig: {not vg_geometrie.isNull()}, "
f"VG-Typ: {vg_geometrie.type()}, VG-WKT (Anfang): {vg_geometrie.asWkt()[:80]}",
duration=15,
)
return sorted(result.items(), key=lambda t: t[0])
def gemarkung_schluessel_von_vg_geometrie(vg_geometrie) -> List[str]:
"""Gibt ausschließlich die Gemarkungsnummern für die VG-Geometrie zurück.
Convenience-Wrapper um
:func:`gemarkung_schluessel_und_namen_von_vg_geometrie`.
Parameters
----------
vg_geometrie :
``QgsGeometry`` des Verfahrensgebiets in EPSG:25833.
Returns
-------
List[str]
Sortierte Liste der Gemarkungsnummern. Leer bei Fehler oder keinem Treffer.
"""
return [nr for nr, _ in gemarkung_schluessel_und_namen_von_vg_geometrie(vg_geometrie)]