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 the memory layer, add it to the project and load initial features. Raises: Exception: if the initial API fetch fails (layer is still added to the project but will be empty until reload_layer() succeeds). """ 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) 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