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.
151 lines
5.8 KiB
JavaScript
151 lines
5.8 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);
|
|
|
|
// GNSS turning on mid-track (this control's own browser-geolocation
|
|
// fallback, not GnssControl's — that one manages its own tracking)
|
|
// should switch over immediately, not just the next time this button
|
|
// gets clicked; the browser's own position has no business still
|
|
// being used once the app's own receiver has a fix. No alertIfNoFix
|
|
// here (unlike a deliberate click, below) — GnssControl only just
|
|
// turned on and almost certainly hasn't gotten its first fix back yet
|
|
// (its own poll is async, still in flight at the moment this fires),
|
|
// so this would otherwise pop a blocking "waiting for a fix" dialog on
|
|
// every single enable; GnssControl's own next poll will show/recenter
|
|
// once it actually has something, same as it would either way.
|
|
this._gnssControl.onEnabledChange((enabled) => {
|
|
if (enabled && this._active) {
|
|
this._stop();
|
|
this._jumpToGnss();
|
|
}
|
|
});
|
|
|
|
return this._container;
|
|
}
|
|
|
|
onRemove() {
|
|
this._stop();
|
|
this._container.remove();
|
|
}
|
|
|
|
getDefaultPosition() {
|
|
return "top-left";
|
|
}
|
|
|
|
_click() {
|
|
if (this._gnssControl.isEnabled()) {
|
|
this._jumpToGnss({ alertIfNoFix: true });
|
|
return;
|
|
}
|
|
|
|
if (this._active) {
|
|
this._stop();
|
|
} else {
|
|
this._start();
|
|
}
|
|
}
|
|
|
|
// 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, for the same instant feedback clicking this button has
|
|
// always given. alertIfNoFix is opt-in (a deliberate click wants to know
|
|
// why nothing happened; the automatic mid-track handoff above doesn't —
|
|
// see its own comment) since a JS alert() blocks the whole page until
|
|
// dismissed, not something to pop up on a path that isn't a direct user
|
|
// action.
|
|
_jumpToGnss({ alertIfNoFix = false } = {}) {
|
|
const position = this._gnssControl.getPosition();
|
|
if (position) {
|
|
this._map.flyTo({ center: position, zoom: Math.max(this._map.getZoom(), 15) });
|
|
} else if (alertIfNoFix) {
|
|
window.alert("Waiting for a GNSS fix…");
|
|
}
|
|
}
|
|
|
|
_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.
|
|
//
|
|
// MapLibre's own blue pulsing "user location" dot styling
|
|
// (.maplibregl-user-location-dot, maplibre-gl.css — the exact look
|
|
// GeolocateControl itself used here before), not GnssControl's green
|
|
// .gnss-marker — reusing that green for a browser-estimated position too
|
|
// looked like this control was somehow still using GNSS data even when
|
|
// it very much wasn't; distinct colors for distinct sources.
|
|
_setPosition(lon, lat) {
|
|
if (!this._marker) {
|
|
const el = document.createElement("div");
|
|
el.className = "maplibregl-user-location-dot";
|
|
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;
|
|
}
|
|
}
|
|
}
|