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