"""Hauptklasse des Plugins. 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). - 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 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, ist_landkreis from .login_dialog import LoginDialog, SETTINGS_GROUP from . import linkliste from . import ogc_layers MENU_TITLE = "&VLN Karten" # Schlüssel für die VKZ-Ablage in der Projektdatei (*.qgz). PROJECT_SCOPE = "vln_karten" PROJECT_KEY_VKZ = "/vkz" class VlnKartenPlugin: def __init__(self, iface): self.iface = iface self.client = KartenApiClient() self.actions = [] self.panel = None self._werkzeuge = {"schild": None, "zusatz": None} # Kartenwerkzeuge, faul gebaut # ------------------------------------------------------------------ # Plugin-Lebenszyklus # ------------------------------------------------------------------ def initGui(self): # Seitenleiste statt Werkzeugleiste (Erik, 08.09.2026). Geladen und gespeichert wird # ausschließlich über die OGC API: die Layer sind nativ editierbar, QGIS schreibt jede # Änderung einzeln zurück (Part 4). Der frühere GeoJSON-Weg ist entfallen. 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_edits": self.run_save_edits, "dienste_holen": self.run_dienste_holen, "dienste_laden": self.run_dienste_laden, "save_style": self.run_save_style, "zeichen_setzen": self.run_zeichen_setzen, "zusatz_anhaengen": self.run_zusatz_anhaengen, }, ) 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) self._set_logged_out_ui() # Gespeicherten API-Key aus früherer Sitzung wiederverwenden; sonst gleich beim Start # anmelden (Erik, 07.09.2026: „zuerst ein Loginfenster, danach den API-Key speichern"). # Beides erst NACH dem QGIS-Start (singleShot), sonst blockiert der Dialog das Laden. settings = QSettings() settings.beginGroup(SETTINGS_GROUP) api_key = settings.value("api_key", "") mail = settings.value("mail", "") settings.endGroup() if api_key: self.client = KartenApiClient(api_key=api_key, mail=mail) QTimer.singleShot(0, self.populate_verfahren) # Der Zeichenkatalog ist oeffentlich und liegt lokal — er kostet nichts und die # Palette ist damit gleich nach dem Start bedienbar. QTimer.singleShot(0, self._katalog_laden) else: QTimer.singleShot(1500, self.run_login) def unload(self): for signal in (self.iface.projectRead, self.iface.newProjectCreated): try: signal.disconnect(self.restore_project_selection) except (TypeError, RuntimeError): pass for action in self.actions: self.iface.removePluginWebMenu(MENU_TITLE, action) self.actions = [] if self.panel is not None: self.iface.removeDockWidget(self.panel) self.panel.deleteLater() self.panel = None # ------------------------------------------------------------------ # Anmeldung & Verfahrensliste # ------------------------------------------------------------------ def run_login(self): dialog = LoginDialog(self.iface.mainWindow()) if dialog.exec() != QDialog.DialogCode.Accepted: return client = KartenApiClient() try: client.login(dialog.mail(), dialog.password()) except ApiError as exc: self._show_error("Anmeldung fehlgeschlagen", str(exc)) return self.client = client # Nach dem Login wird der API-Key (userauth) gespeichert und in jeder weiteren Sitzung # wiederverwendet — das Passwort selbst speichert das Plugin nie. Ablage: QSettings # („vln_karten/api_key"), zurücksetzen über „Abmelden". settings = QSettings() settings.beginGroup(SETTINGS_GROUP) settings.setValue("api_key", client.api_key) settings.setValue("mail", client.mail or dialog.mail()) settings.endGroup() self._katalog_laden() # Palette fuellen (oeffentlicher Katalog) self.iface.messageBar().pushMessage( "VLN Karten", "Angemeldet als %s — der API-Key ist gespeichert und gilt auch nach einem " "QGIS-Neustart." % (client.mail or dialog.mail()), level=Qgis.Success, duration=6, ) self.populate_verfahren() def populate_verfahren(self): """Füllt die Auswahlliste aus ``GET /tgen/auswahl``: eigene Verfahren, als oFB die Verfahren des Landkreises (nur lesen) und die Landkreise selbst (für die Linkliste).""" try: auswahl = self.client.get_auswahl() except AuthError: self._handle_session_expired() return except ApiError as exc: self.iface.messageBar().pushMessage( "VLN Karten", "Verfahrensliste konnte nicht geladen werden: %s" % exc, level=Qgis.Warning, duration=10, ) return self.panel.set_angemeldet(True, self.client.mail or "API-Key") verfahren, aemter = auswahl["verfahren"], auswahl["aemter"] self.panel.set_verfahren(verfahren, aemter) text = "%d Verfahren geladen" % len(verfahren) lesend = sum(1 for v in verfahren if v.get("zugriff") == "lesen") if lesend: text += " (davon %d nur lesen)" % lesend if aemter: text += ", %d Landkreis(e) für die Linkliste" % len(aemter) self.panel.set_status(text + ".") self.restore_project_selection() def _handle_session_expired(self): settings = QSettings() settings.beginGroup(SETTINGS_GROUP) settings.remove("api_key") settings.endGroup() self.client.logout() self._set_logged_out_ui() self._show_error( "Sitzung abgelaufen", "Der gespeicherte API-Key ist nicht mehr gültig — " "bitte neu anmelden.", ) self.run_login() def _set_logged_out_ui(self): 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, vkz): if vkz: QgsProject.instance().writeEntry( PROJECT_SCOPE, PROJECT_KEY_VKZ, vkz ) def restore_project_selection(self): if self.panel is None or self.panel.anzahl_eintraege() == 0: return vkz, _ok = QgsProject.instance().readEntry( PROJECT_SCOPE, PROJECT_KEY_VKZ, "" ) self.panel.waehle_vkz(vkz) def _current_vkz(self): return self.panel.aktuelles_vkz() if self.panel is not None else None # ------------------------------------------------------------------ # Laden & Hochladen # ------------------------------------------------------------------ def run_load_ogc(self): """Alle Sammlungen des gewählten Verfahrens (Verfahrens-Layer, Vorerhebung, Maßnahmenblätter) als OAPIF-Layer laden — editierbar in QGIS, Rückschreiben je Feature.""" if not self._ensure_logged_in(): return vkz = self._current_vkz() if not vkz: self._show_error("Kein Verfahren", "Bitte zuerst ein Verfahren auswählen.") return if ist_landkreis(vkz): self._show_error("Landkreis gewählt", "Für einen Landkreis gibt es keine Verfahrenslayer — bitte ein " "Verfahren wählen. Die Linkliste des Landkreises holen Sie unter " "„Geodienste“.") return authcfg = ogc_layers.authcfg_sicherstellen(self.client.api_key, SETTINGS_GROUP) if not authcfg: self.iface.messageBar().pushMessage( "VLN Karten", "Keine QGIS-Authentifizierungskonfiguration — der API-Key steht jetzt in den " "Datenquellen der Layer (und damit in einer gespeicherten Projektdatei).", level=Qgis.Warning, duration=8) try: collections = ogc_layers.collections_laden(self.client, vkz) except AuthError: self._handle_session_expired() return except ApiError as exc: self._show_error("OGC API", str(exc)) return if not collections: self._show_error("OGC API", "Für dieses Verfahren gibt es keine Sammlungen " "(oder keine Berechtigung).") return # Nur die in der Seitenleiste angehakten Gruppen laden — jede Sammlung kostet beim # Anlegen rund eine halbe Sekunde, und selten braucht man alle 27 gleichzeitig. gewaehlt = self.panel.gewaehlte_gruppen() gruppe_von = dict(ogc_layers.GRUPPEN) collections = [c for c in collections if gruppe_von.get(str(c.get("id") or "").split("-", 1)[0], "Weitere") in gewaehlt] if not collections: self._show_error("Nichts ausgewählt", "Bitte oben mindestens eine Gruppe anhaken (Verfahren, " "Vorerhebung, Maßnahmenblätter oder Widmungen).") 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.panel.aktueller_text(), collections, authcfg, api_key=None if authcfg else self.client.api_key, client=self.client) # ------------------------------------------------------------------ # Verkehrszeichen setzen # ------------------------------------------------------------------ def _zeichen_melden(self, text, fehler=False): self.panel.set_status(text) if fehler: self.iface.messageBar().pushMessage("VLN Karten", text, level=Qgis.Warning, duration=6) def _werkzeug(self, art): """Das Kartenwerkzeug bauen (einmal je Art) — beide teilen Palette und Blattwahl.""" from . import zeichen_werkzeug as zwm if self._werkzeuge.get(art) is None: canvas = self.iface.mapCanvas() code = self.panel.aktueller_zeichencode if art == "schild": self._werkzeuge[art] = zwm.SchildSetzen(canvas, self.iface, code, self._zeichen_melden, self.panel.aktuelles_blatt) else: self._werkzeuge[art] = zwm.ZusatzAnhaengen(canvas, self.iface, code, self._zeichen_melden) return self._werkzeuge[art] def _werkzeug_schalten(self, art, knopf): canvas = self.iface.mapCanvas() if not knopf.isChecked(): canvas.unsetMapTool(self._werkzeuge.get(art)) return # Nur eines von beiden zur Zeit. self.panel.werkzeug_zuruecksetzen() knopf.setChecked(True) werkzeug = self._werkzeug(art) if hasattr(werkzeug, "reset"): werkzeug.reset() canvas.setMapTool(werkzeug) def run_zeichen_setzen(self): """Werkzeug „Schild setzen" ein- oder ausschalten.""" self._blaetter_auffrischen() self._werkzeug_schalten("schild", self.panel.schild_knopf) def run_zusatz_anhaengen(self): """Werkzeug „Zusatzzeichen anhaengen" ein- oder ausschalten.""" self._werkzeug_schalten("zusatz", self.panel.zusatz_knopf) def _blaetter_auffrischen(self): """Die Blattwahl aus den geladenen Layern fuellen (keine eigene Abfrage noetig).""" try: from . import zeichen_werkzeug as zwm self.panel.setze_blaetter(zwm.blaetter_im_projekt()) except Exception: # noqa: BLE001 pass def _katalog_laden(self): """Den Zeichenkatalog in die Palette holen (Server, sonst lokale Ablage).""" try: from . import zeichen_katalog as zk self.panel.palette.setze_katalog(zk.katalog(self.client), self.client) except Exception: # noqa: BLE001 pass # ohne Katalog bleibt die Palette leer def run_save_edits(self): """Offene Bearbeitungssitzungen aller geladenen VLN-Layer schließen. Über die OGC API schreibt QGIS jede Änderung einzeln zurück (Part 4) — dieser Knopf beendet nur die Bearbeitung, so wie „Layeränderungen speichern" in QGIS, aber für alle Layer des Verfahrens auf einmal. """ layer = [l for l in QgsProject.instance().mapLayers().values() if getattr(l, "providerType", None) and l.providerType() == "OAPIF"] offen = [l for l in layer if l.isEditable()] if not offen: self.iface.messageBar().pushMessage( "VLN Karten", "Keine offene Bearbeitung — es gibt nichts zu speichern.", level=Qgis.Info, duration=5) return gespeichert, fehler = [], [] for l in offen: if l.commitChanges(): gespeichert.append(l.name()) else: fehler.append("%s: %s" % (l.name(), "; ".join(l.commitErrors())[:120])) if fehler: self._show_error("Nicht alles gespeichert", "\n".join(fehler)) if gespeichert: self.iface.messageBar().pushMessage( "VLN Karten", "%d Layer gespeichert (%s)." % (len(gespeichert), ", ".join(gespeichert[:4])), level=Qgis.Success, duration=6) if self.panel is not None: self.panel.set_status("%d von %d Layern gespeichert." % (len(gespeichert), len(offen))) def run_dienste_holen(self): """Die Linkliste des gewählten Verfahrens oder Landkreises holen und anzeigen. Der Server liefert sie bereits aufgelöst (landesweit und Landkreis), geerbte Einträge tragen ihre Herkunft. Das Plugin rechnet nichts nach, es zeigt und lädt. Ein 403 meldet nicht ab (seit 0.7.0), sondern zeigt nur, dass hier keine Berechtigung besteht. """ if not self._ensure_logged_in(): return vkz = self._current_vkz() if not vkz: self._show_error("Kein Verfahren", "Bitte zuerst ein Verfahren auswählen.") return try: dienste = linkliste.dienste_laden(self.client, vkz) except AuthError: self._handle_session_expired() return except ApiError as exc: self._show_error("Linkliste", str(exc)) return if not self.panel.karten_haken: # Kartenkatalog einmal je Sitzung; ohne ihn bleibt es beim Anhaken von Hand. try: self.panel.set_karten(linkliste.karten_laden(self.client)) except ApiError: self.panel.set_karten(linkliste.KARTEN_VORGABE) self.panel.set_dienste(dienste, vkz) herkunft = {} for d in dienste: herkunft[d.get("herkunft_text") or "?"] = herkunft.get(d.get("herkunft_text") or "?", 0) + 1 text = ", ".join("%d %s" % (n, k) for k, n in herkunft.items()) or "nichts hinterlegt" self.panel.set_status("%d Geodienste: %s" % (len(dienste), text)) wofuer = "diesen Landkreis" if ist_landkreis(vkz) else "dieses Verfahren" self.iface.messageBar().pushMessage( "VLN Karten", "%d Geodienste für %s (%s)." % (len(dienste), wofuer, text), level=Qgis.Info, duration=6) def run_dienste_laden(self): """Die angehakten Dienste als Layer in die Gruppe „Geodienste" hängen.""" if not self._ensure_logged_in(): return vkz = self._current_vkz() if not vkz: self._show_error("Kein Verfahren", "Bitte zuerst ein Verfahren auswählen.") return gewaehlt = self.panel.gewaehlte_dienste() if not gewaehlt: self._show_error("Nichts ausgewählt", "Bitte zuerst die Liste holen und dann die gewünschten Dienste " "anhaken.") return linkliste.dienste_hinzufuegen( self.iface, self.client.base_url, vkz, self.panel.aktueller_text(), gewaehlt, api_key=self.client.api_key) self.panel.set_status("%d Geodienste werden geladen …" % len(gewaehlt)) 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. 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 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 = (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, "") 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) 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) # ------------------------------------------------------------------ # Hilfsfunktionen # ------------------------------------------------------------------ def _ensure_logged_in(self): if self.client.is_authenticated: return True self.run_login() return self.client.is_authenticated def _show_error(self, title, message): QMessageBox.warning(self.iface.mainWindow(), title, message)