""" sn_plan41/ui/layout.py – Aufbau von Drucklayouts für Plan41. """ from __future__ import annotations import math from typing import Any from sn_basis.functions.qt_wrapper import QFont from sn_basis.functions.qgiscore_wrapper import ( QgsLayoutItem, QgsLayoutItemLabel, QgsLayoutItemMap, QgsLayoutPoint, QgsLayoutSize, QgsPrintLayout, QgsProject, QgsUnitTypes, ) from sn_basis.functions.qgisui_wrapper import open_layout_designer MM = QgsUnitTypes.LayoutMillimeters class Layout: """Erzeugt ein QGIS-Layout für den Druck.""" _TEXT_FORMAT_TEMPLATES = { "Format_1": { "font_family": "New Helvetica roman", "font_size": 10, "font_style": "normal", }, "Format_2": { "font_family": "New Helvetica roman", "font_size": 14, "font_style": "fett", }, "Format_3": { "font_family": "New Helvetica roman", "font_size": 10, "font_style": "normal", }, } _OBJECT_TEMPLATES = { "hauptkarte": { "id": "Hauptkarte", }, "quellenangabe": { "id": "Quellenangabe", "size_mm": (180.0, 100.0), "format": "Format_1", }, "seitenzahl": { "id": "Seitenzahl", "size_mm": (60.0, 8.0), "format": "Format_1", }, "textfeld": { "id": "Textfeld", "size_mm": (60.0, 8.0), "format": "Format_1", "html_mode": True, "text": "", }, } _QUELLENANGABE_TEXT = ( "Quelle Geobasisdaten: GeoSN, " "dl-de/by-2-0
" "Quelle Fachdaten: Darstellung auf der Grundlage von Daten und mit Erlaubnis des " "Sächsischen Landesamtes für Umwelt, Landwirtschaft und Geologie
" "Basemap:" "© GeoBasis-DE / BKG ([%year($now)%]) " "CC BY 4.0 " "mit teilweise angepasster Signatur
" ) _SEITENZAHL_TEXT = "Seite [% attribute(@atlas_feature, 'Seitenzahl') %] von [% @atlas_totalfeatures %]" def __init__(self, project: Any | None = None) -> None: self.project = project or QgsProject.instance() self._current_layout: Any | None = None def _create_map_item( self, layout: Any, template_name: str, x_mm: float, y_mm: float, width_mm: float, height_mm: float, ) -> Any: template = self._OBJECT_TEMPLATES[template_name] map_item = QgsLayoutItemMap(layout) map_item.setId(template["id"]) layout.addLayoutItem(map_item) map_item.attemptMove(QgsLayoutPoint(x_mm, y_mm, MM)) map_item.attemptResize(QgsLayoutSize(width_mm, height_mm, MM)) return map_item def add_text_label( self, text: str, x_mm: float, y_mm: float, template_name: str = "textfeld", text_format: str | None = None, width_mm: float | None = None, height_mm: float | None = None, html_mode: bool = False, expression_enabled: bool = False, ) -> Any: """Einfache öffentliche Methode zum Hinzufügen eines Text-Labels. Beispiel: add_text_label("Ausgabedatum: 2026-03-23", 100, 200, text_format="Format_1") """ if self._current_layout is None: raise RuntimeError("Kein Layout aktiv. Bitte create_single_page_layout() oder create_atlas_layout() aufrufen.") return self._create_label_item( self._current_layout, template_name, text, x_mm, y_mm, text_format=text_format, width_mm=width_mm, height_mm=height_mm, html_mode=html_mode, expression_enabled=expression_enabled, ) def _setup_layout_objects(self, kartenfenster_untere_rechte_ecke_mm: tuple, is_atlas: bool = False) -> None: """Zentrale Methode zum Hinzufügen aller Standard-Objekte ins Layout. Diese Methode wird sowohl von create_single_page_layout() als auch von create_atlas_layout() aufgerufen. Args: kartenfenster_untere_rechte_ecke_mm: Tupel (x_mm, y_mm) der unteren rechten Kartenecke is_atlas: True für Atlas-Layout, False für Einzelblatt-Layout """ if self._current_layout is None: raise RuntimeError("Kein Layout aktiv.") mode_html = getattr(QgsLayoutItemLabel, "ModeHtml", None) print(f"[Layout] QgsLayoutItemLabel.ModeHtml={mode_html!r}") lower_left = getattr(getattr(QgsLayoutItem, "ReferencePoint", object), "LowerLeft", None) print(f"[Layout] QgsLayoutItem.ReferencePoint.LowerLeft={lower_left!r}") # Quellenangabe: unterschiedliche Positionen für Einzelblatt vs. Atlas if is_atlas: quellenangabe_x = kartenfenster_untere_rechte_ecke_mm[0] + 5.0 quellenangabe_y = kartenfenster_untere_rechte_ecke_mm[1] else: quellenangabe_x = kartenfenster_untere_rechte_ecke_mm[0] + 10.0 quellenangabe_y = kartenfenster_untere_rechte_ecke_mm[1] - 120.0 self._create_label_item( self._current_layout, "quellenangabe", self._QUELLENANGABE_TEXT, quellenangabe_x, quellenangabe_y, html_mode=True, ) print("[Layout] Quellenangabe zum Layout hinzugefügt") # Seitenzahl nur bei Atlas-Layout if is_atlas: seitenzahl_x = kartenfenster_untere_rechte_ecke_mm[0] + 5.0 seitenzahl_y = kartenfenster_untere_rechte_ecke_mm[1] - 2.0 self._create_label_item( self._current_layout, "seitenzahl", self._SEITENZAHL_TEXT, seitenzahl_x, seitenzahl_y, expression_enabled=True, ) print("[Layout] Seitenzahl zum Layout hinzugefügt") def _create_label_item( self, layout: Any, template_name: str, text: str, x_mm: float, y_mm: float, *, text_format: str | None = None, font_name: str | None = None, font_size: int | None = None, font_style: str | None = None, width_mm: float | None = None, height_mm: float | None = None, html_mode: bool = False, expression_enabled: bool = False, ) -> Any: template = self._OBJECT_TEMPLATES[template_name] label = QgsLayoutItemLabel(layout) label.setId(template["id"]) label.setText(text) if html_mode: set_mode = getattr(label, "setMode", None) mode_html = getattr(QgsLayoutItemLabel, "ModeHtml", None) if callable(set_mode) and mode_html is not None: set_mode(mode_html) if expression_enabled: set_expr_enabled = getattr(label, "setExpressionEnabled", None) if callable(set_expr_enabled): try: set_expr_enabled(True) except Exception: pass format_name = text_format or template.get("format", "Format_1") format_template = self._TEXT_FORMAT_TEMPLATES.get(format_name, self._TEXT_FORMAT_TEMPLATES["Format_1"]) resolved_font_name = font_name or format_template["font_family"] resolved_font_size = font_size or format_template["font_size"] resolved_font_style = font_style or format_template["font_style"] font = QFont(resolved_font_name, resolved_font_size) if hasattr(font, "setBold"): font.setBold(resolved_font_style == "fett") label.setFont(font) set_reference_point = getattr(label, "setReferencePoint", None) lower_left = getattr(getattr(QgsLayoutItem, "ReferencePoint", object), "LowerLeft", None) if callable(set_reference_point) and lower_left is not None: set_reference_point(lower_left) template_width, template_height = template.get("size_mm", (0.0, 0.0)) label.attemptMove(QgsLayoutPoint(x_mm, y_mm, MM)) label.attemptResize( QgsLayoutSize( width_mm if width_mm is not None else template_width, height_mm if height_mm is not None else template_height, MM, ) ) layout.addLayoutItem(label) return label def create_single_page_layout( self, name: str, page_width_mm: float, page_height_mm: float, map_width_mm: float, map_height_mm: float, extent: Any, plotmassstab: float, thema: str = "", ) -> Any: """Erzeugt ein einseitiges Layout und öffnet es im Designer.""" print(f"[Layout] create_single_page_layout: name='{name}', " f"page=({page_width_mm}x{page_height_mm}), map=({map_width_mm:.1f}x{map_height_mm:.1f}), " f"massstab={plotmassstab}, thema='{thema}'") layout_manager = self.project.layoutManager() print(f"[Layout] layoutManager: {layout_manager!r}") existing_layout = layout_manager.layoutByName(name) if existing_layout is not None: raise ValueError(f"Eine Vorlage mit der Bezeichnung '{name}' existiert bereits.") layout = QgsPrintLayout(self.project) layout.initializeDefaults() layout.setName(name) self._current_layout = layout print(f"[Layout] QgsPrintLayout erstellt: {layout!r}") page = layout.pageCollection().page(0) page.setPageSize(QgsLayoutSize(page_width_mm, page_height_mm, MM)) print(f"[Layout] Seitengröße gesetzt: {page_width_mm}x{page_height_mm} mm") map_left_mm = 10.0 map_top_mm = 10.0 map_right_mm = map_left_mm + map_width_mm map_bottom_mm = map_top_mm + map_height_mm kartenfenster_untere_rechte_ecke_mm = (map_right_mm, map_bottom_mm) print( f"[Layout] Kartenbild-Kanten: rechts={map_right_mm:.1f} mm, unten={map_bottom_mm:.1f} mm" ) hauptkarte = self._create_map_item( layout, "hauptkarte", map_left_mm, map_top_mm, map_width_mm, map_height_mm, ) print(f"[Layout] QgsLayoutItemMap erstellt") print("[Layout] Hauptkarte zum Layout hinzugefügt") print(f"[Layout] Position und Größe gesetzt: pos=({map_left_mm}, {map_top_mm}), size=({map_width_mm:.1f}x{map_height_mm:.1f})") # Extent und Maßstab setzen x_min = getattr(extent, "xMinimum", lambda: float("nan"))() y_min = getattr(extent, "yMinimum", lambda: float("nan"))() x_max = getattr(extent, "xMaximum", lambda: float("nan"))() y_max = getattr(extent, "yMaximum", lambda: float("nan"))() print(f"[Layout] Extent input: xmin={x_min}, ymin={y_min}, xmax={x_max}, ymax={y_max}") if extent is not None and hasattr(extent, "isNull") and callable(extent.isNull) and not extent.isNull(): try: hauptkarte.setExtent(extent) print(f"[Layout] setExtent() erfolgreich") except Exception as exc: print(f"[Layout] WARN: setExtent() fehlgeschlagen: {exc}") else: print(f"[Layout] WARN: Extent nicht gültig/callable, setExtent übersprungen") # Maßstab setzen (NACH Extent) if isinstance(plotmassstab, (int, float)) and math.isfinite(plotmassstab) and plotmassstab > 0: try: hauptkarte.setScale(plotmassstab) print(f"[Layout] setScale({plotmassstab}) erfolgreich") except Exception as exc: print(f"[Layout] WARN: setScale({plotmassstab}) fehlgeschlagen: {exc}") else: print(f"[Layout] WARN: ungültiger plotmassstab={plotmassstab!r}, setScale übersprungen") # Rahmen aktivieren set_frame_enabled = getattr(hauptkarte, "setFrameEnabled", None) if callable(set_frame_enabled): try: set_frame_enabled(True) print(f"[Layout] Rahmen aktiviert") except Exception as exc: print(f"[Layout] WARN: setFrameEnabled fehlgeschlagen: {exc}") set_frame_stroke_width = getattr(hauptkarte, "setFrameStrokeWidth", None) if callable(set_frame_stroke_width): try: set_frame_stroke_width(0.5) print(f"[Layout] Rahmenstrichbreite auf 0.5 mm gesetzt") except Exception as exc: print(f"[Layout] WARN: setFrameStrokeWidth(0.5) fehlgeschlagen: {exc}") # Kartenthema setzen if thema and thema != "aktuell": follow_theme = getattr(hauptkarte, "setFollowVisibilityPreset", None) set_theme_name = getattr(hauptkarte, "setFollowVisibilityPresetName", None) if callable(follow_theme): try: follow_theme(True) print(f"[Layout] setFollowVisibilityPreset(True)") except Exception as exc: print(f"[Layout] WARN: setFollowVisibilityPreset fehlgeschlagen: {exc}") if callable(set_theme_name): try: set_theme_name(thema) print(f"[Layout] Kartenthema auf '{thema}' gesetzt") except Exception as exc: print(f"[Layout] WARN: setFollowVisibilityPresetName('{thema}') fehlgeschlagen: {exc}") else: print(f"[Layout] Kartenthema nicht gesetzt (thema='{thema}')") # Erst nach allen Properties: LayerSet einfrieren für Export set_keep_layer_set = getattr(hauptkarte, "setKeepLayerSet", None) if callable(set_keep_layer_set): try: set_keep_layer_set(True) print("[Layout] setKeepLayerSet(True) – Layerset für Export erhalten") except Exception as exc: print(f"[Layout] WARN: setKeepLayerSet fehlgeschlagen: {exc}") # Refresh-Strategie (optional, nur wenn vorhanden) set_refresh_strategy = getattr(hauptkarte, "setRefreshStrategy", None) if callable(set_refresh_strategy): refresh_cache = getattr(hauptkarte, "RefreshLaterOnly", None) or getattr(hauptkarte, "RefreshWhenRequested", None) or 0 if refresh_cache is not None: try: set_refresh_strategy(refresh_cache) print(f"[Layout] setRefreshStrategy({refresh_cache}) gesetzt") except Exception as exc: print(f"[Layout] WARN: setRefreshStrategy fehlgeschlagen: {exc}") print(f"[Layout] Hauptkarte vollständig konfiguriert") self._setup_layout_objects(kartenfenster_untere_rechte_ecke_mm, is_atlas=False) #Kartenschild-Box #Kartenschild-Linien #Kartenschild-Text #Herausgeber #Bezeichnung_Flurbereinigung #Name Flurbereinigung #Stadt_Gemeinde #Landkreis #Kartenname #Ergänzung(Seitenzahl) #gefertigt #geprüft #Maßstab #Ausgabedatum #Aktenzeichen layout_manager.addLayout(layout) print("[Layout] Layout zum LayoutManager hinzugefügt") self._current_layout = None open_layout_designer(layout) print("[Layout] Layout Designer geöffnet") return layout def create_atlas_layout( self, name: str, page_width_mm: float, page_height_mm: float, map_width_mm: float, map_height_mm: float, extent: Any, plotmassstab: float, atlas_layer: Any, thema: str = "", ) -> Any: """Erzeugt ein Atlas-Layout mit Coverage-Layer ``Atlasobjekte``.""" layout_manager = self.project.layoutManager() existing_layout = layout_manager.layoutByName(name) if existing_layout is not None: raise ValueError(f"Eine Vorlage mit der Bezeichnung '{name}' existiert bereits.") layout = QgsPrintLayout(self.project) layout.initializeDefaults() layout.setName(name) self._current_layout = layout page = layout.pageCollection().page(0) page.setPageSize(QgsLayoutSize(page_width_mm, page_height_mm, MM)) # Verifiziere, dass QGIS die Größe akzeptiert hat page_size = page.pageSize() if hasattr(page, 'pageSize') else None if page_size is not None and hasattr(page_size, 'width'): actual_w = float(page_size.width()) actual_h = float(page_size.height()) print(f"[Layout] Atlas Page gesetzt: x=0, y=0, width={page_width_mm:.1f}mm→{actual_w:.1f}mm, height={page_height_mm:.1f}mm→{actual_h:.1f}mm") else: print(f"[Layout] Atlas Page: x=0, y=0, width={page_width_mm:.1f}mm, height={page_height_mm:.1f}mm") map_left_mm = 10.0 map_top_mm = 10.0 map_right_mm = map_left_mm + map_width_mm map_bottom_mm = map_top_mm + map_height_mm kartenfenster_untere_rechte_ecke_mm = (map_right_mm, map_bottom_mm) hauptkarte = self._create_map_item( layout, "hauptkarte", map_left_mm, map_top_mm, map_width_mm, map_height_mm, ) # Verifiziere mit Units-bewussten Methoden (rect() kann andere Units verwenden). actual_w = None actual_h = None actual_x = None actual_y = None # Versuche zuerst, die Größe mit Unit-Methoden zu lesen try: if hasattr(hauptkarte, 'sizeWithUnits'): size_item = hauptkarte.sizeWithUnits() if hasattr(size_item, 'width') and hasattr(size_item, 'height'): actual_w = float(size_item.width()) actual_h = float(size_item.height()) except Exception: pass try: if hasattr(hauptkarte, 'positionWithUnits'): pos_item = hauptkarte.positionWithUnits() if hasattr(pos_item, 'x') and hasattr(pos_item, 'y'): actual_x = float(pos_item.x()) actual_y = float(pos_item.y()) except Exception: pass # Fallback: nutze rect() und teile durch UnitFaktor, falls nötig if actual_w is None or actual_h is None: try: actual_rect = hauptkarte.rect() if actual_rect is not None: actual_w = float(actual_rect.width()) actual_h = float(actual_rect.height()) actual_x = float(actual_rect.x()) actual_y = float(actual_rect.y()) except Exception: pass if actual_w is not None and actual_h is not None: print(f"[Layout] Atlas Hauptkarte gesetzt: x={map_left_mm:.1f}mm→{actual_x:.1f}mm, y={map_top_mm:.1f}mm→{actual_y:.1f}mm, width={map_width_mm:.1f}mm→{actual_w:.1f}mm, height={map_height_mm:.1f}mm→{actual_h:.1f}mm") else: print(f"[Layout] Atlas Hauptkarte: x={map_left_mm:.1f}mm, y={map_top_mm:.1f}mm, width={map_width_mm:.1f}mm, height={map_height_mm:.1f}mm") if extent is not None and hasattr(extent, "isNull") and callable(extent.isNull) and not extent.isNull(): try: hauptkarte.setExtent(extent) except Exception: pass if isinstance(plotmassstab, (int, float)) and math.isfinite(plotmassstab) and plotmassstab > 0: try: hauptkarte.setScale(plotmassstab) except Exception: pass set_frame_enabled = getattr(hauptkarte, "setFrameEnabled", None) if callable(set_frame_enabled): try: set_frame_enabled(True) except Exception: pass set_frame_stroke_width = getattr(hauptkarte, "setFrameStrokeWidth", None) if callable(set_frame_stroke_width): try: set_frame_stroke_width(0.5) except Exception: pass if thema and thema != "aktuell": follow_theme = getattr(hauptkarte, "setFollowVisibilityPreset", None) set_theme_name = getattr(hauptkarte, "setFollowVisibilityPresetName", None) if callable(follow_theme): try: follow_theme(True) except Exception: pass if callable(set_theme_name): try: set_theme_name(thema) except Exception: pass set_atlas_driven = getattr(hauptkarte, "setAtlasDriven", None) if callable(set_atlas_driven): try: set_atlas_driven(True) except Exception: pass # Fester Atlas-Maßstab: plotmassstab bleibt unverändert. set_scaling_mode = getattr(hauptkarte, "setAtlasScalingMode", None) if callable(set_scaling_mode): fixed_mode = getattr(QgsLayoutItemMap, "Fixed", None) if fixed_mode is not None: try: set_scaling_mode(fixed_mode) except Exception: pass set_atlas_margin = getattr(hauptkarte, "setAtlasMargin", None) if callable(set_atlas_margin): try: set_atlas_margin(0.0) except Exception: pass set_keep_layer_set = getattr(hauptkarte, "setKeepLayerSet", None) if callable(set_keep_layer_set): try: set_keep_layer_set(True) except Exception: pass # Sicherheit: Größe nochmal nach Atlas-Konfiguration setzen, um sicherzustellen, dass sie nicht von der Atlas-Einstellung überschrieben wurde. hauptkarte.attemptResize(QgsLayoutSize(map_width_mm, map_height_mm, MM)) print(f"[Layout] Atlas Hauptkarte Größe nach Atlas-Konfiguration erneut gesetzt: {map_width_mm:.1f}mm × {map_height_mm:.1f}mm") self._setup_layout_objects(kartenfenster_untere_rechte_ecke_mm, is_atlas=True) atlas = layout.atlas() if atlas is not None: set_enabled = getattr(atlas, "setEnabled", None) set_coverage = getattr(atlas, "setCoverageLayer", None) set_hide_coverage = getattr(atlas, "setHideCoverage", None) set_filter_features = getattr(atlas, "setFilterFeatures", None) set_page_name = getattr(atlas, "setPageNameExpression", None) if callable(set_enabled): set_enabled(True) if callable(set_coverage): set_coverage(atlas_layer) if callable(set_hide_coverage): set_hide_coverage(True) if callable(set_filter_features): set_filter_features(False) if callable(set_page_name): set_page_name("attribute(@atlas_feature, 'Seitenzahl')") layout_manager.addLayout(layout) print("[Layout] Layout zum LayoutManager hinzugefügt") self._current_layout = None open_layout_designer(layout) print("[Layout] Layout Designer geöffnet") return layout