2026-07-15 15:29:48 +02:00
|
|
|
|
"""sn_verfahrensgebiet/functions/verfahrensgebiet_shape_komplett.py – VG-Lade-Workflow aus Shape (ZIP).
|
|
|
|
|
|
|
|
|
|
|
|
Orchestriert den vollständigen Arbeitsablauf zum Laden eines Verfahrensgebiets
|
|
|
|
|
|
aus einem ZIP-Archiv mit Shapefile-Set:
|
|
|
|
|
|
|
|
|
|
|
|
1. Dateidialog öffnen (.zip-Filter); Nutzerabbruch beendet den Vorgang.
|
|
|
|
|
|
2. ZIP neben der ZIP-Datei entpacken und auf ein gültiges Shapefile-Set prüfen.
|
|
|
|
|
|
3. Shapefile als QGIS-Vektorlayer laden und validieren (Standard-QGIS-Routinen).
|
|
|
|
|
|
4. Alle Features per ``QgsGeometry.unaryUnion`` zu einem Single-Feature-
|
|
|
|
|
|
Memory-Layer „Verfahrensgebiet" verschmelzen.
|
|
|
|
|
|
5. Gemarkungsschlüssel per Geometrie aus ``AX_Gemarkung`` vorschlagen.
|
|
|
|
|
|
6. Flurstücke per Dialog (mit Vorschlag) laden.
|
|
|
|
|
|
7. Flurstücke räumlich auf das Shape-VG einschränken (``selectByLocation``).
|
|
|
|
|
|
8. Temporären enriched Layer aus Auswahl für den Flächenvergleich erzeugen
|
|
|
|
|
|
(wird **nicht** ins Projekt eingefügt und **nicht** zurückgegeben).
|
|
|
|
|
|
9. Flächenvergleich zwischen Shape-VG und temporärem enriched Layer.
|
|
|
|
|
|
|
|
|
|
|
|
- **Übereinstimmung** (≤ Toleranz): modale OK-Meldung, Ablauf beendet.
|
|
|
|
|
|
- **Abweichung**: 2-Optionen-Dialog (Detailprüfung / Abbrechen).
|
|
|
|
|
|
|
|
|
|
|
|
10. Bei Wahl „Detailprüfung" (knickpunktbasiert):
|
|
|
|
|
|
|
|
|
|
|
|
- Grenzpunkte (AX_PunktortTA) per BBox laden.
|
|
|
|
|
|
- Knickpunkte des Shape-VG mit PunktortTA vergleichen (grün/rot/orange).
|
|
|
|
|
|
- Korrigiertes Polygon aus identischen + gefundenen PunktortTA ableiten.
|
|
|
|
|
|
- Ergebnislayer (Punkte), Verbindungslinien und Polygon ins Projekt einfügen.
|
|
|
|
|
|
- Abschlussmeldung mit Zählung.
|
|
|
|
|
|
|
|
|
|
|
|
Das Shape-VG (``Verfahrensgebiet``) ist das finale Rückgabe-Layer. Ein
|
|
|
|
|
|
``enriched Layer aus Flurstücken`` wird nur temporär für den Flächenvergleich
|
|
|
|
|
|
erzeugt, aber weder ins Projekt eingefügt noch zurückgegeben.
|
|
|
|
|
|
|
|
|
|
|
|
**Rückgabe:**
|
|
|
|
|
|
``Tuple[shape_vg_layer, flst_layer, None, LoadStatus]``
|
|
|
|
|
|
|
|
|
|
|
|
**Abbruchverhalten:** Jeder Schritt, der :attr:`LoadStatus.NONE` zurückgibt
|
|
|
|
|
|
oder kein Ergebnis liefert, bricht den Workflow ab.
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
import os
|
|
|
|
|
|
import zipfile
|
|
|
|
|
|
from typing import Optional, Tuple
|
|
|
|
|
|
|
2026-07-15 15:39:21 +02:00
|
|
|
|
from sn_basis.functions import QgsProject, QgsVectorLayer, QgsFeature, QgsField
|
2026-07-15 15:29:48 +02:00
|
|
|
|
from sn_basis.functions import QMessageBox
|
|
|
|
|
|
from sn_basis.functions.message_wrapper import warning
|
|
|
|
|
|
from sn_basis.functions.variable_wrapper import get_variable
|
2026-07-15 15:39:21 +02:00
|
|
|
|
from sn_basis.functions.qt_wrapper import QFileDialog, QVariant
|
2026-07-15 15:29:48 +02:00
|
|
|
|
from sn_basis.functions.qgiscore_wrapper import QgsGeometry
|
|
|
|
|
|
from sn_basis.functions.dialog_wrapper import (
|
|
|
|
|
|
create_progress_dialog,
|
|
|
|
|
|
show_info_dialog,
|
|
|
|
|
|
ask_detailpruefung_oder_abbrechen,
|
2026-08-31 08:59:33 +02:00
|
|
|
|
ask_yes_no,
|
2026-07-15 15:29:48 +02:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
from sn_verfahrensgebiet.functions.verfahrensgebiet_alkis import LoadStatus, verfahrensgebiet_alkis
|
|
|
|
|
|
from sn_verfahrensgebiet.functions.gemarkung_alkis import (
|
|
|
|
|
|
gemarkung_schluessel_und_namen_von_vg_geometrie,
|
|
|
|
|
|
)
|
|
|
|
|
|
from sn_verfahrensgebiet.functions.flurstueck_alkis import flurstueck_alkis
|
|
|
|
|
|
from sn_verfahrensgebiet.functions.verfahrensgebiet_edit_service import (
|
|
|
|
|
|
erstelle_verfahrensgebiet_aus_auswahl,
|
|
|
|
|
|
VERF_LAYER_NAME,
|
|
|
|
|
|
)
|
|
|
|
|
|
from sn_verfahrensgebiet.functions.grenzpunkt_alkis import lade_punktort_ta
|
|
|
|
|
|
from sn_verfahrensgebiet.functions.knickpunkt_pruefung import (
|
|
|
|
|
|
knickpunkt_pruefung,
|
|
|
|
|
|
_extrahiere_knickpunkte,
|
|
|
|
|
|
erstelle_korrigiertes_polygon,
|
|
|
|
|
|
)
|
|
|
|
|
|
from sn_verfahrensgebiet.functions.verfahrensgebiet_alkis_komplett import (
|
|
|
|
|
|
_select_by_location,
|
|
|
|
|
|
_vergleiche_flaechen,
|
|
|
|
|
|
_lese_flaechen_toleranz,
|
|
|
|
|
|
_entferne_hilfs_layer,
|
|
|
|
|
|
MINDEST_FLAECHEN_TOLERANZ_M2,
|
|
|
|
|
|
BROR_LAYER_NAME,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# Interne Hilfsfunktionen
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
2026-07-15 15:39:21 +02:00
|
|
|
|
def _wende_vergleich_stil_an(layer) -> None:
|
|
|
|
|
|
"""Wendet einen kategorisierten Renderer auf den Vergleich-Layer an.
|
|
|
|
|
|
|
|
|
|
|
|
Farben:
|
|
|
|
|
|
- ``identisch`` (Schnittmenge): Gr\u00fcn
|
|
|
|
|
|
- ``nur_bror`` (nur im BROR): Gelb
|
|
|
|
|
|
- ``nur_vg`` (nur im Verfahrensgebiet): Blau
|
|
|
|
|
|
|
|
|
|
|
|
Schl\u00e4gt der Import der QGIS-Rendering-Klassen fehl (z.\u00a0B. im
|
|
|
|
|
|
Test-Modus), wird der Fehler still ignoriert.
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
from qgis.core import ( # type: ignore[import]
|
|
|
|
|
|
QgsCategorizedSymbolRenderer,
|
|
|
|
|
|
QgsRendererCategory,
|
|
|
|
|
|
QgsFillSymbol,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
_FARBEN = [
|
|
|
|
|
|
("identisch", "#00aa00", 0.5), # gr\u00fcn
|
|
|
|
|
|
("nur_bror", "#ffcc00", 0.5), # gelb
|
|
|
|
|
|
("nur_vg", "#3366ff", 0.5), # blau
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
kategorien = []
|
|
|
|
|
|
for wert, farbe, opazitaet in _FARBEN:
|
|
|
|
|
|
symbol = QgsFillSymbol.createSimple({
|
|
|
|
|
|
"color": farbe,
|
|
|
|
|
|
"outline_color": "#555555",
|
|
|
|
|
|
"outline_width": "0.26",
|
|
|
|
|
|
})
|
|
|
|
|
|
symbol.setOpacity(opazitaet)
|
|
|
|
|
|
kategorien.append(QgsRendererCategory(wert, symbol, wert))
|
|
|
|
|
|
|
|
|
|
|
|
renderer = QgsCategorizedSymbolRenderer("typ", kategorien)
|
|
|
|
|
|
layer.setRenderer(renderer)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _erstelle_vergleich_layer(vg_layer, bror_layer) -> Optional[object]:
|
|
|
|
|
|
"""Erzeugt einen Vergleich-Layer aus geometrischen Differenzoperationen.
|
|
|
|
|
|
|
|
|
|
|
|
F\u00fcr jeden Fl\u00e4chentyp wird ein Feature angelegt:
|
|
|
|
|
|
|
|
|
|
|
|
- ``identisch``: Schnittmenge aus VG und BROR (gr\u00fcn).
|
|
|
|
|
|
- ``nur_bror``: Fl\u00e4che, die nur im BROR-Objekt liegt (gelb).
|
|
|
|
|
|
- ``nur_vg``: Fl\u00e4che, die nur im Verfahrensgebiet liegt (blau).
|
|
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
|
----------
|
|
|
|
|
|
vg_layer :
|
|
|
|
|
|
Verfahrensgebiet-Layer (Shape).
|
|
|
|
|
|
bror_layer :
|
|
|
|
|
|
BauRaumOderBodenordnungsrecht-Layer (ALKIS).
|
|
|
|
|
|
|
|
|
|
|
|
Returns
|
|
|
|
|
|
-------
|
|
|
|
|
|
Optional[QgsVectorLayer]
|
|
|
|
|
|
Memory-Layer ``Vergleich`` mit Stil oder ``None`` bei Fehler.
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
vg_geom = None
|
|
|
|
|
|
for feat in vg_layer.getFeatures():
|
|
|
|
|
|
g = feat.geometry()
|
|
|
|
|
|
if g and not g.isEmpty():
|
|
|
|
|
|
vg_geom = g
|
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
|
|
bror_geom = None
|
|
|
|
|
|
for feat in bror_layer.getFeatures():
|
|
|
|
|
|
g = feat.geometry()
|
|
|
|
|
|
if g and not g.isEmpty():
|
|
|
|
|
|
bror_geom = g
|
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
|
|
if not vg_geom or not bror_geom:
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
geom_identisch = vg_geom.intersection(bror_geom)
|
|
|
|
|
|
geom_nur_bror = bror_geom.difference(vg_geom)
|
|
|
|
|
|
geom_nur_vg = vg_geom.difference(bror_geom)
|
|
|
|
|
|
|
|
|
|
|
|
crs = vg_layer.crs().authid()
|
|
|
|
|
|
mem_layer = QgsVectorLayer(
|
|
|
|
|
|
f"MultiPolygon?crs={crs}", "Vergleich", "memory"
|
|
|
|
|
|
)
|
|
|
|
|
|
pr = mem_layer.dataProvider()
|
|
|
|
|
|
pr.addAttributes([QgsField("typ", QVariant.String)])
|
|
|
|
|
|
mem_layer.updateFields()
|
|
|
|
|
|
|
|
|
|
|
|
feats = []
|
|
|
|
|
|
for geom, typ in [
|
|
|
|
|
|
(geom_identisch, "identisch"),
|
|
|
|
|
|
(geom_nur_bror, "nur_bror"),
|
|
|
|
|
|
(geom_nur_vg, "nur_vg"),
|
|
|
|
|
|
]:
|
|
|
|
|
|
if geom and not geom.isEmpty():
|
|
|
|
|
|
f = QgsFeature()
|
|
|
|
|
|
f.setGeometry(geom)
|
|
|
|
|
|
f.setAttributes([typ])
|
|
|
|
|
|
feats.append(f)
|
|
|
|
|
|
|
|
|
|
|
|
pr.addFeatures(feats)
|
|
|
|
|
|
mem_layer.updateExtents()
|
|
|
|
|
|
_wende_vergleich_stil_an(mem_layer)
|
|
|
|
|
|
|
|
|
|
|
|
return mem_layer if mem_layer.isValid() else None
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
2026-07-15 15:29:48 +02:00
|
|
|
|
def _finde_shp(verzeichnis: str) -> Optional[str]:
|
|
|
|
|
|
"""Sucht rekursiv nach der ersten ``.shp``-Datei im Verzeichnis.
|
|
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
|
----------
|
|
|
|
|
|
verzeichnis :
|
|
|
|
|
|
Startverzeichnis für die Suche.
|
|
|
|
|
|
|
|
|
|
|
|
Returns
|
|
|
|
|
|
-------
|
|
|
|
|
|
Optional[str]
|
|
|
|
|
|
Absoluter Pfad zur ersten gefundenen ``.shp``-Datei, oder ``None``.
|
|
|
|
|
|
"""
|
|
|
|
|
|
for root, _dirs, files in os.walk(verzeichnis):
|
|
|
|
|
|
for fname in files:
|
|
|
|
|
|
if fname.lower().endswith(".shp"):
|
|
|
|
|
|
return os.path.join(root, fname)
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _erstelle_memory_layer_aus_shp(shp_layer) -> Optional[object]:
|
|
|
|
|
|
"""Vereint alle Features des Shapefile-Layers zu einem Single-Feature-
|
|
|
|
|
|
Memory-Layer „Verfahrensgebiet" via ``QgsGeometry.unaryUnion``.
|
|
|
|
|
|
|
|
|
|
|
|
Alle Felder des Shapefiles werden in den Memory-Layer übernommen.
|
|
|
|
|
|
Die Attributwerte des ersten Features mit gültiger Geometrie werden
|
|
|
|
|
|
auf das Union-Feature gesetzt.
|
|
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
|
----------
|
|
|
|
|
|
shp_layer :
|
|
|
|
|
|
Geladener ``QgsVectorLayer`` aus dem Shapefile.
|
|
|
|
|
|
|
|
|
|
|
|
Returns
|
|
|
|
|
|
-------
|
|
|
|
|
|
Optional[QgsVectorLayer]
|
|
|
|
|
|
Single-Feature-Memory-Layer mit Union-Geometrie und Shapefile-Attributen
|
|
|
|
|
|
oder ``None`` bei Fehler.
|
|
|
|
|
|
"""
|
|
|
|
|
|
geom_list = []
|
|
|
|
|
|
first_feat = None
|
|
|
|
|
|
|
|
|
|
|
|
for feat in shp_layer.getFeatures():
|
|
|
|
|
|
try:
|
|
|
|
|
|
geom = feat.geometry()
|
|
|
|
|
|
if geom and not geom.isEmpty():
|
|
|
|
|
|
geom_list.append(geom)
|
|
|
|
|
|
if first_feat is None:
|
|
|
|
|
|
first_feat = feat
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
if not geom_list:
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
union_geom = QgsGeometry.unaryUnion(geom_list)
|
|
|
|
|
|
if not union_geom or union_geom.isEmpty():
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
crs_authid = shp_layer.crs().authid()
|
|
|
|
|
|
mem_layer = QgsVectorLayer(
|
|
|
|
|
|
f"MultiPolygon?crs={crs_authid}", VERF_LAYER_NAME, "memory"
|
|
|
|
|
|
)
|
|
|
|
|
|
pr = mem_layer.dataProvider()
|
|
|
|
|
|
|
|
|
|
|
|
# Felder des Shapefiles übernehmen
|
|
|
|
|
|
try:
|
|
|
|
|
|
pr.addAttributes(list(shp_layer.fields()))
|
|
|
|
|
|
mem_layer.updateFields()
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
feat_out = QgsFeature(mem_layer.fields())
|
|
|
|
|
|
feat_out.setGeometry(union_geom)
|
|
|
|
|
|
|
|
|
|
|
|
# Attributwerte vom ersten Feature übernehmen
|
|
|
|
|
|
if first_feat is not None:
|
|
|
|
|
|
try:
|
|
|
|
|
|
feat_out.setAttributes(first_feat.attributes())
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
pr.addFeatures([feat_out])
|
|
|
|
|
|
mem_layer.updateExtents()
|
|
|
|
|
|
|
|
|
|
|
|
if not mem_layer.isValid() or mem_layer.featureCount() == 0:
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
return mem_layer
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# Öffentliche Lade-Hilfsfunktion
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
def lade_vg_aus_zip(parent_widget) -> Optional[object]:
|
|
|
|
|
|
"""Öffnet Dateidialog, entpackt ZIP und gibt Union-Memory-Layer zurück.
|
|
|
|
|
|
|
|
|
|
|
|
Schrittfolge:
|
|
|
|
|
|
|
|
|
|
|
|
1. ``QFileDialog.getOpenFileName`` mit Filter ``ZIP-Dateien (*.zip)``.
|
|
|
|
|
|
2. ZIP neben der Quelldatei in ein Unterverzeichnis entpacken.
|
|
|
|
|
|
3. Erste ``.shp``-Datei im Verzeichnis finden.
|
|
|
|
|
|
4. Als ``QgsVectorLayer`` laden und ``isValid()`` prüfen.
|
|
|
|
|
|
5. Alle Features per ``QgsGeometry.unaryUnion`` verschmelzen.
|
|
|
|
|
|
6. Single-Feature-Memory-Layer „Verfahrensgebiet" zurückgeben.
|
|
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
|
----------
|
|
|
|
|
|
parent_widget :
|
|
|
|
|
|
Eltern-Widget für Dateidialog und Fehlermeldungen.
|
|
|
|
|
|
|
|
|
|
|
|
Returns
|
|
|
|
|
|
-------
|
|
|
|
|
|
Optional[QgsVectorLayer]
|
|
|
|
|
|
Valider Memory-Layer „Verfahrensgebiet" oder ``None`` bei Abbruch/Fehler.
|
|
|
|
|
|
"""
|
|
|
|
|
|
pfad, _ = QFileDialog.getOpenFileName(
|
|
|
|
|
|
parent_widget,
|
|
|
|
|
|
"ZIP-Datei mit Shapefile-Set auswählen",
|
|
|
|
|
|
"",
|
|
|
|
|
|
"ZIP-Dateien (*.zip)",
|
|
|
|
|
|
)
|
|
|
|
|
|
if not pfad:
|
|
|
|
|
|
return None # Nutzerabbruch
|
|
|
|
|
|
|
|
|
|
|
|
zip_dir = os.path.dirname(pfad)
|
|
|
|
|
|
zip_base = os.path.splitext(os.path.basename(pfad))[0]
|
|
|
|
|
|
extract_dir = os.path.join(zip_dir, zip_base)
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
with zipfile.ZipFile(pfad, "r") as zf:
|
|
|
|
|
|
zf.extractall(extract_dir)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
QMessageBox.warning(
|
|
|
|
|
|
parent_widget,
|
|
|
|
|
|
"Fehler",
|
|
|
|
|
|
"Die ZIP-Datei konnte nicht entpackt werden.\n"
|
|
|
|
|
|
"Bitte eine gültige ZIP-Datei auswählen.",
|
|
|
|
|
|
)
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
shp_path = _finde_shp(extract_dir)
|
|
|
|
|
|
if not shp_path:
|
|
|
|
|
|
QMessageBox.warning(
|
|
|
|
|
|
parent_widget,
|
|
|
|
|
|
"Fehler",
|
|
|
|
|
|
"gültiges Shape-Dateiset angeben",
|
|
|
|
|
|
)
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
raw_layer = QgsVectorLayer(shp_path, "tmp_shape_vg", "ogr")
|
|
|
|
|
|
if not raw_layer or not raw_layer.isValid():
|
|
|
|
|
|
QMessageBox.warning(
|
|
|
|
|
|
parent_widget,
|
|
|
|
|
|
"Fehler",
|
|
|
|
|
|
"gültiges Shape-Dateiset angeben",
|
|
|
|
|
|
)
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
mem_layer = _erstelle_memory_layer_aus_shp(raw_layer)
|
|
|
|
|
|
if mem_layer is None:
|
|
|
|
|
|
QMessageBox.warning(
|
|
|
|
|
|
parent_widget,
|
|
|
|
|
|
"Fehler",
|
|
|
|
|
|
"gültiges Shape-Dateiset angeben",
|
|
|
|
|
|
)
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
return mem_layer
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# Öffentliche API
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
def verfahrensgebiet_shape_komplett(
|
|
|
|
|
|
tab_widget,
|
|
|
|
|
|
) -> Tuple[Optional[object], Optional[object], Optional[object], LoadStatus]:
|
|
|
|
|
|
"""Vollständiger VG-Lade-Workflow aus Shape (ZIP): ZIP → BROR → Gemarkungen →
|
|
|
|
|
|
Flurstücke → Verschneidung → Flächenvergleich → opt. Umringsprüfung.
|
|
|
|
|
|
|
|
|
|
|
|
Das aus der ZIP geladene Shape-VG ist das finale Verfahrensgebiet-Layer.
|
|
|
|
|
|
Ist die Projektvariable ``verfahrensnummer`` gesetzt, wird das zugehörige
|
|
|
|
|
|
BROR-Objekt (``AX_BauRaumOderBodenordnungsrecht``) aus ALKIS geladen und
|
|
|
|
|
|
als Vergleichsobjekt im Flächenvergleich verwendet. Andernfalls wird ein
|
|
|
|
|
|
temporärer enriched Layer aus Flurstücken für den Vergleich herangezogen.
|
|
|
|
|
|
|
|
|
|
|
|
Schrittfolge:
|
|
|
|
|
|
|
|
|
|
|
|
1. ``lade_vg_aus_zip(tab_widget)`` – ZIP öffnen, Shapefile validieren,
|
|
|
|
|
|
Union-Memory-Layer „Verfahrensgebiet" erzeugen.
|
|
|
|
|
|
2. ``verfahrensgebiet_alkis(tab_widget, skip_existing_check=True)`` –
|
|
|
|
|
|
BROR-Objekt als Memory-Layer (sofern VKZ gesetzt).
|
|
|
|
|
|
Der BROR wird **vor** dem Shape-VG ins Projekt eingefügt, um den
|
|
|
|
|
|
Namenskonflikt mit ``_check_existing_layer`` zu vermeiden.
|
|
|
|
|
|
3. Shape-VG ins Projekt einfügen; BROR umbenennen zu
|
|
|
|
|
|
``BauRaumOderBodenordnungsrecht (ALKIS)`` und zum Projekt hinzufügen.
|
|
|
|
|
|
4. ``gemarkung_schluessel_und_namen_von_vg_geometrie(geom)`` – Schlüssel.
|
|
|
|
|
|
5. ``flurstueck_alkis(...)`` – Flurstücke laden.
|
|
|
|
|
|
6. ``_select_by_location(...)`` – räumliche Selektion.
|
|
|
|
|
|
7. Flächenvergleich:
|
|
|
|
|
|
|
|
|
|
|
|
- Mit BROR: ``_vergleiche_flaechen(bror_layer, shape_vg_layer, ...)``.
|
|
|
|
|
|
- Ohne BROR: temp. enriched Layer erzeugen und mit Shape-VG vergleichen.
|
|
|
|
|
|
|
|
|
|
|
|
8. Ggf. Detailprüfung (knickpunktbasiert) auf BROR bzw. Shape-VG.
|
|
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
|
----------
|
|
|
|
|
|
tab_widget :
|
|
|
|
|
|
Eltern-Widget für alle Dialoge.
|
|
|
|
|
|
|
|
|
|
|
|
Returns
|
|
|
|
|
|
-------
|
|
|
|
|
|
Tuple[Optional[shape_vg_layer], Optional[flst_layer], Optional[bror_layer], LoadStatus]
|
|
|
|
|
|
- ``shape_vg_layer``: Memory-Layer „Verfahrensgebiet" (aus Shape).
|
|
|
|
|
|
- ``flst_layer``: Flurstücke-Layer (bleibt im Projekt).
|
|
|
|
|
|
- ``bror_layer``: BROR-Layer ``BauRaumOderBodenordnungsrecht (ALKIS)``
|
|
|
|
|
|
(wenn geladen, bleibt im Projekt); ``None`` wenn VKZ nicht gesetzt.
|
|
|
|
|
|
- :class:`LoadStatus`: ``FIRST`` oder ``NONE``.
|
|
|
|
|
|
"""
|
|
|
|
|
|
project = QgsProject.instance()
|
|
|
|
|
|
shape_vg_layer = None
|
|
|
|
|
|
bror_layer = None
|
|
|
|
|
|
shape_was_added_by_us = False
|
|
|
|
|
|
bror_was_added_by_us = False
|
|
|
|
|
|
abgeschlossen = False
|
|
|
|
|
|
|
|
|
|
|
|
progress = create_progress_dialog(
|
|
|
|
|
|
7, "Verfahrensgebiet aus Shape laden", "Initialisierung …"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
|
|
|
|
# Schritt 1: Shape-VG aus ZIP laden (noch NICHT ins Projekt einfügen)
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
|
|
|
|
progress.set_label("Schritt 1/6: Verfahrensgebiet aus ZIP laden …")
|
|
|
|
|
|
progress.set_value(0)
|
|
|
|
|
|
if progress.is_canceled():
|
|
|
|
|
|
return None, None, None, LoadStatus.NONE
|
|
|
|
|
|
|
|
|
|
|
|
shape_vg_layer = lade_vg_aus_zip(tab_widget)
|
|
|
|
|
|
progress.raise_to_front()
|
|
|
|
|
|
|
|
|
|
|
|
if not shape_vg_layer:
|
|
|
|
|
|
return None, None, None, LoadStatus.NONE
|
|
|
|
|
|
|
2026-08-31 08:59:33 +02:00
|
|
|
|
# TEMPORÄRER STOPP (wieder entfernen): Pause nach "Verfahrensgebiet
|
|
|
|
|
|
# aus ZIP laden" zur Zwischenkontrolle – Layer dafür schon jetzt
|
|
|
|
|
|
# sichtbar ins Projekt einfügen (normalerweise erst nach Schritt 2).
|
|
|
|
|
|
project.addMapLayer(shape_vg_layer)
|
|
|
|
|
|
shape_was_added_by_us = True
|
|
|
|
|
|
if not ask_yes_no(
|
|
|
|
|
|
"Verfahrensgebiet aus ZIP laden",
|
|
|
|
|
|
"Verfahrensgebiet aus ZIP wurde geladen. Weiter?",
|
|
|
|
|
|
default=True,
|
|
|
|
|
|
parent=tab_widget,
|
|
|
|
|
|
):
|
|
|
|
|
|
# abgeschlossen=True, damit der finally-Block den zur Kontrolle
|
|
|
|
|
|
# eingefügten Layer NICHT wieder entfernt (kein Fehlerfall).
|
|
|
|
|
|
abgeschlossen = True
|
|
|
|
|
|
return None, None, None, LoadStatus.NONE
|
|
|
|
|
|
|
2026-07-15 15:29:48 +02:00
|
|
|
|
vg_status = LoadStatus.FIRST
|
|
|
|
|
|
progress.set_value(1)
|
|
|
|
|
|
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
|
|
|
|
# Schritt 2: BROR aus ALKIS laden (vor shape_vg, um Namenskonflikt
|
|
|
|
|
|
# in _check_existing_layer zu vermeiden)
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
|
|
|
|
progress.set_label("Schritt 2/6: BROR aus ALKIS laden …")
|
|
|
|
|
|
if progress.is_canceled():
|
|
|
|
|
|
return None, None, None, LoadStatus.NONE
|
|
|
|
|
|
|
|
|
|
|
|
vkz = get_variable("verfahrensnummer") or ""
|
|
|
|
|
|
if str(vkz).strip():
|
|
|
|
|
|
bror_result, bror_status, _ = verfahrensgebiet_alkis(
|
|
|
|
|
|
tab_widget, skip_existing_check=True
|
|
|
|
|
|
)
|
|
|
|
|
|
progress.raise_to_front()
|
|
|
|
|
|
if bror_status != LoadStatus.NONE and bror_result:
|
|
|
|
|
|
bror_result.setName(BROR_LAYER_NAME)
|
|
|
|
|
|
project.addMapLayer(bror_result)
|
|
|
|
|
|
bror_layer = bror_result
|
|
|
|
|
|
bror_was_added_by_us = True
|
|
|
|
|
|
|
|
|
|
|
|
progress.set_value(2)
|
|
|
|
|
|
|
2026-08-31 08:59:33 +02:00
|
|
|
|
# Shape-VG erst jetzt ins Projekt einfügen (nach BROR-Ladung) – außer
|
|
|
|
|
|
# der TEMPORÄRE STOPP oben hat ihn schon eingefügt.
|
|
|
|
|
|
if not project.mapLayer(shape_vg_layer.id()):
|
|
|
|
|
|
project.addMapLayer(shape_vg_layer)
|
|
|
|
|
|
shape_was_added_by_us = True
|
2026-07-15 15:29:48 +02:00
|
|
|
|
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
|
|
|
|
# Schritt 3: Gemarkungsschlüssel per Geometrie-Spatial-Join ermitteln
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
|
|
|
|
progress.set_label("Schritt 3/6: Gemarkungsschlüssel ermitteln …")
|
|
|
|
|
|
if progress.is_canceled():
|
|
|
|
|
|
return None, None, None, LoadStatus.NONE
|
|
|
|
|
|
|
|
|
|
|
|
shape_feats = list(shape_vg_layer.getFeatures())
|
|
|
|
|
|
vg_geom = shape_feats[0].geometry() if shape_feats else None
|
|
|
|
|
|
|
|
|
|
|
|
schluessel_namen = (
|
|
|
|
|
|
gemarkung_schluessel_und_namen_von_vg_geometrie(vg_geom)
|
|
|
|
|
|
if vg_geom else []
|
|
|
|
|
|
)
|
|
|
|
|
|
vorgabe_schluessel = [nr for nr, _ in schluessel_namen]
|
|
|
|
|
|
vorgabe_namen = [nm for _, nm in schluessel_namen if nm]
|
|
|
|
|
|
|
|
|
|
|
|
if not vorgabe_schluessel:
|
|
|
|
|
|
warning(
|
|
|
|
|
|
"VG laden",
|
|
|
|
|
|
"Keine Gemarkungen in der BBox gefunden. Bitte Schlüssel manuell eingeben.",
|
|
|
|
|
|
duration=5,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
progress.set_value(3)
|
|
|
|
|
|
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
|
|
|
|
# Schritt 4: Flurstücke laden (mit Nutzer-Dialog, Vorschlag befüllt)
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
|
|
|
|
progress.set_label("Schritt 4/6: Flurstücke aus ALKIS laden …")
|
|
|
|
|
|
if progress.is_canceled():
|
|
|
|
|
|
return None, None, None, LoadStatus.NONE
|
|
|
|
|
|
|
|
|
|
|
|
flst_layer, flst_status = flurstueck_alkis(
|
|
|
|
|
|
tab_widget,
|
|
|
|
|
|
vorgabe_schluessel=vorgabe_schluessel,
|
|
|
|
|
|
vorgabe_namen=vorgabe_namen if vorgabe_namen else None,
|
|
|
|
|
|
ohne_dialog=True,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if flst_status == LoadStatus.NONE or not flst_layer:
|
|
|
|
|
|
return None, None, None, LoadStatus.NONE
|
|
|
|
|
|
|
|
|
|
|
|
progress.set_value(4)
|
|
|
|
|
|
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
|
|
|
|
# Schritt 5: Räumliche Selektion
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
|
|
|
|
progress.set_label("Schritt 5/6: Flurstücke räumlich selektieren …")
|
|
|
|
|
|
if progress.is_canceled():
|
|
|
|
|
|
return None, None, None, LoadStatus.NONE
|
|
|
|
|
|
|
|
|
|
|
|
anzahl = _select_by_location(flst_layer, shape_vg_layer)
|
|
|
|
|
|
|
|
|
|
|
|
if anzahl == 0:
|
|
|
|
|
|
QMessageBox.warning(
|
|
|
|
|
|
tab_widget,
|
|
|
|
|
|
"Keine Flurstücke im VG",
|
|
|
|
|
|
"Es wurden keine Flurstücke gefunden, die das Verfahrensgebiet schneiden.\n"
|
|
|
|
|
|
"Bitte Gemarkungsschlüssel und Ausdehnung prüfen.",
|
|
|
|
|
|
)
|
|
|
|
|
|
return None, flst_layer, None, LoadStatus.NONE
|
|
|
|
|
|
|
|
|
|
|
|
progress.set_value(5)
|
|
|
|
|
|
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
|
|
|
|
# Schritt 6: Flächenvergleich als Gate
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
|
|
|
|
progress.set_label("Schritt 6/6: Flächenvergleich …")
|
|
|
|
|
|
if progress.is_canceled():
|
|
|
|
|
|
return None, None, None, LoadStatus.NONE
|
|
|
|
|
|
|
|
|
|
|
|
toleranz = _lese_flaechen_toleranz()
|
|
|
|
|
|
|
|
|
|
|
|
if bror_layer and bror_layer.isValid():
|
|
|
|
|
|
# BROR verfügbar → als Referenz-Objekt verwenden
|
|
|
|
|
|
ref_flaeche, shape_flaeche, stimmt_ueberein = _vergleiche_flaechen(
|
|
|
|
|
|
bror_layer, shape_vg_layer, toleranz
|
|
|
|
|
|
)
|
|
|
|
|
|
dialog_ref_label = "BauRaumOderBodenordnungsrecht"
|
|
|
|
|
|
dialog_vg_label = "Shape-Verfahrensgebiet "
|
|
|
|
|
|
pruef_layer = bror_layer
|
2026-07-15 15:39:21 +02:00
|
|
|
|
|
|
|
|
|
|
# Vergleich-Layer (grün/gelb/blau) ins Projekt einfügen
|
|
|
|
|
|
vergleich_layer = _erstelle_vergleich_layer(shape_vg_layer, bror_layer)
|
|
|
|
|
|
if vergleich_layer:
|
|
|
|
|
|
project.addMapLayer(vergleich_layer)
|
2026-07-15 15:29:48 +02:00
|
|
|
|
else:
|
|
|
|
|
|
# Kein BROR → temp. enriched Layer aus Flurstücken erzeugen
|
|
|
|
|
|
temp_enriched, _, _ = erstelle_verfahrensgebiet_aus_auswahl(flst_layer)
|
|
|
|
|
|
ref_flaeche, shape_flaeche, stimmt_ueberein = _vergleiche_flaechen(
|
|
|
|
|
|
shape_vg_layer, temp_enriched, toleranz
|
|
|
|
|
|
)
|
|
|
|
|
|
dialog_ref_label = "Shape-Verfahrensgebiet "
|
|
|
|
|
|
dialog_vg_label = "Verfahrensgebiet (Flurstücke)"
|
|
|
|
|
|
pruef_layer = shape_vg_layer
|
|
|
|
|
|
|
|
|
|
|
|
progress.set_value(6)
|
|
|
|
|
|
|
|
|
|
|
|
if stimmt_ueberein:
|
|
|
|
|
|
progress.set_value(7)
|
|
|
|
|
|
progress.close()
|
|
|
|
|
|
show_info_dialog(
|
|
|
|
|
|
"Flächenvergleich",
|
|
|
|
|
|
"Verfahrensgebiet mit erwarteter Geometrie erzeugt",
|
|
|
|
|
|
parent=tab_widget,
|
|
|
|
|
|
)
|
|
|
|
|
|
abgeschlossen = True
|
|
|
|
|
|
return shape_vg_layer, flst_layer, bror_layer, vg_status
|
|
|
|
|
|
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
|
|
|
|
# Flächenabweichung → 2-Optionen-Dialog
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
|
|
|
|
progress.close()
|
|
|
|
|
|
diff = abs(ref_flaeche - shape_flaeche)
|
|
|
|
|
|
entscheidung = ask_detailpruefung_oder_abbrechen(
|
|
|
|
|
|
tab_widget,
|
|
|
|
|
|
"Abweichung erkannt",
|
|
|
|
|
|
f"Abweichung erkannt.\n\n"
|
|
|
|
|
|
f"{dialog_ref_label}: {ref_flaeche:.2f} m²\n"
|
|
|
|
|
|
f"{dialog_vg_label}: {shape_flaeche:.2f} m²\n"
|
|
|
|
|
|
f"Differenz: {diff:.2f} m²\n\n"
|
|
|
|
|
|
f"Wie möchten Sie fortfahren?",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if entscheidung == "abbrechen":
|
|
|
|
|
|
abgeschlossen = True
|
|
|
|
|
|
return shape_vg_layer, flst_layer, bror_layer, vg_status
|
|
|
|
|
|
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
|
|
|
|
# Option: Detailprüfung (knickpunktbasiert) auf pruef_layer
|
|
|
|
|
|
# (= BROR wenn vorhanden, sonst Shape-VG)
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
|
|
|
|
knickpunkte = _extrahiere_knickpunkte(pruef_layer)
|
|
|
|
|
|
n_knick = len(knickpunkte)
|
|
|
|
|
|
gesamt_schritte = n_knick + 5
|
|
|
|
|
|
|
|
|
|
|
|
detail_progress = create_progress_dialog(
|
|
|
|
|
|
gesamt_schritte,
|
|
|
|
|
|
"Detailprüfung (Knickpunkte)",
|
|
|
|
|
|
"Schritt 1: Grenzpunkte laden …",
|
|
|
|
|
|
)
|
|
|
|
|
|
detail_progress.set_value(0)
|
|
|
|
|
|
|
|
|
|
|
|
punktort_layer = lade_punktort_ta(
|
|
|
|
|
|
vorgabe_schluessel, vg_geom, progress=detail_progress
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if detail_progress.is_canceled():
|
|
|
|
|
|
detail_progress.close()
|
|
|
|
|
|
abgeschlossen = True
|
|
|
|
|
|
return shape_vg_layer, flst_layer, bror_layer, vg_status
|
|
|
|
|
|
|
|
|
|
|
|
detail_progress.set_value(1)
|
|
|
|
|
|
|
|
|
|
|
|
if punktort_layer is None:
|
|
|
|
|
|
detail_progress.close()
|
|
|
|
|
|
abgeschlossen = True
|
|
|
|
|
|
if not detail_progress.is_canceled():
|
|
|
|
|
|
QMessageBox.critical(
|
|
|
|
|
|
tab_widget,
|
|
|
|
|
|
"WFS-Fehler",
|
|
|
|
|
|
"Die Grenzpunktdaten (PunktortTA) konnten nicht geladen werden.\n\n"
|
|
|
|
|
|
"Mögliche Ursache: Netzwerk- oder Proxy-Fehler.\n"
|
|
|
|
|
|
"Bitte prüfen Sie die QGIS-Proxy-Einstellungen "
|
|
|
|
|
|
"(Einstellungen -> Optionen -> Netzwerk) und den Log-Eintrag "
|
|
|
|
|
|
"unter Ansicht -> Bedienfelder -> Protokollnachrichten "
|
|
|
|
|
|
"(Kategorie 'sn_verfahrensgebiet').",
|
|
|
|
|
|
)
|
|
|
|
|
|
return shape_vg_layer, flst_layer, bror_layer, vg_status
|
|
|
|
|
|
|
|
|
|
|
|
ergebnis_layer, identisch, kein_pta, fehlender_knick = knickpunkt_pruefung(
|
|
|
|
|
|
pruef_layer,
|
|
|
|
|
|
punktort_layer,
|
|
|
|
|
|
flst_layer,
|
|
|
|
|
|
progress=detail_progress,
|
|
|
|
|
|
knickpunkte=knickpunkte,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if detail_progress.is_canceled():
|
|
|
|
|
|
detail_progress.close()
|
|
|
|
|
|
abgeschlossen = True
|
|
|
|
|
|
return shape_vg_layer, flst_layer, bror_layer, vg_status
|
|
|
|
|
|
|
|
|
|
|
|
detail_progress.set_value(gesamt_schritte)
|
|
|
|
|
|
detail_progress.close()
|
|
|
|
|
|
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
|
|
|
|
# Korrigiertes Polygon aus identischen + fehlenden Knickpunkten
|
|
|
|
|
|
# (nur für Visualisierung – shape_vg_layer bleibt das finale VG)
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
|
|
|
|
if ergebnis_layer and ergebnis_layer.isValid():
|
|
|
|
|
|
linien_layer_korr, polygon_layer_korr = erstelle_korrigiertes_polygon(
|
|
|
|
|
|
pruef_layer, ergebnis_layer, pruef_layer.crs().authid()
|
|
|
|
|
|
)
|
|
|
|
|
|
if polygon_layer_korr is not None and polygon_layer_korr.isValid():
|
|
|
|
|
|
if linien_layer_korr is not None and linien_layer_korr.isValid():
|
|
|
|
|
|
project.addMapLayer(linien_layer_korr)
|
|
|
|
|
|
project.addMapLayer(polygon_layer_korr)
|
|
|
|
|
|
|
|
|
|
|
|
if ergebnis_layer and ergebnis_layer.isValid():
|
|
|
|
|
|
project.addMapLayer(ergebnis_layer)
|
|
|
|
|
|
|
|
|
|
|
|
show_info_dialog(
|
|
|
|
|
|
"Detailprüfung abgeschlossen",
|
|
|
|
|
|
f"Knickpunkte geprüft: {n_knick}\n"
|
|
|
|
|
|
f" Identisch (grün): {identisch}\n"
|
|
|
|
|
|
f" Kein PunktortTA (rot): {kein_pta}\n"
|
|
|
|
|
|
f" Fehlende Knickpunkte (orange): {fehlender_knick}",
|
|
|
|
|
|
parent=tab_widget,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
abgeschlossen = True
|
|
|
|
|
|
return shape_vg_layer, flst_layer, bror_layer, vg_status
|
|
|
|
|
|
|
|
|
|
|
|
finally:
|
|
|
|
|
|
_entferne_hilfs_layer(project, "TempVerfahrensgebiet")
|
|
|
|
|
|
# Shape-VG und BROR bei Fehler/Abbruch entfernen
|
|
|
|
|
|
if not abgeschlossen:
|
|
|
|
|
|
if shape_was_added_by_us and shape_vg_layer is not None:
|
|
|
|
|
|
try:
|
|
|
|
|
|
project.removeMapLayer(shape_vg_layer.id())
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
if bror_was_added_by_us and bror_layer is not None:
|
|
|
|
|
|
try:
|
|
|
|
|
|
project.removeMapLayer(bror_layer.id())
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|