190 lines
8.0 KiB
Python
190 lines
8.0 KiB
Python
"""
|
||||
|
|
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
|