Files
Plugin_SN_Basis/tests/test_basemap_legende_layout.py
T
DanielandClaude Opus 5.5 bf898dcca3 Legenden mindestens so breit wie das Kartenschild (#96)
Standard- und Mindestbreite der Haupt- und Basemap-Legende ist die Breite des Kartenschilds
(180 mm, MIN_SPALTENBREITE_MM) – die zunächst genannten 210 mm sind der gesamte Kartenrand
einschließlich Abständen. Die Legenden passen sich weiter ihrem Inhalt an, werden aber nie
schmaler (setMinimumSize).

QGIS speichert die Mindestgröße nicht im Projekt; nach dem Neuladen schrumpfte die
Legende bei der nächsten Größenanpassung wieder (QGIS-Test: 43,7 mm). Deshalb vermerkt
das Layout die Mindestbreite (sn_legende_mindestbreite), und ein Listener auf readProject
setzt sie beim Laden wieder (setup/teardown_mindestbreiten_listener in main.py, wie der
Verfahrensgebiet-Listener).

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

265 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.
"""
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 LegendenEintrag
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 _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()