Compare commits

..
Author SHA1 Message Date
Daniel 5004a8d5bf Merge pull request 'Zoom auf das Verfahrensgebiet ins Projekt-KBS umrechnen (Basis #99)' (#55) from bugfix/99-zoom-kbs into unstable
Release Plugin / release (push) Successful in 4s
Reviewed-on: #55
2026-10-05 20:24:29 +00:00
DanielandClaude Opus 5.5 3bde666d96 Zoom auf das Verfahrensgebiet ins Projekt-KBS umrechnen (Basis #99)
Nach dem Laden aus ALKIS oder Shape wurde mit layer.extent() im
Layer-KBS gezoomt; bei anderem Projekt-KBS sprang die Karte an die
falsche Stelle. Beide Stellen nutzen jetzt zoom_auf_layer aus sn_basis.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 22:15:34 +02:00
Daniel e0660aba86 Merge pull request 'Grenzpunkte aus ALKIS werden im ALKIS-Stil angezeigt (#53)' (#54) from feature/53-grenzpunkt-stil-ergaenzen into unstable
Reviewed-on: #54
2026-09-17 14:31:55 +00:00
DanielandClaude Sonnet 5 84e03d208c Grenzpunkte aus ALKIS werden im ALKIS-Stil angezeigt (#53)
grenzpunkte_gemarkungsweise() ruft nach project.addMapLayer(joined_layer)
jetzt apply_style(joined_layer, "alkis_grenzpunkte.qml") auf - analog zum
bereits etablierten Muster bei Flurstücken/Verfahrensgebiet/BROR in
working_tab.py bzw. verfahrensgebiet_alkis_komplett.py.

Neuer Test test_stil_wird_nach_dem_laden_auf_den_ergebnis_layer_angewendet
mockt die komplette interne WFS-Kette (Extents, Hits, GML-Parsing,
Grenzpunkt-Batch, Join) und prüft, dass apply_style() mit dem Ergebnis-
Layer und dem richtigen Dateinamen aufgerufen wird.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 16:29:19 +02:00
Daniel 341b9ab1c4 Merge pull request 'Bugfix/16 flurstueck alkis testabgleich' (#51) from bugfix/16-flurstueck-alkis-testabgleich into unstable
Reviewed-on: #51
2026-09-17 13:57:27 +00:00
Daniel f7661a5d25 Merge branch 'unstable' into bugfix/16-flurstueck-alkis-testabgleich 2026-09-17 13:57:13 +00:00
Daniel 1aea40d028 Merge pull request 'Stildatei gemarkungen_beschriftet.qml konsequent kleingeschrieben (#50)' (#52) from bugfix/50-qml-dateiname-gross-klein into unstable
Reviewed-on: #52
2026-09-17 13:56:45 +00:00
DanielandClaude Sonnet 5 45a8ce37de Stildatei gemarkungen_beschriftet.qml konsequent kleingeschrieben (#50)
apply_style(gem_layer, "gemarkungen_beschriftet.qml") in working_tab.py
ruft die Datei klein geschrieben auf, im Repo lag sie als
Gemarkungen_beschriftet.qml. Auf Windows (case-insensitive) unsichtbar,
auf einem case-sensitiven Dateisystem (Linux/OSGeo4W) hätte das die
Datei nie gefunden - zusätzlich zum Pfadfehler in get_plugin_root()
(siehe Kommentar zu #50).

Umbenennung über zwei git mv-Schritte, da Windows/core.ignorecase eine
reine Case-Änderung sonst nicht als Rename erkennt.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 15:49:58 +02:00
DanielandClaude Sonnet 5 951b9075fe changelog.txt: Testabgleich TestFlurstueckAlkis ergänzt (#16)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 12:12:38 +02:00
DanielandClaude Sonnet 5 c260ccc2d6 TestFlurstueckAlkis an aktuelle Ladelogik angeglichen, bbox-Doku korrigiert (#16)
Test-Audit (Projektwissen #16) deckte auf: tests/test_flurstueck_alkis.py
wurde im selben Commit (f8bdcde) angelegt, in dem flurstueck_alkis() von
einer einzelnen QgsVectorLayer-Abfrage auf lade_flurstuecke_per_gemarkung()
(eine WFS-Anfrage je Gemarkung, Merge in Memory-Layer) umgestellt wurde -
die Tests mockten aber weiterhin die alte Ein-Query-Variante und liefen
seither ins Leere (8 von 12 Testfaellen rot in der Baseline).

- Tests mocken jetzt lade_flurstuecke_per_gemarkung()/_check_existing_
  flurstueck_layer() statt QgsVectorLayer direkt, passend zum seit #47
  bzw. 944fc82 aktuellen Drei-Knopf-Dialog (Ueberschreiben/Anhaengen/
  Abbrechen) und dem Pro-Gemarkung-Ladevorgang.
- test_existing_layer_keep_no -> test_existing_layer_abbrechen_returns_none:
  LoadStatus.KEEP wird von _check_existing_flurstueck_layer() nicht mehr
  zurueckgegeben, der verbleibende Nicht-Aendern-Pfad ist "Abbrechen".
- test_bbox_embedded_in_wfs_uri entfernt: bbox wird von keinem Aufrufer
  mehr uebergeben und im Code nicht mehr ausgewertet; die vermeintliche
  URI-Einbettung gab es schon lange nicht mehr.
- Modul-Docstring in flurstueck_alkis.py korrigiert: bbox ist reine
  Rueckwaertskompatibilitaet in der Signatur (Flurstueck-Geometrie kommt
  aus dem WFS je Gemarkung, keine BBox-Filterung), Memory-Layer statt
  Direktladung dokumentiert.
- Alle Docstrings von TestFlurstueckAlkis auf "Prueft, dass ..." gebracht
  (Vorgabe docs/leitfaden/06_tests.md) und inhaltlich an die tatsaechlich
  getestete Logik angepasst; Datei-Docstring ebenfalls korrigiert.
- Totes _DummyLayer.extent() entfernt (wird von keiner Aufrufkette mehr
  erreicht).

Kein Verhaltensunterschied in flurstueck_alkis() selbst - nur Tests und
Dokumentation an eine bereits bestehende Implementierung angeglichen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 12:11:13 +02:00
Daniel 49d30eea22 Merge pull request 'Drei getrennte Flurstuecke-Ladelogiken auf eine konsolidiert (#47)' (#49) from bugfix/47-flurstuecke-ladelogik-konsolidieren into unstable
Reviewed-on: #49
2026-09-15 08:31:25 +00:00
DanielandClaude Sonnet 5 0ac0757e42 Drei getrennte Flurstuecke-Ladelogiken auf eine konsolidiert (#47)
handle_flurst_aus_gemarkungen() hatte eine eigene Kopie des
"Layer bereits vorhanden"-Dialogs und rief lade_flurstuecke_per_gemarkung
direkt auf, statt die bereits vorhandene, getestete Logik in
flurstueck_alkis() wiederzuverwenden. Grund war, dass ohne_dialog=True
zwei unabhaengige Dinge zugleich abschaltete: die Schluessel-Abfrage per
ask_text UND den Existenz-Dialog samt Anhaengen-Dedup.

Neuer Parameter ohne_schluessel_abfrage trennt beides: er ueberspringt
nur die Schluessel-Abfrage (Schluessel kommen bereits aus der
Gemarkungen-Layer-Auswahl), der Existenz-Dialog mit Dedup bleibt aktiv.
ohne_dialog bleibt fuer die automatischen Pipelines unveraendert.

handle_flurst_alkis() und handle_flurst_aus_gemarkungen() teilen sich
jetzt zusaetzlich die Nachbehandlung des Ladeergebnisses
(_uebernimm_flurstuecke_ergebnis). lade_flurstuecke_per_gemarkung() wird
wieder nur noch von einer Stelle aus aufgerufen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-15 10:26:25 +02:00
Daniel e267d51607 Merge pull request 'Bugfix/28 doppelte flurstuecke' (#48) from bugfix/28-doppelte-flurstuecke into unstable
Reviewed-on: #48
2026-09-15 08:06:56 +00:00
DanielandClaude Sonnet 5 003ad1957b Zweite Anhaengen-Pipeline (Gemarkungen-Button) auf Dedup umgestellt (#28)
handle_flurst_aus_gemarkungen in working_tab.py hatte eine eigene,
von flurstueck_alkis() unabhaengige Anhaengen-Logik, die
lade_flurstuecke_per_gemarkung direkt aufrief und ungefiltert
anhaengte. Nutzt jetzt dieselbe flurstueckskennzeichen-Pruefung wie
der Fix im vorigen Commit; dafuer vorhandene_kennzeichen() in
flurstueck_alkis.py oeffentlich gemacht.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-15 09:17:30 +02:00
DanielandClaude Sonnet 5 821bb0640e Doppeltes Laden von Flurstuecken beim Anhaengen verhindert (#28)
Beim Anhaengen an einen bestehenden Flurstueck-Layer wurden alle neu
geladenen Objekte unabhaengig davon hinzugefuegt, ob sie ueber
flurstueckskennzeichen bereits im Layer vorhanden waren. Jetzt werden
nur noch neue Kennzeichen angehaengt; fehlt das Feld, bleibt das
bisherige Verhalten als Rueckfall erhalten.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-15 08:48:14 +02:00
Daniel 04f2a49c13 Merge pull request 'Fehlenden Umlaut in Docstring behoben (Plugin_SN_Basis #42)' (#46) from bugfix/42-umlaute-sonderzeichen into unstable
Reviewed-on: #46
2026-09-15 05:59:46 +00:00
DanielandClaude Sonnet 5 eac3b44470 Fehlenden Umlaut in Docstring behoben (Plugin_SN_Basis #42)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MneDjzrjmbpQt8udFYxN9e
2026-09-14 12:19:57 +02:00
22ottomi 0a906d3ac7 Merge pull request 'FBSchl-Feldsuche tolerant gegenueber abweichenden Feldnamen' (#45) from fix-fbschl-feldname into unstable
Release Plugin / release (push) Successful in 3s
Reviewed-on: #45
2026-09-02 11:44:47 +02:00
22ottomi 39f8209b9d Merge pull request 'Nur-laden-Option bei ALKIS-Laden durch Stop-Abfrage ersetzt' (#43) from alkis-nur-laden-stop into unstable
Reviewed-on: #43
2026-09-02 11:44:36 +02:00
Michael OttoandClaude Sonnet 5 3c0e6e5a19 FBSchl-Feldsuche tolerant gegenüber abweichenden Feldnamen
Manche Shape-Exporte liefern das Feld "FBSchl" mit angehängtem Punkt
(z.B. "FBSchl.") oder abweichender Groß-/Kleinschreibung. Der bisherige
exakte Namensvergleich (fields().indexFromName) erkannte das Feld dann
nicht und meldete fälschlich "Feld fehlt", die Datei wurde nicht
eingelesen.

- _normalisiere_feldname(): entfernt alle Nicht-alphanumerischen Zeichen
  und normalisiert auf Kleinbuchstaben
- _finde_fbschl_index(): exakter Namensabgleich zuerst, sonst Fallback auf
  normalisierten Vergleich über alle Felder

Fixes #44

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-02 11:43:34 +02:00
Michael OttoandClaude Sonnet 5 c93776d19e Nur-laden-Option bei ALKIS-Laden durch Stop-Abfrage ersetzt
- Checkbox "Nur Objekte laden (ohne weitere Prüfungen)" im
  Landkreis-Auswahldialog entfernt
- Stattdessen nach dem Laden/Übernehmen des BROR-Objekts eine einheitliche
  Ja/Nein-Stop-Abfrage ("Weiter?") vor Dialog 2 (Gemarkung/Flurstücke/
  Flächenvergleich) - analog zum Ablauf bei "Aus Shape laden"
- Gilt jetzt für Verfahrensnummer- und Landkreis-Zweig gleichermaßen,
  vorher gab es die Option nur im Landkreis-Zweig

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-02 09:21:58 +02:00
22ottomi 70c14783c5 Merge pull request 'Shape-Ladevorgang: Mehrfachauswahl, ZIP-Temp-Entpacken, FBSchl-Pruefung' (#42) from aus-shape-laden into unstable
Release Plugin / release (push) Successful in 4s
Reviewed-on: #42
2026-09-02 08:40:02 +02:00
Michael OttoandClaude Sonnet 5 b4f2222b50 Shape-Ladevorgang: Mehrfachauswahl, ZIP-Temp-Entpacken, FBSchl-Prüfung
- Dateidialog erlaubt Mehrfachauswahl von .zip/.shp; ZIPs werden in ein
  temporäres Verzeichnis entpackt statt dauerhaft neben der ZIP-Datei
  (Cleanup nach dem Einlesen)
- Je Shapefile wird das Feld FBSchl geprüft und in Neuordnungs- bzw.
  Neuvermessungsgebiet unterschieden; Geometrien gleichen Typs werden über
  alle Dateien hinweg zu je einem Layer verschmolzen und entsprechend
  benannt ("Neuordnungsgebiet (NoBIS)" / "Neuvermessungsgebiet (NoBIS)")
- Fehler einzelner Dateien/Shapefiles brechen den Vorgang nicht mehr ab,
  sondern werden gesammelt und am Ende in einer Meldung ausgegeben
- Fortschrittsdialog erscheint erst nach dem Stop nach dem Laden, nicht
  mehr schon vor dem Dateidialog
- Vorprüfung auf gesetzte Verfahrensnummer vorerst auskommentiert
- Ablauf-Dokumentation aufgeteilt in aus_alkis_laden.md und
  aus_shape_laden.md (+ technische Ergänzung aus_shape_laden_technisch.md)

Löst #36, #37, #38

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-02 08:36:13 +02:00
Michael Otto 8447a2615c Temporärer Weiter?-Stopp nach 'Verfahrensgebiet aus ZIP laden' + Ablauf-Doku
- verfahrensgebiet_shape_komplett.py: TEMPORÄRER STOPP (wieder entfernen)
  zur Zwischenkontrolle nach dem ZIP-Import. Layer wird dafür schon an
  dieser Stelle sichtbar ins Projekt eingefügt statt erst nach Schritt 2;
  bei "Nein" bleibt der Layer zur Inspektion erhalten (abgeschlossen=True),
  der spätere reguläre addMapLayer-Aufruf prüft auf Duplikat.
- docs/ablauf_verfahrensgebiet_laden.md: Mermaid-Diagramm der Lade-Abläufe
  (ALKIS/Shape) bis zu den jeweiligen frühen Stopps.

Lokaler Debug-Branch, bewusst nicht für unstable vorgesehen.
2026-08-31 09:20:50 +02:00
17 changed files with 2192 additions and 294 deletions
+5
View File
@@ -1,3 +1,8 @@
- 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)
+69
View File
@@ -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.
+20
View File
@@ -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
```
+127
View File
@@ -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.
+61 -14
View File
@@ -3,8 +3,9 @@
Lädt Flurstücke aus dem ALKIS-WFS-Dienst (``adv:AX_Flurstueck``) anhand von
Gemarkungsschlüsseln, die der Nutzer interaktiv eingibt.
Der WFS-Layer wird direkt (nicht als Memory-Layer) ins Projekt geladen,
da die einzelnen Features für die spätere Auswahl benötigt werden.
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:**
@@ -16,10 +17,11 @@ da die einzelnen Features für die spätere Auswahl benötigt werden.
- ``vorgabe_namen``: Zugehörige Bezeichnungen, die im Dialog als Hinweis
angezeigt werden (kein eigenständiges Feld, nur Anzeigetext).
- ``bbox``: Optionaler BBox-Parameter, der – sofern übergeben – als
zusätzlicher serverseitiger Filter in die WFS-URI eingebettet wird. So
werden ausschließlich Flurstücke geladen, die den Verfahrensgebiets-Rahmen
überlappen.
- ``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
@@ -37,6 +39,7 @@ 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):
@@ -74,6 +77,24 @@ def _check_existing_flurstueck_layer(tab_widget):
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.
@@ -152,6 +173,7 @@ def flurstueck_alkis(
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.
@@ -165,17 +187,27 @@ def flurstueck_alkis(
Eltern-Widget für Dialoge.
vorgabe_schluessel : list of str, optional
Vorausgefüllte Gemarkungsnummern (4-stellig). Im Dialog als Standard
angezeigt; bei ``ohne_dialog=True`` direkt verwendet.
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 die Gemarkungsschlüssel-Nutzerabfrage und verwendet
``vorgabe_schluessel`` direkt. Vorhandene Layer werden ohne Rückfrage
ersetzt. Gedacht für die automatische Pipeline
(``verfahrensgebiet_alkis_komplett``). Standard: ``False``.
``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
-------
@@ -201,8 +233,8 @@ def flurstueck_alkis(
append_target = existing
# --- Schritt 2: Gemarkungsschlüssel bestimmen -------------------------
if ohne_dialog:
# Pipeline-Modus: vorgabe_schluessel direkt verwenden, kein Dialog
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()]
@@ -250,7 +282,22 @@ def flurstueck_alkis(
# --- Anhängen an vorhandenen Layer ------------------------------------
if status == LoadStatus.APPEND and append_target is not None:
append_target.dataProvider().addFeatures(list(layer.getFeatures()))
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
+2
View File
@@ -34,6 +34,7 @@ from sn_basis.functions import QgsVectorLayer, QgsProject
from sn_basis.functions import QMessageBox
from sn_basis.functions.message_wrapper import info, warning
from sn_basis.functions.dialog_wrapper import create_progress_dialog
from sn_basis.functions.ly_style_wrapper import apply_style
ALKIS_SF_URL = "https://geodienste.sachsen.de/aaa/public_alkis/sf/wfs"
PUNKTORT_TA_TYPENAME = "adv:AX_PunktortTA"
@@ -1652,6 +1653,7 @@ def grenzpunkte_gemarkungsweise(
return None
project.addMapLayer(joined_layer)
apply_style(joined_layer, "alkis_grenzpunkte.qml")
prog.set_value(prog_gesamt)
prog.close()
+11 -12
View File
@@ -261,12 +261,12 @@ def _baue_layer_aus_landkreis_auswahl(tab_widget, kombinationen: dict, crs: str,
"""Auswahl + Bau für den Landkreis-Fallback.
Zeigt :func:`~sn_verfahrensgebiet.functions.verfahrensgebiet_dialoge.ask_landkreis_objekte`
(Mehrfachauswahl inkl. "Alle auswählen" und "Nur laden"-Checkbox). 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.
(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
@@ -275,13 +275,12 @@ def _baue_layer_aus_landkreis_auswahl(tab_widget, kombinationen: dict, crs: str,
status: Lade-Status aus Phase 1 (FIRST/RELOAD), wird durchgereicht
Returns:
Tuple[Optional[QgsVectorLayer], LoadStatus, bool]:
``(layer, status, nur_laden)``.
Tuple[Optional[QgsVectorLayer], LoadStatus]: ``(layer, status)``.
"""
key_list = sorted(kombinationen.keys())
selected_keys, nur_laden = ask_landkreis_objekte(tab_widget, key_list)
selected_keys = ask_landkreis_objekte(tab_widget, key_list)
if not selected_keys:
return None, LoadStatus.NONE, False
return None, LoadStatus.NONE
# Nach VKZ (erste 5 Zeichen der bezeichnung) gruppieren
vkz_gruppen: dict[str, list] = {}
@@ -322,12 +321,12 @@ def _baue_layer_aus_landkreis_auswahl(tab_widget, kombinationen: dict, crs: str,
if not neue_features:
QMessageBox.critical(tab_widget, "Fehler", "Keine Geometrien zum Verschmelzen gefunden.")
return None, LoadStatus.NONE, False
return None, LoadStatus.NONE
pr.addFeatures(neue_features)
memory_layer.updateExtents()
return memory_layer, status, nur_laden
return memory_layer, status
def verfahrensgebiet_alkis(tab_widget, skip_existing_check: bool = False):
+21 -9
View File
@@ -51,6 +51,7 @@ 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
@@ -273,9 +274,13 @@ def verfahrensgebiet_alkis_komplett(
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 (direkt nach Dialog 1 bei eindeutigem
Treffer/KEEP, oder erst nach Bestätigung des Auswahldialogs mit OK).
das Verfahrensgebiet feststeht und die Stop-Abfrage mit "Ja" bestätigt
wurde.
Schrittfolge in Dialog 2:
@@ -351,8 +356,6 @@ def verfahrensgebiet_alkis_komplett(
# Dialog 1 schließt hier immer – egal ob eine Auswahl folgt oder nicht.
progress_suche.close()
nur_laden = False
try:
if vorab_layer is not None or not kombinationen:
# KEEP oder Fehler/Abbruch bereits in Phase 1 entschieden
@@ -364,7 +367,7 @@ def verfahrensgebiet_alkis_komplett(
# Objekt(e) des Landkreises auswählen – kein eigener Fortschritts-
# dialog währenddessen, nur der Auswahldialog selbst.
# ------------------------------------------------------------------
alkis_vg_layer, vg_status, nur_laden = _baue_layer_aus_landkreis_auswahl(
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:
@@ -387,12 +390,21 @@ def verfahrensgebiet_alkis_komplett(
project.addMapLayer(alkis_vg_layer)
bror_was_added_by_us = True
# Style direkt nach dem Laden anwenden – unabhängig vom weiteren
# Ablauf, damit auch "Nur Objekte laden" den Style erhält.
# Ablauf, damit auch bei "Nein" auf die Stop-Abfrage der Style
# gesetzt ist.
apply_style(alkis_vg_layer, BROR_STYLE_QML)
if nur_laden:
# Nutzer hat "Nur laden" angehakt – kein Dialog 2, kein weiterer
# Workflow (Gemarkung/Flurstücke/Vergleich).
# ------------------------------------------------------------------
# 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
+10 -13
View File
@@ -101,15 +101,15 @@ def ask_bror_kombinationen(
def ask_landkreis_objekte(
parent: Any,
kombinationen: list[tuple[str, str]],
) -> tuple[list[tuple[str, str]], bool]:
) -> 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) und einer "Nur laden"-Checkbox, mit der gesteuert
wird, ob nach dem Laden der weitere Workflow (Gemarkung/Flurstücke/
Flächenvergleich) fortgesetzt werden soll.
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
----------
@@ -120,16 +120,16 @@ def ask_landkreis_objekte(
Returns
-------
tuple[list[tuple[str, str]], bool]
``(ausgewaehlte_kombinationen, nur_laden)``. ``([], False)`` bei
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 [], False
return []
if QT_VERSION == 0: # Mock-Modus
print(f"Mock-Modus: ask_landkreis_objekte → {kombinationen[:1]}")
return kombinationen[:1], False
return kombinationen[:1]
dialog = QDialog(parent)
dialog.setWindowTitle("Verfahrensgebiete aus Landkreis laden")
@@ -204,9 +204,6 @@ def ask_landkreis_objekte(
mehrfachauswahl_hinweis = QLabel("<small>Mehrfachauswahl mit Strg + Umschalt</small>")
layout.addWidget(mehrfachauswahl_hinweis)
nur_laden_checkbox = QCheckBox("Nur Objekte laden (ohne weitere Prüfungen)")
layout.addWidget(nur_laden_checkbox)
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
buttons.accepted.connect(dialog.accept)
buttons.rejected.connect(dialog.reject)
@@ -235,11 +232,11 @@ def ask_landkreis_objekte(
dialog.resize(breite, dialog.sizeHint().height())
if exec_dialog(dialog) != DialogAccepted:
return [], False
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, nur_laden_checkbox.isChecked()
return ausgewaehlt
def ask_landkreis_auswahl(
+397 -67
View File
@@ -3,8 +3,10 @@
Orchestriert den vollständigen Arbeitsablauf zum Laden eines Verfahrensgebiets
aus einem ZIP-Archiv mit Shapefile-Set:
1. Dateidialog öffnen (.zip-Filter); Nutzerabbruch beendet den Vorgang.
2. ZIP neben der ZIP-Datei entpacken und auf ein gültiges Shapefile-Set prüfen.
1. Dateidialog öffnen (.zip/.shp-Filter, Mehrfachauswahl); Nutzerabbruch
beendet den Vorgang.
2. ZIP in ein temporäres Verzeichnis entpacken (danach wieder gelöscht) und
auf ein gültiges Shapefile-Set prüfen.
3. Shapefile als QGIS-Vektorlayer laden und validieren (Standard-QGIS-Routinen).
4. Alle Features per ``QgsGeometry.unaryUnion`` zu einem Single-Feature-
Memory-Layer „Verfahrensgebiet" verschmelzen.
@@ -40,6 +42,9 @@ oder kein Ergebnis liefert, bricht den Workflow ab.
from __future__ import annotations
import os
import re
import shutil
import tempfile
import zipfile
from typing import Optional, Tuple
@@ -53,6 +58,7 @@ from sn_basis.functions.dialog_wrapper import (
create_progress_dialog,
show_info_dialog,
ask_detailpruefung_oder_abbrechen,
ask_yes_no,
)
from sn_verfahrensgebiet.functions.verfahrensgebiet_alkis import LoadStatus, verfahrensgebiet_alkis
@@ -79,11 +85,129 @@ from sn_verfahrensgebiet.functions.verfahrensgebiet_alkis_komplett import (
BROR_LAYER_NAME,
)
#: Feldname im Shapefile, über den der Gebietstyp bestimmt wird.
FBSCHL_FELD = "FBSchl"
#: FBSchl-Wert für ein Neuordnungsgebiet.
FBSCHL_NEUORDNUNGSGEBIET = "901/10110F"
#: FBSchl-Wert für ein Neuvermessungsgebiet.
FBSCHL_NEUVERMESSUNGSGEBIET = "901/10120F"
#: Anzeigename des Layers für ein Neuordnungsgebiet (NoBIS).
NEUORDNUNGSGEBIET_LAYER_NAME = "Neuordnungsgebiet (NoBIS)"
#: Anzeigename des Layers für ein Neuvermessungsgebiet (NoBIS).
NEUVERMESSUNGSGEBIET_LAYER_NAME = "Neuvermessungsgebiet (NoBIS)"
#: Zuordnung Gebietstyp → Layername.
_GEBIETSTYP_LAYER_NAMEN = {
"Neuordnungsgebiet": NEUORDNUNGSGEBIET_LAYER_NAME,
"Neuvermessungsgebiet": NEUVERMESSUNGSGEBIET_LAYER_NAME,
}
# ---------------------------------------------------------------------------
# Interne Hilfsfunktionen
# ---------------------------------------------------------------------------
def _normalisiere_feldname(name: str) -> str:
"""Normalisiert einen Feldnamen für den toleranten Vergleich.
Entfernt alle Zeichen außer Buchstaben/Ziffern (z.B. Punkte, Leerzeichen,
Unterstriche) und wandelt in Kleinbuchstaben um. Hintergrund: manche
Shape-Exporte liefern den Feldnamen ``FBSchl`` mit einem angehängten
Punkt (``FBSchl.``) oder abweichender Groß-/Kleinschreibung, wodurch ein
exakter Namensvergleich das Feld fälschlich als fehlend meldet (#44).
Parameters
----------
name :
Roher Feldname.
Returns
-------
str
Normalisierter Feldname (nur Kleinbuchstaben/Ziffern).
"""
return re.sub(r"[^a-zA-Z0-9]", "", name).lower()
def _finde_fbschl_index(raw_layer) -> int:
"""Sucht den Feldindex von ``FBSchl`` tolerant gegenüber Abweichungen im
Namen (Groß-/Kleinschreibung, angehängte Sonderzeichen wie ein Punkt).
Versucht zuerst den exakten Namen; schlägt das fehl, wird über alle
Felder mit normalisiertem Namensvergleich gesucht (siehe
:func:`_normalisiere_feldname`).
Parameters
----------
raw_layer :
Dateibasierter Shapefile-Layer.
Returns
-------
int
Feldindex, oder ``-1`` wenn kein passendes Feld gefunden wurde.
"""
idx = raw_layer.fields().indexFromName(FBSCHL_FELD)
if idx >= 0:
return idx
ziel = _normalisiere_feldname(FBSCHL_FELD)
for feld_idx, feld in enumerate(raw_layer.fields()):
if _normalisiere_feldname(feld.name()) == ziel:
return feld_idx
return -1
def _ermittle_gebietstyp_aus_fbschl(raw_layer) -> Tuple[Optional[str], Optional[str]]:
"""Prüft das Feld ``FBSchl`` und ermittelt den Gebietstyp.
Zeigt selbst keine Fehlermeldung an, sondern gibt einen Fehlertext
zurück, damit der Aufrufer Fehler mehrerer Dateien sammeln und am Ende
gemeinsam anzeigen kann.
Die Feldsuche ist tolerant gegenüber Abweichungen im Namen (z.B. ein
angehängter Punkt ``FBSchl.`` in manchen Shape-Exporten, siehe #44) –
siehe :func:`_finde_fbschl_index`.
Parameters
----------
raw_layer :
Dateibasierter Shapefile-Layer (vor dem Verschmelzen zum Memory-Layer).
Returns
-------
Tuple[Optional[str], Optional[str]]
``(gebietstyp, fehlertext)``. Bei Erfolg ist ``gebietstyp``
``"Neuordnungsgebiet"`` oder ``"Neuvermessungsgebiet"`` und
``fehlertext`` ``None``. Bei Fehler ist ``gebietstyp`` ``None`` und
``fehlertext`` beschreibt die Ursache (fehlendes Feld oder
unbekannter Wert).
"""
idx = _finde_fbschl_index(raw_layer)
if idx < 0:
return None, f"Feld '{FBSCHL_FELD}' fehlt (Shape entspricht nicht dem erwarteten Inhalt)."
wert = None
for feat in raw_layer.getFeatures():
try:
roh = feat.attribute(idx)
except Exception:
roh = None
if roh not in (None, ""):
wert = str(roh).strip()
break
if wert == FBSCHL_NEUORDNUNGSGEBIET:
return "Neuordnungsgebiet", None
if wert == FBSCHL_NEUVERMESSUNGSGEBIET:
return "Neuvermessungsgebiet", None
return None, (
f"Unbekannter Wert im Feld '{FBSCHL_FELD}': {wert!r} "
f"(erwartet: '{FBSCHL_NEUORDNUNGSGEBIET}' oder "
f"'{FBSCHL_NEUVERMESSUNGSGEBIET}')."
)
def _wende_vergleich_stil_an(layer) -> None:
"""Wendet einen kategorisierten Renderer auf den Vergleich-Layer an.
@@ -215,6 +339,31 @@ def _finde_shp(verzeichnis: str) -> Optional[str]:
return None
def _finde_alle_shp(verzeichnis: str) -> list:
"""Sucht rekursiv nach allen ``.shp``-Dateien im Verzeichnis.
Ein ZIP-Archiv kann mehrere Shapefile-Sets enthalten (z.B. je eines für
Neuordnungs- und Neuvermessungsgebiet) – im Unterschied zu
:func:`_finde_shp` werden hier alle gefunden, nicht nur die erste.
Parameters
----------
verzeichnis :
Startverzeichnis für die Suche.
Returns
-------
list
Sortierte Liste der absoluten Pfade aller gefundenen ``.shp``-Dateien.
"""
treffer = []
for root, _dirs, files in os.walk(verzeichnis):
for fname in files:
if fname.lower().endswith(".shp"):
treffer.append(os.path.join(root, fname))
return sorted(treffer)
def _erstelle_memory_layer_aus_shp(shp_layer) -> Optional[object]:
"""Vereint alle Features des Shapefile-Layers zu einem Single-Feature-
Memory-Layer „Verfahrensgebiet" via ``QgsGeometry.unaryUnion``.
@@ -290,17 +439,38 @@ def _erstelle_memory_layer_aus_shp(shp_layer) -> Optional[object]:
# Öffentliche Lade-Hilfsfunktion
# ---------------------------------------------------------------------------
def lade_vg_aus_zip(parent_widget) -> Optional[object]:
"""Öffnet Dateidialog, entpackt ZIP und gibt Union-Memory-Layer zurück.
def lade_vg_aus_zip(parent_widget) -> Optional[list]:
"""Öffnet Dateidialog (Mehrfachauswahl), entpackt ZIPs (bzw. nutzt direkt
gewählte ``.shp``-Dateien) und liefert je Gebietstyp einen Union-Memory-Layer.
Schrittfolge:
1. ``QFileDialog.getOpenFileName`` mit Filter ``ZIP-Dateien (*.zip)``.
2. ZIP neben der Quelldatei in ein Unterverzeichnis entpacken.
3. Erste ``.shp``-Datei im Verzeichnis finden.
4. Als ``QgsVectorLayer`` laden und ``isValid()`` prüfen.
5. Alle Features per ``QgsGeometry.unaryUnion`` verschmelzen.
6. Single-Feature-Memory-Layer „Verfahrensgebiet" zurückgeben.
1. ``QFileDialog.getOpenFileNames`` mit Filter für ``.zip`` und ``.shp``
(Mehrfachauswahl möglich).
2. Für jede ausgewählte Datei: bei ``.zip`` in ein temporäres Verzeichnis
entpacken (danach wieder gelöscht) und darin **alle** enthaltenen
``.shp``-Dateien finden (ein ZIP kann mehrere Shapefile-Sets
bündeln, z.B. je eines für Neuordnungs- und Neuvermessungsgebiet);
bei ``.shp`` die Datei direkt verwenden.
3. Jede gefundene Shapefile-Datei einzeln als ``QgsVectorLayer`` laden
und ``isValid()`` prüfen.
4. Je Shapefile-Datei das Feld ``FBSchl`` prüfen und den Gebietstyp
ermitteln (Neuordnungs- bzw. Neuvermessungsgebiet).
5. Die Features aller Dateien **desselben Gebietstyps** werden gemeinsam
per ``QgsGeometry.unaryUnion`` zu einem Single-Feature-Memory-Layer
verschmolzen – d.h. mehrere Neuordnungsgebiet-Dateien ergeben einen
Layer, mehrere Neuvermessungsgebiet-Dateien einen zweiten.
Fehlerverhalten: Schlägt die Prüfung eines einzelnen Shapefiles fehl
(ZIP nicht entpackbar, kein Shapefile enthalten, ungültiger Layer, Feld
``FBSchl`` fehlt oder hat einen unbekannten Wert), wird **nur dieses
Shapefile** übersprungen und sein Fehler gesammelt – das gilt auch,
wenn mehrere Shapefiles in derselben ZIP-Datei liegen: eine fehlerhafte
Datei darin bricht nicht die Verarbeitung der übrigen ab. Alle
verwertbaren Dateien werden gemäß Vorgabe eingelesen. Am Ende erscheint
eine gesammelte Fehlermeldung mit allen fehlerhaften Dateien. Erst wenn
**keine** Datei verwertbare Geometrien liefert, wird ``None``
zurückgegeben.
Parameters
----------
@@ -309,62 +479,173 @@ def lade_vg_aus_zip(parent_widget) -> Optional[object]:
Returns
-------
Optional[QgsVectorLayer]
Valider Memory-Layer „Verfahrensgebiet" oder ``None`` bei Abbruch/Fehler.
Optional[list]
Liste der erzeugten Memory-Layer (ein Layer je in der Auswahl
vorkommendem, erfolgreich geprüftem Gebietstyp, max. 2), oder
``None`` wenn keine Datei verwertet werden konnte oder der Dialog
abgebrochen wurde.
"""
pfad, _ = QFileDialog.getOpenFileName(
pfade, _ = QFileDialog.getOpenFileNames(
parent_widget,
"ZIP-Datei mit Shapefile-Set auswählen",
"Shapefile-Set(s) (ZIP oder .shp) auswählen",
"",
"ZIP-Dateien (*.zip)",
"Shapefile-Set (*.zip *.shp);;ZIP-Dateien (*.zip);;Shapefile (*.shp)",
)
if not pfad:
if not pfade:
return None # Nutzerabbruch
zip_dir = os.path.dirname(pfad)
zip_base = os.path.splitext(os.path.basename(pfad))[0]
extract_dir = os.path.join(zip_dir, zip_base)
# Je Gebietstyp gesammelte Geometrien/Felder/Attribute über alle
# ausgewählten Dateien hinweg (Reihenfolge der ersten Sichtung bleibt
# über dict-Einfügereihenfolge erhalten).
geometrien_je_typ: dict = {}
felder_je_typ: dict = {}
attribute_je_typ: dict = {}
crs_je_typ: dict = {}
temp_dirs: list = []
fehler_liste: list = []
try:
with zipfile.ZipFile(pfad, "r") as zf:
zf.extractall(extract_dir)
except Exception:
QMessageBox.warning(
parent_widget,
"Fehler",
"Die ZIP-Datei konnte nicht entpackt werden.\n"
"Bitte eine gültige ZIP-Datei auswählen.",
)
return None
# (shp_path, anzeige_name) für jede tatsächlich zu prüfende
# Shapefile-Datei sammeln – ein ZIP kann mehrere Shapefile-Sets
# enthalten, jedes wird einzeln behandelt.
shp_eintraege: list = []
shp_path = _finde_shp(extract_dir)
if not shp_path:
QMessageBox.warning(
parent_widget,
"Fehler",
"gültiges Shape-Dateiset angeben",
)
return None
for pfad in pfade:
dateiname = os.path.basename(pfad)
raw_layer = QgsVectorLayer(shp_path, "tmp_shape_vg", "ogr")
if not raw_layer or not raw_layer.isValid():
QMessageBox.warning(
parent_widget,
"Fehler",
"gültiges Shape-Dateiset angeben",
)
return None
if pfad.lower().endswith(".shp"):
shp_eintraege.append((pfad, dateiname))
continue
mem_layer = _erstelle_memory_layer_aus_shp(raw_layer)
if mem_layer is None:
QMessageBox.warning(
parent_widget,
"Fehler",
"gültiges Shape-Dateiset angeben",
)
return None
extract_dir = tempfile.mkdtemp(prefix="sn_vg_shape_")
temp_dirs.append(extract_dir)
return mem_layer
try:
with zipfile.ZipFile(pfad, "r") as zf:
zf.extractall(extract_dir)
except Exception:
fehler_liste.append(
f"{dateiname}: ZIP-Datei konnte nicht entpackt werden."
)
continue
shp_pfade_im_zip = _finde_alle_shp(extract_dir)
if not shp_pfade_im_zip:
fehler_liste.append(
f"{dateiname}: kein gültiges Shape-Dateiset enthalten."
)
continue
for shp_path in shp_pfade_im_zip:
anzeige_name = (
f"{dateiname} ({os.path.basename(shp_path)})"
if len(shp_pfade_im_zip) > 1
else dateiname
)
shp_eintraege.append((shp_path, anzeige_name))
# ------------------------------------------------------------------
# Jede gefundene Shapefile-Datei einzeln prüfen und einlesen.
# ------------------------------------------------------------------
for shp_path, anzeige_name in shp_eintraege:
raw_layer = QgsVectorLayer(shp_path, "tmp_shape_vg", "ogr")
if not raw_layer or not raw_layer.isValid():
fehler_liste.append(
f"{anzeige_name}: kein gültiges Shape-Dateiset."
)
continue
gebietstyp, fehlertext = _ermittle_gebietstyp_aus_fbschl(raw_layer)
if gebietstyp is None:
fehler_liste.append(f"{anzeige_name}: {fehlertext}")
del raw_layer
continue
geom_list = []
first_feat = None
for feat in raw_layer.getFeatures():
try:
geom = feat.geometry()
except Exception:
geom = None
if geom and not geom.isEmpty():
geom_list.append(geom)
if first_feat is None:
first_feat = feat
if geom_list:
geometrien_je_typ.setdefault(gebietstyp, []).extend(geom_list)
if gebietstyp not in felder_je_typ:
felder_je_typ[gebietstyp] = list(raw_layer.fields())
attribute_je_typ[gebietstyp] = (
first_feat.attributes() if first_feat is not None else []
)
crs_je_typ[gebietstyp] = raw_layer.crs().authid()
else:
fehler_liste.append(f"{anzeige_name}: keine gültigen Geometrien enthalten.")
# Referenz auf den dateibasierten Layer freigeben, bevor das
# temporäre Verzeichnis gelöscht wird (Windows hält sonst ggf.
# noch einen Dateihandle auf das Shapefile).
del raw_layer
if fehler_liste:
meldung = (
"Folgende Dateien konnten nicht eingelesen werden:\n\n"
+ "\n".join(fehler_liste)
)
if geometrien_je_typ:
meldung += "\n\nDie übrigen Dateien wurden gemäß Vorgabe eingelesen."
QMessageBox.warning(parent_widget, "Fehler in ausgewählten Dateien", meldung)
if not geometrien_je_typ:
return None
ergebnis_layer = []
for typ, geometrien in geometrien_je_typ.items():
union_geom = QgsGeometry.unaryUnion(geometrien)
if not union_geom or union_geom.isEmpty():
continue
mem_layer = QgsVectorLayer(
f"MultiPolygon?crs={crs_je_typ[typ]}",
_GEBIETSTYP_LAYER_NAMEN[typ],
"memory",
)
pr = mem_layer.dataProvider()
try:
pr.addAttributes(felder_je_typ[typ])
mem_layer.updateFields()
except Exception:
pass
feat_out = QgsFeature(mem_layer.fields())
feat_out.setGeometry(union_geom)
try:
feat_out.setAttributes(attribute_je_typ[typ])
except Exception:
pass
pr.addFeatures([feat_out])
mem_layer.updateExtents()
if mem_layer.isValid() and mem_layer.featureCount() > 0:
mem_layer.setCustomProperty("fb_gebietstyp", typ)
ergebnis_layer.append(mem_layer)
if not ergebnis_layer:
QMessageBox.warning(
parent_widget,
"Fehler",
"gültiges Shape-Dateiset angeben",
)
return None
return ergebnis_layer
finally:
for d in temp_dirs:
shutil.rmtree(d, ignore_errors=True)
# ---------------------------------------------------------------------------
@@ -419,38 +700,66 @@ def verfahrensgebiet_shape_komplett(
"""
project = QgsProject.instance()
shape_vg_layer = None
zusatz_shape_layer: list = []
bror_layer = None
shape_was_added_by_us = False
bror_was_added_by_us = False
abgeschlossen = False
progress = create_progress_dialog(
7, "Verfahrensgebiet aus Shape laden", "Initialisierung …"
)
progress = None
try:
# ------------------------------------------------------------------
# Schritt 1: Shape-VG aus ZIP laden (noch NICHT ins Projekt einfügen)
# Läuft VOR dem Fortschrittsdialog, damit dieser nicht schon während
# des Dateidialogs sichtbar ist.
# ------------------------------------------------------------------
progress.set_label("Schritt 1/6: Verfahrensgebiet aus ZIP laden …")
progress.set_value(0)
if progress.is_canceled():
shape_vg_layers = lade_vg_aus_zip(tab_widget)
if not shape_vg_layers:
return None, None, None, LoadStatus.NONE
shape_vg_layer = lade_vg_aus_zip(tab_widget)
progress.raise_to_front()
# Je Gebietstyp ein Layer (max. 2). Der erste ist das für den
# weiteren Ablauf maßgebliche Shape-VG; etwaige weitere Layer
# (zweiter Gebietstyp) werden vorerst nur mit ins Projekt geladen –
# ihre Einbindung in den weiteren Workflow folgt in einem späteren
# Schritt.
shape_vg_layer = shape_vg_layers[0]
zusatz_shape_layer = shape_vg_layers[1:]
if not shape_vg_layer:
# TEMPORÄRER STOPP (wieder entfernen): Pause nach "Verfahrensgebiet
# aus ZIP laden" zur Zwischenkontrolle – Layer dafür schon jetzt
# sichtbar ins Projekt einfügen (normalerweise erst nach Schritt 2).
project.addMapLayer(shape_vg_layer)
shape_was_added_by_us = True
for lyr in zusatz_shape_layer:
project.addMapLayer(lyr)
geladene_namen = ", ".join(lyr.name() for lyr in shape_vg_layers)
if not ask_yes_no(
"Verfahrensgebiet aus ZIP laden",
f"Layer '{geladene_namen}' wurde(n) geladen. Weiter?",
default=True,
parent=tab_widget,
):
# abgeschlossen=True, damit der finally-Block den zur Kontrolle
# eingefügten Layer NICHT wieder entfernt (kein Fehlerfall).
abgeschlossen = True
return None, None, None, LoadStatus.NONE
vg_status = LoadStatus.FIRST
# Fortschrittsdialog erst jetzt erzeugen (nach dem Stop) – er darf
# nicht schon während Dateidialog/Stop-Abfrage sichtbar sein.
progress = create_progress_dialog(
7, "Verfahrensgebiet aus Shape laden", "Schritt 2/6: BROR aus ALKIS laden …"
)
progress.set_value(1)
# ------------------------------------------------------------------
# Schritt 2: BROR aus ALKIS laden (vor shape_vg, um Namenskonflikt
# in _check_existing_layer zu vermeiden)
# ------------------------------------------------------------------
progress.set_label("Schritt 2/6: BROR aus ALKIS laden …")
if progress.is_canceled():
return None, None, None, LoadStatus.NONE
@@ -468,9 +777,25 @@ def verfahrensgebiet_shape_komplett(
progress.set_value(2)
# Shape-VG erst jetzt ins Projekt einfügen (nach BROR-Ladung)
project.addMapLayer(shape_vg_layer)
shape_was_added_by_us = True
# Shape-VG erst jetzt ins Projekt einfügen (nach BROR-Ladung) – außer
# der TEMPORÄRE STOPP oben hat ihn schon eingefügt.
if not project.mapLayer(shape_vg_layer.id()):
project.addMapLayer(shape_vg_layer)
shape_was_added_by_us = True
# ------------------------------------------------------------------
# Zwischenabfrage: Nutzer kann vor dem Gemarkungsschlüssel-Schritt
# abbrechen (Shape-VG und ggf. BROR bleiben im Projekt erhalten).
# ------------------------------------------------------------------
progress.raise_to_front()
if not ask_yes_no(
"Verfahrensgebiet aus Shape laden",
"Weiter mit Gemarkungsschlüssel ermitteln?",
default=True,
parent=tab_widget,
):
abgeschlossen = True
return shape_vg_layer, None, bror_layer, vg_status
# ------------------------------------------------------------------
# Schritt 3: Gemarkungsschlüssel per Geometrie-Spatial-Join ermitteln
@@ -691,6 +1016,11 @@ def verfahrensgebiet_shape_komplett(
_entferne_hilfs_layer(project, "TempVerfahrensgebiet")
# Shape-VG und BROR bei Fehler/Abbruch entfernen
if not abgeschlossen:
for lyr in zusatz_shape_layer:
try:
project.removeMapLayer(lyr.id())
except Exception:
pass
if shape_was_added_by_us and shape_vg_layer is not None:
try:
project.removeMapLayer(shape_vg_layer.id())
+913
View File
@@ -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=" &quot;gp_abmarkung_Marke&quot; = '1000' " key="{ba76030e-eb1e-4a5a-b10c-b8614af46b1a}" label="Abgemarkt" symbol="0">
<rule filter="&quot;gp_festgestellterGrenzpunkt&quot; = 'true'" key="{0f910cf3-1e11-45f4-b4a5-5b8b11f4e2f3}" label="FGP" symbol="1"/>
</rule>
<rule filter=" &quot;gp_abmarkung_Marke&quot; IN ('9500')" key="{f530f073-c1a2-426e-9394-eb137d98b1fd}" label="Nicht abgemarkt" symbol="2">
<rule filter="&quot;gp_festgestellterGrenzpunkt&quot; = 'true'" key="{c1a0a382-cbf0-4981-b461-33c42c64701e}" label="FGP" symbol="3"/>
</rule>
<rule filter=" &quot;gp_abmarkung_Marke&quot; IN ('9600')" key="{f9d78633-b6a7-40df-88b4-d2d5bd51d1ab}" label="zeitweilig ausgesetzt" symbol="4">
<rule filter="&quot;gp_festgestellterGrenzpunkt&quot; = '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( &quot;gml_id&quot;, '&lt;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", '&lt;NULL>' )</previewExpression>
<mapTip enabled="1"></mapTip>
<layerGeometryType>0</layerGeometryType>
</qgis>
+221 -86
View File
@@ -6,8 +6,11 @@ Getestet werden:
- Nutzerabbruch bei ask_text → (None, LoadStatus.NONE)
- Ungültiger WFS-Layer → (None, LoadStatus.NONE)
- Erfolgreicher Ladevorgang → gültiger Layer + LoadStatus.FIRST
- Bestehender Layer: Nutzer wählt "Neu laden" → LoadStatus.RELOAD
- Bestehender Layer: Nutzer wählt "Behalten" → LoadStatus.KEEP
- 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
@@ -30,7 +33,12 @@ if "qgis" not in sys.modules:
sys.modules["qgis.core"] = qgis_core
sys.modules["qgis.utils"] = qgis_utils
from sn_verfahrensgebiet.functions.flurstueck_alkis import flurstueck_alkis, FLST_LAYER_NAME
from sn_verfahrensgebiet.functions.flurstueck_alkis import (
flurstueck_alkis,
lade_flurstuecke_per_gemarkung,
FLST_LAYER_NAME,
FELD_FLURSTUECKSKENNZEICHEN,
)
from sn_verfahrensgebiet.functions.verfahrensgebiet_alkis import LoadStatus
@@ -49,8 +57,8 @@ class _DummyLayer:
def id(self):
return self._id
def extent(self):
return object()
def featureCount(self):
return 1
class _DummyProject:
@@ -78,7 +86,7 @@ class TestFlurstueckAlkis(unittest.TestCase):
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance")
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text")
def test_user_cancel_returns_none(self, mock_ask_text, mock_project, mock_msgbox):
"""ask_text → ok=False, text="" → (None, NONE)."""
"""Prüft, dass ein Abbruch im Eingabedialog (ok=False) (None, LoadStatus.NONE) liefert."""
mock_ask_text.return_value = ("", False)
mock_project.return_value = _DummyProject()
@@ -91,7 +99,7 @@ class TestFlurstueckAlkis(unittest.TestCase):
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance")
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text")
def test_empty_text_after_confirmation_returns_none(self, mock_ask_text, mock_project, mock_msgbox):
"""ask_text → ok=True, aber Text leer → (None, NONE)."""
"""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()
@@ -110,7 +118,7 @@ class TestFlurstueckAlkis(unittest.TestCase):
def test_invalid_wfs_layer_returns_none(
self, mock_ask_text, mock_project, mock_wfs_layer_cls, mock_msgbox
):
"""WFS-Layer nicht valide → (None, NONE)."""
"""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)
@@ -123,67 +131,64 @@ class TestFlurstueckAlkis(unittest.TestCase):
# ------------------------------------------------------------------ #
# 3. Erfolgreicher Erstladevorgang #
# ------------------------------------------------------------------ #
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsVectorLayer")
@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_wfs_layer_cls
self, mock_ask_text, mock_project, mock_lade
):
"""Kein bestehender Layer, WFS valide → (layer, FIRST)."""
"""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_wfs_layer_cls.return_value = valid_layer
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")
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance")
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text")
def test_sql_filter_contains_all_schluessel(
self, mock_ask_text, mock_project, mock_wfs_layer_cls
):
"""Der WFS-URI enthält alle eingegebenen Gemarkungsschlüssel."""
mock_ask_text.return_value = ("1111, 2222, 3333", True)
mock_project.return_value = _DummyProject()
mock_wfs_layer_cls.return_value = _DummyLayer(valid=True)
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)
flurstueck_alkis(None)
lade_flurstuecke_per_gemarkung(["1111", "2222", "3333"])
# Erster Positionsarg von QgsVectorLayer ist der URI-String
call_args = mock_wfs_layer_cls.call_args
uri_str = call_args[0][0] if call_args[0] else call_args[1].get("path", "")
self.assertIn("1111", uri_str)
self.assertIn("2222", uri_str)
self.assertIn("3333", uri_str)
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 wählt "Neu laden" #
# 4. Bestehender Layer – Nutzer klickt "Überschreiben" #
# ------------------------------------------------------------------ #
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsVectorLayer")
@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_wfs_layer_cls
self, mock_ask_text, mock_project, mock_msgbox_cls, mock_lade
):
"""Bestehender Layer + Nutzer: 'Ja, neu laden' → LoadStatus.RELOAD."""
"""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
# StandardButton als int, damit | Operator funktioniert
mock_msgbox_cls.StandardButton.Yes = 0x4000
mock_msgbox_cls.StandardButton.No = 0x10000
# QMessageBox.question → Yes
mock_msgbox_cls.question.return_value = 0x4000
# 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_wfs_layer_cls.return_value = new_layer
mock_lade.return_value = new_layer
layer, status = flurstueck_alkis(None)
@@ -192,46 +197,46 @@ class TestFlurstueckAlkis(unittest.TestCase):
self.assertEqual(status, LoadStatus.RELOAD)
# ------------------------------------------------------------------ #
# 5. Bestehender Layer – Nutzer wählt "Behalten" #
# 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_keep_no(
def test_existing_layer_abbrechen_returns_none(
self, mock_ask_text, mock_project, mock_msgbox_cls
):
"""Bestehender Layer + Nutzer: 'Nein, behalten' → LoadStatus.KEEP."""
"""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
# StandardButton als int, damit | Operator funktioniert
mock_msgbox_cls.StandardButton.Yes = 0x4000
mock_msgbox_cls.StandardButton.No = 0x10000
# QMessageBox.question → No
mock_msgbox_cls.question.return_value = 0x10000
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.assertIs(layer, existing)
self.assertEqual(status, LoadStatus.KEEP)
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.QgsVectorLayer")
@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_wfs_layer_cls
self, mock_ask_text, mock_project, mock_lade
):
"""vorgabe_schluessel=['1234','5678'] → ask_text erhält '1234, 5678' als default_text."""
"""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_wfs_layer_cls.return_value = _DummyLayer(valid=True)
mock_lade.return_value = _DummyLayer(valid=True)
flurstueck_alkis(None, vorgabe_schluessel=["1234", "5678"])
@@ -245,16 +250,16 @@ class TestFlurstueckAlkis(unittest.TestCase):
# ------------------------------------------------------------------ #
# 7. vorgabe_namen erscheint im Label-Text #
# ------------------------------------------------------------------ #
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsVectorLayer")
@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_wfs_layer_cls
self, mock_ask_text, mock_project, mock_lade
):
"""vorgabe_namen=['Musterstadt'] → ask_text-label enthält den Namen."""
"""Prüft, dass vorgabe_namen im Beschriftungstext des Eingabedialogs erscheint."""
mock_ask_text.return_value = ("9999", True)
mock_project.return_value = _DummyProject()
mock_wfs_layer_cls.return_value = _DummyLayer(valid=True)
mock_lade.return_value = _DummyLayer(valid=True)
flurstueck_alkis(None, vorgabe_namen=["Musterstadt"])
@@ -264,49 +269,179 @@ class TestFlurstueckAlkis(unittest.TestCase):
self.assertIn("Musterstadt", label_arg)
# ------------------------------------------------------------------ #
# 8. bbox wird in WFS-URI eingebettet #
# 8. Rückwärtskompatibilität: alle neuen Params optional #
# ------------------------------------------------------------------ #
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsVectorLayer")
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance")
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text")
def test_bbox_embedded_in_wfs_uri(
self, mock_ask_text, mock_project, mock_wfs_layer_cls
):
"""bbox-Parameter → WFS-URI enthält 'bbox=' Substring."""
mock_ask_text.return_value = ("1234", True)
mock_project.return_value = _DummyProject()
mock_wfs_layer_cls.return_value = _DummyLayer(valid=True)
class _Rect:
def xMinimum(self): return 414000.0
def yMinimum(self): return 5657000.0
def xMaximum(self): return 415000.0
def yMaximum(self): return 5658000.0
flurstueck_alkis(None, bbox=_Rect())
uri_str = mock_wfs_layer_cls.call_args[0][0]
self.assertIn("bbox=", uri_str.lower())
# ------------------------------------------------------------------ #
# 9. Rückwärtskompatibilität: alle neuen Params optional #
# ------------------------------------------------------------------ #
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsVectorLayer")
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.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_wfs_layer_cls
self, mock_ask_text, mock_project, mock_lade
):
"""flurstueck_alkis(tab) ohne neue Parameter → kein Fehler."""
"""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_wfs_layer_cls.return_value = _DummyLayer(valid=True)
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()
+130
View File
@@ -43,6 +43,7 @@ if "qgis" not in sys.modules:
from sn_verfahrensgebiet.functions.grenzpunkt_alkis import (
grenzpunkt_alkis,
grenzpunkte_gemarkungsweise,
_extrahiere_gml_ids,
)
@@ -297,5 +298,134 @@ class TestGrenzpunktAlkis(unittest.TestCase):
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()
+164
View File
@@ -81,6 +81,30 @@ class _DummyCombo:
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):
@@ -96,6 +120,7 @@ class TestWorkingTab(unittest.TestCase):
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(
@@ -175,6 +200,83 @@ class TestWorkingTab(unittest.TestCase):
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
# ------------------------------------------------------------------
@@ -822,6 +924,68 @@ class TestHandleVerfShape(unittest.TestCase):
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()
+1 -1
View File
@@ -38,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)
+40 -92
View File
@@ -5,7 +5,7 @@ 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
@@ -15,12 +15,7 @@ from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
from qgis.utils import iface
from sn_verfahrensgebiet.functions.verfahrensgebiet_alkis import LoadStatus
from sn_verfahrensgebiet.functions.flurstueck_alkis import (
flurstueck_alkis,
lade_flurstuecke_per_gemarkung,
ALKIS_SF_URL,
FLST_LAYER_NAME,
)
from sn_verfahrensgebiet.functions.flurstueck_alkis import flurstueck_alkis
from sn_verfahrensgebiet.functions.verfahrensgebiet_alkis_komplett import (
verfahrensgebiet_alkis_komplett,
BROR_LAYER_NAME,
@@ -230,8 +225,8 @@ class WorkingTab(QWidget):
if not project.mapLayer(enriched_layer.id()):
project.addMapLayer(enriched_layer)
apply_style(enriched_layer, "verfahrensgebiet.qml")
iface.mapCanvas().setExtent(enriched_layer.extent())
iface.mapCanvas().refresh()
# 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)
@@ -251,15 +246,15 @@ class WorkingTab(QWidget):
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
# 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:
@@ -287,8 +282,8 @@ class WorkingTab(QWidget):
if not project.mapLayer(enriched_layer.id()):
project.addMapLayer(enriched_layer)
apply_style(enriched_layer, "verfahrensgebiet.qml")
iface.mapCanvas().setExtent(enriched_layer.extent())
iface.mapCanvas().refresh()
# 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)
@@ -392,6 +387,25 @@ class WorkingTab(QWidget):
# 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")
@@ -399,25 +413,7 @@ class WorkingTab(QWidget):
layer, status = flurstueck_alkis(self)
if not layer or not layer.isValid():
return
if status in (LoadStatus.FIRST, LoadStatus.RELOAD):
QgsProject.instance().addMapLayer(layer)
# iface.mapCanvas().setExtent(layer.extent()) # Kartenausschnitt bleibt unverändert
apply_style(layer, "GIS_Flst_Beschriftung_ALKIS_NAS.qml")
iface.mapCanvas().refresh()
self.setze_haken(self.haken_flurst, True)
msg = "erstmalig geladen" if status == LoadStatus.FIRST else "neu geladen"
success("Flurstücke", f"Layer wurde {msg}.", duration=5)
elif status == LoadStatus.KEEP:
self.setze_haken(self.haken_flurst, True)
info("Flurstücke", "Vorhandener Layer wurde behalten.", duration=4)
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)
self._uebernimm_flurstuecke_ergebnis(layer, status)
finally:
self._set_busy(self.btn_flurst_alkis, False, "Aus ALKIS laden")
@@ -491,68 +487,20 @@ class WorkingTab(QWidget):
)
return
# Prüfen ob bereits ein Flurstücke-Layer vorhanden → 3-Knopf-Dialog
existing_flst = project.mapLayersByName(FLST_LAYER_NAME)
if existing_flst:
msg_box = QMessageBox(self)
msg_box.setWindowTitle("Layer bereits vorhanden")
msg_box.setText(
f"Der Layer '{FLST_LAYER_NAME}' existiert bereits.\nWas soll geschehen?"
)
btn_ueberschreiben = msg_box.addButton("Überschreiben", QMessageBox.ButtonRole.DestructiveRole)
btn_anhaengen = msg_box.addButton("Anhängen", QMessageBox.ButtonRole.AcceptRole)
btn_abbrechen = msg_box.addButton("Abbrechen", QMessageBox.ButtonRole.RejectRole)
msg_box.setDefaultButton(btn_abbrechen)
msg_box.exec()
clicked = msg_box.clickedButton()
if clicked is btn_abbrechen:
return
if clicked is btn_anhaengen:
temp_layer = lade_flurstuecke_per_gemarkung(vorgabe_schluessel)
if not temp_layer or not temp_layer.isValid() or temp_layer.featureCount() == 0:
error(
"Flurstücke",
f"Flurstücke konnten nicht geladen werden – WFS-Dienst ({ALKIS_SF_URL}) "
f"nicht erreichbar oder keine Features für die gewählten Gemarkungen.",
duration=0,
)
return
existing_flst[0].dataProvider().addFeatures(list(temp_layer.getFeatures()))
existing_flst[0].updateExtents()
iface.mapCanvas().refresh()
self.setze_haken(self.haken_flurst, True)
success("Flurstücke", "Features wurden an den vorhandenen Layer angehängt.", duration=5)
return
# else: Überschreiben → fall-through zum flurstueck_alkis(ohne_dialog=True)-Aufruf
# Flurstücke direkt laden (kein Dialog)
# 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_dialog=True,
ohne_schluessel_abfrage=True,
)
if not layer or not layer.isValid():
error(
"Flurstücke",
f"Flurstücke konnten nicht geladen werden – WFS-Dienst ({ALKIS_SF_URL}) "
f"nicht erreichbar oder keine Features für die gewählten Gemarkungen.",
duration=0,
)
# flurstueck_alkis() hat bei Abbruch/Fehler bereits selbst eine Meldung gezeigt.
return
if status in (LoadStatus.FIRST, LoadStatus.RELOAD):
project.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)
self._uebernimm_flurstuecke_ergebnis(layer, status)
finally:
self._set_busy(self.btn_flurst_aus_gemarkungen, False, "aus gewählten Gemarkungen laden")