diff --git a/ui/layout.py b/ui/layout.py index 4aa52c8..a2ec82f 100644 --- a/ui/layout.py +++ b/ui/layout.py @@ -4,12 +4,14 @@ sn_plan41/ui/layout.py – Aufbau von Drucklayouts für Plan41. from __future__ import annotations import math +import importlib 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, @@ -26,8 +28,816 @@ MM = QgsUnitTypes.LayoutMillimeters class Layout: """Erzeugt ein QGIS-Layout für den Druck.""" + _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": (180.0, 100.0), + "format": "Format_1", + }, + "seitenzahl": { + "id": "Seitenzahl", + "object_name": "Seitenzahl", + "size_mm": (60.0, 8.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": (180.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
" + ) + + _SEITENZAHL_TEXT = "Blatt [% 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, + object_name: str | None = None, + ) -> Any: + template = self._OBJECT_TEMPLATES[template_name] + map_item = QgsLayoutItemMap(layout) + map_item.setId(template["id"]) + set_object_name = getattr(map_item, "setObjectName", None) + if callable(set_object_name): + set_object_name(object_name or template.get("object_name", 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 _set_item_frame_stroke_width(self, item: Any, stroke_width_mm: float) -> None: + set_frame_stroke_width = getattr(item, "setFrameStrokeWidth", None) + if not callable(set_frame_stroke_width): + return + + try: + set_frame_stroke_width(stroke_width_mm) + return + except Exception: + pass + + try: + qgis_core = importlib.import_module("qgis.core") + QgsLayoutMeasurement = getattr(qgis_core, "QgsLayoutMeasurement", None) + except Exception: + QgsLayoutMeasurement = None + + if QgsLayoutMeasurement is not None: + try: + set_frame_stroke_width(QgsLayoutMeasurement(stroke_width_mm, MM)) + return + except Exception: + pass + + frame_stroke_width = getattr(item, "frameStrokeWidth", None) + if callable(frame_stroke_width): + try: + measurement = frame_stroke_width() + set_length = getattr(measurement, "setLength", None) + if callable(set_length): + set_length(stroke_width_mm) + set_frame_stroke_width(measurement) + except Exception: + pass + + 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, + object_name: str | None = None, + ) -> 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, + object_name=object_name, + ) + + def _setup_layout_objects( + self, + kartenfenster_untere_rechte_ecke_mm: tuple, + hauptkarte: Any, + map_top_mm: float, + map_height_mm: float, + 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. + Relatipunkt des Objekts ist die untere linke Ecke, berechnet von der unteren, rechten Ecke des Kartefensters + 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.") + + 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, + ) + + 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}") + + 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=180.0, + height_mm=legende_height_mm, + object_name="Legende", + ) + + legende_hoehe_mm = self._configure_legend_size( + legende, + width_limit_mm=180.0, + column_count=1, + equal_column_width=False, + ) + + 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: + legende_hoehe_mm = self._configure_legend_size( + legende, + width_limit_mm=180.0, + column_count=2, + equal_column_width=True, + ) + print("[Layout] Legende überlappt Quellenangabe – auf 2 Spalten umgestellt") + + print("[Layout] Legende zum Layout hinzugefügt") + + # Quellenangabe: + + self.add_text_label( + self._QUELLENANGABE_TEXT, + quellenangabe_x, + quellenangabe_y, + width_mm=180.0, + height_mm=20.0, + text_format="Format_1", + html_mode=True, + expression_enabled=True, + object_name="Quellenangabe", + ) + print("[Layout] Quellenangabe zum Layout hinzugefügt") + # Kartenschild-Box, -Linien und -Text werden hier ergänzt (TODO) + #Kartenschild-Box + 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, + 180.0, + 92.0, + stroke_width_mm=1.0, + ) + print("[Layout] Kartenschild-Box zum Layout hinzugefügt") + + #Kartenschild-Linien + 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, + 180.0, + 0.0, + stroke_width_mm=0.1, + ) + print("[Layout] 6 Kartenschild-Linien zum Layout hinzugefügt") + + #Kartenschild-Text + #Herausgeber + herausgeber_x, herausgeber_y = _coords_rel_untere_rechte_ecke(16.0, -84.0) + self.add_text_label( + "[% @sn_amt %]", + herausgeber_x, + herausgeber_y, + width_mm=171.0, + height_mm=4.0, + text_format="Format_1", + expression_enabled=True, + object_name="Herausgeber", + ) + #Bezeichnung_Flurbereinigung + bezeichnung_flurbereinigung_x, bezeichnung_flurbereinigung_y = _coords_rel_untere_rechte_ecke(16.0, -75.0) + self.add_text_label( + "[% @sn_bezeichnung %]", + bezeichnung_flurbereinigung_x, + bezeichnung_flurbereinigung_y, + width_mm=171.0, + height_mm=4.0, + text_format="Format_1", + expression_enabled=True, + object_name="Bezeichnung_Flurbereinigung", + ) + + #Name Flurbereinigung + name_flurbereinigung_x, name_flurbereinigung_y = _coords_rel_untere_rechte_ecke(16.0, -64.0) + self.add_text_label( + "[% @sn_name %]", + name_flurbereinigung_x, + name_flurbereinigung_y, + width_mm=171.0, + height_mm=5.0, + text_format="Format_2", + expression_enabled=True, + object_name="Name_Flurbereinigung", + ) + + #Stadt_Gemeinde + stadt_gemeinde_x, stadt_gemeinde_y = _coords_rel_untere_rechte_ecke(16.0, -56.0) + self.add_text_label( + "[% @sn_gemeinden %]", + stadt_gemeinde_x, + stadt_gemeinde_y, + width_mm=171.0, + height_mm=4.0, + text_format="Format_1", + expression_enabled=True, + object_name="Stadt_Gemeinde", + ) + + #Landkreis + landkreis_x, landkreis_y = _coords_rel_untere_rechte_ecke(16.0, -48.0) + self.add_text_label( + "[% @sn_landkreise_proj %]", + landkreis_x, + landkreis_y, + width_mm=171.0, + height_mm=4.0, + text_format="Format_1", + expression_enabled=True, + object_name="Landkreis", + ) + + #Kartenname + kartenname_x, kartenname_y = _coords_rel_untere_rechte_ecke(16.0, -34.0) + self.add_text_label( + "[% @sn_kartenname %]", + kartenname_x, + kartenname_y, + width_mm=137.0, + height_mm=5.0, + text_format="Format_3", + expression_enabled=True, + object_name="Kartenname", + ) + + #Ergänzung(Seitenzahl) + + #gefertigt + gefertigt_x, gefertigt_y = _coords_rel_untere_rechte_ecke(16.0, -23.0) + self.add_text_label( + "Gefertigt: ", + gefertigt_x, + gefertigt_y, + width_mm=81.0, + height_mm=4.0, + text_format="Format_1", + expression_enabled=True, + object_name="Gefertigt", + ) + #geprüft + geprüft_x, geprüft_y = _coords_rel_untere_rechte_ecke(101.0, -23.0) + self.add_text_label( + "Geprüft: ", + geprüft_x, + geprüft_y, + width_mm=81.0, + height_mm=4.0, + text_format="Format_1", + expression_enabled=True, + object_name="Geprüft", + ) + #Maßstab + maßstab_x, maßstab_y = _coords_rel_untere_rechte_ecke(16.0, -13.0) + self.add_text_label( + "Maßstab: 1 : [%round(map_get(item_variables('Hauptkarte'),'map_scale'),0)%]", + maßstab_x, + maßstab_y, + width_mm=81.0, + height_mm=4.0, + text_format="Format_1", + expression_enabled=True, + object_name="Maßstab", + ) + #Ausgabedatum + ausgabedatum_x, ausgabedatum_y = _coords_rel_untere_rechte_ecke(101.0, -13.0) + self.add_text_label( + "Ausgabedatum: [%format_date(now(), 'dd.MM.yyyy')%]", + ausgabedatum_x, + ausgabedatum_y, + width_mm=81.0, + height_mm=4.0, + text_format="Format_1", + expression_enabled=True, + object_name="Ausgabedatum", + ) + #Aktenzeichen + aktenzeichen_x, aktenzeichen_y = _coords_rel_untere_rechte_ecke(16.0, -3.0) + self.add_text_label( + "Aktenzeichen: ", + aktenzeichen_x, + aktenzeichen_y, + width_mm=171.0, + height_mm=4.0, + text_format="Format_1", + expression_enabled=True, + object_name="Aktenzeichen", + ) + # Seitenzahl nur bei Atlas-Layout + if is_atlas: + seitenzahl_x, seitenzahl_y = _coords_rel_untere_rechte_ecke(166.0, -33.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, + object_name: str | None = None, + ) -> Any: + template = self._OBJECT_TEMPLATES[template_name] + label = QgsLayoutItemLabel(layout) + item_id = object_name or template["id"] + label.setId(item_id) + set_object_name = getattr(label, "setObjectName", None) + if callable(set_object_name): + set_object_name(item_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 == "bold") + if hasattr(font, "setItalic"): + font.setItalic(resolved_font_style == "italic") + 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, + ) + ) + set_locked = getattr(label, "setLocked", None) + if callable(set_locked): + try: + set_locked(True) + except Exception: + pass + layout.addLayoutItem(label) + return label + + def _resolve_visible_map_layers(self, map_item: Any) -> list[Any]: + layers_method = getattr(map_item, "layers", None) + if callable(layers_method): + try: + resolved_layers = layers_method() + if isinstance(resolved_layers, (list, tuple)): + layers = [layer for layer in resolved_layers if layer is not None] + if layers: + return layers + except Exception: + pass + + layer_tree_root_method = getattr(self.project, "layerTreeRoot", None) + if not callable(layer_tree_root_method): + return [] + + try: + root = layer_tree_root_method() + except Exception: + return [] + + find_layers = getattr(root, "findLayers", None) + if not callable(find_layers): + return [] + + visible_layers: list[Any] = [] + try: + tree_layers = find_layers() + if not isinstance(tree_layers, (list, tuple)): + return [] + for tree_layer in tree_layers: + is_visible = getattr(tree_layer, "itemVisibilityChecked", None) + if callable(is_visible) and not is_visible(): + continue + layer = getattr(tree_layer, "layer", None) + resolved_layer = layer() if callable(layer) else None + if resolved_layer is not None: + visible_layers.append(resolved_layer) + except Exception: + return [] + + return visible_layers + + def _create_legend_item( + self, + layout: Any, + template_name: str, + linked_map: Any, + x_mm: float, + y_mm: float, + *, + width_mm: float | None = None, + height_mm: float | None = None, + object_name: str | None = None, + ) -> Any: + template = self._OBJECT_TEMPLATES[template_name] + legend = QgsLayoutItemLegend(layout) + item_id = object_name or template["id"] + legend.setId(item_id) + + set_object_name = getattr(legend, "setObjectName", None) + if callable(set_object_name): + set_object_name(item_id) + + set_linked_map = getattr(legend, "setLinkedMap", None) + if callable(set_linked_map): + try: + set_linked_map(linked_map) + except Exception: + pass + + set_title = getattr(legend, "setTitle", None) + if callable(set_title): + try: + set_title("Legende") + except Exception: + pass + + set_auto_update_model = getattr(legend, "setAutoUpdateModel", None) + if callable(set_auto_update_model): + try: + set_auto_update_model(False) + except Exception: + pass + + set_filter_by_map = getattr(legend, "setLegendFilterByMapEnabled", None) + if callable(set_filter_by_map): + try: + set_filter_by_map(False) + except Exception: + pass + + model_method = getattr(legend, "model", None) + if callable(model_method): + try: + model = model_method() + root_group_method = getattr(model, "rootGroup", None) + root_group = root_group_method() if callable(root_group_method) else None + clear = getattr(root_group, "clear", None) + if callable(clear): + clear() + add_layer = getattr(root_group, "addLayer", None) + if callable(add_layer): + for layer in self._resolve_visible_map_layers(linked_map): + add_layer(layer) + except Exception: + pass + + set_reference_point = getattr(legend, "setReferencePoint", None) + upper_left = getattr(getattr(QgsLayoutItem, "ReferencePoint", object), "UpperLeft", None) + if callable(set_reference_point) and upper_left is not None: + set_reference_point(upper_left) + + template_width, template_height = template.get("size_mm", (0.0, 0.0)) + legend.attemptMove(QgsLayoutPoint(x_mm, y_mm, MM)) + legend.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, + ) + ) + + set_frame_enabled = getattr(legend, "setFrameEnabled", None) + if callable(set_frame_enabled): + try: + set_frame_enabled(True) + except Exception: + pass + + self._set_item_frame_stroke_width(legend, 0.3) + + set_background_enabled = getattr(legend, "setBackgroundEnabled", None) + if callable(set_background_enabled): + try: + set_background_enabled(False) + except Exception: + pass + + set_locked = getattr(legend, "setLocked", None) + if callable(set_locked): + try: + set_locked(True) + except Exception: + pass + + layout.addLayoutItem(legend) + + refresh = getattr(legend, "refresh", None) + if callable(refresh): + try: + refresh() + except Exception: + pass + + return legend + + def _get_item_size_mm(self, item: Any) -> tuple[float | None, float | None]: + def _to_float(value: Any) -> float | None: + if isinstance(value, (int, float)): + return float(value) + if isinstance(value, str): + try: + return float(value) + except Exception: + return None + return None + + size_with_units = getattr(item, "sizeWithUnits", None) + if callable(size_with_units): + try: + size = size_with_units() + get_width = getattr(size, "width", None) + get_height = getattr(size, "height", None) + width = _to_float(get_width()) if callable(get_width) else None + height = _to_float(get_height()) if callable(get_height) else None + if width is not None and height is not None: + return width, height + except Exception: + pass + + rect = getattr(item, "rect", None) + if callable(rect): + try: + item_rect = rect() + get_width = getattr(item_rect, "width", None) + get_height = getattr(item_rect, "height", None) + width = _to_float(get_width()) if callable(get_width) else None + height = _to_float(get_height()) if callable(get_height) else None + return width, height + except Exception: + pass + + return None, None + + def _configure_legend_size( + self, + legend: Any, + *, + width_limit_mm: float, + column_count: int, + equal_column_width: bool, + ) -> float: + set_locked = getattr(legend, "setLocked", None) + if callable(set_locked): + try: + set_locked(False) + except Exception: + pass + + set_column_count = getattr(legend, "setColumnCount", None) + if callable(set_column_count): + try: + set_column_count(column_count) + except Exception: + pass + + set_equal_column_width = getattr(legend, "setEqualColumnWidth", None) + if callable(set_equal_column_width): + try: + set_equal_column_width(equal_column_width) + except Exception: + pass + + refresh = getattr(legend, "refresh", None) + if callable(refresh): + try: + refresh() + except Exception: + pass + + adjust_box_size = getattr(legend, "adjustBoxSize", None) + if callable(adjust_box_size): + try: + adjust_box_size() + except Exception: + pass + + width_mm, height_mm = self._get_item_size_mm(legend) + resolved_width = width_mm if width_mm is not None else width_limit_mm + resolved_height = height_mm if height_mm is not None else 20.0 + final_width = min(resolved_width, width_limit_mm) + + legend.attemptResize(QgsLayoutSize(final_width, resolved_height, MM)) + + if callable(refresh): + try: + refresh() + except Exception: + pass + + _, updated_height_mm = self._get_item_size_mm(legend) + + if callable(set_locked): + try: + set_locked(True) + except Exception: + pass + + return updated_height_mm if updated_height_mm is not None else resolved_height + + def _create_rectangle_box_item( + self, + layout: Any, + object_name: str, + x_mm: float, + y_mm: float, + width_mm: float, + height_mm: float, + stroke_width_mm: float = 0.3, + ) -> Any: + box = QgsLayoutItemLabel(layout) + box.setId(object_name) + + set_object_name = getattr(box, "setObjectName", None) + if callable(set_object_name): + set_object_name(object_name) + + box.setText("") + + set_reference_point = getattr(box, "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) + + box.attemptMove(QgsLayoutPoint(x_mm, y_mm, MM)) + box.attemptResize(QgsLayoutSize(width_mm, height_mm, MM)) + + set_frame_enabled = getattr(box, "setFrameEnabled", None) + if callable(set_frame_enabled): + try: + set_frame_enabled(True) + except Exception: + pass + + self._set_item_frame_stroke_width(box, stroke_width_mm) + + set_background_enabled = getattr(box, "setBackgroundEnabled", None) + if callable(set_background_enabled): + try: + set_background_enabled(False) + except Exception: + pass + + set_locked = getattr(box, "setLocked", None) + if callable(set_locked): + try: + set_locked(True) + except Exception: + pass + + layout.addLayoutItem(box) + return box def create_single_page_layout( self, @@ -53,6 +863,7 @@ class Layout: 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) @@ -63,21 +874,21 @@ class Layout: 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 = QgsLayoutItemMap(layout) - hauptkarte.setId("Hauptkarte") + hauptkarte = self._create_map_item( + layout, + "hauptkarte", + map_left_mm, + map_top_mm, + map_width_mm, + map_height_mm, + ) print(f"[Layout] QgsLayoutItemMap erstellt") - - # Zum Layout hinzufügen, BEVOR Eigenschaften gesetzt werden - layout.addLayoutItem(hauptkarte) print("[Layout] Hauptkarte zum Layout hinzugefügt") - - # Position und Größe setzen - hauptkarte.attemptMove(QgsLayoutPoint(map_left_mm, map_top_mm, MM)) - hauptkarte.attemptResize(QgsLayoutSize(map_width_mm, map_height_mm, MM)) 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 @@ -118,7 +929,7 @@ class Layout: set_frame_stroke_width = getattr(hauptkarte, "setFrameStrokeWidth", None) if callable(set_frame_stroke_width): try: - set_frame_stroke_width(0.5) + self._set_item_frame_stroke_width(hauptkarte, 0.5) print(f"[Layout] Rahmenstrichbreite auf 0.5 mm gesetzt") except Exception as exc: print(f"[Layout] WARN: setFrameStrokeWidth(0.5) fehlgeschlagen: {exc}") @@ -163,39 +974,18 @@ class Layout: print(f"[Layout] WARN: setRefreshStrategy fehlgeschlagen: {exc}") print(f"[Layout] Hauptkarte vollständig konfiguriert") - - quellenangabe = QgsLayoutItemLabel(layout) - quellenangabe.setId("Quellenangabe") - quellenangabe.setText( - "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
" + + self._setup_layout_objects( + kartenfenster_untere_rechte_ecke_mm, + hauptkarte, + map_top_mm, + map_height_mm, + is_atlas=False, ) - set_mode = getattr(quellenangabe, "setMode", None) - mode_html = getattr(QgsLayoutItemLabel, "ModeHtml", None) - print(f"[Layout] QgsLayoutItemLabel.ModeHtml={mode_html!r}") - if callable(set_mode) and mode_html is not None: - set_mode(mode_html) - quellenangabe.setFont(QFont("Arial", 12)) - set_reference_point = getattr(quellenangabe, "setReferencePoint", None) - lower_left = getattr(getattr(QgsLayoutItem, "ReferencePoint", object), "LowerLeft", None) - print(f"[Layout] QgsLayoutItem.ReferencePoint.LowerLeft={lower_left!r}") - if callable(set_reference_point) and lower_left is not None: - set_reference_point(lower_left) - quellenangabe_x_mm = map_right_mm + 20.0 - quellenangabe_y_mm = map_bottom_mm - 120.0 - quellenangabe.attemptMove(QgsLayoutPoint(quellenangabe_x_mm, quellenangabe_y_mm, MM)) - quellenangabe.attemptResize(QgsLayoutSize(180.0, 100.0, MM)) - layout.addLayoutItem(quellenangabe) - print("[Layout] Quellenangabe zum Layout hinzugefügt") - + 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 @@ -221,6 +1011,7 @@ class Layout: 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)) @@ -237,12 +1028,16 @@ class Layout: 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 = QgsLayoutItemMap(layout) - hauptkarte.setId("Hauptkarte") - layout.addLayoutItem(hauptkarte) - hauptkarte.attemptMove(QgsLayoutPoint(map_left_mm, map_top_mm, MM)) - hauptkarte.attemptResize(QgsLayoutSize(map_width_mm, map_height_mm, 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 @@ -308,7 +1103,7 @@ class Layout: set_frame_stroke_width = getattr(hauptkarte, "setFrameStrokeWidth", None) if callable(set_frame_stroke_width): try: - set_frame_stroke_width(0.5) + self._set_item_frame_stroke_width(hauptkarte, 0.5) except Exception: pass @@ -360,52 +1155,14 @@ class Layout: # 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") - - quellenangabe = QgsLayoutItemLabel(layout) - quellenangabe.setId("Quellenangabe") - quellenangabe.setText( - "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
" + + self._setup_layout_objects( + kartenfenster_untere_rechte_ecke_mm, + hauptkarte, + map_top_mm, + map_height_mm, + is_atlas=True, ) - set_mode = getattr(quellenangabe, "setMode", None) - mode_html = getattr(QgsLayoutItemLabel, "ModeHtml", None) - if callable(set_mode) and mode_html is not None: - set_mode(mode_html) - quellenangabe.setFont(QFont("Arial", 12)) - set_reference_point = getattr(quellenangabe, "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) - quellenangabe.attemptMove(QgsLayoutPoint(map_right_mm + 5.0, map_bottom_mm, MM)) - quellenangabe.attemptResize(QgsLayoutSize(180.0, 100.0, MM)) - layout.addLayoutItem(quellenangabe) - - seitenzahl_label = QgsLayoutItemLabel(layout) - seitenzahl_label.setId("Seitenzahl") - seitenzahl_label.setFont(QFont("Arial", 12)) - set_expr_enabled = getattr(seitenzahl_label, "setExpressionEnabled", None) - if callable(set_expr_enabled): - try: - set_expr_enabled(True) - except Exception: - pass - # Ausdrucksauswertung robust über [% ... %]-Platzhalter. - seitenzahl_label.setText( - "Seite [% attribute(@atlas_feature, 'Seitenzahl') %] von [% @atlas_totalfeatures %]" - ) - set_reference_point = getattr(seitenzahl_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) - seitenzahl_label.attemptMove(QgsLayoutPoint(map_right_mm + 5.0, map_bottom_mm - 2.0, MM)) - seitenzahl_label.attemptResize(QgsLayoutSize(60.0, 8.0, MM)) - layout.addLayoutItem(seitenzahl_label) atlas = layout.atlas() if atlas is not None: @@ -427,5 +1184,8 @@ class Layout: 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 diff --git a/ui/tab_b_logic.py b/ui/tab_b_logic.py index a1d1b35..986bae6 100644 --- a/ui/tab_b_logic.py +++ b/ui/tab_b_logic.py @@ -22,11 +22,11 @@ from sn_basis.modules.layerpruefer import Layerpruefer from sn_plan41.ui.layout import Layout -KARTENNAME_VAR = "sn_kartenname" -PLOTMASSSTAB_VAR = "sn_plotmassstab" -VIEW_VAR = "sn_view" -ZIELGROESSE_VAR = "sn_zielgroesse" -FORMFAKTOR_VAR = "sn_formfaktor" +KARTENNAME_VAR = "kartenname" +PLOTMASSSTAB_VAR = "plotmassstab" +VIEW_VAR = "view" +ZIELGROESSE_VAR = "zielgroesse" +FORMFAKTOR_VAR = "formfaktor" KARTENNAME_38 = "§38" KARTENNAME_41 = "§41" MASSSTAB_WIE_KARTENFENSTER = "Wie Kartenfenster" @@ -287,12 +287,12 @@ class TabBLogic: return loaded_layer def set_kartenname_for_auswahl(self, auswahl: str) -> None: - """Setzt die Projektvariable ``sn_kartenname`` anhand der Kartennamen-Auswahl.""" + """Setzt die Projektvariable ``kartenname`` anhand der Kartennamen-Auswahl.""" kartenname = KARTENNAME_BY_AUSWAHL.get(auswahl, "") set_variable(KARTENNAME_VAR, kartenname, scope="project") def set_plotmassstab_for_auswahl(self, auswahl: str, aktueller_massstab: float | None = None) -> None: - """Setzt die Projektvariable ``sn_plotmassstab`` anhand der Maßstabsauswahl.""" + """Setzt die Projektvariable ``plotmassstab`` anhand der Maßstabsauswahl.""" if auswahl == MASSSTAB_WIE_KARTENFENSTER: if aktueller_massstab and aktueller_massstab > 0: set_variable(PLOTMASSSTAB_VAR, str(int(round(aktueller_massstab))), scope="project") @@ -304,7 +304,7 @@ class TabBLogic: set_variable(PLOTMASSSTAB_VAR, value, scope="project") def set_view_for_auswahl(self, auswahl: str) -> None: - """Setzt ``sn_view`` auf ``aktuell`` oder den Namen des gewählten Layerthemas.""" + """Setzt die Projektvariable ``view`` auf ``aktuell`` oder den Namen des gewählten Layerthemas.""" if auswahl == THEMA_WIE_KARTENFENSTER: set_variable(VIEW_VAR, "aktuell", scope="project") return @@ -312,11 +312,11 @@ class TabBLogic: set_variable(VIEW_VAR, auswahl or "", scope="project") def set_zielgroesse_for_auswahl(self, auswahl: str) -> None: - """Setzt ``sn_zielgroesse`` auf den gewählten DIN-Namen.""" + """Setzt ``zielgroesse`` auf den gewählten DIN-Namen.""" set_variable(ZIELGROESSE_VAR, auswahl if auswahl in DIN_GROESSEN else DIN_STANDARD, scope="project") def set_formfaktor(self, endlosrolle: bool) -> None: - """Setzt ``sn_formfaktor`` auf ``Endlosrolle`` oder ``Blatt``.""" + """Setzt die Projektvariable ``formfaktor`` auf ``Endlosrolle`` oder ``Blatt``.""" set_variable(FORMFAKTOR_VAR, "Endlosrolle" if endlosrolle else "Blatt", scope="project") # ───────────────────────────────────────────────────────────────────────── @@ -358,7 +358,7 @@ class TabBLogic: return {"ok": False, "switch_to_tab_a": True, "atlas_seiten": 0} layer_id = getattr(layer, "id", lambda: "")() or "" - set_variable("sn_verfahrensgebietslayer", layer_id, scope="project") + set_variable("verfahrensgebietslayer", layer_id, scope="project") # ─── 2. Kartenname prüfen ───────────────────────────────────────── if kartenname_auswahl not in (KARTENNAME_38, KARTENNAME_41): diff --git a/ui/tab_b_ui.py b/ui/tab_b_ui.py index 3f1f98b..be1206f 100644 --- a/ui/tab_b_ui.py +++ b/ui/tab_b_ui.py @@ -206,7 +206,7 @@ class TabB(QWidget): self._endlosrolle_cb.setChecked(saved_formfaktor == "Endlosrolle") def _on_kartenname_changed(self, value: str) -> None: - """Persistiert die Kartennamen-Auswahl und setzt ``sn_kartenname``.""" + """Persistiert die Kartennamen-Auswahl und setzt ``kartenname``.""" if value in (KARTENNAME_38, KARTENNAME_41): set_variable(KARTENNAME_VAR, value, scope="project") else: @@ -216,26 +216,26 @@ class TabB(QWidget): self.logic.set_kartenname_for_auswahl(value) def _on_massstab_changed(self, value: str) -> None: - """Persistiert Maßstabsauswahl und setzt ``sn_plotmassstab``.""" + """Persistiert Maßstabsauswahl und setzt ``plotmassstab``.""" set_variable(MASSSTAB_VAR, value, scope="project") if self.logic: self.logic.set_plotmassstab_for_auswahl(value, self._get_current_canvas_scale()) def _on_thema_changed(self, value: str) -> None: - """Persistiert die Thema-Auswahl und setzt ``sn_view``.""" + """Persistiert die Thema-Auswahl und setzt ``view``.""" set_variable(THEMA_VAR, value, scope="project") if self.logic: self.logic.set_view_for_auswahl(value) def _on_zielgroesse_changed(self, value: str) -> None: - """Persistiert Blattgröße und setzt ``sn_zielgroesse``.""" + """Persistiert Blattgröße und setzt ``zielgroesse``.""" set_variable(ZIELGROESSE_UI_VAR, value, scope="project") if self.logic: self.logic.set_zielgroesse_for_auswahl(value) def _on_formfaktor_changed(self, state: int) -> None: - """Persistiert Endlosrolle-Zustand und setzt ``sn_formfaktor``.""" + """Persistiert Endlosrolle-Zustand und setzt ``formfaktor``.""" checked = bool(state) set_variable(FORMFAKTOR_UI_VAR, "Endlosrolle" if checked else "Blatt", scope="project") if self.logic: @@ -335,17 +335,22 @@ class TabB(QWidget): def _disconnect_theme_collection_signals(self) -> None: """Trennt Signale der alten Theme-Collection sauber.""" collection = self._connected_theme_collection - if collection is None: - return - for signal_name in ("mapThemesChanged", "changed", "themeChanged"): - signal = getattr(collection, signal_name, None) - if signal is None: - continue - try: - signal.disconnect(self._refresh_thema_combo_live) - except Exception: - pass + if collection is not None: + for signal_name in ("mapThemesChanged", "changed", "themeChanged"): + try: + signal = getattr(collection, signal_name, None) + except RuntimeError: + break + except Exception: + continue + if signal is None: + continue + try: + signal.disconnect(self._refresh_thema_combo_live) + except Exception: + pass self._connected_theme_collection = None + self._theme_signal_connected = False def _connect_theme_collection_signals(self) -> None: """Verbindet Signale der Theme-Collection für Live-Aktualisierung der Themenliste."""