Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7755e3e8cb | ||
|
|
bc9bb20597 | ||
|
|
113fda7d04 | ||
|
|
8545152317 | ||
|
|
8cea9022f0 | ||
|
|
1b5a715c70 | ||
|
|
749d6d6f52 | ||
|
|
29fea7c18f | ||
|
|
d857343d36 | ||
|
|
7bcd0132e9 | ||
|
|
3489a08638 | ||
|
|
7a5ed12797 | ||
|
|
021747ae06 | ||
|
|
91bafe632f | ||
|
|
7ed22fb23e | ||
|
|
3bbb66ec32 | ||
|
|
9862386646 |
+13
-1
@@ -1,5 +1,17 @@
|
|||||||
|
- 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)
|
||||||
|
- 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.6.1-unstable:
|
Version 26.8.1-unstable:
|
||||||
|
-Vergleich einer CSV aus dem BI-Server mit den Beteiligten Flurstücken lt. ALKIS
|
||||||
|
---
|
||||||
|
Version 26.7.1-unstable:
|
||||||
|
- Verfahrensgebiet als shape laden, inkl. Abgleich mit BROBOR-Objekt
|
||||||
|
---
|
||||||
|
Version 26.7.1-testing:
|
||||||
- VLN-API zum Laden der Plan41-Maßnahmen integriert
|
- VLN-API zum Laden der Plan41-Maßnahmen integriert
|
||||||
---
|
---
|
||||||
Version 26.5.2-testing:
|
Version 26.5.2-testing:
|
||||||
|
|||||||
@@ -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"}
|
||||||
|
]
|
||||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
@@ -21,6 +21,7 @@ from .ly_metadata_wrapper import (
|
|||||||
)
|
)
|
||||||
from .ly_style_wrapper import apply_style
|
from .ly_style_wrapper import apply_style
|
||||||
from .dialog_wrapper import ask_yes_no, ask_overwrite_append_cancel_custom, ask_ergaenzen_entfernen_ersetzen_abbrechen, ask_detailpruefung_oder_vg_laden
|
from .dialog_wrapper import ask_yes_no, ask_overwrite_append_cancel_custom, ask_ergaenzen_entfernen_ersetzen_abbrechen, ask_detailpruefung_oder_vg_laden
|
||||||
|
from .laenderkennung import get_laenderkennung, LAENDERKENNUNGEN
|
||||||
|
|
||||||
from .message_wrapper import (
|
from .message_wrapper import (
|
||||||
_get_message_bar,
|
_get_message_bar,
|
||||||
|
|||||||
+46
-50
@@ -232,6 +232,52 @@ def ask_detailpruefung_oder_vg_laden(
|
|||||||
return "abbrechen"
|
return "abbrechen"
|
||||||
|
|
||||||
|
|
||||||
|
def ask_detailpruefung_oder_abbrechen(
|
||||||
|
parent: Any,
|
||||||
|
title: str,
|
||||||
|
message: str,
|
||||||
|
) -> Literal["detailpruefung", "abbrechen"]:
|
||||||
|
"""Zeigt Dialog mit zwei Optionen nach erkannter Flächenabweichung (Shape-Workflow).
|
||||||
|
|
||||||
|
Optionen:
|
||||||
|
- **Detailprüfung starten**: Knickpunktbasierte Prüfung des Shape-VG-Umrings
|
||||||
|
- **Abbrechen**: Vorgang beenden, Shape-Verfahrensgebiet unverändert behalten
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
parent :
|
||||||
|
Eltern-Widget oder None.
|
||||||
|
title : str
|
||||||
|
Dialog-Titel.
|
||||||
|
message : str
|
||||||
|
Hauptmeldung mit Flächen-Informationen.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
Literal["detailpruefung", "abbrechen"]
|
||||||
|
Genaue Entscheidung des Nutzers.
|
||||||
|
"""
|
||||||
|
if QT_VERSION == 0: # Mock-Modus
|
||||||
|
print(f"Mock-Modus: ask_detailpruefung_oder_abbrechen('{title}') → abbrechen")
|
||||||
|
return "abbrechen"
|
||||||
|
|
||||||
|
msg = QMessageBox(parent)
|
||||||
|
msg.setIcon(ICON_QUESTION)
|
||||||
|
msg.setWindowTitle(title)
|
||||||
|
msg.setText(message)
|
||||||
|
|
||||||
|
detail_btn = msg.addButton("Detailprüfung starten", QMessageBox.ButtonRole.AcceptRole)
|
||||||
|
msg.addButton("Abbrechen", QMessageBox.ButtonRole.RejectRole)
|
||||||
|
|
||||||
|
exec_dialog(msg)
|
||||||
|
|
||||||
|
clicked = msg.clickedButton()
|
||||||
|
if clicked == detail_btn:
|
||||||
|
return "detailpruefung"
|
||||||
|
else:
|
||||||
|
return "abbrechen"
|
||||||
|
|
||||||
|
|
||||||
class ProgressDialog:
|
class ProgressDialog:
|
||||||
def __init__(self, total: int, title: str = "Fortschritt", label: str = "Verarbeite..."):
|
def __init__(self, total: int, title: str = "Fortschritt", label: str = "Verarbeite..."):
|
||||||
self.total = max(total, 1)
|
self.total = max(total, 1)
|
||||||
@@ -344,53 +390,3 @@ class ProgressDialog:
|
|||||||
def create_progress_dialog(total: int, title: str = "Fortschritt", label: str = "Verarbeite...") -> ProgressDialog:
|
def create_progress_dialog(total: int, title: str = "Fortschritt", label: str = "Verarbeite...") -> ProgressDialog:
|
||||||
return ProgressDialog(total, title, label)
|
return ProgressDialog(total, title, label)
|
||||||
|
|
||||||
|
|
||||||
def ask_bror_kombination(
|
|
||||||
parent: Any,
|
|
||||||
kombinationen: list[tuple[str, str]],
|
|
||||||
) -> Optional[tuple[str, str]]:
|
|
||||||
"""Zeigt Dialog zur Auswahl einer BROR-Kombination (adv_name + bezeichnung).
|
|
||||||
|
|
||||||
Wird aufgerufen, wenn der WFS mehrere verschiedene Kombinationen aus
|
|
||||||
``adv_name`` und ``bezeichnung`` zurückliefert, sodass der Nutzer
|
|
||||||
entscheiden muss, welche Objekte in die Pipeline einfließen sollen.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
parent :
|
|
||||||
Eltern-Widget oder None.
|
|
||||||
kombinationen :
|
|
||||||
Sortierte Liste von ``(adv_name, bezeichnung)``-Tupeln.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
Optional[tuple[str, str]]
|
|
||||||
Das gewählte ``(adv_name, bezeichnung)``-Tupel oder ``None`` bei
|
|
||||||
Abbruch bzw. leerer Eingabe.
|
|
||||||
"""
|
|
||||||
if not kombinationen:
|
|
||||||
return None
|
|
||||||
|
|
||||||
if QT_VERSION == 0: # Mock-Modus
|
|
||||||
print(f"Mock-Modus: ask_bror_kombination → {kombinationen[0]}")
|
|
||||||
return kombinationen[0]
|
|
||||||
|
|
||||||
items = [f"{adv} | {bez}" for adv, bez in kombinationen]
|
|
||||||
item, ok = QInputDialog.getItem(
|
|
||||||
parent,
|
|
||||||
"BROR-Objekt auswählen",
|
|
||||||
"Mehrere BauRaumOderBodenordnungsrecht-Objekte gefunden.\n"
|
|
||||||
"Bitte wählen Sie die gewünschte Kombination\n"
|
|
||||||
"aus ADV-Name und Bezeichnung:",
|
|
||||||
items,
|
|
||||||
0,
|
|
||||||
False,
|
|
||||||
)
|
|
||||||
if not ok:
|
|
||||||
return None
|
|
||||||
try:
|
|
||||||
idx = items.index(item)
|
|
||||||
return kombinationen[idx]
|
|
||||||
except ValueError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
"""
|
||||||
|
sn_basis/functions/laenderkennung.py – ALKIS-Länderkennung je Plugin-Präfix.
|
||||||
|
|
||||||
|
AGLB-länderspezifische Funktionen (z.B. Flurstückskennzeichen-Bildung) sollen
|
||||||
|
nicht hart auf Sachsen ("14") verdrahtet sein, sondern die Länderkennung über
|
||||||
|
dieses generische Mapping ermitteln. Für neue Bundesländer genügt ein weiterer
|
||||||
|
Eintrag in ``LAENDERKENNUNGEN`` – die aufrufende Logik bleibt unverändert.
|
||||||
|
"""
|
||||||
|
|
||||||
|
#: Mapping von sn_*-Plugin-Präfix (ohne Unterstrich) auf die zweistellige
|
||||||
|
#: ALKIS-Länderkennung. Aktuell nur Sachsen befüllt; weitere Bundesländer
|
||||||
|
#: können hier ergänzt werden, sobald Schwesterplugins dafür existieren.
|
||||||
|
LAENDERKENNUNGEN: dict[str, str] = {
|
||||||
|
"sn": "14", # Sachsen
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_laenderkennung(plugin_praefix: str | None = None) -> str:
|
||||||
|
"""Ermittelt die ALKIS-Länderkennung für einen Plugin-Präfix.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
plugin_praefix : str, optional
|
||||||
|
Plugin-Präfix ohne Unterstrich, z.B. ``"sn"``. Wird kein Präfix
|
||||||
|
übergeben, wird er aus dem Package-Namen des aufrufenden Moduls
|
||||||
|
abgeleitet (``__name__`` des Aufrufers, erstes ``_``-Segment).
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
str
|
||||||
|
Zweistellige Länderkennung, z.B. ``"14"``.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
KeyError
|
||||||
|
Wenn für den ermittelten Präfix keine Länderkennung hinterlegt ist.
|
||||||
|
"""
|
||||||
|
if not plugin_praefix:
|
||||||
|
import inspect
|
||||||
|
|
||||||
|
frame = inspect.stack()[1]
|
||||||
|
caller_module = frame.frame.f_globals.get("__name__", "")
|
||||||
|
plugin_praefix = caller_module.split(".")[0].split("_")[0]
|
||||||
|
|
||||||
|
plugin_praefix = plugin_praefix.lower()
|
||||||
|
if plugin_praefix not in LAENDERKENNUNGEN:
|
||||||
|
raise KeyError(
|
||||||
|
f"Keine Länderkennung für Plugin-Präfix '{plugin_praefix}' hinterlegt. "
|
||||||
|
f"Bekannte Präfixe: {sorted(LAENDERKENNUNGEN)}"
|
||||||
|
)
|
||||||
|
return LAENDERKENNUNGEN[plugin_praefix]
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
"""sn_basis/functions/landkreise.py – Sächsische Landkreise/Helfer mit Kennzahl.
|
||||||
|
|
||||||
|
Daten liegen in sn_basis/data/landkreise.json (auch von anderen sn_*-Modulen
|
||||||
|
lesbar, z.B. per ``from sn_basis.functions.landkreise import LANDKREISE``).
|
||||||
|
"""
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
#: Name -> Kennzahl
|
||||||
|
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"
|
||||||
+193
-10
@@ -46,6 +46,15 @@ QCheckBox: Type[Any] = object
|
|||||||
QHBoxLayout: Type[Any] = object
|
QHBoxLayout: Type[Any] = object
|
||||||
QFont: Type[Any] = object
|
QFont: Type[Any] = object
|
||||||
QSettings: Type[Any] = object
|
QSettings: Type[Any] = object
|
||||||
|
QDialog: Type[Any] = object
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
def exec_dialog(dialog: Any) -> Any:
|
def exec_dialog(dialog: Any) -> Any:
|
||||||
@@ -92,8 +101,16 @@ try:
|
|||||||
QComboBox as _QComboBox,
|
QComboBox as _QComboBox,
|
||||||
QCheckBox as _QCheckBox,
|
QCheckBox as _QCheckBox,
|
||||||
QHBoxLayout as _QHBoxLayout,
|
QHBoxLayout as _QHBoxLayout,
|
||||||
|
QDialog as _QDialog,
|
||||||
|
QDialogButtonBox as _QDialogButtonBox,
|
||||||
|
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
|
from qgis.PyQt.QtGui import QFont as _QFont, QIcon as _QIcon
|
||||||
from qgis.PyQt.QtCore import (
|
from qgis.PyQt.QtCore import (
|
||||||
QEventLoop as _QEventLoop,
|
QEventLoop as _QEventLoop,
|
||||||
QTimer as _QTimer,
|
QTimer as _QTimer,
|
||||||
@@ -143,6 +160,15 @@ try:
|
|||||||
QHBoxLayout = _QHBoxLayout
|
QHBoxLayout = _QHBoxLayout
|
||||||
QFont = _QFont
|
QFont = _QFont
|
||||||
QSettings = _QSettings
|
QSettings = _QSettings
|
||||||
|
QDialog = _QDialog
|
||||||
|
QDialogButtonBox = _QDialogButtonBox
|
||||||
|
QListWidget = _QListWidget
|
||||||
|
QListWidgetItem = _QListWidgetItem
|
||||||
|
QAbstractItemView = _QAbstractItemView
|
||||||
|
QTableWidget = _QTableWidget
|
||||||
|
QTableWidgetItem = _QTableWidgetItem
|
||||||
|
QTableWidgetSelectionRange = _QTableWidgetSelectionRange
|
||||||
|
QIcon = _QIcon
|
||||||
# ✅ QT6 ENUMS
|
# ✅ QT6 ENUMS
|
||||||
YES = QMessageBox.StandardButton.Yes
|
YES = QMessageBox.StandardButton.Yes
|
||||||
NO = QMessageBox.StandardButton.No
|
NO = QMessageBox.StandardButton.No
|
||||||
@@ -163,7 +189,12 @@ try:
|
|||||||
DockWidgetClosable = QDockWidget.DockWidgetFeature.DockWidgetClosable
|
DockWidgetClosable = QDockWidget.DockWidgetFeature.DockWidgetClosable
|
||||||
DockAreaLeft = Qt.DockWidgetArea.LeftDockWidgetArea
|
DockAreaLeft = Qt.DockWidgetArea.LeftDockWidgetArea
|
||||||
DockAreaRight = Qt.DockWidgetArea.RightDockWidgetArea
|
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:
|
def exec_dialog(dialog: Any) -> Any:
|
||||||
return dialog.exec()
|
return dialog.exec()
|
||||||
|
|
||||||
@@ -194,8 +225,16 @@ except (ImportError, AttributeError):
|
|||||||
QComboBox as _QComboBox,
|
QComboBox as _QComboBox,
|
||||||
QCheckBox as _QCheckBox,
|
QCheckBox as _QCheckBox,
|
||||||
QHBoxLayout as _QHBoxLayout,
|
QHBoxLayout as _QHBoxLayout,
|
||||||
|
QDialog as _QDialog,
|
||||||
|
QDialogButtonBox as _QDialogButtonBox,
|
||||||
|
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
|
from PyQt5.QtGui import QFont as _QFont, QIcon as _QIcon
|
||||||
from PyQt5.QtCore import (
|
from PyQt5.QtCore import (
|
||||||
QEventLoop as _QEventLoop,
|
QEventLoop as _QEventLoop,
|
||||||
QTimer as _QTimer,
|
QTimer as _QTimer,
|
||||||
@@ -243,7 +282,16 @@ except (ImportError, AttributeError):
|
|||||||
QHBoxLayout= _QHBoxLayout
|
QHBoxLayout= _QHBoxLayout
|
||||||
QFont = _QFont
|
QFont = _QFont
|
||||||
QSettings = _QSettings
|
QSettings = _QSettings
|
||||||
|
QDialog = _QDialog
|
||||||
|
QDialogButtonBox = _QDialogButtonBox
|
||||||
|
QListWidget = _QListWidget
|
||||||
|
QListWidgetItem = _QListWidgetItem
|
||||||
|
QAbstractItemView = _QAbstractItemView
|
||||||
|
QTableWidget = _QTableWidget
|
||||||
|
QTableWidgetItem = _QTableWidgetItem
|
||||||
|
QTableWidgetSelectionRange = _QTableWidgetSelectionRange
|
||||||
|
QIcon = _QIcon
|
||||||
|
|
||||||
# ✅ PYQT5 ENUMS
|
# ✅ PYQT5 ENUMS
|
||||||
YES = QMessageBox.Yes
|
YES = QMessageBox.Yes
|
||||||
NO = QMessageBox.No
|
NO = QMessageBox.No
|
||||||
@@ -265,7 +313,12 @@ except (ImportError, AttributeError):
|
|||||||
DockWidgetClosable = QDockWidget.DockWidgetClosable
|
DockWidgetClosable = QDockWidget.DockWidgetClosable
|
||||||
DockAreaLeft = Qt.LeftDockWidgetArea
|
DockAreaLeft = Qt.LeftDockWidgetArea
|
||||||
DockAreaRight = Qt.RightDockWidgetArea
|
DockAreaRight = Qt.RightDockWidgetArea
|
||||||
|
DialogAccepted = QDialog.Accepted
|
||||||
|
ExtendedSelection = QAbstractItemView.ExtendedSelection
|
||||||
|
SelectRows = QAbstractItemView.SelectRows
|
||||||
|
NoEditTriggers = QAbstractItemView.NoEditTriggers
|
||||||
|
AscendingOrder = Qt.AscendingOrder
|
||||||
|
|
||||||
def exec_dialog(dialog: Any) -> Any:
|
def exec_dialog(dialog: Any) -> Any:
|
||||||
return dialog.exec_()
|
return dialog.exec_()
|
||||||
|
|
||||||
@@ -319,6 +372,10 @@ except (ImportError, AttributeError):
|
|||||||
def getText(parent, title, label, mode=None, text=""):
|
def getText(parent, title, label, mode=None, text=""):
|
||||||
return text, True
|
return text, True
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def getItem(parent, title, label, items, current=0, editable=False):
|
||||||
|
return (items[current] if items else ""), True
|
||||||
|
|
||||||
QInputDialog = _MockQInputDialog
|
QInputDialog = _MockQInputDialog
|
||||||
|
|
||||||
class _MockQEventLoop:
|
class _MockQEventLoop:
|
||||||
@@ -482,13 +539,19 @@ except (ImportError, AttributeError):
|
|||||||
|
|
||||||
class _MockComboBox:
|
class _MockComboBox:
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
self._items = []
|
self._items = [] # Liste von (text, userData)
|
||||||
self._index = -1
|
self._index = -1
|
||||||
self.currentTextChanged = type('Signal', (), {'connect': lambda s, cb: None, 'emit': lambda s, v: None})()
|
self.currentTextChanged = type('Signal', (), {'connect': lambda s, cb: None, 'emit': lambda s, v: None})()
|
||||||
def addItem(self, text: str) -> None: self._items.append(text)
|
def addItem(self, text: str, userData=None) -> None:
|
||||||
|
self._items.append((text, userData))
|
||||||
|
if self._index < 0:
|
||||||
|
self._index = 0
|
||||||
def addItems(self, items): [self.addItem(it) for it in items]
|
def addItems(self, items): [self.addItem(it) for it in items]
|
||||||
def findText(self, text: str) -> int:
|
def findText(self, text: str) -> int:
|
||||||
return self._items.index(text) if text in self._items else -1
|
for i, (t, _) in enumerate(self._items):
|
||||||
|
if t == text:
|
||||||
|
return i
|
||||||
|
return -1
|
||||||
def setCurrentIndex(self, idx: int) -> None:
|
def setCurrentIndex(self, idx: int) -> None:
|
||||||
if 0 <= idx < len(self._items):
|
if 0 <= idx < len(self._items):
|
||||||
self._index = idx
|
self._index = idx
|
||||||
@@ -497,7 +560,11 @@ except (ImportError, AttributeError):
|
|||||||
idx = self.findText(text)
|
idx = self.findText(text)
|
||||||
if idx >= 0: self.setCurrentIndex(idx)
|
if idx >= 0: self.setCurrentIndex(idx)
|
||||||
def currentText(self) -> str:
|
def currentText(self) -> str:
|
||||||
return self._items[self._index] if 0 <= self._index < len(self._items) else ""
|
return self._items[self._index][0] if 0 <= self._index < len(self._items) else ""
|
||||||
|
def currentData(self):
|
||||||
|
return self._items[self._index][1] if 0 <= self._index < len(self._items) else None
|
||||||
|
def count(self) -> int:
|
||||||
|
return len(self._items)
|
||||||
|
|
||||||
QComboBox = _MockComboBox
|
QComboBox = _MockComboBox
|
||||||
|
|
||||||
@@ -605,6 +672,122 @@ except (ImportError, AttributeError):
|
|||||||
self._checked = checked
|
self._checked = checked
|
||||||
|
|
||||||
QCheckBox = _MockQCheckBox
|
QCheckBox = _MockQCheckBox
|
||||||
|
|
||||||
|
DialogAccepted = 1
|
||||||
|
ExtendedSelection = 3
|
||||||
|
SelectRows = 1
|
||||||
|
NoEditTriggers = 0
|
||||||
|
AscendingOrder = 0
|
||||||
|
|
||||||
|
class _MockQDialog(_MockWidget):
|
||||||
|
DialogCode = type("DialogCode", (), {"Accepted": DialogAccepted, "Rejected": 0})
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self._layout = None
|
||||||
|
def setWindowTitle(self, title: str) -> None: pass
|
||||||
|
def setWindowIcon(self, icon) -> None: pass
|
||||||
|
def exec(self) -> int: return DialogAccepted
|
||||||
|
def exec_(self) -> int: return DialogAccepted
|
||||||
|
def accept(self) -> None: pass
|
||||||
|
def reject(self) -> None: pass
|
||||||
|
def resize(self, *args, **kwargs) -> None: pass
|
||||||
|
def adjustSize(self) -> None: pass
|
||||||
|
def sizeHint(self):
|
||||||
|
return type("Size", (), {"width": lambda self=None: 200, "height": lambda self=None: 100})()
|
||||||
|
|
||||||
|
QDialog = _MockQDialog
|
||||||
|
|
||||||
|
class _MockQDialogButtonBox(_MockWidget):
|
||||||
|
StandardButton = type("StandardButton", (), {"Ok": 1, "Cancel": 2})
|
||||||
|
Ok = 1
|
||||||
|
Cancel = 2
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.accepted = type('Signal', (), {'connect': lambda s, cb: None})()
|
||||||
|
self.rejected = type('Signal', (), {'connect': lambda s, cb: None})()
|
||||||
|
|
||||||
|
QDialogButtonBox = _MockQDialogButtonBox
|
||||||
|
|
||||||
|
class _MockQListWidgetItem:
|
||||||
|
def __init__(self, text: str = ""):
|
||||||
|
self._text = text
|
||||||
|
self._selected = False
|
||||||
|
def setSelected(self, value: bool) -> None: self._selected = value
|
||||||
|
def isSelected(self) -> bool: return self._selected
|
||||||
|
|
||||||
|
QListWidgetItem = _MockQListWidgetItem
|
||||||
|
|
||||||
|
class _MockQListWidget(_MockWidget):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self._items = []
|
||||||
|
def addItem(self, item) -> None: self._items.append(item)
|
||||||
|
def item(self, index: int): return self._items[index] if 0 <= index < len(self._items) else None
|
||||||
|
def count(self) -> int: return len(self._items)
|
||||||
|
def row(self, item) -> int: return self._items.index(item) if item in self._items else -1
|
||||||
|
def selectedItems(self): return [i for i in self._items if i.isSelected()]
|
||||||
|
def setSelectionMode(self, *args, **kwargs) -> None: pass
|
||||||
|
def sizeHintForRow(self, index: int) -> int: return 20
|
||||||
|
def setFixedHeight(self, height: int) -> None: pass
|
||||||
|
def frameWidth(self) -> int: return 1
|
||||||
|
|
||||||
|
QListWidget = _MockQListWidget
|
||||||
|
|
||||||
|
class _MockQAbstractItemView:
|
||||||
|
SelectionMode = type("SelectionMode", (), {"ExtendedSelection": ExtendedSelection})
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
QIcon = _MockQIcon
|
||||||
|
|
||||||
def exec_dialog(dialog: Any) -> Any:
|
def exec_dialog(dialog: Any) -> Any:
|
||||||
return YES
|
return YES
|
||||||
# --------------------------- TEST ---------------------------
|
# --------------------------- TEST ---------------------------
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ class SettingsLogic:
|
|||||||
"amt",
|
"amt",
|
||||||
"behoerde",
|
"behoerde",
|
||||||
"landkreis_user",
|
"landkreis_user",
|
||||||
|
"landkreis_kennzahl",
|
||||||
"sachgebiet",
|
"sachgebiet",
|
||||||
"bezeichnung",
|
"bezeichnung",
|
||||||
"name",
|
"name",
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
[general]
|
||||||
|
version=26.7.2-testing
|
||||||
|
name=LNO Sachsen | Plugin Basisfunktionen
|
||||||
|
description=Plugin mit Basisfunktionen
|
||||||
|
author=Daniel Helbig, Michael Otto
|
||||||
|
homepage=https://entwicklung.flurneuordnung-sachsen.de/AG_QGIS/Plugin_SN_Basis
|
||||||
|
tracker=https://entwicklung.flurneuordnung-sachsen.de/AG_QGIS/Plugin_SN_Basis/issues
|
||||||
|
repository=https://entwicklung.flurneuordnung-sachsen.de/AG_QGIS/Plugin_SN_Basis/src/branch/testing/
|
||||||
|
QGISMaximumVersion=4.99
|
||||||
|
qgisMinimumVersion=3.40
|
||||||
|
experimental=true
|
||||||
|
deprecated=false
|
||||||
|
supportsQt6=true
|
||||||
+13
-2
@@ -30,7 +30,6 @@ from sn_basis.modules.linkpruefer import Linkpruefer
|
|||||||
from sn_basis.modules.layerpruefer import Layerpruefer
|
from sn_basis.modules.layerpruefer import Layerpruefer
|
||||||
from sn_basis.modules.stilpruefer import Stilpruefer
|
from sn_basis.modules.stilpruefer import Stilpruefer
|
||||||
from sn_basis.modules.excel_importer import ExcelImporter
|
from sn_basis.modules.excel_importer import ExcelImporter
|
||||||
from sn_plan41.modules.listenauswerter import Listenauswerter
|
|
||||||
|
|
||||||
|
|
||||||
SourceType = Literal["service", "database", "excel", "unknown"]
|
SourceType = Literal["service", "database", "excel", "unknown"]
|
||||||
@@ -286,7 +285,19 @@ class DataGrabber:
|
|||||||
}
|
}
|
||||||
source_dict["rows"].append(result_row)
|
source_dict["rows"].append(result_row)
|
||||||
|
|
||||||
# Validierung über Listenauswerter
|
# Validierung über Listenauswerter – Import erst hier (statt am Modul-
|
||||||
|
# anfang), da DataGrabber generisch für alle sn_*-Plugins nutzbar sein
|
||||||
|
# soll und nicht bei jedem Aufruf zwingend sn_plan41 installiert ist;
|
||||||
|
# nur die Excel-Quellenverarbeitung braucht Listenauswerter.
|
||||||
|
try:
|
||||||
|
from sn_plan41.modules.listenauswerter import Listenauswerter
|
||||||
|
except ImportError as exc:
|
||||||
|
raise ImportError(
|
||||||
|
"Die Verarbeitung von Excel-Quellen benötigt das Plugin "
|
||||||
|
"sn_plan41 (sn_plan41.modules.listenauswerter.Listenauswerter), "
|
||||||
|
"das hier nicht installiert ist."
|
||||||
|
) from exc
|
||||||
|
|
||||||
listenauswerter = Listenauswerter(self.pruefmanager, self.stil_pruefer or Stilpruefer())
|
listenauswerter = Listenauswerter(self.pruefmanager, self.stil_pruefer or Stilpruefer())
|
||||||
validated, validation_results = listenauswerter.validate_rows(source_dict)
|
validated, validation_results = listenauswerter.validate_rows(source_dict)
|
||||||
results.extend(validation_results)
|
results.extend(validation_results)
|
||||||
|
|||||||
+55
-12
@@ -19,7 +19,7 @@ Designprinzipien
|
|||||||
|
|
||||||
from typing import Any, Callable, Dict, List, Mapping, Optional, Tuple
|
from typing import Any, Callable, Dict, List, Mapping, Optional, Tuple
|
||||||
|
|
||||||
from urllib.parse import urlparse, parse_qsl, urlencode, urlunparse
|
from urllib.parse import urlparse, parse_qsl, urlencode, urlunparse, quote
|
||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
|
|
||||||
@@ -132,6 +132,7 @@ class Datenabruf:
|
|||||||
# außer bei WMS (WMS bleibt unverändert).
|
# außer bei WMS (WMS bleibt unverändert).
|
||||||
use_bbox = (raumfilter != "ohne") and (str(provider).upper() != "WMS")
|
use_bbox = (raumfilter != "ohne") and (str(provider).upper() != "WMS")
|
||||||
url = self._build_provider_url(link=link, provider=str(provider), bbox_geom=bbox_geom if use_bbox else None)
|
url = self._build_provider_url(link=link, provider=str(provider), bbox_geom=bbox_geom if use_bbox else None)
|
||||||
|
print(f"[Datenabruf] URL für {thema!r} ({provider}): {url}")
|
||||||
|
|
||||||
# 2b) Fachdaten abrufen
|
# 2b) Fachdaten abrufen
|
||||||
features, error_msg = self._fetch_features(
|
features, error_msg = self._fetch_features(
|
||||||
@@ -309,8 +310,11 @@ class Datenabruf:
|
|||||||
if callable(extent_to_bbox):
|
if callable(extent_to_bbox):
|
||||||
bbox_str = extent_to_bbox(extent_obj)
|
bbox_str = extent_to_bbox(extent_obj)
|
||||||
else:
|
else:
|
||||||
# Fallback: einfache xmin/ymin/xmax/ymax-Extraktion (duck-typing)
|
# Fallback: xmin/ymin/xmax/ymax-Extraktion (duck-typing)
|
||||||
if hasattr(extent_obj, "xmin") and callable(getattr(extent_obj, "xmin")):
|
# QgsRectangle verwendet xMinimum()/yMinimum()/xMaximum()/yMaximum()
|
||||||
|
if hasattr(extent_obj, "xMinimum") and callable(getattr(extent_obj, "xMinimum")):
|
||||||
|
bbox_str = f"{extent_obj.xMinimum()},{extent_obj.yMinimum()},{extent_obj.xMaximum()},{extent_obj.yMaximum()}"
|
||||||
|
elif hasattr(extent_obj, "xmin") and callable(getattr(extent_obj, "xmin")):
|
||||||
bbox_str = f"{extent_obj.xmin()},{extent_obj.ymin()},{extent_obj.xmax()},{extent_obj.ymax()}"
|
bbox_str = f"{extent_obj.xmin()},{extent_obj.ymin()},{extent_obj.xmax()},{extent_obj.ymax()}"
|
||||||
elif isinstance(extent_obj, (tuple, list)) and len(extent_obj) == 4:
|
elif isinstance(extent_obj, (tuple, list)) and len(extent_obj) == 4:
|
||||||
bbox_str = f"{extent_obj[0]},{extent_obj[1]},{extent_obj[2]},{extent_obj[3]}"
|
bbox_str = f"{extent_obj[0]},{extent_obj[1]},{extent_obj[2]},{extent_obj[3]}"
|
||||||
@@ -325,12 +329,26 @@ class Datenabruf:
|
|||||||
if provider_norm == "WFS":
|
if provider_norm == "WFS":
|
||||||
query_params.setdefault("service", "WFS")
|
query_params.setdefault("service", "WFS")
|
||||||
query_params.setdefault("request", "GetFeature")
|
query_params.setdefault("request", "GetFeature")
|
||||||
query_params.setdefault("outputFormat", "application/json")
|
query_params.setdefault("VERSION", "2.0.0")
|
||||||
if bbox_str:
|
# WFS 2.0: 'typename' → 'TYPENAMES'
|
||||||
query_params.setdefault("BBOX", bbox_str)
|
if "typename" in query_params and "TYPENAMES" not in query_params:
|
||||||
|
query_params["TYPENAMES"] = query_params.pop("typename")
|
||||||
|
# resultType=hits: nur Anzahl vom Server abfragen, kein GML-Download
|
||||||
|
query_params.setdefault("resultType", "hits")
|
||||||
|
# CRS in OGC-URN-Format (wie QGIS Datenquellenverwaltung)
|
||||||
|
srsname_urn: Optional[str] = None
|
||||||
if crs_authid:
|
if crs_authid:
|
||||||
query_params.setdefault("SRSNAME", crs_authid)
|
if crs_authid.upper().startswith("EPSG:"):
|
||||||
new_query = urlencode(query_params, doseq=True)
|
srsname_urn = f"urn:ogc:def:crs:EPSG::{crs_authid.split(':', 1)[1]}"
|
||||||
|
else:
|
||||||
|
srsname_urn = crs_authid
|
||||||
|
if bbox_str:
|
||||||
|
# 5. Komponente: CRS-URN anhängen (Standard für WFS 2.0)
|
||||||
|
bbox_with_crs = f"{bbox_str},{srsname_urn}" if srsname_urn else bbox_str
|
||||||
|
query_params.setdefault("BBOX", bbox_with_crs)
|
||||||
|
if srsname_urn:
|
||||||
|
query_params.setdefault("SRSNAME", srsname_urn)
|
||||||
|
new_query = urlencode(query_params, doseq=True, quote_via=quote, safe=':/')
|
||||||
rebuilt = parsed._replace(query=new_query)
|
rebuilt = parsed._replace(query=new_query)
|
||||||
return urlunparse(rebuilt)
|
return urlunparse(rebuilt)
|
||||||
|
|
||||||
@@ -389,14 +407,14 @@ class Datenabruf:
|
|||||||
query_params.setdefault("inSR", srid)
|
query_params.setdefault("inSR", srid)
|
||||||
query_params.setdefault("outSR", srid)
|
query_params.setdefault("outSR", srid)
|
||||||
|
|
||||||
new_query = urlencode(query_params, doseq=True)
|
new_query = urlencode(query_params, doseq=True, quote_via=quote, safe=':/')
|
||||||
rebuilt = parsed_rest._replace(query=new_query)
|
rebuilt = parsed_rest._replace(query=new_query)
|
||||||
return urlunparse(rebuilt)
|
return urlunparse(rebuilt)
|
||||||
|
|
||||||
# Default: generischer bbox-Parameter (nur wenn vorhanden)
|
# Default: generischer bbox-Parameter (nur wenn vorhanden)
|
||||||
if bbox_str:
|
if bbox_str:
|
||||||
query_params.setdefault("bbox", bbox_str)
|
query_params.setdefault("bbox", bbox_str)
|
||||||
new_query = urlencode(query_params, doseq=True)
|
new_query = urlencode(query_params, doseq=True, quote_via=quote, safe=':/')
|
||||||
rebuilt = parsed._replace(query=new_query)
|
rebuilt = parsed._replace(query=new_query)
|
||||||
return urlunparse(rebuilt)
|
return urlunparse(rebuilt)
|
||||||
|
|
||||||
@@ -573,7 +591,33 @@ class Datenabruf:
|
|||||||
# Sonst: gib das gesamte JSON als einzelnes Objekt zurück
|
# Sonst: gib das gesamte JSON als einzelnes Objekt zurück
|
||||||
return [parsed], None
|
return [parsed], None
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
# Nicht-JSON-Antwort (z. B. GML). Wenn QGIS verfügbar, versuche GML via temporärer Datei + OGR
|
# Nicht-JSON-Antwort (GML/XML). Für WFS resultType=hits: Anzahl aus XML lesen.
|
||||||
|
if prov == "WFS":
|
||||||
|
try:
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
root = ET.fromstring(response_text)
|
||||||
|
# WFS 2.0: numberOfMatched; WFS 1.x: numberOfFeatures
|
||||||
|
n_str = (
|
||||||
|
root.get("numberOfMatched")
|
||||||
|
or root.get("numberMatched")
|
||||||
|
or root.get("numberOfFeatures")
|
||||||
|
or root.get("numberOfReturned")
|
||||||
|
)
|
||||||
|
if n_str is not None:
|
||||||
|
try:
|
||||||
|
count = int(n_str)
|
||||||
|
return [{}] * count, None
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
# Fallback: wfs:member-Elemente zählen
|
||||||
|
members = [
|
||||||
|
c for c in root
|
||||||
|
if c.tag.lower().endswith(("member", "featuremember"))
|
||||||
|
]
|
||||||
|
return [{}] * len(members), None
|
||||||
|
except Exception as exc:
|
||||||
|
return [], f"WFS-XML-Antwort konnte nicht geparst werden: {exc}"
|
||||||
|
# Nicht-WFS, nicht-JSON: GML via temporärer Datei + OGR
|
||||||
if getattr(qgiscore, "QGIS_AVAILABLE", False):
|
if getattr(qgiscore, "QGIS_AVAILABLE", False):
|
||||||
try:
|
try:
|
||||||
import tempfile
|
import tempfile
|
||||||
@@ -588,5 +632,4 @@ class Datenabruf:
|
|||||||
return [], "GML-Antwort konnte nicht als Layer geladen werden"
|
return [], "GML-Antwort konnte nicht als Layer geladen werden"
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
return [], f"Fehler beim Parsen von GML: {exc}"
|
return [], f"Fehler beim Parsen von GML: {exc}"
|
||||||
# Wenn alles fehlschlägt:
|
|
||||||
return [], "Antwort konnte nicht als JSON oder GML geparst werden"
|
return [], "Antwort konnte nicht als JSON oder GML geparst werden"
|
||||||
|
|||||||
+27
-1
@@ -122,7 +122,33 @@ class LayerLoader:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
if provider_lower == "wfs":
|
if provider_lower == "wfs":
|
||||||
uri = link if link.strip().lower().startswith("url=") else f"url={link}"
|
raw_link = link.strip()
|
||||||
|
if raw_link.lower().startswith("url="):
|
||||||
|
raw_link = raw_link[4:]
|
||||||
|
# QGIS WFS-Provider erwartet die Basis-URL und 'typename' als
|
||||||
|
# separaten URI-Parameter. GetFeature-spezifische Parameter
|
||||||
|
# (request, outputFormat, resultType, BBOX, …) werden entfernt.
|
||||||
|
try:
|
||||||
|
from urllib.parse import urlparse as _up, parse_qsl as _pqsl, urlunparse as _uu, urlencode as _ue
|
||||||
|
_parsed = _up(raw_link)
|
||||||
|
_params = dict(_pqsl(_parsed.query, keep_blank_values=True))
|
||||||
|
_typename = (
|
||||||
|
_params.pop("typename", None)
|
||||||
|
or _params.pop("TYPENAME", None)
|
||||||
|
or _params.pop("TYPENAMES", None)
|
||||||
|
)
|
||||||
|
for _k in ("request", "REQUEST", "outputFormat", "OUTPUTFORMAT",
|
||||||
|
"resultType", "RESULTTYPE", "BBOX", "bbox",
|
||||||
|
"STARTINDEX", "COUNT"):
|
||||||
|
_params.pop(_k, None)
|
||||||
|
_params.setdefault("service", "WFS")
|
||||||
|
_params.setdefault("version", "2.0.0")
|
||||||
|
_base_url = _uu(_parsed._replace(query=_ue(_params)))
|
||||||
|
uri = f"url={_base_url}"
|
||||||
|
if _typename:
|
||||||
|
uri += f" typename={_typename}"
|
||||||
|
except Exception:
|
||||||
|
uri = link if link.strip().lower().startswith("url=") else f"url={link}"
|
||||||
layer = QgsVectorLayer(uri, thema, "WFS")
|
layer = QgsVectorLayer(uri, thema, "WFS")
|
||||||
elif provider_lower == "wms":
|
elif provider_lower == "wms":
|
||||||
uri = link if link.strip().lower().startswith("url=") else f"url={link}"
|
uri = link if link.strip().lower().startswith("url=") else f"url={link}"
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
"""sn_basis/tests/test_laenderkennung.py
|
||||||
|
|
||||||
|
Unit-Tests für :mod:`sn_basis.functions.laenderkennung`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from sn_basis.functions.laenderkennung import get_laenderkennung, LAENDERKENNUNGEN
|
||||||
|
|
||||||
|
|
||||||
|
class TestGetLaenderkennung(unittest.TestCase):
|
||||||
|
def test_bekannter_praefix_gibt_kennung_zurueck(self):
|
||||||
|
self.assertEqual(get_laenderkennung("sn"), "14")
|
||||||
|
|
||||||
|
def test_praefix_gross_klein_unabhaengig(self):
|
||||||
|
self.assertEqual(get_laenderkennung("SN"), "14")
|
||||||
|
|
||||||
|
def test_unbekannter_praefix_wirft_keyerror(self):
|
||||||
|
with self.assertRaises(KeyError):
|
||||||
|
get_laenderkennung("xx")
|
||||||
|
|
||||||
|
def test_ohne_argument_wird_aus_aufrufendem_modul_abgeleitet(self):
|
||||||
|
# __name__ dieses Testmoduls beginnt mit "sn_basis" -> Präfix "sn"
|
||||||
|
self.assertEqual(get_laenderkennung(), "14")
|
||||||
|
|
||||||
|
def test_mapping_enthaelt_sachsen(self):
|
||||||
|
self.assertEqual(LAENDERKENNUNGEN.get("sn"), "14")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
+42
-11
@@ -1,5 +1,5 @@
|
|||||||
"""
|
"""
|
||||||
sn_basis/ui/tabs/settings_tab.py – Einstellungen-Tab für Projekteinformationen.
|
sn_basis/ui/tabs/settings_tab.py – Einstellungen-Tab für Projektinformationen.
|
||||||
Verwaltet benutzerspezifische und projektspezifische Einstellungen.
|
Verwaltet benutzerspezifische und projektspezifische Einstellungen.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -8,28 +8,33 @@ from sn_basis.functions.qt_wrapper import (
|
|||||||
QGridLayout,
|
QGridLayout,
|
||||||
QLabel,
|
QLabel,
|
||||||
QLineEdit,
|
QLineEdit,
|
||||||
|
QComboBox,
|
||||||
QGroupBox,
|
QGroupBox,
|
||||||
QVBoxLayout,
|
QVBoxLayout,
|
||||||
QPushButton,
|
QPushButton,
|
||||||
)
|
)
|
||||||
from sn_basis.functions.settings_logic import SettingsLogic
|
from sn_basis.functions.settings_logic import SettingsLogic
|
||||||
|
from sn_basis.functions.landkreise import LANDKREISE
|
||||||
|
|
||||||
|
#: Feldschlüssel, die als Landkreis-Dropdown (statt Freitext) dargestellt werden.
|
||||||
|
_LANDKREIS_DROPDOWN_FELDER = {"landkreis_user"}
|
||||||
|
|
||||||
|
|
||||||
class SettingsTab(QWidget):
|
class SettingsTab(QWidget):
|
||||||
"""
|
"""
|
||||||
Tab-Widget für die Verwaltung von Projekt- und Benutzereinstellungen.
|
Tab-Widget für die Verwaltung von Projekt- und Benutzereinstellungen.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
tab_title: Anzeigetitel des Tabs ("Projekteigenschaften")
|
tab_title: Anzeigetitel des Tabs ("Projekteigenschaften")
|
||||||
logic: SettingsLogic-Instanz für persistente Einstellungen
|
logic: SettingsLogic-Instanz für persistente Einstellungen
|
||||||
"""
|
"""
|
||||||
|
|
||||||
tab_title = "Projekteigenschaften"
|
tab_title = "Projekteigenschaften"
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
"""
|
"""
|
||||||
Initialisiert den Settings-Tab mit Eingabefeldern.
|
Initialisiert den Settings-Tab mit Eingabefeldern.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
parent: Übergeordnetes Widget (Standard: None)
|
parent: Übergeordnetes Widget (Standard: None)
|
||||||
"""
|
"""
|
||||||
@@ -64,7 +69,7 @@ class SettingsTab(QWidget):
|
|||||||
self.user_inputs = {}
|
self.user_inputs = {}
|
||||||
|
|
||||||
for row, (key, label) in enumerate(self.user_fields.items()):
|
for row, (key, label) in enumerate(self.user_fields.items()):
|
||||||
input_widget = QLineEdit()
|
input_widget = self._build_input(key)
|
||||||
self.user_inputs[key] = input_widget
|
self.user_inputs[key] = input_widget
|
||||||
|
|
||||||
user_layout.addWidget(QLabel(label), row, 0)
|
user_layout.addWidget(QLabel(label), row, 0)
|
||||||
@@ -80,7 +85,7 @@ class SettingsTab(QWidget):
|
|||||||
self.project_inputs = {}
|
self.project_inputs = {}
|
||||||
|
|
||||||
for row, (key, label) in enumerate(self.project_fields.items()):
|
for row, (key, label) in enumerate(self.project_fields.items()):
|
||||||
input_widget = QLineEdit()
|
input_widget = self._build_input(key)
|
||||||
self.project_inputs[key] = input_widget
|
self.project_inputs[key] = input_widget
|
||||||
|
|
||||||
project_layout.addWidget(QLabel(label), row, 0)
|
project_layout.addWidget(QLabel(label), row, 0)
|
||||||
@@ -107,14 +112,35 @@ class SettingsTab(QWidget):
|
|||||||
# Daten laden
|
# Daten laden
|
||||||
self.load_data()
|
self.load_data()
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# Eingabewidget je Feld erzeugen
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
def _build_input(self, key: str):
|
||||||
|
if key in _LANDKREIS_DROPDOWN_FELDER:
|
||||||
|
combo = QComboBox()
|
||||||
|
combo.addItem("", None) # Platzhalter: keine Vorbelegung bei leerem Projekt
|
||||||
|
for kennzahl, name in LANDKREISE:
|
||||||
|
combo.addItem(name, kennzahl)
|
||||||
|
return combo
|
||||||
|
return QLineEdit()
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
# Speichern
|
# Speichern
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
def save_data(self):
|
def save_data(self):
|
||||||
fields = {
|
fields = {}
|
||||||
key: widget.text()
|
for key, widget in {**self.user_inputs, **self.project_inputs}.items():
|
||||||
for key, widget in {**self.user_inputs, **self.project_inputs}.items()
|
if isinstance(widget, QComboBox):
|
||||||
}
|
fields[key] = widget.currentText()
|
||||||
|
else:
|
||||||
|
fields[key] = widget.text()
|
||||||
|
|
||||||
|
# Landkreiskennzahl aus der Dropdown-Auswahl zusätzlich als eigene
|
||||||
|
# Variable bereitstellen (siehe landkreise.py).
|
||||||
|
landkreis_widget = self.user_inputs.get("landkreis_user")
|
||||||
|
if isinstance(landkreis_widget, QComboBox):
|
||||||
|
fields["landkreis_kennzahl"] = landkreis_widget.currentData() or ""
|
||||||
|
|
||||||
self.logic.save(fields)
|
self.logic.save(fields)
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
@@ -123,4 +149,9 @@ class SettingsTab(QWidget):
|
|||||||
def load_data(self):
|
def load_data(self):
|
||||||
data = self.logic.load()
|
data = self.logic.load()
|
||||||
for key, widget in {**self.user_inputs, **self.project_inputs}.items():
|
for key, widget in {**self.user_inputs, **self.project_inputs}.items():
|
||||||
widget.setText(data.get(key, ""))
|
if isinstance(widget, QComboBox):
|
||||||
|
gespeicherter_name = data.get(key, "")
|
||||||
|
index = widget.findText(gespeicherter_name)
|
||||||
|
widget.setCurrentIndex(index if index >= 0 else 0)
|
||||||
|
else:
|
||||||
|
widget.setText(data.get(key, ""))
|
||||||
|
|||||||
Reference in New Issue
Block a user