Prefer this app's own GNSS receiver for "find my location"
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:
1 parent
64a85afe6a
commit
aec665179f
3 files changed
+176
-17
No files matched your search
+31
-7
@@ -166,8 +166,8 @@ sits top-left on the map. It fits to `SLOVAKIA_BOUNDS` (a country-wide
|
|||||||
bounding box) rather than the plain `CENTER`/`ZOOM` point the map
|
bounding box) rather than the plain `CENTER`/`ZOOM` point the map
|
||||||
itself starts at — "home" is meant as a full-country overview, not a
|
itself starts at — "home" is meant as a full-country overview, not a
|
||||||
re-centering on Bratislava. It resets to that fixed default view, not
|
re-centering on Bratislava. It resets to that fixed default view, not
|
||||||
the visitor's own location — that's `GeolocateControl`, a separate
|
the visitor's own location — that's `LocateControl`, a separate button
|
||||||
button in the same stack (below).
|
in the same stack (below).
|
||||||
|
|
||||||
That Home button is the top of a small top-left stack — Home, zoom
|
That Home button is the top of a small top-left stack — Home, zoom
|
||||||
in/out (`NavigationControl`, `showCompass: false` since this map never
|
in/out (`NavigationControl`, `showCompass: false` since this map never
|
||||||
@@ -175,11 +175,12 @@ rotates), a passive zoom-level readout (`ZoomLevelControl`, a plain div
|
|||||||
updated on the map's `zoom` event — not a button, since it has no
|
updated on the map's `zoom` event — not a button, since it has no
|
||||||
click behavior of its own), a distance/area measuring tool
|
click behavior of its own), a distance/area measuring tool
|
||||||
(`MeasureControl`, `public/js/measure.js`), locate-me
|
(`MeasureControl`, `public/js/measure.js`), locate-me
|
||||||
(`GeolocateControl`), and a "Base map" button — matching the National
|
(`LocateControl`, `public/js/locate.js`), and a "Base map" button —
|
||||||
Park Service's own map control layout (grouped together on the map
|
matching the National Park Service's own map control layout (grouped
|
||||||
itself, rather than split across corners or between the map and a side
|
together on the map itself, rather than split across corners or
|
||||||
panel). The last of those, `MapLayersButton` (`public/js/map.js`), is
|
between the map and a side panel). The last of those,
|
||||||
a second entry point into the exact same Online/Offline panel
|
`MapLayersButton` (`public/js/map.js`), is a second entry point into
|
||||||
|
the exact same Online/Offline panel
|
||||||
`LayerToggleControl` already manages in the page's own sidebar — both
|
`LayerToggleControl` already manages in the page's own sidebar — both
|
||||||
call the same `LayerToggleControl#toggle()`, so there's one source of
|
call the same `LayerToggleControl#toggle()`, so there's one source of
|
||||||
truth for the panel's open/closed state either way.
|
truth for the panel's open/closed state either way.
|
||||||
@@ -220,6 +221,29 @@ long any one leg was on its own. Area mode has no equivalent yet;
|
|||||||
a polygon's edges are its perimeter, not a path, so the same treatment
|
a polygon's edges are its perimeter, not a path, so the same treatment
|
||||||
may not even read the same way there.
|
may not even read the same way there.
|
||||||
|
|
||||||
|
=== Finding your location
|
||||||
|
|
||||||
|
`LocateControl` (`public/js/locate.js`) replaces MapLibre's own
|
||||||
|
`GeolocateControl` — same button, reusing its exact CSS classes/icon
|
||||||
|
(`.maplibregl-ctrl-geolocate`) rather than a new one, so it's a visual
|
||||||
|
drop-in — but sourced differently. This app has its own GNSS receiver
|
||||||
|
(gpsd, possibly `bin/gnss_simulator.rb`'s simulated one — see
|
||||||
|
`docs/development.adoc`/`docs/deployment.adoc`), and "find my
|
||||||
|
location" should mean that receiver first, not a device's separate
|
||||||
|
(and often less accurate, e.g. wifi-based) location service, when the
|
||||||
|
receiver actually has a fix. Clicking it checks
|
||||||
|
`GET /api/gnss/position` once: if `available`, it polls that
|
||||||
|
endpoint (same 1.5s cadence as `GnssControl`'s own sidebar polling)
|
||||||
|
and shows the fix with the same `.gnss-marker` styling that control
|
||||||
|
uses; if not, it falls back to the browser's own
|
||||||
|
`navigator.geolocation.watchPosition`, GeolocateControl's own
|
||||||
|
mechanism. Either way, only the *first* fix recenters the map —
|
||||||
|
later updates move the marker without fighting a pan/zoom the visitor
|
||||||
|
made in the meantime, unlike GeolocateControl's own default of
|
||||||
|
recentering on every update. Clicking again stops tracking (clears
|
||||||
|
the poll timer or browser watch, whichever was active) and removes
|
||||||
|
the marker.
|
||||||
|
|
||||||
=== Debug inspector (Alt+click)
|
=== Debug inspector (Alt+click)
|
||||||
|
|
||||||
Alt+click anywhere on the map opens a popup listing every rendered
|
Alt+click anywhere on the map opens a popup listing every rendered
|
||||||
|
|||||||
@@ -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
@@ -1,7 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
Map as MaplibreMap,
|
Map as MaplibreMap,
|
||||||
NavigationControl,
|
NavigationControl,
|
||||||
GeolocateControl,
|
|
||||||
Popup,
|
Popup,
|
||||||
addProtocol,
|
addProtocol,
|
||||||
} from "/vendor/maplibre-gl/maplibre-gl.mjs";
|
} from "/vendor/maplibre-gl/maplibre-gl.mjs";
|
||||||
@@ -11,6 +10,7 @@ import { GnssControl } from "/js/gnss.js";
|
|||||||
import { LiveTrackPanelControl } from "/js/livetrack.js";
|
import { LiveTrackPanelControl } from "/js/livetrack.js";
|
||||||
import { TrackProfilePanel } from "/js/track_profile.js";
|
import { TrackProfilePanel } from "/js/track_profile.js";
|
||||||
import { MeasureControl } from "/js/measure.js";
|
import { MeasureControl } from "/js/measure.js";
|
||||||
|
import { LocateControl } from "/js/locate.js";
|
||||||
import { LAYERS_ICON } from "/js/icons.js";
|
import { LAYERS_ICON } from "/js/icons.js";
|
||||||
import { notifySidePanelOpen, onlyOneSidePanelOpen } from "/js/panels.js";
|
import { notifySidePanelOpen, onlyOneSidePanelOpen } from "/js/panels.js";
|
||||||
import HomeButton from "/vendor/npmap-home-button/home-button.esm.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
|
// 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
|
// 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" —
|
// 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 = [
|
const SLOVAKIA_BOUNDS = [
|
||||||
[16.83, 47.73],
|
[16.83, 47.73],
|
||||||
[22.57, 49.61],
|
[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 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");
|
||||||
map.addControl(
|
// Not MapLibre's own GeolocateControl (browser Geolocation API only) —
|
||||||
new GeolocateControl({
|
// this app has its own GNSS receiver (gpsd, possibly a simulated one, see
|
||||||
positionOptions: { enableHighAccuracy: true },
|
// bin/gnss_simulator.rb) and "find my location" should prefer that over a
|
||||||
trackUserLocation: true,
|
// device's separate, often less accurate location service when it has a
|
||||||
showAccuracyCircle: true,
|
// fix. See locate.js.
|
||||||
}),
|
map.addControl(new LocateControl(), "top-left");
|
||||||
"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
|
||||||
|
|||||||
Reference in new issue
Block a user