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>
182 lines
7.2 KiB
Python
182 lines
7.2 KiB
Python
"""
|
||
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)
|