Files
Plugin_SN_Basis/ui/widgets/basemap_bereich.py
T
DanielandClaude Opus 5.5 d6d4403e46 Basemap: Punktlinien in Karte und Legende sichtbar machen (#90)
basemap.de zeichnet Punktlinien mit Strichlänge 0 und runder Linienkappe (MapLibre-Muster);
QGIS übernimmt das beim Import 1:1, Qt zeichnet Striche der Länge 0 aber nicht. Baumreihen,
Hecken, Durchlässe, Seilbahnen u. a. (12 Styles) fehlten dadurch in Karte und Legende.

Neues Modul basemap_stilkorrektur: korrigiere_punktlinien(layer) ersetzt Strichlänge 0 durch
0,01 im festen Strichmuster und in den array(...)-Mustern des zoomabhängigen Ausdrucks;
idempotent, andere Styles unverändert. Ausgelöst per Knopf „Punktlinien korrigieren“ im
Basemap-Bereich, sicherbar als Stilvorlage. Wrapper: PROPERTY_CUSTOM_DASH,
QgsProperty.fromExpression im Mock.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 12:33:00 +02:00

296 lines
12 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/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, lokale Stilvorlagen des
Basemap-Layers (laden, speichern, als Standard markieren, löschen) und „Punktlinien
korrigieren“ (Basis #90). 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_stilkorrektur import korrigiere_punktlinien
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)
self._punktlinien_btn = QPushButton("Punktlinien korrigieren", self)
self._punktlinien_btn.setToolTip(
"Baumreihen, Hecken, Durchlässe u. a. zeichnet QGIS aus dem Basemap-Stil nicht (Strichlänge 0). "
"Korrigiert die Darstellung im Projekt; mit „Speichern unter …“ als Stilvorlage sichern."
)
self._punktlinien_btn.clicked.connect(lambda *_: self._on_punktlinien_korrigieren())
spalte.addWidget(self._punktlinien_btn)
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._punktlinien_btn.setEnabled(layer is not None)
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: Darstellung korrigieren ------------------------------
def _on_punktlinien_korrigieren(self) -> None:
layer = self.basemap_layer()
if layer is None:
warning(TITEL, KEIN_LAYER_TEXT)
return
anzahl = korrigiere_punktlinien(layer)
if anzahl:
success(TITEL, f"{anzahl} Punktlinien-Styles korrigiert. Basemap-Legende ggf. aktualisieren.")
else:
info(TITEL, "Keine Punktlinien zu korrigieren.")
# --- 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()