"""OGC API Features (``/v2/ogc``) — alle Karten eines Verfahrens als **native, editierbare** QGIS-Layer laden. Seit dem 07.09.2026 bietet die API OGC API Features (Part 1 Core, Part 2 CRS, Part 4 CRUD) unter ``https://api.flurneuordnung-sachsen.de/v2/ogc``. QGIS kann das ohne Plugin: der Datenanbieter ``OAPIF`` liest die Sammlungen, und im Bearbeitungsmodus schickt QGIS jede Änderung als eigene Anfrage (POST/PUT/PATCH/DELETE je Feature) — kein Vollersetzen mehr, kein Geometrie-Splitting, kein Memory-Layer. Dieses Modul zeigt den Weg aus PyQGIS: 1. **Authentifizierung**: nach dem Login (``POST /person/login``, userauth = API-Key) wird eine QGIS-Authentifizierungskonfiguration der Methode ``APIHeader`` angelegt, die den Header ``X-API-Key`` mitschickt. Das Passwort landet nie in QGIS. Alternative ohne Plugin: Methode ``Basic`` mit dem Manager-Login (E-Mail + Passwort) — die API nimmt beides. 2. **Sammlungen** des Verfahrens holen (``GET /ogc/collections?vkz=…``) — die API listet nur, was der Nutzer sehen darf (Vorsitz/Kassenbetreuer/Stellvertreter der TG; Verband alles). 3. Je Sammlung einen ``QgsVectorLayer(uri, titel, "OAPIF")`` in einer Layer-Gruppe anlegen. Sammlungs-IDs: ``-`` (umringe, p41, st, kas, we, ngg, biotop = jüngster Stand wie im Web-GIS), ``vorerhebung--`` (je Vorerhebungsprojekt, schreibbar nur im Status „bearbeitung"), ``massnahmeblatt-`` (je Lage-Geometrie eines Maßnahmenblatts ein Feature, Blatt-Spalten als ``blatt_*``-Attribute; neue Features brauchen ``blatt_uuid`` oder ``blatt_typ`` + ``blatt_name``). """ from qgis.PyQt.QtCore import QSettings from qgis.core import ( Qgis, QgsApplication, QgsAuthMethodConfig, QgsDataSourceUri, QgsProject, QgsVectorLayer, ) from .api_client import ApiError AUTHCFG_NAME = "VLN Karten (API-Key)" SETTINGS_AUTHCFG = "ogc_authcfg" #: Reihenfolge und Gruppen im Layerbaum (Sammlungs-Präfix -> Gruppenname). GRUPPEN = [ ("vorerhebung", "Vorerhebung"), ("massnahmeblatt", "Maßnahmenblätter"), ("umringe", "Verfahren"), ("p41", "Verfahren"), ("ngg", "Verfahren"), ("st", "Verfahren"), ("kas", "Verfahren"), ("we", "Verfahren"), ("biotop", "Verfahren"), ] def ogc_url(base_url): """Landing Page der OGC API aus der API-Basis (``…/v2`` -> ``…/v2/ogc``).""" return base_url.rstrip("/") + "/ogc" def authcfg_sicherstellen(api_key, settings_group): """Authentifizierungskonfiguration ``APIHeader`` (Header ``X-API-Key``) anlegen oder aktualisieren; ihre ID wird in den Plugin-Einstellungen gemerkt. QGIS verlangt dafür ein Master-Passwort für seine Authentifizierungsdatenbank — beim ersten Mal fragt QGIS danach. Ohne Konfiguration (Abbruch) gibt es ``None`` zurück; dann fällt der Aufrufer auf die Basic-Anmeldung zurück, die QGIS beim ersten 401 selbst abfragt. """ manager = QgsApplication.authManager() if manager is None or manager.isDisabled(): return None settings = QSettings() settings.beginGroup(settings_group) bekannt = settings.value(SETTINGS_AUTHCFG, "") settings.endGroup() cfg = QgsAuthMethodConfig() if bekannt and manager.loadAuthenticationConfig(bekannt, cfg, True) and cfg.isValid(): cfg.setConfigMap({"X-API-Key": api_key}) if not manager.updateAuthenticationConfig(cfg): return None return bekannt cfg = QgsAuthMethodConfig("APIHeader") cfg.setName(AUTHCFG_NAME) cfg.setConfigMap({"X-API-Key": api_key}) if not manager.storeAuthenticationConfig(cfg, True): return None settings.beginGroup(settings_group) settings.setValue(SETTINGS_AUTHCFG, cfg.id()) settings.endGroup() return cfg.id() def collections_laden(client, vkz): """``GET /ogc/collections?vkz=…`` -> Liste ``[{id, title, description, storageCrs, …}]``.""" data = client._request("GET", "/ogc/collections?vkz=%s" % vkz) rows = data.get("collections") if isinstance(data, dict) else None if not isinstance(rows, list): raise ApiError("Antwort von /ogc/collections hat ein unerwartetes Format.") return rows def layer_uri(base_url, cid, authcfg=None, storage_crs=None, api_key=None): """Die OAPIF-Datenquelle: Landing-URL + Sammlungs-ID (+ Authentifizierung). Ohne ``authcfg`` (keine Authentifizierungs-DB bzw. Master-Passwort abgelehnt) tragen Benutzer ``apikey`` + der API-Key als Passwort die Anmeldung — die API nimmt den Key auch als HTTP-Basic-Passwort. Achtung: dann steht der Key in der Datenquelle und damit in einer gespeicherten Projektdatei; die Authentifizierungs-DB ist der sichere Weg. ``srsname`` auf das Speicher-CRS der Sammlung: EPSG:25833 für die Verfahrens-Layer, CRS84 (= EPSG:4326 lon/lat) für Vorerhebung und Maßnahmenblätter — so rechnet QGIS nichts hin und her, und die Koordinaten bleiben, wie die API sie speichert. """ uri = QgsDataSourceUri() uri.setParam("url", ogc_url(base_url)) uri.setParam("typename", cid) uri.setParam("pagingEnabled", "true") uri.setParam("pageSize", "1000") uri.setParam("restrictToRequestBBOX", "1") if storage_crs and "25833" in storage_crs: uri.setParam("srsname", "EPSG:25833") if authcfg: uri.setAuthConfigId(authcfg) elif api_key: uri.setUsername("apikey") uri.setPassword(api_key) return uri.uri(False) def layer_hinzufuegen(iface, base_url, vkz, vkz_name, collections, authcfg=None, api_key=None): """Je Sammlung einen OAPIF-Layer anlegen, gruppiert unter „VLN ". :returns: ``(geladen, fehler)`` — Anzahl Layer und Liste ``(cid, Meldung)``. """ projekt = QgsProject.instance() wurzel = projekt.layerTreeRoot() gruppe_name = "VLN %s %s" % (vkz, vkz_name or "") gruppe = wurzel.findGroup(gruppe_name.strip()) if gruppe is None: gruppe = wurzel.insertGroup(0, gruppe_name.strip()) untergruppen = {} geladen, fehler = 0, [] for coll in collections: cid = str(coll.get("id") or "") praefix = cid.split("-", 1)[0] gruppen_name = dict(GRUPPEN).get(praefix, "Weitere") if gruppen_name not in untergruppen: untergruppen[gruppen_name] = gruppe.findGroup(gruppen_name) or gruppe.addGroup(gruppen_name) titel = str(coll.get("title") or cid) layer = QgsVectorLayer(layer_uri(base_url, cid, authcfg, coll.get("storageCrs"), api_key), titel, "OAPIF") if not layer.isValid(): fehler.append((cid, layer.error().message() if layer.error() else "Layer ungültig")) continue projekt.addMapLayer(layer, False) untergruppen[gruppen_name].addLayer(layer) geladen += 1 if geladen: iface.messageBar().pushMessage( "VLN Karten", "%d Layer aus der OGC API geladen — zum Bearbeiten den Layer in den " "Bearbeitungsmodus schalten; jede gespeicherte Änderung geht sofort an die API." % geladen, level=Qgis.Success, duration=8) return geladen, fehler