Files
Plugin_SN_Basis/ui/widgets/basemap_bereich.py
T
DanielandClaude Opus 5.5 24ad564a08 Basemap hinzufügen und Thema wechseln im Basemap-Bereich (#100)
Neuer Abschnitt „Basemap-Layer“ im Basemap-Bereich des Druck-Tabs: Knopf
„Basemap hinzufügen“ und Auswahl „Thema“ (Farbe, Grau). Vorhandene Layer
werden an der Kachel-URL erkannt; Rückfrage Zusätzlich laden / Ersetzen /
Abbrechen. Vor dem Themenwechsel wird über einen Prüfwert der Darstellung
erkannt, ob sie ungespeichert verändert wurde.

Neues Modul modules/basemap_themen.py, neue Wrapper-Funktionen
ask_auswahl und get_active_layer, Stilvorlagen vermerken ihr Thema.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 21:27:35 +02:00

434 lines
18 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
sn_basis/ui/widgets/basemap_bereich.py – einklappbarer Bereich „Basemap“ im Druck-Tab
Basemap-Layer hinzufügen und sein Thema wechseln (Basis #100), Einstellungen der
Basemap-Legende (Auswertebereich, Erzeugen beim Anlegen der Vorlage),
„Basemap-Legende aktualisieren“ für bestehende Vorlagen, lokale Stilvorlagen des
Basemap-Layers (laden, speichern, als Standard markieren, löschen) und „Punktlinien
korrigieren“ (Basis #90). Basis #88.
Die Handler enthalten nur Ablauf und Rückfragen; die Logik liegt in
``modules/basemap_themen``, ``modules/basemap_legende`` und ``modules/basemap_stilvorlagen``.
"""
from __future__ import annotations
from typing import Any, Optional
from sn_basis.functions.dialog_wrapper import ask_auswahl, ask_text, ask_yes_no
from sn_basis.functions.message_wrapper import info, success, warning
from sn_basis.functions.qgiscore_wrapper import QgsProject
from sn_basis.functions.qgisui_wrapper import get_active_layer
from sn_basis.functions.qt_wrapper import (
ArrowDown,
ArrowRight,
QCheckBox,
QComboBox,
QHBoxLayout,
QLabel,
QPushButton,
QToolButton,
QVBoxLayout,
QWidget,
ToolButtonTextBesideIcon,
)
from sn_basis.modules import basemap_legende as legende
from sn_basis.modules import basemap_themen as themen
from sn_basis.modules.basemap_legende_layout import layouts_mit_basemap_legende
from sn_basis.modules.basemap_stilkorrektur import korrigiere_punktlinien
from sn_basis.modules.basemap_stilvorlagen import BasemapStilvorlagen
TITEL = "Basemap"
KEIN_LAYER_TEXT = "Kein Basemap-Layer im Projekt"
class BasemapBereich(QWidget):
"""Einklappbarer Basemap-Bereich; standardmäßig eingeklappt."""
def __init__(self, parent: Optional[Any] = None, project: Any | None = None,
stilvorlagen: BasemapStilvorlagen | None = None,
layer_fabrik: Any | None = None) -> None:
super().__init__(parent)
self._project = project
self._stilvorlagen = stilvorlagen
self._layer_fabrik = layer_fabrik
self._build_ui()
self.aktualisieren()
# --- Aufbau ---------------------------------------------------------
def _build_ui(self) -> None:
"""von Test abgesehen, weil reiner Widget-Aufbau; im QGIS-Test geprüft"""
haupt = QVBoxLayout()
haupt.setSpacing(4)
haupt.setContentsMargins(0, 0, 0, 0)
knopf = QToolButton()
knopf.setText(TITEL)
knopf.setCheckable(True)
knopf.setChecked(False)
knopf.setToolButtonStyle(ToolButtonTextBesideIcon)
knopf.setArrowType(ArrowRight)
knopf.setStyleSheet("font-weight: bold; margin-top: 6px;")
knopf.toggled.connect(self._umschalten)
self._knopf = knopf
haupt.addWidget(knopf)
inhalt = QWidget()
spalte = QVBoxLayout()
spalte.setSpacing(4)
spalte.setContentsMargins(10, 4, 4, 4)
self._status = QLabel(KEIN_LAYER_TEXT)
self._status.setWordWrap(True)
spalte.addWidget(self._status)
spalte.addWidget(self._ueberschrift("Basemap-Layer"))
self._hinzufuegen_btn = QPushButton("Basemap hinzufügen", self)
self._hinzufuegen_btn.setToolTip(
"Lädt basemap.de (Vektorkacheln) im gewählten Thema ganz unten in den Layerbaum."
)
self._hinzufuegen_btn.clicked.connect(lambda *_: self._on_basemap_hinzufuegen())
spalte.addWidget(self._hinzufuegen_btn)
spalte.addWidget(QLabel("Thema"))
self._thema_combo = QComboBox(self)
self._thema_combo.setToolTip(
"Darstellung des Dienstes für den ausgewählten Basemap-Layer. "
"Ohne Basemap-Layer gilt die Wahl für „Basemap hinzufügen“."
)
for schluessel, eintrag in themen.THEMEN.items():
self._thema_combo.addItem(eintrag.bezeichnung, schluessel)
self._thema_combo.setCurrentIndex(max(0, self._thema_combo.findData(themen.STANDARD_THEMA)))
self._thema_combo.currentIndexChanged.connect(self._on_thema_gewaehlt)
spalte.addWidget(self._thema_combo)
spalte.addWidget(self._ueberschrift("Legende"))
spalte.addWidget(QLabel("Auswertebereich"))
self._bereich_combo = QComboBox(self)
for schluessel, text in legende.AUSWERTEBEREICHE.items():
self._bereich_combo.addItem(text, schluessel)
self._bereich_combo.currentIndexChanged.connect(self._on_bereich_geaendert)
spalte.addWidget(self._bereich_combo)
self._erzeugen_cb = QCheckBox("Basemap-Legende beim Erstellen der Vorlage erzeugen (§ 38, § 41)", self)
self._erzeugen_cb.toggled.connect(self._on_erzeugen_geaendert)
spalte.addWidget(self._erzeugen_cb)
spalte.addWidget(QLabel("Vorlage"))
self._layout_combo = QComboBox(self)
spalte.addWidget(self._layout_combo)
self._aktualisieren_btn = QPushButton("Basemap-Legende aktualisieren", self)
self._aktualisieren_btn.clicked.connect(self._on_legende_aktualisieren)
spalte.addWidget(self._aktualisieren_btn)
spalte.addWidget(self._ueberschrift("Stilvorlage"))
self._stil_combo = QComboBox(self)
spalte.addWidget(self._stil_combo)
zeile = QHBoxLayout()
for text, handler in (
("Laden", self._on_stil_laden),
("Speichern unter …", self._on_stil_speichern),
("Als Standard", self._on_stil_standard),
("Löschen", self._on_stil_loeschen),
):
knopf_stil = QPushButton(text, self)
knopf_stil.clicked.connect(lambda *_, h=handler: h())
zeile.addWidget(knopf_stil)
spalte.addLayout(zeile)
self._punktlinien_btn = QPushButton("Punktlinien korrigieren", self)
self._punktlinien_btn.setToolTip(
"Baumreihen, Hecken, Durchlässe u. a. zeichnet QGIS aus dem Basemap-Stil nicht (Strichlänge 0). "
"Korrigiert die Darstellung im Projekt; mit „Speichern unter …“ als Stilvorlage sichern."
)
self._punktlinien_btn.clicked.connect(lambda *_: self._on_punktlinien_korrigieren())
spalte.addWidget(self._punktlinien_btn)
inhalt.setLayout(spalte)
inhalt.setVisible(False)
self._inhalt = inhalt
haupt.addWidget(inhalt)
self.setLayout(haupt)
@staticmethod
def _ueberschrift(text: str) -> Any:
beschriftung = QLabel(text)
beschriftung.setStyleSheet("font-weight: bold; margin-top: 4px;")
return beschriftung
# --- Zustand --------------------------------------------------------
def _projekt(self) -> Any:
return getattr(self, "_project", None) or QgsProject.instance()
def _vorlagen(self) -> BasemapStilvorlagen:
if getattr(self, "_stilvorlagen", None) is None:
self._stilvorlagen = BasemapStilvorlagen()
return self._stilvorlagen
def basemap_layer(self) -> Any | None:
"""Erster Basemap-Layer des Projekts (sichtbar oder nicht)."""
try:
layer = list(self._projekt().mapLayers().values())
except Exception:
return None
gefunden = legende.finde_basemap_layer(layer)
return gefunden[0] if gefunden else None
def _ziel_layer(self) -> Any | None:
"""Der im Layerbaum ausgewählte Basemap-Layer, sonst der erste des Projekts."""
aktiv = get_active_layer()
if aktiv is not None and legende.ist_basemap_layer(aktiv):
return aktiv
return self.basemap_layer()
def _gewaehltes_thema(self) -> str:
return str(self._thema_combo.currentData() or themen.STANDARD_THEMA)
def _zeige_thema(self, layer: Any | None) -> None:
"""Stellt das Thema-Dropdown ohne Themenwechsel auf das Thema des Layers."""
schluessel = themen.thema_des_layers(layer) if layer is not None else None
if schluessel is None:
return
self._thema_combo.blockSignals(True)
self._thema_combo.setCurrentIndex(max(0, self._thema_combo.findData(schluessel)))
self._thema_combo.blockSignals(False)
def _umschalten(self, offen: bool) -> None:
self._knopf.setArrowType(ArrowDown if offen else ArrowRight)
self._inhalt.setVisible(offen)
if offen:
self.aktualisieren()
def aktualisieren(self) -> None:
"""Liest Projektvariablen, Layouts und Stilvorlagen neu ein (z. B. nach Projektwechsel)."""
layer = self._ziel_layer()
self._status.setText(f"Basemap-Layer: {layer.name()}" if layer is not None else KEIN_LAYER_TEXT)
self._zeige_thema(layer)
bereich = legende.lies_auswertebereich()
self._bereich_combo.blockSignals(True)
self._bereich_combo.setCurrentIndex(max(0, self._bereich_combo.findData(bereich)))
self._bereich_combo.blockSignals(False)
self._erzeugen_cb.blockSignals(True)
self._erzeugen_cb.setChecked(legende.legende_erzeugen_aktiv())
self._erzeugen_cb.blockSignals(False)
self._layout_combo.clear()
self._layout_combo.addItems(layouts_mit_basemap_legende(self._projekt()))
self._aktualisieren_btn.setEnabled(layer is not None and self._layout_combo.count() > 0)
self._punktlinien_btn.setEnabled(layer is not None)
self._stil_combo.clear()
for vorlage in self._vorlagen().liste():
self._stil_combo.addItem(f"{vorlage.anzeigename}{' ★' if vorlage.standard else ''}", vorlage.name)
standard = self._vorlagen().standard()
if standard:
self._stil_combo.setCurrentIndex(max(0, self._stil_combo.findData(standard)))
# --- Handler: Basemap-Layer und Thema ------------------------------
def _aenderungen_klaeren(self, layer: Any) -> bool:
"""Fragt vor dem Überschreiben der Darstellung nach, ob Änderungen als Stil gesichert werden sollen.
``True``: weitermachen (gespeichert, verworfen oder nichts geändert); ``False``: abbrechen.
"""
stand = themen.aenderungsstand(layer)
if stand == themen.UNVERAENDERT:
return True
if stand == themen.GEAENDERT:
grund = f"Die Darstellung von „{layer.name()}“ wurde seit dem Laden des Themas oder Stils verändert."
else:
grund = (f"Für „{layer.name()}“ ist nicht bekannt, ob die Darstellung verändert wurde "
"(Layer wurde ohne diese Funktion geladen).")
wahl = ask_auswahl(
TITEL,
f"{grund} Beim Themenwechsel gehen Änderungen verloren.\n\nVorher als Stil speichern?",
[("speichern", "Als Stil speichern …"), ("verwerfen", "Verwerfen"), ("abbrechen", "Abbrechen")],
default="speichern",
abbrechen="abbrechen",
)
if wahl == "abbrechen":
return False
if wahl == "speichern":
return self._on_stil_speichern(layer)
return True
def _on_basemap_hinzufuegen(self) -> None:
schluessel = self._gewaehltes_thema()
try:
vorhanden = themen.layer_mit_quelle(list(self._projekt().mapLayers().values()))
except Exception:
vorhanden = []
if vorhanden:
namen = ", ".join(f"„{l.name()}“" for l in vorhanden)
wahl = ask_auswahl(
TITEL,
f"Im Projekt gibt es schon einen Basemap-Layer mit derselben Quelle: {namen}.\n\n"
"Einen weiteren Basemap-Layer zusätzlich laden, den vorhandenen ersetzen oder abbrechen?",
[("zusaetzlich", "Zusätzlich laden"), ("ersetzen", "Ersetzen"), ("abbrechen", "Abbrechen")],
default="abbrechen",
abbrechen="abbrechen",
)
if wahl == "abbrechen":
return
if wahl == "ersetzen":
ziel = self._ziel_layer()
self._ersetzen(ziel if ziel in vorhanden else vorhanden[0], schluessel)
return
try:
layer = themen.erzeuge_layer(schluessel, getattr(self, "_layer_fabrik", None))
except ValueError as fehler:
warning(TITEL, str(fehler))
return
themen.fuege_hinzu(self._projekt(), layer)
self.aktualisieren()
success(TITEL, f"„{layer.name()}“ hinzugefügt.")
def _ersetzen(self, layer: Any, schluessel: str) -> None:
"""Lädt den vorhandenen Layer im gewählten Thema neu; Position und Bezüge in Layouts bleiben."""
if not self._aenderungen_klaeren(layer):
return
try:
themen.wechsle_thema(layer, schluessel)
except ValueError as fehler:
warning(TITEL, str(fehler))
return
self.aktualisieren()
success(TITEL, f"„{layer.name()}“ im Thema {themen.thema(schluessel).bezeichnung} neu geladen.")
def _on_thema_gewaehlt(self, *_: Any) -> None:
layer = self._ziel_layer()
if layer is None:
return # Wahl gilt für „Basemap hinzufügen“
schluessel = self._gewaehltes_thema()
if themen.thema_des_layers(layer) == schluessel:
return
if not self._aenderungen_klaeren(layer):
self._zeige_thema(layer)
return
try:
themen.wechsle_thema(layer, schluessel)
except ValueError as fehler:
warning(TITEL, str(fehler))
self._zeige_thema(layer)
return
self.aktualisieren()
success(TITEL, f"Thema von „{layer.name()}“: {themen.thema(schluessel).bezeichnung}.")
# --- Handler: Legende -----------------------------------------------
def _on_bereich_geaendert(self, *_: Any) -> None:
bereich = self._bereich_combo.currentData()
if bereich in legende.AUSWERTEBEREICHE:
legende.setze_auswertebereich(bereich)
def _on_erzeugen_geaendert(self, aktiv: bool) -> None:
legende.setze_legende_erzeugen(bool(aktiv))
def _on_legende_aktualisieren(self) -> None:
name = self._layout_combo.currentText()
layout = self._projekt().layoutManager().layoutByName(name) if name else None
if layout is None:
warning(TITEL, "Bitte eine Vorlage mit Basemap-Legende wählen.")
return
try:
ergebnis = legende.BasemapLegende(self._projekt()).aktualisiere(layout)
except Exception as fehler:
warning(TITEL, f"Die Basemap-Legende konnte nicht aktualisiert werden: {fehler}")
return
if ergebnis["status"] == "ok":
success(TITEL, ergebnis["meldung"])
elif ergebnis["status"] != "abgebrochen":
info(TITEL, ergebnis["meldung"])
# --- Handler: Darstellung korrigieren ------------------------------
def _on_punktlinien_korrigieren(self) -> None:
layer = self._ziel_layer()
if layer is None:
warning(TITEL, KEIN_LAYER_TEXT)
return
anzahl = korrigiere_punktlinien(layer)
if anzahl:
success(TITEL, f"{anzahl} Punktlinien-Styles korrigiert. Basemap-Legende ggf. aktualisieren.")
else:
info(TITEL, "Keine Punktlinien zu korrigieren.")
# --- Handler: Stilvorlagen ------------------------------------------
def _gewaehlte_vorlage(self) -> str:
return str(self._stil_combo.currentData() or "")
def _on_stil_laden(self) -> None:
layer = self._ziel_layer()
name = self._gewaehlte_vorlage()
if layer is None or not name:
warning(TITEL, "Kein Basemap-Layer oder keine Stilvorlage gewählt.")
return
if not ask_yes_no("Stilvorlage laden",
f"Darstellung von „{layer.name()}“ durch die Stilvorlage ersetzen?", default=False):
return
unbekannt = self._vorlagen().pruefe_passung(layer, name)
if unbekannt and not ask_yes_no(
"Stilvorlage laden",
f"{len(unbekannt)} Styles der Vorlage gibt es in diesem Layer nicht – die Vorlage stammt "
"vermutlich von einer anderen Basemap-Variante. Trotzdem laden?",
default=False,
):
return
try:
self._vorlagen().laden(layer, name)
except ValueError as fehler:
warning(TITEL, str(fehler))
return
auswahl = self._vorlagen().legendenauswahl(name)
if auswahl is not None:
import json
from sn_basis.functions.variable_wrapper import set_variable
set_variable(legende.AUSWAHL_VAR, json.dumps(auswahl), scope="project")
themen.merke_stand(layer)
success(TITEL, "Stilvorlage geladen.")
def _on_stil_speichern(self, layer: Any | None = None) -> bool:
"""Speichert die Darstellung als Stilvorlage; ``True``, wenn gespeichert wurde."""
layer = layer if layer is not None else self._ziel_layer()
if layer is None:
warning(TITEL, KEIN_LAYER_TEXT)
return False
anzeigename, ok = ask_text("Stilvorlage speichern", "Name der Stilvorlage:",
default_text=self._stil_combo.currentText().replace(" ★", ""))
if not ok or not anzeigename.strip():
return False
if self._vorlagen().vorhanden(anzeigename) and not ask_yes_no(
"Stilvorlage speichern", f"Die Stilvorlage „{anzeigename}“ gibt es schon. Ersetzen?", default=False
):
return False
try:
self._vorlagen().speichern(layer, anzeigename, legende.lies_auswahl())
except ValueError as fehler:
warning(TITEL, str(fehler))
return False
themen.merke_stand(layer)
self.aktualisieren()
success(TITEL, f"Stilvorlage „{anzeigename.strip()}“ gespeichert.")
return True
def _on_stil_standard(self) -> None:
name = self._gewaehlte_vorlage()
if not name:
return
try:
self._vorlagen().setze_standard(name)
except ValueError as fehler:
warning(TITEL, str(fehler))
return
self.aktualisieren()
def _on_stil_loeschen(self) -> None:
name = self._gewaehlte_vorlage()
if not name or not ask_yes_no("Stilvorlage löschen",
f"Stilvorlage „{self._stil_combo.currentText()}“ löschen?", default=False):
return
self._vorlagen().loeschen(name)
self.aktualisieren()