diff --git a/README.md b/README.md index 743b9c2..b4c2c23 100644 --- a/README.md +++ b/README.md @@ -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 " (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 | diff --git a/tools/headless_panel_test.py b/tools/headless_panel_test.py new file mode 100644 index 0000000..84ce712 --- /dev/null +++ b/tools/headless_panel_test.py @@ -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) diff --git a/vln_karten/metadata.txt b/vln_karten/metadata.txt index 7981ca1..594c166 100644 --- a/vln_karten/metadata.txt +++ b/vln_karten/metadata.txt @@ -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 diff --git a/vln_karten/panel.py b/vln_karten/panel.py new file mode 100644 index 0000000..8d27340 --- /dev/null +++ b/vln_karten/panel.py @@ -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) diff --git a/vln_karten/plugin.py b/vln_karten/plugin.py index b224eb2..b6bd17d 100644 --- a/vln_karten/plugin.py +++ b/vln_karten/plugin.py @@ -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") - - 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) + # 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) + + # 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)