Compare commits
96
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5004a8d5bf | ||
|
|
3bde666d96 | ||
|
|
e0660aba86 | ||
|
|
84e03d208c | ||
|
|
341b9ab1c4 | ||
|
|
f7661a5d25 | ||
|
|
1aea40d028 | ||
|
|
45a8ce37de | ||
|
|
951b9075fe | ||
|
|
c260ccc2d6 | ||
|
|
49d30eea22 | ||
|
|
0ac0757e42 | ||
|
|
e267d51607 | ||
|
|
003ad1957b | ||
|
|
821bb0640e | ||
|
|
04f2a49c13 | ||
|
|
eac3b44470 | ||
|
|
0a906d3ac7 | ||
|
|
39f8209b9d | ||
|
|
3c0e6e5a19 | ||
|
|
c93776d19e | ||
|
|
70c14783c5 | ||
|
|
b4f2222b50 | ||
|
|
8447a2615c | ||
|
|
4e7f796840 | ||
|
|
63a70737f9 | ||
|
|
d5db43bc57 | ||
|
|
7b48d29d56 | ||
|
|
5816b2af22 | ||
|
|
148d4dfa15 | ||
|
|
6a65c9d706 | ||
|
|
1a2a22f931 | ||
|
|
4a557e66a7 | ||
|
|
6bc5256d5f | ||
|
|
2cb6193b98 | ||
|
|
e6332e42ad | ||
|
|
2cf0cda199 | ||
|
|
fd54186d62 | ||
|
|
6e0bd213de | ||
|
|
f37f33584d | ||
|
|
0c5e6df736 | ||
|
|
0af4a79f8d | ||
|
|
ac12038a8a | ||
|
|
978c259496 | ||
|
|
b9c03627ae | ||
|
|
bdc7d265e8 | ||
|
|
182ea0532e | ||
|
|
55a0d629d4 | ||
|
|
048bef1f10 | ||
|
|
dbdfd987c6 | ||
|
|
8dbad87181 | ||
|
|
944fc82b1f | ||
|
|
f36418b814 | ||
|
|
d0d0523417 | ||
|
|
d9e97b8b77 | ||
|
|
10823b5e79 | ||
|
|
18efb38a7f | ||
|
|
6719ea7592 | ||
|
|
1115921766 | ||
|
|
78c496f721 | ||
|
|
1db782471b | ||
|
|
8bf089cd6a | ||
|
|
632c376497 | ||
|
|
7c833c7aa0 | ||
|
|
2a4cdd93dc | ||
|
|
9eb2e81fd7 | ||
|
|
6963e1759a | ||
|
|
3b47e41bbe | ||
|
|
1a9595598e | ||
|
|
4bbdb96bcd | ||
|
|
aa6166e652 | ||
|
|
12b52d0468 | ||
|
|
cbd7b84889 | ||
|
|
28cfc75121 | ||
|
|
cee02ae935 | ||
|
|
6f24f93f01 | ||
|
|
a829716810 | ||
|
|
5e3261a8de | ||
|
|
e344a5ad4a | ||
|
|
8730e1bf96 | ||
|
|
00ee470e08 | ||
|
|
821ea4ec2b | ||
|
|
d1a9748461 | ||
|
|
b25b4c83ac | ||
|
|
7948131d5a | ||
|
|
16fa99a84e | ||
|
|
f8bdcde115 | ||
|
|
168b6ec46c | ||
|
|
bcbfa4c49e | ||
|
|
610e70ec5e | ||
|
|
1d7489f4d6 | ||
|
|
f1b463a1c5 | ||
|
|
189855e63b | ||
|
|
72c9d6fa15 | ||
|
|
9caf0e3c86 | ||
|
|
f6591b2099 |
@@ -0,0 +1,13 @@
|
||||
[general]
|
||||
version=
|
||||
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
|
||||
+14
@@ -1,3 +1,17 @@
|
||||
"""sn_verfahrensgebiet – QGIS-Plugin für Verfahrensgebiets-Management.
|
||||
Verwaltet die Abgrenzung und Verwaltung von Verfahrensgebieten.
|
||||
"""
|
||||
|
||||
|
||||
def classFactory(iface):
|
||||
"""Factory-Funktion für QGIS.
|
||||
Wird von QGIS aufgerufen, um die Plugin-Instanz zu erstellen.
|
||||
|
||||
Args:
|
||||
iface: QGIS-Schnittstellenobjekt
|
||||
|
||||
Returns:
|
||||
Verfahrensgebiet: Instanziiertes Plugin
|
||||
"""
|
||||
from .main import Verfahrensgebiet
|
||||
return Verfahrensgebiet(iface)
|
||||
|
||||
@@ -1,3 +1,55 @@
|
||||
- Bugfix: Nach dem Laden des Verfahrensgebiets (ALKIS oder Shape) sprang die Karte an die falsche Stelle, wenn ein anderes Projekt-KBS als EPSG:25833 eingestellt war. Der Zoom rechnet die Ausdehnung jetzt in das KBS der Kartenansicht um (`zoom_auf_layer` aus sn_basis, setzt sn_basis mit Basis #99 voraus) (Basis #99)
|
||||
- Neu: Grenzpunkte aus ALKIS werden nach dem Laden im ALKIS-Stil dargestellt (styles/alkis_grenzpunkte.qml), statt ungestylt zu bleiben (#53)
|
||||
- Intern: Tests von TestFlurstueckAlkis an die seit längerem geänderte Ladelogik angeglichen (Mocks zeigten noch auf die alte Ein-Query-/Zwei-Knopf-Variante), bbox-Dokumentation in flurstueck_alkis.py korrigiert (bbox wird nicht mehr ausgewertet) – keine Verhaltensänderung (Test-Audit #16)
|
||||
- Intern: Der Button "aus gewählten Gemarkungen laden" nutzt jetzt dieselbe Ladelogik wie "Flurstücke laden" (flurstueck_alkis) statt einer eigenen, parallelen Implementierung – konsolidiert drei bisher getrennte Code-Pfade zum Flurstücke-Laden auf einen (#47)
|
||||
- Bugfix: Flurstücke aus ALKIS an bestehenden Layer anhängen überspringt jetzt bereits vorhandene Flurstücke (Abgleich über flurstueckskennzeichen), statt sie doppelt zu laden – betrifft sowohl den Dialog in "Flurstücke laden" als auch den Button "aus gewählten Gemarkungen laden" (#28)
|
||||
- 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)
|
||||
---
|
||||
- Gemarkungen aus ALKIS (vereinfacht) laden ergänzt
|
||||
- Flurstücke aus Gemarkungsauswahl laden ergänzt
|
||||
---
|
||||
Version 26.5.3-testing:
|
||||
- Nutzerauswahl bei mehreren VKZ-Treffern im BauRaumOderBodenordnungsrecht eingefügt
|
||||
- Farbgebung und Benennung BauRaumOderBodenordnungsrecht-Layer geändert
|
||||
- Fix: Abweichung von 0 m² löst Konflikt-Meldung aus
|
||||
---
|
||||
Version 26.5.2-testing:
|
||||
---
|
||||
Version 26.5.1-testing:
|
||||
---
|
||||
Version 26.5.3-unstable:
|
||||
---
|
||||
Version 26.5.2-unstable:
|
||||
---
|
||||
Version 26.5.1-unstable:
|
||||
---
|
||||
Version 26.4.6-unstable:
|
||||
---
|
||||
Version 26.4.5-unstable:
|
||||
---
|
||||
Version 26.4.4-unstable:
|
||||
---
|
||||
Version 26.4.3-unstable:
|
||||
---
|
||||
Version 26.4.2-testing:
|
||||
---
|
||||
Version 26.4.1-unstable:
|
||||
---
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,305 @@
|
||||
"""sn_verfahrensgebiet/functions/flurstueck_alkis.py – ALKIS-Integration für Flurstücke.
|
||||
|
||||
Lädt Flurstücke aus dem ALKIS-WFS-Dienst (``adv:AX_Flurstueck``) anhand von
|
||||
Gemarkungsschlüsseln, die der Nutzer interaktiv eingibt.
|
||||
|
||||
Für jede Gemarkungsnummer wird eine eigene WFS-Anfrage gestellt (umgeht das
|
||||
WFS-Feature-Limit einer Gesamtabfrage); alle Ergebnisse werden anschließend in
|
||||
einem gemeinsamen Memory-Layer zusammengeführt.
|
||||
|
||||
**Erweiterung gegenüber dem Original:**
|
||||
|
||||
- ``vorgabe_schluessel``: Optional vorausgefüllte Gemarkungsnummern (z. B. aus
|
||||
:func:`~sn_verfahrensgebiet.functions.gemarkung_alkis.gemarkung_schluessel_und_namen_by_bbox`).
|
||||
Der Dialog wird mit diesem Vorschlag vorausgefüllt; der Nutzer kann die
|
||||
Eingabe ändern oder ergänzen.
|
||||
|
||||
- ``vorgabe_namen``: Zugehörige Bezeichnungen, die im Dialog als Hinweis
|
||||
angezeigt werden (kein eigenständiges Feld, nur Anzeigetext).
|
||||
|
||||
- ``bbox``: Nicht mehr verwendet. Die Flurstück-Geometrie kommt ausschließlich
|
||||
aus dem ALKIS-WFS je Gemarkung; eine BBox filtert höchstens vorgelagert die
|
||||
Gemarkungsauswahl (siehe ``gemarkung_alkis.py``), nie die Flurstücke selbst.
|
||||
Der Parameter bleibt nur aus Gründen der Signatur-Rückwärtskompatibilität
|
||||
erhalten.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
from sn_basis.functions import QgsVectorLayer, QgsProject, QgsFeature
|
||||
from sn_basis.functions import QMessageBox
|
||||
from sn_basis.functions.dialog_wrapper import ask_text
|
||||
from sn_basis.functions.message_wrapper import info as _info
|
||||
|
||||
# LoadStatus aus verfahrensgebiet_alkis wiederverwenden (identische Semantik)
|
||||
from sn_verfahrensgebiet.functions.verfahrensgebiet_alkis import LoadStatus
|
||||
|
||||
ALKIS_SF_URL = "https://geodienste.sachsen.de/aaa/public_alkis/sf/wfs"
|
||||
FLST_TYPENAME = "adv:AX_Flurstueck"
|
||||
FLST_LAYER_NAME = "Flurstücke"
|
||||
FELD_FLURSTUECKSKENNZEICHEN = "flurstueckskennzeichen"
|
||||
|
||||
|
||||
def _check_existing_flurstueck_layer(tab_widget):
|
||||
"""Prüft, ob ein Flurstück-Layer bereits im Projekt existiert.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Tuple[Optional[QgsVectorLayer], LoadStatus]
|
||||
Bei ``LoadStatus.KEEP`` enthält das Tuple den vorhandenen Layer,
|
||||
bei ``LoadStatus.FIRST`` und ``LoadStatus.RELOAD`` ist es None.
|
||||
"""
|
||||
project = QgsProject.instance()
|
||||
existing_layers = project.mapLayersByName(FLST_LAYER_NAME)
|
||||
if not existing_layers:
|
||||
return None, LoadStatus.FIRST
|
||||
|
||||
msg = QMessageBox(tab_widget)
|
||||
msg.setWindowTitle("Layer bereits vorhanden")
|
||||
msg.setText(
|
||||
f"Der Layer '{FLST_LAYER_NAME}' existiert bereits.\nWas soll geschehen?"
|
||||
)
|
||||
btn_ueberschreiben = msg.addButton("Überschreiben", QMessageBox.ButtonRole.DestructiveRole)
|
||||
btn_anhaengen = msg.addButton("Anhängen", QMessageBox.ButtonRole.AcceptRole)
|
||||
btn_abbrechen = msg.addButton("Abbrechen", QMessageBox.ButtonRole.RejectRole)
|
||||
msg.setDefaultButton(btn_abbrechen)
|
||||
msg.exec()
|
||||
clicked = msg.clickedButton()
|
||||
if clicked is btn_ueberschreiben:
|
||||
for lyr in existing_layers:
|
||||
project.removeMapLayer(lyr.id())
|
||||
return None, LoadStatus.RELOAD
|
||||
elif clicked is btn_anhaengen:
|
||||
return existing_layers[0], LoadStatus.APPEND
|
||||
else:
|
||||
return None, LoadStatus.NONE
|
||||
|
||||
|
||||
def _vorhandene_kennzeichen(layer) -> set:
|
||||
"""Liefert die ``flurstueckskennzeichen``-Werte, die ``layer`` bereits enthält.
|
||||
|
||||
Fehlt das Feld, wird eine leere Menge zurückgegeben (kein Abgleich möglich).
|
||||
"""
|
||||
kennzeichen: set = set()
|
||||
if layer.fields().indexFromName(FELD_FLURSTUECKSKENNZEICHEN) < 0:
|
||||
return kennzeichen
|
||||
for feat in layer.getFeatures():
|
||||
try:
|
||||
kz = str(feat[FELD_FLURSTUECKSKENNZEICHEN] or "").strip()
|
||||
except Exception:
|
||||
kz = ""
|
||||
if kz:
|
||||
kennzeichen.add(kz)
|
||||
return kennzeichen
|
||||
|
||||
|
||||
def lade_flurstuecke_per_gemarkung(schluessel_liste: List[str]):
|
||||
"""Lädt Flurstücke für mehrere Gemarkungen, eine WFS-Anfrage pro Schlüssel.
|
||||
|
||||
Vermeidet das WFS-Feature-Limit einer einzelnen Abfrage mit mehreren
|
||||
Gemarkungsnummern. Alle Features werden in einem Memory-Layer zusammengeführt.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
schluessel_liste : list of str
|
||||
4-stellige Gemarkungsnummern.
|
||||
|
||||
Returns
|
||||
-------
|
||||
QgsVectorLayer or None
|
||||
Memory-Layer mit allen Flurstücken, oder ``None`` wenn nichts geladen wurde.
|
||||
"""
|
||||
alle_feats: list = []
|
||||
schema_fields = None
|
||||
schema_crs = "EPSG:25833"
|
||||
|
||||
for schluessel in schluessel_liste:
|
||||
uri = (
|
||||
f"url='{ALKIS_SF_URL}' typename='{FLST_TYPENAME}' srsname='EPSG:25833' "
|
||||
f"sql=SELECT * FROM AX_Flurstueck "
|
||||
f"WHERE gemarkung_ax_gemarkung_schluessel_gemarkungsnummer = '{schluessel}'"
|
||||
)
|
||||
wfs = QgsVectorLayer(uri, f"_TmpFlst{schluessel}", "WFS")
|
||||
if not wfs.isValid():
|
||||
continue
|
||||
feats = list(wfs.getFeatures())
|
||||
if not feats:
|
||||
continue
|
||||
if schema_fields is None:
|
||||
schema_fields = wfs.fields()
|
||||
schema_crs = wfs.crs().authid()
|
||||
_info(
|
||||
"Flurstücke",
|
||||
f"Gemarkung {schluessel}: {len(feats)} Flurstücke geladen.",
|
||||
duration=3,
|
||||
)
|
||||
alle_feats.append((wfs.fields(), feats))
|
||||
|
||||
if not alle_feats or schema_fields is None:
|
||||
return None
|
||||
|
||||
# Memory-Layer mit dem Feldschema des ersten WFS-Layers aufbauen
|
||||
mem = QgsVectorLayer(f"Polygon?crs={schema_crs}", FLST_LAYER_NAME, "memory")
|
||||
pr = mem.dataProvider()
|
||||
pr.addAttributes(list(schema_fields))
|
||||
mem.updateFields()
|
||||
mem_fields = mem.fields()
|
||||
|
||||
neue_features = []
|
||||
for wfs_fields, feats in alle_feats:
|
||||
for feat in feats:
|
||||
new_f = QgsFeature(mem_fields)
|
||||
new_f.setGeometry(feat.geometry())
|
||||
for i in range(wfs_fields.count()):
|
||||
fname = wfs_fields.field(i).name()
|
||||
idx = mem_fields.indexFromName(fname)
|
||||
if idx >= 0:
|
||||
try:
|
||||
new_f.setAttribute(idx, feat[fname])
|
||||
except Exception:
|
||||
pass
|
||||
neue_features.append(new_f)
|
||||
|
||||
pr.addFeatures(neue_features)
|
||||
mem.updateExtents()
|
||||
return mem
|
||||
|
||||
|
||||
def flurstueck_alkis(
|
||||
tab_widget,
|
||||
vorgabe_schluessel: Optional[List[str]] = None,
|
||||
vorgabe_namen: Optional[List[str]] = None,
|
||||
bbox=None,
|
||||
ohne_dialog: bool = False,
|
||||
ohne_schluessel_abfrage: bool = False,
|
||||
) -> Tuple[Optional[QgsVectorLayer], LoadStatus]:
|
||||
"""Lädt Flurstücke aus dem ALKIS-WFS anhand von Gemarkungsschlüsseln.
|
||||
|
||||
Jede Gemarkungsnummer wird als separate WFS-Anfrage gestellt, um das
|
||||
WFS-Feature-Limit bei einer Gesamtabfrage zu umgehen. Alle Ergebnisse
|
||||
werden in einem Memory-Layer zusammengeführt.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
tab_widget :
|
||||
Eltern-Widget für Dialoge.
|
||||
vorgabe_schluessel : list of str, optional
|
||||
Vorausgefüllte Gemarkungsnummern (4-stellig). Im Dialog als Standard
|
||||
angezeigt; bei ``ohne_dialog=True`` oder ``ohne_schluessel_abfrage=True``
|
||||
direkt verwendet.
|
||||
vorgabe_namen : list of str, optional
|
||||
Menschenlesbare Gemarkungsbezeichnungen für den Dialog-Hinweistext.
|
||||
bbox :
|
||||
Nicht mehr verwendet (WFS unterstützt keinen BBox-Filter). Bleibt
|
||||
für Rückwärtskompatibilität in der Signatur erhalten.
|
||||
ohne_dialog : bool, optional
|
||||
``True``: Überspringt sowohl die Gemarkungsschlüssel-Nutzerabfrage (wie
|
||||
``ohne_schluessel_abfrage``) als auch die Rückfrage bei einem bereits
|
||||
vorhandenen Layer – dieser wird dann immer ersetzt. Gedacht für die
|
||||
automatischen Pipelines (``verfahrensgebiet_alkis_komplett``,
|
||||
``verfahrensgebiet_shape_komplett``). Standard: ``False``.
|
||||
ohne_schluessel_abfrage : bool, optional
|
||||
``True``: Überspringt nur die Gemarkungsschlüssel-Nutzerabfrage und
|
||||
verwendet ``vorgabe_schluessel`` direkt; die Rückfrage bei einem
|
||||
bereits vorhandenen Layer (Überschreiben/Anhängen/Abbrechen, inkl.
|
||||
Dedup beim Anhängen) bleibt aktiv. Für Aufrufer, die die Schlüssel
|
||||
bereits anderweitig ermittelt haben (z. B. aus einer Layer-Auswahl),
|
||||
aber trotzdem interaktiv mit einem bestehenden Layer umgehen wollen.
|
||||
Wirkungslos, wenn ``ohne_dialog=True`` gesetzt ist. Standard: ``False``.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Tuple[Optional[QgsVectorLayer], LoadStatus]
|
||||
Memory-Layer mit allen Flurstücken und Ladestatus.
|
||||
Bei Fehlern ist der Layer ``None`` und der Status :attr:`LoadStatus.NONE`.
|
||||
"""
|
||||
# --- Schritt 1: Vorhandenen Layer behandeln ----------------------------
|
||||
append_target = None
|
||||
if ohne_dialog:
|
||||
project = QgsProject.instance()
|
||||
hatte_layer = bool(project.mapLayersByName(FLST_LAYER_NAME))
|
||||
for lyr in list(project.mapLayersByName(FLST_LAYER_NAME)):
|
||||
project.removeMapLayer(lyr.id())
|
||||
status = LoadStatus.RELOAD if hatte_layer else LoadStatus.FIRST
|
||||
else:
|
||||
existing, status = _check_existing_flurstueck_layer(tab_widget)
|
||||
if status == LoadStatus.KEEP:
|
||||
return existing, status
|
||||
if status == LoadStatus.NONE:
|
||||
return None, LoadStatus.NONE
|
||||
if status == LoadStatus.APPEND:
|
||||
append_target = existing
|
||||
|
||||
# --- Schritt 2: Gemarkungsschlüssel bestimmen -------------------------
|
||||
if ohne_dialog or ohne_schluessel_abfrage:
|
||||
# vorgabe_schluessel direkt verwenden, kein ask_text-Dialog
|
||||
if not vorgabe_schluessel:
|
||||
return None, LoadStatus.NONE
|
||||
schluessel_liste = [s.strip() for s in vorgabe_schluessel if s.strip()]
|
||||
if not schluessel_liste:
|
||||
return None, LoadStatus.NONE
|
||||
else:
|
||||
# Interaktiver Modus: Dialog mit Vorschlag
|
||||
default_text = ", ".join(vorgabe_schluessel) if vorgabe_schluessel else ""
|
||||
hinweis = ""
|
||||
if vorgabe_namen:
|
||||
hinweis = " (Vorschlag: " + ", ".join(vorgabe_namen) + ")"
|
||||
label = f"Gemarkungsschlüssel eingeben (mehrere mit Komma trennen):{hinweis}"
|
||||
|
||||
eingabe, ok = ask_text(
|
||||
"Gemarkungsschlüssel",
|
||||
label,
|
||||
default_text=default_text,
|
||||
parent=tab_widget,
|
||||
)
|
||||
if not ok or not eingabe.strip():
|
||||
QMessageBox.warning(
|
||||
tab_widget,
|
||||
"Abgebrochen",
|
||||
"Bitte mindestens einen gültigen Gemarkungsschlüssel eingeben.",
|
||||
)
|
||||
return None, LoadStatus.NONE
|
||||
|
||||
schluessel_liste = [s.strip() for s in eingabe.split(",") if s.strip()]
|
||||
if not schluessel_liste:
|
||||
QMessageBox.warning(tab_widget, "Fehler", "Keine gültigen Schlüssel gefunden.")
|
||||
return None, LoadStatus.NONE
|
||||
|
||||
# --- Schritt 3: Flurstücke gemarkungsweise laden ----------------------
|
||||
layer = lade_flurstuecke_per_gemarkung(schluessel_liste)
|
||||
|
||||
if not layer or not layer.isValid() or layer.featureCount() == 0:
|
||||
if not ohne_dialog:
|
||||
QMessageBox.critical(
|
||||
tab_widget,
|
||||
"Fehler",
|
||||
"Flurstück-Layer konnte nicht geladen werden.\n"
|
||||
"Bitte Gemarkungsschlüssel und Netzwerkverbindung prüfen.",
|
||||
)
|
||||
return None, LoadStatus.NONE
|
||||
|
||||
# --- Anhängen an vorhandenen Layer ------------------------------------
|
||||
if status == LoadStatus.APPEND and append_target is not None:
|
||||
alle_feats = list(layer.getFeatures())
|
||||
neue_feats = alle_feats
|
||||
if layer.fields().indexFromName(FELD_FLURSTUECKSKENNZEICHEN) >= 0:
|
||||
vorhandene = _vorhandene_kennzeichen(append_target)
|
||||
neue_feats = [
|
||||
f for f in alle_feats
|
||||
if str(f[FELD_FLURSTUECKSKENNZEICHEN] or "").strip() not in vorhandene
|
||||
]
|
||||
uebersprungen = len(alle_feats) - len(neue_feats)
|
||||
if uebersprungen:
|
||||
_info(
|
||||
"Flurstücke",
|
||||
f"{uebersprungen} bereits vorhandene Flurstücke übersprungen.",
|
||||
duration=3,
|
||||
)
|
||||
append_target.dataProvider().addFeatures(neue_feats)
|
||||
append_target.updateExtents()
|
||||
append_target.triggerRepaint()
|
||||
return append_target, LoadStatus.APPEND
|
||||
|
||||
return layer, status
|
||||
@@ -0,0 +1,228 @@
|
||||
"""sn_verfahrensgebiet/functions/gemarkung_alkis.py – ALKIS-Gemarkungsabfrage.
|
||||
|
||||
Stellt Funktionen bereit, um Katasterbezirk-Objekte aus dem sächsischen
|
||||
ALKIS-vereinfachten WFS zu laden und daraus Gemarkungsschlüssel zu extrahieren,
|
||||
die als Vorschlag für die Flurstücksabfrage (Phase 2) dienen.
|
||||
|
||||
**Dienst:** ``https://geodienste.sachsen.de/aaa/public_alkis/vereinf/wfs``
|
||||
**Typename:** ``Katasterbezirk``
|
||||
|
||||
Dieser Dienst hat – im Gegensatz zu ``adv:AX_Gemarkung`` aus dem NAS-Dienst –
|
||||
tatsächliche Geometrien. Die client-seitige Verschneidung mit der VG-Geometrie
|
||||
ist damit möglich.
|
||||
|
||||
**Attributkonvention:**
|
||||
|
||||
- ``schluessel``: vollständiger Katasterbezirksschlüssel (z.B. ``140640001``),
|
||||
die letzten 4 Stellen entsprechen der
|
||||
``gemarkung_ax_gemarkung_schluessel_gemarkungsnummer`` im NAS-Flurstücksdienst.
|
||||
- ``ave_name``: Gemarkungsbezeichnung.
|
||||
|
||||
**Wichtig:** Diese Funktion stellt keine Nutzerabfragen. Sie gibt im
|
||||
Fehlerfall eine leere Liste zurück.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
from sn_basis.functions import QgsVectorLayer, QgsGeometry
|
||||
from sn_basis.functions.message_wrapper import warning as _warn, info as _info
|
||||
|
||||
ALKIS_VEREINF_URL = "https://geodienste.sachsen.de/aaa/public_alkis/vereinf/wfs"
|
||||
KATASTERBEZIRK_TYPENAME = "ave:KatasterBezirk"
|
||||
|
||||
# Attributnamen im vereinfachten Dienst
|
||||
_ATTR_SCHLUESSEL = "schluessel" # vollständiger Schlüssel, letzte 4 = Gemarkungsnummer
|
||||
_ATTR_NAME = "ave_name"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Interne Hilfsfunktionen
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _extract_schluessel_und_name(feature) -> Tuple[str, str]:
|
||||
"""Extrahiert Gemarkungsnummer (letzte 4 Stellen von ``schluessel``)
|
||||
und Namen (``ave_name``) aus einem Katasterbezirk-Feature.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Tuple[str, str]
|
||||
``(gemarkungsnummer_4stellig, name)`` – leer wenn nicht ermittelbar.
|
||||
"""
|
||||
def _get(field: str) -> str:
|
||||
try:
|
||||
val = feature[field]
|
||||
return str(val).strip() if val else ""
|
||||
except (KeyError, Exception):
|
||||
return ""
|
||||
|
||||
schluessel = _get(_ATTR_SCHLUESSEL)
|
||||
name = _get(_ATTR_NAME)
|
||||
|
||||
# Letzte 4 Stellen des Schlüssels = Gemarkungsnummer
|
||||
nummer = schluessel[-4:] if len(schluessel) >= 4 else schluessel
|
||||
|
||||
return nummer, name
|
||||
|
||||
|
||||
def _load_all_gemarkung_layer():
|
||||
"""Lädt alle Katasterbezirk-Objekte aus dem vereinfachten ALKIS-WFS.
|
||||
|
||||
Sachsen hat ≈ 2 000 Gemarkungen. Der vereinfachte Dienst liefert
|
||||
tatsächliche Geometrien, sodass client-seitige Verschneidung möglich ist.
|
||||
|
||||
Returns
|
||||
-------
|
||||
QgsVectorLayer or None
|
||||
Geladener Layer oder ``None`` bei Ungültigkeit.
|
||||
"""
|
||||
uri = (
|
||||
f"url='{ALKIS_VEREINF_URL}' typename='{KATASTERBEZIRK_TYPENAME}' "
|
||||
f"srsname='EPSG:25833'"
|
||||
)
|
||||
layer = QgsVectorLayer(uri, "_TempKatasterbezirk", "WFS")
|
||||
return layer if (layer and layer.isValid()) else None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Öffentliche API
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def gemarkung_schluessel_und_namen_von_vg_geometrie(
|
||||
vg_geometrie,
|
||||
) -> List[Tuple[str, str]]:
|
||||
"""Gibt Gemarkungsschlüssel und -namen aller Gemarkungen zurück, die die
|
||||
VG-Geometrie schneiden.
|
||||
|
||||
Da der WFS-Dienst keine serverseitigen BBox- oder Geometriefilter
|
||||
unterstützt, werden alle ``AX_Gemarkung``-Objekte geladen und client-seitig
|
||||
per ``QgsGeometry.intersects`` gefiltert.
|
||||
|
||||
Der Aufruf ist *ohne Nutzerinteraktion* – im Fehlerfall wird eine leere
|
||||
Liste zurückgegeben, damit der aufrufende Workflow manuellen Fallback
|
||||
ermöglicht.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
vg_geometrie :
|
||||
``QgsGeometry`` der Verfahrensgebiets-Union (aus den Roh-WFS-Features
|
||||
von ``AX_BauRaumOderBodenordnungsrecht``). In EPSG:25833.
|
||||
|
||||
Returns
|
||||
-------
|
||||
List[Tuple[str, str]]
|
||||
Liste von ``(gemarkungsnummer, bezeichnung)``-Tupeln, aufsteigend
|
||||
sortiert nach Nummer. Enthält keine Duplikate. Leer bei Fehler.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from qgis.core import QgsGeometry
|
||||
>>> vg_geom = QgsGeometry.fromWkt("POLYGON((414000 5657000, 415000 5657000, ...))")
|
||||
>>> result = gemarkung_schluessel_und_namen_von_vg_geometrie(vg_geom)
|
||||
>>> # result: [('1234', 'Beispielgemarkung'), ('5678', 'Andere Gemarkung')]
|
||||
"""
|
||||
if vg_geometrie is None or vg_geometrie.isEmpty():
|
||||
_warn("Gemarkung", "[DIAG] VG-Geometrie ist None oder leer – Gemarkungssuche abgebrochen.", duration=8)
|
||||
return []
|
||||
|
||||
try:
|
||||
layer = _load_all_gemarkung_layer()
|
||||
except Exception as exc:
|
||||
_warn("Gemarkung", f"[DIAG] Fehler beim Laden des Gemarkung-Layers: {exc}", duration=8)
|
||||
return []
|
||||
|
||||
if not layer:
|
||||
_warn("Gemarkung", "[DIAG] AX_Gemarkung-Layer ungültig oder nicht ladbar.", duration=8)
|
||||
return []
|
||||
|
||||
# Feature-Anzahl ermitteln (0 = WFS gibt nichts zurück)
|
||||
alle_features = list(layer.getFeatures())
|
||||
if not alle_features:
|
||||
_warn(
|
||||
"Gemarkung",
|
||||
"[DIAG] AX_Gemarkung-Layer ist gültig, aber enthält 0 Features. "
|
||||
"WFS-Dienst liefert ohne Filter möglicherweise kein Ergebnis.",
|
||||
duration=10,
|
||||
)
|
||||
return []
|
||||
|
||||
# Feldnamen der ersten Features loggen
|
||||
feldnamen = [alle_features[0].fields().field(i).name() for i in range(alle_features[0].fields().count())]
|
||||
hat_geom = sum(1 for f in alle_features[:20] if f.hasGeometry() and not f.geometry().isEmpty())
|
||||
_info(
|
||||
"Gemarkung",
|
||||
f"[DIAG] {len(alle_features)} Gemarkungen geladen. "
|
||||
f"Von ersten 20 haben {hat_geom} Geometrie. "
|
||||
f"Alle Felder ({len(feldnamen)}): {feldnamen}",
|
||||
duration=15,
|
||||
)
|
||||
|
||||
result: dict[str, str] = {}
|
||||
kein_schnitt = 0
|
||||
for feat in alle_features:
|
||||
try:
|
||||
gem_geom = feat.geometry()
|
||||
if gem_geom is None or gem_geom.isEmpty():
|
||||
continue
|
||||
if not gem_geom.intersects(vg_geometrie):
|
||||
kein_schnitt += 1
|
||||
continue
|
||||
except Exception as exc:
|
||||
_warn("Gemarkung", f"[DIAG] intersects() Fehler: {exc}", duration=8)
|
||||
continue
|
||||
|
||||
nummer, name = _extract_schluessel_und_name(feat)
|
||||
if nummer and nummer not in result:
|
||||
result[nummer] = name
|
||||
|
||||
if not result:
|
||||
_warn(
|
||||
"Gemarkung",
|
||||
f"[DIAG] 0 Gemarkungen nach Verschneidung. {kein_schnitt} Features ohne Schnitt. "
|
||||
f"VG-Geometrie gültig: {not vg_geometrie.isNull()}, "
|
||||
f"VG-Typ: {vg_geometrie.type()}, VG-WKT (Anfang): {vg_geometrie.asWkt()[:80]}",
|
||||
duration=15,
|
||||
)
|
||||
|
||||
return sorted(result.items(), key=lambda t: t[0])
|
||||
|
||||
|
||||
def lade_gemarkungen_layer():
|
||||
"""Lädt alle Katasterbezirk-Objekte aus dem vereinfachten ALKIS-WFS
|
||||
als benannten Layer „Gemarkungen".
|
||||
|
||||
Dieser Layer dient als Referenzkontext im Bearbeitungs-Tab und wird
|
||||
mit dem Stil ``gemarkungen_beschriftet.qml`` versehen.
|
||||
|
||||
Returns
|
||||
-------
|
||||
QgsVectorLayer or None
|
||||
Geladener Layer (Name ``"Gemarkungen"``) oder ``None`` bei Ungültigkeit
|
||||
(z. B. Dienst nicht erreichbar).
|
||||
"""
|
||||
uri = (
|
||||
f"url='{ALKIS_VEREINF_URL}' typename='{KATASTERBEZIRK_TYPENAME}' "
|
||||
f"srsname='EPSG:25833'"
|
||||
)
|
||||
layer = QgsVectorLayer(uri, "Gemarkungen", "WFS")
|
||||
return layer if (layer and layer.isValid()) else None
|
||||
|
||||
|
||||
def gemarkung_schluessel_von_vg_geometrie(vg_geometrie) -> List[str]:
|
||||
"""Gibt ausschließlich die Gemarkungsnummern für die VG-Geometrie zurück.
|
||||
|
||||
Convenience-Wrapper um
|
||||
:func:`gemarkung_schluessel_und_namen_von_vg_geometrie`.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
vg_geometrie :
|
||||
``QgsGeometry`` des Verfahrensgebiets in EPSG:25833.
|
||||
|
||||
Returns
|
||||
-------
|
||||
List[str]
|
||||
Sortierte Liste der Gemarkungsnummern. Leer bei Fehler oder keinem Treffer.
|
||||
"""
|
||||
return [nr for nr, _ in gemarkung_schluessel_und_namen_von_vg_geometrie(vg_geometrie)]
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,809 @@
|
||||
"""sn_verfahrensgebiet/functions/knickpunkt_pruefung.py – Knickpunktbasierte BROR-Prüfung.
|
||||
|
||||
Vergleicht die Knickpunkte (Stützpunkte) des BauRaumOderBodenordnungsrecht-
|
||||
Polygons mit den Punktorten der geladenen ``AX_PunktortTA``-Objekte und
|
||||
identifiziert:
|
||||
|
||||
- **Identische Punkte** (grün): An einem BROR-Knickpunkt liegt ein PunktortTA.
|
||||
- **Fehlende PunktortTA** (rot): Ein BROR-Knickpunkt hat keinen zugehörigen PunktortTA.
|
||||
- **Fehlende Knickpunkte** (orange): Ein PunktortTA liegt innerhalb des
|
||||
3-cm-Puffers einer Verbindungslinie zwischen zwei aufeinanderfolgenden
|
||||
gematchten PunktortTA am BROR-Umring, hat aber keinen entsprechenden
|
||||
Knickpunkt im BROR.
|
||||
|
||||
**Algorithmus:**
|
||||
|
||||
1. BROR-Polygon-Stützpunkte (Knickpunkte) extrahieren.
|
||||
2. Spatial-Index auf PunktortTA aufbauen.
|
||||
3. Für jeden Knickpunkt nächsten PunktortTA im Radius von
|
||||
:data:`KOORDINATEN_TOLERANZ_M` suchen → grün oder rot.
|
||||
Gematchte PunktortTA-Feature-IDs merken.
|
||||
4. Entlang der BROR-Außengrenze aufeinanderfolgende gematchte PunktortTA
|
||||
ermitteln und für jedes konsekutive Paar eine gerade Verbindungslinie
|
||||
bilden; diese um :data:`VERBINDUNGSLINIE_PUFFER_M` puffern.
|
||||
5. Nicht-gematchte PunktortTA innerhalb der Puffer → orange.
|
||||
Verbindungslinien- und Puffer-Layer werden dem QGIS-Projekt hinzugefügt.
|
||||
|
||||
**Rückgabe:** ``(result_layer, identisch, kein_pta, fehlender_knick)``
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Dict, List, Optional, Set, Tuple
|
||||
|
||||
from sn_basis.functions.qgiscore_wrapper import (
|
||||
QgsVectorLayer,
|
||||
QgsFeature,
|
||||
QgsField,
|
||||
QgsGeometry,
|
||||
)
|
||||
from sn_basis.functions.qt_wrapper import QVariant
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Konstanten
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
#: Koordinatentoleranz für die Knickpunkt ↔ PunktortTA-Übereinstimmung (m).
|
||||
KOORDINATEN_TOLERANZ_M: float = 0.01
|
||||
|
||||
#: Puffer um den BROR-Umring für die Suche nach fehlenden Knickpunkten (m).
|
||||
#: Wird aus Kompatibilitätsgründen behalten, wird aber nicht mehr verwendet.
|
||||
BOUNDARY_PUFFER_M: float = 0.5
|
||||
|
||||
#: Puffer um die Verbindungslinie zwischen zwei aufeinanderfolgenden PunktortTA (m).
|
||||
VERBINDUNGSLINIE_PUFFER_M: float = 0.03
|
||||
|
||||
#: Rundungsgenauigkeit für Vertex-Koordinaten im Flurstück-Set (Dezimalstellen).
|
||||
_VERTEX_ROUND = 3
|
||||
|
||||
ERGEBNIS_LAYER_NAME = "Knickpunktprüfung"
|
||||
FELD_TYP = "typ"
|
||||
WERT_IDENTISCH = "identisch"
|
||||
WERT_KEIN_PTA = "kein_pta"
|
||||
WERT_FEHLENDER_KNICK = "fehlender_knick"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Interne Hilfsfunktionen
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _extrahiere_knickpunkte(bror_layer) -> List[object]:
|
||||
"""Extrahiert alle Stützpunkte (Knickpunkte) des BROR-Polygons.
|
||||
|
||||
Doppelte Start/End-Punkte eines Rings werden dedupliziert. Gibt eine
|
||||
flache Liste von ``QgsPointXY``-Objekten zurück.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
bror_layer :
|
||||
BauRaumOderBodenordnungsrecht-Polygon-Layer.
|
||||
|
||||
Returns
|
||||
-------
|
||||
List[QgsPointXY]
|
||||
Alle eindeutigen Stützpunkte; leer bei Fehler.
|
||||
"""
|
||||
try:
|
||||
from qgis.core import QgsPointXY # type: ignore[import]
|
||||
|
||||
bror_feats = [
|
||||
f for f in bror_layer.getFeatures()
|
||||
if f.geometry() and not f.geometry().isEmpty()
|
||||
]
|
||||
if not bror_feats:
|
||||
return []
|
||||
|
||||
union_geom = QgsGeometry.unaryUnion([f.geometry() for f in bror_feats])
|
||||
if union_geom is None or union_geom.isEmpty():
|
||||
return []
|
||||
|
||||
punkte: List[object] = []
|
||||
seen: Set[Tuple[float, float]] = set()
|
||||
|
||||
def _add_ring(vertices):
|
||||
# Letzter Punkt = erster Punkt (Ring geschlossen) → überspringen
|
||||
ring_pts = list(vertices)
|
||||
n = len(ring_pts)
|
||||
if n >= 2:
|
||||
# Prüfen ob letzter Punkt identisch mit erstem
|
||||
last = n - 1
|
||||
if (
|
||||
abs(ring_pts[last].x() - ring_pts[0].x()) < 1e-9
|
||||
and abs(ring_pts[last].y() - ring_pts[0].y()) < 1e-9
|
||||
):
|
||||
ring_pts = ring_pts[:-1]
|
||||
for pt in ring_pts:
|
||||
key = (round(pt.x(), _VERTEX_ROUND), round(pt.y(), _VERTEX_ROUND))
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
punkte.append(pt)
|
||||
|
||||
geom_type = union_geom.wkbType()
|
||||
# Polygon oder MultiPolygon
|
||||
if union_geom.isMultipart():
|
||||
for poly in union_geom.asMultiPolygon():
|
||||
for ring in poly:
|
||||
_add_ring([QgsPointXY(x, y) for x, y in ring])
|
||||
else:
|
||||
for ring in union_geom.asPolygon():
|
||||
_add_ring([QgsPointXY(x, y) for x, y in ring])
|
||||
|
||||
return punkte
|
||||
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
def _erstelle_ergebnis_layer(crs_authid: str) -> object:
|
||||
"""Erzeugt den leeren Punkt-Ergebnislayer mit Feld ``typ``.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
crs_authid :
|
||||
CRS-Kennung, z.B. ``"EPSG:25833"``.
|
||||
|
||||
Returns
|
||||
-------
|
||||
QgsVectorLayer
|
||||
Leerer Memory-Layer vom Typ Point.
|
||||
"""
|
||||
layer = QgsVectorLayer(
|
||||
f"Point?crs={crs_authid}", ERGEBNIS_LAYER_NAME, "memory"
|
||||
)
|
||||
pr = layer.dataProvider()
|
||||
pr.addAttributes([QgsField(FELD_TYP, QVariant.String)])
|
||||
layer.updateFields()
|
||||
return layer
|
||||
|
||||
|
||||
def _setze_kategorien_symbologie(layer) -> None:
|
||||
"""Wendet Kategoriesymbologie an: grün/rot/orange Punkte.
|
||||
|
||||
- ``identisch`` → grün
|
||||
- ``kein_pta`` → rot
|
||||
- ``fehlender_knick`` → orange
|
||||
|
||||
Wird bei fehlendem QGIS-Kontext still ignoriert.
|
||||
"""
|
||||
try:
|
||||
from qgis.core import ( # type: ignore[import]
|
||||
QgsCategorizedSymbolRenderer,
|
||||
QgsRendererCategory,
|
||||
QgsMarkerSymbol,
|
||||
)
|
||||
|
||||
def _marker(color: str, size: str = "2") -> object:
|
||||
return QgsMarkerSymbol.createSimple(
|
||||
{"color": color, "size": size, "outline_style": "no"}
|
||||
)
|
||||
|
||||
categories = [
|
||||
QgsRendererCategory(
|
||||
WERT_IDENTISCH,
|
||||
_marker("0,180,0,255"),
|
||||
"identisch (Knickpunkt mit PunktortTA)",
|
||||
),
|
||||
QgsRendererCategory(
|
||||
WERT_KEIN_PTA,
|
||||
_marker("220,0,0,255"),
|
||||
"kein PunktortTA an Knickpunkt",
|
||||
),
|
||||
QgsRendererCategory(
|
||||
WERT_FEHLENDER_KNICK,
|
||||
_marker("255,128,0,255"),
|
||||
"fehlender Knickpunkt (PunktortTA ohne BROR-Knickpunkt)",
|
||||
),
|
||||
]
|
||||
renderer = QgsCategorizedSymbolRenderer(FELD_TYP, categories)
|
||||
layer.setRenderer(renderer)
|
||||
except Exception:
|
||||
pass # Kein QGIS-Kontext (Tests) → Standarddarstellung behalten
|
||||
|
||||
|
||||
def _baue_pta_index(punktort_layer) -> Tuple[object, Dict[int, object]]:
|
||||
"""Baut einen QgsSpatialIndex und eine ID→Geometrie-Map für PunktortTA.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
punktort_layer :
|
||||
Geladener ``AX_PunktortTA``-Layer.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Tuple[QgsSpatialIndex, Dict[int, QgsGeometry]]
|
||||
Index und Feature-ID-zu-Geometrie-Dict.
|
||||
"""
|
||||
from qgis.core import QgsSpatialIndex # type: ignore[import]
|
||||
|
||||
index = QgsSpatialIndex()
|
||||
geom_map: Dict[int, object] = {}
|
||||
for feat in punktort_layer.getFeatures():
|
||||
geom = feat.geometry()
|
||||
if geom and not geom.isEmpty():
|
||||
index.insertFeature(feat)
|
||||
geom_map[feat.id()] = geom
|
||||
return index, geom_map
|
||||
|
||||
|
||||
def _fehlende_knickpunkte_via_verbindungslinien(
|
||||
bror_union,
|
||||
pta_index,
|
||||
geom_map: Dict[int, object],
|
||||
matched_pta_ids: Set[int],
|
||||
punktort_layer,
|
||||
crs_authid: str,
|
||||
) -> Tuple[int, List[object]]:
|
||||
"""Ermittelt fehlende Knickpunkte via Verbindungslinien-Puffer.
|
||||
|
||||
Bestimmt entlang der BROR-Außengrenze die Reihenfolge der gematchten
|
||||
``AX_PunktortTA``. Für jedes konsekutive Paar wird eine gerade
|
||||
Verbindungslinie erzeugt und um :data:`VERBINDUNGSLINIE_PUFFER_M` gepuffert.
|
||||
Nicht-gematchte PunktortTA innerhalb dieses Puffers werden als fehlende
|
||||
Knickpunkte (orange) gewertet.
|
||||
|
||||
Fügt dem QGIS-Projekt zwei Hilfs-Layer hinzu (falls ein Projekt-Kontext
|
||||
vorhanden ist):
|
||||
|
||||
- *Knickpunktprüfung – Verbindungslinien*
|
||||
- *Knickpunktprüfung – Puffer*
|
||||
|
||||
Parameters
|
||||
----------
|
||||
bror_union :
|
||||
Vereinigungsgeometrie des BROR-Polygons (``QgsGeometry``).
|
||||
pta_index :
|
||||
Spatial-Index der PunktortTA-Features (``QgsSpatialIndex``).
|
||||
geom_map :
|
||||
Feature-ID → ``QgsGeometry``-Mapping für PunktortTA.
|
||||
matched_pta_ids :
|
||||
IDs der bereits gematchten PunktortTA (identisch mit BROR-Knickpunkten).
|
||||
punktort_layer :
|
||||
Geladener ``AX_PunktortTA``-Layer (für zukünftige Erweiterungen
|
||||
reserviert; wird derzeit nicht direkt verwendet).
|
||||
crs_authid :
|
||||
CRS-Kennung des BROR-Layers, z.B. ``"EPSG:25833"``.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Tuple[int, List[QgsFeature]]
|
||||
``(fehlender_knick_count, orange_feats)`` – Anzahl gefundener fehlender
|
||||
Knickpunkte und Liste der zugehörigen ``QgsFeature``-Objekte.
|
||||
"""
|
||||
try:
|
||||
from qgis.core import QgsPointXY, QgsProject # type: ignore[import]
|
||||
except ImportError:
|
||||
return 0, []
|
||||
|
||||
if bror_union is None or bror_union.isEmpty():
|
||||
return 0, []
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Alle Ringe extrahieren (Außen- und Innenringe)
|
||||
# ------------------------------------------------------------------
|
||||
all_rings: List[List[object]] = []
|
||||
if bror_union.isMultipart():
|
||||
for poly in bror_union.asMultiPolygon():
|
||||
for ring in poly:
|
||||
all_rings.append([QgsPointXY(x, y) for x, y in ring])
|
||||
else:
|
||||
for ring in bror_union.asPolygon():
|
||||
all_rings.append([QgsPointXY(x, y) for x, y in ring])
|
||||
|
||||
# Schlusspunkt (= Startpunkt, Ring geschlossen) entfernen
|
||||
cleaned_rings: List[List[object]] = []
|
||||
for ring in all_rings:
|
||||
if len(ring) >= 2:
|
||||
if (
|
||||
abs(ring[-1].x() - ring[0].x()) < 1e-9
|
||||
and abs(ring[-1].y() - ring[0].y()) < 1e-9
|
||||
):
|
||||
ring = ring[:-1]
|
||||
if len(ring) >= 2:
|
||||
cleaned_rings.append(ring)
|
||||
|
||||
line_geoms: List[object] = []
|
||||
buffer_geoms: List[object] = []
|
||||
orange_ids: Set[int] = set()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Pro Ring: gematchte PunktortTA in Reihenfolge, Paare bilden
|
||||
# ------------------------------------------------------------------
|
||||
for ring_pts in cleaned_rings:
|
||||
# Gematchte PunktortTA auf diesem Ring lokalisieren
|
||||
pta_on_ring: List[Tuple[int, int, object]] = [] # (ring_idx, fid, pt)
|
||||
for fid in matched_pta_ids:
|
||||
pta_geom = geom_map.get(fid)
|
||||
if pta_geom is None:
|
||||
continue
|
||||
pta_pt = pta_geom.asPoint()
|
||||
for i, ring_pt in enumerate(ring_pts):
|
||||
dist_sq = (
|
||||
(ring_pt.x() - pta_pt.x()) ** 2
|
||||
+ (ring_pt.y() - pta_pt.y()) ** 2
|
||||
)
|
||||
if dist_sq <= KOORDINATEN_TOLERANZ_M ** 2:
|
||||
pta_on_ring.append((i, fid, ring_pt))
|
||||
break
|
||||
|
||||
if len(pta_on_ring) < 2:
|
||||
continue
|
||||
|
||||
pta_on_ring.sort(key=lambda t: t[0])
|
||||
num_pta = len(pta_on_ring)
|
||||
|
||||
for k in range(num_pta):
|
||||
_, _, pt1 = pta_on_ring[k]
|
||||
_, _, pt2 = pta_on_ring[(k + 1) % num_pta]
|
||||
|
||||
# Gerade Verbindungslinie zwischen den zwei PunktortTA
|
||||
line_geom = QgsGeometry.fromPolylineXY([pt1, pt2])
|
||||
if line_geom is None or line_geom.isEmpty():
|
||||
continue
|
||||
|
||||
line_geoms.append(line_geom)
|
||||
|
||||
buffer_geom = line_geom.buffer(VERBINDUNGSLINIE_PUFFER_M, 8)
|
||||
if buffer_geom is None or buffer_geom.isEmpty():
|
||||
continue
|
||||
|
||||
buffer_geoms.append(buffer_geom)
|
||||
|
||||
# Nicht-gematchte PunktortTA im Puffer suchen (via Spatial-Index)
|
||||
bb = buffer_geom.boundingBox()
|
||||
kandidaten = pta_index.intersects(bb)
|
||||
for fid in kandidaten:
|
||||
if fid in matched_pta_ids or fid in orange_ids:
|
||||
continue
|
||||
candidate_geom = geom_map.get(fid)
|
||||
if candidate_geom is None:
|
||||
continue
|
||||
try:
|
||||
if candidate_geom.within(buffer_geom):
|
||||
orange_ids.add(fid)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Orange Features erstellen
|
||||
# ------------------------------------------------------------------
|
||||
orange_feats: List[object] = []
|
||||
for fid in orange_ids:
|
||||
pta_geom = geom_map.get(fid)
|
||||
if pta_geom is None:
|
||||
continue
|
||||
feat = QgsFeature()
|
||||
feat.setGeometry(pta_geom)
|
||||
feat.setAttributes([WERT_FEHLENDER_KNICK])
|
||||
orange_feats.append(feat)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Hilfs-Layer erstellen und zum Projekt hinzufügen
|
||||
# ------------------------------------------------------------------
|
||||
try:
|
||||
lines_layer = QgsVectorLayer(
|
||||
f"LineString?crs={crs_authid}",
|
||||
"Knickpunktprüfung – Verbindungslinien",
|
||||
"memory",
|
||||
)
|
||||
lines_pr = lines_layer.dataProvider()
|
||||
line_feats_list = []
|
||||
for lg in line_geoms:
|
||||
f = QgsFeature()
|
||||
f.setGeometry(lg)
|
||||
line_feats_list.append(f)
|
||||
lines_pr.addFeatures(line_feats_list)
|
||||
lines_layer.updateExtents()
|
||||
|
||||
buffer_layer = QgsVectorLayer(
|
||||
f"Polygon?crs={crs_authid}",
|
||||
"Knickpunktprüfung – Puffer",
|
||||
"memory",
|
||||
)
|
||||
buffer_pr = buffer_layer.dataProvider()
|
||||
buffer_feats_list = []
|
||||
for bg in buffer_geoms:
|
||||
f = QgsFeature()
|
||||
f.setGeometry(bg)
|
||||
buffer_feats_list.append(f)
|
||||
buffer_pr.addFeatures(buffer_feats_list)
|
||||
buffer_layer.updateExtents()
|
||||
|
||||
project = QgsProject.instance()
|
||||
if project is not None:
|
||||
project.addMapLayer(lines_layer)
|
||||
project.addMapLayer(buffer_layer)
|
||||
except Exception:
|
||||
pass # Kein QGIS-Kontext (Tests) → Layer-Erstellung überspringen
|
||||
|
||||
return len(orange_feats), orange_feats
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Öffentliche API
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def knickpunkt_pruefung(
|
||||
bror_layer,
|
||||
punktort_layer,
|
||||
flst_layer,
|
||||
progress=None,
|
||||
knickpunkte: Optional[List] = None,
|
||||
) -> Tuple[Optional[object], int, int, int]:
|
||||
"""Knickpunktbasierte BROR-Prüfung.
|
||||
|
||||
Vergleicht BROR-Knickpunkte mit ``AX_PunktortTA``-Objekten und ermittelt
|
||||
fehlende Knickpunkte anhand von Flurstücksgrenzen.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
bror_layer :
|
||||
BauRaumOderBodenordnungsrecht-Polygon-Layer.
|
||||
punktort_layer :
|
||||
Geladener ``AX_PunktortTA``-Layer (Ergebnis aus ``grenzpunkt_alkis``).
|
||||
flst_layer :
|
||||
Nicht mehr verwendet. Wird aus Kompatibilitätsgründen weiterhin
|
||||
akzeptiert.
|
||||
progress :
|
||||
Optionaler ``ProgressDialog`` für Fortschrittsanzeige.
|
||||
knickpunkte :
|
||||
Vorberechnete Knickpunktliste (optional). Wenn ``None``, werden die
|
||||
Knickpunkte aus ``bror_layer`` extrahiert.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Tuple[Optional[QgsVectorLayer], int, int, int]
|
||||
``(result_layer, identisch_count, kein_pta_count, fehlender_knick_count)``
|
||||
|
||||
- ``result_layer``: Punkt-Layer mit grünen/roten/orangen Punkten,
|
||||
oder ``None`` bei Abbruch oder fehlgeschlagener Verarbeitung.
|
||||
- ``identisch_count``: Knickpunkte mit passendem PunktortTA (grün).
|
||||
- ``kein_pta_count``: Knickpunkte ohne passenden PunktortTA (rot).
|
||||
- ``fehlender_knick_count``: PunktortTA ohne BROR-Knickpunkt (orange).
|
||||
"""
|
||||
|
||||
def _canceled() -> bool:
|
||||
return progress is not None and progress.is_canceled()
|
||||
|
||||
def _schritt(label: str, wert: int) -> None:
|
||||
if progress is not None:
|
||||
progress.set_label(label)
|
||||
progress.set_value(wert)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Schritt 0: Knickpunkte ermitteln
|
||||
# ------------------------------------------------------------------
|
||||
_schritt("Knickpunktprüfung: Knickpunkte extrahieren …", 0)
|
||||
|
||||
if knickpunkte is None:
|
||||
knickpunkte = _extrahiere_knickpunkte(bror_layer)
|
||||
|
||||
if not knickpunkte:
|
||||
return None, 0, 0, 0
|
||||
|
||||
n = len(knickpunkte)
|
||||
crs_authid = bror_layer.crs().authid()
|
||||
|
||||
if _canceled():
|
||||
return None, 0, 0, 0
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Schritt 1: Spatial-Index für PunktortTA aufbauen
|
||||
# ------------------------------------------------------------------
|
||||
_schritt("Knickpunktprüfung: PunktortTA-Index aufbauen …", 1)
|
||||
|
||||
if punktort_layer is None:
|
||||
return None, 0, 0, 0
|
||||
|
||||
try:
|
||||
pta_index, geom_map = _baue_pta_index(punktort_layer)
|
||||
except Exception:
|
||||
return None, 0, 0, 0
|
||||
|
||||
if _canceled():
|
||||
return None, 0, 0, 0
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Schritte 2…n+1: Jeden Knickpunkt mit PunktortTA vergleichen
|
||||
# ------------------------------------------------------------------
|
||||
try:
|
||||
from qgis.core import QgsRectangle # type: ignore[import]
|
||||
except ImportError:
|
||||
return None, 0, 0, 0
|
||||
|
||||
ergebnis_layer = _erstelle_ergebnis_layer(crs_authid)
|
||||
pr = ergebnis_layer.dataProvider()
|
||||
feats_to_add: List[object] = []
|
||||
|
||||
identisch_count = 0
|
||||
kein_pta_count = 0
|
||||
matched_pta_ids: Set[int] = set()
|
||||
|
||||
for i, knick_pt in enumerate(knickpunkte):
|
||||
if _canceled():
|
||||
return None, 0, 0, 0
|
||||
|
||||
_schritt(f"Knickpunktprüfung: Knickpunkt {i + 1}/{n} prüfen …", i + 2)
|
||||
|
||||
x, y = knick_pt.x(), knick_pt.y()
|
||||
t = KOORDINATEN_TOLERANZ_M
|
||||
suchbox = QgsRectangle(x - t, y - t, x + t, y + t)
|
||||
kandidaten = pta_index.intersects(suchbox)
|
||||
|
||||
gefunden = False
|
||||
for fid in kandidaten:
|
||||
pta_geom = geom_map.get(fid)
|
||||
if pta_geom is None:
|
||||
continue
|
||||
pta_pt = pta_geom.asPoint()
|
||||
dist = ((pta_pt.x() - x) ** 2 + (pta_pt.y() - y) ** 2) ** 0.5
|
||||
if dist <= KOORDINATEN_TOLERANZ_M:
|
||||
gefunden = True
|
||||
matched_pta_ids.add(fid)
|
||||
break
|
||||
|
||||
feat = QgsFeature()
|
||||
try:
|
||||
from qgis.core import QgsPointXY # type: ignore[import]
|
||||
feat.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(x, y)))
|
||||
except Exception:
|
||||
continue
|
||||
feat.setAttributes([WERT_IDENTISCH if gefunden else WERT_KEIN_PTA])
|
||||
feats_to_add.append(feat)
|
||||
|
||||
if gefunden:
|
||||
identisch_count += 1
|
||||
else:
|
||||
kein_pta_count += 1
|
||||
|
||||
if _canceled():
|
||||
return None, 0, 0, 0
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Schritt n+2: Fehlende Knickpunkte (orange) via Verbindungslinien
|
||||
# ------------------------------------------------------------------
|
||||
_schritt("Knickpunktprüfung: Fehlende Knickpunkte ermitteln …", n + 2)
|
||||
|
||||
fehlender_knick_count = 0
|
||||
|
||||
try:
|
||||
bror_feats_union = [
|
||||
f for f in bror_layer.getFeatures()
|
||||
if f.geometry() and not f.geometry().isEmpty()
|
||||
]
|
||||
bror_union_geom = QgsGeometry.unaryUnion(
|
||||
[f.geometry() for f in bror_feats_union]
|
||||
)
|
||||
except Exception:
|
||||
bror_union_geom = None
|
||||
|
||||
if not _canceled():
|
||||
fehlender_knick_count, orange_feats = _fehlende_knickpunkte_via_verbindungslinien(
|
||||
bror_union_geom,
|
||||
pta_index,
|
||||
geom_map,
|
||||
matched_pta_ids,
|
||||
punktort_layer,
|
||||
crs_authid,
|
||||
)
|
||||
feats_to_add.extend(orange_feats)
|
||||
|
||||
if _canceled():
|
||||
return None, 0, 0, 0
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Schritt n+3: Ergebnislayer fertigstellen
|
||||
# ------------------------------------------------------------------
|
||||
_schritt("Knickpunktprüfung: Ergebnislayer erstellen …", n + 3)
|
||||
|
||||
pr.addFeatures(feats_to_add)
|
||||
ergebnis_layer.updateExtents()
|
||||
_setze_kategorien_symbologie(ergebnis_layer)
|
||||
|
||||
return ergebnis_layer, identisch_count, kein_pta_count, fehlender_knick_count
|
||||
|
||||
|
||||
def erstelle_korrigiertes_polygon(
|
||||
bror_layer,
|
||||
result_layer,
|
||||
crs_authid: str,
|
||||
) -> Tuple[Optional[object], Optional[object]]:
|
||||
"""Erstellt korrigierte Verbindungslinien und ein Polygon aus identischen
|
||||
und gefundenen fehlenden Knickpunkten.
|
||||
|
||||
Kombiniert die gematchten PunktortTA (``identisch``, grün) mit den
|
||||
gefundenen fehlenden Knickpunkten (``fehlender_knick``, orange) zu einem
|
||||
korrigierten Verlauf entlang der BROR-Außengrenze. Orange Punkte werden
|
||||
nach ihrer Projektion auf die Verbindungsstrecke zwischen ihren zwei
|
||||
umgebenden identischen PunktortTA einsortiert.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
bror_layer :
|
||||
BauRaumOderBodenordnungsrecht-Polygon-Layer (für die Ringreihenfolge).
|
||||
result_layer :
|
||||
Punkt-Ergebnislayer aus :func:`knickpunkt_pruefung` mit Feld ``typ``.
|
||||
crs_authid :
|
||||
CRS-Kennung, z.B. ``"EPSG:25833"``.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Tuple[Optional[QgsVectorLayer], Optional[QgsVectorLayer]]
|
||||
``(linien_layer, polygon_layer)`` – Memory-Layer mit den korrigierten
|
||||
Verbindungslinien und dem abgeleiteten Polygon, oder ``(None, None)``
|
||||
bei fehlgeschlagener Verarbeitung.
|
||||
"""
|
||||
try:
|
||||
from qgis.core import QgsPointXY # type: ignore[import]
|
||||
except ImportError:
|
||||
return None, None
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Punkte aus Ergebnislayer extrahieren
|
||||
# ------------------------------------------------------------------
|
||||
identisch_pts: List[object] = []
|
||||
orange_pts: List[object] = []
|
||||
|
||||
try:
|
||||
for feat in result_layer.getFeatures():
|
||||
geom = feat.geometry()
|
||||
if geom is None or geom.isEmpty():
|
||||
continue
|
||||
pt = geom.asPoint()
|
||||
typ = feat.attribute(FELD_TYP)
|
||||
if typ == WERT_IDENTISCH:
|
||||
identisch_pts.append(QgsPointXY(pt.x(), pt.y()))
|
||||
elif typ == WERT_FEHLENDER_KNICK:
|
||||
orange_pts.append(QgsPointXY(pt.x(), pt.y()))
|
||||
except Exception:
|
||||
return None, None
|
||||
|
||||
if len(identisch_pts) < 3:
|
||||
return None, None
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Alle Polygon-Teile mit allen Ringen (Außen- und Innenringe)
|
||||
# ------------------------------------------------------------------
|
||||
try:
|
||||
bror_feats = [
|
||||
f for f in bror_layer.getFeatures()
|
||||
if f.geometry() and not f.geometry().isEmpty()
|
||||
]
|
||||
bror_union = QgsGeometry.unaryUnion([f.geometry() for f in bror_feats])
|
||||
if bror_union is None or bror_union.isEmpty():
|
||||
return None, None
|
||||
|
||||
if bror_union.isMultipart():
|
||||
all_poly_parts = [poly for poly in bror_union.asMultiPolygon() if poly]
|
||||
else:
|
||||
poly = bror_union.asPolygon()
|
||||
all_poly_parts = [poly] if poly else []
|
||||
except Exception:
|
||||
return None, None
|
||||
|
||||
if not all_poly_parts:
|
||||
return None, None
|
||||
|
||||
all_line_segments: List[Tuple[object, object]] = []
|
||||
all_polygon_feats: List[object] = []
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Pro Polygon-Teil: alle Ringe (Außen + Innen) verarbeiten
|
||||
# ------------------------------------------------------------------
|
||||
for poly_rings in all_poly_parts:
|
||||
corrected_qgs_rings: List[List[object]] = []
|
||||
|
||||
for raw_ring in poly_rings:
|
||||
ring_pts = [QgsPointXY(x, y) for x, y in raw_ring]
|
||||
if len(ring_pts) >= 2 and (
|
||||
abs(ring_pts[-1].x() - ring_pts[0].x()) < 1e-9
|
||||
and abs(ring_pts[-1].y() - ring_pts[0].y()) < 1e-9
|
||||
):
|
||||
ring_pts = ring_pts[:-1]
|
||||
|
||||
if len(ring_pts) < 3:
|
||||
continue
|
||||
|
||||
# Identisch-Punkte auf diesem Ring lokalisieren
|
||||
on_ring: List[Tuple[int, object]] = []
|
||||
for pt in identisch_pts:
|
||||
for i, rpt in enumerate(ring_pts):
|
||||
dist_sq = (rpt.x() - pt.x()) ** 2 + (rpt.y() - pt.y()) ** 2
|
||||
if dist_sq <= KOORDINATEN_TOLERANZ_M ** 2:
|
||||
on_ring.append((i, pt))
|
||||
break
|
||||
|
||||
on_ring.sort(key=lambda t: t[0])
|
||||
|
||||
if len(on_ring) < 3:
|
||||
# Fallback: Original-Ring verwenden
|
||||
ordered_pts: List[object] = ring_pts
|
||||
for j in range(len(ring_pts)):
|
||||
all_line_segments.append(
|
||||
(ring_pts[j], ring_pts[(j + 1) % len(ring_pts)])
|
||||
)
|
||||
else:
|
||||
# Korrigierten Verlauf für diesen Ring aufbauen
|
||||
ordered_pts = []
|
||||
n_part = len(on_ring)
|
||||
|
||||
for k in range(n_part):
|
||||
_, pt1 = on_ring[k]
|
||||
_, pt2 = on_ring[(k + 1) % n_part]
|
||||
|
||||
dx = pt2.x() - pt1.x()
|
||||
dy = pt2.y() - pt1.y()
|
||||
len_sq = dx * dx + dy * dy
|
||||
|
||||
between: List[Tuple[float, object]] = []
|
||||
if len_sq > 0:
|
||||
for opt in orange_pts:
|
||||
t = (
|
||||
(opt.x() - pt1.x()) * dx + (opt.y() - pt1.y()) * dy
|
||||
) / len_sq
|
||||
clamped_t = max(0.0, min(1.0, t))
|
||||
foot_x = pt1.x() + clamped_t * dx
|
||||
foot_y = pt1.y() + clamped_t * dy
|
||||
dist = (
|
||||
(opt.x() - foot_x) ** 2 + (opt.y() - foot_y) ** 2
|
||||
) ** 0.5
|
||||
if dist <= VERBINDUNGSLINIE_PUFFER_M:
|
||||
between.append((t, opt))
|
||||
|
||||
between.sort(key=lambda x: x[0])
|
||||
|
||||
segment_pts = [pt1] + [p for _, p in between]
|
||||
ordered_pts.extend(segment_pts)
|
||||
|
||||
all_pts_in_seg = segment_pts + [pt2]
|
||||
for j in range(len(all_pts_in_seg) - 1):
|
||||
all_line_segments.append(
|
||||
(all_pts_in_seg[j], all_pts_in_seg[j + 1])
|
||||
)
|
||||
|
||||
if len(ordered_pts) >= 3:
|
||||
corrected_qgs_rings.append(ordered_pts + [ordered_pts[0]])
|
||||
|
||||
if not corrected_qgs_rings:
|
||||
continue
|
||||
|
||||
# Erstes Element = Außenring, weitere = Innenringe (Löcher)
|
||||
polygon_geom = QgsGeometry.fromPolygonXY(corrected_qgs_rings)
|
||||
if polygon_geom and not polygon_geom.isEmpty():
|
||||
pf = QgsFeature()
|
||||
pf.setGeometry(polygon_geom)
|
||||
all_polygon_feats.append(pf)
|
||||
|
||||
if not all_polygon_feats:
|
||||
return None, None
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Linien-Layer erstellen
|
||||
# ------------------------------------------------------------------
|
||||
linien_layer = QgsVectorLayer(
|
||||
f"LineString?crs={crs_authid}",
|
||||
"Korrigierte Verbindungslinien (Detailprüfung)",
|
||||
"memory",
|
||||
)
|
||||
lines_pr = linien_layer.dataProvider()
|
||||
line_feats_list = []
|
||||
for p1, p2 in all_line_segments:
|
||||
lgeom = QgsGeometry.fromPolylineXY([p1, p2])
|
||||
if lgeom and not lgeom.isEmpty():
|
||||
f = QgsFeature()
|
||||
f.setGeometry(lgeom)
|
||||
line_feats_list.append(f)
|
||||
lines_pr.addFeatures(line_feats_list)
|
||||
linien_layer.updateExtents()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Polygon-Layer erstellen (ein Feature pro Teilgebiet)
|
||||
# ------------------------------------------------------------------
|
||||
polygon_layer = QgsVectorLayer(
|
||||
f"Polygon?crs={crs_authid}",
|
||||
"Korrigiertes Polygon (Detailprüfung)",
|
||||
"memory",
|
||||
)
|
||||
poly_pr = polygon_layer.dataProvider()
|
||||
poly_pr.addFeatures(all_polygon_feats)
|
||||
polygon_layer.updateExtents()
|
||||
|
||||
return linien_layer, polygon_layer
|
||||
@@ -0,0 +1,311 @@
|
||||
"""sn_verfahrensgebiet/functions/umringspruefung.py – Umringsprüfung (Variante 1).
|
||||
|
||||
Vergleicht den Umring (Boundary) des BauRaumOderBodenordnungsrecht-Polygons
|
||||
mit den Grenzen der angrenzenden Flurstücke und hebt Abweichungen hervor.
|
||||
|
||||
**Algorithmus (Variante 1 – linienbasierter Umringvergleich):**
|
||||
|
||||
1. BROR-Polygon → Umringslinie via :func:`sn_basis.functions.qgiscore_wrapper.polygon_zu_linie`
|
||||
(QGIS-3/4-kompatibel).
|
||||
2. Flurstücke, die den BROR-Umring berühren oder schneiden, werden gefiltert.
|
||||
3. Gefilterte Flurstücke → Umringslinien.
|
||||
4. Union aller Flurstücksumringe.
|
||||
5. ``BROR-Umring.difference(Flurstücksumrings-Union)`` → Abweichungen (rot).
|
||||
6. ``BROR-Umring.difference(rote_Segmente)`` → Übereinstimmungen (grün).
|
||||
7. Ergebnislayer mit Kategoriesymbologie (grün/rot) und Legendenbeschriftung.
|
||||
|
||||
**Rückgabe:** ``(result_layer, conflict_count)``
|
||||
|
||||
- ``result_layer``: Temporärer Linienlayer mit Feld ``typ``
|
||||
(``"identisch"`` = grün, ``"Abweichung"`` = rot).
|
||||
- ``conflict_count``: Anzahl der roten Konfliktsegmente (Parts).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
from sn_basis.functions.qgiscore_wrapper import (
|
||||
QgsVectorLayer,
|
||||
QgsFeature,
|
||||
QgsField,
|
||||
QgsGeometry,
|
||||
polygon_zu_linie,
|
||||
)
|
||||
from sn_basis.functions.qt_wrapper import QVariant
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Konstanten
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
#: Geometrische Toleranz für den Linienvergleich (EPSG:25833 = Meter).
|
||||
#: Der BROR-Umring wird gegen die um diesen Betrag gepufferten Flurstücksumringe
|
||||
#: verglichen, um numerische Rundungsfehler aus ALKIS-Koordinaten abzufangen.
|
||||
LINIE_TOLERANZ_M: float = 0.0001
|
||||
|
||||
ERGEBNIS_LAYER_NAME = "Umringsprüfung"
|
||||
FELD_TYP = "typ"
|
||||
WERT_IDENTISCH = "identisch"
|
||||
WERT_ABWEICHUNG = "Abweichung"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Interne Hilfsfunktionen
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _zerlege_in_teile(geom: object) -> List[object]:
|
||||
"""Zerlegt eine (Multi-)LineString-Geometrie in einzelne Teile.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
geom :
|
||||
``QgsGeometry`` vom Typ LineString oder MultiLineString.
|
||||
|
||||
Returns
|
||||
-------
|
||||
List[QgsGeometry]
|
||||
Liste einzelner Geometrieteile; leer wenn ``geom`` None oder leer ist.
|
||||
"""
|
||||
if geom is None:
|
||||
return []
|
||||
try:
|
||||
if getattr(geom, "isEmpty", lambda: True)():
|
||||
return []
|
||||
# asGeometryCollection() gibt eigene Kopien zurück – kein Ownership-Problem.
|
||||
# QgsGeometry(part) über geom.parts() ist in QGIS 4.x aus den Python-Bindings
|
||||
# entfernt worden und führt zu Absturz beim Rendern.
|
||||
parts = geom.asGeometryCollection()
|
||||
if parts:
|
||||
return [p for p in parts if p is not None and not p.isEmpty()]
|
||||
except Exception:
|
||||
pass
|
||||
# Fallback: Geometrie nicht zerlegbar → als Ganzes zurückgeben
|
||||
try:
|
||||
if not geom.isEmpty():
|
||||
return [geom]
|
||||
except Exception:
|
||||
pass
|
||||
return []
|
||||
|
||||
|
||||
def _erstelle_ergebnis_layer(crs_authid: str) -> object:
|
||||
"""Erzeugt den leeren Ergebnislayer mit Feld ``typ``.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
crs_authid :
|
||||
CRS-Kennung des Quell-Layers (z. B. ``"EPSG:25833"``).
|
||||
|
||||
Returns
|
||||
-------
|
||||
QgsVectorLayer
|
||||
Leerer Memory-Layer vom Typ LineString.
|
||||
"""
|
||||
layer = QgsVectorLayer(
|
||||
f"LineString?crs={crs_authid}", ERGEBNIS_LAYER_NAME, "memory"
|
||||
)
|
||||
pr = layer.dataProvider()
|
||||
pr.addAttributes([QgsField(FELD_TYP, QVariant.String)])
|
||||
layer.updateFields()
|
||||
return layer
|
||||
|
||||
|
||||
def _setze_kategorien_symbologie(layer) -> None:
|
||||
"""Wendet Kategoriesymbologie an: grün = identisch, rot = Abweichung.
|
||||
|
||||
Wird bei fehlendem QGIS-Kontext still ignoriert (Unit-Test-Robustheit).
|
||||
"""
|
||||
try:
|
||||
from qgis.core import ( # type: ignore[import]
|
||||
QgsCategorizedSymbolRenderer,
|
||||
QgsRendererCategory,
|
||||
QgsLineSymbol,
|
||||
)
|
||||
|
||||
green_sym = QgsLineSymbol.createSimple(
|
||||
{"color": "0,180,0,255", "line_width": "0.5"}
|
||||
)
|
||||
red_sym = QgsLineSymbol.createSimple(
|
||||
{"color": "220,0,0,255", "line_width": "0.8"}
|
||||
)
|
||||
categories = [
|
||||
QgsRendererCategory(
|
||||
WERT_IDENTISCH,
|
||||
green_sym,
|
||||
"identisch (übereinstimmend)",
|
||||
),
|
||||
QgsRendererCategory(
|
||||
WERT_ABWEICHUNG,
|
||||
red_sym,
|
||||
"Abweichung (nicht in Flurstücken)",
|
||||
),
|
||||
]
|
||||
renderer = QgsCategorizedSymbolRenderer(FELD_TYP, categories)
|
||||
layer.setRenderer(renderer)
|
||||
except Exception:
|
||||
pass # Kein QGIS-Kontext (Tests) → Standarddarstellung behalten
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Öffentliche API
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def umringspruefung(
|
||||
bror_layer,
|
||||
flst_layer,
|
||||
progress=None,
|
||||
linie_toleranz_m: float = LINIE_TOLERANZ_M,
|
||||
) -> Tuple[Optional[object], int]:
|
||||
"""Variante 1: Linienbasierter Umringvergleich.
|
||||
|
||||
Ermittelt, welche Teile des BROR-Umrings mit den Flurstücksgrenzen
|
||||
übereinstimmen (grün) und welche keine Entsprechung haben (rot).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
bror_layer :
|
||||
BauRaumOderBodenordnungsrecht-Memory-Layer (Polygon).
|
||||
flst_layer :
|
||||
Flurstücke-Layer (Polygon, WFS oder Memory).
|
||||
progress :
|
||||
Optionaler ``ProgressDialog`` aus
|
||||
:mod:`sn_basis.functions.dialog_wrapper` für Fortschrittsanzeige
|
||||
und Abbrucherkennung.
|
||||
linie_toleranz_m : float
|
||||
Geometrische Puffertoleranz für den Linienvergleich in
|
||||
CRS-Einheiten (Standardwert :data:`LINIE_TOLERANZ_M` = 0.001 m).
|
||||
|
||||
Returns
|
||||
-------
|
||||
Tuple[Optional[QgsVectorLayer], int]
|
||||
``(result_layer, conflict_count)``
|
||||
|
||||
- ``result_layer``: Linienlayer mit grünen/roten Segmenten,
|
||||
oder ``None`` bei Abbruch oder fehlgeschlagener Verarbeitung.
|
||||
- ``conflict_count``: Anzahl der roten Konfliktsegmente (BROR-Teile
|
||||
ohne Entsprechung in den Flurstücksgrenzen).
|
||||
"""
|
||||
|
||||
def _canceled() -> bool:
|
||||
return progress is not None and progress.is_canceled()
|
||||
|
||||
def _schritt(label: str, wert: int) -> None:
|
||||
if progress is not None:
|
||||
progress.set_label(label)
|
||||
progress.set_value(wert)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 1. BROR-Geometrie und Umringslinie ermitteln
|
||||
# ------------------------------------------------------------------
|
||||
_schritt("Umringsprüfung: BROR-Umring ermitteln …", 0)
|
||||
|
||||
bror_feats = [
|
||||
f for f in bror_layer.getFeatures()
|
||||
if f.geometry() and not f.geometry().isEmpty()
|
||||
]
|
||||
if not bror_feats:
|
||||
return None, 0
|
||||
|
||||
bror_union = QgsGeometry.unaryUnion([f.geometry() for f in bror_feats])
|
||||
if bror_union is None or bror_union.isEmpty():
|
||||
return None, 0
|
||||
|
||||
bror_boundary = polygon_zu_linie(bror_union)
|
||||
if bror_boundary is None or bror_boundary.isEmpty():
|
||||
return None, 0
|
||||
|
||||
crs_authid = bror_layer.crs().authid()
|
||||
|
||||
if _canceled():
|
||||
return None, 0
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 2. Relevante Flurstücke: nur jene, deren Geometrie den BROR-Umring
|
||||
# berührt oder schneidet
|
||||
# ------------------------------------------------------------------
|
||||
_schritt("Umringsprüfung: Relevante Flurstücke filtern …", 1)
|
||||
|
||||
relevante_boundaries: List[object] = []
|
||||
for feat in flst_layer.getFeatures():
|
||||
if _canceled():
|
||||
return None, 0
|
||||
geom = feat.geometry()
|
||||
if geom is None or geom.isEmpty():
|
||||
continue
|
||||
try:
|
||||
if geom.intersects(bror_boundary):
|
||||
boundary = polygon_zu_linie(geom)
|
||||
if boundary is not None and not boundary.isEmpty():
|
||||
relevante_boundaries.append(boundary)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if relevante_boundaries:
|
||||
flst_boundary_union = QgsGeometry.unaryUnion(relevante_boundaries)
|
||||
else:
|
||||
flst_boundary_union = None
|
||||
|
||||
if _canceled():
|
||||
return None, 0
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 3. Segmentvergleich: Differenz und Übereinstimmung berechnen
|
||||
# ------------------------------------------------------------------
|
||||
_schritt("Umringsprüfung: Segmentvergleich …", 2)
|
||||
|
||||
ergebnis_layer = _erstelle_ergebnis_layer(crs_authid)
|
||||
pr = ergebnis_layer.dataProvider()
|
||||
feats_to_add: List[object] = []
|
||||
conflict_count = 0
|
||||
|
||||
if flst_boundary_union is None or flst_boundary_union.isEmpty():
|
||||
# Kein Flurstück berührt den BROR-Umring → alles ist Abweichung
|
||||
for part in _zerlege_in_teile(bror_boundary):
|
||||
feat = QgsFeature()
|
||||
feat.setGeometry(part)
|
||||
feat.setAttributes([WERT_ABWEICHUNG])
|
||||
feats_to_add.append(feat)
|
||||
conflict_count += 1
|
||||
else:
|
||||
# Flurstücksumringe leicht puffern für Toleranz-Matching
|
||||
if linie_toleranz_m > 0:
|
||||
flst_fuer_diff = flst_boundary_union.buffer(linie_toleranz_m, 4)
|
||||
else:
|
||||
flst_fuer_diff = flst_boundary_union
|
||||
|
||||
# Rote Segmente: BROR-Teile ohne Entsprechung in Flurstücksgrenzen
|
||||
try:
|
||||
red_geom = bror_boundary.difference(flst_fuer_diff)
|
||||
except Exception:
|
||||
red_geom = None
|
||||
|
||||
# Grüne Segmente: BROR-Teile, die nicht rot sind
|
||||
try:
|
||||
if red_geom and not red_geom.isEmpty():
|
||||
green_geom = bror_boundary.difference(red_geom)
|
||||
else:
|
||||
green_geom = bror_boundary
|
||||
except Exception:
|
||||
green_geom = None
|
||||
|
||||
for part in _zerlege_in_teile(green_geom):
|
||||
feat = QgsFeature()
|
||||
feat.setGeometry(part)
|
||||
feat.setAttributes([WERT_IDENTISCH])
|
||||
feats_to_add.append(feat)
|
||||
|
||||
for part in _zerlege_in_teile(red_geom):
|
||||
feat = QgsFeature()
|
||||
feat.setGeometry(part)
|
||||
feat.setAttributes([WERT_ABWEICHUNG])
|
||||
feats_to_add.append(feat)
|
||||
conflict_count += 1
|
||||
|
||||
if _canceled():
|
||||
return None, 0
|
||||
|
||||
pr.addFeatures(feats_to_add)
|
||||
ergebnis_layer.updateExtents()
|
||||
_setze_kategorien_symbologie(ergebnis_layer)
|
||||
|
||||
_schritt("Umringsprüfung abgeschlossen.", 3)
|
||||
return ergebnis_layer, conflict_count
|
||||
@@ -0,0 +1,373 @@
|
||||
"""sn_verfahrensgebiet/functions/umringstatistik_grenzpunkte.py – Umringstatistik Grenzpunkte.
|
||||
|
||||
Extrahiert aus einem vorhandenen ``Grenzpunkte (ALKIS)``-Layer alle Punkte,
|
||||
die auf dem Umring des Verfahrensgebiets liegen, und klassifiziert sie in
|
||||
drei Kategorien:
|
||||
|
||||
1. **FGP** (grün): ``gp_festgestellterGrenzpunkt == "true"``
|
||||
2. **<3cm** (gelb): ``gp_festgestellterGrenzpunkt != "true"``
|
||||
UND ``qualitaetsangaben_ax_dqpunktort_genauigkeitsstufe == 2100``
|
||||
3. **>3cm** (rot): ``gp_festgestellterGrenzpunkt != "true"``
|
||||
UND ``qualitaetsangaben_ax_dqpunktort_genauigkeitsstufe > 2100``
|
||||
4. **unbekannt** (grau): keiner der obigen Fälle
|
||||
|
||||
Der Umring-Test verwendet einen Abstandsschwellwert von :data:`UMRING_TOLERANZ_M`.
|
||||
|
||||
**Rückgabe:** Memory-Layer ``Umringstatistik Grenzpunkte`` oder ``None`` bei Fehler.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from sn_basis.functions import QgsProject
|
||||
from sn_basis.functions.qt_wrapper import QMessageBox
|
||||
from sn_basis.functions.dialog_wrapper import create_progress_dialog
|
||||
from sn_basis.functions.message_wrapper import warning
|
||||
from sn_basis.functions.qgiscore_wrapper import (
|
||||
QgsVectorLayer,
|
||||
QgsFeature,
|
||||
QgsField,
|
||||
QgsGeometry,
|
||||
polygon_zu_linie,
|
||||
)
|
||||
from sn_basis.functions.qt_wrapper import QVariant
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Konstanten
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
#: Anzeigename des Ergebnis-Layers.
|
||||
ERGEBNIS_LAYER_NAME = "Umringstatistik Grenzpunkte"
|
||||
|
||||
#: Feldname für die Klassifikationskategorie im Ergebnis-Layer.
|
||||
FELD_KATEGORIE = "kategorie"
|
||||
|
||||
#: Feldname für ``gp_festgestellterGrenzpunkt`` im Grenzpunkte(ALKIS)-Layer.
|
||||
FELD_FESTGESTELLT = "gp_festgestellterGrenzpunkt"
|
||||
|
||||
#: Feldname für das Genauigkeitsattribut im Grenzpunkte(ALKIS)-Layer.
|
||||
FELD_GENAUIGKEIT = "qualitaetsangaben_ax_dqpunktort_genauigkeitsstufe"
|
||||
|
||||
#: Feldname für die GML-ID im Grenzpunkte(ALKIS)-Layer.
|
||||
FELD_GML_ID = "gml_id"
|
||||
|
||||
#: Kategoriewert: festgestellter Grenzpunkt.
|
||||
KATEGORIE_FGP = "FGP"
|
||||
#: Kategoriewert: Lagegenauigkeit ≤ 3 cm (Gütestufe 2100).
|
||||
KATEGORIE_KLEIN = "<3cm"
|
||||
#: Kategoriewert: Lagegenauigkeit > 3 cm (schlechtere Gütestufe).
|
||||
KATEGORIE_GROSS = ">3cm"
|
||||
#: Kategoriewert: Kategorie konnte nicht bestimmt werden.
|
||||
KATEGORIE_UNBEKANNT = "unbekannt"
|
||||
|
||||
#: Maximaler Abstand (m) eines Punktes vom Umring, um als „auf dem Umring" zu gelten.
|
||||
UMRING_TOLERANZ_M: float = 0.01
|
||||
|
||||
#: Grenzwert für ``qualitaetsangaben_ax_dqpunktort_genauigkeitsstufe`` (3-cm-Schwelle).
|
||||
GENAUIGKEIT_GRENZWERT: int = 2100
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Interne Hilfsfunktionen
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _setze_kategorien_symbologie(layer) -> None:
|
||||
"""Wendet Kategoriesymbologie an: grün / gelb / rot / grau.
|
||||
|
||||
Wird bei fehlendem QGIS-Kontext still ignoriert.
|
||||
"""
|
||||
try:
|
||||
from qgis.core import ( # type: ignore[import]
|
||||
QgsCategorizedSymbolRenderer,
|
||||
QgsRendererCategory,
|
||||
QgsMarkerSymbol,
|
||||
)
|
||||
|
||||
def _marker(color: str, size: str = "2.5") -> object:
|
||||
return QgsMarkerSymbol.createSimple(
|
||||
{"color": color, "size": size, "outline_style": "no"}
|
||||
)
|
||||
|
||||
categories = [
|
||||
QgsRendererCategory(
|
||||
KATEGORIE_FGP,
|
||||
_marker("0,180,0,255"),
|
||||
f"{KATEGORIE_FGP} – festgestellter Grenzpunkt",
|
||||
),
|
||||
QgsRendererCategory(
|
||||
KATEGORIE_KLEIN,
|
||||
_marker("255,220,0,255"),
|
||||
f"{KATEGORIE_KLEIN} – Gütestufe {GENAUIGKEIT_GRENZWERT}",
|
||||
),
|
||||
QgsRendererCategory(
|
||||
KATEGORIE_GROSS,
|
||||
_marker("220,0,0,255"),
|
||||
f"{KATEGORIE_GROSS} – Gütestufe > {GENAUIGKEIT_GRENZWERT}",
|
||||
),
|
||||
QgsRendererCategory(
|
||||
KATEGORIE_UNBEKANNT,
|
||||
_marker("128,128,128,255"),
|
||||
f"{KATEGORIE_UNBEKANNT} – Kategorie nicht bestimmbar",
|
||||
),
|
||||
]
|
||||
renderer = QgsCategorizedSymbolRenderer(FELD_KATEGORIE, categories)
|
||||
layer.setRenderer(renderer)
|
||||
except Exception:
|
||||
pass # Kein QGIS-Kontext (Tests) → Standarddarstellung behalten
|
||||
|
||||
|
||||
def _klassifiziere(feat) -> str:
|
||||
"""Bestimmt die Kategorie eines Grenzpunkts anhand seiner Attribute.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
feat :
|
||||
Feature aus dem Grenzpunkte(ALKIS)-Layer.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
Einer der Kategorienwerte :data:`KATEGORIE_FGP`, :data:`KATEGORIE_KLEIN`,
|
||||
:data:`KATEGORIE_GROSS` oder :data:`KATEGORIE_UNBEKANNT`.
|
||||
"""
|
||||
# festgestellterGrenzpunkt
|
||||
try:
|
||||
festgestellt_raw = feat[FELD_FESTGESTELLT]
|
||||
festgestellt = str(festgestellt_raw).lower().strip() == "true" if festgestellt_raw else False
|
||||
except Exception:
|
||||
festgestellt = False
|
||||
|
||||
if festgestellt:
|
||||
return KATEGORIE_FGP
|
||||
|
||||
# Genauigkeitsstufe auslesen
|
||||
try:
|
||||
gen_raw = feat[FELD_GENAUIGKEIT]
|
||||
genauigkeit = int(str(gen_raw).strip()) if gen_raw else None
|
||||
except (ValueError, TypeError, Exception):
|
||||
genauigkeit = None
|
||||
|
||||
if genauigkeit is None:
|
||||
return KATEGORIE_UNBEKANNT
|
||||
if genauigkeit == GENAUIGKEIT_GRENZWERT:
|
||||
return KATEGORIE_KLEIN
|
||||
if genauigkeit > GENAUIGKEIT_GRENZWERT:
|
||||
return KATEGORIE_GROSS
|
||||
# Genauigkeit < 2100 (besser als 3 cm, aber nicht festgestellt)
|
||||
return KATEGORIE_UNBEKANNT
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Öffentliche API
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def umringstatistik_grenzpunkte(
|
||||
tab_widget,
|
||||
grenzpunkte_layer,
|
||||
vg_layer,
|
||||
) -> Optional[object]:
|
||||
"""Klassifiziert Grenzpunkte auf dem VG-Umring nach Lagegenauigkeit.
|
||||
|
||||
Ablauf:
|
||||
|
||||
1. VG-Geometrie aus ``vg_layer`` berechnen → Umringslinie.
|
||||
2. Alle Features aus ``grenzpunkte_layer`` auf Abstand ≤ :data:`UMRING_TOLERANZ_M`
|
||||
zum Umring prüfen.
|
||||
3. Treffer klassifizieren (FGP / <3cm / >3cm / unbekannt).
|
||||
4. Ergebnis-Memory-Layer aufbauen und Kategoriesymbologie setzen.
|
||||
5. Layer ins Projekt einfügen, Zusammenfassungs-MessageBox anzeigen.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
tab_widget :
|
||||
Eltern-Widget für Dialoge.
|
||||
grenzpunkte_layer :
|
||||
Geladener ``Grenzpunkte (ALKIS)``-Layer mit Attributen ``gp_festgestellterGrenzpunkt``
|
||||
und ``qualitaetsangaben_ax_dqpunktort_genauigkeitsstufe``.
|
||||
vg_layer :
|
||||
Verfahrensgebiet-Layer (Polygon, EPSG:25833).
|
||||
|
||||
Returns
|
||||
-------
|
||||
QgsVectorLayer | None
|
||||
Ergebnis-Memory-Layer oder ``None`` bei Fehler/Abbruch.
|
||||
"""
|
||||
project = QgsProject.instance()
|
||||
prog = create_progress_dialog(5, "Umringstatistik Grenzpunkte", "Schritt 1/5: VG-Umring berechnen …")
|
||||
abgeschlossen = False
|
||||
|
||||
try:
|
||||
prog.set_value(0)
|
||||
if prog.is_canceled():
|
||||
return None
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Schritt 1: VG-Umring berechnen
|
||||
# ------------------------------------------------------------------
|
||||
feats_vg = [f for f in vg_layer.getFeatures() if f.hasGeometry() and not f.geometry().isEmpty()]
|
||||
if not feats_vg:
|
||||
prog.close()
|
||||
warning("Umringstatistik", "Verfahrensgebiet-Layer enthält keine Geometrien.", duration=6)
|
||||
return None
|
||||
|
||||
vg_geom = QgsGeometry.unaryUnion([f.geometry() for f in feats_vg])
|
||||
if vg_geom is None or vg_geom.isEmpty():
|
||||
prog.close()
|
||||
warning("Umringstatistik", "Verfahrensgebiet-Geometrie konnte nicht vereint werden.", duration=6)
|
||||
return None
|
||||
|
||||
umring_geom = polygon_zu_linie(vg_geom)
|
||||
if umring_geom is None or umring_geom.isEmpty():
|
||||
prog.close()
|
||||
warning("Umringstatistik", "Umringslinie konnte aus der VG-Geometrie nicht extrahiert werden.", duration=6)
|
||||
return None
|
||||
|
||||
prog.set_value(1)
|
||||
if prog.is_canceled():
|
||||
return None
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Schritt 2: Grenzpunkte auf dem Umring filtern
|
||||
# ------------------------------------------------------------------
|
||||
prog.set_label("Schritt 2/5: Grenzpunkte auf Umring prüfen …")
|
||||
|
||||
n_gesamt = grenzpunkte_layer.featureCount()
|
||||
treffer: list = []
|
||||
|
||||
for i, feat in enumerate(grenzpunkte_layer.getFeatures()):
|
||||
if i % 200 == 0:
|
||||
prog.set_label(
|
||||
f"Schritt 2/5: Grenzpunkte prüfen … ({i}/{n_gesamt})"
|
||||
)
|
||||
if prog.is_canceled():
|
||||
return None
|
||||
|
||||
if not feat.hasGeometry() or feat.geometry().isEmpty():
|
||||
continue
|
||||
try:
|
||||
dist = feat.geometry().distance(umring_geom)
|
||||
except Exception:
|
||||
continue
|
||||
if dist <= UMRING_TOLERANZ_M:
|
||||
treffer.append(feat)
|
||||
|
||||
prog.set_value(2)
|
||||
if prog.is_canceled():
|
||||
return None
|
||||
|
||||
if not treffer:
|
||||
prog.close()
|
||||
QMessageBox.information(
|
||||
tab_widget,
|
||||
"Umringstatistik Grenzpunkte",
|
||||
"Es wurden keine Grenzpunkte auf dem Umring des Verfahrensgebiets gefunden.\n\n"
|
||||
f"Geprüft: {n_gesamt} Grenzpunkte\n"
|
||||
f"Toleranz: {UMRING_TOLERANZ_M} m",
|
||||
)
|
||||
return None
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Schritt 3: Klassifizieren
|
||||
# ------------------------------------------------------------------
|
||||
prog.set_label(f"Schritt 3/5: {len(treffer)} Treffer klassifizieren …")
|
||||
|
||||
n_fgp = 0
|
||||
n_klein = 0
|
||||
n_gross = 0
|
||||
n_unbekannt = 0
|
||||
|
||||
klassifiziert: list = [] # [(feat, kategorie), ...]
|
||||
for feat in treffer:
|
||||
kat = _klassifiziere(feat)
|
||||
klassifiziert.append((feat, kat))
|
||||
if kat == KATEGORIE_FGP:
|
||||
n_fgp += 1
|
||||
elif kat == KATEGORIE_KLEIN:
|
||||
n_klein += 1
|
||||
elif kat == KATEGORIE_GROSS:
|
||||
n_gross += 1
|
||||
else:
|
||||
n_unbekannt += 1
|
||||
|
||||
prog.set_value(3)
|
||||
if prog.is_canceled():
|
||||
return None
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Schritt 4: Ergebnis-Layer aufbauen
|
||||
# ------------------------------------------------------------------
|
||||
prog.set_label("Schritt 4/5: Ergebnis-Layer aufbauen …")
|
||||
|
||||
crs_authid = vg_layer.crs().authid() or "EPSG:25833"
|
||||
result_layer = QgsVectorLayer(
|
||||
f"Point?crs={crs_authid}", ERGEBNIS_LAYER_NAME, "memory"
|
||||
)
|
||||
pr = result_layer.dataProvider()
|
||||
|
||||
felder = [
|
||||
QgsField(FELD_KATEGORIE, QVariant.String),
|
||||
QgsField(FELD_GML_ID, QVariant.String),
|
||||
QgsField(FELD_FESTGESTELLT, QVariant.String),
|
||||
QgsField(FELD_GENAUIGKEIT, QVariant.String),
|
||||
]
|
||||
pr.addAttributes(felder)
|
||||
result_layer.updateFields()
|
||||
|
||||
neue_features = []
|
||||
for feat, kat in klassifiziert:
|
||||
new_f = QgsFeature(result_layer.fields())
|
||||
new_f.setGeometry(feat.geometry())
|
||||
|
||||
new_f[FELD_KATEGORIE] = kat
|
||||
|
||||
for src_feld in (FELD_GML_ID, FELD_FESTGESTELLT, FELD_GENAUIGKEIT):
|
||||
try:
|
||||
new_f[src_feld] = feat[src_feld]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
neue_features.append(new_f)
|
||||
|
||||
pr.addFeatures(neue_features)
|
||||
result_layer.updateExtents()
|
||||
_setze_kategorien_symbologie(result_layer)
|
||||
|
||||
prog.set_value(4)
|
||||
if prog.is_canceled():
|
||||
return None
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Schritt 5: Layer ins Projekt + Ergebnis-MessageBox
|
||||
# ------------------------------------------------------------------
|
||||
prog.set_label("Schritt 5/5: Layer ins Projekt einfügen …")
|
||||
|
||||
# Bestehenden Ergebnis-Layer ggf. ersetzen
|
||||
vorhandene = project.mapLayersByName(ERGEBNIS_LAYER_NAME)
|
||||
for lyr in vorhandene:
|
||||
project.removeMapLayer(lyr.id())
|
||||
|
||||
project.addMapLayer(result_layer)
|
||||
|
||||
prog.set_value(5)
|
||||
abgeschlossen = True
|
||||
prog.close()
|
||||
|
||||
n_umring = len(treffer)
|
||||
QMessageBox.information(
|
||||
tab_widget,
|
||||
"Umringstatistik Grenzpunkte",
|
||||
f"Grenzpunkte auf dem Umring: {n_umring}\n"
|
||||
f"\n"
|
||||
f" FGP (festgestellt): {n_fgp}\n"
|
||||
f" < 3 cm (Gütestufe {GENAUIGKEIT_GRENZWERT}): {n_klein}\n"
|
||||
f" > 3 cm (schlechtere Güte): {n_gross}\n"
|
||||
f" Unbekannt: {n_unbekannt}\n"
|
||||
f"\n"
|
||||
f"(Geprüft: {n_gesamt} Grenzpunkte, Toleranz: {UMRING_TOLERANZ_M} m)",
|
||||
)
|
||||
|
||||
return result_layer
|
||||
|
||||
finally:
|
||||
if not abgeschlossen:
|
||||
prog.close()
|
||||
@@ -1,4 +1,7 @@
|
||||
#sn_verfahrensgebiet/functions/verfahrensgebiet_alkis
|
||||
"""sn_verfahrensgebiet/functions/verfahrensgebiet_alkis.py – ALKIS-Integration.
|
||||
Lädt Verfahrensgebiete aus dem ALKIS-WFS-Dienst.
|
||||
"""
|
||||
|
||||
from enum import Enum
|
||||
from sn_basis.functions import (
|
||||
QgsVectorLayer, QgsProject, QgsFeature, QgsField, QgsGeometry
|
||||
@@ -6,18 +9,49 @@ 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_verfahrensgebiet.functions.verfahrensgebiet_dialoge import ask_bror_kombinationen, ask_landkreis_objekte
|
||||
from sn_basis.functions.landkreise import ALLE_LANDKREISE
|
||||
|
||||
alkis_NAS_url = "https://geodienste.sachsen.de/aaa/public_alkis/nas/wfs"
|
||||
alkis_sf_url = "https://geodienste.sachsen.de/aaa/public_alkis/sf/wfs"
|
||||
typename = "adv:AX_BauRaumOderBodenordnungsrecht"
|
||||
|
||||
#: Attribute, die aus dem BROR-WFS-Dienst geladen und im Memory-Layer gespeichert werden.
|
||||
BROR_ATTRIBUTE_FELDER = [
|
||||
"artderfestlegung",
|
||||
"ausfuehrendestelle_ax_dienststelle_schluessel_stelle",
|
||||
"adv_name",
|
||||
"bezeichnung",
|
||||
"datumanordnung",
|
||||
"datumbesitzeinweisung",
|
||||
"datumrechtskraft",
|
||||
"datumabgabe",
|
||||
]
|
||||
|
||||
class LoadStatus(Enum):
|
||||
"""Status eines Layer-Ladevorgangs.
|
||||
|
||||
Attributes:
|
||||
NONE: Nichts geladen
|
||||
FIRST: Erstmalig geladen
|
||||
RELOAD: Neu geladen
|
||||
KEEP: Vorhandener Layer behalten
|
||||
"""
|
||||
NONE = "none" # nichts geladen
|
||||
FIRST = "first" # erstmalig geladen
|
||||
RELOAD = "reload" # neu geladen
|
||||
KEEP = "keep" # vorhandener Layer behalten
|
||||
APPEND = "append" # Features an vorhandenen Layer angehängt
|
||||
|
||||
|
||||
def _check_existing_layer(tab_widget):
|
||||
"""Prüft, ob ein Verfahrensgebiet-Layer bereits existiert.
|
||||
|
||||
Args:
|
||||
tab_widget: Parent-Widget für Dialoge
|
||||
|
||||
Returns:
|
||||
Tuple[Optional[QgsVectorLayer], LoadStatus]: Layer und Status
|
||||
"""
|
||||
project = QgsProject.instance()
|
||||
existing_layers = project.mapLayersByName("Verfahrensgebiet")
|
||||
if not existing_layers:
|
||||
@@ -39,53 +73,285 @@ def _check_existing_layer(tab_widget):
|
||||
return existing_layers[0], LoadStatus.KEEP
|
||||
|
||||
|
||||
def verfahrensgebiet_alkis(tab_widget):
|
||||
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, 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
|
||||
# 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.
|
||||
vkz_prefix = verfahrensnummer[:5]
|
||||
wfs_uri_find = (
|
||||
f"url='{alkis_NAS_url}' typename='{typename}' srsname='EPSG:25833' "
|
||||
f"url='{alkis_sf_url}' typename='{typename}' srsname='EPSG:25833' "
|
||||
f"sql=SELECT * FROM AX_BauRaumOderBodenordnungsrecht "
|
||||
f"WHERE bezeichnung LIKE '{verfahrensnummer}%'"
|
||||
f"WHERE bezeichnung LIKE '{vkz_prefix}%'"
|
||||
)
|
||||
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
|
||||
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
|
||||
|
||||
union_geom = QgsGeometry.unaryUnion([f.geometry() for f in features])
|
||||
# Objekte nach (adv_name, bezeichnung) gruppieren
|
||||
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_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_feats = next(iter(kombinationen.values()))
|
||||
else:
|
||||
# Mehrere Kombinationen → Nutzerauswahl, ein oder mehrere Objekte
|
||||
# können gemeinsam ausgewählt und geladen (verschmolzen) werden.
|
||||
key_list = sorted(kombinationen.keys())
|
||||
selected_keys = ask_bror_kombinationen(tab_widget, key_list)
|
||||
if not selected_keys:
|
||||
return None, LoadStatus.NONE, []
|
||||
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():
|
||||
QMessageBox.critical(tab_widget, "Fehler", "Keine Geometrien zum Verschmelzen gefunden.")
|
||||
return None, LoadStatus.NONE
|
||||
return None, LoadStatus.NONE, []
|
||||
|
||||
# Memory-Layer erzeugen
|
||||
crs = temp_layer.crs().authid()
|
||||
memory_layer = QgsVectorLayer(f"Polygon?crs={crs}", "Verfahrensgebiet", "memory")
|
||||
pr = memory_layer.dataProvider()
|
||||
pr.addAttributes([QgsField("VKZ", QVariant.String)])
|
||||
pr.addAttributes([QgsField(attr, QVariant.String) for attr in BROR_ATTRIBUTE_FELDER])
|
||||
memory_layer.updateFields()
|
||||
|
||||
# Attributwerte aus dem ersten Feature der gewählten Kombination übernehmen
|
||||
first_feat = selected_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":
|
||||
# QDate-Objekte als DD.MM.YYYY formatieren
|
||||
attrs.append(val.toString("dd.MM.yyyy"))
|
||||
else:
|
||||
attrs.append(str(val))
|
||||
except Exception:
|
||||
attrs.append("")
|
||||
|
||||
feat_union = QgsFeature()
|
||||
feat_union.setGeometry(union_geom)
|
||||
feat_union.setAttributes([verfahrensnummer])
|
||||
feat_union.setAttributes(attrs)
|
||||
pr.addFeatures([feat_union])
|
||||
memory_layer.updateExtents()
|
||||
|
||||
# Status: FIRST oder RELOAD
|
||||
# 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)
|
||||
|
||||
@@ -0,0 +1,672 @@
|
||||
"""sn_verfahrensgebiet/functions/verfahrensgebiet_alkis_komplett.py – Vollständiger VG-Lade-Workflow.
|
||||
|
||||
Orchestriert den vollständigen Arbeitsablauf zum Laden eines Verfahrensgebiets
|
||||
aus ALKIS mit anschließender Flurstücks-Verschneidung, Flächenvergleich und
|
||||
optionaler Umringsprüfung:
|
||||
|
||||
1. Verfahrensgebiet aus ``AX_BauRaumOderBodenordnungsrecht`` per VKZ laden.
|
||||
2. Gemarkungsschlüssel per Geometrie aus ``AX_Gemarkung`` vorschlagen.
|
||||
3. Flurstücke per Dialog (mit Vorschlag) laden.
|
||||
4. Flurstücke räumlich auf das VG einschränken (``selectByLocation``).
|
||||
5. Aus der Auswahl den enriched Verfahrensgebiet-Memory-Layer erzeugen.
|
||||
6. Flächenvergleich zwischen BROR-Objekt und abgeleitetem Verfahrensgebiet.
|
||||
|
||||
- **Übereinstimmung** (≤ Toleranz): modale OK-Meldung, Ablauf beendet.
|
||||
- **Abweichung**: 3-Optionen-Dialog (Detailprüfung / Als VG laden / Abbrechen).
|
||||
|
||||
7. Bei Wahl „Als VG laden":
|
||||
|
||||
- BROR-Objekt direkt mit VG-Namen ins Projekt übernehmen.
|
||||
|
||||
8. Bei Wahl „Detailprüfung" (knickpunktbasiert):
|
||||
|
||||
- Grenzpunkte (AX_PunktortTA) per BBox laden.
|
||||
- Knickpunkte des BROR mit PunktortTA vergleichen (grün/rot/orange).
|
||||
- Korrigiertes Polygon aus identischen + gefundenen PunktortTA ableiten.
|
||||
- Flurstücke mit korrigiertem Polygon neu selektieren und enriched Layer
|
||||
erzeugen.
|
||||
- Ergebnislayer (Punkte), Verbindungslinien und Polygon ins Projekt einfügen.
|
||||
- Abschlussmeldung mit Zählung.
|
||||
|
||||
Das BROR-Objekt (``AX_BauRaumOderBodenordnungsrecht``) bleibt bei Erfolg als
|
||||
temporärer Layer ``BauRaumOderBodenordnungsrecht (ALKIS)`` im Projekt erhalten.
|
||||
|
||||
**Rückgabe:**
|
||||
``Tuple[enriched_layer, flst_layer, bror_layer, LoadStatus]``
|
||||
|
||||
**Abbruchverhalten:** Jeder Schritt, der :attr:`LoadStatus.NONE` zurückgibt
|
||||
oder kein Ergebnis liefert, bricht den Workflow ab. Der BROR-Layer wird nur
|
||||
bei Fehler/Abbruch entfernt (wenn er in diesem Lauf hinzugefügt wurde).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional, Tuple
|
||||
|
||||
from sn_basis.functions import QgsProject
|
||||
from sn_basis.functions import QMessageBox
|
||||
from sn_basis.functions.message_wrapper import warning
|
||||
from sn_basis.functions.variable_wrapper import get_variable
|
||||
from sn_basis.functions.dialog_wrapper import (
|
||||
create_progress_dialog,
|
||||
show_info_dialog,
|
||||
ask_detailpruefung_oder_vg_laden,
|
||||
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 (
|
||||
_suche_verfahrensgebiet_features,
|
||||
_baue_layer_aus_kombinationen,
|
||||
_suche_landkreis_kombinationen,
|
||||
_baue_layer_aus_landkreis_auswahl,
|
||||
LoadStatus,
|
||||
)
|
||||
from sn_verfahrensgebiet.functions.gemarkung_alkis import (
|
||||
gemarkung_schluessel_und_namen_von_vg_geometrie,
|
||||
)
|
||||
from sn_verfahrensgebiet.functions.flurstueck_alkis import flurstueck_alkis
|
||||
from sn_verfahrensgebiet.functions.verfahrensgebiet_edit_service import (
|
||||
erstelle_verfahrensgebiet_aus_auswahl,
|
||||
VERF_LAYER_NAME,
|
||||
)
|
||||
from sn_verfahrensgebiet.functions.grenzpunkt_alkis import lade_punktort_ta
|
||||
from sn_verfahrensgebiet.functions.knickpunkt_pruefung import (
|
||||
knickpunkt_pruefung,
|
||||
_extrahiere_knickpunkte,
|
||||
erstelle_korrigiertes_polygon,
|
||||
)
|
||||
|
||||
# Feldname für das Verfahrenskennzeichen im enriched Layer
|
||||
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
|
||||
#: lösen keine Konfliktabfrage aus (Gleitkomma-Ungenauigkeiten vermeiden).
|
||||
MINDEST_FLAECHEN_TOLERANZ_M2 = 0.01
|
||||
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Interne Hilfsfunktionen
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _entferne_hilfs_layer(project, layer_name: str) -> None:
|
||||
"""Entfernt alle Layer mit dem angegebenen Namen aus dem Projekt.
|
||||
|
||||
Wird verwendet, um temporäre WFS-Hilfs-Layer nach der Verschneidung
|
||||
aus dem Projekt zu bereinigen.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
project :
|
||||
Aktuelles QGIS-Projekt.
|
||||
layer_name :
|
||||
Anzeigename des zu entfernenden Layers.
|
||||
"""
|
||||
for lyr in project.mapLayersByName(layer_name):
|
||||
project.removeMapLayer(lyr.id())
|
||||
|
||||
|
||||
def _select_by_location(flst_layer, vg_layer) -> int:
|
||||
"""Selektiert Flurstücke, die innerhalb des Verfahrensgebiets liegen.
|
||||
|
||||
Verwendet ``processing.run("native:selectbylocation")`` mit Prädikat 6
|
||||
(within). Flurstücke, die das VG nur berühren, werden ausgeschlossen.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
flst_layer :
|
||||
Flurstücks-Layer (Quelle der Selektion).
|
||||
vg_layer :
|
||||
Verfahrensgebiet-Layer (Schnittmaske).
|
||||
|
||||
Returns
|
||||
-------
|
||||
int
|
||||
Anzahl der selektierten Features; 0 bei Fehler oder leerem Ergebnis.
|
||||
"""
|
||||
try:
|
||||
import processing # type: ignore[import]
|
||||
|
||||
processing.run(
|
||||
"native:selectbylocation",
|
||||
{
|
||||
"INPUT": flst_layer,
|
||||
"PREDICATE": [6], # 6 = within; schließt reines Berühren aus
|
||||
"INTERSECT": vg_layer,
|
||||
"METHOD": 0, # 0 = neue Selektion
|
||||
},
|
||||
)
|
||||
return flst_layer.selectedFeatureCount()
|
||||
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
|
||||
def _fuege_vkz_feld_hinzu(layer, vkz: str) -> None:
|
||||
"""Fügt das Feld ``verfahrensnummer`` zum enriched Layer hinzu und befüllt
|
||||
es mit dem Verfahrenskennzeichen, sofern dieses nicht leer ist.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
layer :
|
||||
Enriched Verfahrensgebiet-Memory-Layer (in-place geändert).
|
||||
vkz :
|
||||
Verfahrenskennzeichen aus der Projektvariable ``verfahrensnummer``.
|
||||
"""
|
||||
if not vkz:
|
||||
return
|
||||
|
||||
try:
|
||||
from sn_basis.functions import QgsField
|
||||
from sn_basis.functions.qt_wrapper import QVariant
|
||||
|
||||
pr = layer.dataProvider()
|
||||
pr.addAttributes([QgsField(FELD_VKZ, QVariant.String)])
|
||||
layer.updateFields()
|
||||
|
||||
idx = layer.fields().indexFromName(FELD_VKZ)
|
||||
if idx < 0:
|
||||
return
|
||||
|
||||
changes = {}
|
||||
for feat in layer.getFeatures():
|
||||
changes[feat.id()] = {idx: vkz}
|
||||
pr.changeAttributeValues(changes)
|
||||
layer.updateFields()
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _lese_flaechen_toleranz() -> float:
|
||||
"""Liest die Flächentoleranz aus der Projektvariable (Default: 0.0 m²).
|
||||
|
||||
Die Projektvariable :data:`FLAECHEN_TOLERANZ_VARIABLE` kann in den
|
||||
Projekteigenschaften gesetzt werden. Fehlt sie oder ist sie ungültig,
|
||||
wird 0.0 zurückgegeben (exakter Vergleich).
|
||||
|
||||
Returns
|
||||
-------
|
||||
float
|
||||
Toleranz in m² (bei EPSG:25833 identisch mit den Flächen-Einheiten).
|
||||
"""
|
||||
try:
|
||||
raw = get_variable(FLAECHEN_TOLERANZ_VARIABLE)
|
||||
if raw is not None and str(raw).strip():
|
||||
return float(str(raw).strip())
|
||||
except Exception:
|
||||
pass
|
||||
return 0.0
|
||||
|
||||
|
||||
def _flaeche_des_layers(layer) -> float:
|
||||
"""Summiert die Flächen aller Features eines Layers (in CRS-Einheiten²).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
layer :
|
||||
Beliebiger Polygon-Layer.
|
||||
|
||||
Returns
|
||||
-------
|
||||
float
|
||||
Summe aller Feature-Flächen; 0.0 wenn keine Features vorhanden.
|
||||
"""
|
||||
total = 0.0
|
||||
for feat in layer.getFeatures():
|
||||
try:
|
||||
geom = feat.geometry()
|
||||
if geom and not geom.isEmpty():
|
||||
total += geom.area()
|
||||
except Exception:
|
||||
pass
|
||||
return total
|
||||
|
||||
|
||||
def _vergleiche_flaechen(
|
||||
bror_layer, vg_layer, toleranz_m2: float = 0.0
|
||||
) -> Tuple[float, float, bool]:
|
||||
"""Vergleicht Flächen von BROR- und Verfahrensgebiet-Layer.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
bror_layer :
|
||||
BauRaumOderBodenordnungsrecht-Layer.
|
||||
vg_layer :
|
||||
Enriched Verfahrensgebiet-Layer.
|
||||
toleranz_m2 :
|
||||
Maximale zulässige Flächendifferenz in m².
|
||||
|
||||
Returns
|
||||
-------
|
||||
Tuple[float, float, bool]
|
||||
``(bror_flaeche, vg_flaeche, stimmt_ueberein)``
|
||||
"""
|
||||
bror_flaeche = _flaeche_des_layers(bror_layer)
|
||||
vg_flaeche = _flaeche_des_layers(vg_layer)
|
||||
diff = abs(bror_flaeche - vg_flaeche)
|
||||
stimmt_ueberein = diff < MINDEST_FLAECHEN_TOLERANZ_M2 or diff <= toleranz_m2
|
||||
return bror_flaeche, vg_flaeche, stimmt_ueberein
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Öffentliche API
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def verfahrensgebiet_alkis_komplett(
|
||||
tab_widget,
|
||||
) -> Tuple[Optional[object], Optional[object], Optional[object], LoadStatus]:
|
||||
"""Vollständiger VG-Lade-Workflow: ALKIS-VG → Gemarkungen →
|
||||
Flurstücke → Verschneidung → Flächenvergleich → opt. Umringsprüfung.
|
||||
|
||||
Zwei Fortschrittsdialoge nacheinander:
|
||||
|
||||
- **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
|
||||
Lauf hinzugefügt wurde.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
tab_widget :
|
||||
Eltern-Widget für alle Dialoge.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Tuple[Optional[enriched_layer], Optional[flst_layer], Optional[bror_layer], LoadStatus]
|
||||
- ``enriched_layer``: Memory-Layer „Verfahrensgebiet".
|
||||
- ``flst_layer``: Flurstücke-Layer (bleibt im Projekt).
|
||||
- ``bror_layer``: BROR-Layer ``BauRaumOderBodenordnungsrecht (ALKIS)``
|
||||
(bleibt bei Erfolg im Projekt).
|
||||
- :class:`LoadStatus`: ``FIRST``, ``RELOAD`` oder ``NONE``.
|
||||
"""
|
||||
project = QgsProject.instance()
|
||||
alkis_vg_layer = None
|
||||
bror_was_added_by_us = False
|
||||
abgeschlossen = False
|
||||
progress = None
|
||||
|
||||
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:
|
||||
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)
|
||||
|
||||
if vg_status in (LoadStatus.FIRST, LoadStatus.RELOAD):
|
||||
project.addMapLayer(alkis_vg_layer)
|
||||
bror_was_added_by_us = True
|
||||
# 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)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 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).
|
||||
# ------------------------------------------------------------------
|
||||
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
|
||||
|
||||
bror_feats = list(alkis_vg_layer.getFeatures())
|
||||
vg_geom = bror_feats[0].geometry() if bror_feats else None
|
||||
|
||||
schluessel_namen = (
|
||||
gemarkung_schluessel_und_namen_von_vg_geometrie(vg_geom)
|
||||
if vg_geom else []
|
||||
)
|
||||
vorgabe_schluessel = [nr for nr, _ in schluessel_namen]
|
||||
vorgabe_namen = [nm for _, nm in schluessel_namen if nm]
|
||||
|
||||
if not vorgabe_schluessel:
|
||||
warning(
|
||||
"VG laden",
|
||||
"Keine Gemarkungen in der BBox gefunden. Bitte Schlüssel manuell eingeben.",
|
||||
duration=5,
|
||||
)
|
||||
|
||||
progress.set_value(1)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Schritt 2: Flurstücke laden (mit Nutzer-Dialog, Vorschlag befüllt)
|
||||
# ------------------------------------------------------------------
|
||||
progress.set_label("Schritt 2/6: Flurstücke aus ALKIS laden …")
|
||||
if progress.is_canceled():
|
||||
return None, None, None, LoadStatus.NONE
|
||||
|
||||
flst_layer, flst_status = flurstueck_alkis(
|
||||
tab_widget,
|
||||
vorgabe_schluessel=vorgabe_schluessel,
|
||||
vorgabe_namen=vorgabe_namen if vorgabe_namen else None,
|
||||
ohne_dialog=True,
|
||||
)
|
||||
|
||||
if flst_status == LoadStatus.NONE or not flst_layer:
|
||||
return None, None, None, LoadStatus.NONE
|
||||
|
||||
progress.set_value(2)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Schritt 3: Räumliche Selektion
|
||||
# ------------------------------------------------------------------
|
||||
progress.set_label("Schritt 3/6: Flurstücke räumlich selektieren …")
|
||||
if progress.is_canceled():
|
||||
return None, None, None, LoadStatus.NONE
|
||||
|
||||
anzahl = _select_by_location(flst_layer, alkis_vg_layer)
|
||||
|
||||
if anzahl == 0:
|
||||
QMessageBox.warning(
|
||||
tab_widget,
|
||||
"Keine Flurstücke im VG",
|
||||
"Es wurden keine Flurstücke gefunden, die das Verfahrensgebiet schneiden.\n"
|
||||
"Bitte Gemarkungsschlüssel und Ausdehnung prüfen.",
|
||||
)
|
||||
return None, flst_layer, None, LoadStatus.NONE
|
||||
|
||||
progress.set_value(3)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Schritt 4: Enriched Memory-Layer aus Auswahl erzeugen
|
||||
# ------------------------------------------------------------------
|
||||
progress.set_label("Schritt 4/6: Verfahrensgebiet-Layer erzeugen …")
|
||||
if progress.is_canceled():
|
||||
return None, None, None, LoadStatus.NONE
|
||||
|
||||
enriched_layer, erfolgreich, fehlgeschlagen = erstelle_verfahrensgebiet_aus_auswahl(
|
||||
flst_layer
|
||||
)
|
||||
|
||||
if not enriched_layer or not enriched_layer.isValid():
|
||||
QMessageBox.critical(
|
||||
tab_widget,
|
||||
"Fehler",
|
||||
"Verfahrensgebiet-Layer konnte nicht erstellt werden.",
|
||||
)
|
||||
return None, flst_layer, None, LoadStatus.NONE
|
||||
|
||||
vkz = get_variable("verfahrensnummer") or ""
|
||||
_fuege_vkz_feld_hinzu(enriched_layer, vkz)
|
||||
|
||||
progress.set_value(4)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Schritt 5: Flächenvergleich als Gate
|
||||
# ------------------------------------------------------------------
|
||||
progress.set_label("Schritt 5/6: Flächenvergleich …")
|
||||
if progress.is_canceled():
|
||||
return None, None, None, LoadStatus.NONE
|
||||
|
||||
toleranz = _lese_flaechen_toleranz()
|
||||
bror_flaeche, vg_flaeche, stimmt_ueberein = _vergleiche_flaechen(
|
||||
alkis_vg_layer, enriched_layer, toleranz
|
||||
)
|
||||
|
||||
progress.set_value(5)
|
||||
|
||||
if stimmt_ueberein:
|
||||
progress.set_value(6)
|
||||
show_info_dialog(
|
||||
"Flächenvergleich",
|
||||
"Verfahrensgebiet mit erwarteter Geometrie erzeugt",
|
||||
parent=tab_widget,
|
||||
)
|
||||
abgeschlossen = True
|
||||
return enriched_layer, flst_layer, alkis_vg_layer, vg_status
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Flächenabweichung → 3-Optionen-Dialog
|
||||
# ------------------------------------------------------------------
|
||||
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"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?",
|
||||
)
|
||||
|
||||
if entscheidung == "abbrechen":
|
||||
abgeschlossen = True
|
||||
return enriched_layer, flst_layer, alkis_vg_layer, vg_status
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Option: Bodenordnungsobjekt direkt als Verfahrensgebiet laden
|
||||
# ------------------------------------------------------------------
|
||||
if entscheidung == "als_vg_laden":
|
||||
# Layer bleibt im Projekt – nur umbenennen. working_tab überspringt
|
||||
# addMapLayer wenn der Layer bereits registriert ist.
|
||||
alkis_vg_layer.setName(VERF_LAYER_NAME)
|
||||
bror_was_added_by_us = False
|
||||
abgeschlossen = True
|
||||
return alkis_vg_layer, None, None, vg_status
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Option: Detailprüfung (knickpunktbasiert)
|
||||
# ------------------------------------------------------------------
|
||||
# Knickpunkte vorab extrahieren, um Schrittanzahl zu bestimmen
|
||||
knickpunkte = _extrahiere_knickpunkte(alkis_vg_layer)
|
||||
n_knick = len(knickpunkte)
|
||||
# Gesamtschritte: 0=Start, 1=PTA-Index, 2…n+1=Knickpunkte, n+2=Orange, n+3=Fertig
|
||||
# + 1 Schritt vorab für Grenzpunkte laden
|
||||
gesamt_schritte = n_knick + 5
|
||||
|
||||
detail_progress = create_progress_dialog(
|
||||
gesamt_schritte,
|
||||
"Detailprüfung (Knickpunkte)",
|
||||
"Schritt 1: Grenzpunkte laden …",
|
||||
)
|
||||
detail_progress.set_value(0)
|
||||
|
||||
# Schritt 1: PunktortTA gemarkungsweise laden (Hits + 5000er-Batches)
|
||||
punktort_layer = lade_punktort_ta(
|
||||
vorgabe_schluessel, vg_geom, progress=detail_progress
|
||||
)
|
||||
|
||||
if detail_progress.is_canceled():
|
||||
detail_progress.close()
|
||||
abgeschlossen = True
|
||||
return enriched_layer, flst_layer, alkis_vg_layer, vg_status
|
||||
|
||||
detail_progress.set_value(1)
|
||||
|
||||
if punktort_layer is None:
|
||||
detail_progress.close()
|
||||
abgeschlossen = True
|
||||
if not detail_progress.is_canceled():
|
||||
QMessageBox.critical(
|
||||
tab_widget,
|
||||
"WFS-Fehler",
|
||||
"Die Grenzpunktdaten (PunktortTA) konnten nicht geladen werden.\n\n"
|
||||
"Mögliche Ursache: Netzwerk- oder Proxy-Fehler.\n"
|
||||
"Bitte prüfen Sie die QGIS-Proxy-Einstellungen "
|
||||
"(Einstellungen -> Optionen -> Netzwerk) und den Log-Eintrag "
|
||||
"unter Ansicht -> Bedienfelder -> Protokollnachrichten "
|
||||
"(Kategorie 'sn_verfahrensgebiet').",
|
||||
)
|
||||
return enriched_layer, flst_layer, alkis_vg_layer, vg_status
|
||||
|
||||
# Schritte 2…Ende: Knickpunktprüfung
|
||||
ergebnis_layer, identisch, kein_pta, fehlender_knick = knickpunkt_pruefung(
|
||||
alkis_vg_layer,
|
||||
punktort_layer,
|
||||
flst_layer,
|
||||
progress=detail_progress,
|
||||
knickpunkte=knickpunkte,
|
||||
)
|
||||
|
||||
if detail_progress.is_canceled():
|
||||
detail_progress.close()
|
||||
abgeschlossen = True
|
||||
return enriched_layer, flst_layer, alkis_vg_layer, vg_status
|
||||
|
||||
detail_progress.set_value(gesamt_schritte)
|
||||
detail_progress.close()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Korrigiertes Polygon aus identischen + fehlenden Knickpunkten
|
||||
# ------------------------------------------------------------------
|
||||
if ergebnis_layer and ergebnis_layer.isValid():
|
||||
linien_layer_korr, polygon_layer_korr = erstelle_korrigiertes_polygon(
|
||||
alkis_vg_layer, ergebnis_layer, alkis_vg_layer.crs().authid()
|
||||
)
|
||||
if polygon_layer_korr is not None and polygon_layer_korr.isValid():
|
||||
anzahl_korr = _select_by_location(flst_layer, polygon_layer_korr)
|
||||
if anzahl_korr > 0:
|
||||
enriched_layer_neu, _, _ = erstelle_verfahrensgebiet_aus_auswahl(
|
||||
flst_layer
|
||||
)
|
||||
if enriched_layer_neu and enriched_layer_neu.isValid():
|
||||
_fuege_vkz_feld_hinzu(enriched_layer_neu, vkz)
|
||||
enriched_layer = enriched_layer_neu
|
||||
if linien_layer_korr is not None and linien_layer_korr.isValid():
|
||||
project.addMapLayer(linien_layer_korr)
|
||||
project.addMapLayer(polygon_layer_korr)
|
||||
|
||||
if ergebnis_layer and ergebnis_layer.isValid():
|
||||
project.addMapLayer(ergebnis_layer)
|
||||
|
||||
show_info_dialog(
|
||||
"Detailprüfung abgeschlossen",
|
||||
f"Knickpunkte geprüft: {n_knick}\n"
|
||||
f" Identisch (grün): {identisch}\n"
|
||||
f" Kein PunktortTA (rot): {kein_pta}\n"
|
||||
f" Fehlende Knickpunkte (orange): {fehlender_knick}",
|
||||
parent=tab_widget,
|
||||
)
|
||||
|
||||
abgeschlossen = True
|
||||
return enriched_layer, flst_layer, alkis_vg_layer, vg_status
|
||||
|
||||
finally:
|
||||
# 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)
|
||||
if not abgeschlossen and bror_was_added_by_us and alkis_vg_layer is not None:
|
||||
try:
|
||||
project.removeMapLayer(alkis_vg_layer.id())
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -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))
|
||||
@@ -0,0 +1,358 @@
|
||||
"""sn_verfahrensgebiet/functions/verfahrensgebiet_edit_service.py
|
||||
|
||||
Service-Modul für Verfahrensgebiet-Editoperationen aus Flurstück-Auswahl.
|
||||
|
||||
Der Verfahrensgebiet-Layer ist ein Single-Feature-Memory-Layer: alle
|
||||
Flurstück-Geometrien werden via ``QgsGeometry.unaryUnion`` zu einem einzigen
|
||||
(Multi-)Polygon verschmolzen und als ein Feature gespeichert. Dadurch
|
||||
erscheint das Verfahrensgebiet im Kartenfenster als zusammenhängendes Objekt.
|
||||
|
||||
Die Flurstückskennzeichen werden als kommagetrennte Zeichenkette im
|
||||
Attributfeld ``flurstueckskennzeichen`` des Single-Features gespeichert,
|
||||
sodass Delta-Operationen (Ergänzen/Entfernen) ohne erneuten Zugriff auf den
|
||||
Quell-Flurstück-Layer möglich bleiben und die ID-Menge via
|
||||
``get_kennzeichen_menge()`` jederzeit abrufbar ist.
|
||||
"""
|
||||
|
||||
from typing import Optional, Tuple, List
|
||||
|
||||
from sn_basis.functions import QgsVectorLayer, QgsFeature, QgsField
|
||||
from sn_basis.functions.qgiscore_wrapper import QgsGeometry
|
||||
from sn_basis.functions.qt_wrapper import QVariant
|
||||
from sn_basis.modules.layerpruefer import Layerpruefer
|
||||
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
|
||||
|
||||
VERF_LAYER_NAME = "Verfahrensgebiet"
|
||||
FELD_KENNZEICHEN = "flurstueckskennzeichen"
|
||||
FELD_GEMARKUNG = "gemarkung_ax_gemarkung_schluessel_gemarkungsnummer"
|
||||
PFLICHTFELDER: List[str] = [FELD_KENNZEICHEN, FELD_GEMARKUNG]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Vorbedingungsprüfungen
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def pruefe_flurstueck_layer(layer) -> pruef_ergebnis:
|
||||
"""Prüft, ob ein Layer als Flurstück-Quelle geeignet ist.
|
||||
|
||||
Prüfreihenfolge via Layerpruefer:
|
||||
existiert → Vektorlayer → Polygon-Geometrie → nicht leer → Pflichtfelder vorhanden.
|
||||
|
||||
Returns
|
||||
-------
|
||||
pruef_ergebnis
|
||||
ok=True, wenn alle Prüfungen bestanden wurden.
|
||||
"""
|
||||
pruefer = Layerpruefer(
|
||||
layer=layer,
|
||||
erwarteter_layertyp="vector",
|
||||
erwarteter_geotyp="Polygon",
|
||||
erforderliche_felder=PFLICHTFELDER,
|
||||
)
|
||||
return pruefer.pruefe()
|
||||
|
||||
|
||||
def pruefe_auswahl(layer) -> pruef_ergebnis:
|
||||
"""Prüft, ob auf dem Layer mindestens ein Feature ausgewählt ist.
|
||||
|
||||
Returns
|
||||
-------
|
||||
pruef_ergebnis
|
||||
ok=True bei mindestens einer Auswahl, sonst ok=False mit aktion ``layer_leer``.
|
||||
"""
|
||||
try:
|
||||
count = int(layer.selectedFeatureCount())
|
||||
except Exception:
|
||||
count = 0
|
||||
|
||||
if count == 0:
|
||||
return pruef_ergebnis(
|
||||
ok=False,
|
||||
meldung="Es sind keine Flurstücke ausgewählt. Bitte zuerst eine Auswahl treffen.",
|
||||
aktion="layer_leer",
|
||||
kontext=None,
|
||||
)
|
||||
return pruef_ergebnis(
|
||||
ok=True,
|
||||
meldung=f"{count} Flurstücke ausgewählt.",
|
||||
aktion="ok",
|
||||
kontext=None,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Hilfsfunktionen
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def get_kennzeichen_menge(layer) -> set:
|
||||
"""Gibt die Menge aller ``flurstueckskennzeichen`` aus dem Layer zurück.
|
||||
|
||||
Im Single-Feature-Modell sind die Kennzeichen als kommagetrennte
|
||||
Zeichenkette im einzigen Feature gespeichert.
|
||||
"""
|
||||
result = set()
|
||||
for feat in layer.getFeatures():
|
||||
val = feat[FELD_KENNZEICHEN]
|
||||
if val:
|
||||
for kz in str(val).split(","):
|
||||
kz = kz.strip()
|
||||
if kz:
|
||||
result.add(kz)
|
||||
return result
|
||||
|
||||
|
||||
def _erstelle_memory_layer(crs_authid: str) -> QgsVectorLayer:
|
||||
"""Erzeugt einen leeren Multi-Feature-Memory-Layer für das Verfahrensgebiet."""
|
||||
layer = QgsVectorLayer(
|
||||
f"MultiPolygon?crs={crs_authid}", VERF_LAYER_NAME, "memory"
|
||||
)
|
||||
pr = layer.dataProvider()
|
||||
pr.addAttributes([
|
||||
QgsField(FELD_KENNZEICHEN, QVariant.String),
|
||||
QgsField(FELD_GEMARKUNG, QVariant.String),
|
||||
])
|
||||
layer.updateFields()
|
||||
return layer
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Hauptoperationen
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def erstelle_verfahrensgebiet_aus_auswahl(
|
||||
source_layer,
|
||||
) -> Tuple[Optional[object], int, int]:
|
||||
"""Erstellt neuen Verfahrensgebiet-Memory-Layer aus der aktuellen Auswahl.
|
||||
|
||||
Alle Geometrien werden via ``QgsGeometry.unaryUnion`` zu einem einzigen
|
||||
(Multi-)Polygon verschmolzen. Der Layer enthält danach genau ein Feature.
|
||||
Die Flurstückskennzeichen werden kommagetrennt im Attribut gespeichert.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
source_layer :
|
||||
Flurstück-Layer mit aktiver Auswahl.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Tuple[layer, erfolgreich, fehlgeschlagen]
|
||||
"""
|
||||
crs = source_layer.crs().authid()
|
||||
mem_layer = _erstelle_memory_layer(crs)
|
||||
pr = mem_layer.dataProvider()
|
||||
|
||||
erfolgreich = 0
|
||||
fehlgeschlagen = 0
|
||||
geom_list = []
|
||||
kz_list = []
|
||||
gemarkung_set = set()
|
||||
|
||||
for src_feat in source_layer.selectedFeatures():
|
||||
try:
|
||||
geom = src_feat.geometry()
|
||||
if not geom or geom.isEmpty():
|
||||
fehlgeschlagen += 1
|
||||
continue
|
||||
geom_list.append(geom)
|
||||
kz = str(src_feat[FELD_KENNZEICHEN] or "")
|
||||
gm = str(src_feat[FELD_GEMARKUNG] or "")
|
||||
if kz:
|
||||
kz_list.append(kz)
|
||||
if gm:
|
||||
gemarkung_set.add(gm)
|
||||
erfolgreich += 1
|
||||
except Exception:
|
||||
fehlgeschlagen += 1
|
||||
|
||||
if geom_list:
|
||||
union_geom = QgsGeometry.unaryUnion(geom_list)
|
||||
if union_geom and not union_geom.isEmpty():
|
||||
feat = QgsFeature()
|
||||
feat.setGeometry(union_geom)
|
||||
feat.setAttributes([
|
||||
",".join(kz_list),
|
||||
",".join(sorted(gemarkung_set)),
|
||||
])
|
||||
pr.addFeatures([feat])
|
||||
mem_layer.updateExtents()
|
||||
|
||||
return mem_layer, erfolgreich, fehlgeschlagen
|
||||
|
||||
|
||||
def ergaenze_verfahrensgebiet(
|
||||
verf_layer,
|
||||
source_layer,
|
||||
) -> Tuple[object, int, int, int]:
|
||||
"""Fügt nur neue (noch nicht enthaltene) Flurstücke zum Verfahrensgebiet hinzu.
|
||||
|
||||
Die Geometrien der neuen Flurstücke werden mit der bestehenden Union-Geometrie
|
||||
verschmolzen. Das Single-Feature wird an Ort und Stelle aktualisiert.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
verf_layer :
|
||||
Bestehender Verfahrensgebiet-Memory-Layer (Single-Feature).
|
||||
source_layer :
|
||||
Flurstück-Layer mit aktiver Auswahl.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Tuple[verf_layer, hinzugefuegt, bereits_vorhanden, fehlgeschlagen]
|
||||
"""
|
||||
vorhandene_ids = get_kennzeichen_menge(verf_layer)
|
||||
pr = verf_layer.dataProvider()
|
||||
|
||||
hinzugefuegt = 0
|
||||
bereits_vorhanden = 0
|
||||
fehlgeschlagen = 0
|
||||
neue_geoms = []
|
||||
neue_kz = []
|
||||
neue_gemarkungen: set = set()
|
||||
|
||||
for src_feat in source_layer.selectedFeatures():
|
||||
try:
|
||||
kz = str(src_feat[FELD_KENNZEICHEN] or "")
|
||||
if kz in vorhandene_ids:
|
||||
bereits_vorhanden += 1
|
||||
continue
|
||||
geom = src_feat.geometry()
|
||||
if not geom or geom.isEmpty():
|
||||
fehlgeschlagen += 1
|
||||
continue
|
||||
neue_geoms.append(geom)
|
||||
if kz:
|
||||
neue_kz.append(kz)
|
||||
gm = str(src_feat[FELD_GEMARKUNG] or "")
|
||||
if gm:
|
||||
neue_gemarkungen.add(gm)
|
||||
hinzugefuegt += 1
|
||||
except Exception:
|
||||
fehlgeschlagen += 1
|
||||
|
||||
if neue_geoms:
|
||||
existing_feats = list(verf_layer.getFeatures())
|
||||
if existing_feats:
|
||||
existing_feat = existing_feats[0]
|
||||
all_geoms = [existing_feat.geometry()] + neue_geoms
|
||||
new_union = QgsGeometry.unaryUnion(all_geoms)
|
||||
alle_kz = sorted(vorhandene_ids | set(neue_kz))
|
||||
existing_gm_str = existing_feat[FELD_GEMARKUNG] or ""
|
||||
alle_gm = sorted(
|
||||
{g.strip() for g in existing_gm_str.split(",") if g.strip()} | neue_gemarkungen
|
||||
)
|
||||
kz_idx = verf_layer.fields().indexFromName(FELD_KENNZEICHEN)
|
||||
gm_idx = verf_layer.fields().indexFromName(FELD_GEMARKUNG)
|
||||
pr.changeAttributeValues({existing_feat.id(): {
|
||||
kz_idx: ",".join(alle_kz),
|
||||
gm_idx: ",".join(alle_gm),
|
||||
}})
|
||||
pr.changeGeometryValues({existing_feat.id(): new_union})
|
||||
else:
|
||||
# Kein vorhandenes Feature – neues anlegen
|
||||
union_geom = QgsGeometry.unaryUnion(neue_geoms)
|
||||
if union_geom and not union_geom.isEmpty():
|
||||
feat = QgsFeature()
|
||||
feat.setGeometry(union_geom)
|
||||
feat.setAttributes([
|
||||
",".join(sorted(neue_kz)),
|
||||
",".join(sorted(neue_gemarkungen)),
|
||||
])
|
||||
pr.addFeatures([feat])
|
||||
verf_layer.updateExtents()
|
||||
verf_layer.triggerRepaint()
|
||||
|
||||
return verf_layer, hinzugefuegt, bereits_vorhanden, fehlgeschlagen
|
||||
|
||||
|
||||
def entferne_aus_verfahrensgebiet(
|
||||
verf_layer,
|
||||
source_layer,
|
||||
) -> Tuple[object, int, int, int]:
|
||||
"""Entfernt Flurstücke aus dem Verfahrensgebiet.
|
||||
|
||||
Die Geometrien der zu entfernenden Flurstücke werden per ``difference()``
|
||||
von der bestehenden Union-Geometrie subtrahiert.
|
||||
Nicht enthaltene Flurstücke werden gezählt, aber ignoriert.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
verf_layer :
|
||||
Bestehender Verfahrensgebiet-Memory-Layer (Single-Feature).
|
||||
source_layer :
|
||||
Flurstück-Layer mit aktiver Auswahl.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Tuple[verf_layer, entfernt, nicht_enthalten, fehlgeschlagen]
|
||||
"""
|
||||
existing_feats = list(verf_layer.getFeatures())
|
||||
if not existing_feats:
|
||||
auswahl_kz = {
|
||||
str(f[FELD_KENNZEICHEN] or "")
|
||||
for f in source_layer.selectedFeatures()
|
||||
if f[FELD_KENNZEICHEN]
|
||||
}
|
||||
return verf_layer, 0, len(auswahl_kz), 0
|
||||
|
||||
existing_feat = existing_feats[0]
|
||||
existing_kz_str = existing_feat[FELD_KENNZEICHEN] or ""
|
||||
vorhandene_ids = {k.strip() for k in existing_kz_str.split(",") if k.strip()}
|
||||
|
||||
auswahl_kz = {
|
||||
str(f[FELD_KENNZEICHEN] or "")
|
||||
for f in source_layer.selectedFeatures()
|
||||
if f[FELD_KENNZEICHEN]
|
||||
}
|
||||
nicht_enthalten = len(auswahl_kz - vorhandene_ids)
|
||||
zu_entfernen_kz = auswahl_kz & vorhandene_ids
|
||||
|
||||
if not zu_entfernen_kz:
|
||||
return verf_layer, 0, nicht_enthalten, 0
|
||||
|
||||
result_geom = existing_feat.geometry()
|
||||
fehlgeschlagen = 0
|
||||
tatsaechlich_entfernt = 0
|
||||
|
||||
for src_feat in source_layer.selectedFeatures():
|
||||
kz = str(src_feat[FELD_KENNZEICHEN] or "")
|
||||
if kz not in zu_entfernen_kz:
|
||||
continue
|
||||
try:
|
||||
geom = src_feat.geometry()
|
||||
if geom and not geom.isEmpty():
|
||||
result_geom = result_geom.difference(geom)
|
||||
tatsaechlich_entfernt += 1
|
||||
except Exception:
|
||||
fehlgeschlagen += 1
|
||||
|
||||
pr = verf_layer.dataProvider()
|
||||
verbleibende_kz = sorted(vorhandene_ids - zu_entfernen_kz)
|
||||
|
||||
if not verbleibende_kz or result_geom is None or result_geom.isEmpty():
|
||||
pr.deleteFeatures([existing_feat.id()])
|
||||
else:
|
||||
kz_idx = verf_layer.fields().indexFromName(FELD_KENNZEICHEN)
|
||||
pr.changeGeometryValues({existing_feat.id(): result_geom})
|
||||
pr.changeAttributeValues({existing_feat.id(): {
|
||||
kz_idx: ",".join(verbleibende_kz),
|
||||
}})
|
||||
|
||||
verf_layer.updateExtents()
|
||||
verf_layer.triggerRepaint()
|
||||
return verf_layer, tatsaechlich_entfernt, nicht_enthalten, fehlgeschlagen
|
||||
|
||||
|
||||
def ersetze_verfahrensgebiet(
|
||||
source_layer,
|
||||
) -> Tuple[Optional[object], int, int]:
|
||||
"""Erstellt einen neuen Verfahrensgebiet-Layer aus der Auswahl (Ersetzen-Semantik).
|
||||
|
||||
Der Aufrufer ist verantwortlich, den alten Layer aus dem Projekt zu entfernen,
|
||||
bevor der neue Layer hinzugefügt wird.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Tuple[layer, erfolgreich, fehlgeschlagen]
|
||||
Entspricht ``erstelle_verfahrensgebiet_aus_auswahl``.
|
||||
"""
|
||||
return erstelle_verfahrensgebiet_aus_auswahl(source_layer)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,7 @@
|
||||
"""sn_verfahrensgebiet/main.py – Einstiegspunkt des sn_verfahrensgebiet Plugins.
|
||||
Orchestriert die Verwaltung von Verfahrensgebiets-Layern und deren Eigenschaften.
|
||||
"""
|
||||
|
||||
from qgis.utils import plugins
|
||||
from sn_basis.ui.dockmanager import DockManager
|
||||
from .ui.dockwidget import DockWidget
|
||||
@@ -5,6 +9,9 @@ from sn_basis.modules.DataGrabber import DataGrabber
|
||||
from sn_basis.modules.Pruefmanager import Pruefmanager
|
||||
|
||||
class Verfahrensgebiet:
|
||||
"""Einstiegspunkt des sn_verfahrensgebiet-Plugins.
|
||||
Verwaltet die DockWidget-Initialisierung und das Verfahrensgebiets-Management.
|
||||
"""
|
||||
def __init__(self, iface):
|
||||
self.iface = iface
|
||||
self.pruefmanager = Pruefmanager(ui_modus="qgis")
|
||||
|
||||
+3
-3
@@ -1,12 +1,12 @@
|
||||
[general]
|
||||
version=26.4.1-unstable
|
||||
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/unstable/
|
||||
|
||||
repository=https://entwicklung.flurneuordnung-sachsen.de/AG_QGIS/Plugin_SN_Verfahrensgebiet/src/branch/testing/
|
||||
QGISMaximumVersion=4.99
|
||||
qgisMinimumVersion=3.40
|
||||
experimental=true
|
||||
deprecated=false
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
<!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">
|
||||
<data_defined_properties>
|
||||
<Option type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</data_defined_properties>
|
||||
<layer class="SimpleFill" enabled="1" id="{c32b7d26-d63f-4a20-b079-a5caa68d53db}" locked="0" pass="0">
|
||||
<Option type="Map">
|
||||
<Option name="border_width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="color" type="QString" value="214,165,49,40,hsv:0.11719444394111633,0.77273213863372803,0.83793395757675171,0.15686275064945221"/>
|
||||
<Option name="joinstyle" type="QString" value="bevel"/>
|
||||
<Option name="offset" type="QString" value="0,0"/>
|
||||
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="offset_unit" type="QString" value="MM"/>
|
||||
<Option name="outline_color" type="QString" value="206,185,47,255,hsv:0.14452777802944183,0.77273213863372803,0.80689710378646851,1"/>
|
||||
<Option name="outline_style" type="QString" value="solid"/>
|
||||
<Option name="outline_width" type="QString" value="0.8"/>
|
||||
<Option name="outline_width_unit" type="QString" value="MM"/>
|
||||
<Option name="style" type="QString" value="solid"/>
|
||||
</Option>
|
||||
<data_defined_properties>
|
||||
<Option type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</data_defined_properties>
|
||||
</layer>
|
||||
</symbol>
|
||||
</symbols>
|
||||
<rotation/>
|
||||
<sizescale/>
|
||||
<data-defined-properties>
|
||||
<Option type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</data-defined-properties>
|
||||
</renderer-v2>
|
||||
<selection mode="Default">
|
||||
<selectionColor invalid="1"/>
|
||||
<selectionSymbol>
|
||||
<symbol alpha="1" clip_to_extent="1" force_rhr="0" frame_rate="10" is_animated="0" name="" type="fill">
|
||||
<data_defined_properties>
|
||||
<Option type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</data_defined_properties>
|
||||
<layer class="SimpleFill" enabled="1" id="{9cbdf1f0-8055-4075-828c-a33555f4068e}" locked="0" pass="0">
|
||||
<Option type="Map">
|
||||
<Option name="border_width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="color" type="QString" value="0,0,255,255,rgb:0,0,1,1"/>
|
||||
<Option name="joinstyle" type="QString" value="bevel"/>
|
||||
<Option name="offset" type="QString" value="0,0"/>
|
||||
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="offset_unit" type="QString" value="MM"/>
|
||||
<Option name="outline_color" type="QString" value="35,35,35,255,rgb:0.1372549,0.1372549,0.1372549,1"/>
|
||||
<Option name="outline_style" type="QString" value="solid"/>
|
||||
<Option name="outline_width" type="QString" value="0.26"/>
|
||||
<Option name="outline_width_unit" type="QString" value="MM"/>
|
||||
<Option name="style" type="QString" value="solid"/>
|
||||
</Option>
|
||||
<data_defined_properties>
|
||||
<Option type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</data_defined_properties>
|
||||
</layer>
|
||||
</symbol>
|
||||
</selectionSymbol>
|
||||
</selection>
|
||||
<blendMode>0</blendMode>
|
||||
<featureBlendMode>0</featureBlendMode>
|
||||
<layerGeometryType>2</layerGeometryType>
|
||||
</qgis>
|
||||
@@ -0,0 +1,349 @@
|
||||
<!DOCTYPE qgis PUBLIC 'http://mrcc.com/qgis.dtd' 'SYSTEM'>
|
||||
<qgis labelsEnabled="1" version="3.40.4-Bratislava" styleCategories="Symbology|Labeling">
|
||||
<renderer-v2 enableorderby="0" type="singleSymbol" referencescale="-1" forceraster="0" symbollevels="0">
|
||||
<symbols>
|
||||
<symbol name="0" type="fill" force_rhr="0" clip_to_extent="1" is_animated="0" frame_rate="10" alpha="1">
|
||||
<data_defined_properties>
|
||||
<Option type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</data_defined_properties>
|
||||
<layer id="{bc287a30-ef97-44d4-ac42-1890985a8e6e}" class="SimpleFill" locked="0" enabled="1" pass="0">
|
||||
<Option type="Map">
|
||||
<Option name="border_width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="color" type="QString" value="229,182,54,255,rgb:0.89803921568627454,0.71372549019607845,0.21176470588235294,1"/>
|
||||
<Option name="joinstyle" type="QString" value="bevel"/>
|
||||
<Option name="offset" type="QString" value="0,0"/>
|
||||
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="offset_unit" type="QString" value="MM"/>
|
||||
<Option name="outline_color" type="QString" value="35,35,35,255,rgb:0.13725490196078433,0.13725490196078433,0.13725490196078433,1"/>
|
||||
<Option name="outline_style" type="QString" value="solid"/>
|
||||
<Option name="outline_width" type="QString" value="0.5"/>
|
||||
<Option name="outline_width_unit" type="QString" value="MM"/>
|
||||
<Option name="style" type="QString" value="no"/>
|
||||
</Option>
|
||||
<data_defined_properties>
|
||||
<Option type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</data_defined_properties>
|
||||
</layer>
|
||||
</symbol>
|
||||
</symbols>
|
||||
<rotation/>
|
||||
<sizescale/>
|
||||
<data-defined-properties>
|
||||
<Option type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</data-defined-properties>
|
||||
</renderer-v2>
|
||||
<selection mode="Default">
|
||||
<selectionColor invalid="1"/>
|
||||
<selectionSymbol>
|
||||
<symbol name="" type="fill" force_rhr="0" clip_to_extent="1" is_animated="0" frame_rate="10" alpha="1">
|
||||
<data_defined_properties>
|
||||
<Option type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</data_defined_properties>
|
||||
<layer id="{819785db-4b31-4c4c-8332-821c09f81921}" class="SimpleFill" locked="0" enabled="1" pass="0">
|
||||
<Option type="Map">
|
||||
<Option name="border_width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="color" type="QString" value="0,0,255,255,rgb:0,0,1,1"/>
|
||||
<Option name="joinstyle" type="QString" value="bevel"/>
|
||||
<Option name="offset" type="QString" value="0,0"/>
|
||||
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="offset_unit" type="QString" value="MM"/>
|
||||
<Option name="outline_color" type="QString" value="35,35,35,255,rgb:0.13725490196078433,0.13725490196078433,0.13725490196078433,1"/>
|
||||
<Option name="outline_style" type="QString" value="solid"/>
|
||||
<Option name="outline_width" type="QString" value="0.26"/>
|
||||
<Option name="outline_width_unit" type="QString" value="MM"/>
|
||||
<Option name="style" type="QString" value="solid"/>
|
||||
</Option>
|
||||
<data_defined_properties>
|
||||
<Option type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</data_defined_properties>
|
||||
</layer>
|
||||
</symbol>
|
||||
</selectionSymbol>
|
||||
</selection>
|
||||
<labeling type="rule-based">
|
||||
<rules key="{15ec465c-d050-4a64-b4ba-262af80bbb43}">
|
||||
<rule key="{b56520a8-9937-40dd-b8fe-c4386271088d}" filter=""flurstuecksnummer_ax_flurstuecksnummer_nenner" IS NULL" description="FlstNr_ohne_Zusatz">
|
||||
<settings calloutType="simple">
|
||||
<text-style forcedItalic="0" fontUnderline="0" previewBkgrdColor="255,255,255,255,rgb:1,1,1,1" fontItalic="0" tabStopDistanceUnit="Point" textOpacity="1" multilineHeight="1" fontFamily="Times New Roman" fontStrikeout="0" capitalization="0" fieldName="flurstuecksnummer_ax_flurstuecksnummer_zaehler" legendString="Aa" fontSizeUnit="Point" fontWeight="50" textOrientation="horizontal" multilineHeightUnit="Percentage" fontKerning="1" tabStopDistanceMapUnitScale="3x:0,0,0,0,0,0" forcedBold="0" fontWordSpacing="0" fontLetterSpacing="0" fontSizeMapUnitScale="3x:0,0,0,0,0,0" allowHtml="0" isExpression="0" useSubstitutions="0" textColor="0,0,0,255,rgb:0,0,0,1" blendMode="0" fontSize="7" namedStyle="Standard" tabStopDistance="80">
|
||||
<families/>
|
||||
<text-buffer bufferBlendMode="0" bufferSizeUnits="MM" bufferOpacity="1" bufferJoinStyle="128" bufferNoFill="1" bufferSizeMapUnitScale="3x:0,0,0,0,0,0" bufferDraw="1" bufferColor="255,255,255,255,rgb:1,1,1,1" bufferSize="1"/>
|
||||
<text-mask maskEnabled="0" maskSizeMapUnitScale="3x:0,0,0,0,0,0" maskSizeUnits="MM" maskJoinStyle="128" maskSize2="1.5" maskSize="1.5" maskOpacity="1" maskType="0" maskedSymbolLayers=""/>
|
||||
<background shapeOffsetX="0" shapeRadiiY="0" shapeSVGFile="" shapeRotation="0" shapeBorderWidth="0" shapeRadiiX="0" shapeSizeX="0" shapeBorderWidthMapUnitScale="3x:0,0,0,0,0,0" shapeSizeUnit="MM" shapeRotationType="0" shapeBorderColor="128,128,128,255,rgb:0.50196078431372548,0.50196078431372548,0.50196078431372548,1" shapeJoinStyle="64" shapeRadiiMapUnitScale="3x:0,0,0,0,0,0" shapeType="0" shapeOffsetMapUnitScale="3x:0,0,0,0,0,0" shapeDraw="0" shapeBorderWidthUnit="MM" shapeFillColor="255,255,255,255,rgb:1,1,1,1" shapeOffsetUnit="MM" shapeRadiiUnit="MM" shapeSizeMapUnitScale="3x:0,0,0,0,0,0" shapeOffsetY="0" shapeBlendMode="0" shapeSizeType="0" shapeSizeY="0" shapeOpacity="1">
|
||||
<symbol name="markerSymbol" type="marker" force_rhr="0" clip_to_extent="1" is_animated="0" frame_rate="10" alpha="1">
|
||||
<data_defined_properties>
|
||||
<Option type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</data_defined_properties>
|
||||
<layer id="" class="SimpleMarker" locked="0" enabled="1" pass="0">
|
||||
<Option type="Map">
|
||||
<Option name="angle" type="QString" value="0"/>
|
||||
<Option name="cap_style" type="QString" value="square"/>
|
||||
<Option name="color" type="QString" value="141,90,153,255,rgb:0.55294117647058827,0.35294117647058826,0.59999999999999998,1"/>
|
||||
<Option name="horizontal_anchor_point" type="QString" value="1"/>
|
||||
<Option name="joinstyle" type="QString" value="bevel"/>
|
||||
<Option name="name" type="QString" value="circle"/>
|
||||
<Option name="offset" type="QString" value="0,0"/>
|
||||
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="offset_unit" type="QString" value="MM"/>
|
||||
<Option name="outline_color" type="QString" value="35,35,35,255,rgb:0.13725490196078433,0.13725490196078433,0.13725490196078433,1"/>
|
||||
<Option name="outline_style" type="QString" value="solid"/>
|
||||
<Option name="outline_width" type="QString" value="0"/>
|
||||
<Option name="outline_width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="outline_width_unit" type="QString" value="MM"/>
|
||||
<Option name="scale_method" type="QString" value="diameter"/>
|
||||
<Option name="size" type="QString" value="2"/>
|
||||
<Option name="size_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="size_unit" type="QString" value="MM"/>
|
||||
<Option name="vertical_anchor_point" type="QString" value="1"/>
|
||||
</Option>
|
||||
<data_defined_properties>
|
||||
<Option type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</data_defined_properties>
|
||||
</layer>
|
||||
</symbol>
|
||||
<symbol name="fillSymbol" type="fill" force_rhr="0" clip_to_extent="1" is_animated="0" frame_rate="10" alpha="1">
|
||||
<data_defined_properties>
|
||||
<Option type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</data_defined_properties>
|
||||
<layer id="" class="SimpleFill" locked="0" enabled="1" pass="0">
|
||||
<Option type="Map">
|
||||
<Option name="border_width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="color" type="QString" value="255,255,255,255,rgb:1,1,1,1"/>
|
||||
<Option name="joinstyle" type="QString" value="bevel"/>
|
||||
<Option name="offset" type="QString" value="0,0"/>
|
||||
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="offset_unit" type="QString" value="MM"/>
|
||||
<Option name="outline_color" type="QString" value="128,128,128,255,rgb:0.50196078431372548,0.50196078431372548,0.50196078431372548,1"/>
|
||||
<Option name="outline_style" type="QString" value="no"/>
|
||||
<Option name="outline_width" type="QString" value="0"/>
|
||||
<Option name="outline_width_unit" type="QString" value="MM"/>
|
||||
<Option name="style" type="QString" value="solid"/>
|
||||
</Option>
|
||||
<data_defined_properties>
|
||||
<Option type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</data_defined_properties>
|
||||
</layer>
|
||||
</symbol>
|
||||
</background>
|
||||
<shadow shadowRadiusAlphaOnly="0" shadowOffsetAngle="135" shadowOpacity="0.69999999999999996" shadowOffsetUnit="MM" shadowColor="0,0,0,255,rgb:0,0,0,1" shadowRadiusMapUnitScale="3x:0,0,0,0,0,0" shadowRadius="1.5" shadowBlendMode="6" shadowScale="100" shadowDraw="0" shadowUnder="0" shadowOffsetDist="1" shadowRadiusUnit="MM" shadowOffsetMapUnitScale="3x:0,0,0,0,0,0" shadowOffsetGlobal="1"/>
|
||||
<dd_properties>
|
||||
<Option type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</dd_properties>
|
||||
<substitutions/>
|
||||
</text-style>
|
||||
<text-format multilineAlign="3" autoWrapLength="0" reverseDirectionSymbol="0" plussign="0" rightDirectionSymbol=">" addDirectionSymbol="0" leftDirectionSymbol="<" formatNumbers="0" decimals="3" placeDirectionSymbol="0" wrapChar="" useMaxLineLengthForAutoWrap="1"/>
|
||||
<placement lineAnchorClipping="0" maximumDistanceMapUnitScale="3x:0,0,0,0,0,0" xOffset="0" maximumDistance="0" geometryGenerator="" distMapUnitScale="3x:0,0,0,0,0,0" predefinedPositionOrder="TR,TL,BR,BL,R,L,TSR,BSR" layerType="PolygonGeometry" lineAnchorPercent="0.5" rotationUnit="AngleDegrees" repeatDistanceMapUnitScale="3x:0,0,0,0,0,0" priority="9" yOffset="0" quadOffset="4" polygonPlacementFlags="2" centroidInside="1" placement="1" placementFlags="10" prioritization="PreferCloser" overrunDistance="0" offsetUnits="MM" maxCurvedCharAngleOut="-25" preserveRotation="1" lineAnchorTextPoint="CenterOfText" overlapHandling="PreventOverlap" rotationAngle="0" repeatDistance="0" offsetType="0" distUnits="MM" allowDegraded="0" overrunDistanceUnit="MM" centroidWhole="0" geometryGeneratorType="PointGeometry" fitInPolygonOnly="0" geometryGeneratorEnabled="0" dist="0" maxCurvedCharAngleIn="25" overrunDistanceMapUnitScale="3x:0,0,0,0,0,0" maximumDistanceUnit="MM" lineAnchorType="0" repeatDistanceUnits="MM" labelOffsetMapUnitScale="3x:0,0,0,0,0,0"/>
|
||||
<rendering fontMinPixelSize="3" fontMaxPixelSize="10000" maxNumLabels="2000" obstacleFactor="1" mergeLines="0" fontLimitPixelSize="0" limitNumLabels="0" minFeatureSize="0" scaleMin="0" scaleVisibility="0" labelPerPart="0" scaleMax="0" obstacleType="1" upsidedownLabels="0" drawLabels="1" zIndex="0" unplacedVisibility="0" obstacle="1"/>
|
||||
<dd_properties>
|
||||
<Option type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</dd_properties>
|
||||
<callout type="simple">
|
||||
<Option type="Map">
|
||||
<Option name="anchorPoint" type="QString" value="centroid"/>
|
||||
<Option name="blendMode" type="int" value="0"/>
|
||||
<Option name="ddProperties" type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
<Option name="drawToAllParts" type="bool" value="false"/>
|
||||
<Option name="enabled" type="QString" value="1"/>
|
||||
<Option name="labelAnchorPoint" type="QString" value="point_on_exterior"/>
|
||||
<Option name="lineSymbol" type="QString" value="<symbol name="symbol" type="line" force_rhr="0" clip_to_extent="1" is_animated="0" frame_rate="10" alpha="1"><data_defined_properties><Option type="Map"><Option name="name" type="QString" value=""/><Option name="properties"/><Option name="type" type="QString" value="collection"/></Option></data_defined_properties><layer id="{3356d22c-5f69-4911-8e91-fcf32e8243fa}" class="SimpleLine" locked="0" enabled="1" pass="0"><Option type="Map"><Option name="align_dash_pattern" type="QString" value="0"/><Option name="capstyle" type="QString" value="square"/><Option name="customdash" type="QString" value="5;2"/><Option name="customdash_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/><Option name="customdash_unit" type="QString" value="MM"/><Option name="dash_pattern_offset" type="QString" value="0"/><Option name="dash_pattern_offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/><Option name="dash_pattern_offset_unit" type="QString" value="MM"/><Option name="draw_inside_polygon" type="QString" value="0"/><Option name="joinstyle" type="QString" value="bevel"/><Option name="line_color" type="QString" value="60,60,60,255,rgb:0.23529411764705882,0.23529411764705882,0.23529411764705882,1"/><Option name="line_style" type="QString" value="solid"/><Option name="line_width" type="QString" value="0.3"/><Option name="line_width_unit" type="QString" value="MM"/><Option name="offset" type="QString" value="0"/><Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/><Option name="offset_unit" type="QString" value="MM"/><Option name="ring_filter" type="QString" value="0"/><Option name="trim_distance_end" type="QString" value="0"/><Option name="trim_distance_end_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/><Option name="trim_distance_end_unit" type="QString" value="MM"/><Option name="trim_distance_start" type="QString" value="0"/><Option name="trim_distance_start_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/><Option name="trim_distance_start_unit" type="QString" value="MM"/><Option name="tweak_dash_pattern_on_corners" type="QString" value="0"/><Option name="use_custom_dash" type="QString" value="0"/><Option name="width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/></Option><data_defined_properties><Option type="Map"><Option name="name" type="QString" value=""/><Option name="properties"/><Option name="type" type="QString" value="collection"/></Option></data_defined_properties></layer></symbol>"/>
|
||||
<Option name="minLength" type="double" value="0"/>
|
||||
<Option name="minLengthMapUnitScale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="minLengthUnit" type="QString" value="MM"/>
|
||||
<Option name="offsetFromAnchor" type="double" value="0"/>
|
||||
<Option name="offsetFromAnchorMapUnitScale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="offsetFromAnchorUnit" type="QString" value="MM"/>
|
||||
<Option name="offsetFromLabel" type="double" value="0"/>
|
||||
<Option name="offsetFromLabelMapUnitScale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="offsetFromLabelUnit" type="QString" value="MM"/>
|
||||
</Option>
|
||||
</callout>
|
||||
</settings>
|
||||
</rule>
|
||||
<rule key="{a9800348-c55c-45d0-95ce-be6db84663c8}" filter=""flurstuecksnummer_ax_flurstuecksnummer_nenner" IS NOT NULL" description="Flst_num_Zusatz">
|
||||
<settings calloutType="simple">
|
||||
<text-style forcedItalic="0" fontUnderline="0" previewBkgrdColor="255,255,255,255,rgb:1,1,1,1" fontItalic="0" tabStopDistanceUnit="Point" textOpacity="1" multilineHeight="1" fontFamily="Times New Roman" fontStrikeout="0" capitalization="0" fieldName=""flurstuecksnummer_ax_flurstuecksnummer_zaehler" || '\n' || "flurstuecksnummer_ax_flurstuecksnummer_nenner" " legendString="Aa" fontSizeUnit="Point" fontWeight="50" textOrientation="horizontal" multilineHeightUnit="Percentage" fontKerning="1" tabStopDistanceMapUnitScale="3x:0,0,0,0,0,0" forcedBold="0" fontWordSpacing="0" fontLetterSpacing="0" fontSizeMapUnitScale="3x:0,0,0,0,0,0" allowHtml="0" isExpression="1" useSubstitutions="0" textColor="0,0,0,255,rgb:0,0,0,1" blendMode="0" fontSize="7" namedStyle="Standard" tabStopDistance="80">
|
||||
<families/>
|
||||
<text-buffer bufferBlendMode="0" bufferSizeUnits="MM" bufferOpacity="1" bufferJoinStyle="128" bufferNoFill="1" bufferSizeMapUnitScale="3x:0,0,0,0,0,0" bufferDraw="1" bufferColor="255,255,255,255,rgb:1,1,1,1" bufferSize="0.5"/>
|
||||
<text-mask maskEnabled="0" maskSizeMapUnitScale="3x:0,0,0,0,0,0" maskSizeUnits="MM" maskJoinStyle="128" maskSize2="1.5" maskSize="1.5" maskOpacity="1" maskType="0" maskedSymbolLayers=""/>
|
||||
<background shapeOffsetX="0" shapeRadiiY="0" shapeSVGFile="" shapeRotation="0" shapeBorderWidth="0" shapeRadiiX="0" shapeSizeX="4" shapeBorderWidthMapUnitScale="3x:0,0,0,0,0,0" shapeSizeUnit="MM" shapeRotationType="0" shapeBorderColor="128,128,128,255,rgb:0.50196078431372548,0.50196078431372548,0.50196078431372548,1" shapeJoinStyle="64" shapeRadiiMapUnitScale="3x:0,0,0,0,0,0" shapeType="5" shapeOffsetMapUnitScale="3x:0,0,0,0,0,0" shapeDraw="1" shapeBorderWidthUnit="MM" shapeFillColor="255,255,255,255,rgb:1,1,1,1" shapeOffsetUnit="MM" shapeRadiiUnit="MM" shapeSizeMapUnitScale="3x:0,0,0,0,0,0" shapeOffsetY="0" shapeBlendMode="0" shapeSizeType="1" shapeSizeY="0" shapeOpacity="1">
|
||||
<symbol name="markerSymbol" type="marker" force_rhr="0" clip_to_extent="1" is_animated="0" frame_rate="10" alpha="1">
|
||||
<data_defined_properties>
|
||||
<Option type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</data_defined_properties>
|
||||
<layer id="" class="SimpleMarker" locked="0" enabled="1" pass="0">
|
||||
<Option type="Map">
|
||||
<Option name="angle" type="QString" value="90"/>
|
||||
<Option name="cap_style" type="QString" value="square"/>
|
||||
<Option name="color" type="QString" value="255,158,23,255,rgb:1,0.61960784313725492,0.09019607843137255,1"/>
|
||||
<Option name="horizontal_anchor_point" type="QString" value="1"/>
|
||||
<Option name="joinstyle" type="QString" value="bevel"/>
|
||||
<Option name="name" type="QString" value="line"/>
|
||||
<Option name="offset" type="QString" value="0,0"/>
|
||||
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="offset_unit" type="QString" value="MM"/>
|
||||
<Option name="outline_color" type="QString" value="35,35,35,255,rgb:0.13725490196078433,0.13725490196078433,0.13725490196078433,1"/>
|
||||
<Option name="outline_style" type="QString" value="solid"/>
|
||||
<Option name="outline_width" type="QString" value="0.3"/>
|
||||
<Option name="outline_width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="outline_width_unit" type="QString" value="MM"/>
|
||||
<Option name="scale_method" type="QString" value="diameter"/>
|
||||
<Option name="size" type="QString" value="4"/>
|
||||
<Option name="size_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="size_unit" type="QString" value="MM"/>
|
||||
<Option name="vertical_anchor_point" type="QString" value="1"/>
|
||||
</Option>
|
||||
<data_defined_properties>
|
||||
<Option type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</data_defined_properties>
|
||||
</layer>
|
||||
</symbol>
|
||||
<symbol name="fillSymbol" type="fill" force_rhr="0" clip_to_extent="1" is_animated="0" frame_rate="10" alpha="1">
|
||||
<data_defined_properties>
|
||||
<Option type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</data_defined_properties>
|
||||
<layer id="" class="SimpleFill" locked="0" enabled="1" pass="0">
|
||||
<Option type="Map">
|
||||
<Option name="border_width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="color" type="QString" value="255,255,255,255,rgb:1,1,1,1"/>
|
||||
<Option name="joinstyle" type="QString" value="bevel"/>
|
||||
<Option name="offset" type="QString" value="0,0"/>
|
||||
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="offset_unit" type="QString" value="MM"/>
|
||||
<Option name="outline_color" type="QString" value="128,128,128,255,rgb:0.50196078431372548,0.50196078431372548,0.50196078431372548,1"/>
|
||||
<Option name="outline_style" type="QString" value="no"/>
|
||||
<Option name="outline_width" type="QString" value="0"/>
|
||||
<Option name="outline_width_unit" type="QString" value="MM"/>
|
||||
<Option name="style" type="QString" value="solid"/>
|
||||
</Option>
|
||||
<data_defined_properties>
|
||||
<Option type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</data_defined_properties>
|
||||
</layer>
|
||||
</symbol>
|
||||
</background>
|
||||
<shadow shadowRadiusAlphaOnly="0" shadowOffsetAngle="135" shadowOpacity="0.69999999999999996" shadowOffsetUnit="MM" shadowColor="0,0,0,255,rgb:0,0,0,1" shadowRadiusMapUnitScale="3x:0,0,0,0,0,0" shadowRadius="1.5" shadowBlendMode="6" shadowScale="100" shadowDraw="0" shadowUnder="0" shadowOffsetDist="1" shadowRadiusUnit="MM" shadowOffsetMapUnitScale="3x:0,0,0,0,0,0" shadowOffsetGlobal="1"/>
|
||||
<dd_properties>
|
||||
<Option type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</dd_properties>
|
||||
<substitutions/>
|
||||
</text-style>
|
||||
<text-format multilineAlign="3" autoWrapLength="0" reverseDirectionSymbol="0" plussign="0" rightDirectionSymbol=">" addDirectionSymbol="0" leftDirectionSymbol="<" formatNumbers="0" decimals="3" placeDirectionSymbol="0" wrapChar="" useMaxLineLengthForAutoWrap="1"/>
|
||||
<placement lineAnchorClipping="0" maximumDistanceMapUnitScale="3x:0,0,0,0,0,0" xOffset="0" maximumDistance="0" geometryGenerator="" distMapUnitScale="3x:0,0,0,0,0,0" predefinedPositionOrder="TR,TL,BR,BL,R,L,TSR,BSR" layerType="PolygonGeometry" lineAnchorPercent="0.5" rotationUnit="AngleDegrees" repeatDistanceMapUnitScale="3x:0,0,0,0,0,0" priority="5" yOffset="0" quadOffset="4" polygonPlacementFlags="2" centroidInside="1" placement="1" placementFlags="10" prioritization="PreferCloser" overrunDistance="0" offsetUnits="MM" maxCurvedCharAngleOut="-25" preserveRotation="1" lineAnchorTextPoint="CenterOfText" overlapHandling="PreventOverlap" rotationAngle="0" repeatDistance="0" offsetType="0" distUnits="MM" allowDegraded="0" overrunDistanceUnit="MM" centroidWhole="0" geometryGeneratorType="PointGeometry" fitInPolygonOnly="0" geometryGeneratorEnabled="0" dist="0" maxCurvedCharAngleIn="25" overrunDistanceMapUnitScale="3x:0,0,0,0,0,0" maximumDistanceUnit="MM" lineAnchorType="0" repeatDistanceUnits="MM" labelOffsetMapUnitScale="3x:0,0,0,0,0,0"/>
|
||||
<rendering fontMinPixelSize="3" fontMaxPixelSize="10000" maxNumLabels="2000" obstacleFactor="1" mergeLines="0" fontLimitPixelSize="0" limitNumLabels="0" minFeatureSize="0" scaleMin="0" scaleVisibility="0" labelPerPart="0" scaleMax="0" obstacleType="1" upsidedownLabels="0" drawLabels="1" zIndex="0" unplacedVisibility="0" obstacle="1"/>
|
||||
<dd_properties>
|
||||
<Option type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties" type="Map">
|
||||
<Option name="FontStyle" type="Map">
|
||||
<Option name="active" type="bool" value="false"/>
|
||||
<Option name="field" type="QString" value="flstnrzae"/>
|
||||
<Option name="type" type="int" value="2"/>
|
||||
</Option>
|
||||
<Option name="Underline" type="Map">
|
||||
<Option name="active" type="bool" value="false"/>
|
||||
<Option name="field" type="QString" value="flstnrzae"/>
|
||||
<Option name="type" type="int" value="2"/>
|
||||
</Option>
|
||||
</Option>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</dd_properties>
|
||||
<callout type="simple">
|
||||
<Option type="Map">
|
||||
<Option name="anchorPoint" type="QString" value="pole_of_inaccessibility"/>
|
||||
<Option name="blendMode" type="int" value="0"/>
|
||||
<Option name="ddProperties" type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
<Option name="drawToAllParts" type="bool" value="false"/>
|
||||
<Option name="enabled" type="QString" value="0"/>
|
||||
<Option name="labelAnchorPoint" type="QString" value="point_on_exterior"/>
|
||||
<Option name="lineSymbol" type="QString" value="<symbol name="symbol" type="line" force_rhr="0" clip_to_extent="1" is_animated="0" frame_rate="10" alpha="1"><data_defined_properties><Option type="Map"><Option name="name" type="QString" value=""/><Option name="properties"/><Option name="type" type="QString" value="collection"/></Option></data_defined_properties><layer id="{0a6da4bc-e8f1-4ec2-8062-844ead072d33}" class="SimpleLine" locked="0" enabled="1" pass="0"><Option type="Map"><Option name="align_dash_pattern" type="QString" value="0"/><Option name="capstyle" type="QString" value="square"/><Option name="customdash" type="QString" value="5;2"/><Option name="customdash_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/><Option name="customdash_unit" type="QString" value="MM"/><Option name="dash_pattern_offset" type="QString" value="0"/><Option name="dash_pattern_offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/><Option name="dash_pattern_offset_unit" type="QString" value="MM"/><Option name="draw_inside_polygon" type="QString" value="0"/><Option name="joinstyle" type="QString" value="bevel"/><Option name="line_color" type="QString" value="60,60,60,255,rgb:0.23529411764705882,0.23529411764705882,0.23529411764705882,1"/><Option name="line_style" type="QString" value="solid"/><Option name="line_width" type="QString" value="0.3"/><Option name="line_width_unit" type="QString" value="MM"/><Option name="offset" type="QString" value="0"/><Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/><Option name="offset_unit" type="QString" value="MM"/><Option name="ring_filter" type="QString" value="0"/><Option name="trim_distance_end" type="QString" value="0"/><Option name="trim_distance_end_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/><Option name="trim_distance_end_unit" type="QString" value="MM"/><Option name="trim_distance_start" type="QString" value="0"/><Option name="trim_distance_start_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/><Option name="trim_distance_start_unit" type="QString" value="MM"/><Option name="tweak_dash_pattern_on_corners" type="QString" value="0"/><Option name="use_custom_dash" type="QString" value="0"/><Option name="width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/></Option><data_defined_properties><Option type="Map"><Option name="name" type="QString" value=""/><Option name="properties"/><Option name="type" type="QString" value="collection"/></Option></data_defined_properties></layer></symbol>"/>
|
||||
<Option name="minLength" type="double" value="0"/>
|
||||
<Option name="minLengthMapUnitScale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="minLengthUnit" type="QString" value="MM"/>
|
||||
<Option name="offsetFromAnchor" type="double" value="0"/>
|
||||
<Option name="offsetFromAnchorMapUnitScale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="offsetFromAnchorUnit" type="QString" value="MM"/>
|
||||
<Option name="offsetFromLabel" type="double" value="0"/>
|
||||
<Option name="offsetFromLabelMapUnitScale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="offsetFromLabelUnit" type="QString" value="MM"/>
|
||||
</Option>
|
||||
</callout>
|
||||
</settings>
|
||||
</rule>
|
||||
</rules>
|
||||
</labeling>
|
||||
<blendMode>0</blendMode>
|
||||
<featureBlendMode>0</featureBlendMode>
|
||||
<layerGeometryType>2</layerGeometryType>
|
||||
</qgis>
|
||||
@@ -0,0 +1,913 @@
|
||||
<!DOCTYPE qgis PUBLIC 'http://mrcc.com/qgis.dtd' 'SYSTEM'>
|
||||
<qgis autoRefreshMode="Disabled" autoRefreshTime="0" hasScaleBasedVisibilityFlag="0" labelsEnabled="0" layerType="Vector" maxScale="0" minScale="100000000" readOnly="0" simplifyAlgorithm="0" simplifyDrawingHints="0" simplifyDrawingTol="1" simplifyLocal="1" simplifyMaxScale="1" styleCategories="AllStyleCategories" symbologyReferenceScale="-1" version="4.2.0-Belém do Pará">
|
||||
<flags>
|
||||
<Identifiable>1</Identifiable>
|
||||
<Removable>1</Removable>
|
||||
<Searchable>1</Searchable>
|
||||
<Private>0</Private>
|
||||
</flags>
|
||||
<temporal accumulate="0" durationField="" durationUnit="min" enabled="0" endExpression="" endField="" fixedDuration="0" limitMode="0" mode="0" startExpression="" startField="">
|
||||
<fixedRange>
|
||||
<start></start>
|
||||
<end></end>
|
||||
</fixedRange>
|
||||
</temporal>
|
||||
<elevation binding="Centroid" clamping="Terrain" customToleranceEnabled="0" extrusion="0" extrusionEnabled="0" respectLayerSymbol="1" showMarkerSymbolInSurfacePlots="0" symbology="Line" type="IndividualFeatures" zoffset="0" zscale="1">
|
||||
<data-defined-properties>
|
||||
<Option type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</data-defined-properties>
|
||||
<profileLineSymbol>
|
||||
<symbol alpha="1" clip_to_extent="1" force_rhr="0" frame_rate="10" is_animated="0" name="" type="line">
|
||||
<data_defined_properties>
|
||||
<Option type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</data_defined_properties>
|
||||
<layer class="SimpleLine" enabled="1" id="{524b77c5-d547-434b-918a-eaa8d67032a3}" locked="0" pass="0">
|
||||
<Option type="Map">
|
||||
<Option name="align_dash_pattern" type="QString" value="0"/>
|
||||
<Option name="capstyle" type="QString" value="square"/>
|
||||
<Option name="customdash" type="QString" value="5;2"/>
|
||||
<Option name="customdash_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="customdash_unit" type="QString" value="MM"/>
|
||||
<Option name="dash_pattern_offset" type="QString" value="0"/>
|
||||
<Option name="dash_pattern_offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="dash_pattern_offset_unit" type="QString" value="MM"/>
|
||||
<Option name="draw_inside_polygon" type="QString" value="0"/>
|
||||
<Option name="joinstyle" type="QString" value="bevel"/>
|
||||
<Option name="line_color" type="QString" value="152,125,183,255,rgb:0.5960785,0.4901961,0.7176471,1"/>
|
||||
<Option name="line_style" type="QString" value="solid"/>
|
||||
<Option name="line_width" type="QString" value="0.6"/>
|
||||
<Option name="line_width_unit" type="QString" value="MM"/>
|
||||
<Option name="offset" type="QString" value="0"/>
|
||||
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="offset_unit" type="QString" value="MM"/>
|
||||
<Option name="ring_filter" type="QString" value="0"/>
|
||||
<Option name="trim_distance_end" type="QString" value="0"/>
|
||||
<Option name="trim_distance_end_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="trim_distance_end_unit" type="QString" value="MM"/>
|
||||
<Option name="trim_distance_start" type="QString" value="0"/>
|
||||
<Option name="trim_distance_start_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="trim_distance_start_unit" type="QString" value="MM"/>
|
||||
<Option name="tweak_dash_pattern_on_corners" type="QString" value="0"/>
|
||||
<Option name="use_custom_dash" type="QString" value="0"/>
|
||||
<Option name="width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
</Option>
|
||||
<data_defined_properties>
|
||||
<Option type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</data_defined_properties>
|
||||
</layer>
|
||||
</symbol>
|
||||
</profileLineSymbol>
|
||||
<profileFillSymbol>
|
||||
<symbol alpha="1" clip_to_extent="1" force_rhr="0" frame_rate="10" is_animated="0" name="" type="fill">
|
||||
<data_defined_properties>
|
||||
<Option type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</data_defined_properties>
|
||||
<layer class="SimpleFill" enabled="1" id="{a950bd48-1020-4bd8-ba91-53a188c9d8c0}" locked="0" pass="0">
|
||||
<Option type="Map">
|
||||
<Option name="border_width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="color" type="QString" value="152,125,183,255,rgb:0.5960785,0.4901961,0.7176471,1"/>
|
||||
<Option name="joinstyle" type="QString" value="bevel"/>
|
||||
<Option name="offset" type="QString" value="0,0"/>
|
||||
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="offset_unit" type="QString" value="MM"/>
|
||||
<Option name="outline_color" type="QString" value="109,89,131,255,rgb:0.4257572,0.3501335,0.5125963,1"/>
|
||||
<Option name="outline_style" type="QString" value="solid"/>
|
||||
<Option name="outline_width" type="QString" value="0.2"/>
|
||||
<Option name="outline_width_unit" type="QString" value="MM"/>
|
||||
<Option name="style" type="QString" value="solid"/>
|
||||
</Option>
|
||||
<data_defined_properties>
|
||||
<Option type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</data_defined_properties>
|
||||
</layer>
|
||||
</symbol>
|
||||
</profileFillSymbol>
|
||||
<profileMarkerSymbol>
|
||||
<symbol alpha="1" clip_to_extent="1" force_rhr="0" frame_rate="10" is_animated="0" name="" type="marker">
|
||||
<data_defined_properties>
|
||||
<Option type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</data_defined_properties>
|
||||
<layer class="SimpleMarker" enabled="1" id="{1432e3c3-aaae-4f1c-8df7-441e77bdd112}" locked="0" pass="0">
|
||||
<Option type="Map">
|
||||
<Option name="angle" type="QString" value="0"/>
|
||||
<Option name="cap_style" type="QString" value="square"/>
|
||||
<Option name="color" type="QString" value="152,125,183,255,rgb:0.5960785,0.4901961,0.7176471,1"/>
|
||||
<Option name="horizontal_anchor_point" type="QString" value="1"/>
|
||||
<Option name="joinstyle" type="QString" value="bevel"/>
|
||||
<Option name="name" type="QString" value="diamond"/>
|
||||
<Option name="offset" type="QString" value="0,0"/>
|
||||
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="offset_unit" type="QString" value="MM"/>
|
||||
<Option name="outline_color" type="QString" value="109,89,131,255,rgb:0.4257572,0.3501335,0.5125963,1"/>
|
||||
<Option name="outline_style" type="QString" value="solid"/>
|
||||
<Option name="outline_width" type="QString" value="0.2"/>
|
||||
<Option name="outline_width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="outline_width_unit" type="QString" value="MM"/>
|
||||
<Option name="scale_method" type="QString" value="diameter"/>
|
||||
<Option name="size" type="QString" value="3"/>
|
||||
<Option name="size_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="size_unit" type="QString" value="MM"/>
|
||||
<Option name="vertical_anchor_point" type="QString" value="1"/>
|
||||
</Option>
|
||||
<data_defined_properties>
|
||||
<Option type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</data_defined_properties>
|
||||
</layer>
|
||||
</symbol>
|
||||
</profileMarkerSymbol>
|
||||
</elevation>
|
||||
<renderer-v2 enableorderby="0" forceraster="0" referencescale="-1" symbollevels="0" type="RuleRenderer">
|
||||
<rules key="{b5ed05de-e0db-4029-a532-cc0c11943124}">
|
||||
<rule filter=" "gp_abmarkung_Marke" = '1000' " key="{ba76030e-eb1e-4a5a-b10c-b8614af46b1a}" label="Abgemarkt" symbol="0">
|
||||
<rule filter=""gp_festgestellterGrenzpunkt" = 'true'" key="{0f910cf3-1e11-45f4-b4a5-5b8b11f4e2f3}" label="FGP" symbol="1"/>
|
||||
</rule>
|
||||
<rule filter=" "gp_abmarkung_Marke" IN ('9500')" key="{f530f073-c1a2-426e-9394-eb137d98b1fd}" label="Nicht abgemarkt" symbol="2">
|
||||
<rule filter=""gp_festgestellterGrenzpunkt" = 'true'" key="{c1a0a382-cbf0-4981-b461-33c42c64701e}" label="FGP" symbol="3"/>
|
||||
</rule>
|
||||
<rule filter=" "gp_abmarkung_Marke" IN ('9600')" key="{f9d78633-b6a7-40df-88b4-d2d5bd51d1ab}" label="zeitweilig ausgesetzt" symbol="4">
|
||||
<rule filter=""gp_festgestellterGrenzpunkt" = 'true'" key="{66a1f910-5127-4e2c-8c55-bb48f290be47}" label="FGP" symbol="5"/>
|
||||
</rule>
|
||||
</rules>
|
||||
<symbols>
|
||||
<symbol alpha="1" clip_to_extent="1" force_rhr="0" frame_rate="10" is_animated="0" name="0" type="marker">
|
||||
<data_defined_properties>
|
||||
<Option type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</data_defined_properties>
|
||||
<layer class="SimpleMarker" enabled="1" id="{4b2b5770-c712-4a17-a7e7-e8d833a5ef55}" locked="0" pass="0">
|
||||
<Option type="Map">
|
||||
<Option name="angle" type="QString" value="0"/>
|
||||
<Option name="cap_style" type="QString" value="square"/>
|
||||
<Option name="color" type="QString" value="255,255,255,255,hsv:0,0,1,1"/>
|
||||
<Option name="horizontal_anchor_point" type="QString" value="1"/>
|
||||
<Option name="joinstyle" type="QString" value="bevel"/>
|
||||
<Option name="name" type="QString" value="circle"/>
|
||||
<Option name="offset" type="QString" value="0,0"/>
|
||||
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="offset_unit" type="QString" value="Point"/>
|
||||
<Option name="outline_color" type="QString" value="35,35,35,255,rgb:0.1372549,0.1372549,0.1372549,1"/>
|
||||
<Option name="outline_style" type="QString" value="solid"/>
|
||||
<Option name="outline_width" type="QString" value="1"/>
|
||||
<Option name="outline_width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="outline_width_unit" type="QString" value="Point"/>
|
||||
<Option name="scale_method" type="QString" value="diameter"/>
|
||||
<Option name="size" type="QString" value="6"/>
|
||||
<Option name="size_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="size_unit" type="QString" value="Point"/>
|
||||
<Option name="vertical_anchor_point" type="QString" value="1"/>
|
||||
</Option>
|
||||
<data_defined_properties>
|
||||
<Option type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</data_defined_properties>
|
||||
</layer>
|
||||
</symbol>
|
||||
<symbol alpha="1" clip_to_extent="1" force_rhr="0" frame_rate="10" is_animated="0" name="1" type="marker">
|
||||
<data_defined_properties>
|
||||
<Option type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</data_defined_properties>
|
||||
<layer class="SimpleMarker" enabled="1" id="{e3e8ca93-babc-4c67-803f-73b091b1dcbf}" locked="0" pass="0">
|
||||
<Option type="Map">
|
||||
<Option name="angle" type="QString" value="0"/>
|
||||
<Option name="cap_style" type="QString" value="square"/>
|
||||
<Option name="color" type="QString" value="255,255,255,0,hsv:0,0,1,0"/>
|
||||
<Option name="horizontal_anchor_point" type="QString" value="1"/>
|
||||
<Option name="joinstyle" type="QString" value="bevel"/>
|
||||
<Option name="name" type="QString" value="circle"/>
|
||||
<Option name="offset" type="QString" value="0,0"/>
|
||||
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="offset_unit" type="QString" value="Point"/>
|
||||
<Option name="outline_color" type="QString" value="227,26,28,255,rgb:0.8901961,0.1019608,0.1098039,1"/>
|
||||
<Option name="outline_style" type="QString" value="solid"/>
|
||||
<Option name="outline_width" type="QString" value="1"/>
|
||||
<Option name="outline_width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="outline_width_unit" type="QString" value="Point"/>
|
||||
<Option name="scale_method" type="QString" value="diameter"/>
|
||||
<Option name="size" type="QString" value="8"/>
|
||||
<Option name="size_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="size_unit" type="QString" value="Point"/>
|
||||
<Option name="vertical_anchor_point" type="QString" value="1"/>
|
||||
</Option>
|
||||
<data_defined_properties>
|
||||
<Option type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</data_defined_properties>
|
||||
</layer>
|
||||
</symbol>
|
||||
<symbol alpha="1" clip_to_extent="1" force_rhr="0" frame_rate="10" is_animated="0" name="2" type="marker">
|
||||
<data_defined_properties>
|
||||
<Option type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</data_defined_properties>
|
||||
<layer class="SimpleMarker" enabled="1" id="{3b684a01-3cd1-4ec2-a3b3-5dd097cac078}" locked="0" pass="0">
|
||||
<Option type="Map">
|
||||
<Option name="angle" type="QString" value="0"/>
|
||||
<Option name="cap_style" type="QString" value="square"/>
|
||||
<Option name="color" type="QString" value="255,255,255,255,hsv:0,0,1,1"/>
|
||||
<Option name="horizontal_anchor_point" type="QString" value="1"/>
|
||||
<Option name="joinstyle" type="QString" value="bevel"/>
|
||||
<Option name="name" type="QString" value="circle"/>
|
||||
<Option name="offset" type="QString" value="0,0"/>
|
||||
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="offset_unit" type="QString" value="MM"/>
|
||||
<Option name="outline_color" type="QString" value="35,35,35,255,rgb:0.1372549,0.1372549,0.1372549,1"/>
|
||||
<Option name="outline_style" type="QString" value="no"/>
|
||||
<Option name="outline_width" type="QString" value="0"/>
|
||||
<Option name="outline_width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="outline_width_unit" type="QString" value="MM"/>
|
||||
<Option name="scale_method" type="QString" value="diameter"/>
|
||||
<Option name="size" type="QString" value="6"/>
|
||||
<Option name="size_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="size_unit" type="QString" value="Point"/>
|
||||
<Option name="vertical_anchor_point" type="QString" value="1"/>
|
||||
</Option>
|
||||
<data_defined_properties>
|
||||
<Option type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</data_defined_properties>
|
||||
</layer>
|
||||
<layer class="SimpleMarker" enabled="1" id="{7cf47a18-2311-467c-835d-8a5ba1dcbbf9}" locked="0" pass="0">
|
||||
<Option type="Map">
|
||||
<Option name="angle" type="QString" value="0"/>
|
||||
<Option name="cap_style" type="QString" value="square"/>
|
||||
<Option name="color" type="QString" value="0,0,0,255,hsv:0.99550002813339233,0.60656136274337769,0,1"/>
|
||||
<Option name="horizontal_anchor_point" type="QString" value="1"/>
|
||||
<Option name="joinstyle" type="QString" value="bevel"/>
|
||||
<Option name="name" type="QString" value="circle"/>
|
||||
<Option name="offset" type="QString" value="0,0"/>
|
||||
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="offset_unit" type="QString" value="MM"/>
|
||||
<Option name="outline_color" type="QString" value="35,35,35,255,rgb:0.1372549,0.1372549,0.1372549,1"/>
|
||||
<Option name="outline_style" type="QString" value="solid"/>
|
||||
<Option name="outline_width" type="QString" value="0"/>
|
||||
<Option name="outline_width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="outline_width_unit" type="QString" value="MM"/>
|
||||
<Option name="scale_method" type="QString" value="diameter"/>
|
||||
<Option name="size" type="QString" value="2"/>
|
||||
<Option name="size_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="size_unit" type="QString" value="Point"/>
|
||||
<Option name="vertical_anchor_point" type="QString" value="1"/>
|
||||
</Option>
|
||||
<data_defined_properties>
|
||||
<Option type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</data_defined_properties>
|
||||
</layer>
|
||||
</symbol>
|
||||
<symbol alpha="1" clip_to_extent="1" force_rhr="0" frame_rate="10" is_animated="0" name="3" type="marker">
|
||||
<data_defined_properties>
|
||||
<Option type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</data_defined_properties>
|
||||
<layer class="SimpleMarker" enabled="1" id="{e3e8ca93-babc-4c67-803f-73b091b1dcbf}" locked="0" pass="0">
|
||||
<Option type="Map">
|
||||
<Option name="angle" type="QString" value="0"/>
|
||||
<Option name="cap_style" type="QString" value="square"/>
|
||||
<Option name="color" type="QString" value="255,255,255,0,hsv:0,0,1,0"/>
|
||||
<Option name="horizontal_anchor_point" type="QString" value="1"/>
|
||||
<Option name="joinstyle" type="QString" value="bevel"/>
|
||||
<Option name="name" type="QString" value="circle"/>
|
||||
<Option name="offset" type="QString" value="0,0"/>
|
||||
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="offset_unit" type="QString" value="Point"/>
|
||||
<Option name="outline_color" type="QString" value="227,26,28,255,rgb:0.8901961,0.1019608,0.1098039,1"/>
|
||||
<Option name="outline_style" type="QString" value="solid"/>
|
||||
<Option name="outline_width" type="QString" value="1"/>
|
||||
<Option name="outline_width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="outline_width_unit" type="QString" value="Point"/>
|
||||
<Option name="scale_method" type="QString" value="diameter"/>
|
||||
<Option name="size" type="QString" value="8"/>
|
||||
<Option name="size_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="size_unit" type="QString" value="Point"/>
|
||||
<Option name="vertical_anchor_point" type="QString" value="1"/>
|
||||
</Option>
|
||||
<data_defined_properties>
|
||||
<Option type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</data_defined_properties>
|
||||
</layer>
|
||||
</symbol>
|
||||
<symbol alpha="1" clip_to_extent="1" force_rhr="0" frame_rate="10" is_animated="0" name="4" type="marker">
|
||||
<data_defined_properties>
|
||||
<Option type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</data_defined_properties>
|
||||
<layer class="SimpleMarker" enabled="1" id="{3b684a01-3cd1-4ec2-a3b3-5dd097cac078}" locked="0" pass="0">
|
||||
<Option type="Map">
|
||||
<Option name="angle" type="QString" value="0"/>
|
||||
<Option name="cap_style" type="QString" value="square"/>
|
||||
<Option name="color" type="QString" value="255,255,255,255,hsv:0,0,1,1"/>
|
||||
<Option name="horizontal_anchor_point" type="QString" value="1"/>
|
||||
<Option name="joinstyle" type="QString" value="bevel"/>
|
||||
<Option name="name" type="QString" value="circle"/>
|
||||
<Option name="offset" type="QString" value="0,0"/>
|
||||
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="offset_unit" type="QString" value="MM"/>
|
||||
<Option name="outline_color" type="QString" value="35,35,35,255,rgb:0.1372549,0.1372549,0.1372549,1"/>
|
||||
<Option name="outline_style" type="QString" value="solid"/>
|
||||
<Option name="outline_width" type="QString" value="1"/>
|
||||
<Option name="outline_width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="outline_width_unit" type="QString" value="MM"/>
|
||||
<Option name="scale_method" type="QString" value="diameter"/>
|
||||
<Option name="size" type="QString" value="6"/>
|
||||
<Option name="size_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="size_unit" type="QString" value="Point"/>
|
||||
<Option name="vertical_anchor_point" type="QString" value="1"/>
|
||||
</Option>
|
||||
<data_defined_properties>
|
||||
<Option type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</data_defined_properties>
|
||||
</layer>
|
||||
<layer class="SimpleMarker" enabled="1" id="{7cf47a18-2311-467c-835d-8a5ba1dcbbf9}" locked="0" pass="0">
|
||||
<Option type="Map">
|
||||
<Option name="angle" type="QString" value="0"/>
|
||||
<Option name="cap_style" type="QString" value="square"/>
|
||||
<Option name="color" type="QString" value="0,0,0,255,hsv:0.99550002813339233,0.60656136274337769,0,1"/>
|
||||
<Option name="horizontal_anchor_point" type="QString" value="1"/>
|
||||
<Option name="joinstyle" type="QString" value="bevel"/>
|
||||
<Option name="name" type="QString" value="circle"/>
|
||||
<Option name="offset" type="QString" value="0,0"/>
|
||||
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="offset_unit" type="QString" value="MM"/>
|
||||
<Option name="outline_color" type="QString" value="35,35,35,255,rgb:0.1372549,0.1372549,0.1372549,1"/>
|
||||
<Option name="outline_style" type="QString" value="solid"/>
|
||||
<Option name="outline_width" type="QString" value="0"/>
|
||||
<Option name="outline_width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="outline_width_unit" type="QString" value="MM"/>
|
||||
<Option name="scale_method" type="QString" value="diameter"/>
|
||||
<Option name="size" type="QString" value="2"/>
|
||||
<Option name="size_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="size_unit" type="QString" value="Point"/>
|
||||
<Option name="vertical_anchor_point" type="QString" value="1"/>
|
||||
</Option>
|
||||
<data_defined_properties>
|
||||
<Option type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</data_defined_properties>
|
||||
</layer>
|
||||
</symbol>
|
||||
<symbol alpha="1" clip_to_extent="1" force_rhr="0" frame_rate="10" is_animated="0" name="5" type="marker">
|
||||
<data_defined_properties>
|
||||
<Option type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</data_defined_properties>
|
||||
<layer class="SimpleMarker" enabled="1" id="{e3e8ca93-babc-4c67-803f-73b091b1dcbf}" locked="0" pass="0">
|
||||
<Option type="Map">
|
||||
<Option name="angle" type="QString" value="0"/>
|
||||
<Option name="cap_style" type="QString" value="square"/>
|
||||
<Option name="color" type="QString" value="255,255,255,0,hsv:0,0,1,0"/>
|
||||
<Option name="horizontal_anchor_point" type="QString" value="1"/>
|
||||
<Option name="joinstyle" type="QString" value="bevel"/>
|
||||
<Option name="name" type="QString" value="circle"/>
|
||||
<Option name="offset" type="QString" value="0,0"/>
|
||||
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="offset_unit" type="QString" value="Point"/>
|
||||
<Option name="outline_color" type="QString" value="227,26,28,255,rgb:0.8901961,0.1019608,0.1098039,1"/>
|
||||
<Option name="outline_style" type="QString" value="solid"/>
|
||||
<Option name="outline_width" type="QString" value="1"/>
|
||||
<Option name="outline_width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="outline_width_unit" type="QString" value="Point"/>
|
||||
<Option name="scale_method" type="QString" value="diameter"/>
|
||||
<Option name="size" type="QString" value="8"/>
|
||||
<Option name="size_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="size_unit" type="QString" value="Point"/>
|
||||
<Option name="vertical_anchor_point" type="QString" value="1"/>
|
||||
</Option>
|
||||
<data_defined_properties>
|
||||
<Option type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</data_defined_properties>
|
||||
</layer>
|
||||
</symbol>
|
||||
</symbols>
|
||||
<data-defined-properties>
|
||||
<Option type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</data-defined-properties>
|
||||
</renderer-v2>
|
||||
<selection mode="Default">
|
||||
<selectionColor invalid="1"/>
|
||||
<selectionSymbol>
|
||||
<symbol alpha="1" clip_to_extent="1" force_rhr="0" frame_rate="10" is_animated="0" name="" type="marker">
|
||||
<data_defined_properties>
|
||||
<Option type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</data_defined_properties>
|
||||
<layer class="SimpleMarker" enabled="1" id="{4e7caead-e027-4f3e-a2ff-5a600d1c387c}" locked="0" pass="0">
|
||||
<Option type="Map">
|
||||
<Option name="angle" type="QString" value="0"/>
|
||||
<Option name="cap_style" type="QString" value="square"/>
|
||||
<Option name="color" type="QString" value="255,0,0,255,rgb:1,0,0,1"/>
|
||||
<Option name="horizontal_anchor_point" type="QString" value="1"/>
|
||||
<Option name="joinstyle" type="QString" value="bevel"/>
|
||||
<Option name="name" type="QString" value="circle"/>
|
||||
<Option name="offset" type="QString" value="0,0"/>
|
||||
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="offset_unit" type="QString" value="MM"/>
|
||||
<Option name="outline_color" type="QString" value="35,35,35,255,rgb:0.1372549,0.1372549,0.1372549,1"/>
|
||||
<Option name="outline_style" type="QString" value="solid"/>
|
||||
<Option name="outline_width" type="QString" value="0"/>
|
||||
<Option name="outline_width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="outline_width_unit" type="QString" value="MM"/>
|
||||
<Option name="scale_method" type="QString" value="diameter"/>
|
||||
<Option name="size" type="QString" value="2"/>
|
||||
<Option name="size_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="size_unit" type="QString" value="MM"/>
|
||||
<Option name="vertical_anchor_point" type="QString" value="1"/>
|
||||
</Option>
|
||||
<data_defined_properties>
|
||||
<Option type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</data_defined_properties>
|
||||
</layer>
|
||||
</symbol>
|
||||
</selectionSymbol>
|
||||
</selection>
|
||||
<customproperties>
|
||||
<Option type="Map">
|
||||
<Option name="dualview/previewExpressions" type="List">
|
||||
<Option type="QString" value="COALESCE( "gml_id", '<NULL>' )"/>
|
||||
</Option>
|
||||
<Option name="embeddedWidgets/count" type="int" value="0"/>
|
||||
<Option name="variableNames"/>
|
||||
<Option name="variableValues"/>
|
||||
</Option>
|
||||
</customproperties>
|
||||
<blendMode>0</blendMode>
|
||||
<featureBlendMode>0</featureBlendMode>
|
||||
<layerOpacity>1</layerOpacity>
|
||||
<geometryOptions geometryPrecision="0" removeDuplicateNodes="0">
|
||||
<activeChecks/>
|
||||
<checkConfiguration/>
|
||||
</geometryOptions>
|
||||
<legend showLabelLegend="0" type="default-vector"/>
|
||||
<referencedLayers/>
|
||||
<referencingLayers/>
|
||||
<fieldConfiguration>
|
||||
<field configurationFlags="0" name="gml_id">
|
||||
<editWidget type="TextEdit">
|
||||
<config>
|
||||
<Option/>
|
||||
</config>
|
||||
</editWidget>
|
||||
</field>
|
||||
<field configurationFlags="0" name="identifier">
|
||||
<editWidget type="TextEdit">
|
||||
<config>
|
||||
<Option/>
|
||||
</config>
|
||||
</editWidget>
|
||||
</field>
|
||||
<field configurationFlags="0" name="lebenszeitintervall">
|
||||
<editWidget type="TextEdit">
|
||||
<config>
|
||||
<Option/>
|
||||
</config>
|
||||
</editWidget>
|
||||
</field>
|
||||
<field configurationFlags="0" name="modellart">
|
||||
<editWidget type="TextEdit">
|
||||
<config>
|
||||
<Option/>
|
||||
</config>
|
||||
</editWidget>
|
||||
</field>
|
||||
<field configurationFlags="0" name="anlass">
|
||||
<editWidget type="TextEdit">
|
||||
<config>
|
||||
<Option/>
|
||||
</config>
|
||||
</editWidget>
|
||||
</field>
|
||||
<field configurationFlags="0" name="zeigtAufExternes">
|
||||
<editWidget type="TextEdit">
|
||||
<config>
|
||||
<Option/>
|
||||
</config>
|
||||
</editWidget>
|
||||
</field>
|
||||
<field configurationFlags="0" name="istTeilVon">
|
||||
<editWidget type="TextEdit">
|
||||
<config>
|
||||
<Option/>
|
||||
</config>
|
||||
</editWidget>
|
||||
</field>
|
||||
<field configurationFlags="0" name="kartendarstellung">
|
||||
<editWidget type="TextEdit">
|
||||
<config>
|
||||
<Option/>
|
||||
</config>
|
||||
</editWidget>
|
||||
</field>
|
||||
<field configurationFlags="0" name="koordinatenstatus">
|
||||
<editWidget type="TextEdit">
|
||||
<config>
|
||||
<Option/>
|
||||
</config>
|
||||
</editWidget>
|
||||
</field>
|
||||
<field configurationFlags="0" name="qualitaetsangaben_ax_dqpunktort_genauigkeitsstufe">
|
||||
<editWidget type="TextEdit">
|
||||
<config>
|
||||
<Option/>
|
||||
</config>
|
||||
</editWidget>
|
||||
</field>
|
||||
<field configurationFlags="0" name="gp_gml_id">
|
||||
<editWidget type="TextEdit">
|
||||
<config>
|
||||
<Option/>
|
||||
</config>
|
||||
</editWidget>
|
||||
</field>
|
||||
<field configurationFlags="0" name="gp_identifier">
|
||||
<editWidget type="TextEdit">
|
||||
<config>
|
||||
<Option/>
|
||||
</config>
|
||||
</editWidget>
|
||||
</field>
|
||||
<field configurationFlags="0" name="gp_lebenszeitintervall">
|
||||
<editWidget type="TextEdit">
|
||||
<config>
|
||||
<Option/>
|
||||
</config>
|
||||
</editWidget>
|
||||
</field>
|
||||
<field configurationFlags="0" name="gp_modellart">
|
||||
<editWidget type="TextEdit">
|
||||
<config>
|
||||
<Option/>
|
||||
</config>
|
||||
</editWidget>
|
||||
</field>
|
||||
<field configurationFlags="0" name="gp_anlass">
|
||||
<editWidget type="TextEdit">
|
||||
<config>
|
||||
<Option/>
|
||||
</config>
|
||||
</editWidget>
|
||||
</field>
|
||||
<field configurationFlags="0" name="gp_zeigtAufExternes">
|
||||
<editWidget type="TextEdit">
|
||||
<config>
|
||||
<Option/>
|
||||
</config>
|
||||
</editWidget>
|
||||
</field>
|
||||
<field configurationFlags="0" name="gp_bestehtAus">
|
||||
<editWidget type="TextEdit">
|
||||
<config>
|
||||
<Option/>
|
||||
</config>
|
||||
</editWidget>
|
||||
</field>
|
||||
<field configurationFlags="0" name="gp_punktkennung">
|
||||
<editWidget type="TextEdit">
|
||||
<config>
|
||||
<Option/>
|
||||
</config>
|
||||
</editWidget>
|
||||
</field>
|
||||
<field configurationFlags="0" name="gp_abmarkung_Marke">
|
||||
<editWidget type="TextEdit">
|
||||
<config>
|
||||
<Option/>
|
||||
</config>
|
||||
</editWidget>
|
||||
</field>
|
||||
<field configurationFlags="0" name="gp_festgestellterGrenzpunkt">
|
||||
<editWidget type="TextEdit">
|
||||
<config>
|
||||
<Option/>
|
||||
</config>
|
||||
</editWidget>
|
||||
</field>
|
||||
<field configurationFlags="0" name="gp_inversZu_zeigtAuf">
|
||||
<editWidget type="TextEdit">
|
||||
<config>
|
||||
<Option/>
|
||||
</config>
|
||||
</editWidget>
|
||||
</field>
|
||||
<field configurationFlags="0" name="gp_sonstigeEigenschaft">
|
||||
<editWidget type="TextEdit">
|
||||
<config>
|
||||
<Option/>
|
||||
</config>
|
||||
</editWidget>
|
||||
</field>
|
||||
</fieldConfiguration>
|
||||
<aliases>
|
||||
<alias field="gml_id" index="0" name=""/>
|
||||
<alias field="identifier" index="1" name=""/>
|
||||
<alias field="lebenszeitintervall" index="2" name=""/>
|
||||
<alias field="modellart" index="3" name=""/>
|
||||
<alias field="anlass" index="4" name=""/>
|
||||
<alias field="zeigtAufExternes" index="5" name=""/>
|
||||
<alias field="istTeilVon" index="6" name=""/>
|
||||
<alias field="kartendarstellung" index="7" name=""/>
|
||||
<alias field="koordinatenstatus" index="8" name=""/>
|
||||
<alias field="qualitaetsangaben_ax_dqpunktort_genauigkeitsstufe" index="9" name=""/>
|
||||
<alias field="gp_gml_id" index="10" name=""/>
|
||||
<alias field="gp_identifier" index="11" name=""/>
|
||||
<alias field="gp_lebenszeitintervall" index="12" name=""/>
|
||||
<alias field="gp_modellart" index="13" name=""/>
|
||||
<alias field="gp_anlass" index="14" name=""/>
|
||||
<alias field="gp_zeigtAufExternes" index="15" name=""/>
|
||||
<alias field="gp_bestehtAus" index="16" name=""/>
|
||||
<alias field="gp_punktkennung" index="17" name=""/>
|
||||
<alias field="gp_abmarkung_Marke" index="18" name=""/>
|
||||
<alias field="gp_festgestellterGrenzpunkt" index="19" name=""/>
|
||||
<alias field="gp_inversZu_zeigtAuf" index="20" name=""/>
|
||||
<alias field="gp_sonstigeEigenschaft" index="21" name=""/>
|
||||
</aliases>
|
||||
<defaults>
|
||||
<default applyOnUpdate="0" expression="" field="gml_id"/>
|
||||
<default applyOnUpdate="0" expression="" field="identifier"/>
|
||||
<default applyOnUpdate="0" expression="" field="lebenszeitintervall"/>
|
||||
<default applyOnUpdate="0" expression="" field="modellart"/>
|
||||
<default applyOnUpdate="0" expression="" field="anlass"/>
|
||||
<default applyOnUpdate="0" expression="" field="zeigtAufExternes"/>
|
||||
<default applyOnUpdate="0" expression="" field="istTeilVon"/>
|
||||
<default applyOnUpdate="0" expression="" field="kartendarstellung"/>
|
||||
<default applyOnUpdate="0" expression="" field="koordinatenstatus"/>
|
||||
<default applyOnUpdate="0" expression="" field="qualitaetsangaben_ax_dqpunktort_genauigkeitsstufe"/>
|
||||
<default applyOnUpdate="0" expression="" field="gp_gml_id"/>
|
||||
<default applyOnUpdate="0" expression="" field="gp_identifier"/>
|
||||
<default applyOnUpdate="0" expression="" field="gp_lebenszeitintervall"/>
|
||||
<default applyOnUpdate="0" expression="" field="gp_modellart"/>
|
||||
<default applyOnUpdate="0" expression="" field="gp_anlass"/>
|
||||
<default applyOnUpdate="0" expression="" field="gp_zeigtAufExternes"/>
|
||||
<default applyOnUpdate="0" expression="" field="gp_bestehtAus"/>
|
||||
<default applyOnUpdate="0" expression="" field="gp_punktkennung"/>
|
||||
<default applyOnUpdate="0" expression="" field="gp_abmarkung_Marke"/>
|
||||
<default applyOnUpdate="0" expression="" field="gp_festgestellterGrenzpunkt"/>
|
||||
<default applyOnUpdate="0" expression="" field="gp_inversZu_zeigtAuf"/>
|
||||
<default applyOnUpdate="0" expression="" field="gp_sonstigeEigenschaft"/>
|
||||
</defaults>
|
||||
<constraints>
|
||||
<constraint constraints="0" exp_strength="0" field="gml_id" notnull_strength="0" unique_strength="0"/>
|
||||
<constraint constraints="0" exp_strength="0" field="identifier" notnull_strength="0" unique_strength="0"/>
|
||||
<constraint constraints="0" exp_strength="0" field="lebenszeitintervall" notnull_strength="0" unique_strength="0"/>
|
||||
<constraint constraints="0" exp_strength="0" field="modellart" notnull_strength="0" unique_strength="0"/>
|
||||
<constraint constraints="0" exp_strength="0" field="anlass" notnull_strength="0" unique_strength="0"/>
|
||||
<constraint constraints="0" exp_strength="0" field="zeigtAufExternes" notnull_strength="0" unique_strength="0"/>
|
||||
<constraint constraints="0" exp_strength="0" field="istTeilVon" notnull_strength="0" unique_strength="0"/>
|
||||
<constraint constraints="0" exp_strength="0" field="kartendarstellung" notnull_strength="0" unique_strength="0"/>
|
||||
<constraint constraints="0" exp_strength="0" field="koordinatenstatus" notnull_strength="0" unique_strength="0"/>
|
||||
<constraint constraints="0" exp_strength="0" field="qualitaetsangaben_ax_dqpunktort_genauigkeitsstufe" notnull_strength="0" unique_strength="0"/>
|
||||
<constraint constraints="0" exp_strength="0" field="gp_gml_id" notnull_strength="0" unique_strength="0"/>
|
||||
<constraint constraints="0" exp_strength="0" field="gp_identifier" notnull_strength="0" unique_strength="0"/>
|
||||
<constraint constraints="0" exp_strength="0" field="gp_lebenszeitintervall" notnull_strength="0" unique_strength="0"/>
|
||||
<constraint constraints="0" exp_strength="0" field="gp_modellart" notnull_strength="0" unique_strength="0"/>
|
||||
<constraint constraints="0" exp_strength="0" field="gp_anlass" notnull_strength="0" unique_strength="0"/>
|
||||
<constraint constraints="0" exp_strength="0" field="gp_zeigtAufExternes" notnull_strength="0" unique_strength="0"/>
|
||||
<constraint constraints="0" exp_strength="0" field="gp_bestehtAus" notnull_strength="0" unique_strength="0"/>
|
||||
<constraint constraints="0" exp_strength="0" field="gp_punktkennung" notnull_strength="0" unique_strength="0"/>
|
||||
<constraint constraints="0" exp_strength="0" field="gp_abmarkung_Marke" notnull_strength="0" unique_strength="0"/>
|
||||
<constraint constraints="0" exp_strength="0" field="gp_festgestellterGrenzpunkt" notnull_strength="0" unique_strength="0"/>
|
||||
<constraint constraints="0" exp_strength="0" field="gp_inversZu_zeigtAuf" notnull_strength="0" unique_strength="0"/>
|
||||
<constraint constraints="0" exp_strength="0" field="gp_sonstigeEigenschaft" notnull_strength="0" unique_strength="0"/>
|
||||
</constraints>
|
||||
<constraintExpressions>
|
||||
<constraint desc="" exp="" field="gml_id"/>
|
||||
<constraint desc="" exp="" field="identifier"/>
|
||||
<constraint desc="" exp="" field="lebenszeitintervall"/>
|
||||
<constraint desc="" exp="" field="modellart"/>
|
||||
<constraint desc="" exp="" field="anlass"/>
|
||||
<constraint desc="" exp="" field="zeigtAufExternes"/>
|
||||
<constraint desc="" exp="" field="istTeilVon"/>
|
||||
<constraint desc="" exp="" field="kartendarstellung"/>
|
||||
<constraint desc="" exp="" field="koordinatenstatus"/>
|
||||
<constraint desc="" exp="" field="qualitaetsangaben_ax_dqpunktort_genauigkeitsstufe"/>
|
||||
<constraint desc="" exp="" field="gp_gml_id"/>
|
||||
<constraint desc="" exp="" field="gp_identifier"/>
|
||||
<constraint desc="" exp="" field="gp_lebenszeitintervall"/>
|
||||
<constraint desc="" exp="" field="gp_modellart"/>
|
||||
<constraint desc="" exp="" field="gp_anlass"/>
|
||||
<constraint desc="" exp="" field="gp_zeigtAufExternes"/>
|
||||
<constraint desc="" exp="" field="gp_bestehtAus"/>
|
||||
<constraint desc="" exp="" field="gp_punktkennung"/>
|
||||
<constraint desc="" exp="" field="gp_abmarkung_Marke"/>
|
||||
<constraint desc="" exp="" field="gp_festgestellterGrenzpunkt"/>
|
||||
<constraint desc="" exp="" field="gp_inversZu_zeigtAuf"/>
|
||||
<constraint desc="" exp="" field="gp_sonstigeEigenschaft"/>
|
||||
</constraintExpressions>
|
||||
<expressionfields/>
|
||||
<attributeactions>
|
||||
<defaultAction key="Canvas" value="{00000000-0000-0000-0000-000000000000}"/>
|
||||
</attributeactions>
|
||||
<attributetableconfig actionWidgetStyle="dropDown" sortExpression="" sortOrder="0">
|
||||
<columns>
|
||||
<column hidden="0" name="gml_id" type="field" width="-1"/>
|
||||
<column hidden="0" name="identifier" type="field" width="-1"/>
|
||||
<column hidden="0" name="lebenszeitintervall" type="field" width="-1"/>
|
||||
<column hidden="0" name="modellart" type="field" width="-1"/>
|
||||
<column hidden="0" name="anlass" type="field" width="-1"/>
|
||||
<column hidden="0" name="zeigtAufExternes" type="field" width="-1"/>
|
||||
<column hidden="0" name="istTeilVon" type="field" width="-1"/>
|
||||
<column hidden="0" name="kartendarstellung" type="field" width="-1"/>
|
||||
<column hidden="0" name="koordinatenstatus" type="field" width="-1"/>
|
||||
<column hidden="0" name="qualitaetsangaben_ax_dqpunktort_genauigkeitsstufe" type="field" width="-1"/>
|
||||
<column hidden="0" name="gp_gml_id" type="field" width="-1"/>
|
||||
<column hidden="0" name="gp_identifier" type="field" width="-1"/>
|
||||
<column hidden="0" name="gp_lebenszeitintervall" type="field" width="-1"/>
|
||||
<column hidden="0" name="gp_modellart" type="field" width="-1"/>
|
||||
<column hidden="0" name="gp_anlass" type="field" width="-1"/>
|
||||
<column hidden="0" name="gp_zeigtAufExternes" type="field" width="-1"/>
|
||||
<column hidden="0" name="gp_bestehtAus" type="field" width="-1"/>
|
||||
<column hidden="0" name="gp_punktkennung" type="field" width="-1"/>
|
||||
<column hidden="0" name="gp_abmarkung_Marke" type="field" width="-1"/>
|
||||
<column hidden="0" name="gp_festgestellterGrenzpunkt" type="field" width="-1"/>
|
||||
<column hidden="0" name="gp_inversZu_zeigtAuf" type="field" width="-1"/>
|
||||
<column hidden="0" name="gp_sonstigeEigenschaft" type="field" width="-1"/>
|
||||
<column hidden="1" type="actions" width="-1"/>
|
||||
</columns>
|
||||
</attributetableconfig>
|
||||
<conditionalstyles>
|
||||
<rowstyles/>
|
||||
<fieldstyles/>
|
||||
</conditionalstyles>
|
||||
<storedexpressions/>
|
||||
<editform tolerant="1"></editform>
|
||||
<editforminit/>
|
||||
<editforminitcodesource>0</editforminitcodesource>
|
||||
<editforminitfilepath></editforminitfilepath>
|
||||
<editforminitcode><![CDATA[# -*- coding: utf-8 -*-
|
||||
"""
|
||||
QGIS-Formulare können eine Python-Funktion haben,, die aufgerufen wird, wenn sich das Formular öffnet
|
||||
|
||||
Diese Funktion kann verwendet werden um dem Formular Extralogik hinzuzufügen.
|
||||
|
||||
Der Name der Funktion wird im Feld "Python Init-Function" angegeben
|
||||
Ein Beispiel folgt:
|
||||
"""
|
||||
from qgis.PyQt.QtWidgets import QWidget
|
||||
|
||||
def my_form_open(dialog, layer, feature):
|
||||
geom = feature.geometry()
|
||||
control = dialog.findChild(QWidget, "MyLineEdit")
|
||||
]]></editforminitcode>
|
||||
<featformsuppress>0</featformsuppress>
|
||||
<editorlayout>generatedlayout</editorlayout>
|
||||
<editable>
|
||||
<field editable="1" name="anlass"/>
|
||||
<field editable="1" name="gml_id"/>
|
||||
<field editable="1" name="gp_abmarkung_Marke"/>
|
||||
<field editable="1" name="gp_anlass"/>
|
||||
<field editable="1" name="gp_bestehtAus"/>
|
||||
<field editable="1" name="gp_festgestellterGrenzpunkt"/>
|
||||
<field editable="1" name="gp_gml_id"/>
|
||||
<field editable="1" name="gp_identifier"/>
|
||||
<field editable="1" name="gp_inversZu_zeigtAuf"/>
|
||||
<field editable="1" name="gp_lebenszeitintervall"/>
|
||||
<field editable="1" name="gp_modellart"/>
|
||||
<field editable="1" name="gp_punktkennung"/>
|
||||
<field editable="1" name="gp_sonstigeEigenschaft"/>
|
||||
<field editable="1" name="gp_zeigtAufExternes"/>
|
||||
<field editable="1" name="identifier"/>
|
||||
<field editable="1" name="istTeilVon"/>
|
||||
<field editable="1" name="kartendarstellung"/>
|
||||
<field editable="1" name="koordinatenstatus"/>
|
||||
<field editable="1" name="lebenszeitintervall"/>
|
||||
<field editable="1" name="modellart"/>
|
||||
<field editable="1" name="qualitaetsangaben_ax_dqpunktort_genauigkeitsstufe"/>
|
||||
<field editable="1" name="zeigtAufExternes"/>
|
||||
</editable>
|
||||
<labelOnTop>
|
||||
<field labelOnTop="0" name="anlass"/>
|
||||
<field labelOnTop="0" name="gml_id"/>
|
||||
<field labelOnTop="0" name="gp_abmarkung_Marke"/>
|
||||
<field labelOnTop="0" name="gp_anlass"/>
|
||||
<field labelOnTop="0" name="gp_bestehtAus"/>
|
||||
<field labelOnTop="0" name="gp_festgestellterGrenzpunkt"/>
|
||||
<field labelOnTop="0" name="gp_gml_id"/>
|
||||
<field labelOnTop="0" name="gp_identifier"/>
|
||||
<field labelOnTop="0" name="gp_inversZu_zeigtAuf"/>
|
||||
<field labelOnTop="0" name="gp_lebenszeitintervall"/>
|
||||
<field labelOnTop="0" name="gp_modellart"/>
|
||||
<field labelOnTop="0" name="gp_punktkennung"/>
|
||||
<field labelOnTop="0" name="gp_sonstigeEigenschaft"/>
|
||||
<field labelOnTop="0" name="gp_zeigtAufExternes"/>
|
||||
<field labelOnTop="0" name="identifier"/>
|
||||
<field labelOnTop="0" name="istTeilVon"/>
|
||||
<field labelOnTop="0" name="kartendarstellung"/>
|
||||
<field labelOnTop="0" name="koordinatenstatus"/>
|
||||
<field labelOnTop="0" name="lebenszeitintervall"/>
|
||||
<field labelOnTop="0" name="modellart"/>
|
||||
<field labelOnTop="0" name="qualitaetsangaben_ax_dqpunktort_genauigkeitsstufe"/>
|
||||
<field labelOnTop="0" name="zeigtAufExternes"/>
|
||||
</labelOnTop>
|
||||
<reuseLastValuePolicy>
|
||||
<field name="anlass" reuseLastValuePolicy="NotAllowed"/>
|
||||
<field name="gml_id" reuseLastValuePolicy="NotAllowed"/>
|
||||
<field name="gp_abmarkung_Marke" reuseLastValuePolicy="NotAllowed"/>
|
||||
<field name="gp_anlass" reuseLastValuePolicy="NotAllowed"/>
|
||||
<field name="gp_bestehtAus" reuseLastValuePolicy="NotAllowed"/>
|
||||
<field name="gp_festgestellterGrenzpunkt" reuseLastValuePolicy="NotAllowed"/>
|
||||
<field name="gp_gml_id" reuseLastValuePolicy="NotAllowed"/>
|
||||
<field name="gp_identifier" reuseLastValuePolicy="NotAllowed"/>
|
||||
<field name="gp_inversZu_zeigtAuf" reuseLastValuePolicy="NotAllowed"/>
|
||||
<field name="gp_lebenszeitintervall" reuseLastValuePolicy="NotAllowed"/>
|
||||
<field name="gp_modellart" reuseLastValuePolicy="NotAllowed"/>
|
||||
<field name="gp_punktkennung" reuseLastValuePolicy="NotAllowed"/>
|
||||
<field name="gp_sonstigeEigenschaft" reuseLastValuePolicy="NotAllowed"/>
|
||||
<field name="gp_zeigtAufExternes" reuseLastValuePolicy="NotAllowed"/>
|
||||
<field name="identifier" reuseLastValuePolicy="NotAllowed"/>
|
||||
<field name="istTeilVon" reuseLastValuePolicy="NotAllowed"/>
|
||||
<field name="kartendarstellung" reuseLastValuePolicy="NotAllowed"/>
|
||||
<field name="koordinatenstatus" reuseLastValuePolicy="NotAllowed"/>
|
||||
<field name="lebenszeitintervall" reuseLastValuePolicy="NotAllowed"/>
|
||||
<field name="modellart" reuseLastValuePolicy="NotAllowed"/>
|
||||
<field name="qualitaetsangaben_ax_dqpunktort_genauigkeitsstufe" reuseLastValuePolicy="NotAllowed"/>
|
||||
<field name="zeigtAufExternes" reuseLastValuePolicy="NotAllowed"/>
|
||||
</reuseLastValuePolicy>
|
||||
<dataDefinedFieldProperties/>
|
||||
<widgets/>
|
||||
<previewExpression>COALESCE( "gml_id", '<NULL>' )</previewExpression>
|
||||
<mapTip enabled="1"></mapTip>
|
||||
<layerGeometryType>0</layerGeometryType>
|
||||
</qgis>
|
||||
@@ -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>
|
||||
File diff suppressed because it is too large
Load Diff
+112
-103
@@ -1,133 +1,142 @@
|
||||
<!DOCTYPE qgis PUBLIC 'http://mrcc.com/qgis.dtd' 'SYSTEM'>
|
||||
<qgis version="3.40.7-Bratislava" styleCategories="Symbology">
|
||||
<renderer-v2 referencescale="-1" forceraster="0" enableorderby="0" type="singleSymbol" symbollevels="0">
|
||||
<symbols>
|
||||
<symbol is_animated="0" frame_rate="10" clip_to_extent="1" type="fill" alpha="1" force_rhr="0" name="0">
|
||||
<data_defined_properties>
|
||||
<Option type="Map">
|
||||
<Option type="QString" value="" name="name"/>
|
||||
<Option name="properties"/>
|
||||
<Option type="QString" value="collection" name="type"/>
|
||||
</Option>
|
||||
</data_defined_properties>
|
||||
<layer locked="0" id="{feca00b2-500a-4c9a-b285-67ba2d99d8f6}" enabled="1" class="SimpleLine" pass="0">
|
||||
<Option type="Map">
|
||||
<Option type="QString" value="0" name="align_dash_pattern"/>
|
||||
<Option type="QString" value="square" name="capstyle"/>
|
||||
<Option type="QString" value="5;2" name="customdash"/>
|
||||
<Option type="QString" value="3x:0,0,0,0,0,0" name="customdash_map_unit_scale"/>
|
||||
<Option type="QString" value="MM" name="customdash_unit"/>
|
||||
<Option type="QString" value="0" name="dash_pattern_offset"/>
|
||||
<Option type="QString" value="3x:0,0,0,0,0,0" name="dash_pattern_offset_map_unit_scale"/>
|
||||
<Option type="QString" value="MM" name="dash_pattern_offset_unit"/>
|
||||
<Option type="QString" value="0" name="draw_inside_polygon"/>
|
||||
<Option type="QString" value="round" name="joinstyle"/>
|
||||
<Option type="QString" value="215,168,255,255,rgb:0.84313725490196079,0.6588235294117647,1,1" name="line_color"/>
|
||||
<Option type="QString" value="solid" name="line_style"/>
|
||||
<Option type="QString" value="1.5" name="line_width"/>
|
||||
<Option type="QString" value="MM" name="line_width_unit"/>
|
||||
<Option type="QString" value="-0.75" name="offset"/>
|
||||
<Option type="QString" value="3x:0,0,0,0,0,0" name="offset_map_unit_scale"/>
|
||||
<Option type="QString" value="MM" name="offset_unit"/>
|
||||
<Option type="QString" value="0" name="ring_filter"/>
|
||||
<Option type="QString" value="0" name="trim_distance_end"/>
|
||||
<Option type="QString" value="3x:0,0,0,0,0,0" name="trim_distance_end_map_unit_scale"/>
|
||||
<Option type="QString" value="MM" name="trim_distance_end_unit"/>
|
||||
<Option type="QString" value="0" name="trim_distance_start"/>
|
||||
<Option type="QString" value="3x:0,0,0,0,0,0" name="trim_distance_start_map_unit_scale"/>
|
||||
<Option type="QString" value="MM" name="trim_distance_start_unit"/>
|
||||
<Option type="QString" value="0" name="tweak_dash_pattern_on_corners"/>
|
||||
<Option type="QString" value="0" name="use_custom_dash"/>
|
||||
<Option type="QString" value="3x:0,0,0,0,0,0" name="width_map_unit_scale"/>
|
||||
</Option>
|
||||
<qgis layerType="Vector" styleCategories="Symbology" version="4.0.1-Norrköping">
|
||||
<renderer-v2 enableorderby="0" forceraster="0" referencescale="-1" symbollevels="0" type="mergedFeatureRenderer">
|
||||
<renderer-v2 enableorderby="0" forceraster="0" referencescale="-1" symbollevels="0" type="singleSymbol">
|
||||
<symbols>
|
||||
<symbol alpha="1" clip_to_extent="1" force_rhr="0" frame_rate="10" is_animated="0" name="0" type="fill">
|
||||
<data_defined_properties>
|
||||
<Option type="Map">
|
||||
<Option type="QString" value="" name="name"/>
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option type="QString" value="collection" name="type"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</data_defined_properties>
|
||||
</layer>
|
||||
<layer locked="0" id="{fdc4d6fd-0995-41df-bbfb-19970b4fc2cc}" enabled="1" class="SimpleLine" pass="0">
|
||||
<Option type="Map">
|
||||
<Option type="QString" value="0" name="align_dash_pattern"/>
|
||||
<Option type="QString" value="square" name="capstyle"/>
|
||||
<Option type="QString" value="5;2" name="customdash"/>
|
||||
<Option type="QString" value="3x:0,0,0,0,0,0" name="customdash_map_unit_scale"/>
|
||||
<Option type="QString" value="MM" name="customdash_unit"/>
|
||||
<Option type="QString" value="0" name="dash_pattern_offset"/>
|
||||
<Option type="QString" value="3x:0,0,0,0,0,0" name="dash_pattern_offset_map_unit_scale"/>
|
||||
<Option type="QString" value="MM" name="dash_pattern_offset_unit"/>
|
||||
<Option type="QString" value="0" name="draw_inside_polygon"/>
|
||||
<Option type="QString" value="round" name="joinstyle"/>
|
||||
<Option type="QString" value="204,174,137,255,rgb:0.80000000000000004,0.68235294117647061,0.53725490196078429,1" name="line_color"/>
|
||||
<Option type="QString" value="solid" name="line_style"/>
|
||||
<Option type="QString" value="0.5" name="line_width"/>
|
||||
<Option type="QString" value="MM" name="line_width_unit"/>
|
||||
<Option type="QString" value="0" name="offset"/>
|
||||
<Option type="QString" value="3x:0,0,0,0,0,0" name="offset_map_unit_scale"/>
|
||||
<Option type="QString" value="MM" name="offset_unit"/>
|
||||
<Option type="QString" value="0" name="ring_filter"/>
|
||||
<Option type="QString" value="0" name="trim_distance_end"/>
|
||||
<Option type="QString" value="3x:0,0,0,0,0,0" name="trim_distance_end_map_unit_scale"/>
|
||||
<Option type="QString" value="MM" name="trim_distance_end_unit"/>
|
||||
<Option type="QString" value="0" name="trim_distance_start"/>
|
||||
<Option type="QString" value="3x:0,0,0,0,0,0" name="trim_distance_start_map_unit_scale"/>
|
||||
<Option type="QString" value="MM" name="trim_distance_start_unit"/>
|
||||
<Option type="QString" value="0" name="tweak_dash_pattern_on_corners"/>
|
||||
<Option type="QString" value="0" name="use_custom_dash"/>
|
||||
<Option type="QString" value="3x:0,0,0,0,0,0" name="width_map_unit_scale"/>
|
||||
</Option>
|
||||
<data_defined_properties>
|
||||
<layer class="SimpleLine" enabled="1" id="{feca00b2-500a-4c9a-b285-67ba2d99d8f6}" locked="0" pass="0">
|
||||
<Option type="Map">
|
||||
<Option type="QString" value="" name="name"/>
|
||||
<Option name="properties"/>
|
||||
<Option type="QString" value="collection" name="type"/>
|
||||
<Option name="align_dash_pattern" type="QString" value="0"/>
|
||||
<Option name="capstyle" type="QString" value="square"/>
|
||||
<Option name="customdash" type="QString" value="5;2"/>
|
||||
<Option name="customdash_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="customdash_unit" type="QString" value="MM"/>
|
||||
<Option name="dash_pattern_offset" type="QString" value="0"/>
|
||||
<Option name="dash_pattern_offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="dash_pattern_offset_unit" type="QString" value="MM"/>
|
||||
<Option name="draw_inside_polygon" type="QString" value="0"/>
|
||||
<Option name="joinstyle" type="QString" value="round"/>
|
||||
<Option name="line_color" type="QString" value="215,168,255,255,rgb:0.8431373,0.6588235,1,1"/>
|
||||
<Option name="line_style" type="QString" value="solid"/>
|
||||
<Option name="line_width" type="QString" value="1.5"/>
|
||||
<Option name="line_width_unit" type="QString" value="MM"/>
|
||||
<Option name="offset" type="QString" value="-0.75"/>
|
||||
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="offset_unit" type="QString" value="MM"/>
|
||||
<Option name="ring_filter" type="QString" value="0"/>
|
||||
<Option name="trim_distance_end" type="QString" value="0"/>
|
||||
<Option name="trim_distance_end_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="trim_distance_end_unit" type="QString" value="MM"/>
|
||||
<Option name="trim_distance_start" type="QString" value="0"/>
|
||||
<Option name="trim_distance_start_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="trim_distance_start_unit" type="QString" value="MM"/>
|
||||
<Option name="tweak_dash_pattern_on_corners" type="QString" value="0"/>
|
||||
<Option name="use_custom_dash" type="QString" value="0"/>
|
||||
<Option name="width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
</Option>
|
||||
</data_defined_properties>
|
||||
</layer>
|
||||
</symbol>
|
||||
</symbols>
|
||||
<rotation/>
|
||||
<sizescale/>
|
||||
<data_defined_properties>
|
||||
<Option type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</data_defined_properties>
|
||||
</layer>
|
||||
<layer class="SimpleLine" enabled="1" id="{fdc4d6fd-0995-41df-bbfb-19970b4fc2cc}" locked="0" pass="0">
|
||||
<Option type="Map">
|
||||
<Option name="align_dash_pattern" type="QString" value="0"/>
|
||||
<Option name="capstyle" type="QString" value="square"/>
|
||||
<Option name="customdash" type="QString" value="5;2"/>
|
||||
<Option name="customdash_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="customdash_unit" type="QString" value="MM"/>
|
||||
<Option name="dash_pattern_offset" type="QString" value="0"/>
|
||||
<Option name="dash_pattern_offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="dash_pattern_offset_unit" type="QString" value="MM"/>
|
||||
<Option name="draw_inside_polygon" type="QString" value="0"/>
|
||||
<Option name="joinstyle" type="QString" value="round"/>
|
||||
<Option name="line_color" type="QString" value="204,174,137,255,rgb:0.8,0.682353,0.5372549,1"/>
|
||||
<Option name="line_style" type="QString" value="solid"/>
|
||||
<Option name="line_width" type="QString" value="0.5"/>
|
||||
<Option name="line_width_unit" type="QString" value="MM"/>
|
||||
<Option name="offset" type="QString" value="0"/>
|
||||
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="offset_unit" type="QString" value="MM"/>
|
||||
<Option name="ring_filter" type="QString" value="0"/>
|
||||
<Option name="trim_distance_end" type="QString" value="0"/>
|
||||
<Option name="trim_distance_end_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="trim_distance_end_unit" type="QString" value="MM"/>
|
||||
<Option name="trim_distance_start" type="QString" value="0"/>
|
||||
<Option name="trim_distance_start_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="trim_distance_start_unit" type="QString" value="MM"/>
|
||||
<Option name="tweak_dash_pattern_on_corners" type="QString" value="0"/>
|
||||
<Option name="use_custom_dash" type="QString" value="0"/>
|
||||
<Option name="width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
</Option>
|
||||
<data_defined_properties>
|
||||
<Option type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</data_defined_properties>
|
||||
</layer>
|
||||
</symbol>
|
||||
</symbols>
|
||||
<rotation/>
|
||||
<sizescale/>
|
||||
<data-defined-properties>
|
||||
<Option type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</data-defined-properties>
|
||||
</renderer-v2>
|
||||
<data-defined-properties>
|
||||
<Option type="Map">
|
||||
<Option type="QString" value="" name="name"/>
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option type="QString" value="collection" name="type"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</data-defined-properties>
|
||||
</renderer-v2>
|
||||
<selection mode="Default">
|
||||
<selectionColor invalid="1"/>
|
||||
<selectionSymbol>
|
||||
<symbol is_animated="0" frame_rate="10" clip_to_extent="1" type="fill" alpha="1" force_rhr="0" name="">
|
||||
<symbol alpha="1" clip_to_extent="1" force_rhr="0" frame_rate="10" is_animated="0" name="" type="fill">
|
||||
<data_defined_properties>
|
||||
<Option type="Map">
|
||||
<Option type="QString" value="" name="name"/>
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option type="QString" value="collection" name="type"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</data_defined_properties>
|
||||
<layer locked="0" id="{f18003f5-220a-487f-8a6d-b24facc4c1a5}" enabled="1" class="SimpleFill" pass="0">
|
||||
<layer class="SimpleFill" enabled="1" id="{f18003f5-220a-487f-8a6d-b24facc4c1a5}" locked="0" pass="0">
|
||||
<Option type="Map">
|
||||
<Option type="QString" value="3x:0,0,0,0,0,0" name="border_width_map_unit_scale"/>
|
||||
<Option type="QString" value="0,0,255,255,rgb:0,0,1,1" name="color"/>
|
||||
<Option type="QString" value="bevel" name="joinstyle"/>
|
||||
<Option type="QString" value="0,0" name="offset"/>
|
||||
<Option type="QString" value="3x:0,0,0,0,0,0" name="offset_map_unit_scale"/>
|
||||
<Option type="QString" value="MM" name="offset_unit"/>
|
||||
<Option type="QString" value="35,35,35,255,rgb:0.13725490196078433,0.13725490196078433,0.13725490196078433,1" name="outline_color"/>
|
||||
<Option type="QString" value="solid" name="outline_style"/>
|
||||
<Option type="QString" value="0.26" name="outline_width"/>
|
||||
<Option type="QString" value="MM" name="outline_width_unit"/>
|
||||
<Option type="QString" value="solid" name="style"/>
|
||||
<Option name="border_width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="color" type="QString" value="0,0,255,255,rgb:0,0,1,1"/>
|
||||
<Option name="joinstyle" type="QString" value="bevel"/>
|
||||
<Option name="offset" type="QString" value="0,0"/>
|
||||
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="offset_unit" type="QString" value="MM"/>
|
||||
<Option name="outline_color" type="QString" value="35,35,35,255,rgb:0.1372549,0.1372549,0.1372549,1"/>
|
||||
<Option name="outline_style" type="QString" value="solid"/>
|
||||
<Option name="outline_width" type="QString" value="0.26"/>
|
||||
<Option name="outline_width_unit" type="QString" value="MM"/>
|
||||
<Option name="style" type="QString" value="solid"/>
|
||||
</Option>
|
||||
<data_defined_properties>
|
||||
<Option type="Map">
|
||||
<Option type="QString" value="" name="name"/>
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option type="QString" value="collection" name="type"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</data_defined_properties>
|
||||
</layer>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
"""Tests für das Plugin sn_verfahrensgebiet."""
|
||||
@@ -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()
|
||||
@@ -0,0 +1,86 @@
|
||||
import sys
|
||||
import types
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
if "qgis" not in sys.modules:
|
||||
qgis_mod = types.ModuleType("qgis")
|
||||
qgis_utils = types.ModuleType("qgis.utils")
|
||||
qgis_utils.iface = types.SimpleNamespace()
|
||||
qgis_utils.plugins = {}
|
||||
qgis_mod.utils = qgis_utils
|
||||
sys.modules["qgis"] = qgis_mod
|
||||
sys.modules["qgis.utils"] = qgis_utils
|
||||
|
||||
from sn_verfahrensgebiet.ui.dockwidget import DockWidget
|
||||
|
||||
|
||||
class _DummyTabWidget:
|
||||
def __init__(self):
|
||||
self.tabs = []
|
||||
|
||||
def addTab(self, widget, title):
|
||||
self.tabs.append((widget, title))
|
||||
|
||||
|
||||
class _TabWithServices:
|
||||
tab_title = "WithServices"
|
||||
|
||||
def __init__(self, _parent):
|
||||
self.services = None
|
||||
|
||||
def set_services(self, pruefmanager=None, data_grabber=None):
|
||||
self.services = (pruefmanager, data_grabber)
|
||||
|
||||
|
||||
class _TabWithoutServices:
|
||||
tab_title = "WithoutServices"
|
||||
|
||||
def __init__(self, _parent):
|
||||
pass
|
||||
|
||||
|
||||
class _TabRaises:
|
||||
tab_title = "Broken"
|
||||
|
||||
def __init__(self, _parent):
|
||||
raise RuntimeError("boom")
|
||||
|
||||
|
||||
class TestDockWidget(unittest.TestCase):
|
||||
|
||||
@patch("sn_verfahrensgebiet.ui.dockwidget.QTabWidget", _DummyTabWidget)
|
||||
def test_init_tabs_with_services_injects_available_services(self):
|
||||
obj = DockWidget.__new__(DockWidget)
|
||||
obj.tabs = [_TabWithServices, _TabWithoutServices]
|
||||
obj.pruefmanager = object()
|
||||
obj.data_grabber = object()
|
||||
obj._set_widget_calls = []
|
||||
obj.setWidget = lambda value: obj._set_widget_calls.append(value)
|
||||
|
||||
DockWidget._init_tabs_with_services(obj)
|
||||
|
||||
self.assertEqual(len(obj._set_widget_calls), 2)
|
||||
tab_widget = obj._set_widget_calls[-1]
|
||||
self.assertIsInstance(tab_widget, _DummyTabWidget)
|
||||
self.assertEqual(len(tab_widget.tabs), 2)
|
||||
|
||||
first_tab = tab_widget.tabs[0][0]
|
||||
self.assertEqual(first_tab.services, (obj.pruefmanager, obj.data_grabber))
|
||||
|
||||
@patch("sn_verfahrensgebiet.ui.dockwidget.error")
|
||||
@patch("sn_verfahrensgebiet.ui.dockwidget.QTabWidget", _DummyTabWidget)
|
||||
def test_init_tabs_with_services_reports_errors(self, mock_error):
|
||||
obj = DockWidget.__new__(DockWidget)
|
||||
obj.tabs = [_TabRaises]
|
||||
obj.pruefmanager = object()
|
||||
obj.data_grabber = object()
|
||||
obj.setWidget = lambda _value: None
|
||||
|
||||
DockWidget._init_tabs_with_services(obj)
|
||||
|
||||
mock_error.assert_called_once()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,447 @@
|
||||
"""sn_verfahrensgebiet/tests/test_flurstueck_alkis.py
|
||||
|
||||
Unit-Tests für das flurstueck_alkis-Modul.
|
||||
|
||||
Getestet werden:
|
||||
- Nutzerabbruch bei ask_text → (None, LoadStatus.NONE)
|
||||
- Ungültiger WFS-Layer → (None, LoadStatus.NONE)
|
||||
- Erfolgreicher Ladevorgang → gültiger Layer + LoadStatus.FIRST
|
||||
- Bestehender Layer, Drei-Knopf-Dialog: "Überschreiben" → LoadStatus.RELOAD,
|
||||
"Abbrechen" → (None, LoadStatus.NONE), "Anhängen" siehe
|
||||
TestFlurstueckAlkisAnhaengenDedup
|
||||
- vorgabe_schluessel/vorgabe_namen füllen den Eingabedialog vor
|
||||
- ohne_schluessel_abfrage=True überspringt die ask_text-Abfrage
|
||||
"""
|
||||
|
||||
import sys
|
||||
import types
|
||||
import unittest
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Minimales QGIS-Mock-Setup
|
||||
# ---------------------------------------------------------------------------
|
||||
if "qgis" not in sys.modules:
|
||||
qgis_mod = types.ModuleType("qgis")
|
||||
qgis_core = types.ModuleType("qgis.core")
|
||||
qgis_utils = types.ModuleType("qgis.utils")
|
||||
qgis_utils.iface = types.SimpleNamespace()
|
||||
qgis_utils.plugins = {}
|
||||
qgis_mod.utils = qgis_utils
|
||||
qgis_mod.core = qgis_core
|
||||
sys.modules["qgis"] = qgis_mod
|
||||
sys.modules["qgis.core"] = qgis_core
|
||||
sys.modules["qgis.utils"] = qgis_utils
|
||||
|
||||
from sn_verfahrensgebiet.functions.flurstueck_alkis import (
|
||||
flurstueck_alkis,
|
||||
lade_flurstuecke_per_gemarkung,
|
||||
FLST_LAYER_NAME,
|
||||
FELD_FLURSTUECKSKENNZEICHEN,
|
||||
)
|
||||
from sn_verfahrensgebiet.functions.verfahrensgebiet_alkis import LoadStatus
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Dummies
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class _DummyLayer:
|
||||
def __init__(self, valid: bool, layer_id="L1"):
|
||||
self._valid = valid
|
||||
self._id = layer_id
|
||||
|
||||
def isValid(self):
|
||||
return self._valid
|
||||
|
||||
def id(self):
|
||||
return self._id
|
||||
|
||||
def featureCount(self):
|
||||
return 1
|
||||
|
||||
|
||||
class _DummyProject:
|
||||
def __init__(self, named_layers=None):
|
||||
self._named_layers = named_layers or []
|
||||
self.removed = []
|
||||
|
||||
def mapLayersByName(self, name):
|
||||
return self._named_layers
|
||||
|
||||
def removeMapLayer(self, lid):
|
||||
self.removed.append(lid)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestFlurstueckAlkis(unittest.TestCase):
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# 1. Nutzerabbruch (ask_text liefert ok=False / None) #
|
||||
# ------------------------------------------------------------------ #
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QMessageBox")
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance")
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text")
|
||||
def test_user_cancel_returns_none(self, mock_ask_text, mock_project, mock_msgbox):
|
||||
"""Prüft, dass ein Abbruch im Eingabedialog (ok=False) (None, LoadStatus.NONE) liefert."""
|
||||
mock_ask_text.return_value = ("", False)
|
||||
mock_project.return_value = _DummyProject()
|
||||
|
||||
layer, status = flurstueck_alkis(None)
|
||||
|
||||
self.assertIsNone(layer)
|
||||
self.assertEqual(status, LoadStatus.NONE)
|
||||
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QMessageBox")
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance")
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text")
|
||||
def test_empty_text_after_confirmation_returns_none(self, mock_ask_text, mock_project, mock_msgbox):
|
||||
"""Prüft, dass ein bestätigter, aber leerer Eingabetext ebenfalls (None, LoadStatus.NONE) liefert."""
|
||||
mock_ask_text.return_value = (" ", True)
|
||||
mock_project.return_value = _DummyProject()
|
||||
|
||||
layer, status = flurstueck_alkis(None)
|
||||
|
||||
self.assertIsNone(layer)
|
||||
self.assertEqual(status, LoadStatus.NONE)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# 2. Ungültiger WFS-Layer #
|
||||
# ------------------------------------------------------------------ #
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QMessageBox")
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsVectorLayer")
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance")
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text")
|
||||
def test_invalid_wfs_layer_returns_none(
|
||||
self, mock_ask_text, mock_project, mock_wfs_layer_cls, mock_msgbox
|
||||
):
|
||||
"""Prüft, dass ein ungültiger WFS-Layer (None, LoadStatus.NONE) liefert."""
|
||||
mock_ask_text.return_value = ("1234", True)
|
||||
mock_project.return_value = _DummyProject()
|
||||
mock_wfs_layer_cls.return_value = _DummyLayer(valid=False)
|
||||
|
||||
layer, status = flurstueck_alkis(None)
|
||||
|
||||
self.assertIsNone(layer)
|
||||
self.assertEqual(status, LoadStatus.NONE)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# 3. Erfolgreicher Erstladevorgang #
|
||||
# ------------------------------------------------------------------ #
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.lade_flurstuecke_per_gemarkung")
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance")
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text")
|
||||
def test_success_returns_layer_and_first_status(
|
||||
self, mock_ask_text, mock_project, mock_lade
|
||||
):
|
||||
"""Prüft, dass ohne bestehenden Layer der von lade_flurstuecke_per_gemarkung gelieferte Layer mit LoadStatus.FIRST zurückgegeben wird."""
|
||||
mock_ask_text.return_value = ("1234, 5678", True)
|
||||
mock_project.return_value = _DummyProject()
|
||||
valid_layer = _DummyLayer(valid=True, layer_id="wfs_ok")
|
||||
mock_lade.return_value = valid_layer
|
||||
|
||||
layer, status = flurstueck_alkis(None)
|
||||
|
||||
self.assertIs(layer, valid_layer)
|
||||
self.assertEqual(status, LoadStatus.FIRST)
|
||||
mock_lade.assert_called_once_with(["1234", "5678"])
|
||||
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsVectorLayer")
|
||||
def test_sql_filter_contains_all_schluessel(self, mock_wfs_layer_cls):
|
||||
"""Prüft, dass lade_flurstuecke_per_gemarkung für jeden Gemarkungsschlüssel eine eigene WFS-Anfrage mit dessen Schlüssel stellt."""
|
||||
# Ungültig genügt: nur die aufgebaute URI je Aufruf wird geprüft, das
|
||||
# tatsächliche Zusammenführen der Features ist nicht Teil dieses Tests.
|
||||
mock_wfs_layer_cls.return_value = _DummyLayer(valid=False)
|
||||
|
||||
lade_flurstuecke_per_gemarkung(["1111", "2222", "3333"])
|
||||
|
||||
uris = [call.args[0] for call in mock_wfs_layer_cls.call_args_list]
|
||||
self.assertEqual(len(uris), 3)
|
||||
self.assertIn("1111", uris[0])
|
||||
self.assertIn("2222", uris[1])
|
||||
self.assertIn("3333", uris[2])
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# 4. Bestehender Layer – Nutzer klickt "Überschreiben" #
|
||||
# ------------------------------------------------------------------ #
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.lade_flurstuecke_per_gemarkung")
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QMessageBox")
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance")
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text")
|
||||
def test_existing_layer_reload_yes(
|
||||
self, mock_ask_text, mock_project, mock_msgbox_cls, mock_lade
|
||||
):
|
||||
"""Prüft, dass bei bestehendem Layer und Klick auf 'Überschreiben' der alte Layer entfernt und LoadStatus.RELOAD geliefert wird."""
|
||||
existing = _DummyLayer(valid=True, layer_id="old_flst")
|
||||
project = _DummyProject(named_layers=[existing])
|
||||
mock_ask_text.return_value = ("1234", True)
|
||||
mock_project.return_value = project
|
||||
|
||||
# Drei-Knopf-Dialog: addButton() liefert je Aufruf einen eigenen
|
||||
# Sentinel, clickedButton() simuliert den Klick auf "Überschreiben".
|
||||
btn_ueberschreiben, btn_anhaengen, btn_abbrechen = object(), object(), object()
|
||||
msg_instance = mock_msgbox_cls.return_value
|
||||
msg_instance.addButton.side_effect = [btn_ueberschreiben, btn_anhaengen, btn_abbrechen]
|
||||
msg_instance.clickedButton.return_value = btn_ueberschreiben
|
||||
|
||||
new_layer = _DummyLayer(valid=True, layer_id="new_flst")
|
||||
mock_lade.return_value = new_layer
|
||||
|
||||
layer, status = flurstueck_alkis(None)
|
||||
|
||||
self.assertIn(existing.id(), project.removed)
|
||||
self.assertIs(layer, new_layer)
|
||||
self.assertEqual(status, LoadStatus.RELOAD)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# 5. Bestehender Layer – Nutzer klickt "Abbrechen" #
|
||||
# ------------------------------------------------------------------ #
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QMessageBox")
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance")
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text")
|
||||
def test_existing_layer_abbrechen_returns_none(
|
||||
self, mock_ask_text, mock_project, mock_msgbox_cls
|
||||
):
|
||||
"""Prüft, dass bei bestehendem Layer und Klick auf 'Abbrechen' nichts verändert wird und (None, LoadStatus.NONE) geliefert wird."""
|
||||
existing = _DummyLayer(valid=True, layer_id="keep_flst")
|
||||
project = _DummyProject(named_layers=[existing])
|
||||
mock_ask_text.return_value = ("1234", True)
|
||||
mock_project.return_value = project
|
||||
|
||||
btn_ueberschreiben, btn_anhaengen, btn_abbrechen = object(), object(), object()
|
||||
msg_instance = mock_msgbox_cls.return_value
|
||||
msg_instance.addButton.side_effect = [btn_ueberschreiben, btn_anhaengen, btn_abbrechen]
|
||||
msg_instance.clickedButton.return_value = btn_abbrechen
|
||||
|
||||
layer, status = flurstueck_alkis(None)
|
||||
|
||||
self.assertIsNone(layer)
|
||||
self.assertEqual(status, LoadStatus.NONE)
|
||||
self.assertEqual(project.removed, []) # nichts gelöscht
|
||||
mock_ask_text.assert_not_called() # Abbruch schon vor der Schlüsselabfrage
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# 6. vorgabe_schluessel füllt ask_text-Default vor #
|
||||
# ------------------------------------------------------------------ #
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.lade_flurstuecke_per_gemarkung")
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance")
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text")
|
||||
def test_vorgabe_schluessel_prefills_default_text(
|
||||
self, mock_ask_text, mock_project, mock_lade
|
||||
):
|
||||
"""Prüft, dass vorgabe_schluessel als vorbelegter default_text im Eingabedialog erscheint."""
|
||||
mock_ask_text.return_value = ("1234, 5678", True)
|
||||
mock_project.return_value = _DummyProject()
|
||||
mock_lade.return_value = _DummyLayer(valid=True)
|
||||
|
||||
flurstueck_alkis(None, vorgabe_schluessel=["1234", "5678"])
|
||||
|
||||
args, kwargs = mock_ask_text.call_args
|
||||
# ask_text("title", label, default_text=..., parent=...)
|
||||
# default_text wird als Keyword-Arg übergeben
|
||||
default_text_arg = kwargs.get("default_text", args[2] if len(args) > 2 else "")
|
||||
self.assertIn("1234", default_text_arg)
|
||||
self.assertIn("5678", default_text_arg)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# 7. vorgabe_namen erscheint im Label-Text #
|
||||
# ------------------------------------------------------------------ #
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.lade_flurstuecke_per_gemarkung")
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance")
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text")
|
||||
def test_vorgabe_namen_appears_in_label(
|
||||
self, mock_ask_text, mock_project, mock_lade
|
||||
):
|
||||
"""Prüft, dass vorgabe_namen im Beschriftungstext des Eingabedialogs erscheint."""
|
||||
mock_ask_text.return_value = ("9999", True)
|
||||
mock_project.return_value = _DummyProject()
|
||||
mock_lade.return_value = _DummyLayer(valid=True)
|
||||
|
||||
flurstueck_alkis(None, vorgabe_namen=["Musterstadt"])
|
||||
|
||||
args, kwargs = mock_ask_text.call_args
|
||||
# label ist das 2. Positional-Argument
|
||||
label_arg = args[1] if len(args) > 1 else kwargs.get("label", "")
|
||||
self.assertIn("Musterstadt", label_arg)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# 8. Rückwärtskompatibilität: alle neuen Params optional #
|
||||
# ------------------------------------------------------------------ #
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.lade_flurstuecke_per_gemarkung")
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance")
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text")
|
||||
def test_backwards_compatible_no_new_params(
|
||||
self, mock_ask_text, mock_project, mock_lade
|
||||
):
|
||||
"""Prüft, dass flurstueck_alkis() ohne die neuen optionalen Parameter weiterhin fehlerfrei einen Layer mit LoadStatus.FIRST liefert."""
|
||||
mock_ask_text.return_value = ("0000", True)
|
||||
mock_project.return_value = _DummyProject()
|
||||
mock_lade.return_value = _DummyLayer(valid=True)
|
||||
|
||||
layer, status = flurstueck_alkis(None)
|
||||
|
||||
self.assertIsNotNone(layer)
|
||||
self.assertEqual(status, LoadStatus.FIRST)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# 9. ohne_schluessel_abfrage: vorgabe_schluessel direkt, kein ask_text (#47)
|
||||
# ------------------------------------------------------------------ #
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.lade_flurstuecke_per_gemarkung")
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance")
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text")
|
||||
def test_ohne_schluessel_abfrage_ueberspringt_ask_text(
|
||||
self, mock_ask_text, mock_project, mock_lade
|
||||
):
|
||||
"""Prüft, dass ohne_schluessel_abfrage=True vorgabe_schluessel direkt verwendet und ask_text nicht aufgerufen wird."""
|
||||
mock_project.return_value = _DummyProject()
|
||||
geladen = _DummyFlstLayer([_DummyFlstFeature("110001_0001__00001")])
|
||||
mock_lade.return_value = geladen
|
||||
|
||||
layer, status = flurstueck_alkis(
|
||||
None, vorgabe_schluessel=["1234", "5678"], ohne_schluessel_abfrage=True
|
||||
)
|
||||
|
||||
mock_ask_text.assert_not_called()
|
||||
self.assertIs(layer, geladen)
|
||||
self.assertEqual(status, LoadStatus.FIRST)
|
||||
mock_lade.assert_called_once_with(["1234", "5678"])
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Dummies für den Anhängen-Deduplizierungstest (#28)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class _DummyFlstFeature:
|
||||
def __init__(self, kennzeichen):
|
||||
self._kennzeichen = kennzeichen
|
||||
|
||||
def __getitem__(self, key):
|
||||
if key == FELD_FLURSTUECKSKENNZEICHEN:
|
||||
return self._kennzeichen
|
||||
return None
|
||||
|
||||
|
||||
class _DummyFlstFields:
|
||||
def __init__(self, hat_kennzeichen_feld: bool):
|
||||
self._hat_feld = hat_kennzeichen_feld
|
||||
|
||||
def indexFromName(self, name):
|
||||
if name == FELD_FLURSTUECKSKENNZEICHEN and self._hat_feld:
|
||||
return 0
|
||||
return -1
|
||||
|
||||
|
||||
class _DummyFlstLayer:
|
||||
"""Flurstück-Layer-Dummy mit Feature-Liste und aufzeichnendem ``dataProvider``."""
|
||||
|
||||
def __init__(self, features, hat_kennzeichen_feld: bool = True):
|
||||
self._features = list(features)
|
||||
self._hat_feld = hat_kennzeichen_feld
|
||||
self.added = None
|
||||
|
||||
def getFeatures(self):
|
||||
return list(self._features)
|
||||
|
||||
def fields(self):
|
||||
return _DummyFlstFields(self._hat_feld)
|
||||
|
||||
def dataProvider(self):
|
||||
layer = self
|
||||
|
||||
class _Provider:
|
||||
def addFeatures(self, feats):
|
||||
layer.added = list(feats)
|
||||
layer._features.extend(feats)
|
||||
return True
|
||||
|
||||
return _Provider()
|
||||
|
||||
def updateExtents(self):
|
||||
pass
|
||||
|
||||
def triggerRepaint(self):
|
||||
pass
|
||||
|
||||
def isValid(self):
|
||||
return True
|
||||
|
||||
def featureCount(self):
|
||||
return len(self._features)
|
||||
|
||||
|
||||
class TestFlurstueckAlkisAnhaengenDedup(unittest.TestCase):
|
||||
"""Anhängen an einen bestehenden Flurstück-Layer (Issue #28)."""
|
||||
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.lade_flurstuecke_per_gemarkung")
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis._check_existing_flurstueck_layer")
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text")
|
||||
def test_append_ueberspringt_bereits_vorhandene_flurstuecke(
|
||||
self, mock_ask_text, mock_check, mock_lade
|
||||
):
|
||||
"""Prüft, dass beim Anhängen bereits vorhandene flurstueckskennzeichen nicht erneut hinzugefügt werden."""
|
||||
mock_ask_text.return_value = ("0001, 0002", True)
|
||||
bestehend = _DummyFlstLayer([_DummyFlstFeature("110001_0001__00001")])
|
||||
neu_geladen = _DummyFlstLayer([
|
||||
_DummyFlstFeature("110001_0001__00001"), # bereits im Layer vorhanden
|
||||
_DummyFlstFeature("110001_0002__00007"), # noch nicht vorhanden
|
||||
])
|
||||
mock_check.return_value = (bestehend, LoadStatus.APPEND)
|
||||
mock_lade.return_value = neu_geladen
|
||||
|
||||
layer, status = flurstueck_alkis(None)
|
||||
|
||||
self.assertEqual(status, LoadStatus.APPEND)
|
||||
self.assertIs(layer, bestehend)
|
||||
self.assertEqual(len(bestehend.added), 1)
|
||||
self.assertEqual(bestehend.added[0][FELD_FLURSTUECKSKENNZEICHEN], "110001_0002__00007")
|
||||
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.lade_flurstuecke_per_gemarkung")
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis._check_existing_flurstueck_layer")
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text")
|
||||
def test_append_ohne_kennzeichen_feld_haengt_alles_an(
|
||||
self, mock_ask_text, mock_check, mock_lade
|
||||
):
|
||||
"""Prüft, dass ohne flurstueckskennzeichen-Feld weiterhin alle Features angehängt werden (Rückfallverhalten)."""
|
||||
mock_ask_text.return_value = ("0001", True)
|
||||
bestehend = _DummyFlstLayer([_DummyFlstFeature("110001_0001__00001")], hat_kennzeichen_feld=False)
|
||||
neu_geladen = _DummyFlstLayer(
|
||||
[_DummyFlstFeature("110001_0001__00001"), _DummyFlstFeature("110001_0002__00007")],
|
||||
hat_kennzeichen_feld=False,
|
||||
)
|
||||
mock_check.return_value = (bestehend, LoadStatus.APPEND)
|
||||
mock_lade.return_value = neu_geladen
|
||||
|
||||
layer, status = flurstueck_alkis(None)
|
||||
|
||||
self.assertEqual(status, LoadStatus.APPEND)
|
||||
self.assertEqual(len(bestehend.added), 2)
|
||||
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.lade_flurstuecke_per_gemarkung")
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis._check_existing_flurstueck_layer")
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text")
|
||||
def test_ohne_schluessel_abfrage_append_dedup(self, mock_ask_text, mock_check, mock_lade):
|
||||
"""ohne_schluessel_abfrage=True: Anhängen dedupliziert trotzdem, ohne nach Schlüsseln zu fragen (#47)."""
|
||||
bestehend = _DummyFlstLayer([_DummyFlstFeature("110001_0001__00001")])
|
||||
neu_geladen = _DummyFlstLayer([
|
||||
_DummyFlstFeature("110001_0001__00001"), # bereits vorhanden
|
||||
_DummyFlstFeature("110001_0002__00007"), # neu
|
||||
])
|
||||
mock_check.return_value = (bestehend, LoadStatus.APPEND)
|
||||
mock_lade.return_value = neu_geladen
|
||||
|
||||
layer, status = flurstueck_alkis(
|
||||
None, vorgabe_schluessel=["0001", "0002"], ohne_schluessel_abfrage=True
|
||||
)
|
||||
|
||||
mock_ask_text.assert_not_called()
|
||||
self.assertEqual(status, LoadStatus.APPEND)
|
||||
self.assertEqual(len(bestehend.added), 1)
|
||||
self.assertEqual(bestehend.added[0][FELD_FLURSTUECKSKENNZEICHEN], "110001_0002__00007")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,253 @@
|
||||
"""sn_verfahrensgebiet/tests/test_gemarkung_alkis.py
|
||||
|
||||
Unit-Tests für :mod:`sn_verfahrensgebiet.functions.gemarkung_alkis`.
|
||||
|
||||
Getestete Szenarien:
|
||||
|
||||
Erfolgsfall:
|
||||
- WFS-Layer gültig + Features mit Schlüssel → Liste mit Tupeln
|
||||
- Schlüssel werden dedupliziert und sortiert zurückgegeben
|
||||
- ``gemarkung_schluessel_by_bbox`` gibt nur Nummern (keine Tupel) zurück
|
||||
|
||||
Fehlerfälle:
|
||||
- WFS-Layer nicht gültig (beide Varianten) → Fallback-Ladeversuch
|
||||
- Fallback-Layer ebenfalls ungültig → leere Liste
|
||||
- Alle Layer leer (featureCount == 0) → client-seitige Filterung schlägt leer
|
||||
- Feature ohne Schlüssel-Attribut wird übersprungen
|
||||
- Exception beim Layer-Laden → leere Liste
|
||||
"""
|
||||
|
||||
import sys
|
||||
import types
|
||||
import unittest
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Minimales QGIS-Mock-Setup
|
||||
# ---------------------------------------------------------------------------
|
||||
if "qgis" not in sys.modules:
|
||||
qgis_mod = types.ModuleType("qgis")
|
||||
qgis_core = types.ModuleType("qgis.core")
|
||||
qgis_utils = types.ModuleType("qgis.utils")
|
||||
qgis_utils.iface = types.SimpleNamespace()
|
||||
qgis_utils.plugins = {}
|
||||
qgis_mod.utils = qgis_utils
|
||||
qgis_mod.core = qgis_core
|
||||
sys.modules["qgis"] = qgis_mod
|
||||
sys.modules["qgis.core"] = qgis_core
|
||||
sys.modules["qgis.utils"] = qgis_utils
|
||||
|
||||
from sn_verfahrensgebiet.functions.gemarkung_alkis import (
|
||||
_extract_schluessel_und_name,
|
||||
lade_gemarkungen_layer,
|
||||
)
|
||||
|
||||
# NOTE: gemarkung_schluessel_und_namen_by_bbox / gemarkung_schluessel_by_bbox
|
||||
# were renamed in the module refactoring. The tests below reference the old API
|
||||
# and are skipped until the test bodies are updated.
|
||||
try:
|
||||
from sn_verfahrensgebiet.functions.gemarkung_alkis import (
|
||||
gemarkung_schluessel_und_namen_by_bbox,
|
||||
gemarkung_schluessel_by_bbox,
|
||||
)
|
||||
_LEGACY_API_AVAILABLE = True
|
||||
except ImportError:
|
||||
_LEGACY_API_AVAILABLE = False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Dummies
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class _DummyRect:
|
||||
def xMinimum(self): return 414000.0
|
||||
def yMinimum(self): return 5657000.0
|
||||
def xMaximum(self): return 415000.0
|
||||
def yMaximum(self): return 5658000.0
|
||||
|
||||
|
||||
class _DummyFeature:
|
||||
def __init__(self, attrs: dict):
|
||||
self._attrs = attrs
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self._attrs.get(key, None)
|
||||
|
||||
|
||||
def _make_layer(valid=True, feature_count=2, features=None):
|
||||
layer = MagicMock()
|
||||
layer.isValid.return_value = valid
|
||||
layer.featureCount.return_value = feature_count if valid else 0
|
||||
layer.getFeatures.return_value = features or []
|
||||
return layer
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests: _extract_schluessel_und_name
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestExtractSchluesselUndName(unittest.TestCase):
|
||||
|
||||
def test_standard_attribute(self):
|
||||
feat = _DummyFeature({"schluessel_gemarkungsnummer": "1234", "bezeichnung": "Mustergemarkung"})
|
||||
nr, name = _extract_schluessel_und_name(feat)
|
||||
self.assertEqual(nr, "1234")
|
||||
self.assertEqual(name, "Mustergemarkung")
|
||||
|
||||
def test_fallback_attribute(self):
|
||||
feat = _DummyFeature({"gemarkungsnummer": "5678", "gemarkung_bezeichnung": "Alternativ"})
|
||||
nr, name = _extract_schluessel_und_name(feat)
|
||||
self.assertEqual(nr, "5678")
|
||||
self.assertEqual(name, "Alternativ")
|
||||
|
||||
def test_missing_attribute_returns_empty(self):
|
||||
feat = _DummyFeature({})
|
||||
nr, name = _extract_schluessel_und_name(feat)
|
||||
self.assertEqual(nr, "")
|
||||
self.assertEqual(name, "")
|
||||
|
||||
def test_none_value_returns_empty(self):
|
||||
feat = _DummyFeature({"schluessel_gemarkungsnummer": None, "bezeichnung": None})
|
||||
nr, name = _extract_schluessel_und_name(feat)
|
||||
self.assertEqual(nr, "")
|
||||
self.assertEqual(name, "")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests: gemarkung_schluessel_und_namen_by_bbox
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@unittest.skipUnless(_LEGACY_API_AVAILABLE, "Legacy API (by_bbox) not available in current module")
|
||||
class TestGemarkungSchluesselbbox(unittest.TestCase):
|
||||
|
||||
@patch("sn_verfahrensgebiet.functions.gemarkung_alkis._load_gemarkung_layer_with_bbox")
|
||||
def test_success_bbox_filter(self, mock_load_bbox):
|
||||
"""WFS-BBox-Filter liefert Features → korrekte Tupel-Liste."""
|
||||
feat1 = _DummyFeature({"schluessel_gemarkungsnummer": "1234", "bezeichnung": "Alpha"})
|
||||
feat2 = _DummyFeature({"schluessel_gemarkungsnummer": "5678", "bezeichnung": "Beta"})
|
||||
mock_load_bbox.return_value = _make_layer(valid=True, features=[feat1, feat2])
|
||||
|
||||
result = gemarkung_schluessel_und_namen_by_bbox(_DummyRect())
|
||||
|
||||
self.assertEqual(result, [("1234", "Alpha"), ("5678", "Beta")])
|
||||
|
||||
@patch("sn_verfahrensgebiet.functions.gemarkung_alkis._load_all_gemarkung_layer")
|
||||
@patch("sn_verfahrensgebiet.functions.gemarkung_alkis._load_gemarkung_layer_with_bbox")
|
||||
def test_fallback_when_bbox_fails(self, mock_load_bbox, mock_load_all):
|
||||
"""BBox-Filter schlägt fehl → Fallback auf Volllade + client-seitige Filterung."""
|
||||
mock_load_bbox.return_value = None
|
||||
|
||||
feat1 = _DummyFeature({"schluessel_gemarkungsnummer": "9999", "bezeichnung": "Fallback"})
|
||||
all_layer = _make_layer(valid=True, features=[feat1])
|
||||
# getFeatures mit FilterRect soll dasselbe zurückgeben
|
||||
all_layer.getFeatures.return_value = [feat1]
|
||||
mock_load_all.return_value = all_layer
|
||||
|
||||
result = gemarkung_schluessel_und_namen_by_bbox(_DummyRect())
|
||||
|
||||
self.assertEqual(len(result), 1)
|
||||
self.assertEqual(result[0][0], "9999")
|
||||
|
||||
@patch("sn_verfahrensgebiet.functions.gemarkung_alkis._load_all_gemarkung_layer")
|
||||
@patch("sn_verfahrensgebiet.functions.gemarkung_alkis._load_gemarkung_layer_with_bbox")
|
||||
def test_both_layers_fail_returns_empty(self, mock_load_bbox, mock_load_all):
|
||||
"""Beide Lade-Strategien scheitern → leere Liste."""
|
||||
mock_load_bbox.return_value = None
|
||||
mock_load_all.return_value = None
|
||||
|
||||
result = gemarkung_schluessel_und_namen_by_bbox(_DummyRect())
|
||||
self.assertEqual(result, [])
|
||||
|
||||
@patch("sn_verfahrensgebiet.functions.gemarkung_alkis._load_gemarkung_layer_with_bbox")
|
||||
def test_deduplication_and_sorting(self, mock_load_bbox):
|
||||
"""Doppelte Schlüssel werden dedupliziert, Ergebnis ist sortiert."""
|
||||
feat_a = _DummyFeature({"schluessel_gemarkungsnummer": "2222", "bezeichnung": "Z"})
|
||||
feat_b = _DummyFeature({"schluessel_gemarkungsnummer": "1111", "bezeichnung": "A"})
|
||||
feat_dup = _DummyFeature({"schluessel_gemarkungsnummer": "2222", "bezeichnung": "Z2"})
|
||||
mock_load_bbox.return_value = _make_layer(features=[feat_a, feat_b, feat_dup])
|
||||
|
||||
result = gemarkung_schluessel_und_namen_by_bbox(_DummyRect())
|
||||
|
||||
nummern = [r[0] for r in result]
|
||||
self.assertEqual(nummern, sorted(nummern))
|
||||
self.assertEqual(len(result), 2)
|
||||
|
||||
@patch("sn_verfahrensgebiet.functions.gemarkung_alkis._load_gemarkung_layer_with_bbox")
|
||||
def test_feature_without_schluessel_skipped(self, mock_load_bbox):
|
||||
"""Feature ohne Schlüssel-Attribut wird übersprungen."""
|
||||
feat_ok = _DummyFeature({"schluessel_gemarkungsnummer": "1234", "bezeichnung": "X"})
|
||||
feat_bad = _DummyFeature({})
|
||||
mock_load_bbox.return_value = _make_layer(features=[feat_ok, feat_bad])
|
||||
|
||||
result = gemarkung_schluessel_und_namen_by_bbox(_DummyRect())
|
||||
|
||||
self.assertEqual(len(result), 1)
|
||||
self.assertEqual(result[0][0], "1234")
|
||||
|
||||
@patch("sn_verfahrensgebiet.functions.gemarkung_alkis._load_gemarkung_layer_with_bbox")
|
||||
def test_exception_returns_empty(self, mock_load_bbox):
|
||||
"""Exception im Ladevorgang → leere Liste (kein Crash)."""
|
||||
mock_load_bbox.side_effect = RuntimeError("WFS-Fehler")
|
||||
|
||||
result = gemarkung_schluessel_und_namen_by_bbox(_DummyRect())
|
||||
self.assertEqual(result, [])
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests: gemarkung_schluessel_by_bbox (Convenience-Wrapper)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@unittest.skipUnless(_LEGACY_API_AVAILABLE, "Legacy API (by_bbox) not available in current module")
|
||||
class TestGemarkungSchluesselnurNummern(unittest.TestCase):
|
||||
|
||||
@patch("sn_verfahrensgebiet.functions.gemarkung_alkis.gemarkung_schluessel_und_namen_by_bbox")
|
||||
def test_returns_only_nummern(self, mock_full):
|
||||
mock_full.return_value = [("1234", "Alpha"), ("5678", "Beta")]
|
||||
|
||||
result = gemarkung_schluessel_by_bbox(_DummyRect())
|
||||
|
||||
self.assertEqual(result, ["1234", "5678"])
|
||||
|
||||
@patch("sn_verfahrensgebiet.functions.gemarkung_alkis.gemarkung_schluessel_und_namen_by_bbox")
|
||||
def test_empty_input_returns_empty(self, mock_full):
|
||||
mock_full.return_value = []
|
||||
|
||||
result = gemarkung_schluessel_by_bbox(_DummyRect())
|
||||
self.assertEqual(result, [])
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests: lade_gemarkungen_layer (Diensterreichbarkeit)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestLadeGemarkungenlayer(unittest.TestCase):
|
||||
|
||||
@patch("sn_verfahrensgebiet.functions.gemarkung_alkis.QgsVectorLayer")
|
||||
def test_returns_layer_when_service_reachable(self, mock_qgsvl):
|
||||
"""Dienst erreichbar (isValid=True) → Layer-Objekt zurückgegeben."""
|
||||
layer = MagicMock()
|
||||
layer.isValid.return_value = True
|
||||
mock_qgsvl.return_value = layer
|
||||
|
||||
result = lade_gemarkungen_layer()
|
||||
|
||||
self.assertIs(result, layer)
|
||||
mock_qgsvl.assert_called_once()
|
||||
# Layer-Name muss "Gemarkungen" sein
|
||||
args = mock_qgsvl.call_args[0]
|
||||
self.assertEqual(args[1], "Gemarkungen")
|
||||
|
||||
@patch("sn_verfahrensgebiet.functions.gemarkung_alkis.QgsVectorLayer")
|
||||
def test_returns_none_when_service_unreachable(self, mock_qgsvl):
|
||||
"""Dienst nicht erreichbar (isValid=False) → None zurückgegeben."""
|
||||
layer = MagicMock()
|
||||
layer.isValid.return_value = False
|
||||
mock_qgsvl.return_value = layer
|
||||
|
||||
result = lade_gemarkungen_layer()
|
||||
|
||||
self.assertIsNone(result)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,431 @@
|
||||
"""sn_verfahrensgebiet/tests/test_grenzpunkt_alkis.py
|
||||
|
||||
Unit-Tests für :mod:`sn_verfahrensgebiet.functions.grenzpunkt_alkis`.
|
||||
|
||||
Getestete Szenarien:
|
||||
|
||||
Erfolgsfall:
|
||||
- Vollständiger Workflow: PunktortTA laden → GML-IDs extrahieren →
|
||||
Grenzpunkte laden → beide Layer zurückgegeben
|
||||
|
||||
Fehlerfälle:
|
||||
- PunktortTA-Layer ungültig → (None, None)
|
||||
- PunktortTA leer (featureCount == 0) → Warnung + (None, None)
|
||||
- Keine gehoertZu-Referenzen → (punktort_layer, None) + Warnung
|
||||
- Grenzpunkt-Layer nach IDs leer → (punktort_layer, None) + Warnung
|
||||
- Kein Verfahrensgebiet-Layer für BBox-Ableitung → (None, None)
|
||||
|
||||
Hilfsfunktionen:
|
||||
- _extrahiere_gml_ids: xlink:href-Format wird korrekt geparst
|
||||
- _extrahiere_gml_ids: plain GML-ID ohne Doppelpunkt wird direkt verwendet
|
||||
- _extrahiere_gml_ids: leeres Feld wird übersprungen
|
||||
"""
|
||||
|
||||
import sys
|
||||
import types
|
||||
import unittest
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Minimales QGIS-Mock-Setup
|
||||
# ---------------------------------------------------------------------------
|
||||
if "qgis" not in sys.modules:
|
||||
qgis_mod = types.ModuleType("qgis")
|
||||
qgis_core = types.ModuleType("qgis.core")
|
||||
qgis_utils = types.ModuleType("qgis.utils")
|
||||
qgis_utils.iface = types.SimpleNamespace()
|
||||
qgis_utils.plugins = {}
|
||||
qgis_mod.utils = qgis_utils
|
||||
qgis_mod.core = qgis_core
|
||||
sys.modules["qgis"] = qgis_mod
|
||||
sys.modules["qgis.core"] = qgis_core
|
||||
sys.modules["qgis.utils"] = qgis_utils
|
||||
|
||||
from sn_verfahrensgebiet.functions.grenzpunkt_alkis import (
|
||||
grenzpunkt_alkis,
|
||||
grenzpunkte_gemarkungsweise,
|
||||
_extrahiere_gml_ids,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Dummies
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class _DummyRect:
|
||||
def xMinimum(self): return 414000.0
|
||||
def yMinimum(self): return 5657000.0
|
||||
def xMaximum(self): return 415000.0
|
||||
def yMaximum(self): return 5658000.0
|
||||
|
||||
|
||||
class _DummyFeature:
|
||||
def __init__(self, gehoert_zu_val=None):
|
||||
self._val = gehoert_zu_val
|
||||
|
||||
def __getitem__(self, key):
|
||||
if key == "gehoertZu":
|
||||
return self._val
|
||||
raise KeyError(key)
|
||||
|
||||
|
||||
def _make_layer(valid=True, feature_count=3, features=None, name="Layer"):
|
||||
layer = MagicMock()
|
||||
layer.isValid.return_value = valid
|
||||
layer.featureCount.return_value = feature_count if valid else 0
|
||||
layer.getFeatures.return_value = features or []
|
||||
layer.name.return_value = name
|
||||
layer.id.return_value = "LYR1"
|
||||
return layer
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests: _extrahiere_gml_ids
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestExtrahiereGmlIds(unittest.TestCase):
|
||||
|
||||
def test_xlink_href_format(self):
|
||||
"""URN-Format 'urn:adv:oid:DEABC123' → nur 'DEABC123' extrahiert."""
|
||||
feat = _DummyFeature("urn:adv:oid:DEABC123")
|
||||
layer = _make_layer(features=[feat])
|
||||
result = _extrahiere_gml_ids(layer)
|
||||
self.assertEqual(result, ["DEABC123"])
|
||||
|
||||
def test_plain_gml_id(self):
|
||||
"""Plain-GML-ID ohne Doppelpunkt → direkt übernommen."""
|
||||
feat = _DummyFeature("DEPLAIN456")
|
||||
layer = _make_layer(features=[feat])
|
||||
result = _extrahiere_gml_ids(layer)
|
||||
self.assertEqual(result, ["DEPLAIN456"])
|
||||
|
||||
def test_empty_value_skipped(self):
|
||||
"""Leerer gehoertZu-Wert wird übersprungen."""
|
||||
feat = _DummyFeature(None)
|
||||
layer = _make_layer(features=[feat])
|
||||
result = _extrahiere_gml_ids(layer)
|
||||
self.assertEqual(result, [])
|
||||
|
||||
def test_deduplication(self):
|
||||
"""Doppelte Referenzen werden dedupliziert."""
|
||||
f1 = _DummyFeature("urn:adv:oid:DEDUP001")
|
||||
f2 = _DummyFeature("urn:adv:oid:DEDUP001")
|
||||
f3 = _DummyFeature("urn:adv:oid:DEOTHER")
|
||||
layer = _make_layer(features=[f1, f2, f3])
|
||||
result = _extrahiere_gml_ids(layer)
|
||||
self.assertEqual(len(result), 2)
|
||||
self.assertIn("DEDUP001", result)
|
||||
self.assertIn("DEOTHER", result)
|
||||
|
||||
def test_sorted_output(self):
|
||||
"""Rückgabe ist aufsteigend sortiert."""
|
||||
f1 = _DummyFeature("urn:adv:oid:ZZZ")
|
||||
f2 = _DummyFeature("urn:adv:oid:AAA")
|
||||
layer = _make_layer(features=[f1, f2])
|
||||
result = _extrahiere_gml_ids(layer)
|
||||
self.assertEqual(result, sorted(result))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests: grenzpunkt_alkis
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_PATCH_PROJECT = "sn_verfahrensgebiet.functions.grenzpunkt_alkis.QgsProject"
|
||||
_PATCH_MSGBOX = "sn_verfahrensgebiet.functions.grenzpunkt_alkis.QMessageBox"
|
||||
_PATCH_WFS = "sn_verfahrensgebiet.functions.grenzpunkt_alkis.QgsVectorLayer"
|
||||
_PATCH_EXTRAHIERE = "sn_verfahrensgebiet.functions.grenzpunkt_alkis._extrahiere_gml_ids"
|
||||
_PATCH_LADE_GP = "sn_verfahrensgebiet.functions.grenzpunkt_alkis._lade_grenzpunkte_per_ids"
|
||||
_PATCH_INFO = "sn_verfahrensgebiet.functions.grenzpunkt_alkis.info"
|
||||
_PATCH_WARNING = "sn_verfahrensgebiet.functions.grenzpunkt_alkis.warning"
|
||||
|
||||
|
||||
class TestGrenzpunktAlkis(unittest.TestCase):
|
||||
|
||||
def _proj(self, vg_layer=None):
|
||||
proj = MagicMock()
|
||||
proj.mapLayersByName.return_value = [vg_layer] if vg_layer else []
|
||||
return proj
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# 1. Kein VG-Layer wenn extent=None → (None, None) #
|
||||
# ------------------------------------------------------------------ #
|
||||
@patch(_PATCH_MSGBOX)
|
||||
@patch(_PATCH_PROJECT)
|
||||
def test_no_vg_layer_returns_none(self, mock_proj, mock_msgbox):
|
||||
mock_proj.instance.return_value = self._proj(vg_layer=None)
|
||||
|
||||
pta, gp = grenzpunkt_alkis(None, extent=None)
|
||||
|
||||
self.assertIsNone(pta)
|
||||
self.assertIsNone(gp)
|
||||
mock_msgbox.warning.assert_called_once()
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# 2. PunktortTA-Layer ungültig → (None, None) #
|
||||
# ------------------------------------------------------------------ #
|
||||
@patch(_PATCH_MSGBOX)
|
||||
@patch(_PATCH_WFS)
|
||||
@patch(_PATCH_PROJECT)
|
||||
def test_invalid_punktort_layer(self, mock_proj, mock_wfs, mock_msgbox):
|
||||
proj = MagicMock()
|
||||
proj.mapLayersByName.return_value = []
|
||||
mock_proj.instance.return_value = proj
|
||||
mock_wfs.return_value = _make_layer(valid=False)
|
||||
|
||||
pta, gp = grenzpunkt_alkis(None, extent=_DummyRect())
|
||||
|
||||
self.assertIsNone(pta)
|
||||
self.assertIsNone(gp)
|
||||
mock_msgbox.critical.assert_called_once()
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# 3. PunktortTA leer → Warnung + (None, None) #
|
||||
# ------------------------------------------------------------------ #
|
||||
@patch(_PATCH_MSGBOX)
|
||||
@patch(_PATCH_WFS)
|
||||
@patch(_PATCH_PROJECT)
|
||||
def test_empty_punktort_layer(self, mock_proj, mock_wfs, mock_msgbox):
|
||||
mock_proj.instance.return_value = MagicMock()
|
||||
mock_wfs.return_value = _make_layer(valid=True, feature_count=0)
|
||||
|
||||
pta, gp = grenzpunkt_alkis(None, extent=_DummyRect())
|
||||
|
||||
self.assertIsNone(pta)
|
||||
self.assertIsNone(gp)
|
||||
mock_msgbox.warning.assert_called_once()
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# 4. Keine gehoertZu-Referenzen → (punktort_layer, None) + Warnung #
|
||||
# ------------------------------------------------------------------ #
|
||||
@patch(_PATCH_WARNING)
|
||||
@patch(_PATCH_EXTRAHIERE)
|
||||
@patch(_PATCH_WFS)
|
||||
@patch(_PATCH_PROJECT)
|
||||
def test_no_gml_ids_returns_pta_only(
|
||||
self, mock_proj, mock_wfs, mock_extrahiere, mock_warning
|
||||
):
|
||||
proj = MagicMock()
|
||||
mock_proj.instance.return_value = proj
|
||||
pta_layer = _make_layer(valid=True, feature_count=5)
|
||||
mock_wfs.return_value = pta_layer
|
||||
mock_extrahiere.return_value = []
|
||||
|
||||
pta, gp = grenzpunkt_alkis(None, extent=_DummyRect())
|
||||
|
||||
self.assertIs(pta, pta_layer)
|
||||
self.assertIsNone(gp)
|
||||
mock_warning.assert_called_once()
|
||||
proj.addMapLayer.assert_called_once_with(pta_layer)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# 5. Grenzpunkte-Batch liefert None → (punktort_layer, None) + Warn #
|
||||
# ------------------------------------------------------------------ #
|
||||
@patch(_PATCH_WARNING)
|
||||
@patch(_PATCH_LADE_GP)
|
||||
@patch(_PATCH_EXTRAHIERE)
|
||||
@patch(_PATCH_WFS)
|
||||
@patch(_PATCH_PROJECT)
|
||||
def test_grenzpunkt_batch_empty(
|
||||
self, mock_proj, mock_wfs, mock_extrahiere, mock_lade_gp, mock_warning
|
||||
):
|
||||
proj = MagicMock()
|
||||
mock_proj.instance.return_value = proj
|
||||
pta_layer = _make_layer(valid=True, feature_count=3)
|
||||
mock_wfs.return_value = pta_layer
|
||||
mock_extrahiere.return_value = ["DE0001", "DE0002"]
|
||||
mock_lade_gp.return_value = None # Batch leer
|
||||
|
||||
pta, gp = grenzpunkt_alkis(None, extent=_DummyRect())
|
||||
|
||||
self.assertIs(pta, pta_layer)
|
||||
self.assertIsNone(gp)
|
||||
mock_warning.assert_called_once()
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# 6. Vollständiger Erfolgsfall #
|
||||
# ------------------------------------------------------------------ #
|
||||
@patch(_PATCH_LADE_GP)
|
||||
@patch(_PATCH_EXTRAHIERE)
|
||||
@patch(_PATCH_WFS)
|
||||
@patch(_PATCH_PROJECT)
|
||||
def test_success_returns_both_layers(
|
||||
self, mock_proj, mock_wfs, mock_extrahiere, mock_lade_gp
|
||||
):
|
||||
proj = MagicMock()
|
||||
mock_proj.instance.return_value = proj
|
||||
pta_layer = _make_layer(valid=True, feature_count=10, name="PunktortTA")
|
||||
gp_layer = _make_layer(valid=True, feature_count=10, name="Grenzpunkte")
|
||||
mock_wfs.return_value = pta_layer
|
||||
mock_extrahiere.return_value = ["DE001", "DE002", "DE003"]
|
||||
mock_lade_gp.return_value = gp_layer
|
||||
|
||||
pta, gp = grenzpunkt_alkis(None, extent=_DummyRect())
|
||||
|
||||
self.assertIs(pta, pta_layer)
|
||||
self.assertIs(gp, gp_layer)
|
||||
# Beide Layer müssen ins Projekt eingefügt worden sein
|
||||
calls = [c[0][0] for c in proj.addMapLayer.call_args_list]
|
||||
self.assertIn(pta_layer, calls)
|
||||
self.assertIn(gp_layer, calls)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# 7. Extent aus vorhandenem VG-Layer ableiten #
|
||||
# ------------------------------------------------------------------ #
|
||||
@patch(_PATCH_LADE_GP)
|
||||
@patch(_PATCH_EXTRAHIERE)
|
||||
@patch(_PATCH_WFS)
|
||||
@patch(_PATCH_PROJECT)
|
||||
def test_extent_derived_from_vg_layer(
|
||||
self, mock_proj, mock_wfs, mock_extrahiere, mock_lade_gp
|
||||
):
|
||||
"""Wenn kein extent übergeben → Extent aus VG-Layer holen."""
|
||||
vg_layer = MagicMock()
|
||||
vg_layer.extent.return_value = _DummyRect()
|
||||
proj = MagicMock()
|
||||
proj.mapLayersByName.return_value = [vg_layer]
|
||||
mock_proj.instance.return_value = proj
|
||||
|
||||
pta_layer = _make_layer(valid=True, feature_count=2)
|
||||
gp_layer = _make_layer(valid=True, feature_count=2)
|
||||
mock_wfs.return_value = pta_layer
|
||||
mock_extrahiere.return_value = ["DE999"]
|
||||
mock_lade_gp.return_value = gp_layer
|
||||
|
||||
pta, gp = grenzpunkt_alkis(None, extent=None)
|
||||
|
||||
vg_layer.extent.assert_called_once()
|
||||
self.assertIs(pta, pta_layer)
|
||||
self.assertIs(gp, gp_layer)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests: grenzpunkte_gemarkungsweise – Stilanbringung (#53)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_G_PATCH_EXTENTS = "sn_verfahrensgebiet.functions.grenzpunkt_alkis._lade_gemarkung_extents"
|
||||
_G_PATCH_HITS = "sn_verfahrensgebiet.functions.grenzpunkt_alkis._wfs_hits_count"
|
||||
_G_PATCH_GET_GML = "sn_verfahrensgebiet.functions.grenzpunkt_alkis._wfs_get_gml"
|
||||
_G_PATCH_PARSE_GML = "sn_verfahrensgebiet.functions.grenzpunkt_alkis._parse_alkis_gml_memory_layer"
|
||||
_G_PATCH_ERKENNE_FELD = "sn_verfahrensgebiet.functions.grenzpunkt_alkis._erkenne_gehoert_zu_feld"
|
||||
_G_PATCH_EXTRAHIERE_ID = "sn_verfahrensgebiet.functions.grenzpunkt_alkis._extrahiere_feature_gml_id"
|
||||
_G_PATCH_LADE_GP_DICT = "sn_verfahrensgebiet.functions.grenzpunkt_alkis._lade_grenzpunkte_als_dict"
|
||||
_G_PATCH_ERSTELLE_JOINED = "sn_verfahrensgebiet.functions.grenzpunkt_alkis._erstelle_joined_layer"
|
||||
_G_PATCH_WFS_LAYER = "sn_verfahrensgebiet.functions.grenzpunkt_alkis.QgsVectorLayer"
|
||||
_G_PATCH_PROJECT = "sn_verfahrensgebiet.functions.grenzpunkt_alkis.QgsProject"
|
||||
_G_PATCH_PROGRESS = "sn_verfahrensgebiet.functions.grenzpunkt_alkis.create_progress_dialog"
|
||||
_G_PATCH_APPLY_STIL = "sn_verfahrensgebiet.functions.grenzpunkt_alkis.apply_style"
|
||||
# QgsField/QgsFeature werden innerhalb der Funktion per lokalem Import geholt
|
||||
# (from sn_basis.functions import QgsField/QgsFeature) - Patch-Ziel ist daher
|
||||
# die Quelle, nicht der Aufrufort.
|
||||
_G_PATCH_QGSFIELD = "sn_basis.functions.QgsField"
|
||||
_G_PATCH_QGSFEATURE = "sn_basis.functions.QgsFeature"
|
||||
|
||||
|
||||
class _DummyProgress:
|
||||
def set_value(self, wert): pass
|
||||
def set_label(self, text): pass
|
||||
def set_total(self, wert): pass
|
||||
def is_canceled(self): return False
|
||||
def close(self): pass
|
||||
|
||||
|
||||
class _DummyBbox:
|
||||
def xMinimum(self): return 414000.0
|
||||
def yMinimum(self): return 5657000.0
|
||||
def xMaximum(self): return 415000.0
|
||||
def yMaximum(self): return 5658000.0
|
||||
|
||||
|
||||
class _DummyGmlFeld:
|
||||
def __init__(self, name):
|
||||
self._name = name
|
||||
|
||||
def name(self):
|
||||
return self._name
|
||||
|
||||
|
||||
class _DummyGmlFeature:
|
||||
def __init__(self, gehoert_zu_wert):
|
||||
self._wert = gehoert_zu_wert
|
||||
|
||||
def geometry(self):
|
||||
return object()
|
||||
|
||||
def __getitem__(self, key):
|
||||
if key == "gehoertZu":
|
||||
return self._wert
|
||||
raise KeyError(key)
|
||||
|
||||
|
||||
class _DummyGmlLayer:
|
||||
"""Minimaler Ersatz für das Ergebnis von _parse_alkis_gml_memory_layer()."""
|
||||
|
||||
def __init__(self, feature):
|
||||
self._feature = feature
|
||||
|
||||
def fields(self):
|
||||
return [_DummyGmlFeld("gehoertZu")]
|
||||
|
||||
def getFeatures(self):
|
||||
return [self._feature]
|
||||
|
||||
|
||||
class TestGrenzpunkteGemarkungsweiseStil(unittest.TestCase):
|
||||
|
||||
@patch(_G_PATCH_APPLY_STIL)
|
||||
@patch(_G_PATCH_QGSFEATURE)
|
||||
@patch(_G_PATCH_QGSFIELD)
|
||||
@patch(_G_PATCH_PROGRESS)
|
||||
@patch(_G_PATCH_PROJECT)
|
||||
@patch(_G_PATCH_WFS_LAYER)
|
||||
@patch(_G_PATCH_ERSTELLE_JOINED)
|
||||
@patch(_G_PATCH_LADE_GP_DICT)
|
||||
@patch(_G_PATCH_EXTRAHIERE_ID)
|
||||
@patch(_G_PATCH_ERKENNE_FELD)
|
||||
@patch(_G_PATCH_PARSE_GML)
|
||||
@patch(_G_PATCH_GET_GML)
|
||||
@patch(_G_PATCH_HITS)
|
||||
@patch(_G_PATCH_EXTENTS)
|
||||
def test_stil_wird_nach_dem_laden_auf_den_ergebnis_layer_angewendet(
|
||||
self,
|
||||
mock_extents,
|
||||
mock_hits,
|
||||
mock_get_gml,
|
||||
mock_parse_gml,
|
||||
mock_erkenne_feld,
|
||||
mock_extrahiere_id,
|
||||
mock_lade_gp_dict,
|
||||
mock_erstelle_joined,
|
||||
mock_wfs_layer_cls,
|
||||
mock_project_cls,
|
||||
mock_progress,
|
||||
mock_qgsfield,
|
||||
mock_qgsfeature,
|
||||
mock_apply_style,
|
||||
):
|
||||
"""Prüft, dass grenzpunkte_gemarkungsweise() nach dem Laden apply_style() mit dem neuen ALKIS-Grenzpunkt-Stil auf den fertigen Join-Layer anwendet (#53)."""
|
||||
mock_extents.return_value = {"1234": _DummyBbox()}
|
||||
mock_hits.return_value = 1
|
||||
mock_get_gml.return_value = b"<fake/>"
|
||||
mock_parse_gml.return_value = _DummyGmlLayer(_DummyGmlFeature("urn:adv:oid:GP1"))
|
||||
mock_erkenne_feld.return_value = "gehoertZu"
|
||||
mock_extrahiere_id.return_value = "PTA1"
|
||||
mock_lade_gp_dict.return_value = ({"GP1": object()}, ["gp_feld"])
|
||||
|
||||
joined_layer = MagicMock()
|
||||
mock_erstelle_joined.return_value = joined_layer
|
||||
mock_wfs_layer_cls.return_value = MagicMock()
|
||||
|
||||
project = MagicMock()
|
||||
mock_project_cls.instance.return_value = project
|
||||
mock_progress.return_value = _DummyProgress()
|
||||
|
||||
ergebnis = grenzpunkte_gemarkungsweise(None, ["1234"])
|
||||
|
||||
self.assertIs(ergebnis, joined_layer)
|
||||
project.addMapLayer.assert_called_once_with(joined_layer)
|
||||
mock_apply_style.assert_called_once_with(joined_layer, "alkis_grenzpunkte.qml")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,475 @@
|
||||
"""sn_verfahrensgebiet/tests/test_knickpunkt_pruefung.py
|
||||
|
||||
Unit-Tests für :mod:`sn_verfahrensgebiet.functions.knickpunkt_pruefung`.
|
||||
|
||||
Getestete Szenarien:
|
||||
|
||||
_extrahiere_knickpunkte:
|
||||
- Leerer Layer → leere Liste
|
||||
- Polygon mit 4 Knickpunkten → korrekte Anzahl (ohne doppelten Schlusspunkt)
|
||||
- Keine Geometrie → leere Liste
|
||||
|
||||
knickpunkt_pruefung:
|
||||
- Leere Knickpunktliste → (None, 0, 0, 0)
|
||||
- Kein PunktortTA-Layer → (None, 0, 0, 0)
|
||||
- Identischer Punkt → grün gezählt
|
||||
- Kein passender PunktortTA → rot gezählt
|
||||
- PunktortTA im 3cm-Puffer einer Verbindungslinie zwischen zwei gematchten PunktortTA → orange gezählt
|
||||
- Abbruch via progress.is_canceled() → (None, 0, 0, 0)
|
||||
"""
|
||||
|
||||
import sys
|
||||
import types
|
||||
import unittest
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Minimales QGIS-Mock-Setup (identisch mit test_verfahrensgebiet_alkis_komplett)
|
||||
# ---------------------------------------------------------------------------
|
||||
if "qgis" not in sys.modules:
|
||||
qgis_mod = types.ModuleType("qgis")
|
||||
qgis_core = types.ModuleType("qgis.core")
|
||||
# Attribute vorab setzen, damit patch() sie überschreiben kann
|
||||
qgis_core.QgsPointXY = None
|
||||
qgis_core.QgsRectangle = None
|
||||
qgis_core.QgsSpatialIndex = None
|
||||
qgis_core.QgsMarkerSymbol = None
|
||||
qgis_core.QgsCategorizedSymbolRenderer = None
|
||||
qgis_core.QgsRendererCategory = None
|
||||
qgis_utils = types.ModuleType("qgis.utils")
|
||||
qgis_utils.iface = types.SimpleNamespace()
|
||||
qgis_utils.plugins = {}
|
||||
qgis_mod.utils = qgis_utils
|
||||
qgis_mod.core = qgis_core
|
||||
sys.modules["qgis"] = qgis_mod
|
||||
sys.modules["qgis.core"] = qgis_core
|
||||
sys.modules["qgis.utils"] = qgis_utils
|
||||
|
||||
from sn_verfahrensgebiet.functions.knickpunkt_pruefung import (
|
||||
knickpunkt_pruefung,
|
||||
_extrahiere_knickpunkte,
|
||||
WERT_IDENTISCH,
|
||||
WERT_KEIN_PTA,
|
||||
WERT_FEHLENDER_KNICK,
|
||||
)
|
||||
|
||||
_MOD = "sn_verfahrensgebiet.functions.knickpunkt_pruefung"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Hilfsfunktionen
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _make_layer(valid=True, crs="EPSG:25833", lid="L1"):
|
||||
layer = MagicMock()
|
||||
layer.isValid.return_value = valid
|
||||
layer.id.return_value = lid
|
||||
mock_crs = MagicMock()
|
||||
mock_crs.authid.return_value = crs
|
||||
layer.crs.return_value = mock_crs
|
||||
layer.getFeatures.return_value = []
|
||||
return layer
|
||||
|
||||
|
||||
def _make_progress(canceled_after: int = 9999):
|
||||
"""Progress-Mock: gibt is_canceled() nach N Aufrufen True zurück."""
|
||||
call_count = [0]
|
||||
|
||||
def _is_canceled():
|
||||
call_count[0] += 1
|
||||
return call_count[0] > canceled_after
|
||||
|
||||
prog = MagicMock()
|
||||
prog.is_canceled.side_effect = _is_canceled
|
||||
return prog
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests: _extrahiere_knickpunkte
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestExtrahiereKnickpunkte(unittest.TestCase):
|
||||
|
||||
def test_leerer_layer_gibt_leere_liste(self):
|
||||
layer = _make_layer()
|
||||
layer.getFeatures.return_value = []
|
||||
result = _extrahiere_knickpunkte(layer)
|
||||
self.assertEqual(result, [])
|
||||
|
||||
def test_keine_gueltige_geometrie_gibt_leere_liste(self):
|
||||
feat = MagicMock()
|
||||
feat.geometry.return_value = None
|
||||
layer = _make_layer()
|
||||
layer.getFeatures.return_value = [feat]
|
||||
result = _extrahiere_knickpunkte(layer)
|
||||
self.assertEqual(result, [])
|
||||
|
||||
def test_feature_mit_leerer_geometrie_gibt_leere_liste(self):
|
||||
feat = MagicMock()
|
||||
geom = MagicMock()
|
||||
geom.isEmpty.return_value = True
|
||||
feat.geometry.return_value = geom
|
||||
layer = _make_layer()
|
||||
layer.getFeatures.return_value = [feat]
|
||||
result = _extrahiere_knickpunkte(layer)
|
||||
self.assertEqual(result, [])
|
||||
|
||||
def test_qgis_nicht_verfuegbar_gibt_leere_liste(self):
|
||||
"""Wenn qgis.core kein QgsPointXY hat (ImportError in Funktion), leere Liste."""
|
||||
layer = _make_layer()
|
||||
feat = MagicMock()
|
||||
geom = MagicMock()
|
||||
geom.isEmpty.return_value = False
|
||||
feat.geometry.return_value = geom
|
||||
layer.getFeatures.return_value = [feat]
|
||||
|
||||
# QgsGeometry.unaryUnion wirft eine Exception → leere Liste erwartet
|
||||
with patch(f"{_MOD}.QgsGeometry") as mock_geom_cls:
|
||||
mock_geom_cls.unaryUnion.side_effect = ImportError("kein QGIS")
|
||||
result = _extrahiere_knickpunkte(layer)
|
||||
self.assertEqual(result, [])
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests: knickpunkt_pruefung – Eingabevalidierung
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestKnickpunktPruefungInputvalidierung(unittest.TestCase):
|
||||
|
||||
def test_keine_knickpunkte_gibt_none_zurueck(self):
|
||||
bror = _make_layer()
|
||||
bror.getFeatures.return_value = []
|
||||
result = knickpunkt_pruefung(bror, MagicMock(), None)
|
||||
layer, identisch, kein_pta, fehlend = result
|
||||
self.assertIsNone(layer)
|
||||
self.assertEqual(identisch, 0)
|
||||
self.assertEqual(kein_pta, 0)
|
||||
self.assertEqual(fehlend, 0)
|
||||
|
||||
def test_kein_punktort_layer_gibt_none_zurueck(self):
|
||||
bror = _make_layer()
|
||||
result = knickpunkt_pruefung(bror, None, None, knickpunkte=[MagicMock()])
|
||||
layer, identisch, kein_pta, fehlend = result
|
||||
self.assertIsNone(layer)
|
||||
|
||||
def test_abbruch_via_progress_gibt_none_zurueck(self):
|
||||
bror = _make_layer()
|
||||
pta = _make_layer()
|
||||
prog = _make_progress(canceled_after=0) # sofort abbrechen
|
||||
result = knickpunkt_pruefung(
|
||||
bror, pta, None,
|
||||
progress=prog,
|
||||
knickpunkte=[MagicMock()],
|
||||
)
|
||||
self.assertIsNone(result[0])
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests: knickpunkt_pruefung – Klassifizierungslogik (mit QGIS-Mocks)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestKnickpunktPruefungKlassifizierung(unittest.TestCase):
|
||||
"""Testet die Grün/Rot/Orange-Klassifizierung mit gesteuertem QGIS-Mock."""
|
||||
|
||||
def _make_pta_layer_mit_punkt(self, x: float, y: float, fid: int = 1):
|
||||
"""Erstellt einen PunktortTA-Layer-Mock mit einem Punkt bei (x, y)."""
|
||||
layer = _make_layer(lid=f"PTA_{fid}")
|
||||
geom = MagicMock()
|
||||
geom.isEmpty.return_value = False
|
||||
pt = MagicMock()
|
||||
pt.x.return_value = x
|
||||
pt.y.return_value = y
|
||||
geom.asPoint.return_value = pt
|
||||
feat = MagicMock()
|
||||
feat.id.return_value = fid
|
||||
feat.geometry.return_value = geom
|
||||
layer.getFeatures.return_value = [feat]
|
||||
return layer
|
||||
|
||||
def _make_bror_layer(self, x: float = 0.0, y: float = 0.0):
|
||||
"""BROR-Layer mit einer minimalen Polygon-Geometrie."""
|
||||
layer = _make_layer()
|
||||
geom = MagicMock()
|
||||
geom.isEmpty.return_value = False
|
||||
geom.area.return_value = 10000.0
|
||||
feat = MagicMock()
|
||||
feat.geometry.return_value = geom
|
||||
layer.getFeatures.return_value = [feat]
|
||||
return layer
|
||||
|
||||
@patch(f"{_MOD}.QgsGeometry")
|
||||
@patch(f"{_MOD}._baue_pta_index")
|
||||
@patch(f"{_MOD}._erstelle_ergebnis_layer")
|
||||
@patch(f"qgis.core.QgsRectangle", create=True)
|
||||
@patch(f"qgis.core.QgsPointXY", create=True)
|
||||
def test_identischer_punkt_wird_gruen_gezaehlt(
|
||||
self, mock_ptxy, mock_rect, mock_erstelle, mock_index, mock_geom_cls
|
||||
):
|
||||
"""PunktortTA exakt am Knickpunkt → identisch_count == 1."""
|
||||
# Knickpunkt bei (100, 200)
|
||||
knick = MagicMock()
|
||||
knick.x.return_value = 100.0
|
||||
knick.y.return_value = 200.0
|
||||
|
||||
# PunktortTA ebenfalls bei (100, 200) – exakter Treffer
|
||||
pta_geom = MagicMock()
|
||||
pta_geom.isEmpty.return_value = False
|
||||
pta_pt = MagicMock()
|
||||
pta_pt.x.return_value = 100.0
|
||||
pta_pt.y.return_value = 200.0
|
||||
pta_geom.asPoint.return_value = pta_pt
|
||||
|
||||
# Spatial-Index gibt Feature-ID 1 zurück
|
||||
mock_idx = MagicMock()
|
||||
mock_idx.intersects.return_value = [1]
|
||||
mock_index.return_value = (mock_idx, {1: pta_geom})
|
||||
|
||||
# Ergebnis-Layer
|
||||
pr = MagicMock()
|
||||
pr.addFeatures.return_value = True
|
||||
ergebnis = MagicMock()
|
||||
ergebnis.dataProvider.return_value = pr
|
||||
ergebnis.isValid.return_value = True
|
||||
mock_erstelle.return_value = ergebnis
|
||||
|
||||
bror = self._make_bror_layer()
|
||||
pta = self._make_pta_layer_mit_punkt(100.0, 200.0)
|
||||
|
||||
result = knickpunkt_pruefung(
|
||||
bror, pta, None, knickpunkte=[knick]
|
||||
)
|
||||
_, identisch, kein_pta, _ = result
|
||||
self.assertEqual(identisch, 1)
|
||||
self.assertEqual(kein_pta, 0)
|
||||
|
||||
@patch(f"{_MOD}.QgsGeometry")
|
||||
@patch(f"{_MOD}._baue_pta_index")
|
||||
@patch(f"{_MOD}._erstelle_ergebnis_layer")
|
||||
@patch(f"qgis.core.QgsRectangle", create=True)
|
||||
@patch(f"qgis.core.QgsPointXY", create=True)
|
||||
def test_kein_pta_an_knickpunkt_wird_rot_gezaehlt(
|
||||
self, mock_ptxy, mock_rect, mock_erstelle, mock_index, mock_geom_cls
|
||||
):
|
||||
"""Kein PunktortTA am Knickpunkt → kein_pta_count == 1."""
|
||||
knick = MagicMock()
|
||||
knick.x.return_value = 100.0
|
||||
knick.y.return_value = 200.0
|
||||
|
||||
# Spatial-Index gibt leere Liste zurück → kein Treffer
|
||||
mock_idx = MagicMock()
|
||||
mock_idx.intersects.return_value = []
|
||||
mock_index.return_value = (mock_idx, {})
|
||||
|
||||
pr = MagicMock()
|
||||
pr.addFeatures.return_value = True
|
||||
ergebnis = MagicMock()
|
||||
ergebnis.dataProvider.return_value = pr
|
||||
ergebnis.isValid.return_value = True
|
||||
mock_erstelle.return_value = ergebnis
|
||||
|
||||
bror = self._make_bror_layer()
|
||||
pta = self._make_pta_layer_mit_punkt(999.0, 999.0) # weit weg
|
||||
|
||||
result = knickpunkt_pruefung(
|
||||
bror, pta, None, knickpunkte=[knick]
|
||||
)
|
||||
_, identisch, kein_pta, _ = result
|
||||
self.assertEqual(identisch, 0)
|
||||
self.assertEqual(kein_pta, 1)
|
||||
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests: _fehlende_knickpunkte_via_verbindungslinien
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestFehlendKnickpunkteViaVerbindungslinien(unittest.TestCase):
|
||||
"""Direkte Tests für :func:`_fehlende_knickpunkte_via_verbindungslinien`."""
|
||||
|
||||
def _fn(self, *args, **kwargs):
|
||||
from sn_verfahrensgebiet.functions.knickpunkt_pruefung import (
|
||||
_fehlende_knickpunkte_via_verbindungslinien,
|
||||
)
|
||||
return _fehlende_knickpunkte_via_verbindungslinien(*args, **kwargs)
|
||||
|
||||
def test_leere_union_gibt_null_zurueck(self):
|
||||
"""bror_union.isEmpty() == True → (0, [])."""
|
||||
bror_union = MagicMock()
|
||||
bror_union.isEmpty.return_value = True
|
||||
with patch("qgis.core.QgsPointXY", create=True), \
|
||||
patch("qgis.core.QgsProject", create=True):
|
||||
count, feats = self._fn(
|
||||
bror_union, MagicMock(), {}, set(), MagicMock(), "EPSG:25833"
|
||||
)
|
||||
self.assertEqual(count, 0)
|
||||
self.assertEqual(feats, [])
|
||||
|
||||
def test_weniger_als_zwei_pta_auf_ring_kein_orange(self):
|
||||
"""Nur 1 gematchter PunktortTA auf Ring → keine fehlenden Knickpunkte."""
|
||||
bror_union = MagicMock()
|
||||
bror_union.isEmpty.return_value = False
|
||||
bror_union.isMultipart.return_value = False
|
||||
bror_union.asPolygon.return_value = [
|
||||
[(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 0.0)]
|
||||
]
|
||||
|
||||
def _mock_ptxy(x, y):
|
||||
pt = MagicMock()
|
||||
pt.x.return_value = x
|
||||
pt.y.return_value = y
|
||||
return pt
|
||||
|
||||
pta_geom = MagicMock()
|
||||
pta_pt = MagicMock()
|
||||
pta_pt.x.return_value = 0.0
|
||||
pta_pt.y.return_value = 0.0
|
||||
pta_geom.asPoint.return_value = pta_pt
|
||||
geom_map = {1: pta_geom}
|
||||
matched_pta_ids = {1} # nur 1 → < 2
|
||||
|
||||
with patch("qgis.core.QgsPointXY", create=True, side_effect=_mock_ptxy), \
|
||||
patch("qgis.core.QgsProject", create=True):
|
||||
count, feats = self._fn(
|
||||
bror_union, MagicMock(), geom_map, matched_pta_ids,
|
||||
MagicMock(), "EPSG:25833"
|
||||
)
|
||||
self.assertEqual(count, 0)
|
||||
self.assertEqual(len(feats), 0)
|
||||
|
||||
@patch(f"{_MOD}.QgsGeometry")
|
||||
@patch(f"{_MOD}.QgsVectorLayer")
|
||||
def test_ungematchter_pta_im_puffer_wird_orange(
|
||||
self, mock_vl_cls, mock_geom_cls
|
||||
):
|
||||
"""Nicht-gematchter PunktortTA innerhalb 3-cm-Puffer → orange gezählt."""
|
||||
bror_union = MagicMock()
|
||||
bror_union.isEmpty.return_value = False
|
||||
bror_union.isMultipart.return_value = False
|
||||
bror_union.asPolygon.return_value = [
|
||||
[(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 0.0)]
|
||||
]
|
||||
|
||||
def _mock_ptxy(x, y):
|
||||
pt = MagicMock()
|
||||
pt.x.return_value = x
|
||||
pt.y.return_value = y
|
||||
return pt
|
||||
|
||||
# Zwei gematchte PunktortTA (fid 1 und 2)
|
||||
pta_geom_1 = MagicMock()
|
||||
pta_pt_1 = MagicMock()
|
||||
pta_pt_1.x.return_value = 0.0
|
||||
pta_pt_1.y.return_value = 0.0
|
||||
pta_geom_1.asPoint.return_value = pta_pt_1
|
||||
|
||||
pta_geom_2 = MagicMock()
|
||||
pta_pt_2 = MagicMock()
|
||||
pta_pt_2.x.return_value = 10.0
|
||||
pta_pt_2.y.return_value = 0.0
|
||||
pta_geom_2.asPoint.return_value = pta_pt_2
|
||||
|
||||
# Nicht-gematchter PunktortTA (fid=3) liegt im Puffer
|
||||
pta_geom_3 = MagicMock()
|
||||
pta_geom_3.isEmpty.return_value = False
|
||||
pta_geom_3.within.return_value = True
|
||||
|
||||
geom_map = {1: pta_geom_1, 2: pta_geom_2, 3: pta_geom_3}
|
||||
matched_pta_ids = {1, 2}
|
||||
|
||||
# Spatial-Index liefert fid=3 als Kandidat
|
||||
pta_index = MagicMock()
|
||||
buffer_geom = MagicMock()
|
||||
buffer_geom.isEmpty.return_value = False
|
||||
buffer_geom.boundingBox.return_value = MagicMock()
|
||||
pta_index.intersects.return_value = [3]
|
||||
|
||||
line_geom = MagicMock()
|
||||
line_geom.isEmpty.return_value = False
|
||||
line_geom.buffer.return_value = buffer_geom
|
||||
mock_geom_cls.fromPolylineXY.return_value = line_geom
|
||||
|
||||
pr_mock = MagicMock()
|
||||
pr_mock.addFeatures.return_value = True
|
||||
layer_mock = MagicMock()
|
||||
layer_mock.dataProvider.return_value = pr_mock
|
||||
mock_vl_cls.return_value = layer_mock
|
||||
|
||||
with patch("qgis.core.QgsPointXY", create=True, side_effect=_mock_ptxy), \
|
||||
patch("qgis.core.QgsProject", create=True) as mock_proj:
|
||||
mock_proj.instance.return_value = MagicMock()
|
||||
count, feats = self._fn(
|
||||
bror_union, pta_index, geom_map, matched_pta_ids,
|
||||
MagicMock(), "EPSG:25833"
|
||||
)
|
||||
|
||||
self.assertEqual(count, 1)
|
||||
self.assertEqual(len(feats), 1)
|
||||
|
||||
@patch(f"{_MOD}.QgsGeometry")
|
||||
@patch(f"{_MOD}.QgsVectorLayer")
|
||||
def test_ungematchter_pta_ausserhalb_puffer_kein_orange(
|
||||
self, mock_vl_cls, mock_geom_cls
|
||||
):
|
||||
"""Nicht-gematchter PunktortTA außerhalb Puffer → nicht orange gezählt."""
|
||||
bror_union = MagicMock()
|
||||
bror_union.isEmpty.return_value = False
|
||||
bror_union.isMultipart.return_value = False
|
||||
bror_union.asPolygon.return_value = [
|
||||
[(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 0.0)]
|
||||
]
|
||||
|
||||
def _mock_ptxy(x, y):
|
||||
pt = MagicMock()
|
||||
pt.x.return_value = x
|
||||
pt.y.return_value = y
|
||||
return pt
|
||||
|
||||
pta_geom_1 = MagicMock()
|
||||
pta_pt_1 = MagicMock()
|
||||
pta_pt_1.x.return_value = 0.0
|
||||
pta_pt_1.y.return_value = 0.0
|
||||
pta_geom_1.asPoint.return_value = pta_pt_1
|
||||
|
||||
pta_geom_2 = MagicMock()
|
||||
pta_pt_2 = MagicMock()
|
||||
pta_pt_2.x.return_value = 10.0
|
||||
pta_pt_2.y.return_value = 0.0
|
||||
pta_geom_2.asPoint.return_value = pta_pt_2
|
||||
|
||||
# fid=3 liegt NICHT im Puffer
|
||||
pta_geom_3 = MagicMock()
|
||||
pta_geom_3.within.return_value = False
|
||||
|
||||
geom_map = {1: pta_geom_1, 2: pta_geom_2, 3: pta_geom_3}
|
||||
matched_pta_ids = {1, 2}
|
||||
|
||||
pta_index = MagicMock()
|
||||
buffer_geom = MagicMock()
|
||||
buffer_geom.isEmpty.return_value = False
|
||||
buffer_geom.boundingBox.return_value = MagicMock()
|
||||
pta_index.intersects.return_value = [3]
|
||||
|
||||
line_geom = MagicMock()
|
||||
line_geom.isEmpty.return_value = False
|
||||
line_geom.buffer.return_value = buffer_geom
|
||||
mock_geom_cls.fromPolylineXY.return_value = line_geom
|
||||
|
||||
pr_mock = MagicMock()
|
||||
layer_mock = MagicMock()
|
||||
layer_mock.dataProvider.return_value = pr_mock
|
||||
mock_vl_cls.return_value = layer_mock
|
||||
|
||||
with patch("qgis.core.QgsPointXY", create=True, side_effect=_mock_ptxy), \
|
||||
patch("qgis.core.QgsProject", create=True) as mock_proj:
|
||||
mock_proj.instance.return_value = MagicMock()
|
||||
count, feats = self._fn(
|
||||
bror_union, pta_index, geom_map, matched_pta_ids,
|
||||
MagicMock(), "EPSG:25833"
|
||||
)
|
||||
|
||||
self.assertEqual(count, 0)
|
||||
self.assertEqual(len(feats), 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,121 @@
|
||||
import sys
|
||||
import types
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
if "qgis" not in sys.modules:
|
||||
qgis_mod = types.ModuleType("qgis")
|
||||
qgis_utils = types.ModuleType("qgis.utils")
|
||||
qgis_utils.iface = types.SimpleNamespace()
|
||||
qgis_utils.plugins = {}
|
||||
qgis_mod.utils = qgis_utils
|
||||
sys.modules["qgis"] = qgis_mod
|
||||
sys.modules["qgis.utils"] = qgis_utils
|
||||
|
||||
from sn_verfahrensgebiet.main import Verfahrensgebiet
|
||||
|
||||
|
||||
class _DummyIface:
|
||||
def __init__(self):
|
||||
self.removed = []
|
||||
|
||||
def mainWindow(self):
|
||||
return object()
|
||||
|
||||
def removeDockWidget(self, dock):
|
||||
self.removed.append(dock)
|
||||
|
||||
|
||||
class _DummyDock:
|
||||
def __init__(self):
|
||||
self.object_name = None
|
||||
self.action = None
|
||||
self.deleted = False
|
||||
|
||||
def setObjectName(self, name):
|
||||
self.object_name = name
|
||||
|
||||
def deleteLater(self):
|
||||
self.deleted = True
|
||||
|
||||
|
||||
class TestPluginMain(unittest.TestCase):
|
||||
|
||||
@patch("sn_verfahrensgebiet.main.DataGrabber")
|
||||
@patch("sn_verfahrensgebiet.main.Pruefmanager")
|
||||
def test_init_sets_services_and_names(self, mock_pruefmanager, mock_data_grabber):
|
||||
pm = object()
|
||||
dg = object()
|
||||
mock_pruefmanager.return_value = pm
|
||||
mock_data_grabber.return_value = dg
|
||||
|
||||
plugin = Verfahrensgebiet(_DummyIface())
|
||||
|
||||
self.assertIs(plugin.pruefmanager, pm)
|
||||
self.assertIs(plugin.data_grabber, dg)
|
||||
self.assertEqual(plugin.plugin_name, "Verfahrensgebiet")
|
||||
self.assertEqual(plugin.dock_name, "sn_dock_verfahrensgebiet")
|
||||
|
||||
@patch("sn_verfahrensgebiet.main.plugins", new_callable=dict)
|
||||
def test_initGui_registers_action_when_basis_ui_exists(self, mock_plugins):
|
||||
iface = _DummyIface()
|
||||
plugin = Verfahrensgebiet(iface)
|
||||
|
||||
ui = SimpleNamespace(
|
||||
add_action=Mock(return_value="action_obj"),
|
||||
finalize_menu_and_toolbar=Mock(),
|
||||
)
|
||||
mock_plugins["sn_basis"] = SimpleNamespace(ui=ui)
|
||||
|
||||
plugin.initGui()
|
||||
|
||||
self.assertEqual(plugin.action, "action_obj")
|
||||
ui.add_action.assert_called_once()
|
||||
ui.finalize_menu_and_toolbar.assert_called_once()
|
||||
|
||||
@patch("sn_verfahrensgebiet.main.plugins", new_callable=dict)
|
||||
@patch("sn_verfahrensgebiet.main.DockManager.show")
|
||||
@patch("sn_verfahrensgebiet.main.DockWidget")
|
||||
def test_run_creates_dock_and_marks_toolbar_active(self, mock_dockwidget_cls, mock_show, mock_plugins):
|
||||
iface = _DummyIface()
|
||||
plugin = Verfahrensgebiet(iface)
|
||||
plugin.action = "action_obj"
|
||||
|
||||
dock = _DummyDock()
|
||||
mock_dockwidget_cls.return_value = dock
|
||||
|
||||
ui = SimpleNamespace(set_active_plugin=Mock())
|
||||
mock_plugins["sn_basis"] = SimpleNamespace(ui=ui)
|
||||
|
||||
plugin.run()
|
||||
|
||||
self.assertIs(plugin.dockwidget, dock)
|
||||
self.assertEqual(dock.object_name, "sn_dock_verfahrensgebiet")
|
||||
self.assertEqual(dock.action, "action_obj")
|
||||
mock_show.assert_called_once_with(dock)
|
||||
ui.set_active_plugin.assert_called_once_with("action_obj")
|
||||
|
||||
@patch("sn_verfahrensgebiet.main.plugins", new_callable=dict)
|
||||
def test_unload_removes_dock_and_action(self, mock_plugins):
|
||||
iface = _DummyIface()
|
||||
plugin = Verfahrensgebiet(iface)
|
||||
|
||||
dock = _DummyDock()
|
||||
plugin.dockwidget = dock
|
||||
plugin.action = "action_obj"
|
||||
|
||||
ui = SimpleNamespace(remove_action=Mock())
|
||||
mock_plugins["sn_basis"] = SimpleNamespace(ui=ui)
|
||||
|
||||
plugin.unload()
|
||||
|
||||
self.assertIn(dock, iface.removed)
|
||||
self.assertTrue(dock.deleted)
|
||||
self.assertIsNone(plugin.dockwidget)
|
||||
ui.remove_action.assert_called_once_with("action_obj")
|
||||
self.assertIsNone(plugin.action)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,272 @@
|
||||
import sys
|
||||
import types
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
if "qgis" not in sys.modules:
|
||||
qgis_mod = types.ModuleType("qgis")
|
||||
qgis_utils = types.ModuleType("qgis.utils")
|
||||
qgis_utils.iface = types.SimpleNamespace()
|
||||
qgis_utils.plugins = {}
|
||||
qgis_mod.utils = qgis_utils
|
||||
sys.modules["qgis"] = qgis_mod
|
||||
sys.modules["qgis.utils"] = qgis_utils
|
||||
|
||||
from sn_verfahrensgebiet.functions import verfahrensgebiet_alkis as vg
|
||||
|
||||
|
||||
class _DummyMessageBox:
|
||||
class StandardButton:
|
||||
Yes = 1
|
||||
No = 2
|
||||
|
||||
last_critical = None
|
||||
next_reply = StandardButton.No
|
||||
|
||||
@staticmethod
|
||||
def question(_parent, _title, _message, _buttons, _default):
|
||||
return _DummyMessageBox.next_reply
|
||||
|
||||
@staticmethod
|
||||
def critical(parent, title, message):
|
||||
_DummyMessageBox.last_critical = (parent, title, message)
|
||||
|
||||
|
||||
class _DummyProject:
|
||||
def __init__(self, layers=None):
|
||||
self._layers = layers or []
|
||||
self.removed = []
|
||||
|
||||
def mapLayersByName(self, _name):
|
||||
return self._layers
|
||||
|
||||
def removeMapLayer(self, layer_id):
|
||||
self.removed.append(layer_id)
|
||||
|
||||
|
||||
class _DummyExistingLayer:
|
||||
def __init__(self, layer_id="existing"):
|
||||
self._id = layer_id
|
||||
|
||||
def id(self):
|
||||
return self._id
|
||||
|
||||
|
||||
class _DummyGeom:
|
||||
def __init__(self, empty=False):
|
||||
self._empty = empty
|
||||
|
||||
def isEmpty(self):
|
||||
return self._empty
|
||||
|
||||
|
||||
class _DummyFeature:
|
||||
def __init__(self, geom=None, attrs=None):
|
||||
self._geom = geom or _DummyGeom()
|
||||
self._attrs = attrs or {}
|
||||
|
||||
def geometry(self):
|
||||
return self._geom
|
||||
|
||||
def attribute(self, name):
|
||||
return self._attrs.get(name)
|
||||
|
||||
|
||||
class _DummyTempLayer:
|
||||
def __init__(self, valid=True, features=None):
|
||||
self._valid = valid
|
||||
self._features = features or []
|
||||
|
||||
def isValid(self):
|
||||
return self._valid
|
||||
|
||||
def getFeatures(self):
|
||||
return self._features
|
||||
|
||||
def crs(self):
|
||||
class _Crs:
|
||||
@staticmethod
|
||||
def authid():
|
||||
return "EPSG:25833"
|
||||
|
||||
return _Crs()
|
||||
|
||||
|
||||
class _DummyProvider:
|
||||
def addAttributes(self, _attrs):
|
||||
return True
|
||||
|
||||
def addFeatures(self, _features):
|
||||
return True
|
||||
|
||||
|
||||
class _DummyMemoryLayer:
|
||||
def __init__(self):
|
||||
self._provider = _DummyProvider()
|
||||
|
||||
def dataProvider(self):
|
||||
return self._provider
|
||||
|
||||
def updateFields(self):
|
||||
return None
|
||||
|
||||
def updateExtents(self):
|
||||
return None
|
||||
|
||||
|
||||
class TestVerfahrensgebietAlkis(unittest.TestCase):
|
||||
|
||||
@patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.QgsProject.instance")
|
||||
@patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.QMessageBox", new=_DummyMessageBox)
|
||||
def test_check_existing_layer_first_load(self, mock_project_instance):
|
||||
mock_project_instance.return_value = _DummyProject(layers=[])
|
||||
|
||||
layer, status = vg._check_existing_layer(tab_widget=None)
|
||||
|
||||
self.assertIsNone(layer)
|
||||
self.assertEqual(status, vg.LoadStatus.FIRST)
|
||||
|
||||
@patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.QgsProject.instance")
|
||||
@patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.QMessageBox", new=_DummyMessageBox)
|
||||
def test_check_existing_layer_keep_existing(self, mock_project_instance):
|
||||
existing = _DummyExistingLayer("L1")
|
||||
mock_project_instance.return_value = _DummyProject(layers=[existing])
|
||||
_DummyMessageBox.next_reply = _DummyMessageBox.StandardButton.No
|
||||
|
||||
layer, status = vg._check_existing_layer(tab_widget=None)
|
||||
|
||||
self.assertIs(layer, existing)
|
||||
self.assertEqual(status, vg.LoadStatus.KEEP)
|
||||
|
||||
@patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.QMessageBox", new=_DummyMessageBox)
|
||||
@patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.get_variable", return_value="")
|
||||
def test_verfahrensgebiet_alkis_fails_when_verfahrensnummer_missing(self, _mock_get_var):
|
||||
_DummyMessageBox.last_critical = None
|
||||
|
||||
layer, status, feats = vg.verfahrensgebiet_alkis(tab_widget=None)
|
||||
|
||||
self.assertIsNone(layer)
|
||||
self.assertEqual(status, vg.LoadStatus.NONE)
|
||||
self.assertIsNotNone(_DummyMessageBox.last_critical)
|
||||
|
||||
@patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis._check_existing_layer", return_value=(None, vg.LoadStatus.FIRST))
|
||||
@patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.QMessageBox", new=_DummyMessageBox)
|
||||
@patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.get_variable", return_value="VKZ123")
|
||||
@patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.QgsVectorLayer")
|
||||
def test_verfahrensgebiet_alkis_fails_on_invalid_wfs_layer(self, mock_qgsvectorlayer, _mock_get_var, _mock_check):
|
||||
_DummyMessageBox.last_critical = None
|
||||
mock_qgsvectorlayer.return_value = _DummyTempLayer(valid=False)
|
||||
|
||||
layer, status, feats = vg.verfahrensgebiet_alkis(tab_widget=None)
|
||||
|
||||
self.assertIsNone(layer)
|
||||
self.assertEqual(status, vg.LoadStatus.NONE)
|
||||
self.assertIsNotNone(_DummyMessageBox.last_critical)
|
||||
|
||||
@patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.QgsFeature")
|
||||
@patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.QgsGeometry.unaryUnion", return_value=_DummyGeom(empty=False))
|
||||
@patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis._check_existing_layer", return_value=(None, vg.LoadStatus.FIRST))
|
||||
@patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.get_variable", return_value="VKZ123")
|
||||
@patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.QgsVectorLayer")
|
||||
@patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.QMessageBox", new=_DummyMessageBox)
|
||||
def test_verfahrensgebiet_alkis_success_single_kombination(self, mock_qgsvectorlayer, _mock_get_var, _mock_check, _mock_union, mock_feature_cls):
|
||||
"""Einzelne (adv_name, bezeichnung)-Kombination → kein Dialog, Memory-Layer zurück."""
|
||||
feat = _DummyFeature(attrs={"adv_name": "Bodensanierung", "bezeichnung": "VKZ12/001"})
|
||||
temp_layer = _DummyTempLayer(valid=True, features=[feat])
|
||||
memory_layer = _DummyMemoryLayer()
|
||||
mock_qgsvectorlayer.side_effect = [temp_layer, memory_layer]
|
||||
|
||||
mock_feature = mock_feature_cls.return_value
|
||||
mock_feature.setGeometry.return_value = None
|
||||
mock_feature.setAttributes.return_value = None
|
||||
|
||||
layer, status, feats = vg.verfahrensgebiet_alkis(tab_widget=None)
|
||||
|
||||
self.assertIs(layer, memory_layer)
|
||||
self.assertEqual(status, vg.LoadStatus.FIRST)
|
||||
self.assertEqual(len(feats), 1)
|
||||
|
||||
@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_dialog_auswahl(
|
||||
self, mock_qgsvectorlayer, _mock_get_var, _mock_check, _mock_union, mock_feature_cls, mock_dialog
|
||||
):
|
||||
"""Zwei verschiedene Kombinationen → Dialog wird gezeigt, gewählte Features 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")]
|
||||
|
||||
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)
|
||||
# Nur feat1 gehört zur gewählten Kombination
|
||||
self.assertEqual(feats, [feat1])
|
||||
|
||||
@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")
|
||||
@patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.QMessageBox", new=_DummyMessageBox)
|
||||
def test_verfahrensgebiet_alkis_multi_kombination_dialog_abbruch(
|
||||
self, mock_qgsvectorlayer, _mock_get_var, _mock_check, mock_dialog
|
||||
):
|
||||
"""Zwei verschiedene Kombinationen → Nutzer bricht Dialog ab → NONE."""
|
||||
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])
|
||||
mock_qgsvectorlayer.return_value = temp_layer
|
||||
mock_dialog.return_value = [] # Nutzer bricht ab
|
||||
|
||||
layer, status, feats = vg.verfahrensgebiet_alkis(tab_widget=None)
|
||||
|
||||
mock_dialog.assert_called_once()
|
||||
self.assertIsNone(layer)
|
||||
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()
|
||||
@@ -0,0 +1,783 @@
|
||||
"""sn_verfahrensgebiet/tests/test_verfahrensgebiet_alkis_komplett.py
|
||||
|
||||
Unit-Tests für :mod:`sn_verfahrensgebiet.functions.verfahrensgebiet_alkis_komplett`.
|
||||
|
||||
Getestete Szenarien:
|
||||
|
||||
Erfolgsfall (Flächenübereinstimmung):
|
||||
- Vollständiger Workflow → enriched Layer + flst_layer + bror_layer + LoadStatus.FIRST
|
||||
- VKZ aus Projektvariable wird als Feld gesetzt (wenn vorhanden)
|
||||
- Kein VKZ in Projektvariable → Feld wird nicht gesetzt (kein Fehler)
|
||||
|
||||
Abbruchpfade:
|
||||
- verfahrensgebiet_alkis gibt LoadStatus.NONE → (None, None, None, NONE)
|
||||
- flurstueck_alkis gibt LoadStatus.NONE → (None, None, None, NONE)
|
||||
- Keine Flurstücke selektiert → (None, flst_layer, None, NONE) + Warnung
|
||||
- erstelle_verfahrensgebiet_aus_auswahl liefert ungültigen Layer → (None, flst_layer, None, NONE)
|
||||
- Progress-Abbruch nach Schritt 1 → sofortiger sauberer Rückgabestatus
|
||||
|
||||
Flächenvergleich:
|
||||
- Übereinstimmung → show_info_dialog, keine Detailprüfung
|
||||
- Abweichung + abbrechen → kein Detailprüfungs-Aufruf
|
||||
- Abweichung + als_vg_laden → BROR umbenannt, als enriched_layer zurückgegeben
|
||||
- Abweichung + detailpruefung → lade_punktort_ta + knickpunkt_pruefung aufgerufen
|
||||
|
||||
Cleanup:
|
||||
- BROR-Layer wird bei Erfolg NICHT entfernt
|
||||
- BROR-Layer wird bei Fehler entfernt (wenn in diesem Lauf hinzugefügt)
|
||||
- TempVerfahrensgebiet wird stets entfernt
|
||||
|
||||
Hilfsfunktionen:
|
||||
- _select_by_location: processing-Fallback liefert 0 → keine Exception
|
||||
- _fuege_vkz_feld_hinzu: leeres VKZ wird ignoriert
|
||||
- _vergleiche_flaechen: korrekte Toleranzlogik
|
||||
- _lese_flaechen_toleranz: Default 0.0, parst Projektvariable
|
||||
"""
|
||||
|
||||
import sys
|
||||
import types
|
||||
import unittest
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Minimales QGIS-Mock-Setup
|
||||
# ---------------------------------------------------------------------------
|
||||
if "qgis" not in sys.modules:
|
||||
qgis_mod = types.ModuleType("qgis")
|
||||
qgis_core = types.ModuleType("qgis.core")
|
||||
qgis_utils = types.ModuleType("qgis.utils")
|
||||
qgis_utils.iface = types.SimpleNamespace()
|
||||
qgis_utils.plugins = {}
|
||||
qgis_mod.utils = qgis_utils
|
||||
qgis_mod.core = qgis_core
|
||||
sys.modules["qgis"] = qgis_mod
|
||||
sys.modules["qgis.core"] = qgis_core
|
||||
sys.modules["qgis.utils"] = qgis_utils
|
||||
|
||||
from sn_verfahrensgebiet.functions.verfahrensgebiet_alkis import LoadStatus
|
||||
from sn_verfahrensgebiet.functions.verfahrensgebiet_alkis_komplett import (
|
||||
verfahrensgebiet_alkis_komplett,
|
||||
_select_by_location,
|
||||
_fuege_vkz_feld_hinzu,
|
||||
_vergleiche_flaechen,
|
||||
_lese_flaechen_toleranz,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Dummies
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class _DummyRect:
|
||||
def xMinimum(self): return 414000.0
|
||||
def yMinimum(self): return 5657000.0
|
||||
def xMaximum(self): return 415000.0
|
||||
def yMaximum(self): return 5658000.0
|
||||
|
||||
|
||||
def _make_layer(valid=True, name="Layer", lid="L1", feature_count=1, area=1000.0):
|
||||
"""Erzeugt einen konfigurierten Mock-Layer mit getFeatures-Support."""
|
||||
layer = MagicMock()
|
||||
layer.isValid.return_value = valid
|
||||
layer.id.return_value = lid
|
||||
layer.name.return_value = name
|
||||
layer.extent.return_value = _DummyRect()
|
||||
layer.featureCount.return_value = feature_count
|
||||
layer.selectedFeatureCount.return_value = feature_count
|
||||
|
||||
mock_geom = MagicMock()
|
||||
mock_geom.isEmpty.return_value = False
|
||||
mock_geom.area.return_value = area
|
||||
mock_geom.boundary.return_value = MagicMock(isEmpty=lambda: True)
|
||||
mock_feat = MagicMock()
|
||||
mock_feat.geometry.return_value = mock_geom
|
||||
layer.getFeatures.return_value = [mock_feat]
|
||||
|
||||
mock_crs = MagicMock()
|
||||
mock_crs.authid.return_value = "EPSG:25833"
|
||||
layer.crs.return_value = mock_crs
|
||||
return layer
|
||||
|
||||
|
||||
class _DummyProject:
|
||||
def __init__(self):
|
||||
self.added = []
|
||||
self.removed = []
|
||||
self._layers_by_name: dict = {}
|
||||
|
||||
def addMapLayer(self, layer):
|
||||
self.added.append(layer)
|
||||
|
||||
def removeMapLayer(self, lid):
|
||||
self.removed.append(lid)
|
||||
|
||||
def mapLayersByName(self, name):
|
||||
return self._layers_by_name.get(name, [])
|
||||
|
||||
def instance(self):
|
||||
return self
|
||||
|
||||
|
||||
def _make_progress_mock(canceled=False):
|
||||
prog = MagicMock()
|
||||
prog.is_canceled.return_value = canceled
|
||||
return prog
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Patch-Konstanten
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_MOD = "sn_verfahrensgebiet.functions.verfahrensgebiet_alkis_komplett"
|
||||
_PATCH_VG_ALKIS = f"{_MOD}._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"
|
||||
_PATCH_ERSTELLE = f"{_MOD}.erstelle_verfahrensgebiet_aus_auswahl"
|
||||
_PATCH_VKZ = f"{_MOD}._fuege_vkz_feld_hinzu"
|
||||
_PATCH_PROJECT = f"{_MOD}.QgsProject"
|
||||
_PATCH_MSGBOX = f"{_MOD}.QMessageBox"
|
||||
_PATCH_WARNING = f"{_MOD}.warning"
|
||||
_PATCH_GET_VAR = f"{_MOD}.get_variable"
|
||||
_PATCH_PROGRESS = f"{_MOD}.create_progress_dialog"
|
||||
_PATCH_VERGLEICHE = f"{_MOD}._vergleiche_flaechen"
|
||||
_PATCH_SHOW_INFO = f"{_MOD}.show_info_dialog"
|
||||
_PATCH_ASK_DIALOG = f"{_MOD}.ask_detailpruefung_oder_vg_laden"
|
||||
_PATCH_GP_ALKIS = f"{_MOD}.lade_punktort_ta"
|
||||
_PATCH_KNICK = f"{_MOD}.knickpunkt_pruefung"
|
||||
_PATCH_EXTRAHIERE = f"{_MOD}._extrahiere_knickpunkte"
|
||||
_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"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests: _select_by_location
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestSelectByLocation(unittest.TestCase):
|
||||
|
||||
def test_processing_not_available_returns_zero(self):
|
||||
flst, vg = MagicMock(), MagicMock()
|
||||
with patch.dict("sys.modules", {"processing": None}):
|
||||
result = _select_by_location(flst, vg)
|
||||
self.assertEqual(result, 0)
|
||||
|
||||
def test_processing_success_returns_count(self):
|
||||
flst = MagicMock()
|
||||
flst.selectedFeatureCount.return_value = 7
|
||||
vg = MagicMock()
|
||||
mock_proc = MagicMock()
|
||||
with patch.dict("sys.modules", {"processing": mock_proc}):
|
||||
result = _select_by_location(flst, vg)
|
||||
self.assertEqual(result, 7)
|
||||
|
||||
def test_processing_exception_returns_zero(self):
|
||||
flst, vg = MagicMock(), MagicMock()
|
||||
mock_proc = MagicMock()
|
||||
mock_proc.run.side_effect = RuntimeError("Kein QGIS")
|
||||
with patch.dict("sys.modules", {"processing": mock_proc}):
|
||||
result = _select_by_location(flst, vg)
|
||||
self.assertEqual(result, 0)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests: _fuege_vkz_feld_hinzu
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestFuegeVkzFeldHinzu(unittest.TestCase):
|
||||
|
||||
def test_empty_vkz_does_nothing(self):
|
||||
layer = MagicMock()
|
||||
_fuege_vkz_feld_hinzu(layer, "")
|
||||
layer.dataProvider.assert_not_called()
|
||||
|
||||
def test_none_vkz_does_nothing(self):
|
||||
layer = MagicMock()
|
||||
_fuege_vkz_feld_hinzu(layer, None)
|
||||
layer.dataProvider.assert_not_called()
|
||||
|
||||
def test_vkz_field_added(self):
|
||||
pr = MagicMock()
|
||||
pr.addAttributes.return_value = True
|
||||
pr.changeAttributeValues.return_value = True
|
||||
layer = MagicMock()
|
||||
layer.dataProvider.return_value = pr
|
||||
layer.fields.return_value.indexFromName.return_value = 2
|
||||
layer.getFeatures.return_value = []
|
||||
_fuege_vkz_feld_hinzu(layer, "VF-2024-001")
|
||||
pr.addAttributes.assert_called_once()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests: _vergleiche_flaechen / _lese_flaechen_toleranz
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestFlaechenvergleich(unittest.TestCase):
|
||||
|
||||
def _layer(self, area: float):
|
||||
layer = MagicMock()
|
||||
geom = MagicMock()
|
||||
geom.isEmpty.return_value = False
|
||||
geom.area.return_value = area
|
||||
feat = MagicMock()
|
||||
feat.geometry.return_value = geom
|
||||
layer.getFeatures.return_value = [feat]
|
||||
return layer
|
||||
|
||||
def test_gleiche_flaeche_stimmt_ueberein(self):
|
||||
self.assertTrue(_vergleiche_flaechen(self._layer(1234.5), self._layer(1234.5), 0.0)[2])
|
||||
|
||||
def test_verschiedene_flaeche_stimmt_nicht_ueberein(self):
|
||||
self.assertFalse(_vergleiche_flaechen(self._layer(1234.5), self._layer(1200.0), 0.0)[2])
|
||||
|
||||
def test_differenz_innerhalb_toleranz(self):
|
||||
self.assertTrue(_vergleiche_flaechen(self._layer(1234.5), self._layer(1234.8), 1.0)[2])
|
||||
|
||||
def test_differenz_ausserhalb_toleranz(self):
|
||||
self.assertFalse(_vergleiche_flaechen(self._layer(1234.5), self._layer(1236.0), 1.0)[2])
|
||||
|
||||
def test_flaechen_werte_korrekt(self):
|
||||
bror_f, vg_f, _ = _vergleiche_flaechen(self._layer(800.0), self._layer(750.0))
|
||||
self.assertAlmostEqual(bror_f, 800.0)
|
||||
self.assertAlmostEqual(vg_f, 750.0)
|
||||
|
||||
@patch(f"{_MOD}.get_variable", return_value=None)
|
||||
def test_lese_toleranz_default_ist_null(self, _):
|
||||
self.assertEqual(_lese_flaechen_toleranz(), 0.0)
|
||||
|
||||
@patch(f"{_MOD}.get_variable", return_value="2.5")
|
||||
def test_lese_toleranz_parst_string(self, _):
|
||||
self.assertAlmostEqual(_lese_flaechen_toleranz(), 2.5)
|
||||
|
||||
@patch(f"{_MOD}.get_variable", return_value="ungueltig")
|
||||
def test_lese_toleranz_ungueltig_fallback_null(self, _):
|
||||
self.assertEqual(_lese_flaechen_toleranz(), 0.0)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests: verfahrensgebiet_alkis_komplett (Hauptworkflow)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestVerfahrensgebietAlkisKomplett(unittest.TestCase):
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Abbruchpfade
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
@patch(_PATCH_PROGRESS)
|
||||
@patch(_PATCH_PROJECT)
|
||||
@patch(_PATCH_VG_ALKIS)
|
||||
@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, {}, None)
|
||||
mock_prog.return_value = _make_progress_mock()
|
||||
|
||||
enriched, flst, bror, status = verfahrensgebiet_alkis_komplett(None)
|
||||
|
||||
self.assertIsNone(enriched)
|
||||
self.assertIsNone(flst)
|
||||
self.assertIsNone(bror)
|
||||
self.assertEqual(status, LoadStatus.NONE)
|
||||
|
||||
@patch(_PATCH_PROGRESS)
|
||||
@patch(_PATCH_PROJECT)
|
||||
@patch(_PATCH_GEM)
|
||||
@patch(_PATCH_VG_ALKIS)
|
||||
@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, {}, None)
|
||||
mock_gem.return_value = [("1234", "Muster")]
|
||||
mock_prog.return_value = _make_progress_mock()
|
||||
|
||||
with patch(_PATCH_FLST, return_value=(None, LoadStatus.NONE)):
|
||||
enriched, flst, bror, status = verfahrensgebiet_alkis_komplett(None)
|
||||
|
||||
self.assertIsNone(enriched)
|
||||
self.assertIsNone(flst)
|
||||
self.assertEqual(status, LoadStatus.NONE)
|
||||
|
||||
@patch(_PATCH_PROGRESS)
|
||||
@patch(_PATCH_MSGBOX)
|
||||
@patch(_PATCH_SELECT)
|
||||
@patch(_PATCH_PROJECT)
|
||||
@patch(_PATCH_GEM)
|
||||
@patch(_PATCH_FLST)
|
||||
@patch(_PATCH_VG_ALKIS)
|
||||
@patch(_PATCH_GET_VAR, return_value="VF-2024-001")
|
||||
def test_no_selection_returns_none_enriched(
|
||||
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, {}, None)
|
||||
mock_gem.return_value = []
|
||||
mock_flst.return_value = (flst_layer, LoadStatus.FIRST)
|
||||
mock_select.return_value = 0
|
||||
mock_prog.return_value = _make_progress_mock()
|
||||
|
||||
enriched, flst, bror, status = verfahrensgebiet_alkis_komplett(None)
|
||||
|
||||
self.assertIsNone(enriched)
|
||||
self.assertIs(flst, flst_layer)
|
||||
self.assertIsNone(bror)
|
||||
self.assertEqual(status, LoadStatus.NONE)
|
||||
mock_msgbox.warning.assert_called_once()
|
||||
|
||||
@patch(_PATCH_PROGRESS)
|
||||
@patch(_PATCH_MSGBOX)
|
||||
@patch(_PATCH_ERSTELLE)
|
||||
@patch(_PATCH_SELECT)
|
||||
@patch(_PATCH_PROJECT)
|
||||
@patch(_PATCH_GEM)
|
||||
@patch(_PATCH_FLST)
|
||||
@patch(_PATCH_VG_ALKIS)
|
||||
@patch(_PATCH_GET_VAR, return_value="VF-2024-001")
|
||||
def test_invalid_enriched_layer_returns_none(
|
||||
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, {}, None)
|
||||
mock_gem.return_value = []
|
||||
mock_flst.return_value = (flst_layer, LoadStatus.FIRST)
|
||||
mock_select.return_value = 5
|
||||
mock_erstelle.return_value = (bad_enriched, 0, 5)
|
||||
mock_prog.return_value = _make_progress_mock()
|
||||
|
||||
enriched, flst, bror, status = verfahrensgebiet_alkis_komplett(None)
|
||||
|
||||
self.assertIsNone(enriched)
|
||||
self.assertIs(flst, flst_layer)
|
||||
self.assertIsNone(bror)
|
||||
self.assertEqual(status, LoadStatus.NONE)
|
||||
mock_msgbox.critical.assert_called_once()
|
||||
|
||||
@patch(_PATCH_PROGRESS)
|
||||
@patch(_PATCH_PROJECT)
|
||||
@patch(_PATCH_VG_ALKIS)
|
||||
@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, {}, None)
|
||||
|
||||
prog = MagicMock()
|
||||
prog.is_canceled.side_effect = [False, True] # 1. Aufruf False, 2. True
|
||||
mock_prog.return_value = prog
|
||||
|
||||
enriched, flst, bror, status = verfahrensgebiet_alkis_komplett(None)
|
||||
|
||||
self.assertIsNone(enriched)
|
||||
self.assertEqual(status, LoadStatus.NONE)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Flächenvergleich – Übereinstimmung
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
@patch(_PATCH_PROGRESS)
|
||||
@patch(_PATCH_SHOW_INFO)
|
||||
@patch(_PATCH_VERGLEICHE)
|
||||
@patch(_PATCH_VKZ)
|
||||
@patch(_PATCH_GET_VAR)
|
||||
@patch(_PATCH_ERSTELLE)
|
||||
@patch(_PATCH_SELECT)
|
||||
@patch(_PATCH_PROJECT)
|
||||
@patch(_PATCH_GEM)
|
||||
@patch(_PATCH_FLST)
|
||||
@patch(_PATCH_VG_ALKIS)
|
||||
def test_success_flaechen_match_zeigt_info(
|
||||
self, mock_vg, mock_flst, mock_gem, mock_proj, mock_select,
|
||||
mock_erstelle, mock_get_var, mock_vkz, mock_vergl, mock_info, mock_prog,
|
||||
):
|
||||
"""Flächenübereinstimmung → show_info_dialog mit erwartetem Text."""
|
||||
proj_inst = _DummyProject()
|
||||
mock_proj.instance.return_value = proj_inst
|
||||
|
||||
vg_layer = _make_layer(name="BauRaumOrdnungsrecht (ALKIS)")
|
||||
flst_layer = _make_layer(name="Flurstücke", lid="FLST1")
|
||||
enriched = _make_layer(name="Verfahrensgebiet", lid="VG_ENRICHED", valid=True)
|
||||
|
||||
mock_vg.return_value = (vg_layer, LoadStatus.FIRST, {}, None)
|
||||
mock_gem.return_value = [("1234", "Muster")]
|
||||
mock_flst.return_value = (flst_layer, LoadStatus.FIRST)
|
||||
mock_select.return_value = 3
|
||||
mock_erstelle.return_value = (enriched, 3, 0)
|
||||
mock_get_var.return_value = "VF-2024-001"
|
||||
mock_vergl.return_value = (1234.5, 1234.5, True)
|
||||
mock_prog.return_value = _make_progress_mock()
|
||||
|
||||
result_enriched, result_flst, result_bror, status = verfahrensgebiet_alkis_komplett(None)
|
||||
|
||||
self.assertIs(result_enriched, enriched)
|
||||
self.assertIs(result_flst, flst_layer)
|
||||
self.assertIs(result_bror, vg_layer)
|
||||
self.assertEqual(status, LoadStatus.FIRST)
|
||||
mock_vkz.assert_called_once_with(enriched, "VF-2024-001")
|
||||
mock_info.assert_called_once()
|
||||
self.assertIn("erwarteter Geometrie", mock_info.call_args[0][1])
|
||||
|
||||
@patch(_PATCH_PROGRESS)
|
||||
@patch(_PATCH_SHOW_INFO)
|
||||
@patch(_PATCH_VERGLEICHE)
|
||||
@patch(_PATCH_VKZ)
|
||||
@patch(_PATCH_GET_VAR)
|
||||
@patch(_PATCH_ERSTELLE)
|
||||
@patch(_PATCH_SELECT)
|
||||
@patch(_PATCH_PROJECT)
|
||||
@patch(_PATCH_GEM)
|
||||
@patch(_PATCH_FLST)
|
||||
@patch(_PATCH_VG_ALKIS)
|
||||
def test_no_vkz_variable_passes_empty_string(
|
||||
self, mock_vg, mock_flst, mock_gem, mock_proj, mock_select,
|
||||
mock_erstelle, mock_get_var, mock_vkz, mock_vergl, mock_info, mock_prog,
|
||||
):
|
||||
mock_proj.instance.return_value = _DummyProject()
|
||||
vg_layer = _make_layer(name="BauRaumOrdnungsrecht (ALKIS)")
|
||||
flst_layer = _make_layer(name="Flurstücke")
|
||||
enriched = _make_layer(name="Verfahrensgebiet", valid=True)
|
||||
|
||||
mock_vg.return_value = (vg_layer, LoadStatus.FIRST, {}, 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 = "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, "VF-2024-001")
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Flächenvergleich – Abweichung + Abbrechen
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
@patch(_PATCH_PROGRESS)
|
||||
@patch(_PATCH_KNICK)
|
||||
@patch(_PATCH_ASK_DIALOG)
|
||||
@patch(_PATCH_VERGLEICHE)
|
||||
@patch(_PATCH_VKZ)
|
||||
@patch(_PATCH_GET_VAR)
|
||||
@patch(_PATCH_ERSTELLE)
|
||||
@patch(_PATCH_SELECT)
|
||||
@patch(_PATCH_PROJECT)
|
||||
@patch(_PATCH_GEM)
|
||||
@patch(_PATCH_FLST)
|
||||
@patch(_PATCH_VG_ALKIS)
|
||||
def test_flaechen_abweichung_abbrechen_kein_detailcheck(
|
||||
self, mock_vg, mock_flst, mock_gem, mock_proj, mock_select,
|
||||
mock_erstelle, mock_get_var, mock_vkz, mock_vergl, mock_ask, mock_knick, mock_prog,
|
||||
):
|
||||
"""Abweichung + Abbrechen → knickpunkt_pruefung wird NICHT aufgerufen."""
|
||||
mock_proj.instance.return_value = _DummyProject()
|
||||
vg_layer = _make_layer(name="BauRaumOrdnungsrecht (ALKIS)")
|
||||
flst_layer = _make_layer(name="Flurstücke")
|
||||
enriched = _make_layer(name="Verfahrensgebiet", valid=True)
|
||||
|
||||
mock_vg.return_value = (vg_layer, LoadStatus.FIRST, {}, 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 = "VF-2024-001"
|
||||
mock_vergl.return_value = (1000.0, 800.0, False)
|
||||
mock_ask.return_value = "abbrechen"
|
||||
mock_prog.return_value = _make_progress_mock()
|
||||
|
||||
result_enriched, _, _, status = verfahrensgebiet_alkis_komplett(None)
|
||||
|
||||
mock_knick.assert_not_called()
|
||||
self.assertIs(result_enriched, enriched)
|
||||
self.assertEqual(status, LoadStatus.FIRST)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Flächenvergleich – Abweichung + Als VG laden
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
@patch(_PATCH_PROGRESS)
|
||||
@patch(_PATCH_KNICK)
|
||||
@patch(_PATCH_ASK_DIALOG)
|
||||
@patch(_PATCH_VERGLEICHE)
|
||||
@patch(_PATCH_VKZ)
|
||||
@patch(_PATCH_GET_VAR)
|
||||
@patch(_PATCH_ERSTELLE)
|
||||
@patch(_PATCH_SELECT)
|
||||
@patch(_PATCH_PROJECT)
|
||||
@patch(_PATCH_GEM)
|
||||
@patch(_PATCH_FLST)
|
||||
@patch(_PATCH_VG_ALKIS)
|
||||
def test_flaechen_abweichung_als_vg_laden(
|
||||
self, mock_vg, mock_flst, mock_gem, mock_proj, mock_select,
|
||||
mock_erstelle, mock_get_var, mock_vkz, mock_vergl, mock_ask, mock_knick, mock_prog,
|
||||
):
|
||||
"""Abweichung + Als VG laden → BROR-Layer wird umbenannt und als
|
||||
enriched_layer zurückgegeben; flst_layer und bror_layer sind None."""
|
||||
proj_inst = _DummyProject()
|
||||
mock_proj.instance.return_value = proj_inst
|
||||
|
||||
vg_layer = _make_layer(name="BauRaumOrdnungsrecht (ALKIS)", lid="BROR_VG")
|
||||
flst_layer = _make_layer(name="Flurstücke")
|
||||
enriched = _make_layer(name="Verfahrensgebiet", valid=True)
|
||||
|
||||
mock_vg.return_value = (vg_layer, LoadStatus.FIRST, {}, 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 = "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()
|
||||
|
||||
result_enriched, result_flst, result_bror, status = verfahrensgebiet_alkis_komplett(None)
|
||||
|
||||
mock_knick.assert_not_called()
|
||||
# BROR-Layer wird als enriched_layer zurückgegeben
|
||||
self.assertIs(result_enriched, vg_layer)
|
||||
self.assertIsNone(result_flst)
|
||||
self.assertIsNone(result_bror)
|
||||
self.assertEqual(status, LoadStatus.FIRST)
|
||||
# Name wurde auf VERF_LAYER_NAME geändert
|
||||
vg_layer.setName.assert_called()
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Flächenvergleich – Abweichung + Detailprüfung
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
@patch(_PATCH_PROGRESS)
|
||||
@patch(_PATCH_SHOW_INFO)
|
||||
@patch(_PATCH_EXTRAHIERE)
|
||||
@patch(_PATCH_KNICK)
|
||||
@patch(_PATCH_GP_ALKIS)
|
||||
@patch(_PATCH_ASK_DIALOG)
|
||||
@patch(_PATCH_VERGLEICHE)
|
||||
@patch(_PATCH_VKZ)
|
||||
@patch(_PATCH_GET_VAR)
|
||||
@patch(_PATCH_ERSTELLE)
|
||||
@patch(_PATCH_SELECT)
|
||||
@patch(_PATCH_PROJECT)
|
||||
@patch(_PATCH_GEM)
|
||||
@patch(_PATCH_FLST)
|
||||
@patch(_PATCH_VG_ALKIS)
|
||||
def test_flaechen_abweichung_detailpruefung_startet_knickpruefung(
|
||||
self, mock_vg, mock_flst, mock_gem, mock_proj, mock_select,
|
||||
mock_erstelle, mock_get_var, mock_vkz, mock_vergl, mock_ask,
|
||||
mock_gp, mock_knick, mock_extrahiere, mock_info, mock_prog,
|
||||
):
|
||||
"""Abweichung + Detailprüfung → lade_punktort_ta + knickpunkt_pruefung
|
||||
aufgerufen, Ergebnislayer ins Projekt, Abschlussmeldung mit Zählung."""
|
||||
proj_inst = _DummyProject()
|
||||
mock_proj.instance.return_value = proj_inst
|
||||
|
||||
vg_layer = _make_layer(name="BauRaumOrdnungsrecht (ALKIS)")
|
||||
flst_layer = _make_layer(name="Flurstücke")
|
||||
enriched = _make_layer(name="Verfahrensgebiet", valid=True)
|
||||
punktort_layer = _make_layer(name="PunktortTA", lid="PTA_L")
|
||||
ergebnis_layer = _make_layer(name="Knickpunktprüfung", lid="KNICK_L")
|
||||
|
||||
mock_vg.return_value = (vg_layer, LoadStatus.FIRST, {}, 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 = "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
|
||||
mock_gp.return_value = punktort_layer
|
||||
mock_knick.return_value = (ergebnis_layer, 2, 1, 0)
|
||||
mock_prog.return_value = _make_progress_mock()
|
||||
|
||||
result_enriched, _, _, status = verfahrensgebiet_alkis_komplett(None)
|
||||
|
||||
mock_gp.assert_called_once()
|
||||
mock_knick.assert_called_once()
|
||||
self.assertIn(ergebnis_layer, proj_inst.added)
|
||||
mock_info.assert_called_once()
|
||||
# Abschlussmeldung muss Zahlen enthalten
|
||||
info_text = mock_info.call_args[0][1]
|
||||
self.assertIn("3", info_text) # Anzahl Knickpunkte
|
||||
self.assertIn("2", info_text) # identisch
|
||||
self.assertIn("1", info_text) # kein_pta
|
||||
self.assertEqual(status, LoadStatus.FIRST)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Cleanup-Verhalten
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
@patch(_PATCH_PROGRESS)
|
||||
@patch(_PATCH_SHOW_INFO)
|
||||
@patch(_PATCH_VERGLEICHE)
|
||||
@patch(_PATCH_VKZ)
|
||||
@patch(_PATCH_GET_VAR)
|
||||
@patch(_PATCH_ERSTELLE)
|
||||
@patch(_PATCH_SELECT)
|
||||
@patch(_PATCH_PROJECT)
|
||||
@patch(_PATCH_GEM)
|
||||
@patch(_PATCH_FLST)
|
||||
@patch(_PATCH_VG_ALKIS)
|
||||
def test_bror_layer_bleibt_bei_erfolg_im_projekt(
|
||||
self, mock_vg, mock_flst, mock_gem, mock_proj, mock_select,
|
||||
mock_erstelle, mock_get_var, mock_vkz, mock_vergl, mock_info, mock_prog,
|
||||
):
|
||||
"""Bei Erfolg darf der BROR-Layer NICHT entfernt werden."""
|
||||
proj_inst = _DummyProject()
|
||||
mock_proj.instance.return_value = proj_inst
|
||||
|
||||
vg_layer = _make_layer(name="BauRaumOrdnungsrecht (ALKIS)", lid="BROR_ID")
|
||||
flst_layer = _make_layer(name="Flurstücke")
|
||||
enriched = _make_layer(name="Verfahrensgebiet", valid=True)
|
||||
|
||||
mock_vg.return_value = (vg_layer, LoadStatus.FIRST, {}, 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 = "VF-2024-001"
|
||||
mock_vergl.return_value = (1000.0, 1000.0, True)
|
||||
mock_prog.return_value = _make_progress_mock()
|
||||
|
||||
verfahrensgebiet_alkis_komplett(None)
|
||||
|
||||
self.assertNotIn("BROR_ID", proj_inst.removed)
|
||||
|
||||
@patch(_PATCH_PROGRESS)
|
||||
@patch(_PATCH_PROJECT)
|
||||
@patch(_PATCH_GEM)
|
||||
@patch(_PATCH_VG_ALKIS)
|
||||
@patch(_PATCH_GET_VAR, return_value="VF-2024-001")
|
||||
def test_bror_layer_wird_bei_fehler_entfernt(
|
||||
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, {}, None)
|
||||
mock_gem.return_value = []
|
||||
mock_prog.return_value = _make_progress_mock()
|
||||
|
||||
with patch(_PATCH_FLST, return_value=(None, LoadStatus.NONE)):
|
||||
verfahrensgebiet_alkis_komplett(None)
|
||||
|
||||
self.assertIn("BROR_ERR", proj_inst.removed)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# 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,595 @@
|
||||
"""sn_verfahrensgebiet/tests/test_verfahrensgebiet_edit_service.py
|
||||
|
||||
Unit-Tests für das Verfahrensgebiet-Edit-Service-Modul.
|
||||
|
||||
Getestet werden:
|
||||
- pruefe_flurstueck_layer (über Layerpruefer-Integration)
|
||||
- pruefe_auswahl
|
||||
- erstelle_verfahrensgebiet_aus_auswahl: Erfolgs- und Fehlerpfade
|
||||
- ergaenze_verfahrensgebiet: nur neue IDs werden hinzugefügt
|
||||
- entferne_aus_verfahrensgebiet: nur vorhandene IDs werden entfernt
|
||||
- ersetze_verfahrensgebiet: Ergebnis entspricht Erstellen
|
||||
- get_kennzeichen_menge: Hilfsfunktion
|
||||
- Zähler (erfolgreich/fehlgeschlagen/bereits_vorhanden/nicht_enthalten)
|
||||
"""
|
||||
|
||||
import sys
|
||||
import types
|
||||
import unittest
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Minimales QGIS-Mock-Setup (muss vor allen sn_*-Importen erfolgen)
|
||||
# ---------------------------------------------------------------------------
|
||||
if "qgis" not in sys.modules:
|
||||
qgis_mod = types.ModuleType("qgis")
|
||||
qgis_core = types.ModuleType("qgis.core")
|
||||
qgis_utils = types.ModuleType("qgis.utils")
|
||||
qgis_utils.iface = types.SimpleNamespace()
|
||||
qgis_utils.plugins = {}
|
||||
qgis_mod.utils = qgis_utils
|
||||
qgis_mod.core = qgis_core
|
||||
sys.modules["qgis"] = qgis_mod
|
||||
sys.modules["qgis.core"] = qgis_core
|
||||
sys.modules["qgis.utils"] = qgis_utils
|
||||
|
||||
from sn_verfahrensgebiet.functions.verfahrensgebiet_edit_service import (
|
||||
pruefe_flurstueck_layer,
|
||||
pruefe_auswahl,
|
||||
erstelle_verfahrensgebiet_aus_auswahl,
|
||||
ergaenze_verfahrensgebiet,
|
||||
entferne_aus_verfahrensgebiet,
|
||||
ersetze_verfahrensgebiet,
|
||||
get_kennzeichen_menge,
|
||||
FELD_KENNZEICHEN,
|
||||
FELD_GEMARKUNG,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Test-Dummies
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class _DummyGeom:
|
||||
def __init__(self, empty=False):
|
||||
self._empty = empty
|
||||
|
||||
def isEmpty(self):
|
||||
return self._empty
|
||||
|
||||
def difference(self, other):
|
||||
"""Dummy-Differenz: immer nicht-leeres Ergebnis (kein echter Geometrieschnitt)."""
|
||||
return _DummyGeom(empty=False)
|
||||
|
||||
|
||||
class _DummyFeature:
|
||||
"""Feature-Dummy mit dict-artigem Attributzugriff und konfigurierbarer Geometrie."""
|
||||
|
||||
def __init__(self, kz: str, gemarkung: str = "1234", geom_empty: bool = False, feat_id: int = 0):
|
||||
self._attrs = {FELD_KENNZEICHEN: kz, FELD_GEMARKUNG: gemarkung}
|
||||
self._geom_empty = geom_empty
|
||||
self._id = feat_id
|
||||
self._set_geom = None
|
||||
self._set_attrs = None
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self._attrs.get(key, None)
|
||||
|
||||
def geometry(self):
|
||||
return _DummyGeom(self._geom_empty)
|
||||
|
||||
def id(self):
|
||||
return self._id
|
||||
|
||||
def setGeometry(self, g):
|
||||
self._set_geom = g
|
||||
|
||||
def setAttributes(self, attrs):
|
||||
self._set_attrs = attrs
|
||||
|
||||
|
||||
class _DummyProvider:
|
||||
"""DataProvider-Dummy für Single-Feature-Verfahrensgebiet-Layer."""
|
||||
|
||||
def __init__(self, layer_ref):
|
||||
self._layer = layer_ref
|
||||
|
||||
def addAttributes(self, attrs):
|
||||
pass
|
||||
|
||||
def addFeatures(self, feats):
|
||||
self._layer._features.extend(feats)
|
||||
return True
|
||||
|
||||
def deleteFeatures(self, ids):
|
||||
self._layer._deleted_ids.extend(ids)
|
||||
return True
|
||||
|
||||
def changeAttributeValues(self, changes: dict):
|
||||
"""changes: {fid: {field_idx: value}}"""
|
||||
for feat in self._layer._features:
|
||||
fid = feat.id() if callable(getattr(feat, "id", None)) else None
|
||||
if fid in changes:
|
||||
for field_idx, value in changes[fid].items():
|
||||
feat._attr_by_idx[field_idx] = value
|
||||
return True
|
||||
|
||||
def changeGeometryValues(self, changes: dict):
|
||||
"""changes: {fid: new_geom}"""
|
||||
for feat in self._layer._features:
|
||||
fid = feat.id() if callable(getattr(feat, "id", None)) else None
|
||||
if fid in changes:
|
||||
feat._set_geom = changes[fid]
|
||||
return True
|
||||
|
||||
|
||||
class _DummySingleFeature:
|
||||
"""Single-Feature-Dummy für den Verfahrensgebiet-Layer.
|
||||
|
||||
Speichert Kennzeichen als kommagetrennte Zeichenkette (new-style).
|
||||
Feld-Indizes: 0=FELD_KENNZEICHEN, 1=FELD_GEMARKUNG
|
||||
"""
|
||||
|
||||
def __init__(self, kz_komma: str, gemarkung: str = "", feat_id: int = 0, geom_empty: bool = False):
|
||||
self._kz_komma = kz_komma
|
||||
self._gemarkung = gemarkung
|
||||
self._id = feat_id
|
||||
self._geom_empty = geom_empty
|
||||
self._set_geom = None
|
||||
# Attribut-Mapping nach Index für changeAttributeValues
|
||||
self._attr_by_idx: dict = {}
|
||||
|
||||
def __getitem__(self, key):
|
||||
if key == FELD_KENNZEICHEN:
|
||||
return self._attr_by_idx.get(0, self._kz_komma)
|
||||
if key == FELD_GEMARKUNG:
|
||||
return self._attr_by_idx.get(1, self._gemarkung)
|
||||
return None
|
||||
|
||||
def geometry(self):
|
||||
return _DummyGeom(self._geom_empty)
|
||||
|
||||
def id(self):
|
||||
return self._id
|
||||
|
||||
def setGeometry(self, g):
|
||||
self._set_geom = g
|
||||
|
||||
def setAttributes(self, attrs):
|
||||
if len(attrs) > 0:
|
||||
self._kz_komma = attrs[0]
|
||||
if len(attrs) > 1:
|
||||
self._gemarkung = attrs[1]
|
||||
|
||||
|
||||
class _DummyVerfLayer:
|
||||
"""Memory-Layer-Dummy für Verfahrensgebiet (Single-Feature-Modell)."""
|
||||
|
||||
def __init__(self, single_feature=None):
|
||||
self._features = [single_feature] if single_feature else []
|
||||
self._deleted_ids = []
|
||||
|
||||
def getFeatures(self):
|
||||
deleted = set(self._deleted_ids)
|
||||
return [
|
||||
f for f in self._features
|
||||
if (f.id() if callable(getattr(f, "id", None)) else None) not in deleted
|
||||
]
|
||||
|
||||
def dataProvider(self):
|
||||
return _DummyProvider(self)
|
||||
|
||||
def fields(self):
|
||||
class _Fields:
|
||||
def indexFromName(self, name):
|
||||
if name == FELD_KENNZEICHEN:
|
||||
return 0
|
||||
if name == FELD_GEMARKUNG:
|
||||
return 1
|
||||
return -1
|
||||
return _Fields()
|
||||
|
||||
def updateExtents(self):
|
||||
pass
|
||||
|
||||
def triggerRepaint(self):
|
||||
pass
|
||||
|
||||
def updateFields(self):
|
||||
pass
|
||||
|
||||
def isValid(self):
|
||||
return True
|
||||
|
||||
def crs(self):
|
||||
class _Crs:
|
||||
def authid(self):
|
||||
return "EPSG:25833"
|
||||
return _Crs()
|
||||
|
||||
|
||||
class _DummySourceLayer:
|
||||
"""Flurstück-Quell-Layer-Dummy mit konfigurierbarer Auswahl."""
|
||||
|
||||
def __init__(self, selected=None, all_features=None, feature_count=None):
|
||||
self._selected = list(selected or [])
|
||||
self._all = list(all_features or [])
|
||||
self._count = feature_count if feature_count is not None else len(self._selected)
|
||||
|
||||
def selectedFeatures(self):
|
||||
return self._selected
|
||||
|
||||
def selectedFeatureCount(self):
|
||||
return self._count
|
||||
|
||||
def getFeatures(self):
|
||||
return self._all
|
||||
|
||||
def crs(self):
|
||||
class _Crs:
|
||||
def authid(self):
|
||||
return "EPSG:25833"
|
||||
return _Crs()
|
||||
|
||||
|
||||
class _MockLayerpruefer:
|
||||
"""Steuerbarer Mock für Layerpruefer.pruefe()."""
|
||||
|
||||
def __init__(self, ok, aktion="ok", meldung=""):
|
||||
self._ok = ok
|
||||
self._aktion = aktion
|
||||
self._meldung = meldung
|
||||
|
||||
def pruefe(self):
|
||||
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
|
||||
return pruef_ergebnis(ok=self._ok, aktion=self._aktion, meldung=self._meldung)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests: pruefe_flurstueck_layer
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestPruefeFlurstueckLayer(unittest.TestCase):
|
||||
|
||||
def test_layer_ok_passes(self):
|
||||
"""Gültiger Polygon-Vektor-Layer mit Pflichtfeldern → ok=True."""
|
||||
import sn_verfahrensgebiet.functions.verfahrensgebiet_edit_service as module
|
||||
original = module.Layerpruefer
|
||||
|
||||
class _PassPruefer:
|
||||
def __init__(self, **_):
|
||||
pass
|
||||
def pruefe(self):
|
||||
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
|
||||
return pruef_ergebnis(ok=True, aktion="ok")
|
||||
|
||||
module.Layerpruefer = _PassPruefer
|
||||
try:
|
||||
ergebnis = pruefe_flurstueck_layer(object())
|
||||
self.assertTrue(ergebnis.ok)
|
||||
self.assertEqual(ergebnis.aktion, "ok")
|
||||
finally:
|
||||
module.Layerpruefer = original
|
||||
|
||||
def test_layer_missing_field_fails(self):
|
||||
"""Layer ohne Pflichtfelder → ok=False, aktion=felder_fehlen."""
|
||||
import sn_verfahrensgebiet.functions.verfahrensgebiet_edit_service as module
|
||||
original = module.Layerpruefer
|
||||
|
||||
class _FailPruefer:
|
||||
def __init__(self, **_):
|
||||
pass
|
||||
def pruefe(self):
|
||||
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
|
||||
return pruef_ergebnis(ok=False, aktion="felder_fehlen",
|
||||
meldung="Feld fehlt")
|
||||
|
||||
module.Layerpruefer = _FailPruefer
|
||||
try:
|
||||
ergebnis = pruefe_flurstueck_layer(object())
|
||||
self.assertFalse(ergebnis.ok)
|
||||
self.assertEqual(ergebnis.aktion, "felder_fehlen")
|
||||
finally:
|
||||
module.Layerpruefer = original
|
||||
|
||||
def test_layer_wrong_geometry_fails(self):
|
||||
"""Layer mit falschem Geometrietyp → ok=False, aktion=falscher_geotyp."""
|
||||
import sn_verfahrensgebiet.functions.verfahrensgebiet_edit_service as module
|
||||
original = module.Layerpruefer
|
||||
|
||||
class _WrongGeomPruefer:
|
||||
def __init__(self, **_):
|
||||
pass
|
||||
def pruefe(self):
|
||||
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
|
||||
return pruef_ergebnis(ok=False, aktion="falscher_geotyp",
|
||||
meldung="Kein Polygon")
|
||||
|
||||
module.Layerpruefer = _WrongGeomPruefer
|
||||
try:
|
||||
ergebnis = pruefe_flurstueck_layer(object())
|
||||
self.assertFalse(ergebnis.ok)
|
||||
self.assertEqual(ergebnis.aktion, "falscher_geotyp")
|
||||
finally:
|
||||
module.Layerpruefer = original
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests: pruefe_auswahl
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestPruefeAuswahl(unittest.TestCase):
|
||||
|
||||
def test_no_selection_fails(self):
|
||||
layer = _DummySourceLayer(selected=[], feature_count=0)
|
||||
ergebnis = pruefe_auswahl(layer)
|
||||
self.assertFalse(ergebnis.ok)
|
||||
self.assertEqual(ergebnis.aktion, "layer_leer")
|
||||
|
||||
def test_with_selection_passes(self):
|
||||
feats = [_DummyFeature("1000-001")]
|
||||
layer = _DummySourceLayer(selected=feats)
|
||||
ergebnis = pruefe_auswahl(layer)
|
||||
self.assertTrue(ergebnis.ok)
|
||||
self.assertIn("1", ergebnis.meldung) # "1 Flurstücke ausgewählt."
|
||||
|
||||
def test_selectedFeatureCount_exception_treated_as_zero(self):
|
||||
class _BrokenLayer:
|
||||
def selectedFeatureCount(self):
|
||||
raise RuntimeError("broken")
|
||||
ergebnis = pruefe_auswahl(_BrokenLayer())
|
||||
self.assertFalse(ergebnis.ok)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests: get_kennzeichen_menge
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestGetKennzeichenMenge(unittest.TestCase):
|
||||
|
||||
def test_returns_set_of_ids(self):
|
||||
"""Kommagetrennte Kennzeichen im Single-Feature werden korrekt geparst."""
|
||||
feat = _DummySingleFeature("A001,A002,A001", feat_id=1) # Duplikat → set
|
||||
layer = _DummyVerfLayer(feat)
|
||||
result = get_kennzeichen_menge(layer)
|
||||
self.assertEqual(result, {"A001", "A002"})
|
||||
|
||||
def test_empty_layer_returns_empty_set(self):
|
||||
layer = _DummyVerfLayer()
|
||||
result = get_kennzeichen_menge(layer)
|
||||
self.assertEqual(result, set())
|
||||
|
||||
def test_single_kennzeichen_without_comma(self):
|
||||
"""Einzelnes Kennzeichen ohne Komma wird korrekt erkannt."""
|
||||
feat = _DummySingleFeature("X999", feat_id=1)
|
||||
layer = _DummyVerfLayer(feat)
|
||||
result = get_kennzeichen_menge(layer)
|
||||
self.assertEqual(result, {"X999"})
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests: erstelle_verfahrensgebiet_aus_auswahl
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestErstelle(unittest.TestCase):
|
||||
|
||||
def _patch_all(self, test_func, union_empty=False):
|
||||
"""Patcht _erstelle_memory_layer und QgsGeometry.unaryUnion."""
|
||||
import sn_verfahrensgebiet.functions.verfahrensgebiet_edit_service as module
|
||||
orig_erstelle = module._erstelle_memory_layer
|
||||
orig_union = module.QgsGeometry.unaryUnion
|
||||
|
||||
mem = _DummyVerfLayer()
|
||||
module._erstelle_memory_layer = lambda _crs: mem
|
||||
module.QgsGeometry.unaryUnion = staticmethod(
|
||||
lambda _geoms: _DummyGeom(empty=union_empty)
|
||||
)
|
||||
try:
|
||||
test_func(mem)
|
||||
finally:
|
||||
module._erstelle_memory_layer = orig_erstelle
|
||||
module.QgsGeometry.unaryUnion = staticmethod(orig_union)
|
||||
|
||||
def test_success_counts(self):
|
||||
"""3 gültige Features → erfolgreich=3, fehlgeschlagen=0, 1 Union-Feature im Layer."""
|
||||
feats = [
|
||||
_DummyFeature("A001", feat_id=0),
|
||||
_DummyFeature("A002", feat_id=1),
|
||||
_DummyFeature("A003", feat_id=2),
|
||||
]
|
||||
source = _DummySourceLayer(selected=feats)
|
||||
|
||||
def run(mem):
|
||||
layer, ok, fail = erstelle_verfahrensgebiet_aus_auswahl(source)
|
||||
self.assertIs(layer, mem)
|
||||
self.assertEqual(ok, 3)
|
||||
self.assertEqual(fail, 0)
|
||||
# Single-Feature-Modell: genau 1 Union-Feature
|
||||
self.assertEqual(len(mem._features), 1)
|
||||
|
||||
self._patch_all(run)
|
||||
|
||||
def test_partial_failure_on_empty_geometry(self):
|
||||
"""2 ok + 1 leere Geometrie → erfolgreich=2, fehlgeschlagen=1."""
|
||||
feats = [
|
||||
_DummyFeature("A001"),
|
||||
_DummyFeature("A002", geom_empty=True),
|
||||
_DummyFeature("A003"),
|
||||
]
|
||||
source = _DummySourceLayer(selected=feats)
|
||||
|
||||
def run(mem):
|
||||
_, ok, fail = erstelle_verfahrensgebiet_aus_auswahl(source)
|
||||
self.assertEqual(ok, 2)
|
||||
self.assertEqual(fail, 1)
|
||||
|
||||
self._patch_all(run)
|
||||
|
||||
def test_all_fail_on_empty_geometries(self):
|
||||
"""Alle Features haben leere Geometrie → erfolgreich=0, kein Feature im Layer."""
|
||||
feats = [_DummyFeature("A001", geom_empty=True)]
|
||||
source = _DummySourceLayer(selected=feats)
|
||||
|
||||
def run(mem):
|
||||
_, ok, fail = erstelle_verfahrensgebiet_aus_auswahl(source)
|
||||
self.assertEqual(ok, 0)
|
||||
self.assertEqual(fail, 1)
|
||||
self.assertEqual(len(mem._features), 0) # kein Union-Feature
|
||||
|
||||
self._patch_all(run)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests: ergaenze_verfahrensgebiet
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestErgaenzeVerfahrensgebiet(unittest.TestCase):
|
||||
|
||||
def _patch_union(self):
|
||||
import sn_verfahrensgebiet.functions.verfahrensgebiet_edit_service as module
|
||||
orig = module.QgsGeometry.unaryUnion
|
||||
module.QgsGeometry.unaryUnion = staticmethod(lambda _g: _DummyGeom(empty=False))
|
||||
return module, orig
|
||||
|
||||
def test_adds_only_new_ids(self):
|
||||
"""Auswahl enthält 1 neue + 1 bereits vorhandene ID → hinzugefügt=1."""
|
||||
verf = _DummyVerfLayer(_DummySingleFeature("A001", feat_id=1))
|
||||
selected = [
|
||||
_DummyFeature("A001"), # bereits vorhanden
|
||||
_DummyFeature("A002"), # neu
|
||||
]
|
||||
source = _DummySourceLayer(selected=selected)
|
||||
module, orig = self._patch_union()
|
||||
try:
|
||||
_, hinzugefuegt, bereits_vorhanden, fehlgeschlagen = ergaenze_verfahrensgebiet(
|
||||
verf, source
|
||||
)
|
||||
finally:
|
||||
module.QgsGeometry.unaryUnion = staticmethod(orig)
|
||||
self.assertEqual(hinzugefuegt, 1)
|
||||
self.assertEqual(bereits_vorhanden, 1)
|
||||
self.assertEqual(fehlgeschlagen, 0)
|
||||
|
||||
def test_skip_empty_geometry_counts_as_failed(self):
|
||||
"""Neue ID mit leerer Geometrie → fehlgeschlagen=1, hinzugefügt=0."""
|
||||
verf = _DummyVerfLayer() # leer
|
||||
selected = [_DummyFeature("A001", geom_empty=True)]
|
||||
source = _DummySourceLayer(selected=selected)
|
||||
|
||||
_, hinzugefuegt, _, fehlgeschlagen = ergaenze_verfahrensgebiet(verf, source)
|
||||
self.assertEqual(hinzugefuegt, 0)
|
||||
self.assertEqual(fehlgeschlagen, 1)
|
||||
|
||||
def test_all_already_present(self):
|
||||
"""Alle ausgewählten IDs bereits vorhanden → bereits_vorhanden=n, hinzugefügt=0."""
|
||||
verf = _DummyVerfLayer(_DummySingleFeature("A001,A002", feat_id=1))
|
||||
selected = [_DummyFeature("A001"), _DummyFeature("A002")]
|
||||
source = _DummySourceLayer(selected=selected)
|
||||
|
||||
_, hinzugefuegt, bereits_vorhanden, fehlgeschlagen = ergaenze_verfahrensgebiet(
|
||||
verf, source
|
||||
)
|
||||
self.assertEqual(hinzugefuegt, 0)
|
||||
self.assertEqual(bereits_vorhanden, 2)
|
||||
self.assertEqual(fehlgeschlagen, 0)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests: entferne_aus_verfahrensgebiet
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestEntferneAusVerfahrensgebiet(unittest.TestCase):
|
||||
|
||||
def test_removes_present_ids(self):
|
||||
"""Auswahl enthält 1 vorhandene ID → entfernt=1, nicht_enthalten=0."""
|
||||
verf = _DummyVerfLayer(_DummySingleFeature("A001,A002", feat_id=1))
|
||||
selected = [_DummyFeature("A001")]
|
||||
source = _DummySourceLayer(selected=selected)
|
||||
|
||||
_, entfernt, nicht_enthalten, fehlgeschlagen = entferne_aus_verfahrensgebiet(
|
||||
verf, source
|
||||
)
|
||||
self.assertEqual(entfernt, 1)
|
||||
self.assertEqual(nicht_enthalten, 0)
|
||||
self.assertEqual(fehlgeschlagen, 0)
|
||||
|
||||
def test_counts_ids_not_in_layer(self):
|
||||
"""Auswahl enthält ID, die nicht im Verfahrensgebiet ist → nicht_enthalten=1."""
|
||||
verf = _DummyVerfLayer(_DummySingleFeature("A001", feat_id=1))
|
||||
selected = [
|
||||
_DummyFeature("A001"), # vorhanden
|
||||
_DummyFeature("A999"), # nicht enthalten
|
||||
]
|
||||
source = _DummySourceLayer(selected=selected)
|
||||
|
||||
_, entfernt, nicht_enthalten, fehlgeschlagen = entferne_aus_verfahrensgebiet(
|
||||
verf, source
|
||||
)
|
||||
self.assertEqual(entfernt, 1)
|
||||
self.assertEqual(nicht_enthalten, 1)
|
||||
|
||||
def test_none_present_in_layer(self):
|
||||
"""Auswahl hat IDs, die alle nicht im Layer sind → entfernt=0, nicht_enthalten=n."""
|
||||
verf = _DummyVerfLayer(_DummySingleFeature("X001", feat_id=1))
|
||||
selected = [_DummyFeature("A999"), _DummyFeature("B888")]
|
||||
source = _DummySourceLayer(selected=selected)
|
||||
|
||||
_, entfernt, nicht_enthalten, fehlgeschlagen = entferne_aus_verfahrensgebiet(
|
||||
verf, source
|
||||
)
|
||||
self.assertEqual(entfernt, 0)
|
||||
self.assertEqual(nicht_enthalten, 2)
|
||||
|
||||
def test_empty_selection_removes_nothing(self):
|
||||
"""Leere Auswahl → nichts entfernt, nichts nicht_enthalten."""
|
||||
verf = _DummyVerfLayer(_DummySingleFeature("A001", feat_id=1))
|
||||
source = _DummySourceLayer(selected=[])
|
||||
|
||||
_, entfernt, nicht_enthalten, fehlgeschlagen = entferne_aus_verfahrensgebiet(
|
||||
verf, source
|
||||
)
|
||||
self.assertEqual(entfernt, 0)
|
||||
self.assertEqual(nicht_enthalten, 0)
|
||||
|
||||
def test_removes_last_id_deletes_feature(self):
|
||||
"""Wird das letzte Kennzeichen entfernt, wird das Feature gelöscht."""
|
||||
verf = _DummyVerfLayer(_DummySingleFeature("A001", feat_id=1))
|
||||
selected = [_DummyFeature("A001")]
|
||||
source = _DummySourceLayer(selected=selected)
|
||||
|
||||
_, entfernt, _, _ = entferne_aus_verfahrensgebiet(verf, source)
|
||||
self.assertEqual(entfernt, 1)
|
||||
# Feature soll gelöscht worden sein
|
||||
self.assertIn(1, verf._deleted_ids)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests: ersetze_verfahrensgebiet
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestErsetze(unittest.TestCase):
|
||||
|
||||
def test_ersetze_delegates_to_erstelle(self):
|
||||
"""ersetze_verfahrensgebiet gibt dasselbe Ergebnis wie erstelle zurück."""
|
||||
feats = [_DummyFeature("A001")]
|
||||
source = _DummySourceLayer(selected=feats)
|
||||
|
||||
import sn_verfahrensgebiet.functions.verfahrensgebiet_edit_service as module
|
||||
orig_erstelle = module._erstelle_memory_layer
|
||||
orig_union = module.QgsGeometry.unaryUnion
|
||||
mem = _DummyVerfLayer()
|
||||
module._erstelle_memory_layer = lambda _crs: mem
|
||||
module.QgsGeometry.unaryUnion = staticmethod(lambda _g: _DummyGeom(empty=False))
|
||||
|
||||
try:
|
||||
layer, ok, fail = ersetze_verfahrensgebiet(source)
|
||||
self.assertIs(layer, mem)
|
||||
self.assertEqual(ok, 1)
|
||||
self.assertEqual(fail, 0)
|
||||
finally:
|
||||
module._erstelle_memory_layer = orig_erstelle
|
||||
module.QgsGeometry.unaryUnion = staticmethod(orig_union)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -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()
|
||||
@@ -0,0 +1,991 @@
|
||||
import sys
|
||||
import types
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
if "qgis" not in sys.modules:
|
||||
qgis_mod = types.ModuleType("qgis")
|
||||
qgis_utils = types.ModuleType("qgis.utils")
|
||||
qgis_utils.iface = types.SimpleNamespace(
|
||||
mapCanvas=lambda: types.SimpleNamespace(setExtent=lambda _e: None, refresh=lambda: None)
|
||||
)
|
||||
qgis_utils.plugins = {}
|
||||
qgis_mod.utils = qgis_utils
|
||||
sys.modules["qgis"] = qgis_mod
|
||||
sys.modules["qgis.utils"] = qgis_utils
|
||||
|
||||
from sn_verfahrensgebiet.ui.tabs.working_tab import WorkingTab, LoadStatus
|
||||
|
||||
|
||||
class _DummyLayer:
|
||||
def __init__(self, valid=True, layer_id="L1", layer_name="DummyLayer"):
|
||||
self._valid = valid
|
||||
self._id = layer_id
|
||||
self._name = layer_name
|
||||
|
||||
def isValid(self):
|
||||
return self._valid
|
||||
|
||||
def id(self):
|
||||
return self._id
|
||||
|
||||
def name(self):
|
||||
return self._name
|
||||
|
||||
def extent(self):
|
||||
return object()
|
||||
|
||||
|
||||
class _DummyProject:
|
||||
def __init__(self):
|
||||
self.added = []
|
||||
|
||||
def addMapLayer(self, layer):
|
||||
self.added.append(layer)
|
||||
|
||||
|
||||
class _DummyCanvas:
|
||||
def __init__(self):
|
||||
self.extent = None
|
||||
|
||||
def setExtent(self, extent):
|
||||
self.extent = extent
|
||||
|
||||
def refresh(self):
|
||||
return None
|
||||
|
||||
|
||||
class _DummyButton:
|
||||
def __init__(self):
|
||||
self._enabled = True
|
||||
self._text = "Aus ALKIS laden"
|
||||
|
||||
def setEnabled(self, enabled):
|
||||
self._enabled = enabled
|
||||
|
||||
def isEnabled(self):
|
||||
return self._enabled
|
||||
|
||||
def setText(self, text):
|
||||
self._text = text
|
||||
|
||||
def text(self):
|
||||
return self._text
|
||||
|
||||
|
||||
class _DummyCombo:
|
||||
def __init__(self):
|
||||
self.layer = None
|
||||
|
||||
def setLayer(self, layer):
|
||||
self.layer = layer
|
||||
|
||||
|
||||
class _DummyGemFeature:
|
||||
def __init__(self, schluessel, name):
|
||||
self._schluessel = schluessel
|
||||
self._name = name
|
||||
|
||||
def __getitem__(self, key):
|
||||
if key == "schluessel":
|
||||
return self._schluessel
|
||||
if key == "ave_name":
|
||||
return self._name
|
||||
return None
|
||||
|
||||
|
||||
class _DummyGemarkungenLayer:
|
||||
def __init__(self, selected):
|
||||
self._selected = list(selected)
|
||||
|
||||
def selectedFeatureCount(self):
|
||||
return len(self._selected)
|
||||
|
||||
def selectedFeatures(self):
|
||||
return list(self._selected)
|
||||
|
||||
|
||||
class TestWorkingTab(unittest.TestCase):
|
||||
|
||||
def _make_tab(self):
|
||||
tab = WorkingTab.__new__(WorkingTab)
|
||||
tab.btn_verf_alkis = _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.btn_flurst_aus_gemarkungen = _DummyButton()
|
||||
tab.setze_haken = lambda *_args, **_kwargs: None
|
||||
# Pruefmanager-Mock: verarbeite() wird im Test ggf. überschrieben
|
||||
tab.pruefmanager = types.SimpleNamespace(
|
||||
verarbeite=lambda ergebnis: ergebnis
|
||||
)
|
||||
return tab
|
||||
|
||||
def test_set_busy_updates_button_state(self):
|
||||
tab = self._make_tab()
|
||||
|
||||
tab._set_busy(tab.btn_verf_alkis, True)
|
||||
self.assertFalse(tab.btn_verf_alkis.isEnabled())
|
||||
self.assertEqual(tab.btn_verf_alkis.text(), "Lade ...")
|
||||
|
||||
tab._set_busy(tab.btn_verf_alkis, False)
|
||||
self.assertTrue(tab.btn_verf_alkis.isEnabled())
|
||||
self.assertEqual(tab.btn_verf_alkis.text(), "Aus ALKIS laden")
|
||||
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.set_verfahrensgebiet_layer_id")
|
||||
def test_on_verf_layer_changed_valid_sets_variable(self, mock_set_layer_id):
|
||||
tab = self._make_tab()
|
||||
layer = _DummyLayer(valid=True, layer_id="layer_123")
|
||||
|
||||
tab._on_verf_layer_changed(layer)
|
||||
|
||||
mock_set_layer_id.assert_called_once_with("layer_123")
|
||||
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.set_verfahrensgebiet_layer_id")
|
||||
def test_on_verf_layer_changed_none_clears_variable(self, mock_set_layer_id):
|
||||
tab = self._make_tab()
|
||||
|
||||
tab._on_verf_layer_changed(None)
|
||||
|
||||
mock_set_layer_id.assert_called_once_with("")
|
||||
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.success")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.apply_style")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.QgsProject.instance")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.verfahrensgebiet_alkis_komplett")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.iface")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.set_verfahrensgebiet_layer_id")
|
||||
def test_handle_verf_alkis_success_path(
|
||||
self,
|
||||
mock_set_layer_id,
|
||||
mock_iface,
|
||||
mock_alkis_komplett,
|
||||
mock_project_instance,
|
||||
mock_apply_style,
|
||||
mock_success,
|
||||
):
|
||||
"""verfahrensgebiet_alkis_komplett → 4-Tupel (enriched, flst, bror, FIRST)."""
|
||||
tab = self._make_tab()
|
||||
enriched = _DummyLayer(valid=True, layer_id="enriched_ok")
|
||||
flst = _DummyLayer(valid=True, layer_id="flst_ok")
|
||||
mock_alkis_komplett.return_value = (enriched, flst, None, LoadStatus.FIRST)
|
||||
proj = _DummyProject()
|
||||
proj.mapLayersByName = lambda _n: [] # kein bestehender Flst-Layer
|
||||
mock_project_instance.return_value = proj
|
||||
mock_iface.mapCanvas.return_value = _DummyCanvas()
|
||||
|
||||
tab.handle_verf_alkis()
|
||||
|
||||
mock_apply_style.assert_called()
|
||||
mock_success.assert_called_once()
|
||||
mock_set_layer_id.assert_called_with("enriched_ok")
|
||||
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.set_verfahrensgebiet_layer_id")
|
||||
@patch(
|
||||
"sn_verfahrensgebiet.ui.tabs.working_tab.verfahrensgebiet_alkis_komplett",
|
||||
return_value=(None, None, None, LoadStatus.NONE),
|
||||
)
|
||||
def test_handle_verf_alkis_failure_clears_layer_var(self, _mock_alkis, mock_set_layer_id):
|
||||
"""Status NONE → Layer-Variable wird auf '' gesetzt."""
|
||||
tab = self._make_tab()
|
||||
|
||||
tab.handle_verf_alkis()
|
||||
|
||||
mock_set_layer_id.assert_called_once_with("")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# handle_flurst_alkis / handle_flurst_aus_gemarkungen (#47: eine gemeinsame
|
||||
# Ladelogik in flurstueck_alkis(), hier wird nur noch die Anbindung geprüft)
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.success")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.apply_style")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.flurstueck_alkis")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.QgsProject.instance")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.iface")
|
||||
def test_handle_flurst_alkis_first_load_uebernimmt_layer(
|
||||
self, mock_iface, mock_project_instance, mock_flurstueck_alkis, mock_apply_style, mock_success
|
||||
):
|
||||
"""Erstmaliges Laden über 'Flurstücke laden' fügt den Layer dem Projekt hinzu."""
|
||||
tab = self._make_tab()
|
||||
mock_iface.mapCanvas.return_value = _DummyCanvas()
|
||||
project = _DummyProject()
|
||||
mock_project_instance.return_value = project
|
||||
layer = _DummyLayer(valid=True, layer_id="flst_first")
|
||||
mock_flurstueck_alkis.return_value = (layer, LoadStatus.FIRST)
|
||||
|
||||
tab.handle_flurst_alkis()
|
||||
|
||||
mock_flurstueck_alkis.assert_called_once_with(tab)
|
||||
self.assertIn(layer, project.added)
|
||||
mock_success.assert_called_once()
|
||||
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.success")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.flurstueck_alkis")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.QgsProject.instance")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.iface")
|
||||
def test_handle_flurst_aus_gemarkungen_ruft_flurstueck_alkis_ohne_schluessel_abfrage(
|
||||
self, mock_iface, mock_project_instance, mock_flurstueck_alkis, mock_success
|
||||
):
|
||||
"""Der Gemarkungen-Auswahl-Button ruft dieselbe Ladefunktion mit ohne_schluessel_abfrage=True auf (#47)."""
|
||||
tab = self._make_tab()
|
||||
mock_iface.mapCanvas.return_value = _DummyCanvas()
|
||||
gem_feat = _DummyGemFeature("110001", "Musterstadt")
|
||||
gem_layer = _DummyGemarkungenLayer([gem_feat])
|
||||
project = types.SimpleNamespace(
|
||||
mapLayersByName=lambda name: [gem_layer] if name == "Gemarkungen" else []
|
||||
)
|
||||
mock_project_instance.return_value = project
|
||||
layer = _DummyLayer(valid=True, layer_id="flst_append")
|
||||
mock_flurstueck_alkis.return_value = (layer, LoadStatus.APPEND)
|
||||
|
||||
tab.handle_flurst_aus_gemarkungen()
|
||||
|
||||
mock_flurstueck_alkis.assert_called_once()
|
||||
_args, kwargs = mock_flurstueck_alkis.call_args
|
||||
self.assertEqual(kwargs.get("vorgabe_schluessel"), ["0001"])
|
||||
self.assertEqual(kwargs.get("vorgabe_namen"), ["Musterstadt"])
|
||||
self.assertTrue(kwargs.get("ohne_schluessel_abfrage"))
|
||||
mock_success.assert_called_once()
|
||||
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.error")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.success")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.flurstueck_alkis")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.QgsProject.instance")
|
||||
def test_handle_flurst_aus_gemarkungen_abbruch_zeigt_keine_zusaetzliche_meldung(
|
||||
self, mock_project_instance, mock_flurstueck_alkis, mock_success, mock_error
|
||||
):
|
||||
"""Bricht flurstueck_alkis() ab (None, NONE), zeigt der Button keine eigene Meldung – flurstueck_alkis() hat das schon getan (#47)."""
|
||||
tab = self._make_tab()
|
||||
gem_feat = _DummyGemFeature("110001", "Musterstadt")
|
||||
gem_layer = _DummyGemarkungenLayer([gem_feat])
|
||||
project = types.SimpleNamespace(
|
||||
mapLayersByName=lambda name: [gem_layer] if name == "Gemarkungen" else []
|
||||
)
|
||||
mock_project_instance.return_value = project
|
||||
mock_flurstueck_alkis.return_value = (None, LoadStatus.NONE)
|
||||
|
||||
tab.handle_flurst_aus_gemarkungen()
|
||||
|
||||
mock_success.assert_not_called()
|
||||
mock_error.assert_not_called()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# handle_grenz_alkis
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.grenzpunkt_alkis")
|
||||
def test_handle_grenz_alkis_success_sets_haken(self, mock_grenz):
|
||||
"""Grenzpunkte erfolgreich geladen → haken_grenz wird gesetzt."""
|
||||
tab = self._make_tab()
|
||||
haken_calls = []
|
||||
tab.setze_haken = lambda lbl, v: haken_calls.append((lbl, v))
|
||||
|
||||
pta = _DummyLayer(valid=True)
|
||||
gp = _DummyLayer(valid=True)
|
||||
mock_grenz.return_value = (pta, gp)
|
||||
|
||||
tab.handle_grenz_alkis()
|
||||
|
||||
self.assertEqual(len(haken_calls), 1)
|
||||
self.assertIs(haken_calls[0][0], tab.haken_grenz)
|
||||
self.assertTrue(haken_calls[0][1])
|
||||
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.grenzpunkt_alkis")
|
||||
def test_handle_grenz_alkis_none_result_sets_no_haken(self, mock_grenz):
|
||||
"""grenzpunkt_alkis liefert (None, None) → kein Haken."""
|
||||
tab = self._make_tab()
|
||||
haken_calls = []
|
||||
tab.setze_haken = lambda lbl, v: haken_calls.append((lbl, v))
|
||||
|
||||
mock_grenz.return_value = (None, None)
|
||||
|
||||
tab.handle_grenz_alkis()
|
||||
|
||||
self.assertEqual(len(haken_calls), 0)
|
||||
# Button wieder aktiviert (finally)
|
||||
self.assertTrue(tab.btn_grenz_alkis.isEnabled())
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# handle_flurst_alkis
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.success")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.apply_style")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.QgsProject.instance")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.iface")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.flurstueck_alkis")
|
||||
def test_handle_flurst_alkis_success_sets_haken_and_style(
|
||||
self, mock_alkis, mock_iface, mock_project, mock_apply_style, mock_success
|
||||
):
|
||||
tab = self._make_tab()
|
||||
haken_calls = []
|
||||
tab.setze_haken = lambda lbl, v: haken_calls.append((lbl, v))
|
||||
|
||||
layer = _DummyLayer(valid=True)
|
||||
mock_alkis.return_value = (layer, LoadStatus.FIRST)
|
||||
mock_project.return_value = _DummyProject()
|
||||
mock_iface.mapCanvas.return_value = _DummyCanvas()
|
||||
|
||||
tab.handle_flurst_alkis()
|
||||
|
||||
mock_apply_style.assert_called_once_with(layer, "GIS_Flst_Beschriftung_ALKIS_NAS.qml")
|
||||
mock_success.assert_called_once()
|
||||
self.assertEqual(len(haken_calls), 1)
|
||||
self.assertTrue(haken_calls[0][1]) # haken gesetzt
|
||||
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.info")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.flurstueck_alkis")
|
||||
def test_handle_flurst_alkis_existing_layer_keep_path(self, mock_alkis, mock_info):
|
||||
tab = self._make_tab()
|
||||
haken_calls = []
|
||||
tab.setze_haken = lambda lbl, v: haken_calls.append((lbl, v))
|
||||
|
||||
layer = _DummyLayer(valid=True)
|
||||
mock_alkis.return_value = (layer, LoadStatus.KEEP)
|
||||
|
||||
tab.handle_flurst_alkis()
|
||||
|
||||
mock_info.assert_called_once()
|
||||
self.assertEqual(len(haken_calls), 1)
|
||||
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.flurstueck_alkis")
|
||||
def test_handle_flurst_alkis_invalid_input_returns_early(self, mock_alkis):
|
||||
"""Status NONE (z. B. Nutzerabbruch) → kein Layer, kein Haken."""
|
||||
tab = self._make_tab()
|
||||
haken_calls = []
|
||||
tab.setze_haken = lambda lbl, v: haken_calls.append((lbl, v))
|
||||
mock_alkis.return_value = (None, LoadStatus.NONE)
|
||||
|
||||
tab.handle_flurst_alkis()
|
||||
|
||||
self.assertEqual(len(haken_calls), 0)
|
||||
# Button muss am Ende wieder freigegeben sein (finally)
|
||||
self.assertTrue(tab.btn_flurst_alkis.isEnabled())
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# handle_verf_aus_auswahl – Vorbedingungsprüfungen
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.warning")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.iface")
|
||||
def test_handle_verfahrensgebiet_from_selection_requires_active_layer(
|
||||
self, mock_iface, mock_warning
|
||||
):
|
||||
"""Kein aktiver Layer → warning, kein Layer wird erstellt."""
|
||||
tab = self._make_tab()
|
||||
mock_iface.activeLayer.return_value = None
|
||||
|
||||
tab.handle_verf_aus_auswahl()
|
||||
|
||||
mock_warning.assert_called_once()
|
||||
self.assertIn("aktiver Layer", mock_warning.call_args[0][1])
|
||||
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.warning")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.pruefe_flurstueck_layer")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.iface")
|
||||
def test_handle_verfahrensgebiet_from_selection_requires_required_fields(
|
||||
self, mock_iface, mock_pruefe, mock_warning
|
||||
):
|
||||
"""Layer ohne Pflichtfelder → warning, kein Layer wird erstellt."""
|
||||
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis as PruefErg
|
||||
tab = self._make_tab()
|
||||
active = _DummyLayer(valid=True)
|
||||
mock_iface.activeLayer.return_value = active
|
||||
mock_pruefe.return_value = PruefErg(
|
||||
ok=False, aktion="felder_fehlen", meldung="Feld fehlt"
|
||||
)
|
||||
|
||||
tab.handle_verf_aus_auswahl()
|
||||
|
||||
mock_warning.assert_called_once()
|
||||
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.warning")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.pruefe_auswahl")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.pruefe_flurstueck_layer")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.iface")
|
||||
def test_handle_verfahrensgebiet_from_selection_requires_selection(
|
||||
self, mock_iface, mock_pruefe_layer, mock_pruefe_auswahl, mock_warning
|
||||
):
|
||||
"""Keine Flurstück-Auswahl → warning, kein Layer erstellt."""
|
||||
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis as PruefErg
|
||||
tab = self._make_tab()
|
||||
active = _DummyLayer(valid=True)
|
||||
mock_iface.activeLayer.return_value = active
|
||||
mock_pruefe_layer.return_value = PruefErg(ok=True, aktion="ok")
|
||||
mock_pruefe_auswahl.return_value = PruefErg(
|
||||
ok=False, aktion="layer_leer", meldung="Keine Auswahl"
|
||||
)
|
||||
|
||||
tab.handle_verf_aus_auswahl()
|
||||
|
||||
mock_warning.assert_called_once()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# handle_verf_aus_auswahl – Erstlauf (kein bestehender Layer)
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.QMessageBox")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.apply_style")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.QgsProject.instance")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.erstelle_verfahrensgebiet_aus_auswahl")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.pruefe_auswahl")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.pruefe_flurstueck_layer")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.iface")
|
||||
def test_handle_verfahrensgebiet_from_selection_new_layer_success(
|
||||
self, mock_iface, mock_pruefe_layer, mock_pruefe_auswahl,
|
||||
mock_erstelle, mock_project_inst, mock_apply_style, mock_msgbox
|
||||
):
|
||||
"""Erstlauf ohne bestehenden Layer → Layer wird erstellt und hinzugefügt."""
|
||||
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis as PruefErg
|
||||
tab = self._make_tab()
|
||||
haken_calls = []
|
||||
tab.setze_haken = lambda lbl, v: haken_calls.append(v)
|
||||
active = _DummyLayer(valid=True)
|
||||
mock_iface.activeLayer.return_value = active
|
||||
mock_iface.mapCanvas.return_value = _DummyCanvas()
|
||||
mock_pruefe_layer.return_value = PruefErg(ok=True, aktion="ok")
|
||||
mock_pruefe_auswahl.return_value = PruefErg(ok=True, aktion="ok", meldung="3 Flurstücke")
|
||||
|
||||
new_layer = _DummyLayer(valid=True, layer_id="new_verf")
|
||||
mock_erstelle.return_value = (new_layer, 3, 0)
|
||||
|
||||
project = _DummyProject()
|
||||
project.mapLayersByName = lambda _name: [] # kein bestehender Layer
|
||||
mock_project_inst.return_value = project
|
||||
|
||||
tab.handle_verf_aus_auswahl()
|
||||
|
||||
self.assertIn(new_layer, project.added)
|
||||
mock_apply_style.assert_called_once_with(new_layer, "verfahrensgebiet.qml")
|
||||
self.assertTrue(haken_calls[0])
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# handle_verf_aus_auswahl – Ergänzen / Entfernen / Ersetzen
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _make_entscheidungs_ergebnis(self, aktion: str):
|
||||
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis as PruefErg
|
||||
return PruefErg(ok=True, aktion=aktion)
|
||||
|
||||
def _setup_existierenden_layer(self, tab, mock_iface, mock_pruefe_layer,
|
||||
mock_pruefe_auswahl, mock_project_inst,
|
||||
mock_canvas, entscheidung: str):
|
||||
"""Gemeinsames Setup für Delta-Operationen mit bestehendem Layer."""
|
||||
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis as PruefErg
|
||||
active = _DummyLayer(valid=True)
|
||||
mock_iface.activeLayer.return_value = active
|
||||
mock_iface.mapCanvas.return_value = mock_canvas
|
||||
mock_pruefe_layer.return_value = PruefErg(ok=True, aktion="ok")
|
||||
mock_pruefe_auswahl.return_value = PruefErg(ok=True, aktion="ok", meldung="2 Flurstücke")
|
||||
|
||||
verf_layer = _DummyLayer(valid=True, layer_id="verf_existing")
|
||||
project = _DummyProject()
|
||||
project.added = []
|
||||
project.mapLayersByName = lambda _: [verf_layer]
|
||||
project.removeMapLayer = lambda lid: None
|
||||
mock_project_inst.return_value = project
|
||||
|
||||
# Pruefmanager gibt Entscheidung zurück
|
||||
tab.pruefmanager.verarbeite = lambda _e: self._make_entscheidungs_ergebnis(entscheidung)
|
||||
|
||||
return active, verf_layer, project
|
||||
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.QMessageBox")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.apply_style")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.ergaenze_verfahrensgebiet")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.QgsProject.instance")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.pruefe_auswahl")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.pruefe_flurstueck_layer")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.iface")
|
||||
def test_handle_verfahrensgebiet_from_selection_existing_layer_ergaenzen_dispatch(
|
||||
self, mock_iface, mock_pruefe_layer, mock_pruefe_auswahl,
|
||||
mock_project_inst, mock_ergaenze, mock_apply_style, mock_msgbox
|
||||
):
|
||||
"""Entscheidung 'ergaenzen' → ergaenze_verfahrensgebiet wird aufgerufen."""
|
||||
tab = self._make_tab()
|
||||
canvas = _DummyCanvas()
|
||||
active, verf_layer, _ = self._setup_existierenden_layer(
|
||||
tab, mock_iface, mock_pruefe_layer, mock_pruefe_auswahl,
|
||||
mock_project_inst, canvas, "verfahrensgebiet_ergaenzen"
|
||||
)
|
||||
mock_ergaenze.return_value = (verf_layer, 2, 0, 0)
|
||||
|
||||
tab.handle_verf_aus_auswahl()
|
||||
|
||||
mock_ergaenze.assert_called_once_with(verf_layer, active)
|
||||
mock_apply_style.assert_called_once_with(verf_layer, "verfahrensgebiet.qml")
|
||||
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.QMessageBox")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.apply_style")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.entferne_aus_verfahrensgebiet")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.QgsProject.instance")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.pruefe_auswahl")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.pruefe_flurstueck_layer")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.iface")
|
||||
def test_handle_verfahrensgebiet_from_selection_existing_layer_entfernen_dispatch(
|
||||
self, mock_iface, mock_pruefe_layer, mock_pruefe_auswahl,
|
||||
mock_project_inst, mock_entferne, mock_apply_style, mock_msgbox
|
||||
):
|
||||
"""Entscheidung 'entfernen' → entferne_aus_verfahrensgebiet wird aufgerufen."""
|
||||
tab = self._make_tab()
|
||||
canvas = _DummyCanvas()
|
||||
active, verf_layer, _ = self._setup_existierenden_layer(
|
||||
tab, mock_iface, mock_pruefe_layer, mock_pruefe_auswahl,
|
||||
mock_project_inst, canvas, "verfahrensgebiet_entfernen"
|
||||
)
|
||||
mock_entferne.return_value = (verf_layer, 1, 1, 0)
|
||||
|
||||
tab.handle_verf_aus_auswahl()
|
||||
|
||||
mock_entferne.assert_called_once_with(verf_layer, active)
|
||||
mock_apply_style.assert_called_once_with(verf_layer, "verfahrensgebiet.qml")
|
||||
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.QMessageBox")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.apply_style")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.ersetze_verfahrensgebiet")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.QgsProject.instance")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.pruefe_auswahl")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.pruefe_flurstueck_layer")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.iface")
|
||||
def test_handle_verfahrensgebiet_from_selection_existing_layer_ersetzen_dispatch(
|
||||
self, mock_iface, mock_pruefe_layer, mock_pruefe_auswahl,
|
||||
mock_project_inst, mock_ersetze, mock_apply_style, mock_msgbox
|
||||
):
|
||||
"""Entscheidung 'ersetzen' → alter Layer entfernt, neuer hinzugefügt."""
|
||||
tab = self._make_tab()
|
||||
canvas = _DummyCanvas()
|
||||
active, verf_layer, project = self._setup_existierenden_layer(
|
||||
tab, mock_iface, mock_pruefe_layer, mock_pruefe_auswahl,
|
||||
mock_project_inst, canvas, "verfahrensgebiet_ersetzen"
|
||||
)
|
||||
removed = []
|
||||
project.removeMapLayer = lambda lid: removed.append(lid)
|
||||
new_layer = _DummyLayer(valid=True, layer_id="new_verf_replaced")
|
||||
mock_ersetze.return_value = (new_layer, 3, 0)
|
||||
|
||||
tab.handle_verf_aus_auswahl()
|
||||
|
||||
mock_ersetze.assert_called_once_with(active)
|
||||
self.assertIn(verf_layer.id(), removed)
|
||||
self.assertIn(new_layer, project.added)
|
||||
mock_apply_style.assert_called_once_with(new_layer, "verfahrensgebiet.qml")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# handle_verf_aus_liste
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.show_info_dialog")
|
||||
def test_aus_liste_button_shows_not_implemented_hint(self, mock_info):
|
||||
tab = self._make_tab()
|
||||
tab.handle_verf_aus_liste()
|
||||
mock_info.assert_called_once()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# handle_gem_alkis
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.success")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.apply_style")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.lade_gemarkungen_layer")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.QgsProject.instance")
|
||||
def test_handle_gem_alkis_service_reachable_adds_layer(
|
||||
self, mock_project_inst, mock_lade, mock_apply_style, mock_success
|
||||
):
|
||||
"""Dienst erreichbar → Layer ins Projekt, Stil angewendet, Haken gesetzt."""
|
||||
tab = self._make_tab()
|
||||
haken_calls = []
|
||||
tab.setze_haken = lambda lbl, v: haken_calls.append((lbl, v))
|
||||
|
||||
layer = _DummyLayer(valid=True, layer_name="Gemarkungen")
|
||||
mock_lade.return_value = layer
|
||||
|
||||
project = _DummyProject()
|
||||
project.mapLayersByName = lambda _n: []
|
||||
mock_project_inst.return_value = project
|
||||
|
||||
tab.handle_gem_alkis()
|
||||
|
||||
self.assertIn(layer, project.added)
|
||||
mock_apply_style.assert_called_once_with(layer, "gemarkungen_beschriftet.qml")
|
||||
mock_success.assert_called_once()
|
||||
self.assertEqual(len(haken_calls), 1)
|
||||
self.assertIs(haken_calls[0][0], tab.haken_gem)
|
||||
self.assertTrue(haken_calls[0][1])
|
||||
self.assertTrue(tab.btn_gem_alkis.isEnabled()) # finally
|
||||
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.warning")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.lade_gemarkungen_layer")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.QgsProject.instance")
|
||||
def test_handle_gem_alkis_service_unreachable_shows_warning(
|
||||
self, mock_project_inst, mock_lade, mock_warning
|
||||
):
|
||||
"""Dienst nicht erreichbar (None) → warning, kein addMapLayer."""
|
||||
tab = self._make_tab()
|
||||
haken_calls = []
|
||||
tab.setze_haken = lambda lbl, v: haken_calls.append((lbl, v))
|
||||
|
||||
mock_lade.return_value = None
|
||||
project = _DummyProject()
|
||||
project.mapLayersByName = lambda _n: []
|
||||
mock_project_inst.return_value = project
|
||||
|
||||
tab.handle_gem_alkis()
|
||||
|
||||
mock_warning.assert_called_once()
|
||||
self.assertEqual(len(project.added), 0)
|
||||
self.assertEqual(len(haken_calls), 0)
|
||||
self.assertTrue(tab.btn_gem_alkis.isEnabled()) # finally
|
||||
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.info")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.lade_gemarkungen_layer")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.QgsProject.instance")
|
||||
def test_handle_gem_alkis_layer_already_exists_shows_info(
|
||||
self, mock_project_inst, mock_lade, mock_info
|
||||
):
|
||||
"""Layer 'Gemarkungen' bereits vorhanden → info, kein erneutes Laden."""
|
||||
tab = self._make_tab()
|
||||
haken_calls = []
|
||||
tab.setze_haken = lambda lbl, v: haken_calls.append((lbl, v))
|
||||
|
||||
existing_layer = _DummyLayer(valid=True, layer_name="Gemarkungen")
|
||||
project = _DummyProject()
|
||||
project.mapLayersByName = lambda _n: [existing_layer]
|
||||
mock_project_inst.return_value = project
|
||||
|
||||
tab.handle_gem_alkis()
|
||||
|
||||
mock_info.assert_called_once()
|
||||
mock_lade.assert_not_called()
|
||||
self.assertEqual(len(project.added), 0)
|
||||
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")
|
||||
|
||||
|
||||
class TestZoomImProjektKbs(unittest.TestCase):
|
||||
"""Zoom auf das neue Verfahrensgebiet rechnet in das KBS der Kartenansicht um (Basis #99)."""
|
||||
|
||||
def _tab(self):
|
||||
tab = TestWorkingTab._make_tab(TestWorkingTab())
|
||||
tab.setze_haken = lambda *_: None
|
||||
tab._on_verf_layer_changed = lambda *_: None
|
||||
return tab
|
||||
|
||||
def _projekt(self, mock_project_instance):
|
||||
proj = _DummyProject()
|
||||
proj.mapLayersByName = lambda _n: []
|
||||
proj.mapLayer = lambda _lid: None
|
||||
mock_project_instance.return_value = proj
|
||||
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.zoom_auf_layer")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.success")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.apply_style")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.QgsProject.instance")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.verfahrensgebiet_alkis_komplett")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.iface")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.set_verfahrensgebiet_layer_id")
|
||||
def test_alkis_zoom_ueber_zoom_auf_layer(self, _set_id, mock_iface, mock_alkis, mock_projekt,
|
||||
_stil, _erfolg, mock_zoom):
|
||||
"""Prüft, dass nach dem Laden aus ALKIS über zoom_auf_layer auf das Verfahrensgebiet gezoomt wird und nicht mit der unumgerechneten Layer-Ausdehnung."""
|
||||
tab = self._tab()
|
||||
enriched = _DummyLayer(valid=True, layer_id="vg")
|
||||
mock_alkis.return_value = (enriched, _DummyLayer(layer_id="flst"), None, LoadStatus.FIRST)
|
||||
self._projekt(mock_projekt)
|
||||
canvas = _DummyCanvas()
|
||||
mock_iface.mapCanvas.return_value = canvas
|
||||
|
||||
tab.handle_verf_alkis()
|
||||
|
||||
mock_zoom.assert_called_once_with(enriched, canvas)
|
||||
self.assertIsNone(canvas.extent)
|
||||
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.get_variable", return_value="VF-123")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.zoom_auf_layer")
|
||||
@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_shape_zoom_ueber_zoom_auf_layer(self, _set_id, mock_iface, mock_shape, mock_projekt,
|
||||
_stil, _erfolg, mock_zoom, _var):
|
||||
"""Prüft, dass nach dem Laden aus einem Shape über zoom_auf_layer auf das Verfahrensgebiet gezoomt wird und nicht mit der unumgerechneten Layer-Ausdehnung."""
|
||||
tab = self._tab()
|
||||
tab.btn_verf_shape = _DummyButton()
|
||||
enriched = _DummyLayer(valid=True, layer_id="shape_vg")
|
||||
mock_shape.return_value = (enriched, _DummyLayer(layer_id="flst"), None, LoadStatus.FIRST)
|
||||
self._projekt(mock_projekt)
|
||||
canvas = _DummyCanvas()
|
||||
mock_iface.mapCanvas.return_value = canvas
|
||||
|
||||
tab.handle_verf_shape()
|
||||
|
||||
mock_zoom.assert_called_once_with(enriched, canvas)
|
||||
self.assertIsNone(canvas.extent)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
+21
-1
@@ -1,3 +1,7 @@
|
||||
"""sn_verfahrensgebiet/ui/dockwidget.py – Haupt-DockWidget für sn_verfahrensgebiet.
|
||||
Verwaltet die Tabs für Verfahrensgebiet-Bearbeitung, Druck und Einstellungen.
|
||||
"""
|
||||
|
||||
from sn_basis.ui.tabs.settings_tab import SettingsTab
|
||||
from sn_basis.ui.tabs.print_tab import PrintTab
|
||||
from sn_verfahrensgebiet.ui.tabs.working_tab import WorkingTab
|
||||
@@ -7,9 +11,25 @@ from sn_basis.functions.message_wrapper import error
|
||||
|
||||
|
||||
class DockWidget(BaseDockWidget):
|
||||
"""Haupt-DockWidget für sn_verfahrensgebiet mit Tab-Verwaltung.
|
||||
|
||||
Attributes:
|
||||
tabs: Liste von Tab-Klassen (WorkingTab, PrintTab, SettingsTab)
|
||||
pruefmanager: Prüfmanager-Instanz für Validierung
|
||||
data_grabber: DataGrabber-Instanz für Fachdatenabruf
|
||||
"""
|
||||
|
||||
tabs = [WorkingTab, PrintTab, SettingsTab]
|
||||
|
||||
def __init__(self, parent=None, subtitle="", pruefmanager=None, data_grabber=None):
|
||||
"""Initialisiert das DockWidget mit Services und Tabs.
|
||||
|
||||
Args:
|
||||
parent: Übergeordnetes Widget
|
||||
subtitle: Untertitel des DockWidgets
|
||||
pruefmanager: Prüfmanager-Instanz
|
||||
data_grabber: DataGrabber-Instanz
|
||||
"""
|
||||
super().__init__(parent, subtitle)
|
||||
|
||||
self.pruefmanager = pruefmanager
|
||||
@@ -18,7 +38,7 @@ class DockWidget(BaseDockWidget):
|
||||
self._init_tabs_with_services()
|
||||
|
||||
def _init_tabs_with_services(self):
|
||||
"""Tabs neu aufbauen und Services (z.B. fuer PrintTab) injizieren."""
|
||||
"""Tabs neu aufbauen und Services (z.B. für PrintTab) injizieren."""
|
||||
try:
|
||||
self.setWidget(None)
|
||||
|
||||
|
||||
+696
-38
@@ -1,16 +1,55 @@
|
||||
"""sn_verfahrensgebiet/ui/tabs/working_tab.py – Bearbeitungs-Tab.
|
||||
Verwaltet die Verfahrensgebiet-Abgrenzung und ALKIS-Integration.
|
||||
"""
|
||||
|
||||
from sn_basis.functions.qt_wrapper import QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QMessageBox
|
||||
from sn_basis.functions.qt_wrapper import Qt
|
||||
from sn_basis.functions import Qgis, QgsProject #QgsMessageLog
|
||||
from sn_basis.functions.qgisui_wrapper import QgsMapLayerComboBox
|
||||
from sn_basis.functions.qgisui_wrapper import QgsMapLayerComboBox, zoom_auf_layer
|
||||
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, 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
|
||||
|
||||
from sn_verfahrensgebiet.functions.verfahrensgebiet_alkis import verfahrensgebiet_alkis, LoadStatus
|
||||
from sn_verfahrensgebiet.functions.verfahrensgebiet_alkis import LoadStatus
|
||||
from sn_verfahrensgebiet.functions.flurstueck_alkis import flurstueck_alkis
|
||||
from sn_verfahrensgebiet.functions.verfahrensgebiet_alkis_komplett import (
|
||||
verfahrensgebiet_alkis_komplett,
|
||||
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,
|
||||
lade_gemarkungen_layer,
|
||||
ALKIS_VEREINF_URL,
|
||||
)
|
||||
from sn_verfahrensgebiet.functions.umringstatistik_grenzpunkte import umringstatistik_grenzpunkte
|
||||
from sn_verfahrensgebiet.functions.verfahrensgebiet_edit_service import (
|
||||
pruefe_flurstueck_layer,
|
||||
pruefe_auswahl,
|
||||
erstelle_verfahrensgebiet_aus_auswahl,
|
||||
ergaenze_verfahrensgebiet,
|
||||
entferne_aus_verfahrensgebiet,
|
||||
ersetze_verfahrensgebiet,
|
||||
)
|
||||
from sn_basis.functions.message_wrapper import success, info, warning, error
|
||||
from sn_basis.functions.ly_style_wrapper import apply_style
|
||||
|
||||
class WorkingTab(QWidget):
|
||||
"""Tab für die Verfahrensgebiet-Bearbeitung und Verwaltung.
|
||||
|
||||
Attributes:
|
||||
tab_title: Anzeigetitel des Tabs ("Bearbeitung")
|
||||
"""
|
||||
|
||||
tab_title = "Bearbeitung"
|
||||
|
||||
def __init__(self, parent=None):
|
||||
@@ -19,7 +58,7 @@ class WorkingTab(QWidget):
|
||||
layout = QVBoxLayout()
|
||||
layout.setAlignment(Qt.AlignmentFlag.AlignTop)
|
||||
|
||||
# Abschnitt 1: Verfahrensgebiet
|
||||
# Abschnitt 1: Verfahrensgebiet laden
|
||||
row_verf_header = QHBoxLayout()
|
||||
lbl_verf = QLabel("<b>Verfahrensgebiet</b>")
|
||||
self.haken_verf = self._haken_label()
|
||||
@@ -35,6 +74,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")
|
||||
@@ -49,8 +97,21 @@ class WorkingTab(QWidget):
|
||||
self.verf_layer_combo.layerChanged.connect(self._on_verf_layer_changed)
|
||||
layout.addWidget(self.verf_layer_combo)
|
||||
|
||||
# Abschnitt 2: Flurstücke (Platzhalter für spätere Implementierung)
|
||||
# Abschnitt 2: Gemarkungen
|
||||
row_gem_header = QHBoxLayout()
|
||||
lbl_gem = QLabel("<b>Gemarkungen</b>")
|
||||
self.haken_gem = self._haken_label()
|
||||
row_gem_header.addWidget(lbl_gem)
|
||||
row_gem_header.addWidget(self.haken_gem)
|
||||
row_gem_header.addStretch()
|
||||
layout.addLayout(row_gem_header)
|
||||
|
||||
row_gem_buttons = QHBoxLayout()
|
||||
self.btn_gem_alkis = QPushButton("Aus ALKIS laden")
|
||||
row_gem_buttons.addWidget(self.btn_gem_alkis)
|
||||
layout.addLayout(row_gem_buttons)
|
||||
|
||||
# Abschnitt 2b: Flurstücke
|
||||
row_flurst_header = QHBoxLayout()
|
||||
lbl_flurst = QLabel("<b>Flurstücke</b>")
|
||||
self.haken_flurst = self._haken_label()
|
||||
@@ -66,62 +127,224 @@ class WorkingTab(QWidget):
|
||||
row_flurst_buttons.addWidget(self.btn_flurst_shape)
|
||||
layout.addLayout(row_flurst_buttons)
|
||||
|
||||
row_flurst_gem_buttons = QHBoxLayout()
|
||||
self.btn_flurst_aus_gemarkungen = QPushButton("aus gewählten Gemarkungen laden")
|
||||
row_flurst_gem_buttons.addWidget(self.btn_flurst_aus_gemarkungen)
|
||||
layout.addLayout(row_flurst_gem_buttons)
|
||||
|
||||
# Abschnitt 2c: Grenzpunkte
|
||||
row_grenz_header = QHBoxLayout()
|
||||
lbl_grenz = QLabel("<b>Grenzpunkte</b>")
|
||||
self.haken_grenz = self._haken_label()
|
||||
row_grenz_header.addWidget(lbl_grenz)
|
||||
row_grenz_header.addWidget(self.haken_grenz)
|
||||
row_grenz_header.addStretch()
|
||||
layout.addLayout(row_grenz_header)
|
||||
|
||||
row_grenz_buttons = QHBoxLayout()
|
||||
self.btn_grenz_alkis = QPushButton("Grenzpunkte aus ALKIS laden")
|
||||
row_grenz_buttons.addWidget(self.btn_grenz_alkis)
|
||||
layout.addLayout(row_grenz_buttons)
|
||||
|
||||
row_umring_buttons = QHBoxLayout()
|
||||
self.btn_umring_statistik = QPushButton("Umringstatistik Grenzpunkte")
|
||||
row_umring_buttons.addWidget(self.btn_umring_statistik)
|
||||
layout.addLayout(row_umring_buttons)
|
||||
|
||||
# Abschnitt 3: Verfahrensgebiet erstellen
|
||||
row_verf_erstellen_header = QHBoxLayout()
|
||||
lbl_verf_erstellen = QLabel("<b>Verfahrensgebiet erstellen</b>")
|
||||
row_verf_erstellen_header.addWidget(lbl_verf_erstellen)
|
||||
row_verf_erstellen_header.addStretch()
|
||||
layout.addLayout(row_verf_erstellen_header)
|
||||
|
||||
row_verf_erstellen_buttons = QHBoxLayout()
|
||||
self.btn_verf_aus_auswahl = QPushButton("aus Auswahl")
|
||||
self.btn_verf_aus_liste = QPushButton("Aus Liste")
|
||||
row_verf_erstellen_buttons.addWidget(self.btn_verf_aus_auswahl)
|
||||
row_verf_erstellen_buttons.addWidget(self.btn_verf_aus_liste)
|
||||
layout.addLayout(row_verf_erstellen_buttons)
|
||||
|
||||
layout.addStretch()
|
||||
self.setLayout(layout)
|
||||
|
||||
# Pruefmanager für interaktive Entscheidungen (Ergänzen/Entfernen/Ersetzen)
|
||||
self.pruefmanager = Pruefmanager(ui_modus="qgis", parent=self)
|
||||
|
||||
# Signale verbinden
|
||||
self.btn_verf_alkis.clicked.connect(self.handle_verf_alkis)
|
||||
# self.btn_verf_shape.clicked.connect(self.handle_verf_shape)
|
||||
# self.btn_flurst_alkis.clicked.connect(self.handle_flurst_alkis)
|
||||
self.btn_flurst_alkis.clicked.connect(self.handle_flurst_alkis)
|
||||
self.btn_flurst_aus_gemarkungen.clicked.connect(self.handle_flurst_aus_gemarkungen)
|
||||
self.btn_gem_alkis.clicked.connect(self.handle_gem_alkis)
|
||||
self.btn_grenz_alkis.clicked.connect(self.handle_grenz_alkis)
|
||||
self.btn_umring_statistik.clicked.connect(self.handle_umringstatistik)
|
||||
self.btn_verf_aus_auswahl.clicked.connect(self.handle_verf_aus_auswahl)
|
||||
self.btn_verf_aus_liste.clicked.connect(self.handle_verf_aus_liste)
|
||||
self.btn_verf_shape.clicked.connect(self.handle_verf_shape)
|
||||
# self.btn_flurst_shape.clicked.connect(self.handle_flurst_shape)
|
||||
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()
|
||||
|
||||
def handle_verf_alkis(self):
|
||||
self._set_busy(self.btn_verf_alkis, True)
|
||||
def handle_verf_alkis(self) -> None:
|
||||
"""Startet den vollständigen VG-Lade-Workflow (ALKIS → Gemarkungen → Flurstücke →
|
||||
Verschneidung → Flächenvergleich → opt. Umringsprüfung).
|
||||
|
||||
Delegiert an :func:`~sn_verfahrensgebiet.functions.verfahrensgebiet_alkis_komplett.verfahrensgebiet_alkis_komplett`.
|
||||
Nach Erfolg werden enriched VG-Layer, Flurstücks-Layer und BROR-Layer
|
||||
ins Projekt eingefügt, Styles angewendet und die UI-Elemente aktualisiert.
|
||||
"""
|
||||
self._set_busy(self.btn_verf_alkis, True, "Aus ALKIS laden")
|
||||
try:
|
||||
layer, status = verfahrensgebiet_alkis(self)
|
||||
if not layer or not layer.isValid():
|
||||
enriched_layer, flst_layer, bror_layer, status = verfahrensgebiet_alkis_komplett(self)
|
||||
|
||||
if status == LoadStatus.NONE or not enriched_layer:
|
||||
set_verfahrensgebiet_layer_id("")
|
||||
return
|
||||
|
||||
if status in (LoadStatus.FIRST, LoadStatus.RELOAD):
|
||||
# Gemeinsame Logik für erstmaliges und erneutes Laden
|
||||
QgsProject.instance().addMapLayer(layer)
|
||||
iface.mapCanvas().setExtent(layer.extent())
|
||||
#iface.mapCanvas().refresh()
|
||||
project = QgsProject.instance()
|
||||
|
||||
apply_style(layer, "verfahrensgebiet.qml")
|
||||
iface.mapCanvas().refresh()
|
||||
# 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 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")
|
||||
# Ausdehnung ins KBS der Kartenansicht umrechnen (Basis #99)
|
||||
zoom_auf_layer(enriched_layer, iface.mapCanvas())
|
||||
self.setze_haken(self.haken_verf, True)
|
||||
|
||||
# Dropdown + Variable explizit synchronisieren.
|
||||
self.verf_layer_combo.setLayer(layer)
|
||||
self._on_verf_layer_changed(layer)
|
||||
self.verf_layer_combo.setLayer(enriched_layer)
|
||||
self._on_verf_layer_changed(enriched_layer)
|
||||
|
||||
# Unterschied nur in der Meldung
|
||||
msg = "erstmalig geladen" if status == LoadStatus.FIRST else "neu geladen"
|
||||
success("Verfahrensgebiet", f"Layer wurde {msg}.", duration=5)
|
||||
|
||||
elif status == LoadStatus.KEEP:
|
||||
self.verf_layer_combo.setLayer(layer)
|
||||
self._on_verf_layer_changed(layer)
|
||||
info("Verfahrensgebiet", "Vorhandener Layer wurde behalten.", duration=4)
|
||||
|
||||
elif status == LoadStatus.NONE:
|
||||
self._on_verf_layer_changed(None)
|
||||
warning("Verfahrensgebiet", "Layer konnte nicht geladen werden.", duration=None)
|
||||
success("Verfahrensgebiet", f"Layer wurde {msg} (inkl. Flurstücksverschneidung).", duration=5)
|
||||
|
||||
finally:
|
||||
self._set_busy(self.btn_verf_alkis, False)
|
||||
self._set_busy(self.btn_verf_alkis, False, "Aus ALKIS laden")
|
||||
|
||||
def _set_busy(self, button: QPushButton, busy: bool):
|
||||
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")
|
||||
|
||||
# Shape-VG-Layer ins Projekt
|
||||
if not project.mapLayer(enriched_layer.id()):
|
||||
project.addMapLayer(enriched_layer)
|
||||
apply_style(enriched_layer, "verfahrensgebiet.qml")
|
||||
# Ausdehnung ins KBS der Kartenansicht umrechnen (Basis #99)
|
||||
zoom_auf_layer(enriched_layer, iface.mapCanvas())
|
||||
self.setze_haken(self.haken_verf, True)
|
||||
|
||||
self.verf_layer_combo.setLayer(enriched_layer)
|
||||
self._on_verf_layer_changed(enriched_layer)
|
||||
|
||||
success("Verfahrensgebiet", "Layer aus Shape geladen.", duration=5)
|
||||
|
||||
finally:
|
||||
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.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
button :
|
||||
Der zu sperrende/entsperrende Button.
|
||||
busy : bool
|
||||
True zum Sperren (Text → "Lade ..."), False zum Freigeben.
|
||||
original_text : str
|
||||
Text, der nach dem Freigeben wiederhergestellt wird.
|
||||
Jeder Button gibt seinen eigenen Originaltext explizit an.
|
||||
"""
|
||||
button.setEnabled(not busy)
|
||||
if busy:
|
||||
button.setText("Lade ...")
|
||||
else:
|
||||
button.setText("Aus ALKIS laden")
|
||||
button.setText("Lade ..." if busy else original_text)
|
||||
|
||||
def _haken_label(self) -> QLabel:
|
||||
label = QLabel()
|
||||
@@ -152,7 +375,6 @@ class WorkingTab(QWidget):
|
||||
"""Beim Tab-Init: Lade die zuletzt gespeicherte Verfahrensgebiet-Layer-ID."""
|
||||
layer_id = get_verfahrensgebiet_layer_id()
|
||||
if layer_id:
|
||||
# Versuche, den Layer im Projekt zu finden
|
||||
try:
|
||||
project = QgsProject.instance()
|
||||
layer = project.mapLayer(layer_id)
|
||||
@@ -160,3 +382,439 @@ class WorkingTab(QWidget):
|
||||
self.verf_layer_combo.setLayer(layer)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Flurstücke-Handler
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _uebernimm_flurstuecke_ergebnis(self, layer, status) -> None:
|
||||
"""Übernimmt das Ergebnis von flurstueck_alkis() ins Projekt (FIRST/RELOAD/APPEND/KEEP)."""
|
||||
if status in (LoadStatus.FIRST, LoadStatus.RELOAD):
|
||||
QgsProject.instance().addMapLayer(layer)
|
||||
apply_style(layer, "GIS_Flst_Beschriftung_ALKIS_NAS.qml")
|
||||
iface.mapCanvas().refresh()
|
||||
self.setze_haken(self.haken_flurst, True)
|
||||
msg = "erstmalig geladen" if status == LoadStatus.FIRST else "neu geladen"
|
||||
success("Flurstücke", f"Layer wurde {msg}.", duration=5)
|
||||
|
||||
elif status == LoadStatus.KEEP:
|
||||
self.setze_haken(self.haken_flurst, True)
|
||||
info("Flurstücke", "Vorhandener Layer wurde behalten.", duration=4)
|
||||
|
||||
elif status == LoadStatus.APPEND:
|
||||
self.setze_haken(self.haken_flurst, True)
|
||||
iface.mapCanvas().refresh()
|
||||
success("Flurstücke", "Features wurden an den vorhandenen Layer angehängt.", duration=5)
|
||||
|
||||
def handle_flurst_alkis(self) -> None:
|
||||
"""Lädt Flurstücke aus ALKIS-WFS anhand von Gemarkungsschlüsseln."""
|
||||
self._set_busy(self.btn_flurst_alkis, True, "Aus ALKIS laden")
|
||||
try:
|
||||
layer, status = flurstueck_alkis(self)
|
||||
if not layer or not layer.isValid():
|
||||
return
|
||||
self._uebernimm_flurstuecke_ergebnis(layer, status)
|
||||
finally:
|
||||
self._set_busy(self.btn_flurst_alkis, False, "Aus ALKIS laden")
|
||||
|
||||
def handle_flurst_aus_gemarkungen(self) -> None:
|
||||
"""Lädt Flurstücke aus ALKIS anhand der Auswahl im Layer 'Gemarkungen'.
|
||||
|
||||
- Kein Layer 'Gemarkungen': Layer wird geladen, Nutzer aufgefordert Auswahl zu treffen.
|
||||
- Keine Auswahl: Warnmeldung.
|
||||
- Auswahl vorhanden: Gemarkungsschlüssel extrahieren und Flurstücke direkt laden.
|
||||
"""
|
||||
self._set_busy(self.btn_flurst_aus_gemarkungen, True, "aus gewählten Gemarkungen laden")
|
||||
try:
|
||||
project = QgsProject.instance()
|
||||
|
||||
# Fall A: Layer 'Gemarkungen' nicht vorhanden → laden
|
||||
gem_layers = project.mapLayersByName("Gemarkungen")
|
||||
if not gem_layers:
|
||||
gem_layer = lade_gemarkungen_layer()
|
||||
if not gem_layer:
|
||||
error(
|
||||
"Gemarkungen",
|
||||
f"Gemarkungslayer konnte nicht geladen werden – "
|
||||
f"Dienst nicht erreichbar ({ALKIS_VEREINF_URL}). "
|
||||
f"Bitte Netzwerkverbindung prüfen.",
|
||||
duration=0,
|
||||
)
|
||||
return
|
||||
project.addMapLayer(gem_layer)
|
||||
apply_style(gem_layer, "gemarkungen_beschriftet.qml")
|
||||
self.setze_haken(self.haken_gem, True)
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"Gemarkungen",
|
||||
"Gemarkungslayer wurde geladen – bitte Gemarkungen auswählen.",
|
||||
)
|
||||
return
|
||||
|
||||
gemarkungen_layer = gem_layers[0]
|
||||
|
||||
# Fall B: Keine Auswahl
|
||||
if not gemarkungen_layer.selectedFeatureCount():
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
"Gemarkungen",
|
||||
"Bitte mindestens eine Gemarkung im Layer Gemarkungen auswählen.",
|
||||
)
|
||||
return
|
||||
|
||||
# Fall C: Auswahl vorhanden → Schlüssel extrahieren
|
||||
vorgabe_schluessel = []
|
||||
vorgabe_namen = []
|
||||
for feat in gemarkungen_layer.selectedFeatures():
|
||||
try:
|
||||
schluessel = str(feat["schluessel"]).strip()
|
||||
nummer = schluessel[-4:] if len(schluessel) >= 4 else schluessel
|
||||
except Exception:
|
||||
nummer = ""
|
||||
try:
|
||||
name = str(feat["ave_name"]).strip()
|
||||
except Exception:
|
||||
name = ""
|
||||
if nummer:
|
||||
vorgabe_schluessel.append(nummer)
|
||||
vorgabe_namen.append(name)
|
||||
|
||||
if not vorgabe_schluessel:
|
||||
error(
|
||||
"Flurstücke",
|
||||
"Aus der Gemarkungsauswahl konnten keine gültigen Gemarkungsschlüssel extrahiert werden.",
|
||||
duration=8,
|
||||
)
|
||||
return
|
||||
|
||||
# Flurstücke laden – Schlüssel bereits bekannt, aber bei bestehendem Layer
|
||||
# trotzdem interaktiv nachfragen (Überschreiben/Anhängen/Abbrechen, inkl. Dedup)
|
||||
layer, status = flurstueck_alkis(
|
||||
self,
|
||||
vorgabe_schluessel=vorgabe_schluessel,
|
||||
vorgabe_namen=vorgabe_namen,
|
||||
ohne_schluessel_abfrage=True,
|
||||
)
|
||||
|
||||
if not layer or not layer.isValid():
|
||||
# flurstueck_alkis() hat bei Abbruch/Fehler bereits selbst eine Meldung gezeigt.
|
||||
return
|
||||
|
||||
self._uebernimm_flurstuecke_ergebnis(layer, status)
|
||||
|
||||
finally:
|
||||
self._set_busy(self.btn_flurst_aus_gemarkungen, False, "aus gewählten Gemarkungen laden")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Gemarkungen-Handler
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def handle_gem_alkis(self) -> None:
|
||||
"""Lädt den KatasterBezirk-Layer aus dem vereinfachten ALKIS-WFS
|
||||
als Layer „Gemarkungen" ins Projekt.
|
||||
|
||||
Ist ein Layer namens „Gemarkungen" bereits vorhanden, wird er behalten
|
||||
und eine Info-Meldung angezeigt (kein doppeltes Laden).
|
||||
"""
|
||||
self._set_busy(self.btn_gem_alkis, True, "Aus ALKIS laden")
|
||||
try:
|
||||
existing = QgsProject.instance().mapLayersByName("Gemarkungen")
|
||||
if existing:
|
||||
self.setze_haken(self.haken_gem, True)
|
||||
info("Gemarkungen", "Layer 'Gemarkungen' ist bereits im Projekt vorhanden.", duration=4)
|
||||
return
|
||||
|
||||
layer = lade_gemarkungen_layer()
|
||||
if not layer:
|
||||
warning(
|
||||
"Gemarkungen",
|
||||
"Layer konnte nicht geladen werden – Dienst nicht erreichbar oder ungültig.",
|
||||
duration=6,
|
||||
)
|
||||
return
|
||||
|
||||
QgsProject.instance().addMapLayer(layer)
|
||||
apply_style(layer, "gemarkungen_beschriftet.qml")
|
||||
self.setze_haken(self.haken_gem, True)
|
||||
success("Gemarkungen", "Layer 'Gemarkungen' wurde geladen.", duration=4)
|
||||
|
||||
finally:
|
||||
self._set_busy(self.btn_gem_alkis, False, "Aus ALKIS laden")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Verfahrensgebiet-erstellen-Handler
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def handle_verf_aus_auswahl(self) -> None:
|
||||
"""Erstellt oder aktualisiert den Verfahrensgebiet-Layer aus der Flurstück-Auswahl."""
|
||||
self._set_busy(self.btn_verf_aus_auswahl, True, "aus Auswahl")
|
||||
try:
|
||||
# 1. Aktiven Layer ermitteln
|
||||
active_layer = iface.activeLayer()
|
||||
if not active_layer:
|
||||
warning(
|
||||
"Verfahrensgebiet erstellen",
|
||||
"Kein aktiver Layer. Bitte den Flurstück-Layer aktivieren.",
|
||||
duration=None,
|
||||
)
|
||||
return
|
||||
|
||||
# 2. Vorbedingungen via Layerpruefer
|
||||
ergebnis = pruefe_flurstueck_layer(active_layer)
|
||||
if not ergebnis.ok:
|
||||
warning("Verfahrensgebiet erstellen", ergebnis.meldung, duration=None)
|
||||
return
|
||||
|
||||
# 3. Auswahl prüfen
|
||||
ergebnis_auswahl = pruefe_auswahl(active_layer)
|
||||
if not ergebnis_auswahl.ok:
|
||||
warning("Verfahrensgebiet erstellen", ergebnis_auswahl.meldung, duration=None)
|
||||
return
|
||||
|
||||
# 4. Bestehenden Verfahrensgebiet-Layer prüfen
|
||||
existing_layers = QgsProject.instance().mapLayersByName("Verfahrensgebiet")
|
||||
|
||||
if not existing_layers:
|
||||
# Erstlauf: neuen Memory-Layer erzeugen
|
||||
layer, erfolgreich, fehlgeschlagen = erstelle_verfahrensgebiet_aus_auswahl(
|
||||
active_layer
|
||||
)
|
||||
if not layer or not layer.isValid():
|
||||
error("Verfahrensgebiet erstellen", "Layer konnte nicht erstellt werden.")
|
||||
return
|
||||
QgsProject.instance().addMapLayer(layer)
|
||||
apply_style(layer, "verfahrensgebiet.qml")
|
||||
iface.mapCanvas().refresh()
|
||||
self.verf_layer_combo.setLayer(layer)
|
||||
self._on_verf_layer_changed(layer)
|
||||
self.setze_haken(self.haken_verf, True)
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"Verfahrensgebiet erstellt",
|
||||
f"Verfahrensgebiet aus Auswahl erstellt.\n"
|
||||
f"Erfolgreich: {erfolgreich}\n"
|
||||
f"Fehlgeschlagen: {fehlgeschlagen}",
|
||||
)
|
||||
return
|
||||
|
||||
# 5. Bestehender Layer: Nutzerentscheidung via Pruefmanager
|
||||
entscheidung_ergebnis = self.pruefmanager.verarbeite(
|
||||
pruef_ergebnis(
|
||||
ok=False,
|
||||
aktion="verfahrensgebiet_layer_existiert",
|
||||
meldung=(
|
||||
"Ein Verfahrensgebiet-Layer existiert bereits.\n"
|
||||
"Was soll mit der Auswahl geschehen?"
|
||||
),
|
||||
kontext=existing_layers[0],
|
||||
)
|
||||
)
|
||||
|
||||
aktion = entscheidung_ergebnis.aktion
|
||||
verf_layer = existing_layers[0]
|
||||
|
||||
if aktion == "verfahrensgebiet_abbrechen" or not entscheidung_ergebnis.ok:
|
||||
return
|
||||
|
||||
elif aktion == "verfahrensgebiet_ergaenzen":
|
||||
_, hinzugefuegt, bereits_vorhanden, fehlgeschlagen = ergaenze_verfahrensgebiet(
|
||||
verf_layer, active_layer
|
||||
)
|
||||
apply_style(verf_layer, "verfahrensgebiet.qml")
|
||||
iface.mapCanvas().refresh()
|
||||
self.setze_haken(self.haken_verf, True)
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"Flurstücke ergänzt",
|
||||
f"Hinzugefügt: {hinzugefuegt}\n"
|
||||
f"Bereits vorhanden (übersprungen): {bereits_vorhanden}\n"
|
||||
f"Fehlgeschlagen: {fehlgeschlagen}",
|
||||
)
|
||||
|
||||
elif aktion == "verfahrensgebiet_entfernen":
|
||||
_, entfernt, nicht_enthalten, fehlgeschlagen = entferne_aus_verfahrensgebiet(
|
||||
verf_layer, active_layer
|
||||
)
|
||||
apply_style(verf_layer, "verfahrensgebiet.qml")
|
||||
iface.mapCanvas().refresh()
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"Flurstücke entfernt",
|
||||
f"Entfernt: {entfernt}\n"
|
||||
f"Nicht im Verfahrensgebiet (übersprungen): {nicht_enthalten}\n"
|
||||
f"Fehlgeschlagen: {fehlgeschlagen}",
|
||||
)
|
||||
|
||||
elif aktion == "verfahrensgebiet_ersetzen":
|
||||
for lyr in existing_layers:
|
||||
QgsProject.instance().removeMapLayer(lyr.id())
|
||||
layer, erfolgreich, fehlgeschlagen = ersetze_verfahrensgebiet(active_layer)
|
||||
if not layer or not layer.isValid():
|
||||
error("Verfahrensgebiet erstellen", "Layer konnte nicht erstellt werden.")
|
||||
return
|
||||
QgsProject.instance().addMapLayer(layer)
|
||||
apply_style(layer, "verfahrensgebiet.qml")
|
||||
iface.mapCanvas().refresh()
|
||||
self.verf_layer_combo.setLayer(layer)
|
||||
self._on_verf_layer_changed(layer)
|
||||
self.setze_haken(self.haken_verf, True)
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"Verfahrensgebiet ersetzt",
|
||||
f"Verfahrensgebiet neu erstellt.\n"
|
||||
f"Erfolgreich: {erfolgreich}\n"
|
||||
f"Fehlgeschlagen: {fehlgeschlagen}",
|
||||
)
|
||||
|
||||
finally:
|
||||
self._set_busy(self.btn_verf_aus_auswahl, False, "aus Auswahl")
|
||||
|
||||
def handle_verf_aus_liste(self) -> None:
|
||||
"""Platzhalter – Verfahrensgebiet aus Liste erstellen (noch nicht implementiert)."""
|
||||
show_info_dialog(
|
||||
"Noch nicht implementiert",
|
||||
"Die Funktion 'Aus Liste' steht in einer zukünftigen Version zur Verfügung.",
|
||||
parent=self,
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Grenzpunkt-Handler
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def handle_grenz_alkis(self) -> None:
|
||||
"""Lädt Grenzpunkte gemarkungsweise aus ALKIS.
|
||||
|
||||
Verwendet denselben Gemarkungsfilter wie "Verfahrensgebiet aus ALKIS laden":
|
||||
Die Gemarkungsschlüssel werden per Spatial-Join aus der Geometrie des
|
||||
aktuell gewählten Verfahrensgebiet-Layers abgeleitet.
|
||||
"""
|
||||
# ------------------------------------------------------------------ #
|
||||
# Schritt 0: Fortschrittsdialog sofort anzeigen #
|
||||
# ------------------------------------------------------------------ #
|
||||
# 4 Vorbereitungsschritte (Geometrie, Gemarkungen, Hits, Laden) –
|
||||
# die genaue Schrittzahl wird in grenzpunkte_gemarkungsweise verfeinert.
|
||||
prog = create_progress_dialog(
|
||||
4, "Grenzpunkte aus ALKIS laden", "Verfahrensgebiet prüfen …"
|
||||
)
|
||||
prog.set_value(0)
|
||||
|
||||
# Verfahrensgebiet-Layer aus dem Dropdown holen
|
||||
vg_layer = self.verf_layer_combo.currentLayer()
|
||||
if not vg_layer or not vg_layer.isValid():
|
||||
prog.close()
|
||||
warning(
|
||||
"Grenzpunkte",
|
||||
"Kein Verfahrensgebiet-Layer gewählt. Bitte zuerst ein Verfahrensgebiet laden.",
|
||||
duration=6,
|
||||
)
|
||||
return
|
||||
|
||||
# Geometrie des ersten Features als Suchgeometrie
|
||||
feats = list(vg_layer.getFeatures())
|
||||
if not feats:
|
||||
prog.close()
|
||||
warning("Grenzpunkte", "Verfahrensgebiet-Layer enthält keine Features.", duration=5)
|
||||
return
|
||||
|
||||
# Geometrie über alle Features vereinen
|
||||
from qgis.core import QgsGeometry # type: ignore[import]
|
||||
vg_geom = QgsGeometry.unaryUnion([f.geometry() for f in feats if f.hasGeometry()])
|
||||
|
||||
if not vg_geom or vg_geom.isEmpty():
|
||||
prog.close()
|
||||
warning("Grenzpunkte", "Verfahrensgebiet-Layer hat keine Geometrie.", duration=5)
|
||||
return
|
||||
|
||||
prog.set_label("Gemarkungsschlüssel per WFS ermitteln …")
|
||||
prog.set_value(1)
|
||||
if prog.is_canceled():
|
||||
return
|
||||
|
||||
# Gemarkungsschlüssel per Spatial-Join ermitteln (identisch zu VG-Workflow)
|
||||
schluessel_namen = gemarkung_schluessel_und_namen_von_vg_geometrie(vg_geom)
|
||||
schluessel_liste = [nr for nr, _ in schluessel_namen]
|
||||
|
||||
if not schluessel_liste:
|
||||
prog.close()
|
||||
warning(
|
||||
"Grenzpunkte",
|
||||
"Keine Gemarkungen für das Verfahrensgebiet gefunden. "
|
||||
"Bitte Netzwerkverbindung prüfen.",
|
||||
duration=6,
|
||||
)
|
||||
return
|
||||
|
||||
prog.set_label(f"{len(schluessel_liste)} Gemarkung(en) gefunden – Grenzpunkte laden …")
|
||||
prog.set_value(2)
|
||||
if prog.is_canceled():
|
||||
return
|
||||
|
||||
self._set_busy(self.btn_grenz_alkis, True, "Grenzpunkte aus ALKIS laden")
|
||||
prog.close() # Vorbereitungs-Dialog schließen; grenzpunkte_gemarkungsweise öffnet eigenen
|
||||
try:
|
||||
joined_layer = grenzpunkte_gemarkungsweise(self, schluessel_liste, vg_geom=vg_geom)
|
||||
|
||||
if joined_layer and joined_layer.isValid():
|
||||
iface.mapCanvas().refresh()
|
||||
self.setze_haken(self.haken_grenz, True)
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"Grenzpunkte geladen",
|
||||
f"Grenzpunkte erfolgreich geladen ({joined_layer.featureCount()} Objekte).",
|
||||
)
|
||||
elif joined_layer is None:
|
||||
error(
|
||||
"Grenzpunkte",
|
||||
"Laden fehlgeschlagen – WFS- oder Netzwerkfehler. "
|
||||
"Details unter Ansicht -> Bedienfelder -> Protokollnachrichten "
|
||||
"(Kategorie 'sn_verfahrensgebiet'). "
|
||||
"Bitte prüfen Sie ggf. die Proxy-Einstellungen.",
|
||||
duration=0,
|
||||
)
|
||||
finally:
|
||||
self._set_busy(self.btn_grenz_alkis, False, "Grenzpunkte aus ALKIS laden")
|
||||
|
||||
def handle_umringstatistik(self) -> None:
|
||||
"""Klassifiziert Grenzpunkte auf dem VG-Umring nach Lagegenauigkeit.
|
||||
|
||||
Sucht den vorhandenen ``Grenzpunkte (ALKIS)``-Layer im Projekt und
|
||||
kombiniert ihn mit dem gewählten Verfahrensgebiet-Layer, um die
|
||||
Umringstatistik zu berechnen.
|
||||
"""
|
||||
# 1. Grenzpunkte-Layer aus dem Projekt holen
|
||||
project = QgsProject.instance()
|
||||
gp_layers = project.mapLayersByName(JOINED_LAYER_NAME)
|
||||
if not gp_layers:
|
||||
warning(
|
||||
"Umringstatistik",
|
||||
f"Kein Layer '{JOINED_LAYER_NAME}' im Projekt gefunden. "
|
||||
"Bitte zuerst 'Grenzpunkte aus ALKIS laden' ausführen.",
|
||||
duration=8,
|
||||
)
|
||||
return
|
||||
grenzpunkte_layer = gp_layers[0]
|
||||
|
||||
# 2. Verfahrensgebiet-Layer aus Dropdown
|
||||
vg_layer = self.verf_layer_combo.currentLayer()
|
||||
if not vg_layer or not vg_layer.isValid():
|
||||
warning(
|
||||
"Umringstatistik",
|
||||
"Kein Verfahrensgebiet-Layer gewählt. Bitte zuerst ein Verfahrensgebiet laden.",
|
||||
duration=6,
|
||||
)
|
||||
return
|
||||
|
||||
self._set_busy(self.btn_umring_statistik, True, "Umringstatistik Grenzpunkte")
|
||||
try:
|
||||
result_layer = umringstatistik_grenzpunkte(self, grenzpunkte_layer, vg_layer)
|
||||
if result_layer and result_layer.isValid():
|
||||
iface.mapCanvas().refresh()
|
||||
finally:
|
||||
self._set_busy(self.btn_umring_statistik, False, "Umringstatistik Grenzpunkte")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Modul-Hilfsfunktionen
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user