""" sn_basis/modules/print_layout_baufreigabe.py - Layout-Erstellung fuer Baufreigabe-Vorlagen. Baufreigabe-Layouts verwenden immer feste DIN A3/A4 Seitengroessen im Querformat, ein 120 mm breites Kartenschild (statt 180 mm) und erbenerben die Standard-Logik fuer Hauptkarte, Quellenangabe und Legende vom PrintLayout. Atlas-Integration erfolgt nach Layouterstellung durch externe Konfiguration in der PrintLogic. """ from __future__ import annotations from typing import Any from sn_basis.functions.qt_wrapper import QFont from sn_basis.functions.qgiscore_wrapper import ( QgsLayoutItem, QgsLayoutItemLabel, QgsLayoutItemLegend, QgsLayoutItemMap, QgsLayoutPoint, QgsLayoutSize, QgsPrintLayout, QgsProject, QgsUnitTypes, ) from sn_basis.functions.qgisui_wrapper import open_layout_designer from sn_basis.modules.print_layout import PrintLayout MM = QgsUnitTypes.LayoutMillimeters class PrintLayoutBaufreigabe(PrintLayout): """Erzeugt QGIS-Einzelblatt-Layouts mit fester DIN A3/A4 Querformat-Groesse und 80mm Kartenschild.""" _FRAME_STROKE_WIDTH_MM = 0.5 _KARTENSCHILD_WIDTH_MM = 80.0 _KARTENSCHILD_HEIGHT_MM = 92.0 _ATLAS_FRAME_MARGIN_MM = 10.0 _GRID_INTERVAL_MAP_UNITS = 500.0 # DIN-Formate: nur A3 und A4 im Querformat _DIN_A3_LANDSCAPE = (420, 297) # Breite x Hoehe (Querformat) _DIN_A4_LANDSCAPE = (297, 210) # Querformat _TEXT_FORMAT_TEMPLATES = { "Format_1": {"font_family": "Arial", "font_size": 11, "font_style": "normal"}, "Format_2": {"font_family": "Arial", "font_size": 13, "font_style": "bold"}, "Format_3": {"font_family": "Arial", "font_size": 16, "font_style": "bold"}, } _OBJECT_TEMPLATES = { "hauptkarte": {"id": "Hauptkarte", "object_name": "Hauptkarte"}, "quellenangabe": { "id": "Quellenangabe", "object_name": "Quellenangabe", "size_mm": (80.0, 100.0), "format": "Format_1", }, "textfeld": { "id": "Textfeld", "object_name": "Textfeld", "size_mm": (60.0, 8.0), "format": "Format_1", "html_mode": True, "text": "", }, "legende": { "id": "Legende", "object_name": "Legende", "size_mm": (80.0, 60.0), }, } _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
" ) def __init__(self, project: Any | None = None) -> None: super().__init__(project) def create_baufreigabe_layout( self, name: str, din_format: str, map_width_mm: float, map_height_mm: float, extent: Any, plotmassstab: float, thema: str = "", fit_extent_to_map: bool = True, ) -> Any: """Erstellt ein Baufreigabe-Layout mit fester DIN A3/A4 Querformat-Groesse. Args: name: Name des Layouts din_format: "DIN A3" oder "DIN A4" map_width_mm: Breite der Hauptkarte map_height_mm: Hoehe der Hauptkarte 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) Returns: QgsPrintLayout objektInstance (wird im Layout-Dialog geoeffnet) Raises: ValueError: Falls Name bereits existiert oder Format ungueltig ist """ # Seitenformat auswaehlen (nur A3/A4 im Querformat) if din_format == "DIN A3": page_width_mm, page_height_mm = self._DIN_A3_LANDSCAPE elif din_format == "DIN A4": page_width_mm, page_height_mm = self._DIN_A4_LANDSCAPE else: raise ValueError(f"Baufreigabe: Format {din_format} ist nicht unterstuetzt. Nur DIN A3/A4 Querformat.") 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)) 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, ) _ = fit_extent_to_map if extent is not None and hasattr(extent, "isNull") and callable(extent.isNull) and not extent.isNull(): try: hauptkarte.zoomToExtent(extent) except Exception: pass if isinstance(plotmassstab, (int, float)) and plotmassstab > 0: try: hauptkarte.setScale(plotmassstab) 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 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_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: self._set_item_frame_stroke_width(hauptkarte, self._FRAME_STROKE_WIDTH_MM) 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_keep_layer_set = getattr(hauptkarte, "setKeepLayerSet", None) if callable(set_keep_layer_set): try: set_keep_layer_set(True) except Exception: pass 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 ) try: set_refresh_strategy(refresh_cache) except Exception: pass self._configure_hauptkarte_grids(hauptkarte) self._setup_layout_objects_baufreigabe( kartenfenster_untere_rechte_ecke_mm, hauptkarte, map_top_mm, map_height_mm, ) layout_manager.addLayout(layout) self._current_layout = None open_layout_designer(layout) return layout def _setup_layout_objects_baufreigabe( self, kartenfenster_untere_rechte_ecke_mm: tuple, hauptkarte: Any, map_top_mm: float, map_height_mm: float, ) -> None: """Erstellt Legende, Quellenangabe und Kartenschild fuer Baufreigabe-Layout. Angepasst fuer 80mm Kartenschild statt 180mm. """ if self._current_layout is None: raise RuntimeError("Kein Layout aktiv.") def _coords_rel_untere_rechte_ecke(rel_x_mm: float, rel_y_mm: float) -> tuple[float, float]: return ( kartenfenster_untere_rechte_ecke_mm[0] + rel_x_mm, kartenfenster_untere_rechte_ecke_mm[1] + rel_y_mm, ) # Legende: 80mm Breite, hoehe adaptive wie Standard legende_x = kartenfenster_untere_rechte_ecke_mm[0] + 10.0 legende_height_mm = max(map_height_mm - 122.0, 20.0) legende_y = map_top_mm legende = self._create_legend_item( self._current_layout, "legende", hauptkarte, legende_x, legende_y, width_mm=self._KARTENSCHILD_WIDTH_MM, height_mm=legende_height_mm, object_name="Legende", ) legende_hoehe_mm = self._configure_legend_size( legende, width_limit_mm=self._KARTENSCHILD_WIDTH_MM, column_count=1, equal_column_width=False, ) # Quellenangabe: 80mm Breite quellenangabe_x, quellenangabe_y = _coords_rel_untere_rechte_ecke(10.0, -102.0) quellenangabe_hoehe_mm = 20.0 quellenangabe_obere_y = quellenangabe_y - quellenangabe_hoehe_mm legende_untere_y = legende_y + legende_hoehe_mm if legende_untere_y > quellenangabe_obere_y: self._configure_legend_size( legende, width_limit_mm=self._KARTENSCHILD_WIDTH_MM, column_count=2, equal_column_width=True, ) self.add_text_label( self._QUELLENANGABE_TEXT, quellenangabe_x, quellenangabe_y, width_mm=self._KARTENSCHILD_WIDTH_MM, height_mm=20.0, text_format="Format_1", html_mode=True, expression_enabled=True, object_name="Quellenangabe", ) # Kartenschild: 80mm Breite kartenschild_x, kartenschild_y = _coords_rel_untere_rechte_ecke(10.0, 0.0) self._create_rectangle_box_item( self._current_layout, "Kartenschild", kartenschild_x, kartenschild_y, self._KARTENSCHILD_WIDTH_MM, self._KARTENSCHILD_HEIGHT_MM, stroke_width_mm=self._FRAME_STROKE_WIDTH_MM, ) # Kartenschild-Linien: 6 Trennlinien (Positionen gleich) kartenschild_line_x, _ = _coords_rel_untere_rechte_ecke(10.0, 0.0) kartenschild_line_offsets_y = (-10.0, -20.0, -30.0, -46.0, -62.0, -82.0) for index, offset_y in enumerate(kartenschild_line_offsets_y, start=1): _, line_y = _coords_rel_untere_rechte_ecke(0.0, offset_y) self._create_rectangle_box_item( self._current_layout, f"Kartenschild_Linie_{index}", kartenschild_line_x, line_y, self._KARTENSCHILD_WIDTH_MM, 0.0, stroke_width_mm=0.1, ) # Textfelder im Kartenschild ohne Skalierungslogik. # Positionen werden bewusst als feste Werte gesetzt und bei Bedarf manuell angepasst. labels = [ ("Teilnehmergemeinschaft [%@sn_bezeichnung %] [%@sn_name %]", 14.0, -84.0, 74.0, 8.0, "Format_1", "Herausgeber"), ("[% @sn_bezeichnung %]", 14.0, -75.0, 74.0, 4.0, "Format_1", "Bezeichnung_Flurbereinigung"), ("[% @sn_name %]", 14.0, -64.0, 74.0, 5.0, "Format_2", "Name_Flurbereinigung"), ("[% @sn_gemeinden %]", 14.0, -56.0, 74.0, 4.0, "Format_1", "Stadt_Gemeinde"), ("[% @sn_landkreise_proj %]", 14.0, -48.0, 74.0, 4.0, "Format_1", "Landkreis"), ("[% @sn_kartenname %]", 14.0, -32.0, 74.0, 8.0, "Format_3", "Kartenname"), ("Ausgabedatum: [%format_date(now(), 'dd.MM.yyyy')%]", 14.0, -13.0, 74.0, 4.0, "Format_1", "Ausgabedatum"), ( "Massstab: 1 : [%round(map_get(item_variables('Hauptkarte'),'map_scale'),0)%]", 16.0, -23.0, 74.0, 4.0, "Format_1", "Massstab", ), ] for text, rx, ry, w, h, fmt, obj_name in labels: x, y = _coords_rel_untere_rechte_ecke(rx, ry) self.add_text_label( text, x, y, width_mm=w, height_mm=h, text_format=fmt, expression_enabled=True, object_name=obj_name, ) def _calc_atlas_margin_percent(self, map_width_mm: float, map_height_mm: float) -> float: """Berechnet Atlas-Margin in Prozent, sodass etwa 10 mm Rand sichtbar bleiben.""" margin_mm = self._ATLAS_FRAME_MARGIN_MM if map_width_mm <= 2 * margin_mm or map_height_mm <= 2 * margin_mm: return 5.0 p_width = (100.0 * margin_mm) / (map_width_mm - 2.0 * margin_mm) p_height = (100.0 * margin_mm) / (map_height_mm - 2.0 * margin_mm) return max(p_width, p_height)