Merge pull request 'VLN-API integriert' (#25) from API-Integration_Plan41 into unstable

Reviewed-on: #25
This commit was merged in pull request #25.
This commit is contained in:
2026-06-19 11:49:43 +02:00
8 changed files with 3259 additions and 9 deletions
+368 -9
View File
@@ -9,7 +9,9 @@ from typing import Optional
from sn_basis.functions.qt_wrapper import (
QWidget,
QVBoxLayout,
QHBoxLayout,
QLabel,
QLineEdit,
QPushButton,
QToolButton,
QFileDialog,
@@ -32,7 +34,8 @@ from sn_basis.modules.DataGrabber import DataGrabber
from sn_basis.modules.Dateipruefer import Dateipruefer
from sn_plan41.ui.tab_a_logic import TabALogic
from sn_basis.modules.linkpruefer import Linkpruefer
from sn_basis.modules.stilpruefer import Stilpruefer
from sn_basis.modules.stilpruefer import Stilpruefer
from sn_plan41.modules.vln_api_logic import VlnApiLogic
# Konstanten
RAUMFILTER_VAR = "Raumfilter"
@@ -78,7 +81,11 @@ class TabA(QWidget):
# UI-Referenzen
self._raumfilter_combo: Optional[QComboBox] = None
# VLN-API
self.vln_logic: Optional[VlnApiLogic] = None
self._vln_restoring: bool = False
self._build_ui()
self._restore_state()
@@ -92,14 +99,17 @@ class TabA(QWidget):
self.pruefmanager = pruefmanager
self.data_grabber = data_grabber
self.logic = TabALogic(
pruefmanager=self.pruefmanager,
link_pruefer=Linkpruefer(),
stil_pruefer=Stilpruefer(),
)
# DataGrabber in die Logik injizieren
pruefmanager=self.pruefmanager,
link_pruefer=Linkpruefer(),
stil_pruefer=Stilpruefer(),
)
self.logic.data_grabber = self.data_grabber
# VLN-API-Logik initialisieren
self.vln_logic = VlnApiLogic(self.pruefmanager)
self._update_vln_ui_state()
self._restore_vln_state()
def _build_ui(self) -> None:
"""Erstellt die komplette UI-Hierarchie mit allen Gruppen."""
main_layout = QVBoxLayout()
@@ -180,7 +190,84 @@ class TabA(QWidget):
self.btn_pipeline.clicked.connect(self._on_load_fachdaten)
main_layout.addWidget(self.btn_pipeline)
# === VLN API / PLAN 41 ===
self._vln_group_button = QToolButton()
self._vln_group_button.setText("VLN API / Plan 41")
self._vln_group_button.setCheckable(True)
self._vln_group_button.setChecked(False)
self._vln_group_button.setToolButtonStyle(ToolButtonTextBesideIcon)
self._vln_group_button.setArrowType(ArrowRight)
self._vln_group_button.setStyleSheet("font-weight: bold; margin-top: 8px;")
self._vln_group_button.toggled.connect(self._toggle_vln_group)
main_layout.addWidget(self._vln_group_button)
self._vln_group_content = QWidget()
self._vln_group_content.setSizePolicy(SizePolicyPreferred, SizePolicyMaximum)
vln_layout = QVBoxLayout()
vln_layout.setSpacing(4)
vln_layout.setContentsMargins(10, 4, 4, 8)
# Status
self._vln_status_label = QLabel("Nicht angemeldet")
self._vln_status_label.setStyleSheet("color: gray; font-style: italic;")
vln_layout.addWidget(self._vln_status_label)
# E-Mail und Passwort
vln_layout.addWidget(QLabel("E-Mail:"))
self._vln_mail_edit = QLineEdit()
self._vln_mail_edit.setPlaceholderText("mail@beispiel.de")
vln_layout.addWidget(self._vln_mail_edit)
vln_layout.addWidget(QLabel("Passwort:"))
self._vln_pass_edit = QLineEdit()
try:
self._vln_pass_edit.setEchoMode(QLineEdit.EchoMode.Password)
except AttributeError:
self._vln_pass_edit.setEchoMode(QLineEdit.Password)
vln_layout.addWidget(self._vln_pass_edit)
# Login / Logout Buttons
btn_row = QHBoxLayout()
self._btn_vln_login = QPushButton("Anmelden")
self._btn_vln_login.clicked.connect(self._on_vln_login)
self._btn_vln_logout = QPushButton("Abmelden")
self._btn_vln_logout.clicked.connect(self._on_vln_logout)
btn_row.addWidget(self._btn_vln_login)
btn_row.addWidget(self._btn_vln_logout)
vln_layout.addLayout(btn_row)
# Verfahren
vln_layout.addWidget(QLabel("Verfahren:"))
self._vln_verfahren_combo = QComboBox()
self._vln_verfahren_combo.setToolTip("Verfahren (Teilnehmergemeinschaft) wählen")
self._vln_verfahren_combo.setEnabled(False)
self._vln_verfahren_combo.currentIndexChanged.connect(
self._on_vln_verfahren_changed
)
vln_layout.addWidget(self._vln_verfahren_combo)
# Plan 41 laden
self._btn_p41_laden = QPushButton("Plan 41 (Wege- und Gewässerplan) laden")
self._btn_p41_laden.setToolTip(
"Lädt Flächen- und Linienlayer für das gewählte Verfahren vom Server"
)
self._btn_p41_laden.setEnabled(False)
self._btn_p41_laden.clicked.connect(self._on_p41_laden)
vln_layout.addWidget(self._btn_p41_laden)
# Upload
self._btn_upload = QPushButton("Aktiven Layer hochladen")
self._btn_upload.setToolTip(
"Lädt alle Plan-41-Layer des aktiven Verfahrens zum Server hoch"
)
self._btn_upload.setEnabled(False)
self._btn_upload.clicked.connect(self._on_upload_active)
vln_layout.addWidget(self._btn_upload)
self._vln_group_content.setLayout(vln_layout)
self._vln_group_content.setVisible(False)
main_layout.addWidget(self._vln_group_content)
main_layout.addStretch(1)
self.setLayout(main_layout)
@@ -206,7 +293,279 @@ class TabA(QWidget):
pass
# Raumfilter (schon im _build_ui behandelt)
def _restore_vln_state(self) -> None:
"""Stellt die zuletzt gewählte VKZ aus der Projektdatei wieder her."""
if self.vln_logic is None or not self.vln_logic.is_authenticated:
return
try:
self._populate_verfahren_combo(restore_vkz=True)
except Exception:
pass
# === UI CALLBACKS ===
def _toggle_vln_group(self, checked: bool) -> None:
self._vln_group_button.setArrowType(ArrowDown if checked else ArrowRight)
self._vln_group_content.setVisible(checked)
def _update_vln_ui_state(self) -> None:
"""Passt alle VLN-Controls an den aktuellen Authentifizierungsstatus an."""
if self.vln_logic is None:
authenticated = False
available = False
else:
authenticated = self.vln_logic.is_authenticated
available = self.vln_logic.is_available()
if not available:
self._vln_status_label.setText(
"Plugin 'vln_karten' nicht installiert"
)
self._vln_status_label.setStyleSheet("color: red; font-style: italic;")
self._btn_vln_login.setEnabled(False)
elif authenticated:
mail = (self.vln_logic.mail or "API-Key") if self.vln_logic else ""
self._vln_status_label.setText("Angemeldet: %s" % mail)
self._vln_status_label.setStyleSheet(
"color: green; font-style: italic;"
)
self._btn_vln_login.setEnabled(False)
else:
self._vln_status_label.setText("Nicht angemeldet")
self._vln_status_label.setStyleSheet("color: gray; font-style: italic;")
self._btn_vln_login.setEnabled(True)
self._btn_vln_logout.setEnabled(authenticated)
self._vln_verfahren_combo.setEnabled(authenticated)
vkz_chosen = (
authenticated
and self._vln_verfahren_combo.currentData() is not None
)
self._btn_p41_laden.setEnabled(vkz_chosen)
self._btn_upload.setEnabled(authenticated)
def _populate_verfahren_combo(self, restore_vkz: bool = False) -> None:
"""Füllt die Verfahren-Combo und wählt ggf. die gespeicherte VKZ."""
if self.vln_logic is None:
return
verfahren = self.vln_logic.get_verfahren()
self._vln_restoring = True
self._vln_verfahren_combo.clear()
self._vln_verfahren_combo.addItem("— Verfahren wählen —", None)
for v in verfahren:
label = (
"%s — %s" % (v["vkz"], v["name"])
if v["name"]
else v["vkz"]
)
self._vln_verfahren_combo.addItem(label, v["vkz"])
self._vln_restoring = False
if restore_vkz:
stored_vkz = self.vln_logic.load_stored_vkz()
if stored_vkz:
idx = self._vln_verfahren_combo.findData(stored_vkz)
if idx >= 0:
self._vln_verfahren_combo.setCurrentIndex(idx)
self._vln_verfahren_combo.setEnabled(True)
self._update_vln_ui_state()
def _on_vln_login(self) -> None:
"""Führt den API-Login durch."""
if self.vln_logic is None:
return
mail = self._vln_mail_edit.text().strip()
password = self._vln_pass_edit.text()
if not mail or not password:
QMessageBox.warning(
self,
"Anmeldung",
"Bitte E-Mail-Adresse und Passwort eingeben.",
)
return
try:
self.vln_logic.login(mail, password)
except Exception as exc:
QMessageBox.warning(self, "Anmeldung fehlgeschlagen", str(exc))
return
self._vln_pass_edit.clear()
try:
self._populate_verfahren_combo(restore_vkz=True)
except Exception as exc:
QMessageBox.warning(
self,
"Verfahrensliste",
"Anmeldung erfolgreich, aber Verfahrensliste konnte nicht "
"geladen werden: %s" % exc,
)
self._update_vln_ui_state()
def _on_vln_logout(self) -> None:
"""Meldet den Benutzer lokal ab."""
if self.vln_logic is None:
return
self.vln_logic.logout()
self._vln_restoring = True
self._vln_verfahren_combo.clear()
self._vln_verfahren_combo.setEnabled(False)
self._vln_restoring = False
self._update_vln_ui_state()
def _on_vln_verfahren_changed(self, _index: int) -> None:
"""Persistiert die gewählte VKZ."""
if self._vln_restoring or self.vln_logic is None:
return
vkz = self._vln_verfahren_combo.currentData()
self.vln_logic.save_vkz(vkz)
self._update_vln_ui_state()
def _on_p41_laden(self) -> None:
"""Lädt den Plan-41-Datensatz für die gewählte VKZ."""
if self.vln_logic is None:
return
vkz = self._vln_verfahren_combo.currentData()
if not vkz:
QMessageBox.warning(
self, "Plan 41 laden", "Bitte zuerst ein Verfahren wählen."
)
return
# Bereits geladene Layer prüfen
existing = self.vln_logic.get_p41_layers(vkz)
if existing:
try:
answer = QMessageBox.question(
self,
"Layer ersetzen",
"Plan 41 für VKZ %s ist bereits geladen.\n"
"Beim Neuladen werden die bestehenden Layer ersetzt. Fortfahren?"
% vkz,
)
# Qt6: StandardButton.Yes, Qt5: QMessageBox.Yes
yes = getattr(
getattr(QMessageBox, "StandardButton", QMessageBox),
"Yes",
QMessageBox.Yes if hasattr(QMessageBox, "Yes") else None,
)
if answer != yes:
return
except Exception:
pass
self.vln_logic.remove_p41_layers(vkz)
try:
layers = self.vln_logic.load_p41(vkz)
except Exception as exc:
# AuthError: Sitzung abgelaufen
try:
from sn_plan41.modules.vln_api_client import AuthError as _AuthError
if isinstance(exc, _AuthError):
self.vln_logic.handle_session_expired()
self._on_vln_logout()
QMessageBox.warning(
self,
"Sitzung abgelaufen",
"Der API-Key ist nicht mehr gültig — bitte neu anmelden.",
)
return
except ImportError:
pass
QMessageBox.warning(self, "Laden fehlgeschlagen", str(exc))
return
total = sum(l.featureCount() for l in layers)
detail = ", ".join(
"%s: %d" % (l.name(), l.featureCount()) for l in layers
)
if total == 0:
QMessageBox.warning(
self,
"Plan 41 laden",
"Keine Objekte vorhanden — ein leerer Layer wurde angelegt.",
)
else:
try:
from sn_basis.functions.message_wrapper import info
info(
"Plan 41 geladen",
"%d Layer geladen (%s)." % (len(layers), detail),
)
except Exception:
pass
def _on_upload_active(self) -> None:
"""Lädt den aktiven Layer (und Geschwister-Layer) zum Server hoch."""
if self.vln_logic is None:
return
# Zusammenfassung für Bestaetigungs-Dialog ermitteln
try:
from sn_basis.functions.qgisui_wrapper import iface as _iface
active_layer = _iface.activeLayer() if _iface else None
except Exception:
active_layer = None
summary = self.vln_logic.upload_summary(active_layer)
if summary is None:
QMessageBox.warning(
self,
"Hochladen nicht möglich",
"Der aktive Layer wurde nicht über 'vln_karten' geladen.",
)
return
try:
answer = QMessageBox.question(
self,
"Zum Server hochladen",
"%s für VKZ %s übertragen?\n\n%s\n\n"
"%d Objekte; der bisherige Bestand dieser VKZ wird ersetzt."
% (
summary["dataset_label"],
summary["vkz"],
summary["listing"],
summary["total"],
),
)
yes = getattr(
getattr(QMessageBox, "StandardButton", QMessageBox),
"Yes",
QMessageBox.Yes if hasattr(QMessageBox, "Yes") else None,
)
if answer != yes:
return
except Exception:
return
try:
success, message = self.vln_logic.upload_active_layer(active_layer)
except Exception as exc:
try:
from sn_plan41.modules.vln_api_client import AuthError as _AuthError
if isinstance(exc, _AuthError):
self.vln_logic.handle_session_expired()
self._on_vln_logout()
QMessageBox.warning(
self,
"Sitzung abgelaufen",
"Der API-Key ist nicht mehr gültig — bitte neu anmelden.",
)
return
except ImportError:
pass
QMessageBox.warning(self, "Hochladen fehlgeschlagen", str(exc))
return
if success:
try:
from sn_basis.functions.message_wrapper import info
info("Upload erfolgreich", message)
except Exception:
pass
else:
QMessageBox.warning(self, "Hochladen fehlgeschlagen", message)
def _toggle_group(self, checked: bool) -> None:
"""Zeigt/verbirgt Verfahrens-DB-Gruppe."""
self.group_button.setArrowType(ArrowDown if checked else ArrowRight)