If Locate was already tracking via the browser-geolocation fallback (GNSS was off when clicked) and GNSS then gets turned on via the sidebar, it kept using the browser position until the button was clicked again -- the browser's own (often less accurate) position had no business still being used once the app's own receiver had a fix. GnssControl#onEnabledChange (a small internal callback list) notifies LocateControl the moment the sidebar toggle changes; if it's actively tracking via the browser at that point, it stops that and jumps to GNSS's position instead. Caught a real bug while verifying this: the automatic handoff reused the same jump-to-GNSS helper a deliberate click uses, which alerts if GNSS has no fix yet -- but GNSS only just turned on and almost certainly hasn't gotten its first poll response back yet at the exact moment the handoff fires, so this popped a blocking window.alert() (which halts the whole page until dismissed) on every single enable. A test hung on exactly this before I noticed. Fixed with an alertIfNoFix option, true only for the deliberate-click path. Verified end to end (simulating the "already tracking" state directly since headless geolocation isn't reliable enough to trust as the trigger): toggling GNSS mid-track removes the blue browser marker, shows the green GNSS one once its poll lands, and no dialog appears.
136 lines
4.6 KiB
JavaScript
136 lines
4.6 KiB
JavaScript
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;
|
|
this._enabledListeners = [];
|
|
}
|
|
|
|
// Read by LocateControl: if it's mid-tracking on the browser-geolocation
|
|
// fallback when GNSS turns on, GNSS should take over there and then, not
|
|
// only the next time the button happens to get clicked — the browser's
|
|
// own (often less accurate) position has no business still being used
|
|
// once the app's own receiver has a fix.
|
|
onEnabledChange(callback) {
|
|
this._enabledListeners.push(callback);
|
|
}
|
|
|
|
mount(sidebar) {
|
|
this._container = document.createElement("div");
|
|
this._container.className = "sidebar-item";
|
|
this._container.innerHTML = `
|
|
<button type="button" class="sidebar-icon" title="GNSS" aria-label="GNSS">${GNSS_ICON}</button>
|
|
<div class="sidebar-flyout" hidden>
|
|
<span class="gnss-control__status"></span>
|
|
</div>
|
|
`;
|
|
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 = "";
|
|
}
|
|
this._enabledListeners.forEach((callback) => callback(enabled));
|
|
}
|
|
|
|
_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];
|
|
// Recenters on every fix while enabled — this is the app's own
|
|
// receiver, so "GNSS on" means "follow it", the same way LocateControl
|
|
// (public/js/locate.js) follows it when GNSS is what it's using too.
|
|
// flyTo (with its zoom swoop) only for the first fix, since that's the
|
|
// one jumping from wherever the map already was; easeTo for later
|
|
// updates, a plain follow rather than repeating that swoop every poll.
|
|
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);
|
|
this._map.flyTo({ center: lngLat, zoom: Math.max(this._map.getZoom(), 15) });
|
|
} else {
|
|
this._marker.setLngLat(lngLat);
|
|
this._map.easeTo({ center: lngLat });
|
|
}
|
|
}
|
|
|
|
_removeMarker() {
|
|
if (this._marker) {
|
|
this._marker.remove();
|
|
this._marker = null;
|
|
}
|
|
}
|
|
|
|
// Read by LocateControl: GNSS is only ever used for "find my location"
|
|
// when explicitly turned on here — clicking Locate while this is off
|
|
// falls back to the browser's own geolocation instead, rather than
|
|
// silently switching sources on whatever GET /api/gnss/position happens
|
|
// to report at that moment.
|
|
isEnabled() {
|
|
return this._enabled;
|
|
}
|
|
|
|
// The current fix, if any — null both when disabled and when enabled
|
|
// but not fixed yet (no marker placed either way).
|
|
getPosition() {
|
|
return this._marker ? this._marker.getLngLat() : null;
|
|
}
|
|
}
|
|
|
|
function fixLabel({ mode, eph }) {
|
|
const kind = mode === 3 ? "3D" : mode === 2 ? "2D" : "no fix";
|
|
return eph ? `${kind} (±${Math.round(eph)}m)` : kind;
|
|
}
|