Files
Plugin_SN_Basis/tests/test_basemap_kacheln.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

187 lines
6.9 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_kacheln.py – kachelweise Auswertung des Basemap-Layers (Basis #88).
Layer, Kachelmatrix und Geometrie sind Fakes; ``selectByGeometry`` liefert je Kachel vorbereitete
Objekte. Die echte Auswertung gegen basemap.de ist im QGIS-Test geprüft.
"""
import unittest
from unittest.mock import patch
from sn_basis.modules.basemap_kacheln import anzahl_kacheln, sammle_objekte, stile_aus_layer, zoomstufen
class _Rechteck:
def __init__(self, x0, y0, x1, y1):
self.werte = (x0, y0, x1, y1)
def xMinimum(self): return self.werte[0]
def yMinimum(self): return self.werte[1]
def xMaximum(self): return self.werte[2]
def yMaximum(self): return self.werte[3]
def width(self): return self.werte[2] - self.werte[0]
class _Bereich:
def __init__(self, spalten, zeilen):
self._spalten, self._zeilen = spalten, zeilen
def startColumn(self): return self._spalten[0]
def endColumn(self): return self._spalten[1]
def startRow(self): return self._zeilen[0]
def endRow(self): return self._zeilen[1]
class _Matrix:
def __init__(self, bereich):
self._bereich = bereich
def tileRangeFromExtent(self, ausdehnung):
return self._bereich
def tileExtent(self, kachel):
return _Rechteck(kachel.column() * 10, kachel.row() * 10, kachel.column() * 10 + 10, kachel.row() * 10 + 10)
class _MatrixSet:
def __init__(self, bereich):
self._bereich = bereich
self.gefragt = []
def scaleToZoomLevel(self, massstab, begrenzen=True):
self.gefragt.append((massstab, begrenzen))
return 15 if begrenzen else 16
def scaleToZoom(self, massstab):
return 15.8
def tileMatrix(self, zoom):
return _Matrix(self._bereich)
class _Geometrie:
def __init__(self, leer=False):
self._leer = leer
def boundingBox(self):
return _Rechteck(0, 0, 20, 10)
def intersection(self, andere):
return _Geometrie(self._leer)
def isEmpty(self):
return self._leer
class _Feld:
def __init__(self, name): self._name = name
def name(self): return self._name
class _Merkmal:
def __init__(self, layer, geometrietyp, **attribute):
self._werte = dict(attribute, tile_layer=layer, tile_zoom=15)
self._typ = geometrietyp
def fields(self): return [_Feld(n) for n in self._werte]
def __getitem__(self, name): return self._werte[name]
def geometry(self): return type("G", (), {"type": lambda s: self._typ})()
class _Layer:
def __init__(self, bereich, merkmale_je_aufruf):
self.matrix = _MatrixSet(bereich)
self._merkmale = list(merkmale_je_aufruf)
self._aktuell = []
self.auswahl_aufgehoben = 0
self.aufrufe = 0
def tileMatrixSet(self): return self.matrix
def selectByGeometry(self, geometrie, kontext, verhalten, beziehung):
self.aufrufe += 1
self._aktuell = self._merkmale.pop(0) if self._merkmale else []
def selectedFeatures(self): return self._aktuell
def removeSelection(self): self.auswahl_aufgehoben += 1
class _GeometrieFabrik:
"""Ersatz für QgsGeometry.fromRect (fehlt im Mock-Modus)."""
@staticmethod
def fromRect(rechteck):
return rechteck
@patch("sn_basis.modules.basemap_kacheln.QgsGeometry", _GeometrieFabrik)
class TestSammleObjekte(unittest.TestCase):
def test_gleiche_objekte_aus_mehreren_kacheln_werden_entdoppelt(self):
"""Prüft, dass Objekte mit gleichem Quell-Layer, Geometrietyp und Attributen über Kachelgrenzen hinweg nur einmal gezählt werden."""
strasse = _Merkmal("Verkehrslinie", 1, klasse="Kreisstraße")
layer = _Layer(_Bereich((0, 1), (0, 0)), [
[strasse, _Merkmal("Vegetationsflaeche", 2, klasse="Wald")],
[_Merkmal("Verkehrslinie", 1, klasse="Kreisstraße")],
])
objekte = sammle_objekte(layer, _Geometrie(), 5000.0)
self.assertEqual(layer.aufrufe, 2)
self.assertEqual(sorted(o.layer_name for o in objekte), ["Vegetationsflaeche", "Verkehrslinie"])
kreis = next(o for o in objekte if o.layer_name == "Verkehrslinie")
self.assertEqual(kreis.attribute, {"klasse": "Kreisstraße"})
self.assertIs(kreis.merkmal, strasse)
self.assertEqual(layer.auswahl_aufgehoben, 1)
def test_kacheln_ausserhalb_der_geometrie_werden_uebersprungen(self):
"""Prüft, dass Kacheln ohne Schnitt mit dem Auswertebereich nicht abgefragt werden."""
layer = _Layer(_Bereich((0, 2), (0, 1)), [])
self.assertEqual(sammle_objekte(layer, _Geometrie(leer=True), 5000.0), [])
self.assertEqual(layer.aufrufe, 0)
def test_abbruch_liefert_none_und_hebt_auswahl_auf(self):
"""Prüft, dass ein Abbruch über den Fortschritt None liefert und die Auswahl auf dem Basemap-Layer trotzdem aufgehoben wird."""
layer = _Layer(_Bereich((0, 3), (0, 0)), [[], [], [], []])
fortschritt = []
def melde(nummer, gesamt):
fortschritt.append((nummer, gesamt))
return nummer < 2
self.assertIsNone(sammle_objekte(layer, _Geometrie(), 5000.0, fortschritt=melde))
self.assertEqual(fortschritt, [(1, 4), (2, 4)])
self.assertEqual(layer.aufrufe, 1)
self.assertEqual(layer.auswahl_aufgehoben, 1)
def test_anzahl_kacheln_aus_kachelbereich(self):
"""Prüft, dass die Kachelzahl aus dem Kachelbereich der Geometrie berechnet wird."""
self.assertEqual(anzahl_kacheln(_Layer(_Bereich((3, 5), (7, 10)), []), _Geometrie(), 5000.0), 12)
class TestZoomUndStile(unittest.TestCase):
def test_zoomstufen_wie_im_qgis_renderer(self):
"""Prüft, dass Kachelzoom begrenzt, Darstellungszoom unbegrenzt und der gebrochene Zoom aus dem Maßstab bestimmt werden."""
layer = _Layer(_Bereich((0, 0), (0, 0)), [])
self.assertEqual(zoomstufen(layer, 5000.0), (15, 16, 15.8))
self.assertEqual(layer.matrix.gefragt, [(5000.0, True), (5000.0, False)])
def test_stile_aus_layer_liest_renderer_styles(self):
"""Prüft, dass Name, Quell-Layer, Geometrietyp, Zoombereich, Schalter und Filter der Renderer-Styles übernommen werden."""
class Stil:
def styleName(self): return "Decker_Kreisstr"
def layerName(self): return "Verkehrslinie"
def geometryType(self): return 1
def minZoomLevel(self): return 9
def maxZoomLevel(self): return 21
def isEnabled(self): return False
def filterExpression(self): return "\"klasse\" IS 'Kreisstraße'"
layer = type("L", (), {"renderer": lambda s: type("R", (), {"styles": lambda s: [Stil()]})()})()
stil = stile_aus_layer(layer)[0]
self.assertEqual((stil.name, stil.layer_name, stil.geometrie, stil.min_zoom, stil.max_zoom, stil.aktiv),
("Decker_Kreisstr", "Verkehrslinie", 1, 9, 21, False))
self.assertIn("Kreisstraße", stil.filter)
if __name__ == "__main__":
unittest.main()