Seitenleiste statt Werkzeugleiste

Erik, 08.09.2026: "ich will keine Toolbar, sondern so eine Seitenleiste auf der
rechten Seite."

- vln_karten/panel.py (neu): andockbares QDockWidget, rechts angedockt, mit den
  Abschnitten Anmeldung, Verfahren, OGC API und Klassisch (GeoJSON) sowie einer
  Statuszeile. Reine Oberflaeche; Klicks gehen als Rueckrufe ans Plugin.
- Verfahrensliste bekommt ein Suchfeld (Hausregel: Auswahllisten immer mit
  Filter). Die Auswahl bleibt beim Filtern erhalten, "kein Treffer" liefert
  kein VKZ.
- Der Stil-Knopf hat jetzt ein Haekchen "als Vorlage fuer alle Verfahren"; die
  Umschalttaste wirkt weiterhin.
- Abmelden ist ein eigener Knopf (verwirft den gespeicherten API-Key).
- Menue Web -> VLN Karten -> "Seitenleiste anzeigen" blendet sie wieder ein.
- tools/headless_panel_test.py: prueft Zustaende, Filter, Rueckrufe und rendert
  das Layout als PNG (QGIS 4.2 offscreen). Lauf am 08.09.2026: 17 Pruefungen,
  0 Fehler, 285 Verfahren in der Liste.
