Files
API_Karte_QGISDemo/vln_karten/ogc_layers.py
T
ErikandClaude Opus 4.8 4c52c464dd OGC API Features: alle Layer (Vorerhebung bis Massnahmenblatt) als editierbare OAPIF-Layer
Seit dem 07.09.2026 bietet die API OGC API Features (Part 1/2/3/4) unter /v2/ogc. Neu:
- vln_karten/ogc_layers.py: Auth-Konfiguration "APIHeader" (X-API-Key aus dem Login,
  kein Passwort in QGIS), GET /ogc/collections?vkz=, je Sammlung ein QgsVectorLayer(...,
  "OAPIF") in Gruppen (Vorerhebung / Massnahmenblaetter / Verfahren); Speicher-CRS je
  Sammlung (EPSG:25833 bzw. CRS84).
- plugin.py: Toolbar-Aktion "Alle Layer (OGC API) laden" (run_load_ogc).
- README: Weg ohne Plugin (Datenquellenverwaltung, Basic oder API-Header), Sammlungs-IDs,
  Schreibregeln (in place, Vorerhebung nur "bearbeitung", Massnahmenblatt blatt_*),
  Routenuebersicht; klassischer GeoJSON-Weg bleibt als zweiter Abschnitt.
- metadata: 0.2.0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-07 09:47:55 +02:00

158 lines
6.5 KiB
Python

"""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: ``<layer>-<vkz>`` (umringe, p41, st, kas, we, ngg, biotop = jüngster Stand wie
im Web-GIS), ``vorerhebung-<vkz>-<key>`` (je Vorerhebungsprojekt, schreibbar nur im Status
„bearbeitung"), ``massnahmeblatt-<vkz>`` (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):
"""Die OAPIF-Datenquelle: Landing-URL + Sammlungs-ID (+ Authentifizierung).
``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)
return uri.uri(False)
def layer_hinzufuegen(iface, base_url, vkz, vkz_name, collections, authcfg=None):
"""Je Sammlung einen OAPIF-Layer anlegen, gruppiert unter „VLN <Verfahren>".
: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")), 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