Files
Plugin_SN_Basis/modules/print_layout_baufreigabe.py
T
2026-04-17 17:15:13 +02:00

403 lines
15 KiB
Python

"""
sn_basis/modules/print_layout_baufreigabe.py - Layout-Erstellung fuer Baufreigabe-Vorlagen.
Baufreigabe-Layouts verwenden immer feste DIN A3/A4 Seitengroessen im Querformat,
ein 120 mm breites Kartenschild (statt 180 mm) und erbenerben die Standard-Logik
fuer Hauptkarte, Quellenangabe und Legende vom PrintLayout.
Atlas-Integration erfolgt nach Layouterstellung durch externe Konfiguration
in der PrintLogic.
"""
from __future__ import annotations
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
from sn_basis.modules.print_layout import PrintLayout
MM = QgsUnitTypes.LayoutMillimeters
class PrintLayoutBaufreigabe(PrintLayout):
"""Erzeugt QGIS-Einzelblatt-Layouts mit fester DIN A3/A4 Querformat-Groesse und 80mm Kartenschild."""
_FRAME_STROKE_WIDTH_MM = 0.5
_KARTENSCHILD_WIDTH_MM = 80.0
_KARTENSCHILD_HEIGHT_MM = 58.0
_ATLAS_FRAME_MARGIN_MM = 10.0
_GRID_INTERVAL_MAP_UNITS = 500.0
# DIN-Formate: nur A3 und A4 im Querformat
_DIN_A3_LANDSCAPE = (420, 297) # Breite x Hoehe (Querformat)
_DIN_A4_LANDSCAPE = (297, 210) # Querformat
_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": (80.0, 100.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": (80.0, 60.0),
},
}
_QUELLENANGABE_TEXT = (
"Quelle Geobasisdaten: GeoSN, "
"<a href=\"https://www.govdata.de/dl-de/by-2-0\">dl-de/by-2-0</a><br>"
"Quelle Fachdaten: Darstellung auf der Grundlage von Daten und mit Erlaubnis des "
"Sächsischen Landesamtes für Umwelt, Landwirtschaft und Geologie<br>"
"Basemap:"
"© GeoBasis-DE / <a href=\"https://www.bkg.bund.de\">BKG</a> ([%year($now)%]) "
"<a href=\"https://creativecommons.org/licences/by/4.0/\">CC BY 4.0</a> "
"mit teilweise angepasster Signatur<br>"
)
def __init__(self, project: Any | None = None) -> None:
super().__init__(project)
def create_baufreigabe_layout(
self,
name: str,
din_format: str,
map_width_mm: float,
map_height_mm: float,
extent: Any,
plotmassstab: float,
thema: str = "",
fit_extent_to_map: bool = True,
atlas_driven: bool = True,
) -> Any:
"""Erstellt ein Baufreigabe-Layout mit fester DIN A3/A4 Querformat-Groesse.
Args:
name: Name des Layouts
din_format: "DIN A3" oder "DIN A4"
map_width_mm: Breite der Hauptkarte
map_height_mm: Hoehe der Hauptkarte
extent: QgsRectangle der Kartenausdehnung
plotmassstab: Plotmassstab (z.B. 5000 fuer 1:5000)
thema: Name des Visibility-Presets (optional)
fit_extent_to_map: Extent bei Bedarf auf das Karten-Seitenverhaeltnis erweitern
Returns:
QgsPrintLayout objektInstance (wird im Layout-Dialog geoeffnet)
Raises:
ValueError: Falls Name bereits existiert oder Format ungueltig ist
"""
# Seitenformat auswaehlen (nur A3/A4 im Querformat)
if din_format == "DIN A3":
page_width_mm, page_height_mm = self._DIN_A3_LANDSCAPE
elif din_format == "DIN A4":
page_width_mm, page_height_mm = self._DIN_A4_LANDSCAPE
else:
raise ValueError(f"Baufreigabe: Format {din_format} ist nicht unterstützt. Nur DIN A3/A4 Querformat.")
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))
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,
)
fitted_extent = extent
if fit_extent_to_map:
fitted_extent = self._fit_extent_to_map_item(extent, map_width_mm, map_height_mm)
if fitted_extent is not None and hasattr(fitted_extent, "isNull") and callable(fitted_extent.isNull) and not fitted_extent.isNull():
try:
set_extent = getattr(hauptkarte, "setExtent", None)
if callable(set_extent):
set_extent(fitted_extent)
else:
hauptkarte.zoomToExtent(fitted_extent)
except Exception:
pass
if isinstance(plotmassstab, (int, float)) and plotmassstab > 0:
try:
hauptkarte.setScale(plotmassstab)
except Exception:
pass
# Atlas-Steuerung nur aktivieren, wenn Atlas tatsaechlich genutzt wird.
# Andernfalls bleiben Extent/Scale unveraendert (z.B. Canvas-Ausschnitt).
if atlas_driven:
set_atlas_driven_fn = getattr(hauptkarte, "setAtlasDriven", None)
if callable(set_atlas_driven_fn):
try:
set_atlas_driven_fn(True)
except Exception:
pass
set_atlas_scaling_mode = getattr(hauptkarte, "setAtlasScalingMode", None)
atlas_mode_auto = getattr(getattr(QgsLayoutItemMap, "AtlasScalingMode", object), "Auto", None)
if callable(set_atlas_scaling_mode) and atlas_mode_auto is not None:
try:
set_atlas_scaling_mode(atlas_mode_auto)
except Exception:
pass
set_atlas_margin = getattr(hauptkarte, "setAtlasMargin", None)
if callable(set_atlas_margin):
try:
set_atlas_margin(self._calc_atlas_margin_percent(map_width_mm, map_height_mm))
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, self._FRAME_STROKE_WIDTH_MM)
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_keep_layer_set = getattr(hauptkarte, "setKeepLayerSet", None)
if callable(set_keep_layer_set):
try:
set_keep_layer_set(True)
except Exception:
pass
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
)
try:
set_refresh_strategy(refresh_cache)
except Exception:
pass
self._configure_hauptkarte_grids(hauptkarte)
self._setup_layout_objects_baufreigabe(
kartenfenster_untere_rechte_ecke_mm,
hauptkarte,
map_top_mm,
map_height_mm,
)
layout_manager.addLayout(layout)
self._current_layout = None
open_layout_designer(layout)
return layout
def _setup_layout_objects_baufreigabe(
self,
kartenfenster_untere_rechte_ecke_mm: tuple,
hauptkarte: Any,
map_top_mm: float,
map_height_mm: float,
) -> None:
"""Erstellt Legende, Quellenangabe und Kartenschild fuer Baufreigabe-Layout.
Angepasst fuer 80mm Kartenschild statt 180mm.
"""
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,
)
# Legende: 80mm Breite, hoehe adaptive wie Standard
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=self._KARTENSCHILD_WIDTH_MM,
height_mm=legende_height_mm,
object_name="Legende",
)
legende_hoehe_mm = self._configure_legend_size(
legende,
width_limit_mm=self._KARTENSCHILD_WIDTH_MM,
column_count=1,
equal_column_width=False,
)
# Quellenangabe: 80mm Breite
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:
self._configure_legend_size(
legende,
width_limit_mm=self._KARTENSCHILD_WIDTH_MM,
column_count=2,
equal_column_width=True,
)
self.add_text_label(
self._QUELLENANGABE_TEXT,
quellenangabe_x,
quellenangabe_y,
width_mm=self._KARTENSCHILD_WIDTH_MM,
height_mm=20.0,
text_format="Format_1",
html_mode=True,
expression_enabled=True,
object_name="Quellenangabe",
)
# Kartenschild: 80mm Breite
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,
self._KARTENSCHILD_WIDTH_MM,
self._KARTENSCHILD_HEIGHT_MM,
stroke_width_mm=self._FRAME_STROKE_WIDTH_MM,
)
# Kartenschild-Linien: 6 Trennlinien (Positionen gleich)
kartenschild_line_x, _ = _coords_rel_untere_rechte_ecke(10.0, 0.0)
kartenschild_line_offsets_y = (-10.0, -20.0, -30.0, -46.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,
self._KARTENSCHILD_WIDTH_MM,
0.0,
stroke_width_mm=0.1,
)
# Textfelder im Kartenschild ohne Skalierungslogik.
# Positionen werden bewusst als feste Werte gesetzt und bei Bedarf manuell angepasst.
labels = [
("Teilnehmergemeinschaft [%@sn_bezeichnung %] [%@sn_name %]", 14.0, -48.0, 74.0, 8.0, "Format_1", "Herausgeber","Bottom"),
("[% @sn_bezeichnung %]", 14.0, -41.0, 74.0, 4.0, "Format_1", "Bezeichnung_Flurbereinigung"),
("[% @sn_name %]", 14.0, -32.0, 74.0, 5.0, "Format_2", "Name_Flurbereinigung"),
("[% @sn_kartenname %]", 14.0, -23.0, 74.0, 6.0, "Format_3", "Kartenname"),
("Ausgabedatum: [%format_date(now(), 'dd.MM.yyyy')%]", 14.0, -13.0, 74.0, 4.0, "Format_1", "Ausgabedatum"),
(
"Maßstab: 1 : [%round(map_get(item_variables('Hauptkarte'),'map_scale'),0)%]",
14.0,
-3.0,
74.0,
4.0,
"Format_1",
"Massstab",
),
]
for entry in labels:
text, rx, ry, w, h, fmt, obj_name = entry[:7]
v_align = entry[7] if len(entry) > 7 else "top"
x, y = _coords_rel_untere_rechte_ecke(rx, ry)
self.add_text_label(
text,
x,
y,
width_mm=w,
height_mm=h,
text_format=fmt,
expression_enabled=True,
object_name=obj_name,
v_align=v_align,
)
def _calc_atlas_margin_percent(self, map_width_mm: float, map_height_mm: float) -> float:
"""Berechnet Atlas-Margin in Prozent, sodass etwa 10 mm Rand sichtbar bleiben."""
margin_mm = self._ATLAS_FRAME_MARGIN_MM
if map_width_mm <= 2 * margin_mm or map_height_mm <= 2 * margin_mm:
return 5.0
p_width = (100.0 * margin_mm) / (map_width_mm - 2.0 * margin_mm)
p_height = (100.0 * margin_mm) / (map_height_mm - 2.0 * margin_mm)
return max(p_width, p_height)