- README und metadata.txt (0.3.0) nachgezogen.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
erik
2026-09-08 08:28:11 +02:00
co-authored by Claude Fable 5.1
parent 4504f9a34c
commit b97f066b49
5 changed files with 426 additions and 101 deletions
+11 -7
View File
@@ -122,7 +122,7 @@ Verbandsmitarbeiter alle (`?vkz=` in `/ogc/collections` grenzt ein).
### Mit dem Plugin
Toolbar-Knopf **„Alle Layer (OGC API) laden"**: legt nach dem Login eine
Knopf **„Alle 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 <Verfahren>" (Untergruppen Vorerhebung /
@@ -159,9 +159,10 @@ die Sammlung gespeicherten Stil, sonst die **Vorlage der Art** (`kas-*-flaeche`
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).
- **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
@@ -226,8 +227,10 @@ 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.
- **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.
- **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.
@@ -241,7 +244,8 @@ erlebbar:
| Datei | Zweck |
|---|---|
| [vln_karten/plugin.py](vln_karten/plugin.py) | Toolbar, Aktionen, Verfahrens-Auswahl, `DATASETS`-Registry |
| [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/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 |
+103
View File
@@ -0,0 +1,103 @@
# -*- coding: utf-8 -*-
"""Seitenleiste headless pruefen: Zustaende, Filter, Rueckrufe — und als PNG rendern.
Aufruf (macOS, QGIS 4.2 im Bundle; VLN_TOKEN = API-Key aus dem Login):
APP=/Applications/QGIS-final-4_2_0.app; LIB="$APP/Contents/Resources/python3.12"
QT_QPA_PLATFORM=offscreen PROJ_DATA="$APP/Contents/Resources/qgis/proj" \
PYTHONPATH="$LIB:$LIB/lib-dynload:$LIB/site-packages" \
"$APP/Contents/MacOS/python3.12" tools/headless_panel_test.py /tmp/panel.png
Holt die echte Verfahrensliste (GET /tgen) und prueft Zustaende, Filter und Rueckrufe der
Seitenleiste; das PNG zeigt das Layout ohne QGIS-Fenster.
"""
import os, sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from qgis.core import QgsApplication
app = QgsApplication([], True)
QgsApplication.setPrefixPath("/Applications/QGIS-final-4_2_0.app/Contents/MacOS", True)
QgsApplication.setPluginPath("/Applications/QGIS-final-4_2_0.app/Contents/PlugIns/qgis")
app.initQgis()
from qgis.PyQt.QtNetwork import QNetworkProxyFactory
QNetworkProxyFactory.setUseSystemConfiguration(False)
from qgis.PyQt.QtCore import QSize
from vln_karten.panel import VlnPanel
from vln_karten.plugin import DATASETS
from vln_karten.api_client import KartenApiClient
ZIEL = sys.argv[1]
FEHLER, GERUFEN = [], []
def pruefe(bed, text):
print((" OK " if bed else " FEHL ") + text)
if not bed:
FEHLER.append(text)
rueckrufe = {name: (lambda *a, n=name: GERUFEN.append((n, a)))
for name in ("login", "logout", "verfahren_gewaehlt", "load_ogc",
"save_style", "load_dataset", "upload")}
panel = VlnPanel(None, callbacks=rueckrufe,
datensaetze=[(k, d["label"]) for k, d in DATASETS.items()])
print("== abgemeldet ==")
pruefe(panel.login_knopf.isVisible() or not panel.logout_knopf.isVisibleTo(panel),
"Anmelde-Knopf sichtbar, Abmelden verborgen")
pruefe(not panel.ogc_knopf.isEnabled() and not panel.combo.isEnabled(),
"Knoepfe und Auswahl gesperrt")
print("== angemeldet, echte Verfahrensliste ==")
client = KartenApiClient(api_key=os.environ["VLN_TOKEN"])
verfahren = client.get_verfahren()
panel.set_angemeldet(True, "erik.sefkow@…")
panel.set_verfahren(verfahren)
pruefe(panel.ogc_knopf.isEnabled(), "Knoepfe frei")
pruefe(panel.anzahl_eintraege() == len(verfahren) + 1,
"Liste gefuellt: %d Verfahren + Platzhalter" % len(verfahren))
print("== Filter ==")
panel.filter_feld.setText("berb")
treffer = [panel.combo.itemText(i) for i in range(panel.combo.count())][1:]
pruefe(treffer and all("berb" in t.lower() for t in treffer),
"gefiltert auf %d Treffer: %s" % (len(treffer), ", ".join(treffer)[:70]))
panel.filter_feld.setText("gibtsnicht")
pruefe(panel.combo.count() == 1 and panel.combo.currentData() is None,
"kein Treffer -> Platzhalter, kein VKZ")
panel.filter_feld.clear()
pruefe(panel.anzahl_eintraege() == len(verfahren) + 1, "Filter geleert -> Liste wieder voll")
print("== Auswahl + Rueckruf ==")
panel.waehle_vkz("27027")
pruefe(panel.aktuelles_vkz() == "27027", "Wiederherstellen ohne Rueckruf: %s" % panel.aktuelles_vkz())
pruefe(not any(n == "verfahren_gewaehlt" for n, _ in GERUFEN), "waehle_vkz loest keinen Rueckruf aus")
panel.combo.setCurrentIndex(panel.combo.findData(verfahren[0]["vkz"]))
pruefe(any(n == "verfahren_gewaehlt" for n, _ in GERUFEN), "Nutzerauswahl meldet ans Plugin")
panel.waehle_vkz("27027")
print("== Knoepfe ==")
panel.ogc_knopf.click(); panel.stil_knopf.click(); panel.upload_knopf.click()
panel.datensatz_knoepfe[0].click()
namen = [n for n, _ in GERUFEN]
for erwartet in ("load_ogc", "save_style", "upload", "load_dataset"):
pruefe(erwartet in namen, "Rueckruf %s" % erwartet)
ds = [a for n, a in GERUFEN if n == "load_dataset"]
pruefe(ds and ds[0][0] == list(DATASETS)[0], "Datensatz-Knopf uebergibt Schluessel: %s" % (ds[0] if ds else "-"))
pruefe(not panel.als_vorlage(), "Vorlage-Haken standardmaessig aus")
panel.vorlage_haken.setChecked(True)
pruefe(panel.als_vorlage(), "Vorlage-Haken wirkt")
panel.vorlage_haken.setChecked(False)
print("== Bild ==")
panel.set_status("Stil gespeichert: kas-27027-flaeche (Vorlage)")
panel.widget().resize(QSize(300, 760))
panel.widget().grab().save(ZIEL)
print(" ", ZIEL, os.path.getsize(ZIEL), "Bytes")
app.exitQgis()
print("== Ergebnis: %d Fehler ==" % len(FEHLER))
for f in FEHLER:
print(" -", f)
sys.exit(1 if FEHLER else 0)
+1 -1
View File
@@ -5,7 +5,7 @@ qgisMaximumVersion=4.99
supportsQt6=True
description=Beispiel: Karten-API des VLN Managers aus QGIS ansteuern — OGC API Features (alle Layer editierbar, Vorerhebung bis Maßnahmenblatt) und der klassische GeoJSON-Weg.
about=Referenz-Implementierung für die VLN-Manager-Karten-API: Login (mail/password -> userauth-API-Key), vollständiges Laden je VKZ mit Paging (EPSG:25833), Splitten gemischter Geometrietypen in Memory-Layer und vereintes Zurückschreiben per PUT.
version=0.2.0
version=0.3.0
author=Erik
email=phpwelt@gmail.com
tracker=https://entwicklung.flurneuordnung-sachsen.de/VLN_SN/API_Karte_QGISDemo/issues
+238
View File
@@ -0,0 +1,238 @@
# -*- coding: utf-8 -*-
"""Seitenleiste (andockbares Panel) des Plugins — rechts in QGIS.
Ersetzt die frühere Werkzeugleiste (Erik, 08.09.2026: „keine Toolbar, sondern eine
Seitenleiste auf der rechten Seite"). Das Panel ist reine Oberfläche: es hält keinen
Zustand der Fachlogik, sondern meldet Klicks über Rückrufe (``callbacks``) an
``VlnKartenPlugin`` und wird von dort über ``set_angemeldet`` / ``set_verfahren``
umgeschaltet.
Aufbau von oben nach unten:
1. Anmeldung — Status + Knopf (Anmelden/Abmelden)
2. Verfahren — Suchfeld (filtert) + Auswahlliste
3. OGC API — alle Layer laden, Stil speichern (mit Häkchen „für alle Verfahren")
4. Klassisch — die einzelnen GeoJSON-Datensätze, aktiven Layer hochladen
5. Statuszeile
"""
from qgis.PyQt.QtCore import Qt
from qgis.PyQt.QtWidgets import (QCheckBox, QComboBox, QDockWidget, QFrame, QGroupBox,
QHBoxLayout, QLabel, QLineEdit, QPushButton, QSizePolicy,
QVBoxLayout, QWidget)
PANEL_TITEL = "VLN Karten"
PANEL_OBJEKT = "VlnKartenPanel"
def _klein(widget):
"""Hinweistexte eine Stufe kleiner und gedämpft — wie in QGIS-Panels üblich.
Die Farbe kommt aus der Palette (``palette(mid)``), damit der Text in hellen wie
dunklen QGIS-Themen lesbar bleibt; ``setEnabled(False)`` wäre dafür der falsche Weg,
weil es das Widget zugleich unbedienbar macht.
"""
schrift = widget.font()
schrift.setPointSizeF(max(8.0, schrift.pointSizeF() - 1.0))
widget.setFont(schrift)
widget.setWordWrap(True)
widget.setStyleSheet("color: palette(mid);")
return widget
class VlnPanel(QDockWidget):
"""Die Seitenleiste. ``callbacks`` bündelt die Rückrufe ins Plugin."""
def __init__(self, parent=None, callbacks=None, datensaetze=()):
super().__init__(PANEL_TITEL, parent)
self.setObjectName(PANEL_OBJEKT)
self.setAllowedAreas(Qt.DockWidgetArea.RightDockWidgetArea
| Qt.DockWidgetArea.LeftDockWidgetArea)
self._cb = callbacks or {}
self._verfahren = [] # [(beschriftung, vkz)] — ungefiltert
self._stumm = False # unterdrückt Signale beim programmatischen Füllen
inhalt = QWidget()
aussen = QVBoxLayout(inhalt)
aussen.setContentsMargins(8, 8, 8, 8)
aussen.setSpacing(10)
aussen.addWidget(self._bau_anmeldung())
aussen.addWidget(self._bau_verfahren())
aussen.addWidget(self._bau_ogc())
aussen.addWidget(self._bau_klassisch(datensaetze))
aussen.addStretch(1)
aussen.addWidget(self._bau_status())
inhalt.setMinimumWidth(260)
self.setWidget(inhalt)
self.set_angemeldet(False)
# ------------------------------------------------------------------
# Aufbau
# ------------------------------------------------------------------
def _bau_anmeldung(self):
kasten = QGroupBox("Anmeldung")
lage = QVBoxLayout(kasten)
lage.setSpacing(6)
self.status_label = QLabel("Nicht angemeldet")
self.status_label.setWordWrap(True)
self.login_knopf = QPushButton("Anmelden …")
self.login_knopf.clicked.connect(lambda: self._ruf("login"))
self.logout_knopf = QPushButton("Abmelden")
self.logout_knopf.clicked.connect(lambda: self._ruf("logout"))
zeile = QHBoxLayout()
zeile.addWidget(self.login_knopf)
zeile.addWidget(self.logout_knopf)
lage.addWidget(self.status_label)
lage.addLayout(zeile)
return kasten
def _bau_verfahren(self):
kasten = QGroupBox("Verfahren")
lage = QVBoxLayout(kasten)
lage.setSpacing(6)
self.filter_feld = QLineEdit()
self.filter_feld.setPlaceholderText("filtern (Name oder Kennzeichen) …")
self.filter_feld.setClearButtonEnabled(True)
self.filter_feld.textChanged.connect(self._filter_geaendert)
self.combo = QComboBox()
self.combo.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
self.combo.setToolTip("Teilnehmergemeinschaft, für die geladen und gespeichert wird")
self.combo.currentIndexChanged.connect(self._auswahl_geaendert)
lage.addWidget(self.filter_feld)
lage.addWidget(self.combo)
return kasten
def _bau_ogc(self):
kasten = QGroupBox("OGC API (empfohlen)")
lage = QVBoxLayout(kasten)
lage.setSpacing(6)
self.ogc_knopf = QPushButton("Alle Layer laden")
self.ogc_knopf.setToolTip("Verfahrenslayer, Vorerhebung und Maßnahmenblätter als "
"editierbare OAPIF-Layer; QGIS schreibt Änderungen direkt zurück")
self.ogc_knopf.clicked.connect(lambda: self._ruf("load_ogc"))
self.stil_knopf = QPushButton("Stil des aktiven Layers speichern")
self.stil_knopf.setToolTip("Braucht die Gruppe MODUL_KARTENSTIL (Verbandsmitarbeiter "
"immer)")
self.stil_knopf.clicked.connect(lambda: self._ruf("save_style"))
self.vorlage_haken = QCheckBox("als Vorlage für alle Verfahren")
self.vorlage_haken.setToolTip("Der Stil gilt dann für diese Art in jedem Verfahren, "
"z. B. für jede Karte alter Stand")
lage.addWidget(self.ogc_knopf)
lage.addWidget(self.stil_knopf)
lage.addWidget(self.vorlage_haken)
return kasten
def _bau_klassisch(self, datensaetze):
kasten = QGroupBox("Klassisch (GeoJSON)")
lage = QVBoxLayout(kasten)
lage.setSpacing(6)
lage.addWidget(_klein(QLabel("Lädt eine Kopie; Änderungen gehen erst mit "
"„Aktiven Layer hochladen“ zurück.")))
self.datensatz_knoepfe = []
for schluessel, beschriftung in datensaetze:
knopf = QPushButton(beschriftung)
knopf.clicked.connect(lambda _=False, s=schluessel: self._ruf("load_dataset", s))
lage.addWidget(knopf)
self.datensatz_knoepfe.append(knopf)
linie = QFrame()
linie.setFrameShape(QFrame.Shape.HLine)
linie.setFrameShadow(QFrame.Shadow.Sunken)
lage.addWidget(linie)
self.upload_knopf = QPushButton("Aktiven Layer hochladen")
self.upload_knopf.clicked.connect(lambda: self._ruf("upload"))
lage.addWidget(self.upload_knopf)
return kasten
def _bau_status(self):
self.status_zeile = _klein(QLabel(""))
self.status_zeile.setToolTip("letzte Aktion")
return self.status_zeile
# ------------------------------------------------------------------
# Zustand (vom Plugin gesetzt)
# ------------------------------------------------------------------
def set_angemeldet(self, angemeldet, wer=None):
self.status_label.setText(("Angemeldet: %s" % wer) if angemeldet and wer
else ("Angemeldet" if angemeldet else "Nicht angemeldet"))
self.login_knopf.setVisible(not angemeldet)
self.logout_knopf.setVisible(angemeldet)
for widget in (self.filter_feld, self.combo, self.ogc_knopf, self.stil_knopf,
self.vorlage_haken, self.upload_knopf, *self.datensatz_knoepfe):
widget.setEnabled(angemeldet)
if not angemeldet:
self._stumm = True
self._verfahren = []
self.combo.clear()
self.combo.addItem("— bitte anmelden —", None)
self.filter_feld.clear()
self._stumm = False
def set_verfahren(self, verfahren):
"""``verfahren``: Liste von ``{"vkz":…, "name":…}`` aus der API."""
self._verfahren = [
("%s — %s" % (v["vkz"], v["name"]) if v.get("name") else str(v["vkz"]), v["vkz"])
for v in verfahren
]
self._liste_fuellen()
def set_status(self, text):
self.status_zeile.setText(text or "")
def aktuelles_vkz(self):
return self.combo.currentData()
def aktueller_text(self):
return self.combo.currentText()
def als_vorlage(self):
return self.vorlage_haken.isChecked()
def waehle_vkz(self, vkz):
"""Setzt die Auswahl ohne Rückruf (Wiederherstellen aus der Projektdatei)."""
self._stumm = True
index = self.combo.findData(vkz) if vkz else -1
self.combo.setCurrentIndex(index if index >= 0 else 0)
self._stumm = False
def anzahl_eintraege(self):
return self.combo.count()
# ------------------------------------------------------------------
# Innenleben
# ------------------------------------------------------------------
def _liste_fuellen(self, muster=""):
"""Füllt die Auswahlliste, gefiltert; die bisherige Auswahl bleibt wenn möglich."""
vorher = self.combo.currentData()
muster = (muster or "").strip().lower()
treffer = [(t, v) for t, v in self._verfahren if muster in t.lower()]
self._stumm = True
self.combo.clear()
if not self._verfahren:
self.combo.addItem("— keine Verfahren —", None)
elif not treffer:
self.combo.addItem("— kein Treffer —", None)
else:
self.combo.addItem("— Verfahren wählen —", None)
for beschriftung, vkz in treffer:
self.combo.addItem(beschriftung, vkz)
index = self.combo.findData(vorher) if vorher else -1
self.combo.setCurrentIndex(index if index >= 0 else 0)
self._stumm = False
if index < 0 and vorher:
self._auswahl_geaendert(0)
def _filter_geaendert(self, text):
self._liste_fuellen(text)
def _auswahl_geaendert(self, _index):
if not self._stumm:
self._ruf("verfahren_gewaehlt", self.combo.currentData())
def _ruf(self, name, *args):
rueckruf = self._cb.get(name)
if rueckruf:
rueckruf(*args)
+73 -93
View File
@@ -4,20 +4,20 @@ Ablauf:
- Der API-Key wird nach dem ersten Login in den QGIS-Einstellungen
gespeichert und bei jedem QGIS-Start wiederverwendet (kein erneuter
Login nötig, solange der Key gültig ist).
- Eine Toolbar-Auswahlliste zeigt alle Verfahren (TGs) aus GET /tgen.
Das gewählte Verfahren wird in der Projektdatei gemerkt und beim
Öffnen des Projekts wiederhergestellt.
- Buttons laden Verfahrensgebiet / Plan 41 / Karte alter Stand für das
gewählte Verfahren bzw. laden den aktiven Layer zur API hoch.
- Die Bedienung sitzt in einer andockbaren Seitenleiste rechts (``panel.py``,
seit 08.09.2026 anstelle der früheren Werkzeugleiste). Sie zeigt Anmeldung,
Verfahrensliste aus GET /tgen mit Suchfeld sowie die Knöpfe zum Laden,
Speichern und Hochladen.
- Das gewählte Verfahren wird in der Projektdatei gemerkt und beim Öffnen
des Projekts wiederhergestellt.
"""
from functools import partial
from qgis.PyQt.QtCore import QSettings, QTimer
from qgis.PyQt.QtWidgets import QAction, QComboBox, QDialog, QMessageBox
from qgis.PyQt.QtCore import QSettings, Qt, QTimer
from qgis.PyQt.QtWidgets import QAction, QDialog, QMessageBox
from qgis.core import Qgis, QgsDataSourceUri, QgsProject
from .api_client import ApiError, AuthError, KartenApiClient
from .panel import VlnPanel
from .layer_manager import (
PROP_DATASET,
PROP_VERFAHREN,
@@ -70,48 +70,39 @@ class VlnKartenPlugin:
self.iface = iface
self.client = KartenApiClient()
self.actions = []
self.toolbar = None
self.combo = None
# Unterdrückt das Mitschreiben in die Projektdatei, während die
# Auswahl programmatisch gesetzt wird.
self._restoring = False
self.panel = None
# ------------------------------------------------------------------
# Plugin-Lebenszyklus
# ------------------------------------------------------------------
def initGui(self):
self.toolbar = self.iface.addToolBar("VLN Karten")
self.toolbar.setObjectName("VlnKartenToolbar")
# Seitenleiste statt Werkzeugleiste (Erik, 08.09.2026). Die OGC-Knöpfe stehen oben:
# sie laden native, editierbare OAPIF-Layer, die QGIS selbst zurückschreibt (Part 4).
# Die GeoJSON-Knöpfe darunter sind der klassische Weg (Kopie laden, später hochladen).
self.panel = VlnPanel(
self.iface.mainWindow(),
callbacks={
"login": self.run_login,
"logout": self.run_logout,
"verfahren_gewaehlt": self.on_verfahren_changed,
"load_ogc": self.run_load_ogc,
"save_style": self.run_save_style,
"load_dataset": self.run_load_dataset,
"upload": self.run_upload_active_layer,
},
datensaetze=[(key, dataset["label"]) for key, dataset in DATASETS.items()],
)
self.iface.addDockWidget(Qt.DockWidgetArea.RightDockWidgetArea, self.panel)
self.login_action = self._add_action("Anmelden …", self.run_login)
self.combo = QComboBox()
self.combo.setMinimumWidth(280)
self.combo.setToolTip("Verfahren (Teilnehmergemeinschaft) auswählen")
self.combo.currentIndexChanged.connect(self.on_verfahren_changed)
self.toolbar.addWidget(self.combo)
self.dataset_actions = []
# OGC API Features (seit 07.09.2026) — der empfohlene Weg, darum ganz vorn: alle
# Sammlungen des Verfahrens als native, editierbare OAPIF-Layer; QGIS schreibt jede
# Änderung selbst zurück (Part 4). Die GeoJSON-Knöpfe darunter sind der klassische Weg.
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(
"%s laden" % dataset["label"],
partial(self.run_load_dataset, key),
)
)
self.dataset_actions.append(
self._add_action("Aktiven Layer hochladen", self.run_upload_active_layer)
)
# Menüeintrag zum Wiedereinblenden, falls die Leiste geschlossen wurde.
self.panel_action = QAction("Seitenleiste anzeigen", self.iface.mainWindow())
self.panel_action.setCheckable(True)
self.panel_action.setChecked(True)
self.panel_action.toggled.connect(self.panel.setVisible)
self.panel.visibilityChanged.connect(self.panel_action.setChecked)
self.iface.addPluginToWebMenu(MENU_TITLE, self.panel_action)
self.actions.append(self.panel_action)
self.iface.projectRead.connect(self.restore_project_selection)
self.iface.newProjectCreated.connect(self.restore_project_selection)
@@ -141,18 +132,10 @@ class VlnKartenPlugin:
for action in self.actions:
self.iface.removePluginWebMenu(MENU_TITLE, action)
self.actions = []
if self.toolbar is not None:
self.toolbar.deleteLater()
self.toolbar = None
self.combo = None
def _add_action(self, text, callback):
action = QAction(text, self.iface.mainWindow())
action.triggered.connect(callback)
self.iface.addPluginToWebMenu(MENU_TITLE, action)
self.toolbar.addAction(action)
self.actions.append(action)
return action
if self.panel is not None:
self.iface.removeDockWidget(self.panel)
self.panel.deleteLater()
self.panel = None
# ------------------------------------------------------------------
# Anmeldung & Verfahrensliste
@@ -205,20 +188,9 @@ class VlnKartenPlugin:
)
return
self._restoring = True
self.combo.clear()
self.combo.addItem("— Verfahren wählen —", None)
for v in verfahren:
label = "%s — %s" % (v["vkz"], v["name"]) if v["name"] else v["vkz"]
self.combo.addItem(label, v["vkz"])
self.combo.setEnabled(True)
self._restoring = False
self.login_action.setText(
"Angemeldet: %s" % (self.client.mail or "API-Key")
)
for action in self.dataset_actions:
action.setEnabled(True)
self.panel.set_angemeldet(True, self.client.mail or "API-Key")
self.panel.set_verfahren(verfahren)
self.panel.set_status("%d Verfahren geladen." % len(verfahren))
self.restore_project_selection()
def _handle_session_expired(self):
@@ -236,41 +208,43 @@ class VlnKartenPlugin:
self.run_login()
def _set_logged_out_ui(self):
self.login_action.setText("Anmelden …")
self._restoring = True
self.combo.clear()
self.combo.addItem("— bitte anmelden —", None)
self.combo.setEnabled(False)
self._restoring = False
for action in self.dataset_actions:
action.setEnabled(False)
if self.panel is not None:
self.panel.set_angemeldet(False)
self.panel.set_status("")
def run_logout(self):
"""Abmelden: gespeicherten API-Key verwerfen, Oberfläche zurücksetzen."""
settings = QSettings()
settings.beginGroup(SETTINGS_GROUP)
settings.remove("api_key")
settings.endGroup()
self.client.logout()
self.client = KartenApiClient()
self._set_logged_out_ui()
self.iface.messageBar().pushMessage(
"VLN Karten", "Abgemeldet — der gespeicherte API-Key wurde entfernt.",
level=Qgis.Info, duration=5)
# ------------------------------------------------------------------
# Verfahrens-Auswahl <-> Projektdatei
# ------------------------------------------------------------------
def on_verfahren_changed(self, index):
if self._restoring or self.combo is None:
return
vkz = self.combo.currentData()
def on_verfahren_changed(self, vkz):
if vkz:
QgsProject.instance().writeEntry(
PROJECT_SCOPE, PROJECT_KEY_VKZ, vkz
)
def restore_project_selection(self):
if self.combo is None or self.combo.count() == 0:
if self.panel is None or self.panel.anzahl_eintraege() == 0:
return
vkz, _ok = QgsProject.instance().readEntry(
PROJECT_SCOPE, PROJECT_KEY_VKZ, ""
)
self._restoring = True
index = self.combo.findData(vkz) if vkz else -1
self.combo.setCurrentIndex(index if index >= 0 else 0)
self._restoring = False
self.panel.waehle_vkz(vkz)
def _current_vkz(self):
return self.combo.currentData() if self.combo is not None else None
return self.panel.aktuelles_vkz() if self.panel is not None else None
# ------------------------------------------------------------------
# Laden & Hochladen
@@ -449,16 +423,18 @@ class VlnKartenPlugin:
return
# Schrittweise: je Ereignisschleifen-Durchlauf ein Layer, Fortschritt in der
# Meldungsleiste. Fehler kommen dort am Ende gesammelt, nicht als Modal-Dialog.
self.panel.set_status("Lade %d Sammlungen …" % len(collections))
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)
self.iface, self.client.base_url, vkz, self.panel.aktueller_text(), collections,
authcfg, api_key=None if authcfg else self.client.api_key)
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").
sonst mit 403. Ist in der Seitenleiste „als Vorlage für alle Verfahren“ angehakt (oder
die Umschalttaste gedrückt), gilt der Stil für die **Art** statt nur für dieses
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
@@ -473,7 +449,8 @@ class VlnKartenPlugin:
if not cid:
self._show_error("Kein OGC-Layer", "Die Datenquelle nennt keine Sammlung (typename).")
return
vorlage = bool(QApplication.keyboardModifiers() & _Qt.KeyboardModifier.ShiftModifier)
vorlage = (self.panel is not None and self.panel.als_vorlage()) or bool(
QApplication.keyboardModifiers() & _Qt.KeyboardModifier.ShiftModifier)
settings = QSettings()
settings.beginGroup(SETTINGS_GROUP)
authcfg = settings.value(ogc_layers.SETTINGS_AUTHCFG, "")
@@ -489,6 +466,9 @@ class VlnKartenPlugin:
self.iface.messageBar().pushMessage(
"VLN Karten", "Stil für %s gespeichert — %s" % (cid, text),
level=Qgis.Success, duration=8)
if self.panel is not None:
self.panel.set_status("Stil gespeichert: %s%s"
% (cid, " (Vorlage)" if vorlage else ""))
else:
self._show_error("Stil nicht gespeichert", text)