Compare commits

..
Author SHA1 Message Date
Daniel 6f24f93f01 Merge pull request 'grenzpunktemit 200m puffer um verfahrensgebiet laden, Knickpunktprüfung' (#15) from feature/grenzpunkte_laden into unstable
Reviewed-on: AG_QGIS/Plugin_SN_Verfahrensgebiet#15
2026-05-06 10:38:29 +02:00
Daniel a829716810 Statisitik Umringsgrenzpunkte ergänzt 2026-05-05 21:17:19 +02:00
Daniel 5e3261a8de fortschrittsanzeige grenzpunkte kommt zeitiger 2026-05-05 19:57:35 +02:00
DanielandCopilot e344a5ad4a Verfahrensgebietsprüfung auf Knickpunkte umgestellt
Co-authored-by: Copilot <copilot@github.com>
2026-05-05 19:46:08 +02:00
Daniel 8730e1bf96 attributwertverschiebung gefixt 2026-05-01 23:53:20 +02:00
Daniel 00ee470e08 grenzpunktemit 200m puffer um verfahrensgebiet laden 2026-05-01 23:32:28 +02:00
release-bot 821ea4ec2b Release 26.4.6-unstable 2026-04-24 04:52:38 +00:00
Daniel d1a9748461 Merge pull request 'verfahrensgebiet aus Flst' (#12) from verfahrensgebiet_aus_flst into unstable
Reviewed-on: AG_QGIS/Plugin_SN_Verfahrensgebiet#12
2026-04-23 22:31:36 +02:00
Daniel b25b4c83ac Umringsprüfung-Segmentbasiert 2026-04-23 22:24:14 +02:00
Daniel 7948131d5a von geosn-NAS auf geosn-AAA umgestellt 2026-04-23 13:07:35 +02:00
Daniel 16fa99a84e selectbyposition auf 6 (within) 2026-04-23 13:04:35 +02:00
Daniel f8bdcde115 verfahrensgebiet aus Flst 2026-04-22 14:53:45 +02:00
release-bot 168b6ec46c Release 26.4.5-unstable 2026-04-22 07:05:13 +00:00
Daniel bcbfa4c49e QgsMaxVersion ergänzt 2026-04-22 09:03:23 +02:00
release-bot 610e70ec5e Release 26.4.4-unstable 2026-04-22 06:44:01 +00:00
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
33 changed files with 8878 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)
+19
View File
@@ -0,0 +1,19 @@
---
Version 26.4.6-unstable:
---
Version 26.4.5-unstable:
---
Version 26.4.4-unstable:
---
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
+241
View File
@@ -0,0 +1,241 @@
"""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.
**Erweiterung gegenüber dem Original:**
- ``vorgabe_schluessel``: Optional vorausgefüllte Gemarkungsnummern (z. B. aus
:func:`~sn_verfahrensgebiet.functions.gemarkung_alkis.gemarkung_schluessel_und_namen_by_bbox`).
Der Dialog wird mit diesem Vorschlag vorausgefüllt; der Nutzer kann die
Eingabe ändern oder ergänzen.
- ``vorgabe_namen``: Zugehörige Bezeichnungen, die im Dialog als Hinweis
angezeigt werden (kein eigenständiges Feld, nur Anzeigetext).
- ``bbox``: Optionaler BBox-Parameter, der – sofern übergeben – als
zusätzlicher serverseitiger Filter in die WFS-URI eingebettet wird. So
werden ausschließlich Flurstücke geladen, die den Verfahrensgebiets-Rahmen
überlappen.
"""
from __future__ import annotations
from typing import List, Optional, Tuple
from sn_basis.functions import QgsVectorLayer, QgsProject, QgsFeature
from sn_basis.functions import QMessageBox
from sn_basis.functions.dialog_wrapper import ask_text
from sn_basis.functions.message_wrapper import info as _info
# LoadStatus aus verfahrensgebiet_alkis wiederverwenden (identische Semantik)
from sn_verfahrensgebiet.functions.verfahrensgebiet_alkis import LoadStatus
ALKIS_SF_URL = "https://geodienste.sachsen.de/aaa/public_alkis/sf/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 _lade_flurstuecke_per_gemarkung(schluessel_liste: List[str]):
"""Lädt Flurstücke für mehrere Gemarkungen, eine WFS-Anfrage pro Schlüssel.
Vermeidet das WFS-Feature-Limit einer einzelnen Abfrage mit mehreren
Gemarkungsnummern. Alle Features werden in einem Memory-Layer zusammengeführt.
Parameters
----------
schluessel_liste : list of str
4-stellige Gemarkungsnummern.
Returns
-------
QgsVectorLayer or None
Memory-Layer mit allen Flurstücken, oder ``None`` wenn nichts geladen wurde.
"""
alle_feats: list = []
schema_fields = None
schema_crs = "EPSG:25833"
for schluessel in schluessel_liste:
uri = (
f"url='{ALKIS_SF_URL}' typename='{FLST_TYPENAME}' srsname='EPSG:25833' "
f"sql=SELECT * FROM AX_Flurstueck "
f"WHERE gemarkung_ax_gemarkung_schluessel_gemarkungsnummer = '{schluessel}'"
)
wfs = QgsVectorLayer(uri, f"_TmpFlst{schluessel}", "WFS")
if not wfs.isValid():
continue
feats = list(wfs.getFeatures())
if not feats:
continue
if schema_fields is None:
schema_fields = wfs.fields()
schema_crs = wfs.crs().authid()
_info(
"Flurstücke",
f"Gemarkung {schluessel}: {len(feats)} Flurstücke geladen.",
duration=3,
)
alle_feats.append((wfs.fields(), feats))
if not alle_feats or schema_fields is None:
return None
# Memory-Layer mit dem Feldschema des ersten WFS-Layers aufbauen
mem = QgsVectorLayer(f"Polygon?crs={schema_crs}", FLST_LAYER_NAME, "memory")
pr = mem.dataProvider()
pr.addAttributes(list(schema_fields))
mem.updateFields()
mem_fields = mem.fields()
neue_features = []
for wfs_fields, feats in alle_feats:
for feat in feats:
new_f = QgsFeature(mem_fields)
new_f.setGeometry(feat.geometry())
for i in range(wfs_fields.count()):
fname = wfs_fields.field(i).name()
idx = mem_fields.indexFromName(fname)
if idx >= 0:
try:
new_f.setAttribute(idx, feat[fname])
except Exception:
pass
neue_features.append(new_f)
pr.addFeatures(neue_features)
mem.updateExtents()
return mem
def flurstueck_alkis(
tab_widget,
vorgabe_schluessel: Optional[List[str]] = None,
vorgabe_namen: Optional[List[str]] = None,
bbox=None,
ohne_dialog: bool = False,
) -> Tuple[Optional[QgsVectorLayer], LoadStatus]:
"""Lädt Flurstücke aus dem ALKIS-WFS anhand von Gemarkungsschlüsseln.
Jede Gemarkungsnummer wird als separate WFS-Anfrage gestellt, um das
WFS-Feature-Limit bei einer Gesamtabfrage zu umgehen. Alle Ergebnisse
werden in einem Memory-Layer zusammengeführt.
Parameters
----------
tab_widget :
Eltern-Widget für Dialoge.
vorgabe_schluessel : list of str, optional
Vorausgefüllte Gemarkungsnummern (4-stellig). Im Dialog als Standard
angezeigt; bei ``ohne_dialog=True`` direkt verwendet.
vorgabe_namen : list of str, optional
Menschenlesbare Gemarkungsbezeichnungen für den Dialog-Hinweistext.
bbox :
Nicht mehr verwendet (WFS unterstützt keinen BBox-Filter). Bleibt
für Rückwärtskompatibilität in der Signatur erhalten.
ohne_dialog : bool, optional
``True``: Überspringt die Gemarkungsschlüssel-Nutzerabfrage und verwendet
``vorgabe_schluessel`` direkt. Vorhandene Layer werden ohne Rückfrage
ersetzt. Gedacht für die automatische Pipeline
(``verfahrensgebiet_alkis_komplett``). Standard: ``False``.
Returns
-------
Tuple[Optional[QgsVectorLayer], LoadStatus]
Memory-Layer mit allen Flurstücken und Ladestatus.
Bei Fehlern ist der Layer ``None`` und der Status :attr:`LoadStatus.NONE`.
"""
# --- Schritt 1: Vorhandenen Layer behandeln ----------------------------
if ohne_dialog:
project = QgsProject.instance()
hatte_layer = bool(project.mapLayersByName(FLST_LAYER_NAME))
for lyr in list(project.mapLayersByName(FLST_LAYER_NAME)):
project.removeMapLayer(lyr.id())
status = LoadStatus.RELOAD if hatte_layer else LoadStatus.FIRST
else:
existing, status = _check_existing_flurstueck_layer(tab_widget)
if status == LoadStatus.KEEP:
return existing, status
# --- Schritt 2: Gemarkungsschlüssel bestimmen -------------------------
if ohne_dialog:
# Pipeline-Modus: vorgabe_schluessel direkt verwenden, kein Dialog
if not vorgabe_schluessel:
return None, LoadStatus.NONE
schluessel_liste = [s.strip() for s in vorgabe_schluessel if s.strip()]
if not schluessel_liste:
return None, LoadStatus.NONE
else:
# Interaktiver Modus: Dialog mit Vorschlag
default_text = ", ".join(vorgabe_schluessel) if vorgabe_schluessel else ""
hinweis = ""
if vorgabe_namen:
hinweis = " (Vorschlag: " + ", ".join(vorgabe_namen) + ")"
label = f"Gemarkungsschlüssel eingeben (mehrere mit Komma trennen):{hinweis}"
eingabe, ok = ask_text(
"Gemarkungsschlüssel",
label,
default_text=default_text,
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
# --- Schritt 3: Flurstücke gemarkungsweise laden ----------------------
layer = _lade_flurstuecke_per_gemarkung(schluessel_liste)
if not layer or not layer.isValid() or layer.featureCount() == 0:
if not ohne_dialog:
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
+207
View File
@@ -0,0 +1,207 @@
"""sn_verfahrensgebiet/functions/gemarkung_alkis.py – ALKIS-Gemarkungsabfrage.
Stellt Funktionen bereit, um Katasterbezirk-Objekte aus dem sächsischen
ALKIS-vereinfachten WFS zu laden und daraus Gemarkungsschlüssel zu extrahieren,
die als Vorschlag für die Flurstücksabfrage (Phase 2) dienen.
**Dienst:** ``https://geodienste.sachsen.de/aaa/public_alkis/vereinf/wfs``
**Typename:** ``Katasterbezirk``
Dieser Dienst hat – im Gegensatz zu ``adv:AX_Gemarkung`` aus dem NAS-Dienst –
tatsächliche Geometrien. Die client-seitige Verschneidung mit der VG-Geometrie
ist damit möglich.
**Attributkonvention:**
- ``schluessel``: vollständiger Katasterbezirksschlüssel (z.B. ``140640001``),
die letzten 4 Stellen entsprechen der
``gemarkung_ax_gemarkung_schluessel_gemarkungsnummer`` im NAS-Flurstücksdienst.
- ``ave_name``: Gemarkungsbezeichnung.
**Wichtig:** Diese Funktion stellt keine Nutzerabfragen. Sie gibt im
Fehlerfall eine leere Liste zurück.
"""
from __future__ import annotations
from typing import List, Optional, Tuple
from sn_basis.functions import QgsVectorLayer, QgsGeometry
from sn_basis.functions.message_wrapper import warning as _warn, info as _info
ALKIS_VEREINF_URL = "https://geodienste.sachsen.de/aaa/public_alkis/vereinf/wfs"
KATASTERBEZIRK_TYPENAME = "ave:KatasterBezirk"
# Attributnamen im vereinfachten Dienst
_ATTR_SCHLUESSEL = "schluessel" # vollständiger Schlüssel, letzte 4 = Gemarkungsnummer
_ATTR_NAME = "ave_name"
# ---------------------------------------------------------------------------
# Interne Hilfsfunktionen
# ---------------------------------------------------------------------------
def _extract_schluessel_und_name(feature) -> Tuple[str, str]:
"""Extrahiert Gemarkungsnummer (letzte 4 Stellen von ``schluessel``)
und Namen (``ave_name``) aus einem Katasterbezirk-Feature.
Returns
-------
Tuple[str, str]
``(gemarkungsnummer_4stellig, name)`` – leer wenn nicht ermittelbar.
"""
def _get(field: str) -> str:
try:
val = feature[field]
return str(val).strip() if val else ""
except (KeyError, Exception):
return ""
schluessel = _get(_ATTR_SCHLUESSEL)
name = _get(_ATTR_NAME)
# Letzte 4 Stellen des Schlüssels = Gemarkungsnummer
nummer = schluessel[-4:] if len(schluessel) >= 4 else schluessel
return nummer, name
def _load_all_gemarkung_layer():
"""Lädt alle Katasterbezirk-Objekte aus dem vereinfachten ALKIS-WFS.
Sachsen hat ≈ 2 000 Gemarkungen. Der vereinfachte Dienst liefert
tatsächliche Geometrien, sodass client-seitige Verschneidung möglich ist.
Returns
-------
QgsVectorLayer or None
Geladener Layer oder ``None`` bei Ungültigkeit.
"""
uri = (
f"url='{ALKIS_VEREINF_URL}' typename='{KATASTERBEZIRK_TYPENAME}' "
f"srsname='EPSG:25833'"
)
layer = QgsVectorLayer(uri, "_TempKatasterbezirk", "WFS")
return layer if (layer and layer.isValid()) else None
# ---------------------------------------------------------------------------
# Öffentliche API
# ---------------------------------------------------------------------------
def gemarkung_schluessel_und_namen_von_vg_geometrie(
vg_geometrie,
) -> List[Tuple[str, str]]:
"""Gibt Gemarkungsschlüssel und -namen aller Gemarkungen zurück, die die
VG-Geometrie schneiden.
Da der WFS-Dienst keine serverseitigen BBox- oder Geometriefilter
unterstützt, werden alle ``AX_Gemarkung``-Objekte geladen und client-seitig
per ``QgsGeometry.intersects`` gefiltert.
Der Aufruf ist *ohne Nutzerinteraktion* – im Fehlerfall wird eine leere
Liste zurückgegeben, damit der aufrufende Workflow manuellen Fallback
ermöglicht.
Parameters
----------
vg_geometrie :
``QgsGeometry`` der Verfahrensgebiets-Union (aus den Roh-WFS-Features
von ``AX_BauRaumOderBodenordnungsrecht``). In EPSG:25833.
Returns
-------
List[Tuple[str, str]]
Liste von ``(gemarkungsnummer, bezeichnung)``-Tupeln, aufsteigend
sortiert nach Nummer. Enthält keine Duplikate. Leer bei Fehler.
Examples
--------
>>> from qgis.core import QgsGeometry
>>> vg_geom = QgsGeometry.fromWkt("POLYGON((414000 5657000, 415000 5657000, ...))")
>>> result = gemarkung_schluessel_und_namen_von_vg_geometrie(vg_geom)
>>> # result: [('1234', 'Beispielgemarkung'), ('5678', 'Andere Gemarkung')]
"""
if vg_geometrie is None or vg_geometrie.isEmpty():
_warn("Gemarkung", "[DIAG] VG-Geometrie ist None oder leer – Gemarkungssuche abgebrochen.", duration=8)
return []
try:
layer = _load_all_gemarkung_layer()
except Exception as exc:
_warn("Gemarkung", f"[DIAG] Fehler beim Laden des Gemarkung-Layers: {exc}", duration=8)
return []
if not layer:
_warn("Gemarkung", "[DIAG] AX_Gemarkung-Layer ungültig oder nicht ladbar.", duration=8)
return []
# Feature-Anzahl ermitteln (0 = WFS gibt nichts zurück)
alle_features = list(layer.getFeatures())
if not alle_features:
_warn(
"Gemarkung",
"[DIAG] AX_Gemarkung-Layer ist gültig, aber enthält 0 Features. "
"WFS-Dienst liefert ohne Filter möglicherweise kein Ergebnis.",
duration=10,
)
return []
# Feldnamen der ersten Features loggen
feldnamen = [alle_features[0].fields().field(i).name() for i in range(alle_features[0].fields().count())]
hat_geom = sum(1 for f in alle_features[:20] if f.hasGeometry() and not f.geometry().isEmpty())
_info(
"Gemarkung",
f"[DIAG] {len(alle_features)} Gemarkungen geladen. "
f"Von ersten 20 haben {hat_geom} Geometrie. "
f"Alle Felder ({len(feldnamen)}): {feldnamen}",
duration=15,
)
result: dict[str, str] = {}
kein_schnitt = 0
for feat in alle_features:
try:
gem_geom = feat.geometry()
if gem_geom is None or gem_geom.isEmpty():
continue
if not gem_geom.intersects(vg_geometrie):
kein_schnitt += 1
continue
except Exception as exc:
_warn("Gemarkung", f"[DIAG] intersects() Fehler: {exc}", duration=8)
continue
nummer, name = _extract_schluessel_und_name(feat)
if nummer and nummer not in result:
result[nummer] = name
if not result:
_warn(
"Gemarkung",
f"[DIAG] 0 Gemarkungen nach Verschneidung. {kein_schnitt} Features ohne Schnitt. "
f"VG-Geometrie gültig: {not vg_geometrie.isNull()}, "
f"VG-Typ: {vg_geometrie.type()}, VG-WKT (Anfang): {vg_geometrie.asWkt()[:80]}",
duration=15,
)
return sorted(result.items(), key=lambda t: t[0])
def gemarkung_schluessel_von_vg_geometrie(vg_geometrie) -> List[str]:
"""Gibt ausschließlich die Gemarkungsnummern für die VG-Geometrie zurück.
Convenience-Wrapper um
:func:`gemarkung_schluessel_und_namen_von_vg_geometrie`.
Parameters
----------
vg_geometrie :
``QgsGeometry`` des Verfahrensgebiets in EPSG:25833.
Returns
-------
List[str]
Sortierte Liste der Gemarkungsnummern. Leer bei Fehler oder keinem Treffer.
"""
return [nr for nr, _ in gemarkung_schluessel_und_namen_von_vg_geometrie(vg_geometrie)]
File diff suppressed because it is too large Load Diff
+467
View File
@@ -0,0 +1,467 @@
"""sn_verfahrensgebiet/functions/knickpunkt_pruefung.py – Knickpunktbasierte BROR-Prüfung.
Vergleicht die Knickpunkte (Stützpunkte) des BauRaumOderBodenordnungsrecht-
Polygons mit den Punktorten der geladenen ``AX_PunktortTA``-Objekte und
identifiziert:
- **Identische Punkte** (grün): An einem BROR-Knickpunkt liegt ein PunktortTA.
- **Fehlende PunktortTA** (rot): Ein BROR-Knickpunkt hat keinen zugehörigen PunktortTA.
- **Fehlende Knickpunkte** (orange): Ein PunktortTA liegt nahe des BROR-Umrings
auf einer Flurstücksgrenze, hat aber keinen entsprechenden Knickpunkt im BROR.
**Algorithmus:**
1. BROR-Polygon-Stützpunkte (Knickpunkte) extrahieren.
2. Spatial-Index auf PunktortTA aufbauen.
3. Für jeden Knickpunkt nächsten PunktortTA im Radius von
:data:`KOORDINATEN_TOLERANZ_M` suchen → grün oder rot.
Gematchte PunktortTA-Feature-IDs merken.
4. Gepufferter BROR-Umring + Flurstück-Vertex-Set ermitteln.
5. Nicht-gematchte PunktortTA nahe BROR-Umring, die auf einer Flurstücksgrenze
liegen → orange.
**Rückgabe:** ``(result_layer, identisch, kein_pta, fehlender_knick)``
"""
from __future__ import annotations
from typing import Dict, List, Optional, Set, Tuple
from sn_basis.functions.qgiscore_wrapper import (
QgsVectorLayer,
QgsFeature,
QgsField,
QgsGeometry,
)
from sn_basis.functions.qt_wrapper import QVariant
# ---------------------------------------------------------------------------
# Konstanten
# ---------------------------------------------------------------------------
#: Koordinatentoleranz für die Knickpunkt ↔ PunktortTA-Übereinstimmung (m).
KOORDINATEN_TOLERANZ_M: float = 0.01
#: Puffer um den BROR-Umring für die Suche nach fehlenden Knickpunkten (m).
BOUNDARY_PUFFER_M: float = 0.5
#: Rundungsgenauigkeit für Vertex-Koordinaten im Flurstück-Set (Dezimalstellen).
_VERTEX_ROUND = 3
ERGEBNIS_LAYER_NAME = "Knickpunktprüfung"
FELD_TYP = "typ"
WERT_IDENTISCH = "identisch"
WERT_KEIN_PTA = "kein_pta"
WERT_FEHLENDER_KNICK = "fehlender_knick"
# ---------------------------------------------------------------------------
# Interne Hilfsfunktionen
# ---------------------------------------------------------------------------
def _extrahiere_knickpunkte(bror_layer) -> List[object]:
"""Extrahiert alle Stützpunkte (Knickpunkte) des BROR-Polygons.
Doppelte Start/End-Punkte eines Rings werden dedupliziert. Gibt eine
flache Liste von ``QgsPointXY``-Objekten zurück.
Parameters
----------
bror_layer :
BauRaumOderBodenordnungsrecht-Polygon-Layer.
Returns
-------
List[QgsPointXY]
Alle eindeutigen Stützpunkte; leer bei Fehler.
"""
try:
from qgis.core import QgsPointXY # type: ignore[import]
bror_feats = [
f for f in bror_layer.getFeatures()
if f.geometry() and not f.geometry().isEmpty()
]
if not bror_feats:
return []
union_geom = QgsGeometry.unaryUnion([f.geometry() for f in bror_feats])
if union_geom is None or union_geom.isEmpty():
return []
punkte: List[object] = []
seen: Set[Tuple[float, float]] = set()
def _add_ring(vertices):
# Letzter Punkt = erster Punkt (Ring geschlossen) → überspringen
ring_pts = list(vertices)
n = len(ring_pts)
if n >= 2:
# Prüfen ob letzter Punkt identisch mit erstem
last = n - 1
if (
abs(ring_pts[last].x() - ring_pts[0].x()) < 1e-9
and abs(ring_pts[last].y() - ring_pts[0].y()) < 1e-9
):
ring_pts = ring_pts[:-1]
for pt in ring_pts:
key = (round(pt.x(), _VERTEX_ROUND), round(pt.y(), _VERTEX_ROUND))
if key not in seen:
seen.add(key)
punkte.append(pt)
geom_type = union_geom.wkbType()
# Polygon oder MultiPolygon
if union_geom.isMultipart():
for poly in union_geom.asMultiPolygon():
for ring in poly:
_add_ring([QgsPointXY(x, y) for x, y in ring])
else:
for ring in union_geom.asPolygon():
_add_ring([QgsPointXY(x, y) for x, y in ring])
return punkte
except Exception:
return []
def _erstelle_ergebnis_layer(crs_authid: str) -> object:
"""Erzeugt den leeren Punkt-Ergebnislayer mit Feld ``typ``.
Parameters
----------
crs_authid :
CRS-Kennung, z.B. ``"EPSG:25833"``.
Returns
-------
QgsVectorLayer
Leerer Memory-Layer vom Typ Point.
"""
layer = QgsVectorLayer(
f"Point?crs={crs_authid}", ERGEBNIS_LAYER_NAME, "memory"
)
pr = layer.dataProvider()
pr.addAttributes([QgsField(FELD_TYP, QVariant.String)])
layer.updateFields()
return layer
def _setze_kategorien_symbologie(layer) -> None:
"""Wendet Kategoriesymbologie an: grün/rot/orange Punkte.
- ``identisch`` → grün
- ``kein_pta`` → rot
- ``fehlender_knick`` → orange
Wird bei fehlendem QGIS-Kontext still ignoriert.
"""
try:
from qgis.core import ( # type: ignore[import]
QgsCategorizedSymbolRenderer,
QgsRendererCategory,
QgsMarkerSymbol,
)
def _marker(color: str, size: str = "2") -> object:
return QgsMarkerSymbol.createSimple(
{"color": color, "size": size, "outline_style": "no"}
)
categories = [
QgsRendererCategory(
WERT_IDENTISCH,
_marker("0,180,0,255"),
"identisch (Knickpunkt mit PunktortTA)",
),
QgsRendererCategory(
WERT_KEIN_PTA,
_marker("220,0,0,255"),
"kein PunktortTA an Knickpunkt",
),
QgsRendererCategory(
WERT_FEHLENDER_KNICK,
_marker("255,128,0,255"),
"fehlender Knickpunkt (PunktortTA ohne BROR-Knickpunkt)",
),
]
renderer = QgsCategorizedSymbolRenderer(FELD_TYP, categories)
layer.setRenderer(renderer)
except Exception:
pass # Kein QGIS-Kontext (Tests) → Standarddarstellung behalten
def _baue_pta_index(punktort_layer) -> Tuple[object, Dict[int, object]]:
"""Baut einen QgsSpatialIndex und eine ID→Geometrie-Map für PunktortTA.
Parameters
----------
punktort_layer :
Geladener ``AX_PunktortTA``-Layer.
Returns
-------
Tuple[QgsSpatialIndex, Dict[int, QgsGeometry]]
Index und Feature-ID-zu-Geometrie-Dict.
"""
from qgis.core import QgsSpatialIndex # type: ignore[import]
index = QgsSpatialIndex()
geom_map: Dict[int, object] = {}
for feat in punktort_layer.getFeatures():
geom = feat.geometry()
if geom and not geom.isEmpty():
index.insertFeature(feat)
geom_map[feat.id()] = geom
return index, geom_map
def _baue_flst_vertex_set(
flst_layer,
bror_buffer_geom,
) -> Set[Tuple[float, float]]:
"""Erstellt ein Set gerundeter (x, y)-Koordinaten aller Flurstück-Vertices
innerhalb des gepufferten BROR-Umrings.
Parameters
----------
flst_layer :
Flurstücke-Layer.
bror_buffer_geom :
Gepufferter BROR-Umring (Polygon-Geometrie).
Returns
-------
Set[Tuple[float, float]]
Gerundete Vertex-Koordinaten.
"""
vertex_set: Set[Tuple[float, float]] = set()
if flst_layer is None:
return vertex_set
try:
for feat in flst_layer.getFeatures():
geom = feat.geometry()
if geom is None or geom.isEmpty():
continue
try:
# Nur Flurstücke berücksichtigen, die den Puffer berühren
if not geom.intersects(bror_buffer_geom):
continue
boundary = geom.convertToType(1) # 1 = Line
if boundary is None or boundary.isEmpty():
continue
if boundary.isMultipart():
parts = boundary.asMultiPolyline()
else:
parts = [boundary.asPolyline()]
for line in parts:
for pt in line:
vertex_set.add(
(round(pt.x(), _VERTEX_ROUND), round(pt.y(), _VERTEX_ROUND))
)
except Exception:
continue
except Exception:
pass
return vertex_set
# ---------------------------------------------------------------------------
# Öffentliche API
# ---------------------------------------------------------------------------
def knickpunkt_pruefung(
bror_layer,
punktort_layer,
flst_layer,
progress=None,
knickpunkte: Optional[List] = None,
) -> Tuple[Optional[object], int, int, int]:
"""Knickpunktbasierte BROR-Prüfung.
Vergleicht BROR-Knickpunkte mit ``AX_PunktortTA``-Objekten und ermittelt
fehlende Knickpunkte anhand von Flurstücksgrenzen.
Parameters
----------
bror_layer :
BauRaumOderBodenordnungsrecht-Polygon-Layer.
punktort_layer :
Geladener ``AX_PunktortTA``-Layer (Ergebnis aus ``grenzpunkt_alkis``).
flst_layer :
Flurstücke-Layer für die Orange-Prüfung. Kann ``None`` sein.
progress :
Optionaler ``ProgressDialog`` für Fortschrittsanzeige.
knickpunkte :
Vorberechnete Knickpunktliste (optional). Wenn ``None``, werden die
Knickpunkte aus ``bror_layer`` extrahiert.
Returns
-------
Tuple[Optional[QgsVectorLayer], int, int, int]
``(result_layer, identisch_count, kein_pta_count, fehlender_knick_count)``
- ``result_layer``: Punkt-Layer mit grünen/roten/orangen Punkten,
oder ``None`` bei Abbruch oder fehlgeschlagener Verarbeitung.
- ``identisch_count``: Knickpunkte mit passendem PunktortTA (grün).
- ``kein_pta_count``: Knickpunkte ohne passenden PunktortTA (rot).
- ``fehlender_knick_count``: PunktortTA ohne BROR-Knickpunkt (orange).
"""
def _canceled() -> bool:
return progress is not None and progress.is_canceled()
def _schritt(label: str, wert: int) -> None:
if progress is not None:
progress.set_label(label)
progress.set_value(wert)
# ------------------------------------------------------------------
# Schritt 0: Knickpunkte ermitteln
# ------------------------------------------------------------------
_schritt("Knickpunktprüfung: Knickpunkte extrahieren …", 0)
if knickpunkte is None:
knickpunkte = _extrahiere_knickpunkte(bror_layer)
if not knickpunkte:
return None, 0, 0, 0
n = len(knickpunkte)
crs_authid = bror_layer.crs().authid()
if _canceled():
return None, 0, 0, 0
# ------------------------------------------------------------------
# Schritt 1: Spatial-Index für PunktortTA aufbauen
# ------------------------------------------------------------------
_schritt("Knickpunktprüfung: PunktortTA-Index aufbauen …", 1)
if punktort_layer is None:
return None, 0, 0, 0
try:
pta_index, geom_map = _baue_pta_index(punktort_layer)
except Exception:
return None, 0, 0, 0
if _canceled():
return None, 0, 0, 0
# ------------------------------------------------------------------
# Schritte 2…n+1: Jeden Knickpunkt mit PunktortTA vergleichen
# ------------------------------------------------------------------
try:
from qgis.core import QgsRectangle # type: ignore[import]
except ImportError:
return None, 0, 0, 0
ergebnis_layer = _erstelle_ergebnis_layer(crs_authid)
pr = ergebnis_layer.dataProvider()
feats_to_add: List[object] = []
identisch_count = 0
kein_pta_count = 0
matched_pta_ids: Set[int] = set()
for i, knick_pt in enumerate(knickpunkte):
if _canceled():
return None, 0, 0, 0
_schritt(f"Knickpunktprüfung: Knickpunkt {i + 1}/{n} prüfen …", i + 2)
x, y = knick_pt.x(), knick_pt.y()
t = KOORDINATEN_TOLERANZ_M
suchbox = QgsRectangle(x - t, y - t, x + t, y + t)
kandidaten = pta_index.intersects(suchbox)
gefunden = False
for fid in kandidaten:
pta_geom = geom_map.get(fid)
if pta_geom is None:
continue
pta_pt = pta_geom.asPoint()
dist = ((pta_pt.x() - x) ** 2 + (pta_pt.y() - y) ** 2) ** 0.5
if dist <= KOORDINATEN_TOLERANZ_M:
gefunden = True
matched_pta_ids.add(fid)
break
feat = QgsFeature()
try:
from qgis.core import QgsPointXY # type: ignore[import]
feat.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(x, y)))
except Exception:
continue
feat.setAttributes([WERT_IDENTISCH if gefunden else WERT_KEIN_PTA])
feats_to_add.append(feat)
if gefunden:
identisch_count += 1
else:
kein_pta_count += 1
if _canceled():
return None, 0, 0, 0
# ------------------------------------------------------------------
# Schritt n+2: Fehlende Knickpunkte (orange) ermitteln
# ------------------------------------------------------------------
_schritt("Knickpunktprüfung: Fehlende Knickpunkte ermitteln …", n + 2)
fehlender_knick_count = 0
# BROR-Umring ermitteln und puffern
try:
bror_feats = [
f for f in bror_layer.getFeatures()
if f.geometry() and not f.geometry().isEmpty()
]
bror_union = QgsGeometry.unaryUnion([f.geometry() for f in bror_feats])
bror_boundary = bror_union.convertToType(1) if bror_union else None
bror_buffer_geom = (
bror_boundary.buffer(BOUNDARY_PUFFER_M, 8) if bror_boundary else None
)
except Exception:
bror_buffer_geom = None
if bror_buffer_geom is not None and flst_layer is not None:
flst_vertex_set = _baue_flst_vertex_set(flst_layer, bror_buffer_geom)
for feat in punktort_layer.getFeatures():
if _canceled():
break
if feat.id() in matched_pta_ids:
continue
geom = feat.geometry()
if geom is None or geom.isEmpty():
continue
try:
if not geom.within(bror_buffer_geom):
continue
pta_pt = geom.asPoint()
key = (round(pta_pt.x(), _VERTEX_ROUND), round(pta_pt.y(), _VERTEX_ROUND))
if key in flst_vertex_set:
orange_feat = QgsFeature()
orange_feat.setGeometry(geom)
orange_feat.setAttributes([WERT_FEHLENDER_KNICK])
feats_to_add.append(orange_feat)
fehlender_knick_count += 1
except Exception:
continue
if _canceled():
return None, 0, 0, 0
# ------------------------------------------------------------------
# Schritt n+3: Ergebnislayer fertigstellen
# ------------------------------------------------------------------
_schritt("Knickpunktprüfung: Ergebnislayer erstellen …", n + 3)
pr.addFeatures(feats_to_add)
ergebnis_layer.updateExtents()
_setze_kategorien_symbologie(ergebnis_layer)
return ergebnis_layer, identisch_count, kein_pta_count, fehlender_knick_count
+311
View File
@@ -0,0 +1,311 @@
"""sn_verfahrensgebiet/functions/umringspruefung.py – Umringsprüfung (Variante 1).
Vergleicht den Umring (Boundary) des BauRaumOderBodenordnungsrecht-Polygons
mit den Grenzen der angrenzenden Flurstücke und hebt Abweichungen hervor.
**Algorithmus (Variante 1 – linienbasierter Umringvergleich):**
1. BROR-Polygon → Umringslinie via :func:`sn_basis.functions.qgiscore_wrapper.polygon_zu_linie`
(QGIS-3/4-kompatibel).
2. Flurstücke, die den BROR-Umring berühren oder schneiden, werden gefiltert.
3. Gefilterte Flurstücke → Umringslinien.
4. Union aller Flurstücksumringe.
5. ``BROR-Umring.difference(Flurstücksumrings-Union)`` → Abweichungen (rot).
6. ``BROR-Umring.difference(rote_Segmente)`` → Übereinstimmungen (grün).
7. Ergebnislayer mit Kategoriesymbologie (grün/rot) und Legendenbeschriftung.
**Rückgabe:** ``(result_layer, conflict_count)``
- ``result_layer``: Temporärer Linienlayer mit Feld ``typ``
(``"identisch"`` = grün, ``"Abweichung"`` = rot).
- ``conflict_count``: Anzahl der roten Konfliktsegmente (Parts).
"""
from __future__ import annotations
from typing import List, Optional, Tuple
from sn_basis.functions.qgiscore_wrapper import (
QgsVectorLayer,
QgsFeature,
QgsField,
QgsGeometry,
polygon_zu_linie,
)
from sn_basis.functions.qt_wrapper import QVariant
# ---------------------------------------------------------------------------
# Konstanten
# ---------------------------------------------------------------------------
#: Geometrische Toleranz für den Linienvergleich (EPSG:25833 = Meter).
#: Der BROR-Umring wird gegen die um diesen Betrag gepufferten Flurstücksumringe
#: verglichen, um numerische Rundungsfehler aus ALKIS-Koordinaten abzufangen.
LINIE_TOLERANZ_M: float = 0.0001
ERGEBNIS_LAYER_NAME = "Umringsprüfung"
FELD_TYP = "typ"
WERT_IDENTISCH = "identisch"
WERT_ABWEICHUNG = "Abweichung"
# ---------------------------------------------------------------------------
# Interne Hilfsfunktionen
# ---------------------------------------------------------------------------
def _zerlege_in_teile(geom: object) -> List[object]:
"""Zerlegt eine (Multi-)LineString-Geometrie in einzelne Teile.
Parameters
----------
geom :
``QgsGeometry`` vom Typ LineString oder MultiLineString.
Returns
-------
List[QgsGeometry]
Liste einzelner Geometrieteile; leer wenn ``geom`` None oder leer ist.
"""
if geom is None:
return []
try:
if getattr(geom, "isEmpty", lambda: True)():
return []
# asGeometryCollection() gibt eigene Kopien zurück – kein Ownership-Problem.
# QgsGeometry(part) über geom.parts() ist in QGIS 4.x aus den Python-Bindings
# entfernt worden und führt zu Absturz beim Rendern.
parts = geom.asGeometryCollection()
if parts:
return [p for p in parts if p is not None and not p.isEmpty()]
except Exception:
pass
# Fallback: Geometrie nicht zerlegbar → als Ganzes zurückgeben
try:
if not geom.isEmpty():
return [geom]
except Exception:
pass
return []
def _erstelle_ergebnis_layer(crs_authid: str) -> object:
"""Erzeugt den leeren Ergebnislayer mit Feld ``typ``.
Parameters
----------
crs_authid :
CRS-Kennung des Quell-Layers (z. B. ``"EPSG:25833"``).
Returns
-------
QgsVectorLayer
Leerer Memory-Layer vom Typ LineString.
"""
layer = QgsVectorLayer(
f"LineString?crs={crs_authid}", ERGEBNIS_LAYER_NAME, "memory"
)
pr = layer.dataProvider()
pr.addAttributes([QgsField(FELD_TYP, QVariant.String)])
layer.updateFields()
return layer
def _setze_kategorien_symbologie(layer) -> None:
"""Wendet Kategoriesymbologie an: grün = identisch, rot = Abweichung.
Wird bei fehlendem QGIS-Kontext still ignoriert (Unit-Test-Robustheit).
"""
try:
from qgis.core import ( # type: ignore[import]
QgsCategorizedSymbolRenderer,
QgsRendererCategory,
QgsLineSymbol,
)
green_sym = QgsLineSymbol.createSimple(
{"color": "0,180,0,255", "line_width": "0.5"}
)
red_sym = QgsLineSymbol.createSimple(
{"color": "220,0,0,255", "line_width": "0.8"}
)
categories = [
QgsRendererCategory(
WERT_IDENTISCH,
green_sym,
"identisch (übereinstimmend)",
),
QgsRendererCategory(
WERT_ABWEICHUNG,
red_sym,
"Abweichung (nicht in Flurstücken)",
),
]
renderer = QgsCategorizedSymbolRenderer(FELD_TYP, categories)
layer.setRenderer(renderer)
except Exception:
pass # Kein QGIS-Kontext (Tests) → Standarddarstellung behalten
# ---------------------------------------------------------------------------
# Öffentliche API
# ---------------------------------------------------------------------------
def umringspruefung(
bror_layer,
flst_layer,
progress=None,
linie_toleranz_m: float = LINIE_TOLERANZ_M,
) -> Tuple[Optional[object], int]:
"""Variante 1: Linienbasierter Umringvergleich.
Ermittelt, welche Teile des BROR-Umrings mit den Flurstücksgrenzen
übereinstimmen (grün) und welche keine Entsprechung haben (rot).
Parameters
----------
bror_layer :
BauRaumOderBodenordnungsrecht-Memory-Layer (Polygon).
flst_layer :
Flurstücke-Layer (Polygon, WFS oder Memory).
progress :
Optionaler ``ProgressDialog`` aus
:mod:`sn_basis.functions.dialog_wrapper` für Fortschrittsanzeige
und Abbrucherkennung.
linie_toleranz_m : float
Geometrische Puffertoleranz für den Linienvergleich in
CRS-Einheiten (Standardwert :data:`LINIE_TOLERANZ_M` = 0.001 m).
Returns
-------
Tuple[Optional[QgsVectorLayer], int]
``(result_layer, conflict_count)``
- ``result_layer``: Linienlayer mit grünen/roten Segmenten,
oder ``None`` bei Abbruch oder fehlgeschlagener Verarbeitung.
- ``conflict_count``: Anzahl der roten Konfliktsegmente (BROR-Teile
ohne Entsprechung in den Flurstücksgrenzen).
"""
def _canceled() -> bool:
return progress is not None and progress.is_canceled()
def _schritt(label: str, wert: int) -> None:
if progress is not None:
progress.set_label(label)
progress.set_value(wert)
# ------------------------------------------------------------------
# 1. BROR-Geometrie und Umringslinie ermitteln
# ------------------------------------------------------------------
_schritt("Umringsprüfung: BROR-Umring ermitteln …", 0)
bror_feats = [
f for f in bror_layer.getFeatures()
if f.geometry() and not f.geometry().isEmpty()
]
if not bror_feats:
return None, 0
bror_union = QgsGeometry.unaryUnion([f.geometry() for f in bror_feats])
if bror_union is None or bror_union.isEmpty():
return None, 0
bror_boundary = polygon_zu_linie(bror_union)
if bror_boundary is None or bror_boundary.isEmpty():
return None, 0
crs_authid = bror_layer.crs().authid()
if _canceled():
return None, 0
# ------------------------------------------------------------------
# 2. Relevante Flurstücke: nur jene, deren Geometrie den BROR-Umring
# berührt oder schneidet
# ------------------------------------------------------------------
_schritt("Umringsprüfung: Relevante Flurstücke filtern …", 1)
relevante_boundaries: List[object] = []
for feat in flst_layer.getFeatures():
if _canceled():
return None, 0
geom = feat.geometry()
if geom is None or geom.isEmpty():
continue
try:
if geom.intersects(bror_boundary):
boundary = polygon_zu_linie(geom)
if boundary is not None and not boundary.isEmpty():
relevante_boundaries.append(boundary)
except Exception:
continue
if relevante_boundaries:
flst_boundary_union = QgsGeometry.unaryUnion(relevante_boundaries)
else:
flst_boundary_union = None
if _canceled():
return None, 0
# ------------------------------------------------------------------
# 3. Segmentvergleich: Differenz und Übereinstimmung berechnen
# ------------------------------------------------------------------
_schritt("Umringsprüfung: Segmentvergleich …", 2)
ergebnis_layer = _erstelle_ergebnis_layer(crs_authid)
pr = ergebnis_layer.dataProvider()
feats_to_add: List[object] = []
conflict_count = 0
if flst_boundary_union is None or flst_boundary_union.isEmpty():
# Kein Flurstück berührt den BROR-Umring → alles ist Abweichung
for part in _zerlege_in_teile(bror_boundary):
feat = QgsFeature()
feat.setGeometry(part)
feat.setAttributes([WERT_ABWEICHUNG])
feats_to_add.append(feat)
conflict_count += 1
else:
# Flurstücksumringe leicht puffern für Toleranz-Matching
if linie_toleranz_m > 0:
flst_fuer_diff = flst_boundary_union.buffer(linie_toleranz_m, 4)
else:
flst_fuer_diff = flst_boundary_union
# Rote Segmente: BROR-Teile ohne Entsprechung in Flurstücksgrenzen
try:
red_geom = bror_boundary.difference(flst_fuer_diff)
except Exception:
red_geom = None
# Grüne Segmente: BROR-Teile, die nicht rot sind
try:
if red_geom and not red_geom.isEmpty():
green_geom = bror_boundary.difference(red_geom)
else:
green_geom = bror_boundary
except Exception:
green_geom = None
for part in _zerlege_in_teile(green_geom):
feat = QgsFeature()
feat.setGeometry(part)
feat.setAttributes([WERT_IDENTISCH])
feats_to_add.append(feat)
for part in _zerlege_in_teile(red_geom):
feat = QgsFeature()
feat.setGeometry(part)
feat.setAttributes([WERT_ABWEICHUNG])
feats_to_add.append(feat)
conflict_count += 1
if _canceled():
return None, 0
pr.addFeatures(feats_to_add)
ergebnis_layer.updateExtents()
_setze_kategorien_symbologie(ergebnis_layer)
_schritt("Umringsprüfung abgeschlossen.", 3)
return ergebnis_layer, conflict_count
+373
View File
@@ -0,0 +1,373 @@
"""sn_verfahrensgebiet/functions/umringstatistik_grenzpunkte.py – Umringstatistik Grenzpunkte.
Extrahiert aus einem vorhandenen ``Grenzpunkte (ALKIS)``-Layer alle Punkte,
die auf dem Umring des Verfahrensgebiets liegen, und klassifiziert sie in
drei Kategorien:
1. **FGP** (grün): ``gp_festgestellterGrenzpunkt == "true"``
2. **<3cm** (gelb): ``gp_festgestellterGrenzpunkt != "true"``
UND ``qualitaetsangaben_ax_dqpunktort_genauigkeitsstufe == 2100``
3. **>3cm** (rot): ``gp_festgestellterGrenzpunkt != "true"``
UND ``qualitaetsangaben_ax_dqpunktort_genauigkeitsstufe > 2100``
4. **unbekannt** (grau): keiner der obigen Fälle
Der Umring-Test verwendet einen Abstandsschwellwert von :data:`UMRING_TOLERANZ_M`.
**Rückgabe:** Memory-Layer ``Umringstatistik Grenzpunkte`` oder ``None`` bei Fehler.
"""
from __future__ import annotations
from typing import Optional
from sn_basis.functions import QgsProject
from sn_basis.functions.qt_wrapper import QMessageBox
from sn_basis.functions.dialog_wrapper import create_progress_dialog
from sn_basis.functions.message_wrapper import warning
from sn_basis.functions.qgiscore_wrapper import (
QgsVectorLayer,
QgsFeature,
QgsField,
QgsGeometry,
polygon_zu_linie,
)
from sn_basis.functions.qt_wrapper import QVariant
# ---------------------------------------------------------------------------
# Konstanten
# ---------------------------------------------------------------------------
#: Anzeigename des Ergebnis-Layers.
ERGEBNIS_LAYER_NAME = "Umringstatistik Grenzpunkte"
#: Feldname für die Klassifikationskategorie im Ergebnis-Layer.
FELD_KATEGORIE = "kategorie"
#: Feldname für ``gp_festgestellterGrenzpunkt`` im Grenzpunkte(ALKIS)-Layer.
FELD_FESTGESTELLT = "gp_festgestellterGrenzpunkt"
#: Feldname für das Genauigkeitsattribut im Grenzpunkte(ALKIS)-Layer.
FELD_GENAUIGKEIT = "qualitaetsangaben_ax_dqpunktort_genauigkeitsstufe"
#: Feldname für die GML-ID im Grenzpunkte(ALKIS)-Layer.
FELD_GML_ID = "gml_id"
#: Kategoriewert: festgestellter Grenzpunkt.
KATEGORIE_FGP = "FGP"
#: Kategoriewert: Lagegenauigkeit ≤ 3 cm (Gütestufe 2100).
KATEGORIE_KLEIN = "<3cm"
#: Kategoriewert: Lagegenauigkeit > 3 cm (schlechtere Gütestufe).
KATEGORIE_GROSS = ">3cm"
#: Kategoriewert: Kategorie konnte nicht bestimmt werden.
KATEGORIE_UNBEKANNT = "unbekannt"
#: Maximaler Abstand (m) eines Punktes vom Umring, um als „auf dem Umring" zu gelten.
UMRING_TOLERANZ_M: float = 0.1
#: Grenzwert für ``qualitaetsangaben_ax_dqpunktort_genauigkeitsstufe`` (3-cm-Schwelle).
GENAUIGKEIT_GRENZWERT: int = 2100
# ---------------------------------------------------------------------------
# Interne Hilfsfunktionen
# ---------------------------------------------------------------------------
def _setze_kategorien_symbologie(layer) -> None:
"""Wendet Kategoriesymbologie an: grün / gelb / rot / grau.
Wird bei fehlendem QGIS-Kontext still ignoriert.
"""
try:
from qgis.core import ( # type: ignore[import]
QgsCategorizedSymbolRenderer,
QgsRendererCategory,
QgsMarkerSymbol,
)
def _marker(color: str, size: str = "2.5") -> object:
return QgsMarkerSymbol.createSimple(
{"color": color, "size": size, "outline_style": "no"}
)
categories = [
QgsRendererCategory(
KATEGORIE_FGP,
_marker("0,180,0,255"),
f"{KATEGORIE_FGP} – festgestellter Grenzpunkt",
),
QgsRendererCategory(
KATEGORIE_KLEIN,
_marker("255,220,0,255"),
f"{KATEGORIE_KLEIN} – Gütestufe {GENAUIGKEIT_GRENZWERT}",
),
QgsRendererCategory(
KATEGORIE_GROSS,
_marker("220,0,0,255"),
f"{KATEGORIE_GROSS} – Gütestufe > {GENAUIGKEIT_GRENZWERT}",
),
QgsRendererCategory(
KATEGORIE_UNBEKANNT,
_marker("128,128,128,255"),
f"{KATEGORIE_UNBEKANNT} – Kategorie nicht bestimmbar",
),
]
renderer = QgsCategorizedSymbolRenderer(FELD_KATEGORIE, categories)
layer.setRenderer(renderer)
except Exception:
pass # Kein QGIS-Kontext (Tests) → Standarddarstellung behalten
def _klassifiziere(feat) -> str:
"""Bestimmt die Kategorie eines Grenzpunkts anhand seiner Attribute.
Parameters
----------
feat :
Feature aus dem Grenzpunkte(ALKIS)-Layer.
Returns
-------
str
Einer der Kategorienwerte :data:`KATEGORIE_FGP`, :data:`KATEGORIE_KLEIN`,
:data:`KATEGORIE_GROSS` oder :data:`KATEGORIE_UNBEKANNT`.
"""
# festgestellterGrenzpunkt
try:
festgestellt_raw = feat[FELD_FESTGESTELLT]
festgestellt = str(festgestellt_raw).lower().strip() == "true" if festgestellt_raw else False
except Exception:
festgestellt = False
if festgestellt:
return KATEGORIE_FGP
# Genauigkeitsstufe auslesen
try:
gen_raw = feat[FELD_GENAUIGKEIT]
genauigkeit = int(str(gen_raw).strip()) if gen_raw else None
except (ValueError, TypeError, Exception):
genauigkeit = None
if genauigkeit is None:
return KATEGORIE_UNBEKANNT
if genauigkeit == GENAUIGKEIT_GRENZWERT:
return KATEGORIE_KLEIN
if genauigkeit > GENAUIGKEIT_GRENZWERT:
return KATEGORIE_GROSS
# Genauigkeit < 2100 (besser als 3 cm, aber nicht festgestellt)
return KATEGORIE_UNBEKANNT
# ---------------------------------------------------------------------------
# Öffentliche API
# ---------------------------------------------------------------------------
def umringstatistik_grenzpunkte(
tab_widget,
grenzpunkte_layer,
vg_layer,
) -> Optional[object]:
"""Klassifiziert Grenzpunkte auf dem VG-Umring nach Lagegenauigkeit.
Ablauf:
1. VG-Geometrie aus ``vg_layer`` berechnen → Umringslinie.
2. Alle Features aus ``grenzpunkte_layer`` auf Abstand ≤ :data:`UMRING_TOLERANZ_M`
zum Umring prüfen.
3. Treffer klassifizieren (FGP / <3cm / >3cm / unbekannt).
4. Ergebnis-Memory-Layer aufbauen und Kategoriesymbologie setzen.
5. Layer ins Projekt einfügen, Zusammenfassungs-MessageBox anzeigen.
Parameters
----------
tab_widget :
Eltern-Widget für Dialoge.
grenzpunkte_layer :
Geladener ``Grenzpunkte (ALKIS)``-Layer mit Attributen ``gp_festgestellterGrenzpunkt``
und ``qualitaetsangaben_ax_dqpunktort_genauigkeitsstufe``.
vg_layer :
Verfahrensgebiet-Layer (Polygon, EPSG:25833).
Returns
-------
QgsVectorLayer | None
Ergebnis-Memory-Layer oder ``None`` bei Fehler/Abbruch.
"""
project = QgsProject.instance()
prog = create_progress_dialog(5, "Umringstatistik Grenzpunkte", "Schritt 1/5: VG-Umring berechnen …")
abgeschlossen = False
try:
prog.set_value(0)
if prog.is_canceled():
return None
# ------------------------------------------------------------------
# Schritt 1: VG-Umring berechnen
# ------------------------------------------------------------------
feats_vg = [f for f in vg_layer.getFeatures() if f.hasGeometry() and not f.geometry().isEmpty()]
if not feats_vg:
prog.close()
warning("Umringstatistik", "Verfahrensgebiet-Layer enthält keine Geometrien.", duration=6)
return None
vg_geom = QgsGeometry.unaryUnion([f.geometry() for f in feats_vg])
if vg_geom is None or vg_geom.isEmpty():
prog.close()
warning("Umringstatistik", "Verfahrensgebiet-Geometrie konnte nicht vereint werden.", duration=6)
return None
umring_geom = polygon_zu_linie(vg_geom)
if umring_geom is None or umring_geom.isEmpty():
prog.close()
warning("Umringstatistik", "Umringslinie konnte aus der VG-Geometrie nicht extrahiert werden.", duration=6)
return None
prog.set_value(1)
if prog.is_canceled():
return None
# ------------------------------------------------------------------
# Schritt 2: Grenzpunkte auf dem Umring filtern
# ------------------------------------------------------------------
prog.set_label("Schritt 2/5: Grenzpunkte auf Umring prüfen …")
n_gesamt = grenzpunkte_layer.featureCount()
treffer: list = []
for i, feat in enumerate(grenzpunkte_layer.getFeatures()):
if i % 200 == 0:
prog.set_label(
f"Schritt 2/5: Grenzpunkte prüfen … ({i}/{n_gesamt})"
)
if prog.is_canceled():
return None
if not feat.hasGeometry() or feat.geometry().isEmpty():
continue
try:
dist = feat.geometry().distance(umring_geom)
except Exception:
continue
if dist <= UMRING_TOLERANZ_M:
treffer.append(feat)
prog.set_value(2)
if prog.is_canceled():
return None
if not treffer:
prog.close()
QMessageBox.information(
tab_widget,
"Umringstatistik Grenzpunkte",
"Es wurden keine Grenzpunkte auf dem Umring des Verfahrensgebiets gefunden.\n\n"
f"Geprüft: {n_gesamt} Grenzpunkte\n"
f"Toleranz: {UMRING_TOLERANZ_M} m",
)
return None
# ------------------------------------------------------------------
# Schritt 3: Klassifizieren
# ------------------------------------------------------------------
prog.set_label(f"Schritt 3/5: {len(treffer)} Treffer klassifizieren …")
n_fgp = 0
n_klein = 0
n_gross = 0
n_unbekannt = 0
klassifiziert: list = [] # [(feat, kategorie), ...]
for feat in treffer:
kat = _klassifiziere(feat)
klassifiziert.append((feat, kat))
if kat == KATEGORIE_FGP:
n_fgp += 1
elif kat == KATEGORIE_KLEIN:
n_klein += 1
elif kat == KATEGORIE_GROSS:
n_gross += 1
else:
n_unbekannt += 1
prog.set_value(3)
if prog.is_canceled():
return None
# ------------------------------------------------------------------
# Schritt 4: Ergebnis-Layer aufbauen
# ------------------------------------------------------------------
prog.set_label("Schritt 4/5: Ergebnis-Layer aufbauen …")
crs_authid = vg_layer.crs().authid() or "EPSG:25833"
result_layer = QgsVectorLayer(
f"Point?crs={crs_authid}", ERGEBNIS_LAYER_NAME, "memory"
)
pr = result_layer.dataProvider()
felder = [
QgsField(FELD_KATEGORIE, QVariant.String),
QgsField(FELD_GML_ID, QVariant.String),
QgsField(FELD_FESTGESTELLT, QVariant.String),
QgsField(FELD_GENAUIGKEIT, QVariant.String),
]
pr.addAttributes(felder)
result_layer.updateFields()
neue_features = []
for feat, kat in klassifiziert:
new_f = QgsFeature(result_layer.fields())
new_f.setGeometry(feat.geometry())
new_f[FELD_KATEGORIE] = kat
for src_feld in (FELD_GML_ID, FELD_FESTGESTELLT, FELD_GENAUIGKEIT):
try:
new_f[src_feld] = feat[src_feld]
except Exception:
pass
neue_features.append(new_f)
pr.addFeatures(neue_features)
result_layer.updateExtents()
_setze_kategorien_symbologie(result_layer)
prog.set_value(4)
if prog.is_canceled():
return None
# ------------------------------------------------------------------
# Schritt 5: Layer ins Projekt + Ergebnis-MessageBox
# ------------------------------------------------------------------
prog.set_label("Schritt 5/5: Layer ins Projekt einfügen …")
# Bestehenden Ergebnis-Layer ggf. ersetzen
vorhandene = project.mapLayersByName(ERGEBNIS_LAYER_NAME)
for lyr in vorhandene:
project.removeMapLayer(lyr.id())
project.addMapLayer(result_layer)
prog.set_value(5)
abgeschlossen = True
prog.close()
n_umring = len(treffer)
QMessageBox.information(
tab_widget,
"Umringstatistik Grenzpunkte",
f"Grenzpunkte auf dem Umring: {n_umring}\n"
f"\n"
f" FGP (festgestellt): {n_fgp}\n"
f" < 3 cm (Gütestufe {GENAUIGKEIT_GRENZWERT}): {n_klein}\n"
f" > 3 cm (schlechtere Güte): {n_gross}\n"
f" Unbekannt: {n_unbekannt}\n"
f"\n"
f"(Geprüft: {n_gesamt} Grenzpunkte, Toleranz: {UMRING_TOLERANZ_M} m)",
)
return result_layer
finally:
if not abgeschlossen:
prog.close()
+123
View File
@@ -0,0 +1,123 @@
"""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_sf_url = "https://geodienste.sachsen.de/aaa/public_alkis/sf/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, List[QgsFeature]]:
Geladener Memory-Layer, Lade-Status und Liste der Roh-WFS-Features
(für geometriebasierte Weiterverarbeitung, z.B. Gemarkungsschlüssel-
Ermittlung via Spatial-Join). Bei Abbruch oder Fehler: ``(None,
LoadStatus.NONE, [])``. Bei ``LoadStatus.KEEP`` werden keine
WFS-Features neu geladen; die Feature-Liste ist in diesem Fall leer.
"""
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_sf_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; features für Gemarkungsermittlung weitergeben
return memory_layer, status, features
@@ -0,0 +1,534 @@
"""sn_verfahrensgebiet/functions/verfahrensgebiet_alkis_komplett.py – Vollständiger VG-Lade-Workflow.
Orchestriert den vollständigen Arbeitsablauf zum Laden eines Verfahrensgebiets
aus ALKIS mit anschließender Flurstücks-Verschneidung, Flächenvergleich und
optionaler Umringsprüfung:
1. Verfahrensgebiet aus ``AX_BauRaumOderBodenordnungsrecht`` per VKZ laden.
2. Gemarkungsschlüssel per Geometrie aus ``AX_Gemarkung`` vorschlagen.
3. Flurstücke per Dialog (mit Vorschlag) laden.
4. Flurstücke räumlich auf das VG einschränken (``selectByLocation``).
5. Aus der Auswahl den enriched Verfahrensgebiet-Memory-Layer erzeugen.
6. Flächenvergleich zwischen BROR-Objekt und abgeleitetem Verfahrensgebiet.
- **Übereinstimmung** (≤ Toleranz): modale OK-Meldung, Ablauf beendet.
- **Abweichung**: 3-Optionen-Dialog (Detailprüfung / Als VG laden / Abbrechen).
7. Bei Wahl „Als VG laden":
- BROR-Objekt direkt mit VG-Namen ins Projekt übernehmen.
8. Bei Wahl „Detailprüfung" (knickpunktbasiert):
- Grenzpunkte (AX_PunktortTA) per BBox laden.
- Knickpunkte des BROR mit PunktortTA vergleichen (grün/rot/orange).
- Ergebnislayer (Punkte) ins Projekt einfügen.
- Abschlussmeldung mit Zählung.
Das BROR-Objekt (``AX_BauRaumOderBodenordnungsrecht``) bleibt bei Erfolg als
temporärer Layer ``BauRaumOrdnungsrecht (ALKIS)`` im Projekt erhalten.
**Rückgabe:**
``Tuple[enriched_layer, flst_layer, bror_layer, LoadStatus]``
**Abbruchverhalten:** Jeder Schritt, der :attr:`LoadStatus.NONE` zurückgibt
oder kein Ergebnis liefert, bricht den Workflow ab. Der BROR-Layer wird nur
bei Fehler/Abbruch entfernt (wenn er in diesem Lauf hinzugefügt wurde).
"""
from __future__ import annotations
from typing import Optional, Tuple
from sn_basis.functions import QgsProject
from sn_basis.functions import QMessageBox
from sn_basis.functions.message_wrapper import warning
from sn_basis.functions.variable_wrapper import get_variable
from sn_basis.functions.dialog_wrapper import (
create_progress_dialog,
show_info_dialog,
ask_detailpruefung_oder_vg_laden,
)
from sn_verfahrensgebiet.functions.verfahrensgebiet_alkis import (
verfahrensgebiet_alkis,
LoadStatus,
)
from sn_verfahrensgebiet.functions.gemarkung_alkis import (
gemarkung_schluessel_und_namen_von_vg_geometrie,
)
from sn_verfahrensgebiet.functions.flurstueck_alkis import flurstueck_alkis
from sn_verfahrensgebiet.functions.verfahrensgebiet_edit_service import (
erstelle_verfahrensgebiet_aus_auswahl,
VERF_LAYER_NAME,
)
from sn_verfahrensgebiet.functions.grenzpunkt_alkis import lade_punktort_ta
from sn_verfahrensgebiet.functions.knickpunkt_pruefung import (
knickpunkt_pruefung,
_extrahiere_knickpunkte,
)
# Feldname für das Verfahrenskennzeichen im enriched Layer
FELD_VKZ = "verfahrensnummer"
#: Anzeigename des BROR-Layers im Projekt
BROR_LAYER_NAME = "BauRaumOrdnungsrecht (ALKIS)"
#: Projektvariable für die Flächentoleranz (in m²); Default 0
FLAECHEN_TOLERANZ_VARIABLE = "flaechen_toleranz_m2"
# ---------------------------------------------------------------------------
# Interne Hilfsfunktionen
# ---------------------------------------------------------------------------
def _entferne_hilfs_layer(project, layer_name: str) -> None:
"""Entfernt alle Layer mit dem angegebenen Namen aus dem Projekt.
Wird verwendet, um temporäre WFS-Hilfs-Layer nach der Verschneidung
aus dem Projekt zu bereinigen.
Parameters
----------
project :
Aktuelles QGIS-Projekt.
layer_name :
Anzeigename des zu entfernenden Layers.
"""
for lyr in project.mapLayersByName(layer_name):
project.removeMapLayer(lyr.id())
def _select_by_location(flst_layer, vg_layer) -> int:
"""Selektiert Flurstücke, die innerhalb des Verfahrensgebiets liegen.
Verwendet ``processing.run("native:selectbylocation")`` mit Prädikat 6
(within). Flurstücke, die das VG nur berühren, werden ausgeschlossen.
Parameters
----------
flst_layer :
Flurstücks-Layer (Quelle der Selektion).
vg_layer :
Verfahrensgebiet-Layer (Schnittmaske).
Returns
-------
int
Anzahl der selektierten Features; 0 bei Fehler oder leerem Ergebnis.
"""
try:
import processing # type: ignore[import]
processing.run(
"native:selectbylocation",
{
"INPUT": flst_layer,
"PREDICATE": [6], # 6 = within; schließt reines Berühren aus
"INTERSECT": vg_layer,
"METHOD": 0, # 0 = neue Selektion
},
)
return flst_layer.selectedFeatureCount()
except Exception:
return 0
def _fuege_vkz_feld_hinzu(layer, vkz: str) -> None:
"""Fügt das Feld ``verfahrensnummer`` zum enriched Layer hinzu und befüllt
es mit dem Verfahrenskennzeichen, sofern dieses nicht leer ist.
Parameters
----------
layer :
Enriched Verfahrensgebiet-Memory-Layer (in-place geändert).
vkz :
Verfahrenskennzeichen aus der Projektvariable ``verfahrensnummer``.
"""
if not vkz:
return
try:
from sn_basis.functions import QgsField
from sn_basis.functions.qt_wrapper import QVariant
pr = layer.dataProvider()
pr.addAttributes([QgsField(FELD_VKZ, QVariant.String)])
layer.updateFields()
idx = layer.fields().indexFromName(FELD_VKZ)
if idx < 0:
return
changes = {}
for feat in layer.getFeatures():
changes[feat.id()] = {idx: vkz}
pr.changeAttributeValues(changes)
layer.updateFields()
except Exception:
pass
def _lese_flaechen_toleranz() -> float:
"""Liest die Flächentoleranz aus der Projektvariable (Default: 0.0 m²).
Die Projektvariable :data:`FLAECHEN_TOLERANZ_VARIABLE` kann in den
Projekteigenschaften gesetzt werden. Fehlt sie oder ist sie ungültig,
wird 0.0 zurückgegeben (exakter Vergleich).
Returns
-------
float
Toleranz in m² (bei EPSG:25833 identisch mit den Flächen-Einheiten).
"""
try:
raw = get_variable(FLAECHEN_TOLERANZ_VARIABLE)
if raw is not None and str(raw).strip():
return float(str(raw).strip())
except Exception:
pass
return 0.0
def _flaeche_des_layers(layer) -> float:
"""Summiert die Flächen aller Features eines Layers (in CRS-Einheiten²).
Parameters
----------
layer :
Beliebiger Polygon-Layer.
Returns
-------
float
Summe aller Feature-Flächen; 0.0 wenn keine Features vorhanden.
"""
total = 0.0
for feat in layer.getFeatures():
try:
geom = feat.geometry()
if geom and not geom.isEmpty():
total += geom.area()
except Exception:
pass
return total
def _vergleiche_flaechen(
bror_layer, vg_layer, toleranz_m2: float = 0.0
) -> Tuple[float, float, bool]:
"""Vergleicht Flächen von BROR- und Verfahrensgebiet-Layer.
Parameters
----------
bror_layer :
BauRaumOrdnungsrecht-Layer.
vg_layer :
Enriched Verfahrensgebiet-Layer.
toleranz_m2 :
Maximale zulässige Flächendifferenz in m².
Returns
-------
Tuple[float, float, bool]
``(bror_flaeche, vg_flaeche, stimmt_ueberein)``
"""
bror_flaeche = _flaeche_des_layers(bror_layer)
vg_flaeche = _flaeche_des_layers(vg_layer)
stimmt_ueberein = abs(bror_flaeche - vg_flaeche) <= toleranz_m2
return bror_flaeche, vg_flaeche, stimmt_ueberein
# ---------------------------------------------------------------------------
# Öffentliche API
# ---------------------------------------------------------------------------
def verfahrensgebiet_alkis_komplett(
tab_widget,
) -> Tuple[Optional[object], Optional[object], Optional[object], LoadStatus]:
"""Vollständiger VG-Lade-Workflow: ALKIS-VG → Gemarkungen →
Flurstücke → Verschneidung → Flächenvergleich → opt. Umringsprüfung.
Schrittfolge:
1. ``verfahrensgebiet_alkis(tab_widget)`` – BROR-Objekt als Memory-Layer.
2. ``gemarkung_schluessel_und_namen_von_vg_geometrie(geom)`` – Schlüssel.
3. ``flurstueck_alkis(...)`` – Flurstücke laden.
4. ``_select_by_location(...)`` – räumliche Selektion.
5. ``erstelle_verfahrensgebiet_aus_auswahl(...)`` – enriched Layer.
6. ``_vergleiche_flaechen(...)`` – Flächenvergleich als Gate.
7. Ggf. ``umringspruefung(...)`` nach Nutzerbestätigung.
Der BROR-Layer (``BauRaumOrdnungsrecht (ALKIS)``) bleibt bei Erfolg
im Projekt. Bei Abbruch/Fehler wird er entfernt, falls er in diesem
Lauf hinzugefügt wurde.
Parameters
----------
tab_widget :
Eltern-Widget für alle Dialoge.
Returns
-------
Tuple[Optional[enriched_layer], Optional[flst_layer], Optional[bror_layer], LoadStatus]
- ``enriched_layer``: Memory-Layer „Verfahrensgebiet".
- ``flst_layer``: Flurstücke-Layer (bleibt im Projekt).
- ``bror_layer``: BROR-Layer ``BauRaumOrdnungsrecht (ALKIS)``
(bleibt bei Erfolg im Projekt).
- :class:`LoadStatus`: ``FIRST``, ``RELOAD`` oder ``NONE``.
"""
project = QgsProject.instance()
alkis_vg_layer = None
bror_was_added_by_us = False
abgeschlossen = False
# Fortschrittsdialog für den Basisworkflow (7 Schritte)
progress = create_progress_dialog(
7, "Verfahrensgebiet laden", "Initialisierung …"
)
try:
# ------------------------------------------------------------------
# Schritt 1: VG aus ALKIS laden (BROR-Objekt → Memory-Layer)
# ------------------------------------------------------------------
progress.set_label("Schritt 1/6: Verfahrensgebiet aus ALKIS laden …")
progress.set_value(0)
if progress.is_canceled():
return None, None, None, LoadStatus.NONE
alkis_vg_layer, vg_status, _ = verfahrensgebiet_alkis(tab_widget)
if vg_status == LoadStatus.NONE or not alkis_vg_layer:
return None, None, None, LoadStatus.NONE
# BROR-Layer umbenennen, um Namenskonflikt mit enriched VG-Layer zu vermeiden
alkis_vg_layer.setName(BROR_LAYER_NAME)
if vg_status in (LoadStatus.FIRST, LoadStatus.RELOAD):
project.addMapLayer(alkis_vg_layer)
bror_was_added_by_us = True
progress.set_value(1)
# ------------------------------------------------------------------
# Schritt 2: Gemarkungsschlüssel per Geometrie-Spatial-Join ermitteln
# ------------------------------------------------------------------
progress.set_label("Schritt 2/6: Gemarkungsschlüssel ermitteln …")
if progress.is_canceled():
return None, None, None, LoadStatus.NONE
bror_feats = list(alkis_vg_layer.getFeatures())
vg_geom = bror_feats[0].geometry() if bror_feats else None
schluessel_namen = (
gemarkung_schluessel_und_namen_von_vg_geometrie(vg_geom)
if vg_geom else []
)
vorgabe_schluessel = [nr for nr, _ in schluessel_namen]
vorgabe_namen = [nm for _, nm in schluessel_namen if nm]
if not vorgabe_schluessel:
warning(
"VG laden",
"Keine Gemarkungen in der BBox gefunden. Bitte Schlüssel manuell eingeben.",
duration=5,
)
progress.set_value(2)
# ------------------------------------------------------------------
# Schritt 3: Flurstücke laden (mit Nutzer-Dialog, Vorschlag befüllt)
# ------------------------------------------------------------------
progress.set_label("Schritt 3/6: Flurstücke aus ALKIS laden …")
if progress.is_canceled():
return None, None, None, LoadStatus.NONE
flst_layer, flst_status = flurstueck_alkis(
tab_widget,
vorgabe_schluessel=vorgabe_schluessel,
vorgabe_namen=vorgabe_namen if vorgabe_namen else None,
ohne_dialog=True,
)
if flst_status == LoadStatus.NONE or not flst_layer:
return None, None, None, LoadStatus.NONE
progress.set_value(3)
# ------------------------------------------------------------------
# Schritt 4: Räumliche Selektion
# ------------------------------------------------------------------
progress.set_label("Schritt 4/6: Flurstücke räumlich selektieren …")
if progress.is_canceled():
return None, None, None, LoadStatus.NONE
anzahl = _select_by_location(flst_layer, alkis_vg_layer)
if anzahl == 0:
QMessageBox.warning(
tab_widget,
"Keine Flurstücke im VG",
"Es wurden keine Flurstücke gefunden, die das Verfahrensgebiet schneiden.\n"
"Bitte Gemarkungsschlüssel und Ausdehnung prüfen.",
)
return None, flst_layer, None, LoadStatus.NONE
progress.set_value(4)
# ------------------------------------------------------------------
# Schritt 5: Enriched Memory-Layer aus Auswahl erzeugen
# ------------------------------------------------------------------
progress.set_label("Schritt 5/6: Verfahrensgebiet-Layer erzeugen …")
if progress.is_canceled():
return None, None, None, LoadStatus.NONE
enriched_layer, erfolgreich, fehlgeschlagen = erstelle_verfahrensgebiet_aus_auswahl(
flst_layer
)
if not enriched_layer or not enriched_layer.isValid():
QMessageBox.critical(
tab_widget,
"Fehler",
"Verfahrensgebiet-Layer konnte nicht erstellt werden.",
)
return None, flst_layer, None, LoadStatus.NONE
vkz = get_variable("verfahrensnummer") or ""
_fuege_vkz_feld_hinzu(enriched_layer, vkz)
progress.set_value(5)
# ------------------------------------------------------------------
# Schritt 6: Flächenvergleich als Gate
# ------------------------------------------------------------------
progress.set_label("Schritt 6/6: Flächenvergleich …")
if progress.is_canceled():
return None, None, None, LoadStatus.NONE
toleranz = _lese_flaechen_toleranz()
bror_flaeche, vg_flaeche, stimmt_ueberein = _vergleiche_flaechen(
alkis_vg_layer, enriched_layer, toleranz
)
progress.set_value(6)
if stimmt_ueberein:
progress.set_value(7)
progress.close()
show_info_dialog(
"Flächenvergleich",
"Verfahrensgebiet mit erwarteter Geometrie erzeugt",
parent=tab_widget,
)
abgeschlossen = True
return enriched_layer, flst_layer, alkis_vg_layer, vg_status
# ------------------------------------------------------------------
# Flächenabweichung → 3-Optionen-Dialog
# ------------------------------------------------------------------
progress.close()
diff = abs(bror_flaeche - vg_flaeche)
entscheidung = ask_detailpruefung_oder_vg_laden(
tab_widget,
"Abweichung erkannt",
f"Abweichung zwischen BauRaumOrdnungsrecht-Objekt und abgeleitetem "
f"Verfahrensgebiet erkannt.\n\n"
f"BauRaumOrdnungsrecht: {bror_flaeche:.2f} m²\n"
f"Verfahrensgebiet: {vg_flaeche:.2f} m²\n"
f"Differenz: {diff:.2f} m²\n\n"
f"Wie möchten Sie fortfahren?",
)
if entscheidung == "abbrechen":
abgeschlossen = True
return enriched_layer, flst_layer, alkis_vg_layer, vg_status
# ------------------------------------------------------------------
# Option: Bodenordnungsobjekt direkt als Verfahrensgebiet laden
# ------------------------------------------------------------------
if entscheidung == "als_vg_laden":
# Layer bleibt im Projekt – nur umbenennen. working_tab überspringt
# addMapLayer wenn der Layer bereits registriert ist.
alkis_vg_layer.setName(VERF_LAYER_NAME)
bror_was_added_by_us = False
abgeschlossen = True
return alkis_vg_layer, None, None, vg_status
# ------------------------------------------------------------------
# Option: Detailprüfung (knickpunktbasiert)
# ------------------------------------------------------------------
# Knickpunkte vorab extrahieren, um Schrittanzahl zu bestimmen
knickpunkte = _extrahiere_knickpunkte(alkis_vg_layer)
n_knick = len(knickpunkte)
# Gesamtschritte: 0=Start, 1=PTA-Index, 2…n+1=Knickpunkte, n+2=Orange, n+3=Fertig
# + 1 Schritt vorab für Grenzpunkte laden
gesamt_schritte = n_knick + 5
detail_progress = create_progress_dialog(
gesamt_schritte,
"Detailprüfung (Knickpunkte)",
"Schritt 1: Grenzpunkte laden …",
)
detail_progress.set_value(0)
# Schritt 1: PunktortTA gemarkungsweise laden (Hits + 5000er-Batches)
punktort_layer = lade_punktort_ta(
vorgabe_schluessel, vg_geom, progress=detail_progress
)
if detail_progress.is_canceled():
detail_progress.close()
abgeschlossen = True
return enriched_layer, flst_layer, alkis_vg_layer, vg_status
detail_progress.set_value(1)
if punktort_layer is None:
detail_progress.close()
abgeschlossen = True
return enriched_layer, flst_layer, alkis_vg_layer, vg_status
# Schritte 2…Ende: Knickpunktprüfung
ergebnis_layer, identisch, kein_pta, fehlender_knick = knickpunkt_pruefung(
alkis_vg_layer,
punktort_layer,
flst_layer,
progress=detail_progress,
knickpunkte=knickpunkte,
)
if detail_progress.is_canceled():
detail_progress.close()
abgeschlossen = True
return enriched_layer, flst_layer, alkis_vg_layer, vg_status
detail_progress.set_value(gesamt_schritte)
detail_progress.close()
if ergebnis_layer and ergebnis_layer.isValid():
project.addMapLayer(ergebnis_layer)
show_info_dialog(
"Detailprüfung abgeschlossen",
f"Knickpunkte geprüft: {n_knick}\n"
f" Identisch (grün): {identisch}\n"
f" Kein PunktortTA (rot): {kein_pta}\n"
f" Fehlende Knickpunkte (orange): {fehlender_knick}",
parent=tab_widget,
)
abgeschlossen = True
return enriched_layer, flst_layer, alkis_vg_layer, vg_status
finally:
# WFS-Temp-Layer stets aus dem Projekt entfernen
_entferne_hilfs_layer(project, "TempVerfahrensgebiet")
# BROR-Layer bei Fehler/Abbruch entfernen (nur wenn in diesem Lauf hinzugefügt)
if not abgeschlossen and bror_was_added_by_us and alkis_vg_layer is not None:
try:
project.removeMapLayer(alkis_vg_layer.id())
except Exception:
pass
+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.6-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/
QGISMaximumVersion=4.99
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()
+312
View File
@@ -0,0 +1,312 @@
"""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
# ------------------------------------------------------------------ #
# 6. vorgabe_schluessel füllt ask_text-Default vor #
# ------------------------------------------------------------------ #
@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_vorgabe_schluessel_prefills_default_text(
self, mock_ask_text, mock_project, mock_wfs_layer_cls
):
"""vorgabe_schluessel=['1234','5678'] → ask_text erhält '1234, 5678' als default_text."""
mock_ask_text.return_value = ("1234, 5678", True)
mock_project.return_value = _DummyProject()
mock_wfs_layer_cls.return_value = _DummyLayer(valid=True)
flurstueck_alkis(None, vorgabe_schluessel=["1234", "5678"])
args, kwargs = mock_ask_text.call_args
# ask_text("title", label, default_text=..., parent=...)
# default_text wird als Keyword-Arg übergeben
default_text_arg = kwargs.get("default_text", args[2] if len(args) > 2 else "")
self.assertIn("1234", default_text_arg)
self.assertIn("5678", default_text_arg)
# ------------------------------------------------------------------ #
# 7. vorgabe_namen erscheint im Label-Text #
# ------------------------------------------------------------------ #
@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_vorgabe_namen_appears_in_label(
self, mock_ask_text, mock_project, mock_wfs_layer_cls
):
"""vorgabe_namen=['Musterstadt'] → ask_text-label enthält den Namen."""
mock_ask_text.return_value = ("9999", True)
mock_project.return_value = _DummyProject()
mock_wfs_layer_cls.return_value = _DummyLayer(valid=True)
flurstueck_alkis(None, vorgabe_namen=["Musterstadt"])
args, kwargs = mock_ask_text.call_args
# label ist das 2. Positional-Argument
label_arg = args[1] if len(args) > 1 else kwargs.get("label", "")
self.assertIn("Musterstadt", label_arg)
# ------------------------------------------------------------------ #
# 8. bbox wird in WFS-URI eingebettet #
# ------------------------------------------------------------------ #
@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_bbox_embedded_in_wfs_uri(
self, mock_ask_text, mock_project, mock_wfs_layer_cls
):
"""bbox-Parameter → WFS-URI enthält 'bbox=' Substring."""
mock_ask_text.return_value = ("1234", True)
mock_project.return_value = _DummyProject()
mock_wfs_layer_cls.return_value = _DummyLayer(valid=True)
class _Rect:
def xMinimum(self): return 414000.0
def yMinimum(self): return 5657000.0
def xMaximum(self): return 415000.0
def yMaximum(self): return 5658000.0
flurstueck_alkis(None, bbox=_Rect())
uri_str = mock_wfs_layer_cls.call_args[0][0]
self.assertIn("bbox=", uri_str.lower())
# ------------------------------------------------------------------ #
# 9. Rückwärtskompatibilität: alle neuen Params optional #
# ------------------------------------------------------------------ #
@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_backwards_compatible_no_new_params(
self, mock_ask_text, mock_project, mock_wfs_layer_cls
):
"""flurstueck_alkis(tab) ohne neue Parameter → kein Fehler."""
mock_ask_text.return_value = ("0000", True)
mock_project.return_value = _DummyProject()
mock_wfs_layer_cls.return_value = _DummyLayer(valid=True)
layer, status = flurstueck_alkis(None)
self.assertIsNotNone(layer)
self.assertEqual(status, LoadStatus.FIRST)
if __name__ == "__main__":
unittest.main()
+207
View File
@@ -0,0 +1,207 @@
"""sn_verfahrensgebiet/tests/test_gemarkung_alkis.py
Unit-Tests für :mod:`sn_verfahrensgebiet.functions.gemarkung_alkis`.
Getestete Szenarien:
Erfolgsfall:
- WFS-Layer gültig + Features mit Schlüssel → Liste mit Tupeln
- Schlüssel werden dedupliziert und sortiert zurückgegeben
- ``gemarkung_schluessel_by_bbox`` gibt nur Nummern (keine Tupel) zurück
Fehlerfälle:
- WFS-Layer nicht gültig (beide Varianten) → Fallback-Ladeversuch
- Fallback-Layer ebenfalls ungültig → leere Liste
- Alle Layer leer (featureCount == 0) → client-seitige Filterung schlägt leer
- Feature ohne Schlüssel-Attribut wird übersprungen
- Exception beim Layer-Laden → leere Liste
"""
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.gemarkung_alkis import (
gemarkung_schluessel_und_namen_by_bbox,
gemarkung_schluessel_by_bbox,
_extract_schluessel_und_name,
)
# ---------------------------------------------------------------------------
# Dummies
# ---------------------------------------------------------------------------
class _DummyRect:
def xMinimum(self): return 414000.0
def yMinimum(self): return 5657000.0
def xMaximum(self): return 415000.0
def yMaximum(self): return 5658000.0
class _DummyFeature:
def __init__(self, attrs: dict):
self._attrs = attrs
def __getitem__(self, key):
return self._attrs.get(key, None)
def _make_layer(valid=True, feature_count=2, features=None):
layer = MagicMock()
layer.isValid.return_value = valid
layer.featureCount.return_value = feature_count if valid else 0
layer.getFeatures.return_value = features or []
return layer
# ---------------------------------------------------------------------------
# Tests: _extract_schluessel_und_name
# ---------------------------------------------------------------------------
class TestExtractSchluesselUndName(unittest.TestCase):
def test_standard_attribute(self):
feat = _DummyFeature({"schluessel_gemarkungsnummer": "1234", "bezeichnung": "Mustergemarkung"})
nr, name = _extract_schluessel_und_name(feat)
self.assertEqual(nr, "1234")
self.assertEqual(name, "Mustergemarkung")
def test_fallback_attribute(self):
feat = _DummyFeature({"gemarkungsnummer": "5678", "gemarkung_bezeichnung": "Alternativ"})
nr, name = _extract_schluessel_und_name(feat)
self.assertEqual(nr, "5678")
self.assertEqual(name, "Alternativ")
def test_missing_attribute_returns_empty(self):
feat = _DummyFeature({})
nr, name = _extract_schluessel_und_name(feat)
self.assertEqual(nr, "")
self.assertEqual(name, "")
def test_none_value_returns_empty(self):
feat = _DummyFeature({"schluessel_gemarkungsnummer": None, "bezeichnung": None})
nr, name = _extract_schluessel_und_name(feat)
self.assertEqual(nr, "")
self.assertEqual(name, "")
# ---------------------------------------------------------------------------
# Tests: gemarkung_schluessel_und_namen_by_bbox
# ---------------------------------------------------------------------------
class TestGemarkungSchluesselbbox(unittest.TestCase):
@patch("sn_verfahrensgebiet.functions.gemarkung_alkis._load_gemarkung_layer_with_bbox")
def test_success_bbox_filter(self, mock_load_bbox):
"""WFS-BBox-Filter liefert Features → korrekte Tupel-Liste."""
feat1 = _DummyFeature({"schluessel_gemarkungsnummer": "1234", "bezeichnung": "Alpha"})
feat2 = _DummyFeature({"schluessel_gemarkungsnummer": "5678", "bezeichnung": "Beta"})
mock_load_bbox.return_value = _make_layer(valid=True, features=[feat1, feat2])
result = gemarkung_schluessel_und_namen_by_bbox(_DummyRect())
self.assertEqual(result, [("1234", "Alpha"), ("5678", "Beta")])
@patch("sn_verfahrensgebiet.functions.gemarkung_alkis._load_all_gemarkung_layer")
@patch("sn_verfahrensgebiet.functions.gemarkung_alkis._load_gemarkung_layer_with_bbox")
def test_fallback_when_bbox_fails(self, mock_load_bbox, mock_load_all):
"""BBox-Filter schlägt fehl → Fallback auf Volllade + client-seitige Filterung."""
mock_load_bbox.return_value = None
feat1 = _DummyFeature({"schluessel_gemarkungsnummer": "9999", "bezeichnung": "Fallback"})
all_layer = _make_layer(valid=True, features=[feat1])
# getFeatures mit FilterRect soll dasselbe zurückgeben
all_layer.getFeatures.return_value = [feat1]
mock_load_all.return_value = all_layer
result = gemarkung_schluessel_und_namen_by_bbox(_DummyRect())
self.assertEqual(len(result), 1)
self.assertEqual(result[0][0], "9999")
@patch("sn_verfahrensgebiet.functions.gemarkung_alkis._load_all_gemarkung_layer")
@patch("sn_verfahrensgebiet.functions.gemarkung_alkis._load_gemarkung_layer_with_bbox")
def test_both_layers_fail_returns_empty(self, mock_load_bbox, mock_load_all):
"""Beide Lade-Strategien scheitern → leere Liste."""
mock_load_bbox.return_value = None
mock_load_all.return_value = None
result = gemarkung_schluessel_und_namen_by_bbox(_DummyRect())
self.assertEqual(result, [])
@patch("sn_verfahrensgebiet.functions.gemarkung_alkis._load_gemarkung_layer_with_bbox")
def test_deduplication_and_sorting(self, mock_load_bbox):
"""Doppelte Schlüssel werden dedupliziert, Ergebnis ist sortiert."""
feat_a = _DummyFeature({"schluessel_gemarkungsnummer": "2222", "bezeichnung": "Z"})
feat_b = _DummyFeature({"schluessel_gemarkungsnummer": "1111", "bezeichnung": "A"})
feat_dup = _DummyFeature({"schluessel_gemarkungsnummer": "2222", "bezeichnung": "Z2"})
mock_load_bbox.return_value = _make_layer(features=[feat_a, feat_b, feat_dup])
result = gemarkung_schluessel_und_namen_by_bbox(_DummyRect())
nummern = [r[0] for r in result]
self.assertEqual(nummern, sorted(nummern))
self.assertEqual(len(result), 2)
@patch("sn_verfahrensgebiet.functions.gemarkung_alkis._load_gemarkung_layer_with_bbox")
def test_feature_without_schluessel_skipped(self, mock_load_bbox):
"""Feature ohne Schlüssel-Attribut wird übersprungen."""
feat_ok = _DummyFeature({"schluessel_gemarkungsnummer": "1234", "bezeichnung": "X"})
feat_bad = _DummyFeature({})
mock_load_bbox.return_value = _make_layer(features=[feat_ok, feat_bad])
result = gemarkung_schluessel_und_namen_by_bbox(_DummyRect())
self.assertEqual(len(result), 1)
self.assertEqual(result[0][0], "1234")
@patch("sn_verfahrensgebiet.functions.gemarkung_alkis._load_gemarkung_layer_with_bbox")
def test_exception_returns_empty(self, mock_load_bbox):
"""Exception im Ladevorgang → leere Liste (kein Crash)."""
mock_load_bbox.side_effect = RuntimeError("WFS-Fehler")
result = gemarkung_schluessel_und_namen_by_bbox(_DummyRect())
self.assertEqual(result, [])
# ---------------------------------------------------------------------------
# Tests: gemarkung_schluessel_by_bbox (Convenience-Wrapper)
# ---------------------------------------------------------------------------
class TestGemarkungSchluesselnurNummern(unittest.TestCase):
@patch("sn_verfahrensgebiet.functions.gemarkung_alkis.gemarkung_schluessel_und_namen_by_bbox")
def test_returns_only_nummern(self, mock_full):
mock_full.return_value = [("1234", "Alpha"), ("5678", "Beta")]
result = gemarkung_schluessel_by_bbox(_DummyRect())
self.assertEqual(result, ["1234", "5678"])
@patch("sn_verfahrensgebiet.functions.gemarkung_alkis.gemarkung_schluessel_und_namen_by_bbox")
def test_empty_input_returns_empty(self, mock_full):
mock_full.return_value = []
result = gemarkung_schluessel_by_bbox(_DummyRect())
self.assertEqual(result, [])
if __name__ == "__main__":
unittest.main()
+301
View File
@@ -0,0 +1,301 @@
"""sn_verfahrensgebiet/tests/test_grenzpunkt_alkis.py
Unit-Tests für :mod:`sn_verfahrensgebiet.functions.grenzpunkt_alkis`.
Getestete Szenarien:
Erfolgsfall:
- Vollständiger Workflow: PunktortTA laden → GML-IDs extrahieren →
Grenzpunkte laden → beide Layer zurückgegeben
Fehlerfälle:
- PunktortTA-Layer ungültig → (None, None)
- PunktortTA leer (featureCount == 0) → Warnung + (None, None)
- Keine gehoertZu-Referenzen → (punktort_layer, None) + Warnung
- Grenzpunkt-Layer nach IDs leer → (punktort_layer, None) + Warnung
- Kein Verfahrensgebiet-Layer für BBox-Ableitung → (None, None)
Hilfsfunktionen:
- _extrahiere_gml_ids: xlink:href-Format wird korrekt geparst
- _extrahiere_gml_ids: plain GML-ID ohne Doppelpunkt wird direkt verwendet
- _extrahiere_gml_ids: leeres Feld wird übersprungen
"""
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.grenzpunkt_alkis import (
grenzpunkt_alkis,
_extrahiere_gml_ids,
)
# ---------------------------------------------------------------------------
# Dummies
# ---------------------------------------------------------------------------
class _DummyRect:
def xMinimum(self): return 414000.0
def yMinimum(self): return 5657000.0
def xMaximum(self): return 415000.0
def yMaximum(self): return 5658000.0
class _DummyFeature:
def __init__(self, gehoert_zu_val=None):
self._val = gehoert_zu_val
def __getitem__(self, key):
if key == "gehoertZu":
return self._val
raise KeyError(key)
def _make_layer(valid=True, feature_count=3, features=None, name="Layer"):
layer = MagicMock()
layer.isValid.return_value = valid
layer.featureCount.return_value = feature_count if valid else 0
layer.getFeatures.return_value = features or []
layer.name.return_value = name
layer.id.return_value = "LYR1"
return layer
# ---------------------------------------------------------------------------
# Tests: _extrahiere_gml_ids
# ---------------------------------------------------------------------------
class TestExtrahiereGmlIds(unittest.TestCase):
def test_xlink_href_format(self):
"""URN-Format 'urn:adv:oid:DEABC123' → nur 'DEABC123' extrahiert."""
feat = _DummyFeature("urn:adv:oid:DEABC123")
layer = _make_layer(features=[feat])
result = _extrahiere_gml_ids(layer)
self.assertEqual(result, ["DEABC123"])
def test_plain_gml_id(self):
"""Plain-GML-ID ohne Doppelpunkt → direkt übernommen."""
feat = _DummyFeature("DEPLAIN456")
layer = _make_layer(features=[feat])
result = _extrahiere_gml_ids(layer)
self.assertEqual(result, ["DEPLAIN456"])
def test_empty_value_skipped(self):
"""Leerer gehoertZu-Wert wird übersprungen."""
feat = _DummyFeature(None)
layer = _make_layer(features=[feat])
result = _extrahiere_gml_ids(layer)
self.assertEqual(result, [])
def test_deduplication(self):
"""Doppelte Referenzen werden dedupliziert."""
f1 = _DummyFeature("urn:adv:oid:DEDUP001")
f2 = _DummyFeature("urn:adv:oid:DEDUP001")
f3 = _DummyFeature("urn:adv:oid:DEOTHER")
layer = _make_layer(features=[f1, f2, f3])
result = _extrahiere_gml_ids(layer)
self.assertEqual(len(result), 2)
self.assertIn("DEDUP001", result)
self.assertIn("DEOTHER", result)
def test_sorted_output(self):
"""Rückgabe ist aufsteigend sortiert."""
f1 = _DummyFeature("urn:adv:oid:ZZZ")
f2 = _DummyFeature("urn:adv:oid:AAA")
layer = _make_layer(features=[f1, f2])
result = _extrahiere_gml_ids(layer)
self.assertEqual(result, sorted(result))
# ---------------------------------------------------------------------------
# Tests: grenzpunkt_alkis
# ---------------------------------------------------------------------------
_PATCH_PROJECT = "sn_verfahrensgebiet.functions.grenzpunkt_alkis.QgsProject"
_PATCH_MSGBOX = "sn_verfahrensgebiet.functions.grenzpunkt_alkis.QMessageBox"
_PATCH_WFS = "sn_verfahrensgebiet.functions.grenzpunkt_alkis.QgsVectorLayer"
_PATCH_EXTRAHIERE = "sn_verfahrensgebiet.functions.grenzpunkt_alkis._extrahiere_gml_ids"
_PATCH_LADE_GP = "sn_verfahrensgebiet.functions.grenzpunkt_alkis._lade_grenzpunkte_per_ids"
_PATCH_INFO = "sn_verfahrensgebiet.functions.grenzpunkt_alkis.info"
_PATCH_WARNING = "sn_verfahrensgebiet.functions.grenzpunkt_alkis.warning"
class TestGrenzpunktAlkis(unittest.TestCase):
def _proj(self, vg_layer=None):
proj = MagicMock()
proj.mapLayersByName.return_value = [vg_layer] if vg_layer else []
return proj
# ------------------------------------------------------------------ #
# 1. Kein VG-Layer wenn extent=None → (None, None) #
# ------------------------------------------------------------------ #
@patch(_PATCH_MSGBOX)
@patch(_PATCH_PROJECT)
def test_no_vg_layer_returns_none(self, mock_proj, mock_msgbox):
mock_proj.instance.return_value = self._proj(vg_layer=None)
pta, gp = grenzpunkt_alkis(None, extent=None)
self.assertIsNone(pta)
self.assertIsNone(gp)
mock_msgbox.warning.assert_called_once()
# ------------------------------------------------------------------ #
# 2. PunktortTA-Layer ungültig → (None, None) #
# ------------------------------------------------------------------ #
@patch(_PATCH_MSGBOX)
@patch(_PATCH_WFS)
@patch(_PATCH_PROJECT)
def test_invalid_punktort_layer(self, mock_proj, mock_wfs, mock_msgbox):
proj = MagicMock()
proj.mapLayersByName.return_value = []
mock_proj.instance.return_value = proj
mock_wfs.return_value = _make_layer(valid=False)
pta, gp = grenzpunkt_alkis(None, extent=_DummyRect())
self.assertIsNone(pta)
self.assertIsNone(gp)
mock_msgbox.critical.assert_called_once()
# ------------------------------------------------------------------ #
# 3. PunktortTA leer → Warnung + (None, None) #
# ------------------------------------------------------------------ #
@patch(_PATCH_MSGBOX)
@patch(_PATCH_WFS)
@patch(_PATCH_PROJECT)
def test_empty_punktort_layer(self, mock_proj, mock_wfs, mock_msgbox):
mock_proj.instance.return_value = MagicMock()
mock_wfs.return_value = _make_layer(valid=True, feature_count=0)
pta, gp = grenzpunkt_alkis(None, extent=_DummyRect())
self.assertIsNone(pta)
self.assertIsNone(gp)
mock_msgbox.warning.assert_called_once()
# ------------------------------------------------------------------ #
# 4. Keine gehoertZu-Referenzen → (punktort_layer, None) + Warnung #
# ------------------------------------------------------------------ #
@patch(_PATCH_WARNING)
@patch(_PATCH_EXTRAHIERE)
@patch(_PATCH_WFS)
@patch(_PATCH_PROJECT)
def test_no_gml_ids_returns_pta_only(
self, mock_proj, mock_wfs, mock_extrahiere, mock_warning
):
proj = MagicMock()
mock_proj.instance.return_value = proj
pta_layer = _make_layer(valid=True, feature_count=5)
mock_wfs.return_value = pta_layer
mock_extrahiere.return_value = []
pta, gp = grenzpunkt_alkis(None, extent=_DummyRect())
self.assertIs(pta, pta_layer)
self.assertIsNone(gp)
mock_warning.assert_called_once()
proj.addMapLayer.assert_called_once_with(pta_layer)
# ------------------------------------------------------------------ #
# 5. Grenzpunkte-Batch liefert None → (punktort_layer, None) + Warn #
# ------------------------------------------------------------------ #
@patch(_PATCH_WARNING)
@patch(_PATCH_LADE_GP)
@patch(_PATCH_EXTRAHIERE)
@patch(_PATCH_WFS)
@patch(_PATCH_PROJECT)
def test_grenzpunkt_batch_empty(
self, mock_proj, mock_wfs, mock_extrahiere, mock_lade_gp, mock_warning
):
proj = MagicMock()
mock_proj.instance.return_value = proj
pta_layer = _make_layer(valid=True, feature_count=3)
mock_wfs.return_value = pta_layer
mock_extrahiere.return_value = ["DE0001", "DE0002"]
mock_lade_gp.return_value = None # Batch leer
pta, gp = grenzpunkt_alkis(None, extent=_DummyRect())
self.assertIs(pta, pta_layer)
self.assertIsNone(gp)
mock_warning.assert_called_once()
# ------------------------------------------------------------------ #
# 6. Vollständiger Erfolgsfall #
# ------------------------------------------------------------------ #
@patch(_PATCH_LADE_GP)
@patch(_PATCH_EXTRAHIERE)
@patch(_PATCH_WFS)
@patch(_PATCH_PROJECT)
def test_success_returns_both_layers(
self, mock_proj, mock_wfs, mock_extrahiere, mock_lade_gp
):
proj = MagicMock()
mock_proj.instance.return_value = proj
pta_layer = _make_layer(valid=True, feature_count=10, name="PunktortTA")
gp_layer = _make_layer(valid=True, feature_count=10, name="Grenzpunkte")
mock_wfs.return_value = pta_layer
mock_extrahiere.return_value = ["DE001", "DE002", "DE003"]
mock_lade_gp.return_value = gp_layer
pta, gp = grenzpunkt_alkis(None, extent=_DummyRect())
self.assertIs(pta, pta_layer)
self.assertIs(gp, gp_layer)
# Beide Layer müssen ins Projekt eingefügt worden sein
calls = [c[0][0] for c in proj.addMapLayer.call_args_list]
self.assertIn(pta_layer, calls)
self.assertIn(gp_layer, calls)
# ------------------------------------------------------------------ #
# 7. Extent aus vorhandenem VG-Layer ableiten #
# ------------------------------------------------------------------ #
@patch(_PATCH_LADE_GP)
@patch(_PATCH_EXTRAHIERE)
@patch(_PATCH_WFS)
@patch(_PATCH_PROJECT)
def test_extent_derived_from_vg_layer(
self, mock_proj, mock_wfs, mock_extrahiere, mock_lade_gp
):
"""Wenn kein extent übergeben → Extent aus VG-Layer holen."""
vg_layer = MagicMock()
vg_layer.extent.return_value = _DummyRect()
proj = MagicMock()
proj.mapLayersByName.return_value = [vg_layer]
mock_proj.instance.return_value = proj
pta_layer = _make_layer(valid=True, feature_count=2)
gp_layer = _make_layer(valid=True, feature_count=2)
mock_wfs.return_value = pta_layer
mock_extrahiere.return_value = ["DE999"]
mock_lade_gp.return_value = gp_layer
pta, gp = grenzpunkt_alkis(None, extent=None)
vg_layer.extent.assert_called_once()
self.assertIs(pta, pta_layer)
self.assertIs(gp, gp_layer)
if __name__ == "__main__":
unittest.main()
+307
View File
@@ -0,0 +1,307 @@
"""sn_verfahrensgebiet/tests/test_knickpunkt_pruefung.py
Unit-Tests für :mod:`sn_verfahrensgebiet.functions.knickpunkt_pruefung`.
Getestete Szenarien:
_extrahiere_knickpunkte:
- Leerer Layer → leere Liste
- Polygon mit 4 Knickpunkten → korrekte Anzahl (ohne doppelten Schlusspunkt)
- Keine Geometrie → leere Liste
knickpunkt_pruefung:
- Leere Knickpunktliste → (None, 0, 0, 0)
- Kein PunktortTA-Layer → (None, 0, 0, 0)
- Identischer Punkt → grün gezählt
- Kein passender PunktortTA → rot gezählt
- PunktortTA nahe Umring auf Flurstücksgrenze → orange gezählt
- Abbruch via progress.is_canceled() → (None, 0, 0, 0)
"""
import sys
import types
import unittest
from unittest.mock import MagicMock, patch
# ---------------------------------------------------------------------------
# Minimales QGIS-Mock-Setup (identisch mit test_verfahrensgebiet_alkis_komplett)
# ---------------------------------------------------------------------------
if "qgis" not in sys.modules:
qgis_mod = types.ModuleType("qgis")
qgis_core = types.ModuleType("qgis.core")
# Attribute vorab setzen, damit patch() sie überschreiben kann
qgis_core.QgsPointXY = None
qgis_core.QgsRectangle = None
qgis_core.QgsSpatialIndex = None
qgis_core.QgsMarkerSymbol = None
qgis_core.QgsCategorizedSymbolRenderer = None
qgis_core.QgsRendererCategory = None
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.knickpunkt_pruefung import (
knickpunkt_pruefung,
_extrahiere_knickpunkte,
WERT_IDENTISCH,
WERT_KEIN_PTA,
WERT_FEHLENDER_KNICK,
)
_MOD = "sn_verfahrensgebiet.functions.knickpunkt_pruefung"
# ---------------------------------------------------------------------------
# Hilfsfunktionen
# ---------------------------------------------------------------------------
def _make_layer(valid=True, crs="EPSG:25833", lid="L1"):
layer = MagicMock()
layer.isValid.return_value = valid
layer.id.return_value = lid
mock_crs = MagicMock()
mock_crs.authid.return_value = crs
layer.crs.return_value = mock_crs
layer.getFeatures.return_value = []
return layer
def _make_progress(canceled_after: int = 9999):
"""Progress-Mock: gibt is_canceled() nach N Aufrufen True zurück."""
call_count = [0]
def _is_canceled():
call_count[0] += 1
return call_count[0] > canceled_after
prog = MagicMock()
prog.is_canceled.side_effect = _is_canceled
return prog
# ---------------------------------------------------------------------------
# Tests: _extrahiere_knickpunkte
# ---------------------------------------------------------------------------
class TestExtrahiereKnickpunkte(unittest.TestCase):
def test_leerer_layer_gibt_leere_liste(self):
layer = _make_layer()
layer.getFeatures.return_value = []
result = _extrahiere_knickpunkte(layer)
self.assertEqual(result, [])
def test_keine_gueltige_geometrie_gibt_leere_liste(self):
feat = MagicMock()
feat.geometry.return_value = None
layer = _make_layer()
layer.getFeatures.return_value = [feat]
result = _extrahiere_knickpunkte(layer)
self.assertEqual(result, [])
def test_feature_mit_leerer_geometrie_gibt_leere_liste(self):
feat = MagicMock()
geom = MagicMock()
geom.isEmpty.return_value = True
feat.geometry.return_value = geom
layer = _make_layer()
layer.getFeatures.return_value = [feat]
result = _extrahiere_knickpunkte(layer)
self.assertEqual(result, [])
def test_qgis_nicht_verfuegbar_gibt_leere_liste(self):
"""Wenn qgis.core kein QgsPointXY hat (ImportError in Funktion), leere Liste."""
layer = _make_layer()
feat = MagicMock()
geom = MagicMock()
geom.isEmpty.return_value = False
feat.geometry.return_value = geom
layer.getFeatures.return_value = [feat]
# QgsGeometry.unaryUnion wirft eine Exception → leere Liste erwartet
with patch(f"{_MOD}.QgsGeometry") as mock_geom_cls:
mock_geom_cls.unaryUnion.side_effect = ImportError("kein QGIS")
result = _extrahiere_knickpunkte(layer)
self.assertEqual(result, [])
# ---------------------------------------------------------------------------
# Tests: knickpunkt_pruefung – Eingabevalidierung
# ---------------------------------------------------------------------------
class TestKnickpunktPruefungInputvalidierung(unittest.TestCase):
def test_keine_knickpunkte_gibt_none_zurueck(self):
bror = _make_layer()
bror.getFeatures.return_value = []
result = knickpunkt_pruefung(bror, MagicMock(), None)
layer, identisch, kein_pta, fehlend = result
self.assertIsNone(layer)
self.assertEqual(identisch, 0)
self.assertEqual(kein_pta, 0)
self.assertEqual(fehlend, 0)
def test_kein_punktort_layer_gibt_none_zurueck(self):
bror = _make_layer()
result = knickpunkt_pruefung(bror, None, None, knickpunkte=[MagicMock()])
layer, identisch, kein_pta, fehlend = result
self.assertIsNone(layer)
def test_abbruch_via_progress_gibt_none_zurueck(self):
bror = _make_layer()
pta = _make_layer()
prog = _make_progress(canceled_after=0) # sofort abbrechen
result = knickpunkt_pruefung(
bror, pta, None,
progress=prog,
knickpunkte=[MagicMock()],
)
self.assertIsNone(result[0])
# ---------------------------------------------------------------------------
# Tests: knickpunkt_pruefung – Klassifizierungslogik (mit QGIS-Mocks)
# ---------------------------------------------------------------------------
class TestKnickpunktPruefungKlassifizierung(unittest.TestCase):
"""Testet die Grün/Rot/Orange-Klassifizierung mit gesteuertem QGIS-Mock."""
def _make_pta_layer_mit_punkt(self, x: float, y: float, fid: int = 1):
"""Erstellt einen PunktortTA-Layer-Mock mit einem Punkt bei (x, y)."""
layer = _make_layer(lid=f"PTA_{fid}")
geom = MagicMock()
geom.isEmpty.return_value = False
pt = MagicMock()
pt.x.return_value = x
pt.y.return_value = y
geom.asPoint.return_value = pt
feat = MagicMock()
feat.id.return_value = fid
feat.geometry.return_value = geom
layer.getFeatures.return_value = [feat]
return layer
def _make_bror_layer(self, x: float = 0.0, y: float = 0.0):
"""BROR-Layer mit einer minimalen Polygon-Geometrie."""
layer = _make_layer()
geom = MagicMock()
geom.isEmpty.return_value = False
geom.area.return_value = 10000.0
feat = MagicMock()
feat.geometry.return_value = geom
layer.getFeatures.return_value = [feat]
return layer
@patch(f"{_MOD}.QgsGeometry")
@patch(f"{_MOD}._baue_pta_index")
@patch(f"{_MOD}._erstelle_ergebnis_layer")
@patch(f"qgis.core.QgsRectangle", create=True)
@patch(f"qgis.core.QgsPointXY", create=True)
def test_identischer_punkt_wird_gruen_gezaehlt(
self, mock_ptxy, mock_rect, mock_erstelle, mock_index, mock_geom_cls
):
"""PunktortTA exakt am Knickpunkt → identisch_count == 1."""
# Knickpunkt bei (100, 200)
knick = MagicMock()
knick.x.return_value = 100.0
knick.y.return_value = 200.0
# PunktortTA ebenfalls bei (100, 200) – exakter Treffer
pta_geom = MagicMock()
pta_geom.isEmpty.return_value = False
pta_pt = MagicMock()
pta_pt.x.return_value = 100.0
pta_pt.y.return_value = 200.0
pta_geom.asPoint.return_value = pta_pt
# Spatial-Index gibt Feature-ID 1 zurück
mock_idx = MagicMock()
mock_idx.intersects.return_value = [1]
mock_index.return_value = (mock_idx, {1: pta_geom})
# Ergebnis-Layer
pr = MagicMock()
pr.addFeatures.return_value = True
ergebnis = MagicMock()
ergebnis.dataProvider.return_value = pr
ergebnis.isValid.return_value = True
mock_erstelle.return_value = ergebnis
bror = self._make_bror_layer()
pta = self._make_pta_layer_mit_punkt(100.0, 200.0)
result = knickpunkt_pruefung(
bror, pta, None, knickpunkte=[knick]
)
_, identisch, kein_pta, _ = result
self.assertEqual(identisch, 1)
self.assertEqual(kein_pta, 0)
@patch(f"{_MOD}.QgsGeometry")
@patch(f"{_MOD}._baue_pta_index")
@patch(f"{_MOD}._erstelle_ergebnis_layer")
@patch(f"qgis.core.QgsRectangle", create=True)
@patch(f"qgis.core.QgsPointXY", create=True)
def test_kein_pta_an_knickpunkt_wird_rot_gezaehlt(
self, mock_ptxy, mock_rect, mock_erstelle, mock_index, mock_geom_cls
):
"""Kein PunktortTA am Knickpunkt → kein_pta_count == 1."""
knick = MagicMock()
knick.x.return_value = 100.0
knick.y.return_value = 200.0
# Spatial-Index gibt leere Liste zurück → kein Treffer
mock_idx = MagicMock()
mock_idx.intersects.return_value = []
mock_index.return_value = (mock_idx, {})
pr = MagicMock()
pr.addFeatures.return_value = True
ergebnis = MagicMock()
ergebnis.dataProvider.return_value = pr
ergebnis.isValid.return_value = True
mock_erstelle.return_value = ergebnis
bror = self._make_bror_layer()
pta = self._make_pta_layer_mit_punkt(999.0, 999.0) # weit weg
result = knickpunkt_pruefung(
bror, pta, None, knickpunkte=[knick]
)
_, identisch, kein_pta, _ = result
self.assertEqual(identisch, 0)
self.assertEqual(kein_pta, 1)
# ---------------------------------------------------------------------------
# Tests: _baue_flst_vertex_set
# ---------------------------------------------------------------------------
class TestBaueFlstVertexSet(unittest.TestCase):
def test_none_layer_gibt_leeres_set(self):
from sn_verfahrensgebiet.functions.knickpunkt_pruefung import _baue_flst_vertex_set
result = _baue_flst_vertex_set(None, MagicMock())
self.assertEqual(result, set())
def test_layer_ohne_schnitt_mit_puffer_ignoriert(self):
from sn_verfahrensgebiet.functions.knickpunkt_pruefung import _baue_flst_vertex_set
layer = _make_layer()
geom = MagicMock()
geom.isEmpty.return_value = False
geom.intersects.return_value = False # kein Schnitt
feat = MagicMock()
feat.geometry.return_value = geom
layer.getFeatures.return_value = [feat]
buffer_geom = MagicMock()
result = _baue_flst_vertex_set(layer, buffer_geom)
self.assertEqual(result, set())
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()
@@ -0,0 +1,670 @@
"""sn_verfahrensgebiet/tests/test_verfahrensgebiet_alkis_komplett.py
Unit-Tests für :mod:`sn_verfahrensgebiet.functions.verfahrensgebiet_alkis_komplett`.
Getestete Szenarien:
Erfolgsfall (Flächenübereinstimmung):
- Vollständiger Workflow → enriched Layer + flst_layer + bror_layer + LoadStatus.FIRST
- VKZ aus Projektvariable wird als Feld gesetzt (wenn vorhanden)
- Kein VKZ in Projektvariable → Feld wird nicht gesetzt (kein Fehler)
Abbruchpfade:
- verfahrensgebiet_alkis gibt LoadStatus.NONE → (None, None, None, NONE)
- flurstueck_alkis gibt LoadStatus.NONE → (None, None, None, NONE)
- Keine Flurstücke selektiert → (None, flst_layer, None, NONE) + Warnung
- erstelle_verfahrensgebiet_aus_auswahl liefert ungültigen Layer → (None, flst_layer, None, NONE)
- Progress-Abbruch nach Schritt 1 → sofortiger sauberer Rückgabestatus
Flächenvergleich:
- Übereinstimmung → show_info_dialog, keine Detailprüfung
- Abweichung + abbrechen → kein Detailprüfungs-Aufruf
- Abweichung + als_vg_laden → BROR umbenannt, als enriched_layer zurückgegeben
- Abweichung + detailpruefung → lade_punktort_ta + knickpunkt_pruefung aufgerufen
Cleanup:
- BROR-Layer wird bei Erfolg NICHT entfernt
- BROR-Layer wird bei Fehler entfernt (wenn in diesem Lauf hinzugefügt)
- TempVerfahrensgebiet wird stets entfernt
Hilfsfunktionen:
- _select_by_location: processing-Fallback liefert 0 → keine Exception
- _fuege_vkz_feld_hinzu: leeres VKZ wird ignoriert
- _vergleiche_flaechen: korrekte Toleranzlogik
- _lese_flaechen_toleranz: Default 0.0, parst Projektvariable
"""
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.verfahrensgebiet_alkis import LoadStatus
from sn_verfahrensgebiet.functions.verfahrensgebiet_alkis_komplett import (
verfahrensgebiet_alkis_komplett,
_select_by_location,
_fuege_vkz_feld_hinzu,
_vergleiche_flaechen,
_lese_flaechen_toleranz,
)
# ---------------------------------------------------------------------------
# Dummies
# ---------------------------------------------------------------------------
class _DummyRect:
def xMinimum(self): return 414000.0
def yMinimum(self): return 5657000.0
def xMaximum(self): return 415000.0
def yMaximum(self): return 5658000.0
def _make_layer(valid=True, name="Layer", lid="L1", feature_count=1, area=1000.0):
"""Erzeugt einen konfigurierten Mock-Layer mit getFeatures-Support."""
layer = MagicMock()
layer.isValid.return_value = valid
layer.id.return_value = lid
layer.name.return_value = name
layer.extent.return_value = _DummyRect()
layer.featureCount.return_value = feature_count
layer.selectedFeatureCount.return_value = feature_count
mock_geom = MagicMock()
mock_geom.isEmpty.return_value = False
mock_geom.area.return_value = area
mock_geom.boundary.return_value = MagicMock(isEmpty=lambda: True)
mock_feat = MagicMock()
mock_feat.geometry.return_value = mock_geom
layer.getFeatures.return_value = [mock_feat]
mock_crs = MagicMock()
mock_crs.authid.return_value = "EPSG:25833"
layer.crs.return_value = mock_crs
return layer
class _DummyProject:
def __init__(self):
self.added = []
self.removed = []
self._layers_by_name: dict = {}
def addMapLayer(self, layer):
self.added.append(layer)
def removeMapLayer(self, lid):
self.removed.append(lid)
def mapLayersByName(self, name):
return self._layers_by_name.get(name, [])
def instance(self):
return self
def _make_progress_mock(canceled=False):
prog = MagicMock()
prog.is_canceled.return_value = canceled
return prog
# ---------------------------------------------------------------------------
# Patch-Konstanten
# ---------------------------------------------------------------------------
_MOD = "sn_verfahrensgebiet.functions.verfahrensgebiet_alkis_komplett"
_PATCH_VG_ALKIS = f"{_MOD}.verfahrensgebiet_alkis"
_PATCH_GEM = f"{_MOD}.gemarkung_schluessel_und_namen_von_vg_geometrie"
_PATCH_FLST = f"{_MOD}.flurstueck_alkis"
_PATCH_SELECT = f"{_MOD}._select_by_location"
_PATCH_ERSTELLE = f"{_MOD}.erstelle_verfahrensgebiet_aus_auswahl"
_PATCH_VKZ = f"{_MOD}._fuege_vkz_feld_hinzu"
_PATCH_PROJECT = f"{_MOD}.QgsProject"
_PATCH_MSGBOX = f"{_MOD}.QMessageBox"
_PATCH_WARNING = f"{_MOD}.warning"
_PATCH_GET_VAR = f"{_MOD}.get_variable"
_PATCH_PROGRESS = f"{_MOD}.create_progress_dialog"
_PATCH_VERGLEICHE = f"{_MOD}._vergleiche_flaechen"
_PATCH_SHOW_INFO = f"{_MOD}.show_info_dialog"
_PATCH_ASK_DIALOG = f"{_MOD}.ask_detailpruefung_oder_vg_laden"
_PATCH_GP_ALKIS = f"{_MOD}.lade_punktort_ta"
_PATCH_KNICK = f"{_MOD}.knickpunkt_pruefung"
_PATCH_EXTRAHIERE = f"{_MOD}._extrahiere_knickpunkte"
# ---------------------------------------------------------------------------
# Tests: _select_by_location
# ---------------------------------------------------------------------------
class TestSelectByLocation(unittest.TestCase):
def test_processing_not_available_returns_zero(self):
flst, vg = MagicMock(), MagicMock()
with patch.dict("sys.modules", {"processing": None}):
result = _select_by_location(flst, vg)
self.assertEqual(result, 0)
def test_processing_success_returns_count(self):
flst = MagicMock()
flst.selectedFeatureCount.return_value = 7
vg = MagicMock()
mock_proc = MagicMock()
with patch.dict("sys.modules", {"processing": mock_proc}):
result = _select_by_location(flst, vg)
self.assertEqual(result, 7)
def test_processing_exception_returns_zero(self):
flst, vg = MagicMock(), MagicMock()
mock_proc = MagicMock()
mock_proc.run.side_effect = RuntimeError("Kein QGIS")
with patch.dict("sys.modules", {"processing": mock_proc}):
result = _select_by_location(flst, vg)
self.assertEqual(result, 0)
# ---------------------------------------------------------------------------
# Tests: _fuege_vkz_feld_hinzu
# ---------------------------------------------------------------------------
class TestFuegeVkzFeldHinzu(unittest.TestCase):
def test_empty_vkz_does_nothing(self):
layer = MagicMock()
_fuege_vkz_feld_hinzu(layer, "")
layer.dataProvider.assert_not_called()
def test_none_vkz_does_nothing(self):
layer = MagicMock()
_fuege_vkz_feld_hinzu(layer, None)
layer.dataProvider.assert_not_called()
def test_vkz_field_added(self):
pr = MagicMock()
pr.addAttributes.return_value = True
pr.changeAttributeValues.return_value = True
layer = MagicMock()
layer.dataProvider.return_value = pr
layer.fields.return_value.indexFromName.return_value = 2
layer.getFeatures.return_value = []
_fuege_vkz_feld_hinzu(layer, "VF-2024-001")
pr.addAttributes.assert_called_once()
# ---------------------------------------------------------------------------
# Tests: _vergleiche_flaechen / _lese_flaechen_toleranz
# ---------------------------------------------------------------------------
class TestFlaechenvergleich(unittest.TestCase):
def _layer(self, area: float):
layer = MagicMock()
geom = MagicMock()
geom.isEmpty.return_value = False
geom.area.return_value = area
feat = MagicMock()
feat.geometry.return_value = geom
layer.getFeatures.return_value = [feat]
return layer
def test_gleiche_flaeche_stimmt_ueberein(self):
self.assertTrue(_vergleiche_flaechen(self._layer(1234.5), self._layer(1234.5), 0.0)[2])
def test_verschiedene_flaeche_stimmt_nicht_ueberein(self):
self.assertFalse(_vergleiche_flaechen(self._layer(1234.5), self._layer(1200.0), 0.0)[2])
def test_differenz_innerhalb_toleranz(self):
self.assertTrue(_vergleiche_flaechen(self._layer(1234.5), self._layer(1234.8), 1.0)[2])
def test_differenz_ausserhalb_toleranz(self):
self.assertFalse(_vergleiche_flaechen(self._layer(1234.5), self._layer(1236.0), 1.0)[2])
def test_flaechen_werte_korrekt(self):
bror_f, vg_f, _ = _vergleiche_flaechen(self._layer(800.0), self._layer(750.0))
self.assertAlmostEqual(bror_f, 800.0)
self.assertAlmostEqual(vg_f, 750.0)
@patch(f"{_MOD}.get_variable", return_value=None)
def test_lese_toleranz_default_ist_null(self, _):
self.assertEqual(_lese_flaechen_toleranz(), 0.0)
@patch(f"{_MOD}.get_variable", return_value="2.5")
def test_lese_toleranz_parst_string(self, _):
self.assertAlmostEqual(_lese_flaechen_toleranz(), 2.5)
@patch(f"{_MOD}.get_variable", return_value="ungueltig")
def test_lese_toleranz_ungueltig_fallback_null(self, _):
self.assertEqual(_lese_flaechen_toleranz(), 0.0)
# ---------------------------------------------------------------------------
# Tests: verfahrensgebiet_alkis_komplett (Hauptworkflow)
# ---------------------------------------------------------------------------
class TestVerfahrensgebietAlkisKomplett(unittest.TestCase):
# ------------------------------------------------------------------ #
# Abbruchpfade
# ------------------------------------------------------------------ #
@patch(_PATCH_PROGRESS)
@patch(_PATCH_PROJECT)
@patch(_PATCH_VG_ALKIS)
def test_vg_alkis_none_aborts(self, mock_vg, mock_proj, mock_prog):
mock_proj.instance.return_value = _DummyProject()
mock_vg.return_value = (None, LoadStatus.NONE, [])
mock_prog.return_value = _make_progress_mock()
enriched, flst, bror, status = verfahrensgebiet_alkis_komplett(None)
self.assertIsNone(enriched)
self.assertIsNone(flst)
self.assertIsNone(bror)
self.assertEqual(status, LoadStatus.NONE)
@patch(_PATCH_PROGRESS)
@patch(_PATCH_PROJECT)
@patch(_PATCH_GEM)
@patch(_PATCH_VG_ALKIS)
def test_flst_none_aborts(self, mock_vg, mock_gem, mock_proj, mock_prog):
mock_proj.instance.return_value = _DummyProject()
vg_layer = _make_layer(name="BauRaumOrdnungsrecht (ALKIS)")
mock_vg.return_value = (vg_layer, LoadStatus.FIRST, [])
mock_gem.return_value = [("1234", "Muster")]
mock_prog.return_value = _make_progress_mock()
with patch(_PATCH_FLST, return_value=(None, LoadStatus.NONE)):
enriched, flst, bror, status = verfahrensgebiet_alkis_komplett(None)
self.assertIsNone(enriched)
self.assertIsNone(flst)
self.assertEqual(status, LoadStatus.NONE)
@patch(_PATCH_PROGRESS)
@patch(_PATCH_MSGBOX)
@patch(_PATCH_SELECT)
@patch(_PATCH_PROJECT)
@patch(_PATCH_GEM)
@patch(_PATCH_FLST)
@patch(_PATCH_VG_ALKIS)
def test_no_selection_returns_none_enriched(
self, mock_vg, mock_flst, mock_gem, mock_proj, mock_select, mock_msgbox, mock_prog
):
mock_proj.instance.return_value = _DummyProject()
vg_layer = _make_layer(name="BauRaumOrdnungsrecht (ALKIS)")
flst_layer = _make_layer(name="Flurstücke")
mock_vg.return_value = (vg_layer, LoadStatus.FIRST, [])
mock_gem.return_value = []
mock_flst.return_value = (flst_layer, LoadStatus.FIRST)
mock_select.return_value = 0
mock_prog.return_value = _make_progress_mock()
enriched, flst, bror, status = verfahrensgebiet_alkis_komplett(None)
self.assertIsNone(enriched)
self.assertIs(flst, flst_layer)
self.assertIsNone(bror)
self.assertEqual(status, LoadStatus.NONE)
mock_msgbox.warning.assert_called_once()
@patch(_PATCH_PROGRESS)
@patch(_PATCH_MSGBOX)
@patch(_PATCH_ERSTELLE)
@patch(_PATCH_SELECT)
@patch(_PATCH_PROJECT)
@patch(_PATCH_GEM)
@patch(_PATCH_FLST)
@patch(_PATCH_VG_ALKIS)
def test_invalid_enriched_layer_returns_none(
self, mock_vg, mock_flst, mock_gem, mock_proj, mock_select, mock_erstelle, mock_msgbox, mock_prog
):
mock_proj.instance.return_value = _DummyProject()
vg_layer = _make_layer(name="BauRaumOrdnungsrecht (ALKIS)")
flst_layer = _make_layer(name="Flurstücke")
bad_enriched = _make_layer(valid=False)
mock_vg.return_value = (vg_layer, LoadStatus.FIRST, [])
mock_gem.return_value = []
mock_flst.return_value = (flst_layer, LoadStatus.FIRST)
mock_select.return_value = 5
mock_erstelle.return_value = (bad_enriched, 0, 5)
mock_prog.return_value = _make_progress_mock()
enriched, flst, bror, status = verfahrensgebiet_alkis_komplett(None)
self.assertIsNone(enriched)
self.assertIs(flst, flst_layer)
self.assertIsNone(bror)
self.assertEqual(status, LoadStatus.NONE)
mock_msgbox.critical.assert_called_once()
@patch(_PATCH_PROGRESS)
@patch(_PATCH_PROJECT)
@patch(_PATCH_VG_ALKIS)
def test_progress_cancel_after_step1_returns_none(self, mock_vg, mock_proj, mock_prog):
"""Abbruch nach Schritt 1 → sofortiger sauberer Rückgabestatus."""
mock_proj.instance.return_value = _DummyProject()
vg_layer = _make_layer(name="BauRaumOrdnungsrecht (ALKIS)")
mock_vg.return_value = (vg_layer, LoadStatus.FIRST, [])
prog = MagicMock()
prog.is_canceled.side_effect = [False, True] # 1. Aufruf False, 2. True
mock_prog.return_value = prog
enriched, flst, bror, status = verfahrensgebiet_alkis_komplett(None)
self.assertIsNone(enriched)
self.assertEqual(status, LoadStatus.NONE)
# ------------------------------------------------------------------ #
# Flächenvergleich – Übereinstimmung
# ------------------------------------------------------------------ #
@patch(_PATCH_PROGRESS)
@patch(_PATCH_SHOW_INFO)
@patch(_PATCH_VERGLEICHE)
@patch(_PATCH_VKZ)
@patch(_PATCH_GET_VAR)
@patch(_PATCH_ERSTELLE)
@patch(_PATCH_SELECT)
@patch(_PATCH_PROJECT)
@patch(_PATCH_GEM)
@patch(_PATCH_FLST)
@patch(_PATCH_VG_ALKIS)
def test_success_flaechen_match_zeigt_info(
self, mock_vg, mock_flst, mock_gem, mock_proj, mock_select,
mock_erstelle, mock_get_var, mock_vkz, mock_vergl, mock_info, mock_prog,
):
"""Flächenübereinstimmung → show_info_dialog mit erwartetem Text."""
proj_inst = _DummyProject()
mock_proj.instance.return_value = proj_inst
vg_layer = _make_layer(name="BauRaumOrdnungsrecht (ALKIS)")
flst_layer = _make_layer(name="Flurstücke", lid="FLST1")
enriched = _make_layer(name="Verfahrensgebiet", lid="VG_ENRICHED", valid=True)
mock_vg.return_value = (vg_layer, LoadStatus.FIRST, [])
mock_gem.return_value = [("1234", "Muster")]
mock_flst.return_value = (flst_layer, LoadStatus.FIRST)
mock_select.return_value = 3
mock_erstelle.return_value = (enriched, 3, 0)
mock_get_var.return_value = "VF-2024-001"
mock_vergl.return_value = (1234.5, 1234.5, True)
mock_prog.return_value = _make_progress_mock()
result_enriched, result_flst, result_bror, status = verfahrensgebiet_alkis_komplett(None)
self.assertIs(result_enriched, enriched)
self.assertIs(result_flst, flst_layer)
self.assertIs(result_bror, vg_layer)
self.assertEqual(status, LoadStatus.FIRST)
mock_vkz.assert_called_once_with(enriched, "VF-2024-001")
mock_info.assert_called_once()
self.assertIn("erwarteter Geometrie", mock_info.call_args[0][1])
@patch(_PATCH_PROGRESS)
@patch(_PATCH_SHOW_INFO)
@patch(_PATCH_VERGLEICHE)
@patch(_PATCH_VKZ)
@patch(_PATCH_GET_VAR)
@patch(_PATCH_ERSTELLE)
@patch(_PATCH_SELECT)
@patch(_PATCH_PROJECT)
@patch(_PATCH_GEM)
@patch(_PATCH_FLST)
@patch(_PATCH_VG_ALKIS)
def test_no_vkz_variable_passes_empty_string(
self, mock_vg, mock_flst, mock_gem, mock_proj, mock_select,
mock_erstelle, mock_get_var, mock_vkz, mock_vergl, mock_info, mock_prog,
):
mock_proj.instance.return_value = _DummyProject()
vg_layer = _make_layer(name="BauRaumOrdnungsrecht (ALKIS)")
flst_layer = _make_layer(name="Flurstücke")
enriched = _make_layer(name="Verfahrensgebiet", valid=True)
mock_vg.return_value = (vg_layer, LoadStatus.FIRST, [])
mock_gem.return_value = []
mock_flst.return_value = (flst_layer, LoadStatus.FIRST)
mock_select.return_value = 2
mock_erstelle.return_value = (enriched, 2, 0)
mock_get_var.return_value = ""
mock_vergl.return_value = (1000.0, 1000.0, True)
mock_prog.return_value = _make_progress_mock()
verfahrensgebiet_alkis_komplett(None)
mock_vkz.assert_called_once_with(enriched, "")
# ------------------------------------------------------------------ #
# Flächenvergleich – Abweichung + Abbrechen
# ------------------------------------------------------------------ #
@patch(_PATCH_PROGRESS)
@patch(_PATCH_KNICK)
@patch(_PATCH_ASK_DIALOG)
@patch(_PATCH_VERGLEICHE)
@patch(_PATCH_VKZ)
@patch(_PATCH_GET_VAR)
@patch(_PATCH_ERSTELLE)
@patch(_PATCH_SELECT)
@patch(_PATCH_PROJECT)
@patch(_PATCH_GEM)
@patch(_PATCH_FLST)
@patch(_PATCH_VG_ALKIS)
def test_flaechen_abweichung_abbrechen_kein_detailcheck(
self, mock_vg, mock_flst, mock_gem, mock_proj, mock_select,
mock_erstelle, mock_get_var, mock_vkz, mock_vergl, mock_ask, mock_knick, mock_prog,
):
"""Abweichung + Abbrechen → knickpunkt_pruefung wird NICHT aufgerufen."""
mock_proj.instance.return_value = _DummyProject()
vg_layer = _make_layer(name="BauRaumOrdnungsrecht (ALKIS)")
flst_layer = _make_layer(name="Flurstücke")
enriched = _make_layer(name="Verfahrensgebiet", valid=True)
mock_vg.return_value = (vg_layer, LoadStatus.FIRST, [])
mock_gem.return_value = []
mock_flst.return_value = (flst_layer, LoadStatus.FIRST)
mock_select.return_value = 3
mock_erstelle.return_value = (enriched, 3, 0)
mock_get_var.return_value = ""
mock_vergl.return_value = (1000.0, 800.0, False)
mock_ask.return_value = "abbrechen"
mock_prog.return_value = _make_progress_mock()
result_enriched, _, _, status = verfahrensgebiet_alkis_komplett(None)
mock_knick.assert_not_called()
self.assertIs(result_enriched, enriched)
self.assertEqual(status, LoadStatus.FIRST)
# ------------------------------------------------------------------ #
# Flächenvergleich – Abweichung + Als VG laden
# ------------------------------------------------------------------ #
@patch(_PATCH_PROGRESS)
@patch(_PATCH_KNICK)
@patch(_PATCH_ASK_DIALOG)
@patch(_PATCH_VERGLEICHE)
@patch(_PATCH_VKZ)
@patch(_PATCH_GET_VAR)
@patch(_PATCH_ERSTELLE)
@patch(_PATCH_SELECT)
@patch(_PATCH_PROJECT)
@patch(_PATCH_GEM)
@patch(_PATCH_FLST)
@patch(_PATCH_VG_ALKIS)
def test_flaechen_abweichung_als_vg_laden(
self, mock_vg, mock_flst, mock_gem, mock_proj, mock_select,
mock_erstelle, mock_get_var, mock_vkz, mock_vergl, mock_ask, mock_knick, mock_prog,
):
"""Abweichung + Als VG laden → BROR-Layer wird umbenannt und als
enriched_layer zurückgegeben; flst_layer und bror_layer sind None."""
proj_inst = _DummyProject()
mock_proj.instance.return_value = proj_inst
vg_layer = _make_layer(name="BauRaumOrdnungsrecht (ALKIS)", lid="BROR_VG")
flst_layer = _make_layer(name="Flurstücke")
enriched = _make_layer(name="Verfahrensgebiet", valid=True)
mock_vg.return_value = (vg_layer, LoadStatus.FIRST, [])
mock_gem.return_value = []
mock_flst.return_value = (flst_layer, LoadStatus.FIRST)
mock_select.return_value = 3
mock_erstelle.return_value = (enriched, 3, 0)
mock_get_var.return_value = ""
mock_vergl.return_value = (1000.0, 800.0, False)
mock_ask.return_value = "als_vg_laden"
mock_prog.return_value = _make_progress_mock()
result_enriched, result_flst, result_bror, status = verfahrensgebiet_alkis_komplett(None)
mock_knick.assert_not_called()
# BROR-Layer wird als enriched_layer zurückgegeben
self.assertIs(result_enriched, vg_layer)
self.assertIsNone(result_flst)
self.assertIsNone(result_bror)
self.assertEqual(status, LoadStatus.FIRST)
# Name wurde auf VERF_LAYER_NAME geändert
vg_layer.setName.assert_called()
# ------------------------------------------------------------------ #
# Flächenvergleich – Abweichung + Detailprüfung
# ------------------------------------------------------------------ #
@patch(_PATCH_PROGRESS)
@patch(_PATCH_SHOW_INFO)
@patch(_PATCH_EXTRAHIERE)
@patch(_PATCH_KNICK)
@patch(_PATCH_GP_ALKIS)
@patch(_PATCH_ASK_DIALOG)
@patch(_PATCH_VERGLEICHE)
@patch(_PATCH_VKZ)
@patch(_PATCH_GET_VAR)
@patch(_PATCH_ERSTELLE)
@patch(_PATCH_SELECT)
@patch(_PATCH_PROJECT)
@patch(_PATCH_GEM)
@patch(_PATCH_FLST)
@patch(_PATCH_VG_ALKIS)
def test_flaechen_abweichung_detailpruefung_startet_knickpruefung(
self, mock_vg, mock_flst, mock_gem, mock_proj, mock_select,
mock_erstelle, mock_get_var, mock_vkz, mock_vergl, mock_ask,
mock_gp, mock_knick, mock_extrahiere, mock_info, mock_prog,
):
"""Abweichung + Detailprüfung → lade_punktort_ta + knickpunkt_pruefung
aufgerufen, Ergebnislayer ins Projekt, Abschlussmeldung mit Zählung."""
proj_inst = _DummyProject()
mock_proj.instance.return_value = proj_inst
vg_layer = _make_layer(name="BauRaumOrdnungsrecht (ALKIS)")
flst_layer = _make_layer(name="Flurstücke")
enriched = _make_layer(name="Verfahrensgebiet", valid=True)
punktort_layer = _make_layer(name="PunktortTA", lid="PTA_L")
ergebnis_layer = _make_layer(name="Knickpunktprüfung", lid="KNICK_L")
mock_vg.return_value = (vg_layer, LoadStatus.FIRST, [])
mock_gem.return_value = []
mock_flst.return_value = (flst_layer, LoadStatus.FIRST)
mock_select.return_value = 3
mock_erstelle.return_value = (enriched, 3, 0)
mock_get_var.return_value = ""
mock_vergl.return_value = (1000.0, 800.0, False)
mock_ask.return_value = "detailpruefung"
mock_extrahiere.return_value = [MagicMock(), MagicMock(), MagicMock()] # 3 Knickpunkte
mock_gp.return_value = punktort_layer
mock_knick.return_value = (ergebnis_layer, 2, 1, 0)
mock_prog.return_value = _make_progress_mock()
result_enriched, _, _, status = verfahrensgebiet_alkis_komplett(None)
mock_gp.assert_called_once()
mock_knick.assert_called_once()
self.assertIn(ergebnis_layer, proj_inst.added)
mock_info.assert_called_once()
# Abschlussmeldung muss Zahlen enthalten
info_text = mock_info.call_args[0][1]
self.assertIn("3", info_text) # Anzahl Knickpunkte
self.assertIn("2", info_text) # identisch
self.assertIn("1", info_text) # kein_pta
self.assertEqual(status, LoadStatus.FIRST)
# ------------------------------------------------------------------ #
# Cleanup-Verhalten
# ------------------------------------------------------------------ #
@patch(_PATCH_PROGRESS)
@patch(_PATCH_SHOW_INFO)
@patch(_PATCH_VERGLEICHE)
@patch(_PATCH_VKZ)
@patch(_PATCH_GET_VAR)
@patch(_PATCH_ERSTELLE)
@patch(_PATCH_SELECT)
@patch(_PATCH_PROJECT)
@patch(_PATCH_GEM)
@patch(_PATCH_FLST)
@patch(_PATCH_VG_ALKIS)
def test_bror_layer_bleibt_bei_erfolg_im_projekt(
self, mock_vg, mock_flst, mock_gem, mock_proj, mock_select,
mock_erstelle, mock_get_var, mock_vkz, mock_vergl, mock_info, mock_prog,
):
"""Bei Erfolg darf der BROR-Layer NICHT entfernt werden."""
proj_inst = _DummyProject()
mock_proj.instance.return_value = proj_inst
vg_layer = _make_layer(name="BauRaumOrdnungsrecht (ALKIS)", lid="BROR_ID")
flst_layer = _make_layer(name="Flurstücke")
enriched = _make_layer(name="Verfahrensgebiet", valid=True)
mock_vg.return_value = (vg_layer, LoadStatus.FIRST, [])
mock_gem.return_value = []
mock_flst.return_value = (flst_layer, LoadStatus.FIRST)
mock_select.return_value = 3
mock_erstelle.return_value = (enriched, 3, 0)
mock_get_var.return_value = ""
mock_vergl.return_value = (1000.0, 1000.0, True)
mock_prog.return_value = _make_progress_mock()
verfahrensgebiet_alkis_komplett(None)
self.assertNotIn("BROR_ID", proj_inst.removed)
@patch(_PATCH_PROGRESS)
@patch(_PATCH_PROJECT)
@patch(_PATCH_GEM)
@patch(_PATCH_VG_ALKIS)
def test_bror_layer_wird_bei_fehler_entfernt(
self, mock_vg, mock_gem, mock_proj, mock_prog
):
"""Bei Fehler (flst_alkis NONE) wird der BROR-Layer aus dem Projekt entfernt."""
proj_inst = _DummyProject()
mock_proj.instance.return_value = proj_inst
vg_layer = _make_layer(name="BauRaumOrdnungsrecht (ALKIS)", lid="BROR_ERR")
mock_vg.return_value = (vg_layer, LoadStatus.FIRST, [])
mock_gem.return_value = []
mock_prog.return_value = _make_progress_mock()
with patch(_PATCH_FLST, return_value=(None, LoadStatus.NONE)):
verfahrensgebiet_alkis_komplett(None)
self.assertIn("BROR_ERR", proj_inst.removed)
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()
+489
View File
@@ -0,0 +1,489 @@
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", layer_name="DummyLayer"):
self._valid = valid
self._id = layer_id
self._name = layer_name
def isValid(self):
return self._valid
def id(self):
return self._id
def name(self):
return self._name
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_grenz_alkis = _DummyButton()
tab.btn_verf_aus_auswahl = _DummyButton()
tab.btn_verf_aus_liste = _DummyButton()
tab.verf_layer_combo = _DummyCombo()
tab.haken_verf = types.SimpleNamespace(setText=lambda _t: None, setStyleSheet=lambda _s: None)
tab.haken_flurst = types.SimpleNamespace(setText=lambda _t: None, setStyleSheet=lambda _s: None)
tab.haken_grenz = types.SimpleNamespace(setText=lambda _t: None, setStyleSheet=lambda _s: None)
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_komplett")
@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_komplett,
mock_project_instance,
mock_apply_style,
mock_success,
):
"""verfahrensgebiet_alkis_komplett → 4-Tupel (enriched, flst, bror, FIRST)."""
tab = self._make_tab()
enriched = _DummyLayer(valid=True, layer_id="enriched_ok")
flst = _DummyLayer(valid=True, layer_id="flst_ok")
mock_alkis_komplett.return_value = (enriched, flst, None, LoadStatus.FIRST)
proj = _DummyProject()
proj.mapLayersByName = lambda _n: [] # kein bestehender Flst-Layer
mock_project_instance.return_value = proj
mock_iface.mapCanvas.return_value = _DummyCanvas()
tab.handle_verf_alkis()
mock_apply_style.assert_called()
mock_success.assert_called_once()
mock_set_layer_id.assert_called_with("enriched_ok")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.set_verfahrensgebiet_layer_id")
@patch(
"sn_verfahrensgebiet.ui.tabs.working_tab.verfahrensgebiet_alkis_komplett",
return_value=(None, None, None, LoadStatus.NONE),
)
def test_handle_verf_alkis_failure_clears_layer_var(self, _mock_alkis, mock_set_layer_id):
"""Status NONE → Layer-Variable wird auf '' gesetzt."""
tab = self._make_tab()
tab.handle_verf_alkis()
mock_set_layer_id.assert_called_once_with("")
# ------------------------------------------------------------------
# handle_grenz_alkis
# ------------------------------------------------------------------
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.grenzpunkt_alkis")
def test_handle_grenz_alkis_success_sets_haken(self, mock_grenz):
"""Grenzpunkte erfolgreich geladen → haken_grenz wird gesetzt."""
tab = self._make_tab()
haken_calls = []
tab.setze_haken = lambda lbl, v: haken_calls.append((lbl, v))
pta = _DummyLayer(valid=True)
gp = _DummyLayer(valid=True)
mock_grenz.return_value = (pta, gp)
tab.handle_grenz_alkis()
self.assertEqual(len(haken_calls), 1)
self.assertIs(haken_calls[0][0], tab.haken_grenz)
self.assertTrue(haken_calls[0][1])
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.grenzpunkt_alkis")
def test_handle_grenz_alkis_none_result_sets_no_haken(self, mock_grenz):
"""grenzpunkt_alkis liefert (None, None) → kein Haken."""
tab = self._make_tab()
haken_calls = []
tab.setze_haken = lambda lbl, v: haken_calls.append((lbl, v))
mock_grenz.return_value = (None, None)
tab.handle_grenz_alkis()
self.assertEqual(len(haken_calls), 0)
# Button wieder aktiviert (finally)
self.assertTrue(tab.btn_grenz_alkis.isEnabled())
# ------------------------------------------------------------------
# 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)
+568
View File
@@ -0,0 +1,568 @@
"""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, create_progress_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 LoadStatus
from sn_verfahrensgebiet.functions.flurstueck_alkis import flurstueck_alkis
from sn_verfahrensgebiet.functions.verfahrensgebiet_alkis_komplett import verfahrensgebiet_alkis_komplett
from sn_verfahrensgebiet.functions.grenzpunkt_alkis import grenzpunkte_gemarkungsweise, JOINED_LAYER_NAME
from sn_verfahrensgebiet.functions.gemarkung_alkis import gemarkung_schluessel_und_namen_von_vg_geometrie
from sn_verfahrensgebiet.functions.umringstatistik_grenzpunkte import umringstatistik_grenzpunkte
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 2b: Grenzpunkte
row_grenz_header = QHBoxLayout()
lbl_grenz = QLabel("<b>Grenzpunkte</b>")
self.haken_grenz = self._haken_label()
row_grenz_header.addWidget(lbl_grenz)
row_grenz_header.addWidget(self.haken_grenz)
row_grenz_header.addStretch()
layout.addLayout(row_grenz_header)
row_grenz_buttons = QHBoxLayout()
self.btn_grenz_alkis = QPushButton("Grenzpunkte aus ALKIS laden")
row_grenz_buttons.addWidget(self.btn_grenz_alkis)
layout.addLayout(row_grenz_buttons)
row_umring_buttons = QHBoxLayout()
self.btn_umring_statistik = QPushButton("Umringstatistik Grenzpunkte")
row_umring_buttons.addWidget(self.btn_umring_statistik)
layout.addLayout(row_umring_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_grenz_alkis.clicked.connect(self.handle_grenz_alkis)
self.btn_umring_statistik.clicked.connect(self.handle_umringstatistik)
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) -> None:
"""Startet den vollständigen VG-Lade-Workflow (ALKIS → Gemarkungen → Flurstücke →
Verschneidung → Flächenvergleich → opt. Umringsprüfung).
Delegiert an :func:`~sn_verfahrensgebiet.functions.verfahrensgebiet_alkis_komplett.verfahrensgebiet_alkis_komplett`.
Nach Erfolg werden enriched VG-Layer, Flurstücks-Layer und BROR-Layer
ins Projekt eingefügt, Styles angewendet und die UI-Elemente aktualisiert.
"""
self._set_busy(self.btn_verf_alkis, True, "Aus ALKIS laden")
try:
enriched_layer, flst_layer, bror_layer, status = verfahrensgebiet_alkis_komplett(self)
if status == LoadStatus.NONE or not enriched_layer:
set_verfahrensgebiet_layer_id("")
return
project = QgsProject.instance()
# Flurstücks-Layer ins Projekt (sofern neu geladen)
if flst_layer and flst_layer.isValid():
existing_flst = project.mapLayersByName(flst_layer.name())
if not existing_flst:
project.addMapLayer(flst_layer)
apply_style(flst_layer, "GIS_Flst_Beschriftung_ALKIS_NAS.qml")
self.setze_haken(self.haken_flurst, True)
# BROR-Layer: einfache Darstellung setzen (roter Umring, transparent)
if bror_layer and bror_layer.isValid():
_setze_bror_stil(bror_layer)
# Enriched VG-Layer ins Projekt (ggf. bereits registriert, z.B. bei "als VG laden")
if not project.mapLayer(enriched_layer.id()):
project.addMapLayer(enriched_layer)
apply_style(enriched_layer, "verfahrensgebiet.qml")
iface.mapCanvas().setExtent(enriched_layer.extent())
iface.mapCanvas().refresh()
self.setze_haken(self.haken_verf, True)
self.verf_layer_combo.setLayer(enriched_layer)
self._on_verf_layer_changed(enriched_layer)
msg = "erstmalig geladen" if status == LoadStatus.FIRST else "neu geladen"
success("Verfahrensgebiet", f"Layer wurde {msg} (inkl. Flurstücksverschneidung).", duration=5)
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,
)
# ------------------------------------------------------------------
# Grenzpunkt-Handler
# ------------------------------------------------------------------
def handle_grenz_alkis(self) -> None:
"""Lädt Grenzpunkte gemarkungsweise aus ALKIS.
Verwendet denselben Gemarkungsfilter wie "Verfahrensgebiet aus ALKIS laden":
Die Gemarkungsschlüssel werden per Spatial-Join aus der Geometrie des
aktuell gewählten Verfahrensgebiet-Layers abgeleitet.
"""
# ------------------------------------------------------------------ #
# Schritt 0: Fortschrittsdialog sofort anzeigen #
# ------------------------------------------------------------------ #
# 4 Vorbereitungsschritte (Geometrie, Gemarkungen, Hits, Laden) –
# die genaue Schrittzahl wird in grenzpunkte_gemarkungsweise verfeinert.
prog = create_progress_dialog(
4, "Grenzpunkte aus ALKIS laden", "Verfahrensgebiet prüfen …"
)
prog.set_value(0)
# Verfahrensgebiet-Layer aus dem Dropdown holen
vg_layer = self.verf_layer_combo.currentLayer()
if not vg_layer or not vg_layer.isValid():
prog.close()
warning(
"Grenzpunkte",
"Kein Verfahrensgebiet-Layer gewählt. Bitte zuerst ein Verfahrensgebiet laden.",
duration=6,
)
return
# Geometrie des ersten Features als Suchgeometrie
feats = list(vg_layer.getFeatures())
if not feats:
prog.close()
warning("Grenzpunkte", "Verfahrensgebiet-Layer enthält keine Features.", duration=5)
return
# Geometrie über alle Features vereinen
from qgis.core import QgsGeometry # type: ignore[import]
vg_geom = QgsGeometry.unaryUnion([f.geometry() for f in feats if f.hasGeometry()])
if not vg_geom or vg_geom.isEmpty():
prog.close()
warning("Grenzpunkte", "Verfahrensgebiet-Layer hat keine Geometrie.", duration=5)
return
prog.set_label("Gemarkungsschlüssel per WFS ermitteln …")
prog.set_value(1)
if prog.is_canceled():
return
# Gemarkungsschlüssel per Spatial-Join ermitteln (identisch zu VG-Workflow)
schluessel_namen = gemarkung_schluessel_und_namen_von_vg_geometrie(vg_geom)
schluessel_liste = [nr for nr, _ in schluessel_namen]
if not schluessel_liste:
prog.close()
warning(
"Grenzpunkte",
"Keine Gemarkungen für das Verfahrensgebiet gefunden. "
"Bitte Netzwerkverbindung prüfen.",
duration=6,
)
return
prog.set_label(f"{len(schluessel_liste)} Gemarkung(en) gefunden – Grenzpunkte laden …")
prog.set_value(2)
if prog.is_canceled():
return
self._set_busy(self.btn_grenz_alkis, True, "Grenzpunkte aus ALKIS laden")
prog.close() # Vorbereitungs-Dialog schließen; grenzpunkte_gemarkungsweise öffnet eigenen
try:
joined_layer = grenzpunkte_gemarkungsweise(self, schluessel_liste, vg_geom=vg_geom)
if joined_layer and joined_layer.isValid():
iface.mapCanvas().refresh()
self.setze_haken(self.haken_grenz, True)
success(
"Grenzpunkte",
f"Grenzpunkte erfolgreich geladen ({joined_layer.featureCount()} Objekte).",
duration=5,
)
finally:
self._set_busy(self.btn_grenz_alkis, False, "Grenzpunkte aus ALKIS laden")
def handle_umringstatistik(self) -> None:
"""Klassifiziert Grenzpunkte auf dem VG-Umring nach Lagegenauigkeit.
Sucht den vorhandenen ``Grenzpunkte (ALKIS)``-Layer im Projekt und
kombiniert ihn mit dem gewählten Verfahrensgebiet-Layer, um die
Umringstatistik zu berechnen.
"""
# 1. Grenzpunkte-Layer aus dem Projekt holen
project = QgsProject.instance()
gp_layers = project.mapLayersByName(JOINED_LAYER_NAME)
if not gp_layers:
warning(
"Umringstatistik",
f"Kein Layer '{JOINED_LAYER_NAME}' im Projekt gefunden. "
"Bitte zuerst 'Grenzpunkte aus ALKIS laden' ausführen.",
duration=8,
)
return
grenzpunkte_layer = gp_layers[0]
# 2. Verfahrensgebiet-Layer aus Dropdown
vg_layer = self.verf_layer_combo.currentLayer()
if not vg_layer or not vg_layer.isValid():
warning(
"Umringstatistik",
"Kein Verfahrensgebiet-Layer gewählt. Bitte zuerst ein Verfahrensgebiet laden.",
duration=6,
)
return
self._set_busy(self.btn_umring_statistik, True, "Umringstatistik Grenzpunkte")
try:
result_layer = umringstatistik_grenzpunkte(self, grenzpunkte_layer, vg_layer)
if result_layer and result_layer.isValid():
iface.mapCanvas().refresh()
finally:
self._set_busy(self.btn_umring_statistik, False, "Umringstatistik Grenzpunkte")
# ---------------------------------------------------------------------------
# Modul-Hilfsfunktionen
# ---------------------------------------------------------------------------
def _setze_bror_stil(layer) -> None:
"""Setzt eine einfache Darstellung für den BauRaumOrdnungsrecht-Layer.
Roter Umring, halbtransparente Füllung; kein QGIS-Kontext → stilles Ignorieren.
Parameters
----------
layer :
BauRaumOrdnungsrecht-Memory-Layer (Polygon).
"""
try:
from qgis.core import ( # type: ignore[import]
QgsSimpleFillSymbolLayer,
QgsFillSymbol,
QgsSingleSymbolRenderer,
)
from qgis.PyQt.QtGui import QColor # type: ignore[import]
from qgis.PyQt.QtCore import Qt # type: ignore[import]
fill = QgsSimpleFillSymbolLayer()
fill.setColor(QColor(220, 50, 50, 40)) # halbtransparent rot
fill.setStrokeColor(QColor(220, 50, 50))
fill.setStrokeWidth(0.8)
symbol = QgsFillSymbol([fill])
layer.setRenderer(QgsSingleSymbolRenderer(symbol))
layer.triggerRepaint()
except Exception:
pass # Kein QGIS-Kontext (Tests, Mock-Modus) → Standarddarstellung