Standard- und Mindestbreite der Haupt- und Basemap-Legende ist die Breite des Kartenschilds (180 mm, MIN_SPALTENBREITE_MM) – die zunächst genannten 210 mm sind der gesamte Kartenrand einschließlich Abständen. Die Legenden passen sich weiter ihrem Inhalt an, werden aber nie schmaler (setMinimumSize). QGIS speichert die Mindestgröße nicht im Projekt; nach dem Neuladen schrumpfte die Legende bei der nächsten Größenanpassung wieder (QGIS-Test: 43,7 mm). Deshalb vermerkt das Layout die Mindestbreite (sn_legende_mindestbreite), und ein Listener auf readProject setzt sie beim Laden wieder (setup/teardown_mindestbreiten_listener in main.py, wie der Verfahrensgebiet-Listener). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
285 lines
11 KiB
Python
285 lines
11 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 (
|
||
QGIS_AVAILABLE,
|
||
QgsLayoutExporter,
|
||
QgsLayoutPoint,
|
||
QgsLayoutSize,
|
||
QgsProject,
|
||
QgsUnitTypes,
|
||
)
|
||
from sn_basis.functions.qt_wrapper import QSize
|
||
|
||
MM = QgsUnitTypes.LayoutMillimeters
|
||
|
||
RAND_MM = 10.0 # Seitenrand und Abstand Karte → Kartenrand
|
||
# Breite von Kartenschild und Quellenangabe (PrintLayout._LEGEND_WIDTH_MM) – zugleich Standard- und
|
||
# Mindestbreite der Legenden, damit sie optisch mit dem Kartenschild übereinstimmen.
|
||
MIN_SPALTENBREITE_MM = 180.0
|
||
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
|
||
# Legendentext bricht nach dieser Breite um, Spalten sind ungleich breit – sonst bestimmt der längste
|
||
# Text die Breite aller Spalten (dreispaltige Basemap-Legende 431 statt 226 mm, Basis #96).
|
||
TEXTUMBRUCH_MM = 60.0
|
||
|
||
HAUPTKARTE_ID = "Hauptkarte"
|
||
LEGENDEN_IDS = ("Legende", "Legende Basemap") # von oben nach unten
|
||
BASEMAP_LEGENDE_ID = "Legende Basemap"
|
||
# Layout-Eigenschaft: Mindestbreite der Legenden. QGIS speichert setMinimumSize nicht im Projekt;
|
||
# der Vermerk erlaubt, sie beim Laden wieder zu setzen (wende_mindestbreiten_an).
|
||
LAYOUT_MINDESTBREITE = "sn_legende_mindestbreite"
|
||
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", False)
|
||
_aufruf(legende, "setAutoWrapLinesAfter", TEXTUMBRUCH_MM)
|
||
_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 setze_legenden_mindestbreite(layout: Any, breite: float = MIN_SPALTENBREITE_MM, merken: bool = True) -> int:
|
||
"""Gibt den Legenden des Layouts eine Mindestbreite (Standard: Breite des Kartenschilds).
|
||
|
||
Die Legenden passen sich weiter ihrem Inhalt an, werden aber nie schmaler. Mit ``merken`` wird die
|
||
Breite am Layout vermerkt, damit sie nach dem Laden des Projekts wieder gesetzt werden kann.
|
||
|
||
Returns
|
||
-------
|
||
int
|
||
Anzahl der Legenden, die eine Mindestbreite bekommen haben.
|
||
"""
|
||
anzahl = 0
|
||
for legenden_id in LEGENDEN_IDS:
|
||
legende = _aufruf(layout, "itemById", legenden_id)
|
||
if legende is not None:
|
||
legende.setMinimumSize(QgsLayoutSize(breite, 0.0, MM))
|
||
anzahl += 1
|
||
if merken:
|
||
layout.setCustomProperty(LAYOUT_MINDESTBREITE, float(breite))
|
||
return anzahl
|
||
|
||
|
||
def wende_mindestbreiten_an(project: Any | None = None) -> int:
|
||
"""Setzt die vermerkten Mindestbreiten der Legenden aller Drucklayouts erneut (nach dem Laden).
|
||
|
||
Returns
|
||
-------
|
||
int
|
||
Anzahl der Layouts, an denen eine Mindestbreite gesetzt wurde.
|
||
"""
|
||
project = project or QgsProject.instance()
|
||
verwaltung = _aufruf(project, "layoutManager")
|
||
anzahl = 0
|
||
for layout in _aufruf(verwaltung, "printLayouts", standard=[]) or []:
|
||
wert = _aufruf(layout, "customProperty", LAYOUT_MINDESTBREITE, None)
|
||
try:
|
||
breite = float(wert)
|
||
except (TypeError, ValueError):
|
||
continue
|
||
if breite > 0 and setze_legenden_mindestbreite(layout, breite, merken=False):
|
||
anzahl += 1
|
||
return anzahl
|
||
|
||
|
||
_listener_aktiv = False
|
||
|
||
|
||
def _on_projekt_gelesen(*_: Any) -> None:
|
||
wende_mindestbreiten_an()
|
||
|
||
|
||
def setup_mindestbreiten_listener() -> None:
|
||
"""Setzt die Mindestbreiten bei jedem Laden eines Projekts neu (einmal beim Plugin-Start aufrufen)."""
|
||
global _listener_aktiv
|
||
if _listener_aktiv or not QGIS_AVAILABLE:
|
||
return
|
||
try:
|
||
project = QgsProject.instance()
|
||
project.readProject.connect(_on_projekt_gelesen)
|
||
_listener_aktiv = True
|
||
wende_mindestbreiten_an(project) # bereits geöffnetes Projekt
|
||
except Exception:
|
||
pass
|
||
|
||
|
||
def teardown_mindestbreiten_listener() -> None:
|
||
"""Trennt den Listener wieder (beim Entladen des Plugins; idempotent)."""
|
||
global _listener_aktiv
|
||
if not _listener_aktiv:
|
||
return
|
||
try:
|
||
QgsProject.instance().readProject.disconnect(_on_projekt_gelesen)
|
||
except Exception:
|
||
pass
|
||
finally:
|
||
_listener_aktiv = False
|
||
|
||
|
||
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)
|
||
setze_legenden_mindestbreite(layout)
|
||
(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
|