Compare commits
36
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b47e41bbe | ||
|
|
1a9595598e | ||
|
|
4bbdb96bcd | ||
|
|
aa6166e652 | ||
|
|
12b52d0468 | ||
|
|
cbd7b84889 | ||
|
|
28cfc75121 | ||
|
|
cee02ae935 | ||
|
|
6f24f93f01 | ||
|
|
a829716810 | ||
|
|
5e3261a8de | ||
|
|
e344a5ad4a | ||
|
|
8730e1bf96 | ||
|
|
00ee470e08 | ||
|
|
821ea4ec2b | ||
|
|
d1a9748461 | ||
|
|
b25b4c83ac | ||
|
|
7948131d5a | ||
|
|
16fa99a84e | ||
|
|
f8bdcde115 | ||
|
|
168b6ec46c | ||
|
|
bcbfa4c49e | ||
|
|
610e70ec5e | ||
|
|
1d7489f4d6 | ||
|
|
f1b463a1c5 | ||
|
|
189855e63b | ||
|
|
72c9d6fa15 | ||
|
|
9caf0e3c86 | ||
|
|
f6591b2099 | ||
|
|
1a2d3109d4 | ||
|
|
41b272092e | ||
|
|
5be2d3b4f9 | ||
|
|
62fae1c431 | ||
|
|
5531db9071 | ||
|
|
1796a7d5c0 | ||
|
|
98077e47cf |
+14
@@ -1,3 +1,17 @@
|
|||||||
|
"""sn_verfahrensgebiet – QGIS-Plugin für Verfahrensgebiets-Management.
|
||||||
|
Verwaltet die Abgrenzung und Verwaltung von Verfahrensgebieten.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
def classFactory(iface):
|
def classFactory(iface):
|
||||||
|
"""Factory-Funktion für QGIS.
|
||||||
|
Wird von QGIS aufgerufen, um die Plugin-Instanz zu erstellen.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
iface: QGIS-Schnittstellenobjekt
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Verfahrensgebiet: Instanziiertes Plugin
|
||||||
|
"""
|
||||||
from .main import Verfahrensgebiet
|
from .main import Verfahrensgebiet
|
||||||
return Verfahrensgebiet(iface)
|
return Verfahrensgebiet(iface)
|
||||||
|
|||||||
@@ -1,3 +1,25 @@
|
|||||||
---
|
---
|
||||||
|
Version 26.5.3-unstable:
|
||||||
|
---
|
||||||
|
Version 26.5.2-unstable:
|
||||||
|
---
|
||||||
|
Version 26.5.1-unstable:
|
||||||
|
---
|
||||||
|
Version 26.4.6-unstable:
|
||||||
|
---
|
||||||
|
Version 26.4.5-unstable:
|
||||||
|
---
|
||||||
|
Version 26.4.4-unstable:
|
||||||
|
---
|
||||||
|
Version 26.4.3-unstable:
|
||||||
|
---
|
||||||
|
Version 26.4.2-testing:
|
||||||
|
---
|
||||||
|
Version 26.4.1-unstable:
|
||||||
|
---
|
||||||
|
Version 26.3.3-unstable:
|
||||||
|
---
|
||||||
|
Version 26.3.2-unstable:
|
||||||
|
---
|
||||||
Version 26.3.1-unstable:
|
Version 26.3.1-unstable:
|
||||||
- erster Test als echtes PlugIn
|
- erster Test als echtes PlugIn
|
||||||
@@ -0,0 +1,241 @@
|
|||||||
|
"""sn_verfahrensgebiet/functions/flurstueck_alkis.py – ALKIS-Integration für Flurstücke.
|
||||||
|
|
||||||
|
Lädt Flurstücke aus dem ALKIS-WFS-Dienst (``adv:AX_Flurstueck``) anhand von
|
||||||
|
Gemarkungsschlüsseln, die der Nutzer interaktiv eingibt.
|
||||||
|
|
||||||
|
Der WFS-Layer wird direkt (nicht als Memory-Layer) ins Projekt geladen,
|
||||||
|
da die einzelnen Features für die spätere Auswahl benötigt werden.
|
||||||
|
|
||||||
|
**Erweiterung gegenüber dem Original:**
|
||||||
|
|
||||||
|
- ``vorgabe_schluessel``: Optional vorausgefüllte Gemarkungsnummern (z. B. aus
|
||||||
|
:func:`~sn_verfahrensgebiet.functions.gemarkung_alkis.gemarkung_schluessel_und_namen_by_bbox`).
|
||||||
|
Der Dialog wird mit diesem Vorschlag vorausgefüllt; der Nutzer kann die
|
||||||
|
Eingabe ändern oder ergänzen.
|
||||||
|
|
||||||
|
- ``vorgabe_namen``: Zugehörige Bezeichnungen, die im Dialog als Hinweis
|
||||||
|
angezeigt werden (kein eigenständiges Feld, nur Anzeigetext).
|
||||||
|
|
||||||
|
- ``bbox``: Optionaler BBox-Parameter, der – sofern übergeben – als
|
||||||
|
zusätzlicher serverseitiger Filter in die WFS-URI eingebettet wird. So
|
||||||
|
werden ausschließlich Flurstücke geladen, die den Verfahrensgebiets-Rahmen
|
||||||
|
überlappen.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import List, Optional, Tuple
|
||||||
|
|
||||||
|
from sn_basis.functions import QgsVectorLayer, QgsProject, QgsFeature
|
||||||
|
from sn_basis.functions import QMessageBox
|
||||||
|
from sn_basis.functions.dialog_wrapper import ask_text
|
||||||
|
from sn_basis.functions.message_wrapper import info as _info
|
||||||
|
|
||||||
|
# LoadStatus aus verfahrensgebiet_alkis wiederverwenden (identische Semantik)
|
||||||
|
from sn_verfahrensgebiet.functions.verfahrensgebiet_alkis import LoadStatus
|
||||||
|
|
||||||
|
ALKIS_SF_URL = "https://geodienste.sachsen.de/aaa/public_alkis/sf/wfs"
|
||||||
|
FLST_TYPENAME = "adv:AX_Flurstueck"
|
||||||
|
FLST_LAYER_NAME = "Flurstücke"
|
||||||
|
|
||||||
|
|
||||||
|
def _check_existing_flurstueck_layer(tab_widget):
|
||||||
|
"""Prüft, ob ein Flurstück-Layer bereits im Projekt existiert.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
Tuple[Optional[QgsVectorLayer], LoadStatus]
|
||||||
|
Bei ``LoadStatus.KEEP`` enthält das Tuple den vorhandenen Layer,
|
||||||
|
bei ``LoadStatus.FIRST`` und ``LoadStatus.RELOAD`` ist es None.
|
||||||
|
"""
|
||||||
|
project = QgsProject.instance()
|
||||||
|
existing_layers = project.mapLayersByName(FLST_LAYER_NAME)
|
||||||
|
if not existing_layers:
|
||||||
|
return None, LoadStatus.FIRST
|
||||||
|
|
||||||
|
reply = QMessageBox.question(
|
||||||
|
tab_widget,
|
||||||
|
"Layer bereits vorhanden",
|
||||||
|
f"Der Layer '{FLST_LAYER_NAME}' existiert bereits.\n"
|
||||||
|
"Möchten Sie ihn löschen und neu laden?",
|
||||||
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||||
|
QMessageBox.StandardButton.No,
|
||||||
|
)
|
||||||
|
if reply == QMessageBox.StandardButton.Yes:
|
||||||
|
for lyr in existing_layers:
|
||||||
|
project.removeMapLayer(lyr.id())
|
||||||
|
return None, LoadStatus.RELOAD
|
||||||
|
else:
|
||||||
|
return existing_layers[0], LoadStatus.KEEP
|
||||||
|
|
||||||
|
|
||||||
|
def _lade_flurstuecke_per_gemarkung(schluessel_liste: List[str]):
|
||||||
|
"""Lädt Flurstücke für mehrere Gemarkungen, eine WFS-Anfrage pro Schlüssel.
|
||||||
|
|
||||||
|
Vermeidet das WFS-Feature-Limit einer einzelnen Abfrage mit mehreren
|
||||||
|
Gemarkungsnummern. Alle Features werden in einem Memory-Layer zusammengeführt.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
schluessel_liste : list of str
|
||||||
|
4-stellige Gemarkungsnummern.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
QgsVectorLayer or None
|
||||||
|
Memory-Layer mit allen Flurstücken, oder ``None`` wenn nichts geladen wurde.
|
||||||
|
"""
|
||||||
|
alle_feats: list = []
|
||||||
|
schema_fields = None
|
||||||
|
schema_crs = "EPSG:25833"
|
||||||
|
|
||||||
|
for schluessel in schluessel_liste:
|
||||||
|
uri = (
|
||||||
|
f"url='{ALKIS_SF_URL}' typename='{FLST_TYPENAME}' srsname='EPSG:25833' "
|
||||||
|
f"sql=SELECT * FROM AX_Flurstueck "
|
||||||
|
f"WHERE gemarkung_ax_gemarkung_schluessel_gemarkungsnummer = '{schluessel}'"
|
||||||
|
)
|
||||||
|
wfs = QgsVectorLayer(uri, f"_TmpFlst{schluessel}", "WFS")
|
||||||
|
if not wfs.isValid():
|
||||||
|
continue
|
||||||
|
feats = list(wfs.getFeatures())
|
||||||
|
if not feats:
|
||||||
|
continue
|
||||||
|
if schema_fields is None:
|
||||||
|
schema_fields = wfs.fields()
|
||||||
|
schema_crs = wfs.crs().authid()
|
||||||
|
_info(
|
||||||
|
"Flurstücke",
|
||||||
|
f"Gemarkung {schluessel}: {len(feats)} Flurstücke geladen.",
|
||||||
|
duration=3,
|
||||||
|
)
|
||||||
|
alle_feats.append((wfs.fields(), feats))
|
||||||
|
|
||||||
|
if not alle_feats or schema_fields is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Memory-Layer mit dem Feldschema des ersten WFS-Layers aufbauen
|
||||||
|
mem = QgsVectorLayer(f"Polygon?crs={schema_crs}", FLST_LAYER_NAME, "memory")
|
||||||
|
pr = mem.dataProvider()
|
||||||
|
pr.addAttributes(list(schema_fields))
|
||||||
|
mem.updateFields()
|
||||||
|
mem_fields = mem.fields()
|
||||||
|
|
||||||
|
neue_features = []
|
||||||
|
for wfs_fields, feats in alle_feats:
|
||||||
|
for feat in feats:
|
||||||
|
new_f = QgsFeature(mem_fields)
|
||||||
|
new_f.setGeometry(feat.geometry())
|
||||||
|
for i in range(wfs_fields.count()):
|
||||||
|
fname = wfs_fields.field(i).name()
|
||||||
|
idx = mem_fields.indexFromName(fname)
|
||||||
|
if idx >= 0:
|
||||||
|
try:
|
||||||
|
new_f.setAttribute(idx, feat[fname])
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
neue_features.append(new_f)
|
||||||
|
|
||||||
|
pr.addFeatures(neue_features)
|
||||||
|
mem.updateExtents()
|
||||||
|
return mem
|
||||||
|
|
||||||
|
|
||||||
|
def flurstueck_alkis(
|
||||||
|
tab_widget,
|
||||||
|
vorgabe_schluessel: Optional[List[str]] = None,
|
||||||
|
vorgabe_namen: Optional[List[str]] = None,
|
||||||
|
bbox=None,
|
||||||
|
ohne_dialog: bool = False,
|
||||||
|
) -> Tuple[Optional[QgsVectorLayer], LoadStatus]:
|
||||||
|
"""Lädt Flurstücke aus dem ALKIS-WFS anhand von Gemarkungsschlüsseln.
|
||||||
|
|
||||||
|
Jede Gemarkungsnummer wird als separate WFS-Anfrage gestellt, um das
|
||||||
|
WFS-Feature-Limit bei einer Gesamtabfrage zu umgehen. Alle Ergebnisse
|
||||||
|
werden in einem Memory-Layer zusammengeführt.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
tab_widget :
|
||||||
|
Eltern-Widget für Dialoge.
|
||||||
|
vorgabe_schluessel : list of str, optional
|
||||||
|
Vorausgefüllte Gemarkungsnummern (4-stellig). Im Dialog als Standard
|
||||||
|
angezeigt; bei ``ohne_dialog=True`` direkt verwendet.
|
||||||
|
vorgabe_namen : list of str, optional
|
||||||
|
Menschenlesbare Gemarkungsbezeichnungen für den Dialog-Hinweistext.
|
||||||
|
bbox :
|
||||||
|
Nicht mehr verwendet (WFS unterstützt keinen BBox-Filter). Bleibt
|
||||||
|
für Rückwärtskompatibilität in der Signatur erhalten.
|
||||||
|
ohne_dialog : bool, optional
|
||||||
|
``True``: Überspringt die Gemarkungsschlüssel-Nutzerabfrage und verwendet
|
||||||
|
``vorgabe_schluessel`` direkt. Vorhandene Layer werden ohne Rückfrage
|
||||||
|
ersetzt. Gedacht für die automatische Pipeline
|
||||||
|
(``verfahrensgebiet_alkis_komplett``). Standard: ``False``.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
Tuple[Optional[QgsVectorLayer], LoadStatus]
|
||||||
|
Memory-Layer mit allen Flurstücken und Ladestatus.
|
||||||
|
Bei Fehlern ist der Layer ``None`` und der Status :attr:`LoadStatus.NONE`.
|
||||||
|
"""
|
||||||
|
# --- Schritt 1: Vorhandenen Layer behandeln ----------------------------
|
||||||
|
if ohne_dialog:
|
||||||
|
project = QgsProject.instance()
|
||||||
|
hatte_layer = bool(project.mapLayersByName(FLST_LAYER_NAME))
|
||||||
|
for lyr in list(project.mapLayersByName(FLST_LAYER_NAME)):
|
||||||
|
project.removeMapLayer(lyr.id())
|
||||||
|
status = LoadStatus.RELOAD if hatte_layer else LoadStatus.FIRST
|
||||||
|
else:
|
||||||
|
existing, status = _check_existing_flurstueck_layer(tab_widget)
|
||||||
|
if status == LoadStatus.KEEP:
|
||||||
|
return existing, status
|
||||||
|
|
||||||
|
# --- Schritt 2: Gemarkungsschlüssel bestimmen -------------------------
|
||||||
|
if ohne_dialog:
|
||||||
|
# Pipeline-Modus: vorgabe_schluessel direkt verwenden, kein Dialog
|
||||||
|
if not vorgabe_schluessel:
|
||||||
|
return None, LoadStatus.NONE
|
||||||
|
schluessel_liste = [s.strip() for s in vorgabe_schluessel if s.strip()]
|
||||||
|
if not schluessel_liste:
|
||||||
|
return None, LoadStatus.NONE
|
||||||
|
else:
|
||||||
|
# Interaktiver Modus: Dialog mit Vorschlag
|
||||||
|
default_text = ", ".join(vorgabe_schluessel) if vorgabe_schluessel else ""
|
||||||
|
hinweis = ""
|
||||||
|
if vorgabe_namen:
|
||||||
|
hinweis = " (Vorschlag: " + ", ".join(vorgabe_namen) + ")"
|
||||||
|
label = f"Gemarkungsschlüssel eingeben (mehrere mit Komma trennen):{hinweis}"
|
||||||
|
|
||||||
|
eingabe, ok = ask_text(
|
||||||
|
"Gemarkungsschlüssel",
|
||||||
|
label,
|
||||||
|
default_text=default_text,
|
||||||
|
parent=tab_widget,
|
||||||
|
)
|
||||||
|
if not ok or not eingabe.strip():
|
||||||
|
QMessageBox.warning(
|
||||||
|
tab_widget,
|
||||||
|
"Abgebrochen",
|
||||||
|
"Bitte mindestens einen gültigen Gemarkungsschlüssel eingeben.",
|
||||||
|
)
|
||||||
|
return None, LoadStatus.NONE
|
||||||
|
|
||||||
|
schluessel_liste = [s.strip() for s in eingabe.split(",") if s.strip()]
|
||||||
|
if not schluessel_liste:
|
||||||
|
QMessageBox.warning(tab_widget, "Fehler", "Keine gültigen Schlüssel gefunden.")
|
||||||
|
return None, LoadStatus.NONE
|
||||||
|
|
||||||
|
# --- Schritt 3: Flurstücke gemarkungsweise laden ----------------------
|
||||||
|
layer = _lade_flurstuecke_per_gemarkung(schluessel_liste)
|
||||||
|
|
||||||
|
if not layer or not layer.isValid() or layer.featureCount() == 0:
|
||||||
|
if not ohne_dialog:
|
||||||
|
QMessageBox.critical(
|
||||||
|
tab_widget,
|
||||||
|
"Fehler",
|
||||||
|
"Flurstück-Layer konnte nicht geladen werden.\n"
|
||||||
|
"Bitte Gemarkungsschlüssel und Netzwerkverbindung prüfen.",
|
||||||
|
)
|
||||||
|
return None, LoadStatus.NONE
|
||||||
|
|
||||||
|
return layer, status
|
||||||
@@ -0,0 +1,207 @@
|
|||||||
|
"""sn_verfahrensgebiet/functions/gemarkung_alkis.py – ALKIS-Gemarkungsabfrage.
|
||||||
|
|
||||||
|
Stellt Funktionen bereit, um Katasterbezirk-Objekte aus dem sächsischen
|
||||||
|
ALKIS-vereinfachten WFS zu laden und daraus Gemarkungsschlüssel zu extrahieren,
|
||||||
|
die als Vorschlag für die Flurstücksabfrage (Phase 2) dienen.
|
||||||
|
|
||||||
|
**Dienst:** ``https://geodienste.sachsen.de/aaa/public_alkis/vereinf/wfs``
|
||||||
|
**Typename:** ``Katasterbezirk``
|
||||||
|
|
||||||
|
Dieser Dienst hat – im Gegensatz zu ``adv:AX_Gemarkung`` aus dem NAS-Dienst –
|
||||||
|
tatsächliche Geometrien. Die client-seitige Verschneidung mit der VG-Geometrie
|
||||||
|
ist damit möglich.
|
||||||
|
|
||||||
|
**Attributkonvention:**
|
||||||
|
|
||||||
|
- ``schluessel``: vollständiger Katasterbezirksschlüssel (z.B. ``140640001``),
|
||||||
|
die letzten 4 Stellen entsprechen der
|
||||||
|
``gemarkung_ax_gemarkung_schluessel_gemarkungsnummer`` im NAS-Flurstücksdienst.
|
||||||
|
- ``ave_name``: Gemarkungsbezeichnung.
|
||||||
|
|
||||||
|
**Wichtig:** Diese Funktion stellt keine Nutzerabfragen. Sie gibt im
|
||||||
|
Fehlerfall eine leere Liste zurück.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import List, Optional, Tuple
|
||||||
|
|
||||||
|
from sn_basis.functions import QgsVectorLayer, QgsGeometry
|
||||||
|
from sn_basis.functions.message_wrapper import warning as _warn, info as _info
|
||||||
|
|
||||||
|
ALKIS_VEREINF_URL = "https://geodienste.sachsen.de/aaa/public_alkis/vereinf/wfs"
|
||||||
|
KATASTERBEZIRK_TYPENAME = "ave:KatasterBezirk"
|
||||||
|
|
||||||
|
# Attributnamen im vereinfachten Dienst
|
||||||
|
_ATTR_SCHLUESSEL = "schluessel" # vollständiger Schlüssel, letzte 4 = Gemarkungsnummer
|
||||||
|
_ATTR_NAME = "ave_name"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Interne Hilfsfunktionen
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _extract_schluessel_und_name(feature) -> Tuple[str, str]:
|
||||||
|
"""Extrahiert Gemarkungsnummer (letzte 4 Stellen von ``schluessel``)
|
||||||
|
und Namen (``ave_name``) aus einem Katasterbezirk-Feature.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
Tuple[str, str]
|
||||||
|
``(gemarkungsnummer_4stellig, name)`` – leer wenn nicht ermittelbar.
|
||||||
|
"""
|
||||||
|
def _get(field: str) -> str:
|
||||||
|
try:
|
||||||
|
val = feature[field]
|
||||||
|
return str(val).strip() if val else ""
|
||||||
|
except (KeyError, Exception):
|
||||||
|
return ""
|
||||||
|
|
||||||
|
schluessel = _get(_ATTR_SCHLUESSEL)
|
||||||
|
name = _get(_ATTR_NAME)
|
||||||
|
|
||||||
|
# Letzte 4 Stellen des Schlüssels = Gemarkungsnummer
|
||||||
|
nummer = schluessel[-4:] if len(schluessel) >= 4 else schluessel
|
||||||
|
|
||||||
|
return nummer, name
|
||||||
|
|
||||||
|
|
||||||
|
def _load_all_gemarkung_layer():
|
||||||
|
"""Lädt alle Katasterbezirk-Objekte aus dem vereinfachten ALKIS-WFS.
|
||||||
|
|
||||||
|
Sachsen hat ≈ 2 000 Gemarkungen. Der vereinfachte Dienst liefert
|
||||||
|
tatsächliche Geometrien, sodass client-seitige Verschneidung möglich ist.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
QgsVectorLayer or None
|
||||||
|
Geladener Layer oder ``None`` bei Ungültigkeit.
|
||||||
|
"""
|
||||||
|
uri = (
|
||||||
|
f"url='{ALKIS_VEREINF_URL}' typename='{KATASTERBEZIRK_TYPENAME}' "
|
||||||
|
f"srsname='EPSG:25833'"
|
||||||
|
)
|
||||||
|
layer = QgsVectorLayer(uri, "_TempKatasterbezirk", "WFS")
|
||||||
|
return layer if (layer and layer.isValid()) else None
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Öffentliche API
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def gemarkung_schluessel_und_namen_von_vg_geometrie(
|
||||||
|
vg_geometrie,
|
||||||
|
) -> List[Tuple[str, str]]:
|
||||||
|
"""Gibt Gemarkungsschlüssel und -namen aller Gemarkungen zurück, die die
|
||||||
|
VG-Geometrie schneiden.
|
||||||
|
|
||||||
|
Da der WFS-Dienst keine serverseitigen BBox- oder Geometriefilter
|
||||||
|
unterstützt, werden alle ``AX_Gemarkung``-Objekte geladen und client-seitig
|
||||||
|
per ``QgsGeometry.intersects`` gefiltert.
|
||||||
|
|
||||||
|
Der Aufruf ist *ohne Nutzerinteraktion* – im Fehlerfall wird eine leere
|
||||||
|
Liste zurückgegeben, damit der aufrufende Workflow manuellen Fallback
|
||||||
|
ermöglicht.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
vg_geometrie :
|
||||||
|
``QgsGeometry`` der Verfahrensgebiets-Union (aus den Roh-WFS-Features
|
||||||
|
von ``AX_BauRaumOderBodenordnungsrecht``). In EPSG:25833.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
List[Tuple[str, str]]
|
||||||
|
Liste von ``(gemarkungsnummer, bezeichnung)``-Tupeln, aufsteigend
|
||||||
|
sortiert nach Nummer. Enthält keine Duplikate. Leer bei Fehler.
|
||||||
|
|
||||||
|
Examples
|
||||||
|
--------
|
||||||
|
>>> from qgis.core import QgsGeometry
|
||||||
|
>>> vg_geom = QgsGeometry.fromWkt("POLYGON((414000 5657000, 415000 5657000, ...))")
|
||||||
|
>>> result = gemarkung_schluessel_und_namen_von_vg_geometrie(vg_geom)
|
||||||
|
>>> # result: [('1234', 'Beispielgemarkung'), ('5678', 'Andere Gemarkung')]
|
||||||
|
"""
|
||||||
|
if vg_geometrie is None or vg_geometrie.isEmpty():
|
||||||
|
_warn("Gemarkung", "[DIAG] VG-Geometrie ist None oder leer – Gemarkungssuche abgebrochen.", duration=8)
|
||||||
|
return []
|
||||||
|
|
||||||
|
try:
|
||||||
|
layer = _load_all_gemarkung_layer()
|
||||||
|
except Exception as exc:
|
||||||
|
_warn("Gemarkung", f"[DIAG] Fehler beim Laden des Gemarkung-Layers: {exc}", duration=8)
|
||||||
|
return []
|
||||||
|
|
||||||
|
if not layer:
|
||||||
|
_warn("Gemarkung", "[DIAG] AX_Gemarkung-Layer ungültig oder nicht ladbar.", duration=8)
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Feature-Anzahl ermitteln (0 = WFS gibt nichts zurück)
|
||||||
|
alle_features = list(layer.getFeatures())
|
||||||
|
if not alle_features:
|
||||||
|
_warn(
|
||||||
|
"Gemarkung",
|
||||||
|
"[DIAG] AX_Gemarkung-Layer ist gültig, aber enthält 0 Features. "
|
||||||
|
"WFS-Dienst liefert ohne Filter möglicherweise kein Ergebnis.",
|
||||||
|
duration=10,
|
||||||
|
)
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Feldnamen der ersten Features loggen
|
||||||
|
feldnamen = [alle_features[0].fields().field(i).name() for i in range(alle_features[0].fields().count())]
|
||||||
|
hat_geom = sum(1 for f in alle_features[:20] if f.hasGeometry() and not f.geometry().isEmpty())
|
||||||
|
_info(
|
||||||
|
"Gemarkung",
|
||||||
|
f"[DIAG] {len(alle_features)} Gemarkungen geladen. "
|
||||||
|
f"Von ersten 20 haben {hat_geom} Geometrie. "
|
||||||
|
f"Alle Felder ({len(feldnamen)}): {feldnamen}",
|
||||||
|
duration=15,
|
||||||
|
)
|
||||||
|
|
||||||
|
result: dict[str, str] = {}
|
||||||
|
kein_schnitt = 0
|
||||||
|
for feat in alle_features:
|
||||||
|
try:
|
||||||
|
gem_geom = feat.geometry()
|
||||||
|
if gem_geom is None or gem_geom.isEmpty():
|
||||||
|
continue
|
||||||
|
if not gem_geom.intersects(vg_geometrie):
|
||||||
|
kein_schnitt += 1
|
||||||
|
continue
|
||||||
|
except Exception as exc:
|
||||||
|
_warn("Gemarkung", f"[DIAG] intersects() Fehler: {exc}", duration=8)
|
||||||
|
continue
|
||||||
|
|
||||||
|
nummer, name = _extract_schluessel_und_name(feat)
|
||||||
|
if nummer and nummer not in result:
|
||||||
|
result[nummer] = name
|
||||||
|
|
||||||
|
if not result:
|
||||||
|
_warn(
|
||||||
|
"Gemarkung",
|
||||||
|
f"[DIAG] 0 Gemarkungen nach Verschneidung. {kein_schnitt} Features ohne Schnitt. "
|
||||||
|
f"VG-Geometrie gültig: {not vg_geometrie.isNull()}, "
|
||||||
|
f"VG-Typ: {vg_geometrie.type()}, VG-WKT (Anfang): {vg_geometrie.asWkt()[:80]}",
|
||||||
|
duration=15,
|
||||||
|
)
|
||||||
|
|
||||||
|
return sorted(result.items(), key=lambda t: t[0])
|
||||||
|
|
||||||
|
|
||||||
|
def gemarkung_schluessel_von_vg_geometrie(vg_geometrie) -> List[str]:
|
||||||
|
"""Gibt ausschließlich die Gemarkungsnummern für die VG-Geometrie zurück.
|
||||||
|
|
||||||
|
Convenience-Wrapper um
|
||||||
|
:func:`gemarkung_schluessel_und_namen_von_vg_geometrie`.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
vg_geometrie :
|
||||||
|
``QgsGeometry`` des Verfahrensgebiets in EPSG:25833.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
List[str]
|
||||||
|
Sortierte Liste der Gemarkungsnummern. Leer bei Fehler oder keinem Treffer.
|
||||||
|
"""
|
||||||
|
return [nr for nr, _ in gemarkung_schluessel_und_namen_von_vg_geometrie(vg_geometrie)]
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,467 @@
|
|||||||
|
"""sn_verfahrensgebiet/functions/knickpunkt_pruefung.py – Knickpunktbasierte BROR-Prüfung.
|
||||||
|
|
||||||
|
Vergleicht die Knickpunkte (Stützpunkte) des BauRaumOderBodenordnungsrecht-
|
||||||
|
Polygons mit den Punktorten der geladenen ``AX_PunktortTA``-Objekte und
|
||||||
|
identifiziert:
|
||||||
|
|
||||||
|
- **Identische Punkte** (grün): An einem BROR-Knickpunkt liegt ein PunktortTA.
|
||||||
|
- **Fehlende PunktortTA** (rot): Ein BROR-Knickpunkt hat keinen zugehörigen PunktortTA.
|
||||||
|
- **Fehlende Knickpunkte** (orange): Ein PunktortTA liegt nahe des BROR-Umrings
|
||||||
|
auf einer Flurstücksgrenze, hat aber keinen entsprechenden Knickpunkt im BROR.
|
||||||
|
|
||||||
|
**Algorithmus:**
|
||||||
|
|
||||||
|
1. BROR-Polygon-Stützpunkte (Knickpunkte) extrahieren.
|
||||||
|
2. Spatial-Index auf PunktortTA aufbauen.
|
||||||
|
3. Für jeden Knickpunkt nächsten PunktortTA im Radius von
|
||||||
|
:data:`KOORDINATEN_TOLERANZ_M` suchen → grün oder rot.
|
||||||
|
Gematchte PunktortTA-Feature-IDs merken.
|
||||||
|
4. Gepufferter BROR-Umring + Flurstück-Vertex-Set ermitteln.
|
||||||
|
5. Nicht-gematchte PunktortTA nahe BROR-Umring, die auf einer Flurstücksgrenze
|
||||||
|
liegen → orange.
|
||||||
|
|
||||||
|
**Rückgabe:** ``(result_layer, identisch, kein_pta, fehlender_knick)``
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Dict, List, Optional, Set, Tuple
|
||||||
|
|
||||||
|
from sn_basis.functions.qgiscore_wrapper import (
|
||||||
|
QgsVectorLayer,
|
||||||
|
QgsFeature,
|
||||||
|
QgsField,
|
||||||
|
QgsGeometry,
|
||||||
|
)
|
||||||
|
from sn_basis.functions.qt_wrapper import QVariant
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Konstanten
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#: Koordinatentoleranz für die Knickpunkt ↔ PunktortTA-Übereinstimmung (m).
|
||||||
|
KOORDINATEN_TOLERANZ_M: float = 0.01
|
||||||
|
|
||||||
|
#: Puffer um den BROR-Umring für die Suche nach fehlenden Knickpunkten (m).
|
||||||
|
BOUNDARY_PUFFER_M: float = 0.5
|
||||||
|
|
||||||
|
#: Rundungsgenauigkeit für Vertex-Koordinaten im Flurstück-Set (Dezimalstellen).
|
||||||
|
_VERTEX_ROUND = 3
|
||||||
|
|
||||||
|
ERGEBNIS_LAYER_NAME = "Knickpunktprüfung"
|
||||||
|
FELD_TYP = "typ"
|
||||||
|
WERT_IDENTISCH = "identisch"
|
||||||
|
WERT_KEIN_PTA = "kein_pta"
|
||||||
|
WERT_FEHLENDER_KNICK = "fehlender_knick"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Interne Hilfsfunktionen
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _extrahiere_knickpunkte(bror_layer) -> List[object]:
|
||||||
|
"""Extrahiert alle Stützpunkte (Knickpunkte) des BROR-Polygons.
|
||||||
|
|
||||||
|
Doppelte Start/End-Punkte eines Rings werden dedupliziert. Gibt eine
|
||||||
|
flache Liste von ``QgsPointXY``-Objekten zurück.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
bror_layer :
|
||||||
|
BauRaumOderBodenordnungsrecht-Polygon-Layer.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
List[QgsPointXY]
|
||||||
|
Alle eindeutigen Stützpunkte; leer bei Fehler.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from qgis.core import QgsPointXY # type: ignore[import]
|
||||||
|
|
||||||
|
bror_feats = [
|
||||||
|
f for f in bror_layer.getFeatures()
|
||||||
|
if f.geometry() and not f.geometry().isEmpty()
|
||||||
|
]
|
||||||
|
if not bror_feats:
|
||||||
|
return []
|
||||||
|
|
||||||
|
union_geom = QgsGeometry.unaryUnion([f.geometry() for f in bror_feats])
|
||||||
|
if union_geom is None or union_geom.isEmpty():
|
||||||
|
return []
|
||||||
|
|
||||||
|
punkte: List[object] = []
|
||||||
|
seen: Set[Tuple[float, float]] = set()
|
||||||
|
|
||||||
|
def _add_ring(vertices):
|
||||||
|
# Letzter Punkt = erster Punkt (Ring geschlossen) → überspringen
|
||||||
|
ring_pts = list(vertices)
|
||||||
|
n = len(ring_pts)
|
||||||
|
if n >= 2:
|
||||||
|
# Prüfen ob letzter Punkt identisch mit erstem
|
||||||
|
last = n - 1
|
||||||
|
if (
|
||||||
|
abs(ring_pts[last].x() - ring_pts[0].x()) < 1e-9
|
||||||
|
and abs(ring_pts[last].y() - ring_pts[0].y()) < 1e-9
|
||||||
|
):
|
||||||
|
ring_pts = ring_pts[:-1]
|
||||||
|
for pt in ring_pts:
|
||||||
|
key = (round(pt.x(), _VERTEX_ROUND), round(pt.y(), _VERTEX_ROUND))
|
||||||
|
if key not in seen:
|
||||||
|
seen.add(key)
|
||||||
|
punkte.append(pt)
|
||||||
|
|
||||||
|
geom_type = union_geom.wkbType()
|
||||||
|
# Polygon oder MultiPolygon
|
||||||
|
if union_geom.isMultipart():
|
||||||
|
for poly in union_geom.asMultiPolygon():
|
||||||
|
for ring in poly:
|
||||||
|
_add_ring([QgsPointXY(x, y) for x, y in ring])
|
||||||
|
else:
|
||||||
|
for ring in union_geom.asPolygon():
|
||||||
|
_add_ring([QgsPointXY(x, y) for x, y in ring])
|
||||||
|
|
||||||
|
return punkte
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def _erstelle_ergebnis_layer(crs_authid: str) -> object:
|
||||||
|
"""Erzeugt den leeren Punkt-Ergebnislayer mit Feld ``typ``.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
crs_authid :
|
||||||
|
CRS-Kennung, z.B. ``"EPSG:25833"``.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
QgsVectorLayer
|
||||||
|
Leerer Memory-Layer vom Typ Point.
|
||||||
|
"""
|
||||||
|
layer = QgsVectorLayer(
|
||||||
|
f"Point?crs={crs_authid}", ERGEBNIS_LAYER_NAME, "memory"
|
||||||
|
)
|
||||||
|
pr = layer.dataProvider()
|
||||||
|
pr.addAttributes([QgsField(FELD_TYP, QVariant.String)])
|
||||||
|
layer.updateFields()
|
||||||
|
return layer
|
||||||
|
|
||||||
|
|
||||||
|
def _setze_kategorien_symbologie(layer) -> None:
|
||||||
|
"""Wendet Kategoriesymbologie an: grün/rot/orange Punkte.
|
||||||
|
|
||||||
|
- ``identisch`` → grün
|
||||||
|
- ``kein_pta`` → rot
|
||||||
|
- ``fehlender_knick`` → orange
|
||||||
|
|
||||||
|
Wird bei fehlendem QGIS-Kontext still ignoriert.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from qgis.core import ( # type: ignore[import]
|
||||||
|
QgsCategorizedSymbolRenderer,
|
||||||
|
QgsRendererCategory,
|
||||||
|
QgsMarkerSymbol,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _marker(color: str, size: str = "2") -> object:
|
||||||
|
return QgsMarkerSymbol.createSimple(
|
||||||
|
{"color": color, "size": size, "outline_style": "no"}
|
||||||
|
)
|
||||||
|
|
||||||
|
categories = [
|
||||||
|
QgsRendererCategory(
|
||||||
|
WERT_IDENTISCH,
|
||||||
|
_marker("0,180,0,255"),
|
||||||
|
"identisch (Knickpunkt mit PunktortTA)",
|
||||||
|
),
|
||||||
|
QgsRendererCategory(
|
||||||
|
WERT_KEIN_PTA,
|
||||||
|
_marker("220,0,0,255"),
|
||||||
|
"kein PunktortTA an Knickpunkt",
|
||||||
|
),
|
||||||
|
QgsRendererCategory(
|
||||||
|
WERT_FEHLENDER_KNICK,
|
||||||
|
_marker("255,128,0,255"),
|
||||||
|
"fehlender Knickpunkt (PunktortTA ohne BROR-Knickpunkt)",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
renderer = QgsCategorizedSymbolRenderer(FELD_TYP, categories)
|
||||||
|
layer.setRenderer(renderer)
|
||||||
|
except Exception:
|
||||||
|
pass # Kein QGIS-Kontext (Tests) → Standarddarstellung behalten
|
||||||
|
|
||||||
|
|
||||||
|
def _baue_pta_index(punktort_layer) -> Tuple[object, Dict[int, object]]:
|
||||||
|
"""Baut einen QgsSpatialIndex und eine ID→Geometrie-Map für PunktortTA.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
punktort_layer :
|
||||||
|
Geladener ``AX_PunktortTA``-Layer.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
Tuple[QgsSpatialIndex, Dict[int, QgsGeometry]]
|
||||||
|
Index und Feature-ID-zu-Geometrie-Dict.
|
||||||
|
"""
|
||||||
|
from qgis.core import QgsSpatialIndex # type: ignore[import]
|
||||||
|
|
||||||
|
index = QgsSpatialIndex()
|
||||||
|
geom_map: Dict[int, object] = {}
|
||||||
|
for feat in punktort_layer.getFeatures():
|
||||||
|
geom = feat.geometry()
|
||||||
|
if geom and not geom.isEmpty():
|
||||||
|
index.insertFeature(feat)
|
||||||
|
geom_map[feat.id()] = geom
|
||||||
|
return index, geom_map
|
||||||
|
|
||||||
|
|
||||||
|
def _baue_flst_vertex_set(
|
||||||
|
flst_layer,
|
||||||
|
bror_buffer_geom,
|
||||||
|
) -> Set[Tuple[float, float]]:
|
||||||
|
"""Erstellt ein Set gerundeter (x, y)-Koordinaten aller Flurstück-Vertices
|
||||||
|
innerhalb des gepufferten BROR-Umrings.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
flst_layer :
|
||||||
|
Flurstücke-Layer.
|
||||||
|
bror_buffer_geom :
|
||||||
|
Gepufferter BROR-Umring (Polygon-Geometrie).
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
Set[Tuple[float, float]]
|
||||||
|
Gerundete Vertex-Koordinaten.
|
||||||
|
"""
|
||||||
|
vertex_set: Set[Tuple[float, float]] = set()
|
||||||
|
if flst_layer is None:
|
||||||
|
return vertex_set
|
||||||
|
|
||||||
|
try:
|
||||||
|
for feat in flst_layer.getFeatures():
|
||||||
|
geom = feat.geometry()
|
||||||
|
if geom is None or geom.isEmpty():
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
# Nur Flurstücke berücksichtigen, die den Puffer berühren
|
||||||
|
if not geom.intersects(bror_buffer_geom):
|
||||||
|
continue
|
||||||
|
boundary = geom.convertToType(1) # 1 = Line
|
||||||
|
if boundary is None or boundary.isEmpty():
|
||||||
|
continue
|
||||||
|
if boundary.isMultipart():
|
||||||
|
parts = boundary.asMultiPolyline()
|
||||||
|
else:
|
||||||
|
parts = [boundary.asPolyline()]
|
||||||
|
for line in parts:
|
||||||
|
for pt in line:
|
||||||
|
vertex_set.add(
|
||||||
|
(round(pt.x(), _VERTEX_ROUND), round(pt.y(), _VERTEX_ROUND))
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return vertex_set
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Öffentliche API
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def knickpunkt_pruefung(
|
||||||
|
bror_layer,
|
||||||
|
punktort_layer,
|
||||||
|
flst_layer,
|
||||||
|
progress=None,
|
||||||
|
knickpunkte: Optional[List] = None,
|
||||||
|
) -> Tuple[Optional[object], int, int, int]:
|
||||||
|
"""Knickpunktbasierte BROR-Prüfung.
|
||||||
|
|
||||||
|
Vergleicht BROR-Knickpunkte mit ``AX_PunktortTA``-Objekten und ermittelt
|
||||||
|
fehlende Knickpunkte anhand von Flurstücksgrenzen.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
bror_layer :
|
||||||
|
BauRaumOderBodenordnungsrecht-Polygon-Layer.
|
||||||
|
punktort_layer :
|
||||||
|
Geladener ``AX_PunktortTA``-Layer (Ergebnis aus ``grenzpunkt_alkis``).
|
||||||
|
flst_layer :
|
||||||
|
Flurstücke-Layer für die Orange-Prüfung. Kann ``None`` sein.
|
||||||
|
progress :
|
||||||
|
Optionaler ``ProgressDialog`` für Fortschrittsanzeige.
|
||||||
|
knickpunkte :
|
||||||
|
Vorberechnete Knickpunktliste (optional). Wenn ``None``, werden die
|
||||||
|
Knickpunkte aus ``bror_layer`` extrahiert.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
Tuple[Optional[QgsVectorLayer], int, int, int]
|
||||||
|
``(result_layer, identisch_count, kein_pta_count, fehlender_knick_count)``
|
||||||
|
|
||||||
|
- ``result_layer``: Punkt-Layer mit grünen/roten/orangen Punkten,
|
||||||
|
oder ``None`` bei Abbruch oder fehlgeschlagener Verarbeitung.
|
||||||
|
- ``identisch_count``: Knickpunkte mit passendem PunktortTA (grün).
|
||||||
|
- ``kein_pta_count``: Knickpunkte ohne passenden PunktortTA (rot).
|
||||||
|
- ``fehlender_knick_count``: PunktortTA ohne BROR-Knickpunkt (orange).
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _canceled() -> bool:
|
||||||
|
return progress is not None and progress.is_canceled()
|
||||||
|
|
||||||
|
def _schritt(label: str, wert: int) -> None:
|
||||||
|
if progress is not None:
|
||||||
|
progress.set_label(label)
|
||||||
|
progress.set_value(wert)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Schritt 0: Knickpunkte ermitteln
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
_schritt("Knickpunktprüfung: Knickpunkte extrahieren …", 0)
|
||||||
|
|
||||||
|
if knickpunkte is None:
|
||||||
|
knickpunkte = _extrahiere_knickpunkte(bror_layer)
|
||||||
|
|
||||||
|
if not knickpunkte:
|
||||||
|
return None, 0, 0, 0
|
||||||
|
|
||||||
|
n = len(knickpunkte)
|
||||||
|
crs_authid = bror_layer.crs().authid()
|
||||||
|
|
||||||
|
if _canceled():
|
||||||
|
return None, 0, 0, 0
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Schritt 1: Spatial-Index für PunktortTA aufbauen
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
_schritt("Knickpunktprüfung: PunktortTA-Index aufbauen …", 1)
|
||||||
|
|
||||||
|
if punktort_layer is None:
|
||||||
|
return None, 0, 0, 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
pta_index, geom_map = _baue_pta_index(punktort_layer)
|
||||||
|
except Exception:
|
||||||
|
return None, 0, 0, 0
|
||||||
|
|
||||||
|
if _canceled():
|
||||||
|
return None, 0, 0, 0
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Schritte 2…n+1: Jeden Knickpunkt mit PunktortTA vergleichen
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
try:
|
||||||
|
from qgis.core import QgsRectangle # type: ignore[import]
|
||||||
|
except ImportError:
|
||||||
|
return None, 0, 0, 0
|
||||||
|
|
||||||
|
ergebnis_layer = _erstelle_ergebnis_layer(crs_authid)
|
||||||
|
pr = ergebnis_layer.dataProvider()
|
||||||
|
feats_to_add: List[object] = []
|
||||||
|
|
||||||
|
identisch_count = 0
|
||||||
|
kein_pta_count = 0
|
||||||
|
matched_pta_ids: Set[int] = set()
|
||||||
|
|
||||||
|
for i, knick_pt in enumerate(knickpunkte):
|
||||||
|
if _canceled():
|
||||||
|
return None, 0, 0, 0
|
||||||
|
|
||||||
|
_schritt(f"Knickpunktprüfung: Knickpunkt {i + 1}/{n} prüfen …", i + 2)
|
||||||
|
|
||||||
|
x, y = knick_pt.x(), knick_pt.y()
|
||||||
|
t = KOORDINATEN_TOLERANZ_M
|
||||||
|
suchbox = QgsRectangle(x - t, y - t, x + t, y + t)
|
||||||
|
kandidaten = pta_index.intersects(suchbox)
|
||||||
|
|
||||||
|
gefunden = False
|
||||||
|
for fid in kandidaten:
|
||||||
|
pta_geom = geom_map.get(fid)
|
||||||
|
if pta_geom is None:
|
||||||
|
continue
|
||||||
|
pta_pt = pta_geom.asPoint()
|
||||||
|
dist = ((pta_pt.x() - x) ** 2 + (pta_pt.y() - y) ** 2) ** 0.5
|
||||||
|
if dist <= KOORDINATEN_TOLERANZ_M:
|
||||||
|
gefunden = True
|
||||||
|
matched_pta_ids.add(fid)
|
||||||
|
break
|
||||||
|
|
||||||
|
feat = QgsFeature()
|
||||||
|
try:
|
||||||
|
from qgis.core import QgsPointXY # type: ignore[import]
|
||||||
|
feat.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(x, y)))
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
feat.setAttributes([WERT_IDENTISCH if gefunden else WERT_KEIN_PTA])
|
||||||
|
feats_to_add.append(feat)
|
||||||
|
|
||||||
|
if gefunden:
|
||||||
|
identisch_count += 1
|
||||||
|
else:
|
||||||
|
kein_pta_count += 1
|
||||||
|
|
||||||
|
if _canceled():
|
||||||
|
return None, 0, 0, 0
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Schritt n+2: Fehlende Knickpunkte (orange) ermitteln
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
_schritt("Knickpunktprüfung: Fehlende Knickpunkte ermitteln …", n + 2)
|
||||||
|
|
||||||
|
fehlender_knick_count = 0
|
||||||
|
|
||||||
|
# BROR-Umring ermitteln und puffern
|
||||||
|
try:
|
||||||
|
bror_feats = [
|
||||||
|
f for f in bror_layer.getFeatures()
|
||||||
|
if f.geometry() and not f.geometry().isEmpty()
|
||||||
|
]
|
||||||
|
bror_union = QgsGeometry.unaryUnion([f.geometry() for f in bror_feats])
|
||||||
|
bror_boundary = bror_union.convertToType(1) if bror_union else None
|
||||||
|
bror_buffer_geom = (
|
||||||
|
bror_boundary.buffer(BOUNDARY_PUFFER_M, 8) if bror_boundary else None
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
bror_buffer_geom = None
|
||||||
|
|
||||||
|
if bror_buffer_geom is not None and flst_layer is not None:
|
||||||
|
flst_vertex_set = _baue_flst_vertex_set(flst_layer, bror_buffer_geom)
|
||||||
|
|
||||||
|
for feat in punktort_layer.getFeatures():
|
||||||
|
if _canceled():
|
||||||
|
break
|
||||||
|
if feat.id() in matched_pta_ids:
|
||||||
|
continue
|
||||||
|
geom = feat.geometry()
|
||||||
|
if geom is None or geom.isEmpty():
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
if not geom.within(bror_buffer_geom):
|
||||||
|
continue
|
||||||
|
pta_pt = geom.asPoint()
|
||||||
|
key = (round(pta_pt.x(), _VERTEX_ROUND), round(pta_pt.y(), _VERTEX_ROUND))
|
||||||
|
if key in flst_vertex_set:
|
||||||
|
orange_feat = QgsFeature()
|
||||||
|
orange_feat.setGeometry(geom)
|
||||||
|
orange_feat.setAttributes([WERT_FEHLENDER_KNICK])
|
||||||
|
feats_to_add.append(orange_feat)
|
||||||
|
fehlender_knick_count += 1
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if _canceled():
|
||||||
|
return None, 0, 0, 0
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Schritt n+3: Ergebnislayer fertigstellen
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
_schritt("Knickpunktprüfung: Ergebnislayer erstellen …", n + 3)
|
||||||
|
|
||||||
|
pr.addFeatures(feats_to_add)
|
||||||
|
ergebnis_layer.updateExtents()
|
||||||
|
_setze_kategorien_symbologie(ergebnis_layer)
|
||||||
|
|
||||||
|
return ergebnis_layer, identisch_count, kein_pta_count, fehlender_knick_count
|
||||||
@@ -0,0 +1,311 @@
|
|||||||
|
"""sn_verfahrensgebiet/functions/umringspruefung.py – Umringsprüfung (Variante 1).
|
||||||
|
|
||||||
|
Vergleicht den Umring (Boundary) des BauRaumOderBodenordnungsrecht-Polygons
|
||||||
|
mit den Grenzen der angrenzenden Flurstücke und hebt Abweichungen hervor.
|
||||||
|
|
||||||
|
**Algorithmus (Variante 1 – linienbasierter Umringvergleich):**
|
||||||
|
|
||||||
|
1. BROR-Polygon → Umringslinie via :func:`sn_basis.functions.qgiscore_wrapper.polygon_zu_linie`
|
||||||
|
(QGIS-3/4-kompatibel).
|
||||||
|
2. Flurstücke, die den BROR-Umring berühren oder schneiden, werden gefiltert.
|
||||||
|
3. Gefilterte Flurstücke → Umringslinien.
|
||||||
|
4. Union aller Flurstücksumringe.
|
||||||
|
5. ``BROR-Umring.difference(Flurstücksumrings-Union)`` → Abweichungen (rot).
|
||||||
|
6. ``BROR-Umring.difference(rote_Segmente)`` → Übereinstimmungen (grün).
|
||||||
|
7. Ergebnislayer mit Kategoriesymbologie (grün/rot) und Legendenbeschriftung.
|
||||||
|
|
||||||
|
**Rückgabe:** ``(result_layer, conflict_count)``
|
||||||
|
|
||||||
|
- ``result_layer``: Temporärer Linienlayer mit Feld ``typ``
|
||||||
|
(``"identisch"`` = grün, ``"Abweichung"`` = rot).
|
||||||
|
- ``conflict_count``: Anzahl der roten Konfliktsegmente (Parts).
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import List, Optional, Tuple
|
||||||
|
|
||||||
|
from sn_basis.functions.qgiscore_wrapper import (
|
||||||
|
QgsVectorLayer,
|
||||||
|
QgsFeature,
|
||||||
|
QgsField,
|
||||||
|
QgsGeometry,
|
||||||
|
polygon_zu_linie,
|
||||||
|
)
|
||||||
|
from sn_basis.functions.qt_wrapper import QVariant
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Konstanten
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#: Geometrische Toleranz für den Linienvergleich (EPSG:25833 = Meter).
|
||||||
|
#: Der BROR-Umring wird gegen die um diesen Betrag gepufferten Flurstücksumringe
|
||||||
|
#: verglichen, um numerische Rundungsfehler aus ALKIS-Koordinaten abzufangen.
|
||||||
|
LINIE_TOLERANZ_M: float = 0.0001
|
||||||
|
|
||||||
|
ERGEBNIS_LAYER_NAME = "Umringsprüfung"
|
||||||
|
FELD_TYP = "typ"
|
||||||
|
WERT_IDENTISCH = "identisch"
|
||||||
|
WERT_ABWEICHUNG = "Abweichung"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Interne Hilfsfunktionen
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _zerlege_in_teile(geom: object) -> List[object]:
|
||||||
|
"""Zerlegt eine (Multi-)LineString-Geometrie in einzelne Teile.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
geom :
|
||||||
|
``QgsGeometry`` vom Typ LineString oder MultiLineString.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
List[QgsGeometry]
|
||||||
|
Liste einzelner Geometrieteile; leer wenn ``geom`` None oder leer ist.
|
||||||
|
"""
|
||||||
|
if geom is None:
|
||||||
|
return []
|
||||||
|
try:
|
||||||
|
if getattr(geom, "isEmpty", lambda: True)():
|
||||||
|
return []
|
||||||
|
# asGeometryCollection() gibt eigene Kopien zurück – kein Ownership-Problem.
|
||||||
|
# QgsGeometry(part) über geom.parts() ist in QGIS 4.x aus den Python-Bindings
|
||||||
|
# entfernt worden und führt zu Absturz beim Rendern.
|
||||||
|
parts = geom.asGeometryCollection()
|
||||||
|
if parts:
|
||||||
|
return [p for p in parts if p is not None and not p.isEmpty()]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
# Fallback: Geometrie nicht zerlegbar → als Ganzes zurückgeben
|
||||||
|
try:
|
||||||
|
if not geom.isEmpty():
|
||||||
|
return [geom]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def _erstelle_ergebnis_layer(crs_authid: str) -> object:
|
||||||
|
"""Erzeugt den leeren Ergebnislayer mit Feld ``typ``.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
crs_authid :
|
||||||
|
CRS-Kennung des Quell-Layers (z. B. ``"EPSG:25833"``).
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
QgsVectorLayer
|
||||||
|
Leerer Memory-Layer vom Typ LineString.
|
||||||
|
"""
|
||||||
|
layer = QgsVectorLayer(
|
||||||
|
f"LineString?crs={crs_authid}", ERGEBNIS_LAYER_NAME, "memory"
|
||||||
|
)
|
||||||
|
pr = layer.dataProvider()
|
||||||
|
pr.addAttributes([QgsField(FELD_TYP, QVariant.String)])
|
||||||
|
layer.updateFields()
|
||||||
|
return layer
|
||||||
|
|
||||||
|
|
||||||
|
def _setze_kategorien_symbologie(layer) -> None:
|
||||||
|
"""Wendet Kategoriesymbologie an: grün = identisch, rot = Abweichung.
|
||||||
|
|
||||||
|
Wird bei fehlendem QGIS-Kontext still ignoriert (Unit-Test-Robustheit).
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from qgis.core import ( # type: ignore[import]
|
||||||
|
QgsCategorizedSymbolRenderer,
|
||||||
|
QgsRendererCategory,
|
||||||
|
QgsLineSymbol,
|
||||||
|
)
|
||||||
|
|
||||||
|
green_sym = QgsLineSymbol.createSimple(
|
||||||
|
{"color": "0,180,0,255", "line_width": "0.5"}
|
||||||
|
)
|
||||||
|
red_sym = QgsLineSymbol.createSimple(
|
||||||
|
{"color": "220,0,0,255", "line_width": "0.8"}
|
||||||
|
)
|
||||||
|
categories = [
|
||||||
|
QgsRendererCategory(
|
||||||
|
WERT_IDENTISCH,
|
||||||
|
green_sym,
|
||||||
|
"identisch (übereinstimmend)",
|
||||||
|
),
|
||||||
|
QgsRendererCategory(
|
||||||
|
WERT_ABWEICHUNG,
|
||||||
|
red_sym,
|
||||||
|
"Abweichung (nicht in Flurstücken)",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
renderer = QgsCategorizedSymbolRenderer(FELD_TYP, categories)
|
||||||
|
layer.setRenderer(renderer)
|
||||||
|
except Exception:
|
||||||
|
pass # Kein QGIS-Kontext (Tests) → Standarddarstellung behalten
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Öffentliche API
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def umringspruefung(
|
||||||
|
bror_layer,
|
||||||
|
flst_layer,
|
||||||
|
progress=None,
|
||||||
|
linie_toleranz_m: float = LINIE_TOLERANZ_M,
|
||||||
|
) -> Tuple[Optional[object], int]:
|
||||||
|
"""Variante 1: Linienbasierter Umringvergleich.
|
||||||
|
|
||||||
|
Ermittelt, welche Teile des BROR-Umrings mit den Flurstücksgrenzen
|
||||||
|
übereinstimmen (grün) und welche keine Entsprechung haben (rot).
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
bror_layer :
|
||||||
|
BauRaumOderBodenordnungsrecht-Memory-Layer (Polygon).
|
||||||
|
flst_layer :
|
||||||
|
Flurstücke-Layer (Polygon, WFS oder Memory).
|
||||||
|
progress :
|
||||||
|
Optionaler ``ProgressDialog`` aus
|
||||||
|
:mod:`sn_basis.functions.dialog_wrapper` für Fortschrittsanzeige
|
||||||
|
und Abbrucherkennung.
|
||||||
|
linie_toleranz_m : float
|
||||||
|
Geometrische Puffertoleranz für den Linienvergleich in
|
||||||
|
CRS-Einheiten (Standardwert :data:`LINIE_TOLERANZ_M` = 0.001 m).
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
Tuple[Optional[QgsVectorLayer], int]
|
||||||
|
``(result_layer, conflict_count)``
|
||||||
|
|
||||||
|
- ``result_layer``: Linienlayer mit grünen/roten Segmenten,
|
||||||
|
oder ``None`` bei Abbruch oder fehlgeschlagener Verarbeitung.
|
||||||
|
- ``conflict_count``: Anzahl der roten Konfliktsegmente (BROR-Teile
|
||||||
|
ohne Entsprechung in den Flurstücksgrenzen).
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _canceled() -> bool:
|
||||||
|
return progress is not None and progress.is_canceled()
|
||||||
|
|
||||||
|
def _schritt(label: str, wert: int) -> None:
|
||||||
|
if progress is not None:
|
||||||
|
progress.set_label(label)
|
||||||
|
progress.set_value(wert)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 1. BROR-Geometrie und Umringslinie ermitteln
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
_schritt("Umringsprüfung: BROR-Umring ermitteln …", 0)
|
||||||
|
|
||||||
|
bror_feats = [
|
||||||
|
f for f in bror_layer.getFeatures()
|
||||||
|
if f.geometry() and not f.geometry().isEmpty()
|
||||||
|
]
|
||||||
|
if not bror_feats:
|
||||||
|
return None, 0
|
||||||
|
|
||||||
|
bror_union = QgsGeometry.unaryUnion([f.geometry() for f in bror_feats])
|
||||||
|
if bror_union is None or bror_union.isEmpty():
|
||||||
|
return None, 0
|
||||||
|
|
||||||
|
bror_boundary = polygon_zu_linie(bror_union)
|
||||||
|
if bror_boundary is None or bror_boundary.isEmpty():
|
||||||
|
return None, 0
|
||||||
|
|
||||||
|
crs_authid = bror_layer.crs().authid()
|
||||||
|
|
||||||
|
if _canceled():
|
||||||
|
return None, 0
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 2. Relevante Flurstücke: nur jene, deren Geometrie den BROR-Umring
|
||||||
|
# berührt oder schneidet
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
_schritt("Umringsprüfung: Relevante Flurstücke filtern …", 1)
|
||||||
|
|
||||||
|
relevante_boundaries: List[object] = []
|
||||||
|
for feat in flst_layer.getFeatures():
|
||||||
|
if _canceled():
|
||||||
|
return None, 0
|
||||||
|
geom = feat.geometry()
|
||||||
|
if geom is None or geom.isEmpty():
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
if geom.intersects(bror_boundary):
|
||||||
|
boundary = polygon_zu_linie(geom)
|
||||||
|
if boundary is not None and not boundary.isEmpty():
|
||||||
|
relevante_boundaries.append(boundary)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if relevante_boundaries:
|
||||||
|
flst_boundary_union = QgsGeometry.unaryUnion(relevante_boundaries)
|
||||||
|
else:
|
||||||
|
flst_boundary_union = None
|
||||||
|
|
||||||
|
if _canceled():
|
||||||
|
return None, 0
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 3. Segmentvergleich: Differenz und Übereinstimmung berechnen
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
_schritt("Umringsprüfung: Segmentvergleich …", 2)
|
||||||
|
|
||||||
|
ergebnis_layer = _erstelle_ergebnis_layer(crs_authid)
|
||||||
|
pr = ergebnis_layer.dataProvider()
|
||||||
|
feats_to_add: List[object] = []
|
||||||
|
conflict_count = 0
|
||||||
|
|
||||||
|
if flst_boundary_union is None or flst_boundary_union.isEmpty():
|
||||||
|
# Kein Flurstück berührt den BROR-Umring → alles ist Abweichung
|
||||||
|
for part in _zerlege_in_teile(bror_boundary):
|
||||||
|
feat = QgsFeature()
|
||||||
|
feat.setGeometry(part)
|
||||||
|
feat.setAttributes([WERT_ABWEICHUNG])
|
||||||
|
feats_to_add.append(feat)
|
||||||
|
conflict_count += 1
|
||||||
|
else:
|
||||||
|
# Flurstücksumringe leicht puffern für Toleranz-Matching
|
||||||
|
if linie_toleranz_m > 0:
|
||||||
|
flst_fuer_diff = flst_boundary_union.buffer(linie_toleranz_m, 4)
|
||||||
|
else:
|
||||||
|
flst_fuer_diff = flst_boundary_union
|
||||||
|
|
||||||
|
# Rote Segmente: BROR-Teile ohne Entsprechung in Flurstücksgrenzen
|
||||||
|
try:
|
||||||
|
red_geom = bror_boundary.difference(flst_fuer_diff)
|
||||||
|
except Exception:
|
||||||
|
red_geom = None
|
||||||
|
|
||||||
|
# Grüne Segmente: BROR-Teile, die nicht rot sind
|
||||||
|
try:
|
||||||
|
if red_geom and not red_geom.isEmpty():
|
||||||
|
green_geom = bror_boundary.difference(red_geom)
|
||||||
|
else:
|
||||||
|
green_geom = bror_boundary
|
||||||
|
except Exception:
|
||||||
|
green_geom = None
|
||||||
|
|
||||||
|
for part in _zerlege_in_teile(green_geom):
|
||||||
|
feat = QgsFeature()
|
||||||
|
feat.setGeometry(part)
|
||||||
|
feat.setAttributes([WERT_IDENTISCH])
|
||||||
|
feats_to_add.append(feat)
|
||||||
|
|
||||||
|
for part in _zerlege_in_teile(red_geom):
|
||||||
|
feat = QgsFeature()
|
||||||
|
feat.setGeometry(part)
|
||||||
|
feat.setAttributes([WERT_ABWEICHUNG])
|
||||||
|
feats_to_add.append(feat)
|
||||||
|
conflict_count += 1
|
||||||
|
|
||||||
|
if _canceled():
|
||||||
|
return None, 0
|
||||||
|
|
||||||
|
pr.addFeatures(feats_to_add)
|
||||||
|
ergebnis_layer.updateExtents()
|
||||||
|
_setze_kategorien_symbologie(ergebnis_layer)
|
||||||
|
|
||||||
|
_schritt("Umringsprüfung abgeschlossen.", 3)
|
||||||
|
return ergebnis_layer, conflict_count
|
||||||
@@ -0,0 +1,373 @@
|
|||||||
|
"""sn_verfahrensgebiet/functions/umringstatistik_grenzpunkte.py – Umringstatistik Grenzpunkte.
|
||||||
|
|
||||||
|
Extrahiert aus einem vorhandenen ``Grenzpunkte (ALKIS)``-Layer alle Punkte,
|
||||||
|
die auf dem Umring des Verfahrensgebiets liegen, und klassifiziert sie in
|
||||||
|
drei Kategorien:
|
||||||
|
|
||||||
|
1. **FGP** (grün): ``gp_festgestellterGrenzpunkt == "true"``
|
||||||
|
2. **<3cm** (gelb): ``gp_festgestellterGrenzpunkt != "true"``
|
||||||
|
UND ``qualitaetsangaben_ax_dqpunktort_genauigkeitsstufe == 2100``
|
||||||
|
3. **>3cm** (rot): ``gp_festgestellterGrenzpunkt != "true"``
|
||||||
|
UND ``qualitaetsangaben_ax_dqpunktort_genauigkeitsstufe > 2100``
|
||||||
|
4. **unbekannt** (grau): keiner der obigen Fälle
|
||||||
|
|
||||||
|
Der Umring-Test verwendet einen Abstandsschwellwert von :data:`UMRING_TOLERANZ_M`.
|
||||||
|
|
||||||
|
**Rückgabe:** Memory-Layer ``Umringstatistik Grenzpunkte`` oder ``None`` bei Fehler.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from sn_basis.functions import QgsProject
|
||||||
|
from sn_basis.functions.qt_wrapper import QMessageBox
|
||||||
|
from sn_basis.functions.dialog_wrapper import create_progress_dialog
|
||||||
|
from sn_basis.functions.message_wrapper import warning
|
||||||
|
from sn_basis.functions.qgiscore_wrapper import (
|
||||||
|
QgsVectorLayer,
|
||||||
|
QgsFeature,
|
||||||
|
QgsField,
|
||||||
|
QgsGeometry,
|
||||||
|
polygon_zu_linie,
|
||||||
|
)
|
||||||
|
from sn_basis.functions.qt_wrapper import QVariant
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Konstanten
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#: Anzeigename des Ergebnis-Layers.
|
||||||
|
ERGEBNIS_LAYER_NAME = "Umringstatistik Grenzpunkte"
|
||||||
|
|
||||||
|
#: Feldname für die Klassifikationskategorie im Ergebnis-Layer.
|
||||||
|
FELD_KATEGORIE = "kategorie"
|
||||||
|
|
||||||
|
#: Feldname für ``gp_festgestellterGrenzpunkt`` im Grenzpunkte(ALKIS)-Layer.
|
||||||
|
FELD_FESTGESTELLT = "gp_festgestellterGrenzpunkt"
|
||||||
|
|
||||||
|
#: Feldname für das Genauigkeitsattribut im Grenzpunkte(ALKIS)-Layer.
|
||||||
|
FELD_GENAUIGKEIT = "qualitaetsangaben_ax_dqpunktort_genauigkeitsstufe"
|
||||||
|
|
||||||
|
#: Feldname für die GML-ID im Grenzpunkte(ALKIS)-Layer.
|
||||||
|
FELD_GML_ID = "gml_id"
|
||||||
|
|
||||||
|
#: Kategoriewert: festgestellter Grenzpunkt.
|
||||||
|
KATEGORIE_FGP = "FGP"
|
||||||
|
#: Kategoriewert: Lagegenauigkeit ≤ 3 cm (Gütestufe 2100).
|
||||||
|
KATEGORIE_KLEIN = "<3cm"
|
||||||
|
#: Kategoriewert: Lagegenauigkeit > 3 cm (schlechtere Gütestufe).
|
||||||
|
KATEGORIE_GROSS = ">3cm"
|
||||||
|
#: Kategoriewert: Kategorie konnte nicht bestimmt werden.
|
||||||
|
KATEGORIE_UNBEKANNT = "unbekannt"
|
||||||
|
|
||||||
|
#: Maximaler Abstand (m) eines Punktes vom Umring, um als „auf dem Umring" zu gelten.
|
||||||
|
UMRING_TOLERANZ_M: float = 0.01
|
||||||
|
|
||||||
|
#: Grenzwert für ``qualitaetsangaben_ax_dqpunktort_genauigkeitsstufe`` (3-cm-Schwelle).
|
||||||
|
GENAUIGKEIT_GRENZWERT: int = 2100
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Interne Hilfsfunktionen
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _setze_kategorien_symbologie(layer) -> None:
|
||||||
|
"""Wendet Kategoriesymbologie an: grün / gelb / rot / grau.
|
||||||
|
|
||||||
|
Wird bei fehlendem QGIS-Kontext still ignoriert.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from qgis.core import ( # type: ignore[import]
|
||||||
|
QgsCategorizedSymbolRenderer,
|
||||||
|
QgsRendererCategory,
|
||||||
|
QgsMarkerSymbol,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _marker(color: str, size: str = "2.5") -> object:
|
||||||
|
return QgsMarkerSymbol.createSimple(
|
||||||
|
{"color": color, "size": size, "outline_style": "no"}
|
||||||
|
)
|
||||||
|
|
||||||
|
categories = [
|
||||||
|
QgsRendererCategory(
|
||||||
|
KATEGORIE_FGP,
|
||||||
|
_marker("0,180,0,255"),
|
||||||
|
f"{KATEGORIE_FGP} – festgestellter Grenzpunkt",
|
||||||
|
),
|
||||||
|
QgsRendererCategory(
|
||||||
|
KATEGORIE_KLEIN,
|
||||||
|
_marker("255,220,0,255"),
|
||||||
|
f"{KATEGORIE_KLEIN} – Gütestufe {GENAUIGKEIT_GRENZWERT}",
|
||||||
|
),
|
||||||
|
QgsRendererCategory(
|
||||||
|
KATEGORIE_GROSS,
|
||||||
|
_marker("220,0,0,255"),
|
||||||
|
f"{KATEGORIE_GROSS} – Gütestufe > {GENAUIGKEIT_GRENZWERT}",
|
||||||
|
),
|
||||||
|
QgsRendererCategory(
|
||||||
|
KATEGORIE_UNBEKANNT,
|
||||||
|
_marker("128,128,128,255"),
|
||||||
|
f"{KATEGORIE_UNBEKANNT} – Kategorie nicht bestimmbar",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
renderer = QgsCategorizedSymbolRenderer(FELD_KATEGORIE, categories)
|
||||||
|
layer.setRenderer(renderer)
|
||||||
|
except Exception:
|
||||||
|
pass # Kein QGIS-Kontext (Tests) → Standarddarstellung behalten
|
||||||
|
|
||||||
|
|
||||||
|
def _klassifiziere(feat) -> str:
|
||||||
|
"""Bestimmt die Kategorie eines Grenzpunkts anhand seiner Attribute.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
feat :
|
||||||
|
Feature aus dem Grenzpunkte(ALKIS)-Layer.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
str
|
||||||
|
Einer der Kategorienwerte :data:`KATEGORIE_FGP`, :data:`KATEGORIE_KLEIN`,
|
||||||
|
:data:`KATEGORIE_GROSS` oder :data:`KATEGORIE_UNBEKANNT`.
|
||||||
|
"""
|
||||||
|
# festgestellterGrenzpunkt
|
||||||
|
try:
|
||||||
|
festgestellt_raw = feat[FELD_FESTGESTELLT]
|
||||||
|
festgestellt = str(festgestellt_raw).lower().strip() == "true" if festgestellt_raw else False
|
||||||
|
except Exception:
|
||||||
|
festgestellt = False
|
||||||
|
|
||||||
|
if festgestellt:
|
||||||
|
return KATEGORIE_FGP
|
||||||
|
|
||||||
|
# Genauigkeitsstufe auslesen
|
||||||
|
try:
|
||||||
|
gen_raw = feat[FELD_GENAUIGKEIT]
|
||||||
|
genauigkeit = int(str(gen_raw).strip()) if gen_raw else None
|
||||||
|
except (ValueError, TypeError, Exception):
|
||||||
|
genauigkeit = None
|
||||||
|
|
||||||
|
if genauigkeit is None:
|
||||||
|
return KATEGORIE_UNBEKANNT
|
||||||
|
if genauigkeit == GENAUIGKEIT_GRENZWERT:
|
||||||
|
return KATEGORIE_KLEIN
|
||||||
|
if genauigkeit > GENAUIGKEIT_GRENZWERT:
|
||||||
|
return KATEGORIE_GROSS
|
||||||
|
# Genauigkeit < 2100 (besser als 3 cm, aber nicht festgestellt)
|
||||||
|
return KATEGORIE_UNBEKANNT
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Öffentliche API
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def umringstatistik_grenzpunkte(
|
||||||
|
tab_widget,
|
||||||
|
grenzpunkte_layer,
|
||||||
|
vg_layer,
|
||||||
|
) -> Optional[object]:
|
||||||
|
"""Klassifiziert Grenzpunkte auf dem VG-Umring nach Lagegenauigkeit.
|
||||||
|
|
||||||
|
Ablauf:
|
||||||
|
|
||||||
|
1. VG-Geometrie aus ``vg_layer`` berechnen → Umringslinie.
|
||||||
|
2. Alle Features aus ``grenzpunkte_layer`` auf Abstand ≤ :data:`UMRING_TOLERANZ_M`
|
||||||
|
zum Umring prüfen.
|
||||||
|
3. Treffer klassifizieren (FGP / <3cm / >3cm / unbekannt).
|
||||||
|
4. Ergebnis-Memory-Layer aufbauen und Kategoriesymbologie setzen.
|
||||||
|
5. Layer ins Projekt einfügen, Zusammenfassungs-MessageBox anzeigen.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
tab_widget :
|
||||||
|
Eltern-Widget für Dialoge.
|
||||||
|
grenzpunkte_layer :
|
||||||
|
Geladener ``Grenzpunkte (ALKIS)``-Layer mit Attributen ``gp_festgestellterGrenzpunkt``
|
||||||
|
und ``qualitaetsangaben_ax_dqpunktort_genauigkeitsstufe``.
|
||||||
|
vg_layer :
|
||||||
|
Verfahrensgebiet-Layer (Polygon, EPSG:25833).
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
QgsVectorLayer | None
|
||||||
|
Ergebnis-Memory-Layer oder ``None`` bei Fehler/Abbruch.
|
||||||
|
"""
|
||||||
|
project = QgsProject.instance()
|
||||||
|
prog = create_progress_dialog(5, "Umringstatistik Grenzpunkte", "Schritt 1/5: VG-Umring berechnen …")
|
||||||
|
abgeschlossen = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
prog.set_value(0)
|
||||||
|
if prog.is_canceled():
|
||||||
|
return None
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Schritt 1: VG-Umring berechnen
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
feats_vg = [f for f in vg_layer.getFeatures() if f.hasGeometry() and not f.geometry().isEmpty()]
|
||||||
|
if not feats_vg:
|
||||||
|
prog.close()
|
||||||
|
warning("Umringstatistik", "Verfahrensgebiet-Layer enthält keine Geometrien.", duration=6)
|
||||||
|
return None
|
||||||
|
|
||||||
|
vg_geom = QgsGeometry.unaryUnion([f.geometry() for f in feats_vg])
|
||||||
|
if vg_geom is None or vg_geom.isEmpty():
|
||||||
|
prog.close()
|
||||||
|
warning("Umringstatistik", "Verfahrensgebiet-Geometrie konnte nicht vereint werden.", duration=6)
|
||||||
|
return None
|
||||||
|
|
||||||
|
umring_geom = polygon_zu_linie(vg_geom)
|
||||||
|
if umring_geom is None or umring_geom.isEmpty():
|
||||||
|
prog.close()
|
||||||
|
warning("Umringstatistik", "Umringslinie konnte aus der VG-Geometrie nicht extrahiert werden.", duration=6)
|
||||||
|
return None
|
||||||
|
|
||||||
|
prog.set_value(1)
|
||||||
|
if prog.is_canceled():
|
||||||
|
return None
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Schritt 2: Grenzpunkte auf dem Umring filtern
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
prog.set_label("Schritt 2/5: Grenzpunkte auf Umring prüfen …")
|
||||||
|
|
||||||
|
n_gesamt = grenzpunkte_layer.featureCount()
|
||||||
|
treffer: list = []
|
||||||
|
|
||||||
|
for i, feat in enumerate(grenzpunkte_layer.getFeatures()):
|
||||||
|
if i % 200 == 0:
|
||||||
|
prog.set_label(
|
||||||
|
f"Schritt 2/5: Grenzpunkte prüfen … ({i}/{n_gesamt})"
|
||||||
|
)
|
||||||
|
if prog.is_canceled():
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not feat.hasGeometry() or feat.geometry().isEmpty():
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
dist = feat.geometry().distance(umring_geom)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
if dist <= UMRING_TOLERANZ_M:
|
||||||
|
treffer.append(feat)
|
||||||
|
|
||||||
|
prog.set_value(2)
|
||||||
|
if prog.is_canceled():
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not treffer:
|
||||||
|
prog.close()
|
||||||
|
QMessageBox.information(
|
||||||
|
tab_widget,
|
||||||
|
"Umringstatistik Grenzpunkte",
|
||||||
|
"Es wurden keine Grenzpunkte auf dem Umring des Verfahrensgebiets gefunden.\n\n"
|
||||||
|
f"Geprüft: {n_gesamt} Grenzpunkte\n"
|
||||||
|
f"Toleranz: {UMRING_TOLERANZ_M} m",
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Schritt 3: Klassifizieren
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
prog.set_label(f"Schritt 3/5: {len(treffer)} Treffer klassifizieren …")
|
||||||
|
|
||||||
|
n_fgp = 0
|
||||||
|
n_klein = 0
|
||||||
|
n_gross = 0
|
||||||
|
n_unbekannt = 0
|
||||||
|
|
||||||
|
klassifiziert: list = [] # [(feat, kategorie), ...]
|
||||||
|
for feat in treffer:
|
||||||
|
kat = _klassifiziere(feat)
|
||||||
|
klassifiziert.append((feat, kat))
|
||||||
|
if kat == KATEGORIE_FGP:
|
||||||
|
n_fgp += 1
|
||||||
|
elif kat == KATEGORIE_KLEIN:
|
||||||
|
n_klein += 1
|
||||||
|
elif kat == KATEGORIE_GROSS:
|
||||||
|
n_gross += 1
|
||||||
|
else:
|
||||||
|
n_unbekannt += 1
|
||||||
|
|
||||||
|
prog.set_value(3)
|
||||||
|
if prog.is_canceled():
|
||||||
|
return None
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Schritt 4: Ergebnis-Layer aufbauen
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
prog.set_label("Schritt 4/5: Ergebnis-Layer aufbauen …")
|
||||||
|
|
||||||
|
crs_authid = vg_layer.crs().authid() or "EPSG:25833"
|
||||||
|
result_layer = QgsVectorLayer(
|
||||||
|
f"Point?crs={crs_authid}", ERGEBNIS_LAYER_NAME, "memory"
|
||||||
|
)
|
||||||
|
pr = result_layer.dataProvider()
|
||||||
|
|
||||||
|
felder = [
|
||||||
|
QgsField(FELD_KATEGORIE, QVariant.String),
|
||||||
|
QgsField(FELD_GML_ID, QVariant.String),
|
||||||
|
QgsField(FELD_FESTGESTELLT, QVariant.String),
|
||||||
|
QgsField(FELD_GENAUIGKEIT, QVariant.String),
|
||||||
|
]
|
||||||
|
pr.addAttributes(felder)
|
||||||
|
result_layer.updateFields()
|
||||||
|
|
||||||
|
neue_features = []
|
||||||
|
for feat, kat in klassifiziert:
|
||||||
|
new_f = QgsFeature(result_layer.fields())
|
||||||
|
new_f.setGeometry(feat.geometry())
|
||||||
|
|
||||||
|
new_f[FELD_KATEGORIE] = kat
|
||||||
|
|
||||||
|
for src_feld in (FELD_GML_ID, FELD_FESTGESTELLT, FELD_GENAUIGKEIT):
|
||||||
|
try:
|
||||||
|
new_f[src_feld] = feat[src_feld]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
neue_features.append(new_f)
|
||||||
|
|
||||||
|
pr.addFeatures(neue_features)
|
||||||
|
result_layer.updateExtents()
|
||||||
|
_setze_kategorien_symbologie(result_layer)
|
||||||
|
|
||||||
|
prog.set_value(4)
|
||||||
|
if prog.is_canceled():
|
||||||
|
return None
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Schritt 5: Layer ins Projekt + Ergebnis-MessageBox
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
prog.set_label("Schritt 5/5: Layer ins Projekt einfügen …")
|
||||||
|
|
||||||
|
# Bestehenden Ergebnis-Layer ggf. ersetzen
|
||||||
|
vorhandene = project.mapLayersByName(ERGEBNIS_LAYER_NAME)
|
||||||
|
for lyr in vorhandene:
|
||||||
|
project.removeMapLayer(lyr.id())
|
||||||
|
|
||||||
|
project.addMapLayer(result_layer)
|
||||||
|
|
||||||
|
prog.set_value(5)
|
||||||
|
abgeschlossen = True
|
||||||
|
prog.close()
|
||||||
|
|
||||||
|
n_umring = len(treffer)
|
||||||
|
QMessageBox.information(
|
||||||
|
tab_widget,
|
||||||
|
"Umringstatistik Grenzpunkte",
|
||||||
|
f"Grenzpunkte auf dem Umring: {n_umring}\n"
|
||||||
|
f"\n"
|
||||||
|
f" FGP (festgestellt): {n_fgp}\n"
|
||||||
|
f" < 3 cm (Gütestufe {GENAUIGKEIT_GRENZWERT}): {n_klein}\n"
|
||||||
|
f" > 3 cm (schlechtere Güte): {n_gross}\n"
|
||||||
|
f" Unbekannt: {n_unbekannt}\n"
|
||||||
|
f"\n"
|
||||||
|
f"(Geprüft: {n_gesamt} Grenzpunkte, Toleranz: {UMRING_TOLERANZ_M} m)",
|
||||||
|
)
|
||||||
|
|
||||||
|
return result_layer
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if not abgeschlossen:
|
||||||
|
prog.close()
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
#sn_verfahrensgebiet/functions/verfahrensgebiet_alkis
|
"""sn_verfahrensgebiet/functions/verfahrensgebiet_alkis.py – ALKIS-Integration.
|
||||||
|
Lädt Verfahrensgebiete aus dem ALKIS-WFS-Dienst.
|
||||||
|
"""
|
||||||
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from sn_basis.functions import (
|
from sn_basis.functions import (
|
||||||
QgsVectorLayer, QgsProject, QgsFeature, QgsField, QgsGeometry
|
QgsVectorLayer, QgsProject, QgsFeature, QgsField, QgsGeometry
|
||||||
@@ -7,10 +10,18 @@ from sn_basis.functions import QMessageBox
|
|||||||
from sn_basis.functions.qt_wrapper import QVariant
|
from sn_basis.functions.qt_wrapper import QVariant
|
||||||
from sn_basis.functions.variable_wrapper import get_variable
|
from sn_basis.functions.variable_wrapper import get_variable
|
||||||
|
|
||||||
alkis_NAS_url = "https://geodienste.sachsen.de/aaa/public_alkis/nas/wfs"
|
alkis_sf_url = "https://geodienste.sachsen.de/aaa/public_alkis/sf/wfs"
|
||||||
typename = "adv:AX_BauRaumOderBodenordnungsrecht"
|
typename = "adv:AX_BauRaumOderBodenordnungsrecht"
|
||||||
|
|
||||||
class LoadStatus(Enum):
|
class LoadStatus(Enum):
|
||||||
|
"""Status eines Layer-Ladevorgangs.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
NONE: Nichts geladen
|
||||||
|
FIRST: Erstmalig geladen
|
||||||
|
RELOAD: Neu geladen
|
||||||
|
KEEP: Vorhandener Layer behalten
|
||||||
|
"""
|
||||||
NONE = "none" # nichts geladen
|
NONE = "none" # nichts geladen
|
||||||
FIRST = "first" # erstmalig geladen
|
FIRST = "first" # erstmalig geladen
|
||||||
RELOAD = "reload" # neu geladen
|
RELOAD = "reload" # neu geladen
|
||||||
@@ -18,6 +29,14 @@ class LoadStatus(Enum):
|
|||||||
|
|
||||||
|
|
||||||
def _check_existing_layer(tab_widget):
|
def _check_existing_layer(tab_widget):
|
||||||
|
"""Prüft, ob ein Verfahrensgebiet-Layer bereits existiert.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tab_widget: Parent-Widget für Dialoge
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple[Optional[QgsVectorLayer], LoadStatus]: Layer und Status
|
||||||
|
"""
|
||||||
project = QgsProject.instance()
|
project = QgsProject.instance()
|
||||||
existing_layers = project.mapLayersByName("Verfahrensgebiet")
|
existing_layers = project.mapLayersByName("Verfahrensgebiet")
|
||||||
if not existing_layers:
|
if not existing_layers:
|
||||||
@@ -40,20 +59,33 @@ def _check_existing_layer(tab_widget):
|
|||||||
|
|
||||||
|
|
||||||
def verfahrensgebiet_alkis(tab_widget):
|
def verfahrensgebiet_alkis(tab_widget):
|
||||||
|
"""Lädt Verfahrensgebiet aus ALKIS-WFS basierend auf Verfahrensnummer.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tab_widget: Parent-Widget für Dialoge und Meldungen
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple[Optional[QgsVectorLayer], LoadStatus, List[QgsFeature]]:
|
||||||
|
Geladener Memory-Layer, Lade-Status und Liste der Roh-WFS-Features
|
||||||
|
(für geometriebasierte Weiterverarbeitung, z.B. Gemarkungsschlüssel-
|
||||||
|
Ermittlung via Spatial-Join). Bei Abbruch oder Fehler: ``(None,
|
||||||
|
LoadStatus.NONE, [])``. Bei ``LoadStatus.KEEP`` werden keine
|
||||||
|
WFS-Features neu geladen; die Feature-Liste ist in diesem Fall leer.
|
||||||
|
"""
|
||||||
verfahrensnummer = get_variable("verfahrensnummer")
|
verfahrensnummer = get_variable("verfahrensnummer")
|
||||||
if not verfahrensnummer:
|
if not verfahrensnummer:
|
||||||
QMessageBox.critical(tab_widget, "Fehler",
|
QMessageBox.critical(tab_widget, "Fehler",
|
||||||
"In den Projekteigenschaften ist noch keine Verfahrensnummer eingetragen!")
|
"In den Projekteigenschaften ist noch keine Verfahrensnummer eingetragen!")
|
||||||
return None, LoadStatus.NONE
|
return None, LoadStatus.NONE, []
|
||||||
|
|
||||||
# Vorhandenen Layer prüfen
|
# Vorhandenen Layer prüfen
|
||||||
existing, status = _check_existing_layer(tab_widget)
|
existing, status = _check_existing_layer(tab_widget)
|
||||||
if status == LoadStatus.KEEP:
|
if status == LoadStatus.KEEP:
|
||||||
return existing, status
|
return existing, status, []
|
||||||
|
|
||||||
# WFS mit Filter laden
|
# WFS mit Filter laden
|
||||||
wfs_uri_find = (
|
wfs_uri_find = (
|
||||||
f"url='{alkis_NAS_url}' typename='{typename}' srsname='EPSG:25833' "
|
f"url='{alkis_sf_url}' typename='{typename}' srsname='EPSG:25833' "
|
||||||
f"sql=SELECT * FROM AX_BauRaumOderBodenordnungsrecht "
|
f"sql=SELECT * FROM AX_BauRaumOderBodenordnungsrecht "
|
||||||
f"WHERE bezeichnung LIKE '{verfahrensnummer}%'"
|
f"WHERE bezeichnung LIKE '{verfahrensnummer}%'"
|
||||||
)
|
)
|
||||||
@@ -61,18 +93,18 @@ def verfahrensgebiet_alkis(tab_widget):
|
|||||||
|
|
||||||
if not temp_layer.isValid():
|
if not temp_layer.isValid():
|
||||||
QMessageBox.critical(tab_widget, "Fehler", "Layer konnte nicht geladen werden.")
|
QMessageBox.critical(tab_widget, "Fehler", "Layer konnte nicht geladen werden.")
|
||||||
return None, LoadStatus.NONE
|
return None, LoadStatus.NONE, []
|
||||||
|
|
||||||
features = list(temp_layer.getFeatures())
|
features = list(temp_layer.getFeatures())
|
||||||
if not features:
|
if not features:
|
||||||
QMessageBox.critical(tab_widget, "Fehler",
|
QMessageBox.critical(tab_widget, "Fehler",
|
||||||
f"Verfahrenskennzeichen {verfahrensnummer} nicht im WFS gefunden.")
|
f"Verfahrenskennzeichen {verfahrensnummer} nicht im WFS gefunden.")
|
||||||
return None, LoadStatus.NONE
|
return None, LoadStatus.NONE, []
|
||||||
|
|
||||||
union_geom = QgsGeometry.unaryUnion([f.geometry() for f in features])
|
union_geom = QgsGeometry.unaryUnion([f.geometry() for f in features])
|
||||||
if union_geom is None or union_geom.isEmpty():
|
if union_geom is None or union_geom.isEmpty():
|
||||||
QMessageBox.critical(tab_widget, "Fehler", "Keine Geometrien zum Verschmelzen gefunden.")
|
QMessageBox.critical(tab_widget, "Fehler", "Keine Geometrien zum Verschmelzen gefunden.")
|
||||||
return None, LoadStatus.NONE
|
return None, LoadStatus.NONE, []
|
||||||
|
|
||||||
# Memory-Layer erzeugen
|
# Memory-Layer erzeugen
|
||||||
crs = temp_layer.crs().authid()
|
crs = temp_layer.crs().authid()
|
||||||
@@ -87,5 +119,5 @@ def verfahrensgebiet_alkis(tab_widget):
|
|||||||
pr.addFeatures([feat_union])
|
pr.addFeatures([feat_union])
|
||||||
memory_layer.updateExtents()
|
memory_layer.updateExtents()
|
||||||
|
|
||||||
# Status: FIRST oder RELOAD
|
# Status: FIRST oder RELOAD; features für Gemarkungsermittlung weitergeben
|
||||||
return memory_layer, status
|
return memory_layer, status, features
|
||||||
|
|||||||
@@ -0,0 +1,545 @@
|
|||||||
|
"""sn_verfahrensgebiet/functions/verfahrensgebiet_alkis_komplett.py – Vollständiger VG-Lade-Workflow.
|
||||||
|
|
||||||
|
Orchestriert den vollständigen Arbeitsablauf zum Laden eines Verfahrensgebiets
|
||||||
|
aus ALKIS mit anschließender Flurstücks-Verschneidung, Flächenvergleich und
|
||||||
|
optionaler Umringsprüfung:
|
||||||
|
|
||||||
|
1. Verfahrensgebiet aus ``AX_BauRaumOderBodenordnungsrecht`` per VKZ laden.
|
||||||
|
2. Gemarkungsschlüssel per Geometrie aus ``AX_Gemarkung`` vorschlagen.
|
||||||
|
3. Flurstücke per Dialog (mit Vorschlag) laden.
|
||||||
|
4. Flurstücke räumlich auf das VG einschränken (``selectByLocation``).
|
||||||
|
5. Aus der Auswahl den enriched Verfahrensgebiet-Memory-Layer erzeugen.
|
||||||
|
6. Flächenvergleich zwischen BROR-Objekt und abgeleitetem Verfahrensgebiet.
|
||||||
|
|
||||||
|
- **Übereinstimmung** (≤ Toleranz): modale OK-Meldung, Ablauf beendet.
|
||||||
|
- **Abweichung**: 3-Optionen-Dialog (Detailprüfung / Als VG laden / Abbrechen).
|
||||||
|
|
||||||
|
7. Bei Wahl „Als VG laden":
|
||||||
|
|
||||||
|
- BROR-Objekt direkt mit VG-Namen ins Projekt übernehmen.
|
||||||
|
|
||||||
|
8. Bei Wahl „Detailprüfung" (knickpunktbasiert):
|
||||||
|
|
||||||
|
- Grenzpunkte (AX_PunktortTA) per BBox laden.
|
||||||
|
- Knickpunkte des BROR mit PunktortTA vergleichen (grün/rot/orange).
|
||||||
|
- Ergebnislayer (Punkte) ins Projekt einfügen.
|
||||||
|
- Abschlussmeldung mit Zählung.
|
||||||
|
|
||||||
|
Das BROR-Objekt (``AX_BauRaumOderBodenordnungsrecht``) bleibt bei Erfolg als
|
||||||
|
temporärer Layer ``BauRaumOrdnungsrecht (ALKIS)`` im Projekt erhalten.
|
||||||
|
|
||||||
|
**Rückgabe:**
|
||||||
|
``Tuple[enriched_layer, flst_layer, bror_layer, LoadStatus]``
|
||||||
|
|
||||||
|
**Abbruchverhalten:** Jeder Schritt, der :attr:`LoadStatus.NONE` zurückgibt
|
||||||
|
oder kein Ergebnis liefert, bricht den Workflow ab. Der BROR-Layer wird nur
|
||||||
|
bei Fehler/Abbruch entfernt (wenn er in diesem Lauf hinzugefügt wurde).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Optional, Tuple
|
||||||
|
|
||||||
|
from sn_basis.functions import QgsProject
|
||||||
|
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.dialog_wrapper import (
|
||||||
|
create_progress_dialog,
|
||||||
|
show_info_dialog,
|
||||||
|
ask_detailpruefung_oder_vg_laden,
|
||||||
|
)
|
||||||
|
|
||||||
|
from sn_verfahrensgebiet.functions.verfahrensgebiet_alkis import (
|
||||||
|
verfahrensgebiet_alkis,
|
||||||
|
LoadStatus,
|
||||||
|
)
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Feldname für das Verfahrenskennzeichen im enriched Layer
|
||||||
|
FELD_VKZ = "verfahrensnummer"
|
||||||
|
#: Anzeigename des BROR-Layers im Projekt
|
||||||
|
BROR_LAYER_NAME = "BauRaumOrdnungsrecht (ALKIS)"
|
||||||
|
#: Projektvariable für die Flächentoleranz (in m²); Default 0
|
||||||
|
FLAECHEN_TOLERANZ_VARIABLE = "flaechen_toleranz_m2"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Interne Hilfsfunktionen
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _entferne_hilfs_layer(project, layer_name: str) -> None:
|
||||||
|
"""Entfernt alle Layer mit dem angegebenen Namen aus dem Projekt.
|
||||||
|
|
||||||
|
Wird verwendet, um temporäre WFS-Hilfs-Layer nach der Verschneidung
|
||||||
|
aus dem Projekt zu bereinigen.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
project :
|
||||||
|
Aktuelles QGIS-Projekt.
|
||||||
|
layer_name :
|
||||||
|
Anzeigename des zu entfernenden Layers.
|
||||||
|
"""
|
||||||
|
for lyr in project.mapLayersByName(layer_name):
|
||||||
|
project.removeMapLayer(lyr.id())
|
||||||
|
|
||||||
|
|
||||||
|
def _select_by_location(flst_layer, vg_layer) -> int:
|
||||||
|
"""Selektiert Flurstücke, die innerhalb des Verfahrensgebiets liegen.
|
||||||
|
|
||||||
|
Verwendet ``processing.run("native:selectbylocation")`` mit Prädikat 6
|
||||||
|
(within). Flurstücke, die das VG nur berühren, werden ausgeschlossen.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
flst_layer :
|
||||||
|
Flurstücks-Layer (Quelle der Selektion).
|
||||||
|
vg_layer :
|
||||||
|
Verfahrensgebiet-Layer (Schnittmaske).
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
int
|
||||||
|
Anzahl der selektierten Features; 0 bei Fehler oder leerem Ergebnis.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
import processing # type: ignore[import]
|
||||||
|
|
||||||
|
processing.run(
|
||||||
|
"native:selectbylocation",
|
||||||
|
{
|
||||||
|
"INPUT": flst_layer,
|
||||||
|
"PREDICATE": [6], # 6 = within; schließt reines Berühren aus
|
||||||
|
"INTERSECT": vg_layer,
|
||||||
|
"METHOD": 0, # 0 = neue Selektion
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return flst_layer.selectedFeatureCount()
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def _fuege_vkz_feld_hinzu(layer, vkz: str) -> None:
|
||||||
|
"""Fügt das Feld ``verfahrensnummer`` zum enriched Layer hinzu und befüllt
|
||||||
|
es mit dem Verfahrenskennzeichen, sofern dieses nicht leer ist.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
layer :
|
||||||
|
Enriched Verfahrensgebiet-Memory-Layer (in-place geändert).
|
||||||
|
vkz :
|
||||||
|
Verfahrenskennzeichen aus der Projektvariable ``verfahrensnummer``.
|
||||||
|
"""
|
||||||
|
if not vkz:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
from sn_basis.functions import QgsField
|
||||||
|
from sn_basis.functions.qt_wrapper import QVariant
|
||||||
|
|
||||||
|
pr = layer.dataProvider()
|
||||||
|
pr.addAttributes([QgsField(FELD_VKZ, QVariant.String)])
|
||||||
|
layer.updateFields()
|
||||||
|
|
||||||
|
idx = layer.fields().indexFromName(FELD_VKZ)
|
||||||
|
if idx < 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
changes = {}
|
||||||
|
for feat in layer.getFeatures():
|
||||||
|
changes[feat.id()] = {idx: vkz}
|
||||||
|
pr.changeAttributeValues(changes)
|
||||||
|
layer.updateFields()
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _lese_flaechen_toleranz() -> float:
|
||||||
|
"""Liest die Flächentoleranz aus der Projektvariable (Default: 0.0 m²).
|
||||||
|
|
||||||
|
Die Projektvariable :data:`FLAECHEN_TOLERANZ_VARIABLE` kann in den
|
||||||
|
Projekteigenschaften gesetzt werden. Fehlt sie oder ist sie ungültig,
|
||||||
|
wird 0.0 zurückgegeben (exakter Vergleich).
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
float
|
||||||
|
Toleranz in m² (bei EPSG:25833 identisch mit den Flächen-Einheiten).
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
raw = get_variable(FLAECHEN_TOLERANZ_VARIABLE)
|
||||||
|
if raw is not None and str(raw).strip():
|
||||||
|
return float(str(raw).strip())
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def _flaeche_des_layers(layer) -> float:
|
||||||
|
"""Summiert die Flächen aller Features eines Layers (in CRS-Einheiten²).
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
layer :
|
||||||
|
Beliebiger Polygon-Layer.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
float
|
||||||
|
Summe aller Feature-Flächen; 0.0 wenn keine Features vorhanden.
|
||||||
|
"""
|
||||||
|
total = 0.0
|
||||||
|
for feat in layer.getFeatures():
|
||||||
|
try:
|
||||||
|
geom = feat.geometry()
|
||||||
|
if geom and not geom.isEmpty():
|
||||||
|
total += geom.area()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return total
|
||||||
|
|
||||||
|
|
||||||
|
def _vergleiche_flaechen(
|
||||||
|
bror_layer, vg_layer, toleranz_m2: float = 0.0
|
||||||
|
) -> Tuple[float, float, bool]:
|
||||||
|
"""Vergleicht Flächen von BROR- und Verfahrensgebiet-Layer.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
bror_layer :
|
||||||
|
BauRaumOrdnungsrecht-Layer.
|
||||||
|
vg_layer :
|
||||||
|
Enriched Verfahrensgebiet-Layer.
|
||||||
|
toleranz_m2 :
|
||||||
|
Maximale zulässige Flächendifferenz in m².
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
Tuple[float, float, bool]
|
||||||
|
``(bror_flaeche, vg_flaeche, stimmt_ueberein)``
|
||||||
|
"""
|
||||||
|
bror_flaeche = _flaeche_des_layers(bror_layer)
|
||||||
|
vg_flaeche = _flaeche_des_layers(vg_layer)
|
||||||
|
stimmt_ueberein = abs(bror_flaeche - vg_flaeche) <= toleranz_m2
|
||||||
|
return bror_flaeche, vg_flaeche, stimmt_ueberein
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Öffentliche API
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def verfahrensgebiet_alkis_komplett(
|
||||||
|
tab_widget,
|
||||||
|
) -> Tuple[Optional[object], Optional[object], Optional[object], LoadStatus]:
|
||||||
|
"""Vollständiger VG-Lade-Workflow: ALKIS-VG → Gemarkungen →
|
||||||
|
Flurstücke → Verschneidung → Flächenvergleich → opt. Umringsprüfung.
|
||||||
|
|
||||||
|
Schrittfolge:
|
||||||
|
|
||||||
|
1. ``verfahrensgebiet_alkis(tab_widget)`` – BROR-Objekt als Memory-Layer.
|
||||||
|
2. ``gemarkung_schluessel_und_namen_von_vg_geometrie(geom)`` – Schlüssel.
|
||||||
|
3. ``flurstueck_alkis(...)`` – Flurstücke laden.
|
||||||
|
4. ``_select_by_location(...)`` – räumliche Selektion.
|
||||||
|
5. ``erstelle_verfahrensgebiet_aus_auswahl(...)`` – enriched Layer.
|
||||||
|
6. ``_vergleiche_flaechen(...)`` – Flächenvergleich als Gate.
|
||||||
|
7. Ggf. ``umringspruefung(...)`` nach Nutzerbestätigung.
|
||||||
|
|
||||||
|
Der BROR-Layer (``BauRaumOrdnungsrecht (ALKIS)``) bleibt bei Erfolg
|
||||||
|
im Projekt. Bei Abbruch/Fehler wird er entfernt, falls er in diesem
|
||||||
|
Lauf hinzugefügt wurde.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
tab_widget :
|
||||||
|
Eltern-Widget für alle Dialoge.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
Tuple[Optional[enriched_layer], Optional[flst_layer], Optional[bror_layer], LoadStatus]
|
||||||
|
- ``enriched_layer``: Memory-Layer „Verfahrensgebiet".
|
||||||
|
- ``flst_layer``: Flurstücke-Layer (bleibt im Projekt).
|
||||||
|
- ``bror_layer``: BROR-Layer ``BauRaumOrdnungsrecht (ALKIS)``
|
||||||
|
(bleibt bei Erfolg im Projekt).
|
||||||
|
- :class:`LoadStatus`: ``FIRST``, ``RELOAD`` oder ``NONE``.
|
||||||
|
"""
|
||||||
|
project = QgsProject.instance()
|
||||||
|
alkis_vg_layer = None
|
||||||
|
bror_was_added_by_us = False
|
||||||
|
abgeschlossen = False
|
||||||
|
|
||||||
|
# Fortschrittsdialog für den Basisworkflow (7 Schritte)
|
||||||
|
progress = create_progress_dialog(
|
||||||
|
7, "Verfahrensgebiet laden", "Initialisierung …"
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Schritt 1: VG aus ALKIS laden (BROR-Objekt → Memory-Layer)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
progress.set_label("Schritt 1/6: Verfahrensgebiet aus ALKIS laden …")
|
||||||
|
progress.set_value(0)
|
||||||
|
if progress.is_canceled():
|
||||||
|
return None, None, None, LoadStatus.NONE
|
||||||
|
|
||||||
|
alkis_vg_layer, vg_status, _ = verfahrensgebiet_alkis(tab_widget)
|
||||||
|
|
||||||
|
if vg_status == LoadStatus.NONE or not alkis_vg_layer:
|
||||||
|
return None, None, None, LoadStatus.NONE
|
||||||
|
|
||||||
|
# BROR-Layer umbenennen, um Namenskonflikt mit enriched VG-Layer zu vermeiden
|
||||||
|
alkis_vg_layer.setName(BROR_LAYER_NAME)
|
||||||
|
|
||||||
|
if vg_status in (LoadStatus.FIRST, LoadStatus.RELOAD):
|
||||||
|
project.addMapLayer(alkis_vg_layer)
|
||||||
|
bror_was_added_by_us = True
|
||||||
|
|
||||||
|
progress.set_value(1)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Schritt 2: Gemarkungsschlüssel per Geometrie-Spatial-Join ermitteln
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
progress.set_label("Schritt 2/6: Gemarkungsschlüssel ermitteln …")
|
||||||
|
if progress.is_canceled():
|
||||||
|
return None, None, None, LoadStatus.NONE
|
||||||
|
|
||||||
|
bror_feats = list(alkis_vg_layer.getFeatures())
|
||||||
|
vg_geom = bror_feats[0].geometry() if bror_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(2)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Schritt 3: Flurstücke laden (mit Nutzer-Dialog, Vorschlag befüllt)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
progress.set_label("Schritt 3/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(3)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Schritt 4: Räumliche Selektion
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
progress.set_label("Schritt 4/6: Flurstücke räumlich selektieren …")
|
||||||
|
if progress.is_canceled():
|
||||||
|
return None, None, None, LoadStatus.NONE
|
||||||
|
|
||||||
|
anzahl = _select_by_location(flst_layer, alkis_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(4)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Schritt 5: Enriched Memory-Layer aus Auswahl erzeugen
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
progress.set_label("Schritt 5/6: Verfahrensgebiet-Layer erzeugen …")
|
||||||
|
if progress.is_canceled():
|
||||||
|
return None, None, None, LoadStatus.NONE
|
||||||
|
|
||||||
|
enriched_layer, erfolgreich, fehlgeschlagen = erstelle_verfahrensgebiet_aus_auswahl(
|
||||||
|
flst_layer
|
||||||
|
)
|
||||||
|
|
||||||
|
if not enriched_layer or not enriched_layer.isValid():
|
||||||
|
QMessageBox.critical(
|
||||||
|
tab_widget,
|
||||||
|
"Fehler",
|
||||||
|
"Verfahrensgebiet-Layer konnte nicht erstellt werden.",
|
||||||
|
)
|
||||||
|
return None, flst_layer, None, LoadStatus.NONE
|
||||||
|
|
||||||
|
vkz = get_variable("verfahrensnummer") or ""
|
||||||
|
_fuege_vkz_feld_hinzu(enriched_layer, vkz)
|
||||||
|
|
||||||
|
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()
|
||||||
|
bror_flaeche, vg_flaeche, stimmt_ueberein = _vergleiche_flaechen(
|
||||||
|
alkis_vg_layer, enriched_layer, toleranz
|
||||||
|
)
|
||||||
|
|
||||||
|
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 enriched_layer, flst_layer, alkis_vg_layer, vg_status
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Flächenabweichung → 3-Optionen-Dialog
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
progress.close()
|
||||||
|
diff = abs(bror_flaeche - vg_flaeche)
|
||||||
|
entscheidung = ask_detailpruefung_oder_vg_laden(
|
||||||
|
tab_widget,
|
||||||
|
"Abweichung erkannt",
|
||||||
|
f"Abweichung zwischen BauRaumOrdnungsrecht-Objekt und abgeleitetem "
|
||||||
|
f"Verfahrensgebiet erkannt.\n\n"
|
||||||
|
f"BauRaumOrdnungsrecht: {bror_flaeche:.2f} m²\n"
|
||||||
|
f"Verfahrensgebiet: {vg_flaeche:.2f} m²\n"
|
||||||
|
f"Differenz: {diff:.2f} m²\n\n"
|
||||||
|
f"Wie möchten Sie fortfahren?",
|
||||||
|
)
|
||||||
|
|
||||||
|
if entscheidung == "abbrechen":
|
||||||
|
abgeschlossen = True
|
||||||
|
return enriched_layer, flst_layer, alkis_vg_layer, vg_status
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Option: Bodenordnungsobjekt direkt als Verfahrensgebiet laden
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
if entscheidung == "als_vg_laden":
|
||||||
|
# Layer bleibt im Projekt – nur umbenennen. working_tab überspringt
|
||||||
|
# addMapLayer wenn der Layer bereits registriert ist.
|
||||||
|
alkis_vg_layer.setName(VERF_LAYER_NAME)
|
||||||
|
bror_was_added_by_us = False
|
||||||
|
abgeschlossen = True
|
||||||
|
return alkis_vg_layer, None, None, vg_status
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Option: Detailprüfung (knickpunktbasiert)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Knickpunkte vorab extrahieren, um Schrittanzahl zu bestimmen
|
||||||
|
knickpunkte = _extrahiere_knickpunkte(alkis_vg_layer)
|
||||||
|
n_knick = len(knickpunkte)
|
||||||
|
# Gesamtschritte: 0=Start, 1=PTA-Index, 2…n+1=Knickpunkte, n+2=Orange, n+3=Fertig
|
||||||
|
# + 1 Schritt vorab für Grenzpunkte laden
|
||||||
|
gesamt_schritte = n_knick + 5
|
||||||
|
|
||||||
|
detail_progress = create_progress_dialog(
|
||||||
|
gesamt_schritte,
|
||||||
|
"Detailprüfung (Knickpunkte)",
|
||||||
|
"Schritt 1: Grenzpunkte laden …",
|
||||||
|
)
|
||||||
|
detail_progress.set_value(0)
|
||||||
|
|
||||||
|
# Schritt 1: PunktortTA gemarkungsweise laden (Hits + 5000er-Batches)
|
||||||
|
punktort_layer = lade_punktort_ta(
|
||||||
|
vorgabe_schluessel, vg_geom, progress=detail_progress
|
||||||
|
)
|
||||||
|
|
||||||
|
if detail_progress.is_canceled():
|
||||||
|
detail_progress.close()
|
||||||
|
abgeschlossen = True
|
||||||
|
return enriched_layer, flst_layer, alkis_vg_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 enriched_layer, flst_layer, alkis_vg_layer, vg_status
|
||||||
|
|
||||||
|
# Schritte 2…Ende: Knickpunktprüfung
|
||||||
|
ergebnis_layer, identisch, kein_pta, fehlender_knick = knickpunkt_pruefung(
|
||||||
|
alkis_vg_layer,
|
||||||
|
punktort_layer,
|
||||||
|
flst_layer,
|
||||||
|
progress=detail_progress,
|
||||||
|
knickpunkte=knickpunkte,
|
||||||
|
)
|
||||||
|
|
||||||
|
if detail_progress.is_canceled():
|
||||||
|
detail_progress.close()
|
||||||
|
abgeschlossen = True
|
||||||
|
return enriched_layer, flst_layer, alkis_vg_layer, vg_status
|
||||||
|
|
||||||
|
detail_progress.set_value(gesamt_schritte)
|
||||||
|
detail_progress.close()
|
||||||
|
|
||||||
|
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 enriched_layer, flst_layer, alkis_vg_layer, vg_status
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# WFS-Temp-Layer stets aus dem Projekt entfernen
|
||||||
|
_entferne_hilfs_layer(project, "TempVerfahrensgebiet")
|
||||||
|
# BROR-Layer bei Fehler/Abbruch entfernen (nur wenn in diesem Lauf hinzugefügt)
|
||||||
|
if not abgeschlossen and bror_was_added_by_us and alkis_vg_layer is not None:
|
||||||
|
try:
|
||||||
|
project.removeMapLayer(alkis_vg_layer.id())
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
@@ -0,0 +1,358 @@
|
|||||||
|
"""sn_verfahrensgebiet/functions/verfahrensgebiet_edit_service.py
|
||||||
|
|
||||||
|
Service-Modul für Verfahrensgebiet-Editoperationen aus Flurstück-Auswahl.
|
||||||
|
|
||||||
|
Der Verfahrensgebiet-Layer ist ein Single-Feature-Memory-Layer: alle
|
||||||
|
Flurstück-Geometrien werden via ``QgsGeometry.unaryUnion`` zu einem einzigen
|
||||||
|
(Multi-)Polygon verschmolzen und als ein Feature gespeichert. Dadurch
|
||||||
|
erscheint das Verfahrensgebiet im Kartenfenster als zusammenhängendes Objekt.
|
||||||
|
|
||||||
|
Die Flurstückskennzeichen werden als kommagetrennte Zeichenkette im
|
||||||
|
Attributfeld ``flurstueckskennzeichen`` des Single-Features gespeichert,
|
||||||
|
sodass Delta-Operationen (Ergänzen/Entfernen) ohne erneuten Zugriff auf den
|
||||||
|
Quell-Flurstück-Layer möglich bleiben und die ID-Menge via
|
||||||
|
``get_kennzeichen_menge()`` jederzeit abrufbar ist.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Optional, Tuple, List
|
||||||
|
|
||||||
|
from sn_basis.functions import QgsVectorLayer, QgsFeature, QgsField
|
||||||
|
from sn_basis.functions.qgiscore_wrapper import QgsGeometry
|
||||||
|
from sn_basis.functions.qt_wrapper import QVariant
|
||||||
|
from sn_basis.modules.layerpruefer import Layerpruefer
|
||||||
|
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
|
||||||
|
|
||||||
|
VERF_LAYER_NAME = "Verfahrensgebiet"
|
||||||
|
FELD_KENNZEICHEN = "flurstueckskennzeichen"
|
||||||
|
FELD_GEMARKUNG = "gemarkung_ax_gemarkung_schluessel_gemarkungsnummer"
|
||||||
|
PFLICHTFELDER: List[str] = [FELD_KENNZEICHEN, FELD_GEMARKUNG]
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Vorbedingungsprüfungen
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def pruefe_flurstueck_layer(layer) -> pruef_ergebnis:
|
||||||
|
"""Prüft, ob ein Layer als Flurstück-Quelle geeignet ist.
|
||||||
|
|
||||||
|
Prüfreihenfolge via Layerpruefer:
|
||||||
|
existiert → Vektorlayer → Polygon-Geometrie → nicht leer → Pflichtfelder vorhanden.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
pruef_ergebnis
|
||||||
|
ok=True, wenn alle Prüfungen bestanden wurden.
|
||||||
|
"""
|
||||||
|
pruefer = Layerpruefer(
|
||||||
|
layer=layer,
|
||||||
|
erwarteter_layertyp="vector",
|
||||||
|
erwarteter_geotyp="Polygon",
|
||||||
|
erforderliche_felder=PFLICHTFELDER,
|
||||||
|
)
|
||||||
|
return pruefer.pruefe()
|
||||||
|
|
||||||
|
|
||||||
|
def pruefe_auswahl(layer) -> pruef_ergebnis:
|
||||||
|
"""Prüft, ob auf dem Layer mindestens ein Feature ausgewählt ist.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
pruef_ergebnis
|
||||||
|
ok=True bei mindestens einer Auswahl, sonst ok=False mit aktion ``layer_leer``.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
count = int(layer.selectedFeatureCount())
|
||||||
|
except Exception:
|
||||||
|
count = 0
|
||||||
|
|
||||||
|
if count == 0:
|
||||||
|
return pruef_ergebnis(
|
||||||
|
ok=False,
|
||||||
|
meldung="Es sind keine Flurstücke ausgewählt. Bitte zuerst eine Auswahl treffen.",
|
||||||
|
aktion="layer_leer",
|
||||||
|
kontext=None,
|
||||||
|
)
|
||||||
|
return pruef_ergebnis(
|
||||||
|
ok=True,
|
||||||
|
meldung=f"{count} Flurstücke ausgewählt.",
|
||||||
|
aktion="ok",
|
||||||
|
kontext=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Hilfsfunktionen
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def get_kennzeichen_menge(layer) -> set:
|
||||||
|
"""Gibt die Menge aller ``flurstueckskennzeichen`` aus dem Layer zurück.
|
||||||
|
|
||||||
|
Im Single-Feature-Modell sind die Kennzeichen als kommagetrennte
|
||||||
|
Zeichenkette im einzigen Feature gespeichert.
|
||||||
|
"""
|
||||||
|
result = set()
|
||||||
|
for feat in layer.getFeatures():
|
||||||
|
val = feat[FELD_KENNZEICHEN]
|
||||||
|
if val:
|
||||||
|
for kz in str(val).split(","):
|
||||||
|
kz = kz.strip()
|
||||||
|
if kz:
|
||||||
|
result.add(kz)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _erstelle_memory_layer(crs_authid: str) -> QgsVectorLayer:
|
||||||
|
"""Erzeugt einen leeren Multi-Feature-Memory-Layer für das Verfahrensgebiet."""
|
||||||
|
layer = QgsVectorLayer(
|
||||||
|
f"MultiPolygon?crs={crs_authid}", VERF_LAYER_NAME, "memory"
|
||||||
|
)
|
||||||
|
pr = layer.dataProvider()
|
||||||
|
pr.addAttributes([
|
||||||
|
QgsField(FELD_KENNZEICHEN, QVariant.String),
|
||||||
|
QgsField(FELD_GEMARKUNG, QVariant.String),
|
||||||
|
])
|
||||||
|
layer.updateFields()
|
||||||
|
return layer
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Hauptoperationen
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def erstelle_verfahrensgebiet_aus_auswahl(
|
||||||
|
source_layer,
|
||||||
|
) -> Tuple[Optional[object], int, int]:
|
||||||
|
"""Erstellt neuen Verfahrensgebiet-Memory-Layer aus der aktuellen Auswahl.
|
||||||
|
|
||||||
|
Alle Geometrien werden via ``QgsGeometry.unaryUnion`` zu einem einzigen
|
||||||
|
(Multi-)Polygon verschmolzen. Der Layer enthält danach genau ein Feature.
|
||||||
|
Die Flurstückskennzeichen werden kommagetrennt im Attribut gespeichert.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
source_layer :
|
||||||
|
Flurstück-Layer mit aktiver Auswahl.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
Tuple[layer, erfolgreich, fehlgeschlagen]
|
||||||
|
"""
|
||||||
|
crs = source_layer.crs().authid()
|
||||||
|
mem_layer = _erstelle_memory_layer(crs)
|
||||||
|
pr = mem_layer.dataProvider()
|
||||||
|
|
||||||
|
erfolgreich = 0
|
||||||
|
fehlgeschlagen = 0
|
||||||
|
geom_list = []
|
||||||
|
kz_list = []
|
||||||
|
gemarkung_set = set()
|
||||||
|
|
||||||
|
for src_feat in source_layer.selectedFeatures():
|
||||||
|
try:
|
||||||
|
geom = src_feat.geometry()
|
||||||
|
if not geom or geom.isEmpty():
|
||||||
|
fehlgeschlagen += 1
|
||||||
|
continue
|
||||||
|
geom_list.append(geom)
|
||||||
|
kz = str(src_feat[FELD_KENNZEICHEN] or "")
|
||||||
|
gm = str(src_feat[FELD_GEMARKUNG] or "")
|
||||||
|
if kz:
|
||||||
|
kz_list.append(kz)
|
||||||
|
if gm:
|
||||||
|
gemarkung_set.add(gm)
|
||||||
|
erfolgreich += 1
|
||||||
|
except Exception:
|
||||||
|
fehlgeschlagen += 1
|
||||||
|
|
||||||
|
if geom_list:
|
||||||
|
union_geom = QgsGeometry.unaryUnion(geom_list)
|
||||||
|
if union_geom and not union_geom.isEmpty():
|
||||||
|
feat = QgsFeature()
|
||||||
|
feat.setGeometry(union_geom)
|
||||||
|
feat.setAttributes([
|
||||||
|
",".join(kz_list),
|
||||||
|
",".join(sorted(gemarkung_set)),
|
||||||
|
])
|
||||||
|
pr.addFeatures([feat])
|
||||||
|
mem_layer.updateExtents()
|
||||||
|
|
||||||
|
return mem_layer, erfolgreich, fehlgeschlagen
|
||||||
|
|
||||||
|
|
||||||
|
def ergaenze_verfahrensgebiet(
|
||||||
|
verf_layer,
|
||||||
|
source_layer,
|
||||||
|
) -> Tuple[object, int, int, int]:
|
||||||
|
"""Fügt nur neue (noch nicht enthaltene) Flurstücke zum Verfahrensgebiet hinzu.
|
||||||
|
|
||||||
|
Die Geometrien der neuen Flurstücke werden mit der bestehenden Union-Geometrie
|
||||||
|
verschmolzen. Das Single-Feature wird an Ort und Stelle aktualisiert.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
verf_layer :
|
||||||
|
Bestehender Verfahrensgebiet-Memory-Layer (Single-Feature).
|
||||||
|
source_layer :
|
||||||
|
Flurstück-Layer mit aktiver Auswahl.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
Tuple[verf_layer, hinzugefuegt, bereits_vorhanden, fehlgeschlagen]
|
||||||
|
"""
|
||||||
|
vorhandene_ids = get_kennzeichen_menge(verf_layer)
|
||||||
|
pr = verf_layer.dataProvider()
|
||||||
|
|
||||||
|
hinzugefuegt = 0
|
||||||
|
bereits_vorhanden = 0
|
||||||
|
fehlgeschlagen = 0
|
||||||
|
neue_geoms = []
|
||||||
|
neue_kz = []
|
||||||
|
neue_gemarkungen: set = set()
|
||||||
|
|
||||||
|
for src_feat in source_layer.selectedFeatures():
|
||||||
|
try:
|
||||||
|
kz = str(src_feat[FELD_KENNZEICHEN] or "")
|
||||||
|
if kz in vorhandene_ids:
|
||||||
|
bereits_vorhanden += 1
|
||||||
|
continue
|
||||||
|
geom = src_feat.geometry()
|
||||||
|
if not geom or geom.isEmpty():
|
||||||
|
fehlgeschlagen += 1
|
||||||
|
continue
|
||||||
|
neue_geoms.append(geom)
|
||||||
|
if kz:
|
||||||
|
neue_kz.append(kz)
|
||||||
|
gm = str(src_feat[FELD_GEMARKUNG] or "")
|
||||||
|
if gm:
|
||||||
|
neue_gemarkungen.add(gm)
|
||||||
|
hinzugefuegt += 1
|
||||||
|
except Exception:
|
||||||
|
fehlgeschlagen += 1
|
||||||
|
|
||||||
|
if neue_geoms:
|
||||||
|
existing_feats = list(verf_layer.getFeatures())
|
||||||
|
if existing_feats:
|
||||||
|
existing_feat = existing_feats[0]
|
||||||
|
all_geoms = [existing_feat.geometry()] + neue_geoms
|
||||||
|
new_union = QgsGeometry.unaryUnion(all_geoms)
|
||||||
|
alle_kz = sorted(vorhandene_ids | set(neue_kz))
|
||||||
|
existing_gm_str = existing_feat[FELD_GEMARKUNG] or ""
|
||||||
|
alle_gm = sorted(
|
||||||
|
{g.strip() for g in existing_gm_str.split(",") if g.strip()} | neue_gemarkungen
|
||||||
|
)
|
||||||
|
kz_idx = verf_layer.fields().indexFromName(FELD_KENNZEICHEN)
|
||||||
|
gm_idx = verf_layer.fields().indexFromName(FELD_GEMARKUNG)
|
||||||
|
pr.changeAttributeValues({existing_feat.id(): {
|
||||||
|
kz_idx: ",".join(alle_kz),
|
||||||
|
gm_idx: ",".join(alle_gm),
|
||||||
|
}})
|
||||||
|
pr.changeGeometryValues({existing_feat.id(): new_union})
|
||||||
|
else:
|
||||||
|
# Kein vorhandenes Feature – neues anlegen
|
||||||
|
union_geom = QgsGeometry.unaryUnion(neue_geoms)
|
||||||
|
if union_geom and not union_geom.isEmpty():
|
||||||
|
feat = QgsFeature()
|
||||||
|
feat.setGeometry(union_geom)
|
||||||
|
feat.setAttributes([
|
||||||
|
",".join(sorted(neue_kz)),
|
||||||
|
",".join(sorted(neue_gemarkungen)),
|
||||||
|
])
|
||||||
|
pr.addFeatures([feat])
|
||||||
|
verf_layer.updateExtents()
|
||||||
|
verf_layer.triggerRepaint()
|
||||||
|
|
||||||
|
return verf_layer, hinzugefuegt, bereits_vorhanden, fehlgeschlagen
|
||||||
|
|
||||||
|
|
||||||
|
def entferne_aus_verfahrensgebiet(
|
||||||
|
verf_layer,
|
||||||
|
source_layer,
|
||||||
|
) -> Tuple[object, int, int, int]:
|
||||||
|
"""Entfernt Flurstücke aus dem Verfahrensgebiet.
|
||||||
|
|
||||||
|
Die Geometrien der zu entfernenden Flurstücke werden per ``difference()``
|
||||||
|
von der bestehenden Union-Geometrie subtrahiert.
|
||||||
|
Nicht enthaltene Flurstücke werden gezählt, aber ignoriert.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
verf_layer :
|
||||||
|
Bestehender Verfahrensgebiet-Memory-Layer (Single-Feature).
|
||||||
|
source_layer :
|
||||||
|
Flurstück-Layer mit aktiver Auswahl.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
Tuple[verf_layer, entfernt, nicht_enthalten, fehlgeschlagen]
|
||||||
|
"""
|
||||||
|
existing_feats = list(verf_layer.getFeatures())
|
||||||
|
if not existing_feats:
|
||||||
|
auswahl_kz = {
|
||||||
|
str(f[FELD_KENNZEICHEN] or "")
|
||||||
|
for f in source_layer.selectedFeatures()
|
||||||
|
if f[FELD_KENNZEICHEN]
|
||||||
|
}
|
||||||
|
return verf_layer, 0, len(auswahl_kz), 0
|
||||||
|
|
||||||
|
existing_feat = existing_feats[0]
|
||||||
|
existing_kz_str = existing_feat[FELD_KENNZEICHEN] or ""
|
||||||
|
vorhandene_ids = {k.strip() for k in existing_kz_str.split(",") if k.strip()}
|
||||||
|
|
||||||
|
auswahl_kz = {
|
||||||
|
str(f[FELD_KENNZEICHEN] or "")
|
||||||
|
for f in source_layer.selectedFeatures()
|
||||||
|
if f[FELD_KENNZEICHEN]
|
||||||
|
}
|
||||||
|
nicht_enthalten = len(auswahl_kz - vorhandene_ids)
|
||||||
|
zu_entfernen_kz = auswahl_kz & vorhandene_ids
|
||||||
|
|
||||||
|
if not zu_entfernen_kz:
|
||||||
|
return verf_layer, 0, nicht_enthalten, 0
|
||||||
|
|
||||||
|
result_geom = existing_feat.geometry()
|
||||||
|
fehlgeschlagen = 0
|
||||||
|
tatsaechlich_entfernt = 0
|
||||||
|
|
||||||
|
for src_feat in source_layer.selectedFeatures():
|
||||||
|
kz = str(src_feat[FELD_KENNZEICHEN] or "")
|
||||||
|
if kz not in zu_entfernen_kz:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
geom = src_feat.geometry()
|
||||||
|
if geom and not geom.isEmpty():
|
||||||
|
result_geom = result_geom.difference(geom)
|
||||||
|
tatsaechlich_entfernt += 1
|
||||||
|
except Exception:
|
||||||
|
fehlgeschlagen += 1
|
||||||
|
|
||||||
|
pr = verf_layer.dataProvider()
|
||||||
|
verbleibende_kz = sorted(vorhandene_ids - zu_entfernen_kz)
|
||||||
|
|
||||||
|
if not verbleibende_kz or result_geom is None or result_geom.isEmpty():
|
||||||
|
pr.deleteFeatures([existing_feat.id()])
|
||||||
|
else:
|
||||||
|
kz_idx = verf_layer.fields().indexFromName(FELD_KENNZEICHEN)
|
||||||
|
pr.changeGeometryValues({existing_feat.id(): result_geom})
|
||||||
|
pr.changeAttributeValues({existing_feat.id(): {
|
||||||
|
kz_idx: ",".join(verbleibende_kz),
|
||||||
|
}})
|
||||||
|
|
||||||
|
verf_layer.updateExtents()
|
||||||
|
verf_layer.triggerRepaint()
|
||||||
|
return verf_layer, tatsaechlich_entfernt, nicht_enthalten, fehlgeschlagen
|
||||||
|
|
||||||
|
|
||||||
|
def ersetze_verfahrensgebiet(
|
||||||
|
source_layer,
|
||||||
|
) -> Tuple[Optional[object], int, int]:
|
||||||
|
"""Erstellt einen neuen Verfahrensgebiet-Layer aus der Auswahl (Ersetzen-Semantik).
|
||||||
|
|
||||||
|
Der Aufrufer ist verantwortlich, den alten Layer aus dem Projekt zu entfernen,
|
||||||
|
bevor der neue Layer hinzugefügt wird.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
Tuple[layer, erfolgreich, fehlgeschlagen]
|
||||||
|
Entspricht ``erstelle_verfahrensgebiet_aus_auswahl``.
|
||||||
|
"""
|
||||||
|
return erstelle_verfahrensgebiet_aus_auswahl(source_layer)
|
||||||
@@ -1,10 +1,21 @@
|
|||||||
|
"""sn_verfahrensgebiet/main.py – Einstiegspunkt des sn_verfahrensgebiet Plugins.
|
||||||
|
Orchestriert die Verwaltung von Verfahrensgebiets-Layern und deren Eigenschaften.
|
||||||
|
"""
|
||||||
|
|
||||||
from qgis.utils import plugins
|
from qgis.utils import plugins
|
||||||
from sn_basis.ui.dockmanager import DockManager
|
from sn_basis.ui.dockmanager import DockManager
|
||||||
from .ui.dockwidget import DockWidget
|
from .ui.dockwidget import DockWidget
|
||||||
|
from sn_basis.modules.DataGrabber import DataGrabber
|
||||||
|
from sn_basis.modules.Pruefmanager import Pruefmanager
|
||||||
|
|
||||||
class Verfahrensgebiet:
|
class Verfahrensgebiet:
|
||||||
|
"""Einstiegspunkt des sn_verfahrensgebiet-Plugins.
|
||||||
|
Verwaltet die DockWidget-Initialisierung und das Verfahrensgebiets-Management.
|
||||||
|
"""
|
||||||
def __init__(self, iface):
|
def __init__(self, iface):
|
||||||
self.iface = iface
|
self.iface = iface
|
||||||
|
self.pruefmanager = Pruefmanager(ui_modus="qgis")
|
||||||
|
self.data_grabber = DataGrabber(pruefmanager=self.pruefmanager)
|
||||||
self.action = None
|
self.action = None
|
||||||
self.dockwidget = None
|
self.dockwidget = None
|
||||||
|
|
||||||
@@ -50,7 +61,12 @@ class Verfahrensgebiet:
|
|||||||
# if basis and basis.ui:
|
# if basis and basis.ui:
|
||||||
# basis.ui.set_active_plugin(self.action)
|
# basis.ui.set_active_plugin(self.action)
|
||||||
def run(self):
|
def run(self):
|
||||||
self.dockwidget = DockWidget(self.iface.mainWindow(), subtitle=self.plugin_name)
|
self.dockwidget = DockWidget(
|
||||||
|
self.iface.mainWindow(),
|
||||||
|
subtitle=self.plugin_name,
|
||||||
|
pruefmanager=self.pruefmanager,
|
||||||
|
data_grabber=self.data_grabber,
|
||||||
|
)
|
||||||
self.dockwidget.setObjectName(self.dock_name)
|
self.dockwidget.setObjectName(self.dock_name)
|
||||||
|
|
||||||
# Action-Referenz im Dock speichern
|
# Action-Referenz im Dock speichern
|
||||||
|
|||||||
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
[general]
|
[general]
|
||||||
version=26.3.1-unstable
|
version=26.5.3-unstable
|
||||||
name=LNO Sachsen | Verfahrensgebiet
|
name=LNO Sachsen | Verfahrensgebiet
|
||||||
description=Plugin zum Erzeugen eines Objektes "Verfahrensgebiet"
|
description=Plugin zum Erzeugen eines Objektes "Verfahrensgebiet"
|
||||||
author=Daniel Helbig, Michael Otto
|
author=Daniel Helbig, Michael Otto
|
||||||
homepage=https://entwicklung.flurneuordnung-sachsen.de/AG_QGIS/Plugin_SN_Verfahrensgebiet
|
homepage=https://entwicklung.flurneuordnung-sachsen.de/AG_QGIS/Plugin_SN_Verfahrensgebiet
|
||||||
tracker=https://entwicklung.flurneuordnung-sachsen.de/AG_QGIS/Plugin_SN_Verfahrensgebiet/issues
|
tracker=https://entwicklung.flurneuordnung-sachsen.de/AG_QGIS/Plugin_SN_Verfahrensgebiet/issues
|
||||||
repository=https://entwicklung.flurneuordnung-sachsen.de/AG_QGIS/Plugin_SN_Verfahrensgebiet/src/branch/unstable/
|
repository=https://entwicklung.flurneuordnung-sachsen.de/AG_QGIS/Plugin_SN_Verfahrensgebiet/src/branch/unstable/
|
||||||
|
QGISMaximumVersion=4.99
|
||||||
qgisMinimumVersion=3.40
|
qgisMinimumVersion=3.40
|
||||||
experimental=true
|
experimental=true
|
||||||
deprecated=false
|
deprecated=false
|
||||||
|
|||||||
@@ -0,0 +1,349 @@
|
|||||||
|
<!DOCTYPE qgis PUBLIC 'http://mrcc.com/qgis.dtd' 'SYSTEM'>
|
||||||
|
<qgis labelsEnabled="1" version="3.40.4-Bratislava" styleCategories="Symbology|Labeling">
|
||||||
|
<renderer-v2 enableorderby="0" type="singleSymbol" referencescale="-1" forceraster="0" symbollevels="0">
|
||||||
|
<symbols>
|
||||||
|
<symbol name="0" type="fill" force_rhr="0" clip_to_extent="1" is_animated="0" frame_rate="10" alpha="1">
|
||||||
|
<data_defined_properties>
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="name" type="QString" value=""/>
|
||||||
|
<Option name="properties"/>
|
||||||
|
<Option name="type" type="QString" value="collection"/>
|
||||||
|
</Option>
|
||||||
|
</data_defined_properties>
|
||||||
|
<layer id="{bc287a30-ef97-44d4-ac42-1890985a8e6e}" class="SimpleFill" locked="0" enabled="1" pass="0">
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="border_width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="color" type="QString" value="229,182,54,255,rgb:0.89803921568627454,0.71372549019607845,0.21176470588235294,1"/>
|
||||||
|
<Option name="joinstyle" type="QString" value="bevel"/>
|
||||||
|
<Option name="offset" type="QString" value="0,0"/>
|
||||||
|
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="offset_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="outline_color" type="QString" value="35,35,35,255,rgb:0.13725490196078433,0.13725490196078433,0.13725490196078433,1"/>
|
||||||
|
<Option name="outline_style" type="QString" value="solid"/>
|
||||||
|
<Option name="outline_width" type="QString" value="0.5"/>
|
||||||
|
<Option name="outline_width_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="style" type="QString" value="no"/>
|
||||||
|
</Option>
|
||||||
|
<data_defined_properties>
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="name" type="QString" value=""/>
|
||||||
|
<Option name="properties"/>
|
||||||
|
<Option name="type" type="QString" value="collection"/>
|
||||||
|
</Option>
|
||||||
|
</data_defined_properties>
|
||||||
|
</layer>
|
||||||
|
</symbol>
|
||||||
|
</symbols>
|
||||||
|
<rotation/>
|
||||||
|
<sizescale/>
|
||||||
|
<data-defined-properties>
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="name" type="QString" value=""/>
|
||||||
|
<Option name="properties"/>
|
||||||
|
<Option name="type" type="QString" value="collection"/>
|
||||||
|
</Option>
|
||||||
|
</data-defined-properties>
|
||||||
|
</renderer-v2>
|
||||||
|
<selection mode="Default">
|
||||||
|
<selectionColor invalid="1"/>
|
||||||
|
<selectionSymbol>
|
||||||
|
<symbol name="" type="fill" force_rhr="0" clip_to_extent="1" is_animated="0" frame_rate="10" alpha="1">
|
||||||
|
<data_defined_properties>
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="name" type="QString" value=""/>
|
||||||
|
<Option name="properties"/>
|
||||||
|
<Option name="type" type="QString" value="collection"/>
|
||||||
|
</Option>
|
||||||
|
</data_defined_properties>
|
||||||
|
<layer id="{819785db-4b31-4c4c-8332-821c09f81921}" class="SimpleFill" locked="0" enabled="1" pass="0">
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="border_width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="color" type="QString" value="0,0,255,255,rgb:0,0,1,1"/>
|
||||||
|
<Option name="joinstyle" type="QString" value="bevel"/>
|
||||||
|
<Option name="offset" type="QString" value="0,0"/>
|
||||||
|
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="offset_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="outline_color" type="QString" value="35,35,35,255,rgb:0.13725490196078433,0.13725490196078433,0.13725490196078433,1"/>
|
||||||
|
<Option name="outline_style" type="QString" value="solid"/>
|
||||||
|
<Option name="outline_width" type="QString" value="0.26"/>
|
||||||
|
<Option name="outline_width_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="style" type="QString" value="solid"/>
|
||||||
|
</Option>
|
||||||
|
<data_defined_properties>
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="name" type="QString" value=""/>
|
||||||
|
<Option name="properties"/>
|
||||||
|
<Option name="type" type="QString" value="collection"/>
|
||||||
|
</Option>
|
||||||
|
</data_defined_properties>
|
||||||
|
</layer>
|
||||||
|
</symbol>
|
||||||
|
</selectionSymbol>
|
||||||
|
</selection>
|
||||||
|
<labeling type="rule-based">
|
||||||
|
<rules key="{15ec465c-d050-4a64-b4ba-262af80bbb43}">
|
||||||
|
<rule key="{b56520a8-9937-40dd-b8fe-c4386271088d}" filter=""flurstuecksnummer_ax_flurstuecksnummer_nenner" IS NULL" description="FlstNr_ohne_Zusatz">
|
||||||
|
<settings calloutType="simple">
|
||||||
|
<text-style forcedItalic="0" fontUnderline="0" previewBkgrdColor="255,255,255,255,rgb:1,1,1,1" fontItalic="0" tabStopDistanceUnit="Point" textOpacity="1" multilineHeight="1" fontFamily="Times New Roman" fontStrikeout="0" capitalization="0" fieldName="flurstuecksnummer_ax_flurstuecksnummer_zaehler" legendString="Aa" fontSizeUnit="Point" fontWeight="50" textOrientation="horizontal" multilineHeightUnit="Percentage" fontKerning="1" tabStopDistanceMapUnitScale="3x:0,0,0,0,0,0" forcedBold="0" fontWordSpacing="0" fontLetterSpacing="0" fontSizeMapUnitScale="3x:0,0,0,0,0,0" allowHtml="0" isExpression="0" useSubstitutions="0" textColor="0,0,0,255,rgb:0,0,0,1" blendMode="0" fontSize="7" namedStyle="Standard" tabStopDistance="80">
|
||||||
|
<families/>
|
||||||
|
<text-buffer bufferBlendMode="0" bufferSizeUnits="MM" bufferOpacity="1" bufferJoinStyle="128" bufferNoFill="1" bufferSizeMapUnitScale="3x:0,0,0,0,0,0" bufferDraw="1" bufferColor="255,255,255,255,rgb:1,1,1,1" bufferSize="1"/>
|
||||||
|
<text-mask maskEnabled="0" maskSizeMapUnitScale="3x:0,0,0,0,0,0" maskSizeUnits="MM" maskJoinStyle="128" maskSize2="1.5" maskSize="1.5" maskOpacity="1" maskType="0" maskedSymbolLayers=""/>
|
||||||
|
<background shapeOffsetX="0" shapeRadiiY="0" shapeSVGFile="" shapeRotation="0" shapeBorderWidth="0" shapeRadiiX="0" shapeSizeX="0" shapeBorderWidthMapUnitScale="3x:0,0,0,0,0,0" shapeSizeUnit="MM" shapeRotationType="0" shapeBorderColor="128,128,128,255,rgb:0.50196078431372548,0.50196078431372548,0.50196078431372548,1" shapeJoinStyle="64" shapeRadiiMapUnitScale="3x:0,0,0,0,0,0" shapeType="0" shapeOffsetMapUnitScale="3x:0,0,0,0,0,0" shapeDraw="0" shapeBorderWidthUnit="MM" shapeFillColor="255,255,255,255,rgb:1,1,1,1" shapeOffsetUnit="MM" shapeRadiiUnit="MM" shapeSizeMapUnitScale="3x:0,0,0,0,0,0" shapeOffsetY="0" shapeBlendMode="0" shapeSizeType="0" shapeSizeY="0" shapeOpacity="1">
|
||||||
|
<symbol name="markerSymbol" type="marker" force_rhr="0" clip_to_extent="1" is_animated="0" frame_rate="10" alpha="1">
|
||||||
|
<data_defined_properties>
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="name" type="QString" value=""/>
|
||||||
|
<Option name="properties"/>
|
||||||
|
<Option name="type" type="QString" value="collection"/>
|
||||||
|
</Option>
|
||||||
|
</data_defined_properties>
|
||||||
|
<layer id="" class="SimpleMarker" locked="0" enabled="1" pass="0">
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="angle" type="QString" value="0"/>
|
||||||
|
<Option name="cap_style" type="QString" value="square"/>
|
||||||
|
<Option name="color" type="QString" value="141,90,153,255,rgb:0.55294117647058827,0.35294117647058826,0.59999999999999998,1"/>
|
||||||
|
<Option name="horizontal_anchor_point" type="QString" value="1"/>
|
||||||
|
<Option name="joinstyle" type="QString" value="bevel"/>
|
||||||
|
<Option name="name" type="QString" value="circle"/>
|
||||||
|
<Option name="offset" type="QString" value="0,0"/>
|
||||||
|
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="offset_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="outline_color" type="QString" value="35,35,35,255,rgb:0.13725490196078433,0.13725490196078433,0.13725490196078433,1"/>
|
||||||
|
<Option name="outline_style" type="QString" value="solid"/>
|
||||||
|
<Option name="outline_width" type="QString" value="0"/>
|
||||||
|
<Option name="outline_width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="outline_width_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="scale_method" type="QString" value="diameter"/>
|
||||||
|
<Option name="size" type="QString" value="2"/>
|
||||||
|
<Option name="size_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="size_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="vertical_anchor_point" type="QString" value="1"/>
|
||||||
|
</Option>
|
||||||
|
<data_defined_properties>
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="name" type="QString" value=""/>
|
||||||
|
<Option name="properties"/>
|
||||||
|
<Option name="type" type="QString" value="collection"/>
|
||||||
|
</Option>
|
||||||
|
</data_defined_properties>
|
||||||
|
</layer>
|
||||||
|
</symbol>
|
||||||
|
<symbol name="fillSymbol" type="fill" force_rhr="0" clip_to_extent="1" is_animated="0" frame_rate="10" alpha="1">
|
||||||
|
<data_defined_properties>
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="name" type="QString" value=""/>
|
||||||
|
<Option name="properties"/>
|
||||||
|
<Option name="type" type="QString" value="collection"/>
|
||||||
|
</Option>
|
||||||
|
</data_defined_properties>
|
||||||
|
<layer id="" class="SimpleFill" locked="0" enabled="1" pass="0">
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="border_width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="color" type="QString" value="255,255,255,255,rgb:1,1,1,1"/>
|
||||||
|
<Option name="joinstyle" type="QString" value="bevel"/>
|
||||||
|
<Option name="offset" type="QString" value="0,0"/>
|
||||||
|
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="offset_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="outline_color" type="QString" value="128,128,128,255,rgb:0.50196078431372548,0.50196078431372548,0.50196078431372548,1"/>
|
||||||
|
<Option name="outline_style" type="QString" value="no"/>
|
||||||
|
<Option name="outline_width" type="QString" value="0"/>
|
||||||
|
<Option name="outline_width_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="style" type="QString" value="solid"/>
|
||||||
|
</Option>
|
||||||
|
<data_defined_properties>
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="name" type="QString" value=""/>
|
||||||
|
<Option name="properties"/>
|
||||||
|
<Option name="type" type="QString" value="collection"/>
|
||||||
|
</Option>
|
||||||
|
</data_defined_properties>
|
||||||
|
</layer>
|
||||||
|
</symbol>
|
||||||
|
</background>
|
||||||
|
<shadow shadowRadiusAlphaOnly="0" shadowOffsetAngle="135" shadowOpacity="0.69999999999999996" shadowOffsetUnit="MM" shadowColor="0,0,0,255,rgb:0,0,0,1" shadowRadiusMapUnitScale="3x:0,0,0,0,0,0" shadowRadius="1.5" shadowBlendMode="6" shadowScale="100" shadowDraw="0" shadowUnder="0" shadowOffsetDist="1" shadowRadiusUnit="MM" shadowOffsetMapUnitScale="3x:0,0,0,0,0,0" shadowOffsetGlobal="1"/>
|
||||||
|
<dd_properties>
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="name" type="QString" value=""/>
|
||||||
|
<Option name="properties"/>
|
||||||
|
<Option name="type" type="QString" value="collection"/>
|
||||||
|
</Option>
|
||||||
|
</dd_properties>
|
||||||
|
<substitutions/>
|
||||||
|
</text-style>
|
||||||
|
<text-format multilineAlign="3" autoWrapLength="0" reverseDirectionSymbol="0" plussign="0" rightDirectionSymbol=">" addDirectionSymbol="0" leftDirectionSymbol="<" formatNumbers="0" decimals="3" placeDirectionSymbol="0" wrapChar="" useMaxLineLengthForAutoWrap="1"/>
|
||||||
|
<placement lineAnchorClipping="0" maximumDistanceMapUnitScale="3x:0,0,0,0,0,0" xOffset="0" maximumDistance="0" geometryGenerator="" distMapUnitScale="3x:0,0,0,0,0,0" predefinedPositionOrder="TR,TL,BR,BL,R,L,TSR,BSR" layerType="PolygonGeometry" lineAnchorPercent="0.5" rotationUnit="AngleDegrees" repeatDistanceMapUnitScale="3x:0,0,0,0,0,0" priority="9" yOffset="0" quadOffset="4" polygonPlacementFlags="2" centroidInside="1" placement="1" placementFlags="10" prioritization="PreferCloser" overrunDistance="0" offsetUnits="MM" maxCurvedCharAngleOut="-25" preserveRotation="1" lineAnchorTextPoint="CenterOfText" overlapHandling="PreventOverlap" rotationAngle="0" repeatDistance="0" offsetType="0" distUnits="MM" allowDegraded="0" overrunDistanceUnit="MM" centroidWhole="0" geometryGeneratorType="PointGeometry" fitInPolygonOnly="0" geometryGeneratorEnabled="0" dist="0" maxCurvedCharAngleIn="25" overrunDistanceMapUnitScale="3x:0,0,0,0,0,0" maximumDistanceUnit="MM" lineAnchorType="0" repeatDistanceUnits="MM" labelOffsetMapUnitScale="3x:0,0,0,0,0,0"/>
|
||||||
|
<rendering fontMinPixelSize="3" fontMaxPixelSize="10000" maxNumLabels="2000" obstacleFactor="1" mergeLines="0" fontLimitPixelSize="0" limitNumLabels="0" minFeatureSize="0" scaleMin="0" scaleVisibility="0" labelPerPart="0" scaleMax="0" obstacleType="1" upsidedownLabels="0" drawLabels="1" zIndex="0" unplacedVisibility="0" obstacle="1"/>
|
||||||
|
<dd_properties>
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="name" type="QString" value=""/>
|
||||||
|
<Option name="properties"/>
|
||||||
|
<Option name="type" type="QString" value="collection"/>
|
||||||
|
</Option>
|
||||||
|
</dd_properties>
|
||||||
|
<callout type="simple">
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="anchorPoint" type="QString" value="centroid"/>
|
||||||
|
<Option name="blendMode" type="int" value="0"/>
|
||||||
|
<Option name="ddProperties" type="Map">
|
||||||
|
<Option name="name" type="QString" value=""/>
|
||||||
|
<Option name="properties"/>
|
||||||
|
<Option name="type" type="QString" value="collection"/>
|
||||||
|
</Option>
|
||||||
|
<Option name="drawToAllParts" type="bool" value="false"/>
|
||||||
|
<Option name="enabled" type="QString" value="1"/>
|
||||||
|
<Option name="labelAnchorPoint" type="QString" value="point_on_exterior"/>
|
||||||
|
<Option name="lineSymbol" type="QString" value="<symbol name="symbol" type="line" force_rhr="0" clip_to_extent="1" is_animated="0" frame_rate="10" alpha="1"><data_defined_properties><Option type="Map"><Option name="name" type="QString" value=""/><Option name="properties"/><Option name="type" type="QString" value="collection"/></Option></data_defined_properties><layer id="{3356d22c-5f69-4911-8e91-fcf32e8243fa}" class="SimpleLine" locked="0" enabled="1" pass="0"><Option type="Map"><Option name="align_dash_pattern" type="QString" value="0"/><Option name="capstyle" type="QString" value="square"/><Option name="customdash" type="QString" value="5;2"/><Option name="customdash_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/><Option name="customdash_unit" type="QString" value="MM"/><Option name="dash_pattern_offset" type="QString" value="0"/><Option name="dash_pattern_offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/><Option name="dash_pattern_offset_unit" type="QString" value="MM"/><Option name="draw_inside_polygon" type="QString" value="0"/><Option name="joinstyle" type="QString" value="bevel"/><Option name="line_color" type="QString" value="60,60,60,255,rgb:0.23529411764705882,0.23529411764705882,0.23529411764705882,1"/><Option name="line_style" type="QString" value="solid"/><Option name="line_width" type="QString" value="0.3"/><Option name="line_width_unit" type="QString" value="MM"/><Option name="offset" type="QString" value="0"/><Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/><Option name="offset_unit" type="QString" value="MM"/><Option name="ring_filter" type="QString" value="0"/><Option name="trim_distance_end" type="QString" value="0"/><Option name="trim_distance_end_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/><Option name="trim_distance_end_unit" type="QString" value="MM"/><Option name="trim_distance_start" type="QString" value="0"/><Option name="trim_distance_start_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/><Option name="trim_distance_start_unit" type="QString" value="MM"/><Option name="tweak_dash_pattern_on_corners" type="QString" value="0"/><Option name="use_custom_dash" type="QString" value="0"/><Option name="width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/></Option><data_defined_properties><Option type="Map"><Option name="name" type="QString" value=""/><Option name="properties"/><Option name="type" type="QString" value="collection"/></Option></data_defined_properties></layer></symbol>"/>
|
||||||
|
<Option name="minLength" type="double" value="0"/>
|
||||||
|
<Option name="minLengthMapUnitScale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="minLengthUnit" type="QString" value="MM"/>
|
||||||
|
<Option name="offsetFromAnchor" type="double" value="0"/>
|
||||||
|
<Option name="offsetFromAnchorMapUnitScale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="offsetFromAnchorUnit" type="QString" value="MM"/>
|
||||||
|
<Option name="offsetFromLabel" type="double" value="0"/>
|
||||||
|
<Option name="offsetFromLabelMapUnitScale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="offsetFromLabelUnit" type="QString" value="MM"/>
|
||||||
|
</Option>
|
||||||
|
</callout>
|
||||||
|
</settings>
|
||||||
|
</rule>
|
||||||
|
<rule key="{a9800348-c55c-45d0-95ce-be6db84663c8}" filter=""flurstuecksnummer_ax_flurstuecksnummer_nenner" IS NOT NULL" description="Flst_num_Zusatz">
|
||||||
|
<settings calloutType="simple">
|
||||||
|
<text-style forcedItalic="0" fontUnderline="0" previewBkgrdColor="255,255,255,255,rgb:1,1,1,1" fontItalic="0" tabStopDistanceUnit="Point" textOpacity="1" multilineHeight="1" fontFamily="Times New Roman" fontStrikeout="0" capitalization="0" fieldName=""flurstuecksnummer_ax_flurstuecksnummer_zaehler" || '\n' || "flurstuecksnummer_ax_flurstuecksnummer_nenner" " legendString="Aa" fontSizeUnit="Point" fontWeight="50" textOrientation="horizontal" multilineHeightUnit="Percentage" fontKerning="1" tabStopDistanceMapUnitScale="3x:0,0,0,0,0,0" forcedBold="0" fontWordSpacing="0" fontLetterSpacing="0" fontSizeMapUnitScale="3x:0,0,0,0,0,0" allowHtml="0" isExpression="1" useSubstitutions="0" textColor="0,0,0,255,rgb:0,0,0,1" blendMode="0" fontSize="7" namedStyle="Standard" tabStopDistance="80">
|
||||||
|
<families/>
|
||||||
|
<text-buffer bufferBlendMode="0" bufferSizeUnits="MM" bufferOpacity="1" bufferJoinStyle="128" bufferNoFill="1" bufferSizeMapUnitScale="3x:0,0,0,0,0,0" bufferDraw="1" bufferColor="255,255,255,255,rgb:1,1,1,1" bufferSize="0.5"/>
|
||||||
|
<text-mask maskEnabled="0" maskSizeMapUnitScale="3x:0,0,0,0,0,0" maskSizeUnits="MM" maskJoinStyle="128" maskSize2="1.5" maskSize="1.5" maskOpacity="1" maskType="0" maskedSymbolLayers=""/>
|
||||||
|
<background shapeOffsetX="0" shapeRadiiY="0" shapeSVGFile="" shapeRotation="0" shapeBorderWidth="0" shapeRadiiX="0" shapeSizeX="4" shapeBorderWidthMapUnitScale="3x:0,0,0,0,0,0" shapeSizeUnit="MM" shapeRotationType="0" shapeBorderColor="128,128,128,255,rgb:0.50196078431372548,0.50196078431372548,0.50196078431372548,1" shapeJoinStyle="64" shapeRadiiMapUnitScale="3x:0,0,0,0,0,0" shapeType="5" shapeOffsetMapUnitScale="3x:0,0,0,0,0,0" shapeDraw="1" shapeBorderWidthUnit="MM" shapeFillColor="255,255,255,255,rgb:1,1,1,1" shapeOffsetUnit="MM" shapeRadiiUnit="MM" shapeSizeMapUnitScale="3x:0,0,0,0,0,0" shapeOffsetY="0" shapeBlendMode="0" shapeSizeType="1" shapeSizeY="0" shapeOpacity="1">
|
||||||
|
<symbol name="markerSymbol" type="marker" force_rhr="0" clip_to_extent="1" is_animated="0" frame_rate="10" alpha="1">
|
||||||
|
<data_defined_properties>
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="name" type="QString" value=""/>
|
||||||
|
<Option name="properties"/>
|
||||||
|
<Option name="type" type="QString" value="collection"/>
|
||||||
|
</Option>
|
||||||
|
</data_defined_properties>
|
||||||
|
<layer id="" class="SimpleMarker" locked="0" enabled="1" pass="0">
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="angle" type="QString" value="90"/>
|
||||||
|
<Option name="cap_style" type="QString" value="square"/>
|
||||||
|
<Option name="color" type="QString" value="255,158,23,255,rgb:1,0.61960784313725492,0.09019607843137255,1"/>
|
||||||
|
<Option name="horizontal_anchor_point" type="QString" value="1"/>
|
||||||
|
<Option name="joinstyle" type="QString" value="bevel"/>
|
||||||
|
<Option name="name" type="QString" value="line"/>
|
||||||
|
<Option name="offset" type="QString" value="0,0"/>
|
||||||
|
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="offset_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="outline_color" type="QString" value="35,35,35,255,rgb:0.13725490196078433,0.13725490196078433,0.13725490196078433,1"/>
|
||||||
|
<Option name="outline_style" type="QString" value="solid"/>
|
||||||
|
<Option name="outline_width" type="QString" value="0.3"/>
|
||||||
|
<Option name="outline_width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="outline_width_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="scale_method" type="QString" value="diameter"/>
|
||||||
|
<Option name="size" type="QString" value="4"/>
|
||||||
|
<Option name="size_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="size_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="vertical_anchor_point" type="QString" value="1"/>
|
||||||
|
</Option>
|
||||||
|
<data_defined_properties>
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="name" type="QString" value=""/>
|
||||||
|
<Option name="properties"/>
|
||||||
|
<Option name="type" type="QString" value="collection"/>
|
||||||
|
</Option>
|
||||||
|
</data_defined_properties>
|
||||||
|
</layer>
|
||||||
|
</symbol>
|
||||||
|
<symbol name="fillSymbol" type="fill" force_rhr="0" clip_to_extent="1" is_animated="0" frame_rate="10" alpha="1">
|
||||||
|
<data_defined_properties>
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="name" type="QString" value=""/>
|
||||||
|
<Option name="properties"/>
|
||||||
|
<Option name="type" type="QString" value="collection"/>
|
||||||
|
</Option>
|
||||||
|
</data_defined_properties>
|
||||||
|
<layer id="" class="SimpleFill" locked="0" enabled="1" pass="0">
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="border_width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="color" type="QString" value="255,255,255,255,rgb:1,1,1,1"/>
|
||||||
|
<Option name="joinstyle" type="QString" value="bevel"/>
|
||||||
|
<Option name="offset" type="QString" value="0,0"/>
|
||||||
|
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="offset_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="outline_color" type="QString" value="128,128,128,255,rgb:0.50196078431372548,0.50196078431372548,0.50196078431372548,1"/>
|
||||||
|
<Option name="outline_style" type="QString" value="no"/>
|
||||||
|
<Option name="outline_width" type="QString" value="0"/>
|
||||||
|
<Option name="outline_width_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="style" type="QString" value="solid"/>
|
||||||
|
</Option>
|
||||||
|
<data_defined_properties>
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="name" type="QString" value=""/>
|
||||||
|
<Option name="properties"/>
|
||||||
|
<Option name="type" type="QString" value="collection"/>
|
||||||
|
</Option>
|
||||||
|
</data_defined_properties>
|
||||||
|
</layer>
|
||||||
|
</symbol>
|
||||||
|
</background>
|
||||||
|
<shadow shadowRadiusAlphaOnly="0" shadowOffsetAngle="135" shadowOpacity="0.69999999999999996" shadowOffsetUnit="MM" shadowColor="0,0,0,255,rgb:0,0,0,1" shadowRadiusMapUnitScale="3x:0,0,0,0,0,0" shadowRadius="1.5" shadowBlendMode="6" shadowScale="100" shadowDraw="0" shadowUnder="0" shadowOffsetDist="1" shadowRadiusUnit="MM" shadowOffsetMapUnitScale="3x:0,0,0,0,0,0" shadowOffsetGlobal="1"/>
|
||||||
|
<dd_properties>
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="name" type="QString" value=""/>
|
||||||
|
<Option name="properties"/>
|
||||||
|
<Option name="type" type="QString" value="collection"/>
|
||||||
|
</Option>
|
||||||
|
</dd_properties>
|
||||||
|
<substitutions/>
|
||||||
|
</text-style>
|
||||||
|
<text-format multilineAlign="3" autoWrapLength="0" reverseDirectionSymbol="0" plussign="0" rightDirectionSymbol=">" addDirectionSymbol="0" leftDirectionSymbol="<" formatNumbers="0" decimals="3" placeDirectionSymbol="0" wrapChar="" useMaxLineLengthForAutoWrap="1"/>
|
||||||
|
<placement lineAnchorClipping="0" maximumDistanceMapUnitScale="3x:0,0,0,0,0,0" xOffset="0" maximumDistance="0" geometryGenerator="" distMapUnitScale="3x:0,0,0,0,0,0" predefinedPositionOrder="TR,TL,BR,BL,R,L,TSR,BSR" layerType="PolygonGeometry" lineAnchorPercent="0.5" rotationUnit="AngleDegrees" repeatDistanceMapUnitScale="3x:0,0,0,0,0,0" priority="5" yOffset="0" quadOffset="4" polygonPlacementFlags="2" centroidInside="1" placement="1" placementFlags="10" prioritization="PreferCloser" overrunDistance="0" offsetUnits="MM" maxCurvedCharAngleOut="-25" preserveRotation="1" lineAnchorTextPoint="CenterOfText" overlapHandling="PreventOverlap" rotationAngle="0" repeatDistance="0" offsetType="0" distUnits="MM" allowDegraded="0" overrunDistanceUnit="MM" centroidWhole="0" geometryGeneratorType="PointGeometry" fitInPolygonOnly="0" geometryGeneratorEnabled="0" dist="0" maxCurvedCharAngleIn="25" overrunDistanceMapUnitScale="3x:0,0,0,0,0,0" maximumDistanceUnit="MM" lineAnchorType="0" repeatDistanceUnits="MM" labelOffsetMapUnitScale="3x:0,0,0,0,0,0"/>
|
||||||
|
<rendering fontMinPixelSize="3" fontMaxPixelSize="10000" maxNumLabels="2000" obstacleFactor="1" mergeLines="0" fontLimitPixelSize="0" limitNumLabels="0" minFeatureSize="0" scaleMin="0" scaleVisibility="0" labelPerPart="0" scaleMax="0" obstacleType="1" upsidedownLabels="0" drawLabels="1" zIndex="0" unplacedVisibility="0" obstacle="1"/>
|
||||||
|
<dd_properties>
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="name" type="QString" value=""/>
|
||||||
|
<Option name="properties" type="Map">
|
||||||
|
<Option name="FontStyle" type="Map">
|
||||||
|
<Option name="active" type="bool" value="false"/>
|
||||||
|
<Option name="field" type="QString" value="flstnrzae"/>
|
||||||
|
<Option name="type" type="int" value="2"/>
|
||||||
|
</Option>
|
||||||
|
<Option name="Underline" type="Map">
|
||||||
|
<Option name="active" type="bool" value="false"/>
|
||||||
|
<Option name="field" type="QString" value="flstnrzae"/>
|
||||||
|
<Option name="type" type="int" value="2"/>
|
||||||
|
</Option>
|
||||||
|
</Option>
|
||||||
|
<Option name="type" type="QString" value="collection"/>
|
||||||
|
</Option>
|
||||||
|
</dd_properties>
|
||||||
|
<callout type="simple">
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="anchorPoint" type="QString" value="pole_of_inaccessibility"/>
|
||||||
|
<Option name="blendMode" type="int" value="0"/>
|
||||||
|
<Option name="ddProperties" type="Map">
|
||||||
|
<Option name="name" type="QString" value=""/>
|
||||||
|
<Option name="properties"/>
|
||||||
|
<Option name="type" type="QString" value="collection"/>
|
||||||
|
</Option>
|
||||||
|
<Option name="drawToAllParts" type="bool" value="false"/>
|
||||||
|
<Option name="enabled" type="QString" value="0"/>
|
||||||
|
<Option name="labelAnchorPoint" type="QString" value="point_on_exterior"/>
|
||||||
|
<Option name="lineSymbol" type="QString" value="<symbol name="symbol" type="line" force_rhr="0" clip_to_extent="1" is_animated="0" frame_rate="10" alpha="1"><data_defined_properties><Option type="Map"><Option name="name" type="QString" value=""/><Option name="properties"/><Option name="type" type="QString" value="collection"/></Option></data_defined_properties><layer id="{0a6da4bc-e8f1-4ec2-8062-844ead072d33}" class="SimpleLine" locked="0" enabled="1" pass="0"><Option type="Map"><Option name="align_dash_pattern" type="QString" value="0"/><Option name="capstyle" type="QString" value="square"/><Option name="customdash" type="QString" value="5;2"/><Option name="customdash_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/><Option name="customdash_unit" type="QString" value="MM"/><Option name="dash_pattern_offset" type="QString" value="0"/><Option name="dash_pattern_offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/><Option name="dash_pattern_offset_unit" type="QString" value="MM"/><Option name="draw_inside_polygon" type="QString" value="0"/><Option name="joinstyle" type="QString" value="bevel"/><Option name="line_color" type="QString" value="60,60,60,255,rgb:0.23529411764705882,0.23529411764705882,0.23529411764705882,1"/><Option name="line_style" type="QString" value="solid"/><Option name="line_width" type="QString" value="0.3"/><Option name="line_width_unit" type="QString" value="MM"/><Option name="offset" type="QString" value="0"/><Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/><Option name="offset_unit" type="QString" value="MM"/><Option name="ring_filter" type="QString" value="0"/><Option name="trim_distance_end" type="QString" value="0"/><Option name="trim_distance_end_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/><Option name="trim_distance_end_unit" type="QString" value="MM"/><Option name="trim_distance_start" type="QString" value="0"/><Option name="trim_distance_start_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/><Option name="trim_distance_start_unit" type="QString" value="MM"/><Option name="tweak_dash_pattern_on_corners" type="QString" value="0"/><Option name="use_custom_dash" type="QString" value="0"/><Option name="width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/></Option><data_defined_properties><Option type="Map"><Option name="name" type="QString" value=""/><Option name="properties"/><Option name="type" type="QString" value="collection"/></Option></data_defined_properties></layer></symbol>"/>
|
||||||
|
<Option name="minLength" type="double" value="0"/>
|
||||||
|
<Option name="minLengthMapUnitScale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="minLengthUnit" type="QString" value="MM"/>
|
||||||
|
<Option name="offsetFromAnchor" type="double" value="0"/>
|
||||||
|
<Option name="offsetFromAnchorMapUnitScale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="offsetFromAnchorUnit" type="QString" value="MM"/>
|
||||||
|
<Option name="offsetFromLabel" type="double" value="0"/>
|
||||||
|
<Option name="offsetFromLabelMapUnitScale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="offsetFromLabelUnit" type="QString" value="MM"/>
|
||||||
|
</Option>
|
||||||
|
</callout>
|
||||||
|
</settings>
|
||||||
|
</rule>
|
||||||
|
</rules>
|
||||||
|
</labeling>
|
||||||
|
<blendMode>0</blendMode>
|
||||||
|
<featureBlendMode>0</featureBlendMode>
|
||||||
|
<layerGeometryType>2</layerGeometryType>
|
||||||
|
</qgis>
|
||||||
+112
-103
@@ -1,133 +1,142 @@
|
|||||||
<!DOCTYPE qgis PUBLIC 'http://mrcc.com/qgis.dtd' 'SYSTEM'>
|
<!DOCTYPE qgis PUBLIC 'http://mrcc.com/qgis.dtd' 'SYSTEM'>
|
||||||
<qgis version="3.40.7-Bratislava" styleCategories="Symbology">
|
<qgis layerType="Vector" styleCategories="Symbology" version="4.0.1-Norrköping">
|
||||||
<renderer-v2 referencescale="-1" forceraster="0" enableorderby="0" type="singleSymbol" symbollevels="0">
|
<renderer-v2 enableorderby="0" forceraster="0" referencescale="-1" symbollevels="0" type="mergedFeatureRenderer">
|
||||||
<symbols>
|
<renderer-v2 enableorderby="0" forceraster="0" referencescale="-1" symbollevels="0" type="singleSymbol">
|
||||||
<symbol is_animated="0" frame_rate="10" clip_to_extent="1" type="fill" alpha="1" force_rhr="0" name="0">
|
<symbols>
|
||||||
<data_defined_properties>
|
<symbol alpha="1" clip_to_extent="1" force_rhr="0" frame_rate="10" is_animated="0" name="0" type="fill">
|
||||||
<Option type="Map">
|
|
||||||
<Option type="QString" value="" name="name"/>
|
|
||||||
<Option name="properties"/>
|
|
||||||
<Option type="QString" value="collection" name="type"/>
|
|
||||||
</Option>
|
|
||||||
</data_defined_properties>
|
|
||||||
<layer locked="0" id="{feca00b2-500a-4c9a-b285-67ba2d99d8f6}" enabled="1" class="SimpleLine" pass="0">
|
|
||||||
<Option type="Map">
|
|
||||||
<Option type="QString" value="0" name="align_dash_pattern"/>
|
|
||||||
<Option type="QString" value="square" name="capstyle"/>
|
|
||||||
<Option type="QString" value="5;2" name="customdash"/>
|
|
||||||
<Option type="QString" value="3x:0,0,0,0,0,0" name="customdash_map_unit_scale"/>
|
|
||||||
<Option type="QString" value="MM" name="customdash_unit"/>
|
|
||||||
<Option type="QString" value="0" name="dash_pattern_offset"/>
|
|
||||||
<Option type="QString" value="3x:0,0,0,0,0,0" name="dash_pattern_offset_map_unit_scale"/>
|
|
||||||
<Option type="QString" value="MM" name="dash_pattern_offset_unit"/>
|
|
||||||
<Option type="QString" value="0" name="draw_inside_polygon"/>
|
|
||||||
<Option type="QString" value="round" name="joinstyle"/>
|
|
||||||
<Option type="QString" value="215,168,255,255,rgb:0.84313725490196079,0.6588235294117647,1,1" name="line_color"/>
|
|
||||||
<Option type="QString" value="solid" name="line_style"/>
|
|
||||||
<Option type="QString" value="1.5" name="line_width"/>
|
|
||||||
<Option type="QString" value="MM" name="line_width_unit"/>
|
|
||||||
<Option type="QString" value="-0.75" name="offset"/>
|
|
||||||
<Option type="QString" value="3x:0,0,0,0,0,0" name="offset_map_unit_scale"/>
|
|
||||||
<Option type="QString" value="MM" name="offset_unit"/>
|
|
||||||
<Option type="QString" value="0" name="ring_filter"/>
|
|
||||||
<Option type="QString" value="0" name="trim_distance_end"/>
|
|
||||||
<Option type="QString" value="3x:0,0,0,0,0,0" name="trim_distance_end_map_unit_scale"/>
|
|
||||||
<Option type="QString" value="MM" name="trim_distance_end_unit"/>
|
|
||||||
<Option type="QString" value="0" name="trim_distance_start"/>
|
|
||||||
<Option type="QString" value="3x:0,0,0,0,0,0" name="trim_distance_start_map_unit_scale"/>
|
|
||||||
<Option type="QString" value="MM" name="trim_distance_start_unit"/>
|
|
||||||
<Option type="QString" value="0" name="tweak_dash_pattern_on_corners"/>
|
|
||||||
<Option type="QString" value="0" name="use_custom_dash"/>
|
|
||||||
<Option type="QString" value="3x:0,0,0,0,0,0" name="width_map_unit_scale"/>
|
|
||||||
</Option>
|
|
||||||
<data_defined_properties>
|
<data_defined_properties>
|
||||||
<Option type="Map">
|
<Option type="Map">
|
||||||
<Option type="QString" value="" name="name"/>
|
<Option name="name" type="QString" value=""/>
|
||||||
<Option name="properties"/>
|
<Option name="properties"/>
|
||||||
<Option type="QString" value="collection" name="type"/>
|
<Option name="type" type="QString" value="collection"/>
|
||||||
</Option>
|
</Option>
|
||||||
</data_defined_properties>
|
</data_defined_properties>
|
||||||
</layer>
|
<layer class="SimpleLine" enabled="1" id="{feca00b2-500a-4c9a-b285-67ba2d99d8f6}" locked="0" pass="0">
|
||||||
<layer locked="0" id="{fdc4d6fd-0995-41df-bbfb-19970b4fc2cc}" enabled="1" class="SimpleLine" pass="0">
|
|
||||||
<Option type="Map">
|
|
||||||
<Option type="QString" value="0" name="align_dash_pattern"/>
|
|
||||||
<Option type="QString" value="square" name="capstyle"/>
|
|
||||||
<Option type="QString" value="5;2" name="customdash"/>
|
|
||||||
<Option type="QString" value="3x:0,0,0,0,0,0" name="customdash_map_unit_scale"/>
|
|
||||||
<Option type="QString" value="MM" name="customdash_unit"/>
|
|
||||||
<Option type="QString" value="0" name="dash_pattern_offset"/>
|
|
||||||
<Option type="QString" value="3x:0,0,0,0,0,0" name="dash_pattern_offset_map_unit_scale"/>
|
|
||||||
<Option type="QString" value="MM" name="dash_pattern_offset_unit"/>
|
|
||||||
<Option type="QString" value="0" name="draw_inside_polygon"/>
|
|
||||||
<Option type="QString" value="round" name="joinstyle"/>
|
|
||||||
<Option type="QString" value="204,174,137,255,rgb:0.80000000000000004,0.68235294117647061,0.53725490196078429,1" name="line_color"/>
|
|
||||||
<Option type="QString" value="solid" name="line_style"/>
|
|
||||||
<Option type="QString" value="0.5" name="line_width"/>
|
|
||||||
<Option type="QString" value="MM" name="line_width_unit"/>
|
|
||||||
<Option type="QString" value="0" name="offset"/>
|
|
||||||
<Option type="QString" value="3x:0,0,0,0,0,0" name="offset_map_unit_scale"/>
|
|
||||||
<Option type="QString" value="MM" name="offset_unit"/>
|
|
||||||
<Option type="QString" value="0" name="ring_filter"/>
|
|
||||||
<Option type="QString" value="0" name="trim_distance_end"/>
|
|
||||||
<Option type="QString" value="3x:0,0,0,0,0,0" name="trim_distance_end_map_unit_scale"/>
|
|
||||||
<Option type="QString" value="MM" name="trim_distance_end_unit"/>
|
|
||||||
<Option type="QString" value="0" name="trim_distance_start"/>
|
|
||||||
<Option type="QString" value="3x:0,0,0,0,0,0" name="trim_distance_start_map_unit_scale"/>
|
|
||||||
<Option type="QString" value="MM" name="trim_distance_start_unit"/>
|
|
||||||
<Option type="QString" value="0" name="tweak_dash_pattern_on_corners"/>
|
|
||||||
<Option type="QString" value="0" name="use_custom_dash"/>
|
|
||||||
<Option type="QString" value="3x:0,0,0,0,0,0" name="width_map_unit_scale"/>
|
|
||||||
</Option>
|
|
||||||
<data_defined_properties>
|
|
||||||
<Option type="Map">
|
<Option type="Map">
|
||||||
<Option type="QString" value="" name="name"/>
|
<Option name="align_dash_pattern" type="QString" value="0"/>
|
||||||
<Option name="properties"/>
|
<Option name="capstyle" type="QString" value="square"/>
|
||||||
<Option type="QString" value="collection" name="type"/>
|
<Option name="customdash" type="QString" value="5;2"/>
|
||||||
|
<Option name="customdash_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="customdash_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="dash_pattern_offset" type="QString" value="0"/>
|
||||||
|
<Option name="dash_pattern_offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="dash_pattern_offset_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="draw_inside_polygon" type="QString" value="0"/>
|
||||||
|
<Option name="joinstyle" type="QString" value="round"/>
|
||||||
|
<Option name="line_color" type="QString" value="215,168,255,255,rgb:0.8431373,0.6588235,1,1"/>
|
||||||
|
<Option name="line_style" type="QString" value="solid"/>
|
||||||
|
<Option name="line_width" type="QString" value="1.5"/>
|
||||||
|
<Option name="line_width_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="offset" type="QString" value="-0.75"/>
|
||||||
|
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="offset_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="ring_filter" type="QString" value="0"/>
|
||||||
|
<Option name="trim_distance_end" type="QString" value="0"/>
|
||||||
|
<Option name="trim_distance_end_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="trim_distance_end_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="trim_distance_start" type="QString" value="0"/>
|
||||||
|
<Option name="trim_distance_start_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="trim_distance_start_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="tweak_dash_pattern_on_corners" type="QString" value="0"/>
|
||||||
|
<Option name="use_custom_dash" type="QString" value="0"/>
|
||||||
|
<Option name="width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
</Option>
|
</Option>
|
||||||
</data_defined_properties>
|
<data_defined_properties>
|
||||||
</layer>
|
<Option type="Map">
|
||||||
</symbol>
|
<Option name="name" type="QString" value=""/>
|
||||||
</symbols>
|
<Option name="properties"/>
|
||||||
<rotation/>
|
<Option name="type" type="QString" value="collection"/>
|
||||||
<sizescale/>
|
</Option>
|
||||||
|
</data_defined_properties>
|
||||||
|
</layer>
|
||||||
|
<layer class="SimpleLine" enabled="1" id="{fdc4d6fd-0995-41df-bbfb-19970b4fc2cc}" locked="0" pass="0">
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="align_dash_pattern" type="QString" value="0"/>
|
||||||
|
<Option name="capstyle" type="QString" value="square"/>
|
||||||
|
<Option name="customdash" type="QString" value="5;2"/>
|
||||||
|
<Option name="customdash_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="customdash_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="dash_pattern_offset" type="QString" value="0"/>
|
||||||
|
<Option name="dash_pattern_offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="dash_pattern_offset_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="draw_inside_polygon" type="QString" value="0"/>
|
||||||
|
<Option name="joinstyle" type="QString" value="round"/>
|
||||||
|
<Option name="line_color" type="QString" value="204,174,137,255,rgb:0.8,0.682353,0.5372549,1"/>
|
||||||
|
<Option name="line_style" type="QString" value="solid"/>
|
||||||
|
<Option name="line_width" type="QString" value="0.5"/>
|
||||||
|
<Option name="line_width_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="offset" type="QString" value="0"/>
|
||||||
|
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="offset_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="ring_filter" type="QString" value="0"/>
|
||||||
|
<Option name="trim_distance_end" type="QString" value="0"/>
|
||||||
|
<Option name="trim_distance_end_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="trim_distance_end_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="trim_distance_start" type="QString" value="0"/>
|
||||||
|
<Option name="trim_distance_start_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
<Option name="trim_distance_start_unit" type="QString" value="MM"/>
|
||||||
|
<Option name="tweak_dash_pattern_on_corners" type="QString" value="0"/>
|
||||||
|
<Option name="use_custom_dash" type="QString" value="0"/>
|
||||||
|
<Option name="width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
|
</Option>
|
||||||
|
<data_defined_properties>
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="name" type="QString" value=""/>
|
||||||
|
<Option name="properties"/>
|
||||||
|
<Option name="type" type="QString" value="collection"/>
|
||||||
|
</Option>
|
||||||
|
</data_defined_properties>
|
||||||
|
</layer>
|
||||||
|
</symbol>
|
||||||
|
</symbols>
|
||||||
|
<rotation/>
|
||||||
|
<sizescale/>
|
||||||
|
<data-defined-properties>
|
||||||
|
<Option type="Map">
|
||||||
|
<Option name="name" type="QString" value=""/>
|
||||||
|
<Option name="properties"/>
|
||||||
|
<Option name="type" type="QString" value="collection"/>
|
||||||
|
</Option>
|
||||||
|
</data-defined-properties>
|
||||||
|
</renderer-v2>
|
||||||
<data-defined-properties>
|
<data-defined-properties>
|
||||||
<Option type="Map">
|
<Option type="Map">
|
||||||
<Option type="QString" value="" name="name"/>
|
<Option name="name" type="QString" value=""/>
|
||||||
<Option name="properties"/>
|
<Option name="properties"/>
|
||||||
<Option type="QString" value="collection" name="type"/>
|
<Option name="type" type="QString" value="collection"/>
|
||||||
</Option>
|
</Option>
|
||||||
</data-defined-properties>
|
</data-defined-properties>
|
||||||
</renderer-v2>
|
</renderer-v2>
|
||||||
<selection mode="Default">
|
<selection mode="Default">
|
||||||
<selectionColor invalid="1"/>
|
<selectionColor invalid="1"/>
|
||||||
<selectionSymbol>
|
<selectionSymbol>
|
||||||
<symbol is_animated="0" frame_rate="10" clip_to_extent="1" type="fill" alpha="1" force_rhr="0" name="">
|
<symbol alpha="1" clip_to_extent="1" force_rhr="0" frame_rate="10" is_animated="0" name="" type="fill">
|
||||||
<data_defined_properties>
|
<data_defined_properties>
|
||||||
<Option type="Map">
|
<Option type="Map">
|
||||||
<Option type="QString" value="" name="name"/>
|
<Option name="name" type="QString" value=""/>
|
||||||
<Option name="properties"/>
|
<Option name="properties"/>
|
||||||
<Option type="QString" value="collection" name="type"/>
|
<Option name="type" type="QString" value="collection"/>
|
||||||
</Option>
|
</Option>
|
||||||
</data_defined_properties>
|
</data_defined_properties>
|
||||||
<layer locked="0" id="{f18003f5-220a-487f-8a6d-b24facc4c1a5}" enabled="1" class="SimpleFill" pass="0">
|
<layer class="SimpleFill" enabled="1" id="{f18003f5-220a-487f-8a6d-b24facc4c1a5}" locked="0" pass="0">
|
||||||
<Option type="Map">
|
<Option type="Map">
|
||||||
<Option type="QString" value="3x:0,0,0,0,0,0" name="border_width_map_unit_scale"/>
|
<Option name="border_width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
<Option type="QString" value="0,0,255,255,rgb:0,0,1,1" name="color"/>
|
<Option name="color" type="QString" value="0,0,255,255,rgb:0,0,1,1"/>
|
||||||
<Option type="QString" value="bevel" name="joinstyle"/>
|
<Option name="joinstyle" type="QString" value="bevel"/>
|
||||||
<Option type="QString" value="0,0" name="offset"/>
|
<Option name="offset" type="QString" value="0,0"/>
|
||||||
<Option type="QString" value="3x:0,0,0,0,0,0" name="offset_map_unit_scale"/>
|
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||||
<Option type="QString" value="MM" name="offset_unit"/>
|
<Option name="offset_unit" type="QString" value="MM"/>
|
||||||
<Option type="QString" value="35,35,35,255,rgb:0.13725490196078433,0.13725490196078433,0.13725490196078433,1" name="outline_color"/>
|
<Option name="outline_color" type="QString" value="35,35,35,255,rgb:0.1372549,0.1372549,0.1372549,1"/>
|
||||||
<Option type="QString" value="solid" name="outline_style"/>
|
<Option name="outline_style" type="QString" value="solid"/>
|
||||||
<Option type="QString" value="0.26" name="outline_width"/>
|
<Option name="outline_width" type="QString" value="0.26"/>
|
||||||
<Option type="QString" value="MM" name="outline_width_unit"/>
|
<Option name="outline_width_unit" type="QString" value="MM"/>
|
||||||
<Option type="QString" value="solid" name="style"/>
|
<Option name="style" type="QString" value="solid"/>
|
||||||
</Option>
|
</Option>
|
||||||
<data_defined_properties>
|
<data_defined_properties>
|
||||||
<Option type="Map">
|
<Option type="Map">
|
||||||
<Option type="QString" value="" name="name"/>
|
<Option name="name" type="QString" value=""/>
|
||||||
<Option name="properties"/>
|
<Option name="properties"/>
|
||||||
<Option type="QString" value="collection" name="type"/>
|
<Option name="type" type="QString" value="collection"/>
|
||||||
</Option>
|
</Option>
|
||||||
</data_defined_properties>
|
</data_defined_properties>
|
||||||
</layer>
|
</layer>
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
"""Tests für das Plugin sn_verfahrensgebiet."""
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import sys
|
||||||
|
import types
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
if "qgis" not in sys.modules:
|
||||||
|
qgis_mod = types.ModuleType("qgis")
|
||||||
|
qgis_utils = types.ModuleType("qgis.utils")
|
||||||
|
qgis_utils.iface = types.SimpleNamespace()
|
||||||
|
qgis_utils.plugins = {}
|
||||||
|
qgis_mod.utils = qgis_utils
|
||||||
|
sys.modules["qgis"] = qgis_mod
|
||||||
|
sys.modules["qgis.utils"] = qgis_utils
|
||||||
|
|
||||||
|
from sn_verfahrensgebiet.ui.dockwidget import DockWidget
|
||||||
|
|
||||||
|
|
||||||
|
class _DummyTabWidget:
|
||||||
|
def __init__(self):
|
||||||
|
self.tabs = []
|
||||||
|
|
||||||
|
def addTab(self, widget, title):
|
||||||
|
self.tabs.append((widget, title))
|
||||||
|
|
||||||
|
|
||||||
|
class _TabWithServices:
|
||||||
|
tab_title = "WithServices"
|
||||||
|
|
||||||
|
def __init__(self, _parent):
|
||||||
|
self.services = None
|
||||||
|
|
||||||
|
def set_services(self, pruefmanager=None, data_grabber=None):
|
||||||
|
self.services = (pruefmanager, data_grabber)
|
||||||
|
|
||||||
|
|
||||||
|
class _TabWithoutServices:
|
||||||
|
tab_title = "WithoutServices"
|
||||||
|
|
||||||
|
def __init__(self, _parent):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class _TabRaises:
|
||||||
|
tab_title = "Broken"
|
||||||
|
|
||||||
|
def __init__(self, _parent):
|
||||||
|
raise RuntimeError("boom")
|
||||||
|
|
||||||
|
|
||||||
|
class TestDockWidget(unittest.TestCase):
|
||||||
|
|
||||||
|
@patch("sn_verfahrensgebiet.ui.dockwidget.QTabWidget", _DummyTabWidget)
|
||||||
|
def test_init_tabs_with_services_injects_available_services(self):
|
||||||
|
obj = DockWidget.__new__(DockWidget)
|
||||||
|
obj.tabs = [_TabWithServices, _TabWithoutServices]
|
||||||
|
obj.pruefmanager = object()
|
||||||
|
obj.data_grabber = object()
|
||||||
|
obj._set_widget_calls = []
|
||||||
|
obj.setWidget = lambda value: obj._set_widget_calls.append(value)
|
||||||
|
|
||||||
|
DockWidget._init_tabs_with_services(obj)
|
||||||
|
|
||||||
|
self.assertEqual(len(obj._set_widget_calls), 2)
|
||||||
|
tab_widget = obj._set_widget_calls[-1]
|
||||||
|
self.assertIsInstance(tab_widget, _DummyTabWidget)
|
||||||
|
self.assertEqual(len(tab_widget.tabs), 2)
|
||||||
|
|
||||||
|
first_tab = tab_widget.tabs[0][0]
|
||||||
|
self.assertEqual(first_tab.services, (obj.pruefmanager, obj.data_grabber))
|
||||||
|
|
||||||
|
@patch("sn_verfahrensgebiet.ui.dockwidget.error")
|
||||||
|
@patch("sn_verfahrensgebiet.ui.dockwidget.QTabWidget", _DummyTabWidget)
|
||||||
|
def test_init_tabs_with_services_reports_errors(self, mock_error):
|
||||||
|
obj = DockWidget.__new__(DockWidget)
|
||||||
|
obj.tabs = [_TabRaises]
|
||||||
|
obj.pruefmanager = object()
|
||||||
|
obj.data_grabber = object()
|
||||||
|
obj.setWidget = lambda _value: None
|
||||||
|
|
||||||
|
DockWidget._init_tabs_with_services(obj)
|
||||||
|
|
||||||
|
mock_error.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,312 @@
|
|||||||
|
"""sn_verfahrensgebiet/tests/test_flurstueck_alkis.py
|
||||||
|
|
||||||
|
Unit-Tests für das flurstueck_alkis-Modul.
|
||||||
|
|
||||||
|
Getestet werden:
|
||||||
|
- Nutzerabbruch bei ask_text → (None, LoadStatus.NONE)
|
||||||
|
- Ungültiger WFS-Layer → (None, LoadStatus.NONE)
|
||||||
|
- Erfolgreicher Ladevorgang → gültiger Layer + LoadStatus.FIRST
|
||||||
|
- Bestehender Layer: Nutzer wählt "Neu laden" → LoadStatus.RELOAD
|
||||||
|
- Bestehender Layer: Nutzer wählt "Behalten" → LoadStatus.KEEP
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import types
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import patch, MagicMock
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 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.flurstueck_alkis import flurstueck_alkis, FLST_LAYER_NAME
|
||||||
|
from sn_verfahrensgebiet.functions.verfahrensgebiet_alkis import LoadStatus
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Dummies
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class _DummyLayer:
|
||||||
|
def __init__(self, valid: bool, layer_id="L1"):
|
||||||
|
self._valid = valid
|
||||||
|
self._id = layer_id
|
||||||
|
|
||||||
|
def isValid(self):
|
||||||
|
return self._valid
|
||||||
|
|
||||||
|
def id(self):
|
||||||
|
return self._id
|
||||||
|
|
||||||
|
def extent(self):
|
||||||
|
return object()
|
||||||
|
|
||||||
|
|
||||||
|
class _DummyProject:
|
||||||
|
def __init__(self, named_layers=None):
|
||||||
|
self._named_layers = named_layers or []
|
||||||
|
self.removed = []
|
||||||
|
|
||||||
|
def mapLayersByName(self, name):
|
||||||
|
return self._named_layers
|
||||||
|
|
||||||
|
def removeMapLayer(self, lid):
|
||||||
|
self.removed.append(lid)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Tests
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class TestFlurstueckAlkis(unittest.TestCase):
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# 1. Nutzerabbruch (ask_text liefert ok=False / None) #
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QMessageBox")
|
||||||
|
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance")
|
||||||
|
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text")
|
||||||
|
def test_user_cancel_returns_none(self, mock_ask_text, mock_project, mock_msgbox):
|
||||||
|
"""ask_text → ok=False, text="" → (None, NONE)."""
|
||||||
|
mock_ask_text.return_value = ("", False)
|
||||||
|
mock_project.return_value = _DummyProject()
|
||||||
|
|
||||||
|
layer, status = flurstueck_alkis(None)
|
||||||
|
|
||||||
|
self.assertIsNone(layer)
|
||||||
|
self.assertEqual(status, LoadStatus.NONE)
|
||||||
|
|
||||||
|
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QMessageBox")
|
||||||
|
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance")
|
||||||
|
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text")
|
||||||
|
def test_empty_text_after_confirmation_returns_none(self, mock_ask_text, mock_project, mock_msgbox):
|
||||||
|
"""ask_text → ok=True, aber Text leer → (None, NONE)."""
|
||||||
|
mock_ask_text.return_value = (" ", True)
|
||||||
|
mock_project.return_value = _DummyProject()
|
||||||
|
|
||||||
|
layer, status = flurstueck_alkis(None)
|
||||||
|
|
||||||
|
self.assertIsNone(layer)
|
||||||
|
self.assertEqual(status, LoadStatus.NONE)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# 2. Ungültiger WFS-Layer #
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QMessageBox")
|
||||||
|
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsVectorLayer")
|
||||||
|
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance")
|
||||||
|
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text")
|
||||||
|
def test_invalid_wfs_layer_returns_none(
|
||||||
|
self, mock_ask_text, mock_project, mock_wfs_layer_cls, mock_msgbox
|
||||||
|
):
|
||||||
|
"""WFS-Layer nicht valide → (None, NONE)."""
|
||||||
|
mock_ask_text.return_value = ("1234", True)
|
||||||
|
mock_project.return_value = _DummyProject()
|
||||||
|
mock_wfs_layer_cls.return_value = _DummyLayer(valid=False)
|
||||||
|
|
||||||
|
layer, status = flurstueck_alkis(None)
|
||||||
|
|
||||||
|
self.assertIsNone(layer)
|
||||||
|
self.assertEqual(status, LoadStatus.NONE)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# 3. Erfolgreicher Erstladevorgang #
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsVectorLayer")
|
||||||
|
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance")
|
||||||
|
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text")
|
||||||
|
def test_success_returns_layer_and_first_status(
|
||||||
|
self, mock_ask_text, mock_project, mock_wfs_layer_cls
|
||||||
|
):
|
||||||
|
"""Kein bestehender Layer, WFS valide → (layer, FIRST)."""
|
||||||
|
mock_ask_text.return_value = ("1234, 5678", True)
|
||||||
|
mock_project.return_value = _DummyProject()
|
||||||
|
valid_layer = _DummyLayer(valid=True, layer_id="wfs_ok")
|
||||||
|
mock_wfs_layer_cls.return_value = valid_layer
|
||||||
|
|
||||||
|
layer, status = flurstueck_alkis(None)
|
||||||
|
|
||||||
|
self.assertIs(layer, valid_layer)
|
||||||
|
self.assertEqual(status, LoadStatus.FIRST)
|
||||||
|
|
||||||
|
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsVectorLayer")
|
||||||
|
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance")
|
||||||
|
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text")
|
||||||
|
def test_sql_filter_contains_all_schluessel(
|
||||||
|
self, mock_ask_text, mock_project, mock_wfs_layer_cls
|
||||||
|
):
|
||||||
|
"""Der WFS-URI enthält alle eingegebenen Gemarkungsschlüssel."""
|
||||||
|
mock_ask_text.return_value = ("1111, 2222, 3333", True)
|
||||||
|
mock_project.return_value = _DummyProject()
|
||||||
|
mock_wfs_layer_cls.return_value = _DummyLayer(valid=True)
|
||||||
|
|
||||||
|
flurstueck_alkis(None)
|
||||||
|
|
||||||
|
# Erster Positionsarg von QgsVectorLayer ist der URI-String
|
||||||
|
call_args = mock_wfs_layer_cls.call_args
|
||||||
|
uri_str = call_args[0][0] if call_args[0] else call_args[1].get("path", "")
|
||||||
|
self.assertIn("1111", uri_str)
|
||||||
|
self.assertIn("2222", uri_str)
|
||||||
|
self.assertIn("3333", uri_str)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# 4. Bestehender Layer – Nutzer wählt "Neu laden" #
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsVectorLayer")
|
||||||
|
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QMessageBox")
|
||||||
|
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance")
|
||||||
|
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text")
|
||||||
|
def test_existing_layer_reload_yes(
|
||||||
|
self, mock_ask_text, mock_project, mock_msgbox_cls, mock_wfs_layer_cls
|
||||||
|
):
|
||||||
|
"""Bestehender Layer + Nutzer: 'Ja, neu laden' → LoadStatus.RELOAD."""
|
||||||
|
existing = _DummyLayer(valid=True, layer_id="old_flst")
|
||||||
|
project = _DummyProject(named_layers=[existing])
|
||||||
|
mock_ask_text.return_value = ("1234", True)
|
||||||
|
mock_project.return_value = project
|
||||||
|
|
||||||
|
# StandardButton als int, damit | Operator funktioniert
|
||||||
|
mock_msgbox_cls.StandardButton.Yes = 0x4000
|
||||||
|
mock_msgbox_cls.StandardButton.No = 0x10000
|
||||||
|
# QMessageBox.question → Yes
|
||||||
|
mock_msgbox_cls.question.return_value = 0x4000
|
||||||
|
|
||||||
|
new_layer = _DummyLayer(valid=True, layer_id="new_flst")
|
||||||
|
mock_wfs_layer_cls.return_value = new_layer
|
||||||
|
|
||||||
|
layer, status = flurstueck_alkis(None)
|
||||||
|
|
||||||
|
self.assertIn(existing.id(), project.removed)
|
||||||
|
self.assertIs(layer, new_layer)
|
||||||
|
self.assertEqual(status, LoadStatus.RELOAD)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# 5. Bestehender Layer – Nutzer wählt "Behalten" #
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QMessageBox")
|
||||||
|
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance")
|
||||||
|
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text")
|
||||||
|
def test_existing_layer_keep_no(
|
||||||
|
self, mock_ask_text, mock_project, mock_msgbox_cls
|
||||||
|
):
|
||||||
|
"""Bestehender Layer + Nutzer: 'Nein, behalten' → LoadStatus.KEEP."""
|
||||||
|
existing = _DummyLayer(valid=True, layer_id="keep_flst")
|
||||||
|
project = _DummyProject(named_layers=[existing])
|
||||||
|
mock_ask_text.return_value = ("1234", True)
|
||||||
|
mock_project.return_value = project
|
||||||
|
|
||||||
|
# StandardButton als int, damit | Operator funktioniert
|
||||||
|
mock_msgbox_cls.StandardButton.Yes = 0x4000
|
||||||
|
mock_msgbox_cls.StandardButton.No = 0x10000
|
||||||
|
# QMessageBox.question → No
|
||||||
|
mock_msgbox_cls.question.return_value = 0x10000
|
||||||
|
|
||||||
|
layer, status = flurstueck_alkis(None)
|
||||||
|
|
||||||
|
self.assertIs(layer, existing)
|
||||||
|
self.assertEqual(status, LoadStatus.KEEP)
|
||||||
|
self.assertEqual(project.removed, []) # nichts gelöscht
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# 6. vorgabe_schluessel füllt ask_text-Default vor #
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsVectorLayer")
|
||||||
|
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance")
|
||||||
|
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text")
|
||||||
|
def test_vorgabe_schluessel_prefills_default_text(
|
||||||
|
self, mock_ask_text, mock_project, mock_wfs_layer_cls
|
||||||
|
):
|
||||||
|
"""vorgabe_schluessel=['1234','5678'] → ask_text erhält '1234, 5678' als default_text."""
|
||||||
|
mock_ask_text.return_value = ("1234, 5678", True)
|
||||||
|
mock_project.return_value = _DummyProject()
|
||||||
|
mock_wfs_layer_cls.return_value = _DummyLayer(valid=True)
|
||||||
|
|
||||||
|
flurstueck_alkis(None, vorgabe_schluessel=["1234", "5678"])
|
||||||
|
|
||||||
|
args, kwargs = mock_ask_text.call_args
|
||||||
|
# ask_text("title", label, default_text=..., parent=...)
|
||||||
|
# default_text wird als Keyword-Arg übergeben
|
||||||
|
default_text_arg = kwargs.get("default_text", args[2] if len(args) > 2 else "")
|
||||||
|
self.assertIn("1234", default_text_arg)
|
||||||
|
self.assertIn("5678", default_text_arg)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# 7. vorgabe_namen erscheint im Label-Text #
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsVectorLayer")
|
||||||
|
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance")
|
||||||
|
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text")
|
||||||
|
def test_vorgabe_namen_appears_in_label(
|
||||||
|
self, mock_ask_text, mock_project, mock_wfs_layer_cls
|
||||||
|
):
|
||||||
|
"""vorgabe_namen=['Musterstadt'] → ask_text-label enthält den Namen."""
|
||||||
|
mock_ask_text.return_value = ("9999", True)
|
||||||
|
mock_project.return_value = _DummyProject()
|
||||||
|
mock_wfs_layer_cls.return_value = _DummyLayer(valid=True)
|
||||||
|
|
||||||
|
flurstueck_alkis(None, vorgabe_namen=["Musterstadt"])
|
||||||
|
|
||||||
|
args, kwargs = mock_ask_text.call_args
|
||||||
|
# label ist das 2. Positional-Argument
|
||||||
|
label_arg = args[1] if len(args) > 1 else kwargs.get("label", "")
|
||||||
|
self.assertIn("Musterstadt", label_arg)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# 8. bbox wird in WFS-URI eingebettet #
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsVectorLayer")
|
||||||
|
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance")
|
||||||
|
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text")
|
||||||
|
def test_bbox_embedded_in_wfs_uri(
|
||||||
|
self, mock_ask_text, mock_project, mock_wfs_layer_cls
|
||||||
|
):
|
||||||
|
"""bbox-Parameter → WFS-URI enthält 'bbox=' Substring."""
|
||||||
|
mock_ask_text.return_value = ("1234", True)
|
||||||
|
mock_project.return_value = _DummyProject()
|
||||||
|
mock_wfs_layer_cls.return_value = _DummyLayer(valid=True)
|
||||||
|
|
||||||
|
class _Rect:
|
||||||
|
def xMinimum(self): return 414000.0
|
||||||
|
def yMinimum(self): return 5657000.0
|
||||||
|
def xMaximum(self): return 415000.0
|
||||||
|
def yMaximum(self): return 5658000.0
|
||||||
|
|
||||||
|
flurstueck_alkis(None, bbox=_Rect())
|
||||||
|
|
||||||
|
uri_str = mock_wfs_layer_cls.call_args[0][0]
|
||||||
|
self.assertIn("bbox=", uri_str.lower())
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# 9. Rückwärtskompatibilität: alle neuen Params optional #
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsVectorLayer")
|
||||||
|
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance")
|
||||||
|
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text")
|
||||||
|
def test_backwards_compatible_no_new_params(
|
||||||
|
self, mock_ask_text, mock_project, mock_wfs_layer_cls
|
||||||
|
):
|
||||||
|
"""flurstueck_alkis(tab) ohne neue Parameter → kein Fehler."""
|
||||||
|
mock_ask_text.return_value = ("0000", True)
|
||||||
|
mock_project.return_value = _DummyProject()
|
||||||
|
mock_wfs_layer_cls.return_value = _DummyLayer(valid=True)
|
||||||
|
|
||||||
|
layer, status = flurstueck_alkis(None)
|
||||||
|
|
||||||
|
self.assertIsNotNone(layer)
|
||||||
|
self.assertEqual(status, LoadStatus.FIRST)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,207 @@
|
|||||||
|
"""sn_verfahrensgebiet/tests/test_gemarkung_alkis.py
|
||||||
|
|
||||||
|
Unit-Tests für :mod:`sn_verfahrensgebiet.functions.gemarkung_alkis`.
|
||||||
|
|
||||||
|
Getestete Szenarien:
|
||||||
|
|
||||||
|
Erfolgsfall:
|
||||||
|
- WFS-Layer gültig + Features mit Schlüssel → Liste mit Tupeln
|
||||||
|
- Schlüssel werden dedupliziert und sortiert zurückgegeben
|
||||||
|
- ``gemarkung_schluessel_by_bbox`` gibt nur Nummern (keine Tupel) zurück
|
||||||
|
|
||||||
|
Fehlerfälle:
|
||||||
|
- WFS-Layer nicht gültig (beide Varianten) → Fallback-Ladeversuch
|
||||||
|
- Fallback-Layer ebenfalls ungültig → leere Liste
|
||||||
|
- Alle Layer leer (featureCount == 0) → client-seitige Filterung schlägt leer
|
||||||
|
- Feature ohne Schlüssel-Attribut wird übersprungen
|
||||||
|
- Exception beim Layer-Laden → leere Liste
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import types
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import patch, MagicMock
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 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.gemarkung_alkis import (
|
||||||
|
gemarkung_schluessel_und_namen_by_bbox,
|
||||||
|
gemarkung_schluessel_by_bbox,
|
||||||
|
_extract_schluessel_und_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Dummies
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class _DummyRect:
|
||||||
|
def xMinimum(self): return 414000.0
|
||||||
|
def yMinimum(self): return 5657000.0
|
||||||
|
def xMaximum(self): return 415000.0
|
||||||
|
def yMaximum(self): return 5658000.0
|
||||||
|
|
||||||
|
|
||||||
|
class _DummyFeature:
|
||||||
|
def __init__(self, attrs: dict):
|
||||||
|
self._attrs = attrs
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
return self._attrs.get(key, None)
|
||||||
|
|
||||||
|
|
||||||
|
def _make_layer(valid=True, feature_count=2, features=None):
|
||||||
|
layer = MagicMock()
|
||||||
|
layer.isValid.return_value = valid
|
||||||
|
layer.featureCount.return_value = feature_count if valid else 0
|
||||||
|
layer.getFeatures.return_value = features or []
|
||||||
|
return layer
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Tests: _extract_schluessel_und_name
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class TestExtractSchluesselUndName(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_standard_attribute(self):
|
||||||
|
feat = _DummyFeature({"schluessel_gemarkungsnummer": "1234", "bezeichnung": "Mustergemarkung"})
|
||||||
|
nr, name = _extract_schluessel_und_name(feat)
|
||||||
|
self.assertEqual(nr, "1234")
|
||||||
|
self.assertEqual(name, "Mustergemarkung")
|
||||||
|
|
||||||
|
def test_fallback_attribute(self):
|
||||||
|
feat = _DummyFeature({"gemarkungsnummer": "5678", "gemarkung_bezeichnung": "Alternativ"})
|
||||||
|
nr, name = _extract_schluessel_und_name(feat)
|
||||||
|
self.assertEqual(nr, "5678")
|
||||||
|
self.assertEqual(name, "Alternativ")
|
||||||
|
|
||||||
|
def test_missing_attribute_returns_empty(self):
|
||||||
|
feat = _DummyFeature({})
|
||||||
|
nr, name = _extract_schluessel_und_name(feat)
|
||||||
|
self.assertEqual(nr, "")
|
||||||
|
self.assertEqual(name, "")
|
||||||
|
|
||||||
|
def test_none_value_returns_empty(self):
|
||||||
|
feat = _DummyFeature({"schluessel_gemarkungsnummer": None, "bezeichnung": None})
|
||||||
|
nr, name = _extract_schluessel_und_name(feat)
|
||||||
|
self.assertEqual(nr, "")
|
||||||
|
self.assertEqual(name, "")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Tests: gemarkung_schluessel_und_namen_by_bbox
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class TestGemarkungSchluesselbbox(unittest.TestCase):
|
||||||
|
|
||||||
|
@patch("sn_verfahrensgebiet.functions.gemarkung_alkis._load_gemarkung_layer_with_bbox")
|
||||||
|
def test_success_bbox_filter(self, mock_load_bbox):
|
||||||
|
"""WFS-BBox-Filter liefert Features → korrekte Tupel-Liste."""
|
||||||
|
feat1 = _DummyFeature({"schluessel_gemarkungsnummer": "1234", "bezeichnung": "Alpha"})
|
||||||
|
feat2 = _DummyFeature({"schluessel_gemarkungsnummer": "5678", "bezeichnung": "Beta"})
|
||||||
|
mock_load_bbox.return_value = _make_layer(valid=True, features=[feat1, feat2])
|
||||||
|
|
||||||
|
result = gemarkung_schluessel_und_namen_by_bbox(_DummyRect())
|
||||||
|
|
||||||
|
self.assertEqual(result, [("1234", "Alpha"), ("5678", "Beta")])
|
||||||
|
|
||||||
|
@patch("sn_verfahrensgebiet.functions.gemarkung_alkis._load_all_gemarkung_layer")
|
||||||
|
@patch("sn_verfahrensgebiet.functions.gemarkung_alkis._load_gemarkung_layer_with_bbox")
|
||||||
|
def test_fallback_when_bbox_fails(self, mock_load_bbox, mock_load_all):
|
||||||
|
"""BBox-Filter schlägt fehl → Fallback auf Volllade + client-seitige Filterung."""
|
||||||
|
mock_load_bbox.return_value = None
|
||||||
|
|
||||||
|
feat1 = _DummyFeature({"schluessel_gemarkungsnummer": "9999", "bezeichnung": "Fallback"})
|
||||||
|
all_layer = _make_layer(valid=True, features=[feat1])
|
||||||
|
# getFeatures mit FilterRect soll dasselbe zurückgeben
|
||||||
|
all_layer.getFeatures.return_value = [feat1]
|
||||||
|
mock_load_all.return_value = all_layer
|
||||||
|
|
||||||
|
result = gemarkung_schluessel_und_namen_by_bbox(_DummyRect())
|
||||||
|
|
||||||
|
self.assertEqual(len(result), 1)
|
||||||
|
self.assertEqual(result[0][0], "9999")
|
||||||
|
|
||||||
|
@patch("sn_verfahrensgebiet.functions.gemarkung_alkis._load_all_gemarkung_layer")
|
||||||
|
@patch("sn_verfahrensgebiet.functions.gemarkung_alkis._load_gemarkung_layer_with_bbox")
|
||||||
|
def test_both_layers_fail_returns_empty(self, mock_load_bbox, mock_load_all):
|
||||||
|
"""Beide Lade-Strategien scheitern → leere Liste."""
|
||||||
|
mock_load_bbox.return_value = None
|
||||||
|
mock_load_all.return_value = None
|
||||||
|
|
||||||
|
result = gemarkung_schluessel_und_namen_by_bbox(_DummyRect())
|
||||||
|
self.assertEqual(result, [])
|
||||||
|
|
||||||
|
@patch("sn_verfahrensgebiet.functions.gemarkung_alkis._load_gemarkung_layer_with_bbox")
|
||||||
|
def test_deduplication_and_sorting(self, mock_load_bbox):
|
||||||
|
"""Doppelte Schlüssel werden dedupliziert, Ergebnis ist sortiert."""
|
||||||
|
feat_a = _DummyFeature({"schluessel_gemarkungsnummer": "2222", "bezeichnung": "Z"})
|
||||||
|
feat_b = _DummyFeature({"schluessel_gemarkungsnummer": "1111", "bezeichnung": "A"})
|
||||||
|
feat_dup = _DummyFeature({"schluessel_gemarkungsnummer": "2222", "bezeichnung": "Z2"})
|
||||||
|
mock_load_bbox.return_value = _make_layer(features=[feat_a, feat_b, feat_dup])
|
||||||
|
|
||||||
|
result = gemarkung_schluessel_und_namen_by_bbox(_DummyRect())
|
||||||
|
|
||||||
|
nummern = [r[0] for r in result]
|
||||||
|
self.assertEqual(nummern, sorted(nummern))
|
||||||
|
self.assertEqual(len(result), 2)
|
||||||
|
|
||||||
|
@patch("sn_verfahrensgebiet.functions.gemarkung_alkis._load_gemarkung_layer_with_bbox")
|
||||||
|
def test_feature_without_schluessel_skipped(self, mock_load_bbox):
|
||||||
|
"""Feature ohne Schlüssel-Attribut wird übersprungen."""
|
||||||
|
feat_ok = _DummyFeature({"schluessel_gemarkungsnummer": "1234", "bezeichnung": "X"})
|
||||||
|
feat_bad = _DummyFeature({})
|
||||||
|
mock_load_bbox.return_value = _make_layer(features=[feat_ok, feat_bad])
|
||||||
|
|
||||||
|
result = gemarkung_schluessel_und_namen_by_bbox(_DummyRect())
|
||||||
|
|
||||||
|
self.assertEqual(len(result), 1)
|
||||||
|
self.assertEqual(result[0][0], "1234")
|
||||||
|
|
||||||
|
@patch("sn_verfahrensgebiet.functions.gemarkung_alkis._load_gemarkung_layer_with_bbox")
|
||||||
|
def test_exception_returns_empty(self, mock_load_bbox):
|
||||||
|
"""Exception im Ladevorgang → leere Liste (kein Crash)."""
|
||||||
|
mock_load_bbox.side_effect = RuntimeError("WFS-Fehler")
|
||||||
|
|
||||||
|
result = gemarkung_schluessel_und_namen_by_bbox(_DummyRect())
|
||||||
|
self.assertEqual(result, [])
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Tests: gemarkung_schluessel_by_bbox (Convenience-Wrapper)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class TestGemarkungSchluesselnurNummern(unittest.TestCase):
|
||||||
|
|
||||||
|
@patch("sn_verfahrensgebiet.functions.gemarkung_alkis.gemarkung_schluessel_und_namen_by_bbox")
|
||||||
|
def test_returns_only_nummern(self, mock_full):
|
||||||
|
mock_full.return_value = [("1234", "Alpha"), ("5678", "Beta")]
|
||||||
|
|
||||||
|
result = gemarkung_schluessel_by_bbox(_DummyRect())
|
||||||
|
|
||||||
|
self.assertEqual(result, ["1234", "5678"])
|
||||||
|
|
||||||
|
@patch("sn_verfahrensgebiet.functions.gemarkung_alkis.gemarkung_schluessel_und_namen_by_bbox")
|
||||||
|
def test_empty_input_returns_empty(self, mock_full):
|
||||||
|
mock_full.return_value = []
|
||||||
|
|
||||||
|
result = gemarkung_schluessel_by_bbox(_DummyRect())
|
||||||
|
self.assertEqual(result, [])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,301 @@
|
|||||||
|
"""sn_verfahrensgebiet/tests/test_grenzpunkt_alkis.py
|
||||||
|
|
||||||
|
Unit-Tests für :mod:`sn_verfahrensgebiet.functions.grenzpunkt_alkis`.
|
||||||
|
|
||||||
|
Getestete Szenarien:
|
||||||
|
|
||||||
|
Erfolgsfall:
|
||||||
|
- Vollständiger Workflow: PunktortTA laden → GML-IDs extrahieren →
|
||||||
|
Grenzpunkte laden → beide Layer zurückgegeben
|
||||||
|
|
||||||
|
Fehlerfälle:
|
||||||
|
- PunktortTA-Layer ungültig → (None, None)
|
||||||
|
- PunktortTA leer (featureCount == 0) → Warnung + (None, None)
|
||||||
|
- Keine gehoertZu-Referenzen → (punktort_layer, None) + Warnung
|
||||||
|
- Grenzpunkt-Layer nach IDs leer → (punktort_layer, None) + Warnung
|
||||||
|
- Kein Verfahrensgebiet-Layer für BBox-Ableitung → (None, None)
|
||||||
|
|
||||||
|
Hilfsfunktionen:
|
||||||
|
- _extrahiere_gml_ids: xlink:href-Format wird korrekt geparst
|
||||||
|
- _extrahiere_gml_ids: plain GML-ID ohne Doppelpunkt wird direkt verwendet
|
||||||
|
- _extrahiere_gml_ids: leeres Feld wird übersprungen
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import types
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import patch, MagicMock
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 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.grenzpunkt_alkis import (
|
||||||
|
grenzpunkt_alkis,
|
||||||
|
_extrahiere_gml_ids,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Dummies
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class _DummyRect:
|
||||||
|
def xMinimum(self): return 414000.0
|
||||||
|
def yMinimum(self): return 5657000.0
|
||||||
|
def xMaximum(self): return 415000.0
|
||||||
|
def yMaximum(self): return 5658000.0
|
||||||
|
|
||||||
|
|
||||||
|
class _DummyFeature:
|
||||||
|
def __init__(self, gehoert_zu_val=None):
|
||||||
|
self._val = gehoert_zu_val
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
if key == "gehoertZu":
|
||||||
|
return self._val
|
||||||
|
raise KeyError(key)
|
||||||
|
|
||||||
|
|
||||||
|
def _make_layer(valid=True, feature_count=3, features=None, name="Layer"):
|
||||||
|
layer = MagicMock()
|
||||||
|
layer.isValid.return_value = valid
|
||||||
|
layer.featureCount.return_value = feature_count if valid else 0
|
||||||
|
layer.getFeatures.return_value = features or []
|
||||||
|
layer.name.return_value = name
|
||||||
|
layer.id.return_value = "LYR1"
|
||||||
|
return layer
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Tests: _extrahiere_gml_ids
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class TestExtrahiereGmlIds(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_xlink_href_format(self):
|
||||||
|
"""URN-Format 'urn:adv:oid:DEABC123' → nur 'DEABC123' extrahiert."""
|
||||||
|
feat = _DummyFeature("urn:adv:oid:DEABC123")
|
||||||
|
layer = _make_layer(features=[feat])
|
||||||
|
result = _extrahiere_gml_ids(layer)
|
||||||
|
self.assertEqual(result, ["DEABC123"])
|
||||||
|
|
||||||
|
def test_plain_gml_id(self):
|
||||||
|
"""Plain-GML-ID ohne Doppelpunkt → direkt übernommen."""
|
||||||
|
feat = _DummyFeature("DEPLAIN456")
|
||||||
|
layer = _make_layer(features=[feat])
|
||||||
|
result = _extrahiere_gml_ids(layer)
|
||||||
|
self.assertEqual(result, ["DEPLAIN456"])
|
||||||
|
|
||||||
|
def test_empty_value_skipped(self):
|
||||||
|
"""Leerer gehoertZu-Wert wird übersprungen."""
|
||||||
|
feat = _DummyFeature(None)
|
||||||
|
layer = _make_layer(features=[feat])
|
||||||
|
result = _extrahiere_gml_ids(layer)
|
||||||
|
self.assertEqual(result, [])
|
||||||
|
|
||||||
|
def test_deduplication(self):
|
||||||
|
"""Doppelte Referenzen werden dedupliziert."""
|
||||||
|
f1 = _DummyFeature("urn:adv:oid:DEDUP001")
|
||||||
|
f2 = _DummyFeature("urn:adv:oid:DEDUP001")
|
||||||
|
f3 = _DummyFeature("urn:adv:oid:DEOTHER")
|
||||||
|
layer = _make_layer(features=[f1, f2, f3])
|
||||||
|
result = _extrahiere_gml_ids(layer)
|
||||||
|
self.assertEqual(len(result), 2)
|
||||||
|
self.assertIn("DEDUP001", result)
|
||||||
|
self.assertIn("DEOTHER", result)
|
||||||
|
|
||||||
|
def test_sorted_output(self):
|
||||||
|
"""Rückgabe ist aufsteigend sortiert."""
|
||||||
|
f1 = _DummyFeature("urn:adv:oid:ZZZ")
|
||||||
|
f2 = _DummyFeature("urn:adv:oid:AAA")
|
||||||
|
layer = _make_layer(features=[f1, f2])
|
||||||
|
result = _extrahiere_gml_ids(layer)
|
||||||
|
self.assertEqual(result, sorted(result))
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Tests: grenzpunkt_alkis
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
_PATCH_PROJECT = "sn_verfahrensgebiet.functions.grenzpunkt_alkis.QgsProject"
|
||||||
|
_PATCH_MSGBOX = "sn_verfahrensgebiet.functions.grenzpunkt_alkis.QMessageBox"
|
||||||
|
_PATCH_WFS = "sn_verfahrensgebiet.functions.grenzpunkt_alkis.QgsVectorLayer"
|
||||||
|
_PATCH_EXTRAHIERE = "sn_verfahrensgebiet.functions.grenzpunkt_alkis._extrahiere_gml_ids"
|
||||||
|
_PATCH_LADE_GP = "sn_verfahrensgebiet.functions.grenzpunkt_alkis._lade_grenzpunkte_per_ids"
|
||||||
|
_PATCH_INFO = "sn_verfahrensgebiet.functions.grenzpunkt_alkis.info"
|
||||||
|
_PATCH_WARNING = "sn_verfahrensgebiet.functions.grenzpunkt_alkis.warning"
|
||||||
|
|
||||||
|
|
||||||
|
class TestGrenzpunktAlkis(unittest.TestCase):
|
||||||
|
|
||||||
|
def _proj(self, vg_layer=None):
|
||||||
|
proj = MagicMock()
|
||||||
|
proj.mapLayersByName.return_value = [vg_layer] if vg_layer else []
|
||||||
|
return proj
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# 1. Kein VG-Layer wenn extent=None → (None, None) #
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
@patch(_PATCH_MSGBOX)
|
||||||
|
@patch(_PATCH_PROJECT)
|
||||||
|
def test_no_vg_layer_returns_none(self, mock_proj, mock_msgbox):
|
||||||
|
mock_proj.instance.return_value = self._proj(vg_layer=None)
|
||||||
|
|
||||||
|
pta, gp = grenzpunkt_alkis(None, extent=None)
|
||||||
|
|
||||||
|
self.assertIsNone(pta)
|
||||||
|
self.assertIsNone(gp)
|
||||||
|
mock_msgbox.warning.assert_called_once()
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# 2. PunktortTA-Layer ungültig → (None, None) #
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
@patch(_PATCH_MSGBOX)
|
||||||
|
@patch(_PATCH_WFS)
|
||||||
|
@patch(_PATCH_PROJECT)
|
||||||
|
def test_invalid_punktort_layer(self, mock_proj, mock_wfs, mock_msgbox):
|
||||||
|
proj = MagicMock()
|
||||||
|
proj.mapLayersByName.return_value = []
|
||||||
|
mock_proj.instance.return_value = proj
|
||||||
|
mock_wfs.return_value = _make_layer(valid=False)
|
||||||
|
|
||||||
|
pta, gp = grenzpunkt_alkis(None, extent=_DummyRect())
|
||||||
|
|
||||||
|
self.assertIsNone(pta)
|
||||||
|
self.assertIsNone(gp)
|
||||||
|
mock_msgbox.critical.assert_called_once()
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# 3. PunktortTA leer → Warnung + (None, None) #
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
@patch(_PATCH_MSGBOX)
|
||||||
|
@patch(_PATCH_WFS)
|
||||||
|
@patch(_PATCH_PROJECT)
|
||||||
|
def test_empty_punktort_layer(self, mock_proj, mock_wfs, mock_msgbox):
|
||||||
|
mock_proj.instance.return_value = MagicMock()
|
||||||
|
mock_wfs.return_value = _make_layer(valid=True, feature_count=0)
|
||||||
|
|
||||||
|
pta, gp = grenzpunkt_alkis(None, extent=_DummyRect())
|
||||||
|
|
||||||
|
self.assertIsNone(pta)
|
||||||
|
self.assertIsNone(gp)
|
||||||
|
mock_msgbox.warning.assert_called_once()
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# 4. Keine gehoertZu-Referenzen → (punktort_layer, None) + Warnung #
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
@patch(_PATCH_WARNING)
|
||||||
|
@patch(_PATCH_EXTRAHIERE)
|
||||||
|
@patch(_PATCH_WFS)
|
||||||
|
@patch(_PATCH_PROJECT)
|
||||||
|
def test_no_gml_ids_returns_pta_only(
|
||||||
|
self, mock_proj, mock_wfs, mock_extrahiere, mock_warning
|
||||||
|
):
|
||||||
|
proj = MagicMock()
|
||||||
|
mock_proj.instance.return_value = proj
|
||||||
|
pta_layer = _make_layer(valid=True, feature_count=5)
|
||||||
|
mock_wfs.return_value = pta_layer
|
||||||
|
mock_extrahiere.return_value = []
|
||||||
|
|
||||||
|
pta, gp = grenzpunkt_alkis(None, extent=_DummyRect())
|
||||||
|
|
||||||
|
self.assertIs(pta, pta_layer)
|
||||||
|
self.assertIsNone(gp)
|
||||||
|
mock_warning.assert_called_once()
|
||||||
|
proj.addMapLayer.assert_called_once_with(pta_layer)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# 5. Grenzpunkte-Batch liefert None → (punktort_layer, None) + Warn #
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
@patch(_PATCH_WARNING)
|
||||||
|
@patch(_PATCH_LADE_GP)
|
||||||
|
@patch(_PATCH_EXTRAHIERE)
|
||||||
|
@patch(_PATCH_WFS)
|
||||||
|
@patch(_PATCH_PROJECT)
|
||||||
|
def test_grenzpunkt_batch_empty(
|
||||||
|
self, mock_proj, mock_wfs, mock_extrahiere, mock_lade_gp, mock_warning
|
||||||
|
):
|
||||||
|
proj = MagicMock()
|
||||||
|
mock_proj.instance.return_value = proj
|
||||||
|
pta_layer = _make_layer(valid=True, feature_count=3)
|
||||||
|
mock_wfs.return_value = pta_layer
|
||||||
|
mock_extrahiere.return_value = ["DE0001", "DE0002"]
|
||||||
|
mock_lade_gp.return_value = None # Batch leer
|
||||||
|
|
||||||
|
pta, gp = grenzpunkt_alkis(None, extent=_DummyRect())
|
||||||
|
|
||||||
|
self.assertIs(pta, pta_layer)
|
||||||
|
self.assertIsNone(gp)
|
||||||
|
mock_warning.assert_called_once()
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# 6. Vollständiger Erfolgsfall #
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
@patch(_PATCH_LADE_GP)
|
||||||
|
@patch(_PATCH_EXTRAHIERE)
|
||||||
|
@patch(_PATCH_WFS)
|
||||||
|
@patch(_PATCH_PROJECT)
|
||||||
|
def test_success_returns_both_layers(
|
||||||
|
self, mock_proj, mock_wfs, mock_extrahiere, mock_lade_gp
|
||||||
|
):
|
||||||
|
proj = MagicMock()
|
||||||
|
mock_proj.instance.return_value = proj
|
||||||
|
pta_layer = _make_layer(valid=True, feature_count=10, name="PunktortTA")
|
||||||
|
gp_layer = _make_layer(valid=True, feature_count=10, name="Grenzpunkte")
|
||||||
|
mock_wfs.return_value = pta_layer
|
||||||
|
mock_extrahiere.return_value = ["DE001", "DE002", "DE003"]
|
||||||
|
mock_lade_gp.return_value = gp_layer
|
||||||
|
|
||||||
|
pta, gp = grenzpunkt_alkis(None, extent=_DummyRect())
|
||||||
|
|
||||||
|
self.assertIs(pta, pta_layer)
|
||||||
|
self.assertIs(gp, gp_layer)
|
||||||
|
# Beide Layer müssen ins Projekt eingefügt worden sein
|
||||||
|
calls = [c[0][0] for c in proj.addMapLayer.call_args_list]
|
||||||
|
self.assertIn(pta_layer, calls)
|
||||||
|
self.assertIn(gp_layer, calls)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# 7. Extent aus vorhandenem VG-Layer ableiten #
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
@patch(_PATCH_LADE_GP)
|
||||||
|
@patch(_PATCH_EXTRAHIERE)
|
||||||
|
@patch(_PATCH_WFS)
|
||||||
|
@patch(_PATCH_PROJECT)
|
||||||
|
def test_extent_derived_from_vg_layer(
|
||||||
|
self, mock_proj, mock_wfs, mock_extrahiere, mock_lade_gp
|
||||||
|
):
|
||||||
|
"""Wenn kein extent übergeben → Extent aus VG-Layer holen."""
|
||||||
|
vg_layer = MagicMock()
|
||||||
|
vg_layer.extent.return_value = _DummyRect()
|
||||||
|
proj = MagicMock()
|
||||||
|
proj.mapLayersByName.return_value = [vg_layer]
|
||||||
|
mock_proj.instance.return_value = proj
|
||||||
|
|
||||||
|
pta_layer = _make_layer(valid=True, feature_count=2)
|
||||||
|
gp_layer = _make_layer(valid=True, feature_count=2)
|
||||||
|
mock_wfs.return_value = pta_layer
|
||||||
|
mock_extrahiere.return_value = ["DE999"]
|
||||||
|
mock_lade_gp.return_value = gp_layer
|
||||||
|
|
||||||
|
pta, gp = grenzpunkt_alkis(None, extent=None)
|
||||||
|
|
||||||
|
vg_layer.extent.assert_called_once()
|
||||||
|
self.assertIs(pta, pta_layer)
|
||||||
|
self.assertIs(gp, gp_layer)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,307 @@
|
|||||||
|
"""sn_verfahrensgebiet/tests/test_knickpunkt_pruefung.py
|
||||||
|
|
||||||
|
Unit-Tests für :mod:`sn_verfahrensgebiet.functions.knickpunkt_pruefung`.
|
||||||
|
|
||||||
|
Getestete Szenarien:
|
||||||
|
|
||||||
|
_extrahiere_knickpunkte:
|
||||||
|
- Leerer Layer → leere Liste
|
||||||
|
- Polygon mit 4 Knickpunkten → korrekte Anzahl (ohne doppelten Schlusspunkt)
|
||||||
|
- Keine Geometrie → leere Liste
|
||||||
|
|
||||||
|
knickpunkt_pruefung:
|
||||||
|
- Leere Knickpunktliste → (None, 0, 0, 0)
|
||||||
|
- Kein PunktortTA-Layer → (None, 0, 0, 0)
|
||||||
|
- Identischer Punkt → grün gezählt
|
||||||
|
- Kein passender PunktortTA → rot gezählt
|
||||||
|
- PunktortTA nahe Umring auf Flurstücksgrenze → orange gezählt
|
||||||
|
- Abbruch via progress.is_canceled() → (None, 0, 0, 0)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import types
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Minimales QGIS-Mock-Setup (identisch mit test_verfahrensgebiet_alkis_komplett)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
if "qgis" not in sys.modules:
|
||||||
|
qgis_mod = types.ModuleType("qgis")
|
||||||
|
qgis_core = types.ModuleType("qgis.core")
|
||||||
|
# Attribute vorab setzen, damit patch() sie überschreiben kann
|
||||||
|
qgis_core.QgsPointXY = None
|
||||||
|
qgis_core.QgsRectangle = None
|
||||||
|
qgis_core.QgsSpatialIndex = None
|
||||||
|
qgis_core.QgsMarkerSymbol = None
|
||||||
|
qgis_core.QgsCategorizedSymbolRenderer = None
|
||||||
|
qgis_core.QgsRendererCategory = None
|
||||||
|
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.knickpunkt_pruefung import (
|
||||||
|
knickpunkt_pruefung,
|
||||||
|
_extrahiere_knickpunkte,
|
||||||
|
WERT_IDENTISCH,
|
||||||
|
WERT_KEIN_PTA,
|
||||||
|
WERT_FEHLENDER_KNICK,
|
||||||
|
)
|
||||||
|
|
||||||
|
_MOD = "sn_verfahrensgebiet.functions.knickpunkt_pruefung"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Hilfsfunktionen
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _make_layer(valid=True, crs="EPSG:25833", lid="L1"):
|
||||||
|
layer = MagicMock()
|
||||||
|
layer.isValid.return_value = valid
|
||||||
|
layer.id.return_value = lid
|
||||||
|
mock_crs = MagicMock()
|
||||||
|
mock_crs.authid.return_value = crs
|
||||||
|
layer.crs.return_value = mock_crs
|
||||||
|
layer.getFeatures.return_value = []
|
||||||
|
return layer
|
||||||
|
|
||||||
|
|
||||||
|
def _make_progress(canceled_after: int = 9999):
|
||||||
|
"""Progress-Mock: gibt is_canceled() nach N Aufrufen True zurück."""
|
||||||
|
call_count = [0]
|
||||||
|
|
||||||
|
def _is_canceled():
|
||||||
|
call_count[0] += 1
|
||||||
|
return call_count[0] > canceled_after
|
||||||
|
|
||||||
|
prog = MagicMock()
|
||||||
|
prog.is_canceled.side_effect = _is_canceled
|
||||||
|
return prog
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Tests: _extrahiere_knickpunkte
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class TestExtrahiereKnickpunkte(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_leerer_layer_gibt_leere_liste(self):
|
||||||
|
layer = _make_layer()
|
||||||
|
layer.getFeatures.return_value = []
|
||||||
|
result = _extrahiere_knickpunkte(layer)
|
||||||
|
self.assertEqual(result, [])
|
||||||
|
|
||||||
|
def test_keine_gueltige_geometrie_gibt_leere_liste(self):
|
||||||
|
feat = MagicMock()
|
||||||
|
feat.geometry.return_value = None
|
||||||
|
layer = _make_layer()
|
||||||
|
layer.getFeatures.return_value = [feat]
|
||||||
|
result = _extrahiere_knickpunkte(layer)
|
||||||
|
self.assertEqual(result, [])
|
||||||
|
|
||||||
|
def test_feature_mit_leerer_geometrie_gibt_leere_liste(self):
|
||||||
|
feat = MagicMock()
|
||||||
|
geom = MagicMock()
|
||||||
|
geom.isEmpty.return_value = True
|
||||||
|
feat.geometry.return_value = geom
|
||||||
|
layer = _make_layer()
|
||||||
|
layer.getFeatures.return_value = [feat]
|
||||||
|
result = _extrahiere_knickpunkte(layer)
|
||||||
|
self.assertEqual(result, [])
|
||||||
|
|
||||||
|
def test_qgis_nicht_verfuegbar_gibt_leere_liste(self):
|
||||||
|
"""Wenn qgis.core kein QgsPointXY hat (ImportError in Funktion), leere Liste."""
|
||||||
|
layer = _make_layer()
|
||||||
|
feat = MagicMock()
|
||||||
|
geom = MagicMock()
|
||||||
|
geom.isEmpty.return_value = False
|
||||||
|
feat.geometry.return_value = geom
|
||||||
|
layer.getFeatures.return_value = [feat]
|
||||||
|
|
||||||
|
# QgsGeometry.unaryUnion wirft eine Exception → leere Liste erwartet
|
||||||
|
with patch(f"{_MOD}.QgsGeometry") as mock_geom_cls:
|
||||||
|
mock_geom_cls.unaryUnion.side_effect = ImportError("kein QGIS")
|
||||||
|
result = _extrahiere_knickpunkte(layer)
|
||||||
|
self.assertEqual(result, [])
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Tests: knickpunkt_pruefung – Eingabevalidierung
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class TestKnickpunktPruefungInputvalidierung(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_keine_knickpunkte_gibt_none_zurueck(self):
|
||||||
|
bror = _make_layer()
|
||||||
|
bror.getFeatures.return_value = []
|
||||||
|
result = knickpunkt_pruefung(bror, MagicMock(), None)
|
||||||
|
layer, identisch, kein_pta, fehlend = result
|
||||||
|
self.assertIsNone(layer)
|
||||||
|
self.assertEqual(identisch, 0)
|
||||||
|
self.assertEqual(kein_pta, 0)
|
||||||
|
self.assertEqual(fehlend, 0)
|
||||||
|
|
||||||
|
def test_kein_punktort_layer_gibt_none_zurueck(self):
|
||||||
|
bror = _make_layer()
|
||||||
|
result = knickpunkt_pruefung(bror, None, None, knickpunkte=[MagicMock()])
|
||||||
|
layer, identisch, kein_pta, fehlend = result
|
||||||
|
self.assertIsNone(layer)
|
||||||
|
|
||||||
|
def test_abbruch_via_progress_gibt_none_zurueck(self):
|
||||||
|
bror = _make_layer()
|
||||||
|
pta = _make_layer()
|
||||||
|
prog = _make_progress(canceled_after=0) # sofort abbrechen
|
||||||
|
result = knickpunkt_pruefung(
|
||||||
|
bror, pta, None,
|
||||||
|
progress=prog,
|
||||||
|
knickpunkte=[MagicMock()],
|
||||||
|
)
|
||||||
|
self.assertIsNone(result[0])
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Tests: knickpunkt_pruefung – Klassifizierungslogik (mit QGIS-Mocks)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class TestKnickpunktPruefungKlassifizierung(unittest.TestCase):
|
||||||
|
"""Testet die Grün/Rot/Orange-Klassifizierung mit gesteuertem QGIS-Mock."""
|
||||||
|
|
||||||
|
def _make_pta_layer_mit_punkt(self, x: float, y: float, fid: int = 1):
|
||||||
|
"""Erstellt einen PunktortTA-Layer-Mock mit einem Punkt bei (x, y)."""
|
||||||
|
layer = _make_layer(lid=f"PTA_{fid}")
|
||||||
|
geom = MagicMock()
|
||||||
|
geom.isEmpty.return_value = False
|
||||||
|
pt = MagicMock()
|
||||||
|
pt.x.return_value = x
|
||||||
|
pt.y.return_value = y
|
||||||
|
geom.asPoint.return_value = pt
|
||||||
|
feat = MagicMock()
|
||||||
|
feat.id.return_value = fid
|
||||||
|
feat.geometry.return_value = geom
|
||||||
|
layer.getFeatures.return_value = [feat]
|
||||||
|
return layer
|
||||||
|
|
||||||
|
def _make_bror_layer(self, x: float = 0.0, y: float = 0.0):
|
||||||
|
"""BROR-Layer mit einer minimalen Polygon-Geometrie."""
|
||||||
|
layer = _make_layer()
|
||||||
|
geom = MagicMock()
|
||||||
|
geom.isEmpty.return_value = False
|
||||||
|
geom.area.return_value = 10000.0
|
||||||
|
feat = MagicMock()
|
||||||
|
feat.geometry.return_value = geom
|
||||||
|
layer.getFeatures.return_value = [feat]
|
||||||
|
return layer
|
||||||
|
|
||||||
|
@patch(f"{_MOD}.QgsGeometry")
|
||||||
|
@patch(f"{_MOD}._baue_pta_index")
|
||||||
|
@patch(f"{_MOD}._erstelle_ergebnis_layer")
|
||||||
|
@patch(f"qgis.core.QgsRectangle", create=True)
|
||||||
|
@patch(f"qgis.core.QgsPointXY", create=True)
|
||||||
|
def test_identischer_punkt_wird_gruen_gezaehlt(
|
||||||
|
self, mock_ptxy, mock_rect, mock_erstelle, mock_index, mock_geom_cls
|
||||||
|
):
|
||||||
|
"""PunktortTA exakt am Knickpunkt → identisch_count == 1."""
|
||||||
|
# Knickpunkt bei (100, 200)
|
||||||
|
knick = MagicMock()
|
||||||
|
knick.x.return_value = 100.0
|
||||||
|
knick.y.return_value = 200.0
|
||||||
|
|
||||||
|
# PunktortTA ebenfalls bei (100, 200) – exakter Treffer
|
||||||
|
pta_geom = MagicMock()
|
||||||
|
pta_geom.isEmpty.return_value = False
|
||||||
|
pta_pt = MagicMock()
|
||||||
|
pta_pt.x.return_value = 100.0
|
||||||
|
pta_pt.y.return_value = 200.0
|
||||||
|
pta_geom.asPoint.return_value = pta_pt
|
||||||
|
|
||||||
|
# Spatial-Index gibt Feature-ID 1 zurück
|
||||||
|
mock_idx = MagicMock()
|
||||||
|
mock_idx.intersects.return_value = [1]
|
||||||
|
mock_index.return_value = (mock_idx, {1: pta_geom})
|
||||||
|
|
||||||
|
# Ergebnis-Layer
|
||||||
|
pr = MagicMock()
|
||||||
|
pr.addFeatures.return_value = True
|
||||||
|
ergebnis = MagicMock()
|
||||||
|
ergebnis.dataProvider.return_value = pr
|
||||||
|
ergebnis.isValid.return_value = True
|
||||||
|
mock_erstelle.return_value = ergebnis
|
||||||
|
|
||||||
|
bror = self._make_bror_layer()
|
||||||
|
pta = self._make_pta_layer_mit_punkt(100.0, 200.0)
|
||||||
|
|
||||||
|
result = knickpunkt_pruefung(
|
||||||
|
bror, pta, None, knickpunkte=[knick]
|
||||||
|
)
|
||||||
|
_, identisch, kein_pta, _ = result
|
||||||
|
self.assertEqual(identisch, 1)
|
||||||
|
self.assertEqual(kein_pta, 0)
|
||||||
|
|
||||||
|
@patch(f"{_MOD}.QgsGeometry")
|
||||||
|
@patch(f"{_MOD}._baue_pta_index")
|
||||||
|
@patch(f"{_MOD}._erstelle_ergebnis_layer")
|
||||||
|
@patch(f"qgis.core.QgsRectangle", create=True)
|
||||||
|
@patch(f"qgis.core.QgsPointXY", create=True)
|
||||||
|
def test_kein_pta_an_knickpunkt_wird_rot_gezaehlt(
|
||||||
|
self, mock_ptxy, mock_rect, mock_erstelle, mock_index, mock_geom_cls
|
||||||
|
):
|
||||||
|
"""Kein PunktortTA am Knickpunkt → kein_pta_count == 1."""
|
||||||
|
knick = MagicMock()
|
||||||
|
knick.x.return_value = 100.0
|
||||||
|
knick.y.return_value = 200.0
|
||||||
|
|
||||||
|
# Spatial-Index gibt leere Liste zurück → kein Treffer
|
||||||
|
mock_idx = MagicMock()
|
||||||
|
mock_idx.intersects.return_value = []
|
||||||
|
mock_index.return_value = (mock_idx, {})
|
||||||
|
|
||||||
|
pr = MagicMock()
|
||||||
|
pr.addFeatures.return_value = True
|
||||||
|
ergebnis = MagicMock()
|
||||||
|
ergebnis.dataProvider.return_value = pr
|
||||||
|
ergebnis.isValid.return_value = True
|
||||||
|
mock_erstelle.return_value = ergebnis
|
||||||
|
|
||||||
|
bror = self._make_bror_layer()
|
||||||
|
pta = self._make_pta_layer_mit_punkt(999.0, 999.0) # weit weg
|
||||||
|
|
||||||
|
result = knickpunkt_pruefung(
|
||||||
|
bror, pta, None, knickpunkte=[knick]
|
||||||
|
)
|
||||||
|
_, identisch, kein_pta, _ = result
|
||||||
|
self.assertEqual(identisch, 0)
|
||||||
|
self.assertEqual(kein_pta, 1)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Tests: _baue_flst_vertex_set
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class TestBaueFlstVertexSet(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_none_layer_gibt_leeres_set(self):
|
||||||
|
from sn_verfahrensgebiet.functions.knickpunkt_pruefung import _baue_flst_vertex_set
|
||||||
|
result = _baue_flst_vertex_set(None, MagicMock())
|
||||||
|
self.assertEqual(result, set())
|
||||||
|
|
||||||
|
def test_layer_ohne_schnitt_mit_puffer_ignoriert(self):
|
||||||
|
from sn_verfahrensgebiet.functions.knickpunkt_pruefung import _baue_flst_vertex_set
|
||||||
|
layer = _make_layer()
|
||||||
|
geom = MagicMock()
|
||||||
|
geom.isEmpty.return_value = False
|
||||||
|
geom.intersects.return_value = False # kein Schnitt
|
||||||
|
feat = MagicMock()
|
||||||
|
feat.geometry.return_value = geom
|
||||||
|
layer.getFeatures.return_value = [feat]
|
||||||
|
buffer_geom = MagicMock()
|
||||||
|
result = _baue_flst_vertex_set(layer, buffer_geom)
|
||||||
|
self.assertEqual(result, set())
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
import sys
|
||||||
|
import types
|
||||||
|
import unittest
|
||||||
|
from types import SimpleNamespace
|
||||||
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
|
if "qgis" not in sys.modules:
|
||||||
|
qgis_mod = types.ModuleType("qgis")
|
||||||
|
qgis_utils = types.ModuleType("qgis.utils")
|
||||||
|
qgis_utils.iface = types.SimpleNamespace()
|
||||||
|
qgis_utils.plugins = {}
|
||||||
|
qgis_mod.utils = qgis_utils
|
||||||
|
sys.modules["qgis"] = qgis_mod
|
||||||
|
sys.modules["qgis.utils"] = qgis_utils
|
||||||
|
|
||||||
|
from sn_verfahrensgebiet.main import Verfahrensgebiet
|
||||||
|
|
||||||
|
|
||||||
|
class _DummyIface:
|
||||||
|
def __init__(self):
|
||||||
|
self.removed = []
|
||||||
|
|
||||||
|
def mainWindow(self):
|
||||||
|
return object()
|
||||||
|
|
||||||
|
def removeDockWidget(self, dock):
|
||||||
|
self.removed.append(dock)
|
||||||
|
|
||||||
|
|
||||||
|
class _DummyDock:
|
||||||
|
def __init__(self):
|
||||||
|
self.object_name = None
|
||||||
|
self.action = None
|
||||||
|
self.deleted = False
|
||||||
|
|
||||||
|
def setObjectName(self, name):
|
||||||
|
self.object_name = name
|
||||||
|
|
||||||
|
def deleteLater(self):
|
||||||
|
self.deleted = True
|
||||||
|
|
||||||
|
|
||||||
|
class TestPluginMain(unittest.TestCase):
|
||||||
|
|
||||||
|
@patch("sn_verfahrensgebiet.main.DataGrabber")
|
||||||
|
@patch("sn_verfahrensgebiet.main.Pruefmanager")
|
||||||
|
def test_init_sets_services_and_names(self, mock_pruefmanager, mock_data_grabber):
|
||||||
|
pm = object()
|
||||||
|
dg = object()
|
||||||
|
mock_pruefmanager.return_value = pm
|
||||||
|
mock_data_grabber.return_value = dg
|
||||||
|
|
||||||
|
plugin = Verfahrensgebiet(_DummyIface())
|
||||||
|
|
||||||
|
self.assertIs(plugin.pruefmanager, pm)
|
||||||
|
self.assertIs(plugin.data_grabber, dg)
|
||||||
|
self.assertEqual(plugin.plugin_name, "Verfahrensgebiet")
|
||||||
|
self.assertEqual(plugin.dock_name, "sn_dock_verfahrensgebiet")
|
||||||
|
|
||||||
|
@patch("sn_verfahrensgebiet.main.plugins", new_callable=dict)
|
||||||
|
def test_initGui_registers_action_when_basis_ui_exists(self, mock_plugins):
|
||||||
|
iface = _DummyIface()
|
||||||
|
plugin = Verfahrensgebiet(iface)
|
||||||
|
|
||||||
|
ui = SimpleNamespace(
|
||||||
|
add_action=Mock(return_value="action_obj"),
|
||||||
|
finalize_menu_and_toolbar=Mock(),
|
||||||
|
)
|
||||||
|
mock_plugins["sn_basis"] = SimpleNamespace(ui=ui)
|
||||||
|
|
||||||
|
plugin.initGui()
|
||||||
|
|
||||||
|
self.assertEqual(plugin.action, "action_obj")
|
||||||
|
ui.add_action.assert_called_once()
|
||||||
|
ui.finalize_menu_and_toolbar.assert_called_once()
|
||||||
|
|
||||||
|
@patch("sn_verfahrensgebiet.main.plugins", new_callable=dict)
|
||||||
|
@patch("sn_verfahrensgebiet.main.DockManager.show")
|
||||||
|
@patch("sn_verfahrensgebiet.main.DockWidget")
|
||||||
|
def test_run_creates_dock_and_marks_toolbar_active(self, mock_dockwidget_cls, mock_show, mock_plugins):
|
||||||
|
iface = _DummyIface()
|
||||||
|
plugin = Verfahrensgebiet(iface)
|
||||||
|
plugin.action = "action_obj"
|
||||||
|
|
||||||
|
dock = _DummyDock()
|
||||||
|
mock_dockwidget_cls.return_value = dock
|
||||||
|
|
||||||
|
ui = SimpleNamespace(set_active_plugin=Mock())
|
||||||
|
mock_plugins["sn_basis"] = SimpleNamespace(ui=ui)
|
||||||
|
|
||||||
|
plugin.run()
|
||||||
|
|
||||||
|
self.assertIs(plugin.dockwidget, dock)
|
||||||
|
self.assertEqual(dock.object_name, "sn_dock_verfahrensgebiet")
|
||||||
|
self.assertEqual(dock.action, "action_obj")
|
||||||
|
mock_show.assert_called_once_with(dock)
|
||||||
|
ui.set_active_plugin.assert_called_once_with("action_obj")
|
||||||
|
|
||||||
|
@patch("sn_verfahrensgebiet.main.plugins", new_callable=dict)
|
||||||
|
def test_unload_removes_dock_and_action(self, mock_plugins):
|
||||||
|
iface = _DummyIface()
|
||||||
|
plugin = Verfahrensgebiet(iface)
|
||||||
|
|
||||||
|
dock = _DummyDock()
|
||||||
|
plugin.dockwidget = dock
|
||||||
|
plugin.action = "action_obj"
|
||||||
|
|
||||||
|
ui = SimpleNamespace(remove_action=Mock())
|
||||||
|
mock_plugins["sn_basis"] = SimpleNamespace(ui=ui)
|
||||||
|
|
||||||
|
plugin.unload()
|
||||||
|
|
||||||
|
self.assertIn(dock, iface.removed)
|
||||||
|
self.assertTrue(dock.deleted)
|
||||||
|
self.assertIsNone(plugin.dockwidget)
|
||||||
|
ui.remove_action.assert_called_once_with("action_obj")
|
||||||
|
self.assertIsNone(plugin.action)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
import sys
|
||||||
|
import types
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
if "qgis" not in sys.modules:
|
||||||
|
qgis_mod = types.ModuleType("qgis")
|
||||||
|
qgis_utils = types.ModuleType("qgis.utils")
|
||||||
|
qgis_utils.iface = types.SimpleNamespace()
|
||||||
|
qgis_utils.plugins = {}
|
||||||
|
qgis_mod.utils = qgis_utils
|
||||||
|
sys.modules["qgis"] = qgis_mod
|
||||||
|
sys.modules["qgis.utils"] = qgis_utils
|
||||||
|
|
||||||
|
from sn_verfahrensgebiet.functions import verfahrensgebiet_alkis as vg
|
||||||
|
|
||||||
|
|
||||||
|
class _DummyMessageBox:
|
||||||
|
class StandardButton:
|
||||||
|
Yes = 1
|
||||||
|
No = 2
|
||||||
|
|
||||||
|
last_critical = None
|
||||||
|
next_reply = StandardButton.No
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def question(_parent, _title, _message, _buttons, _default):
|
||||||
|
return _DummyMessageBox.next_reply
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def critical(parent, title, message):
|
||||||
|
_DummyMessageBox.last_critical = (parent, title, message)
|
||||||
|
|
||||||
|
|
||||||
|
class _DummyProject:
|
||||||
|
def __init__(self, layers=None):
|
||||||
|
self._layers = layers or []
|
||||||
|
self.removed = []
|
||||||
|
|
||||||
|
def mapLayersByName(self, _name):
|
||||||
|
return self._layers
|
||||||
|
|
||||||
|
def removeMapLayer(self, layer_id):
|
||||||
|
self.removed.append(layer_id)
|
||||||
|
|
||||||
|
|
||||||
|
class _DummyExistingLayer:
|
||||||
|
def __init__(self, layer_id="existing"):
|
||||||
|
self._id = layer_id
|
||||||
|
|
||||||
|
def id(self):
|
||||||
|
return self._id
|
||||||
|
|
||||||
|
|
||||||
|
class _DummyGeom:
|
||||||
|
def __init__(self, empty=False):
|
||||||
|
self._empty = empty
|
||||||
|
|
||||||
|
def isEmpty(self):
|
||||||
|
return self._empty
|
||||||
|
|
||||||
|
|
||||||
|
class _DummyFeature:
|
||||||
|
def __init__(self, geom=None):
|
||||||
|
self._geom = geom or _DummyGeom()
|
||||||
|
|
||||||
|
def geometry(self):
|
||||||
|
return self._geom
|
||||||
|
|
||||||
|
|
||||||
|
class _DummyTempLayer:
|
||||||
|
def __init__(self, valid=True, features=None):
|
||||||
|
self._valid = valid
|
||||||
|
self._features = features or []
|
||||||
|
|
||||||
|
def isValid(self):
|
||||||
|
return self._valid
|
||||||
|
|
||||||
|
def getFeatures(self):
|
||||||
|
return self._features
|
||||||
|
|
||||||
|
def crs(self):
|
||||||
|
class _Crs:
|
||||||
|
@staticmethod
|
||||||
|
def authid():
|
||||||
|
return "EPSG:25833"
|
||||||
|
|
||||||
|
return _Crs()
|
||||||
|
|
||||||
|
|
||||||
|
class _DummyProvider:
|
||||||
|
def addAttributes(self, _attrs):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def addFeatures(self, _features):
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class _DummyMemoryLayer:
|
||||||
|
def __init__(self):
|
||||||
|
self._provider = _DummyProvider()
|
||||||
|
|
||||||
|
def dataProvider(self):
|
||||||
|
return self._provider
|
||||||
|
|
||||||
|
def updateFields(self):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def updateExtents(self):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class TestVerfahrensgebietAlkis(unittest.TestCase):
|
||||||
|
|
||||||
|
@patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.QgsProject.instance")
|
||||||
|
@patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.QMessageBox", new=_DummyMessageBox)
|
||||||
|
def test_check_existing_layer_first_load(self, mock_project_instance):
|
||||||
|
mock_project_instance.return_value = _DummyProject(layers=[])
|
||||||
|
|
||||||
|
layer, status = vg._check_existing_layer(tab_widget=None)
|
||||||
|
|
||||||
|
self.assertIsNone(layer)
|
||||||
|
self.assertEqual(status, vg.LoadStatus.FIRST)
|
||||||
|
|
||||||
|
@patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.QgsProject.instance")
|
||||||
|
@patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.QMessageBox", new=_DummyMessageBox)
|
||||||
|
def test_check_existing_layer_keep_existing(self, mock_project_instance):
|
||||||
|
existing = _DummyExistingLayer("L1")
|
||||||
|
mock_project_instance.return_value = _DummyProject(layers=[existing])
|
||||||
|
_DummyMessageBox.next_reply = _DummyMessageBox.StandardButton.No
|
||||||
|
|
||||||
|
layer, status = vg._check_existing_layer(tab_widget=None)
|
||||||
|
|
||||||
|
self.assertIs(layer, existing)
|
||||||
|
self.assertEqual(status, vg.LoadStatus.KEEP)
|
||||||
|
|
||||||
|
@patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.QMessageBox", new=_DummyMessageBox)
|
||||||
|
@patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.get_variable", return_value="")
|
||||||
|
def test_verfahrensgebiet_alkis_fails_when_verfahrensnummer_missing(self, _mock_get_var):
|
||||||
|
_DummyMessageBox.last_critical = None
|
||||||
|
|
||||||
|
layer, status = vg.verfahrensgebiet_alkis(tab_widget=None)
|
||||||
|
|
||||||
|
self.assertIsNone(layer)
|
||||||
|
self.assertEqual(status, vg.LoadStatus.NONE)
|
||||||
|
self.assertIsNotNone(_DummyMessageBox.last_critical)
|
||||||
|
|
||||||
|
@patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis._check_existing_layer", return_value=(None, vg.LoadStatus.FIRST))
|
||||||
|
@patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.QMessageBox", new=_DummyMessageBox)
|
||||||
|
@patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.get_variable", return_value="VKZ123")
|
||||||
|
@patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.QgsVectorLayer")
|
||||||
|
def test_verfahrensgebiet_alkis_fails_on_invalid_wfs_layer(self, mock_qgsvectorlayer, _mock_get_var, _mock_check):
|
||||||
|
_DummyMessageBox.last_critical = None
|
||||||
|
mock_qgsvectorlayer.return_value = _DummyTempLayer(valid=False)
|
||||||
|
|
||||||
|
layer, status = vg.verfahrensgebiet_alkis(tab_widget=None)
|
||||||
|
|
||||||
|
self.assertIsNone(layer)
|
||||||
|
self.assertEqual(status, vg.LoadStatus.NONE)
|
||||||
|
self.assertIsNotNone(_DummyMessageBox.last_critical)
|
||||||
|
|
||||||
|
@patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.QgsFeature")
|
||||||
|
@patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.QgsGeometry.unaryUnion", return_value=_DummyGeom(empty=False))
|
||||||
|
@patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis._check_existing_layer", return_value=(None, vg.LoadStatus.FIRST))
|
||||||
|
@patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.get_variable", return_value="VKZ123")
|
||||||
|
@patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.QgsVectorLayer")
|
||||||
|
@patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.QMessageBox", new=_DummyMessageBox)
|
||||||
|
def test_verfahrensgebiet_alkis_success_returns_memory_layer(self, mock_qgsvectorlayer, _mock_get_var, _mock_check, _mock_union, mock_feature_cls):
|
||||||
|
temp_layer = _DummyTempLayer(valid=True, features=[_DummyFeature()])
|
||||||
|
memory_layer = _DummyMemoryLayer()
|
||||||
|
mock_qgsvectorlayer.side_effect = [temp_layer, memory_layer]
|
||||||
|
|
||||||
|
mock_feature = mock_feature_cls.return_value
|
||||||
|
mock_feature.setGeometry.return_value = None
|
||||||
|
mock_feature.setAttributes.return_value = None
|
||||||
|
|
||||||
|
layer, status = vg.verfahrensgebiet_alkis(tab_widget=None)
|
||||||
|
|
||||||
|
self.assertIs(layer, memory_layer)
|
||||||
|
self.assertEqual(status, vg.LoadStatus.FIRST)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,670 @@
|
|||||||
|
"""sn_verfahrensgebiet/tests/test_verfahrensgebiet_alkis_komplett.py
|
||||||
|
|
||||||
|
Unit-Tests für :mod:`sn_verfahrensgebiet.functions.verfahrensgebiet_alkis_komplett`.
|
||||||
|
|
||||||
|
Getestete Szenarien:
|
||||||
|
|
||||||
|
Erfolgsfall (Flächenübereinstimmung):
|
||||||
|
- Vollständiger Workflow → enriched Layer + flst_layer + bror_layer + LoadStatus.FIRST
|
||||||
|
- VKZ aus Projektvariable wird als Feld gesetzt (wenn vorhanden)
|
||||||
|
- Kein VKZ in Projektvariable → Feld wird nicht gesetzt (kein Fehler)
|
||||||
|
|
||||||
|
Abbruchpfade:
|
||||||
|
- verfahrensgebiet_alkis gibt LoadStatus.NONE → (None, None, None, NONE)
|
||||||
|
- flurstueck_alkis gibt LoadStatus.NONE → (None, None, None, NONE)
|
||||||
|
- Keine Flurstücke selektiert → (None, flst_layer, None, NONE) + Warnung
|
||||||
|
- erstelle_verfahrensgebiet_aus_auswahl liefert ungültigen Layer → (None, flst_layer, None, NONE)
|
||||||
|
- Progress-Abbruch nach Schritt 1 → sofortiger sauberer Rückgabestatus
|
||||||
|
|
||||||
|
Flächenvergleich:
|
||||||
|
- Übereinstimmung → show_info_dialog, keine Detailprüfung
|
||||||
|
- Abweichung + abbrechen → kein Detailprüfungs-Aufruf
|
||||||
|
- Abweichung + als_vg_laden → BROR umbenannt, als enriched_layer zurückgegeben
|
||||||
|
- Abweichung + detailpruefung → lade_punktort_ta + knickpunkt_pruefung aufgerufen
|
||||||
|
|
||||||
|
Cleanup:
|
||||||
|
- BROR-Layer wird bei Erfolg NICHT entfernt
|
||||||
|
- BROR-Layer wird bei Fehler entfernt (wenn in diesem Lauf hinzugefügt)
|
||||||
|
- TempVerfahrensgebiet wird stets entfernt
|
||||||
|
|
||||||
|
Hilfsfunktionen:
|
||||||
|
- _select_by_location: processing-Fallback liefert 0 → keine Exception
|
||||||
|
- _fuege_vkz_feld_hinzu: leeres VKZ wird ignoriert
|
||||||
|
- _vergleiche_flaechen: korrekte Toleranzlogik
|
||||||
|
- _lese_flaechen_toleranz: Default 0.0, parst Projektvariable
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import types
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import patch, MagicMock
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 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.verfahrensgebiet_alkis import LoadStatus
|
||||||
|
from sn_verfahrensgebiet.functions.verfahrensgebiet_alkis_komplett import (
|
||||||
|
verfahrensgebiet_alkis_komplett,
|
||||||
|
_select_by_location,
|
||||||
|
_fuege_vkz_feld_hinzu,
|
||||||
|
_vergleiche_flaechen,
|
||||||
|
_lese_flaechen_toleranz,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Dummies
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class _DummyRect:
|
||||||
|
def xMinimum(self): return 414000.0
|
||||||
|
def yMinimum(self): return 5657000.0
|
||||||
|
def xMaximum(self): return 415000.0
|
||||||
|
def yMaximum(self): return 5658000.0
|
||||||
|
|
||||||
|
|
||||||
|
def _make_layer(valid=True, name="Layer", lid="L1", feature_count=1, area=1000.0):
|
||||||
|
"""Erzeugt einen konfigurierten Mock-Layer mit getFeatures-Support."""
|
||||||
|
layer = MagicMock()
|
||||||
|
layer.isValid.return_value = valid
|
||||||
|
layer.id.return_value = lid
|
||||||
|
layer.name.return_value = name
|
||||||
|
layer.extent.return_value = _DummyRect()
|
||||||
|
layer.featureCount.return_value = feature_count
|
||||||
|
layer.selectedFeatureCount.return_value = feature_count
|
||||||
|
|
||||||
|
mock_geom = MagicMock()
|
||||||
|
mock_geom.isEmpty.return_value = False
|
||||||
|
mock_geom.area.return_value = area
|
||||||
|
mock_geom.boundary.return_value = MagicMock(isEmpty=lambda: True)
|
||||||
|
mock_feat = MagicMock()
|
||||||
|
mock_feat.geometry.return_value = mock_geom
|
||||||
|
layer.getFeatures.return_value = [mock_feat]
|
||||||
|
|
||||||
|
mock_crs = MagicMock()
|
||||||
|
mock_crs.authid.return_value = "EPSG:25833"
|
||||||
|
layer.crs.return_value = mock_crs
|
||||||
|
return layer
|
||||||
|
|
||||||
|
|
||||||
|
class _DummyProject:
|
||||||
|
def __init__(self):
|
||||||
|
self.added = []
|
||||||
|
self.removed = []
|
||||||
|
self._layers_by_name: dict = {}
|
||||||
|
|
||||||
|
def addMapLayer(self, layer):
|
||||||
|
self.added.append(layer)
|
||||||
|
|
||||||
|
def removeMapLayer(self, lid):
|
||||||
|
self.removed.append(lid)
|
||||||
|
|
||||||
|
def mapLayersByName(self, name):
|
||||||
|
return self._layers_by_name.get(name, [])
|
||||||
|
|
||||||
|
def instance(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
def _make_progress_mock(canceled=False):
|
||||||
|
prog = MagicMock()
|
||||||
|
prog.is_canceled.return_value = canceled
|
||||||
|
return prog
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Patch-Konstanten
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
_MOD = "sn_verfahrensgebiet.functions.verfahrensgebiet_alkis_komplett"
|
||||||
|
_PATCH_VG_ALKIS = f"{_MOD}.verfahrensgebiet_alkis"
|
||||||
|
_PATCH_GEM = f"{_MOD}.gemarkung_schluessel_und_namen_von_vg_geometrie"
|
||||||
|
_PATCH_FLST = f"{_MOD}.flurstueck_alkis"
|
||||||
|
_PATCH_SELECT = f"{_MOD}._select_by_location"
|
||||||
|
_PATCH_ERSTELLE = f"{_MOD}.erstelle_verfahrensgebiet_aus_auswahl"
|
||||||
|
_PATCH_VKZ = f"{_MOD}._fuege_vkz_feld_hinzu"
|
||||||
|
_PATCH_PROJECT = f"{_MOD}.QgsProject"
|
||||||
|
_PATCH_MSGBOX = f"{_MOD}.QMessageBox"
|
||||||
|
_PATCH_WARNING = f"{_MOD}.warning"
|
||||||
|
_PATCH_GET_VAR = f"{_MOD}.get_variable"
|
||||||
|
_PATCH_PROGRESS = f"{_MOD}.create_progress_dialog"
|
||||||
|
_PATCH_VERGLEICHE = f"{_MOD}._vergleiche_flaechen"
|
||||||
|
_PATCH_SHOW_INFO = f"{_MOD}.show_info_dialog"
|
||||||
|
_PATCH_ASK_DIALOG = f"{_MOD}.ask_detailpruefung_oder_vg_laden"
|
||||||
|
_PATCH_GP_ALKIS = f"{_MOD}.lade_punktort_ta"
|
||||||
|
_PATCH_KNICK = f"{_MOD}.knickpunkt_pruefung"
|
||||||
|
_PATCH_EXTRAHIERE = f"{_MOD}._extrahiere_knickpunkte"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Tests: _select_by_location
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class TestSelectByLocation(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_processing_not_available_returns_zero(self):
|
||||||
|
flst, vg = MagicMock(), MagicMock()
|
||||||
|
with patch.dict("sys.modules", {"processing": None}):
|
||||||
|
result = _select_by_location(flst, vg)
|
||||||
|
self.assertEqual(result, 0)
|
||||||
|
|
||||||
|
def test_processing_success_returns_count(self):
|
||||||
|
flst = MagicMock()
|
||||||
|
flst.selectedFeatureCount.return_value = 7
|
||||||
|
vg = MagicMock()
|
||||||
|
mock_proc = MagicMock()
|
||||||
|
with patch.dict("sys.modules", {"processing": mock_proc}):
|
||||||
|
result = _select_by_location(flst, vg)
|
||||||
|
self.assertEqual(result, 7)
|
||||||
|
|
||||||
|
def test_processing_exception_returns_zero(self):
|
||||||
|
flst, vg = MagicMock(), MagicMock()
|
||||||
|
mock_proc = MagicMock()
|
||||||
|
mock_proc.run.side_effect = RuntimeError("Kein QGIS")
|
||||||
|
with patch.dict("sys.modules", {"processing": mock_proc}):
|
||||||
|
result = _select_by_location(flst, vg)
|
||||||
|
self.assertEqual(result, 0)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Tests: _fuege_vkz_feld_hinzu
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class TestFuegeVkzFeldHinzu(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_empty_vkz_does_nothing(self):
|
||||||
|
layer = MagicMock()
|
||||||
|
_fuege_vkz_feld_hinzu(layer, "")
|
||||||
|
layer.dataProvider.assert_not_called()
|
||||||
|
|
||||||
|
def test_none_vkz_does_nothing(self):
|
||||||
|
layer = MagicMock()
|
||||||
|
_fuege_vkz_feld_hinzu(layer, None)
|
||||||
|
layer.dataProvider.assert_not_called()
|
||||||
|
|
||||||
|
def test_vkz_field_added(self):
|
||||||
|
pr = MagicMock()
|
||||||
|
pr.addAttributes.return_value = True
|
||||||
|
pr.changeAttributeValues.return_value = True
|
||||||
|
layer = MagicMock()
|
||||||
|
layer.dataProvider.return_value = pr
|
||||||
|
layer.fields.return_value.indexFromName.return_value = 2
|
||||||
|
layer.getFeatures.return_value = []
|
||||||
|
_fuege_vkz_feld_hinzu(layer, "VF-2024-001")
|
||||||
|
pr.addAttributes.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Tests: _vergleiche_flaechen / _lese_flaechen_toleranz
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class TestFlaechenvergleich(unittest.TestCase):
|
||||||
|
|
||||||
|
def _layer(self, area: float):
|
||||||
|
layer = MagicMock()
|
||||||
|
geom = MagicMock()
|
||||||
|
geom.isEmpty.return_value = False
|
||||||
|
geom.area.return_value = area
|
||||||
|
feat = MagicMock()
|
||||||
|
feat.geometry.return_value = geom
|
||||||
|
layer.getFeatures.return_value = [feat]
|
||||||
|
return layer
|
||||||
|
|
||||||
|
def test_gleiche_flaeche_stimmt_ueberein(self):
|
||||||
|
self.assertTrue(_vergleiche_flaechen(self._layer(1234.5), self._layer(1234.5), 0.0)[2])
|
||||||
|
|
||||||
|
def test_verschiedene_flaeche_stimmt_nicht_ueberein(self):
|
||||||
|
self.assertFalse(_vergleiche_flaechen(self._layer(1234.5), self._layer(1200.0), 0.0)[2])
|
||||||
|
|
||||||
|
def test_differenz_innerhalb_toleranz(self):
|
||||||
|
self.assertTrue(_vergleiche_flaechen(self._layer(1234.5), self._layer(1234.8), 1.0)[2])
|
||||||
|
|
||||||
|
def test_differenz_ausserhalb_toleranz(self):
|
||||||
|
self.assertFalse(_vergleiche_flaechen(self._layer(1234.5), self._layer(1236.0), 1.0)[2])
|
||||||
|
|
||||||
|
def test_flaechen_werte_korrekt(self):
|
||||||
|
bror_f, vg_f, _ = _vergleiche_flaechen(self._layer(800.0), self._layer(750.0))
|
||||||
|
self.assertAlmostEqual(bror_f, 800.0)
|
||||||
|
self.assertAlmostEqual(vg_f, 750.0)
|
||||||
|
|
||||||
|
@patch(f"{_MOD}.get_variable", return_value=None)
|
||||||
|
def test_lese_toleranz_default_ist_null(self, _):
|
||||||
|
self.assertEqual(_lese_flaechen_toleranz(), 0.0)
|
||||||
|
|
||||||
|
@patch(f"{_MOD}.get_variable", return_value="2.5")
|
||||||
|
def test_lese_toleranz_parst_string(self, _):
|
||||||
|
self.assertAlmostEqual(_lese_flaechen_toleranz(), 2.5)
|
||||||
|
|
||||||
|
@patch(f"{_MOD}.get_variable", return_value="ungueltig")
|
||||||
|
def test_lese_toleranz_ungueltig_fallback_null(self, _):
|
||||||
|
self.assertEqual(_lese_flaechen_toleranz(), 0.0)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Tests: verfahrensgebiet_alkis_komplett (Hauptworkflow)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class TestVerfahrensgebietAlkisKomplett(unittest.TestCase):
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# Abbruchpfade
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
|
||||||
|
@patch(_PATCH_PROGRESS)
|
||||||
|
@patch(_PATCH_PROJECT)
|
||||||
|
@patch(_PATCH_VG_ALKIS)
|
||||||
|
def test_vg_alkis_none_aborts(self, mock_vg, mock_proj, mock_prog):
|
||||||
|
mock_proj.instance.return_value = _DummyProject()
|
||||||
|
mock_vg.return_value = (None, LoadStatus.NONE, [])
|
||||||
|
mock_prog.return_value = _make_progress_mock()
|
||||||
|
|
||||||
|
enriched, flst, bror, status = verfahrensgebiet_alkis_komplett(None)
|
||||||
|
|
||||||
|
self.assertIsNone(enriched)
|
||||||
|
self.assertIsNone(flst)
|
||||||
|
self.assertIsNone(bror)
|
||||||
|
self.assertEqual(status, LoadStatus.NONE)
|
||||||
|
|
||||||
|
@patch(_PATCH_PROGRESS)
|
||||||
|
@patch(_PATCH_PROJECT)
|
||||||
|
@patch(_PATCH_GEM)
|
||||||
|
@patch(_PATCH_VG_ALKIS)
|
||||||
|
def test_flst_none_aborts(self, mock_vg, mock_gem, mock_proj, mock_prog):
|
||||||
|
mock_proj.instance.return_value = _DummyProject()
|
||||||
|
vg_layer = _make_layer(name="BauRaumOrdnungsrecht (ALKIS)")
|
||||||
|
mock_vg.return_value = (vg_layer, LoadStatus.FIRST, [])
|
||||||
|
mock_gem.return_value = [("1234", "Muster")]
|
||||||
|
mock_prog.return_value = _make_progress_mock()
|
||||||
|
|
||||||
|
with patch(_PATCH_FLST, return_value=(None, LoadStatus.NONE)):
|
||||||
|
enriched, flst, bror, status = verfahrensgebiet_alkis_komplett(None)
|
||||||
|
|
||||||
|
self.assertIsNone(enriched)
|
||||||
|
self.assertIsNone(flst)
|
||||||
|
self.assertEqual(status, LoadStatus.NONE)
|
||||||
|
|
||||||
|
@patch(_PATCH_PROGRESS)
|
||||||
|
@patch(_PATCH_MSGBOX)
|
||||||
|
@patch(_PATCH_SELECT)
|
||||||
|
@patch(_PATCH_PROJECT)
|
||||||
|
@patch(_PATCH_GEM)
|
||||||
|
@patch(_PATCH_FLST)
|
||||||
|
@patch(_PATCH_VG_ALKIS)
|
||||||
|
def test_no_selection_returns_none_enriched(
|
||||||
|
self, mock_vg, mock_flst, mock_gem, mock_proj, mock_select, mock_msgbox, mock_prog
|
||||||
|
):
|
||||||
|
mock_proj.instance.return_value = _DummyProject()
|
||||||
|
vg_layer = _make_layer(name="BauRaumOrdnungsrecht (ALKIS)")
|
||||||
|
flst_layer = _make_layer(name="Flurstücke")
|
||||||
|
mock_vg.return_value = (vg_layer, LoadStatus.FIRST, [])
|
||||||
|
mock_gem.return_value = []
|
||||||
|
mock_flst.return_value = (flst_layer, LoadStatus.FIRST)
|
||||||
|
mock_select.return_value = 0
|
||||||
|
mock_prog.return_value = _make_progress_mock()
|
||||||
|
|
||||||
|
enriched, flst, bror, status = verfahrensgebiet_alkis_komplett(None)
|
||||||
|
|
||||||
|
self.assertIsNone(enriched)
|
||||||
|
self.assertIs(flst, flst_layer)
|
||||||
|
self.assertIsNone(bror)
|
||||||
|
self.assertEqual(status, LoadStatus.NONE)
|
||||||
|
mock_msgbox.warning.assert_called_once()
|
||||||
|
|
||||||
|
@patch(_PATCH_PROGRESS)
|
||||||
|
@patch(_PATCH_MSGBOX)
|
||||||
|
@patch(_PATCH_ERSTELLE)
|
||||||
|
@patch(_PATCH_SELECT)
|
||||||
|
@patch(_PATCH_PROJECT)
|
||||||
|
@patch(_PATCH_GEM)
|
||||||
|
@patch(_PATCH_FLST)
|
||||||
|
@patch(_PATCH_VG_ALKIS)
|
||||||
|
def test_invalid_enriched_layer_returns_none(
|
||||||
|
self, mock_vg, mock_flst, mock_gem, mock_proj, mock_select, mock_erstelle, mock_msgbox, mock_prog
|
||||||
|
):
|
||||||
|
mock_proj.instance.return_value = _DummyProject()
|
||||||
|
vg_layer = _make_layer(name="BauRaumOrdnungsrecht (ALKIS)")
|
||||||
|
flst_layer = _make_layer(name="Flurstücke")
|
||||||
|
bad_enriched = _make_layer(valid=False)
|
||||||
|
|
||||||
|
mock_vg.return_value = (vg_layer, LoadStatus.FIRST, [])
|
||||||
|
mock_gem.return_value = []
|
||||||
|
mock_flst.return_value = (flst_layer, LoadStatus.FIRST)
|
||||||
|
mock_select.return_value = 5
|
||||||
|
mock_erstelle.return_value = (bad_enriched, 0, 5)
|
||||||
|
mock_prog.return_value = _make_progress_mock()
|
||||||
|
|
||||||
|
enriched, flst, bror, status = verfahrensgebiet_alkis_komplett(None)
|
||||||
|
|
||||||
|
self.assertIsNone(enriched)
|
||||||
|
self.assertIs(flst, flst_layer)
|
||||||
|
self.assertIsNone(bror)
|
||||||
|
self.assertEqual(status, LoadStatus.NONE)
|
||||||
|
mock_msgbox.critical.assert_called_once()
|
||||||
|
|
||||||
|
@patch(_PATCH_PROGRESS)
|
||||||
|
@patch(_PATCH_PROJECT)
|
||||||
|
@patch(_PATCH_VG_ALKIS)
|
||||||
|
def test_progress_cancel_after_step1_returns_none(self, mock_vg, mock_proj, mock_prog):
|
||||||
|
"""Abbruch nach Schritt 1 → sofortiger sauberer Rückgabestatus."""
|
||||||
|
mock_proj.instance.return_value = _DummyProject()
|
||||||
|
vg_layer = _make_layer(name="BauRaumOrdnungsrecht (ALKIS)")
|
||||||
|
mock_vg.return_value = (vg_layer, LoadStatus.FIRST, [])
|
||||||
|
|
||||||
|
prog = MagicMock()
|
||||||
|
prog.is_canceled.side_effect = [False, True] # 1. Aufruf False, 2. True
|
||||||
|
mock_prog.return_value = prog
|
||||||
|
|
||||||
|
enriched, flst, bror, status = verfahrensgebiet_alkis_komplett(None)
|
||||||
|
|
||||||
|
self.assertIsNone(enriched)
|
||||||
|
self.assertEqual(status, LoadStatus.NONE)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# Flächenvergleich – Übereinstimmung
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
|
||||||
|
@patch(_PATCH_PROGRESS)
|
||||||
|
@patch(_PATCH_SHOW_INFO)
|
||||||
|
@patch(_PATCH_VERGLEICHE)
|
||||||
|
@patch(_PATCH_VKZ)
|
||||||
|
@patch(_PATCH_GET_VAR)
|
||||||
|
@patch(_PATCH_ERSTELLE)
|
||||||
|
@patch(_PATCH_SELECT)
|
||||||
|
@patch(_PATCH_PROJECT)
|
||||||
|
@patch(_PATCH_GEM)
|
||||||
|
@patch(_PATCH_FLST)
|
||||||
|
@patch(_PATCH_VG_ALKIS)
|
||||||
|
def test_success_flaechen_match_zeigt_info(
|
||||||
|
self, mock_vg, mock_flst, mock_gem, mock_proj, mock_select,
|
||||||
|
mock_erstelle, mock_get_var, mock_vkz, mock_vergl, mock_info, mock_prog,
|
||||||
|
):
|
||||||
|
"""Flächenübereinstimmung → show_info_dialog mit erwartetem Text."""
|
||||||
|
proj_inst = _DummyProject()
|
||||||
|
mock_proj.instance.return_value = proj_inst
|
||||||
|
|
||||||
|
vg_layer = _make_layer(name="BauRaumOrdnungsrecht (ALKIS)")
|
||||||
|
flst_layer = _make_layer(name="Flurstücke", lid="FLST1")
|
||||||
|
enriched = _make_layer(name="Verfahrensgebiet", lid="VG_ENRICHED", valid=True)
|
||||||
|
|
||||||
|
mock_vg.return_value = (vg_layer, LoadStatus.FIRST, [])
|
||||||
|
mock_gem.return_value = [("1234", "Muster")]
|
||||||
|
mock_flst.return_value = (flst_layer, LoadStatus.FIRST)
|
||||||
|
mock_select.return_value = 3
|
||||||
|
mock_erstelle.return_value = (enriched, 3, 0)
|
||||||
|
mock_get_var.return_value = "VF-2024-001"
|
||||||
|
mock_vergl.return_value = (1234.5, 1234.5, True)
|
||||||
|
mock_prog.return_value = _make_progress_mock()
|
||||||
|
|
||||||
|
result_enriched, result_flst, result_bror, status = verfahrensgebiet_alkis_komplett(None)
|
||||||
|
|
||||||
|
self.assertIs(result_enriched, enriched)
|
||||||
|
self.assertIs(result_flst, flst_layer)
|
||||||
|
self.assertIs(result_bror, vg_layer)
|
||||||
|
self.assertEqual(status, LoadStatus.FIRST)
|
||||||
|
mock_vkz.assert_called_once_with(enriched, "VF-2024-001")
|
||||||
|
mock_info.assert_called_once()
|
||||||
|
self.assertIn("erwarteter Geometrie", mock_info.call_args[0][1])
|
||||||
|
|
||||||
|
@patch(_PATCH_PROGRESS)
|
||||||
|
@patch(_PATCH_SHOW_INFO)
|
||||||
|
@patch(_PATCH_VERGLEICHE)
|
||||||
|
@patch(_PATCH_VKZ)
|
||||||
|
@patch(_PATCH_GET_VAR)
|
||||||
|
@patch(_PATCH_ERSTELLE)
|
||||||
|
@patch(_PATCH_SELECT)
|
||||||
|
@patch(_PATCH_PROJECT)
|
||||||
|
@patch(_PATCH_GEM)
|
||||||
|
@patch(_PATCH_FLST)
|
||||||
|
@patch(_PATCH_VG_ALKIS)
|
||||||
|
def test_no_vkz_variable_passes_empty_string(
|
||||||
|
self, mock_vg, mock_flst, mock_gem, mock_proj, mock_select,
|
||||||
|
mock_erstelle, mock_get_var, mock_vkz, mock_vergl, mock_info, mock_prog,
|
||||||
|
):
|
||||||
|
mock_proj.instance.return_value = _DummyProject()
|
||||||
|
vg_layer = _make_layer(name="BauRaumOrdnungsrecht (ALKIS)")
|
||||||
|
flst_layer = _make_layer(name="Flurstücke")
|
||||||
|
enriched = _make_layer(name="Verfahrensgebiet", valid=True)
|
||||||
|
|
||||||
|
mock_vg.return_value = (vg_layer, LoadStatus.FIRST, [])
|
||||||
|
mock_gem.return_value = []
|
||||||
|
mock_flst.return_value = (flst_layer, LoadStatus.FIRST)
|
||||||
|
mock_select.return_value = 2
|
||||||
|
mock_erstelle.return_value = (enriched, 2, 0)
|
||||||
|
mock_get_var.return_value = ""
|
||||||
|
mock_vergl.return_value = (1000.0, 1000.0, True)
|
||||||
|
mock_prog.return_value = _make_progress_mock()
|
||||||
|
|
||||||
|
verfahrensgebiet_alkis_komplett(None)
|
||||||
|
|
||||||
|
mock_vkz.assert_called_once_with(enriched, "")
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# Flächenvergleich – Abweichung + Abbrechen
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
|
||||||
|
@patch(_PATCH_PROGRESS)
|
||||||
|
@patch(_PATCH_KNICK)
|
||||||
|
@patch(_PATCH_ASK_DIALOG)
|
||||||
|
@patch(_PATCH_VERGLEICHE)
|
||||||
|
@patch(_PATCH_VKZ)
|
||||||
|
@patch(_PATCH_GET_VAR)
|
||||||
|
@patch(_PATCH_ERSTELLE)
|
||||||
|
@patch(_PATCH_SELECT)
|
||||||
|
@patch(_PATCH_PROJECT)
|
||||||
|
@patch(_PATCH_GEM)
|
||||||
|
@patch(_PATCH_FLST)
|
||||||
|
@patch(_PATCH_VG_ALKIS)
|
||||||
|
def test_flaechen_abweichung_abbrechen_kein_detailcheck(
|
||||||
|
self, mock_vg, mock_flst, mock_gem, mock_proj, mock_select,
|
||||||
|
mock_erstelle, mock_get_var, mock_vkz, mock_vergl, mock_ask, mock_knick, mock_prog,
|
||||||
|
):
|
||||||
|
"""Abweichung + Abbrechen → knickpunkt_pruefung wird NICHT aufgerufen."""
|
||||||
|
mock_proj.instance.return_value = _DummyProject()
|
||||||
|
vg_layer = _make_layer(name="BauRaumOrdnungsrecht (ALKIS)")
|
||||||
|
flst_layer = _make_layer(name="Flurstücke")
|
||||||
|
enriched = _make_layer(name="Verfahrensgebiet", valid=True)
|
||||||
|
|
||||||
|
mock_vg.return_value = (vg_layer, LoadStatus.FIRST, [])
|
||||||
|
mock_gem.return_value = []
|
||||||
|
mock_flst.return_value = (flst_layer, LoadStatus.FIRST)
|
||||||
|
mock_select.return_value = 3
|
||||||
|
mock_erstelle.return_value = (enriched, 3, 0)
|
||||||
|
mock_get_var.return_value = ""
|
||||||
|
mock_vergl.return_value = (1000.0, 800.0, False)
|
||||||
|
mock_ask.return_value = "abbrechen"
|
||||||
|
mock_prog.return_value = _make_progress_mock()
|
||||||
|
|
||||||
|
result_enriched, _, _, status = verfahrensgebiet_alkis_komplett(None)
|
||||||
|
|
||||||
|
mock_knick.assert_not_called()
|
||||||
|
self.assertIs(result_enriched, enriched)
|
||||||
|
self.assertEqual(status, LoadStatus.FIRST)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# Flächenvergleich – Abweichung + Als VG laden
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
|
||||||
|
@patch(_PATCH_PROGRESS)
|
||||||
|
@patch(_PATCH_KNICK)
|
||||||
|
@patch(_PATCH_ASK_DIALOG)
|
||||||
|
@patch(_PATCH_VERGLEICHE)
|
||||||
|
@patch(_PATCH_VKZ)
|
||||||
|
@patch(_PATCH_GET_VAR)
|
||||||
|
@patch(_PATCH_ERSTELLE)
|
||||||
|
@patch(_PATCH_SELECT)
|
||||||
|
@patch(_PATCH_PROJECT)
|
||||||
|
@patch(_PATCH_GEM)
|
||||||
|
@patch(_PATCH_FLST)
|
||||||
|
@patch(_PATCH_VG_ALKIS)
|
||||||
|
def test_flaechen_abweichung_als_vg_laden(
|
||||||
|
self, mock_vg, mock_flst, mock_gem, mock_proj, mock_select,
|
||||||
|
mock_erstelle, mock_get_var, mock_vkz, mock_vergl, mock_ask, mock_knick, mock_prog,
|
||||||
|
):
|
||||||
|
"""Abweichung + Als VG laden → BROR-Layer wird umbenannt und als
|
||||||
|
enriched_layer zurückgegeben; flst_layer und bror_layer sind None."""
|
||||||
|
proj_inst = _DummyProject()
|
||||||
|
mock_proj.instance.return_value = proj_inst
|
||||||
|
|
||||||
|
vg_layer = _make_layer(name="BauRaumOrdnungsrecht (ALKIS)", lid="BROR_VG")
|
||||||
|
flst_layer = _make_layer(name="Flurstücke")
|
||||||
|
enriched = _make_layer(name="Verfahrensgebiet", valid=True)
|
||||||
|
|
||||||
|
mock_vg.return_value = (vg_layer, LoadStatus.FIRST, [])
|
||||||
|
mock_gem.return_value = []
|
||||||
|
mock_flst.return_value = (flst_layer, LoadStatus.FIRST)
|
||||||
|
mock_select.return_value = 3
|
||||||
|
mock_erstelle.return_value = (enriched, 3, 0)
|
||||||
|
mock_get_var.return_value = ""
|
||||||
|
mock_vergl.return_value = (1000.0, 800.0, False)
|
||||||
|
mock_ask.return_value = "als_vg_laden"
|
||||||
|
mock_prog.return_value = _make_progress_mock()
|
||||||
|
|
||||||
|
result_enriched, result_flst, result_bror, status = verfahrensgebiet_alkis_komplett(None)
|
||||||
|
|
||||||
|
mock_knick.assert_not_called()
|
||||||
|
# BROR-Layer wird als enriched_layer zurückgegeben
|
||||||
|
self.assertIs(result_enriched, vg_layer)
|
||||||
|
self.assertIsNone(result_flst)
|
||||||
|
self.assertIsNone(result_bror)
|
||||||
|
self.assertEqual(status, LoadStatus.FIRST)
|
||||||
|
# Name wurde auf VERF_LAYER_NAME geändert
|
||||||
|
vg_layer.setName.assert_called()
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# Flächenvergleich – Abweichung + Detailprüfung
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
|
||||||
|
@patch(_PATCH_PROGRESS)
|
||||||
|
@patch(_PATCH_SHOW_INFO)
|
||||||
|
@patch(_PATCH_EXTRAHIERE)
|
||||||
|
@patch(_PATCH_KNICK)
|
||||||
|
@patch(_PATCH_GP_ALKIS)
|
||||||
|
@patch(_PATCH_ASK_DIALOG)
|
||||||
|
@patch(_PATCH_VERGLEICHE)
|
||||||
|
@patch(_PATCH_VKZ)
|
||||||
|
@patch(_PATCH_GET_VAR)
|
||||||
|
@patch(_PATCH_ERSTELLE)
|
||||||
|
@patch(_PATCH_SELECT)
|
||||||
|
@patch(_PATCH_PROJECT)
|
||||||
|
@patch(_PATCH_GEM)
|
||||||
|
@patch(_PATCH_FLST)
|
||||||
|
@patch(_PATCH_VG_ALKIS)
|
||||||
|
def test_flaechen_abweichung_detailpruefung_startet_knickpruefung(
|
||||||
|
self, mock_vg, mock_flst, mock_gem, mock_proj, mock_select,
|
||||||
|
mock_erstelle, mock_get_var, mock_vkz, mock_vergl, mock_ask,
|
||||||
|
mock_gp, mock_knick, mock_extrahiere, mock_info, mock_prog,
|
||||||
|
):
|
||||||
|
"""Abweichung + Detailprüfung → lade_punktort_ta + knickpunkt_pruefung
|
||||||
|
aufgerufen, Ergebnislayer ins Projekt, Abschlussmeldung mit Zählung."""
|
||||||
|
proj_inst = _DummyProject()
|
||||||
|
mock_proj.instance.return_value = proj_inst
|
||||||
|
|
||||||
|
vg_layer = _make_layer(name="BauRaumOrdnungsrecht (ALKIS)")
|
||||||
|
flst_layer = _make_layer(name="Flurstücke")
|
||||||
|
enriched = _make_layer(name="Verfahrensgebiet", valid=True)
|
||||||
|
punktort_layer = _make_layer(name="PunktortTA", lid="PTA_L")
|
||||||
|
ergebnis_layer = _make_layer(name="Knickpunktprüfung", lid="KNICK_L")
|
||||||
|
|
||||||
|
mock_vg.return_value = (vg_layer, LoadStatus.FIRST, [])
|
||||||
|
mock_gem.return_value = []
|
||||||
|
mock_flst.return_value = (flst_layer, LoadStatus.FIRST)
|
||||||
|
mock_select.return_value = 3
|
||||||
|
mock_erstelle.return_value = (enriched, 3, 0)
|
||||||
|
mock_get_var.return_value = ""
|
||||||
|
mock_vergl.return_value = (1000.0, 800.0, False)
|
||||||
|
mock_ask.return_value = "detailpruefung"
|
||||||
|
mock_extrahiere.return_value = [MagicMock(), MagicMock(), MagicMock()] # 3 Knickpunkte
|
||||||
|
mock_gp.return_value = punktort_layer
|
||||||
|
mock_knick.return_value = (ergebnis_layer, 2, 1, 0)
|
||||||
|
mock_prog.return_value = _make_progress_mock()
|
||||||
|
|
||||||
|
result_enriched, _, _, status = verfahrensgebiet_alkis_komplett(None)
|
||||||
|
|
||||||
|
mock_gp.assert_called_once()
|
||||||
|
mock_knick.assert_called_once()
|
||||||
|
self.assertIn(ergebnis_layer, proj_inst.added)
|
||||||
|
mock_info.assert_called_once()
|
||||||
|
# Abschlussmeldung muss Zahlen enthalten
|
||||||
|
info_text = mock_info.call_args[0][1]
|
||||||
|
self.assertIn("3", info_text) # Anzahl Knickpunkte
|
||||||
|
self.assertIn("2", info_text) # identisch
|
||||||
|
self.assertIn("1", info_text) # kein_pta
|
||||||
|
self.assertEqual(status, LoadStatus.FIRST)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# Cleanup-Verhalten
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
|
||||||
|
@patch(_PATCH_PROGRESS)
|
||||||
|
@patch(_PATCH_SHOW_INFO)
|
||||||
|
@patch(_PATCH_VERGLEICHE)
|
||||||
|
@patch(_PATCH_VKZ)
|
||||||
|
@patch(_PATCH_GET_VAR)
|
||||||
|
@patch(_PATCH_ERSTELLE)
|
||||||
|
@patch(_PATCH_SELECT)
|
||||||
|
@patch(_PATCH_PROJECT)
|
||||||
|
@patch(_PATCH_GEM)
|
||||||
|
@patch(_PATCH_FLST)
|
||||||
|
@patch(_PATCH_VG_ALKIS)
|
||||||
|
def test_bror_layer_bleibt_bei_erfolg_im_projekt(
|
||||||
|
self, mock_vg, mock_flst, mock_gem, mock_proj, mock_select,
|
||||||
|
mock_erstelle, mock_get_var, mock_vkz, mock_vergl, mock_info, mock_prog,
|
||||||
|
):
|
||||||
|
"""Bei Erfolg darf der BROR-Layer NICHT entfernt werden."""
|
||||||
|
proj_inst = _DummyProject()
|
||||||
|
mock_proj.instance.return_value = proj_inst
|
||||||
|
|
||||||
|
vg_layer = _make_layer(name="BauRaumOrdnungsrecht (ALKIS)", lid="BROR_ID")
|
||||||
|
flst_layer = _make_layer(name="Flurstücke")
|
||||||
|
enriched = _make_layer(name="Verfahrensgebiet", valid=True)
|
||||||
|
|
||||||
|
mock_vg.return_value = (vg_layer, LoadStatus.FIRST, [])
|
||||||
|
mock_gem.return_value = []
|
||||||
|
mock_flst.return_value = (flst_layer, LoadStatus.FIRST)
|
||||||
|
mock_select.return_value = 3
|
||||||
|
mock_erstelle.return_value = (enriched, 3, 0)
|
||||||
|
mock_get_var.return_value = ""
|
||||||
|
mock_vergl.return_value = (1000.0, 1000.0, True)
|
||||||
|
mock_prog.return_value = _make_progress_mock()
|
||||||
|
|
||||||
|
verfahrensgebiet_alkis_komplett(None)
|
||||||
|
|
||||||
|
self.assertNotIn("BROR_ID", proj_inst.removed)
|
||||||
|
|
||||||
|
@patch(_PATCH_PROGRESS)
|
||||||
|
@patch(_PATCH_PROJECT)
|
||||||
|
@patch(_PATCH_GEM)
|
||||||
|
@patch(_PATCH_VG_ALKIS)
|
||||||
|
def test_bror_layer_wird_bei_fehler_entfernt(
|
||||||
|
self, mock_vg, mock_gem, mock_proj, mock_prog
|
||||||
|
):
|
||||||
|
"""Bei Fehler (flst_alkis NONE) wird der BROR-Layer aus dem Projekt entfernt."""
|
||||||
|
proj_inst = _DummyProject()
|
||||||
|
mock_proj.instance.return_value = proj_inst
|
||||||
|
|
||||||
|
vg_layer = _make_layer(name="BauRaumOrdnungsrecht (ALKIS)", lid="BROR_ERR")
|
||||||
|
mock_vg.return_value = (vg_layer, LoadStatus.FIRST, [])
|
||||||
|
mock_gem.return_value = []
|
||||||
|
mock_prog.return_value = _make_progress_mock()
|
||||||
|
|
||||||
|
with patch(_PATCH_FLST, return_value=(None, LoadStatus.NONE)):
|
||||||
|
verfahrensgebiet_alkis_komplett(None)
|
||||||
|
|
||||||
|
self.assertIn("BROR_ERR", proj_inst.removed)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
|
|
||||||
@@ -0,0 +1,595 @@
|
|||||||
|
"""sn_verfahrensgebiet/tests/test_verfahrensgebiet_edit_service.py
|
||||||
|
|
||||||
|
Unit-Tests für das Verfahrensgebiet-Edit-Service-Modul.
|
||||||
|
|
||||||
|
Getestet werden:
|
||||||
|
- pruefe_flurstueck_layer (über Layerpruefer-Integration)
|
||||||
|
- pruefe_auswahl
|
||||||
|
- erstelle_verfahrensgebiet_aus_auswahl: Erfolgs- und Fehlerpfade
|
||||||
|
- ergaenze_verfahrensgebiet: nur neue IDs werden hinzugefügt
|
||||||
|
- entferne_aus_verfahrensgebiet: nur vorhandene IDs werden entfernt
|
||||||
|
- ersetze_verfahrensgebiet: Ergebnis entspricht Erstellen
|
||||||
|
- get_kennzeichen_menge: Hilfsfunktion
|
||||||
|
- Zähler (erfolgreich/fehlgeschlagen/bereits_vorhanden/nicht_enthalten)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import types
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import patch, MagicMock
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Minimales QGIS-Mock-Setup (muss vor allen sn_*-Importen erfolgen)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
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.verfahrensgebiet_edit_service import (
|
||||||
|
pruefe_flurstueck_layer,
|
||||||
|
pruefe_auswahl,
|
||||||
|
erstelle_verfahrensgebiet_aus_auswahl,
|
||||||
|
ergaenze_verfahrensgebiet,
|
||||||
|
entferne_aus_verfahrensgebiet,
|
||||||
|
ersetze_verfahrensgebiet,
|
||||||
|
get_kennzeichen_menge,
|
||||||
|
FELD_KENNZEICHEN,
|
||||||
|
FELD_GEMARKUNG,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Test-Dummies
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class _DummyGeom:
|
||||||
|
def __init__(self, empty=False):
|
||||||
|
self._empty = empty
|
||||||
|
|
||||||
|
def isEmpty(self):
|
||||||
|
return self._empty
|
||||||
|
|
||||||
|
def difference(self, other):
|
||||||
|
"""Dummy-Differenz: immer nicht-leeres Ergebnis (kein echter Geometrieschnitt)."""
|
||||||
|
return _DummyGeom(empty=False)
|
||||||
|
|
||||||
|
|
||||||
|
class _DummyFeature:
|
||||||
|
"""Feature-Dummy mit dict-artigem Attributzugriff und konfigurierbarer Geometrie."""
|
||||||
|
|
||||||
|
def __init__(self, kz: str, gemarkung: str = "1234", geom_empty: bool = False, feat_id: int = 0):
|
||||||
|
self._attrs = {FELD_KENNZEICHEN: kz, FELD_GEMARKUNG: gemarkung}
|
||||||
|
self._geom_empty = geom_empty
|
||||||
|
self._id = feat_id
|
||||||
|
self._set_geom = None
|
||||||
|
self._set_attrs = None
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
return self._attrs.get(key, None)
|
||||||
|
|
||||||
|
def geometry(self):
|
||||||
|
return _DummyGeom(self._geom_empty)
|
||||||
|
|
||||||
|
def id(self):
|
||||||
|
return self._id
|
||||||
|
|
||||||
|
def setGeometry(self, g):
|
||||||
|
self._set_geom = g
|
||||||
|
|
||||||
|
def setAttributes(self, attrs):
|
||||||
|
self._set_attrs = attrs
|
||||||
|
|
||||||
|
|
||||||
|
class _DummyProvider:
|
||||||
|
"""DataProvider-Dummy für Single-Feature-Verfahrensgebiet-Layer."""
|
||||||
|
|
||||||
|
def __init__(self, layer_ref):
|
||||||
|
self._layer = layer_ref
|
||||||
|
|
||||||
|
def addAttributes(self, attrs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def addFeatures(self, feats):
|
||||||
|
self._layer._features.extend(feats)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def deleteFeatures(self, ids):
|
||||||
|
self._layer._deleted_ids.extend(ids)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def changeAttributeValues(self, changes: dict):
|
||||||
|
"""changes: {fid: {field_idx: value}}"""
|
||||||
|
for feat in self._layer._features:
|
||||||
|
fid = feat.id() if callable(getattr(feat, "id", None)) else None
|
||||||
|
if fid in changes:
|
||||||
|
for field_idx, value in changes[fid].items():
|
||||||
|
feat._attr_by_idx[field_idx] = value
|
||||||
|
return True
|
||||||
|
|
||||||
|
def changeGeometryValues(self, changes: dict):
|
||||||
|
"""changes: {fid: new_geom}"""
|
||||||
|
for feat in self._layer._features:
|
||||||
|
fid = feat.id() if callable(getattr(feat, "id", None)) else None
|
||||||
|
if fid in changes:
|
||||||
|
feat._set_geom = changes[fid]
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class _DummySingleFeature:
|
||||||
|
"""Single-Feature-Dummy für den Verfahrensgebiet-Layer.
|
||||||
|
|
||||||
|
Speichert Kennzeichen als kommagetrennte Zeichenkette (new-style).
|
||||||
|
Feld-Indizes: 0=FELD_KENNZEICHEN, 1=FELD_GEMARKUNG
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, kz_komma: str, gemarkung: str = "", feat_id: int = 0, geom_empty: bool = False):
|
||||||
|
self._kz_komma = kz_komma
|
||||||
|
self._gemarkung = gemarkung
|
||||||
|
self._id = feat_id
|
||||||
|
self._geom_empty = geom_empty
|
||||||
|
self._set_geom = None
|
||||||
|
# Attribut-Mapping nach Index für changeAttributeValues
|
||||||
|
self._attr_by_idx: dict = {}
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
if key == FELD_KENNZEICHEN:
|
||||||
|
return self._attr_by_idx.get(0, self._kz_komma)
|
||||||
|
if key == FELD_GEMARKUNG:
|
||||||
|
return self._attr_by_idx.get(1, self._gemarkung)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def geometry(self):
|
||||||
|
return _DummyGeom(self._geom_empty)
|
||||||
|
|
||||||
|
def id(self):
|
||||||
|
return self._id
|
||||||
|
|
||||||
|
def setGeometry(self, g):
|
||||||
|
self._set_geom = g
|
||||||
|
|
||||||
|
def setAttributes(self, attrs):
|
||||||
|
if len(attrs) > 0:
|
||||||
|
self._kz_komma = attrs[0]
|
||||||
|
if len(attrs) > 1:
|
||||||
|
self._gemarkung = attrs[1]
|
||||||
|
|
||||||
|
|
||||||
|
class _DummyVerfLayer:
|
||||||
|
"""Memory-Layer-Dummy für Verfahrensgebiet (Single-Feature-Modell)."""
|
||||||
|
|
||||||
|
def __init__(self, single_feature=None):
|
||||||
|
self._features = [single_feature] if single_feature else []
|
||||||
|
self._deleted_ids = []
|
||||||
|
|
||||||
|
def getFeatures(self):
|
||||||
|
deleted = set(self._deleted_ids)
|
||||||
|
return [
|
||||||
|
f for f in self._features
|
||||||
|
if (f.id() if callable(getattr(f, "id", None)) else None) not in deleted
|
||||||
|
]
|
||||||
|
|
||||||
|
def dataProvider(self):
|
||||||
|
return _DummyProvider(self)
|
||||||
|
|
||||||
|
def fields(self):
|
||||||
|
class _Fields:
|
||||||
|
def indexFromName(self, name):
|
||||||
|
if name == FELD_KENNZEICHEN:
|
||||||
|
return 0
|
||||||
|
if name == FELD_GEMARKUNG:
|
||||||
|
return 1
|
||||||
|
return -1
|
||||||
|
return _Fields()
|
||||||
|
|
||||||
|
def updateExtents(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def triggerRepaint(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def updateFields(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def isValid(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def crs(self):
|
||||||
|
class _Crs:
|
||||||
|
def authid(self):
|
||||||
|
return "EPSG:25833"
|
||||||
|
return _Crs()
|
||||||
|
|
||||||
|
|
||||||
|
class _DummySourceLayer:
|
||||||
|
"""Flurstück-Quell-Layer-Dummy mit konfigurierbarer Auswahl."""
|
||||||
|
|
||||||
|
def __init__(self, selected=None, all_features=None, feature_count=None):
|
||||||
|
self._selected = list(selected or [])
|
||||||
|
self._all = list(all_features or [])
|
||||||
|
self._count = feature_count if feature_count is not None else len(self._selected)
|
||||||
|
|
||||||
|
def selectedFeatures(self):
|
||||||
|
return self._selected
|
||||||
|
|
||||||
|
def selectedFeatureCount(self):
|
||||||
|
return self._count
|
||||||
|
|
||||||
|
def getFeatures(self):
|
||||||
|
return self._all
|
||||||
|
|
||||||
|
def crs(self):
|
||||||
|
class _Crs:
|
||||||
|
def authid(self):
|
||||||
|
return "EPSG:25833"
|
||||||
|
return _Crs()
|
||||||
|
|
||||||
|
|
||||||
|
class _MockLayerpruefer:
|
||||||
|
"""Steuerbarer Mock für Layerpruefer.pruefe()."""
|
||||||
|
|
||||||
|
def __init__(self, ok, aktion="ok", meldung=""):
|
||||||
|
self._ok = ok
|
||||||
|
self._aktion = aktion
|
||||||
|
self._meldung = meldung
|
||||||
|
|
||||||
|
def pruefe(self):
|
||||||
|
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
|
||||||
|
return pruef_ergebnis(ok=self._ok, aktion=self._aktion, meldung=self._meldung)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Tests: pruefe_flurstueck_layer
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class TestPruefeFlurstueckLayer(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_layer_ok_passes(self):
|
||||||
|
"""Gültiger Polygon-Vektor-Layer mit Pflichtfeldern → ok=True."""
|
||||||
|
import sn_verfahrensgebiet.functions.verfahrensgebiet_edit_service as module
|
||||||
|
original = module.Layerpruefer
|
||||||
|
|
||||||
|
class _PassPruefer:
|
||||||
|
def __init__(self, **_):
|
||||||
|
pass
|
||||||
|
def pruefe(self):
|
||||||
|
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
|
||||||
|
return pruef_ergebnis(ok=True, aktion="ok")
|
||||||
|
|
||||||
|
module.Layerpruefer = _PassPruefer
|
||||||
|
try:
|
||||||
|
ergebnis = pruefe_flurstueck_layer(object())
|
||||||
|
self.assertTrue(ergebnis.ok)
|
||||||
|
self.assertEqual(ergebnis.aktion, "ok")
|
||||||
|
finally:
|
||||||
|
module.Layerpruefer = original
|
||||||
|
|
||||||
|
def test_layer_missing_field_fails(self):
|
||||||
|
"""Layer ohne Pflichtfelder → ok=False, aktion=felder_fehlen."""
|
||||||
|
import sn_verfahrensgebiet.functions.verfahrensgebiet_edit_service as module
|
||||||
|
original = module.Layerpruefer
|
||||||
|
|
||||||
|
class _FailPruefer:
|
||||||
|
def __init__(self, **_):
|
||||||
|
pass
|
||||||
|
def pruefe(self):
|
||||||
|
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
|
||||||
|
return pruef_ergebnis(ok=False, aktion="felder_fehlen",
|
||||||
|
meldung="Feld fehlt")
|
||||||
|
|
||||||
|
module.Layerpruefer = _FailPruefer
|
||||||
|
try:
|
||||||
|
ergebnis = pruefe_flurstueck_layer(object())
|
||||||
|
self.assertFalse(ergebnis.ok)
|
||||||
|
self.assertEqual(ergebnis.aktion, "felder_fehlen")
|
||||||
|
finally:
|
||||||
|
module.Layerpruefer = original
|
||||||
|
|
||||||
|
def test_layer_wrong_geometry_fails(self):
|
||||||
|
"""Layer mit falschem Geometrietyp → ok=False, aktion=falscher_geotyp."""
|
||||||
|
import sn_verfahrensgebiet.functions.verfahrensgebiet_edit_service as module
|
||||||
|
original = module.Layerpruefer
|
||||||
|
|
||||||
|
class _WrongGeomPruefer:
|
||||||
|
def __init__(self, **_):
|
||||||
|
pass
|
||||||
|
def pruefe(self):
|
||||||
|
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
|
||||||
|
return pruef_ergebnis(ok=False, aktion="falscher_geotyp",
|
||||||
|
meldung="Kein Polygon")
|
||||||
|
|
||||||
|
module.Layerpruefer = _WrongGeomPruefer
|
||||||
|
try:
|
||||||
|
ergebnis = pruefe_flurstueck_layer(object())
|
||||||
|
self.assertFalse(ergebnis.ok)
|
||||||
|
self.assertEqual(ergebnis.aktion, "falscher_geotyp")
|
||||||
|
finally:
|
||||||
|
module.Layerpruefer = original
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Tests: pruefe_auswahl
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class TestPruefeAuswahl(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_no_selection_fails(self):
|
||||||
|
layer = _DummySourceLayer(selected=[], feature_count=0)
|
||||||
|
ergebnis = pruefe_auswahl(layer)
|
||||||
|
self.assertFalse(ergebnis.ok)
|
||||||
|
self.assertEqual(ergebnis.aktion, "layer_leer")
|
||||||
|
|
||||||
|
def test_with_selection_passes(self):
|
||||||
|
feats = [_DummyFeature("1000-001")]
|
||||||
|
layer = _DummySourceLayer(selected=feats)
|
||||||
|
ergebnis = pruefe_auswahl(layer)
|
||||||
|
self.assertTrue(ergebnis.ok)
|
||||||
|
self.assertIn("1", ergebnis.meldung) # "1 Flurstücke ausgewählt."
|
||||||
|
|
||||||
|
def test_selectedFeatureCount_exception_treated_as_zero(self):
|
||||||
|
class _BrokenLayer:
|
||||||
|
def selectedFeatureCount(self):
|
||||||
|
raise RuntimeError("broken")
|
||||||
|
ergebnis = pruefe_auswahl(_BrokenLayer())
|
||||||
|
self.assertFalse(ergebnis.ok)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Tests: get_kennzeichen_menge
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class TestGetKennzeichenMenge(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_returns_set_of_ids(self):
|
||||||
|
"""Kommagetrennte Kennzeichen im Single-Feature werden korrekt geparst."""
|
||||||
|
feat = _DummySingleFeature("A001,A002,A001", feat_id=1) # Duplikat → set
|
||||||
|
layer = _DummyVerfLayer(feat)
|
||||||
|
result = get_kennzeichen_menge(layer)
|
||||||
|
self.assertEqual(result, {"A001", "A002"})
|
||||||
|
|
||||||
|
def test_empty_layer_returns_empty_set(self):
|
||||||
|
layer = _DummyVerfLayer()
|
||||||
|
result = get_kennzeichen_menge(layer)
|
||||||
|
self.assertEqual(result, set())
|
||||||
|
|
||||||
|
def test_single_kennzeichen_without_comma(self):
|
||||||
|
"""Einzelnes Kennzeichen ohne Komma wird korrekt erkannt."""
|
||||||
|
feat = _DummySingleFeature("X999", feat_id=1)
|
||||||
|
layer = _DummyVerfLayer(feat)
|
||||||
|
result = get_kennzeichen_menge(layer)
|
||||||
|
self.assertEqual(result, {"X999"})
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Tests: erstelle_verfahrensgebiet_aus_auswahl
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class TestErstelle(unittest.TestCase):
|
||||||
|
|
||||||
|
def _patch_all(self, test_func, union_empty=False):
|
||||||
|
"""Patcht _erstelle_memory_layer und QgsGeometry.unaryUnion."""
|
||||||
|
import sn_verfahrensgebiet.functions.verfahrensgebiet_edit_service as module
|
||||||
|
orig_erstelle = module._erstelle_memory_layer
|
||||||
|
orig_union = module.QgsGeometry.unaryUnion
|
||||||
|
|
||||||
|
mem = _DummyVerfLayer()
|
||||||
|
module._erstelle_memory_layer = lambda _crs: mem
|
||||||
|
module.QgsGeometry.unaryUnion = staticmethod(
|
||||||
|
lambda _geoms: _DummyGeom(empty=union_empty)
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
test_func(mem)
|
||||||
|
finally:
|
||||||
|
module._erstelle_memory_layer = orig_erstelle
|
||||||
|
module.QgsGeometry.unaryUnion = staticmethod(orig_union)
|
||||||
|
|
||||||
|
def test_success_counts(self):
|
||||||
|
"""3 gültige Features → erfolgreich=3, fehlgeschlagen=0, 1 Union-Feature im Layer."""
|
||||||
|
feats = [
|
||||||
|
_DummyFeature("A001", feat_id=0),
|
||||||
|
_DummyFeature("A002", feat_id=1),
|
||||||
|
_DummyFeature("A003", feat_id=2),
|
||||||
|
]
|
||||||
|
source = _DummySourceLayer(selected=feats)
|
||||||
|
|
||||||
|
def run(mem):
|
||||||
|
layer, ok, fail = erstelle_verfahrensgebiet_aus_auswahl(source)
|
||||||
|
self.assertIs(layer, mem)
|
||||||
|
self.assertEqual(ok, 3)
|
||||||
|
self.assertEqual(fail, 0)
|
||||||
|
# Single-Feature-Modell: genau 1 Union-Feature
|
||||||
|
self.assertEqual(len(mem._features), 1)
|
||||||
|
|
||||||
|
self._patch_all(run)
|
||||||
|
|
||||||
|
def test_partial_failure_on_empty_geometry(self):
|
||||||
|
"""2 ok + 1 leere Geometrie → erfolgreich=2, fehlgeschlagen=1."""
|
||||||
|
feats = [
|
||||||
|
_DummyFeature("A001"),
|
||||||
|
_DummyFeature("A002", geom_empty=True),
|
||||||
|
_DummyFeature("A003"),
|
||||||
|
]
|
||||||
|
source = _DummySourceLayer(selected=feats)
|
||||||
|
|
||||||
|
def run(mem):
|
||||||
|
_, ok, fail = erstelle_verfahrensgebiet_aus_auswahl(source)
|
||||||
|
self.assertEqual(ok, 2)
|
||||||
|
self.assertEqual(fail, 1)
|
||||||
|
|
||||||
|
self._patch_all(run)
|
||||||
|
|
||||||
|
def test_all_fail_on_empty_geometries(self):
|
||||||
|
"""Alle Features haben leere Geometrie → erfolgreich=0, kein Feature im Layer."""
|
||||||
|
feats = [_DummyFeature("A001", geom_empty=True)]
|
||||||
|
source = _DummySourceLayer(selected=feats)
|
||||||
|
|
||||||
|
def run(mem):
|
||||||
|
_, ok, fail = erstelle_verfahrensgebiet_aus_auswahl(source)
|
||||||
|
self.assertEqual(ok, 0)
|
||||||
|
self.assertEqual(fail, 1)
|
||||||
|
self.assertEqual(len(mem._features), 0) # kein Union-Feature
|
||||||
|
|
||||||
|
self._patch_all(run)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Tests: ergaenze_verfahrensgebiet
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class TestErgaenzeVerfahrensgebiet(unittest.TestCase):
|
||||||
|
|
||||||
|
def _patch_union(self):
|
||||||
|
import sn_verfahrensgebiet.functions.verfahrensgebiet_edit_service as module
|
||||||
|
orig = module.QgsGeometry.unaryUnion
|
||||||
|
module.QgsGeometry.unaryUnion = staticmethod(lambda _g: _DummyGeom(empty=False))
|
||||||
|
return module, orig
|
||||||
|
|
||||||
|
def test_adds_only_new_ids(self):
|
||||||
|
"""Auswahl enthält 1 neue + 1 bereits vorhandene ID → hinzugefügt=1."""
|
||||||
|
verf = _DummyVerfLayer(_DummySingleFeature("A001", feat_id=1))
|
||||||
|
selected = [
|
||||||
|
_DummyFeature("A001"), # bereits vorhanden
|
||||||
|
_DummyFeature("A002"), # neu
|
||||||
|
]
|
||||||
|
source = _DummySourceLayer(selected=selected)
|
||||||
|
module, orig = self._patch_union()
|
||||||
|
try:
|
||||||
|
_, hinzugefuegt, bereits_vorhanden, fehlgeschlagen = ergaenze_verfahrensgebiet(
|
||||||
|
verf, source
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
module.QgsGeometry.unaryUnion = staticmethod(orig)
|
||||||
|
self.assertEqual(hinzugefuegt, 1)
|
||||||
|
self.assertEqual(bereits_vorhanden, 1)
|
||||||
|
self.assertEqual(fehlgeschlagen, 0)
|
||||||
|
|
||||||
|
def test_skip_empty_geometry_counts_as_failed(self):
|
||||||
|
"""Neue ID mit leerer Geometrie → fehlgeschlagen=1, hinzugefügt=0."""
|
||||||
|
verf = _DummyVerfLayer() # leer
|
||||||
|
selected = [_DummyFeature("A001", geom_empty=True)]
|
||||||
|
source = _DummySourceLayer(selected=selected)
|
||||||
|
|
||||||
|
_, hinzugefuegt, _, fehlgeschlagen = ergaenze_verfahrensgebiet(verf, source)
|
||||||
|
self.assertEqual(hinzugefuegt, 0)
|
||||||
|
self.assertEqual(fehlgeschlagen, 1)
|
||||||
|
|
||||||
|
def test_all_already_present(self):
|
||||||
|
"""Alle ausgewählten IDs bereits vorhanden → bereits_vorhanden=n, hinzugefügt=0."""
|
||||||
|
verf = _DummyVerfLayer(_DummySingleFeature("A001,A002", feat_id=1))
|
||||||
|
selected = [_DummyFeature("A001"), _DummyFeature("A002")]
|
||||||
|
source = _DummySourceLayer(selected=selected)
|
||||||
|
|
||||||
|
_, hinzugefuegt, bereits_vorhanden, fehlgeschlagen = ergaenze_verfahrensgebiet(
|
||||||
|
verf, source
|
||||||
|
)
|
||||||
|
self.assertEqual(hinzugefuegt, 0)
|
||||||
|
self.assertEqual(bereits_vorhanden, 2)
|
||||||
|
self.assertEqual(fehlgeschlagen, 0)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Tests: entferne_aus_verfahrensgebiet
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class TestEntferneAusVerfahrensgebiet(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_removes_present_ids(self):
|
||||||
|
"""Auswahl enthält 1 vorhandene ID → entfernt=1, nicht_enthalten=0."""
|
||||||
|
verf = _DummyVerfLayer(_DummySingleFeature("A001,A002", feat_id=1))
|
||||||
|
selected = [_DummyFeature("A001")]
|
||||||
|
source = _DummySourceLayer(selected=selected)
|
||||||
|
|
||||||
|
_, entfernt, nicht_enthalten, fehlgeschlagen = entferne_aus_verfahrensgebiet(
|
||||||
|
verf, source
|
||||||
|
)
|
||||||
|
self.assertEqual(entfernt, 1)
|
||||||
|
self.assertEqual(nicht_enthalten, 0)
|
||||||
|
self.assertEqual(fehlgeschlagen, 0)
|
||||||
|
|
||||||
|
def test_counts_ids_not_in_layer(self):
|
||||||
|
"""Auswahl enthält ID, die nicht im Verfahrensgebiet ist → nicht_enthalten=1."""
|
||||||
|
verf = _DummyVerfLayer(_DummySingleFeature("A001", feat_id=1))
|
||||||
|
selected = [
|
||||||
|
_DummyFeature("A001"), # vorhanden
|
||||||
|
_DummyFeature("A999"), # nicht enthalten
|
||||||
|
]
|
||||||
|
source = _DummySourceLayer(selected=selected)
|
||||||
|
|
||||||
|
_, entfernt, nicht_enthalten, fehlgeschlagen = entferne_aus_verfahrensgebiet(
|
||||||
|
verf, source
|
||||||
|
)
|
||||||
|
self.assertEqual(entfernt, 1)
|
||||||
|
self.assertEqual(nicht_enthalten, 1)
|
||||||
|
|
||||||
|
def test_none_present_in_layer(self):
|
||||||
|
"""Auswahl hat IDs, die alle nicht im Layer sind → entfernt=0, nicht_enthalten=n."""
|
||||||
|
verf = _DummyVerfLayer(_DummySingleFeature("X001", feat_id=1))
|
||||||
|
selected = [_DummyFeature("A999"), _DummyFeature("B888")]
|
||||||
|
source = _DummySourceLayer(selected=selected)
|
||||||
|
|
||||||
|
_, entfernt, nicht_enthalten, fehlgeschlagen = entferne_aus_verfahrensgebiet(
|
||||||
|
verf, source
|
||||||
|
)
|
||||||
|
self.assertEqual(entfernt, 0)
|
||||||
|
self.assertEqual(nicht_enthalten, 2)
|
||||||
|
|
||||||
|
def test_empty_selection_removes_nothing(self):
|
||||||
|
"""Leere Auswahl → nichts entfernt, nichts nicht_enthalten."""
|
||||||
|
verf = _DummyVerfLayer(_DummySingleFeature("A001", feat_id=1))
|
||||||
|
source = _DummySourceLayer(selected=[])
|
||||||
|
|
||||||
|
_, entfernt, nicht_enthalten, fehlgeschlagen = entferne_aus_verfahrensgebiet(
|
||||||
|
verf, source
|
||||||
|
)
|
||||||
|
self.assertEqual(entfernt, 0)
|
||||||
|
self.assertEqual(nicht_enthalten, 0)
|
||||||
|
|
||||||
|
def test_removes_last_id_deletes_feature(self):
|
||||||
|
"""Wird das letzte Kennzeichen entfernt, wird das Feature gelöscht."""
|
||||||
|
verf = _DummyVerfLayer(_DummySingleFeature("A001", feat_id=1))
|
||||||
|
selected = [_DummyFeature("A001")]
|
||||||
|
source = _DummySourceLayer(selected=selected)
|
||||||
|
|
||||||
|
_, entfernt, _, _ = entferne_aus_verfahrensgebiet(verf, source)
|
||||||
|
self.assertEqual(entfernt, 1)
|
||||||
|
# Feature soll gelöscht worden sein
|
||||||
|
self.assertIn(1, verf._deleted_ids)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Tests: ersetze_verfahrensgebiet
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class TestErsetze(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_ersetze_delegates_to_erstelle(self):
|
||||||
|
"""ersetze_verfahrensgebiet gibt dasselbe Ergebnis wie erstelle zurück."""
|
||||||
|
feats = [_DummyFeature("A001")]
|
||||||
|
source = _DummySourceLayer(selected=feats)
|
||||||
|
|
||||||
|
import sn_verfahrensgebiet.functions.verfahrensgebiet_edit_service as module
|
||||||
|
orig_erstelle = module._erstelle_memory_layer
|
||||||
|
orig_union = module.QgsGeometry.unaryUnion
|
||||||
|
mem = _DummyVerfLayer()
|
||||||
|
module._erstelle_memory_layer = lambda _crs: mem
|
||||||
|
module.QgsGeometry.unaryUnion = staticmethod(lambda _g: _DummyGeom(empty=False))
|
||||||
|
|
||||||
|
try:
|
||||||
|
layer, ok, fail = ersetze_verfahrensgebiet(source)
|
||||||
|
self.assertIs(layer, mem)
|
||||||
|
self.assertEqual(ok, 1)
|
||||||
|
self.assertEqual(fail, 0)
|
||||||
|
finally:
|
||||||
|
module._erstelle_memory_layer = orig_erstelle
|
||||||
|
module.QgsGeometry.unaryUnion = staticmethod(orig_union)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,489 @@
|
|||||||
|
import sys
|
||||||
|
import types
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
if "qgis" not in sys.modules:
|
||||||
|
qgis_mod = types.ModuleType("qgis")
|
||||||
|
qgis_utils = types.ModuleType("qgis.utils")
|
||||||
|
qgis_utils.iface = types.SimpleNamespace(
|
||||||
|
mapCanvas=lambda: types.SimpleNamespace(setExtent=lambda _e: None, refresh=lambda: None)
|
||||||
|
)
|
||||||
|
qgis_utils.plugins = {}
|
||||||
|
qgis_mod.utils = qgis_utils
|
||||||
|
sys.modules["qgis"] = qgis_mod
|
||||||
|
sys.modules["qgis.utils"] = qgis_utils
|
||||||
|
|
||||||
|
from sn_verfahrensgebiet.ui.tabs.working_tab import WorkingTab, LoadStatus
|
||||||
|
|
||||||
|
|
||||||
|
class _DummyLayer:
|
||||||
|
def __init__(self, valid=True, layer_id="L1", layer_name="DummyLayer"):
|
||||||
|
self._valid = valid
|
||||||
|
self._id = layer_id
|
||||||
|
self._name = layer_name
|
||||||
|
|
||||||
|
def isValid(self):
|
||||||
|
return self._valid
|
||||||
|
|
||||||
|
def id(self):
|
||||||
|
return self._id
|
||||||
|
|
||||||
|
def name(self):
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
def extent(self):
|
||||||
|
return object()
|
||||||
|
|
||||||
|
|
||||||
|
class _DummyProject:
|
||||||
|
def __init__(self):
|
||||||
|
self.added = []
|
||||||
|
|
||||||
|
def addMapLayer(self, layer):
|
||||||
|
self.added.append(layer)
|
||||||
|
|
||||||
|
|
||||||
|
class _DummyCanvas:
|
||||||
|
def __init__(self):
|
||||||
|
self.extent = None
|
||||||
|
|
||||||
|
def setExtent(self, extent):
|
||||||
|
self.extent = extent
|
||||||
|
|
||||||
|
def refresh(self):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class _DummyButton:
|
||||||
|
def __init__(self):
|
||||||
|
self._enabled = True
|
||||||
|
self._text = "Aus ALKIS laden"
|
||||||
|
|
||||||
|
def setEnabled(self, enabled):
|
||||||
|
self._enabled = enabled
|
||||||
|
|
||||||
|
def isEnabled(self):
|
||||||
|
return self._enabled
|
||||||
|
|
||||||
|
def setText(self, text):
|
||||||
|
self._text = text
|
||||||
|
|
||||||
|
def text(self):
|
||||||
|
return self._text
|
||||||
|
|
||||||
|
|
||||||
|
class _DummyCombo:
|
||||||
|
def __init__(self):
|
||||||
|
self.layer = None
|
||||||
|
|
||||||
|
def setLayer(self, layer):
|
||||||
|
self.layer = layer
|
||||||
|
|
||||||
|
|
||||||
|
class TestWorkingTab(unittest.TestCase):
|
||||||
|
|
||||||
|
def _make_tab(self):
|
||||||
|
tab = WorkingTab.__new__(WorkingTab)
|
||||||
|
tab.btn_verf_alkis = _DummyButton()
|
||||||
|
tab.btn_flurst_alkis = _DummyButton()
|
||||||
|
tab.btn_grenz_alkis = _DummyButton()
|
||||||
|
tab.btn_verf_aus_auswahl = _DummyButton()
|
||||||
|
tab.btn_verf_aus_liste = _DummyButton()
|
||||||
|
tab.verf_layer_combo = _DummyCombo()
|
||||||
|
tab.haken_verf = types.SimpleNamespace(setText=lambda _t: None, setStyleSheet=lambda _s: None)
|
||||||
|
tab.haken_flurst = types.SimpleNamespace(setText=lambda _t: None, setStyleSheet=lambda _s: None)
|
||||||
|
tab.haken_grenz = types.SimpleNamespace(setText=lambda _t: None, setStyleSheet=lambda _s: None)
|
||||||
|
tab.setze_haken = lambda *_args, **_kwargs: None
|
||||||
|
# Pruefmanager-Mock: verarbeite() wird im Test ggf. überschrieben
|
||||||
|
tab.pruefmanager = types.SimpleNamespace(
|
||||||
|
verarbeite=lambda ergebnis: ergebnis
|
||||||
|
)
|
||||||
|
return tab
|
||||||
|
|
||||||
|
def test_set_busy_updates_button_state(self):
|
||||||
|
tab = self._make_tab()
|
||||||
|
|
||||||
|
tab._set_busy(tab.btn_verf_alkis, True)
|
||||||
|
self.assertFalse(tab.btn_verf_alkis.isEnabled())
|
||||||
|
self.assertEqual(tab.btn_verf_alkis.text(), "Lade ...")
|
||||||
|
|
||||||
|
tab._set_busy(tab.btn_verf_alkis, False)
|
||||||
|
self.assertTrue(tab.btn_verf_alkis.isEnabled())
|
||||||
|
self.assertEqual(tab.btn_verf_alkis.text(), "Aus ALKIS laden")
|
||||||
|
|
||||||
|
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.set_verfahrensgebiet_layer_id")
|
||||||
|
def test_on_verf_layer_changed_valid_sets_variable(self, mock_set_layer_id):
|
||||||
|
tab = self._make_tab()
|
||||||
|
layer = _DummyLayer(valid=True, layer_id="layer_123")
|
||||||
|
|
||||||
|
tab._on_verf_layer_changed(layer)
|
||||||
|
|
||||||
|
mock_set_layer_id.assert_called_once_with("layer_123")
|
||||||
|
|
||||||
|
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.set_verfahrensgebiet_layer_id")
|
||||||
|
def test_on_verf_layer_changed_none_clears_variable(self, mock_set_layer_id):
|
||||||
|
tab = self._make_tab()
|
||||||
|
|
||||||
|
tab._on_verf_layer_changed(None)
|
||||||
|
|
||||||
|
mock_set_layer_id.assert_called_once_with("")
|
||||||
|
|
||||||
|
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.success")
|
||||||
|
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.apply_style")
|
||||||
|
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.QgsProject.instance")
|
||||||
|
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.verfahrensgebiet_alkis_komplett")
|
||||||
|
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.iface")
|
||||||
|
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.set_verfahrensgebiet_layer_id")
|
||||||
|
def test_handle_verf_alkis_success_path(
|
||||||
|
self,
|
||||||
|
mock_set_layer_id,
|
||||||
|
mock_iface,
|
||||||
|
mock_alkis_komplett,
|
||||||
|
mock_project_instance,
|
||||||
|
mock_apply_style,
|
||||||
|
mock_success,
|
||||||
|
):
|
||||||
|
"""verfahrensgebiet_alkis_komplett → 4-Tupel (enriched, flst, bror, FIRST)."""
|
||||||
|
tab = self._make_tab()
|
||||||
|
enriched = _DummyLayer(valid=True, layer_id="enriched_ok")
|
||||||
|
flst = _DummyLayer(valid=True, layer_id="flst_ok")
|
||||||
|
mock_alkis_komplett.return_value = (enriched, flst, None, LoadStatus.FIRST)
|
||||||
|
proj = _DummyProject()
|
||||||
|
proj.mapLayersByName = lambda _n: [] # kein bestehender Flst-Layer
|
||||||
|
mock_project_instance.return_value = proj
|
||||||
|
mock_iface.mapCanvas.return_value = _DummyCanvas()
|
||||||
|
|
||||||
|
tab.handle_verf_alkis()
|
||||||
|
|
||||||
|
mock_apply_style.assert_called()
|
||||||
|
mock_success.assert_called_once()
|
||||||
|
mock_set_layer_id.assert_called_with("enriched_ok")
|
||||||
|
|
||||||
|
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.set_verfahrensgebiet_layer_id")
|
||||||
|
@patch(
|
||||||
|
"sn_verfahrensgebiet.ui.tabs.working_tab.verfahrensgebiet_alkis_komplett",
|
||||||
|
return_value=(None, None, None, LoadStatus.NONE),
|
||||||
|
)
|
||||||
|
def test_handle_verf_alkis_failure_clears_layer_var(self, _mock_alkis, mock_set_layer_id):
|
||||||
|
"""Status NONE → Layer-Variable wird auf '' gesetzt."""
|
||||||
|
tab = self._make_tab()
|
||||||
|
|
||||||
|
tab.handle_verf_alkis()
|
||||||
|
|
||||||
|
mock_set_layer_id.assert_called_once_with("")
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# handle_grenz_alkis
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.grenzpunkt_alkis")
|
||||||
|
def test_handle_grenz_alkis_success_sets_haken(self, mock_grenz):
|
||||||
|
"""Grenzpunkte erfolgreich geladen → haken_grenz wird gesetzt."""
|
||||||
|
tab = self._make_tab()
|
||||||
|
haken_calls = []
|
||||||
|
tab.setze_haken = lambda lbl, v: haken_calls.append((lbl, v))
|
||||||
|
|
||||||
|
pta = _DummyLayer(valid=True)
|
||||||
|
gp = _DummyLayer(valid=True)
|
||||||
|
mock_grenz.return_value = (pta, gp)
|
||||||
|
|
||||||
|
tab.handle_grenz_alkis()
|
||||||
|
|
||||||
|
self.assertEqual(len(haken_calls), 1)
|
||||||
|
self.assertIs(haken_calls[0][0], tab.haken_grenz)
|
||||||
|
self.assertTrue(haken_calls[0][1])
|
||||||
|
|
||||||
|
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.grenzpunkt_alkis")
|
||||||
|
def test_handle_grenz_alkis_none_result_sets_no_haken(self, mock_grenz):
|
||||||
|
"""grenzpunkt_alkis liefert (None, None) → kein Haken."""
|
||||||
|
tab = self._make_tab()
|
||||||
|
haken_calls = []
|
||||||
|
tab.setze_haken = lambda lbl, v: haken_calls.append((lbl, v))
|
||||||
|
|
||||||
|
mock_grenz.return_value = (None, None)
|
||||||
|
|
||||||
|
tab.handle_grenz_alkis()
|
||||||
|
|
||||||
|
self.assertEqual(len(haken_calls), 0)
|
||||||
|
# Button wieder aktiviert (finally)
|
||||||
|
self.assertTrue(tab.btn_grenz_alkis.isEnabled())
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# handle_flurst_alkis
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.success")
|
||||||
|
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.apply_style")
|
||||||
|
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.QgsProject.instance")
|
||||||
|
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.iface")
|
||||||
|
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.flurstueck_alkis")
|
||||||
|
def test_handle_flurst_alkis_success_sets_haken_and_style(
|
||||||
|
self, mock_alkis, mock_iface, mock_project, mock_apply_style, mock_success
|
||||||
|
):
|
||||||
|
tab = self._make_tab()
|
||||||
|
haken_calls = []
|
||||||
|
tab.setze_haken = lambda lbl, v: haken_calls.append((lbl, v))
|
||||||
|
|
||||||
|
layer = _DummyLayer(valid=True)
|
||||||
|
mock_alkis.return_value = (layer, LoadStatus.FIRST)
|
||||||
|
mock_project.return_value = _DummyProject()
|
||||||
|
mock_iface.mapCanvas.return_value = _DummyCanvas()
|
||||||
|
|
||||||
|
tab.handle_flurst_alkis()
|
||||||
|
|
||||||
|
mock_apply_style.assert_called_once_with(layer, "GIS_Flst_Beschriftung_ALKIS_NAS.qml")
|
||||||
|
mock_success.assert_called_once()
|
||||||
|
self.assertEqual(len(haken_calls), 1)
|
||||||
|
self.assertTrue(haken_calls[0][1]) # haken gesetzt
|
||||||
|
|
||||||
|
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.info")
|
||||||
|
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.flurstueck_alkis")
|
||||||
|
def test_handle_flurst_alkis_existing_layer_keep_path(self, mock_alkis, mock_info):
|
||||||
|
tab = self._make_tab()
|
||||||
|
haken_calls = []
|
||||||
|
tab.setze_haken = lambda lbl, v: haken_calls.append((lbl, v))
|
||||||
|
|
||||||
|
layer = _DummyLayer(valid=True)
|
||||||
|
mock_alkis.return_value = (layer, LoadStatus.KEEP)
|
||||||
|
|
||||||
|
tab.handle_flurst_alkis()
|
||||||
|
|
||||||
|
mock_info.assert_called_once()
|
||||||
|
self.assertEqual(len(haken_calls), 1)
|
||||||
|
|
||||||
|
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.flurstueck_alkis")
|
||||||
|
def test_handle_flurst_alkis_invalid_input_returns_early(self, mock_alkis):
|
||||||
|
"""Status NONE (z. B. Nutzerabbruch) → kein Layer, kein Haken."""
|
||||||
|
tab = self._make_tab()
|
||||||
|
haken_calls = []
|
||||||
|
tab.setze_haken = lambda lbl, v: haken_calls.append((lbl, v))
|
||||||
|
mock_alkis.return_value = (None, LoadStatus.NONE)
|
||||||
|
|
||||||
|
tab.handle_flurst_alkis()
|
||||||
|
|
||||||
|
self.assertEqual(len(haken_calls), 0)
|
||||||
|
# Button muss am Ende wieder freigegeben sein (finally)
|
||||||
|
self.assertTrue(tab.btn_flurst_alkis.isEnabled())
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# handle_verf_aus_auswahl – Vorbedingungsprüfungen
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.warning")
|
||||||
|
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.iface")
|
||||||
|
def test_handle_verfahrensgebiet_from_selection_requires_active_layer(
|
||||||
|
self, mock_iface, mock_warning
|
||||||
|
):
|
||||||
|
"""Kein aktiver Layer → warning, kein Layer wird erstellt."""
|
||||||
|
tab = self._make_tab()
|
||||||
|
mock_iface.activeLayer.return_value = None
|
||||||
|
|
||||||
|
tab.handle_verf_aus_auswahl()
|
||||||
|
|
||||||
|
mock_warning.assert_called_once()
|
||||||
|
self.assertIn("aktiver Layer", mock_warning.call_args[0][1])
|
||||||
|
|
||||||
|
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.warning")
|
||||||
|
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.pruefe_flurstueck_layer")
|
||||||
|
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.iface")
|
||||||
|
def test_handle_verfahrensgebiet_from_selection_requires_required_fields(
|
||||||
|
self, mock_iface, mock_pruefe, mock_warning
|
||||||
|
):
|
||||||
|
"""Layer ohne Pflichtfelder → warning, kein Layer wird erstellt."""
|
||||||
|
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis as PruefErg
|
||||||
|
tab = self._make_tab()
|
||||||
|
active = _DummyLayer(valid=True)
|
||||||
|
mock_iface.activeLayer.return_value = active
|
||||||
|
mock_pruefe.return_value = PruefErg(
|
||||||
|
ok=False, aktion="felder_fehlen", meldung="Feld fehlt"
|
||||||
|
)
|
||||||
|
|
||||||
|
tab.handle_verf_aus_auswahl()
|
||||||
|
|
||||||
|
mock_warning.assert_called_once()
|
||||||
|
|
||||||
|
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.warning")
|
||||||
|
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.pruefe_auswahl")
|
||||||
|
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.pruefe_flurstueck_layer")
|
||||||
|
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.iface")
|
||||||
|
def test_handle_verfahrensgebiet_from_selection_requires_selection(
|
||||||
|
self, mock_iface, mock_pruefe_layer, mock_pruefe_auswahl, mock_warning
|
||||||
|
):
|
||||||
|
"""Keine Flurstück-Auswahl → warning, kein Layer erstellt."""
|
||||||
|
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis as PruefErg
|
||||||
|
tab = self._make_tab()
|
||||||
|
active = _DummyLayer(valid=True)
|
||||||
|
mock_iface.activeLayer.return_value = active
|
||||||
|
mock_pruefe_layer.return_value = PruefErg(ok=True, aktion="ok")
|
||||||
|
mock_pruefe_auswahl.return_value = PruefErg(
|
||||||
|
ok=False, aktion="layer_leer", meldung="Keine Auswahl"
|
||||||
|
)
|
||||||
|
|
||||||
|
tab.handle_verf_aus_auswahl()
|
||||||
|
|
||||||
|
mock_warning.assert_called_once()
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# handle_verf_aus_auswahl – Erstlauf (kein bestehender Layer)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.QMessageBox")
|
||||||
|
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.apply_style")
|
||||||
|
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.QgsProject.instance")
|
||||||
|
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.erstelle_verfahrensgebiet_aus_auswahl")
|
||||||
|
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.pruefe_auswahl")
|
||||||
|
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.pruefe_flurstueck_layer")
|
||||||
|
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.iface")
|
||||||
|
def test_handle_verfahrensgebiet_from_selection_new_layer_success(
|
||||||
|
self, mock_iface, mock_pruefe_layer, mock_pruefe_auswahl,
|
||||||
|
mock_erstelle, mock_project_inst, mock_apply_style, mock_msgbox
|
||||||
|
):
|
||||||
|
"""Erstlauf ohne bestehenden Layer → Layer wird erstellt und hinzugefügt."""
|
||||||
|
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis as PruefErg
|
||||||
|
tab = self._make_tab()
|
||||||
|
haken_calls = []
|
||||||
|
tab.setze_haken = lambda lbl, v: haken_calls.append(v)
|
||||||
|
active = _DummyLayer(valid=True)
|
||||||
|
mock_iface.activeLayer.return_value = active
|
||||||
|
mock_iface.mapCanvas.return_value = _DummyCanvas()
|
||||||
|
mock_pruefe_layer.return_value = PruefErg(ok=True, aktion="ok")
|
||||||
|
mock_pruefe_auswahl.return_value = PruefErg(ok=True, aktion="ok", meldung="3 Flurstücke")
|
||||||
|
|
||||||
|
new_layer = _DummyLayer(valid=True, layer_id="new_verf")
|
||||||
|
mock_erstelle.return_value = (new_layer, 3, 0)
|
||||||
|
|
||||||
|
project = _DummyProject()
|
||||||
|
project.mapLayersByName = lambda _name: [] # kein bestehender Layer
|
||||||
|
mock_project_inst.return_value = project
|
||||||
|
|
||||||
|
tab.handle_verf_aus_auswahl()
|
||||||
|
|
||||||
|
self.assertIn(new_layer, project.added)
|
||||||
|
mock_apply_style.assert_called_once_with(new_layer, "verfahrensgebiet.qml")
|
||||||
|
self.assertTrue(haken_calls[0])
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# handle_verf_aus_auswahl – Ergänzen / Entfernen / Ersetzen
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _make_entscheidungs_ergebnis(self, aktion: str):
|
||||||
|
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis as PruefErg
|
||||||
|
return PruefErg(ok=True, aktion=aktion)
|
||||||
|
|
||||||
|
def _setup_existierenden_layer(self, tab, mock_iface, mock_pruefe_layer,
|
||||||
|
mock_pruefe_auswahl, mock_project_inst,
|
||||||
|
mock_canvas, entscheidung: str):
|
||||||
|
"""Gemeinsames Setup für Delta-Operationen mit bestehendem Layer."""
|
||||||
|
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis as PruefErg
|
||||||
|
active = _DummyLayer(valid=True)
|
||||||
|
mock_iface.activeLayer.return_value = active
|
||||||
|
mock_iface.mapCanvas.return_value = mock_canvas
|
||||||
|
mock_pruefe_layer.return_value = PruefErg(ok=True, aktion="ok")
|
||||||
|
mock_pruefe_auswahl.return_value = PruefErg(ok=True, aktion="ok", meldung="2 Flurstücke")
|
||||||
|
|
||||||
|
verf_layer = _DummyLayer(valid=True, layer_id="verf_existing")
|
||||||
|
project = _DummyProject()
|
||||||
|
project.added = []
|
||||||
|
project.mapLayersByName = lambda _: [verf_layer]
|
||||||
|
project.removeMapLayer = lambda lid: None
|
||||||
|
mock_project_inst.return_value = project
|
||||||
|
|
||||||
|
# Pruefmanager gibt Entscheidung zurück
|
||||||
|
tab.pruefmanager.verarbeite = lambda _e: self._make_entscheidungs_ergebnis(entscheidung)
|
||||||
|
|
||||||
|
return active, verf_layer, project
|
||||||
|
|
||||||
|
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.QMessageBox")
|
||||||
|
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.apply_style")
|
||||||
|
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.ergaenze_verfahrensgebiet")
|
||||||
|
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.QgsProject.instance")
|
||||||
|
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.pruefe_auswahl")
|
||||||
|
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.pruefe_flurstueck_layer")
|
||||||
|
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.iface")
|
||||||
|
def test_handle_verfahrensgebiet_from_selection_existing_layer_ergaenzen_dispatch(
|
||||||
|
self, mock_iface, mock_pruefe_layer, mock_pruefe_auswahl,
|
||||||
|
mock_project_inst, mock_ergaenze, mock_apply_style, mock_msgbox
|
||||||
|
):
|
||||||
|
"""Entscheidung 'ergaenzen' → ergaenze_verfahrensgebiet wird aufgerufen."""
|
||||||
|
tab = self._make_tab()
|
||||||
|
canvas = _DummyCanvas()
|
||||||
|
active, verf_layer, _ = self._setup_existierenden_layer(
|
||||||
|
tab, mock_iface, mock_pruefe_layer, mock_pruefe_auswahl,
|
||||||
|
mock_project_inst, canvas, "verfahrensgebiet_ergaenzen"
|
||||||
|
)
|
||||||
|
mock_ergaenze.return_value = (verf_layer, 2, 0, 0)
|
||||||
|
|
||||||
|
tab.handle_verf_aus_auswahl()
|
||||||
|
|
||||||
|
mock_ergaenze.assert_called_once_with(verf_layer, active)
|
||||||
|
mock_apply_style.assert_called_once_with(verf_layer, "verfahrensgebiet.qml")
|
||||||
|
|
||||||
|
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.QMessageBox")
|
||||||
|
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.apply_style")
|
||||||
|
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.entferne_aus_verfahrensgebiet")
|
||||||
|
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.QgsProject.instance")
|
||||||
|
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.pruefe_auswahl")
|
||||||
|
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.pruefe_flurstueck_layer")
|
||||||
|
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.iface")
|
||||||
|
def test_handle_verfahrensgebiet_from_selection_existing_layer_entfernen_dispatch(
|
||||||
|
self, mock_iface, mock_pruefe_layer, mock_pruefe_auswahl,
|
||||||
|
mock_project_inst, mock_entferne, mock_apply_style, mock_msgbox
|
||||||
|
):
|
||||||
|
"""Entscheidung 'entfernen' → entferne_aus_verfahrensgebiet wird aufgerufen."""
|
||||||
|
tab = self._make_tab()
|
||||||
|
canvas = _DummyCanvas()
|
||||||
|
active, verf_layer, _ = self._setup_existierenden_layer(
|
||||||
|
tab, mock_iface, mock_pruefe_layer, mock_pruefe_auswahl,
|
||||||
|
mock_project_inst, canvas, "verfahrensgebiet_entfernen"
|
||||||
|
)
|
||||||
|
mock_entferne.return_value = (verf_layer, 1, 1, 0)
|
||||||
|
|
||||||
|
tab.handle_verf_aus_auswahl()
|
||||||
|
|
||||||
|
mock_entferne.assert_called_once_with(verf_layer, active)
|
||||||
|
mock_apply_style.assert_called_once_with(verf_layer, "verfahrensgebiet.qml")
|
||||||
|
|
||||||
|
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.QMessageBox")
|
||||||
|
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.apply_style")
|
||||||
|
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.ersetze_verfahrensgebiet")
|
||||||
|
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.QgsProject.instance")
|
||||||
|
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.pruefe_auswahl")
|
||||||
|
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.pruefe_flurstueck_layer")
|
||||||
|
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.iface")
|
||||||
|
def test_handle_verfahrensgebiet_from_selection_existing_layer_ersetzen_dispatch(
|
||||||
|
self, mock_iface, mock_pruefe_layer, mock_pruefe_auswahl,
|
||||||
|
mock_project_inst, mock_ersetze, mock_apply_style, mock_msgbox
|
||||||
|
):
|
||||||
|
"""Entscheidung 'ersetzen' → alter Layer entfernt, neuer hinzugefügt."""
|
||||||
|
tab = self._make_tab()
|
||||||
|
canvas = _DummyCanvas()
|
||||||
|
active, verf_layer, project = self._setup_existierenden_layer(
|
||||||
|
tab, mock_iface, mock_pruefe_layer, mock_pruefe_auswahl,
|
||||||
|
mock_project_inst, canvas, "verfahrensgebiet_ersetzen"
|
||||||
|
)
|
||||||
|
removed = []
|
||||||
|
project.removeMapLayer = lambda lid: removed.append(lid)
|
||||||
|
new_layer = _DummyLayer(valid=True, layer_id="new_verf_replaced")
|
||||||
|
mock_ersetze.return_value = (new_layer, 3, 0)
|
||||||
|
|
||||||
|
tab.handle_verf_aus_auswahl()
|
||||||
|
|
||||||
|
mock_ersetze.assert_called_once_with(active)
|
||||||
|
self.assertIn(verf_layer.id(), removed)
|
||||||
|
self.assertIn(new_layer, project.added)
|
||||||
|
mock_apply_style.assert_called_once_with(new_layer, "verfahrensgebiet.qml")
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# handle_verf_aus_liste
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.show_info_dialog")
|
||||||
|
def test_aus_liste_button_shows_not_implemented_hint(self, mock_info):
|
||||||
|
tab = self._make_tab()
|
||||||
|
tab.handle_verf_aus_liste()
|
||||||
|
mock_info.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -1,8 +1,59 @@
|
|||||||
|
"""sn_verfahrensgebiet/ui/dockwidget.py – Haupt-DockWidget für sn_verfahrensgebiet.
|
||||||
|
Verwaltet die Tabs für Verfahrensgebiet-Bearbeitung, Druck und Einstellungen.
|
||||||
|
"""
|
||||||
|
|
||||||
from sn_basis.ui.tabs.settings_tab import SettingsTab
|
from sn_basis.ui.tabs.settings_tab import SettingsTab
|
||||||
from sn_basis.ui.tabs.print_tab import PrintTab
|
from sn_basis.ui.tabs.print_tab import PrintTab
|
||||||
from sn_verfahrensgebiet.ui.tabs.working_tab import WorkingTab
|
from sn_verfahrensgebiet.ui.tabs.working_tab import WorkingTab
|
||||||
from sn_basis.ui.base_dockwidget import BaseDockWidget
|
from sn_basis.ui.base_dockwidget import BaseDockWidget
|
||||||
|
from sn_basis.functions.qt_wrapper import QTabWidget
|
||||||
|
from sn_basis.functions.message_wrapper import error
|
||||||
|
|
||||||
|
|
||||||
class DockWidget(BaseDockWidget):
|
class DockWidget(BaseDockWidget):
|
||||||
|
"""Haupt-DockWidget für sn_verfahrensgebiet mit Tab-Verwaltung.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
tabs: Liste von Tab-Klassen (WorkingTab, PrintTab, SettingsTab)
|
||||||
|
pruefmanager: Prüfmanager-Instanz für Validierung
|
||||||
|
data_grabber: DataGrabber-Instanz für Fachdatenabruf
|
||||||
|
"""
|
||||||
|
|
||||||
tabs = [WorkingTab, PrintTab, SettingsTab]
|
tabs = [WorkingTab, PrintTab, SettingsTab]
|
||||||
|
|
||||||
|
def __init__(self, parent=None, subtitle="", pruefmanager=None, data_grabber=None):
|
||||||
|
"""Initialisiert das DockWidget mit Services und Tabs.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
parent: Übergeordnetes Widget
|
||||||
|
subtitle: Untertitel des DockWidgets
|
||||||
|
pruefmanager: Prüfmanager-Instanz
|
||||||
|
data_grabber: DataGrabber-Instanz
|
||||||
|
"""
|
||||||
|
super().__init__(parent, subtitle)
|
||||||
|
|
||||||
|
self.pruefmanager = pruefmanager
|
||||||
|
self.data_grabber = data_grabber
|
||||||
|
|
||||||
|
self._init_tabs_with_services()
|
||||||
|
|
||||||
|
def _init_tabs_with_services(self):
|
||||||
|
"""Tabs neu aufbauen und Services (z.B. fuer PrintTab) injizieren."""
|
||||||
|
try:
|
||||||
|
self.setWidget(None)
|
||||||
|
|
||||||
|
tab_widget = QTabWidget()
|
||||||
|
for tab_class in self.tabs:
|
||||||
|
tab_instance = tab_class(self)
|
||||||
|
tab_title = getattr(tab_class, "tab_title", tab_class.__name__)
|
||||||
|
tab_widget.addTab(tab_instance, tab_title)
|
||||||
|
|
||||||
|
if hasattr(tab_instance, "set_services"):
|
||||||
|
tab_instance.set_services(
|
||||||
|
pruefmanager=self.pruefmanager,
|
||||||
|
data_grabber=self.data_grabber,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.setWidget(tab_widget)
|
||||||
|
except Exception as e:
|
||||||
|
error("Services-Tabs konnten nicht initialisiert werden", str(e))
|
||||||
|
|||||||
+495
-30
@@ -1,13 +1,42 @@
|
|||||||
|
"""sn_verfahrensgebiet/ui/tabs/working_tab.py – Bearbeitungs-Tab.
|
||||||
|
Verwaltet die Verfahrensgebiet-Abgrenzung und ALKIS-Integration.
|
||||||
|
"""
|
||||||
|
|
||||||
from sn_basis.functions.qt_wrapper import QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QMessageBox
|
from sn_basis.functions.qt_wrapper import QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QMessageBox
|
||||||
from sn_basis.functions.qt_wrapper import Qt
|
from sn_basis.functions.qt_wrapper import Qt
|
||||||
from sn_basis.functions import Qgis, QgsProject #QgsMessageLog
|
from sn_basis.functions import Qgis, QgsProject #QgsMessageLog
|
||||||
|
from sn_basis.functions.qgisui_wrapper import QgsMapLayerComboBox
|
||||||
|
from sn_basis.functions.qgiscore_wrapper import QgsMapLayerProxyModel
|
||||||
|
from sn_basis.functions.verfahrensgebiet_manager import get_verfahrensgebiet_layer_id, set_verfahrensgebiet_layer_id
|
||||||
|
from sn_basis.functions.dialog_wrapper import show_info_dialog, create_progress_dialog
|
||||||
|
from sn_basis.modules.Pruefmanager import Pruefmanager
|
||||||
|
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
|
||||||
from qgis.utils import iface
|
from qgis.utils import iface
|
||||||
|
|
||||||
from sn_verfahrensgebiet.functions.verfahrensgebiet_alkis import verfahrensgebiet_alkis, LoadStatus
|
from sn_verfahrensgebiet.functions.verfahrensgebiet_alkis import LoadStatus
|
||||||
|
from sn_verfahrensgebiet.functions.flurstueck_alkis import flurstueck_alkis
|
||||||
|
from sn_verfahrensgebiet.functions.verfahrensgebiet_alkis_komplett import verfahrensgebiet_alkis_komplett
|
||||||
|
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
|
||||||
|
from sn_verfahrensgebiet.functions.umringstatistik_grenzpunkte import umringstatistik_grenzpunkte
|
||||||
|
from sn_verfahrensgebiet.functions.verfahrensgebiet_edit_service import (
|
||||||
|
pruefe_flurstueck_layer,
|
||||||
|
pruefe_auswahl,
|
||||||
|
erstelle_verfahrensgebiet_aus_auswahl,
|
||||||
|
ergaenze_verfahrensgebiet,
|
||||||
|
entferne_aus_verfahrensgebiet,
|
||||||
|
ersetze_verfahrensgebiet,
|
||||||
|
)
|
||||||
from sn_basis.functions.message_wrapper import success, info, warning, error
|
from sn_basis.functions.message_wrapper import success, info, warning, error
|
||||||
from sn_basis.functions.ly_style_wrapper import apply_style
|
from sn_basis.functions.ly_style_wrapper import apply_style
|
||||||
|
|
||||||
class WorkingTab(QWidget):
|
class WorkingTab(QWidget):
|
||||||
|
"""Tab für die Verfahrensgebiet-Bearbeitung und Verwaltung.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
tab_title: Anzeigetitel des Tabs ("Bearbeitung")
|
||||||
|
"""
|
||||||
|
|
||||||
tab_title = "Bearbeitung"
|
tab_title = "Bearbeitung"
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
@@ -16,7 +45,7 @@ class WorkingTab(QWidget):
|
|||||||
layout = QVBoxLayout()
|
layout = QVBoxLayout()
|
||||||
layout.setAlignment(Qt.AlignmentFlag.AlignTop)
|
layout.setAlignment(Qt.AlignmentFlag.AlignTop)
|
||||||
|
|
||||||
# Abschnitt 1: Verfahrensgebiet
|
# Abschnitt 1: Verfahrensgebiet laden
|
||||||
row_verf_header = QHBoxLayout()
|
row_verf_header = QHBoxLayout()
|
||||||
lbl_verf = QLabel("<b>Verfahrensgebiet</b>")
|
lbl_verf = QLabel("<b>Verfahrensgebiet</b>")
|
||||||
self.haken_verf = self._haken_label()
|
self.haken_verf = self._haken_label()
|
||||||
@@ -32,7 +61,21 @@ class WorkingTab(QWidget):
|
|||||||
row_verf_buttons.addWidget(self.btn_verf_shape)
|
row_verf_buttons.addWidget(self.btn_verf_shape)
|
||||||
layout.addLayout(row_verf_buttons)
|
layout.addLayout(row_verf_buttons)
|
||||||
|
|
||||||
# Abschnitt 2: Flurstücke (Platzhalter für spätere Implementierung)
|
# Abschnitt 1b: Verfahrensgebiet-Layer auswählen
|
||||||
|
row_layer_label = QHBoxLayout()
|
||||||
|
lbl_layer = QLabel("Oder: Verfahrensgebiet-Layer auswählen")
|
||||||
|
lbl_layer.setStyleSheet("font-style: italic; color: gray; margin-top: 8px;")
|
||||||
|
row_layer_label.addWidget(lbl_layer)
|
||||||
|
row_layer_label.addStretch()
|
||||||
|
layout.addLayout(row_layer_label)
|
||||||
|
|
||||||
|
self.verf_layer_combo = QgsMapLayerComboBox(self)
|
||||||
|
self.verf_layer_combo.setFilters(QgsMapLayerProxyModel.VectorLayer)
|
||||||
|
self.verf_layer_combo.setToolTip("Wähle den Verfahrensgebiet-Layer, oder lasse es leer für Automatik")
|
||||||
|
self.verf_layer_combo.layerChanged.connect(self._on_verf_layer_changed)
|
||||||
|
layout.addWidget(self.verf_layer_combo)
|
||||||
|
|
||||||
|
# Abschnitt 2: Flurstücke
|
||||||
row_flurst_header = QHBoxLayout()
|
row_flurst_header = QHBoxLayout()
|
||||||
lbl_flurst = QLabel("<b>Flurstücke</b>")
|
lbl_flurst = QLabel("<b>Flurstücke</b>")
|
||||||
self.haken_flurst = self._haken_label()
|
self.haken_flurst = self._haken_label()
|
||||||
@@ -48,51 +91,120 @@ class WorkingTab(QWidget):
|
|||||||
row_flurst_buttons.addWidget(self.btn_flurst_shape)
|
row_flurst_buttons.addWidget(self.btn_flurst_shape)
|
||||||
layout.addLayout(row_flurst_buttons)
|
layout.addLayout(row_flurst_buttons)
|
||||||
|
|
||||||
|
# Abschnitt 2b: Grenzpunkte
|
||||||
|
row_grenz_header = QHBoxLayout()
|
||||||
|
lbl_grenz = QLabel("<b>Grenzpunkte</b>")
|
||||||
|
self.haken_grenz = self._haken_label()
|
||||||
|
row_grenz_header.addWidget(lbl_grenz)
|
||||||
|
row_grenz_header.addWidget(self.haken_grenz)
|
||||||
|
row_grenz_header.addStretch()
|
||||||
|
layout.addLayout(row_grenz_header)
|
||||||
|
|
||||||
|
row_grenz_buttons = QHBoxLayout()
|
||||||
|
self.btn_grenz_alkis = QPushButton("Grenzpunkte aus ALKIS laden")
|
||||||
|
row_grenz_buttons.addWidget(self.btn_grenz_alkis)
|
||||||
|
layout.addLayout(row_grenz_buttons)
|
||||||
|
|
||||||
|
row_umring_buttons = QHBoxLayout()
|
||||||
|
self.btn_umring_statistik = QPushButton("Umringstatistik Grenzpunkte")
|
||||||
|
row_umring_buttons.addWidget(self.btn_umring_statistik)
|
||||||
|
layout.addLayout(row_umring_buttons)
|
||||||
|
|
||||||
|
# Abschnitt 3: Verfahrensgebiet erstellen
|
||||||
|
row_verf_erstellen_header = QHBoxLayout()
|
||||||
|
lbl_verf_erstellen = QLabel("<b>Verfahrensgebiet erstellen</b>")
|
||||||
|
row_verf_erstellen_header.addWidget(lbl_verf_erstellen)
|
||||||
|
row_verf_erstellen_header.addStretch()
|
||||||
|
layout.addLayout(row_verf_erstellen_header)
|
||||||
|
|
||||||
|
row_verf_erstellen_buttons = QHBoxLayout()
|
||||||
|
self.btn_verf_aus_auswahl = QPushButton("aus Auswahl")
|
||||||
|
self.btn_verf_aus_liste = QPushButton("Aus Liste")
|
||||||
|
row_verf_erstellen_buttons.addWidget(self.btn_verf_aus_auswahl)
|
||||||
|
row_verf_erstellen_buttons.addWidget(self.btn_verf_aus_liste)
|
||||||
|
layout.addLayout(row_verf_erstellen_buttons)
|
||||||
|
|
||||||
layout.addStretch()
|
layout.addStretch()
|
||||||
self.setLayout(layout)
|
self.setLayout(layout)
|
||||||
|
|
||||||
|
# Pruefmanager für interaktive Entscheidungen (Ergänzen/Entfernen/Ersetzen)
|
||||||
|
self.pruefmanager = Pruefmanager(ui_modus="qgis", parent=self)
|
||||||
|
|
||||||
# Signale verbinden
|
# Signale verbinden
|
||||||
self.btn_verf_alkis.clicked.connect(self.handle_verf_alkis)
|
self.btn_verf_alkis.clicked.connect(self.handle_verf_alkis)
|
||||||
|
self.btn_flurst_alkis.clicked.connect(self.handle_flurst_alkis)
|
||||||
|
self.btn_grenz_alkis.clicked.connect(self.handle_grenz_alkis)
|
||||||
|
self.btn_umring_statistik.clicked.connect(self.handle_umringstatistik)
|
||||||
|
self.btn_verf_aus_auswahl.clicked.connect(self.handle_verf_aus_auswahl)
|
||||||
|
self.btn_verf_aus_liste.clicked.connect(self.handle_verf_aus_liste)
|
||||||
# self.btn_verf_shape.clicked.connect(self.handle_verf_shape)
|
# self.btn_verf_shape.clicked.connect(self.handle_verf_shape)
|
||||||
# self.btn_flurst_alkis.clicked.connect(self.handle_flurst_alkis)
|
|
||||||
# self.btn_flurst_shape.clicked.connect(self.handle_flurst_shape)
|
# self.btn_flurst_shape.clicked.connect(self.handle_flurst_shape)
|
||||||
|
|
||||||
def handle_verf_alkis(self):
|
# Beim Init: aktuelle Verfahrensgebiet-Layer-ID laden und im Dropdown anzeigen
|
||||||
self._set_busy(self.btn_verf_alkis, True)
|
self._restore_verf_layer_selection()
|
||||||
|
|
||||||
|
def handle_verf_alkis(self) -> None:
|
||||||
|
"""Startet den vollständigen VG-Lade-Workflow (ALKIS → Gemarkungen → Flurstücke →
|
||||||
|
Verschneidung → Flächenvergleich → opt. Umringsprüfung).
|
||||||
|
|
||||||
|
Delegiert an :func:`~sn_verfahrensgebiet.functions.verfahrensgebiet_alkis_komplett.verfahrensgebiet_alkis_komplett`.
|
||||||
|
Nach Erfolg werden enriched VG-Layer, Flurstücks-Layer und BROR-Layer
|
||||||
|
ins Projekt eingefügt, Styles angewendet und die UI-Elemente aktualisiert.
|
||||||
|
"""
|
||||||
|
self._set_busy(self.btn_verf_alkis, True, "Aus ALKIS laden")
|
||||||
try:
|
try:
|
||||||
layer, status = verfahrensgebiet_alkis(self)
|
enriched_layer, flst_layer, bror_layer, status = verfahrensgebiet_alkis_komplett(self)
|
||||||
if not layer or not layer.isValid():
|
|
||||||
|
if status == LoadStatus.NONE or not enriched_layer:
|
||||||
|
set_verfahrensgebiet_layer_id("")
|
||||||
return
|
return
|
||||||
|
|
||||||
if status in (LoadStatus.FIRST, LoadStatus.RELOAD):
|
project = QgsProject.instance()
|
||||||
# Gemeinsame Logik für erstmaliges und erneutes Laden
|
|
||||||
QgsProject.instance().addMapLayer(layer)
|
|
||||||
iface.mapCanvas().setExtent(layer.extent())
|
|
||||||
#iface.mapCanvas().refresh()
|
|
||||||
|
|
||||||
apply_style(layer, "verfahrensgebiet.qml")
|
|
||||||
iface.mapCanvas().refresh()
|
|
||||||
self.setze_haken(self.haken_verf, True)
|
|
||||||
|
|
||||||
# Unterschied nur in der Meldung
|
# Flurstücks-Layer ins Projekt (sofern neu geladen)
|
||||||
msg = "erstmalig geladen" if status == LoadStatus.FIRST else "neu geladen"
|
if flst_layer and flst_layer.isValid():
|
||||||
success("Verfahrensgebiet", f"Layer wurde {msg}.", duration=5)
|
existing_flst = project.mapLayersByName(flst_layer.name())
|
||||||
|
if not existing_flst:
|
||||||
|
project.addMapLayer(flst_layer)
|
||||||
|
apply_style(flst_layer, "GIS_Flst_Beschriftung_ALKIS_NAS.qml")
|
||||||
|
self.setze_haken(self.haken_flurst, True)
|
||||||
|
|
||||||
elif status == LoadStatus.KEEP:
|
# BROR-Layer: einfache Darstellung setzen (roter Umring, transparent)
|
||||||
info("Verfahrensgebiet", "Vorhandener Layer wurde behalten.", duration=4)
|
if bror_layer and bror_layer.isValid():
|
||||||
|
_setze_bror_stil(bror_layer)
|
||||||
|
|
||||||
elif status == LoadStatus.NONE:
|
# Enriched VG-Layer ins Projekt (ggf. bereits registriert, z.B. bei "als VG laden")
|
||||||
warning("Verfahrensgebiet", "Layer konnte nicht geladen werden.", duration=None)
|
if not project.mapLayer(enriched_layer.id()):
|
||||||
|
project.addMapLayer(enriched_layer)
|
||||||
|
apply_style(enriched_layer, "verfahrensgebiet.qml")
|
||||||
|
iface.mapCanvas().setExtent(enriched_layer.extent())
|
||||||
|
iface.mapCanvas().refresh()
|
||||||
|
self.setze_haken(self.haken_verf, True)
|
||||||
|
|
||||||
|
self.verf_layer_combo.setLayer(enriched_layer)
|
||||||
|
self._on_verf_layer_changed(enriched_layer)
|
||||||
|
|
||||||
|
msg = "erstmalig geladen" if status == LoadStatus.FIRST else "neu geladen"
|
||||||
|
success("Verfahrensgebiet", f"Layer wurde {msg} (inkl. Flurstücksverschneidung).", duration=5)
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
self._set_busy(self.btn_verf_alkis, False)
|
self._set_busy(self.btn_verf_alkis, False, "Aus ALKIS laden")
|
||||||
|
|
||||||
def _set_busy(self, button: QPushButton, busy: bool):
|
def _set_busy(self, button: QPushButton, busy: bool, original_text: str = "Aus ALKIS laden") -> None:
|
||||||
|
"""Setzt den Busy-State eines Buttons.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
button :
|
||||||
|
Der zu sperrende/entsperrende Button.
|
||||||
|
busy : bool
|
||||||
|
True zum Sperren (Text → "Lade ..."), False zum Freigeben.
|
||||||
|
original_text : str
|
||||||
|
Text, der nach dem Freigeben wiederhergestellt wird.
|
||||||
|
Jeder Button gibt seinen eigenen Originaltext explizit an.
|
||||||
|
"""
|
||||||
button.setEnabled(not busy)
|
button.setEnabled(not busy)
|
||||||
if busy:
|
button.setText("Lade ..." if busy else original_text)
|
||||||
button.setText("Lade ...")
|
|
||||||
else:
|
|
||||||
button.setText("Aus ALKIS laden")
|
|
||||||
|
|
||||||
def _haken_label(self) -> QLabel:
|
def _haken_label(self) -> QLabel:
|
||||||
label = QLabel()
|
label = QLabel()
|
||||||
@@ -110,3 +222,356 @@ class WorkingTab(QWidget):
|
|||||||
label.setText("")
|
label.setText("")
|
||||||
label.setStyleSheet("")
|
label.setStyleSheet("")
|
||||||
|
|
||||||
|
def _on_verf_layer_changed(self, layer) -> None:
|
||||||
|
"""Callback: Nutzer hat einen anderen Layer im Dropdown gewählt."""
|
||||||
|
if layer and layer.isValid():
|
||||||
|
# Speichere die Layer-ID in der zentralen Variable
|
||||||
|
set_verfahrensgebiet_layer_id(layer.id())
|
||||||
|
else:
|
||||||
|
# Layer is None oder invalid; leere die Variable
|
||||||
|
set_verfahrensgebiet_layer_id("")
|
||||||
|
|
||||||
|
def _restore_verf_layer_selection(self) -> None:
|
||||||
|
"""Beim Tab-Init: Lade die zuletzt gespeicherte Verfahrensgebiet-Layer-ID."""
|
||||||
|
layer_id = get_verfahrensgebiet_layer_id()
|
||||||
|
if layer_id:
|
||||||
|
try:
|
||||||
|
project = QgsProject.instance()
|
||||||
|
layer = project.mapLayer(layer_id)
|
||||||
|
if layer and layer.isValid():
|
||||||
|
self.verf_layer_combo.setLayer(layer)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Flurstücke-Handler
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def handle_flurst_alkis(self) -> None:
|
||||||
|
"""Lädt Flurstücke aus ALKIS-WFS anhand von Gemarkungsschlüsseln."""
|
||||||
|
self._set_busy(self.btn_flurst_alkis, True, "Aus ALKIS laden")
|
||||||
|
try:
|
||||||
|
layer, status = flurstueck_alkis(self)
|
||||||
|
if not layer or not layer.isValid():
|
||||||
|
return
|
||||||
|
|
||||||
|
if status in (LoadStatus.FIRST, LoadStatus.RELOAD):
|
||||||
|
QgsProject.instance().addMapLayer(layer)
|
||||||
|
# iface.mapCanvas().setExtent(layer.extent()) # Kartenausschnitt bleibt unverändert
|
||||||
|
apply_style(layer, "GIS_Flst_Beschriftung_ALKIS_NAS.qml")
|
||||||
|
iface.mapCanvas().refresh()
|
||||||
|
self.setze_haken(self.haken_flurst, True)
|
||||||
|
msg = "erstmalig geladen" if status == LoadStatus.FIRST else "neu geladen"
|
||||||
|
success("Flurstücke", f"Layer wurde {msg}.", duration=5)
|
||||||
|
|
||||||
|
elif status == LoadStatus.KEEP:
|
||||||
|
self.setze_haken(self.haken_flurst, True)
|
||||||
|
info("Flurstücke", "Vorhandener Layer wurde behalten.", duration=4)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
self._set_busy(self.btn_flurst_alkis, False, "Aus ALKIS laden")
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Verfahrensgebiet-erstellen-Handler
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def handle_verf_aus_auswahl(self) -> None:
|
||||||
|
"""Erstellt oder aktualisiert den Verfahrensgebiet-Layer aus der Flurstück-Auswahl."""
|
||||||
|
self._set_busy(self.btn_verf_aus_auswahl, True, "aus Auswahl")
|
||||||
|
try:
|
||||||
|
# 1. Aktiven Layer ermitteln
|
||||||
|
active_layer = iface.activeLayer()
|
||||||
|
if not active_layer:
|
||||||
|
warning(
|
||||||
|
"Verfahrensgebiet erstellen",
|
||||||
|
"Kein aktiver Layer. Bitte den Flurstück-Layer aktivieren.",
|
||||||
|
duration=None,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# 2. Vorbedingungen via Layerpruefer
|
||||||
|
ergebnis = pruefe_flurstueck_layer(active_layer)
|
||||||
|
if not ergebnis.ok:
|
||||||
|
warning("Verfahrensgebiet erstellen", ergebnis.meldung, duration=None)
|
||||||
|
return
|
||||||
|
|
||||||
|
# 3. Auswahl prüfen
|
||||||
|
ergebnis_auswahl = pruefe_auswahl(active_layer)
|
||||||
|
if not ergebnis_auswahl.ok:
|
||||||
|
warning("Verfahrensgebiet erstellen", ergebnis_auswahl.meldung, duration=None)
|
||||||
|
return
|
||||||
|
|
||||||
|
# 4. Bestehenden Verfahrensgebiet-Layer prüfen
|
||||||
|
existing_layers = QgsProject.instance().mapLayersByName("Verfahrensgebiet")
|
||||||
|
|
||||||
|
if not existing_layers:
|
||||||
|
# Erstlauf: neuen Memory-Layer erzeugen
|
||||||
|
layer, erfolgreich, fehlgeschlagen = erstelle_verfahrensgebiet_aus_auswahl(
|
||||||
|
active_layer
|
||||||
|
)
|
||||||
|
if not layer or not layer.isValid():
|
||||||
|
error("Verfahrensgebiet erstellen", "Layer konnte nicht erstellt werden.")
|
||||||
|
return
|
||||||
|
QgsProject.instance().addMapLayer(layer)
|
||||||
|
apply_style(layer, "verfahrensgebiet.qml")
|
||||||
|
iface.mapCanvas().refresh()
|
||||||
|
self.verf_layer_combo.setLayer(layer)
|
||||||
|
self._on_verf_layer_changed(layer)
|
||||||
|
self.setze_haken(self.haken_verf, True)
|
||||||
|
QMessageBox.information(
|
||||||
|
self,
|
||||||
|
"Verfahrensgebiet erstellt",
|
||||||
|
f"Verfahrensgebiet aus Auswahl erstellt.\n"
|
||||||
|
f"Erfolgreich: {erfolgreich}\n"
|
||||||
|
f"Fehlgeschlagen: {fehlgeschlagen}",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# 5. Bestehender Layer: Nutzerentscheidung via Pruefmanager
|
||||||
|
entscheidung_ergebnis = self.pruefmanager.verarbeite(
|
||||||
|
pruef_ergebnis(
|
||||||
|
ok=False,
|
||||||
|
aktion="verfahrensgebiet_layer_existiert",
|
||||||
|
meldung=(
|
||||||
|
"Ein Verfahrensgebiet-Layer existiert bereits.\n"
|
||||||
|
"Was soll mit der Auswahl geschehen?"
|
||||||
|
),
|
||||||
|
kontext=existing_layers[0],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
aktion = entscheidung_ergebnis.aktion
|
||||||
|
verf_layer = existing_layers[0]
|
||||||
|
|
||||||
|
if aktion == "verfahrensgebiet_abbrechen" or not entscheidung_ergebnis.ok:
|
||||||
|
return
|
||||||
|
|
||||||
|
elif aktion == "verfahrensgebiet_ergaenzen":
|
||||||
|
_, hinzugefuegt, bereits_vorhanden, fehlgeschlagen = ergaenze_verfahrensgebiet(
|
||||||
|
verf_layer, active_layer
|
||||||
|
)
|
||||||
|
apply_style(verf_layer, "verfahrensgebiet.qml")
|
||||||
|
iface.mapCanvas().refresh()
|
||||||
|
self.setze_haken(self.haken_verf, True)
|
||||||
|
QMessageBox.information(
|
||||||
|
self,
|
||||||
|
"Flurstücke ergänzt",
|
||||||
|
f"Hinzugefügt: {hinzugefuegt}\n"
|
||||||
|
f"Bereits vorhanden (übersprungen): {bereits_vorhanden}\n"
|
||||||
|
f"Fehlgeschlagen: {fehlgeschlagen}",
|
||||||
|
)
|
||||||
|
|
||||||
|
elif aktion == "verfahrensgebiet_entfernen":
|
||||||
|
_, entfernt, nicht_enthalten, fehlgeschlagen = entferne_aus_verfahrensgebiet(
|
||||||
|
verf_layer, active_layer
|
||||||
|
)
|
||||||
|
apply_style(verf_layer, "verfahrensgebiet.qml")
|
||||||
|
iface.mapCanvas().refresh()
|
||||||
|
QMessageBox.information(
|
||||||
|
self,
|
||||||
|
"Flurstücke entfernt",
|
||||||
|
f"Entfernt: {entfernt}\n"
|
||||||
|
f"Nicht im Verfahrensgebiet (übersprungen): {nicht_enthalten}\n"
|
||||||
|
f"Fehlgeschlagen: {fehlgeschlagen}",
|
||||||
|
)
|
||||||
|
|
||||||
|
elif aktion == "verfahrensgebiet_ersetzen":
|
||||||
|
for lyr in existing_layers:
|
||||||
|
QgsProject.instance().removeMapLayer(lyr.id())
|
||||||
|
layer, erfolgreich, fehlgeschlagen = ersetze_verfahrensgebiet(active_layer)
|
||||||
|
if not layer or not layer.isValid():
|
||||||
|
error("Verfahrensgebiet erstellen", "Layer konnte nicht erstellt werden.")
|
||||||
|
return
|
||||||
|
QgsProject.instance().addMapLayer(layer)
|
||||||
|
apply_style(layer, "verfahrensgebiet.qml")
|
||||||
|
iface.mapCanvas().refresh()
|
||||||
|
self.verf_layer_combo.setLayer(layer)
|
||||||
|
self._on_verf_layer_changed(layer)
|
||||||
|
self.setze_haken(self.haken_verf, True)
|
||||||
|
QMessageBox.information(
|
||||||
|
self,
|
||||||
|
"Verfahrensgebiet ersetzt",
|
||||||
|
f"Verfahrensgebiet neu erstellt.\n"
|
||||||
|
f"Erfolgreich: {erfolgreich}\n"
|
||||||
|
f"Fehlgeschlagen: {fehlgeschlagen}",
|
||||||
|
)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
self._set_busy(self.btn_verf_aus_auswahl, False, "aus Auswahl")
|
||||||
|
|
||||||
|
def handle_verf_aus_liste(self) -> None:
|
||||||
|
"""Platzhalter – Verfahrensgebiet aus Liste erstellen (noch nicht implementiert)."""
|
||||||
|
show_info_dialog(
|
||||||
|
"Noch nicht implementiert",
|
||||||
|
"Die Funktion 'Aus Liste' steht in einer zukünftigen Version zur Verfügung.",
|
||||||
|
parent=self,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Grenzpunkt-Handler
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def handle_grenz_alkis(self) -> None:
|
||||||
|
"""Lädt Grenzpunkte gemarkungsweise aus ALKIS.
|
||||||
|
|
||||||
|
Verwendet denselben Gemarkungsfilter wie "Verfahrensgebiet aus ALKIS laden":
|
||||||
|
Die Gemarkungsschlüssel werden per Spatial-Join aus der Geometrie des
|
||||||
|
aktuell gewählten Verfahrensgebiet-Layers abgeleitet.
|
||||||
|
"""
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# Schritt 0: Fortschrittsdialog sofort anzeigen #
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# 4 Vorbereitungsschritte (Geometrie, Gemarkungen, Hits, Laden) –
|
||||||
|
# die genaue Schrittzahl wird in grenzpunkte_gemarkungsweise verfeinert.
|
||||||
|
prog = create_progress_dialog(
|
||||||
|
4, "Grenzpunkte aus ALKIS laden", "Verfahrensgebiet prüfen …"
|
||||||
|
)
|
||||||
|
prog.set_value(0)
|
||||||
|
|
||||||
|
# Verfahrensgebiet-Layer aus dem Dropdown holen
|
||||||
|
vg_layer = self.verf_layer_combo.currentLayer()
|
||||||
|
if not vg_layer or not vg_layer.isValid():
|
||||||
|
prog.close()
|
||||||
|
warning(
|
||||||
|
"Grenzpunkte",
|
||||||
|
"Kein Verfahrensgebiet-Layer gewählt. Bitte zuerst ein Verfahrensgebiet laden.",
|
||||||
|
duration=6,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Geometrie des ersten Features als Suchgeometrie
|
||||||
|
feats = list(vg_layer.getFeatures())
|
||||||
|
if not feats:
|
||||||
|
prog.close()
|
||||||
|
warning("Grenzpunkte", "Verfahrensgebiet-Layer enthält keine Features.", duration=5)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Geometrie über alle Features vereinen
|
||||||
|
from qgis.core import QgsGeometry # type: ignore[import]
|
||||||
|
vg_geom = QgsGeometry.unaryUnion([f.geometry() for f in feats if f.hasGeometry()])
|
||||||
|
|
||||||
|
if not vg_geom or vg_geom.isEmpty():
|
||||||
|
prog.close()
|
||||||
|
warning("Grenzpunkte", "Verfahrensgebiet-Layer hat keine Geometrie.", duration=5)
|
||||||
|
return
|
||||||
|
|
||||||
|
prog.set_label("Gemarkungsschlüssel per WFS ermitteln …")
|
||||||
|
prog.set_value(1)
|
||||||
|
if prog.is_canceled():
|
||||||
|
return
|
||||||
|
|
||||||
|
# Gemarkungsschlüssel per Spatial-Join ermitteln (identisch zu VG-Workflow)
|
||||||
|
schluessel_namen = gemarkung_schluessel_und_namen_von_vg_geometrie(vg_geom)
|
||||||
|
schluessel_liste = [nr for nr, _ in schluessel_namen]
|
||||||
|
|
||||||
|
if not schluessel_liste:
|
||||||
|
prog.close()
|
||||||
|
warning(
|
||||||
|
"Grenzpunkte",
|
||||||
|
"Keine Gemarkungen für das Verfahrensgebiet gefunden. "
|
||||||
|
"Bitte Netzwerkverbindung prüfen.",
|
||||||
|
duration=6,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
prog.set_label(f"{len(schluessel_liste)} Gemarkung(en) gefunden – Grenzpunkte laden …")
|
||||||
|
prog.set_value(2)
|
||||||
|
if prog.is_canceled():
|
||||||
|
return
|
||||||
|
|
||||||
|
self._set_busy(self.btn_grenz_alkis, True, "Grenzpunkte aus ALKIS laden")
|
||||||
|
prog.close() # Vorbereitungs-Dialog schließen; grenzpunkte_gemarkungsweise öffnet eigenen
|
||||||
|
try:
|
||||||
|
joined_layer = grenzpunkte_gemarkungsweise(self, schluessel_liste, vg_geom=vg_geom)
|
||||||
|
|
||||||
|
if joined_layer and joined_layer.isValid():
|
||||||
|
iface.mapCanvas().refresh()
|
||||||
|
self.setze_haken(self.haken_grenz, True)
|
||||||
|
QMessageBox.information(
|
||||||
|
self,
|
||||||
|
"Grenzpunkte geladen",
|
||||||
|
f"Grenzpunkte erfolgreich geladen ({joined_layer.featureCount()} Objekte).",
|
||||||
|
)
|
||||||
|
elif joined_layer is None:
|
||||||
|
error(
|
||||||
|
"Grenzpunkte",
|
||||||
|
"Laden fehlgeschlagen – WFS- oder Netzwerkfehler. "
|
||||||
|
"Details unter Ansicht -> Bedienfelder -> Protokollnachrichten "
|
||||||
|
"(Kategorie 'sn_verfahrensgebiet'). "
|
||||||
|
"Bitte prüfen Sie ggf. die Proxy-Einstellungen.",
|
||||||
|
duration=0,
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
self._set_busy(self.btn_grenz_alkis, False, "Grenzpunkte aus ALKIS laden")
|
||||||
|
|
||||||
|
def handle_umringstatistik(self) -> None:
|
||||||
|
"""Klassifiziert Grenzpunkte auf dem VG-Umring nach Lagegenauigkeit.
|
||||||
|
|
||||||
|
Sucht den vorhandenen ``Grenzpunkte (ALKIS)``-Layer im Projekt und
|
||||||
|
kombiniert ihn mit dem gewählten Verfahrensgebiet-Layer, um die
|
||||||
|
Umringstatistik zu berechnen.
|
||||||
|
"""
|
||||||
|
# 1. Grenzpunkte-Layer aus dem Projekt holen
|
||||||
|
project = QgsProject.instance()
|
||||||
|
gp_layers = project.mapLayersByName(JOINED_LAYER_NAME)
|
||||||
|
if not gp_layers:
|
||||||
|
warning(
|
||||||
|
"Umringstatistik",
|
||||||
|
f"Kein Layer '{JOINED_LAYER_NAME}' im Projekt gefunden. "
|
||||||
|
"Bitte zuerst 'Grenzpunkte aus ALKIS laden' ausführen.",
|
||||||
|
duration=8,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
grenzpunkte_layer = gp_layers[0]
|
||||||
|
|
||||||
|
# 2. Verfahrensgebiet-Layer aus Dropdown
|
||||||
|
vg_layer = self.verf_layer_combo.currentLayer()
|
||||||
|
if not vg_layer or not vg_layer.isValid():
|
||||||
|
warning(
|
||||||
|
"Umringstatistik",
|
||||||
|
"Kein Verfahrensgebiet-Layer gewählt. Bitte zuerst ein Verfahrensgebiet laden.",
|
||||||
|
duration=6,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
self._set_busy(self.btn_umring_statistik, True, "Umringstatistik Grenzpunkte")
|
||||||
|
try:
|
||||||
|
result_layer = umringstatistik_grenzpunkte(self, grenzpunkte_layer, vg_layer)
|
||||||
|
if result_layer and result_layer.isValid():
|
||||||
|
iface.mapCanvas().refresh()
|
||||||
|
finally:
|
||||||
|
self._set_busy(self.btn_umring_statistik, False, "Umringstatistik Grenzpunkte")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Modul-Hilfsfunktionen
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _setze_bror_stil(layer) -> None:
|
||||||
|
"""Setzt eine einfache Darstellung für den BauRaumOrdnungsrecht-Layer.
|
||||||
|
|
||||||
|
Roter Umring, halbtransparente Füllung; kein QGIS-Kontext → stilles Ignorieren.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
layer :
|
||||||
|
BauRaumOrdnungsrecht-Memory-Layer (Polygon).
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from qgis.core import ( # type: ignore[import]
|
||||||
|
QgsSimpleFillSymbolLayer,
|
||||||
|
QgsFillSymbol,
|
||||||
|
QgsSingleSymbolRenderer,
|
||||||
|
)
|
||||||
|
from qgis.PyQt.QtGui import QColor # type: ignore[import]
|
||||||
|
from qgis.PyQt.QtCore import Qt # type: ignore[import]
|
||||||
|
|
||||||
|
fill = QgsSimpleFillSymbolLayer()
|
||||||
|
fill.setColor(QColor(220, 50, 50, 40)) # halbtransparent rot
|
||||||
|
fill.setStrokeColor(QColor(220, 50, 50))
|
||||||
|
fill.setStrokeWidth(0.8)
|
||||||
|
symbol = QgsFillSymbol([fill])
|
||||||
|
layer.setRenderer(QgsSingleSymbolRenderer(symbol))
|
||||||
|
layer.triggerRepaint()
|
||||||
|
except Exception:
|
||||||
|
pass # Kein QGIS-Kontext (Tests, Mock-Modus) → Standarddarstellung
|
||||||
|
|||||||
Reference in New Issue
Block a user