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
14 changed files with 1627 additions and 219 deletions
+5
View File
@@ -1,3 +1,8 @@
- Bugfix: Nach dem Laden des Verfahrensgebiets (ALKIS oder Shape) sprang die Karte an die falsche Stelle, wenn ein anderes Projekt-KBS als EPSG:25833 eingestellt war. Der Zoom rechnet die Ausdehnung jetzt in das KBS der Kartenansicht um (`zoom_auf_layer` aus sn_basis, setzt sn_basis mit Basis #99 voraus) (Basis #99)
- Neu: Grenzpunkte aus ALKIS werden nach dem Laden im ALKIS-Stil dargestellt (styles/alkis_grenzpunkte.qml), statt ungestylt zu bleiben (#53)
- Intern: Tests von TestFlurstueckAlkis an die seit längerem geänderte Ladelogik angeglichen (Mocks zeigten noch auf die alte Ein-Query-/Zwei-Knopf-Variante), bbox-Dokumentation in flurstueck_alkis.py korrigiert (bbox wird nicht mehr ausgewertet) – keine Verhaltensänderung (Test-Audit #16)
- Intern: Der Button "aus gewählten Gemarkungen laden" nutzt jetzt dieselbe Ladelogik wie "Flurstücke laden" (flurstueck_alkis) statt einer eigenen, parallelen Implementierung – konsolidiert drei bisher getrennte Code-Pfade zum Flurstücke-Laden auf einen (#47)
- Bugfix: Flurstücke aus ALKIS an bestehenden Layer anhängen überspringt jetzt bereits vorhandene Flurstücke (Abgleich über flurstueckskennzeichen), statt sie doppelt zu laden – betrifft sowohl den Dialog in "Flurstücke laden" als auch den Button "aus gewählten Gemarkungen laden" (#28)
- Fortschrittsdialog beim Laden aus ALKIS schließt jetzt zuverlässig bei jedem Abbruch (Bugfix) - 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 - 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) - Mehrfachauswahl bei mehreren BauRaumOderBodenordnungsrecht-Treffern zur Verfahrensnummer (gemeinsames Laden/Verschmelzen mehrerer Objekte)
+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()
+11 -12
View File
@@ -261,12 +261,12 @@ def _baue_layer_aus_landkreis_auswahl(tab_widget, kombinationen: dict, crs: str,
"""Auswahl + Bau für den Landkreis-Fallback. """Auswahl + Bau für den Landkreis-Fallback.
Zeigt :func:`~sn_verfahrensgebiet.functions.verfahrensgebiet_dialoge.ask_landkreis_objekte` Zeigt :func:`~sn_verfahrensgebiet.functions.verfahrensgebiet_dialoge.ask_landkreis_objekte`
(Mehrfachauswahl inkl. "Alle auswählen" und "Nur laden"-Checkbox). Die (Mehrfachauswahl inkl. "Alle auswählen"). Die ausgewählten Kombinationen
ausgewählten Kombinationen werden anhand der ersten 5 Zeichen von werden anhand der ersten 5 Zeichen von ``bezeichnung`` (= die eigentliche
``bezeichnung`` (= die eigentliche VKZ) gruppiert; je Gruppe wird EIN VKZ) gruppiert; je Gruppe wird EIN Feature erzeugt (Geometrien der
Feature erzeugt (Geometrien der Gruppe verschmolzen). Damit entsteht bei Gruppe verschmolzen). Damit entsteht bei Auswahl mehrerer/aller Objekte
Auswahl mehrerer/aller Objekte eines Landkreises ein Layer mit mehreren eines Landkreises ein Layer mit mehreren Features – einem je
Features – einem je tatsächlichem Verfahren, nicht ein Gesamt-Blob. tatsächlichem Verfahren, nicht ein Gesamt-Blob.
Args: Args:
tab_widget: Parent-Widget für Dialoge und Meldungen tab_widget: Parent-Widget für Dialoge und Meldungen
@@ -275,13 +275,12 @@ def _baue_layer_aus_landkreis_auswahl(tab_widget, kombinationen: dict, crs: str,
status: Lade-Status aus Phase 1 (FIRST/RELOAD), wird durchgereicht status: Lade-Status aus Phase 1 (FIRST/RELOAD), wird durchgereicht
Returns: Returns:
Tuple[Optional[QgsVectorLayer], LoadStatus, bool]: Tuple[Optional[QgsVectorLayer], LoadStatus]: ``(layer, status)``.
``(layer, status, nur_laden)``.
""" """
key_list = sorted(kombinationen.keys()) key_list = sorted(kombinationen.keys())
selected_keys, nur_laden = ask_landkreis_objekte(tab_widget, key_list) selected_keys = ask_landkreis_objekte(tab_widget, key_list)
if not selected_keys: if not selected_keys:
return None, LoadStatus.NONE, False return None, LoadStatus.NONE
# Nach VKZ (erste 5 Zeichen der bezeichnung) gruppieren # Nach VKZ (erste 5 Zeichen der bezeichnung) gruppieren
vkz_gruppen: dict[str, list] = {} vkz_gruppen: dict[str, list] = {}
@@ -322,12 +321,12 @@ def _baue_layer_aus_landkreis_auswahl(tab_widget, kombinationen: dict, crs: str,
if not neue_features: if not neue_features:
QMessageBox.critical(tab_widget, "Fehler", "Keine Geometrien zum Verschmelzen gefunden.") QMessageBox.critical(tab_widget, "Fehler", "Keine Geometrien zum Verschmelzen gefunden.")
return None, LoadStatus.NONE, False return None, LoadStatus.NONE
pr.addFeatures(neue_features) pr.addFeatures(neue_features)
memory_layer.updateExtents() memory_layer.updateExtents()
return memory_layer, status, nur_laden return memory_layer, status
def verfahrensgebiet_alkis(tab_widget, skip_existing_check: bool = False): def verfahrensgebiet_alkis(tab_widget, skip_existing_check: bool = False):
+21 -9
View File
@@ -51,6 +51,7 @@ from sn_basis.functions.dialog_wrapper import (
create_progress_dialog, 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.landkreise import LANDKREISE, ALLE_LANDKREISE
from sn_basis.functions.ly_style_wrapper import apply_style from sn_basis.functions.ly_style_wrapper import apply_style
@@ -273,9 +274,13 @@ def verfahrensgebiet_alkis_komplett(
durchsuchen) und wird danach immer geschlossen – auch wenn anschließend durchsuchen) und wird danach immer geschlossen – auch wenn anschließend
der BROR-Auswahldialog erscheint (kein Fortschrittsdialog läuft der BROR-Auswahldialog erscheint (kein Fortschrittsdialog läuft
währenddessen parallel zum Auswahldialog). währenddessen parallel zum Auswahldialog).
- **STOP** (Zwischenabfrage "Weiter?"): erscheint nach dem Laden/Übernehmen
des BROR-Objekts, bevor Dialog 2 startet. Bei "Nein" bricht der Ablauf
ab, das BROR-Objekt bleibt aber im Projekt (ersetzt die frühere
"Nur Objekte laden"-Option des Landkreis-Auswahldialogs).
- **Dialog 2** (eigentlicher Workflow, 6 Schritte): startet erst, nachdem - **Dialog 2** (eigentlicher Workflow, 6 Schritte): startet erst, nachdem
das Verfahrensgebiet feststeht (direkt nach Dialog 1 bei eindeutigem das Verfahrensgebiet feststeht und die Stop-Abfrage mit "Ja" bestätigt
Treffer/KEEP, oder erst nach Bestätigung des Auswahldialogs mit OK). wurde.
Schrittfolge in Dialog 2: Schrittfolge in Dialog 2:
@@ -351,8 +356,6 @@ def verfahrensgebiet_alkis_komplett(
# Dialog 1 schließt hier immer – egal ob eine Auswahl folgt oder nicht. # Dialog 1 schließt hier immer – egal ob eine Auswahl folgt oder nicht.
progress_suche.close() progress_suche.close()
nur_laden = False
try: try:
if vorab_layer is not None or not kombinationen: if vorab_layer is not None or not kombinationen:
# KEEP oder Fehler/Abbruch bereits in Phase 1 entschieden # KEEP oder Fehler/Abbruch bereits in Phase 1 entschieden
@@ -364,7 +367,7 @@ def verfahrensgebiet_alkis_komplett(
# Objekt(e) des Landkreises auswählen – kein eigener Fortschritts- # Objekt(e) des Landkreises auswählen – kein eigener Fortschritts-
# dialog währenddessen, nur der Auswahldialog selbst. # dialog währenddessen, nur der Auswahldialog selbst.
# ------------------------------------------------------------------ # ------------------------------------------------------------------
alkis_vg_layer, vg_status, nur_laden = _baue_layer_aus_landkreis_auswahl( alkis_vg_layer, vg_status = _baue_layer_aus_landkreis_auswahl(
tab_widget, kombinationen, crs, vg_status 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:
@@ -387,12 +390,21 @@ def verfahrensgebiet_alkis_komplett(
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 # Style direkt nach dem Laden anwenden – unabhängig vom weiteren
# Ablauf, damit auch "Nur Objekte laden" den Style erhält. # Ablauf, damit auch bei "Nein" auf die Stop-Abfrage der Style
# gesetzt ist.
apply_style(alkis_vg_layer, BROR_STYLE_QML) apply_style(alkis_vg_layer, BROR_STYLE_QML)
if nur_laden: # ------------------------------------------------------------------
# Nutzer hat "Nur laden" angehakt – kein Dialog 2, kein weiterer # STOP: Zwischenabfrage nach dem Laden des BROR-Objekts, bevor mit
# Workflow (Gemarkung/Flurstücke/Vergleich). # Gemarkung/Flurstücke/Flächenvergleich fortgefahren wird (ersetzt
# die frühere "Nur Objekte laden"-Option).
# ------------------------------------------------------------------
if not ask_yes_no(
"Verfahrensgebiet laden",
f"Layer '{alkis_vg_layer.name()}' wurde geladen. Weiter?",
default=True,
parent=tab_widget,
):
abgeschlossen = True abgeschlossen = True
return alkis_vg_layer, None, None, vg_status return alkis_vg_layer, None, None, vg_status
+10 -13
View File
@@ -101,15 +101,15 @@ def ask_bror_kombinationen(
def ask_landkreis_objekte( def ask_landkreis_objekte(
parent: Any, parent: Any,
kombinationen: list[tuple[str, str]], kombinationen: list[tuple[str, str]],
) -> tuple[list[tuple[str, str]], bool]: ) -> list[tuple[str, str]]:
"""Zeigt Dialog zur Auswahl eines, mehrerer oder aller BROR-Objekte eines """Zeigt Dialog zur Auswahl eines, mehrerer oder aller BROR-Objekte eines
Landkreises (Fallback, wenn keine Verfahrensnummer gesetzt ist). Landkreises (Fallback, wenn keine Verfahrensnummer gesetzt ist).
Wie :func:`ask_bror_kombinationen`, zusätzlich mit einer "Alle Wie :func:`ask_bror_kombinationen`, zusätzlich mit einer "Alle
auswählen"-Checkbox (da hier potenziell deutlich mehr Treffer als bei der auswählen"-Checkbox (da hier potenziell deutlich mehr Treffer als bei der
VKZ-Suche auftreten) und einer "Nur laden"-Checkbox, mit der gesteuert VKZ-Suche auftreten). Ob nach dem Laden der weitere Workflow
wird, ob nach dem Laden der weitere Workflow (Gemarkung/Flurstücke/ (Gemarkung/Flurstücke/Flächenvergleich) fortgesetzt wird, klärt eine
Flächenvergleich) fortgesetzt werden soll. separate Stop-Abfrage nach dem Laden – nicht mehr dieser Dialog.
Parameters Parameters
---------- ----------
@@ -120,16 +120,16 @@ def ask_landkreis_objekte(
Returns Returns
------- -------
tuple[list[tuple[str, str]], bool] list[tuple[str, str]]
``(ausgewaehlte_kombinationen, nur_laden)``. ``([], False)`` bei Die gewählten ``(adv_name, bezeichnung)``-Tupel. Leere Liste bei
Abbruch oder wenn nichts ausgewählt wurde. Abbruch oder wenn nichts ausgewählt wurde.
""" """
if not kombinationen: if not kombinationen:
return [], False return []
if QT_VERSION == 0: # Mock-Modus if QT_VERSION == 0: # Mock-Modus
print(f"Mock-Modus: ask_landkreis_objekte → {kombinationen[:1]}") print(f"Mock-Modus: ask_landkreis_objekte → {kombinationen[:1]}")
return kombinationen[:1], False return kombinationen[:1]
dialog = QDialog(parent) dialog = QDialog(parent)
dialog.setWindowTitle("Verfahrensgebiete aus Landkreis laden") dialog.setWindowTitle("Verfahrensgebiete aus Landkreis laden")
@@ -204,9 +204,6 @@ def ask_landkreis_objekte(
mehrfachauswahl_hinweis = QLabel("<small>Mehrfachauswahl mit Strg + Umschalt</small>") mehrfachauswahl_hinweis = QLabel("<small>Mehrfachauswahl mit Strg + Umschalt</small>")
layout.addWidget(mehrfachauswahl_hinweis) layout.addWidget(mehrfachauswahl_hinweis)
nur_laden_checkbox = QCheckBox("Nur Objekte laden (ohne weitere Prüfungen)")
layout.addWidget(nur_laden_checkbox)
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel) buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
buttons.accepted.connect(dialog.accept) buttons.accepted.connect(dialog.accept)
buttons.rejected.connect(dialog.reject) buttons.rejected.connect(dialog.reject)
@@ -235,11 +232,11 @@ def ask_landkreis_objekte(
dialog.resize(breite, dialog.sizeHint().height()) dialog.resize(breite, dialog.sizeHint().height())
if exec_dialog(dialog) != DialogAccepted: if exec_dialog(dialog) != DialogAccepted:
return [], False return []
ausgewaehlte_zeilen = sorted(set(item.row() for item in table.selectedItems())) 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] ausgewaehlt = [(table.item(row, 1).text(), table.item(row, 0).text()) for row in ausgewaehlte_zeilen]
return ausgewaehlt, nur_laden_checkbox.isChecked() return ausgewaehlt
def ask_landkreis_auswahl( def ask_landkreis_auswahl(
+57 -1
View File
@@ -42,6 +42,7 @@ 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 shutil
import tempfile import tempfile
import zipfile import zipfile
@@ -106,6 +107,57 @@ _GEBIETSTYP_LAYER_NAMEN = {
# 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]]: def _ermittle_gebietstyp_aus_fbschl(raw_layer) -> Tuple[Optional[str], Optional[str]]:
"""Prüft das Feld ``FBSchl`` und ermittelt den Gebietstyp. """Prüft das Feld ``FBSchl`` und ermittelt den Gebietstyp.
@@ -113,6 +165,10 @@ def _ermittle_gebietstyp_aus_fbschl(raw_layer) -> Tuple[Optional[str], Optional[
zurück, damit der Aufrufer Fehler mehrerer Dateien sammeln und am Ende zurück, damit der Aufrufer Fehler mehrerer Dateien sammeln und am Ende
gemeinsam anzeigen kann. 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 Parameters
---------- ----------
raw_layer : raw_layer :
@@ -127,7 +183,7 @@ def _ermittle_gebietstyp_aus_fbschl(raw_layer) -> Tuple[Optional[str], Optional[
``fehlertext`` beschreibt die Ursache (fehlendes Feld oder ``fehlertext`` beschreibt die Ursache (fehlendes Feld oder
unbekannter Wert). unbekannter Wert).
""" """
idx = raw_layer.fields().indexFromName(FBSCHL_FELD) idx = _finde_fbschl_index(raw_layer)
if idx < 0: if idx < 0:
return None, f"Feld '{FBSCHL_FELD}' fehlt (Shape entspricht nicht dem erwarteten Inhalt)." return None, f"Feld '{FBSCHL_FELD}' fehlt (Shape entspricht nicht dem erwarteten Inhalt)."
+913
View File
@@ -0,0 +1,913 @@
<!DOCTYPE qgis PUBLIC 'http://mrcc.com/qgis.dtd' 'SYSTEM'>
<qgis autoRefreshMode="Disabled" autoRefreshTime="0" hasScaleBasedVisibilityFlag="0" labelsEnabled="0" layerType="Vector" maxScale="0" minScale="100000000" readOnly="0" simplifyAlgorithm="0" simplifyDrawingHints="0" simplifyDrawingTol="1" simplifyLocal="1" simplifyMaxScale="1" styleCategories="AllStyleCategories" symbologyReferenceScale="-1" version="4.2.0-Belém do Pará">
<flags>
<Identifiable>1</Identifiable>
<Removable>1</Removable>
<Searchable>1</Searchable>
<Private>0</Private>
</flags>
<temporal accumulate="0" durationField="" durationUnit="min" enabled="0" endExpression="" endField="" fixedDuration="0" limitMode="0" mode="0" startExpression="" startField="">
<fixedRange>
<start></start>
<end></end>
</fixedRange>
</temporal>
<elevation binding="Centroid" clamping="Terrain" customToleranceEnabled="0" extrusion="0" extrusionEnabled="0" respectLayerSymbol="1" showMarkerSymbolInSurfacePlots="0" symbology="Line" type="IndividualFeatures" zoffset="0" zscale="1">
<data-defined-properties>
<Option type="Map">
<Option name="name" type="QString" value=""/>
<Option name="properties"/>
<Option name="type" type="QString" value="collection"/>
</Option>
</data-defined-properties>
<profileLineSymbol>
<symbol alpha="1" clip_to_extent="1" force_rhr="0" frame_rate="10" is_animated="0" name="" type="line">
<data_defined_properties>
<Option type="Map">
<Option name="name" type="QString" value=""/>
<Option name="properties"/>
<Option name="type" type="QString" value="collection"/>
</Option>
</data_defined_properties>
<layer class="SimpleLine" enabled="1" id="{524b77c5-d547-434b-918a-eaa8d67032a3}" locked="0" pass="0">
<Option type="Map">
<Option name="align_dash_pattern" type="QString" value="0"/>
<Option name="capstyle" type="QString" value="square"/>
<Option name="customdash" type="QString" value="5;2"/>
<Option name="customdash_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
<Option name="customdash_unit" type="QString" value="MM"/>
<Option name="dash_pattern_offset" type="QString" value="0"/>
<Option name="dash_pattern_offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
<Option name="dash_pattern_offset_unit" type="QString" value="MM"/>
<Option name="draw_inside_polygon" type="QString" value="0"/>
<Option name="joinstyle" type="QString" value="bevel"/>
<Option name="line_color" type="QString" value="152,125,183,255,rgb:0.5960785,0.4901961,0.7176471,1"/>
<Option name="line_style" type="QString" value="solid"/>
<Option name="line_width" type="QString" value="0.6"/>
<Option name="line_width_unit" type="QString" value="MM"/>
<Option name="offset" type="QString" value="0"/>
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
<Option name="offset_unit" type="QString" value="MM"/>
<Option name="ring_filter" type="QString" value="0"/>
<Option name="trim_distance_end" type="QString" value="0"/>
<Option name="trim_distance_end_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
<Option name="trim_distance_end_unit" type="QString" value="MM"/>
<Option name="trim_distance_start" type="QString" value="0"/>
<Option name="trim_distance_start_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
<Option name="trim_distance_start_unit" type="QString" value="MM"/>
<Option name="tweak_dash_pattern_on_corners" type="QString" value="0"/>
<Option name="use_custom_dash" type="QString" value="0"/>
<Option name="width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
</Option>
<data_defined_properties>
<Option type="Map">
<Option name="name" type="QString" value=""/>
<Option name="properties"/>
<Option name="type" type="QString" value="collection"/>
</Option>
</data_defined_properties>
</layer>
</symbol>
</profileLineSymbol>
<profileFillSymbol>
<symbol alpha="1" clip_to_extent="1" force_rhr="0" frame_rate="10" is_animated="0" name="" type="fill">
<data_defined_properties>
<Option type="Map">
<Option name="name" type="QString" value=""/>
<Option name="properties"/>
<Option name="type" type="QString" value="collection"/>
</Option>
</data_defined_properties>
<layer class="SimpleFill" enabled="1" id="{a950bd48-1020-4bd8-ba91-53a188c9d8c0}" locked="0" pass="0">
<Option type="Map">
<Option name="border_width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
<Option name="color" type="QString" value="152,125,183,255,rgb:0.5960785,0.4901961,0.7176471,1"/>
<Option name="joinstyle" type="QString" value="bevel"/>
<Option name="offset" type="QString" value="0,0"/>
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
<Option name="offset_unit" type="QString" value="MM"/>
<Option name="outline_color" type="QString" value="109,89,131,255,rgb:0.4257572,0.3501335,0.5125963,1"/>
<Option name="outline_style" type="QString" value="solid"/>
<Option name="outline_width" type="QString" value="0.2"/>
<Option name="outline_width_unit" type="QString" value="MM"/>
<Option name="style" type="QString" value="solid"/>
</Option>
<data_defined_properties>
<Option type="Map">
<Option name="name" type="QString" value=""/>
<Option name="properties"/>
<Option name="type" type="QString" value="collection"/>
</Option>
</data_defined_properties>
</layer>
</symbol>
</profileFillSymbol>
<profileMarkerSymbol>
<symbol alpha="1" clip_to_extent="1" force_rhr="0" frame_rate="10" is_animated="0" name="" type="marker">
<data_defined_properties>
<Option type="Map">
<Option name="name" type="QString" value=""/>
<Option name="properties"/>
<Option name="type" type="QString" value="collection"/>
</Option>
</data_defined_properties>
<layer class="SimpleMarker" enabled="1" id="{1432e3c3-aaae-4f1c-8df7-441e77bdd112}" locked="0" pass="0">
<Option type="Map">
<Option name="angle" type="QString" value="0"/>
<Option name="cap_style" type="QString" value="square"/>
<Option name="color" type="QString" value="152,125,183,255,rgb:0.5960785,0.4901961,0.7176471,1"/>
<Option name="horizontal_anchor_point" type="QString" value="1"/>
<Option name="joinstyle" type="QString" value="bevel"/>
<Option name="name" type="QString" value="diamond"/>
<Option name="offset" type="QString" value="0,0"/>
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
<Option name="offset_unit" type="QString" value="MM"/>
<Option name="outline_color" type="QString" value="109,89,131,255,rgb:0.4257572,0.3501335,0.5125963,1"/>
<Option name="outline_style" type="QString" value="solid"/>
<Option name="outline_width" type="QString" value="0.2"/>
<Option name="outline_width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
<Option name="outline_width_unit" type="QString" value="MM"/>
<Option name="scale_method" type="QString" value="diameter"/>
<Option name="size" type="QString" value="3"/>
<Option name="size_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
<Option name="size_unit" type="QString" value="MM"/>
<Option name="vertical_anchor_point" type="QString" value="1"/>
</Option>
<data_defined_properties>
<Option type="Map">
<Option name="name" type="QString" value=""/>
<Option name="properties"/>
<Option name="type" type="QString" value="collection"/>
</Option>
</data_defined_properties>
</layer>
</symbol>
</profileMarkerSymbol>
</elevation>
<renderer-v2 enableorderby="0" forceraster="0" referencescale="-1" symbollevels="0" type="RuleRenderer">
<rules key="{b5ed05de-e0db-4029-a532-cc0c11943124}">
<rule filter=" &quot;gp_abmarkung_Marke&quot; = '1000' " key="{ba76030e-eb1e-4a5a-b10c-b8614af46b1a}" label="Abgemarkt" symbol="0">
<rule filter="&quot;gp_festgestellterGrenzpunkt&quot; = 'true'" key="{0f910cf3-1e11-45f4-b4a5-5b8b11f4e2f3}" label="FGP" symbol="1"/>
</rule>
<rule filter=" &quot;gp_abmarkung_Marke&quot; IN ('9500')" key="{f530f073-c1a2-426e-9394-eb137d98b1fd}" label="Nicht abgemarkt" symbol="2">
<rule filter="&quot;gp_festgestellterGrenzpunkt&quot; = 'true'" key="{c1a0a382-cbf0-4981-b461-33c42c64701e}" label="FGP" symbol="3"/>
</rule>
<rule filter=" &quot;gp_abmarkung_Marke&quot; IN ('9600')" key="{f9d78633-b6a7-40df-88b4-d2d5bd51d1ab}" label="zeitweilig ausgesetzt" symbol="4">
<rule filter="&quot;gp_festgestellterGrenzpunkt&quot; = 'true'" key="{66a1f910-5127-4e2c-8c55-bb48f290be47}" label="FGP" symbol="5"/>
</rule>
</rules>
<symbols>
<symbol alpha="1" clip_to_extent="1" force_rhr="0" frame_rate="10" is_animated="0" name="0" type="marker">
<data_defined_properties>
<Option type="Map">
<Option name="name" type="QString" value=""/>
<Option name="properties"/>
<Option name="type" type="QString" value="collection"/>
</Option>
</data_defined_properties>
<layer class="SimpleMarker" enabled="1" id="{4b2b5770-c712-4a17-a7e7-e8d833a5ef55}" locked="0" pass="0">
<Option type="Map">
<Option name="angle" type="QString" value="0"/>
<Option name="cap_style" type="QString" value="square"/>
<Option name="color" type="QString" value="255,255,255,255,hsv:0,0,1,1"/>
<Option name="horizontal_anchor_point" type="QString" value="1"/>
<Option name="joinstyle" type="QString" value="bevel"/>
<Option name="name" type="QString" value="circle"/>
<Option name="offset" type="QString" value="0,0"/>
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
<Option name="offset_unit" type="QString" value="Point"/>
<Option name="outline_color" type="QString" value="35,35,35,255,rgb:0.1372549,0.1372549,0.1372549,1"/>
<Option name="outline_style" type="QString" value="solid"/>
<Option name="outline_width" type="QString" value="1"/>
<Option name="outline_width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
<Option name="outline_width_unit" type="QString" value="Point"/>
<Option name="scale_method" type="QString" value="diameter"/>
<Option name="size" type="QString" value="6"/>
<Option name="size_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
<Option name="size_unit" type="QString" value="Point"/>
<Option name="vertical_anchor_point" type="QString" value="1"/>
</Option>
<data_defined_properties>
<Option type="Map">
<Option name="name" type="QString" value=""/>
<Option name="properties"/>
<Option name="type" type="QString" value="collection"/>
</Option>
</data_defined_properties>
</layer>
</symbol>
<symbol alpha="1" clip_to_extent="1" force_rhr="0" frame_rate="10" is_animated="0" name="1" type="marker">
<data_defined_properties>
<Option type="Map">
<Option name="name" type="QString" value=""/>
<Option name="properties"/>
<Option name="type" type="QString" value="collection"/>
</Option>
</data_defined_properties>
<layer class="SimpleMarker" enabled="1" id="{e3e8ca93-babc-4c67-803f-73b091b1dcbf}" locked="0" pass="0">
<Option type="Map">
<Option name="angle" type="QString" value="0"/>
<Option name="cap_style" type="QString" value="square"/>
<Option name="color" type="QString" value="255,255,255,0,hsv:0,0,1,0"/>
<Option name="horizontal_anchor_point" type="QString" value="1"/>
<Option name="joinstyle" type="QString" value="bevel"/>
<Option name="name" type="QString" value="circle"/>
<Option name="offset" type="QString" value="0,0"/>
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
<Option name="offset_unit" type="QString" value="Point"/>
<Option name="outline_color" type="QString" value="227,26,28,255,rgb:0.8901961,0.1019608,0.1098039,1"/>
<Option name="outline_style" type="QString" value="solid"/>
<Option name="outline_width" type="QString" value="1"/>
<Option name="outline_width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
<Option name="outline_width_unit" type="QString" value="Point"/>
<Option name="scale_method" type="QString" value="diameter"/>
<Option name="size" type="QString" value="8"/>
<Option name="size_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
<Option name="size_unit" type="QString" value="Point"/>
<Option name="vertical_anchor_point" type="QString" value="1"/>
</Option>
<data_defined_properties>
<Option type="Map">
<Option name="name" type="QString" value=""/>
<Option name="properties"/>
<Option name="type" type="QString" value="collection"/>
</Option>
</data_defined_properties>
</layer>
</symbol>
<symbol alpha="1" clip_to_extent="1" force_rhr="0" frame_rate="10" is_animated="0" name="2" type="marker">
<data_defined_properties>
<Option type="Map">
<Option name="name" type="QString" value=""/>
<Option name="properties"/>
<Option name="type" type="QString" value="collection"/>
</Option>
</data_defined_properties>
<layer class="SimpleMarker" enabled="1" id="{3b684a01-3cd1-4ec2-a3b3-5dd097cac078}" locked="0" pass="0">
<Option type="Map">
<Option name="angle" type="QString" value="0"/>
<Option name="cap_style" type="QString" value="square"/>
<Option name="color" type="QString" value="255,255,255,255,hsv:0,0,1,1"/>
<Option name="horizontal_anchor_point" type="QString" value="1"/>
<Option name="joinstyle" type="QString" value="bevel"/>
<Option name="name" type="QString" value="circle"/>
<Option name="offset" type="QString" value="0,0"/>
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
<Option name="offset_unit" type="QString" value="MM"/>
<Option name="outline_color" type="QString" value="35,35,35,255,rgb:0.1372549,0.1372549,0.1372549,1"/>
<Option name="outline_style" type="QString" value="no"/>
<Option name="outline_width" type="QString" value="0"/>
<Option name="outline_width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
<Option name="outline_width_unit" type="QString" value="MM"/>
<Option name="scale_method" type="QString" value="diameter"/>
<Option name="size" type="QString" value="6"/>
<Option name="size_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
<Option name="size_unit" type="QString" value="Point"/>
<Option name="vertical_anchor_point" type="QString" value="1"/>
</Option>
<data_defined_properties>
<Option type="Map">
<Option name="name" type="QString" value=""/>
<Option name="properties"/>
<Option name="type" type="QString" value="collection"/>
</Option>
</data_defined_properties>
</layer>
<layer class="SimpleMarker" enabled="1" id="{7cf47a18-2311-467c-835d-8a5ba1dcbbf9}" locked="0" pass="0">
<Option type="Map">
<Option name="angle" type="QString" value="0"/>
<Option name="cap_style" type="QString" value="square"/>
<Option name="color" type="QString" value="0,0,0,255,hsv:0.99550002813339233,0.60656136274337769,0,1"/>
<Option name="horizontal_anchor_point" type="QString" value="1"/>
<Option name="joinstyle" type="QString" value="bevel"/>
<Option name="name" type="QString" value="circle"/>
<Option name="offset" type="QString" value="0,0"/>
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
<Option name="offset_unit" type="QString" value="MM"/>
<Option name="outline_color" type="QString" value="35,35,35,255,rgb:0.1372549,0.1372549,0.1372549,1"/>
<Option name="outline_style" type="QString" value="solid"/>
<Option name="outline_width" type="QString" value="0"/>
<Option name="outline_width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
<Option name="outline_width_unit" type="QString" value="MM"/>
<Option name="scale_method" type="QString" value="diameter"/>
<Option name="size" type="QString" value="2"/>
<Option name="size_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
<Option name="size_unit" type="QString" value="Point"/>
<Option name="vertical_anchor_point" type="QString" value="1"/>
</Option>
<data_defined_properties>
<Option type="Map">
<Option name="name" type="QString" value=""/>
<Option name="properties"/>
<Option name="type" type="QString" value="collection"/>
</Option>
</data_defined_properties>
</layer>
</symbol>
<symbol alpha="1" clip_to_extent="1" force_rhr="0" frame_rate="10" is_animated="0" name="3" type="marker">
<data_defined_properties>
<Option type="Map">
<Option name="name" type="QString" value=""/>
<Option name="properties"/>
<Option name="type" type="QString" value="collection"/>
</Option>
</data_defined_properties>
<layer class="SimpleMarker" enabled="1" id="{e3e8ca93-babc-4c67-803f-73b091b1dcbf}" locked="0" pass="0">
<Option type="Map">
<Option name="angle" type="QString" value="0"/>
<Option name="cap_style" type="QString" value="square"/>
<Option name="color" type="QString" value="255,255,255,0,hsv:0,0,1,0"/>
<Option name="horizontal_anchor_point" type="QString" value="1"/>
<Option name="joinstyle" type="QString" value="bevel"/>
<Option name="name" type="QString" value="circle"/>
<Option name="offset" type="QString" value="0,0"/>
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
<Option name="offset_unit" type="QString" value="Point"/>
<Option name="outline_color" type="QString" value="227,26,28,255,rgb:0.8901961,0.1019608,0.1098039,1"/>
<Option name="outline_style" type="QString" value="solid"/>
<Option name="outline_width" type="QString" value="1"/>
<Option name="outline_width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
<Option name="outline_width_unit" type="QString" value="Point"/>
<Option name="scale_method" type="QString" value="diameter"/>
<Option name="size" type="QString" value="8"/>
<Option name="size_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
<Option name="size_unit" type="QString" value="Point"/>
<Option name="vertical_anchor_point" type="QString" value="1"/>
</Option>
<data_defined_properties>
<Option type="Map">
<Option name="name" type="QString" value=""/>
<Option name="properties"/>
<Option name="type" type="QString" value="collection"/>
</Option>
</data_defined_properties>
</layer>
</symbol>
<symbol alpha="1" clip_to_extent="1" force_rhr="0" frame_rate="10" is_animated="0" name="4" type="marker">
<data_defined_properties>
<Option type="Map">
<Option name="name" type="QString" value=""/>
<Option name="properties"/>
<Option name="type" type="QString" value="collection"/>
</Option>
</data_defined_properties>
<layer class="SimpleMarker" enabled="1" id="{3b684a01-3cd1-4ec2-a3b3-5dd097cac078}" locked="0" pass="0">
<Option type="Map">
<Option name="angle" type="QString" value="0"/>
<Option name="cap_style" type="QString" value="square"/>
<Option name="color" type="QString" value="255,255,255,255,hsv:0,0,1,1"/>
<Option name="horizontal_anchor_point" type="QString" value="1"/>
<Option name="joinstyle" type="QString" value="bevel"/>
<Option name="name" type="QString" value="circle"/>
<Option name="offset" type="QString" value="0,0"/>
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
<Option name="offset_unit" type="QString" value="MM"/>
<Option name="outline_color" type="QString" value="35,35,35,255,rgb:0.1372549,0.1372549,0.1372549,1"/>
<Option name="outline_style" type="QString" value="solid"/>
<Option name="outline_width" type="QString" value="1"/>
<Option name="outline_width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
<Option name="outline_width_unit" type="QString" value="MM"/>
<Option name="scale_method" type="QString" value="diameter"/>
<Option name="size" type="QString" value="6"/>
<Option name="size_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
<Option name="size_unit" type="QString" value="Point"/>
<Option name="vertical_anchor_point" type="QString" value="1"/>
</Option>
<data_defined_properties>
<Option type="Map">
<Option name="name" type="QString" value=""/>
<Option name="properties"/>
<Option name="type" type="QString" value="collection"/>
</Option>
</data_defined_properties>
</layer>
<layer class="SimpleMarker" enabled="1" id="{7cf47a18-2311-467c-835d-8a5ba1dcbbf9}" locked="0" pass="0">
<Option type="Map">
<Option name="angle" type="QString" value="0"/>
<Option name="cap_style" type="QString" value="square"/>
<Option name="color" type="QString" value="0,0,0,255,hsv:0.99550002813339233,0.60656136274337769,0,1"/>
<Option name="horizontal_anchor_point" type="QString" value="1"/>
<Option name="joinstyle" type="QString" value="bevel"/>
<Option name="name" type="QString" value="circle"/>
<Option name="offset" type="QString" value="0,0"/>
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
<Option name="offset_unit" type="QString" value="MM"/>
<Option name="outline_color" type="QString" value="35,35,35,255,rgb:0.1372549,0.1372549,0.1372549,1"/>
<Option name="outline_style" type="QString" value="solid"/>
<Option name="outline_width" type="QString" value="0"/>
<Option name="outline_width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
<Option name="outline_width_unit" type="QString" value="MM"/>
<Option name="scale_method" type="QString" value="diameter"/>
<Option name="size" type="QString" value="2"/>
<Option name="size_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
<Option name="size_unit" type="QString" value="Point"/>
<Option name="vertical_anchor_point" type="QString" value="1"/>
</Option>
<data_defined_properties>
<Option type="Map">
<Option name="name" type="QString" value=""/>
<Option name="properties"/>
<Option name="type" type="QString" value="collection"/>
</Option>
</data_defined_properties>
</layer>
</symbol>
<symbol alpha="1" clip_to_extent="1" force_rhr="0" frame_rate="10" is_animated="0" name="5" type="marker">
<data_defined_properties>
<Option type="Map">
<Option name="name" type="QString" value=""/>
<Option name="properties"/>
<Option name="type" type="QString" value="collection"/>
</Option>
</data_defined_properties>
<layer class="SimpleMarker" enabled="1" id="{e3e8ca93-babc-4c67-803f-73b091b1dcbf}" locked="0" pass="0">
<Option type="Map">
<Option name="angle" type="QString" value="0"/>
<Option name="cap_style" type="QString" value="square"/>
<Option name="color" type="QString" value="255,255,255,0,hsv:0,0,1,0"/>
<Option name="horizontal_anchor_point" type="QString" value="1"/>
<Option name="joinstyle" type="QString" value="bevel"/>
<Option name="name" type="QString" value="circle"/>
<Option name="offset" type="QString" value="0,0"/>
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
<Option name="offset_unit" type="QString" value="Point"/>
<Option name="outline_color" type="QString" value="227,26,28,255,rgb:0.8901961,0.1019608,0.1098039,1"/>
<Option name="outline_style" type="QString" value="solid"/>
<Option name="outline_width" type="QString" value="1"/>
<Option name="outline_width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
<Option name="outline_width_unit" type="QString" value="Point"/>
<Option name="scale_method" type="QString" value="diameter"/>
<Option name="size" type="QString" value="8"/>
<Option name="size_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
<Option name="size_unit" type="QString" value="Point"/>
<Option name="vertical_anchor_point" type="QString" value="1"/>
</Option>
<data_defined_properties>
<Option type="Map">
<Option name="name" type="QString" value=""/>
<Option name="properties"/>
<Option name="type" type="QString" value="collection"/>
</Option>
</data_defined_properties>
</layer>
</symbol>
</symbols>
<data-defined-properties>
<Option type="Map">
<Option name="name" type="QString" value=""/>
<Option name="properties"/>
<Option name="type" type="QString" value="collection"/>
</Option>
</data-defined-properties>
</renderer-v2>
<selection mode="Default">
<selectionColor invalid="1"/>
<selectionSymbol>
<symbol alpha="1" clip_to_extent="1" force_rhr="0" frame_rate="10" is_animated="0" name="" type="marker">
<data_defined_properties>
<Option type="Map">
<Option name="name" type="QString" value=""/>
<Option name="properties"/>
<Option name="type" type="QString" value="collection"/>
</Option>
</data_defined_properties>
<layer class="SimpleMarker" enabled="1" id="{4e7caead-e027-4f3e-a2ff-5a600d1c387c}" locked="0" pass="0">
<Option type="Map">
<Option name="angle" type="QString" value="0"/>
<Option name="cap_style" type="QString" value="square"/>
<Option name="color" type="QString" value="255,0,0,255,rgb:1,0,0,1"/>
<Option name="horizontal_anchor_point" type="QString" value="1"/>
<Option name="joinstyle" type="QString" value="bevel"/>
<Option name="name" type="QString" value="circle"/>
<Option name="offset" type="QString" value="0,0"/>
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
<Option name="offset_unit" type="QString" value="MM"/>
<Option name="outline_color" type="QString" value="35,35,35,255,rgb:0.1372549,0.1372549,0.1372549,1"/>
<Option name="outline_style" type="QString" value="solid"/>
<Option name="outline_width" type="QString" value="0"/>
<Option name="outline_width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
<Option name="outline_width_unit" type="QString" value="MM"/>
<Option name="scale_method" type="QString" value="diameter"/>
<Option name="size" type="QString" value="2"/>
<Option name="size_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
<Option name="size_unit" type="QString" value="MM"/>
<Option name="vertical_anchor_point" type="QString" value="1"/>
</Option>
<data_defined_properties>
<Option type="Map">
<Option name="name" type="QString" value=""/>
<Option name="properties"/>
<Option name="type" type="QString" value="collection"/>
</Option>
</data_defined_properties>
</layer>
</symbol>
</selectionSymbol>
</selection>
<customproperties>
<Option type="Map">
<Option name="dualview/previewExpressions" type="List">
<Option type="QString" value="COALESCE( &quot;gml_id&quot;, '&lt;NULL>' )"/>
</Option>
<Option name="embeddedWidgets/count" type="int" value="0"/>
<Option name="variableNames"/>
<Option name="variableValues"/>
</Option>
</customproperties>
<blendMode>0</blendMode>
<featureBlendMode>0</featureBlendMode>
<layerOpacity>1</layerOpacity>
<geometryOptions geometryPrecision="0" removeDuplicateNodes="0">
<activeChecks/>
<checkConfiguration/>
</geometryOptions>
<legend showLabelLegend="0" type="default-vector"/>
<referencedLayers/>
<referencingLayers/>
<fieldConfiguration>
<field configurationFlags="0" name="gml_id">
<editWidget type="TextEdit">
<config>
<Option/>
</config>
</editWidget>
</field>
<field configurationFlags="0" name="identifier">
<editWidget type="TextEdit">
<config>
<Option/>
</config>
</editWidget>
</field>
<field configurationFlags="0" name="lebenszeitintervall">
<editWidget type="TextEdit">
<config>
<Option/>
</config>
</editWidget>
</field>
<field configurationFlags="0" name="modellart">
<editWidget type="TextEdit">
<config>
<Option/>
</config>
</editWidget>
</field>
<field configurationFlags="0" name="anlass">
<editWidget type="TextEdit">
<config>
<Option/>
</config>
</editWidget>
</field>
<field configurationFlags="0" name="zeigtAufExternes">
<editWidget type="TextEdit">
<config>
<Option/>
</config>
</editWidget>
</field>
<field configurationFlags="0" name="istTeilVon">
<editWidget type="TextEdit">
<config>
<Option/>
</config>
</editWidget>
</field>
<field configurationFlags="0" name="kartendarstellung">
<editWidget type="TextEdit">
<config>
<Option/>
</config>
</editWidget>
</field>
<field configurationFlags="0" name="koordinatenstatus">
<editWidget type="TextEdit">
<config>
<Option/>
</config>
</editWidget>
</field>
<field configurationFlags="0" name="qualitaetsangaben_ax_dqpunktort_genauigkeitsstufe">
<editWidget type="TextEdit">
<config>
<Option/>
</config>
</editWidget>
</field>
<field configurationFlags="0" name="gp_gml_id">
<editWidget type="TextEdit">
<config>
<Option/>
</config>
</editWidget>
</field>
<field configurationFlags="0" name="gp_identifier">
<editWidget type="TextEdit">
<config>
<Option/>
</config>
</editWidget>
</field>
<field configurationFlags="0" name="gp_lebenszeitintervall">
<editWidget type="TextEdit">
<config>
<Option/>
</config>
</editWidget>
</field>
<field configurationFlags="0" name="gp_modellart">
<editWidget type="TextEdit">
<config>
<Option/>
</config>
</editWidget>
</field>
<field configurationFlags="0" name="gp_anlass">
<editWidget type="TextEdit">
<config>
<Option/>
</config>
</editWidget>
</field>
<field configurationFlags="0" name="gp_zeigtAufExternes">
<editWidget type="TextEdit">
<config>
<Option/>
</config>
</editWidget>
</field>
<field configurationFlags="0" name="gp_bestehtAus">
<editWidget type="TextEdit">
<config>
<Option/>
</config>
</editWidget>
</field>
<field configurationFlags="0" name="gp_punktkennung">
<editWidget type="TextEdit">
<config>
<Option/>
</config>
</editWidget>
</field>
<field configurationFlags="0" name="gp_abmarkung_Marke">
<editWidget type="TextEdit">
<config>
<Option/>
</config>
</editWidget>
</field>
<field configurationFlags="0" name="gp_festgestellterGrenzpunkt">
<editWidget type="TextEdit">
<config>
<Option/>
</config>
</editWidget>
</field>
<field configurationFlags="0" name="gp_inversZu_zeigtAuf">
<editWidget type="TextEdit">
<config>
<Option/>
</config>
</editWidget>
</field>
<field configurationFlags="0" name="gp_sonstigeEigenschaft">
<editWidget type="TextEdit">
<config>
<Option/>
</config>
</editWidget>
</field>
</fieldConfiguration>
<aliases>
<alias field="gml_id" index="0" name=""/>
<alias field="identifier" index="1" name=""/>
<alias field="lebenszeitintervall" index="2" name=""/>
<alias field="modellart" index="3" name=""/>
<alias field="anlass" index="4" name=""/>
<alias field="zeigtAufExternes" index="5" name=""/>
<alias field="istTeilVon" index="6" name=""/>
<alias field="kartendarstellung" index="7" name=""/>
<alias field="koordinatenstatus" index="8" name=""/>
<alias field="qualitaetsangaben_ax_dqpunktort_genauigkeitsstufe" index="9" name=""/>
<alias field="gp_gml_id" index="10" name=""/>
<alias field="gp_identifier" index="11" name=""/>
<alias field="gp_lebenszeitintervall" index="12" name=""/>
<alias field="gp_modellart" index="13" name=""/>
<alias field="gp_anlass" index="14" name=""/>
<alias field="gp_zeigtAufExternes" index="15" name=""/>
<alias field="gp_bestehtAus" index="16" name=""/>
<alias field="gp_punktkennung" index="17" name=""/>
<alias field="gp_abmarkung_Marke" index="18" name=""/>
<alias field="gp_festgestellterGrenzpunkt" index="19" name=""/>
<alias field="gp_inversZu_zeigtAuf" index="20" name=""/>
<alias field="gp_sonstigeEigenschaft" index="21" name=""/>
</aliases>
<defaults>
<default applyOnUpdate="0" expression="" field="gml_id"/>
<default applyOnUpdate="0" expression="" field="identifier"/>
<default applyOnUpdate="0" expression="" field="lebenszeitintervall"/>
<default applyOnUpdate="0" expression="" field="modellart"/>
<default applyOnUpdate="0" expression="" field="anlass"/>
<default applyOnUpdate="0" expression="" field="zeigtAufExternes"/>
<default applyOnUpdate="0" expression="" field="istTeilVon"/>
<default applyOnUpdate="0" expression="" field="kartendarstellung"/>
<default applyOnUpdate="0" expression="" field="koordinatenstatus"/>
<default applyOnUpdate="0" expression="" field="qualitaetsangaben_ax_dqpunktort_genauigkeitsstufe"/>
<default applyOnUpdate="0" expression="" field="gp_gml_id"/>
<default applyOnUpdate="0" expression="" field="gp_identifier"/>
<default applyOnUpdate="0" expression="" field="gp_lebenszeitintervall"/>
<default applyOnUpdate="0" expression="" field="gp_modellart"/>
<default applyOnUpdate="0" expression="" field="gp_anlass"/>
<default applyOnUpdate="0" expression="" field="gp_zeigtAufExternes"/>
<default applyOnUpdate="0" expression="" field="gp_bestehtAus"/>
<default applyOnUpdate="0" expression="" field="gp_punktkennung"/>
<default applyOnUpdate="0" expression="" field="gp_abmarkung_Marke"/>
<default applyOnUpdate="0" expression="" field="gp_festgestellterGrenzpunkt"/>
<default applyOnUpdate="0" expression="" field="gp_inversZu_zeigtAuf"/>
<default applyOnUpdate="0" expression="" field="gp_sonstigeEigenschaft"/>
</defaults>
<constraints>
<constraint constraints="0" exp_strength="0" field="gml_id" notnull_strength="0" unique_strength="0"/>
<constraint constraints="0" exp_strength="0" field="identifier" notnull_strength="0" unique_strength="0"/>
<constraint constraints="0" exp_strength="0" field="lebenszeitintervall" notnull_strength="0" unique_strength="0"/>
<constraint constraints="0" exp_strength="0" field="modellart" notnull_strength="0" unique_strength="0"/>
<constraint constraints="0" exp_strength="0" field="anlass" notnull_strength="0" unique_strength="0"/>
<constraint constraints="0" exp_strength="0" field="zeigtAufExternes" notnull_strength="0" unique_strength="0"/>
<constraint constraints="0" exp_strength="0" field="istTeilVon" notnull_strength="0" unique_strength="0"/>
<constraint constraints="0" exp_strength="0" field="kartendarstellung" notnull_strength="0" unique_strength="0"/>
<constraint constraints="0" exp_strength="0" field="koordinatenstatus" notnull_strength="0" unique_strength="0"/>
<constraint constraints="0" exp_strength="0" field="qualitaetsangaben_ax_dqpunktort_genauigkeitsstufe" notnull_strength="0" unique_strength="0"/>
<constraint constraints="0" exp_strength="0" field="gp_gml_id" notnull_strength="0" unique_strength="0"/>
<constraint constraints="0" exp_strength="0" field="gp_identifier" notnull_strength="0" unique_strength="0"/>
<constraint constraints="0" exp_strength="0" field="gp_lebenszeitintervall" notnull_strength="0" unique_strength="0"/>
<constraint constraints="0" exp_strength="0" field="gp_modellart" notnull_strength="0" unique_strength="0"/>
<constraint constraints="0" exp_strength="0" field="gp_anlass" notnull_strength="0" unique_strength="0"/>
<constraint constraints="0" exp_strength="0" field="gp_zeigtAufExternes" notnull_strength="0" unique_strength="0"/>
<constraint constraints="0" exp_strength="0" field="gp_bestehtAus" notnull_strength="0" unique_strength="0"/>
<constraint constraints="0" exp_strength="0" field="gp_punktkennung" notnull_strength="0" unique_strength="0"/>
<constraint constraints="0" exp_strength="0" field="gp_abmarkung_Marke" notnull_strength="0" unique_strength="0"/>
<constraint constraints="0" exp_strength="0" field="gp_festgestellterGrenzpunkt" notnull_strength="0" unique_strength="0"/>
<constraint constraints="0" exp_strength="0" field="gp_inversZu_zeigtAuf" notnull_strength="0" unique_strength="0"/>
<constraint constraints="0" exp_strength="0" field="gp_sonstigeEigenschaft" notnull_strength="0" unique_strength="0"/>
</constraints>
<constraintExpressions>
<constraint desc="" exp="" field="gml_id"/>
<constraint desc="" exp="" field="identifier"/>
<constraint desc="" exp="" field="lebenszeitintervall"/>
<constraint desc="" exp="" field="modellart"/>
<constraint desc="" exp="" field="anlass"/>
<constraint desc="" exp="" field="zeigtAufExternes"/>
<constraint desc="" exp="" field="istTeilVon"/>
<constraint desc="" exp="" field="kartendarstellung"/>
<constraint desc="" exp="" field="koordinatenstatus"/>
<constraint desc="" exp="" field="qualitaetsangaben_ax_dqpunktort_genauigkeitsstufe"/>
<constraint desc="" exp="" field="gp_gml_id"/>
<constraint desc="" exp="" field="gp_identifier"/>
<constraint desc="" exp="" field="gp_lebenszeitintervall"/>
<constraint desc="" exp="" field="gp_modellart"/>
<constraint desc="" exp="" field="gp_anlass"/>
<constraint desc="" exp="" field="gp_zeigtAufExternes"/>
<constraint desc="" exp="" field="gp_bestehtAus"/>
<constraint desc="" exp="" field="gp_punktkennung"/>
<constraint desc="" exp="" field="gp_abmarkung_Marke"/>
<constraint desc="" exp="" field="gp_festgestellterGrenzpunkt"/>
<constraint desc="" exp="" field="gp_inversZu_zeigtAuf"/>
<constraint desc="" exp="" field="gp_sonstigeEigenschaft"/>
</constraintExpressions>
<expressionfields/>
<attributeactions>
<defaultAction key="Canvas" value="{00000000-0000-0000-0000-000000000000}"/>
</attributeactions>
<attributetableconfig actionWidgetStyle="dropDown" sortExpression="" sortOrder="0">
<columns>
<column hidden="0" name="gml_id" type="field" width="-1"/>
<column hidden="0" name="identifier" type="field" width="-1"/>
<column hidden="0" name="lebenszeitintervall" type="field" width="-1"/>
<column hidden="0" name="modellart" type="field" width="-1"/>
<column hidden="0" name="anlass" type="field" width="-1"/>
<column hidden="0" name="zeigtAufExternes" type="field" width="-1"/>
<column hidden="0" name="istTeilVon" type="field" width="-1"/>
<column hidden="0" name="kartendarstellung" type="field" width="-1"/>
<column hidden="0" name="koordinatenstatus" type="field" width="-1"/>
<column hidden="0" name="qualitaetsangaben_ax_dqpunktort_genauigkeitsstufe" type="field" width="-1"/>
<column hidden="0" name="gp_gml_id" type="field" width="-1"/>
<column hidden="0" name="gp_identifier" type="field" width="-1"/>
<column hidden="0" name="gp_lebenszeitintervall" type="field" width="-1"/>
<column hidden="0" name="gp_modellart" type="field" width="-1"/>
<column hidden="0" name="gp_anlass" type="field" width="-1"/>
<column hidden="0" name="gp_zeigtAufExternes" type="field" width="-1"/>
<column hidden="0" name="gp_bestehtAus" type="field" width="-1"/>
<column hidden="0" name="gp_punktkennung" type="field" width="-1"/>
<column hidden="0" name="gp_abmarkung_Marke" type="field" width="-1"/>
<column hidden="0" name="gp_festgestellterGrenzpunkt" type="field" width="-1"/>
<column hidden="0" name="gp_inversZu_zeigtAuf" type="field" width="-1"/>
<column hidden="0" name="gp_sonstigeEigenschaft" type="field" width="-1"/>
<column hidden="1" type="actions" width="-1"/>
</columns>
</attributetableconfig>
<conditionalstyles>
<rowstyles/>
<fieldstyles/>
</conditionalstyles>
<storedexpressions/>
<editform tolerant="1"></editform>
<editforminit/>
<editforminitcodesource>0</editforminitcodesource>
<editforminitfilepath></editforminitfilepath>
<editforminitcode><![CDATA[# -*- coding: utf-8 -*-
"""
QGIS-Formulare können eine Python-Funktion haben,, die aufgerufen wird, wenn sich das Formular öffnet
Diese Funktion kann verwendet werden um dem Formular Extralogik hinzuzufügen.
Der Name der Funktion wird im Feld "Python Init-Function" angegeben
Ein Beispiel folgt:
"""
from qgis.PyQt.QtWidgets import QWidget
def my_form_open(dialog, layer, feature):
geom = feature.geometry()
control = dialog.findChild(QWidget, "MyLineEdit")
]]></editforminitcode>
<featformsuppress>0</featformsuppress>
<editorlayout>generatedlayout</editorlayout>
<editable>
<field editable="1" name="anlass"/>
<field editable="1" name="gml_id"/>
<field editable="1" name="gp_abmarkung_Marke"/>
<field editable="1" name="gp_anlass"/>
<field editable="1" name="gp_bestehtAus"/>
<field editable="1" name="gp_festgestellterGrenzpunkt"/>
<field editable="1" name="gp_gml_id"/>
<field editable="1" name="gp_identifier"/>
<field editable="1" name="gp_inversZu_zeigtAuf"/>
<field editable="1" name="gp_lebenszeitintervall"/>
<field editable="1" name="gp_modellart"/>
<field editable="1" name="gp_punktkennung"/>
<field editable="1" name="gp_sonstigeEigenschaft"/>
<field editable="1" name="gp_zeigtAufExternes"/>
<field editable="1" name="identifier"/>
<field editable="1" name="istTeilVon"/>
<field editable="1" name="kartendarstellung"/>
<field editable="1" name="koordinatenstatus"/>
<field editable="1" name="lebenszeitintervall"/>
<field editable="1" name="modellart"/>
<field editable="1" name="qualitaetsangaben_ax_dqpunktort_genauigkeitsstufe"/>
<field editable="1" name="zeigtAufExternes"/>
</editable>
<labelOnTop>
<field labelOnTop="0" name="anlass"/>
<field labelOnTop="0" name="gml_id"/>
<field labelOnTop="0" name="gp_abmarkung_Marke"/>
<field labelOnTop="0" name="gp_anlass"/>
<field labelOnTop="0" name="gp_bestehtAus"/>
<field labelOnTop="0" name="gp_festgestellterGrenzpunkt"/>
<field labelOnTop="0" name="gp_gml_id"/>
<field labelOnTop="0" name="gp_identifier"/>
<field labelOnTop="0" name="gp_inversZu_zeigtAuf"/>
<field labelOnTop="0" name="gp_lebenszeitintervall"/>
<field labelOnTop="0" name="gp_modellart"/>
<field labelOnTop="0" name="gp_punktkennung"/>
<field labelOnTop="0" name="gp_sonstigeEigenschaft"/>
<field labelOnTop="0" name="gp_zeigtAufExternes"/>
<field labelOnTop="0" name="identifier"/>
<field labelOnTop="0" name="istTeilVon"/>
<field labelOnTop="0" name="kartendarstellung"/>
<field labelOnTop="0" name="koordinatenstatus"/>
<field labelOnTop="0" name="lebenszeitintervall"/>
<field labelOnTop="0" name="modellart"/>
<field labelOnTop="0" name="qualitaetsangaben_ax_dqpunktort_genauigkeitsstufe"/>
<field labelOnTop="0" name="zeigtAufExternes"/>
</labelOnTop>
<reuseLastValuePolicy>
<field name="anlass" reuseLastValuePolicy="NotAllowed"/>
<field name="gml_id" reuseLastValuePolicy="NotAllowed"/>
<field name="gp_abmarkung_Marke" reuseLastValuePolicy="NotAllowed"/>
<field name="gp_anlass" reuseLastValuePolicy="NotAllowed"/>
<field name="gp_bestehtAus" reuseLastValuePolicy="NotAllowed"/>
<field name="gp_festgestellterGrenzpunkt" reuseLastValuePolicy="NotAllowed"/>
<field name="gp_gml_id" reuseLastValuePolicy="NotAllowed"/>
<field name="gp_identifier" reuseLastValuePolicy="NotAllowed"/>
<field name="gp_inversZu_zeigtAuf" reuseLastValuePolicy="NotAllowed"/>
<field name="gp_lebenszeitintervall" reuseLastValuePolicy="NotAllowed"/>
<field name="gp_modellart" reuseLastValuePolicy="NotAllowed"/>
<field name="gp_punktkennung" reuseLastValuePolicy="NotAllowed"/>
<field name="gp_sonstigeEigenschaft" reuseLastValuePolicy="NotAllowed"/>
<field name="gp_zeigtAufExternes" reuseLastValuePolicy="NotAllowed"/>
<field name="identifier" reuseLastValuePolicy="NotAllowed"/>
<field name="istTeilVon" reuseLastValuePolicy="NotAllowed"/>
<field name="kartendarstellung" reuseLastValuePolicy="NotAllowed"/>
<field name="koordinatenstatus" reuseLastValuePolicy="NotAllowed"/>
<field name="lebenszeitintervall" reuseLastValuePolicy="NotAllowed"/>
<field name="modellart" reuseLastValuePolicy="NotAllowed"/>
<field name="qualitaetsangaben_ax_dqpunktort_genauigkeitsstufe" reuseLastValuePolicy="NotAllowed"/>
<field name="zeigtAufExternes" reuseLastValuePolicy="NotAllowed"/>
</reuseLastValuePolicy>
<dataDefinedFieldProperties/>
<widgets/>
<previewExpression>COALESCE( "gml_id", '&lt;NULL>' )</previewExpression>
<mapTip enabled="1"></mapTip>
<layerGeometryType>0</layerGeometryType>
</qgis>
+221 -86
View File
@@ -6,8 +6,11 @@ Getestet werden:
- Nutzerabbruch bei ask_text → (None, LoadStatus.NONE) - 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()
+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)
+31 -83
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,12 +15,7 @@ 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,
lade_flurstuecke_per_gemarkung,
ALKIS_SF_URL,
FLST_LAYER_NAME,
)
from sn_verfahrensgebiet.functions.verfahrensgebiet_alkis_komplett import ( from sn_verfahrensgebiet.functions.verfahrensgebiet_alkis_komplett import (
verfahrensgebiet_alkis_komplett, verfahrensgebiet_alkis_komplett,
BROR_LAYER_NAME, BROR_LAYER_NAME,
@@ -230,8 +225,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)
@@ -287,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)
@@ -392,6 +387,25 @@ class WorkingTab(QWidget):
# Flurstücke-Handler # Flurstücke-Handler
# ------------------------------------------------------------------ # ------------------------------------------------------------------
def _uebernimm_flurstuecke_ergebnis(self, layer, status) -> None:
"""Übernimmt das Ergebnis von flurstueck_alkis() ins Projekt (FIRST/RELOAD/APPEND/KEEP)."""
if status in (LoadStatus.FIRST, LoadStatus.RELOAD):
QgsProject.instance().addMapLayer(layer)
apply_style(layer, "GIS_Flst_Beschriftung_ALKIS_NAS.qml")
iface.mapCanvas().refresh()
self.setze_haken(self.haken_flurst, True)
msg = "erstmalig geladen" if status == LoadStatus.FIRST else "neu geladen"
success("Flurstücke", f"Layer wurde {msg}.", duration=5)
elif status == LoadStatus.KEEP:
self.setze_haken(self.haken_flurst, True)
info("Flurstücke", "Vorhandener Layer wurde behalten.", duration=4)
elif status == LoadStatus.APPEND:
self.setze_haken(self.haken_flurst, True)
iface.mapCanvas().refresh()
success("Flurstücke", "Features wurden an den vorhandenen Layer angehängt.", duration=5)
def handle_flurst_alkis(self) -> None: def handle_flurst_alkis(self) -> None:
"""Lädt Flurstücke aus ALKIS-WFS anhand von Gemarkungsschlüsseln.""" """Lädt Flurstücke aus ALKIS-WFS anhand von Gemarkungsschlüsseln."""
self._set_busy(self.btn_flurst_alkis, True, "Aus ALKIS laden") self._set_busy(self.btn_flurst_alkis, True, "Aus ALKIS laden")
@@ -399,25 +413,7 @@ class WorkingTab(QWidget):
layer, status = flurstueck_alkis(self) layer, status = flurstueck_alkis(self)
if not layer or not layer.isValid(): if not layer or not layer.isValid():
return return
self._uebernimm_flurstuecke_ergebnis(layer, status)
if status in (LoadStatus.FIRST, LoadStatus.RELOAD):
QgsProject.instance().addMapLayer(layer)
# iface.mapCanvas().setExtent(layer.extent()) # Kartenausschnitt bleibt unverändert
apply_style(layer, "GIS_Flst_Beschriftung_ALKIS_NAS.qml")
iface.mapCanvas().refresh()
self.setze_haken(self.haken_flurst, True)
msg = "erstmalig geladen" if status == LoadStatus.FIRST else "neu geladen"
success("Flurstücke", f"Layer wurde {msg}.", duration=5)
elif status == LoadStatus.KEEP:
self.setze_haken(self.haken_flurst, True)
info("Flurstücke", "Vorhandener Layer wurde behalten.", duration=4)
elif status == LoadStatus.APPEND:
self.setze_haken(self.haken_flurst, True)
iface.mapCanvas().refresh()
success("Flurstücke", "Features wurden an den vorhandenen Layer angehängt.", duration=5)
finally: finally:
self._set_busy(self.btn_flurst_alkis, False, "Aus ALKIS laden") self._set_busy(self.btn_flurst_alkis, False, "Aus ALKIS laden")
@@ -491,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")