Files
Plugin_SN_Basis/modules/basemap_kacheln.py
T

193 lines
6.7 KiB
Python
Raw Normal View History

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