Files
Plugin_SN_Basis/modules/print_logic.py
T
2026-04-17 16:57:26 +02:00

1321 lines
52 KiB
Python

"""
sn_basis/modules/print_logic.py - Fachlogik fuer den Druck-Tab (nur Einzelblatt).
"""
from __future__ import annotations
import importlib
from typing import Any, cast, TypedDict
from sn_basis.functions.qgiscore_wrapper import QgsProject, get_layer_extent
from sn_basis.functions.qgisui_wrapper import iface
from sn_basis.functions.variable_wrapper import get_variable, set_variable
from sn_basis.modules.layerpruefer import Layerpruefer
from sn_basis.modules.Pruefmanager import Pruefmanager
from sn_basis.modules.print_layout import PrintLayout
from sn_basis.modules.print_layout_baufreigabe import PrintLayoutBaufreigabe
class AtlasConfig(TypedDict, total=False):
"""Strukturierte Konfiguration fuer Atlas-Optionen.
Alle Felder sind optional (total=False) und haben vordefinierte Standartwerte.
"""
# Basis-Konfiguration
enabled: bool # Atlas aktivieren (Standard: True nach Coverage-Layer-Setzung)
page_name_expression: str # QGIS Expression für Seitennamen (z.B. "@atlas_featurenumber")
# Filterung
filter_features: bool # Features filtern? (Standard: False)
filter_expression: str # QGIS Expression für Feature-Filter
# Sortierung
sort_features: bool # Features sortieren? (Standard: False)
sort_expression: str # QGIS Expression/Feldname zum Sortieren
sort_ascending: bool # Aufsteigend sortieren? (Standard: True)
# Rendering
hide_coverage: bool # Coverage-Layer in Layout verstecken? (Standard: False)
limit_render_to_current_feature: bool # Rendering auf aktuelles Feature limitieren (QGIS 4.0+, Standard: False)
# Metadaten
description: str # Beschreibung der Atlas-Konfiguration
# Vordefinierte Standardkonfigurationen für Baufreigabe
ATLAS_CONFIG_BAUFREIGABE_DEFAULT: AtlasConfig = {
"enabled": True,
"page_name_expression": "@atlas_featurenumber",
"filter_features": False,
"filter_expression": "",
"sort_features": False,
"sort_expression": "",
"sort_ascending": True,
"hide_coverage": False,
"limit_render_to_current_feature": True,
"description": "Baufreigabe Atlas mit Standardkonfiguration",
}
KARTENNAME_VAR = "kartenname"
PLOTMASSSTAB_VAR = "plotmassstab"
VIEW_VAR = "view"
ZIELGROESSE_VAR = "zielgroesse"
FORMFAKTOR_VAR = "formfaktor"
ATLAS_ERZEUGEN_UI_VAR = "tab_b_atlas_erzeugen"
DRUCKBEREICH_VAR = "druckbereich"
DRUCKPUFFER_VAR = "druckpuffer"
DRUCKRECHTECK_LAYER_NAME = "Druckrechteck"
KARTENNAME_38 = "§38"
KARTENNAME_41 = "§41"
KARTENNAME_BAUFREIGABE = "Anlage zur Baufreigabe"
MASSSTAB_WIE_KARTENFENSTER = "Wie Kartenfenster"
THEMA_WIE_KARTENFENSTER = "wie kartenfenster"
DRUCKBEREICH_VERFAHRENSGEBIET = "Verfahrensgebiet"
DRUCKBEREICH_AKTUELLE_AUSWAHL = "aktuelle Auswahl"
DRUCKBEREICH_RECHTECK = "Rechteck"
DRUCKBEREICH_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)",
KARTENNAME_BAUFREIGABE: "Anlage zur Baufreigabe",
}
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",
}
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_AUTO_REIHENFOLGE = ("DIN A4", "DIN A3", "DIN A2", "DIN A1", "DIN A0")
DIN_STANDARD = "DIN A0"
DRUCKPUFFER_STANDARD_CM = 1
ENDLOSROLLE_MAX_BREITE_MM = 2000.0
STANDARD_LAYOUT_FIXBREITE_MM = 210.0
STANDARD_LAYOUT_FIXHOEHE_MM = 20.0
BAUFREIGABE_LAYOUT_FIXBREITE_MM = 110.0
BAUFREIGABE_LAYOUT_FIXHOEHE_MM = 20.0
STANDARD_A4_LANDSCAPE_W_MM = 297.0
STANDARD_A4_LANDSCAPE_H_MM = 210.0
class PrintLogic:
"""Kapselt die Fachlogik fuer den Druck-Tab (ohne Atlas)."""
def __init__(self, pruefmanager: Pruefmanager) -> None:
self.pruefmanager = pruefmanager
self._rectangle_tool: Any | None = None
self._previous_map_tool: Any | None = None
def _failed_result(self) -> dict:
return {"ok": False, "switch_to_tab_a": False, "atlas_seiten": 0}
def _show_target_size_too_small(self) -> dict:
self.pruefmanager.zeige_hinweis(
"Zielgroesse zu klein",
"Die gewaehlte Zielgroesse reicht fuer den Einzelblatt-Druck nicht aus.",
)
return self._failed_result()
def _is_enabled_flag(self, raw_value: Any) -> bool:
if raw_value in (None, ""):
return True
return str(raw_value).strip().lower() in {"1", "true", "ja", "yes", "on"}
def _resolve_layer_from_project(self) -> object | None:
project = QgsProject.instance()
# Primär: neue zentrale Variable aus sn_verfahrensgebiet_manager
layer_id = (get_variable("verfahrensgebiet_layer", scope="project") or "").strip()
# Rückwärtskompatibilität zu älteren Projekten
if not layer_id:
layer_id = (get_variable("verfahrensgebietslayer", scope="project") or "").strip()
if not layer_id:
layer_id = (get_variable("tab_a_layer_id", scope="project") or "").strip()
if not layer_id:
return None
map_layer = getattr(project, "mapLayer", None)
if not callable(map_layer):
return None
try:
return map_layer(layer_id)
except Exception:
return None
def _resolve_active_layer(self) -> object | None:
active_layer_fn = getattr(iface, "activeLayer", None)
if not callable(active_layer_fn):
return None
try:
return active_layer_fn()
except Exception:
return None
def _get_druckpuffer_cm(self) -> int:
raw_value = get_variable(DRUCKPUFFER_VAR, scope="project")
try:
value = int(str(raw_value).strip())
except (TypeError, ValueError):
value = DRUCKPUFFER_STANDARD_CM
return max(0, value)
def _get_druckpuffer_m(self, plotmassstab: float) -> float:
return (self._get_druckpuffer_cm() * 10.0 * plotmassstab) / 1000.0
def _import_qgis_symbol(self, module_name: str, symbol_name: str) -> Any | None:
try:
module = importlib.import_module(module_name)
except Exception:
return None
return getattr(module, symbol_name, None)
def _build_rectangle(self, x_min: float, y_min: float, x_max: float, y_max: float) -> Any | None:
qgs_rectangle = self._import_qgis_symbol("qgis.core", "QgsRectangle")
if qgs_rectangle is None:
return None
try:
return qgs_rectangle(x_min, y_min, x_max, y_max)
except Exception:
return None
def _clone_extent(self, extent: Any) -> Any | None:
if extent is None:
return None
buffered = getattr(extent, "buffered", None)
if callable(buffered):
try:
return buffered(0.0)
except Exception:
pass
x_min = getattr(extent, "xMinimum", lambda: None)()
y_min = getattr(extent, "yMinimum", lambda: None)()
x_max = getattr(extent, "xMaximum", lambda: None)()
y_max = getattr(extent, "yMaximum", lambda: None)()
if None in (x_min, y_min, x_max, y_max):
return None
assert x_min is not None
assert y_min is not None
assert x_max is not None
assert y_max is not None
x_min_f = float(x_min)
y_min_f = float(y_min)
x_max_f = float(x_max)
y_max_f = float(y_max)
return self._build_rectangle(x_min_f, y_min_f, x_max_f, y_max_f)
def _buffer_extent(self, extent: Any, plotmassstab: float) -> Any | None:
if extent is None:
return None
buffer_m = self._get_druckpuffer_m(plotmassstab)
if buffer_m <= 0:
return extent
buffered = getattr(extent, "buffered", None)
if callable(buffered):
try:
return buffered(buffer_m)
except Exception:
pass
x_min = getattr(extent, "xMinimum", lambda: None)()
y_min = getattr(extent, "yMinimum", lambda: None)()
x_max = getattr(extent, "xMaximum", lambda: None)()
y_max = getattr(extent, "yMaximum", lambda: None)()
if None in (x_min, y_min, x_max, y_max):
return extent
assert x_min is not None
assert y_min is not None
assert x_max is not None
assert y_max is not None
x_min_f = float(x_min)
y_min_f = float(y_min)
x_max_f = float(x_max)
y_max_f = float(y_max)
expanded = self._build_rectangle(
x_min_f - buffer_m,
y_min_f - buffer_m,
x_max_f + buffer_m,
y_max_f + buffer_m,
)
return expanded or extent
def _get_existing_layer_by_name(self, layer_name: str) -> object | None:
project = QgsProject.instance()
map_layers_by_name = getattr(project, "mapLayersByName", None)
if not callable(map_layers_by_name):
return None
try:
matches = map_layers_by_name(layer_name)
except Exception:
return None
if not isinstance(matches, (list, tuple)) or not matches:
return None
return matches[0]
def _create_druckrechteck_layer(self, reference_layer: object | None) -> object | None:
existing_layer = self._get_existing_layer_by_name(DRUCKRECHTECK_LAYER_NAME)
if existing_layer is not None:
return existing_layer
qgs_vector_layer = self._import_qgis_symbol("qgis.core", "QgsVectorLayer")
if qgs_vector_layer is None:
return None
crs_authid = "EPSG:25832"
if reference_layer is not None:
try:
layer_crs = getattr(reference_layer, "crs", lambda: None)()
if layer_crs is not None:
crs_authid = layer_crs.authid()
except Exception:
pass
layer = qgs_vector_layer(f"Polygon?crs={crs_authid}", DRUCKRECHTECK_LAYER_NAME, "memory")
if not layer or not getattr(layer, "isValid", lambda: False)():
return None
try:
QgsProject.instance().addMapLayer(layer)
except Exception:
return None
return layer
def _clear_layer_features(self, layer: object) -> None:
get_features = getattr(layer, "getFeatures", None)
data_provider = getattr(layer, "dataProvider", lambda: None)()
if not callable(get_features) or data_provider is None:
return
feature_ids: list[int] = []
try:
features = cast(Any, get_features())
for feature in features:
feature_id = getattr(feature, "id", lambda: None)()
if feature_id is not None:
feature_ids.append(int(feature_id))
except Exception:
feature_ids = []
if feature_ids:
delete_features = getattr(data_provider, "deleteFeatures", None)
if callable(delete_features):
try:
delete_features(feature_ids)
except Exception:
pass
def create_atlas_layer_from_selection(self, source_layer: object | None = None) -> tuple[object | None, str]:
"""Erstellt einen neuen Layer 'Atlaslayer' aus der Auswahl des Quell-Layers.
Args:
source_layer: Der Layer mit der Auswahl (wenn None, wird aktiver Layer verwendet)
Returns:
(created_layer, layer_id) oder (None, "") bei Fehler
"""
# Source-Layer bestimmen
if source_layer is None:
source_layer = self._resolve_active_layer()
if source_layer is None:
self.pruefmanager.zeige_hinweis(
"Atlas Layer erstellen",
"Kein Layer mit Auswahl gefunden.",
)
return None, ""
# Pruefen ob Auswahl vorhanden ist
selected_count = getattr(source_layer, "selectedFeatureCount", None)
if callable(selected_count):
try:
if int(cast(Any, selected_count())) == 0:
self.pruefmanager.zeige_hinweis(
"Atlas Layer erstellen",
"Auf dem Layer sind keine Features ausgewaehlt.",
)
return None, ""
except Exception:
pass
# Bestehenden Atlaslayer loeschen (optional Wiederverwedung)
existing_atlas = self._get_existing_layer_by_name("Atlaslayer")
if existing_atlas is not None:
try:
QgsProject.instance().removeMapLayer(existing_atlas)
except Exception:
pass
# Neuen Memory-Layer mit Geometrie-Type des Quell-Layers erstellen
qgs_vector_layer = self._import_qgis_symbol("qgis.core", "QgsVectorLayer")
if qgs_vector_layer is None:
self.pruefmanager.zeige_hinweis(
"Atlas Layer erstellen",
"QgsVectorLayer ist nicht verfuegbar.",
)
return None, ""
# CRS vom Quell-Layer übernehmen
crs_authid = "EPSG:25832"
try:
source_crs = getattr(source_layer, "crs", lambda: None)()
if source_crs is not None:
crs_authid_method = getattr(source_crs, "authid", lambda: None)
if callable(crs_authid_method):
crs_authid = crs_authid_method() or crs_authid
except Exception:
pass
# Geometrie-Type vom Quell-Layer bestimmen (vereinfacht: Polygon für Atlas)
# TODO: Intelligentere Bestimmung des Geometrie-Types
atlas_layer = qgs_vector_layer(f"Polygon?crs={crs_authid}", "Atlaslayer", "memory")
if not atlas_layer or not getattr(atlas_layer, "isValid", lambda: False)():
self.pruefmanager.zeige_hinweis(
"Atlas Layer erstellen",
"Der Atlaslayer konnte nicht erstellt werden.",
)
return None, ""
# Ausgewaehlte Features kopieren
try:
get_selected = getattr(source_layer, "selectedFeatures", None)
if not callable(get_selected):
raise Exception("selectedFeatures ist nicht aufrufbar")
selected_features = list(cast(Any, get_selected()))
if not selected_features:
raise Exception("Keine Features ausgewaehlt")
qgs_feature = self._import_qgis_symbol("qgis.core", "QgsFeature")
if qgs_feature is None:
raise Exception("QgsFeature Symbol nicht verfuegbar")
data_provider = getattr(atlas_layer, "dataProvider", lambda: None)()
if data_provider is None:
raise Exception("dataProvider nicht verfuegbar")
new_features = []
for source_feature in selected_features:
geometry = getattr(source_feature, "geometry", lambda: None)()
if geometry is None:
continue
new_feature = qgs_feature()
set_geometry = getattr(new_feature, "setGeometry", None)
if callable(set_geometry):
try:
set_geometry(geometry)
new_features.append(new_feature)
except Exception:
pass
if new_features:
add_features = getattr(data_provider, "addFeatures", None)
if callable(add_features):
add_features(new_features)
update_fields = getattr(atlas_layer, "updateFields", None)
if callable(update_fields):
update_fields()
update_extents = getattr(atlas_layer, "updateExtents", None)
if callable(update_extents):
update_extents()
except Exception as exc:
self.pruefmanager.zeige_hinweis(
"Atlas Layer erstellen",
f"Fehler beim Kopieren der Features: {exc}",
)
return None, ""
# Layer ins Projekt aufnehmen
try:
QgsProject.instance().addMapLayer(atlas_layer)
except Exception:
self.pruefmanager.zeige_hinweis(
"Atlas Layer erstellen",
"Der Layer konnte nicht ins Projekt aufgenommen werden.",
)
return None, ""
# Layer-ID speichern
layer_id = getattr(atlas_layer, "id", lambda: "")() or ""
set_variable("sn_abdeckungslayer", layer_id, scope="project")
self.pruefmanager.zeige_hinweis(
"Atlas Layer erstellen",
f"Atlas Layer 'Atlaslayer' erfolgreich erstellt.",
)
return atlas_layer, layer_id
def configure_atlas_for_baufreigabe_layout(
self,
layout_name: str = "",
atlas_config: AtlasConfig | None = None,
) -> bool:
"""Konfiguriert den Atlas fuer ein Baufreigabe-Layout mit erweiterten Optionen.
Wird nach erfolgreicher Baufreigabe-Layout-Erstellung aufgerufen.
Setzt den Coverage-Layer (aus sn_abdeckungslayer Variable) und konfiguriert den Atlas
mit allen Optionen aus der AtlasConfig.
"""
try:
if atlas_config is None:
atlas_config = ATLAS_CONFIG_BAUFREIGABE_DEFAULT
project = QgsProject.instance()
layout_manager = getattr(project, "layoutManager", lambda: None)()
if layout_manager is None:
self.pruefmanager.zeige_hinweis("Atlas konfigurieren", "Layout Manager nicht verfuegbar.")
return False
if not layout_name:
layouts_fn = getattr(layout_manager, "layouts", None)
if not callable(layouts_fn):
return False
try:
layout_iterable = layouts_fn()
if layout_iterable is None:
return False
last_layout: Any | None = None
for candidate_layout in cast(Any, layout_iterable):
last_layout = candidate_layout
if last_layout is None:
return False
layout = last_layout
except Exception:
return False
else:
layout_by_name = getattr(layout_manager, "layoutByName", None)
if not callable(layout_by_name):
return False
layout = layout_by_name(layout_name)
if layout is None:
return False
atlas = getattr(layout, "atlas", lambda: None)()
if atlas is None:
return False
atlas_enabled = bool(atlas_config.get("enabled", True))
if not atlas_enabled:
set_enabled = getattr(atlas, "setEnabled", None)
if callable(set_enabled):
try:
set_enabled(False)
except Exception:
pass
# Hauptkarte ebenfalls aus Atlas-Steuerung lösen
item_by_id = getattr(layout, "itemById", None)
if callable(item_by_id):
try:
hauptkarte = item_by_id("Hauptkarte")
set_atlas_driven = getattr(hauptkarte, "setAtlasDriven", None)
if callable(set_atlas_driven):
set_atlas_driven(False)
except Exception:
pass
return True
coverage_layer_id = get_variable("sn_abdeckungslayer", scope="project") or ""
if not coverage_layer_id:
return True
coverage_layer = project.mapLayer(coverage_layer_id)
if coverage_layer is None:
self.pruefmanager.zeige_hinweis(
"Atlas konfigurieren",
f"Atlas-Layer mit ID '{coverage_layer_id}' nicht gefunden.",
)
return False
set_coverage = getattr(atlas, "setCoverageLayer", None)
if callable(set_coverage):
set_coverage(coverage_layer)
page_name_expr = atlas_config.get("page_name_expression", "")
if page_name_expr:
set_page_name = getattr(atlas, "setPageNameExpression", None)
if callable(set_page_name):
set_page_name(page_name_expr)
if atlas_config.get("filter_features", False):
set_filter_features = getattr(atlas, "setFilterFeatures", None)
if callable(set_filter_features):
set_filter_features(True)
filter_expr = atlas_config.get("filter_expression", "")
if filter_expr:
set_filter_expr = getattr(atlas, "setFilterExpression", None)
if callable(set_filter_expr):
set_filter_expr(filter_expr)
if atlas_config.get("sort_features", False):
set_sort_features = getattr(atlas, "setSortFeatures", None)
if callable(set_sort_features):
set_sort_features(True)
sort_expr = atlas_config.get("sort_expression", "")
if sort_expr:
set_sort_expr = getattr(atlas, "setSortExpression", None)
if callable(set_sort_expr):
set_sort_expr(sort_expr)
set_sort_asc = getattr(atlas, "setSortAscending", None)
if callable(set_sort_asc):
set_sort_asc(atlas_config.get("sort_ascending", True))
set_hide_coverage = getattr(atlas, "setHideCoverage", None)
if callable(set_hide_coverage):
set_hide_coverage(atlas_config.get("hide_coverage", False))
set_limit_render = getattr(atlas, "setLimitCoverageLayerRenderToCurrentFeature", None)
if callable(set_limit_render):
try:
set_limit_render(atlas_config.get("limit_render_to_current_feature", False))
except Exception:
pass
num_features = 0
update_features = getattr(atlas, "updateFeatures", None)
if callable(update_features):
try:
num_features = update_features() or 0
except Exception:
num_features = 0
set_enabled = getattr(atlas, "setEnabled", None)
if callable(set_enabled):
set_enabled(atlas_config.get("enabled", True))
return True
except Exception as exc:
self.pruefmanager.zeige_hinweis(
"Atlas konfigurieren",
f"Fehler beim Konfigurieren des Atlas: {exc}",
)
return False
def _store_druckrechteck(self, layer: object, rectangle: Any) -> bool:
qgs_feature = self._import_qgis_symbol("qgis.core", "QgsFeature")
qgs_geometry = self._import_qgis_symbol("qgis.core", "QgsGeometry")
if qgs_feature is None or qgs_geometry is None:
return False
if layer is None or rectangle is None:
return False
data_provider = getattr(layer, "dataProvider", lambda: None)()
if data_provider is None:
return False
self._clear_layer_features(layer)
feature = qgs_feature()
try:
feature.setGeometry(qgs_geometry.fromRect(rectangle))
add_features = getattr(data_provider, "addFeatures", None)
if callable(add_features):
add_features([feature])
update_extents = getattr(layer, "updateExtents", None)
if callable(update_extents):
update_extents()
trigger_repaint = getattr(layer, "triggerRepaint", None)
if callable(trigger_repaint):
trigger_repaint()
except Exception:
return False
try:
set_variable("druckrechteck_xmin", str(rectangle.xMinimum()), scope="project")
set_variable("druckrechteck_ymin", str(rectangle.yMinimum()), scope="project")
set_variable("druckrechteck_xmax", str(rectangle.xMaximum()), scope="project")
set_variable("druckrechteck_ymax", str(rectangle.yMaximum()), scope="project")
except Exception:
pass
map_canvas = getattr(iface, "mapCanvas", lambda: None)()
if map_canvas is not None:
refresh = getattr(map_canvas, "refresh", None)
if callable(refresh):
try:
refresh()
except Exception:
pass
return True
def activate_druckrechteck_capture(self, reference_layer: object | None = None) -> bool:
layer = self._create_druckrechteck_layer(reference_layer)
if layer is None:
self.pruefmanager.zeige_hinweis(
"Druckrechteck",
"Der temporaere Layer 'Druckrechteck' konnte nicht erzeugt werden.",
)
return False
map_canvas = getattr(iface, "mapCanvas", lambda: None)()
if map_canvas is None:
self.pruefmanager.zeige_hinweis(
"Druckrechteck",
"Das Kartenfenster ist nicht verfuegbar.",
)
return False
qgs_geometry = self._import_qgis_symbol("qgis.core", "QgsGeometry")
qgs_rectangle = self._import_qgis_symbol("qgis.core", "QgsRectangle")
qgs_wkb_types = self._import_qgis_symbol("qgis.core", "QgsWkbTypes")
qgs_map_tool_emit_point = self._import_qgis_symbol("qgis.gui", "QgsMapToolEmitPoint")
qgs_rubber_band = self._import_qgis_symbol("qgis.gui", "QgsRubberBand")
if None in (qgs_geometry, qgs_rectangle, qgs_wkb_types, qgs_map_tool_emit_point, qgs_rubber_band):
self.pruefmanager.zeige_hinweis(
"Druckrechteck",
"Die Rechteck-Erfassung ist in dieser QGIS-Umgebung nicht verfuegbar.",
)
return False
logic = self
self._previous_map_tool = getattr(map_canvas, "mapTool", lambda: None)()
base_tool_class = cast(type, qgs_map_tool_emit_point)
rubber_band_class = cast(Any, qgs_rubber_band)
geometry_class = cast(Any, qgs_geometry)
rectangle_class = cast(Any, qgs_rectangle)
polygon_geometry = cast(Any, getattr(qgs_wkb_types, "PolygonGeometry", None))
class RectangleCaptureTool(base_tool_class):
def __init__(self, canvas_widget: Any) -> None:
super().__init__(canvas_widget)
self.canvas = canvas_widget
self.start_point: Any | None = None
self.rubber_band: Any = rubber_band_class(canvas_widget, polygon_geometry)
def _to_rect(self, start_point: Any, end_point: Any) -> Any:
x_min = min(start_point.x(), end_point.x())
x_max = max(start_point.x(), end_point.x())
y_min = min(start_point.y(), end_point.y())
y_max = max(start_point.y(), end_point.y())
return rectangle_class(x_min, y_min, x_max, y_max)
def _show_rect(self, rect: Any) -> None:
try:
self.rubber_band.setToGeometry(geometry_class.fromRect(rect), None)
except Exception:
pass
def canvasPressEvent(self, event: Any) -> None:
self.start_point = self.toMapCoordinates(event.pos())
def canvasMoveEvent(self, event: Any) -> None:
if self.start_point is None:
return
end_point = self.toMapCoordinates(event.pos())
self._show_rect(self._to_rect(self.start_point, end_point))
def canvasReleaseEvent(self, event: Any) -> None:
if self.start_point is None:
return
end_point = self.toMapCoordinates(event.pos())
rectangle = self._to_rect(self.start_point, end_point)
self._show_rect(rectangle)
width = getattr(rectangle, "width", lambda: 0.0)()
height = getattr(rectangle, "height", lambda: 0.0)()
if width <= 0 or height <= 0:
logic.pruefmanager.zeige_hinweis(
"Druckrechteck",
"Das gezeichnete Rechteck ist ungueltig.",
)
else:
if logic._store_druckrechteck(layer, rectangle):
logic.pruefmanager.zeige_hinweis(
"Druckrechteck",
"Das Druckrechteck wurde uebernommen.",
)
try:
self.rubber_band.reset(polygon_geometry)
except Exception:
pass
if logic._previous_map_tool is not None:
try:
self.canvas.setMapTool(logic._previous_map_tool)
except Exception:
pass
logic._rectangle_tool = None
rectangle_tool = RectangleCaptureTool(map_canvas)
self._rectangle_tool = rectangle_tool
set_active_layer = getattr(iface, "setActiveLayer", None)
if callable(set_active_layer):
try:
set_active_layer(layer)
except Exception:
pass
start_editing = getattr(layer, "startEditing", None)
if callable(start_editing):
try:
start_editing()
except Exception:
pass
set_map_tool = getattr(map_canvas, "setMapTool", None)
if callable(set_map_tool):
try:
set_map_tool(rectangle_tool)
except Exception:
self.pruefmanager.zeige_hinweis(
"Druckrechteck",
"Das Rechteck-Werkzeug konnte nicht aktiviert werden.",
)
return False
self.pruefmanager.zeige_hinweis(
"Druckrechteck",
"Bitte ziehen Sie ein Rechteck in der Karte auf.",
)
return True
def _resolve_selection_layer(self, fallback_layer: object | None) -> object | None:
active_layer = self._resolve_active_layer()
if active_layer is not None:
selected_count = getattr(active_layer, "selectedFeatureCount", None)
if callable(selected_count):
try:
selected_count_value = selected_count()
if int(cast(Any, selected_count_value)) > 0:
return active_layer
except Exception:
pass
return fallback_layer
def _get_selected_extent(self, layer: object | None) -> Any | None:
if layer is None:
return None
get_selected_features = getattr(layer, "selectedFeatures", None)
if not callable(get_selected_features):
return None
try:
selected_features_any = get_selected_features()
selected_features = list(cast(Any, selected_features_any))
except Exception:
return None
if not selected_features:
return None
merged_extent: Any | None = None
for feature in selected_features:
geometry = getattr(feature, "geometry", lambda: None)()
if geometry is None:
continue
bounding_box = getattr(geometry, "boundingBox", lambda: None)()
if bounding_box is None:
continue
if merged_extent is None:
merged_extent = self._clone_extent(bounding_box)
continue
combine_extent = getattr(merged_extent, "combineExtentWith", None)
if callable(combine_extent):
try:
combine_extent(bounding_box)
continue
except Exception:
pass
x_min = min(float(merged_extent.xMinimum()), float(bounding_box.xMinimum()))
y_min = min(float(merged_extent.yMinimum()), float(bounding_box.yMinimum()))
x_max = max(float(merged_extent.xMaximum()), float(bounding_box.xMaximum()))
y_max = max(float(merged_extent.yMaximum()), float(bounding_box.yMaximum()))
merged_extent = self._build_rectangle(x_min, y_min, x_max, y_max)
return merged_extent
def _get_druckrechteck_extent(self) -> Any | None:
layer = self._get_existing_layer_by_name(DRUCKRECHTECK_LAYER_NAME)
extent = get_layer_extent(layer)
if extent is not None:
width = getattr(extent, "width", lambda: 0.0)()
height = getattr(extent, "height", lambda: 0.0)()
if width > 0 and height > 0:
return extent
try:
x_min = float(get_variable("druckrechteck_xmin", scope="project"))
y_min = float(get_variable("druckrechteck_ymin", scope="project"))
x_max = float(get_variable("druckrechteck_xmax", scope="project"))
y_max = float(get_variable("druckrechteck_ymax", scope="project"))
except (TypeError, ValueError):
return None
return self._build_rectangle(x_min, y_min, x_max, y_max)
def _get_canvas_extent(self) -> Any | None:
map_canvas_fn = getattr(iface, "mapCanvas", None)
if not callable(map_canvas_fn):
return None
try:
canvas = map_canvas_fn()
except Exception:
return None
if canvas is None:
return None
extent_fn = getattr(canvas, "extent", None)
if not callable(extent_fn):
return None
try:
extent = extent_fn()
except Exception:
return None
if extent is None:
return None
is_null = getattr(extent, "isNull", None)
if callable(is_null):
try:
if is_null():
return None
except Exception:
return None
width_fn = getattr(extent, "width", None)
height_fn = getattr(extent, "height", None)
if not callable(width_fn) or not callable(height_fn):
return None
try:
width_value = float(cast(Any, width_fn()))
height_value = float(cast(Any, height_fn()))
if width_value <= 0 or height_value <= 0:
return None
except Exception:
return None
return extent
def _resolve_extent(
self,
resolved_layer: object | None,
druckbereich_auswahl: str,
plotmassstab: float,
) -> tuple[Any | None, object | None]:
if druckbereich_auswahl == DRUCKBEREICH_WIE_KARTENFENSTER:
extent = self._get_canvas_extent()
if extent is None:
self.pruefmanager.zeige_hinweis(
"Druckbereich",
"Das aktuelle Kartenfenster liefert keine gueltige Ausdehnung.",
)
return None, None
return extent, None
if druckbereich_auswahl == DRUCKBEREICH_AKTUELLE_AUSWAHL:
selection_layer = self._resolve_selection_layer(resolved_layer)
extent = self._get_selected_extent(selection_layer)
if extent is None:
self.pruefmanager.zeige_hinweis(
"Druckbereich",
"Auf dem aktuellen Layer sind keine gueltigen Objekte ausgewaehlt.",
)
return None, None
return self._buffer_extent(extent, plotmassstab), None
if druckbereich_auswahl == DRUCKBEREICH_RECHTECK:
extent = self._get_druckrechteck_extent()
if extent is None:
self.activate_druckrechteck_capture(resolved_layer)
return None, None
return extent, None
lp = Layerpruefer(layer=resolved_layer)
ergebnis = lp.pruefe()
if not ergebnis.ok:
self.pruefmanager.zeige_hinweis(
"Verfahrensgebiets-Layer angeben",
"Kein gueltiger Verfahrensgebiets-Layer gefunden. Bitte Layerauswahl im jeweiligen Plugin setzen.",
)
return None, None
extent = get_layer_extent(resolved_layer)
if extent is None:
self.pruefmanager.zeige_hinweis("Fehler", "Layer-Ausdehnung konnte nicht ermittelt werden.")
return None, None
return self._buffer_extent(extent, plotmassstab), resolved_layer
def set_kartenname_for_auswahl(self, auswahl: str) -> None:
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:
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:
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:
set_variable(ZIELGROESSE_VAR, auswahl if auswahl in DIN_GROESSEN else DIN_STANDARD, scope="project")
def set_formfaktor(self, endlosrolle: bool) -> None:
set_variable(FORMFAKTOR_VAR, "Endlosrolle" if endlosrolle else "Blatt", scope="project")
def set_druckbereich_for_auswahl(self, auswahl: str) -> None:
if auswahl not in {
DRUCKBEREICH_VERFAHRENSGEBIET,
DRUCKBEREICH_WIE_KARTENFENSTER,
DRUCKBEREICH_AKTUELLE_AUSWAHL,
DRUCKBEREICH_RECHTECK,
}:
auswahl = DRUCKBEREICH_VERFAHRENSGEBIET
set_variable(DRUCKBEREICH_VAR, auswahl, scope="project")
def _get_baufreigabe_page_dimensions(
self,
required_map_width_mm: float,
required_map_height_mm: float,
max_format: str = "DIN A3",
) -> tuple[str, float, float, float, float, bool]:
"""Waehlt fuer Baufreigabe ein Blatt im Querformat.
Args:
required_map_width_mm: benoetigte Breite der Hauptkarte in mm
required_map_height_mm: benoetigte Hoehe der Hauptkarte in mm
max_format: maximale Blattgroesse, standardmaessig DIN A3
Returns:
(din_format, page_width_mm, page_height_mm, map_w_mm, map_h_mm, fallback_applied)
"""
candidates = (
("DIN A4", 297.0, 210.0),
("DIN A3", 420.0, 297.0),
)
try:
max_index = [candidate[0] for candidate in candidates].index(max_format)
except ValueError:
max_index = len(candidates) - 1
limited_candidates = candidates[: max_index + 1]
for din_format, page_w, page_h in limited_candidates:
map_w = page_w - BAUFREIGABE_LAYOUT_FIXBREITE_MM
map_h = page_h - BAUFREIGABE_LAYOUT_FIXHOEHE_MM
if map_w >= required_map_width_mm and map_h >= required_map_height_mm:
return din_format, page_w, page_h, map_w, map_h, False
# Wenn der gewuenschte Ausschnitt nicht passt, verwende das maximal erlaubte Blatt
# und erweitere die Hauptkarte auf die verfuegbare Kartenflaeche (zentriert per fit_extent_to_map).
din_format, page_w, page_h = limited_candidates[-1]
map_w = max(page_w - BAUFREIGABE_LAYOUT_FIXBREITE_MM, 50.0)
map_h = max(page_h - BAUFREIGABE_LAYOUT_FIXHOEHE_MM, 50.0)
return din_format, page_w, page_h, map_w, map_h, True
def _does_plot_fit_page(
self,
page_width_mm: float,
page_height_mm: float,
map_width_mm: float,
map_height_mm: float,
fixed_width_mm: float,
fixed_height_mm: float,
) -> bool:
required_w = map_width_mm + fixed_width_mm
required_h = map_height_mm + fixed_height_mm
return required_w <= page_width_mm and required_h <= page_height_mm
def _get_plot_dimensions_mm(
self,
map_width_mm: float,
map_height_mm: float,
fixed_width_mm: float,
fixed_height_mm: float,
) -> tuple[float, float]:
return map_width_mm + fixed_width_mm, map_height_mm + fixed_height_mm
def _get_required_map_dimensions_mm(self, extent: Any, plotmassstab: float) -> tuple[float, float]:
"""Berechnet die benoetigte Kartenfenstergroesse in mm fuer Extent und Massstab."""
if extent is None or not isinstance(plotmassstab, (int, float)) or plotmassstab <= 0:
return 0.0, 0.0
try:
width_mm = float(extent.width()) * 1000.0 / float(plotmassstab)
height_mm = float(extent.height()) * 1000.0 / float(plotmassstab)
except Exception:
return 0.0, 0.0
return max(width_mm, 0.0), max(height_mm, 0.0)
def _select_standard_page_layout(
self,
required_map_width_mm: float,
required_map_height_mm: float,
max_format: str,
) -> tuple[float, float, float, float] | None:
"""Waehlt das kleinste passende DIN-Blatt fuer das Standardlayout."""
try:
max_index = DIN_AUTO_REIHENFOLGE.index(max_format)
except ValueError:
max_index = len(DIN_AUTO_REIHENFOLGE) - 1
for fmt in DIN_AUTO_REIHENFOLGE[: max_index + 1]:
base_w, base_h = DIN_GROESSEN[fmt]
fitting_candidates: list[tuple[float, float, float, float]] = []
for page_w, page_h in ((float(base_w), float(base_h)), (float(base_h), float(base_w))):
map_w = page_w - STANDARD_LAYOUT_FIXBREITE_MM
map_h = page_h - STANDARD_LAYOUT_FIXHOEHE_MM
if map_w >= required_map_width_mm and map_h >= required_map_height_mm:
fitting_candidates.append((page_w, page_h, map_w, map_h))
if fitting_candidates:
return min(
fitting_candidates,
key=lambda candidate: (candidate[2] - required_map_width_mm) + (candidate[3] - required_map_height_mm),
)
return None
def _get_standard_layout_dimensions(
self,
required_map_width_mm: float,
required_map_height_mm: float,
max_format: str,
formfaktor: bool,
) -> tuple[float, float, float, float, bool] | None:
if formfaktor:
plotgroesse_w, plotgroesse_h = self._get_plot_dimensions_mm(
required_map_width_mm,
required_map_height_mm,
STANDARD_LAYOUT_FIXBREITE_MM,
STANDARD_LAYOUT_FIXHOEHE_MM,
)
din_dims = DIN_GROESSEN.get(max_format, DIN_GROESSEN[DIN_STANDARD])
max_page_h = float(max(din_dims))
page_w = min(plotgroesse_w, ENDLOSROLLE_MAX_BREITE_MM)
page_h = min(plotgroesse_h, max_page_h)
return self._apply_standard_minimum_a4_fallback(
page_w,
page_h,
required_map_width_mm,
required_map_height_mm,
)
selected_layout = self._select_standard_page_layout(
required_map_width_mm,
required_map_height_mm,
max_format,
)
if selected_layout is None:
return None
page_w, page_h, map_w, map_h = selected_layout
force_fit_extent = map_w > required_map_width_mm or map_h > required_map_height_mm
return page_w, page_h, map_w, map_h, force_fit_extent
def _apply_standard_minimum_a4_fallback(
self,
page_width_mm: float,
page_height_mm: float,
map_width_mm: float,
map_height_mm: float,
) -> tuple[float, float, float, float, bool]:
"""Erzwingt bei zu kleinem Standardlayout mindestens DIN A4 im Querformat.
Liefert zusätzlich ein Flag zurück, damit die Extent-Anpassung im Layout
zur zentrierten Darstellung aktiviert werden kann.
"""
fallback_page_w = max(page_width_mm, STANDARD_A4_LANDSCAPE_W_MM)
fallback_page_h = max(page_height_mm, STANDARD_A4_LANDSCAPE_H_MM)
if fallback_page_w == page_width_mm and fallback_page_h == page_height_mm:
return page_width_mm, page_height_mm, map_width_mm, map_height_mm, False
fallback_map_w = max(map_width_mm, fallback_page_w - STANDARD_LAYOUT_FIXBREITE_MM, 50.0)
fallback_map_h = max(map_height_mm, fallback_page_h - STANDARD_LAYOUT_FIXHOEHE_MM, 50.0)
return fallback_page_w, fallback_page_h, fallback_map_w, fallback_map_h, True
def druckvorlage_anlegen(
self,
layer: object | None,
kartenname_auswahl: str,
massstab_auswahl: str,
zielgroesse: str,
formfaktor: bool,
druckbereich_auswahl: str,
) -> dict:
resolved_layer = layer or self._resolve_layer_from_project()
if kartenname_auswahl not in (KARTENNAME_38, KARTENNAME_41, KARTENNAME_BAUFREIGABE):
self.pruefmanager.zeige_hinweis("Kartennamen waehlen", "Kartennamen waehlen")
return self._failed_result()
if massstab_auswahl == MASSSTAB_WIE_KARTENFENSTER:
# Maßstab direkt vom aktuellen Canvas lesen; Projektvariable als Fallback.
try:
canvas = iface.mapCanvas()
live_scale = canvas.scale() if canvas is not None else 0.0
massstab_zahl = float(live_scale) if live_scale > 0 else 0.0
except Exception:
massstab_zahl = 0.0
if massstab_zahl <= 0:
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("Massstab fehlt", "Kein gueltiger Massstab angegeben.")
return self._failed_result()
extent, layer_for_extent = self._resolve_extent(resolved_layer, druckbereich_auswahl, massstab_zahl)
if extent is None:
return self._failed_result()
if layer_for_extent is not None:
layer_id = getattr(layer_for_extent, "id", lambda: "")() or ""
if layer_id:
# Neu: zentrale Variable
set_variable("verfahrensgebiet_layer", layer_id, scope="project")
# Alt: Rückwärtskompatibilität
set_variable("verfahrensgebietslayer", layer_id, scope="project")
kartenbild_w, kartenbild_h = self._get_required_map_dimensions_mm(extent, massstab_zahl)
force_fit_extent_for_min_a4 = False
if kartenname_auswahl == KARTENNAME_BAUFREIGABE:
_, auto_page_w, auto_page_h, auto_map_w, auto_map_h, _bf_fallback = self._get_baufreigabe_page_dimensions(
kartenbild_w,
kartenbild_h,
max_format=zielgroesse if zielgroesse in {"DIN A4", "DIN A3"} else "DIN A3",
)
else:
standard_layout = self._get_standard_layout_dimensions(
kartenbild_w,
kartenbild_h,
zielgroesse,
formfaktor,
)
if standard_layout is None:
return self._show_target_size_too_small()
auto_page_w, auto_page_h, auto_map_w, auto_map_h, force_fit_extent_for_min_a4 = standard_layout
# Finale Sicherheitspruefung: alle Layoutobjekte muessen auf die Seite passen.
if kartenname_auswahl == KARTENNAME_BAUFREIGABE:
fixed_width_mm = BAUFREIGABE_LAYOUT_FIXBREITE_MM
fixed_height_mm = BAUFREIGABE_LAYOUT_FIXHOEHE_MM
else:
fixed_width_mm = STANDARD_LAYOUT_FIXBREITE_MM
fixed_height_mm = STANDARD_LAYOUT_FIXHOEHE_MM
fits_page = self._does_plot_fit_page(
auto_page_w,
auto_page_h,
auto_map_w,
auto_map_h,
fixed_width_mm,
fixed_height_mm,
)
if not fits_page:
return self._show_target_size_too_small()
kartenname = get_variable(KARTENNAME_VAR, scope="project") or KARTENNAME_BY_AUSWAHL.get(
kartenname_auswahl, "Vorlage"
)
thema = get_variable(VIEW_VAR, scope="project") or ""
vorlage_name, bestaetigt = self.pruefmanager.frage_text(
"Neue Vorlage anlegen",
"Bezeichnung der Vorlage:",
default_text=kartenname,
)
if not bestaetigt:
return self._failed_result()
vorlage_name = (vorlage_name or "").strip() or kartenname
# Atlas-Status vorab bestimmen, damit er sowohl an create_ als auch configure_ uebergeben wird.
atlas_raw_bf = get_variable(ATLAS_ERZEUGEN_UI_VAR, scope="project")
atlas_erzeugen_bf = self._is_enabled_flag(atlas_raw_bf)
# Verzweigung: Baufreigabe vs. Standard-Layout
try:
if kartenname_auswahl == KARTENNAME_BAUFREIGABE:
# Baufreigabe: feste DIN A3/A4 Querformat-Logik mit 80mm Kartenschild
din_format = "DIN A3" if auto_page_w >= 420.0 else "DIN A4"
PrintLayoutBaufreigabe().create_baufreigabe_layout(
name=vorlage_name if vorlage_name != kartenname else "Baufreigabe",
din_format=din_format,
map_width_mm=auto_map_w,
map_height_mm=auto_map_h,
extent=extent,
plotmassstab=massstab_zahl,
thema=thema,
fit_extent_to_map=True,
atlas_driven=atlas_erzeugen_bf,
)
else:
# Standard-Layout: dynamische Groessenanpassung
PrintLayout().create_single_page_layout(
name=vorlage_name,
page_width_mm=auto_page_w,
page_height_mm=auto_page_h,
map_width_mm=auto_map_w,
map_height_mm=auto_map_h,
extent=extent,
plotmassstab=massstab_zahl,
kartenname_auswahl=kartenname_auswahl,
thema=thema,
fit_extent_to_map=(druckbereich_auswahl != DRUCKBEREICH_VERFAHRENSGEBIET) or force_fit_extent_for_min_a4,
)
except ValueError as exc:
self.pruefmanager.zeige_hinweis("Vorlage anlegen", str(exc))
return self._failed_result()
except Exception as exc:
self.pruefmanager.zeige_hinweis(
"Vorlage anlegen",
f"Die Vorlage konnte nicht angelegt werden: {exc}",
)
return self._failed_result()
# Nach erfolgreicher Layout-Erstellung: Atlas ggf. konfigurieren (nur falls aktiviert)
if kartenname_auswahl == KARTENNAME_BAUFREIGABE:
if atlas_erzeugen_bf:
self.configure_atlas_for_baufreigabe_layout()
else:
self.configure_atlas_for_baufreigabe_layout(
atlas_config={
"enabled": False,
}
)
return {"ok": True, "switch_to_tab_a": False, "atlas_seiten": 1}