Files
QGIS_BAPEBridge/layer_manager.py
2026-06-04 17:49:28 +02:00

182 lines
6.1 KiB
Python

import sip
from qgis.core import (
QgsVectorLayer,
QgsFeature,
QgsGeometry,
QgsPointXY,
QgsField,
QgsProject,
QgsMessageLog,
QgsPalLayerSettings,
QgsVectorLayerSimpleLabeling,
QgsTextFormat,
QgsTextBufferSettings,
QgsMarkerSymbol,
QgsSingleSymbolRenderer,
Qgis,
)
from PyQt5.QtCore import QVariant
from PyQt5.QtGui import QColor
from .api_client import ApiClient
class LocationLayerManager:
"""Creates and manages the 'BAPE Bridge' memory vector layer."""
_NAME_OK = "BAPE Bridge"
_NAME_ERR = "🔴 BAPE Bridge"
def __init__(self, api_client: ApiClient):
self._api_client = api_client
self.layer: QgsVectorLayer | None = None
# ------------------------------------------------------------------
# Public API
# ------------------------------------------------------------------
def create_layer(self) -> None:
"""Create or reuse the layer and load initial features.
If a layer with the stored ID already exists in the project (e.g. loaded
from a saved project file) it is reused rather than duplicated.
"""
# Check if a previously created layer is still alive in the project.
stored_id, _ = QgsProject.instance().readEntry("BAPEBridge", "layer_id", "")
if stored_id:
existing = QgsProject.instance().mapLayer(stored_id)
if existing is not None and not sip.isdeleted(existing):
self.layer = existing
return # layer already present — nothing more to do
# Guard against duplicate layers by name (e.g. toggled on twice).
for lyr in QgsProject.instance().mapLayers().values():
if lyr.name() in (self._NAME_OK, self._NAME_ERR):
self.layer = lyr
QgsProject.instance().writeEntry("BAPEBridge", "layer_id", lyr.id())
return
self.layer = QgsVectorLayer("Point?crs=EPSG:4326", self._NAME_OK, "memory")
if not self.layer.isValid():
raise RuntimeError("Failed to create memory layer.")
provider = self.layer.dataProvider()
provider.addAttributes([
QgsField("id", QVariant.String),
QgsField("name", QVariant.String),
])
self.layer.updateFields()
QgsProject.instance().addMapLayer(self.layer)
QgsProject.instance().writeEntry("BAPEBridge", "layer_id", self.layer.id())
self._configure_marker()
self._configure_labels()
# Initial load — set error name on failure so the layer is visible
# in the panel but clearly marked as not yet connected.
try:
self._do_reload()
except Exception:
self.layer.setName(self._NAME_ERR)
raise
def reload_layer(self) -> bool:
"""Fetch fresh data and replace all features in the layer.
Returns:
True on success, False on failure (existing features are kept).
"""
if self.layer is None or not self.layer.isValid():
return False
try:
self._do_reload()
return True
except Exception as exc:
QgsMessageLog.logMessage(
f"Layer reload failed, keeping existing features: {exc}",
"BAPEBridge",
Qgis.Warning,
)
self.layer.setName(self._NAME_ERR)
self.layer.emitStyleChanged()
return False
def clear_layer(self) -> None:
"""Remove all features from the layer without fetching new data."""
if self.layer is None or not self.layer.isValid():
return
self.layer.dataProvider().truncate()
self.layer.updateExtents()
self.layer.triggerRepaint()
def remove_layer(self) -> None:
"""Remove the layer from the project and release the reference."""
if self.layer is not None:
if not sip.isdeleted(self.layer):
layer_id = self.layer.id()
if QgsProject.instance().mapLayer(layer_id) is not None:
QgsProject.instance().removeMapLayer(layer_id)
self.layer = None
# ------------------------------------------------------------------
# Private helpers
# ------------------------------------------------------------------
def _configure_marker(self) -> None:
"""Set a green filled triangle marker at size 3."""
symbol = QgsMarkerSymbol.createSimple({
"name": "triangle",
"color": "72,123,182,255",
"size": "3",
})
self.layer.setRenderer(QgsSingleSymbolRenderer(symbol))
def _configure_labels(self) -> None:
"""Enable simple labels showing the 'name' field next to each point."""
buffer = QgsTextBufferSettings()
buffer.setEnabled(True)
buffer.setColor(QColor("black"))
buffer.setSize(0.4)
text_format = QgsTextFormat()
text_format.setColor(QColor("white"))
text_format.setBuffer(buffer)
pal = QgsPalLayerSettings()
pal.fieldName = "name"
pal.enabled = True
pal.setFormat(text_format)
self.layer.setLabeling(QgsVectorLayerSimpleLabeling(pal))
self.layer.setLabelsEnabled(True)
def _do_reload(self) -> None:
"""Core fetch-and-replace logic. Raises on any failure."""
locations = self._api_client.fetch_locations()
provider = self.layer.dataProvider()
provider.truncate()
features = [self._build_feature(loc) for loc in locations]
provider.addFeatures(features)
self.layer.updateExtents()
self.layer.triggerRepaint()
self.layer.setName(self._NAME_OK)
self.layer.emitStyleChanged()
QgsMessageLog.logMessage(
f"Loaded {len(features)} location(s).",
"BAPEBridge",
Qgis.Info,
)
def _build_feature(self, loc: dict) -> QgsFeature:
feature = QgsFeature(self.layer.fields())
feature.setGeometry(
QgsGeometry.fromPointXY(QgsPointXY(loc["longitude"], loc["latitude"]))
)
feature["id"] = loc["id"]
feature["name"] = loc["name"]
return feature