Diensteabruf integriert
This commit is contained in:
@@ -5,7 +5,7 @@ from typing import Any
|
|||||||
from typing import Literal, Optional
|
from typing import Literal, Optional
|
||||||
from sn_basis.functions.qt_wrapper import (
|
from sn_basis.functions.qt_wrapper import (
|
||||||
QMessageBox, YES, NO, CANCEL, QT_VERSION, exec_dialog, ICON_QUESTION,
|
QMessageBox, YES, NO, CANCEL, QT_VERSION, exec_dialog, ICON_QUESTION,
|
||||||
|
QProgressDialog, QCoreApplication, Qt,
|
||||||
)
|
)
|
||||||
|
|
||||||
def ask_yes_no(
|
def ask_yes_no(
|
||||||
@@ -82,3 +82,101 @@ def ask_overwrite_append_cancel_custom(
|
|||||||
return "append"
|
return "append"
|
||||||
else: # cancel_btn
|
else: # cancel_btn
|
||||||
return "cancel"
|
return "cancel"
|
||||||
|
|
||||||
|
|
||||||
|
class ProgressDialog:
|
||||||
|
def __init__(self, total: int, title: str = "Fortschritt", label: str = "Verarbeite..."):
|
||||||
|
self.total = max(total, 1)
|
||||||
|
self._canceled = False
|
||||||
|
|
||||||
|
if QT_VERSION == 0:
|
||||||
|
self.value = 0
|
||||||
|
self.label = label
|
||||||
|
self.title = title
|
||||||
|
return
|
||||||
|
|
||||||
|
self._dlg = QProgressDialog(label, "Abbrechen", 0, self.total)
|
||||||
|
self._dlg.setWindowTitle(title)
|
||||||
|
|
||||||
|
# Qt5 vs Qt6: WindowModality-Enum unterschiedlich verfügbar
|
||||||
|
modality = None
|
||||||
|
if hasattr(Qt, "WindowModality"):
|
||||||
|
try:
|
||||||
|
modality = Qt.WindowModality.WindowModal
|
||||||
|
except Exception:
|
||||||
|
modality = None
|
||||||
|
if modality is None and hasattr(Qt, "WindowModal"):
|
||||||
|
modality = Qt.WindowModal
|
||||||
|
if modality is not None:
|
||||||
|
try:
|
||||||
|
self._dlg.setWindowModality(modality)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
self._dlg.setMinimumDuration(0)
|
||||||
|
self._dlg.setAutoClose(False)
|
||||||
|
self._dlg.setAutoReset(False)
|
||||||
|
self._dlg.setValue(0)
|
||||||
|
|
||||||
|
def on_cancel():
|
||||||
|
if self._dlg and self._dlg.value() >= self.total:
|
||||||
|
# OK-Button am Ende
|
||||||
|
self._dlg.close()
|
||||||
|
return
|
||||||
|
self._canceled = True
|
||||||
|
self._dlg.close()
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._dlg.canceled.connect(on_cancel)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def set_total(self, total: int) -> None:
|
||||||
|
self.total = max(total, 1)
|
||||||
|
if QT_VERSION == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
if self._dlg is not None:
|
||||||
|
self._dlg.setMaximum(self.total)
|
||||||
|
|
||||||
|
def set_value(self, value: int) -> None:
|
||||||
|
if QT_VERSION == 0:
|
||||||
|
self.value = value
|
||||||
|
return
|
||||||
|
|
||||||
|
if self._dlg is not None:
|
||||||
|
self._dlg.setValue(min(value, self.total))
|
||||||
|
if value >= self.total:
|
||||||
|
self._dlg.setLabelText("Fertig. Klicken Sie auf OK, um das Fenster zu schließen.")
|
||||||
|
self._dlg.setCancelButtonText("OK")
|
||||||
|
QCoreApplication.processEvents()
|
||||||
|
|
||||||
|
def set_label(self, text: str) -> None:
|
||||||
|
if QT_VERSION == 0:
|
||||||
|
self.label = text
|
||||||
|
return
|
||||||
|
|
||||||
|
if self._dlg is not None:
|
||||||
|
self._dlg.setLabelText(text)
|
||||||
|
QCoreApplication.processEvents()
|
||||||
|
|
||||||
|
def is_canceled(self) -> bool:
|
||||||
|
if QT_VERSION == 0:
|
||||||
|
return self._canceled
|
||||||
|
|
||||||
|
if self._dlg is not None:
|
||||||
|
return self._canceled or self._dlg.wasCanceled()
|
||||||
|
|
||||||
|
return self._canceled
|
||||||
|
|
||||||
|
def close(self) -> None:
|
||||||
|
if QT_VERSION == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
if self._dlg is not None:
|
||||||
|
self._dlg.close()
|
||||||
|
|
||||||
|
|
||||||
|
def create_progress_dialog(total: int, title: str = "Fortschritt", label: str = "Verarbeite...") -> ProgressDialog:
|
||||||
|
return ProgressDialog(total, title, label)
|
||||||
|
|
||||||
|
|||||||
@@ -57,6 +57,22 @@ def get_home_dir() -> Path:
|
|||||||
return Path.home()
|
return Path.home()
|
||||||
|
|
||||||
|
|
||||||
|
def is_absolute_path(path: _PathLike) -> bool:
|
||||||
|
"""Prüft, ob ein Pfad absolut ist."""
|
||||||
|
try:
|
||||||
|
return Path(path).is_absolute()
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def basename(path: _PathLike) -> str:
|
||||||
|
"""Gibt den finalen Namen des Pfades zurück (Dateiname oder Ordner)."""
|
||||||
|
try:
|
||||||
|
return Path(path).name
|
||||||
|
except Exception:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
# Dateisystem-Eigenschaften
|
# Dateisystem-Eigenschaften
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
@@ -75,3 +91,11 @@ def is_case_sensitive_fs() -> bool:
|
|||||||
|
|
||||||
# Linux praktisch immer case-sensitiv
|
# Linux praktisch immer case-sensitiv
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def path_suffix(path: _PathLike) -> str:
|
||||||
|
"""Gibt die Dateiendung eines Pfades zurück (inklusive Punkt)."""
|
||||||
|
try:
|
||||||
|
return Path(path).suffix
|
||||||
|
except Exception:
|
||||||
|
return ""
|
||||||
|
|||||||
@@ -20,6 +20,12 @@ QgsNetworkAccessManager: Type[Any]
|
|||||||
Qgis: Type[Any]
|
Qgis: Type[Any]
|
||||||
QgsMapLayerProxyModel: Type[Any]
|
QgsMapLayerProxyModel: Type[Any]
|
||||||
QgsVectorFileWriter: Type[Any] # neu: Schreib-API
|
QgsVectorFileWriter: Type[Any] # neu: Schreib-API
|
||||||
|
QgsFeature: Type[Any]
|
||||||
|
QgsField: Type[Any]
|
||||||
|
QgsGeometry: Type[Any]
|
||||||
|
QgsFeatureRequest: Type[Any]
|
||||||
|
QgsCoordinateTransform: Type[Any]
|
||||||
|
QgsCoordinateReferenceSystem: Type[Any]
|
||||||
|
|
||||||
QGIS_AVAILABLE = False
|
QGIS_AVAILABLE = False
|
||||||
|
|
||||||
@@ -39,6 +45,9 @@ try:
|
|||||||
QgsFeature as _QgsFeature,
|
QgsFeature as _QgsFeature,
|
||||||
QgsField as _QgsField,
|
QgsField as _QgsField,
|
||||||
QgsGeometry as _QgsGeometry,
|
QgsGeometry as _QgsGeometry,
|
||||||
|
QgsFeatureRequest as _QgsFeatureRequest,
|
||||||
|
QgsCoordinateTransform as _QgsCoordinateTransform,
|
||||||
|
QgsCoordinateReferenceSystem as _QgsCoordinateReferenceSystem,
|
||||||
)
|
)
|
||||||
|
|
||||||
QgsProject = _QgsProject
|
QgsProject = _QgsProject
|
||||||
@@ -51,6 +60,9 @@ try:
|
|||||||
QgsFeature = _QgsFeature
|
QgsFeature = _QgsFeature
|
||||||
QgsField = _QgsField
|
QgsField = _QgsField
|
||||||
QgsGeometry = _QgsGeometry
|
QgsGeometry = _QgsGeometry
|
||||||
|
QgsFeatureRequest = _QgsFeatureRequest
|
||||||
|
QgsCoordinateTransform = _QgsCoordinateTransform
|
||||||
|
QgsCoordinateReferenceSystem = _QgsCoordinateReferenceSystem
|
||||||
|
|
||||||
QGIS_AVAILABLE = True
|
QGIS_AVAILABLE = True
|
||||||
|
|
||||||
@@ -122,6 +134,30 @@ except Exception:
|
|||||||
|
|
||||||
QgsRasterLayer = _MockQgsRasterLayer
|
QgsRasterLayer = _MockQgsRasterLayer
|
||||||
|
|
||||||
|
class _MockQgsFeatureRequest:
|
||||||
|
def __init__(self):
|
||||||
|
self._filter_rect = None
|
||||||
|
|
||||||
|
def setFilterRect(self, rect):
|
||||||
|
self._filter_rect = rect
|
||||||
|
return self
|
||||||
|
|
||||||
|
QgsFeatureRequest = _MockQgsFeatureRequest
|
||||||
|
|
||||||
|
class _MockQgsCoordinateTransform:
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def transformBoundingBox(self, rect):
|
||||||
|
return rect
|
||||||
|
|
||||||
|
class _MockQgsCoordinateReferenceSystem:
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
QgsCoordinateTransform = _MockQgsCoordinateTransform
|
||||||
|
QgsCoordinateReferenceSystem = _MockQgsCoordinateReferenceSystem
|
||||||
|
|
||||||
QgsNetworkAccessManager = _MockQgsNetworkAccessManager
|
QgsNetworkAccessManager = _MockQgsNetworkAccessManager
|
||||||
|
|
||||||
class _MockQgis:
|
class _MockQgis:
|
||||||
|
|||||||
@@ -18,7 +18,9 @@ QVariant: Type[Any] = object
|
|||||||
QDockWidget: Type[Any] = object
|
QDockWidget: Type[Any] = object
|
||||||
QMessageBox: Type[Any] = object
|
QMessageBox: Type[Any] = object
|
||||||
QFileDialog: Type[Any] = object
|
QFileDialog: Type[Any] = object
|
||||||
|
QProgressDialog: Type[Any] = object
|
||||||
QEventLoop: Type[Any] = object
|
QEventLoop: Type[Any] = object
|
||||||
|
QTimer: Type[Any] = object
|
||||||
QUrl: Type[Any] = object
|
QUrl: Type[Any] = object
|
||||||
QNetworkRequest: Type[Any] = object
|
QNetworkRequest: Type[Any] = object
|
||||||
QNetworkReply: Type[Any] = object
|
QNetworkReply: Type[Any] = object
|
||||||
@@ -66,6 +68,7 @@ try:
|
|||||||
from qgis.PyQt.QtWidgets import (
|
from qgis.PyQt.QtWidgets import (
|
||||||
QMessageBox as _QMessageBox,
|
QMessageBox as _QMessageBox,
|
||||||
QFileDialog as _QFileDialog,
|
QFileDialog as _QFileDialog,
|
||||||
|
QProgressDialog as _QProgressDialog,
|
||||||
QWidget as _QWidget,
|
QWidget as _QWidget,
|
||||||
QGridLayout as _QGridLayout,
|
QGridLayout as _QGridLayout,
|
||||||
QLabel as _QLabel,
|
QLabel as _QLabel,
|
||||||
@@ -86,6 +89,7 @@ try:
|
|||||||
)
|
)
|
||||||
from qgis.PyQt.QtCore import (
|
from qgis.PyQt.QtCore import (
|
||||||
QEventLoop as _QEventLoop,
|
QEventLoop as _QEventLoop,
|
||||||
|
QTimer as _QTimer,
|
||||||
QUrl as _QUrl,
|
QUrl as _QUrl,
|
||||||
QCoreApplication as _QCoreApplication,
|
QCoreApplication as _QCoreApplication,
|
||||||
Qt as _Qt,
|
Qt as _Qt,
|
||||||
@@ -100,7 +104,10 @@ try:
|
|||||||
QT_VERSION = 6
|
QT_VERSION = 6
|
||||||
QMessageBox = _QMessageBox
|
QMessageBox = _QMessageBox
|
||||||
QFileDialog = _QFileDialog
|
QFileDialog = _QFileDialog
|
||||||
|
QProgressDialog = _QProgressDialog
|
||||||
|
QProgressDialog = _QProgressDialog
|
||||||
QEventLoop = _QEventLoop
|
QEventLoop = _QEventLoop
|
||||||
|
QTimer = _QTimer
|
||||||
QUrl = _QUrl
|
QUrl = _QUrl
|
||||||
QNetworkRequest = _QNetworkRequest
|
QNetworkRequest = _QNetworkRequest
|
||||||
QNetworkReply = _QNetworkReply
|
QNetworkReply = _QNetworkReply
|
||||||
@@ -176,6 +183,7 @@ except (ImportError, AttributeError):
|
|||||||
)
|
)
|
||||||
from PyQt5.QtCore import (
|
from PyQt5.QtCore import (
|
||||||
QEventLoop as _QEventLoop,
|
QEventLoop as _QEventLoop,
|
||||||
|
QTimer as _QTimer,
|
||||||
QUrl as _QUrl,
|
QUrl as _QUrl,
|
||||||
QCoreApplication as _QCoreApplication,
|
QCoreApplication as _QCoreApplication,
|
||||||
Qt as _Qt,
|
Qt as _Qt,
|
||||||
@@ -191,6 +199,7 @@ except (ImportError, AttributeError):
|
|||||||
QMessageBox = _QMessageBox
|
QMessageBox = _QMessageBox
|
||||||
QFileDialog = _QFileDialog
|
QFileDialog = _QFileDialog
|
||||||
QEventLoop = _QEventLoop
|
QEventLoop = _QEventLoop
|
||||||
|
QTimer = _QTimer
|
||||||
QUrl = _QUrl
|
QUrl = _QUrl
|
||||||
QNetworkRequest = _QNetworkRequest
|
QNetworkRequest = _QNetworkRequest
|
||||||
QNetworkReply = _QNetworkReply
|
QNetworkReply = _QNetworkReply
|
||||||
@@ -291,6 +300,17 @@ except (ImportError, AttributeError):
|
|||||||
|
|
||||||
QEventLoop = _MockQEventLoop
|
QEventLoop = _MockQEventLoop
|
||||||
|
|
||||||
|
class _MockQTimer:
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.timeout = type('Signal', (), {
|
||||||
|
'connect': lambda s, cb: None,
|
||||||
|
})()
|
||||||
|
def setSingleShot(self, v: bool) -> None: pass
|
||||||
|
def start(self, ms: int) -> None: pass
|
||||||
|
def stop(self) -> None: pass
|
||||||
|
|
||||||
|
QTimer = _MockQTimer
|
||||||
|
|
||||||
class _MockQUrl(str):
|
class _MockQUrl(str):
|
||||||
def isValid(self) -> bool: return True
|
def isValid(self) -> bool: return True
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from pathlib import Path
|
|||||||
from typing import Union
|
from typing import Union
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
from sn_basis.functions.os_wrapper import is_absolute_path, basename
|
||||||
|
|
||||||
_PathLike = Union[str, Path]
|
_PathLike = Union[str, Path]
|
||||||
|
|
||||||
|
|||||||
@@ -17,9 +17,11 @@ ausschließlich über den ``Pruefmanager`` im aufrufenden Kontext (UI / Pipeline
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from pathlib import Path
|
import re
|
||||||
from typing import Any, Dict, List, Mapping, Optional, Tuple, Literal
|
from typing import Any, Dict, List, Mapping, Optional, Tuple, Literal
|
||||||
|
|
||||||
|
from sn_basis.functions.os_wrapper import basename, path_suffix
|
||||||
|
|
||||||
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
|
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
|
||||||
from sn_basis.modules.Pruefmanager import Pruefmanager
|
from sn_basis.modules.Pruefmanager import Pruefmanager
|
||||||
|
|
||||||
@@ -28,6 +30,7 @@ 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"]
|
||||||
@@ -53,9 +56,9 @@ class DataGrabber:
|
|||||||
) -> None:
|
) -> None:
|
||||||
self.pruefmanager = pruefmanager
|
self.pruefmanager = pruefmanager
|
||||||
self._datei_pruefer_cls = datei_pruefer_cls
|
self._datei_pruefer_cls = datei_pruefer_cls
|
||||||
self.link_pruefer = link_pruefer
|
self.link_pruefer = link_pruefer or Linkpruefer()
|
||||||
self.layer_pruefer = layer_pruefer
|
self.layer_pruefer = layer_pruefer or Layerpruefer()
|
||||||
self.stil_pruefer = stil_pruefer
|
self.stil_pruefer = stil_pruefer or Stilpruefer()
|
||||||
self._excel_importer_cls = excel_importer_cls
|
self._excel_importer_cls = excel_importer_cls
|
||||||
|
|
||||||
self._source: Optional[str] = None
|
self._source: Optional[str] = None
|
||||||
@@ -86,8 +89,8 @@ class DataGrabber:
|
|||||||
datei_ergebnis = dateipruefer.pruefe()
|
datei_ergebnis = dateipruefer.pruefe()
|
||||||
|
|
||||||
if datei_ergebnis.ok:
|
if datei_ergebnis.ok:
|
||||||
pfad = Path(datei_ergebnis.kontext)
|
suffix = path_suffix(datei_ergebnis.kontext).lower()
|
||||||
suffix = pfad.suffix.lower()
|
print(f"[DataGrabber] Debug: analyze_source_type source={quelle} -> suffix={suffix}")
|
||||||
|
|
||||||
if suffix == ".xlsx":
|
if suffix == ".xlsx":
|
||||||
return "excel", datei_ergebnis
|
return "excel", datei_ergebnis
|
||||||
@@ -121,6 +124,7 @@ class DataGrabber:
|
|||||||
"""
|
"""
|
||||||
self.set_source(source)
|
self.set_source(source)
|
||||||
source_type, source_result = self.analyze_source_type(source)
|
source_type, source_result = self.analyze_source_type(source)
|
||||||
|
print(f"[DataGrabber] Debug: run source={source} -> source_type={source_type}")
|
||||||
|
|
||||||
source_dict: SourceDict = {}
|
source_dict: SourceDict = {}
|
||||||
partial_results: List[pruef_ergebnis] = []
|
partial_results: List[pruef_ergebnis] = []
|
||||||
@@ -143,9 +147,150 @@ class DataGrabber:
|
|||||||
def _process_excel_source(
|
def _process_excel_source(
|
||||||
self, filepath: str
|
self, filepath: str
|
||||||
) -> Tuple[SourceDict, List[pruef_ergebnis]]:
|
) -> Tuple[SourceDict, List[pruef_ergebnis]]:
|
||||||
source_dict: SourceDict = {}
|
source_dict: SourceDict = {"rows": []}
|
||||||
results: List[pruef_ergebnis] = []
|
results: List[pruef_ergebnis] = []
|
||||||
return source_dict, results
|
|
||||||
|
rows = ExcelImporter(filepath, self.pruefmanager).import_xlsx()
|
||||||
|
print(f"[DataGrabber] Debug: Excel-Linkliste geladen: {filepath}")
|
||||||
|
print(f"[DataGrabber] Debug: raw rows count: {len(rows)}")
|
||||||
|
if rows:
|
||||||
|
first = rows[:min(5, len(rows))]
|
||||||
|
print(f"[DataGrabber] Debug: first rows: {first}")
|
||||||
|
|
||||||
|
if not rows:
|
||||||
|
return source_dict, results
|
||||||
|
|
||||||
|
required_keys = {"ident", "gruppe", "kartenebene", "inhalt", "link", "provider", "stildatei"}
|
||||||
|
|
||||||
|
def extract_url(raw_link: str, provider: str) -> str:
|
||||||
|
if not raw_link:
|
||||||
|
return ""
|
||||||
|
if not isinstance(raw_link, str):
|
||||||
|
return str(raw_link)
|
||||||
|
|
||||||
|
if provider == "wfs":
|
||||||
|
url_match = re.search(r"url\s*=\s*['\"]([^'\"]+)['\"]", raw_link, re.IGNORECASE)
|
||||||
|
type_match = re.search(r"typename\s*=\s*['\"]([^'\"]+)['\"]", raw_link, re.IGNORECASE)
|
||||||
|
if url_match:
|
||||||
|
url = url_match.group(1).strip()
|
||||||
|
if type_match:
|
||||||
|
typename = type_match.group(1).strip()
|
||||||
|
separator = "&" if "?" in url else "?"
|
||||||
|
return f"url={url}{separator}service=WFS&request=GetFeature&typename={typename}"
|
||||||
|
return f"url={url}"
|
||||||
|
|
||||||
|
if provider == "wms":
|
||||||
|
# falls WMS-URL als url='...' vorliegt
|
||||||
|
match = re.search(r"url\s*=\s*['\"]([^'\"]+)['\"]", raw_link, re.IGNORECASE)
|
||||||
|
if match:
|
||||||
|
return match.group(1).strip()
|
||||||
|
|
||||||
|
if provider == "rest":
|
||||||
|
# REST/ArcGIS-Server: direkt nutzen
|
||||||
|
match = re.search(r"url\s*=\s*['\"]([^'\"]+)['\"]", raw_link, re.IGNORECASE)
|
||||||
|
if match:
|
||||||
|
return match.group(1).strip()
|
||||||
|
|
||||||
|
# allgemeines Rückfallverhalten
|
||||||
|
match = re.search(r"url\s*=\s*['\"]([^'\"]+)['\"]", raw_link, re.IGNORECASE)
|
||||||
|
if match:
|
||||||
|
return match.group(1).strip()
|
||||||
|
return raw_link.strip()
|
||||||
|
|
||||||
|
for row_index, raw_row in enumerate(rows, start=2):
|
||||||
|
if not isinstance(raw_row, Mapping):
|
||||||
|
pe = pruef_ergebnis(
|
||||||
|
ok=False,
|
||||||
|
meldung="Linklisten-Zeile ist nicht als Dictionary formatiert.",
|
||||||
|
aktion="ungueltige_zeile",
|
||||||
|
kontext={"zeile": row_index, "wert": raw_row},
|
||||||
|
)
|
||||||
|
results.append(self.pruefmanager.verarbeite(pe))
|
||||||
|
continue
|
||||||
|
|
||||||
|
normalized = {str(k).strip().lower(): v for k, v in raw_row.items() if k is not None}
|
||||||
|
if not required_keys.issubset(normalized.keys()):
|
||||||
|
missing = required_keys.difference(normalized.keys())
|
||||||
|
pe = pruef_ergebnis(
|
||||||
|
ok=False,
|
||||||
|
meldung=f"Linkliste fehlt erforderliche Spalten: {', '.join(sorted(missing))}",
|
||||||
|
aktion="spaltenfehlend",
|
||||||
|
kontext={"zeile": row_index, "fehlend": sorted(missing)},
|
||||||
|
)
|
||||||
|
results.append(self.pruefmanager.verarbeite(pe))
|
||||||
|
continue
|
||||||
|
|
||||||
|
ident = normalized.get("ident")
|
||||||
|
link_raw = normalized.get("link") or ""
|
||||||
|
provider = str(normalized.get("provider") or "").strip().lower()
|
||||||
|
stildatei_raw = normalized.get("stildatei") or ""
|
||||||
|
stildatei = None
|
||||||
|
|
||||||
|
if stildatei_raw and str(stildatei_raw).strip():
|
||||||
|
style_result = self.stil_pruefer.pruefe(str(stildatei_raw).strip())
|
||||||
|
results.append(self.pruefmanager.verarbeite(style_result))
|
||||||
|
if style_result.ok:
|
||||||
|
# Style-Pfad in der Datenkette beibehalten (absolut, wenn vorhanden).
|
||||||
|
stildatei = str(style_result.kontext or stildatei_raw).strip()
|
||||||
|
else:
|
||||||
|
stildatei = None
|
||||||
|
else:
|
||||||
|
results.append(self.pruefmanager.verarbeite(pruef_ergebnis(ok=True, meldung="Kein Stil angegeben", aktion="stil_optional", kontext=None)))
|
||||||
|
stildatei = None
|
||||||
|
|
||||||
|
if not ident or not link_raw or not provider:
|
||||||
|
pe = pruef_ergebnis(
|
||||||
|
ok=False,
|
||||||
|
meldung="Linklisten-Zeile hat fehlende Pflichtfelder (ident/link/provider).",
|
||||||
|
aktion="pflichtfelder_fehlen",
|
||||||
|
kontext={"zeile": row_index, "daten": raw_row},
|
||||||
|
)
|
||||||
|
results.append(self.pruefmanager.verarbeite(pe))
|
||||||
|
continue
|
||||||
|
|
||||||
|
link_url = extract_url(link_raw, provider)
|
||||||
|
|
||||||
|
# Provider-abhängige Linkvalidierung
|
||||||
|
if provider in ("wfs", "wms", "rest"):
|
||||||
|
# Webdienste: wir akzeptieren die URL-Form und prüfen nicht per network_head.
|
||||||
|
link_result = pruef_ergebnis(ok=True, meldung="Service-Link angenommen", aktion="service_link", kontext=link_url)
|
||||||
|
elif provider in ("ogr", "gpkg", "shp", "geojson"):
|
||||||
|
# OGR/Pfad: mit Linkpruefer (pfad oder lokale Datei) prüfen
|
||||||
|
link_result = self.link_pruefer.pruefe(link_url)
|
||||||
|
else:
|
||||||
|
link_result = self.link_pruefer.pruefe(link_url)
|
||||||
|
|
||||||
|
results.append(self.pruefmanager.verarbeite(link_result))
|
||||||
|
|
||||||
|
# stildatei wurde bereits oben geprüft und ggf. auf Dateiname gesetzt oder auf None
|
||||||
|
|
||||||
|
if not link_result.ok:
|
||||||
|
self.pruefmanager.verarbeite(
|
||||||
|
pruef_ergebnis(
|
||||||
|
ok=False,
|
||||||
|
meldung=f"Zeile {row_index}: fehlerhafter Link",
|
||||||
|
aktion="link_unvollstaendig",
|
||||||
|
kontext={"row": row_index, "ident": ident},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
result_row = {
|
||||||
|
"ident": ident,
|
||||||
|
"gruppe": normalized.get("gruppe"),
|
||||||
|
"Kartenebene": normalized.get("kartenebene"),
|
||||||
|
"Inhalt": normalized.get("inhalt"),
|
||||||
|
"Link": link_url,
|
||||||
|
"Provider": provider,
|
||||||
|
"stildatei": stildatei,
|
||||||
|
}
|
||||||
|
source_dict["rows"].append(result_row)
|
||||||
|
|
||||||
|
# Validierung über Listenauswerter
|
||||||
|
listenauswerter = Listenauswerter(self.pruefmanager, self.stil_pruefer or Stilpruefer())
|
||||||
|
validated, validation_results = listenauswerter.validate_rows(source_dict)
|
||||||
|
results.extend(validation_results)
|
||||||
|
return validated, results
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Datenbank‑Quellen
|
# Datenbank‑Quellen
|
||||||
@@ -153,6 +298,7 @@ class DataGrabber:
|
|||||||
def _process_database_source(
|
def _process_database_source(
|
||||||
self, db_path: str
|
self, db_path: str
|
||||||
) -> Tuple[SourceDict, List[pruef_ergebnis]]:
|
) -> Tuple[SourceDict, List[pruef_ergebnis]]:
|
||||||
|
print(f"[DataGrabber] Debug: _process_database_source called, db_path={db_path}")
|
||||||
source_dict: SourceDict = {}
|
source_dict: SourceDict = {}
|
||||||
results: List[pruef_ergebnis] = []
|
results: List[pruef_ergebnis] = []
|
||||||
return source_dict, results
|
return source_dict, results
|
||||||
@@ -177,24 +323,29 @@ class DataGrabber:
|
|||||||
partial_results: List[pruef_ergebnis],
|
partial_results: List[pruef_ergebnis],
|
||||||
) -> pruef_ergebnis:
|
) -> pruef_ergebnis:
|
||||||
"""
|
"""
|
||||||
Aggregiert Einzelprüfungen zu einem Gesamt‑``pruef_ergebnis``.
|
Aggregiert Einzelprüfungen zu einem Gesamt-``pruef_ergebnis``.
|
||||||
|
|
||||||
**Keine UI‑Interaktion.**
|
**Keine UI-Interaktion.**
|
||||||
"""
|
"""
|
||||||
if source_dict:
|
rows = source_dict.get("rows") if isinstance(source_dict, dict) else None
|
||||||
|
if rows:
|
||||||
return pruef_ergebnis(
|
return pruef_ergebnis(
|
||||||
ok=True,
|
ok=True,
|
||||||
meldung="Quelle erfolgreich geprüft",
|
meldung="Quelle erfolgreich geprüft",
|
||||||
aktion="ok",
|
aktion="ok",
|
||||||
kontext={
|
kontext={
|
||||||
"source": source,
|
"source": source,
|
||||||
"valid_entries": sum(len(v) for v in source_dict.values()),
|
"valid_entries": len(rows),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Wenn die Linkliste zwar gelesen wurde, aber keine gültigen Zeilen verfügbar sind, geben wir spezifischere Infos zurück.
|
||||||
return pruef_ergebnis(
|
return pruef_ergebnis(
|
||||||
ok=False,
|
ok=False,
|
||||||
meldung="Keine gültigen Einträge in der Quelle gefunden",
|
meldung="Keine validen Einträge in der Linkliste gefunden",
|
||||||
aktion="read_error",
|
aktion="keine_validen_eintraege",
|
||||||
kontext={"source": source},
|
kontext={
|
||||||
|
"source": source,
|
||||||
|
"eintraege_gesamt": len(source_dict.get("rows", [])),
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -17,10 +17,11 @@ Designprinzipien
|
|||||||
- Die Methode ist pdoc-kompatibel dokumentiert und bewusst einfach gehalten.
|
- Die Methode ist pdoc-kompatibel dokumentiert und bewusst einfach gehalten.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Any, 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
|
||||||
import json
|
import json
|
||||||
|
import time
|
||||||
|
|
||||||
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
|
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
|
||||||
from sn_basis.functions import qgiscore_wrapper as qgiscore
|
from sn_basis.functions import qgiscore_wrapper as qgiscore
|
||||||
@@ -59,6 +60,7 @@ class Datenabruf:
|
|||||||
verfahrensgebiet_layer: Any,
|
verfahrensgebiet_layer: Any,
|
||||||
speicherort: str,
|
speicherort: str,
|
||||||
pruef_ergebnisse: Optional[List[Any]] = None,
|
pruef_ergebnisse: Optional[List[Any]] = None,
|
||||||
|
progress: Optional[Any] = None,
|
||||||
) -> Tuple[Dict[str, Any], List[Any]]:
|
) -> Tuple[Dict[str, Any], List[Any]]:
|
||||||
"""
|
"""
|
||||||
Ruft für alle Zeilen in ``result_dict["rows"]`` die Fachdaten ab und
|
Ruft für alle Zeilen in ``result_dict["rows"]`` die Fachdaten ab und
|
||||||
@@ -82,6 +84,10 @@ class Datenabruf:
|
|||||||
|
|
||||||
# 1) Räumliche Filtergeometrie bestimmen (BBox oder None)
|
# 1) Räumliche Filtergeometrie bestimmen (BBox oder None)
|
||||||
bbox_geom = self._determine_spatial_filter(raumfilter, verfahrensgebiet_layer)
|
bbox_geom = self._determine_spatial_filter(raumfilter, verfahrensgebiet_layer)
|
||||||
|
filter_crs_authid = None
|
||||||
|
if isinstance(bbox_geom, dict):
|
||||||
|
raw_crs = bbox_geom.get("crs_authid")
|
||||||
|
filter_crs_authid = str(raw_crs) if raw_crs else None
|
||||||
|
|
||||||
# Globale Logs über alle Dienste hinweg
|
# Globale Logs über alle Dienste hinweg
|
||||||
log_geladen: Dict[str, int] = {}
|
log_geladen: Dict[str, int] = {}
|
||||||
@@ -90,7 +96,20 @@ class Datenabruf:
|
|||||||
log_ausserhalb: Dict[str, int] = {}
|
log_ausserhalb: Dict[str, int] = {}
|
||||||
|
|
||||||
# 2) Über alle Zeilen iterieren
|
# 2) Über alle Zeilen iterieren
|
||||||
for row in rows:
|
total_rows = len(rows)
|
||||||
|
for idx, row in enumerate(rows, start=1):
|
||||||
|
if progress is not None:
|
||||||
|
progress.set_label(f"Datenabruf {idx}/{total_rows}…")
|
||||||
|
if progress.is_canceled():
|
||||||
|
pe_cancel = pruef_ergebnis(
|
||||||
|
ok=False,
|
||||||
|
meldung="Datenabruf durch Benutzer abgebrochen",
|
||||||
|
aktion="abbruch",
|
||||||
|
kontext={"schritt": idx},
|
||||||
|
)
|
||||||
|
processed_results.append(self.pruefmanager.verarbeite(pe_cancel))
|
||||||
|
break
|
||||||
|
|
||||||
ident = row.get("ident")
|
ident = row.get("ident")
|
||||||
link = row.get("Link")
|
link = row.get("Link")
|
||||||
provider = row.get("Provider")
|
provider = row.get("Provider")
|
||||||
@@ -115,7 +134,16 @@ class Datenabruf:
|
|||||||
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)
|
||||||
|
|
||||||
# 2b) Fachdaten abrufen
|
# 2b) Fachdaten abrufen
|
||||||
features, error_msg = self._fetch_features(url=url, provider=str(provider))
|
features, error_msg = self._fetch_features(
|
||||||
|
url=url,
|
||||||
|
provider=str(provider),
|
||||||
|
cancel_callback=(progress.is_canceled if progress is not None else None),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if progress is not None:
|
||||||
|
if hasattr(progress, "set_value"):
|
||||||
|
progress.set_value(idx)
|
||||||
|
|
||||||
# 2c) Logs und Aggregation
|
# 2c) Logs und Aggregation
|
||||||
if error_msg:
|
if error_msg:
|
||||||
@@ -207,7 +235,18 @@ class Datenabruf:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
if raumfilter == "Verfahrensgebiet":
|
if raumfilter == "Verfahrensgebiet":
|
||||||
return qgiscore.get_layer_extent(verfahrensgebiet_layer)
|
extent = qgiscore.get_layer_extent(verfahrensgebiet_layer)
|
||||||
|
if extent is None:
|
||||||
|
return None
|
||||||
|
crs_authid = None
|
||||||
|
try:
|
||||||
|
if hasattr(verfahrensgebiet_layer, "crs") and callable(getattr(verfahrensgebiet_layer, "crs")):
|
||||||
|
crs = verfahrensgebiet_layer.crs()
|
||||||
|
if crs is not None and hasattr(crs, "authid") and callable(getattr(crs, "authid")):
|
||||||
|
crs_authid = crs.authid()
|
||||||
|
except Exception:
|
||||||
|
crs_authid = None
|
||||||
|
return {"extent": extent, "crs_authid": crs_authid}
|
||||||
|
|
||||||
if raumfilter == "Pufferlayer":
|
if raumfilter == "Pufferlayer":
|
||||||
buffer_layer = qgiscore.create_buffer_layer(
|
buffer_layer = qgiscore.create_buffer_layer(
|
||||||
@@ -216,8 +255,18 @@ class Datenabruf:
|
|||||||
layer_name="Verfahrensgebiet_Puffer_1km",
|
layer_name="Verfahrensgebiet_Puffer_1km",
|
||||||
)
|
)
|
||||||
if buffer_layer is not None:
|
if buffer_layer is not None:
|
||||||
qgisui.add_layer_to_project(buffer_layer)
|
extent = qgiscore.get_layer_extent(buffer_layer)
|
||||||
return qgiscore.get_layer_extent(buffer_layer)
|
if extent is None:
|
||||||
|
return None
|
||||||
|
crs_authid = None
|
||||||
|
try:
|
||||||
|
if hasattr(buffer_layer, "crs") and callable(getattr(buffer_layer, "crs")):
|
||||||
|
crs = buffer_layer.crs()
|
||||||
|
if crs is not None and hasattr(crs, "authid") and callable(getattr(crs, "authid")):
|
||||||
|
crs_authid = crs.authid()
|
||||||
|
except Exception:
|
||||||
|
crs_authid = None
|
||||||
|
return {"extent": extent, "crs_authid": crs_authid}
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -233,60 +282,130 @@ class Datenabruf:
|
|||||||
Erwartet: provider ist gesetzt (z. B. "WFS", "REST", "OGR", "WMS").
|
Erwartet: provider ist gesetzt (z. B. "WFS", "REST", "OGR", "WMS").
|
||||||
"""
|
"""
|
||||||
provider_norm = (provider or "").upper()
|
provider_norm = (provider or "").upper()
|
||||||
base_link = link or ""
|
base_link = (link or "").strip()
|
||||||
|
if base_link.lower().startswith("url="):
|
||||||
|
base_link = base_link[4:].strip()
|
||||||
|
|
||||||
# WMS: niemals BBOX anhängen
|
if provider_norm == "WFS" and base_link.count("?") > 1:
|
||||||
|
first, rest = base_link.split("?", 1)
|
||||||
|
base_link = f"{first}?{rest.replace('?', '&')}"
|
||||||
|
|
||||||
|
extent_obj = bbox_geom
|
||||||
|
crs_authid: Optional[str] = None
|
||||||
|
if isinstance(bbox_geom, dict):
|
||||||
|
extent_obj = bbox_geom.get("extent")
|
||||||
|
raw_crs = bbox_geom.get("crs_authid")
|
||||||
|
crs_authid = str(raw_crs) if raw_crs else None
|
||||||
|
|
||||||
|
# WMS: unverändert durchreichen
|
||||||
if provider_norm == "WMS":
|
if provider_norm == "WMS":
|
||||||
return base_link
|
return base_link
|
||||||
|
|
||||||
if bbox_geom is None:
|
# Versuche bbox-String zu erzeugen (falls Raumfilter aktiv)
|
||||||
return base_link
|
|
||||||
|
|
||||||
# Versuche bbox-String zu erzeugen (nutzt qgiscore.extent_to_bbox_string wenn vorhanden)
|
|
||||||
bbox_str: Optional[str] = None
|
bbox_str: Optional[str] = None
|
||||||
try:
|
if extent_obj is not None:
|
||||||
extent_to_bbox = getattr(__import__("sn_basis.functions.qgiscore_wrapper", fromlist=["qgiscore_wrapper"]), "extent_to_bbox_string", None)
|
try:
|
||||||
if callable(extent_to_bbox):
|
extent_to_bbox = getattr(__import__("sn_basis.functions.qgiscore_wrapper", fromlist=["qgiscore_wrapper"]), "extent_to_bbox_string", None)
|
||||||
bbox_str = extent_to_bbox(bbox_geom)
|
if callable(extent_to_bbox):
|
||||||
else:
|
bbox_str = extent_to_bbox(extent_obj)
|
||||||
# Fallback: einfache xmin/ymin/xmax/ymax-Extraktion (duck-typing)
|
|
||||||
if hasattr(bbox_geom, "xmin") and callable(getattr(bbox_geom, "xmin")):
|
|
||||||
bbox_str = f"{bbox_geom.xmin()},{bbox_geom.ymin()},{bbox_geom.xmax()},{bbox_geom.ymax()}"
|
|
||||||
elif isinstance(bbox_geom, (tuple, list)) and len(bbox_geom) == 4:
|
|
||||||
bbox_str = f"{bbox_geom[0]},{bbox_geom[1]},{bbox_geom[2]},{bbox_geom[3]}"
|
|
||||||
else:
|
else:
|
||||||
bbox_str = str(bbox_geom)
|
# Fallback: einfache xmin/ymin/xmax/ymax-Extraktion (duck-typing)
|
||||||
except Exception:
|
if hasattr(extent_obj, "xmin") and callable(getattr(extent_obj, "xmin")):
|
||||||
bbox_str = None
|
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:
|
||||||
if not bbox_str:
|
bbox_str = f"{extent_obj[0]},{extent_obj[1]},{extent_obj[2]},{extent_obj[3]}"
|
||||||
return base_link
|
else:
|
||||||
|
bbox_str = str(extent_obj)
|
||||||
|
except Exception:
|
||||||
|
bbox_str = None
|
||||||
|
|
||||||
parsed = urlparse(base_link)
|
parsed = urlparse(base_link)
|
||||||
query_params = dict(parse_qsl(parsed.query, keep_blank_values=True))
|
query_params = dict(parse_qsl(parsed.query, keep_blank_values=True))
|
||||||
|
|
||||||
if provider_norm == "WFS":
|
if provider_norm == "WFS":
|
||||||
query_params.setdefault("BBOX", bbox_str)
|
query_params.setdefault("service", "WFS")
|
||||||
|
query_params.setdefault("request", "GetFeature")
|
||||||
|
query_params.setdefault("outputFormat", "application/json")
|
||||||
|
if bbox_str:
|
||||||
|
query_params.setdefault("BBOX", bbox_str)
|
||||||
|
if crs_authid:
|
||||||
|
query_params.setdefault("SRSNAME", crs_authid)
|
||||||
new_query = urlencode(query_params, doseq=True)
|
new_query = urlencode(query_params, doseq=True)
|
||||||
rebuilt = parsed._replace(query=new_query)
|
rebuilt = parsed._replace(query=new_query)
|
||||||
return urlunparse(rebuilt)
|
return urlunparse(rebuilt)
|
||||||
|
|
||||||
if provider_norm in ("REST", "ARCGIS", "ARCGISFEATURESERVER", "ARCGIS_FEATURESERVER"):
|
if provider_norm in ("REST", "ARCGIS", "ARCGISFEATURESERVER", "ARCGIS_FEATURESERVER"):
|
||||||
query_params.setdefault("geometry", bbox_str)
|
# ArcGIS FeatureServer erwartet i.d.R. den /query-Endpunkt
|
||||||
query_params.setdefault("geometryType", "esriGeometryEnvelope")
|
rest_base = base_link.rstrip("/")
|
||||||
query_params.setdefault("spatialRel", "esriSpatialRelIntersects")
|
if not rest_base.lower().endswith("/query"):
|
||||||
|
rest_base = f"{rest_base}/query"
|
||||||
|
|
||||||
|
parsed_rest = urlparse(rest_base)
|
||||||
|
query_params = dict(parse_qsl(parsed_rest.query, keep_blank_values=True))
|
||||||
|
query_params.setdefault("where", "1=1")
|
||||||
|
query_params.setdefault("outFields", "*")
|
||||||
|
query_params.setdefault("returnGeometry", "true")
|
||||||
query_params.setdefault("f", query_params.get("f", "json"))
|
query_params.setdefault("f", query_params.get("f", "json"))
|
||||||
|
|
||||||
|
if bbox_str:
|
||||||
|
geometry_envelope = None
|
||||||
|
try:
|
||||||
|
if hasattr(extent_obj, "xmin") and callable(getattr(extent_obj, "xmin")):
|
||||||
|
geometry_envelope = {
|
||||||
|
"xmin": extent_obj.xmin(),
|
||||||
|
"ymin": extent_obj.ymin(),
|
||||||
|
"xmax": extent_obj.xmax(),
|
||||||
|
"ymax": extent_obj.ymax(),
|
||||||
|
}
|
||||||
|
elif isinstance(extent_obj, (tuple, list)) and len(extent_obj) == 4:
|
||||||
|
geometry_envelope = {
|
||||||
|
"xmin": extent_obj[0],
|
||||||
|
"ymin": extent_obj[1],
|
||||||
|
"xmax": extent_obj[2],
|
||||||
|
"ymax": extent_obj[3],
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
parts = [p.strip() for p in str(bbox_str).split(",")]
|
||||||
|
if len(parts) == 4:
|
||||||
|
geometry_envelope = {
|
||||||
|
"xmin": float(parts[0]),
|
||||||
|
"ymin": float(parts[1]),
|
||||||
|
"xmax": float(parts[2]),
|
||||||
|
"ymax": float(parts[3]),
|
||||||
|
}
|
||||||
|
except Exception:
|
||||||
|
geometry_envelope = None
|
||||||
|
|
||||||
|
if geometry_envelope is not None:
|
||||||
|
query_params.setdefault("geometry", json.dumps(geometry_envelope))
|
||||||
|
else:
|
||||||
|
query_params.setdefault("geometry", bbox_str)
|
||||||
|
query_params.setdefault("geometryType", "esriGeometryEnvelope")
|
||||||
|
query_params.setdefault("spatialRel", "esriSpatialRelIntersects")
|
||||||
|
|
||||||
|
if crs_authid and ":" in crs_authid:
|
||||||
|
srid = crs_authid.split(":", 1)[1]
|
||||||
|
if srid.isdigit():
|
||||||
|
query_params.setdefault("inSR", srid)
|
||||||
|
query_params.setdefault("outSR", srid)
|
||||||
|
|
||||||
new_query = urlencode(query_params, doseq=True)
|
new_query = urlencode(query_params, doseq=True)
|
||||||
rebuilt = parsed._replace(query=new_query)
|
rebuilt = parsed_rest._replace(query=new_query)
|
||||||
return urlunparse(rebuilt)
|
return urlunparse(rebuilt)
|
||||||
|
|
||||||
# Default: generischer bbox-Parameter
|
# Default: generischer bbox-Parameter (nur wenn vorhanden)
|
||||||
query_params.setdefault("bbox", bbox_str)
|
if bbox_str:
|
||||||
|
query_params.setdefault("bbox", bbox_str)
|
||||||
new_query = urlencode(query_params, doseq=True)
|
new_query = urlencode(query_params, doseq=True)
|
||||||
rebuilt = parsed._replace(query=new_query)
|
rebuilt = parsed._replace(query=new_query)
|
||||||
return urlunparse(rebuilt)
|
return urlunparse(rebuilt)
|
||||||
|
|
||||||
def _fetch_features(self, url: str, provider: str) -> Tuple[List[Any], Optional[str]]:
|
def _fetch_features(
|
||||||
|
self,
|
||||||
|
url: str,
|
||||||
|
provider: str,
|
||||||
|
cancel_callback: Optional[Callable[[], bool]] = None,
|
||||||
|
) -> Tuple[List[Any], Optional[str]]:
|
||||||
"""
|
"""
|
||||||
Führt den eigentlichen Abruf der Fachdaten durch.
|
Führt den eigentlichen Abruf der Fachdaten durch.
|
||||||
|
|
||||||
@@ -336,34 +455,100 @@ class Datenabruf:
|
|||||||
http_error: Optional[str] = None
|
http_error: Optional[str] = None
|
||||||
|
|
||||||
# QGIS NetworkAccessManager bevorzugen
|
# QGIS NetworkAccessManager bevorzugen
|
||||||
|
_FETCH_TIMEOUT_MS = 30_000 # 30 Sekunden
|
||||||
|
aborted_or_timed_out = False
|
||||||
|
attempted_qgis_fetch = False
|
||||||
|
|
||||||
|
if callable(cancel_callback) and cancel_callback():
|
||||||
|
return [], "Abbruch durch Benutzer"
|
||||||
|
|
||||||
if getattr(qgiscore, "QGIS_AVAILABLE", False) and getattr(qgiscore, "QgsNetworkAccessManager", None) is not None:
|
if getattr(qgiscore, "QGIS_AVAILABLE", False) and getattr(qgiscore, "QgsNetworkAccessManager", None) is not None:
|
||||||
|
attempted_qgis_fetch = True
|
||||||
try:
|
try:
|
||||||
manager = qgiscore.QgsNetworkAccessManager.instance()
|
manager = qgiscore.QgsNetworkAccessManager.instance()
|
||||||
QUrl = getattr(__import__("sn_basis.functions.qt_wrapper", fromlist=["qt_wrapper"]), "QUrl", None)
|
# Netzwerk-Timeout global setzen (QGIS >= 3.6)
|
||||||
QNetworkRequest = getattr(__import__("sn_basis.functions.qt_wrapper", fromlist=["qt_wrapper"]), "QNetworkRequest", None)
|
if hasattr(manager, "setTimeout"):
|
||||||
QEventLoop = getattr(__import__("sn_basis.functions.qt_wrapper", fromlist=["qt_wrapper"]), "QEventLoop", None)
|
manager.setTimeout(_FETCH_TIMEOUT_MS)
|
||||||
|
_qt = __import__("sn_basis.functions.qt_wrapper", fromlist=["qt_wrapper"])
|
||||||
|
QUrl = getattr(_qt, "QUrl", None)
|
||||||
|
QNetworkRequest = getattr(_qt, "QNetworkRequest", None)
|
||||||
|
QEventLoop = getattr(_qt, "QEventLoop", None)
|
||||||
|
QTimer = getattr(_qt, "QTimer", None)
|
||||||
if QUrl is not None and QNetworkRequest is not None:
|
if QUrl is not None and QNetworkRequest is not None:
|
||||||
req = QNetworkRequest(QUrl(url))
|
req = QNetworkRequest(QUrl(url))
|
||||||
reply = manager.get(req)
|
reply = manager.get(req)
|
||||||
if QEventLoop is not None:
|
if QEventLoop is not None:
|
||||||
loop = QEventLoop()
|
loop = QEventLoop()
|
||||||
reply.finished.connect(loop.quit)
|
reply.finished.connect(loop.quit)
|
||||||
loop.exec()
|
_poll_timer = None
|
||||||
try:
|
if QTimer is not None:
|
||||||
raw = reply.readAll()
|
try:
|
||||||
data_bytes = bytes(raw) if hasattr(raw, "__bytes__") else raw
|
_poll_timer = QTimer()
|
||||||
response_text = data_bytes.decode("utf-8", errors="replace")
|
_poll_timer.setSingleShot(False)
|
||||||
except Exception:
|
_poll_timer.timeout.connect(loop.quit)
|
||||||
|
_poll_timer.start(100)
|
||||||
|
except Exception:
|
||||||
|
_poll_timer = None
|
||||||
|
|
||||||
|
start_time = time.monotonic()
|
||||||
|
while True:
|
||||||
|
if callable(cancel_callback) and cancel_callback():
|
||||||
|
reply.abort()
|
||||||
|
http_error = "Abbruch durch Benutzer"
|
||||||
|
aborted_or_timed_out = True
|
||||||
|
break
|
||||||
|
|
||||||
|
elapsed_ms = int((time.monotonic() - start_time) * 1000)
|
||||||
|
if elapsed_ms >= _FETCH_TIMEOUT_MS:
|
||||||
|
reply.abort()
|
||||||
|
http_error = f"Timeout nach {_FETCH_TIMEOUT_MS // 1000} s: {url}"
|
||||||
|
aborted_or_timed_out = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if hasattr(reply, "isFinished") and reply.isFinished():
|
||||||
|
break
|
||||||
|
|
||||||
|
loop.exec()
|
||||||
|
try:
|
||||||
|
if hasattr(qt, "QCoreApplication") and hasattr(qt.QCoreApplication, "processEvents"):
|
||||||
|
qt.QCoreApplication.processEvents()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if _poll_timer is not None:
|
||||||
|
try:
|
||||||
|
_poll_timer.stop()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if not aborted_or_timed_out:
|
||||||
|
# Fehler aus Reply auslesen
|
||||||
|
err_code = None
|
||||||
|
try:
|
||||||
|
err_code = reply.error()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if err_code and int(err_code) != 0:
|
||||||
|
http_error = f"Netzwerkfehler ({err_code}): {reply.errorString()}"
|
||||||
|
if http_error:
|
||||||
|
# Timeout oder Netzwerkfehler – keinen Body lesen
|
||||||
|
pass
|
||||||
|
else:
|
||||||
try:
|
try:
|
||||||
response_text = reply.text()
|
raw = reply.readAll()
|
||||||
|
data_bytes = bytes(raw) if hasattr(raw, "__bytes__") else raw
|
||||||
|
response_text = data_bytes.decode("utf-8", errors="replace")
|
||||||
except Exception:
|
except Exception:
|
||||||
response_text = None
|
try:
|
||||||
|
response_text = reply.text()
|
||||||
|
except Exception:
|
||||||
|
response_text = None
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
http_error = f"QgsNetworkAccessManager error: {exc}"
|
http_error = f"QgsNetworkAccessManager error: {exc}"
|
||||||
response_text = None
|
response_text = None
|
||||||
|
|
||||||
# Fallback: requests
|
# Fallback: requests nur wenn kein harter Abbruch/Timeout im QGIS-Request vorlag
|
||||||
if response_text is None:
|
if response_text is None and (not attempted_qgis_fetch or not aborted_or_timed_out):
|
||||||
try:
|
try:
|
||||||
import requests # lokal import, keine harte Abhängigkeit
|
import requests # lokal import, keine harte Abhängigkeit
|
||||||
r = requests.get(url, timeout=30)
|
r = requests.get(url, timeout=30)
|
||||||
@@ -383,6 +568,8 @@ class Datenabruf:
|
|||||||
return parsed.get("features", []), None
|
return parsed.get("features", []), None
|
||||||
if isinstance(parsed, dict) and "features" in parsed:
|
if isinstance(parsed, dict) and "features" in parsed:
|
||||||
return parsed.get("features", []), None
|
return parsed.get("features", []), None
|
||||||
|
if prov in ("REST", "ARCGIS", "ARCGISFEATURESERVER", "ARCGIS_FEATURESERVER", "WFS"):
|
||||||
|
return [], "Antwort enthält keine Feature-Liste"
|
||||||
# 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:
|
||||||
|
|||||||
@@ -30,9 +30,13 @@ from __future__ import annotations
|
|||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
|
import re
|
||||||
import datetime
|
import datetime
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
from sn_basis.functions import qgiscore_wrapper as qgiscore
|
from sn_basis.functions import qgiscore_wrapper as qgiscore
|
||||||
|
from sn_basis.functions.os_wrapper import normalize_path, is_absolute_path
|
||||||
|
from sn_basis.functions.sys_wrapper import get_plugin_root, join_path, file_exists
|
||||||
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
|
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
|
||||||
|
|
||||||
|
|
||||||
@@ -55,8 +59,95 @@ class Datenschreiber:
|
|||||||
self.pruefmanager = pruefmanager
|
self.pruefmanager = pruefmanager
|
||||||
self.gpkg_path = str(gpkg_path) if gpkg_path else None
|
self.gpkg_path = str(gpkg_path) if gpkg_path else None
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
def _resolve_style_path(self, style_path: Optional[str]) -> Optional[str]:
|
||||||
# Schreibe Daten
|
if not style_path:
|
||||||
|
return None
|
||||||
|
|
||||||
|
style_path_str = str(style_path).strip()
|
||||||
|
if not style_path_str:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not is_absolute_path(style_path_str):
|
||||||
|
plugin_root = get_plugin_root()
|
||||||
|
style_path_str = str(join_path(plugin_root, "sn_plan41", "assets", style_path_str))
|
||||||
|
|
||||||
|
style_path_str = str(normalize_path(style_path_str))
|
||||||
|
return style_path_str if file_exists(style_path_str) else None
|
||||||
|
|
||||||
|
def _store_style_in_gpkg(self, layer_name: str, style_path: str, layer: Optional[Any] = None) -> None:
|
||||||
|
"""Stellt sicher, dass der Stil in der layer_styles-Tabelle der GPKG gespeichert wird."""
|
||||||
|
try:
|
||||||
|
with open(style_path, "r", encoding="utf-8") as fh:
|
||||||
|
style_qml = fh.read()
|
||||||
|
|
||||||
|
f_geometry_column = ''
|
||||||
|
if layer is not None:
|
||||||
|
try:
|
||||||
|
if hasattr(layer, 'geometryColumn'):
|
||||||
|
f_geometry_column = str(layer.geometryColumn())
|
||||||
|
elif hasattr(layer, 'dataProvider') and hasattr(layer.dataProvider(), 'geometryColumnName'):
|
||||||
|
f_geometry_column = str(layer.dataProvider().geometryColumnName())
|
||||||
|
except Exception:
|
||||||
|
f_geometry_column = ''
|
||||||
|
|
||||||
|
with sqlite3.connect(self.gpkg_path) as conn:
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS layer_styles (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
f_table_catalog TEXT,
|
||||||
|
f_table_schema TEXT,
|
||||||
|
f_table_name TEXT NOT NULL,
|
||||||
|
f_geometry_column TEXT,
|
||||||
|
styleName TEXT,
|
||||||
|
styleQML TEXT,
|
||||||
|
styleSLD TEXT,
|
||||||
|
useAsDefault BOOLEAN,
|
||||||
|
description TEXT,
|
||||||
|
owner TEXT,
|
||||||
|
ui TEXT,
|
||||||
|
update_time DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
# Das aktuelle QGIS-Style-Verhalten: bestehenden Style für denselben Layer nicht löschen (nur appenden)
|
||||||
|
# Wir wollen aber Default-Style setzen: alte Default-Styles entfernen.
|
||||||
|
cur.execute(
|
||||||
|
"UPDATE layer_styles SET useAsDefault = 0 WHERE f_table_name = ?",
|
||||||
|
(layer_name,),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Fülle die bekannten QGIS-Kolonnen
|
||||||
|
style_name = os.path.basename(style_path)
|
||||||
|
|
||||||
|
cur.execute(
|
||||||
|
"INSERT INTO layer_styles (f_table_catalog, f_table_schema, f_table_name, f_geometry_column, styleName, styleQML, styleSLD, useAsDefault, description, owner, ui) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||||
|
(
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
layer_name,
|
||||||
|
f_geometry_column,
|
||||||
|
style_name,
|
||||||
|
style_qml,
|
||||||
|
None,
|
||||||
|
1,
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
except Exception as exc:
|
||||||
|
self.pruefmanager.verarbeite(
|
||||||
|
pruef_ergebnis(
|
||||||
|
ok=False,
|
||||||
|
meldung=f"Fehler beim Speichern des Layer-Stils in GPKG: {exc}",
|
||||||
|
aktion="style_gpkg_speichern_fehlgeschlagen",
|
||||||
|
kontext={"layer_name": layer_name, "style_path": style_path},
|
||||||
|
)
|
||||||
|
)
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
def schreibe_Daten(
|
def schreibe_Daten(
|
||||||
self,
|
self,
|
||||||
@@ -85,12 +176,14 @@ class Datenschreiber:
|
|||||||
|
|
||||||
for ident, entry in daten_map.items():
|
for ident, entry in daten_map.items():
|
||||||
layer = None
|
layer = None
|
||||||
|
style_path = None
|
||||||
|
|
||||||
# -----------------------------
|
# -----------------------------
|
||||||
# Layer extrahieren
|
# Layer extrahieren
|
||||||
# -----------------------------
|
# -----------------------------
|
||||||
if isinstance(entry, dict) and "layer" in entry:
|
if isinstance(entry, dict):
|
||||||
layer = entry["layer"]
|
layer = entry.get("layer")
|
||||||
|
style_path = self._resolve_style_path(entry.get("style_path"))
|
||||||
|
|
||||||
if layer is None or not hasattr(layer, "isValid") or not layer.isValid():
|
if layer is None or not hasattr(layer, "isValid") or not layer.isValid():
|
||||||
pe_err = pruef_ergebnis(
|
pe_err = pruef_ergebnis(
|
||||||
@@ -115,7 +208,11 @@ class Datenschreiber:
|
|||||||
except Exception:
|
except Exception:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
layer_name = thema or str(ident)
|
layer_name_raw = thema or str(ident)
|
||||||
|
layer_name = re.sub(r"[^A-Za-z0-9_]+", "_", layer_name_raw).strip("_")
|
||||||
|
if not layer_name:
|
||||||
|
layer_name = f"layer_{ident}"
|
||||||
|
|
||||||
# Layer in GPKG schreiben
|
# Layer in GPKG schreiben
|
||||||
err_msg = self._write_layer_to_gpkg(layer_name=layer_name, layer=layer)
|
err_msg = self._write_layer_to_gpkg(layer_name=layer_name, layer=layer)
|
||||||
if err_msg is not None:
|
if err_msg is not None:
|
||||||
@@ -128,12 +225,17 @@ class Datenschreiber:
|
|||||||
self.pruefmanager.verarbeite(pe_err)
|
self.pruefmanager.verarbeite(pe_err)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Wenn der Stil vorhanden und valide ist, als Default in GPKG-Style-Tabelle ablegen
|
||||||
|
if style_path:
|
||||||
|
self._store_style_in_gpkg(layer_name, style_path, layer)
|
||||||
|
|
||||||
# Erfolgsfall: Info für lade_Layer sammeln
|
# Erfolgsfall: Info für lade_Layer sammeln
|
||||||
layer_path = f"{self.gpkg_path}|layername={layer_name}"
|
layer_path = f"{self.gpkg_path}|layername={layer_name}"
|
||||||
results.append({
|
results.append({
|
||||||
"layer_path": layer_path,
|
"layer_path": layer_path,
|
||||||
"thema": layer_name,
|
"thema": layer_name,
|
||||||
"ident": ident,
|
"ident": ident,
|
||||||
|
"style_path": style_path,
|
||||||
})
|
})
|
||||||
|
|
||||||
return results
|
return results
|
||||||
@@ -178,18 +280,33 @@ class Datenschreiber:
|
|||||||
self.pruefmanager.verarbeite(pe_err)
|
self.pruefmanager.verarbeite(pe_err)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
try:
|
style_path = info.get("style_path")
|
||||||
apply_style_fn = getattr(qgiscore, "apply_default_style_from_gpkg", None)
|
resolved_style_path = self._resolve_style_path(style_path)
|
||||||
if callable(apply_style_fn):
|
if resolved_style_path:
|
||||||
apply_style_fn(self.gpkg_path, layer)
|
try:
|
||||||
except Exception:
|
layer.loadNamedStyle(resolved_style_path)
|
||||||
pe_warn = pruef_ergebnis(
|
layer.triggerRepaint()
|
||||||
ok=True,
|
except Exception as exc:
|
||||||
meldung=f"Style konnte für {thema} nicht automatisch angewendet werden",
|
pe_warn = pruef_ergebnis(
|
||||||
aktion="stil_not_implemented",
|
ok=True,
|
||||||
kontext={"thema": thema},
|
meldung=f"Style konnte für {thema} nicht geladen werden: {exc}",
|
||||||
)
|
aktion="stil_laden_fehlgeschlagen",
|
||||||
self.pruefmanager.verarbeite(pe_warn)
|
kontext={"thema": thema, "style_path": resolved_style_path},
|
||||||
|
)
|
||||||
|
self.pruefmanager.verarbeite(pe_warn)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
apply_style_fn = getattr(qgiscore, "apply_default_style_from_gpkg", None)
|
||||||
|
if callable(apply_style_fn):
|
||||||
|
apply_style_fn(self.gpkg_path, layer)
|
||||||
|
except Exception:
|
||||||
|
pe_warn = pruef_ergebnis(
|
||||||
|
ok=True,
|
||||||
|
meldung=f"Style konnte für {thema} nicht automatisch angewendet werden",
|
||||||
|
aktion="stil_not_implemented",
|
||||||
|
kontext={"thema": thema},
|
||||||
|
)
|
||||||
|
self.pruefmanager.verarbeite(pe_warn)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# qgisui wrapper wird hier nicht direkt für die Abfrage verwendet;
|
# qgisui wrapper wird hier nicht direkt für die Abfrage verwendet;
|
||||||
@@ -286,6 +403,19 @@ class Datenschreiber:
|
|||||||
opts.layerName = layer_name
|
opts.layerName = layer_name
|
||||||
opts.fileEncoding = "UTF-8"
|
opts.fileEncoding = "UTF-8"
|
||||||
|
|
||||||
|
# Style in der GPKG speichern, wenn möglich
|
||||||
|
if hasattr(opts, "symbologyExport"):
|
||||||
|
try:
|
||||||
|
# QGIS: SymbologyExport-Wert z.B. QgsVectorFileWriter.SaveVectorOptions.Symbology
|
||||||
|
saveOpts = qgiscore.QgsVectorFileWriter.SaveVectorOptions
|
||||||
|
sym_val = getattr(saveOpts, "Symbology", None)
|
||||||
|
if sym_val is None:
|
||||||
|
sym_val = getattr(saveOpts, "SymbologyExport", None)
|
||||||
|
if sym_val is not None:
|
||||||
|
opts.symbologyExport = sym_val
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
# Datei existiert → Layer überschreiben
|
# Datei existiert → Layer überschreiben
|
||||||
# Datei existiert nicht → neue GPKG anlegen
|
# Datei existiert nicht → neue GPKG anlegen
|
||||||
if not os.path.exists(self.gpkg_path):
|
if not os.path.exists(self.gpkg_path):
|
||||||
|
|||||||
395
modules/LayerLoader.py
Normal file
395
modules/LayerLoader.py
Normal file
@@ -0,0 +1,395 @@
|
|||||||
|
"""sn_basis/modules/LayerLoader.py
|
||||||
|
|
||||||
|
Kapselt Layer-Erstellung, Raumfilter und Stil-Logik.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
import time
|
||||||
|
|
||||||
|
from sn_basis.functions.os_wrapper import normalize_path, is_absolute_path
|
||||||
|
from sn_basis.functions.qgiscore_wrapper import (
|
||||||
|
QgsVectorLayer,
|
||||||
|
QgsRasterLayer,
|
||||||
|
QgsFeatureRequest,
|
||||||
|
QgsProject,
|
||||||
|
QgsNetworkAccessManager,
|
||||||
|
QgsCoordinateTransform,
|
||||||
|
)
|
||||||
|
from sn_basis.functions.sys_wrapper import get_plugin_root, join_path, file_exists
|
||||||
|
from sn_basis.modules.stilpruefer import Stilpruefer
|
||||||
|
from sn_basis.modules.layerpruefer import Layerpruefer
|
||||||
|
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
|
||||||
|
from sn_basis.functions import qt_wrapper as qt
|
||||||
|
|
||||||
|
|
||||||
|
class LayerLoader:
|
||||||
|
"""Lädt und filtert Layer aus Dienst-/Datenquellen."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
pruefmanager: Any,
|
||||||
|
stil_pruefer: Optional[Stilpruefer] = None,
|
||||||
|
layer_pruefer: Optional[Layerpruefer] = None,
|
||||||
|
) -> None:
|
||||||
|
self.pruefmanager = pruefmanager
|
||||||
|
self.stil_pruefer = stil_pruefer or Stilpruefer()
|
||||||
|
self.layer_pruefer = layer_pruefer or Layerpruefer()
|
||||||
|
|
||||||
|
_LAYER_TIMEOUT_MS = 30_000 # 30 Sekunden
|
||||||
|
|
||||||
|
def _was_canceled(self, cancel_callback: Optional[Any]) -> bool:
|
||||||
|
if not callable(cancel_callback):
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
return bool(cancel_callback())
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _process_events(self) -> None:
|
||||||
|
try:
|
||||||
|
if hasattr(qt, "QCoreApplication") and hasattr(qt.QCoreApplication, "processEvents"):
|
||||||
|
qt.QCoreApplication.processEvents()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _transform_geometry_to_layer_crs(self, geometry: Any, source_layer: Any, target_layer: Any) -> Any:
|
||||||
|
if geometry is None or source_layer is None or target_layer is None:
|
||||||
|
return geometry
|
||||||
|
|
||||||
|
if QgsCoordinateTransform is None or QgsProject is None:
|
||||||
|
return geometry
|
||||||
|
|
||||||
|
try:
|
||||||
|
source_crs = source_layer.crs() if hasattr(source_layer, "crs") else None
|
||||||
|
target_crs = target_layer.crs() if hasattr(target_layer, "crs") else None
|
||||||
|
if source_crs is None or target_crs is None:
|
||||||
|
return geometry
|
||||||
|
|
||||||
|
source_authid = source_crs.authid() if hasattr(source_crs, "authid") else None
|
||||||
|
target_authid = target_crs.authid() if hasattr(target_crs, "authid") else None
|
||||||
|
if source_authid and target_authid and source_authid == target_authid:
|
||||||
|
return geometry
|
||||||
|
|
||||||
|
ct = QgsCoordinateTransform(source_crs, target_crs, QgsProject.instance())
|
||||||
|
if hasattr(geometry, "clone") and callable(getattr(geometry, "clone")):
|
||||||
|
geom_copy = geometry.clone()
|
||||||
|
else:
|
||||||
|
geom_copy = geometry
|
||||||
|
geom_copy.transform(ct)
|
||||||
|
return geom_copy
|
||||||
|
except Exception:
|
||||||
|
return geometry
|
||||||
|
|
||||||
|
def _transform_extent_to_layer_crs(self, extent: Any, source_layer: Any, target_layer: Any) -> Any:
|
||||||
|
if extent is None or source_layer is None or target_layer is None:
|
||||||
|
return extent
|
||||||
|
|
||||||
|
if QgsCoordinateTransform is None or QgsProject is None:
|
||||||
|
return extent
|
||||||
|
|
||||||
|
try:
|
||||||
|
source_crs = source_layer.crs() if hasattr(source_layer, "crs") else None
|
||||||
|
target_crs = target_layer.crs() if hasattr(target_layer, "crs") else None
|
||||||
|
if source_crs is None or target_crs is None:
|
||||||
|
return extent
|
||||||
|
|
||||||
|
source_authid = source_crs.authid() if hasattr(source_crs, "authid") else None
|
||||||
|
target_authid = target_crs.authid() if hasattr(target_crs, "authid") else None
|
||||||
|
if source_authid and target_authid and source_authid == target_authid:
|
||||||
|
return extent
|
||||||
|
|
||||||
|
ct = QgsCoordinateTransform(source_crs, target_crs, QgsProject.instance())
|
||||||
|
if hasattr(ct, "transformBoundingBox"):
|
||||||
|
return ct.transformBoundingBox(extent)
|
||||||
|
return extent
|
||||||
|
except Exception:
|
||||||
|
return extent
|
||||||
|
|
||||||
|
def create_layer(self, provider: str, link: str, thema: str) -> Optional[QgsVectorLayer]:
|
||||||
|
provider_lower = provider.lower() if provider else ""
|
||||||
|
layer = None
|
||||||
|
|
||||||
|
# Netzwerk-Timeout für alle netzwerkbasierten Provider setzen
|
||||||
|
if provider_lower in ("wfs", "wms", "rest"):
|
||||||
|
try:
|
||||||
|
nam = QgsNetworkAccessManager.instance()
|
||||||
|
if hasattr(nam, "setTimeout"):
|
||||||
|
nam.setTimeout(self._LAYER_TIMEOUT_MS)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
if provider_lower == "wfs":
|
||||||
|
uri = link if link.strip().lower().startswith("url=") else f"url={link}"
|
||||||
|
layer = QgsVectorLayer(uri, thema, "WFS")
|
||||||
|
elif provider_lower == "wms":
|
||||||
|
uri = link if link.strip().lower().startswith("url=") else f"url={link}"
|
||||||
|
layer = QgsRasterLayer(uri, thema, "wms")
|
||||||
|
elif provider_lower in ("ogr", "gpkg", "shp", "geojson"):
|
||||||
|
layer = QgsVectorLayer(link, thema, "ogr")
|
||||||
|
elif provider_lower == "rest":
|
||||||
|
rest_link = link.strip()
|
||||||
|
if rest_link.lower().endswith("/featureserver"):
|
||||||
|
rest_link = rest_link.rstrip("/") + "/0"
|
||||||
|
uri = rest_link if rest_link.lower().startswith("url=") else f"url={rest_link}"
|
||||||
|
layer = QgsVectorLayer(uri, thema, "arcgisfeatureserver")
|
||||||
|
else:
|
||||||
|
layer = QgsVectorLayer(link, thema, "ogr")
|
||||||
|
except Exception as exc:
|
||||||
|
self.pruefmanager.verarbeite(
|
||||||
|
pruef_ergebnis(
|
||||||
|
ok=False,
|
||||||
|
meldung=f"Fehler beim Erstellen des Layers {thema}: {exc}",
|
||||||
|
aktion="layer_nicht_verfuegbar",
|
||||||
|
kontext={"provider": provider, "link": link},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not layer or not layer.isValid():
|
||||||
|
self.pruefmanager.verarbeite(
|
||||||
|
pruef_ergebnis(
|
||||||
|
ok=False,
|
||||||
|
meldung=f"Layer {thema} (Provider={provider}) konnte nicht geladen werden."
|
||||||
|
,aktion="layer_nicht_verfuegbar",
|
||||||
|
kontext={"provider": provider, "link": link},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
return layer
|
||||||
|
|
||||||
|
def apply_style(self, layer: QgsVectorLayer, style_path: Optional[str]) -> None:
|
||||||
|
if not style_path or layer is None or not layer.isValid():
|
||||||
|
return
|
||||||
|
|
||||||
|
if not style_path.strip():
|
||||||
|
return
|
||||||
|
|
||||||
|
if not is_absolute_path(style_path):
|
||||||
|
plugin_root = get_plugin_root()
|
||||||
|
style_path = str(join_path(plugin_root, "sn_plan41", "assets", style_path))
|
||||||
|
|
||||||
|
# normalize path for consistency
|
||||||
|
style_path = str(normalize_path(style_path))
|
||||||
|
|
||||||
|
# Debug: welche Stil-Datei wird geprüft?
|
||||||
|
print(f"[LayerLoader] Überprüfe Stildatei: '{style_path}'")
|
||||||
|
|
||||||
|
if file_exists(style_path):
|
||||||
|
try:
|
||||||
|
layer.loadNamedStyle(style_path)
|
||||||
|
layer.triggerRepaint()
|
||||||
|
except Exception as exc:
|
||||||
|
self.pruefmanager.verarbeite(
|
||||||
|
pruef_ergebnis(
|
||||||
|
ok=False,
|
||||||
|
meldung=f"Fehler beim Stil-Laden für {layer.name()}: {exc}",
|
||||||
|
aktion="stil_laden_fehlgeschlagen",
|
||||||
|
kontext={"thema": layer.name(), "style_path": style_path},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.pruefmanager.verarbeite(
|
||||||
|
pruef_ergebnis(
|
||||||
|
ok=True,
|
||||||
|
meldung=f"Stildatei nicht gefunden (optional): {style_path}",
|
||||||
|
aktion="stil_nicht_gefunden",
|
||||||
|
kontext={"thema": layer.name(), "style_path": style_path},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def filter_by_extent(self, layer: QgsVectorLayer, extent, cancel_callback: Optional[Any] = None, source_layer: Optional[Any] = None) -> Optional[QgsVectorLayer]:
|
||||||
|
"""Beschneidet <layer> auf die rechteckige Ausdehnung <extent>.
|
||||||
|
|
||||||
|
Diese Methode verwendet einen einfachen BBOX-Filter. Für komplexere
|
||||||
|
Raumeinschränkungen (z.B. Verfahrensgebiet) sollte stattdessen
|
||||||
|
:meth:`filter_by_layer` verwendet werden, da dort echte Geometrie-Tests
|
||||||
|
stattfinden.
|
||||||
|
"""
|
||||||
|
if not layer or not layer.isValid() or extent is None:
|
||||||
|
return layer
|
||||||
|
|
||||||
|
if layer.type() != QgsVectorLayer.VectorLayer:
|
||||||
|
return layer
|
||||||
|
|
||||||
|
extent_for_layer = self._transform_extent_to_layer_crs(extent, source_layer, layer)
|
||||||
|
request = QgsFeatureRequest().setFilterRect(extent_for_layer)
|
||||||
|
if hasattr(request, "setTimeout"):
|
||||||
|
try:
|
||||||
|
request.setTimeout(self._LAYER_TIMEOUT_MS)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
start = time.monotonic()
|
||||||
|
features: List[Any] = []
|
||||||
|
try:
|
||||||
|
for feat in layer.getFeatures(request):
|
||||||
|
if self._was_canceled(cancel_callback):
|
||||||
|
self.pruefmanager.verarbeite(
|
||||||
|
pruef_ergebnis(
|
||||||
|
ok=False,
|
||||||
|
meldung=f"Abbruch beim Raumfilter (BBOX) für {layer.name()}",
|
||||||
|
aktion="needs_user_action",
|
||||||
|
kontext={"thema": layer.name()},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
elapsed_ms = int((time.monotonic() - start) * 1000)
|
||||||
|
if elapsed_ms >= self._LAYER_TIMEOUT_MS:
|
||||||
|
self.pruefmanager.verarbeite(
|
||||||
|
pruef_ergebnis(
|
||||||
|
ok=False,
|
||||||
|
meldung=f"Timeout beim Raumfilter (BBOX) für {layer.name()} nach {self._LAYER_TIMEOUT_MS // 1000}s",
|
||||||
|
aktion="url_nicht_erreichbar",
|
||||||
|
kontext={"thema": layer.name(), "timeout_s": self._LAYER_TIMEOUT_MS // 1000},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
features.append(feat)
|
||||||
|
if len(features) % 100 == 0:
|
||||||
|
self._process_events()
|
||||||
|
except Exception as exc:
|
||||||
|
self.pruefmanager.verarbeite(
|
||||||
|
pruef_ergebnis(
|
||||||
|
ok=False,
|
||||||
|
meldung=f"Fehler beim Lesen der Features für {layer.name()}: {exc}",
|
||||||
|
aktion="layer_nicht_verfuegbar",
|
||||||
|
kontext={"thema": layer.name()},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not features:
|
||||||
|
return None
|
||||||
|
|
||||||
|
geom_type_map = {0: "Point", 1: "LineString", 2: "Polygon"}
|
||||||
|
geom_type = geom_type_map.get(layer.geometryType(), "Polygon")
|
||||||
|
uri = f"{geom_type}?crs={layer.crs().authid()}"
|
||||||
|
filtered_layer = QgsVectorLayer(uri, f"{layer.name()}_bbox", "memory")
|
||||||
|
if not filtered_layer or not filtered_layer.isValid():
|
||||||
|
self.pruefmanager.verarbeite(
|
||||||
|
pruef_ergebnis(
|
||||||
|
ok=False,
|
||||||
|
meldung=f"Fehler beim Erzeugen des Filter-Layers für {layer.name()}",
|
||||||
|
aktion="filterlayer_nicht_erzeugt",
|
||||||
|
kontext={"thema": layer.name()},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
provider = filtered_layer.dataProvider()
|
||||||
|
provider.addAttributes(layer.fields())
|
||||||
|
filtered_layer.updateFields()
|
||||||
|
provider.addFeatures(features)
|
||||||
|
filtered_layer.updateExtents()
|
||||||
|
|
||||||
|
return filtered_layer
|
||||||
|
|
||||||
|
def filter_by_layer(self, layer: QgsVectorLayer, filter_layer: QgsVectorLayer, cancel_callback: Optional[Any] = None) -> Optional[QgsVectorLayer]:
|
||||||
|
"""Beschneidet <layer> auf die tatsächliche Geometrie des
|
||||||
|
<filter_layer>.
|
||||||
|
|
||||||
|
Diese Methode wird z.B. für das Verfahrensgebiet verwendet, damit nicht
|
||||||
|
die gesamte Bounding-Box, sondern nur die echten Flächen als Raumfilter
|
||||||
|
gelten. Wenn der Filter-Layer mehrere Features enthält, werden deren
|
||||||
|
Geometrien zu einem Multi-Geom vereinigt.
|
||||||
|
"""
|
||||||
|
if not layer or not layer.isValid() or not filter_layer or not filter_layer.isValid():
|
||||||
|
return layer
|
||||||
|
|
||||||
|
if layer.type() != QgsVectorLayer.VectorLayer:
|
||||||
|
return layer
|
||||||
|
|
||||||
|
# vereinigte Geometrie aller Features im Filter-Layer
|
||||||
|
union_geom = None
|
||||||
|
for f in filter_layer.getFeatures():
|
||||||
|
try:
|
||||||
|
geom = self._transform_geometry_to_layer_crs(f.geometry(), filter_layer, layer)
|
||||||
|
if union_geom is None:
|
||||||
|
union_geom = geom
|
||||||
|
else:
|
||||||
|
union_geom = union_geom.combine(geom)
|
||||||
|
except Exception:
|
||||||
|
# bei einem Fehler einfach weiterfahren
|
||||||
|
continue
|
||||||
|
|
||||||
|
if union_geom is None or union_geom.isEmpty():
|
||||||
|
return None
|
||||||
|
|
||||||
|
# nun alle Features aus <layer> nehmen, deren Geometrie sich schneidet
|
||||||
|
filtered = []
|
||||||
|
request = QgsFeatureRequest().setFilterRect(union_geom.boundingBox())
|
||||||
|
if hasattr(request, "setTimeout"):
|
||||||
|
try:
|
||||||
|
request.setTimeout(self._LAYER_TIMEOUT_MS)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
start = time.monotonic()
|
||||||
|
for f in layer.getFeatures(request):
|
||||||
|
if self._was_canceled(cancel_callback):
|
||||||
|
self.pruefmanager.verarbeite(
|
||||||
|
pruef_ergebnis(
|
||||||
|
ok=False,
|
||||||
|
meldung=f"Abbruch beim Raumfilter (Geometrie) für {layer.name()}",
|
||||||
|
aktion="needs_user_action",
|
||||||
|
kontext={"thema": layer.name()},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
elapsed_ms = int((time.monotonic() - start) * 1000)
|
||||||
|
if elapsed_ms >= self._LAYER_TIMEOUT_MS:
|
||||||
|
self.pruefmanager.verarbeite(
|
||||||
|
pruef_ergebnis(
|
||||||
|
ok=False,
|
||||||
|
meldung=f"Timeout beim Raumfilter (Geometrie) für {layer.name()} nach {self._LAYER_TIMEOUT_MS // 1000}s",
|
||||||
|
aktion="url_nicht_erreichbar",
|
||||||
|
kontext={"thema": layer.name(), "timeout_s": self._LAYER_TIMEOUT_MS // 1000},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
if f.geometry() and f.geometry().intersects(union_geom):
|
||||||
|
filtered.append(f)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if len(filtered) % 100 == 0:
|
||||||
|
self._process_events()
|
||||||
|
|
||||||
|
if not filtered:
|
||||||
|
return None
|
||||||
|
|
||||||
|
geom_type_map = {0: "Point", 1: "LineString", 2: "Polygon"}
|
||||||
|
geom_type = geom_type_map.get(layer.geometryType(), "Polygon")
|
||||||
|
uri = f"{geom_type}?crs={layer.crs().authid()}"
|
||||||
|
filtered_layer = QgsVectorLayer(uri, f"{layer.name()}_filtered", "memory")
|
||||||
|
if not filtered_layer or not filtered_layer.isValid():
|
||||||
|
self.pruefmanager.verarbeite(
|
||||||
|
pruef_ergebnis(
|
||||||
|
ok=False,
|
||||||
|
meldung=f"Fehler beim Erzeugen des Filter-Layers für {layer.name()}",
|
||||||
|
aktion="filterlayer_nicht_erzeugt",
|
||||||
|
kontext={"thema": layer.name()},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
provider = filtered_layer.dataProvider()
|
||||||
|
provider.addAttributes(layer.fields())
|
||||||
|
filtered_layer.updateFields()
|
||||||
|
provider.addFeatures(filtered)
|
||||||
|
filtered_layer.updateExtents()
|
||||||
|
|
||||||
|
return filtered_layer
|
||||||
|
|
||||||
|
def add_to_project(self, layer: QgsVectorLayer) -> None:
|
||||||
|
if layer and layer.isValid():
|
||||||
|
QgsProject.instance().addMapLayer(layer)
|
||||||
@@ -4,9 +4,10 @@ Prüft ausschließlich, ob ein Stilpfad gültig ist.
|
|||||||
Die Anwendung erfolgt später über eine Aktion.
|
Die Anwendung erfolgt später über eine Aktion.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from pathlib import Path
|
import os
|
||||||
|
|
||||||
from sn_basis.functions.sys_wrapper import file_exists
|
from sn_basis.functions.os_wrapper import is_absolute_path
|
||||||
|
from sn_basis.functions.sys_wrapper import get_plugin_root, file_exists, join_path
|
||||||
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
|
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
|
||||||
|
|
||||||
|
|
||||||
@@ -40,7 +41,11 @@ class Stilpruefer:
|
|||||||
kontext=None,
|
kontext=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
pfad = Path(stil_pfad)
|
pfad = str(stil_pfad)
|
||||||
|
|
||||||
|
if not is_absolute_path(pfad):
|
||||||
|
plugin_root = get_plugin_root()
|
||||||
|
pfad = str(join_path(plugin_root, "sn_plan41", "assets", pfad))
|
||||||
|
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
# 2. Datei existiert nicht
|
# 2. Datei existiert nicht
|
||||||
@@ -56,7 +61,7 @@ class Stilpruefer:
|
|||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
# 3. Falsche Endung
|
# 3. Falsche Endung
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
if pfad.suffix.lower() != ".qml":
|
if os.path.splitext(pfad)[1].lower() != ".qml":
|
||||||
return pruef_ergebnis(
|
return pruef_ergebnis(
|
||||||
ok=False,
|
ok=False,
|
||||||
meldung="Die Stil-Datei muss die Endung '.qml' haben.",
|
meldung="Die Stil-Datei muss die Endung '.qml' haben.",
|
||||||
|
|||||||
Reference in New Issue
Block a user