Files
Plugin_SN_Basis/modules/basemap_stilvorlagen.py
T

182 lines
7.2 KiB
Python
Raw Normal View History

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