Mit QGIS 4.2.0 am 07.09.2026 gemessen: 0 Fehler. README nennt die Server-Anforderungen (service-desc/OpenAPI, Part-5-Schema, OPTIONS Allow, Merge Patch). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
245 lines
12 KiB
Markdown
245 lines
12 KiB
Markdown
# 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-Memory-Layer können
|
|
das nicht. Das Plugin splittet beim Laden nach Geometrie-Familie
|
|
und vereint beim Hochladen wieder ([vln_karten/layer_manager.py](vln_karten/layer_manager.py)).
|
|
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": "<url_token>", "id": ...}}
|
|
userauth = API-Key, danach Header "X-API-Key: <userauth>"
|
|
|
|
GET /tgen Verfahren/TGs (Auth nötig)
|
|
|
|
GET /maps/<layer>?vkz=&limit=&offset= Layer lesen, mit Paging
|
|
GET /maps/<layer>/{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/<layer>/{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-<key>-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
|
|
|
|
Toolbar-Knopf **„Alle Layer (OGC API) laden"**: 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 <Verfahren>" (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("<authcfg-id>") # APIHeader (X-API-Key) oder Basic
|
|
layer = QgsVectorLayer(uri.uri(False), "Plan 41 · 27998 · Flächen", "OAPIF")
|
|
```
|
|
|
|
### 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 = <Blatt-UUID>:<n>`. 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 (klassischer GeoJSON-Weg)
|
|
|
|
Das Plugin macht den kompletten Arbeitsablauf in der QGIS-Oberfläche
|
|
erlebbar:
|
|
|
|
- **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.
|
|
- **Verfahrens-Auswahl in der Toolbar** (aus `GET /tgen`), gemerkt je
|
|
QGIS-Projektdatei.
|
|
- **Buttons je Layer:** Verfahrensgebiet (`umringe`), Plan 41 (`p41`),
|
|
Karte alter Stand (`kas`), Wertermittlung (`we`) — vollständig
|
|
geladen über den Listen-Endpunkt mit Paging. Weitere Layer (z. B.
|
|
`st`) sind je ein Eintrag mehr im `DATASETS`-Dict in
|
|
[vln_karten/plugin.py](vln_karten/plugin.py).
|
|
- **„Alle Layer (OGC API) laden“** — der neue Weg (s. o.), editierbar ohne Splitting.
|
|
- **„Aktiven Layer hochladen“** vereint alle Teil-Layer des Datensatzes
|
|
und schreibt per PUT zurück (mit Sicherheitsabfrage).
|
|
- Erneutes Laden **ersetzt** vorhandene Layer nach Rückfrage, statt sie
|
|
zu stapeln — sonst würde der nächste Upload alles doppelt senden.
|
|
|
|
| Datei | Zweck |
|
|
|---|---|
|
|
| [vln_karten/plugin.py](vln_karten/plugin.py) | Toolbar, Aktionen, Verfahrens-Auswahl, `DATASETS`-Registry |
|
|
| [vln_karten/api_client.py](vln_karten/api_client.py) | HTTP-Client: Login, Paging-Loader, PUT, RFC-7807-Fehler |
|
|
| [vln_karten/layer_manager.py](vln_karten/layer_manager.py) | GeoJSON ↔ Memory-Layer, Geometrie-Splitting/-Vereinigung |
|
|
| [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 (zum Ausprobieren)
|
|
|
|
Voraussetzung: QGIS **3.22+** oder **QGIS 4** (Qt6/PyQt6 — der Code
|
|
nutzt scoped Enums und `exec()`), plus ein VLN-Manager-Konto.
|
|
|
|
```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.
|
|
|
|
## 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).
|