"""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). - 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. """ from functools import partial from qgis.PyQt.QtCore import QSettings, QTimer from qgis.PyQt.QtWidgets import QAction, QComboBox, QDialog, QMessageBox from qgis.core import Qgis, QgsProject from .api_client import ApiError, AuthError, KartenApiClient from .layer_manager import ( PROP_DATASET, PROP_VERFAHREN, feature_collection_to_layers, layer_api_path, layers_to_feature_collection, plugin_layers, ) from .login_dialog import LoginDialog, SETTINGS_GROUP MENU_TITLE = "&VLN Karten" # Schlüssel für die VKZ-Ablage in der Projektdatei (*.qgz). PROJECT_SCOPE = "vln_karten" PROJECT_KEY_VKZ = "/vkz" # Karten-Layer der VLN-Manager-API (map.php). Weitere Layer (st, we) # wären je ein Eintrag mehr. {vkz} = Verfahrenskennzeichen der TG. DATASETS = { "umringe": { "label": "Verfahrensgebiet", "path": "/maps/umringe/{vkz}", "geometry": "MultiPolygon", }, "p41": { "label": "Plan 41 (Wege- und Gewässerplan)", "path": "/maps/p41/{vkz}", "geometry": "MultiPolygon", }, # DB-Art KAS: enthält, was im Web-GIS über "in Datenbank speichern" # abgelegt wurde — kann je VKZ leer sein, solange dort noch nichts # gespeichert wurde. "kas": { "label": "Karte alter Stand (KAS)", "path": "/maps/kas/{vkz}", "geometry": "MultiPolygon", }, # DB-Art WE: Wertermittlung (Web-GIS-Modul we.js). "we": { "label": "Wertermittlung (WE)", "path": "/maps/we/{vkz}", "geometry": "MultiPolygon", }, } class VlnKartenPlugin: def __init__(self, iface): 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 # ------------------------------------------------------------------ # 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 = [] 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) ) 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; # die Verfahrensliste erst nach dem QGIS-Start abrufen. 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) 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.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 # ------------------------------------------------------------------ # 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 settings = QSettings() settings.beginGroup(SETTINGS_GROUP) settings.setValue("api_key", client.api_key) settings.endGroup() self.iface.messageBar().pushMessage( "VLN Karten", "Anmeldung erfolgreich — die Zugangsdaten werden für künftige " "QGIS-Sitzungen gemerkt.", level=Qgis.Success, duration=5, ) self.populate_verfahren() def populate_verfahren(self): """Füllt die Auswahlliste mit allen Verfahren aus GET /tgen.""" try: verfahren = self.client.get_verfahren() 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._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.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): 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) # ------------------------------------------------------------------ # Verfahrens-Auswahl <-> Projektdatei # ------------------------------------------------------------------ def on_verfahren_changed(self, index): if self._restoring or self.combo is None: return vkz = self.combo.currentData() 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: 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 def _current_vkz(self): return self.combo.currentData() if self.combo is not None else None # ------------------------------------------------------------------ # Laden & Hochladen # ------------------------------------------------------------------ def run_load_dataset(self, dataset_key): if not self._ensure_logged_in(): return vkz = self._current_vkz() if not vkz: self._show_error( "Kein Verfahren gewählt", "Bitte zuerst in der Auswahlliste ein Verfahren wählen.", ) return dataset = DATASETS[dataset_key] if not self._confirm_replace_existing(dataset_key, vkz, dataset["label"]): return path = dataset["path"].format(vkz=vkz) try: # Vollständig aus KARTE_OBJEKT: Listen-Endpunkt mit Paging # (wie das Web-GIS, aber ohne 2000er-Limit). feature_collection = self.client.load_layer_complete(dataset_key, vkz) layers = feature_collection_to_layers( feature_collection, "%s %s" % (dataset["label"], vkz), dataset["geometry"], dataset_key, vkz, path, ) except AuthError: self._handle_session_expired() return except (ApiError, RuntimeError) as exc: self._show_error("Laden fehlgeschlagen", str(exc)) return total = sum(layer.featureCount() for layer in layers) if total == 0: self.iface.messageBar().pushMessage( "VLN Karten", "Der Server hat für VKZ %s keine Objekte im Layer " "'%s' — es wurde ein leerer Layer zum Digitalisieren " "angelegt." % (vkz, dataset["label"]), level=Qgis.Warning, duration=10, ) return detail = ", ".join( "%s: %d" % (layer.name(), layer.featureCount()) for layer in layers ) self.iface.messageBar().pushMessage( "VLN Karten", "%d Layer geladen (%s)." % (len(layers), detail), level=Qgis.Success, duration=5, ) def _confirm_replace_existing(self, dataset_key, vkz, label): """Ersetzt bereits geladene Layer dieses Datensatzes nach Rückfrage — doppelte Layer-Sätze würden beim Hochladen alle Objekte dupliziert an den Server schicken.""" existing = plugin_layers(dataset_key, vkz) if not existing: return True answer = QMessageBox.question( self.iface.mainWindow(), "Layer ersetzen", "%s für VKZ %s ist bereits im Projekt geladen (%s).\n" "Beim Neuladen werden diese Layer ersetzt — nicht " "hochgeladene Änderungen gehen verloren. Fortfahren?" % (label, vkz, ", ".join(layer.name() for layer in existing)), ) if answer != QMessageBox.StandardButton.Yes: return False QgsProject.instance().removeMapLayers([layer.id() for layer in existing]) return True def run_upload_active_layer(self): if not self._ensure_logged_in(): return active = self.iface.activeLayer() path = layer_api_path(active) if active else None if not path: self._show_error( "Hochladen nicht möglich", "Der aktive Layer wurde nicht über dieses Plugin geladen. " "Bitte einen über 'VLN Karten' geladenen Layer auswählen.", ) return # Der PUT ersetzt den kompletten Layer-Bestand der VKZ auf dem # Server. Deshalb werden immer alle Teil-Layer dieses Datensatzes # (Punkte/Linien/Flächen) gemeinsam hochgeladen — nur den aktiven # zu senden, würde die anderen Geometrietypen serverseitig löschen. dataset_key = active.customProperty(PROP_DATASET) vkz = active.customProperty(PROP_VERFAHREN) siblings = plugin_layers(dataset_key, vkz) for layer in siblings: if layer.isEditable() and not layer.commitChanges(): self._show_error( "Hochladen nicht möglich", "Die offene Bearbeitungssitzung von '%s' konnte nicht " "gespeichert werden." % layer.name(), ) return dataset = DATASETS.get(dataset_key, {}) total = sum(layer.featureCount() for layer in siblings) listing = "\n".join( "• %s (%d Objekte)" % (layer.name(), layer.featureCount()) for layer in siblings ) answer = QMessageBox.question( self.iface.mainWindow(), "Zum Server hochladen", "%s für VKZ %s übertragen?\n\n%s\n\n" "Die Layer werden zusammengeführt (%d Objekte); der bisherige " "Bestand dieser VKZ auf dem Server wird ersetzt." % (dataset.get("label", dataset_key), vkz, listing, total), ) if answer != QMessageBox.StandardButton.Yes: return try: feature_collection = layers_to_feature_collection(siblings) self.client.save_feature_collection(path, feature_collection) except AuthError: self._handle_session_expired() return except ApiError as exc: self._show_error("Hochladen fehlgeschlagen", str(exc)) return self.iface.messageBar().pushMessage( "VLN Karten", "%d Objekte aus %d Layer(n) erfolgreich hochgeladen." % (total, len(siblings)), level=Qgis.Success, duration=5, ) # ------------------------------------------------------------------ # 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)