Stile: beim Laden vom Server anwenden, Knopf zum Zurueckspeichern (Gruppe MODUL_KARTENSTIL)

- ogc_layers.stil_anwenden(): GET …/style.qml, in eine Temp-Datei, loadNamedStyle; scheitert
  der Abruf, bleibt die QGIS-Zufallsfarbe (kein Abbruch des Ladens).
- ogc_layers.stil_speichern(): saveNamedStyle -> PUT; 403 meldet die fehlende Gruppe im Klartext.
- plugin.run_save_style(): Knopf 'Stil des aktiven Layers speichern'; mit Umschalttaste als
  Vorlage der Art (gilt fuer alle Verfahren, z. B. Karte alter Stand).
- README: Abschnitt Stile samt Routen und Berechtigung.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Erik
2026-09-07 21:07:54 +02:00
co-authored by Claude Opus 4.8
parent dc960ddca4
commit 4504f9a34c
3 changed files with 197 additions and 31 deletions
+128 -26
View File
@@ -24,7 +24,12 @@ Blatt-Spalten als ``blatt_*``-Attribute; neue Features brauchen ``blatt_uuid`` o
``blatt_typ`` + ``blatt_name``).
"""
from qgis.PyQt.QtCore import QSettings
import os
import tempfile
from qgis.PyQt.QtCore import QSettings, QUrl, Qt, QTimer
from qgis.PyQt.QtNetwork import QNetworkRequest
from qgis.PyQt.QtWidgets import QProgressBar
from qgis.core import (
Qgis,
QgsApplication,
@@ -34,7 +39,9 @@ from qgis.core import (
QgsVectorLayer,
)
from .api_client import ApiError
from qgis.core import QgsBlockingNetworkRequest
from .api_client import ApiError, AuthError
AUTHCFG_NAME = "VLN Karten (API-Key)"
SETTINGS_AUTHCFG = "ogc_authcfg"
@@ -101,6 +108,74 @@ def collections_laden(client, vkz):
return rows
def stil_url(base_url, cid):
"""Adresse des QGIS-Stils einer Sammlung (QML) auf dem Server."""
return "%s/collections/%s/style.qml" % (ogc_url(base_url), cid)
def stil_anwenden(layer, base_url, cid, api_key=None, authcfg=None):
"""Den Stil der Sammlung vom Server holen und auf den Layer anwenden.
Der Server liefert immer etwas: den fuer die Sammlung gespeicherten Stil, sonst die Vorlage
der Art, sonst einen erzeugten Standard (kategorisiert nach ``typ``). Schlaegt der Abruf
fehl, behaelt der Layer die QGIS-Zufallsfarbe — kein Grund, das Laden abzubrechen.
:returns: ``(True, quelle)`` oder ``(False, Meldung)``.
"""
req = QNetworkRequest(QUrl(stil_url(base_url, cid)))
if api_key:
req.setRawHeader(b"X-API-Key", api_key.encode("utf-8"))
blk = QgsBlockingNetworkRequest()
if authcfg:
blk.setAuthCfg(authcfg)
if blk.get(req) != QgsBlockingNetworkRequest.NoError:
return False, blk.errorMessage()
antwort = blk.reply()
if antwort.attribute(QNetworkRequest.Attribute.HttpStatusCodeAttribute) != 200:
return False, "HTTP %s" % antwort.attribute(QNetworkRequest.Attribute.HttpStatusCodeAttribute)
quelle = bytes(antwort.rawHeader(b"X-Stil-Quelle")).decode() or "?"
pfad = os.path.join(tempfile.gettempdir(), "vln_%s.qml" % cid.replace("*", "_"))
with open(pfad, "wb") as fh:
fh.write(bytes(antwort.content()))
meldung, ok = layer.loadNamedStyle(pfad)
if ok:
layer.triggerRepaint()
return bool(ok), quelle if ok else meldung
def stil_speichern(layer, base_url, cid, api_key=None, authcfg=None, vorlage=False):
"""Den aktuellen Layer-Stil als QML an den Server schicken (PUT …/style.qml).
:param vorlage: als Vorlage der Art speichern — gilt dann fuer alle Verfahren.
:returns: ``(True, Text)`` oder ``(False, Meldung)``; 403 heisst: Gruppe MODUL_KARTENSTIL fehlt.
"""
pfad = os.path.join(tempfile.gettempdir(), "vln_upload_%s.qml" % cid.replace("*", "_"))
meldung = layer.saveNamedStyle(pfad)
if not os.path.exists(pfad):
return False, "QGIS konnte den Stil nicht schreiben: %s" % meldung
with open(pfad, "rb") as fh:
rumpf = fh.read()
url = stil_url(base_url, cid) + ("?vorlage=1" if vorlage else "")
req = QNetworkRequest(QUrl(url))
req.setHeader(QNetworkRequest.KnownHeaders.ContentTypeHeader, "application/x-qgis-layer-settings")
if api_key:
req.setRawHeader(b"X-API-Key", api_key.encode("utf-8"))
blk = QgsBlockingNetworkRequest()
if authcfg:
blk.setAuthCfg(authcfg)
blk.put(req, rumpf)
antwort = blk.reply()
code = antwort.attribute(QNetworkRequest.Attribute.HttpStatusCodeAttribute)
if code == 200:
return True, "%d Zeichen gespeichert%s" % (len(rumpf), " (Vorlage der Art)" if vorlage else "")
if code == 403:
return False, ("Keine Berechtigung: Stile darf nur speichern, wer in der Gruppe "
"MODUL_KARTENSTIL ist (oder Verbandsmitarbeiter).")
if code in (401, None):
raise AuthError("Anmeldung abgelaufen")
return False, "Server antwortete mit HTTP %s: %s" % (code, bytes(antwort.content())[:200].decode("utf-8", "replace"))
def layer_uri(base_url, cid, authcfg=None, storage_crs=None, api_key=None):
"""Die OAPIF-Datenquelle: Landing-URL + Sammlungs-ID (+ Authentifizierung).
@@ -130,36 +205,63 @@ def layer_uri(base_url, cid, authcfg=None, storage_crs=None, api_key=None):
def layer_hinzufuegen(iface, base_url, vkz, vkz_name, collections, authcfg=None, api_key=None):
"""Je Sammlung einen OAPIF-Layer anlegen, gruppiert unter „VLN <Verfahren>".
"""Alle Sammlungen als OAPIF-Layer in die Gruppe „VLN <Verfahren>" hängen — **schrittweise**.
:returns: ``(geladen, fehler)`` — Anzahl Layer und Liste ``(cid, Meldung)``.
Warum nicht in einer Schleife: der OAPIF-Provider stellt beim Anlegen jedes Layers mehrere
**blockierende** Netzanfragen (Landing Page, OpenAPI, Conformance, Collection, Items, Schema,
zweimal OPTIONS). Bei rund 30 Sammlungen friert die Oberfläche dabei minutenlang ein und
sieht aus wie abgestürzt (Erik, 07.09.2026). Darum wird je Ereignisschleifen-Durchlauf
genau EIN Layer angelegt: QGIS bleibt bedienbar, der Fortschritt steht in der Meldungsleiste.
:returns: ``QProgressBar``-Träger (die Meldung) — der Aufrufer muss nichts damit tun.
"""
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())
gruppe_name = ("VLN %s %s" % (vkz, vkz_name or "")).strip()
gruppe = wurzel.findGroup(gruppe_name) or wurzel.insertGroup(0, gruppe_name)
untergruppen = {}
geladen, fehler = 0, []
for coll in collections:
balken = QProgressBar()
balken.setMaximum(len(collections))
balken.setValue(0)
balken.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
meldung = iface.messageBar().createMessage("VLN Karten", "Layer werden geladen …")
meldung.layout().addWidget(balken)
iface.messageBar().pushWidget(meldung, Qgis.Info)
zustand = {"i": 0, "geladen": 0, "fehler": []}
def schritt():
if zustand["i"] >= len(collections):
iface.messageBar().popWidget(meldung)
text = "%d Layer geladen — zum Bearbeiten den Layer in den Bearbeitungsmodus " \
"schalten; jede gespeicherte Änderung geht sofort an die API." % zustand["geladen"]
if zustand["fehler"]:
text += " %d Sammlung(en) nicht ladbar: %s" % (
len(zustand["fehler"]), ", ".join(f[0] for f in zustand["fehler"][:3]))
iface.messageBar().pushMessage(
"VLN Karten", text,
level=Qgis.Success if zustand["geladen"] else Qgis.Warning, duration=10)
return
coll = collections[zustand["i"]]
zustand["i"] += 1
cid = str(coll.get("id") or "")
praefix = cid.split("-", 1)[0]
gruppen_name = dict(GRUPPEN).get(praefix, "Weitere")
gruppen_name = dict(GRUPPEN).get(cid.split("-", 1)[0], "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
layer = QgsVectorLayer(layer_uri(base_url, cid, authcfg, coll.get("storageCrs"), api_key),
titel, "OAPIF")
if layer.isValid():
stil_anwenden(layer, base_url, cid, api_key, authcfg) # Darstellung wie im Web-GIS
projekt.addMapLayer(layer, False)
untergruppen[gruppen_name].addLayer(layer)
zustand["geladen"] += 1
else:
zustand["fehler"].append((cid, layer.error().message() if layer.error() else "ungültig"))
balken.setValue(zustand["i"])
meldung.setText("Layer werden geladen … %d von %d (%s)" % (zustand["i"], len(collections), titel))
QTimer.singleShot(0, schritt) # zurück in die Ereignisschleife: QGIS bleibt bedienbar
QTimer.singleShot(0, schritt)
return meldung