Files
Plugin_SN_Plan41/ui/tab_a_ui.py
T

633 lines
24 KiB
Python
Raw Normal View History

"""
sn_plan41/ui/tab_a_ui.py UI für Tab A (Daten)
"""
from __future__ import annotations
2026-01-08 17:13:43 +01:00
from typing import Optional
from sn_basis.functions.qt_wrapper import (
2026-01-08 17:13:43 +01:00
QWidget,
QVBoxLayout,
2026-06-19 11:47:15 +02:00
QHBoxLayout,
2026-01-08 17:13:43 +01:00
QLabel,
2026-06-19 11:47:15 +02:00
QLineEdit,
2026-01-08 17:13:43 +01:00
QPushButton,
QToolButton,
QFileDialog,
QMessageBox,
ToolButtonTextBesideIcon,
ArrowDown,
2026-01-08 17:13:43 +01:00
ArrowRight,
SizePolicyPreferred,
SizePolicyMaximum,
QComboBox,
)
from sn_basis.functions.qgisui_wrapper import QgsFileWidget, QgsMapLayerComboBox
from sn_basis.functions.qgiscore_wrapper import QgsProject, QgsMapLayerProxyModel
from sn_basis.functions.variable_wrapper import get_variable, set_variable
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
2026-01-08 17:13:43 +01:00
# Services (werden von DockWidget injiziert)
from sn_basis.modules.Pruefmanager import Pruefmanager
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
2026-06-19 11:47:15 +02:00
from sn_basis.modules.stilpruefer import Stilpruefer
from sn_plan41.modules.vln_api_logic import VlnApiLogic
# Konstanten
RAUMFILTER_VAR = "Raumfilter"
RAUMFILTER_OPTIONS = ("Verfahrensgebiet", "Pufferlayer", "ohne")
RAUMFILTER_DEFAULT = "Pufferlayer"
2026-01-08 17:13:43 +01:00
class TabA(QWidget):
"""
UI-Klasse für Tab A (Daten) des Plan41-Plugins.
Zuständig für:
- Anzeige und Auswahl von Verfahrens-DB, Linklisten und Layern
- Steuerung der Pipeline über "Fachdaten laden"
- Persistierung von UI-States via Projektvariablen
Services (Pruefmanager, DataGrabber) werden zur Laufzeit vom DockWidget injiziert.
Alle fachlichen Prüfungen laufen über den zentralen Pruefmanager.
2026-01-08 17:13:43 +01:00
"""
tab_title = "Daten" #: Tab-Titel für BaseDockWidget
def __init__(self, parent: Optional[QWidget] = None):
"""
Initialisiert die UI-Struktur.
Services werden später über :meth:`set_services` injiziert.
:param parent: Parent-Widget (typischerweise DockWidget)
"""
2026-01-08 17:13:43 +01:00
super().__init__(parent)
# Services (werden von DockWidget gesetzt)
self.pruefmanager: Optional[Pruefmanager] = None
self.logic: Optional[TabALogic] = None
# UI-State
2026-01-08 17:13:43 +01:00
self.verfahrens_db: Optional[str] = None
self.lokale_linkliste: Optional[str] = None
self._pufferlayer = None
self._attributes_list = []
# UI-Referenzen
self._raumfilter_combo: Optional[QComboBox] = None
2026-06-19 11:47:15 +02:00
# VLN-API
self.vln_logic: Optional[VlnApiLogic] = None
self._vln_restoring: bool = False
self._build_ui()
self._restore_state()
def set_services(self, pruefmanager: Pruefmanager, data_grabber: DataGrabber) -> None:
"""
Injiziert Services vom übergeordneten DockWidget.
:param pruefmanager: Zentrale Prüfmanager-Instanz
:param data_grabber: DataGrabber für Quellenprüfung/Abruf
"""
self.pruefmanager = pruefmanager
self.data_grabber = data_grabber
self.logic = TabALogic(
2026-06-19 11:47:15 +02:00
pruefmanager=self.pruefmanager,
link_pruefer=Linkpruefer(),
stil_pruefer=Stilpruefer(),
)
self.logic.data_grabber = self.data_grabber
2026-01-08 17:13:43 +01:00
2026-06-19 11:47:15 +02:00
# VLN-API-Logik initialisieren
self.vln_logic = VlnApiLogic(self.pruefmanager)
self._update_vln_ui_state()
self._restore_vln_state()
2026-01-08 17:13:43 +01:00
def _build_ui(self) -> None:
"""Erstellt die komplette UI-Hierarchie mit allen Gruppen."""
2026-01-08 17:13:43 +01:00
main_layout = QVBoxLayout()
main_layout.setSpacing(4)
main_layout.setContentsMargins(4, 4, 4, 4)
# === VERFAHRENS-DATENBANK ===
2026-01-08 17:13:43 +01:00
self.group_button = QToolButton()
self.group_button.setText("Verfahrens-Datenbank")
self.group_button.setCheckable(True)
self.group_button.setChecked(True)
self.group_button.setToolButtonStyle(ToolButtonTextBesideIcon)
self.group_button.setArrowType(ArrowDown)
self.group_button.setStyleSheet("font-weight: bold;")
self.group_button.toggled.connect(self._toggle_group)
main_layout.addWidget(self.group_button)
self.group_content = QWidget()
self.group_content.setSizePolicy(SizePolicyPreferred, SizePolicyMaximum)
group_layout = QVBoxLayout()
group_layout.setSpacing(2)
group_layout.setContentsMargins(10, 4, 4, 4)
2026-01-08 17:13:43 +01:00
group_layout.addWidget(QLabel("bestehende Datei auswählen"))
self.file_widget = QgsFileWidget()
self.file_widget.setStorageMode(QgsFileWidget.GetFile)
self.file_widget.setFilter("Geopackage (*.gpkg)")
self.file_widget.fileChanged.connect(self._on_verfahrens_db_changed)
group_layout.addWidget(self.file_widget)
2026-01-08 17:13:43 +01:00
group_layout.addWidget(QLabel("-oder-"))
self.btn_new = QPushButton("Neue Verfahrens-DB anlegen")
self.btn_new.clicked.connect(self._create_new_gpkg)
group_layout.addWidget(self.btn_new)
2026-01-08 17:13:43 +01:00
self.group_content.setLayout(group_layout)
main_layout.addWidget(self.group_content)
# === OPTIONALE LINKLISTE ===
2026-01-08 17:13:43 +01:00
self.optional_button = QToolButton()
self.optional_button.setText("Optional: Lokale Linkliste")
self.optional_button.setCheckable(True)
self.optional_button.setChecked(False)
self.optional_button.setToolButtonStyle(ToolButtonTextBesideIcon)
self.optional_button.setArrowType(ArrowRight)
self.optional_button.setStyleSheet("font-weight: bold; margin-top: 6px;")
self.optional_button.toggled.connect(self._toggle_optional)
main_layout.addWidget(self.optional_button)
self.optional_content = QWidget()
self.optional_content.setSizePolicy(SizePolicyPreferred, SizePolicyMaximum)
optional_layout = QVBoxLayout()
optional_layout.setSpacing(2)
optional_layout.setContentsMargins(10, 4, 4, 20)
2026-01-08 17:13:43 +01:00
optional_layout.addWidget(QLabel("(frei lassen für globale Linkliste)"))
self.linkliste_widget = QgsFileWidget()
self.linkliste_widget.setStorageMode(QgsFileWidget.GetFile)
self.linkliste_widget.setFilter("Excelliste (*.xlsx)")
self.linkliste_widget.fileChanged.connect(self._on_linkliste_changed)
optional_layout.addWidget(self.linkliste_widget)
2026-01-08 17:13:43 +01:00
self.optional_content.setLayout(optional_layout)
self.optional_content.setVisible(False)
main_layout.addWidget(self.optional_content)
# === RAUMFILTER ===
main_layout.addWidget(QLabel("Raumfilter"))
self._raumfilter_combo = QComboBox(self)
self._raumfilter_combo.setToolTip("Wählt die räumliche Bezugsfläche für die Datenextraktion.")
self._raumfilter_combo.addItems(RAUMFILTER_OPTIONS)
self._raumfilter_combo.currentTextChanged.connect(self._on_raumfilter_changed)
main_layout.addWidget(self._raumfilter_combo)
# === PIPELINE-STEUERUNG ===
self.btn_pipeline = QPushButton("Fachdaten laden")
self.btn_pipeline.setToolTip("Starte Pipeline: Linkliste → DataGrabber → Datenschreiber → Log")
self.btn_pipeline.clicked.connect(self._on_load_fachdaten)
main_layout.addWidget(self.btn_pipeline)
2026-06-19 11:47:15 +02:00
# === 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)
2026-01-08 17:13:43 +01:00
main_layout.addStretch(1)
self.setLayout(main_layout)
def _restore_state(self) -> None:
"""Stellt UI-State aus Projektvariablen/Persistenz wieder her."""
# Verfahrens-DB
try:
db = get_variable("tab_a_verfahrens_db", scope="project")
if db and self.file_widget:
self.file_widget.setFilePath(db)
self.verfahrens_db = db
self._update_group_color()
except Exception:
pass
# Linkliste
try:
link = get_variable("tab_a_linkliste", scope="project")
if link and self.linkliste_widget:
self.linkliste_widget.setFilePath(link)
self.lokale_linkliste = link
except Exception:
pass
# Raumfilter (schon im _build_ui behandelt)
2026-06-19 11:47:15 +02:00
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 ===
2026-06-19 11:47:15 +02:00
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 Bestätigungs-Dialog ermitteln
2026-06-19 11:47:15 +02:00
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)
2026-01-08 17:13:43 +01:00
self.group_content.setVisible(checked)
def _toggle_optional(self, checked: bool) -> None:
"""Zeigt/verbirgt optionale Linkliste."""
self.optional_button.setArrowType(ArrowDown if checked else ArrowRight)
2026-01-08 17:13:43 +01:00
self.optional_content.setVisible(checked)
def _on_verfahrens_db_changed(self, path: str) -> None:
"""Persistieret Verfahrens-DB-Pfad."""
2026-01-08 17:13:43 +01:00
self.verfahrens_db = path
set_variable("tab_a_verfahrens_db", path, scope="project")
2026-01-08 17:13:43 +01:00
self._update_group_color()
def _on_linkliste_changed(self, path: str) -> None:
"""Persistieret lokale Linkliste."""
2026-01-08 17:13:43 +01:00
self.lokale_linkliste = path
set_variable("tab_a_linkliste", path, scope="project")
2026-01-08 17:13:43 +01:00
def _on_raumfilter_changed(self, value: str) -> None:
"""Persistieret Raumfilter-Auswahl."""
set_variable(RAUMFILTER_VAR, value, scope="project")
2026-01-08 17:13:43 +01:00
def _create_new_gpkg(self) -> None:
"""Delegiert GPKG-Erstellung (Prüfungen über Services)."""
file_path, _ = QFileDialog.getSaveFileName(
self, "Neue Verfahrens-Datenbank", "", "Geopackage (*.gpkg)"
)
if file_path:
if not file_path.lower().endswith(".gpkg"):
file_path += ".gpkg"
self.verfahrens_db = file_path
self.file_widget.setFilePath(file_path)
set_variable("tab_a_verfahrens_db", file_path, scope="project")
self._update_group_color()
2026-01-08 17:13:43 +01:00
def _update_group_color(self) -> None:
"""Visuelles Feedback für Verfahrens-DB-Status."""
2026-01-08 17:13:43 +01:00
if self.verfahrens_db:
self.group_button.setStyleSheet("font-weight: bold; background-color: ##d7a8ff;")
2026-01-08 17:13:43 +01:00
else:
self.group_button.setStyleSheet("font-weight: bold;")
def _on_load_fachdaten(self) -> None:
"""Kompatibilitäts-Handler → neue Pipeline."""
2026-03-12 16:14:24 +01:00
source = self.file_widget.filePath()
raumfilter = self._raumfilter_combo.currentText()
linkliste = self.linkliste_widget.filePath()
if self.logic:
try:
self.logic._on_run_pipeline(source, linkliste, raumfilter)
except Exception as exc:
QMessageBox.warning(
self,
"Fehler beim Laden",
f"Fehler beim Ausführen der Pipeline: {exc}",
QMessageBox.StandardButton.Ok,
)