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