Files
Plugin_SN_Basis/tests/test_basemap_legende.py
T

328 lines
16 KiB
Python
Raw Normal View History

"""
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()