From 6261b88fee11d0e613762c811ab0d46d94057fc2 Mon Sep 17 00:00:00 2001 From: daniel Date: Thu, 19 Mar 2026 16:32:01 +0100 Subject: [PATCH 01/11] Tab B und Tab B logic angelegt --- ui/dockwidget.py | 4 +- ui/tab_b_logic.py | 57 +++++++++++++++++++++++ ui/tab_b_ui.py | 113 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 172 insertions(+), 2 deletions(-) create mode 100644 ui/tab_b_logic.py create mode 100644 ui/tab_b_ui.py diff --git a/ui/dockwidget.py b/ui/dockwidget.py index c004891..718be90 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.tabs.tab_b import TabB +from sn_plan41.ui.tab_b_ui import TabB 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, SettingsTab] + tabs = [TabA, TabB,SettingsTab] def __init__(self, parent=None, subtitle="", pruefmanager=None, data_grabber=None): super().__init__(parent, subtitle) diff --git a/ui/tab_b_logic.py b/ui/tab_b_logic.py new file mode 100644 index 0000000..7588e6f --- /dev/null +++ b/ui/tab_b_logic.py @@ -0,0 +1,57 @@ +""" +sn_plan41/ui/tab_b_logic.py – Fachlogik für Tab B (Druck) +""" +from __future__ import annotations +from sn_basis.functions.sys_wrapper import get_plugin_root, join_path, file_exists + +from typing import Any, Dict, List, Optional +from collections.abc import Mapping as _Mapping +import os +import datetime +import json +import tempfile + +from sn_basis.functions.qgiscore_wrapper import ( + QgsVectorFileWriter, + QgsVectorLayer, + QgsProject, + QgsGeometry, + QgsFeature, + QgsField, + QgsFeatureRequest, + QgsCoordinateReferenceSystem, + +) + +from sn_basis.functions.variable_wrapper import ( + get_variable, + set_variable, +) +from sn_basis.functions.ly_existence_wrapper import layer_exists +from sn_basis.functions.ly_metadata_wrapper import get_layer_type +from sn_basis.functions.qt_wrapper import QVariant +from sn_basis.functions.dialog_wrapper import create_progress_dialog +from sn_basis.functions.message_wrapper import info, warning, error + + +# Prüfer-Typen +from sn_basis.modules.Pruefmanager import Pruefmanager +from sn_basis.modules.linkpruefer import Linkpruefer +from sn_basis.modules.stilpruefer import Stilpruefer +from sn_basis.modules.Dateipruefer import Dateipruefer +from sn_basis.modules.layerpruefer import Layerpruefer +from sn_basis.modules.LayerLoader import LayerLoader +from sn_basis.modules.Datenschreiber import Datenschreiber + +from sn_basis.modules.pruef_ergebnis import pruef_ergebnis +from sn_basis.modules.DataGrabber import DataGrabber, SourceType, SourceDict +from sn_basis.modules.Datenabruf import Datenabruf + +class TabBLogic: + """ + Kapselt die Fachlogik von Tab B. + """ + + def __init__(self, pruefmanager: Pruefmanager) -> None: + self.pruefmanager = pruefmanager + \ No newline at end of file diff --git a/ui/tab_b_ui.py b/ui/tab_b_ui.py new file mode 100644 index 0000000..e706d25 --- /dev/null +++ b/ui/tab_b_ui.py @@ -0,0 +1,113 @@ +""" +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, + QPushButton, + QToolButton, + QFileDialog, + QMessageBox, + ToolButtonTextBesideIcon, + ArrowDown, + ArrowRight, + SizePolicyPreferred, + SizePolicyMaximum, + QComboBox, +) +from sn_basis.functions.qgisui_wrapper import QgsFileWidget, QgsMapLayerComboBox +from sn_basis.functions.qgiscore_wrapper import QgsProject, QgsMapLayerProxyModel +from sn_basis.functions.variable_wrapper import get_variable, set_variable +from sn_basis.modules.pruef_ergebnis import pruef_ergebnis + +# Services (werden von DockWidget injiziert) +from sn_basis.modules.Pruefmanager import Pruefmanager +from sn_basis.modules.DataGrabber import DataGrabber +from sn_basis.modules.Dateipruefer import Dateipruefer +from sn_plan41.ui.tab_b_logic import TabBLogic + +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._build_ui() + self._restore_state() + + def _build_ui(self) -> None: + """Erstellt die komplette UI-Hierarchie mit allen Gruppen.""" + main_layout = QVBoxLayout() + main_layout.setSpacing(4) + main_layout.setContentsMargins(4, 4, 4, 4) + + + # === LAYER-AUSWAHL === + layer_label = QLabel("Verfahrensgebiet-Layer auswählen") + layer_label.setStyleSheet("font-weight: bold; margin-top: 6px;") + main_layout.addWidget(layer_label) + + self.layer_combo = QgsMapLayerComboBox() + self.layer_combo.setFilters(QgsMapLayerProxyModel.VectorLayer) + self.layer_combo.layerChanged.connect(self._on_layer_changed) + main_layout.addWidget(self.layer_combo) + + def _restore_state(self) -> None: + """Stellt UI-State aus Projektvariablen/Persistenz wieder her.""" + + # === UI CALLBACKS === + def _toggle_group(self, checked: bool) -> None: + """Zeigt/verbirgt Verfahrens-DB-Gruppe.""" + self.group_button.setArrowType(ArrowDown if checked else ArrowRight) + self.group_content.setVisible(checked) + + def _toggle_optional(self, checked: bool) -> None: + """Zeigt/verbirgt optionale Linkliste.""" + self.optional_button.setArrowType(ArrowDown if checked else ArrowRight) + self.optional_content.setVisible(checked) + + def _on_layer_changed(self, layer) -> None: + """Persistiert Layer-Auswahl und registriert Verfahrensgebiet.""" + self._pufferlayer = layer + + if not layer: + return + + # UI-State speichern + set_variable("tab_b_layer_id", layer.id(), scope="project") + + # 🔹 NEU: Verfahrensgebiet explizit registrieren + if self.logic: + self.logic.save_verfahrensgebiet_layer(layer) + -- 2.49.1 From 5c00b4abee38b27682743f684f9636b1a8b75297 Mon Sep 17 00:00:00 2001 From: daniel Date: Fri, 20 Mar 2026 10:11:42 +0100 Subject: [PATCH 02/11] =?UTF-8?q?Comboboxen=20Thema=20und=20Massstab=20hin?= =?UTF-8?q?zugef=C3=BCgt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ui/tab_b_logic.py | 81 +++++++++++++-------------- ui/tab_b_ui.py | 139 ++++++++++++++++++++++++++++++---------------- 2 files changed, 131 insertions(+), 89 deletions(-) diff --git a/ui/tab_b_logic.py b/ui/tab_b_logic.py index 7588e6f..eea9e9e 100644 --- a/ui/tab_b_logic.py +++ b/ui/tab_b_logic.py @@ -2,50 +2,30 @@ sn_plan41/ui/tab_b_logic.py – Fachlogik für Tab B (Druck) """ from __future__ import annotations -from sn_basis.functions.sys_wrapper import get_plugin_root, join_path, file_exists - -from typing import Any, Dict, List, Optional -from collections.abc import Mapping as _Mapping -import os -import datetime -import json -import tempfile - -from sn_basis.functions.qgiscore_wrapper import ( - QgsVectorFileWriter, - QgsVectorLayer, - QgsProject, - QgsGeometry, - QgsFeature, - QgsField, - QgsFeatureRequest, - QgsCoordinateReferenceSystem, - -) - -from sn_basis.functions.variable_wrapper import ( - get_variable, - set_variable, -) -from sn_basis.functions.ly_existence_wrapper import layer_exists -from sn_basis.functions.ly_metadata_wrapper import get_layer_type -from sn_basis.functions.qt_wrapper import QVariant -from sn_basis.functions.dialog_wrapper import create_progress_dialog -from sn_basis.functions.message_wrapper import info, warning, error - - -# Prüfer-Typen +from sn_basis.functions.variable_wrapper import set_variable from sn_basis.modules.Pruefmanager import Pruefmanager -from sn_basis.modules.linkpruefer import Linkpruefer -from sn_basis.modules.stilpruefer import Stilpruefer -from sn_basis.modules.Dateipruefer import Dateipruefer -from sn_basis.modules.layerpruefer import Layerpruefer -from sn_basis.modules.LayerLoader import LayerLoader -from sn_basis.modules.Datenschreiber import Datenschreiber -from sn_basis.modules.pruef_ergebnis import pruef_ergebnis -from sn_basis.modules.DataGrabber import DataGrabber, SourceType, SourceDict -from sn_basis.modules.Datenabruf import Datenabruf + +KARTENNAME_VAR = "sn_kartenname" +PLOTMASSSTAB_VAR = "sn_plotmassstab" +THEMA_38 = "§38" +THEMA_41 = "§41" +MASSSTAB_WIE_KARTENFENSTER = "Wie Kartenfenster" + +KARTENNAME_BY_THEMA = { + THEMA_38: "Planungsübersicht §38 FlurbG", + THEMA_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", +} class TabBLogic: """ @@ -54,4 +34,21 @@ class TabBLogic: def __init__(self, pruefmanager: Pruefmanager) -> None: self.pruefmanager = pruefmanager + + def set_kartenname_for_thema(self, thema: str) -> None: + """Setzt die Projektvariable ``sn_kartenname`` anhand des gewählten Druckthemas.""" + kartenname = KARTENNAME_BY_THEMA.get(thema, "") + 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.""" + 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") \ No newline at end of file diff --git a/ui/tab_b_ui.py b/ui/tab_b_ui.py index e706d25..02b6638 100644 --- a/ui/tab_b_ui.py +++ b/ui/tab_b_ui.py @@ -9,27 +9,26 @@ from sn_basis.functions.qt_wrapper import ( QWidget, QVBoxLayout, QLabel, - QPushButton, - QToolButton, - QFileDialog, - QMessageBox, - ToolButtonTextBesideIcon, - ArrowDown, - ArrowRight, - SizePolicyPreferred, - SizePolicyMaximum, QComboBox, ) -from sn_basis.functions.qgisui_wrapper import QgsFileWidget, QgsMapLayerComboBox -from sn_basis.functions.qgiscore_wrapper import QgsProject, QgsMapLayerProxyModel +from sn_basis.functions.qgisui_wrapper import iface from sn_basis.functions.variable_wrapper import get_variable, set_variable -from sn_basis.modules.pruef_ergebnis import pruef_ergebnis # Services (werden von DockWidget injiziert) from sn_basis.modules.Pruefmanager import Pruefmanager from sn_basis.modules.DataGrabber import DataGrabber -from sn_basis.modules.Dateipruefer import Dateipruefer -from sn_plan41.ui.tab_b_logic import TabBLogic +from sn_plan41.ui.tab_b_logic import ( + TabBLogic, + MASSSTAB_WIE_KARTENFENSTER, + PLOTMASSSTAB_BY_AUSWAHL, +) + + +THEMA_VAR = "tab_b_thema" +THEMA_PLACEHOLDER = "Thema wählen" +THEMA_38 = "§38" +THEMA_41 = "§41" +MASSSTAB_VAR = "tab_b_massstab" class TabB(QWidget): """ @@ -60,54 +59,100 @@ class TabB(QWidget): self.pruefmanager: Optional[Pruefmanager] = None self.logic: Optional[TabBLogic] = None - + self._thema_combo: Optional[QComboBox] = None + self._massstab_combo: Optional[QComboBox] = None self._build_ui() self._restore_state() + 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._thema_combo: + self.logic.set_kartenname_for_thema(self._thema_combo.currentText()) + if self._massstab_combo: + self.logic.set_plotmassstab_for_auswahl( + self._massstab_combo.currentText(), + self._get_current_canvas_scale(), + ) + def _build_ui(self) -> None: - """Erstellt die komplette UI-Hierarchie mit allen Gruppen.""" + """Erstellt die reduzierte UI für die Themenauswahl.""" main_layout = QVBoxLayout() main_layout.setSpacing(4) main_layout.setContentsMargins(4, 4, 4, 4) + thema_label = QLabel("Thema") + thema_label.setStyleSheet("font-weight: bold; margin-top: 6px;") + main_layout.addWidget(thema_label) - # === LAYER-AUSWAHL === - layer_label = QLabel("Verfahrensgebiet-Layer auswählen") - layer_label.setStyleSheet("font-weight: bold; margin-top: 6px;") - main_layout.addWidget(layer_label) - - self.layer_combo = QgsMapLayerComboBox() - self.layer_combo.setFilters(QgsMapLayerProxyModel.VectorLayer) - self.layer_combo.layerChanged.connect(self._on_layer_changed) - main_layout.addWidget(self.layer_combo) + self._thema_combo = QComboBox(self) + self._thema_combo.addItem(THEMA_PLACEHOLDER) + self._thema_combo.addItem(THEMA_38) + self._thema_combo.addItem(THEMA_41) + self._thema_combo.currentTextChanged.connect(self._on_thema_changed) + main_layout.addWidget(self._thema_combo) + + massstab_label = QLabel("Massstab") + 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) + + main_layout.addStretch(1) + self.setLayout(main_layout) def _restore_state(self) -> None: - """Stellt UI-State aus Projektvariablen/Persistenz wieder her.""" - - # === UI CALLBACKS === - def _toggle_group(self, checked: bool) -> None: - """Zeigt/verbirgt Verfahrens-DB-Gruppe.""" - self.group_button.setArrowType(ArrowDown if checked else ArrowRight) - self.group_content.setVisible(checked) - - def _toggle_optional(self, checked: bool) -> None: - """Zeigt/verbirgt optionale Linkliste.""" - self.optional_button.setArrowType(ArrowDown if checked else ArrowRight) - self.optional_content.setVisible(checked) - - def _on_layer_changed(self, layer) -> None: - """Persistiert Layer-Auswahl und registriert Verfahrensgebiet.""" - self._pufferlayer = layer - - if not layer: + """Stellt die gespeicherten Combobox-Zustände wieder her.""" + if not self._thema_combo or not self._massstab_combo: return - # UI-State speichern - set_variable("tab_b_layer_id", layer.id(), scope="project") + saved_thema = get_variable(THEMA_VAR, scope="project") + if saved_thema in (THEMA_38, THEMA_41): + self._thema_combo.setCurrentText(saved_thema) + else: + self._thema_combo.setCurrentText(THEMA_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) + + def _on_thema_changed(self, value: str) -> None: + """Persistiert die Themenauswahl und setzt den Kartennamen.""" + if value in (THEMA_38, THEMA_41): + set_variable(THEMA_VAR, value, scope="project") + else: + set_variable(THEMA_VAR, "", scope="project") - # 🔹 NEU: Verfahrensgebiet explizit registrieren if self.logic: - self.logic.save_verfahrensgebiet_layer(layer) + self.logic.set_kartenname_for_thema(value) + + def _on_massstab_changed(self, value: str) -> None: + """Persistiert Maßstabsauswahl und setzt ``sn_plotmassstab``.""" + set_variable(MASSSTAB_VAR, value, scope="project") + + if self.logic: + self.logic.set_plotmassstab_for_auswahl(value, self._get_current_canvas_scale()) + + 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 -- 2.49.1 From a3b22d044405630c6292bc0ecc5635acedfb94d8 Mon Sep 17 00:00:00 2001 From: daniel Date: Fri, 20 Mar 2026 10:37:08 +0100 Subject: [PATCH 03/11] =?UTF-8?q?Combobox=20Ansicht=20hinzugef=C3=BCgt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ui/tab_b_logic.py | 10 +++++ ui/tab_b_ui.py | 103 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 111 insertions(+), 2 deletions(-) diff --git a/ui/tab_b_logic.py b/ui/tab_b_logic.py index eea9e9e..6de350b 100644 --- a/ui/tab_b_logic.py +++ b/ui/tab_b_logic.py @@ -8,9 +8,11 @@ from sn_basis.modules.Pruefmanager import Pruefmanager KARTENNAME_VAR = "sn_kartenname" PLOTMASSSTAB_VAR = "sn_plotmassstab" +VIEW_VAR = "sn_view" THEMA_38 = "§38" THEMA_41 = "§41" MASSSTAB_WIE_KARTENFENSTER = "Wie Kartenfenster" +ANSICHT_WIE_KARTENFENSTER = "wie kartenfenster" KARTENNAME_BY_THEMA = { THEMA_38: "Planungsübersicht §38 FlurbG", @@ -51,4 +53,12 @@ class TabBLogic: value = PLOTMASSSTAB_BY_AUSWAHL.get(auswahl, "") set_variable(PLOTMASSSTAB_VAR, value, scope="project") + + def set_view_for_auswahl(self, auswahl: str) -> None: + """Setzt ``sn_view`` auf ``aktuell`` oder den gewählten Layer/Themennamen.""" + if auswahl == ANSICHT_WIE_KARTENFENSTER: + set_variable(VIEW_VAR, "aktuell", scope="project") + return + + set_variable(VIEW_VAR, auswahl or "", scope="project") \ No newline at end of file diff --git a/ui/tab_b_ui.py b/ui/tab_b_ui.py index 02b6638..b3e7e9e 100644 --- a/ui/tab_b_ui.py +++ b/ui/tab_b_ui.py @@ -11,6 +11,7 @@ from sn_basis.functions.qt_wrapper import ( QLabel, QComboBox, ) +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 @@ -21,6 +22,7 @@ from sn_plan41.ui.tab_b_logic import ( TabBLogic, MASSSTAB_WIE_KARTENFENSTER, PLOTMASSSTAB_BY_AUSWAHL, + ANSICHT_WIE_KARTENFENSTER, ) @@ -29,6 +31,7 @@ THEMA_PLACEHOLDER = "Thema wählen" THEMA_38 = "§38" THEMA_41 = "§41" MASSSTAB_VAR = "tab_b_massstab" +ANSICHT_VAR = "tab_b_ansicht" class TabB(QWidget): """ @@ -61,10 +64,13 @@ class TabB(QWidget): self.logic: Optional[TabBLogic] = None self._thema_combo: Optional[QComboBox] = None self._massstab_combo: Optional[QComboBox] = None + self._ansicht_combo: Optional[QComboBox] = None + self._theme_signal_connected = False self._build_ui() self._restore_state() + self._connect_theme_collection_signals() def set_services(self, pruefmanager: Pruefmanager, data_grabber: DataGrabber) -> None: """Injiziert Services vom übergeordneten DockWidget.""" @@ -79,6 +85,8 @@ class TabB(QWidget): self._massstab_combo.currentText(), self._get_current_canvas_scale(), ) + if self._ansicht_combo: + self.logic.set_view_for_auswahl(self._ansicht_combo.currentText()) def _build_ui(self) -> None: """Erstellt die reduzierte UI für die Themenauswahl.""" @@ -97,7 +105,7 @@ class TabB(QWidget): self._thema_combo.currentTextChanged.connect(self._on_thema_changed) main_layout.addWidget(self._thema_combo) - massstab_label = QLabel("Massstab") + massstab_label = QLabel("Maßstab") massstab_label.setStyleSheet("font-weight: bold; margin-top: 6px;") main_layout.addWidget(massstab_label) @@ -107,12 +115,22 @@ class TabB(QWidget): self._massstab_combo.currentTextChanged.connect(self._on_massstab_changed) main_layout.addWidget(self._massstab_combo) + ansicht_label = QLabel("Ansicht") + ansicht_label.setStyleSheet("font-weight: bold; margin-top: 6px;") + main_layout.addWidget(ansicht_label) + + self._ansicht_combo = QComboBox(self) + self._ansicht_combo.addItem(ANSICHT_WIE_KARTENFENSTER) + self._ansicht_combo.addItems(self._get_gespeicherte_ansichten()) + self._ansicht_combo.currentTextChanged.connect(self._on_ansicht_changed) + main_layout.addWidget(self._ansicht_combo) + main_layout.addStretch(1) self.setLayout(main_layout) def _restore_state(self) -> None: """Stellt die gespeicherten Combobox-Zustände wieder her.""" - if not self._thema_combo or not self._massstab_combo: + if not self._thema_combo or not self._massstab_combo or not self._ansicht_combo: return saved_thema = get_variable(THEMA_VAR, scope="project") @@ -128,6 +146,16 @@ class TabB(QWidget): else: self._massstab_combo.setCurrentText(MASSSTAB_WIE_KARTENFENSTER) + aktuelle_themen = [ANSICHT_WIE_KARTENFENSTER, *self._get_gespeicherte_ansichten()] + self._ansicht_combo.clear() + self._ansicht_combo.addItems(aktuelle_themen) + + saved_ansicht = get_variable(ANSICHT_VAR, scope="project") + if saved_ansicht in aktuelle_themen: + self._ansicht_combo.setCurrentText(saved_ansicht) + else: + self._ansicht_combo.setCurrentText(ANSICHT_WIE_KARTENFENSTER) + def _on_thema_changed(self, value: str) -> None: """Persistiert die Themenauswahl und setzt den Kartennamen.""" if value in (THEMA_38, THEMA_41): @@ -145,6 +173,77 @@ class TabB(QWidget): if self.logic: self.logic.set_plotmassstab_for_auswahl(value, self._get_current_canvas_scale()) + def _on_ansicht_changed(self, value: str) -> None: + """Persistiert die Ansichtsauswahl und setzt ``sn_view``.""" + set_variable(ANSICHT_VAR, value, scope="project") + if self.logic: + self.logic.set_view_for_auswahl(value) + + def _connect_theme_collection_signals(self) -> None: + """Verbindet Signale der Theme-Collection für Live-Aktualisierung der Ansichtsliste.""" + 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_ansicht_combo_live) + connected_any = True + except Exception: + pass + + self._theme_signal_connected = connected_any + + def _refresh_ansicht_combo_live(self, *args) -> None: + """Aktualisiert die Ansicht-Combobox bei Änderungen gespeicherter Layerthemen.""" + _ = args + if not self._ansicht_combo: + return + + vorherige_auswahl = self._ansicht_combo.currentText() or ANSICHT_WIE_KARTENFENSTER + eintraege = [ANSICHT_WIE_KARTENFENSTER, *self._get_gespeicherte_ansichten()] + + self._ansicht_combo.blockSignals(True) + self._ansicht_combo.clear() + self._ansicht_combo.addItems(eintraege) + + if vorherige_auswahl in eintraege: + self._ansicht_combo.setCurrentText(vorherige_auswahl) + else: + self._ansicht_combo.setCurrentText(ANSICHT_WIE_KARTENFENSTER) + self._ansicht_combo.blockSignals(False) + + self._on_ansicht_changed(self._ansicht_combo.currentText()) + + def _get_gespeicherte_ansichten(self) -> list[str]: + """Liefert die Namen der im Projekt gespeicherten Layerthemen.""" + 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: -- 2.49.1 From 6a4c7b4609fb1a48485e1982438e3ee6912077de Mon Sep 17 00:00:00 2001 From: daniel Date: Fri, 20 Mar 2026 11:31:44 +0100 Subject: [PATCH 04/11] Ansicht-Combobox bleibt bei Projektwechsel konsistent --- ui/tab_b_ui.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/ui/tab_b_ui.py b/ui/tab_b_ui.py index b3e7e9e..da457ef 100644 --- a/ui/tab_b_ui.py +++ b/ui/tab_b_ui.py @@ -66,11 +66,13 @@ class TabB(QWidget): self._massstab_combo: Optional[QComboBox] = None self._ansicht_combo: Optional[QComboBox] = None self._theme_signal_connected = False + self._connected_theme_collection: object = None # Referenz für sauberes Trennen 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.""" @@ -179,6 +181,44 @@ class TabB(QWidget): if self.logic: self.logic.set_view_for_auswahl(value) + 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 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_ansicht_combo_live) + except Exception: + pass + self._connected_theme_collection = None + def _connect_theme_collection_signals(self) -> None: """Verbindet Signale der Theme-Collection für Live-Aktualisierung der Ansichtsliste.""" if self._theme_signal_connected: @@ -203,6 +243,8 @@ class TabB(QWidget): except Exception: pass + if connected_any: + self._connected_theme_collection = theme_collection self._theme_signal_connected = connected_any def _refresh_ansicht_combo_live(self, *args) -> None: -- 2.49.1 From fcf5b113730211d4c82956180194f18369289329 Mon Sep 17 00:00:00 2001 From: daniel Date: Fri, 20 Mar 2026 11:45:36 +0100 Subject: [PATCH 05/11] Bezeichnungen Thema und Ansicht konsistent zur QGIS-Verwendung getauscht (alt=Thema, neu=Kartenname, alt=Ansicht,neu=Thema) --- ui/tab_b_logic.py | 22 ++++---- ui/tab_b_ui.py | 134 +++++++++++++++++++++++----------------------- 2 files changed, 78 insertions(+), 78 deletions(-) diff --git a/ui/tab_b_logic.py b/ui/tab_b_logic.py index 6de350b..f927251 100644 --- a/ui/tab_b_logic.py +++ b/ui/tab_b_logic.py @@ -9,14 +9,14 @@ from sn_basis.modules.Pruefmanager import Pruefmanager KARTENNAME_VAR = "sn_kartenname" PLOTMASSSTAB_VAR = "sn_plotmassstab" VIEW_VAR = "sn_view" -THEMA_38 = "§38" -THEMA_41 = "§41" +KARTENNAME_38 = "§38" +KARTENNAME_41 = "§41" MASSSTAB_WIE_KARTENFENSTER = "Wie Kartenfenster" -ANSICHT_WIE_KARTENFENSTER = "wie kartenfenster" +THEMA_WIE_KARTENFENSTER = "wie kartenfenster" -KARTENNAME_BY_THEMA = { - THEMA_38: "Planungsübersicht §38 FlurbG", - THEMA_41: "Karte zum Plan über die gemeinschaftlichen und öffentlichen Anlagen (§ 41 FlurbG)", +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 = { @@ -37,9 +37,9 @@ class TabBLogic: def __init__(self, pruefmanager: Pruefmanager) -> None: self.pruefmanager = pruefmanager - def set_kartenname_for_thema(self, thema: str) -> None: - """Setzt die Projektvariable ``sn_kartenname`` anhand des gewählten Druckthemas.""" - kartenname = KARTENNAME_BY_THEMA.get(thema, "") + def set_kartenname_for_auswahl(self, auswahl: str) -> None: + """Setzt die Projektvariable ``sn_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: @@ -55,8 +55,8 @@ 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 gewählten Layer/Themennamen.""" - if auswahl == ANSICHT_WIE_KARTENFENSTER: + """Setzt ``sn_view`` auf ``aktuell`` oder den Namen des gewählten Layerthemas.""" + if auswahl == THEMA_WIE_KARTENFENSTER: set_variable(VIEW_VAR, "aktuell", scope="project") return diff --git a/ui/tab_b_ui.py b/ui/tab_b_ui.py index da457ef..c434b3c 100644 --- a/ui/tab_b_ui.py +++ b/ui/tab_b_ui.py @@ -22,16 +22,16 @@ from sn_plan41.ui.tab_b_logic import ( TabBLogic, MASSSTAB_WIE_KARTENFENSTER, PLOTMASSSTAB_BY_AUSWAHL, - ANSICHT_WIE_KARTENFENSTER, + THEMA_WIE_KARTENFENSTER, ) -THEMA_VAR = "tab_b_thema" -THEMA_PLACEHOLDER = "Thema wählen" -THEMA_38 = "§38" -THEMA_41 = "§41" +KARTENNAME_VAR = "tab_b_kartenname" +KARTENNAME_PLACEHOLDER = "Kartenname wählen" +KARTENNAME_38 = "§38" +KARTENNAME_41 = "§41" MASSSTAB_VAR = "tab_b_massstab" -ANSICHT_VAR = "tab_b_ansicht" +THEMA_VAR = "tab_b_thema" class TabB(QWidget): """ @@ -62,9 +62,9 @@ class TabB(QWidget): self.pruefmanager: Optional[Pruefmanager] = None self.logic: Optional[TabBLogic] = None - self._thema_combo: Optional[QComboBox] = None + self._kartenname_combo: Optional[QComboBox] = None self._massstab_combo: Optional[QComboBox] = None - self._ansicht_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 @@ -80,15 +80,15 @@ class TabB(QWidget): self.pruefmanager = pruefmanager self.logic = TabBLogic(pruefmanager=self.pruefmanager) - if self._thema_combo: - self.logic.set_kartenname_for_thema(self._thema_combo.currentText()) + 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._ansicht_combo: - self.logic.set_view_for_auswahl(self._ansicht_combo.currentText()) + if self._thema_combo: + self.logic.set_view_for_auswahl(self._thema_combo.currentText()) def _build_ui(self) -> None: """Erstellt die reduzierte UI für die Themenauswahl.""" @@ -96,16 +96,16 @@ class TabB(QWidget): main_layout.setSpacing(4) main_layout.setContentsMargins(4, 4, 4, 4) - thema_label = QLabel("Thema") - thema_label.setStyleSheet("font-weight: bold; margin-top: 6px;") - main_layout.addWidget(thema_label) + kartenname_label = QLabel("Kartenname") + kartenname_label.setStyleSheet("font-weight: bold; margin-top: 6px;") + main_layout.addWidget(kartenname_label) - self._thema_combo = QComboBox(self) - self._thema_combo.addItem(THEMA_PLACEHOLDER) - self._thema_combo.addItem(THEMA_38) - self._thema_combo.addItem(THEMA_41) - self._thema_combo.currentTextChanged.connect(self._on_thema_changed) - main_layout.addWidget(self._thema_combo) + 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;") @@ -117,29 +117,29 @@ class TabB(QWidget): self._massstab_combo.currentTextChanged.connect(self._on_massstab_changed) main_layout.addWidget(self._massstab_combo) - ansicht_label = QLabel("Ansicht") - ansicht_label.setStyleSheet("font-weight: bold; margin-top: 6px;") - main_layout.addWidget(ansicht_label) + thema_label = QLabel("Thema") + thema_label.setStyleSheet("font-weight: bold; margin-top: 6px;") + main_layout.addWidget(thema_label) - self._ansicht_combo = QComboBox(self) - self._ansicht_combo.addItem(ANSICHT_WIE_KARTENFENSTER) - self._ansicht_combo.addItems(self._get_gespeicherte_ansichten()) - self._ansicht_combo.currentTextChanged.connect(self._on_ansicht_changed) - main_layout.addWidget(self._ansicht_combo) + 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) main_layout.addStretch(1) self.setLayout(main_layout) def _restore_state(self) -> None: """Stellt die gespeicherten Combobox-Zustände wieder her.""" - if not self._thema_combo or not self._massstab_combo or not self._ansicht_combo: + if not self._kartenname_combo or not self._massstab_combo or not self._thema_combo: return - saved_thema = get_variable(THEMA_VAR, scope="project") - if saved_thema in (THEMA_38, THEMA_41): - self._thema_combo.setCurrentText(saved_thema) + saved_kartenname = get_variable(KARTENNAME_VAR, scope="project") + if saved_kartenname in (KARTENNAME_38, KARTENNAME_41): + self._kartenname_combo.setCurrentText(saved_kartenname) else: - self._thema_combo.setCurrentText(THEMA_PLACEHOLDER) + self._kartenname_combo.setCurrentText(KARTENNAME_PLACEHOLDER) saved_massstab = get_variable(MASSSTAB_VAR, scope="project") valid_massstaebe = [MASSSTAB_WIE_KARTENFENSTER, *PLOTMASSSTAB_BY_AUSWAHL.keys()] @@ -148,25 +148,25 @@ class TabB(QWidget): else: self._massstab_combo.setCurrentText(MASSSTAB_WIE_KARTENFENSTER) - aktuelle_themen = [ANSICHT_WIE_KARTENFENSTER, *self._get_gespeicherte_ansichten()] - self._ansicht_combo.clear() - self._ansicht_combo.addItems(aktuelle_themen) + aktuelle_themen = [THEMA_WIE_KARTENFENSTER, *self._get_gespeicherte_themen()] + self._thema_combo.clear() + self._thema_combo.addItems(aktuelle_themen) - saved_ansicht = get_variable(ANSICHT_VAR, scope="project") - if saved_ansicht in aktuelle_themen: - self._ansicht_combo.setCurrentText(saved_ansicht) + saved_thema = get_variable(THEMA_VAR, scope="project") + if saved_thema in aktuelle_themen: + self._thema_combo.setCurrentText(saved_thema) else: - self._ansicht_combo.setCurrentText(ANSICHT_WIE_KARTENFENSTER) + self._thema_combo.setCurrentText(THEMA_WIE_KARTENFENSTER) - def _on_thema_changed(self, value: str) -> None: - """Persistiert die Themenauswahl und setzt den Kartennamen.""" - if value in (THEMA_38, THEMA_41): - set_variable(THEMA_VAR, value, scope="project") + def _on_kartenname_changed(self, value: str) -> None: + """Persistiert die Kartennamen-Auswahl und setzt ``sn_kartenname``.""" + if value in (KARTENNAME_38, KARTENNAME_41): + set_variable(KARTENNAME_VAR, value, scope="project") else: - set_variable(THEMA_VAR, "", scope="project") + set_variable(KARTENNAME_VAR, "", scope="project") if self.logic: - self.logic.set_kartenname_for_thema(value) + self.logic.set_kartenname_for_auswahl(value) def _on_massstab_changed(self, value: str) -> None: """Persistiert Maßstabsauswahl und setzt ``sn_plotmassstab``.""" @@ -175,9 +175,9 @@ class TabB(QWidget): if self.logic: self.logic.set_plotmassstab_for_auswahl(value, self._get_current_canvas_scale()) - def _on_ansicht_changed(self, value: str) -> None: - """Persistiert die Ansichtsauswahl und setzt ``sn_view``.""" - set_variable(ANSICHT_VAR, value, scope="project") + def _on_thema_changed(self, value: str) -> None: + """Persistiert die Thema-Auswahl und setzt ``sn_view``.""" + set_variable(THEMA_VAR, value, scope="project") if self.logic: self.logic.set_view_for_auswahl(value) @@ -214,13 +214,13 @@ class TabB(QWidget): if signal is None: continue try: - signal.disconnect(self._refresh_ansicht_combo_live) + signal.disconnect(self._refresh_thema_combo_live) except Exception: pass self._connected_theme_collection = None def _connect_theme_collection_signals(self) -> None: - """Verbindet Signale der Theme-Collection für Live-Aktualisierung der Ansichtsliste.""" + """Verbindet Signale der Theme-Collection für Live-Aktualisierung der Themenliste.""" if self._theme_signal_connected: return @@ -238,7 +238,7 @@ class TabB(QWidget): if signal is None: continue try: - signal.connect(self._refresh_ansicht_combo_live) + signal.connect(self._refresh_thema_combo_live) connected_any = True except Exception: pass @@ -247,29 +247,29 @@ class TabB(QWidget): self._connected_theme_collection = theme_collection self._theme_signal_connected = connected_any - def _refresh_ansicht_combo_live(self, *args) -> None: - """Aktualisiert die Ansicht-Combobox bei Änderungen gespeicherter Layerthemen.""" + def _refresh_thema_combo_live(self, *args) -> None: + """Aktualisiert die Thema-Combobox bei Änderungen gespeicherter Layerthemen.""" _ = args - if not self._ansicht_combo: + if not self._thema_combo: return - vorherige_auswahl = self._ansicht_combo.currentText() or ANSICHT_WIE_KARTENFENSTER - eintraege = [ANSICHT_WIE_KARTENFENSTER, *self._get_gespeicherte_ansichten()] + vorherige_auswahl = self._thema_combo.currentText() or THEMA_WIE_KARTENFENSTER + eintraege = [THEMA_WIE_KARTENFENSTER, *self._get_gespeicherte_themen()] - self._ansicht_combo.blockSignals(True) - self._ansicht_combo.clear() - self._ansicht_combo.addItems(eintraege) + self._thema_combo.blockSignals(True) + self._thema_combo.clear() + self._thema_combo.addItems(eintraege) if vorherige_auswahl in eintraege: - self._ansicht_combo.setCurrentText(vorherige_auswahl) + self._thema_combo.setCurrentText(vorherige_auswahl) else: - self._ansicht_combo.setCurrentText(ANSICHT_WIE_KARTENFENSTER) - self._ansicht_combo.blockSignals(False) + self._thema_combo.setCurrentText(THEMA_WIE_KARTENFENSTER) + self._thema_combo.blockSignals(False) - self._on_ansicht_changed(self._ansicht_combo.currentText()) + self._on_thema_changed(self._thema_combo.currentText()) - def _get_gespeicherte_ansichten(self) -> list[str]: - """Liefert die Namen der im Projekt gespeicherten Layerthemen.""" + 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: -- 2.49.1 From ff5fd990bc05035ef644da3b73ebe45420774afe Mon Sep 17 00:00:00 2001 From: daniel Date: Fri, 20 Mar 2026 12:01:44 +0100 Subject: [PATCH 06/11] =?UTF-8?q?Blattgr=C3=B6=C3=9Fen-Auswahl=20und=20End?= =?UTF-8?q?losrollen-Checkbox=20erg=C3=A4nzt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ui/tab_b_logic.py | 20 +++++++++++++++++ ui/tab_b_ui.py | 55 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+) diff --git a/ui/tab_b_logic.py b/ui/tab_b_logic.py index f927251..495fb52 100644 --- a/ui/tab_b_logic.py +++ b/ui/tab_b_logic.py @@ -9,6 +9,8 @@ from sn_basis.modules.Pruefmanager import Pruefmanager KARTENNAME_VAR = "sn_kartenname" PLOTMASSSTAB_VAR = "sn_plotmassstab" VIEW_VAR = "sn_view" +ZIELGROESSE_VAR = "sn_zielgroesse" +FORMFAKTOR_VAR = "sn_formfaktor" KARTENNAME_38 = "§38" KARTENNAME_41 = "§41" MASSSTAB_WIE_KARTENFENSTER = "Wie Kartenfenster" @@ -29,6 +31,16 @@ PLOTMASSSTAB_BY_AUSWAHL = { "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. @@ -61,4 +73,12 @@ class TabBLogic: return 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.""" + 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``.""" + set_variable(FORMFAKTOR_VAR, "Endlosrolle" if endlosrolle else "Blatt", scope="project") \ No newline at end of file diff --git a/ui/tab_b_ui.py b/ui/tab_b_ui.py index c434b3c..4260cd3 100644 --- a/ui/tab_b_ui.py +++ b/ui/tab_b_ui.py @@ -10,6 +10,8 @@ from sn_basis.functions.qt_wrapper import ( QVBoxLayout, QLabel, QComboBox, + QCheckBox, + QHBoxLayout, ) from sn_basis.functions.qgiscore_wrapper import QgsProject from sn_basis.functions.qgisui_wrapper import iface @@ -23,6 +25,10 @@ from sn_plan41.ui.tab_b_logic import ( MASSSTAB_WIE_KARTENFENSTER, PLOTMASSSTAB_BY_AUSWAHL, THEMA_WIE_KARTENFENSTER, + DIN_GROESSEN, + DIN_STANDARD, + ZIELGROESSE_VAR, + FORMFAKTOR_VAR, ) @@ -32,6 +38,8 @@ 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): """ @@ -67,6 +75,8 @@ class TabB(QWidget): 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._build_ui() @@ -89,6 +99,10 @@ class TabB(QWidget): ) 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.""" @@ -127,6 +141,23 @@ class TabB(QWidget): 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) + main_layout.addStretch(1) self.setLayout(main_layout) @@ -134,6 +165,8 @@ class TabB(QWidget): """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): @@ -158,6 +191,15 @@ class TabB(QWidget): 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 ``sn_kartenname``.""" if value in (KARTENNAME_38, KARTENNAME_41): @@ -181,6 +223,19 @@ class TabB(QWidget): 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``.""" + 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``.""" + checked = bool(state) + set_variable(FORMFAKTOR_UI_VAR, "Endlosrolle" if checked else "Blatt", scope="project") + if self.logic: + self.logic.set_formfaktor(checked) + def _connect_project_signals(self) -> None: """Verbindet QgsProject-Signale für Projektwechsel/-neuladen.""" project = QgsProject.instance() -- 2.49.1 From bfc9fae32427d0186272d254ddfa8e7e35f029c0 Mon Sep 17 00:00:00 2001 From: daniel Date: Fri, 20 Mar 2026 12:43:01 +0100 Subject: [PATCH 07/11] =?UTF-8?q?Vorlagen-Pipeline=20angelegt=20(Start=20b?= =?UTF-8?q?is=20Pr=C3=BCfung=20Atlas-Anzahl)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ui/tab_b_logic.py | 131 +++++++++++++++++++++++++++++++++++++++++++++- ui/tab_b_ui.py | 35 ++++++++++++- 2 files changed, 163 insertions(+), 3 deletions(-) diff --git a/ui/tab_b_logic.py b/ui/tab_b_logic.py index 495fb52..2e12e2c 100644 --- a/ui/tab_b_logic.py +++ b/ui/tab_b_logic.py @@ -2,8 +2,11 @@ sn_plan41/ui/tab_b_logic.py – Fachlogik für Tab B (Druck) """ from __future__ import annotations -from sn_basis.functions.variable_wrapper import set_variable +import math +from sn_basis.functions.variable_wrapper import set_variable, get_variable +from sn_basis.functions.qgiscore_wrapper import QgsProject, get_layer_extent from sn_basis.modules.Pruefmanager import Pruefmanager +from sn_basis.modules.layerpruefer import Layerpruefer KARTENNAME_VAR = "sn_kartenname" @@ -81,4 +84,128 @@ class TabBLogic: def set_formfaktor(self, endlosrolle: bool) -> None: """Setzt ``sn_formfaktor`` auf ``Endlosrolle`` oder ``Blatt``.""" set_variable(FORMFAKTOR_VAR, "Endlosrolle" if endlosrolle else "Blatt", scope="project") - \ No newline at end of file + + # ───────────────────────────────────────────────────────────────────────── + # Pipeline: Druckvorlage_anlegen + # ───────────────────────────────────────────────────────────────────────── + + def druckvorlage_anlegen( + self, + 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. + + 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 ───────────────────────────── + layer_id = get_variable("tab_a_layer_id", scope="project") or "" + layer = None + if layer_id: + try: + layer = QgsProject.instance().mapLayer(layer_id) + except Exception: + pass + + 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} + + set_variable("sn_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 auf 2000 mm begrenzt + ziel_w, ziel_h = 2000.0, float(din_dims[1]) + else: + ziel_w, ziel_h = float(din_dims[0]), float(din_dims[1]) + + # ─── 7. Passt auf ein Blatt? ────────────────────────────────────── + if plotgroesse_w <= ziel_w and plotgroesse_h <= ziel_h: + 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) + 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, + ) + if not ja: + return {"ok": False, "switch_to_tab_a": False, "atlas_seiten": anzahl_seiten} + + 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 index 4260cd3..99799b8 100644 --- a/ui/tab_b_ui.py +++ b/ui/tab_b_ui.py @@ -12,6 +12,7 @@ from sn_basis.functions.qt_wrapper import ( QComboBox, QCheckBox, QHBoxLayout, + QPushButton, ) from sn_basis.functions.qgiscore_wrapper import QgsProject from sn_basis.functions.qgisui_wrapper import iface @@ -77,8 +78,8 @@ class TabB(QWidget): 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() @@ -158,6 +159,10 @@ class TabB(QWidget): 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) @@ -236,6 +241,34 @@ class TabB(QWidget): if self.logic: self.logic.set_formfaktor(checked) + def _on_vorlage_erstellen(self) -> None: + """Startet die Pipeline Druckvorlage_anlegen.""" + if not self.logic or not self.pruefmanager: + return + + result = self.logic.druckvorlage_anlegen( + 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, + ) + + if result.get("switch_to_tab_a"): + self._aktiviere_tab_a() + + def _aktiviere_tab_a(self) -> None: + """Wechselt zum Tab A im übergeordneten TabWidget.""" + try: + widget = self.parent() + while widget is not None: + if hasattr(widget, "setCurrentIndex") and hasattr(widget, "count"): + widget.setCurrentIndex(0) + return + parent_fn = getattr(widget, "parent", None) + widget = parent_fn() if callable(parent_fn) else None + except Exception: + pass + def _connect_project_signals(self) -> None: """Verbindet QgsProject-Signale für Projektwechsel/-neuladen.""" project = QgsProject.instance() -- 2.49.1 From d21483ce53e3084da3c70489a832cf62decd5b71 Mon Sep 17 00:00:00 2001 From: daniel Date: Fri, 20 Mar 2026 14:02:15 +0100 Subject: [PATCH 08/11] =?UTF-8?q?Layouterzeugung=20erg=C3=A4nzt=20(nur=20E?= =?UTF-8?q?inzelbl=C3=A4tter)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ui/layout.py | 125 ++++++++++++++++++++++++++++++++++++++++++++++ ui/tab_b_logic.py | 66 +++++++++++++++++++----- ui/tab_b_ui.py | 50 +++++++++++++++++-- 3 files changed, 224 insertions(+), 17 deletions(-) create mode 100644 ui/layout.py diff --git a/ui/layout.py b/ui/layout.py new file mode 100644 index 0000000..06cd3b9 --- /dev/null +++ b/ui/layout.py @@ -0,0 +1,125 @@ +""" +sn_plan41/ui/layout.py – Aufbau von Drucklayouts für Plan41. +""" +from __future__ import annotations + +import math +from typing import Any + +from sn_basis.functions.qt_wrapper import QFont +from sn_basis.functions.qgiscore_wrapper import ( + QgsLayoutItem, + QgsLayoutItemLabel, + QgsLayoutItemMap, + QgsLayoutPoint, + QgsLayoutSize, + QgsPrintLayout, + QgsProject, + QgsUnitTypes, +) +from sn_basis.functions.qgisui_wrapper import open_layout_designer + + +MM = QgsUnitTypes.LayoutMillimeters + + +class Layout: + """Erzeugt ein QGIS-Layout für den Druck.""" + + def __init__(self, project: Any | None = None) -> None: + self.project = project or QgsProject.instance() + + 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) + 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") + + hauptkarte = QgsLayoutItemMap(layout) + hauptkarte.setId("Hauptkarte") + set_rect = getattr(hauptkarte, "setRect", None) + if callable(set_rect): + set_rect(0.0, 0.0, map_width_mm, map_height_mm) + hauptkarte.attemptMove(QgsLayoutPoint(10.0, 10.0, MM)) + hauptkarte.attemptResize(QgsLayoutSize(map_width_mm, map_height_mm, MM)) + 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}") + hauptkarte.setExtent(extent) + if isinstance(plotmassstab, (int, float)) and math.isfinite(plotmassstab) and plotmassstab > 0: + hauptkarte.setScale(plotmassstab) + else: + print(f"[Layout] WARN: ungültiger plotmassstab={plotmassstab!r}, setScale übersprungen") + print(f"[Layout] Hauptkarte angelegt: pos=(10,10), size=({map_width_mm:.1f}x{map_height_mm:.1f})") + + if thema and thema != "aktuell": + follow_theme = getattr(hauptkarte, "setFollowVisibilityPreset", None) + set_theme_name = getattr(hauptkarte, "setFollowVisibilityPresetName", None) + if callable(follow_theme): + follow_theme(True) + if callable(set_theme_name): + set_theme_name(thema) + print(f"[Layout] Kartenthema gesetzt: '{thema}'") + + layout.addLayoutItem(hauptkarte) + print("[Layout] Hauptkarte zum Layout hinzugefügt") + + 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
" + ) + 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.attemptMove(QgsLayoutPoint(map_width_mm + 30.0, page_height_mm - 120.0, 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") + 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 2e12e2c..8f0024c 100644 --- a/ui/tab_b_logic.py +++ b/ui/tab_b_logic.py @@ -4,9 +4,10 @@ sn_plan41/ui/tab_b_logic.py – Fachlogik für Tab B (Druck) from __future__ import annotations import math from sn_basis.functions.variable_wrapper import set_variable, get_variable -from sn_basis.functions.qgiscore_wrapper import QgsProject, get_layer_extent +from sn_basis.functions.qgiscore_wrapper import get_layer_extent from sn_basis.modules.Pruefmanager import Pruefmanager from sn_basis.modules.layerpruefer import Layerpruefer +from sn_plan41.ui.layout import Layout KARTENNAME_VAR = "sn_kartenname" @@ -91,6 +92,7 @@ class TabBLogic: def druckvorlage_anlegen( self, + layer: object, kartenname_auswahl: str, massstab_auswahl: str, zielgroesse: str, @@ -100,6 +102,11 @@ class TabBLogic: 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 @@ -108,14 +115,6 @@ class TabBLogic: ``atlas_seiten`` (int): Anzahl benötigter Seiten (1 = kein Atlas). """ # ─── 1. Verfahrensgebiet-Layer prüfen ───────────────────────────── - layer_id = get_variable("tab_a_layer_id", scope="project") or "" - layer = None - if layer_id: - try: - layer = QgsProject.instance().mapLayer(layer_id) - except Exception: - pass - lp = Layerpruefer(layer=layer) ergebnis = lp.pruefe() if not ergebnis.ok: @@ -125,6 +124,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") # ─── 2. Kartenname prüfen ───────────────────────────────────────── @@ -173,13 +173,55 @@ class TabBLogic: # ─── 6. Zielgröße bestimmen ─────────────────────────────────────── din_dims = DIN_GROESSEN.get(zielgroesse, DIN_GROESSEN[DIN_STANDARD]) - if formfaktor: # Endlosrolle: X-Richtung auf 2000 mm begrenzt - ziel_w, ziel_h = 2000.0, float(din_dims[1]) + 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]) - # ─── 7. Passt auf ein Blatt? ────────────────────────────────────── + # ─── 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 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 ──────────────────────────── diff --git a/ui/tab_b_ui.py b/ui/tab_b_ui.py index 99799b8..3f1f98b 100644 --- a/ui/tab_b_ui.py +++ b/ui/tab_b_ui.py @@ -243,31 +243,71 @@ class TabB(QWidget): 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 _aktiviere_tab_a(self) -> None: - """Wechselt zum Tab A im übergeordneten TabWidget.""" + 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, "setCurrentIndex") and hasattr(widget, "count"): - widget.setCurrentIndex(0) - return + 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.""" -- 2.49.1 From 284f2a2a038c802ece8c27e56f25b6ca9183b804 Mon Sep 17 00:00:00 2001 From: daniel Date: Fri, 20 Mar 2026 14:31:10 +0100 Subject: [PATCH 09/11] bug/Hauptkarte verschwindet beim Druck behoben --- ui/layout.py | 106 +++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 91 insertions(+), 15 deletions(-) diff --git a/ui/layout.py b/ui/layout.py index 06cd3b9..a4da86e 100644 --- a/ui/layout.py +++ b/ui/layout.py @@ -59,36 +59,110 @@ class Layout: 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 + print( + f"[Layout] Kartenbild-Kanten: rechts={map_right_mm:.1f} mm, unten={map_bottom_mm:.1f} mm" + ) + hauptkarte = QgsLayoutItemMap(layout) hauptkarte.setId("Hauptkarte") - set_rect = getattr(hauptkarte, "setRect", None) - if callable(set_rect): - set_rect(0.0, 0.0, map_width_mm, map_height_mm) - hauptkarte.attemptMove(QgsLayoutPoint(10.0, 10.0, 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 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}") - hauptkarte.setExtent(extent) + + 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: - hauptkarte.setScale(plotmassstab) + 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") - print(f"[Layout] Hauptkarte angelegt: pos=(10,10), size=({map_width_mm:.1f}x{map_height_mm:.1f})") - + + # Rahmen aktivieren + set_frame_enabled = getattr(hauptkarte, "setFrameEnabled", None) + if callable(set_frame_enabled): + try: + set_frame_enabled(True) + print(f"[Layout] Rahmen aktiviert") + except Exception as exc: + print(f"[Layout] WARN: setFrameEnabled fehlgeschlagen: {exc}") + + set_frame_stroke_width = getattr(hauptkarte, "setFrameStrokeWidth", None) + if callable(set_frame_stroke_width): + try: + set_frame_stroke_width(0.5) + print(f"[Layout] Rahmenstrichbreite auf 0.5 mm gesetzt") + except Exception as exc: + print(f"[Layout] WARN: setFrameStrokeWidth(0.5) fehlgeschlagen: {exc}") + + # Kartenthema setzen if thema and thema != "aktuell": follow_theme = getattr(hauptkarte, "setFollowVisibilityPreset", None) set_theme_name = getattr(hauptkarte, "setFollowVisibilityPresetName", None) if callable(follow_theme): - follow_theme(True) + 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): - set_theme_name(thema) - print(f"[Layout] Kartenthema gesetzt: '{thema}'") - - layout.addLayoutItem(hauptkarte) - print("[Layout] Hauptkarte zum Layout hinzugefügt") + 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") quellenangabe = QgsLayoutItemLabel(layout) quellenangabe.setId("Quellenangabe") @@ -113,7 +187,9 @@ class Layout: 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.attemptMove(QgsLayoutPoint(map_width_mm + 30.0, page_height_mm - 120.0, MM)) + 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") -- 2.49.1 From a54d4fbe3c1c2477e185e4297ec6add7fe08fab0 Mon Sep 17 00:00:00 2001 From: daniel Date: Fri, 20 Mar 2026 22:32:30 +0100 Subject: [PATCH 10/11] =?UTF-8?q?Altlasfunktion=20eingebaut=20(wenn=20plot?= =?UTF-8?q?gr=C3=B6=C3=9Fe=20gr=C3=B6=C3=9Fer=20als=20Zielformat)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/atlasobjekte.qml | 43 ++++++ ui/layout.py | 230 ++++++++++++++++++++++++++++++ ui/tab_b_logic.py | 304 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 577 insertions(+) create mode 100644 assets/atlasobjekte.qml diff --git a/assets/atlasobjekte.qml b/assets/atlasobjekte.qml new file mode 100644 index 0000000..952ddd5 --- /dev/null +++ b/assets/atlasobjekte.qml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui/layout.py b/ui/layout.py index a4da86e..4aa52c8 100644 --- a/ui/layout.py +++ b/ui/layout.py @@ -199,3 +199,233 @@ class Layout: 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) + + 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 + + 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)) + + # Verifiziere mit Units-bewussten Methoden (rect() kann andere Units verwenden). + actual_w = None + actual_h = None + actual_x = None + actual_y = None + + # Versuche zuerst, die Größe mit Unit-Methoden zu lesen + try: + if hasattr(hauptkarte, 'sizeWithUnits'): + size_item = hauptkarte.sizeWithUnits() + if hasattr(size_item, 'width') and hasattr(size_item, 'height'): + actual_w = float(size_item.width()) + actual_h = float(size_item.height()) + except Exception: + pass + + try: + if hasattr(hauptkarte, 'positionWithUnits'): + pos_item = hauptkarte.positionWithUnits() + if hasattr(pos_item, 'x') and hasattr(pos_item, 'y'): + actual_x = float(pos_item.x()) + actual_y = float(pos_item.y()) + except Exception: + pass + + # Fallback: nutze rect() und teile durch UnitFaktor, falls nötig + if actual_w is None or actual_h is None: + try: + actual_rect = hauptkarte.rect() + if actual_rect is not None: + actual_w = float(actual_rect.width()) + actual_h = float(actual_rect.height()) + actual_x = float(actual_rect.x()) + actual_y = float(actual_rect.y()) + except Exception: + pass + + if actual_w is not None and actual_h is not None: + print(f"[Layout] Atlas Hauptkarte gesetzt: x={map_left_mm:.1f}mm→{actual_x:.1f}mm, y={map_top_mm:.1f}mm→{actual_y:.1f}mm, width={map_width_mm:.1f}mm→{actual_w:.1f}mm, height={map_height_mm:.1f}mm→{actual_h:.1f}mm") + else: + print(f"[Layout] Atlas Hauptkarte: x={map_left_mm:.1f}mm, y={map_top_mm:.1f}mm, width={map_width_mm:.1f}mm, height={map_height_mm:.1f}mm") + + if extent is not None and hasattr(extent, "isNull") and callable(extent.isNull) and not extent.isNull(): + try: + hauptkarte.setExtent(extent) + except Exception: + pass + + if isinstance(plotmassstab, (int, float)) and math.isfinite(plotmassstab) and plotmassstab > 0: + try: + hauptkarte.setScale(plotmassstab) + except Exception: + pass + + set_frame_enabled = getattr(hauptkarte, "setFrameEnabled", None) + if callable(set_frame_enabled): + try: + set_frame_enabled(True) + except Exception: + pass + + set_frame_stroke_width = getattr(hauptkarte, "setFrameStrokeWidth", None) + if callable(set_frame_stroke_width): + try: + set_frame_stroke_width(0.5) + except Exception: + pass + + if thema and thema != "aktuell": + follow_theme = getattr(hauptkarte, "setFollowVisibilityPreset", None) + set_theme_name = getattr(hauptkarte, "setFollowVisibilityPresetName", None) + if callable(follow_theme): + try: + follow_theme(True) + except Exception: + pass + if callable(set_theme_name): + try: + set_theme_name(thema) + except Exception: + pass + + set_atlas_driven = getattr(hauptkarte, "setAtlasDriven", None) + if callable(set_atlas_driven): + try: + set_atlas_driven(True) + except Exception: + pass + + # Fester Atlas-Maßstab: plotmassstab bleibt unverändert. + set_scaling_mode = getattr(hauptkarte, "setAtlasScalingMode", None) + if callable(set_scaling_mode): + fixed_mode = getattr(QgsLayoutItemMap, "Fixed", None) + if fixed_mode is not None: + try: + set_scaling_mode(fixed_mode) + except Exception: + pass + + set_atlas_margin = getattr(hauptkarte, "setAtlasMargin", None) + if callable(set_atlas_margin): + try: + set_atlas_margin(0.0) + except Exception: + pass + + set_keep_layer_set = getattr(hauptkarte, "setKeepLayerSet", None) + if callable(set_keep_layer_set): + try: + set_keep_layer_set(True) + except Exception: + pass + + # Sicherheit: Größe nochmal nach Atlas-Konfiguration setzen, um sicherzustellen, dass sie nicht von der Atlas-Einstellung überschrieben wurde. + hauptkarte.attemptResize(QgsLayoutSize(map_width_mm, map_height_mm, MM)) + print(f"[Layout] Atlas Hauptkarte Größe nach Atlas-Konfiguration erneut gesetzt: {map_width_mm:.1f}mm × {map_height_mm:.1f}mm") + + 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
" + ) + 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: + 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) + open_layout_designer(layout) + return layout diff --git a/ui/tab_b_logic.py b/ui/tab_b_logic.py index 8f0024c..14157f8 100644 --- a/ui/tab_b_logic.py +++ b/ui/tab_b_logic.py @@ -3,8 +3,20 @@ 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 @@ -53,6 +65,140 @@ class TabBLogic: 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 + + 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 ``sn_kartenname`` anhand der Kartennamen-Auswahl.""" kartenname = KARTENNAME_BY_AUSWAHL.get(auswahl, "") @@ -178,9 +324,20 @@ class TabBLogic: 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" @@ -247,7 +404,154 @@ class TabBLogic: "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 -- 2.49.1 From 4d0dcc0310fb0c182d25527e23ccf8f73e595f32 Mon Sep 17 00:00:00 2001 From: daniel Date: Fri, 20 Mar 2026 22:56:10 +0100 Subject: [PATCH 11/11] Atlasobjekte sind jetzt weniger langgestrckt --- ui/tab_b_logic.py | 123 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) diff --git a/ui/tab_b_logic.py b/ui/tab_b_logic.py index 14157f8..a1d1b35 100644 --- a/ui/tab_b_logic.py +++ b/ui/tab_b_logic.py @@ -94,6 +94,93 @@ class TabBLogic: 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, @@ -396,6 +483,42 @@ class TabBLogic: 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( -- 2.49.1