Restructure the map controls from MapLibre corner buttons into a left sidebar of icons with flyouts/panels (radar, GPX, GNSS), matching OsmAnd's layout more closely: - Base map and GPX selection are now full-height side panels docked next to the sidebar (not small dropdown flyouts or a right-docked panel), opening/closing exclusively via a small shared event bus (panels.js) so only one is ever open at a time. - The GPX panel's upload/new-folder controls are now icon buttons in the header instead of an always-visible dropzone and form; dragging a GPX file onto the tree, a specific folder, or straight onto the map itself all upload it. - New: Maptool::GpsdClient (lib/maptool/gpsd_client.rb) streams live GNSS fixes from a local gpsd daemon over Server-Sent Events (/api/gnss/stream) to a marker on the map (public/js/gnss.js) — one background gpsd connection shared by every browser tab. - The online map style (public/js/osmand-style.js) is now styled to resemble OsmAnd's own rendering, applied to both the online and offline (PMTiles) styles. Also adds a GeolocateControl (browser geolocation) next to the zoom controls.
102 lines
3.0 KiB
JavaScript
102 lines
3.0 KiB
JavaScript
import { Marker } from "/vendor/maplibre-gl/maplibre-gl.mjs";
|
|
import { GNSS_ICON } from "/js/icons.js";
|
|
|
|
const POSITION_URL = "/api/gnss/position";
|
|
const POLL_INTERVAL_MS = 1500;
|
|
|
|
// A sidebar icon that polls /api/gnss/position (backed server-side by a
|
|
// gpsd connection, see lib/maptool/gpsd_client.rb) and shows the fix as a
|
|
// marker, with fix status in a flyout. Polling rather than a push channel
|
|
// is deliberate — see the comment in app.rb next to GNSS_LATEST — and fits
|
|
// gpsd's own ~1Hz report rate fine.
|
|
export class GnssControl {
|
|
constructor(map) {
|
|
this._map = map;
|
|
this._marker = null;
|
|
this._timer = null;
|
|
this._enabled = false;
|
|
}
|
|
|
|
mount(sidebar) {
|
|
this._container = document.createElement("div");
|
|
this._container.className = "sidebar-item";
|
|
this._container.innerHTML = `
|
|
<button type="button" class="sidebar-icon" title="GNSS" aria-label="GNSS">${GNSS_ICON}</button>
|
|
<div class="sidebar-flyout" hidden>
|
|
<span class="gnss-control__status"></span>
|
|
</div>
|
|
`;
|
|
this._icon = this._container.querySelector(".sidebar-icon");
|
|
this._flyout = this._container.querySelector(".sidebar-flyout");
|
|
this._status = this._container.querySelector(".gnss-control__status");
|
|
this._icon.addEventListener("click", () => this._setEnabled(!this._enabled));
|
|
|
|
sidebar.appendChild(this._container);
|
|
}
|
|
|
|
_setEnabled(enabled) {
|
|
this._enabled = enabled;
|
|
this._icon.classList.toggle("active", enabled);
|
|
this._flyout.hidden = !enabled;
|
|
if (enabled) {
|
|
this._poll();
|
|
this._timer = setInterval(() => this._poll(), POLL_INTERVAL_MS);
|
|
} else {
|
|
this._stopPolling();
|
|
this._removeMarker();
|
|
this._status.textContent = "";
|
|
}
|
|
}
|
|
|
|
_stopPolling() {
|
|
if (this._timer) {
|
|
clearInterval(this._timer);
|
|
this._timer = null;
|
|
}
|
|
}
|
|
|
|
async _poll() {
|
|
let data;
|
|
try {
|
|
const response = await fetch(POSITION_URL);
|
|
data = await response.json();
|
|
} catch (err) {
|
|
this._status.textContent = "unreachable";
|
|
return;
|
|
}
|
|
|
|
if (!data.available) {
|
|
this._removeMarker();
|
|
this._status.textContent = "no fix";
|
|
return;
|
|
}
|
|
|
|
this._status.textContent = fixLabel(data);
|
|
|
|
const lngLat = [data.lon, data.lat];
|
|
if (!this._marker) {
|
|
const el = document.createElement("div");
|
|
el.className = "gnss-marker";
|
|
// MapLibre markers are plain positioned DOM elements, not tied to the
|
|
// style's sources/layers, so — unlike the radar/GPX layers — this
|
|
// survives map.setStyle() (switching Online/Offline) with no extra
|
|
// re-add handling needed.
|
|
this._marker = new Marker({ element: el }).setLngLat(lngLat).addTo(this._map);
|
|
} else {
|
|
this._marker.setLngLat(lngLat);
|
|
}
|
|
}
|
|
|
|
_removeMarker() {
|
|
if (this._marker) {
|
|
this._marker.remove();
|
|
this._marker = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
function fixLabel({ mode, eph }) {
|
|
const kind = mode === 3 ? "3D" : mode === 2 ? "2D" : "no fix";
|
|
return eph ? `${kind} (±${Math.round(eph)}m)` : kind;
|
|
}
|