verfahrensgebiet aus Flst
This commit is contained in:
@@ -0,0 +1,280 @@
|
||||
"""sn_verfahrensgebiet/functions/grenzpunkt_alkis.py – ALKIS-Grenzpunktabfrage.
|
||||
|
||||
Lädt Grenzpunkte aus dem ALKIS-NAS-WFS in zwei Schritten:
|
||||
|
||||
1. ``adv:AX_PunktortTA`` per BBox laden.
|
||||
Diese Objektart enthält die eigentliche Koordinate (Geometrie) sowie das
|
||||
Attribut ``gehoertZu``, das per xlink-Referenz auf die GML-ID des
|
||||
zugehörigen ``AX_Grenzpunkt``-Objekts verweist.
|
||||
|
||||
2. GML-IDs aus ``gehoertZu`` extrahieren → ``AX_Grenzpunkt`` per
|
||||
``gml_id IN (...)`` abfragen (batching, ~25 IDs/Batch).
|
||||
|
||||
3. Beide Layer werden über ``gehoertZu ↔ gml_id`` verknüpft (Join) und als
|
||||
einzelner angereicherter Layer zurückgegeben.
|
||||
|
||||
**Einschränkungen des WFS-Dienstes:**
|
||||
|
||||
- ``AX_Grenzpunkt`` hat **keine eigene Geometrie** → kein BBOX-Filter möglich.
|
||||
- Das Attribut ``gehoertZu`` in ``AX_PunktortTA`` ist serverseitig **nicht
|
||||
filterbar** (bestätigt durch WFS-Fehlermeldung ``ErrorInPropertyName``).
|
||||
- Der serverseitige GML-ID-Filter (``ResourceId``) ist aktiviert
|
||||
(``ImplementsResourceId=TRUE`` in den Capabilities).
|
||||
|
||||
**Strategie:** BBox → PunktortTA → GML-IDs → Grenzpunkt-Batches →
|
||||
client-seitiger Join.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
from sn_basis.functions import QgsVectorLayer, QgsProject
|
||||
from sn_basis.functions import QMessageBox
|
||||
from sn_basis.functions.message_wrapper import info, warning
|
||||
|
||||
ALKIS_NAS_URL = "https://geodienste.sachsen.de/aaa/public_alkis/nas/wfs"
|
||||
PUNKTORT_TA_TYPENAME = "adv:AX_PunktortTA"
|
||||
GRENZPUNKT_TYPENAME = "adv:AX_Grenzpunkt"
|
||||
|
||||
PUNKTORT_LAYER_NAME = "PunktortTA"
|
||||
GRENZPUNKT_LAYER_NAME = "Grenzpunkte"
|
||||
|
||||
# Attributname in AX_PunktortTA, der auf AX_Grenzpunkt verweist
|
||||
_ATTR_GEHOERT_ZU = "gehoertZu"
|
||||
# Attributname für die GML-ID in AX_Grenzpunkt
|
||||
_ATTR_GML_ID = "gml_id"
|
||||
|
||||
# Batch-Größe für ResourceId-Abfragen (URL-Längenlimit)
|
||||
_BATCH_GROESSE = 25
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Interne Hilfsfunktionen
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _extrahiere_gml_ids(punktort_layer) -> List[str]:
|
||||
"""Extrahiert GML-IDs aus dem ``gehoertZu``-Attribut des PunktortTA-Layers.
|
||||
|
||||
Der WFS gibt ``gehoertZu`` üblicherweise als ``xlink:href``-Referenz im
|
||||
Format ``urn:adv:oid:DEXXXXXXXXXXXXXXXXXX`` oder als flachen Wert.
|
||||
Es wird der letzte Bestandteil nach dem letzten ``:`` verwendet.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
punktort_layer :
|
||||
Geladener ``AX_PunktortTA``-WFS-Layer.
|
||||
|
||||
Returns
|
||||
-------
|
||||
List[str]
|
||||
Deduplizierte, sortierte Liste von GML-IDs.
|
||||
"""
|
||||
ids: set[str] = set()
|
||||
for feat in punktort_layer.getFeatures():
|
||||
try:
|
||||
val = feat[_ATTR_GEHOERT_ZU]
|
||||
if val:
|
||||
raw = str(val).strip()
|
||||
# xlink:href hat Format "urn:adv:oid:DE..." oder reiner GML-ID-Wert
|
||||
gml_id = raw.rsplit(":", 1)[-1] if ":" in raw else raw
|
||||
if gml_id:
|
||||
ids.add(gml_id)
|
||||
except (KeyError, Exception):
|
||||
continue
|
||||
return sorted(ids)
|
||||
|
||||
|
||||
def _lade_grenzpunkte_per_ids(id_liste: List[str]) -> Optional[object]:
|
||||
"""Lädt ``AX_Grenzpunkt``-Features für die angegebenen GML-IDs.
|
||||
|
||||
Die ID-Liste wird in Batches der Größe :data:`_BATCH_GROESSE` aufgeteilt,
|
||||
um URL-Längenlimits zu umgehen. Alle Batch-Ergebnisse werden zu einem
|
||||
Memory-Layer zusammengeführt.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
id_liste :
|
||||
GML-IDs der gesuchten ``AX_Grenzpunkt``-Objekte.
|
||||
|
||||
Returns
|
||||
-------
|
||||
QgsVectorLayer | None
|
||||
Memory-Layer mit allen gefundenen Grenzpunkten oder ``None`` bei Fehler.
|
||||
"""
|
||||
from sn_basis.functions import QgsFeature, QgsField
|
||||
from sn_basis.functions.qt_wrapper import QVariant
|
||||
|
||||
alle_features: List = []
|
||||
feld_namen: List[str] = []
|
||||
|
||||
anzahl_batches = math.ceil(len(id_liste) / _BATCH_GROESSE)
|
||||
for i in range(anzahl_batches):
|
||||
batch = id_liste[i * _BATCH_GROESSE: (i + 1) * _BATCH_GROESSE]
|
||||
ids_str = ", ".join(f"'{gid}'" for gid in batch)
|
||||
uri = (
|
||||
f"url='{ALKIS_NAS_URL}' typename='{GRENZPUNKT_TYPENAME}' "
|
||||
f"srsname='EPSG:25833' "
|
||||
f"sql=SELECT * FROM AX_Grenzpunkt WHERE gml_id IN ({ids_str})"
|
||||
)
|
||||
tmp = QgsVectorLayer(uri, "_TmpGP", "WFS")
|
||||
if not tmp or not tmp.isValid():
|
||||
continue
|
||||
|
||||
if not feld_namen:
|
||||
feld_namen = [f.name() for f in tmp.fields()]
|
||||
|
||||
for feat in tmp.getFeatures():
|
||||
alle_features.append(feat)
|
||||
|
||||
if not alle_features:
|
||||
return None
|
||||
|
||||
# Ergebnis-Memory-Layer aufbauen (ohne Geometrie, da AX_Grenzpunkt keine hat)
|
||||
mem_uri = "Point?crs=EPSG:25833" if _hat_geometrie(alle_features[0]) else "none"
|
||||
result_layer = QgsVectorLayer(mem_uri, GRENZPUNKT_LAYER_NAME, "memory")
|
||||
pr = result_layer.dataProvider()
|
||||
|
||||
felder = [QgsField(n, QVariant.String) for n in feld_namen if n]
|
||||
pr.addAttributes(felder)
|
||||
result_layer.updateFields()
|
||||
pr.addFeatures(alle_features)
|
||||
result_layer.updateExtents()
|
||||
|
||||
return result_layer
|
||||
|
||||
|
||||
def _hat_geometrie(feature) -> bool:
|
||||
"""Gibt ``True`` zurück, wenn das Feature eine nicht-leere Geometrie hat."""
|
||||
try:
|
||||
g = feature.geometry()
|
||||
return g is not None and not g.isEmpty()
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Öffentliche API
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def grenzpunkt_alkis(tab_widget, extent=None) -> Tuple[Optional[object], Optional[object]]:
|
||||
"""Lädt Grenzpunkte aus dem ALKIS-WFS für den angegebenen Begrenzungsrahmen.
|
||||
|
||||
Ablauf:
|
||||
|
||||
1. ``AX_PunktortTA`` per BBox laden (enthält Koordinaten + Referenz auf
|
||||
``AX_Grenzpunkt`` via ``gehoertZu``).
|
||||
2. GML-IDs aus ``gehoertZu`` extrahieren.
|
||||
3. ``AX_Grenzpunkt`` per ID-Liste in Batches laden.
|
||||
4. Beide Layer ins Projekt einfügen. Der Caller ist verantwortlich dafür,
|
||||
den Join in der QGIS-Projektkonfiguration einzurichten oder die
|
||||
Verknüpfung client-seitig vorzunehmen.
|
||||
|
||||
Wenn kein ``extent`` übergeben wird, fragt die Funktion nach dem aktuellen
|
||||
Verfahrensgebiet-Layer und nutzt dessen BBox.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
tab_widget :
|
||||
Eltern-Widget für Dialoge.
|
||||
extent :
|
||||
Optional. ``QgsRectangle`` in EPSG:25833. Wenn ``None``, wird der
|
||||
aktive Verfahrensgebiet-Layer gesucht und dessen Extent genutzt.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Tuple[Optional[punktort_layer], Optional[grenzpunkt_layer]]
|
||||
Beide geladenen Layer (bereits im Projekt) oder ``(None, None)``
|
||||
bei Fehler.
|
||||
"""
|
||||
project = QgsProject.instance()
|
||||
|
||||
# BBox bestimmen
|
||||
if extent is None:
|
||||
vg_layers = project.mapLayersByName("Verfahrensgebiet")
|
||||
if not vg_layers:
|
||||
QMessageBox.warning(
|
||||
tab_widget,
|
||||
"Kein Verfahrensgebiet",
|
||||
"Bitte zuerst ein Verfahrensgebiet laden, um die BBox abzuleiten.",
|
||||
)
|
||||
return None, None
|
||||
extent = vg_layers[0].extent()
|
||||
|
||||
xmin = extent.xMinimum()
|
||||
ymin = extent.yMinimum()
|
||||
xmax = extent.xMaximum()
|
||||
ymax = extent.yMaximum()
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Schritt 1: AX_PunktortTA per BBox laden
|
||||
# -----------------------------------------------------------------------
|
||||
info("Grenzpunkte", "Schritt 1/3: PunktortTA per BBox laden …", duration=3)
|
||||
uri_pta = (
|
||||
f"url='{ALKIS_NAS_URL}' typename='{PUNKTORT_TA_TYPENAME}' "
|
||||
f"srsname='EPSG:25833' "
|
||||
f"bbox={xmin},{ymin},{xmax},{ymax},EPSG:25833"
|
||||
)
|
||||
punktort_layer = QgsVectorLayer(uri_pta, PUNKTORT_LAYER_NAME, "WFS")
|
||||
|
||||
if not punktort_layer or not punktort_layer.isValid():
|
||||
QMessageBox.critical(
|
||||
tab_widget,
|
||||
"Fehler",
|
||||
"PunktortTA-Layer konnte nicht geladen werden.\n"
|
||||
"Bitte Netzwerkverbindung und Ausdehnung prüfen.",
|
||||
)
|
||||
return None, None
|
||||
|
||||
anzahl_pta = punktort_layer.featureCount()
|
||||
if anzahl_pta == 0:
|
||||
QMessageBox.warning(
|
||||
tab_widget,
|
||||
"Keine Grenzpunkte",
|
||||
"Im Bereich des Verfahrensgebiets wurden keine PunktortTA-Objekte gefunden.",
|
||||
)
|
||||
return None, None
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Schritt 2: GML-IDs extrahieren
|
||||
# -----------------------------------------------------------------------
|
||||
info("Grenzpunkte", "Schritt 2/3: GML-IDs aus gehoertZu extrahieren …", duration=3)
|
||||
gml_ids = _extrahiere_gml_ids(punktort_layer)
|
||||
|
||||
if not gml_ids:
|
||||
QMessageBox.warning(
|
||||
tab_widget,
|
||||
"Keine Verknüpfungen",
|
||||
f"{anzahl_pta} PunktortTA-Objekte gefunden, aber kein 'gehoertZu'-Attribut.\n"
|
||||
"Bitte Attributnamen im WFS prüfen.",
|
||||
)
|
||||
# PunktortTA trotzdem zurückgeben
|
||||
project.addMapLayer(punktort_layer)
|
||||
return punktort_layer, None
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Schritt 3: AX_Grenzpunkt per ID-Listen-Batches laden
|
||||
# -----------------------------------------------------------------------
|
||||
info(
|
||||
"Grenzpunkte",
|
||||
f"Schritt 3/3: {len(gml_ids)} Grenzpunkte in "
|
||||
f"{math.ceil(len(gml_ids) / _BATCH_GROESSE)} Batches laden …",
|
||||
duration=3,
|
||||
)
|
||||
grenzpunkt_layer = _lade_grenzpunkte_per_ids(gml_ids)
|
||||
|
||||
if not grenzpunkt_layer:
|
||||
warning(
|
||||
"Grenzpunkte",
|
||||
"Grenzpunkt-Daten konnten nicht geladen werden (IDs gefunden, aber WFS lieferte kein Ergebnis).",
|
||||
duration=6,
|
||||
)
|
||||
project.addMapLayer(punktort_layer)
|
||||
return punktort_layer, None
|
||||
|
||||
# Beide Layer ins Projekt
|
||||
project.addMapLayer(punktort_layer)
|
||||
project.addMapLayer(grenzpunkt_layer)
|
||||
|
||||
return punktort_layer, grenzpunkt_layer
|
||||
Reference in New Issue
Block a user