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.
This commit is contained in:
1 parent
aec665179f
commit
6e5676c890
4 files changed
+93
-76
No files matched your search
+26
-17
@@ -229,20 +229,28 @@ may not even read the same way there.
|
|||||||
drop-in — but sourced differently. This app has its own GNSS receiver
|
drop-in — but sourced differently. This app has its own GNSS receiver
|
||||||
(gpsd, possibly `bin/gnss_simulator.rb`'s simulated one — see
|
(gpsd, possibly `bin/gnss_simulator.rb`'s simulated one — see
|
||||||
`docs/development.adoc`/`docs/deployment.adoc`), and "find my
|
`docs/development.adoc`/`docs/deployment.adoc`), and "find my
|
||||||
location" should mean that receiver first, not a device's separate
|
location" should mean that receiver, not a device's separate (and
|
||||||
(and often less accurate, e.g. wifi-based) location service, when the
|
often less accurate, e.g. wifi-based) location service — but only
|
||||||
receiver actually has a fix. Clicking it checks
|
once it's actually turned on, via the sidebar's "GNSS" icon
|
||||||
`GET /api/gnss/position` once: if `available`, it polls that
|
(`GnssControl`, `public/js/gnss.js`), not just whenever a receiver
|
||||||
endpoint (same 1.5s cadence as `GnssControl`'s own sidebar polling)
|
happens to have a fix. `GnssControl` itself now recenters the map on
|
||||||
and shows the fix with the same `.gnss-marker` styling that control
|
every fix while enabled (`flyTo` for the first one, `easeTo` — a
|
||||||
uses; if not, it falls back to the browser's own
|
plainer follow, no zoom swoop — for each update after), the same way
|
||||||
`navigator.geolocation.watchPosition`, GeolocateControl's own
|
LocateControl's own browser-geolocation fallback follows below;
|
||||||
mechanism. Either way, only the *first* fix recenters the map —
|
clicking Locate while GNSS is already on just jumps to
|
||||||
later updates move the marker without fighting a pan/zoom the visitor
|
`GnssControl#getPosition()`'s current fix for the same instant
|
||||||
made in the meantime, unlike GeolocateControl's own default of
|
feedback the button has always given, reusing its existing
|
||||||
recentering on every update. Clicking again stops tracking (clears
|
`.gnss-marker` rather than placing a second one — GNSS following the
|
||||||
the poll timer or browser watch, whichever was active) and removes
|
map is `GnssControl`'s job for as long as it's enabled, regardless of
|
||||||
the marker.
|
whether Locate was ever clicked. Both controls are constructed
|
||||||
|
together in `map.js` (`GnssControl` first, `LocateControl` holding
|
||||||
|
that same instance) specifically so `LocateControl` can call
|
||||||
|
`isEnabled()`/`getPosition()` on it. Only when GNSS is off does
|
||||||
|
Locate fall back to the browser's own
|
||||||
|
`navigator.geolocation.watchPosition` — GeolocateControl's own
|
||||||
|
mechanism — with its own toggle/marker/recentering-per-fix, since
|
||||||
|
that's the one case this control does own the tracking itself;
|
||||||
|
clicking again in that case stops the watch and removes the marker.
|
||||||
|
|
||||||
=== Debug inspector (Alt+click)
|
=== Debug inspector (Alt+click)
|
||||||
|
|
||||||
@@ -665,9 +673,10 @@ toggling is a cheap re-render of just that strip.
|
|||||||
|
|
||||||
A "GNSS" map control (`public/js/gnss.js`) shows the fix from a local
|
A "GNSS" map control (`public/js/gnss.js`) shows the fix from a local
|
||||||
GNSS receiver (a real one, plugged into the machine running the
|
GNSS receiver (a real one, plugged into the machine running the
|
||||||
server — distinct from `NavigationControl`'s browser
|
server, or `bin/gnss_simulator.rb`'s simulated one — distinct from
|
||||||
`navigator.geolocation`, which the OS may or may not derive from such
|
`LocateControl`'s browser-geolocation fallback, `navigator.geolocation`,
|
||||||
a receiver at all).
|
which the OS may or may not derive from such a receiver at all —
|
||||||
|
see "Finding your location" above).
|
||||||
|
|
||||||
`lib/maptool/gpsd_client.rb` is a small client for
|
`lib/maptool/gpsd_client.rb` is a small client for
|
||||||
https://gpsd.io[gpsd]'s JSON protocol over TCP (port 2947 by default):
|
https://gpsd.io[gpsd]'s JSON protocol over TCP (port 2947 by default):
|
||||||
|
|||||||
@@ -74,6 +74,12 @@ export class GnssControl {
|
|||||||
this._status.textContent = fixLabel(data);
|
this._status.textContent = fixLabel(data);
|
||||||
|
|
||||||
const lngLat = [data.lon, data.lat];
|
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) {
|
if (!this._marker) {
|
||||||
const el = document.createElement("div");
|
const el = document.createElement("div");
|
||||||
el.className = "gnss-marker";
|
el.className = "gnss-marker";
|
||||||
@@ -82,8 +88,10 @@ export class GnssControl {
|
|||||||
// survives map.setStyle() (switching Online/Offline) with no extra
|
// survives map.setStyle() (switching Online/Offline) with no extra
|
||||||
// re-add handling needed.
|
// re-add handling needed.
|
||||||
this._marker = new Marker({ element: el }).setLngLat(lngLat).addTo(this._map);
|
this._marker = new Marker({ element: el }).setLngLat(lngLat).addTo(this._map);
|
||||||
|
this._map.flyTo({ center: lngLat, zoom: Math.max(this._map.getZoom(), 15) });
|
||||||
} else {
|
} else {
|
||||||
this._marker.setLngLat(lngLat);
|
this._marker.setLngLat(lngLat);
|
||||||
|
this._map.easeTo({ center: lngLat });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,6 +101,21 @@ export class GnssControl {
|
|||||||
this._marker = null;
|
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 }) {
|
function fixLabel({ mode, eph }) {
|
||||||
|
|||||||
+33
-54
@@ -1,24 +1,24 @@
|
|||||||
import { Marker } from "/vendor/maplibre-gl/maplibre-gl.mjs";
|
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)
|
// Replaces MapLibre's own GeolocateControl (browser Geolocation API only)
|
||||||
// with one that prefers this app's own GNSS receiver (gpsd, via
|
// with one that prefers this app's own GNSS receiver when it's turned on
|
||||||
// /api/gnss/position) when it has a fix, falling back to the browser's own
|
// (the sidebar "GNSS" icon, GnssControl) over a phone/laptop's separate
|
||||||
// geolocation otherwise — "find my location" should mean this app's own
|
// (and often less accurate, e.g. wifi-based) location service. GNSS itself
|
||||||
// receiver first, not a phone/laptop's separate (and often less accurate,
|
// is only ever used here when explicitly enabled via that sidebar
|
||||||
// e.g. wifi-based) location service, when both are available.
|
// 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
|
// Reuses MapLibre's own geolocate button styling (same classes/icon
|
||||||
// GeolocateControl itself uses, maplibre-gl.css) rather than a new icon —
|
// GeolocateControl itself uses, maplibre-gl.css) rather than a new icon —
|
||||||
// visually a drop-in replacement, just sourced differently.
|
// visually a drop-in replacement, just sourced differently.
|
||||||
export class LocateControl {
|
export class LocateControl {
|
||||||
constructor() {
|
constructor(gnssControl) {
|
||||||
|
this._gnssControl = gnssControl;
|
||||||
this._active = false;
|
this._active = false;
|
||||||
this._marker = null;
|
this._marker = null;
|
||||||
this._watchId = null;
|
this._watchId = null;
|
||||||
this._pollTimer = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onAdd(map) {
|
onAdd(map) {
|
||||||
@@ -32,7 +32,7 @@ export class LocateControl {
|
|||||||
icon.className = "maplibregl-ctrl-icon";
|
icon.className = "maplibregl-ctrl-icon";
|
||||||
icon.setAttribute("aria-hidden", "true");
|
icon.setAttribute("aria-hidden", "true");
|
||||||
this._button.appendChild(icon);
|
this._button.appendChild(icon);
|
||||||
this._button.addEventListener("click", () => this._toggle());
|
this._button.addEventListener("click", () => this._click());
|
||||||
|
|
||||||
this._container = document.createElement("div");
|
this._container = document.createElement("div");
|
||||||
this._container.className = "maplibregl-ctrl maplibregl-ctrl-group";
|
this._container.className = "maplibregl-ctrl maplibregl-ctrl-group";
|
||||||
@@ -49,7 +49,21 @@ export class LocateControl {
|
|||||||
return "top-left";
|
return "top-left";
|
||||||
}
|
}
|
||||||
|
|
||||||
_toggle() {
|
_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) {
|
if (this._active) {
|
||||||
this._stop();
|
this._stop();
|
||||||
} else {
|
} else {
|
||||||
@@ -57,46 +71,9 @@ export class LocateControl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async _start() {
|
_start() {
|
||||||
this._active = true;
|
this._active = true;
|
||||||
this._button.classList.add("maplibregl-ctrl-geolocate-active");
|
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(
|
this._watchId = navigator.geolocation.watchPosition(
|
||||||
(position) => this._setPosition(position.coords.longitude, position.coords.latitude),
|
(position) => this._setPosition(position.coords.longitude, position.coords.latitude),
|
||||||
() => {
|
() => {
|
||||||
@@ -107,6 +84,11 @@ export class LocateControl {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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) {
|
_setPosition(lon, lat) {
|
||||||
if (!this._marker) {
|
if (!this._marker) {
|
||||||
const el = document.createElement("div");
|
const el = document.createElement("div");
|
||||||
@@ -115,16 +97,13 @@ export class LocateControl {
|
|||||||
this._map.flyTo({ center: [lon, lat], zoom: Math.max(this._map.getZoom(), 15) });
|
this._map.flyTo({ center: [lon, lat], zoom: Math.max(this._map.getZoom(), 15) });
|
||||||
} else {
|
} else {
|
||||||
this._marker.setLngLat([lon, lat]);
|
this._marker.setLngLat([lon, lat]);
|
||||||
|
this._map.easeTo({ center: [lon, lat] });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_stop() {
|
_stop() {
|
||||||
this._active = false;
|
this._active = false;
|
||||||
this._button.classList.remove("maplibregl-ctrl-geolocate-active");
|
this._button.classList.remove("maplibregl-ctrl-geolocate-active");
|
||||||
if (this._pollTimer) {
|
|
||||||
clearInterval(this._pollTimer);
|
|
||||||
this._pollTimer = null;
|
|
||||||
}
|
|
||||||
if (this._watchId !== null) {
|
if (this._watchId !== null) {
|
||||||
navigator.geolocation.clearWatch(this._watchId);
|
navigator.geolocation.clearWatch(this._watchId);
|
||||||
this._watchId = null;
|
this._watchId = null;
|
||||||
|
|||||||
+11
-5
@@ -360,12 +360,18 @@ map.addControl(new HomeButton({ bounds: SLOVAKIA_BOUNDS }), "top-left");
|
|||||||
map.addControl(new NavigationControl({ showCompass: false }), "top-left");
|
map.addControl(new NavigationControl({ showCompass: false }), "top-left");
|
||||||
map.addControl(new ZoomLevelControl(), "top-left");
|
map.addControl(new ZoomLevelControl(), "top-left");
|
||||||
map.addControl(new MeasureControl(), "top-left");
|
map.addControl(new MeasureControl(), "top-left");
|
||||||
|
// Constructed here (rather than down where it's mounted, alongside the
|
||||||
|
// other sidebar controls) so LocateControl can hold the same instance —
|
||||||
|
// its constructor only touches `map`, mount() (which needs the sidebar
|
||||||
|
// element) can still happen later.
|
||||||
|
const gnssControl = new GnssControl(map);
|
||||||
// Not MapLibre's own GeolocateControl (browser Geolocation API only) —
|
// Not MapLibre's own GeolocateControl (browser Geolocation API only) —
|
||||||
// this app has its own GNSS receiver (gpsd, possibly a simulated one, see
|
// 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
|
// 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
|
// device's separate, often less accurate location service, but only once
|
||||||
// fix. See locate.js.
|
// the "GNSS" sidebar icon actually turns it on — not just whenever a
|
||||||
map.addControl(new LocateControl(), "top-left");
|
// receiver happens to have a fix. See locate.js.
|
||||||
|
map.addControl(new LocateControl(gnssControl), "top-left");
|
||||||
|
|
||||||
// A small inset map showing where the current view sits in context — NPS's
|
// 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
|
// own overview plugin (docs/nps-resources.adoc's "overview" entry, still on
|
||||||
@@ -398,7 +404,7 @@ const profilePanel = new TrackProfilePanel(map);
|
|||||||
profilePanel.mount();
|
profilePanel.mount();
|
||||||
new GpxPanelControl(map, profilePanel).mount(sidebar);
|
new GpxPanelControl(map, profilePanel).mount(sidebar);
|
||||||
new RadarControl(map).mount(sidebar);
|
new RadarControl(map).mount(sidebar);
|
||||||
new GnssControl(map).mount(sidebar);
|
gnssControl.mount(sidebar);
|
||||||
new LiveTrackPanelControl(map).mount(sidebar);
|
new LiveTrackPanelControl(map).mount(sidebar);
|
||||||
|
|
||||||
function applyStyle(name) {
|
function applyStyle(name) {
|
||||||
|
|||||||
Reference in new issue
Block a user