""" 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