Prefer this app's own GNSS receiver for "find my location"
CI / check (push) Successful in 41s
CI / deploy (push) Successful in 22s

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.
This commit is contained in:
randy committed 2026-09-08 00:46:14 +02:00
1 parent 64a85afe6a
commit aec665179f
3 files changed
+176 -17

No files matched your search

+137
View File
@@ -0,0 +1,137 @@
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;
}
}
}
+8 -10
View File
@@ -1,7 +1,6 @@
import {
Map as MaplibreMap,
NavigationControl,
GeolocateControl,
Popup,
addProtocol,
} from "/vendor/maplibre-gl/maplibre-gl.mjs";
@@ -11,6 +10,7 @@ import { GnssControl } from "/js/gnss.js";
import { LiveTrackPanelControl } from "/js/livetrack.js";
import { TrackProfilePanel } from "/js/track_profile.js";
import { MeasureControl } from "/js/measure.js";
import { LocateControl } from "/js/locate.js";
import { LAYERS_ICON } from "/js/icons.js";
import { notifySidePanelOpen, onlyOneSidePanelOpen } from "/js/panels.js";
import HomeButton from "/vendor/npmap-home-button/home-button.esm.js";
@@ -349,7 +349,7 @@ const map = new MaplibreMap({
// above), "current view" and "the default view" are no longer the same
// thing, and a home button that resets to wherever you happened to load
// the page wouldn't be a home button. This is *not* "go to my location" —
// that's GeolocateControl, below.
// that's LocateControl, below.
const SLOVAKIA_BOUNDS = [
[16.83, 47.73],
[22.57, 49.61],
@@ -360,14 +360,12 @@ map.addControl(new HomeButton({ bounds: SLOVAKIA_BOUNDS }), "top-left");
map.addControl(new NavigationControl({ showCompass: false }), "top-left");
map.addControl(new ZoomLevelControl(), "top-left");
map.addControl(new MeasureControl(), "top-left");
map.addControl(
new GeolocateControl({
positionOptions: { enableHighAccuracy: true },
trackUserLocation: true,
showAccuracyCircle: true,
}),
"top-left",
);
// Not MapLibre's own GeolocateControl (browser Geolocation API only) —
// this app has its own GNSS receiver (gpsd, possibly a simulated one, see
// bin/gnss_simulator.rb) and "find my location" should prefer that over a
// device's separate, often less accurate location service when it has a
// fix. See locate.js.
map.addControl(new LocateControl(), "top-left");
// A small inset map showing where the current view sits in context — NPS's
// own overview plugin (docs/nps-resources.adoc's "overview" entry, still on