More fixes

This commit is contained in:
2026-06-04 17:49:28 +02:00
parent bb51549193
commit 5839a3ce66
3 changed files with 134 additions and 48 deletions

View File

@@ -19,7 +19,7 @@ class ApiClient:
""" """
url = f"{self.base_url}/api/locations" url = f"{self.base_url}/api/locations"
try: try:
response = requests.get(url, timeout=10) response = requests.get(url, timeout=1)
response.raise_for_status() response.raise_for_status()
except requests.HTTPError as exc: except requests.HTTPError as exc:
QgsMessageLog.logMessage( QgsMessageLog.logMessage(

View File

@@ -36,12 +36,26 @@ class LocationLayerManager:
# ------------------------------------------------------------------ # ------------------------------------------------------------------
def create_layer(self) -> None: def create_layer(self) -> None:
"""Create the memory layer, add it to the project and load initial features. """Create or reuse the layer and load initial features.
Raises: If a layer with the stored ID already exists in the project (e.g. loaded
Exception: if the initial API fetch fails (layer is still added to the from a saved project file) it is reused rather than duplicated.
project but will be empty until reload_layer() succeeds).
""" """
# 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") self.layer = QgsVectorLayer("Point?crs=EPSG:4326", self._NAME_OK, "memory")
if not self.layer.isValid(): if not self.layer.isValid():
raise RuntimeError("Failed to create memory layer.") raise RuntimeError("Failed to create memory layer.")
@@ -54,6 +68,7 @@ class LocationLayerManager:
self.layer.updateFields() self.layer.updateFields()
QgsProject.instance().addMapLayer(self.layer) QgsProject.instance().addMapLayer(self.layer)
QgsProject.instance().writeEntry("BAPEBridge", "layer_id", self.layer.id())
self._configure_marker() self._configure_marker()
self._configure_labels() self._configure_labels()

157
plugin.py
View File

@@ -1,7 +1,7 @@
import os import os
from PyQt5.QtCore import QTimer from PyQt5.QtCore import QTimer
from PyQt5.QtGui import QIcon from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QAction from PyQt5.QtWidgets import QAction, QMessageBox
from qgis.core import Qgis, QgsProject from qgis.core import Qgis, QgsProject
from qgis.gui import QgisInterface from qgis.gui import QgisInterface
@@ -9,7 +9,9 @@ from .api_client import ApiClient
from .layer_manager import LocationLayerManager from .layer_manager import LocationLayerManager
from .notification_server import NotificationServer from .notification_server import NotificationServer
_RETRY_INTERVAL_MS = 30_000 # 30 seconds between automatic retries _RETRY_INTERVAL_MS = 30_000 # 30 s retry when API is unreachable on startup
_POLL_INTERVAL_MS = 10_000 # 10 s periodic health-check poll
_POST_CLEAR_DELAY_MS = 1_000 # 1 s delay before checking liveness after /clear
_API_BASE_URL = "http://localhost:22650" _API_BASE_URL = "http://localhost:22650"
_NOTIF_PORT = 22651 _NOTIF_PORT = 22651
@@ -28,61 +30,79 @@ class BAPEBridgePlugin:
self._layer_manager: LocationLayerManager | None = None self._layer_manager: LocationLayerManager | None = None
self._notif_server: NotificationServer | None = None self._notif_server: NotificationServer | None = None
self._toggle_action: QAction | None = None self._add_action: QAction | None = None
# Timer drives periodic retry when the initial API call fails. # Timer: retry when the initial API call fails at startup.
self._retry_timer = QTimer() self._retry_timer = QTimer()
self._retry_timer.setInterval(_RETRY_INTERVAL_MS) self._retry_timer.setInterval(_RETRY_INTERVAL_MS)
self._retry_timer.timeout.connect(self._retry_load) self._retry_timer.timeout.connect(self._retry_load)
# Timer: periodic health-check / reload while active.
self._poll_timer = QTimer()
self._poll_timer.setInterval(_POLL_INTERVAL_MS)
self._poll_timer.timeout.connect(self._poll_locations)
# One-shot timer: re-check liveness 5 s after a /clear command.
self._post_clear_timer = QTimer()
self._post_clear_timer.setSingleShot(True)
self._post_clear_timer.setInterval(_POST_CLEAR_DELAY_MS)
self._post_clear_timer.timeout.connect(self._poll_locations)
QgsProject.instance().cleared.connect(self._on_project_cleared) QgsProject.instance().cleared.connect(self._on_project_cleared)
QgsProject.instance().readProject.connect(self._on_project_read)
# Reconnect immediately if a layer from a previous plugin instance is
# already loaded (e.g. after a plugin reload without restarting QGIS).
self._try_reconnect()
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# QGIS plugin lifecycle # QGIS plugin lifecycle
# ------------------------------------------------------------------ # ------------------------------------------------------------------
def initGui(self) -> None: def initGui(self) -> None:
"""Add entry to the Layer menu, after 'Add From Layer Definition File'.""" """Add entry under Layer > Add Layer."""
icon = QIcon(os.path.join(os.path.dirname(__file__), "icon.png")) icon = QIcon(os.path.join(os.path.dirname(__file__), "icon.png"))
self._toggle_action = QAction(icon, "BAPE Bridge", self._iface.mainWindow()) self._add_action = QAction(icon, "Add BAPE Bridge Layer...", self._iface.mainWindow())
self._toggle_action.setCheckable(True) self._add_action.setToolTip("Add the BAPE Bridge live locations layer to this project")
self._toggle_action.setToolTip("Enable / disable the BAPE Bridge layer") self._add_action.triggered.connect(self._on_add_layer)
self._toggle_action.triggered.connect(self._on_toggle)
layer_menu = self._iface.layerMenu() # iface.addLayerMenu() is the "Add Layer" submenu inside the Layer menu.
# Find the 'Add From Layer Definition File' action and insert after it. self._iface.addLayerMenu().addAction(self._add_action)
after_action = None
for action in layer_menu.actions():
if "layer definition" in action.text().lower():
after_action = action
break
if after_action is not None:
# insertAction inserts *before* the reference; get the next item instead.
actions = layer_menu.actions()
idx = actions.index(after_action)
next_action = actions[idx + 1] if idx + 1 < len(actions) else None
if next_action is not None:
layer_menu.insertAction(next_action, self._toggle_action)
else:
layer_menu.addAction(self._toggle_action)
else:
layer_menu.addAction(self._toggle_action)
def unload(self) -> None: def unload(self) -> None:
"""Clean up when the plugin is disabled or QGIS closes.""" """Clean up when the plugin is disabled or QGIS closes.
self._deactivate()
self._iface.layerMenu().removeAction(self._toggle_action) Intentionally leaves the layer in the project so it survives a plugin
reload. Only the server and timers are torn down.
"""
self._retry_timer.stop()
self._poll_timer.stop()
self._post_clear_timer.stop()
if self._notif_server is not None:
self._notif_server.stop()
self._notif_server = None
# Drop the Python reference but do NOT remove the layer from the project.
self._layer_manager = None
self._active = False
self._iface.addLayerMenu().removeAction(self._add_action)
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# Toggle on / off # Add layer
# ------------------------------------------------------------------ # ------------------------------------------------------------------
def _on_toggle(self, checked: bool) -> None: def _on_add_layer(self) -> None:
if checked: """Called when the user clicks 'Add BAPE Bridge Layer'."""
self._activate() if self._active:
else: QMessageBox.warning(
self._deactivate() self._iface.mainWindow(),
"BAPE Bridge",
"There is already a BAPE Bridge layer inside this project.",
)
return
self._activate()
def _activate(self) -> None: def _activate(self) -> None:
if self._active: if self._active:
@@ -117,12 +137,15 @@ class BAPEBridgePlugin:
) )
self._active = True self._active = True
self._poll_timer.start()
def _deactivate(self) -> None: def _deactivate(self) -> None:
if not self._active: if not self._active:
return return
self._retry_timer.stop() self._retry_timer.stop()
self._poll_timer.stop()
self._post_clear_timer.stop()
if self._notif_server is not None: if self._notif_server is not None:
self._notif_server.stop() self._notif_server.stop()
@@ -134,9 +157,6 @@ class BAPEBridgePlugin:
self._active = False self._active = False
if self._toggle_action is not None:
self._toggle_action.setChecked(False)
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# Reload handling # Reload handling
# ------------------------------------------------------------------ # ------------------------------------------------------------------
@@ -155,9 +175,11 @@ class BAPEBridgePlugin:
if self._layer_manager is None: if self._layer_manager is None:
return return
self._layer_manager.clear_layer() self._layer_manager.clear_layer()
# Check 5 s later whether BAPE is still up.
self._post_clear_timer.start()
def _retry_load(self) -> None: def _retry_load(self) -> None:
"""Periodic retry: stop the timer once features load successfully.""" """Retry on startup failure: stop once the API responds."""
if self._layer_manager is None: if self._layer_manager is None:
self._retry_timer.stop() self._retry_timer.stop()
return return
@@ -165,6 +187,12 @@ class BAPEBridgePlugin:
if self._layer_manager.reload_layer(): if self._layer_manager.reload_layer():
self._retry_timer.stop() self._retry_timer.stop()
def _poll_locations(self) -> None:
"""Periodic health-check: reload from API to confirm BAPE is still up."""
if self._layer_manager is None:
return
self._layer_manager.reload_layer()
def _on_project_cleared(self) -> None: def _on_project_cleared(self) -> None:
"""Called when QGIS clears the project (open new/different project). """Called when QGIS clears the project (open new/different project).
@@ -172,6 +200,8 @@ class BAPEBridgePlugin:
not touch them — just drop references and reset state. not touch them — just drop references and reset state.
""" """
self._retry_timer.stop() self._retry_timer.stop()
self._poll_timer.stop()
self._post_clear_timer.stop()
if self._notif_server is not None: if self._notif_server is not None:
self._notif_server.stop() self._notif_server.stop()
@@ -183,7 +213,48 @@ class BAPEBridgePlugin:
self._active = False self._active = False
if self._toggle_action is not None: def _try_reconnect(self) -> None:
self._toggle_action.setChecked(False) """Reconnect to a BAPE layer that already exists in the current project.
Called both from __init__ (plugin reload) and from _on_project_read
(project loaded from disk).
"""
if self._active:
return
stored_id, _ = QgsProject.instance().readEntry("BAPEBridge", "layer_id", "")
if not stored_id:
return
existing = QgsProject.instance().mapLayer(stored_id)
if existing is None:
return
# Layer is present — restore active state.
api_client = ApiClient(_API_BASE_URL)
self._layer_manager = LocationLayerManager(api_client)
self._layer_manager.layer = existing
self._notif_server = NotificationServer(
self._on_reload_requested,
self._on_clear_requested,
port=_NOTIF_PORT,
)
try:
self._notif_server.start()
except RuntimeError as exc:
self._iface.messageBar().pushMessage(
"BAPE Bridge",
f"Notification server error: {exc}",
level=Qgis.Critical,
duration=0,
)
self._active = True
self._poll_timer.start()
def _on_project_read(self) -> None:
"""Called when a project is loaded from disk."""
self._try_reconnect()
# Reload to get fresh data; failures just set the 🔴 name.
if self._layer_manager is not None:
self._layer_manager.reload_layer()