diff --git a/modules/print_layout_baufreigabe.py b/modules/print_layout_baufreigabe.py index eefdfde..7a65e94 100644 --- a/modules/print_layout_baufreigabe.py +++ b/modules/print_layout_baufreigabe.py @@ -97,6 +97,7 @@ class PrintLayoutBaufreigabe(PrintLayout): plotmassstab: float, thema: str = "", fit_extent_to_map: bool = True, + atlas_driven: bool = True, ) -> Any: """Erstellt ein Baufreigabe-Layout mit fester DIN A3/A4 Querformat-Groesse. @@ -108,7 +109,7 @@ class PrintLayoutBaufreigabe(PrintLayout): extent: QgsRectangle der Kartenausdehnung plotmassstab: Plotmassstab (z.B. 5000 fuer 1:5000) thema: Name des Visibility-Presets (optional) - fit_extent_to_map: Wird fuer Baufreigabe ignoriert (feste Kartenflaeche) + fit_extent_to_map: Extent bei Bedarf auf das Karten-Seitenverhaeltnis erweitern Returns: QgsPrintLayout objektInstance (wird im Layout-Dialog geoeffnet) @@ -152,10 +153,17 @@ class PrintLayoutBaufreigabe(PrintLayout): map_height_mm, ) - _ = fit_extent_to_map - if extent is not None and hasattr(extent, "isNull") and callable(extent.isNull) and not extent.isNull(): + fitted_extent = extent + if fit_extent_to_map: + fitted_extent = self._fit_extent_to_map_item(extent, map_width_mm, map_height_mm) + + if fitted_extent is not None and hasattr(fitted_extent, "isNull") and callable(fitted_extent.isNull) and not fitted_extent.isNull(): try: - hauptkarte.zoomToExtent(extent) + set_extent = getattr(hauptkarte, "setExtent", None) + if callable(set_extent): + set_extent(fitted_extent) + else: + hauptkarte.zoomToExtent(fitted_extent) except Exception: pass @@ -165,31 +173,30 @@ class PrintLayoutBaufreigabe(PrintLayout): except Exception: pass - # Hauptkarte standardmaessig Atlas-gesteuert: - # - Extent folgt aktuellem Atlas-Objekt - # - Skalierung automatisch - # - Margin entspricht ca. 1 cm Rand innerhalb der Kartenflaeche - set_atlas_driven = getattr(hauptkarte, "setAtlasDriven", None) - if callable(set_atlas_driven): - try: - set_atlas_driven(True) - except Exception: - pass + # Atlas-Steuerung nur aktivieren, wenn Atlas tatsaechlich genutzt wird. + # Andernfalls bleiben Extent/Scale unveraendert (z.B. Canvas-Ausschnitt). + if atlas_driven: + set_atlas_driven_fn = getattr(hauptkarte, "setAtlasDriven", None) + if callable(set_atlas_driven_fn): + try: + set_atlas_driven_fn(True) + except Exception: + pass - set_atlas_scaling_mode = getattr(hauptkarte, "setAtlasScalingMode", None) - atlas_mode_auto = getattr(getattr(QgsLayoutItemMap, "AtlasScalingMode", object), "Auto", None) - if callable(set_atlas_scaling_mode) and atlas_mode_auto is not None: - try: - set_atlas_scaling_mode(atlas_mode_auto) - except Exception: - pass + set_atlas_scaling_mode = getattr(hauptkarte, "setAtlasScalingMode", None) + atlas_mode_auto = getattr(getattr(QgsLayoutItemMap, "AtlasScalingMode", object), "Auto", None) + if callable(set_atlas_scaling_mode) and atlas_mode_auto is not None: + try: + set_atlas_scaling_mode(atlas_mode_auto) + except Exception: + pass - set_atlas_margin = getattr(hauptkarte, "setAtlasMargin", None) - if callable(set_atlas_margin): - try: - set_atlas_margin(self._calc_atlas_margin_percent(map_width_mm, map_height_mm)) - except Exception: - pass + set_atlas_margin = getattr(hauptkarte, "setAtlasMargin", None) + if callable(set_atlas_margin): + try: + set_atlas_margin(self._calc_atlas_margin_percent(map_width_mm, map_height_mm)) + except Exception: + pass set_frame_enabled = getattr(hauptkarte, "setFrameEnabled", None) if callable(set_frame_enabled): diff --git a/modules/print_logic.py b/modules/print_logic.py index c485bb7..27fc600 100644 --- a/modules/print_logic.py +++ b/modules/print_logic.py @@ -75,6 +75,7 @@ THEMA_WIE_KARTENFENSTER = "wie kartenfenster" DRUCKBEREICH_VERFAHRENSGEBIET = "Verfahrensgebiet" DRUCKBEREICH_AKTUELLE_AUSWAHL = "aktuelle Auswahl" DRUCKBEREICH_RECHTECK = "Rechteck" +DRUCKBEREICH_WIE_KARTENFENSTER = "Wie Kartenfenster" KARTENNAME_BY_AUSWAHL = { KARTENNAME_38: "Planungsübersicht §38 FlurbG", @@ -99,8 +100,16 @@ DIN_GROESSEN: dict[str, tuple[int, int]] = { "DIN A3": (297, 420), "DIN A4": (210, 297), } +DIN_AUTO_REIHENFOLGE = ("DIN A4", "DIN A3", "DIN A2", "DIN A1", "DIN A0") DIN_STANDARD = "DIN A0" DRUCKPUFFER_STANDARD_CM = 1 +ENDLOSROLLE_MAX_BREITE_MM = 2000.0 +STANDARD_LAYOUT_FIXBREITE_MM = 210.0 +STANDARD_LAYOUT_FIXHOEHE_MM = 20.0 +BAUFREIGABE_LAYOUT_FIXBREITE_MM = 110.0 +BAUFREIGABE_LAYOUT_FIXHOEHE_MM = 20.0 +STANDARD_A4_LANDSCAPE_W_MM = 297.0 +STANDARD_A4_LANDSCAPE_H_MM = 210.0 class PrintLogic: @@ -833,12 +842,69 @@ class PrintLogic: return None return self._build_rectangle(x_min, y_min, x_max, y_max) + def _get_canvas_extent(self) -> Any | None: + map_canvas_fn = getattr(iface, "mapCanvas", None) + if not callable(map_canvas_fn): + return None + + try: + canvas = map_canvas_fn() + except Exception: + return None + + if canvas is None: + return None + + extent_fn = getattr(canvas, "extent", None) + if not callable(extent_fn): + return None + + try: + extent = extent_fn() + except Exception: + return None + + if extent is None: + return None + + is_null = getattr(extent, "isNull", None) + if callable(is_null): + try: + if is_null(): + return None + except Exception: + return None + + width_fn = getattr(extent, "width", None) + height_fn = getattr(extent, "height", None) + if not callable(width_fn) or not callable(height_fn): + return None + try: + width_value = float(cast(Any, width_fn())) + height_value = float(cast(Any, height_fn())) + if width_value <= 0 or height_value <= 0: + return None + except Exception: + return None + + return extent + def _resolve_extent( self, resolved_layer: object | None, druckbereich_auswahl: str, plotmassstab: float, ) -> tuple[Any | None, object | None]: + if druckbereich_auswahl == DRUCKBEREICH_WIE_KARTENFENSTER: + extent = self._get_canvas_extent() + if extent is None: + self.pruefmanager.zeige_hinweis( + "Druckbereich", + "Das aktuelle Kartenfenster liefert keine gueltige Ausdehnung.", + ) + return None, None + return extent, None + if druckbereich_auswahl == DRUCKBEREICH_AKTUELLE_AUSWAHL: selection_layer = self._resolve_selection_layer(resolved_layer) extent = self._get_selected_extent(selection_layer) @@ -855,7 +921,7 @@ class PrintLogic: if extent is None: self.activate_druckrechteck_capture(resolved_layer) return None, None - return self._buffer_extent(extent, plotmassstab), None + return extent, None lp = Layerpruefer(layer=resolved_layer) ergebnis = lp.pruefe() @@ -902,6 +968,7 @@ class PrintLogic: def set_druckbereich_for_auswahl(self, auswahl: str) -> None: if auswahl not in { DRUCKBEREICH_VERFAHRENSGEBIET, + DRUCKBEREICH_WIE_KARTENFENSTER, DRUCKBEREICH_AKTUELLE_AUSWAHL, DRUCKBEREICH_RECHTECK, }: @@ -910,39 +977,166 @@ class PrintLogic: def _get_baufreigabe_page_dimensions( self, - zielgroesse: str, - ) -> tuple[str, float, float, float, float]: - """Berechnet feste Seiten- und Hauptkartengroessen fuer Baufreigabe. + required_map_width_mm: float, + required_map_height_mm: float, + max_format: str = "DIN A3", + ) -> tuple[str, float, float, float, float, bool]: + """Waehlt fuer Baufreigabe ein Blatt im Querformat. Args: - zielgroesse: "DIN A3" oder "DIN A4" (wird zu Querformat konvertiert) + required_map_width_mm: benoetigte Breite der Hauptkarte in mm + required_map_height_mm: benoetigte Hoehe der Hauptkarte in mm + max_format: maximale Blattgroesse, standardmaessig DIN A3 Returns: - (din_format, page_width_mm, page_height_mm, map_w_mm, map_h_mm) + (din_format, page_width_mm, page_height_mm, map_w_mm, map_h_mm, fallback_applied) """ - # Nur A3/A4 im Querformat fuer Baufreigabe - if zielgroesse == "DIN A3": - din_format = "DIN A3" - page_w, page_h = 420.0, 297.0 - else: - din_format = "DIN A4" - page_w, page_h = 297.0, 210.0 + candidates = ( + ("DIN A4", 297.0, 210.0), + ("DIN A3", 420.0, 297.0), + ) - # Feste Hauptkartenflaeche aus Querformat mit festen Rändern: - # links 10 mm, oben 10 mm, rechts 10 mm, unten 10 mm, - # plus 10 mm Abstand zum rechten Seitenbereich. - kartenschild_w = 80.0 - kartenschild_h = 92.0 - margin = 10.0 + try: + max_index = [candidate[0] for candidate in candidates].index(max_format) + except ValueError: + max_index = len(candidates) - 1 - map_w = page_w - (3 * margin + kartenschild_w) - map_h = page_h - (2 * margin) + limited_candidates = candidates[: max_index + 1] - # Untergrenzen zur Sicherheit bei abweichenden Seiteneinstellungen - map_w = max(map_w, 50.0) - map_h = max(map_h, kartenschild_h + margin) + for din_format, page_w, page_h in limited_candidates: + map_w = page_w - BAUFREIGABE_LAYOUT_FIXBREITE_MM + map_h = page_h - BAUFREIGABE_LAYOUT_FIXHOEHE_MM + if map_w >= required_map_width_mm and map_h >= required_map_height_mm: + return din_format, page_w, page_h, map_w, map_h, False - return din_format, page_w, page_h, map_w, map_h + # Wenn der gewuenschte Ausschnitt nicht passt, verwende das maximal erlaubte Blatt + # und erweitere die Hauptkarte auf die verfuegbare Kartenflaeche (zentriert per fit_extent_to_map). + din_format, page_w, page_h = limited_candidates[-1] + map_w = max(page_w - BAUFREIGABE_LAYOUT_FIXBREITE_MM, 50.0) + map_h = max(page_h - BAUFREIGABE_LAYOUT_FIXHOEHE_MM, 50.0) + return din_format, page_w, page_h, map_w, map_h, True + + def _does_plot_fit_page( + self, + page_width_mm: float, + page_height_mm: float, + map_width_mm: float, + map_height_mm: float, + fixed_width_mm: float, + fixed_height_mm: float, + ) -> bool: + required_w = map_width_mm + fixed_width_mm + required_h = map_height_mm + fixed_height_mm + return required_w <= page_width_mm and required_h <= page_height_mm + + def _get_plot_dimensions_mm( + self, + map_width_mm: float, + map_height_mm: float, + fixed_width_mm: float, + fixed_height_mm: float, + ) -> tuple[float, float]: + return map_width_mm + fixed_width_mm, map_height_mm + fixed_height_mm + + def _get_required_map_dimensions_mm(self, extent: Any, plotmassstab: float) -> tuple[float, float]: + """Berechnet die benoetigte Kartenfenstergroesse in mm fuer Extent und Massstab.""" + if extent is None or not isinstance(plotmassstab, (int, float)) or plotmassstab <= 0: + return 0.0, 0.0 + + try: + width_mm = float(extent.width()) * 1000.0 / float(plotmassstab) + height_mm = float(extent.height()) * 1000.0 / float(plotmassstab) + except Exception: + return 0.0, 0.0 + + return max(width_mm, 0.0), max(height_mm, 0.0) + + def _select_standard_page_layout( + self, + required_map_width_mm: float, + required_map_height_mm: float, + max_format: str, + ) -> tuple[float, float, float, float] | None: + """Waehlt das kleinste passende DIN-Blatt fuer das Standardlayout.""" + try: + max_index = DIN_AUTO_REIHENFOLGE.index(max_format) + except ValueError: + max_index = len(DIN_AUTO_REIHENFOLGE) - 1 + + for fmt in DIN_AUTO_REIHENFOLGE[: max_index + 1]: + base_w, base_h = DIN_GROESSEN[fmt] + fitting_candidates: list[tuple[float, float, float, float]] = [] + for page_w, page_h in ((float(base_w), float(base_h)), (float(base_h), float(base_w))): + map_w = page_w - STANDARD_LAYOUT_FIXBREITE_MM + map_h = page_h - STANDARD_LAYOUT_FIXHOEHE_MM + if map_w >= required_map_width_mm and map_h >= required_map_height_mm: + fitting_candidates.append((page_w, page_h, map_w, map_h)) + + if fitting_candidates: + return min( + fitting_candidates, + key=lambda candidate: (candidate[2] - required_map_width_mm) + (candidate[3] - required_map_height_mm), + ) + + return None + + def _get_standard_layout_dimensions( + self, + required_map_width_mm: float, + required_map_height_mm: float, + max_format: str, + formfaktor: bool, + ) -> tuple[float, float, float, float, bool] | None: + if formfaktor: + plotgroesse_w, plotgroesse_h = self._get_plot_dimensions_mm( + required_map_width_mm, + required_map_height_mm, + STANDARD_LAYOUT_FIXBREITE_MM, + STANDARD_LAYOUT_FIXHOEHE_MM, + ) + din_dims = DIN_GROESSEN.get(max_format, DIN_GROESSEN[DIN_STANDARD]) + max_page_h = float(max(din_dims)) + page_w = min(plotgroesse_w, ENDLOSROLLE_MAX_BREITE_MM) + page_h = min(plotgroesse_h, max_page_h) + return self._apply_standard_minimum_a4_fallback( + page_w, + page_h, + required_map_width_mm, + required_map_height_mm, + ) + + selected_layout = self._select_standard_page_layout( + required_map_width_mm, + required_map_height_mm, + max_format, + ) + if selected_layout is None: + return None + + page_w, page_h, map_w, map_h = selected_layout + force_fit_extent = map_w > required_map_width_mm or map_h > required_map_height_mm + return page_w, page_h, map_w, map_h, force_fit_extent + + def _apply_standard_minimum_a4_fallback( + self, + page_width_mm: float, + page_height_mm: float, + map_width_mm: float, + map_height_mm: float, + ) -> tuple[float, float, float, float, bool]: + """Erzwingt bei zu kleinem Standardlayout mindestens DIN A4 im Querformat. + + Liefert zusätzlich ein Flag zurück, damit die Extent-Anpassung im Layout + zur zentrierten Darstellung aktiviert werden kann. + """ + fallback_page_w = max(page_width_mm, STANDARD_A4_LANDSCAPE_W_MM) + fallback_page_h = max(page_height_mm, STANDARD_A4_LANDSCAPE_H_MM) + if fallback_page_w == page_width_mm and fallback_page_h == page_height_mm: + return page_width_mm, page_height_mm, map_width_mm, map_height_mm, False + + fallback_map_w = max(map_width_mm, fallback_page_w - STANDARD_LAYOUT_FIXBREITE_MM, 50.0) + fallback_map_h = max(map_height_mm, fallback_page_h - STANDARD_LAYOUT_FIXHOEHE_MM, 50.0) + return fallback_page_w, fallback_page_h, fallback_map_w, fallback_map_h, True def druckvorlage_anlegen( self, @@ -960,11 +1154,19 @@ class PrintLogic: return {"ok": False, "switch_to_tab_a": False, "atlas_seiten": 0} if massstab_auswahl == MASSSTAB_WIE_KARTENFENSTER: - massstab_str = get_variable(PLOTMASSSTAB_VAR, scope="project") or "" + # Maßstab direkt vom aktuellen Canvas lesen; Projektvariable als Fallback. try: - massstab_zahl = float(massstab_str) - except (ValueError, TypeError): + canvas = iface.mapCanvas() + live_scale = canvas.scale() if canvas is not None else 0.0 + massstab_zahl = float(live_scale) if live_scale > 0 else 0.0 + except Exception: massstab_zahl = 0.0 + if massstab_zahl <= 0: + massstab_str = get_variable(PLOTMASSSTAB_VAR, scope="project") or "" + try: + massstab_zahl = float(massstab_str) + except (ValueError, TypeError): + massstab_zahl = 0.0 else: massstab_zahl = float(PLOTMASSSTAB_BY_AUSWAHL.get(massstab_auswahl, 0)) @@ -984,44 +1186,58 @@ class PrintLogic: # Alt: Rückwärtskompatibilität set_variable("verfahrensgebietslayer", layer_id, scope="project") - kartenbild_w = extent.width() * 1000.0 / massstab_zahl - kartenbild_h = extent.height() * 1000.0 / massstab_zahl + kartenbild_w, kartenbild_h = self._get_required_map_dimensions_mm(extent, massstab_zahl) - plotgroesse_w = kartenbild_w + 210.0 - plotgroesse_h = kartenbild_h + 20.0 - - din_dims = DIN_GROESSEN.get(zielgroesse, DIN_GROESSEN[DIN_STANDARD]) - din_w = float(din_dims[0]) - din_h = float(din_dims[1]) - din_max_h = float(max(din_dims)) - if formfaktor: - ziel_w, ziel_h = plotgroesse_w, min(plotgroesse_h, din_max_h) + force_fit_extent_for_min_a4 = False + if kartenname_auswahl == KARTENNAME_BAUFREIGABE: + din_format, auto_page_w, auto_page_h, auto_map_w, auto_map_h, _bf_fallback = self._get_baufreigabe_page_dimensions( + kartenbild_w, + kartenbild_h, + max_format=zielgroesse if zielgroesse in {"DIN A4", "DIN A3"} else "DIN A3", + ) else: - cap_w, cap_h = din_w, din_h - if not (plotgroesse_w <= cap_w and plotgroesse_h <= cap_h): - if plotgroesse_w <= din_h and plotgroesse_h <= din_w: - cap_w, cap_h = din_h, din_w - ziel_w = min(plotgroesse_w, cap_w) - ziel_h = min(plotgroesse_h, cap_h) - - if kartenname_auswahl != KARTENNAME_BAUFREIGABE: - if ziel_w < DIN_GROESSEN["DIN A4"][0] or ziel_h < DIN_GROESSEN["DIN A4"][1]: - self.pruefmanager.zeige_hinweis( - "Blattgroesse kleiner als DIN A4", - "Die Layouterstellung wird trotzdem fortgesetzt.", - ) - - if plotgroesse_w > ziel_w or plotgroesse_h > ziel_h: - if plotgroesse_w <= ziel_h and plotgroesse_h <= ziel_w: - ziel_w, ziel_h = ziel_h, ziel_w - - if plotgroesse_w > ziel_w or plotgroesse_h > ziel_h: + standard_layout = self._get_standard_layout_dimensions( + kartenbild_w, + kartenbild_h, + zielgroesse, + formfaktor, + ) + if standard_layout is None: self.pruefmanager.zeige_hinweis( "Zielgroesse zu klein", "Die gewaehlte Zielgroesse reicht fuer den Einzelblatt-Druck nicht aus.", ) return {"ok": False, "switch_to_tab_a": False, "atlas_seiten": 0} + auto_page_w, auto_page_h, auto_map_w, auto_map_h, force_fit_extent_for_min_a4 = standard_layout + + # Finale Sicherheitspruefung: alle Layoutobjekte muessen auf die Seite passen. + if kartenname_auswahl == KARTENNAME_BAUFREIGABE: + fits_page = self._does_plot_fit_page( + auto_page_w, + auto_page_h, + auto_map_w, + auto_map_h, + BAUFREIGABE_LAYOUT_FIXBREITE_MM, + BAUFREIGABE_LAYOUT_FIXHOEHE_MM, + ) + else: + fits_page = self._does_plot_fit_page( + auto_page_w, + auto_page_h, + auto_map_w, + auto_map_h, + STANDARD_LAYOUT_FIXBREITE_MM, + STANDARD_LAYOUT_FIXHOEHE_MM, + ) + + if not fits_page: + self.pruefmanager.zeige_hinweis( + "Zielgroesse zu klein", + "Die gewaehlte Zielgroesse reicht fuer den Einzelblatt-Druck nicht aus.", + ) + return {"ok": False, "switch_to_tab_a": False, "atlas_seiten": 0} + kartenname = get_variable(KARTENNAME_VAR, scope="project") or KARTENNAME_BY_AUSWAHL.get( kartenname_auswahl, "Vorlage" ) @@ -1037,34 +1253,41 @@ class PrintLogic: vorlage_name = (vorlage_name or "").strip() or kartenname + # Atlas-Status vorab bestimmen, damit er sowohl an create_ als auch configure_ uebergeben wird. + atlas_raw_bf = get_variable(ATLAS_ERZEUGEN_UI_VAR, scope="project") + atlas_erzeugen_bf = True if atlas_raw_bf in (None, "") else str(atlas_raw_bf).strip().lower() in { + "1", "true", "ja", "yes", "on", + } + # Verzweigung: Baufreigabe vs. Standard-Layout try: if kartenname_auswahl == KARTENNAME_BAUFREIGABE: # Baufreigabe: feste DIN A3/A4 Querformat-Logik mit 80mm Kartenschild - din_format, page_w, page_h, map_w, map_h = self._get_baufreigabe_page_dimensions(zielgroesse) + din_format = "DIN A3" if auto_page_w >= 420.0 else "DIN A4" PrintLayoutBaufreigabe().create_baufreigabe_layout( name=vorlage_name if vorlage_name != kartenname else "Baufreigabe", din_format=din_format, - map_width_mm=map_w, - map_height_mm=map_h, + map_width_mm=auto_map_w, + map_height_mm=auto_map_h, extent=extent, plotmassstab=massstab_zahl, thema=thema, - fit_extent_to_map=(druckbereich_auswahl != DRUCKBEREICH_VERFAHRENSGEBIET), + fit_extent_to_map=True, + atlas_driven=atlas_erzeugen_bf, ) else: # Standard-Layout: dynamische Groessenanpassung PrintLayout().create_single_page_layout( name=vorlage_name, - page_width_mm=ziel_w, - page_height_mm=ziel_h, - map_width_mm=kartenbild_w, - map_height_mm=kartenbild_h, + page_width_mm=auto_page_w, + page_height_mm=auto_page_h, + map_width_mm=auto_map_w, + map_height_mm=auto_map_h, extent=extent, plotmassstab=massstab_zahl, kartenname_auswahl=kartenname_auswahl, thema=thema, - fit_extent_to_map=(druckbereich_auswahl != DRUCKBEREICH_VERFAHRENSGEBIET), + fit_extent_to_map=(druckbereich_auswahl != DRUCKBEREICH_VERFAHRENSGEBIET) or force_fit_extent_for_min_a4, ) except ValueError as exc: self.pruefmanager.zeige_hinweis("Vorlage anlegen", str(exc)) @@ -1078,16 +1301,7 @@ class PrintLogic: # Nach erfolgreicher Layout-Erstellung: Atlas ggf. konfigurieren (nur falls aktiviert) if kartenname_auswahl == KARTENNAME_BAUFREIGABE: - atlas_raw = get_variable(ATLAS_ERZEUGEN_UI_VAR, scope="project") - atlas_erzeugen = True if atlas_raw in (None, "") else str(atlas_raw).strip().lower() in { - "1", - "true", - "ja", - "yes", - "on", - } - - if atlas_erzeugen: + if atlas_erzeugen_bf: self.configure_atlas_for_baufreigabe_layout() else: self.configure_atlas_for_baufreigabe_layout( diff --git a/tests/test_print_logic.py b/tests/test_print_logic.py new file mode 100644 index 0000000..a6c787a --- /dev/null +++ b/tests/test_print_logic.py @@ -0,0 +1,191 @@ +import unittest +from unittest.mock import MagicMock, patch + +from sn_basis.modules.print_logic import ( + PrintLogic, + DRUCKBEREICH_RECHTECK, + DRUCKBEREICH_WIE_KARTENFENSTER, + ENDLOSROLLE_MAX_BREITE_MM, +) + + +class _FakeExtent: + def __init__(self, width: float = 100.0, height: float = 80.0) -> None: + self._width = width + self._height = height + + def width(self) -> float: + return self._width + + def height(self) -> float: + return self._height + + def isNull(self) -> bool: + return False + + +class _FakeCanvas: + def __init__(self, extent: object | None) -> None: + self._extent = extent + + def extent(self) -> object | None: + return self._extent + + +class TestPrintLogic(unittest.TestCase): + + def setUp(self) -> None: + self.logic = PrintLogic(pruefmanager=MagicMock()) + + def test_get_baufreigabe_page_dimensions_prefers_a4_for_small_extent(self): + din_format, page_w, page_h, map_w, map_h, fallback = self.logic._get_baufreigabe_page_dimensions(80.0, 120.0) + + self.assertEqual(din_format, "DIN A4") + self.assertEqual((page_w, page_h), (297.0, 210.0)) + self.assertEqual((map_w, map_h), (187.0, 190.0)) + self.assertFalse(fallback) + + def test_get_baufreigabe_page_dimensions_escalates_to_a3(self): + din_format, page_w, page_h, map_w, map_h, fallback = self.logic._get_baufreigabe_page_dimensions(200.0, 180.0) + + self.assertEqual(din_format, "DIN A3") + self.assertEqual((page_w, page_h), (420.0, 297.0)) + self.assertEqual((map_w, map_h), (310.0, 277.0)) + self.assertFalse(fallback) + + def test_get_baufreigabe_page_dimensions_respects_a4_as_maximum_with_fallback(self): + din_format, page_w, page_h, map_w, map_h, fallback = self.logic._get_baufreigabe_page_dimensions( + 200.0, + 180.0, + max_format="DIN A4", + ) + + self.assertEqual(din_format, "DIN A4") + self.assertEqual((page_w, page_h), (297.0, 210.0)) + self.assertEqual((map_w, map_h), (187.0, 190.0)) + self.assertTrue(fallback) + + def test_get_baufreigabe_page_dimensions_falls_back_to_a3_for_oversize_extent(self): + din_format, page_w, page_h, map_w, map_h, fallback = self.logic._get_baufreigabe_page_dimensions(400.0, 300.0) + + self.assertEqual(din_format, "DIN A3") + self.assertEqual((page_w, page_h), (420.0, 297.0)) + self.assertEqual((map_w, map_h), (310.0, 277.0)) + self.assertTrue(fallback) + + def test_select_standard_page_layout_uses_a4_as_minimum_sheet(self): + selected = self.logic._select_standard_page_layout(60.0, 100.0, max_format="DIN A3") + + self.assertEqual(selected, (297.0, 210.0, 87.0, 190.0)) + + def test_select_standard_page_layout_limits_hauptkarte_to_a3(self): + selected = self.logic._select_standard_page_layout(120.0, 240.0, max_format="DIN A3") + + self.assertEqual(selected, (420.0, 297.0, 210.0, 277.0)) + + def test_select_standard_page_layout_returns_none_if_a3_is_too_small(self): + selected = self.logic._select_standard_page_layout(260.0, 280.0, max_format="DIN A3") + + self.assertIsNone(selected) + + def test_get_standard_layout_dimensions_uses_a4_and_forces_fit_for_small_extent(self): + page_w, page_h, map_w, map_h, force_fit = self.logic._get_standard_layout_dimensions( + 60.0, + 100.0, + max_format="DIN A3", + formfaktor=False, + ) + + self.assertEqual((page_w, page_h), (297.0, 210.0)) + self.assertEqual((map_w, map_h), (87.0, 190.0)) + self.assertTrue(force_fit) + + def test_get_standard_layout_dimensions_caps_endlosrolle_width_at_2000_mm(self): + page_w, page_h, map_w, map_h, force_fit = self.logic._get_standard_layout_dimensions( + 2500.0, + 100.0, + max_format="DIN A0", + formfaktor=True, + ) + + self.assertEqual(page_w, ENDLOSROLLE_MAX_BREITE_MM) + self.assertEqual(page_h, 210.0) + self.assertEqual((map_w, map_h), (2500.0, 190.0)) + self.assertTrue(force_fit) + + def test_apply_standard_minimum_a4_fallback_for_small_page(self): + page_w, page_h, map_w, map_h, forced = self.logic._apply_standard_minimum_a4_fallback( + 250.0, + 180.0, + 40.0, + 20.0, + ) + + self.assertTrue(forced) + self.assertEqual((page_w, page_h), (297.0, 210.0)) + self.assertEqual((map_w, map_h), (87.0, 190.0)) + + def test_apply_standard_minimum_a4_fallback_keeps_large_page(self): + page_w, page_h, map_w, map_h, forced = self.logic._apply_standard_minimum_a4_fallback( + 420.0, + 297.0, + 210.0, + 277.0, + ) + + self.assertFalse(forced) + self.assertEqual((page_w, page_h), (420.0, 297.0)) + self.assertEqual((map_w, map_h), (210.0, 277.0)) + + def test_does_plot_fit_page_true_for_standard_a4(self): + fits = self.logic._does_plot_fit_page(297.0, 210.0, 87.0, 190.0, 210.0, 20.0) + + self.assertTrue(fits) + + def test_does_plot_fit_page_false_if_objects_exceed_page(self): + fits = self.logic._does_plot_fit_page(297.0, 210.0, 120.0, 190.0, 210.0, 20.0) + + self.assertFalse(fits) + + @patch("sn_basis.modules.print_logic.set_variable") + def test_set_druckbereich_accepts_wie_kartenfenster(self, mock_set_variable): + self.logic.set_druckbereich_for_auswahl(DRUCKBEREICH_WIE_KARTENFENSTER) + + mock_set_variable.assert_called_once_with("druckbereich", DRUCKBEREICH_WIE_KARTENFENSTER, scope="project") + + @patch("sn_basis.modules.print_logic.iface") + def test_get_canvas_extent_returns_valid_extent(self, mock_iface): + extent = _FakeExtent(120.0, 90.0) + mock_iface.mapCanvas.return_value = _FakeCanvas(extent) + + result = self.logic._get_canvas_extent() + + self.assertIs(result, extent) + + @patch.object(PrintLogic, "_buffer_extent") + @patch.object(PrintLogic, "_get_canvas_extent") + def test_resolve_extent_uses_canvas_extent_without_buffer(self, mock_get_canvas_extent, mock_buffer_extent): + extent = _FakeExtent(120.0, 90.0) + mock_get_canvas_extent.return_value = extent + + result_extent, result_layer = self.logic._resolve_extent(None, DRUCKBEREICH_WIE_KARTENFENSTER, 5000.0) + + self.assertIs(result_extent, extent) + self.assertIsNone(result_layer) + mock_buffer_extent.assert_not_called() + + @patch.object(PrintLogic, "_buffer_extent") + @patch.object(PrintLogic, "_get_druckrechteck_extent") + def test_resolve_extent_uses_rectangle_extent_without_buffer(self, mock_get_rect_extent, mock_buffer_extent): + extent = _FakeExtent(120.0, 90.0) + mock_get_rect_extent.return_value = extent + + result_extent, result_layer = self.logic._resolve_extent(None, DRUCKBEREICH_RECHTECK, 5000.0) + + self.assertIs(result_extent, extent) + self.assertIsNone(result_layer) + mock_buffer_extent.assert_not_called() + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/ui/tabs/print_tab.py b/ui/tabs/print_tab.py index 10e164f..b7434e5 100644 --- a/ui/tabs/print_tab.py +++ b/ui/tabs/print_tab.py @@ -34,6 +34,7 @@ from sn_basis.modules.print_logic import ( DRUCKPUFFER_VAR, DRUCKPUFFER_STANDARD_CM, DRUCKBEREICH_VERFAHRENSGEBIET, + DRUCKBEREICH_WIE_KARTENFENSTER, DRUCKBEREICH_AKTUELLE_AUSWAHL, DRUCKBEREICH_RECHTECK, ) @@ -51,6 +52,7 @@ FORMFAKTOR_UI_VAR = "tab_b_formfaktor" ATLAS_ERZEUGEN_VAR = "tab_b_atlas_erzeugen" DRUCKBEREICH_OPTIONEN = [ DRUCKBEREICH_VERFAHRENSGEBIET, + DRUCKBEREICH_WIE_KARTENFENSTER, DRUCKBEREICH_AKTUELLE_AUSWAHL, DRUCKBEREICH_RECHTECK, ]