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
22ottomi 4e7f796840 Merge pull request 'Feature/mehrfachauswahl alkis' (#40) from feature/mehrfachauswahl-alkis into unstable
Release Plugin / release (push) Successful in 4s
Reviewed-on: #40
2026-08-31 09:13:12 +02:00
Michael Otto 63a70737f9 Merge branch 'unstable' into aus-zip-laden
Konflikte aufgelöst wie mit Daniel abgestimmt:
- functions/beteiligung_pruefung.py, tests/test_beteiligung_pruefung.py:
  unsere Version behalten (ohne silent-Parameter, Aufforderung statt
  automatischem Hintergrund-Laden bei try2/try3).
- functions/verfahrensgebiet_alkis.py, functions/verfahrensgebiet_alkis_komplett.py:
  unsere Version behalten (Zwei-Dialog-Split, Landkreis-Fallback; Daniels
  silent-Parameter dadurch nicht mehr benötigt).
- ui/tabs/working_tab.py: Docstring-Konflikt trivial aufgelöst, unsere
  Fassung behalten (beschreibt das tatsächliche Verhalten ohne silent).
- changelog.txt: beide Versionsblöcke zusammengeführt.
2026-08-31 08:53:52 +02:00
Michael OttoandClaude Sonnet 5 d5db43bc57 Beteiligungsprüfung portiert (ohne automatisches Hintergrund-Laden)
Portiert aus origin/unstable (Feature "Beteiligung prüfen": Abgleich einer
BI-Server-CSV-Exportliste mit dem ALKIS-Flurstücksbestand des
Verfahrensgebiets), bewusst OHNE den dortigen silent-Modus für
verfahrensgebiet_alkis()/verfahrensgebiet_alkis_komplett():

- try1 (Verfahrensgebiet-Layer mit Kennzeichen vorhanden) bleibt unverändert
  – schneller Mengenvergleich ohne WFS-Zugriff.
- try2 (Layer vorhanden, aber ohne verwertbare Kennzeichen) und try3 (kein
  Layer vorhanden) laden nicht mehr automatisch/silent über die komplette
  ALKIS-Pipeline, sondern fordern den Nutzer per Meldung auf, das
  Verfahrensgebiet zunächst regulär über "Aus ALKIS laden" zu laden und die
  Beteiligungsprüfung danach erneut zu starten (landet dann automatisch bei
  try1). Grund: ein automatischer Durchlauf ohne Nutzerinteraktion
  (mehrdeutige BROR-Treffer, Flächenabweichungen) hat sich in der Praxis
  nicht als zuverlässig genug erwiesen.
- verfahrensgebiet_alkis.py/verfahrensgebiet_alkis_komplett.py bleiben dadurch
  unangetastet – kein silent-Parameter, kein Konflikt mit dem heutigen
  Zwei-Dialog-Split und dem Landkreis-Fallback.
- Neuer Button "Beteiligung prüfen" in working_tab.py, delegiert an
  beteiligung_pruefen().

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-31 06:33:09 +02:00
Michael OttoandClaude Sonnet 5 7b48d29d56 Kein Fehlerdialog mehr bei Abbruch des Landkreis-Auswahldialogs
Abbruch des Dialogs ist eine bewusste Nutzerentscheidung, kein Fehlerzustand
– verfahrensgebiet_alkis_komplett() bricht jetzt still ab statt eine
QMessageBox.critical zu zeigen. Test entsprechend angepasst/umbenannt.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-28 14:44:25 +02:00
Michael OttoandClaude Sonnet 5 5816b2af22 Landkreis-Auswahldialog bei fehlenden Projekteigenschaften, Style-Fix "Nur laden"
- Ist weder Verfahrensnummer noch Landkreis in den Projekteigenschaften
  gesetzt, wird jetzt per Dialog ein Landkreis abgefragt (inkl. "Alle
  Landkreise") statt direkt abzubrechen.
- Verfahrensgebiet-spezifische ALKIS-Dialoge aus sn_basis hierher verschoben:
  neue Datei functions/verfahrensgebiet_dialoge.py
  (ask_bror_kombinationen, ask_landkreis_objekte, ask_landkreis_auswahl).
- Bugfix: BROR-Layer bekommt seinen Style (BauRaumOderBodenordnungsrecht.qml)
  jetzt direkt beim Laden in verfahrensgebiet_alkis_komplett(), unabhängig
  vom weiteren Ablauf. working_tab.py überspringt bei "Nur Objekte laden"
  jetzt den gesamten VG-Nachbehandlungsblock (Style/Extent/Haken/Combobox/
  Erfolgsmeldung), da dort kein echter VG-Layer entsteht. Detailprüfung/
  Verifikation im echten QGIS (nicht nur per Unittest) steht noch aus.
- Bugfix: Erfolgsmeldung "Layer wurde ... geladen (inkl.
  Flurstücksverschneidung)" erscheint nicht mehr bei "Nur Objekte laden".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-28 13:55:03 +02:00
Michael Otto 148d4dfa15 BROR-Mehrfachauswahl, zweistufiger Fortschrittsdialog, Landkreis-Fallback
- Fortschrittsdialog beim Laden aus ALKIS schließt jetzt zuverlässig bei
  jedem Abbruch (progress.close() zentral im finally-Block).
- Fortschrittsanzeige in zwei Dialoge aufgeteilt: Suche (1 Schritt) läuft
  und schließt separat, bevor der eigentliche Workflow (Gemarkung/
  Flurstücke/Vergleich, 6 Schritte) startet – auch während des BROR-
  Auswahldialogs läuft kein störender Hintergrund-Fortschrittsbalken mehr.
- verfahrensgebiet_alkis() in Suche-/Auswahl-Phasen aufgeteilt, damit
  mehrere gefundene BauRaumOderBodenordnungsrecht-Objekte gemeinsam
  ausgewählt und verschmolzen werden können (öffentliche Signatur
  unverändert).
- Neu: Landkreis-weite Suche als Fallback, wenn keine Verfahrensnummer
  gesetzt ist (Landkreiskennzahl aus Projektvariable), mit Mehrfach-
  auswahl/"Alle auswählen" und "Nur laden"-Option (überspringt den
  restlichen Workflow). Gruppierung der Auswahl nach VKZ, damit je
  Verfahren ein eigenes Feature entsteht statt eines Gesamt-Blobs.
- Tests entsprechend angepasst/ergänzt (29 Tests in
  test_verfahrensgebiet_alkis_komplett.py, davon 3 neu für den
  Landkreis-Fallback).
2026-08-27 15:14:07 +02:00
Daniel 6a65c9d706 changelog.txt aktualisiert
Release Plugin / release (push) Successful in 3s
2026-08-26 16:19:14 +02:00
Daniel 1a2a22f931 Merge pull request 'Beteiligung pruefen' (#34) from beteiligung_pruefen into unstable
Reviewed-on: #34
2026-08-26 16:15:53 +02:00
Daniel 4a557e66a7 try1-fix: direkte Prüfung ohne ALKIS-Pipeline 2026-08-26 16:13:51 +02:00
Daniel 6bc5256d5f try2 und try3 laden das Verfahrensgebiet mit Detailprüfung anstelle der Flurstücke vom wfs 2026-08-26 16:08:01 +02:00
Daniel 2cb6193b98 csv-header korrigiert 2026-08-26 14:30:07 +02:00
Daniel e6332e42ad Funktion angelegt, Button eingefügt 2026-08-26 12:19:46 +02:00
23 changed files with 3870 additions and 370 deletions
+18
View File
@@ -1,3 +1,21 @@
- Bugfix: Nach dem Laden des Verfahrensgebiets (ALKIS oder Shape) sprang die Karte an die falsche Stelle, wenn ein anderes Projekt-KBS als EPSG:25833 eingestellt war. Der Zoom rechnet die Ausdehnung jetzt in das KBS der Kartenansicht um (`zoom_auf_layer` aus sn_basis, setzt sn_basis mit Basis #99 voraus) (Basis #99)
- Neu: Grenzpunkte aus ALKIS werden nach dem Laden im ALKIS-Stil dargestellt (styles/alkis_grenzpunkte.qml), statt ungestylt zu bleiben (#53)
- Intern: Tests von TestFlurstueckAlkis an die seit längerem geänderte Ladelogik angeglichen (Mocks zeigten noch auf die alte Ein-Query-/Zwei-Knopf-Variante), bbox-Dokumentation in flurstueck_alkis.py korrigiert (bbox wird nicht mehr ausgewertet) – keine Verhaltensänderung (Test-Audit #16)
- Intern: Der Button "aus gewählten Gemarkungen laden" nutzt jetzt dieselbe Ladelogik wie "Flurstücke laden" (flurstueck_alkis) statt einer eigenen, parallelen Implementierung – konsolidiert drei bisher getrennte Code-Pfade zum Flurstücke-Laden auf einen (#47)
- Bugfix: Flurstücke aus ALKIS an bestehenden Layer anhängen überspringt jetzt bereits vorhandene Flurstücke (Abgleich über flurstueckskennzeichen), statt sie doppelt zu laden – betrifft sowohl den Dialog in "Flurstücke laden" als auch den Button "aus gewählten Gemarkungen laden" (#28)
- Fortschrittsdialog beim Laden aus ALKIS schließt jetzt zuverlässig bei jedem Abbruch (Bugfix)
- Fortschrittsanzeige beim Laden aus ALKIS in zwei Dialoge aufgeteilt (Suche / eigentlicher Ladevorgang), inkl. eigenem Suchschritt
- Mehrfachauswahl bei mehreren BauRaumOderBodenordnungsrecht-Treffern zur Verfahrensnummer (gemeinsames Laden/Verschmelzen mehrerer Objekte)
- Neu: Laden aller/mehrerer Objekte eines ganzen Landkreises als Fallback, wenn keine Verfahrensnummer gesetzt ist (Gruppierung je Verfahren, optional "Nur laden" ohne weiteren Ablauf)
- Neu: Ist weder Verfahrensnummer noch Landkreis in den Projekteigenschaften gesetzt, fragt ein Dialog den Landkreis ab (inkl. Option "Alle Landkreise"), statt direkt abzubrechen
- Landkreisweiter Objekt-Auswahldialog überarbeitet: Tabelle mit sortierbaren Spalten "Nummer"/"Name" (Standard: aufsteigend nach Nummer), Suchfeld, "Alle auswählen" jetzt korrekt (vorheriger Qt-Bug bei programmatischer Mehrfachauswahl behoben)
- Bugfix: BROR-Layer bekommt seinen ALKIS-Style jetzt direkt beim Laden (auch bei "Nur Objekte laden") statt später fälschlich den Verfahrensgebiet-Style zu erhalten – **Detailprüfung/Verifikation im echten QGIS-Test steht noch aus**
- Bugfix: Erfolgsmeldung "Layer wurde ... geladen (inkl. Flurstücksverschneidung)" erscheint nicht mehr fälschlich bei "Nur Objekte laden" (da dort keine Verschneidung stattfindet)
- Verfahrensgebiet-spezifische ALKIS-Dialoge aus sn_basis nach sn_verfahrensgebiet verschoben (functions/verfahrensgebiet_dialoge.py)
---
Version 26.8.1-unstable:
- Neu: "Beteiligung prüfen" – Abgleich einer BI-Server-CSV-Exportliste mit dem ALKIS-Flurstücksbestand des Verfahrensgebiets (try1: schneller Listenabgleich bei vorhandenem Kennzeichen-Bestand; try2/try3: Aufforderung, das Verfahrensgebiet zunächst über "Aus ALKIS laden" zu laden, statt automatischem Hintergrund-Laden)
---
Version 26.7.1-unstable: Version 26.7.1-unstable:
- Verfahrensgebiet als shape laden, inkl. Abgleich mit BROBOR-Objekt - Verfahrensgebiet als shape laden, inkl. Abgleich mit BROBOR-Objekt
--- ---
+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.
+452
View File
@@ -0,0 +1,452 @@
"""sn_verfahrensgebiet/functions/beteiligung_pruefung.py – Beteiligung prüfen.
AGLB-länderspezifisch (Flurstückskennzeichen-Bildung nach sächsischem ALKIS-
Schema). Die Länderkennung wird nicht hartcodiert, sondern über
:func:`sn_basis.functions.laenderkennung.get_laenderkennung` ermittelt, damit
Schwesterplugins für andere Bundesländer dieselbe Logik mit einer anderen
Länderkennung wiederverwenden können.
Gleicht eine CSV-Exportliste des BI-Servers (Kopfzeile
``VEF_VKZF,FLA_Beteiligung,FLURSTUECK``) mit dem Verfahrensgebiet-Bestand
ab. Aus ``FLURSTUECK`` (Format ``<Gemarkung>/<Zähler>/<Nenner>/<Folge>``) wird
je Zeile ein zu ALKIS kompatibles Flurstückskennzeichen ``Flurstueck_csv``
gebildet.
Kaskade je nach Projektzustand:
- **try1**: Ein Layer ``Verfahrensgebiet`` mit befülltem Attribut
``flurstueckskennzeichen`` (Single-Feature-Modell, siehe
:mod:`verfahrensgebiet_edit_service`) ist vorhanden → schneller
Mengenvergleich ohne WFS-Zugriff.
- **try2**: Ein Verfahrensgebiet-Layer ist vorhanden, aber ohne verwertbare
Kennzeichen → statt einer automatischen (nutzerlosen) ALKIS-Ladepipeline
wird der Nutzer per Meldung aufgefordert, das Verfahrensgebiet zunächst
regulär über "Aus ALKIS laden" neu zu laden und die Beteiligungsprüfung
danach erneut zu starten (landet dann automatisch bei try1). Ein
automatischer Durchlauf hat sich in der Praxis nicht als zuverlässig genug
erwiesen (mehrdeutige BROR-Treffer, Flächenabweichungen etc. lassen sich
nur im interaktiven Ablauf sinnvoll auflösen).
- **try3**: Kein Verfahrensgebiet-Layer im Projekt → Verfahrensnummer aus
``VEF_VKZF`` wird als Projektvariable übernommen, danach wie try2 (Meldung
statt automatischem Laden).
"""
from __future__ import annotations
import csv
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Tuple
from sn_basis.functions import QgsVectorLayer, QgsProject, QgsFeature, QgsField
from sn_basis.functions.qt_wrapper import QFileDialog, QVariant
from sn_basis.functions.variable_wrapper import set_variable
from sn_basis.functions.laenderkennung import get_laenderkennung
from sn_basis.functions.dialog_wrapper import create_progress_dialog
from sn_verfahrensgebiet.functions.flurstueck_alkis import FLST_LAYER_NAME
from sn_verfahrensgebiet.functions.verfahrensgebiet_edit_service import (
VERF_LAYER_NAME,
get_kennzeichen_menge,
)
CSV_SPALTE_FLURSTUECK = "FLURSTUECK"
CSV_SPALTE_VEF_VKZF = "VEF_VKZF"
CSV_SPALTE_BETEILIGUNG = "FLA_Beteiligung"
FELD_FLURSTUECK_CSV = "Flurstueck_csv"
FELD_FLURSTUECKSKENNZEICHEN_WFS = "flurstueckskennzeichen"
ERGEBNIS_LAYER_VERGLEICH = "Beteiligung – Vergleich"
WERT_BEIDE = "beide"
WERT_NUR_VERFAHRENSGEBIET = "nur_verfahrensgebiet"
WERT_NUR_AGLB = "nur_aglb"
class BeteiligungPruefungFehler(Exception):
"""Erwarteter, dem Nutzer per Messagebox mitzuteilender Fehlerfall."""
@dataclass
class BeteiligungErgebnis:
"""Ergebnis eines Prüflaufs, das die aufrufende UI ins Projekt einfügt."""
modus: str
layers: List[Tuple[object, Optional[str]]] = field(default_factory=list)
statistik: Dict[str, int] = field(default_factory=dict)
statistik_text: str = ""
# ---------------------------------------------------------------------------
# CSV laden
# ---------------------------------------------------------------------------
def lade_beteiligung_csv(parent_widget) -> Optional[Tuple[List[dict], List[str]]]:
"""Öffnet Dateidialog (CSV-Filter) und liest die BI-Server-Exportliste.
Returns
-------
Optional[Tuple[List[dict], List[str]]]
``(zeilen, spaltennamen)`` oder ``None`` bei Nutzerabbruch.
Raises
------
BeteiligungPruefungFehler
Bei ungültigem CSV-Format oder fehlendem Attribut ``FLURSTUECK``.
"""
pfad, _ = QFileDialog.getOpenFileName(
parent_widget,
"CSV-Export aus BI-Server auswählen",
"",
"CSV-Dateien (*.csv)",
)
if not pfad:
return None # Nutzerabbruch
try:
with open(pfad, "r", encoding="utf-8-sig", newline="") as f:
reader = csv.DictReader(f, delimiter=",")
zeilen = list(reader)
spaltennamen = reader.fieldnames or []
except (OSError, csv.Error, UnicodeDecodeError) as exc:
raise BeteiligungPruefungFehler(
f"CSV-Format ungültig – Datei konnte nicht gelesen werden: {exc}"
) from exc
if not spaltennamen or not zeilen:
raise BeteiligungPruefungFehler(
"CSV-Format ungültig – die Datei ist leer oder enthält keine Kopfzeile."
)
if CSV_SPALTE_FLURSTUECK not in spaltennamen:
raise BeteiligungPruefungFehler(
f"Die CSV enthält nicht das benötigte Attribut '{CSV_SPALTE_FLURSTUECK}'."
)
return zeilen, spaltennamen
# ---------------------------------------------------------------------------
# Flurstueck_csv-Bildung
# ---------------------------------------------------------------------------
#: Alte CSV-Exporte führen Buchstaben-Nenner (ALKIS: a, b, c, ...) teils als
#: numerischen Platzhalter 9001, 9002, ... statt als Buchstabe. 9001 → "a",
#: 9002 → "b" usw. (Basiswert, auf den 1..26 für a..z addiert wird).
_NENNER_BUCHSTABEN_BASIS = 9000
def _baue_nenner_kennzeichen(nenner: str) -> str:
"""Bildet den 4-stelligen Nenner-Teil des Flurstückskennzeichens.
Normalfall: numerischer Nenner, 4-stellig mit führenden Nullen
aufgefüllt; Nenner 0/leer → Platzhalter ``"____"``.
Format-Inkonsistenz alter CSV-Exporte: ein Buchstaben-Nenner (z.B. "a")
wird dort als ``9001`` (a), ``9002`` (b), usw. geführt. Wird ein solcher
Platzhalterwert erkannt, wird stattdessen der Buchstabe selbst
(4-stellig mit führenden Nullen aufgefüllt, z.B. ``"000a"``) verwendet,
damit das Kennzeichen mit dem ALKIS-Bestand übereinstimmt.
"""
nenner = (nenner or "").strip()
nenner_ohne_null = nenner.lstrip("0")
if not nenner_ohne_null:
return "____"
if nenner_ohne_null.isdigit():
zahl = int(nenner_ohne_null)
if _NENNER_BUCHSTABEN_BASIS < zahl <= _NENNER_BUCHSTABEN_BASIS + 26:
buchstabe = chr(ord("a") + (zahl - _NENNER_BUCHSTABEN_BASIS - 1))
return buchstabe.zfill(4)
return nenner.zfill(4)
def baue_flurstueck_csv(flurstueck_wert: str, laenderkennung: str) -> Optional[str]:
"""Bildet aus dem CSV-Attribut ``FLURSTUECK`` ein ALKIS-Flurstückskennzeichen.
Das Attribut hat das Format
``<Gemarkungsnummer>/<Zähler>/<Nenner>/<Folge>``. Die Flurnummer ist in der
CSV nicht enthalten und wird als fixer Platzhalter ``___`` geführt (so wie
ALKIS unbesetzte Kennzeichen-Bestandteile selbst mit Unterstrichen füllt).
#Für Länderanpassung dieses Verhalten ändern, so dass die Flurnummer extrahiert und berücksichtigt wird, wenn angegeben
Returns
-------
Optional[str]
Das zusammengesetzte Kennzeichen, oder ``None`` bei ungültigem Format
(nicht genau 4 durch ``/`` getrennte Teile).
"""
if not flurstueck_wert:
return None
teile = [t.strip() for t in flurstueck_wert.strip().split("/")]
if len(teile) != 4:
return None
gemarkung, zaehler, nenner, folge = teile
gemarkung_kz = gemarkung.zfill(4)
flur_kz = "___"
zaehler_kz = zaehler.zfill(5)
nenner_kz = _baue_nenner_kennzeichen(nenner)
folge_kz = folge.zfill(2)
return f"{laenderkennung}{gemarkung_kz}{flur_kz}{zaehler_kz}{nenner_kz}{folge_kz}"
def baue_flurstueck_csv_liste(zeilen: List[dict], laenderkennung: str) -> List[str]:
"""Bildet für alle CSV-Zeilen ``Flurstueck_csv`` und gibt die Liste zurück.
Schreibt das Ergebnis zusätzlich als Attribut ``Flurstueck_csv`` in jede
Zeile zurück und gibt die ersten 20 Einträge als DEBUG-Ausgabe aus.
"""
ergebnisse: List[str] = []
for zeile in zeilen:
wert = zeile.get(CSV_SPALTE_FLURSTUECK, "")
fkz = baue_flurstueck_csv(wert, laenderkennung)
zeile[FELD_FLURSTUECK_CSV] = fkz or ""
if fkz:
ergebnisse.append(fkz)
print(f">>> Flurstueck_csv – {len(ergebnisse)} gebildet, erste 20:")
for wert in ergebnisse[:20]:
print(f">>> {wert}")
return ergebnisse
# ---------------------------------------------------------------------------
# Hilfsfunktionen: Layer-Lookup
# ---------------------------------------------------------------------------
def _hole_verfahrensgebiet_layer(tab_widget) -> Optional[object]:
"""Liefert den Verfahrensgebiet-Layer, falls vorhanden.
Der per Name (``VERF_LAYER_NAME``) identifizierte Layer hat Vorrang vor
der Dropdown-Auswahl im Tab: try1/try2/try3 orientieren sich am
tatsächlichen "Verfahrensgebiet"-Bestand im Projekt, nicht an einer
ggf. anderweitig belegten Nutzerauswahl (z.B. für Grenzpunkte/
Umringstatistik). Die Dropdown-Auswahl dient nur als Fallback, falls
kein so benannter Layer existiert.
"""
treffer = QgsProject.instance().mapLayersByName(VERF_LAYER_NAME)
if treffer:
return treffer[0]
combo = getattr(tab_widget, "verf_layer_combo", None)
if combo is not None:
layer = combo.currentLayer()
if layer is not None and layer.isValid():
return layer
return None
def _hat_verwendbare_kennzeichen(verf_layer) -> bool:
try:
return bool(get_kennzeichen_menge(verf_layer))
except Exception:
return False
def _hole_flurstuecke_layer() -> Optional[object]:
treffer = QgsProject.instance().mapLayersByName(FLST_LAYER_NAME)
return treffer[0] if treffer else None
def _flurstueckskennzeichen_geometrien(flst_layer) -> Dict[str, object]:
"""Baut ``flurstueckskennzeichen`` → Geometrie-Map aus dem Flurstücke-Layer."""
mapping: Dict[str, object] = {}
if flst_layer is None:
return mapping
if flst_layer.fields().indexFromName(FELD_FLURSTUECKSKENNZEICHEN_WFS) < 0:
return mapping
for feat in flst_layer.getFeatures():
try:
kz = str(feat[FELD_FLURSTUECKSKENNZEICHEN_WFS] or "").strip()
except Exception:
kz = ""
if kz:
mapping[kz] = feat.geometry()
return mapping
# ---------------------------------------------------------------------------
# try1: schneller Listenabgleich
# ---------------------------------------------------------------------------
def _try1_listenabgleich(
tab_widget, verf_layer, flurstueck_csv_liste: List[str]
) -> Tuple[object, Dict[str, int]]:
progress = create_progress_dialog(
3, "Beteiligung prüfen", "Verfahrensgebiet-Kennzeichen einlesen …"
)
progress.set_value(0)
progress.raise_to_front()
verf_menge = get_kennzeichen_menge(verf_layer)
csv_menge = set(flurstueck_csv_liste)
progress.set_value(1)
progress.set_label("CSV-Liste abgleichen …")
beide = sorted(verf_menge & csv_menge)
nur_verfahrensgebiet = sorted(verf_menge - csv_menge)
nur_aglb = sorted(csv_menge - verf_menge)
progress.set_value(2)
progress.set_label("Ergebnislayer erstellen …")
flst_layer = _hole_flurstuecke_layer()
geom_map = _flurstueckskennzeichen_geometrien(flst_layer)
if flst_layer is not None and flst_layer.isValid():
crs = flst_layer.crs().authid()
elif verf_layer.isValid():
crs = verf_layer.crs().authid()
else:
crs = "EPSG:25833"
layer = QgsVectorLayer(f"Polygon?crs={crs}", ERGEBNIS_LAYER_VERGLEICH, "memory")
pr = layer.dataProvider()
pr.addAttributes([
QgsField(FELD_FLURSTUECK_CSV, QVariant.String),
QgsField("status", QVariant.String),
])
layer.updateFields()
fields = layer.fields()
neue_features = []
for status, menge in (
(WERT_BEIDE, beide),
(WERT_NUR_VERFAHRENSGEBIET, nur_verfahrensgebiet),
(WERT_NUR_AGLB, nur_aglb),
):
for kz in menge:
feat = QgsFeature(fields)
geom = geom_map.get(kz)
if geom is not None:
feat.setGeometry(geom)
feat.setAttributes([kz, status])
neue_features.append(feat)
pr.addFeatures(neue_features)
layer.updateExtents()
progress.set_value(3)
progress.close()
statistik = {
WERT_BEIDE: len(beide),
WERT_NUR_VERFAHRENSGEBIET: len(nur_verfahrensgebiet),
WERT_NUR_AGLB: len(nur_aglb),
}
return layer, statistik
# ---------------------------------------------------------------------------
# try2/try3: Aufforderung statt automatischer ALKIS-Pipeline
# ---------------------------------------------------------------------------
#
# Ein automatischer (nutzerloser) Durchlauf der kompletten Ladepipeline
# (mehrdeutige BROR-Treffer, Flächenabweichungen, Detailprüfung) hat sich in
# der Praxis nicht als zuverlässig genug erwiesen – deshalb hier bewusst
# KEIN silent-Aufruf von verfahrensgebiet_alkis_komplett(), sondern eine
# Aufforderung an den Nutzer, den regulären interaktiven Ablauf
# ("Aus ALKIS laden") selbst zu starten. Ein erneuter Klick auf
# "Beteiligung prüfen" landet danach automatisch bei try1, sobald der Layer
# verwertbare Kennzeichen hat.
def _fordere_manuelles_laden_an(vef_vkzf: Optional[str] = None) -> None:
"""Bricht try2/try3 mit einer Aufforderung an den Nutzer ab (statt eines
automatischen Ladevorgangs)."""
hinweis = (
"Bitte laden Sie das Verfahrensgebiet jetzt über 'Aus ALKIS laden' "
"und starten Sie die Beteiligungsprüfung danach erneut."
)
if vef_vkzf:
raise BeteiligungPruefungFehler(
f"Es ist noch kein Verfahrensgebiet geladen. Die Verfahrensnummer "
f"{vef_vkzf} wurde aus der CSV übernommen. {hinweis}"
)
raise BeteiligungPruefungFehler(
f"Der vorhandene Verfahrensgebiet-Layer enthält keine verwertbaren "
f"Flurstückskennzeichen. {hinweis}"
)
# ---------------------------------------------------------------------------
# try3: Bootstrap (Verfahrensnummer aus CSV) + Aufforderung wie try2
# ---------------------------------------------------------------------------
def _lese_vef_vkzf(zeilen: List[dict], spaltennamen: List[str]) -> str:
if CSV_SPALTE_VEF_VKZF not in spaltennamen:
raise BeteiligungPruefungFehler(
f"Die CSV enthält nicht das benötigte Attribut '{CSV_SPALTE_VEF_VKZF}'."
)
for zeile in zeilen:
wert = (zeile.get(CSV_SPALTE_VEF_VKZF) or "").strip()
if wert:
return wert
raise BeteiligungPruefungFehler(
f"Die CSV enthält keinen befüllten Wert für '{CSV_SPALTE_VEF_VKZF}'."
)
def _try3_bootstrap(tab_widget, zeilen: List[dict], spaltennamen: List[str]) -> None:
vef_vkzf = _lese_vef_vkzf(zeilen, spaltennamen)
set_variable("verfahrensnummer", vef_vkzf, scope="project")
_fordere_manuelles_laden_an(vef_vkzf)
# ---------------------------------------------------------------------------
# Öffentliche API
# ---------------------------------------------------------------------------
def beteiligung_pruefen(tab_widget) -> Optional[BeteiligungErgebnis]:
"""Vollständiger Ablauf: CSV laden → Flurstueck_csv bilden → try1/try2/try3.
Returns
-------
Optional[BeteiligungErgebnis]
``None`` bei Nutzerabbruch des Dateidialogs. Sonst das Ergebnis mit
den ins Projekt einzufügenden Layern und der Statistik.
Raises
------
BeteiligungPruefungFehler
Bei einem der dokumentierten Fehlerfälle (CSV-Format, fehlende
Spalten) oder wenn kein Verfahrensgebiet mit verwertbaren
Kennzeichen vorliegt (try2/try3 – der Nutzer wird dann aufgefordert,
das Verfahrensgebiet zunächst über "Aus ALKIS laden" zu laden und
die Beteiligungsprüfung danach erneut zu starten). Der Aufrufer
zeigt daraufhin eine Messagebox an.
"""
geladen = lade_beteiligung_csv(tab_widget)
if geladen is None:
return None
zeilen, spaltennamen = geladen
laenderkennung = get_laenderkennung("sn")
flurstueck_csv_liste = baue_flurstueck_csv_liste(zeilen, laenderkennung)
if not flurstueck_csv_liste:
raise BeteiligungPruefungFehler(
"Aus der CSV konnten keine gültigen Flurstück-Kennzeichen gebildet werden "
"(Attribut 'FLURSTUECK' muss 4 durch '/' getrennte Teile enthalten)."
)
verf_layer = _hole_verfahrensgebiet_layer(tab_widget)
if verf_layer is not None and _hat_verwendbare_kennzeichen(verf_layer):
layer, statistik = _try1_listenabgleich(tab_widget, verf_layer, flurstueck_csv_liste)
text = (
f"Übereinstimmend: {statistik[WERT_BEIDE]}\n"
f"Nur im Verfahrensgebiet: {statistik[WERT_NUR_VERFAHRENSGEBIET]}\n"
f"Nur in der CSV-Liste (AGLB): {statistik[WERT_NUR_AGLB]}"
)
return BeteiligungErgebnis("try1", [(layer, "beteiligung_vergleich.qml")], statistik, text)
if verf_layer is not None:
_fordere_manuelles_laden_an()
return None # unreachable – _fordere_manuelles_laden_an() wirft immer
_try3_bootstrap(tab_widget, zeilen, spaltennamen)
return None # unreachable – _try3_bootstrap() wirft immer
+61 -14
View File
@@ -3,8 +3,9 @@
Lädt Flurstücke aus dem ALKIS-WFS-Dienst (``adv:AX_Flurstueck``) anhand von Lädt Flurstücke aus dem ALKIS-WFS-Dienst (``adv:AX_Flurstueck``) anhand von
Gemarkungsschlüsseln, die der Nutzer interaktiv eingibt. Gemarkungsschlüsseln, die der Nutzer interaktiv eingibt.
Der WFS-Layer wird direkt (nicht als Memory-Layer) ins Projekt geladen, Für jede Gemarkungsnummer wird eine eigene WFS-Anfrage gestellt (umgeht das
da die einzelnen Features für die spätere Auswahl benötigt werden. WFS-Feature-Limit einer Gesamtabfrage); alle Ergebnisse werden anschließend in
einem gemeinsamen Memory-Layer zusammengeführt.
**Erweiterung gegenüber dem Original:** **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 - ``vorgabe_namen``: Zugehörige Bezeichnungen, die im Dialog als Hinweis
angezeigt werden (kein eigenständiges Feld, nur Anzeigetext). angezeigt werden (kein eigenständiges Feld, nur Anzeigetext).
- ``bbox``: Optionaler BBox-Parameter, der – sofern übergeben – als - ``bbox``: Nicht mehr verwendet. Die Flurstück-Geometrie kommt ausschließlich
zusätzlicher serverseitiger Filter in die WFS-URI eingebettet wird. So aus dem ALKIS-WFS je Gemarkung; eine BBox filtert höchstens vorgelagert die
werden ausschließlich Flurstücke geladen, die den Verfahrensgebiets-Rahmen Gemarkungsauswahl (siehe ``gemarkung_alkis.py``), nie die Flurstücke selbst.
überlappen. Der Parameter bleibt nur aus Gründen der Signatur-Rückwärtskompatibilität
erhalten.
""" """
from __future__ import annotations 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" ALKIS_SF_URL = "https://geodienste.sachsen.de/aaa/public_alkis/sf/wfs"
FLST_TYPENAME = "adv:AX_Flurstueck" FLST_TYPENAME = "adv:AX_Flurstueck"
FLST_LAYER_NAME = "Flurstücke" FLST_LAYER_NAME = "Flurstücke"
FELD_FLURSTUECKSKENNZEICHEN = "flurstueckskennzeichen"
def _check_existing_flurstueck_layer(tab_widget): def _check_existing_flurstueck_layer(tab_widget):
@@ -74,6 +77,24 @@ def _check_existing_flurstueck_layer(tab_widget):
return None, LoadStatus.NONE 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]): def lade_flurstuecke_per_gemarkung(schluessel_liste: List[str]):
"""Lädt Flurstücke für mehrere Gemarkungen, eine WFS-Anfrage pro Schlüssel. """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, vorgabe_namen: Optional[List[str]] = None,
bbox=None, bbox=None,
ohne_dialog: bool = False, ohne_dialog: bool = False,
ohne_schluessel_abfrage: bool = False,
) -> Tuple[Optional[QgsVectorLayer], LoadStatus]: ) -> Tuple[Optional[QgsVectorLayer], LoadStatus]:
"""Lädt Flurstücke aus dem ALKIS-WFS anhand von Gemarkungsschlüsseln. """Lädt Flurstücke aus dem ALKIS-WFS anhand von Gemarkungsschlüsseln.
@@ -165,17 +187,27 @@ def flurstueck_alkis(
Eltern-Widget für Dialoge. Eltern-Widget für Dialoge.
vorgabe_schluessel : list of str, optional vorgabe_schluessel : list of str, optional
Vorausgefüllte Gemarkungsnummern (4-stellig). Im Dialog als Standard 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 vorgabe_namen : list of str, optional
Menschenlesbare Gemarkungsbezeichnungen für den Dialog-Hinweistext. Menschenlesbare Gemarkungsbezeichnungen für den Dialog-Hinweistext.
bbox : bbox :
Nicht mehr verwendet (WFS unterstützt keinen BBox-Filter). Bleibt Nicht mehr verwendet (WFS unterstützt keinen BBox-Filter). Bleibt
für Rückwärtskompatibilität in der Signatur erhalten. für Rückwärtskompatibilität in der Signatur erhalten.
ohne_dialog : bool, optional ohne_dialog : bool, optional
``True``: Überspringt die Gemarkungsschlüssel-Nutzerabfrage und verwendet ``True``: Überspringt sowohl die Gemarkungsschlüssel-Nutzerabfrage (wie
``vorgabe_schluessel`` direkt. Vorhandene Layer werden ohne Rückfrage ``ohne_schluessel_abfrage``) als auch die Rückfrage bei einem bereits
ersetzt. Gedacht für die automatische Pipeline vorhandenen Layer – dieser wird dann immer ersetzt. Gedacht für die
(``verfahrensgebiet_alkis_komplett``). Standard: ``False``. 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 Returns
------- -------
@@ -201,8 +233,8 @@ def flurstueck_alkis(
append_target = existing append_target = existing
# --- Schritt 2: Gemarkungsschlüssel bestimmen ------------------------- # --- Schritt 2: Gemarkungsschlüssel bestimmen -------------------------
if ohne_dialog: if ohne_dialog or ohne_schluessel_abfrage:
# Pipeline-Modus: vorgabe_schluessel direkt verwenden, kein Dialog # vorgabe_schluessel direkt verwenden, kein ask_text-Dialog
if not vorgabe_schluessel: if not vorgabe_schluessel:
return None, LoadStatus.NONE return None, LoadStatus.NONE
schluessel_liste = [s.strip() for s in vorgabe_schluessel if s.strip()] schluessel_liste = [s.strip() for s in vorgabe_schluessel if s.strip()]
@@ -250,7 +282,22 @@ def flurstueck_alkis(
# --- Anhängen an vorhandenen Layer ------------------------------------ # --- Anhängen an vorhandenen Layer ------------------------------------
if status == LoadStatus.APPEND and append_target is not None: 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.updateExtents()
append_target.triggerRepaint() append_target.triggerRepaint()
return append_target, LoadStatus.APPEND 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 import QMessageBox
from sn_basis.functions.message_wrapper import info, warning from sn_basis.functions.message_wrapper import info, warning
from sn_basis.functions.dialog_wrapper import create_progress_dialog 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" ALKIS_SF_URL = "https://geodienste.sachsen.de/aaa/public_alkis/sf/wfs"
PUNKTORT_TA_TYPENAME = "adv:AX_PunktortTA" PUNKTORT_TA_TYPENAME = "adv:AX_PunktortTA"
@@ -1652,6 +1653,7 @@ def grenzpunkte_gemarkungsweise(
return None return None
project.addMapLayer(joined_layer) project.addMapLayer(joined_layer)
apply_style(joined_layer, "alkis_grenzpunkte.qml")
prog.set_value(prog_gesamt) prog.set_value(prog_gesamt)
prog.close() prog.close()
+195 -20
View File
@@ -9,7 +9,8 @@ from sn_basis.functions import (
from sn_basis.functions import QMessageBox from sn_basis.functions import QMessageBox
from sn_basis.functions.qt_wrapper import QVariant from sn_basis.functions.qt_wrapper import QVariant
from sn_basis.functions.variable_wrapper import get_variable from sn_basis.functions.variable_wrapper import get_variable
from sn_basis.functions.dialog_wrapper import ask_bror_kombination from sn_verfahrensgebiet.functions.verfahrensgebiet_dialoge import ask_bror_kombinationen, ask_landkreis_objekte
from sn_basis.functions.landkreise import ALLE_LANDKREISE
alkis_sf_url = "https://geodienste.sachsen.de/aaa/public_alkis/sf/wfs" alkis_sf_url = "https://geodienste.sachsen.de/aaa/public_alkis/sf/wfs"
typename = "adv:AX_BauRaumOderBodenordnungsrecht" typename = "adv:AX_BauRaumOderBodenordnungsrecht"
@@ -72,32 +73,36 @@ def _check_existing_layer(tab_widget):
return existing_layers[0], LoadStatus.KEEP return existing_layers[0], LoadStatus.KEEP
def verfahrensgebiet_alkis(tab_widget, skip_existing_check: bool = False): def _suche_verfahrensgebiet_features(tab_widget, skip_existing_check: bool = False):
"""Lädt Verfahrensgebiet aus ALKIS-WFS basierend auf Verfahrensnummer. """Phase 1 ("Suchen"): vorhandenen Layer prüfen, WFS abfragen, nach
(adv_name, bezeichnung) gruppieren.
Args: Args:
tab_widget: Parent-Widget für Dialoge und Meldungen tab_widget: Parent-Widget für Dialoge und Meldungen
skip_existing_check: Überspringt die Nachfrage bei bereits vorhandenem
Layer (immer neu laden).
Returns: Returns:
Tuple[Optional[QgsVectorLayer], LoadStatus, List[QgsFeature]]: Tuple[Optional[QgsVectorLayer], LoadStatus, dict, Optional[str]]:
Geladener Memory-Layer, Lade-Status und Liste der Roh-WFS-Features ``(fertig_layer, status, kombinationen, crs)``.
(für geometriebasierte Weiterverarbeitung, z.B. Gemarkungsschlüssel- Bei ``KEEP`` oder Fehler/Abbruch ist ``fertig_layer`` bereits das
Ermittlung via Spatial-Join). Bei Abbruch oder Fehler: ``(None, Endergebnis (ggf. ``None``) und ``kombinationen`` leer – Phase 2
LoadStatus.NONE, [])``. Bei ``LoadStatus.KEEP`` werden keine entfällt dann. Bei Erfolg mit mindestens einer Kombination ist
WFS-Features neu geladen; die Feature-Liste ist in diesem Fall leer. ``fertig_layer`` ``None`` und ``kombinationen``/``crs`` sind für
Phase 2 (:func:`_baue_layer_aus_kombinationen`) befüllt.
""" """
verfahrensnummer = get_variable("verfahrensnummer") verfahrensnummer = get_variable("verfahrensnummer")
if not verfahrensnummer: if not verfahrensnummer:
QMessageBox.critical(tab_widget, "Fehler", QMessageBox.critical(tab_widget, "Fehler",
"In den Projekteigenschaften ist noch keine Verfahrensnummer eingetragen!") "In den Projekteigenschaften ist noch keine Verfahrensnummer eingetragen!")
return None, LoadStatus.NONE, [] return None, LoadStatus.NONE, {}, None
# Vorhandenen Layer prüfen (überspringbar für die Shape-Pipeline) # Vorhandenen Layer prüfen (überspringbar für die Shape-Pipeline)
status = LoadStatus.FIRST status = LoadStatus.FIRST
if not skip_existing_check: if not skip_existing_check:
existing, status = _check_existing_layer(tab_widget) existing, status = _check_existing_layer(tab_widget)
if status == LoadStatus.KEEP: if status == LoadStatus.KEEP:
return existing, status, [] return existing, status, {}, None
# WFS mit Filter laden – nur erste 5 Stellen der VKZ für den Vergleich verwenden, # WFS mit Filter laden – nur erste 5 Stellen der VKZ für den Vergleich verwenden,
# da die Daten im Dienst ggf. abweichende Suffixe führen. # da die Daten im Dienst ggf. abweichende Suffixe führen.
@@ -111,13 +116,13 @@ def verfahrensgebiet_alkis(tab_widget, skip_existing_check: bool = False):
if not temp_layer.isValid(): if not temp_layer.isValid():
QMessageBox.critical(tab_widget, "Fehler", "Layer konnte nicht geladen werden.") QMessageBox.critical(tab_widget, "Fehler", "Layer konnte nicht geladen werden.")
return None, LoadStatus.NONE, [] return None, LoadStatus.NONE, {}, None
features = list(temp_layer.getFeatures()) features = list(temp_layer.getFeatures())
if not features: if not features:
QMessageBox.critical(tab_widget, "Fehler", QMessageBox.critical(tab_widget, "Fehler",
f"Verfahrenskennzeichen {verfahrensnummer} nicht im WFS gefunden.") f"Verfahrenskennzeichen {verfahrensnummer} nicht im WFS gefunden.")
return None, LoadStatus.NONE, [] return None, LoadStatus.NONE, {}, None
# Objekte nach (adv_name, bezeichnung) gruppieren # Objekte nach (adv_name, bezeichnung) gruppieren
kombinationen: dict[tuple[str, str], list] = {} kombinationen: dict[tuple[str, str], list] = {}
@@ -133,16 +138,32 @@ def verfahrensgebiet_alkis(tab_widget, skip_existing_check: bool = False):
kombinationen[key] = [] kombinationen[key] = []
kombinationen[key].append(feat) kombinationen[key].append(feat)
return None, status, kombinationen, temp_layer.crs().authid()
def _baue_layer_aus_kombinationen(tab_widget, kombinationen: dict, crs: str, status: "LoadStatus"):
"""Phase 2 ("Auswählen + Laden"): bei mehreren Kombinationen Auswahldialog
zeigen, Geometrien der gewählten Kombination(en) verschmelzen, Memory-Layer bauen.
Args:
tab_widget: Parent-Widget für Dialoge und Meldungen
kombinationen: Ergebnis aus :func:`_suche_verfahrensgebiet_features`
crs: CRS-Authid aus :func:`_suche_verfahrensgebiet_features`
status: Lade-Status aus Phase 1 (FIRST/RELOAD), wird durchgereicht
Returns:
Tuple[Optional[QgsVectorLayer], LoadStatus, List[QgsFeature]]
"""
if len(kombinationen) == 1: if len(kombinationen) == 1:
selected_key = next(iter(kombinationen)) selected_feats = next(iter(kombinationen.values()))
selected_feats = kombinationen[selected_key]
else: else:
# Mehrere Kombinationen → Nutzerauswahl # Mehrere Kombinationen → Nutzerauswahl, ein oder mehrere Objekte
# können gemeinsam ausgewählt und geladen (verschmolzen) werden.
key_list = sorted(kombinationen.keys()) key_list = sorted(kombinationen.keys())
selected_key = ask_bror_kombination(tab_widget, key_list) selected_keys = ask_bror_kombinationen(tab_widget, key_list)
if selected_key is None: if not selected_keys:
return None, LoadStatus.NONE, [] return None, LoadStatus.NONE, []
selected_feats = kombinationen[selected_key] selected_feats = [feat for key in selected_keys for feat in kombinationen[key]]
union_geom = QgsGeometry.unaryUnion([f.geometry() for f in selected_feats]) union_geom = QgsGeometry.unaryUnion([f.geometry() for f in selected_feats])
if union_geom is None or union_geom.isEmpty(): if union_geom is None or union_geom.isEmpty():
@@ -150,7 +171,6 @@ def verfahrensgebiet_alkis(tab_widget, skip_existing_check: bool = False):
return None, LoadStatus.NONE, [] return None, LoadStatus.NONE, []
# Memory-Layer erzeugen # Memory-Layer erzeugen
crs = temp_layer.crs().authid()
memory_layer = QgsVectorLayer(f"Polygon?crs={crs}", "Verfahrensgebiet", "memory") memory_layer = QgsVectorLayer(f"Polygon?crs={crs}", "Verfahrensgebiet", "memory")
pr = memory_layer.dataProvider() pr = memory_layer.dataProvider()
pr.addAttributes([QgsField(attr, QVariant.String) for attr in BROR_ATTRIBUTE_FELDER]) pr.addAttributes([QgsField(attr, QVariant.String) for attr in BROR_ATTRIBUTE_FELDER])
@@ -180,3 +200,158 @@ def verfahrensgebiet_alkis(tab_widget, skip_existing_check: bool = False):
# Status: FIRST oder RELOAD; selected_feats für Gemarkungsermittlung weitergeben # Status: FIRST oder RELOAD; selected_feats für Gemarkungsermittlung weitergeben
return memory_layer, status, selected_feats return memory_layer, status, selected_feats
def _suche_landkreis_kombinationen(tab_widget, landkreis_kennzahl: str, skip_existing_check: bool = False):
"""Fallback-Suche über einen ganzen Landkreis (statt einer Verfahrensnummer).
Wie :func:`_suche_verfahrensgebiet_features`, aber mit WFS-Filter über die
Landkreiskennzahl (``bezeichnung LIKE '{landkreis_kennzahl}%'``) statt
über den 5-stelligen VKZ-Präfix. Gruppierung der Rohfeatures identisch:
nach exaktem (adv_name, bezeichnung).
Returns:
Tuple[Optional[QgsVectorLayer], LoadStatus, dict, Optional[str]]:
Gleiche Bedeutung wie bei :func:`_suche_verfahrensgebiet_features`.
"""
status = LoadStatus.FIRST
if not skip_existing_check:
existing, status = _check_existing_layer(tab_widget)
if status == LoadStatus.KEEP:
return existing, status, {}, None
# "Alle Landkreise" (Sentinel) -> kein Kennzahl-Präfix, LIKE '%' matcht alles.
praefix = "" if landkreis_kennzahl == ALLE_LANDKREISE else landkreis_kennzahl
landkreis_anzeige = "allen Landkreisen" if landkreis_kennzahl == ALLE_LANDKREISE else f"Landkreis {landkreis_kennzahl}"
wfs_uri_find = (
f"url='{alkis_sf_url}' typename='{typename}' srsname='EPSG:25833' "
f"sql=SELECT * FROM AX_BauRaumOderBodenordnungsrecht "
f"WHERE bezeichnung LIKE '{praefix}%'"
)
temp_layer = QgsVectorLayer(wfs_uri_find, "TempVerfahrensgebiet", "WFS")
if not temp_layer.isValid():
QMessageBox.critical(tab_widget, "Fehler", "Layer konnte nicht geladen werden.")
return None, LoadStatus.NONE, {}, None
features = list(temp_layer.getFeatures())
if not features:
QMessageBox.critical(tab_widget, "Fehler",
f"Für {landkreis_anzeige} wurden keine Objekte im WFS gefunden.")
return None, LoadStatus.NONE, {}, None
kombinationen: dict[tuple[str, str], list] = {}
for feat in features:
try:
adv_name_val = str(feat.attribute("adv_name") or "")
bezeichnung_val = str(feat.attribute("bezeichnung") or "")
except Exception:
adv_name_val = ""
bezeichnung_val = ""
key = (adv_name_val, bezeichnung_val)
if key not in kombinationen:
kombinationen[key] = []
kombinationen[key].append(feat)
return None, status, kombinationen, temp_layer.crs().authid()
def _baue_layer_aus_landkreis_auswahl(tab_widget, kombinationen: dict, crs: str, status: "LoadStatus"):
"""Auswahl + Bau für den Landkreis-Fallback.
Zeigt :func:`~sn_verfahrensgebiet.functions.verfahrensgebiet_dialoge.ask_landkreis_objekte`
(Mehrfachauswahl inkl. "Alle auswählen"). Die ausgewählten Kombinationen
werden anhand der ersten 5 Zeichen von ``bezeichnung`` (= die eigentliche
VKZ) gruppiert; je Gruppe wird EIN Feature erzeugt (Geometrien der
Gruppe verschmolzen). Damit entsteht bei Auswahl mehrerer/aller Objekte
eines Landkreises ein Layer mit mehreren Features – einem je
tatsächlichem Verfahren, nicht ein Gesamt-Blob.
Args:
tab_widget: Parent-Widget für Dialoge und Meldungen
kombinationen: Ergebnis aus :func:`_suche_landkreis_kombinationen`
crs: CRS-Authid aus :func:`_suche_landkreis_kombinationen`
status: Lade-Status aus Phase 1 (FIRST/RELOAD), wird durchgereicht
Returns:
Tuple[Optional[QgsVectorLayer], LoadStatus]: ``(layer, status)``.
"""
key_list = sorted(kombinationen.keys())
selected_keys = ask_landkreis_objekte(tab_widget, key_list)
if not selected_keys:
return None, LoadStatus.NONE
# Nach VKZ (erste 5 Zeichen der bezeichnung) gruppieren
vkz_gruppen: dict[str, list] = {}
for key in selected_keys:
_, bezeichnung = key
vkz = bezeichnung[:5]
vkz_gruppen.setdefault(vkz, []).extend(kombinationen[key])
memory_layer = QgsVectorLayer(f"Polygon?crs={crs}", "Verfahrensgebiet", "memory")
pr = memory_layer.dataProvider()
pr.addAttributes([QgsField(attr, QVariant.String) for attr in BROR_ATTRIBUTE_FELDER])
memory_layer.updateFields()
neue_features = []
for vkz, feats in sorted(vkz_gruppen.items()):
union_geom = QgsGeometry.unaryUnion([f.geometry() for f in feats])
if union_geom is None or union_geom.isEmpty():
continue
first_feat = feats[0]
attrs = []
for attr in BROR_ATTRIBUTE_FELDER:
try:
val = first_feat.attribute(attr)
if val is None:
attrs.append("")
elif type(val).__name__ == "QDate":
attrs.append(val.toString("dd.MM.yyyy"))
else:
attrs.append(str(val))
except Exception:
attrs.append("")
feat_out = QgsFeature()
feat_out.setGeometry(union_geom)
feat_out.setAttributes(attrs)
neue_features.append(feat_out)
if not neue_features:
QMessageBox.critical(tab_widget, "Fehler", "Keine Geometrien zum Verschmelzen gefunden.")
return None, LoadStatus.NONE
pr.addFeatures(neue_features)
memory_layer.updateExtents()
return memory_layer, status
def verfahrensgebiet_alkis(tab_widget, skip_existing_check: bool = False):
"""Lädt Verfahrensgebiet aus ALKIS-WFS basierend auf Verfahrensnummer.
Args:
tab_widget: Parent-Widget für Dialoge und Meldungen
Returns:
Tuple[Optional[QgsVectorLayer], LoadStatus, List[QgsFeature]]:
Geladener Memory-Layer, Lade-Status und Liste der Roh-WFS-Features
(für geometriebasierte Weiterverarbeitung, z.B. Gemarkungsschlüssel-
Ermittlung via Spatial-Join). Bei Abbruch oder Fehler: ``(None,
LoadStatus.NONE, [])``. Bei ``LoadStatus.KEEP`` werden keine
WFS-Features neu geladen; die Feature-Liste ist in diesem Fall leer.
Ruft intern nacheinander :func:`_suche_verfahrensgebiet_features` (Phase 1)
und ggf. :func:`_baue_layer_aus_kombinationen` (Phase 2) auf – aufgeteilt,
damit ``verfahrensgebiet_alkis_komplett()`` dazwischen einen eigenen
Fortschrittsschritt anzeigen kann. Verhalten/Signatur dieser Funktion
selbst ist unverändert.
"""
layer, status, kombinationen, crs = _suche_verfahrensgebiet_features(
tab_widget, skip_existing_check
)
if layer is not None or not kombinationen:
return layer, status, []
return _baue_layer_aus_kombinationen(tab_widget, kombinationen, crs, status)
+140 -42
View File
@@ -51,10 +51,17 @@ from sn_basis.functions.dialog_wrapper import (
create_progress_dialog, create_progress_dialog,
show_info_dialog, show_info_dialog,
ask_detailpruefung_oder_vg_laden, ask_detailpruefung_oder_vg_laden,
ask_yes_no,
) )
from sn_basis.functions.landkreise import LANDKREISE, ALLE_LANDKREISE
from sn_basis.functions.ly_style_wrapper import apply_style
from sn_verfahrensgebiet.functions.verfahrensgebiet_dialoge import ask_landkreis_auswahl
from sn_verfahrensgebiet.functions.verfahrensgebiet_alkis import ( from sn_verfahrensgebiet.functions.verfahrensgebiet_alkis import (
verfahrensgebiet_alkis, _suche_verfahrensgebiet_features,
_baue_layer_aus_kombinationen,
_suche_landkreis_kombinationen,
_baue_layer_aus_landkreis_auswahl,
LoadStatus, LoadStatus,
) )
from sn_verfahrensgebiet.functions.gemarkung_alkis import ( from sn_verfahrensgebiet.functions.gemarkung_alkis import (
@@ -76,6 +83,8 @@ from sn_verfahrensgebiet.functions.knickpunkt_pruefung import (
FELD_VKZ = "verfahrensnummer" FELD_VKZ = "verfahrensnummer"
#: Anzeigename des BROR-Layers im Projekt #: Anzeigename des BROR-Layers im Projekt
BROR_LAYER_NAME = "BauRaumOderBodenordnungsrecht (ALKIS)" BROR_LAYER_NAME = "BauRaumOderBodenordnungsrecht (ALKIS)"
#: QML-Stildatei für den BROR-Layer (sn_verfahrensgebiet/styles/)
BROR_STYLE_QML = "BauRaumOderBodenordnungsrecht.qml"
#: Projektvariable für die Flächentoleranz (in m²); Default 0 #: Projektvariable für die Flächentoleranz (in m²); Default 0
FLAECHEN_TOLERANZ_VARIABLE = "flaechen_toleranz_m2" FLAECHEN_TOLERANZ_VARIABLE = "flaechen_toleranz_m2"
#: Minimale implizite Flächentoleranz – Abweichungen unter diesem Wert #: Minimale implizite Flächentoleranz – Abweichungen unter diesem Wert
@@ -258,15 +267,29 @@ def verfahrensgebiet_alkis_komplett(
"""Vollständiger VG-Lade-Workflow: ALKIS-VG → Gemarkungen → """Vollständiger VG-Lade-Workflow: ALKIS-VG → Gemarkungen →
Flurstücke → Verschneidung → Flächenvergleich → opt. Umringsprüfung. Flurstücke → Verschneidung → Flächenvergleich → opt. Umringsprüfung.
Schrittfolge: Zwei Fortschrittsdialoge nacheinander:
1. ``verfahrensgebiet_alkis(tab_widget)`` – BROR-Objekt als Memory-Layer. - **Dialog 1** ("Verfahrensgebiet(e) in ALKIS ermitteln/suchen"): läuft
2. ``gemarkung_schluessel_und_namen_von_vg_geometrie(geom)`` – Schlüssel. während ``_suche_verfahrensgebiet_features(tab_widget)`` (ALKIS-WFS
3. ``flurstueck_alkis(...)`` – Flurstücke laden. durchsuchen) und wird danach immer geschlossen – auch wenn anschließend
4. ``_select_by_location(...)`` – räumliche Selektion. der BROR-Auswahldialog erscheint (kein Fortschrittsdialog läuft
5. ``erstelle_verfahrensgebiet_aus_auswahl(...)`` – enriched Layer. währenddessen parallel zum Auswahldialog).
6. ``_vergleiche_flaechen(...)`` – Flächenvergleich als Gate. - **STOP** (Zwischenabfrage "Weiter?"): erscheint nach dem Laden/Übernehmen
7. Ggf. ``umringspruefung(...)`` nach Nutzerbestätigung. des BROR-Objekts, bevor Dialog 2 startet. Bei "Nein" bricht der Ablauf
ab, das BROR-Objekt bleibt aber im Projekt (ersetzt die frühere
"Nur Objekte laden"-Option des Landkreis-Auswahldialogs).
- **Dialog 2** (eigentlicher Workflow, 6 Schritte): startet erst, nachdem
das Verfahrensgebiet feststeht und die Stop-Abfrage mit "Ja" bestätigt
wurde.
Schrittfolge in Dialog 2:
1. ``gemarkung_schluessel_und_namen_von_vg_geometrie(geom)`` – Schlüssel.
2. ``flurstueck_alkis(...)`` – Flurstücke laden.
3. ``_select_by_location(...)`` – räumliche Selektion.
4. ``erstelle_verfahrensgebiet_aus_auswahl(...)`` – enriched Layer.
5. ``_vergleiche_flaechen(...)`` – Flächenvergleich als Gate.
6. Ggf. ``umringspruefung(...)`` nach Nutzerbestätigung.
Der BROR-Layer (``BauRaumOderBodenordnungsrecht (ALKIS)``) bleibt bei Erfolg Der BROR-Layer (``BauRaumOderBodenordnungsrecht (ALKIS)``) bleibt bei Erfolg
im Projekt. Bei Abbruch/Fehler wird er entfernt, falls er in diesem im Projekt. Bei Abbruch/Fehler wird er entfernt, falls er in diesem
@@ -290,24 +313,73 @@ def verfahrensgebiet_alkis_komplett(
alkis_vg_layer = None alkis_vg_layer = None
bror_was_added_by_us = False bror_was_added_by_us = False
abgeschlossen = False abgeschlossen = False
progress = None
# Fortschrittsdialog für den Basisworkflow (7 Schritte) verfahrensnummer = get_variable("verfahrensnummer")
progress = create_progress_dialog( landkreis_kennzahl = get_variable("landkreis_kennzahl")
7, "Verfahrensgebiet laden", "Initialisierung …"
)
try: if not verfahrensnummer and not landkreis_kennzahl:
# ------------------------------------------------------------------ # Keine Projekteigenschaften vorhanden – Landkreis per Dialog abfragen,
# Schritt 1: VG aus ALKIS laden (BROR-Objekt → Memory-Layer) # statt direkt abzubrechen.
# ------------------------------------------------------------------ landkreis_kennzahl = ask_landkreis_auswahl(tab_widget, LANDKREISE)
progress.set_label("Schritt 1/6: Verfahrensgebiet aus ALKIS laden …") if not landkreis_kennzahl:
progress.set_value(0) # Nutzer hat den Landkreis-Dialog abgebrochen – stiller Abbruch,
if progress.is_canceled(): # keine Fehlermeldung (bewusster Abbruch, kein Fehlerzustand).
return None, None, None, LoadStatus.NONE return None, None, None, LoadStatus.NONE
alkis_vg_layer, vg_status, _ = verfahrensgebiet_alkis(tab_widget) landkreis_modus = not verfahrensnummer
progress.raise_to_front()
# ------------------------------------------------------------------
# Dialog 1: Verfahrensgebiet(e) in ALKIS ermitteln/suchen
# ------------------------------------------------------------------
if landkreis_modus:
if landkreis_kennzahl == ALLE_LANDKREISE:
such_label = "Objekte in allen Landkreisen ermitteln/suchen …"
else:
such_label = f"Objekte in Landkreis {landkreis_kennzahl} ermitteln/suchen …"
else:
such_label = "Verfahrensgebiet(e) in ALKIS ermitteln/suchen …"
progress_suche = create_progress_dialog(1, "Verfahrensgebiet laden", such_label)
try:
progress_suche.set_value(0)
if progress_suche.is_canceled():
return None, None, None, LoadStatus.NONE
if landkreis_modus:
vorab_layer, vg_status, kombinationen, crs = _suche_landkreis_kombinationen(
tab_widget, landkreis_kennzahl
)
else:
vorab_layer, vg_status, kombinationen, crs = _suche_verfahrensgebiet_features(tab_widget)
finally:
# Dialog 1 schließt hier immer – egal ob eine Auswahl folgt oder nicht.
progress_suche.close()
try:
if vorab_layer is not None or not kombinationen:
# KEEP oder Fehler/Abbruch bereits in Phase 1 entschieden
if vg_status == LoadStatus.NONE or not vorab_layer:
return None, None, None, LoadStatus.NONE
alkis_vg_layer = vorab_layer
elif landkreis_modus:
# ------------------------------------------------------------------
# Objekt(e) des Landkreises auswählen – kein eigener Fortschritts-
# dialog währenddessen, nur der Auswahldialog selbst.
# ------------------------------------------------------------------
alkis_vg_layer, vg_status = _baue_layer_aus_landkreis_auswahl(
tab_widget, kombinationen, crs, vg_status
)
if vg_status == LoadStatus.NONE or not alkis_vg_layer:
return None, None, None, LoadStatus.NONE
else:
# ------------------------------------------------------------------
# Verfahrensgebiet auswählen (bei mehreren Treffern) – kein eigener
# Fortschrittsdialog währenddessen, nur der Auswahldialog selbst.
# ------------------------------------------------------------------
alkis_vg_layer, vg_status, _ = _baue_layer_aus_kombinationen(
tab_widget, kombinationen, crs, vg_status
)
if vg_status == LoadStatus.NONE or not alkis_vg_layer: if vg_status == LoadStatus.NONE or not alkis_vg_layer:
return None, None, None, LoadStatus.NONE return None, None, None, LoadStatus.NONE
@@ -317,13 +389,38 @@ def verfahrensgebiet_alkis_komplett(
if vg_status in (LoadStatus.FIRST, LoadStatus.RELOAD): if vg_status in (LoadStatus.FIRST, LoadStatus.RELOAD):
project.addMapLayer(alkis_vg_layer) project.addMapLayer(alkis_vg_layer)
bror_was_added_by_us = True bror_was_added_by_us = True
# Style direkt nach dem Laden anwenden – unabhängig vom weiteren
progress.set_value(1) # Ablauf, damit auch bei "Nein" auf die Stop-Abfrage der Style
# gesetzt ist.
apply_style(alkis_vg_layer, BROR_STYLE_QML)
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# Schritt 2: Gemarkungsschlüssel per Geometrie-Spatial-Join ermitteln # STOP: Zwischenabfrage nach dem Laden des BROR-Objekts, bevor mit
# Gemarkung/Flurstücke/Flächenvergleich fortgefahren wird (ersetzt
# die frühere "Nur Objekte laden"-Option).
# ------------------------------------------------------------------ # ------------------------------------------------------------------
progress.set_label("Schritt 2/6: Gemarkungsschlüssel ermitteln …") if not ask_yes_no(
"Verfahrensgebiet laden",
f"Layer '{alkis_vg_layer.name()}' wurde geladen. Weiter?",
default=True,
parent=tab_widget,
):
abgeschlossen = True
return alkis_vg_layer, None, None, vg_status
# ------------------------------------------------------------------
# Dialog 2: eigentlicher Fortschrittsdialog für die restlichen Schritte
# (startet erst jetzt, nach erfolgreicher Auswahl/Übernahme des VG)
# ------------------------------------------------------------------
progress = create_progress_dialog(
6, "Verfahrensgebiet laden", "Initialisierung …"
)
# ------------------------------------------------------------------
# Schritt 1: Gemarkungsschlüssel per Geometrie-Spatial-Join ermitteln
# ------------------------------------------------------------------
progress.set_label("Schritt 1/6: Gemarkungen ermitteln und laden …")
progress.set_value(0)
if progress.is_canceled(): if progress.is_canceled():
return None, None, None, LoadStatus.NONE return None, None, None, LoadStatus.NONE
@@ -344,12 +441,12 @@ def verfahrensgebiet_alkis_komplett(
duration=5, duration=5,
) )
progress.set_value(2) progress.set_value(1)
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# Schritt 3: Flurstücke laden (mit Nutzer-Dialog, Vorschlag befüllt) # Schritt 2: Flurstücke laden (mit Nutzer-Dialog, Vorschlag befüllt)
# ------------------------------------------------------------------ # ------------------------------------------------------------------
progress.set_label("Schritt 3/6: Flurstücke aus ALKIS laden …") progress.set_label("Schritt 2/6: Flurstücke aus ALKIS laden …")
if progress.is_canceled(): if progress.is_canceled():
return None, None, None, LoadStatus.NONE return None, None, None, LoadStatus.NONE
@@ -363,12 +460,12 @@ def verfahrensgebiet_alkis_komplett(
if flst_status == LoadStatus.NONE or not flst_layer: if flst_status == LoadStatus.NONE or not flst_layer:
return None, None, None, LoadStatus.NONE return None, None, None, LoadStatus.NONE
progress.set_value(3) progress.set_value(2)
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# Schritt 4: Räumliche Selektion # Schritt 3: Räumliche Selektion
# ------------------------------------------------------------------ # ------------------------------------------------------------------
progress.set_label("Schritt 4/6: Flurstücke räumlich selektieren …") progress.set_label("Schritt 3/6: Flurstücke räumlich selektieren …")
if progress.is_canceled(): if progress.is_canceled():
return None, None, None, LoadStatus.NONE return None, None, None, LoadStatus.NONE
@@ -383,12 +480,12 @@ def verfahrensgebiet_alkis_komplett(
) )
return None, flst_layer, None, LoadStatus.NONE return None, flst_layer, None, LoadStatus.NONE
progress.set_value(4) progress.set_value(3)
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# Schritt 5: Enriched Memory-Layer aus Auswahl erzeugen # Schritt 4: Enriched Memory-Layer aus Auswahl erzeugen
# ------------------------------------------------------------------ # ------------------------------------------------------------------
progress.set_label("Schritt 5/6: Verfahrensgebiet-Layer erzeugen …") progress.set_label("Schritt 4/6: Verfahrensgebiet-Layer erzeugen …")
if progress.is_canceled(): if progress.is_canceled():
return None, None, None, LoadStatus.NONE return None, None, None, LoadStatus.NONE
@@ -407,12 +504,12 @@ def verfahrensgebiet_alkis_komplett(
vkz = get_variable("verfahrensnummer") or "" vkz = get_variable("verfahrensnummer") or ""
_fuege_vkz_feld_hinzu(enriched_layer, vkz) _fuege_vkz_feld_hinzu(enriched_layer, vkz)
progress.set_value(5) progress.set_value(4)
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# Schritt 6: Flächenvergleich als Gate # Schritt 5: Flächenvergleich als Gate
# ------------------------------------------------------------------ # ------------------------------------------------------------------
progress.set_label("Schritt 6/6: Flächenvergleich …") progress.set_label("Schritt 5/6: Flächenvergleich …")
if progress.is_canceled(): if progress.is_canceled():
return None, None, None, LoadStatus.NONE return None, None, None, LoadStatus.NONE
@@ -421,11 +518,10 @@ def verfahrensgebiet_alkis_komplett(
alkis_vg_layer, enriched_layer, toleranz alkis_vg_layer, enriched_layer, toleranz
) )
progress.set_value(6) progress.set_value(5)
if stimmt_ueberein: if stimmt_ueberein:
progress.set_value(7) progress.set_value(6)
progress.close()
show_info_dialog( show_info_dialog(
"Flächenvergleich", "Flächenvergleich",
"Verfahrensgebiet mit erwarteter Geometrie erzeugt", "Verfahrensgebiet mit erwarteter Geometrie erzeugt",
@@ -437,7 +533,6 @@ def verfahrensgebiet_alkis_komplett(
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# Flächenabweichung → 3-Optionen-Dialog # Flächenabweichung → 3-Optionen-Dialog
# ------------------------------------------------------------------ # ------------------------------------------------------------------
progress.close()
diff = abs(bror_flaeche - vg_flaeche) diff = abs(bror_flaeche - vg_flaeche)
entscheidung = ask_detailpruefung_oder_vg_laden( entscheidung = ask_detailpruefung_oder_vg_laden(
tab_widget, tab_widget,
@@ -563,6 +658,9 @@ def verfahrensgebiet_alkis_komplett(
return enriched_layer, flst_layer, alkis_vg_layer, vg_status return enriched_layer, flst_layer, alkis_vg_layer, vg_status
finally: finally:
# Dialog 2 (falls erstellt) bei jedem Ausgang (Erfolg, Abbruch, Fehler) schließen
if progress is not None:
progress.close()
# WFS-Temp-Layer stets aus dem Projekt entfernen # WFS-Temp-Layer stets aus dem Projekt entfernen
_entferne_hilfs_layer(project, "TempVerfahrensgebiet") _entferne_hilfs_layer(project, "TempVerfahrensgebiet")
# BROR-Layer bei Fehler/Abbruch entfernen (nur wenn in diesem Lauf hinzugefügt) # BROR-Layer bei Fehler/Abbruch entfernen (nur wenn in diesem Lauf hinzugefügt)
+296
View File
@@ -0,0 +1,296 @@
"""sn_verfahrensgebiet/functions/verfahrensgebiet_dialoge.py – Dialoge zum
Laden eines Verfahrensgebiets aus ALKIS (Qt5/6/Mock-kompatibel).
"""
from typing import Any, Optional
from sn_basis.functions.qt_wrapper import (
QT_VERSION, exec_dialog, QInputDialog, QLineEdit,
QDialog, QDialogButtonBox, QListWidget, QListWidgetItem, QAbstractItemView,
QTableWidget, QTableWidgetItem, QTableWidgetSelectionRange,
QIcon, QVBoxLayout, QHBoxLayout, QLabel, QCheckBox,
DialogAccepted, ExtendedSelection, SelectRows, NoEditTriggers, AscendingOrder,
)
from sn_basis.functions.landkreise import ALLE_LANDKREISE
import os
# .../python/plugins (drei Ebenen über dieser Datei)
_PLUGINS_DIR = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
_WAPPEN_ICON = os.path.join(_PLUGINS_DIR, "sn_basis", "favicon.ico")
def ask_bror_kombinationen(
parent: Any,
kombinationen: list[tuple[str, str]],
) -> list[tuple[str, str]]:
"""Zeigt Dialog zur Auswahl einer oder mehrerer BROR-Kombinationen
(adv_name + bezeichnung).
Wird aufgerufen, wenn der WFS mehrere verschiedene Kombinationen aus
``adv_name`` und ``bezeichnung`` zurückliefert. Es können ein oder
mehrere Objekte gemeinsam ausgewählt und geladen (verschmolzen) werden.
Parameters
----------
parent :
Eltern-Widget oder None.
kombinationen :
Sortierte Liste von ``(adv_name, bezeichnung)``-Tupeln.
Returns
-------
list[tuple[str, str]]
Die gewählten ``(adv_name, bezeichnung)``-Tupel. Leere Liste bei
Abbruch oder wenn nichts ausgewählt wurde.
"""
if not kombinationen:
return []
if QT_VERSION == 0: # Mock-Modus
print(f"Mock-Modus: ask_bror_kombinationen → {kombinationen[:1]}")
return kombinationen[:1]
dialog = QDialog(parent)
dialog.setWindowTitle("Verfahrensgebiet aus ALKIS laden")
if os.path.exists(_WAPPEN_ICON):
dialog.setWindowIcon(QIcon(_WAPPEN_ICON))
layout = QVBoxLayout(dialog)
hinweis = QLabel(
f"Es wurden {len(kombinationen)} Objekte gefunden.\n\n"
"Bitte wählen Sie eines oder mehrere Objekte aus:"
)
hinweis.setWordWrap(True)
layout.addWidget(hinweis)
list_widget = QListWidget()
list_widget.setSelectionMode(ExtendedSelection)
for adv, bez in kombinationen:
item = QListWidgetItem(f"{adv} | {bez}")
list_widget.addItem(item)
list_widget.item(0).setSelected(True)
layout.addWidget(list_widget, 1) # Stretch-Faktor: wächst mit Fenstergröße
mehrfachauswahl_hinweis = QLabel("<small>Mehrfachauswahl mit Strg + Umschalt</small>")
layout.addWidget(mehrfachauswahl_hinweis)
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
buttons.accepted.connect(dialog.accept)
buttons.rejected.connect(dialog.reject)
layout.addWidget(buttons)
dialog.adjustSize()
breite = dialog.sizeHint().width()
# Liste auf max. 5 sichtbare Zeilen begrenzen, Rest per Scrollbar
sichtbare_zeilen = min(len(kombinationen), 5)
zeilenhoehe = list_widget.sizeHintForRow(0) if list_widget.count() else 20
# + Puffer für eine ggf. eingeblendete horizontale Scrollbar (bei langen
# Einträgen), damit diese nicht die letzte Zeile abschneidet.
scrollbar_puffer = list_widget.horizontalScrollBar().sizeHint().height()
list_hoehe = zeilenhoehe * sichtbare_zeilen + 2 * list_widget.frameWidth() + 4 + scrollbar_puffer
list_widget.setMinimumHeight(list_hoehe)
dialog.resize(breite, dialog.sizeHint().height())
if exec_dialog(dialog) != DialogAccepted:
return []
ausgewaehlte_indizes = sorted(list_widget.row(item) for item in list_widget.selectedItems())
return [kombinationen[i] for i in ausgewaehlte_indizes]
def ask_landkreis_objekte(
parent: Any,
kombinationen: list[tuple[str, str]],
) -> list[tuple[str, str]]:
"""Zeigt Dialog zur Auswahl eines, mehrerer oder aller BROR-Objekte eines
Landkreises (Fallback, wenn keine Verfahrensnummer gesetzt ist).
Wie :func:`ask_bror_kombinationen`, zusätzlich mit einer "Alle
auswählen"-Checkbox (da hier potenziell deutlich mehr Treffer als bei der
VKZ-Suche auftreten). Ob nach dem Laden der weitere Workflow
(Gemarkung/Flurstücke/Flächenvergleich) fortgesetzt wird, klärt eine
separate Stop-Abfrage nach dem Laden – nicht mehr dieser Dialog.
Parameters
----------
parent :
Eltern-Widget oder None.
kombinationen :
Sortierte Liste von ``(adv_name, bezeichnung)``-Tupeln.
Returns
-------
list[tuple[str, str]]
Die gewählten ``(adv_name, bezeichnung)``-Tupel. Leere Liste bei
Abbruch oder wenn nichts ausgewählt wurde.
"""
if not kombinationen:
return []
if QT_VERSION == 0: # Mock-Modus
print(f"Mock-Modus: ask_landkreis_objekte → {kombinationen[:1]}")
return kombinationen[:1]
dialog = QDialog(parent)
dialog.setWindowTitle("Verfahrensgebiete aus Landkreis laden")
if os.path.exists(_WAPPEN_ICON):
dialog.setWindowIcon(QIcon(_WAPPEN_ICON))
layout = QVBoxLayout(dialog)
hinweis = QLabel(
f"Es wurden {len(kombinationen)} Objekte gefunden.\n\n"
"Bitte wählen Sie eines oder mehrere Objekte aus:"
)
hinweis.setWordWrap(True)
layout.addWidget(hinweis)
suche_zeile = QHBoxLayout()
suche_zeile.addWidget(QLabel("Suche:"))
suche_feld = QLineEdit()
suche_feld.setPlaceholderText("Nummer oder Name …")
suche_zeile.addWidget(suche_feld)
layout.addLayout(suche_zeile)
alle_auswaehlen = QCheckBox("Alle auswählen")
layout.addWidget(alle_auswaehlen)
# Standardsortierung nach Bezeichnung; per Klick auf Spaltenkopf änderbar
# (setSortingEnabled aktiviert die eingebaute Tabellensortierung).
sortierte_kombinationen = sorted(kombinationen, key=lambda kb: kb[1])
table = QTableWidget(len(sortierte_kombinationen), 2)
table.setHorizontalHeaderLabels(["Nummer", "Name"])
table.setSelectionMode(ExtendedSelection)
table.setSelectionBehavior(SelectRows)
table.setEditTriggers(NoEditTriggers)
table.verticalHeader().setVisible(False)
table.verticalHeader().setDefaultSectionSize(22) # flachere Zeilen
table.horizontalHeader().setStretchLastSection(True)
table.setSortingEnabled(False)
for row, (adv, bez) in enumerate(sortierte_kombinationen):
table.setItem(row, 0, QTableWidgetItem(bez))
table.setItem(row, 1, QTableWidgetItem(adv))
table.setSortingEnabled(True)
table.sortByColumn(0, AscendingOrder) # Standard: aufsteigend nach Nummer
table.resizeColumnToContents(0)
table.setColumnWidth(0, max(110, table.columnWidth(0)))
table.selectRow(0)
layout.addWidget(table, 1) # Stretch-Faktor: wächst mit Fenstergröße
def _suche_geaendert(text):
text_lower = text.strip().lower()
for row in range(table.rowCount()):
bez_text = table.item(row, 0).text().lower()
adv_text = table.item(row, 1).text().lower()
treffer = not text_lower or text_lower in bez_text or text_lower in adv_text
table.setRowHidden(row, not treffer)
suche_feld.textChanged.connect(_suche_geaendert)
def _alle_auswaehlen_umschalten(checked):
table.clearSelection()
if checked:
letzte_spalte = table.columnCount() - 1
for row in range(table.rowCount()):
if not table.isRowHidden(row):
# setRangeSelected (statt selectRow) fügt der Selektion
# hinzu, ohne vorherige Zeilen wieder abzuwählen.
table.setRangeSelected(
QTableWidgetSelectionRange(row, 0, row, letzte_spalte), True
)
alle_auswaehlen.stateChanged.connect(_alle_auswaehlen_umschalten)
mehrfachauswahl_hinweis = QLabel("<small>Mehrfachauswahl mit Strg + Umschalt</small>")
layout.addWidget(mehrfachauswahl_hinweis)
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
buttons.accepted.connect(dialog.accept)
buttons.rejected.connect(dialog.reject)
layout.addWidget(buttons)
dialog.adjustSize()
# Bezeichnungs-Spalte + großzügige Breite für den ADV-Namen (lange Namen
# sollen möglichst ohne Abschneiden sichtbar sein) + Rahmen/Scrollbar-Puffer.
breite = max(dialog.sizeHint().width(), table.columnWidth(0) + 480)
# Tabelle auf max. 15 sichtbare Zeilen begrenzen, Rest per Scrollbar – hier
# potenziell deutlich mehr Treffer als bei der VKZ-Suche, daher großzügiger
# bemessen als bei ask_bror_kombinationen (dort 5).
sichtbare_zeilen = min(len(sortierte_kombinationen), 15)
zeilenhoehe = table.verticalHeader().defaultSectionSize() if table.rowCount() else 20
kopfzeile_hoehe = table.horizontalHeader().height()
# + Puffer für eine ggf. eingeblendete horizontale Scrollbar (bei langen
# Einträgen), damit diese nicht die letzte Zeile abschneidet.
scrollbar_puffer = table.horizontalScrollBar().sizeHint().height()
table_hoehe = (
zeilenhoehe * sichtbare_zeilen + kopfzeile_hoehe
+ 2 * table.frameWidth() + 4 + scrollbar_puffer
)
table.setMinimumHeight(table_hoehe)
dialog.resize(breite, dialog.sizeHint().height())
if exec_dialog(dialog) != DialogAccepted:
return []
ausgewaehlte_zeilen = sorted(set(item.row() for item in table.selectedItems()))
ausgewaehlt = [(table.item(row, 1).text(), table.item(row, 0).text()) for row in ausgewaehlte_zeilen]
return ausgewaehlt
def ask_landkreis_auswahl(
parent: Any,
landkreise: list[tuple[str, str]],
) -> Optional[str]:
"""Fragt per Dropdown-Dialog einen Landkreis ab (Fallback, wenn weder
Verfahrensnummer noch Landkreis in den Projekteigenschaften gesetzt ist).
Parameters
----------
parent :
Eltern-Widget oder None.
landkreise :
Liste von ``(kennzahl, name)``-Tupeln, z.B. ``LANDKREISE`` aus
:mod:`sn_basis.functions.landkreise`.
Die Auswahlliste beginnt mit einem leeren Platzhalter (der Nutzer muss
aktiv einen Eintrag wählen) und enthält zusätzlich "Alle Landkreise"
(Sentinel :data:`~sn_basis.functions.landkreise.ALLE_LANDKREISE`).
Returns
-------
Optional[str]
Gewählte Landkreiskennzahl, :data:`ALLE_LANDKREISE` bei Auswahl aller
Landkreise, oder ``None`` bei Abbruch, leerem Platzhalter oder leerer
Landkreisliste.
"""
if not landkreise:
return None
if QT_VERSION == 0: # Mock-Modus
print("Mock-Modus: ask_landkreis_auswahl → None (keine aktive Auswahl)")
return None
ALLE_LABEL = "Alle Landkreise"
namen = [""] + [ALLE_LABEL] + [name for _, name in landkreise]
dialog = QInputDialog(parent)
dialog.setWindowTitle("Landkreis auswählen")
if os.path.exists(_WAPPEN_ICON):
dialog.setWindowIcon(QIcon(_WAPPEN_ICON))
dialog.setLabelText("Landkreis:")
dialog.setComboBoxItems(namen)
dialog.setComboBoxEditable(False)
if exec_dialog(dialog) != DialogAccepted:
return None
name = dialog.textValue()
if not name:
return None
if name == ALLE_LABEL:
return ALLE_LANDKREISE
name_zu_kennzahl = {n: k for k, n in landkreise}
return name_zu_kennzahl.get(str(name))
+382 -52
View File
@@ -3,8 +3,10 @@
Orchestriert den vollständigen Arbeitsablauf zum Laden eines Verfahrensgebiets Orchestriert den vollständigen Arbeitsablauf zum Laden eines Verfahrensgebiets
aus einem ZIP-Archiv mit Shapefile-Set: aus einem ZIP-Archiv mit Shapefile-Set:
1. Dateidialog öffnen (.zip-Filter); Nutzerabbruch beendet den Vorgang. 1. Dateidialog öffnen (.zip/.shp-Filter, Mehrfachauswahl); Nutzerabbruch
2. ZIP neben der ZIP-Datei entpacken und auf ein gültiges Shapefile-Set prüfen. 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). 3. Shapefile als QGIS-Vektorlayer laden und validieren (Standard-QGIS-Routinen).
4. Alle Features per ``QgsGeometry.unaryUnion`` zu einem Single-Feature- 4. Alle Features per ``QgsGeometry.unaryUnion`` zu einem Single-Feature-
Memory-Layer „Verfahrensgebiet" verschmelzen. Memory-Layer „Verfahrensgebiet" verschmelzen.
@@ -40,6 +42,9 @@ oder kein Ergebnis liefert, bricht den Workflow ab.
from __future__ import annotations from __future__ import annotations
import os import os
import re
import shutil
import tempfile
import zipfile import zipfile
from typing import Optional, Tuple from typing import Optional, Tuple
@@ -53,6 +58,7 @@ from sn_basis.functions.dialog_wrapper import (
create_progress_dialog, create_progress_dialog,
show_info_dialog, show_info_dialog,
ask_detailpruefung_oder_abbrechen, ask_detailpruefung_oder_abbrechen,
ask_yes_no,
) )
from sn_verfahrensgebiet.functions.verfahrensgebiet_alkis import LoadStatus, verfahrensgebiet_alkis 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, 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 # 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: def _wende_vergleich_stil_an(layer) -> None:
"""Wendet einen kategorisierten Renderer auf den Vergleich-Layer an. """Wendet einen kategorisierten Renderer auf den Vergleich-Layer an.
@@ -215,6 +339,31 @@ def _finde_shp(verzeichnis: str) -> Optional[str]:
return None 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]: def _erstelle_memory_layer_aus_shp(shp_layer) -> Optional[object]:
"""Vereint alle Features des Shapefile-Layers zu einem Single-Feature- """Vereint alle Features des Shapefile-Layers zu einem Single-Feature-
Memory-Layer „Verfahrensgebiet" via ``QgsGeometry.unaryUnion``. Memory-Layer „Verfahrensgebiet" via ``QgsGeometry.unaryUnion``.
@@ -290,17 +439,38 @@ def _erstelle_memory_layer_aus_shp(shp_layer) -> Optional[object]:
# Öffentliche Lade-Hilfsfunktion # Öffentliche Lade-Hilfsfunktion
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def lade_vg_aus_zip(parent_widget) -> Optional[object]: def lade_vg_aus_zip(parent_widget) -> Optional[list]:
"""Öffnet Dateidialog, entpackt ZIP und gibt Union-Memory-Layer zurück. """Öffnet Dateidialog (Mehrfachauswahl), entpackt ZIPs (bzw. nutzt direkt
gewählte ``.shp``-Dateien) und liefert je Gebietstyp einen Union-Memory-Layer.
Schrittfolge: Schrittfolge:
1. ``QFileDialog.getOpenFileName`` mit Filter ``ZIP-Dateien (*.zip)``. 1. ``QFileDialog.getOpenFileNames`` mit Filter für ``.zip`` und ``.shp``
2. ZIP neben der Quelldatei in ein Unterverzeichnis entpacken. (Mehrfachauswahl möglich).
3. Erste ``.shp``-Datei im Verzeichnis finden. 2. Für jede ausgewählte Datei: bei ``.zip`` in ein temporäres Verzeichnis
4. Als ``QgsVectorLayer`` laden und ``isValid()`` prüfen. entpacken (danach wieder gelöscht) und darin **alle** enthaltenen
5. Alle Features per ``QgsGeometry.unaryUnion`` verschmelzen. ``.shp``-Dateien finden (ein ZIP kann mehrere Shapefile-Sets
6. Single-Feature-Memory-Layer „Verfahrensgebiet" zurückgeben. 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 Parameters
---------- ----------
@@ -309,45 +479,161 @@ def lade_vg_aus_zip(parent_widget) -> Optional[object]:
Returns Returns
------- -------
Optional[QgsVectorLayer] Optional[list]
Valider Memory-Layer „Verfahrensgebiet" oder ``None`` bei Abbruch/Fehler. 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, 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 return None # Nutzerabbruch
zip_dir = os.path.dirname(pfad) # Je Gebietstyp gesammelte Geometrien/Felder/Attribute über alle
zip_base = os.path.splitext(os.path.basename(pfad))[0] # ausgewählten Dateien hinweg (Reihenfolge der ersten Sichtung bleibt
extract_dir = os.path.join(zip_dir, zip_base) # ü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:
# (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 = []
for pfad in pfade:
dateiname = os.path.basename(pfad)
if pfad.lower().endswith(".shp"):
shp_eintraege.append((pfad, dateiname))
continue
extract_dir = tempfile.mkdtemp(prefix="sn_vg_shape_")
temp_dirs.append(extract_dir)
try: try:
with zipfile.ZipFile(pfad, "r") as zf: with zipfile.ZipFile(pfad, "r") as zf:
zf.extractall(extract_dir) zf.extractall(extract_dir)
except Exception: except Exception:
QMessageBox.warning( fehler_liste.append(
parent_widget, f"{dateiname}: ZIP-Datei konnte nicht entpackt werden."
"Fehler",
"Die ZIP-Datei konnte nicht entpackt werden.\n"
"Bitte eine gültige ZIP-Datei auswählen.",
) )
return None continue
shp_path = _finde_shp(extract_dir) shp_pfade_im_zip = _finde_alle_shp(extract_dir)
if not shp_path: if not shp_pfade_im_zip:
QMessageBox.warning( fehler_liste.append(
parent_widget, f"{dateiname}: kein gültiges Shape-Dateiset enthalten."
"Fehler",
"gültiges Shape-Dateiset angeben",
) )
return None 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") raw_layer = QgsVectorLayer(shp_path, "tmp_shape_vg", "ogr")
if not raw_layer or not raw_layer.isValid(): 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( QMessageBox.warning(
parent_widget, parent_widget,
"Fehler", "Fehler",
@@ -355,16 +641,11 @@ def lade_vg_aus_zip(parent_widget) -> Optional[object]:
) )
return None return None
mem_layer = _erstelle_memory_layer_aus_shp(raw_layer) return ergebnis_layer
if mem_layer is None:
QMessageBox.warning(
parent_widget,
"Fehler",
"gültiges Shape-Dateiset angeben",
)
return None
return mem_layer finally:
for d in temp_dirs:
shutil.rmtree(d, ignore_errors=True)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -419,38 +700,66 @@ def verfahrensgebiet_shape_komplett(
""" """
project = QgsProject.instance() project = QgsProject.instance()
shape_vg_layer = None shape_vg_layer = None
zusatz_shape_layer: list = []
bror_layer = None bror_layer = None
shape_was_added_by_us = False shape_was_added_by_us = False
bror_was_added_by_us = False bror_was_added_by_us = False
abgeschlossen = False abgeschlossen = False
progress = create_progress_dialog( progress = None
7, "Verfahrensgebiet aus Shape laden", "Initialisierung …"
)
try: try:
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# Schritt 1: Shape-VG aus ZIP laden (noch NICHT ins Projekt einfügen) # 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 …") shape_vg_layers = lade_vg_aus_zip(tab_widget)
progress.set_value(0)
if progress.is_canceled(): if not shape_vg_layers:
return None, None, None, LoadStatus.NONE return None, None, None, LoadStatus.NONE
shape_vg_layer = lade_vg_aus_zip(tab_widget) # Je Gebietstyp ein Layer (max. 2). Der erste ist das für den
progress.raise_to_front() # 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 return None, None, None, LoadStatus.NONE
vg_status = LoadStatus.FIRST 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) progress.set_value(1)
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# Schritt 2: BROR aus ALKIS laden (vor shape_vg, um Namenskonflikt # Schritt 2: BROR aus ALKIS laden (vor shape_vg, um Namenskonflikt
# in _check_existing_layer zu vermeiden) # in _check_existing_layer zu vermeiden)
# ------------------------------------------------------------------ # ------------------------------------------------------------------
progress.set_label("Schritt 2/6: BROR aus ALKIS laden …")
if progress.is_canceled(): if progress.is_canceled():
return None, None, None, LoadStatus.NONE return None, None, None, LoadStatus.NONE
@@ -468,10 +777,26 @@ def verfahrensgebiet_shape_komplett(
progress.set_value(2) progress.set_value(2)
# Shape-VG erst jetzt ins Projekt einfügen (nach BROR-Ladung) # 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) project.addMapLayer(shape_vg_layer)
shape_was_added_by_us = True 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 # Schritt 3: Gemarkungsschlüssel per Geometrie-Spatial-Join ermitteln
# ------------------------------------------------------------------ # ------------------------------------------------------------------
@@ -691,6 +1016,11 @@ def verfahrensgebiet_shape_komplett(
_entferne_hilfs_layer(project, "TempVerfahrensgebiet") _entferne_hilfs_layer(project, "TempVerfahrensgebiet")
# Shape-VG und BROR bei Fehler/Abbruch entfernen # Shape-VG und BROR bei Fehler/Abbruch entfernen
if not abgeschlossen: 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: if shape_was_added_by_us and shape_vg_layer is not None:
try: try:
project.removeMapLayer(shape_vg_layer.id()) 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>
+22
View File
@@ -0,0 +1,22 @@
<!DOCTYPE qgis PUBLIC 'http://mrcc.com/qgis.dtd' 'SYSTEM'>
<qgis layerType="Vector" styleCategories="Symbology" version="4.0.1-Norrköping">
<renderer-v2 enableorderby="0" forceraster="0" referencescale="-1" symbollevels="0" type="singleSymbol">
<symbols>
<symbol alpha="1" clip_to_extent="1" force_rhr="0" frame_rate="10" is_animated="0" name="0" type="fill">
<layer class="SimpleFill" enabled="1" locked="0" pass="0">
<Option type="Map">
<Option name="color" type="QString" value="0,180,0,150,rgb:0,0.7058824,0,0.5882353"/>
<Option name="outline_color" type="QString" value="0,120,0,255,rgb:0,0.4705882,0,1"/>
<Option name="outline_style" type="QString" value="solid"/>
<Option name="outline_width" type="QString" value="0.4"/>
<Option name="outline_width_unit" type="QString" value="MM"/>
<Option name="style" type="QString" value="solid"/>
</Option>
</layer>
</symbol>
</symbols>
</renderer-v2>
<blendMode>0</blendMode>
<featureBlendMode>0</featureBlendMode>
<layerGeometryType>2</layerGeometryType>
</qgis>
+51
View File
@@ -0,0 +1,51 @@
<!DOCTYPE qgis PUBLIC 'http://mrcc.com/qgis.dtd' 'SYSTEM'>
<qgis layerType="Vector" styleCategories="Symbology" version="4.0.1-Norrköping">
<renderer-v2 attr="status" enableorderby="0" forceraster="0" referencescale="-1" symbollevels="0" type="categorizedSymbol">
<categories>
<category label="beide (Übereinstimmung)" render="true" symbol="0" value="beide"/>
<category label="nur im Verfahrensgebiet" render="true" symbol="1" value="nur_verfahrensgebiet"/>
<category label="nur in der CSV-Liste (AGLB)" render="true" symbol="2" value="nur_aglb"/>
</categories>
<symbols>
<symbol alpha="1" clip_to_extent="1" force_rhr="0" frame_rate="10" is_animated="0" name="0" type="fill">
<layer class="SimpleFill" enabled="1" locked="0" pass="0">
<Option type="Map">
<Option name="color" type="QString" value="0,180,0,150,rgb:0,0.7058824,0,0.5882353"/>
<Option name="outline_color" type="QString" value="0,120,0,255,rgb:0,0.4705882,0,1"/>
<Option name="outline_style" type="QString" value="solid"/>
<Option name="outline_width" type="QString" value="0.4"/>
<Option name="outline_width_unit" type="QString" value="MM"/>
<Option name="style" type="QString" value="solid"/>
</Option>
</layer>
</symbol>
<symbol alpha="1" clip_to_extent="1" force_rhr="0" frame_rate="10" is_animated="0" name="1" type="fill">
<layer class="SimpleFill" enabled="1" locked="0" pass="0">
<Option type="Map">
<Option name="color" type="QString" value="220,0,0,150,rgb:0.8627451,0,0,0.5882353"/>
<Option name="outline_color" type="QString" value="150,0,0,255,rgb:0.5882353,0,0,1"/>
<Option name="outline_style" type="QString" value="solid"/>
<Option name="outline_width" type="QString" value="0.4"/>
<Option name="outline_width_unit" type="QString" value="MM"/>
<Option name="style" type="QString" value="solid"/>
</Option>
</layer>
</symbol>
<symbol alpha="1" clip_to_extent="1" force_rhr="0" frame_rate="10" is_animated="0" name="2" type="fill">
<layer class="SimpleFill" enabled="1" locked="0" pass="0">
<Option type="Map">
<Option name="color" type="QString" value="255,128,0,150,rgb:1,0.5019608,0,0.5882353"/>
<Option name="outline_color" type="QString" value="190,96,0,255,rgb:0.745098,0.3764706,0,1"/>
<Option name="outline_style" type="QString" value="solid"/>
<Option name="outline_width" type="QString" value="0.4"/>
<Option name="outline_width_unit" type="QString" value="MM"/>
<Option name="style" type="QString" value="solid"/>
</Option>
</layer>
</symbol>
</symbols>
</renderer-v2>
<blendMode>0</blendMode>
<featureBlendMode>0</featureBlendMode>
<layerGeometryType>2</layerGeometryType>
</qgis>
+304
View File
@@ -0,0 +1,304 @@
"""sn_verfahrensgebiet/tests/test_beteiligung_pruefung.py
Unit-Tests für :mod:`sn_verfahrensgebiet.functions.beteiligung_pruefung`.
Getestete Szenarien:
baue_flurstueck_csv:
- Normalfall mit führenden Nullen bei Zähler/Nenner
- Nenner = 0 → Platzhalter "____"
- Nenner leer → Platzhalter "____"
- Ungültiges Format (nicht 4 Teile) → None
- Leerer Wert → None
baue_flurstueck_csv_liste:
- Bildet Liste nur aus gültigen Zeilen, schreibt Flurstueck_csv in jede Zeile zurück
lade_beteiligung_csv:
- Dateidialog abgebrochen → None
- Spalte FLURSTUECK fehlt → BeteiligungPruefungFehler
- Leere Datei → BeteiligungPruefungFehler
- Erfolgsfall → (zeilen, spaltennamen)
try2/try3 (kein automatisches Laden mehr, siehe Modul-Docstring):
- Verfahrensgebiet-Layer ohne Kennzeichen vorhanden → Aufforderung statt Auto-Laden
- Kein Verfahrensgebiet-Layer vorhanden → Verfahrensnummer aus CSV wird gesetzt,
dann Aufforderung statt Auto-Laden
"""
import csv
import os
import sys
import tempfile
import types
import unittest
from unittest.mock import patch, MagicMock
# ---------------------------------------------------------------------------
# Minimales QGIS-Mock-Setup
# ---------------------------------------------------------------------------
if "qgis" not in sys.modules:
qgis_mod = types.ModuleType("qgis")
qgis_core = types.ModuleType("qgis.core")
qgis_utils = types.ModuleType("qgis.utils")
qgis_utils.iface = types.SimpleNamespace()
qgis_utils.plugins = {}
qgis_mod.utils = qgis_utils
qgis_mod.core = qgis_core
sys.modules["qgis"] = qgis_mod
sys.modules["qgis.core"] = qgis_core
sys.modules["qgis.utils"] = qgis_utils
from sn_verfahrensgebiet.functions.beteiligung_pruefung import (
baue_flurstueck_csv,
baue_flurstueck_csv_liste,
lade_beteiligung_csv,
beteiligung_pruefen,
BeteiligungPruefungFehler,
FELD_FLURSTUECK_CSV,
CSV_SPALTE_FLURSTUECK,
CSV_SPALTE_VEF_VKZF,
_fordere_manuelles_laden_an,
_try3_bootstrap,
_hole_verfahrensgebiet_layer,
)
_MOD = "sn_verfahrensgebiet.functions.beteiligung_pruefung"
class TestBaueFlurstueckCsv(unittest.TestCase):
def test_normalfall_mit_fuehrenden_nullen(self):
ergebnis = baue_flurstueck_csv("1234/123/45/00", "14")
self.assertEqual(ergebnis, "141234___00123004500")
def test_nenner_null_wird_zu_unterstrichen(self):
ergebnis = baue_flurstueck_csv("1234/123/0/00", "14")
self.assertEqual(ergebnis, "141234___00123____00")
def test_nenner_leer_wird_zu_unterstrichen(self):
ergebnis = baue_flurstueck_csv("1234//45/00", "14")
# Gemarkung 1234, Zähler leer -> "00000"? -> zfill auf leeren String ergibt "00000"
self.assertTrue(ergebnis.startswith("141234___00000"))
def test_gemarkung_ohne_fuehrende_nullen_wird_aufgefuellt(self):
ergebnis = baue_flurstueck_csv("12/1/1/00", "14")
self.assertTrue(ergebnis.startswith("140012___"))
def test_ungueltiges_format_gibt_none(self):
self.assertIsNone(baue_flurstueck_csv("1234/123/45", "14"))
self.assertIsNone(baue_flurstueck_csv("1234/123/45/00/99", "14"))
def test_leerer_wert_gibt_none(self):
self.assertIsNone(baue_flurstueck_csv("", "14"))
self.assertIsNone(baue_flurstueck_csv(None, "14"))
def test_nenner_9001_wird_zu_buchstabe_a(self):
ergebnis = baue_flurstueck_csv("1234/123/9001/00", "14")
self.assertEqual(ergebnis, "141234___00123000a00")
def test_nenner_9002_wird_zu_buchstabe_b(self):
ergebnis = baue_flurstueck_csv("1234/123/9002/00", "14")
self.assertEqual(ergebnis, "141234___00123000b00")
def test_nenner_9026_wird_zu_buchstabe_z(self):
ergebnis = baue_flurstueck_csv("1234/123/9026/00", "14")
self.assertEqual(ergebnis, "141234___00123000z00")
def test_nenner_ausserhalb_buchstaben_bereich_bleibt_numerisch(self):
ergebnis = baue_flurstueck_csv("1234/123/9027/00", "14")
self.assertEqual(ergebnis, "141234___00123902700")
def test_nenner_9001_mit_fuehrenden_nullen_wird_erkannt(self):
ergebnis = baue_flurstueck_csv("1234/123/09001/00", "14")
self.assertEqual(ergebnis, "141234___00123000a00")
def test_einstellige_folge_wird_auf_2_stellen_aufgefuellt(self):
ergebnis = baue_flurstueck_csv("1234/123/45/5", "14")
self.assertEqual(ergebnis, "141234___00123004505")
class TestBaueFlurstueckCsvListe(unittest.TestCase):
def test_nur_gueltige_zeilen_in_liste(self):
zeilen = [
{CSV_SPALTE_FLURSTUECK: "1234/123/45/00"},
{CSV_SPALTE_FLURSTUECK: "ungueltig"},
{CSV_SPALTE_FLURSTUECK: "5678/1/0/01"},
]
ergebnis = baue_flurstueck_csv_liste(zeilen, "14")
self.assertEqual(len(ergebnis), 2)
self.assertEqual(zeilen[0][FELD_FLURSTUECK_CSV], "141234___00123004500")
self.assertEqual(zeilen[1][FELD_FLURSTUECK_CSV], "")
self.assertEqual(zeilen[2][FELD_FLURSTUECK_CSV], "145678___00001____01")
class TestHoleVerfahrensgebietLayer(unittest.TestCase):
@patch(f"{_MOD}.QgsProject")
def test_layer_per_name_hat_vorrang_vor_combo_auswahl(self, mock_project_cls):
namens_layer = MagicMock(name="verfahrensgebiet_layer")
mock_project = MagicMock()
mock_project.mapLayersByName.return_value = [namens_layer]
mock_project_cls.instance.return_value = mock_project
andere_auswahl = MagicMock(name="andere_layer_in_combo")
andere_auswahl.isValid.return_value = True
tab_widget = MagicMock()
tab_widget.verf_layer_combo.currentLayer.return_value = andere_auswahl
ergebnis = _hole_verfahrensgebiet_layer(tab_widget)
self.assertIs(ergebnis, namens_layer)
@patch(f"{_MOD}.QgsProject")
def test_combo_auswahl_als_fallback_ohne_namens_layer(self, mock_project_cls):
mock_project = MagicMock()
mock_project.mapLayersByName.return_value = []
mock_project_cls.instance.return_value = mock_project
combo_layer = MagicMock(name="combo_layer")
combo_layer.isValid.return_value = True
tab_widget = MagicMock()
tab_widget.verf_layer_combo.currentLayer.return_value = combo_layer
ergebnis = _hole_verfahrensgebiet_layer(tab_widget)
self.assertIs(ergebnis, combo_layer)
@patch(f"{_MOD}.QgsProject")
def test_kein_layer_gibt_none(self, mock_project_cls):
mock_project = MagicMock()
mock_project.mapLayersByName.return_value = []
mock_project_cls.instance.return_value = mock_project
tab_widget = MagicMock()
tab_widget.verf_layer_combo.currentLayer.return_value = None
self.assertIsNone(_hole_verfahrensgebiet_layer(tab_widget))
class TestFordereManuellesLadenAn(unittest.TestCase):
"""try2/try3 laden nicht mehr automatisch, sondern fordern den Nutzer auf,
den regulären interaktiven Ablauf ('Aus ALKIS laden') selbst zu starten."""
def test_ohne_vef_vkzf_nennt_fehlende_kennzeichen(self):
with self.assertRaises(BeteiligungPruefungFehler) as ctx:
_fordere_manuelles_laden_an()
self.assertIn("keine verwertbaren", str(ctx.exception))
self.assertIn("Aus ALKIS laden", str(ctx.exception))
def test_mit_vef_vkzf_nennt_verfahrensnummer(self):
with self.assertRaises(BeteiligungPruefungFehler) as ctx:
_fordere_manuelles_laden_an("12345")
self.assertIn("12345", str(ctx.exception))
self.assertIn("Aus ALKIS laden", str(ctx.exception))
class TestTry3Bootstrap(unittest.TestCase):
@patch(f"{_MOD}.set_variable")
def test_setzt_verfahrensnummer_und_fordert_manuelles_laden_an(self, mock_set_variable):
zeilen = [{CSV_SPALTE_VEF_VKZF: "12345", CSV_SPALTE_FLURSTUECK: "1234/1/1/00"}]
spaltennamen = [CSV_SPALTE_VEF_VKZF, CSV_SPALTE_FLURSTUECK]
with self.assertRaises(BeteiligungPruefungFehler) as ctx:
_try3_bootstrap(object(), zeilen, spaltennamen)
mock_set_variable.assert_called_once_with("verfahrensnummer", "12345", scope="project")
self.assertIn("12345", str(ctx.exception))
class TestBeteiligungPruefenDispatch(unittest.TestCase):
"""Dispatch-Logik in beteiligung_pruefen(): try1 unverändert, try2/try3
fordern jetzt auf statt automatisch zu laden."""
@patch(f"{_MOD}._hat_verwendbare_kennzeichen", return_value=False)
@patch(f"{_MOD}._hole_verfahrensgebiet_layer")
@patch(f"{_MOD}.baue_flurstueck_csv_liste")
@patch(f"{_MOD}.get_laenderkennung", return_value="14")
@patch(f"{_MOD}.lade_beteiligung_csv")
def test_layer_ohne_kennzeichen_fordert_manuelles_laden_an(
self, mock_lade_csv, mock_laenderkennung, mock_liste, mock_hole_layer, mock_hat_kz
):
mock_lade_csv.return_value = ([{}], ["FLURSTUECK"])
mock_liste.return_value = ["145678___00001____01"]
mock_hole_layer.return_value = MagicMock(name="verf_layer_ohne_kz")
with self.assertRaises(BeteiligungPruefungFehler) as ctx:
beteiligung_pruefen(object())
self.assertIn("keine verwertbaren", str(ctx.exception))
@patch(f"{_MOD}.set_variable")
@patch(f"{_MOD}._hole_verfahrensgebiet_layer", return_value=None)
@patch(f"{_MOD}.baue_flurstueck_csv_liste")
@patch(f"{_MOD}.get_laenderkennung", return_value="14")
@patch(f"{_MOD}.lade_beteiligung_csv")
def test_kein_layer_setzt_verfahrensnummer_und_fordert_laden_an(
self, mock_lade_csv, mock_laenderkennung, mock_liste, mock_hole_layer, mock_set_variable
):
zeilen = [{CSV_SPALTE_VEF_VKZF: "12345", CSV_SPALTE_FLURSTUECK: "1234/1/1/00"}]
mock_lade_csv.return_value = (zeilen, [CSV_SPALTE_VEF_VKZF, CSV_SPALTE_FLURSTUECK])
mock_liste.return_value = ["145678___00001____01"]
with self.assertRaises(BeteiligungPruefungFehler) as ctx:
beteiligung_pruefen(object())
mock_set_variable.assert_called_once_with("verfahrensnummer", "12345", scope="project")
self.assertIn("12345", str(ctx.exception))
class TestLadeBeteiligungCsv(unittest.TestCase):
def setUp(self):
self._tmp_files = []
def tearDown(self):
for pfad in self._tmp_files:
try:
os.remove(pfad)
except OSError:
pass
def _schreibe_csv(self, header, zeilen):
fd, pfad = tempfile.mkstemp(suffix=".csv")
self._tmp_files.append(pfad)
with os.fdopen(fd, "w", encoding="utf-8", newline="") as f:
writer = csv.writer(f)
writer.writerow(header)
for zeile in zeilen:
writer.writerow(zeile)
return pfad
@patch(f"{_MOD}.QFileDialog")
def test_dialog_abgebrochen_gibt_none(self, mock_fd):
mock_fd.getOpenFileName.return_value = ("", "")
self.assertIsNone(lade_beteiligung_csv(None))
@patch(f"{_MOD}.QFileDialog")
def test_fehlende_flurstueck_spalte_wirft_fehler(self, mock_fd):
pfad = self._schreibe_csv(["FOO", "BAR"], [["1", "2"]])
mock_fd.getOpenFileName.return_value = (pfad, "")
with self.assertRaises(BeteiligungPruefungFehler) as ctx:
lade_beteiligung_csv(None)
self.assertIn(CSV_SPALTE_FLURSTUECK, str(ctx.exception))
@patch(f"{_MOD}.QFileDialog")
def test_leere_datei_wirft_fehler(self, mock_fd):
fd, pfad = tempfile.mkstemp(suffix=".csv")
os.close(fd)
self._tmp_files.append(pfad)
mock_fd.getOpenFileName.return_value = (pfad, "")
with self.assertRaises(BeteiligungPruefungFehler):
lade_beteiligung_csv(None)
@patch(f"{_MOD}.QFileDialog")
def test_erfolgsfall_gibt_zeilen_und_spalten_zurueck(self, mock_fd):
pfad = self._schreibe_csv(
[CSV_SPALTE_VEF_VKZF, "FLA_Beteiligung", CSV_SPALTE_FLURSTUECK],
[["12345", "1", "1234/123/45/00"]],
)
mock_fd.getOpenFileName.return_value = (pfad, "")
zeilen, spalten = lade_beteiligung_csv(None)
self.assertEqual(len(zeilen), 1)
self.assertIn(CSV_SPALTE_FLURSTUECK, spalten)
self.assertEqual(zeilen[0][CSV_SPALTE_FLURSTUECK], "1234/123/45/00")
if __name__ == "__main__":
unittest.main()
+221 -86
View File
@@ -6,8 +6,11 @@ Getestet werden:
- Nutzerabbruch bei ask_text → (None, LoadStatus.NONE) - Nutzerabbruch bei ask_text → (None, LoadStatus.NONE)
- Ungültiger WFS-Layer → (None, LoadStatus.NONE) - Ungültiger WFS-Layer → (None, LoadStatus.NONE)
- Erfolgreicher Ladevorgang → gültiger Layer + LoadStatus.FIRST - Erfolgreicher Ladevorgang → gültiger Layer + LoadStatus.FIRST
- Bestehender Layer: Nutzer wählt "Neu laden" → LoadStatus.RELOAD - Bestehender Layer, Drei-Knopf-Dialog: "Überschreiben" → LoadStatus.RELOAD,
- Bestehender Layer: Nutzer wählt "Behalten" → LoadStatus.KEEP "Abbrechen" → (None, LoadStatus.NONE), "Anhängen" siehe
TestFlurstueckAlkisAnhaengenDedup
- vorgabe_schluessel/vorgabe_namen füllen den Eingabedialog vor
- ohne_schluessel_abfrage=True überspringt die ask_text-Abfrage
""" """
import sys import sys
@@ -30,7 +33,12 @@ if "qgis" not in sys.modules:
sys.modules["qgis.core"] = qgis_core sys.modules["qgis.core"] = qgis_core
sys.modules["qgis.utils"] = qgis_utils 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 from sn_verfahrensgebiet.functions.verfahrensgebiet_alkis import LoadStatus
@@ -49,8 +57,8 @@ class _DummyLayer:
def id(self): def id(self):
return self._id return self._id
def extent(self): def featureCount(self):
return object() return 1
class _DummyProject: class _DummyProject:
@@ -78,7 +86,7 @@ class TestFlurstueckAlkis(unittest.TestCase):
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance") @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance")
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text") @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text")
def test_user_cancel_returns_none(self, mock_ask_text, mock_project, mock_msgbox): 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_ask_text.return_value = ("", False)
mock_project.return_value = _DummyProject() 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.QgsProject.instance")
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text") @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text")
def test_empty_text_after_confirmation_returns_none(self, mock_ask_text, mock_project, mock_msgbox): 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_ask_text.return_value = (" ", True)
mock_project.return_value = _DummyProject() mock_project.return_value = _DummyProject()
@@ -110,7 +118,7 @@ class TestFlurstueckAlkis(unittest.TestCase):
def test_invalid_wfs_layer_returns_none( def test_invalid_wfs_layer_returns_none(
self, mock_ask_text, mock_project, mock_wfs_layer_cls, mock_msgbox 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_ask_text.return_value = ("1234", True)
mock_project.return_value = _DummyProject() mock_project.return_value = _DummyProject()
mock_wfs_layer_cls.return_value = _DummyLayer(valid=False) mock_wfs_layer_cls.return_value = _DummyLayer(valid=False)
@@ -123,67 +131,64 @@ class TestFlurstueckAlkis(unittest.TestCase):
# ------------------------------------------------------------------ # # ------------------------------------------------------------------ #
# 3. Erfolgreicher Erstladevorgang # # 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.QgsProject.instance")
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text") @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text")
def test_success_returns_layer_and_first_status( 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_ask_text.return_value = ("1234, 5678", True)
mock_project.return_value = _DummyProject() mock_project.return_value = _DummyProject()
valid_layer = _DummyLayer(valid=True, layer_id="wfs_ok") 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) layer, status = flurstueck_alkis(None)
self.assertIs(layer, valid_layer) self.assertIs(layer, valid_layer)
self.assertEqual(status, LoadStatus.FIRST) 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.QgsVectorLayer")
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance") def test_sql_filter_contains_all_schluessel(self, mock_wfs_layer_cls):
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text") """Prüft, dass lade_flurstuecke_per_gemarkung für jeden Gemarkungsschlüssel eine eigene WFS-Anfrage mit dessen Schlüssel stellt."""
def test_sql_filter_contains_all_schluessel( # Ungültig genügt: nur die aufgebaute URI je Aufruf wird geprüft, das
self, mock_ask_text, mock_project, mock_wfs_layer_cls # tatsächliche Zusammenführen der Features ist nicht Teil dieses Tests.
): mock_wfs_layer_cls.return_value = _DummyLayer(valid=False)
"""Der WFS-URI enthält alle eingegebenen Gemarkungsschlüssel."""
mock_ask_text.return_value = ("1111, 2222, 3333", True)
mock_project.return_value = _DummyProject()
mock_wfs_layer_cls.return_value = _DummyLayer(valid=True)
flurstueck_alkis(None) lade_flurstuecke_per_gemarkung(["1111", "2222", "3333"])
# Erster Positionsarg von QgsVectorLayer ist der URI-String uris = [call.args[0] for call in mock_wfs_layer_cls.call_args_list]
call_args = mock_wfs_layer_cls.call_args self.assertEqual(len(uris), 3)
uri_str = call_args[0][0] if call_args[0] else call_args[1].get("path", "") self.assertIn("1111", uris[0])
self.assertIn("1111", uri_str) self.assertIn("2222", uris[1])
self.assertIn("2222", uri_str) self.assertIn("3333", uris[2])
self.assertIn("3333", uri_str)
# ------------------------------------------------------------------ # # ------------------------------------------------------------------ #
# 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.QMessageBox")
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance") @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance")
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text") @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text")
def test_existing_layer_reload_yes( 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") existing = _DummyLayer(valid=True, layer_id="old_flst")
project = _DummyProject(named_layers=[existing]) project = _DummyProject(named_layers=[existing])
mock_ask_text.return_value = ("1234", True) mock_ask_text.return_value = ("1234", True)
mock_project.return_value = project mock_project.return_value = project
# StandardButton als int, damit | Operator funktioniert # Drei-Knopf-Dialog: addButton() liefert je Aufruf einen eigenen
mock_msgbox_cls.StandardButton.Yes = 0x4000 # Sentinel, clickedButton() simuliert den Klick auf "Überschreiben".
mock_msgbox_cls.StandardButton.No = 0x10000 btn_ueberschreiben, btn_anhaengen, btn_abbrechen = object(), object(), object()
# QMessageBox.question → Yes msg_instance = mock_msgbox_cls.return_value
mock_msgbox_cls.question.return_value = 0x4000 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") 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) layer, status = flurstueck_alkis(None)
@@ -192,46 +197,46 @@ class TestFlurstueckAlkis(unittest.TestCase):
self.assertEqual(status, LoadStatus.RELOAD) 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.QMessageBox")
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance") @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance")
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text") @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 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") existing = _DummyLayer(valid=True, layer_id="keep_flst")
project = _DummyProject(named_layers=[existing]) project = _DummyProject(named_layers=[existing])
mock_ask_text.return_value = ("1234", True) mock_ask_text.return_value = ("1234", True)
mock_project.return_value = project mock_project.return_value = project
# StandardButton als int, damit | Operator funktioniert btn_ueberschreiben, btn_anhaengen, btn_abbrechen = object(), object(), object()
mock_msgbox_cls.StandardButton.Yes = 0x4000 msg_instance = mock_msgbox_cls.return_value
mock_msgbox_cls.StandardButton.No = 0x10000 msg_instance.addButton.side_effect = [btn_ueberschreiben, btn_anhaengen, btn_abbrechen]
# QMessageBox.question → No msg_instance.clickedButton.return_value = btn_abbrechen
mock_msgbox_cls.question.return_value = 0x10000
layer, status = flurstueck_alkis(None) layer, status = flurstueck_alkis(None)
self.assertIs(layer, existing) self.assertIsNone(layer)
self.assertEqual(status, LoadStatus.KEEP) self.assertEqual(status, LoadStatus.NONE)
self.assertEqual(project.removed, []) # nichts gelöscht 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 # # 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.QgsProject.instance")
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text") @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text")
def test_vorgabe_schluessel_prefills_default_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_ask_text.return_value = ("1234, 5678", True)
mock_project.return_value = _DummyProject() 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"]) flurstueck_alkis(None, vorgabe_schluessel=["1234", "5678"])
@@ -245,16 +250,16 @@ class TestFlurstueckAlkis(unittest.TestCase):
# ------------------------------------------------------------------ # # ------------------------------------------------------------------ #
# 7. vorgabe_namen erscheint im Label-Text # # 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.QgsProject.instance")
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text") @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text")
def test_vorgabe_namen_appears_in_label( 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_ask_text.return_value = ("9999", True)
mock_project.return_value = _DummyProject() 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"]) flurstueck_alkis(None, vorgabe_namen=["Musterstadt"])
@@ -264,49 +269,179 @@ class TestFlurstueckAlkis(unittest.TestCase):
self.assertIn("Musterstadt", label_arg) 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.lade_flurstuecke_per_gemarkung")
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance")
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text")
def test_bbox_embedded_in_wfs_uri(
self, mock_ask_text, mock_project, mock_wfs_layer_cls
):
"""bbox-Parameter → WFS-URI enthält 'bbox=' Substring."""
mock_ask_text.return_value = ("1234", True)
mock_project.return_value = _DummyProject()
mock_wfs_layer_cls.return_value = _DummyLayer(valid=True)
class _Rect:
def xMinimum(self): return 414000.0
def yMinimum(self): return 5657000.0
def xMaximum(self): return 415000.0
def yMaximum(self): return 5658000.0
flurstueck_alkis(None, bbox=_Rect())
uri_str = mock_wfs_layer_cls.call_args[0][0]
self.assertIn("bbox=", uri_str.lower())
# ------------------------------------------------------------------ #
# 9. Rückwärtskompatibilität: alle neuen Params optional #
# ------------------------------------------------------------------ #
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsVectorLayer")
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance") @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance")
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text") @patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text")
def test_backwards_compatible_no_new_params( 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_ask_text.return_value = ("0000", True)
mock_project.return_value = _DummyProject() 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) layer, status = flurstueck_alkis(None)
self.assertIsNotNone(layer) self.assertIsNotNone(layer)
self.assertEqual(status, LoadStatus.FIRST) 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__": if __name__ == "__main__":
unittest.main() unittest.main()
+130
View File
@@ -43,6 +43,7 @@ if "qgis" not in sys.modules:
from sn_verfahrensgebiet.functions.grenzpunkt_alkis import ( from sn_verfahrensgebiet.functions.grenzpunkt_alkis import (
grenzpunkt_alkis, grenzpunkt_alkis,
grenzpunkte_gemarkungsweise,
_extrahiere_gml_ids, _extrahiere_gml_ids,
) )
@@ -297,5 +298,134 @@ class TestGrenzpunktAlkis(unittest.TestCase):
self.assertIs(gp, gp_layer) self.assertIs(gp, gp_layer)
# ---------------------------------------------------------------------------
# Tests: grenzpunkte_gemarkungsweise – Stilanbringung (#53)
# ---------------------------------------------------------------------------
_G_PATCH_EXTENTS = "sn_verfahrensgebiet.functions.grenzpunkt_alkis._lade_gemarkung_extents"
_G_PATCH_HITS = "sn_verfahrensgebiet.functions.grenzpunkt_alkis._wfs_hits_count"
_G_PATCH_GET_GML = "sn_verfahrensgebiet.functions.grenzpunkt_alkis._wfs_get_gml"
_G_PATCH_PARSE_GML = "sn_verfahrensgebiet.functions.grenzpunkt_alkis._parse_alkis_gml_memory_layer"
_G_PATCH_ERKENNE_FELD = "sn_verfahrensgebiet.functions.grenzpunkt_alkis._erkenne_gehoert_zu_feld"
_G_PATCH_EXTRAHIERE_ID = "sn_verfahrensgebiet.functions.grenzpunkt_alkis._extrahiere_feature_gml_id"
_G_PATCH_LADE_GP_DICT = "sn_verfahrensgebiet.functions.grenzpunkt_alkis._lade_grenzpunkte_als_dict"
_G_PATCH_ERSTELLE_JOINED = "sn_verfahrensgebiet.functions.grenzpunkt_alkis._erstelle_joined_layer"
_G_PATCH_WFS_LAYER = "sn_verfahrensgebiet.functions.grenzpunkt_alkis.QgsVectorLayer"
_G_PATCH_PROJECT = "sn_verfahrensgebiet.functions.grenzpunkt_alkis.QgsProject"
_G_PATCH_PROGRESS = "sn_verfahrensgebiet.functions.grenzpunkt_alkis.create_progress_dialog"
_G_PATCH_APPLY_STIL = "sn_verfahrensgebiet.functions.grenzpunkt_alkis.apply_style"
# QgsField/QgsFeature werden innerhalb der Funktion per lokalem Import geholt
# (from sn_basis.functions import QgsField/QgsFeature) - Patch-Ziel ist daher
# die Quelle, nicht der Aufrufort.
_G_PATCH_QGSFIELD = "sn_basis.functions.QgsField"
_G_PATCH_QGSFEATURE = "sn_basis.functions.QgsFeature"
class _DummyProgress:
def set_value(self, wert): pass
def set_label(self, text): pass
def set_total(self, wert): pass
def is_canceled(self): return False
def close(self): pass
class _DummyBbox:
def xMinimum(self): return 414000.0
def yMinimum(self): return 5657000.0
def xMaximum(self): return 415000.0
def yMaximum(self): return 5658000.0
class _DummyGmlFeld:
def __init__(self, name):
self._name = name
def name(self):
return self._name
class _DummyGmlFeature:
def __init__(self, gehoert_zu_wert):
self._wert = gehoert_zu_wert
def geometry(self):
return object()
def __getitem__(self, key):
if key == "gehoertZu":
return self._wert
raise KeyError(key)
class _DummyGmlLayer:
"""Minimaler Ersatz für das Ergebnis von _parse_alkis_gml_memory_layer()."""
def __init__(self, feature):
self._feature = feature
def fields(self):
return [_DummyGmlFeld("gehoertZu")]
def getFeatures(self):
return [self._feature]
class TestGrenzpunkteGemarkungsweiseStil(unittest.TestCase):
@patch(_G_PATCH_APPLY_STIL)
@patch(_G_PATCH_QGSFEATURE)
@patch(_G_PATCH_QGSFIELD)
@patch(_G_PATCH_PROGRESS)
@patch(_G_PATCH_PROJECT)
@patch(_G_PATCH_WFS_LAYER)
@patch(_G_PATCH_ERSTELLE_JOINED)
@patch(_G_PATCH_LADE_GP_DICT)
@patch(_G_PATCH_EXTRAHIERE_ID)
@patch(_G_PATCH_ERKENNE_FELD)
@patch(_G_PATCH_PARSE_GML)
@patch(_G_PATCH_GET_GML)
@patch(_G_PATCH_HITS)
@patch(_G_PATCH_EXTENTS)
def test_stil_wird_nach_dem_laden_auf_den_ergebnis_layer_angewendet(
self,
mock_extents,
mock_hits,
mock_get_gml,
mock_parse_gml,
mock_erkenne_feld,
mock_extrahiere_id,
mock_lade_gp_dict,
mock_erstelle_joined,
mock_wfs_layer_cls,
mock_project_cls,
mock_progress,
mock_qgsfield,
mock_qgsfeature,
mock_apply_style,
):
"""Prüft, dass grenzpunkte_gemarkungsweise() nach dem Laden apply_style() mit dem neuen ALKIS-Grenzpunkt-Stil auf den fertigen Join-Layer anwendet (#53)."""
mock_extents.return_value = {"1234": _DummyBbox()}
mock_hits.return_value = 1
mock_get_gml.return_value = b"<fake/>"
mock_parse_gml.return_value = _DummyGmlLayer(_DummyGmlFeature("urn:adv:oid:GP1"))
mock_erkenne_feld.return_value = "gehoertZu"
mock_extrahiere_id.return_value = "PTA1"
mock_lade_gp_dict.return_value = ({"GP1": object()}, ["gp_feld"])
joined_layer = MagicMock()
mock_erstelle_joined.return_value = joined_layer
mock_wfs_layer_cls.return_value = MagicMock()
project = MagicMock()
mock_project_cls.instance.return_value = project
mock_progress.return_value = _DummyProgress()
ergebnis = grenzpunkte_gemarkungsweise(None, ["1234"])
self.assertIs(ergebnis, joined_layer)
project.addMapLayer.assert_called_once_with(joined_layer)
mock_apply_style.assert_called_once_with(joined_layer, "alkis_grenzpunkte.qml")
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
+33 -4
View File
@@ -186,7 +186,7 @@ class TestVerfahrensgebietAlkis(unittest.TestCase):
self.assertEqual(status, vg.LoadStatus.FIRST) self.assertEqual(status, vg.LoadStatus.FIRST)
self.assertEqual(len(feats), 1) self.assertEqual(len(feats), 1)
@patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.ask_bror_kombination") @patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.ask_bror_kombinationen")
@patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.QgsFeature") @patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.QgsFeature")
@patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.QgsGeometry.unaryUnion", return_value=_DummyGeom(empty=False)) @patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.QgsGeometry.unaryUnion", return_value=_DummyGeom(empty=False))
@patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis._check_existing_layer", return_value=(None, vg.LoadStatus.FIRST)) @patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis._check_existing_layer", return_value=(None, vg.LoadStatus.FIRST))
@@ -202,7 +202,7 @@ class TestVerfahrensgebietAlkis(unittest.TestCase):
temp_layer = _DummyTempLayer(valid=True, features=[feat1, feat2]) temp_layer = _DummyTempLayer(valid=True, features=[feat1, feat2])
memory_layer = _DummyMemoryLayer() memory_layer = _DummyMemoryLayer()
mock_qgsvectorlayer.side_effect = [temp_layer, memory_layer] mock_qgsvectorlayer.side_effect = [temp_layer, memory_layer]
mock_dialog.return_value = ("ADV-A", "VKZ12/001") mock_dialog.return_value = [("ADV-A", "VKZ12/001")]
mock_feature = mock_feature_cls.return_value mock_feature = mock_feature_cls.return_value
mock_feature.setGeometry.return_value = None mock_feature.setGeometry.return_value = None
@@ -216,7 +216,7 @@ class TestVerfahrensgebietAlkis(unittest.TestCase):
# Nur feat1 gehört zur gewählten Kombination # Nur feat1 gehört zur gewählten Kombination
self.assertEqual(feats, [feat1]) self.assertEqual(feats, [feat1])
@patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.ask_bror_kombination") @patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.ask_bror_kombinationen")
@patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis._check_existing_layer", return_value=(None, vg.LoadStatus.FIRST)) @patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis._check_existing_layer", return_value=(None, vg.LoadStatus.FIRST))
@patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.get_variable", return_value="VKZ123") @patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.get_variable", return_value="VKZ123")
@patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.QgsVectorLayer") @patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.QgsVectorLayer")
@@ -229,7 +229,7 @@ class TestVerfahrensgebietAlkis(unittest.TestCase):
feat2 = _DummyFeature(attrs={"adv_name": "ADV-B", "bezeichnung": "VKZ12/002"}) feat2 = _DummyFeature(attrs={"adv_name": "ADV-B", "bezeichnung": "VKZ12/002"})
temp_layer = _DummyTempLayer(valid=True, features=[feat1, feat2]) temp_layer = _DummyTempLayer(valid=True, features=[feat1, feat2])
mock_qgsvectorlayer.return_value = temp_layer mock_qgsvectorlayer.return_value = temp_layer
mock_dialog.return_value = None # Nutzer bricht ab mock_dialog.return_value = [] # Nutzer bricht ab
layer, status, feats = vg.verfahrensgebiet_alkis(tab_widget=None) layer, status, feats = vg.verfahrensgebiet_alkis(tab_widget=None)
@@ -238,6 +238,35 @@ class TestVerfahrensgebietAlkis(unittest.TestCase):
self.assertEqual(status, vg.LoadStatus.NONE) self.assertEqual(status, vg.LoadStatus.NONE)
self.assertEqual(feats, []) self.assertEqual(feats, [])
@patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.ask_bror_kombinationen")
@patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.QgsFeature")
@patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.QgsGeometry.unaryUnion", return_value=_DummyGeom(empty=False))
@patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis._check_existing_layer", return_value=(None, vg.LoadStatus.FIRST))
@patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.get_variable", return_value="VKZ123")
@patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.QgsVectorLayer")
@patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.QMessageBox", new=_DummyMessageBox)
def test_verfahrensgebiet_alkis_multi_kombination_mehrfachauswahl(
self, mock_qgsvectorlayer, _mock_get_var, _mock_check, _mock_union, mock_feature_cls, mock_dialog
):
"""Zwei verschiedene Kombinationen → beide ausgewählt → beide Feature-Gruppen genutzt."""
feat1 = _DummyFeature(attrs={"adv_name": "ADV-A", "bezeichnung": "VKZ12/001"})
feat2 = _DummyFeature(attrs={"adv_name": "ADV-B", "bezeichnung": "VKZ12/002"})
temp_layer = _DummyTempLayer(valid=True, features=[feat1, feat2])
memory_layer = _DummyMemoryLayer()
mock_qgsvectorlayer.side_effect = [temp_layer, memory_layer]
mock_dialog.return_value = [("ADV-A", "VKZ12/001"), ("ADV-B", "VKZ12/002")]
mock_feature = mock_feature_cls.return_value
mock_feature.setGeometry.return_value = None
mock_feature.setAttributes.return_value = None
layer, status, feats = vg.verfahrensgebiet_alkis(tab_widget=None)
mock_dialog.assert_called_once()
self.assertIs(layer, memory_layer)
self.assertEqual(status, vg.LoadStatus.FIRST)
self.assertEqual(feats, [feat1, feat2])
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
+138 -25
View File
@@ -129,7 +129,7 @@ def _make_progress_mock(canceled=False):
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
_MOD = "sn_verfahrensgebiet.functions.verfahrensgebiet_alkis_komplett" _MOD = "sn_verfahrensgebiet.functions.verfahrensgebiet_alkis_komplett"
_PATCH_VG_ALKIS = f"{_MOD}.verfahrensgebiet_alkis" _PATCH_VG_ALKIS = f"{_MOD}._suche_verfahrensgebiet_features"
_PATCH_GEM = f"{_MOD}.gemarkung_schluessel_und_namen_von_vg_geometrie" _PATCH_GEM = f"{_MOD}.gemarkung_schluessel_und_namen_von_vg_geometrie"
_PATCH_FLST = f"{_MOD}.flurstueck_alkis" _PATCH_FLST = f"{_MOD}.flurstueck_alkis"
_PATCH_SELECT = f"{_MOD}._select_by_location" _PATCH_SELECT = f"{_MOD}._select_by_location"
@@ -146,6 +146,9 @@ _PATCH_ASK_DIALOG = f"{_MOD}.ask_detailpruefung_oder_vg_laden"
_PATCH_GP_ALKIS = f"{_MOD}.lade_punktort_ta" _PATCH_GP_ALKIS = f"{_MOD}.lade_punktort_ta"
_PATCH_KNICK = f"{_MOD}.knickpunkt_pruefung" _PATCH_KNICK = f"{_MOD}.knickpunkt_pruefung"
_PATCH_EXTRAHIERE = f"{_MOD}._extrahiere_knickpunkte" _PATCH_EXTRAHIERE = f"{_MOD}._extrahiere_knickpunkte"
_PATCH_LANDKREIS_SUCHE = f"{_MOD}._suche_landkreis_kombinationen"
_PATCH_LANDKREIS_BAU = f"{_MOD}._baue_layer_aus_landkreis_auswahl"
_PATCH_ASK_LANDKREIS = f"{_MOD}.ask_landkreis_auswahl"
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -265,9 +268,10 @@ class TestVerfahrensgebietAlkisKomplett(unittest.TestCase):
@patch(_PATCH_PROGRESS) @patch(_PATCH_PROGRESS)
@patch(_PATCH_PROJECT) @patch(_PATCH_PROJECT)
@patch(_PATCH_VG_ALKIS) @patch(_PATCH_VG_ALKIS)
def test_vg_alkis_none_aborts(self, mock_vg, mock_proj, mock_prog): @patch(_PATCH_GET_VAR, return_value="VF-2024-001")
def test_vg_alkis_none_aborts(self, mock_get_var, mock_vg, mock_proj, mock_prog):
mock_proj.instance.return_value = _DummyProject() mock_proj.instance.return_value = _DummyProject()
mock_vg.return_value = (None, LoadStatus.NONE, []) mock_vg.return_value = (None, LoadStatus.NONE, {}, None)
mock_prog.return_value = _make_progress_mock() mock_prog.return_value = _make_progress_mock()
enriched, flst, bror, status = verfahrensgebiet_alkis_komplett(None) enriched, flst, bror, status = verfahrensgebiet_alkis_komplett(None)
@@ -281,10 +285,11 @@ class TestVerfahrensgebietAlkisKomplett(unittest.TestCase):
@patch(_PATCH_PROJECT) @patch(_PATCH_PROJECT)
@patch(_PATCH_GEM) @patch(_PATCH_GEM)
@patch(_PATCH_VG_ALKIS) @patch(_PATCH_VG_ALKIS)
def test_flst_none_aborts(self, mock_vg, mock_gem, mock_proj, mock_prog): @patch(_PATCH_GET_VAR, return_value="VF-2024-001")
def test_flst_none_aborts(self, mock_get_var, mock_vg, mock_gem, mock_proj, mock_prog):
mock_proj.instance.return_value = _DummyProject() mock_proj.instance.return_value = _DummyProject()
vg_layer = _make_layer(name="BauRaumOrdnungsrecht (ALKIS)") vg_layer = _make_layer(name="BauRaumOrdnungsrecht (ALKIS)")
mock_vg.return_value = (vg_layer, LoadStatus.FIRST, []) mock_vg.return_value = (vg_layer, LoadStatus.FIRST, {}, None)
mock_gem.return_value = [("1234", "Muster")] mock_gem.return_value = [("1234", "Muster")]
mock_prog.return_value = _make_progress_mock() mock_prog.return_value = _make_progress_mock()
@@ -302,13 +307,14 @@ class TestVerfahrensgebietAlkisKomplett(unittest.TestCase):
@patch(_PATCH_GEM) @patch(_PATCH_GEM)
@patch(_PATCH_FLST) @patch(_PATCH_FLST)
@patch(_PATCH_VG_ALKIS) @patch(_PATCH_VG_ALKIS)
@patch(_PATCH_GET_VAR, return_value="VF-2024-001")
def test_no_selection_returns_none_enriched( def test_no_selection_returns_none_enriched(
self, mock_vg, mock_flst, mock_gem, mock_proj, mock_select, mock_msgbox, mock_prog self, mock_get_var, mock_vg, mock_flst, mock_gem, mock_proj, mock_select, mock_msgbox, mock_prog
): ):
mock_proj.instance.return_value = _DummyProject() mock_proj.instance.return_value = _DummyProject()
vg_layer = _make_layer(name="BauRaumOrdnungsrecht (ALKIS)") vg_layer = _make_layer(name="BauRaumOrdnungsrecht (ALKIS)")
flst_layer = _make_layer(name="Flurstücke") flst_layer = _make_layer(name="Flurstücke")
mock_vg.return_value = (vg_layer, LoadStatus.FIRST, []) mock_vg.return_value = (vg_layer, LoadStatus.FIRST, {}, None)
mock_gem.return_value = [] mock_gem.return_value = []
mock_flst.return_value = (flst_layer, LoadStatus.FIRST) mock_flst.return_value = (flst_layer, LoadStatus.FIRST)
mock_select.return_value = 0 mock_select.return_value = 0
@@ -330,15 +336,16 @@ class TestVerfahrensgebietAlkisKomplett(unittest.TestCase):
@patch(_PATCH_GEM) @patch(_PATCH_GEM)
@patch(_PATCH_FLST) @patch(_PATCH_FLST)
@patch(_PATCH_VG_ALKIS) @patch(_PATCH_VG_ALKIS)
@patch(_PATCH_GET_VAR, return_value="VF-2024-001")
def test_invalid_enriched_layer_returns_none( def test_invalid_enriched_layer_returns_none(
self, mock_vg, mock_flst, mock_gem, mock_proj, mock_select, mock_erstelle, mock_msgbox, mock_prog self, mock_get_var, mock_vg, mock_flst, mock_gem, mock_proj, mock_select, mock_erstelle, mock_msgbox, mock_prog
): ):
mock_proj.instance.return_value = _DummyProject() mock_proj.instance.return_value = _DummyProject()
vg_layer = _make_layer(name="BauRaumOrdnungsrecht (ALKIS)") vg_layer = _make_layer(name="BauRaumOrdnungsrecht (ALKIS)")
flst_layer = _make_layer(name="Flurstücke") flst_layer = _make_layer(name="Flurstücke")
bad_enriched = _make_layer(valid=False) bad_enriched = _make_layer(valid=False)
mock_vg.return_value = (vg_layer, LoadStatus.FIRST, []) mock_vg.return_value = (vg_layer, LoadStatus.FIRST, {}, None)
mock_gem.return_value = [] mock_gem.return_value = []
mock_flst.return_value = (flst_layer, LoadStatus.FIRST) mock_flst.return_value = (flst_layer, LoadStatus.FIRST)
mock_select.return_value = 5 mock_select.return_value = 5
@@ -356,11 +363,12 @@ class TestVerfahrensgebietAlkisKomplett(unittest.TestCase):
@patch(_PATCH_PROGRESS) @patch(_PATCH_PROGRESS)
@patch(_PATCH_PROJECT) @patch(_PATCH_PROJECT)
@patch(_PATCH_VG_ALKIS) @patch(_PATCH_VG_ALKIS)
def test_progress_cancel_after_step1_returns_none(self, mock_vg, mock_proj, mock_prog): @patch(_PATCH_GET_VAR, return_value="VF-2024-001")
def test_progress_cancel_after_step1_returns_none(self, mock_get_var, mock_vg, mock_proj, mock_prog):
"""Abbruch nach Schritt 1 → sofortiger sauberer Rückgabestatus.""" """Abbruch nach Schritt 1 → sofortiger sauberer Rückgabestatus."""
mock_proj.instance.return_value = _DummyProject() mock_proj.instance.return_value = _DummyProject()
vg_layer = _make_layer(name="BauRaumOrdnungsrecht (ALKIS)") vg_layer = _make_layer(name="BauRaumOrdnungsrecht (ALKIS)")
mock_vg.return_value = (vg_layer, LoadStatus.FIRST, []) mock_vg.return_value = (vg_layer, LoadStatus.FIRST, {}, None)
prog = MagicMock() prog = MagicMock()
prog.is_canceled.side_effect = [False, True] # 1. Aufruf False, 2. True prog.is_canceled.side_effect = [False, True] # 1. Aufruf False, 2. True
@@ -398,7 +406,7 @@ class TestVerfahrensgebietAlkisKomplett(unittest.TestCase):
flst_layer = _make_layer(name="Flurstücke", lid="FLST1") flst_layer = _make_layer(name="Flurstücke", lid="FLST1")
enriched = _make_layer(name="Verfahrensgebiet", lid="VG_ENRICHED", valid=True) enriched = _make_layer(name="Verfahrensgebiet", lid="VG_ENRICHED", valid=True)
mock_vg.return_value = (vg_layer, LoadStatus.FIRST, []) mock_vg.return_value = (vg_layer, LoadStatus.FIRST, {}, None)
mock_gem.return_value = [("1234", "Muster")] mock_gem.return_value = [("1234", "Muster")]
mock_flst.return_value = (flst_layer, LoadStatus.FIRST) mock_flst.return_value = (flst_layer, LoadStatus.FIRST)
mock_select.return_value = 3 mock_select.return_value = 3
@@ -437,18 +445,18 @@ class TestVerfahrensgebietAlkisKomplett(unittest.TestCase):
flst_layer = _make_layer(name="Flurstücke") flst_layer = _make_layer(name="Flurstücke")
enriched = _make_layer(name="Verfahrensgebiet", valid=True) enriched = _make_layer(name="Verfahrensgebiet", valid=True)
mock_vg.return_value = (vg_layer, LoadStatus.FIRST, []) mock_vg.return_value = (vg_layer, LoadStatus.FIRST, {}, None)
mock_gem.return_value = [] mock_gem.return_value = []
mock_flst.return_value = (flst_layer, LoadStatus.FIRST) mock_flst.return_value = (flst_layer, LoadStatus.FIRST)
mock_select.return_value = 2 mock_select.return_value = 2
mock_erstelle.return_value = (enriched, 2, 0) mock_erstelle.return_value = (enriched, 2, 0)
mock_get_var.return_value = "" mock_get_var.return_value = "VF-2024-001"
mock_vergl.return_value = (1000.0, 1000.0, True) mock_vergl.return_value = (1000.0, 1000.0, True)
mock_prog.return_value = _make_progress_mock() mock_prog.return_value = _make_progress_mock()
verfahrensgebiet_alkis_komplett(None) verfahrensgebiet_alkis_komplett(None)
mock_vkz.assert_called_once_with(enriched, "") mock_vkz.assert_called_once_with(enriched, "VF-2024-001")
# ------------------------------------------------------------------ # # ------------------------------------------------------------------ #
# Flächenvergleich – Abweichung + Abbrechen # Flächenvergleich – Abweichung + Abbrechen
@@ -476,12 +484,12 @@ class TestVerfahrensgebietAlkisKomplett(unittest.TestCase):
flst_layer = _make_layer(name="Flurstücke") flst_layer = _make_layer(name="Flurstücke")
enriched = _make_layer(name="Verfahrensgebiet", valid=True) enriched = _make_layer(name="Verfahrensgebiet", valid=True)
mock_vg.return_value = (vg_layer, LoadStatus.FIRST, []) mock_vg.return_value = (vg_layer, LoadStatus.FIRST, {}, None)
mock_gem.return_value = [] mock_gem.return_value = []
mock_flst.return_value = (flst_layer, LoadStatus.FIRST) mock_flst.return_value = (flst_layer, LoadStatus.FIRST)
mock_select.return_value = 3 mock_select.return_value = 3
mock_erstelle.return_value = (enriched, 3, 0) mock_erstelle.return_value = (enriched, 3, 0)
mock_get_var.return_value = "" mock_get_var.return_value = "VF-2024-001"
mock_vergl.return_value = (1000.0, 800.0, False) mock_vergl.return_value = (1000.0, 800.0, False)
mock_ask.return_value = "abbrechen" mock_ask.return_value = "abbrechen"
mock_prog.return_value = _make_progress_mock() mock_prog.return_value = _make_progress_mock()
@@ -521,12 +529,12 @@ class TestVerfahrensgebietAlkisKomplett(unittest.TestCase):
flst_layer = _make_layer(name="Flurstücke") flst_layer = _make_layer(name="Flurstücke")
enriched = _make_layer(name="Verfahrensgebiet", valid=True) enriched = _make_layer(name="Verfahrensgebiet", valid=True)
mock_vg.return_value = (vg_layer, LoadStatus.FIRST, []) mock_vg.return_value = (vg_layer, LoadStatus.FIRST, {}, None)
mock_gem.return_value = [] mock_gem.return_value = []
mock_flst.return_value = (flst_layer, LoadStatus.FIRST) mock_flst.return_value = (flst_layer, LoadStatus.FIRST)
mock_select.return_value = 3 mock_select.return_value = 3
mock_erstelle.return_value = (enriched, 3, 0) mock_erstelle.return_value = (enriched, 3, 0)
mock_get_var.return_value = "" mock_get_var.return_value = "VF-2024-001"
mock_vergl.return_value = (1000.0, 800.0, False) mock_vergl.return_value = (1000.0, 800.0, False)
mock_ask.return_value = "als_vg_laden" mock_ask.return_value = "als_vg_laden"
mock_prog.return_value = _make_progress_mock() mock_prog.return_value = _make_progress_mock()
@@ -577,12 +585,12 @@ class TestVerfahrensgebietAlkisKomplett(unittest.TestCase):
punktort_layer = _make_layer(name="PunktortTA", lid="PTA_L") punktort_layer = _make_layer(name="PunktortTA", lid="PTA_L")
ergebnis_layer = _make_layer(name="Knickpunktprüfung", lid="KNICK_L") ergebnis_layer = _make_layer(name="Knickpunktprüfung", lid="KNICK_L")
mock_vg.return_value = (vg_layer, LoadStatus.FIRST, []) mock_vg.return_value = (vg_layer, LoadStatus.FIRST, {}, None)
mock_gem.return_value = [] mock_gem.return_value = []
mock_flst.return_value = (flst_layer, LoadStatus.FIRST) mock_flst.return_value = (flst_layer, LoadStatus.FIRST)
mock_select.return_value = 3 mock_select.return_value = 3
mock_erstelle.return_value = (enriched, 3, 0) mock_erstelle.return_value = (enriched, 3, 0)
mock_get_var.return_value = "" mock_get_var.return_value = "VF-2024-001"
mock_vergl.return_value = (1000.0, 800.0, False) mock_vergl.return_value = (1000.0, 800.0, False)
mock_ask.return_value = "detailpruefung" mock_ask.return_value = "detailpruefung"
mock_extrahiere.return_value = [MagicMock(), MagicMock(), MagicMock()] # 3 Knickpunkte mock_extrahiere.return_value = [MagicMock(), MagicMock(), MagicMock()] # 3 Knickpunkte
@@ -630,12 +638,12 @@ class TestVerfahrensgebietAlkisKomplett(unittest.TestCase):
flst_layer = _make_layer(name="Flurstücke") flst_layer = _make_layer(name="Flurstücke")
enriched = _make_layer(name="Verfahrensgebiet", valid=True) enriched = _make_layer(name="Verfahrensgebiet", valid=True)
mock_vg.return_value = (vg_layer, LoadStatus.FIRST, []) mock_vg.return_value = (vg_layer, LoadStatus.FIRST, {}, None)
mock_gem.return_value = [] mock_gem.return_value = []
mock_flst.return_value = (flst_layer, LoadStatus.FIRST) mock_flst.return_value = (flst_layer, LoadStatus.FIRST)
mock_select.return_value = 3 mock_select.return_value = 3
mock_erstelle.return_value = (enriched, 3, 0) mock_erstelle.return_value = (enriched, 3, 0)
mock_get_var.return_value = "" mock_get_var.return_value = "VF-2024-001"
mock_vergl.return_value = (1000.0, 1000.0, True) mock_vergl.return_value = (1000.0, 1000.0, True)
mock_prog.return_value = _make_progress_mock() mock_prog.return_value = _make_progress_mock()
@@ -647,15 +655,16 @@ class TestVerfahrensgebietAlkisKomplett(unittest.TestCase):
@patch(_PATCH_PROJECT) @patch(_PATCH_PROJECT)
@patch(_PATCH_GEM) @patch(_PATCH_GEM)
@patch(_PATCH_VG_ALKIS) @patch(_PATCH_VG_ALKIS)
@patch(_PATCH_GET_VAR, return_value="VF-2024-001")
def test_bror_layer_wird_bei_fehler_entfernt( def test_bror_layer_wird_bei_fehler_entfernt(
self, mock_vg, mock_gem, mock_proj, mock_prog self, mock_get_var, mock_vg, mock_gem, mock_proj, mock_prog
): ):
"""Bei Fehler (flst_alkis NONE) wird der BROR-Layer aus dem Projekt entfernt.""" """Bei Fehler (flst_alkis NONE) wird der BROR-Layer aus dem Projekt entfernt."""
proj_inst = _DummyProject() proj_inst = _DummyProject()
mock_proj.instance.return_value = proj_inst mock_proj.instance.return_value = proj_inst
vg_layer = _make_layer(name="BauRaumOrdnungsrecht (ALKIS)", lid="BROR_ERR") vg_layer = _make_layer(name="BauRaumOrdnungsrecht (ALKIS)", lid="BROR_ERR")
mock_vg.return_value = (vg_layer, LoadStatus.FIRST, []) mock_vg.return_value = (vg_layer, LoadStatus.FIRST, {}, None)
mock_gem.return_value = [] mock_gem.return_value = []
mock_prog.return_value = _make_progress_mock() mock_prog.return_value = _make_progress_mock()
@@ -664,6 +673,110 @@ class TestVerfahrensgebietAlkisKomplett(unittest.TestCase):
self.assertIn("BROR_ERR", proj_inst.removed) self.assertIn("BROR_ERR", proj_inst.removed)
# ------------------------------------------------------------------ #
# Landkreis-Fallback (keine Verfahrensnummer)
# ------------------------------------------------------------------ #
@patch(_PATCH_PROGRESS)
@patch(_PATCH_MSGBOX)
@patch(_PATCH_ASK_LANDKREIS, return_value=None)
@patch(_PATCH_GET_VAR, return_value="")
def test_kein_vkz_kein_landkreis_dialog_abgebrochen(
self, mock_get_var, mock_ask_landkreis, mock_msgbox, mock_prog
):
"""Weder Verfahrensnummer noch Landkreis gesetzt, Nutzer bricht den
Landkreis-Auswahldialog ab → stiller Abbruch, kein Layer, KEINE
Fehlermeldung (bewusster Abbruch, kein Fehlerzustand)."""
enriched, flst, bror, status = verfahrensgebiet_alkis_komplett(None)
self.assertIsNone(enriched)
self.assertIsNone(flst)
self.assertIsNone(bror)
self.assertEqual(status, LoadStatus.NONE)
mock_msgbox.critical.assert_not_called()
@patch(_PATCH_PROGRESS)
@patch(_PATCH_PROJECT)
@patch(_PATCH_LANDKREIS_BAU)
@patch(_PATCH_LANDKREIS_SUCHE)
@patch(_PATCH_ASK_LANDKREIS, return_value="22")
@patch(_PATCH_GET_VAR, return_value="")
def test_kein_vkz_kein_landkreis_dialog_liefert_landkreis(
self, mock_get_var, mock_ask_landkreis, mock_such, mock_bau, mock_proj, mock_prog
):
"""Weder Verfahrensnummer noch Landkreis gesetzt, Nutzer wählt einen
Landkreis im Dialog → Landkreis-Suchpfad wird mit dieser Kennzahl
verwendet (wie im Fall 'Landkreis aus Projekteigenschaften')."""
proj_inst = _DummyProject()
mock_proj.instance.return_value = proj_inst
vg_layer = _make_layer(name="BauRaumOrdnungsrecht (ALKIS)")
mock_such.return_value = (None, LoadStatus.FIRST, {("A", "22015"): [MagicMock()]}, "EPSG:25833")
mock_bau.return_value = (vg_layer, LoadStatus.FIRST, True) # nur_laden=True
mock_prog.return_value = _make_progress_mock()
enriched, flst, bror, status = verfahrensgebiet_alkis_komplett(None)
mock_ask_landkreis.assert_called_once()
mock_such.assert_called_once_with(None, "22")
mock_bau.assert_called_once()
self.assertIs(enriched, vg_layer)
self.assertEqual(status, LoadStatus.FIRST)
@patch(_PATCH_PROGRESS)
@patch(_PATCH_PROJECT)
@patch(_PATCH_LANDKREIS_BAU)
@patch(_PATCH_LANDKREIS_SUCHE)
def test_landkreis_modus_wird_bei_fehlender_vkz_verwendet(
self, mock_such, mock_bau, mock_proj, mock_prog
):
"""Keine Verfahrensnummer, aber Landkreis gesetzt → Landkreis-Suchpfad
statt VKZ-Pfad wird verwendet."""
def _get_var_side_effect(key, scope="project"):
return {"verfahrensnummer": "", "landkreis_kennzahl": "22"}.get(key, "")
proj_inst = _DummyProject()
mock_proj.instance.return_value = proj_inst
vg_layer = _make_layer(name="BauRaumOrdnungsrecht (ALKIS)")
mock_such.return_value = (None, LoadStatus.FIRST, {("A", "22015"): [MagicMock()]}, "EPSG:25833")
mock_bau.return_value = (vg_layer, LoadStatus.FIRST, True) # nur_laden=True
mock_prog.return_value = _make_progress_mock()
with patch(_PATCH_GET_VAR, side_effect=_get_var_side_effect):
enriched, flst, bror, status = verfahrensgebiet_alkis_komplett(None)
mock_such.assert_called_once()
mock_bau.assert_called_once()
self.assertIs(enriched, vg_layer)
self.assertIsNone(flst)
self.assertIsNone(bror)
self.assertEqual(status, LoadStatus.FIRST)
@patch(_PATCH_PROGRESS)
@patch(_PATCH_PROJECT)
@patch(_PATCH_LANDKREIS_BAU)
@patch(_PATCH_LANDKREIS_SUCHE)
def test_landkreis_modus_nur_laden_ueberspringt_dialog2(
self, mock_such, mock_bau, mock_proj, mock_prog
):
"""nur_laden=True → Funktion kehrt sofort zurück, Dialog 2 (Gemarkung
etc.) wird nicht gestartet."""
def _get_var_side_effect(key, scope="project"):
return {"verfahrensnummer": "", "landkreis_kennzahl": "22"}.get(key, "")
proj_inst = _DummyProject()
mock_proj.instance.return_value = proj_inst
vg_layer = _make_layer(name="BauRaumOrdnungsrecht (ALKIS)")
mock_such.return_value = (None, LoadStatus.FIRST, {("A", "22015"): [MagicMock()]}, "EPSG:25833")
mock_bau.return_value = (vg_layer, LoadStatus.FIRST, True)
mock_prog.return_value = _make_progress_mock()
with patch(_PATCH_GET_VAR, side_effect=_get_var_side_effect), \
patch(_PATCH_GEM) as mock_gem:
verfahrensgebiet_alkis_komplett(None)
mock_gem.assert_not_called()
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
+164
View File
@@ -81,6 +81,30 @@ class _DummyCombo:
self.layer = layer self.layer = layer
class _DummyGemFeature:
def __init__(self, schluessel, name):
self._schluessel = schluessel
self._name = name
def __getitem__(self, key):
if key == "schluessel":
return self._schluessel
if key == "ave_name":
return self._name
return None
class _DummyGemarkungenLayer:
def __init__(self, selected):
self._selected = list(selected)
def selectedFeatureCount(self):
return len(self._selected)
def selectedFeatures(self):
return list(self._selected)
class TestWorkingTab(unittest.TestCase): class TestWorkingTab(unittest.TestCase):
def _make_tab(self): 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_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_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.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 tab.setze_haken = lambda *_args, **_kwargs: None
# Pruefmanager-Mock: verarbeite() wird im Test ggf. überschrieben # Pruefmanager-Mock: verarbeite() wird im Test ggf. überschrieben
tab.pruefmanager = types.SimpleNamespace( tab.pruefmanager = types.SimpleNamespace(
@@ -175,6 +200,83 @@ class TestWorkingTab(unittest.TestCase):
mock_set_layer_id.assert_called_once_with("") 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 # handle_grenz_alkis
# ------------------------------------------------------------------ # ------------------------------------------------------------------
@@ -822,6 +924,68 @@ class TestHandleVerfShape(unittest.TestCase):
self.assertIn(False, busy_states, "Button muss am Ende wieder freigegeben werden") self.assertIn(False, busy_states, "Button muss am Ende wieder freigegeben werden")
class TestZoomImProjektKbs(unittest.TestCase):
"""Zoom auf das neue Verfahrensgebiet rechnet in das KBS der Kartenansicht um (Basis #99)."""
def _tab(self):
tab = TestWorkingTab._make_tab(TestWorkingTab())
tab.setze_haken = lambda *_: None
tab._on_verf_layer_changed = lambda *_: None
return tab
def _projekt(self, mock_project_instance):
proj = _DummyProject()
proj.mapLayersByName = lambda _n: []
proj.mapLayer = lambda _lid: None
mock_project_instance.return_value = proj
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.zoom_auf_layer")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.success")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.apply_style")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.QgsProject.instance")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.verfahrensgebiet_alkis_komplett")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.iface")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.set_verfahrensgebiet_layer_id")
def test_alkis_zoom_ueber_zoom_auf_layer(self, _set_id, mock_iface, mock_alkis, mock_projekt,
_stil, _erfolg, mock_zoom):
"""Prüft, dass nach dem Laden aus ALKIS über zoom_auf_layer auf das Verfahrensgebiet gezoomt wird und nicht mit der unumgerechneten Layer-Ausdehnung."""
tab = self._tab()
enriched = _DummyLayer(valid=True, layer_id="vg")
mock_alkis.return_value = (enriched, _DummyLayer(layer_id="flst"), None, LoadStatus.FIRST)
self._projekt(mock_projekt)
canvas = _DummyCanvas()
mock_iface.mapCanvas.return_value = canvas
tab.handle_verf_alkis()
mock_zoom.assert_called_once_with(enriched, canvas)
self.assertIsNone(canvas.extent)
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.get_variable", return_value="VF-123")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.zoom_auf_layer")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.success")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.apply_style")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.QgsProject.instance")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.verfahrensgebiet_shape_komplett")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.iface")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.set_verfahrensgebiet_layer_id")
def test_shape_zoom_ueber_zoom_auf_layer(self, _set_id, mock_iface, mock_shape, mock_projekt,
_stil, _erfolg, mock_zoom, _var):
"""Prüft, dass nach dem Laden aus einem Shape über zoom_auf_layer auf das Verfahrensgebiet gezoomt wird und nicht mit der unumgerechneten Layer-Ausdehnung."""
tab = self._tab()
tab.btn_verf_shape = _DummyButton()
enriched = _DummyLayer(valid=True, layer_id="shape_vg")
mock_shape.return_value = (enriched, _DummyLayer(layer_id="flst"), None, LoadStatus.FIRST)
self._projekt(mock_projekt)
canvas = _DummyCanvas()
mock_iface.mapCanvas.return_value = canvas
tab.handle_verf_shape()
mock_zoom.assert_called_once_with(enriched, canvas)
self.assertIsNone(canvas.extent)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
+1 -1
View File
@@ -38,7 +38,7 @@ class DockWidget(BaseDockWidget):
self._init_tabs_with_services() self._init_tabs_with_services()
def _init_tabs_with_services(self): 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: try:
self.setWidget(None) self.setWidget(None)
+90 -85
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 QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QMessageBox
from sn_basis.functions.qt_wrapper import Qt from sn_basis.functions.qt_wrapper import Qt
from sn_basis.functions import Qgis, QgsProject #QgsMessageLog 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.qgiscore_wrapper import QgsMapLayerProxyModel
from sn_basis.functions.verfahrensgebiet_manager import get_verfahrensgebiet_layer_id, set_verfahrensgebiet_layer_id from sn_basis.functions.verfahrensgebiet_manager import get_verfahrensgebiet_layer_id, set_verfahrensgebiet_layer_id
from sn_basis.functions.dialog_wrapper import show_info_dialog, create_progress_dialog, ask_yes_no from sn_basis.functions.dialog_wrapper import show_info_dialog, create_progress_dialog, ask_yes_no
@@ -15,14 +15,16 @@ from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
from qgis.utils import iface from qgis.utils import iface
from sn_verfahrensgebiet.functions.verfahrensgebiet_alkis import LoadStatus from sn_verfahrensgebiet.functions.verfahrensgebiet_alkis import LoadStatus
from sn_verfahrensgebiet.functions.flurstueck_alkis import ( from sn_verfahrensgebiet.functions.flurstueck_alkis import flurstueck_alkis
flurstueck_alkis, from sn_verfahrensgebiet.functions.verfahrensgebiet_alkis_komplett import (
lade_flurstuecke_per_gemarkung, verfahrensgebiet_alkis_komplett,
ALKIS_SF_URL, BROR_LAYER_NAME,
FLST_LAYER_NAME,
) )
from sn_verfahrensgebiet.functions.verfahrensgebiet_alkis_komplett import verfahrensgebiet_alkis_komplett
from sn_verfahrensgebiet.functions.verfahrensgebiet_shape_komplett import verfahrensgebiet_shape_komplett from sn_verfahrensgebiet.functions.verfahrensgebiet_shape_komplett import verfahrensgebiet_shape_komplett
from sn_verfahrensgebiet.functions.beteiligung_pruefung import (
beteiligung_pruefen,
BeteiligungPruefungFehler,
)
from sn_verfahrensgebiet.functions.grenzpunkt_alkis import grenzpunkte_gemarkungsweise, JOINED_LAYER_NAME from sn_verfahrensgebiet.functions.grenzpunkt_alkis import grenzpunkte_gemarkungsweise, JOINED_LAYER_NAME
from sn_verfahrensgebiet.functions.gemarkung_alkis import ( from sn_verfahrensgebiet.functions.gemarkung_alkis import (
gemarkung_schluessel_und_namen_von_vg_geometrie, gemarkung_schluessel_und_namen_von_vg_geometrie,
@@ -72,6 +74,15 @@ class WorkingTab(QWidget):
row_verf_buttons.addWidget(self.btn_verf_shape) row_verf_buttons.addWidget(self.btn_verf_shape)
layout.addLayout(row_verf_buttons) layout.addLayout(row_verf_buttons)
row_beteiligung_buttons = QHBoxLayout()
self.btn_beteiligung_pruefen = QPushButton("Beteiligung prüfen")
self.btn_beteiligung_pruefen.setToolTip(
"CSV-Export aus dem BI-Server verwenden. benötigte Spalten: "
"FLA_Verfahren-VEF_VKZF, FLA-Flurstuecke-Flurstueck, FLA_Beteiligung"
)
row_beteiligung_buttons.addWidget(self.btn_beteiligung_pruefen)
layout.addLayout(row_beteiligung_buttons)
# Abschnitt 1b: Verfahrensgebiet-Layer auswählen # Abschnitt 1b: Verfahrensgebiet-Layer auswählen
row_layer_label = QHBoxLayout() row_layer_label = QHBoxLayout()
lbl_layer = QLabel("Oder: Verfahrensgebiet-Layer auswählen") lbl_layer = QLabel("Oder: Verfahrensgebiet-Layer auswählen")
@@ -171,6 +182,7 @@ class WorkingTab(QWidget):
self.btn_verf_aus_liste.clicked.connect(self.handle_verf_aus_liste) self.btn_verf_aus_liste.clicked.connect(self.handle_verf_aus_liste)
self.btn_verf_shape.clicked.connect(self.handle_verf_shape) self.btn_verf_shape.clicked.connect(self.handle_verf_shape)
# self.btn_flurst_shape.clicked.connect(self.handle_flurst_shape) # self.btn_flurst_shape.clicked.connect(self.handle_flurst_shape)
self.btn_beteiligung_pruefen.clicked.connect(self.handle_beteiligung_pruefen)
# Beim Init: aktuelle Verfahrensgebiet-Layer-ID laden und im Dropdown anzeigen # Beim Init: aktuelle Verfahrensgebiet-Layer-ID laden und im Dropdown anzeigen
self._restore_verf_layer_selection() self._restore_verf_layer_selection()
@@ -201,16 +213,20 @@ class WorkingTab(QWidget):
apply_style(flst_layer, "GIS_Flst_Beschriftung_ALKIS_NAS.qml") apply_style(flst_layer, "GIS_Flst_Beschriftung_ALKIS_NAS.qml")
self.setze_haken(self.haken_flurst, True) self.setze_haken(self.haken_flurst, True)
# BROR-Layer: QML-Stil anwenden # BROR-Layer bekommt seinen Style bereits direkt beim Laden in
if bror_layer and bror_layer.isValid(): # verfahrensgebiet_alkis_komplett() (auch im "Nur laden"-Fall).
apply_style(bror_layer, "BauRaumOderBodenordnungsrecht.qml")
# Bei "Nur Objekte laden" (kein weiterer Ablauf) ist enriched_layer
# tatsächlich nur der rohe BROR-Layer (erkennbar am Namen) – hier
# fand keine Flurstücksverschneidung statt, es gibt also keinen
# echten VG-Layer: VG-Style/-Kombobox/-Haken/-Meldung überspringen.
if enriched_layer.name() != BROR_LAYER_NAME:
# Enriched VG-Layer ins Projekt (ggf. bereits registriert, z.B. bei "als VG laden") # Enriched VG-Layer ins Projekt (ggf. bereits registriert, z.B. bei "als VG laden")
if not project.mapLayer(enriched_layer.id()): if not project.mapLayer(enriched_layer.id()):
project.addMapLayer(enriched_layer) project.addMapLayer(enriched_layer)
apply_style(enriched_layer, "verfahrensgebiet.qml") apply_style(enriched_layer, "verfahrensgebiet.qml")
iface.mapCanvas().setExtent(enriched_layer.extent()) # Ausdehnung ins KBS der Kartenansicht umrechnen (Basis #99)
iface.mapCanvas().refresh() zoom_auf_layer(enriched_layer, iface.mapCanvas())
self.setze_haken(self.haken_verf, True) self.setze_haken(self.haken_verf, True)
self.verf_layer_combo.setLayer(enriched_layer) self.verf_layer_combo.setLayer(enriched_layer)
@@ -230,15 +246,15 @@ class WorkingTab(QWidget):
Styles angewendet und die UI-Elemente aktualisiert. Styles angewendet und die UI-Elemente aktualisiert.
""" """
# Vorprüfung: Verfahrensnummer muss gesetzt sein # Vorprüfung: Verfahrensnummer muss gesetzt sein
vkz = get_variable("verfahrensnummer") or "" # vkz = get_variable("verfahrensnummer") or ""
if not str(vkz).strip(): # if not str(vkz).strip():
if not ask_yes_no( # if not ask_yes_no(
"Verfahrensnummer nicht gesetzt", # "Verfahrensnummer nicht gesetzt",
"Verfahrensnummer nicht gesetzt. Trotzdem fortfahren?", # "Verfahrensnummer nicht gesetzt. Trotzdem fortfahren?",
default=False, # default=False,
parent=self, # parent=self,
): # ):
return # return
self._set_busy(self.btn_verf_shape, True, "Aus Shape laden") self._set_busy(self.btn_verf_shape, True, "Aus Shape laden")
try: try:
@@ -266,8 +282,8 @@ class WorkingTab(QWidget):
if not project.mapLayer(enriched_layer.id()): if not project.mapLayer(enriched_layer.id()):
project.addMapLayer(enriched_layer) project.addMapLayer(enriched_layer)
apply_style(enriched_layer, "verfahrensgebiet.qml") apply_style(enriched_layer, "verfahrensgebiet.qml")
iface.mapCanvas().setExtent(enriched_layer.extent()) # Ausdehnung ins KBS der Kartenansicht umrechnen (Basis #99)
iface.mapCanvas().refresh() zoom_auf_layer(enriched_layer, iface.mapCanvas())
self.setze_haken(self.haken_verf, True) self.setze_haken(self.haken_verf, True)
self.verf_layer_combo.setLayer(enriched_layer) self.verf_layer_combo.setLayer(enriched_layer)
@@ -278,6 +294,42 @@ class WorkingTab(QWidget):
finally: finally:
self._set_busy(self.btn_verf_shape, False, "Aus Shape laden") self._set_busy(self.btn_verf_shape, False, "Aus Shape laden")
def handle_beteiligung_pruefen(self) -> None:
"""Gleicht eine BI-Server-CSV-Beteiligungsliste mit dem Verfahrensgebiet ab.
Delegiert an :func:`~sn_verfahrensgebiet.functions.beteiligung_pruefung.beteiligung_pruefen`.
Ist noch kein (verwertbares) Verfahrensgebiet geladen, fordert die
Funktion den Nutzer per Meldung auf, es zunächst über "Aus ALKIS
laden" zu laden – es gibt kein automatisches Laden im Hintergrund.
Sonst werden ein oder mehrere Ergebnis-Layer ins Projekt eingefügt,
gestylt und eine Statistik als Messagebox angezeigt.
"""
self._set_busy(self.btn_beteiligung_pruefen, True, "Beteiligung prüfen")
try:
try:
ergebnis = beteiligung_pruefen(self)
except BeteiligungPruefungFehler as exc:
QMessageBox.critical(self, "Beteiligung prüfen", str(exc))
return
if ergebnis is None:
return # Nutzerabbruch des Dateidialogs
project = QgsProject.instance()
for layer, style_name in ergebnis.layers:
if layer is None or not layer.isValid():
continue
if not project.mapLayer(layer.id()):
project.addMapLayer(layer)
if style_name:
apply_style(layer, style_name)
iface.mapCanvas().refresh()
QMessageBox.information(self, "Beteiligung prüfen", ergebnis.statistik_text)
finally:
self._set_busy(self.btn_beteiligung_pruefen, False, "Beteiligung prüfen")
def _set_busy(self, button: QPushButton, busy: bool, original_text: str = "Aus ALKIS laden") -> None: def _set_busy(self, button: QPushButton, busy: bool, original_text: str = "Aus ALKIS laden") -> None:
"""Setzt den Busy-State eines Buttons. """Setzt den Busy-State eines Buttons.
@@ -335,17 +387,10 @@ class WorkingTab(QWidget):
# Flurstücke-Handler # Flurstücke-Handler
# ------------------------------------------------------------------ # ------------------------------------------------------------------
def handle_flurst_alkis(self) -> None: def _uebernimm_flurstuecke_ergebnis(self, layer, status) -> None:
"""Lädt Flurstücke aus ALKIS-WFS anhand von Gemarkungsschlüsseln.""" """Übernimmt das Ergebnis von flurstueck_alkis() ins Projekt (FIRST/RELOAD/APPEND/KEEP)."""
self._set_busy(self.btn_flurst_alkis, True, "Aus ALKIS laden")
try:
layer, status = flurstueck_alkis(self)
if not layer or not layer.isValid():
return
if status in (LoadStatus.FIRST, LoadStatus.RELOAD): if status in (LoadStatus.FIRST, LoadStatus.RELOAD):
QgsProject.instance().addMapLayer(layer) QgsProject.instance().addMapLayer(layer)
# iface.mapCanvas().setExtent(layer.extent()) # Kartenausschnitt bleibt unverändert
apply_style(layer, "GIS_Flst_Beschriftung_ALKIS_NAS.qml") apply_style(layer, "GIS_Flst_Beschriftung_ALKIS_NAS.qml")
iface.mapCanvas().refresh() iface.mapCanvas().refresh()
self.setze_haken(self.haken_flurst, True) self.setze_haken(self.haken_flurst, True)
@@ -361,6 +406,14 @@ class WorkingTab(QWidget):
iface.mapCanvas().refresh() iface.mapCanvas().refresh()
success("Flurstücke", "Features wurden an den vorhandenen Layer angehängt.", duration=5) success("Flurstücke", "Features wurden an den vorhandenen Layer angehängt.", duration=5)
def handle_flurst_alkis(self) -> None:
"""Lädt Flurstücke aus ALKIS-WFS anhand von Gemarkungsschlüsseln."""
self._set_busy(self.btn_flurst_alkis, True, "Aus ALKIS laden")
try:
layer, status = flurstueck_alkis(self)
if not layer or not layer.isValid():
return
self._uebernimm_flurstuecke_ergebnis(layer, status)
finally: finally:
self._set_busy(self.btn_flurst_alkis, False, "Aus ALKIS laden") self._set_busy(self.btn_flurst_alkis, False, "Aus ALKIS laden")
@@ -434,68 +487,20 @@ class WorkingTab(QWidget):
) )
return return
# Prüfen ob bereits ein Flurstücke-Layer vorhanden → 3-Knopf-Dialog # Flurstücke laden – Schlüssel bereits bekannt, aber bei bestehendem Layer
existing_flst = project.mapLayersByName(FLST_LAYER_NAME) # trotzdem interaktiv nachfragen (Überschreiben/Anhängen/Abbrechen, inkl. Dedup)
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)
layer, status = flurstueck_alkis( layer, status = flurstueck_alkis(
self, self,
vorgabe_schluessel=vorgabe_schluessel, vorgabe_schluessel=vorgabe_schluessel,
vorgabe_namen=vorgabe_namen, vorgabe_namen=vorgabe_namen,
ohne_dialog=True, ohne_schluessel_abfrage=True,
) )
if not layer or not layer.isValid(): if not layer or not layer.isValid():
error( # flurstueck_alkis() hat bei Abbruch/Fehler bereits selbst eine Meldung gezeigt.
"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 return
if status in (LoadStatus.FIRST, LoadStatus.RELOAD): self._uebernimm_flurstuecke_ergebnis(layer, status)
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)
finally: finally:
self._set_busy(self.btn_flurst_aus_gemarkungen, False, "aus gewählten Gemarkungen laden") self._set_busy(self.btn_flurst_aus_gemarkungen, False, "aus gewählten Gemarkungen laden")