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>
248 lines
13 KiB
Python
248 lines
13 KiB
Python
"""
|
||
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
|
||
self.gleiche_breite, self.umbruch = True, 0
|
||
self.mindestbreite = None
|
||
|
||
def setColumnCount(self, n): self.spalten = n
|
||
def setSplitLayer(self, wert): pass
|
||
def setEqualColumnWidth(self, wert): self.gleiche_breite = wert
|
||
def setAutoWrapLinesAfter(self, mm): self.umbruch = mm
|
||
def setMinimumSize(self, groesse):
|
||
self.mindestbreite = groesse.width() if callable(groesse.width) else groesse.width
|
||
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
|
||
self.eigenschaften = {}
|
||
|
||
def setCustomProperty(self, k, v): self.eigenschaften[k] = v
|
||
def customProperty(self, k, standard=None): return self.eigenschaften.get(k, standard)
|
||
|
||
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_spalten_ungleich_breit_und_text_umgebrochen(self):
|
||
"""Prüft, dass die Legenden ungleich breite Spalten und einen automatischen Textumbruch bekommen, damit mehrspaltige Legenden nicht unnötig breit werden (#96)."""
|
||
layout, elemente = self._layout({1: (150, 300), 2: (300, 160), 3: (450, 110)})
|
||
ordne_kartenrand_an(layout, rendern=lambda *a: None)
|
||
self.assertFalse(elemente["Legende"].gleiche_breite)
|
||
self.assertEqual(elemente["Legende"].umbruch, kr.TEXTUMBRUCH_MM)
|
||
|
||
def test_legenden_mindestens_so_breit_wie_kartenschild(self):
|
||
"""Prüft, dass die Legenden die Breite des Kartenschilds als Mindestbreite bekommen und das Layout sie für das Neuladen vermerkt."""
|
||
layout, elemente = self._layout({1: (60, 300), 2: (120, 160), 3: (175, 110)})
|
||
ordne_kartenrand_an(layout, rendern=lambda *a: None)
|
||
self.assertEqual(elemente["Legende"].mindestbreite, kr.MIN_SPALTENBREITE_MM)
|
||
self.assertEqual(kr.MIN_SPALTENBREITE_MM, 180.0) # Breite von Kartenschild und Quellenangabe
|
||
self.assertEqual(float(layout.customProperty(kr.LAYOUT_MINDESTBREITE)), kr.MIN_SPALTENBREITE_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 TestMindestbreiteBeimLaden(unittest.TestCase):
|
||
|
||
def _projekt(self, *layouts):
|
||
verwaltung = type("V", (), {"printLayouts": lambda s: list(layouts)})()
|
||
return type("P", (), {"layoutManager": lambda s: verwaltung})()
|
||
|
||
def test_mindestbreite_nur_fuer_vermerkte_layouts(self):
|
||
"""Prüft, dass beim Laden die Mindestbreite nur an Layouts mit Vermerk wieder gesetzt wird."""
|
||
mit = _Layout({"Legende": _Legende(0, 0, {1: (60, 10)}), "Legende Basemap": _Legende(0, 0, {1: (90, 10)})}, _Seite(1, 1))
|
||
mit.setCustomProperty(kr.LAYOUT_MINDESTBREITE, "180.0")
|
||
ohne = _Layout({"Legende": _Legende(0, 0, {1: (60, 10)})}, _Seite(1, 1))
|
||
self.assertEqual(kr.wende_mindestbreiten_an(self._projekt(mit, ohne)), 1)
|
||
self.assertEqual(mit.elemente["Legende"].mindestbreite, 180.0)
|
||
self.assertEqual(mit.elemente["Legende Basemap"].mindestbreite, 180.0)
|
||
self.assertIsNone(ohne.elemente["Legende"].mindestbreite)
|
||
|
||
def test_listener_verbinden_und_trennen(self):
|
||
"""Prüft, dass der Listener readProject einmal verbindet, beim Trennen wieder löst und beides mehrfach aufgerufen werden darf."""
|
||
signal = MagicMock()
|
||
projekt = MagicMock()
|
||
projekt.readProject = signal
|
||
projekt.layoutManager.return_value.printLayouts.return_value = []
|
||
with patch.object(kr, "QGIS_AVAILABLE", True), patch.object(kr, "QgsProject") as qgs_projekt:
|
||
qgs_projekt.instance.return_value = projekt
|
||
kr.setup_mindestbreiten_listener()
|
||
kr.setup_mindestbreiten_listener()
|
||
self.assertEqual(signal.connect.call_count, 1)
|
||
kr.teardown_mindestbreiten_listener()
|
||
kr.teardown_mindestbreiten_listener()
|
||
self.assertEqual(signal.disconnect.call_count, 1)
|
||
|
||
def test_ohne_qgis_kein_listener(self):
|
||
"""Prüft, dass im Mock-Modus kein Listener verbunden wird."""
|
||
with patch.object(kr, "QGIS_AVAILABLE", False), patch.object(kr, "QgsProject") as qgs_projekt:
|
||
kr.setup_mindestbreiten_listener()
|
||
qgs_projekt.instance.assert_not_called()
|
||
|
||
|
||
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()
|