Files
Plugin_SN_Basis/functions/qgiscore_wrapper.py
T

1417 lines
46 KiB
Python
Raw Normal View History

"""
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]
2026-01-08 17:13:51 +01:00
QgsMapLayerProxyModel: Type[Any]
QgsVectorFileWriter: Type[Any] # neu: Schreib-API
2026-03-12 16:14:02 +01:00
QgsFeature: Type[Any]
QgsField: Type[Any]
QgsGeometry: Type[Any]
QgsFeatureRequest: Type[Any]
2026-04-22 14:53:34 +02:00
QgsRectangle: Type[Any]
2026-03-12 16:14:02 +01:00
QgsCoordinateTransform: Type[Any]
QgsCoordinateReferenceSystem: Type[Any]
QgsPrintLayout: Type[Any]
QgsLayoutItemMap: Type[Any]
QgsLayoutItemMapGrid: Type[Any]
QgsTextFormat: Type[Any]
QgsTextBackgroundSettings: Type[Any]
QgsLayoutItemLabel: Type[Any]
2026-03-24 07:53:04 +01:00
QgsLayoutItemLegend: Type[Any]
QgsLayoutPoint: Type[Any]
QgsLayoutSize: Type[Any]
QgsUnitTypes: Type[Any]
QgsLayoutItem: Type[Any]
2026-06-19 11:47:36 +02:00
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,
2026-03-12 16:14:02 +01:00
QgsFeature as _QgsFeature,
QgsField as _QgsField,
2026-03-12 16:14:02 +01:00
QgsGeometry as _QgsGeometry,
QgsFeatureRequest as _QgsFeatureRequest,
2026-04-22 14:53:34 +02:00
QgsRectangle as _QgsRectangle,
2026-03-12 16:14:02 +01:00
QgsCoordinateTransform as _QgsCoordinateTransform,
QgsCoordinateReferenceSystem as _QgsCoordinateReferenceSystem,
QgsPrintLayout as _QgsPrintLayout,
QgsLayoutItemMap as _QgsLayoutItemMap,
QgsLayoutItemMapGrid as _QgsLayoutItemMapGrid,
QgsTextFormat as _QgsTextFormat,
QgsTextBackgroundSettings as _QgsTextBackgroundSettings,
QgsLayoutItemLabel as _QgsLayoutItemLabel,
2026-03-24 07:53:04 +01:00
QgsLayoutItemLegend as _QgsLayoutItemLegend,
QgsLayoutPoint as _QgsLayoutPoint,
QgsLayoutSize as _QgsLayoutSize,
QgsUnitTypes as _QgsUnitTypes,
QgsLayoutItem as _QgsLayoutItem,
2026-06-19 11:47:36 +02:00
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
2026-03-12 16:14:02 +01:00
QgsGeometry = _QgsGeometry
QgsFeatureRequest = _QgsFeatureRequest
2026-04-22 14:53:34 +02:00
QgsRectangle = _QgsRectangle
2026-03-12 16:14:02 +01:00
QgsCoordinateTransform = _QgsCoordinateTransform
QgsCoordinateReferenceSystem = _QgsCoordinateReferenceSystem
QgsPrintLayout = _QgsPrintLayout
QgsLayoutItemMap = _QgsLayoutItemMap
QgsLayoutItemMapGrid = _QgsLayoutItemMapGrid
QgsTextFormat = _QgsTextFormat
QgsTextBackgroundSettings = _QgsTextBackgroundSettings
QgsLayoutItemLabel = _QgsLayoutItemLabel
2026-03-24 07:53:04 +01:00
QgsLayoutItemLegend = _QgsLayoutItemLegend
QgsLayoutPoint = _QgsLayoutPoint
QgsLayoutSize = _QgsLayoutSize
QgsUnitTypes = _QgsUnitTypes
QgsLayoutItem = _QgsLayoutItem
2026-06-19 11:47:36 +02:00
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
def transformContext(self):
return None
def addMapLayer(self, layer: Any) -> Any:
return layer
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
2026-03-24 07:53:04 +01:00
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 = ""
2026-03-24 07:53:04 +01:00
def setId(self, item_id: str) -> None:
self.item_id = item_id
2026-03-24 07:53:04 +01:00
def setObjectName(self, name: str) -> None:
self.object_name = name
2026-03-24 07:53:04 +01:00
def setLinkedMap(self, map_item: Any) -> None:
self.linked_map = map_item
2026-03-24 07:53:04 +01:00
def setAutoUpdateModel(self, enabled: bool) -> None:
self.auto_update_model = enabled
2026-03-24 07:53:04 +01:00
def setLegendFilterByMapEnabled(self, enabled: bool) -> None:
self.legend_filter_by_map_enabled = enabled
2026-03-24 07:53:04 +01:00
def setTitle(self, title: str) -> None:
self.title = title
2026-03-24 07:53:04 +01:00
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
2026-03-24 07:53:04 +01:00
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
2026-03-12 16:14:02 +01:00
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
2026-01-08 17:13:51 +01:00
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
2026-06-19 11:47:36 +02:00
# 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
# ---------------------------------------------------------
# Vektorkacheln, Ausdrücke, Symbole und Legende (Basemap-Legende, Basis #88)
# Eigener Block, damit ein fehlendes Symbol den übrigen Wrapper nicht in den Mock-Modus zwingt.
# ---------------------------------------------------------
QgsVectorTileLayer: Type[Any]
QgsSelectionContext: Type[Any]
QgsTileXYZ: Type[Any]
QgsExpression: Type[Any]
QgsExpressionContext: Type[Any]
QgsExpressionContextScope: Type[Any]
QgsProperty: Type[Any]
QgsCategorizedSymbolRenderer: Type[Any]
QgsRendererCategory: Type[Any]
QgsLegendRenderer: Type[Any]
QgsLegendStyle: Type[Any]
QgsMapLayer: Type[Any]
QgsApplication: Type[Any]
QgsSymbolLayerUtils: Type[Any]
QgsLayoutMeasurement: Type[Any]
QgsLayoutUtils: Type[Any]
QgsSimpleLineSymbolLayer: Type[Any]
SELECT_BEHAVIOR_SET: Any = None
SELECT_RELATION_INTERSECT: Any = None
PROPERTY_TYPE_EXPRESSION: Any = None
PROPERTY_CUSTOM_DASH: Any = None
LEGEND_STYLE_HIDDEN: Any = None
LEGEND_STYLE_SUBGROUP: Any = None
STYLE_CATEGORIES_SYMBOLOGIE_BESCHRIFTUNG: Any = None
VECTOR_TILES_AVAILABLE = False
try:
from qgis.core import (
QgsVectorTileLayer as _QgsVectorTileLayer,
QgsSelectionContext as _QgsSelectionContext,
QgsTileXYZ as _QgsTileXYZ,
QgsExpression as _QgsExpression,
QgsExpressionContext as _QgsExpressionContext,
QgsExpressionContextScope as _QgsExpressionContextScope,
QgsProperty as _QgsProperty,
QgsCategorizedSymbolRenderer as _QgsCategorizedSymbolRenderer,
QgsRendererCategory as _QgsRendererCategory,
QgsLegendRenderer as _QgsLegendRenderer,
QgsLegendStyle as _QgsLegendStyle,
QgsMapLayer as _QgsMapLayer,
QgsApplication as _QgsApplication,
QgsSymbolLayerUtils as _QgsSymbolLayerUtils,
QgsLayoutMeasurement as _QgsLayoutMeasurement,
QgsLayoutUtils as _QgsLayoutUtils,
QgsSymbolLayer as _QgsSymbolLayer,
QgsSimpleLineSymbolLayer as _QgsSimpleLineSymbolLayer,
Qgis as _QgisVT,
)
QgsVectorTileLayer = _QgsVectorTileLayer
QgsSelectionContext = _QgsSelectionContext
QgsTileXYZ = _QgsTileXYZ
QgsExpression = _QgsExpression
QgsExpressionContext = _QgsExpressionContext
QgsExpressionContextScope = _QgsExpressionContextScope
QgsProperty = _QgsProperty
QgsCategorizedSymbolRenderer = _QgsCategorizedSymbolRenderer
QgsRendererCategory = _QgsRendererCategory
QgsLegendRenderer = _QgsLegendRenderer
QgsLegendStyle = _QgsLegendStyle
QgsMapLayer = _QgsMapLayer
QgsApplication = _QgsApplication
QgsSymbolLayerUtils = _QgsSymbolLayerUtils
QgsLayoutMeasurement = _QgsLayoutMeasurement
QgsLayoutUtils = _QgsLayoutUtils
QgsSimpleLineSymbolLayer = _QgsSimpleLineSymbolLayer
SELECT_BEHAVIOR_SET = _QgisVT.SelectBehavior.SetSelection
SELECT_RELATION_INTERSECT = _QgisVT.SelectGeometryRelationship.Intersect
PROPERTY_TYPE_EXPRESSION = getattr(getattr(_QgsProperty, "Type", None), "ExpressionBasedProperty", None)
if PROPERTY_TYPE_EXPRESSION is None:
PROPERTY_TYPE_EXPRESSION = getattr(_QgsProperty, "ExpressionBasedProperty", None)
# Schlüssel der datendefinierten Eigenschaft „eigenes Strichmuster“ als int (Basis #90);
# propertyKeys() liefert ints, das Enum ist je nach QGIS-Version gescoped oder nicht.
_strichmuster = getattr(getattr(_QgsSymbolLayer, "Property", None), "CustomDash", None)
if _strichmuster is None:
_strichmuster = getattr(_QgsSymbolLayer, "PropertyCustomDash", None)
PROPERTY_CUSTOM_DASH = int(getattr(_strichmuster, "value", _strichmuster))
LEGEND_STYLE_HIDDEN = getattr(getattr(_QgsLegendStyle, "Style", _QgsLegendStyle), "Hidden", None)
LEGEND_STYLE_SUBGROUP = getattr(getattr(_QgsLegendStyle, "Style", _QgsLegendStyle), "Subgroup", None)
_kategorie = getattr(_QgsMapLayer, "StyleCategory", _QgsMapLayer)
STYLE_CATEGORIES_SYMBOLOGIE_BESCHRIFTUNG = _kategorie.Symbology | _kategorie.Labeling
VECTOR_TILES_AVAILABLE = True
except Exception:
VECTOR_TILES_AVAILABLE = False
class _MockQgsVectorTileLayer:
"""Platzhalter: Vektorkachel-Layer gibt es im Mock-Modus nicht."""
class _MockQgsSelectionContext:
def __init__(self):
self._scale = 0.0
def setScale(self, scale: float) -> None:
self._scale = scale
def scale(self) -> float:
return self._scale
class _MockQgsTileXYZ:
def __init__(self, column: int = 0, row: int = 0, zoom: int = 0):
self._column, self._row, self._zoom = column, row, zoom
def column(self) -> int:
return self._column
def row(self) -> int:
return self._row
def zoomLevel(self) -> int:
return self._zoom
class _MockQgsExpression:
def __init__(self, text: str = ""):
self._text = text
def prepare(self, context: Any) -> bool:
return True
def evaluate(self, context: Any = None) -> Any:
return True
class _MockQgsExpressionContextScope:
def __init__(self, name: str = ""):
self._variables: dict[str, Any] = {}
def setVariable(self, name: str, value: Any, *args: Any) -> None:
self._variables[name] = value
def variable(self, name: str) -> Any:
return self._variables.get(name)
class _MockQgsExpressionContext:
def __init__(self):
self._scopes: list[Any] = []
self._feature: Any = None
def appendScope(self, scope: Any) -> None:
self._scopes.append(scope)
def setFeature(self, feature: Any) -> None:
self._feature = feature
class _MockQgsProperty:
def __init__(self, value: Any = None):
self._value = value
@staticmethod
def fromValue(value: Any) -> "_MockQgsProperty":
return _MockQgsProperty(value)
@staticmethod
def fromExpression(expression: str, is_active: bool = True) -> "_MockQgsProperty":
eigenschaft = _MockQgsProperty()
eigenschaft._ausdruck = expression
return eigenschaft
def expressionString(self) -> str:
return getattr(self, "_ausdruck", "")
class _MockQgsRendererCategory:
def __init__(self, value: Any = None, symbol: Any = None, label: str = ""):
self._value, self._symbol, self._label = value, symbol, label
def value(self) -> Any:
return self._value
def symbol(self) -> Any:
return self._symbol
def label(self) -> str:
return self._label
class _MockQgsCategorizedSymbolRenderer:
def __init__(self, attribute: str = "", categories: list | None = None):
self._attribute = attribute
self._categories = list(categories or [])
def categories(self) -> list:
return list(self._categories)
class _MockQgsLegendStyle:
Hidden = 0
Group = 1
Subgroup = 2
class _MockQgsLegendRenderer:
@staticmethod
def setNodeLegendStyle(node: Any, style: Any) -> None:
setattr(node, "_legend_style", style)
class _MockQgsMapLayer:
class StyleCategory:
Symbology = 1
Labeling = 2
class _MockQgsApplication:
@staticmethod
def qgisSettingsDirPath() -> str:
return ""
class _MockQgsSymbolLayerUtils:
@staticmethod
def symbolPreviewIcon(symbol: Any, size: Any, *args: Any) -> Any:
return None
QgsVectorTileLayer = _MockQgsVectorTileLayer
QgsSelectionContext = _MockQgsSelectionContext
QgsTileXYZ = _MockQgsTileXYZ
QgsExpression = _MockQgsExpression
QgsExpressionContext = _MockQgsExpressionContext
QgsExpressionContextScope = _MockQgsExpressionContextScope
QgsProperty = _MockQgsProperty
QgsCategorizedSymbolRenderer = _MockQgsCategorizedSymbolRenderer
QgsRendererCategory = _MockQgsRendererCategory
QgsLegendRenderer = _MockQgsLegendRenderer
QgsLegendStyle = _MockQgsLegendStyle
QgsMapLayer = _MockQgsMapLayer
QgsApplication = _MockQgsApplication
QgsSymbolLayerUtils = _MockQgsSymbolLayerUtils
class _MockQgsLayoutMeasurement:
def __init__(self, length: float = 0.0, units: Any = None):
self._length = length
def length(self) -> float:
return self._length
QgsLayoutMeasurement = _MockQgsLayoutMeasurement
class _MockQgsLayoutUtils:
@staticmethod
def createRenderContextForLayout(layout: Any, painter: Any, dpi: float = -1) -> Any:
return None
QgsLayoutUtils = _MockQgsLayoutUtils
class _MockQgsSimpleLineSymbolLayer:
def __init__(self, eigenschaften: dict | None = None):
self.eigenschaften = dict(eigenschaften or {})
@staticmethod
def create(eigenschaften: dict) -> "_MockQgsSimpleLineSymbolLayer":
return _MockQgsSimpleLineSymbolLayer(eigenschaften)
def layerType(self) -> str:
return "SimpleLine"
QgsSimpleLineSymbolLayer = _MockQgsSimpleLineSymbolLayer
# QgsRectangle hatte im allgemeinen Mock-Block keinen Ersatz (nur Typannotation).
if "QgsRectangle" not in globals():
class _MockQgsRectangle:
def __init__(self, x_min: float = 0.0, y_min: float = 0.0, x_max: float = 0.0, y_max: float = 0.0):
self._werte = (x_min, y_min, x_max, y_max)
def xMinimum(self) -> float:
return self._werte[0]
def yMinimum(self) -> float:
return self._werte[1]
def xMaximum(self) -> float:
return self._werte[2]
def yMaximum(self) -> float:
return self._werte[3]
def width(self) -> float:
return self._werte[2] - self._werte[0]
def height(self) -> float:
return self._werte[3] - self._werte[1]
QgsRectangle = _MockQgsRectangle
SELECT_BEHAVIOR_SET = 0
SELECT_RELATION_INTERSECT = 0
PROPERTY_TYPE_EXPRESSION = 2
PROPERTY_CUSTOM_DASH = 36
LEGEND_STYLE_HIDDEN = _MockQgsLegendStyle.Hidden
LEGEND_STYLE_SUBGROUP = _MockQgsLegendStyle.Subgroup
STYLE_CATEGORIES_SYMBOLOGIE_BESCHRIFTUNG = 3
# ---------------------------------------------------------
# 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
2026-04-23 22:23:50 +02:00
# ---------------------------------------------------------
# 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