From 6e5fcefa42a6fa47a391f1bb0f219a7a5ec37ac7 Mon Sep 17 00:00:00 2001 From: Mees van der Wijk Date: Thu, 4 Jun 2026 17:11:29 +0200 Subject: [PATCH] Initial commit --- __init__.py | 3 + __pycache__/__init__.cpython-312.pyc | Bin 0 -> 384 bytes __pycache__/api_client.cpython-312.pyc | Bin 0 -> 2608 bytes __pycache__/layer_manager.cpython-312.pyc | Bin 0 -> 8365 bytes .../notification_server.cpython-312.pyc | Bin 0 -> 5921 bytes __pycache__/plugin.cpython-312.pyc | Bin 0 -> 7450 bytes api_client.py | 52 ++++++ icon.png | Bin 0 -> 464 bytes layer_manager.py | 166 +++++++++++++++++ metadata.txt | 16 ++ notification_server.py | 112 ++++++++++++ plugin.py | 167 ++++++++++++++++++ resources.qrc | 4 + 13 files changed, 520 insertions(+) create mode 100644 __init__.py create mode 100644 __pycache__/__init__.cpython-312.pyc create mode 100644 __pycache__/api_client.cpython-312.pyc create mode 100644 __pycache__/layer_manager.cpython-312.pyc create mode 100644 __pycache__/notification_server.cpython-312.pyc create mode 100644 __pycache__/plugin.cpython-312.pyc create mode 100644 api_client.py create mode 100644 icon.png create mode 100644 layer_manager.py create mode 100644 metadata.txt create mode 100644 notification_server.py create mode 100644 plugin.py create mode 100644 resources.qrc diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..da4a62d --- /dev/null +++ b/__init__.py @@ -0,0 +1,3 @@ +def classFactory(iface): + from .plugin import BAPEBridgePlugin + return BAPEBridgePlugin(iface) diff --git a/__pycache__/__init__.cpython-312.pyc b/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..16de37c32674cad7ef371c5cba0ae17b636ee772 GIT binary patch literal 384 zcmX@j%ge<81k7!USs6h3F^B^Lj8MjBkdo;PDGV(PQ4E!gnoP+s8IS^IDE=%5QN@_T zl*5qASj))Bz{F6)Si`UysydiKliBYjh@;7POTfu7z}2ZJGbKGWAg44vGf$K07Fz+B zRm1{P!gPx@Gc7SW70fGU0tzW8{7P}QiU}=FEh^T}O-(J&CE;J z4|Mko1{20H1x5L3nK`M&F)68OiKRIu`URCG8Tono5G#sf3ez)-4HS z%*!l^kJl@xyv388lUQ8rmY7_UUsPGd0kj(A=wbmN(ZFz*MdK#7^bFU_+)5W%ls*H+ z82mID!Kzj=6mbH%MeIQ07l%!5eoARhs$CH`kP9{d6pSC385tQrGchqLgVh26V^LeT literal 0 HcmV?d00001 diff --git a/__pycache__/api_client.cpython-312.pyc b/__pycache__/api_client.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..597ca54665315e474a598707c53a26946de0a23b GIT binary patch literal 2608 zcmbtWUu;uV7(e$D9x?OnGD(HH2WOA9dw_XhP_BvGIpmBObj3#0)Z!Az5fttg<0Ua<`K&zHlNt{7U7w3!m#CJbZ$B} zrX=G~gW)W73Yd=*OwMycZQa5{>ERLFck+0`Z8@zoqLrwXsrm1@Bb*YyTJ#kuFM0fR zlOf%xaVtn$JujoUJn&dbLhMbHacUN6Gl;=fWyH#Q8zyXF{5X3gHFk;-%8~^_ShBBJ z?AN)T9CGx6Y3GuIM~@GC(<@^|>SRreurY&V^^(Pt#WEjv>}1g@zTKxQs$bCjb3U)W zUPaer|NN3vQHRk^#8;apFVfgBx)FVOQsEnXc0TM=_0>=-667gRQ9!?HH1Rh7EFbl$ z`g)_5M+sd*M)Ra{K{+R_@j+CvVf-r}TK=yO=g@iO19VP;jBNBB0LzJgj6UeaE&rFe zeVg$O`cY`jqv;WxgtkxmpFhL8Z2$&{-o-k$OvbU3t)qisW;jcPLbx{4dS)C8CeAoE z*Ws&5Vbj2SOq<7hu&oye-UHt=&=$Z8FB!1UT8=JGEyvESW3ACUq??Se)T$j}j*_z_ z!od7cl}Mk@kfOk>6b9VV?J~9rKj+Xh*rE8?$jC`dD0OIJos#!-t3=X*4~}L%fTM&J z9h(t++A+%5WY~5%{!q6dj<<(T3+ayJKwP1$o^uatC*%}%Lx=lLrVo2?G%)ePKrJ3Gj;9NDAGg=IYualx z`?00)#MZR$yk3n%7F=pa{fE9~EDf z8?u~S?WLjuq}=BBb!tO&ZnUvVu9|0#?S|?Va6=|!6*Su!;;N$8T$PuLL_n`-9Zib| zr~m?2fwx9DS1pvQknn%arYb?k095{lVg_y{hd zoY_9T{gcR*3-fJ>DP>uX24nX-cg$3#E7yw}>?pO@(S_tpD7v8;$TY|Br z_N_Aqrw@Mm=2ZVm3`MtG>Hbo^7uj_`wrze#>ic~+)$dY^2L=`n49ura%x^z=H#RsQ z9{erRKDFV`l{Zjy(_*A+A=32_HG!@0=EZRLLb!V=_UdeA_SD?-bHj7#`RL)L=(hXO zj+yXuc%@lwZCehZ*4UH#kCG*kVr^`KNDM;J&?!I31ZL~jte(Y`Umz7(Qp2G)}TL(4*efyc7BL|h+2LnLg z2?YCll{; z@vuO>0cPkfAiqXb0KQjo3je{7H;GPQT`*lnKdXn9RVlFddix^;%W_jZ&~d%vy79%9 z6$E5eS_ndab->~V#b1F0{P7`PTYIfs)%+qH*R4Hn>8*Nwi0*|0@whYbrYjRS RzoPgbO-ZTaA%e*h`wLapcX|K- literal 0 HcmV?d00001 diff --git a/__pycache__/layer_manager.cpython-312.pyc b/__pycache__/layer_manager.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6badb2f7ada172775a500e67d2af1520548b537c GIT binary patch literal 8365 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 literal 0 HcmV?d00001 diff --git a/__pycache__/notification_server.cpython-312.pyc b/__pycache__/notification_server.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2a6abe5f55f1838fc65e859d84a20e8c5febdbb8 GIT binary patch literal 5921 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 D*WWV| literal 0 HcmV?d00001 diff --git a/__pycache__/plugin.cpython-312.pyc b/__pycache__/plugin.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dd664629c11a68993a1a86e697fef17d2c8b9a6b GIT binary patch literal 7450 zcmbVRU2Gf25nld@NAmPX+p?`6`mDd0{-GTCr?%|Gk>#Jbwq;3HnwT+grFbXl*dveG zJKCWt7YdL9PMX3fkk$p#0)DjNpiN)epy*4}zV*ciQ>Ai;CFEcD;1qwQvpfr(+e9LgMFq;KRTu8 zR(NzuAE8=CP8%>>kF;|+L#C6dwETs~ULG3ED?_Rx=WxpA3tBpbEH0#qGQF6}rKT9? z=&)uelZridS*F)ys_ZWiGSf0`82N#|J{8uf)0%D!^!Fb=v|st*YlNij-P*M)I2E36 z6PYAE5=nX`PtqrOW&fmC^1ba(2Bapb4%#|sgU|*gPUa-P6qxi&A$Se}Jq&Fa+6c4} zXnAOPXzQV^hqeLQ251|hZ7c@j%~sRN!I9G^sUl6uBWhtv$<2ItF{R{$(X;0+3wcHn z(rQZA6XCD`1jFrgVcm7 z17}U0NKs*;U>I5s9`RjMI4~^>DoS3^a(&w5q|L($D0pD$%IPzC`k==DWeUf5o6LG< ziO2@9lJE(SmlcV?le?BZ;mYcmb2P+=IQ09K4y>$mXtN~4JN;^#X{`30p3&s=?pJo< zfj!h*9d%|_+su|~-|4~X7N=irtK5z44mH#2^s24%HJSD*&FQW4wwN*D^g*gYAK20aao*_ z(~VRvEyE;LPElF1Hi=qJ%-gm=tPs&o=(0LV8(@)+Dk2C^CWZ#Cz5@Hz`?9jE_YLOr zr&30$Z%j*NmE2SxVun=*t^&77MV0ldl02Czs77DDXiS6R+KAGxz5(V&oJi&62?o8Z zHa(FqTAU~nj;G>Z8!bzMHaTUL&Gl~J8{!r;@TFD9LKhG zG11SSAJ>tNt`AXZhM*QF}a>n?~xLBbOmvz$?Yt2ag&SR?Jn;gFn14> zxMQDl(K6R&a%~T}4trgh+h=n7?n>pv88dNao;$k?`yo~x99V(ohSM&_fEs{*hREMD zL>lfGqA}y*4;k1cgDX}QM^hb35BPJj3S3+)R+nW$)xOh{yj5LYXO)X|XAM{l-#4!M zmiUKvx)z|y|0(XZPv@9P?-L|Nw~ag4@D)>Rdp;bGP&Bi&87eEB1@H5!l9ROS z6t`;e0Qw=vJy>CQ8V46KuH>zdL6s@+!LmfruvuY*v28@GhLj}Xypt(<4t8p7hG*Nq zjcd05hHc;T&>gT)M|BjafINbTy|V>nX4@L@RYrE>irY}lkdLKM@sl<(}AR-(cSp8PJrG4J@;Up{i)JaT@XyFfc(Z%?5Qh0D z+kd0X_QRyOmtlq;gnq>>GUQOi#Z228DO54}0w}#&tp6A;a`KB`8 zZu0H(d~B`q24MmFwuA7MOnHH@x=>G%81B~?sPL>f#_)#6Kt8O@dK@Y^5O-z><-s{~ zs%@r*GpNeQaLqET-dtVf^nh9;G&8n2;~#qNS9T41sduPsFEv874C?HvQr(MG2Rz)- zBIzQA>yo+&?O?#hU}ja>nClhto_?LY>AOO%dw`=GBm&1vH@xtDHG;v+&Q-5~M`xdz zuC-$l+;3*RACO;rjc#W&)9duAt>phOaKCb59&!aPl{dUU@~-}Fn|j^j>^dGChRd5j zuTg_pf|<6J_YuL4J_VUljSdJDp#(}FP~~f~YH?D5vMb+sd^5#xoTBZ5$aQe!oOp8Z z@@esvu?rM$#1u8b;?q6RLl3Dy%A%~IfY238qHf3Fh3n}82Q4QgCXG8fgJj}GNg|cHjhhSX)3MvRcBaOF? zzk7UvkAA)-_NyBYx9qza{w&u2(ZIvli_8x$g?*u30DGC=Ve&i5{9cpaTjCRsc@o>t z$UP?C!#dq2-~Cy0`{STL7RIHmOJOe5z=mQbA1m`Xp~UwtMaeb@c7MwM_i>ywZU2&Z zDlh0T`HnK*W%6Br=X;iXK>qsiHqzAoC8=Y4Zk>PUe3|bw`Ob%YcbVUB^7|K>Iu@e) z?xyd)@}TL!XDzL_vp>r&Y;Ro(k^1MqB7ypbB~~?5-uU%WGq8H1$Kl(5(|WRn{P|$( z$rI$0madb>eV-ijV0|KVYOC)r+>588zQ0BT(0|Goqnz=7Sppxto;lOOoB_1Hr5ARf z#Q`6F9?bdMx2~AU#Y&v87-=j=I?PDNgGhXZmZScb01`WUZk{Uf zF*Yk^Mq&>lyVs4MTll)PO#iO(|{%bt9K9j|+Xa zX%teFLh3+IKp^5C7PFZwl~9RAte{>NZO-(pJ*OIW7xR@m1~(9+RV4^k zE4mnMDMx$EXiquXZ$|qcMi1T$e%jaq2LYmN+PZ-#$AH~>_O}GEcHr zh7sGtFcGd%@0VcTGu<`Cw#qU67-s5SP_2;~1hLND=Q)#mZk~&8AgDh9hjj(j1{(K& zo56MU)Qth*m6hTcPx_2+Vn=df1X8}5q@)Fwtr`%}!Y~bIc`Ha*re$FgB7*|vPEmh_ zipEYBRJ91&?PuD}!UTE_wni^vwc7Mx@LbFE*f}!f`xBU95U7MQQ{!#-CP=22?UH|E8Bh^r=` zGBv#2Tie#la#Sw?j1PVrD-wjaB8E?_)b>FCmuFg$d1409S6Ben6*#jCElavHm_dpi zFSmA1QrM;C;kuiXulGsH4@Y0Z3Jn8AiQOX% ztacb*k5sJiX9)c=RBx02c>MuxiEjwJ?D=JOnP9uTD;T)wS&sSxW1gkW{=kkq-Ae?T zy9bxCS>}a6`|>GIbD)1Y5ezir#NEq$FwpZjx+&0azYpIs9N%Q|BgIkUP-4^=(x_~w z@EaY{tN0s}p|f0{6~Y8~B8{^UVwKc9BTf|*TLx!vz`k%Xm2rMKU>7cH6Z`d`^0Qmz zM+A1aqIkWg$FLg2iizP0_Byc=pnAd`w@=|k`h9o=BSv(TD_j~po=0TIBhvK`());X p{+sN4MEV|)1B+YQ9+2ol)2_Mtg@#RYk+14r^LqNfBv>=P{{!)#2gCpX literal 0 HcmV?d00001 diff --git a/api_client.py b/api_client.py new file mode 100644 index 0000000..8f0d829 --- /dev/null +++ b/api_client.py @@ -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 diff --git a/icon.png b/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..e9b19f833f7369e3a7a8f9e4730b51cf6f7876d2 GIT binary patch literal 464 zcmV;>0WbcEP)TntD7%YdK3 zr~_$WeKEXnCT;UQAT*%ySIR(K(fab=HmB{GQnX@m#s0$qN9B|6G+FZCGV{~w8HcrF z0D|5iGl7U3SwOj|x_i1OQ_gnwI}aHQ({Llju|eIcvt3G~(Va6(TWlB8n6IW|1CLX| ztG1c+PGDnmW!?f1@lBynSlYseudiORMo#ywXrs|=Ty6reI=zA*Ze-384a)y-;5g)W z1YzDE>#SYt+z$Dt3_qgt`UT~eYS+uah@ZK!*}jqV(~o6L$^@LY5ra-mryJQ||gPdvWVdYZrE*x%+YAu?`D^YPlw)ma68iH&oQSr5CBL4=zaDwz 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 diff --git a/metadata.txt b/metadata.txt new file mode 100644 index 0000000..80fab97 --- /dev/null +++ b/metadata.txt @@ -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 diff --git a/notification_server.py b/notification_server.py new file mode 100644 index 0000000..ac4032c --- /dev/null +++ b/notification_server.py @@ -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, + ) diff --git a/plugin.py b/plugin.py new file mode 100644 index 0000000..2055e4d --- /dev/null +++ b/plugin.py @@ -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) + + diff --git a/resources.qrc b/resources.qrc new file mode 100644 index 0000000..c6a44f7 --- /dev/null +++ b/resources.qrc @@ -0,0 +1,4 @@ + + + +