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>
158 lines
6.5 KiB
Python
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
|