Files
Plugin_SN_Basis/modules/basemap_stilvorlagen.py
T
DanielandClaude Opus 5.5 f9241a11f9 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>
2026-10-02 11:57:10 +02:00

182 lines
7.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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)