Basemap-Bereich im Druck-Tab mit lokalen Stilvorlagen (#88)
Einklappbarer Bereich „Basemap“: Auswertebereich (Verfahrensgebiet/Druckbereich), Basemap-Legende beim Erstellen an/aus, „Basemap-Legende aktualisieren“ für bestehende Vorlagen und Stilvorlagen des Basemap-Layers. Stilvorlagen liegen lokal im QGIS-Profil (QML plus Begleitdatei mit Legendenauswahl), mehrere Varianten, eine als Standard markierbar; geladen wird nur auf Knopfdruck nach Rückfrage, fremde Varianten (andere Style-Namen) erst nach zweiter Bestätigung. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,6 @@
|
|||||||
|
- Basemap-Legende für Drucklayouts (#88): Beim Erstellen einer §38-/§41-Vorlage wird ausgewertet, welche Objekte des basemap.de-Vektorkachel-Layers im Verfahrensgebiet (wahlweise im Druckbereich) tatsächlich dargestellt werden; ein Auswahldialog lässt Kategorien abwählen, die gewählten erscheinen mit der Signatur des Projektlayers in einem eigenen Legendenelement „Kartengrundlage (basemap.de)“ über der Quellenangabe. Zuordnung von 434 Basemap-Styles zu 190 Legendeneinträgen (Beschriftungen, Nummernschilder und Hintergrund ausgenommen) in `data/basemap_legende.json` (erzeugt mit `modules/basemap_katalog.py`, fachlich nachbearbeitet)
|
||||||
|
- Neuer einklappbarer Bereich „Basemap“ im Druck-Tab: Auswertebereich, Basemap-Legende beim Erstellen an/aus, „Basemap-Legende aktualisieren“ für bestehende Vorlagen und lokale Stilvorlagen des Basemap-Layers (mehrere Varianten, laden, speichern, als Standard markieren, löschen) – z. B. einmal Hausnummern ausschalten und in jedem Projekt wieder laden (#88)
|
||||||
|
- Neuer allgemeiner Dialog `ask_mehrfachauswahl` (Liste mit Häkchen, Gruppen, Suche, Symbolvorschau) in `functions/dialog_wrapper.py` (#88)
|
||||||
- Bugfix: `Navigation.remove_all()` ruft jetzt `deleteLater()` auf Menü und Toolbar auf, bevor die Referenzen verworfen werden – vorher blieb die Toolbar beim Unload als Kind von main_window bestehen und der Plugin Reloader meldete beim Neuladen "removing duplicated widget(s) not cleaned up by the plugin during unload: LnoSachsenToolbar" (#72)
|
- Bugfix: `Navigation.remove_all()` ruft jetzt `deleteLater()` auf Menü und Toolbar auf, bevor die Referenzen verworfen werden – vorher blieb die Toolbar beim Unload als Kind von main_window bestehen und der Plugin Reloader meldete beim Neuladen "removing duplicated widget(s) not cleaned up by the plugin during unload: LnoSachsenToolbar" (#72)
|
||||||
- Restliche „ungeklärt"-Funktionen aus dem Prüfstand entschieden: Tests für Dateipruefer (`_ist_leer`, Priorität in `_handle_leere_eingabe`), Datenschreiber (`_store_style_in_gpkg` – Tabelle anlegen, Default-Stil zurücksetzen, Geometriespalte übernehmen) und Pruefmanager (`_handle_verfahrensgebiet_layer_existiert` „entfernen"/„ersetzen"); Linkpruefer (`_ist_url`, `_pruefe_url`, `_pruefe_dateipfad`), `Pruefmanager._handle_datei_existiert` und `Pruefmanager._ask_use_or_replace_pufferlayer` (toter Code, Bug #60) bekamen den Vermerk „von Test abgesehen" – sn_basis steht jetzt bei 0 ungeklärten Funktionen (#70)
|
- Restliche „ungeklärt"-Funktionen aus dem Prüfstand entschieden: Tests für Dateipruefer (`_ist_leer`, Priorität in `_handle_leere_eingabe`), Datenschreiber (`_store_style_in_gpkg` – Tabelle anlegen, Default-Stil zurücksetzen, Geometriespalte übernehmen) und Pruefmanager (`_handle_verfahrensgebiet_layer_existiert` „entfernen"/„ersetzen"); Linkpruefer (`_ist_url`, `_pruefe_url`, `_pruefe_dateipfad`), `Pruefmanager._handle_datei_existiert` und `Pruefmanager._ask_use_or_replace_pufferlayer` (toter Code, Bug #60) bekamen den Vermerk „von Test abgesehen" – sn_basis steht jetzt bei 0 ungeklärten Funktionen (#70)
|
||||||
- Kategorie „vielleicht testen" abgeschlossen: 30 weitere Funktionen bekamen Tests (os_wrapper, qgiscore_wrapper, qgisui_wrapper, DataGrabber, pruef_ergebnis, Navigation, SettingsTab, PrintTab – Zustandslogik, Signal-Handling, Atlas-/Theme-Verwaltung), `PrintTab._build_ui` bekam den Vermerk „Test ausgesetzt, zunächst Refactor" (#67)
|
- Kategorie „vielleicht testen" abgeschlossen: 30 weitere Funktionen bekamen Tests (os_wrapper, qgiscore_wrapper, qgisui_wrapper, DataGrabber, pruef_ergebnis, Navigation, SettingsTab, PrintTab – Zustandslogik, Signal-Handling, Atlas-/Theme-Verwaltung), `PrintTab._build_ui` bekam den Vermerk „Test ausgesetzt, zunächst Refactor" (#67)
|
||||||
|
|||||||
@@ -0,0 +1,181 @@
|
|||||||
|
"""
|
||||||
|
sn_basis/modules/basemap_stilvorlagen.py – lokale Stilvorlagen für den Basemap-Layer
|
||||||
|
|
||||||
|
Speichert die Darstellung des Basemap-Layers (Symbologie und Beschriftung, also auch
|
||||||
|
abgeschaltete Styles wie Hausnummern) als benannte Stilvorlage im QGIS-Profil und lädt sie
|
||||||
|
wieder. Mehrere Varianten sind möglich; eine davon kann als Standard markiert werden.
|
||||||
|
|
||||||
|
Ablage: ``<QGIS-Profil>/sn_basis/basemap_stile/<name>.qml`` plus ``<name>.json`` mit
|
||||||
|
Anzeigename, Erstellungszeit, Style-Namen der Vorlage und – optional – der Legendenauswahl.
|
||||||
|
Der Standard steht in ``QSettings`` unter ``sn_basis/basemap_standardstil``.
|
||||||
|
Die Vorlagen sind lokal (je Rechner und Profil) und werden nicht geteilt.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from sn_basis.functions.qgiscore_wrapper import QgsApplication, STYLE_CATEGORIES_SYMBOLOGIE_BESCHRIFTUNG
|
||||||
|
from sn_basis.functions.qt_wrapper import QSettings
|
||||||
|
from sn_basis.functions.text_wrapper import sanitize_bezeichner
|
||||||
|
|
||||||
|
STANDARD_SCHLUESSEL = "sn_basis/basemap_standardstil"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Stilvorlage:
|
||||||
|
name: str # Dateiname ohne Endung
|
||||||
|
anzeigename: str
|
||||||
|
pfad: str # QML-Datei
|
||||||
|
erstellt: str = ""
|
||||||
|
standard: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
def standard_ordner() -> str:
|
||||||
|
"""Ordner der Stilvorlagen im aktiven QGIS-Profil (außerhalb von QGIS im Benutzerordner)."""
|
||||||
|
try:
|
||||||
|
basis = QgsApplication.qgisSettingsDirPath() or ""
|
||||||
|
except Exception:
|
||||||
|
basis = ""
|
||||||
|
if not basis:
|
||||||
|
basis = os.path.join(os.path.expanduser("~"), ".sn_basis")
|
||||||
|
return os.path.join(basis, "sn_basis", "basemap_stile")
|
||||||
|
|
||||||
|
|
||||||
|
def _stilnamen(layer: Any) -> list[str]:
|
||||||
|
try:
|
||||||
|
return [s.styleName() for s in layer.renderer().styles()]
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
class BasemapStilvorlagen:
|
||||||
|
"""Verwaltet die lokalen Stilvorlagen des Basemap-Layers."""
|
||||||
|
|
||||||
|
def __init__(self, ordner: str | None = None, einstellungen: Any | None = None) -> None:
|
||||||
|
self.ordner = ordner or standard_ordner()
|
||||||
|
self.einstellungen = einstellungen if einstellungen is not None else QSettings()
|
||||||
|
|
||||||
|
# --- Abfragen -------------------------------------------------------
|
||||||
|
|
||||||
|
def _pfade(self, name: str) -> tuple[str, str]:
|
||||||
|
basis = os.path.join(self.ordner, name)
|
||||||
|
return basis + ".qml", basis + ".json"
|
||||||
|
|
||||||
|
def _metadaten(self, name: str) -> dict:
|
||||||
|
_, json_pfad = self._pfade(name)
|
||||||
|
try:
|
||||||
|
with open(json_pfad, encoding="utf-8") as datei:
|
||||||
|
daten = json.load(datei)
|
||||||
|
return daten if isinstance(daten, dict) else {}
|
||||||
|
except (OSError, ValueError):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def dateiname(anzeigename: str) -> str:
|
||||||
|
return sanitize_bezeichner(anzeigename.strip())
|
||||||
|
|
||||||
|
def standard(self) -> str:
|
||||||
|
return str(self.einstellungen.value(STANDARD_SCHLUESSEL, "") or "")
|
||||||
|
|
||||||
|
def liste(self) -> list[Stilvorlage]:
|
||||||
|
"""Alle Stilvorlagen, alphabetisch nach Anzeigename. Fehlende Begleitdatei wird toleriert."""
|
||||||
|
if not os.path.isdir(self.ordner):
|
||||||
|
return []
|
||||||
|
standard = self.standard()
|
||||||
|
vorlagen = []
|
||||||
|
for datei in os.listdir(self.ordner):
|
||||||
|
if not datei.lower().endswith(".qml"):
|
||||||
|
continue
|
||||||
|
name = datei[:-4]
|
||||||
|
meta = self._metadaten(name)
|
||||||
|
vorlagen.append(Stilvorlage(
|
||||||
|
name=name,
|
||||||
|
anzeigename=str(meta.get("anzeigename") or name),
|
||||||
|
pfad=os.path.join(self.ordner, datei),
|
||||||
|
erstellt=str(meta.get("erstellt", "")),
|
||||||
|
standard=(name == standard),
|
||||||
|
))
|
||||||
|
return sorted(vorlagen, key=lambda v: v.anzeigename.lower())
|
||||||
|
|
||||||
|
def vorhanden(self, anzeigename: str) -> bool:
|
||||||
|
return os.path.exists(self._pfade(self.dateiname(anzeigename))[0])
|
||||||
|
|
||||||
|
def legendenauswahl(self, name: str) -> dict | None:
|
||||||
|
"""In der Vorlage gespeicherte Legendenauswahl (``ausgeblendet``/``eingeblendet``) oder ``None``."""
|
||||||
|
auswahl = self._metadaten(name).get("legende_auswahl")
|
||||||
|
return auswahl if isinstance(auswahl, dict) else None
|
||||||
|
|
||||||
|
def pruefe_passung(self, layer: Any, name: str) -> list[str]:
|
||||||
|
"""Style-Namen der Vorlage, die der Layer nicht kennt (z. B. Vorlage aus „Grau“ für „Farbe“)."""
|
||||||
|
vorlage = self._metadaten(name).get("stile")
|
||||||
|
if not isinstance(vorlage, list):
|
||||||
|
return []
|
||||||
|
vorhanden = set(_stilnamen(layer))
|
||||||
|
return [s for s in vorlage if s not in vorhanden]
|
||||||
|
|
||||||
|
# --- Ändern ---------------------------------------------------------
|
||||||
|
|
||||||
|
def speichern(self, layer: Any, anzeigename: str, legende_auswahl: dict | None = None) -> Stilvorlage:
|
||||||
|
"""Speichert die aktuelle Darstellung des Layers; eine gleichnamige Vorlage wird ersetzt.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
ValueError
|
||||||
|
Bei leerem Namen oder wenn QGIS die Datei nicht schreiben konnte.
|
||||||
|
"""
|
||||||
|
name = self.dateiname(anzeigename)
|
||||||
|
if not name:
|
||||||
|
raise ValueError("Bitte einen Namen für die Stilvorlage angeben.")
|
||||||
|
os.makedirs(self.ordner, exist_ok=True)
|
||||||
|
qml_pfad, json_pfad = self._pfade(name)
|
||||||
|
ergebnis = layer.saveNamedStyle(qml_pfad, STYLE_CATEGORIES_SYMBOLOGIE_BESCHRIFTUNG)
|
||||||
|
meldung, ok = ergebnis if isinstance(ergebnis, tuple) else ("", bool(ergebnis))
|
||||||
|
if not ok:
|
||||||
|
raise ValueError(f"Die Stilvorlage konnte nicht gespeichert werden: {meldung}")
|
||||||
|
erstellt = datetime.now().isoformat(timespec="seconds")
|
||||||
|
meta = {
|
||||||
|
"anzeigename": anzeigename.strip(),
|
||||||
|
"erstellt": erstellt,
|
||||||
|
"layer": layer.name(),
|
||||||
|
"stile": _stilnamen(layer),
|
||||||
|
}
|
||||||
|
if legende_auswahl:
|
||||||
|
meta["legende_auswahl"] = legende_auswahl
|
||||||
|
with open(json_pfad, "w", encoding="utf-8") as datei:
|
||||||
|
json.dump(meta, datei, ensure_ascii=False, indent=1)
|
||||||
|
return Stilvorlage(name, anzeigename.strip(), qml_pfad, erstellt, name == self.standard())
|
||||||
|
|
||||||
|
def laden(self, layer: Any, name: str) -> None:
|
||||||
|
"""Ersetzt Symbologie und Beschriftung des Layers durch die Vorlage.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
ValueError
|
||||||
|
Wenn die Vorlage fehlt oder QGIS sie nicht laden konnte.
|
||||||
|
"""
|
||||||
|
qml_pfad, _ = self._pfade(name)
|
||||||
|
if not os.path.exists(qml_pfad):
|
||||||
|
raise ValueError(f"Die Stilvorlage „{name}“ gibt es nicht mehr.")
|
||||||
|
ergebnis = layer.loadNamedStyle(qml_pfad, STYLE_CATEGORIES_SYMBOLOGIE_BESCHRIFTUNG)
|
||||||
|
meldung, ok = ergebnis if isinstance(ergebnis, tuple) else ("", bool(ergebnis))
|
||||||
|
if not ok:
|
||||||
|
raise ValueError(f"Die Stilvorlage konnte nicht geladen werden: {meldung}")
|
||||||
|
nachzeichnen = getattr(layer, "triggerRepaint", None)
|
||||||
|
if callable(nachzeichnen):
|
||||||
|
nachzeichnen()
|
||||||
|
|
||||||
|
def loeschen(self, name: str) -> None:
|
||||||
|
for pfad in self._pfade(name):
|
||||||
|
if os.path.exists(pfad):
|
||||||
|
os.remove(pfad)
|
||||||
|
if self.standard() == name:
|
||||||
|
self.einstellungen.remove(STANDARD_SCHLUESSEL)
|
||||||
|
|
||||||
|
def setze_standard(self, name: str) -> None:
|
||||||
|
if not os.path.exists(self._pfade(name)[0]):
|
||||||
|
raise ValueError(f"Die Stilvorlage „{name}“ gibt es nicht.")
|
||||||
|
self.einstellungen.setValue(STANDARD_SCHLUESSEL, name)
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
"""
|
||||||
|
Tests für die Anbindung der Basemap-Legende an Druckablauf, Dialog und Druck-Tab (Basis #88):
|
||||||
|
Mehrfachauswahl-Dialog, PrintLogic/PrintLayout und Basemap-Bereich im Druck-Tab.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
from sn_basis.functions.dialog_wrapper import ask_mehrfachauswahl
|
||||||
|
from sn_basis.functions.variable_wrapper import get_variable, set_variable
|
||||||
|
from sn_basis.modules import basemap_legende as bl
|
||||||
|
from sn_basis.modules.print_layout import PrintLayout
|
||||||
|
from sn_basis.modules.print_logic import (
|
||||||
|
KARTENNAME_38,
|
||||||
|
KARTENNAME_41,
|
||||||
|
KARTENNAME_BAUFREIGABE,
|
||||||
|
LAYOUTS_MIT_BASEMAP_LEGENDE,
|
||||||
|
PrintLogic,
|
||||||
|
)
|
||||||
|
from sn_basis.ui.tabs.print_tab import PrintTab
|
||||||
|
from sn_basis.ui.widgets.basemap_bereich import BasemapBereich
|
||||||
|
|
||||||
|
|
||||||
|
class TestMehrfachauswahl(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_mock_modus_liefert_vorauswahl_in_listenreihenfolge(self):
|
||||||
|
"""Prüft, dass der Mehrfachauswahl-Dialog im Mock-Modus die vorausgewählten Schlüssel in Listenreihenfolge liefert."""
|
||||||
|
eintraege = [{"schluessel": s, "text": s.upper()} for s in ("a", "b", "c")]
|
||||||
|
self.assertEqual(ask_mehrfachauswahl("T", "H", eintraege, {"c", "a"}), ["a", "c"])
|
||||||
|
|
||||||
|
|
||||||
|
class TestDruckanbindung(unittest.TestCase):
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
set_variable(bl.LEGENDE_ERZEUGEN_VAR, "", scope="project")
|
||||||
|
|
||||||
|
def test_basemap_legende_nur_fuer_38_und_41_und_bei_eingeschaltetem_schalter(self):
|
||||||
|
"""Prüft, dass nur §-38- und §-41-Vorlagen eine Basemap-Legende bekommen und nur, wenn der Schalter im Druck-Tab an ist."""
|
||||||
|
logik = PrintLogic(pruefmanager=MagicMock())
|
||||||
|
self.assertEqual(LAYOUTS_MIT_BASEMAP_LEGENDE, {KARTENNAME_38, KARTENNAME_41})
|
||||||
|
self.assertTrue(logik._basemap_legende_erzeugen(KARTENNAME_41))
|
||||||
|
self.assertFalse(logik._basemap_legende_erzeugen(KARTENNAME_BAUFREIGABE))
|
||||||
|
bl.setze_legende_erzeugen(False)
|
||||||
|
self.assertFalse(logik._basemap_legende_erzeugen(KARTENNAME_38))
|
||||||
|
|
||||||
|
def test_layout_wird_markiert_und_legende_erzeugt(self):
|
||||||
|
"""Prüft, dass PrintLayout das Layout als Basemap-Nutzer kennzeichnet und die Basemap-Legende erzeugen lässt."""
|
||||||
|
layout = MagicMock()
|
||||||
|
with patch("sn_basis.modules.basemap_legende.BasemapLegende") as klasse:
|
||||||
|
klasse.return_value.aktualisiere.return_value = {"status": "ok"}
|
||||||
|
PrintLayout(project=MagicMock())._erzeuge_basemap_legende(layout)
|
||||||
|
layout.setCustomProperty.assert_any_call("sn_basemap_legende", 1)
|
||||||
|
klasse.return_value.aktualisiere.assert_called_once_with(layout)
|
||||||
|
|
||||||
|
def test_fehler_der_basemap_legende_verhindert_die_vorlage_nicht(self):
|
||||||
|
"""Prüft, dass ein Fehler beim Erzeugen der Basemap-Legende nur als Warnung gemeldet wird und keine Ausnahme durchreicht."""
|
||||||
|
with patch("sn_basis.modules.basemap_legende.BasemapLegende") as klasse, \
|
||||||
|
patch("sn_basis.functions.message_wrapper.warning") as warnung:
|
||||||
|
klasse.return_value.aktualisiere.side_effect = RuntimeError("Netz weg")
|
||||||
|
PrintLayout(project=MagicMock())._erzeuge_basemap_legende(MagicMock())
|
||||||
|
warnung.assert_called_once()
|
||||||
|
self.assertIn("Netz weg", warnung.call_args[0][1])
|
||||||
|
|
||||||
|
def test_druck_tab_aktualisiert_basemap_bereich_falls_vorhanden(self):
|
||||||
|
"""Prüft, dass der Druck-Tab den Basemap-Bereich neu einliest und ohne Bereich nichts tut."""
|
||||||
|
tab = PrintTab.__new__(PrintTab)
|
||||||
|
tab._basemap_bereich_aktualisieren() # ohne Bereich: keine Ausnahme
|
||||||
|
tab._basemap_bereich = MagicMock()
|
||||||
|
tab._basemap_bereich_aktualisieren()
|
||||||
|
tab._basemap_bereich.aktualisieren.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
def _bereich(stil_name="Ohne_Hausnummern", layer=None, layout_name="§41"):
|
||||||
|
bereich = BasemapBereich.__new__(BasemapBereich)
|
||||||
|
bereich._project = MagicMock()
|
||||||
|
bereich._stilvorlagen = MagicMock()
|
||||||
|
bereich._stil_combo = MagicMock()
|
||||||
|
bereich._stil_combo.currentData.return_value = stil_name
|
||||||
|
bereich._stil_combo.currentText.return_value = stil_name
|
||||||
|
bereich._layout_combo = MagicMock()
|
||||||
|
bereich._layout_combo.currentText.return_value = layout_name
|
||||||
|
bereich._bereich_combo = MagicMock()
|
||||||
|
bereich.basemap_layer = MagicMock(return_value=layer)
|
||||||
|
bereich.aktualisieren = MagicMock()
|
||||||
|
return bereich
|
||||||
|
|
||||||
|
|
||||||
|
class TestBasemapBereich(unittest.TestCase):
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
for schluessel in (bl.AUSWAHL_VAR, bl.AUSWERTEBEREICH_VAR):
|
||||||
|
set_variable(schluessel, "", scope="project")
|
||||||
|
|
||||||
|
def test_auswertebereich_wird_gespeichert(self):
|
||||||
|
"""Prüft, dass die Wahl des Auswertebereichs in der Projektvariable landet."""
|
||||||
|
bereich = _bereich()
|
||||||
|
bereich._bereich_combo.currentData.return_value = bl.AUSWERTEBEREICH_DRUCKBEREICH
|
||||||
|
bereich._on_bereich_geaendert(1)
|
||||||
|
self.assertEqual(bl.lies_auswertebereich(), bl.AUSWERTEBEREICH_DRUCKBEREICH)
|
||||||
|
|
||||||
|
@patch("sn_basis.ui.widgets.basemap_bereich.warning")
|
||||||
|
def test_aktualisieren_ohne_vorlage_warnt(self, warnung):
|
||||||
|
"""Prüft, dass „Basemap-Legende aktualisieren“ ohne vorhandene Vorlage warnt und nichts auswertet."""
|
||||||
|
bereich = _bereich()
|
||||||
|
bereich._project.layoutManager.return_value.layoutByName.return_value = None
|
||||||
|
with patch("sn_basis.modules.basemap_legende.BasemapLegende") as klasse:
|
||||||
|
bereich._on_legende_aktualisieren()
|
||||||
|
klasse.assert_not_called()
|
||||||
|
warnung.assert_called_once()
|
||||||
|
|
||||||
|
@patch("sn_basis.ui.widgets.basemap_bereich.success")
|
||||||
|
def test_aktualisieren_wertet_gewaehlte_vorlage_aus(self, erfolg):
|
||||||
|
"""Prüft, dass „Basemap-Legende aktualisieren“ die gewählte Vorlage auswertet und den Erfolg meldet."""
|
||||||
|
bereich = _bereich()
|
||||||
|
layout = bereich._project.layoutManager.return_value.layoutByName.return_value
|
||||||
|
with patch("sn_basis.modules.basemap_legende.BasemapLegende") as klasse:
|
||||||
|
klasse.return_value.aktualisiere.return_value = {"status": "ok", "meldung": "3 Einträge"}
|
||||||
|
bereich._on_legende_aktualisieren()
|
||||||
|
klasse.return_value.aktualisiere.assert_called_once_with(layout)
|
||||||
|
erfolg.assert_called_once()
|
||||||
|
|
||||||
|
@patch("sn_basis.ui.widgets.basemap_bereich.ask_yes_no", return_value=True)
|
||||||
|
@patch("sn_basis.ui.widgets.basemap_bereich.success")
|
||||||
|
def test_stil_laden_uebernimmt_legendenauswahl(self, _erfolg, _frage):
|
||||||
|
"""Prüft, dass das Laden einer Stilvorlage den Layerstil ersetzt und ihre Legendenauswahl in das Projekt übernimmt."""
|
||||||
|
layer = MagicMock()
|
||||||
|
bereich = _bereich(layer=layer)
|
||||||
|
bereich._stilvorlagen.pruefe_passung.return_value = []
|
||||||
|
bereich._stilvorlagen.legendenauswahl.return_value = {"ausgeblendet": ["wald"], "eingeblendet": []}
|
||||||
|
bereich._on_stil_laden()
|
||||||
|
bereich._stilvorlagen.laden.assert_called_once_with(layer, "Ohne_Hausnummern")
|
||||||
|
self.assertEqual(json.loads(get_variable(bl.AUSWAHL_VAR, scope="project"))["ausgeblendet"], ["wald"])
|
||||||
|
|
||||||
|
@patch("sn_basis.ui.widgets.basemap_bereich.ask_yes_no", side_effect=[True, False])
|
||||||
|
def test_stil_laden_bricht_bei_fremder_vorlage_ab(self, _frage):
|
||||||
|
"""Prüft, dass eine Vorlage mit unbekannten Styles nur nach Bestätigung geladen wird."""
|
||||||
|
bereich = _bereich(layer=MagicMock())
|
||||||
|
bereich._stilvorlagen.pruefe_passung.return_value = ["Fremder_Style"]
|
||||||
|
bereich._on_stil_laden()
|
||||||
|
bereich._stilvorlagen.laden.assert_not_called()
|
||||||
|
|
||||||
|
@patch("sn_basis.ui.widgets.basemap_bereich.ask_text", return_value=("Ohne Hausnummern", True))
|
||||||
|
@patch("sn_basis.ui.widgets.basemap_bereich.ask_yes_no", return_value=False)
|
||||||
|
def test_vorhandene_vorlage_wird_nur_nach_rueckfrage_ersetzt(self, _frage, _text):
|
||||||
|
"""Prüft, dass eine gleichnamige Stilvorlage ohne Bestätigung nicht überschrieben wird."""
|
||||||
|
bereich = _bereich(layer=MagicMock())
|
||||||
|
bereich._stilvorlagen.vorhanden.return_value = True
|
||||||
|
bereich._on_stil_speichern()
|
||||||
|
bereich._stilvorlagen.speichern.assert_not_called()
|
||||||
|
|
||||||
|
@patch("sn_basis.ui.widgets.basemap_bereich.ask_text", return_value=("Neu", True))
|
||||||
|
@patch("sn_basis.ui.widgets.basemap_bereich.success")
|
||||||
|
def test_speichern_mit_aktueller_legendenauswahl(self, _erfolg, _text):
|
||||||
|
"""Prüft, dass eine neue Stilvorlage zusammen mit der aktuellen Legendenauswahl gespeichert wird."""
|
||||||
|
layer = MagicMock()
|
||||||
|
bereich = _bereich(layer=layer)
|
||||||
|
bereich._stilvorlagen.vorhanden.return_value = False
|
||||||
|
bl.merke_auswahl([bl.LegendenEintrag("wald", "Wald", "Vegetation", [])], [])
|
||||||
|
bereich._on_stil_speichern()
|
||||||
|
bereich._stilvorlagen.speichern.assert_called_once_with(
|
||||||
|
layer, "Neu", {"ausgeblendet": ["wald"], "eingeblendet": []})
|
||||||
|
bereich.aktualisieren.assert_called_once()
|
||||||
|
|
||||||
|
@patch("sn_basis.ui.widgets.basemap_bereich.warning")
|
||||||
|
def test_stil_speichern_ohne_basemap_layer_warnt(self, warnung):
|
||||||
|
"""Prüft, dass ohne Basemap-Layer keine Stilvorlage gespeichert, sondern gewarnt wird."""
|
||||||
|
bereich = _bereich(layer=None)
|
||||||
|
bereich._on_stil_speichern()
|
||||||
|
bereich._stilvorlagen.speichern.assert_not_called()
|
||||||
|
warnung.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
"""
|
||||||
|
Tests für modules/basemap_stilvorlagen.py – lokale Stilvorlagen des Basemap-Layers (Basis #88).
|
||||||
|
|
||||||
|
Gespeichert wird in einem temporären Ordner; der Layer schreibt statt einer echten QML-Datei
|
||||||
|
eine kleine Textdatei. Laden und Speichern am echten Vektorkachel-Layer ist im QGIS-Test geprüft.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from sn_basis.modules.basemap_stilvorlagen import STANDARD_SCHLUESSEL, BasemapStilvorlagen
|
||||||
|
|
||||||
|
|
||||||
|
class _Einstellungen:
|
||||||
|
"""Ersatz für QSettings – die Tests dürfen keine echten Benutzereinstellungen verändern."""
|
||||||
|
|
||||||
|
def __init__(self): self.daten = {}
|
||||||
|
def value(self, k, standard=""): return self.daten.get(k, standard)
|
||||||
|
def setValue(self, k, v): self.daten[k] = v
|
||||||
|
def remove(self, k): self.daten.pop(k, None)
|
||||||
|
|
||||||
|
|
||||||
|
class _Stil:
|
||||||
|
def __init__(self, name): self._name = name
|
||||||
|
def styleName(self): return self._name
|
||||||
|
|
||||||
|
|
||||||
|
class _Layer:
|
||||||
|
def __init__(self, stile=("Kontur_Kreisstr", "Decker_Kreisstr"), speichern_ok=True):
|
||||||
|
self.stile = list(stile)
|
||||||
|
self.geladen = None
|
||||||
|
self.neu_gezeichnet = False
|
||||||
|
self._speichern_ok = speichern_ok
|
||||||
|
|
||||||
|
def name(self): return "Basemap.de - Farbe"
|
||||||
|
def renderer(self): return type("R", (), {"styles": lambda s: [_Stil(n) for n in self.stile]})()
|
||||||
|
|
||||||
|
def saveNamedStyle(self, pfad, kategorien):
|
||||||
|
if not self._speichern_ok:
|
||||||
|
return "kein Schreibrecht", False
|
||||||
|
with open(pfad, "w", encoding="utf-8") as datei:
|
||||||
|
datei.write("<qgis/>")
|
||||||
|
return "", True
|
||||||
|
|
||||||
|
def loadNamedStyle(self, pfad, kategorien):
|
||||||
|
self.geladen = pfad
|
||||||
|
return "", True
|
||||||
|
|
||||||
|
def triggerRepaint(self): self.neu_gezeichnet = True
|
||||||
|
|
||||||
|
|
||||||
|
class TestStilvorlagen(unittest.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.ordner = tempfile.mkdtemp()
|
||||||
|
self.einstellungen = _Einstellungen()
|
||||||
|
self.vorlagen = BasemapStilvorlagen(os.path.join(self.ordner, "stile"), self.einstellungen)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
shutil.rmtree(self.ordner, ignore_errors=True)
|
||||||
|
|
||||||
|
def test_speichern_und_auflisten(self):
|
||||||
|
"""Prüft, dass eine gespeicherte Vorlage mit Anzeigenamen, QML-Datei und Begleitdatei aufgelistet wird."""
|
||||||
|
self.vorlagen.speichern(_Layer(), "Ohne Hausnummern", {"ausgeblendet": ["wald"], "eingeblendet": []})
|
||||||
|
liste = self.vorlagen.liste()
|
||||||
|
self.assertEqual([v.anzeigename for v in liste], ["Ohne Hausnummern"])
|
||||||
|
self.assertEqual(liste[0].name, "Ohne_Hausnummern")
|
||||||
|
self.assertTrue(os.path.exists(liste[0].pfad))
|
||||||
|
self.assertEqual(self.vorlagen.legendenauswahl("Ohne_Hausnummern")["ausgeblendet"], ["wald"])
|
||||||
|
|
||||||
|
def test_mehrere_varianten_alphabetisch(self):
|
||||||
|
"""Prüft, dass mehrere Varianten nebeneinander bestehen und alphabetisch nach Anzeigename sortiert werden."""
|
||||||
|
for name in ("Zeichnung", "Abstimmung", "mit Gebäuden"):
|
||||||
|
self.vorlagen.speichern(_Layer(), name)
|
||||||
|
self.assertEqual([v.anzeigename for v in self.vorlagen.liste()], ["Abstimmung", "mit Gebäuden", "Zeichnung"])
|
||||||
|
|
||||||
|
def test_leerer_name_oder_fehler_beim_schreiben(self):
|
||||||
|
"""Prüft, dass ein leerer Name und ein Schreibfehler von QGIS als ValueError gemeldet werden."""
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
self.vorlagen.speichern(_Layer(), " ")
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
self.vorlagen.speichern(_Layer(speichern_ok=False), "Test")
|
||||||
|
|
||||||
|
def test_laden_setzt_stil_und_zeichnet_neu(self):
|
||||||
|
"""Prüft, dass das Laden die QML-Datei der Vorlage in den Layer lädt und neu zeichnen lässt."""
|
||||||
|
self.vorlagen.speichern(_Layer(), "Standard LNO")
|
||||||
|
layer = _Layer()
|
||||||
|
self.vorlagen.laden(layer, "Standard_LNO")
|
||||||
|
self.assertTrue(layer.geladen.endswith("Standard_LNO.qml"))
|
||||||
|
self.assertTrue(layer.neu_gezeichnet)
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
self.vorlagen.laden(layer, "gibt_es_nicht")
|
||||||
|
|
||||||
|
def test_passung_meldet_fremde_styles(self):
|
||||||
|
"""Prüft, dass Style-Namen der Vorlage gemeldet werden, die der Ziel-Layer nicht kennt."""
|
||||||
|
self.vorlagen.speichern(_Layer(stile=("A", "B", "C")), "Grau")
|
||||||
|
self.assertEqual(self.vorlagen.pruefe_passung(_Layer(stile=("A",)), "Grau"), ["B", "C"])
|
||||||
|
self.assertEqual(self.vorlagen.pruefe_passung(_Layer(stile=("A", "B", "C")), "Grau"), [])
|
||||||
|
|
||||||
|
def test_standard_setzen_und_beim_loeschen_entfernen(self):
|
||||||
|
"""Prüft, dass eine Vorlage als Standard markiert wird und die Markierung beim Löschen der Vorlage entfällt."""
|
||||||
|
self.vorlagen.speichern(_Layer(), "Standard LNO")
|
||||||
|
self.vorlagen.setze_standard("Standard_LNO")
|
||||||
|
self.assertTrue(self.vorlagen.liste()[0].standard)
|
||||||
|
self.vorlagen.loeschen("Standard_LNO")
|
||||||
|
self.assertEqual(self.vorlagen.liste(), [])
|
||||||
|
self.assertEqual(self.vorlagen.standard(), "")
|
||||||
|
self.assertNotIn(STANDARD_SCHLUESSEL, self.einstellungen.daten)
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
self.vorlagen.setze_standard("Standard_LNO")
|
||||||
|
|
||||||
|
def test_fehlende_begleitdatei_wird_toleriert(self):
|
||||||
|
"""Prüft, dass eine QML-Datei ohne Begleitdatei mit ihrem Dateinamen aufgelistet wird."""
|
||||||
|
os.makedirs(self.vorlagen.ordner, exist_ok=True)
|
||||||
|
with open(os.path.join(self.vorlagen.ordner, "Alt.qml"), "w", encoding="utf-8") as datei:
|
||||||
|
datei.write("<qgis/>")
|
||||||
|
self.assertEqual([v.anzeigename for v in self.vorlagen.liste()], ["Alt"])
|
||||||
|
self.assertIsNone(self.vorlagen.legendenauswahl("Alt"))
|
||||||
|
self.assertEqual(self.vorlagen.pruefe_passung(_Layer(), "Alt"), [])
|
||||||
|
|
||||||
|
def test_ohne_ordner_leere_liste(self):
|
||||||
|
"""Prüft, dass ohne angelegten Vorlagenordner eine leere Liste zurückkommt."""
|
||||||
|
self.assertEqual(self.vorlagen.liste(), [])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -22,6 +22,7 @@ from sn_basis.functions.qgiscore_wrapper import QgsProject, QgsMapLayerProxyMode
|
|||||||
from sn_basis.functions.qgisui_wrapper import iface, QgsMapLayerComboBox
|
from sn_basis.functions.qgisui_wrapper import iface, QgsMapLayerComboBox
|
||||||
from sn_basis.functions.variable_wrapper import get_variable, set_variable
|
from sn_basis.functions.variable_wrapper import get_variable, set_variable
|
||||||
from sn_basis.modules.Pruefmanager import Pruefmanager
|
from sn_basis.modules.Pruefmanager import Pruefmanager
|
||||||
|
from sn_basis.ui.widgets.basemap_bereich import BasemapBereich
|
||||||
from sn_basis.modules.DataGrabber import DataGrabber
|
from sn_basis.modules.DataGrabber import DataGrabber
|
||||||
from sn_basis.modules.print_logic import (
|
from sn_basis.modules.print_logic import (
|
||||||
PrintLogic,
|
PrintLogic,
|
||||||
@@ -257,6 +258,10 @@ class PrintTab(QWidget):
|
|||||||
self._atlas_content = atlas_content
|
self._atlas_content = atlas_content
|
||||||
main_layout.addWidget(atlas_content)
|
main_layout.addWidget(atlas_content)
|
||||||
|
|
||||||
|
# Basemap-Bereich (einklappbar): Basemap-Legende und Stilvorlagen (Basis #88)
|
||||||
|
self._basemap_bereich = BasemapBereich(self)
|
||||||
|
main_layout.addWidget(self._basemap_bereich)
|
||||||
|
|
||||||
btn_vorlage_erstellen = QPushButton("Vorlage erstellen", self)
|
btn_vorlage_erstellen = QPushButton("Vorlage erstellen", self)
|
||||||
btn_vorlage_erstellen.clicked.connect(self._on_vorlage_erstellen)
|
btn_vorlage_erstellen.clicked.connect(self._on_vorlage_erstellen)
|
||||||
self._btn_vorlage_erstellen = btn_vorlage_erstellen
|
self._btn_vorlage_erstellen = btn_vorlage_erstellen
|
||||||
@@ -445,6 +450,13 @@ class PrintTab(QWidget):
|
|||||||
formfaktor=self._endlosrolle_cb.isChecked() if self._endlosrolle_cb else False,
|
formfaktor=self._endlosrolle_cb.isChecked() if self._endlosrolle_cb else False,
|
||||||
druckbereich_auswahl=self._druckbereich_combo.currentText() if self._druckbereich_combo else DRUCKBEREICH_VERFAHRENSGEBIET,
|
druckbereich_auswahl=self._druckbereich_combo.currentText() if self._druckbereich_combo else DRUCKBEREICH_VERFAHRENSGEBIET,
|
||||||
)
|
)
|
||||||
|
self._basemap_bereich_aktualisieren()
|
||||||
|
|
||||||
|
def _basemap_bereich_aktualisieren(self) -> None:
|
||||||
|
"""Liest den Basemap-Bereich neu ein (neue Vorlage, Projektwechsel)."""
|
||||||
|
bereich = getattr(self, "_basemap_bereich", None)
|
||||||
|
if bereich is not None:
|
||||||
|
bereich.aktualisieren()
|
||||||
|
|
||||||
def _disconnect_project_signals(self) -> None:
|
def _disconnect_project_signals(self) -> None:
|
||||||
try:
|
try:
|
||||||
@@ -494,6 +506,7 @@ class PrintTab(QWidget):
|
|||||||
self._theme_signal_connected = False
|
self._theme_signal_connected = False
|
||||||
self._connect_theme_collection_signals()
|
self._connect_theme_collection_signals()
|
||||||
self._restore_state()
|
self._restore_state()
|
||||||
|
self._basemap_bereich_aktualisieren()
|
||||||
|
|
||||||
def _toggle_atlas(self, checked: bool) -> None:
|
def _toggle_atlas(self, checked: bool) -> None:
|
||||||
if self._atlas_button:
|
if self._atlas_button:
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
"""sn_basis/ui/widgets – wiederverwendbare Bereiche für Tabs."""
|
||||||
@@ -0,0 +1,271 @@
|
|||||||
|
"""
|
||||||
|
sn_basis/ui/widgets/basemap_bereich.py – einklappbarer Bereich „Basemap“ im Druck-Tab
|
||||||
|
|
||||||
|
Einstellungen der Basemap-Legende (Auswertebereich, Erzeugen beim Anlegen der Vorlage),
|
||||||
|
„Basemap-Legende aktualisieren“ für bestehende Vorlagen und lokale Stilvorlagen des
|
||||||
|
Basemap-Layers (laden, speichern, als Standard markieren, löschen). Basis #88.
|
||||||
|
|
||||||
|
Die Handler enthalten nur Ablauf und Rückfragen; die Logik liegt in
|
||||||
|
``modules/basemap_legende`` und ``modules/basemap_stilvorlagen``.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
from sn_basis.functions.dialog_wrapper import 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.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.basemap_legende_layout import layouts_mit_basemap_legende
|
||||||
|
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) -> None:
|
||||||
|
super().__init__(parent)
|
||||||
|
self._project = project
|
||||||
|
self._stilvorlagen = stilvorlagen
|
||||||
|
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("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)
|
||||||
|
|
||||||
|
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 _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.basemap_layer()
|
||||||
|
self._status.setText(f"Basemap-Layer: {layer.name()}" if layer is not None else KEIN_LAYER_TEXT)
|
||||||
|
|
||||||
|
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._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: 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: Stilvorlagen ------------------------------------------
|
||||||
|
|
||||||
|
def _gewaehlte_vorlage(self) -> str:
|
||||||
|
return str(self._stil_combo.currentData() or "")
|
||||||
|
|
||||||
|
def _on_stil_laden(self) -> None:
|
||||||
|
layer = self.basemap_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")
|
||||||
|
success(TITEL, "Stilvorlage geladen.")
|
||||||
|
|
||||||
|
def _on_stil_speichern(self) -> None:
|
||||||
|
layer = self.basemap_layer()
|
||||||
|
if layer is None:
|
||||||
|
warning(TITEL, KEIN_LAYER_TEXT)
|
||||||
|
return
|
||||||
|
anzeigename, ok = ask_text("Stilvorlage speichern", "Name der Stilvorlage:",
|
||||||
|
default_text=self._stil_combo.currentText().replace(" ★", ""))
|
||||||
|
if not ok or not anzeigename.strip():
|
||||||
|
return
|
||||||
|
if self._vorlagen().vorhanden(anzeigename) and not ask_yes_no(
|
||||||
|
"Stilvorlage speichern", f"Die Stilvorlage „{anzeigename}“ gibt es schon. Ersetzen?", default=False
|
||||||
|
):
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
self._vorlagen().speichern(layer, anzeigename, legende.lies_auswahl())
|
||||||
|
except ValueError as fehler:
|
||||||
|
warning(TITEL, str(fehler))
|
||||||
|
return
|
||||||
|
self.aktualisieren()
|
||||||
|
success(TITEL, f"Stilvorlage „{anzeigename.strip()}“ gespeichert.")
|
||||||
|
|
||||||
|
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()
|
||||||
Reference in New Issue
Block a user