Files
Plugin_SN_Basis/modules/print_kartenrand.py
T
DanielandClaude Opus 5.5 006459ab61 Kartenrand überlappungsfrei anordnen (#93)
Im Standardlayout hingen Hauptlegende (oben), Quellenangabe und Kartenschild (unten) an der
Kartenhöhe; bei kleinen Verfahren waren sie zusammen höher als die Karte und überlappten.
Neues Modul print_kartenrand: Nach dem Aufbau der Vorlage (inkl. Basemap-Legende) werden die
Legenden einmal gezeichnet – die Hauptlegende filtert nach Karteninhalt, ihre Höhe steht erst
danach fest – und für 1–3 Spalten vermessen. Gewählt wird die kleinste passende Spaltenzahl
(Kartenrand wird dafür breiter); passt keine, die mit dem geringsten Platzbedarf, und die Seite
wird höher, Quellenangabe und Kartenschild rutschen nach unten. Karte bleibt in Größe und Lage.
Fehler bei der Anordnung verhindern die Vorlage nicht (Warnung). Wrapper: QgsLayoutExporter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 13:06:35 +02:00

190 lines
8.0 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/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