LocateControl (public/js/locate.js) replaces MapLibre's own GeolocateControl -- same button, reusing its exact CSS/icon so it's a visual drop-in, but sourced differently. Checks GET /api/gnss/position once on click: if a fix is available (gpsd, possibly bin/gnss_simulator.rb's simulated one), tracks that instead of the browser's separate (and often less accurate) location service, which is now only a fallback. Only the first fix recenters the map; later updates just move the marker, unlike GeolocateControl's own default of recentering on every update, which would fight a pan/zoom made in the meantime. Verified end to end with the GNSS simulator running: clicking the button centered on the simulated position and dropped a marker, clicking again cleared both, no console errors.
138 lines
4.1 KiB
JavaScript
138 lines
4.1 KiB
JavaScript
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;
|
|
}
|
|
}
|
|
}
|