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>
150 lines
6.1 KiB
Python
150 lines
6.1 KiB
Python
"""
|
||
Tests für modules/basemap_stilkorrektur.py – Punktlinien des Basemap-Layers sichtbar machen (Basis #90).
|
||
|
||
basemap.de zeichnet Punktlinien mit Strichlänge 0 und runder Kappe; Qt zeichnet Striche der
|
||
Länge 0 nicht. Die Korrektur ersetzt Strichlänge 0 durch einen kleinen Wert – im festen
|
||
Strichmuster und im zoomabhängigen Ausdruck. Symbole und Layer sind Fakes; die Wirkung in
|
||
Karte und Legende ist im QGIS-Test geprüft.
|
||
"""
|
||
import unittest
|
||
|
||
from sn_basis.functions.qgiscore_wrapper import PROPERTY_CUSTOM_DASH, PROPERTY_TYPE_EXPRESSION
|
||
from sn_basis.modules.basemap_stilkorrektur import (
|
||
MINDESTSTRICH,
|
||
korrigiere_ausdruck,
|
||
korrigiere_muster,
|
||
korrigiere_punktlinien,
|
||
)
|
||
|
||
# Ausdruck, wie ihn der QGIS-Konverter für line-dasharray mit Stopps erzeugt (gekürzt)
|
||
AUSDRUCK_BAUMREIHE = (
|
||
"array_to_string(array_foreach(CASE WHEN @vector_tile_zoom <= 12 THEN array(0,1.75) "
|
||
"WHEN @vector_tile_zoom > 12 AND @vector_tile_zoom <= 14 THEN array(0,2) "
|
||
"ELSE array(0,4) END,@element * (scale_linear(@vector_tile_zoom,14,17,3,8))),';')"
|
||
)
|
||
|
||
|
||
class _Eigenschaft:
|
||
def __init__(self, ausdruck):
|
||
self._ausdruck = ausdruck
|
||
|
||
def propertyType(self): return PROPERTY_TYPE_EXPRESSION
|
||
def expressionString(self): return self._ausdruck
|
||
|
||
|
||
class _Eigenschaften:
|
||
def __init__(self, ausdruck=None):
|
||
self.werte = {PROPERTY_CUSTOM_DASH: _Eigenschaft(ausdruck)} if ausdruck else {}
|
||
|
||
def propertyKeys(self): return list(self.werte)
|
||
def property(self, schluessel): return self.werte[schluessel]
|
||
|
||
def setProperty(self, schluessel, eigenschaft):
|
||
text = getattr(eigenschaft, "_ausdruck", None) or getattr(eigenschaft, "_value", None)
|
||
self.werte[schluessel] = _Eigenschaft(text)
|
||
|
||
|
||
class _Linie:
|
||
def __init__(self, muster=None, ausdruck=None):
|
||
self.muster = list(muster) if muster is not None else None
|
||
self.eigenschaften = _Eigenschaften(ausdruck)
|
||
|
||
def useCustomDashPattern(self): return self.muster is not None
|
||
def customDashVector(self): return list(self.muster or [])
|
||
def setCustomDashVector(self, werte): self.muster = list(werte)
|
||
def dataDefinedProperties(self): return self.eigenschaften
|
||
def subSymbol(self): return None
|
||
|
||
|
||
class _Symbol:
|
||
def __init__(self, *ebenen): self.ebenen = list(ebenen)
|
||
def symbolLayers(self): return self.ebenen
|
||
def clone(self): return self # die Fakes prüfen nur das Ergebnis
|
||
|
||
|
||
class _Stil:
|
||
def __init__(self, name, symbol): self._name, self._symbol = name, symbol
|
||
def styleName(self): return self._name
|
||
def symbol(self): return self._symbol
|
||
def setSymbol(self, symbol): self._symbol = symbol
|
||
|
||
|
||
class _Renderer:
|
||
def __init__(self, stile):
|
||
self.stile = stile
|
||
self.gesetzt = []
|
||
|
||
def styles(self): return list(self.stile)
|
||
|
||
def setStyle(self, index, stil):
|
||
self.gesetzt.append(index)
|
||
self.stile[index] = stil
|
||
|
||
|
||
class _Layer:
|
||
def __init__(self, stile):
|
||
self._renderer = _Renderer(stile)
|
||
self.neu_gezeichnet = 0
|
||
|
||
def renderer(self): return self._renderer
|
||
def triggerRepaint(self): self.neu_gezeichnet += 1
|
||
|
||
|
||
def _baumreihe():
|
||
return _Stil("VegetationsL_Baumreihe", _Symbol(_Linie([0.0, 0.93], AUSDRUCK_BAUMREIHE)))
|
||
|
||
|
||
class TestMusterUndAusdruck(unittest.TestCase):
|
||
|
||
def test_strichlaenge_null_im_festen_muster(self):
|
||
"""Prüft, dass Strichlängen 0 (gerade Positionen) im Strichmuster ersetzt und Lücken der Länge 0 nicht verändert werden."""
|
||
self.assertEqual(korrigiere_muster([0.0, 0.93]), [MINDESTSTRICH, 0.93])
|
||
self.assertEqual(korrigiere_muster([2.0, 0.0, 0.0, 1.0]), [2.0, 0.0, MINDESTSTRICH, 1.0])
|
||
self.assertEqual(korrigiere_muster([5.0, 2.0]), [5.0, 2.0])
|
||
|
||
def test_strichlaenge_null_im_zoomabhaengigen_ausdruck(self):
|
||
"""Prüft, dass jede Strichlänge 0 in den array(...)-Mustern des Konverter-Ausdrucks ersetzt wird und der übrige Ausdruck gleich bleibt."""
|
||
neu = korrigiere_ausdruck(AUSDRUCK_BAUMREIHE)
|
||
self.assertNotIn("array(0,", neu)
|
||
self.assertEqual(neu.count(f"array({MINDESTSTRICH},"), 3)
|
||
self.assertIn("@element * (scale_linear(@vector_tile_zoom,14,17,3,8))", neu)
|
||
self.assertEqual(korrigiere_ausdruck("array(3,0,1,2)"), "array(3,0,1,2)")
|
||
self.assertEqual(korrigiere_ausdruck("array(1,0,0,2)"), f"array(1,0,{MINDESTSTRICH},2)")
|
||
|
||
|
||
class TestLayerkorrektur(unittest.TestCase):
|
||
|
||
def test_punktlinien_werden_korrigiert_andere_styles_nicht(self):
|
||
"""Prüft, dass nur Styles mit Strichlänge 0 korrigiert werden – Muster und Ausdruck – und die Anzahl zurückkommt."""
|
||
weg = _Stil("Kontur_Wirtschaftsweg", _Symbol(_Linie()))
|
||
strichlinie = _Stil("Grenze", _Symbol(_Linie([4.0, 2.0], "array_to_string(array(4,2),';')")))
|
||
layer = _Layer([weg, _baumreihe(), strichlinie])
|
||
|
||
self.assertEqual(korrigiere_punktlinien(layer), 1)
|
||
|
||
linie = layer.renderer().styles()[1].symbol().symbolLayers()[0]
|
||
self.assertEqual(linie.customDashVector(), [MINDESTSTRICH, 0.93])
|
||
self.assertNotIn("array(0,", linie.dataDefinedProperties().property(PROPERTY_CUSTOM_DASH).expressionString())
|
||
self.assertEqual(layer.renderer().gesetzt, [1])
|
||
self.assertEqual(layer.neu_gezeichnet, 1)
|
||
|
||
def test_zweiter_lauf_korrigiert_nichts(self):
|
||
"""Prüft, dass ein zweiter Lauf keine Styles mehr korrigiert und nicht neu zeichnen lässt."""
|
||
layer = _Layer([_baumreihe()])
|
||
korrigiere_punktlinien(layer)
|
||
self.assertEqual(korrigiere_punktlinien(layer), 0)
|
||
self.assertEqual(layer.neu_gezeichnet, 1)
|
||
|
||
def test_nur_ausdruck_ohne_festes_muster(self):
|
||
"""Prüft, dass auch ein Style korrigiert wird, dessen Strichlänge 0 nur im zoomabhängigen Ausdruck steht."""
|
||
layer = _Layer([_Stil("Hecke", _Symbol(_Linie([1.0, 2.0], "array_to_string(array(0,3),';')")))])
|
||
self.assertEqual(korrigiere_punktlinien(layer), 1)
|
||
|
||
def test_layer_ohne_renderer(self):
|
||
"""Prüft, dass ein Layer ohne Renderer 0 korrigierte Styles liefert."""
|
||
self.assertEqual(korrigiere_punktlinien(type("L", (), {"renderer": lambda s: None})()), 0)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
unittest.main()
|