Files
maptool/public/js/locate.js
T
randy 6e5676c890
CI / check (push) Successful in 44s
CI / deploy (push) Successful in 18s
Make "find my location" actually follow GNSS, gated on the sidebar toggle
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.
2026-09-08 00:56:25 +02:00

117 lines
4.1 KiB
JavaScript

import { Marker } from "/vendor/maplibre-gl/maplibre-gl.mjs";
// Replaces MapLibre's own GeolocateControl (browser Geolocation API only)
// with one that prefers this app's own GNSS receiver when it's turned on
// (the sidebar "GNSS" icon, GnssControl) over a phone/laptop's separate
// (and often less accurate, e.g. wifi-based) location service. GNSS itself
// is only ever used here when explicitly enabled via that sidebar
// control — not just whenever a receiver happens to have a fix — so this
// doesn't do any polling of its own for that case; GnssControl already
// does, and already keeps the map following it once enabled, the same way
// this control's own browser-geolocation fallback follows below.
//
// 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(gnssControl) {
this._gnssControl = gnssControl;
this._active = false;
this._marker = null;
this._watchId = 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._click());
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";
}
_click() {
if (this._gnssControl.isEnabled()) {
// GNSS already follows the map on its own once enabled (GnssControl)
// and has no separate "off" state to toggle from here — just jump to
// its current fix now, for the same instant feedback clicking this
// button has always given.
const position = this._gnssControl.getPosition();
if (position) {
this._map.flyTo({ center: position, zoom: Math.max(this._map.getZoom(), 15) });
} else {
window.alert("Waiting for a GNSS fix…");
}
return;
}
if (this._active) {
this._stop();
} else {
this._start();
}
}
_start() {
this._active = true;
this._button.classList.add("maplibregl-ctrl-geolocate-active");
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 },
);
}
// Recenters on every fix, following the position the way GeolocateControl
// itself does while tracking — 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 pan/zoom-preserving follow
// rather than repeating that same swoop on every update.
_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]);
this._map.easeTo({ center: [lon, lat] });
}
}
_stop() {
this._active = false;
this._button.classList.remove("maplibregl-ctrl-geolocate-active");
if (this._watchId !== null) {
navigator.geolocation.clearWatch(this._watchId);
this._watchId = null;
}
if (this._marker) {
this._marker.remove();
this._marker = null;
}
}
}