2026-04-21 21:50:26 +02:00
|
|
|
|
"""sn_verfahrensgebiet/functions/flurstueck_alkis.py – ALKIS-Integration für Flurstücke.
|
|
|
|
|
|
|
2026-04-22 14:53:45 +02:00
|
|
|
|
Lädt Flurstücke aus dem ALKIS-WFS-Dienst (``adv:AX_Flurstueck``) anhand von
|
2026-04-21 21:50:26 +02:00
|
|
|
|
Gemarkungsschlüsseln, die der Nutzer interaktiv eingibt.
|
|
|
|
|
|
|
2026-09-17 12:11:13 +02:00
|
|
|
|
Für jede Gemarkungsnummer wird eine eigene WFS-Anfrage gestellt (umgeht das
|
|
|
|
|
|
WFS-Feature-Limit einer Gesamtabfrage); alle Ergebnisse werden anschließend in
|
|
|
|
|
|
einem gemeinsamen Memory-Layer zusammengeführt.
|
2026-04-22 14:53:45 +02:00
|
|
|
|
|
|
|
|
|
|
**Erweiterung gegenüber dem Original:**
|
|
|
|
|
|
|
|
|
|
|
|
- ``vorgabe_schluessel``: Optional vorausgefüllte Gemarkungsnummern (z. B. aus
|
|
|
|
|
|
:func:`~sn_verfahrensgebiet.functions.gemarkung_alkis.gemarkung_schluessel_und_namen_by_bbox`).
|
|
|
|
|
|
Der Dialog wird mit diesem Vorschlag vorausgefüllt; der Nutzer kann die
|
|
|
|
|
|
Eingabe ändern oder ergänzen.
|
|
|
|
|
|
|
|
|
|
|
|
- ``vorgabe_namen``: Zugehörige Bezeichnungen, die im Dialog als Hinweis
|
|
|
|
|
|
angezeigt werden (kein eigenständiges Feld, nur Anzeigetext).
|
|
|
|
|
|
|
2026-09-17 12:11:13 +02:00
|
|
|
|
- ``bbox``: Nicht mehr verwendet. Die Flurstück-Geometrie kommt ausschließlich
|
|
|
|
|
|
aus dem ALKIS-WFS je Gemarkung; eine BBox filtert höchstens vorgelagert die
|
|
|
|
|
|
Gemarkungsauswahl (siehe ``gemarkung_alkis.py``), nie die Flurstücke selbst.
|
|
|
|
|
|
Der Parameter bleibt nur aus Gründen der Signatur-Rückwärtskompatibilität
|
|
|
|
|
|
erhalten.
|
2026-04-21 21:50:26 +02:00
|
|
|
|
"""
|
|
|
|
|
|
|
2026-04-22 14:53:45 +02:00
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
from typing import List, Optional, Tuple
|
|
|
|
|
|
|
|
|
|
|
|
from sn_basis.functions import QgsVectorLayer, QgsProject, QgsFeature
|
2026-04-21 21:50:26 +02:00
|
|
|
|
from sn_basis.functions import QMessageBox
|
|
|
|
|
|
from sn_basis.functions.dialog_wrapper import ask_text
|
2026-04-22 14:53:45 +02:00
|
|
|
|
from sn_basis.functions.message_wrapper import info as _info
|
2026-04-21 21:50:26 +02:00
|
|
|
|
|
|
|
|
|
|
# LoadStatus aus verfahrensgebiet_alkis wiederverwenden (identische Semantik)
|
|
|
|
|
|
from sn_verfahrensgebiet.functions.verfahrensgebiet_alkis import LoadStatus
|
|
|
|
|
|
|
2026-04-23 13:07:35 +02:00
|
|
|
|
ALKIS_SF_URL = "https://geodienste.sachsen.de/aaa/public_alkis/sf/wfs"
|
2026-04-21 21:50:26 +02:00
|
|
|
|
FLST_TYPENAME = "adv:AX_Flurstueck"
|
|
|
|
|
|
FLST_LAYER_NAME = "Flurstücke"
|
2026-09-15 08:48:14 +02:00
|
|
|
|
FELD_FLURSTUECKSKENNZEICHEN = "flurstueckskennzeichen"
|
2026-04-21 21:50:26 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _check_existing_flurstueck_layer(tab_widget):
|
|
|
|
|
|
"""Prüft, ob ein Flurstück-Layer bereits im Projekt existiert.
|
|
|
|
|
|
|
|
|
|
|
|
Returns
|
|
|
|
|
|
-------
|
|
|
|
|
|
Tuple[Optional[QgsVectorLayer], LoadStatus]
|
|
|
|
|
|
Bei ``LoadStatus.KEEP`` enthält das Tuple den vorhandenen Layer,
|
|
|
|
|
|
bei ``LoadStatus.FIRST`` und ``LoadStatus.RELOAD`` ist es None.
|
|
|
|
|
|
"""
|
|
|
|
|
|
project = QgsProject.instance()
|
|
|
|
|
|
existing_layers = project.mapLayersByName(FLST_LAYER_NAME)
|
|
|
|
|
|
if not existing_layers:
|
|
|
|
|
|
return None, LoadStatus.FIRST
|
|
|
|
|
|
|
2026-06-23 13:54:08 +02:00
|
|
|
|
msg = QMessageBox(tab_widget)
|
|
|
|
|
|
msg.setWindowTitle("Layer bereits vorhanden")
|
|
|
|
|
|
msg.setText(
|
|
|
|
|
|
f"Der Layer '{FLST_LAYER_NAME}' existiert bereits.\nWas soll geschehen?"
|
2026-04-21 21:50:26 +02:00
|
|
|
|
)
|
2026-06-23 13:54:08 +02:00
|
|
|
|
btn_ueberschreiben = msg.addButton("Überschreiben", QMessageBox.ButtonRole.DestructiveRole)
|
|
|
|
|
|
btn_anhaengen = msg.addButton("Anhängen", QMessageBox.ButtonRole.AcceptRole)
|
|
|
|
|
|
btn_abbrechen = msg.addButton("Abbrechen", QMessageBox.ButtonRole.RejectRole)
|
|
|
|
|
|
msg.setDefaultButton(btn_abbrechen)
|
|
|
|
|
|
msg.exec()
|
|
|
|
|
|
clicked = msg.clickedButton()
|
|
|
|
|
|
if clicked is btn_ueberschreiben:
|
2026-04-21 21:50:26 +02:00
|
|
|
|
for lyr in existing_layers:
|
|
|
|
|
|
project.removeMapLayer(lyr.id())
|
|
|
|
|
|
return None, LoadStatus.RELOAD
|
2026-06-23 13:54:08 +02:00
|
|
|
|
elif clicked is btn_anhaengen:
|
|
|
|
|
|
return existing_layers[0], LoadStatus.APPEND
|
2026-04-21 21:50:26 +02:00
|
|
|
|
else:
|
2026-06-23 13:54:08 +02:00
|
|
|
|
return None, LoadStatus.NONE
|
2026-04-21 21:50:26 +02:00
|
|
|
|
|
|
|
|
|
|
|
2026-09-15 10:26:25 +02:00
|
|
|
|
def _vorhandene_kennzeichen(layer) -> set:
|
2026-09-15 08:48:14 +02:00
|
|
|
|
"""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
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-06-23 13:54:08 +02:00
|
|
|
|
def lade_flurstuecke_per_gemarkung(schluessel_liste: List[str]):
|
2026-04-22 14:53:45 +02:00
|
|
|
|
"""Lädt Flurstücke für mehrere Gemarkungen, eine WFS-Anfrage pro Schlüssel.
|
2026-04-21 21:50:26 +02:00
|
|
|
|
|
2026-04-22 14:53:45 +02:00
|
|
|
|
Vermeidet das WFS-Feature-Limit einer einzelnen Abfrage mit mehreren
|
|
|
|
|
|
Gemarkungsnummern. Alle Features werden in einem Memory-Layer zusammengeführt.
|
|
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
|
----------
|
|
|
|
|
|
schluessel_liste : list of str
|
|
|
|
|
|
4-stellige Gemarkungsnummern.
|
|
|
|
|
|
|
|
|
|
|
|
Returns
|
|
|
|
|
|
-------
|
|
|
|
|
|
QgsVectorLayer or None
|
|
|
|
|
|
Memory-Layer mit allen Flurstücken, oder ``None`` wenn nichts geladen wurde.
|
|
|
|
|
|
"""
|
|
|
|
|
|
alle_feats: list = []
|
|
|
|
|
|
schema_fields = None
|
|
|
|
|
|
schema_crs = "EPSG:25833"
|
|
|
|
|
|
|
|
|
|
|
|
for schluessel in schluessel_liste:
|
|
|
|
|
|
uri = (
|
2026-04-23 13:07:35 +02:00
|
|
|
|
f"url='{ALKIS_SF_URL}' typename='{FLST_TYPENAME}' srsname='EPSG:25833' "
|
2026-04-22 14:53:45 +02:00
|
|
|
|
f"sql=SELECT * FROM AX_Flurstueck "
|
|
|
|
|
|
f"WHERE gemarkung_ax_gemarkung_schluessel_gemarkungsnummer = '{schluessel}'"
|
|
|
|
|
|
)
|
|
|
|
|
|
wfs = QgsVectorLayer(uri, f"_TmpFlst{schluessel}", "WFS")
|
|
|
|
|
|
if not wfs.isValid():
|
|
|
|
|
|
continue
|
|
|
|
|
|
feats = list(wfs.getFeatures())
|
|
|
|
|
|
if not feats:
|
|
|
|
|
|
continue
|
|
|
|
|
|
if schema_fields is None:
|
|
|
|
|
|
schema_fields = wfs.fields()
|
|
|
|
|
|
schema_crs = wfs.crs().authid()
|
|
|
|
|
|
_info(
|
|
|
|
|
|
"Flurstücke",
|
|
|
|
|
|
f"Gemarkung {schluessel}: {len(feats)} Flurstücke geladen.",
|
|
|
|
|
|
duration=3,
|
|
|
|
|
|
)
|
|
|
|
|
|
alle_feats.append((wfs.fields(), feats))
|
|
|
|
|
|
|
|
|
|
|
|
if not alle_feats or schema_fields is None:
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
# Memory-Layer mit dem Feldschema des ersten WFS-Layers aufbauen
|
|
|
|
|
|
mem = QgsVectorLayer(f"Polygon?crs={schema_crs}", FLST_LAYER_NAME, "memory")
|
|
|
|
|
|
pr = mem.dataProvider()
|
|
|
|
|
|
pr.addAttributes(list(schema_fields))
|
|
|
|
|
|
mem.updateFields()
|
|
|
|
|
|
mem_fields = mem.fields()
|
|
|
|
|
|
|
|
|
|
|
|
neue_features = []
|
|
|
|
|
|
for wfs_fields, feats in alle_feats:
|
|
|
|
|
|
for feat in feats:
|
|
|
|
|
|
new_f = QgsFeature(mem_fields)
|
|
|
|
|
|
new_f.setGeometry(feat.geometry())
|
|
|
|
|
|
for i in range(wfs_fields.count()):
|
|
|
|
|
|
fname = wfs_fields.field(i).name()
|
|
|
|
|
|
idx = mem_fields.indexFromName(fname)
|
|
|
|
|
|
if idx >= 0:
|
|
|
|
|
|
try:
|
|
|
|
|
|
new_f.setAttribute(idx, feat[fname])
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
neue_features.append(new_f)
|
|
|
|
|
|
|
|
|
|
|
|
pr.addFeatures(neue_features)
|
|
|
|
|
|
mem.updateExtents()
|
|
|
|
|
|
return mem
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def flurstueck_alkis(
|
|
|
|
|
|
tab_widget,
|
|
|
|
|
|
vorgabe_schluessel: Optional[List[str]] = None,
|
|
|
|
|
|
vorgabe_namen: Optional[List[str]] = None,
|
|
|
|
|
|
bbox=None,
|
|
|
|
|
|
ohne_dialog: bool = False,
|
2026-09-15 10:26:25 +02:00
|
|
|
|
ohne_schluessel_abfrage: bool = False,
|
2026-04-22 14:53:45 +02:00
|
|
|
|
) -> Tuple[Optional[QgsVectorLayer], LoadStatus]:
|
|
|
|
|
|
"""Lädt Flurstücke aus dem ALKIS-WFS anhand von Gemarkungsschlüsseln.
|
|
|
|
|
|
|
|
|
|
|
|
Jede Gemarkungsnummer wird als separate WFS-Anfrage gestellt, um das
|
|
|
|
|
|
WFS-Feature-Limit bei einer Gesamtabfrage zu umgehen. Alle Ergebnisse
|
|
|
|
|
|
werden in einem Memory-Layer zusammengeführt.
|
2026-04-21 21:50:26 +02:00
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
|
----------
|
|
|
|
|
|
tab_widget :
|
|
|
|
|
|
Eltern-Widget für Dialoge.
|
2026-04-22 14:53:45 +02:00
|
|
|
|
vorgabe_schluessel : list of str, optional
|
|
|
|
|
|
Vorausgefüllte Gemarkungsnummern (4-stellig). Im Dialog als Standard
|
2026-09-15 10:26:25 +02:00
|
|
|
|
angezeigt; bei ``ohne_dialog=True`` oder ``ohne_schluessel_abfrage=True``
|
|
|
|
|
|
direkt verwendet.
|
2026-04-22 14:53:45 +02:00
|
|
|
|
vorgabe_namen : list of str, optional
|
|
|
|
|
|
Menschenlesbare Gemarkungsbezeichnungen für den Dialog-Hinweistext.
|
|
|
|
|
|
bbox :
|
|
|
|
|
|
Nicht mehr verwendet (WFS unterstützt keinen BBox-Filter). Bleibt
|
|
|
|
|
|
für Rückwärtskompatibilität in der Signatur erhalten.
|
|
|
|
|
|
ohne_dialog : bool, optional
|
2026-09-15 10:26:25 +02:00
|
|
|
|
``True``: Überspringt sowohl die Gemarkungsschlüssel-Nutzerabfrage (wie
|
|
|
|
|
|
``ohne_schluessel_abfrage``) als auch die Rückfrage bei einem bereits
|
|
|
|
|
|
vorhandenen Layer – dieser wird dann immer ersetzt. Gedacht für die
|
|
|
|
|
|
automatischen Pipelines (``verfahrensgebiet_alkis_komplett``,
|
|
|
|
|
|
``verfahrensgebiet_shape_komplett``). Standard: ``False``.
|
|
|
|
|
|
ohne_schluessel_abfrage : bool, optional
|
|
|
|
|
|
``True``: Überspringt nur die Gemarkungsschlüssel-Nutzerabfrage und
|
|
|
|
|
|
verwendet ``vorgabe_schluessel`` direkt; die Rückfrage bei einem
|
|
|
|
|
|
bereits vorhandenen Layer (Überschreiben/Anhängen/Abbrechen, inkl.
|
|
|
|
|
|
Dedup beim Anhängen) bleibt aktiv. Für Aufrufer, die die Schlüssel
|
|
|
|
|
|
bereits anderweitig ermittelt haben (z. B. aus einer Layer-Auswahl),
|
|
|
|
|
|
aber trotzdem interaktiv mit einem bestehenden Layer umgehen wollen.
|
|
|
|
|
|
Wirkungslos, wenn ``ohne_dialog=True`` gesetzt ist. Standard: ``False``.
|
2026-04-21 21:50:26 +02:00
|
|
|
|
|
|
|
|
|
|
Returns
|
|
|
|
|
|
-------
|
|
|
|
|
|
Tuple[Optional[QgsVectorLayer], LoadStatus]
|
2026-04-22 14:53:45 +02:00
|
|
|
|
Memory-Layer mit allen Flurstücken und Ladestatus.
|
|
|
|
|
|
Bei Fehlern ist der Layer ``None`` und der Status :attr:`LoadStatus.NONE`.
|
2026-04-21 21:50:26 +02:00
|
|
|
|
"""
|
2026-04-22 14:53:45 +02:00
|
|
|
|
# --- Schritt 1: Vorhandenen Layer behandeln ----------------------------
|
2026-06-23 13:54:08 +02:00
|
|
|
|
append_target = None
|
2026-04-22 14:53:45 +02:00
|
|
|
|
if ohne_dialog:
|
|
|
|
|
|
project = QgsProject.instance()
|
|
|
|
|
|
hatte_layer = bool(project.mapLayersByName(FLST_LAYER_NAME))
|
|
|
|
|
|
for lyr in list(project.mapLayersByName(FLST_LAYER_NAME)):
|
|
|
|
|
|
project.removeMapLayer(lyr.id())
|
|
|
|
|
|
status = LoadStatus.RELOAD if hatte_layer else LoadStatus.FIRST
|
|
|
|
|
|
else:
|
|
|
|
|
|
existing, status = _check_existing_flurstueck_layer(tab_widget)
|
|
|
|
|
|
if status == LoadStatus.KEEP:
|
|
|
|
|
|
return existing, status
|
2026-06-23 13:54:08 +02:00
|
|
|
|
if status == LoadStatus.NONE:
|
|
|
|
|
|
return None, LoadStatus.NONE
|
|
|
|
|
|
if status == LoadStatus.APPEND:
|
|
|
|
|
|
append_target = existing
|
2026-04-21 21:50:26 +02:00
|
|
|
|
|
2026-04-22 14:53:45 +02:00
|
|
|
|
# --- Schritt 2: Gemarkungsschlüssel bestimmen -------------------------
|
2026-09-15 10:26:25 +02:00
|
|
|
|
if ohne_dialog or ohne_schluessel_abfrage:
|
|
|
|
|
|
# vorgabe_schluessel direkt verwenden, kein ask_text-Dialog
|
2026-04-22 14:53:45 +02:00
|
|
|
|
if not vorgabe_schluessel:
|
|
|
|
|
|
return None, LoadStatus.NONE
|
|
|
|
|
|
schluessel_liste = [s.strip() for s in vorgabe_schluessel if s.strip()]
|
|
|
|
|
|
if not schluessel_liste:
|
|
|
|
|
|
return None, LoadStatus.NONE
|
|
|
|
|
|
else:
|
|
|
|
|
|
# Interaktiver Modus: Dialog mit Vorschlag
|
|
|
|
|
|
default_text = ", ".join(vorgabe_schluessel) if vorgabe_schluessel else ""
|
|
|
|
|
|
hinweis = ""
|
|
|
|
|
|
if vorgabe_namen:
|
|
|
|
|
|
hinweis = " (Vorschlag: " + ", ".join(vorgabe_namen) + ")"
|
|
|
|
|
|
label = f"Gemarkungsschlüssel eingeben (mehrere mit Komma trennen):{hinweis}"
|
|
|
|
|
|
|
|
|
|
|
|
eingabe, ok = ask_text(
|
|
|
|
|
|
"Gemarkungsschlüssel",
|
|
|
|
|
|
label,
|
|
|
|
|
|
default_text=default_text,
|
|
|
|
|
|
parent=tab_widget,
|
2026-04-21 21:50:26 +02:00
|
|
|
|
)
|
2026-04-22 14:53:45 +02:00
|
|
|
|
if not ok or not eingabe.strip():
|
|
|
|
|
|
QMessageBox.warning(
|
|
|
|
|
|
tab_widget,
|
|
|
|
|
|
"Abgebrochen",
|
|
|
|
|
|
"Bitte mindestens einen gültigen Gemarkungsschlüssel eingeben.",
|
|
|
|
|
|
)
|
|
|
|
|
|
return None, LoadStatus.NONE
|
2026-04-21 21:50:26 +02:00
|
|
|
|
|
2026-04-22 14:53:45 +02:00
|
|
|
|
schluessel_liste = [s.strip() for s in eingabe.split(",") if s.strip()]
|
|
|
|
|
|
if not schluessel_liste:
|
|
|
|
|
|
QMessageBox.warning(tab_widget, "Fehler", "Keine gültigen Schlüssel gefunden.")
|
|
|
|
|
|
return None, LoadStatus.NONE
|
2026-04-21 21:50:26 +02:00
|
|
|
|
|
2026-04-22 14:53:45 +02:00
|
|
|
|
# --- Schritt 3: Flurstücke gemarkungsweise laden ----------------------
|
2026-06-23 13:54:08 +02:00
|
|
|
|
layer = lade_flurstuecke_per_gemarkung(schluessel_liste)
|
2026-04-21 21:50:26 +02:00
|
|
|
|
|
2026-04-22 14:53:45 +02:00
|
|
|
|
if not layer or not layer.isValid() or layer.featureCount() == 0:
|
|
|
|
|
|
if not ohne_dialog:
|
|
|
|
|
|
QMessageBox.critical(
|
|
|
|
|
|
tab_widget,
|
|
|
|
|
|
"Fehler",
|
|
|
|
|
|
"Flurstück-Layer konnte nicht geladen werden.\n"
|
|
|
|
|
|
"Bitte Gemarkungsschlüssel und Netzwerkverbindung prüfen.",
|
|
|
|
|
|
)
|
2026-04-21 21:50:26 +02:00
|
|
|
|
return None, LoadStatus.NONE
|
|
|
|
|
|
|
2026-06-23 13:54:08 +02:00
|
|
|
|
# --- Anhängen an vorhandenen Layer ------------------------------------
|
|
|
|
|
|
if status == LoadStatus.APPEND and append_target is not None:
|
2026-09-15 08:48:14 +02:00
|
|
|
|
alle_feats = list(layer.getFeatures())
|
|
|
|
|
|
neue_feats = alle_feats
|
|
|
|
|
|
if layer.fields().indexFromName(FELD_FLURSTUECKSKENNZEICHEN) >= 0:
|
2026-09-15 10:26:25 +02:00
|
|
|
|
vorhandene = _vorhandene_kennzeichen(append_target)
|
2026-09-15 08:48:14 +02:00
|
|
|
|
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)
|
2026-06-23 13:54:08 +02:00
|
|
|
|
append_target.updateExtents()
|
2026-06-23 13:55:58 +02:00
|
|
|
|
append_target.triggerRepaint()
|
2026-06-23 13:54:08 +02:00
|
|
|
|
return append_target, LoadStatus.APPEND
|
|
|
|
|
|
|
2026-04-21 21:50:26 +02:00
|
|
|
|
return layer, status
|