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>
1034 lines
39 KiB
Python
1034 lines
39 KiB
Python
"""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/.shp-Filter, Mehrfachauswahl); Nutzerabbruch
|
||
beendet den Vorgang.
|
||
2. ZIP in ein temporäres Verzeichnis entpacken (danach wieder gelöscht) und
|
||
auf ein gültiges Shapefile-Set prüfen.
|
||
3. Shapefile als QGIS-Vektorlayer laden und validieren (Standard-QGIS-Routinen).
|
||
4. Alle Features per ``QgsGeometry.unaryUnion`` zu einem Single-Feature-
|
||
Memory-Layer „Verfahrensgebiet" verschmelzen.
|
||
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 re
|
||
import shutil
|
||
import tempfile
|
||
import zipfile
|
||
from typing import Optional, Tuple
|
||
|
||
from sn_basis.functions import QgsProject, QgsVectorLayer, QgsFeature, QgsField
|
||
from sn_basis.functions import QMessageBox
|
||
from sn_basis.functions.message_wrapper import warning
|
||
from sn_basis.functions.variable_wrapper import get_variable
|
||
from sn_basis.functions.qt_wrapper import QFileDialog, QVariant
|
||
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,
|
||
ask_yes_no,
|
||
)
|
||
|
||
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,
|
||
)
|
||
|
||
#: Feldname im Shapefile, über den der Gebietstyp bestimmt wird.
|
||
FBSCHL_FELD = "FBSchl"
|
||
#: FBSchl-Wert für ein Neuordnungsgebiet.
|
||
FBSCHL_NEUORDNUNGSGEBIET = "901/10110F"
|
||
#: FBSchl-Wert für ein Neuvermessungsgebiet.
|
||
FBSCHL_NEUVERMESSUNGSGEBIET = "901/10120F"
|
||
|
||
#: Anzeigename des Layers für ein Neuordnungsgebiet (NoBIS).
|
||
NEUORDNUNGSGEBIET_LAYER_NAME = "Neuordnungsgebiet (NoBIS)"
|
||
#: Anzeigename des Layers für ein Neuvermessungsgebiet (NoBIS).
|
||
NEUVERMESSUNGSGEBIET_LAYER_NAME = "Neuvermessungsgebiet (NoBIS)"
|
||
#: Zuordnung Gebietstyp → Layername.
|
||
_GEBIETSTYP_LAYER_NAMEN = {
|
||
"Neuordnungsgebiet": NEUORDNUNGSGEBIET_LAYER_NAME,
|
||
"Neuvermessungsgebiet": NEUVERMESSUNGSGEBIET_LAYER_NAME,
|
||
}
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Interne Hilfsfunktionen
|
||
# ---------------------------------------------------------------------------
|
||
|
||
def _normalisiere_feldname(name: str) -> str:
|
||
"""Normalisiert einen Feldnamen für den toleranten Vergleich.
|
||
|
||
Entfernt alle Zeichen außer Buchstaben/Ziffern (z.B. Punkte, Leerzeichen,
|
||
Unterstriche) und wandelt in Kleinbuchstaben um. Hintergrund: manche
|
||
Shape-Exporte liefern den Feldnamen ``FBSchl`` mit einem angehängten
|
||
Punkt (``FBSchl.``) oder abweichender Groß-/Kleinschreibung, wodurch ein
|
||
exakter Namensvergleich das Feld fälschlich als fehlend meldet (#44).
|
||
|
||
Parameters
|
||
----------
|
||
name :
|
||
Roher Feldname.
|
||
|
||
Returns
|
||
-------
|
||
str
|
||
Normalisierter Feldname (nur Kleinbuchstaben/Ziffern).
|
||
"""
|
||
return re.sub(r"[^a-zA-Z0-9]", "", name).lower()
|
||
|
||
|
||
def _finde_fbschl_index(raw_layer) -> int:
|
||
"""Sucht den Feldindex von ``FBSchl`` tolerant gegenüber Abweichungen im
|
||
Namen (Groß-/Kleinschreibung, angehängte Sonderzeichen wie ein Punkt).
|
||
|
||
Versucht zuerst den exakten Namen; schlägt das fehl, wird über alle
|
||
Felder mit normalisiertem Namensvergleich gesucht (siehe
|
||
:func:`_normalisiere_feldname`).
|
||
|
||
Parameters
|
||
----------
|
||
raw_layer :
|
||
Dateibasierter Shapefile-Layer.
|
||
|
||
Returns
|
||
-------
|
||
int
|
||
Feldindex, oder ``-1`` wenn kein passendes Feld gefunden wurde.
|
||
"""
|
||
idx = raw_layer.fields().indexFromName(FBSCHL_FELD)
|
||
if idx >= 0:
|
||
return idx
|
||
|
||
ziel = _normalisiere_feldname(FBSCHL_FELD)
|
||
for feld_idx, feld in enumerate(raw_layer.fields()):
|
||
if _normalisiere_feldname(feld.name()) == ziel:
|
||
return feld_idx
|
||
return -1
|
||
|
||
|
||
def _ermittle_gebietstyp_aus_fbschl(raw_layer) -> Tuple[Optional[str], Optional[str]]:
|
||
"""Prüft das Feld ``FBSchl`` und ermittelt den Gebietstyp.
|
||
|
||
Zeigt selbst keine Fehlermeldung an, sondern gibt einen Fehlertext
|
||
zurück, damit der Aufrufer Fehler mehrerer Dateien sammeln und am Ende
|
||
gemeinsam anzeigen kann.
|
||
|
||
Die Feldsuche ist tolerant gegenüber Abweichungen im Namen (z.B. ein
|
||
angehängter Punkt ``FBSchl.`` in manchen Shape-Exporten, siehe #44) –
|
||
siehe :func:`_finde_fbschl_index`.
|
||
|
||
Parameters
|
||
----------
|
||
raw_layer :
|
||
Dateibasierter Shapefile-Layer (vor dem Verschmelzen zum Memory-Layer).
|
||
|
||
Returns
|
||
-------
|
||
Tuple[Optional[str], Optional[str]]
|
||
``(gebietstyp, fehlertext)``. Bei Erfolg ist ``gebietstyp``
|
||
``"Neuordnungsgebiet"`` oder ``"Neuvermessungsgebiet"`` und
|
||
``fehlertext`` ``None``. Bei Fehler ist ``gebietstyp`` ``None`` und
|
||
``fehlertext`` beschreibt die Ursache (fehlendes Feld oder
|
||
unbekannter Wert).
|
||
"""
|
||
idx = _finde_fbschl_index(raw_layer)
|
||
if idx < 0:
|
||
return None, f"Feld '{FBSCHL_FELD}' fehlt (Shape entspricht nicht dem erwarteten Inhalt)."
|
||
|
||
wert = None
|
||
for feat in raw_layer.getFeatures():
|
||
try:
|
||
roh = feat.attribute(idx)
|
||
except Exception:
|
||
roh = None
|
||
if roh not in (None, ""):
|
||
wert = str(roh).strip()
|
||
break
|
||
|
||
if wert == FBSCHL_NEUORDNUNGSGEBIET:
|
||
return "Neuordnungsgebiet", None
|
||
if wert == FBSCHL_NEUVERMESSUNGSGEBIET:
|
||
return "Neuvermessungsgebiet", None
|
||
|
||
return None, (
|
||
f"Unbekannter Wert im Feld '{FBSCHL_FELD}': {wert!r} "
|
||
f"(erwartet: '{FBSCHL_NEUORDNUNGSGEBIET}' oder "
|
||
f"'{FBSCHL_NEUVERMESSUNGSGEBIET}')."
|
||
)
|
||
|
||
def _wende_vergleich_stil_an(layer) -> None:
|
||
"""Wendet einen kategorisierten Renderer auf den Vergleich-Layer an.
|
||
|
||
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
|
||
|
||
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 _finde_alle_shp(verzeichnis: str) -> list:
|
||
"""Sucht rekursiv nach allen ``.shp``-Dateien im Verzeichnis.
|
||
|
||
Ein ZIP-Archiv kann mehrere Shapefile-Sets enthalten (z.B. je eines für
|
||
Neuordnungs- und Neuvermessungsgebiet) – im Unterschied zu
|
||
:func:`_finde_shp` werden hier alle gefunden, nicht nur die erste.
|
||
|
||
Parameters
|
||
----------
|
||
verzeichnis :
|
||
Startverzeichnis für die Suche.
|
||
|
||
Returns
|
||
-------
|
||
list
|
||
Sortierte Liste der absoluten Pfade aller gefundenen ``.shp``-Dateien.
|
||
"""
|
||
treffer = []
|
||
for root, _dirs, files in os.walk(verzeichnis):
|
||
for fname in files:
|
||
if fname.lower().endswith(".shp"):
|
||
treffer.append(os.path.join(root, fname))
|
||
return sorted(treffer)
|
||
|
||
|
||
def _erstelle_memory_layer_aus_shp(shp_layer) -> Optional[object]:
|
||
"""Vereint alle Features des Shapefile-Layers zu einem Single-Feature-
|
||
Memory-Layer „Verfahrensgebiet" via ``QgsGeometry.unaryUnion``.
|
||
|
||
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[list]:
|
||
"""Öffnet Dateidialog (Mehrfachauswahl), entpackt ZIPs (bzw. nutzt direkt
|
||
gewählte ``.shp``-Dateien) und liefert je Gebietstyp einen Union-Memory-Layer.
|
||
|
||
Schrittfolge:
|
||
|
||
1. ``QFileDialog.getOpenFileNames`` mit Filter für ``.zip`` und ``.shp``
|
||
(Mehrfachauswahl möglich).
|
||
2. Für jede ausgewählte Datei: bei ``.zip`` in ein temporäres Verzeichnis
|
||
entpacken (danach wieder gelöscht) und darin **alle** enthaltenen
|
||
``.shp``-Dateien finden (ein ZIP kann mehrere Shapefile-Sets
|
||
bündeln, z.B. je eines für Neuordnungs- und Neuvermessungsgebiet);
|
||
bei ``.shp`` die Datei direkt verwenden.
|
||
3. Jede gefundene Shapefile-Datei einzeln als ``QgsVectorLayer`` laden
|
||
und ``isValid()`` prüfen.
|
||
4. Je Shapefile-Datei das Feld ``FBSchl`` prüfen und den Gebietstyp
|
||
ermitteln (Neuordnungs- bzw. Neuvermessungsgebiet).
|
||
5. Die Features aller Dateien **desselben Gebietstyps** werden gemeinsam
|
||
per ``QgsGeometry.unaryUnion`` zu einem Single-Feature-Memory-Layer
|
||
verschmolzen – d.h. mehrere Neuordnungsgebiet-Dateien ergeben einen
|
||
Layer, mehrere Neuvermessungsgebiet-Dateien einen zweiten.
|
||
|
||
Fehlerverhalten: Schlägt die Prüfung eines einzelnen Shapefiles fehl
|
||
(ZIP nicht entpackbar, kein Shapefile enthalten, ungültiger Layer, Feld
|
||
``FBSchl`` fehlt oder hat einen unbekannten Wert), wird **nur dieses
|
||
Shapefile** übersprungen und sein Fehler gesammelt – das gilt auch,
|
||
wenn mehrere Shapefiles in derselben ZIP-Datei liegen: eine fehlerhafte
|
||
Datei darin bricht nicht die Verarbeitung der übrigen ab. Alle
|
||
verwertbaren Dateien werden gemäß Vorgabe eingelesen. Am Ende erscheint
|
||
eine gesammelte Fehlermeldung mit allen fehlerhaften Dateien. Erst wenn
|
||
**keine** Datei verwertbare Geometrien liefert, wird ``None``
|
||
zurückgegeben.
|
||
|
||
Parameters
|
||
----------
|
||
parent_widget :
|
||
Eltern-Widget für Dateidialog und Fehlermeldungen.
|
||
|
||
Returns
|
||
-------
|
||
Optional[list]
|
||
Liste der erzeugten Memory-Layer (ein Layer je in der Auswahl
|
||
vorkommendem, erfolgreich geprüftem Gebietstyp, max. 2), oder
|
||
``None`` wenn keine Datei verwertet werden konnte oder der Dialog
|
||
abgebrochen wurde.
|
||
"""
|
||
pfade, _ = QFileDialog.getOpenFileNames(
|
||
parent_widget,
|
||
"Shapefile-Set(s) (ZIP oder .shp) auswählen",
|
||
"",
|
||
"Shapefile-Set (*.zip *.shp);;ZIP-Dateien (*.zip);;Shapefile (*.shp)",
|
||
)
|
||
if not pfade:
|
||
return None # Nutzerabbruch
|
||
|
||
# Je Gebietstyp gesammelte Geometrien/Felder/Attribute über alle
|
||
# ausgewählten Dateien hinweg (Reihenfolge der ersten Sichtung bleibt
|
||
# über dict-Einfügereihenfolge erhalten).
|
||
geometrien_je_typ: dict = {}
|
||
felder_je_typ: dict = {}
|
||
attribute_je_typ: dict = {}
|
||
crs_je_typ: dict = {}
|
||
temp_dirs: list = []
|
||
fehler_liste: list = []
|
||
|
||
try:
|
||
# (shp_path, anzeige_name) für jede tatsächlich zu prüfende
|
||
# Shapefile-Datei sammeln – ein ZIP kann mehrere Shapefile-Sets
|
||
# enthalten, jedes wird einzeln behandelt.
|
||
shp_eintraege: list = []
|
||
|
||
for pfad in pfade:
|
||
dateiname = os.path.basename(pfad)
|
||
|
||
if pfad.lower().endswith(".shp"):
|
||
shp_eintraege.append((pfad, dateiname))
|
||
continue
|
||
|
||
extract_dir = tempfile.mkdtemp(prefix="sn_vg_shape_")
|
||
temp_dirs.append(extract_dir)
|
||
|
||
try:
|
||
with zipfile.ZipFile(pfad, "r") as zf:
|
||
zf.extractall(extract_dir)
|
||
except Exception:
|
||
fehler_liste.append(
|
||
f"{dateiname}: ZIP-Datei konnte nicht entpackt werden."
|
||
)
|
||
continue
|
||
|
||
shp_pfade_im_zip = _finde_alle_shp(extract_dir)
|
||
if not shp_pfade_im_zip:
|
||
fehler_liste.append(
|
||
f"{dateiname}: kein gültiges Shape-Dateiset enthalten."
|
||
)
|
||
continue
|
||
|
||
for shp_path in shp_pfade_im_zip:
|
||
anzeige_name = (
|
||
f"{dateiname} ({os.path.basename(shp_path)})"
|
||
if len(shp_pfade_im_zip) > 1
|
||
else dateiname
|
||
)
|
||
shp_eintraege.append((shp_path, anzeige_name))
|
||
|
||
# ------------------------------------------------------------------
|
||
# Jede gefundene Shapefile-Datei einzeln prüfen und einlesen.
|
||
# ------------------------------------------------------------------
|
||
for shp_path, anzeige_name in shp_eintraege:
|
||
raw_layer = QgsVectorLayer(shp_path, "tmp_shape_vg", "ogr")
|
||
if not raw_layer or not raw_layer.isValid():
|
||
fehler_liste.append(
|
||
f"{anzeige_name}: kein gültiges Shape-Dateiset."
|
||
)
|
||
continue
|
||
|
||
gebietstyp, fehlertext = _ermittle_gebietstyp_aus_fbschl(raw_layer)
|
||
if gebietstyp is None:
|
||
fehler_liste.append(f"{anzeige_name}: {fehlertext}")
|
||
del raw_layer
|
||
continue
|
||
|
||
geom_list = []
|
||
first_feat = None
|
||
for feat in raw_layer.getFeatures():
|
||
try:
|
||
geom = feat.geometry()
|
||
except Exception:
|
||
geom = None
|
||
if geom and not geom.isEmpty():
|
||
geom_list.append(geom)
|
||
if first_feat is None:
|
||
first_feat = feat
|
||
|
||
if geom_list:
|
||
geometrien_je_typ.setdefault(gebietstyp, []).extend(geom_list)
|
||
if gebietstyp not in felder_je_typ:
|
||
felder_je_typ[gebietstyp] = list(raw_layer.fields())
|
||
attribute_je_typ[gebietstyp] = (
|
||
first_feat.attributes() if first_feat is not None else []
|
||
)
|
||
crs_je_typ[gebietstyp] = raw_layer.crs().authid()
|
||
else:
|
||
fehler_liste.append(f"{anzeige_name}: keine gültigen Geometrien enthalten.")
|
||
|
||
# Referenz auf den dateibasierten Layer freigeben, bevor das
|
||
# temporäre Verzeichnis gelöscht wird (Windows hält sonst ggf.
|
||
# noch einen Dateihandle auf das Shapefile).
|
||
del raw_layer
|
||
|
||
if fehler_liste:
|
||
meldung = (
|
||
"Folgende Dateien konnten nicht eingelesen werden:\n\n"
|
||
+ "\n".join(fehler_liste)
|
||
)
|
||
if geometrien_je_typ:
|
||
meldung += "\n\nDie übrigen Dateien wurden gemäß Vorgabe eingelesen."
|
||
QMessageBox.warning(parent_widget, "Fehler in ausgewählten Dateien", meldung)
|
||
|
||
if not geometrien_je_typ:
|
||
return None
|
||
|
||
ergebnis_layer = []
|
||
for typ, geometrien in geometrien_je_typ.items():
|
||
union_geom = QgsGeometry.unaryUnion(geometrien)
|
||
if not union_geom or union_geom.isEmpty():
|
||
continue
|
||
|
||
mem_layer = QgsVectorLayer(
|
||
f"MultiPolygon?crs={crs_je_typ[typ]}",
|
||
_GEBIETSTYP_LAYER_NAMEN[typ],
|
||
"memory",
|
||
)
|
||
pr = mem_layer.dataProvider()
|
||
try:
|
||
pr.addAttributes(felder_je_typ[typ])
|
||
mem_layer.updateFields()
|
||
except Exception:
|
||
pass
|
||
|
||
feat_out = QgsFeature(mem_layer.fields())
|
||
feat_out.setGeometry(union_geom)
|
||
try:
|
||
feat_out.setAttributes(attribute_je_typ[typ])
|
||
except Exception:
|
||
pass
|
||
|
||
pr.addFeatures([feat_out])
|
||
mem_layer.updateExtents()
|
||
|
||
if mem_layer.isValid() and mem_layer.featureCount() > 0:
|
||
mem_layer.setCustomProperty("fb_gebietstyp", typ)
|
||
ergebnis_layer.append(mem_layer)
|
||
|
||
if not ergebnis_layer:
|
||
QMessageBox.warning(
|
||
parent_widget,
|
||
"Fehler",
|
||
"gültiges Shape-Dateiset angeben",
|
||
)
|
||
return None
|
||
|
||
return ergebnis_layer
|
||
|
||
finally:
|
||
for d in temp_dirs:
|
||
shutil.rmtree(d, ignore_errors=True)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Ö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
|
||
zusatz_shape_layer: list = []
|
||
bror_layer = None
|
||
shape_was_added_by_us = False
|
||
bror_was_added_by_us = False
|
||
abgeschlossen = False
|
||
|
||
progress = None
|
||
|
||
try:
|
||
# ------------------------------------------------------------------
|
||
# Schritt 1: Shape-VG aus ZIP laden (noch NICHT ins Projekt einfügen)
|
||
# Läuft VOR dem Fortschrittsdialog, damit dieser nicht schon während
|
||
# des Dateidialogs sichtbar ist.
|
||
# ------------------------------------------------------------------
|
||
shape_vg_layers = lade_vg_aus_zip(tab_widget)
|
||
|
||
if not shape_vg_layers:
|
||
return None, None, None, LoadStatus.NONE
|
||
|
||
# Je Gebietstyp ein Layer (max. 2). Der erste ist das für den
|
||
# weiteren Ablauf maßgebliche Shape-VG; etwaige weitere Layer
|
||
# (zweiter Gebietstyp) werden vorerst nur mit ins Projekt geladen –
|
||
# ihre Einbindung in den weiteren Workflow folgt in einem späteren
|
||
# Schritt.
|
||
shape_vg_layer = shape_vg_layers[0]
|
||
zusatz_shape_layer = shape_vg_layers[1:]
|
||
|
||
# TEMPORÄRER STOPP (wieder entfernen): Pause nach "Verfahrensgebiet
|
||
# aus ZIP laden" zur Zwischenkontrolle – Layer dafür schon jetzt
|
||
# sichtbar ins Projekt einfügen (normalerweise erst nach Schritt 2).
|
||
project.addMapLayer(shape_vg_layer)
|
||
shape_was_added_by_us = True
|
||
for lyr in zusatz_shape_layer:
|
||
project.addMapLayer(lyr)
|
||
|
||
geladene_namen = ", ".join(lyr.name() for lyr in shape_vg_layers)
|
||
if not ask_yes_no(
|
||
"Verfahrensgebiet aus ZIP laden",
|
||
f"Layer '{geladene_namen}' wurde(n) geladen. Weiter?",
|
||
default=True,
|
||
parent=tab_widget,
|
||
):
|
||
# abgeschlossen=True, damit der finally-Block den zur Kontrolle
|
||
# eingefügten Layer NICHT wieder entfernt (kein Fehlerfall).
|
||
abgeschlossen = True
|
||
return None, None, None, LoadStatus.NONE
|
||
|
||
vg_status = LoadStatus.FIRST
|
||
|
||
# Fortschrittsdialog erst jetzt erzeugen (nach dem Stop) – er darf
|
||
# nicht schon während Dateidialog/Stop-Abfrage sichtbar sein.
|
||
progress = create_progress_dialog(
|
||
7, "Verfahrensgebiet aus Shape laden", "Schritt 2/6: BROR aus ALKIS laden …"
|
||
)
|
||
progress.set_value(1)
|
||
|
||
# ------------------------------------------------------------------
|
||
# Schritt 2: BROR aus ALKIS laden (vor shape_vg, um Namenskonflikt
|
||
# in _check_existing_layer zu vermeiden)
|
||
# ------------------------------------------------------------------
|
||
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)
|
||
|
||
# Shape-VG erst jetzt ins Projekt einfügen (nach BROR-Ladung) – außer
|
||
# der TEMPORÄRE STOPP oben hat ihn schon eingefügt.
|
||
if not project.mapLayer(shape_vg_layer.id()):
|
||
project.addMapLayer(shape_vg_layer)
|
||
shape_was_added_by_us = True
|
||
|
||
# ------------------------------------------------------------------
|
||
# Zwischenabfrage: Nutzer kann vor dem Gemarkungsschlüssel-Schritt
|
||
# abbrechen (Shape-VG und ggf. BROR bleiben im Projekt erhalten).
|
||
# ------------------------------------------------------------------
|
||
progress.raise_to_front()
|
||
if not ask_yes_no(
|
||
"Verfahrensgebiet aus Shape laden",
|
||
"Weiter mit Gemarkungsschlüssel ermitteln?",
|
||
default=True,
|
||
parent=tab_widget,
|
||
):
|
||
abgeschlossen = True
|
||
return shape_vg_layer, None, bror_layer, vg_status
|
||
|
||
# ------------------------------------------------------------------
|
||
# Schritt 3: Gemarkungsschlüssel per Geometrie-Spatial-Join ermitteln
|
||
# ------------------------------------------------------------------
|
||
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
|
||
|
||
# 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)
|
||
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:
|
||
for lyr in zusatz_shape_layer:
|
||
try:
|
||
project.removeMapLayer(lyr.id())
|
||
except Exception:
|
||
pass
|
||
if shape_was_added_by_us and shape_vg_layer is not None:
|
||
try:
|
||
project.removeMapLayer(shape_vg_layer.id())
|
||
except Exception:
|
||
pass
|
||
if bror_was_added_by_us and bror_layer is not None:
|
||
try:
|
||
project.removeMapLayer(bror_layer.id())
|
||
except Exception:
|
||
pass
|