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
+45
-23
@@ -6,10 +6,13 @@ import {
|
||||
} from "/vendor/maplibre-gl/maplibre-gl.mjs";
|
||||
import { RadarControl } from "/js/radar.js";
|
||||
import { GpxPanelControl } from "/js/gpx.js";
|
||||
import { GnssControl } from "/js/gnss.js";
|
||||
import { onlineStyle, offlineTheme } from "/js/osmand-style.js";
|
||||
import { LAYERS_ICON } from "/js/icons.js";
|
||||
import { notifySidePanelOpen, onlyOneSidePanelOpen } from "/js/panels.js";
|
||||
|
||||
const CENTER = [17.1077, 48.1486];
|
||||
const ZOOM = 13;
|
||||
const ONLINE_STYLE = "https://tiles.openfreemap.org/styles/liberty";
|
||||
|
||||
const protocol = new pmtiles.Protocol();
|
||||
addProtocol("pmtiles", protocol.tile);
|
||||
@@ -17,7 +20,7 @@ addProtocol("pmtiles", protocol.tile);
|
||||
// No label layers here (see docs/architecture.adoc): rendering text needs a
|
||||
// glyphs server, which the offline style deliberately does not depend on.
|
||||
function offlineStyle() {
|
||||
const theme = protomaps_themes_base.namedTheme("light");
|
||||
const theme = offlineTheme(protomaps_themes_base.namedTheme("light"));
|
||||
const pmtilesUrl = `${window.location.origin}/tiles/offline.pmtiles`;
|
||||
|
||||
return {
|
||||
@@ -33,31 +36,46 @@ function offlineStyle() {
|
||||
};
|
||||
}
|
||||
|
||||
// A sidebar icon that opens a full-height side panel (like the GPX one)
|
||||
// with the Online/Offline choice — modelled after OsmAnd's "Configure map"
|
||||
// panel, rather than a small dropdown flyout.
|
||||
class LayerToggleControl {
|
||||
constructor(onSelect) {
|
||||
this._onSelect = onSelect;
|
||||
}
|
||||
|
||||
onAdd() {
|
||||
mount(sidebar) {
|
||||
this._container = document.createElement("div");
|
||||
this._container.className = "maplibregl-ctrl maplibregl-ctrl-group layer-toggle";
|
||||
this._container.innerHTML = `
|
||||
<button type="button" data-style="online">Online</button>
|
||||
<button type="button" data-style="offline">Offline</button>
|
||||
`;
|
||||
this._container.addEventListener("click", (event) => {
|
||||
const button = event.target.closest("button[data-style]");
|
||||
if (button) this._onSelect(button.dataset.style);
|
||||
this._container.className = "sidebar-item";
|
||||
this._container.innerHTML = `<button type="button" class="sidebar-icon" title="Base map" aria-label="Base map">${LAYERS_ICON}</button>`;
|
||||
this._icon = this._container.querySelector(".sidebar-icon");
|
||||
this._icon.addEventListener("click", () => this._togglePanel());
|
||||
|
||||
this._panel = document.getElementById("basemap-panel");
|
||||
this._panel
|
||||
.querySelector(".basemap-panel__close")
|
||||
.addEventListener("click", () => this._togglePanel(false));
|
||||
this._panel.querySelectorAll(".basemap-panel__option").forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
this._onSelect(button.dataset.style);
|
||||
this._togglePanel(false);
|
||||
});
|
||||
});
|
||||
return this._container;
|
||||
|
||||
onlyOneSidePanelOpen(this._panel, () => this._icon.classList.remove("active"));
|
||||
|
||||
sidebar.appendChild(this._container);
|
||||
}
|
||||
|
||||
onRemove() {
|
||||
this._container.remove();
|
||||
_togglePanel(force) {
|
||||
const open = force === undefined ? this._panel.hidden : force;
|
||||
this._panel.hidden = !open;
|
||||
this._icon.classList.toggle("active", open);
|
||||
if (open) notifySidePanelOpen(this._panel);
|
||||
}
|
||||
|
||||
setActive(name) {
|
||||
this._container.querySelectorAll("button").forEach((button) => {
|
||||
this._panel.querySelectorAll(".basemap-panel__option").forEach((button) => {
|
||||
button.classList.toggle("active", button.dataset.style === name);
|
||||
});
|
||||
}
|
||||
@@ -65,30 +83,34 @@ class LayerToggleControl {
|
||||
|
||||
const map = new MaplibreMap({
|
||||
container: "map",
|
||||
style: ONLINE_STYLE,
|
||||
style: await onlineStyle(),
|
||||
center: CENTER,
|
||||
zoom: ZOOM,
|
||||
});
|
||||
|
||||
map.addControl(new NavigationControl(), "top-right");
|
||||
map.addControl(new NavigationControl(), "bottom-right");
|
||||
map.addControl(
|
||||
new GeolocateControl({
|
||||
positionOptions: { enableHighAccuracy: true },
|
||||
trackUserLocation: true,
|
||||
showAccuracyCircle: true,
|
||||
}),
|
||||
"top-right",
|
||||
"bottom-right",
|
||||
);
|
||||
map.addControl(new RadarControl(map), "top-right");
|
||||
|
||||
const sidebar = document.getElementById("sidebar");
|
||||
|
||||
const layerToggle = new LayerToggleControl(applyStyle);
|
||||
map.addControl(layerToggle, "top-left");
|
||||
layerToggle.mount(sidebar);
|
||||
layerToggle.setActive("online");
|
||||
map.addControl(new GpxPanelControl(map), "top-left");
|
||||
|
||||
function applyStyle(name) {
|
||||
new GpxPanelControl(map).mount(sidebar);
|
||||
new RadarControl(map).mount(sidebar);
|
||||
new GnssControl(map).mount(sidebar);
|
||||
|
||||
async function applyStyle(name) {
|
||||
layerToggle.setActive(name);
|
||||
map.setStyle(name === "offline" ? offlineStyle() : ONLINE_STYLE);
|
||||
map.setStyle(name === "offline" ? offlineStyle() : await onlineStyle());
|
||||
}
|
||||
|
||||
// If a local PMTiles file is present, prefer it as the default style — its
|
||||
|
||||
Reference in new issue
Block a user