diff --git a/functions/verfahrensgebiet_alkis.py b/functions/verfahrensgebiet_alkis.py index 00e05e2..f187cfc 100644 --- a/functions/verfahrensgebiet_alkis.py +++ b/functions/verfahrensgebiet_alkis.py @@ -9,10 +9,23 @@ from sn_basis.functions import ( from sn_basis.functions import QMessageBox from sn_basis.functions.qt_wrapper import QVariant 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" 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): """Status eines Layer-Ladevorgangs. @@ -83,11 +96,13 @@ def verfahrensgebiet_alkis(tab_widget): if status == LoadStatus.KEEP: 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 = ( f"url='{alkis_sf_url}' typename='{typename}' srsname='EPSG:25833' " 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") @@ -101,7 +116,32 @@ def verfahrensgebiet_alkis(tab_widget): f"Verfahrenskennzeichen {verfahrensnummer} nicht im WFS gefunden.") 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(): QMessageBox.critical(tab_widget, "Fehler", "Keine Geometrien zum Verschmelzen gefunden.") return None, LoadStatus.NONE, [] @@ -110,14 +150,30 @@ def verfahrensgebiet_alkis(tab_widget): crs = temp_layer.crs().authid() memory_layer = QgsVectorLayer(f"Polygon?crs={crs}", "Verfahrensgebiet", "memory") 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() + # 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.setGeometry(union_geom) - feat_union.setAttributes([verfahrensnummer]) + feat_union.setAttributes(attrs) pr.addFeatures([feat_union]) memory_layer.updateExtents() - # Status: FIRST oder RELOAD; features für Gemarkungsermittlung weitergeben - return memory_layer, status, features + # Status: FIRST oder RELOAD; selected_feats für Gemarkungsermittlung weitergeben + return memory_layer, status, selected_feats diff --git a/functions/verfahrensgebiet_alkis_komplett.py b/functions/verfahrensgebiet_alkis_komplett.py index 7de8af0..efae825 100644 --- a/functions/verfahrensgebiet_alkis_komplett.py +++ b/functions/verfahrensgebiet_alkis_komplett.py @@ -26,7 +26,7 @@ optionaler Umringsprüfung: - Abschlussmeldung mit Zählung. 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:** ``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 FELD_VKZ = "verfahrensnummer" #: 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 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 ---------- bror_layer : - BauRaumOrdnungsrecht-Layer. + BauRaumOderBodenordnungsrecht-Layer. vg_layer : Enriched Verfahrensgebiet-Layer. toleranz_m2 : @@ -236,7 +239,8 @@ def _vergleiche_flaechen( """ bror_flaeche = _flaeche_des_layers(bror_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 @@ -260,7 +264,7 @@ def verfahrensgebiet_alkis_komplett( 6. ``_vergleiche_flaechen(...)`` – Flächenvergleich als Gate. 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 Lauf hinzugefügt wurde. @@ -274,7 +278,7 @@ def verfahrensgebiet_alkis_komplett( 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)`` + - ``bror_layer``: BROR-Layer ``BauRaumOderBodenordnungsrecht (ALKIS)`` (bleibt bei Erfolg im Projekt). - :class:`LoadStatus`: ``FIRST``, ``RELOAD`` oder ``NONE``. """ @@ -298,6 +302,7 @@ def verfahrensgebiet_alkis_komplett( return None, None, None, LoadStatus.NONE alkis_vg_layer, vg_status, _ = verfahrensgebiet_alkis(tab_widget) + progress.raise_to_front() if vg_status == LoadStatus.NONE or not alkis_vg_layer: return None, None, None, LoadStatus.NONE @@ -433,9 +438,9 @@ def verfahrensgebiet_alkis_komplett( entscheidung = ask_detailpruefung_oder_vg_laden( tab_widget, "Abweichung erkannt", - f"Abweichung zwischen BauRaumOrdnungsrecht-Objekt und abgeleitetem " + f"Abweichung zwischen BauRaumOderBodenordnungsrecht-Objekt und abgeleitetem " 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"Differenz: {diff:.2f} m²\n\n" f"Wie möchten Sie fortfahren?", diff --git a/styles/BauRaumOderBodenordnungsrecht.qml b/styles/BauRaumOderBodenordnungsrecht.qml new file mode 100644 index 0000000..5bf1a6b --- /dev/null +++ b/styles/BauRaumOderBodenordnungsrecht.qml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 0 + 2 + diff --git a/tests/test_verfahrensgebiet_alkis.py b/tests/test_verfahrensgebiet_alkis.py index 46ba82b..9ed7c0c 100644 --- a/tests/test_verfahrensgebiet_alkis.py +++ b/tests/test_verfahrensgebiet_alkis.py @@ -61,12 +61,16 @@ class _DummyGeom: class _DummyFeature: - def __init__(self, geom=None): + def __init__(self, geom=None, attrs=None): self._geom = geom or _DummyGeom() + self._attrs = attrs or {} def geometry(self): return self._geom + def attribute(self, name): + return self._attrs.get(name) + class _DummyTempLayer: 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): _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.assertEqual(status, vg.LoadStatus.NONE) @@ -153,7 +157,7 @@ class TestVerfahrensgebietAlkis(unittest.TestCase): _DummyMessageBox.last_critical = None 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.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.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()]) + def test_verfahrensgebiet_alkis_success_single_kombination(self, mock_qgsvectorlayer, _mock_get_var, _mock_check, _mock_union, mock_feature_cls): + """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() mock_qgsvectorlayer.side_effect = [temp_layer, memory_layer] @@ -174,10 +180,63 @@ class TestVerfahrensgebietAlkis(unittest.TestCase): mock_feature.setGeometry.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.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__": diff --git a/ui/tabs/working_tab.py b/ui/tabs/working_tab.py index dd0a1f5..1885fa5 100644 --- a/ui/tabs/working_tab.py +++ b/ui/tabs/working_tab.py @@ -169,9 +169,9 @@ class WorkingTab(QWidget): apply_style(flst_layer, "GIS_Flst_Beschriftung_ALKIS_NAS.qml") 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(): - _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") if not project.mapLayer(enriched_layer.id()): @@ -547,31 +547,4 @@ class WorkingTab(QWidget): # 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