Compare commits

..
Author SHA1 Message Date
22ottomi 0a906d3ac7 Merge pull request 'FBSchl-Feldsuche tolerant gegenueber abweichenden Feldnamen' (#45) from fix-fbschl-feldname into unstable
Release Plugin / release (push) Successful in 3s
Reviewed-on: #45
2026-09-02 11:44:47 +02:00
22ottomi 39f8209b9d Merge pull request 'Nur-laden-Option bei ALKIS-Laden durch Stop-Abfrage ersetzt' (#43) from alkis-nur-laden-stop into unstable
Reviewed-on: #43
2026-09-02 11:44:36 +02:00
Michael OttoandClaude Sonnet 5 3c0e6e5a19 FBSchl-Feldsuche tolerant gegenüber abweichenden Feldnamen
Manche Shape-Exporte liefern das Feld "FBSchl" mit angehängtem Punkt
(z.B. "FBSchl.") oder abweichender Groß-/Kleinschreibung. Der bisherige
exakte Namensvergleich (fields().indexFromName) erkannte das Feld dann
nicht und meldete fälschlich "Feld fehlt", die Datei wurde nicht
eingelesen.

- _normalisiere_feldname(): entfernt alle Nicht-alphanumerischen Zeichen
  und normalisiert auf Kleinbuchstaben
- _finde_fbschl_index(): exakter Namensabgleich zuerst, sonst Fallback auf
  normalisierten Vergleich über alle Felder

Fixes #44

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-02 11:43:34 +02:00
Michael OttoandClaude Sonnet 5 c93776d19e Nur-laden-Option bei ALKIS-Laden durch Stop-Abfrage ersetzt
- Checkbox "Nur Objekte laden (ohne weitere Prüfungen)" im
  Landkreis-Auswahldialog entfernt
- Stattdessen nach dem Laden/Übernehmen des BROR-Objekts eine einheitliche
  Ja/Nein-Stop-Abfrage ("Weiter?") vor Dialog 2 (Gemarkung/Flurstücke/
  Flächenvergleich) - analog zum Ablauf bei "Aus Shape laden"
- Gilt jetzt für Verfahrensnummer- und Landkreis-Zweig gleichermaßen,
  vorher gab es die Option nur im Landkreis-Zweig

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-02 09:21:58 +02:00
4 changed files with 99 additions and 35 deletions
+11 -12
View File
@@ -261,12 +261,12 @@ def _baue_layer_aus_landkreis_auswahl(tab_widget, kombinationen: dict, crs: str,
"""Auswahl + Bau für den Landkreis-Fallback. """Auswahl + Bau für den Landkreis-Fallback.
Zeigt :func:`~sn_verfahrensgebiet.functions.verfahrensgebiet_dialoge.ask_landkreis_objekte` Zeigt :func:`~sn_verfahrensgebiet.functions.verfahrensgebiet_dialoge.ask_landkreis_objekte`
(Mehrfachauswahl inkl. "Alle auswählen" und "Nur laden"-Checkbox). Die (Mehrfachauswahl inkl. "Alle auswählen"). Die ausgewählten Kombinationen
ausgewählten Kombinationen werden anhand der ersten 5 Zeichen von werden anhand der ersten 5 Zeichen von ``bezeichnung`` (= die eigentliche
``bezeichnung`` (= die eigentliche VKZ) gruppiert; je Gruppe wird EIN VKZ) gruppiert; je Gruppe wird EIN Feature erzeugt (Geometrien der
Feature erzeugt (Geometrien der Gruppe verschmolzen). Damit entsteht bei Gruppe verschmolzen). Damit entsteht bei Auswahl mehrerer/aller Objekte
Auswahl mehrerer/aller Objekte eines Landkreises ein Layer mit mehreren eines Landkreises ein Layer mit mehreren Features – einem je
Features – einem je tatsächlichem Verfahren, nicht ein Gesamt-Blob. tatsächlichem Verfahren, nicht ein Gesamt-Blob.
Args: Args:
tab_widget: Parent-Widget für Dialoge und Meldungen tab_widget: Parent-Widget für Dialoge und Meldungen
@@ -275,13 +275,12 @@ def _baue_layer_aus_landkreis_auswahl(tab_widget, kombinationen: dict, crs: str,
status: Lade-Status aus Phase 1 (FIRST/RELOAD), wird durchgereicht status: Lade-Status aus Phase 1 (FIRST/RELOAD), wird durchgereicht
Returns: Returns:
Tuple[Optional[QgsVectorLayer], LoadStatus, bool]: Tuple[Optional[QgsVectorLayer], LoadStatus]: ``(layer, status)``.
``(layer, status, nur_laden)``.
""" """
key_list = sorted(kombinationen.keys()) key_list = sorted(kombinationen.keys())
selected_keys, nur_laden = ask_landkreis_objekte(tab_widget, key_list) selected_keys = ask_landkreis_objekte(tab_widget, key_list)
if not selected_keys: if not selected_keys:
return None, LoadStatus.NONE, False return None, LoadStatus.NONE
# Nach VKZ (erste 5 Zeichen der bezeichnung) gruppieren # Nach VKZ (erste 5 Zeichen der bezeichnung) gruppieren
vkz_gruppen: dict[str, list] = {} vkz_gruppen: dict[str, list] = {}
@@ -322,12 +321,12 @@ def _baue_layer_aus_landkreis_auswahl(tab_widget, kombinationen: dict, crs: str,
if not neue_features: if not neue_features:
QMessageBox.critical(tab_widget, "Fehler", "Keine Geometrien zum Verschmelzen gefunden.") QMessageBox.critical(tab_widget, "Fehler", "Keine Geometrien zum Verschmelzen gefunden.")
return None, LoadStatus.NONE, False return None, LoadStatus.NONE
pr.addFeatures(neue_features) pr.addFeatures(neue_features)
memory_layer.updateExtents() memory_layer.updateExtents()
return memory_layer, status, nur_laden return memory_layer, status
def verfahrensgebiet_alkis(tab_widget, skip_existing_check: bool = False): def verfahrensgebiet_alkis(tab_widget, skip_existing_check: bool = False):
+21 -9
View File
@@ -51,6 +51,7 @@ from sn_basis.functions.dialog_wrapper import (
create_progress_dialog, create_progress_dialog,
show_info_dialog, show_info_dialog,
ask_detailpruefung_oder_vg_laden, ask_detailpruefung_oder_vg_laden,
ask_yes_no,
) )
from sn_basis.functions.landkreise import LANDKREISE, ALLE_LANDKREISE from sn_basis.functions.landkreise import LANDKREISE, ALLE_LANDKREISE
from sn_basis.functions.ly_style_wrapper import apply_style from sn_basis.functions.ly_style_wrapper import apply_style
@@ -273,9 +274,13 @@ def verfahrensgebiet_alkis_komplett(
durchsuchen) und wird danach immer geschlossen – auch wenn anschließend durchsuchen) und wird danach immer geschlossen – auch wenn anschließend
der BROR-Auswahldialog erscheint (kein Fortschrittsdialog läuft der BROR-Auswahldialog erscheint (kein Fortschrittsdialog läuft
währenddessen parallel zum Auswahldialog). währenddessen parallel zum Auswahldialog).
- **STOP** (Zwischenabfrage "Weiter?"): erscheint nach dem Laden/Übernehmen
des BROR-Objekts, bevor Dialog 2 startet. Bei "Nein" bricht der Ablauf
ab, das BROR-Objekt bleibt aber im Projekt (ersetzt die frühere
"Nur Objekte laden"-Option des Landkreis-Auswahldialogs).
- **Dialog 2** (eigentlicher Workflow, 6 Schritte): startet erst, nachdem - **Dialog 2** (eigentlicher Workflow, 6 Schritte): startet erst, nachdem
das Verfahrensgebiet feststeht (direkt nach Dialog 1 bei eindeutigem das Verfahrensgebiet feststeht und die Stop-Abfrage mit "Ja" bestätigt
Treffer/KEEP, oder erst nach Bestätigung des Auswahldialogs mit OK). wurde.
Schrittfolge in Dialog 2: Schrittfolge in Dialog 2:
@@ -351,8 +356,6 @@ def verfahrensgebiet_alkis_komplett(
# Dialog 1 schließt hier immer – egal ob eine Auswahl folgt oder nicht. # Dialog 1 schließt hier immer – egal ob eine Auswahl folgt oder nicht.
progress_suche.close() progress_suche.close()
nur_laden = False
try: try:
if vorab_layer is not None or not kombinationen: if vorab_layer is not None or not kombinationen:
# KEEP oder Fehler/Abbruch bereits in Phase 1 entschieden # KEEP oder Fehler/Abbruch bereits in Phase 1 entschieden
@@ -364,7 +367,7 @@ def verfahrensgebiet_alkis_komplett(
# Objekt(e) des Landkreises auswählen – kein eigener Fortschritts- # Objekt(e) des Landkreises auswählen – kein eigener Fortschritts-
# dialog währenddessen, nur der Auswahldialog selbst. # dialog währenddessen, nur der Auswahldialog selbst.
# ------------------------------------------------------------------ # ------------------------------------------------------------------
alkis_vg_layer, vg_status, nur_laden = _baue_layer_aus_landkreis_auswahl( alkis_vg_layer, vg_status = _baue_layer_aus_landkreis_auswahl(
tab_widget, kombinationen, crs, vg_status tab_widget, kombinationen, crs, vg_status
) )
if vg_status == LoadStatus.NONE or not alkis_vg_layer: if vg_status == LoadStatus.NONE or not alkis_vg_layer:
@@ -387,12 +390,21 @@ def verfahrensgebiet_alkis_komplett(
project.addMapLayer(alkis_vg_layer) project.addMapLayer(alkis_vg_layer)
bror_was_added_by_us = True bror_was_added_by_us = True
# Style direkt nach dem Laden anwenden – unabhängig vom weiteren # Style direkt nach dem Laden anwenden – unabhängig vom weiteren
# Ablauf, damit auch "Nur Objekte laden" den Style erhält. # Ablauf, damit auch bei "Nein" auf die Stop-Abfrage der Style
# gesetzt ist.
apply_style(alkis_vg_layer, BROR_STYLE_QML) apply_style(alkis_vg_layer, BROR_STYLE_QML)
if nur_laden: # ------------------------------------------------------------------
# Nutzer hat "Nur laden" angehakt – kein Dialog 2, kein weiterer # STOP: Zwischenabfrage nach dem Laden des BROR-Objekts, bevor mit
# Workflow (Gemarkung/Flurstücke/Vergleich). # Gemarkung/Flurstücke/Flächenvergleich fortgefahren wird (ersetzt
# die frühere "Nur Objekte laden"-Option).
# ------------------------------------------------------------------
if not ask_yes_no(
"Verfahrensgebiet laden",
f"Layer '{alkis_vg_layer.name()}' wurde geladen. Weiter?",
default=True,
parent=tab_widget,
):
abgeschlossen = True abgeschlossen = True
return alkis_vg_layer, None, None, vg_status return alkis_vg_layer, None, None, vg_status
+10 -13
View File
@@ -101,15 +101,15 @@ def ask_bror_kombinationen(
def ask_landkreis_objekte( def ask_landkreis_objekte(
parent: Any, parent: Any,
kombinationen: list[tuple[str, str]], kombinationen: list[tuple[str, str]],
) -> tuple[list[tuple[str, str]], bool]: ) -> list[tuple[str, str]]:
"""Zeigt Dialog zur Auswahl eines, mehrerer oder aller BROR-Objekte eines """Zeigt Dialog zur Auswahl eines, mehrerer oder aller BROR-Objekte eines
Landkreises (Fallback, wenn keine Verfahrensnummer gesetzt ist). Landkreises (Fallback, wenn keine Verfahrensnummer gesetzt ist).
Wie :func:`ask_bror_kombinationen`, zusätzlich mit einer "Alle Wie :func:`ask_bror_kombinationen`, zusätzlich mit einer "Alle
auswählen"-Checkbox (da hier potenziell deutlich mehr Treffer als bei der auswählen"-Checkbox (da hier potenziell deutlich mehr Treffer als bei der
VKZ-Suche auftreten) und einer "Nur laden"-Checkbox, mit der gesteuert VKZ-Suche auftreten). Ob nach dem Laden der weitere Workflow
wird, ob nach dem Laden der weitere Workflow (Gemarkung/Flurstücke/ (Gemarkung/Flurstücke/Flächenvergleich) fortgesetzt wird, klärt eine
Flächenvergleich) fortgesetzt werden soll. separate Stop-Abfrage nach dem Laden – nicht mehr dieser Dialog.
Parameters Parameters
---------- ----------
@@ -120,16 +120,16 @@ def ask_landkreis_objekte(
Returns Returns
------- -------
tuple[list[tuple[str, str]], bool] list[tuple[str, str]]
``(ausgewaehlte_kombinationen, nur_laden)``. ``([], False)`` bei Die gewählten ``(adv_name, bezeichnung)``-Tupel. Leere Liste bei
Abbruch oder wenn nichts ausgewählt wurde. Abbruch oder wenn nichts ausgewählt wurde.
""" """
if not kombinationen: if not kombinationen:
return [], False return []
if QT_VERSION == 0: # Mock-Modus if QT_VERSION == 0: # Mock-Modus
print(f"Mock-Modus: ask_landkreis_objekte → {kombinationen[:1]}") print(f"Mock-Modus: ask_landkreis_objekte → {kombinationen[:1]}")
return kombinationen[:1], False return kombinationen[:1]
dialog = QDialog(parent) dialog = QDialog(parent)
dialog.setWindowTitle("Verfahrensgebiete aus Landkreis laden") dialog.setWindowTitle("Verfahrensgebiete aus Landkreis laden")
@@ -204,9 +204,6 @@ def ask_landkreis_objekte(
mehrfachauswahl_hinweis = QLabel("<small>Mehrfachauswahl mit Strg + Umschalt</small>") mehrfachauswahl_hinweis = QLabel("<small>Mehrfachauswahl mit Strg + Umschalt</small>")
layout.addWidget(mehrfachauswahl_hinweis) layout.addWidget(mehrfachauswahl_hinweis)
nur_laden_checkbox = QCheckBox("Nur Objekte laden (ohne weitere Prüfungen)")
layout.addWidget(nur_laden_checkbox)
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel) buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
buttons.accepted.connect(dialog.accept) buttons.accepted.connect(dialog.accept)
buttons.rejected.connect(dialog.reject) buttons.rejected.connect(dialog.reject)
@@ -235,11 +232,11 @@ def ask_landkreis_objekte(
dialog.resize(breite, dialog.sizeHint().height()) dialog.resize(breite, dialog.sizeHint().height())
if exec_dialog(dialog) != DialogAccepted: if exec_dialog(dialog) != DialogAccepted:
return [], False return []
ausgewaehlte_zeilen = sorted(set(item.row() for item in table.selectedItems())) ausgewaehlte_zeilen = sorted(set(item.row() for item in table.selectedItems()))
ausgewaehlt = [(table.item(row, 1).text(), table.item(row, 0).text()) for row in ausgewaehlte_zeilen] ausgewaehlt = [(table.item(row, 1).text(), table.item(row, 0).text()) for row in ausgewaehlte_zeilen]
return ausgewaehlt, nur_laden_checkbox.isChecked() return ausgewaehlt
def ask_landkreis_auswahl( def ask_landkreis_auswahl(
+57 -1
View File
@@ -42,6 +42,7 @@ oder kein Ergebnis liefert, bricht den Workflow ab.
from __future__ import annotations from __future__ import annotations
import os import os
import re
import shutil import shutil
import tempfile import tempfile
import zipfile import zipfile
@@ -106,6 +107,57 @@ _GEBIETSTYP_LAYER_NAMEN = {
# Interne Hilfsfunktionen # Interne Hilfsfunktionen
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def _normalisiere_feldname(name: str) -> str:
"""Normalisiert einen Feldnamen für den toleranten Vergleich.
Entfernt alle Zeichen außer Buchstaben/Ziffern (z.B. Punkte, Leerzeichen,
Unterstriche) und wandelt in Kleinbuchstaben um. Hintergrund: manche
Shape-Exporte liefern den Feldnamen ``FBSchl`` mit einem angehängten
Punkt (``FBSchl.``) oder abweichender Groß-/Kleinschreibung, wodurch ein
exakter Namensvergleich das Feld fälschlich als fehlend meldet (#44).
Parameters
----------
name :
Roher Feldname.
Returns
-------
str
Normalisierter Feldname (nur Kleinbuchstaben/Ziffern).
"""
return re.sub(r"[^a-zA-Z0-9]", "", name).lower()
def _finde_fbschl_index(raw_layer) -> int:
"""Sucht den Feldindex von ``FBSchl`` tolerant gegenüber Abweichungen im
Namen (Groß-/Kleinschreibung, angehängte Sonderzeichen wie ein Punkt).
Versucht zuerst den exakten Namen; schlägt das fehl, wird über alle
Felder mit normalisiertem Namensvergleich gesucht (siehe
:func:`_normalisiere_feldname`).
Parameters
----------
raw_layer :
Dateibasierter Shapefile-Layer.
Returns
-------
int
Feldindex, oder ``-1`` wenn kein passendes Feld gefunden wurde.
"""
idx = raw_layer.fields().indexFromName(FBSCHL_FELD)
if idx >= 0:
return idx
ziel = _normalisiere_feldname(FBSCHL_FELD)
for feld_idx, feld in enumerate(raw_layer.fields()):
if _normalisiere_feldname(feld.name()) == ziel:
return feld_idx
return -1
def _ermittle_gebietstyp_aus_fbschl(raw_layer) -> Tuple[Optional[str], Optional[str]]: def _ermittle_gebietstyp_aus_fbschl(raw_layer) -> Tuple[Optional[str], Optional[str]]:
"""Prüft das Feld ``FBSchl`` und ermittelt den Gebietstyp. """Prüft das Feld ``FBSchl`` und ermittelt den Gebietstyp.
@@ -113,6 +165,10 @@ def _ermittle_gebietstyp_aus_fbschl(raw_layer) -> Tuple[Optional[str], Optional[
zurück, damit der Aufrufer Fehler mehrerer Dateien sammeln und am Ende zurück, damit der Aufrufer Fehler mehrerer Dateien sammeln und am Ende
gemeinsam anzeigen kann. gemeinsam anzeigen kann.
Die Feldsuche ist tolerant gegenüber Abweichungen im Namen (z.B. ein
angehängter Punkt ``FBSchl.`` in manchen Shape-Exporten, siehe #44) –
siehe :func:`_finde_fbschl_index`.
Parameters Parameters
---------- ----------
raw_layer : raw_layer :
@@ -127,7 +183,7 @@ def _ermittle_gebietstyp_aus_fbschl(raw_layer) -> Tuple[Optional[str], Optional[
``fehlertext`` beschreibt die Ursache (fehlendes Feld oder ``fehlertext`` beschreibt die Ursache (fehlendes Feld oder
unbekannter Wert). unbekannter Wert).
""" """
idx = raw_layer.fields().indexFromName(FBSCHL_FELD) idx = _finde_fbschl_index(raw_layer)
if idx < 0: if idx < 0:
return None, f"Feld '{FBSCHL_FELD}' fehlt (Shape entspricht nicht dem erwarteten Inhalt)." return None, f"Feld '{FBSCHL_FELD}' fehlt (Shape entspricht nicht dem erwarteten Inhalt)."