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