diff --git a/changelog.txt b/changelog.txt index d6aebb1..df8d39d 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,8 @@ +- Fortschrittsdialog beim Laden aus ALKIS schließt jetzt zuverlässig bei jedem Abbruch (Bugfix) +- Fortschrittsanzeige beim Laden aus ALKIS in zwei Dialoge aufgeteilt (Suche / eigentlicher Ladevorgang), inkl. eigenem Suchschritt +- Mehrfachauswahl bei mehreren BauRaumOderBodenordnungsrecht-Treffern zur Verfahrensnummer (gemeinsames Laden/Verschmelzen mehrerer Objekte) +- Neu: Laden aller/mehrerer Objekte eines ganzen Landkreises als Fallback, wenn keine Verfahrensnummer gesetzt ist (Gruppierung je Verfahren, optional "Nur laden" ohne weiteren Ablauf) +--- Version 26.7.1-unstable: - Verfahrensgebiet als shape laden, inkl. Abgleich mit BROBOR-Objekt --- diff --git a/functions/verfahrensgebiet_alkis.py b/functions/verfahrensgebiet_alkis.py index be56b8e..a58332c 100644 --- a/functions/verfahrensgebiet_alkis.py +++ b/functions/verfahrensgebiet_alkis.py @@ -9,7 +9,7 @@ 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 +from sn_basis.functions.dialog_wrapper import ask_bror_kombinationen, ask_landkreis_objekte alkis_sf_url = "https://geodienste.sachsen.de/aaa/public_alkis/sf/wfs" typename = "adv:AX_BauRaumOderBodenordnungsrecht" @@ -28,7 +28,7 @@ BROR_ATTRIBUTE_FELDER = [ class LoadStatus(Enum): """Status eines Layer-Ladevorgangs. - + Attributes: NONE: Nichts geladen FIRST: Erstmalig geladen @@ -44,10 +44,10 @@ class LoadStatus(Enum): 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 """ @@ -72,32 +72,36 @@ def _check_existing_layer(tab_widget): return existing_layers[0], LoadStatus.KEEP -def verfahrensgebiet_alkis(tab_widget, skip_existing_check: bool = False): - """Lädt Verfahrensgebiet aus ALKIS-WFS basierend auf Verfahrensnummer. +def _suche_verfahrensgebiet_features(tab_widget, skip_existing_check: bool = False): + """Phase 1 ("Suchen"): vorhandenen Layer prüfen, WFS abfragen, nach + (adv_name, bezeichnung) gruppieren. Args: tab_widget: Parent-Widget für Dialoge und Meldungen + skip_existing_check: Überspringt die Nachfrage bei bereits vorhandenem + Layer (immer neu laden). 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. + Tuple[Optional[QgsVectorLayer], LoadStatus, dict, Optional[str]]: + ``(fertig_layer, status, kombinationen, crs)``. + Bei ``KEEP`` oder Fehler/Abbruch ist ``fertig_layer`` bereits das + Endergebnis (ggf. ``None``) und ``kombinationen`` leer – Phase 2 + entfällt dann. Bei Erfolg mit mindestens einer Kombination ist + ``fertig_layer`` ``None`` und ``kombinationen``/``crs`` sind für + Phase 2 (:func:`_baue_layer_aus_kombinationen`) befüllt. """ verfahrensnummer = get_variable("verfahrensnummer") if not verfahrensnummer: QMessageBox.critical(tab_widget, "Fehler", "In den Projekteigenschaften ist noch keine Verfahrensnummer eingetragen!") - return None, LoadStatus.NONE, [] + return None, LoadStatus.NONE, {}, None # Vorhandenen Layer prüfen (überspringbar für die Shape-Pipeline) status = LoadStatus.FIRST if not skip_existing_check: existing, status = _check_existing_layer(tab_widget) if status == LoadStatus.KEEP: - return existing, status, [] + return existing, status, {}, None # 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. @@ -111,13 +115,13 @@ def verfahrensgebiet_alkis(tab_widget, skip_existing_check: bool = False): if not temp_layer.isValid(): QMessageBox.critical(tab_widget, "Fehler", "Layer konnte nicht geladen werden.") - return None, LoadStatus.NONE, [] + return None, LoadStatus.NONE, {}, None features = list(temp_layer.getFeatures()) if not features: QMessageBox.critical(tab_widget, "Fehler", f"Verfahrenskennzeichen {verfahrensnummer} nicht im WFS gefunden.") - return None, LoadStatus.NONE, [] + return None, LoadStatus.NONE, {}, None # Objekte nach (adv_name, bezeichnung) gruppieren kombinationen: dict[tuple[str, str], list] = {} @@ -133,16 +137,32 @@ def verfahrensgebiet_alkis(tab_widget, skip_existing_check: bool = False): kombinationen[key] = [] kombinationen[key].append(feat) + return None, status, kombinationen, temp_layer.crs().authid() + + +def _baue_layer_aus_kombinationen(tab_widget, kombinationen: dict, crs: str, status: "LoadStatus"): + """Phase 2 ("Auswählen + Laden"): bei mehreren Kombinationen Auswahldialog + zeigen, Geometrien der gewählten Kombination(en) verschmelzen, Memory-Layer bauen. + + Args: + tab_widget: Parent-Widget für Dialoge und Meldungen + kombinationen: Ergebnis aus :func:`_suche_verfahrensgebiet_features` + crs: CRS-Authid aus :func:`_suche_verfahrensgebiet_features` + status: Lade-Status aus Phase 1 (FIRST/RELOAD), wird durchgereicht + + Returns: + Tuple[Optional[QgsVectorLayer], LoadStatus, List[QgsFeature]] + """ if len(kombinationen) == 1: - selected_key = next(iter(kombinationen)) - selected_feats = kombinationen[selected_key] + selected_feats = next(iter(kombinationen.values())) else: - # Mehrere Kombinationen → Nutzerauswahl + # Mehrere Kombinationen → Nutzerauswahl, ein oder mehrere Objekte + # können gemeinsam ausgewählt und geladen (verschmolzen) werden. key_list = sorted(kombinationen.keys()) - selected_key = ask_bror_kombination(tab_widget, key_list) - if selected_key is None: + selected_keys = ask_bror_kombinationen(tab_widget, key_list) + if not selected_keys: return None, LoadStatus.NONE, [] - selected_feats = kombinationen[selected_key] + selected_feats = [feat for key in selected_keys for feat in kombinationen[key]] union_geom = QgsGeometry.unaryUnion([f.geometry() for f in selected_feats]) if union_geom is None or union_geom.isEmpty(): @@ -150,7 +170,6 @@ def verfahrensgebiet_alkis(tab_widget, skip_existing_check: bool = False): return None, LoadStatus.NONE, [] # Memory-Layer erzeugen - crs = temp_layer.crs().authid() memory_layer = QgsVectorLayer(f"Polygon?crs={crs}", "Verfahrensgebiet", "memory") pr = memory_layer.dataProvider() pr.addAttributes([QgsField(attr, QVariant.String) for attr in BROR_ATTRIBUTE_FELDER]) @@ -180,3 +199,155 @@ def verfahrensgebiet_alkis(tab_widget, skip_existing_check: bool = False): # Status: FIRST oder RELOAD; selected_feats für Gemarkungsermittlung weitergeben return memory_layer, status, selected_feats + + +def _suche_landkreis_kombinationen(tab_widget, landkreis_kennzahl: str, skip_existing_check: bool = False): + """Fallback-Suche über einen ganzen Landkreis (statt einer Verfahrensnummer). + + Wie :func:`_suche_verfahrensgebiet_features`, aber mit WFS-Filter über die + Landkreiskennzahl (``bezeichnung LIKE '{landkreis_kennzahl}%'``) statt + über den 5-stelligen VKZ-Präfix. Gruppierung der Rohfeatures identisch: + nach exaktem (adv_name, bezeichnung). + + Returns: + Tuple[Optional[QgsVectorLayer], LoadStatus, dict, Optional[str]]: + Gleiche Bedeutung wie bei :func:`_suche_verfahrensgebiet_features`. + """ + status = LoadStatus.FIRST + if not skip_existing_check: + existing, status = _check_existing_layer(tab_widget) + if status == LoadStatus.KEEP: + return existing, status, {}, None + + wfs_uri_find = ( + f"url='{alkis_sf_url}' typename='{typename}' srsname='EPSG:25833' " + f"sql=SELECT * FROM AX_BauRaumOderBodenordnungsrecht " + f"WHERE bezeichnung LIKE '{landkreis_kennzahl}%'" + ) + temp_layer = QgsVectorLayer(wfs_uri_find, "TempVerfahrensgebiet", "WFS") + + if not temp_layer.isValid(): + QMessageBox.critical(tab_widget, "Fehler", "Layer konnte nicht geladen werden.") + return None, LoadStatus.NONE, {}, None + + features = list(temp_layer.getFeatures()) + if not features: + QMessageBox.critical(tab_widget, "Fehler", + f"Für Landkreis {landkreis_kennzahl} wurden keine Objekte im WFS gefunden.") + return None, LoadStatus.NONE, {}, None + + 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) + + return None, status, kombinationen, temp_layer.crs().authid() + + +def _baue_layer_aus_landkreis_auswahl(tab_widget, kombinationen: dict, crs: str, status: "LoadStatus"): + """Auswahl + Bau für den Landkreis-Fallback. + + Zeigt :func:`~sn_basis.functions.dialog_wrapper.ask_landkreis_objekte` + (Mehrfachauswahl inkl. "Alle auswählen" und "Nur laden"-Checkbox). Die + ausgewählten Kombinationen werden anhand der ersten 5 Zeichen von + ``bezeichnung`` (= die eigentliche VKZ) gruppiert; je Gruppe wird EIN + Feature erzeugt (Geometrien der Gruppe verschmolzen). Damit entsteht bei + Auswahl mehrerer/aller Objekte eines Landkreises ein Layer mit mehreren + Features – einem je tatsächlichem Verfahren, nicht ein Gesamt-Blob. + + Args: + tab_widget: Parent-Widget für Dialoge und Meldungen + kombinationen: Ergebnis aus :func:`_suche_landkreis_kombinationen` + crs: CRS-Authid aus :func:`_suche_landkreis_kombinationen` + status: Lade-Status aus Phase 1 (FIRST/RELOAD), wird durchgereicht + + Returns: + Tuple[Optional[QgsVectorLayer], LoadStatus, bool]: + ``(layer, status, nur_laden)``. + """ + key_list = sorted(kombinationen.keys()) + selected_keys, nur_laden = ask_landkreis_objekte(tab_widget, key_list) + if not selected_keys: + return None, LoadStatus.NONE, False + + # Nach VKZ (erste 5 Zeichen der bezeichnung) gruppieren + vkz_gruppen: dict[str, list] = {} + for key in selected_keys: + _, bezeichnung = key + vkz = bezeichnung[:5] + vkz_gruppen.setdefault(vkz, []).extend(kombinationen[key]) + + memory_layer = QgsVectorLayer(f"Polygon?crs={crs}", "Verfahrensgebiet", "memory") + pr = memory_layer.dataProvider() + pr.addAttributes([QgsField(attr, QVariant.String) for attr in BROR_ATTRIBUTE_FELDER]) + memory_layer.updateFields() + + neue_features = [] + for vkz, feats in sorted(vkz_gruppen.items()): + union_geom = QgsGeometry.unaryUnion([f.geometry() for f in feats]) + if union_geom is None or union_geom.isEmpty(): + continue + + first_feat = 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": + attrs.append(val.toString("dd.MM.yyyy")) + else: + attrs.append(str(val)) + except Exception: + attrs.append("") + + feat_out = QgsFeature() + feat_out.setGeometry(union_geom) + feat_out.setAttributes(attrs) + neue_features.append(feat_out) + + if not neue_features: + QMessageBox.critical(tab_widget, "Fehler", "Keine Geometrien zum Verschmelzen gefunden.") + return None, LoadStatus.NONE, False + + pr.addFeatures(neue_features) + memory_layer.updateExtents() + + return memory_layer, status, nur_laden + + +def verfahrensgebiet_alkis(tab_widget, skip_existing_check: bool = False): + """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. + + Ruft intern nacheinander :func:`_suche_verfahrensgebiet_features` (Phase 1) + und ggf. :func:`_baue_layer_aus_kombinationen` (Phase 2) auf – aufgeteilt, + damit ``verfahrensgebiet_alkis_komplett()`` dazwischen einen eigenen + Fortschrittsschritt anzeigen kann. Verhalten/Signatur dieser Funktion + selbst ist unverändert. + """ + layer, status, kombinationen, crs = _suche_verfahrensgebiet_features( + tab_widget, skip_existing_check + ) + if layer is not None or not kombinationen: + return layer, status, [] + return _baue_layer_aus_kombinationen(tab_widget, kombinationen, crs, status) diff --git a/functions/verfahrensgebiet_alkis_komplett.py b/functions/verfahrensgebiet_alkis_komplett.py index adca779..8e329c8 100644 --- a/functions/verfahrensgebiet_alkis_komplett.py +++ b/functions/verfahrensgebiet_alkis_komplett.py @@ -54,7 +54,10 @@ from sn_basis.functions.dialog_wrapper import ( ) from sn_verfahrensgebiet.functions.verfahrensgebiet_alkis import ( - verfahrensgebiet_alkis, + _suche_verfahrensgebiet_features, + _baue_layer_aus_kombinationen, + _suche_landkreis_kombinationen, + _baue_layer_aus_landkreis_auswahl, LoadStatus, ) from sn_verfahrensgebiet.functions.gemarkung_alkis import ( @@ -258,15 +261,25 @@ def verfahrensgebiet_alkis_komplett( """Vollständiger VG-Lade-Workflow: ALKIS-VG → Gemarkungen → Flurstücke → Verschneidung → Flächenvergleich → opt. Umringsprüfung. - Schrittfolge: + Zwei Fortschrittsdialoge nacheinander: - 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. + - **Dialog 1** ("Verfahrensgebiet(e) in ALKIS ermitteln/suchen"): läuft + während ``_suche_verfahrensgebiet_features(tab_widget)`` (ALKIS-WFS + durchsuchen) und wird danach immer geschlossen – auch wenn anschließend + der BROR-Auswahldialog erscheint (kein Fortschrittsdialog läuft + währenddessen parallel zum Auswahldialog). + - **Dialog 2** (eigentlicher Workflow, 6 Schritte): startet erst, nachdem + das Verfahrensgebiet feststeht (direkt nach Dialog 1 bei eindeutigem + Treffer/KEEP, oder erst nach Bestätigung des Auswahldialogs mit OK). + + Schrittfolge in Dialog 2: + + 1. ``gemarkung_schluessel_und_namen_von_vg_geometrie(geom)`` – Schlüssel. + 2. ``flurstueck_alkis(...)`` – Flurstücke laden. + 3. ``_select_by_location(...)`` – räumliche Selektion. + 4. ``erstelle_verfahrensgebiet_aus_auswahl(...)`` – enriched Layer. + 5. ``_vergleiche_flaechen(...)`` – Flächenvergleich als Gate. + 6. Ggf. ``umringspruefung(...)`` nach Nutzerbestätigung. Der BROR-Layer (``BauRaumOderBodenordnungsrecht (ALKIS)``) bleibt bei Erfolg im Projekt. Bei Abbruch/Fehler wird er entfernt, falls er in diesem @@ -290,26 +303,74 @@ def verfahrensgebiet_alkis_komplett( alkis_vg_layer = None bror_was_added_by_us = False abgeschlossen = False + progress = None - # Fortschrittsdialog für den Basisworkflow (7 Schritte) - progress = create_progress_dialog( - 7, "Verfahrensgebiet laden", "Initialisierung …" - ) + verfahrensnummer = get_variable("verfahrensnummer") + landkreis_kennzahl = get_variable("landkreis_kennzahl") + + if not verfahrensnummer and not landkreis_kennzahl: + QMessageBox.critical( + tab_widget, + "Fehler", + "Weder eine Verfahrensnummer noch ein Landkreis ist in den " + "Projekteigenschaften eingetragen!", + ) + return None, None, None, LoadStatus.NONE + + landkreis_modus = not verfahrensnummer + + # ------------------------------------------------------------------ + # Dialog 1: Verfahrensgebiet(e) in ALKIS ermitteln/suchen + # ------------------------------------------------------------------ + if landkreis_modus: + such_label = f"Objekte in Landkreis {landkreis_kennzahl} ermitteln/suchen …" + else: + such_label = "Verfahrensgebiet(e) in ALKIS ermitteln/suchen …" + + progress_suche = create_progress_dialog(1, "Verfahrensgebiet laden", such_label) + try: + progress_suche.set_value(0) + if progress_suche.is_canceled(): + return None, None, None, LoadStatus.NONE + + if landkreis_modus: + vorab_layer, vg_status, kombinationen, crs = _suche_landkreis_kombinationen( + tab_widget, landkreis_kennzahl + ) + else: + vorab_layer, vg_status, kombinationen, crs = _suche_verfahrensgebiet_features(tab_widget) + finally: + # Dialog 1 schließt hier immer – egal ob eine Auswahl folgt oder nicht. + progress_suche.close() + + nur_laden = False 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) - progress.raise_to_front() - - if vg_status == LoadStatus.NONE or not alkis_vg_layer: - return None, None, None, LoadStatus.NONE + if vorab_layer is not None or not kombinationen: + # KEEP oder Fehler/Abbruch bereits in Phase 1 entschieden + if vg_status == LoadStatus.NONE or not vorab_layer: + return None, None, None, LoadStatus.NONE + alkis_vg_layer = vorab_layer + elif landkreis_modus: + # ------------------------------------------------------------------ + # Objekt(e) des Landkreises auswählen – kein eigener Fortschritts- + # dialog währenddessen, nur der Auswahldialog selbst. + # ------------------------------------------------------------------ + alkis_vg_layer, vg_status, nur_laden = _baue_layer_aus_landkreis_auswahl( + tab_widget, kombinationen, crs, vg_status + ) + if vg_status == LoadStatus.NONE or not alkis_vg_layer: + return None, None, None, LoadStatus.NONE + else: + # ------------------------------------------------------------------ + # Verfahrensgebiet auswählen (bei mehreren Treffern) – kein eigener + # Fortschrittsdialog währenddessen, nur der Auswahldialog selbst. + # ------------------------------------------------------------------ + alkis_vg_layer, vg_status, _ = _baue_layer_aus_kombinationen( + tab_widget, kombinationen, crs, vg_status + ) + 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) @@ -318,12 +379,25 @@ def verfahrensgebiet_alkis_komplett( project.addMapLayer(alkis_vg_layer) bror_was_added_by_us = True - progress.set_value(1) + if nur_laden: + # Nutzer hat "Nur laden" angehakt – kein Dialog 2, kein weiterer + # Workflow (Gemarkung/Flurstücke/Vergleich). + abgeschlossen = True + return alkis_vg_layer, None, None, vg_status # ------------------------------------------------------------------ - # Schritt 2: Gemarkungsschlüssel per Geometrie-Spatial-Join ermitteln + # Dialog 2: eigentlicher Fortschrittsdialog für die restlichen Schritte + # (startet erst jetzt, nach erfolgreicher Auswahl/Übernahme des VG) # ------------------------------------------------------------------ - progress.set_label("Schritt 2/6: Gemarkungsschlüssel ermitteln …") + progress = create_progress_dialog( + 6, "Verfahrensgebiet laden", "Initialisierung …" + ) + + # ------------------------------------------------------------------ + # Schritt 1: Gemarkungsschlüssel per Geometrie-Spatial-Join ermitteln + # ------------------------------------------------------------------ + progress.set_label("Schritt 1/6: Gemarkungen ermitteln und laden …") + progress.set_value(0) if progress.is_canceled(): return None, None, None, LoadStatus.NONE @@ -344,12 +418,12 @@ def verfahrensgebiet_alkis_komplett( duration=5, ) - progress.set_value(2) + progress.set_value(1) # ------------------------------------------------------------------ - # Schritt 3: Flurstücke laden (mit Nutzer-Dialog, Vorschlag befüllt) + # Schritt 2: Flurstücke laden (mit Nutzer-Dialog, Vorschlag befüllt) # ------------------------------------------------------------------ - progress.set_label("Schritt 3/6: Flurstücke aus ALKIS laden …") + progress.set_label("Schritt 2/6: Flurstücke aus ALKIS laden …") if progress.is_canceled(): return None, None, None, LoadStatus.NONE @@ -363,12 +437,12 @@ def verfahrensgebiet_alkis_komplett( if flst_status == LoadStatus.NONE or not flst_layer: return None, None, None, LoadStatus.NONE - progress.set_value(3) + progress.set_value(2) # ------------------------------------------------------------------ - # Schritt 4: Räumliche Selektion + # Schritt 3: Räumliche Selektion # ------------------------------------------------------------------ - progress.set_label("Schritt 4/6: Flurstücke räumlich selektieren …") + progress.set_label("Schritt 3/6: Flurstücke räumlich selektieren …") if progress.is_canceled(): return None, None, None, LoadStatus.NONE @@ -383,12 +457,12 @@ def verfahrensgebiet_alkis_komplett( ) return None, flst_layer, None, LoadStatus.NONE - progress.set_value(4) + progress.set_value(3) # ------------------------------------------------------------------ - # Schritt 5: Enriched Memory-Layer aus Auswahl erzeugen + # Schritt 4: Enriched Memory-Layer aus Auswahl erzeugen # ------------------------------------------------------------------ - progress.set_label("Schritt 5/6: Verfahrensgebiet-Layer erzeugen …") + progress.set_label("Schritt 4/6: Verfahrensgebiet-Layer erzeugen …") if progress.is_canceled(): return None, None, None, LoadStatus.NONE @@ -407,12 +481,12 @@ def verfahrensgebiet_alkis_komplett( vkz = get_variable("verfahrensnummer") or "" _fuege_vkz_feld_hinzu(enriched_layer, vkz) - progress.set_value(5) + progress.set_value(4) # ------------------------------------------------------------------ - # Schritt 6: Flächenvergleich als Gate + # Schritt 5: Flächenvergleich als Gate # ------------------------------------------------------------------ - progress.set_label("Schritt 6/6: Flächenvergleich …") + progress.set_label("Schritt 5/6: Flächenvergleich …") if progress.is_canceled(): return None, None, None, LoadStatus.NONE @@ -421,11 +495,10 @@ def verfahrensgebiet_alkis_komplett( alkis_vg_layer, enriched_layer, toleranz ) - progress.set_value(6) + progress.set_value(5) if stimmt_ueberein: - progress.set_value(7) - progress.close() + progress.set_value(6) show_info_dialog( "Flächenvergleich", "Verfahrensgebiet mit erwarteter Geometrie erzeugt", @@ -437,7 +510,6 @@ def verfahrensgebiet_alkis_komplett( # ------------------------------------------------------------------ # Flächenabweichung → 3-Optionen-Dialog # ------------------------------------------------------------------ - progress.close() diff = abs(bror_flaeche - vg_flaeche) entscheidung = ask_detailpruefung_oder_vg_laden( tab_widget, @@ -563,6 +635,9 @@ def verfahrensgebiet_alkis_komplett( return enriched_layer, flst_layer, alkis_vg_layer, vg_status finally: + # Dialog 2 (falls erstellt) bei jedem Ausgang (Erfolg, Abbruch, Fehler) schließen + if progress is not None: + progress.close() # 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) diff --git a/tests/test_verfahrensgebiet_alkis.py b/tests/test_verfahrensgebiet_alkis.py index 9ed7c0c..d0e57c9 100644 --- a/tests/test_verfahrensgebiet_alkis.py +++ b/tests/test_verfahrensgebiet_alkis.py @@ -186,7 +186,7 @@ class TestVerfahrensgebietAlkis(unittest.TestCase): 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.ask_bror_kombinationen") @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)) @@ -202,7 +202,7 @@ class TestVerfahrensgebietAlkis(unittest.TestCase): 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_dialog.return_value = [("ADV-A", "VKZ12/001")] mock_feature = mock_feature_cls.return_value mock_feature.setGeometry.return_value = None @@ -216,7 +216,7 @@ class TestVerfahrensgebietAlkis(unittest.TestCase): # 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.ask_bror_kombinationen") @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") @@ -229,7 +229,7 @@ class TestVerfahrensgebietAlkis(unittest.TestCase): 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 + mock_dialog.return_value = [] # Nutzer bricht ab layer, status, feats = vg.verfahrensgebiet_alkis(tab_widget=None) @@ -238,6 +238,35 @@ class TestVerfahrensgebietAlkis(unittest.TestCase): self.assertEqual(status, vg.LoadStatus.NONE) self.assertEqual(feats, []) + @patch("sn_verfahrensgebiet.functions.verfahrensgebiet_alkis.ask_bror_kombinationen") + @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_mehrfachauswahl( + self, mock_qgsvectorlayer, _mock_get_var, _mock_check, _mock_union, mock_feature_cls, mock_dialog + ): + """Zwei verschiedene Kombinationen → beide ausgewählt → beide Feature-Gruppen 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"), ("ADV-B", "VKZ12/002")] + + 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) + self.assertEqual(feats, [feat1, feat2]) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_verfahrensgebiet_alkis_komplett.py b/tests/test_verfahrensgebiet_alkis_komplett.py index 3048b43..04f4287 100644 --- a/tests/test_verfahrensgebiet_alkis_komplett.py +++ b/tests/test_verfahrensgebiet_alkis_komplett.py @@ -129,7 +129,7 @@ def _make_progress_mock(canceled=False): # --------------------------------------------------------------------------- _MOD = "sn_verfahrensgebiet.functions.verfahrensgebiet_alkis_komplett" -_PATCH_VG_ALKIS = f"{_MOD}.verfahrensgebiet_alkis" +_PATCH_VG_ALKIS = f"{_MOD}._suche_verfahrensgebiet_features" _PATCH_GEM = f"{_MOD}.gemarkung_schluessel_und_namen_von_vg_geometrie" _PATCH_FLST = f"{_MOD}.flurstueck_alkis" _PATCH_SELECT = f"{_MOD}._select_by_location" @@ -146,6 +146,8 @@ _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" +_PATCH_LANDKREIS_SUCHE = f"{_MOD}._suche_landkreis_kombinationen" +_PATCH_LANDKREIS_BAU = f"{_MOD}._baue_layer_aus_landkreis_auswahl" # --------------------------------------------------------------------------- @@ -265,9 +267,10 @@ class TestVerfahrensgebietAlkisKomplett(unittest.TestCase): @patch(_PATCH_PROGRESS) @patch(_PATCH_PROJECT) @patch(_PATCH_VG_ALKIS) - def test_vg_alkis_none_aborts(self, mock_vg, mock_proj, mock_prog): + @patch(_PATCH_GET_VAR, return_value="VF-2024-001") + def test_vg_alkis_none_aborts(self, mock_get_var, mock_vg, mock_proj, mock_prog): mock_proj.instance.return_value = _DummyProject() - mock_vg.return_value = (None, LoadStatus.NONE, []) + mock_vg.return_value = (None, LoadStatus.NONE, {}, None) mock_prog.return_value = _make_progress_mock() enriched, flst, bror, status = verfahrensgebiet_alkis_komplett(None) @@ -281,10 +284,11 @@ class TestVerfahrensgebietAlkisKomplett(unittest.TestCase): @patch(_PATCH_PROJECT) @patch(_PATCH_GEM) @patch(_PATCH_VG_ALKIS) - def test_flst_none_aborts(self, mock_vg, mock_gem, mock_proj, mock_prog): + @patch(_PATCH_GET_VAR, return_value="VF-2024-001") + def test_flst_none_aborts(self, mock_get_var, 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_vg.return_value = (vg_layer, LoadStatus.FIRST, {}, None) mock_gem.return_value = [("1234", "Muster")] mock_prog.return_value = _make_progress_mock() @@ -302,13 +306,14 @@ class TestVerfahrensgebietAlkisKomplett(unittest.TestCase): @patch(_PATCH_GEM) @patch(_PATCH_FLST) @patch(_PATCH_VG_ALKIS) + @patch(_PATCH_GET_VAR, return_value="VF-2024-001") def test_no_selection_returns_none_enriched( - self, mock_vg, mock_flst, mock_gem, mock_proj, mock_select, mock_msgbox, mock_prog + self, mock_get_var, 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_vg.return_value = (vg_layer, LoadStatus.FIRST, {}, None) mock_gem.return_value = [] mock_flst.return_value = (flst_layer, LoadStatus.FIRST) mock_select.return_value = 0 @@ -330,15 +335,16 @@ class TestVerfahrensgebietAlkisKomplett(unittest.TestCase): @patch(_PATCH_GEM) @patch(_PATCH_FLST) @patch(_PATCH_VG_ALKIS) + @patch(_PATCH_GET_VAR, return_value="VF-2024-001") def test_invalid_enriched_layer_returns_none( - self, mock_vg, mock_flst, mock_gem, mock_proj, mock_select, mock_erstelle, mock_msgbox, mock_prog + self, mock_get_var, 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_vg.return_value = (vg_layer, LoadStatus.FIRST, {}, None) mock_gem.return_value = [] mock_flst.return_value = (flst_layer, LoadStatus.FIRST) mock_select.return_value = 5 @@ -356,11 +362,12 @@ class TestVerfahrensgebietAlkisKomplett(unittest.TestCase): @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): + @patch(_PATCH_GET_VAR, return_value="VF-2024-001") + def test_progress_cancel_after_step1_returns_none(self, mock_get_var, 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, []) + mock_vg.return_value = (vg_layer, LoadStatus.FIRST, {}, None) prog = MagicMock() prog.is_canceled.side_effect = [False, True] # 1. Aufruf False, 2. True @@ -398,7 +405,7 @@ class TestVerfahrensgebietAlkisKomplett(unittest.TestCase): 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_vg.return_value = (vg_layer, LoadStatus.FIRST, {}, None) mock_gem.return_value = [("1234", "Muster")] mock_flst.return_value = (flst_layer, LoadStatus.FIRST) mock_select.return_value = 3 @@ -437,18 +444,18 @@ class TestVerfahrensgebietAlkisKomplett(unittest.TestCase): flst_layer = _make_layer(name="Flurstücke") enriched = _make_layer(name="Verfahrensgebiet", valid=True) - mock_vg.return_value = (vg_layer, LoadStatus.FIRST, []) + mock_vg.return_value = (vg_layer, LoadStatus.FIRST, {}, None) 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_get_var.return_value = "VF-2024-001" 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, "") + mock_vkz.assert_called_once_with(enriched, "VF-2024-001") # ------------------------------------------------------------------ # # Flächenvergleich – Abweichung + Abbrechen @@ -476,12 +483,12 @@ class TestVerfahrensgebietAlkisKomplett(unittest.TestCase): flst_layer = _make_layer(name="Flurstücke") enriched = _make_layer(name="Verfahrensgebiet", valid=True) - mock_vg.return_value = (vg_layer, LoadStatus.FIRST, []) + mock_vg.return_value = (vg_layer, LoadStatus.FIRST, {}, None) 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_get_var.return_value = "VF-2024-001" mock_vergl.return_value = (1000.0, 800.0, False) mock_ask.return_value = "abbrechen" mock_prog.return_value = _make_progress_mock() @@ -521,12 +528,12 @@ class TestVerfahrensgebietAlkisKomplett(unittest.TestCase): flst_layer = _make_layer(name="Flurstücke") enriched = _make_layer(name="Verfahrensgebiet", valid=True) - mock_vg.return_value = (vg_layer, LoadStatus.FIRST, []) + mock_vg.return_value = (vg_layer, LoadStatus.FIRST, {}, None) 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_get_var.return_value = "VF-2024-001" mock_vergl.return_value = (1000.0, 800.0, False) mock_ask.return_value = "als_vg_laden" mock_prog.return_value = _make_progress_mock() @@ -577,12 +584,12 @@ class TestVerfahrensgebietAlkisKomplett(unittest.TestCase): 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_vg.return_value = (vg_layer, LoadStatus.FIRST, {}, None) 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_get_var.return_value = "VF-2024-001" mock_vergl.return_value = (1000.0, 800.0, False) mock_ask.return_value = "detailpruefung" mock_extrahiere.return_value = [MagicMock(), MagicMock(), MagicMock()] # 3 Knickpunkte @@ -630,12 +637,12 @@ class TestVerfahrensgebietAlkisKomplett(unittest.TestCase): flst_layer = _make_layer(name="Flurstücke") enriched = _make_layer(name="Verfahrensgebiet", valid=True) - mock_vg.return_value = (vg_layer, LoadStatus.FIRST, []) + mock_vg.return_value = (vg_layer, LoadStatus.FIRST, {}, None) 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_get_var.return_value = "VF-2024-001" mock_vergl.return_value = (1000.0, 1000.0, True) mock_prog.return_value = _make_progress_mock() @@ -647,15 +654,16 @@ class TestVerfahrensgebietAlkisKomplett(unittest.TestCase): @patch(_PATCH_PROJECT) @patch(_PATCH_GEM) @patch(_PATCH_VG_ALKIS) + @patch(_PATCH_GET_VAR, return_value="VF-2024-001") def test_bror_layer_wird_bei_fehler_entfernt( - self, mock_vg, mock_gem, mock_proj, mock_prog + self, mock_get_var, 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_vg.return_value = (vg_layer, LoadStatus.FIRST, {}, None) mock_gem.return_value = [] mock_prog.return_value = _make_progress_mock() @@ -664,6 +672,78 @@ class TestVerfahrensgebietAlkisKomplett(unittest.TestCase): self.assertIn("BROR_ERR", proj_inst.removed) + # ------------------------------------------------------------------ # + # Landkreis-Fallback (keine Verfahrensnummer) + # ------------------------------------------------------------------ # + + @patch(_PATCH_PROGRESS) + @patch(_PATCH_MSGBOX) + @patch(_PATCH_GET_VAR, return_value="") + def test_kein_vkz_kein_landkreis_zeigt_fehler(self, mock_get_var, mock_msgbox, mock_prog): + """Weder Verfahrensnummer noch Landkreis gesetzt → Fehlermeldung, kein Layer.""" + enriched, flst, bror, status = verfahrensgebiet_alkis_komplett(None) + + self.assertIsNone(enriched) + self.assertIsNone(flst) + self.assertIsNone(bror) + self.assertEqual(status, LoadStatus.NONE) + mock_msgbox.critical.assert_called_once() + + @patch(_PATCH_PROGRESS) + @patch(_PATCH_PROJECT) + @patch(_PATCH_LANDKREIS_BAU) + @patch(_PATCH_LANDKREIS_SUCHE) + def test_landkreis_modus_wird_bei_fehlender_vkz_verwendet( + self, mock_such, mock_bau, mock_proj, mock_prog + ): + """Keine Verfahrensnummer, aber Landkreis gesetzt → Landkreis-Suchpfad + statt VKZ-Pfad wird verwendet.""" + + def _get_var_side_effect(key, scope="project"): + return {"verfahrensnummer": "", "landkreis_kennzahl": "22"}.get(key, "") + + proj_inst = _DummyProject() + mock_proj.instance.return_value = proj_inst + vg_layer = _make_layer(name="BauRaumOrdnungsrecht (ALKIS)") + mock_such.return_value = (None, LoadStatus.FIRST, {("A", "22015"): [MagicMock()]}, "EPSG:25833") + mock_bau.return_value = (vg_layer, LoadStatus.FIRST, True) # nur_laden=True + mock_prog.return_value = _make_progress_mock() + + with patch(_PATCH_GET_VAR, side_effect=_get_var_side_effect): + enriched, flst, bror, status = verfahrensgebiet_alkis_komplett(None) + + mock_such.assert_called_once() + mock_bau.assert_called_once() + self.assertIs(enriched, vg_layer) + self.assertIsNone(flst) + self.assertIsNone(bror) + self.assertEqual(status, LoadStatus.FIRST) + + @patch(_PATCH_PROGRESS) + @patch(_PATCH_PROJECT) + @patch(_PATCH_LANDKREIS_BAU) + @patch(_PATCH_LANDKREIS_SUCHE) + def test_landkreis_modus_nur_laden_ueberspringt_dialog2( + self, mock_such, mock_bau, mock_proj, mock_prog + ): + """nur_laden=True → Funktion kehrt sofort zurück, Dialog 2 (Gemarkung + etc.) wird nicht gestartet.""" + + def _get_var_side_effect(key, scope="project"): + return {"verfahrensnummer": "", "landkreis_kennzahl": "22"}.get(key, "") + + proj_inst = _DummyProject() + mock_proj.instance.return_value = proj_inst + vg_layer = _make_layer(name="BauRaumOrdnungsrecht (ALKIS)") + mock_such.return_value = (None, LoadStatus.FIRST, {("A", "22015"): [MagicMock()]}, "EPSG:25833") + mock_bau.return_value = (vg_layer, LoadStatus.FIRST, True) + mock_prog.return_value = _make_progress_mock() + + with patch(_PATCH_GET_VAR, side_effect=_get_var_side_effect), \ + patch(_PATCH_GEM) as mock_gem: + verfahrensgebiet_alkis_komplett(None) + mock_gem.assert_not_called() + if __name__ == "__main__": unittest.main()