Compare commits
33
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9d9a2294d | ||
|
|
c8fde119b4 | ||
|
|
692f690fea | ||
|
|
94b957bb73 | ||
|
|
00023c632f | ||
|
|
fba89478a0 | ||
|
|
3c1d8fc4e5 | ||
|
|
44eee228a8 | ||
|
|
34e2ff4f66 | ||
|
|
91669b34d6 | ||
|
|
d2035139d9 | ||
|
|
30bfcc597d | ||
|
|
88761eab22 | ||
|
|
d762f2d94f | ||
|
|
9f7e81e8b2 | ||
|
|
b4433222f3 | ||
|
|
779a1ed63f | ||
|
|
4049090e92 | ||
|
|
4b842bf635 | ||
|
|
943aab7509 | ||
|
|
5564c06d5c | ||
|
|
5b4cbc3127 | ||
|
|
f0944aab99 | ||
|
|
9306028df8 | ||
|
|
abf5808117 | ||
|
|
7ad8ba054f | ||
|
|
0900c6d3b0 | ||
|
|
40c224959d | ||
|
|
9d949d158f | ||
|
|
dbccb5322f | ||
|
|
fef72b7e53 | ||
|
|
8e80ffb02e | ||
|
|
5dce7c4d96 |
+16
@@ -1,3 +1,19 @@
|
||||
"""
|
||||
sn_basis – QGIS-Plugin für Basis-Verwaltung und zentrale Koordination.
|
||||
Orchestriert die Navigation und initialisiert abhängige sn_-Plugins.
|
||||
"""
|
||||
|
||||
|
||||
def classFactory(iface):
|
||||
"""
|
||||
Factory-Funktion für QGIS.
|
||||
Wird von QGIS aufgerufen, um die Plugin-Instanz zu erstellen.
|
||||
|
||||
Args:
|
||||
iface: QGIS-Schnittstellenobjekt
|
||||
|
||||
Returns:
|
||||
BasisPlugin: Instanziiertes Plugin
|
||||
"""
|
||||
from .main import BasisPlugin
|
||||
return BasisPlugin(iface)
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 115 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 160 KiB |
@@ -1,188 +0,0 @@
|
||||
# Wrapper‑Architektur – Übersicht
|
||||
Die Wrapper‑Architektur von sn_basis bildet das Fundament für eine robuste, testbare und zukunftssichere QGIS‑Plugin‑Entwicklung.
|
||||
Sie kapselt sämtliche QGIS‑ und Qt‑Abhängigkeiten hinter klar definierten Schnittstellen und ermöglicht dadurch:
|
||||
|
||||
Mock‑fähige Unit‑Tests ohne QGIS
|
||||
|
||||
PyQt5/6‑Kompatibilität ohne Code‑Änderungen
|
||||
|
||||
saubere Trennung von UI, Logik und Infrastruktur
|
||||
|
||||
stabile APIs, die unabhängig von QGIS‑Versionen bleiben
|
||||
|
||||
klare Erweiterbarkeit für zukünftige Module und Plugins
|
||||
|
||||
Die Wrapper‑Schicht ist das zentrale Bindeglied zwischen der Plugin‑Logik und der QGIS‑/Qt‑Umgebung.
|
||||
|
||||
## Ziele der Wrapper‑Architektur
|
||||
🎯 1. Entkopplung von QGIS und Qt
|
||||
Alle direkten Importe wie from qgis.core import ... oder from qgis.PyQt.QtWidgets import ... verschwinden aus der Plugin‑Logik.
|
||||
Stattdessen werden sie über Wrapper‑Module abstrahiert.
|
||||
|
||||
🎯 2. Testbarkeit ohne QGIS
|
||||
Im Mock‑Modus liefern die Wrapper:
|
||||
|
||||
Dummy‑Objekte
|
||||
|
||||
simulierte Rückgabewerte
|
||||
|
||||
speicherbare Zustände (z. B. Variablen, Layer, Nachrichten)
|
||||
|
||||
Damit laufen Tests in jeder CI‑Umgebung.
|
||||
|
||||
🎯 3. Einheitliche API für alle Plugins
|
||||
Plugins greifen nicht mehr direkt auf QGIS zu, sondern nutzen:
|
||||
|
||||
Code
|
||||
sn_basis.functions.qgiscore_wrapper
|
||||
sn_basis.functions.qgisui_wrapper
|
||||
sn_basis.functions.qt_wrapper
|
||||
sn_basis.functions.variable_wrapper
|
||||
sn_basis.functions.message_wrapper
|
||||
sn_basis.functions.dialog_wrapper
|
||||
🎯 4. Zukunftssicherheit
|
||||
Ändert sich die QGIS‑ oder Qt‑API, wird nur der Wrapper angepasst, nicht jedes Plugin.
|
||||
|
||||
## Architekturüberblick
|
||||
Die Wrapper‑Schicht besteht aus mehreren Modulen, die jeweils einen klar abgegrenzten Verantwortungsbereich haben.
|
||||
|
||||
### 1. qt_wrapper – Qt‑Abstraktion
|
||||
Kapselt alle Qt‑Widgets, Dialoge und Konstanten:
|
||||
|
||||
QWidget, QDialog, QMessageBox, QToolBar, QMenu, …
|
||||
|
||||
Layouts, Buttons, Labels, LineEdits
|
||||
|
||||
Qt‑Konstanten wie YES, NO, Dock‑Areas
|
||||
|
||||
Mock‑Modus:
|
||||
Stellt Dummy‑Widgets bereit, die keine UI öffnen.
|
||||
|
||||
### 2. qgiscore_wrapper – QGIS‑Core‑Abstraktion
|
||||
Abstraktion für:
|
||||
|
||||
QgsProject
|
||||
|
||||
Layer‑Zugriff
|
||||
|
||||
Projekt‑Metadaten
|
||||
|
||||
Pfade, CRS, Feature‑Zugriff
|
||||
|
||||
Mock‑Modus:
|
||||
Simuliert ein Projekt und Layer‑Container.
|
||||
|
||||
### 3. qgisui_wrapper – QGIS‑UI‑Abstraktion
|
||||
Kapselt UI‑bezogene QGIS‑Funktionen:
|
||||
|
||||
Zugriff auf iface
|
||||
|
||||
Dock‑Management
|
||||
|
||||
Menü‑ und Toolbar‑Integration
|
||||
|
||||
Hauptfenster‑Zugriff
|
||||
|
||||
Mock‑Modus:
|
||||
Stellt ein Dummy‑Interface bereit.
|
||||
|
||||
### 4. variable_wrapper – QGIS‑Variablen
|
||||
Abstraktion für:
|
||||
|
||||
Projektvariablen (projectScope)
|
||||
|
||||
globale Variablen (globalScope)
|
||||
|
||||
Mock‑Speicher für Tests
|
||||
|
||||
Vorteile:
|
||||
|
||||
keine QGIS‑Abhängigkeit in der Logik
|
||||
|
||||
testbare Variablenverwaltung
|
||||
|
||||
einheitliches API
|
||||
|
||||
### 5. message_wrapper – Meldungen & Logging
|
||||
Einheitliche Schnittstelle für:
|
||||
|
||||
Fehlermeldungen
|
||||
|
||||
Warnungen
|
||||
|
||||
Info‑Meldungen
|
||||
|
||||
Logging
|
||||
|
||||
Mock‑Modus:
|
||||
Speichert Nachrichten statt sie an QGIS zu senden.
|
||||
|
||||
### 6. dialog_wrapper – Benutzer‑Dialoge
|
||||
Abstraktion für:
|
||||
|
||||
Ja/Nein‑Dialoge
|
||||
|
||||
spätere Erweiterungen (Eingabedialoge, Dateidialoge, etc.)
|
||||
|
||||
Mock‑Modus:
|
||||
Gibt Default‑Werte zurück, öffnet keine UI.
|
||||
|
||||
### 7. DockManager & Navigation
|
||||
Diese Module nutzen die Wrapper‑Schicht, um:
|
||||
|
||||
DockWidgets sicher zu verwalten
|
||||
|
||||
Toolbars und Menüs zu erzeugen
|
||||
|
||||
Reload‑sichere UI‑Strukturen aufzubauen
|
||||
|
||||
Sie sind keine Wrapper, sondern Wrapper‑Konsumenten.
|
||||
|
||||
## Designprinzipien
|
||||
🧱 1. Single Source of Truth
|
||||
Jede QGIS‑ oder Qt‑Funktionalität wird nur an einer Stelle implementiert.
|
||||
|
||||
🔄 2. Austauschbarkeit
|
||||
Mock‑Modus und Echtmodus sind vollständig austauschbar.
|
||||
|
||||
🧪 3. Testbarkeit
|
||||
Jede Funktion kann ohne QGIS getestet werden.
|
||||
|
||||
🧼 4. Saubere Trennung
|
||||
UI → qt_wrapper
|
||||
|
||||
QGIS‑Core → qgiscore_wrapper
|
||||
|
||||
QGIS‑UI → qgisui_wrapper
|
||||
|
||||
Logik → settings_logic, layer_logic, prüfmanager, …
|
||||
|
||||
🔌 5. Erweiterbarkeit
|
||||
Neue Wrapper können jederzeit ergänzt werden, ohne bestehende Plugins zu brechen.
|
||||
|
||||
## Vorteile für Entwickler
|
||||
Keine QGIS‑Abhängigkeiten in der Logik
|
||||
|
||||
IDE‑freundlich (Pylance, Autocomplete, Typing)
|
||||
|
||||
CI‑fähig (Tests ohne QGIS)
|
||||
|
||||
saubere Architektur
|
||||
|
||||
leichte Wartbarkeit
|
||||
|
||||
klare Dokumentation
|
||||
|
||||
## Fazit
|
||||
Die Wrapper‑Architektur ist das Herzstück von sn_basis.
|
||||
Sie ermöglicht eine moderne, modulare und testbare QGIS‑Plugin‑Entwicklung, die unabhängig von QGIS‑Versionen, Qt‑Versionen und Entwicklungsumgebungen funktioniert.
|
||||
|
||||
Sie bildet die Grundlage für:
|
||||
|
||||
stabile APIs
|
||||
|
||||
saubere UI‑Abstraktion
|
||||
|
||||
automatisierte Tests
|
||||
|
||||
nachhaltige Weiterentwicklung
|
||||
@@ -1,144 +0,0 @@
|
||||
# Wrapper‑Architektur – Übersicht
|
||||
Die Wrapper‑Architektur von sn_basis bildet das Fundament für eine robuste, testbare und zukunftssichere QGIS‑Plugin‑Entwicklung.
|
||||
Sie kapselt sämtliche QGIS‑ und Qt‑Abhängigkeiten hinter klar definierten Schnittstellen und ermöglicht dadurch:
|
||||
|
||||
- Mock‑fähige Unit‑Tests ohne QGIS
|
||||
- PyQt5/6‑Kompatibilität ohne Code‑Änderungen
|
||||
- saubere Trennung von UI, Logik und Infrastruktur
|
||||
- stabile APIs, die unabhängig von QGIS‑Versionen bleiben
|
||||
- klare Erweiterbarkeit für zukünftige Module und Plugins
|
||||
|
||||
Die Wrapper‑Schicht ist das zentrale Bindeglied zwischen der Plugin‑Logik und der QGIS‑/Qt‑Umgebung.
|
||||
|
||||
## Ziele der Wrapper‑Architektur
|
||||
1. Entkopplung von QGIS und Qt
|
||||
Alle direkten Importe wie from qgis.core import ... oder from qgis.PyQt.QtWidgets import ... verschwinden aus der Plugin‑Logik.
|
||||
Stattdessen werden sie über Wrapper‑Module abstrahiert.
|
||||
|
||||
2. Testbarkeit ohne QGIS
|
||||
Im Mock‑Modus liefern die Wrapper:
|
||||
|
||||
- Dummy‑Objekte
|
||||
- simulierte Rückgabewerte
|
||||
- speicherbare Zustände (z. B. Variablen, Layer, Nachrichten)
|
||||
|
||||
Damit laufen Tests in jeder CI‑Umgebung.
|
||||
|
||||
3. Einheitliche API für alle Plugins
|
||||
Plugins greifen nicht mehr direkt auf QGIS zu, sondern nutzen:
|
||||
|
||||
|
||||
- sn_basis.functions.qgiscore_wrapper
|
||||
- sn_basis.functions.qgisui_wrapper
|
||||
- sn_basis.functions.qt_wrapper
|
||||
- sn_basis.functions.variable_wrapper
|
||||
- sn_basis.functions.message_wrapper
|
||||
- sn_basis.functions.dialog_wrapper
|
||||
Aufgrund des Umfangs ist der Wrapper für die Layerbehandlung aufgeteilt:
|
||||
- ly_existence_wrapper
|
||||
- ly_geometry_wrapper
|
||||
- ly_Metadata_wrapper
|
||||
- ly_style_wrapper
|
||||
- ly_visibility_wrapper
|
||||
|
||||
4. Zukunftssicherheit
|
||||
Ändert sich die QGIS‑ oder Qt‑API, wird nur der Wrapper angepasst, nicht jedes Plugin.
|
||||
|
||||
## Architekturüberblick
|
||||
Die Wrapper‑Schicht besteht aus mehreren Modulen, die jeweils einen klar abgegrenzten Verantwortungsbereich haben.
|
||||
|
||||
### 1. qt_wrapper – Qt‑Abstraktion
|
||||
Kapselt alle Qt‑Widgets, Dialoge und Konstanten:
|
||||
|
||||
- QWidget, QDialog, QMessageBox, QToolBar, QMenu, …
|
||||
- Layouts, Buttons, Labels, LineEdits
|
||||
- Qt‑Konstanten wie YES, NO, Dock‑Areas
|
||||
|
||||
Mock‑Modus:
|
||||
Stellt Dummy‑Widgets bereit, die keine UI öffnen.
|
||||
|
||||
### 2. qgiscore_wrapper – QGIS‑Core‑Abstraktion
|
||||
Abstraktion für:
|
||||
|
||||
- QgsProject
|
||||
- Layer‑Zugriff
|
||||
- Projekt‑Metadaten
|
||||
- Pfade, CRS, Feature‑Zugriff
|
||||
|
||||
Mock‑Modus:
|
||||
Simuliert ein Projekt und Layer‑Container.
|
||||
|
||||
### 3. qgisui_wrapper – QGIS‑UI‑Abstraktion
|
||||
Kapselt UI‑bezogene QGIS‑Funktionen:
|
||||
|
||||
- Zugriff auf iface
|
||||
- Dock‑Management
|
||||
- Menü‑ und Toolbar‑Integration
|
||||
- Hauptfenster‑Zugriff
|
||||
|
||||
Mock‑Modus:
|
||||
Stellt ein Dummy‑Interface bereit.
|
||||
|
||||
### 4. variable_wrapper – QGIS‑Variablen
|
||||
Abstraktion für:
|
||||
|
||||
- Projektvariablen (projectScope)
|
||||
- globale Variablen (globalScope)
|
||||
- Mock‑Speicher für Tests
|
||||
|
||||
Vorteile:
|
||||
|
||||
- keine QGIS‑Abhängigkeit in der Logik
|
||||
- testbare Variablenverwaltung
|
||||
- einheitliches API
|
||||
|
||||
### 5. message_wrapper – Meldungen & Logging
|
||||
Einheitliche Schnittstelle für:
|
||||
|
||||
- Fehlermeldungen
|
||||
- Warnungen
|
||||
- Info‑Meldungen
|
||||
- Logging
|
||||
|
||||
Mock‑Modus:
|
||||
Speichert Nachrichten statt sie an QGIS zu senden.
|
||||
|
||||
### 6. dialog_wrapper – Benutzer‑Dialoge
|
||||
Abstraktion für:
|
||||
|
||||
- Ja/Nein‑Dialoge
|
||||
- spätere Erweiterungen (Eingabedialoge, Dateidialoge, etc.)
|
||||
|
||||
Mock‑Modus:
|
||||
Gibt Default‑Werte zurück, öffnet keine UI.
|
||||
|
||||
### 7. DockManager & Navigation
|
||||
Diese Module nutzen die Wrapper‑Schicht, um:
|
||||
|
||||
- DockWidgets sicher zu verwalten
|
||||
- Toolbars und Menüs zu erzeugen
|
||||
- Reload‑sichere UI‑Strukturen aufzubauen
|
||||
|
||||
Sie sind keine Wrapper, sondern Wrapper‑Konsumenten. Alle Fach-Plugins nutzen den Dockmanager des Basisplugins.
|
||||
|
||||
## Designprinzipien
|
||||
1. Single Source of Truth
|
||||
Jede QGIS‑ oder Qt‑Funktionalität wird nur an einer Stelle implementiert.
|
||||
|
||||
2. Austauschbarkeit
|
||||
Mock‑Modus und Echtmodus sind vollständig austauschbar.
|
||||
|
||||
3. Testbarkeit
|
||||
Jede Funktion kann ohne QGIS getestet werden.
|
||||
|
||||
4. Saubere Trennung
|
||||
- UI → qt_wrapper
|
||||
- QGIS‑Core → qgiscore_wrapper
|
||||
- QGIS‑UI → qgisui_wrapper
|
||||
- Logik → settings_logic, layer_logic, prüfmanager, …
|
||||
|
||||
5. Erweiterbarkeit
|
||||
Neue Wrapper können jederzeit ergänzt werden, ohne bestehende Plugins zu brechen.
|
||||
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 15 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 138 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 100 KiB |
@@ -1,38 +0,0 @@
|
||||
```mermaid
|
||||
flowchart TD
|
||||
subgraph Plugin
|
||||
P[sn_plan41 Fachplugin]
|
||||
A[Adapter Plan41LinklistAdapter]
|
||||
PM[Pruefmanager]
|
||||
LP[Layerpruefer]
|
||||
KP[Linkpruefer]
|
||||
SP[Stilpruefer]
|
||||
end
|
||||
|
||||
subgraph Core
|
||||
DG[DataGrabber]
|
||||
NL[normalized entries]
|
||||
LL[Layer Loader Provider Dispatch]
|
||||
SM[Spatial Matcher]
|
||||
ST[Storage GPKG / PostGIS]
|
||||
PR[Project QGIS - addMapLayer]
|
||||
LOG[Log / Ergebnisstruktur]
|
||||
end
|
||||
|
||||
P -->|gibt Adapter, Prüfer, Pruefmanager| DG
|
||||
A -->|load liefert Rohdaten| DG
|
||||
DG -->|adapter.normalize| NL
|
||||
NL --> DG
|
||||
DG -->|für jeden Eintrag: _check_link -> KP.check| KP
|
||||
DG -->|für jeden Eintrag: _check_style -> SP.check| SP
|
||||
DG -->|prüfe vorhandene Layer| LP
|
||||
DG -->|lade Layer via provider| LL
|
||||
LL -->|Features| SM
|
||||
SM -->|Abgleich| DG
|
||||
DG -->|speichern| ST
|
||||
ST --> PR
|
||||
DG --> PR
|
||||
DG -->|Ergebnis/Fehler| LOG
|
||||
LOG --> PM
|
||||
DG --> PM
|
||||
```
|
||||
Binary file not shown.
@@ -1,9 +0,0 @@
|
||||
```mermaid
|
||||
graph TD
|
||||
M1["<div style='text-align:center'><b>sn_basis</b><br/><br/></b></div><div style='text-align:left'>➡ Initialisierung der GUI<br/> ➡ Exception Handling<br/> ➡ Bereitstellung der Stile"]
|
||||
M2["<div style='text-align:center'><b>sn_verfahrensgebiet</b><br/><br/><div style='text-align:left'>➡ Abruf und Aufbereitung der Gebietsgrenze"<br/>➡ Erstellung neuer Gebietsgrenzen <br/>➡ Grenzpunktextraktion<br/>➡ Grenzpunktprüfung]
|
||||
M3["<div style='text-align:center'><b>sn_Plan41</b><br/><br/><div style='text-align:left'>➡ Fachdatenabruf<br/>➡Versionierung der Fachdaten<br/>➡ Planung der TG-Maßnahmen <br/>➡Kartenerzeugung (NGG und P41)<br/>➡ Erzeugung der Begleitdokumente (Anlagenverzeichnis, MVZ, Maßnahmeblätter)"]
|
||||
|
||||
M1 --> M2
|
||||
M1 --> M3
|
||||
```
|
||||
@@ -0,0 +1,57 @@
|
||||
# Plan: QGIS 3.40 Exit-Crash absichern
|
||||
|
||||
Der wahrscheinlichste Auslöser ist fehlendes Signal-Cleanup beim Plugin-Unload: globale Projekt-Listener und PrintTab-Listener bleiben aktiv, während QgsProject beim QGIS-Exit bereits zerstört wird. Die empfohlene Lösung ist ein minimaler, versionsneutraler Cleanup-Pfad (disconnect + defensive Guards), ohne Fachlogik zu ändern.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Teardown-Funktion für globale Verfahrensgebiet-Listener ergänzen, inkl. idempotentem Disconnect und Rücksetzen des Installationsflags.
|
||||
- Datei: `sn_basis/functions/verfahrensgebiet_manager.py` (nach `setup_verfahrensgebiet_listener`, ca. Zeile 132)
|
||||
- Neue Funktion `teardown_verfahrensgebiet_listener()` disconnect alle 5 QgsProject-Signale (`layersAdded`, `layerRemoved`, `readProject`, `newProjectCreated`, `cleared`) und setzt `_listener_installed = False` zurück.
|
||||
- RuntimeError abfangen (Projekt wird gerade zerstört → normal).
|
||||
|
||||
2. Teardown in Unload-Pfad vor UI-Abbau aufrufen. (depends on step 1)
|
||||
- Datei: `sn_basis/main.py` (in `BasisPlugin.unload`, ca. Zeile 52)
|
||||
- Import und Aufruf von `teardown_verfahrensgebiet_listener()` als erste Zeile in `unload()`, bevor `self.ui.remove_all()` ausgeführt wird.
|
||||
|
||||
3. PrintTab um kleinen Projekt-Signal-Cleanup ergänzen, symmetrisch zu bestehendem Theme-Cleanup. (parallel mit step 1)
|
||||
- Datei: `sn_basis/ui/tabs/print_tab.py`
|
||||
- Neue Methode `cleanup()` nach `_disconnect_theme_collection_signals` (ca. Zeile 485).
|
||||
- Disconnectet QgsProject-Signale (`readProject`, `newProjectCreated`, `cleared`) von `self._on_project_changed` (analog zu bestehendem Theme-Disconnect-Muster).
|
||||
- Ruft danach `self._disconnect_theme_collection_signals()` auf.
|
||||
|
||||
4. BaseDockWidget.closeEvent so erweitern, dass Tab-Cleanup-Hooks vor Schließen aufgerufen werden. (depends on step 3)
|
||||
- Datei: `sn_basis/ui/base_dockwidget.py` (in `closeEvent`, ca. Zeile 97)
|
||||
- Vor dem bestehenden `self.action.setChecked(False)` Block: über alle Tabs iterieren, auf `cleanup`-Attribut prüfen, aufrufen; Exception abfangen.
|
||||
|
||||
5. Optional nur bei Restinstabilität: defensive Guards bei Projektzugriffen in kritischen Handlern ergänzen.
|
||||
- Datei: `sn_basis/functions/variable_wrapper.py` (in `get_variable`/`set_variable`, ca. Zeile 60 und 89)
|
||||
- Datei: `sn_basis/functions/verfahrensgebiet_manager.py` (in `_on_layer_removed`, ca. Zeile 114)
|
||||
- `QgsProject.instance()` auf None/RuntimeError prüfen und bei Zerstörung früh zurückkehren.
|
||||
|
||||
## Relevant files
|
||||
|
||||
- `sn_basis/functions/verfahrensgebiet_manager.py` — Listener-Setup/Teardown, Handler-Guards
|
||||
- `sn_basis/main.py` — Unload-Reihenfolge
|
||||
- `sn_basis/ui/tabs/print_tab.py` — Projekt-Signal-Cleanup
|
||||
- `sn_basis/ui/base_dockwidget.py` — zentraler Cleanup-Hook
|
||||
- `sn_basis/ui/dockmanager.py` — asynchrones deleteLater als Lifecycle-Kontext
|
||||
- `sn_basis/functions/variable_wrapper.py` — optional defensive Guards
|
||||
|
||||
## Decisions
|
||||
|
||||
- Include: minimaler Lifecycle-Fix über disconnect + Guards.
|
||||
- Exclude: Refactoring der gesamten Signal-Architektur, größere UI-Umbauten, versionsspezifische If-Branches.
|
||||
- Begründung: geringstes Risiko für Regressionen bei maximaler Wirkung gegen Shutdown-Crashs.
|
||||
- Keine Änderung an Business-Logik, keine Änderung an Variablennamen, keine QGIS-versionsspezifischen Sonderpfade — rein sichere Disconnect/Guard-Muster.
|
||||
|
||||
## Verification
|
||||
|
||||
1. Plugin laden, Dock öffnen/schließen, Projekt neu laden, Layer „Verfahrensgebiet" add/remove, dann QGIS 3.40.7 beenden → kein Access Violation.
|
||||
2. Dasselbe unter QGIS 4 → unverändertes Verhalten, keine Regression.
|
||||
3. Mehrfacher Plugin-Reload in einer Sitzung → keine doppelten Signal-Reaktionen, keine Exceptions beim Unload.
|
||||
4. Optionaler Stresstest: Unit-Test für idempotentes Teardown (mehrfaches unload ohne Exception).
|
||||
|
||||
## Further considerations
|
||||
|
||||
- Step 5 (defensive Guards in variable_wrapper) nur umsetzen, wenn nach Steps 1–4 noch reproduzierbare Exit-Crashs auftreten.
|
||||
- Ein kleiner Unit-Test für idempotentes Teardown (mehrfaches `unload()` ohne Exception) wäre empfehlenswert, falls die Testsuite erweitert wird.
|
||||
@@ -1,4 +1,14 @@
|
||||
---
|
||||
Version 26.4.7-unstable:
|
||||
---
|
||||
Version 26.4.6-unstable:
|
||||
---
|
||||
Version 26.4.5-unstable:
|
||||
---
|
||||
Version 26.4.4-unstable:
|
||||
---
|
||||
Version 26.4.3-unstable:
|
||||
---
|
||||
Version 26.04.1-unstable:
|
||||
---
|
||||
Version 26.3.3-unstable:
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
"""
|
||||
sn_basis/functions – Wrapper und Hilfsfunktionen für QGIS-Kompatibilität.
|
||||
Bietet abstrahierte Schnittstellen für Qt, QGIS und OS-Operationen.
|
||||
"""
|
||||
|
||||
from .ly_existence_wrapper import layer_exists
|
||||
from .ly_geometry_wrapper import (
|
||||
get_layer_geometry_type,
|
||||
@@ -15,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
|
||||
from .dialog_wrapper import ask_yes_no, ask_overwrite_append_cancel_custom, ask_ergaenzen_entfernen_ersetzen_abbrechen
|
||||
|
||||
from .message_wrapper import (
|
||||
_get_message_bar,
|
||||
|
||||
@@ -128,6 +128,60 @@ def ask_overwrite_append_cancel_custom(
|
||||
return "cancel"
|
||||
|
||||
|
||||
def ask_ergaenzen_entfernen_ersetzen_abbrechen(
|
||||
parent: Any,
|
||||
title: str,
|
||||
message: str,
|
||||
) -> Literal["ergaenzen", "entfernen", "ersetzen", "abbrechen"]:
|
||||
"""Zeigt Dialog mit vier Optionen für Verfahrensgebiet-Editoperationen.
|
||||
|
||||
Optionen:
|
||||
- **Ergänzen**: ausgewählte Flurstücke zum Verfahrensgebiet hinzufügen
|
||||
- **Entfernen**: ausgewählte Flurstücke aus dem Verfahrensgebiet entfernen
|
||||
- **Ersetzen**: Verfahrensgebiet vollständig durch Auswahl ersetzen
|
||||
- **Abbrechen**: Vorgang beenden ohne Änderung
|
||||
|
||||
Parameters
|
||||
----------
|
||||
parent :
|
||||
Eltern-Widget oder None.
|
||||
title : str
|
||||
Dialog-Titel.
|
||||
message : str
|
||||
Hauptmeldung mit Erklärung.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Literal["ergaenzen", "entfernen", "ersetzen", "abbrechen"]
|
||||
Genaue Entscheidung des Nutzers.
|
||||
"""
|
||||
if QT_VERSION == 0: # Mock-Modus
|
||||
print(f"Mock-Modus: ask_ergaenzen_entfernen_ersetzen_abbrechen('{title}') → abbrechen")
|
||||
return "abbrechen"
|
||||
|
||||
msg = QMessageBox(parent)
|
||||
msg.setIcon(ICON_QUESTION)
|
||||
msg.setWindowTitle(title)
|
||||
msg.setText(message)
|
||||
|
||||
ergaenzen_btn = msg.addButton("Ergänzen", QMessageBox.ButtonRole.ActionRole)
|
||||
entfernen_btn = msg.addButton("Entfernen", QMessageBox.ButtonRole.ActionRole)
|
||||
ersetzen_btn = msg.addButton("Ersetzen", QMessageBox.ButtonRole.AcceptRole)
|
||||
msg.addButton("Abbrechen", QMessageBox.ButtonRole.RejectRole)
|
||||
|
||||
exec_dialog(msg)
|
||||
|
||||
clicked = msg.clickedButton()
|
||||
if clicked == ergaenzen_btn:
|
||||
return "ergaenzen"
|
||||
elif clicked == entfernen_btn:
|
||||
return "entfernen"
|
||||
elif clicked == ersetzen_btn:
|
||||
return "ersetzen"
|
||||
else:
|
||||
return "abbrechen"
|
||||
|
||||
|
||||
class ProgressDialog:
|
||||
def __init__(self, total: int, title: str = "Fortschritt", label: str = "Verarbeite..."):
|
||||
self.total = max(total, 1)
|
||||
|
||||
@@ -24,10 +24,14 @@ QgsFeature: Type[Any]
|
||||
QgsField: Type[Any]
|
||||
QgsGeometry: Type[Any]
|
||||
QgsFeatureRequest: Type[Any]
|
||||
QgsRectangle: Type[Any]
|
||||
QgsCoordinateTransform: Type[Any]
|
||||
QgsCoordinateReferenceSystem: Type[Any]
|
||||
QgsPrintLayout: Type[Any]
|
||||
QgsLayoutItemMap: Type[Any]
|
||||
QgsLayoutItemMapGrid: Type[Any]
|
||||
QgsTextFormat: Type[Any]
|
||||
QgsTextBackgroundSettings: Type[Any]
|
||||
QgsLayoutItemLabel: Type[Any]
|
||||
QgsLayoutItemLegend: Type[Any]
|
||||
QgsLayoutPoint: Type[Any]
|
||||
@@ -35,6 +39,25 @@ QgsLayoutSize: Type[Any]
|
||||
QgsUnitTypes: Type[Any]
|
||||
QgsLayoutItem: Type[Any]
|
||||
|
||||
MAP_GRID_STYLE_MARKERS: Any = None
|
||||
MAP_GRID_STYLE_FRAME_ANNOTATIONS: Any = None
|
||||
MAP_GRID_FRAME_STYLE_INTERIOR_TICKS: Any = None
|
||||
MAP_GRID_UNIT_MAP_UNIT: Any = None
|
||||
MAP_GRID_ANNOTATION_FORMAT_DECIMAL: Any = None
|
||||
MAP_GRID_ANNOTATION_DISPLAY_HIDE_ALL: Any = None
|
||||
MAP_GRID_ANNOTATION_DISPLAY_SHOW_ALL: Any = None
|
||||
MAP_GRID_ANNOTATION_DISPLAY_SHOW_X: Any = None
|
||||
MAP_GRID_ANNOTATION_DISPLAY_SHOW_Y: Any = None
|
||||
MAP_GRID_BORDER_LEFT: Any = None
|
||||
MAP_GRID_BORDER_RIGHT: Any = None
|
||||
MAP_GRID_BORDER_TOP: Any = None
|
||||
MAP_GRID_BORDER_BOTTOM: Any = None
|
||||
MAP_GRID_ANNOTATION_POSITION_INSIDE_FRAME: Any = None
|
||||
TEXT_BACKGROUND_SHAPE_RECTANGLE: Any = None
|
||||
TEXT_BACKGROUND_SIZE_BUFFER: Any = None
|
||||
TEXT_RENDER_UNIT_MILLIMETERS: Any = None
|
||||
TEXT_RENDER_UNIT_POINTS: Any = None
|
||||
|
||||
QGIS_AVAILABLE = False
|
||||
|
||||
# ---------------------------------------------------------
|
||||
@@ -54,10 +77,14 @@ try:
|
||||
QgsField as _QgsField,
|
||||
QgsGeometry as _QgsGeometry,
|
||||
QgsFeatureRequest as _QgsFeatureRequest,
|
||||
QgsRectangle as _QgsRectangle,
|
||||
QgsCoordinateTransform as _QgsCoordinateTransform,
|
||||
QgsCoordinateReferenceSystem as _QgsCoordinateReferenceSystem,
|
||||
QgsPrintLayout as _QgsPrintLayout,
|
||||
QgsLayoutItemMap as _QgsLayoutItemMap,
|
||||
QgsLayoutItemMapGrid as _QgsLayoutItemMapGrid,
|
||||
QgsTextFormat as _QgsTextFormat,
|
||||
QgsTextBackgroundSettings as _QgsTextBackgroundSettings,
|
||||
QgsLayoutItemLabel as _QgsLayoutItemLabel,
|
||||
QgsLayoutItemLegend as _QgsLayoutItemLegend,
|
||||
QgsLayoutPoint as _QgsLayoutPoint,
|
||||
@@ -77,10 +104,14 @@ try:
|
||||
QgsField = _QgsField
|
||||
QgsGeometry = _QgsGeometry
|
||||
QgsFeatureRequest = _QgsFeatureRequest
|
||||
QgsRectangle = _QgsRectangle
|
||||
QgsCoordinateTransform = _QgsCoordinateTransform
|
||||
QgsCoordinateReferenceSystem = _QgsCoordinateReferenceSystem
|
||||
QgsPrintLayout = _QgsPrintLayout
|
||||
QgsLayoutItemMap = _QgsLayoutItemMap
|
||||
QgsLayoutItemMapGrid = _QgsLayoutItemMapGrid
|
||||
QgsTextFormat = _QgsTextFormat
|
||||
QgsTextBackgroundSettings = _QgsTextBackgroundSettings
|
||||
QgsLayoutItemLabel = _QgsLayoutItemLabel
|
||||
QgsLayoutItemLegend = _QgsLayoutItemLegend
|
||||
QgsLayoutPoint = _QgsLayoutPoint
|
||||
@@ -88,6 +119,98 @@ try:
|
||||
QgsUnitTypes = _QgsUnitTypes
|
||||
QgsLayoutItem = _QgsLayoutItem
|
||||
|
||||
def _resolve_qgis_enum(*paths: tuple[Any, ...]) -> Any:
|
||||
for path in paths:
|
||||
value: Any = path[0]
|
||||
ok = True
|
||||
for name in path[1:]:
|
||||
value = getattr(value, name, None)
|
||||
if value is None:
|
||||
ok = False
|
||||
break
|
||||
if ok:
|
||||
return value
|
||||
return None
|
||||
|
||||
MAP_GRID_STYLE_MARKERS = _resolve_qgis_enum(
|
||||
(_Qgis, "MapGridStyle", "Markers"),
|
||||
(_QgsLayoutItemMapGrid, "Markers"),
|
||||
)
|
||||
MAP_GRID_STYLE_FRAME_ANNOTATIONS = _resolve_qgis_enum(
|
||||
(_Qgis, "MapGridStyle", "FrameAndAnnotationsOnly"),
|
||||
(_QgsLayoutItemMapGrid, "FrameAndAnnotationsOnly"),
|
||||
(_Qgis, "MapGridStyle", "FrameAnnotations"),
|
||||
(_QgsLayoutItemMapGrid, "FrameAnnotations"),
|
||||
)
|
||||
MAP_GRID_FRAME_STYLE_INTERIOR_TICKS = _resolve_qgis_enum(
|
||||
(_Qgis, "MapGridFrameStyle", "InteriorTicks"),
|
||||
(_QgsLayoutItemMapGrid, "InteriorTicks"),
|
||||
)
|
||||
MAP_GRID_UNIT_MAP_UNIT = _resolve_qgis_enum(
|
||||
(_Qgis, "MapGridUnit", "MapUnits"),
|
||||
(_QgsLayoutItemMapGrid, "MapUnits"),
|
||||
(_Qgis, "MapGridUnit", "MapUnit"),
|
||||
(_QgsLayoutItemMapGrid, "MapUnit"),
|
||||
)
|
||||
MAP_GRID_ANNOTATION_FORMAT_DECIMAL = _resolve_qgis_enum(
|
||||
(_Qgis, "MapGridAnnotationFormat", "Decimal"),
|
||||
(_QgsLayoutItemMapGrid, "Decimal"),
|
||||
)
|
||||
MAP_GRID_ANNOTATION_DISPLAY_HIDE_ALL = _resolve_qgis_enum(
|
||||
(_Qgis, "MapGridComponentVisibility", "HideAll"),
|
||||
(_QgsLayoutItemMapGrid, "HideAll"),
|
||||
)
|
||||
MAP_GRID_ANNOTATION_DISPLAY_SHOW_ALL = _resolve_qgis_enum(
|
||||
(_Qgis, "MapGridComponentVisibility", "ShowAll"),
|
||||
(_QgsLayoutItemMapGrid, "ShowAll"),
|
||||
)
|
||||
MAP_GRID_ANNOTATION_DISPLAY_SHOW_X = _resolve_qgis_enum(
|
||||
(_Qgis, "MapGridComponentVisibility", "LongitudeOnly"),
|
||||
(_QgsLayoutItemMapGrid, "LongitudeOnly"),
|
||||
(_Qgis, "MapGridComponentVisibility", "ShowLongitudeOnly"),
|
||||
(_QgsLayoutItemMapGrid, "ShowLongitudeOnly"),
|
||||
)
|
||||
MAP_GRID_ANNOTATION_DISPLAY_SHOW_Y = _resolve_qgis_enum(
|
||||
(_Qgis, "MapGridComponentVisibility", "LatitudeOnly"),
|
||||
(_QgsLayoutItemMapGrid, "LatitudeOnly"),
|
||||
(_Qgis, "MapGridComponentVisibility", "ShowLatitudeOnly"),
|
||||
(_QgsLayoutItemMapGrid, "ShowLatitudeOnly"),
|
||||
)
|
||||
MAP_GRID_BORDER_LEFT = _resolve_qgis_enum(
|
||||
(_Qgis, "MapGridBorderSide", "Left"),
|
||||
(_QgsLayoutItemMapGrid, "Left"),
|
||||
)
|
||||
MAP_GRID_BORDER_RIGHT = _resolve_qgis_enum(
|
||||
(_Qgis, "MapGridBorderSide", "Right"),
|
||||
(_QgsLayoutItemMapGrid, "Right"),
|
||||
)
|
||||
MAP_GRID_BORDER_TOP = _resolve_qgis_enum(
|
||||
(_Qgis, "MapGridBorderSide", "Top"),
|
||||
(_QgsLayoutItemMapGrid, "Top"),
|
||||
)
|
||||
MAP_GRID_BORDER_BOTTOM = _resolve_qgis_enum(
|
||||
(_Qgis, "MapGridBorderSide", "Bottom"),
|
||||
(_QgsLayoutItemMapGrid, "Bottom"),
|
||||
)
|
||||
MAP_GRID_ANNOTATION_POSITION_INSIDE_FRAME = _resolve_qgis_enum(
|
||||
(_Qgis, "MapGridAnnotationPosition", "InsideMapFrame"),
|
||||
(_QgsLayoutItemMapGrid, "InsideMapFrame"),
|
||||
)
|
||||
TEXT_BACKGROUND_SHAPE_RECTANGLE = _resolve_qgis_enum(
|
||||
(_QgsTextBackgroundSettings, "ShapeRectangle"),
|
||||
)
|
||||
TEXT_BACKGROUND_SIZE_BUFFER = _resolve_qgis_enum(
|
||||
(_QgsTextBackgroundSettings, "SizeBuffer"),
|
||||
)
|
||||
TEXT_RENDER_UNIT_MILLIMETERS = _resolve_qgis_enum(
|
||||
(_Qgis, "RenderUnit", "Millimeters"),
|
||||
(_QgsUnitTypes, "RenderMillimeters"),
|
||||
)
|
||||
TEXT_RENDER_UNIT_POINTS = _resolve_qgis_enum(
|
||||
(_Qgis, "RenderUnit", "Points"),
|
||||
(_QgsUnitTypes, "RenderPoints"),
|
||||
)
|
||||
|
||||
QGIS_AVAILABLE = True
|
||||
|
||||
# ---------------------------------------------------------
|
||||
@@ -208,6 +331,7 @@ except Exception:
|
||||
self.follow_visibility_preset = False
|
||||
self.follow_visibility_preset_name = ""
|
||||
self.keep_layer_set = False
|
||||
self._grids = _MockQgsLayoutItemMapGridStack(self)
|
||||
|
||||
def setId(self, item_id: str) -> None:
|
||||
self.item_id = item_id
|
||||
@@ -233,6 +357,154 @@ except Exception:
|
||||
def setKeepLayerSet(self, enabled: bool) -> None:
|
||||
self.keep_layer_set = enabled
|
||||
|
||||
def grids(self):
|
||||
return self._grids
|
||||
|
||||
def updateBoundingRect(self) -> None:
|
||||
pass
|
||||
|
||||
def refresh(self) -> None:
|
||||
pass
|
||||
|
||||
class _MockQgsLayoutItemMapGrid:
|
||||
Markers = "Markers"
|
||||
FrameAndAnnotationsOnly = "FrameAndAnnotationsOnly"
|
||||
InteriorTicks = "InteriorTicks"
|
||||
MapUnits = "MapUnits"
|
||||
Decimal = "Decimal"
|
||||
HideAll = "HideAll"
|
||||
ShowAll = "ShowAll"
|
||||
LongitudeOnly = "LongitudeOnly"
|
||||
LatitudeOnly = "LatitudeOnly"
|
||||
Left = "Left"
|
||||
Right = "Right"
|
||||
Top = "Top"
|
||||
Bottom = "Bottom"
|
||||
InsideMapFrame = "InsideMapFrame"
|
||||
|
||||
def __init__(self, name: str, map_item: Any):
|
||||
self.name = name
|
||||
self.map_item = map_item
|
||||
self.enabled = False
|
||||
self.style = None
|
||||
self.units = None
|
||||
self.interval_x = None
|
||||
self.interval_y = None
|
||||
self.annotation_enabled = False
|
||||
self.annotation_format = None
|
||||
self.annotation_precision = None
|
||||
self.annotation_display: dict[Any, Any] = {}
|
||||
self.annotation_position: dict[Any, Any] = {}
|
||||
self.frame_style = None
|
||||
self.annotation_text_format = None
|
||||
|
||||
def setEnabled(self, enabled: bool) -> None:
|
||||
self.enabled = enabled
|
||||
|
||||
def setStyle(self, style: Any) -> None:
|
||||
self.style = style
|
||||
|
||||
def setUnits(self, units: Any) -> None:
|
||||
self.units = units
|
||||
|
||||
def setIntervalX(self, interval: float) -> None:
|
||||
self.interval_x = interval
|
||||
|
||||
def setIntervalY(self, interval: float) -> None:
|
||||
self.interval_y = interval
|
||||
|
||||
def setAnnotationEnabled(self, enabled: bool) -> None:
|
||||
self.annotation_enabled = enabled
|
||||
|
||||
def setAnnotationFormat(self, annotation_format: Any) -> None:
|
||||
self.annotation_format = annotation_format
|
||||
|
||||
def setAnnotationPrecision(self, precision: int) -> None:
|
||||
self.annotation_precision = precision
|
||||
|
||||
def setAnnotationDisplay(self, display: Any, border: Any) -> None:
|
||||
self.annotation_display[border] = display
|
||||
|
||||
def setAnnotationPosition(self, position: Any, border: Any) -> None:
|
||||
self.annotation_position[border] = position
|
||||
|
||||
def setFrameStyle(self, frame_style: Any) -> None:
|
||||
self.frame_style = frame_style
|
||||
|
||||
def setAnnotationTextFormat(self, annotation_text_format: Any) -> None:
|
||||
self.annotation_text_format = annotation_text_format
|
||||
|
||||
class _MockQgsTextBackgroundSettings:
|
||||
ShapeRectangle = "ShapeRectangle"
|
||||
SizeBuffer = "SizeBuffer"
|
||||
|
||||
def __init__(self):
|
||||
self.enabled = False
|
||||
self.shape_type = None
|
||||
self.size_type = None
|
||||
self.size = None
|
||||
self.size_unit = None
|
||||
self.offset = None
|
||||
self.offset_unit = None
|
||||
self.fill_color = None
|
||||
|
||||
def setEnabled(self, enabled: bool) -> None:
|
||||
self.enabled = enabled
|
||||
|
||||
def setType(self, shape_type: Any) -> None:
|
||||
self.shape_type = shape_type
|
||||
|
||||
def setSizeType(self, size_type: Any) -> None:
|
||||
self.size_type = size_type
|
||||
|
||||
def setSize(self, size: Any) -> None:
|
||||
self.size = size
|
||||
|
||||
def setSizeUnit(self, unit: Any) -> None:
|
||||
self.size_unit = unit
|
||||
|
||||
def setOffset(self, offset: Any) -> None:
|
||||
self.offset = offset
|
||||
|
||||
def setOffsetUnit(self, unit: Any) -> None:
|
||||
self.offset_unit = unit
|
||||
|
||||
def setFillColor(self, color: Any) -> None:
|
||||
self.fill_color = color
|
||||
|
||||
class _MockQgsTextFormat:
|
||||
def __init__(self):
|
||||
self.font = None
|
||||
self.size = None
|
||||
self.size_unit = None
|
||||
self.background = None
|
||||
|
||||
def setFont(self, font: Any) -> None:
|
||||
self.font = font
|
||||
|
||||
def setSize(self, size: float) -> None:
|
||||
self.size = size
|
||||
|
||||
def setSizeUnit(self, unit: Any) -> None:
|
||||
self.size_unit = unit
|
||||
|
||||
def setBackground(self, background: Any) -> None:
|
||||
self.background = background
|
||||
|
||||
class _MockQgsLayoutItemMapGridStack:
|
||||
def __init__(self, map_item: Any):
|
||||
self.map_item = map_item
|
||||
self._grids: list[_MockQgsLayoutItemMapGrid] = []
|
||||
|
||||
def addGrid(self, grid: Any) -> None:
|
||||
self._grids.append(grid)
|
||||
|
||||
def removeGrid(self, grid_id: str) -> None:
|
||||
self._grids = [grid for grid in self._grids if getattr(grid, "name", "") != grid_id]
|
||||
|
||||
def asList(self) -> list[Any]:
|
||||
return list(self._grids)
|
||||
|
||||
class _MockQgsLayoutItemLabel:
|
||||
ModeHtml = 1
|
||||
|
||||
@@ -254,6 +526,9 @@ except Exception:
|
||||
def setReferencePoint(self, point: Any) -> None:
|
||||
pass
|
||||
|
||||
def setVAlign(self, alignment: Any) -> None:
|
||||
pass
|
||||
|
||||
def attemptMove(self, point: Any) -> None:
|
||||
pass
|
||||
|
||||
@@ -338,6 +613,9 @@ except Exception:
|
||||
|
||||
QgsPrintLayout = _MockQgsPrintLayout
|
||||
QgsLayoutItemMap = _MockQgsLayoutItemMap
|
||||
QgsLayoutItemMapGrid = _MockQgsLayoutItemMapGrid
|
||||
QgsTextFormat = _MockQgsTextFormat
|
||||
QgsTextBackgroundSettings = _MockQgsTextBackgroundSettings
|
||||
QgsLayoutItemLabel = _MockQgsLayoutItemLabel
|
||||
QgsLayoutItemLegend = _MockQgsLayoutItemLegend
|
||||
QgsLayoutPoint = _MockQgsLayoutPoint
|
||||
@@ -345,6 +623,25 @@ except Exception:
|
||||
QgsUnitTypes = _MockQgsUnitTypes
|
||||
QgsLayoutItem = _MockQgsLayoutItem
|
||||
|
||||
MAP_GRID_STYLE_MARKERS = _MockQgsLayoutItemMapGrid.Markers
|
||||
MAP_GRID_STYLE_FRAME_ANNOTATIONS = _MockQgsLayoutItemMapGrid.FrameAndAnnotationsOnly
|
||||
MAP_GRID_FRAME_STYLE_INTERIOR_TICKS = _MockQgsLayoutItemMapGrid.InteriorTicks
|
||||
MAP_GRID_UNIT_MAP_UNIT = _MockQgsLayoutItemMapGrid.MapUnits
|
||||
MAP_GRID_ANNOTATION_FORMAT_DECIMAL = _MockQgsLayoutItemMapGrid.Decimal
|
||||
MAP_GRID_ANNOTATION_DISPLAY_HIDE_ALL = _MockQgsLayoutItemMapGrid.HideAll
|
||||
MAP_GRID_ANNOTATION_DISPLAY_SHOW_ALL = _MockQgsLayoutItemMapGrid.ShowAll
|
||||
MAP_GRID_ANNOTATION_DISPLAY_SHOW_X = _MockQgsLayoutItemMapGrid.LongitudeOnly
|
||||
MAP_GRID_ANNOTATION_DISPLAY_SHOW_Y = _MockQgsLayoutItemMapGrid.LatitudeOnly
|
||||
MAP_GRID_BORDER_LEFT = _MockQgsLayoutItemMapGrid.Left
|
||||
MAP_GRID_BORDER_RIGHT = _MockQgsLayoutItemMapGrid.Right
|
||||
MAP_GRID_BORDER_TOP = _MockQgsLayoutItemMapGrid.Top
|
||||
MAP_GRID_BORDER_BOTTOM = _MockQgsLayoutItemMapGrid.Bottom
|
||||
MAP_GRID_ANNOTATION_POSITION_INSIDE_FRAME = _MockQgsLayoutItemMapGrid.InsideMapFrame
|
||||
TEXT_BACKGROUND_SHAPE_RECTANGLE = _MockQgsTextBackgroundSettings.ShapeRectangle
|
||||
TEXT_BACKGROUND_SIZE_BUFFER = _MockQgsTextBackgroundSettings.SizeBuffer
|
||||
TEXT_RENDER_UNIT_MILLIMETERS = "Millimeters"
|
||||
TEXT_RENDER_UNIT_POINTS = "Points"
|
||||
|
||||
class _MockQgsFeature:
|
||||
def __init__(self):
|
||||
self._geometry = None
|
||||
@@ -657,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,14 +0,0 @@
|
||||
mermaid´´´
|
||||
flowchart TD
|
||||
A[Projekt]
|
||||
|
||||
subgraph children[ ]
|
||||
direction TB
|
||||
B[src]
|
||||
C[docs]
|
||||
D[README.md]
|
||||
end
|
||||
|
||||
A --> B
|
||||
A --> C
|
||||
A --> D
|
||||
@@ -171,3 +171,46 @@ def setup_verfahrensgebiet_listener() -> None:
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def teardown_verfahrensgebiet_listener() -> None:
|
||||
"""
|
||||
Trennt globale Event-Listener vom QgsProject-Singleton.
|
||||
|
||||
Diese Funktion ist idempotent und kann mehrfach aufgerufen werden.
|
||||
"""
|
||||
global _listener_installed
|
||||
|
||||
if not _listener_installed:
|
||||
return
|
||||
|
||||
try:
|
||||
project = QgsProject.instance()
|
||||
if project is None:
|
||||
return
|
||||
|
||||
try:
|
||||
project.layersAdded.disconnect(_on_layers_added)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
project.layerRemoved.disconnect(_on_layer_removed)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
for signal_name in ("readProject", "newProjectCreated", "cleared"):
|
||||
signal = getattr(project, signal_name, None)
|
||||
if signal is None:
|
||||
continue
|
||||
try:
|
||||
signal.disconnect(_on_project_changed)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
except RuntimeError:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
_listener_installed = False
|
||||
|
||||
@@ -4,7 +4,10 @@ from qgis.utils import plugins
|
||||
|
||||
from sn_basis.functions.qt_wrapper import QCoreApplication
|
||||
from sn_basis.functions.sys_wrapper import getattr_safe
|
||||
from sn_basis.functions.verfahrensgebiet_manager import setup_verfahrensgebiet_listener
|
||||
from sn_basis.functions.verfahrensgebiet_manager import (
|
||||
setup_verfahrensgebiet_listener,
|
||||
teardown_verfahrensgebiet_listener,
|
||||
)
|
||||
from sn_basis.ui.navigation import Navigation
|
||||
|
||||
|
||||
@@ -53,6 +56,8 @@ class BasisPlugin:
|
||||
"""
|
||||
Räumt UI-Komponenten sauber auf.
|
||||
"""
|
||||
teardown_verfahrensgebiet_listener()
|
||||
|
||||
if self.ui:
|
||||
self.ui.remove_all()
|
||||
self.ui = None
|
||||
|
||||
+2
-2
@@ -1,12 +1,12 @@
|
||||
[general]
|
||||
version=26.04.1-unstable
|
||||
version=26.4.7-unstable
|
||||
name=LNO Sachsen | Plugin Basisfunktionen
|
||||
description=Plugin mit Basisfunktionen
|
||||
author=Daniel Helbig, Michael Otto
|
||||
homepage=https://entwicklung.flurneuordnung-sachsen.de/AG_QGIS/Plugin_SN_Basis
|
||||
tracker=https://entwicklung.flurneuordnung-sachsen.de/AG_QGIS/Plugin_SN_Basis/issues
|
||||
repository=https://entwicklung.flurneuordnung-sachsen.de/AG_QGIS/Plugin_SN_Basis/src/branch/unstable/
|
||||
|
||||
QGISMaximumVersion=4.99
|
||||
qgisMinimumVersion=3.40
|
||||
experimental=true
|
||||
deprecated=false
|
||||
|
||||
+20
-2
@@ -46,7 +46,7 @@ class Dateipruefer:
|
||||
standarddatei: Optional[str] = None,
|
||||
temporaer_erlaubt: bool = False,
|
||||
*,
|
||||
verfahrens_db_modus: bool = True, # 🆕 Verfahrens-DB-spezifische Logik
|
||||
verfahrens_db_modus: bool = False,
|
||||
) -> None:
|
||||
"""
|
||||
Parameters
|
||||
@@ -90,6 +90,8 @@ class Dateipruefer:
|
||||
|
||||
def _pfad(self, relativer_pfad: str) -> Path:
|
||||
"""Erzeugt OS-unabhängigen Pfad relativ zum Basisverzeichnis."""
|
||||
if relativer_pfad.startswith(("/", "\\")):
|
||||
return Path(relativer_pfad)
|
||||
return join_path(self.basis_pfad, relativer_pfad)
|
||||
|
||||
|
||||
@@ -152,6 +154,22 @@ class Dateipruefer:
|
||||
#Excel-dateien erkennen
|
||||
dateityp = self.erkenne_dateityp(pfad)
|
||||
|
||||
if not self.verfahrens_db_modus:
|
||||
if not file_exists(pfad):
|
||||
return pruef_ergebnis(
|
||||
ok=False,
|
||||
meldung=f"Datei '{self.pfad}' wurde nicht gefunden.",
|
||||
aktion="datei_nicht_gefunden",
|
||||
kontext=pfad,
|
||||
)
|
||||
|
||||
return pruef_ergebnis(
|
||||
ok=True,
|
||||
meldung="Datei ist gültig.",
|
||||
aktion="ok",
|
||||
kontext=pfad,
|
||||
)
|
||||
|
||||
if dateityp == "excel":
|
||||
if not file_exists(pfad):
|
||||
return pruef_ergebnis(
|
||||
@@ -176,7 +194,7 @@ class Dateipruefer:
|
||||
)
|
||||
|
||||
# 🆕 2.c: Ungültiger GPKG-Pfad?
|
||||
if not self.verfahrens_db_modus or not self._ist_gueltiger_gpkg_pfad(pfad):
|
||||
if not self._ist_gueltiger_gpkg_pfad(pfad):
|
||||
return pruef_ergebnis(
|
||||
ok=False,
|
||||
meldung=f"Der Pfad '{self.pfad}' ist kein gültiger GPKG-Pfad.",
|
||||
|
||||
@@ -1 +1,40 @@
|
||||
#Datenbankpruefer.py
|
||||
"""
|
||||
sn_basis/modules/Datenbankpruefer.py – Prüfung von Datenbanken.
|
||||
Validiert Datenbank-Verbindungen und -Strukturen.
|
||||
"""
|
||||
|
||||
from typing import Optional, Any
|
||||
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
|
||||
|
||||
|
||||
class Datenbankpruefer:
|
||||
"""
|
||||
Prüft Datenbankverbindungen und Datenbankstrukturen auf Gültigkeit.
|
||||
Gibt pruef_ergebnis für die Weiterverarbeitung durch Pruefmanager zurück.
|
||||
"""
|
||||
|
||||
def __init__(self, pfad: Optional[str] = None) -> None:
|
||||
"""
|
||||
Initialisiert den Datenbankprüfer.
|
||||
|
||||
Args:
|
||||
pfad: Optional Pfad zur Datenbankdatei (z.B. .gpkg, .sqlite)
|
||||
"""
|
||||
self.pfad = pfad
|
||||
|
||||
def pruefe(self) -> pruef_ergebnis:
|
||||
"""
|
||||
Prüft die Datenbank auf Gültigkeit und Verbindbarkeit.
|
||||
|
||||
Returns:
|
||||
pruef_ergebnis: Ergebnis der Prüfung mit ok-Flag und Aktion
|
||||
"""
|
||||
if not self.pfad:
|
||||
return pruef_ergebnis(
|
||||
ok=False,
|
||||
meldung="Kein Datenbankpfad angegeben.",
|
||||
aktion="kein_dateipfad",
|
||||
kontext=None,
|
||||
)
|
||||
# Implementierung folgt
|
||||
return pruef_ergebnis(ok=True, meldung="Datenbank OK", aktion="ok", kontext=self.pfad)
|
||||
+63
-31
@@ -5,9 +5,9 @@ sn_basis/modules/Pruefmanager.py
|
||||
from __future__ import annotations
|
||||
from typing import Optional, Any
|
||||
|
||||
from sn_basis.functions import ask_yes_no, info, warning, error, ask_overwrite_append_cancel_custom
|
||||
from sn_basis.functions import ask_yes_no, info, warning, error, ask_overwrite_append_cancel_custom, ask_ergaenzen_entfernen_ersetzen_abbrechen
|
||||
from sn_basis.functions.ly_visibility_wrapper import set_layer_visible
|
||||
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis, PruefAktion
|
||||
print("DEBUG: Pruefmanager DATEI GELADEN:", __file__)
|
||||
|
||||
class Pruefmanager:
|
||||
def __init__(self, ui_modus: str = "qgis", parent: Optional[Any] = None) -> None:
|
||||
@@ -146,10 +146,7 @@ class Pruefmanager:
|
||||
# ------------------------------------------------------------------
|
||||
def _handle_basic_decision(self, ergebnis: pruef_ergebnis) -> pruef_ergebnis:
|
||||
"""Basis-Entscheidung für einfache Ja/Nein-Fragen."""
|
||||
print(f"DEBUG _handle_basic_decision: aktion='{ergebnis.aktion}', ui_modus='{self.ui_modus}'")
|
||||
|
||||
if self.ui_modus != "qgis":
|
||||
print("DEBUG: Nicht QGIS → ergebnis unverändert")
|
||||
return ergebnis
|
||||
|
||||
title_map = {
|
||||
@@ -163,27 +160,32 @@ class Pruefmanager:
|
||||
meldung = ergebnis.meldung or ""
|
||||
|
||||
try:
|
||||
print(f"DEBUG ask_yes_no: title='{title}', meldung='{meldung[:50]}...'")
|
||||
yes = ask_yes_no(title, meldung, default=False, parent=self.parent)
|
||||
print(f"DEBUG ask_yes_no: yes={yes}")
|
||||
except Exception as e:
|
||||
print(f"DEBUG ask_yes_no Exception: {e}")
|
||||
except Exception:
|
||||
return ergebnis
|
||||
|
||||
if not yes:
|
||||
print("DEBUG: Nutzer sagte Nein → ok=False")
|
||||
return ergebnis
|
||||
|
||||
# Nutzer sagte Ja
|
||||
if ergebnis.aktion == "temporaer_erlaubt":
|
||||
print("DEBUG: temporaer_erlaubt bestätigt → ok=True")
|
||||
return pruef_ergebnis(
|
||||
ok=True,
|
||||
aktion="temporaer_erlaubt",
|
||||
aktion="temporaer_erzeugen",
|
||||
kontext=ergebnis.kontext
|
||||
)
|
||||
|
||||
print("DEBUG: Andere Aktion bestätigt → ok=True, aktion='ok'")
|
||||
if ergebnis.aktion == "layer_unsichtbar":
|
||||
try:
|
||||
set_layer_visible(ergebnis.kontext, True)
|
||||
except Exception:
|
||||
return ergebnis
|
||||
return pruef_ergebnis(
|
||||
ok=True,
|
||||
aktion="ok",
|
||||
kontext=ergebnis.kontext,
|
||||
)
|
||||
|
||||
return pruef_ergebnis(
|
||||
ok=True,
|
||||
aktion="ok",
|
||||
@@ -194,50 +196,80 @@ class Pruefmanager:
|
||||
# Hauptlogik: verarbeite() (KORRIGIERT!)
|
||||
# ------------------------------------------------------------------
|
||||
def verarbeite(self, ergebnis: pruef_ergebnis) -> pruef_ergebnis:
|
||||
print("🔥 verarbeite() START")
|
||||
print("DEBUG Pruefmanager:", ergebnis.ok, ergebnis.aktion)
|
||||
print("DEBUG ergebnis.aktion TYPE:", type(ergebnis.aktion), repr(ergebnis.aktion))
|
||||
|
||||
# 1. Erfolg → direkt weiter
|
||||
print("🔍 Schritt 1: Prüfe ergebnis.ok =", ergebnis.ok)
|
||||
if ergebnis.ok:
|
||||
print("✅ Schritt 1: ok=True → return")
|
||||
return ergebnis
|
||||
|
||||
# 2. VERFAHRENS-DB: Bestehende Datei
|
||||
print("🔍 Schritt 2: Prüfe datei_existiert =", ergebnis.aktion == "datei_existiert")
|
||||
if ergebnis.aktion == "datei_existiert":
|
||||
print("✅ Schritt 2: _handle_datei_existiert")
|
||||
return self._handle_datei_existiert(ergebnis)
|
||||
|
||||
# 3. Verfahrensgebiet-Editentscheidung
|
||||
if ergebnis.aktion == "verfahrensgebiet_layer_existiert":
|
||||
return self._handle_verfahrensgebiet_layer_existiert(ergebnis)
|
||||
|
||||
# 3. Basis interaktive Aktionen
|
||||
print("🔍 Schritt 3: Definiere interactive_actions")
|
||||
interactive_actions = {
|
||||
"leereingabe_erlaubt",
|
||||
"standarddatei_vorschlagen",
|
||||
"temporaer_erlaubt",
|
||||
"layer_unsichtbar",
|
||||
}
|
||||
print("DEBUG interactive_actions:", repr(interactive_actions))
|
||||
print("DEBUG ergebnis.aktion in interactive_actions?", ergebnis.aktion in interactive_actions)
|
||||
|
||||
if ergebnis.aktion in interactive_actions:
|
||||
print("✅ Schritt 3: Interaktive Aktion → _handle_basic_decision")
|
||||
decision = self._handle_basic_decision(ergebnis)
|
||||
print(f"DEBUG: _handle_basic_decision Ergebnis: ok={decision.ok}, aktion='{decision.aktion}'")
|
||||
return decision
|
||||
return self._handle_basic_decision(ergebnis)
|
||||
|
||||
# 4. Fehler behandeln
|
||||
print("❌ Schritt 4: FEHLER BEHANDELN")
|
||||
self.report_error(
|
||||
thema=ergebnis.aktion or "pruefung",
|
||||
meldung=ergebnis.meldung or "",
|
||||
aktion=ergebnis.aktion,
|
||||
kontext=ergebnis.kontext,
|
||||
)
|
||||
print("🔥 verarbeite() ENDE mit ok=False")
|
||||
return ergebnis
|
||||
|
||||
def _handle_verfahrensgebiet_layer_existiert(self, ergebnis: pruef_ergebnis) -> pruef_ergebnis:
|
||||
"""Fragt Nutzer, was mit dem bestehenden Verfahrensgebiet-Layer geschehen soll.
|
||||
|
||||
Zeigt einen Dialog mit vier Optionen:
|
||||
- **Ergänzen**: neue Flurstücke hinzufügen
|
||||
- **Entfernen**: ausgewählte Flurstücke entfernen
|
||||
- **Ersetzen**: Verfahrensgebiet vollständig ersetzen
|
||||
- **Abbrechen**: Vorgang beenden
|
||||
|
||||
Returns
|
||||
-------
|
||||
pruef_ergebnis
|
||||
ok=True mit aktion ``verfahrensgebiet_ergaenzen`` / ``verfahrensgebiet_entfernen`` /
|
||||
``verfahrensgebiet_ersetzen``, oder ok=False mit ``verfahrensgebiet_abbrechen``.
|
||||
"""
|
||||
if self.ui_modus != "qgis":
|
||||
return ergebnis
|
||||
|
||||
titel = "Verfahrensgebiet bereits vorhanden"
|
||||
meldung = (
|
||||
ergebnis.meldung
|
||||
or "Ein Verfahrensgebiet-Layer existiert bereits.\nWas soll geschehen?"
|
||||
)
|
||||
|
||||
entscheidung = ask_ergaenzen_entfernen_ersetzen_abbrechen(
|
||||
parent=self.parent,
|
||||
title=titel,
|
||||
message=meldung,
|
||||
)
|
||||
|
||||
aktion_map = {
|
||||
"ergaenzen": "verfahrensgebiet_ergaenzen",
|
||||
"entfernen": "verfahrensgebiet_entfernen",
|
||||
"ersetzen": "verfahrensgebiet_ersetzen",
|
||||
"abbrechen": "verfahrensgebiet_abbrechen",
|
||||
}
|
||||
|
||||
return pruef_ergebnis(
|
||||
ok=(entscheidung != "abbrechen"),
|
||||
aktion=aktion_map[entscheidung],
|
||||
kontext=ergebnis.kontext,
|
||||
)
|
||||
|
||||
def _ask_use_or_replace_pufferlayer(self) -> str:
|
||||
"""
|
||||
Fragt den Nutzer, ob ein vorhandener Pufferlayer verwendet
|
||||
|
||||
+281
-30
@@ -7,16 +7,35 @@ import importlib
|
||||
import math
|
||||
from typing import Any
|
||||
|
||||
from sn_basis.functions.qt_wrapper import QFont
|
||||
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
|
||||
@@ -28,6 +47,10 @@ MM = QgsUnitTypes.LayoutMillimeters
|
||||
class PrintLayout:
|
||||
"""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
|
||||
|
||||
_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"},
|
||||
@@ -61,7 +84,7 @@ class PrintLayout:
|
||||
"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 "
|
||||
"Saechsischen Landesamtes fuer Umwelt, Landwirtschaft und Geologie<br>"
|
||||
"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> "
|
||||
@@ -197,6 +220,7 @@ class PrintLayout:
|
||||
html_mode: bool = False,
|
||||
expression_enabled: bool = False,
|
||||
object_name: str | None = None,
|
||||
v_align: str = "top",
|
||||
) -> Any:
|
||||
if self._current_layout is None:
|
||||
raise RuntimeError("Kein Layout aktiv. Bitte create_single_page_layout() aufrufen.")
|
||||
@@ -212,6 +236,7 @@ class PrintLayout:
|
||||
html_mode=html_mode,
|
||||
expression_enabled=expression_enabled,
|
||||
object_name=object_name,
|
||||
v_align=v_align,
|
||||
)
|
||||
|
||||
def _setup_layout_objects(
|
||||
@@ -220,6 +245,7 @@ class PrintLayout:
|
||||
hauptkarte: Any,
|
||||
map_top_mm: float,
|
||||
map_height_mm: float,
|
||||
kartenname_auswahl: str = "",
|
||||
) -> None:
|
||||
if self._current_layout is None:
|
||||
raise RuntimeError("Kein Layout aktiv.")
|
||||
@@ -239,14 +265,14 @@ class PrintLayout:
|
||||
hauptkarte,
|
||||
legende_x,
|
||||
legende_y,
|
||||
width_mm=180.0,
|
||||
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=180.0,
|
||||
width_limit_mm=self._LEGEND_WIDTH_MM,
|
||||
column_count=1,
|
||||
equal_column_width=False,
|
||||
)
|
||||
@@ -259,7 +285,7 @@ class PrintLayout:
|
||||
if legende_untere_y > quellenangabe_obere_y:
|
||||
self._configure_legend_size(
|
||||
legende,
|
||||
width_limit_mm=180.0,
|
||||
width_limit_mm=self._LEGEND_WIDTH_MM,
|
||||
column_count=2,
|
||||
equal_column_width=True,
|
||||
)
|
||||
@@ -268,7 +294,7 @@ class PrintLayout:
|
||||
self._QUELLENANGABE_TEXT,
|
||||
quellenangabe_x,
|
||||
quellenangabe_y,
|
||||
width_mm=180.0,
|
||||
width_mm=self._LEGEND_WIDTH_MM,
|
||||
height_mm=20.0,
|
||||
text_format="Format_1",
|
||||
html_mode=True,
|
||||
@@ -282,9 +308,9 @@ class PrintLayout:
|
||||
"Kartenschild",
|
||||
kartenschild_x,
|
||||
kartenschild_y,
|
||||
180.0,
|
||||
self._LEGEND_WIDTH_MM,
|
||||
92.0,
|
||||
stroke_width_mm=1.0,
|
||||
stroke_width_mm=self._FRAME_STROKE_WIDTH_MM,
|
||||
)
|
||||
|
||||
kartenschild_line_x, _ = _coords_rel_untere_rechte_ecke(10.0, 0.0)
|
||||
@@ -296,22 +322,27 @@ class PrintLayout:
|
||||
f"Kartenschild_Linie_{index}",
|
||||
kartenschild_line_x,
|
||||
line_y,
|
||||
180.0,
|
||||
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 = [
|
||||
("[% @sn_amt %]", 16.0, -84.0, 171.0, 4.0, "Format_1", "Herausgeber"),
|
||||
(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, -34.0, 137.0, 5.0, "Format_3", "Kartenname"),
|
||||
("[% @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", "Geprueft"),
|
||||
("Geprueft: ", 101.0, -23.0, 81.0, 4.0, "Format_1", "Geprüft"),
|
||||
(
|
||||
"Massstab: 1 : [%round(map_get(item_variables('Hauptkarte'),'map_scale'),0)%]",
|
||||
"Maßstab: 1 : [%round(map_get(item_variables('Hauptkarte'),'map_scale'),0)%]",
|
||||
16.0,
|
||||
-13.0,
|
||||
81.0,
|
||||
@@ -323,7 +354,9 @@ class PrintLayout:
|
||||
("Aktenzeichen: ", 16.0, -3.0, 171.0, 4.0, "Format_1", "Aktenzeichen"),
|
||||
]
|
||||
|
||||
for text, rx, ry, w, h, fmt, obj_name in labels:
|
||||
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,
|
||||
@@ -334,6 +367,7 @@ class PrintLayout:
|
||||
text_format=fmt,
|
||||
expression_enabled=True,
|
||||
object_name=obj_name,
|
||||
v_align=v_align,
|
||||
)
|
||||
|
||||
def _create_label_item(
|
||||
@@ -353,6 +387,7 @@ class PrintLayout:
|
||||
html_mode: bool = False,
|
||||
expression_enabled: bool = False,
|
||||
object_name: str | None = None,
|
||||
v_align: str = "top",
|
||||
) -> Any:
|
||||
template = self._OBJECT_TEMPLATES[template_name]
|
||||
label = QgsLayoutItemLabel(layout)
|
||||
@@ -395,6 +430,25 @@ class PrintLayout:
|
||||
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(
|
||||
@@ -527,7 +581,7 @@ class PrintLayout:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self._set_item_frame_stroke_width(legend, 0.3)
|
||||
self._set_item_frame_stroke_width(legend, self._FRAME_STROKE_WIDTH_MM)
|
||||
|
||||
set_background_enabled = getattr(legend, "setBackgroundEnabled", None)
|
||||
if callable(set_background_enabled):
|
||||
@@ -635,20 +689,9 @@ class PrintLayout:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
width_mm, height_mm = self._get_item_size_mm(legend)
|
||||
resolved_width = width_mm if width_mm is not None else width_limit_mm
|
||||
resolved_height = height_mm if height_mm is not None else 20.0
|
||||
final_width = min(resolved_width, width_limit_mm)
|
||||
|
||||
legend.attemptResize(QgsLayoutSize(final_width, resolved_height, MM))
|
||||
|
||||
if callable(refresh):
|
||||
try:
|
||||
refresh()
|
||||
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:
|
||||
@@ -656,7 +699,211 @@ class PrintLayout:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return updated_height_mm if updated_height_mm is not None else resolved_height
|
||||
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:
|
||||
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:
|
||||
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:
|
||||
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,
|
||||
@@ -720,6 +967,7 @@ class PrintLayout:
|
||||
map_height_mm: float,
|
||||
extent: Any,
|
||||
plotmassstab: float,
|
||||
kartenname_auswahl: str = "",
|
||||
thema: str = "",
|
||||
fit_extent_to_map: bool = True,
|
||||
) -> Any:
|
||||
@@ -776,7 +1024,7 @@ class PrintLayout:
|
||||
set_frame_stroke_width = getattr(hauptkarte, "setFrameStrokeWidth", None)
|
||||
if callable(set_frame_stroke_width):
|
||||
try:
|
||||
self._set_item_frame_stroke_width(hauptkarte, 0.5)
|
||||
self._set_item_frame_stroke_width(hauptkarte, self._FRAME_STROKE_WIDTH_MM)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -813,11 +1061,14 @@ class PrintLayout:
|
||||
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,
|
||||
)
|
||||
|
||||
layout_manager.addLayout(layout)
|
||||
|
||||
@@ -0,0 +1,402 @@
|
||||
"""
|
||||
sn_basis/modules/print_layout_baufreigabe.py - Layout-Erstellung fuer Baufreigabe-Vorlagen.
|
||||
|
||||
Baufreigabe-Layouts verwenden immer feste DIN A3/A4 Seitengroessen im Querformat,
|
||||
ein 120 mm breites Kartenschild (statt 180 mm) und erbenerben die Standard-Logik
|
||||
fuer Hauptkarte, Quellenangabe und Legende vom PrintLayout.
|
||||
|
||||
Atlas-Integration erfolgt nach Layouterstellung durch externe Konfiguration
|
||||
in der PrintLogic.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from sn_basis.functions.qt_wrapper import QFont
|
||||
from sn_basis.functions.qgiscore_wrapper import (
|
||||
QgsLayoutItem,
|
||||
QgsLayoutItemLabel,
|
||||
QgsLayoutItemLegend,
|
||||
QgsLayoutItemMap,
|
||||
QgsLayoutPoint,
|
||||
QgsLayoutSize,
|
||||
QgsPrintLayout,
|
||||
QgsProject,
|
||||
QgsUnitTypes,
|
||||
)
|
||||
from sn_basis.functions.qgisui_wrapper import open_layout_designer
|
||||
from sn_basis.modules.print_layout import PrintLayout
|
||||
|
||||
|
||||
MM = QgsUnitTypes.LayoutMillimeters
|
||||
|
||||
|
||||
class PrintLayoutBaufreigabe(PrintLayout):
|
||||
"""Erzeugt QGIS-Einzelblatt-Layouts mit fester DIN A3/A4 Querformat-Groesse und 80mm Kartenschild."""
|
||||
|
||||
_FRAME_STROKE_WIDTH_MM = 0.5
|
||||
_KARTENSCHILD_WIDTH_MM = 80.0
|
||||
_KARTENSCHILD_HEIGHT_MM = 58.0
|
||||
_ATLAS_FRAME_MARGIN_MM = 10.0
|
||||
_GRID_INTERVAL_MAP_UNITS = 500.0
|
||||
|
||||
# DIN-Formate: nur A3 und A4 im Querformat
|
||||
_DIN_A3_LANDSCAPE = (420, 297) # Breite x Hoehe (Querformat)
|
||||
_DIN_A4_LANDSCAPE = (297, 210) # Querformat
|
||||
|
||||
_TEXT_FORMAT_TEMPLATES = {
|
||||
"Format_1": {"font_family": "Arial", "font_size": 11, "font_style": "normal"},
|
||||
"Format_2": {"font_family": "Arial", "font_size": 13, "font_style": "bold"},
|
||||
"Format_3": {"font_family": "Arial", "font_size": 16, "font_style": "bold"},
|
||||
}
|
||||
|
||||
_OBJECT_TEMPLATES = {
|
||||
"hauptkarte": {"id": "Hauptkarte", "object_name": "Hauptkarte"},
|
||||
"quellenangabe": {
|
||||
"id": "Quellenangabe",
|
||||
"object_name": "Quellenangabe",
|
||||
"size_mm": (80.0, 100.0),
|
||||
"format": "Format_1",
|
||||
},
|
||||
"textfeld": {
|
||||
"id": "Textfeld",
|
||||
"object_name": "Textfeld",
|
||||
"size_mm": (60.0, 8.0),
|
||||
"format": "Format_1",
|
||||
"html_mode": True,
|
||||
"text": "",
|
||||
},
|
||||
"legende": {
|
||||
"id": "Legende",
|
||||
"object_name": "Legende",
|
||||
"size_mm": (80.0, 60.0),
|
||||
},
|
||||
}
|
||||
|
||||
_QUELLENANGABE_TEXT = (
|
||||
"Quelle Geobasisdaten: GeoSN, "
|
||||
"<a href=\"https://www.govdata.de/dl-de/by-2-0\">dl-de/by-2-0</a><br>"
|
||||
"Quelle Fachdaten: Darstellung auf der Grundlage von Daten und mit Erlaubnis des "
|
||||
"Sächsischen Landesamtes für Umwelt, Landwirtschaft und Geologie<br>"
|
||||
"Basemap:"
|
||||
"© GeoBasis-DE / <a href=\"https://www.bkg.bund.de\">BKG</a> ([%year($now)%]) "
|
||||
"<a href=\"https://creativecommons.org/licences/by/4.0/\">CC BY 4.0</a> "
|
||||
"mit teilweise angepasster Signatur<br>"
|
||||
)
|
||||
|
||||
def __init__(self, project: Any | None = None) -> None:
|
||||
super().__init__(project)
|
||||
|
||||
def create_baufreigabe_layout(
|
||||
self,
|
||||
name: str,
|
||||
din_format: str,
|
||||
map_width_mm: float,
|
||||
map_height_mm: float,
|
||||
extent: Any,
|
||||
plotmassstab: float,
|
||||
thema: str = "",
|
||||
fit_extent_to_map: bool = True,
|
||||
atlas_driven: bool = True,
|
||||
) -> Any:
|
||||
"""Erstellt ein Baufreigabe-Layout mit fester DIN A3/A4 Querformat-Groesse.
|
||||
|
||||
Args:
|
||||
name: Name des Layouts
|
||||
din_format: "DIN A3" oder "DIN A4"
|
||||
map_width_mm: Breite der Hauptkarte
|
||||
map_height_mm: Hoehe der Hauptkarte
|
||||
extent: QgsRectangle der Kartenausdehnung
|
||||
plotmassstab: Plotmassstab (z.B. 5000 fuer 1:5000)
|
||||
thema: Name des Visibility-Presets (optional)
|
||||
fit_extent_to_map: Extent bei Bedarf auf das Karten-Seitenverhaeltnis erweitern
|
||||
|
||||
Returns:
|
||||
QgsPrintLayout objektInstance (wird im Layout-Dialog geoeffnet)
|
||||
|
||||
Raises:
|
||||
ValueError: Falls Name bereits existiert oder Format ungueltig ist
|
||||
"""
|
||||
# Seitenformat auswaehlen (nur A3/A4 im Querformat)
|
||||
if din_format == "DIN A3":
|
||||
page_width_mm, page_height_mm = self._DIN_A3_LANDSCAPE
|
||||
elif din_format == "DIN A4":
|
||||
page_width_mm, page_height_mm = self._DIN_A4_LANDSCAPE
|
||||
else:
|
||||
raise ValueError(f"Baufreigabe: Format {din_format} ist nicht unterstützt. Nur DIN A3/A4 Querformat.")
|
||||
|
||||
layout_manager = self.project.layoutManager()
|
||||
existing_layout = layout_manager.layoutByName(name)
|
||||
if existing_layout is not None:
|
||||
raise ValueError(f"Eine Vorlage mit der Bezeichnung '{name}' existiert bereits.")
|
||||
|
||||
layout = QgsPrintLayout(self.project)
|
||||
layout.initializeDefaults()
|
||||
layout.setName(name)
|
||||
self._current_layout = layout
|
||||
|
||||
page = layout.pageCollection().page(0)
|
||||
page.setPageSize(QgsLayoutSize(page_width_mm, page_height_mm, MM))
|
||||
|
||||
map_left_mm = 10.0
|
||||
map_top_mm = 10.0
|
||||
map_right_mm = map_left_mm + map_width_mm
|
||||
map_bottom_mm = map_top_mm + map_height_mm
|
||||
kartenfenster_untere_rechte_ecke_mm = (map_right_mm, map_bottom_mm)
|
||||
|
||||
hauptkarte = self._create_map_item(
|
||||
layout,
|
||||
"hauptkarte",
|
||||
map_left_mm,
|
||||
map_top_mm,
|
||||
map_width_mm,
|
||||
map_height_mm,
|
||||
)
|
||||
|
||||
fitted_extent = extent
|
||||
if fit_extent_to_map:
|
||||
fitted_extent = self._fit_extent_to_map_item(extent, map_width_mm, map_height_mm)
|
||||
|
||||
if fitted_extent is not None and hasattr(fitted_extent, "isNull") and callable(fitted_extent.isNull) and not fitted_extent.isNull():
|
||||
try:
|
||||
set_extent = getattr(hauptkarte, "setExtent", None)
|
||||
if callable(set_extent):
|
||||
set_extent(fitted_extent)
|
||||
else:
|
||||
hauptkarte.zoomToExtent(fitted_extent)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if isinstance(plotmassstab, (int, float)) and plotmassstab > 0:
|
||||
try:
|
||||
hauptkarte.setScale(plotmassstab)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Atlas-Steuerung nur aktivieren, wenn Atlas tatsaechlich genutzt wird.
|
||||
# Andernfalls bleiben Extent/Scale unveraendert (z.B. Canvas-Ausschnitt).
|
||||
if atlas_driven:
|
||||
set_atlas_driven_fn = getattr(hauptkarte, "setAtlasDriven", None)
|
||||
if callable(set_atlas_driven_fn):
|
||||
try:
|
||||
set_atlas_driven_fn(True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
set_atlas_scaling_mode = getattr(hauptkarte, "setAtlasScalingMode", None)
|
||||
atlas_mode_auto = getattr(getattr(QgsLayoutItemMap, "AtlasScalingMode", object), "Auto", None)
|
||||
if callable(set_atlas_scaling_mode) and atlas_mode_auto is not None:
|
||||
try:
|
||||
set_atlas_scaling_mode(atlas_mode_auto)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
set_atlas_margin = getattr(hauptkarte, "setAtlasMargin", None)
|
||||
if callable(set_atlas_margin):
|
||||
try:
|
||||
set_atlas_margin(self._calc_atlas_margin_percent(map_width_mm, map_height_mm))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
set_frame_enabled = getattr(hauptkarte, "setFrameEnabled", None)
|
||||
if callable(set_frame_enabled):
|
||||
try:
|
||||
set_frame_enabled(True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
set_frame_stroke_width = getattr(hauptkarte, "setFrameStrokeWidth", None)
|
||||
if callable(set_frame_stroke_width):
|
||||
try:
|
||||
self._set_item_frame_stroke_width(hauptkarte, self._FRAME_STROKE_WIDTH_MM)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if thema and thema != "aktuell":
|
||||
follow_theme = getattr(hauptkarte, "setFollowVisibilityPreset", None)
|
||||
set_theme_name = getattr(hauptkarte, "setFollowVisibilityPresetName", None)
|
||||
if callable(follow_theme):
|
||||
try:
|
||||
follow_theme(True)
|
||||
except Exception:
|
||||
pass
|
||||
if callable(set_theme_name):
|
||||
try:
|
||||
set_theme_name(thema)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
set_keep_layer_set = getattr(hauptkarte, "setKeepLayerSet", None)
|
||||
if callable(set_keep_layer_set):
|
||||
try:
|
||||
set_keep_layer_set(True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
set_refresh_strategy = getattr(hauptkarte, "setRefreshStrategy", None)
|
||||
if callable(set_refresh_strategy):
|
||||
refresh_cache = (
|
||||
getattr(hauptkarte, "RefreshLaterOnly", None)
|
||||
or getattr(hauptkarte, "RefreshWhenRequested", None)
|
||||
or 0
|
||||
)
|
||||
try:
|
||||
set_refresh_strategy(refresh_cache)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self._configure_hauptkarte_grids(hauptkarte)
|
||||
|
||||
self._setup_layout_objects_baufreigabe(
|
||||
kartenfenster_untere_rechte_ecke_mm,
|
||||
hauptkarte,
|
||||
map_top_mm,
|
||||
map_height_mm,
|
||||
)
|
||||
|
||||
layout_manager.addLayout(layout)
|
||||
self._current_layout = None
|
||||
open_layout_designer(layout)
|
||||
return layout
|
||||
|
||||
def _setup_layout_objects_baufreigabe(
|
||||
self,
|
||||
kartenfenster_untere_rechte_ecke_mm: tuple,
|
||||
hauptkarte: Any,
|
||||
map_top_mm: float,
|
||||
map_height_mm: float,
|
||||
) -> None:
|
||||
"""Erstellt Legende, Quellenangabe und Kartenschild fuer Baufreigabe-Layout.
|
||||
|
||||
Angepasst fuer 80mm Kartenschild statt 180mm.
|
||||
"""
|
||||
if self._current_layout is None:
|
||||
raise RuntimeError("Kein Layout aktiv.")
|
||||
|
||||
def _coords_rel_untere_rechte_ecke(rel_x_mm: float, rel_y_mm: float) -> tuple[float, float]:
|
||||
return (
|
||||
kartenfenster_untere_rechte_ecke_mm[0] + rel_x_mm,
|
||||
kartenfenster_untere_rechte_ecke_mm[1] + rel_y_mm,
|
||||
)
|
||||
|
||||
# Legende: 80mm Breite, hoehe adaptive wie Standard
|
||||
legende_x = kartenfenster_untere_rechte_ecke_mm[0] + 10.0
|
||||
legende_height_mm = max(map_height_mm - 122.0, 20.0)
|
||||
legende_y = map_top_mm
|
||||
legende = self._create_legend_item(
|
||||
self._current_layout,
|
||||
"legende",
|
||||
hauptkarte,
|
||||
legende_x,
|
||||
legende_y,
|
||||
width_mm=self._KARTENSCHILD_WIDTH_MM,
|
||||
height_mm=legende_height_mm,
|
||||
object_name="Legende",
|
||||
)
|
||||
|
||||
legende_hoehe_mm = self._configure_legend_size(
|
||||
legende,
|
||||
width_limit_mm=self._KARTENSCHILD_WIDTH_MM,
|
||||
column_count=1,
|
||||
equal_column_width=False,
|
||||
)
|
||||
|
||||
# Quellenangabe: 80mm Breite
|
||||
quellenangabe_x, quellenangabe_y = _coords_rel_untere_rechte_ecke(10.0, -102.0)
|
||||
quellenangabe_hoehe_mm = 20.0
|
||||
quellenangabe_obere_y = quellenangabe_y - quellenangabe_hoehe_mm
|
||||
legende_untere_y = legende_y + legende_hoehe_mm
|
||||
|
||||
if legende_untere_y > quellenangabe_obere_y:
|
||||
self._configure_legend_size(
|
||||
legende,
|
||||
width_limit_mm=self._KARTENSCHILD_WIDTH_MM,
|
||||
column_count=2,
|
||||
equal_column_width=True,
|
||||
)
|
||||
|
||||
self.add_text_label(
|
||||
self._QUELLENANGABE_TEXT,
|
||||
quellenangabe_x,
|
||||
quellenangabe_y,
|
||||
width_mm=self._KARTENSCHILD_WIDTH_MM,
|
||||
height_mm=20.0,
|
||||
text_format="Format_1",
|
||||
html_mode=True,
|
||||
expression_enabled=True,
|
||||
object_name="Quellenangabe",
|
||||
)
|
||||
|
||||
# Kartenschild: 80mm Breite
|
||||
kartenschild_x, kartenschild_y = _coords_rel_untere_rechte_ecke(10.0, 0.0)
|
||||
self._create_rectangle_box_item(
|
||||
self._current_layout,
|
||||
"Kartenschild",
|
||||
kartenschild_x,
|
||||
kartenschild_y,
|
||||
self._KARTENSCHILD_WIDTH_MM,
|
||||
self._KARTENSCHILD_HEIGHT_MM,
|
||||
stroke_width_mm=self._FRAME_STROKE_WIDTH_MM,
|
||||
)
|
||||
|
||||
# Kartenschild-Linien: 6 Trennlinien (Positionen gleich)
|
||||
kartenschild_line_x, _ = _coords_rel_untere_rechte_ecke(10.0, 0.0)
|
||||
kartenschild_line_offsets_y = (-10.0, -20.0, -30.0, -46.0)
|
||||
for index, offset_y in enumerate(kartenschild_line_offsets_y, start=1):
|
||||
_, line_y = _coords_rel_untere_rechte_ecke(0.0, offset_y)
|
||||
self._create_rectangle_box_item(
|
||||
self._current_layout,
|
||||
f"Kartenschild_Linie_{index}",
|
||||
kartenschild_line_x,
|
||||
line_y,
|
||||
self._KARTENSCHILD_WIDTH_MM,
|
||||
0.0,
|
||||
stroke_width_mm=0.1,
|
||||
)
|
||||
|
||||
# Textfelder im Kartenschild ohne Skalierungslogik.
|
||||
# Positionen werden bewusst als feste Werte gesetzt und bei Bedarf manuell angepasst.
|
||||
|
||||
labels = [
|
||||
("Teilnehmergemeinschaft [%@sn_bezeichnung %] [%@sn_name %]", 14.0, -48.0, 74.0, 8.0, "Format_1", "Herausgeber","Bottom"),
|
||||
("[% @sn_bezeichnung %]", 14.0, -41.0, 74.0, 4.0, "Format_1", "Bezeichnung_Flurbereinigung"),
|
||||
("[% @sn_name %]", 14.0, -32.0, 74.0, 5.0, "Format_2", "Name_Flurbereinigung"),
|
||||
("[% @sn_kartenname %]", 14.0, -23.0, 74.0, 6.0, "Format_3", "Kartenname"),
|
||||
("Ausgabedatum: [%format_date(now(), 'dd.MM.yyyy')%]", 14.0, -13.0, 74.0, 4.0, "Format_1", "Ausgabedatum"),
|
||||
(
|
||||
"Maßstab: 1 : [%round(map_get(item_variables('Hauptkarte'),'map_scale'),0)%]",
|
||||
14.0,
|
||||
-3.0,
|
||||
74.0,
|
||||
4.0,
|
||||
"Format_1",
|
||||
"Massstab",
|
||||
),
|
||||
|
||||
]
|
||||
|
||||
for entry in labels:
|
||||
text, rx, ry, w, h, fmt, obj_name = entry[:7]
|
||||
v_align = entry[7] if len(entry) > 7 else "top"
|
||||
x, y = _coords_rel_untere_rechte_ecke(rx, ry)
|
||||
self.add_text_label(
|
||||
text,
|
||||
x,
|
||||
y,
|
||||
width_mm=w,
|
||||
height_mm=h,
|
||||
text_format=fmt,
|
||||
expression_enabled=True,
|
||||
object_name=obj_name,
|
||||
v_align=v_align,
|
||||
)
|
||||
|
||||
def _calc_atlas_margin_percent(self, map_width_mm: float, map_height_mm: float) -> float:
|
||||
"""Berechnet Atlas-Margin in Prozent, sodass etwa 10 mm Rand sichtbar bleiben."""
|
||||
margin_mm = self._ATLAS_FRAME_MARGIN_MM
|
||||
if map_width_mm <= 2 * margin_mm or map_height_mm <= 2 * margin_mm:
|
||||
return 5.0
|
||||
|
||||
p_width = (100.0 * margin_mm) / (map_width_mm - 2.0 * margin_mm)
|
||||
p_height = (100.0 * margin_mm) / (map_height_mm - 2.0 * margin_mm)
|
||||
return max(p_width, p_height)
|
||||
+680
-56
@@ -4,7 +4,7 @@ sn_basis/modules/print_logic.py - Fachlogik fuer den Druck-Tab (nur Einzelblatt)
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
from typing import Any, cast
|
||||
from typing import Any, cast, TypedDict
|
||||
|
||||
from sn_basis.functions.qgiscore_wrapper import QgsProject, get_layer_extent
|
||||
from sn_basis.functions.qgisui_wrapper import iface
|
||||
@@ -12,6 +12,48 @@ from sn_basis.functions.variable_wrapper import get_variable, set_variable
|
||||
from sn_basis.modules.layerpruefer import Layerpruefer
|
||||
from sn_basis.modules.Pruefmanager import Pruefmanager
|
||||
from sn_basis.modules.print_layout import PrintLayout
|
||||
from sn_basis.modules.print_layout_baufreigabe import PrintLayoutBaufreigabe
|
||||
|
||||
|
||||
class AtlasConfig(TypedDict, total=False):
|
||||
"""Strukturierte Konfiguration fuer Atlas-Optionen.
|
||||
|
||||
Alle Felder sind optional (total=False) und haben vordefinierte Standartwerte.
|
||||
"""
|
||||
# Basis-Konfiguration
|
||||
enabled: bool # Atlas aktivieren (Standard: True nach Coverage-Layer-Setzung)
|
||||
page_name_expression: str # QGIS Expression für Seitennamen (z.B. "@atlas_featurenumber")
|
||||
|
||||
# Filterung
|
||||
filter_features: bool # Features filtern? (Standard: False)
|
||||
filter_expression: str # QGIS Expression für Feature-Filter
|
||||
|
||||
# Sortierung
|
||||
sort_features: bool # Features sortieren? (Standard: False)
|
||||
sort_expression: str # QGIS Expression/Feldname zum Sortieren
|
||||
sort_ascending: bool # Aufsteigend sortieren? (Standard: True)
|
||||
|
||||
# Rendering
|
||||
hide_coverage: bool # Coverage-Layer in Layout verstecken? (Standard: False)
|
||||
limit_render_to_current_feature: bool # Rendering auf aktuelles Feature limitieren (QGIS 4.0+, Standard: False)
|
||||
|
||||
# Metadaten
|
||||
description: str # Beschreibung der Atlas-Konfiguration
|
||||
|
||||
|
||||
# Vordefinierte Standardkonfigurationen für Baufreigabe
|
||||
ATLAS_CONFIG_BAUFREIGABE_DEFAULT: AtlasConfig = {
|
||||
"enabled": True,
|
||||
"page_name_expression": "@atlas_featurenumber",
|
||||
"filter_features": False,
|
||||
"filter_expression": "",
|
||||
"sort_features": False,
|
||||
"sort_expression": "",
|
||||
"sort_ascending": True,
|
||||
"hide_coverage": False,
|
||||
"limit_render_to_current_feature": True,
|
||||
"description": "Baufreigabe Atlas mit Standardkonfiguration",
|
||||
}
|
||||
|
||||
|
||||
KARTENNAME_VAR = "kartenname"
|
||||
@@ -19,22 +61,26 @@ PLOTMASSSTAB_VAR = "plotmassstab"
|
||||
VIEW_VAR = "view"
|
||||
ZIELGROESSE_VAR = "zielgroesse"
|
||||
FORMFAKTOR_VAR = "formfaktor"
|
||||
ATLAS_ERZEUGEN_UI_VAR = "tab_b_atlas_erzeugen"
|
||||
DRUCKBEREICH_VAR = "druckbereich"
|
||||
DRUCKPUFFER_VAR = "druckpuffer"
|
||||
DRUCKRECHTECK_LAYER_NAME = "Druckrechteck"
|
||||
|
||||
KARTENNAME_38 = "§38"
|
||||
KARTENNAME_41 = "§41"
|
||||
KARTENNAME_BAUFREIGABE = "Anlage zur Baufreigabe"
|
||||
MASSSTAB_WIE_KARTENFENSTER = "Wie Kartenfenster"
|
||||
THEMA_WIE_KARTENFENSTER = "wie kartenfenster"
|
||||
|
||||
DRUCKBEREICH_VERFAHRENSGEBIET = "Verfahrensgebiet"
|
||||
DRUCKBEREICH_AKTUELLE_AUSWAHL = "aktuelle Auswahl"
|
||||
DRUCKBEREICH_RECHTECK = "Rechteck"
|
||||
DRUCKBEREICH_WIE_KARTENFENSTER = "Wie Kartenfenster"
|
||||
|
||||
KARTENNAME_BY_AUSWAHL = {
|
||||
KARTENNAME_38: "Planungsuebersicht §38 FlurbG",
|
||||
KARTENNAME_41: "Karte zum Plan ueber die gemeinschaftlichen und oeffentlichen Anlagen (§ 41 FlurbG)",
|
||||
KARTENNAME_38: "Planungsübersicht §38 FlurbG",
|
||||
KARTENNAME_41: "Karte zum Plan über die gemeinschaftlichen und öffentlichen Anlagen (§ 41 FlurbG)",
|
||||
KARTENNAME_BAUFREIGABE: "Anlage zur Baufreigabe",
|
||||
}
|
||||
|
||||
PLOTMASSSTAB_BY_AUSWAHL = {
|
||||
@@ -54,8 +100,16 @@ DIN_GROESSEN: dict[str, tuple[int, int]] = {
|
||||
"DIN A3": (297, 420),
|
||||
"DIN A4": (210, 297),
|
||||
}
|
||||
DIN_AUTO_REIHENFOLGE = ("DIN A4", "DIN A3", "DIN A2", "DIN A1", "DIN A0")
|
||||
DIN_STANDARD = "DIN A0"
|
||||
DRUCKPUFFER_STANDARD_CM = 1
|
||||
ENDLOSROLLE_MAX_BREITE_MM = 2000.0
|
||||
STANDARD_LAYOUT_FIXBREITE_MM = 210.0
|
||||
STANDARD_LAYOUT_FIXHOEHE_MM = 20.0
|
||||
BAUFREIGABE_LAYOUT_FIXBREITE_MM = 110.0
|
||||
BAUFREIGABE_LAYOUT_FIXHOEHE_MM = 20.0
|
||||
STANDARD_A4_LANDSCAPE_W_MM = 297.0
|
||||
STANDARD_A4_LANDSCAPE_H_MM = 210.0
|
||||
|
||||
|
||||
class PrintLogic:
|
||||
@@ -66,6 +120,21 @@ class PrintLogic:
|
||||
self._rectangle_tool: Any | None = None
|
||||
self._previous_map_tool: Any | None = None
|
||||
|
||||
def _failed_result(self) -> dict:
|
||||
return {"ok": False, "switch_to_tab_a": False, "atlas_seiten": 0}
|
||||
|
||||
def _show_target_size_too_small(self) -> dict:
|
||||
self.pruefmanager.zeige_hinweis(
|
||||
"Zielgroesse zu klein",
|
||||
"Die gewaehlte Zielgroesse reicht fuer den Einzelblatt-Druck nicht aus.",
|
||||
)
|
||||
return self._failed_result()
|
||||
|
||||
def _is_enabled_flag(self, raw_value: Any) -> bool:
|
||||
if raw_value in (None, ""):
|
||||
return True
|
||||
return str(raw_value).strip().lower() in {"1", "true", "ja", "yes", "on"}
|
||||
|
||||
def _resolve_layer_from_project(self) -> object | None:
|
||||
project = QgsProject.instance()
|
||||
# Primär: neue zentrale Variable aus sn_verfahrensgebiet_manager
|
||||
@@ -253,6 +322,295 @@ class PrintLogic:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def create_atlas_layer_from_selection(self, source_layer: object | None = None) -> tuple[object | None, str]:
|
||||
"""Erstellt einen neuen Layer 'Atlaslayer' aus der Auswahl des Quell-Layers.
|
||||
|
||||
Args:
|
||||
source_layer: Der Layer mit der Auswahl (wenn None, wird aktiver Layer verwendet)
|
||||
|
||||
Returns:
|
||||
(created_layer, layer_id) oder (None, "") bei Fehler
|
||||
"""
|
||||
# Source-Layer bestimmen
|
||||
if source_layer is None:
|
||||
source_layer = self._resolve_active_layer()
|
||||
if source_layer is None:
|
||||
self.pruefmanager.zeige_hinweis(
|
||||
"Atlas Layer erstellen",
|
||||
"Kein Layer mit Auswahl gefunden.",
|
||||
)
|
||||
return None, ""
|
||||
|
||||
# Pruefen ob Auswahl vorhanden ist
|
||||
selected_count = getattr(source_layer, "selectedFeatureCount", None)
|
||||
if callable(selected_count):
|
||||
try:
|
||||
if int(cast(Any, selected_count())) == 0:
|
||||
self.pruefmanager.zeige_hinweis(
|
||||
"Atlas Layer erstellen",
|
||||
"Auf dem Layer sind keine Features ausgewaehlt.",
|
||||
)
|
||||
return None, ""
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Bestehenden Atlaslayer loeschen (optional Wiederverwedung)
|
||||
existing_atlas = self._get_existing_layer_by_name("Atlaslayer")
|
||||
if existing_atlas is not None:
|
||||
try:
|
||||
QgsProject.instance().removeMapLayer(existing_atlas)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Neuen Memory-Layer mit Geometrie-Type des Quell-Layers erstellen
|
||||
qgs_vector_layer = self._import_qgis_symbol("qgis.core", "QgsVectorLayer")
|
||||
if qgs_vector_layer is None:
|
||||
self.pruefmanager.zeige_hinweis(
|
||||
"Atlas Layer erstellen",
|
||||
"QgsVectorLayer ist nicht verfuegbar.",
|
||||
)
|
||||
return None, ""
|
||||
|
||||
# CRS vom Quell-Layer übernehmen
|
||||
crs_authid = "EPSG:25832"
|
||||
try:
|
||||
source_crs = getattr(source_layer, "crs", lambda: None)()
|
||||
if source_crs is not None:
|
||||
crs_authid_method = getattr(source_crs, "authid", lambda: None)
|
||||
if callable(crs_authid_method):
|
||||
crs_authid = crs_authid_method() or crs_authid
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Geometrie-Type vom Quell-Layer bestimmen (vereinfacht: Polygon für Atlas)
|
||||
# TODO: Intelligentere Bestimmung des Geometrie-Types
|
||||
atlas_layer = qgs_vector_layer(f"Polygon?crs={crs_authid}", "Atlaslayer", "memory")
|
||||
if not atlas_layer or not getattr(atlas_layer, "isValid", lambda: False)():
|
||||
self.pruefmanager.zeige_hinweis(
|
||||
"Atlas Layer erstellen",
|
||||
"Der Atlaslayer konnte nicht erstellt werden.",
|
||||
)
|
||||
return None, ""
|
||||
|
||||
# Ausgewaehlte Features kopieren
|
||||
try:
|
||||
get_selected = getattr(source_layer, "selectedFeatures", None)
|
||||
if not callable(get_selected):
|
||||
raise Exception("selectedFeatures ist nicht aufrufbar")
|
||||
|
||||
selected_features = list(cast(Any, get_selected()))
|
||||
if not selected_features:
|
||||
raise Exception("Keine Features ausgewaehlt")
|
||||
|
||||
qgs_feature = self._import_qgis_symbol("qgis.core", "QgsFeature")
|
||||
if qgs_feature is None:
|
||||
raise Exception("QgsFeature Symbol nicht verfuegbar")
|
||||
|
||||
data_provider = getattr(atlas_layer, "dataProvider", lambda: None)()
|
||||
if data_provider is None:
|
||||
raise Exception("dataProvider nicht verfuegbar")
|
||||
|
||||
new_features = []
|
||||
for source_feature in selected_features:
|
||||
geometry = getattr(source_feature, "geometry", lambda: None)()
|
||||
if geometry is None:
|
||||
continue
|
||||
new_feature = qgs_feature()
|
||||
set_geometry = getattr(new_feature, "setGeometry", None)
|
||||
if callable(set_geometry):
|
||||
try:
|
||||
set_geometry(geometry)
|
||||
new_features.append(new_feature)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if new_features:
|
||||
add_features = getattr(data_provider, "addFeatures", None)
|
||||
if callable(add_features):
|
||||
add_features(new_features)
|
||||
|
||||
update_fields = getattr(atlas_layer, "updateFields", None)
|
||||
if callable(update_fields):
|
||||
update_fields()
|
||||
|
||||
update_extents = getattr(atlas_layer, "updateExtents", None)
|
||||
if callable(update_extents):
|
||||
update_extents()
|
||||
|
||||
except Exception as exc:
|
||||
self.pruefmanager.zeige_hinweis(
|
||||
"Atlas Layer erstellen",
|
||||
f"Fehler beim Kopieren der Features: {exc}",
|
||||
)
|
||||
return None, ""
|
||||
|
||||
# Layer ins Projekt aufnehmen
|
||||
try:
|
||||
QgsProject.instance().addMapLayer(atlas_layer)
|
||||
except Exception:
|
||||
self.pruefmanager.zeige_hinweis(
|
||||
"Atlas Layer erstellen",
|
||||
"Der Layer konnte nicht ins Projekt aufgenommen werden.",
|
||||
)
|
||||
return None, ""
|
||||
|
||||
# Layer-ID speichern
|
||||
layer_id = getattr(atlas_layer, "id", lambda: "")() or ""
|
||||
set_variable("sn_abdeckungslayer", layer_id, scope="project")
|
||||
|
||||
self.pruefmanager.zeige_hinweis(
|
||||
"Atlas Layer erstellen",
|
||||
f"Atlas Layer 'Atlaslayer' erfolgreich erstellt.",
|
||||
)
|
||||
|
||||
return atlas_layer, layer_id
|
||||
|
||||
def configure_atlas_for_baufreigabe_layout(
|
||||
self,
|
||||
layout_name: str = "",
|
||||
atlas_config: AtlasConfig | None = None,
|
||||
) -> bool:
|
||||
"""Konfiguriert den Atlas fuer ein Baufreigabe-Layout mit erweiterten Optionen.
|
||||
|
||||
Wird nach erfolgreicher Baufreigabe-Layout-Erstellung aufgerufen.
|
||||
Setzt den Coverage-Layer (aus sn_abdeckungslayer Variable) und konfiguriert den Atlas
|
||||
mit allen Optionen aus der AtlasConfig.
|
||||
"""
|
||||
try:
|
||||
if atlas_config is None:
|
||||
atlas_config = ATLAS_CONFIG_BAUFREIGABE_DEFAULT
|
||||
|
||||
project = QgsProject.instance()
|
||||
layout_manager = getattr(project, "layoutManager", lambda: None)()
|
||||
if layout_manager is None:
|
||||
self.pruefmanager.zeige_hinweis("Atlas konfigurieren", "Layout Manager nicht verfuegbar.")
|
||||
return False
|
||||
|
||||
if not layout_name:
|
||||
layouts_fn = getattr(layout_manager, "layouts", None)
|
||||
if not callable(layouts_fn):
|
||||
return False
|
||||
try:
|
||||
layout_iterable = layouts_fn()
|
||||
if layout_iterable is None:
|
||||
return False
|
||||
last_layout: Any | None = None
|
||||
for candidate_layout in cast(Any, layout_iterable):
|
||||
last_layout = candidate_layout
|
||||
if last_layout is None:
|
||||
return False
|
||||
layout = last_layout
|
||||
except Exception:
|
||||
return False
|
||||
else:
|
||||
layout_by_name = getattr(layout_manager, "layoutByName", None)
|
||||
if not callable(layout_by_name):
|
||||
return False
|
||||
layout = layout_by_name(layout_name)
|
||||
if layout is None:
|
||||
return False
|
||||
|
||||
atlas = getattr(layout, "atlas", lambda: None)()
|
||||
if atlas is None:
|
||||
return False
|
||||
|
||||
atlas_enabled = bool(atlas_config.get("enabled", True))
|
||||
if not atlas_enabled:
|
||||
set_enabled = getattr(atlas, "setEnabled", None)
|
||||
if callable(set_enabled):
|
||||
try:
|
||||
set_enabled(False)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Hauptkarte ebenfalls aus Atlas-Steuerung lösen
|
||||
item_by_id = getattr(layout, "itemById", None)
|
||||
if callable(item_by_id):
|
||||
try:
|
||||
hauptkarte = item_by_id("Hauptkarte")
|
||||
set_atlas_driven = getattr(hauptkarte, "setAtlasDriven", None)
|
||||
if callable(set_atlas_driven):
|
||||
set_atlas_driven(False)
|
||||
except Exception:
|
||||
pass
|
||||
return True
|
||||
|
||||
coverage_layer_id = get_variable("sn_abdeckungslayer", scope="project") or ""
|
||||
if not coverage_layer_id:
|
||||
return True
|
||||
|
||||
coverage_layer = project.mapLayer(coverage_layer_id)
|
||||
if coverage_layer is None:
|
||||
self.pruefmanager.zeige_hinweis(
|
||||
"Atlas konfigurieren",
|
||||
f"Atlas-Layer mit ID '{coverage_layer_id}' nicht gefunden.",
|
||||
)
|
||||
return False
|
||||
|
||||
set_coverage = getattr(atlas, "setCoverageLayer", None)
|
||||
if callable(set_coverage):
|
||||
set_coverage(coverage_layer)
|
||||
|
||||
page_name_expr = atlas_config.get("page_name_expression", "")
|
||||
if page_name_expr:
|
||||
set_page_name = getattr(atlas, "setPageNameExpression", None)
|
||||
if callable(set_page_name):
|
||||
set_page_name(page_name_expr)
|
||||
|
||||
if atlas_config.get("filter_features", False):
|
||||
set_filter_features = getattr(atlas, "setFilterFeatures", None)
|
||||
if callable(set_filter_features):
|
||||
set_filter_features(True)
|
||||
filter_expr = atlas_config.get("filter_expression", "")
|
||||
if filter_expr:
|
||||
set_filter_expr = getattr(atlas, "setFilterExpression", None)
|
||||
if callable(set_filter_expr):
|
||||
set_filter_expr(filter_expr)
|
||||
|
||||
if atlas_config.get("sort_features", False):
|
||||
set_sort_features = getattr(atlas, "setSortFeatures", None)
|
||||
if callable(set_sort_features):
|
||||
set_sort_features(True)
|
||||
sort_expr = atlas_config.get("sort_expression", "")
|
||||
if sort_expr:
|
||||
set_sort_expr = getattr(atlas, "setSortExpression", None)
|
||||
if callable(set_sort_expr):
|
||||
set_sort_expr(sort_expr)
|
||||
set_sort_asc = getattr(atlas, "setSortAscending", None)
|
||||
if callable(set_sort_asc):
|
||||
set_sort_asc(atlas_config.get("sort_ascending", True))
|
||||
|
||||
set_hide_coverage = getattr(atlas, "setHideCoverage", None)
|
||||
if callable(set_hide_coverage):
|
||||
set_hide_coverage(atlas_config.get("hide_coverage", False))
|
||||
|
||||
set_limit_render = getattr(atlas, "setLimitCoverageLayerRenderToCurrentFeature", None)
|
||||
if callable(set_limit_render):
|
||||
try:
|
||||
set_limit_render(atlas_config.get("limit_render_to_current_feature", False))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
num_features = 0
|
||||
update_features = getattr(atlas, "updateFeatures", None)
|
||||
if callable(update_features):
|
||||
try:
|
||||
num_features = update_features() or 0
|
||||
except Exception:
|
||||
num_features = 0
|
||||
|
||||
set_enabled = getattr(atlas, "setEnabled", None)
|
||||
if callable(set_enabled):
|
||||
set_enabled(atlas_config.get("enabled", True))
|
||||
return True
|
||||
|
||||
except Exception as exc:
|
||||
self.pruefmanager.zeige_hinweis(
|
||||
"Atlas konfigurieren",
|
||||
f"Fehler beim Konfigurieren des Atlas: {exc}",
|
||||
)
|
||||
return False
|
||||
|
||||
def _store_druckrechteck(self, layer: object, rectangle: Any) -> bool:
|
||||
qgs_feature = self._import_qgis_symbol("qgis.core", "QgsFeature")
|
||||
qgs_geometry = self._import_qgis_symbol("qgis.core", "QgsGeometry")
|
||||
@@ -504,12 +862,69 @@ class PrintLogic:
|
||||
return None
|
||||
return self._build_rectangle(x_min, y_min, x_max, y_max)
|
||||
|
||||
def _get_canvas_extent(self) -> Any | None:
|
||||
map_canvas_fn = getattr(iface, "mapCanvas", None)
|
||||
if not callable(map_canvas_fn):
|
||||
return None
|
||||
|
||||
try:
|
||||
canvas = map_canvas_fn()
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
if canvas is None:
|
||||
return None
|
||||
|
||||
extent_fn = getattr(canvas, "extent", None)
|
||||
if not callable(extent_fn):
|
||||
return None
|
||||
|
||||
try:
|
||||
extent = extent_fn()
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
if extent is None:
|
||||
return None
|
||||
|
||||
is_null = getattr(extent, "isNull", None)
|
||||
if callable(is_null):
|
||||
try:
|
||||
if is_null():
|
||||
return None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
width_fn = getattr(extent, "width", None)
|
||||
height_fn = getattr(extent, "height", None)
|
||||
if not callable(width_fn) or not callable(height_fn):
|
||||
return None
|
||||
try:
|
||||
width_value = float(cast(Any, width_fn()))
|
||||
height_value = float(cast(Any, height_fn()))
|
||||
if width_value <= 0 or height_value <= 0:
|
||||
return None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
return extent
|
||||
|
||||
def _resolve_extent(
|
||||
self,
|
||||
resolved_layer: object | None,
|
||||
druckbereich_auswahl: str,
|
||||
plotmassstab: float,
|
||||
) -> tuple[Any | None, object | None]:
|
||||
if druckbereich_auswahl == DRUCKBEREICH_WIE_KARTENFENSTER:
|
||||
extent = self._get_canvas_extent()
|
||||
if extent is None:
|
||||
self.pruefmanager.zeige_hinweis(
|
||||
"Druckbereich",
|
||||
"Das aktuelle Kartenfenster liefert keine gueltige Ausdehnung.",
|
||||
)
|
||||
return None, None
|
||||
return extent, None
|
||||
|
||||
if druckbereich_auswahl == DRUCKBEREICH_AKTUELLE_AUSWAHL:
|
||||
selection_layer = self._resolve_selection_layer(resolved_layer)
|
||||
extent = self._get_selected_extent(selection_layer)
|
||||
@@ -526,7 +941,7 @@ class PrintLogic:
|
||||
if extent is None:
|
||||
self.activate_druckrechteck_capture(resolved_layer)
|
||||
return None, None
|
||||
return self._buffer_extent(extent, plotmassstab), None
|
||||
return extent, None
|
||||
|
||||
lp = Layerpruefer(layer=resolved_layer)
|
||||
ergebnis = lp.pruefe()
|
||||
@@ -573,12 +988,176 @@ class PrintLogic:
|
||||
def set_druckbereich_for_auswahl(self, auswahl: str) -> None:
|
||||
if auswahl not in {
|
||||
DRUCKBEREICH_VERFAHRENSGEBIET,
|
||||
DRUCKBEREICH_WIE_KARTENFENSTER,
|
||||
DRUCKBEREICH_AKTUELLE_AUSWAHL,
|
||||
DRUCKBEREICH_RECHTECK,
|
||||
}:
|
||||
auswahl = DRUCKBEREICH_VERFAHRENSGEBIET
|
||||
set_variable(DRUCKBEREICH_VAR, auswahl, scope="project")
|
||||
|
||||
def _get_baufreigabe_page_dimensions(
|
||||
self,
|
||||
required_map_width_mm: float,
|
||||
required_map_height_mm: float,
|
||||
max_format: str = "DIN A3",
|
||||
) -> tuple[str, float, float, float, float, bool]:
|
||||
"""Waehlt fuer Baufreigabe ein Blatt im Querformat.
|
||||
|
||||
Args:
|
||||
required_map_width_mm: benoetigte Breite der Hauptkarte in mm
|
||||
required_map_height_mm: benoetigte Hoehe der Hauptkarte in mm
|
||||
max_format: maximale Blattgroesse, standardmaessig DIN A3
|
||||
|
||||
Returns:
|
||||
(din_format, page_width_mm, page_height_mm, map_w_mm, map_h_mm, fallback_applied)
|
||||
"""
|
||||
candidates = (
|
||||
("DIN A4", 297.0, 210.0),
|
||||
("DIN A3", 420.0, 297.0),
|
||||
)
|
||||
|
||||
try:
|
||||
max_index = [candidate[0] for candidate in candidates].index(max_format)
|
||||
except ValueError:
|
||||
max_index = len(candidates) - 1
|
||||
|
||||
limited_candidates = candidates[: max_index + 1]
|
||||
|
||||
for din_format, page_w, page_h in limited_candidates:
|
||||
map_w = page_w - BAUFREIGABE_LAYOUT_FIXBREITE_MM
|
||||
map_h = page_h - BAUFREIGABE_LAYOUT_FIXHOEHE_MM
|
||||
if map_w >= required_map_width_mm and map_h >= required_map_height_mm:
|
||||
return din_format, page_w, page_h, map_w, map_h, False
|
||||
|
||||
# Wenn der gewuenschte Ausschnitt nicht passt, verwende das maximal erlaubte Blatt
|
||||
# und erweitere die Hauptkarte auf die verfuegbare Kartenflaeche (zentriert per fit_extent_to_map).
|
||||
din_format, page_w, page_h = limited_candidates[-1]
|
||||
map_w = max(page_w - BAUFREIGABE_LAYOUT_FIXBREITE_MM, 50.0)
|
||||
map_h = max(page_h - BAUFREIGABE_LAYOUT_FIXHOEHE_MM, 50.0)
|
||||
return din_format, page_w, page_h, map_w, map_h, True
|
||||
|
||||
def _does_plot_fit_page(
|
||||
self,
|
||||
page_width_mm: float,
|
||||
page_height_mm: float,
|
||||
map_width_mm: float,
|
||||
map_height_mm: float,
|
||||
fixed_width_mm: float,
|
||||
fixed_height_mm: float,
|
||||
) -> bool:
|
||||
required_w = map_width_mm + fixed_width_mm
|
||||
required_h = map_height_mm + fixed_height_mm
|
||||
return required_w <= page_width_mm and required_h <= page_height_mm
|
||||
|
||||
def _get_plot_dimensions_mm(
|
||||
self,
|
||||
map_width_mm: float,
|
||||
map_height_mm: float,
|
||||
fixed_width_mm: float,
|
||||
fixed_height_mm: float,
|
||||
) -> tuple[float, float]:
|
||||
return map_width_mm + fixed_width_mm, map_height_mm + fixed_height_mm
|
||||
|
||||
def _get_required_map_dimensions_mm(self, extent: Any, plotmassstab: float) -> tuple[float, float]:
|
||||
"""Berechnet die benoetigte Kartenfenstergroesse in mm fuer Extent und Massstab."""
|
||||
if extent is None or not isinstance(plotmassstab, (int, float)) or plotmassstab <= 0:
|
||||
return 0.0, 0.0
|
||||
|
||||
try:
|
||||
width_mm = float(extent.width()) * 1000.0 / float(plotmassstab)
|
||||
height_mm = float(extent.height()) * 1000.0 / float(plotmassstab)
|
||||
except Exception:
|
||||
return 0.0, 0.0
|
||||
|
||||
return max(width_mm, 0.0), max(height_mm, 0.0)
|
||||
|
||||
def _select_standard_page_layout(
|
||||
self,
|
||||
required_map_width_mm: float,
|
||||
required_map_height_mm: float,
|
||||
max_format: str,
|
||||
) -> tuple[float, float, float, float] | None:
|
||||
"""Waehlt das kleinste passende DIN-Blatt fuer das Standardlayout."""
|
||||
try:
|
||||
max_index = DIN_AUTO_REIHENFOLGE.index(max_format)
|
||||
except ValueError:
|
||||
max_index = len(DIN_AUTO_REIHENFOLGE) - 1
|
||||
|
||||
for fmt in DIN_AUTO_REIHENFOLGE[: max_index + 1]:
|
||||
base_w, base_h = DIN_GROESSEN[fmt]
|
||||
fitting_candidates: list[tuple[float, float, float, float]] = []
|
||||
for page_w, page_h in ((float(base_w), float(base_h)), (float(base_h), float(base_w))):
|
||||
map_w = page_w - STANDARD_LAYOUT_FIXBREITE_MM
|
||||
map_h = page_h - STANDARD_LAYOUT_FIXHOEHE_MM
|
||||
if map_w >= required_map_width_mm and map_h >= required_map_height_mm:
|
||||
fitting_candidates.append((page_w, page_h, map_w, map_h))
|
||||
|
||||
if fitting_candidates:
|
||||
return min(
|
||||
fitting_candidates,
|
||||
key=lambda candidate: (candidate[2] - required_map_width_mm) + (candidate[3] - required_map_height_mm),
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
def _get_standard_layout_dimensions(
|
||||
self,
|
||||
required_map_width_mm: float,
|
||||
required_map_height_mm: float,
|
||||
max_format: str,
|
||||
formfaktor: bool,
|
||||
) -> tuple[float, float, float, float, bool] | None:
|
||||
if formfaktor:
|
||||
plotgroesse_w, plotgroesse_h = self._get_plot_dimensions_mm(
|
||||
required_map_width_mm,
|
||||
required_map_height_mm,
|
||||
STANDARD_LAYOUT_FIXBREITE_MM,
|
||||
STANDARD_LAYOUT_FIXHOEHE_MM,
|
||||
)
|
||||
din_dims = DIN_GROESSEN.get(max_format, DIN_GROESSEN[DIN_STANDARD])
|
||||
max_page_h = float(max(din_dims))
|
||||
page_w = min(plotgroesse_w, ENDLOSROLLE_MAX_BREITE_MM)
|
||||
page_h = min(plotgroesse_h, max_page_h)
|
||||
return self._apply_standard_minimum_a4_fallback(
|
||||
page_w,
|
||||
page_h,
|
||||
required_map_width_mm,
|
||||
required_map_height_mm,
|
||||
)
|
||||
|
||||
selected_layout = self._select_standard_page_layout(
|
||||
required_map_width_mm,
|
||||
required_map_height_mm,
|
||||
max_format,
|
||||
)
|
||||
if selected_layout is None:
|
||||
return None
|
||||
|
||||
page_w, page_h, map_w, map_h = selected_layout
|
||||
force_fit_extent = map_w > required_map_width_mm or map_h > required_map_height_mm
|
||||
return page_w, page_h, map_w, map_h, force_fit_extent
|
||||
|
||||
def _apply_standard_minimum_a4_fallback(
|
||||
self,
|
||||
page_width_mm: float,
|
||||
page_height_mm: float,
|
||||
map_width_mm: float,
|
||||
map_height_mm: float,
|
||||
) -> tuple[float, float, float, float, bool]:
|
||||
"""Erzwingt bei zu kleinem Standardlayout mindestens DIN A4 im Querformat.
|
||||
|
||||
Liefert zusätzlich ein Flag zurück, damit die Extent-Anpassung im Layout
|
||||
zur zentrierten Darstellung aktiviert werden kann.
|
||||
"""
|
||||
fallback_page_w = max(page_width_mm, STANDARD_A4_LANDSCAPE_W_MM)
|
||||
fallback_page_h = max(page_height_mm, STANDARD_A4_LANDSCAPE_H_MM)
|
||||
if fallback_page_w == page_width_mm and fallback_page_h == page_height_mm:
|
||||
return page_width_mm, page_height_mm, map_width_mm, map_height_mm, False
|
||||
|
||||
fallback_map_w = max(map_width_mm, fallback_page_w - STANDARD_LAYOUT_FIXBREITE_MM, 50.0)
|
||||
fallback_map_h = max(map_height_mm, fallback_page_h - STANDARD_LAYOUT_FIXHOEHE_MM, 50.0)
|
||||
return fallback_page_w, fallback_page_h, fallback_map_w, fallback_map_h, True
|
||||
|
||||
def druckvorlage_anlegen(
|
||||
self,
|
||||
layer: object | None,
|
||||
@@ -590,26 +1169,34 @@ class PrintLogic:
|
||||
) -> dict:
|
||||
resolved_layer = layer or self._resolve_layer_from_project()
|
||||
|
||||
if kartenname_auswahl not in (KARTENNAME_38, KARTENNAME_41):
|
||||
if kartenname_auswahl not in (KARTENNAME_38, KARTENNAME_41, KARTENNAME_BAUFREIGABE):
|
||||
self.pruefmanager.zeige_hinweis("Kartennamen waehlen", "Kartennamen waehlen")
|
||||
return {"ok": False, "switch_to_tab_a": False, "atlas_seiten": 0}
|
||||
return self._failed_result()
|
||||
|
||||
if massstab_auswahl == MASSSTAB_WIE_KARTENFENSTER:
|
||||
massstab_str = get_variable(PLOTMASSSTAB_VAR, scope="project") or ""
|
||||
# Maßstab direkt vom aktuellen Canvas lesen; Projektvariable als Fallback.
|
||||
try:
|
||||
massstab_zahl = float(massstab_str)
|
||||
except (ValueError, TypeError):
|
||||
canvas = iface.mapCanvas()
|
||||
live_scale = canvas.scale() if canvas is not None else 0.0
|
||||
massstab_zahl = float(live_scale) if live_scale > 0 else 0.0
|
||||
except Exception:
|
||||
massstab_zahl = 0.0
|
||||
if massstab_zahl <= 0:
|
||||
massstab_str = get_variable(PLOTMASSSTAB_VAR, scope="project") or ""
|
||||
try:
|
||||
massstab_zahl = float(massstab_str)
|
||||
except (ValueError, TypeError):
|
||||
massstab_zahl = 0.0
|
||||
else:
|
||||
massstab_zahl = float(PLOTMASSSTAB_BY_AUSWAHL.get(massstab_auswahl, 0))
|
||||
|
||||
if massstab_zahl <= 0:
|
||||
self.pruefmanager.zeige_hinweis("Massstab fehlt", "Kein gueltiger Massstab angegeben.")
|
||||
return {"ok": False, "switch_to_tab_a": False, "atlas_seiten": 0}
|
||||
return self._failed_result()
|
||||
|
||||
extent, layer_for_extent = self._resolve_extent(resolved_layer, druckbereich_auswahl, massstab_zahl)
|
||||
if extent is None:
|
||||
return {"ok": False, "switch_to_tab_a": False, "atlas_seiten": 0}
|
||||
return self._failed_result()
|
||||
|
||||
if layer_for_extent is not None:
|
||||
layer_id = getattr(layer_for_extent, "id", lambda: "")() or ""
|
||||
@@ -619,42 +1206,46 @@ class PrintLogic:
|
||||
# Alt: Rückwärtskompatibilität
|
||||
set_variable("verfahrensgebietslayer", layer_id, scope="project")
|
||||
|
||||
kartenbild_w = extent.width() * 1000.0 / massstab_zahl
|
||||
kartenbild_h = extent.height() * 1000.0 / massstab_zahl
|
||||
kartenbild_w, kartenbild_h = self._get_required_map_dimensions_mm(extent, massstab_zahl)
|
||||
|
||||
plotgroesse_w = kartenbild_w + 210.0
|
||||
plotgroesse_h = kartenbild_h + 20.0
|
||||
|
||||
din_dims = DIN_GROESSEN.get(zielgroesse, DIN_GROESSEN[DIN_STANDARD])
|
||||
din_w = float(din_dims[0])
|
||||
din_h = float(din_dims[1])
|
||||
din_max_h = float(max(din_dims))
|
||||
if formfaktor:
|
||||
ziel_w, ziel_h = plotgroesse_w, min(plotgroesse_h, din_max_h)
|
||||
force_fit_extent_for_min_a4 = False
|
||||
if kartenname_auswahl == KARTENNAME_BAUFREIGABE:
|
||||
_, auto_page_w, auto_page_h, auto_map_w, auto_map_h, _bf_fallback = self._get_baufreigabe_page_dimensions(
|
||||
kartenbild_w,
|
||||
kartenbild_h,
|
||||
max_format=zielgroesse if zielgroesse in {"DIN A4", "DIN A3"} else "DIN A3",
|
||||
)
|
||||
else:
|
||||
cap_w, cap_h = din_w, din_h
|
||||
if not (plotgroesse_w <= cap_w and plotgroesse_h <= cap_h):
|
||||
if plotgroesse_w <= din_h and plotgroesse_h <= din_w:
|
||||
cap_w, cap_h = din_h, din_w
|
||||
ziel_w = min(plotgroesse_w, cap_w)
|
||||
ziel_h = min(plotgroesse_h, cap_h)
|
||||
|
||||
if ziel_w < DIN_GROESSEN["DIN A4"][0] or ziel_h < DIN_GROESSEN["DIN A4"][1]:
|
||||
self.pruefmanager.zeige_hinweis(
|
||||
"Blattgroesse kleiner als DIN A4",
|
||||
"Die Layouterstellung wird trotzdem fortgesetzt.",
|
||||
standard_layout = self._get_standard_layout_dimensions(
|
||||
kartenbild_w,
|
||||
kartenbild_h,
|
||||
zielgroesse,
|
||||
formfaktor,
|
||||
)
|
||||
if standard_layout is None:
|
||||
return self._show_target_size_too_small()
|
||||
|
||||
if plotgroesse_w > ziel_w or plotgroesse_h > ziel_h:
|
||||
if plotgroesse_w <= ziel_h and plotgroesse_h <= ziel_w:
|
||||
ziel_w, ziel_h = ziel_h, ziel_w
|
||||
auto_page_w, auto_page_h, auto_map_w, auto_map_h, force_fit_extent_for_min_a4 = standard_layout
|
||||
|
||||
if plotgroesse_w > ziel_w or plotgroesse_h > ziel_h:
|
||||
self.pruefmanager.zeige_hinweis(
|
||||
"Zielgroesse zu klein",
|
||||
"Die gewaehlte Zielgroesse reicht fuer den Einzelblatt-Druck nicht aus.",
|
||||
)
|
||||
return {"ok": False, "switch_to_tab_a": False, "atlas_seiten": 0}
|
||||
# Finale Sicherheitspruefung: alle Layoutobjekte muessen auf die Seite passen.
|
||||
if kartenname_auswahl == KARTENNAME_BAUFREIGABE:
|
||||
fixed_width_mm = BAUFREIGABE_LAYOUT_FIXBREITE_MM
|
||||
fixed_height_mm = BAUFREIGABE_LAYOUT_FIXHOEHE_MM
|
||||
else:
|
||||
fixed_width_mm = STANDARD_LAYOUT_FIXBREITE_MM
|
||||
fixed_height_mm = STANDARD_LAYOUT_FIXHOEHE_MM
|
||||
|
||||
fits_page = self._does_plot_fit_page(
|
||||
auto_page_w,
|
||||
auto_page_h,
|
||||
auto_map_w,
|
||||
auto_map_h,
|
||||
fixed_width_mm,
|
||||
fixed_height_mm,
|
||||
)
|
||||
|
||||
if not fits_page:
|
||||
return self._show_target_size_too_small()
|
||||
|
||||
kartenname = get_variable(KARTENNAME_VAR, scope="project") or KARTENNAME_BY_AUSWAHL.get(
|
||||
kartenname_auswahl, "Vorlage"
|
||||
@@ -667,30 +1258,63 @@ class PrintLogic:
|
||||
default_text=kartenname,
|
||||
)
|
||||
if not bestaetigt:
|
||||
return {"ok": False, "switch_to_tab_a": False, "atlas_seiten": 0}
|
||||
return self._failed_result()
|
||||
|
||||
vorlage_name = (vorlage_name or "").strip() or kartenname
|
||||
|
||||
# Atlas-Status vorab bestimmen, damit er sowohl an create_ als auch configure_ uebergeben wird.
|
||||
atlas_raw_bf = get_variable(ATLAS_ERZEUGEN_UI_VAR, scope="project")
|
||||
atlas_erzeugen_bf = self._is_enabled_flag(atlas_raw_bf)
|
||||
|
||||
# Verzweigung: Baufreigabe vs. Standard-Layout
|
||||
try:
|
||||
PrintLayout().create_single_page_layout(
|
||||
name=vorlage_name,
|
||||
page_width_mm=ziel_w,
|
||||
page_height_mm=ziel_h,
|
||||
map_width_mm=kartenbild_w,
|
||||
map_height_mm=kartenbild_h,
|
||||
extent=extent,
|
||||
plotmassstab=massstab_zahl,
|
||||
thema=thema,
|
||||
fit_extent_to_map=(druckbereich_auswahl != DRUCKBEREICH_VERFAHRENSGEBIET),
|
||||
)
|
||||
if kartenname_auswahl == KARTENNAME_BAUFREIGABE:
|
||||
# Baufreigabe: feste DIN A3/A4 Querformat-Logik mit 80mm Kartenschild
|
||||
din_format = "DIN A3" if auto_page_w >= 420.0 else "DIN A4"
|
||||
PrintLayoutBaufreigabe().create_baufreigabe_layout(
|
||||
name=vorlage_name if vorlage_name != kartenname else "Baufreigabe",
|
||||
din_format=din_format,
|
||||
map_width_mm=auto_map_w,
|
||||
map_height_mm=auto_map_h,
|
||||
extent=extent,
|
||||
plotmassstab=massstab_zahl,
|
||||
thema=thema,
|
||||
fit_extent_to_map=True,
|
||||
atlas_driven=atlas_erzeugen_bf,
|
||||
)
|
||||
else:
|
||||
# Standard-Layout: dynamische Groessenanpassung
|
||||
PrintLayout().create_single_page_layout(
|
||||
name=vorlage_name,
|
||||
page_width_mm=auto_page_w,
|
||||
page_height_mm=auto_page_h,
|
||||
map_width_mm=auto_map_w,
|
||||
map_height_mm=auto_map_h,
|
||||
extent=extent,
|
||||
plotmassstab=massstab_zahl,
|
||||
kartenname_auswahl=kartenname_auswahl,
|
||||
thema=thema,
|
||||
fit_extent_to_map=(druckbereich_auswahl != DRUCKBEREICH_VERFAHRENSGEBIET) or force_fit_extent_for_min_a4,
|
||||
)
|
||||
except ValueError as exc:
|
||||
self.pruefmanager.zeige_hinweis("Vorlage anlegen", str(exc))
|
||||
return {"ok": False, "switch_to_tab_a": False, "atlas_seiten": 0}
|
||||
return self._failed_result()
|
||||
except Exception as exc:
|
||||
self.pruefmanager.zeige_hinweis(
|
||||
"Vorlage anlegen",
|
||||
f"Die Vorlage konnte nicht angelegt werden: {exc}",
|
||||
)
|
||||
return {"ok": False, "switch_to_tab_a": False, "atlas_seiten": 0}
|
||||
return self._failed_result()
|
||||
|
||||
# Nach erfolgreicher Layout-Erstellung: Atlas ggf. konfigurieren (nur falls aktiviert)
|
||||
if kartenname_auswahl == KARTENNAME_BAUFREIGABE:
|
||||
if atlas_erzeugen_bf:
|
||||
self.configure_atlas_for_baufreigabe_layout()
|
||||
else:
|
||||
self.configure_atlas_for_baufreigabe_layout(
|
||||
atlas_config={
|
||||
"enabled": False,
|
||||
}
|
||||
)
|
||||
|
||||
return {"ok": True, "switch_to_tab_a": False, "atlas_seiten": 1}
|
||||
|
||||
@@ -74,6 +74,13 @@ PruefAktion = Literal[
|
||||
"stil_not_implemented",
|
||||
"datei_unbekannt",
|
||||
"needs_user_action",
|
||||
|
||||
# Verfahrensgebiet-Editoperationen
|
||||
"verfahrensgebiet_layer_existiert", # bestehender Layer → Nutzerentscheidung
|
||||
"verfahrensgebiet_ergaenzen", # Flurstücke hinzufügen
|
||||
"verfahrensgebiet_entfernen", # Flurstücke entfernen
|
||||
"verfahrensgebiet_ersetzen", # Verfahrensgebiet vollständig ersetzen
|
||||
"verfahrensgebiet_abbrechen", # Nutzer hat abgebrochen
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ Die Anwendung erfolgt später über eine Aktion.
|
||||
"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from sn_basis.functions.os_wrapper import is_absolute_path
|
||||
from sn_basis.functions.sys_wrapper import get_plugin_root, file_exists, join_path
|
||||
@@ -43,7 +44,7 @@ class Stilpruefer:
|
||||
|
||||
pfad = str(stil_pfad)
|
||||
|
||||
if not is_absolute_path(pfad):
|
||||
if not is_absolute_path(pfad) and not pfad.startswith(("/", "\\")):
|
||||
plugin_root = get_plugin_root()
|
||||
pfad = str(join_path(plugin_root, "sn_plan41", "assets", pfad))
|
||||
|
||||
@@ -55,7 +56,7 @@ class Stilpruefer:
|
||||
ok=False,
|
||||
meldung=f"Die Stil-Datei '{stil_pfad}' wurde nicht gefunden.",
|
||||
aktion="datei_nicht_gefunden",
|
||||
kontext=pfad,
|
||||
kontext=Path(pfad),
|
||||
)
|
||||
|
||||
# -----------------------------------------------------
|
||||
@@ -66,7 +67,7 @@ class Stilpruefer:
|
||||
ok=False,
|
||||
meldung="Die Stil-Datei muss die Endung '.qml' haben.",
|
||||
aktion="falsche_endung",
|
||||
kontext=pfad,
|
||||
kontext=Path(pfad),
|
||||
)
|
||||
|
||||
# -----------------------------------------------------
|
||||
@@ -76,5 +77,5 @@ class Stilpruefer:
|
||||
ok=True,
|
||||
meldung="Stil-Datei ist gültig.",
|
||||
aktion="stil_anwendbar",
|
||||
kontext=pfad,
|
||||
kontext=Path(pfad),
|
||||
)
|
||||
|
||||
+4
-1
@@ -1 +1,4 @@
|
||||
#Testordner
|
||||
"""
|
||||
sn_basis/tests – Test-Suite für das sn_basis Plugin.
|
||||
Enthält Unit-Tests und Integrationstests für alle Module.
|
||||
"""
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import unittest
|
||||
from typing import Any, cast
|
||||
from unittest.mock import patch
|
||||
|
||||
from sn_basis.modules.DataGrabber import DataGrabber
|
||||
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
|
||||
|
||||
|
||||
class _DummyPruefmanager:
|
||||
def verarbeite(self, ergebnis: Any):
|
||||
return ergebnis
|
||||
|
||||
|
||||
class TestDataGrabber(unittest.TestCase):
|
||||
|
||||
def _make_grabber(self) -> DataGrabber:
|
||||
return DataGrabber(pruefmanager=cast(Any, _DummyPruefmanager()))
|
||||
|
||||
@patch("sn_basis.modules.DataGrabber.Dateipruefer")
|
||||
def test_analyze_source_type_detects_excel(self, mock_dateipruefer):
|
||||
mock_dateipruefer.return_value.pruefe.return_value = pruef_ergebnis(
|
||||
ok=True,
|
||||
meldung="ok",
|
||||
aktion="ok",
|
||||
kontext="C:/tmp/list.xlsx",
|
||||
)
|
||||
grabber = self._make_grabber()
|
||||
|
||||
source_type, result = grabber.analyze_source_type("C:/tmp/list.xlsx")
|
||||
|
||||
self.assertEqual(source_type, "excel")
|
||||
self.assertTrue(result.ok)
|
||||
|
||||
@patch("sn_basis.modules.DataGrabber.Dateipruefer")
|
||||
def test_analyze_source_type_returns_unknown_filetype(self, mock_dateipruefer):
|
||||
mock_dateipruefer.return_value.pruefe.return_value = pruef_ergebnis(
|
||||
ok=True,
|
||||
meldung="ok",
|
||||
aktion="ok",
|
||||
kontext="C:/tmp/file.txt",
|
||||
)
|
||||
grabber = self._make_grabber()
|
||||
|
||||
source_type, _result = grabber.analyze_source_type("C:/tmp/file.txt")
|
||||
|
||||
self.assertEqual(source_type, "unbekannter_dateityp")
|
||||
|
||||
@patch("sn_basis.modules.DataGrabber.Linkpruefer")
|
||||
@patch("sn_basis.modules.DataGrabber.Dateipruefer")
|
||||
def test_analyze_source_type_detects_service_when_file_check_fails(self, mock_dateipruefer, mock_linkpruefer):
|
||||
mock_dateipruefer.return_value.pruefe.return_value = pruef_ergebnis(
|
||||
ok=False,
|
||||
meldung="missing",
|
||||
aktion="datei_nicht_gefunden",
|
||||
kontext=None,
|
||||
)
|
||||
mock_linkpruefer.return_value.pruefe.return_value = pruef_ergebnis(
|
||||
ok=True,
|
||||
meldung="ok",
|
||||
aktion="url_ok",
|
||||
kontext="https://example.test",
|
||||
)
|
||||
grabber = self._make_grabber()
|
||||
|
||||
source_type, result = grabber.analyze_source_type("https://example.test")
|
||||
|
||||
self.assertEqual(source_type, "dienst")
|
||||
self.assertTrue(result.ok)
|
||||
|
||||
@patch("sn_basis.modules.DataGrabber.Linkpruefer")
|
||||
@patch("sn_basis.modules.DataGrabber.Dateipruefer")
|
||||
def test_analyze_source_type_returns_unbekannte_quelle_on_full_failure(self, mock_dateipruefer, mock_linkpruefer):
|
||||
mock_dateipruefer.return_value.pruefe.return_value = pruef_ergebnis(
|
||||
ok=False,
|
||||
meldung="missing",
|
||||
aktion="datei_nicht_gefunden",
|
||||
kontext=None,
|
||||
)
|
||||
mock_linkpruefer.return_value.pruefe.return_value = pruef_ergebnis(
|
||||
ok=False,
|
||||
meldung="bad",
|
||||
aktion="url_nicht_erreichbar",
|
||||
kontext=None,
|
||||
)
|
||||
grabber = self._make_grabber()
|
||||
|
||||
source_type, result = grabber.analyze_source_type("broken-input")
|
||||
|
||||
self.assertEqual(source_type, "unbekannte_quelle")
|
||||
self.assertFalse(result.ok)
|
||||
|
||||
@patch.object(DataGrabber, "analyze_source_type")
|
||||
@patch.object(DataGrabber, "_process_excel_source")
|
||||
def test_run_returns_success_summary_with_valid_rows(self, mock_process_excel, mock_analyze):
|
||||
mock_analyze.return_value = (
|
||||
"excel",
|
||||
pruef_ergebnis(ok=True, meldung="ok", aktion="ok", kontext=None),
|
||||
)
|
||||
mock_process_excel.return_value = (
|
||||
{"rows": [{"ident": "A1"}]},
|
||||
[],
|
||||
)
|
||||
|
||||
grabber = self._make_grabber()
|
||||
source_dict, summary = grabber.run("C:/tmp/list.xlsx")
|
||||
|
||||
self.assertEqual(len(source_dict["rows"]), 1)
|
||||
self.assertTrue(summary.ok)
|
||||
self.assertEqual(summary.aktion, "ok")
|
||||
|
||||
@patch.object(DataGrabber, "analyze_source_type")
|
||||
def test_run_returns_failure_summary_without_rows(self, mock_analyze):
|
||||
mock_analyze.return_value = (
|
||||
"unbekannte_quelle",
|
||||
pruef_ergebnis(ok=False, meldung="bad", aktion="unbekannte_quelle", kontext=None),
|
||||
)
|
||||
|
||||
grabber = self._make_grabber()
|
||||
source_dict, summary = grabber.run("???")
|
||||
|
||||
self.assertEqual(source_dict, {})
|
||||
self.assertFalse(summary.ok)
|
||||
self.assertEqual(summary.aktion, "keine_validen_eintraege")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -2,6 +2,19 @@ import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
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_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,
|
||||
TEXT_BACKGROUND_SHAPE_RECTANGLE,
|
||||
TEXT_BACKGROUND_SIZE_BUFFER,
|
||||
TEXT_RENDER_UNIT_MILLIMETERS,
|
||||
TEXT_RENDER_UNIT_POINTS,
|
||||
QgsLayoutItemLegend,
|
||||
QgsLayoutItemMap,
|
||||
QgsPrintLayout,
|
||||
@@ -15,6 +28,29 @@ class _FakeExtent:
|
||||
return False
|
||||
|
||||
|
||||
class _FakeLegendForResize:
|
||||
def __init__(self) -> None:
|
||||
self.last_resize = None
|
||||
|
||||
def setLocked(self, _enabled: bool) -> None:
|
||||
pass
|
||||
|
||||
def setColumnCount(self, _count: int) -> None:
|
||||
pass
|
||||
|
||||
def setEqualColumnWidth(self, _enabled: bool) -> None:
|
||||
pass
|
||||
|
||||
def refresh(self) -> None:
|
||||
pass
|
||||
|
||||
def adjustBoxSize(self) -> None:
|
||||
pass
|
||||
|
||||
def attemptResize(self, size) -> None:
|
||||
self.last_resize = size
|
||||
|
||||
|
||||
class TestPrintLayout(unittest.TestCase):
|
||||
|
||||
def test_create_legend_item_links_map_and_enables_map_filtering(self):
|
||||
@@ -54,6 +90,67 @@ class TestPrintLayout(unittest.TestCase):
|
||||
root_group = legend.model().rootGroup()
|
||||
self.assertEqual(root_group.layers, [])
|
||||
|
||||
def test_configure_legend_size_uses_content_adaptive_size(self):
|
||||
project = QgsProject.instance()
|
||||
print_layout = PrintLayout(project=project)
|
||||
legend = _FakeLegendForResize()
|
||||
|
||||
with patch.object(print_layout, "_get_item_size_mm", return_value=(95.0, 33.0)):
|
||||
height = print_layout._configure_legend_size(
|
||||
legend,
|
||||
width_limit_mm=180.0,
|
||||
column_count=2,
|
||||
equal_column_width=True,
|
||||
)
|
||||
|
||||
self.assertIsNone(legend.last_resize)
|
||||
self.assertEqual(height, 33.0)
|
||||
|
||||
def test_configure_hauptkarte_grids_uses_wrapper_constants(self):
|
||||
project = QgsProject.instance()
|
||||
print_layout = PrintLayout(project=project)
|
||||
layout = QgsPrintLayout(project)
|
||||
hauptkarte = QgsLayoutItemMap(layout)
|
||||
|
||||
print_layout._configure_hauptkarte_grids(hauptkarte)
|
||||
|
||||
grid_stack = hauptkarte.grids()
|
||||
grids = grid_stack.asList()
|
||||
self.assertEqual(len(grids), 1)
|
||||
|
||||
frame_grid = grids[0]
|
||||
|
||||
self.assertEqual(frame_grid.style, MAP_GRID_STYLE_FRAME_ANNOTATIONS)
|
||||
self.assertEqual(frame_grid.frame_style, MAP_GRID_FRAME_STYLE_INTERIOR_TICKS)
|
||||
self.assertTrue(frame_grid.annotation_enabled)
|
||||
self.assertEqual(frame_grid.annotation_display[MAP_GRID_BORDER_LEFT], MAP_GRID_ANNOTATION_DISPLAY_HIDE_ALL)
|
||||
self.assertEqual(frame_grid.annotation_display[MAP_GRID_BORDER_TOP], MAP_GRID_ANNOTATION_DISPLAY_HIDE_ALL)
|
||||
self.assertEqual(frame_grid.annotation_display[MAP_GRID_BORDER_RIGHT], MAP_GRID_ANNOTATION_DISPLAY_SHOW_Y)
|
||||
self.assertEqual(frame_grid.annotation_display[MAP_GRID_BORDER_BOTTOM], MAP_GRID_ANNOTATION_DISPLAY_SHOW_X)
|
||||
|
||||
text_format = frame_grid.annotation_text_format
|
||||
self.assertIsNotNone(text_format)
|
||||
self.assertEqual(getattr(text_format, "size", None), 10)
|
||||
self.assertEqual(getattr(text_format, "size_unit", None), TEXT_RENDER_UNIT_POINTS)
|
||||
|
||||
background = getattr(text_format, "background", None)
|
||||
self.assertIsNotNone(background)
|
||||
self.assertTrue(getattr(background, "enabled", False))
|
||||
self.assertEqual(getattr(background, "shape_type", None), TEXT_BACKGROUND_SHAPE_RECTANGLE)
|
||||
self.assertEqual(getattr(background, "size_type", None), TEXT_BACKGROUND_SIZE_BUFFER)
|
||||
self.assertEqual(getattr(background, "size_unit", None), TEXT_RENDER_UNIT_MILLIMETERS)
|
||||
self.assertEqual(getattr(background, "offset_unit", None), TEXT_RENDER_UNIT_MILLIMETERS)
|
||||
offset = getattr(background, "offset", None)
|
||||
self.assertIsNotNone(offset)
|
||||
if isinstance(offset, tuple):
|
||||
self.assertEqual(offset[1], -1.0)
|
||||
else:
|
||||
y_value = getattr(offset, "y", None)
|
||||
if callable(y_value):
|
||||
self.assertEqual(y_value(), -1.0)
|
||||
else:
|
||||
self.assertEqual(getattr(offset, "y", None), -1.0)
|
||||
|
||||
@patch("sn_basis.modules.print_layout.open_layout_designer")
|
||||
def test_create_single_page_layout_keeps_legend_linked_to_hauptkarte_and_theme(self, _mock_open_designer):
|
||||
project = QgsProject.instance()
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
import unittest
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from sn_basis.modules.print_logic import (
|
||||
PrintLogic,
|
||||
DRUCKBEREICH_RECHTECK,
|
||||
DRUCKBEREICH_WIE_KARTENFENSTER,
|
||||
ENDLOSROLLE_MAX_BREITE_MM,
|
||||
)
|
||||
|
||||
|
||||
class _FakeExtent:
|
||||
def __init__(self, width: float = 100.0, height: float = 80.0) -> None:
|
||||
self._width = width
|
||||
self._height = height
|
||||
|
||||
def width(self) -> float:
|
||||
return self._width
|
||||
|
||||
def height(self) -> float:
|
||||
return self._height
|
||||
|
||||
def isNull(self) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
class _FakeCanvas:
|
||||
def __init__(self, extent: object | None) -> None:
|
||||
self._extent = extent
|
||||
|
||||
def extent(self) -> object | None:
|
||||
return self._extent
|
||||
|
||||
|
||||
class TestPrintLogic(unittest.TestCase):
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.logic = PrintLogic(pruefmanager=MagicMock())
|
||||
|
||||
def test_get_baufreigabe_page_dimensions_prefers_a4_for_small_extent(self):
|
||||
din_format, page_w, page_h, map_w, map_h, fallback = self.logic._get_baufreigabe_page_dimensions(80.0, 120.0)
|
||||
|
||||
self.assertEqual(din_format, "DIN A4")
|
||||
self.assertEqual((page_w, page_h), (297.0, 210.0))
|
||||
self.assertEqual((map_w, map_h), (187.0, 190.0))
|
||||
self.assertFalse(fallback)
|
||||
|
||||
def test_get_baufreigabe_page_dimensions_escalates_to_a3(self):
|
||||
din_format, page_w, page_h, map_w, map_h, fallback = self.logic._get_baufreigabe_page_dimensions(200.0, 180.0)
|
||||
|
||||
self.assertEqual(din_format, "DIN A3")
|
||||
self.assertEqual((page_w, page_h), (420.0, 297.0))
|
||||
self.assertEqual((map_w, map_h), (310.0, 277.0))
|
||||
self.assertFalse(fallback)
|
||||
|
||||
def test_get_baufreigabe_page_dimensions_respects_a4_as_maximum_with_fallback(self):
|
||||
din_format, page_w, page_h, map_w, map_h, fallback = self.logic._get_baufreigabe_page_dimensions(
|
||||
200.0,
|
||||
180.0,
|
||||
max_format="DIN A4",
|
||||
)
|
||||
|
||||
self.assertEqual(din_format, "DIN A4")
|
||||
self.assertEqual((page_w, page_h), (297.0, 210.0))
|
||||
self.assertEqual((map_w, map_h), (187.0, 190.0))
|
||||
self.assertTrue(fallback)
|
||||
|
||||
def test_get_baufreigabe_page_dimensions_falls_back_to_a3_for_oversize_extent(self):
|
||||
din_format, page_w, page_h, map_w, map_h, fallback = self.logic._get_baufreigabe_page_dimensions(400.0, 300.0)
|
||||
|
||||
self.assertEqual(din_format, "DIN A3")
|
||||
self.assertEqual((page_w, page_h), (420.0, 297.0))
|
||||
self.assertEqual((map_w, map_h), (310.0, 277.0))
|
||||
self.assertTrue(fallback)
|
||||
|
||||
def test_select_standard_page_layout_uses_a4_as_minimum_sheet(self):
|
||||
selected = self.logic._select_standard_page_layout(60.0, 100.0, max_format="DIN A3")
|
||||
|
||||
self.assertEqual(selected, (297.0, 210.0, 87.0, 190.0))
|
||||
|
||||
def test_select_standard_page_layout_limits_hauptkarte_to_a3(self):
|
||||
selected = self.logic._select_standard_page_layout(120.0, 240.0, max_format="DIN A3")
|
||||
|
||||
self.assertEqual(selected, (420.0, 297.0, 210.0, 277.0))
|
||||
|
||||
def test_select_standard_page_layout_returns_none_if_a3_is_too_small(self):
|
||||
selected = self.logic._select_standard_page_layout(260.0, 280.0, max_format="DIN A3")
|
||||
|
||||
self.assertIsNone(selected)
|
||||
|
||||
def test_get_standard_layout_dimensions_uses_a4_and_forces_fit_for_small_extent(self):
|
||||
page_w, page_h, map_w, map_h, force_fit = self.logic._get_standard_layout_dimensions(
|
||||
60.0,
|
||||
100.0,
|
||||
max_format="DIN A3",
|
||||
formfaktor=False,
|
||||
)
|
||||
|
||||
self.assertEqual((page_w, page_h), (297.0, 210.0))
|
||||
self.assertEqual((map_w, map_h), (87.0, 190.0))
|
||||
self.assertTrue(force_fit)
|
||||
|
||||
def test_get_standard_layout_dimensions_caps_endlosrolle_width_at_2000_mm(self):
|
||||
page_w, page_h, map_w, map_h, force_fit = self.logic._get_standard_layout_dimensions(
|
||||
2500.0,
|
||||
100.0,
|
||||
max_format="DIN A0",
|
||||
formfaktor=True,
|
||||
)
|
||||
|
||||
self.assertEqual(page_w, ENDLOSROLLE_MAX_BREITE_MM)
|
||||
self.assertEqual(page_h, 210.0)
|
||||
self.assertEqual((map_w, map_h), (2500.0, 190.0))
|
||||
self.assertTrue(force_fit)
|
||||
|
||||
def test_apply_standard_minimum_a4_fallback_for_small_page(self):
|
||||
page_w, page_h, map_w, map_h, forced = self.logic._apply_standard_minimum_a4_fallback(
|
||||
250.0,
|
||||
180.0,
|
||||
40.0,
|
||||
20.0,
|
||||
)
|
||||
|
||||
self.assertTrue(forced)
|
||||
self.assertEqual((page_w, page_h), (297.0, 210.0))
|
||||
self.assertEqual((map_w, map_h), (87.0, 190.0))
|
||||
|
||||
def test_apply_standard_minimum_a4_fallback_keeps_large_page(self):
|
||||
page_w, page_h, map_w, map_h, forced = self.logic._apply_standard_minimum_a4_fallback(
|
||||
420.0,
|
||||
297.0,
|
||||
210.0,
|
||||
277.0,
|
||||
)
|
||||
|
||||
self.assertFalse(forced)
|
||||
self.assertEqual((page_w, page_h), (420.0, 297.0))
|
||||
self.assertEqual((map_w, map_h), (210.0, 277.0))
|
||||
|
||||
def test_does_plot_fit_page_true_for_standard_a4(self):
|
||||
fits = self.logic._does_plot_fit_page(297.0, 210.0, 87.0, 190.0, 210.0, 20.0)
|
||||
|
||||
self.assertTrue(fits)
|
||||
|
||||
def test_does_plot_fit_page_false_if_objects_exceed_page(self):
|
||||
fits = self.logic._does_plot_fit_page(297.0, 210.0, 120.0, 190.0, 210.0, 20.0)
|
||||
|
||||
self.assertFalse(fits)
|
||||
|
||||
@patch("sn_basis.modules.print_logic.set_variable")
|
||||
def test_set_druckbereich_accepts_wie_kartenfenster(self, mock_set_variable):
|
||||
self.logic.set_druckbereich_for_auswahl(DRUCKBEREICH_WIE_KARTENFENSTER)
|
||||
|
||||
mock_set_variable.assert_called_once_with("druckbereich", DRUCKBEREICH_WIE_KARTENFENSTER, scope="project")
|
||||
|
||||
@patch("sn_basis.modules.print_logic.iface")
|
||||
def test_get_canvas_extent_returns_valid_extent(self, mock_iface):
|
||||
extent = _FakeExtent(120.0, 90.0)
|
||||
mock_iface.mapCanvas.return_value = _FakeCanvas(extent)
|
||||
|
||||
result = self.logic._get_canvas_extent()
|
||||
|
||||
self.assertIs(result, extent)
|
||||
|
||||
@patch.object(PrintLogic, "_buffer_extent")
|
||||
@patch.object(PrintLogic, "_get_canvas_extent")
|
||||
def test_resolve_extent_uses_canvas_extent_without_buffer(self, mock_get_canvas_extent, mock_buffer_extent):
|
||||
extent = _FakeExtent(120.0, 90.0)
|
||||
mock_get_canvas_extent.return_value = extent
|
||||
|
||||
result_extent, result_layer = self.logic._resolve_extent(None, DRUCKBEREICH_WIE_KARTENFENSTER, 5000.0)
|
||||
|
||||
self.assertIs(result_extent, extent)
|
||||
self.assertIsNone(result_layer)
|
||||
mock_buffer_extent.assert_not_called()
|
||||
|
||||
@patch.object(PrintLogic, "_buffer_extent")
|
||||
@patch.object(PrintLogic, "_get_druckrechteck_extent")
|
||||
def test_resolve_extent_uses_rectangle_extent_without_buffer(self, mock_get_rect_extent, mock_buffer_extent):
|
||||
extent = _FakeExtent(120.0, 90.0)
|
||||
mock_get_rect_extent.return_value = extent
|
||||
|
||||
result_extent, result_layer = self.logic._resolve_extent(None, DRUCKBEREICH_RECHTECK, 5000.0)
|
||||
|
||||
self.assertIs(result_extent, extent)
|
||||
self.assertIsNone(result_layer)
|
||||
mock_buffer_extent.assert_not_called()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -141,6 +141,40 @@ class TestPruefmanager(unittest.TestCase):
|
||||
self.assertFalse(entscheidung.ok)
|
||||
self.assertEqual(entscheidung.aktion, "unbekannt")
|
||||
|
||||
@patch("sn_basis.modules.Pruefmanager.ask_overwrite_append_cancel_custom", return_value="overwrite")
|
||||
def test_datei_existiert_overwrite(self, _mock_choice):
|
||||
ergebnis = pruef_ergebnis(False, "Datei existiert", "datei_existiert", "C:/tmp/v.gpkg")
|
||||
entscheidung = self.manager.verarbeite(ergebnis)
|
||||
|
||||
self.assertTrue(entscheidung.ok)
|
||||
self.assertEqual(entscheidung.aktion, "datei_existiert_ueberschreiben")
|
||||
self.assertEqual(entscheidung.kontext, "C:/tmp/v.gpkg")
|
||||
|
||||
@patch("sn_basis.modules.Pruefmanager.ask_overwrite_append_cancel_custom", return_value="append")
|
||||
def test_datei_existiert_append(self, _mock_choice):
|
||||
ergebnis = pruef_ergebnis(False, "Datei existiert", "datei_existiert", "C:/tmp/v.gpkg")
|
||||
entscheidung = self.manager.verarbeite(ergebnis)
|
||||
|
||||
self.assertTrue(entscheidung.ok)
|
||||
self.assertEqual(entscheidung.aktion, "datei_existiert_anhaengen")
|
||||
|
||||
@patch("sn_basis.modules.Pruefmanager.ask_overwrite_append_cancel_custom", return_value="cancel")
|
||||
def test_datei_existiert_cancel(self, _mock_choice):
|
||||
ergebnis = pruef_ergebnis(False, "Datei existiert", "datei_existiert", "C:/tmp/v.gpkg")
|
||||
entscheidung = self.manager.verarbeite(ergebnis)
|
||||
|
||||
self.assertTrue(entscheidung.ok)
|
||||
self.assertEqual(entscheidung.aktion, "datei_existiert_ueberspringen")
|
||||
|
||||
def test_datei_existiert_in_non_qgis_mode_returns_input(self):
|
||||
manager = Pruefmanager(ui_modus="test")
|
||||
ergebnis = pruef_ergebnis(False, "Datei existiert", "datei_existiert", "C:/tmp/v.gpkg")
|
||||
|
||||
entscheidung = manager.verarbeite(ergebnis)
|
||||
|
||||
self.assertFalse(entscheidung.ok)
|
||||
self.assertEqual(entscheidung.aktion, "datei_existiert")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -55,6 +55,32 @@ class TestSettingsLogic(unittest.TestCase):
|
||||
self.assertIn("gemeinden", saved_keys)
|
||||
self.assertNotIn("unbekannt", saved_keys)
|
||||
|
||||
@patch("sn_basis.functions.settings_logic.get_variable")
|
||||
def test_load_handles_partial_values_with_empty_fallback(self, mock_get):
|
||||
values = {
|
||||
"amt": "A1",
|
||||
"behoerde": "",
|
||||
"landkreis_user": "L1",
|
||||
}
|
||||
mock_get.side_effect = lambda key, scope="project": values.get(key, "")
|
||||
|
||||
logic = SettingsLogic()
|
||||
daten = logic.load()
|
||||
|
||||
self.assertEqual(daten["amt"], "A1")
|
||||
self.assertEqual(daten["behoerde"], "")
|
||||
self.assertEqual(daten["landkreis_user"], "L1")
|
||||
self.assertEqual(daten["sachgebiet"], "")
|
||||
|
||||
@patch("sn_basis.functions.settings_logic.set_variable")
|
||||
def test_save_empty_string_value_is_persisted(self, mock_set):
|
||||
logic = SettingsLogic()
|
||||
|
||||
logic.save({"amt": "", "behoerde": "B2"})
|
||||
|
||||
mock_set.assert_any_call("amt", "", scope="project")
|
||||
mock_set.assert_any_call("behoerde", "B2", scope="project")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -51,6 +51,28 @@ class TestVariableWrapper(unittest.TestCase):
|
||||
"new_value",
|
||||
)
|
||||
|
||||
@patch("sn_basis.functions.variable_wrapper.QgsExpressionContextUtils.setGlobalVariable")
|
||||
def test_set_variable_global_writes_prefixed_name(self, mock_set_global):
|
||||
variable_wrapper.set_variable("verfahrensnummer", "123", scope="global")
|
||||
|
||||
mock_set_global.assert_called_once_with("sn_verfahrensnummer", "123")
|
||||
|
||||
@patch("sn_basis.functions.variable_wrapper.QgsExpressionContextUtils.globalScope")
|
||||
def test_get_variable_global_returns_empty_string_fallback(self, mock_global_scope):
|
||||
mock_global_scope.return_value = _Scope("")
|
||||
|
||||
value = variable_wrapper.get_variable("nicht_vorhanden", scope="global")
|
||||
|
||||
self.assertEqual(value, "")
|
||||
|
||||
def test_get_variable_invalid_scope_raises(self):
|
||||
with self.assertRaises(ValueError):
|
||||
variable_wrapper.get_variable("foo", scope="invalid")
|
||||
|
||||
def test_set_variable_invalid_scope_raises(self):
|
||||
with self.assertRaises(ValueError):
|
||||
variable_wrapper.set_variable("foo", "bar", scope="invalid")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
"""
|
||||
sn_basis/ui – Benutzeroberflächen-Module für sn_basis.
|
||||
Enthält Dock-Manager, Navigation und einzelne Tabs.
|
||||
"""
|
||||
|
||||
@@ -99,6 +99,20 @@ class BaseDockWidget(QDockWidget):
|
||||
Wird aufgerufen, wenn das Dock geschlossen wird.
|
||||
Setzt die zugehörige Toolbar-Action zurück.
|
||||
"""
|
||||
try:
|
||||
tab_widget = self.widget()
|
||||
if tab_widget and hasattr(tab_widget, "count") and hasattr(tab_widget, "widget"):
|
||||
for index in range(tab_widget.count()):
|
||||
tab = tab_widget.widget(index)
|
||||
cleanup = getattr(tab, "cleanup", None)
|
||||
if callable(cleanup):
|
||||
try:
|
||||
cleanup()
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
if self.action:
|
||||
self.action.setChecked(False)
|
||||
|
||||
+8
-3
@@ -1,4 +1,9 @@
|
||||
from sn_basis.ui.tabs.print_tab import PrintTab
|
||||
from sn_basis.ui.tabs.settings_tab import SettingsTab
|
||||
"""
|
||||
sn_basis/ui/tabs – Tab-Widgets für die sn_basis Benutzeroberfläche.
|
||||
Stellt print_tab und settings_tab bereit.
|
||||
"""
|
||||
|
||||
__all__ = ["PrintTab", "SettingsTab"]
|
||||
# from sn_basis.ui.tabs.print_tab import PrintTab
|
||||
# from sn_basis.ui.tabs.settings_tab import SettingsTab
|
||||
|
||||
# __all__ = ["PrintTab", "SettingsTab"]
|
||||
|
||||
+263
-7
@@ -13,9 +13,13 @@ from sn_basis.functions.qt_wrapper import (
|
||||
QCheckBox,
|
||||
QHBoxLayout,
|
||||
QPushButton,
|
||||
QToolButton,
|
||||
ToolButtonTextBesideIcon,
|
||||
ArrowDown,
|
||||
ArrowRight,
|
||||
)
|
||||
from sn_basis.functions.qgiscore_wrapper import QgsProject
|
||||
from sn_basis.functions.qgisui_wrapper import iface
|
||||
from sn_basis.functions.qgiscore_wrapper import QgsProject, QgsMapLayerProxyModel
|
||||
from sn_basis.functions.qgisui_wrapper import iface, QgsMapLayerComboBox
|
||||
from sn_basis.functions.variable_wrapper import get_variable, set_variable
|
||||
from sn_basis.modules.Pruefmanager import Pruefmanager
|
||||
from sn_basis.modules.DataGrabber import DataGrabber
|
||||
@@ -30,6 +34,7 @@ from sn_basis.modules.print_logic import (
|
||||
DRUCKPUFFER_VAR,
|
||||
DRUCKPUFFER_STANDARD_CM,
|
||||
DRUCKBEREICH_VERFAHRENSGEBIET,
|
||||
DRUCKBEREICH_WIE_KARTENFENSTER,
|
||||
DRUCKBEREICH_AKTUELLE_AUSWAHL,
|
||||
DRUCKBEREICH_RECHTECK,
|
||||
)
|
||||
@@ -39,12 +44,15 @@ KARTENNAME_VAR = "tab_b_kartenname"
|
||||
KARTENNAME_PLACEHOLDER = "Kartenname waehlen"
|
||||
KARTENNAME_38 = "§38"
|
||||
KARTENNAME_41 = "§41"
|
||||
KARTENNAME_BAUFREIGABE = "Anlage zur Baufreigabe"
|
||||
MASSSTAB_VAR = "tab_b_massstab"
|
||||
THEMA_VAR = "tab_b_thema"
|
||||
ZIELGROESSE_UI_VAR = "tab_b_zielgroesse"
|
||||
FORMFAKTOR_UI_VAR = "tab_b_formfaktor"
|
||||
ATLAS_ERZEUGEN_VAR = "tab_b_atlas_erzeugen"
|
||||
DRUCKBEREICH_OPTIONEN = [
|
||||
DRUCKBEREICH_VERFAHRENSGEBIET,
|
||||
DRUCKBEREICH_WIE_KARTENFENSTER,
|
||||
DRUCKBEREICH_AKTUELLE_AUSWAHL,
|
||||
DRUCKBEREICH_RECHTECK,
|
||||
]
|
||||
@@ -57,9 +65,11 @@ class PrintTab(QWidget):
|
||||
|
||||
def __init__(self, parent: Optional[Any] = None):
|
||||
super().__init__(parent)
|
||||
|
||||
self.pruefmanager: Optional[Pruefmanager] = None
|
||||
self.logic: Optional[PrintLogic] = None
|
||||
self.logic = PrintLogic(self.pruefmanager)
|
||||
|
||||
|
||||
#self.logic: Optional[PrintLogic] = None
|
||||
self._kartenname_combo: Optional[Any] = None
|
||||
self._massstab_combo: Optional[Any] = None
|
||||
self._thema_combo: Optional[Any] = None
|
||||
@@ -70,6 +80,11 @@ class PrintTab(QWidget):
|
||||
self._endlosrolle_cb: Optional[Any] = None
|
||||
self._druckpuffer_button: Optional[Any] = None
|
||||
self._btn_vorlage_erstellen: Optional[Any] = None
|
||||
self._atlas_layer_combo: Optional[Any] = None
|
||||
self._btn_auswahl_as_atlas: Optional[Any] = None
|
||||
self._atlas_erzeugen_cb: Optional[Any] = None
|
||||
self._atlas_button: Optional[Any] = None
|
||||
self._atlas_content: Optional[Any] = None
|
||||
|
||||
self._build_ui()
|
||||
self._restore_state()
|
||||
@@ -110,6 +125,7 @@ class PrintTab(QWidget):
|
||||
kartenname_combo.addItem(KARTENNAME_PLACEHOLDER)
|
||||
kartenname_combo.addItem(KARTENNAME_38)
|
||||
kartenname_combo.addItem(KARTENNAME_41)
|
||||
kartenname_combo.addItem(KARTENNAME_BAUFREIGABE)
|
||||
kartenname_combo.currentTextChanged.connect(self._on_kartenname_changed)
|
||||
self._kartenname_combo = kartenname_combo
|
||||
main_layout.addWidget(kartenname_combo)
|
||||
@@ -191,6 +207,55 @@ class PrintTab(QWidget):
|
||||
|
||||
main_layout.addLayout(druckbereich_row)
|
||||
|
||||
# Atlas-Bereich (einklappbar, analog Tab A)
|
||||
atlas_button = QToolButton()
|
||||
atlas_button.setText("Atlas")
|
||||
atlas_button.setCheckable(True)
|
||||
atlas_button.setChecked(False)
|
||||
atlas_button.setToolButtonStyle(ToolButtonTextBesideIcon)
|
||||
atlas_button.setArrowType(ArrowRight)
|
||||
atlas_button.setStyleSheet("font-weight: bold; margin-top: 6px;")
|
||||
atlas_button.toggled.connect(self._toggle_atlas)
|
||||
self._atlas_button = atlas_button
|
||||
main_layout.addWidget(atlas_button)
|
||||
|
||||
atlas_content = QWidget()
|
||||
atlas_layout = QVBoxLayout()
|
||||
atlas_layout.setSpacing(4)
|
||||
atlas_layout.setContentsMargins(10, 4, 4, 4)
|
||||
|
||||
atlas_erzeugen_cb = QCheckBox("Atlas erzeugen", self)
|
||||
atlas_erzeugen_cb.setChecked(True)
|
||||
atlas_erzeugen_cb.stateChanged.connect(self._on_atlas_erzeugen_changed)
|
||||
self._atlas_erzeugen_cb = atlas_erzeugen_cb
|
||||
atlas_layout.addWidget(atlas_erzeugen_cb)
|
||||
|
||||
atlas_layer_label = QLabel("Abdeckungslayer", self)
|
||||
atlas_layout.addWidget(atlas_layer_label)
|
||||
|
||||
atlas_layer_combo = QgsMapLayerComboBox(self)
|
||||
set_filters = getattr(atlas_layer_combo, "setFilters", None)
|
||||
if callable(set_filters):
|
||||
set_filters(QgsMapLayerProxyModel.VectorLayer)
|
||||
layer_changed = getattr(atlas_layer_combo, "layerChanged", None)
|
||||
if layer_changed is not None:
|
||||
try:
|
||||
layer_changed.connect(self._on_atlas_layer_changed)
|
||||
except Exception:
|
||||
pass
|
||||
self._atlas_layer_combo = atlas_layer_combo
|
||||
atlas_layout.addWidget(atlas_layer_combo)
|
||||
|
||||
btn_auswahl_as_atlas = QPushButton("Auswahl als Atlas anlegen", self)
|
||||
btn_auswahl_as_atlas.clicked.connect(self._on_auswahl_as_atlas_clicked)
|
||||
self._btn_auswahl_as_atlas = btn_auswahl_as_atlas
|
||||
atlas_layout.addWidget(btn_auswahl_as_atlas)
|
||||
|
||||
atlas_content.setLayout(atlas_layout)
|
||||
atlas_content.setVisible(False)
|
||||
self._atlas_content = atlas_content
|
||||
main_layout.addWidget(atlas_content)
|
||||
|
||||
btn_vorlage_erstellen = QPushButton("Vorlage erstellen", self)
|
||||
btn_vorlage_erstellen.clicked.connect(self._on_vorlage_erstellen)
|
||||
self._btn_vorlage_erstellen = btn_vorlage_erstellen
|
||||
@@ -208,7 +273,7 @@ class PrintTab(QWidget):
|
||||
return
|
||||
|
||||
saved_kartenname = get_variable(KARTENNAME_VAR, scope="project")
|
||||
if saved_kartenname in (KARTENNAME_38, KARTENNAME_41):
|
||||
if saved_kartenname in (KARTENNAME_38, KARTENNAME_41, KARTENNAME_BAUFREIGABE):
|
||||
self._kartenname_combo.setCurrentText(saved_kartenname)
|
||||
else:
|
||||
self._kartenname_combo.setCurrentText(KARTENNAME_PLACEHOLDER)
|
||||
@@ -252,12 +317,30 @@ class PrintTab(QWidget):
|
||||
druckpuffer_cm = DRUCKPUFFER_STANDARD_CM
|
||||
self._druckpuffer_button.setText(self._format_druckpuffer_label(max(0, druckpuffer_cm)))
|
||||
|
||||
if self._atlas_erzeugen_cb:
|
||||
atlas_raw = get_variable(ATLAS_ERZEUGEN_VAR, scope="project")
|
||||
atlas_checked = True if atlas_raw in (None, "") else str(atlas_raw).strip().lower() in {
|
||||
"1",
|
||||
"true",
|
||||
"ja",
|
||||
"yes",
|
||||
"on",
|
||||
}
|
||||
self._atlas_erzeugen_cb.setChecked(atlas_checked)
|
||||
self._on_atlas_erzeugen_changed(1 if atlas_checked else 0)
|
||||
|
||||
self._restore_atlas_layer_selection()
|
||||
|
||||
def _on_kartenname_changed(self, value: str) -> None:
|
||||
if value in (KARTENNAME_38, KARTENNAME_41):
|
||||
if value in (KARTENNAME_38, KARTENNAME_41, KARTENNAME_BAUFREIGABE):
|
||||
set_variable(KARTENNAME_VAR, value, scope="project")
|
||||
else:
|
||||
set_variable(KARTENNAME_VAR, "", scope="project")
|
||||
|
||||
# Atlas-Bereich automatisch ein-/ausklappen je nach Kartenname.
|
||||
if self._atlas_button:
|
||||
self._atlas_button.setChecked(value == KARTENNAME_BAUFREIGABE)
|
||||
|
||||
if self.logic:
|
||||
self.logic.set_kartenname_for_auswahl(value)
|
||||
|
||||
@@ -291,6 +374,21 @@ class PrintTab(QWidget):
|
||||
if value == DRUCKBEREICH_RECHTECK:
|
||||
self.logic.activate_druckrechteck_capture()
|
||||
|
||||
def _on_atlas_erzeugen_changed(self, state: int) -> None:
|
||||
enabled = bool(state)
|
||||
set_variable(ATLAS_ERZEUGEN_VAR, "1" if enabled else "0", scope="project")
|
||||
|
||||
if self._atlas_layer_combo:
|
||||
set_enabled = getattr(self._atlas_layer_combo, "setEnabled", None)
|
||||
if callable(set_enabled):
|
||||
try:
|
||||
set_enabled(enabled)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if self._btn_auswahl_as_atlas:
|
||||
self._btn_auswahl_as_atlas.setEnabled(enabled)
|
||||
|
||||
def _on_druckpuffer_clicked(self) -> None:
|
||||
if not self.pruefmanager:
|
||||
return
|
||||
@@ -340,8 +438,38 @@ class PrintTab(QWidget):
|
||||
druckbereich_auswahl=self._druckbereich_combo.currentText() if self._druckbereich_combo else DRUCKBEREICH_VERFAHRENSGEBIET,
|
||||
)
|
||||
|
||||
def _disconnect_project_signals(self) -> None:
|
||||
try:
|
||||
project = QgsProject.instance()
|
||||
except Exception:
|
||||
return
|
||||
|
||||
if project is None:
|
||||
return
|
||||
|
||||
for signal_name in ("readProject", "newProjectCreated", "cleared"):
|
||||
signal = getattr(project, signal_name, None)
|
||||
if signal is None:
|
||||
continue
|
||||
try:
|
||||
signal.disconnect(self._on_project_changed)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def cleanup(self) -> None:
|
||||
"""Trennt Signalverbindungen vor dem Schliessen des Tabs."""
|
||||
self._disconnect_theme_collection_signals()
|
||||
self._disconnect_project_signals()
|
||||
|
||||
def _connect_project_signals(self) -> None:
|
||||
project = QgsProject.instance()
|
||||
try:
|
||||
project = QgsProject.instance()
|
||||
except Exception:
|
||||
return
|
||||
|
||||
if project is None:
|
||||
return
|
||||
|
||||
for signal_name in ("readProject", "newProjectCreated", "cleared"):
|
||||
signal = getattr(project, signal_name, None)
|
||||
if signal is None:
|
||||
@@ -358,6 +486,36 @@ class PrintTab(QWidget):
|
||||
self._connect_theme_collection_signals()
|
||||
self._restore_state()
|
||||
|
||||
def _toggle_atlas(self, checked: bool) -> None:
|
||||
if self._atlas_button:
|
||||
self._atlas_button.setArrowType(ArrowDown if checked else ArrowRight)
|
||||
if self._atlas_content:
|
||||
self._atlas_content.setVisible(checked)
|
||||
|
||||
def _restore_atlas_layer_selection(self) -> None:
|
||||
if not self._atlas_layer_combo:
|
||||
return
|
||||
|
||||
layer_id = (get_variable("sn_abdeckungslayer", scope="project") or "").strip()
|
||||
if not layer_id:
|
||||
return
|
||||
|
||||
try:
|
||||
layer = QgsProject.instance().mapLayer(layer_id)
|
||||
if layer is None:
|
||||
return
|
||||
|
||||
set_current_layer = getattr(self._atlas_layer_combo, "setLayer", None)
|
||||
if callable(set_current_layer):
|
||||
set_current_layer(layer)
|
||||
return
|
||||
|
||||
set_current_layer = getattr(self._atlas_layer_combo, "setCurrentLayer", None)
|
||||
if callable(set_current_layer):
|
||||
set_current_layer(layer)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _disconnect_theme_collection_signals(self) -> None:
|
||||
collection = self._connected_theme_collection
|
||||
if collection is not None:
|
||||
@@ -450,3 +608,101 @@ class PrintTab(QWidget):
|
||||
return float(scale) if scale else None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _on_atlas_layer_changed(self, layer_or_name: Any) -> None:
|
||||
"""Speichert den ausgewaehlten Atlas-Layer als Layer-ID in Projektvariablen."""
|
||||
if layer_or_name is None or layer_or_name == "":
|
||||
set_variable("sn_abdeckungslayer", "", scope="project")
|
||||
return
|
||||
|
||||
# Standardfall QgsMapLayerComboBox: echtes Layer-Objekt
|
||||
layer_id_getter = getattr(layer_or_name, "id", None)
|
||||
if callable(layer_id_getter):
|
||||
layer_id = layer_id_getter() or ""
|
||||
set_variable("sn_abdeckungslayer", layer_id, scope="project")
|
||||
return
|
||||
|
||||
# Fallback: ggf. Name aus Mock/anderen Widgets
|
||||
value = str(layer_or_name).strip()
|
||||
if not value:
|
||||
set_variable("sn_abdeckungslayer", "", scope="project")
|
||||
return
|
||||
|
||||
try:
|
||||
project = QgsProject.instance()
|
||||
map_layers = project.mapLayers()
|
||||
for layer_id, layer in map_layers.items():
|
||||
if getattr(layer, "name", lambda: "")() == value:
|
||||
set_variable("sn_abdeckungslayer", layer_id, scope="project")
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _on_auswahl_as_atlas_clicked(self) -> None:
|
||||
"""Handler fuer Button 'Auswahl als Atlas anlegen'.
|
||||
|
||||
Erstellt einen neuen Layer 'Atlaslayer' aus der aktuellen Feature-Auswahl
|
||||
und traegt ihn ins Atlas-Dropdown ein.
|
||||
"""
|
||||
if not self.logic or not self.pruefmanager:
|
||||
return
|
||||
|
||||
atlas_layer, layer_id = self.logic.create_atlas_layer_from_selection()
|
||||
if atlas_layer is None or not layer_id:
|
||||
return
|
||||
|
||||
if self._atlas_button and self._atlas_content:
|
||||
self._atlas_button.setChecked(True)
|
||||
self._toggle_atlas(True)
|
||||
|
||||
# Neu erstellten Layer im Layer-Combo setzen
|
||||
if self._atlas_layer_combo:
|
||||
set_current_layer = getattr(self._atlas_layer_combo, "setLayer", None)
|
||||
if callable(set_current_layer):
|
||||
try:
|
||||
set_current_layer(atlas_layer)
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
set_current_layer = getattr(self._atlas_layer_combo, "setCurrentLayer", None)
|
||||
if callable(set_current_layer):
|
||||
try:
|
||||
set_current_layer(atlas_layer)
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Fallback nur fuer Mock/klassische ComboBox
|
||||
self._populate_atlas_layer_combo()
|
||||
|
||||
def _populate_atlas_layer_combo(self) -> None:
|
||||
"""Fallback-Befuellung, falls kein echtes QgsMapLayerComboBox Verhalten verfuegbar ist."""
|
||||
if not self._atlas_layer_combo:
|
||||
return
|
||||
|
||||
clear_fn = getattr(self._atlas_layer_combo, "clear", None)
|
||||
add_item_fn = getattr(self._atlas_layer_combo, "addItem", None)
|
||||
if not callable(clear_fn) or not callable(add_item_fn):
|
||||
return
|
||||
|
||||
self._atlas_layer_combo.blockSignals(True)
|
||||
clear_fn()
|
||||
add_item_fn("")
|
||||
|
||||
try:
|
||||
project = QgsProject.instance()
|
||||
map_layers = getattr(project, "mapLayers", None)
|
||||
if not callable(map_layers):
|
||||
self._atlas_layer_combo.blockSignals(False)
|
||||
return
|
||||
|
||||
layers = map_layers().values() if hasattr(map_layers(), "values") else map_layers()
|
||||
for layer in layers:
|
||||
layer_name = getattr(layer, "name", lambda: None)()
|
||||
if layer_name and layer_name not in ("", "Druckrechteck"):
|
||||
add_item_fn(layer_name)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self._atlas_layer_combo.blockSignals(False)
|
||||
|
||||
+19
-1
@@ -1,4 +1,8 @@
|
||||
#sn_basis/ui/tabs/settings_tab.py
|
||||
"""
|
||||
sn_basis/ui/tabs/settings_tab.py – Einstellungen-Tab für Projekteinformationen.
|
||||
Verwaltet benutzerspezifische und projektspezifische Einstellungen.
|
||||
"""
|
||||
|
||||
from sn_basis.functions.qt_wrapper import (
|
||||
QWidget,
|
||||
QGridLayout,
|
||||
@@ -12,9 +16,23 @@ from sn_basis.functions.settings_logic import SettingsLogic
|
||||
|
||||
|
||||
class SettingsTab(QWidget):
|
||||
"""
|
||||
Tab-Widget für die Verwaltung von Projekt- und Benutzereinstellungen.
|
||||
|
||||
Attributes:
|
||||
tab_title: Anzeigetitel des Tabs ("Projekteigenschaften")
|
||||
logic: SettingsLogic-Instanz für persistente Einstellungen
|
||||
"""
|
||||
|
||||
tab_title = "Projekteigenschaften"
|
||||
|
||||
def __init__(self, parent=None):
|
||||
"""
|
||||
Initialisiert den Settings-Tab mit Eingabefeldern.
|
||||
|
||||
Args:
|
||||
parent: Übergeordnetes Widget (Standard: None)
|
||||
"""
|
||||
super().__init__(parent)
|
||||
self.logic = SettingsLogic()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user