Funktion angelegt, Button eingefügt
This commit is contained in:
@@ -0,0 +1,467 @@
|
||||
"""sn_verfahrensgebiet/functions/beteiligung_pruefung.py – Beteiligung prüfen.
|
||||
|
||||
AGLB-länderspezifisch (Flurstückskennzeichen-Bildung nach sächsischem ALKIS-
|
||||
Schema). Die Länderkennung wird nicht hartcodiert, sondern über
|
||||
:func:`sn_basis.functions.laenderkennung.get_laenderkennung` ermittelt, damit
|
||||
Schwesterplugins für andere Bundesländer dieselbe Logik mit einer anderen
|
||||
Länderkennung wiederverwenden können.
|
||||
|
||||
Gleicht eine CSV-Exportliste des BI-Servers (Spalten ``FLURSTUECK``,
|
||||
``VEF_VKZF``, ``FLA_BETEILIGUNG``) mit dem Verfahrensgebiet-Bestand ab. Aus
|
||||
``FLURSTUECK`` (Format ``<Gemarkung>/<Zähler>/<Nenner>/<Folge>``) wird je Zeile
|
||||
ein zu ALKIS kompatibles Flurstückskennzeichen ``Flurstueck_csv`` gebildet.
|
||||
|
||||
Automatische Kaskade je nach Projektzustand:
|
||||
|
||||
- **try1**: Ein Layer ``Verfahrensgebiet`` mit befülltem Attribut
|
||||
``flurstueckskennzeichen`` (Single-Feature-Modell, siehe
|
||||
:mod:`verfahrensgebiet_edit_service`) ist vorhanden → schneller
|
||||
Mengenvergleich ohne WFS-Zugriff.
|
||||
- **try2**: Ein Verfahrensgebiet-Layer ist vorhanden, aber ohne verwertbare
|
||||
Kennzeichen → je CSV-Zeile eine WFS-Einzelabfrage gegen ``AX_Flurstueck``.
|
||||
- **try3**: Kein Verfahrensgebiet-Layer im Projekt → Verfahrensnummer aus
|
||||
``VEF_VKZF`` als Projektvariable setzen, Verfahrensgebiet analog zur
|
||||
bestehenden ALKIS-Pipeline laden (ohne Detailprüfung), danach wie try2.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
from sn_basis.functions import QgsVectorLayer, QgsProject, QgsFeature, QgsField
|
||||
from sn_basis.functions import QMessageBox
|
||||
from sn_basis.functions.qt_wrapper import QFileDialog, QVariant
|
||||
from sn_basis.functions.variable_wrapper import set_variable
|
||||
from sn_basis.functions.laenderkennung import get_laenderkennung
|
||||
|
||||
from sn_verfahrensgebiet.functions.verfahrensgebiet_alkis import verfahrensgebiet_alkis
|
||||
from sn_verfahrensgebiet.functions.flurstueck_alkis import ALKIS_SF_URL, FLST_TYPENAME, FLST_LAYER_NAME
|
||||
from sn_verfahrensgebiet.functions.verfahrensgebiet_edit_service import (
|
||||
VERF_LAYER_NAME,
|
||||
get_kennzeichen_menge,
|
||||
)
|
||||
|
||||
CSV_SPALTE_FLURSTUECK = "FLURSTUECK"
|
||||
CSV_SPALTE_VEF_VKZF = "VEF_VKZF"
|
||||
CSV_SPALTE_BETEILIGUNG = "FLA_BETEILIGUNG"
|
||||
|
||||
FELD_FLURSTUECK_CSV = "Flurstueck_csv"
|
||||
FELD_FLURSTUECKSKENNZEICHEN_WFS = "flurstueckskennzeichen"
|
||||
|
||||
ERGEBNIS_LAYER_VERGLEICH = "Beteiligung – Vergleich"
|
||||
ERGEBNIS_LAYER_ERFOLGREICH = "Beteiligung – gefunden"
|
||||
ERGEBNIS_LAYER_NICHT_GEFUNDEN = "Beteiligung – nicht gefunden"
|
||||
|
||||
WERT_BEIDE = "beide"
|
||||
WERT_NUR_VERFAHRENSGEBIET = "nur_verfahrensgebiet"
|
||||
WERT_NUR_AGLB = "nur_aglb"
|
||||
|
||||
|
||||
class BeteiligungPruefungFehler(Exception):
|
||||
"""Erwarteter, dem Nutzer per Messagebox mitzuteilender Fehlerfall."""
|
||||
|
||||
|
||||
@dataclass
|
||||
class BeteiligungErgebnis:
|
||||
"""Ergebnis eines Prüflaufs, das die aufrufende UI ins Projekt einfügt."""
|
||||
|
||||
modus: str
|
||||
layers: List[Tuple[object, Optional[str]]] = field(default_factory=list)
|
||||
statistik: Dict[str, int] = field(default_factory=dict)
|
||||
statistik_text: str = ""
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CSV laden
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def lade_beteiligung_csv(parent_widget) -> Optional[Tuple[List[dict], List[str]]]:
|
||||
"""Öffnet Dateidialog (CSV-Filter) und liest die BI-Server-Exportliste.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Optional[Tuple[List[dict], List[str]]]
|
||||
``(zeilen, spaltennamen)`` oder ``None`` bei Nutzerabbruch.
|
||||
|
||||
Raises
|
||||
------
|
||||
BeteiligungPruefungFehler
|
||||
Bei ungültigem CSV-Format oder fehlendem Attribut ``FLURSTUECK``.
|
||||
"""
|
||||
pfad, _ = QFileDialog.getOpenFileName(
|
||||
parent_widget,
|
||||
"CSV-Export aus BI-Server auswählen",
|
||||
"",
|
||||
"CSV-Dateien (*.csv)",
|
||||
)
|
||||
if not pfad:
|
||||
return None # Nutzerabbruch
|
||||
|
||||
try:
|
||||
with open(pfad, "r", encoding="utf-8-sig", newline="") as f:
|
||||
reader = csv.DictReader(f, delimiter=",")
|
||||
zeilen = list(reader)
|
||||
spaltennamen = reader.fieldnames or []
|
||||
except (OSError, csv.Error, UnicodeDecodeError) as exc:
|
||||
raise BeteiligungPruefungFehler(
|
||||
f"CSV-Format ungültig – Datei konnte nicht gelesen werden: {exc}"
|
||||
) from exc
|
||||
|
||||
if not spaltennamen or not zeilen:
|
||||
raise BeteiligungPruefungFehler(
|
||||
"CSV-Format ungültig – die Datei ist leer oder enthält keine Kopfzeile."
|
||||
)
|
||||
|
||||
if CSV_SPALTE_FLURSTUECK not in spaltennamen:
|
||||
raise BeteiligungPruefungFehler(
|
||||
f"Die CSV enthält nicht das benötigte Attribut '{CSV_SPALTE_FLURSTUECK}'."
|
||||
)
|
||||
|
||||
return zeilen, spaltennamen
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Flurstueck_csv-Bildung
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def baue_flurstueck_csv(flurstueck_wert: str, laenderkennung: str) -> Optional[str]:
|
||||
"""Bildet aus dem CSV-Attribut ``FLURSTUECK`` ein ALKIS-Flurstückskennzeichen.
|
||||
|
||||
``FLURSTUECK`` hat das Format
|
||||
``<Gemarkungsnummer>/<Zähler>/<Nenner>/<Folge>``. Die Flurnummer ist in der
|
||||
CSV nicht enthalten und wird als fixer Platzhalter ``___`` geführt (so wie
|
||||
ALKIS unbesetzte Kennzeichen-Bestandteile selbst mit Unterstrichen füllt).
|
||||
|
||||
Returns
|
||||
-------
|
||||
Optional[str]
|
||||
Das zusammengesetzte Kennzeichen, oder ``None`` bei ungültigem Format
|
||||
(nicht genau 4 durch ``/`` getrennte Teile).
|
||||
"""
|
||||
if not flurstueck_wert:
|
||||
return None
|
||||
|
||||
teile = [t.strip() for t in flurstueck_wert.strip().split("/")]
|
||||
if len(teile) != 4:
|
||||
return None
|
||||
|
||||
gemarkung, zaehler, nenner, folge = teile
|
||||
|
||||
gemarkung_kz = gemarkung.zfill(4)
|
||||
flur_kz = "___"
|
||||
zaehler_kz = zaehler.zfill(5)
|
||||
|
||||
nenner_ohne_null = nenner.lstrip("0")
|
||||
nenner_kz = "____" if not nenner_ohne_null else nenner.zfill(4)
|
||||
|
||||
return f"{laenderkennung}{gemarkung_kz}{flur_kz}{zaehler_kz}{nenner_kz}{folge}"
|
||||
|
||||
|
||||
def baue_flurstueck_csv_liste(zeilen: List[dict], laenderkennung: str) -> List[str]:
|
||||
"""Bildet für alle CSV-Zeilen ``Flurstueck_csv`` und gibt die Liste zurück.
|
||||
|
||||
Schreibt das Ergebnis zusätzlich als Attribut ``Flurstueck_csv`` in jede
|
||||
Zeile zurück und gibt die ersten 20 Einträge als DEBUG-Ausgabe aus.
|
||||
"""
|
||||
ergebnisse: List[str] = []
|
||||
for zeile in zeilen:
|
||||
wert = zeile.get(CSV_SPALTE_FLURSTUECK, "")
|
||||
fkz = baue_flurstueck_csv(wert, laenderkennung)
|
||||
zeile[FELD_FLURSTUECK_CSV] = fkz or ""
|
||||
if fkz:
|
||||
ergebnisse.append(fkz)
|
||||
|
||||
print(f">>> Flurstueck_csv – {len(ergebnisse)} gebildet, erste 20:")
|
||||
for wert in ergebnisse[:20]:
|
||||
print(f">>> {wert}")
|
||||
|
||||
return ergebnisse
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Hilfsfunktionen: Layer-Lookup
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _hole_verfahrensgebiet_layer(tab_widget) -> Optional[object]:
|
||||
"""Liefert den aktuell relevanten Verfahrensgebiet-Layer, falls vorhanden."""
|
||||
combo = getattr(tab_widget, "verf_layer_combo", None)
|
||||
if combo is not None:
|
||||
layer = combo.currentLayer()
|
||||
if layer is not None and layer.isValid():
|
||||
return layer
|
||||
|
||||
treffer = QgsProject.instance().mapLayersByName(VERF_LAYER_NAME)
|
||||
return treffer[0] if treffer else None
|
||||
|
||||
|
||||
def _hat_verwendbare_kennzeichen(verf_layer) -> bool:
|
||||
try:
|
||||
return bool(get_kennzeichen_menge(verf_layer))
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _hole_flurstuecke_layer() -> Optional[object]:
|
||||
treffer = QgsProject.instance().mapLayersByName(FLST_LAYER_NAME)
|
||||
return treffer[0] if treffer else None
|
||||
|
||||
|
||||
def _flurstueckskennzeichen_geometrien(flst_layer) -> Dict[str, object]:
|
||||
"""Baut ``flurstueckskennzeichen`` → Geometrie-Map aus dem Flurstücke-Layer."""
|
||||
mapping: Dict[str, object] = {}
|
||||
if flst_layer is None:
|
||||
return mapping
|
||||
if flst_layer.fields().indexFromName(FELD_FLURSTUECKSKENNZEICHEN_WFS) < 0:
|
||||
return mapping
|
||||
for feat in flst_layer.getFeatures():
|
||||
try:
|
||||
kz = str(feat[FELD_FLURSTUECKSKENNZEICHEN_WFS] or "").strip()
|
||||
except Exception:
|
||||
kz = ""
|
||||
if kz:
|
||||
mapping[kz] = feat.geometry()
|
||||
return mapping
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# try1: schneller Listenabgleich
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _try1_listenabgleich(verf_layer, flurstueck_csv_liste: List[str]) -> Tuple[object, Dict[str, int]]:
|
||||
verf_menge = get_kennzeichen_menge(verf_layer)
|
||||
csv_menge = set(flurstueck_csv_liste)
|
||||
|
||||
beide = sorted(verf_menge & csv_menge)
|
||||
nur_verfahrensgebiet = sorted(verf_menge - csv_menge)
|
||||
nur_aglb = sorted(csv_menge - verf_menge)
|
||||
|
||||
flst_layer = _hole_flurstuecke_layer()
|
||||
geom_map = _flurstueckskennzeichen_geometrien(flst_layer)
|
||||
if flst_layer is not None and flst_layer.isValid():
|
||||
crs = flst_layer.crs().authid()
|
||||
elif verf_layer.isValid():
|
||||
crs = verf_layer.crs().authid()
|
||||
else:
|
||||
crs = "EPSG:25833"
|
||||
|
||||
layer = QgsVectorLayer(f"Polygon?crs={crs}", ERGEBNIS_LAYER_VERGLEICH, "memory")
|
||||
pr = layer.dataProvider()
|
||||
pr.addAttributes([
|
||||
QgsField(FELD_FLURSTUECK_CSV, QVariant.String),
|
||||
QgsField("status", QVariant.String),
|
||||
])
|
||||
layer.updateFields()
|
||||
fields = layer.fields()
|
||||
|
||||
neue_features = []
|
||||
for status, menge in (
|
||||
(WERT_BEIDE, beide),
|
||||
(WERT_NUR_VERFAHRENSGEBIET, nur_verfahrensgebiet),
|
||||
(WERT_NUR_AGLB, nur_aglb),
|
||||
):
|
||||
for kz in menge:
|
||||
feat = QgsFeature(fields)
|
||||
geom = geom_map.get(kz)
|
||||
if geom is not None:
|
||||
feat.setGeometry(geom)
|
||||
feat.setAttributes([kz, status])
|
||||
neue_features.append(feat)
|
||||
|
||||
pr.addFeatures(neue_features)
|
||||
layer.updateExtents()
|
||||
|
||||
statistik = {
|
||||
WERT_BEIDE: len(beide),
|
||||
WERT_NUR_VERFAHRENSGEBIET: len(nur_verfahrensgebiet),
|
||||
WERT_NUR_AGLB: len(nur_aglb),
|
||||
}
|
||||
return layer, statistik
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# try2: WFS-Einzelabgleich
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _try2_wfs_einzelabgleich(
|
||||
flurstueck_csv_liste: List[str],
|
||||
) -> Tuple[Optional[object], object, Dict[str, int]]:
|
||||
erfolgreich: List[tuple] = []
|
||||
nicht_gefunden: List[str] = []
|
||||
schema_fields = None
|
||||
crs = "EPSG:25833"
|
||||
dienst_erreichbar = False
|
||||
|
||||
for kz in flurstueck_csv_liste:
|
||||
uri = (
|
||||
f"url='{ALKIS_SF_URL}' typename='{FLST_TYPENAME}' srsname='EPSG:25833' "
|
||||
f"sql=SELECT * FROM AX_Flurstueck "
|
||||
f"WHERE {FELD_FLURSTUECKSKENNZEICHEN_WFS} = '{kz}'"
|
||||
)
|
||||
wfs = QgsVectorLayer(uri, f"_TmpBet{kz}", "WFS")
|
||||
if not wfs.isValid():
|
||||
continue
|
||||
dienst_erreichbar = True
|
||||
|
||||
feats = list(wfs.getFeatures())
|
||||
if not feats:
|
||||
nicht_gefunden.append(kz)
|
||||
continue
|
||||
|
||||
if schema_fields is None:
|
||||
schema_fields = wfs.fields()
|
||||
crs = wfs.crs().authid()
|
||||
erfolgreich.append((wfs.fields(), feats[0]))
|
||||
|
||||
if not dienst_erreichbar:
|
||||
raise BeteiligungPruefungFehler(
|
||||
f"Dienst nicht erreichbar – ALKIS-WFS ({ALKIS_SF_URL}) konnte nicht "
|
||||
f"abgefragt werden. Bitte Netzwerkverbindung prüfen."
|
||||
)
|
||||
|
||||
erfolg_layer = None
|
||||
if schema_fields is not None:
|
||||
erfolg_layer = QgsVectorLayer(f"Polygon?crs={crs}", ERGEBNIS_LAYER_ERFOLGREICH, "memory")
|
||||
pr = erfolg_layer.dataProvider()
|
||||
pr.addAttributes(list(schema_fields))
|
||||
erfolg_layer.updateFields()
|
||||
mem_fields = erfolg_layer.fields()
|
||||
|
||||
neue_features = []
|
||||
for wfs_fields, feat in erfolgreich:
|
||||
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)
|
||||
erfolg_layer.updateExtents()
|
||||
|
||||
ng_layer = QgsVectorLayer(
|
||||
f"None?field={FELD_FLURSTUECK_CSV}:string", ERGEBNIS_LAYER_NICHT_GEFUNDEN, "memory"
|
||||
)
|
||||
pr = ng_layer.dataProvider()
|
||||
ng_fields = ng_layer.fields()
|
||||
neue_ng = []
|
||||
for kz in nicht_gefunden:
|
||||
f = QgsFeature(ng_fields)
|
||||
f.setAttribute(0, kz)
|
||||
neue_ng.append(f)
|
||||
pr.addFeatures(neue_ng)
|
||||
|
||||
statistik = {"erfolgreich": len(erfolgreich), "nicht_gefunden": len(nicht_gefunden)}
|
||||
return erfolg_layer, ng_layer, statistik
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# try3: Bootstrap (Verfahrensnummer aus CSV, VG laden) + try2
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _lese_vef_vkzf(zeilen: List[dict], spaltennamen: List[str]) -> str:
|
||||
if CSV_SPALTE_VEF_VKZF not in spaltennamen:
|
||||
raise BeteiligungPruefungFehler(
|
||||
f"Die CSV enthält nicht das benötigte Attribut '{CSV_SPALTE_VEF_VKZF}'."
|
||||
)
|
||||
for zeile in zeilen:
|
||||
wert = (zeile.get(CSV_SPALTE_VEF_VKZF) or "").strip()
|
||||
if wert:
|
||||
return wert
|
||||
raise BeteiligungPruefungFehler(
|
||||
f"Die CSV enthält keinen befüllten Wert für '{CSV_SPALTE_VEF_VKZF}'."
|
||||
)
|
||||
|
||||
|
||||
def _try3_bootstrap_und_try2(
|
||||
tab_widget, zeilen: List[dict], spaltennamen: List[str], flurstueck_csv_liste: List[str]
|
||||
) -> BeteiligungErgebnis:
|
||||
vef_vkzf = _lese_vef_vkzf(zeilen, spaltennamen)
|
||||
set_variable("verfahrensnummer", vef_vkzf, scope="project")
|
||||
|
||||
# Identische BROR-Ladung wie die bestehende ALKIS-Pipeline, ohne die
|
||||
# nachgelagerte Detailprüfung/Flächenvergleich aus verfahrensgebiet_alkis_komplett.
|
||||
neuer_vg_layer, status, _feats = verfahrensgebiet_alkis(tab_widget, skip_existing_check=True)
|
||||
if neuer_vg_layer is None:
|
||||
# verfahrensgebiet_alkis() hat bereits eine erklärende Messagebox gezeigt.
|
||||
raise BeteiligungPruefungFehler(
|
||||
f"Verfahrensgebiet konnte für Verfahrensnummer '{vef_vkzf}' nicht geladen werden."
|
||||
)
|
||||
|
||||
erfolg_layer, ng_layer, statistik = _try2_wfs_einzelabgleich(flurstueck_csv_liste)
|
||||
|
||||
layers: List[Tuple[object, Optional[str]]] = [(neuer_vg_layer, "verfahrensgebiet.qml")]
|
||||
if erfolg_layer is not None:
|
||||
layers.append((erfolg_layer, "beteiligung_erfolgreich.qml"))
|
||||
layers.append((ng_layer, None))
|
||||
|
||||
text = (
|
||||
f"Verfahrensnummer aus CSV: {vef_vkzf}\n"
|
||||
f"Erfolgreich geladen: {statistik['erfolgreich']}\n"
|
||||
f"Nicht gefunden: {statistik['nicht_gefunden']}"
|
||||
)
|
||||
return BeteiligungErgebnis("try3", layers, statistik, text)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Öffentliche API
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def beteiligung_pruefen(tab_widget) -> Optional[BeteiligungErgebnis]:
|
||||
"""Vollständiger Ablauf: CSV laden → Flurstueck_csv bilden → try1/try2/try3.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Optional[BeteiligungErgebnis]
|
||||
``None`` bei Nutzerabbruch des Dateidialogs. Sonst das Ergebnis mit
|
||||
den ins Projekt einzufügenden Layern und der Statistik.
|
||||
|
||||
Raises
|
||||
------
|
||||
BeteiligungPruefungFehler
|
||||
Bei einem der dokumentierten Fehlerfälle (CSV-Format, fehlende
|
||||
Spalten, WFS-Dienst nicht erreichbar). Der Aufrufer zeigt daraufhin
|
||||
eine Messagebox an.
|
||||
"""
|
||||
geladen = lade_beteiligung_csv(tab_widget)
|
||||
if geladen is None:
|
||||
return None
|
||||
zeilen, spaltennamen = geladen
|
||||
|
||||
laenderkennung = get_laenderkennung("sn")
|
||||
flurstueck_csv_liste = baue_flurstueck_csv_liste(zeilen, laenderkennung)
|
||||
if not flurstueck_csv_liste:
|
||||
raise BeteiligungPruefungFehler(
|
||||
"Aus der CSV konnten keine gültigen Flurstück-Kennzeichen gebildet werden "
|
||||
"(Attribut 'FLURSTUECK' muss 4 durch '/' getrennte Teile enthalten)."
|
||||
)
|
||||
|
||||
verf_layer = _hole_verfahrensgebiet_layer(tab_widget)
|
||||
|
||||
if verf_layer is not None and _hat_verwendbare_kennzeichen(verf_layer):
|
||||
layer, statistik = _try1_listenabgleich(verf_layer, flurstueck_csv_liste)
|
||||
text = (
|
||||
f"Übereinstimmend: {statistik[WERT_BEIDE]}\n"
|
||||
f"Nur im Verfahrensgebiet: {statistik[WERT_NUR_VERFAHRENSGEBIET]}\n"
|
||||
f"Nur in der CSV-Liste (AGLB): {statistik[WERT_NUR_AGLB]}"
|
||||
)
|
||||
return BeteiligungErgebnis("try1", [(layer, "beteiligung_vergleich.qml")], statistik, text)
|
||||
|
||||
if verf_layer is not None:
|
||||
erfolg_layer, ng_layer, statistik = _try2_wfs_einzelabgleich(flurstueck_csv_liste)
|
||||
layers: List[Tuple[object, Optional[str]]] = []
|
||||
if erfolg_layer is not None:
|
||||
layers.append((erfolg_layer, "beteiligung_erfolgreich.qml"))
|
||||
layers.append((ng_layer, None))
|
||||
text = (
|
||||
f"Erfolgreich geladen: {statistik['erfolgreich']}\n"
|
||||
f"Nicht gefunden: {statistik['nicht_gefunden']}"
|
||||
)
|
||||
return BeteiligungErgebnis("try2", layers, statistik, text)
|
||||
|
||||
return _try3_bootstrap_und_try2(tab_widget, zeilen, spaltennamen, flurstueck_csv_liste)
|
||||
@@ -0,0 +1,22 @@
|
||||
<!DOCTYPE qgis PUBLIC 'http://mrcc.com/qgis.dtd' 'SYSTEM'>
|
||||
<qgis layerType="Vector" styleCategories="Symbology" version="4.0.1-Norrköping">
|
||||
<renderer-v2 enableorderby="0" forceraster="0" referencescale="-1" symbollevels="0" type="singleSymbol">
|
||||
<symbols>
|
||||
<symbol alpha="1" clip_to_extent="1" force_rhr="0" frame_rate="10" is_animated="0" name="0" type="fill">
|
||||
<layer class="SimpleFill" enabled="1" locked="0" pass="0">
|
||||
<Option type="Map">
|
||||
<Option name="color" type="QString" value="0,180,0,150,rgb:0,0.7058824,0,0.5882353"/>
|
||||
<Option name="outline_color" type="QString" value="0,120,0,255,rgb:0,0.4705882,0,1"/>
|
||||
<Option name="outline_style" type="QString" value="solid"/>
|
||||
<Option name="outline_width" type="QString" value="0.4"/>
|
||||
<Option name="outline_width_unit" type="QString" value="MM"/>
|
||||
<Option name="style" type="QString" value="solid"/>
|
||||
</Option>
|
||||
</layer>
|
||||
</symbol>
|
||||
</symbols>
|
||||
</renderer-v2>
|
||||
<blendMode>0</blendMode>
|
||||
<featureBlendMode>0</featureBlendMode>
|
||||
<layerGeometryType>2</layerGeometryType>
|
||||
</qgis>
|
||||
@@ -0,0 +1,51 @@
|
||||
<!DOCTYPE qgis PUBLIC 'http://mrcc.com/qgis.dtd' 'SYSTEM'>
|
||||
<qgis layerType="Vector" styleCategories="Symbology" version="4.0.1-Norrköping">
|
||||
<renderer-v2 attr="status" enableorderby="0" forceraster="0" referencescale="-1" symbollevels="0" type="categorizedSymbol">
|
||||
<categories>
|
||||
<category label="beide (Übereinstimmung)" render="true" symbol="0" value="beide"/>
|
||||
<category label="nur im Verfahrensgebiet" render="true" symbol="1" value="nur_verfahrensgebiet"/>
|
||||
<category label="nur in der CSV-Liste (AGLB)" render="true" symbol="2" value="nur_aglb"/>
|
||||
</categories>
|
||||
<symbols>
|
||||
<symbol alpha="1" clip_to_extent="1" force_rhr="0" frame_rate="10" is_animated="0" name="0" type="fill">
|
||||
<layer class="SimpleFill" enabled="1" locked="0" pass="0">
|
||||
<Option type="Map">
|
||||
<Option name="color" type="QString" value="0,180,0,150,rgb:0,0.7058824,0,0.5882353"/>
|
||||
<Option name="outline_color" type="QString" value="0,120,0,255,rgb:0,0.4705882,0,1"/>
|
||||
<Option name="outline_style" type="QString" value="solid"/>
|
||||
<Option name="outline_width" type="QString" value="0.4"/>
|
||||
<Option name="outline_width_unit" type="QString" value="MM"/>
|
||||
<Option name="style" type="QString" value="solid"/>
|
||||
</Option>
|
||||
</layer>
|
||||
</symbol>
|
||||
<symbol alpha="1" clip_to_extent="1" force_rhr="0" frame_rate="10" is_animated="0" name="1" type="fill">
|
||||
<layer class="SimpleFill" enabled="1" locked="0" pass="0">
|
||||
<Option type="Map">
|
||||
<Option name="color" type="QString" value="220,0,0,150,rgb:0.8627451,0,0,0.5882353"/>
|
||||
<Option name="outline_color" type="QString" value="150,0,0,255,rgb:0.5882353,0,0,1"/>
|
||||
<Option name="outline_style" type="QString" value="solid"/>
|
||||
<Option name="outline_width" type="QString" value="0.4"/>
|
||||
<Option name="outline_width_unit" type="QString" value="MM"/>
|
||||
<Option name="style" type="QString" value="solid"/>
|
||||
</Option>
|
||||
</layer>
|
||||
</symbol>
|
||||
<symbol alpha="1" clip_to_extent="1" force_rhr="0" frame_rate="10" is_animated="0" name="2" type="fill">
|
||||
<layer class="SimpleFill" enabled="1" locked="0" pass="0">
|
||||
<Option type="Map">
|
||||
<Option name="color" type="QString" value="255,128,0,150,rgb:1,0.5019608,0,0.5882353"/>
|
||||
<Option name="outline_color" type="QString" value="190,96,0,255,rgb:0.745098,0.3764706,0,1"/>
|
||||
<Option name="outline_style" type="QString" value="solid"/>
|
||||
<Option name="outline_width" type="QString" value="0.4"/>
|
||||
<Option name="outline_width_unit" type="QString" value="MM"/>
|
||||
<Option name="style" type="QString" value="solid"/>
|
||||
</Option>
|
||||
</layer>
|
||||
</symbol>
|
||||
</symbols>
|
||||
</renderer-v2>
|
||||
<blendMode>0</blendMode>
|
||||
<featureBlendMode>0</featureBlendMode>
|
||||
<layerGeometryType>2</layerGeometryType>
|
||||
</qgis>
|
||||
@@ -0,0 +1,157 @@
|
||||
"""sn_verfahrensgebiet/tests/test_beteiligung_pruefung.py
|
||||
|
||||
Unit-Tests für :mod:`sn_verfahrensgebiet.functions.beteiligung_pruefung`.
|
||||
|
||||
Getestete Szenarien:
|
||||
|
||||
baue_flurstueck_csv:
|
||||
- Normalfall mit führenden Nullen bei Zähler/Nenner
|
||||
- Nenner = 0 → Platzhalter "____"
|
||||
- Nenner leer → Platzhalter "____"
|
||||
- Ungültiges Format (nicht 4 Teile) → None
|
||||
- Leerer Wert → None
|
||||
|
||||
baue_flurstueck_csv_liste:
|
||||
- Bildet Liste nur aus gültigen Zeilen, schreibt Flurstueck_csv in jede Zeile zurück
|
||||
|
||||
lade_beteiligung_csv:
|
||||
- Dateidialog abgebrochen → None
|
||||
- Spalte FLURSTUECK fehlt → BeteiligungPruefungFehler
|
||||
- Leere Datei → BeteiligungPruefungFehler
|
||||
- Erfolgsfall → (zeilen, spaltennamen)
|
||||
"""
|
||||
|
||||
import csv
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import types
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Minimales QGIS-Mock-Setup
|
||||
# ---------------------------------------------------------------------------
|
||||
if "qgis" not in sys.modules:
|
||||
qgis_mod = types.ModuleType("qgis")
|
||||
qgis_core = types.ModuleType("qgis.core")
|
||||
qgis_utils = types.ModuleType("qgis.utils")
|
||||
qgis_utils.iface = types.SimpleNamespace()
|
||||
qgis_utils.plugins = {}
|
||||
qgis_mod.utils = qgis_utils
|
||||
qgis_mod.core = qgis_core
|
||||
sys.modules["qgis"] = qgis_mod
|
||||
sys.modules["qgis.core"] = qgis_core
|
||||
sys.modules["qgis.utils"] = qgis_utils
|
||||
|
||||
from sn_verfahrensgebiet.functions.beteiligung_pruefung import (
|
||||
baue_flurstueck_csv,
|
||||
baue_flurstueck_csv_liste,
|
||||
lade_beteiligung_csv,
|
||||
BeteiligungPruefungFehler,
|
||||
FELD_FLURSTUECK_CSV,
|
||||
CSV_SPALTE_FLURSTUECK,
|
||||
)
|
||||
|
||||
_MOD = "sn_verfahrensgebiet.functions.beteiligung_pruefung"
|
||||
|
||||
|
||||
class TestBaueFlurstueckCsv(unittest.TestCase):
|
||||
def test_normalfall_mit_fuehrenden_nullen(self):
|
||||
ergebnis = baue_flurstueck_csv("1234/123/45/00", "14")
|
||||
self.assertEqual(ergebnis, "141234___00123004500")
|
||||
|
||||
def test_nenner_null_wird_zu_unterstrichen(self):
|
||||
ergebnis = baue_flurstueck_csv("1234/123/0/00", "14")
|
||||
self.assertEqual(ergebnis, "141234___00123____00")
|
||||
|
||||
def test_nenner_leer_wird_zu_unterstrichen(self):
|
||||
ergebnis = baue_flurstueck_csv("1234//45/00", "14")
|
||||
# Gemarkung 1234, Zähler leer -> "00000"? -> zfill auf leeren String ergibt "00000"
|
||||
self.assertTrue(ergebnis.startswith("141234___00000"))
|
||||
|
||||
def test_gemarkung_ohne_fuehrende_nullen_wird_aufgefuellt(self):
|
||||
ergebnis = baue_flurstueck_csv("12/1/1/00", "14")
|
||||
self.assertTrue(ergebnis.startswith("140012___"))
|
||||
|
||||
def test_ungueltiges_format_gibt_none(self):
|
||||
self.assertIsNone(baue_flurstueck_csv("1234/123/45", "14"))
|
||||
self.assertIsNone(baue_flurstueck_csv("1234/123/45/00/99", "14"))
|
||||
|
||||
def test_leerer_wert_gibt_none(self):
|
||||
self.assertIsNone(baue_flurstueck_csv("", "14"))
|
||||
self.assertIsNone(baue_flurstueck_csv(None, "14"))
|
||||
|
||||
|
||||
class TestBaueFlurstueckCsvListe(unittest.TestCase):
|
||||
def test_nur_gueltige_zeilen_in_liste(self):
|
||||
zeilen = [
|
||||
{CSV_SPALTE_FLURSTUECK: "1234/123/45/00"},
|
||||
{CSV_SPALTE_FLURSTUECK: "ungueltig"},
|
||||
{CSV_SPALTE_FLURSTUECK: "5678/1/0/01"},
|
||||
]
|
||||
ergebnis = baue_flurstueck_csv_liste(zeilen, "14")
|
||||
self.assertEqual(len(ergebnis), 2)
|
||||
self.assertEqual(zeilen[0][FELD_FLURSTUECK_CSV], "141234___00123004500")
|
||||
self.assertEqual(zeilen[1][FELD_FLURSTUECK_CSV], "")
|
||||
self.assertEqual(zeilen[2][FELD_FLURSTUECK_CSV], "145678___00001____01")
|
||||
|
||||
|
||||
class TestLadeBeteiligungCsv(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self._tmp_files = []
|
||||
|
||||
def tearDown(self):
|
||||
for pfad in self._tmp_files:
|
||||
try:
|
||||
os.remove(pfad)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
def _schreibe_csv(self, header, zeilen):
|
||||
fd, pfad = tempfile.mkstemp(suffix=".csv")
|
||||
self._tmp_files.append(pfad)
|
||||
with os.fdopen(fd, "w", encoding="utf-8", newline="") as f:
|
||||
writer = csv.writer(f)
|
||||
writer.writerow(header)
|
||||
for zeile in zeilen:
|
||||
writer.writerow(zeile)
|
||||
return pfad
|
||||
|
||||
@patch(f"{_MOD}.QFileDialog")
|
||||
def test_dialog_abgebrochen_gibt_none(self, mock_fd):
|
||||
mock_fd.getOpenFileName.return_value = ("", "")
|
||||
self.assertIsNone(lade_beteiligung_csv(None))
|
||||
|
||||
@patch(f"{_MOD}.QFileDialog")
|
||||
def test_fehlende_flurstueck_spalte_wirft_fehler(self, mock_fd):
|
||||
pfad = self._schreibe_csv(["FOO", "BAR"], [["1", "2"]])
|
||||
mock_fd.getOpenFileName.return_value = (pfad, "")
|
||||
with self.assertRaises(BeteiligungPruefungFehler) as ctx:
|
||||
lade_beteiligung_csv(None)
|
||||
self.assertIn(CSV_SPALTE_FLURSTUECK, str(ctx.exception))
|
||||
|
||||
@patch(f"{_MOD}.QFileDialog")
|
||||
def test_leere_datei_wirft_fehler(self, mock_fd):
|
||||
fd, pfad = tempfile.mkstemp(suffix=".csv")
|
||||
os.close(fd)
|
||||
self._tmp_files.append(pfad)
|
||||
mock_fd.getOpenFileName.return_value = (pfad, "")
|
||||
with self.assertRaises(BeteiligungPruefungFehler):
|
||||
lade_beteiligung_csv(None)
|
||||
|
||||
@patch(f"{_MOD}.QFileDialog")
|
||||
def test_erfolgsfall_gibt_zeilen_und_spalten_zurueck(self, mock_fd):
|
||||
pfad = self._schreibe_csv(
|
||||
["FLURSTUECK", "VEF_VKZF", "FLA_BETEILIGUNG"],
|
||||
[["1234/123/45/00", "12345", "1"]],
|
||||
)
|
||||
mock_fd.getOpenFileName.return_value = (pfad, "")
|
||||
zeilen, spalten = lade_beteiligung_csv(None)
|
||||
self.assertEqual(len(zeilen), 1)
|
||||
self.assertIn("FLURSTUECK", spalten)
|
||||
self.assertEqual(zeilen[0]["FLURSTUECK"], "1234/123/45/00")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -23,6 +23,10 @@ from sn_verfahrensgebiet.functions.flurstueck_alkis import (
|
||||
)
|
||||
from sn_verfahrensgebiet.functions.verfahrensgebiet_alkis_komplett import verfahrensgebiet_alkis_komplett
|
||||
from sn_verfahrensgebiet.functions.verfahrensgebiet_shape_komplett import verfahrensgebiet_shape_komplett
|
||||
from sn_verfahrensgebiet.functions.beteiligung_pruefung import (
|
||||
beteiligung_pruefen,
|
||||
BeteiligungPruefungFehler,
|
||||
)
|
||||
from sn_verfahrensgebiet.functions.grenzpunkt_alkis import grenzpunkte_gemarkungsweise, JOINED_LAYER_NAME
|
||||
from sn_verfahrensgebiet.functions.gemarkung_alkis import (
|
||||
gemarkung_schluessel_und_namen_von_vg_geometrie,
|
||||
@@ -72,6 +76,15 @@ class WorkingTab(QWidget):
|
||||
row_verf_buttons.addWidget(self.btn_verf_shape)
|
||||
layout.addLayout(row_verf_buttons)
|
||||
|
||||
row_beteiligung_buttons = QHBoxLayout()
|
||||
self.btn_beteiligung_pruefen = QPushButton("Beteiligung prüfen")
|
||||
self.btn_beteiligung_pruefen.setToolTip(
|
||||
"CSV-Export aus dem BI-Server verwenden. benötigte Spalten: "
|
||||
"FLA_Verfahren-VEF_VKZF, FLA-Flurstuecke-Flurstueck, FLA_Beteiligung"
|
||||
)
|
||||
row_beteiligung_buttons.addWidget(self.btn_beteiligung_pruefen)
|
||||
layout.addLayout(row_beteiligung_buttons)
|
||||
|
||||
# Abschnitt 1b: Verfahrensgebiet-Layer auswählen
|
||||
row_layer_label = QHBoxLayout()
|
||||
lbl_layer = QLabel("Oder: Verfahrensgebiet-Layer auswählen")
|
||||
@@ -171,6 +184,7 @@ class WorkingTab(QWidget):
|
||||
self.btn_verf_aus_liste.clicked.connect(self.handle_verf_aus_liste)
|
||||
self.btn_verf_shape.clicked.connect(self.handle_verf_shape)
|
||||
# self.btn_flurst_shape.clicked.connect(self.handle_flurst_shape)
|
||||
self.btn_beteiligung_pruefen.clicked.connect(self.handle_beteiligung_pruefen)
|
||||
|
||||
# Beim Init: aktuelle Verfahrensgebiet-Layer-ID laden und im Dropdown anzeigen
|
||||
self._restore_verf_layer_selection()
|
||||
@@ -278,6 +292,40 @@ class WorkingTab(QWidget):
|
||||
finally:
|
||||
self._set_busy(self.btn_verf_shape, False, "Aus Shape laden")
|
||||
|
||||
def handle_beteiligung_pruefen(self) -> None:
|
||||
"""Gleicht eine BI-Server-CSV-Beteiligungsliste mit dem Verfahrensgebiet ab.
|
||||
|
||||
Delegiert an :func:`~sn_verfahrensgebiet.functions.beteiligung_pruefung.beteiligung_pruefen`.
|
||||
Je nach Projektzustand (try1/try2/try3) werden ein oder mehrere
|
||||
Ergebnis-Layer ins Projekt eingefügt, gestylt und eine Statistik als
|
||||
Messagebox angezeigt.
|
||||
"""
|
||||
self._set_busy(self.btn_beteiligung_pruefen, True, "Beteiligung prüfen")
|
||||
try:
|
||||
try:
|
||||
ergebnis = beteiligung_pruefen(self)
|
||||
except BeteiligungPruefungFehler as exc:
|
||||
QMessageBox.critical(self, "Beteiligung prüfen", str(exc))
|
||||
return
|
||||
|
||||
if ergebnis is None:
|
||||
return # Nutzerabbruch des Dateidialogs
|
||||
|
||||
project = QgsProject.instance()
|
||||
for layer, style_name in ergebnis.layers:
|
||||
if layer is None or not layer.isValid():
|
||||
continue
|
||||
if not project.mapLayer(layer.id()):
|
||||
project.addMapLayer(layer)
|
||||
if style_name:
|
||||
apply_style(layer, style_name)
|
||||
|
||||
iface.mapCanvas().refresh()
|
||||
QMessageBox.information(self, "Beteiligung prüfen", ergebnis.statistik_text)
|
||||
|
||||
finally:
|
||||
self._set_busy(self.btn_beteiligung_pruefen, False, "Beteiligung prüfen")
|
||||
|
||||
def _set_busy(self, button: QPushButton, busy: bool, original_text: str = "Aus ALKIS laden") -> None:
|
||||
"""Setzt den Busy-State eines Buttons.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user