""" sn_basis/functions/qgiscore_wrapper.py – zentrale QGIS-Core-Abstraktion """ from typing import Type, Any, Optional from sn_basis.functions.qt_wrapper import ( QUrl, QEventLoop, QNetworkRequest, ) # --------------------------------------------------------- # QGIS-Symbole (werden dynamisch gesetzt) # --------------------------------------------------------- QgsProject: Type[Any] QgsVectorLayer: Type[Any] QgsRasterLayer: Type[Any] QgsNetworkAccessManager: Type[Any] Qgis: Type[Any] QgsMapLayerProxyModel: Type[Any] QgsVectorFileWriter: Type[Any] # neu: Schreib-API QgsFeature: Type[Any] QgsField: Type[Any] QgsGeometry: Type[Any] QgsFeatureRequest: Type[Any] QgsRectangle: Type[Any] QgsCoordinateTransform: Type[Any] QgsCoordinateReferenceSystem: Type[Any] QgsPrintLayout: Type[Any] QgsLayoutItemMap: Type[Any] QgsLayoutItemMapGrid: Type[Any] QgsTextFormat: Type[Any] QgsTextBackgroundSettings: Type[Any] QgsLayoutItemLabel: Type[Any] QgsLayoutItemLegend: Type[Any] QgsLayoutPoint: Type[Any] QgsLayoutSize: Type[Any] QgsUnitTypes: Type[Any] QgsLayoutItem: Type[Any] QgsBlockingNetworkRequest: Type[Any] QgsJsonExporter: Type[Any] QgsJsonUtils: Type[Any] GEOM_POINT: Any = None GEOM_LINE: Any = None GEOM_POLYGON: Any = None MAP_GRID_STYLE_MARKERS: Any = None MAP_GRID_STYLE_FRAME_ANNOTATIONS: Any = None MAP_GRID_FRAME_STYLE_INTERIOR_TICKS: Any = None MAP_GRID_UNIT_MAP_UNIT: Any = None MAP_GRID_ANNOTATION_FORMAT_DECIMAL: Any = None MAP_GRID_ANNOTATION_DISPLAY_HIDE_ALL: Any = None MAP_GRID_ANNOTATION_DISPLAY_SHOW_ALL: Any = None MAP_GRID_ANNOTATION_DISPLAY_SHOW_X: Any = None MAP_GRID_ANNOTATION_DISPLAY_SHOW_Y: Any = None MAP_GRID_BORDER_LEFT: Any = None MAP_GRID_BORDER_RIGHT: Any = None MAP_GRID_BORDER_TOP: Any = None MAP_GRID_BORDER_BOTTOM: Any = None MAP_GRID_ANNOTATION_POSITION_INSIDE_FRAME: Any = None TEXT_BACKGROUND_SHAPE_RECTANGLE: Any = None TEXT_BACKGROUND_SIZE_BUFFER: Any = None TEXT_RENDER_UNIT_MILLIMETERS: Any = None TEXT_RENDER_UNIT_POINTS: Any = None QGIS_AVAILABLE = False # --------------------------------------------------------- # Versuch: QGIS-Core importieren # --------------------------------------------------------- try: from qgis.core import ( QgsProject as _QgsProject, QgsVectorLayer as _QgsVectorLayer, QgsRasterLayer as _QgsRasterLayer, QgsNetworkAccessManager as _QgsNetworkAccessManager, Qgis as _Qgis, QgsMapLayerProxyModel as _QgsMaplLayerProxyModel, QgsVectorFileWriter as _QgsVectorFileWriter, QgsFeature as _QgsFeature, QgsField as _QgsField, QgsGeometry as _QgsGeometry, QgsFeatureRequest as _QgsFeatureRequest, QgsRectangle as _QgsRectangle, QgsCoordinateTransform as _QgsCoordinateTransform, QgsCoordinateReferenceSystem as _QgsCoordinateReferenceSystem, QgsPrintLayout as _QgsPrintLayout, QgsLayoutItemMap as _QgsLayoutItemMap, QgsLayoutItemMapGrid as _QgsLayoutItemMapGrid, QgsTextFormat as _QgsTextFormat, QgsTextBackgroundSettings as _QgsTextBackgroundSettings, QgsLayoutItemLabel as _QgsLayoutItemLabel, QgsLayoutItemLegend as _QgsLayoutItemLegend, QgsLayoutPoint as _QgsLayoutPoint, QgsLayoutSize as _QgsLayoutSize, QgsUnitTypes as _QgsUnitTypes, QgsLayoutItem as _QgsLayoutItem, QgsBlockingNetworkRequest as _QgsBlockingNetworkRequest, QgsJsonExporter as _QgsJsonExporter, QgsJsonUtils as _QgsJsonUtils, ) QgsProject = _QgsProject QgsVectorLayer = _QgsVectorLayer QgsRasterLayer = _QgsRasterLayer QgsNetworkAccessManager = _QgsNetworkAccessManager Qgis = _Qgis QgsMapLayerProxyModel = _QgsMaplLayerProxyModel QgsVectorFileWriter = _QgsVectorFileWriter QgsFeature = _QgsFeature QgsField = _QgsField QgsGeometry = _QgsGeometry QgsFeatureRequest = _QgsFeatureRequest QgsRectangle = _QgsRectangle QgsCoordinateTransform = _QgsCoordinateTransform QgsCoordinateReferenceSystem = _QgsCoordinateReferenceSystem QgsPrintLayout = _QgsPrintLayout QgsLayoutItemMap = _QgsLayoutItemMap QgsLayoutItemMapGrid = _QgsLayoutItemMapGrid QgsTextFormat = _QgsTextFormat QgsTextBackgroundSettings = _QgsTextBackgroundSettings QgsLayoutItemLabel = _QgsLayoutItemLabel QgsLayoutItemLegend = _QgsLayoutItemLegend QgsLayoutPoint = _QgsLayoutPoint QgsLayoutSize = _QgsLayoutSize QgsUnitTypes = _QgsUnitTypes QgsLayoutItem = _QgsLayoutItem QgsBlockingNetworkRequest = _QgsBlockingNetworkRequest QgsJsonExporter = _QgsJsonExporter QgsJsonUtils = _QgsJsonUtils # Geometrietyp-Konstanten (QGIS 4 / QGIS 3.30+ vs. ältere QGIS-3) def _resolve_geom_type(*paths): for obj, *attrs in paths: val = obj for attr in attrs: val = getattr(val, attr, None) if val is None: break else: if val is not None: return val return None GEOM_POINT = _resolve_geom_type( (_Qgis, "GeometryType", "Point"), ) or getattr(__import__("qgis.core", fromlist=["QgsWkbTypes"]), "QgsWkbTypes", type("_", (), {"PointGeometry": 0})).PointGeometry GEOM_LINE = _resolve_geom_type( (_Qgis, "GeometryType", "Line"), ) or getattr(__import__("qgis.core", fromlist=["QgsWkbTypes"]), "QgsWkbTypes", type("_", (), {"LineGeometry": 1})).LineGeometry GEOM_POLYGON = _resolve_geom_type( (_Qgis, "GeometryType", "Polygon"), ) or getattr(__import__("qgis.core", fromlist=["QgsWkbTypes"]), "QgsWkbTypes", type("_", (), {"PolygonGeometry": 2})).PolygonGeometry def _resolve_qgis_enum(*paths: tuple[Any, ...]) -> Any: for path in paths: value: Any = path[0] ok = True for name in path[1:]: value = getattr(value, name, None) if value is None: ok = False break if ok: return value return None MAP_GRID_STYLE_MARKERS = _resolve_qgis_enum( (_Qgis, "MapGridStyle", "Markers"), (_QgsLayoutItemMapGrid, "Markers"), ) MAP_GRID_STYLE_FRAME_ANNOTATIONS = _resolve_qgis_enum( (_Qgis, "MapGridStyle", "FrameAndAnnotationsOnly"), (_QgsLayoutItemMapGrid, "FrameAndAnnotationsOnly"), (_Qgis, "MapGridStyle", "FrameAnnotations"), (_QgsLayoutItemMapGrid, "FrameAnnotations"), ) MAP_GRID_FRAME_STYLE_INTERIOR_TICKS = _resolve_qgis_enum( (_Qgis, "MapGridFrameStyle", "InteriorTicks"), (_QgsLayoutItemMapGrid, "InteriorTicks"), ) MAP_GRID_UNIT_MAP_UNIT = _resolve_qgis_enum( (_Qgis, "MapGridUnit", "MapUnits"), (_QgsLayoutItemMapGrid, "MapUnits"), (_Qgis, "MapGridUnit", "MapUnit"), (_QgsLayoutItemMapGrid, "MapUnit"), ) MAP_GRID_ANNOTATION_FORMAT_DECIMAL = _resolve_qgis_enum( (_Qgis, "MapGridAnnotationFormat", "Decimal"), (_QgsLayoutItemMapGrid, "Decimal"), ) MAP_GRID_ANNOTATION_DISPLAY_HIDE_ALL = _resolve_qgis_enum( (_Qgis, "MapGridComponentVisibility", "HideAll"), (_QgsLayoutItemMapGrid, "HideAll"), ) MAP_GRID_ANNOTATION_DISPLAY_SHOW_ALL = _resolve_qgis_enum( (_Qgis, "MapGridComponentVisibility", "ShowAll"), (_QgsLayoutItemMapGrid, "ShowAll"), ) MAP_GRID_ANNOTATION_DISPLAY_SHOW_X = _resolve_qgis_enum( (_Qgis, "MapGridComponentVisibility", "LongitudeOnly"), (_QgsLayoutItemMapGrid, "LongitudeOnly"), (_Qgis, "MapGridComponentVisibility", "ShowLongitudeOnly"), (_QgsLayoutItemMapGrid, "ShowLongitudeOnly"), ) MAP_GRID_ANNOTATION_DISPLAY_SHOW_Y = _resolve_qgis_enum( (_Qgis, "MapGridComponentVisibility", "LatitudeOnly"), (_QgsLayoutItemMapGrid, "LatitudeOnly"), (_Qgis, "MapGridComponentVisibility", "ShowLatitudeOnly"), (_QgsLayoutItemMapGrid, "ShowLatitudeOnly"), ) MAP_GRID_BORDER_LEFT = _resolve_qgis_enum( (_Qgis, "MapGridBorderSide", "Left"), (_QgsLayoutItemMapGrid, "Left"), ) MAP_GRID_BORDER_RIGHT = _resolve_qgis_enum( (_Qgis, "MapGridBorderSide", "Right"), (_QgsLayoutItemMapGrid, "Right"), ) MAP_GRID_BORDER_TOP = _resolve_qgis_enum( (_Qgis, "MapGridBorderSide", "Top"), (_QgsLayoutItemMapGrid, "Top"), ) MAP_GRID_BORDER_BOTTOM = _resolve_qgis_enum( (_Qgis, "MapGridBorderSide", "Bottom"), (_QgsLayoutItemMapGrid, "Bottom"), ) MAP_GRID_ANNOTATION_POSITION_INSIDE_FRAME = _resolve_qgis_enum( (_Qgis, "MapGridAnnotationPosition", "InsideMapFrame"), (_QgsLayoutItemMapGrid, "InsideMapFrame"), ) TEXT_BACKGROUND_SHAPE_RECTANGLE = _resolve_qgis_enum( (_QgsTextBackgroundSettings, "ShapeRectangle"), ) TEXT_BACKGROUND_SIZE_BUFFER = _resolve_qgis_enum( (_QgsTextBackgroundSettings, "SizeBuffer"), ) TEXT_RENDER_UNIT_MILLIMETERS = _resolve_qgis_enum( (_Qgis, "RenderUnit", "Millimeters"), (_QgsUnitTypes, "RenderMillimeters"), ) TEXT_RENDER_UNIT_POINTS = _resolve_qgis_enum( (_Qgis, "RenderUnit", "Points"), (_QgsUnitTypes, "RenderPoints"), ) QGIS_AVAILABLE = True # --------------------------------------------------------- # Mock-Modus # --------------------------------------------------------- except Exception: QGIS_AVAILABLE = False class _MockLayoutManager: def layoutByName(self, name: str): return None def addLayout(self, layout: Any) -> bool: return True class _MockQgsProject: def __init__(self): self._variables = {} self._layout_manager = _MockLayoutManager() @staticmethod def instance() -> "_MockQgsProject": return _MockQgsProject() def read(self) -> bool: return True def layoutManager(self): return self._layout_manager 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 def dataProvider(self): return None QgsVectorLayer = _MockQgsVectorLayer class _MockQgsNetworkAccessManager: @staticmethod def instance(): return _MockQgsNetworkAccessManager() def head(self, request: Any): return None class _MockQgsRasterLayer: """ Minimaler Mock für QgsRasterLayer, ausreichend für Tests und um im Datenabruf ein Raster-Layer-Objekt im pruef_ergebnis kontext mitzugeben. """ def __init__(self, source: str, name: str = "Raster", provider: str = "wms"): self.source = source self._name = name self.provider = provider self._valid = True def isValid(self) -> bool: return self._valid def name(self) -> str: return self._name def dataProvider(self): return None QgsRasterLayer = _MockQgsRasterLayer class _MockQgsPrintLayout: def __init__(self, project: Any): self.project = project self._name = "" self._page = _MockQgsLayoutPage() self.items: list[Any] = [] def initializeDefaults(self) -> None: pass def setName(self, name: str) -> None: self._name = name def pageCollection(self): return self def page(self, index: int): return self._page def addLayoutItem(self, item: Any) -> None: self.items.append(item) class _MockQgsLayoutPage: def setPageSize(self, size: Any) -> None: self.size = size class _MockQgsLayoutItem: class ReferencePoint: LowerLeft = 0 class _MockQgsLayoutItemMap: def __init__(self, layout: Any): self.layout = layout self.item_id = "" self.extent = None self.scale = None self.follow_visibility_preset = False self.follow_visibility_preset_name = "" self.keep_layer_set = False self._grids = _MockQgsLayoutItemMapGridStack(self) def setId(self, item_id: str) -> None: self.item_id = item_id def setExtent(self, extent: Any) -> None: self.extent = extent def setScale(self, scale: float) -> None: self.scale = scale def attemptMove(self, point: Any) -> None: pass def attemptResize(self, size: Any) -> None: pass def setFollowVisibilityPreset(self, active: bool) -> None: self.follow_visibility_preset = active def setFollowVisibilityPresetName(self, name: str) -> None: self.follow_visibility_preset_name = name def setKeepLayerSet(self, enabled: bool) -> None: self.keep_layer_set = enabled def grids(self): return self._grids def updateBoundingRect(self) -> None: pass def refresh(self) -> None: pass class _MockQgsLayoutItemMapGrid: Markers = "Markers" FrameAndAnnotationsOnly = "FrameAndAnnotationsOnly" InteriorTicks = "InteriorTicks" MapUnits = "MapUnits" Decimal = "Decimal" HideAll = "HideAll" ShowAll = "ShowAll" LongitudeOnly = "LongitudeOnly" LatitudeOnly = "LatitudeOnly" Left = "Left" Right = "Right" Top = "Top" Bottom = "Bottom" InsideMapFrame = "InsideMapFrame" def __init__(self, name: str, map_item: Any): self.name = name self.map_item = map_item self.enabled = False self.style = None self.units = None self.interval_x = None self.interval_y = None self.annotation_enabled = False self.annotation_format = None self.annotation_precision = None self.annotation_display: dict[Any, Any] = {} self.annotation_position: dict[Any, Any] = {} self.frame_style = None self.annotation_text_format = None def setEnabled(self, enabled: bool) -> None: self.enabled = enabled def setStyle(self, style: Any) -> None: self.style = style def setUnits(self, units: Any) -> None: self.units = units def setIntervalX(self, interval: float) -> None: self.interval_x = interval def setIntervalY(self, interval: float) -> None: self.interval_y = interval def setAnnotationEnabled(self, enabled: bool) -> None: self.annotation_enabled = enabled def setAnnotationFormat(self, annotation_format: Any) -> None: self.annotation_format = annotation_format def setAnnotationPrecision(self, precision: int) -> None: self.annotation_precision = precision def setAnnotationDisplay(self, display: Any, border: Any) -> None: self.annotation_display[border] = display def setAnnotationPosition(self, position: Any, border: Any) -> None: self.annotation_position[border] = position def setFrameStyle(self, frame_style: Any) -> None: self.frame_style = frame_style def setAnnotationTextFormat(self, annotation_text_format: Any) -> None: self.annotation_text_format = annotation_text_format class _MockQgsTextBackgroundSettings: ShapeRectangle = "ShapeRectangle" SizeBuffer = "SizeBuffer" def __init__(self): self.enabled = False self.shape_type = None self.size_type = None self.size = None self.size_unit = None self.offset = None self.offset_unit = None self.fill_color = None def setEnabled(self, enabled: bool) -> None: self.enabled = enabled def setType(self, shape_type: Any) -> None: self.shape_type = shape_type def setSizeType(self, size_type: Any) -> None: self.size_type = size_type def setSize(self, size: Any) -> None: self.size = size def setSizeUnit(self, unit: Any) -> None: self.size_unit = unit def setOffset(self, offset: Any) -> None: self.offset = offset def setOffsetUnit(self, unit: Any) -> None: self.offset_unit = unit def setFillColor(self, color: Any) -> None: self.fill_color = color class _MockQgsTextFormat: def __init__(self): self.font = None self.size = None self.size_unit = None self.background = None def setFont(self, font: Any) -> None: self.font = font def setSize(self, size: float) -> None: self.size = size def setSizeUnit(self, unit: Any) -> None: self.size_unit = unit def setBackground(self, background: Any) -> None: self.background = background class _MockQgsLayoutItemMapGridStack: def __init__(self, map_item: Any): self.map_item = map_item self._grids: list[_MockQgsLayoutItemMapGrid] = [] def addGrid(self, grid: Any) -> None: self._grids.append(grid) def removeGrid(self, grid_id: str) -> None: self._grids = [grid for grid in self._grids if getattr(grid, "name", "") != grid_id] def asList(self) -> list[Any]: return list(self._grids) class _MockQgsLayoutItemLabel: ModeHtml = 1 def __init__(self, layout: Any): self.layout = layout def setId(self, item_id: str) -> None: pass def setText(self, text: str) -> None: pass def setMode(self, mode: Any) -> None: pass def setFont(self, font: Any) -> None: pass def setReferencePoint(self, point: Any) -> None: pass def setVAlign(self, alignment: Any) -> None: pass def attemptMove(self, point: Any) -> None: pass def attemptResize(self, size: Any) -> None: pass class _MockLegendRootGroup: def __init__(self) -> None: self.layers: list[Any] = [] def clear(self) -> None: self.layers = [] def addLayer(self, layer: Any) -> None: self.layers.append(layer) class _MockLegendModel: def __init__(self) -> None: self._root_group = _MockLegendRootGroup() def rootGroup(self) -> _MockLegendRootGroup: return self._root_group class _MockQgsLayoutItemLegend: def __init__(self, layout: Any): self.layout = layout self._model = _MockLegendModel() self.item_id = "" self.object_name = "" self.linked_map = None self.auto_update_model = None self.legend_filter_by_map_enabled = None self.title = "" def setId(self, item_id: str) -> None: self.item_id = item_id def setObjectName(self, name: str) -> None: self.object_name = name def setLinkedMap(self, map_item: Any) -> None: self.linked_map = map_item def setAutoUpdateModel(self, enabled: bool) -> None: self.auto_update_model = enabled def setLegendFilterByMapEnabled(self, enabled: bool) -> None: self.legend_filter_by_map_enabled = enabled def setTitle(self, title: str) -> None: self.title = title def setReferencePoint(self, point: Any) -> None: pass def attemptMove(self, point: Any) -> None: pass def attemptResize(self, size: Any) -> None: pass def model(self) -> _MockLegendModel: return self._model def refresh(self) -> None: pass class _MockQgsLayoutPoint: def __init__(self, x: float, y: float, unit: Any): self.x = x self.y = y self.unit = unit class _MockQgsLayoutSize: def __init__(self, width: float, height: float, unit: Any): self.width = width self.height = height self.unit = unit class _MockQgsUnitTypes: LayoutMillimeters = 0 QgsPrintLayout = _MockQgsPrintLayout QgsLayoutItemMap = _MockQgsLayoutItemMap QgsLayoutItemMapGrid = _MockQgsLayoutItemMapGrid QgsTextFormat = _MockQgsTextFormat QgsTextBackgroundSettings = _MockQgsTextBackgroundSettings QgsLayoutItemLabel = _MockQgsLayoutItemLabel QgsLayoutItemLegend = _MockQgsLayoutItemLegend QgsLayoutPoint = _MockQgsLayoutPoint QgsLayoutSize = _MockQgsLayoutSize QgsUnitTypes = _MockQgsUnitTypes QgsLayoutItem = _MockQgsLayoutItem MAP_GRID_STYLE_MARKERS = _MockQgsLayoutItemMapGrid.Markers MAP_GRID_STYLE_FRAME_ANNOTATIONS = _MockQgsLayoutItemMapGrid.FrameAndAnnotationsOnly MAP_GRID_FRAME_STYLE_INTERIOR_TICKS = _MockQgsLayoutItemMapGrid.InteriorTicks MAP_GRID_UNIT_MAP_UNIT = _MockQgsLayoutItemMapGrid.MapUnits MAP_GRID_ANNOTATION_FORMAT_DECIMAL = _MockQgsLayoutItemMapGrid.Decimal MAP_GRID_ANNOTATION_DISPLAY_HIDE_ALL = _MockQgsLayoutItemMapGrid.HideAll MAP_GRID_ANNOTATION_DISPLAY_SHOW_ALL = _MockQgsLayoutItemMapGrid.ShowAll MAP_GRID_ANNOTATION_DISPLAY_SHOW_X = _MockQgsLayoutItemMapGrid.LongitudeOnly MAP_GRID_ANNOTATION_DISPLAY_SHOW_Y = _MockQgsLayoutItemMapGrid.LatitudeOnly MAP_GRID_BORDER_LEFT = _MockQgsLayoutItemMapGrid.Left MAP_GRID_BORDER_RIGHT = _MockQgsLayoutItemMapGrid.Right MAP_GRID_BORDER_TOP = _MockQgsLayoutItemMapGrid.Top MAP_GRID_BORDER_BOTTOM = _MockQgsLayoutItemMapGrid.Bottom MAP_GRID_ANNOTATION_POSITION_INSIDE_FRAME = _MockQgsLayoutItemMapGrid.InsideMapFrame TEXT_BACKGROUND_SHAPE_RECTANGLE = _MockQgsTextBackgroundSettings.ShapeRectangle TEXT_BACKGROUND_SIZE_BUFFER = _MockQgsTextBackgroundSettings.SizeBuffer TEXT_RENDER_UNIT_MILLIMETERS = "Millimeters" TEXT_RENDER_UNIT_POINTS = "Points" class _MockQgsFeature: def __init__(self): self._geometry = None self._attributes = [] def setGeometry(self, geometry: Any) -> None: self._geometry = geometry def setAttributes(self, attributes: list[Any]) -> None: self._attributes = attributes class _MockQgsField: def __init__(self, name: str = "", field_type: Any = None): self.name = name self.field_type = field_type class _MockQgsGeometry: @staticmethod def unaryUnion(_geometries: list[Any]): return None def isEmpty(self) -> bool: return True QgsFeature = _MockQgsFeature QgsField = _MockQgsField QgsGeometry = _MockQgsGeometry 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 class _MockQgis: class MessageLevel: Success = 0 Info = 1 Warning = 2 Critical = 3 Qgis = _MockQgis class _MockQgsMapLayerProxyModel: # Layer-Typen (entsprechen QGIS-Konstanten) NoLayer = 0 VectorLayer = 1 RasterLayer = 2 PluginLayer = 3 MeshLayer = 4 VectorTileLayer = 5 PointCloudLayer = 6 def __init__(self, *args, **kwargs): pass QgsMapLayerProxyModel = _MockQgsMapLayerProxyModel # --------------------------------------------------------- # Mock für QgsVectorFileWriter # --------------------------------------------------------- class _MockSaveVectorOptions: """ Minimaler Ersatz für QgsVectorFileWriter.SaveVectorOptions. Felder werden als einfache Attribute bereitgestellt. """ def __init__(self): self.driverName: str = "GPKG" self.layerName: Optional[str] = None self.fileEncoding: str = "UTF-8" # Action-Konstanten werden symbolisch verwendet self.actionOnExistingFile: Optional[int] = None class _MockQgsVectorFileWriter: """ Minimaler Mock für QgsVectorFileWriter mit der benötigten API: - SaveVectorOptions (als Klasse) - writeAsVectorFormatV3(layer, path, transformContext, options) -> error_code - NoError (Konstante) - CreateOrOverwriteFile / CreateOrOverwriteLayer (Konstanten) """ # Fehlerkonstanten (0 = NoError) NoError = 0 # Action-Konstanten (Werte nur symbolisch) CreateOrOverwriteFile = 1 CreateOrOverwriteLayer = 2 # SaveVectorOptions-Klasse SaveVectorOptions = _MockSaveVectorOptions @staticmethod def writeAsVectorFormatV3(layer: Any, path: str, transform_context: Any, options: Any) -> int: """ Mock-Schreibfunktion. Verhalten im Mock: - Wenn 'layer' None oder options.layerName fehlt, geben wir NoError zurück, aber schreiben nichts (Tests erwarten nur Rückgabecode). - Diese Implementierung versucht nicht, echte Dateien zu schreiben. - Rückgabewert: 0 (NoError) bei Erfolg, sonst eine positive Fehlernummer. """ try: # Sehr einfache Validierung: wenn path leer -> Fehler if not path: return 999 # Simuliere Erfolg return _MockQgsVectorFileWriter.NoError except Exception: return 999 # generischer Fehlercode QgsVectorFileWriter = _MockQgsVectorFileWriter # Geometrietyp-Konstanten (Mock-Werte entsprechen QgsWkbTypes) GEOM_POINT = 0 GEOM_LINE = 1 GEOM_POLYGON = 2 class _MockQgsBlockingNetworkRequest: NoError = 0 def __init__(self): self._reply = None def get(self, request: Any) -> int: return self.NoError def post(self, request: Any, data: bytes) -> int: return self.NoError def put(self, request: Any, data: bytes) -> int: return self.NoError def reply(self) -> Any: return self._reply def errorMessage(self) -> str: return "" QgsBlockingNetworkRequest = _MockQgsBlockingNetworkRequest class _MockQgsJsonUtils: @staticmethod def stringToFields(text: str) -> Any: return type("_Fields", (), {"toList": lambda self: []})() @staticmethod def stringToFeatureList(text: str, fields: Any) -> list: return [] QgsJsonUtils = _MockQgsJsonUtils class _MockQgsJsonExporter: def __init__(self, layer: Any = None): self._layer = layer def setSourceCrs(self, crs: Any) -> None: pass def setTransformGeometries(self, transform: bool) -> None: pass def exportFeatures(self, features: list) -> str: return '{"type":"FeatureCollection","features":[]}' QgsJsonExporter = _MockQgsJsonExporter # --------------------------------------------------------- # Netzwerk # --------------------------------------------------------- class NetworkReply: """ Minimaler Wrapper für Netzwerkantworten. """ def __init__(self, error: int): self.error = error def network_head(url: str) -> NetworkReply | None: """ Führt einen HTTP-HEAD-Request aus. Rückgabe: - NetworkReply(error=0) → erreichbar - NetworkReply(error!=0) → nicht erreichbar - None → Netzwerk nicht verfügbar / Fehler beim Request """ if not QGIS_AVAILABLE: return None if QUrl is None or QNetworkRequest is None: return None try: manager = QgsNetworkAccessManager.instance() request = QNetworkRequest(QUrl(url)) reply = manager.head(request) # synchron warten (kurz) if QEventLoop is not None: loop = QEventLoop() reply.finished.connect(loop.quit) loop.exec() return NetworkReply(error=reply.error()) except Exception: return None # --------------------------------------------------------- # Layer-Geometrie / Extent # --------------------------------------------------------- def get_layer_extent(layer: Any) -> Any: """ Gibt die Ausdehnung (Extent) eines Layers zurück. Diese Funktion kapselt den Zugriff auf ``layer.extent()`` und dient als zentrale Abstraktion für alle Stellen, die die Bounding Box eines Layers benötigen (z.B. für räumliche Filter im Datenabruf). Verhalten --------- - Wenn QGIS verfügbar ist und der Layer eine ``extent()``-Methode besitzt, wird deren Rückgabewert zurückgegeben. - Wenn QGIS nicht verfügbar ist oder der Layer keine ``extent()``-Methode hat, wird ``None`` zurückgegeben. """ if not QGIS_AVAILABLE or layer is None: return None extent_func = getattr(layer, "extent", None) if callable(extent_func): try: return extent_func() except Exception: return None return None # --------------------------------------------------------- # Buffer-Layer erzeugen # --------------------------------------------------------- def create_buffer_layer( source_layer: Any, distance_m: float, layer_name: str = "BufferLayer" ) -> Optional[Any]: """ Erzeugt einen Pufferlayer um alle Features eines Quelllayers. Diese Funktion dient als zentrale Abstraktion für die Erzeugung eines Pufferlayers in QGIS. Sie wird z.B. im Datenabruf verwendet, wenn der Raumfilter ``"Pufferlayer"`` aktiv ist. Verhalten --------- - Wenn QGIS verfügbar ist und der ``source_layer`` gültig ist, wird ein temporärer Vektorlayer erzeugt, der die gepufferten Geometrien enthält. - Der Puffer wird in Metern angegeben. - Der zurückgegebene Layer ist **nicht gespeichert**, sondern ein temporärer Speicherlayer, der anschließend über den UI‑Wrapper ins Projekt geladen werden kann. - Wenn QGIS nicht verfügbar ist oder ein Fehler auftritt, wird ``None`` zurückgegeben. """ if not QGIS_AVAILABLE: return None if source_layer is None or not hasattr(source_layer, "getFeatures"): return None try: # Geometrien puffern buffered_geoms = [] for feat in source_layer.getFeatures(): geom = feat.geometry() if geom is None: continue buf = geom.buffer(distance_m, 8) if buf is not None: buffered_geoms.append(buf) if not buffered_geoms: return None # Neuen Memory-Layer erzeugen crs = source_layer.crs().authid() if hasattr(source_layer, "crs") else "EPSG:4326" mem_layer = QgsVectorLayer(f"Polygon?crs={crs}", layer_name, "memory") prov = mem_layer.dataProvider() prov.addAttributes([]) mem_layer.updateFields() # Features hinzufügen from qgis.core import QgsFeature for geom in buffered_geoms: f = QgsFeature() f.setGeometry(geom) prov.addFeature(f) mem_layer.updateExtents() return mem_layer except Exception: return None #Hilfsfunktion, keine qgiscore-Entsprechung def layer_exists_in_gpkg(gpkg_path: str, layer_name: str) -> bool: """ Prüft, ob ein Layer mit dem Namen `layer_name` in `gpkg_path` existiert. - bevorzugt: SQLite-Abfrage auf gpkg_contents - fallback: kurzer Versuch, mit QgsVectorLayer zu laden (wenn QGIS verfügbar) """ import os, sqlite3 if not gpkg_path or not layer_name or not os.path.exists(gpkg_path): return False # 1) SQLite-Check (schnell) try: conn = sqlite3.connect(gpkg_path) cur = conn.cursor() cur.execute("SELECT COUNT(1) FROM gpkg_contents WHERE table_name = ?", (layer_name,)) row = cur.fetchone() conn.close() if row and row[0] > 0: return True except Exception: # falls sqlite fehlschlägt, weiter zum QGIS-Fallback pass # 2) QGIS-Fallback: versuche kurz, den Layer zu laden try: if getattr(QgsVectorLayer, "__call__", None) and QGIS_AVAILABLE: uri = f"{gpkg_path}|layername={layer_name}" layer = QgsVectorLayer(uri, layer_name, "ogr") return bool(layer and getattr(layer, "isValid", lambda: False)()) except Exception: pass return False # --------------------------------------------------------- # QGIS-3/4-kompatible Geometrie-Hilfsfunktionen # --------------------------------------------------------- def polygon_zu_linie(geom: Any) -> Optional[Any]: """Konvertiert ein Polygon/MultiPolygon zur Umringslinie (QGIS 3 + 4). In QGIS 3.x existiert ``QgsGeometry.boundary()``, in QGIS 4.x wurde sie entfernt. Diese Funktion kapselt die Versionsunterschiede und stellt dem Fachplugin eine stabile API bereit. Strategie (dreistufig): 1. ``geom.boundary()`` – funktioniert in QGIS 3.x 2. ``geom.convertToType(1, False)`` – ``1`` = LineGeometry-Wert, identisch in ``QgsWkbTypes.LineGeometry`` (3.x) und ``Qgis.GeometryType.Line`` (4.x) 3. Manuelle Ring-Extraktion via ``asPolygon()`` / ``asMultiPolygon()`` + ``QgsGeometry.fromPolylineXY()`` Parameters ---------- geom : Polygon- oder MultiPolygon-``QgsGeometry``. Returns ------- Optional[QgsGeometry] Liniengeometrie (Umring) oder ``None`` bei Fehler / leerer Eingabe. """ if geom is None: return None try: if geom.isEmpty(): return None except Exception: return None # Stufe 1: QGIS 3.x – .boundary() existiert und liefert alle Ringe korrekt if hasattr(geom, "boundary"): try: result = geom.boundary() if result is not None and not result.isEmpty(): return result except Exception: pass # Stufe 2: Manuelle Ring-Extraktion (primärer QGIS-4.x-Pfad). # convertToType(1, False) würde bei MultiPolygon nur den ersten Außenring # zurückgeben und alle weiteren Polygone sowie Innenringe verwerfen → daher # direkt die vollständige manuelle Extraktion als nächste Stufe. lines = [] try: if geom.isMultipart(): for polygon in geom.asMultiPolygon(): for ring in polygon: lines.append(QgsGeometry.fromPolylineXY(ring)) else: for ring in geom.asPolygon(): lines.append(QgsGeometry.fromPolylineXY(ring)) except Exception: pass if lines: return QgsGeometry.unaryUnion(lines) # Stufe 3: Letzter Fallback – convertToType mit destMultipart=True, damit # zumindest alle Außenringe eines MultiPolygons erfasst werden. try: result = geom.convertToType(1, True) if result is not None and not result.isEmpty(): return result except Exception: pass return None