Files
Plugin_SN_Basis/modules/basemap_kacheln.py
T
DanielandClaude Opus 5.5 b549cb8a5a Basemap-Legende: Auswertung und Legendenelement im Drucklayout (#88)
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>
2026-10-02 11:57:09 +02:00

193 lines
6.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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)