Compare commits

..
Author SHA1 Message Date
Michael Otto 182ea0532e fehlerhaften Name der metadata.template korrigiert
Release Plugin / release (push) Successful in 4s
2026-07-02 11:15:51 +02:00
Michael Otto 55a0d629d4 auf neuen Release-Workflow umgestellt
Release Plugin / release (push) Successful in 4s
2026-07-02 11:10:47 +02:00
Daniel 048bef1f10 changelog.txt aktualisiert
Release Plugin / release (push) Successful in 4s
2026-06-23 14:02:31 +02:00
Daniel dbdfd987c6 Merge pull request 'Feature/gemarkungen laden' (#25) from feature/gemarkungen_laden into unstable
Reviewed-on: #25
2026-06-23 13:58:40 +02:00
Daniel 8dbad87181 repaint des Flurstückslayers nach dem Anhängen ergänzt 2026-06-23 13:55:58 +02:00
Daniel 944fc82b1f anhängen-Funktion für Flurstücke 2026-06-23 13:54:08 +02:00
Daniel f36418b814 Button und Funktion "Flurstücke aus Gemarkungen laden" ergänzt 2026-06-23 13:30:11 +02:00
Daniel d0d0523417 Gemarkungsabschnitt über Flurstücke verschoben 2026-06-23 13:02:43 +02:00
Daniel d9e97b8b77 Gemarkungen laden-Button eingefügt 2026-06-23 12:57:35 +02:00
release-bot 10823b5e79 Release 26.5.3-testing 2026-05-22 12:39:54 +00:00
Daniel 18efb38a7f changelog.txt aktualisiert
Release Plugin / release (push) Successful in 4s
2026-05-22 14:39:21 +02:00
release-bot 6719ea7592 Release 26.5.2-testing 2026-05-22 12:35:00 +00:00
Daniel 1115921766 Merge pull request 'Feature/vkz differenzierung' (#20) from feature/VKZ-differenzierung into testing
Release Plugin / release (push) Successful in 4s
Reviewed-on: #20
2026-05-22 14:34:13 +02:00
Daniel 78c496f721 Farbgebung BauRaumOderBodenordnungsrecht gefixt (auf qml umgestellt) 2026-05-22 14:14:04 +02:00
Daniel 1db782471b Bezeichnung BauRaumOderBodenordnungsrech gefixt 2026-05-22 13:26:11 +02:00
Daniel 8bf089cd6a fokusverlust Statusleiste bei Kombinationsauswahl gefixt 2026-05-22 08:55:53 +02:00
Daniel 632c376497 Differenz=0-Bug gefixt 2026-05-22 08:47:13 +02:00
Daniel 7c833c7aa0 falsche Datumsangabe in Attributtagelle gefixt 2026-05-22 08:46:42 +02:00
Daniel 2a4cdd93dc Verfahrensgebiet-Unterschiedung bei ähnlicher VKZ ergänzt 2026-05-22 08:40:43 +02:00
release-bot 9eb2e81fd7 Release 26.5.1-testing 2026-05-06 11:16:00 +00:00
Daniel 6963e1759a Merge pull request 'Grenzpunkt-Funktionen' (#18) from unstable into testing
Release Plugin / release (push) Successful in 4s
Reviewed-on: #18
2026-05-06 13:15:23 +02:00
release-bot 3b47e41bbe Release 26.5.3-unstable 2026-05-06 10:17:14 +00:00
Daniel 1a9595598e Merge pull request 'grenzpunkttolerant auf 1mm gesetzt' (#17) from feature/grenzpunkte_laden into unstable
Release Plugin / release (push) Successful in 4s
Reviewed-on: #17
2026-05-06 12:16:47 +02:00
Daniel 4bbdb96bcd grenzpunkttolerant auf 1mm gesetzt 2026-05-06 11:57:50 +02:00
release-bot aa6166e652 Release 26.5.2-unstable 2026-05-06 09:50:27 +00:00
Daniel 12b52d0468 Merge pull request 'proxy-fix' (#16) from feature/grenzpunkte_laden into unstable
Release Plugin / release (push) Successful in 4s
Reviewed-on: #16
2026-05-06 11:49:45 +02:00
Daniel cbd7b84889 Merge branch 'unstable' of https://entwicklung.flurneuordnung-sachsen.de/AG_QGIS/Plugin_SN_Verfahrensgebiet into feature/grenzpunkte_laden 2026-05-06 11:45:50 +02:00
DanielandCopilot 28cfc75121 proxy-fix
Co-authored-by: Copilot <copilot@github.com>
2026-05-06 11:43:44 +02:00
release-bot cee02ae935 Release 26.5.1-unstable 2026-05-06 08:39:31 +00:00
14 changed files with 14351 additions and 127 deletions
@@ -1,11 +1,11 @@
[general] [general]
version=26.4.6-unstable version=
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/testing/
QGISMaximumVersion=4.99 QGISMaximumVersion=4.99
qgisMinimumVersion=3.40 qgisMinimumVersion=3.40
experimental=true experimental=true
+17
View File
@@ -1,3 +1,20 @@
- Gemarkungen aus ALKIS (vereinfacht) laden ergänzt
- Flurstücke aus Gemarkungsauswahl laden ergänzt
---
Version 26.5.3-testing:
- Nutzerauswahl bei mehreren VKZ-Treffern im BauRaumOderBodenordnungsrecht eingefügt
- Farbgebung und Benennung BauRaumOderBodenordnungsrecht-Layer geändert
- Fix: Abweichung von 0 m² löst Konflikt-Meldung aus
---
Version 26.5.2-testing:
---
Version 26.5.1-testing:
---
Version 26.5.3-unstable:
---
Version 26.5.2-unstable:
---
Version 26.5.1-unstable:
--- ---
Version 26.4.6-unstable: Version 26.4.6-unstable:
--- ---
+28 -11
View File
@@ -53,23 +53,28 @@ def _check_existing_flurstueck_layer(tab_widget):
if not existing_layers: if not existing_layers:
return None, LoadStatus.FIRST return None, LoadStatus.FIRST
reply = QMessageBox.question( msg = QMessageBox(tab_widget)
tab_widget, msg.setWindowTitle("Layer bereits vorhanden")
"Layer bereits vorhanden", msg.setText(
f"Der Layer '{FLST_LAYER_NAME}' existiert bereits.\n" f"Der Layer '{FLST_LAYER_NAME}' existiert bereits.\nWas soll geschehen?"
"Möchten Sie ihn löschen und neu laden?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No,
) )
if reply == QMessageBox.StandardButton.Yes: btn_ueberschreiben = msg.addButton("Überschreiben", QMessageBox.ButtonRole.DestructiveRole)
btn_anhaengen = msg.addButton("Anhängen", QMessageBox.ButtonRole.AcceptRole)
btn_abbrechen = msg.addButton("Abbrechen", QMessageBox.ButtonRole.RejectRole)
msg.setDefaultButton(btn_abbrechen)
msg.exec()
clicked = msg.clickedButton()
if clicked is btn_ueberschreiben:
for lyr in existing_layers: for lyr in existing_layers:
project.removeMapLayer(lyr.id()) project.removeMapLayer(lyr.id())
return None, LoadStatus.RELOAD return None, LoadStatus.RELOAD
elif clicked is btn_anhaengen:
return existing_layers[0], LoadStatus.APPEND
else: else:
return existing_layers[0], LoadStatus.KEEP return None, LoadStatus.NONE
def _lade_flurstuecke_per_gemarkung(schluessel_liste: List[str]): def lade_flurstuecke_per_gemarkung(schluessel_liste: List[str]):
"""Lädt Flurstücke für mehrere Gemarkungen, eine WFS-Anfrage pro Schlüssel. """Lädt Flurstücke für mehrere Gemarkungen, eine WFS-Anfrage pro Schlüssel.
Vermeidet das WFS-Feature-Limit einer einzelnen Abfrage mit mehreren Vermeidet das WFS-Feature-Limit einer einzelnen Abfrage mit mehreren
@@ -179,6 +184,7 @@ def flurstueck_alkis(
Bei Fehlern ist der Layer ``None`` und der Status :attr:`LoadStatus.NONE`. Bei Fehlern ist der Layer ``None`` und der Status :attr:`LoadStatus.NONE`.
""" """
# --- Schritt 1: Vorhandenen Layer behandeln ---------------------------- # --- Schritt 1: Vorhandenen Layer behandeln ----------------------------
append_target = None
if ohne_dialog: if ohne_dialog:
project = QgsProject.instance() project = QgsProject.instance()
hatte_layer = bool(project.mapLayersByName(FLST_LAYER_NAME)) hatte_layer = bool(project.mapLayersByName(FLST_LAYER_NAME))
@@ -189,6 +195,10 @@ def flurstueck_alkis(
existing, status = _check_existing_flurstueck_layer(tab_widget) existing, status = _check_existing_flurstueck_layer(tab_widget)
if status == LoadStatus.KEEP: if status == LoadStatus.KEEP:
return existing, status return existing, status
if status == LoadStatus.NONE:
return None, LoadStatus.NONE
if status == LoadStatus.APPEND:
append_target = existing
# --- Schritt 2: Gemarkungsschlüssel bestimmen ------------------------- # --- Schritt 2: Gemarkungsschlüssel bestimmen -------------------------
if ohne_dialog: if ohne_dialog:
@@ -226,7 +236,7 @@ def flurstueck_alkis(
return None, LoadStatus.NONE return None, LoadStatus.NONE
# --- Schritt 3: Flurstücke gemarkungsweise laden ---------------------- # --- Schritt 3: Flurstücke gemarkungsweise laden ----------------------
layer = _lade_flurstuecke_per_gemarkung(schluessel_liste) layer = lade_flurstuecke_per_gemarkung(schluessel_liste)
if not layer or not layer.isValid() or layer.featureCount() == 0: if not layer or not layer.isValid() or layer.featureCount() == 0:
if not ohne_dialog: if not ohne_dialog:
@@ -238,4 +248,11 @@ def flurstueck_alkis(
) )
return None, LoadStatus.NONE return None, LoadStatus.NONE
# --- Anhängen an vorhandenen Layer ------------------------------------
if status == LoadStatus.APPEND and append_target is not None:
append_target.dataProvider().addFeatures(list(layer.getFeatures()))
append_target.updateExtents()
append_target.triggerRepaint()
return append_target, LoadStatus.APPEND
return layer, status return layer, status
+21
View File
@@ -188,6 +188,27 @@ def gemarkung_schluessel_und_namen_von_vg_geometrie(
return sorted(result.items(), key=lambda t: t[0]) return sorted(result.items(), key=lambda t: t[0])
def lade_gemarkungen_layer():
"""Lädt alle Katasterbezirk-Objekte aus dem vereinfachten ALKIS-WFS
als benannten Layer „Gemarkungen".
Dieser Layer dient als Referenzkontext im Bearbeitungs-Tab und wird
mit dem Stil ``gemarkungen_beschriftet.qml`` versehen.
Returns
-------
QgsVectorLayer or None
Geladener Layer (Name ``"Gemarkungen"``) oder ``None`` bei Ungültigkeit
(z. B. Dienst nicht erreichbar).
"""
uri = (
f"url='{ALKIS_VEREINF_URL}' typename='{KATASTERBEZIRK_TYPENAME}' "
f"srsname='EPSG:25833'"
)
layer = QgsVectorLayer(uri, "Gemarkungen", "WFS")
return layer if (layer and layer.isValid()) else None
def gemarkung_schluessel_von_vg_geometrie(vg_geometrie) -> List[str]: def gemarkung_schluessel_von_vg_geometrie(vg_geometrie) -> List[str]:
"""Gibt ausschließlich die Gemarkungsnummern für die VG-Geometrie zurück. """Gibt ausschließlich die Gemarkungsnummern für die VG-Geometrie zurück.
+66 -54
View File
@@ -146,6 +146,9 @@ def _wfs_get_gml(url: str, typename: str, extra_params: dict) -> Optional[bytes]
GET-Requests mit ``BBOX=``-KVP-Parameter oder ResourceId-``FILTER`` (nur GET-Requests mit ``BBOX=``-KVP-Parameter oder ResourceId-``FILTER`` (nur
``xmlns:fes``, kein GML) werden dagegen akzeptiert. ``xmlns:fes``, kein GML) werden dagegen akzeptiert.
**Proxy-Unterstützung:** Verwendet ``QgsBlockingNetworkRequest``, das automatisch
die in QGIS konfigurierten Proxy-Einstellungen berücksichtigt.
Parameters Parameters
---------- ----------
url : url :
@@ -161,10 +164,10 @@ def _wfs_get_gml(url: str, typename: str, extra_params: dict) -> Optional[bytes]
bytes | None bytes | None
Rohe GML-Antwort oder ``None`` bei Fehler. Rohe GML-Antwort oder ``None`` bei Fehler.
""" """
import urllib.request
import urllib.parse import urllib.parse
import urllib.error from qgis.core import QgsMessageLog, Qgis, QgsBlockingNetworkRequest
from qgis.core import QgsMessageLog, Qgis from qgis.PyQt.QtNetwork import QNetworkRequest
from qgis.PyQt.QtCore import QUrl
params = { params = {
'SERVICE': 'WFS', 'SERVICE': 'WFS',
@@ -177,26 +180,34 @@ def _wfs_get_gml(url: str, typename: str, extra_params: dict) -> Optional[bytes]
full_url = url + '?' + urllib.parse.urlencode(params) full_url = url + '?' + urllib.parse.urlencode(params)
try: req = QNetworkRequest(QUrl(full_url))
with urllib.request.urlopen(full_url, timeout=120) as resp: req.setTransferTimeout(120_000)
gml_bytes = resp.read() blocker = QgsBlockingNetworkRequest()
except urllib.error.HTTPError as exc: err = blocker.get(req)
body_preview = exc.read(200).decode('utf-8', errors='replace')
if err != QgsBlockingNetworkRequest.ErrorCode.NoError:
msg = blocker.errorMessage()
QgsMessageLog.logMessage( QgsMessageLog.logMessage(
f"WFS GET HTTP {exc.code} ({typename}): {exc.reason}\n" f"WFS GET fehlgeschlagen ({typename}): {msg}\nURL: {full_url[:300]}",
"sn_verfahrensgebiet",
Qgis.MessageLevel.Critical,
)
warning("WFS GET", f"Netzwerk-/Proxy-Fehler: {msg}", duration=10)
return None
reply = blocker.reply()
http_status = reply.attribute(QNetworkRequest.Attribute.HttpStatusCodeAttribute)
gml_bytes = bytes(reply.content())
if http_status is not None and http_status >= 400:
body_preview = gml_bytes[:200].decode('utf-8', errors='replace')
QgsMessageLog.logMessage(
f"WFS GET HTTP {http_status} ({typename})\n"
f"URL: {full_url[:300]}\nAntwort: {body_preview}", f"URL: {full_url[:300]}\nAntwort: {body_preview}",
"sn_verfahrensgebiet", "sn_verfahrensgebiet",
Qgis.MessageLevel.Critical, Qgis.MessageLevel.Critical,
) )
warning("WFS GET", f"HTTP {exc.code}: {exc.reason}", duration=10) warning("WFS GET", f"HTTP {http_status}", duration=10)
return None
except Exception as exc:
QgsMessageLog.logMessage(
f"WFS GET fehlgeschlagen ({typename}): {exc}\nURL: {full_url[:300]}",
"sn_verfahrensgebiet",
Qgis.MessageLevel.Critical,
)
warning("WFS GET", f"Netzwerkfehler: {exc}", duration=10)
return None return None
if not gml_bytes: if not gml_bytes:
@@ -477,11 +488,7 @@ def _lade_grenzpunkte_als_dict(
) )
_gml_bytes = _wfs_get_gml(ALKIS_SF_URL, GRENZPUNKT_TYPENAME, {'FILTER': filter_xml}) _gml_bytes = _wfs_get_gml(ALKIS_SF_URL, GRENZPUNKT_TYPENAME, {'FILTER': filter_xml})
if not _gml_bytes: if not _gml_bytes:
warning( return {}, []
"Grenzpunkte (Diagnose)",
f"Batch {i + 1}/{anzahl_batches}: GET-Anfrage fehlgeschlagen – Batch wird übersprungen.",
duration=4,
)
else: else:
tmp = _parse_alkis_gml_memory_layer(_gml_bytes, "AX_Grenzpunkt", "_TmpGP", geom_prop=None) tmp = _parse_alkis_gml_memory_layer(_gml_bytes, "AX_Grenzpunkt", "_TmpGP", geom_prop=None)
if not tmp: if not tmp:
@@ -704,9 +711,12 @@ def _lade_gemarkung_extents_mit_puffer(
return extents return extents
def _wfs_hits_count(url: str, typename: str, extra_params: dict) -> int: def _wfs_hits_count(url: str, typename: str, extra_params: dict) -> Optional[int]:
"""Fragt die Anzahl der Features per ``resultType=hits`` ab. """Fragt die Anzahl der Features per ``resultType=hits`` ab.
**Proxy-Unterstützung:** Verwendet ``QgsBlockingNetworkRequest``, das automatisch
die in QGIS konfigurierten Proxy-Einstellungen berücksichtigt.
Parameters Parameters
---------- ----------
url : url :
@@ -714,17 +724,21 @@ def _wfs_hits_count(url: str, typename: str, extra_params: dict) -> int:
typename : typename :
Vollqualifizierter Feature-Typ. Vollqualifizierter Feature-Typ.
extra_params : extra_params :
Zus\u00e4tzliche KVP-Parameter (z.B. ``{'BBOX': '...'}``). Zusätzliche KVP-Parameter (z.B. ``{'BBOX': '...'}``).
Returns Returns
------- -------
int Optional[int]
Anzahl der Features oder ``-1`` wenn nicht ermittelbar. Anzahl der Features,
``-1`` wenn der Server antwortete aber ``numberMatched='unknown'`` lieferte
(gültiger WFS-Fallback, kein Fehler), oder
``None`` bei Netzwerk-/Proxy-Fehler (Aufrufer soll abbrechen).
""" """
import urllib.request
import urllib.parse import urllib.parse
import urllib.error
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
from qgis.core import QgsMessageLog, Qgis, QgsBlockingNetworkRequest
from qgis.PyQt.QtNetwork import QNetworkRequest
from qgis.PyQt.QtCore import QUrl
params = { params = {
'SERVICE': 'WFS', 'SERVICE': 'WFS',
@@ -736,11 +750,22 @@ def _wfs_hits_count(url: str, typename: str, extra_params: dict) -> int:
params.update(extra_params) params.update(extra_params)
full_url = url + '?' + urllib.parse.urlencode(params) full_url = url + '?' + urllib.parse.urlencode(params)
try:
with urllib.request.urlopen(full_url, timeout=30) as resp: req = QNetworkRequest(QUrl(full_url))
gml_bytes = resp.read() req.setTransferTimeout(30_000)
except Exception: blocker = QgsBlockingNetworkRequest()
return -1 err = blocker.get(req)
if err != QgsBlockingNetworkRequest.ErrorCode.NoError:
msg = blocker.errorMessage()
QgsMessageLog.logMessage(
f"WFS Hits-Abfrage fehlgeschlagen ({typename}): {msg}\nURL: {full_url[:300]}",
"sn_verfahrensgebiet",
Qgis.MessageLevel.Critical,
)
return None
gml_bytes = bytes(blocker.reply().content())
try: try:
root = ET.fromstring(gml_bytes) root = ET.fromstring(gml_bytes)
@@ -796,12 +821,7 @@ def _lade_punktort_ta_batches_per_gemarkung(
) )
_gml_bytes = _wfs_get_gml(ALKIS_SF_URL, PUNKTORT_TA_TYPENAME, {'BBOX': _punktort_ta_bbox_param(bbox)}) _gml_bytes = _wfs_get_gml(ALKIS_SF_URL, PUNKTORT_TA_TYPENAME, {'BBOX': _punktort_ta_bbox_param(bbox)})
if not _gml_bytes: if not _gml_bytes:
warning( return None
"Grenzpunkte (Diagnose)",
f"Gemarkung {_gemarkungsnummer}: GET-Anfrage fehlgeschlagen – wird übersprungen.",
duration=4,
)
continue
tmp = _parse_alkis_gml_memory_layer(_gml_bytes, "AX_PunktortTA", "_TmpPTA", geom_prop="position", extract_qualitaet=True) tmp = _parse_alkis_gml_memory_layer(_gml_bytes, "AX_PunktortTA", "_TmpPTA", geom_prop="position", extract_qualitaet=True)
if not tmp: if not tmp:
warning( warning(
@@ -997,6 +1017,8 @@ def lade_punktort_ta(
bbox_param = _punktort_ta_bbox_param(bbox) bbox_param = _punktort_ta_bbox_param(bbox)
n_hits = _wfs_hits_count(ALKIS_SF_URL, PUNKTORT_TA_TYPENAME, {'BBOX': bbox_param}) n_hits = _wfs_hits_count(ALKIS_SF_URL, PUNKTORT_TA_TYPENAME, {'BBOX': bbox_param})
if n_hits is None:
return None
n_batches = max(1, math.ceil(n_hits / _BBOX_BATCH)) if n_hits > 0 else 1 n_batches = max(1, math.ceil(n_hits / _BBOX_BATCH)) if n_hits > 0 else 1
info( info(
@@ -1022,13 +1044,7 @@ def lade_punktort_ta(
_gml_bytes = _wfs_get_gml(ALKIS_SF_URL, PUNKTORT_TA_TYPENAME, extra) _gml_bytes = _wfs_get_gml(ALKIS_SF_URL, PUNKTORT_TA_TYPENAME, extra)
if not _gml_bytes: if not _gml_bytes:
warning( return None
"Grenzpunkte (Diagnose)",
f"Gemarkung {gemarkungsnummer} Batch {b_idx + 1}: "
f"GET-Anfrage fehlgeschlagen – übersprungen.",
duration=4,
)
continue
tmp = _parse_alkis_gml_memory_layer( tmp = _parse_alkis_gml_memory_layer(
_gml_bytes, "AX_PunktortTA", "_TmpPTA", geom_prop="position", extract_qualitaet=True _gml_bytes, "AX_PunktortTA", "_TmpPTA", geom_prop="position", extract_qualitaet=True
@@ -1343,6 +1359,8 @@ def grenzpunkte_gemarkungsweise(
for gemarkungsnummer, bbox in extents.items(): for gemarkungsnummer, bbox in extents.items():
bbox_param = _punktort_ta_bbox_param(bbox) bbox_param = _punktort_ta_bbox_param(bbox)
n_hits = _wfs_hits_count(ALKIS_SF_URL, PUNKTORT_TA_TYPENAME, {'BBOX': bbox_param}) n_hits = _wfs_hits_count(ALKIS_SF_URL, PUNKTORT_TA_TYPENAME, {'BBOX': bbox_param})
if n_hits is None:
return None
hits_pro_gemarkung[gemarkungsnummer] = n_hits hits_pro_gemarkung[gemarkungsnummer] = n_hits
batches = max(1, math.ceil(n_hits / _BBOX_BATCH)) if n_hits > 0 else 1 batches = max(1, math.ceil(n_hits / _BBOX_BATCH)) if n_hits > 0 else 1
n_pta_batches_gesamt += batches n_pta_batches_gesamt += batches
@@ -1413,13 +1431,7 @@ def grenzpunkte_gemarkungsweise(
_gml_bytes = _wfs_get_gml(ALKIS_SF_URL, PUNKTORT_TA_TYPENAME, extra) _gml_bytes = _wfs_get_gml(ALKIS_SF_URL, PUNKTORT_TA_TYPENAME, extra)
if not _gml_bytes: if not _gml_bytes:
warning( return None
"Grenzpunkte (Diagnose)",
f"Gemarkung {gemarkungsnummer} Batch {b_idx + 1}: "
f"GET-Anfrage fehlgeschlagen – Batch wird übersprungen.",
duration=6,
)
continue
tmp = _parse_alkis_gml_memory_layer( tmp = _parse_alkis_gml_memory_layer(
_gml_bytes, "AX_PunktortTA", "_TmpPTA", geom_prop="position", extract_qualitaet=True _gml_bytes, "AX_PunktortTA", "_TmpPTA", geom_prop="position", extract_qualitaet=True
+1 -1
View File
@@ -61,7 +61,7 @@ KATEGORIE_GROSS = ">3cm"
KATEGORIE_UNBEKANNT = "unbekannt" KATEGORIE_UNBEKANNT = "unbekannt"
#: Maximaler Abstand (m) eines Punktes vom Umring, um als „auf dem Umring" zu gelten. #: Maximaler Abstand (m) eines Punktes vom Umring, um als „auf dem Umring" zu gelten.
UMRING_TOLERANZ_M: float = 0.1 UMRING_TOLERANZ_M: float = 0.01
#: Grenzwert für ``qualitaetsangaben_ax_dqpunktort_genauigkeitsstufe`` (3-cm-Schwelle). #: Grenzwert für ``qualitaetsangaben_ax_dqpunktort_genauigkeitsstufe`` (3-cm-Schwelle).
GENAUIGKEIT_GRENZWERT: int = 2100 GENAUIGKEIT_GRENZWERT: int = 2100
+64 -7
View File
@@ -9,10 +9,23 @@ from sn_basis.functions import (
from sn_basis.functions import QMessageBox 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
from sn_basis.functions.dialog_wrapper import ask_bror_kombination
alkis_sf_url = "https://geodienste.sachsen.de/aaa/public_alkis/sf/wfs" alkis_sf_url = "https://geodienste.sachsen.de/aaa/public_alkis/sf/wfs"
typename = "adv:AX_BauRaumOderBodenordnungsrecht" typename = "adv:AX_BauRaumOderBodenordnungsrecht"
#: Attribute, die aus dem BROR-WFS-Dienst geladen und im Memory-Layer gespeichert werden.
BROR_ATTRIBUTE_FELDER = [
"artderfestlegung",
"ausfuehrendestelle_ax_dienststelle_schluessel_stelle",
"adv_name",
"bezeichnung",
"datumanordnung",
"datumbesitzeinweisung",
"datumrechtskraft",
"datumabgabe",
]
class LoadStatus(Enum): class LoadStatus(Enum):
"""Status eines Layer-Ladevorgangs. """Status eines Layer-Ladevorgangs.
@@ -26,6 +39,7 @@ class LoadStatus(Enum):
FIRST = "first" # erstmalig geladen FIRST = "first" # erstmalig geladen
RELOAD = "reload" # neu geladen RELOAD = "reload" # neu geladen
KEEP = "keep" # vorhandener Layer behalten KEEP = "keep" # vorhandener Layer behalten
APPEND = "append" # Features an vorhandenen Layer angehängt
def _check_existing_layer(tab_widget): def _check_existing_layer(tab_widget):
@@ -83,11 +97,13 @@ def verfahrensgebiet_alkis(tab_widget):
if status == LoadStatus.KEEP: if status == LoadStatus.KEEP:
return existing, status, [] return existing, status, []
# WFS mit Filter laden # WFS mit Filter laden – nur erste 5 Stellen der VKZ für den Vergleich verwenden,
# da die Daten im Dienst ggf. abweichende Suffixe führen.
vkz_prefix = verfahrensnummer[:5]
wfs_uri_find = ( wfs_uri_find = (
f"url='{alkis_sf_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 '{vkz_prefix}%'"
) )
temp_layer = QgsVectorLayer(wfs_uri_find, "TempVerfahrensgebiet", "WFS") temp_layer = QgsVectorLayer(wfs_uri_find, "TempVerfahrensgebiet", "WFS")
@@ -101,7 +117,32 @@ def verfahrensgebiet_alkis(tab_widget):
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]) # Objekte nach (adv_name, bezeichnung) gruppieren
kombinationen: dict[tuple[str, str], list] = {}
for feat in features:
try:
adv_name_val = str(feat.attribute("adv_name") or "")
bezeichnung_val = str(feat.attribute("bezeichnung") or "")
except Exception:
adv_name_val = ""
bezeichnung_val = ""
key = (adv_name_val, bezeichnung_val)
if key not in kombinationen:
kombinationen[key] = []
kombinationen[key].append(feat)
if len(kombinationen) == 1:
selected_key = next(iter(kombinationen))
selected_feats = kombinationen[selected_key]
else:
# Mehrere Kombinationen → Nutzerauswahl
key_list = sorted(kombinationen.keys())
selected_key = ask_bror_kombination(tab_widget, key_list)
if selected_key is None:
return None, LoadStatus.NONE, []
selected_feats = kombinationen[selected_key]
union_geom = QgsGeometry.unaryUnion([f.geometry() for f in selected_feats])
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, []
@@ -110,14 +151,30 @@ def verfahrensgebiet_alkis(tab_widget):
crs = temp_layer.crs().authid() crs = temp_layer.crs().authid()
memory_layer = QgsVectorLayer(f"Polygon?crs={crs}", "Verfahrensgebiet", "memory") memory_layer = QgsVectorLayer(f"Polygon?crs={crs}", "Verfahrensgebiet", "memory")
pr = memory_layer.dataProvider() pr = memory_layer.dataProvider()
pr.addAttributes([QgsField("VKZ", QVariant.String)]) pr.addAttributes([QgsField(attr, QVariant.String) for attr in BROR_ATTRIBUTE_FELDER])
memory_layer.updateFields() memory_layer.updateFields()
# Attributwerte aus dem ersten Feature der gewählten Kombination übernehmen
first_feat = selected_feats[0]
attrs = []
for attr in BROR_ATTRIBUTE_FELDER:
try:
val = first_feat.attribute(attr)
if val is None:
attrs.append("")
elif type(val).__name__ == "QDate":
# QDate-Objekte als DD.MM.YYYY formatieren
attrs.append(val.toString("dd.MM.yyyy"))
else:
attrs.append(str(val))
except Exception:
attrs.append("")
feat_union = QgsFeature() feat_union = QgsFeature()
feat_union.setGeometry(union_geom) feat_union.setGeometry(union_geom)
feat_union.setAttributes([verfahrensnummer]) feat_union.setAttributes(attrs)
pr.addFeatures([feat_union]) pr.addFeatures([feat_union])
memory_layer.updateExtents() memory_layer.updateExtents()
# Status: FIRST oder RELOAD; features für Gemarkungsermittlung weitergeben # Status: FIRST oder RELOAD; selected_feats für Gemarkungsermittlung weitergeben
return memory_layer, status, features return memory_layer, status, selected_feats
+24 -8
View File
@@ -26,7 +26,7 @@ optionaler Umringsprüfung:
- Abschlussmeldung mit Zählung. - Abschlussmeldung mit Zählung.
Das BROR-Objekt (``AX_BauRaumOderBodenordnungsrecht``) bleibt bei Erfolg als Das BROR-Objekt (``AX_BauRaumOderBodenordnungsrecht``) bleibt bei Erfolg als
temporärer Layer ``BauRaumOrdnungsrecht (ALKIS)`` im Projekt erhalten. temporärer Layer ``BauRaumOderBodenordnungsrecht (ALKIS)`` im Projekt erhalten.
**Rückgabe:** **Rückgabe:**
``Tuple[enriched_layer, flst_layer, bror_layer, LoadStatus]`` ``Tuple[enriched_layer, flst_layer, bror_layer, LoadStatus]``
@@ -71,9 +71,12 @@ from sn_verfahrensgebiet.functions.knickpunkt_pruefung import (
# Feldname für das Verfahrenskennzeichen im enriched Layer # Feldname für das Verfahrenskennzeichen im enriched Layer
FELD_VKZ = "verfahrensnummer" FELD_VKZ = "verfahrensnummer"
#: Anzeigename des BROR-Layers im Projekt #: Anzeigename des BROR-Layers im Projekt
BROR_LAYER_NAME = "BauRaumOrdnungsrecht (ALKIS)" BROR_LAYER_NAME = "BauRaumOderBodenordnungsrecht (ALKIS)"
#: Projektvariable für die Flächentoleranz (in m²); Default 0 #: Projektvariable für die Flächentoleranz (in m²); Default 0
FLAECHEN_TOLERANZ_VARIABLE = "flaechen_toleranz_m2" FLAECHEN_TOLERANZ_VARIABLE = "flaechen_toleranz_m2"
#: Minimale implizite Flächentoleranz – Abweichungen unter diesem Wert
#: lösen keine Konfliktabfrage aus (Gleitkomma-Ungenauigkeiten vermeiden).
MINDEST_FLAECHEN_TOLERANZ_M2 = 0.01
@@ -223,7 +226,7 @@ def _vergleiche_flaechen(
Parameters Parameters
---------- ----------
bror_layer : bror_layer :
BauRaumOrdnungsrecht-Layer. BauRaumOderBodenordnungsrecht-Layer.
vg_layer : vg_layer :
Enriched Verfahrensgebiet-Layer. Enriched Verfahrensgebiet-Layer.
toleranz_m2 : toleranz_m2 :
@@ -236,7 +239,8 @@ def _vergleiche_flaechen(
""" """
bror_flaeche = _flaeche_des_layers(bror_layer) bror_flaeche = _flaeche_des_layers(bror_layer)
vg_flaeche = _flaeche_des_layers(vg_layer) vg_flaeche = _flaeche_des_layers(vg_layer)
stimmt_ueberein = abs(bror_flaeche - vg_flaeche) <= toleranz_m2 diff = abs(bror_flaeche - vg_flaeche)
stimmt_ueberein = diff < MINDEST_FLAECHEN_TOLERANZ_M2 or diff <= toleranz_m2
return bror_flaeche, vg_flaeche, stimmt_ueberein return bror_flaeche, vg_flaeche, stimmt_ueberein
@@ -260,7 +264,7 @@ def verfahrensgebiet_alkis_komplett(
6. ``_vergleiche_flaechen(...)`` – Flächenvergleich als Gate. 6. ``_vergleiche_flaechen(...)`` – Flächenvergleich als Gate.
7. Ggf. ``umringspruefung(...)`` nach Nutzerbestätigung. 7. Ggf. ``umringspruefung(...)`` nach Nutzerbestätigung.
Der BROR-Layer (``BauRaumOrdnungsrecht (ALKIS)``) bleibt bei Erfolg Der BROR-Layer (``BauRaumOderBodenordnungsrecht (ALKIS)``) bleibt bei Erfolg
im Projekt. Bei Abbruch/Fehler wird er entfernt, falls er in diesem im Projekt. Bei Abbruch/Fehler wird er entfernt, falls er in diesem
Lauf hinzugefügt wurde. Lauf hinzugefügt wurde.
@@ -274,7 +278,7 @@ def verfahrensgebiet_alkis_komplett(
Tuple[Optional[enriched_layer], Optional[flst_layer], Optional[bror_layer], LoadStatus] Tuple[Optional[enriched_layer], Optional[flst_layer], Optional[bror_layer], LoadStatus]
- ``enriched_layer``: Memory-Layer „Verfahrensgebiet". - ``enriched_layer``: Memory-Layer „Verfahrensgebiet".
- ``flst_layer``: Flurstücke-Layer (bleibt im Projekt). - ``flst_layer``: Flurstücke-Layer (bleibt im Projekt).
- ``bror_layer``: BROR-Layer ``BauRaumOrdnungsrecht (ALKIS)`` - ``bror_layer``: BROR-Layer ``BauRaumOderBodenordnungsrecht (ALKIS)``
(bleibt bei Erfolg im Projekt). (bleibt bei Erfolg im Projekt).
- :class:`LoadStatus`: ``FIRST``, ``RELOAD`` oder ``NONE``. - :class:`LoadStatus`: ``FIRST``, ``RELOAD`` oder ``NONE``.
""" """
@@ -298,6 +302,7 @@ def verfahrensgebiet_alkis_komplett(
return None, None, None, LoadStatus.NONE return None, None, None, LoadStatus.NONE
alkis_vg_layer, vg_status, _ = verfahrensgebiet_alkis(tab_widget) alkis_vg_layer, vg_status, _ = verfahrensgebiet_alkis(tab_widget)
progress.raise_to_front()
if vg_status == LoadStatus.NONE or not alkis_vg_layer: if vg_status == LoadStatus.NONE or not alkis_vg_layer:
return None, None, None, LoadStatus.NONE return None, None, None, LoadStatus.NONE
@@ -433,9 +438,9 @@ def verfahrensgebiet_alkis_komplett(
entscheidung = ask_detailpruefung_oder_vg_laden( entscheidung = ask_detailpruefung_oder_vg_laden(
tab_widget, tab_widget,
"Abweichung erkannt", "Abweichung erkannt",
f"Abweichung zwischen BauRaumOrdnungsrecht-Objekt und abgeleitetem " f"Abweichung zwischen BauRaumOderBodenordnungsrecht-Objekt und abgeleitetem "
f"Verfahrensgebiet erkannt.\n\n" f"Verfahrensgebiet erkannt.\n\n"
f"BauRaumOrdnungsrecht: {bror_flaeche:.2f} m²\n" f"BauRaumOderBodenordnungsrecht: {bror_flaeche:.2f} m²\n"
f"Verfahrensgebiet: {vg_flaeche:.2f} m²\n" f"Verfahrensgebiet: {vg_flaeche:.2f} m²\n"
f"Differenz: {diff:.2f} m²\n\n" f"Differenz: {diff:.2f} m²\n\n"
f"Wie möchten Sie fortfahren?", f"Wie möchten Sie fortfahren?",
@@ -488,6 +493,17 @@ def verfahrensgebiet_alkis_komplett(
if punktort_layer is None: if punktort_layer is None:
detail_progress.close() detail_progress.close()
abgeschlossen = True 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 return enriched_layer, flst_layer, alkis_vg_layer, vg_status
# Schritte 2…Ende: Knickpunktprüfung # Schritte 2…Ende: Knickpunktprüfung
+86
View File
@@ -0,0 +1,86 @@
<!DOCTYPE qgis PUBLIC 'http://mrcc.com/qgis.dtd' 'SYSTEM'>
<qgis layerType="Vector" styleCategories="Symbology" version="4.0.1-Norrköping">
<renderer-v2 enableorderby="0" forceraster="0" referencescale="-1" symbollevels="0" type="singleSymbol">
<symbols>
<symbol alpha="1" clip_to_extent="1" force_rhr="0" frame_rate="10" is_animated="0" name="0" type="fill">
<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 class="SimpleFill" enabled="1" id="{c32b7d26-d63f-4a20-b079-a5caa68d53db}" locked="0" 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="214,165,49,40,hsv:0.11719444394111633,0.77273213863372803,0.83793395757675171,0.15686275064945221"/>
<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="206,185,47,255,hsv:0.14452777802944183,0.77273213863372803,0.80689710378646851,1"/>
<Option name="outline_style" type="QString" value="solid"/>
<Option name="outline_width" type="QString" value="0.8"/>
<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>
</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 alpha="1" clip_to_extent="1" force_rhr="0" frame_rate="10" is_animated="0" name="" type="fill">
<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 class="SimpleFill" enabled="1" id="{9cbdf1f0-8055-4075-828c-a33555f4068e}" locked="0" 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.1372549,0.1372549,0.1372549,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>
<blendMode>0</blendMode>
<featureBlendMode>0</featureBlendMode>
<layerGeometryType>2</layerGeometryType>
</qgis>
File diff suppressed because it is too large Load Diff
+48 -2
View File
@@ -38,10 +38,21 @@ if "qgis" not in sys.modules:
sys.modules["qgis.utils"] = qgis_utils sys.modules["qgis.utils"] = qgis_utils
from sn_verfahrensgebiet.functions.gemarkung_alkis import ( from sn_verfahrensgebiet.functions.gemarkung_alkis import (
_extract_schluessel_und_name,
lade_gemarkungen_layer,
)
# NOTE: gemarkung_schluessel_und_namen_by_bbox / gemarkung_schluessel_by_bbox
# were renamed in the module refactoring. The tests below reference the old API
# and are skipped until the test bodies are updated.
try:
from sn_verfahrensgebiet.functions.gemarkung_alkis import (
gemarkung_schluessel_und_namen_by_bbox, gemarkung_schluessel_und_namen_by_bbox,
gemarkung_schluessel_by_bbox, gemarkung_schluessel_by_bbox,
_extract_schluessel_und_name, )
) _LEGACY_API_AVAILABLE = True
except ImportError:
_LEGACY_API_AVAILABLE = False
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -106,6 +117,7 @@ class TestExtractSchluesselUndName(unittest.TestCase):
# Tests: gemarkung_schluessel_und_namen_by_bbox # Tests: gemarkung_schluessel_und_namen_by_bbox
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@unittest.skipUnless(_LEGACY_API_AVAILABLE, "Legacy API (by_bbox) not available in current module")
class TestGemarkungSchluesselbbox(unittest.TestCase): class TestGemarkungSchluesselbbox(unittest.TestCase):
@patch("sn_verfahrensgebiet.functions.gemarkung_alkis._load_gemarkung_layer_with_bbox") @patch("sn_verfahrensgebiet.functions.gemarkung_alkis._load_gemarkung_layer_with_bbox")
@@ -185,6 +197,7 @@ class TestGemarkungSchluesselbbox(unittest.TestCase):
# Tests: gemarkung_schluessel_by_bbox (Convenience-Wrapper) # Tests: gemarkung_schluessel_by_bbox (Convenience-Wrapper)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@unittest.skipUnless(_LEGACY_API_AVAILABLE, "Legacy API (by_bbox) not available in current module")
class TestGemarkungSchluesselnurNummern(unittest.TestCase): class TestGemarkungSchluesselnurNummern(unittest.TestCase):
@patch("sn_verfahrensgebiet.functions.gemarkung_alkis.gemarkung_schluessel_und_namen_by_bbox") @patch("sn_verfahrensgebiet.functions.gemarkung_alkis.gemarkung_schluessel_und_namen_by_bbox")
@@ -203,5 +216,38 @@ class TestGemarkungSchluesselnurNummern(unittest.TestCase):
self.assertEqual(result, []) self.assertEqual(result, [])
# ---------------------------------------------------------------------------
# Tests: lade_gemarkungen_layer (Diensterreichbarkeit)
# ---------------------------------------------------------------------------
class TestLadeGemarkungenlayer(unittest.TestCase):
@patch("sn_verfahrensgebiet.functions.gemarkung_alkis.QgsVectorLayer")
def test_returns_layer_when_service_reachable(self, mock_qgsvl):
"""Dienst erreichbar (isValid=True) → Layer-Objekt zurückgegeben."""
layer = MagicMock()
layer.isValid.return_value = True
mock_qgsvl.return_value = layer
result = lade_gemarkungen_layer()
self.assertIs(result, layer)
mock_qgsvl.assert_called_once()
# Layer-Name muss "Gemarkungen" sein
args = mock_qgsvl.call_args[0]
self.assertEqual(args[1], "Gemarkungen")
@patch("sn_verfahrensgebiet.functions.gemarkung_alkis.QgsVectorLayer")
def test_returns_none_when_service_unreachable(self, mock_qgsvl):
"""Dienst nicht erreichbar (isValid=False) → None zurückgegeben."""
layer = MagicMock()
layer.isValid.return_value = False
mock_qgsvl.return_value = layer
result = lade_gemarkungen_layer()
self.assertIsNone(result)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
+65 -6
View File
@@ -61,12 +61,16 @@ class _DummyGeom:
class _DummyFeature: class _DummyFeature:
def __init__(self, geom=None): def __init__(self, geom=None, attrs=None):
self._geom = geom or _DummyGeom() self._geom = geom or _DummyGeom()
self._attrs = attrs or {}
def geometry(self): def geometry(self):
return self._geom return self._geom
def attribute(self, name):
return self._attrs.get(name)
class _DummyTempLayer: class _DummyTempLayer:
def __init__(self, valid=True, features=None): def __init__(self, valid=True, features=None):
@@ -139,7 +143,7 @@ class TestVerfahrensgebietAlkis(unittest.TestCase):
def test_verfahrensgebiet_alkis_fails_when_verfahrensnummer_missing(self, _mock_get_var): def test_verfahrensgebiet_alkis_fails_when_verfahrensnummer_missing(self, _mock_get_var):
_DummyMessageBox.last_critical = None _DummyMessageBox.last_critical = None
layer, status = vg.verfahrensgebiet_alkis(tab_widget=None) layer, status, feats = vg.verfahrensgebiet_alkis(tab_widget=None)
self.assertIsNone(layer) self.assertIsNone(layer)
self.assertEqual(status, vg.LoadStatus.NONE) self.assertEqual(status, vg.LoadStatus.NONE)
@@ -153,7 +157,7 @@ class TestVerfahrensgebietAlkis(unittest.TestCase):
_DummyMessageBox.last_critical = None _DummyMessageBox.last_critical = None
mock_qgsvectorlayer.return_value = _DummyTempLayer(valid=False) mock_qgsvectorlayer.return_value = _DummyTempLayer(valid=False)
layer, status = vg.verfahrensgebiet_alkis(tab_widget=None) layer, status, feats = vg.verfahrensgebiet_alkis(tab_widget=None)
self.assertIsNone(layer) self.assertIsNone(layer)
self.assertEqual(status, vg.LoadStatus.NONE) self.assertEqual(status, vg.LoadStatus.NONE)
@@ -165,8 +169,10 @@ class TestVerfahrensgebietAlkis(unittest.TestCase):
@patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.get_variable", return_value="VKZ123") @patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.get_variable", return_value="VKZ123")
@patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.QgsVectorLayer") @patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.QgsVectorLayer")
@patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.QMessageBox", new=_DummyMessageBox) @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): def test_verfahrensgebiet_alkis_success_single_kombination(self, mock_qgsvectorlayer, _mock_get_var, _mock_check, _mock_union, mock_feature_cls):
temp_layer = _DummyTempLayer(valid=True, features=[_DummyFeature()]) """Einzelne (adv_name, bezeichnung)-Kombination → kein Dialog, Memory-Layer zurück."""
feat = _DummyFeature(attrs={"adv_name": "Bodensanierung", "bezeichnung": "VKZ12/001"})
temp_layer = _DummyTempLayer(valid=True, features=[feat])
memory_layer = _DummyMemoryLayer() memory_layer = _DummyMemoryLayer()
mock_qgsvectorlayer.side_effect = [temp_layer, memory_layer] mock_qgsvectorlayer.side_effect = [temp_layer, memory_layer]
@@ -174,10 +180,63 @@ class TestVerfahrensgebietAlkis(unittest.TestCase):
mock_feature.setGeometry.return_value = None mock_feature.setGeometry.return_value = None
mock_feature.setAttributes.return_value = None mock_feature.setAttributes.return_value = None
layer, status = vg.verfahrensgebiet_alkis(tab_widget=None) layer, status, feats = vg.verfahrensgebiet_alkis(tab_widget=None)
self.assertIs(layer, memory_layer) self.assertIs(layer, memory_layer)
self.assertEqual(status, vg.LoadStatus.FIRST) self.assertEqual(status, vg.LoadStatus.FIRST)
self.assertEqual(len(feats), 1)
@patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.ask_bror_kombination")
@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_multi_kombination_dialog_auswahl(
self, mock_qgsvectorlayer, _mock_get_var, _mock_check, _mock_union, mock_feature_cls, mock_dialog
):
"""Zwei verschiedene Kombinationen → Dialog wird gezeigt, gewählte Features genutzt."""
feat1 = _DummyFeature(attrs={"adv_name": "ADV-A", "bezeichnung": "VKZ12/001"})
feat2 = _DummyFeature(attrs={"adv_name": "ADV-B", "bezeichnung": "VKZ12/002"})
temp_layer = _DummyTempLayer(valid=True, features=[feat1, feat2])
memory_layer = _DummyMemoryLayer()
mock_qgsvectorlayer.side_effect = [temp_layer, memory_layer]
mock_dialog.return_value = ("ADV-A", "VKZ12/001")
mock_feature = mock_feature_cls.return_value
mock_feature.setGeometry.return_value = None
mock_feature.setAttributes.return_value = None
layer, status, feats = vg.verfahrensgebiet_alkis(tab_widget=None)
mock_dialog.assert_called_once()
self.assertIs(layer, memory_layer)
self.assertEqual(status, vg.LoadStatus.FIRST)
# Nur feat1 gehört zur gewählten Kombination
self.assertEqual(feats, [feat1])
@patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.ask_bror_kombination")
@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_multi_kombination_dialog_abbruch(
self, mock_qgsvectorlayer, _mock_get_var, _mock_check, mock_dialog
):
"""Zwei verschiedene Kombinationen → Nutzer bricht Dialog ab → NONE."""
feat1 = _DummyFeature(attrs={"adv_name": "ADV-A", "bezeichnung": "VKZ12/001"})
feat2 = _DummyFeature(attrs={"adv_name": "ADV-B", "bezeichnung": "VKZ12/002"})
temp_layer = _DummyTempLayer(valid=True, features=[feat1, feat2])
mock_qgsvectorlayer.return_value = temp_layer
mock_dialog.return_value = None # Nutzer bricht ab
layer, status, feats = vg.verfahrensgebiet_alkis(tab_widget=None)
mock_dialog.assert_called_once()
self.assertIsNone(layer)
self.assertEqual(status, vg.LoadStatus.NONE)
self.assertEqual(feats, [])
if __name__ == "__main__": if __name__ == "__main__":
+81
View File
@@ -87,12 +87,14 @@ class TestWorkingTab(unittest.TestCase):
tab = WorkingTab.__new__(WorkingTab) tab = WorkingTab.__new__(WorkingTab)
tab.btn_verf_alkis = _DummyButton() tab.btn_verf_alkis = _DummyButton()
tab.btn_flurst_alkis = _DummyButton() tab.btn_flurst_alkis = _DummyButton()
tab.btn_gem_alkis = _DummyButton()
tab.btn_grenz_alkis = _DummyButton() tab.btn_grenz_alkis = _DummyButton()
tab.btn_verf_aus_auswahl = _DummyButton() tab.btn_verf_aus_auswahl = _DummyButton()
tab.btn_verf_aus_liste = _DummyButton() tab.btn_verf_aus_liste = _DummyButton()
tab.verf_layer_combo = _DummyCombo() tab.verf_layer_combo = _DummyCombo()
tab.haken_verf = types.SimpleNamespace(setText=lambda _t: None, setStyleSheet=lambda _s: None) 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_flurst = types.SimpleNamespace(setText=lambda _t: None, setStyleSheet=lambda _s: None)
tab.haken_gem = types.SimpleNamespace(setText=lambda _t: None, setStyleSheet=lambda _s: None)
tab.haken_grenz = 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 tab.setze_haken = lambda *_args, **_kwargs: None
# Pruefmanager-Mock: verarbeite() wird im Test ggf. überschrieben # Pruefmanager-Mock: verarbeite() wird im Test ggf. überschrieben
@@ -484,6 +486,85 @@ class TestWorkingTab(unittest.TestCase):
tab.handle_verf_aus_liste() tab.handle_verf_aus_liste()
mock_info.assert_called_once() mock_info.assert_called_once()
# ------------------------------------------------------------------
# handle_gem_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.lade_gemarkungen_layer")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.QgsProject.instance")
def test_handle_gem_alkis_service_reachable_adds_layer(
self, mock_project_inst, mock_lade, mock_apply_style, mock_success
):
"""Dienst erreichbar → Layer ins Projekt, Stil angewendet, Haken gesetzt."""
tab = self._make_tab()
haken_calls = []
tab.setze_haken = lambda lbl, v: haken_calls.append((lbl, v))
layer = _DummyLayer(valid=True, layer_name="Gemarkungen")
mock_lade.return_value = layer
project = _DummyProject()
project.mapLayersByName = lambda _n: []
mock_project_inst.return_value = project
tab.handle_gem_alkis()
self.assertIn(layer, project.added)
mock_apply_style.assert_called_once_with(layer, "gemarkungen_beschriftet.qml")
mock_success.assert_called_once()
self.assertEqual(len(haken_calls), 1)
self.assertIs(haken_calls[0][0], tab.haken_gem)
self.assertTrue(haken_calls[0][1])
self.assertTrue(tab.btn_gem_alkis.isEnabled()) # finally
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.warning")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.lade_gemarkungen_layer")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.QgsProject.instance")
def test_handle_gem_alkis_service_unreachable_shows_warning(
self, mock_project_inst, mock_lade, mock_warning
):
"""Dienst nicht erreichbar (None) → warning, kein addMapLayer."""
tab = self._make_tab()
haken_calls = []
tab.setze_haken = lambda lbl, v: haken_calls.append((lbl, v))
mock_lade.return_value = None
project = _DummyProject()
project.mapLayersByName = lambda _n: []
mock_project_inst.return_value = project
tab.handle_gem_alkis()
mock_warning.assert_called_once()
self.assertEqual(len(project.added), 0)
self.assertEqual(len(haken_calls), 0)
self.assertTrue(tab.btn_gem_alkis.isEnabled()) # finally
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.info")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.lade_gemarkungen_layer")
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.QgsProject.instance")
def test_handle_gem_alkis_layer_already_exists_shows_info(
self, mock_project_inst, mock_lade, mock_info
):
"""Layer 'Gemarkungen' bereits vorhanden → info, kein erneutes Laden."""
tab = self._make_tab()
haken_calls = []
tab.setze_haken = lambda lbl, v: haken_calls.append((lbl, v))
existing_layer = _DummyLayer(valid=True, layer_name="Gemarkungen")
project = _DummyProject()
project.mapLayersByName = lambda _n: [existing_layer]
mock_project_inst.return_value = project
tab.handle_gem_alkis()
mock_info.assert_called_once()
mock_lade.assert_not_called()
self.assertEqual(len(project.added), 0)
self.assertTrue(tab.btn_gem_alkis.isEnabled()) # finally
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
+225 -36
View File
@@ -14,10 +14,19 @@ 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 LoadStatus from sn_verfahrensgebiet.functions.verfahrensgebiet_alkis import LoadStatus
from sn_verfahrensgebiet.functions.flurstueck_alkis import flurstueck_alkis from sn_verfahrensgebiet.functions.flurstueck_alkis import (
flurstueck_alkis,
lade_flurstuecke_per_gemarkung,
ALKIS_SF_URL,
FLST_LAYER_NAME,
)
from sn_verfahrensgebiet.functions.verfahrensgebiet_alkis_komplett import verfahrensgebiet_alkis_komplett 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.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.gemarkung_alkis import (
gemarkung_schluessel_und_namen_von_vg_geometrie,
lade_gemarkungen_layer,
ALKIS_VEREINF_URL,
)
from sn_verfahrensgebiet.functions.umringstatistik_grenzpunkte import umringstatistik_grenzpunkte from sn_verfahrensgebiet.functions.umringstatistik_grenzpunkte import umringstatistik_grenzpunkte
from sn_verfahrensgebiet.functions.verfahrensgebiet_edit_service import ( from sn_verfahrensgebiet.functions.verfahrensgebiet_edit_service import (
pruefe_flurstueck_layer, pruefe_flurstueck_layer,
@@ -75,7 +84,21 @@ class WorkingTab(QWidget):
self.verf_layer_combo.layerChanged.connect(self._on_verf_layer_changed) self.verf_layer_combo.layerChanged.connect(self._on_verf_layer_changed)
layout.addWidget(self.verf_layer_combo) layout.addWidget(self.verf_layer_combo)
# Abschnitt 2: Flurstücke # Abschnitt 2: Gemarkungen
row_gem_header = QHBoxLayout()
lbl_gem = QLabel("<b>Gemarkungen</b>")
self.haken_gem = self._haken_label()
row_gem_header.addWidget(lbl_gem)
row_gem_header.addWidget(self.haken_gem)
row_gem_header.addStretch()
layout.addLayout(row_gem_header)
row_gem_buttons = QHBoxLayout()
self.btn_gem_alkis = QPushButton("Aus ALKIS laden")
row_gem_buttons.addWidget(self.btn_gem_alkis)
layout.addLayout(row_gem_buttons)
# Abschnitt 2b: 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()
@@ -91,7 +114,12 @@ 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_flurst_gem_buttons = QHBoxLayout()
self.btn_flurst_aus_gemarkungen = QPushButton("aus gewählten Gemarkungen laden")
row_flurst_gem_buttons.addWidget(self.btn_flurst_aus_gemarkungen)
layout.addLayout(row_flurst_gem_buttons)
# Abschnitt 2c: Grenzpunkte
row_grenz_header = QHBoxLayout() row_grenz_header = QHBoxLayout()
lbl_grenz = QLabel("<b>Grenzpunkte</b>") lbl_grenz = QLabel("<b>Grenzpunkte</b>")
self.haken_grenz = self._haken_label() self.haken_grenz = self._haken_label()
@@ -133,6 +161,8 @@ class WorkingTab(QWidget):
# 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_flurst_alkis.clicked.connect(self.handle_flurst_alkis)
self.btn_flurst_aus_gemarkungen.clicked.connect(self.handle_flurst_aus_gemarkungen)
self.btn_gem_alkis.clicked.connect(self.handle_gem_alkis)
self.btn_grenz_alkis.clicked.connect(self.handle_grenz_alkis) self.btn_grenz_alkis.clicked.connect(self.handle_grenz_alkis)
self.btn_umring_statistik.clicked.connect(self.handle_umringstatistik) 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_auswahl.clicked.connect(self.handle_verf_aus_auswahl)
@@ -169,9 +199,9 @@ class WorkingTab(QWidget):
apply_style(flst_layer, "GIS_Flst_Beschriftung_ALKIS_NAS.qml") apply_style(flst_layer, "GIS_Flst_Beschriftung_ALKIS_NAS.qml")
self.setze_haken(self.haken_flurst, True) self.setze_haken(self.haken_flurst, True)
# BROR-Layer: einfache Darstellung setzen (roter Umring, transparent) # BROR-Layer: QML-Stil anwenden
if bror_layer and bror_layer.isValid(): if bror_layer and bror_layer.isValid():
_setze_bror_stil(bror_layer) apply_style(bror_layer, "BauRaumOderBodenordnungsrecht.qml")
# Enriched VG-Layer ins Projekt (ggf. bereits registriert, z.B. bei "als VG laden") # Enriched VG-Layer ins Projekt (ggf. bereits registriert, z.B. bei "als VG laden")
if not project.mapLayer(enriched_layer.id()): if not project.mapLayer(enriched_layer.id()):
@@ -268,9 +298,186 @@ class WorkingTab(QWidget):
self.setze_haken(self.haken_flurst, True) self.setze_haken(self.haken_flurst, True)
info("Flurstücke", "Vorhandener Layer wurde behalten.", duration=4) info("Flurstücke", "Vorhandener Layer wurde behalten.", duration=4)
elif status == LoadStatus.APPEND:
self.setze_haken(self.haken_flurst, True)
iface.mapCanvas().refresh()
success("Flurstücke", "Features wurden an den vorhandenen Layer angehängt.", duration=5)
finally: finally:
self._set_busy(self.btn_flurst_alkis, False, "Aus ALKIS laden") self._set_busy(self.btn_flurst_alkis, False, "Aus ALKIS laden")
def handle_flurst_aus_gemarkungen(self) -> None:
"""Lädt Flurstücke aus ALKIS anhand der Auswahl im Layer 'Gemarkungen'.
- Kein Layer 'Gemarkungen': Layer wird geladen, Nutzer aufgefordert Auswahl zu treffen.
- Keine Auswahl: Warnmeldung.
- Auswahl vorhanden: Gemarkungsschlüssel extrahieren und Flurstücke direkt laden.
"""
self._set_busy(self.btn_flurst_aus_gemarkungen, True, "aus gewählten Gemarkungen laden")
try:
project = QgsProject.instance()
# Fall A: Layer 'Gemarkungen' nicht vorhanden → laden
gem_layers = project.mapLayersByName("Gemarkungen")
if not gem_layers:
gem_layer = lade_gemarkungen_layer()
if not gem_layer:
error(
"Gemarkungen",
f"Gemarkungslayer konnte nicht geladen werden – "
f"Dienst nicht erreichbar ({ALKIS_VEREINF_URL}). "
f"Bitte Netzwerkverbindung prüfen.",
duration=0,
)
return
project.addMapLayer(gem_layer)
apply_style(gem_layer, "gemarkungen_beschriftet.qml")
self.setze_haken(self.haken_gem, True)
QMessageBox.information(
self,
"Gemarkungen",
"Gemarkungslayer wurde geladen – bitte Gemarkungen auswählen.",
)
return
gemarkungen_layer = gem_layers[0]
# Fall B: Keine Auswahl
if not gemarkungen_layer.selectedFeatureCount():
QMessageBox.warning(
self,
"Gemarkungen",
"Bitte mindestens eine Gemarkung im Layer Gemarkungen auswählen.",
)
return
# Fall C: Auswahl vorhanden → Schlüssel extrahieren
vorgabe_schluessel = []
vorgabe_namen = []
for feat in gemarkungen_layer.selectedFeatures():
try:
schluessel = str(feat["schluessel"]).strip()
nummer = schluessel[-4:] if len(schluessel) >= 4 else schluessel
except Exception:
nummer = ""
try:
name = str(feat["ave_name"]).strip()
except Exception:
name = ""
if nummer:
vorgabe_schluessel.append(nummer)
vorgabe_namen.append(name)
if not vorgabe_schluessel:
error(
"Flurstücke",
"Aus der Gemarkungsauswahl konnten keine gültigen Gemarkungsschlüssel extrahiert werden.",
duration=8,
)
return
# Prüfen ob bereits ein Flurstücke-Layer vorhanden → 3-Knopf-Dialog
existing_flst = project.mapLayersByName(FLST_LAYER_NAME)
if existing_flst:
msg_box = QMessageBox(self)
msg_box.setWindowTitle("Layer bereits vorhanden")
msg_box.setText(
f"Der Layer '{FLST_LAYER_NAME}' existiert bereits.\nWas soll geschehen?"
)
btn_ueberschreiben = msg_box.addButton("Überschreiben", QMessageBox.ButtonRole.DestructiveRole)
btn_anhaengen = msg_box.addButton("Anhängen", QMessageBox.ButtonRole.AcceptRole)
btn_abbrechen = msg_box.addButton("Abbrechen", QMessageBox.ButtonRole.RejectRole)
msg_box.setDefaultButton(btn_abbrechen)
msg_box.exec()
clicked = msg_box.clickedButton()
if clicked is btn_abbrechen:
return
if clicked is btn_anhaengen:
temp_layer = lade_flurstuecke_per_gemarkung(vorgabe_schluessel)
if not temp_layer or not temp_layer.isValid() or temp_layer.featureCount() == 0:
error(
"Flurstücke",
f"Flurstücke konnten nicht geladen werden – WFS-Dienst ({ALKIS_SF_URL}) "
f"nicht erreichbar oder keine Features für die gewählten Gemarkungen.",
duration=0,
)
return
existing_flst[0].dataProvider().addFeatures(list(temp_layer.getFeatures()))
existing_flst[0].updateExtents()
iface.mapCanvas().refresh()
self.setze_haken(self.haken_flurst, True)
success("Flurstücke", "Features wurden an den vorhandenen Layer angehängt.", duration=5)
return
# else: Überschreiben → fall-through zum flurstueck_alkis(ohne_dialog=True)-Aufruf
# Flurstücke direkt laden (kein Dialog)
layer, status = flurstueck_alkis(
self,
vorgabe_schluessel=vorgabe_schluessel,
vorgabe_namen=vorgabe_namen,
ohne_dialog=True,
)
if not layer or not layer.isValid():
error(
"Flurstücke",
f"Flurstücke konnten nicht geladen werden – WFS-Dienst ({ALKIS_SF_URL}) "
f"nicht erreichbar oder keine Features für die gewählten Gemarkungen.",
duration=0,
)
return
if status in (LoadStatus.FIRST, LoadStatus.RELOAD):
project.addMapLayer(layer)
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_aus_gemarkungen, False, "aus gewählten Gemarkungen laden")
# ------------------------------------------------------------------
# Gemarkungen-Handler
# ------------------------------------------------------------------
def handle_gem_alkis(self) -> None:
"""Lädt den KatasterBezirk-Layer aus dem vereinfachten ALKIS-WFS
als Layer „Gemarkungen" ins Projekt.
Ist ein Layer namens „Gemarkungen" bereits vorhanden, wird er behalten
und eine Info-Meldung angezeigt (kein doppeltes Laden).
"""
self._set_busy(self.btn_gem_alkis, True, "Aus ALKIS laden")
try:
existing = QgsProject.instance().mapLayersByName("Gemarkungen")
if existing:
self.setze_haken(self.haken_gem, True)
info("Gemarkungen", "Layer 'Gemarkungen' ist bereits im Projekt vorhanden.", duration=4)
return
layer = lade_gemarkungen_layer()
if not layer:
warning(
"Gemarkungen",
"Layer konnte nicht geladen werden – Dienst nicht erreichbar oder ungültig.",
duration=6,
)
return
QgsProject.instance().addMapLayer(layer)
apply_style(layer, "gemarkungen_beschriftet.qml")
self.setze_haken(self.haken_gem, True)
success("Gemarkungen", "Layer 'Gemarkungen' wurde geladen.", duration=4)
finally:
self._set_busy(self.btn_gem_alkis, False, "Aus ALKIS laden")
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# Verfahrensgebiet-erstellen-Handler # Verfahrensgebiet-erstellen-Handler
# ------------------------------------------------------------------ # ------------------------------------------------------------------
@@ -487,10 +694,19 @@ class WorkingTab(QWidget):
if joined_layer and joined_layer.isValid(): if joined_layer and joined_layer.isValid():
iface.mapCanvas().refresh() iface.mapCanvas().refresh()
self.setze_haken(self.haken_grenz, True) self.setze_haken(self.haken_grenz, True)
success( QMessageBox.information(
"Grenzpunkte", self,
"Grenzpunkte geladen",
f"Grenzpunkte erfolgreich geladen ({joined_layer.featureCount()} Objekte).", f"Grenzpunkte erfolgreich geladen ({joined_layer.featureCount()} Objekte).",
duration=5, )
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: finally:
self._set_busy(self.btn_grenz_alkis, False, "Grenzpunkte aus ALKIS laden") self._set_busy(self.btn_grenz_alkis, False, "Grenzpunkte aus ALKIS laden")
@@ -538,31 +754,4 @@ class WorkingTab(QWidget):
# Modul-Hilfsfunktionen # 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