import { Marker } from "/vendor/maplibre-gl/maplibre-gl.mjs"; const GNSS_URL = "/api/gnss/position"; const POLL_INTERVAL_MS = 1500; // matches gnss.js's own polling cadence // Replaces MapLibre's own GeolocateControl (browser Geolocation API only) // with one that prefers this app's own GNSS receiver (gpsd, via // /api/gnss/position) when it has a fix, falling back to the browser's own // geolocation otherwise — "find my location" should mean this app's own // receiver first, not a phone/laptop's separate (and often less accurate, // e.g. wifi-based) location service, when both are available. // // Reuses MapLibre's own geolocate button styling (same classes/icon // GeolocateControl itself uses, maplibre-gl.css) rather than a new icon — // visually a drop-in replacement, just sourced differently. export class LocateControl { constructor() { this._active = false; this._marker = null; this._watchId = null; this._pollTimer = null; } onAdd(map) { this._map = map; this._button = document.createElement("button"); this._button.type = "button"; this._button.className = "maplibregl-ctrl-geolocate"; this._button.title = "Find my location"; this._button.setAttribute("aria-label", "Find my location"); const icon = document.createElement("span"); icon.className = "maplibregl-ctrl-icon"; icon.setAttribute("aria-hidden", "true"); this._button.appendChild(icon); this._button.addEventListener("click", () => this._toggle()); this._container = document.createElement("div"); this._container.className = "maplibregl-ctrl maplibregl-ctrl-group"; this._container.appendChild(this._button); return this._container; } onRemove() { this._stop(); this._container.remove(); } getDefaultPosition() { return "top-left"; } _toggle() { if (this._active) { this._stop(); } else { this._start(); } } async _start() { this._active = true; this._button.classList.add("maplibregl-ctrl-geolocate-active"); let data; try { const response = await fetch(GNSS_URL); data = await response.json(); } catch (err) { data = { available: false }; } if (data.available) { this._trackGnss(); } else { this._trackBrowser(); } } // Keeps the marker updated on every poll, but only recenters the map on // the very first fix — GeolocateControl's own default behavior keeps // recentering on every update too, but replicating its "stop fighting // the user once they've panned away" state machine isn't worth it here; // once is enough for what this button is for. _trackGnss() { const poll = async () => { let data; try { const response = await fetch(GNSS_URL); data = await response.json(); } catch (err) { return; // transient — try again next poll rather than falling back mid-track } if (data.available) this._setPosition(data.lon, data.lat); }; poll(); this._pollTimer = setInterval(poll, POLL_INTERVAL_MS); } _trackBrowser() { this._watchId = navigator.geolocation.watchPosition( (position) => this._setPosition(position.coords.longitude, position.coords.latitude), () => { window.alert("Could not determine your location."); this._stop(); }, { enableHighAccuracy: true }, ); } _setPosition(lon, lat) { if (!this._marker) { const el = document.createElement("div"); el.className = "gnss-marker"; this._marker = new Marker({ element: el }).setLngLat([lon, lat]).addTo(this._map); this._map.flyTo({ center: [lon, lat], zoom: Math.max(this._map.getZoom(), 15) }); } else { this._marker.setLngLat([lon, lat]); } } _stop() { this._active = false; this._button.classList.remove("maplibregl-ctrl-geolocate-active"); if (this._pollTimer) { clearInterval(this._pollTimer); this._pollTimer = null; } if (this._watchId !== null) { navigator.geolocation.clearWatch(this._watchId); this._watchId = null; } if (this._marker) { this._marker.remove(); this._marker = null; } } }