diff --git a/changelog.txt b/changelog.txt
index 267d226..6fee27b 100644
--- a/changelog.txt
+++ b/changelog.txt
@@ -1,4 +1,10 @@
---
+Version 26.04.1-unstable:
+---
+Version 26.3.3-unstable:
+---
+Version 26.3.2-unstable:
+---
Version 26.3.1-testing:
- unstable als testing veröffentlicht
---
diff --git a/metadata.txt b/metadata.txt
index 498fa7e..c9ed787 100644
--- a/metadata.txt
+++ b/metadata.txt
@@ -1,11 +1,11 @@
[general]
-version=26.3.1-testing
+version=26.04.1-unstable
name=LNO Sachsen | Plan41
description=Plugin zum Erzeugen der Pläne nach §38 und §41
author=Daniel Helbig, Michael Otto
homepage=https://entwicklung.flurneuordnung-sachsen.de/AG_QGIS/Plugin_SN_Plan41
tracker=https://entwicklung.flurneuordnung-sachsen.de/AG_QGIS/Plugin_SN_Plan41/issues
-repository=https://entwicklung.flurneuordnung-sachsen.de/AG_QGIS/Plugin_Test_Action/src/branch/testing/
+repository=https://entwicklung.flurneuordnung-sachsen.de/AG_QGIS/Plugin_Test_Action/src/branch/unstable/
qgisMinimumVersion=3.40
experimental=true
diff --git a/tests/test_tab_a_logic.py b/tests/test_tab_a_logic.py
index eed5bd4..94c3bd8 100644
--- a/tests/test_tab_a_logic.py
+++ b/tests/test_tab_a_logic.py
@@ -94,59 +94,6 @@ class TestTabALogic(unittest.TestCase):
# -----------------------------------------------------
# 6. Layer-ID speichern
# -----------------------------------------------------
- def test_verfahrensgebiet_layer_id_storage(self):
- logic = TabALogic()
-
- class MockLayer:
- def id(self):
- return "layer-123"
-
- logic.save_verfahrensgebiet_layer(MockLayer())
-
- stored = get_variable("verfahrensgebiet_layer", scope="project")
- self.assertEqual(stored, "layer-123")
-
- # -----------------------------------------------------
- # 7. Ungültiger Layer wird ignoriert
- # -----------------------------------------------------
- def test_invalid_layer_is_rejected(self):
- logic = TabALogic()
-
- class InvalidLayer:
- pass
-
- logic.save_verfahrensgebiet_layer(InvalidLayer())
-
- stored = get_variable("verfahrensgebiet_layer", scope="project")
- self.assertEqual(stored, "")
- #-----------------------------------------------------
- # 8. Layer-ID wirft Exception
- #----------------------------------------------------
- def test_layer_id_raises_exception(self):
- logic = TabALogic()
-
- class BadLayer:
- def id(self):
- raise RuntimeError("boom")
-
- logic.save_verfahrensgebiet_layer(BadLayer())
-
- stored = get_variable("verfahrensgebiet_layer", scope="project")
- self.assertEqual(stored, "")
- # -----------------------------------------------------
- # 11. Layer ID wird leer zurückgegeben
- # -----------------------------------------------------
- def test_layer_id_returns_empty(self):
- logic = TabALogic()
-
- class EmptyLayer:
- def id(self):
- return ""
-
- logic.save_verfahrensgebiet_layer(EmptyLayer())
-
- stored = get_variable("verfahrensgebiet_layer", scope="project")
- self.assertEqual(stored, "")
if __name__ == "__main__":
diff --git a/tests/test_tab_a_preconditions.py b/tests/test_tab_a_preconditions.py
new file mode 100644
index 0000000..5196baf
--- /dev/null
+++ b/tests/test_tab_a_preconditions.py
@@ -0,0 +1,111 @@
+import unittest
+from unittest.mock import patch
+from typing import Any, cast
+
+from sn_plan41.ui.tab_a_logic import TabALogic
+
+
+class _DummyPruefmanager:
+ def __init__(self) -> None:
+ self.hinweise: list[tuple[str, str]] = []
+
+ def verarbeite(self, ergebnis):
+ return ergebnis
+
+ def zeige_hinweis(self, titel: str, meldung: str) -> None:
+ self.hinweise.append((titel, meldung))
+
+
+class _DummyDateiErgebnis:
+ def __init__(self, ok: bool = True, kontext: str = "C:/tmp/v_db.gpkg") -> None:
+ self.ok = ok
+ self.kontext = kontext
+
+
+class _DummyProgress:
+ def __init__(self) -> None:
+ self.closed = False
+
+ def set_total(self, _value: int) -> None:
+ pass
+
+ def set_value(self, _value: int) -> None:
+ pass
+
+ def set_label(self, _value: str) -> None:
+ pass
+
+ def close(self) -> None:
+ self.closed = True
+
+
+class _DummyProject:
+ def mapLayer(self, _layer_id: str):
+ return None
+
+
+class _DummyGrabber:
+ pass
+
+
+class TestTabAPreconditions(unittest.TestCase):
+
+ def _make_logic(self) -> tuple[TabALogic, _DummyPruefmanager]:
+ pruefmanager = _DummyPruefmanager()
+ logic = TabALogic(
+ pruefmanager=cast(Any, pruefmanager),
+ link_pruefer=cast(Any, object()),
+ stil_pruefer=cast(Any, object()),
+ )
+ return logic, pruefmanager
+
+ def test_get_verfahrensgebiet_layer_shows_hint_when_layer_id_missing(self):
+ logic, pruefmanager = self._make_logic()
+
+ with patch.object(logic, "load_verfahrensgebiet_layer_id", return_value=None):
+ layer = logic._get_verfahrensgebiet_layer()
+
+ self.assertIsNone(layer)
+ self.assertEqual(len(pruefmanager.hinweise), 1)
+ self.assertIn("Verfahrensgebiet", pruefmanager.hinweise[0][0])
+
+ @patch("sn_plan41.ui.tab_a_logic.QgsProject.instance", return_value=_DummyProject())
+ def test_get_verfahrensgebiet_layer_shows_hint_when_layer_not_in_project(self, _mock_project_instance):
+ logic, pruefmanager = self._make_logic()
+
+ with patch.object(logic, "load_verfahrensgebiet_layer_id", return_value="layer_404"):
+ layer = logic._get_verfahrensgebiet_layer()
+
+ self.assertIsNone(layer)
+ self.assertEqual(len(pruefmanager.hinweise), 1)
+ self.assertIn("Verfahrensgebiet", pruefmanager.hinweise[0][0])
+
+ @patch("sn_plan41.ui.tab_a_logic.Dateipruefer")
+ def test_pipeline_aborts_and_shows_hint_when_verfahrensgebiet_missing(
+ self,
+ mock_dateipruefer,
+ ):
+ logic, pruefmanager = self._make_logic()
+ logic.data_grabber = cast(Any, _DummyGrabber())
+
+ mock_dateipruefer.return_value.pruefe.return_value = _DummyDateiErgebnis(ok=True, kontext="C:/tmp/v_db.gpkg")
+
+ progress = _DummyProgress()
+
+ with patch("sn_plan41.ui.tab_a_logic.create_progress_dialog", return_value=progress):
+ with patch.object(logic, "_resolve_linkliste", return_value="C:/tmp/linkliste.xlsx"):
+ with patch.object(logic, "load_verfahrensgebiet_layer_id", return_value=None):
+ result = logic._on_run_pipeline(
+ source="C:/tmp/v_db.gpkg",
+ linkliste=None,
+ raumfilter="Verfahrensgebiet",
+ )
+
+ self.assertIsNone(result)
+ self.assertEqual(len(pruefmanager.hinweise), 1)
+ self.assertIn("Verfahrensgebiet", pruefmanager.hinweise[0][0])
+ self.assertTrue(progress.closed)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/ui/dockwidget.py b/ui/dockwidget.py
index 718be90..51388b3 100644
--- a/ui/dockwidget.py
+++ b/ui/dockwidget.py
@@ -1,14 +1,14 @@
#sn_plan41/ui/dockwidget.py
from sn_basis.ui.tabs.settings_tab import SettingsTab
from sn_plan41.ui.tab_a_ui import TabA
-from sn_plan41.ui.tab_b_ui import TabB
+from sn_basis.ui.tabs.print_tab import PrintTab
from sn_basis.ui.base_dockwidget import BaseDockWidget
from sn_basis.functions.qt_wrapper import QTabWidget
from sn_basis.functions.message_wrapper import error
class DockWidget(BaseDockWidget):
- tabs = [TabA, TabB,SettingsTab]
+ tabs = [TabA, PrintTab, SettingsTab]
def __init__(self, parent=None, subtitle="", pruefmanager=None, data_grabber=None):
super().__init__(parent, subtitle)
diff --git a/ui/layout.py b/ui/layout.py
deleted file mode 100644
index a2ec82f..0000000
--- a/ui/layout.py
+++ /dev/null
@@ -1,1191 +0,0 @@
-"""
-sn_plan41/ui/layout.py – Aufbau von Drucklayouts für Plan41.
-"""
-from __future__ import annotations
-
-import math
-import importlib
-from typing import Any
-
-from sn_basis.functions.qt_wrapper import QFont
-from sn_basis.functions.qgiscore_wrapper import (
- QgsLayoutItem,
- QgsLayoutItemLabel,
- QgsLayoutItemLegend,
- QgsLayoutItemMap,
- QgsLayoutPoint,
- QgsLayoutSize,
- QgsPrintLayout,
- QgsProject,
- QgsUnitTypes,
-)
-from sn_basis.functions.qgisui_wrapper import open_layout_designer
-
-
-MM = QgsUnitTypes.LayoutMillimeters
-
-
-class Layout:
- """Erzeugt ein QGIS-Layout für den Druck."""
-
- _TEXT_FORMAT_TEMPLATES = {
- "Format_1": {
- "font_family": "Arial",
- "font_size": 11,
- "font_style": "normal",
- },
- "Format_2": {
- "font_family": "Arial",
- "font_size": 13,
- "font_style": "bold",
- },
- "Format_3": {
- "font_family": "Arial",
- "font_size": 16,
- "font_style": "bold",
- },
- }
-
- _OBJECT_TEMPLATES = {
- "hauptkarte": {
- "id": "Hauptkarte",
- "object_name": "Hauptkarte",
- },
- "quellenangabe": {
- "id": "Quellenangabe",
- "object_name": "Quellenangabe",
- "size_mm": (180.0, 100.0),
- "format": "Format_1",
- },
- "seitenzahl": {
- "id": "Seitenzahl",
- "object_name": "Seitenzahl",
- "size_mm": (60.0, 8.0),
- "format": "Format_1",
- },
- "textfeld": {
- "id": "Textfeld",
- "object_name": "Textfeld",
- "size_mm": (60.0, 8.0),
- "format": "Format_1",
- "html_mode": True,
- "text": "",
- },
- "legende": {
- "id": "Legende",
- "object_name": "Legende",
- "size_mm": (180.0, 60.0),
- },
- }
-
- _QUELLENANGABE_TEXT = (
- "Quelle Geobasisdaten: GeoSN, "
- "dl-de/by-2-0
"
- "Quelle Fachdaten: Darstellung auf der Grundlage von Daten und mit Erlaubnis des "
- "Sächsischen Landesamtes für Umwelt, Landwirtschaft und Geologie
"
- "Basemap:"
- "© GeoBasis-DE / BKG ([%year($now)%]) "
- "CC BY 4.0 "
- "mit teilweise angepasster Signatur
"
- )
-
- _SEITENZAHL_TEXT = "Blatt [% attribute(@atlas_feature, 'Seitenzahl') %] von [% @atlas_totalfeatures %]"
-
- def __init__(self, project: Any | None = None) -> None:
- self.project = project or QgsProject.instance()
- self._current_layout: Any | None = None
-
- def _create_map_item(
- self,
- layout: Any,
- template_name: str,
- x_mm: float,
- y_mm: float,
- width_mm: float,
- height_mm: float,
- object_name: str | None = None,
- ) -> Any:
- template = self._OBJECT_TEMPLATES[template_name]
- map_item = QgsLayoutItemMap(layout)
- map_item.setId(template["id"])
- set_object_name = getattr(map_item, "setObjectName", None)
- if callable(set_object_name):
- set_object_name(object_name or template.get("object_name", template["id"]))
- layout.addLayoutItem(map_item)
- map_item.attemptMove(QgsLayoutPoint(x_mm, y_mm, MM))
- map_item.attemptResize(QgsLayoutSize(width_mm, height_mm, MM))
- return map_item
-
- def _set_item_frame_stroke_width(self, item: Any, stroke_width_mm: float) -> None:
- set_frame_stroke_width = getattr(item, "setFrameStrokeWidth", None)
- if not callable(set_frame_stroke_width):
- return
-
- try:
- set_frame_stroke_width(stroke_width_mm)
- return
- except Exception:
- pass
-
- try:
- qgis_core = importlib.import_module("qgis.core")
- QgsLayoutMeasurement = getattr(qgis_core, "QgsLayoutMeasurement", None)
- except Exception:
- QgsLayoutMeasurement = None
-
- if QgsLayoutMeasurement is not None:
- try:
- set_frame_stroke_width(QgsLayoutMeasurement(stroke_width_mm, MM))
- return
- except Exception:
- pass
-
- frame_stroke_width = getattr(item, "frameStrokeWidth", None)
- if callable(frame_stroke_width):
- try:
- measurement = frame_stroke_width()
- set_length = getattr(measurement, "setLength", None)
- if callable(set_length):
- set_length(stroke_width_mm)
- set_frame_stroke_width(measurement)
- except Exception:
- pass
-
- def add_text_label(
- self,
- text: str,
- x_mm: float,
- y_mm: float,
- template_name: str = "textfeld",
- text_format: str | None = None,
- width_mm: float | None = None,
- height_mm: float | None = None,
- html_mode: bool = False,
- expression_enabled: bool = False,
- object_name: str | None = None,
- ) -> Any:
- """Einfache öffentliche Methode zum Hinzufügen eines Text-Labels.
-
- Beispiel: add_text_label("Ausgabedatum: 2026-03-23", 100, 200, text_format="Format_1")
- """
- if self._current_layout is None:
- raise RuntimeError("Kein Layout aktiv. Bitte create_single_page_layout() oder create_atlas_layout() aufrufen.")
- return self._create_label_item(
- self._current_layout,
- template_name,
- text,
- x_mm,
- y_mm,
- text_format=text_format,
- width_mm=width_mm,
- height_mm=height_mm,
- html_mode=html_mode,
- expression_enabled=expression_enabled,
- object_name=object_name,
- )
-
- def _setup_layout_objects(
- self,
- kartenfenster_untere_rechte_ecke_mm: tuple,
- hauptkarte: Any,
- map_top_mm: float,
- map_height_mm: float,
- is_atlas: bool = False,
- ) -> None:
- """Zentrale Methode zum Hinzufügen aller Standard-Objekte ins Layout.
-
- Diese Methode wird sowohl von create_single_page_layout() als auch von create_atlas_layout() aufgerufen.
- Relatipunkt des Objekts ist die untere linke Ecke, berechnet von der unteren, rechten Ecke des Kartefensters
- Args:
- kartenfenster_untere_rechte_ecke_mm: Tupel (x_mm, y_mm) der unteren rechten Kartenecke
- is_atlas: True für Atlas-Layout, False für Einzelblatt-Layout
- """
- if self._current_layout is None:
- raise RuntimeError("Kein Layout aktiv.")
-
- def _coords_rel_untere_rechte_ecke(rel_x_mm: float, rel_y_mm: float) -> tuple[float, float]:
- return (
- kartenfenster_untere_rechte_ecke_mm[0] + rel_x_mm,
- kartenfenster_untere_rechte_ecke_mm[1] + rel_y_mm,
- )
-
- mode_html = getattr(QgsLayoutItemLabel, "ModeHtml", None)
- print(f"[Layout] QgsLayoutItemLabel.ModeHtml={mode_html!r}")
- lower_left = getattr(getattr(QgsLayoutItem, "ReferencePoint", object), "LowerLeft", None)
- print(f"[Layout] QgsLayoutItem.ReferencePoint.LowerLeft={lower_left!r}")
-
- legende_x = kartenfenster_untere_rechte_ecke_mm[0] + 10.0
- legende_height_mm = max(map_height_mm - 122.0, 20.0)
- legende_y = map_top_mm
- legende = self._create_legend_item(
- self._current_layout,
- "legende",
- hauptkarte,
- legende_x,
- legende_y,
- width_mm=180.0,
- height_mm=legende_height_mm,
- object_name="Legende",
- )
-
- legende_hoehe_mm = self._configure_legend_size(
- legende,
- width_limit_mm=180.0,
- column_count=1,
- equal_column_width=False,
- )
-
- quellenangabe_x, quellenangabe_y = _coords_rel_untere_rechte_ecke(10.0, -102.0)
- quellenangabe_hoehe_mm = 20.0
- quellenangabe_obere_y = quellenangabe_y - quellenangabe_hoehe_mm
- legende_untere_y = legende_y + legende_hoehe_mm
-
- if legende_untere_y > quellenangabe_obere_y:
- legende_hoehe_mm = self._configure_legend_size(
- legende,
- width_limit_mm=180.0,
- column_count=2,
- equal_column_width=True,
- )
- print("[Layout] Legende überlappt Quellenangabe – auf 2 Spalten umgestellt")
-
- print("[Layout] Legende zum Layout hinzugefügt")
-
- # Quellenangabe:
-
- self.add_text_label(
- self._QUELLENANGABE_TEXT,
- quellenangabe_x,
- quellenangabe_y,
- width_mm=180.0,
- height_mm=20.0,
- text_format="Format_1",
- html_mode=True,
- expression_enabled=True,
- object_name="Quellenangabe",
- )
- print("[Layout] Quellenangabe zum Layout hinzugefügt")
- # Kartenschild-Box, -Linien und -Text werden hier ergänzt (TODO)
- #Kartenschild-Box
- kartenschild_x, kartenschild_y = _coords_rel_untere_rechte_ecke(10.0, 0.0)
- self._create_rectangle_box_item(
- self._current_layout,
- "Kartenschild",
- kartenschild_x,
- kartenschild_y,
- 180.0,
- 92.0,
- stroke_width_mm=1.0,
- )
- print("[Layout] Kartenschild-Box zum Layout hinzugefügt")
-
- #Kartenschild-Linien
- kartenschild_line_x, _ = _coords_rel_untere_rechte_ecke(10.0, 0.0)
- kartenschild_line_offsets_y = (-10.0, -20.0, -30.0, -46.0, -62.0, -82.0)
- for index, offset_y in enumerate(kartenschild_line_offsets_y, start=1):
- _, line_y = _coords_rel_untere_rechte_ecke(0.0, offset_y)
- self._create_rectangle_box_item(
- self._current_layout,
- f"Kartenschild_Linie_{index}",
- kartenschild_line_x,
- line_y,
- 180.0,
- 0.0,
- stroke_width_mm=0.1,
- )
- print("[Layout] 6 Kartenschild-Linien zum Layout hinzugefügt")
-
- #Kartenschild-Text
- #Herausgeber
- herausgeber_x, herausgeber_y = _coords_rel_untere_rechte_ecke(16.0, -84.0)
- self.add_text_label(
- "[% @sn_amt %]",
- herausgeber_x,
- herausgeber_y,
- width_mm=171.0,
- height_mm=4.0,
- text_format="Format_1",
- expression_enabled=True,
- object_name="Herausgeber",
- )
- #Bezeichnung_Flurbereinigung
- bezeichnung_flurbereinigung_x, bezeichnung_flurbereinigung_y = _coords_rel_untere_rechte_ecke(16.0, -75.0)
- self.add_text_label(
- "[% @sn_bezeichnung %]",
- bezeichnung_flurbereinigung_x,
- bezeichnung_flurbereinigung_y,
- width_mm=171.0,
- height_mm=4.0,
- text_format="Format_1",
- expression_enabled=True,
- object_name="Bezeichnung_Flurbereinigung",
- )
-
- #Name Flurbereinigung
- name_flurbereinigung_x, name_flurbereinigung_y = _coords_rel_untere_rechte_ecke(16.0, -64.0)
- self.add_text_label(
- "[% @sn_name %]",
- name_flurbereinigung_x,
- name_flurbereinigung_y,
- width_mm=171.0,
- height_mm=5.0,
- text_format="Format_2",
- expression_enabled=True,
- object_name="Name_Flurbereinigung",
- )
-
- #Stadt_Gemeinde
- stadt_gemeinde_x, stadt_gemeinde_y = _coords_rel_untere_rechte_ecke(16.0, -56.0)
- self.add_text_label(
- "[% @sn_gemeinden %]",
- stadt_gemeinde_x,
- stadt_gemeinde_y,
- width_mm=171.0,
- height_mm=4.0,
- text_format="Format_1",
- expression_enabled=True,
- object_name="Stadt_Gemeinde",
- )
-
- #Landkreis
- landkreis_x, landkreis_y = _coords_rel_untere_rechte_ecke(16.0, -48.0)
- self.add_text_label(
- "[% @sn_landkreise_proj %]",
- landkreis_x,
- landkreis_y,
- width_mm=171.0,
- height_mm=4.0,
- text_format="Format_1",
- expression_enabled=True,
- object_name="Landkreis",
- )
-
- #Kartenname
- kartenname_x, kartenname_y = _coords_rel_untere_rechte_ecke(16.0, -34.0)
- self.add_text_label(
- "[% @sn_kartenname %]",
- kartenname_x,
- kartenname_y,
- width_mm=137.0,
- height_mm=5.0,
- text_format="Format_3",
- expression_enabled=True,
- object_name="Kartenname",
- )
-
- #Ergänzung(Seitenzahl)
-
- #gefertigt
- gefertigt_x, gefertigt_y = _coords_rel_untere_rechte_ecke(16.0, -23.0)
- self.add_text_label(
- "Gefertigt: ",
- gefertigt_x,
- gefertigt_y,
- width_mm=81.0,
- height_mm=4.0,
- text_format="Format_1",
- expression_enabled=True,
- object_name="Gefertigt",
- )
- #geprüft
- geprüft_x, geprüft_y = _coords_rel_untere_rechte_ecke(101.0, -23.0)
- self.add_text_label(
- "Geprüft: ",
- geprüft_x,
- geprüft_y,
- width_mm=81.0,
- height_mm=4.0,
- text_format="Format_1",
- expression_enabled=True,
- object_name="Geprüft",
- )
- #Maßstab
- maßstab_x, maßstab_y = _coords_rel_untere_rechte_ecke(16.0, -13.0)
- self.add_text_label(
- "Maßstab: 1 : [%round(map_get(item_variables('Hauptkarte'),'map_scale'),0)%]",
- maßstab_x,
- maßstab_y,
- width_mm=81.0,
- height_mm=4.0,
- text_format="Format_1",
- expression_enabled=True,
- object_name="Maßstab",
- )
- #Ausgabedatum
- ausgabedatum_x, ausgabedatum_y = _coords_rel_untere_rechte_ecke(101.0, -13.0)
- self.add_text_label(
- "Ausgabedatum: [%format_date(now(), 'dd.MM.yyyy')%]",
- ausgabedatum_x,
- ausgabedatum_y,
- width_mm=81.0,
- height_mm=4.0,
- text_format="Format_1",
- expression_enabled=True,
- object_name="Ausgabedatum",
- )
- #Aktenzeichen
- aktenzeichen_x, aktenzeichen_y = _coords_rel_untere_rechte_ecke(16.0, -3.0)
- self.add_text_label(
- "Aktenzeichen: ",
- aktenzeichen_x,
- aktenzeichen_y,
- width_mm=171.0,
- height_mm=4.0,
- text_format="Format_1",
- expression_enabled=True,
- object_name="Aktenzeichen",
- )
- # Seitenzahl nur bei Atlas-Layout
- if is_atlas:
- seitenzahl_x, seitenzahl_y = _coords_rel_untere_rechte_ecke(166.0, -33.0)
- self._create_label_item(
- self._current_layout,
- "seitenzahl",
- self._SEITENZAHL_TEXT,
- seitenzahl_x,
- seitenzahl_y,
- expression_enabled=True,
- )
- print("[Layout] Seitenzahl zum Layout hinzugefügt")
-
- def _create_label_item(
- self,
- layout: Any,
- template_name: str,
- text: str,
- x_mm: float,
- y_mm: float,
- *,
- text_format: str | None = None,
- font_name: str | None = None,
- font_size: int | None = None,
- font_style: str | None = None,
- width_mm: float | None = None,
- height_mm: float | None = None,
- html_mode: bool = False,
- expression_enabled: bool = False,
- object_name: str | None = None,
- ) -> Any:
- template = self._OBJECT_TEMPLATES[template_name]
- label = QgsLayoutItemLabel(layout)
- item_id = object_name or template["id"]
- label.setId(item_id)
- set_object_name = getattr(label, "setObjectName", None)
- if callable(set_object_name):
- set_object_name(item_id)
- label.setText(text)
-
- if html_mode:
- set_mode = getattr(label, "setMode", None)
- mode_html = getattr(QgsLayoutItemLabel, "ModeHtml", None)
- if callable(set_mode) and mode_html is not None:
- set_mode(mode_html)
-
- if expression_enabled:
- set_expr_enabled = getattr(label, "setExpressionEnabled", None)
- if callable(set_expr_enabled):
- try:
- set_expr_enabled(True)
- except Exception:
- pass
-
- format_name = text_format or template.get("format", "Format_1")
- format_template = self._TEXT_FORMAT_TEMPLATES.get(format_name, self._TEXT_FORMAT_TEMPLATES["Format_1"])
- resolved_font_name = font_name or format_template["font_family"]
- resolved_font_size = font_size or format_template["font_size"]
- resolved_font_style = font_style or format_template["font_style"]
-
- font = QFont(resolved_font_name, resolved_font_size)
- if hasattr(font, "setBold"):
- font.setBold(resolved_font_style == "bold")
- if hasattr(font, "setItalic"):
- font.setItalic(resolved_font_style == "italic")
- label.setFont(font)
-
- set_reference_point = getattr(label, "setReferencePoint", None)
- lower_left = getattr(getattr(QgsLayoutItem, "ReferencePoint", object), "LowerLeft", None)
- if callable(set_reference_point) and lower_left is not None:
- set_reference_point(lower_left)
-
- template_width, template_height = template.get("size_mm", (0.0, 0.0))
- label.attemptMove(QgsLayoutPoint(x_mm, y_mm, MM))
- label.attemptResize(
- QgsLayoutSize(
- width_mm if width_mm is not None else template_width,
- height_mm if height_mm is not None else template_height,
- MM,
- )
- )
- set_locked = getattr(label, "setLocked", None)
- if callable(set_locked):
- try:
- set_locked(True)
- except Exception:
- pass
- layout.addLayoutItem(label)
- return label
-
- def _resolve_visible_map_layers(self, map_item: Any) -> list[Any]:
- layers_method = getattr(map_item, "layers", None)
- if callable(layers_method):
- try:
- resolved_layers = layers_method()
- if isinstance(resolved_layers, (list, tuple)):
- layers = [layer for layer in resolved_layers if layer is not None]
- if layers:
- return layers
- except Exception:
- pass
-
- layer_tree_root_method = getattr(self.project, "layerTreeRoot", None)
- if not callable(layer_tree_root_method):
- return []
-
- try:
- root = layer_tree_root_method()
- except Exception:
- return []
-
- find_layers = getattr(root, "findLayers", None)
- if not callable(find_layers):
- return []
-
- visible_layers: list[Any] = []
- try:
- tree_layers = find_layers()
- if not isinstance(tree_layers, (list, tuple)):
- return []
- for tree_layer in tree_layers:
- is_visible = getattr(tree_layer, "itemVisibilityChecked", None)
- if callable(is_visible) and not is_visible():
- continue
- layer = getattr(tree_layer, "layer", None)
- resolved_layer = layer() if callable(layer) else None
- if resolved_layer is not None:
- visible_layers.append(resolved_layer)
- except Exception:
- return []
-
- return visible_layers
-
- def _create_legend_item(
- self,
- layout: Any,
- template_name: str,
- linked_map: Any,
- x_mm: float,
- y_mm: float,
- *,
- width_mm: float | None = None,
- height_mm: float | None = None,
- object_name: str | None = None,
- ) -> Any:
- template = self._OBJECT_TEMPLATES[template_name]
- legend = QgsLayoutItemLegend(layout)
- item_id = object_name or template["id"]
- legend.setId(item_id)
-
- set_object_name = getattr(legend, "setObjectName", None)
- if callable(set_object_name):
- set_object_name(item_id)
-
- set_linked_map = getattr(legend, "setLinkedMap", None)
- if callable(set_linked_map):
- try:
- set_linked_map(linked_map)
- except Exception:
- pass
-
- set_title = getattr(legend, "setTitle", None)
- if callable(set_title):
- try:
- set_title("Legende")
- except Exception:
- pass
-
- set_auto_update_model = getattr(legend, "setAutoUpdateModel", None)
- if callable(set_auto_update_model):
- try:
- set_auto_update_model(False)
- except Exception:
- pass
-
- set_filter_by_map = getattr(legend, "setLegendFilterByMapEnabled", None)
- if callable(set_filter_by_map):
- try:
- set_filter_by_map(False)
- except Exception:
- pass
-
- model_method = getattr(legend, "model", None)
- if callable(model_method):
- try:
- model = model_method()
- root_group_method = getattr(model, "rootGroup", None)
- root_group = root_group_method() if callable(root_group_method) else None
- clear = getattr(root_group, "clear", None)
- if callable(clear):
- clear()
- add_layer = getattr(root_group, "addLayer", None)
- if callable(add_layer):
- for layer in self._resolve_visible_map_layers(linked_map):
- add_layer(layer)
- except Exception:
- pass
-
- set_reference_point = getattr(legend, "setReferencePoint", None)
- upper_left = getattr(getattr(QgsLayoutItem, "ReferencePoint", object), "UpperLeft", None)
- if callable(set_reference_point) and upper_left is not None:
- set_reference_point(upper_left)
-
- template_width, template_height = template.get("size_mm", (0.0, 0.0))
- legend.attemptMove(QgsLayoutPoint(x_mm, y_mm, MM))
- legend.attemptResize(
- QgsLayoutSize(
- width_mm if width_mm is not None else template_width,
- height_mm if height_mm is not None else template_height,
- MM,
- )
- )
-
- set_frame_enabled = getattr(legend, "setFrameEnabled", None)
- if callable(set_frame_enabled):
- try:
- set_frame_enabled(True)
- except Exception:
- pass
-
- self._set_item_frame_stroke_width(legend, 0.3)
-
- set_background_enabled = getattr(legend, "setBackgroundEnabled", None)
- if callable(set_background_enabled):
- try:
- set_background_enabled(False)
- except Exception:
- pass
-
- set_locked = getattr(legend, "setLocked", None)
- if callable(set_locked):
- try:
- set_locked(True)
- except Exception:
- pass
-
- layout.addLayoutItem(legend)
-
- refresh = getattr(legend, "refresh", None)
- if callable(refresh):
- try:
- refresh()
- except Exception:
- pass
-
- return legend
-
- def _get_item_size_mm(self, item: Any) -> tuple[float | None, float | None]:
- def _to_float(value: Any) -> float | None:
- if isinstance(value, (int, float)):
- return float(value)
- if isinstance(value, str):
- try:
- return float(value)
- except Exception:
- return None
- return None
-
- size_with_units = getattr(item, "sizeWithUnits", None)
- if callable(size_with_units):
- try:
- size = size_with_units()
- get_width = getattr(size, "width", None)
- get_height = getattr(size, "height", None)
- width = _to_float(get_width()) if callable(get_width) else None
- height = _to_float(get_height()) if callable(get_height) else None
- if width is not None and height is not None:
- return width, height
- except Exception:
- pass
-
- rect = getattr(item, "rect", None)
- if callable(rect):
- try:
- item_rect = rect()
- get_width = getattr(item_rect, "width", None)
- get_height = getattr(item_rect, "height", None)
- width = _to_float(get_width()) if callable(get_width) else None
- height = _to_float(get_height()) if callable(get_height) else None
- return width, height
- except Exception:
- pass
-
- return None, None
-
- def _configure_legend_size(
- self,
- legend: Any,
- *,
- width_limit_mm: float,
- column_count: int,
- equal_column_width: bool,
- ) -> float:
- set_locked = getattr(legend, "setLocked", None)
- if callable(set_locked):
- try:
- set_locked(False)
- except Exception:
- pass
-
- set_column_count = getattr(legend, "setColumnCount", None)
- if callable(set_column_count):
- try:
- set_column_count(column_count)
- except Exception:
- pass
-
- set_equal_column_width = getattr(legend, "setEqualColumnWidth", None)
- if callable(set_equal_column_width):
- try:
- set_equal_column_width(equal_column_width)
- except Exception:
- pass
-
- refresh = getattr(legend, "refresh", None)
- if callable(refresh):
- try:
- refresh()
- except Exception:
- pass
-
- adjust_box_size = getattr(legend, "adjustBoxSize", None)
- if callable(adjust_box_size):
- try:
- adjust_box_size()
- except Exception:
- pass
-
- width_mm, height_mm = self._get_item_size_mm(legend)
- resolved_width = width_mm if width_mm is not None else width_limit_mm
- resolved_height = height_mm if height_mm is not None else 20.0
- final_width = min(resolved_width, width_limit_mm)
-
- legend.attemptResize(QgsLayoutSize(final_width, resolved_height, MM))
-
- if callable(refresh):
- try:
- refresh()
- except Exception:
- pass
-
- _, updated_height_mm = self._get_item_size_mm(legend)
-
- if callable(set_locked):
- try:
- set_locked(True)
- except Exception:
- pass
-
- return updated_height_mm if updated_height_mm is not None else resolved_height
-
- def _create_rectangle_box_item(
- self,
- layout: Any,
- object_name: str,
- x_mm: float,
- y_mm: float,
- width_mm: float,
- height_mm: float,
- stroke_width_mm: float = 0.3,
- ) -> Any:
- box = QgsLayoutItemLabel(layout)
- box.setId(object_name)
-
- set_object_name = getattr(box, "setObjectName", None)
- if callable(set_object_name):
- set_object_name(object_name)
-
- box.setText("")
-
- set_reference_point = getattr(box, "setReferencePoint", None)
- lower_left = getattr(getattr(QgsLayoutItem, "ReferencePoint", object), "LowerLeft", None)
- if callable(set_reference_point) and lower_left is not None:
- set_reference_point(lower_left)
-
- box.attemptMove(QgsLayoutPoint(x_mm, y_mm, MM))
- box.attemptResize(QgsLayoutSize(width_mm, height_mm, MM))
-
- set_frame_enabled = getattr(box, "setFrameEnabled", None)
- if callable(set_frame_enabled):
- try:
- set_frame_enabled(True)
- except Exception:
- pass
-
- self._set_item_frame_stroke_width(box, stroke_width_mm)
-
- set_background_enabled = getattr(box, "setBackgroundEnabled", None)
- if callable(set_background_enabled):
- try:
- set_background_enabled(False)
- except Exception:
- pass
-
- set_locked = getattr(box, "setLocked", None)
- if callable(set_locked):
- try:
- set_locked(True)
- except Exception:
- pass
-
- layout.addLayoutItem(box)
- return box
-
- def create_single_page_layout(
- self,
- name: str,
- page_width_mm: float,
- page_height_mm: float,
- map_width_mm: float,
- map_height_mm: float,
- extent: Any,
- plotmassstab: float,
- thema: str = "",
- ) -> Any:
- """Erzeugt ein einseitiges Layout und öffnet es im Designer."""
- print(f"[Layout] create_single_page_layout: name='{name}', "
- f"page=({page_width_mm}x{page_height_mm}), map=({map_width_mm:.1f}x{map_height_mm:.1f}), "
- f"massstab={plotmassstab}, thema='{thema}'")
- layout_manager = self.project.layoutManager()
- print(f"[Layout] layoutManager: {layout_manager!r}")
- existing_layout = layout_manager.layoutByName(name)
- if existing_layout is not None:
- raise ValueError(f"Eine Vorlage mit der Bezeichnung '{name}' existiert bereits.")
-
- layout = QgsPrintLayout(self.project)
- layout.initializeDefaults()
- layout.setName(name)
- self._current_layout = layout
- print(f"[Layout] QgsPrintLayout erstellt: {layout!r}")
-
- page = layout.pageCollection().page(0)
- page.setPageSize(QgsLayoutSize(page_width_mm, page_height_mm, MM))
- print(f"[Layout] Seitengröße gesetzt: {page_width_mm}x{page_height_mm} mm")
-
- map_left_mm = 10.0
- map_top_mm = 10.0
- map_right_mm = map_left_mm + map_width_mm
- map_bottom_mm = map_top_mm + map_height_mm
- kartenfenster_untere_rechte_ecke_mm = (map_right_mm, map_bottom_mm)
- print(
- f"[Layout] Kartenbild-Kanten: rechts={map_right_mm:.1f} mm, unten={map_bottom_mm:.1f} mm"
- )
-
- hauptkarte = self._create_map_item(
- layout,
- "hauptkarte",
- map_left_mm,
- map_top_mm,
- map_width_mm,
- map_height_mm,
- )
- print(f"[Layout] QgsLayoutItemMap erstellt")
- print("[Layout] Hauptkarte zum Layout hinzugefügt")
- print(f"[Layout] Position und Größe gesetzt: pos=({map_left_mm}, {map_top_mm}), size=({map_width_mm:.1f}x{map_height_mm:.1f})")
-
- # Extent und Maßstab setzen
- x_min = getattr(extent, "xMinimum", lambda: float("nan"))()
- y_min = getattr(extent, "yMinimum", lambda: float("nan"))()
- x_max = getattr(extent, "xMaximum", lambda: float("nan"))()
- y_max = getattr(extent, "yMaximum", lambda: float("nan"))()
- print(f"[Layout] Extent input: xmin={x_min}, ymin={y_min}, xmax={x_max}, ymax={y_max}")
-
- if extent is not None and hasattr(extent, "isNull") and callable(extent.isNull) and not extent.isNull():
- try:
- hauptkarte.setExtent(extent)
- print(f"[Layout] setExtent() erfolgreich")
- except Exception as exc:
- print(f"[Layout] WARN: setExtent() fehlgeschlagen: {exc}")
- else:
- print(f"[Layout] WARN: Extent nicht gültig/callable, setExtent übersprungen")
-
- # Maßstab setzen (NACH Extent)
- if isinstance(plotmassstab, (int, float)) and math.isfinite(plotmassstab) and plotmassstab > 0:
- try:
- hauptkarte.setScale(plotmassstab)
- print(f"[Layout] setScale({plotmassstab}) erfolgreich")
- except Exception as exc:
- print(f"[Layout] WARN: setScale({plotmassstab}) fehlgeschlagen: {exc}")
- else:
- print(f"[Layout] WARN: ungültiger plotmassstab={plotmassstab!r}, setScale übersprungen")
-
- # Rahmen aktivieren
- set_frame_enabled = getattr(hauptkarte, "setFrameEnabled", None)
- if callable(set_frame_enabled):
- try:
- set_frame_enabled(True)
- print(f"[Layout] Rahmen aktiviert")
- except Exception as exc:
- print(f"[Layout] WARN: setFrameEnabled fehlgeschlagen: {exc}")
-
- set_frame_stroke_width = getattr(hauptkarte, "setFrameStrokeWidth", None)
- if callable(set_frame_stroke_width):
- try:
- self._set_item_frame_stroke_width(hauptkarte, 0.5)
- print(f"[Layout] Rahmenstrichbreite auf 0.5 mm gesetzt")
- except Exception as exc:
- print(f"[Layout] WARN: setFrameStrokeWidth(0.5) fehlgeschlagen: {exc}")
-
- # Kartenthema setzen
- if thema and thema != "aktuell":
- follow_theme = getattr(hauptkarte, "setFollowVisibilityPreset", None)
- set_theme_name = getattr(hauptkarte, "setFollowVisibilityPresetName", None)
- if callable(follow_theme):
- try:
- follow_theme(True)
- print(f"[Layout] setFollowVisibilityPreset(True)")
- except Exception as exc:
- print(f"[Layout] WARN: setFollowVisibilityPreset fehlgeschlagen: {exc}")
- if callable(set_theme_name):
- try:
- set_theme_name(thema)
- print(f"[Layout] Kartenthema auf '{thema}' gesetzt")
- except Exception as exc:
- print(f"[Layout] WARN: setFollowVisibilityPresetName('{thema}') fehlgeschlagen: {exc}")
- else:
- print(f"[Layout] Kartenthema nicht gesetzt (thema='{thema}')")
-
- # Erst nach allen Properties: LayerSet einfrieren für Export
- set_keep_layer_set = getattr(hauptkarte, "setKeepLayerSet", None)
- if callable(set_keep_layer_set):
- try:
- set_keep_layer_set(True)
- print("[Layout] setKeepLayerSet(True) – Layerset für Export erhalten")
- except Exception as exc:
- print(f"[Layout] WARN: setKeepLayerSet fehlgeschlagen: {exc}")
-
- # Refresh-Strategie (optional, nur wenn vorhanden)
- set_refresh_strategy = getattr(hauptkarte, "setRefreshStrategy", None)
- if callable(set_refresh_strategy):
- refresh_cache = getattr(hauptkarte, "RefreshLaterOnly", None) or getattr(hauptkarte, "RefreshWhenRequested", None) or 0
- if refresh_cache is not None:
- try:
- set_refresh_strategy(refresh_cache)
- print(f"[Layout] setRefreshStrategy({refresh_cache}) gesetzt")
- except Exception as exc:
- print(f"[Layout] WARN: setRefreshStrategy fehlgeschlagen: {exc}")
-
- print(f"[Layout] Hauptkarte vollständig konfiguriert")
-
- self._setup_layout_objects(
- kartenfenster_untere_rechte_ecke_mm,
- hauptkarte,
- map_top_mm,
- map_height_mm,
- is_atlas=False,
- )
-
- layout_manager.addLayout(layout)
- print("[Layout] Layout zum LayoutManager hinzugefügt")
- self._current_layout = None
- open_layout_designer(layout)
- print("[Layout] Layout Designer geöffnet")
- return layout
-
- def create_atlas_layout(
- self,
- name: str,
- page_width_mm: float,
- page_height_mm: float,
- map_width_mm: float,
- map_height_mm: float,
- extent: Any,
- plotmassstab: float,
- atlas_layer: Any,
- thema: str = "",
- ) -> Any:
- """Erzeugt ein Atlas-Layout mit Coverage-Layer ``Atlasobjekte``."""
- layout_manager = self.project.layoutManager()
- existing_layout = layout_manager.layoutByName(name)
- if existing_layout is not None:
- raise ValueError(f"Eine Vorlage mit der Bezeichnung '{name}' existiert bereits.")
-
- layout = QgsPrintLayout(self.project)
- layout.initializeDefaults()
- layout.setName(name)
- self._current_layout = layout
-
- page = layout.pageCollection().page(0)
- page.setPageSize(QgsLayoutSize(page_width_mm, page_height_mm, MM))
- # Verifiziere, dass QGIS die Größe akzeptiert hat
- page_size = page.pageSize() if hasattr(page, 'pageSize') else None
- if page_size is not None and hasattr(page_size, 'width'):
- actual_w = float(page_size.width())
- actual_h = float(page_size.height())
- print(f"[Layout] Atlas Page gesetzt: x=0, y=0, width={page_width_mm:.1f}mm→{actual_w:.1f}mm, height={page_height_mm:.1f}mm→{actual_h:.1f}mm")
- else:
- print(f"[Layout] Atlas Page: x=0, y=0, width={page_width_mm:.1f}mm, height={page_height_mm:.1f}mm")
-
- map_left_mm = 10.0
- map_top_mm = 10.0
- map_right_mm = map_left_mm + map_width_mm
- map_bottom_mm = map_top_mm + map_height_mm
- kartenfenster_untere_rechte_ecke_mm = (map_right_mm, map_bottom_mm)
-
- hauptkarte = self._create_map_item(
- layout,
- "hauptkarte",
- map_left_mm,
- map_top_mm,
- map_width_mm,
- map_height_mm,
- )
-
- # Verifiziere mit Units-bewussten Methoden (rect() kann andere Units verwenden).
- actual_w = None
- actual_h = None
- actual_x = None
- actual_y = None
-
- # Versuche zuerst, die Größe mit Unit-Methoden zu lesen
- try:
- if hasattr(hauptkarte, 'sizeWithUnits'):
- size_item = hauptkarte.sizeWithUnits()
- if hasattr(size_item, 'width') and hasattr(size_item, 'height'):
- actual_w = float(size_item.width())
- actual_h = float(size_item.height())
- except Exception:
- pass
-
- try:
- if hasattr(hauptkarte, 'positionWithUnits'):
- pos_item = hauptkarte.positionWithUnits()
- if hasattr(pos_item, 'x') and hasattr(pos_item, 'y'):
- actual_x = float(pos_item.x())
- actual_y = float(pos_item.y())
- except Exception:
- pass
-
- # Fallback: nutze rect() und teile durch UnitFaktor, falls nötig
- if actual_w is None or actual_h is None:
- try:
- actual_rect = hauptkarte.rect()
- if actual_rect is not None:
- actual_w = float(actual_rect.width())
- actual_h = float(actual_rect.height())
- actual_x = float(actual_rect.x())
- actual_y = float(actual_rect.y())
- except Exception:
- pass
-
- if actual_w is not None and actual_h is not None:
- print(f"[Layout] Atlas Hauptkarte gesetzt: x={map_left_mm:.1f}mm→{actual_x:.1f}mm, y={map_top_mm:.1f}mm→{actual_y:.1f}mm, width={map_width_mm:.1f}mm→{actual_w:.1f}mm, height={map_height_mm:.1f}mm→{actual_h:.1f}mm")
- else:
- print(f"[Layout] Atlas Hauptkarte: x={map_left_mm:.1f}mm, y={map_top_mm:.1f}mm, width={map_width_mm:.1f}mm, height={map_height_mm:.1f}mm")
-
- if extent is not None and hasattr(extent, "isNull") and callable(extent.isNull) and not extent.isNull():
- try:
- hauptkarte.setExtent(extent)
- except Exception:
- pass
-
- if isinstance(plotmassstab, (int, float)) and math.isfinite(plotmassstab) and plotmassstab > 0:
- try:
- hauptkarte.setScale(plotmassstab)
- except Exception:
- pass
-
- set_frame_enabled = getattr(hauptkarte, "setFrameEnabled", None)
- if callable(set_frame_enabled):
- try:
- set_frame_enabled(True)
- except Exception:
- pass
-
- set_frame_stroke_width = getattr(hauptkarte, "setFrameStrokeWidth", None)
- if callable(set_frame_stroke_width):
- try:
- self._set_item_frame_stroke_width(hauptkarte, 0.5)
- except Exception:
- pass
-
- if thema and thema != "aktuell":
- follow_theme = getattr(hauptkarte, "setFollowVisibilityPreset", None)
- set_theme_name = getattr(hauptkarte, "setFollowVisibilityPresetName", None)
- if callable(follow_theme):
- try:
- follow_theme(True)
- except Exception:
- pass
- if callable(set_theme_name):
- try:
- set_theme_name(thema)
- except Exception:
- pass
-
- set_atlas_driven = getattr(hauptkarte, "setAtlasDriven", None)
- if callable(set_atlas_driven):
- try:
- set_atlas_driven(True)
- except Exception:
- pass
-
- # Fester Atlas-Maßstab: plotmassstab bleibt unverändert.
- set_scaling_mode = getattr(hauptkarte, "setAtlasScalingMode", None)
- if callable(set_scaling_mode):
- fixed_mode = getattr(QgsLayoutItemMap, "Fixed", None)
- if fixed_mode is not None:
- try:
- set_scaling_mode(fixed_mode)
- except Exception:
- pass
-
- set_atlas_margin = getattr(hauptkarte, "setAtlasMargin", None)
- if callable(set_atlas_margin):
- try:
- set_atlas_margin(0.0)
- except Exception:
- pass
-
- set_keep_layer_set = getattr(hauptkarte, "setKeepLayerSet", None)
- if callable(set_keep_layer_set):
- try:
- set_keep_layer_set(True)
- except Exception:
- pass
-
- # Sicherheit: Größe nochmal nach Atlas-Konfiguration setzen, um sicherzustellen, dass sie nicht von der Atlas-Einstellung überschrieben wurde.
- hauptkarte.attemptResize(QgsLayoutSize(map_width_mm, map_height_mm, MM))
- print(f"[Layout] Atlas Hauptkarte Größe nach Atlas-Konfiguration erneut gesetzt: {map_width_mm:.1f}mm × {map_height_mm:.1f}mm")
-
- self._setup_layout_objects(
- kartenfenster_untere_rechte_ecke_mm,
- hauptkarte,
- map_top_mm,
- map_height_mm,
- is_atlas=True,
- )
-
- atlas = layout.atlas()
- if atlas is not None:
- set_enabled = getattr(atlas, "setEnabled", None)
- set_coverage = getattr(atlas, "setCoverageLayer", None)
- set_hide_coverage = getattr(atlas, "setHideCoverage", None)
- set_filter_features = getattr(atlas, "setFilterFeatures", None)
- set_page_name = getattr(atlas, "setPageNameExpression", None)
-
- if callable(set_enabled):
- set_enabled(True)
- if callable(set_coverage):
- set_coverage(atlas_layer)
- if callable(set_hide_coverage):
- set_hide_coverage(True)
- if callable(set_filter_features):
- set_filter_features(False)
- if callable(set_page_name):
- set_page_name("attribute(@atlas_feature, 'Seitenzahl')")
-
- layout_manager.addLayout(layout)
- print("[Layout] Layout zum LayoutManager hinzugefügt")
- self._current_layout = None
- open_layout_designer(layout)
- print("[Layout] Layout Designer geöffnet")
- return layout
diff --git a/ui/tab_a_logic.py b/ui/tab_a_logic.py
index 3e5dc02..2caf102 100644
--- a/ui/tab_a_logic.py
+++ b/ui/tab_a_logic.py
@@ -12,6 +12,7 @@ import os
import datetime
import json
import tempfile
+import html
from sn_basis.functions.qgiscore_wrapper import (
QgsVectorFileWriter,
@@ -63,9 +64,88 @@ class TabALogic:
self.link_pruefer = link_pruefer
self.stil_pruefer = stil_pruefer
self.data_grabber: Optional[DataGrabber] = None
+
+ class _SilentPruefmanagerProxy:
+ """Proxy, der Prüfergebnisse ohne UI-Meldungen verarbeitet."""
+
+ def __init__(self, outer: "TabALogic", fallback: Optional[Any]) -> None:
+ self._outer = outer
+ self._fallback = fallback
+ self.results: List[Any] = []
+
+ def verarbeite(self, ergebnis: Any) -> Any:
+ self.results.append(ergebnis)
+ ok = bool(getattr(ergebnis, "ok", False))
+ if not ok:
+ aktion = getattr(ergebnis, "aktion", "unbekannt")
+ meldung = getattr(ergebnis, "meldung", "")
+ self._outer._log(f"[SILENT-PRUEFMANAGER] {aktion}: {meldung}")
+ return ergebnis
+
+ def __getattr__(self, name: str) -> Any:
+ if self._fallback is not None:
+ return getattr(self._fallback, name)
+ raise AttributeError(name)
+
def _log(self, msg: str) -> None:
print(f"[TabALogic] {msg}")
+ def _create_silent_pruefmanager(self) -> "TabALogic._SilentPruefmanagerProxy":
+ return TabALogic._SilentPruefmanagerProxy(self, self.pruefmanager)
+
+ def _extract_datenabruf_error_maps(self, datenabruf_results: List[Any]) -> Dict[str, Dict[str, str]]:
+ by_ident: Dict[str, str] = {}
+ by_thema: Dict[str, str] = {}
+
+ for result in datenabruf_results or []:
+ if not result:
+ continue
+ context = getattr(result, "kontext", None)
+ if not isinstance(context, dict):
+ continue
+
+ if getattr(result, "aktion", None) == "url_nicht_erreichbar":
+ ident = str(context.get("ident") or "").strip()
+ thema = str(context.get("thema") or "").strip()
+ reason = str(context.get("error") or getattr(result, "meldung", "") or "unbekannter Fehler").strip()
+ if ident and reason:
+ by_ident[ident] = reason
+ if thema and reason:
+ by_thema[thema] = reason
+
+ if getattr(result, "aktion", None) == "datenabruf":
+ fehler_map = context.get("fehler")
+ if isinstance(fehler_map, dict):
+ for thema_key, value in fehler_map.items():
+ thema = str(thema_key or "").strip()
+ reason = str(value or "").strip()
+ if thema and reason and thema not in by_thema:
+ by_thema[thema] = reason
+
+ return {"by_ident": by_ident, "by_thema": by_thema}
+
+ def _zeige_verfahrensgebiet_hinweis(self) -> None:
+ if not self.pruefmanager:
+ return
+ show_hint = getattr(self.pruefmanager, "zeige_hinweis", None)
+ if callable(show_hint):
+ show_hint(
+ "Verfahrensgebiet fehlt",
+ "Kein gueltiger Verfahrensgebiet-Layer vorhanden. "
+ "Bitte zuerst in sn_verfahrensgebiet den Layer ueber 'Aus ALKIS laden' laden "
+ "oder im Dropdown einen gueltigen Layer auswaehlen.",
+ )
+
+ def _close_progress_dialog(self, progress: Optional[Any]) -> None:
+ if progress is None:
+ return
+ close = getattr(progress, "close", None)
+ if callable(close):
+ try:
+ close()
+ except Exception:
+ pass
+
# -------------------------------
# Verfahrens-Datenbank (Pfad-Management)
# -------------------------------
@@ -155,12 +235,6 @@ class TabALogic:
# -------------------------------
# Verfahrensgebiet-Layer
# -------------------------------
-
- def save_verfahrensgebiet_layer(self, layer: QgsVectorLayer) -> None:
- """Speichert die Verfahrensgebiet-Layer-ID, unter Annahme, dass der Layer prevalidiert ist."""
- layer_id = layer.id() if layer is not None else ""
- set_variable("verfahrensgebiet_layer", layer_id or "", scope="project")
-
def load_verfahrensgebiet_layer_id(self) -> Optional[str]:
value = get_variable("verfahrensgebiet_layer", scope="project")
return value or None
@@ -217,6 +291,7 @@ class TabALogic:
linkliste_final = self._resolve_linkliste(linkliste)
if linkliste_final is None:
self._log("Linkliste kann nicht aufgelöst werden")
+ self._close_progress_dialog(progress)
return None
else:
self._log(f"Linkliste final: '{linkliste_final}'")
@@ -224,6 +299,7 @@ class TabALogic:
raumfilter_layer = self._resolve_raumfilter(raumfilter, final_pfad)
if raumfilter in ("Verfahrensgebiet", "Pufferlayer") and raumfilter_layer is None:
self._log(f"Raumfilter '{raumfilter}' nicht verfügbar")
+ self._close_progress_dialog(progress)
return None
# 4) Lade-Status initialisieren (funktioniert ab Bestätigung überschreiben/anhängen)
@@ -236,7 +312,16 @@ class TabALogic:
progress.set_label("Prüfe Eingaben...")
# 5) Daten aus Linkliste laden und prüfen
- source_dict, grabber_summary = self.data_grabber.run(linkliste_final)
+ silent_pruefmanager = self._create_silent_pruefmanager()
+ local_data_grabber = DataGrabber(
+ silent_pruefmanager,
+ datei_pruefer_cls=self.data_grabber._datei_pruefer_cls,
+ link_pruefer=self.data_grabber.link_pruefer,
+ layer_pruefer=self.data_grabber.layer_pruefer,
+ stil_pruefer=self.data_grabber.stil_pruefer,
+ excel_importer_cls=self.data_grabber._excel_importer_cls,
+ )
+ source_dict, grabber_summary = local_data_grabber.run(linkliste_final)
self._log(f"DataGrabber: {grabber_summary.meldung} [{grabber_summary.aktion}]")
# DEBUG: detaillierter Status
@@ -249,6 +334,7 @@ class TabALogic:
if not source_dict.get("rows"):
self._log("Keine validen Linkliste-Einträge für Datenabruf")
print("[TabALogic] ... STOP: rows:", len(source_dict.get("rows", [])))
+ self._close_progress_dialog(progress)
return None
total_rows = len(source_dict.get("rows", []))
@@ -266,7 +352,8 @@ class TabALogic:
self._log("Warnung: DataGrabber meldet fehlerhafte Zeilen, fahre mit Validierungsdaten fort")
# 5) Datenabruf (aus validierten Zeilen)
- datenabruf = Datenabruf(self.pruefmanager)
+ silent_pruefmanager = self._create_silent_pruefmanager()
+ datenabruf = Datenabruf(silent_pruefmanager)
result_dict, datenabruf_results = datenabruf.datenabruf(
result_dict=source_dict,
raumfilter=raumfilter,
@@ -296,7 +383,7 @@ class TabALogic:
progress.set_label("Fachdaten laden abgeschlossen. Bitte OK klicken, um den Dialog zu schließen.")
# 7) Log-Datei schreiben
- self._write_markdown_log(final_pfad, source_dict, pipeline_context, load_summary)
+ self._write_html_log(final_pfad, source_dict, pipeline_context, load_summary)
print("=" * 60 + "\n")
return pipeline_context
@@ -310,7 +397,15 @@ class TabALogic:
aborted = False
if not rows:
self._log("Keine Dienste zum Laden")
- return
+ return {
+ "row_count": 0,
+ "loaded_count": 0,
+ "skipped_outside": 0,
+ "aborted": False,
+ "row_stats": [],
+ "layer_call_status": {},
+ "raumfilter_name": pipeline_context.get("raumfilter_name", "unbekannt"),
+ }
final_pfad = pipeline_context.get("source") or ""
use_datenschreiber = bool(final_pfad)
@@ -340,7 +435,12 @@ class TabALogic:
raumfilter_extent = raumfilter_layer.extent()
temp_layers: List[Any] = []
- layer_loader = LayerLoader(self.pruefmanager, stil_pruefer=self.stil_pruefer, layer_pruefer=self.link_pruefer)
+ silent_pruefmanager = self._create_silent_pruefmanager()
+ layer_loader = LayerLoader(silent_pruefmanager, stil_pruefer=self.stil_pruefer, layer_pruefer=self.link_pruefer)
+
+ error_maps = self._extract_datenabruf_error_maps(pipeline_context.get("datenabruf_results", []))
+ error_by_ident = error_maps.get("by_ident", {})
+ error_by_thema = error_maps.get("by_thema", {})
# Statistiken für Log: Raumfilter-Info pro Dienst
row_stats: List[Dict[str, Any]] = []
@@ -357,6 +457,7 @@ class TabALogic:
daten_map = (pipeline_context.get("result_dict") or {}).get("daten", {})
fetched_features = daten_map.get(ident, []) if isinstance(daten_map, dict) else []
fetched_count = len(fetched_features) if isinstance(fetched_features, list) else None
+ base_reason = error_by_ident.get(ident) or error_by_thema.get(thema)
if progress is not None:
progress.set_label(f"Lade Dienst {idx}/{total}: {thema}")
@@ -385,6 +486,8 @@ class TabALogic:
if not layer:
layer_call_status[thema] = "layer_nicht_ladbar"
self._log(f"[DEBUG] Layeraufruf fehlgeschlagen: thema='{thema}'")
+ reason = base_reason or "Layer konnte nicht geladen werden (Dienst nicht erreichbar oder Link fehlerhaft)"
+ self._log(f"[DEBUG] Dienst nicht geladen: {thema} | Grund: {reason}")
row_stats.append({
"dienst": thema,
"provider": provider,
@@ -394,7 +497,7 @@ class TabALogic:
"total_features": None,
"filtered_features": None,
"status": "layer_nicht_ladbar",
- "raumfilter": raumfilter_name,
+ "reason": reason,
})
continue
@@ -448,6 +551,7 @@ class TabALogic:
layer_call_status[thema] = "raumfilter_ausserhalb"
self._log(f"Dienst {thema} ist außerhalb des Raumfilters")
skipped_outside += 1
+ reason = "Keine Objekte innerhalb des Raumfilters"
row_stats.append({
"dienst": thema,
"provider": provider,
@@ -457,7 +561,7 @@ class TabALogic:
"total_features": total_features,
"filtered_features": 0,
"status": "außerhalb",
- "raumfilter": raumfilter_name,
+ "reason": reason,
})
continue
@@ -473,7 +577,7 @@ class TabALogic:
"total_features": total_features,
"filtered_features": filtered_features,
"status": "geladen",
- "raumfilter": raumfilter_name,
+ "reason": "geladen",
})
layer_call_status[thema] = "geladen"
self._log(f"[DEBUG] Dienst geladen: thema='{thema}', provider='{provider}', filtered_features={filtered_features}")
@@ -671,75 +775,118 @@ class TabALogic:
self._log(f"[DEBUG] Fehler beim Erzeugen lokaler Featureschicht für {thema}: {exc}")
return None
- def _write_markdown_log(
+ def _write_html_log(
self,
final_pfad: str,
source_dict: DataDict,
pipeline_context: Dict[str, Any],
load_summary: Dict[str, Any],
) -> None:
- """Schreibt den Pipeline-Log (Markdown)."""
+ """Schreibt den Pipeline-Log als HTML-Datei."""
lines = [
- "# Plan41 Fachdaten-Ladevorgang",
- "",
- f"**Datum**: {datetime.datetime.now().isoformat()}",
- f"**Verfahrens-DB**: {final_pfad or 'temporär'}",
- f"**Linkliste**: {pipeline_context.get('linkliste')}",
- "",
- "## Zusammenfassung",
- "",
- f"- **Zeilen gesamt**: {load_summary.get('row_count', 0)}",
- f"- **Geladene Dienste**: {load_summary.get('loaded_count', 0)}",
- f"- **Außerhalb Raumfilter**: {load_summary.get('skipped_outside', 0)}",
- f"- **Abgebrochen**: {load_summary.get('aborted', False)}",
- f"- **Raumfilter**: {load_summary.get('raumfilter_name', 'unbekannt')}",
- f"- **Raumfilter-Typ**: {pipeline_context.get('raumfilter_name', 'unbekannt')}",
- "",
- "## Dienstliste",
- "",
- "| Dienst | Provider | Linkadresse | Aufrufstatus | Ergebnisstatus |",
- "|---|---|---|---|---|",
+ "",
+ "",
+ "
",
+ " ",
+ " Plan41 Fachdaten-Ladevorgang",
+ " ",
+ "",
+ "",
+ "Plan41 Fachdaten-Ladevorgang
",
+ "",
+ "Zusammenfassung
",
+ "",
+ f" - Zeilen gesamt: {load_summary.get('row_count', 0)}
",
+ f" - Geladene Dienste: {load_summary.get('loaded_count', 0)}
",
+ f" - Außerhalb Raumfilter: {load_summary.get('skipped_outside', 0)}
",
+ f" - Abgebrochen: {load_summary.get('aborted', False)}
",
+ "
",
+ "Dienstliste
",
+ "",
+ "| Dienst | Provider | Linkadresse | Aufrufstatus |
",
+ "",
]
- status_by_dienst = {
- str(stat.get("dienst", "")): str(stat.get("status", "n/a"))
- for stat in load_summary.get("row_stats", [])
- }
aufrufstatus_by_dienst = {
str(key): str(value)
for key, value in (load_summary.get("layer_call_status", {}) or {}).items()
}
+ aufrufstatus_labels = {
+ "layeraufruf_start": "Layer-Aufruf gestartet",
+ "layeraufruf_ok": "Layer-Aufruf erfolgreich",
+ "geladen": "Geladen",
+ "layer_nicht_ladbar": "Nicht ladbar",
+ "raumfilter_ausserhalb": "Außerhalb Raumfilter",
+ "abbruch_vor_layeraufruf": "Abbruch vor Layer-Aufruf",
+ "abbruch_nach_layeraufruf": "Abbruch nach Layer-Aufruf",
+ "abbruch_nach_raumfilter": "Abbruch nach Raumfilter",
+ "nicht_aufgerufen": "Nicht aufgerufen",
+ }
+ status_labels = {
+ "geladen": "Geladen",
+ "außerhalb": "Außerhalb Raumfilter",
+ "layer_nicht_ladbar": "Nicht ladbar",
+ }
for row in source_dict.get('rows', []):
dienst = row.get('Inhalt') or row.get('ident') or ''
provider = row.get('Provider') or ''
link = row.get('Link') or ''
aufrufstatus = aufrufstatus_by_dienst.get(str(dienst), "nicht_aufgerufen")
- ergebnisstatus = status_by_dienst.get(str(dienst), "n/a")
- lines.append(f"| {dienst} | {provider} | {link} | {aufrufstatus} | {ergebnisstatus} |")
+ aufrufstatus_text = aufrufstatus_labels.get(str(aufrufstatus), str(aufrufstatus))
+ lines.append(
+ f"| {html.escape(str(dienst))} | {html.escape(str(provider))} | {html.escape(str(link))} | {html.escape(str(aufrufstatus_text))} |
"
+ )
lines.extend([
- "",
- "## Raumfilter-Statistik",
- "",
- "| Dienst | Provider | Linkadresse | Datenabruf-Objekte | Gesamt-Objekte | Gefilterte Objekte | Raumfilter | Status |",
- "|---|---|---|---|---|---|---|---|",
+ "",
+ "
",
+ "Raumfilter-Statistik
",
+ "",
+ "| Dienst | Datenabruf-Objekte | Gesamt-Objekte | Gefilterte Objekte | Status |
",
+ "",
])
for stat in load_summary.get('row_stats', []):
+ status = str(stat.get('status', 'n/a'))
+ status_label = status_labels.get(status, status)
+ reason = str(stat.get('reason', '') or '')
+ status_text = status_label if not reason else f"{status_label}: {reason}"
lines.append(
- f"| {stat.get('dienst', '')} | {stat.get('provider', '')} | {stat.get('link', '')} | {stat.get('datenabruf_features', 'n/a')} | {stat.get('total_features', 'n/a')} | {stat.get('filtered_features', 'n/a')} | {stat.get('raumfilter', '')} | {stat.get('status', 'n/a')} |"
+ f"| {html.escape(str(stat.get('dienst', '')))} | {html.escape(str(stat.get('datenabruf_features', 'n/a')))} | {html.escape(str(stat.get('total_features', 'n/a')))} | {html.escape(str(stat.get('filtered_features', 'n/a')))} | {html.escape(status_text)} |
"
)
- markdown = "\n".join(lines)
+ lines.extend([
+ "",
+ "
",
+ "",
+ "",
+ ])
+
+ html_text = "\n".join(lines)
if final_pfad:
log_dir = os.path.dirname(final_pfad)
os.makedirs(log_dir, exist_ok=True)
- log_file = os.path.join(log_dir, "plan41_lade_log.md")
+ log_file = os.path.join(log_dir, "plan41_lade_log.html")
try:
with open(log_file, "w", encoding="utf-8") as fh:
- fh.write(markdown)
+ fh.write(html_text)
self.pruefmanager.verarbeite(
pruef_ergebnis(
@@ -754,15 +901,33 @@ class TabALogic:
self.pruefmanager.verarbeite(
pruef_ergebnis(
ok=False,
- meldung=f"Fehler beim Schreiben des Logle (md): {exc}",
+ meldung=f"Fehler beim Schreiben des Logs (html): {exc}",
aktion="log_schreiben_fehlgeschlagen",
kontext={"error": str(exc)},
)
)
warning("Lade-Log", f"Konnte Datei nicht schreiben: {exc}", duration=10)
else:
- # temporärer Modus: nur anzeigen
- info("Lade-Log (temporär)", markdown, duration=20)
+ try:
+ tmp_dir = os.path.join(tempfile.gettempdir(), "sn_plan41")
+ os.makedirs(tmp_dir, exist_ok=True)
+ timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
+ log_file = os.path.join(tmp_dir, f"plan41_lade_log_{timestamp}.html")
+
+ with open(log_file, "w", encoding="utf-8") as fh:
+ fh.write(html_text)
+
+ self._log(f"Lade-Log (temporär) gespeichert: {log_file}")
+
+ if hasattr(os, "startfile"):
+ try:
+ os.startfile(log_file)
+ except Exception as exc:
+ self._log(f"Konnte temporäre Log-Datei nicht öffnen: {exc}")
+
+ info("Lade-Log (temporär)", f"Lade-Protokoll gespeichert: {log_file}", duration=20)
+ except Exception as exc:
+ warning("Lade-Log", f"Konnte temporäre Log-Datei nicht schreiben: {exc}", duration=10)
def _clone_layer_with_extent(self, layer: QgsVectorLayer, extent, thema: str) -> QgsVectorLayer | None:
@@ -875,15 +1040,18 @@ class TabALogic:
if not layer_id:
self._log("❌ Keine Layer-ID gespeichert")
+ self._zeige_verfahrensgebiet_hinweis()
return None
layer = QgsProject.instance().mapLayer(layer_id)
if not layer:
self._log("❌ Layer-ID existiert nicht im Projekt")
+ self._zeige_verfahrensgebiet_hinweis()
return None
if not self.is_valid_verfahrensgebiet_layer(layer):
self._log("❌ Layer ist kein gültiger Vektorlayer")
+ self._zeige_verfahrensgebiet_hinweis()
return None
self._log(f"Verfahrensgebiet-Layer OK: '{layer.name()}'")
diff --git a/ui/tab_a_ui.py b/ui/tab_a_ui.py
index 1da6369..a224e77 100644
--- a/ui/tab_a_ui.py
+++ b/ui/tab_a_ui.py
@@ -166,16 +166,7 @@ class TabA(QWidget):
self.optional_content.setVisible(False)
main_layout.addWidget(self.optional_content)
- # === LAYER-AUSWAHL + RAUMFILTER ===
- 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)
-
+ # === RAUMFILTER ===
main_layout.addWidget(QLabel("Raumfilter"))
self._raumfilter_combo = QComboBox(self)
self._raumfilter_combo.setToolTip("Wählt die räumliche Bezugsfläche für die Datenextraktion.")
@@ -213,18 +204,6 @@ class TabA(QWidget):
self.lokale_linkliste = link
except Exception:
pass
-
- # Layer
- try:
- layer_id = get_variable("tab_a_layer_id", scope="project")
- if layer_id:
- layer = QgsProject.instance().mapLayer(layer_id)
- if layer and self.layer_combo:
- self.layer_combo.setLayer(layer)
- self._pufferlayer = layer
- except Exception:
- pass
-
# Raumfilter (schon im _build_ui behandelt)
# === UI CALLBACKS ===
@@ -249,20 +228,6 @@ class TabA(QWidget):
self.lokale_linkliste = path
set_variable("tab_a_linkliste", path, scope="project")
- 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_a_layer_id", layer.id(), scope="project")
-
- # 🔹 NEU: Verfahrensgebiet explizit registrieren
- if self.logic:
- self.logic.save_verfahrensgebiet_layer(layer)
-
def _on_raumfilter_changed(self, value: str) -> None:
"""Persistieret Raumfilter-Auswahl."""
@@ -296,15 +261,6 @@ class TabA(QWidget):
source = self.file_widget.filePath()
raumfilter = self._raumfilter_combo.currentText()
linkliste = self.linkliste_widget.filePath()
-
- if self.logic and self.layer_combo:
- layer = self.layer_combo.currentLayer()
- else:
- layer = None
-
- if layer and layer.name() == "Verfahrensgebiet":
- self.logic.save_verfahrensgebiet_layer(layer)
-
if self.logic:
try:
self.logic._on_run_pipeline(source, linkliste, raumfilter)
diff --git a/ui/tab_b_logic.py b/ui/tab_b_logic.py
deleted file mode 100644
index 986bae6..0000000
--- a/ui/tab_b_logic.py
+++ /dev/null
@@ -1,680 +0,0 @@
-"""
-sn_plan41/ui/tab_b_logic.py – Fachlogik für Tab B (Druck)
-"""
-from __future__ import annotations
-import math
-from typing import Any
-
-from sn_basis.functions.qt_wrapper import QVariant
-from sn_basis.functions.variable_wrapper import set_variable, get_variable
-from sn_basis.functions.qgiscore_wrapper import get_layer_extent
-from sn_basis.functions.qgiscore_wrapper import (
- QgsProject,
- QgsVectorLayer,
- QgsGeometry,
- QgsFeature,
- QgsField,
- QgsVectorFileWriter,
-)
-from sn_basis.functions.sys_wrapper import get_plugin_root, join_path, file_exists
-from sn_basis.modules.Pruefmanager import Pruefmanager
-from sn_basis.modules.layerpruefer import Layerpruefer
-from sn_plan41.ui.layout import Layout
-
-
-KARTENNAME_VAR = "kartenname"
-PLOTMASSSTAB_VAR = "plotmassstab"
-VIEW_VAR = "view"
-ZIELGROESSE_VAR = "zielgroesse"
-FORMFAKTOR_VAR = "formfaktor"
-KARTENNAME_38 = "§38"
-KARTENNAME_41 = "§41"
-MASSSTAB_WIE_KARTENFENSTER = "Wie Kartenfenster"
-THEMA_WIE_KARTENFENSTER = "wie kartenfenster"
-
-KARTENNAME_BY_AUSWAHL = {
- KARTENNAME_38: "Planungsübersicht §38 FlurbG",
- KARTENNAME_41: "Karte zum Plan über die gemeinschaftlichen und öffentlichen Anlagen (§ 41 FlurbG)",
-}
-
-PLOTMASSSTAB_BY_AUSWAHL = {
- "1:5.000": "5000",
- "1:10.000": "10000",
- "1:15.000": "15000",
- "1:20.000": "20000",
- "1:25.000": "25000",
- "1:50.000": "50000",
- "1:100.000": "100000",
-}
-
-# Breite x Höhe in mm (Hochformat, DIN-Standard)
-DIN_GROESSEN: dict[str, tuple[int, int]] = {
- "DIN A0": (841, 1189),
- "DIN A1": (594, 841),
- "DIN A2": (420, 594),
- "DIN A3": (297, 420),
- "DIN A4": (210, 297),
-}
-DIN_STANDARD = "DIN A0"
-
-class TabBLogic:
- """
- Kapselt die Fachlogik von Tab B.
- """
-
- def __init__(self, pruefmanager: Pruefmanager) -> None:
- self.pruefmanager = pruefmanager
-
- @staticmethod
- def _wkt_rect(x_min: float, y_min: float, x_max: float, y_max: float) -> str:
- return (
- f"POLYGON(({x_min} {y_min}, {x_max} {y_min}, {x_max} {y_max}, "
- f"{x_min} {y_max}, {x_min} {y_min}))"
- )
-
- @staticmethod
- def _set_topological_editing_enabled() -> None:
- project = QgsProject.instance()
- set_topological = getattr(project, "setTopologicalEditing", None)
- if callable(set_topological):
- try:
- set_topological(True)
- except Exception:
- pass
-
- @staticmethod
- def _apply_atlas_style(layer: Any) -> None:
- style_path = join_path(get_plugin_root(), "sn_plan41", "assets", "atlasobjekte.qml")
- if not file_exists(style_path):
- return
- try:
- ok, _ = layer.loadNamedStyle(str(style_path))
- if ok:
- getattr(layer, "triggerRepaint", lambda: None)()
- except Exception:
- pass
-
- @staticmethod
- def _find_tile_grid_for_roll_atlas(
- kartenbild_w_mm: float,
- kartenbild_h_mm: float,
- din_dims: tuple[int, int],
- respect_max_sheet_size: bool = False,
- ) -> tuple[int, int, float, float]:
- """Bestimmt ein Atlas-Raster für Endlosrolle, das Kacheln statt Streifen erzeugt.
-
- Ziel: Kachel-Seitenverhältnis möglichst nah am Einzelblatt-Kartenfenster
- (inkl. Rändern) bei zugleich moderater Seitenanzahl.
- """
- if kartenbild_w_mm <= 0 or kartenbild_h_mm <= 0:
- return 1, 1, max(1.0, kartenbild_w_mm), max(1.0, kartenbild_h_mm)
-
- # Einzelblatt-Kartenfenster für beide Orientierungen
- dim_w, dim_h = float(din_dims[0]), float(din_dims[1])
- orientation_candidates: list[tuple[float, float]] = [
- (dim_w - 210.0, dim_h - 20.0),
- (dim_h - 210.0, dim_w - 20.0),
- ]
-
- best_score = math.inf
- best_result: tuple[int, int, float, float] | None = None
-
- for target_w, target_h in orientation_candidates:
- if target_w <= 0 or target_h <= 0:
- continue
-
- target_aspect = target_w / target_h
- px0 = max(1, int(round(kartenbild_w_mm / target_w)))
- py0 = max(1, int(round(kartenbild_h_mm / target_h)))
-
- for pages_x in range(max(1, px0 - 3), px0 + 4):
- for pages_y in range(max(1, py0 - 3), py0 + 4):
- tile_w = kartenbild_w_mm / pages_x
- tile_h = kartenbild_h_mm / pages_y
- if tile_w <= 0 or tile_h <= 0:
- continue
-
- # Im Blatt-Modus darf die resultierende Atlasseite die
- # gewählte Zielgröße (inkl. Orientierung) nicht überschreiten.
- if respect_max_sheet_size and (tile_w > target_w or tile_h > target_h):
- continue
-
- tile_aspect = tile_w / tile_h
- # 0 bei perfekter Übereinstimmung; symmetrisch für >1/<1
- aspect_error = abs(math.log(tile_aspect / target_aspect))
-
- # Streifen bestrafen
- strip_penalty = 0.0
- if tile_aspect < 0.5:
- strip_penalty = abs(math.log(tile_aspect / 0.5))
- elif tile_aspect > 2.0:
- strip_penalty = abs(math.log(tile_aspect / 2.0))
-
- page_count = pages_x * pages_y
-
- # Gewichtung: zuerst Formatnähe, dann Streifenvermeidung,
- # danach Seitenzahl minimieren.
- score = (aspect_error * 12.0) + (strip_penalty * 6.0) + (page_count * 0.20)
-
- if score < best_score:
- best_score = score
- best_result = (pages_x, pages_y, tile_w, tile_h)
-
- if best_result is None:
- if respect_max_sheet_size:
- fallback_candidates: list[tuple[int, int, float, float, int]] = []
- for target_w, target_h in orientation_candidates:
- if target_w <= 0 or target_h <= 0:
- continue
- pages_x = max(1, math.ceil(kartenbild_w_mm / target_w))
- pages_y = max(1, math.ceil(kartenbild_h_mm / target_h))
- tile_w = kartenbild_w_mm / pages_x
- tile_h = kartenbild_h_mm / pages_y
- fallback_candidates.append((pages_x, pages_y, tile_w, tile_h, pages_x * pages_y))
-
- if fallback_candidates:
- fallback_candidates.sort(key=lambda entry: (entry[4], abs(math.log((entry[2] / entry[3]) if entry[3] > 0 else 1.0))))
- fx, fy, fw, fh, _ = fallback_candidates[0]
- return fx, fy, fw, fh
-
- return 1, 1, kartenbild_w_mm, kartenbild_h_mm
-
- return best_result
-
- def _create_atlasobjekte_layer(
- self,
- layer: Any,
- extent: Any,
- pages_x: int,
- pages_y: int,
- seite_karte_w: float,
- seite_karte_h: float,
- massstab_zahl: float,
- ) -> Any | None:
- layer_crs = layer.crs() if hasattr(layer, "crs") else None
- crs_authid = layer_crs.authid() if layer_crs is not None and hasattr(layer_crs, "authid") else "EPSG:25832"
- atlas_layer = QgsVectorLayer(f"Polygon?crs={crs_authid}", "Atlasobjekte", "memory")
- if not atlas_layer or not atlas_layer.isValid():
- return None
-
- provider = atlas_layer.dataProvider()
- provider.addAttributes([
- QgsField("Seitenzahl", QVariant.Int),
- ])
- atlas_layer.updateFields()
-
- tile_w_m = seite_karte_w * massstab_zahl / 1000.0
- tile_h_m = seite_karte_h * massstab_zahl / 1000.0
-
- x_min = extent.xMinimum()
- y_min = extent.yMinimum()
- x_max = extent.xMaximum()
- y_max = extent.yMaximum()
-
- seitenzahl = 1
- features = []
- for row_idx in range(pages_y):
- tile_y_max = y_max - row_idx * tile_h_m
- tile_y_min = tile_y_max - tile_h_m
-
- for col_idx in range(pages_x):
- tile_x_min = x_min + col_idx * tile_w_m
- tile_x_max = tile_x_min + tile_w_m
-
- tile_geom = QgsGeometry.fromWkt(
- self._wkt_rect(tile_x_min, tile_y_min, tile_x_max, tile_y_max)
- )
-
- feat = QgsFeature(atlas_layer.fields())
- feat.setGeometry(tile_geom)
- feat["Seitenzahl"] = seitenzahl
- features.append(feat)
- seitenzahl += 1
-
- if not features:
- return None
-
- provider.addFeatures(features)
- atlas_layer.updateExtents()
- self._set_topological_editing_enabled()
-
- verfahrens_db = get_variable("verfahrens_db", scope="project") or ""
- print(f"[TabBLogic] Atlasobjekte: verfahrens_db='{verfahrens_db}'")
- if not verfahrens_db:
- QgsProject.instance().addMapLayer(atlas_layer)
- self._apply_atlas_style(atlas_layer)
- print("[TabBLogic] Atlasobjekte temporär ins Projekt geladen")
- return atlas_layer
-
- opts = QgsVectorFileWriter.SaveVectorOptions()
- opts.driverName = "GPKG"
- opts.fileEncoding = "UTF-8"
- opts.layerName = "Atlasobjekte"
- if file_exists(verfahrens_db):
- opts.actionOnExistingFile = QgsVectorFileWriter.CreateOrOverwriteLayer
- else:
- opts.actionOnExistingFile = QgsVectorFileWriter.CreateOrOverwriteFile
-
- err_result = QgsVectorFileWriter.writeAsVectorFormatV3(
- atlas_layer,
- verfahrens_db,
- QgsProject.instance().transformContext(),
- opts,
- )
-
- # QGIS-Versionen liefern hier entweder nur WriterError
- # oder ein Tupel (WriterError, msg, newPath, layerName).
- err_code = err_result[0] if isinstance(err_result, tuple) else err_result
- if err_code != QgsVectorFileWriter.NoError:
- print(f"[TabBLogic] Atlasobjekte schreiben fehlgeschlagen: err={err_result}")
- return None
-
- for existing in QgsProject.instance().mapLayersByName("Atlasobjekte"):
- try:
- QgsProject.instance().removeMapLayer(existing.id())
- except Exception:
- pass
-
- uri = f"{verfahrens_db}|layername=Atlasobjekte"
- loaded_layer = QgsVectorLayer(uri, "Atlasobjekte", "ogr")
- if not loaded_layer or not loaded_layer.isValid():
- print(f"[TabBLogic] Atlasobjekte laden aus GPKG fehlgeschlagen: uri='{uri}'")
- return None
-
- QgsProject.instance().addMapLayer(loaded_layer)
- self._apply_atlas_style(loaded_layer)
- print("[TabBLogic] Atlasobjekte aus Verfahrens-DB geladen und gestylt")
- return loaded_layer
-
- def set_kartenname_for_auswahl(self, auswahl: str) -> None:
- """Setzt die Projektvariable ``kartenname`` anhand der Kartennamen-Auswahl."""
- kartenname = KARTENNAME_BY_AUSWAHL.get(auswahl, "")
- set_variable(KARTENNAME_VAR, kartenname, scope="project")
-
- def set_plotmassstab_for_auswahl(self, auswahl: str, aktueller_massstab: float | None = None) -> None:
- """Setzt die Projektvariable ``plotmassstab`` anhand der Maßstabsauswahl."""
- if auswahl == MASSSTAB_WIE_KARTENFENSTER:
- if aktueller_massstab and aktueller_massstab > 0:
- set_variable(PLOTMASSSTAB_VAR, str(int(round(aktueller_massstab))), scope="project")
- else:
- set_variable(PLOTMASSSTAB_VAR, "", scope="project")
- return
-
- value = PLOTMASSSTAB_BY_AUSWAHL.get(auswahl, "")
- set_variable(PLOTMASSSTAB_VAR, value, scope="project")
-
- def set_view_for_auswahl(self, auswahl: str) -> None:
- """Setzt die Projektvariable ``view`` auf ``aktuell`` oder den Namen des gewählten Layerthemas."""
- if auswahl == THEMA_WIE_KARTENFENSTER:
- set_variable(VIEW_VAR, "aktuell", scope="project")
- return
-
- set_variable(VIEW_VAR, auswahl or "", scope="project")
-
- def set_zielgroesse_for_auswahl(self, auswahl: str) -> None:
- """Setzt ``zielgroesse`` auf den gewählten DIN-Namen."""
- set_variable(ZIELGROESSE_VAR, auswahl if auswahl in DIN_GROESSEN else DIN_STANDARD, scope="project")
-
- def set_formfaktor(self, endlosrolle: bool) -> None:
- """Setzt die Projektvariable ``formfaktor`` auf ``Endlosrolle`` oder ``Blatt``."""
- set_variable(FORMFAKTOR_VAR, "Endlosrolle" if endlosrolle else "Blatt", scope="project")
-
- # ─────────────────────────────────────────────────────────────────────────
- # Pipeline: Druckvorlage_anlegen
- # ─────────────────────────────────────────────────────────────────────────
-
- def druckvorlage_anlegen(
- self,
- layer: object,
- kartenname_auswahl: str,
- massstab_auswahl: str,
- zielgroesse: str,
- formfaktor: bool,
- ) -> dict:
- """Pipeline 'Druckvorlage_anlegen'.
-
- Prüft Parameter, berechnet Plotgröße und stellt bei Bedarf Atlas-Rückfrage.
-
- Parameters
- ----------
- layer:
- Aktuell gewählter Verfahrensgebiet-Layer aus Tab A.
-
- Returns
- -------
- dict
- ``ok`` (bool): Ob die Pipeline erfolgreich durchlaufen werden soll.
- ``switch_to_tab_a`` (bool): Ob Tab A aktiviert werden soll.
- ``atlas_seiten`` (int): Anzahl benötigter Seiten (1 = kein Atlas).
- """
- # ─── 1. Verfahrensgebiet-Layer prüfen ─────────────────────────────
- lp = Layerpruefer(layer=layer)
- ergebnis = lp.pruefe()
- if not ergebnis.ok:
- self.pruefmanager.zeige_hinweis(
- "Verfahrensgebiets-Layer angeben",
- "Verfahrensgebiets-Layer angeben",
- )
- return {"ok": False, "switch_to_tab_a": True, "atlas_seiten": 0}
-
- layer_id = getattr(layer, "id", lambda: "")() or ""
- set_variable("verfahrensgebietslayer", layer_id, scope="project")
-
- # ─── 2. Kartenname prüfen ─────────────────────────────────────────
- if kartenname_auswahl not in (KARTENNAME_38, KARTENNAME_41):
- self.pruefmanager.zeige_hinweis(
- "Kartennamen wählen",
- "Kartennamen wählen",
- )
- return {"ok": False, "switch_to_tab_a": False, "atlas_seiten": 0}
-
- # ─── 3. Maßstab ermitteln ─────────────────────────────────────────
- if massstab_auswahl == MASSSTAB_WIE_KARTENFENSTER:
- massstab_str = get_variable(PLOTMASSSTAB_VAR, scope="project") or ""
- try:
- massstab_zahl = float(massstab_str)
- except (ValueError, TypeError):
- massstab_zahl = 0.0
- else:
- massstab_zahl = float(PLOTMASSSTAB_BY_AUSWAHL.get(massstab_auswahl, 0))
-
- if massstab_zahl <= 0:
- self.pruefmanager.zeige_hinweis(
- "Maßstab fehlt",
- "Kein gültiger Maßstab angegeben.",
- )
- return {"ok": False, "switch_to_tab_a": False, "atlas_seiten": 0}
-
- # ─── 4. Kartenbild berechnen ──────────────────────────────────────
- # Der Layer wird als metrisch projiziert (Einheit: m) vorausgesetzt,
- # wie es für deutsche Planungslagen (z.B. EPSG:25832) üblich ist.
- extent = get_layer_extent(layer)
- if extent is None:
- self.pruefmanager.zeige_hinweis(
- "Fehler",
- "Layer-Ausdehnung konnte nicht ermittelt werden.",
- )
- return {"ok": False, "switch_to_tab_a": False, "atlas_seiten": 0}
-
- # Naturgröße (m) → Papiergröße (mm): mm = m * 1000 / massstab
- kartenbild_w = extent.width() * 1000.0 / massstab_zahl
- kartenbild_h = extent.height() * 1000.0 / massstab_zahl
-
- # ─── 5. Plotgröße = Kartenbild + Randabstand (x+210 mm, y+20 mm) ──
- plotgroesse_w = kartenbild_w + 210.0
- plotgroesse_h = kartenbild_h + 20.0
-
- # ─── 6. Zielgröße bestimmen ───────────────────────────────────────
- din_dims = DIN_GROESSEN.get(zielgroesse, DIN_GROESSEN[DIN_STANDARD])
- if formfaktor: # Endlosrolle: X-Richtung entspricht der Plotgröße
- ziel_w, ziel_h = plotgroesse_w, float(min(din_dims))
- else:
- ziel_w, ziel_h = float(din_dims[0]), float(din_dims[1])
-
- if ziel_w < DIN_GROESSEN["DIN A4"][0] or ziel_h < DIN_GROESSEN["DIN A4"][1]:
- self.pruefmanager.zeige_hinweis(
- "Blattgröße zu klein",
- "Die Zielgröße darf nicht kleiner als DIN A4 sein.",
- )
- return {"ok": False, "switch_to_tab_a": False, "atlas_seiten": 0}
-
- # ─── 7. Passt auf ein Blatt? -> Layout erzeugen ───────────────────
- print(f"[TabBLogic] plotgroesse=({plotgroesse_w:.1f}x{plotgroesse_h:.1f}), "
- f"zielgroesse=({ziel_w:.1f}x{ziel_h:.1f}), passt={plotgroesse_w <= ziel_w and plotgroesse_h <= ziel_h}")
- if plotgroesse_w > ziel_w or plotgroesse_h > ziel_h:
- if plotgroesse_w <= ziel_h and plotgroesse_h <= ziel_w:
- ziel_w, ziel_h = ziel_h, ziel_w
-
- if plotgroesse_w <= ziel_w and plotgroesse_h <= ziel_h:
- kartenname = get_variable(KARTENNAME_VAR, scope="project") or KARTENNAME_BY_AUSWAHL.get(
- kartenname_auswahl, "Vorlage"
- )
- thema = get_variable(VIEW_VAR, scope="project") or ""
- print(f"[TabBLogic] frage_text aufrufen, default='{kartenname}', thema='{thema}'")
- vorlage_name, bestaetigt = self.pruefmanager.frage_text(
- "Neue Vorlage anlegen",
- "Bezeichnung der Vorlage:",
- default_text=kartenname,
- )
- print(f"[TabBLogic] frage_text Ergebnis: name='{vorlage_name}', bestaetigt={bestaetigt}")
- if not bestaetigt:
- return {"ok": False, "switch_to_tab_a": False, "atlas_seiten": 0}
-
- vorlage_name = (vorlage_name or "").strip() or kartenname
-
- print(f"[TabBLogic] Rufe Layout().create_single_page_layout auf: name='{vorlage_name}', "
- f"page=({ziel_w}x{ziel_h}), map=({kartenbild_w:.1f}x{kartenbild_h:.1f}), "
- f"massstab={massstab_zahl}, thema='{thema}'")
- try:
- Layout().create_single_page_layout(
- name=vorlage_name,
- page_width_mm=ziel_w,
- page_height_mm=ziel_h,
- map_width_mm=kartenbild_w,
- map_height_mm=kartenbild_h,
- extent=extent,
- plotmassstab=massstab_zahl,
- thema=thema,
- )
- print("[TabBLogic] create_single_page_layout erfolgreich abgeschlossen")
- except ValueError as exc:
- print(f"[TabBLogic] ValueError: {exc}")
- self.pruefmanager.zeige_hinweis("Vorlage anlegen", str(exc))
- return {"ok": False, "switch_to_tab_a": False, "atlas_seiten": 0}
- except Exception as exc:
- print(f"[TabBLogic] Exception: {exc!r}")
- self.pruefmanager.zeige_hinweis("Vorlage anlegen", f"Die Vorlage konnte nicht angelegt werden: {exc}")
- return {"ok": False, "switch_to_tab_a": False, "atlas_seiten": 0}
-
- return {"ok": True, "switch_to_tab_a": False, "atlas_seiten": 1}
-
- # ─── 8. Atlas: Anzahl Seiten berechnen ────────────────────────────
- # Nutzbarer Kartenbereich pro Atlasseite (abzüglich gleichem Randabstand)
- seite_karte_w = ziel_w - 210.0
- seite_karte_h = ziel_h - 20.0
-
- if seite_karte_w <= 0 or seite_karte_h <= 0:
- self.pruefmanager.zeige_hinweis(
- "Blattgröße zu klein",
- "Die gewählte Zielgröße ist kleiner als der Mindest-Randabstand. "
- "Bitte eine größere Blattgröße wählen.",
- )
- return {"ok": False, "switch_to_tab_a": False, "atlas_seiten": 0}
-
- pages_x = math.ceil(kartenbild_w / seite_karte_w)
- pages_y = math.ceil(kartenbild_h / seite_karte_h)
-
- # Für Atlas in beiden Modi (Endlosrolle + Blatt): Seitenraster so wählen,
- # dass Atlasobjekte näher am Einzelblattformat liegen und keine Streifen entstehen.
- opt_pages_x, opt_pages_y, opt_tile_w_mm, opt_tile_h_mm = self._find_tile_grid_for_roll_atlas(
- kartenbild_w_mm=kartenbild_w,
- kartenbild_h_mm=kartenbild_h,
- din_dims=din_dims,
- respect_max_sheet_size=(not formfaktor),
- )
-
- # Endlosrolle: Nur die Breite darf wachsen, die Höhe muss innerhalb
- # der gewählten Zielhöhe bleiben.
- if formfaktor:
- max_tile_h_mm = max(1.0, ziel_h - 20.0)
- if opt_tile_h_mm > max_tile_h_mm:
- required_pages_y = max(1, math.ceil(kartenbild_h / max_tile_h_mm))
- opt_pages_y = max(opt_pages_y, required_pages_y)
- opt_tile_h_mm = kartenbild_h / opt_pages_y
-
- pages_x = max(1, opt_pages_x)
- pages_y = max(1, opt_pages_y)
- seite_karte_w = max(1.0, opt_tile_w_mm)
- seite_karte_h = max(1.0, opt_tile_h_mm)
- modus = "Endlosrolle" if formfaktor else "Blatt"
- print(
- f"[TabBLogic] {modus} Rasteroptimierung: pages_x={pages_x}, pages_y={pages_y}, "
- f"tile_mm=({seite_karte_w:.1f}x{seite_karte_h:.1f}), "
- f"tile_aspect={seite_karte_w / seite_karte_h:.3f}"
- )
- if formfaktor:
- max_tile_h_mm = max(1.0, ziel_h - 20.0)
- print(
- f"[TabBLogic] Endlosrolle Höhenlimit: tile_h={seite_karte_h:.1f}mm, "
- f"max_tile_h={max_tile_h_mm:.1f}mm, within_limit={seite_karte_h <= max_tile_h_mm}"
- )
-
- anzahl_seiten = pages_x * pages_y
-
- ja = self.pruefmanager.frage_ja_nein(
- "Ausdruck als Atlas anlegen?",
- f"Für die ausgewählten Parameter sind {anzahl_seiten} Einzelseiten erforderlich.\n"
- "Ausdruck als Atlas anlegen?",
- default=True,
- )
- print(f"[TabBLogic] Atlas-Rückfrage: ja={ja}, geplante_seiten={anzahl_seiten}")
- if not ja:
- return {"ok": False, "switch_to_tab_a": False, "atlas_seiten": anzahl_seiten}
-
- atlas_layer = self._create_atlasobjekte_layer(
- layer=layer,
- extent=extent,
- pages_x=pages_x,
- pages_y=pages_y,
- seite_karte_w=seite_karte_w,
- seite_karte_h=seite_karte_h,
- massstab_zahl=massstab_zahl,
- )
- if atlas_layer is None:
- self.pruefmanager.zeige_hinweis(
- "Atlasobjekte",
- "Atlasobjekte-Layer konnte nicht erzeugt werden.",
- )
- return {"ok": False, "switch_to_tab_a": False, "atlas_seiten": 0}
- print("[TabBLogic] Atlasobjekte-Layer erfolgreich erzeugt")
-
- try:
- anzahl_seiten = int(atlas_layer.featureCount())
- except Exception:
- pass
-
- # Berechne die Kachelgröße aus den Atlasobjekten.
- # Für die Layout-Kartengröße muss die größte Atlas-Kachel passen,
- # sonst werden größere Atlasobjekte abgeschnitten.
- total_w_m = 0.0
- total_h_m = 0.0
- min_w_m = math.inf
- min_h_m = math.inf
- max_w_m = 0.0
- max_h_m = 0.0
- feature_count = 0
- get_features = getattr(atlas_layer, "getFeatures", None)
- if callable(get_features):
- try:
- for feat in get_features():
- geom = feat.geometry() if hasattr(feat, "geometry") else None
- if geom is None or geom.isEmpty():
- continue
- bbox = geom.boundingBox() if hasattr(geom, "boundingBox") else None
- if bbox is None:
- continue
- feat_w_m = float(bbox.width())
- feat_h_m = float(bbox.height())
- total_w_m += feat_w_m
- total_h_m += feat_h_m
- min_w_m = min(min_w_m, feat_w_m)
- min_h_m = min(min_h_m, feat_h_m)
- max_w_m = max(max_w_m, feat_w_m)
- max_h_m = max(max_h_m, feat_h_m)
- feature_count += 1
- except Exception:
- pass
-
- if feature_count > 0:
- avg_tile_w_m = total_w_m / feature_count
- avg_tile_h_m = total_h_m / feature_count
- if not math.isfinite(min_w_m):
- min_w_m = avg_tile_w_m
- if not math.isfinite(min_h_m):
- min_h_m = avg_tile_h_m
- target_tile_w_m = max_w_m
- target_tile_h_m = max_h_m
- else:
- avg_tile_w_m = seite_karte_w * massstab_zahl / 1000.0
- avg_tile_h_m = seite_karte_h * massstab_zahl / 1000.0
- min_w_m = avg_tile_w_m
- min_h_m = avg_tile_h_m
- max_w_m = avg_tile_w_m
- max_h_m = avg_tile_h_m
- target_tile_w_m = avg_tile_w_m
- target_tile_h_m = avg_tile_h_m
-
- # Konvertiere Kachelgrößen zu mm
- avg_tile_w_mm = avg_tile_w_m * 1000.0 / massstab_zahl
- avg_tile_h_mm = avg_tile_h_m * 1000.0 / massstab_zahl
- min_tile_w_mm = min_w_m * 1000.0 / massstab_zahl
- min_tile_h_mm = min_h_m * 1000.0 / massstab_zahl
- max_tile_w_mm = max_w_m * 1000.0 / massstab_zahl
- max_tile_h_mm = max_h_m * 1000.0 / massstab_zahl
- target_tile_w_mm = target_tile_w_m * 1000.0 / massstab_zahl
- target_tile_h_mm = target_tile_h_m * 1000.0 / massstab_zahl
-
- # Layout-Kartengröße = größte Kachelgröße
- atlas_map_w = max(1.0, target_tile_w_mm)
- atlas_map_h = max(1.0, target_tile_h_mm)
- atlas_page_w = atlas_map_w + 210.0
- atlas_page_h = atlas_map_h + 20.0
-
- # Debug: Atlasobjekte Geometrien
- print(
- f"[TabBLogic] Atlasobjekte Geometrien (Meter): "
- f"total_w={total_w_m:.1f}m, total_h={total_h_m:.1f}m, "
- f"min_w={min_w_m:.1f}m, min_h={min_h_m:.1f}m, "
- f"avg_w={avg_tile_w_m:.1f}m, avg_h={avg_tile_h_m:.1f}m, "
- f"max_w={max_w_m:.1f}m, max_h={max_h_m:.1f}m, features={feature_count}"
- )
- print(
- f"[TabBLogic] Atlas layout Größen: "
- f"page=(x=10, y=10, w={atlas_page_w:.1f}mm, h={atlas_page_h:.1f}mm), "
- f"map=(x=10, y=10, w={atlas_map_w:.1f}mm, h={atlas_map_h:.1f}mm), "
- f"kachel_min_mm=({min_tile_w_mm:.1f}x{min_tile_h_mm:.1f}), "
- f"kachel_avg_mm=({avg_tile_w_mm:.1f}x{avg_tile_h_mm:.1f}), "
- f"kachel_max_mm=({max_tile_w_mm:.1f}x{max_tile_h_mm:.1f})"
- )
-
- kartenname = get_variable(KARTENNAME_VAR, scope="project") or KARTENNAME_BY_AUSWAHL.get(
- kartenname_auswahl, "Atlas"
- )
- thema = get_variable(VIEW_VAR, scope="project") or ""
- vorlage_name, bestaetigt = self.pruefmanager.frage_text(
- "Neue Atlasvorlage anlegen",
- "Bezeichnung der Vorlage:",
- default_text=f"{kartenname} Atlas",
- )
- print(f"[TabBLogic] Atlas frage_text Ergebnis: name='{vorlage_name}', bestaetigt={bestaetigt}")
- if not bestaetigt:
- return {"ok": False, "switch_to_tab_a": False, "atlas_seiten": anzahl_seiten}
-
- vorlage_name = (vorlage_name or "").strip() or f"{kartenname} Atlas"
-
- try:
- print(
- f"[TabBLogic] Rufe create_atlas_layout auf: name='{vorlage_name}', "
- f"page=({atlas_page_w:.1f}x{atlas_page_h:.1f}), "
- f"map=({atlas_map_w:.1f}x{atlas_map_h:.1f}), massstab={massstab_zahl}, thema='{thema}'"
- )
- Layout().create_atlas_layout(
- name=vorlage_name,
- page_width_mm=atlas_page_w,
- page_height_mm=atlas_page_h,
- map_width_mm=atlas_map_w,
- map_height_mm=atlas_map_h,
- extent=extent,
- plotmassstab=massstab_zahl,
- atlas_layer=atlas_layer,
- thema=thema,
- )
- print("[TabBLogic] create_atlas_layout erfolgreich abgeschlossen")
- except ValueError as exc:
- self.pruefmanager.zeige_hinweis("Atlasvorlage anlegen", str(exc))
- return {"ok": False, "switch_to_tab_a": False, "atlas_seiten": 0}
- except Exception as exc:
- self.pruefmanager.zeige_hinweis("Atlasvorlage anlegen", f"Die Atlasvorlage konnte nicht angelegt werden: {exc}")
- return {"ok": False, "switch_to_tab_a": False, "atlas_seiten": 0}
-
- return {"ok": True, "switch_to_tab_a": False, "atlas_seiten": anzahl_seiten}
\ No newline at end of file
diff --git a/ui/tab_b_ui.py b/ui/tab_b_ui.py
deleted file mode 100644
index be1206f..0000000
--- a/ui/tab_b_ui.py
+++ /dev/null
@@ -1,432 +0,0 @@
-"""
-sn_plan41/ui/tab_b_ui.py – UI für Tab B (Druck)
-"""
-from __future__ import annotations
-
-from typing import Optional
-
-from sn_basis.functions.qt_wrapper import (
- QWidget,
- QVBoxLayout,
- QLabel,
- QComboBox,
- QCheckBox,
- QHBoxLayout,
- QPushButton,
-)
-from sn_basis.functions.qgiscore_wrapper import QgsProject
-from sn_basis.functions.qgisui_wrapper import iface
-from sn_basis.functions.variable_wrapper import get_variable, set_variable
-
-# Services (werden von DockWidget injiziert)
-from sn_basis.modules.Pruefmanager import Pruefmanager
-from sn_basis.modules.DataGrabber import DataGrabber
-from sn_plan41.ui.tab_b_logic import (
- TabBLogic,
- MASSSTAB_WIE_KARTENFENSTER,
- PLOTMASSSTAB_BY_AUSWAHL,
- THEMA_WIE_KARTENFENSTER,
- DIN_GROESSEN,
- DIN_STANDARD,
- ZIELGROESSE_VAR,
- FORMFAKTOR_VAR,
-)
-
-
-KARTENNAME_VAR = "tab_b_kartenname"
-KARTENNAME_PLACEHOLDER = "Kartenname wählen"
-KARTENNAME_38 = "§38"
-KARTENNAME_41 = "§41"
-MASSSTAB_VAR = "tab_b_massstab"
-THEMA_VAR = "tab_b_thema"
-ZIELGROESSE_UI_VAR = "tab_b_zielgroesse"
-FORMFAKTOR_UI_VAR = "tab_b_formfaktor"
-
-class TabB(QWidget):
- """
- UI-Klasse für Tab B (Druck) des Plan41-Plugins.
-
- Zuständig für:
- - Auswahl des Druckthemas
- - Auswahl der Druckparameter
- - Start der Vorlagenanlage (Druck über QGIS-Druckfunktion)
-
- Services (Pruefmanager, DataGrabber) werden zur Laufzeit vom DockWidget injiziert.
- Alle fachlichen Prüfungen laufen über den zentralen Pruefmanager.
- """
-
- tab_title = "Druck" #: Tab-Titel für BaseDockWidget
-
- def __init__(self, parent: Optional[QWidget] = None):
- """
- Initialisiert die UI-Struktur.
-
- Services werden später über :meth:`set_services` injiziert.
-
- :param parent: Parent-Widget (typischerweise DockWidget)
- """
- super().__init__(parent)
-
- # Services (werden von DockWidget gesetzt)
- self.pruefmanager: Optional[Pruefmanager] = None
-
- self.logic: Optional[TabBLogic] = None
- self._kartenname_combo: Optional[QComboBox] = None
- self._massstab_combo: Optional[QComboBox] = None
- self._thema_combo: Optional[QComboBox] = None
- self._theme_signal_connected = False
- self._connected_theme_collection: object = None # Referenz für sauberes Trennen
- self._zielgroesse_combo: Optional[QComboBox] = None
- self._endlosrolle_cb: Optional[QCheckBox] = None
- self._btn_vorlage_erstellen: Optional[QPushButton] = None
-
- self._build_ui()
- self._restore_state()
- self._connect_theme_collection_signals()
- self._connect_project_signals()
-
- def set_services(self, pruefmanager: Pruefmanager, data_grabber: DataGrabber) -> None:
- """Injiziert Services vom übergeordneten DockWidget."""
- _ = data_grabber
- self.pruefmanager = pruefmanager
- self.logic = TabBLogic(pruefmanager=self.pruefmanager)
-
- if self._kartenname_combo:
- self.logic.set_kartenname_for_auswahl(self._kartenname_combo.currentText())
- if self._massstab_combo:
- self.logic.set_plotmassstab_for_auswahl(
- self._massstab_combo.currentText(),
- self._get_current_canvas_scale(),
- )
- if self._thema_combo:
- self.logic.set_view_for_auswahl(self._thema_combo.currentText())
- if self._zielgroesse_combo:
- self.logic.set_zielgroesse_for_auswahl(self._zielgroesse_combo.currentText())
- if self._endlosrolle_cb:
- self.logic.set_formfaktor(self._endlosrolle_cb.isChecked())
-
- def _build_ui(self) -> None:
- """Erstellt die reduzierte UI für die Themenauswahl."""
- main_layout = QVBoxLayout()
- main_layout.setSpacing(4)
- main_layout.setContentsMargins(4, 4, 4, 4)
-
- kartenname_label = QLabel("Kartenname")
- kartenname_label.setStyleSheet("font-weight: bold; margin-top: 6px;")
- main_layout.addWidget(kartenname_label)
-
- self._kartenname_combo = QComboBox(self)
- self._kartenname_combo.addItem(KARTENNAME_PLACEHOLDER)
- self._kartenname_combo.addItem(KARTENNAME_38)
- self._kartenname_combo.addItem(KARTENNAME_41)
- self._kartenname_combo.currentTextChanged.connect(self._on_kartenname_changed)
- main_layout.addWidget(self._kartenname_combo)
-
- massstab_label = QLabel("Maßstab")
- massstab_label.setStyleSheet("font-weight: bold; margin-top: 6px;")
- main_layout.addWidget(massstab_label)
-
- self._massstab_combo = QComboBox(self)
- self._massstab_combo.addItem(MASSSTAB_WIE_KARTENFENSTER)
- self._massstab_combo.addItems(list(PLOTMASSSTAB_BY_AUSWAHL.keys()))
- self._massstab_combo.currentTextChanged.connect(self._on_massstab_changed)
- main_layout.addWidget(self._massstab_combo)
-
- thema_label = QLabel("Thema")
- thema_label.setStyleSheet("font-weight: bold; margin-top: 6px;")
- main_layout.addWidget(thema_label)
-
- self._thema_combo = QComboBox(self)
- self._thema_combo.addItem(THEMA_WIE_KARTENFENSTER)
- self._thema_combo.addItems(self._get_gespeicherte_themen())
- self._thema_combo.currentTextChanged.connect(self._on_thema_changed)
- main_layout.addWidget(self._thema_combo)
-
- zielgroesse_label = QLabel("max. Blattgröße")
- zielgroesse_label.setStyleSheet("font-weight: bold; margin-top: 6px;")
- main_layout.addWidget(zielgroesse_label)
-
- zielgroesse_row = QHBoxLayout()
- zielgroesse_row.setSpacing(6)
- self._zielgroesse_combo = QComboBox(self)
- self._zielgroesse_combo.addItems(list(DIN_GROESSEN.keys()))
- self._zielgroesse_combo.setCurrentText(DIN_STANDARD)
- self._zielgroesse_combo.currentTextChanged.connect(self._on_zielgroesse_changed)
- zielgroesse_row.addWidget(self._zielgroesse_combo)
- self._endlosrolle_cb = QCheckBox("Endlosrolle", self)
- self._endlosrolle_cb.setChecked(False)
- self._endlosrolle_cb.stateChanged.connect(self._on_formfaktor_changed)
- zielgroesse_row.addWidget(self._endlosrolle_cb)
- main_layout.addLayout(zielgroesse_row)
-
- self._btn_vorlage_erstellen = QPushButton("Vorlage erstellen", self)
- self._btn_vorlage_erstellen.clicked.connect(self._on_vorlage_erstellen)
- main_layout.addWidget(self._btn_vorlage_erstellen)
-
- main_layout.addStretch(1)
- self.setLayout(main_layout)
-
- def _restore_state(self) -> None:
- """Stellt die gespeicherten Combobox-Zustände wieder her."""
- if not self._kartenname_combo or not self._massstab_combo or not self._thema_combo:
- return
- if not self._zielgroesse_combo or not self._endlosrolle_cb:
- return
-
- saved_kartenname = get_variable(KARTENNAME_VAR, scope="project")
- if saved_kartenname in (KARTENNAME_38, KARTENNAME_41):
- self._kartenname_combo.setCurrentText(saved_kartenname)
- else:
- self._kartenname_combo.setCurrentText(KARTENNAME_PLACEHOLDER)
-
- saved_massstab = get_variable(MASSSTAB_VAR, scope="project")
- valid_massstaebe = [MASSSTAB_WIE_KARTENFENSTER, *PLOTMASSSTAB_BY_AUSWAHL.keys()]
- if saved_massstab in valid_massstaebe:
- self._massstab_combo.setCurrentText(saved_massstab)
- else:
- self._massstab_combo.setCurrentText(MASSSTAB_WIE_KARTENFENSTER)
-
- aktuelle_themen = [THEMA_WIE_KARTENFENSTER, *self._get_gespeicherte_themen()]
- self._thema_combo.clear()
- self._thema_combo.addItems(aktuelle_themen)
-
- saved_thema = get_variable(THEMA_VAR, scope="project")
- if saved_thema in aktuelle_themen:
- self._thema_combo.setCurrentText(saved_thema)
- else:
- self._thema_combo.setCurrentText(THEMA_WIE_KARTENFENSTER)
-
- saved_zielgroesse = get_variable(ZIELGROESSE_UI_VAR, scope="project")
- if saved_zielgroesse in DIN_GROESSEN:
- self._zielgroesse_combo.setCurrentText(saved_zielgroesse)
- else:
- self._zielgroesse_combo.setCurrentText(DIN_STANDARD)
-
- saved_formfaktor = get_variable(FORMFAKTOR_UI_VAR, scope="project")
- self._endlosrolle_cb.setChecked(saved_formfaktor == "Endlosrolle")
-
- def _on_kartenname_changed(self, value: str) -> None:
- """Persistiert die Kartennamen-Auswahl und setzt ``kartenname``."""
- if value in (KARTENNAME_38, KARTENNAME_41):
- set_variable(KARTENNAME_VAR, value, scope="project")
- else:
- set_variable(KARTENNAME_VAR, "", scope="project")
-
- if self.logic:
- self.logic.set_kartenname_for_auswahl(value)
-
- def _on_massstab_changed(self, value: str) -> None:
- """Persistiert Maßstabsauswahl und setzt ``plotmassstab``."""
- set_variable(MASSSTAB_VAR, value, scope="project")
-
- if self.logic:
- self.logic.set_plotmassstab_for_auswahl(value, self._get_current_canvas_scale())
-
- def _on_thema_changed(self, value: str) -> None:
- """Persistiert die Thema-Auswahl und setzt ``view``."""
- set_variable(THEMA_VAR, value, scope="project")
- if self.logic:
- self.logic.set_view_for_auswahl(value)
-
- def _on_zielgroesse_changed(self, value: str) -> None:
- """Persistiert Blattgröße und setzt ``zielgroesse``."""
- set_variable(ZIELGROESSE_UI_VAR, value, scope="project")
- if self.logic:
- self.logic.set_zielgroesse_for_auswahl(value)
-
- def _on_formfaktor_changed(self, state: int) -> None:
- """Persistiert Endlosrolle-Zustand und setzt ``formfaktor``."""
- checked = bool(state)
- set_variable(FORMFAKTOR_UI_VAR, "Endlosrolle" if checked else "Blatt", scope="project")
- if self.logic:
- self.logic.set_formfaktor(checked)
-
- def _on_vorlage_erstellen(self) -> None:
- """Startet die Pipeline Druckvorlage_anlegen."""
- print("[TabB] _on_vorlage_erstellen aufgerufen")
- if not self.logic or not self.pruefmanager:
- print(f"[TabB] Abbruch: logic={self.logic}, pruefmanager={self.pruefmanager}")
- return
-
- # Layer direkt aus Tab A lesen (unabhängig von Projektvariable)
- layer = None
- tab_a = self._get_tab_a_widget()
- print(f"[TabB] tab_a Widget: {tab_a!r}")
- if tab_a is not None:
- layer_combo = getattr(tab_a, "layer_combo", None)
- print(f"[TabB] layer_combo: {layer_combo!r}")
- if layer_combo is not None:
- layer = layer_combo.currentLayer()
- print(f"[TabB] layer: {layer!r} (Name: {getattr(layer, 'name', lambda: '–')()})")
-
- print(f"[TabB] Rufe druckvorlage_anlegen auf mit layer={layer!r}, "
- f"kartenname={self._kartenname_combo.currentText() if self._kartenname_combo else '?'}, "
- f"massstab={self._massstab_combo.currentText() if self._massstab_combo else '?'}, "
- f"zielgroesse={self._zielgroesse_combo.currentText() if self._zielgroesse_combo else '?'}, "
- f"formfaktor={self._endlosrolle_cb.isChecked() if self._endlosrolle_cb else '?'}")
-
- result = self.logic.druckvorlage_anlegen(
- layer=layer,
- kartenname_auswahl=self._kartenname_combo.currentText() if self._kartenname_combo else "",
- massstab_auswahl=self._massstab_combo.currentText() if self._massstab_combo else "",
- zielgroesse=self._zielgroesse_combo.currentText() if self._zielgroesse_combo else DIN_STANDARD,
- formfaktor=self._endlosrolle_cb.isChecked() if self._endlosrolle_cb else False,
- )
-
- print(f"[TabB] druckvorlage_anlegen Ergebnis: {result}")
- if result.get("switch_to_tab_a"):
- self._aktiviere_tab_a()
-
- def _get_tab_widget(self):
- """Findet das übergeordnete QTabWidget anhand des ``tabBar``-Attributs."""
- try:
- widget = self.parent()
- while widget is not None:
- if hasattr(widget, "tabBar") and hasattr(widget, "setCurrentIndex"):
- return widget
- parent_fn = getattr(widget, "parent", None)
- widget = parent_fn() if callable(parent_fn) else None
- except Exception:
- pass
- return None
-
- def _get_tab_a_widget(self):
- """Gibt die Tab-A-Widget-Instanz zurück (Index 0 im übergeordneten QTabWidget)."""
- tab_widget = self._get_tab_widget()
- if tab_widget is None:
- return None
- try:
- return tab_widget.widget(0)
- except Exception:
- return None
-
- def _aktiviere_tab_a(self) -> None:
- """Wechselt den aktiven Reiter auf Tab A im übergeordneten QTabWidget."""
- tab_widget = self._get_tab_widget()
- if tab_widget is not None:
- try:
- tab_widget.setCurrentIndex(0)
- except Exception:
- pass
-
- def _connect_project_signals(self) -> None:
- """Verbindet QgsProject-Signale für Projektwechsel/-neuladen."""
- project = QgsProject.instance()
- for signal_name in ("readProject", "newProjectCreated", "cleared"):
- signal = getattr(project, signal_name, None)
- if signal is None:
- continue
- try:
- signal.connect(self._on_project_changed)
- except Exception:
- pass
-
- def _on_project_changed(self, *args) -> None:
- """Reagiert auf Projektwechsel: Signale neu binden, Combobox und State auffrischen."""
- _ = args
- # Alte Theme-Collection-Signals zuerst trennen
- self._disconnect_theme_collection_signals()
- # Neu verbinden für das jetzt geladene Projekt
- self._theme_signal_connected = False
- self._connect_theme_collection_signals()
- # Ansicht-Liste + gespeicherten State wiederherstellen
- self._restore_state()
-
- def _disconnect_theme_collection_signals(self) -> None:
- """Trennt Signale der alten Theme-Collection sauber."""
- collection = self._connected_theme_collection
- if collection is not None:
- for signal_name in ("mapThemesChanged", "changed", "themeChanged"):
- try:
- signal = getattr(collection, signal_name, None)
- except RuntimeError:
- break
- except Exception:
- continue
- if signal is None:
- continue
- try:
- signal.disconnect(self._refresh_thema_combo_live)
- except Exception:
- pass
- self._connected_theme_collection = None
- self._theme_signal_connected = False
-
- def _connect_theme_collection_signals(self) -> None:
- """Verbindet Signale der Theme-Collection für Live-Aktualisierung der Themenliste."""
- if self._theme_signal_connected:
- return
-
- try:
- theme_collection = QgsProject.instance().mapThemeCollection()
- except Exception:
- return
-
- if theme_collection is None:
- return
-
- connected_any = False
- for signal_name in ("mapThemesChanged", "changed", "themeChanged"):
- signal = getattr(theme_collection, signal_name, None)
- if signal is None:
- continue
- try:
- signal.connect(self._refresh_thema_combo_live)
- connected_any = True
- except Exception:
- pass
-
- if connected_any:
- self._connected_theme_collection = theme_collection
- self._theme_signal_connected = connected_any
-
- def _refresh_thema_combo_live(self, *args) -> None:
- """Aktualisiert die Thema-Combobox bei Änderungen gespeicherter Layerthemen."""
- _ = args
- if not self._thema_combo:
- return
-
- vorherige_auswahl = self._thema_combo.currentText() or THEMA_WIE_KARTENFENSTER
- eintraege = [THEMA_WIE_KARTENFENSTER, *self._get_gespeicherte_themen()]
-
- self._thema_combo.blockSignals(True)
- self._thema_combo.clear()
- self._thema_combo.addItems(eintraege)
-
- if vorherige_auswahl in eintraege:
- self._thema_combo.setCurrentText(vorherige_auswahl)
- else:
- self._thema_combo.setCurrentText(THEMA_WIE_KARTENFENSTER)
- self._thema_combo.blockSignals(False)
-
- self._on_thema_changed(self._thema_combo.currentText())
-
- def _get_gespeicherte_themen(self) -> list[str]:
- """Liefert die Namen der im Projekt gespeicherten Layerthemen (QgsMapThemeCollection)."""
- try:
- theme_collection = QgsProject.instance().mapThemeCollection()
- if theme_collection is None:
- return []
- themes = theme_collection.mapThemes()
- except Exception:
- return []
-
- namen: list[str] = []
- for theme_name in themes:
- name = str(theme_name or "").strip()
- if name and name not in namen:
- namen.append(name)
-
- return namen
-
- def _get_current_canvas_scale(self) -> float | None:
- """Liest den aktuellen Maßstab aus der Kartensicht."""
- try:
- canvas = iface.mapCanvas()
- if canvas is None:
- return None
- scale = canvas.scale()
- return float(scale) if scale else None
- except Exception:
- return None
-