Der Legendenkatalog nannte für die Autobahn nur Kontur und Decker der getrennten Fahrbahn, die basemap.de nur bis Zoom 13 zeichnet. Bei den üblichen Druckmaßstäben (Zoom 14 und mehr) griff deshalb der Rückfall auf den ersten getroffenen Style, die graue Kontur. Jetzt stehen die Styles für alle Zoombereiche in symbol_stile; inaktive werden wie bisher übergangen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
305 lines
15 KiB
Python
305 lines
15 KiB
Python
"""
|
||
Tests für modules/basemap_legende_layout.py – Basemap-Legende im Drucklayout (Basis #88).
|
||
|
||
Layout, Legende und Projekt sind Fakes. Aussehen der Symbole und Platzierung im echten Layout
|
||
sind im QGIS-Test geprüft (siehe Pull Request).
|
||
"""
|
||
import json
|
||
import unittest
|
||
from unittest.mock import patch
|
||
|
||
from sn_basis.modules import basemap_legende_layout as bll
|
||
from sn_basis.modules.basemap_legende import Katalog, LegendenEintrag, bilde_eintraege
|
||
|
||
|
||
def _wert(objekt, name):
|
||
"""Liest x/y/width/height – im Mock-Modus Attribute, in QGIS Methoden."""
|
||
wert = getattr(objekt, name)
|
||
return wert() if callable(wert) else wert
|
||
|
||
|
||
class _Rechteck:
|
||
def __init__(self, x, y, b, h): self.w = (x, y, b, h)
|
||
def x(self): return self.w[0]
|
||
def y(self): return self.w[1]
|
||
def width(self): return self.w[2]
|
||
def height(self): return self.w[3]
|
||
|
||
|
||
class _Element:
|
||
def __init__(self, x=0.0, y=0.0, b=0.0, h=0.0):
|
||
self.rechteck = _Rechteck(x, y, b, h)
|
||
|
||
def sceneBoundingRect(self): return self.rechteck
|
||
|
||
|
||
class _Seite:
|
||
def __init__(self, breite): self._breite = breite
|
||
def pageSize(self): return type("S", (), {"width": lambda s: self._breite})()
|
||
|
||
|
||
class _Layout:
|
||
def __init__(self, elemente=None, breite=841.0, name="Plan"):
|
||
self.elemente = dict(elemente or {})
|
||
self.eigenschaften = {}
|
||
self.entfernt = []
|
||
self._breite, self._name = breite, name
|
||
|
||
def itemById(self, element_id): return self.elemente.get(element_id)
|
||
def items(self): return list(self.elemente.values())
|
||
def setCustomProperty(self, k, v): self.eigenschaften[k] = v
|
||
def customProperty(self, k, standard=None): return self.eigenschaften.get(k, standard)
|
||
def removeLayoutItem(self, element): self.entfernt.append(element)
|
||
def name(self): return self._name
|
||
def pageCollection(self): return type("P", (), {"page": lambda s, i: _Seite(self._breite)})()
|
||
|
||
|
||
class _Legende(_Element):
|
||
"""Legende, deren Platzbedarf je Spaltenzahl vorgegeben ist."""
|
||
|
||
def __init__(self):
|
||
super().__init__()
|
||
self.spalten = 1
|
||
self.bewegt = None
|
||
self.groesse = None
|
||
self.gleiche_breite = True
|
||
|
||
def setColumnCount(self, n): self.spalten = n
|
||
def setSplitLayer(self, wert): pass
|
||
def setEqualColumnWidth(self, wert): self.gleiche_breite = wert
|
||
def setReferencePoint(self, punkt): pass
|
||
def attemptMove(self, punkt): self.bewegt = punkt
|
||
def attemptResize(self, groesse): self.groesse = groesse
|
||
|
||
|
||
class TestKennzeichenUndKarte(unittest.TestCase):
|
||
|
||
def test_layout_kennzeichen_setzen_und_erkennen(self):
|
||
"""Prüft, dass ein markiertes Layout als Nutzer der Basemap-Legende erkannt und in der Projektliste geführt wird."""
|
||
markiert, normal = _Layout(name="§41 Plan"), _Layout(name="Baufreigabe")
|
||
bll.markiere_layout(markiert)
|
||
self.assertTrue(bll.nutzt_basemap_legende(markiert))
|
||
self.assertFalse(bll.nutzt_basemap_legende(normal))
|
||
projekt = type("P", (), {"layoutManager": lambda s: type("M", (), {
|
||
"printLayouts": lambda s: [markiert, normal]})()})()
|
||
self.assertEqual(bll.layouts_mit_basemap_legende(projekt), ["§41 Plan"])
|
||
|
||
def test_hauptkarte_ueber_id(self):
|
||
"""Prüft, dass die Karte mit der ID „Hauptkarte“ gefunden wird."""
|
||
karte = _Element()
|
||
self.assertIs(bll.hauptkarte(_Layout({"Hauptkarte": karte})), karte)
|
||
self.assertIsNone(bll.hauptkarte(_Layout()))
|
||
|
||
|
||
class TestSymbolwahl(unittest.TestCase):
|
||
|
||
def test_symbolnamen_aus_aktiven_symbol_stilen_in_renderer_reihenfolge(self):
|
||
"""Prüft, dass die Symbol-Styles des Katalogs in Zeichenreihenfolge genommen und inaktive übergangen werden."""
|
||
eintrag = LegendenEintrag("kreis", "Kreisstraße", "Verkehr", ["Decker_Kreisstr", "Kontur_Kreisstr"],
|
||
treffer=["Bruecke_Kreisstr_Decker"])
|
||
aktive = {"Kontur_Kreisstr": 3, "Decker_Kreisstr": 7, "Bruecke_Kreisstr_Decker": 9}
|
||
self.assertEqual(bll.waehle_symbolnamen(eintrag, aktive), ["Kontur_Kreisstr", "Decker_Kreisstr"])
|
||
|
||
def test_ohne_aktive_symbol_stile_erster_getroffener_style(self):
|
||
"""Prüft, dass ohne aktiven Symbol-Style der erste tatsächlich getroffene Style das Legendensymbol liefert."""
|
||
eintrag = LegendenEintrag("kreis", "Kreisstraße", "Verkehr", ["Decker_Kreisstr"],
|
||
treffer=["Bruecke_Kreisstr_Decker", "Tunnel_Kreisstr"])
|
||
aktive = {"Bruecke_Kreisstr_Decker": 9, "Tunnel_Kreisstr": 2}
|
||
self.assertEqual(bll.waehle_symbolnamen(eintrag, aktive), ["Bruecke_Kreisstr_Decker"])
|
||
|
||
|
||
class TestAutobahnSymbol(unittest.TestCase):
|
||
"""Autobahn-Symbol aus dem ausgelieferten Katalog (Basis #98).
|
||
|
||
Zoombereiche und Renderer-Reihenfolge nach basemap.de ``bm_web_col`` Version 8: Die Styles
|
||
``*_Autobahn_getrennt`` gelten nur bis Zoom 13, ab Zoom 14 ``*_Autobahn_getrennt_Fahrbahnachse``,
|
||
unter Zoom 11 nur ``Decker_Autobahn_getrennt_ohne_Tunnel_Bruecke``.
|
||
"""
|
||
|
||
REIHENFOLGE = {
|
||
"Staumauer_Staudamm_Autobahn": 140, "Kontur_Autobahn_Abfahrt": 152, "Kontur_Autobahn_getrennt": 169,
|
||
"Kontur_Autobahn_getrennt_Fahrbahnachse": 170, "Decker_Autobahn_Abfahrt_einblenden": 177,
|
||
"Decker_Autobahn_Abfahrt": 183, "Decker_Autobahn_getrennt_ohne_Tunnel_Bruecke": 203,
|
||
"Decker_Autobahn_getrennt": 204, "Decker_Autobahn_getrennt_Fahrbahnachse": 205,
|
||
"Mittellinie_Autobahn": 206,
|
||
}
|
||
|
||
def _symbolnamen(self, aktiv: list[str]) -> list[str]:
|
||
katalog = Katalog.laden()
|
||
eintrag, = bilde_eintraege(aktiv, katalog)
|
||
return bll.waehle_symbolnamen(eintrag, {name: self.REIHENFOLGE[name] for name in aktiv})
|
||
|
||
def test_druckmassstab_ab_zoom_14_blauer_decker_auf_grauer_kontur(self):
|
||
"""Prüft, dass das Autobahn-Symbol ab Zoom 14 aus grauer Kontur und blauem Decker der Fahrbahnachse besteht statt nur aus einem grauen Style."""
|
||
aktiv = ["Staumauer_Staudamm_Autobahn", "Kontur_Autobahn_Abfahrt", "Kontur_Autobahn_getrennt_Fahrbahnachse",
|
||
"Decker_Autobahn_Abfahrt", "Decker_Autobahn_getrennt_Fahrbahnachse", "Mittellinie_Autobahn"]
|
||
self.assertEqual(self._symbolnamen(aktiv),
|
||
["Kontur_Autobahn_getrennt_Fahrbahnachse", "Decker_Autobahn_getrennt_Fahrbahnachse"])
|
||
|
||
def test_zoom_11_bis_13_bisherige_kontur_und_decker(self):
|
||
"""Prüft, dass das Autobahn-Symbol bei Zoom 11 bis 13 weiterhin aus Kontur und Decker der getrennten Fahrbahn besteht."""
|
||
aktiv = ["Staumauer_Staudamm_Autobahn", "Kontur_Autobahn_Abfahrt", "Kontur_Autobahn_getrennt",
|
||
"Decker_Autobahn_Abfahrt", "Decker_Autobahn_getrennt", "Mittellinie_Autobahn"]
|
||
self.assertEqual(self._symbolnamen(aktiv), ["Kontur_Autobahn_getrennt", "Decker_Autobahn_getrennt"])
|
||
|
||
def test_unter_zoom_11_blauer_decker(self):
|
||
"""Prüft, dass das Autobahn-Symbol unter Zoom 11 den blauen Decker der Übersichtsdarstellung nutzt."""
|
||
aktiv = ["Decker_Autobahn_Abfahrt_einblenden", "Decker_Autobahn_getrennt_ohne_Tunnel_Bruecke"]
|
||
self.assertEqual(self._symbolnamen(aktiv), ["Decker_Autobahn_getrennt_ohne_Tunnel_Bruecke"])
|
||
|
||
|
||
class _Symbol:
|
||
def __init__(self, typ): self._typ = typ
|
||
def type(self): return self._typ
|
||
|
||
|
||
class _Projekt:
|
||
def __init__(self):
|
||
self.layer = {}
|
||
|
||
def addMapLayer(self, layer, in_baum=True):
|
||
layer._in_baum = in_baum
|
||
self.layer[layer.id()] = layer
|
||
|
||
def mapLayer(self, layer_id): return self.layer.get(layer_id)
|
||
def removeMapLayer(self, layer_id): self.layer.pop(layer_id, None)
|
||
|
||
|
||
class _Speicherlayer:
|
||
zaehler = 0
|
||
|
||
def __init__(self, uri, name, provider):
|
||
_Speicherlayer.zaehler += 1
|
||
self.uri, self._name, self._id = uri, name, f"hl{_Speicherlayer.zaehler}"
|
||
self.eigenschaften = {}
|
||
self.renderer = None
|
||
|
||
def id(self): return self._id
|
||
def setRenderer(self, renderer): self.renderer = renderer
|
||
def setCustomProperty(self, k, v): self.eigenschaften[k] = v
|
||
|
||
|
||
class TestHilfslayer(unittest.TestCase):
|
||
|
||
@patch("sn_basis.modules.basemap_legende_layout.QgsVectorLayer", _Speicherlayer)
|
||
def test_je_gruppe_und_geometrietyp_ein_leerer_hilfslayer(self):
|
||
"""Prüft, dass je Gruppe und Geometrietyp ein Hilfslayer außerhalb des Layerbaums entsteht, Flächen vor Linien, ohne Rückfrage beim Schließen."""
|
||
eintraege = [LegendenEintrag("kreis", "Kreisstraße", "Verkehr", []),
|
||
LegendenEintrag("platz", "Platz", "Verkehr", []),
|
||
LegendenEintrag("wald", "Wald", "Vegetation", []),
|
||
LegendenEintrag("ohne", "Ohne Symbol", "Vegetation", [])]
|
||
symbole = {"kreis": _Symbol(1), "platz": _Symbol(2), "wald": _Symbol(2)}
|
||
projekt = _Projekt()
|
||
|
||
gruppen = bll.erzeuge_hilfslayer(projekt, "Plan", "EPSG:25833", eintraege, symbole)
|
||
|
||
self.assertEqual([g for g, _ in gruppen], ["Verkehr", "Vegetation"])
|
||
verkehr = gruppen[0][1]
|
||
self.assertTrue(verkehr[0].uri.startswith("Polygon?crs=EPSG:25833"))
|
||
self.assertTrue(verkehr[1].uri.startswith("LineString?"))
|
||
self.assertEqual([k.label() for k in verkehr[1].renderer.categories()], ["Kreisstraße"])
|
||
self.assertEqual(len(gruppen[1][1]), 1)
|
||
for layer in projekt.layer.values():
|
||
self.assertFalse(layer._in_baum)
|
||
self.assertEqual(layer.eigenschaften["skipMemoryLayersCheck"], 1)
|
||
self.assertEqual(layer.eigenschaften[bll.HILFSLAYER_KENNUNG], "Plan")
|
||
|
||
def test_alte_hilfslayer_werden_entfernt(self):
|
||
"""Prüft, dass die Hilfslayer einer früheren Basemap-Legende des Layouts aus dem Projekt entfernt werden."""
|
||
projekt = _Projekt()
|
||
alt = _Speicherlayer("Point", "alt", "memory")
|
||
projekt.addMapLayer(alt, False)
|
||
layout = _Layout()
|
||
layout.setCustomProperty(bll.LAYOUT_HILFSLAYER, f"{alt.id()},gibt-es-nicht")
|
||
bll.entferne_hilfslayer(projekt, layout)
|
||
self.assertEqual(projekt.layer, {})
|
||
|
||
def test_ohne_eintraege_wird_vorhandene_basemap_legende_entfernt(self):
|
||
"""Prüft, dass ohne gewählte Einträge eine vorhandene Basemap-Legende entfernt und die Auswahl am Layout vermerkt wird."""
|
||
vorhandene = _Legende()
|
||
layout = _Layout({bll.LEGENDE_ID: vorhandene})
|
||
hinweis = bll.schreibe_legende(layout, _Element(), [], {}, _Projekt(), bereich="druckbereich", massstab=2500)
|
||
self.assertEqual(hinweis, "")
|
||
self.assertEqual(layout.entfernt, [vorhandene])
|
||
self.assertEqual(layout.eigenschaften[bll.LAYOUT_BEREICH], "druckbereich")
|
||
self.assertEqual(json.loads(layout.eigenschaften[bll.LAYOUT_EINTRAEGE]), [])
|
||
self.assertTrue(bll.nutzt_basemap_legende(layout))
|
||
|
||
|
||
class TestPlatzierung(unittest.TestCase):
|
||
|
||
def _layout(self, breite=841.0):
|
||
return _Layout({
|
||
bll.HAUPTLEGENDE_ID: _Element(380, 10, 180, 50),
|
||
bll.QUELLENANGABE_ID: _Element(380, 198, 180, 20),
|
||
bll.HAUPTKARTE_ID: _Element(10, 10, 360, 310),
|
||
}, breite=breite)
|
||
|
||
def _platziere(self, layout, bedarf):
|
||
legende = _Legende()
|
||
with patch.object(bll, "mindestgroesse", side_effect=lambda l, lay, n: bedarf[n]):
|
||
hinweis = bll.platziere(layout, legende)
|
||
return legende, hinweis
|
||
|
||
def test_einspaltig_ueber_der_quellenangabe(self):
|
||
"""Prüft, dass die Basemap-Legende einspaltig unten an der Quellenangabe verankert wird, wenn der Platz reicht."""
|
||
legende, hinweis = self._platziere(self._layout(), {1: (100.0, 120.0)})
|
||
self.assertEqual(hinweis, "")
|
||
self.assertEqual(legende.spalten, 1)
|
||
self.assertEqual((_wert(legende.bewegt, "x"), _wert(legende.bewegt, "y")), (380, 198 - bll.ABSTAND_MM))
|
||
self.assertEqual((_wert(legende.groesse, "width"), _wert(legende.groesse, "height")), (100.0, 120.0))
|
||
|
||
def test_mehrspaltig_wenn_hoehe_nicht_reicht(self):
|
||
"""Prüft, dass die Legende zwei- oder dreispaltig wird, wenn sie einspaltig nicht zwischen Hauptlegende und Quellenangabe passt."""
|
||
legende, hinweis = self._platziere(self._layout(), {1: (100.0, 300.0), 2: (200.0, 150.0), 3: (300.0, 100.0)})
|
||
self.assertEqual(hinweis, "")
|
||
self.assertEqual(legende.spalten, 3)
|
||
|
||
def test_spalten_ungleich_breit(self):
|
||
"""Prüft, dass die Basemap-Legende ungleich breite Spalten bekommt (#96)."""
|
||
legende, _ = self._platziere(self._layout(), {1: (100.0, 300.0), 2: (200.0, 150.0), 3: (300.0, 100.0)})
|
||
self.assertFalse(legende.gleiche_breite)
|
||
|
||
def test_mindestgroesse_mit_ungleichen_spalten_und_umbruch(self):
|
||
"""Prüft, dass der Platzbedarf mit ungleich breiten Spalten und Textumbruch berechnet wird (#96)."""
|
||
class Einstellungen:
|
||
def __init__(self): self.werte = {}
|
||
def __getattr__(self, name): return lambda wert: self.werte.__setitem__(name, wert)
|
||
|
||
einstellungen = Einstellungen()
|
||
legende = type("L", (), {"legendSettings": lambda s: einstellungen, "model": lambda s: None})()
|
||
renderer = type("R", (), {"minimumSize": lambda s, k: type("G", (), {"width": lambda s: 80.0, "height": lambda s: 40.0})()})
|
||
with patch.object(bll, "QgsLayoutUtils", type("U", (), {"createRenderContextForLayout": staticmethod(lambda l, p: object())})), patch.object(bll, "QgsLegendRenderer", lambda modell, einst: renderer()):
|
||
self.assertEqual(bll.mindestgroesse(legende, object(), 2), (80.0, 40.0))
|
||
self.assertEqual(einstellungen.werte["setColumnCount"], 2)
|
||
self.assertFalse(einstellungen.werte["setEqualColumnWidth"])
|
||
self.assertEqual(einstellungen.werte["setAutoWrapLinesAfter"], bll.TEXTUMBRUCH_MM)
|
||
|
||
def test_neue_basemap_legende_mindestens_so_breit_wie_kartenschild(self):
|
||
"""Prüft, dass eine neu angelegte Basemap-Legende die Breite des Kartenschilds als Mindestbreite bekommt."""
|
||
class Element:
|
||
def __init__(self, layout): self.mindestbreite = None
|
||
def __getattr__(self, name): return lambda *a: None
|
||
def setMinimumSize(self, groesse):
|
||
self.mindestbreite = groesse.width() if callable(groesse.width) else groesse.width
|
||
|
||
layout = type("L", (), {"addLayoutItem": lambda s, e: None})()
|
||
with patch.object(bll, "QgsLayoutItemLegend", Element):
|
||
legende = bll._neues_legendenelement(layout)
|
||
self.assertEqual(legende.mindestbreite, 180.0)
|
||
|
||
def test_titel_zweizeilig(self):
|
||
"""Prüft, dass der Titel der Basemap-Legende zweizeilig ist, damit er die Mindestbreite nicht bestimmt (#96)."""
|
||
self.assertEqual(bll.LEGENDE_TITEL.splitlines(), ["Kartengrundlage", "(basemap.de)"])
|
||
|
||
def test_nie_breiter_als_der_seitenrand_mit_hinweis(self):
|
||
"""Prüft, dass keine Spaltenzahl gewählt wird, die über den Seitenrand ragt, und dann ein Hinweis kommt."""
|
||
legende, hinweis = self._platziere(self._layout(breite=600.0),
|
||
{1: (100.0, 300.0), 2: (200.0, 160.0), 3: (300.0, 100.0)})
|
||
self.assertEqual(legende.spalten, 2)
|
||
self.assertIn("mehr Platz", hinweis)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
unittest.main()
|