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:
@@ -152,6 +152,29 @@ networksetup -setautoproxystate Wi-Fi off # nur wenn im Netz kein Proxy nöti
|
||||
|
||||
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. Toolbar-Knopf **„Stil des aktiven
|
||||
Layers speichern“** schreibt den in QGIS eingestellten Stil zurück; mit gedrückter
|
||||
**Umschalttaste** als Vorlage für die ganze Art (alle Verfahren).
|
||||
- **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).
|
||||
|
||||
+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
|
||||
|
||||
+46
-5
@@ -15,7 +15,7 @@ from functools import partial
|
||||
|
||||
from qgis.PyQt.QtCore import QSettings, QTimer
|
||||
from qgis.PyQt.QtWidgets import QAction, QComboBox, QDialog, QMessageBox
|
||||
from qgis.core import Qgis, QgsProject
|
||||
from qgis.core import Qgis, QgsDataSourceUri, QgsProject
|
||||
|
||||
from .api_client import ApiError, AuthError, KartenApiClient
|
||||
from .layer_manager import (
|
||||
@@ -99,6 +99,9 @@ class VlnKartenPlugin:
|
||||
self.dataset_actions.append(
|
||||
self._add_action("Alle Layer (OGC API) laden", self.run_load_ogc)
|
||||
)
|
||||
self.dataset_actions.append(
|
||||
self._add_action("Stil des aktiven Layers speichern", self.run_save_style)
|
||||
)
|
||||
for key, dataset in DATASETS.items():
|
||||
self.dataset_actions.append(
|
||||
self._add_action(
|
||||
@@ -444,12 +447,50 @@ class VlnKartenPlugin:
|
||||
self._show_error("OGC API", "Für dieses Verfahren gibt es keine Sammlungen "
|
||||
"(oder keine Berechtigung).")
|
||||
return
|
||||
geladen, fehler = ogc_layers.layer_hinzufuegen(
|
||||
# Schrittweise: je Ereignisschleifen-Durchlauf ein Layer, Fortschritt in der
|
||||
# Meldungsleiste. Fehler kommen dort am Ende gesammelt, nicht als Modal-Dialog.
|
||||
ogc_layers.layer_hinzufuegen(
|
||||
self.iface, self.client.base_url, vkz, self.combo.currentText(), collections, authcfg,
|
||||
api_key=None if authcfg else self.client.api_key)
|
||||
if fehler:
|
||||
self._show_error("OGC API — nicht geladen",
|
||||
"\n".join("%s: %s" % f for f in fehler))
|
||||
|
||||
def run_save_style(self):
|
||||
"""Den Stil des aktiven OGC-Layers auf den Server zurückschreiben.
|
||||
|
||||
Erlaubt für Verbandsmitarbeiter und die Gruppe MODUL_KARTENSTIL; der Server antwortet
|
||||
sonst mit 403. Mit gedrückter Umschalttaste wird als **Vorlage der Art** gespeichert —
|
||||
die gilt dann für alle Verfahren (z. B. ein Stil für jede „Karte alter Stand").
|
||||
"""
|
||||
from qgis.PyQt.QtWidgets import QApplication
|
||||
from qgis.PyQt.QtCore import Qt as _Qt
|
||||
|
||||
layer = self.iface.activeLayer()
|
||||
if layer is None or layer.providerType() != "OAPIF":
|
||||
self._show_error("Kein OGC-Layer",
|
||||
"Bitte zuerst einen über „Alle Layer (OGC API) laden“ geladenen "
|
||||
"Layer auswählen.")
|
||||
return
|
||||
cid = QgsDataSourceUri(layer.source()).param("typename")
|
||||
if not cid:
|
||||
self._show_error("Kein OGC-Layer", "Die Datenquelle nennt keine Sammlung (typename).")
|
||||
return
|
||||
vorlage = bool(QApplication.keyboardModifiers() & _Qt.KeyboardModifier.ShiftModifier)
|
||||
settings = QSettings()
|
||||
settings.beginGroup(SETTINGS_GROUP)
|
||||
authcfg = settings.value(ogc_layers.SETTINGS_AUTHCFG, "")
|
||||
settings.endGroup()
|
||||
try:
|
||||
ok, text = ogc_layers.stil_speichern(layer, self.client.base_url, cid,
|
||||
api_key=self.client.api_key, authcfg=authcfg or None,
|
||||
vorlage=vorlage)
|
||||
except AuthError:
|
||||
self._handle_session_expired()
|
||||
return
|
||||
if ok:
|
||||
self.iface.messageBar().pushMessage(
|
||||
"VLN Karten", "Stil für %s gespeichert — %s" % (cid, text),
|
||||
level=Qgis.Success, duration=8)
|
||||
else:
|
||||
self._show_error("Stil nicht gespeichert", text)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Hilfsfunktionen
|
||||
|
||||
Reference in New Issue
Block a user