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>
1128 lines
40 KiB
Python
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", ""))
|