45 Commits

Author SHA1 Message Date
2ff465b86d Merge pull request 'feture/Druck_tab' (#11) from feture/Druck_tab into unstable
Reviewed-on: #11
2026-03-20 22:58:08 +01:00
f19fe71bfa Ergänzungen in de Wrappern/ Prüfmanager für Layouts 2026-03-20 14:01:57 +01:00
ae5f88c5b8 Dialoge für Vorlagen-Piepeline ergänzt 2026-03-20 12:42:21 +01:00
7cd6e3ef24 checkbox im qt_wrapper ergänzt 2026-03-20 12:01:16 +01:00
1be1420f66 changelog.txt aktualisiert 2026-03-19 06:44:35 +01:00
f25e30c489 Release 26.3.6-unstable 2026-03-19 05:32:02 +00:00
0eb32453d6 changelog.txt aktualisiert
All checks were successful
Release Plugin / release (push) Successful in 3s
2026-03-19 06:31:36 +01:00
841b529ad8 Release 26.3.5-unstable 2026-03-19 05:30:39 +00:00
ae5725cd03 Release 26.3.4-unstable
All checks were successful
Release Plugin / release (push) Successful in 3s
2026-03-19 05:23:20 +00:00
ac5a3993c8 Release 26.3.3-unstable
All checks were successful
Release Plugin / release (push) Successful in 7s
2026-03-18 14:34:41 +00:00
22b45fe19a Release 26.3.2-unstable
All checks were successful
Release Plugin / release (push) Successful in 3s
2026-03-18 14:18:14 +00:00
Michael Otto
24c2137dc2 Auf neuen Release Workflow umgestellt
All checks were successful
Release Plugin / release (push) Successful in 3s
2026-03-18 15:13:37 +01:00
Michael Otto
c0c0387b1d Änderungen an plugin.cfg
All checks were successful
Release Plugin / release (push) Successful in 7s
2026-03-13 13:58:33 +01:00
Michael Otto
663ca770a1 Schritt zu 'plugin.cfg einlesen' umbenannt und Schleife angepasst, um letzte Zeile ohne Newline zu lesen 2026-03-13 12:08:32 +01:00
Michael Otto
04319b6f7b plugin.info->plugin.cfg
Some checks failed
Release Plugin / release (push) Failing after 5s
2026-03-13 12:06:34 +01:00
Michael Otto
1c70d62739 plugin.info einlesen: echo eingefügt
Some checks failed
Release Plugin / release (push) Has been cancelled
2026-03-13 12:04:32 +01:00
Michael Otto
3971bd3408 plugin.info einlesen angepasst 2026-03-13 12:02:39 +01:00
Michael Otto
fa04fc80e3 ZIP Erstellung auf Original zurückgesetzt
Some checks failed
Release Plugin / release (push) Has been cancelled
2026-03-13 11:30:11 +01:00
Michael Otto
04bdfbe9d8 Fallback für ZIP_FOLDER hinzugefügt: Wenn leer, auf sn_basis setzen 2026-03-13 11:27:18 +01:00
Michael Otto
b6b791e5bd Behebung des Parsens von plugin.info: Robuste Schleife für Outputs
Some checks failed
Release Plugin / release (push) Failing after 5s
2026-03-13 11:24:36 +01:00
Michael Otto
82be564c29 Verbesserung des ZIP-Erstellungsprozesses: Debugging hinzugefügt und Warnungen bei /dev-Dateien durch Behandlung symbolischer Links behoben
Some checks failed
Release Plugin / release (push) Failing after 5s
2026-03-13 11:21:38 +01:00
Michael Otto
f42260b66c ZIP Erstellung geändert um im Runner genauer zu sehen wo es zu Problemen gekommen ist
Some checks failed
Release Plugin / release (push) Has been cancelled
2026-03-13 11:16:46 +01:00
Michael Otto
327c25388f metadata.txt gelöscht, changelog.txt eingefügt
Some checks failed
Release Plugin / release (push) Has been cancelled
2026-03-13 11:06:58 +01:00
Michael Otto
c6c9613120 Update plugin.info based on metadata.txt
Some checks failed
Release Plugin / release (push) Failing after 3s
2026-03-13 08:39:24 +01:00
6e1f4c615b Merge pull request 'daniel@feature/dataGrabber_anbinden' (#9) from Daniel/Plugin_SN_Basis:daniel@feature/dataGrabber_anbinden into unstable
Reviewed-on: #9
2026-03-13 06:51:56 +01:00
f876218134 plugin.info hinzugefügt 2026-03-13 06:45:55 +01:00
9829ac9c81 Diensteabruf integriert 2026-03-12 16:14:02 +01:00
ae956b0046 Überarbeitung für Pufferlayer-Fachdaten laden und gpkg-speichern/laden 2026-03-11 20:56:02 +01:00
0ec24029d8 fix QGIS 4.0-Kompatibilität;
Linkpruefer-Eingabe als String normalisiert, falls Paf-Objekte übergeben werden
2026-03-11 12:38:48 +01:00
26f426dfcd Merge pull request 'dev' (#8) from Daniel/Plugin_SN_Basis:dev into main, Basisfunktion für sn_Verfahrensgebiet
Reviewed-on: #8
2026-03-06 10:34:43 +01:00
5dc8412a6a Imports für sn_Verfahrensgebiet ergänzt, Stilprüfer wird jetzt auch für apply_style verwendet 2026-03-06 10:20:40 +01:00
Michael Otto
00f800b1e6 metadata.txt gelöscht, plugin.info eingefügt, workflow hinzugefügt 2026-03-05 16:02:03 +01:00
948041da52 Merge pull request 'qt_wrapper, dialog;wrapper, Pruef_ergebnis und Pruefmanager überarbeitet, so dass die Übergaben jetzt stimmen. Nutzerabfragen werden tatsächlich ausgelöst- Nutzerabfrage Datei überschreiebn... ist noch Blödsinn' (#7) from Daniel/Plugin_SN_Basis:dev into dev
Reviewed-on: #7
2026-03-04 19:42:41 +01:00
3b56725e4f qt_wrapper, dialog;wrapper, Pruef_ergebnis und Pruefmanager überarbeitet, so dass die Übergaben jetzt stimmen. Nutzerabfragen werden tatsächlich ausgelöst- Nutzerabfrage Datei überschreiebn... ist noch Blödsinn 2026-03-04 15:32:49 +01:00
137baaf19c .gitea/workflows/release.yaml hinzugefügt
All checks were successful
Automatisches Release mit ZIP-Archiv / Build-Release (push) Successful in 3s
2026-03-01 13:36:27 +01:00
439de5527a Merge pull request 'dev' (#6) from Daniel/Plugin_SN_Basis:dev into dev
Reviewed-on: https://entwicklung.vln-sn.de/AG_QGIS/Plugin_SN_Basis/pulls/6
2026-02-25 13:27:01 +01:00
f8be65f6f6 DataGrabber aktualisiert, grabberfunktionen aus dem Prototyp implementiert 2026-02-14 22:14:33 +01:00
e6ffab1c10 Angefangen, DataGrabber anzulegen (Grundlagen gelegt, noch nicht lauffähig) 2026-02-13 21:39:12 +01:00
039c614592 Fix: beim Plugin-Reload werden neue Toolbars hinzugefügt aber keine gelöscht
Fix: Settings-Tab ist leer
Dokumentation begonnen
Pluginkonzept.md überarbeitet
2026-01-09 15:19:25 +01:00
b805f78f02 Anpassung an den Wrappern für sn_plan41 2026-01-08 17:13:51 +01:00
f88b5da51f Wrappe modular aufgebaut, Tests erfolgreich, Menüleiste und Werzeugleiste werden eingetragen (QT6 und QT5)- (Es fehlen noch Fachplugins, um zu prüfen, ob es auch wirklich in QGIS geht) 2025-12-19 14:29:52 +01:00
e8fea163b5 Auf Wrapper umgestellt, Prüfarchitektur QT6-kompatibel gemacht (Nicht lauffähig) 2025-12-18 22:00:31 +01:00
f64d56d4bc Tests überarbeitet, Mocks und coverage eingefügt 2025-12-17 17:45:18 +01:00
2d67ce8adc Datenbank überarbeitet, V Absprache Andreas 2025-12-17 11:41:17 +01:00
1881af93f8 PruefManager und Daten aus P41 übertragen 2025-12-02 20:55:51 +01:00
71 changed files with 8961 additions and 545 deletions

12
.coveragerc Normal file
View File

@@ -0,0 +1,12 @@
[run]
source = modules
omit =
*/test/*
*/__init__.py
[report]
show_missing = True
skip_covered = False
[html]
directory = coverage_html

View File

@@ -0,0 +1,133 @@
name: Release Plugin
run-name: "Release | ${{ github.ref_name }}"
on:
push:
tags:
- 'v*'
jobs:
release:
runs-on: alpine-latest
defaults:
run:
shell: bash
steps:
- name: Notwendige Abhängigkeiten installieren
shell: sh
run: |
apk add --no-cache bash git jq curl
- name: Code holen
run: |
REPO_URL="https://${RELEASE_TOKEN}:x-oauth-basic@${{ vars.RELEASE_URL }}/${GITHUB_REPOSITORY}.git"
git clone "$REPO_URL" repo
cd repo
git checkout "$TAG"
env:
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
TAG: "${{ github.ref_name }}"
- name: Version und Kanal bestimmen
id: releaseinfo
run: |
TAG="${{ github.ref_name }}"
RAW_VERSION="${TAG#v}"
VERSION="${RAW_VERSION%%-*}"
# Channel und Suffix automatisch bestimmen anhand des Tag-Suffix
case "$RAW_VERSION" in
*-testing*|*-t|*-T)
CHANNEL="testing"
PRERELEASE="true"
SUFFIX="-testing"
;;
*-unstable*|*-u|*-U)
CHANNEL="unstable"
PRERELEASE="true"
SUFFIX="-unstable"
;;
*)
CHANNEL="stable"
PRERELEASE="false"
SUFFIX=""
;;
esac
# Output setzen
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "channel=$CHANNEL" >> $GITHUB_OUTPUT
echo "prerelease=$PRERELEASE" >> $GITHUB_OUTPUT
# Optional Debug
echo "VERSION=$VERSION"
echo "CHANNEL=$CHANNEL"
echo "PRERELEASE=$PRERELEASE"
# - name: plugin.cfg einlesen
# id: config
# run: |
# cd repo
# while read -r line || [ -n "$line" ]; do
# key="${line%%=*}"
# value="${line#*=}"
# echo "$key=$value" >> $GITHUB_OUTPUT
# done < plugin.cfg
- name: Payload erzeugen
id: payload
run: |
cd repo
NAME="${GITHUB_REPOSITORY##*/}"
GROUP="${GITHUB_REPOSITORY%%/*}"
VERSION="${{ steps.releaseinfo.outputs.version }}"
CHANNEL="${{ steps.releaseinfo.outputs.channel }}"
PRERELEASE="${{ steps.releaseinfo.outputs.prerelease }}"
ZIP_FOLDER="${{ vars.ZIP_FOLDER }}"
ZIP_FILE="${ZIP_FOLDER}.zip"
TAG="${{ github.ref_name }}"
#GIT_URL=${GITHUB_REPOSITORY}
jq -n \
--arg name "$NAME" \
--arg group "$GROUP" \
--arg version "$VERSION" \
--arg channel "$CHANNEL" \
--arg prerelease "$PRERELEASE" \
--arg zip_folder "$ZIP_FOLDER" \
--arg zip_file "$ZIP_FILE" \
--arg tag "$TAG" \
'{
name: $name,
group: $group,
version: $version,
channel: $channel,
prerelease: ($prerelease == "true"),
zip_folder: $zip_folder,
zip_file: $zip_file,
tag: $tag
}' > payload.json
cat payload.json
- name: Repository aktualisieren
run: |
NAME="${GITHUB_REPOSITORY##*/}"
TAG="${{ steps.releaseinfo.outputs.version }}"-"${{ steps.releaseinfo.outputs.channel }}"
PAYLOAD_B64=$(base64 -w0 repo/payload.json)
JSON="{\"ref\":\"hidden/workflows\",\"inputs\":{\"payload\":\"$PAYLOAD_B64\",\"name\":\"$NAME\",\"tag\":\"$TAG\"}}"
curl -X POST \
-H "Authorization: token ${{ secrets.RELEASE_TOKEN }}" \
-H "Content-Type: application/json" \
-d "$JSON" \
"https://${{ vars.RELEASE_URL }}/api/v1/repos/${OWNER}/Repository/actions/workflows/${WORKFLOW}/dispatches"
env:
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
OWNER: "AG_QGIS"
WORKFLOW: "release.yaml"

20
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,20 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Attach to QGIS (Port 5678)",
"type": "debugpy",
"request": "attach",
"connect": {
"host": "localhost",
"port": 5678
},
"pathMappings": [
{
"localRoot": "C:/Users/helbi/AppData/Roaming/QGIS/QGIS3/profiles/dev/python/plugins/sn_basis",
"remoteRoot": "C:/Users/helbi/AppData/Roaming/QGIS/QGIS3/profiles/dev/python/plugins/sn_basis"
}
]
}
]
}

35
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,35 @@
{
// OSGeo Python als Interpreter (QGIS 4.0, Qt6)
"python.defaultInterpreterPath": "D:/OSGeo/apps/Python312/python.exe",
// Pylance: zusätzliche Suchpfade
"python.analysis.extraPaths": [
"D:/OSGeo/apps/qgis/python",
"D:/OSGeo/apps/Python312/Lib/site-packages",
"C:/Users/helbi/AppData/Roaming/QGIS/QGIS3/profiles/dev/python/plugins",
"C:/Users/helbi/AppData/Roaming/QGIS/QGIS3/profiles/dev/python/plugins/sn_basis"
],
// Autocomplete ebenfalls erweitern
"python.autoComplete.extraPaths": [
"D:/OSGeo/apps/qgis/python",
"D:/OSGeo/apps/Python312/Lib/site-packages",
"C:/Users/helbi/AppData/Roaming/QGIS/QGIS3/profiles/dev/python/plugins",
"C:/Users/helbi/AppData/Roaming/QGIS/QGIS3/profiles/dev/python/plugins/sn_basis"
],
// Pylance-Modus
"python.analysis.typeCheckingMode": "basic",
"python.analysis.diagnosticMode": "workspace",
// Tests aktivieren
"python.testing.pytestEnabled": false,
"python.testing.unittestEnabled": true,
"python.testing.unittestArgs": [
"-v",
"-s",
"./test",
"-p",
"*test*.py"
]
}

View File

@@ -1,5 +1,3 @@
from .functions.variable_utils import get_variable
def classFactory(iface): def classFactory(iface):
from .main import BasisPlugin from .main import BasisPlugin
return BasisPlugin(iface) return BasisPlugin(iface)

3
__pdoc__.py Normal file
View File

@@ -0,0 +1,3 @@
__pdoc__ = {
"main": False,
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 115 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 160 KiB

188
assets/Objektstruktur.txt Normal file
View File

@@ -0,0 +1,188 @@
# WrapperArchitektur Übersicht
Die WrapperArchitektur von sn_basis bildet das Fundament für eine robuste, testbare und zukunftssichere QGISPluginEntwicklung.
Sie kapselt sämtliche QGIS und QtAbhängigkeiten hinter klar definierten Schnittstellen und ermöglicht dadurch:
Mockfähige UnitTests ohne QGIS
PyQt5/6Kompatibilität ohne CodeÄnderungen
saubere Trennung von UI, Logik und Infrastruktur
stabile APIs, die unabhängig von QGISVersionen bleiben
klare Erweiterbarkeit für zukünftige Module und Plugins
Die WrapperSchicht ist das zentrale Bindeglied zwischen der PluginLogik und der QGIS/QtUmgebung.
## Ziele der WrapperArchitektur
🎯 1. Entkopplung von QGIS und Qt
Alle direkten Importe wie from qgis.core import ... oder from qgis.PyQt.QtWidgets import ... verschwinden aus der PluginLogik.
Stattdessen werden sie über WrapperModule abstrahiert.
🎯 2. Testbarkeit ohne QGIS
Im MockModus liefern die Wrapper:
DummyObjekte
simulierte Rückgabewerte
speicherbare Zustände (z.B. Variablen, Layer, Nachrichten)
Damit laufen Tests in jeder CIUmgebung.
🎯 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 QtAPI, wird nur der Wrapper angepasst, nicht jedes Plugin.
## Architekturüberblick
Die WrapperSchicht besteht aus mehreren Modulen, die jeweils einen klar abgegrenzten Verantwortungsbereich haben.
### 1. qt_wrapper QtAbstraktion
Kapselt alle QtWidgets, Dialoge und Konstanten:
QWidget, QDialog, QMessageBox, QToolBar, QMenu, …
Layouts, Buttons, Labels, LineEdits
QtKonstanten wie YES, NO, DockAreas
MockModus:
Stellt DummyWidgets bereit, die keine UI öffnen.
### 2. qgiscore_wrapper QGISCoreAbstraktion
Abstraktion für:
QgsProject
LayerZugriff
ProjektMetadaten
Pfade, CRS, FeatureZugriff
MockModus:
Simuliert ein Projekt und LayerContainer.
### 3. qgisui_wrapper QGISUIAbstraktion
Kapselt UIbezogene QGISFunktionen:
Zugriff auf iface
DockManagement
Menü und ToolbarIntegration
HauptfensterZugriff
MockModus:
Stellt ein DummyInterface bereit.
### 4. variable_wrapper QGISVariablen
Abstraktion für:
Projektvariablen (projectScope)
globale Variablen (globalScope)
MockSpeicher für Tests
Vorteile:
keine QGISAbhängigkeit in der Logik
testbare Variablenverwaltung
einheitliches API
### 5. message_wrapper Meldungen & Logging
Einheitliche Schnittstelle für:
Fehlermeldungen
Warnungen
InfoMeldungen
Logging
MockModus:
Speichert Nachrichten statt sie an QGIS zu senden.
### 6. dialog_wrapper BenutzerDialoge
Abstraktion für:
Ja/NeinDialoge
spätere Erweiterungen (Eingabedialoge, Dateidialoge, etc.)
MockModus:
Gibt DefaultWerte zurück, öffnet keine UI.
### 7. DockManager & Navigation
Diese Module nutzen die WrapperSchicht, um:
DockWidgets sicher zu verwalten
Toolbars und Menüs zu erzeugen
Reloadsichere UIStrukturen aufzubauen
Sie sind keine Wrapper, sondern WrapperKonsumenten.
## Designprinzipien
🧱 1. Single Source of Truth
Jede QGIS oder QtFunktionalität wird nur an einer Stelle implementiert.
🔄 2. Austauschbarkeit
MockModus und Echtmodus sind vollständig austauschbar.
🧪 3. Testbarkeit
Jede Funktion kann ohne QGIS getestet werden.
🧼 4. Saubere Trennung
UI → qt_wrapper
QGISCore → qgiscore_wrapper
QGISUI → 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 QGISAbhängigkeiten in der Logik
IDEfreundlich (Pylance, Autocomplete, Typing)
CIfähig (Tests ohne QGIS)
saubere Architektur
leichte Wartbarkeit
klare Dokumentation
## Fazit
Die WrapperArchitektur ist das Herzstück von sn_basis.
Sie ermöglicht eine moderne, modulare und testbare QGISPluginEntwicklung, die unabhängig von QGISVersionen, QtVersionen und Entwicklungsumgebungen funktioniert.
Sie bildet die Grundlage für:
stabile APIs
saubere UIAbstraktion
automatisierte Tests
nachhaltige Weiterentwicklung

144
assets/Pluginkonzept.md Normal file
View File

@@ -0,0 +1,144 @@
# WrapperArchitektur Übersicht
Die WrapperArchitektur von sn_basis bildet das Fundament für eine robuste, testbare und zukunftssichere QGISPluginEntwicklung.
Sie kapselt sämtliche QGIS und QtAbhängigkeiten hinter klar definierten Schnittstellen und ermöglicht dadurch:
- Mockfähige UnitTests ohne QGIS
- PyQt5/6Kompatibilität ohne CodeÄnderungen
- saubere Trennung von UI, Logik und Infrastruktur
- stabile APIs, die unabhängig von QGISVersionen bleiben
- klare Erweiterbarkeit für zukünftige Module und Plugins
Die WrapperSchicht ist das zentrale Bindeglied zwischen der PluginLogik und der QGIS/QtUmgebung.
## Ziele der WrapperArchitektur
1. Entkopplung von QGIS und Qt
Alle direkten Importe wie from qgis.core import ... oder from qgis.PyQt.QtWidgets import ... verschwinden aus der PluginLogik.
Stattdessen werden sie über WrapperModule abstrahiert.
2. Testbarkeit ohne QGIS
Im MockModus liefern die Wrapper:
- DummyObjekte
- simulierte Rückgabewerte
- speicherbare Zustände (z.B. Variablen, Layer, Nachrichten)
Damit laufen Tests in jeder CIUmgebung.
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 QtAPI, wird nur der Wrapper angepasst, nicht jedes Plugin.
## Architekturüberblick
Die WrapperSchicht besteht aus mehreren Modulen, die jeweils einen klar abgegrenzten Verantwortungsbereich haben.
### 1. qt_wrapper QtAbstraktion
Kapselt alle QtWidgets, Dialoge und Konstanten:
- QWidget, QDialog, QMessageBox, QToolBar, QMenu, …
- Layouts, Buttons, Labels, LineEdits
- QtKonstanten wie YES, NO, DockAreas
MockModus:
Stellt DummyWidgets bereit, die keine UI öffnen.
### 2. qgiscore_wrapper QGISCoreAbstraktion
Abstraktion für:
- QgsProject
- LayerZugriff
- ProjektMetadaten
- Pfade, CRS, FeatureZugriff
MockModus:
Simuliert ein Projekt und LayerContainer.
### 3. qgisui_wrapper QGISUIAbstraktion
Kapselt UIbezogene QGISFunktionen:
- Zugriff auf iface
- DockManagement
- Menü und ToolbarIntegration
- HauptfensterZugriff
MockModus:
Stellt ein DummyInterface bereit.
### 4. variable_wrapper QGISVariablen
Abstraktion für:
- Projektvariablen (projectScope)
- globale Variablen (globalScope)
- MockSpeicher für Tests
Vorteile:
- keine QGISAbhängigkeit in der Logik
- testbare Variablenverwaltung
- einheitliches API
### 5. message_wrapper Meldungen & Logging
Einheitliche Schnittstelle für:
- Fehlermeldungen
- Warnungen
- InfoMeldungen
- Logging
MockModus:
Speichert Nachrichten statt sie an QGIS zu senden.
### 6. dialog_wrapper BenutzerDialoge
Abstraktion für:
- Ja/NeinDialoge
- spätere Erweiterungen (Eingabedialoge, Dateidialoge, etc.)
MockModus:
Gibt DefaultWerte zurück, öffnet keine UI.
### 7. DockManager & Navigation
Diese Module nutzen die WrapperSchicht, um:
- DockWidgets sicher zu verwalten
- Toolbars und Menüs zu erzeugen
- Reloadsichere UIStrukturen aufzubauen
Sie sind keine Wrapper, sondern WrapperKonsumenten. Alle Fach-Plugins nutzen den Dockmanager des Basisplugins.
## Designprinzipien
1. Single Source of Truth
Jede QGIS oder QtFunktionalität wird nur an einer Stelle implementiert.
2. Austauschbarkeit
MockModus und Echtmodus sind vollständig austauschbar.
3. Testbarkeit
Jede Funktion kann ohne QGIS getestet werden.
4. Saubere Trennung
- UI → qt_wrapper
- QGISCore → qgiscore_wrapper
- QGISUI → 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

After

Width:  |  Height:  |  Size: 15 KiB

BIN
assets/UML_Struktur.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

BIN
assets/datagrabber.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

38
assets/datagrabber.md Normal file
View File

@@ -0,0 +1,38 @@
```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
```

BIN
assets/datagrabber.pdf Normal file

Binary file not shown.

View File

@@ -0,0 +1,9 @@
```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
changelog.txt Normal file
View File

View File

@@ -0,0 +1,43 @@
from .ly_existence_wrapper import layer_exists
from .ly_geometry_wrapper import (
get_layer_geometry_type,
get_layer_feature_count,
)
from .ly_visibility_wrapper import (
is_layer_visible,
set_layer_visible,
)
from .ly_metadata_wrapper import (
get_layer_type,
get_layer_crs,
get_layer_fields,
get_layer_source,
is_layer_editable,
)
from .ly_style_wrapper import apply_style
from .dialog_wrapper import ask_yes_no, ask_overwrite_append_cancel_custom
from .message_wrapper import (
_get_message_bar,
push_message,
error,
warning,
info,
success,
)
from .os_wrapper import *
from .qgiscore_wrapper import *
from .qt_wrapper import *
from .settings_logic import *
from .sys_wrapper import *
from .variable_wrapper import *
from .qgisui_wrapper import (
get_main_window,
add_dock_widget,
remove_dock_widget,
find_dock_widgets,
add_menu,
remove_menu,
add_toolbar,
remove_toolbar)

226
functions/dialog_wrapper.py Normal file
View File

@@ -0,0 +1,226 @@
"""
sn_basis/functions/dialog_wrapper.py Benutzer-Dialoge (Qt5/6/Mock-kompatibel)
"""
from typing import Any
from typing import Literal, Optional
from sn_basis.functions.qt_wrapper import (
QMessageBox, YES, NO, CANCEL, QT_VERSION, exec_dialog, ICON_QUESTION,
QProgressDialog, QCoreApplication, Qt, QInputDialog, QLineEdit,
)
def ask_yes_no(
title: str,
message: str,
default: bool = True,
parent: Any = None,
) -> bool:
"""
Stellt Ja/Nein-Frage. Funktioniert in PyQt5/6 UND Mock-Modus.
"""
try:
if QT_VERSION == 0: # Mock-Modus
print(f"🔍 Mock-Modus: ask_yes_no('{title}') → {default}")
return default
# ✅ KORREKT: Verwende YES/NO-Aliase aus qt_wrapper!
buttons = YES | NO
default_button = YES if default else NO
result = QMessageBox.question(
parent, title, message, buttons, default_button
)
# ✅ int(result) == int(YES) funktioniert Qt5/6/Mock
print(f"DEBUG ask_yes_no: result={result}, YES={YES}, match={int(result) == int(YES)}")
return int(result) == int(YES)
except Exception as e:
print(f"⚠️ ask_yes_no Fehler: {e}")
return default
def show_info_dialog(title: str, message: str, parent: Any = None) -> None:
"""
Zeigt einen modalen Info-Dialog mit OK-Button.
Blockiert bis der Nutzer bestätigt.
"""
try:
if QT_VERSION == 0: # Mock-Modus
print(f"Mock-Modus: show_info_dialog('{title}')")
return
QMessageBox.information(parent, title, message)
except Exception as e:
print(f"⚠️ show_info_dialog Fehler: {e}")
def ask_text(
title: str,
label: str,
default_text: str = "",
parent: Any = None,
) -> tuple[str, bool]:
"""Zeigt einen modalen Texteingabe-Dialog und gibt Text + OK-Status zurück."""
try:
if QT_VERSION == 0: # Mock-Modus
print(f"Mock-Modus: ask_text('{title}') -> '{default_text}'")
return default_text, True
# PyQt6: QLineEdit.EchoMode.Normal / PyQt5: QLineEdit.Normal
echo_mode = (
getattr(QLineEdit, "Normal", None)
or getattr(getattr(QLineEdit, "EchoMode", None), "Normal", None)
or 0
)
text, accepted = QInputDialog.getText(
parent,
title,
label,
echo_mode,
default_text,
)
return str(text or ""), bool(accepted)
except Exception as e:
print(f"⚠️ ask_text Fehler: {e}")
return default_text, False
OverwriteDecision = Optional[Literal["overwrite", "append", "cancel"]]
def ask_overwrite_append_cancel_custom(
parent,
title: str,
message: str,
) -> Literal["overwrite", "append", "cancel"]:
"""Zeigt Dialog mit benutzerdefinierten Buttons: Überschreiben/Anhängen/Abbrechen.
Parameters
----------
parent :
Eltern-Widget oder None.
title : str
Dialog-Titel.
message : str
Hauptmeldung mit Erklärung.
Returns
-------
Literal["overwrite", "append", "cancel"]
Genaue Entscheidung des Nutzers.
"""
msg = QMessageBox(parent)
msg.setIcon(ICON_QUESTION)
msg.setWindowTitle(title)
msg.setText(message)
# Eigene Buttons mit exakten Texten
overwrite_btn = msg.addButton("Überschreiben", QMessageBox.ButtonRole.AcceptRole)
append_btn = msg.addButton("Anhängen", QMessageBox.ButtonRole.ActionRole)
cancel_btn = msg.addButton("Abbrechen", QMessageBox.ButtonRole.RejectRole)
exec_dialog(msg)
clicked = msg.clickedButton()
if clicked == overwrite_btn:
return "overwrite"
elif clicked == append_btn:
return "append"
else: # cancel_btn
return "cancel"
class ProgressDialog:
def __init__(self, total: int, title: str = "Fortschritt", label: str = "Verarbeite..."):
self.total = max(total, 1)
self._canceled = False
if QT_VERSION == 0:
self.value = 0
self.label = label
self.title = title
return
self._dlg = QProgressDialog(label, "Abbrechen", 0, self.total)
self._dlg.setWindowTitle(title)
# Qt5 vs Qt6: WindowModality-Enum unterschiedlich verfügbar
modality = None
if hasattr(Qt, "WindowModality"):
try:
modality = Qt.WindowModality.WindowModal
except Exception:
modality = None
if modality is None and hasattr(Qt, "WindowModal"):
modality = Qt.WindowModal
if modality is not None:
try:
self._dlg.setWindowModality(modality)
except Exception:
pass
self._dlg.setMinimumDuration(0)
self._dlg.setAutoClose(False)
self._dlg.setAutoReset(False)
self._dlg.setValue(0)
def on_cancel():
if self._dlg and self._dlg.value() >= self.total:
# OK-Button am Ende
self._dlg.close()
return
self._canceled = True
self._dlg.close()
try:
self._dlg.canceled.connect(on_cancel)
except Exception:
pass
def set_total(self, total: int) -> None:
self.total = max(total, 1)
if QT_VERSION == 0:
return
if self._dlg is not None:
self._dlg.setMaximum(self.total)
def set_value(self, value: int) -> None:
if QT_VERSION == 0:
self.value = value
return
if self._dlg is not None:
self._dlg.setValue(min(value, self.total))
if value >= self.total:
self._dlg.setLabelText("Fertig. Klicken Sie auf OK, um das Fenster zu schließen.")
self._dlg.setCancelButtonText("OK")
QCoreApplication.processEvents()
def set_label(self, text: str) -> None:
if QT_VERSION == 0:
self.label = text
return
if self._dlg is not None:
self._dlg.setLabelText(text)
QCoreApplication.processEvents()
def is_canceled(self) -> bool:
if QT_VERSION == 0:
return self._canceled
if self._dlg is not None:
return self._canceled or self._dlg.wasCanceled()
return self._canceled
def close(self) -> None:
if QT_VERSION == 0:
return
if self._dlg is not None:
self._dlg.close()
def create_progress_dialog(total: int, title: str = "Fortschritt", label: str = "Verarbeite...") -> ProgressDialog:
return ProgressDialog(total, title, label)

View File

@@ -0,0 +1,31 @@
# sn_basis/functions/ly_existence_wrapper.py
def layer_exists(layer) -> bool:
"""
Prüft, ob ein Layer-Objekt existiert (nicht None).
"""
return layer is not None
def layer_is_valid(layer) -> bool:
"""
Prüft, ob ein Layer gültig ist (QGIS-konform).
"""
if layer is None:
return False
is_valid = getattr(layer, "isValid", None)
if callable(is_valid):
try:
return bool(is_valid())
except Exception:
return False
return False
def layer_is_usable(layer) -> bool:
"""
Prüft, ob ein Layer existiert und gültig ist.
"""
return layer_exists(layer) and layer_is_valid(layer)

View File

@@ -0,0 +1,65 @@
# sn_basis/functions/ly_geometry_wrapper.py
from typing import Optional
GEOM_NONE = None
GEOM_POINT = "Point"
GEOM_LINE = "LineString"
GEOM_POLYGON = "Polygon"
def get_layer_geometry_type(layer) -> Optional[str]:
"""
Gibt den Geometrietyp eines Layers zurück.
Rückgabewerte:
- "Point"
- "LineString"
- "Polygon"
- None (nicht räumlich / ungültig / unbekannt)
"""
if layer is None:
return None
try:
is_spatial = getattr(layer, "isSpatial", None)
if callable(is_spatial) and not is_spatial():
return None
gtype = getattr(layer, "geometryType", None)
if callable(gtype):
value = gtype()
if value == 0:
return GEOM_POINT
if value == 1:
return GEOM_LINE
if value == 2:
return GEOM_POLYGON
except Exception:
pass
return None
def get_layer_feature_count(layer) -> int:
"""
Gibt die Anzahl der Features eines Layers zurück.
"""
if layer is None:
return 0
try:
is_spatial = getattr(layer, "isSpatial", None)
if callable(is_spatial) and not is_spatial():
return 0
fc = getattr(layer, "featureCount", None)
if callable(fc):
value = fc()
if isinstance(value, int):
return value
except Exception:
pass
return 0

View File

@@ -0,0 +1,97 @@
# sn_basis/functions/ly_metadata_wrapper.py
from typing import Optional, List
LAYER_TYPE_VECTOR = "vector"
LAYER_TYPE_TABLE = "table"
def get_layer_type(layer) -> Optional[str]:
"""
Gibt den Layer-Typ zurück.
Rückgabewerte:
- "vector"
- "table"
- None (unbekannt / nicht bestimmbar)
"""
if layer is None:
return None
try:
is_spatial = getattr(layer, "isSpatial", None)
if callable(is_spatial):
return LAYER_TYPE_VECTOR if is_spatial() else LAYER_TYPE_TABLE
except Exception:
pass
return None
def get_layer_crs(layer) -> Optional[str]:
"""
Gibt das CRS als AuthID zurück (z.B. 'EPSG:25833').
"""
if layer is None:
return None
try:
crs = layer.crs()
authid = getattr(crs, "authid", None)
if callable(authid):
value = authid()
if isinstance(value, str):
return value
except Exception:
pass
return None
def get_layer_fields(layer) -> List[str]:
"""
Gibt die Feldnamen eines Layers zurück.
"""
if layer is None:
return []
try:
return list(layer.fields().names())
except Exception:
return []
def get_layer_source(layer) -> Optional[str]:
"""
Gibt die Datenquelle eines Layers zurück.
"""
if layer is None:
return None
try:
value = layer.source()
if isinstance(value, str) and value:
return value
except Exception:
pass
return None
def is_layer_editable(layer) -> bool:
"""
Prüft, ob ein Layer editierbar ist.
"""
if layer is None:
return False
try:
is_editable = getattr(layer, "isEditable", None)
if callable(is_editable):
return bool(is_editable())
except Exception:
pass
return False

View File

@@ -0,0 +1,48 @@
# sn_basis/functions/ly_style_wrapper.py
from sn_basis.functions.ly_existence_wrapper import layer_exists
from sn_basis.functions.sys_wrapper import get_plugin_root, join_path
from sn_basis.modules.stilpruefer import Stilpruefer
from typing import Optional
def apply_style(layer, style_name: str) -> bool:
"""
Wendet einen Layerstil an, sofern er gültig ist.
- Validierung erfolgt ausschließlich über Stilpruefer
- Keine eigenen Dateisystem- oder Endungsprüfungen
- Keine Seiteneffekte bei ungültigem Stil
"""
print(">>> apply_style() START")
if not layer_exists(layer):
return False
# Stilpfad zusammensetzen
style_path = join_path(get_plugin_root(), "sn_verfahrensgebiet","styles", style_name)
# Stil prüfen
pruefer = Stilpruefer()
ergebnis = pruefer.pruefe(style_path)
print(">>> Stilprüfung:", ergebnis)
print(
f"[Stilprüfung] ok={ergebnis.ok} | "
f"aktion={ergebnis.aktion} | "
f"meldung={ergebnis.meldung}"
)
if not ergebnis.ok:
return False
# Stil anwenden
try:
ok, _ = layer.loadNamedStyle(str(ergebnis.kontext))
if ok:
getattr(layer, "triggerRepaint", lambda: None)()
return True
except Exception:
pass
return False

View File

@@ -0,0 +1,41 @@
# sn_basis/functions/ly_visibility_wrapper.py
def is_layer_visible(layer) -> bool:
"""
Prüft, ob ein Layer im Layer-Tree sichtbar ist.
"""
if layer is None:
return False
try:
node = getattr(layer, "treeLayer", None)
if callable(node):
tree_node = node()
is_visible = getattr(tree_node, "isVisible", None)
if callable(is_visible):
return bool(is_visible())
except Exception:
pass
return False
def set_layer_visible(layer, visible: bool) -> bool:
"""
Setzt die Sichtbarkeit eines Layers im Layer-Tree.
"""
if layer is None:
return False
try:
node = getattr(layer, "treeLayer", None)
if callable(node):
tree_node = node()
setter = getattr(tree_node, "setItemVisibilityChecked", None)
if callable(setter):
setter(bool(visible))
return True
except Exception:
pass
return False

View File

@@ -0,0 +1,84 @@
"""
sn_basis/functions/message_wrapper.py zentrale MessageBar-Abstraktion
"""
from typing import Any
from sn_basis.functions.qgisui_wrapper import iface
from sn_basis.functions.qgiscore_wrapper import Qgis
# ---------------------------------------------------------
# Interne Hilfsfunktion
# ---------------------------------------------------------
def _get_message_bar():
"""
Liefert eine MessageBar-Instanz (QGIS oder Mock).
"""
try:
bar = iface.messageBar()
if bar is not None:
return bar
except Exception:
pass
class _MockMessageBar:
def pushMessage(self, title, text, level=0, duration=5):
return {
"title": title,
"text": text,
"level": level,
"duration": duration,
}
return _MockMessageBar()
# ---------------------------------------------------------
# Öffentliche API
# ---------------------------------------------------------
def push_message(
level: int,
title: str,
text: str,
duration: int = 5,
parent: Any = None,
):
"""
Zeigt eine Message in der QGIS-MessageBar an.
Im Mock-Modus wird ein strukturierter Dict zurückgegeben.
"""
bar = _get_message_bar()
try:
return bar.pushMessage(
title,
text,
level=level,
duration=duration,
)
except Exception:
return None
def info(title: str, text: str, duration: int = 5):
level = Qgis.MessageLevel.Info
return push_message(level, title, text, duration)
def warning(title: str, text: str, duration: int = 5):
level = Qgis.MessageLevel.Warning
return push_message(level, title, text, duration)
def error(title: str, text: str, duration: int = 5):
level = Qgis.MessageLevel.Critical
return push_message(level, title, text, duration)
def success(title: str, text: str, duration: int = 5):
level = Qgis.MessageLevel.Success
return push_message(level, title, text, duration)

View File

@@ -1,44 +0,0 @@
# sn_basis/functions/messages.py
from typing import Optional
from qgis.core import Qgis
from qgis.PyQt.QtWidgets import QWidget
from qgis.utils import iface
def push_message(
level: Qgis.MessageLevel,
title: str,
text: str,
duration: Optional[int] = 5,
parent: Optional[QWidget] = None,
):
"""
Zeigt eine Meldung in der QGIS-MessageBar.
- level: Qgis.Success | Qgis.Info | Qgis.Warning | Qgis.Critical
- title: Überschrift links (kurz halten)
- text: eigentliche Nachricht
- duration: Sekunden bis Auto-Ausblendung; None => bleibt sichtbar (mit Close-Button)
- parent: optionales Eltern-Widget (für Kontext), normalerweise nicht nötig
Rückgabe: MessageBarItem-Widget (kann später geschlossen/entfernt werden).
"""
bar = iface.messageBar()
# QGIS akzeptiert None als "sticky" Meldung
return bar.pushMessage(title, text, level=level, duration=duration)
def success(title: str, text: str, duration: int = 5):
return push_message(Qgis.Success, title, text, duration)
def info(title: str, text: str, duration: int = 5):
return push_message(Qgis.Info, title, text, duration)
def warning(title: str, text: str, duration: int = 5):
return push_message(Qgis.Warning, title, text, duration)
def error(title: str, text: str, duration: Optional[int] = 5):
# Fehler evtl. länger sichtbar lassen; setze duration=None falls gewünscht
return push_message(Qgis.Critical, title, text, duration)

101
functions/os_wrapper.py Normal file
View File

@@ -0,0 +1,101 @@
"""
sn_basis/functions/os_wrapper.py Betriebssystem-Abstraktion
"""
from pathlib import Path
import platform
from typing import Union
# ---------------------------------------------------------
# OS-Erkennung
# ---------------------------------------------------------
_SYSTEM = platform.system().lower()
if _SYSTEM.startswith("win"):
OS_NAME = "windows"
elif _SYSTEM.startswith("darwin"):
OS_NAME = "macos"
else:
OS_NAME = "linux"
IS_WINDOWS = OS_NAME == "windows"
IS_LINUX = OS_NAME == "linux"
IS_MACOS = OS_NAME == "macos"
# ---------------------------------------------------------
# OS-Eigenschaften
# ---------------------------------------------------------
PATH_SEPARATOR = "\\" if IS_WINDOWS else "/"
LINE_SEPARATOR = "\r\n" if IS_WINDOWS else "\n"
# ---------------------------------------------------------
# Pfad-Utilities
# ---------------------------------------------------------
_PathLike = Union[str, Path]
def normalize_path(path: _PathLike) -> Path:
"""
Normalisiert einen Pfad OS-unabhängig.
"""
try:
return Path(path).expanduser().resolve()
except Exception:
return Path(path)
def get_home_dir() -> Path:
"""
Liefert das Home-Verzeichnis des aktuellen Users.
"""
return Path.home()
def is_absolute_path(path: _PathLike) -> bool:
"""Prüft, ob ein Pfad absolut ist."""
try:
return Path(path).is_absolute()
except Exception:
return False
def basename(path: _PathLike) -> str:
"""Gibt den finalen Namen des Pfades zurück (Dateiname oder Ordner)."""
try:
return Path(path).name
except Exception:
return ""
# ---------------------------------------------------------
# Dateisystem-Eigenschaften
# ---------------------------------------------------------
def is_case_sensitive_fs() -> bool:
"""
Gibt zurück, ob das Dateisystem case-sensitiv ist.
"""
# Windows ist immer case-insensitive
if IS_WINDOWS:
return False
# macOS meist case-insensitive, aber nicht garantiert
if IS_MACOS:
return False
# Linux praktisch immer case-sensitiv
return True
def path_suffix(path: _PathLike) -> str:
"""Gibt die Dateiendung eines Pfades zurück (inklusive Punkt)."""
try:
return Path(path).suffix
except Exception:
return ""

View File

@@ -0,0 +1,556 @@
"""
sn_basis/functions/qgiscore_wrapper.py zentrale QGIS-Core-Abstraktion
"""
from typing import Type, Any, Optional
from sn_basis.functions.qt_wrapper import (
QUrl,
QEventLoop,
QNetworkRequest,
)
# ---------------------------------------------------------
# QGIS-Symbole (werden dynamisch gesetzt)
# ---------------------------------------------------------
QgsProject: Type[Any]
QgsVectorLayer: Type[Any]
QgsRasterLayer: Type[Any]
QgsNetworkAccessManager: Type[Any]
Qgis: Type[Any]
QgsMapLayerProxyModel: Type[Any]
QgsVectorFileWriter: Type[Any] # neu: Schreib-API
QgsFeature: Type[Any]
QgsField: Type[Any]
QgsGeometry: Type[Any]
QgsFeatureRequest: Type[Any]
QgsCoordinateTransform: Type[Any]
QgsCoordinateReferenceSystem: Type[Any]
QgsPrintLayout: Type[Any]
QgsLayoutItemMap: Type[Any]
QgsLayoutItemLabel: Type[Any]
QgsLayoutPoint: Type[Any]
QgsLayoutSize: Type[Any]
QgsUnitTypes: Type[Any]
QgsLayoutItem: Type[Any]
QGIS_AVAILABLE = False
# ---------------------------------------------------------
# Versuch: QGIS-Core importieren
# ---------------------------------------------------------
try:
from qgis.core import (
QgsProject as _QgsProject,
QgsVectorLayer as _QgsVectorLayer,
QgsRasterLayer as _QgsRasterLayer,
QgsNetworkAccessManager as _QgsNetworkAccessManager,
Qgis as _Qgis,
QgsMapLayerProxyModel as _QgsMaplLayerProxyModel,
QgsVectorFileWriter as _QgsVectorFileWriter,
QgsFeature as _QgsFeature,
QgsField as _QgsField,
QgsGeometry as _QgsGeometry,
QgsFeatureRequest as _QgsFeatureRequest,
QgsCoordinateTransform as _QgsCoordinateTransform,
QgsCoordinateReferenceSystem as _QgsCoordinateReferenceSystem,
QgsPrintLayout as _QgsPrintLayout,
QgsLayoutItemMap as _QgsLayoutItemMap,
QgsLayoutItemLabel as _QgsLayoutItemLabel,
QgsLayoutPoint as _QgsLayoutPoint,
QgsLayoutSize as _QgsLayoutSize,
QgsUnitTypes as _QgsUnitTypes,
QgsLayoutItem as _QgsLayoutItem,
)
QgsProject = _QgsProject
QgsVectorLayer = _QgsVectorLayer
QgsRasterLayer = _QgsRasterLayer
QgsNetworkAccessManager = _QgsNetworkAccessManager
Qgis = _Qgis
QgsMapLayerProxyModel = _QgsMaplLayerProxyModel
QgsVectorFileWriter = _QgsVectorFileWriter
QgsFeature = _QgsFeature
QgsField = _QgsField
QgsGeometry = _QgsGeometry
QgsFeatureRequest = _QgsFeatureRequest
QgsCoordinateTransform = _QgsCoordinateTransform
QgsCoordinateReferenceSystem = _QgsCoordinateReferenceSystem
QgsPrintLayout = _QgsPrintLayout
QgsLayoutItemMap = _QgsLayoutItemMap
QgsLayoutItemLabel = _QgsLayoutItemLabel
QgsLayoutPoint = _QgsLayoutPoint
QgsLayoutSize = _QgsLayoutSize
QgsUnitTypes = _QgsUnitTypes
QgsLayoutItem = _QgsLayoutItem
QGIS_AVAILABLE = True
# ---------------------------------------------------------
# Mock-Modus
# ---------------------------------------------------------
except Exception:
QGIS_AVAILABLE = False
class _MockLayoutManager:
def layoutByName(self, name: str):
return None
def addLayout(self, layout: Any) -> bool:
return True
class _MockQgsProject:
def __init__(self):
self._variables = {}
self._layout_manager = _MockLayoutManager()
@staticmethod
def instance() -> "_MockQgsProject":
return _MockQgsProject()
def read(self) -> bool:
return True
def layoutManager(self):
return self._layout_manager
QgsProject = _MockQgsProject
class _MockQgsVectorLayer:
def __init__(self, *args, **kwargs):
self._valid = True
def isValid(self) -> bool:
return self._valid
def loadNamedStyle(self, path: str):
return True, ""
def triggerRepaint(self) -> None:
pass
def dataProvider(self):
return None
QgsVectorLayer = _MockQgsVectorLayer
class _MockQgsNetworkAccessManager:
@staticmethod
def instance():
return _MockQgsNetworkAccessManager()
def head(self, request: Any):
return None
class _MockQgsRasterLayer:
"""
Minimaler Mock für QgsRasterLayer, ausreichend für Tests und
um im Datenabruf ein Raster-Layer-Objekt im pruef_ergebnis kontext mitzugeben.
"""
def __init__(self, source: str, name: str = "Raster", provider: str = "wms"):
self.source = source
self._name = name
self.provider = provider
self._valid = True
def isValid(self) -> bool:
return self._valid
def name(self) -> str:
return self._name
def dataProvider(self):
return None
QgsRasterLayer = _MockQgsRasterLayer
class _MockQgsPrintLayout:
def __init__(self, project: Any):
self.project = project
self._name = ""
self._page = _MockQgsLayoutPage()
def initializeDefaults(self) -> None:
pass
def setName(self, name: str) -> None:
self._name = name
def pageCollection(self):
return self
def page(self, index: int):
return self._page
def addLayoutItem(self, item: Any) -> None:
pass
class _MockQgsLayoutPage:
def setPageSize(self, size: Any) -> None:
self.size = size
class _MockQgsLayoutItem:
class ReferencePoint:
LowerLeft = 0
class _MockQgsLayoutItemMap:
def __init__(self, layout: Any):
self.layout = layout
def setId(self, item_id: str) -> None:
pass
def setExtent(self, extent: Any) -> None:
pass
def setScale(self, scale: float) -> None:
pass
def attemptMove(self, point: Any) -> None:
pass
def attemptResize(self, size: Any) -> None:
pass
def setFollowVisibilityPreset(self, active: bool) -> None:
pass
def setFollowVisibilityPresetName(self, name: str) -> None:
pass
class _MockQgsLayoutItemLabel:
ModeHtml = 1
def __init__(self, layout: Any):
self.layout = layout
def setId(self, item_id: str) -> None:
pass
def setText(self, text: str) -> None:
pass
def setMode(self, mode: Any) -> None:
pass
def setFont(self, font: Any) -> None:
pass
def setReferencePoint(self, point: Any) -> None:
pass
def attemptMove(self, point: Any) -> None:
pass
def attemptResize(self, size: Any) -> None:
pass
class _MockQgsLayoutPoint:
def __init__(self, x: float, y: float, unit: Any):
self.x = x
self.y = y
self.unit = unit
class _MockQgsLayoutSize:
def __init__(self, width: float, height: float, unit: Any):
self.width = width
self.height = height
self.unit = unit
class _MockQgsUnitTypes:
LayoutMillimeters = 0
QgsPrintLayout = _MockQgsPrintLayout
QgsLayoutItemMap = _MockQgsLayoutItemMap
QgsLayoutItemLabel = _MockQgsLayoutItemLabel
QgsLayoutPoint = _MockQgsLayoutPoint
QgsLayoutSize = _MockQgsLayoutSize
QgsUnitTypes = _MockQgsUnitTypes
QgsLayoutItem = _MockQgsLayoutItem
class _MockQgsFeatureRequest:
def __init__(self):
self._filter_rect = None
def setFilterRect(self, rect):
self._filter_rect = rect
return self
QgsFeatureRequest = _MockQgsFeatureRequest
class _MockQgsCoordinateTransform:
def __init__(self, *args, **kwargs):
pass
def transformBoundingBox(self, rect):
return rect
class _MockQgsCoordinateReferenceSystem:
def __init__(self, *args, **kwargs):
pass
QgsCoordinateTransform = _MockQgsCoordinateTransform
QgsCoordinateReferenceSystem = _MockQgsCoordinateReferenceSystem
QgsNetworkAccessManager = _MockQgsNetworkAccessManager
class _MockQgis:
class MessageLevel:
Success = 0
Info = 1
Warning = 2
Critical = 3
Qgis = _MockQgis
class _MockQgsMapLayerProxyModel:
# Layer-Typen (entsprechen QGIS-Konstanten)
NoLayer = 0
VectorLayer = 1
RasterLayer = 2
PluginLayer = 3
MeshLayer = 4
VectorTileLayer = 5
PointCloudLayer = 6
def __init__(self, *args, **kwargs):
pass
QgsMapLayerProxyModel = _MockQgsMapLayerProxyModel
# ---------------------------------------------------------
# Mock für QgsVectorFileWriter
# ---------------------------------------------------------
class _MockSaveVectorOptions:
"""
Minimaler Ersatz für QgsVectorFileWriter.SaveVectorOptions.
Felder werden als einfache Attribute bereitgestellt.
"""
def __init__(self):
self.driverName: str = "GPKG"
self.layerName: Optional[str] = None
self.fileEncoding: str = "UTF-8"
# Action-Konstanten werden symbolisch verwendet
self.actionOnExistingFile: Optional[int] = None
class _MockQgsVectorFileWriter:
"""
Minimaler Mock für QgsVectorFileWriter mit der benötigten API:
- SaveVectorOptions (als Klasse)
- writeAsVectorFormatV3(layer, path, transformContext, options) -> error_code
- NoError (Konstante)
- CreateOrOverwriteFile / CreateOrOverwriteLayer (Konstanten)
"""
# Fehlerkonstanten (0 = NoError)
NoError = 0
# Action-Konstanten (Werte nur symbolisch)
CreateOrOverwriteFile = 1
CreateOrOverwriteLayer = 2
# SaveVectorOptions-Klasse
SaveVectorOptions = _MockSaveVectorOptions
@staticmethod
def writeAsVectorFormatV3(layer: Any, path: str, transform_context: Any, options: Any) -> int:
"""
Mock-Schreibfunktion.
Verhalten im Mock:
- Wenn 'layer' None oder options.layerName fehlt, geben wir NoError zurück,
aber schreiben nichts (Tests erwarten nur Rückgabecode).
- Diese Implementierung versucht nicht, echte Dateien zu schreiben.
- Rückgabewert: 0 (NoError) bei Erfolg, sonst eine positive Fehlernummer.
"""
try:
# Sehr einfache Validierung: wenn path leer -> Fehler
if not path:
return 999
# Simuliere Erfolg
return _MockQgsVectorFileWriter.NoError
except Exception:
return 999 # generischer Fehlercode
QgsVectorFileWriter = _MockQgsVectorFileWriter
# ---------------------------------------------------------
# Netzwerk
# ---------------------------------------------------------
class NetworkReply:
"""
Minimaler Wrapper für Netzwerkantworten.
"""
def __init__(self, error: int):
self.error = error
def network_head(url: str) -> NetworkReply | None:
"""
Führt einen HTTP-HEAD-Request aus.
Rückgabe:
- NetworkReply(error=0) → erreichbar
- NetworkReply(error!=0) → nicht erreichbar
- None → Netzwerk nicht verfügbar / Fehler beim Request
"""
if not QGIS_AVAILABLE:
return None
if QUrl is None or QNetworkRequest is None:
return None
try:
manager = QgsNetworkAccessManager.instance()
request = QNetworkRequest(QUrl(url))
reply = manager.head(request)
# synchron warten (kurz)
if QEventLoop is not None:
loop = QEventLoop()
reply.finished.connect(loop.quit)
loop.exec()
return NetworkReply(error=reply.error())
except Exception:
return None
# ---------------------------------------------------------
# Layer-Geometrie / Extent
# ---------------------------------------------------------
def get_layer_extent(layer: Any) -> Any:
"""
Gibt die Ausdehnung (Extent) eines Layers zurück.
Diese Funktion kapselt den Zugriff auf ``layer.extent()`` und dient als
zentrale Abstraktion für alle Stellen, die die Bounding Box eines Layers
benötigen (z.B. für räumliche Filter im Datenabruf).
Verhalten
---------
- Wenn QGIS verfügbar ist und der Layer eine ``extent()``-Methode besitzt,
wird deren Rückgabewert zurückgegeben.
- Wenn QGIS nicht verfügbar ist oder der Layer keine ``extent()``-Methode
hat, wird ``None`` zurückgegeben.
"""
if not QGIS_AVAILABLE or layer is None:
return None
extent_func = getattr(layer, "extent", None)
if callable(extent_func):
try:
return extent_func()
except Exception:
return None
return None
# ---------------------------------------------------------
# Buffer-Layer erzeugen
# ---------------------------------------------------------
def create_buffer_layer(
source_layer: Any,
distance_m: float,
layer_name: str = "BufferLayer"
) -> Optional[Any]:
"""
Erzeugt einen Pufferlayer um alle Features eines Quelllayers.
Diese Funktion dient als zentrale Abstraktion für die Erzeugung eines
Pufferlayers in QGIS. Sie wird z.B. im Datenabruf verwendet, wenn der
Raumfilter ``"Pufferlayer"`` aktiv ist.
Verhalten
---------
- Wenn QGIS verfügbar ist und der ``source_layer`` gültig ist, wird ein
temporärer Vektorlayer erzeugt, der die gepufferten Geometrien enthält.
- Der Puffer wird in Metern angegeben.
- Der zurückgegebene Layer ist **nicht gespeichert**, sondern ein
temporärer Speicherlayer, der anschließend über den UIWrapper ins
Projekt geladen werden kann.
- Wenn QGIS nicht verfügbar ist oder ein Fehler auftritt, wird ``None``
zurückgegeben.
"""
if not QGIS_AVAILABLE:
return None
if source_layer is None or not hasattr(source_layer, "getFeatures"):
return None
try:
# Geometrien puffern
buffered_geoms = []
for feat in source_layer.getFeatures():
geom = feat.geometry()
if geom is None:
continue
buf = geom.buffer(distance_m, 8)
if buf is not None:
buffered_geoms.append(buf)
if not buffered_geoms:
return None
# Neuen Memory-Layer erzeugen
crs = source_layer.crs().authid() if hasattr(source_layer, "crs") else "EPSG:4326"
mem_layer = QgsVectorLayer(f"Polygon?crs={crs}", layer_name, "memory")
prov = mem_layer.dataProvider()
prov.addAttributes([])
mem_layer.updateFields()
# Features hinzufügen
from qgis.core import QgsFeature
for geom in buffered_geoms:
f = QgsFeature()
f.setGeometry(geom)
prov.addFeature(f)
mem_layer.updateExtents()
return mem_layer
except Exception:
return None
#Hilfsfunktion, keine qgiscore-Entsprechung
def layer_exists_in_gpkg(gpkg_path: str, layer_name: str) -> bool:
"""
Prüft, ob ein Layer mit dem Namen `layer_name` in `gpkg_path` existiert.
- bevorzugt: SQLite-Abfrage auf gpkg_contents
- fallback: kurzer Versuch, mit QgsVectorLayer zu laden (wenn QGIS verfügbar)
"""
import os, sqlite3
if not gpkg_path or not layer_name or not os.path.exists(gpkg_path):
return False
# 1) SQLite-Check (schnell)
try:
conn = sqlite3.connect(gpkg_path)
cur = conn.cursor()
cur.execute("SELECT COUNT(1) FROM gpkg_contents WHERE table_name = ?", (layer_name,))
row = cur.fetchone()
conn.close()
if row and row[0] > 0:
return True
except Exception:
# falls sqlite fehlschlägt, weiter zum QGIS-Fallback
pass
# 2) QGIS-Fallback: versuche kurz, den Layer zu laden
try:
if getattr(QgsVectorLayer, "__call__", None) and QGIS_AVAILABLE:
uri = f"{gpkg_path}|layername={layer_name}"
layer = QgsVectorLayer(uri, layer_name, "ogr")
return bool(layer and getattr(layer, "isValid", lambda: False)())
except Exception:
pass
return False

257
functions/qgisui_wrapper.py Normal file
View File

@@ -0,0 +1,257 @@
"""
sn_basis/functions/qgisui_wrapper.py zentrale QGIS-UI-Abstraktion
"""
from __future__ import annotations
from typing import Any, List, Type
from sn_basis.functions.qt_wrapper import QDockWidget
from sn_basis.functions.qgiscore_wrapper import QgsProject, QGIS_AVAILABLE
iface: Any
QGIS_UI_AVAILABLE = False
QgsFileWidget: Type[Any]
QgsMapLayerComboBox: Type[Any]
# ---------------------------------------------------------
# iface + QGIS-Widgets initialisieren (QGIS oder Mock)
# ---------------------------------------------------------
try:
from qgis.utils import iface as _iface
from qgis.gui import (
QgsFileWidget as _QgsFileWidget,
QgsMapLayerComboBox as _QgsMapLayerComboBox,
)
iface = _iface
QgsFileWidget = _QgsFileWidget
QgsMapLayerComboBox = _QgsMapLayerComboBox
QGIS_UI_AVAILABLE = True
except Exception:
QGIS_UI_AVAILABLE = False
class _MockSignal:
def __init__(self):
self._callbacks: list[Any] = []
def connect(self, callback):
self._callbacks.append(callback)
def emit(self, *args, **kwargs):
for cb in list(self._callbacks):
cb(*args, **kwargs)
class _MockMessageBar:
def pushMessage(self, title, text, level=0, duration=5):
return {
"title": title,
"text": text,
"level": level,
"duration": duration,
}
class _MockIface:
def messageBar(self):
return _MockMessageBar()
def mainWindow(self):
return None
def addDockWidget(self, *args, **kwargs):
pass
def removeDockWidget(self, *args, **kwargs):
pass
def addToolBar(self, *args, **kwargs):
pass
def removeToolBar(self, *args, **kwargs):
pass
def openLayoutDesigner(self, layout):
return layout
iface = _MockIface()
class _MockQgsFileWidget:
GetFile = 0
def __init__(self, *args, **kwargs):
self._path = ""
self.fileChanged = _MockSignal()
def setStorageMode(self, *args, **kwargs):
pass
def setFilter(self, *args, **kwargs):
pass
def setFilePath(self, path: str):
self._path = path
self.fileChanged.emit(path)
def filePath(self) -> str:
return self._path
class _MockQgsMapLayerComboBox:
def __init__(self, *args, **kwargs):
self.layerChanged = _MockSignal()
self._layer = None
self._count = 0
def setFilters(self, *args, **kwargs):
pass
def setLayer(self, layer):
self._layer = layer
self.layerChanged.emit(layer)
def count(self) -> int:
return self._count
def setCurrentIndex(self, idx: int):
pass
QgsFileWidget = _MockQgsFileWidget
QgsMapLayerComboBox = _MockQgsMapLayerComboBox
# ---------------------------------------------------------
# Main Window
# ---------------------------------------------------------
def get_main_window():
try:
return iface.mainWindow()
except Exception:
return None
def open_layout_designer(layout: Any) -> Any:
try:
return iface.openLayoutDesigner(layout)
except Exception:
return None
# ---------------------------------------------------------
# Dock-Handling
# ---------------------------------------------------------
def add_dock_widget(area, dock: Any) -> None:
try:
iface.addDockWidget(area, dock)
except Exception:
pass
def remove_dock_widget(dock: Any) -> None:
try:
iface.removeDockWidget(dock)
except Exception:
pass
def find_dock_widgets() -> List[Any]:
main_window = get_main_window()
if not main_window:
return []
try:
return main_window.findChildren(QDockWidget)
except Exception:
return []
# ---------------------------------------------------------
# Menü-Handling
# ---------------------------------------------------------
def add_menu(menu):
main_window = iface.mainWindow()
if not main_window:
return
# Nur echte Qt-Menüs an Qt übergeben
if hasattr(menu, "menuAction"):
main_window.menuBar().addMenu(menu)
def remove_menu(menu):
main_window = iface.mainWindow()
if not main_window:
return
if hasattr(menu, "menuAction"):
main_window.menuBar().removeAction(menu.menuAction())
# ---------------------------------------------------------
# Toolbar-Handling
# ---------------------------------------------------------
def add_toolbar(toolbar: Any) -> None:
try:
iface.addToolBar(toolbar)
except Exception:
pass
def remove_toolbar(toolbar: Any) -> None:
try:
iface.removeToolBar(toolbar)
except Exception:
pass
# ---------------------------------------------------------
# Layer zum Projekt hinzufügen
# ---------------------------------------------------------
def add_layer_to_project(layer: Any) -> bool:
"""
Fügt einen Layer dem aktuellen QGIS-Projekt hinzu.
Diese Funktion kapselt den Zugriff auf ``QgsProject.instance().addMapLayer``
und dient als zentrale Abstraktion für alle Stellen, die Layer dynamisch
ins Projekt einfügen möchten (z.B. Pufferlayer im Datenabruf).
Verhalten
---------
- Wenn QGIS verfügbar ist und der Layer gültig ist, wird er dem Projekt
hinzugefügt und ``True`` zurückgegeben.
- Wenn QGIS nicht verfügbar ist oder der Layer ungültig ist, wird
``False`` zurückgegeben.
- Im Mock-Modus wird kein Layer hinzugefügt, aber ``True`` zurückgegeben,
damit Tests ohne QGIS nicht fehlschlagen.
Parameters
----------
layer:
Ein QGIS-Layer (typischerweise ``QgsVectorLayer``), der dem Projekt
hinzugefügt werden soll.
Returns
-------
bool
``True`` bei Erfolg oder im Mock-Modus, sonst ``False``.
"""
if layer is None:
return False
# Mock-Modus: Erfolg simulieren
if not QGIS_AVAILABLE:
return True
try:
project = QgsProject.instance()
project.addMapLayer(layer)
return True
except Exception:
return False

582
functions/qt_wrapper.py Normal file
View File

@@ -0,0 +1,582 @@
"""
sn_basis/functions/qt_wrapper.py zentrale Qt-Abstraktion (PyQt6 primär / PyQt5 Fallback / Mock)
"""
from typing import Optional, Type, Any, Callable
# Globale Qt-Symbole (werden dynamisch gesetzt)
QT_VERSION = 0 # 0 = Mock, 5 = PyQt5, 6 = PyQt6
YES: Optional[Any] = None
NO: Optional[Any] = None
CANCEL: Optional[Any] = None
ICON_QUESTION: Optional[Any] = None
QVariant: Type[Any] = object
# Qt-Klassen (werden dynamisch gesetzt)
QDockWidget: Type[Any] = object
QMessageBox: Type[Any] = object
QFileDialog: Type[Any] = object
QProgressDialog: Type[Any] = object
QEventLoop: Type[Any] = object
QTimer: Type[Any] = object
QUrl: Type[Any] = object
QNetworkRequest: Type[Any] = object
QNetworkReply: Type[Any] = object
QCoreApplication: Type[Any] = object
QWidget: Type[Any] = object
QGridLayout: Type[Any] = object
QLabel: Type[Any] = object
QLineEdit: Type[Any] = object
QInputDialog: Type[Any] = object
QGroupBox: Type[Any] = object
QVBoxLayout: Type[Any] = object
QPushButton: Type[Any] = object
QAction: Type[Any] = object
QMenu: Type[Any] = object
QToolBar: Type[Any] = object
QActionGroup: Type[Any] = object
QTabWidget: Type[Any] = object
QToolButton: Type[Any] = object
QSizePolicy: Type[Any] = object
Qt: Type[Any] = object
QComboBox: Type[Any] = object
QCheckBox: Type[Any] = object
QHBoxLayout: Type[Any] = object
QFont: Type[Any] = object
def exec_dialog(dialog: Any) -> Any:
"""Führt Dialog modal aus (Qt6: exec(), Qt5: exec_(), Mock: YES)"""
raise NotImplementedError("Qt nicht initialisiert")
def debug_qt_status() -> None:
"""Debug: Zeigt Qt-Status für Troubleshooting."""
print(f"🔍 QT_VERSION: {QT_VERSION}")
print(f"🔍 QMessageBox Typ: {getattr(QMessageBox, '__name__', type(QMessageBox).__name__)}")
print(f"🔍 YES Wert: {YES} (Typ: {type(YES) if YES is not None else 'None'})")
if QT_VERSION == 0:
print("❌ MOCK-MODUS AKTIV! Keine Dialoge möglich!")
elif QT_VERSION == 5:
print("✅ PyQt5 geladen (Fallback) Dialoge sollten funktionieren!")
elif QT_VERSION == 6:
print("✅ PyQt6 geladen (primär) Dialoge sollten funktionieren!")
else:
print("❓ Unbekannte Qt-Version!")
# --------------------------- PYQT6 PRIMÄR ---------------------------
try:
from qgis.PyQt.QtWidgets import (
QMessageBox as _QMessageBox,
QFileDialog as _QFileDialog,
QProgressDialog as _QProgressDialog,
QWidget as _QWidget,
QGridLayout as _QGridLayout,
QLabel as _QLabel,
QLineEdit as _QLineEdit,
QInputDialog as _QInputDialog,
QGroupBox as _QGroupBox,
QVBoxLayout as _QVBoxLayout,
QPushButton as _QPushButton,
QAction as _QAction,
QMenu as _QMenu,
QToolBar as _QToolBar,
QActionGroup as _QActionGroup,
QDockWidget as _QDockWidget,
QTabWidget as _QTabWidget,
QToolButton as _QToolButton,
QSizePolicy as _QSizePolicy,
QComboBox as _QComboBox,
QCheckBox as _QCheckBox,
QHBoxLayout as _QHBoxLayout,
)
from qgis.PyQt.QtGui import QFont as _QFont
from qgis.PyQt.QtCore import (
QEventLoop as _QEventLoop,
QTimer as _QTimer,
QUrl as _QUrl,
QCoreApplication as _QCoreApplication,
Qt as _Qt,
QVariant as _QVariant
)
from qgis.PyQt.QtNetwork import (
QNetworkRequest as _QNetworkRequest,
QNetworkReply as _QNetworkReply,
)
# ✅ ALLE GLOBALS ZUWEISEN
QT_VERSION = 6
QMessageBox = _QMessageBox
QFileDialog = _QFileDialog
QProgressDialog = _QProgressDialog
QProgressDialog = _QProgressDialog
QEventLoop = _QEventLoop
QTimer = _QTimer
QUrl = _QUrl
QNetworkRequest = _QNetworkRequest
QNetworkReply = _QNetworkReply
QCoreApplication = _QCoreApplication
Qt = _Qt
QDockWidget = _QDockWidget
QWidget = _QWidget
QGridLayout = _QGridLayout
QLabel = _QLabel
QLineEdit = _QLineEdit
QInputDialog = _QInputDialog
QGroupBox = _QGroupBox
QVBoxLayout = _QVBoxLayout
QPushButton = _QPushButton
QAction = _QAction
QMenu = _QMenu
QToolBar = _QToolBar
QActionGroup = _QActionGroup
QTabWidget = _QTabWidget
QToolButton = _QToolButton
QSizePolicy = _QSizePolicy
QComboBox = _QComboBox
QCheckBox = _QCheckBox
QVariant = _QVariant
QHBoxLayout = _QHBoxLayout
QFont = _QFont
# ✅ QT6 ENUMS
YES = QMessageBox.StandardButton.Yes
NO = QMessageBox.StandardButton.No
CANCEL = QMessageBox.StandardButton.Cancel
ICON_QUESTION = QMessageBox.Icon.Question
AcceptRole = QMessageBox.ButtonRole.AcceptRole
ActionRole = QMessageBox.ButtonRole.ActionRole
RejectRole = QMessageBox.ButtonRole.RejectRole
# Qt6 Enum-Aliase
ToolButtonTextBesideIcon = Qt.ToolButtonStyle.ToolButtonTextBesideIcon
ArrowDown = Qt.ArrowType.DownArrow
ArrowRight = Qt.ArrowType.RightArrow
SizePolicyPreferred = QSizePolicy.Policy.Preferred
SizePolicyMaximum = QSizePolicy.Policy.Maximum
DockWidgetMovable = QDockWidget.DockWidgetFeature.DockWidgetMovable
DockWidgetFloatable = QDockWidget.DockWidgetFeature.DockWidgetFloatable
DockWidgetClosable = QDockWidget.DockWidgetFeature.DockWidgetClosable
DockAreaLeft = Qt.DockWidgetArea.LeftDockWidgetArea
DockAreaRight = Qt.DockWidgetArea.RightDockWidgetArea
def exec_dialog(dialog: Any) -> Any:
return dialog.exec()
print(f"✅ qt_wrapper: PyQt6 geladen (QT_VERSION={QT_VERSION})")
# --------------------------- PYQT5 FALLBACK ---------------------------
except (ImportError, AttributeError):
try:
from PyQt5.QtWidgets import (
QMessageBox as _QMessageBox,
QFileDialog as _QFileDialog,
QWidget as _QWidget,
QGridLayout as _QGridLayout,
QLabel as _QLabel,
QLineEdit as _QLineEdit,
QInputDialog as _QInputDialog,
QGroupBox as _QGroupBox,
QVBoxLayout as _QVBoxLayout,
QPushButton as _QPushButton,
QAction as _QAction,
QMenu as _QMenu,
QToolBar as _QToolBar,
QActionGroup as _QActionGroup,
QDockWidget as _QDockWidget,
QTabWidget as _QTabWidget,
QToolButton as _QToolButton,
QSizePolicy as _QSizePolicy,
QComboBox as _QComboBox,
QCheckBox as _QCheckBox,
QHBoxLayout as _QHBoxLayout,
)
from PyQt5.QtGui import QFont as _QFont
from PyQt5.QtCore import (
QEventLoop as _QEventLoop,
QTimer as _QTimer,
QUrl as _QUrl,
QCoreApplication as _QCoreApplication,
Qt as _Qt,
QVariant as _QVariant
)
from PyQt5.QtNetwork import (
QNetworkRequest as _QNetworkRequest,
QNetworkReply as _QNetworkReply,
)
# ✅ ALLE GLOBALS ZUWEISEN
QT_VERSION = 5
QMessageBox = _QMessageBox
QFileDialog = _QFileDialog
QEventLoop = _QEventLoop
QTimer = _QTimer
QUrl = _QUrl
QNetworkRequest = _QNetworkRequest
QNetworkReply = _QNetworkReply
QCoreApplication = _QCoreApplication
Qt = _Qt
QDockWidget = _QDockWidget
QWidget = _QWidget
QGridLayout = _QGridLayout
QLabel = _QLabel
QLineEdit = _QLineEdit
QInputDialog = _QInputDialog
QGroupBox = _QGroupBox
QVBoxLayout = _QVBoxLayout
QPushButton = _QPushButton
QAction = _QAction
QMenu = _QMenu
QToolBar = _QToolBar
QActionGroup = _QActionGroup
QTabWidget = _QTabWidget
QToolButton = _QToolButton
QSizePolicy = _QSizePolicy
QComboBox = _QComboBox
QCheckBox = _QCheckBox
QVariant = _QVariant
QHBoxLayout= _QHBoxLayout
QFont = _QFont
# ✅ PYQT5 ENUMS
YES = QMessageBox.Yes
NO = QMessageBox.No
CANCEL = QMessageBox.Cancel
ICON_QUESTION = QMessageBox.Question
AcceptRole = QMessageBox.AcceptRole
ActionRole = QMessageBox.ActionRole
RejectRole = QMessageBox.RejectRole
# PyQt5 Enum-Aliase
ToolButtonTextBesideIcon = Qt.ToolButtonTextBesideIcon
ArrowDown = Qt.DownArrow
ArrowRight = Qt.RightArrow
SizePolicyPreferred = QSizePolicy.Preferred
SizePolicyMaximum = QSizePolicy.Maximum
DockWidgetMovable = QDockWidget.DockWidgetMovable
DockWidgetFloatable = QDockWidget.DockWidgetFloatable
DockWidgetClosable = QDockWidget.DockWidgetClosable
DockAreaLeft = Qt.LeftDockWidgetArea
DockAreaRight = Qt.RightDockWidgetArea
def exec_dialog(dialog: Any) -> Any:
return dialog.exec_()
print(f"✅ qt_wrapper: PyQt5 Fallback geladen (QT_VERSION={QT_VERSION})")
# --------------------------- MOCK-MODUS ---------------------------
except Exception:
QT_VERSION = 0
print("⚠️ qt_wrapper: Mock-Modus aktiviert (QT_VERSION=0)")
# Fake Enum für Bit-Operationen
class FakeEnum(int):
def __or__(self, other: Any) -> "FakeEnum":
return FakeEnum(int(self) | int(other))
YES = FakeEnum(1)
NO = FakeEnum(2)
CANCEL = FakeEnum(4)
ICON_QUESTION = FakeEnum(8)
# Im Mock-Block von qt_wrapper.py:
class _MockQMessageBox:
Yes = YES
No = NO
Cancel = CANCEL
Question = ICON_QUESTION
AcceptRole = 0
ActionRole = 3
RejectRole = 1
@classmethod
def question(cls, parent, title, message, buttons, default_button):
"""Mock: Gibt immer default_button zurück"""
print(f"🔍 Mock QMessageBox.question: '{title}'{default_button}")
return default_button
QMessageBox = _MockQMessageBox
class _MockQFileDialog:
@staticmethod
def getOpenFileName(*args, **kwargs): return ("", "")
@staticmethod
def getSaveFileName(*args, **kwargs): return ("", "")
QFileDialog = _MockQFileDialog
class _MockQInputDialog:
@staticmethod
def getText(parent, title, label, mode=None, text=""):
return text, True
QInputDialog = _MockQInputDialog
class _MockQEventLoop:
def exec(self) -> int: return 0
def quit(self) -> None: pass
QEventLoop = _MockQEventLoop
class _MockQTimer:
def __init__(self, *args, **kwargs):
self.timeout = type('Signal', (), {
'connect': lambda s, cb: None,
})()
def setSingleShot(self, v: bool) -> None: pass
def start(self, ms: int) -> None: pass
def stop(self) -> None: pass
QTimer = _MockQTimer
class _MockQUrl(str):
def isValid(self) -> bool: return True
QUrl = _MockQUrl
class _MockQNetworkRequest:
def __init__(self, url: Any): self.url = url
QNetworkRequest = _MockQNetworkRequest
class _MockQNetworkReply:
def error(self) -> int: return 0
def errorString(self) -> str: return ""
def readAll(self) -> bytes: return b""
def deleteLater(self) -> None: pass
QNetworkReply = _MockQNetworkReply
class _MockWidget: pass
class _MockLayout:
def __init__(self, *args, **kwargs): self._widgets = []
def addWidget(self, widget): self._widgets.append(widget)
def addLayout(self, layout): pass
def addStretch(self, *args, **kwargs): pass
def setSpacing(self, *args, **kwargs): pass
def setContentsMargins(self, *args, **kwargs): pass
class _MockLabel:
def __init__(self, text: str = ""): self._text = text
class _MockLineEdit:
Normal = 0
def __init__(self, *args, **kwargs): self._text = ""
def text(self) -> str: return self._text
def setText(self, value: str) -> None: self._text = value
class _MockFont:
def __init__(self, family: str = "", pointSize: int = 10):
self.family = family
self.pointSize = pointSize
class _MockButton:
def __init__(self, *args, **kwargs): self.clicked = lambda *a, **k: None
QWidget = _MockWidget
QGridLayout = _MockLayout
QLabel = _MockLabel
QLineEdit = _MockLineEdit
QGroupBox = _MockWidget
QVBoxLayout = _MockLayout
QPushButton = _MockButton
QFont = _MockFont
QCoreApplication = object()
class _MockQt:
ToolButtonTextBesideIcon = 0
ArrowDown = 1
ArrowRight = 2
LeftDockWidgetArea = 1
RightDockWidgetArea = 2
Qt = _MockQt()
ToolButtonTextBesideIcon = Qt.ToolButtonTextBesideIcon
ArrowDown = Qt.ArrowDown
ArrowRight = Qt.ArrowRight
DockAreaLeft = Qt.LeftDockWidgetArea
DockAreaRight = Qt.RightDockWidgetArea
class _MockQDockWidget(_MockWidget):
def __init__(self, *args, **kwargs):
self._object_name = ""
def setObjectName(self, name: str) -> None: self._object_name = name
def objectName(self) -> str: return self._object_name
def show(self) -> None: pass
def deleteLater(self) -> None: pass
QDockWidget = _MockQDockWidget
class _MockAction:
def __init__(self, *args, **kwargs):
self._checked = False
self.triggered = lambda *a, **k: None
def setToolTip(self, text: str) -> None: pass
def setCheckable(self, value: bool) -> None: pass
def setChecked(self, value: bool) -> None: self._checked = value
class _MockMenu:
def __init__(self, *args, **kwargs): self._actions = []
def addAction(self, action): self._actions.append(action)
def removeAction(self, action):
if action in self._actions: self._actions.remove(action)
def clear(self): self._actions.clear()
def menuAction(self): return self
class _MockToolBar:
def __init__(self, *args, **kwargs): self._actions = []
def setObjectName(self, name: str) -> None: pass
def addAction(self, action): self._actions.append(action)
def removeAction(self, action):
if action in self._actions: self._actions.remove(action)
def clear(self): self._actions.clear()
class _MockActionGroup:
def __init__(self, *args, **kwargs): self._actions = []
def setExclusive(self, value: bool) -> None: pass
def addAction(self, action): self._actions.append(action)
QAction = _MockAction
QMenu = _MockMenu
QToolBar = _MockToolBar
QActionGroup = _MockActionGroup
class _MockToolButton(_MockWidget):
def __init__(self, *args, **kwargs):
self._checked = False
self.toggled = lambda *a, **k: None
def setText(self, text: str) -> None: pass
def setCheckable(self, value: bool) -> None: pass
def setChecked(self, value: bool) -> None: self._checked = value
def setToolButtonStyle(self, *args, **kwargs): pass
def setArrowType(self, *args, **kwargs): pass
def setStyleSheet(self, *args, **kwargs): pass
QToolButton = _MockToolButton
class _MockQSizePolicy:
Preferred = 3
Maximum = 2
QSizePolicy = _MockQSizePolicy
SizePolicyPreferred = QSizePolicy.Preferred
SizePolicyMaximum = QSizePolicy.Maximum
DockWidgetMovable = 1
DockWidgetFloatable = 2
DockWidgetClosable = 4
class _MockTabWidget:
def __init__(self, *args, **kwargs): self._tabs = []
def addTab(self, widget, title: str): self._tabs.append((widget, title))
QTabWidget = _MockTabWidget
class _MockComboBox:
def __init__(self, parent=None):
self._items = []
self._index = -1
self.currentTextChanged = type('Signal', (), {'connect': lambda s, cb: None, 'emit': lambda s, v: None})()
def addItem(self, text: str) -> None: self._items.append(text)
def addItems(self, items): [self.addItem(it) for it in items]
def findText(self, text: str) -> int:
return self._items.index(text) if text in self._items else -1
def setCurrentIndex(self, idx: int) -> None:
if 0 <= idx < len(self._items):
self._index = idx
self.currentTextChanged.emit(self.currentText())
def setCurrentText(self, text: str) -> None:
idx = self.findText(text)
if idx >= 0: self.setCurrentIndex(idx)
def currentText(self) -> str:
return self._items[self._index] if 0 <= self._index < len(self._items) else ""
QComboBox = _MockComboBox
# ---------------------------
# Mock für QVariant
# ---------------------------
class _MockQVariant:
"""
Minimaler Ersatz für QtCore.QVariant.
Ziel:
- Werte transparent durchreichen
- Typ-Konstanten bereitstellen
- Keine Qt-Abhängigkeiten
"""
# Typ-Konstanten (symbolisch, Werte egal)
Invalid = 0
Int = 1
Double = 2
String = 3
Bool = 4
Date = 5
DateTime = 6
def __init__(self, value: Any = None):
self._value = value
def value(self) -> Any:
return self._value
def __repr__(self) -> str:
return f"QVariant({self._value!r})"
# Optional: automatische Entpackung
def __int__(self):
return int(self._value)
def __float__(self):
return float(self._value)
def __str__(self):
return str(self._value)
QVariant = _MockQVariant
class _MockQHBoxLayout:
def __init__(self, *args, **kwargs):
self._widgets = []
def addWidget(self, widget):
self._widgets.append(widget)
def addLayout(self, layout):
pass
def addStretch(self, *args, **kwargs):
pass
def setSpacing(self, *args, **kwargs):
pass
def setContentsMargins(self, *args, **kwargs):
pass
QHBoxLayout = _MockQHBoxLayout
class _MockQCheckBox:
def __init__(self, text: str = "", *args, **kwargs):
self._text = text
self._checked = False
def setText(self, text: str) -> None:
self._text = text
def isChecked(self) -> bool:
return self._checked
def setChecked(self, checked: bool) -> None:
self._checked = checked
QCheckBox = _MockQCheckBox
def exec_dialog(dialog: Any) -> Any:
return YES
# --------------------------- TEST ---------------------------
if __name__ == "__main__":
debug_qt_status()

View File

@@ -1,37 +1,47 @@
from qgis.core import QgsProject, QgsExpressionContextUtils """
sn_basis/functions/settings_logic.py Logik zum Lesen und Schreiben der Plugin-Einstellungen
über den zentralen variable_wrapper.
"""
from sn_basis.functions.variable_wrapper import (
get_variable,
set_variable,
)
class SettingsLogic: class SettingsLogic:
def __init__(self): """
self.project = QgsProject.instance() Verwaltet das Laden und Speichern der Plugin-Einstellungen.
Alle Variablen werden als sn_* Projektvariablen gespeichert.
"""
# Definition der Variablen-Namen # Alle Variablen, die gespeichert werden sollen
self.global_vars = ["amt", "behoerde", "landkreis_user", "sachgebiet"] VARIABLEN = [
self.project_vars = ["bezeichnung", "verfahrensnummer", "gemeinden", "landkreise_proj"] "amt",
"behoerde",
"landkreis_user",
"sachgebiet",
"bezeichnung",
"verfahrensnummer",
"gemeinden",
"landkreise_proj",
]
def save(self, fields: dict): def load(self) -> dict[str, str]:
"""Speichert Felder als globale und projektbezogene Ausdrucksvariablen.""" """
Lädt alle Variablen aus dem Projekt.
Rückgabe: dict mit allen Werten (leere Strings, wenn nicht gesetzt).
"""
daten: dict[str, str] = {}
for key in self.VARIABLEN:
daten[key] = get_variable(key, scope="project")
return daten
# Globale Variablen def save(self, daten: dict[str, str]) -> None:
for key in self.global_vars: """
QgsExpressionContextUtils.setGlobalVariable(f"sn_{key}", fields.get(key, "")) Speichert alle übergebenen Variablen im Projekt.
daten: dict mit key → value
# Projektvariablen """
for key in self.project_vars: for key, value in daten.items():
QgsExpressionContextUtils.setProjectVariable(self.project, f"sn_{key}", fields.get(key, "")) if key in self.VARIABLEN:
set_variable(key, value, scope="project")
print("✅ Ausdrucksvariablen gespeichert.")
def load(self) -> dict:
"""Lädt Werte ausschließlich aus Ausdrucksvariablen (global + projektbezogen)."""
data = {}
# Globale Variablen
for key in self.global_vars:
data[key] = QgsExpressionContextUtils.globalScope().variable(f"sn_{key}") or ""
# Projektvariablen
for key in self.project_vars:
data[key] = QgsExpressionContextUtils.projectScope(self.project).variable(f"sn_{key}") or ""
return data

View File

@@ -1,28 +0,0 @@
# sn_basis/functions/styles.py
import os
from qgis.core import QgsVectorLayer
def apply_style(layer: QgsVectorLayer, style_name: str) -> bool:
"""
Lädt einen QML-Style aus dem styles-Ordner des Plugins und wendet ihn auf den Layer an.
style_name: Dateiname ohne Pfad, z.B. 'verfahrensgebiet.qml'
Rückgabe: True bei Erfolg, False sonst
"""
if not layer or not layer.isValid():
return False
# Basis-Pfad: sn_basis/styles
base_dir = os.path.dirname(os.path.dirname(__file__)) # geht von functions/ eins hoch
style_path = os.path.join(base_dir, "styles", style_name)
if not os.path.exists(style_path):
print(f"Style-Datei nicht gefunden: {style_path}")
return False
ok, error_msg = layer.loadNamedStyle(style_path)
if not ok:
print(f"Style konnte nicht geladen werden: {error_msg}")
return False
layer.triggerRepaint()
return True

105
functions/sys_wrapper.py Normal file
View File

@@ -0,0 +1,105 @@
"""
sn_basis/functions/sys_wrapper.py System- und Pfad-Abstraktion
"""
from pathlib import Path
from typing import Union
import sys
from sn_basis.functions.os_wrapper import is_absolute_path, basename
_PathLike = Union[str, Path]
# ---------------------------------------------------------
# Plugin Root
# ---------------------------------------------------------
def get_plugin_root() -> Path:
"""
Liefert das Basisverzeichnis des Plugins.
"""
return Path(__file__).resolve().parents[2]
# ---------------------------------------------------------
# Pfad-Utilities
# ---------------------------------------------------------
def join_path(*parts: _PathLike) -> Path:
"""
Verbindet Pfadbestandteile OS-sicher.
"""
path = Path(parts[0])
for part in parts[1:]:
path /= part
return path
def file_exists(path: _PathLike) -> bool:
"""
Prüft, ob eine Datei existiert.
"""
try:
return Path(path).exists()
except Exception:
return False
def ensure_dir(path: _PathLike) -> Path:
"""
Stellt sicher, dass ein Verzeichnis existiert.
"""
p = Path(path)
p.mkdir(parents=True, exist_ok=True)
return p
# ---------------------------------------------------------
# Datei-IO
# ---------------------------------------------------------
def read_text(path: _PathLike, encoding: str = "utf-8") -> str:
"""
Liest eine Textdatei.
"""
try:
return Path(path).read_text(encoding=encoding)
except Exception:
return ""
def write_text(
path: _PathLike,
content: str,
encoding: str = "utf-8",
) -> bool:
"""
Schreibt eine Textdatei.
"""
try:
Path(path).write_text(content, encoding=encoding)
return True
except Exception:
return False
def add_to_sys_path(path: Union[str, Path]) -> None:
"""
Fügt einen Pfad zu sys.path hinzu, falls er noch nicht enthalten ist.
"""
p = str(path)
if p not in sys.path:
sys.path.insert(0, p)
def getattr_safe(obj, attr, default=None):
"""
Sicherer Zugriff auf ein Attribut.
Gibt das Attribut zurück, wenn es existiert,
ansonsten den Default-Wert (None, wenn nicht angegeben).
"""
try:
return getattr(obj, attr)
except Exception:
return default

14
functions/test.md Normal file
View File

@@ -0,0 +1,14 @@
mermaid´´´
flowchart TD
A[Projekt]
subgraph children[ ]
direction TB
B[src]
C[docs]
D[README.md]
end
A --> B
A --> C
A --> D

View File

@@ -1,35 +0,0 @@
from qgis.core import QgsProject, QgsExpressionContextUtils
def get_variable(key: str, scope: str = "project") -> str:
"""
Liefert den Wert einer sn_* Variable zurück.
key: Name ohne Präfix, z.B. "verfahrensnummer"
scope: 'project' oder 'global'
"""
projekt = QgsProject.instance()
var_name = f"sn_{key}"
if scope == "project":
return QgsExpressionContextUtils.projectScope(projekt).variable(var_name) or ""
elif scope == "global":
return QgsExpressionContextUtils.globalScope().variable(var_name) or ""
else:
raise ValueError("Scope muss 'project' oder 'global' sein.")
def set_variable(key: str, value: str, scope: str = "project"):
"""
Schreibt den Wert einer sn_* Variable.
key: Name ohne Präfix, z.B. "verfahrensnummer"
value: Wert, der gespeichert werden soll
scope: 'project' oder 'global'
"""
projekt = QgsProject.instance()
var_name = f"sn_{key}"
if scope == "project":
QgsExpressionContextUtils.setProjectVariable(projekt, var_name, value)
elif scope == "global":
QgsExpressionContextUtils.setGlobalVariable(var_name, value)
else:
raise ValueError("Scope muss 'project' oder 'global' sein.")

View File

@@ -0,0 +1,115 @@
"""
sn_basis/functions/variable_wrapper.py QGIS-Variablen-Abstraktion
"""
from typing import Any
from sn_basis.functions.qgiscore_wrapper import QgsProject
# ---------------------------------------------------------
# Versuch: QgsExpressionContextUtils importieren
# ---------------------------------------------------------
try:
from qgis.core import QgsExpressionContextUtils
_HAS_QGIS_VARIABLES = True
# ---------------------------------------------------------
# Mock-Modus
# ---------------------------------------------------------
except Exception:
_HAS_QGIS_VARIABLES = False
class _MockVariableStore:
global_vars: dict[str, str] = {}
project_vars: dict[str, str] = {}
class QgsExpressionContextUtils:
@staticmethod
def setGlobalVariable(name: str, value: str) -> None:
_MockVariableStore.global_vars[name] = value
@staticmethod
def globalScope():
class _Scope:
def variable(self, name: str) -> str:
return _MockVariableStore.global_vars.get(name, "")
return _Scope()
@staticmethod
def setProjectVariable(project: Any, name: str, value: str) -> None:
_MockVariableStore.project_vars[name] = value
@staticmethod
def projectScope(project: Any):
class _Scope:
def variable(self, name: str) -> str:
return _MockVariableStore.project_vars.get(name, "")
return _Scope()
# ---------------------------------------------------------
# Öffentliche API
# ---------------------------------------------------------
def get_variable(key: str, scope: str = "project") -> str:
"""
Liest eine QGIS-Variable.
:param key: Variablenname ohne Prefix
:param scope: 'project' oder 'global'
"""
var_name = f"sn_{key}"
if scope == "project":
project = QgsProject.instance()
return (
QgsExpressionContextUtils
.projectScope(project)
.variable(var_name)
or ""
)
if scope == "global":
return (
QgsExpressionContextUtils
.globalScope()
.variable(var_name)
or ""
)
raise ValueError("Scope muss 'project' oder 'global' sein.")
def set_variable(key: str, value: str, scope: str = "project") -> None:
"""
Setzt eine QGIS-Variable.
:param key: Variablenname ohne Prefix
:param value: Wert
:param scope: 'project' oder 'global'
"""
var_name = f"sn_{key}"
if scope == "project":
project = QgsProject.instance()
QgsExpressionContextUtils.setProjectVariable(
project,
var_name,
value,
)
return
if scope == "global":
QgsExpressionContextUtils.setGlobalVariable(
var_name,
value,
)
return
raise ValueError("Scope muss 'project' oder 'global' sein.")

43
main.py
View File

@@ -1,26 +1,53 @@
from qgis.PyQt.QtCore import QCoreApplication # sn_basis/main.py
from qgis.utils import plugins 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.ui.navigation import Navigation from sn_basis.ui.navigation import Navigation
class BasisPlugin: class BasisPlugin:
"""
Einstiegspunkt des sn_basis-Plugins.
Orchestriert UI und Fachmodule keine UI-Logik.
"""
def __init__(self, iface): def __init__(self, iface):
self.iface = iface # iface wird von QGIS übergeben, aber nicht direkt verwendet
self.ui = None self.ui = None
QCoreApplication.instance().aboutToQuit.connect(self.unload)
# QCoreApplication kann im Mock-Modus None sein
if QCoreApplication is not None:
app = getattr_safe(QCoreApplication, "instance")
if callable(app):
instance = app()
about_to_quit = getattr_safe(instance, "aboutToQuit")
connect = getattr_safe(about_to_quit, "connect")
if callable(connect):
connect(self.unload)
def initGui(self): def initGui(self):
# Basis-Navigation neu aufbauen """
self.ui = Navigation(self.iface) Initialisiert die Basis-Navigation und triggert initGui
aller abhängigen sn_-Plugins.
# Alle Fachplugins mit "sn_" prüfen und neu initialisieren """
self.ui = Navigation()
self.ui.init_ui()
for name, plugin in plugins.items(): for name, plugin in plugins.items():
if name.startswith("sn_") and name != "sn_basis": if name.startswith("sn_") and name != "sn_basis":
try: try:
plugin.initGui() init_gui = getattr_safe(plugin, "initGui")
if callable(init_gui):
init_gui()
except Exception as e: except Exception as e:
print(f"Fehler beim Neuinitialisieren von {name}: {e}") print(f"Fehler beim Neuinitialisieren von {name}: {e}")
self.ui.finalize_menu_and_toolbar()
def unload(self): def unload(self):
"""
Räumt UI-Komponenten sauber auf.
"""
if self.ui: if self.ui:
self.ui.remove_all() self.ui.remove_all()
self.ui = None self.ui = None

View File

@@ -1,13 +1,14 @@
[general] [general]
name=LNO Sachsen | Basisfunktionen name=LNO Sachsen | Plugin Basisfunktionen
qgisMinimumVersion=3.0 qgisMinimumVersion=3.40
qgisMaximumVersion=3.99
description=Plugin mit Basisfunktionen description=Plugin mit Basisfunktionen
version=25.11.4 version=26.3.6-unstable
author=Michael Otto author=Daniel Helbig
email=michael.otto@landkreis-mittelsachsen.de email=daniel.helbig@kreis-meissen.de
about=Plugin mit Basisfunktionen homepage=https://entwicklung.flurneuordnung-sachsen.de/AG_QGIS/Plugin_SN_Basis
category=Plugins tracker=https://entwicklung.flurneuordnung-sachsen.de/AG_QGIS/Plugin_SN_Basis/issues
homepage=https://entwicklung.vln-sn.de/AG_QGIS/Plugin_SN_Basis repository=https://entwicklung.flurneuordnung-sachsen.de/AG_QGIS/Plugin_SN_Basis/src/branch/unstable/
repository=https://entwicklung.vln-sn.de/AG_QGIS/Repository
supportsQt6=true
experimental=true experimental=true
deprecated=false
supportsQt6=true

351
modules/DataGrabber.py Normal file
View File

@@ -0,0 +1,351 @@
"""
DataGrabber module
==================
UI-freier Orchestrator für die Prüfung und Klassifikation von Datenquellen.
Der DataGrabber:
- klassifiziert die übergebene Quelle (Datei, Dienst, Datenbank, Excel),
- ruft passende Prüfer (Dateipruefer, Linkpruefer, Layerpruefer, Stilpruefer) auf,
- sammelt alle rohen ``pruef_ergebnis``-Objekte,
- aggregiert diese zu einem zusammenfassenden Ergebnis,
- **löst selbst keinerlei UI-Interaktion aus**.
Alle Nutzerinteraktionen (MessageBar, QMessageBox, Logging) erfolgen
ausschließlich über den ``Pruefmanager`` im aufrufenden Kontext (UI / Pipeline).
"""
from __future__ import annotations
import re
from typing import Any, Dict, List, Mapping, Optional, Tuple, Literal
from sn_basis.functions.os_wrapper import basename, path_suffix
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
from sn_basis.modules.Pruefmanager import Pruefmanager
from sn_basis.modules.Dateipruefer import Dateipruefer
from sn_basis.modules.linkpruefer import Linkpruefer
from sn_basis.modules.layerpruefer import Layerpruefer
from sn_basis.modules.stilpruefer import Stilpruefer
from sn_basis.modules.excel_importer import ExcelImporter
from sn_plan41.modules.listenauswerter import Listenauswerter
SourceType = Literal["service", "database", "excel", "unknown"]
SourceDict = Dict[str, List[Mapping[str, Any]]]
class DataGrabber:
"""
Analysiert und prüft Datenquellen für den Fachdatenabruf.
"""
def __init__(
self,
pruefmanager: Pruefmanager,
*,
datei_pruefer_cls: type[Dateipruefer] = Dateipruefer,
link_pruefer: Optional[Linkpruefer] = None,
layer_pruefer: Optional[Layerpruefer] = None,
stil_pruefer: Optional[Stilpruefer] = None,
excel_importer_cls: type[ExcelImporter] = ExcelImporter,
) -> None:
self.pruefmanager = pruefmanager
self._datei_pruefer_cls = datei_pruefer_cls
self.link_pruefer = link_pruefer or Linkpruefer()
self.layer_pruefer = layer_pruefer or Layerpruefer()
self.stil_pruefer = stil_pruefer or Stilpruefer()
self._excel_importer_cls = excel_importer_cls
self._source: Optional[str] = None
# ------------------------------------------------------------------
# Öffentliche API
# ------------------------------------------------------------------
def set_source(self, source: str) -> None:
"""Setzt die aktuell zu untersuchende Rohquelle."""
self._source = source
SourceType = str # "excel" | "datenbank" | "dienst" | "unbekannt"
def analyze_source_type(self, quelle: str) -> Tuple[SourceType, pruef_ergebnis]:
"""
Klassifiziert die Quelle und liefert das zugehörige pruef_ergebnis.
Reihenfolge:
1. Dateipruefer (Datei + Dateityp)
2. Linkpruefer (Dienst)
"""
# --------------------------------------------------
# 1. Datei prüfen (inkl. Typ-Erkennung)
# --------------------------------------------------
dateipruefer = Dateipruefer(pfad=quelle)
datei_ergebnis = dateipruefer.pruefe()
if datei_ergebnis.ok:
suffix = path_suffix(datei_ergebnis.kontext).lower()
print(f"[DataGrabber] Debug: analyze_source_type source={quelle} -> suffix={suffix}")
if suffix == ".xlsx":
return "excel", datei_ergebnis
if suffix in (".gpkg", ".sqlite"):
return "datenbank", datei_ergebnis
return "unbekannter_dateityp", datei_ergebnis
# --------------------------------------------------
# 2. Keine Datei → Link prüfen
# --------------------------------------------------
linkpruefer = Linkpruefer()
link_ergebnis = linkpruefer.pruefe(quelle)
if link_ergebnis.ok:
return "dienst", link_ergebnis
# --------------------------------------------------
# 3. Weder Datei noch Dienst
# --------------------------------------------------
return "unbekannte_quelle", link_ergebnis
def run(self, source: str) -> Tuple[SourceDict, pruef_ergebnis]:
"""
Führt die vollständige Quellprüfung aus.
Diese Methode ist **UIfrei**. Sie gibt rohe Ergebnisse zurück,
die vom Aufrufer über den ``Pruefmanager`` verarbeitet werden.
"""
self.set_source(source)
source_type, source_result = self.analyze_source_type(source)
print(f"[DataGrabber] Debug: run source={source} -> source_type={source_type}")
source_dict: SourceDict = {}
partial_results: List[pruef_ergebnis] = []
if source_type == "excel":
source_dict, partial_results = self._process_excel_source(source)
elif source_type == "database":
source_dict, partial_results = self._process_database_source(source)
elif source_type == "service":
source_dict, partial_results = self._process_service_source(source)
else:
partial_results.append(source_result)
summary = self._aggregate_results(source, source_dict, partial_results)
return source_dict, summary
# ------------------------------------------------------------------
# ExcelQuellen
# ------------------------------------------------------------------
def _process_excel_source(
self, filepath: str
) -> Tuple[SourceDict, List[pruef_ergebnis]]:
source_dict: SourceDict = {"rows": []}
results: List[pruef_ergebnis] = []
rows = ExcelImporter(filepath, self.pruefmanager).import_xlsx()
print(f"[DataGrabber] Debug: Excel-Linkliste geladen: {filepath}")
print(f"[DataGrabber] Debug: raw rows count: {len(rows)}")
if rows:
first = rows[:min(5, len(rows))]
print(f"[DataGrabber] Debug: first rows: {first}")
if not rows:
return source_dict, results
required_keys = {"ident", "gruppe", "kartenebene", "inhalt", "link", "provider", "stildatei"}
def extract_url(raw_link: str, provider: str) -> str:
if not raw_link:
return ""
if not isinstance(raw_link, str):
return str(raw_link)
if provider == "wfs":
url_match = re.search(r"url\s*=\s*['\"]([^'\"]+)['\"]", raw_link, re.IGNORECASE)
type_match = re.search(r"typename\s*=\s*['\"]([^'\"]+)['\"]", raw_link, re.IGNORECASE)
if url_match:
url = url_match.group(1).strip()
if type_match:
typename = type_match.group(1).strip()
separator = "&" if "?" in url else "?"
return f"url={url}{separator}service=WFS&request=GetFeature&typename={typename}"
return f"url={url}"
if provider == "wms":
# falls WMS-URL als url='...' vorliegt
match = re.search(r"url\s*=\s*['\"]([^'\"]+)['\"]", raw_link, re.IGNORECASE)
if match:
return match.group(1).strip()
if provider == "rest":
# REST/ArcGIS-Server: direkt nutzen
match = re.search(r"url\s*=\s*['\"]([^'\"]+)['\"]", raw_link, re.IGNORECASE)
if match:
return match.group(1).strip()
# allgemeines Rückfallverhalten
match = re.search(r"url\s*=\s*['\"]([^'\"]+)['\"]", raw_link, re.IGNORECASE)
if match:
return match.group(1).strip()
return raw_link.strip()
for row_index, raw_row in enumerate(rows, start=2):
if not isinstance(raw_row, Mapping):
pe = pruef_ergebnis(
ok=False,
meldung="Linklisten-Zeile ist nicht als Dictionary formatiert.",
aktion="ungueltige_zeile",
kontext={"zeile": row_index, "wert": raw_row},
)
results.append(self.pruefmanager.verarbeite(pe))
continue
normalized = {str(k).strip().lower(): v for k, v in raw_row.items() if k is not None}
if not required_keys.issubset(normalized.keys()):
missing = required_keys.difference(normalized.keys())
pe = pruef_ergebnis(
ok=False,
meldung=f"Linkliste fehlt erforderliche Spalten: {', '.join(sorted(missing))}",
aktion="spaltenfehlend",
kontext={"zeile": row_index, "fehlend": sorted(missing)},
)
results.append(self.pruefmanager.verarbeite(pe))
continue
ident = normalized.get("ident")
link_raw = normalized.get("link") or ""
provider = str(normalized.get("provider") or "").strip().lower()
stildatei_raw = normalized.get("stildatei") or ""
stildatei = None
if stildatei_raw and str(stildatei_raw).strip():
style_result = self.stil_pruefer.pruefe(str(stildatei_raw).strip())
results.append(self.pruefmanager.verarbeite(style_result))
if style_result.ok:
# Style-Pfad in der Datenkette beibehalten (absolut, wenn vorhanden).
stildatei = str(style_result.kontext or stildatei_raw).strip()
else:
stildatei = None
else:
results.append(self.pruefmanager.verarbeite(pruef_ergebnis(ok=True, meldung="Kein Stil angegeben", aktion="stil_optional", kontext=None)))
stildatei = None
if not ident or not link_raw or not provider:
pe = pruef_ergebnis(
ok=False,
meldung="Linklisten-Zeile hat fehlende Pflichtfelder (ident/link/provider).",
aktion="pflichtfelder_fehlen",
kontext={"zeile": row_index, "daten": raw_row},
)
results.append(self.pruefmanager.verarbeite(pe))
continue
link_url = extract_url(link_raw, provider)
# Provider-abhängige Linkvalidierung
if provider in ("wfs", "wms", "rest"):
# Webdienste: wir akzeptieren die URL-Form und prüfen nicht per network_head.
link_result = pruef_ergebnis(ok=True, meldung="Service-Link angenommen", aktion="service_link", kontext=link_url)
elif provider in ("ogr", "gpkg", "shp", "geojson"):
# OGR/Pfad: mit Linkpruefer (pfad oder lokale Datei) prüfen
link_result = self.link_pruefer.pruefe(link_url)
else:
link_result = self.link_pruefer.pruefe(link_url)
results.append(self.pruefmanager.verarbeite(link_result))
# stildatei wurde bereits oben geprüft und ggf. auf Dateiname gesetzt oder auf None
if not link_result.ok:
self.pruefmanager.verarbeite(
pruef_ergebnis(
ok=False,
meldung=f"Zeile {row_index}: fehlerhafter Link",
aktion="link_unvollstaendig",
kontext={"row": row_index, "ident": ident},
)
)
continue
result_row = {
"ident": ident,
"gruppe": normalized.get("gruppe"),
"Kartenebene": normalized.get("kartenebene"),
"Inhalt": normalized.get("inhalt"),
"Link": link_url,
"Provider": provider,
"stildatei": stildatei,
}
source_dict["rows"].append(result_row)
# Validierung über Listenauswerter
listenauswerter = Listenauswerter(self.pruefmanager, self.stil_pruefer or Stilpruefer())
validated, validation_results = listenauswerter.validate_rows(source_dict)
results.extend(validation_results)
return validated, results
# ------------------------------------------------------------------
# DatenbankQuellen
# ------------------------------------------------------------------
def _process_database_source(
self, db_path: str
) -> Tuple[SourceDict, List[pruef_ergebnis]]:
print(f"[DataGrabber] Debug: _process_database_source called, db_path={db_path}")
source_dict: SourceDict = {}
results: List[pruef_ergebnis] = []
return source_dict, results
# ------------------------------------------------------------------
# DienstQuellen
# ------------------------------------------------------------------
def _process_service_source(
self, link: str
) -> Tuple[SourceDict, List[pruef_ergebnis]]:
source_dict: SourceDict = {}
results: List[pruef_ergebnis] = []
return source_dict, results
# ------------------------------------------------------------------
# Aggregation
# ------------------------------------------------------------------
def _aggregate_results(
self,
source: str,
source_dict: SourceDict,
partial_results: List[pruef_ergebnis],
) -> pruef_ergebnis:
"""
Aggregiert Einzelprüfungen zu einem Gesamt-``pruef_ergebnis``.
**Keine UI-Interaktion.**
"""
rows = source_dict.get("rows") if isinstance(source_dict, dict) else None
if rows:
return pruef_ergebnis(
ok=True,
meldung="Quelle erfolgreich geprüft",
aktion="ok",
kontext={
"source": source,
"valid_entries": len(rows),
},
)
# Wenn die Linkliste zwar gelesen wurde, aber keine gültigen Zeilen verfügbar sind, geben wir spezifischere Infos zurück.
return pruef_ergebnis(
ok=False,
meldung="Keine validen Einträge in der Linkliste gefunden",
aktion="keine_validen_eintraege",
kontext={
"source": source,
"eintraege_gesamt": len(source_dict.get("rows", [])),
},
)

248
modules/Dateipruefer.py Normal file
View File

@@ -0,0 +1,248 @@
"""
sn_basis/modules/Dateipruefer.py
Erweiterter Dateiprüfer für Verfahrens-DB-Workflows mit vollständiger Unterstützung
der Anforderungen 1-2.e (leerer Pfad, fehlende Datei, bestehende Datei).
"""
from pathlib import Path
from typing import Optional, Literal
from sn_basis.functions.sys_wrapper import join_path, file_exists
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis, PruefAktion
DateiTyp = Literal["excel","datenbank","unbekannt"]
class Dateipruefer:
"""
Prüft Dateieingaben für Verfahrens-DB-Workflows und liefert :class:`pruef_ergebnis`.
**Funktionsweise (deine Anforderungen 1-2.e):**
+---------------------+------------------------------------------+---------------+
| **Fall** | **Ergebnis** | **ok** |
+=====================+==========================================+===============+
| 1. Leerer Pfad | ``temporaer_erlaubt`` | False |
+---------------------+------------------------------------------+---------------+
| 2.a Leerer Pfad | Pruefmanager fragt → ``temporaer_erzeugen`` | True |
+---------------------+------------------------------------------+---------------+
| 2.b Datei existiert | ``ok`` | True |
+---------------------+------------------------------------------+---------------+
| 2.c Ungültiger Pfad | ``datei_nicht_gefunden`` | False |
+---------------------+------------------------------------------+---------------+
| **2.d Datei fehlt** | **``datei_wird_erzeugt``** | **True** |
+---------------------+------------------------------------------+---------------+
| **2.e Datei da** | **``datei_existiert``** | **False** |
+---------------------+------------------------------------------+---------------+
Der Dateiprüfer führt **keine UI-Interaktion** durch.
Entscheidungen werden ausschließlich vom :class:`Pruefmanager` getroffen.
"""
def __init__(
self,
pfad: Optional[str],
basis_pfad: str = "",
leereingabe_erlaubt: bool = False,
standarddatei: Optional[str] = None,
temporaer_erlaubt: bool = False,
*,
verfahrens_db_modus: bool = True, # 🆕 Verfahrens-DB-spezifische Logik
) -> None:
"""
Parameters
----------
pfad : Optional[str]
Vom UI gelieferter Dateipfad (kann leer oder Whitespace sein).
basis_pfad : str, optional
Basisverzeichnis für relative Pfade (default: "").
leereingabe_erlaubt : bool, optional
Ob leere Eingabe grundsätzlich erlaubt ist (default: False).
standarddatei : Optional[str], optional
Optionaler Standardpfad (default: None).
temporaer_erlaubt : bool, optional
Ob bei leerer Eingabe temporäre Layer erlaubt sind (default: False).
verfahrens_db_modus : bool, optional
Aktiviert Verfahrens-DB-spezifische Logik (2.d, 2.e) (default: True).
"""
self.pfad = pfad
self.basis_pfad = basis_pfad
self.leereingabe_erlaubt = leereingabe_erlaubt
self.standarddatei = standarddatei
self.temporaer_erlaubt = temporaer_erlaubt
self.verfahrens_db_modus = verfahrens_db_modus
# ------------------------------------------------------------------
# Hilfsfunktionen
# ------------------------------------------------------------------
def erkenne_dateityp(self, pfad: Path) -> DateiTyp:
"""
Erkennt den Dateityp anhand der Endung.
"""
suffix = pfad.suffix.lower()
if suffix == ".xlsx":
return "excel"
if suffix in (".gpkg", ".sqlite"):
return "datenbank"
return "unbekannt"
def _pfad(self, relativer_pfad: str) -> Path:
"""Erzeugt OS-unabhängigen Pfad relativ zum Basisverzeichnis."""
return join_path(self.basis_pfad, relativer_pfad)
def _ist_leer(self) -> bool:
"""
Prüft robust, ob Eingabe als „leer" zu behandeln ist.
Returns
-------
bool
True bei None, leerem String oder reinem Whitespace.
"""
if self.pfad is None:
return True
if not isinstance(self.pfad, str):
return True
return not self.pfad.strip()
def _ist_gueltiger_gpkg_pfad(self, pfad: Path) -> bool:
"""
Prüft, ob Pfad für GPKG geeignet ist (Endung + Schreibrechte).
Returns
-------
bool
True wenn `.gpkg`-Endung und Verzeichnis beschreibbar.
"""
if not str(pfad).lower().endswith('.gpkg'):
return False
# Verzeichnis muss beschreibbar sein
return pfad.parent.exists() and pfad.parent.is_dir()
# ------------------------------------------------------------------
# Hauptlogik: deine Anforderungen 1-2.e
# ------------------------------------------------------------------
def pruefe(self) -> pruef_ergebnis:
"""
🆕 Prüft Dateieingabe gemäß Anforderungen 1-2.e.
**Workflow:**
1. **Leere Eingabe** → ``temporaer_erlaubt`` (Pruefmanager fragt)
2. **Pfad prüfen**:
- **Ungültig** → 2.c ``datei_nicht_gefunden``
- **Gültig, fehlt** → **2.d** ``datei_wird_erzeugt`` (ok=True!)
- **Gültig, existiert** → **2.e** ``datei_existiert`` (Pruefmanager fragt)
3. **Datei OK** → 2.b ``ok``
Returns
-------
pruef_ergebnis
Mit korrekter Aktion für jeden Fall.
"""
# 1. 🎯 ANFORDERUNG 1: Leere Eingabe
if self._ist_leer():
return self._handle_leere_eingabe()
# 2. Pfad normalisieren
pfad = self._pfad(self.pfad.strip())
#Excel-dateien erkennen
dateityp = self.erkenne_dateityp(pfad)
if dateityp == "excel":
if not file_exists(pfad):
return pruef_ergebnis(
ok=False,
meldung=f"Excel-Datei '{self.pfad}' wurde nicht gefunden.",
aktion="datei_nicht_gefunden",
kontext=pfad,
)
return pruef_ergebnis(
ok=True,
meldung="Excel-Datei ist gültig.",
aktion="ok",
kontext=pfad,
)
if dateityp != "datenbank":
return pruef_ergebnis(
ok=False,
meldung=f"Der Pfad '{self.pfad}' ist kein unterstützter Dateityp.",
aktion="unbekannter_dateityp",
kontext=pfad,
)
# 🆕 2.c: Ungültiger GPKG-Pfad?
if not self.verfahrens_db_modus or not self._ist_gueltiger_gpkg_pfad(pfad):
return pruef_ergebnis(
ok=False,
meldung=f"Der Pfad '{self.pfad}' ist kein gültiger GPKG-Pfad.",
aktion="datei_nicht_gefunden",
kontext=pfad,
)
# 🆕 2.d: Gültiger Pfad, Datei fehlt → DIREKT WEITER (ok=True!)
if not file_exists(pfad):
return pruef_ergebnis(
ok=True, # 🎯 WICHTIG: Pipeline fortsetzen!
meldung=f"Datei '{self.pfad}' wird erzeugt.",
aktion="datei_wird_erzeugt",
kontext=pfad,
)
# 🆕 2.e: Datei existiert → Pruefmanager fragt Überschreiben/etc.
return pruef_ergebnis(
ok=False, # 🎯 Pruefmanager soll 4-Optionen-Dialog zeigen
meldung=f"Datei '{self.pfad}' existiert bereits.",
aktion="datei_existiert",
kontext=pfad,
)
# 2.b: Wird nicht erreicht (durch 2.e abgefangen)
# ------------------------------------------------------------------
# Leere Eingabe (ANFORDERUNG 1, 2.a)
# ------------------------------------------------------------------
def _handle_leere_eingabe(self) -> pruef_ergebnis:
"""
Behandelt leere Eingaben (Priorität: leereingabe → Standard → temporär → Fehler).
"""
if self.leereingabe_erlaubt:
return pruef_ergebnis(
ok=False,
meldung="Das Dateifeld ist leer. Soll ohne Datei fortgefahren werden?",
aktion="leereingabe_erlaubt",
kontext=None,
)
if self.standarddatei:
return pruef_ergebnis(
ok=False,
meldung=(
"Es wurde keine Datei angegeben. "
f"Soll die Standarddatei '{self.standarddatei}' verwendet werden?"
),
aktion="standarddatei_vorschlagen",
kontext=self._pfad(self.standarddatei),
)
if self.temporaer_erlaubt:
return pruef_ergebnis(
ok=False,
meldung=(
"Es wurde keine Datei angegeben. "
"Sollen temporäre Layer erzeugt werden?"
),
aktion="temporaer_erlaubt",
kontext=None,
)
return pruef_ergebnis(
ok=False,
meldung="Es wurde keine Datei angegeben.",
aktion="leereingabe_nicht_erlaubt",
kontext=None,
)

592
modules/Datenabruf.py Normal file
View File

@@ -0,0 +1,592 @@
# sn_basis/modules/Datenabruf.py
"""
Modul ``datenabruf``
Enthält die Klasse :class:`Datenabruf`, die für eine Menge bereits
validierter Links (aus ``validate_rows``) die Fachdaten abruft und
aggregierte Prüfergebnisse liefert.
Designprinzipien
----------------
- Die BBOX wird serverseitig angewendet: wenn ein Raumfilter aktiv ist,
wird die BBOX in die Abruf-URL eingebettet (außer bei WMS).
- Alle QGIS-Interaktionen laufen über die Wrapper `qgiscore_wrapper` und
`qgisui_wrapper`.
- Fehler werden als kurze Strings zurückgegeben und zentral in `log_fehler`
gesammelt; erfolgreiche Aufrufe werden in `log_geladen` protokolliert.
- Die Methode ist pdoc-kompatibel dokumentiert und bewusst einfach gehalten.
"""
from typing import Any, Callable, Dict, List, Mapping, Optional, Tuple
from urllib.parse import urlparse, parse_qsl, urlencode, urlunparse
import json
import time
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
from sn_basis.functions import qgiscore_wrapper as qgiscore
from sn_basis.functions import qgisui_wrapper as qgisui
from sn_basis.functions import qt_wrapper as qt
DataDict = Dict[str, List[Mapping[str, Any]]]
class Datenabruf:
"""
Führt den eigentlichen Fachdatenabruf für eine Menge validierter Links durch.
Erwartet ein ``DataDict`` der Form ``{"rows": [row1, row2, ...]}``.
"""
def __init__(self, pruefmanager: Any) -> None:
"""
Initialisiert eine neue Instanz des Datenabrufs.
Parameters
----------
pruefmanager:
Instanz des Pruefmanagers, der :class:`pruef_ergebnis` verarbeitet.
"""
self.pruefmanager = pruefmanager
# ------------------------------------------------------------------ #
# Öffentliche API
# ------------------------------------------------------------------ #
def datenabruf(
self,
result_dict: DataDict,
raumfilter: str,
verfahrensgebiet_layer: Any,
speicherort: str,
pruef_ergebnisse: Optional[List[Any]] = None,
progress: Optional[Any] = None,
) -> Tuple[Dict[str, Any], List[Any]]:
"""
Ruft für alle Zeilen in ``result_dict["rows"]`` die Fachdaten ab und
liefert ein DatenDict sowie die Liste verarbeiteter Pruefergebnisse.
Logging / Aggregation
---------------------
Am Ende enthält das zusammenfassende PruefErgebnis im Kontext:
- geladen: dict(dienst -> anzahl geladen)
- fehler: dict(dienst -> fehlermeldung)
- relevant: dict(dienst -> anzahl relevant)
- ausserhalb: dict(dienst -> anzahl geladen, aber ausserhalb)
"""
if pruef_ergebnisse is None:
processed_results: List[Any] = []
else:
processed_results = list(pruef_ergebnisse)
rows = result_dict.get("rows", [])
daten: Dict[str, List[Any]] = {}
# 1) Räumliche Filtergeometrie bestimmen (BBox oder None)
bbox_geom = self._determine_spatial_filter(raumfilter, verfahrensgebiet_layer)
filter_crs_authid = None
if isinstance(bbox_geom, dict):
raw_crs = bbox_geom.get("crs_authid")
filter_crs_authid = str(raw_crs) if raw_crs else None
# Globale Logs über alle Dienste hinweg
log_geladen: Dict[str, int] = {}
log_fehler: Dict[str, str] = {}
log_relevant: Dict[str, int] = {}
log_ausserhalb: Dict[str, int] = {}
# 2) Über alle Zeilen iterieren
total_rows = len(rows)
for idx, row in enumerate(rows, start=1):
if progress is not None:
progress.set_label(f"Datenabruf {idx}/{total_rows}")
if progress.is_canceled():
pe_cancel = pruef_ergebnis(
ok=False,
meldung="Datenabruf durch Benutzer abgebrochen",
aktion="abbruch",
kontext={"schritt": idx},
)
processed_results.append(self.pruefmanager.verarbeite(pe_cancel))
break
ident = row.get("ident")
link = row.get("Link")
provider = row.get("Provider")
if not ident or not link or not provider:
pe = pruef_ergebnis(
ok=False,
meldung="Ungültige Zeile im Datenabruf (fehlende Pflichtfelder)",
aktion="pflichtfelder_fehlen",
kontext=row,
)
processed_results.append(self.pruefmanager.verarbeite(pe))
continue
# Lesbarer Dienstname für Logs
thema = row.get("Inhalt") or row.get("Thema") or row.get("Titel") or str(ident)
# 2a) Provider-spezifische URL zusammenbauen
# Wenn Raumfilter aktiv ist, übergeben wir bbox_geom an _build_provider_url,
# außer bei WMS (WMS bleibt unverändert).
use_bbox = (raumfilter != "ohne") and (str(provider).upper() != "WMS")
url = self._build_provider_url(link=link, provider=str(provider), bbox_geom=bbox_geom if use_bbox else None)
# 2b) Fachdaten abrufen
features, error_msg = self._fetch_features(
url=url,
provider=str(provider),
cancel_callback=(progress.is_canceled if progress is not None else None),
)
if progress is not None:
if hasattr(progress, "set_value"):
progress.set_value(idx)
# 2c) Logs und Aggregation
if error_msg:
# Fehler beim Abruf
log_fehler[thema] = error_msg
pe_err = pruef_ergebnis(
ok=False,
meldung=f"Fehler beim Abruf von {thema}: {error_msg}",
aktion="url_nicht_erreichbar",
kontext={"ident": ident, "thema": thema, "url": url, "error": error_msg},
)
processed_results.append(self.pruefmanager.verarbeite(pe_err))
# daten[ident] bleibt nicht gesetzt oder leer
daten[str(ident)] = []
continue
# Erfolgreich aufgerufen (auch wenn features == [])
anzahl_geladen = len(features)
log_geladen[thema] = anzahl_geladen
# Da die BBOX serverseitig angewendet wurde:
# - anzahl_geladen > 0 -> relevant
# - anzahl_geladen == 0 -> ausserhalb
if anzahl_geladen > 0:
log_relevant[thema] = anzahl_geladen
daten[str(ident)] = features
else:
log_ausserhalb[thema] = 0
daten[str(ident)] = []
# 2d) Kurzes Prüfergebnis pro Zeile
pe_row = pruef_ergebnis(
ok=True,
meldung=(
f"Datenabruf für ident={ident}: {anzahl_geladen} geladene Objekte"
),
aktion="datenabruf",
kontext={
"ident": ident,
"thema": thema,
"anzahl_gesamt": anzahl_geladen,
"url": url,
},
)
processed_results.append(self.pruefmanager.verarbeite(pe_row))
# 3) Zusammenfassendes Prüfergebnis (wie alter DataGrabber)
summary_kontext = {
"geladen": log_geladen,
"fehler": log_fehler,
"relevant": log_relevant,
"ausserhalb": log_ausserhalb,
}
pe_summary = pruef_ergebnis(
ok=(len(log_fehler) == 0),
meldung=(
f"Datenabruf abgeschlossen: {len(log_geladen)} Dienste geladen, "
f"{len(log_fehler)} Fehler"
),
aktion="datenabruf",
kontext=summary_kontext,
)
processed_results.append(self.pruefmanager.verarbeite(pe_summary))
daten_dict: Dict[str, Any] = {
"speicherort": speicherort,
"daten": daten,
}
return daten_dict, processed_results
# ------------------------------------------------------------------ #
# Hilfsmethoden: räumlicher Filter
# ------------------------------------------------------------------ #
def _determine_spatial_filter(self, raumfilter: str, verfahrensgebiet_layer: Any) -> Optional[Any]:
"""
Bestimmt die räumliche Filtergeometrie (BBox) abhängig vom Raumfilter.
Returns
-------
Optional[Any]
Eine Geometrie/Extent (z. B. QgsRectangle) oder ``None``.
"""
if raumfilter == "ohne":
return None
if verfahrensgebiet_layer is None:
return None
if raumfilter == "Verfahrensgebiet":
extent = qgiscore.get_layer_extent(verfahrensgebiet_layer)
if extent is None:
return None
crs_authid = None
try:
if hasattr(verfahrensgebiet_layer, "crs") and callable(getattr(verfahrensgebiet_layer, "crs")):
crs = verfahrensgebiet_layer.crs()
if crs is not None and hasattr(crs, "authid") and callable(getattr(crs, "authid")):
crs_authid = crs.authid()
except Exception:
crs_authid = None
return {"extent": extent, "crs_authid": crs_authid}
if raumfilter == "Pufferlayer":
buffer_layer = qgiscore.create_buffer_layer(
source_layer=verfahrensgebiet_layer,
distance_m=1000.0,
layer_name="Verfahrensgebiet_Puffer_1km",
)
if buffer_layer is not None:
extent = qgiscore.get_layer_extent(buffer_layer)
if extent is None:
return None
crs_authid = None
try:
if hasattr(buffer_layer, "crs") and callable(getattr(buffer_layer, "crs")):
crs = buffer_layer.crs()
if crs is not None and hasattr(crs, "authid") and callable(getattr(crs, "authid")):
crs_authid = crs.authid()
except Exception:
crs_authid = None
return {"extent": extent, "crs_authid": crs_authid}
return None
# ------------------------------------------------------------------ #
# Hilfsmethoden: Provider-URL und Datenabruf
# ------------------------------------------------------------------ #
def _build_provider_url(self, link: str, provider: str, bbox_geom: Optional[Any]) -> str:
"""
Baut eine Provider-spezifische Abruf-URL. Wenn `bbox_geom` übergeben
wird, wird sie in die URL eingebettet (außer bei WMS).
Erwartet: provider ist gesetzt (z. B. "WFS", "REST", "OGR", "WMS").
"""
provider_norm = (provider or "").upper()
base_link = (link or "").strip()
if base_link.lower().startswith("url="):
base_link = base_link[4:].strip()
if provider_norm == "WFS" and base_link.count("?") > 1:
first, rest = base_link.split("?", 1)
base_link = f"{first}?{rest.replace('?', '&')}"
extent_obj = bbox_geom
crs_authid: Optional[str] = None
if isinstance(bbox_geom, dict):
extent_obj = bbox_geom.get("extent")
raw_crs = bbox_geom.get("crs_authid")
crs_authid = str(raw_crs) if raw_crs else None
# WMS: unverändert durchreichen
if provider_norm == "WMS":
return base_link
# Versuche bbox-String zu erzeugen (falls Raumfilter aktiv)
bbox_str: Optional[str] = None
if extent_obj is not None:
try:
extent_to_bbox = getattr(__import__("sn_basis.functions.qgiscore_wrapper", fromlist=["qgiscore_wrapper"]), "extent_to_bbox_string", None)
if callable(extent_to_bbox):
bbox_str = extent_to_bbox(extent_obj)
else:
# Fallback: einfache xmin/ymin/xmax/ymax-Extraktion (duck-typing)
if hasattr(extent_obj, "xmin") and callable(getattr(extent_obj, "xmin")):
bbox_str = f"{extent_obj.xmin()},{extent_obj.ymin()},{extent_obj.xmax()},{extent_obj.ymax()}"
elif isinstance(extent_obj, (tuple, list)) and len(extent_obj) == 4:
bbox_str = f"{extent_obj[0]},{extent_obj[1]},{extent_obj[2]},{extent_obj[3]}"
else:
bbox_str = str(extent_obj)
except Exception:
bbox_str = None
parsed = urlparse(base_link)
query_params = dict(parse_qsl(parsed.query, keep_blank_values=True))
if provider_norm == "WFS":
query_params.setdefault("service", "WFS")
query_params.setdefault("request", "GetFeature")
query_params.setdefault("outputFormat", "application/json")
if bbox_str:
query_params.setdefault("BBOX", bbox_str)
if crs_authid:
query_params.setdefault("SRSNAME", crs_authid)
new_query = urlencode(query_params, doseq=True)
rebuilt = parsed._replace(query=new_query)
return urlunparse(rebuilt)
if provider_norm in ("REST", "ARCGIS", "ARCGISFEATURESERVER", "ARCGIS_FEATURESERVER"):
# ArcGIS FeatureServer erwartet i.d.R. den /query-Endpunkt
rest_base = base_link.rstrip("/")
if not rest_base.lower().endswith("/query"):
rest_base = f"{rest_base}/query"
parsed_rest = urlparse(rest_base)
query_params = dict(parse_qsl(parsed_rest.query, keep_blank_values=True))
query_params.setdefault("where", "1=1")
query_params.setdefault("outFields", "*")
query_params.setdefault("returnGeometry", "true")
query_params.setdefault("f", query_params.get("f", "json"))
if bbox_str:
geometry_envelope = None
try:
if hasattr(extent_obj, "xmin") and callable(getattr(extent_obj, "xmin")):
geometry_envelope = {
"xmin": extent_obj.xmin(),
"ymin": extent_obj.ymin(),
"xmax": extent_obj.xmax(),
"ymax": extent_obj.ymax(),
}
elif isinstance(extent_obj, (tuple, list)) and len(extent_obj) == 4:
geometry_envelope = {
"xmin": extent_obj[0],
"ymin": extent_obj[1],
"xmax": extent_obj[2],
"ymax": extent_obj[3],
}
else:
parts = [p.strip() for p in str(bbox_str).split(",")]
if len(parts) == 4:
geometry_envelope = {
"xmin": float(parts[0]),
"ymin": float(parts[1]),
"xmax": float(parts[2]),
"ymax": float(parts[3]),
}
except Exception:
geometry_envelope = None
if geometry_envelope is not None:
query_params.setdefault("geometry", json.dumps(geometry_envelope))
else:
query_params.setdefault("geometry", bbox_str)
query_params.setdefault("geometryType", "esriGeometryEnvelope")
query_params.setdefault("spatialRel", "esriSpatialRelIntersects")
if crs_authid and ":" in crs_authid:
srid = crs_authid.split(":", 1)[1]
if srid.isdigit():
query_params.setdefault("inSR", srid)
query_params.setdefault("outSR", srid)
new_query = urlencode(query_params, doseq=True)
rebuilt = parsed_rest._replace(query=new_query)
return urlunparse(rebuilt)
# Default: generischer bbox-Parameter (nur wenn vorhanden)
if bbox_str:
query_params.setdefault("bbox", bbox_str)
new_query = urlencode(query_params, doseq=True)
rebuilt = parsed._replace(query=new_query)
return urlunparse(rebuilt)
def _fetch_features(
self,
url: str,
provider: str,
cancel_callback: Optional[Callable[[], bool]] = None,
) -> Tuple[List[Any], Optional[str]]:
"""
Führt den eigentlichen Abruf der Fachdaten durch.
Returns
-------
Tuple[List[Any], Optional[str]]
- features: Liste der geladenen Features (ggf. leer)
- error_msg: None bei Erfolg, sonst kurzer Fehlertext
"""
features: List[Any] = []
prov = str(provider).upper()
# WMS: kein Featureabruf; caller behandelt WMS separat (hier defensiv)
if prov == "WMS":
return [], None
# OGR / lokale Dateien: versuche QGIS-Layer (wenn QGIS verfügbar)
if prov in ("OGR", "GPKG", "SHP", "GEOJSON"):
if getattr(qgiscore, "QGIS_AVAILABLE", False):
try:
layer = qgiscore.QgsVectorLayer(url, "tmp", "ogr")
if not layer or not getattr(layer, "isValid", lambda: False)():
return [], "Layer ungültig oder konnte nicht geladen werden"
for feat in layer.getFeatures():
features.append(feat)
return features, None
except FileNotFoundError:
return [], "Lokale Datei nicht gefunden"
except Exception as exc:
return [], f"Fehler beim Laden der OGR-Quelle: {exc}"
else:
# Mock: falls GeoJSON-Datei vorhanden, versuche lokale Datei zu lesen
try:
if url.lower().endswith(".geojson"):
with open(url, "r", encoding="utf-8") as fh:
data = json.load(fh)
if isinstance(data, dict) and data.get("type") == "FeatureCollection":
return data.get("features", []), None
return [], "Keine QGIS-Umgebung und keine lesbare lokale GeoJSON"
except FileNotFoundError:
return [], "Lokale Datei nicht gefunden"
except Exception as exc:
return [], f"Fehler beim Lesen lokaler GeoJSON (Mock): {exc}"
# HTTP-basierte Dienste (WFS, REST/ArcGIS, generisch)
response_text: Optional[str] = None
http_error: Optional[str] = None
# QGIS NetworkAccessManager bevorzugen
_FETCH_TIMEOUT_MS = 30_000 # 30 Sekunden
aborted_or_timed_out = False
attempted_qgis_fetch = False
if callable(cancel_callback) and cancel_callback():
return [], "Abbruch durch Benutzer"
if getattr(qgiscore, "QGIS_AVAILABLE", False) and getattr(qgiscore, "QgsNetworkAccessManager", None) is not None:
attempted_qgis_fetch = True
try:
manager = qgiscore.QgsNetworkAccessManager.instance()
# Netzwerk-Timeout global setzen (QGIS >= 3.6)
if hasattr(manager, "setTimeout"):
manager.setTimeout(_FETCH_TIMEOUT_MS)
_qt = __import__("sn_basis.functions.qt_wrapper", fromlist=["qt_wrapper"])
QUrl = getattr(_qt, "QUrl", None)
QNetworkRequest = getattr(_qt, "QNetworkRequest", None)
QEventLoop = getattr(_qt, "QEventLoop", None)
QTimer = getattr(_qt, "QTimer", None)
if QUrl is not None and QNetworkRequest is not None:
req = QNetworkRequest(QUrl(url))
reply = manager.get(req)
if QEventLoop is not None:
loop = QEventLoop()
reply.finished.connect(loop.quit)
_poll_timer = None
if QTimer is not None:
try:
_poll_timer = QTimer()
_poll_timer.setSingleShot(False)
_poll_timer.timeout.connect(loop.quit)
_poll_timer.start(100)
except Exception:
_poll_timer = None
start_time = time.monotonic()
while True:
if callable(cancel_callback) and cancel_callback():
reply.abort()
http_error = "Abbruch durch Benutzer"
aborted_or_timed_out = True
break
elapsed_ms = int((time.monotonic() - start_time) * 1000)
if elapsed_ms >= _FETCH_TIMEOUT_MS:
reply.abort()
http_error = f"Timeout nach {_FETCH_TIMEOUT_MS // 1000} s: {url}"
aborted_or_timed_out = True
break
if hasattr(reply, "isFinished") and reply.isFinished():
break
loop.exec()
try:
if hasattr(qt, "QCoreApplication") and hasattr(qt.QCoreApplication, "processEvents"):
qt.QCoreApplication.processEvents()
except Exception:
pass
if _poll_timer is not None:
try:
_poll_timer.stop()
except Exception:
pass
if not aborted_or_timed_out:
# Fehler aus Reply auslesen
err_code = None
try:
err_code = reply.error()
except Exception:
pass
if err_code and int(err_code) != 0:
http_error = f"Netzwerkfehler ({err_code}): {reply.errorString()}"
if http_error:
# Timeout oder Netzwerkfehler keinen Body lesen
pass
else:
try:
raw = reply.readAll()
data_bytes = bytes(raw) if hasattr(raw, "__bytes__") else raw
response_text = data_bytes.decode("utf-8", errors="replace")
except Exception:
try:
response_text = reply.text()
except Exception:
response_text = None
except Exception as exc:
http_error = f"QgsNetworkAccessManager error: {exc}"
response_text = None
# Fallback: requests nur wenn kein harter Abbruch/Timeout im QGIS-Request vorlag
if response_text is None and (not attempted_qgis_fetch or not aborted_or_timed_out):
try:
import requests # lokal import, keine harte Abhängigkeit
r = requests.get(url, timeout=30)
r.raise_for_status()
response_text = r.text
except Exception as exc:
http_error = f"requests error: {exc}"
response_text = None
if response_text is None:
return [], http_error or "keine Antwort vom Server"
# Versuche JSON/GeoJSON zu parsen
try:
parsed = json.loads(response_text)
if isinstance(parsed, dict) and parsed.get("type") == "FeatureCollection":
return parsed.get("features", []), None
if isinstance(parsed, dict) and "features" in parsed:
return parsed.get("features", []), None
if prov in ("REST", "ARCGIS", "ARCGISFEATURESERVER", "ARCGIS_FEATURESERVER", "WFS"):
return [], "Antwort enthält keine Feature-Liste"
# Sonst: gib das gesamte JSON als einzelnes Objekt zurück
return [parsed], None
except json.JSONDecodeError:
# Nicht-JSON-Antwort (z. B. GML). Wenn QGIS verfügbar, versuche GML via temporärer Datei + OGR
if getattr(qgiscore, "QGIS_AVAILABLE", False):
try:
import tempfile
with tempfile.NamedTemporaryFile(suffix=".gml", delete=False, mode="w", encoding="utf-8") as fh:
fh.write(response_text)
tmp_path = fh.name
layer = qgiscore.QgsVectorLayer(tmp_path, "tmp_gml", "ogr")
if layer and getattr(layer, "isValid", lambda: False)():
for feat in layer.getFeatures():
features.append(feat)
return features, None
return [], "GML-Antwort konnte nicht als Layer geladen werden"
except Exception as exc:
return [], f"Fehler beim Parsen von GML: {exc}"
# Wenn alles fehlschlägt:
return [], "Antwort konnte nicht als JSON oder GML geparst werden"

View File

@@ -0,0 +1 @@
#Datenbankpruefer.py

447
modules/Datenschreiber.py Normal file
View File

@@ -0,0 +1,447 @@
# sn_basis/modules/Datenschreiber.py
"""
Modul Datenschreiber
Enthält die Klasse Datenschreiber mit drei Hauptmethoden:
- schreibe_Daten: schreibt die abgerufenen Daten in die Ziel-GPKG/Dateien,
fragt bei vorhandenen Layern nach Überschreiben/Anhängen/Abbrechen und
legt Stile in der Datenbank ab.
- lade_Layer: lädt die erzeugten/aktualisierten Layer ins Projekt und
wendet die Vorgabestile an; sortiert abschließend die Layer.
- schreibe_log: schreibt die verarbeiteten Pruefergebnisse strukturiert in
eine Log-Datei im angegebenen Speicherort.
Die Implementierung verwendet die Wrapper-APIs:
- qgiscore_wrapper als qgiscore
- qgisui_wrapper als qgisui (nur wenn nötig)
- qt_wrapper als qt
Wichtig
------
Alle Nutzerinteraktionen (z. B. Überschreiben / Anhängen / Abbrechen) werden
zentral über den Pruefmanager gebündelt. Die Methode `ask_overwrite_append_cancel`
des Pruefmanagers wird verwendet, damit UI-Interaktionen an einer Stelle
konsolidiert und testbar sind.
"""
from __future__ import annotations
from typing import Any, Dict, List, Optional
import os
import json
import re
import datetime
import sqlite3
from sn_basis.functions import qgiscore_wrapper as qgiscore
from sn_basis.functions.os_wrapper import normalize_path, is_absolute_path
from sn_basis.functions.sys_wrapper import get_plugin_root, join_path, file_exists
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
class Datenschreiber:
"""
Schreibt abgerufene Fachdaten in die Zieldatenbank/Dateien und lädt
die Layer ins Projekt.
Konstruktor
----------
pruefmanager:
Instanz des Pruefmanagers; wird verwendet, um Pruefergebnisse zu
verarbeiten und Nutzerinteraktionen zu zentralisieren.
gpkg_path:
Pfad zur Ziel-GPKG-Datei (oder Verzeichnis). Wenn None, muss der
Aufrufer einen Speicherort übergeben.
"""
def __init__(self, pruefmanager: Any, gpkg_path: Optional[str] = None) -> None:
self.pruefmanager = pruefmanager
self.gpkg_path = str(gpkg_path) if gpkg_path else None
def _resolve_style_path(self, style_path: Optional[str]) -> Optional[str]:
if not style_path:
return None
style_path_str = str(style_path).strip()
if not style_path_str:
return None
if not is_absolute_path(style_path_str):
plugin_root = get_plugin_root()
style_path_str = str(join_path(plugin_root, "sn_plan41", "assets", style_path_str))
style_path_str = str(normalize_path(style_path_str))
return style_path_str if file_exists(style_path_str) else None
def _store_style_in_gpkg(self, layer_name: str, style_path: str, layer: Optional[Any] = None) -> None:
"""Stellt sicher, dass der Stil in der layer_styles-Tabelle der GPKG gespeichert wird."""
try:
with open(style_path, "r", encoding="utf-8") as fh:
style_qml = fh.read()
f_geometry_column = ''
if layer is not None:
try:
if hasattr(layer, 'geometryColumn'):
f_geometry_column = str(layer.geometryColumn())
elif hasattr(layer, 'dataProvider') and hasattr(layer.dataProvider(), 'geometryColumnName'):
f_geometry_column = str(layer.dataProvider().geometryColumnName())
except Exception:
f_geometry_column = ''
with sqlite3.connect(self.gpkg_path) as conn:
cur = conn.cursor()
cur.execute(
"""
CREATE TABLE IF NOT EXISTS layer_styles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
f_table_catalog TEXT,
f_table_schema TEXT,
f_table_name TEXT NOT NULL,
f_geometry_column TEXT,
styleName TEXT,
styleQML TEXT,
styleSLD TEXT,
useAsDefault BOOLEAN,
description TEXT,
owner TEXT,
ui TEXT,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP
)
"""
)
# Das aktuelle QGIS-Style-Verhalten: bestehenden Style für denselben Layer nicht löschen (nur appenden)
# Wir wollen aber Default-Style setzen: alte Default-Styles entfernen.
cur.execute(
"UPDATE layer_styles SET useAsDefault = 0 WHERE f_table_name = ?",
(layer_name,),
)
# Fülle die bekannten QGIS-Kolonnen
style_name = os.path.basename(style_path)
cur.execute(
"INSERT INTO layer_styles (f_table_catalog, f_table_schema, f_table_name, f_geometry_column, styleName, styleQML, styleSLD, useAsDefault, description, owner, ui) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
(
'',
'',
layer_name,
f_geometry_column,
style_name,
style_qml,
None,
1,
'',
'',
'',
),
)
conn.commit()
except Exception as exc:
self.pruefmanager.verarbeite(
pruef_ergebnis(
ok=False,
meldung=f"Fehler beim Speichern des Layer-Stils in GPKG: {exc}",
aktion="style_gpkg_speichern_fehlgeschlagen",
kontext={"layer_name": layer_name, "style_path": style_path},
)
)
# ------------------------------------------------------------------ #
def schreibe_Daten(
self,
daten_dict: Dict[str, Any],
processed_results: List[Any],
speicherort: str,
) -> List[Dict[str, Any]]:
"""
Schreibt die übergebenen Layer in die Ziel-GPKG.
Erwartung:
- daten_dict["daten"] enthält Einträge der Form:
ident -> {"layer": QgsVectorLayer}
- self.gpkg_path ist ein str
"""
if not speicherort:
raise ValueError("Ein gültiger Speicherort (speicherort) muss übergeben werden.")
# gpkg_path einmalig setzen / normalisieren
if not self.gpkg_path:
self.gpkg_path = str(speicherort)
results: List[Dict[str, Any]] = []
daten_map: Dict[str, Any] = daten_dict.get("daten", {})
for ident, entry in daten_map.items():
layer = None
style_path = None
# -----------------------------
# Layer extrahieren
# -----------------------------
if isinstance(entry, dict):
layer = entry.get("layer")
style_path = self._resolve_style_path(entry.get("style_path"))
if layer is None or not hasattr(layer, "isValid") or not layer.isValid():
pe_err = pruef_ergebnis(
ok=False,
meldung=f"Ungültiger Layer für {ident}",
aktion="save_exception",
kontext={"ident": ident},
)
self.pruefmanager.verarbeite(pe_err)
continue
# -----------------------------
# Layername bestimmen
# -----------------------------
thema = None
for pe in processed_results:
try:
kontext = getattr(pe, "kontext", None) or {}
if kontext.get("ident") == ident:
thema = kontext.get("thema")
break
except Exception:
continue
layer_name_raw = thema or str(ident)
layer_name = re.sub(r"[^A-Za-z0-9_]+", "_", layer_name_raw).strip("_")
if not layer_name:
layer_name = f"layer_{ident}"
# Layer in GPKG schreiben
err_msg = self._write_layer_to_gpkg(layer_name=layer_name, layer=layer)
if err_msg is not None:
pe_err = pruef_ergebnis(
ok=False,
meldung=f"Fehler beim Schreiben des Layers {layer_name}: {err_msg}",
aktion="save_exception",
kontext={"ident": ident, "layer_name": layer_name},
)
self.pruefmanager.verarbeite(pe_err)
continue
# Wenn der Stil vorhanden und valide ist, als Default in GPKG-Style-Tabelle ablegen
if style_path:
self._store_style_in_gpkg(layer_name, style_path, layer)
# Erfolgsfall: Info für lade_Layer sammeln
layer_path = f"{self.gpkg_path}|layername={layer_name}"
results.append({
"layer_path": layer_path,
"thema": layer_name,
"ident": ident,
"style_path": style_path,
})
return results
# -----------------------------
# ------------------------------------------------------------------ #
# Lade Layer ins Projekt
# ------------------------------------------------------------------ #
def lade_Layer(self, layer_infos: List[Dict[str, Any]]) -> None:
"""
Lädt die in schreibe_Daten erzeugten/aktualisierten Layer ins Projekt
und wendet die Vorgabestile an.
"""
loaded_layers = []
for info in layer_infos:
layer_path = info.get("layer_path")
thema = info.get("thema")
if not layer_path:
continue
try:
layer = qgiscore.QgsVectorLayer(layer_path, thema, "ogr")
if not layer or not getattr(layer, "isValid", lambda: False)():
pe_err = pruef_ergebnis(
ok=False,
meldung=f"Layer {thema} konnte nicht geladen werden",
aktion="layer_nicht_gefunden",
kontext={"layer_path": layer_path},
)
self.pruefmanager.verarbeite(pe_err)
continue
except Exception as exc:
pe_err = pruef_ergebnis(
ok=False,
meldung=f"Fehler beim Erzeugen des Layers {thema}: {exc}",
aktion="layer_nicht_gefunden",
kontext={"layer_path": layer_path, "error": str(exc)},
)
self.pruefmanager.verarbeite(pe_err)
continue
style_path = info.get("style_path")
resolved_style_path = self._resolve_style_path(style_path)
if resolved_style_path:
try:
layer.loadNamedStyle(resolved_style_path)
layer.triggerRepaint()
except Exception as exc:
pe_warn = pruef_ergebnis(
ok=True,
meldung=f"Style konnte für {thema} nicht geladen werden: {exc}",
aktion="stil_laden_fehlgeschlagen",
kontext={"thema": thema, "style_path": resolved_style_path},
)
self.pruefmanager.verarbeite(pe_warn)
else:
try:
apply_style_fn = getattr(qgiscore, "apply_default_style_from_gpkg", None)
if callable(apply_style_fn):
apply_style_fn(self.gpkg_path, layer)
except Exception:
pe_warn = pruef_ergebnis(
ok=True,
meldung=f"Style konnte für {thema} nicht automatisch angewendet werden",
aktion="stil_not_implemented",
kontext={"thema": thema},
)
self.pruefmanager.verarbeite(pe_warn)
try:
# qgisui wrapper wird hier nicht direkt für die Abfrage verwendet;
# qgisui.add_layer_to_project sollte aber vorhanden sein.
from sn_basis.functions import qgisui_wrapper as qgisui
add_fn = getattr(qgisui, "add_layer_to_project", None)
if callable(add_fn):
add_fn(layer)
else:
# Fallback: falls wrapper nicht vorhanden, versuche QGIS-API direkt
if getattr(qgiscore, "QgsProject", None) is not None and qgiscore.QGIS_AVAILABLE:
qgiscore.QgsProject.instance().addMapLayer(layer)
loaded_layers.append(layer)
except Exception:
pe_err = pruef_ergebnis(
ok=False,
meldung=f"Layer {thema} konnte nicht ins Projekt geladen werden",
aktion="layer_nicht_gefunden",
kontext={"thema": thema},
)
self.pruefmanager.verarbeite(pe_err)
continue
# Sortiere Layer im Projekt nach ID (Wrapper-Funktion bevorzugt)
sort_fn = getattr(qgiscore, "sort_layers_by_id", None)
if callable(sort_fn):
try:
sort_fn()
except Exception:
pass
# ------------------------------------------------------------------ #
# Schreibe Log
# ------------------------------------------------------------------ #
def schreibe_log(self, processed_results: List[Any], speicherort: str) -> str:
"""
Schreibt die verarbeiteten Pruefergebnisse strukturiert in eine Log-Datei.
"""
if not speicherort:
raise ValueError("Ein gültiger Speicherort muss übergeben werden.")
log_dir = speicherort
os.makedirs(log_dir, exist_ok=True)
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
log_path = os.path.join(log_dir, f"datenabruf_log_{timestamp}.json")
serializable: List[Dict[str, Any]] = []
for pe in processed_results:
try:
entry = {}
entry["ok"] = getattr(pe, "ok", None) if hasattr(pe, "ok") else None
entry["meldung"] = getattr(pe, "meldung", None) if hasattr(pe, "meldung") else None
kontext = getattr(pe, "kontext", None) if hasattr(pe, "kontext") else None
entry["kontext"] = kontext
serializable.append(entry)
except Exception:
serializable.append({"raw": str(pe)})
with open(log_path, "w", encoding="utf-8") as fh:
json.dump(serializable, fh, ensure_ascii=False, indent=2)
pe_log = pruef_ergebnis(
ok=True,
meldung=f"Log geschrieben: {os.path.basename(log_path)}",
aktion="standarddatei_vorschlagen",
kontext={"log_path": log_path},
)
self.pruefmanager.verarbeite(pe_log)
return log_path
# ------------------------------------------------------------------ #
# Hilfsfunktionen intern
# ------------------------------------------------------------------ #
def _write_layer_to_gpkg(
self,
layer_name: str,
layer: Any,
) -> Optional[str]:
"""
Schreibt einen QgsVectorLayer in die Ziel-GPKG.
Voraussetzungen:
- self.gpkg_path ist ein str
- layer ist ein gültiger QgsVectorLayer
"""
if layer is None or not hasattr(layer, "isValid") or not layer.isValid():
return "Ungültiger Layer zum Schreiben übergeben"
try:
opts = qgiscore.QgsVectorFileWriter.SaveVectorOptions()
opts.driverName = "GPKG"
opts.layerName = layer_name
opts.fileEncoding = "UTF-8"
# Style in der GPKG speichern, wenn möglich
if hasattr(opts, "symbologyExport"):
try:
# QGIS: SymbologyExport-Wert z.B. QgsVectorFileWriter.SaveVectorOptions.Symbology
saveOpts = qgiscore.QgsVectorFileWriter.SaveVectorOptions
sym_val = getattr(saveOpts, "Symbology", None)
if sym_val is None:
sym_val = getattr(saveOpts, "SymbologyExport", None)
if sym_val is not None:
opts.symbologyExport = sym_val
except Exception:
pass
# Datei existiert → Layer überschreiben
# Datei existiert nicht → neue GPKG anlegen
if not os.path.exists(self.gpkg_path):
opts.actionOnExistingFile = qgiscore.QgsVectorFileWriter.CreateOrOverwriteFile
else:
opts.actionOnExistingFile = qgiscore.QgsVectorFileWriter.CreateOrOverwriteLayer
err = qgiscore.QgsVectorFileWriter.writeAsVectorFormatV3(
layer,
self.gpkg_path,
qgiscore.QgsProject.instance().transformContext(),
opts,
)
# QGIS ≥3 liefert ein Tupel: (error_code, error_message, new_filename, new_layer_name)
if isinstance(err, tuple):
error_code = err[0]
error_msg = err[1] if len(err) > 1 else ""
else:
error_code = err
error_msg = ""
if error_code != qgiscore.QgsVectorFileWriter.NoError:
return f"Fehler beim Schreiben (Code {error_code}, msg='{error_msg}')"
return None
except Exception as exc:
return str(exc)

395
modules/LayerLoader.py Normal file
View File

@@ -0,0 +1,395 @@
"""sn_basis/modules/LayerLoader.py
Kapselt Layer-Erstellung, Raumfilter und Stil-Logik.
"""
from __future__ import annotations
from typing import Any, Dict, List, Optional
import time
from sn_basis.functions.os_wrapper import normalize_path, is_absolute_path
from sn_basis.functions.qgiscore_wrapper import (
QgsVectorLayer,
QgsRasterLayer,
QgsFeatureRequest,
QgsProject,
QgsNetworkAccessManager,
QgsCoordinateTransform,
)
from sn_basis.functions.sys_wrapper import get_plugin_root, join_path, file_exists
from sn_basis.modules.stilpruefer import Stilpruefer
from sn_basis.modules.layerpruefer import Layerpruefer
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
from sn_basis.functions import qt_wrapper as qt
class LayerLoader:
"""Lädt und filtert Layer aus Dienst-/Datenquellen."""
def __init__(
self,
pruefmanager: Any,
stil_pruefer: Optional[Stilpruefer] = None,
layer_pruefer: Optional[Layerpruefer] = None,
) -> None:
self.pruefmanager = pruefmanager
self.stil_pruefer = stil_pruefer or Stilpruefer()
self.layer_pruefer = layer_pruefer or Layerpruefer()
_LAYER_TIMEOUT_MS = 30_000 # 30 Sekunden
def _was_canceled(self, cancel_callback: Optional[Any]) -> bool:
if not callable(cancel_callback):
return False
try:
return bool(cancel_callback())
except Exception:
return False
def _process_events(self) -> None:
try:
if hasattr(qt, "QCoreApplication") and hasattr(qt.QCoreApplication, "processEvents"):
qt.QCoreApplication.processEvents()
except Exception:
pass
def _transform_geometry_to_layer_crs(self, geometry: Any, source_layer: Any, target_layer: Any) -> Any:
if geometry is None or source_layer is None or target_layer is None:
return geometry
if QgsCoordinateTransform is None or QgsProject is None:
return geometry
try:
source_crs = source_layer.crs() if hasattr(source_layer, "crs") else None
target_crs = target_layer.crs() if hasattr(target_layer, "crs") else None
if source_crs is None or target_crs is None:
return geometry
source_authid = source_crs.authid() if hasattr(source_crs, "authid") else None
target_authid = target_crs.authid() if hasattr(target_crs, "authid") else None
if source_authid and target_authid and source_authid == target_authid:
return geometry
ct = QgsCoordinateTransform(source_crs, target_crs, QgsProject.instance())
if hasattr(geometry, "clone") and callable(getattr(geometry, "clone")):
geom_copy = geometry.clone()
else:
geom_copy = geometry
geom_copy.transform(ct)
return geom_copy
except Exception:
return geometry
def _transform_extent_to_layer_crs(self, extent: Any, source_layer: Any, target_layer: Any) -> Any:
if extent is None or source_layer is None or target_layer is None:
return extent
if QgsCoordinateTransform is None or QgsProject is None:
return extent
try:
source_crs = source_layer.crs() if hasattr(source_layer, "crs") else None
target_crs = target_layer.crs() if hasattr(target_layer, "crs") else None
if source_crs is None or target_crs is None:
return extent
source_authid = source_crs.authid() if hasattr(source_crs, "authid") else None
target_authid = target_crs.authid() if hasattr(target_crs, "authid") else None
if source_authid and target_authid and source_authid == target_authid:
return extent
ct = QgsCoordinateTransform(source_crs, target_crs, QgsProject.instance())
if hasattr(ct, "transformBoundingBox"):
return ct.transformBoundingBox(extent)
return extent
except Exception:
return extent
def create_layer(self, provider: str, link: str, thema: str) -> Optional[QgsVectorLayer]:
provider_lower = provider.lower() if provider else ""
layer = None
# Netzwerk-Timeout für alle netzwerkbasierten Provider setzen
if provider_lower in ("wfs", "wms", "rest"):
try:
nam = QgsNetworkAccessManager.instance()
if hasattr(nam, "setTimeout"):
nam.setTimeout(self._LAYER_TIMEOUT_MS)
except Exception:
pass
try:
if provider_lower == "wfs":
uri = link if link.strip().lower().startswith("url=") else f"url={link}"
layer = QgsVectorLayer(uri, thema, "WFS")
elif provider_lower == "wms":
uri = link if link.strip().lower().startswith("url=") else f"url={link}"
layer = QgsRasterLayer(uri, thema, "wms")
elif provider_lower in ("ogr", "gpkg", "shp", "geojson"):
layer = QgsVectorLayer(link, thema, "ogr")
elif provider_lower == "rest":
rest_link = link.strip()
if rest_link.lower().endswith("/featureserver"):
rest_link = rest_link.rstrip("/") + "/0"
uri = rest_link if rest_link.lower().startswith("url=") else f"url={rest_link}"
layer = QgsVectorLayer(uri, thema, "arcgisfeatureserver")
else:
layer = QgsVectorLayer(link, thema, "ogr")
except Exception as exc:
self.pruefmanager.verarbeite(
pruef_ergebnis(
ok=False,
meldung=f"Fehler beim Erstellen des Layers {thema}: {exc}",
aktion="layer_nicht_verfuegbar",
kontext={"provider": provider, "link": link},
)
)
return None
if not layer or not layer.isValid():
self.pruefmanager.verarbeite(
pruef_ergebnis(
ok=False,
meldung=f"Layer {thema} (Provider={provider}) konnte nicht geladen werden."
,aktion="layer_nicht_verfuegbar",
kontext={"provider": provider, "link": link},
)
)
return None
return layer
def apply_style(self, layer: QgsVectorLayer, style_path: Optional[str]) -> None:
if not style_path or layer is None or not layer.isValid():
return
if not style_path.strip():
return
if not is_absolute_path(style_path):
plugin_root = get_plugin_root()
style_path = str(join_path(plugin_root, "sn_plan41", "assets", style_path))
# normalize path for consistency
style_path = str(normalize_path(style_path))
# Debug: welche Stil-Datei wird geprüft?
print(f"[LayerLoader] Überprüfe Stildatei: '{style_path}'")
if file_exists(style_path):
try:
layer.loadNamedStyle(style_path)
layer.triggerRepaint()
except Exception as exc:
self.pruefmanager.verarbeite(
pruef_ergebnis(
ok=False,
meldung=f"Fehler beim Stil-Laden für {layer.name()}: {exc}",
aktion="stil_laden_fehlgeschlagen",
kontext={"thema": layer.name(), "style_path": style_path},
)
)
else:
self.pruefmanager.verarbeite(
pruef_ergebnis(
ok=True,
meldung=f"Stildatei nicht gefunden (optional): {style_path}",
aktion="stil_nicht_gefunden",
kontext={"thema": layer.name(), "style_path": style_path},
)
)
def filter_by_extent(self, layer: QgsVectorLayer, extent, cancel_callback: Optional[Any] = None, source_layer: Optional[Any] = None) -> Optional[QgsVectorLayer]:
"""Beschneidet <layer> auf die rechteckige Ausdehnung <extent>.
Diese Methode verwendet einen einfachen BBOX-Filter. Für komplexere
Raumeinschränkungen (z.B. Verfahrensgebiet) sollte stattdessen
:meth:`filter_by_layer` verwendet werden, da dort echte Geometrie-Tests
stattfinden.
"""
if not layer or not layer.isValid() or extent is None:
return layer
if layer.type() != QgsVectorLayer.VectorLayer:
return layer
extent_for_layer = self._transform_extent_to_layer_crs(extent, source_layer, layer)
request = QgsFeatureRequest().setFilterRect(extent_for_layer)
if hasattr(request, "setTimeout"):
try:
request.setTimeout(self._LAYER_TIMEOUT_MS)
except Exception:
pass
start = time.monotonic()
features: List[Any] = []
try:
for feat in layer.getFeatures(request):
if self._was_canceled(cancel_callback):
self.pruefmanager.verarbeite(
pruef_ergebnis(
ok=False,
meldung=f"Abbruch beim Raumfilter (BBOX) für {layer.name()}",
aktion="needs_user_action",
kontext={"thema": layer.name()},
)
)
return None
elapsed_ms = int((time.monotonic() - start) * 1000)
if elapsed_ms >= self._LAYER_TIMEOUT_MS:
self.pruefmanager.verarbeite(
pruef_ergebnis(
ok=False,
meldung=f"Timeout beim Raumfilter (BBOX) für {layer.name()} nach {self._LAYER_TIMEOUT_MS // 1000}s",
aktion="url_nicht_erreichbar",
kontext={"thema": layer.name(), "timeout_s": self._LAYER_TIMEOUT_MS // 1000},
)
)
return None
features.append(feat)
if len(features) % 100 == 0:
self._process_events()
except Exception as exc:
self.pruefmanager.verarbeite(
pruef_ergebnis(
ok=False,
meldung=f"Fehler beim Lesen der Features für {layer.name()}: {exc}",
aktion="layer_nicht_verfuegbar",
kontext={"thema": layer.name()},
)
)
return None
if not features:
return None
geom_type_map = {0: "Point", 1: "LineString", 2: "Polygon"}
geom_type = geom_type_map.get(layer.geometryType(), "Polygon")
uri = f"{geom_type}?crs={layer.crs().authid()}"
filtered_layer = QgsVectorLayer(uri, f"{layer.name()}_bbox", "memory")
if not filtered_layer or not filtered_layer.isValid():
self.pruefmanager.verarbeite(
pruef_ergebnis(
ok=False,
meldung=f"Fehler beim Erzeugen des Filter-Layers für {layer.name()}",
aktion="filterlayer_nicht_erzeugt",
kontext={"thema": layer.name()},
)
)
return None
provider = filtered_layer.dataProvider()
provider.addAttributes(layer.fields())
filtered_layer.updateFields()
provider.addFeatures(features)
filtered_layer.updateExtents()
return filtered_layer
def filter_by_layer(self, layer: QgsVectorLayer, filter_layer: QgsVectorLayer, cancel_callback: Optional[Any] = None) -> Optional[QgsVectorLayer]:
"""Beschneidet <layer> auf die tatsächliche Geometrie des
<filter_layer>.
Diese Methode wird z.B. für das Verfahrensgebiet verwendet, damit nicht
die gesamte Bounding-Box, sondern nur die echten Flächen als Raumfilter
gelten. Wenn der Filter-Layer mehrere Features enthält, werden deren
Geometrien zu einem Multi-Geom vereinigt.
"""
if not layer or not layer.isValid() or not filter_layer or not filter_layer.isValid():
return layer
if layer.type() != QgsVectorLayer.VectorLayer:
return layer
# vereinigte Geometrie aller Features im Filter-Layer
union_geom = None
for f in filter_layer.getFeatures():
try:
geom = self._transform_geometry_to_layer_crs(f.geometry(), filter_layer, layer)
if union_geom is None:
union_geom = geom
else:
union_geom = union_geom.combine(geom)
except Exception:
# bei einem Fehler einfach weiterfahren
continue
if union_geom is None or union_geom.isEmpty():
return None
# nun alle Features aus <layer> nehmen, deren Geometrie sich schneidet
filtered = []
request = QgsFeatureRequest().setFilterRect(union_geom.boundingBox())
if hasattr(request, "setTimeout"):
try:
request.setTimeout(self._LAYER_TIMEOUT_MS)
except Exception:
pass
start = time.monotonic()
for f in layer.getFeatures(request):
if self._was_canceled(cancel_callback):
self.pruefmanager.verarbeite(
pruef_ergebnis(
ok=False,
meldung=f"Abbruch beim Raumfilter (Geometrie) für {layer.name()}",
aktion="needs_user_action",
kontext={"thema": layer.name()},
)
)
return None
elapsed_ms = int((time.monotonic() - start) * 1000)
if elapsed_ms >= self._LAYER_TIMEOUT_MS:
self.pruefmanager.verarbeite(
pruef_ergebnis(
ok=False,
meldung=f"Timeout beim Raumfilter (Geometrie) für {layer.name()} nach {self._LAYER_TIMEOUT_MS // 1000}s",
aktion="url_nicht_erreichbar",
kontext={"thema": layer.name(), "timeout_s": self._LAYER_TIMEOUT_MS // 1000},
)
)
return None
try:
if f.geometry() and f.geometry().intersects(union_geom):
filtered.append(f)
except Exception:
continue
if len(filtered) % 100 == 0:
self._process_events()
if not filtered:
return None
geom_type_map = {0: "Point", 1: "LineString", 2: "Polygon"}
geom_type = geom_type_map.get(layer.geometryType(), "Polygon")
uri = f"{geom_type}?crs={layer.crs().authid()}"
filtered_layer = QgsVectorLayer(uri, f"{layer.name()}_filtered", "memory")
if not filtered_layer or not filtered_layer.isValid():
self.pruefmanager.verarbeite(
pruef_ergebnis(
ok=False,
meldung=f"Fehler beim Erzeugen des Filter-Layers für {layer.name()}",
aktion="filterlayer_nicht_erzeugt",
kontext={"thema": layer.name()},
)
)
return None
provider = filtered_layer.dataProvider()
provider.addAttributes(layer.fields())
filtered_layer.updateFields()
provider.addFeatures(filtered)
filtered_layer.updateExtents()
return filtered_layer
def add_to_project(self, layer: QgsVectorLayer) -> None:
if layer and layer.isValid():
QgsProject.instance().addMapLayer(layer)

262
modules/Pruefmanager.py Normal file
View File

@@ -0,0 +1,262 @@
"""
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.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:
self.ui_modus = ui_modus
self.parent = parent
# ------------------------------------------------------------------
# Meldungen / Zusammenfassungen
# ------------------------------------------------------------------
def report_error(
self,
thema: str,
meldung: str,
*,
aktion: Optional[PruefAktion] = None,
kontext: Optional[Any] = None,
) -> None:
critical_actions = {
"netzwerkfehler", "pruefe_exception", "save_exception",
"layer_create_failed", "read_error", "open_error",
}
warn_actions = {
"datei_nicht_gefunden", "pfad_nicht_gefunden", "url_nicht_erreichbar",
"falsche_endung", "kein_header", "kein_arbeitsblatt",
}
if aktion in critical_actions:
error(thema, meldung)
return
if aktion in warn_actions:
warning(thema, meldung)
return
warning(thema, meldung)
def report_summary(self, summary: dict) -> None:
geladen = summary.get("geladen", [])
fehler = summary.get("fehler", {})
ausserhalb = summary.get("ausserhalb", [])
relevant = summary.get("relevant", [])
message = (
f"Geladene Dienste: {len(geladen)}\n"
f"Relevante Dienste: {len(relevant)}\n"
f"Dienste ausserhalb: {len(ausserhalb)}\n"
f"Fehler: {len(fehler)}"
)
info("DataGrabber Zusammenfassung", message)
# ------------------------------------------------------------------
# Allgemeine Nutzerinteraktionen
# ------------------------------------------------------------------
def zeige_hinweis(self, titel: str, meldung: str) -> None:
"""Zeigt eine modale Hinweismeldung mit OK-Button."""
from sn_basis.functions.dialog_wrapper import show_info_dialog
show_info_dialog(titel, meldung, parent=self.parent)
def frage_ja_nein(self, titel: str, meldung: str, default: bool = True) -> bool:
"""Stellt eine Ja/Nein-Frage. Gibt True zurück, wenn der Nutzer Ja wählt."""
if self.ui_modus != "qgis":
return default
return ask_yes_no(titel, meldung, default=default, parent=self.parent)
def frage_text(self, titel: str, meldung: str, default_text: str = "") -> tuple[str, bool]:
"""Fragt einen Textwert ab und gibt Text + OK-Status zurück."""
from sn_basis.functions.dialog_wrapper import ask_text
if self.ui_modus != "qgis":
return default_text, True
return ask_text(titel, meldung, default_text=default_text, parent=self.parent)
# ------------------------------------------------------------------
# VERFAHRENS-DB-spezifische Entscheidungen
# ------------------------------------------------------------------
def _handle_datei_existiert(self, ergebnis: pruef_ergebnis) -> pruef_ergebnis:
"""Handhabt das Szenario, dass die Ziel-Verfahrens-DB bereits existiert.
Zeigt einen einzigen Dialog mit drei Optionen an:
- **Überschreiben**: Bestehende Layer ersetzen (entspricht YES)
- **Anhängen**: Neue Layer zur Datei hinzufügen (entspricht NO)
- **Abbrechen**: Vorgang beenden (entspricht CANCEL)
Parameters
----------
ergebnis : pruef_ergebnis
Eingabe-Ergebnis mit Dateipfad im ``kontext``-Attribut.
Returns
-------
pruef_ergebnis
Ergebnis mit Aktion:
- ``datei_existiert_ueberschreiben``
- ``datei_existiert_anhaengen``
- ``datei_existiert_ueberspringen`` (für Cancel-Fall)
"""
if self.ui_modus != "qgis":
return ergebnis
pfad = ergebnis.kontext
pfad_str = str(pfad) if pfad else "unbekannt"
titel = "Verfahrens-DB existiert bereits"
meldung = (
f"Die Datei '{pfad_str}' existiert bereits.\n\n"
"Was soll geschehen?\n\n"
"• **Überschreiben**: Bestehende Layer ersetzen\n"
"• **Anhängen**: Neue Layer hinzufügen\n"
"• **Abbrechen**: Vorgang beenden"
)
# Einzelner Dialog mit drei Optionen
entscheidung = ask_overwrite_append_cancel_custom(
parent=self.parent,
title=titel,
message=meldung
)
if entscheidung == "overwrite":
return pruef_ergebnis(
ok=True,
aktion="datei_existiert_ueberschreiben",
kontext=ergebnis.kontext,
)
elif entscheidung == "append":
return pruef_ergebnis(
ok=True,
aktion="datei_existiert_anhaengen",
kontext=ergebnis.kontext,
)
else: # cancel
return pruef_ergebnis(
ok=True,
aktion="datei_existiert_ueberspringen",
kontext=ergebnis.kontext,
)
# ------------------------------------------------------------------
# Basis-Entscheidungen (KORREKT: → pruef_ergebnis)
# ------------------------------------------------------------------
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 = {
"leereingabe_erlaubt": "Ohne Eingabe fortfahren",
"standarddatei_vorschlagen": "Standarddatei verwenden",
"temporaer_erlaubt": "Temporäre Layer erzeugen",
"layer_unsichtbar": "Layer einblenden",
}
title = title_map.get(ergebnis.aktion, "Entscheidung erforderlich")
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}")
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",
kontext=ergebnis.kontext
)
print("DEBUG: Andere Aktion bestätigt → ok=True, aktion='ok'")
return pruef_ergebnis(
ok=True,
aktion="ok",
kontext=ergebnis.kontext
)
# ------------------------------------------------------------------
# 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. 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
# 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 _ask_use_or_replace_pufferlayer(self) -> str:
"""
Fragt den Nutzer, ob ein vorhandener Pufferlayer verwendet
oder ersetzt werden soll.
Returns
-------
str
"verwenden", "ersetzen" oder "abbrechen"
"""
ergebnis = pruef_ergebnis(
ok=False,
aktion="layer_existiert",
meldung="Ein Pufferlayer ist bereits vorhanden.",
)
ergebnis = self.pruefmanager.verarbeite(ergebnis)
if not ergebnis.ok:
return "abbrechen"
return "verwenden" if ergebnis.aktion == "ok" else "ersetzen"

0
modules/__init__py Normal file
View File

91
modules/excel_importer.py Normal file
View File

@@ -0,0 +1,91 @@
# sn_plan41/modules/excel_importer.py
import os
from typing import Optional, Iterable, Mapping, Any, List, cast
from openpyxl import load_workbook
from openpyxl.workbook.workbook import Workbook
from openpyxl.worksheet.worksheet import Worksheet
from sn_basis.modules.Dateipruefer import Dateipruefer
from sn_basis.modules.Pruefmanager import Pruefmanager
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
class ExcelImporter:
"""
Excel-Importer für Linklisten, verwendet Dateipruefer und Pruefmanager zur Meldungsbehandlung.
- Der Aufrufer übergibt einen konkreten Dateipfad.
- Vor dem Öffnen wird der Pfad mit Dateipruefer geprüft.
- Link- und Stilprüfungen erfolgen nicht hier, sondern im DataGrabber.
- Nach dem Ladevorgang wird die Arbeitsmappe geschlossen, damit die Datei vom OS freigegeben wird.
"""
def __init__(self, filepath: str, pruefmanager: Pruefmanager):
if not filepath:
raise ValueError("ExcelImporter benötigt einen gültigen Dateipfad.")
if pruefmanager is None:
raise ValueError("ExcelImporter benötigt einen Pruefmanager.")
self.filepath = filepath
self.pruefmanager = pruefmanager
def import_xlsx(self) -> List[Mapping[str, Any]]:
"""
Liest die Excel-Datei und gibt eine Liste von Dicts (Zeilen) zurück.
Bei Prüf- oder Leseproblemen wird der Pruefmanager zur Verarbeitung des pruef_ergebnis aufgerufen.
Im Fehlerfall wird eine leere Liste zurückgegeben.
"""
# 1) Dateiprüfung über Dateipruefer
datei_pruefer = Dateipruefer(pfad=self.filepath, temporaer_erlaubt=False)
ergebnis: pruef_ergebnis = datei_pruefer.pruefe()
ergebnis = self.pruefmanager.verarbeite(ergebnis)
if not ergebnis.ok:
return []
workbook: Optional[Workbook] = None
try:
workbook = load_workbook(filename=self.filepath, data_only=True)
# workbook.active kann typmäßig als Optional angesehen werden; cast/prüfen, damit Pylance weiß, dass sheet ein Worksheet ist
sheet = workbook.active
if sheet is None:
pe = pruef_ergebnis(ok=False, meldung=f"Kein aktives Blatt in der Arbeitsmappe: {self.filepath}", aktion="kein_arbeitsblatt", kontext=self.filepath)
self.pruefmanager.verarbeite(pe)
return []
# Typengranularität für den Linter
sheet = cast(Worksheet, sheet)
# Header aus erster Zeile (als Werte)
header_row = next(sheet.iter_rows(min_row=1, max_row=1, values_only=True), None)
if not header_row:
pe = pruef_ergebnis(ok=False, meldung=f"Excel-Datei enthält keine Header-Zeile: {self.filepath}", aktion="kein_header", kontext=self.filepath)
self.pruefmanager.verarbeite(pe)
return []
header = list(header_row)
if not header or all(h is None for h in header):
pe = pruef_ergebnis(ok=False, meldung=f"Excel-Header ist leer oder ungültig: {self.filepath}", aktion="kein_header", kontext=self.filepath)
self.pruefmanager.verarbeite(pe)
return []
ergebnis_list: List[Mapping[str, Any]] = []
# Werte-only lesen für Performance und Einfachheit
for row in sheet.iter_rows(min_row=2, values_only=True):
if row is None:
continue
# zip stoppt bei kürzerer Länge; das ist beabsichtigt
attributes = dict(zip(header, row))
ergebnis_list.append(attributes)
return ergebnis_list
except Exception as exc:
pe = pruef_ergebnis(ok=False, meldung=f"Fehler beim Lesen der Excel-Datei '{self.filepath}': {exc}", aktion="read_error", kontext=self.filepath)
self.pruefmanager.verarbeite(pe)
return []
finally:
if workbook is not None:
workbook.close()

182
modules/layerpruefer.py Normal file
View File

@@ -0,0 +1,182 @@
"""
sn_basis/modules/layerpruefer.py Prüfung von QGIS-Layern.
Verwendet ausschließlich Wrapper und gibt pruef_ergebnis zurück.
"""
from typing import Optional, Any
from sn_basis.functions import (
layer_exists,
get_layer_geometry_type,
get_layer_feature_count,
is_layer_visible,
get_layer_type,
get_layer_crs,
get_layer_fields,
get_layer_source,
is_layer_editable,
)
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis, PruefAktion
class Layerpruefer:
"""
Prüft Layer auf Existenz, Sichtbarkeit, Geometrietyp, Objektanzahl,
Layertyp, CRS, Felder, Datenquelle und Editierbarkeit.
"""
def __init__(
self,
layer:Optional[Any]=None,
erwarteter_geotyp: str | None = None,
muss_sichtbar_sein: bool = False,
erwarteter_layertyp: str | None = None,
erwartetes_crs: str | None = None,
erforderliche_felder: list[str] | None = None,
erlaubte_datenquellen: list[str] | None = None,
muss_editierbar_sein: bool = False,
):
self.layer = layer
self.erwarteter_geotyp = erwarteter_geotyp
self.muss_sichtbar_sein = muss_sichtbar_sein
self.erwarteter_layertyp = erwarteter_layertyp
self.erwartetes_crs = erwartetes_crs
self.erforderliche_felder = erforderliche_felder or []
self.erlaubte_datenquellen = erlaubte_datenquellen or []
self.muss_editierbar_sein = muss_editierbar_sein
# ---------------------------------------------------------
# Hauptfunktion
# ---------------------------------------------------------
def pruefe(self) -> pruef_ergebnis:
# -----------------------------------------------------
# 1. Existenz
# -----------------------------------------------------
if not layer_exists(self.layer):
return pruef_ergebnis(
ok=False,
meldung="Der Layer existiert nicht oder wurde nicht geladen.",
aktion="layer_nicht_gefunden",
kontext=None,
)
# -----------------------------------------------------
# 2. Sichtbarkeit
# -----------------------------------------------------
sichtbar = is_layer_visible(self.layer)
if self.muss_sichtbar_sein and not sichtbar:
return pruef_ergebnis(
ok=False,
meldung="Der Layer ist unsichtbar. Soll er eingeblendet werden?",
aktion="layer_unsichtbar",
kontext=self.layer, # Layerobjekt als Kontext
)
# -----------------------------------------------------
# 3. Layertyp
# -----------------------------------------------------
layertyp = get_layer_type(self.layer)
if self.erwarteter_layertyp and layertyp != self.erwarteter_layertyp:
return pruef_ergebnis(
ok=False,
meldung=(
f"Der Layer hat den Typ '{layertyp}', "
f"erwartet wurde '{self.erwarteter_layertyp}'."
),
aktion="falscher_layertyp",
kontext=None,
)
# -----------------------------------------------------
# 4. Geometrietyp
# -----------------------------------------------------
geotyp = get_layer_geometry_type(self.layer)
if self.erwarteter_geotyp and geotyp != self.erwarteter_geotyp:
return pruef_ergebnis(
ok=False,
meldung=(
f"Der Layer hat den Geometrietyp '{geotyp}', "
f"erwartet wurde '{self.erwarteter_geotyp}'."
),
aktion="falscher_geotyp",
kontext=None,
)
# -----------------------------------------------------
# 5. Featureanzahl
# -----------------------------------------------------
anzahl = get_layer_feature_count(self.layer)
if anzahl == 0:
return pruef_ergebnis(
ok=False,
meldung="Der Layer enthält keine Objekte.",
aktion="layer_leer",
kontext=None,
)
# -----------------------------------------------------
# 6. CRS
# -----------------------------------------------------
crs = get_layer_crs(self.layer)
if self.erwartetes_crs and crs != self.erwartetes_crs:
return pruef_ergebnis(
ok=False,
meldung=(
f"Der Layer hat das CRS '{crs}', "
f"erwartet wurde '{self.erwartetes_crs}'."
),
aktion="falsches_crs",
kontext=None,
)
# -----------------------------------------------------
# 7. Felder
# -----------------------------------------------------
felder = get_layer_fields(self.layer)
fehlende = [f for f in self.erforderliche_felder if f not in felder]
if fehlende:
return pruef_ergebnis(
ok=False,
meldung=(
"Der Layer enthält nicht alle erforderlichen Felder: "
+ ", ".join(fehlende)
),
aktion="felder_fehlen",
kontext=None,
)
# -----------------------------------------------------
# 8. Datenquelle
# -----------------------------------------------------
quelle = get_layer_source(self.layer)
if self.erlaubte_datenquellen and quelle not in self.erlaubte_datenquellen:
return pruef_ergebnis(
ok=False,
meldung=f"Die Datenquelle '{quelle}' ist nicht erlaubt.",
aktion="datenquelle_unerwartet",
kontext=None,
)
# -----------------------------------------------------
# 9. Editierbarkeit
# -----------------------------------------------------
editable = is_layer_editable(self.layer)
if self.muss_editierbar_sein and not editable:
return pruef_ergebnis(
ok=False,
meldung="Der Layer ist nicht editierbar.",
aktion="layer_nicht_editierbar",
kontext=None,
)
# -----------------------------------------------------
# 10. Alles OK
# -----------------------------------------------------
return pruef_ergebnis(
ok=True,
meldung="Layerprüfung erfolgreich.",
aktion="ok",
kontext=None,
)

135
modules/linkpruefer.py Normal file
View File

@@ -0,0 +1,135 @@
"""
sn_basis/modules/linkpruefer.py Prüfung von URLs und lokalen Links.
Verwendet Wrapper und gibt pruef_ergebnis an den Pruefmanager zurück.
"""
from pathlib import Path
from sn_basis.functions import (
file_exists,
join_path,
network_head,
)
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis, PruefAktion
class Linkpruefer:
"""
Prüft URLs und lokale Pfade.
Die eigentliche Nutzerinteraktion übernimmt der Pruefmanager.
"""
def __init__(self, basis_pfad: str | None = None):
"""
basis_pfad: optionaler Basisordner für relative Pfade.
"""
self.basis = basis_pfad
# ---------------------------------------------------------
# Hilfsfunktionen
# ---------------------------------------------------------
def _pfad(self, relativer_pfad: str) -> Path:
"""
Erzeugt einen OS-unabhängigen Pfad relativ zum Basisverzeichnis.
"""
if not self.basis:
return Path(relativer_pfad)
return join_path(self.basis, relativer_pfad)
def _ist_url(self, text: str) -> bool:
"""
Einfache URL-Erkennung.
"""
return text.startswith("http://") or text.startswith("https://")
# ---------------------------------------------------------
# Hauptfunktion
# ---------------------------------------------------------
def pruefe(self, eingabe: str) -> pruef_ergebnis:
"""
Prüft einen Link (URL oder lokalen Pfad).
Rückgabe: pruef_ergebnis
"""
if not eingabe:
return pruef_ergebnis(
ok=False,
meldung="Es wurde kein Link angegeben.",
aktion="leer",
kontext=None,
)
#evtl. Pfad-Objekte in string umwandeln
eingabe = str(eingabe)
# -----------------------------------------------------
# 1. Fall: URL
# -----------------------------------------------------
if self._ist_url(eingabe):
return self._pruefe_url(eingabe)
# -----------------------------------------------------
# 2. Fall: lokaler Pfad
# -----------------------------------------------------
return self._pruefe_dateipfad(eingabe)
# ---------------------------------------------------------
# URLPrüfung
# ---------------------------------------------------------
def _pruefe_url(self, url: str) -> pruef_ergebnis:
"""
Prüft eine URL über einen HEAD-Request.
"""
reply = network_head(url)
if reply is None:
return pruef_ergebnis(
ok=False,
meldung=f"Die URL '{url}' konnte nicht geprüft werden.",
aktion="netzwerkfehler",
kontext=url,
)
if reply.error != 0:
return pruef_ergebnis(
ok=False,
meldung=f"Die URL '{url}' ist nicht erreichbar.",
aktion="url_nicht_erreichbar",
kontext=url,
)
return pruef_ergebnis(
ok=True,
meldung="URL ist erreichbar.",
aktion="ok",
kontext=url,
)
# ---------------------------------------------------------
# Lokale Datei/Pfadprüfung
# ---------------------------------------------------------
def _pruefe_dateipfad(self, eingabe: str) -> pruef_ergebnis:
"""
Prüft einen lokalen Pfad.
"""
pfad = self._pfad(eingabe)
if not file_exists(pfad):
return pruef_ergebnis(
ok=False,
meldung=f"Der Pfad '{eingabe}' wurde nicht gefunden.",
aktion="pfad_nicht_gefunden",
kontext=pfad,
)
return pruef_ergebnis(
ok=True,
meldung="Dateipfad ist gültig.",
aktion="ok",
kontext=pfad,
)

188
modules/pruef_ergebnis.py Normal file
View File

@@ -0,0 +1,188 @@
"""
sn_basis/modules/pruef_ergebnis.py
Erweitertes Ergebnisobjekt für Dateiprüfungen mit Verfahrens-DB-spezifischen Aktionen.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Optional, Literal
from pathlib import Path
# =============================================================================
# Erweiterte PruefAktionen für Verfahrens-DB-Workflow
# =============================================================================
PruefAktion = Literal[
# Basis-Aktionen (bestehend)
"ok",
"leer",
"leereingabe_erlaubt",
"leereingabe_nicht_erlaubt",
"standarddatei_vorschlagen",
"temporaer_erlaubt",
"temporaer_erzeugen",
"datei_nicht_gefunden",
"kein_dateipfad",
"pfad_nicht_gefunden",
"url_nicht_erreichbar",
"netzwerkfehler",
# Layer-spezifisch
"layer_nicht_gefunden",
"layer_unsichtbar",
"falscher_geotyp",
"layer_leer",
"falscher_layertyp",
"falsches_crs",
"felder_fehlen",
"datenquelle_unerwartet",
"layer_nicht_editierbar",
# Dateiendung/Format
"falsche_endung",
"pflichtfelder_fehlen",
"unbekannter_dateityp",
"Datenbank",
"dienst",
"excel",
"unbekannte_quelle",
# Excel/Import
"kein_header",
"kein_arbeitsblatt",
"read_error",
"open_error",
"datenabruf",
# 🆕 VERFAHRENS-DB SPEZIFISCH (deine Anforderungen 2.d, 2.e)
"datei_wird_erzeugt", # 2.d: Pfad gültig, Datei fehlt → weiter
"datei_existiert", # Datei vorhanden → Layer-Entscheidung
"datei_existiert_ueberschreiben", # 2.e: Nutzer wählt "Überschreiben"
"datei_existiert_anhaengen", # 2.e: Nutzer wählt "Anhängen"
"datei_existiert_ueberspringen", # 2.e: Nutzer wählt "Überspringen"
# Generisch
"pruefe_exception",
"save_exception",
"save_not_implemented",
"stil_not_implemented",
"datei_unbekannt",
"needs_user_action",
]
@dataclass
class pruef_ergebnis:
"""
Einheitliches Ergebnisobjekt für Prüfer im Verfahrens-DB-Workflow.
Attributes
----------
ok : bool
True wenn Prüfung bestanden und Pipeline fortgesetzt werden kann.
False signalisiert Fehler oder Nutzerentscheidung erforderlich.
meldung : Optional[str], optional
Menschenlesbare Meldung für UI-Dialoge (BY: Pruefmanager).
aktion : Optional[PruefAktion], optional
Maschinenlesbarer Aktionscode für nachfolgende Pipeline-Schritte.
kontext : Optional[Any], optional
Zusatzkontext: meist `pathlib.Path` für Dateipfade oder Layer-Objekte.
Verfahrens-DB-spezifische Aktionen:
+-----------------------------+-------------------------------------------------+
| Aktion | Bedeutung |
+=============================+=================================================+
| ``datei_wird_erzeugt`` | 2.d: Neues GPKG wird angelegt (Pfad gültig) |
+-----------------------------+-------------------------------------------------+
| ``datei_existiert`` | Datei vorhanden → Layer-Überschreibung prüfen |
+-----------------------------+-------------------------------------------------+
| ``datei_existiert_*`` | 2.e: Nutzerentscheidung für bestehende Datei |
+-----------------------------+-------------------------------------------------+
"""
ok: bool
meldung: Optional[str] = None
aktion: Optional[PruefAktion] = None
kontext: Optional[Any] = None
def __init__(
self,
ok: bool,
meldung: Optional[str] = None,
aktion: Optional[PruefAktion] = None,
kontext: Optional[Any] = None,
) -> None:
"""
Erstellt ein neues Prüfergebnis.
Parameters
----------
ok : bool
True für "weiter mit Pipeline", False für "Entscheidung/Fehler".
meldung : Optional[str]
UI-Text für Nutzerdialoge.
aktion : Optional[PruefAktion]
Maschinenaktion für nachfolgende Verarbeitung.
kontext : Optional[Any]
Typischerweise `pathlib.Path` (Dateipfad) oder `QgsVectorLayer`.
"""
self.ok = ok
self.meldung = meldung
self.aktion = aktion
self.kontext = kontext
@property
def ist_verfahrens_db_aktion(self) -> bool:
"""
Prüft, ob es sich um eine Verfahrens-DB-spezifische Aktion handelt.
Returns
-------
bool
True für ``datei_wird_erzeugt`` oder ``datei_existiert*``.
"""
return self.aktion in {
"datei_wird_erzeugt",
"datei_existiert",
"datei_existiert_ueberschreiben",
"datei_existiert_anhaengen",
"datei_existiert_ueberspringen",
}
@property
def dateipfad(self) -> Optional[Path]:
"""
Extrahiert den Dateipfad aus dem Kontext (falls vorhanden).
Returns
-------
Optional[Path]
`Path`-Objekt oder None.
"""
if isinstance(self.kontext, Path):
return self.kontext
return None
@property
def erlaubte_persistierung(self) -> bool:
"""
Prüft, ob die Pipeline Daten persistieren darf.
Returns
-------
bool
True für ``datei_wird_erzeugt``, ``datei_existiert_ueberschreiben``,
``datei_existiert_anhaengen``.
"""
return self.aktion in {
"datei_wird_erzeugt",
"datei_existiert_ueberschreiben",
"datei_existiert_anhaengen",
}

80
modules/stilpruefer.py Normal file
View File

@@ -0,0 +1,80 @@
"""
sn_basis/modules/stilpruefer.py Prüfung von Layerstilen.
Prüft ausschließlich, ob ein Stilpfad gültig ist.
Die Anwendung erfolgt später über eine Aktion.
"""
import os
from sn_basis.functions.os_wrapper import is_absolute_path
from sn_basis.functions.sys_wrapper import get_plugin_root, file_exists, join_path
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
class Stilpruefer:
"""
Prüft, ob ein Stilpfad gültig ist und angewendet werden kann.
Keine Seiteneffekte, keine QGIS-Aufrufe.
"""
def __init__(self):
pass
# ---------------------------------------------------------
# Hauptfunktion
# ---------------------------------------------------------
def pruefe(self, stil_pfad: str) -> pruef_ergebnis:
"""
Prüft einen Stilpfad.
Rückgabe: pruef_ergebnis
"""
# -----------------------------------------------------
# 1. Kein Stil angegeben → OK
# -----------------------------------------------------
if not stil_pfad:
return pruef_ergebnis(
ok=True,
meldung="Kein Stil angegeben.",
aktion="ok",
kontext=None,
)
pfad = str(stil_pfad)
if not is_absolute_path(pfad):
plugin_root = get_plugin_root()
pfad = str(join_path(plugin_root, "sn_plan41", "assets", pfad))
# -----------------------------------------------------
# 2. Datei existiert nicht
# -----------------------------------------------------
if not file_exists(pfad):
return pruef_ergebnis(
ok=False,
meldung=f"Die Stil-Datei '{stil_pfad}' wurde nicht gefunden.",
aktion="datei_nicht_gefunden",
kontext=pfad,
)
# -----------------------------------------------------
# 3. Falsche Endung
# -----------------------------------------------------
if os.path.splitext(pfad)[1].lower() != ".qml":
return pruef_ergebnis(
ok=False,
meldung="Die Stil-Datei muss die Endung '.qml' haben.",
aktion="falsche_endung",
kontext=pfad,
)
# -----------------------------------------------------
# 4. Stil ist gültig → Anwendung später
# -----------------------------------------------------
return pruef_ergebnis(
ok=True,
meldung="Stil-Datei ist gültig.",
aktion="stil_anwendbar",
kontext=pfad,
)

View File

@@ -0,0 +1,609 @@
<!DOCTYPE qgis PUBLIC 'http://mrcc.com/qgis.dtd' 'SYSTEM'>
<qgis hasScaleBasedVisibilityFlag="0" version="3.16.0-Hannover" minScale="100000000" labelsEnabled="0" readOnly="0" styleCategories="AllStyleCategories" simplifyLocal="1" maxScale="0" simplifyMaxScale="1" simplifyDrawingTol="1" simplifyDrawingHints="1" simplifyAlgorithm="0">
<flags>
<Identifiable>1</Identifiable>
<Removable>1</Removable>
<Searchable>1</Searchable>
</flags>
<temporal startExpression="" enabled="0" endExpression="" accumulate="0" durationField="" durationUnit="min" fixedDuration="0" startField="" mode="0" endField="">
<fixedRange>
<start></start>
<end></end>
</fixedRange>
</temporal>
<renderer-v2 symbollevels="0" forceraster="0" type="singleSymbol" enableorderby="0">
<symbols>
<symbol name="0" force_rhr="0" type="fill" alpha="1" clip_to_extent="1">
<layer pass="0" enabled="1" class="LinePatternFill" locked="0">
<prop k="angle" v="45"/>
<prop k="color" v="196,60,57,255"/>
<prop k="distance" v="10"/>
<prop k="distance_map_unit_scale" v="3x:0,0,0,0,0,0"/>
<prop k="distance_unit" v="Pixel"/>
<prop k="line_width" v="0.26"/>
<prop k="line_width_map_unit_scale" v="3x:0,0,0,0,0,0"/>
<prop k="line_width_unit" v="MM"/>
<prop k="offset" v="0"/>
<prop k="offset_map_unit_scale" v="3x:0,0,0,0,0,0"/>
<prop k="offset_unit" v="MM"/>
<prop k="outline_width_map_unit_scale" v="3x:0,0,0,0,0,0"/>
<prop k="outline_width_unit" v="MM"/>
<data_defined_properties>
<Option type="Map">
<Option name="name" type="QString" value=""/>
<Option name="properties"/>
<Option name="type" type="QString" value="collection"/>
</Option>
</data_defined_properties>
<symbol name="@0@0" force_rhr="0" type="line" alpha="1" clip_to_extent="1">
<layer pass="0" enabled="1" class="SimpleLine" locked="0">
<prop k="align_dash_pattern" v="0"/>
<prop k="capstyle" v="square"/>
<prop k="customdash" v="5;2"/>
<prop k="customdash_map_unit_scale" v="3x:0,0,0,0,0,0"/>
<prop k="customdash_unit" v="MM"/>
<prop k="dash_pattern_offset" v="0"/>
<prop k="dash_pattern_offset_map_unit_scale" v="3x:0,0,0,0,0,0"/>
<prop k="dash_pattern_offset_unit" v="MM"/>
<prop k="draw_inside_polygon" v="0"/>
<prop k="joinstyle" v="bevel"/>
<prop k="line_color" v="0,0,0,255"/>
<prop k="line_style" v="solid"/>
<prop k="line_width" v="1"/>
<prop k="line_width_unit" v="Pixel"/>
<prop k="offset" v="0"/>
<prop k="offset_map_unit_scale" v="3x:0,0,0,0,0,0"/>
<prop k="offset_unit" v="MM"/>
<prop k="ring_filter" v="0"/>
<prop k="tweak_dash_pattern_on_corners" v="0"/>
<prop k="use_custom_dash" v="0"/>
<prop k="width_map_unit_scale" v="3x:0,0,0,0,0,0"/>
<data_defined_properties>
<Option type="Map">
<Option name="name" type="QString" value=""/>
<Option name="properties"/>
<Option name="type" type="QString" value="collection"/>
</Option>
</data_defined_properties>
</layer>
</symbol>
</layer>
<layer pass="0" enabled="1" class="LinePatternFill" locked="0">
<prop k="angle" v="135"/>
<prop k="color" v="0,0,255,255"/>
<prop k="distance" v="10"/>
<prop k="distance_map_unit_scale" v="3x:0,0,0,0,0,0"/>
<prop k="distance_unit" v="Pixel"/>
<prop k="line_width" v="0.26"/>
<prop k="line_width_map_unit_scale" v="3x:0,0,0,0,0,0"/>
<prop k="line_width_unit" v="MM"/>
<prop k="offset" v="0"/>
<prop k="offset_map_unit_scale" v="3x:0,0,0,0,0,0"/>
<prop k="offset_unit" v="MM"/>
<prop k="outline_width_map_unit_scale" v="3x:0,0,0,0,0,0"/>
<prop k="outline_width_unit" v="MM"/>
<data_defined_properties>
<Option type="Map">
<Option name="name" type="QString" value=""/>
<Option name="properties"/>
<Option name="type" type="QString" value="collection"/>
</Option>
</data_defined_properties>
<symbol name="@0@1" force_rhr="0" type="line" alpha="1" clip_to_extent="1">
<layer pass="0" enabled="1" class="SimpleLine" locked="0">
<prop k="align_dash_pattern" v="0"/>
<prop k="capstyle" v="square"/>
<prop k="customdash" v="5;2"/>
<prop k="customdash_map_unit_scale" v="3x:0,0,0,0,0,0"/>
<prop k="customdash_unit" v="MM"/>
<prop k="dash_pattern_offset" v="0"/>
<prop k="dash_pattern_offset_map_unit_scale" v="3x:0,0,0,0,0,0"/>
<prop k="dash_pattern_offset_unit" v="MM"/>
<prop k="draw_inside_polygon" v="0"/>
<prop k="joinstyle" v="bevel"/>
<prop k="line_color" v="0,0,0,255"/>
<prop k="line_style" v="solid"/>
<prop k="line_width" v="1"/>
<prop k="line_width_unit" v="Pixel"/>
<prop k="offset" v="0"/>
<prop k="offset_map_unit_scale" v="3x:0,0,0,0,0,0"/>
<prop k="offset_unit" v="MM"/>
<prop k="ring_filter" v="0"/>
<prop k="tweak_dash_pattern_on_corners" v="0"/>
<prop k="use_custom_dash" v="0"/>
<prop k="width_map_unit_scale" v="3x:0,0,0,0,0,0"/>
<data_defined_properties>
<Option type="Map">
<Option name="name" type="QString" value=""/>
<Option name="properties"/>
<Option name="type" type="QString" value="collection"/>
</Option>
</data_defined_properties>
</layer>
</symbol>
</layer>
<layer pass="0" enabled="1" class="SimpleLine" locked="0">
<prop k="align_dash_pattern" v="0"/>
<prop k="capstyle" v="square"/>
<prop k="customdash" v="5;2"/>
<prop k="customdash_map_unit_scale" v="3x:0,0,0,0,0,0"/>
<prop k="customdash_unit" v="MM"/>
<prop k="dash_pattern_offset" v="0"/>
<prop k="dash_pattern_offset_map_unit_scale" v="3x:0,0,0,0,0,0"/>
<prop k="dash_pattern_offset_unit" v="MM"/>
<prop k="draw_inside_polygon" v="0"/>
<prop k="joinstyle" v="bevel"/>
<prop k="line_color" v="35,35,35,255"/>
<prop k="line_style" v="solid"/>
<prop k="line_width" v="1"/>
<prop k="line_width_unit" v="Pixel"/>
<prop k="offset" v="0"/>
<prop k="offset_map_unit_scale" v="3x:0,0,0,0,0,0"/>
<prop k="offset_unit" v="MM"/>
<prop k="ring_filter" v="0"/>
<prop k="tweak_dash_pattern_on_corners" v="0"/>
<prop k="use_custom_dash" v="0"/>
<prop k="width_map_unit_scale" v="3x:0,0,0,0,0,0"/>
<data_defined_properties>
<Option type="Map">
<Option name="name" type="QString" value=""/>
<Option name="properties"/>
<Option name="type" type="QString" value="collection"/>
</Option>
</data_defined_properties>
</layer>
</symbol>
</symbols>
<rotation/>
<sizescale/>
</renderer-v2>
<customproperties>
<property key="dualview/previewExpressions">
<value>"gml_id"</value>
</property>
<property key="embeddedWidgets/count" value="0"/>
<property key="variableNames"/>
<property key="variableValues"/>
</customproperties>
<blendMode>0</blendMode>
<featureBlendMode>0</featureBlendMode>
<layerOpacity>1</layerOpacity>
<SingleCategoryDiagramRenderer diagramType="Histogram" attributeLegend="1">
<DiagramCategory enabled="0" spacing="5" showAxis="1" direction="0" sizeType="MM" barWidth="5" height="15" minScaleDenominator="0" scaleBasedVisibility="0" spacingUnit="MM" maxScaleDenominator="1e+08" sizeScale="3x:0,0,0,0,0,0" diagramOrientation="Up" rotationOffset="270" penColor="#000000" penWidth="0" backgroundColor="#ffffff" penAlpha="255" width="15" spacingUnitScale="3x:0,0,0,0,0,0" backgroundAlpha="255" opacity="1" lineSizeType="MM" scaleDependency="Area" minimumSize="0" labelPlacementMethod="XHeight" lineSizeScale="3x:0,0,0,0,0,0">
<fontProperties description="MS Shell Dlg 2,8.25,-1,5,50,0,0,0,0,0" style=""/>
<axisSymbol>
<symbol name="" force_rhr="0" type="line" alpha="1" clip_to_extent="1">
<layer pass="0" enabled="1" class="SimpleLine" locked="0">
<prop k="align_dash_pattern" v="0"/>
<prop k="capstyle" v="square"/>
<prop k="customdash" v="5;2"/>
<prop k="customdash_map_unit_scale" v="3x:0,0,0,0,0,0"/>
<prop k="customdash_unit" v="MM"/>
<prop k="dash_pattern_offset" v="0"/>
<prop k="dash_pattern_offset_map_unit_scale" v="3x:0,0,0,0,0,0"/>
<prop k="dash_pattern_offset_unit" v="MM"/>
<prop k="draw_inside_polygon" v="0"/>
<prop k="joinstyle" v="bevel"/>
<prop k="line_color" v="35,35,35,255"/>
<prop k="line_style" v="solid"/>
<prop k="line_width" v="0.26"/>
<prop k="line_width_unit" v="MM"/>
<prop k="offset" v="0"/>
<prop k="offset_map_unit_scale" v="3x:0,0,0,0,0,0"/>
<prop k="offset_unit" v="MM"/>
<prop k="ring_filter" v="0"/>
<prop k="tweak_dash_pattern_on_corners" v="0"/>
<prop k="use_custom_dash" v="0"/>
<prop k="width_map_unit_scale" v="3x:0,0,0,0,0,0"/>
<data_defined_properties>
<Option type="Map">
<Option name="name" type="QString" value=""/>
<Option name="properties"/>
<Option name="type" type="QString" value="collection"/>
</Option>
</data_defined_properties>
</layer>
</symbol>
</axisSymbol>
</DiagramCategory>
</SingleCategoryDiagramRenderer>
<DiagramLayerSettings priority="0" obstacle="0" showAll="1" placement="1" dist="0" linePlacementFlags="18" zIndex="0">
<properties>
<Option type="Map">
<Option name="name" type="QString" value=""/>
<Option name="properties"/>
<Option name="type" type="QString" value="collection"/>
</Option>
</properties>
</DiagramLayerSettings>
<geometryOptions removeDuplicateNodes="0" geometryPrecision="0">
<activeChecks/>
<checkConfiguration type="Map">
<Option name="QgsGeometryGapCheck" type="Map">
<Option name="allowedGapsBuffer" type="double" value="0"/>
<Option name="allowedGapsEnabled" type="bool" value="false"/>
<Option name="allowedGapsLayer" type="QString" value=""/>
</Option>
</checkConfiguration>
</geometryOptions>
<legend type="default-vector"/>
<referencedLayers/>
<fieldConfiguration>
<field configurationFlags="None" name="gml_id">
<editWidget type="TextEdit">
<config>
<Option/>
</config>
</editWidget>
</field>
<field configurationFlags="None" name="objectid">
<editWidget type="TextEdit">
<config>
<Option/>
</config>
</editWidget>
</field>
<field configurationFlags="None" name="obj_nr">
<editWidget type="TextEdit">
<config>
<Option/>
</config>
</editWidget>
</field>
<field configurationFlags="None" name="typ">
<editWidget type="TextEdit">
<config>
<Option/>
</config>
</editWidget>
</field>
<field configurationFlags="None" name="text_1">
<editWidget type="TextEdit">
<config>
<Option/>
</config>
</editWidget>
</field>
<field configurationFlags="None" name="ext_objnr">
<editWidget type="TextEdit">
<config>
<Option/>
</config>
</editWidget>
</field>
<field configurationFlags="None" name="ext_hida_n">
<editWidget type="TextEdit">
<config>
<Option/>
</config>
</editWidget>
</field>
<field configurationFlags="None" name="ext_eigenn">
<editWidget type="TextEdit">
<config>
<Option/>
</config>
</editWidget>
</field>
<field configurationFlags="None" name="ext_kreis">
<editWidget type="TextEdit">
<config>
<Option/>
</config>
</editWidget>
</field>
<field configurationFlags="None" name="ext_gemein">
<editWidget type="TextEdit">
<config>
<Option/>
</config>
</editWidget>
</field>
<field configurationFlags="None" name="ext_ortste">
<editWidget type="TextEdit">
<config>
<Option/>
</config>
</editWidget>
</field>
<field configurationFlags="None" name="ext_strass">
<editWidget type="TextEdit">
<config>
<Option/>
</config>
</editWidget>
</field>
<field configurationFlags="None" name="ext_hausnu">
<editWidget type="TextEdit">
<config>
<Option/>
</config>
</editWidget>
</field>
<field configurationFlags="None" name="ext_gemark">
<editWidget type="TextEdit">
<config>
<Option/>
</config>
</editWidget>
</field>
<field configurationFlags="None" name="ext_flurst">
<editWidget type="TextEdit">
<config>
<Option/>
</config>
</editWidget>
</field>
<field configurationFlags="None" name="ext_kchar1">
<editWidget type="TextEdit">
<config>
<Option/>
</config>
</editWidget>
</field>
<field configurationFlags="None" name="ext_kchar2">
<editWidget type="TextEdit">
<config>
<Option/>
</config>
</editWidget>
</field>
<field configurationFlags="None" name="ext_kchar3">
<editWidget type="TextEdit">
<config>
<Option/>
</config>
</editWidget>
</field>
<field configurationFlags="None" name="ext_datier">
<editWidget type="TextEdit">
<config>
<Option/>
</config>
</editWidget>
</field>
<field configurationFlags="None" name="ext_typ">
<editWidget type="TextEdit">
<config>
<Option/>
</config>
</editWidget>
</field>
<field configurationFlags="None" name="gd_ueberar">
<editWidget type="TextEdit">
<config>
<Option/>
</config>
</editWidget>
</field>
<field configurationFlags="None" name="sort">
<editWidget type="TextEdit">
<config>
<Option/>
</config>
</editWidget>
</field>
<field configurationFlags="None" name="Stand">
<editWidget type="TextEdit">
<config>
<Option/>
</config>
</editWidget>
</field>
</fieldConfiguration>
<aliases>
<alias name="" field="gml_id" index="0"/>
<alias name="" field="objectid" index="1"/>
<alias name="" field="obj_nr" index="2"/>
<alias name="" field="typ" index="3"/>
<alias name="" field="text_1" index="4"/>
<alias name="" field="ext_objnr" index="5"/>
<alias name="" field="ext_hida_n" index="6"/>
<alias name="" field="ext_eigenn" index="7"/>
<alias name="" field="ext_kreis" index="8"/>
<alias name="" field="ext_gemein" index="9"/>
<alias name="" field="ext_ortste" index="10"/>
<alias name="" field="ext_strass" index="11"/>
<alias name="" field="ext_hausnu" index="12"/>
<alias name="" field="ext_gemark" index="13"/>
<alias name="" field="ext_flurst" index="14"/>
<alias name="" field="ext_kchar1" index="15"/>
<alias name="" field="ext_kchar2" index="16"/>
<alias name="" field="ext_kchar3" index="17"/>
<alias name="" field="ext_datier" index="18"/>
<alias name="" field="ext_typ" index="19"/>
<alias name="" field="gd_ueberar" index="20"/>
<alias name="" field="sort" index="21"/>
<alias name="" field="Stand" index="22"/>
</aliases>
<defaults>
<default field="gml_id" expression="" applyOnUpdate="0"/>
<default field="objectid" expression="" applyOnUpdate="0"/>
<default field="obj_nr" expression="" applyOnUpdate="0"/>
<default field="typ" expression="" applyOnUpdate="0"/>
<default field="text_1" expression="" applyOnUpdate="0"/>
<default field="ext_objnr" expression="" applyOnUpdate="0"/>
<default field="ext_hida_n" expression="" applyOnUpdate="0"/>
<default field="ext_eigenn" expression="" applyOnUpdate="0"/>
<default field="ext_kreis" expression="" applyOnUpdate="0"/>
<default field="ext_gemein" expression="" applyOnUpdate="0"/>
<default field="ext_ortste" expression="" applyOnUpdate="0"/>
<default field="ext_strass" expression="" applyOnUpdate="0"/>
<default field="ext_hausnu" expression="" applyOnUpdate="0"/>
<default field="ext_gemark" expression="" applyOnUpdate="0"/>
<default field="ext_flurst" expression="" applyOnUpdate="0"/>
<default field="ext_kchar1" expression="" applyOnUpdate="0"/>
<default field="ext_kchar2" expression="" applyOnUpdate="0"/>
<default field="ext_kchar3" expression="" applyOnUpdate="0"/>
<default field="ext_datier" expression="" applyOnUpdate="0"/>
<default field="ext_typ" expression="" applyOnUpdate="0"/>
<default field="gd_ueberar" expression="" applyOnUpdate="0"/>
<default field="sort" expression="" applyOnUpdate="0"/>
<default field="Stand" expression="" applyOnUpdate="0"/>
</defaults>
<constraints>
<constraint exp_strength="0" field="gml_id" unique_strength="0" notnull_strength="0" constraints="0"/>
<constraint exp_strength="0" field="objectid" unique_strength="0" notnull_strength="0" constraints="0"/>
<constraint exp_strength="0" field="obj_nr" unique_strength="0" notnull_strength="0" constraints="0"/>
<constraint exp_strength="0" field="typ" unique_strength="0" notnull_strength="0" constraints="0"/>
<constraint exp_strength="0" field="text_1" unique_strength="0" notnull_strength="0" constraints="0"/>
<constraint exp_strength="0" field="ext_objnr" unique_strength="0" notnull_strength="0" constraints="0"/>
<constraint exp_strength="0" field="ext_hida_n" unique_strength="0" notnull_strength="0" constraints="0"/>
<constraint exp_strength="0" field="ext_eigenn" unique_strength="0" notnull_strength="0" constraints="0"/>
<constraint exp_strength="0" field="ext_kreis" unique_strength="0" notnull_strength="0" constraints="0"/>
<constraint exp_strength="0" field="ext_gemein" unique_strength="0" notnull_strength="0" constraints="0"/>
<constraint exp_strength="0" field="ext_ortste" unique_strength="0" notnull_strength="0" constraints="0"/>
<constraint exp_strength="0" field="ext_strass" unique_strength="0" notnull_strength="0" constraints="0"/>
<constraint exp_strength="0" field="ext_hausnu" unique_strength="0" notnull_strength="0" constraints="0"/>
<constraint exp_strength="0" field="ext_gemark" unique_strength="0" notnull_strength="0" constraints="0"/>
<constraint exp_strength="0" field="ext_flurst" unique_strength="0" notnull_strength="0" constraints="0"/>
<constraint exp_strength="0" field="ext_kchar1" unique_strength="0" notnull_strength="0" constraints="0"/>
<constraint exp_strength="0" field="ext_kchar2" unique_strength="0" notnull_strength="0" constraints="0"/>
<constraint exp_strength="0" field="ext_kchar3" unique_strength="0" notnull_strength="0" constraints="0"/>
<constraint exp_strength="0" field="ext_datier" unique_strength="0" notnull_strength="0" constraints="0"/>
<constraint exp_strength="0" field="ext_typ" unique_strength="0" notnull_strength="0" constraints="0"/>
<constraint exp_strength="0" field="gd_ueberar" unique_strength="0" notnull_strength="0" constraints="0"/>
<constraint exp_strength="0" field="sort" unique_strength="0" notnull_strength="0" constraints="0"/>
<constraint exp_strength="0" field="Stand" unique_strength="0" notnull_strength="0" constraints="0"/>
</constraints>
<constraintExpressions>
<constraint field="gml_id" desc="" exp=""/>
<constraint field="objectid" desc="" exp=""/>
<constraint field="obj_nr" desc="" exp=""/>
<constraint field="typ" desc="" exp=""/>
<constraint field="text_1" desc="" exp=""/>
<constraint field="ext_objnr" desc="" exp=""/>
<constraint field="ext_hida_n" desc="" exp=""/>
<constraint field="ext_eigenn" desc="" exp=""/>
<constraint field="ext_kreis" desc="" exp=""/>
<constraint field="ext_gemein" desc="" exp=""/>
<constraint field="ext_ortste" desc="" exp=""/>
<constraint field="ext_strass" desc="" exp=""/>
<constraint field="ext_hausnu" desc="" exp=""/>
<constraint field="ext_gemark" desc="" exp=""/>
<constraint field="ext_flurst" desc="" exp=""/>
<constraint field="ext_kchar1" desc="" exp=""/>
<constraint field="ext_kchar2" desc="" exp=""/>
<constraint field="ext_kchar3" desc="" exp=""/>
<constraint field="ext_datier" desc="" exp=""/>
<constraint field="ext_typ" desc="" exp=""/>
<constraint field="gd_ueberar" desc="" exp=""/>
<constraint field="sort" desc="" exp=""/>
<constraint field="Stand" desc="" exp=""/>
</constraintExpressions>
<expressionfields/>
<attributeactions>
<defaultAction key="Canvas" value="{00000000-0000-0000-0000-000000000000}"/>
</attributeactions>
<attributetableconfig sortOrder="0" actionWidgetStyle="dropDown" sortExpression="&quot;gml_id&quot;">
<columns>
<column width="-1" name="gml_id" type="field" hidden="0"/>
<column width="-1" name="objectid" type="field" hidden="0"/>
<column width="-1" name="obj_nr" type="field" hidden="0"/>
<column width="-1" name="typ" type="field" hidden="0"/>
<column width="-1" name="text_1" type="field" hidden="0"/>
<column width="-1" name="ext_objnr" type="field" hidden="0"/>
<column width="-1" name="ext_hida_n" type="field" hidden="0"/>
<column width="-1" name="ext_eigenn" type="field" hidden="0"/>
<column width="-1" name="ext_kreis" type="field" hidden="0"/>
<column width="-1" name="ext_gemein" type="field" hidden="0"/>
<column width="-1" name="ext_ortste" type="field" hidden="0"/>
<column width="-1" name="ext_strass" type="field" hidden="0"/>
<column width="-1" name="ext_hausnu" type="field" hidden="0"/>
<column width="-1" name="ext_gemark" type="field" hidden="0"/>
<column width="-1" name="ext_flurst" type="field" hidden="0"/>
<column width="-1" name="ext_kchar1" type="field" hidden="0"/>
<column width="-1" name="ext_kchar2" type="field" hidden="0"/>
<column width="-1" name="ext_kchar3" type="field" hidden="0"/>
<column width="-1" name="ext_datier" type="field" hidden="0"/>
<column width="-1" name="ext_typ" type="field" hidden="0"/>
<column width="-1" name="gd_ueberar" type="field" hidden="0"/>
<column width="-1" name="sort" type="field" hidden="0"/>
<column width="-1" name="Stand" type="field" hidden="0"/>
<column width="-1" type="actions" hidden="1"/>
</columns>
</attributetableconfig>
<conditionalstyles>
<rowstyles/>
<fieldstyles/>
</conditionalstyles>
<storedexpressions/>
<editform tolerant="1"></editform>
<editforminit/>
<editforminitcodesource>0</editforminitcodesource>
<editforminitfilepath></editforminitfilepath>
<editforminitcode><![CDATA[# -*- coding: utf-8 -*-
"""
QGIS forms can have a Python function that is called when the form is
opened.
Use this function to add extra logic to your forms.
Enter the name of the function in the "Python Init function"
field.
An example follows:
"""
from qgis.PyQt.QtWidgets import QWidget
def my_form_open(dialog, layer, feature):
geom = feature.geometry()
control = dialog.findChild(QWidget, "MyLineEdit")
]]></editforminitcode>
<featformsuppress>0</featformsuppress>
<editorlayout>generatedlayout</editorlayout>
<editable>
<field name="Stand" editable="1"/>
<field name="ext_datier" editable="1"/>
<field name="ext_eigenn" editable="1"/>
<field name="ext_flurst" editable="1"/>
<field name="ext_gemark" editable="1"/>
<field name="ext_gemein" editable="1"/>
<field name="ext_hausnu" editable="1"/>
<field name="ext_hida_n" editable="1"/>
<field name="ext_kchar1" editable="1"/>
<field name="ext_kchar2" editable="1"/>
<field name="ext_kchar3" editable="1"/>
<field name="ext_kreis" editable="1"/>
<field name="ext_objnr" editable="1"/>
<field name="ext_ortste" editable="1"/>
<field name="ext_strass" editable="1"/>
<field name="ext_typ" editable="1"/>
<field name="gd_ueberar" editable="1"/>
<field name="gml_id" editable="1"/>
<field name="obj_nr" editable="1"/>
<field name="objectid" editable="1"/>
<field name="sort" editable="1"/>
<field name="text_1" editable="1"/>
<field name="typ" editable="1"/>
</editable>
<labelOnTop>
<field name="Stand" labelOnTop="0"/>
<field name="ext_datier" labelOnTop="0"/>
<field name="ext_eigenn" labelOnTop="0"/>
<field name="ext_flurst" labelOnTop="0"/>
<field name="ext_gemark" labelOnTop="0"/>
<field name="ext_gemein" labelOnTop="0"/>
<field name="ext_hausnu" labelOnTop="0"/>
<field name="ext_hida_n" labelOnTop="0"/>
<field name="ext_kchar1" labelOnTop="0"/>
<field name="ext_kchar2" labelOnTop="0"/>
<field name="ext_kchar3" labelOnTop="0"/>
<field name="ext_kreis" labelOnTop="0"/>
<field name="ext_objnr" labelOnTop="0"/>
<field name="ext_ortste" labelOnTop="0"/>
<field name="ext_strass" labelOnTop="0"/>
<field name="ext_typ" labelOnTop="0"/>
<field name="gd_ueberar" labelOnTop="0"/>
<field name="gml_id" labelOnTop="0"/>
<field name="obj_nr" labelOnTop="0"/>
<field name="objectid" labelOnTop="0"/>
<field name="sort" labelOnTop="0"/>
<field name="text_1" labelOnTop="0"/>
<field name="typ" labelOnTop="0"/>
</labelOnTop>
<dataDefinedFieldProperties/>
<widgets/>
<previewExpression>"gml_id"</previewExpression>
<mapTip></mapTip>
<layerGeometryType>2</layerGeometryType>
</qgis>

225
styles/GIS_Biotope_F.qml Normal file
View File

@@ -0,0 +1,225 @@
<!DOCTYPE qgis PUBLIC 'http://mrcc.com/qgis.dtd' 'SYSTEM'>
<qgis version="3.40.7-Bratislava" styleCategories="Symbology">
<renderer-v2 forceraster="0" enableorderby="0" referencescale="-1" type="singleSymbol" symbollevels="0">
<symbols>
<symbol alpha="1" clip_to_extent="1" frame_rate="10" type="fill" force_rhr="0" is_animated="0" name="0">
<data_defined_properties>
<Option type="Map">
<Option type="QString" name="name" value=""/>
<Option name="properties"/>
<Option type="QString" name="type" value="collection"/>
</Option>
</data_defined_properties>
<layer enabled="1" pass="0" id="{ef62a644-15f0-4a1c-aa78-8ae94dc5749c}" class="SimpleFill" locked="0">
<Option type="Map">
<Option type="QString" name="border_width_map_unit_scale" value="3x:0,0,0,0,0,0"/>
<Option type="QString" name="color" value="0,0,255,255,rgb:0,0,1,1"/>
<Option type="QString" name="joinstyle" value="bevel"/>
<Option type="QString" name="offset" value="0,0"/>
<Option type="QString" name="offset_map_unit_scale" value="3x:0,0,0,0,0,0"/>
<Option type="QString" name="offset_unit" value="MM"/>
<Option type="QString" name="outline_color" value="89,89,89,255,rgb:0.34901960784313724,0.34901960784313724,0.34901960784313724,1"/>
<Option type="QString" name="outline_style" value="solid"/>
<Option type="QString" name="outline_width" value="0.4"/>
<Option type="QString" name="outline_width_unit" value="MM"/>
<Option type="QString" name="style" value="no"/>
</Option>
<data_defined_properties>
<Option type="Map">
<Option type="QString" name="name" value=""/>
<Option name="properties"/>
<Option type="QString" name="type" value="collection"/>
</Option>
</data_defined_properties>
</layer>
<layer enabled="1" pass="0" id="{4b0f1a3b-14cf-4149-bf17-8f30fa594ef1}" class="LinePatternFill" locked="0">
<Option type="Map">
<Option type="QString" name="angle" value="0"/>
<Option type="QString" name="clip_mode" value="during_render"/>
<Option type="QString" name="color" value="255,0,0,255,rgb:1,0,0,1"/>
<Option type="QString" name="coordinate_reference" value="feature"/>
<Option type="QString" name="distance" value="4"/>
<Option type="QString" name="distance_map_unit_scale" value="3x:0,0,0,0,0,0"/>
<Option type="QString" name="distance_unit" value="MM"/>
<Option type="QString" name="line_width" value="0.25"/>
<Option type="QString" name="line_width_map_unit_scale" value="3x:0,0,0,0,0,0"/>
<Option type="QString" name="line_width_unit" value="Point"/>
<Option type="QString" name="offset" value="0"/>
<Option type="QString" name="offset_map_unit_scale" value="3x:0,0,0,0,0,0"/>
<Option type="QString" name="offset_unit" value="MM"/>
<Option type="QString" name="outline_width_map_unit_scale" value="3x:0,0,0,0,0,0"/>
<Option type="QString" name="outline_width_unit" value="Point"/>
</Option>
<data_defined_properties>
<Option type="Map">
<Option type="QString" name="name" value=""/>
<Option name="properties"/>
<Option type="QString" name="type" value="collection"/>
</Option>
</data_defined_properties>
<symbol alpha="1" clip_to_extent="1" frame_rate="10" type="line" force_rhr="0" is_animated="0" name="@0@1">
<data_defined_properties>
<Option type="Map">
<Option type="QString" name="name" value=""/>
<Option name="properties"/>
<Option type="QString" name="type" value="collection"/>
</Option>
</data_defined_properties>
<layer enabled="1" pass="0" id="{853c5350-e21e-49ba-a151-7de585769136}" class="SimpleLine" locked="0">
<Option type="Map">
<Option type="QString" name="align_dash_pattern" value="0"/>
<Option type="QString" name="capstyle" value="square"/>
<Option type="QString" name="customdash" value="5;2"/>
<Option type="QString" name="customdash_map_unit_scale" value="3x:0,0,0,0,0,0"/>
<Option type="QString" name="customdash_unit" value="MM"/>
<Option type="QString" name="dash_pattern_offset" value="0"/>
<Option type="QString" name="dash_pattern_offset_map_unit_scale" value="3x:0,0,0,0,0,0"/>
<Option type="QString" name="dash_pattern_offset_unit" value="MM"/>
<Option type="QString" name="draw_inside_polygon" value="0"/>
<Option type="QString" name="joinstyle" value="bevel"/>
<Option type="QString" name="line_color" value="0,0,0,255,rgb:0,0,0,1"/>
<Option type="QString" name="line_style" value="solid"/>
<Option type="QString" name="line_width" value="0.3"/>
<Option type="QString" name="line_width_unit" value="MM"/>
<Option type="QString" name="offset" value="0"/>
<Option type="QString" name="offset_map_unit_scale" value="3x:0,0,0,0,0,0"/>
<Option type="QString" name="offset_unit" value="MM"/>
<Option type="QString" name="ring_filter" value="0"/>
<Option type="QString" name="trim_distance_end" value="0"/>
<Option type="QString" name="trim_distance_end_map_unit_scale" value="3x:0,0,0,0,0,0"/>
<Option type="QString" name="trim_distance_end_unit" value="MM"/>
<Option type="QString" name="trim_distance_start" value="0"/>
<Option type="QString" name="trim_distance_start_map_unit_scale" value="3x:0,0,0,0,0,0"/>
<Option type="QString" name="trim_distance_start_unit" value="MM"/>
<Option type="QString" name="tweak_dash_pattern_on_corners" value="0"/>
<Option type="QString" name="use_custom_dash" value="0"/>
<Option type="QString" name="width_map_unit_scale" value="3x:0,0,0,0,0,0"/>
</Option>
<data_defined_properties>
<Option type="Map">
<Option type="QString" name="name" value=""/>
<Option name="properties"/>
<Option type="QString" name="type" value="collection"/>
</Option>
</data_defined_properties>
</layer>
</symbol>
</layer>
<layer enabled="1" pass="0" id="{8675ccd2-9bf1-447f-9cc7-f95b932972f6}" class="MarkerLine" locked="0">
<Option type="Map">
<Option type="QString" name="average_angle_length" value="4"/>
<Option type="QString" name="average_angle_map_unit_scale" value="3x:0,0,0,0,0,0"/>
<Option type="QString" name="average_angle_unit" value="MM"/>
<Option type="QString" name="interval" value="5"/>
<Option type="QString" name="interval_map_unit_scale" value="3x:0,0,0,0,0,0"/>
<Option type="QString" name="interval_unit" value="MM"/>
<Option type="QString" name="offset" value="0"/>
<Option type="QString" name="offset_along_line" value="0"/>
<Option type="QString" name="offset_along_line_map_unit_scale" value="3x:0,0,0,0,0,0"/>
<Option type="QString" name="offset_along_line_unit" value="MM"/>
<Option type="QString" name="offset_map_unit_scale" value="3x:0,0,0,0,0,0"/>
<Option type="QString" name="offset_unit" value="MM"/>
<Option type="bool" name="place_on_every_part" value="true"/>
<Option type="QString" name="placements" value="Interval"/>
<Option type="QString" name="ring_filter" value="0"/>
<Option type="QString" name="rotate" value="1"/>
</Option>
<data_defined_properties>
<Option type="Map">
<Option type="QString" name="name" value=""/>
<Option name="properties"/>
<Option type="QString" name="type" value="collection"/>
</Option>
</data_defined_properties>
<symbol alpha="1" clip_to_extent="1" frame_rate="10" type="marker" force_rhr="0" is_animated="0" name="@0@2">
<data_defined_properties>
<Option type="Map">
<Option type="QString" name="name" value=""/>
<Option name="properties"/>
<Option type="QString" name="type" value="collection"/>
</Option>
</data_defined_properties>
<layer enabled="1" pass="0" id="{3260c32a-e9bb-45db-8ef8-3abb54ae868e}" class="SimpleMarker" locked="0">
<Option type="Map">
<Option type="QString" name="angle" value="270"/>
<Option type="QString" name="cap_style" value="square"/>
<Option type="QString" name="color" value="255,0,0,255,rgb:1,0,0,1"/>
<Option type="QString" name="horizontal_anchor_point" value="1"/>
<Option type="QString" name="joinstyle" value="bevel"/>
<Option type="QString" name="name" value="arrowhead"/>
<Option type="QString" name="offset" value="-0.29999999999999999,0"/>
<Option type="QString" name="offset_map_unit_scale" value="3x:0,0,0,0,0,0"/>
<Option type="QString" name="offset_unit" value="MM"/>
<Option type="QString" name="outline_color" value="89,89,89,255,rgb:0.34901960784313724,0.34901960784313724,0.34901960784313724,1"/>
<Option type="QString" name="outline_style" value="solid"/>
<Option type="QString" name="outline_width" value="0.3"/>
<Option type="QString" name="outline_width_map_unit_scale" value="3x:0,0,0,0,0,0"/>
<Option type="QString" name="outline_width_unit" value="MM"/>
<Option type="QString" name="scale_method" value="diameter"/>
<Option type="QString" name="size" value="1.5"/>
<Option type="QString" name="size_map_unit_scale" value="3x:0,0,0,0,0,0"/>
<Option type="QString" name="size_unit" value="MM"/>
<Option type="QString" name="vertical_anchor_point" value="1"/>
</Option>
<data_defined_properties>
<Option type="Map">
<Option type="QString" name="name" value=""/>
<Option name="properties"/>
<Option type="QString" name="type" value="collection"/>
</Option>
</data_defined_properties>
</layer>
</symbol>
</layer>
</symbol>
</symbols>
<rotation/>
<sizescale/>
<data-defined-properties>
<Option type="Map">
<Option type="QString" name="name" value=""/>
<Option name="properties"/>
<Option type="QString" name="type" value="collection"/>
</Option>
</data-defined-properties>
</renderer-v2>
<selection mode="Default">
<selectionColor invalid="1"/>
<selectionSymbol>
<symbol alpha="1" clip_to_extent="1" frame_rate="10" type="fill" force_rhr="0" is_animated="0" name="">
<data_defined_properties>
<Option type="Map">
<Option type="QString" name="name" value=""/>
<Option name="properties"/>
<Option type="QString" name="type" value="collection"/>
</Option>
</data_defined_properties>
<layer enabled="1" pass="0" id="{fc42b18b-5949-4fd5-b614-0205e0f7d69c}" class="SimpleFill" locked="0">
<Option type="Map">
<Option type="QString" name="border_width_map_unit_scale" value="3x:0,0,0,0,0,0"/>
<Option type="QString" name="color" value="0,0,255,255,rgb:0,0,1,1"/>
<Option type="QString" name="joinstyle" value="bevel"/>
<Option type="QString" name="offset" value="0,0"/>
<Option type="QString" name="offset_map_unit_scale" value="3x:0,0,0,0,0,0"/>
<Option type="QString" name="offset_unit" value="MM"/>
<Option type="QString" name="outline_color" value="35,35,35,255,rgb:0.13725490196078433,0.13725490196078433,0.13725490196078433,1"/>
<Option type="QString" name="outline_style" value="solid"/>
<Option type="QString" name="outline_width" value="0.26"/>
<Option type="QString" name="outline_width_unit" value="MM"/>
<Option type="QString" name="style" value="solid"/>
</Option>
<data_defined_properties>
<Option type="Map">
<Option type="QString" name="name" value=""/>
<Option name="properties"/>
<Option type="QString" name="type" value="collection"/>
</Option>
</data_defined_properties>
</layer>
</symbol>
</selectionSymbol>
</selection>
<blendMode>0</blendMode>
<featureBlendMode>0</featureBlendMode>
<layerGeometryType>2</layerGeometryType>
</qgis>

View File

@@ -0,0 +1,349 @@
<!DOCTYPE qgis PUBLIC 'http://mrcc.com/qgis.dtd' 'SYSTEM'>
<qgis labelsEnabled="1" version="3.40.4-Bratislava" styleCategories="Symbology|Labeling">
<renderer-v2 enableorderby="0" type="singleSymbol" referencescale="-1" forceraster="0" symbollevels="0">
<symbols>
<symbol name="0" type="fill" force_rhr="0" clip_to_extent="1" is_animated="0" frame_rate="10" alpha="1">
<data_defined_properties>
<Option type="Map">
<Option name="name" type="QString" value=""/>
<Option name="properties"/>
<Option name="type" type="QString" value="collection"/>
</Option>
</data_defined_properties>
<layer id="{bc287a30-ef97-44d4-ac42-1890985a8e6e}" class="SimpleFill" locked="0" enabled="1" pass="0">
<Option type="Map">
<Option name="border_width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
<Option name="color" type="QString" value="229,182,54,255,rgb:0.89803921568627454,0.71372549019607845,0.21176470588235294,1"/>
<Option name="joinstyle" type="QString" value="bevel"/>
<Option name="offset" type="QString" value="0,0"/>
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
<Option name="offset_unit" type="QString" value="MM"/>
<Option name="outline_color" type="QString" value="35,35,35,255,rgb:0.13725490196078433,0.13725490196078433,0.13725490196078433,1"/>
<Option name="outline_style" type="QString" value="solid"/>
<Option name="outline_width" type="QString" value="0.5"/>
<Option name="outline_width_unit" type="QString" value="MM"/>
<Option name="style" type="QString" value="no"/>
</Option>
<data_defined_properties>
<Option type="Map">
<Option name="name" type="QString" value=""/>
<Option name="properties"/>
<Option name="type" type="QString" value="collection"/>
</Option>
</data_defined_properties>
</layer>
</symbol>
</symbols>
<rotation/>
<sizescale/>
<data-defined-properties>
<Option type="Map">
<Option name="name" type="QString" value=""/>
<Option name="properties"/>
<Option name="type" type="QString" value="collection"/>
</Option>
</data-defined-properties>
</renderer-v2>
<selection mode="Default">
<selectionColor invalid="1"/>
<selectionSymbol>
<symbol name="" type="fill" force_rhr="0" clip_to_extent="1" is_animated="0" frame_rate="10" alpha="1">
<data_defined_properties>
<Option type="Map">
<Option name="name" type="QString" value=""/>
<Option name="properties"/>
<Option name="type" type="QString" value="collection"/>
</Option>
</data_defined_properties>
<layer id="{819785db-4b31-4c4c-8332-821c09f81921}" class="SimpleFill" locked="0" enabled="1" pass="0">
<Option type="Map">
<Option name="border_width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
<Option name="color" type="QString" value="0,0,255,255,rgb:0,0,1,1"/>
<Option name="joinstyle" type="QString" value="bevel"/>
<Option name="offset" type="QString" value="0,0"/>
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
<Option name="offset_unit" type="QString" value="MM"/>
<Option name="outline_color" type="QString" value="35,35,35,255,rgb:0.13725490196078433,0.13725490196078433,0.13725490196078433,1"/>
<Option name="outline_style" type="QString" value="solid"/>
<Option name="outline_width" type="QString" value="0.26"/>
<Option name="outline_width_unit" type="QString" value="MM"/>
<Option name="style" type="QString" value="solid"/>
</Option>
<data_defined_properties>
<Option type="Map">
<Option name="name" type="QString" value=""/>
<Option name="properties"/>
<Option name="type" type="QString" value="collection"/>
</Option>
</data_defined_properties>
</layer>
</symbol>
</selectionSymbol>
</selection>
<labeling type="rule-based">
<rules key="{15ec465c-d050-4a64-b4ba-262af80bbb43}">
<rule key="{b56520a8-9937-40dd-b8fe-c4386271088d}" filter="&quot;flurstuecksnummer_ax_flurstuecksnummer_nenner&quot; IS NULL" description="FlstNr_ohne_Zusatz">
<settings calloutType="simple">
<text-style forcedItalic="0" fontUnderline="0" previewBkgrdColor="255,255,255,255,rgb:1,1,1,1" fontItalic="0" tabStopDistanceUnit="Point" textOpacity="1" multilineHeight="1" fontFamily="Times New Roman" fontStrikeout="0" capitalization="0" fieldName="flurstuecksnummer_ax_flurstuecksnummer_zaehler" legendString="Aa" fontSizeUnit="Point" fontWeight="50" textOrientation="horizontal" multilineHeightUnit="Percentage" fontKerning="1" tabStopDistanceMapUnitScale="3x:0,0,0,0,0,0" forcedBold="0" fontWordSpacing="0" fontLetterSpacing="0" fontSizeMapUnitScale="3x:0,0,0,0,0,0" allowHtml="0" isExpression="0" useSubstitutions="0" textColor="0,0,0,255,rgb:0,0,0,1" blendMode="0" fontSize="7" namedStyle="Standard" tabStopDistance="80">
<families/>
<text-buffer bufferBlendMode="0" bufferSizeUnits="MM" bufferOpacity="1" bufferJoinStyle="128" bufferNoFill="1" bufferSizeMapUnitScale="3x:0,0,0,0,0,0" bufferDraw="1" bufferColor="255,255,255,255,rgb:1,1,1,1" bufferSize="1"/>
<text-mask maskEnabled="0" maskSizeMapUnitScale="3x:0,0,0,0,0,0" maskSizeUnits="MM" maskJoinStyle="128" maskSize2="1.5" maskSize="1.5" maskOpacity="1" maskType="0" maskedSymbolLayers=""/>
<background shapeOffsetX="0" shapeRadiiY="0" shapeSVGFile="" shapeRotation="0" shapeBorderWidth="0" shapeRadiiX="0" shapeSizeX="0" shapeBorderWidthMapUnitScale="3x:0,0,0,0,0,0" shapeSizeUnit="MM" shapeRotationType="0" shapeBorderColor="128,128,128,255,rgb:0.50196078431372548,0.50196078431372548,0.50196078431372548,1" shapeJoinStyle="64" shapeRadiiMapUnitScale="3x:0,0,0,0,0,0" shapeType="0" shapeOffsetMapUnitScale="3x:0,0,0,0,0,0" shapeDraw="0" shapeBorderWidthUnit="MM" shapeFillColor="255,255,255,255,rgb:1,1,1,1" shapeOffsetUnit="MM" shapeRadiiUnit="MM" shapeSizeMapUnitScale="3x:0,0,0,0,0,0" shapeOffsetY="0" shapeBlendMode="0" shapeSizeType="0" shapeSizeY="0" shapeOpacity="1">
<symbol name="markerSymbol" type="marker" force_rhr="0" clip_to_extent="1" is_animated="0" frame_rate="10" alpha="1">
<data_defined_properties>
<Option type="Map">
<Option name="name" type="QString" value=""/>
<Option name="properties"/>
<Option name="type" type="QString" value="collection"/>
</Option>
</data_defined_properties>
<layer id="" class="SimpleMarker" locked="0" enabled="1" pass="0">
<Option type="Map">
<Option name="angle" type="QString" value="0"/>
<Option name="cap_style" type="QString" value="square"/>
<Option name="color" type="QString" value="141,90,153,255,rgb:0.55294117647058827,0.35294117647058826,0.59999999999999998,1"/>
<Option name="horizontal_anchor_point" type="QString" value="1"/>
<Option name="joinstyle" type="QString" value="bevel"/>
<Option name="name" type="QString" value="circle"/>
<Option name="offset" type="QString" value="0,0"/>
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
<Option name="offset_unit" type="QString" value="MM"/>
<Option name="outline_color" type="QString" value="35,35,35,255,rgb:0.13725490196078433,0.13725490196078433,0.13725490196078433,1"/>
<Option name="outline_style" type="QString" value="solid"/>
<Option name="outline_width" type="QString" value="0"/>
<Option name="outline_width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
<Option name="outline_width_unit" type="QString" value="MM"/>
<Option name="scale_method" type="QString" value="diameter"/>
<Option name="size" type="QString" value="2"/>
<Option name="size_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
<Option name="size_unit" type="QString" value="MM"/>
<Option name="vertical_anchor_point" type="QString" value="1"/>
</Option>
<data_defined_properties>
<Option type="Map">
<Option name="name" type="QString" value=""/>
<Option name="properties"/>
<Option name="type" type="QString" value="collection"/>
</Option>
</data_defined_properties>
</layer>
</symbol>
<symbol name="fillSymbol" type="fill" force_rhr="0" clip_to_extent="1" is_animated="0" frame_rate="10" alpha="1">
<data_defined_properties>
<Option type="Map">
<Option name="name" type="QString" value=""/>
<Option name="properties"/>
<Option name="type" type="QString" value="collection"/>
</Option>
</data_defined_properties>
<layer id="" class="SimpleFill" locked="0" enabled="1" pass="0">
<Option type="Map">
<Option name="border_width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
<Option name="color" type="QString" value="255,255,255,255,rgb:1,1,1,1"/>
<Option name="joinstyle" type="QString" value="bevel"/>
<Option name="offset" type="QString" value="0,0"/>
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
<Option name="offset_unit" type="QString" value="MM"/>
<Option name="outline_color" type="QString" value="128,128,128,255,rgb:0.50196078431372548,0.50196078431372548,0.50196078431372548,1"/>
<Option name="outline_style" type="QString" value="no"/>
<Option name="outline_width" type="QString" value="0"/>
<Option name="outline_width_unit" type="QString" value="MM"/>
<Option name="style" type="QString" value="solid"/>
</Option>
<data_defined_properties>
<Option type="Map">
<Option name="name" type="QString" value=""/>
<Option name="properties"/>
<Option name="type" type="QString" value="collection"/>
</Option>
</data_defined_properties>
</layer>
</symbol>
</background>
<shadow shadowRadiusAlphaOnly="0" shadowOffsetAngle="135" shadowOpacity="0.69999999999999996" shadowOffsetUnit="MM" shadowColor="0,0,0,255,rgb:0,0,0,1" shadowRadiusMapUnitScale="3x:0,0,0,0,0,0" shadowRadius="1.5" shadowBlendMode="6" shadowScale="100" shadowDraw="0" shadowUnder="0" shadowOffsetDist="1" shadowRadiusUnit="MM" shadowOffsetMapUnitScale="3x:0,0,0,0,0,0" shadowOffsetGlobal="1"/>
<dd_properties>
<Option type="Map">
<Option name="name" type="QString" value=""/>
<Option name="properties"/>
<Option name="type" type="QString" value="collection"/>
</Option>
</dd_properties>
<substitutions/>
</text-style>
<text-format multilineAlign="3" autoWrapLength="0" reverseDirectionSymbol="0" plussign="0" rightDirectionSymbol=">" addDirectionSymbol="0" leftDirectionSymbol="&lt;" formatNumbers="0" decimals="3" placeDirectionSymbol="0" wrapChar="" useMaxLineLengthForAutoWrap="1"/>
<placement lineAnchorClipping="0" maximumDistanceMapUnitScale="3x:0,0,0,0,0,0" xOffset="0" maximumDistance="0" geometryGenerator="" distMapUnitScale="3x:0,0,0,0,0,0" predefinedPositionOrder="TR,TL,BR,BL,R,L,TSR,BSR" layerType="PolygonGeometry" lineAnchorPercent="0.5" rotationUnit="AngleDegrees" repeatDistanceMapUnitScale="3x:0,0,0,0,0,0" priority="9" yOffset="0" quadOffset="4" polygonPlacementFlags="2" centroidInside="1" placement="1" placementFlags="10" prioritization="PreferCloser" overrunDistance="0" offsetUnits="MM" maxCurvedCharAngleOut="-25" preserveRotation="1" lineAnchorTextPoint="CenterOfText" overlapHandling="PreventOverlap" rotationAngle="0" repeatDistance="0" offsetType="0" distUnits="MM" allowDegraded="0" overrunDistanceUnit="MM" centroidWhole="0" geometryGeneratorType="PointGeometry" fitInPolygonOnly="0" geometryGeneratorEnabled="0" dist="0" maxCurvedCharAngleIn="25" overrunDistanceMapUnitScale="3x:0,0,0,0,0,0" maximumDistanceUnit="MM" lineAnchorType="0" repeatDistanceUnits="MM" labelOffsetMapUnitScale="3x:0,0,0,0,0,0"/>
<rendering fontMinPixelSize="3" fontMaxPixelSize="10000" maxNumLabels="2000" obstacleFactor="1" mergeLines="0" fontLimitPixelSize="0" limitNumLabels="0" minFeatureSize="0" scaleMin="0" scaleVisibility="0" labelPerPart="0" scaleMax="0" obstacleType="1" upsidedownLabels="0" drawLabels="1" zIndex="0" unplacedVisibility="0" obstacle="1"/>
<dd_properties>
<Option type="Map">
<Option name="name" type="QString" value=""/>
<Option name="properties"/>
<Option name="type" type="QString" value="collection"/>
</Option>
</dd_properties>
<callout type="simple">
<Option type="Map">
<Option name="anchorPoint" type="QString" value="centroid"/>
<Option name="blendMode" type="int" value="0"/>
<Option name="ddProperties" type="Map">
<Option name="name" type="QString" value=""/>
<Option name="properties"/>
<Option name="type" type="QString" value="collection"/>
</Option>
<Option name="drawToAllParts" type="bool" value="false"/>
<Option name="enabled" type="QString" value="1"/>
<Option name="labelAnchorPoint" type="QString" value="point_on_exterior"/>
<Option name="lineSymbol" type="QString" value="&lt;symbol name=&quot;symbol&quot; type=&quot;line&quot; force_rhr=&quot;0&quot; clip_to_extent=&quot;1&quot; is_animated=&quot;0&quot; frame_rate=&quot;10&quot; alpha=&quot;1&quot;>&lt;data_defined_properties>&lt;Option type=&quot;Map&quot;>&lt;Option name=&quot;name&quot; type=&quot;QString&quot; value=&quot;&quot;/>&lt;Option name=&quot;properties&quot;/>&lt;Option name=&quot;type&quot; type=&quot;QString&quot; value=&quot;collection&quot;/>&lt;/Option>&lt;/data_defined_properties>&lt;layer id=&quot;{3356d22c-5f69-4911-8e91-fcf32e8243fa}&quot; class=&quot;SimpleLine&quot; locked=&quot;0&quot; enabled=&quot;1&quot; pass=&quot;0&quot;>&lt;Option type=&quot;Map&quot;>&lt;Option name=&quot;align_dash_pattern&quot; type=&quot;QString&quot; value=&quot;0&quot;/>&lt;Option name=&quot;capstyle&quot; type=&quot;QString&quot; value=&quot;square&quot;/>&lt;Option name=&quot;customdash&quot; type=&quot;QString&quot; value=&quot;5;2&quot;/>&lt;Option name=&quot;customdash_map_unit_scale&quot; type=&quot;QString&quot; value=&quot;3x:0,0,0,0,0,0&quot;/>&lt;Option name=&quot;customdash_unit&quot; type=&quot;QString&quot; value=&quot;MM&quot;/>&lt;Option name=&quot;dash_pattern_offset&quot; type=&quot;QString&quot; value=&quot;0&quot;/>&lt;Option name=&quot;dash_pattern_offset_map_unit_scale&quot; type=&quot;QString&quot; value=&quot;3x:0,0,0,0,0,0&quot;/>&lt;Option name=&quot;dash_pattern_offset_unit&quot; type=&quot;QString&quot; value=&quot;MM&quot;/>&lt;Option name=&quot;draw_inside_polygon&quot; type=&quot;QString&quot; value=&quot;0&quot;/>&lt;Option name=&quot;joinstyle&quot; type=&quot;QString&quot; value=&quot;bevel&quot;/>&lt;Option name=&quot;line_color&quot; type=&quot;QString&quot; value=&quot;60,60,60,255,rgb:0.23529411764705882,0.23529411764705882,0.23529411764705882,1&quot;/>&lt;Option name=&quot;line_style&quot; type=&quot;QString&quot; value=&quot;solid&quot;/>&lt;Option name=&quot;line_width&quot; type=&quot;QString&quot; value=&quot;0.3&quot;/>&lt;Option name=&quot;line_width_unit&quot; type=&quot;QString&quot; value=&quot;MM&quot;/>&lt;Option name=&quot;offset&quot; type=&quot;QString&quot; value=&quot;0&quot;/>&lt;Option name=&quot;offset_map_unit_scale&quot; type=&quot;QString&quot; value=&quot;3x:0,0,0,0,0,0&quot;/>&lt;Option name=&quot;offset_unit&quot; type=&quot;QString&quot; value=&quot;MM&quot;/>&lt;Option name=&quot;ring_filter&quot; type=&quot;QString&quot; value=&quot;0&quot;/>&lt;Option name=&quot;trim_distance_end&quot; type=&quot;QString&quot; value=&quot;0&quot;/>&lt;Option name=&quot;trim_distance_end_map_unit_scale&quot; type=&quot;QString&quot; value=&quot;3x:0,0,0,0,0,0&quot;/>&lt;Option name=&quot;trim_distance_end_unit&quot; type=&quot;QString&quot; value=&quot;MM&quot;/>&lt;Option name=&quot;trim_distance_start&quot; type=&quot;QString&quot; value=&quot;0&quot;/>&lt;Option name=&quot;trim_distance_start_map_unit_scale&quot; type=&quot;QString&quot; value=&quot;3x:0,0,0,0,0,0&quot;/>&lt;Option name=&quot;trim_distance_start_unit&quot; type=&quot;QString&quot; value=&quot;MM&quot;/>&lt;Option name=&quot;tweak_dash_pattern_on_corners&quot; type=&quot;QString&quot; value=&quot;0&quot;/>&lt;Option name=&quot;use_custom_dash&quot; type=&quot;QString&quot; value=&quot;0&quot;/>&lt;Option name=&quot;width_map_unit_scale&quot; type=&quot;QString&quot; value=&quot;3x:0,0,0,0,0,0&quot;/>&lt;/Option>&lt;data_defined_properties>&lt;Option type=&quot;Map&quot;>&lt;Option name=&quot;name&quot; type=&quot;QString&quot; value=&quot;&quot;/>&lt;Option name=&quot;properties&quot;/>&lt;Option name=&quot;type&quot; type=&quot;QString&quot; value=&quot;collection&quot;/>&lt;/Option>&lt;/data_defined_properties>&lt;/layer>&lt;/symbol>"/>
<Option name="minLength" type="double" value="0"/>
<Option name="minLengthMapUnitScale" type="QString" value="3x:0,0,0,0,0,0"/>
<Option name="minLengthUnit" type="QString" value="MM"/>
<Option name="offsetFromAnchor" type="double" value="0"/>
<Option name="offsetFromAnchorMapUnitScale" type="QString" value="3x:0,0,0,0,0,0"/>
<Option name="offsetFromAnchorUnit" type="QString" value="MM"/>
<Option name="offsetFromLabel" type="double" value="0"/>
<Option name="offsetFromLabelMapUnitScale" type="QString" value="3x:0,0,0,0,0,0"/>
<Option name="offsetFromLabelUnit" type="QString" value="MM"/>
</Option>
</callout>
</settings>
</rule>
<rule key="{a9800348-c55c-45d0-95ce-be6db84663c8}" filter="&quot;flurstuecksnummer_ax_flurstuecksnummer_nenner&quot; IS NOT NULL" description="Flst_num_Zusatz">
<settings calloutType="simple">
<text-style forcedItalic="0" fontUnderline="0" previewBkgrdColor="255,255,255,255,rgb:1,1,1,1" fontItalic="0" tabStopDistanceUnit="Point" textOpacity="1" multilineHeight="1" fontFamily="Times New Roman" fontStrikeout="0" capitalization="0" fieldName="&quot;flurstuecksnummer_ax_flurstuecksnummer_zaehler&quot; || '\n' || &quot;flurstuecksnummer_ax_flurstuecksnummer_nenner&quot; " legendString="Aa" fontSizeUnit="Point" fontWeight="50" textOrientation="horizontal" multilineHeightUnit="Percentage" fontKerning="1" tabStopDistanceMapUnitScale="3x:0,0,0,0,0,0" forcedBold="0" fontWordSpacing="0" fontLetterSpacing="0" fontSizeMapUnitScale="3x:0,0,0,0,0,0" allowHtml="0" isExpression="1" useSubstitutions="0" textColor="0,0,0,255,rgb:0,0,0,1" blendMode="0" fontSize="7" namedStyle="Standard" tabStopDistance="80">
<families/>
<text-buffer bufferBlendMode="0" bufferSizeUnits="MM" bufferOpacity="1" bufferJoinStyle="128" bufferNoFill="1" bufferSizeMapUnitScale="3x:0,0,0,0,0,0" bufferDraw="1" bufferColor="255,255,255,255,rgb:1,1,1,1" bufferSize="0.5"/>
<text-mask maskEnabled="0" maskSizeMapUnitScale="3x:0,0,0,0,0,0" maskSizeUnits="MM" maskJoinStyle="128" maskSize2="1.5" maskSize="1.5" maskOpacity="1" maskType="0" maskedSymbolLayers=""/>
<background shapeOffsetX="0" shapeRadiiY="0" shapeSVGFile="" shapeRotation="0" shapeBorderWidth="0" shapeRadiiX="0" shapeSizeX="4" shapeBorderWidthMapUnitScale="3x:0,0,0,0,0,0" shapeSizeUnit="MM" shapeRotationType="0" shapeBorderColor="128,128,128,255,rgb:0.50196078431372548,0.50196078431372548,0.50196078431372548,1" shapeJoinStyle="64" shapeRadiiMapUnitScale="3x:0,0,0,0,0,0" shapeType="5" shapeOffsetMapUnitScale="3x:0,0,0,0,0,0" shapeDraw="1" shapeBorderWidthUnit="MM" shapeFillColor="255,255,255,255,rgb:1,1,1,1" shapeOffsetUnit="MM" shapeRadiiUnit="MM" shapeSizeMapUnitScale="3x:0,0,0,0,0,0" shapeOffsetY="0" shapeBlendMode="0" shapeSizeType="1" shapeSizeY="0" shapeOpacity="1">
<symbol name="markerSymbol" type="marker" force_rhr="0" clip_to_extent="1" is_animated="0" frame_rate="10" alpha="1">
<data_defined_properties>
<Option type="Map">
<Option name="name" type="QString" value=""/>
<Option name="properties"/>
<Option name="type" type="QString" value="collection"/>
</Option>
</data_defined_properties>
<layer id="" class="SimpleMarker" locked="0" enabled="1" pass="0">
<Option type="Map">
<Option name="angle" type="QString" value="90"/>
<Option name="cap_style" type="QString" value="square"/>
<Option name="color" type="QString" value="255,158,23,255,rgb:1,0.61960784313725492,0.09019607843137255,1"/>
<Option name="horizontal_anchor_point" type="QString" value="1"/>
<Option name="joinstyle" type="QString" value="bevel"/>
<Option name="name" type="QString" value="line"/>
<Option name="offset" type="QString" value="0,0"/>
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
<Option name="offset_unit" type="QString" value="MM"/>
<Option name="outline_color" type="QString" value="35,35,35,255,rgb:0.13725490196078433,0.13725490196078433,0.13725490196078433,1"/>
<Option name="outline_style" type="QString" value="solid"/>
<Option name="outline_width" type="QString" value="0.3"/>
<Option name="outline_width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
<Option name="outline_width_unit" type="QString" value="MM"/>
<Option name="scale_method" type="QString" value="diameter"/>
<Option name="size" type="QString" value="4"/>
<Option name="size_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
<Option name="size_unit" type="QString" value="MM"/>
<Option name="vertical_anchor_point" type="QString" value="1"/>
</Option>
<data_defined_properties>
<Option type="Map">
<Option name="name" type="QString" value=""/>
<Option name="properties"/>
<Option name="type" type="QString" value="collection"/>
</Option>
</data_defined_properties>
</layer>
</symbol>
<symbol name="fillSymbol" type="fill" force_rhr="0" clip_to_extent="1" is_animated="0" frame_rate="10" alpha="1">
<data_defined_properties>
<Option type="Map">
<Option name="name" type="QString" value=""/>
<Option name="properties"/>
<Option name="type" type="QString" value="collection"/>
</Option>
</data_defined_properties>
<layer id="" class="SimpleFill" locked="0" enabled="1" pass="0">
<Option type="Map">
<Option name="border_width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
<Option name="color" type="QString" value="255,255,255,255,rgb:1,1,1,1"/>
<Option name="joinstyle" type="QString" value="bevel"/>
<Option name="offset" type="QString" value="0,0"/>
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
<Option name="offset_unit" type="QString" value="MM"/>
<Option name="outline_color" type="QString" value="128,128,128,255,rgb:0.50196078431372548,0.50196078431372548,0.50196078431372548,1"/>
<Option name="outline_style" type="QString" value="no"/>
<Option name="outline_width" type="QString" value="0"/>
<Option name="outline_width_unit" type="QString" value="MM"/>
<Option name="style" type="QString" value="solid"/>
</Option>
<data_defined_properties>
<Option type="Map">
<Option name="name" type="QString" value=""/>
<Option name="properties"/>
<Option name="type" type="QString" value="collection"/>
</Option>
</data_defined_properties>
</layer>
</symbol>
</background>
<shadow shadowRadiusAlphaOnly="0" shadowOffsetAngle="135" shadowOpacity="0.69999999999999996" shadowOffsetUnit="MM" shadowColor="0,0,0,255,rgb:0,0,0,1" shadowRadiusMapUnitScale="3x:0,0,0,0,0,0" shadowRadius="1.5" shadowBlendMode="6" shadowScale="100" shadowDraw="0" shadowUnder="0" shadowOffsetDist="1" shadowRadiusUnit="MM" shadowOffsetMapUnitScale="3x:0,0,0,0,0,0" shadowOffsetGlobal="1"/>
<dd_properties>
<Option type="Map">
<Option name="name" type="QString" value=""/>
<Option name="properties"/>
<Option name="type" type="QString" value="collection"/>
</Option>
</dd_properties>
<substitutions/>
</text-style>
<text-format multilineAlign="3" autoWrapLength="0" reverseDirectionSymbol="0" plussign="0" rightDirectionSymbol=">" addDirectionSymbol="0" leftDirectionSymbol="&lt;" formatNumbers="0" decimals="3" placeDirectionSymbol="0" wrapChar="" useMaxLineLengthForAutoWrap="1"/>
<placement lineAnchorClipping="0" maximumDistanceMapUnitScale="3x:0,0,0,0,0,0" xOffset="0" maximumDistance="0" geometryGenerator="" distMapUnitScale="3x:0,0,0,0,0,0" predefinedPositionOrder="TR,TL,BR,BL,R,L,TSR,BSR" layerType="PolygonGeometry" lineAnchorPercent="0.5" rotationUnit="AngleDegrees" repeatDistanceMapUnitScale="3x:0,0,0,0,0,0" priority="5" yOffset="0" quadOffset="4" polygonPlacementFlags="2" centroidInside="1" placement="1" placementFlags="10" prioritization="PreferCloser" overrunDistance="0" offsetUnits="MM" maxCurvedCharAngleOut="-25" preserveRotation="1" lineAnchorTextPoint="CenterOfText" overlapHandling="PreventOverlap" rotationAngle="0" repeatDistance="0" offsetType="0" distUnits="MM" allowDegraded="0" overrunDistanceUnit="MM" centroidWhole="0" geometryGeneratorType="PointGeometry" fitInPolygonOnly="0" geometryGeneratorEnabled="0" dist="0" maxCurvedCharAngleIn="25" overrunDistanceMapUnitScale="3x:0,0,0,0,0,0" maximumDistanceUnit="MM" lineAnchorType="0" repeatDistanceUnits="MM" labelOffsetMapUnitScale="3x:0,0,0,0,0,0"/>
<rendering fontMinPixelSize="3" fontMaxPixelSize="10000" maxNumLabels="2000" obstacleFactor="1" mergeLines="0" fontLimitPixelSize="0" limitNumLabels="0" minFeatureSize="0" scaleMin="0" scaleVisibility="0" labelPerPart="0" scaleMax="0" obstacleType="1" upsidedownLabels="0" drawLabels="1" zIndex="0" unplacedVisibility="0" obstacle="1"/>
<dd_properties>
<Option type="Map">
<Option name="name" type="QString" value=""/>
<Option name="properties" type="Map">
<Option name="FontStyle" type="Map">
<Option name="active" type="bool" value="false"/>
<Option name="field" type="QString" value="flstnrzae"/>
<Option name="type" type="int" value="2"/>
</Option>
<Option name="Underline" type="Map">
<Option name="active" type="bool" value="false"/>
<Option name="field" type="QString" value="flstnrzae"/>
<Option name="type" type="int" value="2"/>
</Option>
</Option>
<Option name="type" type="QString" value="collection"/>
</Option>
</dd_properties>
<callout type="simple">
<Option type="Map">
<Option name="anchorPoint" type="QString" value="pole_of_inaccessibility"/>
<Option name="blendMode" type="int" value="0"/>
<Option name="ddProperties" type="Map">
<Option name="name" type="QString" value=""/>
<Option name="properties"/>
<Option name="type" type="QString" value="collection"/>
</Option>
<Option name="drawToAllParts" type="bool" value="false"/>
<Option name="enabled" type="QString" value="0"/>
<Option name="labelAnchorPoint" type="QString" value="point_on_exterior"/>
<Option name="lineSymbol" type="QString" value="&lt;symbol name=&quot;symbol&quot; type=&quot;line&quot; force_rhr=&quot;0&quot; clip_to_extent=&quot;1&quot; is_animated=&quot;0&quot; frame_rate=&quot;10&quot; alpha=&quot;1&quot;>&lt;data_defined_properties>&lt;Option type=&quot;Map&quot;>&lt;Option name=&quot;name&quot; type=&quot;QString&quot; value=&quot;&quot;/>&lt;Option name=&quot;properties&quot;/>&lt;Option name=&quot;type&quot; type=&quot;QString&quot; value=&quot;collection&quot;/>&lt;/Option>&lt;/data_defined_properties>&lt;layer id=&quot;{0a6da4bc-e8f1-4ec2-8062-844ead072d33}&quot; class=&quot;SimpleLine&quot; locked=&quot;0&quot; enabled=&quot;1&quot; pass=&quot;0&quot;>&lt;Option type=&quot;Map&quot;>&lt;Option name=&quot;align_dash_pattern&quot; type=&quot;QString&quot; value=&quot;0&quot;/>&lt;Option name=&quot;capstyle&quot; type=&quot;QString&quot; value=&quot;square&quot;/>&lt;Option name=&quot;customdash&quot; type=&quot;QString&quot; value=&quot;5;2&quot;/>&lt;Option name=&quot;customdash_map_unit_scale&quot; type=&quot;QString&quot; value=&quot;3x:0,0,0,0,0,0&quot;/>&lt;Option name=&quot;customdash_unit&quot; type=&quot;QString&quot; value=&quot;MM&quot;/>&lt;Option name=&quot;dash_pattern_offset&quot; type=&quot;QString&quot; value=&quot;0&quot;/>&lt;Option name=&quot;dash_pattern_offset_map_unit_scale&quot; type=&quot;QString&quot; value=&quot;3x:0,0,0,0,0,0&quot;/>&lt;Option name=&quot;dash_pattern_offset_unit&quot; type=&quot;QString&quot; value=&quot;MM&quot;/>&lt;Option name=&quot;draw_inside_polygon&quot; type=&quot;QString&quot; value=&quot;0&quot;/>&lt;Option name=&quot;joinstyle&quot; type=&quot;QString&quot; value=&quot;bevel&quot;/>&lt;Option name=&quot;line_color&quot; type=&quot;QString&quot; value=&quot;60,60,60,255,rgb:0.23529411764705882,0.23529411764705882,0.23529411764705882,1&quot;/>&lt;Option name=&quot;line_style&quot; type=&quot;QString&quot; value=&quot;solid&quot;/>&lt;Option name=&quot;line_width&quot; type=&quot;QString&quot; value=&quot;0.3&quot;/>&lt;Option name=&quot;line_width_unit&quot; type=&quot;QString&quot; value=&quot;MM&quot;/>&lt;Option name=&quot;offset&quot; type=&quot;QString&quot; value=&quot;0&quot;/>&lt;Option name=&quot;offset_map_unit_scale&quot; type=&quot;QString&quot; value=&quot;3x:0,0,0,0,0,0&quot;/>&lt;Option name=&quot;offset_unit&quot; type=&quot;QString&quot; value=&quot;MM&quot;/>&lt;Option name=&quot;ring_filter&quot; type=&quot;QString&quot; value=&quot;0&quot;/>&lt;Option name=&quot;trim_distance_end&quot; type=&quot;QString&quot; value=&quot;0&quot;/>&lt;Option name=&quot;trim_distance_end_map_unit_scale&quot; type=&quot;QString&quot; value=&quot;3x:0,0,0,0,0,0&quot;/>&lt;Option name=&quot;trim_distance_end_unit&quot; type=&quot;QString&quot; value=&quot;MM&quot;/>&lt;Option name=&quot;trim_distance_start&quot; type=&quot;QString&quot; value=&quot;0&quot;/>&lt;Option name=&quot;trim_distance_start_map_unit_scale&quot; type=&quot;QString&quot; value=&quot;3x:0,0,0,0,0,0&quot;/>&lt;Option name=&quot;trim_distance_start_unit&quot; type=&quot;QString&quot; value=&quot;MM&quot;/>&lt;Option name=&quot;tweak_dash_pattern_on_corners&quot; type=&quot;QString&quot; value=&quot;0&quot;/>&lt;Option name=&quot;use_custom_dash&quot; type=&quot;QString&quot; value=&quot;0&quot;/>&lt;Option name=&quot;width_map_unit_scale&quot; type=&quot;QString&quot; value=&quot;3x:0,0,0,0,0,0&quot;/>&lt;/Option>&lt;data_defined_properties>&lt;Option type=&quot;Map&quot;>&lt;Option name=&quot;name&quot; type=&quot;QString&quot; value=&quot;&quot;/>&lt;Option name=&quot;properties&quot;/>&lt;Option name=&quot;type&quot; type=&quot;QString&quot; value=&quot;collection&quot;/>&lt;/Option>&lt;/data_defined_properties>&lt;/layer>&lt;/symbol>"/>
<Option name="minLength" type="double" value="0"/>
<Option name="minLengthMapUnitScale" type="QString" value="3x:0,0,0,0,0,0"/>
<Option name="minLengthUnit" type="QString" value="MM"/>
<Option name="offsetFromAnchor" type="double" value="0"/>
<Option name="offsetFromAnchorMapUnitScale" type="QString" value="3x:0,0,0,0,0,0"/>
<Option name="offsetFromAnchorUnit" type="QString" value="MM"/>
<Option name="offsetFromLabel" type="double" value="0"/>
<Option name="offsetFromLabelMapUnitScale" type="QString" value="3x:0,0,0,0,0,0"/>
<Option name="offsetFromLabelUnit" type="QString" value="MM"/>
</Option>
</callout>
</settings>
</rule>
</rules>
</labeling>
<blendMode>0</blendMode>
<featureBlendMode>0</featureBlendMode>
<layerGeometryType>2</layerGeometryType>
</qgis>

371
styles/GIS_LfULG_LSG.qml Normal file
View File

@@ -0,0 +1,371 @@
<!DOCTYPE qgis PUBLIC 'http://mrcc.com/qgis.dtd' 'SYSTEM'>
<qgis styleCategories="Symbology" version="3.40.7-Bratislava">
<renderer-v2 type="singleSymbol" forceraster="0" enableorderby="0" symbollevels="0" referencescale="-1">
<symbols>
<symbol type="fill" force_rhr="0" is_animated="0" frame_rate="10" name="0" alpha="1" clip_to_extent="1">
<data_defined_properties>
<Option type="Map">
<Option type="QString" value="" name="name"/>
<Option name="properties"/>
<Option type="QString" value="collection" name="type"/>
</Option>
</data_defined_properties>
<layer class="MarkerLine" id="{edf1fe5f-96c0-427c-ac7b-b04c95e0da9c}" enabled="1" locked="0" pass="0">
<Option type="Map">
<Option type="QString" value="4" name="average_angle_length"/>
<Option type="QString" value="3x:0,0,0,0,0,0" name="average_angle_map_unit_scale"/>
<Option type="QString" value="MM" name="average_angle_unit"/>
<Option type="QString" value="5" name="interval"/>
<Option type="QString" value="3x:0,0,0,0,0,0" name="interval_map_unit_scale"/>
<Option type="QString" value="MM" name="interval_unit"/>
<Option type="QString" value="0" name="offset"/>
<Option type="QString" value="0" name="offset_along_line"/>
<Option type="QString" value="3x:0,0,0,0,0,0" name="offset_along_line_map_unit_scale"/>
<Option type="QString" value="MM" name="offset_along_line_unit"/>
<Option type="QString" value="3x:0,0,0,0,0,0" name="offset_map_unit_scale"/>
<Option type="QString" value="Pixel" name="offset_unit"/>
<Option type="bool" value="true" name="place_on_every_part"/>
<Option type="QString" value="Interval" name="placements"/>
<Option type="QString" value="1" name="ring_filter"/>
<Option type="QString" value="1" name="rotate"/>
</Option>
<data_defined_properties>
<Option type="Map">
<Option type="QString" value="" name="name"/>
<Option name="properties"/>
<Option type="QString" value="collection" name="type"/>
</Option>
</data_defined_properties>
<symbol type="marker" force_rhr="0" is_animated="0" frame_rate="10" name="@0@0" alpha="1" clip_to_extent="1">
<data_defined_properties>
<Option type="Map">
<Option type="QString" value="" name="name"/>
<Option name="properties"/>
<Option type="QString" value="collection" name="type"/>
</Option>
</data_defined_properties>
<layer class="SimpleMarker" id="{857cb357-9b68-40ea-9e9f-edbdbd7de638}" enabled="1" locked="0" pass="0">
<Option type="Map">
<Option type="QString" value="90" name="angle"/>
<Option type="QString" value="square" name="cap_style"/>
<Option type="QString" value="255,0,0,255,rgb:1,0,0,1" name="color"/>
<Option type="QString" value="1" name="horizontal_anchor_point"/>
<Option type="QString" value="bevel" name="joinstyle"/>
<Option type="QString" value="line" name="name"/>
<Option type="QString" value="1.99999999999999978,-1.5" name="offset"/>
<Option type="QString" value="3x:0,0,0,0,0,0" name="offset_map_unit_scale"/>
<Option type="QString" value="MM" name="offset_unit"/>
<Option type="QString" value="128,128,128,255,rgb:0.50196078431372548,0.50196078431372548,0.50196078431372548,1" name="outline_color"/>
<Option type="QString" value="solid" name="outline_style"/>
<Option type="QString" value="0.5" name="outline_width"/>
<Option type="QString" value="3x:0,0,0,0,0,0" name="outline_width_map_unit_scale"/>
<Option type="QString" value="MM" name="outline_width_unit"/>
<Option type="QString" value="diameter" name="scale_method"/>
<Option type="QString" value="3" name="size"/>
<Option type="QString" value="3x:0,0,0,0,0,0" name="size_map_unit_scale"/>
<Option type="QString" value="MM" name="size_unit"/>
<Option type="QString" value="1" name="vertical_anchor_point"/>
</Option>
<data_defined_properties>
<Option type="Map">
<Option type="QString" value="" name="name"/>
<Option name="properties"/>
<Option type="QString" value="collection" name="type"/>
</Option>
</data_defined_properties>
</layer>
<layer class="SimpleMarker" id="{8da6ec0d-bd1d-4942-8241-be9e412a4ecd}" enabled="1" locked="0" pass="0">
<Option type="Map">
<Option type="QString" value="0" name="angle"/>
<Option type="QString" value="square" name="cap_style"/>
<Option type="QString" value="128,128,128,255,rgb:0.50196078431372548,0.50196078431372548,0.50196078431372548,1" name="color"/>
<Option type="QString" value="1" name="horizontal_anchor_point"/>
<Option type="QString" value="bevel" name="joinstyle"/>
<Option type="QString" value="line" name="name"/>
<Option type="QString" value="3,1" name="offset"/>
<Option type="QString" value="3x:0,0,0,0,0,0" name="offset_map_unit_scale"/>
<Option type="QString" value="MM" name="offset_unit"/>
<Option type="QString" value="128,128,128,255,rgb:0.50196078431372548,0.50196078431372548,0.50196078431372548,1" name="outline_color"/>
<Option type="QString" value="solid" name="outline_style"/>
<Option type="QString" value="0.5" name="outline_width"/>
<Option type="QString" value="3x:0,0,0,0,0,0" name="outline_width_map_unit_scale"/>
<Option type="QString" value="MM" name="outline_width_unit"/>
<Option type="QString" value="diameter" name="scale_method"/>
<Option type="QString" value="2" name="size"/>
<Option type="QString" value="3x:0,0,0,0,0,0" name="size_map_unit_scale"/>
<Option type="QString" value="MM" name="size_unit"/>
<Option type="QString" value="1" name="vertical_anchor_point"/>
</Option>
<data_defined_properties>
<Option type="Map">
<Option type="QString" value="" name="name"/>
<Option name="properties"/>
<Option type="QString" value="collection" name="type"/>
</Option>
</data_defined_properties>
</layer>
<layer class="SimpleMarker" id="{cf06f0f3-27d8-4b64-b56f-6bda0ed8508c}" enabled="1" locked="0" pass="0">
<Option type="Map">
<Option type="QString" value="0" name="angle"/>
<Option type="QString" value="square" name="cap_style"/>
<Option type="QString" value="255,0,0,255,rgb:1,0,0,1" name="color"/>
<Option type="QString" value="1" name="horizontal_anchor_point"/>
<Option type="QString" value="bevel" name="joinstyle"/>
<Option type="QString" value="line" name="name"/>
<Option type="QString" value="2,1" name="offset"/>
<Option type="QString" value="3x:0,0,0,0,0,0" name="offset_map_unit_scale"/>
<Option type="QString" value="MM" name="offset_unit"/>
<Option type="QString" value="128,128,128,255,rgb:0.50196078431372548,0.50196078431372548,0.50196078431372548,1" name="outline_color"/>
<Option type="QString" value="solid" name="outline_style"/>
<Option type="QString" value="0.5" name="outline_width"/>
<Option type="QString" value="3x:0,0,0,0,0,0" name="outline_width_map_unit_scale"/>
<Option type="QString" value="MM" name="outline_width_unit"/>
<Option type="QString" value="diameter" name="scale_method"/>
<Option type="QString" value="2" name="size"/>
<Option type="QString" value="3x:0,0,0,0,0,0" name="size_map_unit_scale"/>
<Option type="QString" value="MM" name="size_unit"/>
<Option type="QString" value="1" name="vertical_anchor_point"/>
</Option>
<data_defined_properties>
<Option type="Map">
<Option type="QString" value="" name="name"/>
<Option name="properties"/>
<Option type="QString" value="collection" name="type"/>
</Option>
</data_defined_properties>
</layer>
<layer class="SimpleMarker" id="{0f1b9591-1b5b-4e8a-8353-c0a7caaf7b21}" enabled="1" locked="0" pass="0">
<Option type="Map">
<Option type="QString" value="0" name="angle"/>
<Option type="QString" value="square" name="cap_style"/>
<Option type="QString" value="128,128,128,255,rgb:0.50196078431372548,0.50196078431372548,0.50196078431372548,1" name="color"/>
<Option type="QString" value="1" name="horizontal_anchor_point"/>
<Option type="QString" value="bevel" name="joinstyle"/>
<Option type="QString" value="line" name="name"/>
<Option type="QString" value="1,1" name="offset"/>
<Option type="QString" value="3x:0,0,0,0,0,0" name="offset_map_unit_scale"/>
<Option type="QString" value="MM" name="offset_unit"/>
<Option type="QString" value="128,128,128,255,rgb:0.50196078431372548,0.50196078431372548,0.50196078431372548,1" name="outline_color"/>
<Option type="QString" value="solid" name="outline_style"/>
<Option type="QString" value="0.5" name="outline_width"/>
<Option type="QString" value="3x:0,0,0,0,0,0" name="outline_width_map_unit_scale"/>
<Option type="QString" value="MM" name="outline_width_unit"/>
<Option type="QString" value="diameter" name="scale_method"/>
<Option type="QString" value="2" name="size"/>
<Option type="QString" value="3x:0,0,0,0,0,0" name="size_map_unit_scale"/>
<Option type="QString" value="MM" name="size_unit"/>
<Option type="QString" value="1" name="vertical_anchor_point"/>
</Option>
<data_defined_properties>
<Option type="Map">
<Option type="QString" value="" name="name"/>
<Option name="properties"/>
<Option type="QString" value="collection" name="type"/>
</Option>
</data_defined_properties>
</layer>
<layer class="SimpleMarker" id="{c9ae8a84-1a7d-4599-b27d-37bc1484098b}" enabled="1" locked="0" pass="0">
<Option type="Map">
<Option type="QString" value="0" name="angle"/>
<Option type="QString" value="square" name="cap_style"/>
<Option type="QString" value="128,128,128,255,rgb:0.50196078431372548,0.50196078431372548,0.50196078431372548,1" name="color"/>
<Option type="QString" value="1" name="horizontal_anchor_point"/>
<Option type="QString" value="bevel" name="joinstyle"/>
<Option type="QString" value="line" name="name"/>
<Option type="QString" value="0,1" name="offset"/>
<Option type="QString" value="3x:0,0,0,0,0,0" name="offset_map_unit_scale"/>
<Option type="QString" value="MM" name="offset_unit"/>
<Option type="QString" value="128,128,128,255,rgb:0.50196078431372548,0.50196078431372548,0.50196078431372548,1" name="outline_color"/>
<Option type="QString" value="solid" name="outline_style"/>
<Option type="QString" value="0.5" name="outline_width"/>
<Option type="QString" value="3x:0,0,0,0,0,0" name="outline_width_map_unit_scale"/>
<Option type="QString" value="MM" name="outline_width_unit"/>
<Option type="QString" value="diameter" name="scale_method"/>
<Option type="QString" value="2" name="size"/>
<Option type="QString" value="3x:0,0,0,0,0,0" name="size_map_unit_scale"/>
<Option type="QString" value="MM" name="size_unit"/>
<Option type="QString" value="1" name="vertical_anchor_point"/>
</Option>
<data_defined_properties>
<Option type="Map">
<Option type="QString" value="" name="name"/>
<Option name="properties"/>
<Option type="QString" value="collection" name="type"/>
</Option>
</data_defined_properties>
</layer>
</symbol>
</layer>
<layer class="SimpleLine" id="{fd1fd75a-85c6-4ace-a1bc-300a87cb1bcf}" enabled="1" locked="0" pass="0">
<Option type="Map">
<Option type="QString" value="0" name="align_dash_pattern"/>
<Option type="QString" value="square" name="capstyle"/>
<Option type="QString" value="5;2" name="customdash"/>
<Option type="QString" value="3x:0,0,0,0,0,0" name="customdash_map_unit_scale"/>
<Option type="QString" value="MM" name="customdash_unit"/>
<Option type="QString" value="0" name="dash_pattern_offset"/>
<Option type="QString" value="3x:0,0,0,0,0,0" name="dash_pattern_offset_map_unit_scale"/>
<Option type="QString" value="MM" name="dash_pattern_offset_unit"/>
<Option type="QString" value="0" name="draw_inside_polygon"/>
<Option type="QString" value="bevel" name="joinstyle"/>
<Option type="QString" value="128,128,128,255,rgb:0.50196078431372548,0.50196078431372548,0.50196078431372548,1" name="line_color"/>
<Option type="QString" value="solid" name="line_style"/>
<Option type="QString" value="0.5" name="line_width"/>
<Option type="QString" value="MM" name="line_width_unit"/>
<Option type="QString" value="0" name="offset"/>
<Option type="QString" value="3x:0,0,0,0,0,0" name="offset_map_unit_scale"/>
<Option type="QString" value="MM" name="offset_unit"/>
<Option type="QString" value="0" name="ring_filter"/>
<Option type="QString" value="0" name="trim_distance_end"/>
<Option type="QString" value="3x:0,0,0,0,0,0" name="trim_distance_end_map_unit_scale"/>
<Option type="QString" value="MM" name="trim_distance_end_unit"/>
<Option type="QString" value="0" name="trim_distance_start"/>
<Option type="QString" value="3x:0,0,0,0,0,0" name="trim_distance_start_map_unit_scale"/>
<Option type="QString" value="MM" name="trim_distance_start_unit"/>
<Option type="QString" value="0" name="tweak_dash_pattern_on_corners"/>
<Option type="QString" value="0" name="use_custom_dash"/>
<Option type="QString" value="3x:0,0,0,0,0,0" name="width_map_unit_scale"/>
</Option>
<data_defined_properties>
<Option type="Map">
<Option type="QString" value="" name="name"/>
<Option name="properties"/>
<Option type="QString" value="collection" name="type"/>
</Option>
</data_defined_properties>
</layer>
<layer class="CentroidFill" id="{801668fb-6226-4c79-a72a-ec430781b326}" enabled="1" locked="0" pass="0">
<Option type="Map">
<Option type="QString" value="1" name="clip_on_current_part_only"/>
<Option type="QString" value="1" name="clip_points"/>
<Option type="QString" value="1" name="point_on_all_parts"/>
<Option type="QString" value="1" name="point_on_surface"/>
</Option>
<data_defined_properties>
<Option type="Map">
<Option type="QString" value="" name="name"/>
<Option name="properties"/>
<Option type="QString" value="collection" name="type"/>
</Option>
</data_defined_properties>
<symbol type="marker" force_rhr="0" is_animated="0" frame_rate="10" name="@0@2" alpha="1" clip_to_extent="1">
<data_defined_properties>
<Option type="Map">
<Option type="QString" value="" name="name"/>
<Option name="properties"/>
<Option type="QString" value="collection" name="type"/>
</Option>
</data_defined_properties>
<layer class="SimpleMarker" id="{d075cd2b-6c94-49c8-b6a6-61d75f00fbc5}" enabled="1" locked="0" pass="0">
<Option type="Map">
<Option type="QString" value="0" name="angle"/>
<Option type="QString" value="square" name="cap_style"/>
<Option type="QString" value="255,0,0,0,rgb:1,0,0,0" name="color"/>
<Option type="QString" value="1" name="horizontal_anchor_point"/>
<Option type="QString" value="bevel" name="joinstyle"/>
<Option type="QString" value="circle" name="name"/>
<Option type="QString" value="0,0" name="offset"/>
<Option type="QString" value="3x:0,0,0,0,0,0" name="offset_map_unit_scale"/>
<Option type="QString" value="MapUnit" name="offset_unit"/>
<Option type="QString" value="0,0,0,255,rgb:0,0,0,1" name="outline_color"/>
<Option type="QString" value="solid" name="outline_style"/>
<Option type="QString" value="0.5" name="outline_width"/>
<Option type="QString" value="3x:0,0,0,0,0,0" name="outline_width_map_unit_scale"/>
<Option type="QString" value="MM" name="outline_width_unit"/>
<Option type="QString" value="diameter" name="scale_method"/>
<Option type="QString" value="15" name="size"/>
<Option type="QString" value="3x:0,0,0,0,0,0" name="size_map_unit_scale"/>
<Option type="QString" value="MM" name="size_unit"/>
<Option type="QString" value="1" name="vertical_anchor_point"/>
</Option>
<data_defined_properties>
<Option type="Map">
<Option type="QString" value="" name="name"/>
<Option name="properties"/>
<Option type="QString" value="collection" name="type"/>
</Option>
</data_defined_properties>
</layer>
<layer class="FontMarker" id="{d6023cfc-3bfd-46de-9557-e37b755164f3}" enabled="1" locked="0" pass="0">
<Option type="Map">
<Option type="QString" value="0" name="angle"/>
<Option type="QString" value="L" name="chr"/>
<Option type="QString" value="0,0,0,255,rgb:0,0,0,1" name="color"/>
<Option type="QString" value="Arial" name="font"/>
<Option type="QString" value="Standard" name="font_style"/>
<Option type="QString" value="1" name="horizontal_anchor_point"/>
<Option type="QString" value="bevel" name="joinstyle"/>
<Option type="QString" value="0,0" name="offset"/>
<Option type="QString" value="3x:0,0,0,0,0,0" name="offset_map_unit_scale"/>
<Option type="QString" value="MapUnit" name="offset_unit"/>
<Option type="QString" value="35,35,35,255,rgb:0.13725490196078433,0.13725490196078433,0.13725490196078433,1" name="outline_color"/>
<Option type="QString" value="0" name="outline_width"/>
<Option type="QString" value="3x:0,0,0,0,0,0" name="outline_width_map_unit_scale"/>
<Option type="QString" value="MM" name="outline_width_unit"/>
<Option type="QString" value="5" name="size"/>
<Option type="QString" value="3x:0,0,0,0,0,0" name="size_map_unit_scale"/>
<Option type="QString" value="MM" name="size_unit"/>
<Option type="QString" value="1" name="vertical_anchor_point"/>
</Option>
<data_defined_properties>
<Option type="Map">
<Option type="QString" value="" name="name"/>
<Option name="properties"/>
<Option type="QString" value="collection" name="type"/>
</Option>
</data_defined_properties>
</layer>
</symbol>
</layer>
</symbol>
</symbols>
<rotation/>
<sizescale/>
<data-defined-properties>
<Option type="Map">
<Option type="QString" value="" name="name"/>
<Option name="properties"/>
<Option type="QString" value="collection" name="type"/>
</Option>
</data-defined-properties>
</renderer-v2>
<selection mode="Default">
<selectionColor invalid="1"/>
<selectionSymbol>
<symbol type="fill" force_rhr="0" is_animated="0" frame_rate="10" name="" alpha="1" clip_to_extent="1">
<data_defined_properties>
<Option type="Map">
<Option type="QString" value="" name="name"/>
<Option name="properties"/>
<Option type="QString" value="collection" name="type"/>
</Option>
</data_defined_properties>
<layer class="SimpleFill" id="{790c0e9c-4587-46ac-845d-d8d4c15b81b7}" enabled="1" locked="0" pass="0">
<Option type="Map">
<Option type="QString" value="3x:0,0,0,0,0,0" name="border_width_map_unit_scale"/>
<Option type="QString" value="0,0,255,255,rgb:0,0,1,1" name="color"/>
<Option type="QString" value="bevel" name="joinstyle"/>
<Option type="QString" value="0,0" name="offset"/>
<Option type="QString" value="3x:0,0,0,0,0,0" name="offset_map_unit_scale"/>
<Option type="QString" value="MM" name="offset_unit"/>
<Option type="QString" value="35,35,35,255,rgb:0.13725490196078433,0.13725490196078433,0.13725490196078433,1" name="outline_color"/>
<Option type="QString" value="solid" name="outline_style"/>
<Option type="QString" value="0.26" name="outline_width"/>
<Option type="QString" value="MM" name="outline_width_unit"/>
<Option type="QString" value="solid" name="style"/>
</Option>
<data_defined_properties>
<Option type="Map">
<Option type="QString" value="" name="name"/>
<Option name="properties"/>
<Option type="QString" value="collection" name="type"/>
</Option>
</data_defined_properties>
</layer>
</symbol>
</selectionSymbol>
</selection>
<blendMode>0</blendMode>
<featureBlendMode>0</featureBlendMode>
<layerGeometryType>2</layerGeometryType>
</qgis>

View File

@@ -1,25 +1,83 @@
<!DOCTYPE qgis PUBLIC 'http://mrcc.com/qgis.dtd' 'SYSTEM'> <!DOCTYPE qgis PUBLIC 'http://mrcc.com/qgis.dtd' 'SYSTEM'>
<qgis maxScale="0" simplifyDrawingHints="1" simplifyDrawingTol="1" styleCategories="AllStyleCategories" version="3.10.8-A Coruña" simplifyMaxScale="1" simplifyLocal="1" readOnly="0" hasScaleBasedVisibilityFlag="0" minScale="1e+08" simplifyAlgorithm="0" labelsEnabled="1"> <qgis version="3.40.7-Bratislava" styleCategories="Symbology">
<flags> <renderer-v2 referencescale="-1" forceraster="0" enableorderby="0" type="singleSymbol" symbollevels="0">
<Identifiable>1</Identifiable>
<Removable>1</Removable>
<Searchable>1</Searchable>
</flags>
<renderer-v2 type="singleSymbol" forceraster="0" symbollevels="0" enableorderby="0">
<symbols> <symbols>
<symbol alpha="1" type="fill" clip_to_extent="1" name="0" force_rhr="0"> <symbol is_animated="0" frame_rate="10" clip_to_extent="1" type="fill" alpha="1" force_rhr="0" name="0">
<layer class="SimpleFill" locked="0" pass="0" enabled="1"> <data_defined_properties>
<prop k="border_width_map_unit_scale" v="3x:0,0,0,0,0,0"/> <Option type="Map">
<prop k="color" v="255,255,153,173"/> <Option type="QString" value="" name="name"/>
<prop k="joinstyle" v="miter"/> <Option name="properties"/>
<prop k="offset" v="0,0"/> <Option type="QString" value="collection" name="type"/>
<prop k="offset_map_unit_scale" v="3x:0,0,0,0,0,0"/> </Option>
<prop k="offset_unit" v="MM"/> </data_defined_properties>
<prop k="outline_color" v="161,2,213,255"/> <layer locked="0" id="{feca00b2-500a-4c9a-b285-67ba2d99d8f6}" enabled="1" class="SimpleLine" pass="0">
<prop k="outline_style" v="solid"/> <Option type="Map">
<prop k="outline_width" v="1"/> <Option type="QString" value="0" name="align_dash_pattern"/>
<prop k="outline_width_unit" v="MM"/> <Option type="QString" value="square" name="capstyle"/>
<prop k="style" v="solid"/> <Option type="QString" value="5;2" name="customdash"/>
<Option type="QString" value="3x:0,0,0,0,0,0" name="customdash_map_unit_scale"/>
<Option type="QString" value="MM" name="customdash_unit"/>
<Option type="QString" value="0" name="dash_pattern_offset"/>
<Option type="QString" value="3x:0,0,0,0,0,0" name="dash_pattern_offset_map_unit_scale"/>
<Option type="QString" value="MM" name="dash_pattern_offset_unit"/>
<Option type="QString" value="0" name="draw_inside_polygon"/>
<Option type="QString" value="round" name="joinstyle"/>
<Option type="QString" value="215,168,255,255,rgb:0.84313725490196079,0.6588235294117647,1,1" name="line_color"/>
<Option type="QString" value="solid" name="line_style"/>
<Option type="QString" value="1.5" name="line_width"/>
<Option type="QString" value="MM" name="line_width_unit"/>
<Option type="QString" value="-0.75" name="offset"/>
<Option type="QString" value="3x:0,0,0,0,0,0" name="offset_map_unit_scale"/>
<Option type="QString" value="MM" name="offset_unit"/>
<Option type="QString" value="0" name="ring_filter"/>
<Option type="QString" value="0" name="trim_distance_end"/>
<Option type="QString" value="3x:0,0,0,0,0,0" name="trim_distance_end_map_unit_scale"/>
<Option type="QString" value="MM" name="trim_distance_end_unit"/>
<Option type="QString" value="0" name="trim_distance_start"/>
<Option type="QString" value="3x:0,0,0,0,0,0" name="trim_distance_start_map_unit_scale"/>
<Option type="QString" value="MM" name="trim_distance_start_unit"/>
<Option type="QString" value="0" name="tweak_dash_pattern_on_corners"/>
<Option type="QString" value="0" name="use_custom_dash"/>
<Option type="QString" value="3x:0,0,0,0,0,0" name="width_map_unit_scale"/>
</Option>
<data_defined_properties>
<Option type="Map">
<Option type="QString" value="" name="name"/>
<Option name="properties"/>
<Option type="QString" value="collection" name="type"/>
</Option>
</data_defined_properties>
</layer>
<layer locked="0" id="{fdc4d6fd-0995-41df-bbfb-19970b4fc2cc}" enabled="1" class="SimpleLine" pass="0">
<Option type="Map">
<Option type="QString" value="0" name="align_dash_pattern"/>
<Option type="QString" value="square" name="capstyle"/>
<Option type="QString" value="5;2" name="customdash"/>
<Option type="QString" value="3x:0,0,0,0,0,0" name="customdash_map_unit_scale"/>
<Option type="QString" value="MM" name="customdash_unit"/>
<Option type="QString" value="0" name="dash_pattern_offset"/>
<Option type="QString" value="3x:0,0,0,0,0,0" name="dash_pattern_offset_map_unit_scale"/>
<Option type="QString" value="MM" name="dash_pattern_offset_unit"/>
<Option type="QString" value="0" name="draw_inside_polygon"/>
<Option type="QString" value="round" name="joinstyle"/>
<Option type="QString" value="204,174,137,255,rgb:0.80000000000000004,0.68235294117647061,0.53725490196078429,1" name="line_color"/>
<Option type="QString" value="solid" name="line_style"/>
<Option type="QString" value="0.5" name="line_width"/>
<Option type="QString" value="MM" name="line_width_unit"/>
<Option type="QString" value="0" name="offset"/>
<Option type="QString" value="3x:0,0,0,0,0,0" name="offset_map_unit_scale"/>
<Option type="QString" value="MM" name="offset_unit"/>
<Option type="QString" value="0" name="ring_filter"/>
<Option type="QString" value="0" name="trim_distance_end"/>
<Option type="QString" value="3x:0,0,0,0,0,0" name="trim_distance_end_map_unit_scale"/>
<Option type="QString" value="MM" name="trim_distance_end_unit"/>
<Option type="QString" value="0" name="trim_distance_start"/>
<Option type="QString" value="3x:0,0,0,0,0,0" name="trim_distance_start_map_unit_scale"/>
<Option type="QString" value="MM" name="trim_distance_start_unit"/>
<Option type="QString" value="0" name="tweak_dash_pattern_on_corners"/>
<Option type="QString" value="0" name="use_custom_dash"/>
<Option type="QString" value="3x:0,0,0,0,0,0" name="width_map_unit_scale"/>
</Option>
<data_defined_properties> <data_defined_properties>
<Option type="Map"> <Option type="Map">
<Option type="QString" value="" name="name"/> <Option type="QString" value="" name="name"/>
@@ -32,285 +90,51 @@
</symbols> </symbols>
<rotation/> <rotation/>
<sizescale/> <sizescale/>
</renderer-v2> <data-defined-properties>
<labeling type="simple">
<settings calloutType="simple">
<text-style fontFamily="MS Shell Dlg 2" fontSize="8.25" previewBkgrdColor="255,255,255,255" textOrientation="horizontal" fieldName="Name" textColor="0,0,0,255" fontItalic="0" fontUnderline="0" fontSizeUnit="Point" fontCapitals="0" isExpression="0" fontStrikeout="0" fontWeight="50" fontLetterSpacing="0" fontSizeMapUnitScale="3x:0,0,0,0,0,0" fontWordSpacing="0" textOpacity="1" useSubstitutions="0" fontKerning="1" blendMode="0" namedStyle="Standard" multilineHeight="1">
<text-buffer bufferSizeMapUnitScale="3x:0,0,0,0,0,0" bufferJoinStyle="128" bufferSizeUnits="MM" bufferSize="1" bufferDraw="0" bufferColor="255,255,255,255" bufferNoFill="0" bufferBlendMode="0" bufferOpacity="1"/>
<background shapeRadiiMapUnitScale="3x:0,0,0,0,0,0" shapeRotation="0" shapeSizeType="0" shapeOffsetX="0" shapeBlendMode="0" shapeFillColor="255,255,255,255" shapeBorderColor="128,128,128,255" shapeRadiiX="0" shapeRadiiUnit="MM" shapeDraw="0" shapeJoinStyle="64" shapeOffsetUnit="MM" shapeBorderWidthUnit="MM" shapeSizeX="0" shapeSizeUnit="MM" shapeSizeY="0" shapeRadiiY="0" shapeOpacity="1" shapeOffsetY="0" shapeSVGFile="" shapeType="0" shapeBorderWidth="0" shapeRotationType="0" shapeOffsetMapUnitScale="3x:0,0,0,0,0,0" shapeBorderWidthMapUnitScale="3x:0,0,0,0,0,0" shapeSizeMapUnitScale="3x:0,0,0,0,0,0">
<symbol alpha="1" type="marker" clip_to_extent="1" name="markerSymbol" force_rhr="0">
<layer class="SimpleMarker" locked="0" pass="0" enabled="1">
<prop k="angle" v="0"/>
<prop k="color" v="213,180,60,255"/>
<prop k="horizontal_anchor_point" v="1"/>
<prop k="joinstyle" v="bevel"/>
<prop k="name" v="circle"/>
<prop k="offset" v="0,0"/>
<prop k="offset_map_unit_scale" v="3x:0,0,0,0,0,0"/>
<prop k="offset_unit" v="MM"/>
<prop k="outline_color" v="35,35,35,255"/>
<prop k="outline_style" v="solid"/>
<prop k="outline_width" v="0"/>
<prop k="outline_width_map_unit_scale" v="3x:0,0,0,0,0,0"/>
<prop k="outline_width_unit" v="MM"/>
<prop k="scale_method" v="diameter"/>
<prop k="size" v="2"/>
<prop k="size_map_unit_scale" v="3x:0,0,0,0,0,0"/>
<prop k="size_unit" v="MM"/>
<prop k="vertical_anchor_point" v="1"/>
<data_defined_properties>
<Option type="Map">
<Option type="QString" value="" name="name"/>
<Option name="properties"/>
<Option type="QString" value="collection" name="type"/>
</Option>
</data_defined_properties>
</layer>
</symbol>
</background>
<shadow shadowUnder="0" shadowOffsetUnit="MM" shadowScale="100" shadowColor="0,0,0,255" shadowBlendMode="6" shadowOffsetAngle="135" shadowOffsetDist="1" shadowRadiusUnit="MM" shadowOffsetMapUnitScale="3x:0,0,0,0,0,0" shadowRadiusMapUnitScale="3x:0,0,0,0,0,0" shadowOpacity="0.7" shadowDraw="0" shadowRadiusAlphaOnly="0" shadowOffsetGlobal="1" shadowRadius="1.5"/>
<dd_properties>
<Option type="Map">
<Option type="QString" value="" name="name"/>
<Option name="properties"/>
<Option type="QString" value="collection" name="type"/>
</Option>
</dd_properties>
<substitutions/>
</text-style>
<text-format autoWrapLength="0" placeDirectionSymbol="0" leftDirectionSymbol="&lt;" rightDirectionSymbol=">" decimals="3" wrapChar="" useMaxLineLengthForAutoWrap="1" plussign="0" formatNumbers="0" addDirectionSymbol="0" reverseDirectionSymbol="0" multilineAlign="4294967295"/>
<placement xOffset="0" distMapUnitScale="3x:0,0,0,0,0,0" dist="0" repeatDistanceUnits="MM" predefinedPositionOrder="TR,TL,BR,BL,R,L,TSR,BSR" centroidWhole="0" rotationAngle="0" overrunDistanceUnit="MM" placement="1" repeatDistance="0" geometryGeneratorEnabled="0" layerType="PolygonGeometry" offsetUnits="MapUnit" centroidInside="0" offsetType="0" yOffset="0" labelOffsetMapUnitScale="3x:0,0,0,0,0,0" overrunDistance="0" geometryGenerator="" geometryGeneratorType="PointGeometry" placementFlags="10" preserveRotation="1" distUnits="MM" fitInPolygonOnly="0" maxCurvedCharAngleOut="-25" repeatDistanceMapUnitScale="3x:0,0,0,0,0,0" quadOffset="4" maxCurvedCharAngleIn="25" priority="5" overrunDistanceMapUnitScale="3x:0,0,0,0,0,0"/>
<rendering fontLimitPixelSize="0" upsidedownLabels="0" zIndex="0" drawLabels="1" fontMinPixelSize="3" displayAll="0" scaleMax="10000000" labelPerPart="0" fontMaxPixelSize="10000" mergeLines="0" obstacle="1" minFeatureSize="0" obstacleType="0" obstacleFactor="1" scaleMin="1" limitNumLabels="0" scaleVisibility="0" maxNumLabels="2000"/>
<dd_properties>
<Option type="Map">
<Option type="QString" value="" name="name"/>
<Option name="properties"/>
<Option type="QString" value="collection" name="type"/>
</Option>
</dd_properties>
<callout type="simple">
<Option type="Map">
<Option type="QString" value="pole_of_inaccessibility" name="anchorPoint"/>
<Option type="Map" name="ddProperties">
<Option type="QString" value="" name="name"/>
<Option name="properties"/>
<Option type="QString" value="collection" name="type"/>
</Option>
<Option type="bool" value="false" name="drawToAllParts"/>
<Option type="QString" value="0" name="enabled"/>
<Option type="QString" value="&lt;symbol alpha=&quot;1&quot; type=&quot;line&quot; clip_to_extent=&quot;1&quot; name=&quot;symbol&quot; force_rhr=&quot;0&quot;>&lt;layer class=&quot;SimpleLine&quot; locked=&quot;0&quot; pass=&quot;0&quot; enabled=&quot;1&quot;>&lt;prop k=&quot;capstyle&quot; v=&quot;square&quot;/>&lt;prop k=&quot;customdash&quot; v=&quot;5;2&quot;/>&lt;prop k=&quot;customdash_map_unit_scale&quot; v=&quot;3x:0,0,0,0,0,0&quot;/>&lt;prop k=&quot;customdash_unit&quot; v=&quot;MM&quot;/>&lt;prop k=&quot;draw_inside_polygon&quot; v=&quot;0&quot;/>&lt;prop k=&quot;joinstyle&quot; v=&quot;bevel&quot;/>&lt;prop k=&quot;line_color&quot; v=&quot;60,60,60,255&quot;/>&lt;prop k=&quot;line_style&quot; v=&quot;solid&quot;/>&lt;prop k=&quot;line_width&quot; v=&quot;0.3&quot;/>&lt;prop k=&quot;line_width_unit&quot; v=&quot;MM&quot;/>&lt;prop k=&quot;offset&quot; v=&quot;0&quot;/>&lt;prop k=&quot;offset_map_unit_scale&quot; v=&quot;3x:0,0,0,0,0,0&quot;/>&lt;prop k=&quot;offset_unit&quot; v=&quot;MM&quot;/>&lt;prop k=&quot;ring_filter&quot; v=&quot;0&quot;/>&lt;prop k=&quot;use_custom_dash&quot; v=&quot;0&quot;/>&lt;prop k=&quot;width_map_unit_scale&quot; v=&quot;3x:0,0,0,0,0,0&quot;/>&lt;data_defined_properties>&lt;Option type=&quot;Map&quot;>&lt;Option type=&quot;QString&quot; value=&quot;&quot; name=&quot;name&quot;/>&lt;Option name=&quot;properties&quot;/>&lt;Option type=&quot;QString&quot; value=&quot;collection&quot; name=&quot;type&quot;/>&lt;/Option>&lt;/data_defined_properties>&lt;/layer>&lt;/symbol>" name="lineSymbol"/>
<Option type="double" value="0" name="minLength"/>
<Option type="QString" value="3x:0,0,0,0,0,0" name="minLengthMapUnitScale"/>
<Option type="QString" value="MM" name="minLengthUnit"/>
<Option type="double" value="0" name="offsetFromAnchor"/>
<Option type="QString" value="3x:0,0,0,0,0,0" name="offsetFromAnchorMapUnitScale"/>
<Option type="QString" value="MM" name="offsetFromAnchorUnit"/>
<Option type="double" value="0" name="offsetFromLabel"/>
<Option type="QString" value="3x:0,0,0,0,0,0" name="offsetFromLabelMapUnitScale"/>
<Option type="QString" value="MM" name="offsetFromLabelUnit"/>
</Option>
</callout>
</settings>
</labeling>
<customproperties>
<property value="0" key="embeddedWidgets/count"/>
<property key="variableNames"/>
<property key="variableValues"/>
</customproperties>
<blendMode>0</blendMode>
<featureBlendMode>0</featureBlendMode>
<layerOpacity>1</layerOpacity>
<SingleCategoryDiagramRenderer attributeLegend="1" diagramType="Histogram">
<DiagramCategory minimumSize="0" enabled="0" scaleBasedVisibility="0" labelPlacementMethod="XHeight" rotationOffset="270" backgroundColor="#ffffff" width="15" backgroundAlpha="255" sizeType="MM" penWidth="0" penColor="#000000" lineSizeScale="3x:0,0,0,0,0,0" maxScaleDenominator="1e+08" sizeScale="3x:0,0,0,0,0,0" scaleDependency="Area" barWidth="5" lineSizeType="MM" minScaleDenominator="0" height="15" opacity="1" diagramOrientation="Up" penAlpha="255">
<fontProperties description="MS Shell Dlg 2,8.25,-1,5,50,0,0,0,0,0" style=""/>
</DiagramCategory>
</SingleCategoryDiagramRenderer>
<DiagramLayerSettings placement="1" showAll="1" priority="0" zIndex="0" obstacle="0" linePlacementFlags="18" dist="0">
<properties>
<Option type="Map"> <Option type="Map">
<Option type="QString" value="" name="name"/> <Option type="QString" value="" name="name"/>
<Option name="properties"/> <Option name="properties"/>
<Option type="QString" value="collection" name="type"/> <Option type="QString" value="collection" name="type"/>
</Option> </Option>
</properties> </data-defined-properties>
</DiagramLayerSettings> </renderer-v2>
<geometryOptions removeDuplicateNodes="0" geometryPrecision="0"> <selection mode="Default">
<activeChecks/> <selectionColor invalid="1"/>
<checkConfiguration type="Map"> <selectionSymbol>
<Option type="Map" name="QgsGeometryGapCheck"> <symbol is_animated="0" frame_rate="10" clip_to_extent="1" type="fill" alpha="1" force_rhr="0" name="">
<Option type="double" value="0" name="allowedGapsBuffer"/> <data_defined_properties>
<Option type="bool" value="false" name="allowedGapsEnabled"/> <Option type="Map">
<Option type="QString" value="" name="allowedGapsLayer"/> <Option type="QString" value="" name="name"/>
</Option> <Option name="properties"/>
</checkConfiguration> <Option type="QString" value="collection" name="type"/>
</geometryOptions> </Option>
<fieldConfiguration> </data_defined_properties>
<field name="id"> <layer locked="0" id="{f18003f5-220a-487f-8a6d-b24facc4c1a5}" enabled="1" class="SimpleFill" pass="0">
<editWidget type="TextEdit"> <Option type="Map">
<config> <Option type="QString" value="3x:0,0,0,0,0,0" name="border_width_map_unit_scale"/>
<Option/> <Option type="QString" value="0,0,255,255,rgb:0,0,1,1" name="color"/>
</config> <Option type="QString" value="bevel" name="joinstyle"/>
</editWidget> <Option type="QString" value="0,0" name="offset"/>
</field> <Option type="QString" value="3x:0,0,0,0,0,0" name="offset_map_unit_scale"/>
<field name="Name"> <Option type="QString" value="MM" name="offset_unit"/>
<editWidget type="TextEdit"> <Option type="QString" value="35,35,35,255,rgb:0.13725490196078433,0.13725490196078433,0.13725490196078433,1" name="outline_color"/>
<config> <Option type="QString" value="solid" name="outline_style"/>
<Option/> <Option type="QString" value="0.26" name="outline_width"/>
</config> <Option type="QString" value="MM" name="outline_width_unit"/>
</editWidget> <Option type="QString" value="solid" name="style"/>
</field> </Option>
<field name="Nummer"> <data_defined_properties>
<editWidget type="Range"> <Option type="Map">
<config> <Option type="QString" value="" name="name"/>
<Option/> <Option name="properties"/>
</config> <Option type="QString" value="collection" name="type"/>
</editWidget> </Option>
</field> </data_defined_properties>
<field name="Referat"> </layer>
<editWidget type="TextEdit"> </symbol>
<config> </selectionSymbol>
<Option/> </selection>
</config> <blendMode>0</blendMode>
</editWidget> <featureBlendMode>0</featureBlendMode>
</field>
<field name="Ansprechpartner">
<editWidget type="TextEdit">
<config>
<Option/>
</config>
</editWidget>
</field>
<field name="Telefon">
<editWidget type="TextEdit">
<config>
<Option/>
</config>
</editWidget>
</field>
<field name="E-Mail">
<editWidget type="TextEdit">
<config>
<Option/>
</config>
</editWidget>
</field>
<field name="Letzte Änderung">
<editWidget type="DateTime">
<config>
<Option/>
</config>
</editWidget>
</field>
</fieldConfiguration>
<aliases>
<alias index="0" name="" field="id"/>
<alias index="1" name="" field="Name"/>
<alias index="2" name="" field="Nummer"/>
<alias index="3" name="" field="Referat"/>
<alias index="4" name="" field="Ansprechpartner"/>
<alias index="5" name="" field="Telefon"/>
<alias index="6" name="" field="E-Mail"/>
<alias index="7" name="" field="Letzte Änderung"/>
</aliases>
<excludeAttributesWMS/>
<excludeAttributesWFS/>
<defaults>
<default expression="" applyOnUpdate="0" field="id"/>
<default expression="" applyOnUpdate="0" field="Name"/>
<default expression="" applyOnUpdate="0" field="Nummer"/>
<default expression="" applyOnUpdate="0" field="Referat"/>
<default expression="" applyOnUpdate="0" field="Ansprechpartner"/>
<default expression="" applyOnUpdate="0" field="Telefon"/>
<default expression="" applyOnUpdate="0" field="E-Mail"/>
<default expression="" applyOnUpdate="0" field="Letzte Änderung"/>
</defaults>
<constraints>
<constraint exp_strength="0" notnull_strength="1" unique_strength="1" field="id" constraints="3"/>
<constraint exp_strength="0" notnull_strength="0" unique_strength="0" field="Name" constraints="0"/>
<constraint exp_strength="0" notnull_strength="0" unique_strength="0" field="Nummer" constraints="0"/>
<constraint exp_strength="0" notnull_strength="0" unique_strength="0" field="Referat" constraints="0"/>
<constraint exp_strength="0" notnull_strength="0" unique_strength="0" field="Ansprechpartner" constraints="0"/>
<constraint exp_strength="0" notnull_strength="0" unique_strength="0" field="Telefon" constraints="0"/>
<constraint exp_strength="0" notnull_strength="0" unique_strength="0" field="E-Mail" constraints="0"/>
<constraint exp_strength="0" notnull_strength="0" unique_strength="0" field="Letzte Änderung" constraints="0"/>
</constraints>
<constraintExpressions>
<constraint desc="" exp="" field="id"/>
<constraint desc="" exp="" field="Name"/>
<constraint desc="" exp="" field="Nummer"/>
<constraint desc="" exp="" field="Referat"/>
<constraint desc="" exp="" field="Ansprechpartner"/>
<constraint desc="" exp="" field="Telefon"/>
<constraint desc="" exp="" field="E-Mail"/>
<constraint desc="" exp="" field="Letzte Änderung"/>
</constraintExpressions>
<expressionfields/>
<attributeactions>
<defaultAction value="{00000000-0000-0000-0000-000000000000}" key="Canvas"/>
</attributeactions>
<attributetableconfig sortOrder="0" actionWidgetStyle="dropDown" sortExpression="">
<columns>
<column type="actions" hidden="1" width="-1"/>
<column type="field" hidden="0" width="-1" name="Nummer"/>
<column type="field" hidden="0" width="-1" name="Name"/>
<column type="field" hidden="0" width="-1" name="E-Mail"/>
<column type="field" hidden="0" width="-1" name="Letzte Änderung"/>
<column type="field" hidden="0" width="-1" name="id"/>
<column type="field" hidden="0" width="-1" name="Referat"/>
<column type="field" hidden="0" width="-1" name="Ansprechpartner"/>
<column type="field" hidden="0" width="-1" name="Telefon"/>
</columns>
</attributetableconfig>
<conditionalstyles>
<rowstyles/>
<fieldstyles/>
</conditionalstyles>
<storedexpressions/>
<editform tolerant="1">.</editform>
<editforminit/>
<editforminitcodesource>0</editforminitcodesource>
<editforminitfilepath>.</editforminitfilepath>
<editforminitcode><![CDATA[# -*- coding: utf-8 -*-
"""
QGIS forms can have a Python function that is called when the form is
opened.
Use this function to add extra logic to your forms.
Enter the name of the function in the "Python Init function"
field.
An example follows:
"""
from qgis.PyQt.QtWidgets import QWidget
def my_form_open(dialog, layer, feature):
geom = feature.geometry()
control = dialog.findChild(QWidget, "MyLineEdit")
]]></editforminitcode>
<featformsuppress>0</featformsuppress>
<editorlayout>generatedlayout</editorlayout>
<editable>
<field name="Ansprechpartner" editable="1"/>
<field name="E-Mail" editable="1"/>
<field name="Letzte Änderung" editable="1"/>
<field name="Name" editable="1"/>
<field name="Nummer" editable="1"/>
<field name="Referat" editable="1"/>
<field name="Telefon" editable="1"/>
<field name="id" editable="1"/>
</editable>
<labelOnTop>
<field labelOnTop="0" name="Ansprechpartner"/>
<field labelOnTop="0" name="E-Mail"/>
<field labelOnTop="0" name="Letzte Änderung"/>
<field labelOnTop="0" name="Name"/>
<field labelOnTop="0" name="Nummer"/>
<field labelOnTop="0" name="Referat"/>
<field labelOnTop="0" name="Telefon"/>
<field labelOnTop="0" name="id"/>
</labelOnTop>
<widgets/>
<previewExpression>COALESCE( "name", '&lt;NULL>' )</previewExpression>
<mapTip></mapTip>
<layerGeometryType>2</layerGeometryType> <layerGeometryType>2</layerGeometryType>
</qgis> </qgis>

1
tests/__init__.py Normal file
View File

@@ -0,0 +1 @@
#Testordner

154
tests/run_tests.py Normal file
View File

@@ -0,0 +1,154 @@
"""
sn_basis/test/run_tests.py
Zentraler Test-Runner für sn_basis.
Wrapper-konform, QGIS-unabhängig, CI- und IDE-fähig.
"""
import unittest
import datetime
import inspect
import os
import sys
from pathlib import Path
# ---------------------------------------------------------
# Pre-Bootstrap: Plugin-Root in sys.path eintragen
# ---------------------------------------------------------
THIS_FILE = Path(__file__).resolve()
PLUGIN_ROOT = THIS_FILE.parents[2]
if str(PLUGIN_ROOT) not in sys.path:
sys.path.insert(0, str(PLUGIN_ROOT))
from sn_basis.functions import (
get_plugin_root,
add_to_sys_path,
)
# ---------------------------------------------------------
# Bootstrap: Plugin-Root in sys.path eintragen
# ---------------------------------------------------------
def bootstrap():
"""
Simuliert das QGIS-Plugin-Startverhalten:
stellt sicher, dass sn_basis importierbar ist.
"""
plugin_root = get_plugin_root()
add_to_sys_path(plugin_root)
bootstrap()
# ---------------------------------------------------------
# Farben
# ---------------------------------------------------------
RED = "\033[91m"
YELLOW = "\033[93m"
GREEN = "\033[92m"
CYAN = "\033[96m"
MAGENTA = "\033[95m"
RESET = "\033[0m"
GLOBAL_TEST_COUNTER = 0
# ---------------------------------------------------------
# Farbige TestResult-Klasse
# ---------------------------------------------------------
class ColoredTestResult(unittest.TextTestResult):
_last_test_class: type | None = None
def startTest(self, test):
global GLOBAL_TEST_COUNTER
GLOBAL_TEST_COUNTER += 1
self.stream.write(f"{CYAN}[Test {GLOBAL_TEST_COUNTER}]{RESET}\n")
super().startTest(test)
def startTestClass(self, test):
cls = test.__class__
file = inspect.getfile(cls)
filename = os.path.basename(file)
self.stream.write(
f"\n{MAGENTA}{'=' * 70}\n"
f"Starte Testklasse: {filename}{cls.__name__}\n"
f"{'=' * 70}{RESET}\n"
)
def addError(self, test, err):
super().addError(test, err)
self.stream.write(f"{RED}ERROR{RESET}\n")
def addFailure(self, test, err):
super().addFailure(test, err)
self.stream.write(f"{RED}FAILURE{RESET}\n")
def addSkip(self, test, reason):
super().addSkip(test, reason)
self.stream.write(f"{YELLOW}SKIPPED{RESET}: {reason}\n")
def addSuccess(self, test):
super().addSuccess(test)
self.stream.write(f"{GREEN}OK{RESET}\n")
# ---------------------------------------------------------
# Farbiger TestRunner
# ---------------------------------------------------------
class ColoredTestRunner(unittest.TextTestRunner):
def _makeResult(self):
result = ColoredTestResult(
self.stream,
self.descriptions,
self.verbosity,
)
original_start_test = result.startTest
def patched_start_test(test):
if not hasattr(result, "_last_test_class") or \
result._last_test_class != test.__class__:
result.startTestClass(test)
result._last_test_class = test.__class__
original_start_test(test)
result.startTest = patched_start_test
return result
# ---------------------------------------------------------
# Testlauf starten
# ---------------------------------------------------------
def main():
print("\n" + "=" * 70)
print(
f"{CYAN}Testlauf gestartet am: "
f"{datetime.datetime.now():%Y-%m-%d %H:%M:%S}{RESET}"
)
print("=" * 70 + "\n")
loader = unittest.TestLoader()
suite = loader.discover(
start_dir=os.path.dirname(__file__),
pattern="test_*.py"
)
runner = ColoredTestRunner(verbosity=2)
result = runner.run(suite)
# Exit-Code für CI / Skripte
return 0 if result.wasSuccessful() else 1
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,9 @@
@echo off
SET OSGEO4W_ROOT=D:\QGISQT5
call %OSGEO4W_ROOT%\bin\o4w_env.bat
set QGIS_PREFIX_PATH=%OSGEO4W_ROOT%\apps\qgis
set PYTHONPATH=%QGIS_PREFIX_PATH%\python;%PYTHONPATH%
set PATH=%OSGEO4W_ROOT%\bin;%QGIS_PREFIX_PATH%\bin;%PATH%
REM Neue Eingabeaufforderung starten und Python-Skript ausführen
start cmd /k "python run_tests.py"

2
tests/test_bootstrap.py Normal file
View File

@@ -0,0 +1,2 @@
from sn_basis.functions import sys_wrapper
sys_wrapper.add_to_sys_path(sys_wrapper.get_plugin_root())

104
tests/test_dateipruefer.py Normal file
View File

@@ -0,0 +1,104 @@
# sn_basis/test/test_dateipruefer.py
import unittest
from pathlib import Path
from unittest.mock import patch
from sn_basis.modules.Dateipruefer import Dateipruefer
class TestDateipruefer(unittest.TestCase):
# -----------------------------------------------------
# 1. Leere Eingabe erlaubt
# -----------------------------------------------------
def test_leereingabe_erlaubt(self):
pruefer = Dateipruefer(
pfad="",
leereingabe_erlaubt=True
)
result = pruefer.pruefe()
self.assertFalse(result.ok)
self.assertEqual(result.aktion, "leereingabe_erlaubt")
self.assertIsNone(result.kontext)
# -----------------------------------------------------
# 2. Leere Eingabe nicht erlaubt
# -----------------------------------------------------
def test_leereingabe_nicht_erlaubt(self):
pruefer = Dateipruefer(
pfad="",
leereingabe_erlaubt=False
)
result = pruefer.pruefe()
self.assertFalse(result.ok)
self.assertEqual(result.aktion, "leereingabe_nicht_erlaubt")
self.assertIsNone(result.kontext)
# -----------------------------------------------------
# 3. Standarddatei vorschlagen
# -----------------------------------------------------
def test_standarddatei_vorschlagen(self):
pruefer = Dateipruefer(
pfad="",
standarddatei="/tmp/std.txt"
)
result = pruefer.pruefe()
self.assertFalse(result.ok)
self.assertEqual(result.aktion, "standarddatei_vorschlagen")
self.assertEqual(result.kontext, Path("/tmp/std.txt"))
# -----------------------------------------------------
# 4. Temporäre Datei erlaubt
# -----------------------------------------------------
def test_temporaer_erlaubt(self):
pruefer = Dateipruefer(
pfad="",
temporaer_erlaubt=True
)
result = pruefer.pruefe()
self.assertFalse(result.ok)
self.assertEqual(result.aktion, "temporaer_erlaubt")
self.assertIsNone(result.kontext)
# -----------------------------------------------------
# 5. Datei existiert nicht
# -----------------------------------------------------
@patch("sn_basis.modules.Dateipruefer.file_exists", return_value=False)
def test_datei_nicht_gefunden(self, mock_exists):
pruefer = Dateipruefer(
pfad="/tmp/nichtvorhanden.txt"
)
result = pruefer.pruefe()
self.assertFalse(result.ok)
self.assertEqual(result.aktion, "datei_nicht_gefunden")
self.assertEqual(result.kontext, Path("/tmp/nichtvorhanden.txt"))
# -----------------------------------------------------
# 6. Datei existiert
# -----------------------------------------------------
@patch("sn_basis.modules.Dateipruefer.file_exists", return_value=True)
def test_datei_ok(self, mock_exists):
pruefer = Dateipruefer(
pfad="/tmp/test.txt"
)
result = pruefer.pruefe()
self.assertTrue(result.ok)
self.assertEqual(result.aktion, "ok")
self.assertEqual(result.kontext, Path("/tmp/test.txt"))
if __name__ == "__main__":
unittest.main()

171
tests/test_layerpruefer.py Normal file
View File

@@ -0,0 +1,171 @@
# sn_basis/test/test_layerpruefer.py
import unittest
from sn_basis.modules.layerpruefer import Layerpruefer
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
# ---------------------------------------------------------
# Mock-Layer für Wrapper-Tests
# ---------------------------------------------------------
class MockLayer:
def __init__(
self,
exists=True,
visible=True,
layer_type="vector",
geometry_type="Polygon",
feature_count=10,
crs="EPSG:25833",
fields=None,
source="/tmp/test.shp",
editable=True,
):
self.exists = exists
self.visible = visible
self.layer_type = layer_type
self.geometry_type = geometry_type
self.feature_count = feature_count
self.crs = crs
self.fields = fields or []
self.source = source
self.editable = editable
# ---------------------------------------------------------
# Wrapper-Mocks (monkeypatching)
# ---------------------------------------------------------
def mock_layer_exists(layer):
return layer is not None and layer.exists
def mock_is_layer_visible(layer):
return layer.visible
def mock_get_layer_type(layer):
return layer.layer_type
def mock_get_layer_geometry_type(layer):
return layer.geometry_type
def mock_get_layer_feature_count(layer):
return layer.feature_count
def mock_get_layer_crs(layer):
return layer.crs
def mock_get_layer_fields(layer):
return layer.fields
def mock_get_layer_source(layer):
return layer.source
def mock_is_layer_editable(layer):
return layer.editable
# ---------------------------------------------------------
# Testklasse
# ---------------------------------------------------------
class TestLayerpruefer(unittest.TestCase):
def setUp(self):
# Monkeypatching der im Layerpruefer verwendeten Wrapper-Funktionen
import sn_basis.modules.layerpruefer as module
module.layer_exists = mock_layer_exists
module.is_layer_visible = mock_is_layer_visible
module.get_layer_type = mock_get_layer_type
module.get_layer_geometry_type = mock_get_layer_geometry_type
module.get_layer_feature_count = mock_get_layer_feature_count
module.get_layer_crs = mock_get_layer_crs
module.get_layer_fields = mock_get_layer_fields
module.get_layer_source = mock_get_layer_source
module.is_layer_editable = mock_is_layer_editable
# -----------------------------------------------------
# Tests
# -----------------------------------------------------
def test_layer_exists(self):
layer = MockLayer(exists=False)
pruefer = Layerpruefer(layer)
ergebnis = pruefer.pruefe()
self.assertFalse(ergebnis.ok)
self.assertEqual(ergebnis.aktion, "layer_nicht_gefunden")
def test_layer_unsichtbar(self):
layer = MockLayer(visible=False)
pruefer = Layerpruefer(layer, muss_sichtbar_sein=True)
ergebnis = pruefer.pruefe()
self.assertFalse(ergebnis.ok)
self.assertEqual(ergebnis.aktion, "layer_unsichtbar")
def test_falscher_layertyp(self):
layer = MockLayer(layer_type="raster")
pruefer = Layerpruefer(layer, erwarteter_layertyp="vector")
ergebnis = pruefer.pruefe()
self.assertFalse(ergebnis.ok)
self.assertEqual(ergebnis.aktion, "falscher_layertyp")
def test_falscher_geotyp(self):
layer = MockLayer(geometry_type="Point")
pruefer = Layerpruefer(layer, erwarteter_geotyp="Polygon")
ergebnis = pruefer.pruefe()
self.assertFalse(ergebnis.ok)
self.assertEqual(ergebnis.aktion, "falscher_geotyp")
def test_layer_leer(self):
layer = MockLayer(feature_count=0)
pruefer = Layerpruefer(layer)
ergebnis = pruefer.pruefe()
self.assertFalse(ergebnis.ok)
self.assertEqual(ergebnis.aktion, "layer_leer")
def test_falsches_crs(self):
layer = MockLayer(crs="EPSG:4326")
pruefer = Layerpruefer(layer, erwartetes_crs="EPSG:25833")
ergebnis = pruefer.pruefe()
self.assertFalse(ergebnis.ok)
self.assertEqual(ergebnis.aktion, "falsches_crs")
def test_felder_fehlen(self):
layer = MockLayer(fields=["id"])
pruefer = Layerpruefer(layer, erforderliche_felder=["id", "name"])
ergebnis = pruefer.pruefe()
self.assertFalse(ergebnis.ok)
self.assertEqual(ergebnis.aktion, "felder_fehlen")
def test_datenquelle_unerwartet(self):
layer = MockLayer(source="/tmp/test.shp")
pruefer = Layerpruefer(layer, erlaubte_datenquellen=["/tmp/allowed.shp"])
ergebnis = pruefer.pruefe()
self.assertFalse(ergebnis.ok)
self.assertEqual(ergebnis.aktion, "datenquelle_unerwartet")
def test_layer_nicht_editierbar(self):
layer = MockLayer(editable=False)
pruefer = Layerpruefer(layer, muss_editierbar_sein=True)
ergebnis = pruefer.pruefe()
self.assertFalse(ergebnis.ok)
self.assertEqual(ergebnis.aktion, "layer_nicht_editierbar")
def test_layer_ok(self):
layer = MockLayer()
pruefer = Layerpruefer(layer)
ergebnis = pruefer.pruefe()
self.assertTrue(ergebnis.ok)
self.assertEqual(ergebnis.aktion, "ok")
if __name__ == "__main__":
unittest.main()

79
tests/test_linkpruefer.py Normal file
View File

@@ -0,0 +1,79 @@
# sn_basis/test/test_linkpruefer.py
import unittest
from pathlib import Path
from unittest.mock import patch
from sn_basis.modules.linkpruefer import Linkpruefer
from sn_basis.functions.qgiscore_wrapper import NetworkReply
class TestLinkpruefer(unittest.TestCase):
# -----------------------------------------------------
# 1. Remote-Link erreichbar
# -----------------------------------------------------
@patch("sn_basis.modules.linkpruefer.network_head")
def test_remote_link_ok(self, mock_head):
mock_head.return_value = NetworkReply(error=0)
lp = Linkpruefer()
result = lp.pruefe("http://example.com")
self.assertTrue(result.ok)
self.assertEqual(result.aktion, "ok")
self.assertEqual(result.kontext, "http://example.com")
# -----------------------------------------------------
# 2. Remote-Link nicht erreichbar
# -----------------------------------------------------
@patch("sn_basis.modules.linkpruefer.network_head")
def test_remote_link_error(self, mock_head):
mock_head.return_value = NetworkReply(error=1)
lp = Linkpruefer()
result = lp.pruefe("http://example.com")
self.assertFalse(result.ok)
self.assertEqual(result.aktion, "url_nicht_erreichbar")
self.assertEqual(result.kontext, "http://example.com")
# -----------------------------------------------------
# 3. Netzwerkfehler (None)
# -----------------------------------------------------
@patch("sn_basis.modules.linkpruefer.network_head", return_value=None)
def test_remote_link_network_error(self, mock_head):
lp = Linkpruefer()
result = lp.pruefe("http://example.com")
self.assertFalse(result.ok)
self.assertEqual(result.aktion, "netzwerkfehler")
self.assertEqual(result.kontext, "http://example.com")
# -----------------------------------------------------
# 4. Lokaler Pfad existiert nicht
# -----------------------------------------------------
@patch("sn_basis.modules.linkpruefer.file_exists", return_value=False)
def test_local_link_not_found(self, mock_exists):
lp = Linkpruefer()
result = lp.pruefe("/path/to/missing/file.shp")
self.assertFalse(result.ok)
self.assertEqual(result.aktion, "pfad_nicht_gefunden")
self.assertEqual(result.kontext, Path("/path/to/missing/file.shp"))
# -----------------------------------------------------
# 5. Lokaler Pfad existiert
# -----------------------------------------------------
@patch("sn_basis.modules.linkpruefer.file_exists", return_value=True)
def test_local_link_ok(self, mock_exists):
lp = Linkpruefer()
result = lp.pruefe("/path/to/file.shp")
self.assertTrue(result.ok)
self.assertEqual(result.aktion, "ok")
self.assertEqual(result.kontext, Path("/path/to/file.shp"))
if __name__ == "__main__":
unittest.main()

146
tests/test_pruefmanager.py Normal file
View File

@@ -0,0 +1,146 @@
# sn_basis/test/test_pruefmanager.py
import unittest
from unittest.mock import patch
from sn_basis.modules.Pruefmanager import Pruefmanager
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
class TestPruefmanager(unittest.TestCase):
def setUp(self):
self.manager = Pruefmanager()
# -----------------------------------------------------
# 1. OK-Ergebnis → keine Interaktion
# -----------------------------------------------------
def test_ok(self):
ergebnis = pruef_ergebnis(True, "Alles gut", "ok", None)
entscheidung = self.manager.verarbeite(ergebnis)
self.assertTrue(entscheidung.ok)
self.assertEqual(entscheidung.aktion, "ok")
# -----------------------------------------------------
# 2. Leere Eingabe erlaubt → Nutzer sagt JA
# -----------------------------------------------------
@patch("sn_basis.modules.Pruefmanager.ask_yes_no", return_value=True)
def test_leereingabe_erlaubt_ja(self, mock_ask):
ergebnis = pruef_ergebnis(False, "Leer?", "leereingabe_erlaubt", None)
entscheidung = self.manager.verarbeite(ergebnis)
self.assertTrue(entscheidung.ok)
self.assertEqual(entscheidung.aktion, "ok")
# -----------------------------------------------------
# 3. Leere Eingabe erlaubt → Nutzer sagt NEIN
# -----------------------------------------------------
@patch("sn_basis.modules.Pruefmanager.ask_yes_no", return_value=False)
def test_leereingabe_erlaubt_nein(self, mock_ask):
ergebnis = pruef_ergebnis(False, "Leer?", "leereingabe_erlaubt", None)
entscheidung = self.manager.verarbeite(ergebnis)
self.assertFalse(entscheidung.ok)
self.assertEqual(entscheidung.aktion, "leereingabe_erlaubt")
# -----------------------------------------------------
# 4. Standarddatei vorschlagen → Nutzer sagt JA
# -----------------------------------------------------
@patch("sn_basis.modules.Pruefmanager.ask_yes_no", return_value=True)
def test_standarddatei_vorschlagen_ja(self, mock_ask):
ergebnis = pruef_ergebnis(
False,
"Standarddatei verwenden?",
"standarddatei_vorschlagen",
"/tmp/std.txt",
)
entscheidung = self.manager.verarbeite(ergebnis)
self.assertTrue(entscheidung.ok)
self.assertEqual(entscheidung.aktion, "ok")
self.assertEqual(entscheidung.kontext, "/tmp/std.txt")
# -----------------------------------------------------
# 5. Standarddatei vorschlagen → Nutzer sagt NEIN
# -----------------------------------------------------
@patch("sn_basis.modules.Pruefmanager.ask_yes_no", return_value=False)
def test_standarddatei_vorschlagen_nein(self, mock_ask):
ergebnis = pruef_ergebnis(
False,
"Standarddatei verwenden?",
"standarddatei_vorschlagen",
"/tmp/std.txt",
)
entscheidung = self.manager.verarbeite(ergebnis)
self.assertFalse(entscheidung.ok)
self.assertEqual(entscheidung.aktion, "standarddatei_vorschlagen")
# -----------------------------------------------------
# 6. Temporäre Datei erzeugen → Nutzer sagt JA
# -----------------------------------------------------
@patch("sn_basis.modules.Pruefmanager.ask_yes_no", return_value=True)
def test_temporaer_erlaubt_ja(self, mock_ask):
ergebnis = pruef_ergebnis(False, "Temporär?", "temporaer_erlaubt", None)
entscheidung = self.manager.verarbeite(ergebnis)
self.assertTrue(entscheidung.ok)
self.assertEqual(entscheidung.aktion, "temporaer_erzeugen")
# -----------------------------------------------------
# 7. Temporäre Datei erzeugen → Nutzer sagt NEIN
# -----------------------------------------------------
@patch("sn_basis.modules.Pruefmanager.ask_yes_no", return_value=False)
def test_temporaer_erlaubt_nein(self, mock_ask):
ergebnis = pruef_ergebnis(False, "Temporär?", "temporaer_erlaubt", None)
entscheidung = self.manager.verarbeite(ergebnis)
self.assertFalse(entscheidung.ok)
self.assertEqual(entscheidung.aktion, "temporaer_erlaubt")
# -----------------------------------------------------
# 8. Layer unsichtbar → Nutzer sagt JA
# -----------------------------------------------------
@patch("sn_basis.modules.Pruefmanager.ask_yes_no", return_value=True)
@patch("sn_basis.modules.Pruefmanager.set_layer_visible")
def test_layer_unsichtbar_ja(self, mock_set, mock_ask):
fake_layer = object()
ergebnis = pruef_ergebnis(False, "Layer unsichtbar", "layer_unsichtbar", fake_layer)
entscheidung = self.manager.verarbeite(ergebnis)
mock_set.assert_called_once_with(fake_layer, True)
self.assertTrue(entscheidung.ok)
self.assertEqual(entscheidung.aktion, "ok")
# -----------------------------------------------------
# 9. Layer unsichtbar → Nutzer sagt NEIN
# -----------------------------------------------------
@patch("sn_basis.modules.Pruefmanager.ask_yes_no", return_value=False)
def test_layer_unsichtbar_nein(self, mock_ask):
fake_layer = object()
ergebnis = pruef_ergebnis(False, "Layer unsichtbar", "layer_unsichtbar", fake_layer)
entscheidung = self.manager.verarbeite(ergebnis)
self.assertFalse(entscheidung.ok)
self.assertEqual(entscheidung.aktion, "layer_unsichtbar")
# -----------------------------------------------------
# 10. Fehlerhafte Aktion → Fallback
# -----------------------------------------------------
@patch("sn_basis.modules.Pruefmanager.warning")
def test_unbekannte_aktion(self, mock_warn):
ergebnis = pruef_ergebnis(False, "???", "unbekannt", None)
entscheidung = self.manager.verarbeite(ergebnis)
mock_warn.assert_called_once()
self.assertFalse(entscheidung.ok)
self.assertEqual(entscheidung.aktion, "unbekannt")
if __name__ == "__main__":
unittest.main()

52
tests/test_qgis.bat Normal file
View File

@@ -0,0 +1,52 @@
@echo off
setlocal
echo BATCH WIRD AUSGEFÜHRT
pause
echo ================================================
echo Starte Tests in QGIS-Python-Umgebung
echo ================================================
REM Pfad zur QGIS-Installation
set QGIS_BIN=D:\OSGeo\bin
REM Prüfen, ob python-qgis.bat existiert
if not exist "%QGIS_BIN%\python-qgis.bat" (
echo.
echo [FEHLER] python-qgis.bat wurde nicht gefunden!
echo Erwarteter Pfad:
echo %QGIS_BIN%\python-qgis.bat
echo.
echo Bitte korrigiere den Pfad in test_qgis.bat.
echo.
pause
exit /b 1
)
echo.
echo [INFO] QGIS-Python gefunden. Starte Tests...
echo.
"%QGIS_BIN%\python-qgis.bat" -m coverage run run_tests.py
if errorlevel 1 (
echo.
echo [FEHLER] Testlauf fehlgeschlagen.
echo.
pause
exit /b 1
)
echo.
echo ================================================
echo Coverage HTML-Bericht wird erzeugt...
echo ================================================
"%QGIS_BIN%\python-qgis.bat" -m coverage html
echo.
echo Fertig!
echo Öffne jetzt: coverage_html\index.html
echo ================================================
pause
endlocal

View File

@@ -0,0 +1,60 @@
# sn_basis/test/test_settings_logic.py
import unittest
from unittest.mock import patch
from sn_basis.functions.settings_logic import SettingsLogic
class TestSettingsLogic(unittest.TestCase):
# -----------------------------------------------------
# Test: load() liest alle Variablen über get_variable()
# -----------------------------------------------------
@patch("sn_basis.functions.settings_logic.get_variable")
def test_load(self, mock_get):
# Mock-Rückgabe für jede Variable
mock_get.side_effect = lambda key, scope="project": f"wert_{key}"
logic = SettingsLogic()
daten = logic.load()
# Alle Variablen müssen enthalten sein
for key in SettingsLogic.VARIABLEN:
self.assertIn(key, daten)
self.assertEqual(daten[key], f"wert_{key}")
# get_variable muss für jede Variable genau einmal aufgerufen werden
self.assertEqual(mock_get.call_count, len(SettingsLogic.VARIABLEN))
# -----------------------------------------------------
# Test: save() ruft set_variable() nur für bekannte Keys auf
# -----------------------------------------------------
@patch("sn_basis.functions.settings_logic.set_variable")
def test_save(self, mock_set):
logic = SettingsLogic()
# Eingabedaten enthalten gültige und ungültige Keys
daten = {
"amt": "A1",
"behoerde": "B1",
"unbekannt": "IGNORIEREN",
"gemeinden": "G1",
}
logic.save(daten)
# set_variable muss nur für gültige Keys aufgerufen werden
expected_calls = 3 # amt, behoerde, gemeinden
self.assertEqual(mock_set.call_count, expected_calls)
# Prüfen, ob die richtigen Keys gespeichert wurden
saved_keys = [call.args[0] for call in mock_set.call_args_list]
self.assertIn("amt", saved_keys)
self.assertIn("behoerde", saved_keys)
self.assertIn("gemeinden", saved_keys)
self.assertNotIn("unbekannt", saved_keys)
if __name__ == "__main__":
unittest.main()

81
tests/test_stilpruefer.py Normal file
View File

@@ -0,0 +1,81 @@
# sn_basis/test/test_stilpruefer.py
import unittest
import tempfile
import os
from pathlib import Path
from unittest.mock import patch
from sn_basis.modules.stilpruefer import Stilpruefer
class TestStilpruefer(unittest.TestCase):
def setUp(self):
self.pruefer = Stilpruefer()
# -----------------------------------------------------
# 1. Keine Datei angegeben
# -----------------------------------------------------
def test_keine_datei_angegeben(self):
result = self.pruefer.pruefe("")
self.assertTrue(result.ok)
self.assertEqual(result.aktion, "ok")
self.assertIn("Kein Stil angegeben", result.meldung)
self.assertIsNone(result.kontext)
# -----------------------------------------------------
# 2. Datei existiert und ist .qml
# -----------------------------------------------------
@patch("sn_basis.modules.stilpruefer.file_exists", return_value=True)
def test_datei_existiert_mit_qml(self, mock_exists):
with tempfile.NamedTemporaryFile(suffix=".qml", delete=False) as tmp:
tmp_path = tmp.name
try:
result = self.pruefer.pruefe(tmp_path)
self.assertTrue(result.ok)
self.assertEqual(result.aktion, "stil_anwendbar")
self.assertEqual(result.kontext, Path(tmp_path))
finally:
os.remove(tmp_path)
# -----------------------------------------------------
# 3. Datei existiert, aber falsche Endung
# -----------------------------------------------------
@patch("sn_basis.modules.stilpruefer.file_exists", return_value=True)
def test_datei_existiert_falsche_endung(self, mock_exists):
with tempfile.NamedTemporaryFile(suffix=".txt", delete=False) as tmp:
tmp_path = tmp.name
try:
result = self.pruefer.pruefe(tmp_path)
self.assertFalse(result.ok)
self.assertEqual(result.aktion, "falsche_endung")
self.assertIn(".qml", result.meldung)
self.assertEqual(result.kontext, Path(tmp_path))
finally:
os.remove(tmp_path)
# -----------------------------------------------------
# 4. Datei existiert nicht
# -----------------------------------------------------
@patch("sn_basis.modules.stilpruefer.file_exists", return_value=False)
def test_datei_existiert_nicht(self, mock_exists):
fake_path = "/tmp/nichtvorhanden.qml"
result = self.pruefer.pruefe(fake_path)
self.assertFalse(result.ok)
self.assertEqual(result.aktion, "datei_nicht_gefunden")
self.assertIn("nicht gefunden", result.meldung)
self.assertEqual(result.kontext, Path(fake_path))
if __name__ == "__main__":
unittest.main()

View File

@@ -1,28 +1,111 @@
from qgis.PyQt.QtWidgets import QDockWidget, QTabWidget """
sn_basis/ui/base_dockwidget.py
Basis-Dockwidget für alle LNO-Module.
"""
from sn_basis.functions.qt_wrapper import QDockWidget, QTabWidget
from sn_basis.functions.message_wrapper import warning, error
from sn_basis.functions.qt_wrapper import (
QDockWidget,
QTabWidget,
Qt,
DockWidgetMovable,
DockWidgetFloatable,
DockWidgetClosable,
DockAreaLeft,
DockAreaRight,
)
class BaseDockWidget(QDockWidget): class BaseDockWidget(QDockWidget):
"""
Basis-Dockwidget für alle LNO-Module.
- Titel wird automatisch aus base_title + subtitle erzeugt
- Tabs werden dynamisch aus der Klassenvariable 'tabs' erzeugt
- Die zugehörige Toolbar-Action wird beim Schließen zurückgesetzt
"""
base_title = "LNO Sachsen" base_title = "LNO Sachsen"
tabs = [] tabs = [] # Liste von Tab-Klassen
action = None # Referenz auf die Toolbar-Action action = None # Referenz auf die Toolbar-Action
def __init__(self, parent=None, subtitle=""): def __init__(self, parent=None, subtitle=""):
super().__init__(parent) super().__init__(parent)
# -----------------------------------------------------
# Dock-Konfiguration (WICHTIG)
# -----------------------------------------------------
self.setFeatures(
DockWidgetMovable
| DockWidgetFloatable
| DockWidgetClosable
)
# Titel zusammensetzen self.setAllowedAreas(
title = self.base_title if not subtitle else f"{self.base_title} | {subtitle}" DockAreaLeft
self.setWindowTitle(title) | DockAreaRight
)
# Dock fixieren (nur schließen erlaubt) # -----------------------------------------------------
self.setFeatures(QDockWidget.DockWidgetFeature.DockWidgetClosable) # Titel setzen
# -----------------------------------------------------
try:
title = (
self.base_title
if not subtitle
else f"{self.base_title} | {subtitle}"
)
self.setWindowTitle(title)
except Exception as e:
warning("Titel konnte nicht gesetzt werden", str(e))
# Tabs hinzufügen # -----------------------------------------------------
tab_widget = QTabWidget() # Tabs erzeugen
for tab_class in self.tabs: # -----------------------------------------------------
tab_widget.addTab(tab_class(), getattr(tab_class, "tab_title", tab_class.__name__)) try:
self.setWidget(tab_widget) tab_widget = QTabWidget()
for tab_class in self.tabs:
try:
tab_instance = tab_class()
tab_title = getattr(
tab_class,
"tab_title",
tab_class.__name__,
)
tab_widget.addTab(tab_instance, tab_title)
except Exception as e:
error(
"Tab konnte nicht geladen werden",
f"{tab_class}: {e}",
)
self.setWidget(tab_widget)
except Exception as e:
error(
"Tab-Widget konnte nicht initialisiert werden",
str(e),
)
# ---------------------------------------------------------
# Dock schließen
# ---------------------------------------------------------
def closeEvent(self, event): def closeEvent(self, event):
"""Wird aufgerufen, wenn das Dock geschlossen wird.""" """
if self.action: Wird aufgerufen, wenn das Dock geschlossen wird.
self.action.setChecked(False) # Toolbar-Button zurücksetzen Setzt die zugehörige Toolbar-Action zurück.
"""
try:
if self.action:
self.action.setChecked(False)
except Exception as e:
warning(
"Toolbar-Status konnte nicht zurückgesetzt werden",
str(e),
)
super().closeEvent(event) super().closeEvent(event)

View File

@@ -1,21 +1,85 @@
from qgis.PyQt.QtCore import Qt """
from qgis.PyQt.QtWidgets import QDockWidget sn_basis/ui/dockmanager.py
from qgis.utils import iface
Verwaltet das Anzeigen und Ersetzen von DockWidgets.
Stellt sicher, dass immer nur ein sn_basis-Dock gleichzeitig sichtbar ist.
"""
from typing import Any, Optional
from sn_basis.functions import (
add_dock_widget,
remove_dock_widget,
find_dock_widgets,
warning,
error,
)
from sn_basis.functions.qt_wrapper import (
DockAreaRight,
)
class DockManager: class DockManager:
default_area = Qt.DockWidgetArea.RightDockWidgetArea """
Verwaltet das Anzeigen und Ersetzen von DockWidgets.
"""
dock_prefix = "sn_dock_"
@classmethod @classmethod
def show(cls, dock_widget, area=None): def show(cls, dock_widget: Any, area: Optional[Any] = None) -> None:
area = area or cls.default_area """
Zeigt ein DockWidget an und entfernt vorher alle anderen
sn_basis-Docks (erkennbar am Prefix 'sn_dock_').
"""
# Bestehende Plugin-Docks mit Präfix schließen # -----------------------------------------------------
for widget in iface.mainWindow().findChildren(QDockWidget): # Default-Dock-Area (wrapper-konform)
if widget is not dock_widget and widget.objectName().startswith("sn_dock_"): # -----------------------------------------------------
iface.removeDockWidget(widget) if area is None:
widget.deleteLater() area = DockAreaRight
# Neues Dock anzeigen if dock_widget is None:
iface.addDockWidget(area, dock_widget) error("Dock konnte nicht angezeigt werden", "Dock-Widget ist None.")
dock_widget.show() return
try:
# -------------------------------------------------
# Sicherstellen, dass das Dock einen Namen hat
# -------------------------------------------------
if not dock_widget.objectName():
dock_widget.setObjectName(
f"{cls.dock_prefix}{id(dock_widget)}"
)
# -------------------------------------------------
# Vorhandene Plugin-Docks entfernen
# -------------------------------------------------
try:
for widget in find_dock_widgets():
if (
widget is not dock_widget
and widget.objectName().startswith(cls.dock_prefix)
):
remove_dock_widget(widget)
widget.deleteLater()
except Exception as e:
warning(
"Vorherige Docks konnten nicht entfernt werden",
str(e),
)
# -------------------------------------------------
# Neues Dock anzeigen
# -------------------------------------------------
try:
add_dock_widget(area, dock_widget)
dock_widget.show()
except Exception as e:
error(
"Dock konnte nicht angezeigt werden",
str(e),
)
except Exception as e:
error("DockManager-Fehler", str(e))

View File

@@ -1,83 +1,126 @@
from qgis.PyQt.QtWidgets import QAction, QMenu, QToolBar, QActionGroup """
sn_basis/ui/navigation.py
Zentrale Navigation (Menü + Toolbar) für sn_basis.
"""
from typing import Any, List, Tuple
from sn_basis.functions.qt_wrapper import (
QAction,
QMenu,
QToolBar,
QActionGroup,
)
from sn_basis.functions import (
get_main_window,
add_toolbar,
remove_toolbar,
add_menu,
remove_menu,
)
class Navigation: class Navigation:
def __init__(self, iface): def __init__(self):
self.iface = iface
self.actions = [] self.actions = []
self.menu = None
self.toolbar = None
self.plugin_group = None
# Menü und Toolbar einmalig anlegen
self.menu = QMenu("LNO Sachsen", iface.mainWindow())
iface.mainWindow().menuBar().addMenu(self.menu)
self.toolbar = QToolBar("LNO Sachsen")
def init_ui(self):
print(">>> Navigation.init_ui() CALLED")
main_window = get_main_window()
if not main_window:
return
# -----------------------------------------
# Vorherige Toolbars entfernen
# -----------------------------------------
for tb in main_window.findChildren(QToolBar):
if tb.objectName() == "LnoSachsenToolbar":
remove_toolbar(tb)
tb.deleteLater()
# -----------------------------------------
# Menü und Toolbar neu erzeugen
# -----------------------------------------
self.menu = QMenu("LNO Sachsen", main_window)
add_menu(self.menu)
self.toolbar = QToolBar("LNO Sachsen", main_window)
self.toolbar.setObjectName("LnoSachsenToolbar") self.toolbar.setObjectName("LnoSachsenToolbar")
iface.addToolBar(self.toolbar) add_toolbar(self.toolbar)
# Gruppe für exklusive Auswahl (nur ein Plugin aktiv) test_action = QAction("TEST ACTION", main_window)
self.plugin_group = QActionGroup(iface.mainWindow()) self.menu.addAction(test_action)
self.toolbar.addAction(test_action)
self.plugin_group = QActionGroup(main_window)
self.plugin_group.setExclusive(True) self.plugin_group.setExclusive(True)
# -----------------------------------------------------
# Actions
# -----------------------------------------------------
def add_action(self, text, callback, tooltip="", priority=100): def add_action(self, text, callback, tooltip="", priority=100):
action = QAction(text, self.iface.mainWindow()) if not self.plugin_group:
return None
action = QAction(text, get_main_window())
action.setToolTip(tooltip) action.setToolTip(tooltip)
action.setCheckable(True) # Button kann aktiv sein action.setCheckable(True)
action.triggered.connect(callback) action.triggered.connect(callback)
# Action in Gruppe aufnehmen
self.plugin_group.addAction(action) self.plugin_group.addAction(action)
# Action mit Priority speichern
self.actions.append((priority, action)) self.actions.append((priority, action))
return action return action
def finalize_menu_and_toolbar(self): def finalize_menu_and_toolbar(self):
# Sortieren nach Priority if not self.menu or not self.toolbar:
return
self.actions.sort(key=lambda x: x[0]) self.actions.sort(key=lambda x: x[0])
# Menüeinträge
self.menu.clear() self.menu.clear()
self.toolbar.clear()
for _, action in self.actions: for _, action in self.actions:
self.menu.addAction(action) self.menu.addAction(action)
# Toolbar-Einträge
self.toolbar.clear()
for _, action in self.actions:
self.toolbar.addAction(action) self.toolbar.addAction(action)
def set_active_plugin(self, active_action): def set_active_plugin(self, active_action):
# Alle zurücksetzen, dann aktives Plugin markieren
for _, action in self.actions: for _, action in self.actions:
action.setChecked(False) action.setChecked(False)
if active_action: if active_action:
active_action.setChecked(True) active_action.setChecked(True)
def remove_all(self): # -----------------------------------------------------
"""Alles entfernen beim Entladen des Basisplugins""" # Cleanup
# Menü entfernen # -----------------------------------------------------
if self.menu:
self.iface.mainWindow().menuBar().removeAction(self.menu.menuAction())
self.menu = None
# Toolbar entfernen
if self.toolbar:
self.iface.mainWindow().removeToolBar(self.toolbar)
self.toolbar = None
# Actions zurücksetzen
self.actions.clear()
# Gruppe leeren
self.plugin_group = None
def remove_action(self, action): def remove_action(self, action):
"""Entfernt eine einzelne Action aus Menü und Toolbar"""
if not action: if not action:
return return
# Menüeintrag entfernen
if self.menu: if self.menu:
self.menu.removeAction(action) self.menu.removeAction(action)
# Toolbar-Eintrag entfernen
if self.toolbar: if self.toolbar:
self.toolbar.removeAction(action) self.toolbar.removeAction(action)
# Aus der internen Liste löschen
self.actions = [(p, a) for p, a in self.actions if a != action] self.actions = [(p, a) for p, a in self.actions if a != action]
def remove_all(self):
if self.menu:
remove_menu(self.menu)
self.menu = None
if self.toolbar:
remove_toolbar(self.toolbar)
self.toolbar = None
self.actions.clear()
self.plugin_group = None

View File

@@ -1,12 +1,18 @@
from qgis.PyQt.QtWidgets import ( #sn_basis/ui/tabs/settings_tab.py
QWidget, QGridLayout, QLabel, QLineEdit, from sn_basis.functions.qt_wrapper import (
QGroupBox, QVBoxLayout, QPushButton QWidget,
QGridLayout,
QLabel,
QLineEdit,
QGroupBox,
QVBoxLayout,
QPushButton,
) )
from sn_basis.functions.settings_logic import SettingsLogic from sn_basis.functions.settings_logic import SettingsLogic
class SettingsTab(QWidget): class SettingsTab(QWidget):
tab_title = "Projekteigenschaften" # Titel für den Tab tab_title = "Projekteigenschaften"
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
@@ -14,58 +20,87 @@ class SettingsTab(QWidget):
main_layout = QVBoxLayout() main_layout = QVBoxLayout()
# -----------------------------
# Definition der Felder # Definition der Felder
# -----------------------------
self.user_fields = { self.user_fields = {
"amt": "Amt:", "amt": "Amt:",
"behoerde": "Behörde:", "behoerde": "Behörde:",
"landkreis_user": "Landkreis:", "landkreis_user": "Landkreis:",
"sachgebiet": "Sachgebiet:" "sachgebiet": "Sachgebiet:",
} }
self.project_fields = { self.project_fields = {
"bezeichnung": "Bezeichnung:", "bezeichnung": "Bezeichnung:",
"verfahrensnummer": "Verfahrensnummer:", "verfahrensnummer": "Verfahrensnummer:",
"gemeinden": "Gemeinde(n):", "gemeinden": "Gemeinde(n):",
"landkreise_proj": "Landkreis(e):" "landkreise_proj": "Landkreis(e):",
} }
# 🟦 Benutzerspezifische Festlegungen # -----------------------------
# Benutzerspezifische Festlegungen
# -----------------------------
user_group = QGroupBox("Benutzerspezifische Festlegungen") user_group = QGroupBox("Benutzerspezifische Festlegungen")
user_layout = QGridLayout() user_layout = QGridLayout()
self.user_inputs = {} self.user_inputs = {}
for row, (key, label) in enumerate(self.user_fields.items()): for row, (key, label) in enumerate(self.user_fields.items()):
self.user_inputs[key] = QLineEdit() input_widget = QLineEdit()
self.user_inputs[key] = input_widget
user_layout.addWidget(QLabel(label), row, 0) user_layout.addWidget(QLabel(label), row, 0)
user_layout.addWidget(self.user_inputs[key], row, 1) user_layout.addWidget(input_widget, row, 1)
user_group.setLayout(user_layout) user_group.setLayout(user_layout)
# 🟨 Projektspezifische Festlegungen # -----------------------------
# Projektspezifische Festlegungen
# -----------------------------
project_group = QGroupBox("Projektspezifische Festlegungen") project_group = QGroupBox("Projektspezifische Festlegungen")
project_layout = QGridLayout() project_layout = QGridLayout()
self.project_inputs = {} self.project_inputs = {}
for row, (key, label) in enumerate(self.project_fields.items()): for row, (key, label) in enumerate(self.project_fields.items()):
self.project_inputs[key] = QLineEdit() input_widget = QLineEdit()
self.project_inputs[key] = input_widget
project_layout.addWidget(QLabel(label), row, 0) project_layout.addWidget(QLabel(label), row, 0)
project_layout.addWidget(self.project_inputs[key], row, 1) project_layout.addWidget(input_widget, row, 1)
project_group.setLayout(project_layout) project_group.setLayout(project_layout)
# 🟩 Speichern-Button # -----------------------------
# Speichern-Button
# -----------------------------
save_button = QPushButton("Speichern") save_button = QPushButton("Speichern")
save_button.clicked.connect(self.save_data) save_button.clicked.connect(self.save_data)
# -----------------------------
# Layout zusammenfügen # Layout zusammenfügen
# -----------------------------
main_layout.addWidget(user_group) main_layout.addWidget(user_group)
main_layout.addWidget(project_group) main_layout.addWidget(project_group)
main_layout.addStretch() main_layout.addStretch()
main_layout.addWidget(save_button) main_layout.addWidget(save_button)
self.setLayout(main_layout) self.setLayout(main_layout)
# Daten laden
self.load_data() self.load_data()
# ---------------------------------------------------------
# Speichern
# ---------------------------------------------------------
def save_data(self): def save_data(self):
# Alle Felder zusammenführen fields = {
fields = {key: widget.text() for key, widget in {**self.user_inputs, **self.project_inputs}.items()} key: widget.text()
for key, widget in {**self.user_inputs, **self.project_inputs}.items()
}
self.logic.save(fields) self.logic.save(fields)
# ---------------------------------------------------------
# Laden
# ---------------------------------------------------------
def load_data(self): def load_data(self):
data = self.logic.load() data = self.logic.load()
for key, widget in {**self.user_inputs, **self.project_inputs}.items(): for key, widget in {**self.user_inputs, **self.project_inputs}.items():