Files
Plugin_SN_Basis/modules/print_kartenrand.py
T

190 lines
8.0 KiB
Python
Raw Normal View History

2026-10-02 13:06:35 +02:00
"""
sn_basis/modules/print_kartenrand.py – Kartenrand neben der Karte überlappungsfrei anordnen (Basis #93)
Im Standardlayout (§ 38, § 41) stehen rechts neben der Karte Hauptlegende, Basemap-Legende,
Quellenangabe und Kartenschild. Bei kleinen Verfahren oder großem Maßstab ist die Karte
niedriger als diese Elemente zusammen, und sie überlappten sich. ``ordne_kartenrand_an``
läuft nach dem Aufbau des Layouts:
1. Die Legenden werden für 1, 2 und 3 Spalten vermessen. Die Hauptlegende filtert nach
Karteninhalt; ihre Höhe steht erst nach einmaligem Zeichnen fest, deshalb wird der
Legendenbereich vorher einmal gerendert.
2. Gewählt wird die kleinste Spaltenzahl, bei der alles neben die Karte passt; der Kartenrand
wird dafür so weit verbreitert, wie die Legenden es brauchen.
3. Reicht es auch dreispaltig nicht, wird die Seite höher; Quellenangabe und Kartenschild
rutschen nach unten.
Größe und Lage der Karte bleiben unverändert.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Callable
from sn_basis.functions.qgiscore_wrapper import QgsLayoutExporter, QgsLayoutPoint, QgsLayoutSize, QgsUnitTypes
from sn_basis.functions.qt_wrapper import QSize
MM = QgsUnitTypes.LayoutMillimeters
RAND_MM = 10.0 # Seitenrand und Abstand Karte → Kartenrand
MIN_SPALTENBREITE_MM = 180.0 # Breite von Kartenschild und Quellenangabe
ABSTAND_LEGENDEN_MM = 4.0 # unter jeder Legende
QUELLE_HOEHE_MM = 20.0
ABSTAND_QUELLE_SCHILD_MM = 10.0
SCHILD_HOEHE_MM = 92.0
MAX_SPALTEN = 3
VERMESSUNG_DPI = 25 # Auflösung des einmaligen Zeichnens zum Vermessen
HAUPTKARTE_ID = "Hauptkarte"
LEGENDEN_IDS = ("Legende", "Legende Basemap") # von oben nach unten
BASEMAP_LEGENDE_ID = "Legende Basemap"
QUELLENANGABE_ID = "Quellenangabe"
# Elemente unterhalb der Legenden, die bei höherer Seite gemeinsam nach unten rutschen
UNTERE_ELEMENTE = (
QUELLENANGABE_ID, "Kartenschild",
"Kartenschild_Linie_1", "Kartenschild_Linie_2", "Kartenschild_Linie_3",
"Kartenschild_Linie_4", "Kartenschild_Linie_5", "Kartenschild_Linie_6",
"Herausgeber", "Bezeichnung_Flurbereinigung", "Name_Flurbereinigung", "Stadt_Gemeinde", "Landkreis",
"Kartenname", "Gefertigt", "Geprüft", "Massstab", "Ausgabedatum", "Aktenzeichen",
)
@dataclass
class Anordnung:
spalten: int
spaltenbreite: float
seite_breite: float
seite_hoehe: float
verschiebung_y: float # um so viel rutschen Quellenangabe und Kartenschild nach unten
def berechne_anordnung(
karte: tuple[float, float, float, float],
seite: tuple[float, float],
legenden_je_spalten: dict[int, list[tuple[float, float]]],
) -> Anordnung:
"""Berechnet Spaltenzahl, Seitengröße und Verschiebung des Kartenschilds.
Parameters
----------
karte : (links, oben, Breite, Höhe) der Karte in mm
seite : (Breite, Höhe) der Seite in mm
legenden_je_spalten : je Spaltenzahl 1–3 die Größen (Breite, Höhe) aller Legenden, von oben nach unten
"""
links, oben, breite, hoehe = karte
spalte_links = links + breite + RAND_MM
fest = QUELLE_HOEHE_MM + ABSTAND_QUELLE_SCHILD_MM + SCHILD_HOEHE_MM
def bedarf(spalten: int) -> tuple[float, float]:
groessen = legenden_je_spalten.get(spalten, [])
benoetigt = sum(h + ABSTAND_LEGENDEN_MM for _, h in groessen) + fest
spaltenbreite = max([MIN_SPALTENBREITE_MM] + [b for b, _ in groessen])
return benoetigt, spaltenbreite
# Kleinste Spaltenzahl, die neben die Karte passt; passt keine, die mit dem geringsten Platzbedarf
# (bei Gleichstand weniger Spalten – mehr Spalten ohne Gewinn verbreitern nur den Rand).
kandidaten = [(n, *bedarf(n)) for n in range(1, MAX_SPALTEN + 1)]
passend = [k for k in kandidaten if k[1] <= hoehe]
spalten, benoetigt, spaltenbreite = passend[0] if passend else min(kandidaten, key=lambda k: (k[1], k[0]))
verschiebung = max(0.0, benoetigt - hoehe)
return Anordnung(
spalten=spalten,
spaltenbreite=spaltenbreite,
seite_breite=max(seite[0], spalte_links + spaltenbreite + RAND_MM),
seite_hoehe=max(seite[1], oben + benoetigt + RAND_MM) if verschiebung else seite[1],
verschiebung_y=verschiebung,
)
# ---------------------------------------------------------
# Anwendung auf ein Layout
# ---------------------------------------------------------
def _aufruf(objekt: Any, methode: str, *args: Any, standard: Any = None) -> Any:
funktion = getattr(objekt, methode, None)
if not callable(funktion):
return standard
try:
return funktion(*args)
except Exception:
return standard
def _wert(objekt: Any, name: str) -> float:
wert = getattr(objekt, name)
return float(wert() if callable(wert) else wert)
def _rechteck(element: Any) -> tuple[float, float, float, float]:
r = element.sceneBoundingRect()
return _wert(r, "x"), _wert(r, "y"), _wert(r, "width"), _wert(r, "height")
def _legende_groesse(legende: Any, spalten: int) -> tuple[float, float]:
_aufruf(legende, "setColumnCount", spalten)
_aufruf(legende, "setSplitLayer", True)
_aufruf(legende, "setEqualColumnWidth", True)
_aufruf(legende, "adjustBoxSize")
groesse = legende.sizeWithUnits()
return _wert(groesse, "width"), _wert(groesse, "height")
def _rendere_legenden(layout: Any, legenden: list[Any]) -> None:
"""Zeichnet die Legenden einmal mit geringer Auflösung, damit Kartenfilter und Größe feststehen."""
exporter = QgsLayoutExporter(layout)
for legende in legenden:
_aufruf(exporter, "renderRegionToImage", legende.sceneBoundingRect(), QSize(), VERMESSUNG_DPI)
def _verschiebe(element: Any, dy: float) -> None:
position = element.positionWithUnits()
element.attemptMove(QgsLayoutPoint(_wert(position, "x"), _wert(position, "y") + dy, MM))
def ordne_kartenrand_an(layout: Any, rendern: Callable[[Any, list], None] | None = None) -> Anordnung | None:
"""Ordnet den Kartenrand eines Standardlayouts überlappungsfrei an (Seite ggf. breiter/höher).
Returns
-------
Anordnung | None
Die angewandte Anordnung oder ``None``, wenn das Layout keine Hauptkarte hat.
"""
karte = _aufruf(layout, "itemById", HAUPTKARTE_ID)
if karte is None:
return None
legenden = [l for l in (_aufruf(layout, "itemById", i) for i in LEGENDEN_IDS) if l is not None]
for legende in legenden:
_aufruf(legende, "setLocked", False)
(rendern or _rendere_legenden)(layout, legenden)
groessen = {n: [_legende_groesse(l, n) for l in legenden] for n in range(1, MAX_SPALTEN + 1)}
seite = layout.pageCollection().page(0)
seitengroesse = seite.pageSize()
anordnung = berechne_anordnung(_rechteck(karte), (_wert(seitengroesse, "width"), _wert(seitengroesse, "height")),
groessen)
for legende in legenden:
_legende_groesse(legende, anordnung.spalten)
if (anordnung.seite_breite, anordnung.seite_hoehe) != (_wert(seitengroesse, "width"), _wert(seitengroesse, "height")):
seite.setPageSize(QgsLayoutSize(anordnung.seite_breite, anordnung.seite_hoehe, MM))
if anordnung.verschiebung_y > 0:
quelle = _aufruf(layout, "itemById", QUELLENANGABE_ID)
quelle_oben = _rechteck(quelle)[1] if quelle is not None else None
for element_id in UNTERE_ELEMENTE:
element = _aufruf(layout, "itemById", element_id)
if element is not None:
_verschiebe(element, anordnung.verschiebung_y)
basemap = _aufruf(layout, "itemById", BASEMAP_LEGENDE_ID)
if basemap is not None and quelle_oben is not None:
# unten verankert (siehe basemap_legende_layout.platziere): direkt über die verschobene Quellenangabe
position = basemap.positionWithUnits()
basemap.attemptMove(QgsLayoutPoint(_wert(position, "x"),
quelle_oben + anordnung.verschiebung_y - ABSTAND_LEGENDEN_MM, MM))
for legende in legenden:
_aufruf(legende, "setLocked", True)
return anordnung