Initial commit
This commit is contained in:
3
__init__.py
Normal file
3
__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
def classFactory(iface):
|
||||||
|
from .plugin import BAPEBridgePlugin
|
||||||
|
return BAPEBridgePlugin(iface)
|
||||||
BIN
__pycache__/__init__.cpython-312.pyc
Normal file
BIN
__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/api_client.cpython-312.pyc
Normal file
BIN
__pycache__/api_client.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/layer_manager.cpython-312.pyc
Normal file
BIN
__pycache__/layer_manager.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/notification_server.cpython-312.pyc
Normal file
BIN
__pycache__/notification_server.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/plugin.cpython-312.pyc
Normal file
BIN
__pycache__/plugin.cpython-312.pyc
Normal file
Binary file not shown.
52
api_client.py
Normal file
52
api_client.py
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import requests
|
||||||
|
from qgis.core import QgsMessageLog, Qgis
|
||||||
|
|
||||||
|
|
||||||
|
class ApiClient:
|
||||||
|
"""Fetches location records from the external REST API."""
|
||||||
|
|
||||||
|
def __init__(self, base_url: str):
|
||||||
|
self.base_url = base_url.rstrip("/")
|
||||||
|
|
||||||
|
def fetch_locations(self) -> list[dict]:
|
||||||
|
"""GET /api/locations and return a list of location dicts.
|
||||||
|
|
||||||
|
Each dict contains: id (int), name (str), latitude (float), longitude (float).
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
requests.RequestException: on any network or HTTP error.
|
||||||
|
ValueError: if the response body is not valid JSON or missing expected keys.
|
||||||
|
"""
|
||||||
|
url = f"{self.base_url}/api/locations"
|
||||||
|
try:
|
||||||
|
response = requests.get(url, timeout=10)
|
||||||
|
response.raise_for_status()
|
||||||
|
except requests.HTTPError as exc:
|
||||||
|
QgsMessageLog.logMessage(
|
||||||
|
f"HTTP error fetching locations from {url}: {exc}",
|
||||||
|
"BAPEBridge",
|
||||||
|
Qgis.Warning,
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
except requests.RequestException as exc:
|
||||||
|
QgsMessageLog.logMessage(
|
||||||
|
f"Network error fetching locations from {url}: {exc}",
|
||||||
|
"BAPEBridge",
|
||||||
|
Qgis.Warning,
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = response.json()
|
||||||
|
except ValueError as exc:
|
||||||
|
QgsMessageLog.logMessage(
|
||||||
|
f"Invalid JSON in locations response: {exc}",
|
||||||
|
"BAPEBridge",
|
||||||
|
Qgis.Warning,
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
|
||||||
|
if not isinstance(data, list):
|
||||||
|
raise ValueError(f"Expected a JSON array, got {type(data).__name__}")
|
||||||
|
|
||||||
|
return data
|
||||||
166
layer_manager.py
Normal file
166
layer_manager.py
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
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
|
||||||
16
metadata.txt
Normal file
16
metadata.txt
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
[general]
|
||||||
|
name=BAPE Bridge
|
||||||
|
qgisMinimumVersion=3.0
|
||||||
|
description=Live feedback from BAPE.
|
||||||
|
version=0.1.0
|
||||||
|
author=BunkerArchive
|
||||||
|
email=
|
||||||
|
about=Live feedback from BAPE.
|
||||||
|
tracker=
|
||||||
|
repository=
|
||||||
|
tags=locations,live,api,points,real-time
|
||||||
|
homepage=
|
||||||
|
category=Plugins
|
||||||
|
icon=icon.png
|
||||||
|
experimental=True
|
||||||
|
deprecated=False
|
||||||
112
notification_server.py
Normal file
112
notification_server.py
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import json
|
||||||
|
import threading
|
||||||
|
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
|
from PyQt5.QtCore import QObject, pyqtSignal
|
||||||
|
from qgis.core import QgsMessageLog, Qgis
|
||||||
|
|
||||||
|
|
||||||
|
class _Signals(QObject):
|
||||||
|
"""Thin QObject wrapper so we can emit signals from the server thread.
|
||||||
|
|
||||||
|
Qt auto-connection (the default) marshals cross-thread signals via the
|
||||||
|
event queue, so connected slots always run in the main Qt thread.
|
||||||
|
"""
|
||||||
|
reload = pyqtSignal()
|
||||||
|
clear = pyqtSignal()
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationServer:
|
||||||
|
"""Lightweight HTTP server that listens on localhost:22651.
|
||||||
|
|
||||||
|
Exposes:
|
||||||
|
POST /reload → triggers the reload callback in the main Qt thread
|
||||||
|
POST /clear → triggers the clear callback in the main Qt thread
|
||||||
|
|
||||||
|
Both respond with {"status": "ok"}.
|
||||||
|
The server runs in a dedicated daemon thread so it never blocks QGIS.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, reload_callback: Callable[[], None], clear_callback: Callable[[], None], port: int = 8765):
|
||||||
|
self._port = port
|
||||||
|
self._signal = _Signals()
|
||||||
|
self._signal.reload.connect(reload_callback)
|
||||||
|
self._signal.clear.connect(clear_callback)
|
||||||
|
self._server: HTTPServer | None = None
|
||||||
|
self._thread: threading.Thread | None = None
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Public API
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def start(self) -> None:
|
||||||
|
"""Bind to localhost:{port} and begin serving in a background thread.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
RuntimeError: if the port is already in use.
|
||||||
|
"""
|
||||||
|
signal = self._signal # captured by the handler class below
|
||||||
|
|
||||||
|
class _Handler(BaseHTTPRequestHandler):
|
||||||
|
def do_POST(self): # noqa: N802
|
||||||
|
if self.path == "/reload":
|
||||||
|
signal.reload.emit()
|
||||||
|
self._respond_ok()
|
||||||
|
elif self.path == "/clear":
|
||||||
|
signal.clear.emit()
|
||||||
|
self._respond_ok()
|
||||||
|
else:
|
||||||
|
self.send_response(404)
|
||||||
|
self.end_headers()
|
||||||
|
|
||||||
|
def _respond_ok(self):
|
||||||
|
body = json.dumps({"status": "ok"}).encode()
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header("Content-Type", "application/json")
|
||||||
|
self.send_header("Content-Length", str(len(body)))
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(body)
|
||||||
|
|
||||||
|
# Suppress the default access log to keep the QGIS console clean.
|
||||||
|
def log_message(self, fmt, *args): # noqa: ANN001
|
||||||
|
QgsMessageLog.logMessage(
|
||||||
|
fmt % args, "BAPEBridge", Qgis.Info
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._server = HTTPServer(("127.0.0.1", self._port), _Handler)
|
||||||
|
except OSError as exc:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Cannot bind notification server to port {self._port}: {exc}"
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
self._thread = threading.Thread(
|
||||||
|
target=self._server.serve_forever,
|
||||||
|
name="BAPEBridge-NotifServer",
|
||||||
|
daemon=True,
|
||||||
|
)
|
||||||
|
self._thread.start()
|
||||||
|
|
||||||
|
QgsMessageLog.logMessage(
|
||||||
|
f"Notification server started on http://127.0.0.1:{self._port}",
|
||||||
|
"BAPEBridge",
|
||||||
|
Qgis.Info,
|
||||||
|
)
|
||||||
|
|
||||||
|
def stop(self) -> None:
|
||||||
|
"""Shut down the server and wait for the thread to exit."""
|
||||||
|
if self._server is not None:
|
||||||
|
self._server.shutdown()
|
||||||
|
self._server.server_close()
|
||||||
|
self._server = None
|
||||||
|
|
||||||
|
if self._thread is not None:
|
||||||
|
self._thread.join(timeout=5)
|
||||||
|
self._thread = None
|
||||||
|
|
||||||
|
QgsMessageLog.logMessage(
|
||||||
|
"Notification server stopped.",
|
||||||
|
"BAPEBridge",
|
||||||
|
Qgis.Info,
|
||||||
|
)
|
||||||
167
plugin.py
Normal file
167
plugin.py
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
from PyQt5.QtCore import QTimer
|
||||||
|
from PyQt5.QtWidgets import QAction
|
||||||
|
from qgis.core import Qgis, QgsProject
|
||||||
|
from qgis.gui import QgisInterface
|
||||||
|
|
||||||
|
from .api_client import ApiClient
|
||||||
|
from .layer_manager import LocationLayerManager
|
||||||
|
from .notification_server import NotificationServer
|
||||||
|
|
||||||
|
_RETRY_INTERVAL_MS = 30_000 # 30 seconds between automatic retries
|
||||||
|
_API_BASE_URL = "http://localhost:22650"
|
||||||
|
_NOTIF_PORT = 22651
|
||||||
|
|
||||||
|
|
||||||
|
class BAPEBridgePlugin:
|
||||||
|
"""Main QGIS plugin class.
|
||||||
|
|
||||||
|
Wires together ApiClient, LocationLayerManager and NotificationServer.
|
||||||
|
Provides a checkable toolbar button to toggle the layer on/off.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, iface: QgisInterface):
|
||||||
|
self._iface = iface
|
||||||
|
self._active = False
|
||||||
|
|
||||||
|
self._layer_manager: LocationLayerManager | None = None
|
||||||
|
self._notif_server: NotificationServer | None = None
|
||||||
|
|
||||||
|
self._toggle_action: QAction | None = None
|
||||||
|
|
||||||
|
# Timer drives periodic retry when the initial API call fails.
|
||||||
|
self._retry_timer = QTimer()
|
||||||
|
self._retry_timer.setInterval(_RETRY_INTERVAL_MS)
|
||||||
|
self._retry_timer.timeout.connect(self._retry_load)
|
||||||
|
|
||||||
|
QgsProject.instance().cleared.connect(self._on_project_cleared)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# QGIS plugin lifecycle
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def initGui(self) -> None:
|
||||||
|
"""Add entry to the Layer menu."""
|
||||||
|
self._toggle_action = QAction("BAPE Bridge", self._iface.mainWindow())
|
||||||
|
self._toggle_action.setCheckable(True)
|
||||||
|
self._toggle_action.setToolTip("Enable / disable the BAPE Bridge layer")
|
||||||
|
self._toggle_action.triggered.connect(self._on_toggle)
|
||||||
|
self._iface.addPluginToLayerMenu("BAPE Bridge", self._toggle_action)
|
||||||
|
|
||||||
|
def unload(self) -> None:
|
||||||
|
"""Clean up when the plugin is disabled or QGIS closes."""
|
||||||
|
self._deactivate()
|
||||||
|
self._iface.removePluginFromLayerMenu("BAPE Bridge", self._toggle_action)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Toggle on / off
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _on_toggle(self, checked: bool) -> None:
|
||||||
|
if checked:
|
||||||
|
self._activate()
|
||||||
|
else:
|
||||||
|
self._deactivate()
|
||||||
|
|
||||||
|
def _activate(self) -> None:
|
||||||
|
if self._active:
|
||||||
|
return
|
||||||
|
|
||||||
|
api_client = ApiClient(_API_BASE_URL)
|
||||||
|
self._layer_manager = LocationLayerManager(api_client)
|
||||||
|
|
||||||
|
# Create the layer structure (always succeeds). Initial feature load
|
||||||
|
# may fail if the API is not yet reachable; we surface a message and
|
||||||
|
# start the retry timer in that case.
|
||||||
|
try:
|
||||||
|
self._layer_manager.create_layer()
|
||||||
|
except Exception:
|
||||||
|
self._retry_timer.start()
|
||||||
|
|
||||||
|
# Start the notification server regardless; the layer may be empty
|
||||||
|
# but the server should be ready to receive reload calls.
|
||||||
|
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
|
||||||
|
|
||||||
|
def _deactivate(self) -> None:
|
||||||
|
if not self._active:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._retry_timer.stop()
|
||||||
|
|
||||||
|
if self._notif_server is not None:
|
||||||
|
self._notif_server.stop()
|
||||||
|
self._notif_server = None
|
||||||
|
|
||||||
|
if self._layer_manager is not None:
|
||||||
|
self._layer_manager.remove_layer()
|
||||||
|
self._layer_manager = None
|
||||||
|
|
||||||
|
self._active = False
|
||||||
|
|
||||||
|
if self._toggle_action is not None:
|
||||||
|
self._toggle_action.setChecked(False)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Reload handling
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _on_reload_requested(self) -> None:
|
||||||
|
"""Called (in the main Qt thread) when POST /reload arrives."""
|
||||||
|
if self._layer_manager is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
success = self._layer_manager.reload_layer()
|
||||||
|
if not success:
|
||||||
|
pass # layer name already shows 🔴
|
||||||
|
|
||||||
|
def _on_clear_requested(self) -> None:
|
||||||
|
"""Called (in the main Qt thread) when POST /clear arrives."""
|
||||||
|
if self._layer_manager is None:
|
||||||
|
return
|
||||||
|
self._layer_manager.clear_layer()
|
||||||
|
|
||||||
|
def _retry_load(self) -> None:
|
||||||
|
"""Periodic retry: stop the timer once features load successfully."""
|
||||||
|
if self._layer_manager is None:
|
||||||
|
self._retry_timer.stop()
|
||||||
|
return
|
||||||
|
|
||||||
|
if self._layer_manager.reload_layer():
|
||||||
|
self._retry_timer.stop()
|
||||||
|
|
||||||
|
def _on_project_cleared(self) -> None:
|
||||||
|
"""Called when QGIS clears the project (open new/different project).
|
||||||
|
|
||||||
|
The C++ layer objects are already deleted by this point, so we must
|
||||||
|
not touch them — just drop references and reset state.
|
||||||
|
"""
|
||||||
|
self._retry_timer.stop()
|
||||||
|
|
||||||
|
if self._notif_server is not None:
|
||||||
|
self._notif_server.stop()
|
||||||
|
self._notif_server = None
|
||||||
|
|
||||||
|
if self._layer_manager is not None:
|
||||||
|
self._layer_manager.layer = None
|
||||||
|
self._layer_manager = None
|
||||||
|
|
||||||
|
self._active = False
|
||||||
|
|
||||||
|
if self._toggle_action is not None:
|
||||||
|
self._toggle_action.setChecked(False)
|
||||||
|
|
||||||
|
|
||||||
4
resources.qrc
Normal file
4
resources.qrc
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<RCC>
|
||||||
|
<qresource prefix="/plugins/my_location_plugin">
|
||||||
|
</qresource>
|
||||||
|
</RCC>
|
||||||
Reference in New Issue
Block a user