Beim Erstellen einer §38-/§41-Vorlage wertet BasemapLegende die Kacheln des sichtbaren basemap.de-Layers im Verfahrensgebiet (wahlweise Druckbereich) kachelweise über selectByGeometry aus, prüft die Renderer-Styles mit derselben Zoomlogik wie QGIS und bildet daraus Legendeneinträge. Ein Mehrfachauswahl-Dialog (neu: ask_mehrfachauswahl) lässt Kategorien abwählen; die Auswahl wird im Projekt gemerkt. Die gewählten Einträge stehen mit der Signatur des Projektlayers (zoomabhängige Werte festgeschrieben) in einem eigenen Legendenelement über der Quellenangabe. Ein eigenes Element ist nötig, weil die nach Karteninhalt filternde Hauptlegende die Hilfslayer ausblenden würde. Wrapper um die nötigen QGIS-/Qt-Symbole mit Mock ergänzt (eigener Block in qgiscore_wrapper; QgsRectangle hatte bisher keinen Mock). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
227 lines
9.9 KiB
Python
227 lines
9.9 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
|
||
|
||
def setColumnCount(self, n): self.spalten = n
|
||
def setSplitLayer(self, wert): pass
|
||
def setEqualColumnWidth(self, wert): pass
|
||
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_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()
|