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:
+128
-26
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user