Compare commits

...
34 Commits
Author SHA1 Message Date
release-bot 1d7489f4d6 Release 26.4.3-unstable 2026-04-21 20:22:04 +00:00
Daniel f1b463a1c5 Merge pull request 'Verfahrensgebiet aus Auswahl ergänzt' (#9) from feature/Verfahrensgebiet_bilden into unstable
Reviewed-on: AG_QGIS/Plugin_SN_Verfahrensgebiet#9
2026-04-21 22:20:03 +02:00
Daniel 189855e63b Flst_Laden, VerfGebiet bilden 2026-04-21 21:50:26 +02:00
Daniel 72c9d6fa15 tests überarbeitet 2026-04-20 15:36:18 +02:00
Daniel 9caf0e3c86 pdock-dockstrings ergänzt 2026-04-20 14:33:13 +02:00
release-bot f6591b2099 Release 26.4.2-testing 2026-04-10 09:34:10 +00:00
release-bot 1a2d3109d4 Release 26.4.1-unstable 2026-04-08 12:52:25 +00:00
Daniel 41b272092e Merge pull request 'verfahrensgebiet-Vaiable wird sauber gesetzt' (#8) from bug/Verfahrensgebiet_nicht_übernommen into unstable
Reviewed-on: AG_QGIS/Plugin_SN_Verfahrensgebiet#8
2026-04-08 14:38:20 +02:00
Daniel 5be2d3b4f9 verfahrensgebiet-Vaiable wird sauber gesetzt 2026-04-08 14:36:39 +02:00
release-bot 62fae1c431 Release 26.3.3-unstable 2026-03-27 10:28:23 +00:00
Daniel 5531db9071 Merge pull request 'Verfahrensgebiet-Layerauswahl nach sn_verfahrensgebiet verschoben' (#7) from refactor/Verfahrensgebiet-Auswahl_nach_sn_Verfahrensgebiet into unstable
Reviewed-on: AG_QGIS/Plugin_SN_Verfahrensgebiet#7
2026-03-27 11:27:13 +01:00
Daniel 1796a7d5c0 Verfahrensgebiet-Layerauswahl nach sn_verfahrensgebiet verschoben 2026-03-27 11:19:07 +01:00
release-bot 98077e47cf Release 26.3.2-unstable 2026-03-26 20:48:56 +00:00
Daniel df0ebc5543 Merge pull request 'printtab aus sn_basis eingefügt' (#6) from refactor/Druckfunktion-in-sn_basis2 into unstable
Reviewed-on: AG_QGIS/Plugin_SN_Verfahrensgebiet#6
2026-03-26 21:42:25 +01:00
Daniel e4de5950a7 printtab aus sn_basis eingefügt 2026-03-26 20:16:02 +01:00
release-bot 52cf55d242 Release 26.3.1-unstable 2026-03-25 08:10:05 +00:00
22ottomi 195a3d949b changelog.txt aktualisiert 2026-03-25 09:05:55 +01:00
Michael Otto 472bf1b304 Umstellung auf neuen Release Workflow 2026-03-25 08:38:51 +01:00
Michael Otto 9f4e83257f Release-Workflow hinzugefügt und plugin.info zu plugin.cfg umbenannt 2026-03-13 14:03:12 +01:00
Michael Otto 06198f3e15 Add plugin.info based on metadata.txt 2026-03-13 08:39:31 +01:00
Michael Otto 85ac7597a5 Add .gitea from Plugin_Test_Action main 2026-03-13 08:28:57 +01:00
Daniel 229207e4dd Merge pull request 'auf wrapper/Prüfer umgestellt, Verfahrensgebiet aus ALKIS laden funktioniert' (#5) from Daniel/Plugin_SN_Verfahrensgebiet:main into main
Reviewed-on: AG_QGIS/Plugin_SN_Verfahrensgebiet#5
2026-03-06 10:31:55 +01:00
Daniel 797ff85e35 Merge pull request 'auf wrapper/Prüfer umgestellt, Verfahrensgebiet aus ALKIS laden funktioniert' (#1) from dev into main
Reviewed-on: #1
2026-03-06 10:30:28 +01:00
Daniel c265bb3dcf auf wrapper/Prüfer umgestellt, Verfahrensgebiet aus ALKIS laden funktioniert 2026-03-06 10:11:17 +01:00
22ottomi faed780dbf Merge pull request 'Meldung bei fehlender Verfahrensnummer geändert' (#4) from 22ottomi/Plugin_SN_Verfahrensgebiet:main into main
Reviewed-on: AG_QGIS/Plugin_SN_Verfahrensgebiet#4
2025-11-20 12:25:11 +01:00
Michael Otto 6a7e2c28f6 Merge branch 'main' of https://entwicklung.flurneuordnung-sachsen.de/22ottomi/Plugin_SN_Verfahrensgebiet 2025-11-20 12:20:07 +01:00
Michael Otto 0be47794b0 Meldung bei fehlender Verfahrensnummer geändert 2025-11-20 12:19:43 +01:00
22ottomi 2e2799930d Merge pull request 'Text geändert' (#3) from 22ottomi/Plugin_SN_Verfahrensgebiet:main into main
Reviewed-on: AG_QGIS/Plugin_SN_Verfahrensgebiet#3
2025-11-20 11:23:54 +01:00
22ottomi 01cbb76dcd Text geändert
Zum Testen eines Pull-Request den Text geändert
2025-11-20 11:23:17 +01:00
22ottomi 1c3de100e4 README.md aktualisiert 2025-11-20 11:22:28 +01:00
22ottomi 34d8253919 README.md aktualisiert 2025-11-20 11:21:39 +01:00
22ottomi 178487f22e README.md aktualisiert 2025-11-20 10:11:49 +01:00
Michael Otto cc757452bc Ablauf optimiert 2025-11-18 16:10:58 +01:00
Michael Otto bf479bdb64 25.11.4 2025-11-18 12:45:48 +01:00
23 changed files with 3364 additions and 353 deletions
+133
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"
+14
View File
@@ -1,3 +1,17 @@
"""sn_verfahrensgebiet – QGIS-Plugin für Verfahrensgebiets-Management.
Verwaltet die Abgrenzung und Verwaltung von Verfahrensgebieten.
"""
def classFactory(iface):
"""Factory-Funktion für QGIS.
Wird von QGIS aufgerufen, um die Plugin-Instanz zu erstellen.
Args:
iface: QGIS-Schnittstellenobjekt
Returns:
Verfahrensgebiet: Instanziiertes Plugin
"""
from .main import Verfahrensgebiet
return Verfahrensgebiet(iface)
+13
View File
@@ -0,0 +1,13 @@
---
Version 26.4.3-unstable:
---
Version 26.4.2-testing:
---
Version 26.4.1-unstable:
---
Version 26.3.3-unstable:
---
Version 26.3.2-unstable:
---
Version 26.3.1-unstable:
- erster Test als echtes PlugIn
+113
View File
@@ -0,0 +1,113 @@
"""sn_verfahrensgebiet/functions/flurstueck_alkis.py – ALKIS-Integration für Flurstücke.
Lädt Flurstücke aus dem ALKIS-WFS-Dienst (adv:AX_Flurstueck) anhand von
Gemarkungsschlüsseln, die der Nutzer interaktiv eingibt.
Der WFS-Layer wird direkt (nicht als Memory-Layer) ins Projekt geladen,
da die einzelnen Features für die spätere Auswahl benötigt werden.
"""
from sn_basis.functions import QgsVectorLayer, QgsProject
from sn_basis.functions import QMessageBox
from sn_basis.functions.dialog_wrapper import ask_text
# LoadStatus aus verfahrensgebiet_alkis wiederverwenden (identische Semantik)
from sn_verfahrensgebiet.functions.verfahrensgebiet_alkis import LoadStatus
ALKIS_NAS_URL = "https://geodienste.sachsen.de/aaa/public_alkis/nas/wfs"
FLST_TYPENAME = "adv:AX_Flurstueck"
FLST_LAYER_NAME = "Flurstücke"
def _check_existing_flurstueck_layer(tab_widget):
"""Prüft, ob ein Flurstück-Layer bereits im Projekt existiert.
Returns
-------
Tuple[Optional[QgsVectorLayer], LoadStatus]
Bei ``LoadStatus.KEEP`` enthält das Tuple den vorhandenen Layer,
bei ``LoadStatus.FIRST`` und ``LoadStatus.RELOAD`` ist es None.
"""
project = QgsProject.instance()
existing_layers = project.mapLayersByName(FLST_LAYER_NAME)
if not existing_layers:
return None, LoadStatus.FIRST
reply = QMessageBox.question(
tab_widget,
"Layer bereits vorhanden",
f"Der Layer '{FLST_LAYER_NAME}' existiert bereits.\n"
"Möchten Sie ihn löschen und neu laden?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No,
)
if reply == QMessageBox.StandardButton.Yes:
for lyr in existing_layers:
project.removeMapLayer(lyr.id())
return None, LoadStatus.RELOAD
else:
return existing_layers[0], LoadStatus.KEEP
def flurstueck_alkis(tab_widget):
"""Lädt Flurstücke aus ALKIS-WFS anhand von Gemarkungsschlüsseln.
Der Nutzer gibt einen oder mehrere Gemarkungsschlüssel (kommagetrennt) ein.
Der WFS wird per SQL-Filter auf
``gemarkung_ax_gemarkung_schluessel_gemarkungsnummer IN (...)`` abgefragt.
Parameters
----------
tab_widget :
Eltern-Widget für Dialoge.
Returns
-------
Tuple[Optional[QgsVectorLayer], LoadStatus]
Geladener WFS-Layer und Ladestatus.
Bei Fehlern ist der Layer None und der Status LoadStatus.NONE.
"""
# 1. Vorhandenen Layer prüfen
existing, status = _check_existing_flurstueck_layer(tab_widget)
if status == LoadStatus.KEEP:
return existing, status
# 2. Gemarkungsschlüssel abfragen
eingabe, ok = ask_text(
"Gemarkungsschlüssel",
"Gemarkungsschlüssel eingeben (mehrere mit Komma trennen):",
parent=tab_widget,
)
if not ok or not eingabe.strip():
QMessageBox.warning(
tab_widget,
"Abgebrochen",
"Bitte mindestens einen gültigen Gemarkungsschlüssel eingeben.",
)
return None, LoadStatus.NONE
schluessel_liste = [s.strip() for s in eingabe.split(",") if s.strip()]
if not schluessel_liste:
QMessageBox.warning(tab_widget, "Fehler", "Keine gültigen Schlüssel gefunden.")
return None, LoadStatus.NONE
schluessel_str = ", ".join(f"'{s}'" for s in schluessel_liste)
# 3. WFS-URI aufbauen und Layer laden
wfs_uri = (
f"url='{ALKIS_NAS_URL}' typename='{FLST_TYPENAME}' srsname='EPSG:25833' "
f"sql=SELECT * FROM AX_Flurstueck "
f"WHERE gemarkung_ax_gemarkung_schluessel_gemarkungsnummer IN ({schluessel_str})"
)
layer = QgsVectorLayer(wfs_uri, FLST_LAYER_NAME, "WFS")
if not layer.isValid():
QMessageBox.critical(
tab_widget,
"Fehler",
"Flurstück-Layer konnte nicht geladen werden.\n"
"Bitte Gemarkungsschlüssel und Netzwerkverbindung prüfen.",
)
return None, LoadStatus.NONE
return layer, status
+118
View File
@@ -0,0 +1,118 @@
"""sn_verfahrensgebiet/functions/verfahrensgebiet_alkis.py – ALKIS-Integration.
Lädt Verfahrensgebiete aus dem ALKIS-WFS-Dienst.
"""
from enum import Enum
from sn_basis.functions import (
QgsVectorLayer, QgsProject, QgsFeature, QgsField, QgsGeometry
)
from sn_basis.functions import QMessageBox
from sn_basis.functions.qt_wrapper import QVariant
from sn_basis.functions.variable_wrapper import get_variable
alkis_NAS_url = "https://geodienste.sachsen.de/aaa/public_alkis/nas/wfs"
typename = "adv:AX_BauRaumOderBodenordnungsrecht"
class LoadStatus(Enum):
"""Status eines Layer-Ladevorgangs.
Attributes:
NONE: Nichts geladen
FIRST: Erstmalig geladen
RELOAD: Neu geladen
KEEP: Vorhandener Layer behalten
"""
NONE = "none" # nichts geladen
FIRST = "first" # erstmalig geladen
RELOAD = "reload" # neu geladen
KEEP = "keep" # vorhandener Layer behalten
def _check_existing_layer(tab_widget):
"""Prüft, ob ein Verfahrensgebiet-Layer bereits existiert.
Args:
tab_widget: Parent-Widget für Dialoge
Returns:
Tuple[Optional[QgsVectorLayer], LoadStatus]: Layer und Status
"""
project = QgsProject.instance()
existing_layers = project.mapLayersByName("Verfahrensgebiet")
if not existing_layers:
return None, LoadStatus.FIRST
reply = QMessageBox.question(
tab_widget,
"Layer bereits vorhanden",
"Der Layer 'Verfahrensgebiet' existiert bereits.\n"
"Möchten Sie ihn löschen und neu laden?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No
)
if reply == QMessageBox.StandardButton.Yes:
for lyr in existing_layers:
project.removeMapLayer(lyr.id())
return None, LoadStatus.RELOAD
else:
return existing_layers[0], LoadStatus.KEEP
def verfahrensgebiet_alkis(tab_widget):
"""Lädt Verfahrensgebiet aus ALKIS-WFS basierend auf Verfahrensnummer.
Args:
tab_widget: Parent-Widget für Dialoge und Meldungen
Returns:
Tuple[Optional[QgsVectorLayer], LoadStatus]: Geladener Layer und Status
"""
verfahrensnummer = get_variable("verfahrensnummer")
if not verfahrensnummer:
QMessageBox.critical(tab_widget, "Fehler",
"In den Projekteigenschaften ist noch keine Verfahrensnummer eingetragen!")
return None, LoadStatus.NONE
# Vorhandenen Layer prüfen
existing, status = _check_existing_layer(tab_widget)
if status == LoadStatus.KEEP:
return existing, status
# WFS mit Filter laden
wfs_uri_find = (
f"url='{alkis_NAS_url}' typename='{typename}' srsname='EPSG:25833' "
f"sql=SELECT * FROM AX_BauRaumOderBodenordnungsrecht "
f"WHERE bezeichnung LIKE '{verfahrensnummer}%'"
)
temp_layer = QgsVectorLayer(wfs_uri_find, "TempVerfahrensgebiet", "WFS")
if not temp_layer.isValid():
QMessageBox.critical(tab_widget, "Fehler", "Layer konnte nicht geladen werden.")
return None, LoadStatus.NONE
features = list(temp_layer.getFeatures())
if not features:
QMessageBox.critical(tab_widget, "Fehler",
f"Verfahrenskennzeichen {verfahrensnummer} nicht im WFS gefunden.")
return None, LoadStatus.NONE
union_geom = QgsGeometry.unaryUnion([f.geometry() for f in features])
if union_geom is None or union_geom.isEmpty():
QMessageBox.critical(tab_widget, "Fehler", "Keine Geometrien zum Verschmelzen gefunden.")
return None, LoadStatus.NONE
# Memory-Layer erzeugen
crs = temp_layer.crs().authid()
memory_layer = QgsVectorLayer(f"Polygon?crs={crs}", "Verfahrensgebiet", "memory")
pr = memory_layer.dataProvider()
pr.addAttributes([QgsField("VKZ", QVariant.String)])
memory_layer.updateFields()
feat_union = QgsFeature()
feat_union.setGeometry(union_geom)
feat_union.setAttributes([verfahrensnummer])
pr.addFeatures([feat_union])
memory_layer.updateExtents()
# Status: FIRST oder RELOAD
return memory_layer, status
+358
View File
@@ -0,0 +1,358 @@
"""sn_verfahrensgebiet/functions/verfahrensgebiet_edit_service.py
Service-Modul für Verfahrensgebiet-Editoperationen aus Flurstück-Auswahl.
Der Verfahrensgebiet-Layer ist ein Single-Feature-Memory-Layer: alle
Flurstück-Geometrien werden via ``QgsGeometry.unaryUnion`` zu einem einzigen
(Multi-)Polygon verschmolzen und als ein Feature gespeichert. Dadurch
erscheint das Verfahrensgebiet im Kartenfenster als zusammenhängendes Objekt.
Die Flurstückskennzeichen werden als kommagetrennte Zeichenkette im
Attributfeld ``flurstueckskennzeichen`` des Single-Features gespeichert,
sodass Delta-Operationen (Ergänzen/Entfernen) ohne erneuten Zugriff auf den
Quell-Flurstück-Layer möglich bleiben und die ID-Menge via
``get_kennzeichen_menge()`` jederzeit abrufbar ist.
"""
from typing import Optional, Tuple, List
from sn_basis.functions import QgsVectorLayer, QgsFeature, QgsField
from sn_basis.functions.qgiscore_wrapper import QgsGeometry
from sn_basis.functions.qt_wrapper import QVariant
from sn_basis.modules.layerpruefer import Layerpruefer
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
VERF_LAYER_NAME = "Verfahrensgebiet"
FELD_KENNZEICHEN = "flurstueckskennzeichen"
FELD_GEMARKUNG = "gemarkung_ax_gemarkung_schluessel_gemarkungsnummer"
PFLICHTFELDER: List[str] = [FELD_KENNZEICHEN, FELD_GEMARKUNG]
# ---------------------------------------------------------------------------
# Vorbedingungsprüfungen
# ---------------------------------------------------------------------------
def pruefe_flurstueck_layer(layer) -> pruef_ergebnis:
"""Prüft, ob ein Layer als Flurstück-Quelle geeignet ist.
Prüfreihenfolge via Layerpruefer:
existiert → Vektorlayer → Polygon-Geometrie → nicht leer → Pflichtfelder vorhanden.
Returns
-------
pruef_ergebnis
ok=True, wenn alle Prüfungen bestanden wurden.
"""
pruefer = Layerpruefer(
layer=layer,
erwarteter_layertyp="vector",
erwarteter_geotyp="Polygon",
erforderliche_felder=PFLICHTFELDER,
)
return pruefer.pruefe()
def pruefe_auswahl(layer) -> pruef_ergebnis:
"""Prüft, ob auf dem Layer mindestens ein Feature ausgewählt ist.
Returns
-------
pruef_ergebnis
ok=True bei mindestens einer Auswahl, sonst ok=False mit aktion ``layer_leer``.
"""
try:
count = int(layer.selectedFeatureCount())
except Exception:
count = 0
if count == 0:
return pruef_ergebnis(
ok=False,
meldung="Es sind keine Flurstücke ausgewählt. Bitte zuerst eine Auswahl treffen.",
aktion="layer_leer",
kontext=None,
)
return pruef_ergebnis(
ok=True,
meldung=f"{count} Flurstücke ausgewählt.",
aktion="ok",
kontext=None,
)
# ---------------------------------------------------------------------------
# Hilfsfunktionen
# ---------------------------------------------------------------------------
def get_kennzeichen_menge(layer) -> set:
"""Gibt die Menge aller ``flurstueckskennzeichen`` aus dem Layer zurück.
Im Single-Feature-Modell sind die Kennzeichen als kommagetrennte
Zeichenkette im einzigen Feature gespeichert.
"""
result = set()
for feat in layer.getFeatures():
val = feat[FELD_KENNZEICHEN]
if val:
for kz in str(val).split(","):
kz = kz.strip()
if kz:
result.add(kz)
return result
def _erstelle_memory_layer(crs_authid: str) -> QgsVectorLayer:
"""Erzeugt einen leeren Multi-Feature-Memory-Layer für das Verfahrensgebiet."""
layer = QgsVectorLayer(
f"MultiPolygon?crs={crs_authid}", VERF_LAYER_NAME, "memory"
)
pr = layer.dataProvider()
pr.addAttributes([
QgsField(FELD_KENNZEICHEN, QVariant.String),
QgsField(FELD_GEMARKUNG, QVariant.String),
])
layer.updateFields()
return layer
# ---------------------------------------------------------------------------
# Hauptoperationen
# ---------------------------------------------------------------------------
def erstelle_verfahrensgebiet_aus_auswahl(
source_layer,
) -> Tuple[Optional[object], int, int]:
"""Erstellt neuen Verfahrensgebiet-Memory-Layer aus der aktuellen Auswahl.
Alle Geometrien werden via ``QgsGeometry.unaryUnion`` zu einem einzigen
(Multi-)Polygon verschmolzen. Der Layer enthält danach genau ein Feature.
Die Flurstückskennzeichen werden kommagetrennt im Attribut gespeichert.
Parameters
----------
source_layer :
Flurstück-Layer mit aktiver Auswahl.
Returns
-------
Tuple[layer, erfolgreich, fehlgeschlagen]
"""
crs = source_layer.crs().authid()
mem_layer = _erstelle_memory_layer(crs)
pr = mem_layer.dataProvider()
erfolgreich = 0
fehlgeschlagen = 0
geom_list = []
kz_list = []
gemarkung_set = set()
for src_feat in source_layer.selectedFeatures():
try:
geom = src_feat.geometry()
if not geom or geom.isEmpty():
fehlgeschlagen += 1
continue
geom_list.append(geom)
kz = str(src_feat[FELD_KENNZEICHEN] or "")
gm = str(src_feat[FELD_GEMARKUNG] or "")
if kz:
kz_list.append(kz)
if gm:
gemarkung_set.add(gm)
erfolgreich += 1
except Exception:
fehlgeschlagen += 1
if geom_list:
union_geom = QgsGeometry.unaryUnion(geom_list)
if union_geom and not union_geom.isEmpty():
feat = QgsFeature()
feat.setGeometry(union_geom)
feat.setAttributes([
",".join(kz_list),
",".join(sorted(gemarkung_set)),
])
pr.addFeatures([feat])
mem_layer.updateExtents()
return mem_layer, erfolgreich, fehlgeschlagen
def ergaenze_verfahrensgebiet(
verf_layer,
source_layer,
) -> Tuple[object, int, int, int]:
"""Fügt nur neue (noch nicht enthaltene) Flurstücke zum Verfahrensgebiet hinzu.
Die Geometrien der neuen Flurstücke werden mit der bestehenden Union-Geometrie
verschmolzen. Das Single-Feature wird an Ort und Stelle aktualisiert.
Parameters
----------
verf_layer :
Bestehender Verfahrensgebiet-Memory-Layer (Single-Feature).
source_layer :
Flurstück-Layer mit aktiver Auswahl.
Returns
-------
Tuple[verf_layer, hinzugefuegt, bereits_vorhanden, fehlgeschlagen]
"""
vorhandene_ids = get_kennzeichen_menge(verf_layer)
pr = verf_layer.dataProvider()
hinzugefuegt = 0
bereits_vorhanden = 0
fehlgeschlagen = 0
neue_geoms = []
neue_kz = []
neue_gemarkungen: set = set()
for src_feat in source_layer.selectedFeatures():
try:
kz = str(src_feat[FELD_KENNZEICHEN] or "")
if kz in vorhandene_ids:
bereits_vorhanden += 1
continue
geom = src_feat.geometry()
if not geom or geom.isEmpty():
fehlgeschlagen += 1
continue
neue_geoms.append(geom)
if kz:
neue_kz.append(kz)
gm = str(src_feat[FELD_GEMARKUNG] or "")
if gm:
neue_gemarkungen.add(gm)
hinzugefuegt += 1
except Exception:
fehlgeschlagen += 1
if neue_geoms:
existing_feats = list(verf_layer.getFeatures())
if existing_feats:
existing_feat = existing_feats[0]
all_geoms = [existing_feat.geometry()] + neue_geoms
new_union = QgsGeometry.unaryUnion(all_geoms)
alle_kz = sorted(vorhandene_ids | set(neue_kz))
existing_gm_str = existing_feat[FELD_GEMARKUNG] or ""
alle_gm = sorted(
{g.strip() for g in existing_gm_str.split(",") if g.strip()} | neue_gemarkungen
)
kz_idx = verf_layer.fields().indexFromName(FELD_KENNZEICHEN)
gm_idx = verf_layer.fields().indexFromName(FELD_GEMARKUNG)
pr.changeAttributeValues({existing_feat.id(): {
kz_idx: ",".join(alle_kz),
gm_idx: ",".join(alle_gm),
}})
pr.changeGeometryValues({existing_feat.id(): new_union})
else:
# Kein vorhandenes Feature – neues anlegen
union_geom = QgsGeometry.unaryUnion(neue_geoms)
if union_geom and not union_geom.isEmpty():
feat = QgsFeature()
feat.setGeometry(union_geom)
feat.setAttributes([
",".join(sorted(neue_kz)),
",".join(sorted(neue_gemarkungen)),
])
pr.addFeatures([feat])
verf_layer.updateExtents()
verf_layer.triggerRepaint()
return verf_layer, hinzugefuegt, bereits_vorhanden, fehlgeschlagen
def entferne_aus_verfahrensgebiet(
verf_layer,
source_layer,
) -> Tuple[object, int, int, int]:
"""Entfernt Flurstücke aus dem Verfahrensgebiet.
Die Geometrien der zu entfernenden Flurstücke werden per ``difference()``
von der bestehenden Union-Geometrie subtrahiert.
Nicht enthaltene Flurstücke werden gezählt, aber ignoriert.
Parameters
----------
verf_layer :
Bestehender Verfahrensgebiet-Memory-Layer (Single-Feature).
source_layer :
Flurstück-Layer mit aktiver Auswahl.
Returns
-------
Tuple[verf_layer, entfernt, nicht_enthalten, fehlgeschlagen]
"""
existing_feats = list(verf_layer.getFeatures())
if not existing_feats:
auswahl_kz = {
str(f[FELD_KENNZEICHEN] or "")
for f in source_layer.selectedFeatures()
if f[FELD_KENNZEICHEN]
}
return verf_layer, 0, len(auswahl_kz), 0
existing_feat = existing_feats[0]
existing_kz_str = existing_feat[FELD_KENNZEICHEN] or ""
vorhandene_ids = {k.strip() for k in existing_kz_str.split(",") if k.strip()}
auswahl_kz = {
str(f[FELD_KENNZEICHEN] or "")
for f in source_layer.selectedFeatures()
if f[FELD_KENNZEICHEN]
}
nicht_enthalten = len(auswahl_kz - vorhandene_ids)
zu_entfernen_kz = auswahl_kz & vorhandene_ids
if not zu_entfernen_kz:
return verf_layer, 0, nicht_enthalten, 0
result_geom = existing_feat.geometry()
fehlgeschlagen = 0
tatsaechlich_entfernt = 0
for src_feat in source_layer.selectedFeatures():
kz = str(src_feat[FELD_KENNZEICHEN] or "")
if kz not in zu_entfernen_kz:
continue
try:
geom = src_feat.geometry()
if geom and not geom.isEmpty():
result_geom = result_geom.difference(geom)
tatsaechlich_entfernt += 1
except Exception:
fehlgeschlagen += 1
pr = verf_layer.dataProvider()
verbleibende_kz = sorted(vorhandene_ids - zu_entfernen_kz)
if not verbleibende_kz or result_geom is None or result_geom.isEmpty():
pr.deleteFeatures([existing_feat.id()])
else:
kz_idx = verf_layer.fields().indexFromName(FELD_KENNZEICHEN)
pr.changeGeometryValues({existing_feat.id(): result_geom})
pr.changeAttributeValues({existing_feat.id(): {
kz_idx: ",".join(verbleibende_kz),
}})
verf_layer.updateExtents()
verf_layer.triggerRepaint()
return verf_layer, tatsaechlich_entfernt, nicht_enthalten, fehlgeschlagen
def ersetze_verfahrensgebiet(
source_layer,
) -> Tuple[Optional[object], int, int]:
"""Erstellt einen neuen Verfahrensgebiet-Layer aus der Auswahl (Ersetzen-Semantik).
Der Aufrufer ist verantwortlich, den alten Layer aus dem Projekt zu entfernen,
bevor der neue Layer hinzugefügt wird.
Returns
-------
Tuple[layer, erfolgreich, fehlgeschlagen]
Entspricht ``erstelle_verfahrensgebiet_aus_auswahl``.
"""
return erstelle_verfahrensgebiet_aus_auswahl(source_layer)
-85
View File
@@ -1,85 +0,0 @@
import os
from qgis.core import (
QgsVectorLayer, QgsProject, QgsFeature, QgsField,
QgsFeatureRequest, QgsGeometry
)
from qgis.PyQt.QtWidgets import QMessageBox
from qgis.PyQt.QtCore import QVariant
from sn_basis import get_variable
alkis_NAS_url = "https://geodienste.sachsen.de/aaa/public_alkis/nas/wfs"
typename = "adv:AX_BauRaumOderBodenordnungsrecht"
def alkis_verfahrensgebiet(tab_widget):
verfahrensnummer = get_variable("verfahrensnummer")
if not verfahrensnummer:
QMessageBox.critical(tab_widget, "Fehler",
"Die Projektvariable 'sn_verfahrensnummer' ist nicht gesetzt.")
tab_widget.setze_haken(tab_widget.haken1, False)
return None, False
project = QgsProject.instance()
existing_layers = project.mapLayersByName("Verfahrensgebiet")
if existing_layers:
reply = QMessageBox.question(
tab_widget,
"Layer bereits vorhanden",
"Der Layer 'Verfahrensgebiet' existiert bereits.\n"
"Möchten Sie ihn löschen und neu laden?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No
)
if reply == QMessageBox.StandardButton.Yes:
for lyr in existing_layers:
project.removeMapLayer(lyr.id())
else:
# Vorhandenen Layer zurückgeben, kein Neu-Laden
return existing_layers[0], False
# WFS mit Filter laden
wfs_uri_find = (
f"url='{alkis_NAS_url}' typename='{typename}' srsname='EPSG:25833' "
f"sql=SELECT * FROM AX_BauRaumOderBodenordnungsrecht "
f"WHERE bezeichnung LIKE '{verfahrensnummer}%'"
)
temp_layer = QgsVectorLayer(wfs_uri_find, "TempVerfahrensgebiet", "WFS")
if not temp_layer.isValid():
QMessageBox.critical(tab_widget, "Fehler", "Layer konnte nicht geladen werden.")
return None, False
features = list(temp_layer.getFeatures())
if not features:
QMessageBox.critical(tab_widget, "Fehler",
f"Verfahrenskennzeichen {verfahrensnummer} nicht im WFS gefunden.")
return None, False
# Geometrie bestimmen
if len(features) == 1:
union_geom = features[0].geometry()
else:
geoms = [f.geometry() for f in features]
union_geom = QgsGeometry.unaryUnion(geoms)
if union_geom is None or union_geom.isEmpty():
QMessageBox.critical(tab_widget, "Fehler", "Keine Geometrien zum Verschmelzen gefunden.")
return None, False
# Memory-Layer mit festem Namen "Verfahrensgebiet"
crs = temp_layer.crs().authid()
memory_layer = QgsVectorLayer(f"Polygon?crs={crs}", "Verfahrensgebiet", "memory")
pr = memory_layer.dataProvider()
# Feld "VKZ" hinzufügen
pr.addAttributes([QgsField("VKZ", QVariant.String)])
memory_layer.updateFields()
# Feature mit Geometrie erstellen
feat_union = QgsFeature()
feat_union.setGeometry(union_geom)
feat_union.setAttributes([verfahrensnummer])
pr.addFeatures([feat_union])
memory_layer.updateExtents()
return memory_layer, True
+34 -6
View File
@@ -1,11 +1,21 @@
"""sn_verfahrensgebiet/main.py – Einstiegspunkt des sn_verfahrensgebiet Plugins.
Orchestriert die Verwaltung von Verfahrensgebiets-Layern und deren Eigenschaften.
"""
from qgis.utils import plugins
from sn_basis.ui.dockmanager import DockManager
from .ui.dockwidget import DockWidget
from sn_basis.modules.DataGrabber import DataGrabber
from sn_basis.modules.Pruefmanager import Pruefmanager
class Verfahrensgebiet:
"""Einstiegspunkt des sn_verfahrensgebiet-Plugins.
Verwaltet die DockWidget-Initialisierung und das Verfahrensgebiets-Management.
"""
def __init__(self, iface):
self.iface = iface
self.pruefmanager = Pruefmanager(ui_modus="qgis")
self.data_grabber = DataGrabber(pruefmanager=self.pruefmanager)
self.action = None
self.dockwidget = None
@@ -13,8 +23,11 @@ class Verfahrensgebiet:
self.plugin_name = self.__class__.__name__
self.dock_name = f"sn_dock_{self.plugin_name.lower()}"
def _basis(self):
return plugins.get("sn_basis")
def initGui(self):
basis = plugins.get("sn_basis")
basis = self._basis()
if basis and basis.ui:
self.action = basis.ui.add_action(
self.plugin_name,
@@ -31,14 +44,29 @@ class Verfahrensgebiet:
self.dockwidget = None
if self.action:
basis = plugins.get("sn_basis")
basis = self._basis()
if basis and basis.ui:
# Action aus Menü und Toolbar entfernen
basis.ui.remove_action(self.action)
self.action = None
# def run(self):
# if not self.dockwidget:
# self.dockwidget = DockWidget(self.iface.mainWindow(), subtitle=self.plugin_name)
# self.dockwidget.setObjectName(self.dock_name)
# self.dockwidget.action = self.action
# DockManager.show(self.dockwidget)
# basis = self._basis()
# if basis and basis.ui:
# basis.ui.set_active_plugin(self.action)
def run(self):
self.dockwidget = DockWidget(self.iface.mainWindow(), subtitle=self.plugin_name)
self.dockwidget = DockWidget(
self.iface.mainWindow(),
subtitle=self.plugin_name,
pruefmanager=self.pruefmanager,
data_grabber=self.data_grabber,
)
self.dockwidget.setObjectName(self.dock_name)
# Action-Referenz im Dock speichern
@@ -49,4 +77,4 @@ class Verfahrensgebiet:
# Toolbar-Button als aktiv markieren
basis = plugins.get("sn_basis")
if basis and basis.ui:
basis.ui.set_active_plugin(self.action)
basis.ui.set_active_plugin(self.action)
+10 -10
View File
@@ -1,13 +1,13 @@
[general]
version=26.4.3-unstable
name=LNO Sachsen | Verfahrensgebiet
qgisMinimumVersion=3.0
description=Plugin zum Erzeugen eines Objektes "Verfahrensgebiet"
version=25.11.3
author=Michael Otto
email=michael.otto@landkreis-mittelsachsen.de
about=Plugin zum Erzeugen eines Objektes "Verfahrensgebiet"
category=Plugins
homepage=https://entwicklung.vln-sn.de/AG_QGIS/Plugin_SN_Verfahrensgebiet
repository=https://entwicklung.vln-sn.de/AG_QGIS/Repository
supportsQt6=true
experimental=true
author=Daniel Helbig, Michael Otto
homepage=https://entwicklung.flurneuordnung-sachsen.de/AG_QGIS/Plugin_SN_Verfahrensgebiet
tracker=https://entwicklung.flurneuordnung-sachsen.de/AG_QGIS/Plugin_SN_Verfahrensgebiet/issues
repository=https://entwicklung.flurneuordnung-sachsen.de/AG_QGIS/Plugin_SN_Verfahrensgebiet/src/branch/unstable/
qgisMinimumVersion=3.40
experimental=true
deprecated=false
supportsQt6=true
+349
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>
+149
View File
@@ -0,0 +1,149 @@
<!DOCTYPE qgis PUBLIC 'http://mrcc.com/qgis.dtd' 'SYSTEM'>
<qgis layerType="Vector" styleCategories="Symbology" version="4.0.1-Norrköping">
<renderer-v2 enableorderby="0" forceraster="0" referencescale="-1" symbollevels="0" type="mergedFeatureRenderer">
<renderer-v2 enableorderby="0" forceraster="0" referencescale="-1" symbollevels="0" type="singleSymbol">
<symbols>
<symbol alpha="1" clip_to_extent="1" force_rhr="0" frame_rate="10" is_animated="0" name="0" type="fill">
<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 class="SimpleLine" enabled="1" id="{feca00b2-500a-4c9a-b285-67ba2d99d8f6}" locked="0" pass="0">
<Option type="Map">
<Option name="align_dash_pattern" type="QString" value="0"/>
<Option name="capstyle" type="QString" value="square"/>
<Option name="customdash" type="QString" value="5;2"/>
<Option name="customdash_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
<Option name="customdash_unit" type="QString" value="MM"/>
<Option name="dash_pattern_offset" type="QString" value="0"/>
<Option name="dash_pattern_offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
<Option name="dash_pattern_offset_unit" type="QString" value="MM"/>
<Option name="draw_inside_polygon" type="QString" value="0"/>
<Option name="joinstyle" type="QString" value="round"/>
<Option name="line_color" type="QString" value="215,168,255,255,rgb:0.8431373,0.6588235,1,1"/>
<Option name="line_style" type="QString" value="solid"/>
<Option name="line_width" type="QString" value="1.5"/>
<Option name="line_width_unit" type="QString" value="MM"/>
<Option name="offset" type="QString" value="-0.75"/>
<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="ring_filter" type="QString" value="0"/>
<Option name="trim_distance_end" type="QString" value="0"/>
<Option name="trim_distance_end_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
<Option name="trim_distance_end_unit" type="QString" value="MM"/>
<Option name="trim_distance_start" type="QString" value="0"/>
<Option name="trim_distance_start_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
<Option name="trim_distance_start_unit" type="QString" value="MM"/>
<Option name="tweak_dash_pattern_on_corners" type="QString" value="0"/>
<Option name="use_custom_dash" type="QString" value="0"/>
<Option name="width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
</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>
<layer class="SimpleLine" enabled="1" id="{fdc4d6fd-0995-41df-bbfb-19970b4fc2cc}" locked="0" pass="0">
<Option type="Map">
<Option name="align_dash_pattern" type="QString" value="0"/>
<Option name="capstyle" type="QString" value="square"/>
<Option name="customdash" type="QString" value="5;2"/>
<Option name="customdash_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
<Option name="customdash_unit" type="QString" value="MM"/>
<Option name="dash_pattern_offset" type="QString" value="0"/>
<Option name="dash_pattern_offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
<Option name="dash_pattern_offset_unit" type="QString" value="MM"/>
<Option name="draw_inside_polygon" type="QString" value="0"/>
<Option name="joinstyle" type="QString" value="round"/>
<Option name="line_color" type="QString" value="204,174,137,255,rgb:0.8,0.682353,0.5372549,1"/>
<Option name="line_style" type="QString" value="solid"/>
<Option name="line_width" type="QString" value="0.5"/>
<Option name="line_width_unit" type="QString" value="MM"/>
<Option name="offset" type="QString" value="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="ring_filter" type="QString" value="0"/>
<Option name="trim_distance_end" type="QString" value="0"/>
<Option name="trim_distance_end_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
<Option name="trim_distance_end_unit" type="QString" value="MM"/>
<Option name="trim_distance_start" type="QString" value="0"/>
<Option name="trim_distance_start_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
<Option name="trim_distance_start_unit" type="QString" value="MM"/>
<Option name="tweak_dash_pattern_on_corners" type="QString" value="0"/>
<Option name="use_custom_dash" type="QString" value="0"/>
<Option name="width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
</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>
<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 alpha="1" clip_to_extent="1" force_rhr="0" frame_rate="10" is_animated="0" name="" type="fill">
<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 class="SimpleFill" enabled="1" id="{f18003f5-220a-487f-8a6d-b24facc4c1a5}" locked="0" 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.1372549,0.1372549,0.1372549,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>
<blendMode>0</blendMode>
<featureBlendMode>0</featureBlendMode>
<layerGeometryType>2</layerGeometryType>
</qgis>
+1
View File
@@ -0,0 +1 @@
"""Tests für das Plugin sn_verfahrensgebiet."""
+86
View File
@@ -0,0 +1,86 @@
import sys
import types
import unittest
from unittest.mock import patch
if "qgis" not in sys.modules:
qgis_mod = types.ModuleType("qgis")
qgis_utils = types.ModuleType("qgis.utils")
qgis_utils.iface = types.SimpleNamespace()
qgis_utils.plugins = {}
qgis_mod.utils = qgis_utils
sys.modules["qgis"] = qgis_mod
sys.modules["qgis.utils"] = qgis_utils
from sn_verfahrensgebiet.ui.dockwidget import DockWidget
class _DummyTabWidget:
def __init__(self):
self.tabs = []
def addTab(self, widget, title):
self.tabs.append((widget, title))
class _TabWithServices:
tab_title = "WithServices"
def __init__(self, _parent):
self.services = None
def set_services(self, pruefmanager=None, data_grabber=None):
self.services = (pruefmanager, data_grabber)
class _TabWithoutServices:
tab_title = "WithoutServices"
def __init__(self, _parent):
pass
class _TabRaises:
tab_title = "Broken"
def __init__(self, _parent):
raise RuntimeError("boom")
class TestDockWidget(unittest.TestCase):
@patch("sn_verfahrensgebiet.ui.dockwidget.QTabWidget", _DummyTabWidget)
def test_init_tabs_with_services_injects_available_services(self):
obj = DockWidget.__new__(DockWidget)
obj.tabs = [_TabWithServices, _TabWithoutServices]
obj.pruefmanager = object()
obj.data_grabber = object()
obj._set_widget_calls = []
obj.setWidget = lambda value: obj._set_widget_calls.append(value)
DockWidget._init_tabs_with_services(obj)
self.assertEqual(len(obj._set_widget_calls), 2)
tab_widget = obj._set_widget_calls[-1]
self.assertIsInstance(tab_widget, _DummyTabWidget)
self.assertEqual(len(tab_widget.tabs), 2)
first_tab = tab_widget.tabs[0][0]
self.assertEqual(first_tab.services, (obj.pruefmanager, obj.data_grabber))
@patch("sn_verfahrensgebiet.ui.dockwidget.error")
@patch("sn_verfahrensgebiet.ui.dockwidget.QTabWidget", _DummyTabWidget)
def test_init_tabs_with_services_reports_errors(self, mock_error):
obj = DockWidget.__new__(DockWidget)
obj.tabs = [_TabRaises]
obj.pruefmanager = object()
obj.data_grabber = object()
obj.setWidget = lambda _value: None
DockWidget._init_tabs_with_services(obj)
mock_error.assert_called_once()
if __name__ == "__main__":
unittest.main()
+223
View File
@@ -0,0 +1,223 @@
"""sn_verfahrensgebiet/tests/test_flurstueck_alkis.py
Unit-Tests für das flurstueck_alkis-Modul.
Getestet werden:
- Nutzerabbruch bei ask_text → (None, LoadStatus.NONE)
- Ungültiger WFS-Layer → (None, LoadStatus.NONE)
- Erfolgreicher Ladevorgang → gültiger Layer + LoadStatus.FIRST
- Bestehender Layer: Nutzer wählt "Neu laden" → LoadStatus.RELOAD
- Bestehender Layer: Nutzer wählt "Behalten" → LoadStatus.KEEP
"""
import sys
import types
import unittest
from unittest.mock import patch, MagicMock
# ---------------------------------------------------------------------------
# Minimales QGIS-Mock-Setup
# ---------------------------------------------------------------------------
if "qgis" not in sys.modules:
qgis_mod = types.ModuleType("qgis")
qgis_core = types.ModuleType("qgis.core")
qgis_utils = types.ModuleType("qgis.utils")
qgis_utils.iface = types.SimpleNamespace()
qgis_utils.plugins = {}
qgis_mod.utils = qgis_utils
qgis_mod.core = qgis_core
sys.modules["qgis"] = qgis_mod
sys.modules["qgis.core"] = qgis_core
sys.modules["qgis.utils"] = qgis_utils
from sn_verfahrensgebiet.functions.flurstueck_alkis import flurstueck_alkis, FLST_LAYER_NAME
from sn_verfahrensgebiet.functions.verfahrensgebiet_alkis import LoadStatus
# ---------------------------------------------------------------------------
# Dummies
# ---------------------------------------------------------------------------
class _DummyLayer:
def __init__(self, valid: bool, layer_id="L1"):
self._valid = valid
self._id = layer_id
def isValid(self):
return self._valid
def id(self):
return self._id
def extent(self):
return object()
class _DummyProject:
def __init__(self, named_layers=None):
self._named_layers = named_layers or []
self.removed = []
def mapLayersByName(self, name):
return self._named_layers
def removeMapLayer(self, lid):
self.removed.append(lid)
# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------
class TestFlurstueckAlkis(unittest.TestCase):
# ------------------------------------------------------------------ #
# 1. Nutzerabbruch (ask_text liefert ok=False / None) #
# ------------------------------------------------------------------ #
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QMessageBox")
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance")
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text")
def test_user_cancel_returns_none(self, mock_ask_text, mock_project, mock_msgbox):
"""ask_text → ok=False, text="" → (None, NONE)."""
mock_ask_text.return_value = ("", False)
mock_project.return_value = _DummyProject()
layer, status = flurstueck_alkis(None)
self.assertIsNone(layer)
self.assertEqual(status, LoadStatus.NONE)
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QMessageBox")
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance")
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text")
def test_empty_text_after_confirmation_returns_none(self, mock_ask_text, mock_project, mock_msgbox):
"""ask_text → ok=True, aber Text leer → (None, NONE)."""
mock_ask_text.return_value = (" ", True)
mock_project.return_value = _DummyProject()
layer, status = flurstueck_alkis(None)
self.assertIsNone(layer)
self.assertEqual(status, LoadStatus.NONE)
# ------------------------------------------------------------------ #
# 2. Ungültiger WFS-Layer #
# ------------------------------------------------------------------ #
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QMessageBox")
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsVectorLayer")
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance")
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text")
def test_invalid_wfs_layer_returns_none(
self, mock_ask_text, mock_project, mock_wfs_layer_cls, mock_msgbox
):
"""WFS-Layer nicht valide → (None, NONE)."""
mock_ask_text.return_value = ("1234", True)
mock_project.return_value = _DummyProject()
mock_wfs_layer_cls.return_value = _DummyLayer(valid=False)
layer, status = flurstueck_alkis(None)
self.assertIsNone(layer)
self.assertEqual(status, LoadStatus.NONE)
# ------------------------------------------------------------------ #
# 3. Erfolgreicher Erstladevorgang #
# ------------------------------------------------------------------ #
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsVectorLayer")
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance")
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text")
def test_success_returns_layer_and_first_status(
self, mock_ask_text, mock_project, mock_wfs_layer_cls
):
"""Kein bestehender Layer, WFS valide → (layer, FIRST)."""
mock_ask_text.return_value = ("1234, 5678", True)
mock_project.return_value = _DummyProject()
valid_layer = _DummyLayer(valid=True, layer_id="wfs_ok")
mock_wfs_layer_cls.return_value = valid_layer
layer, status = flurstueck_alkis(None)
self.assertIs(layer, valid_layer)
self.assertEqual(status, LoadStatus.FIRST)
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsVectorLayer")
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance")
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text")
def test_sql_filter_contains_all_schluessel(
self, mock_ask_text, mock_project, mock_wfs_layer_cls
):
"""Der WFS-URI enthält alle eingegebenen Gemarkungsschlüssel."""
mock_ask_text.return_value = ("1111, 2222, 3333", True)
mock_project.return_value = _DummyProject()
mock_wfs_layer_cls.return_value = _DummyLayer(valid=True)
flurstueck_alkis(None)
# Erster Positionsarg von QgsVectorLayer ist der URI-String
call_args = mock_wfs_layer_cls.call_args
uri_str = call_args[0][0] if call_args[0] else call_args[1].get("path", "")
self.assertIn("1111", uri_str)
self.assertIn("2222", uri_str)
self.assertIn("3333", uri_str)
# ------------------------------------------------------------------ #
# 4. Bestehender Layer – Nutzer wählt "Neu laden" #
# ------------------------------------------------------------------ #
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsVectorLayer")
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QMessageBox")
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance")
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text")
def test_existing_layer_reload_yes(
self, mock_ask_text, mock_project, mock_msgbox_cls, mock_wfs_layer_cls
):
"""Bestehender Layer + Nutzer: 'Ja, neu laden' → LoadStatus.RELOAD."""
existing = _DummyLayer(valid=True, layer_id="old_flst")
project = _DummyProject(named_layers=[existing])
mock_ask_text.return_value = ("1234", True)
mock_project.return_value = project
# StandardButton als int, damit | Operator funktioniert
mock_msgbox_cls.StandardButton.Yes = 0x4000
mock_msgbox_cls.StandardButton.No = 0x10000
# QMessageBox.question → Yes
mock_msgbox_cls.question.return_value = 0x4000
new_layer = _DummyLayer(valid=True, layer_id="new_flst")
mock_wfs_layer_cls.return_value = new_layer
layer, status = flurstueck_alkis(None)
self.assertIn(existing.id(), project.removed)
self.assertIs(layer, new_layer)
self.assertEqual(status, LoadStatus.RELOAD)
# ------------------------------------------------------------------ #
# 5. Bestehender Layer – Nutzer wählt "Behalten" #
# ------------------------------------------------------------------ #
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QMessageBox")
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance")
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text")
def test_existing_layer_keep_no(
self, mock_ask_text, mock_project, mock_msgbox_cls
):
"""Bestehender Layer + Nutzer: 'Nein, behalten' → LoadStatus.KEEP."""
existing = _DummyLayer(valid=True, layer_id="keep_flst")
project = _DummyProject(named_layers=[existing])
mock_ask_text.return_value = ("1234", True)
mock_project.return_value = project
# StandardButton als int, damit | Operator funktioniert
mock_msgbox_cls.StandardButton.Yes = 0x4000
mock_msgbox_cls.StandardButton.No = 0x10000
# QMessageBox.question → No
mock_msgbox_cls.question.return_value = 0x10000
layer, status = flurstueck_alkis(None)
self.assertIs(layer, existing)
self.assertEqual(status, LoadStatus.KEEP)
self.assertEqual(project.removed, []) # nichts gelöscht
if __name__ == "__main__":
unittest.main()
+121
View File
@@ -0,0 +1,121 @@
import sys
import types
import unittest
from types import SimpleNamespace
from unittest.mock import Mock, patch
if "qgis" not in sys.modules:
qgis_mod = types.ModuleType("qgis")
qgis_utils = types.ModuleType("qgis.utils")
qgis_utils.iface = types.SimpleNamespace()
qgis_utils.plugins = {}
qgis_mod.utils = qgis_utils
sys.modules["qgis"] = qgis_mod
sys.modules["qgis.utils"] = qgis_utils
from sn_verfahrensgebiet.main import Verfahrensgebiet
class _DummyIface:
def __init__(self):
self.removed = []
def mainWindow(self):
return object()
def removeDockWidget(self, dock):
self.removed.append(dock)
class _DummyDock:
def __init__(self):
self.object_name = None
self.action = None
self.deleted = False
def setObjectName(self, name):
self.object_name = name
def deleteLater(self):
self.deleted = True
class TestPluginMain(unittest.TestCase):
@patch("sn_verfahrensgebiet.main.DataGrabber")
@patch("sn_verfahrensgebiet.main.Pruefmanager")
def test_init_sets_services_and_names(self, mock_pruefmanager, mock_data_grabber):
pm = object()
dg = object()
mock_pruefmanager.return_value = pm
mock_data_grabber.return_value = dg
plugin = Verfahrensgebiet(_DummyIface())
self.assertIs(plugin.pruefmanager, pm)
self.assertIs(plugin.data_grabber, dg)
self.assertEqual(plugin.plugin_name, "Verfahrensgebiet")
self.assertEqual(plugin.dock_name, "sn_dock_verfahrensgebiet")
@patch("sn_verfahrensgebiet.main.plugins", new_callable=dict)
def test_initGui_registers_action_when_basis_ui_exists(self, mock_plugins):
iface = _DummyIface()
plugin = Verfahrensgebiet(iface)
ui = SimpleNamespace(
add_action=Mock(return_value="action_obj"),
finalize_menu_and_toolbar=Mock(),
)
mock_plugins["sn_basis"] = SimpleNamespace(ui=ui)
plugin.initGui()
self.assertEqual(plugin.action, "action_obj")
ui.add_action.assert_called_once()
ui.finalize_menu_and_toolbar.assert_called_once()
@patch("sn_verfahrensgebiet.main.plugins", new_callable=dict)
@patch("sn_verfahrensgebiet.main.DockManager.show")
@patch("sn_verfahrensgebiet.main.DockWidget")
def test_run_creates_dock_and_marks_toolbar_active(self, mock_dockwidget_cls, mock_show, mock_plugins):
iface = _DummyIface()
plugin = Verfahrensgebiet(iface)
plugin.action = "action_obj"
dock = _DummyDock()
mock_dockwidget_cls.return_value = dock
ui = SimpleNamespace(set_active_plugin=Mock())
mock_plugins["sn_basis"] = SimpleNamespace(ui=ui)
plugin.run()
self.assertIs(plugin.dockwidget, dock)
self.assertEqual(dock.object_name, "sn_dock_verfahrensgebiet")
self.assertEqual(dock.action, "action_obj")
mock_show.assert_called_once_with(dock)
ui.set_active_plugin.assert_called_once_with("action_obj")
@patch("sn_verfahrensgebiet.main.plugins", new_callable=dict)
def test_unload_removes_dock_and_action(self, mock_plugins):
iface = _DummyIface()
plugin = Verfahrensgebiet(iface)
dock = _DummyDock()
plugin.dockwidget = dock
plugin.action = "action_obj"
ui = SimpleNamespace(remove_action=Mock())
mock_plugins["sn_basis"] = SimpleNamespace(ui=ui)
plugin.unload()
self.assertIn(dock, iface.removed)
self.assertTrue(dock.deleted)
self.assertIsNone(plugin.dockwidget)
ui.remove_action.assert_called_once_with("action_obj")
self.assertIsNone(plugin.action)
if __name__ == "__main__":
unittest.main()
+184
View File
@@ -0,0 +1,184 @@
import sys
import types
import unittest
from unittest.mock import patch
if "qgis" not in sys.modules:
qgis_mod = types.ModuleType("qgis")
qgis_utils = types.ModuleType("qgis.utils")
qgis_utils.iface = types.SimpleNamespace()
qgis_utils.plugins = {}
qgis_mod.utils = qgis_utils
sys.modules["qgis"] = qgis_mod
sys.modules["qgis.utils"] = qgis_utils
from sn_verfahrensgebiet.functions import verfahrensgebiet_alkis as vg
class _DummyMessageBox:
class StandardButton:
Yes = 1
No = 2
last_critical = None
next_reply = StandardButton.No
@staticmethod
def question(_parent, _title, _message, _buttons, _default):
return _DummyMessageBox.next_reply
@staticmethod
def critical(parent, title, message):
_DummyMessageBox.last_critical = (parent, title, message)
class _DummyProject:
def __init__(self, layers=None):
self._layers = layers or []
self.removed = []
def mapLayersByName(self, _name):
return self._layers
def removeMapLayer(self, layer_id):
self.removed.append(layer_id)
class _DummyExistingLayer:
def __init__(self, layer_id="existing"):
self._id = layer_id
def id(self):
return self._id
class _DummyGeom:
def __init__(self, empty=False):
self._empty = empty
def isEmpty(self):
return self._empty
class _DummyFeature:
def __init__(self, geom=None):
self._geom = geom or _DummyGeom()
def geometry(self):
return self._geom
class _DummyTempLayer:
def __init__(self, valid=True, features=None):
self._valid = valid
self._features = features or []
def isValid(self):
return self._valid
def getFeatures(self):
return self._features
def crs(self):
class _Crs:
@staticmethod
def authid():
return "EPSG:25833"
return _Crs()
class _DummyProvider:
def addAttributes(self, _attrs):
return True
def addFeatures(self, _features):
return True
class _DummyMemoryLayer:
def __init__(self):
self._provider = _DummyProvider()
def dataProvider(self):
return self._provider
def updateFields(self):
return None
def updateExtents(self):
return None
class TestVerfahrensgebietAlkis(unittest.TestCase):
@patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.QgsProject.instance")
@patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.QMessageBox", new=_DummyMessageBox)
def test_check_existing_layer_first_load(self, mock_project_instance):
mock_project_instance.return_value = _DummyProject(layers=[])
layer, status = vg._check_existing_layer(tab_widget=None)
self.assertIsNone(layer)
self.assertEqual(status, vg.LoadStatus.FIRST)
@patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.QgsProject.instance")
@patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.QMessageBox", new=_DummyMessageBox)
def test_check_existing_layer_keep_existing(self, mock_project_instance):
existing = _DummyExistingLayer("L1")
mock_project_instance.return_value = _DummyProject(layers=[existing])
_DummyMessageBox.next_reply = _DummyMessageBox.StandardButton.No
layer, status = vg._check_existing_layer(tab_widget=None)
self.assertIs(layer, existing)
self.assertEqual(status, vg.LoadStatus.KEEP)
@patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.QMessageBox", new=_DummyMessageBox)
@patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.get_variable", return_value="")
def test_verfahrensgebiet_alkis_fails_when_verfahrensnummer_missing(self, _mock_get_var):
_DummyMessageBox.last_critical = None
layer, status = vg.verfahrensgebiet_alkis(tab_widget=None)
self.assertIsNone(layer)
self.assertEqual(status, vg.LoadStatus.NONE)
self.assertIsNotNone(_DummyMessageBox.last_critical)
@patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis._check_existing_layer", return_value=(None, vg.LoadStatus.FIRST))
@patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.QMessageBox", new=_DummyMessageBox)
@patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.get_variable", return_value="VKZ123")
@patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.QgsVectorLayer")
def test_verfahrensgebiet_alkis_fails_on_invalid_wfs_layer(self, mock_qgsvectorlayer, _mock_get_var, _mock_check):
_DummyMessageBox.last_critical = None
mock_qgsvectorlayer.return_value = _DummyTempLayer(valid=False)
layer, status = vg.verfahrensgebiet_alkis(tab_widget=None)
self.assertIsNone(layer)
self.assertEqual(status, vg.LoadStatus.NONE)
self.assertIsNotNone(_DummyMessageBox.last_critical)
@patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.QgsFeature")
@patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.QgsGeometry.unaryUnion", return_value=_DummyGeom(empty=False))
@patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis._check_existing_layer", return_value=(None, vg.LoadStatus.FIRST))
@patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.get_variable", return_value="VKZ123")
@patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.QgsVectorLayer")
@patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.QMessageBox", new=_DummyMessageBox)
def test_verfahrensgebiet_alkis_success_returns_memory_layer(self, mock_qgsvectorlayer, _mock_get_var, _mock_check, _mock_union, mock_feature_cls):
temp_layer = _DummyTempLayer(valid=True, features=[_DummyFeature()])
memory_layer = _DummyMemoryLayer()
mock_qgsvectorlayer.side_effect = [temp_layer, memory_layer]
mock_feature = mock_feature_cls.return_value
mock_feature.setGeometry.return_value = None
mock_feature.setAttributes.return_value = None
layer, status = vg.verfahrensgebiet_alkis(tab_widget=None)
self.assertIs(layer, memory_layer)
self.assertEqual(status, vg.LoadStatus.FIRST)
if __name__ == "__main__":
unittest.main()
+595
View File
@@ -0,0 +1,595 @@
"""sn_verfahrensgebiet/tests/test_verfahrensgebiet_edit_service.py
Unit-Tests für das Verfahrensgebiet-Edit-Service-Modul.
Getestet werden:
- pruefe_flurstueck_layer (über Layerpruefer-Integration)
- pruefe_auswahl
- erstelle_verfahrensgebiet_aus_auswahl: Erfolgs- und Fehlerpfade
- ergaenze_verfahrensgebiet: nur neue IDs werden hinzugefügt
- entferne_aus_verfahrensgebiet: nur vorhandene IDs werden entfernt
- ersetze_verfahrensgebiet: Ergebnis entspricht Erstellen
- get_kennzeichen_menge: Hilfsfunktion
- Zähler (erfolgreich/fehlgeschlagen/bereits_vorhanden/nicht_enthalten)
"""
import sys
import types
import unittest
from unittest.mock import patch, MagicMock
# ---------------------------------------------------------------------------
# Minimales QGIS-Mock-Setup (muss vor allen sn_*-Importen erfolgen)
# ---------------------------------------------------------------------------
if "qgis" not in sys.modules:
qgis_mod = types.ModuleType("qgis")
qgis_core = types.ModuleType("qgis.core")
qgis_utils = types.ModuleType("qgis.utils")
qgis_utils.iface = types.SimpleNamespace()
qgis_utils.plugins = {}
qgis_mod.utils = qgis_utils
qgis_mod.core = qgis_core
sys.modules["qgis"] = qgis_mod
sys.modules["qgis.core"] = qgis_core
sys.modules["qgis.utils"] = qgis_utils
from sn_verfahrensgebiet.functions.verfahrensgebiet_edit_service import (
pruefe_flurstueck_layer,
pruefe_auswahl,
erstelle_verfahrensgebiet_aus_auswahl,
ergaenze_verfahrensgebiet,
entferne_aus_verfahrensgebiet,
ersetze_verfahrensgebiet,
get_kennzeichen_menge,
FELD_KENNZEICHEN,
FELD_GEMARKUNG,
)
# ---------------------------------------------------------------------------
# Test-Dummies
# ---------------------------------------------------------------------------
class _DummyGeom:
def __init__(self, empty=False):
self._empty = empty
def isEmpty(self):
return self._empty
def difference(self, other):
"""Dummy-Differenz: immer nicht-leeres Ergebnis (kein echter Geometrieschnitt)."""
return _DummyGeom(empty=False)
class _DummyFeature:
"""Feature-Dummy mit dict-artigem Attributzugriff und konfigurierbarer Geometrie."""
def __init__(self, kz: str, gemarkung: str = "1234", geom_empty: bool = False, feat_id: int = 0):
self._attrs = {FELD_KENNZEICHEN: kz, FELD_GEMARKUNG: gemarkung}
self._geom_empty = geom_empty
self._id = feat_id
self._set_geom = None
self._set_attrs = None
def __getitem__(self, key):
return self._attrs.get(key, None)
def geometry(self):
return _DummyGeom(self._geom_empty)
def id(self):
return self._id
def setGeometry(self, g):
self._set_geom = g
def setAttributes(self, attrs):
self._set_attrs = attrs
class _DummyProvider:
"""DataProvider-Dummy für Single-Feature-Verfahrensgebiet-Layer."""
def __init__(self, layer_ref):
self._layer = layer_ref
def addAttributes(self, attrs):
pass
def addFeatures(self, feats):
self._layer._features.extend(feats)
return True
def deleteFeatures(self, ids):
self._layer._deleted_ids.extend(ids)
return True
def changeAttributeValues(self, changes: dict):
"""changes: {fid: {field_idx: value}}"""
for feat in self._layer._features:
fid = feat.id() if callable(getattr(feat, "id", None)) else None
if fid in changes:
for field_idx, value in changes[fid].items():
feat._attr_by_idx[field_idx] = value
return True
def changeGeometryValues(self, changes: dict):
"""changes: {fid: new_geom}"""
for feat in self._layer._features:
fid = feat.id() if callable(getattr(feat, "id", None)) else None
if fid in changes:
feat._set_geom = changes[fid]
return True
class _DummySingleFeature:
"""Single-Feature-Dummy für den Verfahrensgebiet-Layer.
Speichert Kennzeichen als kommagetrennte Zeichenkette (new-style).
Feld-Indizes: 0=FELD_KENNZEICHEN, 1=FELD_GEMARKUNG
"""
def __init__(self, kz_komma: str, gemarkung: str = "", feat_id: int = 0, geom_empty: bool = False):
self._kz_komma = kz_komma
self._gemarkung = gemarkung
self._id = feat_id
self._geom_empty = geom_empty
self._set_geom = None
# Attribut-Mapping nach Index für changeAttributeValues
self._attr_by_idx: dict = {}
def __getitem__(self, key):
if key == FELD_KENNZEICHEN:
return self._attr_by_idx.get(0, self._kz_komma)
if key == FELD_GEMARKUNG:
return self._attr_by_idx.get(1, self._gemarkung)
return None
def geometry(self):
return _DummyGeom(self._geom_empty)
def id(self):
return self._id
def setGeometry(self, g):
self._set_geom = g
def setAttributes(self, attrs):
if len(attrs) > 0:
self._kz_komma = attrs[0]
if len(attrs) > 1:
self._gemarkung = attrs[1]
class _DummyVerfLayer:
"""Memory-Layer-Dummy für Verfahrensgebiet (Single-Feature-Modell)."""
def __init__(self, single_feature=None):
self._features = [single_feature] if single_feature else []
self._deleted_ids = []
def getFeatures(self):
deleted = set(self._deleted_ids)
return [
f for f in self._features
if (f.id() if callable(getattr(f, "id", None)) else None) not in deleted
]
def dataProvider(self):
return _DummyProvider(self)
def fields(self):
class _Fields:
def indexFromName(self, name):
if name == FELD_KENNZEICHEN:
return 0
if name == FELD_GEMARKUNG:
return 1
return -1
return _Fields()
def updateExtents(self):
pass
def triggerRepaint(self):
pass
def updateFields(self):
pass
def isValid(self):
return True
def crs(self):
class _Crs:
def authid(self):
return "EPSG:25833"
return _Crs()
class _DummySourceLayer:
"""Flurstück-Quell-Layer-Dummy mit konfigurierbarer Auswahl."""
def __init__(self, selected=None, all_features=None, feature_count=None):
self._selected = list(selected or [])
self._all = list(all_features or [])
self._count = feature_count if feature_count is not None else len(self._selected)
def selectedFeatures(self):
return self._selected
def selectedFeatureCount(self):
return self._count
def getFeatures(self):
return self._all
def crs(self):
class _Crs:
def authid(self):
return "EPSG:25833"
return _Crs()
class _MockLayerpruefer:
"""Steuerbarer Mock für Layerpruefer.pruefe()."""
def __init__(self, ok, aktion="ok", meldung=""):
self._ok = ok
self._aktion = aktion
self._meldung = meldung
def pruefe(self):
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
return pruef_ergebnis(ok=self._ok, aktion=self._aktion, meldung=self._meldung)
# ---------------------------------------------------------------------------
# Tests: pruefe_flurstueck_layer
# ---------------------------------------------------------------------------
class TestPruefeFlurstueckLayer(unittest.TestCase):
def test_layer_ok_passes(self):
"""Gültiger Polygon-Vektor-Layer mit Pflichtfeldern → ok=True."""
import sn_verfahrensgebiet.functions.verfahrensgebiet_edit_service as module
original = module.Layerpruefer
class _PassPruefer:
def __init__(self, **_):
pass
def pruefe(self):
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
return pruef_ergebnis(ok=True, aktion="ok")
module.Layerpruefer = _PassPruefer
try:
ergebnis = pruefe_flurstueck_layer(object())
self.assertTrue(ergebnis.ok)
self.assertEqual(ergebnis.aktion, "ok")
finally:
module.Layerpruefer = original
def test_layer_missing_field_fails(self):
"""Layer ohne Pflichtfelder → ok=False, aktion=felder_fehlen."""
import sn_verfahrensgebiet.functions.verfahrensgebiet_edit_service as module
original = module.Layerpruefer
class _FailPruefer:
def __init__(self, **_):
pass
def pruefe(self):
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
return pruef_ergebnis(ok=False, aktion="felder_fehlen",
meldung="Feld fehlt")
module.Layerpruefer = _FailPruefer
try:
ergebnis = pruefe_flurstueck_layer(object())
self.assertFalse(ergebnis.ok)
self.assertEqual(ergebnis.aktion, "felder_fehlen")
finally:
module.Layerpruefer = original
def test_layer_wrong_geometry_fails(self):
"""Layer mit falschem Geometrietyp → ok=False, aktion=falscher_geotyp."""
import sn_verfahrensgebiet.functions.verfahrensgebiet_edit_service as module
original = module.Layerpruefer
class _WrongGeomPruefer:
def __init__(self, **_):
pass
def pruefe(self):
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
return pruef_ergebnis(ok=False, aktion="falscher_geotyp",
meldung="Kein Polygon")
module.Layerpruefer = _WrongGeomPruefer
try:
ergebnis = pruefe_flurstueck_layer(object())
self.assertFalse(ergebnis.ok)
self.assertEqual(ergebnis.aktion, "falscher_geotyp")
finally:
module.Layerpruefer = original
# ---------------------------------------------------------------------------
# Tests: pruefe_auswahl
# ---------------------------------------------------------------------------
class TestPruefeAuswahl(unittest.TestCase):
def test_no_selection_fails(self):
layer = _DummySourceLayer(selected=[], feature_count=0)
ergebnis = pruefe_auswahl(layer)
self.assertFalse(ergebnis.ok)
self.assertEqual(ergebnis.aktion, "layer_leer")
def test_with_selection_passes(self):
feats = [_DummyFeature("1000-001")]
layer = _DummySourceLayer(selected=feats)
ergebnis = pruefe_auswahl(layer)
self.assertTrue(ergebnis.ok)
self.assertIn("1", ergebnis.meldung) # "1 Flurstücke ausgewählt."
def test_selectedFeatureCount_exception_treated_as_zero(self):
class _BrokenLayer:
def selectedFeatureCount(self):
raise RuntimeError("broken")
ergebnis = pruefe_auswahl(_BrokenLayer())
self.assertFalse(ergebnis.ok)
# ---------------------------------------------------------------------------
# Tests: get_kennzeichen_menge
# ---------------------------------------------------------------------------
class TestGetKennzeichenMenge(unittest.TestCase):
def test_returns_set_of_ids(self):
"""Kommagetrennte Kennzeichen im Single-Feature werden korrekt geparst."""
feat = _DummySingleFeature("A001,A002,A001", feat_id=1) # Duplikat → set
layer = _DummyVerfLayer(feat)
result = get_kennzeichen_menge(layer)
self.assertEqual(result, {"A001", "A002"})
def test_empty_layer_returns_empty_set(self):
layer = _DummyVerfLayer()
result = get_kennzeichen_menge(layer)
self.assertEqual(result, set())
def test_single_kennzeichen_without_comma(self):
"""Einzelnes Kennzeichen ohne Komma wird korrekt erkannt."""
feat = _DummySingleFeature("X999", feat_id=1)
layer = _DummyVerfLayer(feat)
result = get_kennzeichen_menge(layer)
self.assertEqual(result, {"X999"})
# ---------------------------------------------------------------------------
# Tests: erstelle_verfahrensgebiet_aus_auswahl
# ---------------------------------------------------------------------------
class TestErstelle(unittest.TestCase):
def _patch_all(self, test_func, union_empty=False):
"""Patcht _erstelle_memory_layer und QgsGeometry.unaryUnion."""
import sn_verfahrensgebiet.functions.verfahrensgebiet_edit_service as module
orig_erstelle = module._erstelle_memory_layer
orig_union = module.QgsGeometry.unaryUnion
mem = _DummyVerfLayer()
module._erstelle_memory_layer = lambda _crs: mem
module.QgsGeometry.unaryUnion = staticmethod(
lambda _geoms: _DummyGeom(empty=union_empty)
)
try:
test_func(mem)
finally:
module._erstelle_memory_layer = orig_erstelle
module.QgsGeometry.unaryUnion = staticmethod(orig_union)
def test_success_counts(self):
"""3 gültige Features → erfolgreich=3, fehlgeschlagen=0, 1 Union-Feature im Layer."""
feats = [
_DummyFeature("A001", feat_id=0),
_DummyFeature("A002", feat_id=1),
_DummyFeature("A003", feat_id=2),
]
source = _DummySourceLayer(selected=feats)
def run(mem):
layer, ok, fail = erstelle_verfahrensgebiet_aus_auswahl(source)
self.assertIs(layer, mem)
self.assertEqual(ok, 3)
self.assertEqual(fail, 0)
# Single-Feature-Modell: genau 1 Union-Feature
self.assertEqual(len(mem._features), 1)
self._patch_all(run)
def test_partial_failure_on_empty_geometry(self):
"""2 ok + 1 leere Geometrie → erfolgreich=2, fehlgeschlagen=1."""
feats = [
_DummyFeature("A001"),
_DummyFeature("A002", geom_empty=True),
_DummyFeature("A003"),
]
source = _DummySourceLayer(selected=feats)
def run(mem):
_, ok, fail = erstelle_verfahrensgebiet_aus_auswahl(source)
self.assertEqual(ok, 2)
self.assertEqual(fail, 1)
self._patch_all(run)
def test_all_fail_on_empty_geometries(self):
"""Alle Features haben leere Geometrie → erfolgreich=0, kein Feature im Layer."""
feats = [_DummyFeature("A001", geom_empty=True)]
source = _DummySourceLayer(selected=feats)
def run(mem):
_, ok, fail = erstelle_verfahrensgebiet_aus_auswahl(source)
self.assertEqual(ok, 0)
self.assertEqual(fail, 1)
self.assertEqual(len(mem._features), 0) # kein Union-Feature
self._patch_all(run)
# ---------------------------------------------------------------------------
# Tests: ergaenze_verfahrensgebiet
# ---------------------------------------------------------------------------
class TestErgaenzeVerfahrensgebiet(unittest.TestCase):
def _patch_union(self):
import sn_verfahrensgebiet.functions.verfahrensgebiet_edit_service as module
orig = module.QgsGeometry.unaryUnion
module.QgsGeometry.unaryUnion = staticmethod(lambda _g: _DummyGeom(empty=False))
return module, orig
def test_adds_only_new_ids(self):
"""Auswahl enthält 1 neue + 1 bereits vorhandene ID → hinzugefügt=1."""
verf = _DummyVerfLayer(_DummySingleFeature("A001", feat_id=1))
selected = [
_DummyFeature("A001"), # bereits vorhanden
_DummyFeature("A002"), # neu
]
source = _DummySourceLayer(selected=selected)
module, orig = self._patch_union()
try:
_, hinzugefuegt, bereits_vorhanden, fehlgeschlagen = ergaenze_verfahrensgebiet(
verf, source
)
finally:
module.QgsGeometry.unaryUnion = staticmethod(orig)
self.assertEqual(hinzugefuegt, 1)
self.assertEqual(bereits_vorhanden, 1)
self.assertEqual(fehlgeschlagen, 0)
def test_skip_empty_geometry_counts_as_failed(self):
"""Neue ID mit leerer Geometrie → fehlgeschlagen=1, hinzugefügt=0."""
verf = _DummyVerfLayer() # leer
selected = [_DummyFeature("A001", geom_empty=True)]
source = _DummySourceLayer(selected=selected)
_, hinzugefuegt, _, fehlgeschlagen = ergaenze_verfahrensgebiet(verf, source)
self.assertEqual(hinzugefuegt, 0)
self.assertEqual(fehlgeschlagen, 1)
def test_all_already_present(self):
"""Alle ausgewählten IDs bereits vorhanden → bereits_vorhanden=n, hinzugefügt=0."""
verf = _DummyVerfLayer(_DummySingleFeature("A001,A002", feat_id=1))
selected = [_DummyFeature("A001"), _DummyFeature("A002")]
source = _DummySourceLayer(selected=selected)
_, hinzugefuegt, bereits_vorhanden, fehlgeschlagen = ergaenze_verfahrensgebiet(
verf, source
)
self.assertEqual(hinzugefuegt, 0)
self.assertEqual(bereits_vorhanden, 2)
self.assertEqual(fehlgeschlagen, 0)
# ---------------------------------------------------------------------------
# Tests: entferne_aus_verfahrensgebiet
# ---------------------------------------------------------------------------
class TestEntferneAusVerfahrensgebiet(unittest.TestCase):
def test_removes_present_ids(self):
"""Auswahl enthält 1 vorhandene ID → entfernt=1, nicht_enthalten=0."""
verf = _DummyVerfLayer(_DummySingleFeature("A001,A002", feat_id=1))
selected = [_DummyFeature("A001")]
source = _DummySourceLayer(selected=selected)
_, entfernt, nicht_enthalten, fehlgeschlagen = entferne_aus_verfahrensgebiet(
verf, source
)
self.assertEqual(entfernt, 1)
self.assertEqual(nicht_enthalten, 0)
self.assertEqual(fehlgeschlagen, 0)
def test_counts_ids_not_in_layer(self):
"""Auswahl enthält ID, die nicht im Verfahrensgebiet ist → nicht_enthalten=1."""
verf = _DummyVerfLayer(_DummySingleFeature("A001", feat_id=1))
selected = [
_DummyFeature("A001"), # vorhanden
_DummyFeature("A999"), # nicht enthalten
]
source = _DummySourceLayer(selected=selected)
_, entfernt, nicht_enthalten, fehlgeschlagen = entferne_aus_verfahrensgebiet(
verf, source
)
self.assertEqual(entfernt, 1)
self.assertEqual(nicht_enthalten, 1)
def test_none_present_in_layer(self):
"""Auswahl hat IDs, die alle nicht im Layer sind → entfernt=0, nicht_enthalten=n."""
verf = _DummyVerfLayer(_DummySingleFeature("X001", feat_id=1))
selected = [_DummyFeature("A999"), _DummyFeature("B888")]
source = _DummySourceLayer(selected=selected)
_, entfernt, nicht_enthalten, fehlgeschlagen = entferne_aus_verfahrensgebiet(
verf, source
)
self.assertEqual(entfernt, 0)
self.assertEqual(nicht_enthalten, 2)
def test_empty_selection_removes_nothing(self):
"""Leere Auswahl → nichts entfernt, nichts nicht_enthalten."""
verf = _DummyVerfLayer(_DummySingleFeature("A001", feat_id=1))
source = _DummySourceLayer(selected=[])
_, entfernt, nicht_enthalten, fehlgeschlagen = entferne_aus_verfahrensgebiet(
verf, source
)
self.assertEqual(entfernt, 0)
self.assertEqual(nicht_enthalten, 0)
def test_removes_last_id_deletes_feature(self):
"""Wird das letzte Kennzeichen entfernt, wird das Feature gelöscht."""
verf = _DummyVerfLayer(_DummySingleFeature("A001", feat_id=1))
selected = [_DummyFeature("A001")]
source = _DummySourceLayer(selected=selected)
_, entfernt, _, _ = entferne_aus_verfahrensgebiet(verf, source)
self.assertEqual(entfernt, 1)
# Feature soll gelöscht worden sein
self.assertIn(1, verf._deleted_ids)
# ---------------------------------------------------------------------------
# Tests: ersetze_verfahrensgebiet
# ---------------------------------------------------------------------------
class TestErsetze(unittest.TestCase):
def test_ersetze_delegates_to_erstelle(self):
"""ersetze_verfahrensgebiet gibt dasselbe Ergebnis wie erstelle zurück."""
feats = [_DummyFeature("A001")]
source = _DummySourceLayer(selected=feats)
import sn_verfahrensgebiet.functions.verfahrensgebiet_edit_service as module
orig_erstelle = module._erstelle_memory_layer
orig_union = module.QgsGeometry.unaryUnion
mem = _DummyVerfLayer()
module._erstelle_memory_layer = lambda _crs: mem
module.QgsGeometry.unaryUnion = staticmethod(lambda _g: _DummyGeom(empty=False))
try:
layer, ok, fail = ersetze_verfahrensgebiet(source)
self.assertIs(layer, mem)
self.assertEqual(ok, 1)
self.assertEqual(fail, 0)
finally:
module._erstelle_memory_layer = orig_erstelle
module.QgsGeometry.unaryUnion = staticmethod(orig_union)
if __name__ == "__main__":
unittest.main()
+439
View File
@@ -0,0 +1,439 @@
import sys
import types
import unittest
from unittest.mock import patch
if "qgis" not in sys.modules:
qgis_mod = types.ModuleType("qgis")
qgis_utils = types.ModuleType("qgis.utils")
qgis_utils.iface = types.SimpleNamespace(
mapCanvas=lambda: types.SimpleNamespace(setExtent=lambda _e: None, refresh=lambda: None)
)
qgis_utils.plugins = {}
qgis_mod.utils = qgis_utils
sys.modules["qgis"] = qgis_mod
sys.modules["qgis.utils"] = qgis_utils
from sn_verfahrensgebiet.ui.tabs.working_tab import WorkingTab, LoadStatus
class _DummyLayer:
def __init__(self, valid=True, layer_id="L1"):
self._valid = valid
self._id = layer_id
def isValid(self):
return self._valid
def id(self):
return self._id
def extent(self):
return object()
class _DummyProject:
def __init__(self):
self.added = []
def addMapLayer(self, layer):
self.added.append(layer)
class _DummyCanvas:
def __init__(self):
self.extent = None
def setExtent(self, extent):
self.extent = extent
def refresh(self):
return None
class _DummyButton:
def __init__(self):
self._enabled = True
self._text = "Aus ALKIS laden"
def setEnabled(self, enabled):
self._enabled = enabled
def isEnabled(self):
return self._enabled
def setText(self, text):
self._text = text
def text(self):
return self._text
class _DummyCombo:
def __init__(self):
self.layer = None
def setLayer(self, layer):
self.layer = layer
class TestWorkingTab(unittest.TestCase):
def _make_tab(self):
tab = WorkingTab.__new__(WorkingTab)
tab.btn_verf_alkis = _DummyButton()
tab.btn_flurst_alkis = _DummyButton()
tab.btn_verf_aus_auswahl = _DummyButton()
tab.btn_verf_aus_liste = _DummyButton()
tab.verf_layer_combo = _DummyCombo()
tab.haken_verf = object()
tab.haken_flurst = object()
tab.setze_haken = lambda *_args, **_kwargs: None
# Pruefmanager-Mock: verarbeite() wird im Test ggf. überschrieben
tab.pruefmanager = types.SimpleNamespace(
verarbeite=lambda ergebnis: ergebnis
)
return tab
def test_set_busy_updates_button_state(self):
tab = self._make_tab()
tab._set_busy(tab.btn_verf_alkis, True)
self.assertFalse(tab.btn_verf_alkis.isEnabled())
self.assertEqual(tab.btn_verf_alkis.text(), "Lade ...")
tab._set_busy(tab.btn_verf_alkis, False)
self.assertTrue(tab.btn_verf_alkis.isEnabled())
self.assertEqual(tab.btn_verf_alkis.text(), "Aus ALKIS laden")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.set_verfahrensgebiet_layer_id")
def test_on_verf_layer_changed_valid_sets_variable(self, mock_set_layer_id):
tab = self._make_tab()
layer = _DummyLayer(valid=True, layer_id="layer_123")
tab._on_verf_layer_changed(layer)
mock_set_layer_id.assert_called_once_with("layer_123")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.set_verfahrensgebiet_layer_id")
def test_on_verf_layer_changed_none_clears_variable(self, mock_set_layer_id):
tab = self._make_tab()
tab._on_verf_layer_changed(None)
mock_set_layer_id.assert_called_once_with("")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.success")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.apply_style")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.QgsProject.instance")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.verfahrensgebiet_alkis")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.iface")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.set_verfahrensgebiet_layer_id")
def test_handle_verf_alkis_success_path(
self,
mock_set_layer_id,
mock_iface,
mock_alkis,
mock_project_instance,
mock_apply_style,
mock_success,
):
tab = self._make_tab()
layer = _DummyLayer(valid=True, layer_id="layer_ok")
mock_alkis.return_value = (layer, LoadStatus.FIRST)
mock_project_instance.return_value = _DummyProject()
mock_iface.mapCanvas.return_value = _DummyCanvas()
tab.handle_verf_alkis()
mock_apply_style.assert_called_once()
mock_success.assert_called_once()
mock_set_layer_id.assert_called_with("layer_ok")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.set_verfahrensgebiet_layer_id")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.verfahrensgebiet_alkis", return_value=(None, LoadStatus.NONE))
def test_handle_verf_alkis_failure_clears_layer_var(self, _mock_alkis, mock_set_layer_id):
tab = self._make_tab()
tab.handle_verf_alkis()
mock_set_layer_id.assert_called_once_with("")
# ------------------------------------------------------------------
# handle_flurst_alkis
# ------------------------------------------------------------------
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.success")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.apply_style")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.QgsProject.instance")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.iface")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.flurstueck_alkis")
def test_handle_flurst_alkis_success_sets_haken_and_style(
self, mock_alkis, mock_iface, mock_project, mock_apply_style, mock_success
):
tab = self._make_tab()
haken_calls = []
tab.setze_haken = lambda lbl, v: haken_calls.append((lbl, v))
layer = _DummyLayer(valid=True)
mock_alkis.return_value = (layer, LoadStatus.FIRST)
mock_project.return_value = _DummyProject()
mock_iface.mapCanvas.return_value = _DummyCanvas()
tab.handle_flurst_alkis()
mock_apply_style.assert_called_once_with(layer, "GIS_Flst_Beschriftung_ALKIS_NAS.qml")
mock_success.assert_called_once()
self.assertEqual(len(haken_calls), 1)
self.assertTrue(haken_calls[0][1]) # haken gesetzt
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.info")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.flurstueck_alkis")
def test_handle_flurst_alkis_existing_layer_keep_path(self, mock_alkis, mock_info):
tab = self._make_tab()
haken_calls = []
tab.setze_haken = lambda lbl, v: haken_calls.append((lbl, v))
layer = _DummyLayer(valid=True)
mock_alkis.return_value = (layer, LoadStatus.KEEP)
tab.handle_flurst_alkis()
mock_info.assert_called_once()
self.assertEqual(len(haken_calls), 1)
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.flurstueck_alkis")
def test_handle_flurst_alkis_invalid_input_returns_early(self, mock_alkis):
"""Status NONE (z. B. Nutzerabbruch) → kein Layer, kein Haken."""
tab = self._make_tab()
haken_calls = []
tab.setze_haken = lambda lbl, v: haken_calls.append((lbl, v))
mock_alkis.return_value = (None, LoadStatus.NONE)
tab.handle_flurst_alkis()
self.assertEqual(len(haken_calls), 0)
# Button muss am Ende wieder freigegeben sein (finally)
self.assertTrue(tab.btn_flurst_alkis.isEnabled())
# ------------------------------------------------------------------
# handle_verf_aus_auswahl – Vorbedingungsprüfungen
# ------------------------------------------------------------------
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.warning")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.iface")
def test_handle_verfahrensgebiet_from_selection_requires_active_layer(
self, mock_iface, mock_warning
):
"""Kein aktiver Layer → warning, kein Layer wird erstellt."""
tab = self._make_tab()
mock_iface.activeLayer.return_value = None
tab.handle_verf_aus_auswahl()
mock_warning.assert_called_once()
self.assertIn("aktiver Layer", mock_warning.call_args[0][1])
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.warning")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.pruefe_flurstueck_layer")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.iface")
def test_handle_verfahrensgebiet_from_selection_requires_required_fields(
self, mock_iface, mock_pruefe, mock_warning
):
"""Layer ohne Pflichtfelder → warning, kein Layer wird erstellt."""
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis as PruefErg
tab = self._make_tab()
active = _DummyLayer(valid=True)
mock_iface.activeLayer.return_value = active
mock_pruefe.return_value = PruefErg(
ok=False, aktion="felder_fehlen", meldung="Feld fehlt"
)
tab.handle_verf_aus_auswahl()
mock_warning.assert_called_once()
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.warning")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.pruefe_auswahl")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.pruefe_flurstueck_layer")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.iface")
def test_handle_verfahrensgebiet_from_selection_requires_selection(
self, mock_iface, mock_pruefe_layer, mock_pruefe_auswahl, mock_warning
):
"""Keine Flurstück-Auswahl → warning, kein Layer erstellt."""
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis as PruefErg
tab = self._make_tab()
active = _DummyLayer(valid=True)
mock_iface.activeLayer.return_value = active
mock_pruefe_layer.return_value = PruefErg(ok=True, aktion="ok")
mock_pruefe_auswahl.return_value = PruefErg(
ok=False, aktion="layer_leer", meldung="Keine Auswahl"
)
tab.handle_verf_aus_auswahl()
mock_warning.assert_called_once()
# ------------------------------------------------------------------
# handle_verf_aus_auswahl – Erstlauf (kein bestehender Layer)
# ------------------------------------------------------------------
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.QMessageBox")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.apply_style")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.QgsProject.instance")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.erstelle_verfahrensgebiet_aus_auswahl")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.pruefe_auswahl")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.pruefe_flurstueck_layer")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.iface")
def test_handle_verfahrensgebiet_from_selection_new_layer_success(
self, mock_iface, mock_pruefe_layer, mock_pruefe_auswahl,
mock_erstelle, mock_project_inst, mock_apply_style, mock_msgbox
):
"""Erstlauf ohne bestehenden Layer → Layer wird erstellt und hinzugefügt."""
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis as PruefErg
tab = self._make_tab()
haken_calls = []
tab.setze_haken = lambda lbl, v: haken_calls.append(v)
active = _DummyLayer(valid=True)
mock_iface.activeLayer.return_value = active
mock_iface.mapCanvas.return_value = _DummyCanvas()
mock_pruefe_layer.return_value = PruefErg(ok=True, aktion="ok")
mock_pruefe_auswahl.return_value = PruefErg(ok=True, aktion="ok", meldung="3 Flurstücke")
new_layer = _DummyLayer(valid=True, layer_id="new_verf")
mock_erstelle.return_value = (new_layer, 3, 0)
project = _DummyProject()
project.mapLayersByName = lambda _name: [] # kein bestehender Layer
mock_project_inst.return_value = project
tab.handle_verf_aus_auswahl()
self.assertIn(new_layer, project.added)
mock_apply_style.assert_called_once_with(new_layer, "verfahrensgebiet.qml")
self.assertTrue(haken_calls[0])
# ------------------------------------------------------------------
# handle_verf_aus_auswahl – Ergänzen / Entfernen / Ersetzen
# ------------------------------------------------------------------
def _make_entscheidungs_ergebnis(self, aktion: str):
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis as PruefErg
return PruefErg(ok=True, aktion=aktion)
def _setup_existierenden_layer(self, tab, mock_iface, mock_pruefe_layer,
mock_pruefe_auswahl, mock_project_inst,
mock_canvas, entscheidung: str):
"""Gemeinsames Setup für Delta-Operationen mit bestehendem Layer."""
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis as PruefErg
active = _DummyLayer(valid=True)
mock_iface.activeLayer.return_value = active
mock_iface.mapCanvas.return_value = mock_canvas
mock_pruefe_layer.return_value = PruefErg(ok=True, aktion="ok")
mock_pruefe_auswahl.return_value = PruefErg(ok=True, aktion="ok", meldung="2 Flurstücke")
verf_layer = _DummyLayer(valid=True, layer_id="verf_existing")
project = _DummyProject()
project.added = []
project.mapLayersByName = lambda _: [verf_layer]
project.removeMapLayer = lambda lid: None
mock_project_inst.return_value = project
# Pruefmanager gibt Entscheidung zurück
tab.pruefmanager.verarbeite = lambda _e: self._make_entscheidungs_ergebnis(entscheidung)
return active, verf_layer, project
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.QMessageBox")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.apply_style")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.ergaenze_verfahrensgebiet")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.QgsProject.instance")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.pruefe_auswahl")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.pruefe_flurstueck_layer")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.iface")
def test_handle_verfahrensgebiet_from_selection_existing_layer_ergaenzen_dispatch(
self, mock_iface, mock_pruefe_layer, mock_pruefe_auswahl,
mock_project_inst, mock_ergaenze, mock_apply_style, mock_msgbox
):
"""Entscheidung 'ergaenzen' → ergaenze_verfahrensgebiet wird aufgerufen."""
tab = self._make_tab()
canvas = _DummyCanvas()
active, verf_layer, _ = self._setup_existierenden_layer(
tab, mock_iface, mock_pruefe_layer, mock_pruefe_auswahl,
mock_project_inst, canvas, "verfahrensgebiet_ergaenzen"
)
mock_ergaenze.return_value = (verf_layer, 2, 0, 0)
tab.handle_verf_aus_auswahl()
mock_ergaenze.assert_called_once_with(verf_layer, active)
mock_apply_style.assert_called_once_with(verf_layer, "verfahrensgebiet.qml")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.QMessageBox")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.apply_style")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.entferne_aus_verfahrensgebiet")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.QgsProject.instance")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.pruefe_auswahl")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.pruefe_flurstueck_layer")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.iface")
def test_handle_verfahrensgebiet_from_selection_existing_layer_entfernen_dispatch(
self, mock_iface, mock_pruefe_layer, mock_pruefe_auswahl,
mock_project_inst, mock_entferne, mock_apply_style, mock_msgbox
):
"""Entscheidung 'entfernen' → entferne_aus_verfahrensgebiet wird aufgerufen."""
tab = self._make_tab()
canvas = _DummyCanvas()
active, verf_layer, _ = self._setup_existierenden_layer(
tab, mock_iface, mock_pruefe_layer, mock_pruefe_auswahl,
mock_project_inst, canvas, "verfahrensgebiet_entfernen"
)
mock_entferne.return_value = (verf_layer, 1, 1, 0)
tab.handle_verf_aus_auswahl()
mock_entferne.assert_called_once_with(verf_layer, active)
mock_apply_style.assert_called_once_with(verf_layer, "verfahrensgebiet.qml")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.QMessageBox")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.apply_style")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.ersetze_verfahrensgebiet")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.QgsProject.instance")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.pruefe_auswahl")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.pruefe_flurstueck_layer")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.iface")
def test_handle_verfahrensgebiet_from_selection_existing_layer_ersetzen_dispatch(
self, mock_iface, mock_pruefe_layer, mock_pruefe_auswahl,
mock_project_inst, mock_ersetze, mock_apply_style, mock_msgbox
):
"""Entscheidung 'ersetzen' → alter Layer entfernt, neuer hinzugefügt."""
tab = self._make_tab()
canvas = _DummyCanvas()
active, verf_layer, project = self._setup_existierenden_layer(
tab, mock_iface, mock_pruefe_layer, mock_pruefe_auswahl,
mock_project_inst, canvas, "verfahrensgebiet_ersetzen"
)
removed = []
project.removeMapLayer = lambda lid: removed.append(lid)
new_layer = _DummyLayer(valid=True, layer_id="new_verf_replaced")
mock_ersetze.return_value = (new_layer, 3, 0)
tab.handle_verf_aus_auswahl()
mock_ersetze.assert_called_once_with(active)
self.assertIn(verf_layer.id(), removed)
self.assertIn(new_layer, project.added)
mock_apply_style.assert_called_once_with(new_layer, "verfahrensgebiet.qml")
# ------------------------------------------------------------------
# handle_verf_aus_liste
# ------------------------------------------------------------------
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.show_info_dialog")
def test_aus_liste_button_shows_not_implemented_hint(self, mock_info):
tab = self._make_tab()
tab.handle_verf_aus_liste()
mock_info.assert_called_once()
if __name__ == "__main__":
unittest.main()
+54 -3
View File
@@ -1,8 +1,59 @@
"""sn_verfahrensgebiet/ui/dockwidget.py – Haupt-DockWidget für sn_verfahrensgebiet.
Verwaltet die Tabs für Verfahrensgebiet-Bearbeitung, Druck und Einstellungen.
"""
from sn_basis.ui.tabs.settings_tab import SettingsTab
from sn_verfahrensgebiet.ui.tabs.tab_a import TabA
from sn_verfahrensgebiet.ui.tabs.tab_b import TabB
from sn_basis.ui.tabs.print_tab import PrintTab
from sn_verfahrensgebiet.ui.tabs.working_tab import WorkingTab
from sn_basis.ui.base_dockwidget import BaseDockWidget
from sn_basis.functions.qt_wrapper import QTabWidget
from sn_basis.functions.message_wrapper import error
class DockWidget(BaseDockWidget):
tabs = [TabA, SettingsTab]
"""Haupt-DockWidget für sn_verfahrensgebiet mit Tab-Verwaltung.
Attributes:
tabs: Liste von Tab-Klassen (WorkingTab, PrintTab, SettingsTab)
pruefmanager: Prüfmanager-Instanz für Validierung
data_grabber: DataGrabber-Instanz für Fachdatenabruf
"""
tabs = [WorkingTab, PrintTab, SettingsTab]
def __init__(self, parent=None, subtitle="", pruefmanager=None, data_grabber=None):
"""Initialisiert das DockWidget mit Services und Tabs.
Args:
parent: Übergeordnetes Widget
subtitle: Untertitel des DockWidgets
pruefmanager: Prüfmanager-Instanz
data_grabber: DataGrabber-Instanz
"""
super().__init__(parent, subtitle)
self.pruefmanager = pruefmanager
self.data_grabber = data_grabber
self._init_tabs_with_services()
def _init_tabs_with_services(self):
"""Tabs neu aufbauen und Services (z.B. fuer PrintTab) injizieren."""
try:
self.setWidget(None)
tab_widget = QTabWidget()
for tab_class in self.tabs:
tab_instance = tab_class(self)
tab_title = getattr(tab_class, "tab_title", tab_class.__name__)
tab_widget.addTab(tab_instance, tab_title)
if hasattr(tab_instance, "set_services"):
tab_instance.set_services(
pruefmanager=self.pruefmanager,
data_grabber=self.data_grabber,
)
self.setWidget(tab_widget)
except Exception as e:
error("Services-Tabs konnten nicht initialisiert werden", str(e))
-140
View File
@@ -1,140 +0,0 @@
<!DOCTYPE qgis PUBLIC 'http://mrcc.com/qgis.dtd' 'SYSTEM'>
<qgis version="3.40.7-Bratislava" styleCategories="Symbology">
<renderer-v2 referencescale="-1" forceraster="0" enableorderby="0" type="singleSymbol" symbollevels="0">
<symbols>
<symbol is_animated="0" frame_rate="10" clip_to_extent="1" type="fill" alpha="1" force_rhr="0" name="0">
<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 locked="0" id="{feca00b2-500a-4c9a-b285-67ba2d99d8f6}" 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="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>
<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>
</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 is_animated="0" frame_rate="10" clip_to_extent="1" type="fill" alpha="1" force_rhr="0" name="">
<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 locked="0" id="{f18003f5-220a-487f-8a6d-b24facc4c1a5}" enabled="1" class="SimpleFill" 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>
-98
View File
@@ -1,98 +0,0 @@
from qgis.PyQt.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QMessageBox
from qgis.PyQt.QtGui import QIcon, QPixmap
from qgis.PyQt.QtCore import Qt, QTimer
from qgis.core import Qgis, QgsProject, QgsMessageLog
from qgis.utils import iface
import os
from sn_verfahrensgebiet.logic.alkis_verfahrensgebiet import alkis_verfahrensgebiet
from sn_basis.logic.utils import zoom_to_layer
class TabA(QWidget):
tab_title = "Bearbeitung"
def __init__(self, parent=None):
super().__init__(parent)
layout = QVBoxLayout()
layout.setAlignment(Qt.AlignmentFlag.AlignTop)
# Abschnitt 1: Verfahrensgebiet
row_verf_header = QHBoxLayout()
lbl_verf = QLabel("<b>Verfahrensgebiet</b>")
self.haken1 = self._haken_label()
row_verf_header.addWidget(lbl_verf)
row_verf_header.addWidget(self.haken1)
row_verf_header.addStretch()
layout.addLayout(row_verf_header)
row_verf_buttons = QHBoxLayout()
self.btn_verf_alkis = QPushButton("Aus ALKIS laden")
self.btn_verf_shape = QPushButton("Aus Shape laden")
row_verf_buttons.addWidget(self.btn_verf_alkis)
row_verf_buttons.addWidget(self.btn_verf_shape)
layout.addLayout(row_verf_buttons)
# Abschnitt 2: Flurstücke
row_flurst_header = QHBoxLayout()
lbl_flurst = QLabel("<b>Flurstücke</b>")
self.haken2 = self._haken_label()
row_flurst_header.addWidget(lbl_flurst)
row_flurst_header.addWidget(self.haken2)
row_flurst_header.addStretch()
layout.addLayout(row_flurst_header)
row_flurst_buttons = QHBoxLayout()
self.btn_flurst_alkis = QPushButton("Aus ALKIS laden")
self.btn_flurst_shape = QPushButton("Aus Shape laden")
row_flurst_buttons.addWidget(self.btn_flurst_alkis)
row_flurst_buttons.addWidget(self.btn_flurst_shape)
layout.addLayout(row_flurst_buttons)
layout.addStretch()
self.setLayout(layout)
# Beispiel: Haken anzeigen nach Klick
# self.button1.clicked.connect(lambda: alkis_verfahrensgebiet(self))
self.btn_verf_alkis.clicked.connect(lambda: self.handle_button1())
#self.button2.clicked.connect(lambda: self.setze_haken(self.haken2, True))
def handle_button1(self):
layer, neu_geladen = alkis_verfahrensgebiet(self)
if not layer or not layer.isValid():
return
if neu_geladen:
QgsProject.instance().addMapLayer(layer)
iface.mapCanvas().setExtent(layer.extent())
iface.mapCanvas().refresh()
# Beispiel: Haken setzen oder Meldung ausgeben
self.setze_haken(self.haken1, True)
# QMessageBox.information(self, "Info", "Layer 'Verfahrensgebiet' wurde neu geladen.")
else:
# Kein Zoom, kein Neu-Laden
QgsMessageLog.logMessage("Vorhandener Layer behalten.", "sn_verfahrensgebiet", level=Qgis.Info)
def _haken_label(self):
label = QLabel()
label.setFixedSize(20, 20)
# label.setAlignment(Qt.AlignCenter)
label.setAlignment(Qt.AlignmentFlag.AlignCenter)
label.setScaledContents(False)
label.setText("")
return label
def setze_haken(self, label: QLabel, aktiv: bool):
"""Zeigt oder entfernt den grünen Haken."""
if aktiv:
# grünes Unicode-Häkchen
label.setPixmap(QPixmap())
label.setText("✓")
label.setStyleSheet("color: #2ea043; font-weight: bold;")
else:
label.setPixmap(QPixmap())
label.setText("")
label.setStyleSheet("")
-11
View File
@@ -1,11 +0,0 @@
from qgis.PyQt.QtWidgets import QWidget, QVBoxLayout, QLabel, QTextEdit
class TabB(QWidget):
tab_title = "Tab B"
def __init__(self, parent=None):
super().__init__(parent)
layout = QVBoxLayout()
layout.addWidget(QLabel("Plugin1 – Tab B"))
layout.addWidget(QTextEdit("Mehrzeiliger Text für Plugin1"))
self.setLayout(layout)
+370
View File
@@ -0,0 +1,370 @@
"""sn_verfahrensgebiet/ui/tabs/working_tab.py – Bearbeitungs-Tab.
Verwaltet die Verfahrensgebiet-Abgrenzung und ALKIS-Integration.
"""
from sn_basis.functions.qt_wrapper import QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QMessageBox
from sn_basis.functions.qt_wrapper import Qt
from sn_basis.functions import Qgis, QgsProject #QgsMessageLog
from sn_basis.functions.qgisui_wrapper import QgsMapLayerComboBox
from sn_basis.functions.qgiscore_wrapper import QgsMapLayerProxyModel
from sn_basis.functions.verfahrensgebiet_manager import get_verfahrensgebiet_layer_id, set_verfahrensgebiet_layer_id
from sn_basis.functions.dialog_wrapper import show_info_dialog
from sn_basis.modules.Pruefmanager import Pruefmanager
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
from qgis.utils import iface
from sn_verfahrensgebiet.functions.verfahrensgebiet_alkis import verfahrensgebiet_alkis, LoadStatus
from sn_verfahrensgebiet.functions.flurstueck_alkis import flurstueck_alkis
from sn_verfahrensgebiet.functions.verfahrensgebiet_edit_service import (
pruefe_flurstueck_layer,
pruefe_auswahl,
erstelle_verfahrensgebiet_aus_auswahl,
ergaenze_verfahrensgebiet,
entferne_aus_verfahrensgebiet,
ersetze_verfahrensgebiet,
)
from sn_basis.functions.message_wrapper import success, info, warning, error
from sn_basis.functions.ly_style_wrapper import apply_style
class WorkingTab(QWidget):
"""Tab für die Verfahrensgebiet-Bearbeitung und Verwaltung.
Attributes:
tab_title: Anzeigetitel des Tabs ("Bearbeitung")
"""
tab_title = "Bearbeitung"
def __init__(self, parent=None):
super().__init__(parent)
layout = QVBoxLayout()
layout.setAlignment(Qt.AlignmentFlag.AlignTop)
# Abschnitt 1: Verfahrensgebiet laden
row_verf_header = QHBoxLayout()
lbl_verf = QLabel("<b>Verfahrensgebiet</b>")
self.haken_verf = self._haken_label()
row_verf_header.addWidget(lbl_verf)
row_verf_header.addWidget(self.haken_verf)
row_verf_header.addStretch()
layout.addLayout(row_verf_header)
row_verf_buttons = QHBoxLayout()
self.btn_verf_alkis = QPushButton("Aus ALKIS laden")
self.btn_verf_shape = QPushButton("Aus Shape laden")
row_verf_buttons.addWidget(self.btn_verf_alkis)
row_verf_buttons.addWidget(self.btn_verf_shape)
layout.addLayout(row_verf_buttons)
# Abschnitt 1b: Verfahrensgebiet-Layer auswählen
row_layer_label = QHBoxLayout()
lbl_layer = QLabel("Oder: Verfahrensgebiet-Layer auswählen")
lbl_layer.setStyleSheet("font-style: italic; color: gray; margin-top: 8px;")
row_layer_label.addWidget(lbl_layer)
row_layer_label.addStretch()
layout.addLayout(row_layer_label)
self.verf_layer_combo = QgsMapLayerComboBox(self)
self.verf_layer_combo.setFilters(QgsMapLayerProxyModel.VectorLayer)
self.verf_layer_combo.setToolTip("Wähle den Verfahrensgebiet-Layer, oder lasse es leer für Automatik")
self.verf_layer_combo.layerChanged.connect(self._on_verf_layer_changed)
layout.addWidget(self.verf_layer_combo)
# Abschnitt 2: Flurstücke
row_flurst_header = QHBoxLayout()
lbl_flurst = QLabel("<b>Flurstücke</b>")
self.haken_flurst = self._haken_label()
row_flurst_header.addWidget(lbl_flurst)
row_flurst_header.addWidget(self.haken_flurst)
row_flurst_header.addStretch()
layout.addLayout(row_flurst_header)
row_flurst_buttons = QHBoxLayout()
self.btn_flurst_alkis = QPushButton("Aus ALKIS laden")
self.btn_flurst_shape = QPushButton("Aus Shape laden")
row_flurst_buttons.addWidget(self.btn_flurst_alkis)
row_flurst_buttons.addWidget(self.btn_flurst_shape)
layout.addLayout(row_flurst_buttons)
# Abschnitt 3: Verfahrensgebiet erstellen
row_verf_erstellen_header = QHBoxLayout()
lbl_verf_erstellen = QLabel("<b>Verfahrensgebiet erstellen</b>")
row_verf_erstellen_header.addWidget(lbl_verf_erstellen)
row_verf_erstellen_header.addStretch()
layout.addLayout(row_verf_erstellen_header)
row_verf_erstellen_buttons = QHBoxLayout()
self.btn_verf_aus_auswahl = QPushButton("aus Auswahl")
self.btn_verf_aus_liste = QPushButton("Aus Liste")
row_verf_erstellen_buttons.addWidget(self.btn_verf_aus_auswahl)
row_verf_erstellen_buttons.addWidget(self.btn_verf_aus_liste)
layout.addLayout(row_verf_erstellen_buttons)
layout.addStretch()
self.setLayout(layout)
# Pruefmanager für interaktive Entscheidungen (Ergänzen/Entfernen/Ersetzen)
self.pruefmanager = Pruefmanager(ui_modus="qgis", parent=self)
# Signale verbinden
self.btn_verf_alkis.clicked.connect(self.handle_verf_alkis)
self.btn_flurst_alkis.clicked.connect(self.handle_flurst_alkis)
self.btn_verf_aus_auswahl.clicked.connect(self.handle_verf_aus_auswahl)
self.btn_verf_aus_liste.clicked.connect(self.handle_verf_aus_liste)
# self.btn_verf_shape.clicked.connect(self.handle_verf_shape)
# self.btn_flurst_shape.clicked.connect(self.handle_flurst_shape)
# Beim Init: aktuelle Verfahrensgebiet-Layer-ID laden und im Dropdown anzeigen
self._restore_verf_layer_selection()
def handle_verf_alkis(self):
self._set_busy(self.btn_verf_alkis, True, "Aus ALKIS laden")
try:
layer, status = verfahrensgebiet_alkis(self)
if not layer or not layer.isValid():
set_verfahrensgebiet_layer_id("")
return
if status in (LoadStatus.FIRST, LoadStatus.RELOAD):
QgsProject.instance().addMapLayer(layer)
iface.mapCanvas().setExtent(layer.extent())
apply_style(layer, "verfahrensgebiet.qml")
iface.mapCanvas().refresh()
self.setze_haken(self.haken_verf, True)
self.verf_layer_combo.setLayer(layer)
self._on_verf_layer_changed(layer)
msg = "erstmalig geladen" if status == LoadStatus.FIRST else "neu geladen"
success("Verfahrensgebiet", f"Layer wurde {msg}.", duration=5)
elif status == LoadStatus.KEEP:
self.verf_layer_combo.setLayer(layer)
self._on_verf_layer_changed(layer)
info("Verfahrensgebiet", "Vorhandener Layer wurde behalten.", duration=4)
elif status == LoadStatus.NONE:
self._on_verf_layer_changed(None)
warning("Verfahrensgebiet", "Layer konnte nicht geladen werden.", duration=None)
finally:
self._set_busy(self.btn_verf_alkis, False, "Aus ALKIS laden")
def _set_busy(self, button: QPushButton, busy: bool, original_text: str = "Aus ALKIS laden") -> None:
"""Setzt den Busy-State eines Buttons.
Parameters
----------
button :
Der zu sperrende/entsperrende Button.
busy : bool
True zum Sperren (Text → "Lade ..."), False zum Freigeben.
original_text : str
Text, der nach dem Freigeben wiederhergestellt wird.
Jeder Button gibt seinen eigenen Originaltext explizit an.
"""
button.setEnabled(not busy)
button.setText("Lade ..." if busy else original_text)
def _haken_label(self) -> QLabel:
label = QLabel()
label.setFixedSize(20, 20)
label.setAlignment(Qt.AlignmentFlag.AlignCenter)
label.setText("") # Start ohne Haken
return label
def setze_haken(self, label: QLabel, aktiv: bool):
"""Zeigt oder entfernt den grünen Haken (Unicode), ohne Theme-Icons."""
if aktiv:
label.setText("✓")
label.setStyleSheet("color: #2ea043; font-weight: bold;")
else:
label.setText("")
label.setStyleSheet("")
def _on_verf_layer_changed(self, layer) -> None:
"""Callback: Nutzer hat einen anderen Layer im Dropdown gewählt."""
if layer and layer.isValid():
# Speichere die Layer-ID in der zentralen Variable
set_verfahrensgebiet_layer_id(layer.id())
else:
# Layer is None oder invalid; leere die Variable
set_verfahrensgebiet_layer_id("")
def _restore_verf_layer_selection(self) -> None:
"""Beim Tab-Init: Lade die zuletzt gespeicherte Verfahrensgebiet-Layer-ID."""
layer_id = get_verfahrensgebiet_layer_id()
if layer_id:
try:
project = QgsProject.instance()
layer = project.mapLayer(layer_id)
if layer and layer.isValid():
self.verf_layer_combo.setLayer(layer)
except Exception:
pass
# ------------------------------------------------------------------
# Flurstücke-Handler
# ------------------------------------------------------------------
def handle_flurst_alkis(self) -> None:
"""Lädt Flurstücke aus ALKIS-WFS anhand von Gemarkungsschlüsseln."""
self._set_busy(self.btn_flurst_alkis, True, "Aus ALKIS laden")
try:
layer, status = flurstueck_alkis(self)
if not layer or not layer.isValid():
return
if status in (LoadStatus.FIRST, LoadStatus.RELOAD):
QgsProject.instance().addMapLayer(layer)
# iface.mapCanvas().setExtent(layer.extent()) # Kartenausschnitt bleibt unverändert
apply_style(layer, "GIS_Flst_Beschriftung_ALKIS_NAS.qml")
iface.mapCanvas().refresh()
self.setze_haken(self.haken_flurst, True)
msg = "erstmalig geladen" if status == LoadStatus.FIRST else "neu geladen"
success("Flurstücke", f"Layer wurde {msg}.", duration=5)
elif status == LoadStatus.KEEP:
self.setze_haken(self.haken_flurst, True)
info("Flurstücke", "Vorhandener Layer wurde behalten.", duration=4)
finally:
self._set_busy(self.btn_flurst_alkis, False, "Aus ALKIS laden")
# ------------------------------------------------------------------
# Verfahrensgebiet-erstellen-Handler
# ------------------------------------------------------------------
def handle_verf_aus_auswahl(self) -> None:
"""Erstellt oder aktualisiert den Verfahrensgebiet-Layer aus der Flurstück-Auswahl."""
self._set_busy(self.btn_verf_aus_auswahl, True, "aus Auswahl")
try:
# 1. Aktiven Layer ermitteln
active_layer = iface.activeLayer()
if not active_layer:
warning(
"Verfahrensgebiet erstellen",
"Kein aktiver Layer. Bitte den Flurstück-Layer aktivieren.",
duration=None,
)
return
# 2. Vorbedingungen via Layerpruefer
ergebnis = pruefe_flurstueck_layer(active_layer)
if not ergebnis.ok:
warning("Verfahrensgebiet erstellen", ergebnis.meldung, duration=None)
return
# 3. Auswahl prüfen
ergebnis_auswahl = pruefe_auswahl(active_layer)
if not ergebnis_auswahl.ok:
warning("Verfahrensgebiet erstellen", ergebnis_auswahl.meldung, duration=None)
return
# 4. Bestehenden Verfahrensgebiet-Layer prüfen
existing_layers = QgsProject.instance().mapLayersByName("Verfahrensgebiet")
if not existing_layers:
# Erstlauf: neuen Memory-Layer erzeugen
layer, erfolgreich, fehlgeschlagen = erstelle_verfahrensgebiet_aus_auswahl(
active_layer
)
if not layer or not layer.isValid():
error("Verfahrensgebiet erstellen", "Layer konnte nicht erstellt werden.")
return
QgsProject.instance().addMapLayer(layer)
apply_style(layer, "verfahrensgebiet.qml")
iface.mapCanvas().refresh()
self.verf_layer_combo.setLayer(layer)
self._on_verf_layer_changed(layer)
self.setze_haken(self.haken_verf, True)
QMessageBox.information(
self,
"Verfahrensgebiet erstellt",
f"Verfahrensgebiet aus Auswahl erstellt.\n"
f"Erfolgreich: {erfolgreich}\n"
f"Fehlgeschlagen: {fehlgeschlagen}",
)
return
# 5. Bestehender Layer: Nutzerentscheidung via Pruefmanager
entscheidung_ergebnis = self.pruefmanager.verarbeite(
pruef_ergebnis(
ok=False,
aktion="verfahrensgebiet_layer_existiert",
meldung=(
"Ein Verfahrensgebiet-Layer existiert bereits.\n"
"Was soll mit der Auswahl geschehen?"
),
kontext=existing_layers[0],
)
)
aktion = entscheidung_ergebnis.aktion
verf_layer = existing_layers[0]
if aktion == "verfahrensgebiet_abbrechen" or not entscheidung_ergebnis.ok:
return
elif aktion == "verfahrensgebiet_ergaenzen":
_, hinzugefuegt, bereits_vorhanden, fehlgeschlagen = ergaenze_verfahrensgebiet(
verf_layer, active_layer
)
apply_style(verf_layer, "verfahrensgebiet.qml")
iface.mapCanvas().refresh()
self.setze_haken(self.haken_verf, True)
QMessageBox.information(
self,
"Flurstücke ergänzt",
f"Hinzugefügt: {hinzugefuegt}\n"
f"Bereits vorhanden (übersprungen): {bereits_vorhanden}\n"
f"Fehlgeschlagen: {fehlgeschlagen}",
)
elif aktion == "verfahrensgebiet_entfernen":
_, entfernt, nicht_enthalten, fehlgeschlagen = entferne_aus_verfahrensgebiet(
verf_layer, active_layer
)
apply_style(verf_layer, "verfahrensgebiet.qml")
iface.mapCanvas().refresh()
QMessageBox.information(
self,
"Flurstücke entfernt",
f"Entfernt: {entfernt}\n"
f"Nicht im Verfahrensgebiet (übersprungen): {nicht_enthalten}\n"
f"Fehlgeschlagen: {fehlgeschlagen}",
)
elif aktion == "verfahrensgebiet_ersetzen":
for lyr in existing_layers:
QgsProject.instance().removeMapLayer(lyr.id())
layer, erfolgreich, fehlgeschlagen = ersetze_verfahrensgebiet(active_layer)
if not layer or not layer.isValid():
error("Verfahrensgebiet erstellen", "Layer konnte nicht erstellt werden.")
return
QgsProject.instance().addMapLayer(layer)
apply_style(layer, "verfahrensgebiet.qml")
iface.mapCanvas().refresh()
self.verf_layer_combo.setLayer(layer)
self._on_verf_layer_changed(layer)
self.setze_haken(self.haken_verf, True)
QMessageBox.information(
self,
"Verfahrensgebiet ersetzt",
f"Verfahrensgebiet neu erstellt.\n"
f"Erfolgreich: {erfolgreich}\n"
f"Fehlgeschlagen: {fehlgeschlagen}",
)
finally:
self._set_busy(self.btn_verf_aus_auswahl, False, "aus Auswahl")
def handle_verf_aus_liste(self) -> None:
"""Platzhalter – Verfahrensgebiet aus Liste erstellen (noch nicht implementiert)."""
show_info_dialog(
"Noch nicht implementiert",
"Die Funktion 'Aus Liste' steht in einer zukünftigen Version zur Verfügung.",
parent=self,
)