""" sn_basis/modules/basemap_kacheln.py – Kacheln des Basemap-Layers auswerten Liest die Renderer-Styles eines Vektorkachel-Layers, rechnet Maßstab in Zoomstufen um wie der QGIS-Renderer und sammelt die Objekte eines Auswertebereichs kachelweise über ``QgsVectorTileLayer.selectByGeometry``. Teil der Basemap-Legende (``basemap_legende``). """ from __future__ import annotations from dataclasses import dataclass from typing import Any, Callable from sn_basis.functions.qgiscore_wrapper import ( QgsExpression, QgsExpressionContext, QgsExpressionContextScope, QgsGeometry, QgsRectangle, QgsSelectionContext, QgsTileXYZ, SELECT_BEHAVIOR_SET, SELECT_RELATION_INTERSECT, ) _KACHELFELDER = ("tile_zoom", "tile_layer") def _aufruf(objekt: Any, methode: str, standard: Any = None) -> Any: funktion = getattr(objekt, methode, None) if not callable(funktion): return standard try: return funktion() except Exception: return standard @dataclass(frozen=True) class StilInfo: """Renderer-Style des Basemap-Layers in QGIS-unabhängiger Form.""" name: str layer_name: str geometrie: int # 0 Punkt, 1 Linie, 2 Fläche min_zoom: int = -1 max_zoom: int = -1 aktiv: bool = True filter: str = "" @dataclass class ObjektInfo: """Ein (entdoppeltes) Objekt aus den Kacheln des Auswertebereichs.""" layer_name: str geometrie: int attribute: dict merkmal: Any = None # QgsFeature für die Auswertung der Filterausdrücke in QGIS def _enum_int(wert: Any) -> int: try: return int(wert) except (TypeError, ValueError): return int(getattr(wert, "value", -1)) def stile_aus_layer(layer: Any) -> list[StilInfo]: """Liest die Renderer-Styles eines Vektorkachel-Layers.""" renderer = _aufruf(layer, "renderer") stile = _aufruf(renderer, "styles", []) or [] return [ StilInfo( name=s.styleName(), layer_name=s.layerName(), geometrie=_enum_int(s.geometryType()), min_zoom=s.minZoomLevel(), max_zoom=s.maxZoomLevel(), aktiv=s.isEnabled(), filter=s.filterExpression(), ) for s in stile ] def zoomstufen(layer: Any, massstab: float) -> tuple[int, int, float]: """Kachel-, Darstellungs- und gebrochene Zoomstufe wie im QGIS-Vektorkachel-Renderer. ``zoom_level`` = ``scaleToZoomLevel(maßstab, False)`` (nicht auf die Kachelstufen begrenzt), ``vector_tile_zoom`` = ``scaleToZoom(maßstab)``. Für Karten in EPSG:25833 entspricht der Kachelmaßstab dem Kartenmaßstab (geprüft mit ``calculateTileScaleForMap``, QGIS 3.44). """ matrix = layer.tileMatrixSet() return ( matrix.scaleToZoomLevel(massstab), matrix.scaleToZoomLevel(massstab, False), matrix.scaleToZoom(massstab), ) def qgis_filterpruefer(vector_tile_zoom: float, zoom_level: int) -> Callable[[str, ObjektInfo], bool]: """Liefert eine Filterprüfung, die QGIS-Ausdrücke wie der Renderer auswertet.""" kontext = QgsExpressionContext() scope = QgsExpressionContextScope() scope.setVariable("vector_tile_zoom", vector_tile_zoom) scope.setVariable("zoom_level", zoom_level) kontext.appendScope(scope) ausdruecke: dict[str, Any] = {} def pruefe(filter_text: str, objekt: ObjektInfo) -> bool: ausdruck = ausdruecke.get(filter_text) if ausdruck is None: ausdruck = QgsExpression(filter_text) ausdruck.prepare(kontext) ausdruecke[filter_text] = ausdruck kontext.setFeature(objekt.merkmal) return bool(ausdruck.evaluate(kontext)) return pruefe def _objekt_schluessel(merkmal: Any) -> tuple: werte = [] for feld in merkmal.fields(): name = feld.name() if name not in _KACHELFELDER: werte.append((name, str(merkmal[name]))) return merkmal["tile_layer"], _enum_int(merkmal.geometry().type()), tuple(sorted(werte)) def sammle_objekte( layer: Any, geometrie: Any, massstab: float, *, fortschritt: Callable[[int, int], bool] | None = None, ) -> list[ObjektInfo] | None: """Wertet die Kacheln des Basemap-Layers im Auswertebereich aus. ``geometrie`` liegt im KBS des Layers (EPSG:3857). Ausgewertet wird kachelweise über ``selectByGeometry``, damit Fortschritt und Abbruch möglich sind. Gleiche Objekte (gleicher Quell-Layer, Geometrietyp und Attribute) werden entdoppelt. Eine vorhandene Auswahl auf dem Basemap-Layer wird dabei aufgehoben. Returns ------- list[ObjektInfo] | None ``None``, wenn der Nutzer abgebrochen hat. """ matrix_set = layer.tileMatrixSet() zoom = matrix_set.scaleToZoomLevel(massstab) matrix = matrix_set.tileMatrix(zoom) bereich = matrix.tileRangeFromExtent(geometrie.boundingBox()) kacheln = [ (spalte, zeile) for spalte in range(bereich.startColumn(), bereich.endColumn() + 1) for zeile in range(bereich.startRow(), bereich.endRow() + 1) ] objekte: dict[tuple, ObjektInfo] = {} try: for nummer, (spalte, zeile) in enumerate(kacheln, start=1): if fortschritt is not None and not fortschritt(nummer, len(kacheln)): return None rechteck = matrix.tileExtent(QgsTileXYZ(spalte, zeile, zoom)) rand = rechteck.width() * 1e-6 innen = QgsRectangle(rechteck.xMinimum() + rand, rechteck.yMinimum() + rand, rechteck.xMaximum() - rand, rechteck.yMaximum() - rand) teil = geometrie.intersection(QgsGeometry.fromRect(innen)) if teil is None or teil.isEmpty(): continue kontext = QgsSelectionContext() kontext.setScale(massstab) layer.selectByGeometry(teil, kontext, SELECT_BEHAVIOR_SET, SELECT_RELATION_INTERSECT) for merkmal in layer.selectedFeatures(): schluessel = _objekt_schluessel(merkmal) if schluessel not in objekte: objekte[schluessel] = ObjektInfo( layer_name=schluessel[0], geometrie=schluessel[1], attribute=dict(schluessel[2]), merkmal=merkmal, ) finally: _aufruf(layer, "removeSelection") return list(objekte.values()) def anzahl_kacheln(layer: Any, geometrie: Any, massstab: float) -> int: matrix_set = layer.tileMatrixSet() zoom = matrix_set.scaleToZoomLevel(massstab) bereich = matrix_set.tileMatrix(zoom).tileRangeFromExtent(geometrie.boundingBox()) return max(0, bereich.endColumn() - bereich.startColumn() + 1) * max(0, bereich.endRow() - bereich.startRow() + 1)