Redesign UI as an OsmAnd-style sidebar, add live GNSS position via gpsd
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.
This commit is contained in:
1 parent
008e034932
commit
87df90f75c
14 files changed
+939
-200
No files matched your search
+27
-31
@@ -1,3 +1,5 @@
|
||||
import { RADAR_ICON } from "/js/icons.js";
|
||||
|
||||
const RAINVIEWER_API = "https://api.rainviewer.com/public/weather-maps.json";
|
||||
|
||||
// Matches the default window used by github.com/jpettitt/weather-radar-card
|
||||
@@ -9,10 +11,10 @@ const OPACITY = 0.7;
|
||||
const ATTRIBUTION =
|
||||
'Weather data © <a href="https://www.rainviewer.com" target="_blank">RainViewer</a>';
|
||||
|
||||
// A MapLibre IControl: a "Radar" checkbox that fetches RainViewer's recent
|
||||
// precipitation frames on demand and animates them as a raster overlay, with
|
||||
// play/pause and a scrub slider. No API key needed — see
|
||||
// docs/architecture.adoc for RainViewer's usage terms.
|
||||
// A sidebar icon that fetches RainViewer's recent precipitation frames on
|
||||
// demand and animates them as a raster overlay, with play/pause and a scrub
|
||||
// slider in a flyout. No API key needed — see docs/architecture.adoc for
|
||||
// RainViewer's usage terms.
|
||||
export class RadarControl {
|
||||
constructor(map) {
|
||||
this._map = map;
|
||||
@@ -29,45 +31,39 @@ export class RadarControl {
|
||||
});
|
||||
}
|
||||
|
||||
onAdd() {
|
||||
mount(sidebar) {
|
||||
this._container = document.createElement("div");
|
||||
this._container.className = "maplibregl-ctrl maplibregl-ctrl-group radar-control";
|
||||
this._container.className = "sidebar-item";
|
||||
this._container.innerHTML = `
|
||||
<label class="radar-control__toggle">
|
||||
<input type="checkbox" class="radar-control__checkbox">
|
||||
Radar
|
||||
</label>
|
||||
<div class="radar-control__playback" hidden>
|
||||
<button type="button" class="radar-control__play" aria-label="Play">▶</button>
|
||||
<input type="range" class="radar-control__scrub" min="0" max="0" step="1" value="0">
|
||||
<span class="radar-control__time"></span>
|
||||
<button type="button" class="sidebar-icon" title="Radar" aria-label="Radar">${RADAR_ICON}</button>
|
||||
<div class="sidebar-flyout" hidden>
|
||||
<div class="radar-control__playback">
|
||||
<button type="button" class="radar-control__play" aria-label="Play">▶</button>
|
||||
<input type="range" class="radar-control__scrub" min="0" max="0" step="1" value="0">
|
||||
<span class="radar-control__time"></span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
this._checkbox = this._container.querySelector(".radar-control__checkbox");
|
||||
this._playback = this._container.querySelector(".radar-control__playback");
|
||||
this._icon = this._container.querySelector(".sidebar-icon");
|
||||
this._flyout = this._container.querySelector(".sidebar-flyout");
|
||||
this._playButton = this._container.querySelector(".radar-control__play");
|
||||
this._scrub = this._container.querySelector(".radar-control__scrub");
|
||||
this._time = this._container.querySelector(".radar-control__time");
|
||||
|
||||
this._checkbox.addEventListener("change", () => this._setEnabled(this._checkbox.checked));
|
||||
this._icon.addEventListener("click", () => this._setEnabled(!this._enabled));
|
||||
this._playButton.addEventListener("click", () => this._togglePlayback());
|
||||
this._scrub.addEventListener("input", () => {
|
||||
this._stopAnimation();
|
||||
this._showFrame(Number(this._scrub.value));
|
||||
});
|
||||
|
||||
return this._container;
|
||||
}
|
||||
|
||||
onRemove() {
|
||||
this._stopAnimation();
|
||||
this._removeLayers();
|
||||
this._container.remove();
|
||||
sidebar.appendChild(this._container);
|
||||
}
|
||||
|
||||
async _setEnabled(enabled) {
|
||||
this._enabled = enabled;
|
||||
this._icon.classList.toggle("active", enabled);
|
||||
if (!enabled) {
|
||||
this._stopAnimation();
|
||||
this._removeLayers();
|
||||
@@ -76,20 +72,20 @@ export class RadarControl {
|
||||
return;
|
||||
}
|
||||
|
||||
this._checkbox.disabled = true;
|
||||
this._icon.disabled = true;
|
||||
let frames;
|
||||
try {
|
||||
frames = await fetchFrames();
|
||||
} catch (err) {
|
||||
this._enabled = false;
|
||||
this._checkbox.checked = false;
|
||||
this._checkbox.disabled = false;
|
||||
this._icon.classList.remove("active");
|
||||
this._icon.disabled = false;
|
||||
return;
|
||||
}
|
||||
this._checkbox.disabled = false;
|
||||
this._icon.disabled = false;
|
||||
|
||||
// The checkbox may have been switched off again while the fetch was in
|
||||
// flight.
|
||||
// The icon may have been clicked again (disabling) while the fetch was
|
||||
// in flight.
|
||||
if (!this._enabled || !frames.length) return;
|
||||
|
||||
this._frames = frames;
|
||||
@@ -168,7 +164,7 @@ export class RadarControl {
|
||||
}
|
||||
|
||||
_updateFrameUI() {
|
||||
this._playback.hidden = this._frames.length === 0;
|
||||
this._flyout.hidden = this._frames.length === 0;
|
||||
this._scrub.max = String(Math.max(0, this._frames.length - 1));
|
||||
this._scrub.value = String(this._frameIndex);
|
||||
const frame = this._frames[this._frameIndex];
|
||||
|
||||
Reference in new issue
Block a user