""" 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: ``/sn_basis/basemap_stile/.qml`` plus ``.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)