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>
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
- Bugfix: Bei kleinen Verfahren oder großem Maßstab überlappten sich Legenden, Quellenangabe und Kartenschild, weil sie zusammen höher als die Karte waren. Beim Anlegen einer §38-/§41-Vorlage werden die Legenden jetzt bei Bedarf zwei- oder dreispaltig gesetzt (Kartenrand wird dafür breiter); reicht auch das nicht, wird die Seite höher und Quellenangabe und Kartenschild rutschen nach unten. Größe und Lage der Karte bleiben gleich (#93)
|
||||
- Bugfix: Weiße Basemap-Flächen ohne Kontur (z. B. „Platz“) waren in der Basemap-Legende auf weißem Grund unsichtbar; ihr Legendensymbol bekommt jetzt einen dünnen grauen Rand (0,15 mm), die Karte bleibt unverändert (#92)
|
||||
- Bugfix: Punktlinien des Basemap-Layers (Baumreihe, Hecke, Durchlass, Seilbahnen u. a., 12 Styles) wurden in Karte und Legende nicht gezeichnet – basemap.de nutzt Strichlänge 0 mit runder Kappe, Qt zeichnet Striche der Länge 0 nicht. Neuer Knopf „Punktlinien korrigieren“ im Basemap-Bereich des Druck-Tabs ersetzt die Strichlänge 0 durch 0,01 (festes Muster und zoomabhängiger Ausdruck); die Korrektur lässt sich als Stilvorlage sichern (#90)
|
||||
- Basemap-Legende für Drucklayouts (#88): Beim Erstellen einer §38-/§41-Vorlage wird ausgewertet, welche Objekte des basemap.de-Vektorkachel-Layers im Verfahrensgebiet (wahlweise im Druckbereich) tatsächlich dargestellt werden; ein Auswahldialog lässt Kategorien abwählen, die gewählten erscheinen mit der Signatur des Projektlayers in einem eigenen Legendenelement „Kartengrundlage (basemap.de)“ über der Quellenangabe. Zuordnung von 434 Basemap-Styles zu 190 Legendeneinträgen (Beschriftungen, Nummernschilder und Hintergrund ausgenommen) in `data/basemap_legende.json` (erzeugt mit `modules/basemap_katalog.py`, fachlich nachbearbeitet)
|
||||
|
||||
@@ -894,6 +894,7 @@ QgsSymbolLayerUtils: Type[Any]
|
||||
QgsLayoutMeasurement: Type[Any]
|
||||
QgsLayoutUtils: Type[Any]
|
||||
QgsSimpleLineSymbolLayer: Type[Any]
|
||||
QgsLayoutExporter: Type[Any]
|
||||
SELECT_BEHAVIOR_SET: Any = None
|
||||
SELECT_RELATION_INTERSECT: Any = None
|
||||
PROPERTY_TYPE_EXPRESSION: Any = None
|
||||
@@ -923,6 +924,7 @@ try:
|
||||
QgsLayoutUtils as _QgsLayoutUtils,
|
||||
QgsSymbolLayer as _QgsSymbolLayer,
|
||||
QgsSimpleLineSymbolLayer as _QgsSimpleLineSymbolLayer,
|
||||
QgsLayoutExporter as _QgsLayoutExporter,
|
||||
Qgis as _QgisVT,
|
||||
)
|
||||
|
||||
@@ -943,6 +945,7 @@ try:
|
||||
QgsLayoutMeasurement = _QgsLayoutMeasurement
|
||||
QgsLayoutUtils = _QgsLayoutUtils
|
||||
QgsSimpleLineSymbolLayer = _QgsSimpleLineSymbolLayer
|
||||
QgsLayoutExporter = _QgsLayoutExporter
|
||||
|
||||
SELECT_BEHAVIOR_SET = _QgisVT.SelectBehavior.SetSelection
|
||||
SELECT_RELATION_INTERSECT = _QgisVT.SelectGeometryRelationship.Intersect
|
||||
@@ -1128,6 +1131,15 @@ except Exception:
|
||||
|
||||
QgsSimpleLineSymbolLayer = _MockQgsSimpleLineSymbolLayer
|
||||
|
||||
class _MockQgsLayoutExporter:
|
||||
def __init__(self, layout: Any = None):
|
||||
self._layout = layout
|
||||
|
||||
def renderRegionToImage(self, region: Any, size: Any = None, dpi: float = -1) -> Any:
|
||||
return None
|
||||
|
||||
QgsLayoutExporter = _MockQgsLayoutExporter
|
||||
|
||||
# QgsRectangle hatte im allgemeinen Mock-Block keinen Ersatz (nur Typannotation).
|
||||
if "QgsRectangle" not in globals():
|
||||
class _MockQgsRectangle:
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
"""
|
||||
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
|
||||
@@ -1086,11 +1086,27 @@ class PrintLayout:
|
||||
if basemap_legende:
|
||||
self._erzeuge_basemap_legende(layout)
|
||||
|
||||
self._ordne_kartenrand_an(layout)
|
||||
|
||||
layout_manager.addLayout(layout)
|
||||
self._current_layout = None
|
||||
open_layout_designer(layout)
|
||||
return layout
|
||||
|
||||
def _ordne_kartenrand_an(self, layout: Any) -> None:
|
||||
"""Ordnet Legenden, Quellenangabe und Kartenschild überlappungsfrei an (Basis #93).
|
||||
|
||||
Rand ggf. breiter (bis zu drei Legendenspalten) und Seite ggf. höher; die Karte bleibt.
|
||||
Ein Fehler dabei darf das Anlegen der Vorlage nicht verhindern; er wird als Warnung gemeldet.
|
||||
"""
|
||||
from sn_basis.functions.message_wrapper import warning
|
||||
from sn_basis.modules.print_kartenrand import ordne_kartenrand_an
|
||||
|
||||
try:
|
||||
ordne_kartenrand_an(layout)
|
||||
except Exception as fehler:
|
||||
warning("Vorlage anlegen", f"Der Kartenrand konnte nicht angeordnet werden: {fehler}")
|
||||
|
||||
def _erzeuge_basemap_legende(self, layout: Any) -> None:
|
||||
"""Kennzeichnet das Layout und erzeugt die Basemap-Legende (Basis #88).
|
||||
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
"""
|
||||
Tests für modules/print_kartenrand.py – Anordnung des Kartenrands neben der Karte (Basis #93).
|
||||
|
||||
Bei kleinen Verfahren oder großem Maßstab ist die Karte niedriger als Legenden, Quellenangabe
|
||||
und Kartenschild zusammen; die Elemente überlappten sich. Die Anordnung wählt bis zu drei
|
||||
Legendenspalten (Kartenrand wird dafür breiter) und vergrößert notfalls die Seitenhöhe. Karte
|
||||
bleibt in Größe und Lage gleich. Die Wirkung im echten Layout ist im QGIS-Test geprüft.
|
||||
"""
|
||||
import unittest
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from sn_basis.modules import print_kartenrand as kr
|
||||
from sn_basis.modules.print_layout import PrintLayout
|
||||
from sn_basis.modules.print_kartenrand import berechne_anordnung, ordne_kartenrand_an
|
||||
|
||||
KARTE = (10.0, 10.0, 360.0, 300.0) # links, oben, Breite, Höhe in mm
|
||||
SEITE = (580.0, 320.0)
|
||||
FEST = kr.QUELLE_HOEHE_MM + kr.ABSTAND_QUELLE_SCHILD_MM + kr.SCHILD_HOEHE_MM # 122 mm
|
||||
|
||||
|
||||
def _legenden(*je_spalten):
|
||||
"""Eine Legende je Argument: (w1, h1, w2, h2, w3, h3)."""
|
||||
return {n: [(l[2 * (n - 1)], l[2 * (n - 1) + 1]) for l in je_spalten] for n in (1, 2, 3)}
|
||||
|
||||
|
||||
class TestBerechneAnordnung(unittest.TestCase):
|
||||
|
||||
def test_passt_einspaltig_nichts_aendert_sich(self):
|
||||
"""Prüft, dass bei genug Platz neben der Karte einspaltig angeordnet wird und Seite und Kartenschild bleiben."""
|
||||
anordnung = berechne_anordnung(KARTE, SEITE, _legenden((80, 100, 120, 50, 160, 34)))
|
||||
self.assertEqual(anordnung.spalten, 1)
|
||||
self.assertEqual((anordnung.seite_breite, anordnung.seite_hoehe), SEITE)
|
||||
self.assertEqual(anordnung.verschiebung_y, 0.0)
|
||||
self.assertEqual(anordnung.spaltenbreite, kr.MIN_SPALTENBREITE_MM)
|
||||
|
||||
def test_mehr_spalten_und_breiterer_rand(self):
|
||||
"""Prüft, dass bei zu hohen Legenden die kleinste passende Spaltenzahl gewählt und der Kartenrand dafür verbreitert wird."""
|
||||
# Hauptlegende + Basemap-Legende; einspaltig 250 mm, zweispaltig 140 mm hoch – passt in 300 mm Karte ab 2 Spalten
|
||||
legenden = _legenden((150, 150, 300, 80, 450, 55), (170, 100, 340, 60, 510, 40))
|
||||
anordnung = berechne_anordnung(KARTE, SEITE, legenden)
|
||||
self.assertEqual(anordnung.spalten, 2)
|
||||
self.assertEqual(anordnung.spaltenbreite, 340)
|
||||
spalte_links = KARTE[0] + KARTE[2] + kr.RAND_MM
|
||||
self.assertEqual(anordnung.seite_breite, spalte_links + 340 + kr.RAND_MM)
|
||||
self.assertEqual(anordnung.seite_hoehe, SEITE[1])
|
||||
self.assertEqual(anordnung.verschiebung_y, 0.0)
|
||||
|
||||
def test_seite_wird_hoeher_wenn_auch_drei_spalten_nicht_reichen(self):
|
||||
"""Prüft, dass die Seite höher wird und Quellenangabe und Kartenschild nach unten rutschen, wenn auch drei Spalten nicht reichen."""
|
||||
karte = (10.0, 10.0, 160.0, 80.0) # kleines Verfahren
|
||||
anordnung = berechne_anordnung(karte, (380.0, 100.0), _legenden((150, 300, 300, 160, 450, 110)))
|
||||
self.assertEqual(anordnung.spalten, 3)
|
||||
benoetigt = 110 + kr.ABSTAND_LEGENDEN_MM + FEST
|
||||
self.assertAlmostEqual(anordnung.seite_hoehe, karte[1] + benoetigt + kr.RAND_MM)
|
||||
self.assertAlmostEqual(anordnung.verschiebung_y, benoetigt - karte[3])
|
||||
self.assertEqual(anordnung.spaltenbreite, 450)
|
||||
|
||||
def test_seite_wird_nie_kleiner(self):
|
||||
"""Prüft, dass Seitenbreite und -höhe nie unter die ursprüngliche Seitengröße fallen."""
|
||||
anordnung = berechne_anordnung(KARTE, (900.0, 600.0), _legenden((80, 50, 120, 30, 160, 20)))
|
||||
self.assertEqual((anordnung.seite_breite, anordnung.seite_hoehe), (900.0, 600.0))
|
||||
|
||||
def test_ohne_legenden_nur_kartenschild(self):
|
||||
"""Prüft, dass ohne Legenden nur Quellenangabe und Kartenschild zählen und die Seite bei zu niedriger Karte höher wird."""
|
||||
karte = (10.0, 10.0, 100.0, 100.0)
|
||||
anordnung = berechne_anordnung(karte, (310.0, 120.0), {1: [], 2: [], 3: []})
|
||||
self.assertEqual(anordnung.spalten, 1)
|
||||
self.assertAlmostEqual(anordnung.verschiebung_y, FEST - 100.0)
|
||||
self.assertAlmostEqual(anordnung.seite_hoehe, 10.0 + FEST + kr.RAND_MM)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- Layout-Fakes
|
||||
|
||||
class _Punkt:
|
||||
def __init__(self, x, y): self._x, self._y = x, y
|
||||
def x(self): return self._x
|
||||
def y(self): return self._y
|
||||
|
||||
|
||||
class _Element:
|
||||
def __init__(self, x, y, b, h):
|
||||
self.x, self.y, self.b, self.h = x, y, b, h
|
||||
self.verschoben = []
|
||||
|
||||
def positionWithUnits(self): return _Punkt(self.x, self.y)
|
||||
def sceneBoundingRect(self):
|
||||
return type("R", (), {"x": lambda s: self.x, "y": lambda s: self.y,
|
||||
"width": lambda s: self.b, "height": lambda s: self.h})()
|
||||
def attemptMove(self, punkt):
|
||||
neu_y = punkt.y() if callable(punkt.y) else punkt.y
|
||||
self.verschoben.append(neu_y)
|
||||
self.y = neu_y
|
||||
|
||||
|
||||
class _Legende(_Element):
|
||||
def __init__(self, x, y, groessen, unten_verankert=False):
|
||||
super().__init__(x, y, groessen[1][0], groessen[1][1])
|
||||
self.groessen, self.spalten, self.unten = groessen, 1, unten_verankert
|
||||
|
||||
def setColumnCount(self, n): self.spalten = n
|
||||
def setSplitLayer(self, wert): pass
|
||||
def setEqualColumnWidth(self, wert): pass
|
||||
def setLocked(self, wert): pass
|
||||
def adjustBoxSize(self):
|
||||
self.b, self.h = self.groessen[self.spalten]
|
||||
def sizeWithUnits(self):
|
||||
return type("S", (), {"width": lambda s: self.b, "height": lambda s: self.h})()
|
||||
def attemptResize(self, groesse): pass
|
||||
|
||||
|
||||
class _Seite:
|
||||
def __init__(self, b, h): self.b, self.h = b, h
|
||||
def pageSize(self): return type("S", (), {"width": lambda s: self.b, "height": lambda s: self.h})()
|
||||
def setPageSize(self, groesse):
|
||||
self.b = groesse.width() if callable(groesse.width) else groesse.width
|
||||
self.h = groesse.height() if callable(groesse.height) else groesse.height
|
||||
|
||||
|
||||
class _Layout:
|
||||
def __init__(self, elemente, seite):
|
||||
self.elemente, self.seite = elemente, seite
|
||||
|
||||
def itemById(self, element_id): return self.elemente.get(element_id)
|
||||
def pageCollection(self): return type("P", (), {"page": lambda s, i: self.seite})()
|
||||
|
||||
|
||||
class TestOrdneKartenrandAn(unittest.TestCase):
|
||||
|
||||
def _layout(self, legende_groessen):
|
||||
karte = _Element(10, 10, 160, 80)
|
||||
elemente = {
|
||||
"Hauptkarte": karte,
|
||||
"Legende": _Legende(180, 10, legende_groessen),
|
||||
"Quellenangabe": _Element(180, -32, 180, 20),
|
||||
"Kartenschild": _Element(180, -2, 180, 92),
|
||||
"Aktenzeichen": _Element(196, 87, 171, 4),
|
||||
}
|
||||
return _Layout(elemente, _Seite(380, 100)), elemente
|
||||
|
||||
def test_seite_hoeher_und_kartenschild_verschoben(self):
|
||||
"""Prüft, dass bei zu niedriger Karte die Legende dreispaltig wird, die Seite wächst und Quellenangabe und Kartenschild um denselben Betrag nach unten rutschen."""
|
||||
layout, elemente = self._layout({1: (150, 300), 2: (300, 160), 3: (450, 110)})
|
||||
anordnung = ordne_kartenrand_an(layout, rendern=lambda *a: None)
|
||||
self.assertEqual(elemente["Legende"].spalten, 3)
|
||||
self.assertGreater(layout.seite.h, 100)
|
||||
dy = anordnung.verschiebung_y
|
||||
self.assertGreater(dy, 0)
|
||||
self.assertAlmostEqual(elemente["Quellenangabe"].y, -32 + dy)
|
||||
self.assertAlmostEqual(elemente["Kartenschild"].y, -2 + dy)
|
||||
self.assertAlmostEqual(elemente["Aktenzeichen"].y, 87 + dy)
|
||||
self.assertEqual(elemente["Hauptkarte"].verschoben, [])
|
||||
self.assertEqual(layout.seite.b, 180 + 450 + kr.RAND_MM)
|
||||
|
||||
def test_passt_nichts_wird_verschoben(self):
|
||||
"""Prüft, dass bei ausreichend hoher Karte weder Seite noch Kartenschild verändert werden."""
|
||||
layout, elemente = self._layout({1: (100, 10), 2: (200, 6), 3: (300, 4)})
|
||||
elemente["Hauptkarte"].h = 300
|
||||
anordnung = ordne_kartenrand_an(layout, rendern=lambda *a: None)
|
||||
self.assertEqual(anordnung.spalten, 1)
|
||||
self.assertEqual((layout.seite.b, layout.seite.h), (380, 100))
|
||||
self.assertEqual(elemente["Kartenschild"].verschoben, [])
|
||||
|
||||
def test_ohne_karte_keine_anordnung(self):
|
||||
"""Prüft, dass ein Layout ohne Hauptkarte unverändert bleibt."""
|
||||
self.assertIsNone(ordne_kartenrand_an(_Layout({}, _Seite(100, 100)), rendern=lambda *a: None))
|
||||
|
||||
|
||||
class TestAnbindungPrintLayout(unittest.TestCase):
|
||||
|
||||
def test_vorlage_ordnet_kartenrand_an(self):
|
||||
"""Prüft, dass PrintLayout beim Anlegen einer Vorlage den Kartenrand anordnen lässt."""
|
||||
layout = MagicMock()
|
||||
with patch("sn_basis.modules.print_kartenrand.ordne_kartenrand_an") as anordnen:
|
||||
PrintLayout(project=MagicMock())._ordne_kartenrand_an(layout)
|
||||
anordnen.assert_called_once_with(layout)
|
||||
|
||||
def test_fehler_bei_der_anordnung_verhindert_die_vorlage_nicht(self):
|
||||
"""Prüft, dass ein Fehler bei der Anordnung nur als Warnung gemeldet wird und keine Ausnahme durchreicht."""
|
||||
with patch("sn_basis.modules.print_kartenrand.ordne_kartenrand_an", side_effect=RuntimeError("kaputt")), patch("sn_basis.functions.message_wrapper.warning") as warnung:
|
||||
PrintLayout(project=MagicMock())._ordne_kartenrand_an(MagicMock())
|
||||
self.assertIn("kaputt", warnung.call_args[0][1])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user