forked from AG_QGIS/Plugin_SN_Basis
881 lines
24 KiB
Python
881 lines
24 KiB
Python
"""
|
||
sn_basis/functions/qgisqt_wrapper.py – zentrale QGIS/Qt-Abstraktion
|
||
"""
|
||
|
||
from typing import Optional, Type, Any
|
||
|
||
|
||
# ---------------------------------------------------------
|
||
# Hilfsfunktionen
|
||
# ---------------------------------------------------------
|
||
|
||
def getattr_safe(obj: Any, name: str, default: Any = None) -> Any:
|
||
"""
|
||
Sichere getattr-Variante:
|
||
- fängt Exceptions beim Attributzugriff ab
|
||
- liefert default zurück, wenn Attribut fehlt oder fehlschlägt
|
||
"""
|
||
try:
|
||
return getattr(obj, name)
|
||
except Exception:
|
||
return default
|
||
|
||
|
||
# ---------------------------------------------------------
|
||
# Qt‑Symbole (werden später dynamisch importiert)
|
||
# ---------------------------------------------------------
|
||
|
||
QMessageBox: Optional[Type[Any]] = None
|
||
QFileDialog: Optional[Type[Any]] = None
|
||
QEventLoop: Optional[Type[Any]] = None
|
||
QUrl: Optional[Type[Any]] = None
|
||
QNetworkRequest: Optional[Type[Any]] = None
|
||
QNetworkReply: Optional[Type[Any]] = None
|
||
QCoreApplication: Optional[Type[Any]] = None
|
||
|
||
QWidget: Type[Any]
|
||
QGridLayout: Type[Any]
|
||
QLabel: Type[Any]
|
||
QLineEdit: Type[Any]
|
||
QGroupBox: Type[Any]
|
||
QVBoxLayout: Type[Any]
|
||
QPushButton: Type[Any]
|
||
|
||
YES: Optional[Any] = None
|
||
NO: Optional[Any] = None
|
||
CANCEL: Optional[Any] = None
|
||
ICON_QUESTION: Optional[Any] = None
|
||
|
||
|
||
def exec_dialog(dialog: Any) -> Any:
|
||
raise NotImplementedError
|
||
|
||
|
||
# ---------------------------------------------------------
|
||
# QGIS‑Symbole (werden später dynamisch importiert)
|
||
# ---------------------------------------------------------
|
||
|
||
QgsProject: Optional[Type[Any]] = None
|
||
QgsVectorLayer: Optional[Type[Any]] = None
|
||
QgsNetworkAccessManager: Optional[Type[Any]] = None
|
||
Qgis: Optional[Type[Any]] = None
|
||
iface: Optional[Any] = None
|
||
|
||
|
||
# ---------------------------------------------------------
|
||
# Qt‑Versionserkennung
|
||
# ---------------------------------------------------------
|
||
|
||
QT_VERSION = 0 # 0 = Mock, 5 = PyQt5, 6 = PyQt6
|
||
|
||
|
||
# ---------------------------------------------------------
|
||
# Versuch: PyQt6 importieren
|
||
# ---------------------------------------------------------
|
||
|
||
try:
|
||
from PyQt6.QtWidgets import ( #type: ignore
|
||
QMessageBox as _QMessageBox,
|
||
QFileDialog as _QFileDialog,
|
||
QWidget as _QWidget,
|
||
QGridLayout as _QGridLayout,
|
||
QLabel as _QLabel,
|
||
QLineEdit as _QLineEdit,
|
||
QGroupBox as _QGroupBox,
|
||
QVBoxLayout as _QVBoxLayout,
|
||
QPushButton as _QPushButton,
|
||
)
|
||
from PyQt6.QtCore import ( #type: ignore
|
||
Qt,
|
||
QEventLoop as _QEventLoop,
|
||
QUrl as _QUrl,
|
||
QCoreApplication as _QCoreApplication,
|
||
)
|
||
from PyQt6.QtNetwork import ( #type: ignore
|
||
QNetworkRequest as _QNetworkRequest,
|
||
QNetworkReply as _QNetworkReply,
|
||
)
|
||
|
||
QMessageBox = _QMessageBox
|
||
QFileDialog = _QFileDialog
|
||
QEventLoop = _QEventLoop
|
||
QUrl = _QUrl
|
||
QNetworkRequest = _QNetworkRequest
|
||
QNetworkReply = _QNetworkReply
|
||
QCoreApplication = _QCoreApplication
|
||
|
||
QWidget = _QWidget
|
||
QGridLayout = _QGridLayout
|
||
QLabel = _QLabel
|
||
QLineEdit = _QLineEdit
|
||
QGroupBox = _QGroupBox
|
||
QVBoxLayout = _QVBoxLayout
|
||
QPushButton = _QPushButton
|
||
|
||
if QMessageBox is not None:
|
||
YES = QMessageBox.StandardButton.Yes
|
||
NO = QMessageBox.StandardButton.No
|
||
CANCEL = QMessageBox.StandardButton.Cancel
|
||
ICON_QUESTION = QMessageBox.Icon.Question
|
||
|
||
QT_VERSION = 6
|
||
|
||
def exec_dialog(dialog: Any) -> Any:
|
||
return dialog.exec()
|
||
|
||
# ---------------------------------------------------------
|
||
# Versuch: PyQt5 importieren
|
||
# ---------------------------------------------------------
|
||
|
||
except Exception:
|
||
try:
|
||
from PyQt5.QtWidgets import (
|
||
QMessageBox as _QMessageBox,
|
||
QFileDialog as _QFileDialog,
|
||
QWidget as _QWidget,
|
||
QGridLayout as _QGridLayout,
|
||
QLabel as _QLabel,
|
||
QLineEdit as _QLineEdit,
|
||
QGroupBox as _QGroupBox,
|
||
QVBoxLayout as _QVBoxLayout,
|
||
QPushButton as _QPushButton,
|
||
)
|
||
from PyQt5.QtCore import (
|
||
Qt,
|
||
QEventLoop as _QEventLoop,
|
||
QUrl as _QUrl,
|
||
QCoreApplication as _QCoreApplication,
|
||
)
|
||
from PyQt5.QtNetwork import (
|
||
QNetworkRequest as _QNetworkRequest,
|
||
QNetworkReply as _QNetworkReply,
|
||
)
|
||
|
||
QMessageBox = _QMessageBox
|
||
QFileDialog = _QFileDialog
|
||
QEventLoop = _QEventLoop
|
||
QUrl = _QUrl
|
||
QNetworkRequest = _QNetworkRequest
|
||
QNetworkReply = _QNetworkReply
|
||
QCoreApplication = _QCoreApplication
|
||
|
||
QWidget = _QWidget
|
||
QGridLayout = _QGridLayout
|
||
QLabel = _QLabel
|
||
QLineEdit = _QLineEdit
|
||
QGroupBox = _QGroupBox
|
||
QVBoxLayout = _QVBoxLayout
|
||
QPushButton = _QPushButton
|
||
|
||
if QMessageBox is not None:
|
||
YES = QMessageBox.Yes
|
||
NO = QMessageBox.No
|
||
CANCEL = QMessageBox.Cancel
|
||
ICON_QUESTION = QMessageBox.Question
|
||
|
||
QT_VERSION = 5
|
||
|
||
def exec_dialog(dialog: Any) -> Any:
|
||
return dialog.exec_()
|
||
|
||
# ---------------------------------------------------------
|
||
# Mock‑Modus (kein Qt verfügbar)
|
||
# ---------------------------------------------------------
|
||
|
||
except Exception:
|
||
QT_VERSION = 0
|
||
|
||
class FakeEnum(int):
|
||
"""OR‑fähiger Enum‑Ersatz für Mock‑Modus."""
|
||
|
||
def __new__(cls, value: int):
|
||
return int.__new__(cls, value)
|
||
|
||
def __or__(self, other: "FakeEnum") -> "FakeEnum":
|
||
return FakeEnum(int(self) | int(other))
|
||
|
||
class _MockQMessageBox:
|
||
Yes = FakeEnum(1)
|
||
No = FakeEnum(2)
|
||
Cancel = FakeEnum(4)
|
||
Question = FakeEnum(8)
|
||
|
||
QMessageBox = _MockQMessageBox
|
||
|
||
class _MockQFileDialog:
|
||
@staticmethod
|
||
def getOpenFileName(*args, **kwargs):
|
||
return ("", "")
|
||
|
||
@staticmethod
|
||
def getSaveFileName(*args, **kwargs):
|
||
return ("", "")
|
||
|
||
QFileDialog = _MockQFileDialog
|
||
|
||
class _MockQEventLoop:
|
||
def exec(self) -> int:
|
||
return 0
|
||
|
||
def quit(self) -> None:
|
||
pass
|
||
|
||
QEventLoop = _MockQEventLoop
|
||
|
||
class _MockQUrl(str):
|
||
def isValid(self) -> bool:
|
||
return True
|
||
|
||
QUrl = _MockQUrl
|
||
|
||
class _MockQNetworkRequest:
|
||
def __init__(self, url: Any):
|
||
self.url = url
|
||
|
||
QNetworkRequest = _MockQNetworkRequest
|
||
|
||
class _MockQNetworkReply:
|
||
class NetworkError:
|
||
NoError = 0
|
||
|
||
def __init__(self):
|
||
self._data = b""
|
||
|
||
def error(self) -> int:
|
||
return 0
|
||
|
||
def errorString(self) -> str:
|
||
return ""
|
||
|
||
def attribute(self, *args, **kwargs) -> Any:
|
||
return 200
|
||
|
||
def readAll(self) -> bytes:
|
||
return self._data
|
||
|
||
def deleteLater(self) -> None:
|
||
pass
|
||
|
||
QNetworkReply = _MockQNetworkReply
|
||
|
||
YES = FakeEnum(1)
|
||
NO = FakeEnum(2)
|
||
CANCEL = FakeEnum(4)
|
||
ICON_QUESTION = FakeEnum(8)
|
||
|
||
def exec_dialog(dialog: Any) -> Any:
|
||
return YES
|
||
|
||
class _MockWidget:
|
||
def __init__(self, *args, **kwargs):
|
||
pass
|
||
|
||
class _MockLayout:
|
||
def __init__(self, *args, **kwargs):
|
||
pass
|
||
|
||
def addWidget(self, *args, **kwargs):
|
||
pass
|
||
|
||
def addLayout(self, *args, **kwargs):
|
||
pass
|
||
|
||
def addStretch(self, *args, **kwargs):
|
||
pass
|
||
|
||
def setLayout(self, *args, **kwargs):
|
||
pass
|
||
|
||
class _MockLabel:
|
||
def __init__(self, text: str = ""):
|
||
self._text = text
|
||
|
||
class _MockLineEdit:
|
||
def __init__(self, *args, **kwargs):
|
||
self._text = ""
|
||
|
||
def text(self) -> str:
|
||
return self._text
|
||
|
||
def setText(self, value: str) -> None:
|
||
self._text = value
|
||
|
||
class _MockButton:
|
||
def __init__(self, *args, **kwargs):
|
||
# einfache Attr für Kompatibilität mit Qt-Signal-Syntax
|
||
self.clicked = lambda *a, **k: None
|
||
|
||
def connect(self, *args, **kwargs):
|
||
pass
|
||
|
||
QWidget = _MockWidget
|
||
QGridLayout = _MockLayout
|
||
QLabel = _MockLabel
|
||
QLineEdit = _MockLineEdit
|
||
QGroupBox = _MockWidget
|
||
QVBoxLayout = _MockLayout
|
||
QPushButton = _MockButton
|
||
|
||
# Kein echtes QCoreApplication im Mock
|
||
QCoreApplication = None
|
||
|
||
|
||
# ---------------------------------------------------------
|
||
# QGIS‑Imports
|
||
# ---------------------------------------------------------
|
||
|
||
try:
|
||
from qgis.core import (
|
||
QgsProject as _QgsProject,
|
||
QgsVectorLayer as _QgsVectorLayer,
|
||
QgsNetworkAccessManager as _QgsNetworkAccessManager,
|
||
Qgis as _Qgis,
|
||
)
|
||
from qgis.utils import iface as _iface
|
||
|
||
QgsProject = _QgsProject
|
||
QgsVectorLayer = _QgsVectorLayer
|
||
QgsNetworkAccessManager = _QgsNetworkAccessManager
|
||
Qgis = _Qgis
|
||
iface = _iface
|
||
|
||
QGIS_AVAILABLE = True
|
||
|
||
except Exception:
|
||
QGIS_AVAILABLE = False
|
||
|
||
class _MockQgsProject:
|
||
@staticmethod
|
||
def instance() -> "_MockQgsProject":
|
||
return _MockQgsProject()
|
||
|
||
def __init__(self):
|
||
self._variables = {}
|
||
|
||
def read(self) -> bool:
|
||
return True
|
||
|
||
QgsProject = _MockQgsProject
|
||
|
||
class _MockQgsVectorLayer:
|
||
def __init__(self, *args, **kwargs):
|
||
self._valid = True
|
||
|
||
def isValid(self) -> bool:
|
||
return self._valid
|
||
|
||
def loadNamedStyle(self, path: str):
|
||
return True, ""
|
||
|
||
def triggerRepaint(self) -> None:
|
||
pass
|
||
|
||
QgsVectorLayer = _MockQgsVectorLayer
|
||
|
||
class _MockQgsNetworkAccessManager:
|
||
def head(self, request: Any) -> _MockQNetworkReply:
|
||
return _MockQNetworkReply()
|
||
|
||
QgsNetworkAccessManager = _MockQgsNetworkAccessManager
|
||
|
||
class _MockQgis:
|
||
class MessageLevel:
|
||
Success = 0
|
||
Info = 1
|
||
Warning = 2
|
||
Critical = 3
|
||
|
||
Qgis = _MockQgis
|
||
|
||
class FakeIface:
|
||
class FakeMessageBar:
|
||
def pushMessage(self, title, text, level=0, duration=5):
|
||
return {"title": title, "text": text, "level": level, "duration": duration}
|
||
|
||
def messageBar(self):
|
||
return self.FakeMessageBar()
|
||
|
||
def mainWindow(self):
|
||
return None
|
||
|
||
iface = FakeIface()
|
||
|
||
|
||
# ---------------------------------------------------------
|
||
# Message‑Funktionen
|
||
# ---------------------------------------------------------
|
||
|
||
def _get_message_bar():
|
||
if iface is not None:
|
||
bar_attr = getattr_safe(iface, "messageBar")
|
||
if callable(bar_attr):
|
||
try:
|
||
return bar_attr()
|
||
except Exception:
|
||
pass
|
||
|
||
class _MockMessageBar:
|
||
def pushMessage(self, title, text, level=0, duration=5):
|
||
return {
|
||
"title": title,
|
||
"text": text,
|
||
"level": level,
|
||
"duration": duration,
|
||
}
|
||
|
||
return _MockMessageBar()
|
||
|
||
|
||
def push_message(level, title, text, duration=5, parent=None):
|
||
bar = _get_message_bar()
|
||
push = getattr_safe(bar, "pushMessage")
|
||
if callable(push):
|
||
return push(title, text, level=level, duration=duration)
|
||
return None
|
||
|
||
|
||
def info(title, text, duration=5):
|
||
level = Qgis.MessageLevel.Info if Qgis is not None else 1
|
||
return push_message(level, title, text, duration)
|
||
|
||
|
||
def warning(title, text, duration=5):
|
||
level = Qgis.MessageLevel.Warning if Qgis is not None else 2
|
||
return push_message(level, title, text, duration)
|
||
|
||
|
||
def error(title, text, duration=5):
|
||
level = Qgis.MessageLevel.Critical if Qgis is not None else 3
|
||
return push_message(level, title, text, duration)
|
||
|
||
|
||
def success(title, text, duration=5):
|
||
level = Qgis.MessageLevel.Success if Qgis is not None else 0
|
||
return push_message(level, title, text, duration)
|
||
|
||
# ---------------------------------------------------------
|
||
# Dialog‑Interaktionen
|
||
# ---------------------------------------------------------
|
||
|
||
def ask_yes_no(
|
||
title: str,
|
||
message: str,
|
||
default: bool = False,
|
||
parent: Any = None,
|
||
) -> bool:
|
||
"""
|
||
Fragt den Benutzer eine Ja/Nein‑Frage.
|
||
|
||
- In QGIS/Qt: zeigt einen QMessageBox‑Dialog
|
||
- Im Mock/Test‑Modus: gibt default zurück
|
||
"""
|
||
if QMessageBox is None:
|
||
return default
|
||
|
||
try:
|
||
buttons = YES | NO
|
||
result = QMessageBox.question(
|
||
parent,
|
||
title,
|
||
message,
|
||
buttons,
|
||
YES if default else NO,
|
||
)
|
||
return result == YES
|
||
except Exception:
|
||
return default
|
||
|
||
|
||
# ---------------------------------------------------------
|
||
# Variablen‑Wrapper
|
||
# ---------------------------------------------------------
|
||
|
||
try:
|
||
from qgis.core import QgsExpressionContextUtils
|
||
|
||
_HAS_QGIS_VARIABLES = True
|
||
except Exception:
|
||
_HAS_QGIS_VARIABLES = False
|
||
|
||
class _MockVariableStore:
|
||
global_vars: dict[str, str] = {}
|
||
project_vars: dict[str, str] = {}
|
||
|
||
class QgsExpressionContextUtils:
|
||
@staticmethod
|
||
def setGlobalVariable(name: str, value: str) -> None:
|
||
_MockVariableStore.global_vars[name] = value
|
||
|
||
@staticmethod
|
||
def globalScope():
|
||
class _Scope:
|
||
def variable(self, name: str) -> str:
|
||
return _MockVariableStore.global_vars.get(name, "")
|
||
|
||
return _Scope()
|
||
|
||
@staticmethod
|
||
def setProjectVariable(project: Any, name: str, value: str) -> None:
|
||
_MockVariableStore.project_vars[name] = value
|
||
|
||
@staticmethod
|
||
def projectScope(project: Any):
|
||
class _Scope:
|
||
def variable(self, name: str) -> str:
|
||
return _MockVariableStore.project_vars.get(name, "")
|
||
|
||
return _Scope()
|
||
|
||
|
||
def get_variable(key: str, scope: str = "project") -> str:
|
||
var_name = f"sn_{key}"
|
||
|
||
if scope == "project":
|
||
if QgsProject is not None:
|
||
projekt = QgsProject.instance()
|
||
else:
|
||
projekt = None # type: ignore[assignment]
|
||
return QgsExpressionContextUtils.projectScope(projekt).variable(var_name) or ""
|
||
|
||
if scope == "global":
|
||
return QgsExpressionContextUtils.globalScope().variable(var_name) or ""
|
||
|
||
raise ValueError("Scope muss 'project' oder 'global' sein.")
|
||
|
||
|
||
def set_variable(key: str, value: str, scope: str = "project") -> None:
|
||
var_name = f"sn_{key}"
|
||
|
||
if scope == "project":
|
||
if QgsProject is not None:
|
||
projekt = QgsProject.instance()
|
||
else:
|
||
projekt = None # type: ignore[assignment]
|
||
QgsExpressionContextUtils.setProjectVariable(projekt, var_name, value)
|
||
return
|
||
|
||
if scope == "global":
|
||
QgsExpressionContextUtils.setGlobalVariable(var_name, value)
|
||
return
|
||
|
||
raise ValueError("Scope muss 'project' oder 'global' sein.")
|
||
|
||
|
||
# ---------------------------------------------------------
|
||
# syswrapper Lazy‑Import
|
||
# ---------------------------------------------------------
|
||
|
||
def _sys():
|
||
from sn_basis.functions import syswrapper
|
||
return syswrapper
|
||
|
||
|
||
# ---------------------------------------------------------
|
||
# Style‑Funktion
|
||
# ---------------------------------------------------------
|
||
|
||
def apply_style(layer, style_name: str) -> bool:
|
||
if layer is None:
|
||
return False
|
||
|
||
is_valid_attr = getattr_safe(layer, "isValid")
|
||
if not callable(is_valid_attr) or not is_valid_attr():
|
||
return False
|
||
|
||
sys = _sys()
|
||
base_dir = sys.get_plugin_root()
|
||
style_path = sys.join_path(base_dir, "styles", style_name)
|
||
|
||
if not sys.file_exists(style_path):
|
||
return False
|
||
|
||
try:
|
||
ok, error_msg = layer.loadNamedStyle(style_path)
|
||
except Exception:
|
||
return False
|
||
|
||
if not ok:
|
||
return False
|
||
|
||
try:
|
||
trigger = getattr_safe(layer, "triggerRepaint")
|
||
if callable(trigger):
|
||
trigger()
|
||
except Exception:
|
||
pass
|
||
|
||
return True
|
||
|
||
|
||
# ---------------------------------------------------------
|
||
# Layer‑Wrapper
|
||
# ---------------------------------------------------------
|
||
|
||
def layer_exists(layer) -> bool:
|
||
if layer is None:
|
||
return False
|
||
|
||
# Mock/Wrapper-Attribut
|
||
is_valid_flag = getattr_safe(layer, "is_valid")
|
||
if is_valid_flag is not None:
|
||
try:
|
||
return bool(is_valid_flag)
|
||
except Exception:
|
||
return False
|
||
|
||
try:
|
||
is_valid_attr = getattr_safe(layer, "isValid")
|
||
if callable(is_valid_attr):
|
||
return bool(is_valid_attr())
|
||
return True
|
||
except Exception:
|
||
return False
|
||
|
||
|
||
def get_layer_geometry_type(layer) -> str:
|
||
if layer is None:
|
||
return "None"
|
||
|
||
geometry_type_attr = getattr_safe(layer, "geometry_type")
|
||
if geometry_type_attr is not None:
|
||
return str(geometry_type_attr)
|
||
|
||
try:
|
||
is_spatial_attr = getattr_safe(layer, "isSpatial")
|
||
if callable(is_spatial_attr) and not is_spatial_attr():
|
||
return "None"
|
||
|
||
geometry_type_qgis = getattr_safe(layer, "geometryType")
|
||
if callable(geometry_type_qgis):
|
||
gtype = geometry_type_qgis()
|
||
if gtype == 0:
|
||
return "Point"
|
||
if gtype == 1:
|
||
return "LineString"
|
||
if gtype == 2:
|
||
return "Polygon"
|
||
return "None"
|
||
|
||
return "None"
|
||
except Exception:
|
||
return "None"
|
||
|
||
|
||
def get_layer_feature_count(layer) -> int:
|
||
if layer is None:
|
||
return 0
|
||
|
||
feature_count_attr = getattr_safe(layer, "feature_count")
|
||
if feature_count_attr is not None:
|
||
try:
|
||
return int(feature_count_attr)
|
||
except Exception:
|
||
return 0
|
||
|
||
try:
|
||
is_spatial_attr = getattr_safe(layer, "isSpatial")
|
||
if callable(is_spatial_attr) and not is_spatial_attr():
|
||
return 0
|
||
|
||
feature_count_qgis = getattr_safe(layer, "featureCount")
|
||
if callable(feature_count_qgis):
|
||
return int(feature_count_qgis())
|
||
|
||
return 0
|
||
except Exception:
|
||
return 0
|
||
|
||
|
||
def is_layer_visible(layer) -> bool:
|
||
if layer is None:
|
||
return False
|
||
|
||
visible_attr = getattr_safe(layer, "visible")
|
||
if visible_attr is not None:
|
||
try:
|
||
return bool(visible_attr)
|
||
except Exception:
|
||
return False
|
||
|
||
try:
|
||
is_visible_attr = getattr_safe(layer, "isVisible")
|
||
if callable(is_visible_attr):
|
||
return bool(is_visible_attr())
|
||
|
||
tree_layer_attr = getattr_safe(layer, "treeLayer")
|
||
if callable(tree_layer_attr):
|
||
node = tree_layer_attr()
|
||
else:
|
||
node = tree_layer_attr
|
||
|
||
if node is not None:
|
||
node_visible_attr = getattr_safe(node, "isVisible")
|
||
if callable(node_visible_attr):
|
||
return bool(node_visible_attr())
|
||
|
||
return False
|
||
except Exception:
|
||
return False
|
||
|
||
def set_layer_visible(layer, visible: bool) -> bool:
|
||
"""
|
||
Setzt die Sichtbarkeit eines Layers.
|
||
|
||
Unterstützt:
|
||
- Mock-/Wrapper-Attribute (layer.visible)
|
||
- QGIS-LayerTreeNode (treeLayer().setItemVisibilityChecked)
|
||
- Fallbacks ohne Exception-Wurf
|
||
|
||
Gibt True zurück, wenn die Sichtbarkeit gesetzt werden konnte.
|
||
"""
|
||
if layer is None:
|
||
return False
|
||
|
||
# 1️⃣ Mock / Wrapper-Attribut
|
||
try:
|
||
if hasattr(layer, "visible"):
|
||
layer.visible = bool(visible)
|
||
return True
|
||
except Exception:
|
||
pass
|
||
|
||
# 2️⃣ QGIS: LayerTreeNode
|
||
try:
|
||
tree_layer_attr = getattr_safe(layer, "treeLayer")
|
||
node = tree_layer_attr() if callable(tree_layer_attr) else tree_layer_attr
|
||
|
||
if node is not None:
|
||
set_visible = getattr_safe(node, "setItemVisibilityChecked")
|
||
if callable(set_visible):
|
||
set_visible(bool(visible))
|
||
return True
|
||
except Exception:
|
||
pass
|
||
|
||
# 3️⃣ QGIS-Fallback: setVisible (selten, aber vorhanden)
|
||
try:
|
||
set_visible_attr = getattr_safe(layer, "setVisible")
|
||
if callable(set_visible_attr):
|
||
set_visible_attr(bool(visible))
|
||
return True
|
||
except Exception:
|
||
pass
|
||
|
||
return False
|
||
|
||
|
||
def get_layer_type(layer) -> str:
|
||
if layer is None:
|
||
return "unknown"
|
||
|
||
layer_type_attr = getattr_safe(layer, "layer_type")
|
||
if layer_type_attr is not None:
|
||
return str(layer_type_attr)
|
||
|
||
try:
|
||
is_spatial_attr = getattr_safe(layer, "isSpatial")
|
||
if callable(is_spatial_attr):
|
||
return "vector" if is_spatial_attr() else "table"
|
||
|
||
data_provider_attr = getattr_safe(layer, "dataProvider")
|
||
raster_type_attr = getattr_safe(layer, "rasterType")
|
||
if data_provider_attr is not None and raster_type_attr is not None:
|
||
return "raster"
|
||
|
||
return "unknown"
|
||
except Exception:
|
||
return "unknown"
|
||
|
||
|
||
def get_layer_crs(layer) -> str:
|
||
if layer is None:
|
||
return "None"
|
||
|
||
crs_attr_direct = getattr_safe(layer, "crs")
|
||
if crs_attr_direct is not None and not callable(crs_attr_direct):
|
||
# direkter Attributzugriff (z. B. im Mock)
|
||
return str(crs_attr_direct)
|
||
|
||
try:
|
||
crs_callable = getattr_safe(layer, "crs")
|
||
if callable(crs_callable):
|
||
crs = crs_callable()
|
||
authid_attr = getattr_safe(crs, "authid")
|
||
if callable(authid_attr):
|
||
return authid_attr() or "None"
|
||
return "None"
|
||
except Exception:
|
||
return "None"
|
||
|
||
|
||
def get_layer_fields(layer) -> list[str]:
|
||
if layer is None:
|
||
return []
|
||
|
||
# direkter Attributzugriff (Mock / Wrapper)
|
||
fields_attr_direct = getattr_safe(layer, "fields")
|
||
if fields_attr_direct is not None and not callable(fields_attr_direct):
|
||
try:
|
||
# direkter Iterable oder Mapping von Namen
|
||
if hasattr(fields_attr_direct, "__iter__") and not isinstance(
|
||
fields_attr_direct, (str, bytes)
|
||
):
|
||
return list(fields_attr_direct)
|
||
except Exception:
|
||
return []
|
||
|
||
try:
|
||
fields_callable = getattr_safe(layer, "fields")
|
||
if callable(fields_callable):
|
||
fields = fields_callable()
|
||
|
||
# QGIS: QgsFields.names()
|
||
names_attr = getattr_safe(fields, "names")
|
||
if callable(names_attr):
|
||
return list(names_attr())
|
||
|
||
# Fallback: iterierbar?
|
||
if hasattr(fields, "__iter__") and not isinstance(fields, (str, bytes)):
|
||
return list(fields)
|
||
|
||
return []
|
||
except Exception:
|
||
return []
|
||
|
||
|
||
def get_layer_source(layer) -> str:
|
||
if layer is None:
|
||
return "None"
|
||
|
||
source_attr_direct = getattr_safe(layer, "source")
|
||
if source_attr_direct is not None and not callable(source_attr_direct):
|
||
return str(source_attr_direct)
|
||
|
||
try:
|
||
source_callable = getattr_safe(layer, "source")
|
||
if callable(source_callable):
|
||
return source_callable() or "None"
|
||
return "None"
|
||
except Exception:
|
||
return "None"
|
||
|
||
|
||
def is_layer_editable(layer) -> bool:
|
||
if layer is None:
|
||
return False
|
||
|
||
editable_attr = getattr_safe(layer, "editable")
|
||
if editable_attr is not None:
|
||
try:
|
||
return bool(editable_attr)
|
||
except Exception:
|
||
return False
|
||
|
||
try:
|
||
editable_callable = getattr_safe(layer, "isEditable")
|
||
if callable(editable_callable):
|
||
return bool(editable_callable())
|
||
return False
|
||
except Exception:
|
||
return False
|