Files
Plugin_SN_Basis/modules/print_layout.py
T
DanielandClaude Opus 5.5 006459ab61 Kartenrand überlappungsfrei anordnen (#93)
Im Standardlayout hingen Hauptlegende (oben), Quellenangabe und Kartenschild (unten) an der
Kartenhöhe; bei kleinen Verfahren waren sie zusammen höher als die Karte und überlappten.
Neues Modul print_kartenrand: Nach dem Aufbau der Vorlage (inkl. Basemap-Legende) werden die
Legenden einmal gezeichnet – die Hauptlegende filtert nach Karteninhalt, ihre Höhe steht erst
danach fest – und für 1–3 Spalten vermessen. Gewählt wird die kleinste passende Spaltenzahl
(Kartenrand wird dafür breiter); passt keine, die mit dem geringsten Platzbedarf, und die Seite
wird höher, Quellenangabe und Kartenschild rutschen nach unten. Karte bleibt in Größe und Lage.
Fehler bei der Anordnung verhindern die Vorlage nicht (Warnung). Wrapper: QgsLayoutExporter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 13:06:35 +02:00

1128 lines
40 KiB
Python

"""
sn_basis/modules/print_layout.py - Aufbau von Einzelblatt-Drucklayouts.
"""
from __future__ import annotations
import importlib
import math
from typing import Any
from sn_basis.functions.qt_wrapper import QFont, Qt
from sn_basis.functions.qgiscore_wrapper import (
MAP_GRID_ANNOTATION_DISPLAY_HIDE_ALL,
MAP_GRID_ANNOTATION_DISPLAY_SHOW_X,
MAP_GRID_ANNOTATION_DISPLAY_SHOW_Y,
MAP_GRID_ANNOTATION_FORMAT_DECIMAL,
MAP_GRID_ANNOTATION_POSITION_INSIDE_FRAME,
MAP_GRID_BORDER_BOTTOM,
MAP_GRID_BORDER_LEFT,
MAP_GRID_BORDER_RIGHT,
MAP_GRID_BORDER_TOP,
MAP_GRID_FRAME_STYLE_INTERIOR_TICKS,
MAP_GRID_STYLE_FRAME_ANNOTATIONS,
MAP_GRID_UNIT_MAP_UNIT,
TEXT_BACKGROUND_SHAPE_RECTANGLE,
TEXT_BACKGROUND_SIZE_BUFFER,
TEXT_RENDER_UNIT_MILLIMETERS,
TEXT_RENDER_UNIT_POINTS,
QgsLayoutItem,
QgsLayoutItemLabel,
QgsLayoutItemLegend,
QgsLayoutItemMap,
QgsLayoutItemMapGrid,
QgsLayoutPoint,
QgsLayoutSize,
QgsPrintLayout,
QgsProject,
QgsTextBackgroundSettings,
QgsTextFormat,
QgsUnitTypes,
)
from sn_basis.functions.qgisui_wrapper import open_layout_designer
MM = QgsUnitTypes.LayoutMillimeters
class PrintLayout:
"""Erzeugt ein QGIS-Einzelblatt-Layout für den Druck."""
_FRAME_STROKE_WIDTH_MM = 0.5
_LEGEND_WIDTH_MM = 180.0
_GRID_INTERVAL_MAP_UNITS = 500.0
_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",
},
"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, "
"<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:
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:
"""Test ausgesetzt, zunächst Refactor"""
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:
"""Test ausgesetzt, zunächst Refactor"""
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 _fit_extent_to_map_item(self, extent: Any, map_width_mm: float, map_height_mm: float) -> Any:
"""Test ausgesetzt, zunächst Refactor"""
if extent is None:
return extent
if map_width_mm <= 0 or map_height_mm <= 0:
return extent
try:
extent_width = float(extent.width())
extent_height = float(extent.height())
except Exception:
return extent
if extent_width <= 0 or extent_height <= 0:
return extent
target_ratio = map_width_mm / map_height_mm
current_ratio = extent_width / extent_height
if not math.isfinite(target_ratio) or not math.isfinite(current_ratio) or target_ratio <= 0:
return extent
try:
center_x = (float(extent.xMinimum()) + float(extent.xMaximum())) / 2.0
center_y = (float(extent.yMinimum()) + float(extent.yMaximum())) / 2.0
except Exception:
return extent
fitted_width = extent_width
fitted_height = extent_height
if not math.isclose(current_ratio, target_ratio, rel_tol=1e-9, abs_tol=1e-9):
if current_ratio > target_ratio:
fitted_height = extent_width / target_ratio
else:
fitted_width = extent_height * target_ratio
safety_factor = 1.002
half_width = (fitted_width * safety_factor) / 2.0
half_height = (fitted_height * safety_factor) / 2.0
try:
qgis_core = importlib.import_module("qgis.core")
qgs_rectangle = getattr(qgis_core, "QgsRectangle", None)
except Exception:
qgs_rectangle = None
if qgs_rectangle is None:
return extent
try:
return qgs_rectangle(
center_x - half_width,
center_y - half_height,
center_x + half_width,
center_y + half_height,
)
except Exception:
return extent
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,
v_align: str = "top",
) -> Any:
"""Test ausgesetzt, zunächst Refactor"""
if self._current_layout is None:
raise RuntimeError("Kein Layout aktiv. Bitte create_single_page_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,
v_align=v_align,
)
def _setup_layout_objects(
self,
kartenfenster_untere_rechte_ecke_mm: tuple,
hauptkarte: Any,
map_top_mm: float,
map_height_mm: float,
kartenname_auswahl: str = "",
) -> None:
"""Test ausgesetzt, zunächst Refactor"""
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_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._LEGEND_WIDTH_MM,
height_mm=legende_height_mm,
object_name="Legende",
)
legende_hoehe_mm = self._configure_legend_size(
legende,
width_limit_mm=self._LEGEND_WIDTH_MM,
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:
self._configure_legend_size(
legende,
width_limit_mm=self._LEGEND_WIDTH_MM,
column_count=2,
equal_column_width=True,
)
self.add_text_label(
self._QUELLENANGABE_TEXT,
quellenangabe_x,
quellenangabe_y,
width_mm=self._LEGEND_WIDTH_MM,
height_mm=20.0,
text_format="Format_1",
html_mode=True,
expression_enabled=True,
object_name="Quellenangabe",
)
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._LEGEND_WIDTH_MM,
92.0,
stroke_width_mm=self._FRAME_STROKE_WIDTH_MM,
)
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,
self._LEGEND_WIDTH_MM,
0.0,
stroke_width_mm=0.1,
)
if kartenname_auswahl == "§38":
herausgeber_text = "[%@sn_landkreis_user%] [%@sn_behoerde%]"
else:
herausgeber_text = "Teilnehmergemeinschaft [%@sn_bezeichnung %] [%@sn_name %]"
labels = [
(herausgeber_text, 16.0, -84.0, 171.0, 4.0, "Format_1", "Herausgeber"),
("[% @sn_bezeichnung %]", 16.0, -75.0, 171.0, 4.0, "Format_1", "Bezeichnung_Flurbereinigung"),
("[% @sn_name %]", 16.0, -64.0, 171.0, 5.0, "Format_2", "Name_Flurbereinigung"),
("[% @sn_gemeinden %]", 16.0, -56.0, 171.0, 4.0, "Format_1", "Stadt_Gemeinde"),
("[% @sn_landkreise_proj %]", 16.0, -48.0, 171.0, 4.0, "Format_1", "Landkreis"),
("[% @sn_kartenname %]", 16.0, -32.0, 137.0, 12.0, "Format_3", "Kartenname"),
("Gefertigt: ", 16.0, -23.0, 81.0, 4.0, "Format_1", "Gefertigt"),
("Geprüft: ", 101.0, -23.0, 81.0, 4.0, "Format_1", "Geprüft"),
(
"Maßstab: 1 : [%round(map_get(item_variables('Hauptkarte'),'map_scale'),0)%]",
16.0,
-13.0,
81.0,
4.0,
"Format_1",
"Massstab",
),
("Ausgabedatum: [%format_date(now(), 'dd.MM.yyyy')%]", 101.0, -13.0, 81.0, 4.0, "Format_1", "Ausgabedatum"),
("Aktenzeichen: ", 16.0, -3.0, 171.0, 4.0, "Format_1", "Aktenzeichen"),
]
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 _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,
v_align: str = "top",
) -> Any:
"""Test ausgesetzt, zunächst Refactor"""
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)
align_map = {
"top": "AlignTop",
"center": "AlignVCenter",
"bottom": "AlignBottom",
}
align_attr = align_map.get(str(v_align).lower(), "AlignTop")
align_qt = getattr(Qt, align_attr, None)
if align_qt is None:
alignment_flag = getattr(Qt, "AlignmentFlag", None)
if alignment_flag is not None:
align_qt = getattr(alignment_flag, align_attr, None)
set_v_align = getattr(label, "setVAlign", None)
if callable(set_v_align) and align_qt is not None:
try:
set_v_align(align_qt)
except Exception:
pass
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]:
"""Test ausgesetzt, zunächst Refactor"""
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(True)
except Exception:
pass
set_filter_by_map = getattr(legend, "setLegendFilterByMapEnabled", None)
if callable(set_filter_by_map):
try:
set_filter_by_map(True)
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, self._FRAME_STROKE_WIDTH_MM)
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
_, updated_height_mm = self._get_item_size_mm(legend)
if updated_height_mm is None:
updated_height_mm = 20.0
if callable(set_locked):
try:
set_locked(True)
except Exception:
pass
return updated_height_mm
def _configure_hauptkarte_grids(self, hauptkarte: Any) -> None:
grids_method = getattr(hauptkarte, "grids", None)
if not callable(grids_method):
return
try:
grid_stack = grids_method()
except Exception:
return
if grid_stack is None:
return
remove_grid = getattr(grid_stack, "removeGrid", None)
if callable(remove_grid):
for grid_id in ("SN_FrameAnnotationGrid", "SN_MarkerGrid"):
try:
remove_grid(grid_id)
except Exception:
pass
add_grid = getattr(grid_stack, "addGrid", None)
if not callable(add_grid):
return
if QgsLayoutItemMapGrid is object:
return
frame_annotation_grid = QgsLayoutItemMapGrid("SN_FrameAnnotationGrid", hauptkarte)
self._apply_grid_common_settings(frame_annotation_grid)
self._safe_grid_call(frame_annotation_grid, "setStyle", MAP_GRID_STYLE_FRAME_ANNOTATIONS)
self._safe_grid_call(frame_annotation_grid, "setFrameStyle", MAP_GRID_FRAME_STYLE_INTERIOR_TICKS)
self._safe_grid_call(frame_annotation_grid, "setAnnotationEnabled", True)
self._safe_grid_call(frame_annotation_grid, "setAnnotationFormat", MAP_GRID_ANNOTATION_FORMAT_DECIMAL)
self._safe_grid_call(frame_annotation_grid, "setAnnotationPrecision", 0)
self._safe_grid_call(
frame_annotation_grid,
"setAnnotationDisplay",
MAP_GRID_ANNOTATION_DISPLAY_HIDE_ALL,
MAP_GRID_BORDER_LEFT,
)
self._safe_grid_call(
frame_annotation_grid,
"setAnnotationDisplay",
MAP_GRID_ANNOTATION_DISPLAY_HIDE_ALL,
MAP_GRID_BORDER_TOP,
)
self._safe_grid_call(
frame_annotation_grid,
"setAnnotationDisplay",
MAP_GRID_ANNOTATION_DISPLAY_SHOW_Y,
MAP_GRID_BORDER_RIGHT,
)
self._safe_grid_call(
frame_annotation_grid,
"setAnnotationDisplay",
MAP_GRID_ANNOTATION_DISPLAY_SHOW_X,
MAP_GRID_BORDER_BOTTOM,
)
self._safe_grid_call(
frame_annotation_grid,
"setAnnotationPosition",
MAP_GRID_ANNOTATION_POSITION_INSIDE_FRAME,
MAP_GRID_BORDER_RIGHT,
)
self._safe_grid_call(
frame_annotation_grid,
"setAnnotationPosition",
MAP_GRID_ANNOTATION_POSITION_INSIDE_FRAME,
MAP_GRID_BORDER_BOTTOM,
)
self._apply_grid_annotation_text_style(frame_annotation_grid)
add_grid(frame_annotation_grid)
update_bounding_rect = getattr(hauptkarte, "updateBoundingRect", None)
if callable(update_bounding_rect):
try:
update_bounding_rect()
except Exception:
pass
refresh = getattr(hauptkarte, "refresh", None)
if callable(refresh):
try:
refresh()
except Exception:
pass
def _apply_grid_common_settings(self, grid: Any) -> None:
"""Test ausgesetzt, zunächst Refactor"""
self._safe_grid_call(grid, "setEnabled", True)
self._safe_grid_call(grid, "setUnits", MAP_GRID_UNIT_MAP_UNIT)
self._safe_grid_call(grid, "setIntervalX", self._GRID_INTERVAL_MAP_UNITS)
self._safe_grid_call(grid, "setIntervalY", self._GRID_INTERVAL_MAP_UNITS)
def _safe_grid_call(self, grid: Any, method_name: str, *args: Any) -> None:
"""Test ausgesetzt, zunächst Refactor"""
if any(arg is None for arg in args):
return
method = getattr(grid, method_name, None)
if not callable(method):
return
try:
method(*args)
except Exception:
pass
def _apply_grid_annotation_text_style(self, grid: Any) -> None:
"""Test ausgesetzt, zunächst Refactor"""
if QgsTextFormat is object:
self._safe_grid_call(
grid,
"setAnnotationTextFormat",
{
"font_family": "Arial",
"font_size": 10,
"background": {
"enabled": True,
"shape": "rectangle",
"size_type": "buffer",
"buffer_mm": 1.0,
"fill_color": (255, 255, 255),
},
},
)
return
try:
text_format = QgsTextFormat()
text_format.setFont(QFont("Arial", 10))
if TEXT_RENDER_UNIT_POINTS is not None:
set_size_unit = getattr(text_format, "setSizeUnit", None)
if callable(set_size_unit):
set_size_unit(TEXT_RENDER_UNIT_POINTS)
set_size = getattr(text_format, "setSize", None)
if callable(set_size):
set_size(10)
if QgsTextBackgroundSettings is not object:
try:
qt_core = importlib.import_module("qgis.PyQt.QtCore")
qt_gui = importlib.import_module("qgis.PyQt.QtGui")
QSizeF = getattr(qt_core, "QSizeF", None)
QPointF = getattr(qt_core, "QPointF", None)
QColor = getattr(qt_gui, "QColor", None)
except Exception:
QSizeF = None
QPointF = None
QColor = None
background = QgsTextBackgroundSettings()
set_enabled = getattr(background, "setEnabled", None)
if callable(set_enabled):
set_enabled(True)
set_type = getattr(background, "setType", None)
if callable(set_type) and TEXT_BACKGROUND_SHAPE_RECTANGLE is not None:
set_type(TEXT_BACKGROUND_SHAPE_RECTANGLE)
set_size_type = getattr(background, "setSizeType", None)
if callable(set_size_type) and TEXT_BACKGROUND_SIZE_BUFFER is not None:
set_size_type(TEXT_BACKGROUND_SIZE_BUFFER)
#steuert die Größe des Hintergrunds, wenn die Größe auf "Puffer" eingestellt ist
set_size = getattr(background, "setSize", None)
if callable(set_size) and QSizeF is not None:
set_size(QSizeF(0.0, 0.0))
set_size_unit = getattr(background, "setSizeUnit", None)
if callable(set_size_unit) and TEXT_RENDER_UNIT_MILLIMETERS is not None:
set_size_unit(TEXT_RENDER_UNIT_MILLIMETERS)
set_offset = getattr(background, "setOffset", None)
if callable(set_offset):
try:
if QPointF is not None:
set_offset(QPointF(0.0, -1.0))
elif QSizeF is not None:
set_offset(QSizeF(0.0, -1.0))
else:
set_offset((0.0, -1.0))
except Exception:
set_offset_x = getattr(background, "setOffsetX", None)
set_offset_y = getattr(background, "setOffsetY", None)
if callable(set_offset_x) and callable(set_offset_y):
try:
set_offset_x(0.0)
set_offset_y(-1.0)
except Exception:
pass
set_offset_unit = getattr(background, "setOffsetUnit", None)
if callable(set_offset_unit) and TEXT_RENDER_UNIT_MILLIMETERS is not None:
set_offset_unit(TEXT_RENDER_UNIT_MILLIMETERS)
set_fill_color = getattr(background, "setFillColor", None)
if callable(set_fill_color) and QColor is not None:
set_fill_color(QColor(255, 255, 255))
set_background = getattr(text_format, "setBackground", None)
if callable(set_background):
set_background(background)
self._safe_grid_call(grid, "setAnnotationTextFormat", text_format)
except Exception:
pass
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:
"""Test ausgesetzt, zunächst Refactor"""
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,
kartenname_auswahl: str = "",
thema: str = "",
fit_extent_to_map: bool = True,
basemap_legende: bool = False,
) -> Any:
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:
hauptkarte.setExtent(fitted_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, 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(
kartenfenster_untere_rechte_ecke_mm,
hauptkarte,
map_top_mm,
map_height_mm,
kartenname_auswahl,
)
if basemap_legende:
self._erzeuge_basemap_legende(layout)
self._ordne_kartenrand_an(layout)
layout_manager.addLayout(layout)
self._current_layout = None
open_layout_designer(layout)
return layout
def _ordne_kartenrand_an(self, layout: Any) -> None:
"""Ordnet Legenden, Quellenangabe und Kartenschild überlappungsfrei an (Basis #93).
Rand ggf. breiter (bis zu drei Legendenspalten) und Seite ggf. höher; die Karte bleibt.
Ein Fehler dabei darf das Anlegen der Vorlage nicht verhindern; er wird als Warnung gemeldet.
"""
from sn_basis.functions.message_wrapper import warning
from sn_basis.modules.print_kartenrand import ordne_kartenrand_an
try:
ordne_kartenrand_an(layout)
except Exception as fehler:
warning("Vorlage anlegen", f"Der Kartenrand konnte nicht angeordnet werden: {fehler}")
def _erzeuge_basemap_legende(self, layout: Any) -> None:
"""Kennzeichnet das Layout und erzeugt die Basemap-Legende (Basis #88).
Ein Fehler bei der Basemap-Legende darf das Anlegen der Vorlage nicht verhindern; er wird
als Warnung gemeldet.
"""
from sn_basis.functions.message_wrapper import warning
from sn_basis.modules.basemap_legende import BasemapLegende
from sn_basis.modules.basemap_legende_layout import markiere_layout
markiere_layout(layout)
try:
ergebnis = BasemapLegende(self.project).aktualisiere(layout)
except Exception as fehler:
warning("Basemap-Legende", f"Die Basemap-Legende konnte nicht erzeugt werden: {fehler}")
return
if ergebnis.get("status") == "keine_eintraege":
warning("Basemap-Legende", ergebnis.get("meldung", ""))