From 44972dc9e09b6084dd3473432ab64d91595de894 Mon Sep 17 00:00:00 2001 From: Daniel Helbig Date: Wed, 25 Mar 2026 11:43:07 +0100 Subject: [PATCH 1/3] changelog.txt aktualisiert --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index 882da76..94da874 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,4 @@ +- unstable als testing veröffentlicht --- Version 26.3.1-unstable: - erster Test als echtes PlugIn \ No newline at end of file From 927da6fe2986e20e779ca02418341f0876b271e5 Mon Sep 17 00:00:00 2001 From: release-bot Date: Wed, 25 Mar 2026 10:44:55 +0000 Subject: [PATCH 2/3] Release 26.3.1-testing --- changelog.txt | 2 ++ metadata.txt | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/changelog.txt b/changelog.txt index 94da874..267d226 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,5 @@ +--- +Version 26.3.1-testing: - unstable als testing veröffentlicht --- Version 26.3.1-unstable: diff --git a/metadata.txt b/metadata.txt index ba52be1..498fa7e 100644 --- a/metadata.txt +++ b/metadata.txt @@ -1,11 +1,11 @@ [general] -version=26.3.1-unstable +version=26.3.1-testing name=LNO Sachsen | Plan41 description=Plugin zum Erzeugen der Pläne nach §38 und §41 author=Daniel Helbig, Michael Otto homepage=https://entwicklung.flurneuordnung-sachsen.de/AG_QGIS/Plugin_SN_Plan41 tracker=https://entwicklung.flurneuordnung-sachsen.de/AG_QGIS/Plugin_SN_Plan41/issues -repository=https://entwicklung.flurneuordnung-sachsen.de/AG_QGIS/Plugin_Test_Action/src/branch/unstable/ +repository=https://entwicklung.flurneuordnung-sachsen.de/AG_QGIS/Plugin_Test_Action/src/branch/testing/ qgisMinimumVersion=3.40 experimental=true From 7cd71ba3661443ff44fa6e9290e4b0d7133a81ed Mon Sep 17 00:00:00 2001 From: daniel Date: Thu, 26 Mar 2026 20:17:45 +0100 Subject: [PATCH 3/3] print-tab nach sn_basis verschoben --- ui/dockwidget.py | 4 +- ui/layout.py | 1191 --------------------------------------------- ui/tab_b_logic.py | 680 -------------------------- ui/tab_b_ui.py | 432 ---------------- 4 files changed, 2 insertions(+), 2305 deletions(-) delete mode 100644 ui/layout.py delete mode 100644 ui/tab_b_logic.py delete mode 100644 ui/tab_b_ui.py diff --git a/ui/dockwidget.py b/ui/dockwidget.py index 718be90..51388b3 100644 --- a/ui/dockwidget.py +++ b/ui/dockwidget.py @@ -1,14 +1,14 @@ #sn_plan41/ui/dockwidget.py from sn_basis.ui.tabs.settings_tab import SettingsTab from sn_plan41.ui.tab_a_ui import TabA -from sn_plan41.ui.tab_b_ui import TabB +from sn_basis.ui.tabs.print_tab import PrintTab from sn_basis.ui.base_dockwidget import BaseDockWidget from sn_basis.functions.qt_wrapper import QTabWidget from sn_basis.functions.message_wrapper import error class DockWidget(BaseDockWidget): - tabs = [TabA, TabB,SettingsTab] + tabs = [TabA, PrintTab, SettingsTab] def __init__(self, parent=None, subtitle="", pruefmanager=None, data_grabber=None): super().__init__(parent, subtitle) diff --git a/ui/layout.py b/ui/layout.py deleted file mode 100644 index a2ec82f..0000000 --- a/ui/layout.py +++ /dev/null @@ -1,1191 +0,0 @@ -""" -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, - 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": "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, - 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: - 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}") - - # 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, - hauptkarte, - map_top_mm, - map_height_mm, - is_atlas=False, - ) - - 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: - self._set_item_frame_stroke_width(hauptkarte, 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, - hauptkarte, - map_top_mm, - map_height_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 diff --git a/ui/tab_b_logic.py b/ui/tab_b_logic.py deleted file mode 100644 index 986bae6..0000000 --- a/ui/tab_b_logic.py +++ /dev/null @@ -1,680 +0,0 @@ -""" -sn_plan41/ui/tab_b_logic.py – Fachlogik für Tab B (Druck) -""" -from __future__ import annotations -import math -from typing import Any - -from sn_basis.functions.qt_wrapper import QVariant -from sn_basis.functions.variable_wrapper import set_variable, get_variable -from sn_basis.functions.qgiscore_wrapper import get_layer_extent -from sn_basis.functions.qgiscore_wrapper import ( - QgsProject, - QgsVectorLayer, - QgsGeometry, - QgsFeature, - QgsField, - QgsVectorFileWriter, -) -from sn_basis.functions.sys_wrapper import get_plugin_root, join_path, file_exists -from sn_basis.modules.Pruefmanager import Pruefmanager -from sn_basis.modules.layerpruefer import Layerpruefer -from sn_plan41.ui.layout import Layout - - -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" -THEMA_WIE_KARTENFENSTER = "wie kartenfenster" - -KARTENNAME_BY_AUSWAHL = { - KARTENNAME_38: "Planungsübersicht §38 FlurbG", - KARTENNAME_41: "Karte zum Plan über die gemeinschaftlichen und öffentlichen Anlagen (§ 41 FlurbG)", -} - -PLOTMASSSTAB_BY_AUSWAHL = { - "1:5.000": "5000", - "1:10.000": "10000", - "1:15.000": "15000", - "1:20.000": "20000", - "1:25.000": "25000", - "1:50.000": "50000", - "1:100.000": "100000", -} - -# Breite x Höhe in mm (Hochformat, DIN-Standard) -DIN_GROESSEN: dict[str, tuple[int, int]] = { - "DIN A0": (841, 1189), - "DIN A1": (594, 841), - "DIN A2": (420, 594), - "DIN A3": (297, 420), - "DIN A4": (210, 297), -} -DIN_STANDARD = "DIN A0" - -class TabBLogic: - """ - Kapselt die Fachlogik von Tab B. - """ - - def __init__(self, pruefmanager: Pruefmanager) -> None: - self.pruefmanager = pruefmanager - - @staticmethod - def _wkt_rect(x_min: float, y_min: float, x_max: float, y_max: float) -> str: - return ( - f"POLYGON(({x_min} {y_min}, {x_max} {y_min}, {x_max} {y_max}, " - f"{x_min} {y_max}, {x_min} {y_min}))" - ) - - @staticmethod - def _set_topological_editing_enabled() -> None: - project = QgsProject.instance() - set_topological = getattr(project, "setTopologicalEditing", None) - if callable(set_topological): - try: - set_topological(True) - except Exception: - pass - - @staticmethod - def _apply_atlas_style(layer: Any) -> None: - style_path = join_path(get_plugin_root(), "sn_plan41", "assets", "atlasobjekte.qml") - if not file_exists(style_path): - return - try: - ok, _ = layer.loadNamedStyle(str(style_path)) - if ok: - getattr(layer, "triggerRepaint", lambda: None)() - except Exception: - pass - - @staticmethod - def _find_tile_grid_for_roll_atlas( - kartenbild_w_mm: float, - kartenbild_h_mm: float, - din_dims: tuple[int, int], - respect_max_sheet_size: bool = False, - ) -> tuple[int, int, float, float]: - """Bestimmt ein Atlas-Raster für Endlosrolle, das Kacheln statt Streifen erzeugt. - - Ziel: Kachel-Seitenverhältnis möglichst nah am Einzelblatt-Kartenfenster - (inkl. Rändern) bei zugleich moderater Seitenanzahl. - """ - if kartenbild_w_mm <= 0 or kartenbild_h_mm <= 0: - return 1, 1, max(1.0, kartenbild_w_mm), max(1.0, kartenbild_h_mm) - - # Einzelblatt-Kartenfenster für beide Orientierungen - dim_w, dim_h = float(din_dims[0]), float(din_dims[1]) - orientation_candidates: list[tuple[float, float]] = [ - (dim_w - 210.0, dim_h - 20.0), - (dim_h - 210.0, dim_w - 20.0), - ] - - best_score = math.inf - best_result: tuple[int, int, float, float] | None = None - - for target_w, target_h in orientation_candidates: - if target_w <= 0 or target_h <= 0: - continue - - target_aspect = target_w / target_h - px0 = max(1, int(round(kartenbild_w_mm / target_w))) - py0 = max(1, int(round(kartenbild_h_mm / target_h))) - - for pages_x in range(max(1, px0 - 3), px0 + 4): - for pages_y in range(max(1, py0 - 3), py0 + 4): - tile_w = kartenbild_w_mm / pages_x - tile_h = kartenbild_h_mm / pages_y - if tile_w <= 0 or tile_h <= 0: - continue - - # Im Blatt-Modus darf die resultierende Atlasseite die - # gewählte Zielgröße (inkl. Orientierung) nicht überschreiten. - if respect_max_sheet_size and (tile_w > target_w or tile_h > target_h): - continue - - tile_aspect = tile_w / tile_h - # 0 bei perfekter Übereinstimmung; symmetrisch für >1/<1 - aspect_error = abs(math.log(tile_aspect / target_aspect)) - - # Streifen bestrafen - strip_penalty = 0.0 - if tile_aspect < 0.5: - strip_penalty = abs(math.log(tile_aspect / 0.5)) - elif tile_aspect > 2.0: - strip_penalty = abs(math.log(tile_aspect / 2.0)) - - page_count = pages_x * pages_y - - # Gewichtung: zuerst Formatnähe, dann Streifenvermeidung, - # danach Seitenzahl minimieren. - score = (aspect_error * 12.0) + (strip_penalty * 6.0) + (page_count * 0.20) - - if score < best_score: - best_score = score - best_result = (pages_x, pages_y, tile_w, tile_h) - - if best_result is None: - if respect_max_sheet_size: - fallback_candidates: list[tuple[int, int, float, float, int]] = [] - for target_w, target_h in orientation_candidates: - if target_w <= 0 or target_h <= 0: - continue - pages_x = max(1, math.ceil(kartenbild_w_mm / target_w)) - pages_y = max(1, math.ceil(kartenbild_h_mm / target_h)) - tile_w = kartenbild_w_mm / pages_x - tile_h = kartenbild_h_mm / pages_y - fallback_candidates.append((pages_x, pages_y, tile_w, tile_h, pages_x * pages_y)) - - if fallback_candidates: - fallback_candidates.sort(key=lambda entry: (entry[4], abs(math.log((entry[2] / entry[3]) if entry[3] > 0 else 1.0)))) - fx, fy, fw, fh, _ = fallback_candidates[0] - return fx, fy, fw, fh - - return 1, 1, kartenbild_w_mm, kartenbild_h_mm - - return best_result - - def _create_atlasobjekte_layer( - self, - layer: Any, - extent: Any, - pages_x: int, - pages_y: int, - seite_karte_w: float, - seite_karte_h: float, - massstab_zahl: float, - ) -> Any | None: - layer_crs = layer.crs() if hasattr(layer, "crs") else None - crs_authid = layer_crs.authid() if layer_crs is not None and hasattr(layer_crs, "authid") else "EPSG:25832" - atlas_layer = QgsVectorLayer(f"Polygon?crs={crs_authid}", "Atlasobjekte", "memory") - if not atlas_layer or not atlas_layer.isValid(): - return None - - provider = atlas_layer.dataProvider() - provider.addAttributes([ - QgsField("Seitenzahl", QVariant.Int), - ]) - atlas_layer.updateFields() - - tile_w_m = seite_karte_w * massstab_zahl / 1000.0 - tile_h_m = seite_karte_h * massstab_zahl / 1000.0 - - x_min = extent.xMinimum() - y_min = extent.yMinimum() - x_max = extent.xMaximum() - y_max = extent.yMaximum() - - seitenzahl = 1 - features = [] - for row_idx in range(pages_y): - tile_y_max = y_max - row_idx * tile_h_m - tile_y_min = tile_y_max - tile_h_m - - for col_idx in range(pages_x): - tile_x_min = x_min + col_idx * tile_w_m - tile_x_max = tile_x_min + tile_w_m - - tile_geom = QgsGeometry.fromWkt( - self._wkt_rect(tile_x_min, tile_y_min, tile_x_max, tile_y_max) - ) - - feat = QgsFeature(atlas_layer.fields()) - feat.setGeometry(tile_geom) - feat["Seitenzahl"] = seitenzahl - features.append(feat) - seitenzahl += 1 - - if not features: - return None - - provider.addFeatures(features) - atlas_layer.updateExtents() - self._set_topological_editing_enabled() - - verfahrens_db = get_variable("verfahrens_db", scope="project") or "" - print(f"[TabBLogic] Atlasobjekte: verfahrens_db='{verfahrens_db}'") - if not verfahrens_db: - QgsProject.instance().addMapLayer(atlas_layer) - self._apply_atlas_style(atlas_layer) - print("[TabBLogic] Atlasobjekte temporär ins Projekt geladen") - return atlas_layer - - opts = QgsVectorFileWriter.SaveVectorOptions() - opts.driverName = "GPKG" - opts.fileEncoding = "UTF-8" - opts.layerName = "Atlasobjekte" - if file_exists(verfahrens_db): - opts.actionOnExistingFile = QgsVectorFileWriter.CreateOrOverwriteLayer - else: - opts.actionOnExistingFile = QgsVectorFileWriter.CreateOrOverwriteFile - - err_result = QgsVectorFileWriter.writeAsVectorFormatV3( - atlas_layer, - verfahrens_db, - QgsProject.instance().transformContext(), - opts, - ) - - # QGIS-Versionen liefern hier entweder nur WriterError - # oder ein Tupel (WriterError, msg, newPath, layerName). - err_code = err_result[0] if isinstance(err_result, tuple) else err_result - if err_code != QgsVectorFileWriter.NoError: - print(f"[TabBLogic] Atlasobjekte schreiben fehlgeschlagen: err={err_result}") - return None - - for existing in QgsProject.instance().mapLayersByName("Atlasobjekte"): - try: - QgsProject.instance().removeMapLayer(existing.id()) - except Exception: - pass - - uri = f"{verfahrens_db}|layername=Atlasobjekte" - loaded_layer = QgsVectorLayer(uri, "Atlasobjekte", "ogr") - if not loaded_layer or not loaded_layer.isValid(): - print(f"[TabBLogic] Atlasobjekte laden aus GPKG fehlgeschlagen: uri='{uri}'") - return None - - QgsProject.instance().addMapLayer(loaded_layer) - self._apply_atlas_style(loaded_layer) - print("[TabBLogic] Atlasobjekte aus Verfahrens-DB geladen und gestylt") - return loaded_layer - - def set_kartenname_for_auswahl(self, auswahl: str) -> None: - """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 ``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") - else: - set_variable(PLOTMASSSTAB_VAR, "", scope="project") - return - - value = PLOTMASSSTAB_BY_AUSWAHL.get(auswahl, "") - set_variable(PLOTMASSSTAB_VAR, value, scope="project") - - def set_view_for_auswahl(self, auswahl: str) -> None: - """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 - - set_variable(VIEW_VAR, auswahl or "", scope="project") - - def set_zielgroesse_for_auswahl(self, auswahl: str) -> None: - """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 die Projektvariable ``formfaktor`` auf ``Endlosrolle`` oder ``Blatt``.""" - set_variable(FORMFAKTOR_VAR, "Endlosrolle" if endlosrolle else "Blatt", scope="project") - - # ───────────────────────────────────────────────────────────────────────── - # Pipeline: Druckvorlage_anlegen - # ───────────────────────────────────────────────────────────────────────── - - def druckvorlage_anlegen( - self, - layer: object, - kartenname_auswahl: str, - massstab_auswahl: str, - zielgroesse: str, - formfaktor: bool, - ) -> dict: - """Pipeline 'Druckvorlage_anlegen'. - - Prüft Parameter, berechnet Plotgröße und stellt bei Bedarf Atlas-Rückfrage. - - Parameters - ---------- - layer: - Aktuell gewählter Verfahrensgebiet-Layer aus Tab A. - - Returns - ------- - dict - ``ok`` (bool): Ob die Pipeline erfolgreich durchlaufen werden soll. - ``switch_to_tab_a`` (bool): Ob Tab A aktiviert werden soll. - ``atlas_seiten`` (int): Anzahl benötigter Seiten (1 = kein Atlas). - """ - # ─── 1. Verfahrensgebiet-Layer prüfen ───────────────────────────── - lp = Layerpruefer(layer=layer) - ergebnis = lp.pruefe() - if not ergebnis.ok: - self.pruefmanager.zeige_hinweis( - "Verfahrensgebiets-Layer angeben", - "Verfahrensgebiets-Layer angeben", - ) - return {"ok": False, "switch_to_tab_a": True, "atlas_seiten": 0} - - layer_id = getattr(layer, "id", lambda: "")() or "" - set_variable("verfahrensgebietslayer", layer_id, scope="project") - - # ─── 2. Kartenname prüfen ───────────────────────────────────────── - if kartenname_auswahl not in (KARTENNAME_38, KARTENNAME_41): - self.pruefmanager.zeige_hinweis( - "Kartennamen wählen", - "Kartennamen wählen", - ) - return {"ok": False, "switch_to_tab_a": False, "atlas_seiten": 0} - - # ─── 3. Maßstab ermitteln ───────────────────────────────────────── - if massstab_auswahl == MASSSTAB_WIE_KARTENFENSTER: - 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)) - - if massstab_zahl <= 0: - self.pruefmanager.zeige_hinweis( - "Maßstab fehlt", - "Kein gültiger Maßstab angegeben.", - ) - return {"ok": False, "switch_to_tab_a": False, "atlas_seiten": 0} - - # ─── 4. Kartenbild berechnen ────────────────────────────────────── - # Der Layer wird als metrisch projiziert (Einheit: m) vorausgesetzt, - # wie es für deutsche Planungslagen (z.B. EPSG:25832) üblich ist. - extent = get_layer_extent(layer) - if extent is None: - self.pruefmanager.zeige_hinweis( - "Fehler", - "Layer-Ausdehnung konnte nicht ermittelt werden.", - ) - return {"ok": False, "switch_to_tab_a": False, "atlas_seiten": 0} - - # Naturgröße (m) → Papiergröße (mm): mm = m * 1000 / massstab - kartenbild_w = extent.width() * 1000.0 / massstab_zahl - kartenbild_h = extent.height() * 1000.0 / massstab_zahl - - # ─── 5. Plotgröße = Kartenbild + Randabstand (x+210 mm, y+20 mm) ── - plotgroesse_w = kartenbild_w + 210.0 - plotgroesse_h = kartenbild_h + 20.0 - - # ─── 6. Zielgröße bestimmen ─────────────────────────────────────── - din_dims = DIN_GROESSEN.get(zielgroesse, DIN_GROESSEN[DIN_STANDARD]) - if formfaktor: # Endlosrolle: X-Richtung entspricht der Plotgröße - ziel_w, ziel_h = plotgroesse_w, float(min(din_dims)) - else: - ziel_w, ziel_h = float(din_dims[0]), float(din_dims[1]) - - if ziel_w < DIN_GROESSEN["DIN A4"][0] or ziel_h < DIN_GROESSEN["DIN A4"][1]: - self.pruefmanager.zeige_hinweis( - "Blattgröße zu klein", - "Die Zielgröße darf nicht kleiner als DIN A4 sein.", - ) - return {"ok": False, "switch_to_tab_a": False, "atlas_seiten": 0} - - # ─── 7. Passt auf ein Blatt? -> Layout erzeugen ─────────────────── - print(f"[TabBLogic] plotgroesse=({plotgroesse_w:.1f}x{plotgroesse_h:.1f}), " - f"zielgroesse=({ziel_w:.1f}x{ziel_h:.1f}), passt={plotgroesse_w <= ziel_w and plotgroesse_h <= ziel_h}") - 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 and plotgroesse_h <= ziel_h: - kartenname = get_variable(KARTENNAME_VAR, scope="project") or KARTENNAME_BY_AUSWAHL.get( - kartenname_auswahl, "Vorlage" - ) - thema = get_variable(VIEW_VAR, scope="project") or "" - print(f"[TabBLogic] frage_text aufrufen, default='{kartenname}', thema='{thema}'") - vorlage_name, bestaetigt = self.pruefmanager.frage_text( - "Neue Vorlage anlegen", - "Bezeichnung der Vorlage:", - default_text=kartenname, - ) - print(f"[TabBLogic] frage_text Ergebnis: name='{vorlage_name}', bestaetigt={bestaetigt}") - if not bestaetigt: - return {"ok": False, "switch_to_tab_a": False, "atlas_seiten": 0} - - vorlage_name = (vorlage_name or "").strip() or kartenname - - print(f"[TabBLogic] Rufe Layout().create_single_page_layout auf: name='{vorlage_name}', " - f"page=({ziel_w}x{ziel_h}), map=({kartenbild_w:.1f}x{kartenbild_h:.1f}), " - f"massstab={massstab_zahl}, thema='{thema}'") - try: - Layout().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, - extent=extent, - plotmassstab=massstab_zahl, - thema=thema, - ) - print("[TabBLogic] create_single_page_layout erfolgreich abgeschlossen") - except ValueError as exc: - print(f"[TabBLogic] ValueError: {exc}") - self.pruefmanager.zeige_hinweis("Vorlage anlegen", str(exc)) - return {"ok": False, "switch_to_tab_a": False, "atlas_seiten": 0} - except Exception as exc: - print(f"[TabBLogic] Exception: {exc!r}") - self.pruefmanager.zeige_hinweis("Vorlage anlegen", f"Die Vorlage konnte nicht angelegt werden: {exc}") - return {"ok": False, "switch_to_tab_a": False, "atlas_seiten": 0} - - return {"ok": True, "switch_to_tab_a": False, "atlas_seiten": 1} - - # ─── 8. Atlas: Anzahl Seiten berechnen ──────────────────────────── - # Nutzbarer Kartenbereich pro Atlasseite (abzüglich gleichem Randabstand) - seite_karte_w = ziel_w - 210.0 - seite_karte_h = ziel_h - 20.0 - - if seite_karte_w <= 0 or seite_karte_h <= 0: - self.pruefmanager.zeige_hinweis( - "Blattgröße zu klein", - "Die gewählte Zielgröße ist kleiner als der Mindest-Randabstand. " - "Bitte eine größere Blattgröße wählen.", - ) - return {"ok": False, "switch_to_tab_a": False, "atlas_seiten": 0} - - pages_x = math.ceil(kartenbild_w / seite_karte_w) - pages_y = math.ceil(kartenbild_h / seite_karte_h) - - # Für Atlas in beiden Modi (Endlosrolle + Blatt): Seitenraster so wählen, - # dass Atlasobjekte näher am Einzelblattformat liegen und keine Streifen entstehen. - opt_pages_x, opt_pages_y, opt_tile_w_mm, opt_tile_h_mm = self._find_tile_grid_for_roll_atlas( - kartenbild_w_mm=kartenbild_w, - kartenbild_h_mm=kartenbild_h, - din_dims=din_dims, - respect_max_sheet_size=(not formfaktor), - ) - - # Endlosrolle: Nur die Breite darf wachsen, die Höhe muss innerhalb - # der gewählten Zielhöhe bleiben. - if formfaktor: - max_tile_h_mm = max(1.0, ziel_h - 20.0) - if opt_tile_h_mm > max_tile_h_mm: - required_pages_y = max(1, math.ceil(kartenbild_h / max_tile_h_mm)) - opt_pages_y = max(opt_pages_y, required_pages_y) - opt_tile_h_mm = kartenbild_h / opt_pages_y - - pages_x = max(1, opt_pages_x) - pages_y = max(1, opt_pages_y) - seite_karte_w = max(1.0, opt_tile_w_mm) - seite_karte_h = max(1.0, opt_tile_h_mm) - modus = "Endlosrolle" if formfaktor else "Blatt" - print( - f"[TabBLogic] {modus} Rasteroptimierung: pages_x={pages_x}, pages_y={pages_y}, " - f"tile_mm=({seite_karte_w:.1f}x{seite_karte_h:.1f}), " - f"tile_aspect={seite_karte_w / seite_karte_h:.3f}" - ) - if formfaktor: - max_tile_h_mm = max(1.0, ziel_h - 20.0) - print( - f"[TabBLogic] Endlosrolle Höhenlimit: tile_h={seite_karte_h:.1f}mm, " - f"max_tile_h={max_tile_h_mm:.1f}mm, within_limit={seite_karte_h <= max_tile_h_mm}" - ) - - anzahl_seiten = pages_x * pages_y - - ja = self.pruefmanager.frage_ja_nein( - "Ausdruck als Atlas anlegen?", - f"Für die ausgewählten Parameter sind {anzahl_seiten} Einzelseiten erforderlich.\n" - "Ausdruck als Atlas anlegen?", - default=True, - ) - print(f"[TabBLogic] Atlas-Rückfrage: ja={ja}, geplante_seiten={anzahl_seiten}") - if not ja: - return {"ok": False, "switch_to_tab_a": False, "atlas_seiten": anzahl_seiten} - - atlas_layer = self._create_atlasobjekte_layer( - layer=layer, - extent=extent, - pages_x=pages_x, - pages_y=pages_y, - seite_karte_w=seite_karte_w, - seite_karte_h=seite_karte_h, - massstab_zahl=massstab_zahl, - ) - if atlas_layer is None: - self.pruefmanager.zeige_hinweis( - "Atlasobjekte", - "Atlasobjekte-Layer konnte nicht erzeugt werden.", - ) - return {"ok": False, "switch_to_tab_a": False, "atlas_seiten": 0} - print("[TabBLogic] Atlasobjekte-Layer erfolgreich erzeugt") - - try: - anzahl_seiten = int(atlas_layer.featureCount()) - except Exception: - pass - - # Berechne die Kachelgröße aus den Atlasobjekten. - # Für die Layout-Kartengröße muss die größte Atlas-Kachel passen, - # sonst werden größere Atlasobjekte abgeschnitten. - total_w_m = 0.0 - total_h_m = 0.0 - min_w_m = math.inf - min_h_m = math.inf - max_w_m = 0.0 - max_h_m = 0.0 - feature_count = 0 - get_features = getattr(atlas_layer, "getFeatures", None) - if callable(get_features): - try: - for feat in get_features(): - geom = feat.geometry() if hasattr(feat, "geometry") else None - if geom is None or geom.isEmpty(): - continue - bbox = geom.boundingBox() if hasattr(geom, "boundingBox") else None - if bbox is None: - continue - feat_w_m = float(bbox.width()) - feat_h_m = float(bbox.height()) - total_w_m += feat_w_m - total_h_m += feat_h_m - min_w_m = min(min_w_m, feat_w_m) - min_h_m = min(min_h_m, feat_h_m) - max_w_m = max(max_w_m, feat_w_m) - max_h_m = max(max_h_m, feat_h_m) - feature_count += 1 - except Exception: - pass - - if feature_count > 0: - avg_tile_w_m = total_w_m / feature_count - avg_tile_h_m = total_h_m / feature_count - if not math.isfinite(min_w_m): - min_w_m = avg_tile_w_m - if not math.isfinite(min_h_m): - min_h_m = avg_tile_h_m - target_tile_w_m = max_w_m - target_tile_h_m = max_h_m - else: - avg_tile_w_m = seite_karte_w * massstab_zahl / 1000.0 - avg_tile_h_m = seite_karte_h * massstab_zahl / 1000.0 - min_w_m = avg_tile_w_m - min_h_m = avg_tile_h_m - max_w_m = avg_tile_w_m - max_h_m = avg_tile_h_m - target_tile_w_m = avg_tile_w_m - target_tile_h_m = avg_tile_h_m - - # Konvertiere Kachelgrößen zu mm - avg_tile_w_mm = avg_tile_w_m * 1000.0 / massstab_zahl - avg_tile_h_mm = avg_tile_h_m * 1000.0 / massstab_zahl - min_tile_w_mm = min_w_m * 1000.0 / massstab_zahl - min_tile_h_mm = min_h_m * 1000.0 / massstab_zahl - max_tile_w_mm = max_w_m * 1000.0 / massstab_zahl - max_tile_h_mm = max_h_m * 1000.0 / massstab_zahl - target_tile_w_mm = target_tile_w_m * 1000.0 / massstab_zahl - target_tile_h_mm = target_tile_h_m * 1000.0 / massstab_zahl - - # Layout-Kartengröße = größte Kachelgröße - atlas_map_w = max(1.0, target_tile_w_mm) - atlas_map_h = max(1.0, target_tile_h_mm) - atlas_page_w = atlas_map_w + 210.0 - atlas_page_h = atlas_map_h + 20.0 - - # Debug: Atlasobjekte Geometrien - print( - f"[TabBLogic] Atlasobjekte Geometrien (Meter): " - f"total_w={total_w_m:.1f}m, total_h={total_h_m:.1f}m, " - f"min_w={min_w_m:.1f}m, min_h={min_h_m:.1f}m, " - f"avg_w={avg_tile_w_m:.1f}m, avg_h={avg_tile_h_m:.1f}m, " - f"max_w={max_w_m:.1f}m, max_h={max_h_m:.1f}m, features={feature_count}" - ) - print( - f"[TabBLogic] Atlas layout Größen: " - f"page=(x=10, y=10, w={atlas_page_w:.1f}mm, h={atlas_page_h:.1f}mm), " - f"map=(x=10, y=10, w={atlas_map_w:.1f}mm, h={atlas_map_h:.1f}mm), " - f"kachel_min_mm=({min_tile_w_mm:.1f}x{min_tile_h_mm:.1f}), " - f"kachel_avg_mm=({avg_tile_w_mm:.1f}x{avg_tile_h_mm:.1f}), " - f"kachel_max_mm=({max_tile_w_mm:.1f}x{max_tile_h_mm:.1f})" - ) - - kartenname = get_variable(KARTENNAME_VAR, scope="project") or KARTENNAME_BY_AUSWAHL.get( - kartenname_auswahl, "Atlas" - ) - thema = get_variable(VIEW_VAR, scope="project") or "" - vorlage_name, bestaetigt = self.pruefmanager.frage_text( - "Neue Atlasvorlage anlegen", - "Bezeichnung der Vorlage:", - default_text=f"{kartenname} Atlas", - ) - print(f"[TabBLogic] Atlas frage_text Ergebnis: name='{vorlage_name}', bestaetigt={bestaetigt}") - if not bestaetigt: - return {"ok": False, "switch_to_tab_a": False, "atlas_seiten": anzahl_seiten} - - vorlage_name = (vorlage_name or "").strip() or f"{kartenname} Atlas" - - try: - print( - f"[TabBLogic] Rufe create_atlas_layout auf: name='{vorlage_name}', " - f"page=({atlas_page_w:.1f}x{atlas_page_h:.1f}), " - f"map=({atlas_map_w:.1f}x{atlas_map_h:.1f}), massstab={massstab_zahl}, thema='{thema}'" - ) - Layout().create_atlas_layout( - name=vorlage_name, - page_width_mm=atlas_page_w, - page_height_mm=atlas_page_h, - map_width_mm=atlas_map_w, - map_height_mm=atlas_map_h, - extent=extent, - plotmassstab=massstab_zahl, - atlas_layer=atlas_layer, - thema=thema, - ) - print("[TabBLogic] create_atlas_layout erfolgreich abgeschlossen") - except ValueError as exc: - self.pruefmanager.zeige_hinweis("Atlasvorlage anlegen", str(exc)) - return {"ok": False, "switch_to_tab_a": False, "atlas_seiten": 0} - except Exception as exc: - self.pruefmanager.zeige_hinweis("Atlasvorlage anlegen", f"Die Atlasvorlage konnte nicht angelegt werden: {exc}") - return {"ok": False, "switch_to_tab_a": False, "atlas_seiten": 0} - - return {"ok": True, "switch_to_tab_a": False, "atlas_seiten": anzahl_seiten} \ No newline at end of file diff --git a/ui/tab_b_ui.py b/ui/tab_b_ui.py deleted file mode 100644 index be1206f..0000000 --- a/ui/tab_b_ui.py +++ /dev/null @@ -1,432 +0,0 @@ -""" -sn_plan41/ui/tab_b_ui.py – UI für Tab B (Druck) -""" -from __future__ import annotations - -from typing import Optional - -from sn_basis.functions.qt_wrapper import ( - QWidget, - QVBoxLayout, - QLabel, - QComboBox, - QCheckBox, - QHBoxLayout, - QPushButton, -) -from sn_basis.functions.qgiscore_wrapper import QgsProject -from sn_basis.functions.qgisui_wrapper import iface -from sn_basis.functions.variable_wrapper import get_variable, set_variable - -# Services (werden von DockWidget injiziert) -from sn_basis.modules.Pruefmanager import Pruefmanager -from sn_basis.modules.DataGrabber import DataGrabber -from sn_plan41.ui.tab_b_logic import ( - TabBLogic, - MASSSTAB_WIE_KARTENFENSTER, - PLOTMASSSTAB_BY_AUSWAHL, - THEMA_WIE_KARTENFENSTER, - DIN_GROESSEN, - DIN_STANDARD, - ZIELGROESSE_VAR, - FORMFAKTOR_VAR, -) - - -KARTENNAME_VAR = "tab_b_kartenname" -KARTENNAME_PLACEHOLDER = "Kartenname wählen" -KARTENNAME_38 = "§38" -KARTENNAME_41 = "§41" -MASSSTAB_VAR = "tab_b_massstab" -THEMA_VAR = "tab_b_thema" -ZIELGROESSE_UI_VAR = "tab_b_zielgroesse" -FORMFAKTOR_UI_VAR = "tab_b_formfaktor" - -class TabB(QWidget): - """ - UI-Klasse für Tab B (Druck) des Plan41-Plugins. - - Zuständig für: - - Auswahl des Druckthemas - - Auswahl der Druckparameter - - Start der Vorlagenanlage (Druck über QGIS-Druckfunktion) - - Services (Pruefmanager, DataGrabber) werden zur Laufzeit vom DockWidget injiziert. - Alle fachlichen Prüfungen laufen über den zentralen Pruefmanager. - """ - - tab_title = "Druck" #: Tab-Titel für BaseDockWidget - - def __init__(self, parent: Optional[QWidget] = None): - """ - Initialisiert die UI-Struktur. - - Services werden später über :meth:`set_services` injiziert. - - :param parent: Parent-Widget (typischerweise DockWidget) - """ - super().__init__(parent) - - # Services (werden von DockWidget gesetzt) - self.pruefmanager: Optional[Pruefmanager] = None - - self.logic: Optional[TabBLogic] = None - self._kartenname_combo: Optional[QComboBox] = None - self._massstab_combo: Optional[QComboBox] = None - self._thema_combo: Optional[QComboBox] = None - self._theme_signal_connected = False - self._connected_theme_collection: object = None # Referenz für sauberes Trennen - self._zielgroesse_combo: Optional[QComboBox] = None - self._endlosrolle_cb: Optional[QCheckBox] = None - self._btn_vorlage_erstellen: Optional[QPushButton] = None - - self._build_ui() - self._restore_state() - self._connect_theme_collection_signals() - self._connect_project_signals() - - def set_services(self, pruefmanager: Pruefmanager, data_grabber: DataGrabber) -> None: - """Injiziert Services vom übergeordneten DockWidget.""" - _ = data_grabber - self.pruefmanager = pruefmanager - self.logic = TabBLogic(pruefmanager=self.pruefmanager) - - if self._kartenname_combo: - self.logic.set_kartenname_for_auswahl(self._kartenname_combo.currentText()) - if self._massstab_combo: - self.logic.set_plotmassstab_for_auswahl( - self._massstab_combo.currentText(), - self._get_current_canvas_scale(), - ) - if self._thema_combo: - self.logic.set_view_for_auswahl(self._thema_combo.currentText()) - if self._zielgroesse_combo: - self.logic.set_zielgroesse_for_auswahl(self._zielgroesse_combo.currentText()) - if self._endlosrolle_cb: - self.logic.set_formfaktor(self._endlosrolle_cb.isChecked()) - - def _build_ui(self) -> None: - """Erstellt die reduzierte UI für die Themenauswahl.""" - main_layout = QVBoxLayout() - main_layout.setSpacing(4) - main_layout.setContentsMargins(4, 4, 4, 4) - - kartenname_label = QLabel("Kartenname") - kartenname_label.setStyleSheet("font-weight: bold; margin-top: 6px;") - main_layout.addWidget(kartenname_label) - - self._kartenname_combo = QComboBox(self) - self._kartenname_combo.addItem(KARTENNAME_PLACEHOLDER) - self._kartenname_combo.addItem(KARTENNAME_38) - self._kartenname_combo.addItem(KARTENNAME_41) - self._kartenname_combo.currentTextChanged.connect(self._on_kartenname_changed) - main_layout.addWidget(self._kartenname_combo) - - massstab_label = QLabel("Maßstab") - massstab_label.setStyleSheet("font-weight: bold; margin-top: 6px;") - main_layout.addWidget(massstab_label) - - self._massstab_combo = QComboBox(self) - self._massstab_combo.addItem(MASSSTAB_WIE_KARTENFENSTER) - self._massstab_combo.addItems(list(PLOTMASSSTAB_BY_AUSWAHL.keys())) - self._massstab_combo.currentTextChanged.connect(self._on_massstab_changed) - main_layout.addWidget(self._massstab_combo) - - thema_label = QLabel("Thema") - thema_label.setStyleSheet("font-weight: bold; margin-top: 6px;") - main_layout.addWidget(thema_label) - - self._thema_combo = QComboBox(self) - self._thema_combo.addItem(THEMA_WIE_KARTENFENSTER) - self._thema_combo.addItems(self._get_gespeicherte_themen()) - self._thema_combo.currentTextChanged.connect(self._on_thema_changed) - main_layout.addWidget(self._thema_combo) - - zielgroesse_label = QLabel("max. Blattgröße") - zielgroesse_label.setStyleSheet("font-weight: bold; margin-top: 6px;") - main_layout.addWidget(zielgroesse_label) - - zielgroesse_row = QHBoxLayout() - zielgroesse_row.setSpacing(6) - self._zielgroesse_combo = QComboBox(self) - self._zielgroesse_combo.addItems(list(DIN_GROESSEN.keys())) - self._zielgroesse_combo.setCurrentText(DIN_STANDARD) - self._zielgroesse_combo.currentTextChanged.connect(self._on_zielgroesse_changed) - zielgroesse_row.addWidget(self._zielgroesse_combo) - self._endlosrolle_cb = QCheckBox("Endlosrolle", self) - self._endlosrolle_cb.setChecked(False) - self._endlosrolle_cb.stateChanged.connect(self._on_formfaktor_changed) - zielgroesse_row.addWidget(self._endlosrolle_cb) - main_layout.addLayout(zielgroesse_row) - - self._btn_vorlage_erstellen = QPushButton("Vorlage erstellen", self) - self._btn_vorlage_erstellen.clicked.connect(self._on_vorlage_erstellen) - main_layout.addWidget(self._btn_vorlage_erstellen) - - main_layout.addStretch(1) - self.setLayout(main_layout) - - def _restore_state(self) -> None: - """Stellt die gespeicherten Combobox-Zustände wieder her.""" - if not self._kartenname_combo or not self._massstab_combo or not self._thema_combo: - return - if not self._zielgroesse_combo or not self._endlosrolle_cb: - return - - saved_kartenname = get_variable(KARTENNAME_VAR, scope="project") - if saved_kartenname in (KARTENNAME_38, KARTENNAME_41): - self._kartenname_combo.setCurrentText(saved_kartenname) - else: - self._kartenname_combo.setCurrentText(KARTENNAME_PLACEHOLDER) - - saved_massstab = get_variable(MASSSTAB_VAR, scope="project") - valid_massstaebe = [MASSSTAB_WIE_KARTENFENSTER, *PLOTMASSSTAB_BY_AUSWAHL.keys()] - if saved_massstab in valid_massstaebe: - self._massstab_combo.setCurrentText(saved_massstab) - else: - self._massstab_combo.setCurrentText(MASSSTAB_WIE_KARTENFENSTER) - - aktuelle_themen = [THEMA_WIE_KARTENFENSTER, *self._get_gespeicherte_themen()] - self._thema_combo.clear() - self._thema_combo.addItems(aktuelle_themen) - - saved_thema = get_variable(THEMA_VAR, scope="project") - if saved_thema in aktuelle_themen: - self._thema_combo.setCurrentText(saved_thema) - else: - self._thema_combo.setCurrentText(THEMA_WIE_KARTENFENSTER) - - saved_zielgroesse = get_variable(ZIELGROESSE_UI_VAR, scope="project") - if saved_zielgroesse in DIN_GROESSEN: - self._zielgroesse_combo.setCurrentText(saved_zielgroesse) - else: - self._zielgroesse_combo.setCurrentText(DIN_STANDARD) - - saved_formfaktor = get_variable(FORMFAKTOR_UI_VAR, scope="project") - self._endlosrolle_cb.setChecked(saved_formfaktor == "Endlosrolle") - - def _on_kartenname_changed(self, value: str) -> None: - """Persistiert die Kartennamen-Auswahl und setzt ``kartenname``.""" - if value in (KARTENNAME_38, KARTENNAME_41): - set_variable(KARTENNAME_VAR, value, scope="project") - else: - set_variable(KARTENNAME_VAR, "", scope="project") - - if self.logic: - self.logic.set_kartenname_for_auswahl(value) - - def _on_massstab_changed(self, value: str) -> None: - """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 ``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 ``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 ``formfaktor``.""" - checked = bool(state) - set_variable(FORMFAKTOR_UI_VAR, "Endlosrolle" if checked else "Blatt", scope="project") - if self.logic: - self.logic.set_formfaktor(checked) - - def _on_vorlage_erstellen(self) -> None: - """Startet die Pipeline Druckvorlage_anlegen.""" - print("[TabB] _on_vorlage_erstellen aufgerufen") - if not self.logic or not self.pruefmanager: - print(f"[TabB] Abbruch: logic={self.logic}, pruefmanager={self.pruefmanager}") - return - - # Layer direkt aus Tab A lesen (unabhängig von Projektvariable) - layer = None - tab_a = self._get_tab_a_widget() - print(f"[TabB] tab_a Widget: {tab_a!r}") - if tab_a is not None: - layer_combo = getattr(tab_a, "layer_combo", None) - print(f"[TabB] layer_combo: {layer_combo!r}") - if layer_combo is not None: - layer = layer_combo.currentLayer() - print(f"[TabB] layer: {layer!r} (Name: {getattr(layer, 'name', lambda: '–')()})") - - print(f"[TabB] Rufe druckvorlage_anlegen auf mit layer={layer!r}, " - f"kartenname={self._kartenname_combo.currentText() if self._kartenname_combo else '?'}, " - f"massstab={self._massstab_combo.currentText() if self._massstab_combo else '?'}, " - f"zielgroesse={self._zielgroesse_combo.currentText() if self._zielgroesse_combo else '?'}, " - f"formfaktor={self._endlosrolle_cb.isChecked() if self._endlosrolle_cb else '?'}") - - result = self.logic.druckvorlage_anlegen( - layer=layer, - kartenname_auswahl=self._kartenname_combo.currentText() if self._kartenname_combo else "", - massstab_auswahl=self._massstab_combo.currentText() if self._massstab_combo else "", - zielgroesse=self._zielgroesse_combo.currentText() if self._zielgroesse_combo else DIN_STANDARD, - formfaktor=self._endlosrolle_cb.isChecked() if self._endlosrolle_cb else False, - ) - - print(f"[TabB] druckvorlage_anlegen Ergebnis: {result}") - if result.get("switch_to_tab_a"): - self._aktiviere_tab_a() - - def _get_tab_widget(self): - """Findet das übergeordnete QTabWidget anhand des ``tabBar``-Attributs.""" - try: - widget = self.parent() - while widget is not None: - if hasattr(widget, "tabBar") and hasattr(widget, "setCurrentIndex"): - return widget - parent_fn = getattr(widget, "parent", None) - widget = parent_fn() if callable(parent_fn) else None - except Exception: - pass - return None - - def _get_tab_a_widget(self): - """Gibt die Tab-A-Widget-Instanz zurück (Index 0 im übergeordneten QTabWidget).""" - tab_widget = self._get_tab_widget() - if tab_widget is None: - return None - try: - return tab_widget.widget(0) - except Exception: - return None - - def _aktiviere_tab_a(self) -> None: - """Wechselt den aktiven Reiter auf Tab A im übergeordneten QTabWidget.""" - tab_widget = self._get_tab_widget() - if tab_widget is not None: - try: - tab_widget.setCurrentIndex(0) - except Exception: - pass - - def _connect_project_signals(self) -> None: - """Verbindet QgsProject-Signale für Projektwechsel/-neuladen.""" - project = QgsProject.instance() - for signal_name in ("readProject", "newProjectCreated", "cleared"): - signal = getattr(project, signal_name, None) - if signal is None: - continue - try: - signal.connect(self._on_project_changed) - except Exception: - pass - - def _on_project_changed(self, *args) -> None: - """Reagiert auf Projektwechsel: Signale neu binden, Combobox und State auffrischen.""" - _ = args - # Alte Theme-Collection-Signals zuerst trennen - self._disconnect_theme_collection_signals() - # Neu verbinden für das jetzt geladene Projekt - self._theme_signal_connected = False - self._connect_theme_collection_signals() - # Ansicht-Liste + gespeicherten State wiederherstellen - self._restore_state() - - def _disconnect_theme_collection_signals(self) -> None: - """Trennt Signale der alten Theme-Collection sauber.""" - collection = self._connected_theme_collection - 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.""" - if self._theme_signal_connected: - return - - try: - theme_collection = QgsProject.instance().mapThemeCollection() - except Exception: - return - - if theme_collection is None: - return - - connected_any = False - for signal_name in ("mapThemesChanged", "changed", "themeChanged"): - signal = getattr(theme_collection, signal_name, None) - if signal is None: - continue - try: - signal.connect(self._refresh_thema_combo_live) - connected_any = True - except Exception: - pass - - if connected_any: - self._connected_theme_collection = theme_collection - self._theme_signal_connected = connected_any - - def _refresh_thema_combo_live(self, *args) -> None: - """Aktualisiert die Thema-Combobox bei Änderungen gespeicherter Layerthemen.""" - _ = args - if not self._thema_combo: - return - - vorherige_auswahl = self._thema_combo.currentText() or THEMA_WIE_KARTENFENSTER - eintraege = [THEMA_WIE_KARTENFENSTER, *self._get_gespeicherte_themen()] - - self._thema_combo.blockSignals(True) - self._thema_combo.clear() - self._thema_combo.addItems(eintraege) - - if vorherige_auswahl in eintraege: - self._thema_combo.setCurrentText(vorherige_auswahl) - else: - self._thema_combo.setCurrentText(THEMA_WIE_KARTENFENSTER) - self._thema_combo.blockSignals(False) - - self._on_thema_changed(self._thema_combo.currentText()) - - def _get_gespeicherte_themen(self) -> list[str]: - """Liefert die Namen der im Projekt gespeicherten Layerthemen (QgsMapThemeCollection).""" - try: - theme_collection = QgsProject.instance().mapThemeCollection() - if theme_collection is None: - return [] - themes = theme_collection.mapThemes() - except Exception: - return [] - - namen: list[str] = [] - for theme_name in themes: - name = str(theme_name or "").strip() - if name and name not in namen: - namen.append(name) - - return namen - - def _get_current_canvas_scale(self) -> float | None: - """Liest den aktuellen Maßstab aus der Kartensicht.""" - try: - canvas = iface.mapCanvas() - if canvas is None: - return None - scale = canvas.scale() - return float(scale) if scale else None - except Exception: - return None -