# API_Karte_QGISDemo — Beispiel: Karten-API des VLN Managers aus QGIS ansteuern Dieses Repository zeigt **an einem lauffähigen QGIS-Plugin**, wie die Karten-API des VLN Managers (`https://api.flurneuordnung-sachsen.de/v2`) aus QGIS bzw. PyQGIS angesteuert wird: Anmeldung, Laden der Verfahrens-Layer als editierbare QGIS-Layer und Zurückschreiben der Änderungen. Es liest und schreibt dieselben Daten, die auch das Web-GIS (karte.flurneuordnung-sachsen.de) verwendet. Der Code ist bewusst als **Referenz-Implementierung** gehalten — wer eine eigene Integration (Plugin, Skript, Processing-Werkzeug) bauen will, findet hier alle Bausteine. ## Die Ansteuerung in Kürze So sieht der minimale Ablauf in PyQGIS aus (vollständige, robuste Fassung in [vln_karten/api_client.py](vln_karten/api_client.py)): ```python import json from qgis.PyQt.QtCore import QUrl from qgis.PyQt.QtNetwork import QNetworkRequest from qgis.core import QgsBlockingNetworkRequest BASE = "https://api.flurneuordnung-sachsen.de/v2" def request(method, path, payload=None, api_key=None): req = QNetworkRequest(QUrl(BASE + path)) req.setHeader(QNetworkRequest.KnownHeaders.ContentTypeHeader, "application/json") if api_key: req.setRawHeader(b"X-API-Key", api_key.encode()) blk = QgsBlockingNetworkRequest() body = json.dumps(payload).encode() if payload is not None else b"" getattr(blk, method)(req, *([body] if method != "get" else [])) return json.loads(bytes(blk.reply().content()) or b"null") # 1) Login: liefert das userauth-Token = API-Key login = request("post", "/person/login", {"mail": "...", "password": "..."}) api_key = login["data"]["userauth"] # 2) Layer lesen: GeoJSON FeatureCollection in EPSG:25833 fc = request("get", "/maps/p41?vkz=22017&limit=2000&offset=0", api_key=api_key) # 3) Layer schreiben: PUT ersetzt den kompletten Bestand dieser VKZ! request("put", "/maps/p41/22017", payload=fc, api_key=api_key) ``` Die drei Stolpersteine, die das Beispiel-Plugin jeweils sauber löst: 1. **CRS:** Die API liefert/erwartet **EPSG:25833** (ETRS89/UTM33), nicht WGS84 wie in RFC 7946 üblich. GeoJSON-Export aus QGIS daher mit `QgsJsonExporter.setTransformGeometries(False)`. 2. **Gemischte Geometrietypen:** Eine FeatureCollection kann Punkte, Linien und Flächen zugleich enthalten — QGIS-Layer können das nicht. Über die OGC API stellt sich die Frage nicht mehr: der Server bietet je Sammlung drei Familien an (`-punkt`, `-linie`, `-flaeche`), und jede wird ein eigener Layer. 3. **PUT ersetzt alles:** Der Schreibendpunkt legt einen neuen Snapshot an und ersetzt den kompletten Layer-Bestand der VKZ. Immer den vollständigen Datensatz senden — Teilmengen löschen den Rest serverseitig. ## API-Kommunikation ``` POST /person/login Body: {"mail": "...", "password": "..."} Antwort: {"data": {"userauth": "", "id": ...}} userauth = API-Key, danach Header "X-API-Key: " GET /tgen Verfahren/TGs (Auth nötig) GET /maps/?vkz=&limit=&offset= Layer lesen, mit Paging GET /maps//{vkz} Layer lesen, ohne Paging layer: umringe | p41 | st | kas | we Auth: umringe/p41/st öffentlich, kas/we mit X-API-Key Antwort: GeoJSON FeatureCollection in EPSG:25833 PUT /maps//{vkz} Layer schreiben (Auth nötig) Body: GeoJSON (ersetzt den Layer dieser VKZ, neuer Snapshot) Antwort: {"data": {"vkz","layer","art","speicher_id"}, "status":"ok"} Fehlerformat: RFC 7807 (application/problem+json) Leerer vkz-Parameter (?vkz=) wird mit HTTP 400 abgelehnt. ``` ## OGC API Features — laden, bearbeiten, zurückspeichern (empfohlen) Seit dem 07.09.2026 spricht die API **OGC API Features** (Part 1 Core, Part 2 CRS, Part 3 Queryables, Part 4 CRUD) unter `https://api.flurneuordnung-sachsen.de/v2/ogc`. Das ist der Weg für alle, die nur einen Manager-Login haben: QGIS lädt die Layer nativ, und **jede Änderung im Bearbeitungsmodus geht beim Speichern einzeln an die API** (POST/PUT/PATCH/DELETE je Feature) — kein Vollersetzen, kein Memory-Layer. ### Ohne Plugin (QGIS 3.28+ / QGIS 4) 1. *Layer → Datenquellenverwaltung → WFS / OGC API - Features → Neu* - Name: `VLN Manager`, URL: `https://api.flurneuordnung-sachsen.de/v2/ogc` - Version: *OGC API - Features* - Authentifizierung: **Basic** (E-Mail + Passwort des Managers) — oder Methode *API-Header* mit `X-API-Key` (Wert = userauth aus `POST /person/login` bzw. ein `vlnk_…`-Schlüssel aus meinVLN → API-Keys) 2. *Verbinden* → die Sammlungen des eigenen Verfahrens erscheinen, z. B. | Sammlung | Inhalt | Speicher-CRS | |---|---|---| | `umringe-27998-flaeche`, `p41-…`, `st-…`, `kas-…`, `we-…`, `ngg-…`, `biotop-…` | jüngster Stand des Layers (wie im Web-GIS) | EPSG:25833 | | `vorerhebung-27998--punkt|-linie|-flaeche` | ein Vorerhebungsprojekt (schreibbar nur im Status „bearbeitung") | CRS84 | | `massnahmeblatt-27998-linie|-flaeche|-punkt` | je Lage-Geometrie eines Maßnahmenblatts ein Feature | CRS84 | Jede Sammlung gibt es **je Geometriefamilie** (`-punkt`, `-linie`, `-flaeche`): QGIS-Layer sind eintypig, die Stände des Web-GIS mischen Punkte, Linien und Flächen. Die Queryables nennen je Familie den Geometrietyp und die Felder — so lassen sich auch **leere** Layer direkt bearbeiten. Die Basis ohne Suffix (`st-27998`) ist nicht gelistet, aber lesbar. 3. Layer hinzufügen → *Bearbeitungsmodus* → zeichnen/ändern → *Layer-Änderungen speichern*. QGIS schickt `Content-Crs` mit; die API rechnet in PostGIS um (EPSG:25833 ↔ CRS84). Wer nur seine Verfahren sieht: Vorsitz, Kassenbetreuer und Stellvertreter der TG; Verbandsmitarbeiter alle (`?vkz=` in `/ogc/collections` grenzt ein). ### Mit dem Plugin Knopf **„Layer laden"** in der Seitenleiste: legt nach dem Login eine QGIS-Authentifizierungskonfiguration *API-Header* (`X-API-Key`) an — das Passwort bleibt draußen —, holt `GET /ogc/collections?vkz=…` und hängt jede Sammlung als `OAPIF`-Layer in eine Gruppe „VLN " (Untergruppen Vorerhebung / Maßnahmenblätter / Verfahren). Code: [vln_karten/ogc_layers.py](vln_karten/ogc_layers.py). ```python from qgis.core import QgsDataSourceUri, QgsVectorLayer uri = QgsDataSourceUri() uri.setParam("url", "https://api.flurneuordnung-sachsen.de/v2/ogc") uri.setParam("typename", "p41-27998-flaeche") uri.setParam("pagingEnabled", "true") uri.setParam("srsname", "EPSG:25833") uri.setAuthConfigId("") # APIHeader (X-API-Key) oder Basic layer = QgsVectorLayer(uri.uri(False), "Plan 41 · 27998 · Flächen", "OAPIF") ``` ### Wenn das Laden quälend langsam ist Nicht die API: ein **System-Proxy (PAC-Datei)**, den macOS nicht ausführen kann, lässt jede Qt-Anfrage in einen Timeout laufen (~10 s statt 0,3 s) — QGIS wirkt dann hängend, obwohl der Server jede Anfrage mit 200 beantwortet. Prüfen und abschalten: ```bash networksetup -getautoproxyurl Wi-Fi # Enabled: Yes + unerreichbare .pac-Datei? networksetup -setautoproxystate Wi-Fi off # nur wenn im Netz kein Proxy nötig ist ``` In der QGIS-Meldungsleiste steht dann `Execution of PAC script … failed (…-1002)`. ### Stile (QML) — mitgeladen und zurückspeicherbar Jede Sammlung hat einen Stil: `GET …/collections/{cid}/style.qml`. Der Server liefert den für die Sammlung gespeicherten Stil, sonst die **Vorlage der Art** (`kas-*-flaeche` gilt für die Karte alter Stand aller Verfahren), sonst einen erzeugten Standard — kategorisiert nach `typ` mit den Farben des Web-GIS (Wege dunkel, Wasser blau, Ökologie grün, Problemstellen rot). - **Plugin:** wendet den Stil beim Laden automatisch an. Der Knopf **„Stil des aktiven Layers speichern“** in der Seitenleiste schreibt den in QGIS eingestellten Stil zurück; mit dem Häkchen **„als Vorlage für alle Verfahren“** (oder gedrückter Umschalttaste) gilt er für die ganze Art. - **Ohne Plugin:** Layer → Eigenschaften → Stil → *Stil laden* … bzw. die QML-Datei von der URL holen. Zurückschreiben geht per `PUT` (z. B. `curl -T stil.qml -H "X-API-Key: …"`). - **Berechtigung:** Lesen darf jeder, der die Sammlung sieht. Speichern und Löschen nur Verbandsmitarbeiter oder Mitglieder der Gruppe **`MODUL_KARTENSTIL`**; sonst HTTP 403. Ob der eigene Zugang darf, steht in der Sammlung als `vln:styleWritable` und in der Kopfzeile `X-Stil-Schreibbar` der QML-Antwort. ``` GET /ogc/collections/{cid}/style.qml Stil (Kopfzeile X-Stil-Quelle: sammlung|vorlage|standard) PUT /ogc/collections/{cid}/style.qml[?vorlage=1] Stil speichern (Gruppe MODUL_KARTENSTIL) DELETE /ogc/collections/{cid}/style.qml[?vorlage=1] gespeicherten Stil entfernen ``` ### Regeln beim Schreiben - **Geometriefamilie:** in `…-punkt` nur Punkte, in `…-linie` nur Linien, in `…-flaeche` nur Flächen (sonst HTTP 400). - **Verfahrens-Layer:** Änderungen landen *in place* im jüngsten Stand; fehlt ein Stand, legt der erste Schreibzugriff ihn an. Das Web-GIS speichert weiterhin als neuer Stand — beide treffen sich am jüngsten. - **Vorerhebung:** nur Status „bearbeitung" ist schreibbar (sonst HTTP 423). - **Maßnahmenblatt:** `fid = :`. Neues Feature mit `blatt_uuid` hängt eine Geometrie an ein Blatt; mit `blatt_typ` (pflanzung|wasserbau|wegebau|entsiegelung) + `blatt_name` (+ `blatt_mkz`) entsteht ein neues Blatt. `blatt_*`-Attribute ändern die Blatt-Spalten, alles andere die Geometrie-Eigenschaften. - Fehler kommen als RFC 7807 (`application/problem+json`); ohne Anmeldung 401 mit `WWW-Authenticate: Basic` — QGIS fragt dann selbst nach dem Login. ``` GET /ogc Landing Page (öffentlich) GET /ogc/conformance Konformitätsklassen (öffentlich) GET /ogc/collections[?vkz=] Sammlungen des Nutzers GET /ogc/collections/{cid} Sammlung: crs, storageCrs, extent GET /ogc/collections/{cid}/queryables GET /ogc/collections/{cid}/items ?limit&offset&bbox&bbox-crs&crs → FeatureCollection GET /ogc/collections/{cid}/items/{fid} POST /ogc/collections/{cid}/items Feature | FeatureCollection → 201 {ids} PUT /ogc/collections/{cid}/items/{fid} → 204 PATCH /ogc/collections/{cid}/items/{fid} Eigenschaften mergen, Geometrie optional → 204 DELETE /ogc/collections/{cid}/items/{fid} → 204 CRS-URIs: http://www.opengis.net/def/crs/OGC/1.3/CRS84 (Vorgabe), http://www.opengis.net/def/crs/EPSG/0/25833 ``` ### Verifiziert mit QGIS 4.2 (headless) [tools/headless_ogc_test.py](tools/headless_ogc_test.py) treibt das installierte QGIS ohne Oberfläche (PyQGIS, `QT_QPA_PLATFORM=offscreen`) gegen die API: Layer laden, Feature anlegen, Attribut und Geometrie ändern, löschen — für einen Verfahrens-Layer und ein Maßnahmenblatt, jeder Schritt per API gegengeprüft. Stand 07.09.2026: **0 Fehler** mit QGIS 4.2.0. Was QGIS dafür vom Server braucht (und die API liefert): `service-desc`-Link auf ein OpenAPI-Dokument, Part-5-Schema für Felder/Geometrietyp, `OPTIONS …/items` mit `Allow`, PATCH als JSON Merge Patch. ## Das Beispiel-Plugin Das Plugin macht den kompletten Arbeitsablauf in der QGIS-Oberfläche erlebbar: - **Login beim Start**: ohne gespeicherten API-Key zeigt das Plugin nach dem QGIS-Start gleich den Anmeldedialog (E-Mail/Passwort). Der Login liefert das `userauth`-Token; das Plugin speichert **nur dieses Token** (QSettings `vln_karten/api_key`), nie das Passwort, und nutzt es in jeder weiteren Sitzung. „Abmelden“ löscht es. - **Einmaliger Login** (E-Mail/Passwort) — der API-Key wird in den QGIS-Einstellungen gespeichert und über QGIS-Neustarts hinweg wiederverwendet; bei 401/403 öffnet sich der Anmeldedialog erneut. - **Seitenleiste rechts** (andockbar, seit 08.09.2026 anstelle der Werkzeugleiste): Anmeldung, Verfahrensliste aus `GET /tgen` mit Suchfeld, Lade- und Speicherknöpfe, Statuszeile. Wieder einblenden über *Web → VLN Karten → Seitenleiste anzeigen*. - **Verfahrens-Auswahl** gemerkt je QGIS-Projektdatei. - **Speichern:** über die OGC API schreibt QGIS jede Änderung einzeln zurück (Part 4); der Knopf **„Änderungen speichern"** schließt die offenen Bearbeitungssitzungen aller geladenen VLN-Layer auf einmal. Der frühere GeoJSON-Weg (ganze Layer laden und als Ganzes hochladen) ist am 08.09.2026 entfallen — samt `layer_manager.py` und den beiden Client-Methoden dafür. - **Ladezeit:** jede Sammlung kostet beim Anlegen rund eine halbe Sekunde — der OAPIF-Provider fragt Sammlung, Schema, Stil, `OPTIONS` und die erste Seite ab, nacheinander und blockierend. Zwei Gegenmittel: `vorwaermen()` holt vorab alle diese Antworten gleichzeitig (unter 1,5 s), und die Häkchen in der Seitenleiste laden nur die gewünschten Gruppen. Gemessen am 08.09.2026 (Berbisdorf, 27 Sammlungen): **20,5 s ohne, 14,2 s mit** Vorwärmen; nur Verfahren 11,1 s (21 Layer), nur Vorerhebung oder nur Maßnahmenblätter je 1,6 s (3 Layer). **Nicht parallel:** ein in einem Python-Thread gebauter OAPIF-Layer stürzt beim ersten `getFeatures()` ab (Segfault, gemessen) — der Provider gehört dem Thread, der ihn erzeugt hat. | Datei | Zweck | |---|---| | [vln_karten/plugin.py](vln_karten/plugin.py) | Ablaufsteuerung: Anmeldung, Laden, Speichern, Hochladen, `DATASETS`-Registry | | [vln_karten/panel.py](vln_karten/panel.py) | Seitenleiste (Anmeldung, Verfahrensfilter, Knöpfe, Statuszeile) | | [vln_karten/api_client.py](vln_karten/api_client.py) | HTTP-Client: Login, Paging-Loader, PUT, RFC-7807-Fehler | | [vln_karten/ogc_layers.py](vln_karten/ogc_layers.py) | OGC API Features: Auth-Konfiguration (API-Header), Sammlungen → OAPIF-Layer | | [vln_karten/login_dialog.py](vln_karten/login_dialog.py) | Anmeldedialog | | [vln_karten/metadata.txt](vln_karten/metadata.txt) | QGIS-Plugin-Metadaten | ## Installation Voraussetzung: QGIS **3.22+** oder **QGIS 4** (Qt6/PyQt6 — der Code nutzt scoped Enums und `exec()`), plus ein VLN-Manager-Konto. ### Für Anwender: über das VLN-Plugin-Verzeichnis *Erweiterungen → Erweiterungen verwalten → Einstellungen → Hinzufügen*, Name „VLN“, Adresse: ``` https://api.flurneuordnung-sachsen.de/v2/qgis/plugins.xml ``` Danach unter *Alle* nach „VLN Karten“ suchen und installieren (experimentelle Erweiterungen zulassen). Neue Fassungen bietet QGIS von sich aus an. Das Paket baut `python3 tools/baue_paket.py`; ZIP und `plugins.xml` gehören danach in den Manager nach `api/qgis/`. ### Zum Entwickeln: Ordner verlinken ```bash git clone https://entwicklung.flurneuordnung-sachsen.de/VLN_SN/API_Karte_QGISDemo.git cd API_Karte_QGISDemo # macOS, QGIS 4 (bei QGIS 3: "QGIS4" durch "QGIS3" ersetzen): ln -s "$PWD/vln_karten" \ ~/Library/Application\ Support/QGIS/QGIS4/profiles/default/python/plugins/vln_karten # Linux: ~/.local/share/QGIS/QGIS4/profiles/default/python/plugins/ # Windows: %APPDATA%\QGIS\QGIS4\profiles\default\python\plugins\ ``` Dann QGIS starten und unter *Erweiterungen → Erweiterungen verwalten* „VLN Karten“ aktivieren (experimentelle Erweiterungen zulassen). Beim Entwickeln hilft das Plugin **Plugin Reloader** — Codeänderungen wirken dann ohne QGIS-Neustart. ## Verkehrszeichen und Widmungen Die Sammlungen `widmung--punkt` (Schilder) und `-linie` (Widmungen) führen den **Bestand** des Verfahrens; `blattzeichen-` führt die **neuen Zeichen am Weg** eines Maßnahmenblatts (sie liegen im Blatt, ein neues Zeichen braucht darum `blatt_uuid`). Beides ist über die OGC API voll bearbeitbar. Den Stil liefert der Server fertig: Schildgrafik statt Kreis, gedreht nach `drehung`, Größe nach `groesse`, bis zu zwei Zusatzzeichen darunter; Widmungslinien nach Straßenklasse eingefärbt, Strichmuster nach Zustand, Änderungen seitlich versetzt. Ab 1:2500 wird aus dem Schild eine Marke, ab 1:10000 ein Punkt — sonst liegt über einem großen Verfahren nur noch ein Haufen. Die Grafiken kommen von der API (`/v2/zeichen/svg/.svg`, Katalog unter `/v2/zeichen/katalog.json`, beides öffentlich). Das Plugin legt sie beim Laden im QGIS-Profil ab (`/svg/vln/`) und schreibt den Stil auf diesen Ordner um; fehlt eine Datei, bleibt die Serveradresse stehen. Gesetzt wird wie im Web: in der Seitenleiste unter *Verkehrszeichen setzen* ein Schild wählen (Suche nach Nummer oder Name), dann **Schild setzen** einschalten — erster Klick Standort, zweiter Klick Blickrichtung. **Zusatzzeichen** hängt das gewählte Zusatzzeichen an ein vorhandenes Schild (höchstens zwei). Für Zeichen am Weg eines Blattes oben das Blatt auswählen; in der Verfahrenskarte bleibt die Auswahl leer. Zum Drehen vorhandener Schilder taugt auch das QGIS-Bordwerkzeug *Punktsymbole drehen*, weil der Stil die Drehung datendefiniert führt. ## Hinweise für eigene Integrationen - `QgsBlockingNetworkRequest` statt `requests`/`urllib` verwenden — damit greifen Proxy- und Zertifikatseinstellungen aus QGIS. - Der API-Key wird im Beispiel unverschlüsselt in QSettings abgelegt; für Produktivcode bietet sich der QGIS-Authentifizierungsmanager an (`QgsApplication.authManager()`). - Requests laufen im Beispiel synchron (blockierend); für große Datensätze auf `QgsNetworkAccessManager` mit Tasks wechseln. - Features ohne Geometrie landen beim Laden in einem Tabellen-Layer „ohne Geometrie“; Geometrien werden beim Hochladen als Multi-Typen gesendet (`ST_GeomFromGeoJSON` akzeptiert beides).