Two fixes based on feedback after the last commit: 1. The map wasn't following the position at all -- only the first fix recentered, later updates just moved the marker. Now every fix recenters (easeTo, not flyTo, after the first one, so it doesn't repeat that zoom swoop on every update) -- matches how GeolocateControl's own trackUserLocation always behaved. 2. GNSS should only be used when explicitly turned on via the sidebar "GNSS" icon, not whenever a receiver happens to have a fix -- simpler to test against the simulator that way, and puts the visitor in control of whether GNSS is engaged at all. GnssControl now owns following the map on its own (same flyTo-once/easeTo-after pattern) whenever it's enabled; LocateControl checks GnssControl#isEnabled() and, when true, just jumps to #getPosition()'s current fix and reuses its existing .gnss-marker instead of polling/marking on its own. Only falls back to the browser's own geolocation when GNSS is off. Verified end to end with the GNSS simulator: enabling sidebar GNSS recenters immediately and keeps following over several polls; clicking Locate while GNSS is on jumps to the same position without creating a second marker or touching its own active state; turning GNSS back off and clicking Locate correctly falls through to the browser geolocation path instead.
125 lines
4.1 KiB
JavaScript
125 lines
4.1 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;
|
|
}
|
|
|
|
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 = "";
|
|
}
|
|
}
|
|
|
|
_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;
|
|
}
|