import { Marker } from "/vendor/maplibre-gl/maplibre-gl.mjs"; import { GNSS_ICON } from "/js/icons.js"; const POSITION_URL = "/api/gnss/position"; const POLL_INTERVAL_MS = 1500; // A sidebar icon that polls /api/gnss/position (backed server-side by a // gpsd connection, see lib/maptool/gpsd_client.rb) and shows the fix as a // marker, with fix status in a flyout. Polling rather than a push channel // is deliberate — see the comment in app.rb next to GNSS_LATEST — and fits // gpsd's own ~1Hz report rate fine. export class GnssControl { constructor(map) { this._map = map; this._marker = null; this._timer = null; this._enabled = false; } mount(sidebar) { this._container = document.createElement("div"); this._container.className = "sidebar-item"; this._container.innerHTML = `
`; this._icon = this._container.querySelector(".sidebar-icon"); this._flyout = this._container.querySelector(".sidebar-flyout"); this._status = this._container.querySelector(".gnss-control__status"); this._icon.addEventListener("click", () => this._setEnabled(!this._enabled)); sidebar.appendChild(this._container); } _setEnabled(enabled) { this._enabled = enabled; this._icon.classList.toggle("active", enabled); this._flyout.hidden = !enabled; if (enabled) { this._poll(); this._timer = setInterval(() => this._poll(), POLL_INTERVAL_MS); } else { this._stopPolling(); this._removeMarker(); this._status.textContent = ""; } } _stopPolling() { if (this._timer) { clearInterval(this._timer); this._timer = null; } } async _poll() { let data; try { const response = await fetch(POSITION_URL); data = await response.json(); } catch (err) { this._status.textContent = "unreachable"; return; } if (!data.available) { this._removeMarker(); this._status.textContent = "no fix"; return; } this._status.textContent = fixLabel(data); const lngLat = [data.lon, data.lat]; if (!this._marker) { const el = document.createElement("div"); el.className = "gnss-marker"; // MapLibre markers are plain positioned DOM elements, not tied to the // style's sources/layers, so — unlike the radar/GPX layers — this // survives map.setStyle() (switching Online/Offline) with no extra // re-add handling needed. this._marker = new Marker({ element: el }).setLngLat(lngLat).addTo(this._map); } else { this._marker.setLngLat(lngLat); } } _removeMarker() { if (this._marker) { this._marker.remove(); this._marker = null; } } } function fixLabel({ mode, eph }) { const kind = mode === 3 ? "3D" : mode === 2 ? "2D" : "no fix"; return eph ? `${kind} (±${Math.round(eph)}m)` : kind; }