193 lines
6.7 KiB
Python
193 lines
6.7 KiB
Python
"""
|
||||
|
|
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)
|