"""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