Im Standardlayout hingen Hauptlegende (oben), Quellenangabe und Kartenschild (unten) an der Kartenhöhe; bei kleinen Verfahren waren sie zusammen höher als die Karte und überlappten. Neues Modul print_kartenrand: Nach dem Aufbau der Vorlage (inkl. Basemap-Legende) werden die Legenden einmal gezeichnet – die Hauptlegende filtert nach Karteninhalt, ihre Höhe steht erst danach fest – und für 1–3 Spalten vermessen. Gewählt wird die kleinste passende Spaltenzahl (Kartenrand wird dafür breiter); passt keine, die mit dem geringsten Platzbedarf, und die Seite wird höher, Quellenangabe und Kartenschild rutschen nach unten. Karte bleibt in Größe und Lage. Fehler bei der Anordnung verhindern die Vorlage nicht (Warnung). Wrapper: QgsLayoutExporter. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1429 lines
46 KiB
Python
1429 lines
46 KiB
Python
"""
|
||
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
|
||
|
||
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
|
||
|
||
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
|
||
|
||
# ---------------------------------------------------------
|
||
# 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]
|
||
QgsLayoutExporter: 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,
|
||
QgsLayoutExporter as _QgsLayoutExporter,
|
||
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
|
||
QgsLayoutExporter = _QgsLayoutExporter
|
||
|
||
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
|
||
|
||
class _MockQgsLayoutExporter:
|
||
def __init__(self, layout: Any = None):
|
||
self._layout = layout
|
||
|
||
def renderRegionToImage(self, region: Any, size: Any = None, dpi: float = -1) -> Any:
|
||
return None
|
||
|
||
QgsLayoutExporter = _MockQgsLayoutExporter
|
||
|
||
# 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
|
||
|
||
|
||
# ---------------------------------------------------------
|
||
# 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
|