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>
187 lines
6.9 KiB
Python
187 lines
6.9 KiB
Python
"""
|
||
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()
|