Files
Plugin_SN_Basis/tests/test_basemap_legende.py
T
DanielandClaude Opus 5.5 b549cb8a5a Basemap-Legende: Auswertung und Legendenelement im Drucklayout (#88)
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>
2026-10-02 11:57:09 +02:00

328 lines
16 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.py – Zuordnung, Auswahl und Ablauf der Basemap-Legende (Basis #88).
Die Kachelauswertung und die Layoutausgabe werden durch Fakes ersetzt; die echte Auswertung
gegen basemap.de ist im QGIS-Test geprüft (siehe Pull Request).
"""
import json
import unittest
from unittest.mock import patch
from sn_basis.functions.variable_wrapper import get_variable, set_variable
from sn_basis.modules import basemap_legende as bl
from sn_basis.modules.basemap_legende import (
AUSWAHL_VAR,
AUSWERTEBEREICH_DRUCKBEREICH,
AUSWERTEBEREICH_VAR,
AUSWERTEBEREICH_VERFAHRENSGEBIET,
BasemapLegende,
Katalog,
LEGENDE_ERZEUGEN_VAR,
LegendenEintrag,
ObjektInfo,
StilInfo,
bilde_eintraege,
ermittle_getroffene_stile,
ist_basemap_layer,
legende_erzeugen_aktiv,
lies_auswahl,
lies_auswertebereich,
merke_auswahl,
setze_auswertebereich,
sichtbare_layer_der_karte,
stil_ist_aktiv,
vorauswahl,
)
KATALOG = Katalog({
"gruppen": ["Verkehr", "Vegetation"],
"nie_in_legende": ["Hintergrund"],
"nie_in_legende_praefixe": ["Nummer_"],
"eintraege": [
{"schluessel": "kreis", "bezeichnung": "Kreisstraße", "gruppe": "Verkehr",
"treffer_stile": ["Kontur_Kreisstr", "Decker_Kreisstr", "Bruecke_Kreisstr_Decker"],
"symbol_stile": ["Kontur_Kreisstr", "Decker_Kreisstr"]},
{"schluessel": "wald", "bezeichnung": "Wald", "gruppe": "Vegetation",
"treffer_stile": ["VegetationsF_Wald"], "symbol_stile": ["VegetationsF_Wald"]},
{"schluessel": "weg", "bezeichnung": "Wirtschaftsweg", "gruppe": "Verkehr",
"treffer_stile": ["Kontur_Wirtschaftsweg"], "symbol_stile": ["Kontur_Wirtschaftsweg"]},
],
})
def _filter_ueber_attribute(filter_text, objekt):
"""Einfache Filterprüfung für Tests: Filter der Form klasse=Wert."""
feld, wert = filter_text.split("=")
return objekt.attribute.get(feld) == wert
def _leere_variablen():
for schluessel in (AUSWAHL_VAR, AUSWERTEBEREICH_VAR, LEGENDE_ERZEUGEN_VAR):
set_variable(schluessel, "", scope="project")
class TestStileUndEintraege(unittest.TestCase):
def test_stil_aktiv_nur_eingeschaltet_und_im_zoombereich(self):
"""Prüft, dass ein Style nur aktiv ist, wenn er eingeschaltet ist und die Zoomstufe im Bereich liegt (-1 = unbegrenzt)."""
stil = StilInfo("A", "L", 1, min_zoom=11, max_zoom=14)
self.assertTrue(stil_ist_aktiv(stil, 11))
self.assertFalse(stil_ist_aktiv(stil, 15))
self.assertFalse(stil_ist_aktiv(StilInfo("A", "L", 1, aktiv=False), 12))
self.assertTrue(stil_ist_aktiv(StilInfo("A", "L", 1), 20))
def test_getroffene_stile_pruefen_layer_geometrie_zoom_und_filter(self):
"""Prüft, dass ein Style nur Objekte seines Quell-Layers und Geometrietyps zählt, deren Filter zutrifft, und nur bei passender Zoomstufe."""
stile = [
StilInfo("Decker_Kreisstr", "Verkehrslinie", 1, filter="klasse=Kreisstraße"),
StilInfo("Decker_Bundesstr", "Verkehrslinie", 1, filter="klasse=Bundesstraße"),
StilInfo("VegetationsF_Wald", "Vegetationsflaeche", 2, filter="klasse=Wald"),
StilInfo("Nur_klein", "Verkehrslinie", 1, max_zoom=10),
StilInfo("Alle_Layer", "", 2),
]
objekte = [
ObjektInfo("Verkehrslinie", 1, {"klasse": "Kreisstraße"}),
ObjektInfo("Vegetationsflaeche", 2, {"klasse": "Wald"}),
ObjektInfo("Vegetationsflaeche", 1, {"klasse": "Wald"}), # Linie: passt nicht zum Flächenstyle
]
getroffen = ermittle_getroffene_stile(objekte, stile, 15, _filter_ueber_attribute)
self.assertEqual(getroffen, {"Decker_Kreisstr": 1, "VegetationsF_Wald": 1, "Alle_Layer": 1})
def test_eintraege_fassen_styles_zusammen_und_sortieren_nach_gruppe(self):
"""Prüft, dass mehrere Styles eines Katalogeintrags einen Eintrag ergeben und die Einträge nach Gruppen- und Katalogreihenfolge sortiert sind."""
eintraege = bilde_eintraege(
["VegetationsF_Wald", "Kontur_Wirtschaftsweg", "Decker_Kreisstr", "Bruecke_Kreisstr_Decker"], KATALOG)
self.assertEqual([e.schluessel for e in eintraege], ["kreis", "weg", "wald"])
self.assertEqual(eintraege[0].treffer, ["Decker_Kreisstr", "Bruecke_Kreisstr_Decker"])
self.assertEqual(eintraege[0].symbol_stile, ["Kontur_Kreisstr", "Decker_Kreisstr"])
def test_ausgeschlossene_und_unbekannte_styles(self):
"""Prüft, dass ausgeschlossene Styles entfallen und unbekannte Styles als nicht bekannter Eintrag in „Sonstiges“ landen."""
eintraege = bilde_eintraege(["Hintergrund", "Nummer_B6", "Eigener_Style"], KATALOG)
self.assertEqual(len(eintraege), 1)
self.assertEqual(eintraege[0].schluessel, "stil:Eigener_Style")
self.assertEqual(eintraege[0].gruppe, "Sonstiges")
self.assertFalse(eintraege[0].bekannt)
class TestAuswahlUndEinstellungen(unittest.TestCase):
def setUp(self):
_leere_variablen()
def tearDown(self):
_leere_variablen()
def test_vorauswahl_bekannte_an_unbekannte_aus(self):
"""Prüft, dass ohne gespeicherte Auswahl Katalogeinträge vorausgewählt und unbekannte Styles abgewählt sind."""
eintraege = [LegendenEintrag("kreis", "Kreisstraße", "Verkehr", []),
LegendenEintrag("stil:X", "X", "Sonstiges", [], bekannt=False)]
self.assertEqual(vorauswahl(eintraege), {"kreis"})
def test_auswahl_wird_gemerkt_und_vorbelegt(self):
"""Prüft, dass ab- und angewählte Einträge in der Projektvariable gespeichert und beim nächsten Mal so vorbelegt werden."""
eintraege = [LegendenEintrag("kreis", "Kreisstraße", "Verkehr", []),
LegendenEintrag("stil:X", "X", "Sonstiges", [], bekannt=False)]
merke_auswahl(eintraege, ["stil:X"])
self.assertEqual(lies_auswahl(), {"ausgeblendet": ["kreis"], "eingeblendet": ["stil:X"]})
self.assertEqual(vorauswahl(eintraege, lies_auswahl()), {"stil:X"})
def test_merken_ergaenzt_die_bisherige_auswahl(self):
"""Prüft, dass das Merken die Auswahl nicht angezeigter Einträge aus früheren Läufen behält."""
merke_auswahl([LegendenEintrag("wald", "Wald", "Vegetation", [])], [])
merke_auswahl([LegendenEintrag("kreis", "Kreisstraße", "Verkehr", [])], ["kreis"])
self.assertEqual(lies_auswahl()["ausgeblendet"], ["wald"])
def test_kaputte_projektvariable_ergibt_leere_auswahl(self):
"""Prüft, dass eine nicht lesbare Projektvariable als leere Auswahl gilt."""
set_variable(AUSWAHL_VAR, "{kein json", scope="project")
self.assertEqual(lies_auswahl(), {"ausgeblendet": [], "eingeblendet": []})
def test_auswertebereich_standard_und_umstellung(self):
"""Prüft, dass der Auswertebereich standardmäßig das Verfahrensgebiet ist, umgestellt werden kann und unbekannte Werte abgewiesen werden."""
self.assertEqual(lies_auswertebereich(), AUSWERTEBEREICH_VERFAHRENSGEBIET)
setze_auswertebereich(AUSWERTEBEREICH_DRUCKBEREICH)
self.assertEqual(lies_auswertebereich(), AUSWERTEBEREICH_DRUCKBEREICH)
with self.assertRaises(ValueError):
setze_auswertebereich("irgendwo")
def test_legende_erzeugen_standard_an(self):
"""Prüft, dass das Erzeugen der Basemap-Legende standardmäßig eingeschaltet ist und abgeschaltet werden kann."""
self.assertTrue(legende_erzeugen_aktiv())
bl.setze_legende_erzeugen(False)
self.assertFalse(legende_erzeugen_aktiv())
class _FakeVektorkachelLayer:
def __init__(self, quelle="", name="Layer"):
self._quelle, self._name = quelle, name
def tileMatrixSet(self):
return None
def source(self):
return self._quelle
def name(self):
return self._name
def crs(self):
return "EPSG:3857"
class _FakeVektorLayer:
def source(self):
return "https://sgx.geodatenzentrum.de/gdz_basemapde_vektor/wms"
def name(self):
return "Basemap.de WMS"
class TestLayerErkennung(unittest.TestCase):
def test_erkennung_ueber_quelle_und_namen(self):
"""Prüft, dass der Basemap-Layer an Vektorkacheln mit basemap.de-Quelle erkannt wird, der Name nur als Rückfall dient und andere Layertypen nicht zählen."""
self.assertTrue(ist_basemap_layer(_FakeVektorkachelLayer(
"type=xyz&url=https://sgx.geodatenzentrum.de/gdz_basemapde_vektor/tiles/v2/{z}/{x}/{y}.pbf")))
self.assertTrue(ist_basemap_layer(_FakeVektorkachelLayer("type=mbtiles", "Basemap.de - Farbe")))
self.assertFalse(ist_basemap_layer(_FakeVektorkachelLayer("type=xyz&url=https://andere.de", "OSM")))
self.assertFalse(ist_basemap_layer(_FakeVektorLayer()))
self.assertFalse(ist_basemap_layer(None))
def test_sichtbare_layer_aus_thema_feste_liste_oder_layerbaum(self):
"""Prüft, dass die Layer der Karte aus dem Kartenthema, sonst aus der festen Layerliste, sonst aus dem sichtbaren Layerbaum kommen."""
class Karte:
def __init__(self, thema="", feste=None):
self._thema, self._feste = thema, feste or []
def followVisibilityPreset(self): return bool(self._thema)
def followVisibilityPresetName(self): return self._thema
def keepLayerSet(self): return bool(self._feste)
def layers(self): return self._feste
class Knoten:
def __init__(self, layer, sichtbar): self._layer, self._sichtbar = layer, sichtbar
def layer(self): return self._layer
def isVisible(self): return self._sichtbar
class Projekt:
def mapThemeCollection(self):
return type("T", (), {"mapThemeVisibleLayers": lambda s, t: ["thema-" + t]})()
def layerTreeRoot(self):
return type("W", (), {"findLayers": lambda s: [Knoten("a", True), Knoten("b", False)]})()
self.assertEqual(sichtbare_layer_der_karte(Karte(thema="Plan"), Projekt()), ["thema-Plan"])
self.assertEqual(sichtbare_layer_der_karte(Karte(feste=["x"]), Projekt()), ["x"])
self.assertEqual(sichtbare_layer_der_karte(Karte(), Projekt()), ["a"])
class _FakeAusgabe:
"""Ersatz für basemap_legende_layout im Ablauftest."""
def __init__(self, karte):
self.karte = karte
self.geschrieben = None
def hauptkarte(self, layout):
return self.karte
def baue_symbole(self, layer, eintraege, zoom, vtz):
return {e.schluessel: f"symbol-{e.schluessel}" for e in eintraege}
def vorschau_icon(self, symbol):
return None
def schreibe_legende(self, layout, karte, eintraege, symbole, projekt, *, bereich, massstab):
self.geschrieben = {"eintraege": [e.schluessel for e in eintraege], "bereich": bereich, "massstab": massstab}
return ""
class _FakeKarte:
def scale(self):
return 5000.0
class TestAblauf(unittest.TestCase):
def setUp(self):
_leere_variablen()
self.dialog_aufrufe = []
self.meldungen = []
def tearDown(self):
_leere_variablen()
def _legende(self, dialog_ergebnis=("kreis",)):
def dialog(titel, hinweis, eintraege, vorauswahl):
self.dialog_aufrufe.append((eintraege, set(vorauswahl)))
return None if dialog_ergebnis is None else list(dialog_ergebnis)
return BasemapLegende(project=object(), katalog=KATALOG, dialog=dialog,
fortschritt_fabrik=lambda *a: None, bestaetigen=lambda *a: True,
melden=lambda titel, text: self.meldungen.append(text))
def _starte(self, legende, basemap=True, genutzt=AUSWERTEBEREICH_VERFAHRENSGEBIET, eintraege=None):
ausgabe = _FakeAusgabe(_FakeKarte())
layer = [_FakeVektorkachelLayer("gdz_basemapde_vektor", "Basemap.de - Farbe")] if basemap else []
eintraege = eintraege if eintraege is not None else bilde_eintraege(
["Decker_Kreisstr", "VegetationsF_Wald"], KATALOG)
with patch.dict("sys.modules", {"sn_basis.modules.basemap_legende_layout": ausgabe}), \
patch("sn_basis.modules.basemap_legende.sichtbare_layer_der_karte", return_value=layer), \
patch("sn_basis.modules.basemap_legende.auswertegeometrie", return_value=("geometrie", genutzt)), \
patch("sn_basis.modules.basemap_legende.zoomstufen", return_value=(15, 16, 15.8)), \
patch.object(BasemapLegende, "ermittle_eintraege", return_value=eintraege):
import sn_basis.modules as paket
with patch.object(paket, "basemap_legende_layout", ausgabe, create=True):
ergebnis = legende.aktualisiere(layout="layout")
return ergebnis, ausgabe
def test_ohne_basemap_layer_passiert_nichts(self):
"""Prüft, dass ohne sichtbaren Basemap-Layer weder Dialog noch Legende entstehen."""
ergebnis, ausgabe = self._starte(self._legende(), basemap=False)
self.assertEqual(ergebnis["status"], "kein_layer")
self.assertEqual(self.dialog_aufrufe, [])
self.assertIsNone(ausgabe.geschrieben)
def test_gewaehlte_eintraege_werden_geschrieben_und_gemerkt(self):
"""Prüft, dass nur die im Dialog gewählten Einträge in die Legende geschrieben und die Auswahl gespeichert wird."""
ergebnis, ausgabe = self._starte(self._legende(dialog_ergebnis=("kreis",)))
self.assertEqual(ergebnis["status"], "ok")
self.assertEqual(ausgabe.geschrieben["eintraege"], ["kreis"])
self.assertEqual(ausgabe.geschrieben["massstab"], 5000.0)
eintraege, vorbelegt = self.dialog_aufrufe[0]
self.assertEqual([e["gruppe"] for e in eintraege], ["Verkehr", "Vegetation"])
self.assertEqual(vorbelegt, {"kreis", "wald"})
self.assertEqual(json.loads(get_variable(AUSWAHL_VAR, scope="project"))["ausgeblendet"], ["wald"])
def test_abbruch_im_dialog_aendert_nichts(self):
"""Prüft, dass ein Abbruch im Auswahldialog die Legende nicht verändert und keine Auswahl speichert."""
ergebnis, ausgabe = self._starte(self._legende(dialog_ergebnis=None))
self.assertEqual(ergebnis["status"], "abgebrochen")
self.assertIsNone(ausgabe.geschrieben)
self.assertFalse(get_variable(AUSWAHL_VAR, scope="project"))
def test_rueckfall_auf_druckbereich_wird_gemeldet(self):
"""Prüft, dass ohne Verfahrensgebiet auf den Druckbereich zurückgefallen und das gemeldet wird."""
ergebnis, ausgabe = self._starte(self._legende(), genutzt=AUSWERTEBEREICH_DRUCKBEREICH)
self.assertEqual(ergebnis["status"], "ok")
self.assertEqual(ausgabe.geschrieben["bereich"], AUSWERTEBEREICH_DRUCKBEREICH)
self.assertTrue(any("Druckbereich" in m for m in self.meldungen))
def test_ohne_objekte_keine_legende(self):
"""Prüft, dass ohne Basemap-Objekte im Auswertebereich weder Dialog noch Legende entstehen."""
ergebnis, ausgabe = self._starte(self._legende(), eintraege=[])
self.assertEqual(ergebnis["status"], "keine_eintraege")
self.assertEqual(self.dialog_aufrufe, [])
self.assertIsNone(ausgabe.geschrieben)
def test_viele_kacheln_werden_vorher_bestaetigt(self):
"""Prüft, dass vor einer Auswertung mit sehr vielen Kacheln nachgefragt wird und eine Ablehnung die Auswertung abbricht."""
legende = BasemapLegende(project=object(), katalog=KATALOG, dialog=lambda *a: [],
fortschritt_fabrik=lambda *a: self.fail("keine Auswertung erwartet"),
bestaetigen=lambda *a: False, melden=lambda *a: None)
with patch("sn_basis.modules.basemap_legende.anzahl_kacheln", return_value=bl.KACHELN_NACHFRAGEN_AB):
self.assertIsNone(legende.ermittle_eintraege(object(), object(), 5000.0))
if __name__ == "__main__":
unittest.main()