""" 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()