Merge pull request 'Kartenrand überlappungsfrei anordnen (#93)' (#95) from bugfix/93-kartenrand-ueberlappung into unstable
Reviewed-on: #95
This commit was merged in pull request #95.
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: 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)
|
- 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)
|
- 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]
|
QgsLayoutMeasurement: Type[Any]
|
||||||
QgsLayoutUtils: Type[Any]
|
QgsLayoutUtils: Type[Any]
|
||||||
QgsSimpleLineSymbolLayer: Type[Any]
|
QgsSimpleLineSymbolLayer: Type[Any]
|
||||||
|
QgsLayoutExporter: Type[Any]
|
||||||
SELECT_BEHAVIOR_SET: Any = None
|
SELECT_BEHAVIOR_SET: Any = None
|
||||||
SELECT_RELATION_INTERSECT: Any = None
|
SELECT_RELATION_INTERSECT: Any = None
|
||||||
PROPERTY_TYPE_EXPRESSION: Any = None
|
PROPERTY_TYPE_EXPRESSION: Any = None
|
||||||
@@ -923,6 +924,7 @@ try:
|
|||||||
QgsLayoutUtils as _QgsLayoutUtils,
|
QgsLayoutUtils as _QgsLayoutUtils,
|
||||||
QgsSymbolLayer as _QgsSymbolLayer,
|
QgsSymbolLayer as _QgsSymbolLayer,
|
||||||
QgsSimpleLineSymbolLayer as _QgsSimpleLineSymbolLayer,
|
QgsSimpleLineSymbolLayer as _QgsSimpleLineSymbolLayer,
|
||||||
|
QgsLayoutExporter as _QgsLayoutExporter,
|
||||||
Qgis as _QgisVT,
|
Qgis as _QgisVT,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -943,6 +945,7 @@ try:
|
|||||||
QgsLayoutMeasurement = _QgsLayoutMeasurement
|
QgsLayoutMeasurement = _QgsLayoutMeasurement
|
||||||
QgsLayoutUtils = _QgsLayoutUtils
|
QgsLayoutUtils = _QgsLayoutUtils
|
||||||
QgsSimpleLineSymbolLayer = _QgsSimpleLineSymbolLayer
|
QgsSimpleLineSymbolLayer = _QgsSimpleLineSymbolLayer
|
||||||
|
QgsLayoutExporter = _QgsLayoutExporter
|
||||||
|
|
||||||
SELECT_BEHAVIOR_SET = _QgisVT.SelectBehavior.SetSelection
|
SELECT_BEHAVIOR_SET = _QgisVT.SelectBehavior.SetSelection
|
||||||
SELECT_RELATION_INTERSECT = _QgisVT.SelectGeometryRelationship.Intersect
|
SELECT_RELATION_INTERSECT = _QgisVT.SelectGeometryRelationship.Intersect
|
||||||
@@ -1128,6 +1131,15 @@ except Exception:
|
|||||||
|
|
||||||
QgsSimpleLineSymbolLayer = _MockQgsSimpleLineSymbolLayer
|
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).
|
# QgsRectangle hatte im allgemeinen Mock-Block keinen Ersatz (nur Typannotation).
|
||||||
if "QgsRectangle" not in globals():
|
if "QgsRectangle" not in globals():
|
||||||
class _MockQgsRectangle:
|
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:
|
if basemap_legende:
|
||||||
self._erzeuge_basemap_legende(layout)
|
self._erzeuge_basemap_legende(layout)
|
||||||
|
|
||||||
|
self._ordne_kartenrand_an(layout)
|
||||||
|
|
||||||
layout_manager.addLayout(layout)
|
layout_manager.addLayout(layout)
|
||||||
self._current_layout = None
|
self._current_layout = None
|
||||||
open_layout_designer(layout)
|
open_layout_designer(layout)
|
||||||
return 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:
|
def _erzeuge_basemap_legende(self, layout: Any) -> None:
|
||||||
"""Kennzeichnet das Layout und erzeugt die Basemap-Legende (Basis #88).
|
"""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