Basemap-Legende: Rand für weiße Flächen ohne Kontur (#92)
Die Basemap-Styles „Platz“ (Verkehrsfläche, weitere Nutzung) haben eine rein weiße Füllung ohne Umrandung und waren auf dem weißen Legendenhintergrund unsichtbar. baue_symbole gibt solchen Flächensymbolen jetzt in der Legende eine dünne graue Randlinie (0,15 mm). Erkannt wird das an Füllfarbe und fehlender Kontur (braucht_legendenrand), nicht an Style-Namen; der Basemap-Layer in der Karte bleibt unverändert. Wrapper: QgsSimpleLineSymbolLayer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
- Bugfix: Weiße Basemap-Flächen ohne Kontur (z. B. „Platz“) waren in der Basemap-Legende auf weißem Grund unsichtbar; ihr Legendensymbol bekommt jetzt einen dünnen grauen Rand (0,15 mm), die Karte bleibt unverändert (#92)
|
||||||
- Bugfix: Punktlinien des Basemap-Layers (Baumreihe, Hecke, Durchlass, Seilbahnen u. a., 12 Styles) wurden in Karte und Legende nicht gezeichnet – basemap.de nutzt Strichlänge 0 mit runder Kappe, Qt zeichnet Striche der Länge 0 nicht. Neuer Knopf „Punktlinien korrigieren“ im Basemap-Bereich des Druck-Tabs ersetzt die Strichlänge 0 durch 0,01 (festes Muster und zoomabhängiger Ausdruck); die Korrektur lässt sich als Stilvorlage sichern (#90)
|
- Bugfix: Punktlinien des Basemap-Layers (Baumreihe, Hecke, Durchlass, Seilbahnen u. a., 12 Styles) wurden in Karte und Legende nicht gezeichnet – basemap.de nutzt Strichlänge 0 mit runder Kappe, Qt zeichnet Striche der Länge 0 nicht. Neuer Knopf „Punktlinien korrigieren“ im Basemap-Bereich des Druck-Tabs ersetzt die Strichlänge 0 durch 0,01 (festes Muster und zoomabhängiger Ausdruck); die Korrektur lässt sich als Stilvorlage sichern (#90)
|
||||||
- Basemap-Legende für Drucklayouts (#88): Beim Erstellen einer §38-/§41-Vorlage wird ausgewertet, welche Objekte des basemap.de-Vektorkachel-Layers im Verfahrensgebiet (wahlweise im Druckbereich) tatsächlich dargestellt werden; ein Auswahldialog lässt Kategorien abwählen, die gewählten erscheinen mit der Signatur des Projektlayers in einem eigenen Legendenelement „Kartengrundlage (basemap.de)“ über der Quellenangabe. Zuordnung von 434 Basemap-Styles zu 190 Legendeneinträgen (Beschriftungen, Nummernschilder und Hintergrund ausgenommen) in `data/basemap_legende.json` (erzeugt mit `modules/basemap_katalog.py`, fachlich nachbearbeitet)
|
- Basemap-Legende für Drucklayouts (#88): Beim Erstellen einer §38-/§41-Vorlage wird ausgewertet, welche Objekte des basemap.de-Vektorkachel-Layers im Verfahrensgebiet (wahlweise im Druckbereich) tatsächlich dargestellt werden; ein Auswahldialog lässt Kategorien abwählen, die gewählten erscheinen mit der Signatur des Projektlayers in einem eigenen Legendenelement „Kartengrundlage (basemap.de)“ über der Quellenangabe. Zuordnung von 434 Basemap-Styles zu 190 Legendeneinträgen (Beschriftungen, Nummernschilder und Hintergrund ausgenommen) in `data/basemap_legende.json` (erzeugt mit `modules/basemap_katalog.py`, fachlich nachbearbeitet)
|
||||||
- Neuer einklappbarer Bereich „Basemap“ im Druck-Tab: Auswertebereich, Basemap-Legende beim Erstellen an/aus, „Basemap-Legende aktualisieren“ für bestehende Vorlagen und lokale Stilvorlagen des Basemap-Layers (mehrere Varianten, laden, speichern, als Standard markieren, löschen) – z. B. einmal Hausnummern ausschalten und in jedem Projekt wieder laden (#88)
|
- Neuer einklappbarer Bereich „Basemap“ im Druck-Tab: Auswertebereich, Basemap-Legende beim Erstellen an/aus, „Basemap-Legende aktualisieren“ für bestehende Vorlagen und lokale Stilvorlagen des Basemap-Layers (mehrere Varianten, laden, speichern, als Standard markieren, löschen) – z. B. einmal Hausnummern ausschalten und in jedem Projekt wieder laden (#88)
|
||||||
|
|||||||
@@ -893,6 +893,7 @@ QgsApplication: Type[Any]
|
|||||||
QgsSymbolLayerUtils: Type[Any]
|
QgsSymbolLayerUtils: Type[Any]
|
||||||
QgsLayoutMeasurement: Type[Any]
|
QgsLayoutMeasurement: Type[Any]
|
||||||
QgsLayoutUtils: Type[Any]
|
QgsLayoutUtils: Type[Any]
|
||||||
|
QgsSimpleLineSymbolLayer: Type[Any]
|
||||||
SELECT_BEHAVIOR_SET: Any = None
|
SELECT_BEHAVIOR_SET: Any = None
|
||||||
SELECT_RELATION_INTERSECT: Any = None
|
SELECT_RELATION_INTERSECT: Any = None
|
||||||
PROPERTY_TYPE_EXPRESSION: Any = None
|
PROPERTY_TYPE_EXPRESSION: Any = None
|
||||||
@@ -921,6 +922,7 @@ try:
|
|||||||
QgsLayoutMeasurement as _QgsLayoutMeasurement,
|
QgsLayoutMeasurement as _QgsLayoutMeasurement,
|
||||||
QgsLayoutUtils as _QgsLayoutUtils,
|
QgsLayoutUtils as _QgsLayoutUtils,
|
||||||
QgsSymbolLayer as _QgsSymbolLayer,
|
QgsSymbolLayer as _QgsSymbolLayer,
|
||||||
|
QgsSimpleLineSymbolLayer as _QgsSimpleLineSymbolLayer,
|
||||||
Qgis as _QgisVT,
|
Qgis as _QgisVT,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -940,6 +942,7 @@ try:
|
|||||||
QgsSymbolLayerUtils = _QgsSymbolLayerUtils
|
QgsSymbolLayerUtils = _QgsSymbolLayerUtils
|
||||||
QgsLayoutMeasurement = _QgsLayoutMeasurement
|
QgsLayoutMeasurement = _QgsLayoutMeasurement
|
||||||
QgsLayoutUtils = _QgsLayoutUtils
|
QgsLayoutUtils = _QgsLayoutUtils
|
||||||
|
QgsSimpleLineSymbolLayer = _QgsSimpleLineSymbolLayer
|
||||||
|
|
||||||
SELECT_BEHAVIOR_SET = _QgisVT.SelectBehavior.SetSelection
|
SELECT_BEHAVIOR_SET = _QgisVT.SelectBehavior.SetSelection
|
||||||
SELECT_RELATION_INTERSECT = _QgisVT.SelectGeometryRelationship.Intersect
|
SELECT_RELATION_INTERSECT = _QgisVT.SelectGeometryRelationship.Intersect
|
||||||
@@ -1112,6 +1115,19 @@ except Exception:
|
|||||||
|
|
||||||
QgsLayoutUtils = _MockQgsLayoutUtils
|
QgsLayoutUtils = _MockQgsLayoutUtils
|
||||||
|
|
||||||
|
class _MockQgsSimpleLineSymbolLayer:
|
||||||
|
def __init__(self, eigenschaften: dict | None = None):
|
||||||
|
self.eigenschaften = dict(eigenschaften or {})
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create(eigenschaften: dict) -> "_MockQgsSimpleLineSymbolLayer":
|
||||||
|
return _MockQgsSimpleLineSymbolLayer(eigenschaften)
|
||||||
|
|
||||||
|
def layerType(self) -> str:
|
||||||
|
return "SimpleLine"
|
||||||
|
|
||||||
|
QgsSimpleLineSymbolLayer = _MockQgsSimpleLineSymbolLayer
|
||||||
|
|
||||||
# QgsRectangle hatte im allgemeinen Mock-Block keinen Ersatz (nur Typannotation).
|
# QgsRectangle hatte im allgemeinen Mock-Block keinen Ersatz (nur Typannotation).
|
||||||
if "QgsRectangle" not in globals():
|
if "QgsRectangle" not in globals():
|
||||||
class _MockQgsRectangle:
|
class _MockQgsRectangle:
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ from sn_basis.functions.qgiscore_wrapper import (
|
|||||||
QgsLegendRenderer,
|
QgsLegendRenderer,
|
||||||
QgsProperty,
|
QgsProperty,
|
||||||
QgsRendererCategory,
|
QgsRendererCategory,
|
||||||
|
QgsSimpleLineSymbolLayer,
|
||||||
QgsSymbolLayerUtils,
|
QgsSymbolLayerUtils,
|
||||||
QgsUnitTypes,
|
QgsUnitTypes,
|
||||||
QgsVectorLayer,
|
QgsVectorLayer,
|
||||||
@@ -62,6 +63,13 @@ LAYOUT_HILFSLAYER = "sn_basemap_hilfslayer"
|
|||||||
HILFSLAYER_KENNUNG = "sn_basemap_legende"
|
HILFSLAYER_KENNUNG = "sn_basemap_legende"
|
||||||
|
|
||||||
_GEOMETRIE_JE_SYMBOLTYP = {0: "Point", 1: "LineString", 2: "Polygon"}
|
_GEOMETRIE_JE_SYMBOLTYP = {0: "Point", 1: "LineString", 2: "Polygon"}
|
||||||
|
|
||||||
|
# Legendenrand für weiße Flächen ohne Kontur (Basis #92)
|
||||||
|
LEGENDENRAND_BREITE_MM = 0.15
|
||||||
|
LEGENDENRAND_FARBE = "150,150,150,255"
|
||||||
|
# Ab diesem Wert je Farbkanal gilt eine Farbe auf weißem Legendenhintergrund als unsichtbar.
|
||||||
|
HELL_AB = 240
|
||||||
|
DURCHSICHTIG_BIS = 25
|
||||||
_ZOOM_VARIABLEN = {"vector_tile_zoom", "zoom_level"}
|
_ZOOM_VARIABLEN = {"vector_tile_zoom", "zoom_level"}
|
||||||
_VARIABLE = re.compile(r"@([A-Za-z_][A-Za-z0-9_]*)")
|
_VARIABLE = re.compile(r"@([A-Za-z_][A-Za-z0-9_]*)")
|
||||||
|
|
||||||
@@ -181,10 +189,71 @@ def baue_symbole(layer: Any, eintraege: list, zoom_level: int, vector_tile_zoom:
|
|||||||
basis.appendSymbolLayer(symbol_layer.clone())
|
basis.appendSymbolLayer(symbol_layer.clone())
|
||||||
if basis is not None:
|
if basis is not None:
|
||||||
friere_zoom_ein(basis, zoom_level, vector_tile_zoom)
|
friere_zoom_ein(basis, zoom_level, vector_tile_zoom)
|
||||||
|
ergaenze_legendenrand(basis)
|
||||||
symbole[eintrag.schluessel] = basis
|
symbole[eintrag.schluessel] = basis
|
||||||
return symbole
|
return symbole
|
||||||
|
|
||||||
|
|
||||||
|
def _ist_unsichtbar(farbe: tuple) -> bool:
|
||||||
|
"""Ob eine RGBA-Farbe auf weißem Legendenhintergrund praktisch nicht zu sehen ist."""
|
||||||
|
rot, gruen, blau, alpha = farbe
|
||||||
|
return alpha <= DURCHSICHTIG_BIS or min(rot, gruen, blau) >= HELL_AB
|
||||||
|
|
||||||
|
|
||||||
|
def braucht_legendenrand(ebenen: list[dict]) -> bool:
|
||||||
|
"""Ob ein Flächensymbol in der Legende einen Rand braucht.
|
||||||
|
|
||||||
|
``ebenen`` beschreibt die Symbolebenen: ``art`` (``fuellung``, ``linie``, ``sonst``), ``farbe``
|
||||||
|
(RGBA) und bei Füllungen ``rand`` (RGBA oder ``None`` ohne Kontur). Ein Rand ist nötig, wenn
|
||||||
|
alle Ebenen auf weißem Hintergrund unsichtbar sind – z. B. die weiße Fläche „Platz“.
|
||||||
|
"""
|
||||||
|
if not ebenen:
|
||||||
|
return False
|
||||||
|
for ebene in ebenen:
|
||||||
|
if ebene.get("art") not in ("fuellung", "linie") or not _ist_unsichtbar(ebene["farbe"]):
|
||||||
|
return False
|
||||||
|
if ebene.get("rand") is not None and not _ist_unsichtbar(ebene["rand"]):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _rgba(farbe: Any) -> tuple:
|
||||||
|
return farbe.red(), farbe.green(), farbe.blue(), farbe.alpha()
|
||||||
|
|
||||||
|
|
||||||
|
def _ebenen_beschreiben(symbol: Any) -> list[dict]:
|
||||||
|
ebenen = []
|
||||||
|
for ebene in symbol.symbolLayers():
|
||||||
|
art = ebene.layerType()
|
||||||
|
if art == "SimpleFill":
|
||||||
|
ohne_kontur = _enum_int(ebene.strokeStyle()) == 0 # Qt.NoPen
|
||||||
|
ebenen.append({"art": "fuellung", "farbe": _rgba(ebene.color()),
|
||||||
|
"rand": None if ohne_kontur else _rgba(ebene.strokeColor())})
|
||||||
|
elif art == "SimpleLine":
|
||||||
|
ebenen.append({"art": "linie", "farbe": _rgba(ebene.color())})
|
||||||
|
else:
|
||||||
|
ebenen.append({"art": "sonst"})
|
||||||
|
return ebenen
|
||||||
|
|
||||||
|
|
||||||
|
def ergaenze_legendenrand(symbol: Any) -> bool:
|
||||||
|
"""Gibt einem praktisch weißen Flächensymbol ohne Kontur einen dünnen grauen Rand (nur Legende).
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
bool
|
||||||
|
Ob ein Rand ergänzt wurde.
|
||||||
|
"""
|
||||||
|
if _enum_int(symbol.type()) != 2 or not braucht_legendenrand(_ebenen_beschreiben(symbol)):
|
||||||
|
return False
|
||||||
|
symbol.appendSymbolLayer(QgsSimpleLineSymbolLayer.create({
|
||||||
|
"line_color": LEGENDENRAND_FARBE,
|
||||||
|
"line_width": str(LEGENDENRAND_BREITE_MM),
|
||||||
|
"line_width_unit": "MM",
|
||||||
|
}))
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def vorschau_icon(symbol: Any) -> Any:
|
def vorschau_icon(symbol: Any) -> Any:
|
||||||
if symbol is None:
|
if symbol is None:
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
"""
|
||||||
|
Tests für den Legendenrand weißer Basemap-Flächen (Basis #92).
|
||||||
|
|
||||||
|
Weiße Flächen ohne Umrandung (z. B. „Platz“) verschwinden auf dem weißen Legendenhintergrund.
|
||||||
|
Das Legendensymbol bekommt dann einen dünnen grauen Rand; die Karte bleibt unverändert.
|
||||||
|
Das Aussehen in der Legende ist im QGIS-Test geprüft.
|
||||||
|
"""
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from sn_basis.modules import basemap_legende_layout as bll
|
||||||
|
from sn_basis.modules.basemap_legende_layout import braucht_legendenrand, ergaenze_legendenrand
|
||||||
|
|
||||||
|
WEISS = (255, 255, 255, 255)
|
||||||
|
GRUEN = (200, 230, 160, 255)
|
||||||
|
GRAU = (120, 120, 120, 255)
|
||||||
|
|
||||||
|
|
||||||
|
class TestBrauchtLegendenrand(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_weisse_flaeche_ohne_rand(self):
|
||||||
|
"""Prüft, dass eine weiße Fläche ohne Umrandung einen Legendenrand braucht."""
|
||||||
|
self.assertTrue(braucht_legendenrand([{"art": "fuellung", "farbe": WEISS, "rand": None}]))
|
||||||
|
|
||||||
|
def test_farbige_flaeche_oder_flaeche_mit_kontur(self):
|
||||||
|
"""Prüft, dass farbige Flächen und weiße Flächen mit dunkler Kontur oder eigener Konturlinie keinen Rand bekommen."""
|
||||||
|
self.assertFalse(braucht_legendenrand([{"art": "fuellung", "farbe": GRUEN, "rand": None}]))
|
||||||
|
self.assertFalse(braucht_legendenrand([{"art": "fuellung", "farbe": WEISS, "rand": GRAU}]))
|
||||||
|
self.assertFalse(braucht_legendenrand([{"art": "fuellung", "farbe": WEISS, "rand": None},
|
||||||
|
{"art": "linie", "farbe": GRAU}]))
|
||||||
|
|
||||||
|
def test_heller_rand_und_fast_weiss_zaehlen_als_unsichtbar(self):
|
||||||
|
"""Prüft, dass fast weiße Füllungen und helle oder durchsichtige Ränder als unsichtbar gelten."""
|
||||||
|
self.assertTrue(braucht_legendenrand([{"art": "fuellung", "farbe": (250, 248, 245, 255),
|
||||||
|
"rand": (255, 255, 255, 255)}]))
|
||||||
|
self.assertTrue(braucht_legendenrand([{"art": "fuellung", "farbe": WEISS, "rand": (0, 0, 0, 0)}]))
|
||||||
|
|
||||||
|
def test_andere_ebenen_und_leeres_symbol(self):
|
||||||
|
"""Prüft, dass Symbole mit anderen Ebenen (Muster, Punkte) und leere Symbole keinen Rand bekommen."""
|
||||||
|
self.assertFalse(braucht_legendenrand([{"art": "sonst", "farbe": WEISS}]))
|
||||||
|
self.assertFalse(braucht_legendenrand([]))
|
||||||
|
|
||||||
|
|
||||||
|
class _Farbe:
|
||||||
|
def __init__(self, r, g, b, a=255): self.w = (r, g, b, a)
|
||||||
|
def red(self): return self.w[0]
|
||||||
|
def green(self): return self.w[1]
|
||||||
|
def blue(self): return self.w[2]
|
||||||
|
def alpha(self): return self.w[3]
|
||||||
|
|
||||||
|
|
||||||
|
class _Fuellung:
|
||||||
|
def __init__(self, farbe, rand_stil=0, rand=(0, 0, 0, 255)):
|
||||||
|
self._farbe, self._rand_stil, self._rand = _Farbe(*farbe), rand_stil, _Farbe(*rand)
|
||||||
|
|
||||||
|
def layerType(self): return "SimpleFill"
|
||||||
|
def color(self): return self._farbe
|
||||||
|
def strokeStyle(self): return self._rand_stil
|
||||||
|
def strokeColor(self): return self._rand
|
||||||
|
|
||||||
|
|
||||||
|
class _Symbol:
|
||||||
|
def __init__(self, typ, *ebenen):
|
||||||
|
self._typ, self.ebenen = typ, list(ebenen)
|
||||||
|
|
||||||
|
def type(self): return self._typ
|
||||||
|
def symbolLayers(self): return list(self.ebenen)
|
||||||
|
def appendSymbolLayer(self, ebene): self.ebenen.append(ebene)
|
||||||
|
|
||||||
|
|
||||||
|
class _Linienebene:
|
||||||
|
erzeugt = []
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create(cls, eigenschaften):
|
||||||
|
cls.erzeugt.append(eigenschaften)
|
||||||
|
return ("Rand", eigenschaften)
|
||||||
|
|
||||||
|
|
||||||
|
@patch.object(bll, "QgsSimpleLineSymbolLayer", _Linienebene)
|
||||||
|
class TestErgaenzeLegendenrand(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_weisse_flaeche_bekommt_grauen_rand(self):
|
||||||
|
"""Prüft, dass eine weiße Fläche ohne Kontur eine graue Randlinie als oberste Ebene bekommt."""
|
||||||
|
symbol = _Symbol(2, _Fuellung(WEISS))
|
||||||
|
self.assertTrue(ergaenze_legendenrand(symbol))
|
||||||
|
self.assertEqual(len(symbol.ebenen), 2)
|
||||||
|
eigenschaften = symbol.ebenen[-1][1]
|
||||||
|
self.assertEqual(eigenschaften["line_width"], str(bll.LEGENDENRAND_BREITE_MM))
|
||||||
|
self.assertEqual(eigenschaften["line_color"], bll.LEGENDENRAND_FARBE)
|
||||||
|
|
||||||
|
def test_farbige_flaeche_und_linien_bleiben_unveraendert(self):
|
||||||
|
"""Prüft, dass farbige Flächen, Flächen mit Kontur und Liniensymbole keinen Rand bekommen."""
|
||||||
|
for symbol in (_Symbol(2, _Fuellung(GRUEN)), _Symbol(2, _Fuellung(WEISS, rand_stil=1, rand=GRAU)),
|
||||||
|
_Symbol(1, _Fuellung(WEISS))):
|
||||||
|
anzahl = len(symbol.ebenen)
|
||||||
|
self.assertFalse(ergaenze_legendenrand(symbol))
|
||||||
|
self.assertEqual(len(symbol.ebenen), anzahl)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user