Compare commits

..
Author SHA1 Message Date
release-bot e11f84e89b Release 26.5.1-unstable 2026-05-06 08:40:38 +00:00
Daniel 171ebcbe4f Merge pull request 'Verfahrensgebiet-Prüfung als Knickpunkte' (#46) from feature/grenzpunkte_laden into unstable
Release Plugin / release (push) Successful in 3s
Reviewed-on: #46
2026-05-06 10:39:54 +02:00
Daniel b7dfac68f1 Verfahrensgebiet-Prüfung als Knickpunkte 2026-05-05 19:47:25 +02:00
release-bot 62e9bcee1e Release 26.4.8-unstable 2026-04-24 04:52:28 +00:00
Daniel e9d9a2294d Merge pull request 'verfahrensgebiet aus Flst' (#35) from verfahrensgebiet_aus_flst into unstable
Release Plugin / release (push) Successful in 4s
Reviewed-on: #35
2026-04-23 22:30:12 +02:00
Daniel c8fde119b4 boundary-Unterscheidung für QGIS 3/4 2026-04-23 22:23:50 +02:00
Daniel 692f690fea verfahrensgebiet aus Flst 2026-04-22 14:53:34 +02:00
12 changed files with 387 additions and 468 deletions
+4
View File
@@ -1,4 +1,8 @@
---
Version 26.5.1-unstable:
---
Version 26.4.8-unstable:
---
Version 26.4.7-unstable:
---
Version 26.4.6-unstable:
-87
View File
@@ -1,87 +0,0 @@
{
"template_name": "baufreigabe",
"description": "Baufreigabe-Layout mit 80mm Kartenschild (DIN A3/A4 Querformat)",
"kartenschild_width_mm": 80.0,
"kartenschild_height_mm": 58.0,
"kartenschild_line_offsets_y": [-10.0, -20.0, -30.0, -46.0],
"labels": [
{
"id": "Herausgeber",
"object_name": "Herausgeber",
"text": "Teilnehmergemeinschaft [%@sn_bezeichnung %] [%@sn_name %]",
"x_mm": 14.0,
"y_mm": -48.0,
"width_mm": 74.0,
"height_mm": 8.0,
"format": "Format_1",
"html_mode": false,
"expression_enabled": true,
"v_align": "bottom"
},
{
"id": "Bezeichnung_Flurbereinigung",
"object_name": "Bezeichnung_Flurbereinigung",
"text": "[% @sn_bezeichnung %]",
"x_mm": 14.0,
"y_mm": -41.0,
"width_mm": 74.0,
"height_mm": 4.0,
"format": "Format_1",
"html_mode": false,
"expression_enabled": true,
"v_align": "top"
},
{
"id": "Name_Flurbereinigung",
"object_name": "Name_Flurbereinigung",
"text": "[% @sn_name %]",
"x_mm": 14.0,
"y_mm": -32.0,
"width_mm": 74.0,
"height_mm": 5.0,
"format": "Format_2",
"html_mode": false,
"expression_enabled": true,
"v_align": "top"
},
{
"id": "Kartenname",
"object_name": "Kartenname",
"text": "[% @sn_kartenname %]",
"x_mm": 14.0,
"y_mm": -23.0,
"width_mm": 74.0,
"height_mm": 6.0,
"format": "Format_3",
"html_mode": false,
"expression_enabled": true,
"v_align": "top"
},
{
"id": "Ausgabedatum",
"object_name": "Ausgabedatum",
"text": "Ausgabedatum: [%format_date(now(), 'dd.MM.yyyy')%]",
"x_mm": 14.0,
"y_mm": -13.0,
"width_mm": 74.0,
"height_mm": 4.0,
"format": "Format_1",
"html_mode": false,
"expression_enabled": true,
"v_align": "top"
},
{
"id": "Massstab",
"object_name": "Massstab",
"text": "Ma\u00dfstab: 1 : [%round(map_get(item_variables('Hauptkarte'),'map_scale'),0)%]",
"x_mm": 14.0,
"y_mm": -3.0,
"width_mm": 74.0,
"height_mm": 4.0,
"format": "Format_1",
"html_mode": false,
"expression_enabled": true,
"v_align": "top"
}
]
}
-153
View File
@@ -1,153 +0,0 @@
{
"template_name": "standard",
"description": "Standard-Layout mit 180mm Kartenschild fuer §38/§41 FlurbG Karten",
"kartenschild_width_mm": 180.0,
"kartenschild_height_mm": 92.0,
"kartenschild_line_offsets_y": [-10.0, -20.0, -30.0, -46.0, -62.0, -82.0],
"labels": [
{
"id": "Herausgeber",
"object_name": "Herausgeber",
"text": "Teilnehmergemeinschaft [%@sn_bezeichnung %] [%@sn_name %]",
"text_if_kartenname_38": "[%@sn_landkreis_user%] [%@sn_behoerde%]",
"x_mm": 16.0,
"y_mm": -84.0,
"width_mm": 171.0,
"height_mm": 4.0,
"format": "Format_1",
"html_mode": false,
"expression_enabled": true,
"v_align": "top"
},
{
"id": "Bezeichnung_Flurbereinigung",
"object_name": "Bezeichnung_Flurbereinigung",
"text": "[% @sn_bezeichnung %]",
"x_mm": 16.0,
"y_mm": -75.0,
"width_mm": 171.0,
"height_mm": 4.0,
"format": "Format_1",
"html_mode": false,
"expression_enabled": true,
"v_align": "top"
},
{
"id": "Name_Flurbereinigung",
"object_name": "Name_Flurbereinigung",
"text": "[% @sn_name %]",
"x_mm": 16.0,
"y_mm": -64.0,
"width_mm": 171.0,
"height_mm": 5.0,
"format": "Format_2",
"html_mode": false,
"expression_enabled": true,
"v_align": "top"
},
{
"id": "Stadt_Gemeinde",
"object_name": "Stadt_Gemeinde",
"text": "[% @sn_gemeinden %]",
"x_mm": 16.0,
"y_mm": -56.0,
"width_mm": 171.0,
"height_mm": 4.0,
"format": "Format_1",
"html_mode": false,
"expression_enabled": true,
"v_align": "top"
},
{
"id": "Landkreis",
"object_name": "Landkreis",
"text": "[% @sn_landkreise_proj %]",
"x_mm": 16.0,
"y_mm": -48.0,
"width_mm": 171.0,
"height_mm": 4.0,
"format": "Format_1",
"html_mode": false,
"expression_enabled": true,
"v_align": "top"
},
{
"id": "Kartenname",
"object_name": "Kartenname",
"text": "[% @sn_kartenname %]",
"x_mm": 16.0,
"y_mm": -32.0,
"width_mm": 137.0,
"height_mm": 12.0,
"format": "Format_3",
"html_mode": false,
"expression_enabled": true,
"v_align": "top"
},
{
"id": "Gefertigt",
"object_name": "Gefertigt",
"text": "Gefertigt: ",
"x_mm": 16.0,
"y_mm": -23.0,
"width_mm": 81.0,
"height_mm": 4.0,
"format": "Format_1",
"html_mode": false,
"expression_enabled": true,
"v_align": "top"
},
{
"id": "Gepr\u00fcft",
"object_name": "Gepr\u00fcft",
"text": "Geprueft: ",
"x_mm": 101.0,
"y_mm": -23.0,
"width_mm": 81.0,
"height_mm": 4.0,
"format": "Format_1",
"html_mode": false,
"expression_enabled": true,
"v_align": "top"
},
{
"id": "Massstab",
"object_name": "Massstab",
"text": "Ma\u00dfstab: 1 : [%round(map_get(item_variables('Hauptkarte'),'map_scale'),0)%]",
"x_mm": 16.0,
"y_mm": -13.0,
"width_mm": 81.0,
"height_mm": 4.0,
"format": "Format_1",
"html_mode": false,
"expression_enabled": true,
"v_align": "top"
},
{
"id": "Ausgabedatum",
"object_name": "Ausgabedatum",
"text": "Ausgabedatum: [%format_date(now(), 'dd.MM.yyyy')%]",
"x_mm": 101.0,
"y_mm": -13.0,
"width_mm": 81.0,
"height_mm": 4.0,
"format": "Format_1",
"html_mode": false,
"expression_enabled": true,
"v_align": "top"
},
{
"id": "Aktenzeichen",
"object_name": "Aktenzeichen",
"text": "Aktenzeichen: XXX",
"x_mm": 16.0,
"y_mm": -3.0,
"width_mm": 171.0,
"height_mm": 4.0,
"format": "Format_1",
"html_mode": false,
"expression_enabled": true,
"v_align": "top"
}
]
}
-17
View File
@@ -1,17 +0,0 @@
{
"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"
}
}
+1 -1
View File
@@ -20,7 +20,7 @@ from .ly_metadata_wrapper import (
is_layer_editable,
)
from .ly_style_wrapper import apply_style
from .dialog_wrapper import ask_yes_no, ask_overwrite_append_cancel_custom, ask_ergaenzen_entfernen_ersetzen_abbrechen
from .dialog_wrapper import ask_yes_no, ask_overwrite_append_cancel_custom, ask_ergaenzen_entfernen_ersetzen_abbrechen, ask_detailpruefung_oder_vg_laden
from .message_wrapper import (
_get_message_bar,
+50
View File
@@ -182,6 +182,56 @@ def ask_ergaenzen_entfernen_ersetzen_abbrechen(
return "abbrechen"
def ask_detailpruefung_oder_vg_laden(
parent: Any,
title: str,
message: str,
) -> Literal["detailpruefung", "als_vg_laden", "abbrechen"]:
"""Zeigt Dialog mit drei Optionen nach erkannter Flächenabweichung.
Optionen:
- **Detailprüfung starten**: Knickpunktbasierte Prüfung des BROR-Umrings
- **Bodenordnungsobjekt als VG laden**: BROR direkt als Verfahrensgebiet übernehmen
- **Abbrechen**: Vorgang beenden ohne weitere Aktion
Parameters
----------
parent :
Eltern-Widget oder None.
title : str
Dialog-Titel.
message : str
Hauptmeldung mit Flächen-Informationen.
Returns
-------
Literal["detailpruefung", "als_vg_laden", "abbrechen"]
Genaue Entscheidung des Nutzers.
"""
if QT_VERSION == 0: # Mock-Modus
print(f"Mock-Modus: ask_detailpruefung_oder_vg_laden('{title}') → abbrechen")
return "abbrechen"
msg = QMessageBox(parent)
msg.setIcon(ICON_QUESTION)
msg.setWindowTitle(title)
msg.setText(message)
detail_btn = msg.addButton("Detailprüfung starten", QMessageBox.ButtonRole.AcceptRole)
vg_btn = msg.addButton("Bodenordnungsobjekt als VG laden", QMessageBox.ButtonRole.ActionRole)
msg.addButton("Abbrechen", QMessageBox.ButtonRole.RejectRole)
exec_dialog(msg)
clicked = msg.clickedButton()
if clicked == detail_btn:
return "detailpruefung"
elif clicked == vg_btn:
return "als_vg_laden"
else:
return "abbrechen"
class ProgressDialog:
def __init__(self, total: int, title: str = "Fortschritt", label: str = "Verarbeite..."):
self.total = max(total, 1)
+79
View File
@@ -24,6 +24,7 @@ QgsFeature: Type[Any]
QgsField: Type[Any]
QgsGeometry: Type[Any]
QgsFeatureRequest: Type[Any]
QgsRectangle: Type[Any]
QgsCoordinateTransform: Type[Any]
QgsCoordinateReferenceSystem: Type[Any]
QgsPrintLayout: Type[Any]
@@ -76,6 +77,7 @@ try:
QgsField as _QgsField,
QgsGeometry as _QgsGeometry,
QgsFeatureRequest as _QgsFeatureRequest,
QgsRectangle as _QgsRectangle,
QgsCoordinateTransform as _QgsCoordinateTransform,
QgsCoordinateReferenceSystem as _QgsCoordinateReferenceSystem,
QgsPrintLayout as _QgsPrintLayout,
@@ -102,6 +104,7 @@ try:
QgsField = _QgsField
QgsGeometry = _QgsGeometry
QgsFeatureRequest = _QgsFeatureRequest
QgsRectangle = _QgsRectangle
QgsCoordinateTransform = _QgsCoordinateTransform
QgsCoordinateReferenceSystem = _QgsCoordinateReferenceSystem
QgsPrintLayout = _QgsPrintLayout
@@ -951,3 +954,79 @@ def layer_exists_in_gpkg(gpkg_path: str, layer_name: str) -> bool:
pass
return False
# ---------------------------------------------------------
# QGIS-3/4-kompatible Geometrie-Hilfsfunktionen
# ---------------------------------------------------------
def polygon_zu_linie(geom: Any) -> Optional[Any]:
"""Konvertiert ein Polygon/MultiPolygon zur Umringslinie (QGIS 3 + 4).
In QGIS 3.x existiert ``QgsGeometry.boundary()``, in QGIS 4.x wurde sie
entfernt. Diese Funktion kapselt die Versionsunterschiede und stellt dem
Fachplugin eine stabile API bereit.
Strategie (dreistufig):
1. ``geom.boundary()`` funktioniert in QGIS 3.x
2. ``geom.convertToType(1, False)`` ``1`` = LineGeometry-Wert,
identisch in ``QgsWkbTypes.LineGeometry`` (3.x) und
``Qgis.GeometryType.Line`` (4.x)
3. Manuelle Ring-Extraktion via ``asPolygon()`` / ``asMultiPolygon()``
+ ``QgsGeometry.fromPolylineXY()``
Parameters
----------
geom :
Polygon- oder MultiPolygon-``QgsGeometry``.
Returns
-------
Optional[QgsGeometry]
Liniengeometrie (Umring) oder ``None`` bei Fehler / leerer Eingabe.
"""
if geom is None:
return None
try:
if geom.isEmpty():
return None
except Exception:
return None
# Stufe 1: QGIS 3.x .boundary() existiert und liefert alle Ringe korrekt
if hasattr(geom, "boundary"):
try:
result = geom.boundary()
if result is not None and not result.isEmpty():
return result
except Exception:
pass
# Stufe 2: Manuelle Ring-Extraktion (primärer QGIS-4.x-Pfad).
# convertToType(1, False) würde bei MultiPolygon nur den ersten Außenring
# zurückgeben und alle weiteren Polygone sowie Innenringe verwerfen → daher
# direkt die vollständige manuelle Extraktion als nächste Stufe.
lines = []
try:
if geom.isMultipart():
for polygon in geom.asMultiPolygon():
for ring in polygon:
lines.append(QgsGeometry.fromPolylineXY(ring))
else:
for ring in geom.asPolygon():
lines.append(QgsGeometry.fromPolylineXY(ring))
except Exception:
pass
if lines:
return QgsGeometry.unaryUnion(lines)
# Stufe 3: Letzter Fallback convertToType mit destMultipart=True, damit
# zumindest alle Außenringe eines MultiPolygons erfasst werden.
try:
result = geom.convertToType(1, True)
if result is not None and not result.isEmpty():
return result
except Exception:
pass
return None
+1 -1
View File
@@ -1,5 +1,5 @@
[general]
version=26.4.7-unstable
version=26.5.1-unstable
name=LNO Sachsen | Plugin Basisfunktionen
description=Plugin mit Basisfunktionen
author=Daniel Helbig, Michael Otto
+61 -146
View File
@@ -4,9 +4,7 @@ sn_basis/modules/print_layout.py - Aufbau von Einzelblatt-Drucklayouts.
from __future__ import annotations
import importlib
import json
import math
from pathlib import Path
from typing import Any
from sn_basis.functions.qt_wrapper import QFont, Qt
@@ -47,19 +45,13 @@ MM = QgsUnitTypes.LayoutMillimeters
class PrintLayout:
"""Erzeugt ein QGIS-Einzelblatt-Layout fuer den Druck.
Der optionale Parameter config_name waehlt ein Schriftfeld-Template aus
config/label_configs/{config_name}.json. Wird kein Name angegeben, wird
"standard" verwendet. Neue Templates koennen ohne Code-Aenderungen durch
Erstellen einer neuen JSON-Datei hinzugefuegt werden.
"""
"""Erzeugt ein QGIS-Einzelblatt-Layout fuer den Druck."""
_FRAME_STROKE_WIDTH_MM = 0.5
_LEGEND_WIDTH_MM = 180.0
_GRID_INTERVAL_MAP_UNITS = 500.0
# Eingebaute Fallback-Formate fuer den Fall, dass text_formats.json fehlt
_TEXT_FORMAT_TEMPLATES_DEFAULT = {
_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"},
@@ -99,51 +91,9 @@ class PrintLayout:
"mit teilweise angepasster Signatur<br>"
)
def __init__(self, project: Any | None = None, config_name: str = "standard") -> None:
def __init__(self, project: Any | None = None) -> None:
self.project = project or QgsProject.instance()
self._current_layout: Any | None = None
self.config_name = config_name
self._label_config = self._load_label_config(config_name)
self._TEXT_FORMAT_TEMPLATES = self._load_text_formats()
self._LEGEND_WIDTH_MM = float(
self._label_config.get("kartenschild_width_mm", 180.0)
)
@staticmethod
def _get_config_dir() -> Path:
return Path(__file__).parent.parent / "config"
def _load_label_config(self, config_name: str) -> dict:
"""Laedt Schriftfeld-Konfiguration aus config/label_configs/{config_name}.json.
Raises:
FileNotFoundError: Wenn die Konfigurationsdatei nicht existiert.
ValueError: Wenn die JSON-Datei nicht geparst werden kann.
"""
config_file = self._get_config_dir() / "label_configs" / f"{config_name}.json"
if not config_file.exists():
raise FileNotFoundError(
f"Label-Config '{config_name}' nicht gefunden: {config_file}"
)
try:
with open(config_file, encoding="utf-8") as fh:
return json.load(fh)
except json.JSONDecodeError as exc:
raise ValueError(f"JSON-Fehler in {config_file}: {exc}") from exc
def _load_text_formats(self) -> dict:
"""Laedt zentrale Schriftformat-Definitionen aus config/text_formats.json.
Faellt auf eingebaute Standardwerte zurueck, wenn die Datei fehlt.
"""
formats_file = self._get_config_dir() / "text_formats.json"
if not formats_file.exists():
return dict(self._TEXT_FORMAT_TEMPLATES_DEFAULT)
try:
with open(formats_file, encoding="utf-8") as fh:
return json.load(fh)
except (json.JSONDecodeError, OSError):
return dict(self._TEXT_FORMAT_TEMPLATES_DEFAULT)
def _create_map_item(
self,
@@ -297,51 +247,17 @@ class PrintLayout:
map_height_mm: float,
kartenname_auswahl: str = "",
) -> None:
"""Richtet Legende, Quellenangabe und Kartenschild ein.
Delegiert an _setup_layout_objects_from_config() unter Aufloesung
des optionalen kartenname_auswahl-Parameters fuer §38-spezifische Texte.
"""
self._setup_layout_objects_from_config(
kartenfenster_untere_rechte_ecke_mm,
hauptkarte,
map_top_mm,
map_height_mm,
kartenname_auswahl=kartenname_auswahl,
)
def _setup_layout_objects_from_config(
self,
kartenfenster_untere_rechte_ecke_mm: tuple,
hauptkarte: Any,
map_top_mm: float,
map_height_mm: float,
kartenname_auswahl: str = "",
) -> None:
"""Erstellt Legende, Quellenangabe, Kartenschild und alle Labels aus der JSON-Config.
Die Label-Definitionen werden vollstaendig aus self._label_config geladen.
Einzige Ausnahme: Das Feld "text_if_kartenname_38" in einem Label-Eintrag
erlaubt einen alternativen Text speziell fuer kartenname_auswahl == '§38'.
"""
if self._current_layout is None:
raise RuntimeError("Kein Layout aktiv.")
kartenschild_width_mm = float(self._label_config.get("kartenschild_width_mm", 180.0))
kartenschild_height_mm = float(self._label_config.get("kartenschild_height_mm", 92.0))
kartenschild_line_offsets_y: list[float] = [
float(v) for v in self._label_config.get("kartenschild_line_offsets_y", [-10.0, -20.0, -30.0, -46.0, -62.0, -82.0])
]
def _coords(rel_x_mm: float, rel_y_mm: float) -> tuple[float, float]:
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
legende_x = kartenfenster_untere_rechte_ecke_mm[0] + 10.0
legende_height_mm = 50.0 # Klein anfangen, fitBoxToContents() wird es vergrößern
legende_height_mm = max(map_height_mm - 122.0, 20.0)
legende_y = map_top_mm
legende = self._create_legend_item(
self._current_layout,
@@ -349,36 +265,36 @@ class PrintLayout:
hauptkarte,
legende_x,
legende_y,
width_mm=kartenschild_width_mm,
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=kartenschild_width_mm,
width_limit_mm=self._LEGEND_WIDTH_MM,
column_count=1,
equal_column_width=False,
)
quellenangabe_x, quellenangabe_y = _coords(10.0, -102.0)
quellenangabe_obere_y = quellenangabe_y - 20.0
quellenangabe_x, quellenangabe_y = _coords_rel_untere_rechte_ecke(10.0, -102.0)
quellenangabe_hoehe_mm = 20.0
quellenangabe_obere_y = quellenangabe_y - quellenangabe_hoehe_mm
legende_untere_y = legende_y + legende_hoehe_mm
if legende_untere_y > quellenangabe_obere_y:
legende_hoehe_mm = self._configure_legend_size(
self._configure_legend_size(
legende,
width_limit_mm=kartenschild_width_mm,
width_limit_mm=self._LEGEND_WIDTH_MM,
column_count=2,
equal_column_width=True,
)
# Quellenangabe
self.add_text_label(
self._QUELLENANGABE_TEXT,
quellenangabe_x,
quellenangabe_y,
width_mm=kartenschild_width_mm,
width_mm=self._LEGEND_WIDTH_MM,
height_mm=20.0,
text_format="Format_1",
html_mode=True,
@@ -386,52 +302,72 @@ class PrintLayout:
object_name="Quellenangabe",
)
# Kartenschild-Rahmen
kartenschild_x, kartenschild_y = _coords(10.0, 0.0)
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,
kartenschild_width_mm,
kartenschild_height_mm,
self._LEGEND_WIDTH_MM,
92.0,
stroke_width_mm=self._FRAME_STROKE_WIDTH_MM,
)
# Kartenschild-Trennlinien
kartenschild_line_x, _ = _coords(10.0, 0.0)
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(0.0, offset_y)
_, 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,
kartenschild_width_mm,
self._LEGEND_WIDTH_MM,
0.0,
stroke_width_mm=0.1,
)
# Labels aus Config
for label_def in self._label_config.get("labels", []):
# Alternativer Text fuer §38-Karten (falls in Config definiert)
if kartenname_auswahl == "§38" and "text_if_kartenname_38" in label_def:
text = label_def["text_if_kartenname_38"]
else:
text = label_def["text"]
if kartenname_auswahl == "§38":
herausgeber_text = "[%@sn_landkreis_user%] [%@sn_behoerde%]"
else:
herausgeber_text = "Teilnehmergemeinschaft [%@sn_bezeichnung %] [%@sn_name %]"
x, y = _coords(float(label_def["x_mm"]), float(label_def["y_mm"]))
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"),
("Geprueft: ", 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=float(label_def["width_mm"]),
height_mm=float(label_def["height_mm"]),
text_format=label_def.get("format", "Format_1"),
html_mode=bool(label_def.get("html_mode", False)),
expression_enabled=bool(label_def.get("expression_enabled", True)),
object_name=label_def.get("object_name") or label_def.get("id"),
v_align=label_def.get("v_align", "top"),
width_mm=w,
height_mm=h,
text_format=fmt,
expression_enabled=True,
object_name=obj_name,
v_align=v_align,
)
def _create_label_item(
@@ -725,14 +661,6 @@ class PrintLayout:
except Exception:
pass
# Deaktiviere Auto-Fit: Größe an Inhalt anpassen soll aus sein
set_resize_to_contents = getattr(legend, "setResizeToContents", None)
if callable(set_resize_to_contents):
try:
set_resize_to_contents(False)
except Exception:
pass
set_column_count = getattr(legend, "setColumnCount", None)
if callable(set_column_count):
try:
@@ -754,30 +682,17 @@ class PrintLayout:
except Exception:
pass
# Versuche fitBoxToContents() zuerst, dann Fallback zu adjustBoxSize()
fit_box_to_contents = getattr(legend, "fitBoxToContents", None)
if callable(fit_box_to_contents):
adjust_box_size = getattr(legend, "adjustBoxSize", None)
if callable(adjust_box_size):
try:
fit_box_to_contents()
adjust_box_size()
except Exception:
pass
else:
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
# Breite auf Kartenschild-Breite setzen, Höhe an Inhalt angepasst behalten
legend.attemptResize(
QgsLayoutSize(width_limit_mm, updated_height_mm, MM)
)
if callable(set_locked):
try:
set_locked(True)
+189 -11
View File
@@ -1,8 +1,9 @@
"""
sn_basis/modules/print_layout_baufreigabe.py - Layout-Erstellung fuer Baufreigabe-Vorlagen.
Baufreigabe-Layouts verwenden immer feste DIN A3/A4 Seitengroessen im Querformat.
Das Schriftfeld (80mm breit) wird ueber config/label_configs/baufreigabe.json definiert.
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.
@@ -11,7 +12,11 @@ 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,
@@ -27,19 +32,60 @@ MM = QgsUnitTypes.LayoutMillimeters
class PrintLayoutBaufreigabe(PrintLayout):
"""Erzeugt QGIS-Einzelblatt-Layouts mit fester DIN A3/A4 Querformat-Groesse.
Schriftfeld-Definition kommt aus config/label_configs/baufreigabe.json.
"""
"""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
def __init__(self, project: Any | None = None, config_name: str = "baufreigabe") -> None:
super().__init__(project, config_name=config_name)
_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,
@@ -64,10 +110,9 @@ class PrintLayoutBaufreigabe(PrintLayout):
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
atlas_driven: Atlas-Steuerung fuer die Hauptkarte aktivieren
Returns:
QgsPrintLayout-Instanz (wird im Layout-Designer geoeffnet)
QgsPrintLayout objektInstance (wird im Layout-Dialog geoeffnet)
Raises:
ValueError: Falls Name bereits existiert oder Format ungueltig ist
@@ -129,6 +174,7 @@ class PrintLayoutBaufreigabe(PrintLayout):
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):
@@ -201,7 +247,7 @@ class PrintLayoutBaufreigabe(PrintLayout):
self._configure_hauptkarte_grids(hauptkarte)
self._setup_layout_objects(
self._setup_layout_objects_baufreigabe(
kartenfenster_untere_rechte_ecke_mm,
hauptkarte,
map_top_mm,
@@ -213,6 +259,138 @@ class PrintLayoutBaufreigabe(PrintLayout):
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
+2 -5
View File
@@ -1166,7 +1166,6 @@ class PrintLogic:
zielgroesse: str,
formfaktor: bool,
druckbereich_auswahl: str,
schriftfeld_template: str = "",
) -> dict:
resolved_layer = layer or self._resolve_layer_from_project()
@@ -1271,9 +1270,8 @@ class PrintLogic:
try:
if kartenname_auswahl == KARTENNAME_BAUFREIGABE:
# Baufreigabe: feste DIN A3/A4 Querformat-Logik mit 80mm Kartenschild
config_to_use = schriftfeld_template or "baufreigabe"
din_format = "DIN A3" if auto_page_w >= 420.0 else "DIN A4"
PrintLayoutBaufreigabe(config_name=config_to_use).create_baufreigabe_layout(
PrintLayoutBaufreigabe().create_baufreigabe_layout(
name=vorlage_name if vorlage_name != kartenname else "Baufreigabe",
din_format=din_format,
map_width_mm=auto_map_w,
@@ -1286,8 +1284,7 @@ class PrintLogic:
)
else:
# Standard-Layout: dynamische Groessenanpassung
config_to_use = schriftfeld_template or "standard"
PrintLayout(config_name=config_to_use).create_single_page_layout(
PrintLayout().create_single_page_layout(
name=vorlage_name,
page_width_mm=auto_page_w,
page_height_mm=auto_page_h,
-47
View File
@@ -3,7 +3,6 @@ sn_basis/ui/tabs/print_tab.py - Zentraler Druck-Tab fuer sn-Plugins.
"""
from __future__ import annotations
from pathlib import Path
from typing import Any, Optional
from sn_basis.functions.qt_wrapper import (
@@ -86,7 +85,6 @@ class PrintTab(QWidget):
self._atlas_erzeugen_cb: Optional[Any] = None
self._atlas_button: Optional[Any] = None
self._atlas_content: Optional[Any] = None
self._schriftfeld_combo: Optional[Any] = None
self._build_ui()
self._restore_state()
@@ -258,16 +256,6 @@ class PrintTab(QWidget):
self._atlas_content = atlas_content
main_layout.addWidget(atlas_content)
schriftfeld_label = QLabel("Schriftfeld-Template")
schriftfeld_label.setStyleSheet("font-weight: bold; margin-top: 6px;")
main_layout.addWidget(schriftfeld_label)
schriftfeld_combo = QComboBox(self)
schriftfeld_combo.addItems(self._get_available_templates())
schriftfeld_combo.currentTextChanged.connect(self._on_schriftfeld_changed)
self._schriftfeld_combo = schriftfeld_combo
main_layout.addWidget(schriftfeld_combo)
btn_vorlage_erstellen = QPushButton("Vorlage erstellen", self)
btn_vorlage_erstellen.clicked.connect(self._on_vorlage_erstellen)
self._btn_vorlage_erstellen = btn_vorlage_erstellen
@@ -341,14 +329,6 @@ class PrintTab(QWidget):
self._atlas_erzeugen_cb.setChecked(atlas_checked)
self._on_atlas_erzeugen_changed(1 if atlas_checked else 0)
if self._schriftfeld_combo:
saved_schriftfeld = get_variable("tab_b_schriftfeld_template", scope="project")
available_templates = self._get_available_templates()
if saved_schriftfeld in available_templates:
self._schriftfeld_combo.setCurrentText(saved_schriftfeld)
else:
self._schriftfeld_combo.setCurrentText("standard")
self._restore_atlas_layer_selection()
def _on_kartenname_changed(self, value: str) -> None:
@@ -456,7 +436,6 @@ class PrintTab(QWidget):
zielgroesse=self._zielgroesse_combo.currentText() if self._zielgroesse_combo else DIN_STANDARD,
formfaktor=self._endlosrolle_cb.isChecked() if self._endlosrolle_cb else False,
druckbereich_auswahl=self._druckbereich_combo.currentText() if self._druckbereich_combo else DRUCKBEREICH_VERFAHRENSGEBIET,
schriftfeld_template=self._schriftfeld_combo.currentText() if self._schriftfeld_combo else "",
)
def _disconnect_project_signals(self) -> None:
@@ -727,29 +706,3 @@ class PrintTab(QWidget):
pass
self._atlas_layer_combo.blockSignals(False)
def _get_available_templates(self) -> list[str]:
"""Liest verfügbare Schriftfeld-Vorlagen aus dem config/label_configs Verzeichnis."""
config_dir = Path(__file__).parent.parent / "config" / "label_configs"
templates: list[str] = []
try:
if config_dir.exists() and config_dir.is_dir():
for json_file in config_dir.glob("*.json"):
template_name = json_file.stem
if template_name and template_name not in templates:
templates.append(template_name)
except Exception:
pass
if not templates:
templates = ["standard", "baufreigabe"]
elif "standard" not in templates:
templates.insert(0, "standard")
return sorted(templates)
def _on_schriftfeld_changed(self, template_name: str) -> None:
"""Handler wenn Schriftfeld-Template wechselt."""
set_variable("tab_b_schriftfeld_template", template_name, scope="project")