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>
328 lines
16 KiB
Python
328 lines
16 KiB
Python
"""
|
||
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()
|