Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a906d3ac7 | ||
|
|
39f8209b9d | ||
|
|
3c0e6e5a19 | ||
|
|
c93776d19e | ||
|
|
70c14783c5 | ||
|
|
b4f2222b50 | ||
|
|
8447a2615c | ||
|
|
4e7f796840 | ||
|
|
63a70737f9 | ||
|
|
d5db43bc57 | ||
|
|
7b48d29d56 | ||
|
|
5816b2af22 | ||
|
|
148d4dfa15 | ||
|
|
6a65c9d706 | ||
|
|
1a2a22f931 | ||
|
|
4a557e66a7 | ||
|
|
6bc5256d5f | ||
|
|
2cb6193b98 | ||
|
|
e6332e42ad | ||
|
|
2cf0cda199 | ||
|
|
fd54186d62 | ||
|
|
6e0bd213de | ||
|
|
f37f33584d |
@@ -1,3 +1,19 @@
|
||||
- Fortschrittsdialog beim Laden aus ALKIS schließt jetzt zuverlässig bei jedem Abbruch (Bugfix)
|
||||
- Fortschrittsanzeige beim Laden aus ALKIS in zwei Dialoge aufgeteilt (Suche / eigentlicher Ladevorgang), inkl. eigenem Suchschritt
|
||||
- Mehrfachauswahl bei mehreren BauRaumOderBodenordnungsrecht-Treffern zur Verfahrensnummer (gemeinsames Laden/Verschmelzen mehrerer Objekte)
|
||||
- Neu: Laden aller/mehrerer Objekte eines ganzen Landkreises als Fallback, wenn keine Verfahrensnummer gesetzt ist (Gruppierung je Verfahren, optional "Nur laden" ohne weiteren Ablauf)
|
||||
- Neu: Ist weder Verfahrensnummer noch Landkreis in den Projekteigenschaften gesetzt, fragt ein Dialog den Landkreis ab (inkl. Option "Alle Landkreise"), statt direkt abzubrechen
|
||||
- Landkreisweiter Objekt-Auswahldialog überarbeitet: Tabelle mit sortierbaren Spalten "Nummer"/"Name" (Standard: aufsteigend nach Nummer), Suchfeld, "Alle auswählen" jetzt korrekt (vorheriger Qt-Bug bei programmatischer Mehrfachauswahl behoben)
|
||||
- Bugfix: BROR-Layer bekommt seinen ALKIS-Style jetzt direkt beim Laden (auch bei "Nur Objekte laden") statt später fälschlich den Verfahrensgebiet-Style zu erhalten – **Detailprüfung/Verifikation im echten QGIS-Test steht noch aus**
|
||||
- Bugfix: Erfolgsmeldung "Layer wurde ... geladen (inkl. Flurstücksverschneidung)" erscheint nicht mehr fälschlich bei "Nur Objekte laden" (da dort keine Verschneidung stattfindet)
|
||||
- Verfahrensgebiet-spezifische ALKIS-Dialoge aus sn_basis nach sn_verfahrensgebiet verschoben (functions/verfahrensgebiet_dialoge.py)
|
||||
---
|
||||
Version 26.8.1-unstable:
|
||||
- Neu: "Beteiligung prüfen" – Abgleich einer BI-Server-CSV-Exportliste mit dem ALKIS-Flurstücksbestand des Verfahrensgebiets (try1: schneller Listenabgleich bei vorhandenem Kennzeichen-Bestand; try2/try3: Aufforderung, das Verfahrensgebiet zunächst über "Aus ALKIS laden" zu laden, statt automatischem Hintergrund-Laden)
|
||||
---
|
||||
Version 26.7.1-unstable:
|
||||
- Verfahrensgebiet als shape laden, inkl. Abgleich mit BROBOR-Objekt
|
||||
---
|
||||
- Knickpunktprüfung "fehlende Knickpunkte" auf Extraktion der Verbindungslinien zwischen bekannten Punktorten umgestellt
|
||||
- Umringsdetektion und Ladepuffer Punktort_TA als Layer im Projekt bei der Knickpunktprüfung ergänzt
|
||||
- Verfahrensgebiet-Bildung mit korrigierten Knickpunkten (keine manuelle Korrektur der Flurstückszugehörigkeit mehr erforderlich)
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
# Ablauf: Aus ALKIS laden
|
||||
|
||||
Betrachteter Teilablauf: nur bis zum Erzeugen des Layers
|
||||
"BauRaumOderBodenordnungsrecht (ALKIS)". Die anschließenden Schritte des
|
||||
vollständigen Workflows (Gemarkungen, Flurstücke, Flächenvergleich,
|
||||
Detailprüfung) sind hier bewusst nicht dargestellt.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start(["Aus ALKIS laden"])
|
||||
B0{"Verfahrensnummer oder Landkreis in Projekteigenschaften gesetzt?"}
|
||||
LKAUSW["Landkreis auswählen (Dialog)"]
|
||||
Abbruch(["Abbruch"])
|
||||
MODUS{"Verfahrensnummer gesetzt?"}
|
||||
EXIST1{"Layer 'Verfahrensgebiet' bereits vorhanden?"}
|
||||
EXIST2{"Layer 'Verfahrensgebiet' bereits vorhanden?"}
|
||||
Keep[("Verfahrensgebiet (bestehender Layer)")]
|
||||
WFS1["WFS-Suche per Verfahrensnummer"]
|
||||
WFS2["WFS-Suche per Landkreis"]
|
||||
C1{"Mehrere Objekt-Kombinationen gefunden?"}
|
||||
AUSW1["Objektauswahl"]
|
||||
AUSW2["Objektauswahl"]
|
||||
BUILD["Layer erzeugen"]
|
||||
Ende[("BauRaumOderBodenordnungsrecht (ALKIS)")]
|
||||
|
||||
Start --> B0
|
||||
B0 -- Nein --> LKAUSW
|
||||
LKAUSW -- Abbruch --> Abbruch
|
||||
LKAUSW -- OK --> MODUS
|
||||
B0 -- Ja --> MODUS
|
||||
|
||||
MODUS -- Ja --> EXIST1
|
||||
MODUS -- Nein --> EXIST2
|
||||
|
||||
EXIST1 -- "Ja, behalten" --> Keep
|
||||
EXIST2 -- "Ja, behalten" --> Keep
|
||||
|
||||
EXIST1 -- "Nein / neu laden" --> WFS1
|
||||
EXIST2 -- "Nein / neu laden" --> WFS2
|
||||
|
||||
WFS1 --> C1
|
||||
C1 -- Ja --> AUSW1
|
||||
C1 -- Nein --> BUILD
|
||||
AUSW1 --> BUILD
|
||||
|
||||
WFS2 --> AUSW2
|
||||
AUSW2 --> BUILD
|
||||
|
||||
BUILD --> Ende
|
||||
```
|
||||
|
||||
Hinweise zu den Entscheidungsknoten:
|
||||
|
||||
- **B0**: Es werden die Projektvariablen `verfahrensnummer` und
|
||||
`landkreis_kennzahl` gemeinsam geprüft. Nur wenn **beide** fehlen, wird der
|
||||
Landkreis per Dialog abgefragt.
|
||||
- **MODUS**: Ist eine Verfahrensnummer gesetzt, wird über die Verfahrensnummer
|
||||
gesucht (auch wenn zusätzlich ein Landkreis gesetzt ist); andernfalls über
|
||||
den Landkreis.
|
||||
- **EXIST1/EXIST2**: Existiert bereits ein Layer "Verfahrensgebiet" und lehnt
|
||||
der Nutzer das Neuladen ab, wird direkt dieser vorhandene Layer verwendet –
|
||||
ohne erneute WFS-Suche oder Objektauswahl.
|
||||
- **C1 (nur im Verfahrensnummer-Zweig)**: Die Objektauswahl erscheint nur,
|
||||
wenn der WFS mehrere unterschiedliche (adv_name, bezeichnung)-Kombinationen
|
||||
liefert. Bei genau einem Treffer wird der Layer ohne Nutzerinteraktion
|
||||
erzeugt.
|
||||
- **Landkreis-Zweig**: Hier erscheint die Objektauswahl immer (inkl.
|
||||
"Alle auswählen"-Option), da hier potenziell deutlich mehr Treffer als bei
|
||||
der Verfahrensnummer-Suche auftreten.
|
||||
@@ -0,0 +1,20 @@
|
||||
# Ablauf: Aus Shape laden
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start(["Aus Shape laden"])
|
||||
A0{"Datei öffnen?"}
|
||||
A2["entpacken"]
|
||||
A3["Inhalt prüfen"]
|
||||
|
||||
Ende1[("Layer:</br>Neuordnungsgebiet (NoBIS)")]
|
||||
Ende2[("Layer:</br>Neuvermessungsgebiet (NoBIS)")]
|
||||
|
||||
Start --> A0
|
||||
A0 -- Shape --> A3
|
||||
A0 -- ZIP --> A2
|
||||
A2 --> A3
|
||||
A3 -- Neuordnungsgebiet --> Ende1
|
||||
A3 -- Neuvermessungsgebiet --> Ende2
|
||||
|
||||
```
|
||||
@@ -0,0 +1,127 @@
|
||||
# Technischer Ablauf: Aus Shape laden (bis zum Stop)
|
||||
|
||||
Ergänzung zu [aus_shape_laden.md](aus_shape_laden.md), die den fachlichen
|
||||
Ablauf in Kurzform beschreibt. Dieses Dokument zeigt den tatsächlichen
|
||||
technischen Ablauf in `lade_vg_aus_zip()` und dem anschließenden Stop in
|
||||
`verfahrensgebiet_shape_komplett()` (Datei
|
||||
`functions/verfahrensgebiet_shape_komplett.py`).
|
||||
|
||||
Betrachteter Teilablauf: nur bis zum Stop nach dem Einfügen der Layer ins
|
||||
Projekt. Die anschließenden Schritte (Gemarkung, Flurstücke,
|
||||
Flächenvergleich, Detailprüfung) sind hier nicht dargestellt.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start(["Aus Shape laden"])
|
||||
DLG["Dateidialog (Mehrfachauswahl):<br/>.zip und/oder .shp"]
|
||||
ABBRUCH1(["Abbruch (keine Auswahl)"])
|
||||
|
||||
SAMMEL["Für jede ausgewählte Datei:"]
|
||||
ISZIP{"Endet auf .shp?"}
|
||||
DIREKT["Direkt als Shapefile-Eintrag übernehmen"]
|
||||
ENTPACK["In temporäres Verzeichnis entpacken"]
|
||||
ENTPACKFEHLER{"Entpacken erfolgreich?"}
|
||||
FEHLER_ZIP["Fehler notieren:<br/>'ZIP konnte nicht entpackt werden'"]
|
||||
FINDEALL["Alle .shp-Dateien im<br/>entpackten Verzeichnis suchen"]
|
||||
KEINSHP{"Mind. eine .shp gefunden?"}
|
||||
FEHLER_KEIN_SHP["Fehler notieren:<br/>'kein gültiges Shape-Dateiset'"]
|
||||
ALS_EINTRAG["Jede gefundene .shp als<br/>eigenen Eintrag übernehmen"]
|
||||
|
||||
PROJEDEN["Für jeden Shapefile-Eintrag:"]
|
||||
LOAD["Als QgsVectorLayer laden"]
|
||||
VALID{"isValid()?"}
|
||||
FEHLER_INVALID["Fehler notieren:<br/>'kein gültiges Shape-Dateiset'"]
|
||||
FBSCHL{"Feld 'FBSchl' vorhanden<br/>und Wert bekannt?"}
|
||||
FEHLER_FBSCHL["Fehler notieren:<br/>Feld fehlt / unbekannter Wert"]
|
||||
TYP{"FBSchl-Wert"}
|
||||
ALS_NEUORDNUNG["Geometrien zu<br/>'Neuordnungsgebiet' sammeln"]
|
||||
ALS_NEUVERMESSUNG["Geometrien zu<br/>'Neuvermessungsgebiet' sammeln"]
|
||||
|
||||
NACHSCHLEIFE["Nach Verarbeitung aller Einträge"]
|
||||
FEHLERVORHANDEN{"Fehler gesammelt?"}
|
||||
SAMMELMELDUNG["Sammelmeldung anzeigen:<br/>alle fehlerhaften Dateien"]
|
||||
NICHTSVERWERTBAR{"Mind. ein Typ mit<br/>gültigen Geometrien?"}
|
||||
ABBRUCH2(["Abbruch (None)"])
|
||||
|
||||
MERGE["Je Gebietstyp:<br/>unaryUnion aller gesammelten Geometrien<br/>→ ein Memory-Layer je Typ"]
|
||||
BENENNEN["Layer benennen:<br/>'Neuordnungsgebiet (NoBIS)' /<br/>'Neuvermessungsgebiet (NoBIS)'"]
|
||||
RETURN["Liste der Layer zurückgeben<br/>(1 oder 2 Layer)"]
|
||||
|
||||
EINFUEGEN["Alle Layer ins Projekt einfügen"]
|
||||
STOP{{"STOP:<br/>'Layer ... wurde(n) geladen. Weiter?'"}}
|
||||
WEITER(["weiter mit Gemarkung/Flurstücke/…"])
|
||||
ABBRUCH3(["Abbruch, Layer bleiben im Projekt"])
|
||||
|
||||
Start --> DLG
|
||||
DLG -- keine Auswahl --> ABBRUCH1
|
||||
DLG -- Auswahl --> SAMMEL
|
||||
SAMMEL --> ISZIP
|
||||
ISZIP -- Ja --> DIREKT
|
||||
ISZIP -- Nein --> ENTPACK
|
||||
ENTPACK --> ENTPACKFEHLER
|
||||
ENTPACKFEHLER -- Nein --> FEHLER_ZIP
|
||||
ENTPACKFEHLER -- Ja --> FINDEALL
|
||||
FINDEALL --> KEINSHP
|
||||
KEINSHP -- Nein --> FEHLER_KEIN_SHP
|
||||
KEINSHP -- Ja --> ALS_EINTRAG
|
||||
|
||||
DIREKT --> PROJEDEN
|
||||
ALS_EINTRAG --> PROJEDEN
|
||||
FEHLER_ZIP -.->|nächste Datei| SAMMEL
|
||||
FEHLER_KEIN_SHP -.->|nächste Datei| SAMMEL
|
||||
|
||||
PROJEDEN --> LOAD
|
||||
LOAD --> VALID
|
||||
VALID -- Nein --> FEHLER_INVALID
|
||||
VALID -- Ja --> FBSCHL
|
||||
FBSCHL -- Nein --> FEHLER_FBSCHL
|
||||
FBSCHL -- Ja --> TYP
|
||||
TYP -- "901/10110F" --> ALS_NEUORDNUNG
|
||||
TYP -- "901/10120F" --> ALS_NEUVERMESSUNG
|
||||
FEHLER_INVALID -.->|nächstes Shapefile| PROJEDEN
|
||||
FEHLER_FBSCHL -.->|nächstes Shapefile| PROJEDEN
|
||||
ALS_NEUORDNUNG -.->|nächstes Shapefile| PROJEDEN
|
||||
ALS_NEUVERMESSUNG -.->|nächstes Shapefile| PROJEDEN
|
||||
|
||||
PROJEDEN --> NACHSCHLEIFE
|
||||
NACHSCHLEIFE --> FEHLERVORHANDEN
|
||||
FEHLERVORHANDEN -- Ja --> SAMMELMELDUNG
|
||||
FEHLERVORHANDEN -- Nein --> NICHTSVERWERTBAR
|
||||
SAMMELMELDUNG --> NICHTSVERWERTBAR
|
||||
NICHTSVERWERTBAR -- Nein --> ABBRUCH2
|
||||
NICHTSVERWERTBAR -- Ja --> MERGE
|
||||
MERGE --> BENENNEN
|
||||
BENENNEN --> RETURN
|
||||
|
||||
RETURN --> EINFUEGEN
|
||||
EINFUEGEN --> STOP
|
||||
STOP -- Ja --> WEITER
|
||||
STOP -- Nein --> ABBRUCH3
|
||||
```
|
||||
|
||||
## Hinweise
|
||||
|
||||
- **Mehrfachauswahl**: Es können mehrere Dateien gleichzeitig gewählt werden,
|
||||
auch gemischt `.zip` und `.shp`.
|
||||
- **ZIP mit mehreren Shapefile-Sets**: Enthält ein ZIP mehrere `.shp`-Dateien,
|
||||
wird jede einzeln geprüft und verarbeitet – wie ein eigener Eintrag in der
|
||||
Auswahl.
|
||||
- **Fehler pro Datei, nicht pro Vorgang**: Schlägt die Prüfung einer
|
||||
einzelnen Datei/eines einzelnen Shapefiles fehl (ZIP nicht entpackbar,
|
||||
kein Shapefile enthalten, ungültiger Layer, Feld `FBSchl` fehlt oder hat
|
||||
einen unbekannten Wert), wird nur dieser Eintrag übersprungen. Alle
|
||||
anderen werden weiterverarbeitet. Am Ende erscheint eine **gesammelte**
|
||||
Fehlermeldung mit allen fehlerhaften Einträgen – Abbruch (`None`) nur,
|
||||
wenn *kein* Eintrag verwertbare Geometrien geliefert hat.
|
||||
- **Zusammenführen gleicher Gebietstypen**: Alle Geometrien mit demselben
|
||||
`FBSchl`-Typ (über alle Dateien/Shapefiles hinweg) werden per
|
||||
`QgsGeometry.unaryUnion` zu **einem** Memory-Layer verschmolzen – nicht
|
||||
ein Layer pro Datei.
|
||||
- **Temporäre Verzeichnisse**: Jedes entpackte ZIP landet in einem eigenen
|
||||
`tempfile.mkdtemp()`-Verzeichnis, das nach der Verarbeitung (auch im
|
||||
Fehlerfall) wieder gelöscht wird.
|
||||
- **Stop-Punkt**: Nach dem Einfügen aller erzeugten Layer ins Projekt fragt
|
||||
ein Ja/Nein-Dialog, ob mit dem restlichen Workflow (Gemarkung,
|
||||
Flurstücke, Flächenvergleich, Detailprüfung) fortgefahren werden soll.
|
||||
Bei "Nein" bleiben die geladenen Layer im Projekt erhalten, der weitere
|
||||
Ablauf wird aber nicht gestartet.
|
||||
@@ -0,0 +1,452 @@
|
||||
"""sn_verfahrensgebiet/functions/beteiligung_pruefung.py – Beteiligung prüfen.
|
||||
|
||||
AGLB-länderspezifisch (Flurstückskennzeichen-Bildung nach sächsischem ALKIS-
|
||||
Schema). Die Länderkennung wird nicht hartcodiert, sondern über
|
||||
:func:`sn_basis.functions.laenderkennung.get_laenderkennung` ermittelt, damit
|
||||
Schwesterplugins für andere Bundesländer dieselbe Logik mit einer anderen
|
||||
Länderkennung wiederverwenden können.
|
||||
|
||||
Gleicht eine CSV-Exportliste des BI-Servers (Kopfzeile
|
||||
``VEF_VKZF,FLA_Beteiligung,FLURSTUECK``) mit dem Verfahrensgebiet-Bestand
|
||||
ab. Aus ``FLURSTUECK`` (Format ``<Gemarkung>/<Zähler>/<Nenner>/<Folge>``) wird
|
||||
je Zeile ein zu ALKIS kompatibles Flurstückskennzeichen ``Flurstueck_csv``
|
||||
gebildet.
|
||||
|
||||
Kaskade je nach Projektzustand:
|
||||
|
||||
- **try1**: Ein Layer ``Verfahrensgebiet`` mit befülltem Attribut
|
||||
``flurstueckskennzeichen`` (Single-Feature-Modell, siehe
|
||||
:mod:`verfahrensgebiet_edit_service`) ist vorhanden → schneller
|
||||
Mengenvergleich ohne WFS-Zugriff.
|
||||
- **try2**: Ein Verfahrensgebiet-Layer ist vorhanden, aber ohne verwertbare
|
||||
Kennzeichen → statt einer automatischen (nutzerlosen) ALKIS-Ladepipeline
|
||||
wird der Nutzer per Meldung aufgefordert, das Verfahrensgebiet zunächst
|
||||
regulär über "Aus ALKIS laden" neu zu laden und die Beteiligungsprüfung
|
||||
danach erneut zu starten (landet dann automatisch bei try1). Ein
|
||||
automatischer Durchlauf hat sich in der Praxis nicht als zuverlässig genug
|
||||
erwiesen (mehrdeutige BROR-Treffer, Flächenabweichungen etc. lassen sich
|
||||
nur im interaktiven Ablauf sinnvoll auflösen).
|
||||
- **try3**: Kein Verfahrensgebiet-Layer im Projekt → Verfahrensnummer aus
|
||||
``VEF_VKZF`` wird als Projektvariable übernommen, danach wie try2 (Meldung
|
||||
statt automatischem Laden).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
from sn_basis.functions import QgsVectorLayer, QgsProject, QgsFeature, QgsField
|
||||
from sn_basis.functions.qt_wrapper import QFileDialog, QVariant
|
||||
from sn_basis.functions.variable_wrapper import set_variable
|
||||
from sn_basis.functions.laenderkennung import get_laenderkennung
|
||||
from sn_basis.functions.dialog_wrapper import create_progress_dialog
|
||||
|
||||
from sn_verfahrensgebiet.functions.flurstueck_alkis import FLST_LAYER_NAME
|
||||
from sn_verfahrensgebiet.functions.verfahrensgebiet_edit_service import (
|
||||
VERF_LAYER_NAME,
|
||||
get_kennzeichen_menge,
|
||||
)
|
||||
|
||||
CSV_SPALTE_FLURSTUECK = "FLURSTUECK"
|
||||
CSV_SPALTE_VEF_VKZF = "VEF_VKZF"
|
||||
CSV_SPALTE_BETEILIGUNG = "FLA_Beteiligung"
|
||||
|
||||
FELD_FLURSTUECK_CSV = "Flurstueck_csv"
|
||||
FELD_FLURSTUECKSKENNZEICHEN_WFS = "flurstueckskennzeichen"
|
||||
|
||||
ERGEBNIS_LAYER_VERGLEICH = "Beteiligung – Vergleich"
|
||||
|
||||
WERT_BEIDE = "beide"
|
||||
WERT_NUR_VERFAHRENSGEBIET = "nur_verfahrensgebiet"
|
||||
WERT_NUR_AGLB = "nur_aglb"
|
||||
|
||||
|
||||
class BeteiligungPruefungFehler(Exception):
|
||||
"""Erwarteter, dem Nutzer per Messagebox mitzuteilender Fehlerfall."""
|
||||
|
||||
|
||||
@dataclass
|
||||
class BeteiligungErgebnis:
|
||||
"""Ergebnis eines Prüflaufs, das die aufrufende UI ins Projekt einfügt."""
|
||||
|
||||
modus: str
|
||||
layers: List[Tuple[object, Optional[str]]] = field(default_factory=list)
|
||||
statistik: Dict[str, int] = field(default_factory=dict)
|
||||
statistik_text: str = ""
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CSV laden
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def lade_beteiligung_csv(parent_widget) -> Optional[Tuple[List[dict], List[str]]]:
|
||||
"""Öffnet Dateidialog (CSV-Filter) und liest die BI-Server-Exportliste.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Optional[Tuple[List[dict], List[str]]]
|
||||
``(zeilen, spaltennamen)`` oder ``None`` bei Nutzerabbruch.
|
||||
|
||||
Raises
|
||||
------
|
||||
BeteiligungPruefungFehler
|
||||
Bei ungültigem CSV-Format oder fehlendem Attribut ``FLURSTUECK``.
|
||||
"""
|
||||
pfad, _ = QFileDialog.getOpenFileName(
|
||||
parent_widget,
|
||||
"CSV-Export aus BI-Server auswählen",
|
||||
"",
|
||||
"CSV-Dateien (*.csv)",
|
||||
)
|
||||
if not pfad:
|
||||
return None # Nutzerabbruch
|
||||
|
||||
try:
|
||||
with open(pfad, "r", encoding="utf-8-sig", newline="") as f:
|
||||
reader = csv.DictReader(f, delimiter=",")
|
||||
zeilen = list(reader)
|
||||
spaltennamen = reader.fieldnames or []
|
||||
except (OSError, csv.Error, UnicodeDecodeError) as exc:
|
||||
raise BeteiligungPruefungFehler(
|
||||
f"CSV-Format ungültig – Datei konnte nicht gelesen werden: {exc}"
|
||||
) from exc
|
||||
|
||||
if not spaltennamen or not zeilen:
|
||||
raise BeteiligungPruefungFehler(
|
||||
"CSV-Format ungültig – die Datei ist leer oder enthält keine Kopfzeile."
|
||||
)
|
||||
|
||||
if CSV_SPALTE_FLURSTUECK not in spaltennamen:
|
||||
raise BeteiligungPruefungFehler(
|
||||
f"Die CSV enthält nicht das benötigte Attribut '{CSV_SPALTE_FLURSTUECK}'."
|
||||
)
|
||||
|
||||
return zeilen, spaltennamen
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Flurstueck_csv-Bildung
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
#: Alte CSV-Exporte führen Buchstaben-Nenner (ALKIS: a, b, c, ...) teils als
|
||||
#: numerischen Platzhalter 9001, 9002, ... statt als Buchstabe. 9001 → "a",
|
||||
#: 9002 → "b" usw. (Basiswert, auf den 1..26 für a..z addiert wird).
|
||||
_NENNER_BUCHSTABEN_BASIS = 9000
|
||||
|
||||
|
||||
def _baue_nenner_kennzeichen(nenner: str) -> str:
|
||||
"""Bildet den 4-stelligen Nenner-Teil des Flurstückskennzeichens.
|
||||
|
||||
Normalfall: numerischer Nenner, 4-stellig mit führenden Nullen
|
||||
aufgefüllt; Nenner 0/leer → Platzhalter ``"____"``.
|
||||
|
||||
Format-Inkonsistenz alter CSV-Exporte: ein Buchstaben-Nenner (z.B. "a")
|
||||
wird dort als ``9001`` (a), ``9002`` (b), usw. geführt. Wird ein solcher
|
||||
Platzhalterwert erkannt, wird stattdessen der Buchstabe selbst
|
||||
(4-stellig mit führenden Nullen aufgefüllt, z.B. ``"000a"``) verwendet,
|
||||
damit das Kennzeichen mit dem ALKIS-Bestand übereinstimmt.
|
||||
"""
|
||||
nenner = (nenner or "").strip()
|
||||
nenner_ohne_null = nenner.lstrip("0")
|
||||
if not nenner_ohne_null:
|
||||
return "____"
|
||||
|
||||
if nenner_ohne_null.isdigit():
|
||||
zahl = int(nenner_ohne_null)
|
||||
if _NENNER_BUCHSTABEN_BASIS < zahl <= _NENNER_BUCHSTABEN_BASIS + 26:
|
||||
buchstabe = chr(ord("a") + (zahl - _NENNER_BUCHSTABEN_BASIS - 1))
|
||||
return buchstabe.zfill(4)
|
||||
|
||||
return nenner.zfill(4)
|
||||
|
||||
|
||||
def baue_flurstueck_csv(flurstueck_wert: str, laenderkennung: str) -> Optional[str]:
|
||||
"""Bildet aus dem CSV-Attribut ``FLURSTUECK`` ein ALKIS-Flurstückskennzeichen.
|
||||
|
||||
Das Attribut hat das Format
|
||||
``<Gemarkungsnummer>/<Zähler>/<Nenner>/<Folge>``. Die Flurnummer ist in der
|
||||
CSV nicht enthalten und wird als fixer Platzhalter ``___`` geführt (so wie
|
||||
ALKIS unbesetzte Kennzeichen-Bestandteile selbst mit Unterstrichen füllt).
|
||||
#Für Länderanpassung dieses Verhalten ändern, so dass die Flurnummer extrahiert und berücksichtigt wird, wenn angegeben
|
||||
|
||||
Returns
|
||||
-------
|
||||
Optional[str]
|
||||
Das zusammengesetzte Kennzeichen, oder ``None`` bei ungültigem Format
|
||||
(nicht genau 4 durch ``/`` getrennte Teile).
|
||||
"""
|
||||
if not flurstueck_wert:
|
||||
return None
|
||||
|
||||
teile = [t.strip() for t in flurstueck_wert.strip().split("/")]
|
||||
if len(teile) != 4:
|
||||
return None
|
||||
|
||||
gemarkung, zaehler, nenner, folge = teile
|
||||
|
||||
gemarkung_kz = gemarkung.zfill(4)
|
||||
flur_kz = "___"
|
||||
zaehler_kz = zaehler.zfill(5)
|
||||
nenner_kz = _baue_nenner_kennzeichen(nenner)
|
||||
folge_kz = folge.zfill(2)
|
||||
|
||||
return f"{laenderkennung}{gemarkung_kz}{flur_kz}{zaehler_kz}{nenner_kz}{folge_kz}"
|
||||
|
||||
|
||||
def baue_flurstueck_csv_liste(zeilen: List[dict], laenderkennung: str) -> List[str]:
|
||||
"""Bildet für alle CSV-Zeilen ``Flurstueck_csv`` und gibt die Liste zurück.
|
||||
|
||||
Schreibt das Ergebnis zusätzlich als Attribut ``Flurstueck_csv`` in jede
|
||||
Zeile zurück und gibt die ersten 20 Einträge als DEBUG-Ausgabe aus.
|
||||
"""
|
||||
ergebnisse: List[str] = []
|
||||
for zeile in zeilen:
|
||||
wert = zeile.get(CSV_SPALTE_FLURSTUECK, "")
|
||||
fkz = baue_flurstueck_csv(wert, laenderkennung)
|
||||
zeile[FELD_FLURSTUECK_CSV] = fkz or ""
|
||||
if fkz:
|
||||
ergebnisse.append(fkz)
|
||||
|
||||
print(f">>> Flurstueck_csv – {len(ergebnisse)} gebildet, erste 20:")
|
||||
for wert in ergebnisse[:20]:
|
||||
print(f">>> {wert}")
|
||||
|
||||
return ergebnisse
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Hilfsfunktionen: Layer-Lookup
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _hole_verfahrensgebiet_layer(tab_widget) -> Optional[object]:
|
||||
"""Liefert den Verfahrensgebiet-Layer, falls vorhanden.
|
||||
|
||||
Der per Name (``VERF_LAYER_NAME``) identifizierte Layer hat Vorrang vor
|
||||
der Dropdown-Auswahl im Tab: try1/try2/try3 orientieren sich am
|
||||
tatsächlichen "Verfahrensgebiet"-Bestand im Projekt, nicht an einer
|
||||
ggf. anderweitig belegten Nutzerauswahl (z.B. für Grenzpunkte/
|
||||
Umringstatistik). Die Dropdown-Auswahl dient nur als Fallback, falls
|
||||
kein so benannter Layer existiert.
|
||||
"""
|
||||
treffer = QgsProject.instance().mapLayersByName(VERF_LAYER_NAME)
|
||||
if treffer:
|
||||
return treffer[0]
|
||||
|
||||
combo = getattr(tab_widget, "verf_layer_combo", None)
|
||||
if combo is not None:
|
||||
layer = combo.currentLayer()
|
||||
if layer is not None and layer.isValid():
|
||||
return layer
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _hat_verwendbare_kennzeichen(verf_layer) -> bool:
|
||||
try:
|
||||
return bool(get_kennzeichen_menge(verf_layer))
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _hole_flurstuecke_layer() -> Optional[object]:
|
||||
treffer = QgsProject.instance().mapLayersByName(FLST_LAYER_NAME)
|
||||
return treffer[0] if treffer else None
|
||||
|
||||
|
||||
def _flurstueckskennzeichen_geometrien(flst_layer) -> Dict[str, object]:
|
||||
"""Baut ``flurstueckskennzeichen`` → Geometrie-Map aus dem Flurstücke-Layer."""
|
||||
mapping: Dict[str, object] = {}
|
||||
if flst_layer is None:
|
||||
return mapping
|
||||
if flst_layer.fields().indexFromName(FELD_FLURSTUECKSKENNZEICHEN_WFS) < 0:
|
||||
return mapping
|
||||
for feat in flst_layer.getFeatures():
|
||||
try:
|
||||
kz = str(feat[FELD_FLURSTUECKSKENNZEICHEN_WFS] or "").strip()
|
||||
except Exception:
|
||||
kz = ""
|
||||
if kz:
|
||||
mapping[kz] = feat.geometry()
|
||||
return mapping
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# try1: schneller Listenabgleich
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _try1_listenabgleich(
|
||||
tab_widget, verf_layer, flurstueck_csv_liste: List[str]
|
||||
) -> Tuple[object, Dict[str, int]]:
|
||||
progress = create_progress_dialog(
|
||||
3, "Beteiligung prüfen", "Verfahrensgebiet-Kennzeichen einlesen …"
|
||||
)
|
||||
progress.set_value(0)
|
||||
progress.raise_to_front()
|
||||
|
||||
verf_menge = get_kennzeichen_menge(verf_layer)
|
||||
csv_menge = set(flurstueck_csv_liste)
|
||||
progress.set_value(1)
|
||||
|
||||
progress.set_label("CSV-Liste abgleichen …")
|
||||
beide = sorted(verf_menge & csv_menge)
|
||||
nur_verfahrensgebiet = sorted(verf_menge - csv_menge)
|
||||
nur_aglb = sorted(csv_menge - verf_menge)
|
||||
progress.set_value(2)
|
||||
|
||||
progress.set_label("Ergebnislayer erstellen …")
|
||||
flst_layer = _hole_flurstuecke_layer()
|
||||
geom_map = _flurstueckskennzeichen_geometrien(flst_layer)
|
||||
if flst_layer is not None and flst_layer.isValid():
|
||||
crs = flst_layer.crs().authid()
|
||||
elif verf_layer.isValid():
|
||||
crs = verf_layer.crs().authid()
|
||||
else:
|
||||
crs = "EPSG:25833"
|
||||
|
||||
layer = QgsVectorLayer(f"Polygon?crs={crs}", ERGEBNIS_LAYER_VERGLEICH, "memory")
|
||||
pr = layer.dataProvider()
|
||||
pr.addAttributes([
|
||||
QgsField(FELD_FLURSTUECK_CSV, QVariant.String),
|
||||
QgsField("status", QVariant.String),
|
||||
])
|
||||
layer.updateFields()
|
||||
fields = layer.fields()
|
||||
|
||||
neue_features = []
|
||||
for status, menge in (
|
||||
(WERT_BEIDE, beide),
|
||||
(WERT_NUR_VERFAHRENSGEBIET, nur_verfahrensgebiet),
|
||||
(WERT_NUR_AGLB, nur_aglb),
|
||||
):
|
||||
for kz in menge:
|
||||
feat = QgsFeature(fields)
|
||||
geom = geom_map.get(kz)
|
||||
if geom is not None:
|
||||
feat.setGeometry(geom)
|
||||
feat.setAttributes([kz, status])
|
||||
neue_features.append(feat)
|
||||
|
||||
pr.addFeatures(neue_features)
|
||||
layer.updateExtents()
|
||||
|
||||
progress.set_value(3)
|
||||
progress.close()
|
||||
|
||||
statistik = {
|
||||
WERT_BEIDE: len(beide),
|
||||
WERT_NUR_VERFAHRENSGEBIET: len(nur_verfahrensgebiet),
|
||||
WERT_NUR_AGLB: len(nur_aglb),
|
||||
}
|
||||
return layer, statistik
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# try2/try3: Aufforderung statt automatischer ALKIS-Pipeline
|
||||
# ---------------------------------------------------------------------------
|
||||
#
|
||||
# Ein automatischer (nutzerloser) Durchlauf der kompletten Ladepipeline
|
||||
# (mehrdeutige BROR-Treffer, Flächenabweichungen, Detailprüfung) hat sich in
|
||||
# der Praxis nicht als zuverlässig genug erwiesen – deshalb hier bewusst
|
||||
# KEIN silent-Aufruf von verfahrensgebiet_alkis_komplett(), sondern eine
|
||||
# Aufforderung an den Nutzer, den regulären interaktiven Ablauf
|
||||
# ("Aus ALKIS laden") selbst zu starten. Ein erneuter Klick auf
|
||||
# "Beteiligung prüfen" landet danach automatisch bei try1, sobald der Layer
|
||||
# verwertbare Kennzeichen hat.
|
||||
|
||||
def _fordere_manuelles_laden_an(vef_vkzf: Optional[str] = None) -> None:
|
||||
"""Bricht try2/try3 mit einer Aufforderung an den Nutzer ab (statt eines
|
||||
automatischen Ladevorgangs)."""
|
||||
hinweis = (
|
||||
"Bitte laden Sie das Verfahrensgebiet jetzt über 'Aus ALKIS laden' "
|
||||
"und starten Sie die Beteiligungsprüfung danach erneut."
|
||||
)
|
||||
if vef_vkzf:
|
||||
raise BeteiligungPruefungFehler(
|
||||
f"Es ist noch kein Verfahrensgebiet geladen. Die Verfahrensnummer "
|
||||
f"{vef_vkzf} wurde aus der CSV übernommen. {hinweis}"
|
||||
)
|
||||
raise BeteiligungPruefungFehler(
|
||||
f"Der vorhandene Verfahrensgebiet-Layer enthält keine verwertbaren "
|
||||
f"Flurstückskennzeichen. {hinweis}"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# try3: Bootstrap (Verfahrensnummer aus CSV) + Aufforderung wie try2
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _lese_vef_vkzf(zeilen: List[dict], spaltennamen: List[str]) -> str:
|
||||
if CSV_SPALTE_VEF_VKZF not in spaltennamen:
|
||||
raise BeteiligungPruefungFehler(
|
||||
f"Die CSV enthält nicht das benötigte Attribut '{CSV_SPALTE_VEF_VKZF}'."
|
||||
)
|
||||
for zeile in zeilen:
|
||||
wert = (zeile.get(CSV_SPALTE_VEF_VKZF) or "").strip()
|
||||
if wert:
|
||||
return wert
|
||||
raise BeteiligungPruefungFehler(
|
||||
f"Die CSV enthält keinen befüllten Wert für '{CSV_SPALTE_VEF_VKZF}'."
|
||||
)
|
||||
|
||||
|
||||
def _try3_bootstrap(tab_widget, zeilen: List[dict], spaltennamen: List[str]) -> None:
|
||||
vef_vkzf = _lese_vef_vkzf(zeilen, spaltennamen)
|
||||
set_variable("verfahrensnummer", vef_vkzf, scope="project")
|
||||
_fordere_manuelles_laden_an(vef_vkzf)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Öffentliche API
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def beteiligung_pruefen(tab_widget) -> Optional[BeteiligungErgebnis]:
|
||||
"""Vollständiger Ablauf: CSV laden → Flurstueck_csv bilden → try1/try2/try3.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Optional[BeteiligungErgebnis]
|
||||
``None`` bei Nutzerabbruch des Dateidialogs. Sonst das Ergebnis mit
|
||||
den ins Projekt einzufügenden Layern und der Statistik.
|
||||
|
||||
Raises
|
||||
------
|
||||
BeteiligungPruefungFehler
|
||||
Bei einem der dokumentierten Fehlerfälle (CSV-Format, fehlende
|
||||
Spalten) oder wenn kein Verfahrensgebiet mit verwertbaren
|
||||
Kennzeichen vorliegt (try2/try3 – der Nutzer wird dann aufgefordert,
|
||||
das Verfahrensgebiet zunächst über "Aus ALKIS laden" zu laden und
|
||||
die Beteiligungsprüfung danach erneut zu starten). Der Aufrufer
|
||||
zeigt daraufhin eine Messagebox an.
|
||||
"""
|
||||
geladen = lade_beteiligung_csv(tab_widget)
|
||||
if geladen is None:
|
||||
return None
|
||||
zeilen, spaltennamen = geladen
|
||||
|
||||
laenderkennung = get_laenderkennung("sn")
|
||||
flurstueck_csv_liste = baue_flurstueck_csv_liste(zeilen, laenderkennung)
|
||||
if not flurstueck_csv_liste:
|
||||
raise BeteiligungPruefungFehler(
|
||||
"Aus der CSV konnten keine gültigen Flurstück-Kennzeichen gebildet werden "
|
||||
"(Attribut 'FLURSTUECK' muss 4 durch '/' getrennte Teile enthalten)."
|
||||
)
|
||||
|
||||
verf_layer = _hole_verfahrensgebiet_layer(tab_widget)
|
||||
|
||||
if verf_layer is not None and _hat_verwendbare_kennzeichen(verf_layer):
|
||||
layer, statistik = _try1_listenabgleich(tab_widget, verf_layer, flurstueck_csv_liste)
|
||||
text = (
|
||||
f"Übereinstimmend: {statistik[WERT_BEIDE]}\n"
|
||||
f"Nur im Verfahrensgebiet: {statistik[WERT_NUR_VERFAHRENSGEBIET]}\n"
|
||||
f"Nur in der CSV-Liste (AGLB): {statistik[WERT_NUR_AGLB]}"
|
||||
)
|
||||
return BeteiligungErgebnis("try1", [(layer, "beteiligung_vergleich.qml")], statistik, text)
|
||||
|
||||
if verf_layer is not None:
|
||||
_fordere_manuelles_laden_an()
|
||||
return None # unreachable – _fordere_manuelles_laden_an() wirft immer
|
||||
|
||||
_try3_bootstrap(tab_widget, zeilen, spaltennamen)
|
||||
return None # unreachable – _try3_bootstrap() wirft immer
|
||||
@@ -9,7 +9,8 @@ from sn_basis.functions import (
|
||||
from sn_basis.functions import QMessageBox
|
||||
from sn_basis.functions.qt_wrapper import QVariant
|
||||
from sn_basis.functions.variable_wrapper import get_variable
|
||||
from sn_basis.functions.dialog_wrapper import ask_bror_kombination
|
||||
from sn_verfahrensgebiet.functions.verfahrensgebiet_dialoge import ask_bror_kombinationen, ask_landkreis_objekte
|
||||
from sn_basis.functions.landkreise import ALLE_LANDKREISE
|
||||
|
||||
alkis_sf_url = "https://geodienste.sachsen.de/aaa/public_alkis/sf/wfs"
|
||||
typename = "adv:AX_BauRaumOderBodenordnungsrecht"
|
||||
@@ -28,7 +29,7 @@ BROR_ATTRIBUTE_FELDER = [
|
||||
|
||||
class LoadStatus(Enum):
|
||||
"""Status eines Layer-Ladevorgangs.
|
||||
|
||||
|
||||
Attributes:
|
||||
NONE: Nichts geladen
|
||||
FIRST: Erstmalig geladen
|
||||
@@ -44,10 +45,10 @@ class LoadStatus(Enum):
|
||||
|
||||
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
|
||||
"""
|
||||
@@ -72,30 +73,36 @@ def _check_existing_layer(tab_widget):
|
||||
return existing_layers[0], LoadStatus.KEEP
|
||||
|
||||
|
||||
def verfahrensgebiet_alkis(tab_widget):
|
||||
"""Lädt Verfahrensgebiet aus ALKIS-WFS basierend auf Verfahrensnummer.
|
||||
def _suche_verfahrensgebiet_features(tab_widget, skip_existing_check: bool = False):
|
||||
"""Phase 1 ("Suchen"): vorhandenen Layer prüfen, WFS abfragen, nach
|
||||
(adv_name, bezeichnung) gruppieren.
|
||||
|
||||
Args:
|
||||
tab_widget: Parent-Widget für Dialoge und Meldungen
|
||||
skip_existing_check: Überspringt die Nachfrage bei bereits vorhandenem
|
||||
Layer (immer neu laden).
|
||||
|
||||
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.
|
||||
Tuple[Optional[QgsVectorLayer], LoadStatus, dict, Optional[str]]:
|
||||
``(fertig_layer, status, kombinationen, crs)``.
|
||||
Bei ``KEEP`` oder Fehler/Abbruch ist ``fertig_layer`` bereits das
|
||||
Endergebnis (ggf. ``None``) und ``kombinationen`` leer – Phase 2
|
||||
entfällt dann. Bei Erfolg mit mindestens einer Kombination ist
|
||||
``fertig_layer`` ``None`` und ``kombinationen``/``crs`` sind für
|
||||
Phase 2 (:func:`_baue_layer_aus_kombinationen`) befüllt.
|
||||
"""
|
||||
verfahrensnummer = get_variable("verfahrensnummer")
|
||||
if not verfahrensnummer:
|
||||
QMessageBox.critical(tab_widget, "Fehler",
|
||||
"In den Projekteigenschaften ist noch keine Verfahrensnummer eingetragen!")
|
||||
return None, LoadStatus.NONE, []
|
||||
return None, LoadStatus.NONE, {}, None
|
||||
|
||||
# Vorhandenen Layer prüfen
|
||||
existing, status = _check_existing_layer(tab_widget)
|
||||
if status == LoadStatus.KEEP:
|
||||
return existing, status, []
|
||||
# Vorhandenen Layer prüfen (überspringbar für die Shape-Pipeline)
|
||||
status = LoadStatus.FIRST
|
||||
if not skip_existing_check:
|
||||
existing, status = _check_existing_layer(tab_widget)
|
||||
if status == LoadStatus.KEEP:
|
||||
return existing, status, {}, None
|
||||
|
||||
# WFS mit Filter laden – nur erste 5 Stellen der VKZ für den Vergleich verwenden,
|
||||
# da die Daten im Dienst ggf. abweichende Suffixe führen.
|
||||
@@ -109,13 +116,13 @@ def verfahrensgebiet_alkis(tab_widget):
|
||||
|
||||
if not temp_layer.isValid():
|
||||
QMessageBox.critical(tab_widget, "Fehler", "Layer konnte nicht geladen werden.")
|
||||
return None, LoadStatus.NONE, []
|
||||
return None, LoadStatus.NONE, {}, 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, []
|
||||
return None, LoadStatus.NONE, {}, None
|
||||
|
||||
# Objekte nach (adv_name, bezeichnung) gruppieren
|
||||
kombinationen: dict[tuple[str, str], list] = {}
|
||||
@@ -131,16 +138,32 @@ def verfahrensgebiet_alkis(tab_widget):
|
||||
kombinationen[key] = []
|
||||
kombinationen[key].append(feat)
|
||||
|
||||
return None, status, kombinationen, temp_layer.crs().authid()
|
||||
|
||||
|
||||
def _baue_layer_aus_kombinationen(tab_widget, kombinationen: dict, crs: str, status: "LoadStatus"):
|
||||
"""Phase 2 ("Auswählen + Laden"): bei mehreren Kombinationen Auswahldialog
|
||||
zeigen, Geometrien der gewählten Kombination(en) verschmelzen, Memory-Layer bauen.
|
||||
|
||||
Args:
|
||||
tab_widget: Parent-Widget für Dialoge und Meldungen
|
||||
kombinationen: Ergebnis aus :func:`_suche_verfahrensgebiet_features`
|
||||
crs: CRS-Authid aus :func:`_suche_verfahrensgebiet_features`
|
||||
status: Lade-Status aus Phase 1 (FIRST/RELOAD), wird durchgereicht
|
||||
|
||||
Returns:
|
||||
Tuple[Optional[QgsVectorLayer], LoadStatus, List[QgsFeature]]
|
||||
"""
|
||||
if len(kombinationen) == 1:
|
||||
selected_key = next(iter(kombinationen))
|
||||
selected_feats = kombinationen[selected_key]
|
||||
selected_feats = next(iter(kombinationen.values()))
|
||||
else:
|
||||
# Mehrere Kombinationen → Nutzerauswahl
|
||||
# Mehrere Kombinationen → Nutzerauswahl, ein oder mehrere Objekte
|
||||
# können gemeinsam ausgewählt und geladen (verschmolzen) werden.
|
||||
key_list = sorted(kombinationen.keys())
|
||||
selected_key = ask_bror_kombination(tab_widget, key_list)
|
||||
if selected_key is None:
|
||||
selected_keys = ask_bror_kombinationen(tab_widget, key_list)
|
||||
if not selected_keys:
|
||||
return None, LoadStatus.NONE, []
|
||||
selected_feats = kombinationen[selected_key]
|
||||
selected_feats = [feat for key in selected_keys for feat in kombinationen[key]]
|
||||
|
||||
union_geom = QgsGeometry.unaryUnion([f.geometry() for f in selected_feats])
|
||||
if union_geom is None or union_geom.isEmpty():
|
||||
@@ -148,7 +171,6 @@ def verfahrensgebiet_alkis(tab_widget):
|
||||
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(attr, QVariant.String) for attr in BROR_ATTRIBUTE_FELDER])
|
||||
@@ -178,3 +200,158 @@ def verfahrensgebiet_alkis(tab_widget):
|
||||
|
||||
# Status: FIRST oder RELOAD; selected_feats für Gemarkungsermittlung weitergeben
|
||||
return memory_layer, status, selected_feats
|
||||
|
||||
|
||||
def _suche_landkreis_kombinationen(tab_widget, landkreis_kennzahl: str, skip_existing_check: bool = False):
|
||||
"""Fallback-Suche über einen ganzen Landkreis (statt einer Verfahrensnummer).
|
||||
|
||||
Wie :func:`_suche_verfahrensgebiet_features`, aber mit WFS-Filter über die
|
||||
Landkreiskennzahl (``bezeichnung LIKE '{landkreis_kennzahl}%'``) statt
|
||||
über den 5-stelligen VKZ-Präfix. Gruppierung der Rohfeatures identisch:
|
||||
nach exaktem (adv_name, bezeichnung).
|
||||
|
||||
Returns:
|
||||
Tuple[Optional[QgsVectorLayer], LoadStatus, dict, Optional[str]]:
|
||||
Gleiche Bedeutung wie bei :func:`_suche_verfahrensgebiet_features`.
|
||||
"""
|
||||
status = LoadStatus.FIRST
|
||||
if not skip_existing_check:
|
||||
existing, status = _check_existing_layer(tab_widget)
|
||||
if status == LoadStatus.KEEP:
|
||||
return existing, status, {}, None
|
||||
|
||||
# "Alle Landkreise" (Sentinel) -> kein Kennzahl-Präfix, LIKE '%' matcht alles.
|
||||
praefix = "" if landkreis_kennzahl == ALLE_LANDKREISE else landkreis_kennzahl
|
||||
landkreis_anzeige = "allen Landkreisen" if landkreis_kennzahl == ALLE_LANDKREISE else f"Landkreis {landkreis_kennzahl}"
|
||||
|
||||
wfs_uri_find = (
|
||||
f"url='{alkis_sf_url}' typename='{typename}' srsname='EPSG:25833' "
|
||||
f"sql=SELECT * FROM AX_BauRaumOderBodenordnungsrecht "
|
||||
f"WHERE bezeichnung LIKE '{praefix}%'"
|
||||
)
|
||||
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, {}, None
|
||||
|
||||
features = list(temp_layer.getFeatures())
|
||||
if not features:
|
||||
QMessageBox.critical(tab_widget, "Fehler",
|
||||
f"Für {landkreis_anzeige} wurden keine Objekte im WFS gefunden.")
|
||||
return None, LoadStatus.NONE, {}, None
|
||||
|
||||
kombinationen: dict[tuple[str, str], list] = {}
|
||||
for feat in features:
|
||||
try:
|
||||
adv_name_val = str(feat.attribute("adv_name") or "")
|
||||
bezeichnung_val = str(feat.attribute("bezeichnung") or "")
|
||||
except Exception:
|
||||
adv_name_val = ""
|
||||
bezeichnung_val = ""
|
||||
key = (adv_name_val, bezeichnung_val)
|
||||
if key not in kombinationen:
|
||||
kombinationen[key] = []
|
||||
kombinationen[key].append(feat)
|
||||
|
||||
return None, status, kombinationen, temp_layer.crs().authid()
|
||||
|
||||
|
||||
def _baue_layer_aus_landkreis_auswahl(tab_widget, kombinationen: dict, crs: str, status: "LoadStatus"):
|
||||
"""Auswahl + Bau für den Landkreis-Fallback.
|
||||
|
||||
Zeigt :func:`~sn_verfahrensgebiet.functions.verfahrensgebiet_dialoge.ask_landkreis_objekte`
|
||||
(Mehrfachauswahl inkl. "Alle auswählen"). Die ausgewählten Kombinationen
|
||||
werden anhand der ersten 5 Zeichen von ``bezeichnung`` (= die eigentliche
|
||||
VKZ) gruppiert; je Gruppe wird EIN Feature erzeugt (Geometrien der
|
||||
Gruppe verschmolzen). Damit entsteht bei Auswahl mehrerer/aller Objekte
|
||||
eines Landkreises ein Layer mit mehreren Features – einem je
|
||||
tatsächlichem Verfahren, nicht ein Gesamt-Blob.
|
||||
|
||||
Args:
|
||||
tab_widget: Parent-Widget für Dialoge und Meldungen
|
||||
kombinationen: Ergebnis aus :func:`_suche_landkreis_kombinationen`
|
||||
crs: CRS-Authid aus :func:`_suche_landkreis_kombinationen`
|
||||
status: Lade-Status aus Phase 1 (FIRST/RELOAD), wird durchgereicht
|
||||
|
||||
Returns:
|
||||
Tuple[Optional[QgsVectorLayer], LoadStatus]: ``(layer, status)``.
|
||||
"""
|
||||
key_list = sorted(kombinationen.keys())
|
||||
selected_keys = ask_landkreis_objekte(tab_widget, key_list)
|
||||
if not selected_keys:
|
||||
return None, LoadStatus.NONE
|
||||
|
||||
# Nach VKZ (erste 5 Zeichen der bezeichnung) gruppieren
|
||||
vkz_gruppen: dict[str, list] = {}
|
||||
for key in selected_keys:
|
||||
_, bezeichnung = key
|
||||
vkz = bezeichnung[:5]
|
||||
vkz_gruppen.setdefault(vkz, []).extend(kombinationen[key])
|
||||
|
||||
memory_layer = QgsVectorLayer(f"Polygon?crs={crs}", "Verfahrensgebiet", "memory")
|
||||
pr = memory_layer.dataProvider()
|
||||
pr.addAttributes([QgsField(attr, QVariant.String) for attr in BROR_ATTRIBUTE_FELDER])
|
||||
memory_layer.updateFields()
|
||||
|
||||
neue_features = []
|
||||
for vkz, feats in sorted(vkz_gruppen.items()):
|
||||
union_geom = QgsGeometry.unaryUnion([f.geometry() for f in feats])
|
||||
if union_geom is None or union_geom.isEmpty():
|
||||
continue
|
||||
|
||||
first_feat = feats[0]
|
||||
attrs = []
|
||||
for attr in BROR_ATTRIBUTE_FELDER:
|
||||
try:
|
||||
val = first_feat.attribute(attr)
|
||||
if val is None:
|
||||
attrs.append("")
|
||||
elif type(val).__name__ == "QDate":
|
||||
attrs.append(val.toString("dd.MM.yyyy"))
|
||||
else:
|
||||
attrs.append(str(val))
|
||||
except Exception:
|
||||
attrs.append("")
|
||||
|
||||
feat_out = QgsFeature()
|
||||
feat_out.setGeometry(union_geom)
|
||||
feat_out.setAttributes(attrs)
|
||||
neue_features.append(feat_out)
|
||||
|
||||
if not neue_features:
|
||||
QMessageBox.critical(tab_widget, "Fehler", "Keine Geometrien zum Verschmelzen gefunden.")
|
||||
return None, LoadStatus.NONE
|
||||
|
||||
pr.addFeatures(neue_features)
|
||||
memory_layer.updateExtents()
|
||||
|
||||
return memory_layer, status
|
||||
|
||||
|
||||
def verfahrensgebiet_alkis(tab_widget, skip_existing_check: bool = False):
|
||||
"""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.
|
||||
|
||||
Ruft intern nacheinander :func:`_suche_verfahrensgebiet_features` (Phase 1)
|
||||
und ggf. :func:`_baue_layer_aus_kombinationen` (Phase 2) auf – aufgeteilt,
|
||||
damit ``verfahrensgebiet_alkis_komplett()`` dazwischen einen eigenen
|
||||
Fortschrittsschritt anzeigen kann. Verhalten/Signatur dieser Funktion
|
||||
selbst ist unverändert.
|
||||
"""
|
||||
layer, status, kombinationen, crs = _suche_verfahrensgebiet_features(
|
||||
tab_widget, skip_existing_check
|
||||
)
|
||||
if layer is not None or not kombinationen:
|
||||
return layer, status, []
|
||||
return _baue_layer_aus_kombinationen(tab_widget, kombinationen, crs, status)
|
||||
|
||||
@@ -51,10 +51,17 @@ from sn_basis.functions.dialog_wrapper import (
|
||||
create_progress_dialog,
|
||||
show_info_dialog,
|
||||
ask_detailpruefung_oder_vg_laden,
|
||||
ask_yes_no,
|
||||
)
|
||||
from sn_basis.functions.landkreise import LANDKREISE, ALLE_LANDKREISE
|
||||
from sn_basis.functions.ly_style_wrapper import apply_style
|
||||
from sn_verfahrensgebiet.functions.verfahrensgebiet_dialoge import ask_landkreis_auswahl
|
||||
|
||||
from sn_verfahrensgebiet.functions.verfahrensgebiet_alkis import (
|
||||
verfahrensgebiet_alkis,
|
||||
_suche_verfahrensgebiet_features,
|
||||
_baue_layer_aus_kombinationen,
|
||||
_suche_landkreis_kombinationen,
|
||||
_baue_layer_aus_landkreis_auswahl,
|
||||
LoadStatus,
|
||||
)
|
||||
from sn_verfahrensgebiet.functions.gemarkung_alkis import (
|
||||
@@ -76,6 +83,8 @@ from sn_verfahrensgebiet.functions.knickpunkt_pruefung import (
|
||||
FELD_VKZ = "verfahrensnummer"
|
||||
#: Anzeigename des BROR-Layers im Projekt
|
||||
BROR_LAYER_NAME = "BauRaumOderBodenordnungsrecht (ALKIS)"
|
||||
#: QML-Stildatei für den BROR-Layer (sn_verfahrensgebiet/styles/)
|
||||
BROR_STYLE_QML = "BauRaumOderBodenordnungsrecht.qml"
|
||||
#: Projektvariable für die Flächentoleranz (in m²); Default 0
|
||||
FLAECHEN_TOLERANZ_VARIABLE = "flaechen_toleranz_m2"
|
||||
#: Minimale implizite Flächentoleranz – Abweichungen unter diesem Wert
|
||||
@@ -258,15 +267,29 @@ def verfahrensgebiet_alkis_komplett(
|
||||
"""Vollständiger VG-Lade-Workflow: ALKIS-VG → Gemarkungen →
|
||||
Flurstücke → Verschneidung → Flächenvergleich → opt. Umringsprüfung.
|
||||
|
||||
Schrittfolge:
|
||||
Zwei Fortschrittsdialoge nacheinander:
|
||||
|
||||
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.
|
||||
- **Dialog 1** ("Verfahrensgebiet(e) in ALKIS ermitteln/suchen"): läuft
|
||||
während ``_suche_verfahrensgebiet_features(tab_widget)`` (ALKIS-WFS
|
||||
durchsuchen) und wird danach immer geschlossen – auch wenn anschließend
|
||||
der BROR-Auswahldialog erscheint (kein Fortschrittsdialog läuft
|
||||
währenddessen parallel zum Auswahldialog).
|
||||
- **STOP** (Zwischenabfrage "Weiter?"): erscheint nach dem Laden/Übernehmen
|
||||
des BROR-Objekts, bevor Dialog 2 startet. Bei "Nein" bricht der Ablauf
|
||||
ab, das BROR-Objekt bleibt aber im Projekt (ersetzt die frühere
|
||||
"Nur Objekte laden"-Option des Landkreis-Auswahldialogs).
|
||||
- **Dialog 2** (eigentlicher Workflow, 6 Schritte): startet erst, nachdem
|
||||
das Verfahrensgebiet feststeht und die Stop-Abfrage mit "Ja" bestätigt
|
||||
wurde.
|
||||
|
||||
Schrittfolge in Dialog 2:
|
||||
|
||||
1. ``gemarkung_schluessel_und_namen_von_vg_geometrie(geom)`` – Schlüssel.
|
||||
2. ``flurstueck_alkis(...)`` – Flurstücke laden.
|
||||
3. ``_select_by_location(...)`` – räumliche Selektion.
|
||||
4. ``erstelle_verfahrensgebiet_aus_auswahl(...)`` – enriched Layer.
|
||||
5. ``_vergleiche_flaechen(...)`` – Flächenvergleich als Gate.
|
||||
6. Ggf. ``umringspruefung(...)`` nach Nutzerbestätigung.
|
||||
|
||||
Der BROR-Layer (``BauRaumOderBodenordnungsrecht (ALKIS)``) bleibt bei Erfolg
|
||||
im Projekt. Bei Abbruch/Fehler wird er entfernt, falls er in diesem
|
||||
@@ -290,26 +313,75 @@ def verfahrensgebiet_alkis_komplett(
|
||||
alkis_vg_layer = None
|
||||
bror_was_added_by_us = False
|
||||
abgeschlossen = False
|
||||
progress = None
|
||||
|
||||
# Fortschrittsdialog für den Basisworkflow (7 Schritte)
|
||||
progress = create_progress_dialog(
|
||||
7, "Verfahrensgebiet laden", "Initialisierung …"
|
||||
)
|
||||
verfahrensnummer = get_variable("verfahrensnummer")
|
||||
landkreis_kennzahl = get_variable("landkreis_kennzahl")
|
||||
|
||||
if not verfahrensnummer and not landkreis_kennzahl:
|
||||
# Keine Projekteigenschaften vorhanden – Landkreis per Dialog abfragen,
|
||||
# statt direkt abzubrechen.
|
||||
landkreis_kennzahl = ask_landkreis_auswahl(tab_widget, LANDKREISE)
|
||||
if not landkreis_kennzahl:
|
||||
# Nutzer hat den Landkreis-Dialog abgebrochen – stiller Abbruch,
|
||||
# keine Fehlermeldung (bewusster Abbruch, kein Fehlerzustand).
|
||||
return None, None, None, LoadStatus.NONE
|
||||
|
||||
landkreis_modus = not verfahrensnummer
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Dialog 1: Verfahrensgebiet(e) in ALKIS ermitteln/suchen
|
||||
# ------------------------------------------------------------------
|
||||
if landkreis_modus:
|
||||
if landkreis_kennzahl == ALLE_LANDKREISE:
|
||||
such_label = "Objekte in allen Landkreisen ermitteln/suchen …"
|
||||
else:
|
||||
such_label = f"Objekte in Landkreis {landkreis_kennzahl} ermitteln/suchen …"
|
||||
else:
|
||||
such_label = "Verfahrensgebiet(e) in ALKIS ermitteln/suchen …"
|
||||
|
||||
progress_suche = create_progress_dialog(1, "Verfahrensgebiet laden", such_label)
|
||||
try:
|
||||
progress_suche.set_value(0)
|
||||
if progress_suche.is_canceled():
|
||||
return None, None, None, LoadStatus.NONE
|
||||
|
||||
if landkreis_modus:
|
||||
vorab_layer, vg_status, kombinationen, crs = _suche_landkreis_kombinationen(
|
||||
tab_widget, landkreis_kennzahl
|
||||
)
|
||||
else:
|
||||
vorab_layer, vg_status, kombinationen, crs = _suche_verfahrensgebiet_features(tab_widget)
|
||||
finally:
|
||||
# Dialog 1 schließt hier immer – egal ob eine Auswahl folgt oder nicht.
|
||||
progress_suche.close()
|
||||
|
||||
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)
|
||||
progress.raise_to_front()
|
||||
|
||||
if vg_status == LoadStatus.NONE or not alkis_vg_layer:
|
||||
return None, None, None, LoadStatus.NONE
|
||||
if vorab_layer is not None or not kombinationen:
|
||||
# KEEP oder Fehler/Abbruch bereits in Phase 1 entschieden
|
||||
if vg_status == LoadStatus.NONE or not vorab_layer:
|
||||
return None, None, None, LoadStatus.NONE
|
||||
alkis_vg_layer = vorab_layer
|
||||
elif landkreis_modus:
|
||||
# ------------------------------------------------------------------
|
||||
# Objekt(e) des Landkreises auswählen – kein eigener Fortschritts-
|
||||
# dialog währenddessen, nur der Auswahldialog selbst.
|
||||
# ------------------------------------------------------------------
|
||||
alkis_vg_layer, vg_status = _baue_layer_aus_landkreis_auswahl(
|
||||
tab_widget, kombinationen, crs, vg_status
|
||||
)
|
||||
if vg_status == LoadStatus.NONE or not alkis_vg_layer:
|
||||
return None, None, None, LoadStatus.NONE
|
||||
else:
|
||||
# ------------------------------------------------------------------
|
||||
# Verfahrensgebiet auswählen (bei mehreren Treffern) – kein eigener
|
||||
# Fortschrittsdialog währenddessen, nur der Auswahldialog selbst.
|
||||
# ------------------------------------------------------------------
|
||||
alkis_vg_layer, vg_status, _ = _baue_layer_aus_kombinationen(
|
||||
tab_widget, kombinationen, crs, vg_status
|
||||
)
|
||||
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)
|
||||
@@ -317,13 +389,38 @@ def verfahrensgebiet_alkis_komplett(
|
||||
if vg_status in (LoadStatus.FIRST, LoadStatus.RELOAD):
|
||||
project.addMapLayer(alkis_vg_layer)
|
||||
bror_was_added_by_us = True
|
||||
|
||||
progress.set_value(1)
|
||||
# Style direkt nach dem Laden anwenden – unabhängig vom weiteren
|
||||
# Ablauf, damit auch bei "Nein" auf die Stop-Abfrage der Style
|
||||
# gesetzt ist.
|
||||
apply_style(alkis_vg_layer, BROR_STYLE_QML)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Schritt 2: Gemarkungsschlüssel per Geometrie-Spatial-Join ermitteln
|
||||
# STOP: Zwischenabfrage nach dem Laden des BROR-Objekts, bevor mit
|
||||
# Gemarkung/Flurstücke/Flächenvergleich fortgefahren wird (ersetzt
|
||||
# die frühere "Nur Objekte laden"-Option).
|
||||
# ------------------------------------------------------------------
|
||||
progress.set_label("Schritt 2/6: Gemarkungsschlüssel ermitteln …")
|
||||
if not ask_yes_no(
|
||||
"Verfahrensgebiet laden",
|
||||
f"Layer '{alkis_vg_layer.name()}' wurde geladen. Weiter?",
|
||||
default=True,
|
||||
parent=tab_widget,
|
||||
):
|
||||
abgeschlossen = True
|
||||
return alkis_vg_layer, None, None, vg_status
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Dialog 2: eigentlicher Fortschrittsdialog für die restlichen Schritte
|
||||
# (startet erst jetzt, nach erfolgreicher Auswahl/Übernahme des VG)
|
||||
# ------------------------------------------------------------------
|
||||
progress = create_progress_dialog(
|
||||
6, "Verfahrensgebiet laden", "Initialisierung …"
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Schritt 1: Gemarkungsschlüssel per Geometrie-Spatial-Join ermitteln
|
||||
# ------------------------------------------------------------------
|
||||
progress.set_label("Schritt 1/6: Gemarkungen ermitteln und laden …")
|
||||
progress.set_value(0)
|
||||
if progress.is_canceled():
|
||||
return None, None, None, LoadStatus.NONE
|
||||
|
||||
@@ -344,12 +441,12 @@ def verfahrensgebiet_alkis_komplett(
|
||||
duration=5,
|
||||
)
|
||||
|
||||
progress.set_value(2)
|
||||
progress.set_value(1)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Schritt 3: Flurstücke laden (mit Nutzer-Dialog, Vorschlag befüllt)
|
||||
# Schritt 2: Flurstücke laden (mit Nutzer-Dialog, Vorschlag befüllt)
|
||||
# ------------------------------------------------------------------
|
||||
progress.set_label("Schritt 3/6: Flurstücke aus ALKIS laden …")
|
||||
progress.set_label("Schritt 2/6: Flurstücke aus ALKIS laden …")
|
||||
if progress.is_canceled():
|
||||
return None, None, None, LoadStatus.NONE
|
||||
|
||||
@@ -363,12 +460,12 @@ def verfahrensgebiet_alkis_komplett(
|
||||
if flst_status == LoadStatus.NONE or not flst_layer:
|
||||
return None, None, None, LoadStatus.NONE
|
||||
|
||||
progress.set_value(3)
|
||||
progress.set_value(2)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Schritt 4: Räumliche Selektion
|
||||
# Schritt 3: Räumliche Selektion
|
||||
# ------------------------------------------------------------------
|
||||
progress.set_label("Schritt 4/6: Flurstücke räumlich selektieren …")
|
||||
progress.set_label("Schritt 3/6: Flurstücke räumlich selektieren …")
|
||||
if progress.is_canceled():
|
||||
return None, None, None, LoadStatus.NONE
|
||||
|
||||
@@ -383,12 +480,12 @@ def verfahrensgebiet_alkis_komplett(
|
||||
)
|
||||
return None, flst_layer, None, LoadStatus.NONE
|
||||
|
||||
progress.set_value(4)
|
||||
progress.set_value(3)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Schritt 5: Enriched Memory-Layer aus Auswahl erzeugen
|
||||
# Schritt 4: Enriched Memory-Layer aus Auswahl erzeugen
|
||||
# ------------------------------------------------------------------
|
||||
progress.set_label("Schritt 5/6: Verfahrensgebiet-Layer erzeugen …")
|
||||
progress.set_label("Schritt 4/6: Verfahrensgebiet-Layer erzeugen …")
|
||||
if progress.is_canceled():
|
||||
return None, None, None, LoadStatus.NONE
|
||||
|
||||
@@ -407,12 +504,12 @@ def verfahrensgebiet_alkis_komplett(
|
||||
vkz = get_variable("verfahrensnummer") or ""
|
||||
_fuege_vkz_feld_hinzu(enriched_layer, vkz)
|
||||
|
||||
progress.set_value(5)
|
||||
progress.set_value(4)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Schritt 6: Flächenvergleich als Gate
|
||||
# Schritt 5: Flächenvergleich als Gate
|
||||
# ------------------------------------------------------------------
|
||||
progress.set_label("Schritt 6/6: Flächenvergleich …")
|
||||
progress.set_label("Schritt 5/6: Flächenvergleich …")
|
||||
if progress.is_canceled():
|
||||
return None, None, None, LoadStatus.NONE
|
||||
|
||||
@@ -421,11 +518,10 @@ def verfahrensgebiet_alkis_komplett(
|
||||
alkis_vg_layer, enriched_layer, toleranz
|
||||
)
|
||||
|
||||
progress.set_value(6)
|
||||
progress.set_value(5)
|
||||
|
||||
if stimmt_ueberein:
|
||||
progress.set_value(7)
|
||||
progress.close()
|
||||
progress.set_value(6)
|
||||
show_info_dialog(
|
||||
"Flächenvergleich",
|
||||
"Verfahrensgebiet mit erwarteter Geometrie erzeugt",
|
||||
@@ -437,14 +533,13 @@ def verfahrensgebiet_alkis_komplett(
|
||||
# ------------------------------------------------------------------
|
||||
# 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 BauRaumOderBodenordnungsrecht-Objekt und abgeleitetem "
|
||||
f"Verfahrensgebiet erkannt.\n\n"
|
||||
f"BauRaumOderBodenordnungsrecht: {bror_flaeche:.2f} m²\n"
|
||||
f"As: {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?",
|
||||
@@ -563,6 +658,9 @@ def verfahrensgebiet_alkis_komplett(
|
||||
return enriched_layer, flst_layer, alkis_vg_layer, vg_status
|
||||
|
||||
finally:
|
||||
# Dialog 2 (falls erstellt) bei jedem Ausgang (Erfolg, Abbruch, Fehler) schließen
|
||||
if progress is not None:
|
||||
progress.close()
|
||||
# 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)
|
||||
|
||||
@@ -0,0 +1,296 @@
|
||||
"""sn_verfahrensgebiet/functions/verfahrensgebiet_dialoge.py – Dialoge zum
|
||||
Laden eines Verfahrensgebiets aus ALKIS (Qt5/6/Mock-kompatibel).
|
||||
"""
|
||||
from typing import Any, Optional
|
||||
from sn_basis.functions.qt_wrapper import (
|
||||
QT_VERSION, exec_dialog, QInputDialog, QLineEdit,
|
||||
QDialog, QDialogButtonBox, QListWidget, QListWidgetItem, QAbstractItemView,
|
||||
QTableWidget, QTableWidgetItem, QTableWidgetSelectionRange,
|
||||
QIcon, QVBoxLayout, QHBoxLayout, QLabel, QCheckBox,
|
||||
DialogAccepted, ExtendedSelection, SelectRows, NoEditTriggers, AscendingOrder,
|
||||
)
|
||||
from sn_basis.functions.landkreise import ALLE_LANDKREISE
|
||||
import os
|
||||
|
||||
# .../python/plugins (drei Ebenen über dieser Datei)
|
||||
_PLUGINS_DIR = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
||||
_WAPPEN_ICON = os.path.join(_PLUGINS_DIR, "sn_basis", "favicon.ico")
|
||||
|
||||
|
||||
def ask_bror_kombinationen(
|
||||
parent: Any,
|
||||
kombinationen: list[tuple[str, str]],
|
||||
) -> list[tuple[str, str]]:
|
||||
"""Zeigt Dialog zur Auswahl einer oder mehrerer BROR-Kombinationen
|
||||
(adv_name + bezeichnung).
|
||||
|
||||
Wird aufgerufen, wenn der WFS mehrere verschiedene Kombinationen aus
|
||||
``adv_name`` und ``bezeichnung`` zurückliefert. Es können ein oder
|
||||
mehrere Objekte gemeinsam ausgewählt und geladen (verschmolzen) werden.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
parent :
|
||||
Eltern-Widget oder None.
|
||||
kombinationen :
|
||||
Sortierte Liste von ``(adv_name, bezeichnung)``-Tupeln.
|
||||
|
||||
Returns
|
||||
-------
|
||||
list[tuple[str, str]]
|
||||
Die gewählten ``(adv_name, bezeichnung)``-Tupel. Leere Liste bei
|
||||
Abbruch oder wenn nichts ausgewählt wurde.
|
||||
"""
|
||||
if not kombinationen:
|
||||
return []
|
||||
|
||||
if QT_VERSION == 0: # Mock-Modus
|
||||
print(f"Mock-Modus: ask_bror_kombinationen → {kombinationen[:1]}")
|
||||
return kombinationen[:1]
|
||||
|
||||
dialog = QDialog(parent)
|
||||
dialog.setWindowTitle("Verfahrensgebiet aus ALKIS laden")
|
||||
if os.path.exists(_WAPPEN_ICON):
|
||||
dialog.setWindowIcon(QIcon(_WAPPEN_ICON))
|
||||
layout = QVBoxLayout(dialog)
|
||||
|
||||
hinweis = QLabel(
|
||||
f"Es wurden {len(kombinationen)} Objekte gefunden.\n\n"
|
||||
"Bitte wählen Sie eines oder mehrere Objekte aus:"
|
||||
)
|
||||
hinweis.setWordWrap(True)
|
||||
layout.addWidget(hinweis)
|
||||
|
||||
list_widget = QListWidget()
|
||||
list_widget.setSelectionMode(ExtendedSelection)
|
||||
for adv, bez in kombinationen:
|
||||
item = QListWidgetItem(f"{adv} | {bez}")
|
||||
list_widget.addItem(item)
|
||||
list_widget.item(0).setSelected(True)
|
||||
layout.addWidget(list_widget, 1) # Stretch-Faktor: wächst mit Fenstergröße
|
||||
|
||||
mehrfachauswahl_hinweis = QLabel("<small>Mehrfachauswahl mit Strg + Umschalt</small>")
|
||||
layout.addWidget(mehrfachauswahl_hinweis)
|
||||
|
||||
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
|
||||
buttons.accepted.connect(dialog.accept)
|
||||
buttons.rejected.connect(dialog.reject)
|
||||
layout.addWidget(buttons)
|
||||
|
||||
dialog.adjustSize()
|
||||
breite = dialog.sizeHint().width()
|
||||
|
||||
# Liste auf max. 5 sichtbare Zeilen begrenzen, Rest per Scrollbar
|
||||
sichtbare_zeilen = min(len(kombinationen), 5)
|
||||
zeilenhoehe = list_widget.sizeHintForRow(0) if list_widget.count() else 20
|
||||
# + Puffer für eine ggf. eingeblendete horizontale Scrollbar (bei langen
|
||||
# Einträgen), damit diese nicht die letzte Zeile abschneidet.
|
||||
scrollbar_puffer = list_widget.horizontalScrollBar().sizeHint().height()
|
||||
list_hoehe = zeilenhoehe * sichtbare_zeilen + 2 * list_widget.frameWidth() + 4 + scrollbar_puffer
|
||||
list_widget.setMinimumHeight(list_hoehe)
|
||||
|
||||
dialog.resize(breite, dialog.sizeHint().height())
|
||||
|
||||
if exec_dialog(dialog) != DialogAccepted:
|
||||
return []
|
||||
|
||||
ausgewaehlte_indizes = sorted(list_widget.row(item) for item in list_widget.selectedItems())
|
||||
return [kombinationen[i] for i in ausgewaehlte_indizes]
|
||||
|
||||
|
||||
def ask_landkreis_objekte(
|
||||
parent: Any,
|
||||
kombinationen: list[tuple[str, str]],
|
||||
) -> list[tuple[str, str]]:
|
||||
"""Zeigt Dialog zur Auswahl eines, mehrerer oder aller BROR-Objekte eines
|
||||
Landkreises (Fallback, wenn keine Verfahrensnummer gesetzt ist).
|
||||
|
||||
Wie :func:`ask_bror_kombinationen`, zusätzlich mit einer "Alle
|
||||
auswählen"-Checkbox (da hier potenziell deutlich mehr Treffer als bei der
|
||||
VKZ-Suche auftreten). Ob nach dem Laden der weitere Workflow
|
||||
(Gemarkung/Flurstücke/Flächenvergleich) fortgesetzt wird, klärt eine
|
||||
separate Stop-Abfrage nach dem Laden – nicht mehr dieser Dialog.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
parent :
|
||||
Eltern-Widget oder None.
|
||||
kombinationen :
|
||||
Sortierte Liste von ``(adv_name, bezeichnung)``-Tupeln.
|
||||
|
||||
Returns
|
||||
-------
|
||||
list[tuple[str, str]]
|
||||
Die gewählten ``(adv_name, bezeichnung)``-Tupel. Leere Liste bei
|
||||
Abbruch oder wenn nichts ausgewählt wurde.
|
||||
"""
|
||||
if not kombinationen:
|
||||
return []
|
||||
|
||||
if QT_VERSION == 0: # Mock-Modus
|
||||
print(f"Mock-Modus: ask_landkreis_objekte → {kombinationen[:1]}")
|
||||
return kombinationen[:1]
|
||||
|
||||
dialog = QDialog(parent)
|
||||
dialog.setWindowTitle("Verfahrensgebiete aus Landkreis laden")
|
||||
if os.path.exists(_WAPPEN_ICON):
|
||||
dialog.setWindowIcon(QIcon(_WAPPEN_ICON))
|
||||
layout = QVBoxLayout(dialog)
|
||||
|
||||
hinweis = QLabel(
|
||||
f"Es wurden {len(kombinationen)} Objekte gefunden.\n\n"
|
||||
"Bitte wählen Sie eines oder mehrere Objekte aus:"
|
||||
)
|
||||
hinweis.setWordWrap(True)
|
||||
layout.addWidget(hinweis)
|
||||
|
||||
suche_zeile = QHBoxLayout()
|
||||
suche_zeile.addWidget(QLabel("Suche:"))
|
||||
suche_feld = QLineEdit()
|
||||
suche_feld.setPlaceholderText("Nummer oder Name …")
|
||||
suche_zeile.addWidget(suche_feld)
|
||||
layout.addLayout(suche_zeile)
|
||||
|
||||
alle_auswaehlen = QCheckBox("Alle auswählen")
|
||||
layout.addWidget(alle_auswaehlen)
|
||||
|
||||
# Standardsortierung nach Bezeichnung; per Klick auf Spaltenkopf änderbar
|
||||
# (setSortingEnabled aktiviert die eingebaute Tabellensortierung).
|
||||
sortierte_kombinationen = sorted(kombinationen, key=lambda kb: kb[1])
|
||||
|
||||
table = QTableWidget(len(sortierte_kombinationen), 2)
|
||||
table.setHorizontalHeaderLabels(["Nummer", "Name"])
|
||||
table.setSelectionMode(ExtendedSelection)
|
||||
table.setSelectionBehavior(SelectRows)
|
||||
table.setEditTriggers(NoEditTriggers)
|
||||
table.verticalHeader().setVisible(False)
|
||||
table.verticalHeader().setDefaultSectionSize(22) # flachere Zeilen
|
||||
table.horizontalHeader().setStretchLastSection(True)
|
||||
table.setSortingEnabled(False)
|
||||
for row, (adv, bez) in enumerate(sortierte_kombinationen):
|
||||
table.setItem(row, 0, QTableWidgetItem(bez))
|
||||
table.setItem(row, 1, QTableWidgetItem(adv))
|
||||
table.setSortingEnabled(True)
|
||||
table.sortByColumn(0, AscendingOrder) # Standard: aufsteigend nach Nummer
|
||||
table.resizeColumnToContents(0)
|
||||
table.setColumnWidth(0, max(110, table.columnWidth(0)))
|
||||
table.selectRow(0)
|
||||
layout.addWidget(table, 1) # Stretch-Faktor: wächst mit Fenstergröße
|
||||
|
||||
def _suche_geaendert(text):
|
||||
text_lower = text.strip().lower()
|
||||
for row in range(table.rowCount()):
|
||||
bez_text = table.item(row, 0).text().lower()
|
||||
adv_text = table.item(row, 1).text().lower()
|
||||
treffer = not text_lower or text_lower in bez_text or text_lower in adv_text
|
||||
table.setRowHidden(row, not treffer)
|
||||
|
||||
suche_feld.textChanged.connect(_suche_geaendert)
|
||||
|
||||
def _alle_auswaehlen_umschalten(checked):
|
||||
table.clearSelection()
|
||||
if checked:
|
||||
letzte_spalte = table.columnCount() - 1
|
||||
for row in range(table.rowCount()):
|
||||
if not table.isRowHidden(row):
|
||||
# setRangeSelected (statt selectRow) fügt der Selektion
|
||||
# hinzu, ohne vorherige Zeilen wieder abzuwählen.
|
||||
table.setRangeSelected(
|
||||
QTableWidgetSelectionRange(row, 0, row, letzte_spalte), True
|
||||
)
|
||||
|
||||
alle_auswaehlen.stateChanged.connect(_alle_auswaehlen_umschalten)
|
||||
|
||||
mehrfachauswahl_hinweis = QLabel("<small>Mehrfachauswahl mit Strg + Umschalt</small>")
|
||||
layout.addWidget(mehrfachauswahl_hinweis)
|
||||
|
||||
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
|
||||
buttons.accepted.connect(dialog.accept)
|
||||
buttons.rejected.connect(dialog.reject)
|
||||
layout.addWidget(buttons)
|
||||
|
||||
dialog.adjustSize()
|
||||
# Bezeichnungs-Spalte + großzügige Breite für den ADV-Namen (lange Namen
|
||||
# sollen möglichst ohne Abschneiden sichtbar sein) + Rahmen/Scrollbar-Puffer.
|
||||
breite = max(dialog.sizeHint().width(), table.columnWidth(0) + 480)
|
||||
|
||||
# Tabelle auf max. 15 sichtbare Zeilen begrenzen, Rest per Scrollbar – hier
|
||||
# potenziell deutlich mehr Treffer als bei der VKZ-Suche, daher großzügiger
|
||||
# bemessen als bei ask_bror_kombinationen (dort 5).
|
||||
sichtbare_zeilen = min(len(sortierte_kombinationen), 15)
|
||||
zeilenhoehe = table.verticalHeader().defaultSectionSize() if table.rowCount() else 20
|
||||
kopfzeile_hoehe = table.horizontalHeader().height()
|
||||
# + Puffer für eine ggf. eingeblendete horizontale Scrollbar (bei langen
|
||||
# Einträgen), damit diese nicht die letzte Zeile abschneidet.
|
||||
scrollbar_puffer = table.horizontalScrollBar().sizeHint().height()
|
||||
table_hoehe = (
|
||||
zeilenhoehe * sichtbare_zeilen + kopfzeile_hoehe
|
||||
+ 2 * table.frameWidth() + 4 + scrollbar_puffer
|
||||
)
|
||||
table.setMinimumHeight(table_hoehe)
|
||||
|
||||
dialog.resize(breite, dialog.sizeHint().height())
|
||||
|
||||
if exec_dialog(dialog) != DialogAccepted:
|
||||
return []
|
||||
|
||||
ausgewaehlte_zeilen = sorted(set(item.row() for item in table.selectedItems()))
|
||||
ausgewaehlt = [(table.item(row, 1).text(), table.item(row, 0).text()) for row in ausgewaehlte_zeilen]
|
||||
return ausgewaehlt
|
||||
|
||||
|
||||
def ask_landkreis_auswahl(
|
||||
parent: Any,
|
||||
landkreise: list[tuple[str, str]],
|
||||
) -> Optional[str]:
|
||||
"""Fragt per Dropdown-Dialog einen Landkreis ab (Fallback, wenn weder
|
||||
Verfahrensnummer noch Landkreis in den Projekteigenschaften gesetzt ist).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
parent :
|
||||
Eltern-Widget oder None.
|
||||
landkreise :
|
||||
Liste von ``(kennzahl, name)``-Tupeln, z.B. ``LANDKREISE`` aus
|
||||
:mod:`sn_basis.functions.landkreise`.
|
||||
|
||||
Die Auswahlliste beginnt mit einem leeren Platzhalter (der Nutzer muss
|
||||
aktiv einen Eintrag wählen) und enthält zusätzlich "Alle Landkreise"
|
||||
(Sentinel :data:`~sn_basis.functions.landkreise.ALLE_LANDKREISE`).
|
||||
|
||||
Returns
|
||||
-------
|
||||
Optional[str]
|
||||
Gewählte Landkreiskennzahl, :data:`ALLE_LANDKREISE` bei Auswahl aller
|
||||
Landkreise, oder ``None`` bei Abbruch, leerem Platzhalter oder leerer
|
||||
Landkreisliste.
|
||||
"""
|
||||
if not landkreise:
|
||||
return None
|
||||
|
||||
if QT_VERSION == 0: # Mock-Modus
|
||||
print("Mock-Modus: ask_landkreis_auswahl → None (keine aktive Auswahl)")
|
||||
return None
|
||||
|
||||
ALLE_LABEL = "Alle Landkreise"
|
||||
namen = [""] + [ALLE_LABEL] + [name for _, name in landkreise]
|
||||
|
||||
dialog = QInputDialog(parent)
|
||||
dialog.setWindowTitle("Landkreis auswählen")
|
||||
if os.path.exists(_WAPPEN_ICON):
|
||||
dialog.setWindowIcon(QIcon(_WAPPEN_ICON))
|
||||
dialog.setLabelText("Landkreis:")
|
||||
dialog.setComboBoxItems(namen)
|
||||
dialog.setComboBoxEditable(False)
|
||||
|
||||
if exec_dialog(dialog) != DialogAccepted:
|
||||
return None
|
||||
|
||||
name = dialog.textValue()
|
||||
if not name:
|
||||
return None
|
||||
if name == ALLE_LABEL:
|
||||
return ALLE_LANDKREISE
|
||||
|
||||
name_zu_kennzahl = {n: k for k, n in landkreise}
|
||||
return name_zu_kennzahl.get(str(name))
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,13 @@
|
||||
[general]
|
||||
version=26.7.3-testing
|
||||
name=LNO Sachsen | Verfahrensgebiet
|
||||
description=Plugin zum Erzeugen eines Objektes "Verfahrensgebiet"
|
||||
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/testing/
|
||||
QGISMaximumVersion=4.99
|
||||
qgisMinimumVersion=3.40
|
||||
experimental=true
|
||||
deprecated=false
|
||||
supportsQt6=true
|
||||
@@ -0,0 +1,22 @@
|
||||
<!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="singleSymbol">
|
||||
<symbols>
|
||||
<symbol alpha="1" clip_to_extent="1" force_rhr="0" frame_rate="10" is_animated="0" name="0" type="fill">
|
||||
<layer class="SimpleFill" enabled="1" locked="0" pass="0">
|
||||
<Option type="Map">
|
||||
<Option name="color" type="QString" value="0,180,0,150,rgb:0,0.7058824,0,0.5882353"/>
|
||||
<Option name="outline_color" type="QString" value="0,120,0,255,rgb:0,0.4705882,0,1"/>
|
||||
<Option name="outline_style" type="QString" value="solid"/>
|
||||
<Option name="outline_width" type="QString" value="0.4"/>
|
||||
<Option name="outline_width_unit" type="QString" value="MM"/>
|
||||
<Option name="style" type="QString" value="solid"/>
|
||||
</Option>
|
||||
</layer>
|
||||
</symbol>
|
||||
</symbols>
|
||||
</renderer-v2>
|
||||
<blendMode>0</blendMode>
|
||||
<featureBlendMode>0</featureBlendMode>
|
||||
<layerGeometryType>2</layerGeometryType>
|
||||
</qgis>
|
||||
@@ -0,0 +1,51 @@
|
||||
<!DOCTYPE qgis PUBLIC 'http://mrcc.com/qgis.dtd' 'SYSTEM'>
|
||||
<qgis layerType="Vector" styleCategories="Symbology" version="4.0.1-Norrköping">
|
||||
<renderer-v2 attr="status" enableorderby="0" forceraster="0" referencescale="-1" symbollevels="0" type="categorizedSymbol">
|
||||
<categories>
|
||||
<category label="beide (Übereinstimmung)" render="true" symbol="0" value="beide"/>
|
||||
<category label="nur im Verfahrensgebiet" render="true" symbol="1" value="nur_verfahrensgebiet"/>
|
||||
<category label="nur in der CSV-Liste (AGLB)" render="true" symbol="2" value="nur_aglb"/>
|
||||
</categories>
|
||||
<symbols>
|
||||
<symbol alpha="1" clip_to_extent="1" force_rhr="0" frame_rate="10" is_animated="0" name="0" type="fill">
|
||||
<layer class="SimpleFill" enabled="1" locked="0" pass="0">
|
||||
<Option type="Map">
|
||||
<Option name="color" type="QString" value="0,180,0,150,rgb:0,0.7058824,0,0.5882353"/>
|
||||
<Option name="outline_color" type="QString" value="0,120,0,255,rgb:0,0.4705882,0,1"/>
|
||||
<Option name="outline_style" type="QString" value="solid"/>
|
||||
<Option name="outline_width" type="QString" value="0.4"/>
|
||||
<Option name="outline_width_unit" type="QString" value="MM"/>
|
||||
<Option name="style" type="QString" value="solid"/>
|
||||
</Option>
|
||||
</layer>
|
||||
</symbol>
|
||||
<symbol alpha="1" clip_to_extent="1" force_rhr="0" frame_rate="10" is_animated="0" name="1" type="fill">
|
||||
<layer class="SimpleFill" enabled="1" locked="0" pass="0">
|
||||
<Option type="Map">
|
||||
<Option name="color" type="QString" value="220,0,0,150,rgb:0.8627451,0,0,0.5882353"/>
|
||||
<Option name="outline_color" type="QString" value="150,0,0,255,rgb:0.5882353,0,0,1"/>
|
||||
<Option name="outline_style" type="QString" value="solid"/>
|
||||
<Option name="outline_width" type="QString" value="0.4"/>
|
||||
<Option name="outline_width_unit" type="QString" value="MM"/>
|
||||
<Option name="style" type="QString" value="solid"/>
|
||||
</Option>
|
||||
</layer>
|
||||
</symbol>
|
||||
<symbol alpha="1" clip_to_extent="1" force_rhr="0" frame_rate="10" is_animated="0" name="2" type="fill">
|
||||
<layer class="SimpleFill" enabled="1" locked="0" pass="0">
|
||||
<Option type="Map">
|
||||
<Option name="color" type="QString" value="255,128,0,150,rgb:1,0.5019608,0,0.5882353"/>
|
||||
<Option name="outline_color" type="QString" value="190,96,0,255,rgb:0.745098,0.3764706,0,1"/>
|
||||
<Option name="outline_style" type="QString" value="solid"/>
|
||||
<Option name="outline_width" type="QString" value="0.4"/>
|
||||
<Option name="outline_width_unit" type="QString" value="MM"/>
|
||||
<Option name="style" type="QString" value="solid"/>
|
||||
</Option>
|
||||
</layer>
|
||||
</symbol>
|
||||
</symbols>
|
||||
</renderer-v2>
|
||||
<blendMode>0</blendMode>
|
||||
<featureBlendMode>0</featureBlendMode>
|
||||
<layerGeometryType>2</layerGeometryType>
|
||||
</qgis>
|
||||
@@ -0,0 +1,304 @@
|
||||
"""sn_verfahrensgebiet/tests/test_beteiligung_pruefung.py
|
||||
|
||||
Unit-Tests für :mod:`sn_verfahrensgebiet.functions.beteiligung_pruefung`.
|
||||
|
||||
Getestete Szenarien:
|
||||
|
||||
baue_flurstueck_csv:
|
||||
- Normalfall mit führenden Nullen bei Zähler/Nenner
|
||||
- Nenner = 0 → Platzhalter "____"
|
||||
- Nenner leer → Platzhalter "____"
|
||||
- Ungültiges Format (nicht 4 Teile) → None
|
||||
- Leerer Wert → None
|
||||
|
||||
baue_flurstueck_csv_liste:
|
||||
- Bildet Liste nur aus gültigen Zeilen, schreibt Flurstueck_csv in jede Zeile zurück
|
||||
|
||||
lade_beteiligung_csv:
|
||||
- Dateidialog abgebrochen → None
|
||||
- Spalte FLURSTUECK fehlt → BeteiligungPruefungFehler
|
||||
- Leere Datei → BeteiligungPruefungFehler
|
||||
- Erfolgsfall → (zeilen, spaltennamen)
|
||||
|
||||
try2/try3 (kein automatisches Laden mehr, siehe Modul-Docstring):
|
||||
- Verfahrensgebiet-Layer ohne Kennzeichen vorhanden → Aufforderung statt Auto-Laden
|
||||
- Kein Verfahrensgebiet-Layer vorhanden → Verfahrensnummer aus CSV wird gesetzt,
|
||||
dann Aufforderung statt Auto-Laden
|
||||
"""
|
||||
|
||||
import csv
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
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.beteiligung_pruefung import (
|
||||
baue_flurstueck_csv,
|
||||
baue_flurstueck_csv_liste,
|
||||
lade_beteiligung_csv,
|
||||
beteiligung_pruefen,
|
||||
BeteiligungPruefungFehler,
|
||||
FELD_FLURSTUECK_CSV,
|
||||
CSV_SPALTE_FLURSTUECK,
|
||||
CSV_SPALTE_VEF_VKZF,
|
||||
_fordere_manuelles_laden_an,
|
||||
_try3_bootstrap,
|
||||
_hole_verfahrensgebiet_layer,
|
||||
)
|
||||
|
||||
_MOD = "sn_verfahrensgebiet.functions.beteiligung_pruefung"
|
||||
|
||||
|
||||
class TestBaueFlurstueckCsv(unittest.TestCase):
|
||||
def test_normalfall_mit_fuehrenden_nullen(self):
|
||||
ergebnis = baue_flurstueck_csv("1234/123/45/00", "14")
|
||||
self.assertEqual(ergebnis, "141234___00123004500")
|
||||
|
||||
def test_nenner_null_wird_zu_unterstrichen(self):
|
||||
ergebnis = baue_flurstueck_csv("1234/123/0/00", "14")
|
||||
self.assertEqual(ergebnis, "141234___00123____00")
|
||||
|
||||
def test_nenner_leer_wird_zu_unterstrichen(self):
|
||||
ergebnis = baue_flurstueck_csv("1234//45/00", "14")
|
||||
# Gemarkung 1234, Zähler leer -> "00000"? -> zfill auf leeren String ergibt "00000"
|
||||
self.assertTrue(ergebnis.startswith("141234___00000"))
|
||||
|
||||
def test_gemarkung_ohne_fuehrende_nullen_wird_aufgefuellt(self):
|
||||
ergebnis = baue_flurstueck_csv("12/1/1/00", "14")
|
||||
self.assertTrue(ergebnis.startswith("140012___"))
|
||||
|
||||
def test_ungueltiges_format_gibt_none(self):
|
||||
self.assertIsNone(baue_flurstueck_csv("1234/123/45", "14"))
|
||||
self.assertIsNone(baue_flurstueck_csv("1234/123/45/00/99", "14"))
|
||||
|
||||
def test_leerer_wert_gibt_none(self):
|
||||
self.assertIsNone(baue_flurstueck_csv("", "14"))
|
||||
self.assertIsNone(baue_flurstueck_csv(None, "14"))
|
||||
|
||||
def test_nenner_9001_wird_zu_buchstabe_a(self):
|
||||
ergebnis = baue_flurstueck_csv("1234/123/9001/00", "14")
|
||||
self.assertEqual(ergebnis, "141234___00123000a00")
|
||||
|
||||
def test_nenner_9002_wird_zu_buchstabe_b(self):
|
||||
ergebnis = baue_flurstueck_csv("1234/123/9002/00", "14")
|
||||
self.assertEqual(ergebnis, "141234___00123000b00")
|
||||
|
||||
def test_nenner_9026_wird_zu_buchstabe_z(self):
|
||||
ergebnis = baue_flurstueck_csv("1234/123/9026/00", "14")
|
||||
self.assertEqual(ergebnis, "141234___00123000z00")
|
||||
|
||||
def test_nenner_ausserhalb_buchstaben_bereich_bleibt_numerisch(self):
|
||||
ergebnis = baue_flurstueck_csv("1234/123/9027/00", "14")
|
||||
self.assertEqual(ergebnis, "141234___00123902700")
|
||||
|
||||
def test_nenner_9001_mit_fuehrenden_nullen_wird_erkannt(self):
|
||||
ergebnis = baue_flurstueck_csv("1234/123/09001/00", "14")
|
||||
self.assertEqual(ergebnis, "141234___00123000a00")
|
||||
|
||||
def test_einstellige_folge_wird_auf_2_stellen_aufgefuellt(self):
|
||||
ergebnis = baue_flurstueck_csv("1234/123/45/5", "14")
|
||||
self.assertEqual(ergebnis, "141234___00123004505")
|
||||
|
||||
|
||||
class TestBaueFlurstueckCsvListe(unittest.TestCase):
|
||||
def test_nur_gueltige_zeilen_in_liste(self):
|
||||
zeilen = [
|
||||
{CSV_SPALTE_FLURSTUECK: "1234/123/45/00"},
|
||||
{CSV_SPALTE_FLURSTUECK: "ungueltig"},
|
||||
{CSV_SPALTE_FLURSTUECK: "5678/1/0/01"},
|
||||
]
|
||||
ergebnis = baue_flurstueck_csv_liste(zeilen, "14")
|
||||
self.assertEqual(len(ergebnis), 2)
|
||||
self.assertEqual(zeilen[0][FELD_FLURSTUECK_CSV], "141234___00123004500")
|
||||
self.assertEqual(zeilen[1][FELD_FLURSTUECK_CSV], "")
|
||||
self.assertEqual(zeilen[2][FELD_FLURSTUECK_CSV], "145678___00001____01")
|
||||
|
||||
|
||||
class TestHoleVerfahrensgebietLayer(unittest.TestCase):
|
||||
@patch(f"{_MOD}.QgsProject")
|
||||
def test_layer_per_name_hat_vorrang_vor_combo_auswahl(self, mock_project_cls):
|
||||
namens_layer = MagicMock(name="verfahrensgebiet_layer")
|
||||
mock_project = MagicMock()
|
||||
mock_project.mapLayersByName.return_value = [namens_layer]
|
||||
mock_project_cls.instance.return_value = mock_project
|
||||
|
||||
andere_auswahl = MagicMock(name="andere_layer_in_combo")
|
||||
andere_auswahl.isValid.return_value = True
|
||||
tab_widget = MagicMock()
|
||||
tab_widget.verf_layer_combo.currentLayer.return_value = andere_auswahl
|
||||
|
||||
ergebnis = _hole_verfahrensgebiet_layer(tab_widget)
|
||||
|
||||
self.assertIs(ergebnis, namens_layer)
|
||||
|
||||
@patch(f"{_MOD}.QgsProject")
|
||||
def test_combo_auswahl_als_fallback_ohne_namens_layer(self, mock_project_cls):
|
||||
mock_project = MagicMock()
|
||||
mock_project.mapLayersByName.return_value = []
|
||||
mock_project_cls.instance.return_value = mock_project
|
||||
|
||||
combo_layer = MagicMock(name="combo_layer")
|
||||
combo_layer.isValid.return_value = True
|
||||
tab_widget = MagicMock()
|
||||
tab_widget.verf_layer_combo.currentLayer.return_value = combo_layer
|
||||
|
||||
ergebnis = _hole_verfahrensgebiet_layer(tab_widget)
|
||||
|
||||
self.assertIs(ergebnis, combo_layer)
|
||||
|
||||
@patch(f"{_MOD}.QgsProject")
|
||||
def test_kein_layer_gibt_none(self, mock_project_cls):
|
||||
mock_project = MagicMock()
|
||||
mock_project.mapLayersByName.return_value = []
|
||||
mock_project_cls.instance.return_value = mock_project
|
||||
|
||||
tab_widget = MagicMock()
|
||||
tab_widget.verf_layer_combo.currentLayer.return_value = None
|
||||
|
||||
self.assertIsNone(_hole_verfahrensgebiet_layer(tab_widget))
|
||||
|
||||
|
||||
class TestFordereManuellesLadenAn(unittest.TestCase):
|
||||
"""try2/try3 laden nicht mehr automatisch, sondern fordern den Nutzer auf,
|
||||
den regulären interaktiven Ablauf ('Aus ALKIS laden') selbst zu starten."""
|
||||
|
||||
def test_ohne_vef_vkzf_nennt_fehlende_kennzeichen(self):
|
||||
with self.assertRaises(BeteiligungPruefungFehler) as ctx:
|
||||
_fordere_manuelles_laden_an()
|
||||
self.assertIn("keine verwertbaren", str(ctx.exception))
|
||||
self.assertIn("Aus ALKIS laden", str(ctx.exception))
|
||||
|
||||
def test_mit_vef_vkzf_nennt_verfahrensnummer(self):
|
||||
with self.assertRaises(BeteiligungPruefungFehler) as ctx:
|
||||
_fordere_manuelles_laden_an("12345")
|
||||
self.assertIn("12345", str(ctx.exception))
|
||||
self.assertIn("Aus ALKIS laden", str(ctx.exception))
|
||||
|
||||
|
||||
class TestTry3Bootstrap(unittest.TestCase):
|
||||
@patch(f"{_MOD}.set_variable")
|
||||
def test_setzt_verfahrensnummer_und_fordert_manuelles_laden_an(self, mock_set_variable):
|
||||
zeilen = [{CSV_SPALTE_VEF_VKZF: "12345", CSV_SPALTE_FLURSTUECK: "1234/1/1/00"}]
|
||||
spaltennamen = [CSV_SPALTE_VEF_VKZF, CSV_SPALTE_FLURSTUECK]
|
||||
|
||||
with self.assertRaises(BeteiligungPruefungFehler) as ctx:
|
||||
_try3_bootstrap(object(), zeilen, spaltennamen)
|
||||
|
||||
mock_set_variable.assert_called_once_with("verfahrensnummer", "12345", scope="project")
|
||||
self.assertIn("12345", str(ctx.exception))
|
||||
|
||||
|
||||
class TestBeteiligungPruefenDispatch(unittest.TestCase):
|
||||
"""Dispatch-Logik in beteiligung_pruefen(): try1 unverändert, try2/try3
|
||||
fordern jetzt auf statt automatisch zu laden."""
|
||||
|
||||
@patch(f"{_MOD}._hat_verwendbare_kennzeichen", return_value=False)
|
||||
@patch(f"{_MOD}._hole_verfahrensgebiet_layer")
|
||||
@patch(f"{_MOD}.baue_flurstueck_csv_liste")
|
||||
@patch(f"{_MOD}.get_laenderkennung", return_value="14")
|
||||
@patch(f"{_MOD}.lade_beteiligung_csv")
|
||||
def test_layer_ohne_kennzeichen_fordert_manuelles_laden_an(
|
||||
self, mock_lade_csv, mock_laenderkennung, mock_liste, mock_hole_layer, mock_hat_kz
|
||||
):
|
||||
mock_lade_csv.return_value = ([{}], ["FLURSTUECK"])
|
||||
mock_liste.return_value = ["145678___00001____01"]
|
||||
mock_hole_layer.return_value = MagicMock(name="verf_layer_ohne_kz")
|
||||
|
||||
with self.assertRaises(BeteiligungPruefungFehler) as ctx:
|
||||
beteiligung_pruefen(object())
|
||||
self.assertIn("keine verwertbaren", str(ctx.exception))
|
||||
|
||||
@patch(f"{_MOD}.set_variable")
|
||||
@patch(f"{_MOD}._hole_verfahrensgebiet_layer", return_value=None)
|
||||
@patch(f"{_MOD}.baue_flurstueck_csv_liste")
|
||||
@patch(f"{_MOD}.get_laenderkennung", return_value="14")
|
||||
@patch(f"{_MOD}.lade_beteiligung_csv")
|
||||
def test_kein_layer_setzt_verfahrensnummer_und_fordert_laden_an(
|
||||
self, mock_lade_csv, mock_laenderkennung, mock_liste, mock_hole_layer, mock_set_variable
|
||||
):
|
||||
zeilen = [{CSV_SPALTE_VEF_VKZF: "12345", CSV_SPALTE_FLURSTUECK: "1234/1/1/00"}]
|
||||
mock_lade_csv.return_value = (zeilen, [CSV_SPALTE_VEF_VKZF, CSV_SPALTE_FLURSTUECK])
|
||||
mock_liste.return_value = ["145678___00001____01"]
|
||||
|
||||
with self.assertRaises(BeteiligungPruefungFehler) as ctx:
|
||||
beteiligung_pruefen(object())
|
||||
|
||||
mock_set_variable.assert_called_once_with("verfahrensnummer", "12345", scope="project")
|
||||
self.assertIn("12345", str(ctx.exception))
|
||||
|
||||
|
||||
class TestLadeBeteiligungCsv(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self._tmp_files = []
|
||||
|
||||
def tearDown(self):
|
||||
for pfad in self._tmp_files:
|
||||
try:
|
||||
os.remove(pfad)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
def _schreibe_csv(self, header, zeilen):
|
||||
fd, pfad = tempfile.mkstemp(suffix=".csv")
|
||||
self._tmp_files.append(pfad)
|
||||
with os.fdopen(fd, "w", encoding="utf-8", newline="") as f:
|
||||
writer = csv.writer(f)
|
||||
writer.writerow(header)
|
||||
for zeile in zeilen:
|
||||
writer.writerow(zeile)
|
||||
return pfad
|
||||
|
||||
@patch(f"{_MOD}.QFileDialog")
|
||||
def test_dialog_abgebrochen_gibt_none(self, mock_fd):
|
||||
mock_fd.getOpenFileName.return_value = ("", "")
|
||||
self.assertIsNone(lade_beteiligung_csv(None))
|
||||
|
||||
@patch(f"{_MOD}.QFileDialog")
|
||||
def test_fehlende_flurstueck_spalte_wirft_fehler(self, mock_fd):
|
||||
pfad = self._schreibe_csv(["FOO", "BAR"], [["1", "2"]])
|
||||
mock_fd.getOpenFileName.return_value = (pfad, "")
|
||||
with self.assertRaises(BeteiligungPruefungFehler) as ctx:
|
||||
lade_beteiligung_csv(None)
|
||||
self.assertIn(CSV_SPALTE_FLURSTUECK, str(ctx.exception))
|
||||
|
||||
@patch(f"{_MOD}.QFileDialog")
|
||||
def test_leere_datei_wirft_fehler(self, mock_fd):
|
||||
fd, pfad = tempfile.mkstemp(suffix=".csv")
|
||||
os.close(fd)
|
||||
self._tmp_files.append(pfad)
|
||||
mock_fd.getOpenFileName.return_value = (pfad, "")
|
||||
with self.assertRaises(BeteiligungPruefungFehler):
|
||||
lade_beteiligung_csv(None)
|
||||
|
||||
@patch(f"{_MOD}.QFileDialog")
|
||||
def test_erfolgsfall_gibt_zeilen_und_spalten_zurueck(self, mock_fd):
|
||||
pfad = self._schreibe_csv(
|
||||
[CSV_SPALTE_VEF_VKZF, "FLA_Beteiligung", CSV_SPALTE_FLURSTUECK],
|
||||
[["12345", "1", "1234/123/45/00"]],
|
||||
)
|
||||
mock_fd.getOpenFileName.return_value = (pfad, "")
|
||||
zeilen, spalten = lade_beteiligung_csv(None)
|
||||
self.assertEqual(len(zeilen), 1)
|
||||
self.assertIn(CSV_SPALTE_FLURSTUECK, spalten)
|
||||
self.assertEqual(zeilen[0][CSV_SPALTE_FLURSTUECK], "1234/123/45/00")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -186,7 +186,7 @@ class TestVerfahrensgebietAlkis(unittest.TestCase):
|
||||
self.assertEqual(status, vg.LoadStatus.FIRST)
|
||||
self.assertEqual(len(feats), 1)
|
||||
|
||||
@patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.ask_bror_kombination")
|
||||
@patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.ask_bror_kombinationen")
|
||||
@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))
|
||||
@@ -202,7 +202,7 @@ class TestVerfahrensgebietAlkis(unittest.TestCase):
|
||||
temp_layer = _DummyTempLayer(valid=True, features=[feat1, feat2])
|
||||
memory_layer = _DummyMemoryLayer()
|
||||
mock_qgsvectorlayer.side_effect = [temp_layer, memory_layer]
|
||||
mock_dialog.return_value = ("ADV-A", "VKZ12/001")
|
||||
mock_dialog.return_value = [("ADV-A", "VKZ12/001")]
|
||||
|
||||
mock_feature = mock_feature_cls.return_value
|
||||
mock_feature.setGeometry.return_value = None
|
||||
@@ -216,7 +216,7 @@ class TestVerfahrensgebietAlkis(unittest.TestCase):
|
||||
# Nur feat1 gehört zur gewählten Kombination
|
||||
self.assertEqual(feats, [feat1])
|
||||
|
||||
@patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.ask_bror_kombination")
|
||||
@patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.ask_bror_kombinationen")
|
||||
@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")
|
||||
@@ -229,7 +229,7 @@ class TestVerfahrensgebietAlkis(unittest.TestCase):
|
||||
feat2 = _DummyFeature(attrs={"adv_name": "ADV-B", "bezeichnung": "VKZ12/002"})
|
||||
temp_layer = _DummyTempLayer(valid=True, features=[feat1, feat2])
|
||||
mock_qgsvectorlayer.return_value = temp_layer
|
||||
mock_dialog.return_value = None # Nutzer bricht ab
|
||||
mock_dialog.return_value = [] # Nutzer bricht ab
|
||||
|
||||
layer, status, feats = vg.verfahrensgebiet_alkis(tab_widget=None)
|
||||
|
||||
@@ -238,6 +238,35 @@ class TestVerfahrensgebietAlkis(unittest.TestCase):
|
||||
self.assertEqual(status, vg.LoadStatus.NONE)
|
||||
self.assertEqual(feats, [])
|
||||
|
||||
@patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.ask_bror_kombinationen")
|
||||
@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_multi_kombination_mehrfachauswahl(
|
||||
self, mock_qgsvectorlayer, _mock_get_var, _mock_check, _mock_union, mock_feature_cls, mock_dialog
|
||||
):
|
||||
"""Zwei verschiedene Kombinationen → beide ausgewählt → beide Feature-Gruppen genutzt."""
|
||||
feat1 = _DummyFeature(attrs={"adv_name": "ADV-A", "bezeichnung": "VKZ12/001"})
|
||||
feat2 = _DummyFeature(attrs={"adv_name": "ADV-B", "bezeichnung": "VKZ12/002"})
|
||||
temp_layer = _DummyTempLayer(valid=True, features=[feat1, feat2])
|
||||
memory_layer = _DummyMemoryLayer()
|
||||
mock_qgsvectorlayer.side_effect = [temp_layer, memory_layer]
|
||||
mock_dialog.return_value = [("ADV-A", "VKZ12/001"), ("ADV-B", "VKZ12/002")]
|
||||
|
||||
mock_feature = mock_feature_cls.return_value
|
||||
mock_feature.setGeometry.return_value = None
|
||||
mock_feature.setAttributes.return_value = None
|
||||
|
||||
layer, status, feats = vg.verfahrensgebiet_alkis(tab_widget=None)
|
||||
|
||||
mock_dialog.assert_called_once()
|
||||
self.assertIs(layer, memory_layer)
|
||||
self.assertEqual(status, vg.LoadStatus.FIRST)
|
||||
self.assertEqual(feats, [feat1, feat2])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -129,7 +129,7 @@ def _make_progress_mock(canceled=False):
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_MOD = "sn_verfahrensgebiet.functions.verfahrensgebiet_alkis_komplett"
|
||||
_PATCH_VG_ALKIS = f"{_MOD}.verfahrensgebiet_alkis"
|
||||
_PATCH_VG_ALKIS = f"{_MOD}._suche_verfahrensgebiet_features"
|
||||
_PATCH_GEM = f"{_MOD}.gemarkung_schluessel_und_namen_von_vg_geometrie"
|
||||
_PATCH_FLST = f"{_MOD}.flurstueck_alkis"
|
||||
_PATCH_SELECT = f"{_MOD}._select_by_location"
|
||||
@@ -146,6 +146,9 @@ _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"
|
||||
_PATCH_LANDKREIS_SUCHE = f"{_MOD}._suche_landkreis_kombinationen"
|
||||
_PATCH_LANDKREIS_BAU = f"{_MOD}._baue_layer_aus_landkreis_auswahl"
|
||||
_PATCH_ASK_LANDKREIS = f"{_MOD}.ask_landkreis_auswahl"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -265,9 +268,10 @@ class TestVerfahrensgebietAlkisKomplett(unittest.TestCase):
|
||||
@patch(_PATCH_PROGRESS)
|
||||
@patch(_PATCH_PROJECT)
|
||||
@patch(_PATCH_VG_ALKIS)
|
||||
def test_vg_alkis_none_aborts(self, mock_vg, mock_proj, mock_prog):
|
||||
@patch(_PATCH_GET_VAR, return_value="VF-2024-001")
|
||||
def test_vg_alkis_none_aborts(self, mock_get_var, mock_vg, mock_proj, mock_prog):
|
||||
mock_proj.instance.return_value = _DummyProject()
|
||||
mock_vg.return_value = (None, LoadStatus.NONE, [])
|
||||
mock_vg.return_value = (None, LoadStatus.NONE, {}, None)
|
||||
mock_prog.return_value = _make_progress_mock()
|
||||
|
||||
enriched, flst, bror, status = verfahrensgebiet_alkis_komplett(None)
|
||||
@@ -281,10 +285,11 @@ class TestVerfahrensgebietAlkisKomplett(unittest.TestCase):
|
||||
@patch(_PATCH_PROJECT)
|
||||
@patch(_PATCH_GEM)
|
||||
@patch(_PATCH_VG_ALKIS)
|
||||
def test_flst_none_aborts(self, mock_vg, mock_gem, mock_proj, mock_prog):
|
||||
@patch(_PATCH_GET_VAR, return_value="VF-2024-001")
|
||||
def test_flst_none_aborts(self, mock_get_var, 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_vg.return_value = (vg_layer, LoadStatus.FIRST, {}, None)
|
||||
mock_gem.return_value = [("1234", "Muster")]
|
||||
mock_prog.return_value = _make_progress_mock()
|
||||
|
||||
@@ -302,13 +307,14 @@ class TestVerfahrensgebietAlkisKomplett(unittest.TestCase):
|
||||
@patch(_PATCH_GEM)
|
||||
@patch(_PATCH_FLST)
|
||||
@patch(_PATCH_VG_ALKIS)
|
||||
@patch(_PATCH_GET_VAR, return_value="VF-2024-001")
|
||||
def test_no_selection_returns_none_enriched(
|
||||
self, mock_vg, mock_flst, mock_gem, mock_proj, mock_select, mock_msgbox, mock_prog
|
||||
self, mock_get_var, 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_vg.return_value = (vg_layer, LoadStatus.FIRST, {}, None)
|
||||
mock_gem.return_value = []
|
||||
mock_flst.return_value = (flst_layer, LoadStatus.FIRST)
|
||||
mock_select.return_value = 0
|
||||
@@ -330,15 +336,16 @@ class TestVerfahrensgebietAlkisKomplett(unittest.TestCase):
|
||||
@patch(_PATCH_GEM)
|
||||
@patch(_PATCH_FLST)
|
||||
@patch(_PATCH_VG_ALKIS)
|
||||
@patch(_PATCH_GET_VAR, return_value="VF-2024-001")
|
||||
def test_invalid_enriched_layer_returns_none(
|
||||
self, mock_vg, mock_flst, mock_gem, mock_proj, mock_select, mock_erstelle, mock_msgbox, mock_prog
|
||||
self, mock_get_var, 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_vg.return_value = (vg_layer, LoadStatus.FIRST, {}, None)
|
||||
mock_gem.return_value = []
|
||||
mock_flst.return_value = (flst_layer, LoadStatus.FIRST)
|
||||
mock_select.return_value = 5
|
||||
@@ -356,11 +363,12 @@ class TestVerfahrensgebietAlkisKomplett(unittest.TestCase):
|
||||
@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):
|
||||
@patch(_PATCH_GET_VAR, return_value="VF-2024-001")
|
||||
def test_progress_cancel_after_step1_returns_none(self, mock_get_var, 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, [])
|
||||
mock_vg.return_value = (vg_layer, LoadStatus.FIRST, {}, None)
|
||||
|
||||
prog = MagicMock()
|
||||
prog.is_canceled.side_effect = [False, True] # 1. Aufruf False, 2. True
|
||||
@@ -398,7 +406,7 @@ class TestVerfahrensgebietAlkisKomplett(unittest.TestCase):
|
||||
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_vg.return_value = (vg_layer, LoadStatus.FIRST, {}, None)
|
||||
mock_gem.return_value = [("1234", "Muster")]
|
||||
mock_flst.return_value = (flst_layer, LoadStatus.FIRST)
|
||||
mock_select.return_value = 3
|
||||
@@ -437,18 +445,18 @@ class TestVerfahrensgebietAlkisKomplett(unittest.TestCase):
|
||||
flst_layer = _make_layer(name="Flurstücke")
|
||||
enriched = _make_layer(name="Verfahrensgebiet", valid=True)
|
||||
|
||||
mock_vg.return_value = (vg_layer, LoadStatus.FIRST, [])
|
||||
mock_vg.return_value = (vg_layer, LoadStatus.FIRST, {}, None)
|
||||
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_get_var.return_value = "VF-2024-001"
|
||||
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, "")
|
||||
mock_vkz.assert_called_once_with(enriched, "VF-2024-001")
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Flächenvergleich – Abweichung + Abbrechen
|
||||
@@ -476,12 +484,12 @@ class TestVerfahrensgebietAlkisKomplett(unittest.TestCase):
|
||||
flst_layer = _make_layer(name="Flurstücke")
|
||||
enriched = _make_layer(name="Verfahrensgebiet", valid=True)
|
||||
|
||||
mock_vg.return_value = (vg_layer, LoadStatus.FIRST, [])
|
||||
mock_vg.return_value = (vg_layer, LoadStatus.FIRST, {}, None)
|
||||
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_get_var.return_value = "VF-2024-001"
|
||||
mock_vergl.return_value = (1000.0, 800.0, False)
|
||||
mock_ask.return_value = "abbrechen"
|
||||
mock_prog.return_value = _make_progress_mock()
|
||||
@@ -521,12 +529,12 @@ class TestVerfahrensgebietAlkisKomplett(unittest.TestCase):
|
||||
flst_layer = _make_layer(name="Flurstücke")
|
||||
enriched = _make_layer(name="Verfahrensgebiet", valid=True)
|
||||
|
||||
mock_vg.return_value = (vg_layer, LoadStatus.FIRST, [])
|
||||
mock_vg.return_value = (vg_layer, LoadStatus.FIRST, {}, None)
|
||||
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_get_var.return_value = "VF-2024-001"
|
||||
mock_vergl.return_value = (1000.0, 800.0, False)
|
||||
mock_ask.return_value = "als_vg_laden"
|
||||
mock_prog.return_value = _make_progress_mock()
|
||||
@@ -577,12 +585,12 @@ class TestVerfahrensgebietAlkisKomplett(unittest.TestCase):
|
||||
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_vg.return_value = (vg_layer, LoadStatus.FIRST, {}, None)
|
||||
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_get_var.return_value = "VF-2024-001"
|
||||
mock_vergl.return_value = (1000.0, 800.0, False)
|
||||
mock_ask.return_value = "detailpruefung"
|
||||
mock_extrahiere.return_value = [MagicMock(), MagicMock(), MagicMock()] # 3 Knickpunkte
|
||||
@@ -630,12 +638,12 @@ class TestVerfahrensgebietAlkisKomplett(unittest.TestCase):
|
||||
flst_layer = _make_layer(name="Flurstücke")
|
||||
enriched = _make_layer(name="Verfahrensgebiet", valid=True)
|
||||
|
||||
mock_vg.return_value = (vg_layer, LoadStatus.FIRST, [])
|
||||
mock_vg.return_value = (vg_layer, LoadStatus.FIRST, {}, None)
|
||||
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_get_var.return_value = "VF-2024-001"
|
||||
mock_vergl.return_value = (1000.0, 1000.0, True)
|
||||
mock_prog.return_value = _make_progress_mock()
|
||||
|
||||
@@ -647,15 +655,16 @@ class TestVerfahrensgebietAlkisKomplett(unittest.TestCase):
|
||||
@patch(_PATCH_PROJECT)
|
||||
@patch(_PATCH_GEM)
|
||||
@patch(_PATCH_VG_ALKIS)
|
||||
@patch(_PATCH_GET_VAR, return_value="VF-2024-001")
|
||||
def test_bror_layer_wird_bei_fehler_entfernt(
|
||||
self, mock_vg, mock_gem, mock_proj, mock_prog
|
||||
self, mock_get_var, 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_vg.return_value = (vg_layer, LoadStatus.FIRST, {}, None)
|
||||
mock_gem.return_value = []
|
||||
mock_prog.return_value = _make_progress_mock()
|
||||
|
||||
@@ -664,6 +673,110 @@ class TestVerfahrensgebietAlkisKomplett(unittest.TestCase):
|
||||
|
||||
self.assertIn("BROR_ERR", proj_inst.removed)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Landkreis-Fallback (keine Verfahrensnummer)
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
@patch(_PATCH_PROGRESS)
|
||||
@patch(_PATCH_MSGBOX)
|
||||
@patch(_PATCH_ASK_LANDKREIS, return_value=None)
|
||||
@patch(_PATCH_GET_VAR, return_value="")
|
||||
def test_kein_vkz_kein_landkreis_dialog_abgebrochen(
|
||||
self, mock_get_var, mock_ask_landkreis, mock_msgbox, mock_prog
|
||||
):
|
||||
"""Weder Verfahrensnummer noch Landkreis gesetzt, Nutzer bricht den
|
||||
Landkreis-Auswahldialog ab → stiller Abbruch, kein Layer, KEINE
|
||||
Fehlermeldung (bewusster Abbruch, kein Fehlerzustand)."""
|
||||
enriched, flst, bror, status = verfahrensgebiet_alkis_komplett(None)
|
||||
|
||||
self.assertIsNone(enriched)
|
||||
self.assertIsNone(flst)
|
||||
self.assertIsNone(bror)
|
||||
self.assertEqual(status, LoadStatus.NONE)
|
||||
mock_msgbox.critical.assert_not_called()
|
||||
|
||||
@patch(_PATCH_PROGRESS)
|
||||
@patch(_PATCH_PROJECT)
|
||||
@patch(_PATCH_LANDKREIS_BAU)
|
||||
@patch(_PATCH_LANDKREIS_SUCHE)
|
||||
@patch(_PATCH_ASK_LANDKREIS, return_value="22")
|
||||
@patch(_PATCH_GET_VAR, return_value="")
|
||||
def test_kein_vkz_kein_landkreis_dialog_liefert_landkreis(
|
||||
self, mock_get_var, mock_ask_landkreis, mock_such, mock_bau, mock_proj, mock_prog
|
||||
):
|
||||
"""Weder Verfahrensnummer noch Landkreis gesetzt, Nutzer wählt einen
|
||||
Landkreis im Dialog → Landkreis-Suchpfad wird mit dieser Kennzahl
|
||||
verwendet (wie im Fall 'Landkreis aus Projekteigenschaften')."""
|
||||
proj_inst = _DummyProject()
|
||||
mock_proj.instance.return_value = proj_inst
|
||||
vg_layer = _make_layer(name="BauRaumOrdnungsrecht (ALKIS)")
|
||||
mock_such.return_value = (None, LoadStatus.FIRST, {("A", "22015"): [MagicMock()]}, "EPSG:25833")
|
||||
mock_bau.return_value = (vg_layer, LoadStatus.FIRST, True) # nur_laden=True
|
||||
mock_prog.return_value = _make_progress_mock()
|
||||
|
||||
enriched, flst, bror, status = verfahrensgebiet_alkis_komplett(None)
|
||||
|
||||
mock_ask_landkreis.assert_called_once()
|
||||
mock_such.assert_called_once_with(None, "22")
|
||||
mock_bau.assert_called_once()
|
||||
self.assertIs(enriched, vg_layer)
|
||||
self.assertEqual(status, LoadStatus.FIRST)
|
||||
|
||||
@patch(_PATCH_PROGRESS)
|
||||
@patch(_PATCH_PROJECT)
|
||||
@patch(_PATCH_LANDKREIS_BAU)
|
||||
@patch(_PATCH_LANDKREIS_SUCHE)
|
||||
def test_landkreis_modus_wird_bei_fehlender_vkz_verwendet(
|
||||
self, mock_such, mock_bau, mock_proj, mock_prog
|
||||
):
|
||||
"""Keine Verfahrensnummer, aber Landkreis gesetzt → Landkreis-Suchpfad
|
||||
statt VKZ-Pfad wird verwendet."""
|
||||
|
||||
def _get_var_side_effect(key, scope="project"):
|
||||
return {"verfahrensnummer": "", "landkreis_kennzahl": "22"}.get(key, "")
|
||||
|
||||
proj_inst = _DummyProject()
|
||||
mock_proj.instance.return_value = proj_inst
|
||||
vg_layer = _make_layer(name="BauRaumOrdnungsrecht (ALKIS)")
|
||||
mock_such.return_value = (None, LoadStatus.FIRST, {("A", "22015"): [MagicMock()]}, "EPSG:25833")
|
||||
mock_bau.return_value = (vg_layer, LoadStatus.FIRST, True) # nur_laden=True
|
||||
mock_prog.return_value = _make_progress_mock()
|
||||
|
||||
with patch(_PATCH_GET_VAR, side_effect=_get_var_side_effect):
|
||||
enriched, flst, bror, status = verfahrensgebiet_alkis_komplett(None)
|
||||
|
||||
mock_such.assert_called_once()
|
||||
mock_bau.assert_called_once()
|
||||
self.assertIs(enriched, vg_layer)
|
||||
self.assertIsNone(flst)
|
||||
self.assertIsNone(bror)
|
||||
self.assertEqual(status, LoadStatus.FIRST)
|
||||
|
||||
@patch(_PATCH_PROGRESS)
|
||||
@patch(_PATCH_PROJECT)
|
||||
@patch(_PATCH_LANDKREIS_BAU)
|
||||
@patch(_PATCH_LANDKREIS_SUCHE)
|
||||
def test_landkreis_modus_nur_laden_ueberspringt_dialog2(
|
||||
self, mock_such, mock_bau, mock_proj, mock_prog
|
||||
):
|
||||
"""nur_laden=True → Funktion kehrt sofort zurück, Dialog 2 (Gemarkung
|
||||
etc.) wird nicht gestartet."""
|
||||
|
||||
def _get_var_side_effect(key, scope="project"):
|
||||
return {"verfahrensnummer": "", "landkreis_kennzahl": "22"}.get(key, "")
|
||||
|
||||
proj_inst = _DummyProject()
|
||||
mock_proj.instance.return_value = proj_inst
|
||||
vg_layer = _make_layer(name="BauRaumOrdnungsrecht (ALKIS)")
|
||||
mock_such.return_value = (None, LoadStatus.FIRST, {("A", "22015"): [MagicMock()]}, "EPSG:25833")
|
||||
mock_bau.return_value = (vg_layer, LoadStatus.FIRST, True)
|
||||
mock_prog.return_value = _make_progress_mock()
|
||||
|
||||
with patch(_PATCH_GET_VAR, side_effect=_get_var_side_effect), \
|
||||
patch(_PATCH_GEM) as mock_gem:
|
||||
verfahrensgebiet_alkis_komplett(None)
|
||||
mock_gem.assert_not_called()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -0,0 +1,932 @@
|
||||
"""sn_verfahrensgebiet/tests/test_verfahrensgebiet_shape_komplett.py
|
||||
|
||||
Unit-Tests für :mod:`sn_verfahrensgebiet.functions.verfahrensgebiet_shape_komplett`.
|
||||
|
||||
Getestete Szenarien:
|
||||
|
||||
lade_vg_aus_zip:
|
||||
- Dateidialog abgebrochen (leerer Pfad) → None
|
||||
- ZIP-Entpacken schlägt fehl (Exception) → None + QMessageBox.warning
|
||||
- Kein .shp in ZIP → None + Warning "gültiges Shape-Dateiset angeben"
|
||||
- QgsVectorLayer nicht valid → None + Warning
|
||||
- Leere Geometrien im Shapefile → None + Warning
|
||||
- Erfolgspfad → Memory-Layer mit Union-Geometrie, Name "Verfahrensgebiet"
|
||||
|
||||
verfahrensgebiet_shape_komplett:
|
||||
- lade_vg_aus_zip None (Abbruch) → (None, None, None, NONE)
|
||||
- flurstueck_alkis NONE → (None, None, None, NONE)
|
||||
- Keine Flurstücke selektiert → (None, flst, None, NONE) + QMessageBox.warning
|
||||
- erstelle_verfahrensgebiet_aus_auswahl ungültig → Workflow noch immer abgeschlossen
|
||||
(temp_enriched-Fehler: stimmt_ueberein=True wegen 0-Fläche)
|
||||
- Flächenübereinstimmung → show_info_dialog, return (shape_vg, flst, None, FIRST)
|
||||
- Flächenabweichung + abbrechen → return (shape_vg, flst, None, FIRST)
|
||||
- Flächenabweichung + detailpruefung → lade_punktort_ta + knickpunkt_pruefung aufgerufen
|
||||
- Cleanup: TempVerfahrensgebiet stets aus Projekt entfernt
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import types
|
||||
import unittest
|
||||
from unittest.mock import patch, MagicMock, call
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 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_shape_komplett import (
|
||||
verfahrensgebiet_shape_komplett,
|
||||
lade_vg_aus_zip,
|
||||
_finde_shp,
|
||||
_erstelle_memory_layer_aus_shp,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Hilfs-Dummies
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _make_layer(valid=True, name="Layer", lid="L1", feature_count=1, area=1000.0):
|
||||
layer = MagicMock()
|
||||
layer.isValid.return_value = valid
|
||||
layer.id.return_value = lid
|
||||
layer.name.return_value = name
|
||||
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_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 mapLayer(self, lid):
|
||||
return None
|
||||
|
||||
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_shape_komplett"
|
||||
_PATCH_LADE_ZIP = f"{_MOD}.lade_vg_aus_zip"
|
||||
_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_PROJECT = f"{_MOD}.QgsProject"
|
||||
_PATCH_MSGBOX = f"{_MOD}.QMessageBox"
|
||||
_PATCH_WARNING = f"{_MOD}.warning"
|
||||
_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_abbrechen"
|
||||
_PATCH_GP_ALKIS = f"{_MOD}.lade_punktort_ta"
|
||||
_PATCH_KNICK = f"{_MOD}.knickpunkt_pruefung"
|
||||
_PATCH_EXTRAHIERE = f"{_MOD}._extrahiere_knickpunkte"
|
||||
_PATCH_LESE_TOL = f"{_MOD}._lese_flaechen_toleranz"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests: _finde_shp
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestFindShp(unittest.TestCase):
|
||||
|
||||
def test_findet_shp_in_root(self):
|
||||
with patch("os.walk") as mock_walk:
|
||||
mock_walk.return_value = [
|
||||
("/extract", [], ["shape.shp", "shape.dbf"]),
|
||||
]
|
||||
result = _finde_shp("/extract")
|
||||
self.assertEqual(result, os.path.join("/extract", "shape.shp"))
|
||||
|
||||
def test_findet_shp_in_unterordner(self):
|
||||
with patch("os.walk") as mock_walk:
|
||||
mock_walk.return_value = [
|
||||
("/extract", ["sub"], []),
|
||||
("/extract/sub", [], ["daten.shp"]),
|
||||
]
|
||||
result = _finde_shp("/extract")
|
||||
self.assertEqual(result, os.path.join("/extract/sub", "daten.shp"))
|
||||
|
||||
def test_kein_shp_gibt_none_zurueck(self):
|
||||
with patch("os.walk") as mock_walk:
|
||||
mock_walk.return_value = [
|
||||
("/extract", [], ["shape.dbf", "shape.prj"]),
|
||||
]
|
||||
result = _finde_shp("/extract")
|
||||
self.assertIsNone(result)
|
||||
|
||||
def test_case_insensitive_erkennung(self):
|
||||
with patch("os.walk") as mock_walk:
|
||||
mock_walk.return_value = [
|
||||
("/extract", [], ["DATEN.SHP"]),
|
||||
]
|
||||
result = _finde_shp("/extract")
|
||||
self.assertIsNotNone(result)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests: _erstelle_memory_layer_aus_shp
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestErstelleMemoryLayerAusShp(unittest.TestCase):
|
||||
|
||||
def test_leere_geometrien_geben_none(self):
|
||||
shp_layer = MagicMock()
|
||||
geom = MagicMock()
|
||||
geom.isEmpty.return_value = True
|
||||
feat = MagicMock()
|
||||
feat.geometry.return_value = geom
|
||||
shp_layer.getFeatures.return_value = [feat]
|
||||
shp_layer.crs.return_value.authid.return_value = "EPSG:25833"
|
||||
|
||||
result = _erstelle_memory_layer_aus_shp(shp_layer)
|
||||
self.assertIsNone(result)
|
||||
|
||||
def test_keine_features_gibt_none(self):
|
||||
shp_layer = MagicMock()
|
||||
shp_layer.getFeatures.return_value = []
|
||||
shp_layer.crs.return_value.authid.return_value = "EPSG:25833"
|
||||
|
||||
result = _erstelle_memory_layer_aus_shp(shp_layer)
|
||||
self.assertIsNone(result)
|
||||
|
||||
@patch(f"{_MOD}.QgsGeometry")
|
||||
@patch(f"{_MOD}.QgsVectorLayer")
|
||||
@patch(f"{_MOD}.QgsFeature")
|
||||
def test_attribute_von_erstem_feature_werden_gesetzt(
|
||||
self, mock_feat_cls, mock_vl_cls, mock_geom_cls
|
||||
):
|
||||
"""Attributwerte des ersten Features werden auf das Union-Feature übertragen."""
|
||||
# Geometrie-Mock
|
||||
mock_union = MagicMock()
|
||||
mock_union.isEmpty.return_value = False
|
||||
mock_geom_cls.unaryUnion.return_value = mock_union
|
||||
|
||||
# Memory-Layer-Mock
|
||||
mock_mem = MagicMock()
|
||||
mock_mem.isValid.return_value = True
|
||||
mock_mem.featureCount.return_value = 1
|
||||
mock_vl_cls.return_value = mock_mem
|
||||
mock_pr = MagicMock()
|
||||
mock_mem.dataProvider.return_value = mock_pr
|
||||
|
||||
# Feature-Mock (Output)
|
||||
mock_feat_out = MagicMock()
|
||||
mock_feat_cls.return_value = mock_feat_out
|
||||
|
||||
# Shapefile-Layer-Mock
|
||||
shp_layer = MagicMock()
|
||||
shp_layer.crs.return_value.authid.return_value = "EPSG:25833"
|
||||
shp_layer.fields.return_value = [MagicMock(), MagicMock()] # list → iterable
|
||||
|
||||
mock_geom = MagicMock()
|
||||
mock_geom.isEmpty.return_value = False
|
||||
src_feat = MagicMock()
|
||||
src_feat.geometry.return_value = mock_geom
|
||||
src_feat.attributes.return_value = ["wert_a", 42]
|
||||
shp_layer.getFeatures.return_value = [src_feat]
|
||||
|
||||
result = _erstelle_memory_layer_aus_shp(shp_layer)
|
||||
|
||||
# Felder sollen übernommen werden
|
||||
mock_pr.addAttributes.assert_called_once()
|
||||
# Attributwerte sollen vom ersten Feature gesetzt werden
|
||||
mock_feat_out.setAttributes.assert_called_once_with(["wert_a", 42])
|
||||
self.assertIs(result, mock_mem)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests: lade_vg_aus_zip
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestLadeVgAusZip(unittest.TestCase):
|
||||
|
||||
def _patch(self, **kwargs):
|
||||
"""Hilfsmethode: kombinierter Patch-Kontext für lade_vg_aus_zip."""
|
||||
return kwargs
|
||||
|
||||
@patch(f"{_MOD}.QFileDialog")
|
||||
def test_dialog_abgebrochen_gibt_none_zurueck(self, mock_fd):
|
||||
mock_fd.getOpenFileName.return_value = ("", "")
|
||||
result = lade_vg_aus_zip(None)
|
||||
self.assertIsNone(result)
|
||||
|
||||
@patch(f"{_MOD}.QMessageBox")
|
||||
@patch(f"{_MOD}.zipfile")
|
||||
@patch(f"{_MOD}.QFileDialog")
|
||||
def test_zip_fehler_gibt_none_und_warning(self, mock_fd, mock_zf, mock_mb):
|
||||
mock_fd.getOpenFileName.return_value = ("/tmp/vg.zip", "")
|
||||
mock_zf.ZipFile.side_effect = Exception("bad zip")
|
||||
|
||||
with patch("os.path.dirname", return_value="/tmp"), \
|
||||
patch("os.path.splitext", return_value=("/tmp/vg", ".zip")), \
|
||||
patch("os.path.basename", return_value="vg.zip"), \
|
||||
patch("os.path.join", return_value="/tmp/vg"):
|
||||
result = lade_vg_aus_zip(None)
|
||||
|
||||
self.assertIsNone(result)
|
||||
mock_mb.warning.assert_called_once()
|
||||
|
||||
@patch(f"{_MOD}.QMessageBox")
|
||||
@patch(f"{_MOD}._finde_shp", return_value=None)
|
||||
@patch(f"{_MOD}.zipfile")
|
||||
@patch(f"{_MOD}.QFileDialog")
|
||||
def test_kein_shp_in_zip_gibt_none_und_warning(self, mock_fd, mock_zf, mock_find, mock_mb):
|
||||
mock_fd.getOpenFileName.return_value = ("/tmp/vg.zip", "")
|
||||
mock_zf.ZipFile.return_value.__enter__ = MagicMock(return_value=MagicMock())
|
||||
mock_zf.ZipFile.return_value.__exit__ = MagicMock(return_value=False)
|
||||
|
||||
with patch("os.path.dirname", return_value="/tmp"), \
|
||||
patch("os.path.splitext", return_value=("/tmp/vg", ".zip")), \
|
||||
patch("os.path.basename", return_value="vg.zip"), \
|
||||
patch("os.path.join", return_value="/tmp/vg"):
|
||||
result = lade_vg_aus_zip(None)
|
||||
|
||||
self.assertIsNone(result)
|
||||
mock_mb.warning.assert_called_once()
|
||||
args = mock_mb.warning.call_args[0]
|
||||
self.assertIn("gültiges Shape-Dateiset angeben", args)
|
||||
|
||||
@patch(f"{_MOD}.QMessageBox")
|
||||
@patch(f"{_MOD}.QgsVectorLayer")
|
||||
@patch(f"{_MOD}._finde_shp", return_value="/tmp/vg/shape.shp")
|
||||
@patch(f"{_MOD}.zipfile")
|
||||
@patch(f"{_MOD}.QFileDialog")
|
||||
def test_invalider_qgslayer_gibt_none_und_warning(
|
||||
self, mock_fd, mock_zf, mock_find, mock_vl, mock_mb
|
||||
):
|
||||
mock_fd.getOpenFileName.return_value = ("/tmp/vg.zip", "")
|
||||
mock_zf.ZipFile.return_value.__enter__ = MagicMock(return_value=MagicMock())
|
||||
mock_zf.ZipFile.return_value.__exit__ = MagicMock(return_value=False)
|
||||
|
||||
invalid_layer = MagicMock()
|
||||
invalid_layer.isValid.return_value = False
|
||||
mock_vl.return_value = invalid_layer
|
||||
|
||||
with patch("os.path.dirname", return_value="/tmp"), \
|
||||
patch("os.path.splitext", return_value=("/tmp/vg", ".zip")), \
|
||||
patch("os.path.basename", return_value="vg.zip"), \
|
||||
patch("os.path.join", return_value="/tmp/vg"):
|
||||
result = lade_vg_aus_zip(None)
|
||||
|
||||
self.assertIsNone(result)
|
||||
mock_mb.warning.assert_called_once()
|
||||
args = mock_mb.warning.call_args[0]
|
||||
self.assertIn("gültiges Shape-Dateiset angeben", args)
|
||||
|
||||
@patch(f"{_MOD}.QMessageBox")
|
||||
@patch(f"{_MOD}._erstelle_memory_layer_aus_shp", return_value=None)
|
||||
@patch(f"{_MOD}.QgsVectorLayer")
|
||||
@patch(f"{_MOD}._finde_shp", return_value="/tmp/vg/shape.shp")
|
||||
@patch(f"{_MOD}.zipfile")
|
||||
@patch(f"{_MOD}.QFileDialog")
|
||||
def test_leere_geometrien_geben_none_und_warning(
|
||||
self, mock_fd, mock_zf, mock_find, mock_vl, mock_mem, mock_mb
|
||||
):
|
||||
mock_fd.getOpenFileName.return_value = ("/tmp/vg.zip", "")
|
||||
mock_zf.ZipFile.return_value.__enter__ = MagicMock(return_value=MagicMock())
|
||||
mock_zf.ZipFile.return_value.__exit__ = MagicMock(return_value=False)
|
||||
valid_layer = MagicMock()
|
||||
valid_layer.isValid.return_value = True
|
||||
mock_vl.return_value = valid_layer
|
||||
|
||||
with patch("os.path.dirname", return_value="/tmp"), \
|
||||
patch("os.path.splitext", return_value=("/tmp/vg", ".zip")), \
|
||||
patch("os.path.basename", return_value="vg.zip"), \
|
||||
patch("os.path.join", return_value="/tmp/vg"):
|
||||
result = lade_vg_aus_zip(None)
|
||||
|
||||
self.assertIsNone(result)
|
||||
mock_mb.warning.assert_called_once()
|
||||
|
||||
@patch(f"{_MOD}._erstelle_memory_layer_aus_shp")
|
||||
@patch(f"{_MOD}.QgsVectorLayer")
|
||||
@patch(f"{_MOD}._finde_shp", return_value="/tmp/vg/shape.shp")
|
||||
@patch(f"{_MOD}.zipfile")
|
||||
@patch(f"{_MOD}.QFileDialog")
|
||||
def test_erfolg_gibt_memory_layer_zurueck(
|
||||
self, mock_fd, mock_zf, mock_find, mock_vl, mock_mem
|
||||
):
|
||||
mock_fd.getOpenFileName.return_value = ("/tmp/vg.zip", "")
|
||||
mock_zf.ZipFile.return_value.__enter__ = MagicMock(return_value=MagicMock())
|
||||
mock_zf.ZipFile.return_value.__exit__ = MagicMock(return_value=False)
|
||||
valid_layer = MagicMock()
|
||||
valid_layer.isValid.return_value = True
|
||||
mock_vl.return_value = valid_layer
|
||||
|
||||
mem_layer = _make_layer(name="Verfahrensgebiet", lid="MEM1")
|
||||
mock_mem.return_value = mem_layer
|
||||
|
||||
with patch("os.path.dirname", return_value="/tmp"), \
|
||||
patch("os.path.splitext", return_value=("/tmp/vg", ".zip")), \
|
||||
patch("os.path.basename", return_value="vg.zip"), \
|
||||
patch("os.path.join", return_value="/tmp/vg"):
|
||||
result = lade_vg_aus_zip(None)
|
||||
|
||||
self.assertIs(result, mem_layer)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests: verfahrensgebiet_shape_komplett (Hauptworkflow)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestVerfahrensgebietShapeKomplett(unittest.TestCase):
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Abbruchpfade
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
@patch(_PATCH_PROGRESS)
|
||||
@patch(_PATCH_PROJECT)
|
||||
@patch(_PATCH_LADE_ZIP)
|
||||
def test_lade_zip_none_aborts(self, mock_lade, mock_proj, mock_prog):
|
||||
mock_proj.instance.return_value = _DummyProject()
|
||||
mock_lade.return_value = None
|
||||
mock_prog.return_value = _make_progress_mock()
|
||||
|
||||
enriched, flst, bror, status = verfahrensgebiet_shape_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_LADE_ZIP)
|
||||
def test_shape_vg_sofort_ins_projekt_nach_laden(self, mock_lade, mock_gem, mock_proj, mock_prog):
|
||||
"""Shape-VG wird unmittelbar nach dem Laden ins Projekt eingefügt."""
|
||||
proj = _DummyProject()
|
||||
mock_proj.instance.return_value = proj
|
||||
shape_vg = _make_layer(name="Verfahrensgebiet", lid="S1")
|
||||
mock_lade.return_value = shape_vg
|
||||
mock_gem.return_value = []
|
||||
mock_prog.return_value = _make_progress_mock()
|
||||
|
||||
with patch(_PATCH_FLST, return_value=(None, LoadStatus.NONE)):
|
||||
verfahrensgebiet_shape_komplett(None)
|
||||
|
||||
# Shape-VG muss ins Projekt eingefügt worden sein
|
||||
self.assertIn(shape_vg, proj.added)
|
||||
|
||||
@patch(_PATCH_PROGRESS)
|
||||
@patch(_PATCH_PROJECT)
|
||||
@patch(_PATCH_GEM)
|
||||
@patch(_PATCH_LADE_ZIP)
|
||||
def test_shape_vg_wird_bei_abbruch_aus_projekt_entfernt(
|
||||
self, mock_lade, mock_gem, mock_proj, mock_prog
|
||||
):
|
||||
"""Shape-VG wird bei Fehler/Abbruch aus dem Projekt entfernt."""
|
||||
proj = _DummyProject()
|
||||
mock_proj.instance.return_value = proj
|
||||
shape_vg = _make_layer(name="Verfahrensgebiet", lid="S1")
|
||||
mock_lade.return_value = shape_vg
|
||||
mock_gem.return_value = []
|
||||
mock_prog.return_value = _make_progress_mock()
|
||||
|
||||
with patch(_PATCH_FLST, return_value=(None, LoadStatus.NONE)):
|
||||
_, _, _, status = verfahrensgebiet_shape_komplett(None)
|
||||
|
||||
self.assertEqual(status, LoadStatus.NONE)
|
||||
# Layer-ID muss in removed sein
|
||||
self.assertIn("S1", proj.removed)
|
||||
|
||||
@patch(_PATCH_PROGRESS)
|
||||
@patch(_PATCH_PROJECT)
|
||||
@patch(_PATCH_GEM)
|
||||
@patch(_PATCH_LADE_ZIP)
|
||||
def test_shape_vg_bleibt_im_projekt_bei_erfolg(self, mock_lade, mock_gem, mock_proj, mock_prog):
|
||||
"""Bei erfolgreichem Abschluss wird shape_vg NICHT aus dem Projekt entfernt."""
|
||||
proj = _DummyProject()
|
||||
mock_proj.instance.return_value = proj
|
||||
shape_vg = _make_layer(name="Verfahrensgebiet", lid="S1")
|
||||
mock_lade.return_value = shape_vg
|
||||
mock_gem.return_value = []
|
||||
flst_layer = _make_layer(name="Flurstücke", lid="F1")
|
||||
mock_prog.return_value = _make_progress_mock()
|
||||
|
||||
with patch(_PATCH_FLST, return_value=(flst_layer, LoadStatus.FIRST)), \
|
||||
patch(_PATCH_SELECT, return_value=5), \
|
||||
patch(_PATCH_ERSTELLE, return_value=(_make_layer(lid="T1"), 5, 0)), \
|
||||
patch(_PATCH_VERGLEICHE, return_value=(1000.0, 1000.0, True)), \
|
||||
patch(_PATCH_SHOW_INFO):
|
||||
verfahrensgebiet_shape_komplett(None)
|
||||
|
||||
# Layer-ID darf NICHT in removed sein
|
||||
self.assertNotIn("S1", proj.removed)
|
||||
|
||||
@patch(_PATCH_PROGRESS)
|
||||
@patch(_PATCH_PROJECT)
|
||||
@patch(_PATCH_GEM)
|
||||
@patch(_PATCH_LADE_ZIP)
|
||||
def test_flst_none_aborts(self, mock_lade, mock_gem, mock_proj, mock_prog):
|
||||
mock_proj.instance.return_value = _DummyProject()
|
||||
mock_lade.return_value = _make_layer(name="Verfahrensgebiet")
|
||||
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_shape_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_LADE_ZIP)
|
||||
def test_keine_flurstuecke_selektiert_gibt_warning(
|
||||
self, mock_lade, mock_flst, mock_gem, mock_proj, mock_select, mock_mb, mock_prog
|
||||
):
|
||||
mock_proj.instance.return_value = _DummyProject()
|
||||
mock_lade.return_value = _make_layer(name="Verfahrensgebiet")
|
||||
mock_gem.return_value = []
|
||||
flst_layer = _make_layer(name="Flurstücke")
|
||||
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_shape_komplett(None)
|
||||
|
||||
self.assertIsNone(enriched)
|
||||
self.assertIs(flst, flst_layer)
|
||||
self.assertIsNone(bror)
|
||||
self.assertEqual(status, LoadStatus.NONE)
|
||||
mock_mb.warning.assert_called_once()
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Flächenübereinstimmung
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
@patch(_PATCH_SHOW_INFO)
|
||||
@patch(_PATCH_PROGRESS)
|
||||
@patch(_PATCH_VERGLEICHE)
|
||||
@patch(_PATCH_ERSTELLE)
|
||||
@patch(_PATCH_SELECT)
|
||||
@patch(_PATCH_PROJECT)
|
||||
@patch(_PATCH_GEM)
|
||||
@patch(_PATCH_FLST)
|
||||
@patch(_PATCH_LADE_ZIP)
|
||||
def test_flaechen_uebereinstimmung_gibt_info_und_kein_dialog(
|
||||
self, mock_lade, mock_flst, mock_gem, mock_proj,
|
||||
mock_select, mock_erstelle, mock_vergleiche, mock_prog, mock_info
|
||||
):
|
||||
proj = _DummyProject()
|
||||
mock_proj.instance.return_value = proj
|
||||
shape_vg = _make_layer(name="Verfahrensgebiet", lid="S1")
|
||||
mock_lade.return_value = shape_vg
|
||||
mock_gem.return_value = [("1234", "Muster")]
|
||||
flst_layer = _make_layer(name="Flurstücke", lid="F1")
|
||||
mock_flst.return_value = (flst_layer, LoadStatus.FIRST)
|
||||
mock_select.return_value = 5
|
||||
temp_enriched = _make_layer(name="Verfahrensgebiet", lid="T1")
|
||||
mock_erstelle.return_value = (temp_enriched, 5, 0)
|
||||
mock_vergleiche.return_value = (1000.0, 1000.0, True)
|
||||
mock_prog.return_value = _make_progress_mock()
|
||||
|
||||
enriched, flst, bror, status = verfahrensgebiet_shape_komplett(None)
|
||||
|
||||
self.assertIs(enriched, shape_vg)
|
||||
self.assertIs(flst, flst_layer)
|
||||
self.assertIsNone(bror)
|
||||
self.assertEqual(status, LoadStatus.FIRST)
|
||||
mock_info.assert_called_once()
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Flächenabweichung + Abbrechen
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
@patch(_PATCH_ASK_DIALOG)
|
||||
@patch(_PATCH_SHOW_INFO)
|
||||
@patch(_PATCH_PROGRESS)
|
||||
@patch(_PATCH_VERGLEICHE)
|
||||
@patch(_PATCH_ERSTELLE)
|
||||
@patch(_PATCH_SELECT)
|
||||
@patch(_PATCH_PROJECT)
|
||||
@patch(_PATCH_GEM)
|
||||
@patch(_PATCH_FLST)
|
||||
@patch(_PATCH_LADE_ZIP)
|
||||
def test_abweichung_abbrechen_gibt_shape_vg_zurueck(
|
||||
self, mock_lade, mock_flst, mock_gem, mock_proj,
|
||||
mock_select, mock_erstelle, mock_vergleiche, mock_prog, mock_info, mock_ask
|
||||
):
|
||||
proj = _DummyProject()
|
||||
mock_proj.instance.return_value = proj
|
||||
shape_vg = _make_layer(name="Verfahrensgebiet", lid="S1")
|
||||
mock_lade.return_value = shape_vg
|
||||
mock_gem.return_value = []
|
||||
flst_layer = _make_layer(name="Flurstücke", lid="F1")
|
||||
mock_flst.return_value = (flst_layer, LoadStatus.FIRST)
|
||||
mock_select.return_value = 5
|
||||
temp_enriched = _make_layer(name="Verfahrensgebiet", lid="T1")
|
||||
mock_erstelle.return_value = (temp_enriched, 5, 0)
|
||||
mock_vergleiche.return_value = (1000.0, 800.0, False)
|
||||
mock_prog.return_value = _make_progress_mock()
|
||||
mock_ask.return_value = "abbrechen"
|
||||
|
||||
enriched, flst, bror, status = verfahrensgebiet_shape_komplett(None)
|
||||
|
||||
self.assertIs(enriched, shape_vg)
|
||||
self.assertIs(flst, flst_layer)
|
||||
self.assertIsNone(bror)
|
||||
self.assertEqual(status, LoadStatus.FIRST)
|
||||
mock_info.assert_not_called()
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Detailprüfung
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
@patch(_PATCH_SHOW_INFO)
|
||||
@patch(_PATCH_KNICK)
|
||||
@patch(_PATCH_GP_ALKIS)
|
||||
@patch(_PATCH_EXTRAHIERE)
|
||||
@patch(_PATCH_ASK_DIALOG)
|
||||
@patch(_PATCH_PROGRESS)
|
||||
@patch(_PATCH_VERGLEICHE)
|
||||
@patch(_PATCH_ERSTELLE)
|
||||
@patch(_PATCH_SELECT)
|
||||
@patch(_PATCH_PROJECT)
|
||||
@patch(_PATCH_GEM)
|
||||
@patch(_PATCH_FLST)
|
||||
@patch(_PATCH_LADE_ZIP)
|
||||
def test_abweichung_detailpruefung_ruft_knickpunkt_pruefung_auf(
|
||||
self, mock_lade, mock_flst, mock_gem, mock_proj,
|
||||
mock_select, mock_erstelle, mock_vergleiche, mock_prog,
|
||||
mock_ask, mock_extrahiere, mock_gp, mock_knick, mock_info
|
||||
):
|
||||
proj = _DummyProject()
|
||||
mock_proj.instance.return_value = proj
|
||||
shape_vg = _make_layer(name="Verfahrensgebiet", lid="S1")
|
||||
mock_lade.return_value = shape_vg
|
||||
mock_gem.return_value = [("1234", "Muster")]
|
||||
flst_layer = _make_layer(name="Flurstücke", lid="F1")
|
||||
mock_flst.return_value = (flst_layer, LoadStatus.FIRST)
|
||||
mock_select.return_value = 5
|
||||
temp_enriched = _make_layer(name="Verfahrensgebiet", lid="T1")
|
||||
mock_erstelle.return_value = (temp_enriched, 5, 0)
|
||||
mock_vergleiche.return_value = (1000.0, 800.0, False)
|
||||
mock_prog.return_value = _make_progress_mock()
|
||||
mock_ask.return_value = "detailpruefung"
|
||||
|
||||
knickpunkte_mock = [MagicMock() for _ in range(3)]
|
||||
mock_extrahiere.return_value = knickpunkte_mock
|
||||
|
||||
pta_layer = _make_layer(name="PunktortTA", lid="P1")
|
||||
mock_gp.return_value = pta_layer
|
||||
|
||||
ergebnis = _make_layer(name="Knickpunkte", lid="K1")
|
||||
mock_knick.return_value = (ergebnis, 2, 1, 0)
|
||||
|
||||
enriched, flst, bror, status = verfahrensgebiet_shape_komplett(None)
|
||||
|
||||
mock_gp.assert_called_once()
|
||||
mock_knick.assert_called_once()
|
||||
self.assertIs(enriched, shape_vg)
|
||||
self.assertEqual(status, LoadStatus.FIRST)
|
||||
|
||||
@patch(_PATCH_SHOW_INFO)
|
||||
@patch(_PATCH_KNICK)
|
||||
@patch(_PATCH_GP_ALKIS, return_value=None)
|
||||
@patch(_PATCH_EXTRAHIERE)
|
||||
@patch(_PATCH_ASK_DIALOG)
|
||||
@patch(_PATCH_PROGRESS)
|
||||
@patch(_PATCH_VERGLEICHE)
|
||||
@patch(_PATCH_ERSTELLE)
|
||||
@patch(_PATCH_SELECT)
|
||||
@patch(_PATCH_PROJECT)
|
||||
@patch(_PATCH_GEM)
|
||||
@patch(_PATCH_FLST)
|
||||
@patch(_PATCH_LADE_ZIP)
|
||||
def test_punktort_ta_none_gibt_shape_vg_zurueck_und_zeigt_fehler(
|
||||
self, mock_lade, mock_flst, mock_gem, mock_proj,
|
||||
mock_select, mock_erstelle, mock_vergleiche, mock_prog,
|
||||
mock_ask, mock_extrahiere, mock_gp, mock_knick, mock_info
|
||||
):
|
||||
proj = _DummyProject()
|
||||
mock_proj.instance.return_value = proj
|
||||
shape_vg = _make_layer(name="Verfahrensgebiet", lid="S1")
|
||||
mock_lade.return_value = shape_vg
|
||||
mock_gem.return_value = []
|
||||
flst_layer = _make_layer(name="Flurstücke", lid="F1")
|
||||
mock_flst.return_value = (flst_layer, LoadStatus.FIRST)
|
||||
mock_select.return_value = 5
|
||||
temp_enriched = _make_layer(name="Verfahrensgebiet", lid="T1")
|
||||
mock_erstelle.return_value = (temp_enriched, 5, 0)
|
||||
mock_vergleiche.return_value = (1000.0, 800.0, False)
|
||||
mock_prog.return_value = _make_progress_mock()
|
||||
mock_ask.return_value = "detailpruefung"
|
||||
mock_extrahiere.return_value = [MagicMock()]
|
||||
|
||||
with patch(f"{_MOD}.QMessageBox") as mock_mb:
|
||||
enriched, flst, bror, status = verfahrensgebiet_shape_komplett(None)
|
||||
|
||||
mock_knick.assert_not_called()
|
||||
self.assertIs(enriched, shape_vg)
|
||||
mock_mb.critical.assert_called_once()
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Cleanup: TempVerfahrensgebiet
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
@patch(_PATCH_PROGRESS)
|
||||
@patch(_PATCH_GEM)
|
||||
@patch(_PATCH_PROJECT)
|
||||
@patch(_PATCH_LADE_ZIP)
|
||||
def test_temp_layer_wird_stets_entfernt(self, mock_lade, mock_proj, mock_gem, mock_prog):
|
||||
proj = _DummyProject()
|
||||
temp_layer = _make_layer(name="TempVerfahrensgebiet", lid="TEMP")
|
||||
proj._layers_by_name["TempVerfahrensgebiet"] = [temp_layer]
|
||||
mock_proj.instance.return_value = proj
|
||||
mock_lade.return_value = None # → sofortiger Abbruch nach Schritt 1
|
||||
mock_gem.return_value = []
|
||||
mock_prog.return_value = _make_progress_mock()
|
||||
|
||||
verfahrensgebiet_shape_komplett(None)
|
||||
|
||||
self.assertIn("TEMP", proj.removed)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Rückgabe-Konsistenz
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
@patch(_PATCH_SHOW_INFO)
|
||||
@patch(_PATCH_PROGRESS)
|
||||
@patch(_PATCH_VERGLEICHE)
|
||||
@patch(_PATCH_ERSTELLE)
|
||||
@patch(_PATCH_SELECT)
|
||||
@patch(_PATCH_PROJECT)
|
||||
@patch(_PATCH_GEM)
|
||||
@patch(_PATCH_FLST)
|
||||
@patch(_PATCH_LADE_ZIP)
|
||||
def test_bror_layer_ist_stets_none(
|
||||
self, mock_lade, mock_flst, mock_gem, mock_proj,
|
||||
mock_select, mock_erstelle, mock_vergleiche, mock_prog, mock_info
|
||||
):
|
||||
"""Dritter Rückgabewert ist im Shape-Workflow immer None (kein BROR)."""
|
||||
mock_proj.instance.return_value = _DummyProject()
|
||||
shape_vg = _make_layer(name="Verfahrensgebiet", lid="S1")
|
||||
mock_lade.return_value = shape_vg
|
||||
mock_gem.return_value = []
|
||||
flst_layer = _make_layer(name="Flurstücke", lid="F1")
|
||||
mock_flst.return_value = (flst_layer, LoadStatus.FIRST)
|
||||
mock_select.return_value = 5
|
||||
temp_enriched = _make_layer(lid="T1")
|
||||
mock_erstelle.return_value = (temp_enriched, 5, 0)
|
||||
mock_vergleiche.return_value = (1000.0, 1000.0, True)
|
||||
mock_prog.return_value = _make_progress_mock()
|
||||
|
||||
_, _, bror, _ = verfahrensgebiet_shape_komplett(None)
|
||||
self.assertIsNone(bror)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# BROR-Ladelogik
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
@patch(_PATCH_SHOW_INFO)
|
||||
@patch(_PATCH_PROGRESS)
|
||||
@patch(_PATCH_VERGLEICHE)
|
||||
@patch(_PATCH_SELECT)
|
||||
@patch(_PATCH_PROJECT)
|
||||
@patch(_PATCH_GEM)
|
||||
@patch(_PATCH_FLST)
|
||||
@patch(f"{_MOD}.verfahrensgebiet_alkis")
|
||||
@patch(f"{_MOD}.get_variable", return_value="VF-2026-001")
|
||||
@patch(_PATCH_LADE_ZIP)
|
||||
def test_bror_geladen_und_zurueckgegeben_wenn_vkz_gesetzt(
|
||||
self, mock_lade, mock_get_var, mock_vg_alkis, mock_flst,
|
||||
mock_gem, mock_proj, mock_select, mock_vergleiche, mock_prog, mock_info
|
||||
):
|
||||
"""VKZ gesetzt → verfahrensgebiet_alkis aufgerufen, bror_layer zurückgegeben."""
|
||||
proj = _DummyProject()
|
||||
mock_proj.instance.return_value = proj
|
||||
shape_vg = _make_layer(name="Verfahrensgebiet", lid="S1")
|
||||
mock_lade.return_value = shape_vg
|
||||
mock_gem.return_value = []
|
||||
flst_layer = _make_layer(name="Flurstücke", lid="F1")
|
||||
mock_flst.return_value = (flst_layer, LoadStatus.FIRST)
|
||||
mock_select.return_value = 5
|
||||
bror = _make_layer(name="Verfahrensgebiet", lid="BROR")
|
||||
mock_vg_alkis.return_value = (bror, LoadStatus.FIRST, [])
|
||||
mock_vergleiche.return_value = (1000.0, 1000.0, True)
|
||||
mock_prog.return_value = _make_progress_mock()
|
||||
|
||||
enriched, flst, bror_ret, status = verfahrensgebiet_shape_komplett(None)
|
||||
|
||||
mock_vg_alkis.assert_called_once()
|
||||
self.assertIs(bror_ret, bror)
|
||||
self.assertIs(enriched, shape_vg)
|
||||
self.assertEqual(status, LoadStatus.FIRST)
|
||||
|
||||
@patch(_PATCH_SHOW_INFO)
|
||||
@patch(_PATCH_PROGRESS)
|
||||
@patch(_PATCH_VERGLEICHE)
|
||||
@patch(_PATCH_SELECT)
|
||||
@patch(_PATCH_PROJECT)
|
||||
@patch(_PATCH_GEM)
|
||||
@patch(_PATCH_FLST)
|
||||
@patch(f"{_MOD}.verfahrensgebiet_alkis")
|
||||
@patch(f"{_MOD}.get_variable", return_value="VF-2026-001")
|
||||
@patch(_PATCH_LADE_ZIP)
|
||||
def test_bror_wird_vor_shape_vg_ins_projekt_eingefuegt(
|
||||
self, mock_lade, mock_get_var, mock_vg_alkis, mock_flst,
|
||||
mock_gem, mock_proj, mock_select, mock_vergleiche, mock_prog, mock_info
|
||||
):
|
||||
"""BROR wird VOR shape_vg ins Projekt eingefügt (kein Namenskonflikt)."""
|
||||
proj = _DummyProject()
|
||||
mock_proj.instance.return_value = proj
|
||||
shape_vg = _make_layer(name="Verfahrensgebiet", lid="S1")
|
||||
mock_lade.return_value = shape_vg
|
||||
mock_gem.return_value = []
|
||||
flst_layer = _make_layer(name="Flurstücke", lid="F1")
|
||||
mock_flst.return_value = (flst_layer, LoadStatus.FIRST)
|
||||
mock_select.return_value = 5
|
||||
bror = _make_layer(name="Verfahrensgebiet", lid="BROR")
|
||||
mock_vg_alkis.return_value = (bror, LoadStatus.FIRST, [])
|
||||
mock_vergleiche.return_value = (1000.0, 1000.0, True)
|
||||
mock_prog.return_value = _make_progress_mock()
|
||||
|
||||
verfahrensgebiet_shape_komplett(None)
|
||||
|
||||
# BROR muss in added sein
|
||||
self.assertIn(bror, proj.added)
|
||||
# BROR muss vor shape_vg hinzugefügt werden
|
||||
bror_idx = proj.added.index(bror)
|
||||
shape_idx = proj.added.index(shape_vg)
|
||||
self.assertLess(bror_idx, shape_idx, "BROR muss vor shape_vg eingefügt werden")
|
||||
|
||||
@patch(_PATCH_PROGRESS)
|
||||
@patch(_PATCH_VERGLEICHE)
|
||||
@patch(_PATCH_SELECT)
|
||||
@patch(_PATCH_PROJECT)
|
||||
@patch(_PATCH_GEM)
|
||||
@patch(_PATCH_FLST)
|
||||
@patch(f"{_MOD}.verfahrensgebiet_alkis")
|
||||
@patch(f"{_MOD}.get_variable", return_value="VF-2026-001")
|
||||
@patch(_PATCH_LADE_ZIP)
|
||||
def test_bror_wird_bei_abbruch_aus_projekt_entfernt(
|
||||
self, mock_lade, mock_get_var, mock_vg_alkis, mock_flst,
|
||||
mock_gem, mock_proj, mock_select, mock_vergleiche, mock_prog
|
||||
):
|
||||
"""BROR und shape_vg werden bei Fehler aus dem Projekt entfernt."""
|
||||
proj = _DummyProject()
|
||||
mock_proj.instance.return_value = proj
|
||||
shape_vg = _make_layer(name="Verfahrensgebiet", lid="S1")
|
||||
mock_lade.return_value = shape_vg
|
||||
mock_gem.return_value = []
|
||||
bror = _make_layer(name="Verfahrensgebiet", lid="BROR")
|
||||
mock_vg_alkis.return_value = (bror, LoadStatus.FIRST, [])
|
||||
mock_flst.return_value = (None, LoadStatus.NONE) # → Abbruch
|
||||
mock_prog.return_value = _make_progress_mock()
|
||||
|
||||
_, _, _, status = verfahrensgebiet_shape_komplett(None)
|
||||
|
||||
self.assertEqual(status, LoadStatus.NONE)
|
||||
self.assertIn("S1", proj.removed)
|
||||
self.assertIn("BROR", proj.removed)
|
||||
|
||||
@patch(_PATCH_PROGRESS)
|
||||
@patch(_PATCH_PROJECT)
|
||||
@patch(_PATCH_GEM)
|
||||
@patch(f"{_MOD}.verfahrensgebiet_alkis")
|
||||
@patch(f"{_MOD}.get_variable", return_value="")
|
||||
@patch(_PATCH_LADE_ZIP)
|
||||
def test_verfahrensgebiet_alkis_nicht_aufgerufen_wenn_vkz_leer(
|
||||
self, mock_lade, mock_get_var, mock_vg_alkis, mock_gem, mock_proj, mock_prog
|
||||
):
|
||||
"""Leere VKZ → verfahrensgebiet_alkis wird NICHT aufgerufen."""
|
||||
mock_proj.instance.return_value = _DummyProject()
|
||||
mock_lade.return_value = _make_layer(name="Verfahrensgebiet")
|
||||
mock_gem.return_value = []
|
||||
mock_prog.return_value = _make_progress_mock()
|
||||
|
||||
with patch(_PATCH_FLST, return_value=(None, LoadStatus.NONE)):
|
||||
verfahrensgebiet_shape_komplett(None)
|
||||
|
||||
mock_vg_alkis.assert_not_called()
|
||||
|
||||
@patch(_PATCH_SHOW_INFO)
|
||||
@patch(_PATCH_PROGRESS)
|
||||
@patch(_PATCH_VERGLEICHE)
|
||||
@patch(_PATCH_SELECT)
|
||||
@patch(_PATCH_PROJECT)
|
||||
@patch(_PATCH_GEM)
|
||||
@patch(_PATCH_FLST)
|
||||
@patch(f"{_MOD}.verfahrensgebiet_alkis")
|
||||
@patch(f"{_MOD}.get_variable", return_value="VF-2026-001")
|
||||
@patch(_PATCH_LADE_ZIP)
|
||||
def test_bror_als_erstes_argument_beim_flaechenvergleich(
|
||||
self, mock_lade, mock_get_var, mock_vg_alkis, mock_flst,
|
||||
mock_gem, mock_proj, mock_select, mock_vergleiche, mock_prog, mock_info
|
||||
):
|
||||
"""Mit BROR: _vergleiche_flaechen wird (bror, shape_vg, ...) aufgerufen."""
|
||||
proj = _DummyProject()
|
||||
mock_proj.instance.return_value = proj
|
||||
shape_vg = _make_layer(name="Verfahrensgebiet", lid="S1")
|
||||
mock_lade.return_value = shape_vg
|
||||
mock_gem.return_value = []
|
||||
flst_layer = _make_layer(name="Flurstücke", lid="F1")
|
||||
mock_flst.return_value = (flst_layer, LoadStatus.FIRST)
|
||||
mock_select.return_value = 5
|
||||
bror = _make_layer(name="Verfahrensgebiet", lid="BROR")
|
||||
mock_vg_alkis.return_value = (bror, LoadStatus.FIRST, [])
|
||||
mock_vergleiche.return_value = (1000.0, 1000.0, True)
|
||||
mock_prog.return_value = _make_progress_mock()
|
||||
|
||||
verfahrensgebiet_shape_komplett(None)
|
||||
|
||||
# Erster Arg = bror, zweiter Arg = shape_vg
|
||||
call_args = mock_vergleiche.call_args[0]
|
||||
self.assertIs(call_args[0], bror)
|
||||
self.assertIs(call_args[1], shape_vg)
|
||||
|
||||
@patch(_PATCH_SHOW_INFO)
|
||||
@patch(_PATCH_PROGRESS)
|
||||
@patch(_PATCH_VERGLEICHE)
|
||||
@patch(_PATCH_ERSTELLE)
|
||||
@patch(_PATCH_SELECT)
|
||||
@patch(_PATCH_PROJECT)
|
||||
@patch(_PATCH_GEM)
|
||||
@patch(_PATCH_FLST)
|
||||
@patch(f"{_MOD}.verfahrensgebiet_alkis", return_value=(None, LoadStatus.NONE, []))
|
||||
@patch(f"{_MOD}.get_variable", return_value="VF-2026-001")
|
||||
@patch(_PATCH_LADE_ZIP)
|
||||
def test_fallback_auf_temp_enriched_wenn_bror_ladeversagen(
|
||||
self, mock_lade, mock_get_var, mock_vg_alkis, mock_flst,
|
||||
mock_gem, mock_proj, mock_select, mock_erstelle, mock_vergleiche, mock_prog, mock_info
|
||||
):
|
||||
"""BROR-Ladeversagen → Fallback: _vergleiche_flaechen mit shape_vg vs temp_enriched."""
|
||||
proj = _DummyProject()
|
||||
mock_proj.instance.return_value = proj
|
||||
shape_vg = _make_layer(name="Verfahrensgebiet", lid="S1")
|
||||
mock_lade.return_value = shape_vg
|
||||
mock_gem.return_value = []
|
||||
flst_layer = _make_layer(name="Flurstücke", lid="F1")
|
||||
mock_flst.return_value = (flst_layer, LoadStatus.FIRST)
|
||||
mock_select.return_value = 5
|
||||
temp_enriched = _make_layer(lid="T1")
|
||||
mock_erstelle.return_value = (temp_enriched, 5, 0)
|
||||
mock_vergleiche.return_value = (1000.0, 1000.0, True)
|
||||
mock_prog.return_value = _make_progress_mock()
|
||||
|
||||
_, _, bror_ret, _ = verfahrensgebiet_shape_komplett(None)
|
||||
|
||||
# kein BROR zurückgegeben
|
||||
self.assertIsNone(bror_ret)
|
||||
# Fallback: erste Arg = shape_vg
|
||||
call_args = mock_vergleiche.call_args[0]
|
||||
self.assertIs(call_args[0], shape_vg)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -566,5 +566,262 @@ class TestWorkingTab(unittest.TestCase):
|
||||
self.assertTrue(tab.btn_gem_alkis.isEnabled()) # finally
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# handle_verf_shape
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestHandleVerfShape(unittest.TestCase):
|
||||
|
||||
def _make_tab(self):
|
||||
tab = WorkingTab.__new__(WorkingTab)
|
||||
tab.btn_verf_alkis = _DummyButton()
|
||||
tab.btn_verf_shape = _DummyButton()
|
||||
tab.btn_flurst_alkis = _DummyButton()
|
||||
tab.btn_gem_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_gem = 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
|
||||
tab.pruefmanager = types.SimpleNamespace(verarbeite=lambda ergebnis: ergebnis)
|
||||
return tab
|
||||
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.set_verfahrensgebiet_layer_id")
|
||||
@patch(
|
||||
"sn_verfahrensgebiet.ui.tabs.working_tab.verfahrensgebiet_shape_komplett",
|
||||
return_value=(None, None, None, LoadStatus.NONE),
|
||||
)
|
||||
def test_status_none_clears_layer_variable(self, _mock_shape, mock_set_id):
|
||||
"""Status NONE → Layer-Variable wird auf '' gesetzt."""
|
||||
tab = self._make_tab()
|
||||
|
||||
with patch("sn_verfahrensgebiet.ui.tabs.working_tab.get_variable", return_value="VF-123"), \
|
||||
patch("sn_verfahrensgebiet.ui.tabs.working_tab.ask_yes_no"):
|
||||
tab.handle_verf_shape()
|
||||
|
||||
mock_set_id.assert_called_once_with("")
|
||||
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.set_verfahrensgebiet_layer_id")
|
||||
@patch(
|
||||
"sn_verfahrensgebiet.ui.tabs.working_tab.verfahrensgebiet_shape_komplett",
|
||||
return_value=(None, None, None, LoadStatus.NONE),
|
||||
)
|
||||
def test_button_always_re_enabled_on_failure(self, _mock_shape, _mock_set_id):
|
||||
"""Button muss nach Status NONE wieder freigegeben sein (finally)."""
|
||||
tab = self._make_tab()
|
||||
|
||||
with patch("sn_verfahrensgebiet.ui.tabs.working_tab.get_variable", return_value="VF-123"), \
|
||||
patch("sn_verfahrensgebiet.ui.tabs.working_tab.ask_yes_no"):
|
||||
tab.handle_verf_shape()
|
||||
|
||||
self.assertTrue(tab.btn_verf_shape.isEnabled())
|
||||
self.assertEqual(tab.btn_verf_shape.text(), "Aus Shape laden")
|
||||
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.ask_yes_no", return_value=False)
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.get_variable", return_value="")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.verfahrensgebiet_shape_komplett")
|
||||
def test_verfahrensnummer_nicht_gesetzt_nein_bricht_ab(
|
||||
self, mock_shape, mock_get_var, mock_ask
|
||||
):
|
||||
"""Verfahrensnummer leer + Nutzer wählt 'Nein' → Vorgang abgebrochen."""
|
||||
tab = self._make_tab()
|
||||
|
||||
tab.handle_verf_shape()
|
||||
|
||||
mock_ask.assert_called_once()
|
||||
mock_shape.assert_not_called()
|
||||
# Button darf nicht busy geblieben sein
|
||||
self.assertTrue(tab.btn_verf_shape.isEnabled())
|
||||
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.set_verfahrensgebiet_layer_id")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.ask_yes_no", return_value=True)
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.get_variable", return_value="")
|
||||
@patch(
|
||||
"sn_verfahrensgebiet.ui.tabs.working_tab.verfahrensgebiet_shape_komplett",
|
||||
return_value=(None, None, None, LoadStatus.NONE),
|
||||
)
|
||||
def test_verfahrensnummer_nicht_gesetzt_ja_fuehrt_fort(
|
||||
self, mock_shape, mock_get_var, mock_ask, mock_set_id
|
||||
):
|
||||
"""Verfahrensnummer leer + Nutzer wählt 'Ja' → Pipeline wird aufgerufen."""
|
||||
tab = self._make_tab()
|
||||
|
||||
tab.handle_verf_shape()
|
||||
|
||||
mock_ask.assert_called_once()
|
||||
mock_shape.assert_called_once_with(tab)
|
||||
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.set_verfahrensgebiet_layer_id")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.ask_yes_no")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.get_variable", return_value="VF-2026-001")
|
||||
@patch(
|
||||
"sn_verfahrensgebiet.ui.tabs.working_tab.verfahrensgebiet_shape_komplett",
|
||||
return_value=(None, None, None, LoadStatus.NONE),
|
||||
)
|
||||
def test_verfahrensnummer_gesetzt_kein_rueckfrage_dialog(
|
||||
self, mock_shape, mock_get_var, mock_ask, mock_set_id
|
||||
):
|
||||
"""Verfahrensnummer gesetzt → kein ask_yes_no-Dialog."""
|
||||
tab = self._make_tab()
|
||||
|
||||
tab.handle_verf_shape()
|
||||
|
||||
mock_ask.assert_not_called()
|
||||
mock_shape.assert_called_once_with(tab)
|
||||
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.get_variable", return_value="VF-123")
|
||||
@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_shape_komplett")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.iface")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.set_verfahrensgebiet_layer_id")
|
||||
def test_success_path_applies_style_and_sets_haken(
|
||||
self,
|
||||
mock_set_layer_id,
|
||||
mock_iface,
|
||||
mock_shape_komplett,
|
||||
mock_project_instance,
|
||||
mock_apply_style,
|
||||
mock_success,
|
||||
mock_get_var,
|
||||
):
|
||||
"""Erfolg → verfahrensgebiet.qml angewendet, haken_verf gesetzt, success aufgerufen."""
|
||||
tab = self._make_tab()
|
||||
haken_calls = []
|
||||
tab.setze_haken = lambda lbl, v: haken_calls.append((lbl, v))
|
||||
|
||||
enriched = _DummyLayer(valid=True, layer_id="shape_vg")
|
||||
flst = _DummyLayer(valid=True, layer_id="flst_ok")
|
||||
mock_shape_komplett.return_value = (enriched, flst, None, LoadStatus.FIRST)
|
||||
proj = _DummyProject()
|
||||
proj.mapLayersByName = lambda _n: []
|
||||
proj.mapLayer = lambda _lid: None
|
||||
mock_project_instance.return_value = proj
|
||||
mock_iface.mapCanvas.return_value = _DummyCanvas()
|
||||
|
||||
tab.handle_verf_shape()
|
||||
|
||||
# Style auf enriched layer angewendet
|
||||
apply_calls = [str(c) for c in mock_apply_style.call_args_list]
|
||||
self.assertTrue(
|
||||
any("verfahrensgebiet.qml" in c for c in apply_calls),
|
||||
"verfahrensgebiet.qml muss angewendet werden",
|
||||
)
|
||||
# Flurstücke-Style angewendet
|
||||
self.assertTrue(
|
||||
any("GIS_Flst_Beschriftung_ALKIS_NAS.qml" in c for c in apply_calls),
|
||||
"Flurstücke-Style muss angewendet werden",
|
||||
)
|
||||
# Haken gesetzt
|
||||
self.assertTrue(any(v for _, v in haken_calls), "Mindestens ein Haken muss gesetzt werden")
|
||||
# Layer-ID gespeichert
|
||||
mock_set_layer_id.assert_called_with("shape_vg")
|
||||
# Success-Meldung
|
||||
mock_success.assert_called_once()
|
||||
# Button wieder freigegeben
|
||||
self.assertTrue(tab.btn_verf_shape.isEnabled())
|
||||
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.get_variable", return_value="VF-123")
|
||||
@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_shape_komplett")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.iface")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.set_verfahrensgebiet_layer_id")
|
||||
def test_bror_layer_erhaelt_qml_stil(
|
||||
self,
|
||||
mock_set_layer_id,
|
||||
mock_iface,
|
||||
mock_shape_komplett,
|
||||
mock_project_instance,
|
||||
mock_apply_style,
|
||||
mock_success,
|
||||
mock_get_var,
|
||||
):
|
||||
"""Wenn bror_layer zurückgegeben → BauRaumOderBodenordnungsrecht.qml angewendet."""
|
||||
tab = self._make_tab()
|
||||
tab.setze_haken = lambda *a, **kw: None
|
||||
|
||||
enriched = _DummyLayer(valid=True, layer_id="shape_vg")
|
||||
bror = _DummyLayer(valid=True, layer_id="bror_id")
|
||||
mock_shape_komplett.return_value = (enriched, None, bror, LoadStatus.FIRST)
|
||||
|
||||
proj = _DummyProject()
|
||||
proj.mapLayersByName = lambda _n: []
|
||||
proj.mapLayer = lambda lid: enriched if lid == "shape_vg" else None
|
||||
mock_project_instance.return_value = proj
|
||||
mock_iface.mapCanvas.return_value = _DummyCanvas()
|
||||
|
||||
tab.handle_verf_shape()
|
||||
|
||||
apply_calls = [str(c) for c in mock_apply_style.call_args_list]
|
||||
self.assertTrue(
|
||||
any("BauRaumOderBodenordnungsrecht.qml" in c for c in apply_calls),
|
||||
"BauRaumOderBodenordnungsrecht.qml muss auf bror_layer angewendet werden",
|
||||
)
|
||||
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.get_variable", return_value="VF-123")
|
||||
@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_shape_komplett")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.iface")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.set_verfahrensgebiet_layer_id")
|
||||
def test_enriched_layer_already_in_project_not_added_again(
|
||||
self,
|
||||
mock_set_layer_id,
|
||||
mock_iface,
|
||||
mock_shape_komplett,
|
||||
mock_project_instance,
|
||||
mock_apply_style,
|
||||
mock_get_var,
|
||||
):
|
||||
"""Layer bereits im Projekt → kein erneutes addMapLayer für VG-Layer."""
|
||||
tab = self._make_tab()
|
||||
tab.setze_haken = lambda *a, **kw: None
|
||||
|
||||
enriched = _DummyLayer(valid=True, layer_id="shape_vg")
|
||||
mock_shape_komplett.return_value = (enriched, None, None, LoadStatus.FIRST)
|
||||
|
||||
proj = _DummyProject()
|
||||
proj.mapLayersByName = lambda _n: []
|
||||
proj.mapLayer = lambda lid: enriched if lid == "shape_vg" else None
|
||||
mock_project_instance.return_value = proj
|
||||
mock_iface.mapCanvas.return_value = _DummyCanvas()
|
||||
|
||||
tab.handle_verf_shape()
|
||||
|
||||
# Enriched-Layer darf NICHT erneut via addMapLayer hinzugefügt worden sein
|
||||
self.assertNotIn(enriched, proj.added)
|
||||
|
||||
def test_busy_state_set_during_execution(self):
|
||||
"""Button wird während der Ausführung auf busy gesetzt."""
|
||||
tab = self._make_tab()
|
||||
busy_states = []
|
||||
|
||||
original_set_busy = WorkingTab._set_busy
|
||||
|
||||
def capture_busy(self_ref, button, busy, original_text="Aus Shape laden"):
|
||||
busy_states.append(busy)
|
||||
original_set_busy(self_ref, button, busy, original_text)
|
||||
|
||||
with patch.object(WorkingTab, "_set_busy", capture_busy), \
|
||||
patch(
|
||||
"sn_verfahrensgebiet.ui.tabs.working_tab.verfahrensgebiet_shape_komplett",
|
||||
return_value=(None, None, None, LoadStatus.NONE),
|
||||
), \
|
||||
patch("sn_verfahrensgebiet.ui.tabs.working_tab.set_verfahrensgebiet_layer_id"), \
|
||||
patch("sn_verfahrensgebiet.ui.tabs.working_tab.get_variable", return_value="VF-123"):
|
||||
tab.handle_verf_shape()
|
||||
|
||||
self.assertIn(True, busy_states, "Button muss mindestens einmal auf busy gesetzt werden")
|
||||
self.assertIn(False, busy_states, "Button muss am Ende wieder freigegeben werden")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
|
||||
+123
-8
@@ -8,7 +8,8 @@ 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.functions.dialog_wrapper import show_info_dialog, create_progress_dialog, ask_yes_no
|
||||
from sn_basis.functions.variable_wrapper import get_variable
|
||||
from sn_basis.modules.Pruefmanager import Pruefmanager
|
||||
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
|
||||
from qgis.utils import iface
|
||||
@@ -20,7 +21,15 @@ from sn_verfahrensgebiet.functions.flurstueck_alkis import (
|
||||
ALKIS_SF_URL,
|
||||
FLST_LAYER_NAME,
|
||||
)
|
||||
from sn_verfahrensgebiet.functions.verfahrensgebiet_alkis_komplett import verfahrensgebiet_alkis_komplett
|
||||
from sn_verfahrensgebiet.functions.verfahrensgebiet_alkis_komplett import (
|
||||
verfahrensgebiet_alkis_komplett,
|
||||
BROR_LAYER_NAME,
|
||||
)
|
||||
from sn_verfahrensgebiet.functions.verfahrensgebiet_shape_komplett import verfahrensgebiet_shape_komplett
|
||||
from sn_verfahrensgebiet.functions.beteiligung_pruefung import (
|
||||
beteiligung_pruefen,
|
||||
BeteiligungPruefungFehler,
|
||||
)
|
||||
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,
|
||||
@@ -70,6 +79,15 @@ class WorkingTab(QWidget):
|
||||
row_verf_buttons.addWidget(self.btn_verf_shape)
|
||||
layout.addLayout(row_verf_buttons)
|
||||
|
||||
row_beteiligung_buttons = QHBoxLayout()
|
||||
self.btn_beteiligung_pruefen = QPushButton("Beteiligung prüfen")
|
||||
self.btn_beteiligung_pruefen.setToolTip(
|
||||
"CSV-Export aus dem BI-Server verwenden. benötigte Spalten: "
|
||||
"FLA_Verfahren-VEF_VKZF, FLA-Flurstuecke-Flurstueck, FLA_Beteiligung"
|
||||
)
|
||||
row_beteiligung_buttons.addWidget(self.btn_beteiligung_pruefen)
|
||||
layout.addLayout(row_beteiligung_buttons)
|
||||
|
||||
# Abschnitt 1b: Verfahrensgebiet-Layer auswählen
|
||||
row_layer_label = QHBoxLayout()
|
||||
lbl_layer = QLabel("Oder: Verfahrensgebiet-Layer auswählen")
|
||||
@@ -167,8 +185,9 @@ class WorkingTab(QWidget):
|
||||
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_verf_shape.clicked.connect(self.handle_verf_shape)
|
||||
# self.btn_flurst_shape.clicked.connect(self.handle_flurst_shape)
|
||||
self.btn_beteiligung_pruefen.clicked.connect(self.handle_beteiligung_pruefen)
|
||||
|
||||
# Beim Init: aktuelle Verfahrensgebiet-Layer-ID laden und im Dropdown anzeigen
|
||||
self._restore_verf_layer_selection()
|
||||
@@ -199,11 +218,72 @@ class WorkingTab(QWidget):
|
||||
apply_style(flst_layer, "GIS_Flst_Beschriftung_ALKIS_NAS.qml")
|
||||
self.setze_haken(self.haken_flurst, True)
|
||||
|
||||
# BROR-Layer: QML-Stil anwenden
|
||||
# BROR-Layer bekommt seinen Style bereits direkt beim Laden in
|
||||
# verfahrensgebiet_alkis_komplett() (auch im "Nur laden"-Fall).
|
||||
|
||||
# Bei "Nur Objekte laden" (kein weiterer Ablauf) ist enriched_layer
|
||||
# tatsächlich nur der rohe BROR-Layer (erkennbar am Namen) – hier
|
||||
# fand keine Flurstücksverschneidung statt, es gibt also keinen
|
||||
# echten VG-Layer: VG-Style/-Kombobox/-Haken/-Meldung überspringen.
|
||||
if enriched_layer.name() != BROR_LAYER_NAME:
|
||||
# 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 handle_verf_shape(self) -> None:
|
||||
"""Lädt ein Verfahrensgebiet aus einem ZIP-Archiv mit Shapefile-Set.
|
||||
|
||||
Delegiert an :func:`~sn_verfahrensgebiet.functions.verfahrensgebiet_shape_komplett.verfahrensgebiet_shape_komplett`.
|
||||
Nach Erfolg werden Shape-VG-Layer und Flurstücks-Layer ins Projekt eingefügt,
|
||||
Styles angewendet und die UI-Elemente aktualisiert.
|
||||
"""
|
||||
# Vorprüfung: Verfahrensnummer muss gesetzt sein
|
||||
# vkz = get_variable("verfahrensnummer") or ""
|
||||
# if not str(vkz).strip():
|
||||
# if not ask_yes_no(
|
||||
# "Verfahrensnummer nicht gesetzt",
|
||||
# "Verfahrensnummer nicht gesetzt. Trotzdem fortfahren?",
|
||||
# default=False,
|
||||
# parent=self,
|
||||
# ):
|
||||
# return
|
||||
|
||||
self._set_busy(self.btn_verf_shape, True, "Aus Shape laden")
|
||||
try:
|
||||
enriched_layer, flst_layer, bror_layer, status = verfahrensgebiet_shape_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: QML-Stil anwenden (sofern geladen)
|
||||
if bror_layer and bror_layer.isValid():
|
||||
apply_style(bror_layer, "BauRaumOderBodenordnungsrecht.qml")
|
||||
|
||||
# Enriched VG-Layer ins Projekt (ggf. bereits registriert, z.B. bei "als VG laden")
|
||||
# Shape-VG-Layer ins Projekt
|
||||
if not project.mapLayer(enriched_layer.id()):
|
||||
project.addMapLayer(enriched_layer)
|
||||
apply_style(enriched_layer, "verfahrensgebiet.qml")
|
||||
@@ -214,11 +294,46 @@ class WorkingTab(QWidget):
|
||||
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)
|
||||
success("Verfahrensgebiet", "Layer aus Shape geladen.", duration=5)
|
||||
|
||||
finally:
|
||||
self._set_busy(self.btn_verf_alkis, False, "Aus ALKIS laden")
|
||||
self._set_busy(self.btn_verf_shape, False, "Aus Shape laden")
|
||||
|
||||
def handle_beteiligung_pruefen(self) -> None:
|
||||
"""Gleicht eine BI-Server-CSV-Beteiligungsliste mit dem Verfahrensgebiet ab.
|
||||
|
||||
Delegiert an :func:`~sn_verfahrensgebiet.functions.beteiligung_pruefung.beteiligung_pruefen`.
|
||||
Ist noch kein (verwertbares) Verfahrensgebiet geladen, fordert die
|
||||
Funktion den Nutzer per Meldung auf, es zunächst über "Aus ALKIS
|
||||
laden" zu laden – es gibt kein automatisches Laden im Hintergrund.
|
||||
Sonst werden ein oder mehrere Ergebnis-Layer ins Projekt eingefügt,
|
||||
gestylt und eine Statistik als Messagebox angezeigt.
|
||||
"""
|
||||
self._set_busy(self.btn_beteiligung_pruefen, True, "Beteiligung prüfen")
|
||||
try:
|
||||
try:
|
||||
ergebnis = beteiligung_pruefen(self)
|
||||
except BeteiligungPruefungFehler as exc:
|
||||
QMessageBox.critical(self, "Beteiligung prüfen", str(exc))
|
||||
return
|
||||
|
||||
if ergebnis is None:
|
||||
return # Nutzerabbruch des Dateidialogs
|
||||
|
||||
project = QgsProject.instance()
|
||||
for layer, style_name in ergebnis.layers:
|
||||
if layer is None or not layer.isValid():
|
||||
continue
|
||||
if not project.mapLayer(layer.id()):
|
||||
project.addMapLayer(layer)
|
||||
if style_name:
|
||||
apply_style(layer, style_name)
|
||||
|
||||
iface.mapCanvas().refresh()
|
||||
QMessageBox.information(self, "Beteiligung prüfen", ergebnis.statistik_text)
|
||||
|
||||
finally:
|
||||
self._set_busy(self.btn_beteiligung_pruefen, False, "Beteiligung prüfen")
|
||||
|
||||
def _set_busy(self, button: QPushButton, busy: bool, original_text: str = "Aus ALKIS laden") -> None:
|
||||
"""Setzt den Busy-State eines Buttons.
|
||||
|
||||
Reference in New Issue
Block a user