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>
265 lines
12 KiB
Python
265 lines
12 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 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()
|