Landkreisliste als JSON auslagern; Verfahrensgebiet-Dialoge nach sn_verfahrensgebiet verschoben

- Landkreisliste liegt jetzt in data/landkreise.json statt hartcodiert in
  landkreise.py, damit sie ohne Codeänderung pflegbar und auch aus anderen
  Modulen nutzbar ist (Sentinel ALLE_LANDKREISE für "Alle Landkreise").
- ask_bror_kombinationen, ask_landkreis_objekte und ask_landkreis_auswahl aus
  dialog_wrapper.py entfernt: sie sind sn_verfahrensgebiet-spezifisches
  Vokabular (BROR/VKZ/Landkreis-Fallback) und gehören nicht in den
  domänenfreien sn_basis-Hub. Sie leben jetzt in
  sn_verfahrensgebiet/functions/verfahrensgebiet_dialoge.py.
- qt_wrapper.py: QTableWidget/QTableWidgetItem/QTableWidgetSelectionRange,
  SelectRows, NoEditTriggers, AscendingOrder ergänzt (für die neue
  Tabellen-Auswahl im Landkreis-Objektdialog).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Michael Otto
2026-08-28 13:54:43 +02:00
co-authored by Claude Sonnet 5
parent 749d6d6f52
commit 1b5a715c70
5 changed files with 131 additions and 210 deletions
+3 -1
View File
@@ -1,7 +1,9 @@
- Landkreis-Dropdown (statt Freitext) in den Projekteigenschaften ergänzt, inkl. Landkreiskennzahl als eigene Projektvariable
- Landkreisliste liegt jetzt als JSON (sn_basis/data/landkreise.json) statt hartcodiert vor, damit auch andere Module und Nicht-Entwickler sie pflegen können
- Auswahldialog für mehrere BauRaumOderBodenordnungsrecht-Treffer auf Mehrfachauswahl umgestellt (mehrere Objekte gemeinsam laden/verschmelzen)
- Neuer Dialog zur landkreisweiten Objektsuche (Mehrfachauswahl, "Alle auswählen", "Nur laden ohne weiteren Ablauf")
- Landkreisweiter Objekt-Auswahldialog jetzt als Tabelle (Spalten "Nummer"/"Name", per Spaltenkopf sortierbar, Standard aufsteigend nach Nummer) mit Suchfeld (Filter nach Nummer/Name); "Alle auswählen" berücksichtigt die Filterung; Checkbox-Text zu "Nur Objekte laden (ohne weitere Prüfungen)" geändert
- Dialoge: Titel, Wappen-Icon, kompaktere Größe, Scrollbar-Fix und größenveränderbare Liste ergänzt
- Die drei Verfahrensgebiet-spezifischen ALKIS-Dialoge (BROR-Auswahl, Landkreis-Objektauswahl, Landkreis-Auswahl) sind aus sn_basis nach sn_verfahrensgebiet verschoben (functions/verfahrensgebiet_dialoge.py), da sie fachlich dorthin gehören
---
Version 26.7.1-unstable:
- Verfahrensgebiet als shape laden, inkl. Abgleich mit BROBOR-Objekt
+15
View File
@@ -0,0 +1,15 @@
[
{"kennzahl": "11", "name": "Chemnitz, Stadt"},
{"kennzahl": "12", "name": "Dresden, Stadt"},
{"kennzahl": "13", "name": "Leipzig, Stadt"},
{"kennzahl": "21", "name": "Erzgebirgskreis"},
{"kennzahl": "22", "name": "Mittelsachsen"},
{"kennzahl": "23", "name": "Vogtlandkreis"},
{"kennzahl": "24", "name": "Zwickau"},
{"kennzahl": "25", "name": "Bautzen"},
{"kennzahl": "26", "name": "Görlitz"},
{"kennzahl": "27", "name": "Meißen"},
{"kennzahl": "28", "name": "Sächsische Schweiz-Osterzgebirge"},
{"kennzahl": "29", "name": "Leipzig"},
{"kennzahl": "30", "name": "Nordsachsen"}
]
-189
View File
@@ -6,10 +6,7 @@ from typing import Literal, Optional
from sn_basis.functions.qt_wrapper import (
QMessageBox, YES, NO, CANCEL, QT_VERSION, exec_dialog, ICON_QUESTION,
QProgressDialog, QCoreApplication, Qt, QInputDialog, QLineEdit,
QDialog, QDialogButtonBox, QListWidget, QListWidgetItem, QAbstractItemView,
QIcon, QVBoxLayout, QLabel, QCheckBox, DialogAccepted, ExtendedSelection,
)
import os
def ask_yes_no(
title: str,
@@ -393,189 +390,3 @@ class ProgressDialog:
def create_progress_dialog(total: int, title: str = "Fortschritt", label: str = "Verarbeite...") -> ProgressDialog:
return ProgressDialog(total, title, label)
# .../python/plugins (drei Ebenen über dieser Datei)
_PLUGINS_DIR = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
_WAPPEN_ICON = os.path.join(_PLUGINS_DIR, "sn_basis", "favicon.ico")
def ask_bror_kombinationen(
parent: Any,
kombinationen: list[tuple[str, str]],
) -> list[tuple[str, str]]:
"""Zeigt Dialog zur Auswahl einer oder mehrerer BROR-Kombinationen
(adv_name + bezeichnung).
Wird aufgerufen, wenn der WFS mehrere verschiedene Kombinationen aus
``adv_name`` und ``bezeichnung`` zurückliefert. Es können ein oder
mehrere Objekte gemeinsam ausgewählt und geladen (verschmolzen) werden.
Parameters
----------
parent :
Eltern-Widget oder None.
kombinationen :
Sortierte Liste von ``(adv_name, bezeichnung)``-Tupeln.
Returns
-------
list[tuple[str, str]]
Die gewählten ``(adv_name, bezeichnung)``-Tupel. Leere Liste bei
Abbruch oder wenn nichts ausgewählt wurde.
"""
if not kombinationen:
return []
if QT_VERSION == 0: # Mock-Modus
print(f"Mock-Modus: ask_bror_kombinationen → {kombinationen[:1]}")
return kombinationen[:1]
dialog = QDialog(parent)
dialog.setWindowTitle("Verfahrensgebiet aus ALKIS laden")
if os.path.exists(_WAPPEN_ICON):
dialog.setWindowIcon(QIcon(_WAPPEN_ICON))
layout = QVBoxLayout(dialog)
hinweis = QLabel(
f"Es wurden {len(kombinationen)} Objekte gefunden.\n\n"
"Bitte wählen Sie eines oder mehrere Objekte aus:"
)
hinweis.setWordWrap(True)
layout.addWidget(hinweis)
list_widget = QListWidget()
list_widget.setSelectionMode(ExtendedSelection)
for adv, bez in kombinationen:
item = QListWidgetItem(f"{adv} | {bez}")
list_widget.addItem(item)
list_widget.item(0).setSelected(True)
layout.addWidget(list_widget, 1) # Stretch-Faktor: wächst mit Fenstergröße
mehrfachauswahl_hinweis = QLabel("<small>Mehrfachauswahl mit Strg + Umschalt</small>")
layout.addWidget(mehrfachauswahl_hinweis)
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
buttons.accepted.connect(dialog.accept)
buttons.rejected.connect(dialog.reject)
layout.addWidget(buttons)
dialog.adjustSize()
breite = dialog.sizeHint().width()
# Liste auf max. 5 sichtbare Zeilen begrenzen, Rest per Scrollbar
sichtbare_zeilen = min(len(kombinationen), 5)
zeilenhoehe = list_widget.sizeHintForRow(0) if list_widget.count() else 20
# + Puffer für eine ggf. eingeblendete horizontale Scrollbar (bei langen
# Einträgen), damit diese nicht die letzte Zeile abschneidet.
scrollbar_puffer = list_widget.horizontalScrollBar().sizeHint().height()
list_hoehe = zeilenhoehe * sichtbare_zeilen + 2 * list_widget.frameWidth() + 4 + scrollbar_puffer
list_widget.setMinimumHeight(list_hoehe)
dialog.resize(breite, dialog.sizeHint().height())
if exec_dialog(dialog) != DialogAccepted:
return []
ausgewaehlte_indizes = sorted(list_widget.row(item) for item in list_widget.selectedItems())
return [kombinationen[i] for i in ausgewaehlte_indizes]
def ask_landkreis_objekte(
parent: Any,
kombinationen: list[tuple[str, str]],
) -> tuple[list[tuple[str, str]], bool]:
"""Zeigt Dialog zur Auswahl eines, mehrerer oder aller BROR-Objekte eines
Landkreises (Fallback, wenn keine Verfahrensnummer gesetzt ist).
Wie :func:`ask_bror_kombinationen`, zusätzlich mit einer "Alle
auswählen"-Checkbox (da hier potenziell deutlich mehr Treffer als bei der
VKZ-Suche auftreten) und einer "Nur laden"-Checkbox, mit der gesteuert
wird, ob nach dem Laden der weitere Workflow (Gemarkung/Flurstücke/
Flächenvergleich) fortgesetzt werden soll.
Parameters
----------
parent :
Eltern-Widget oder None.
kombinationen :
Sortierte Liste von ``(adv_name, bezeichnung)``-Tupeln.
Returns
-------
tuple[list[tuple[str, str]], bool]
``(ausgewaehlte_kombinationen, nur_laden)``. ``([], False)`` bei
Abbruch oder wenn nichts ausgewählt wurde.
"""
if not kombinationen:
return [], False
if QT_VERSION == 0: # Mock-Modus
print(f"Mock-Modus: ask_landkreis_objekte → {kombinationen[:1]}")
return kombinationen[:1], False
dialog = QDialog(parent)
dialog.setWindowTitle("Verfahrensgebiete aus Landkreis laden")
if os.path.exists(_WAPPEN_ICON):
dialog.setWindowIcon(QIcon(_WAPPEN_ICON))
layout = QVBoxLayout(dialog)
hinweis = QLabel(
f"Es wurden {len(kombinationen)} Objekte gefunden.\n\n"
"Bitte wählen Sie eines oder mehrere Objekte aus:"
)
hinweis.setWordWrap(True)
layout.addWidget(hinweis)
alle_auswaehlen = QCheckBox("Alle auswählen")
layout.addWidget(alle_auswaehlen)
list_widget = QListWidget()
list_widget.setSelectionMode(ExtendedSelection)
for adv, bez in kombinationen:
item = QListWidgetItem(f"{bez} | {adv}")
list_widget.addItem(item)
list_widget.item(0).setSelected(True)
layout.addWidget(list_widget, 1) # Stretch-Faktor: wächst mit Fenstergröße
def _alle_auswaehlen_umschalten(checked):
if checked:
list_widget.selectAll()
else:
list_widget.clearSelection()
alle_auswaehlen.stateChanged.connect(_alle_auswaehlen_umschalten)
mehrfachauswahl_hinweis = QLabel("<small>Mehrfachauswahl mit Strg + Umschalt</small>")
layout.addWidget(mehrfachauswahl_hinweis)
nur_laden_checkbox = QCheckBox("Nur laden (kein weiterer Ablauf)")
layout.addWidget(nur_laden_checkbox)
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
buttons.accepted.connect(dialog.accept)
buttons.rejected.connect(dialog.reject)
layout.addWidget(buttons)
dialog.adjustSize()
breite = dialog.sizeHint().width()
# Liste auf max. 15 sichtbare Zeilen begrenzen, Rest per Scrollbar hier
# potenziell deutlich mehr Treffer als bei der VKZ-Suche, daher großzügiger
# bemessen als bei ask_bror_kombinationen (dort 5).
sichtbare_zeilen = min(len(kombinationen), 15)
zeilenhoehe = list_widget.sizeHintForRow(0) if list_widget.count() else 20
# + Puffer für eine ggf. eingeblendete horizontale Scrollbar (bei langen
# Einträgen), damit diese nicht die letzte Zeile abschneidet.
scrollbar_puffer = list_widget.horizontalScrollBar().sizeHint().height()
list_hoehe = zeilenhoehe * sichtbare_zeilen + 2 * list_widget.frameWidth() + 4 + scrollbar_puffer
list_widget.setMinimumHeight(list_hoehe)
dialog.resize(breite, dialog.sizeHint().height())
if exec_dialog(dialog) != DialogAccepted:
return [], False
ausgewaehlte_indizes = sorted(list_widget.row(item) for item in list_widget.selectedItems())
ausgewaehlt = [kombinationen[i] for i in ausgewaehlte_indizes]
return ausgewaehlt, nur_laden_checkbox.isChecked()
+39 -20
View File
@@ -1,28 +1,47 @@
"""sn_basis/functions/landkreise.py Sächsische Landkreise/Helfer mit Kennzahl.
Quelle: landkreiskennzahlen.csv
Daten liegen in sn_basis/data/landkreise.json (auch von anderen sn_*-Modulen
lesbar, z.B. per ``from sn_basis.functions.landkreise import LANDKREISE``).
"""
#: Liste von (Landkreiskennzahl, Landkreis/Helfer)-Tupeln, in CSV-Reihenfolge.
LANDKREISE: list[tuple[str, str]] = [
("11", "Chemnitz, Stadt"),
("12", "Dresden, Stadt"),
("13", "Leipzig, Stadt"),
("21", "Erzgebirgskreis"),
("22", "Mittelsachsen"),
("23", "Vogtlandkreis"),
("24", "Zwickau"),
("25", "Bautzen"),
("26", "Görlitz"),
("27", "Meißen"),
("28", "Sächsische Schweiz-Osterzgebirge"),
("29", "Leipzig"),
("30", "Nordsachsen"),
("80", "LfULG"),
]
import json
import os
try:
from qgis.core import Qgis, QgsMessageLog
except Exception: # Mock-Modus / außerhalb QGIS
Qgis = None
QgsMessageLog = None
# .../sn_basis/data/landkreise.json (eine Ebene über dieser Datei, dann data/)
_DATA_DATEI = os.path.join(os.path.dirname(os.path.dirname(__file__)), "data", "landkreise.json")
def _lade_landkreise() -> "list[tuple[str, str]]":
try:
with open(_DATA_DATEI, "r", encoding="utf-8") as f:
rohdaten = json.load(f)
return [(eintrag["kennzahl"], eintrag["name"]) for eintrag in rohdaten]
except Exception as exc:
if QgsMessageLog is not None:
QgsMessageLog.logMessage(
f"landkreise.json konnte nicht geladen werden ({_DATA_DATEI}): {exc}",
"sn_basis",
Qgis.Warning,
)
else:
print(f"⚠️ landkreise.json konnte nicht geladen werden ({_DATA_DATEI}): {exc}")
return []
#: Liste von (Landkreiskennzahl, Landkreis/Helfer)-Tupeln, in Datei-Reihenfolge.
LANDKREISE: "list[tuple[str, str]]" = _lade_landkreise()
#: Kennzahl -> Name
KENNZAHL_ZU_NAME: dict[str, str] = dict(LANDKREISE)
KENNZAHL_ZU_NAME: "dict[str, str]" = dict(LANDKREISE)
#: Name -> Kennzahl
NAME_ZU_KENNZAHL: dict[str, str] = {name: kennzahl for kennzahl, name in LANDKREISE}
NAME_ZU_KENNZAHL: "dict[str, str]" = {name: kennzahl for kennzahl, name in LANDKREISE}
#: Sentinel-"Kennzahl" für die Auswahl "Alle Landkreise" (kein Landkreis-Filter).
ALLE_LANDKREISE = "ALLE"
+74
View File
@@ -51,6 +51,9 @@ QDialogButtonBox: Type[Any] = object
QListWidget: Type[Any] = object
QListWidgetItem: Type[Any] = object
QAbstractItemView: Type[Any] = object
QTableWidget: Type[Any] = object
QTableWidgetItem: Type[Any] = object
QTableWidgetSelectionRange: Type[Any] = object
QIcon: Type[Any] = object
@@ -103,6 +106,9 @@ try:
QListWidget as _QListWidget,
QListWidgetItem as _QListWidgetItem,
QAbstractItemView as _QAbstractItemView,
QTableWidget as _QTableWidget,
QTableWidgetItem as _QTableWidgetItem,
QTableWidgetSelectionRange as _QTableWidgetSelectionRange,
)
from qgis.PyQt.QtGui import QFont as _QFont, QIcon as _QIcon
from qgis.PyQt.QtCore import (
@@ -159,6 +165,9 @@ try:
QListWidget = _QListWidget
QListWidgetItem = _QListWidgetItem
QAbstractItemView = _QAbstractItemView
QTableWidget = _QTableWidget
QTableWidgetItem = _QTableWidgetItem
QTableWidgetSelectionRange = _QTableWidgetSelectionRange
QIcon = _QIcon
# ✅ QT6 ENUMS
YES = QMessageBox.StandardButton.Yes
@@ -182,6 +191,9 @@ try:
DockAreaRight = Qt.DockWidgetArea.RightDockWidgetArea
DialogAccepted = QDialog.DialogCode.Accepted
ExtendedSelection = QAbstractItemView.SelectionMode.ExtendedSelection
SelectRows = QAbstractItemView.SelectionBehavior.SelectRows
NoEditTriggers = QAbstractItemView.EditTrigger.NoEditTriggers
AscendingOrder = Qt.SortOrder.AscendingOrder
def exec_dialog(dialog: Any) -> Any:
return dialog.exec()
@@ -218,6 +230,9 @@ except (ImportError, AttributeError):
QListWidget as _QListWidget,
QListWidgetItem as _QListWidgetItem,
QAbstractItemView as _QAbstractItemView,
QTableWidget as _QTableWidget,
QTableWidgetItem as _QTableWidgetItem,
QTableWidgetSelectionRange as _QTableWidgetSelectionRange,
)
from PyQt5.QtGui import QFont as _QFont, QIcon as _QIcon
from PyQt5.QtCore import (
@@ -272,6 +287,9 @@ except (ImportError, AttributeError):
QListWidget = _QListWidget
QListWidgetItem = _QListWidgetItem
QAbstractItemView = _QAbstractItemView
QTableWidget = _QTableWidget
QTableWidgetItem = _QTableWidgetItem
QTableWidgetSelectionRange = _QTableWidgetSelectionRange
QIcon = _QIcon
# ✅ PYQT5 ENUMS
@@ -297,6 +315,9 @@ except (ImportError, AttributeError):
DockAreaRight = Qt.RightDockWidgetArea
DialogAccepted = QDialog.Accepted
ExtendedSelection = QAbstractItemView.ExtendedSelection
SelectRows = QAbstractItemView.SelectRows
NoEditTriggers = QAbstractItemView.NoEditTriggers
AscendingOrder = Qt.AscendingOrder
def exec_dialog(dialog: Any) -> Any:
return dialog.exec_()
@@ -351,6 +372,10 @@ except (ImportError, AttributeError):
def getText(parent, title, label, mode=None, text=""):
return text, True
@staticmethod
def getItem(parent, title, label, items, current=0, editable=False):
return (items[current] if items else ""), True
QInputDialog = _MockQInputDialog
class _MockQEventLoop:
@@ -650,6 +675,9 @@ except (ImportError, AttributeError):
DialogAccepted = 1
ExtendedSelection = 3
SelectRows = 1
NoEditTriggers = 0
AscendingOrder = 0
class _MockQDialog(_MockWidget):
DialogCode = type("DialogCode", (), {"Accepted": DialogAccepted, "Rejected": 0})
@@ -709,6 +737,52 @@ except (ImportError, AttributeError):
QAbstractItemView = _MockQAbstractItemView
class _MockQTableWidgetItem:
def __init__(self, text: str = ""):
self._text = text
def text(self) -> str: return self._text
def setText(self, text: str) -> None: self._text = text
QTableWidgetItem = _MockQTableWidgetItem
class _MockQTableWidget(_MockWidget):
def __init__(self, rows: int = 0, cols: int = 0, *args, **kwargs):
self._items = {}
self._row_count = rows
self._col_count = cols
self._hidden_rows = set()
def setItem(self, row: int, col: int, item) -> None: self._items[(row, col)] = item
def item(self, row: int, col: int): return self._items.get((row, col))
def rowCount(self) -> int: return self._row_count
def setRowCount(self, rows: int) -> None: self._row_count = rows
def setHorizontalHeaderLabels(self, labels) -> None: pass
def setSelectionMode(self, *args, **kwargs) -> None: pass
def setSelectionBehavior(self, *args, **kwargs) -> None: pass
def setEditTriggers(self, *args, **kwargs) -> None: pass
def setSortingEnabled(self, *args, **kwargs) -> None: pass
def setRowHidden(self, row: int, hidden: bool) -> None:
if hidden: self._hidden_rows.add(row)
else: self._hidden_rows.discard(row)
def isRowHidden(self, row: int) -> bool: return row in self._hidden_rows
def selectRow(self, row: int) -> None: pass
def selectedItems(self): return []
def setRangeSelected(self, *args, **kwargs) -> None: pass
def sortByColumn(self, *args, **kwargs) -> None: pass
def resizeColumnToContents(self, col: int) -> None: pass
def setColumnWidth(self, col: int, width: int) -> None: pass
def columnWidth(self, col: int) -> int: return 100
def columnCount(self) -> int: return self._col_count
def verticalHeader(self): return _MockWidget()
def horizontalHeader(self): return _MockWidget()
def frameWidth(self) -> int: return 1
QTableWidget = _MockQTableWidget
class _MockQTableWidgetSelectionRange:
def __init__(self, *args, **kwargs): pass
QTableWidgetSelectionRange = _MockQTableWidgetSelectionRange
class _MockQIcon:
def __init__(self, *args, **kwargs): pass