A Gmail-watching Apps Script (documented in docs/deployment.adoc) posts the session ID from each LiveTrack email to a new webhook, which stores it in data/livetrack.json; a new sidebar panel lists what's been received.
148 lines
4.7 KiB
JavaScript
148 lines
4.7 KiB
JavaScript
import {
|
|
Map as MaplibreMap,
|
|
NavigationControl,
|
|
GeolocateControl,
|
|
addProtocol,
|
|
} 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 { LiveTrackPanelControl } from "/js/livetrack.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);
|
|
|
|
// A few color roles nudged from protomaps-themes-base's stock "light" theme
|
|
// towards the online OpenFreeMap "liberty" style's actual palette (sampled
|
|
// from its style JSON — see docs/architecture.adoc), so switching between
|
|
// Online/Offline doesn't look like two different apps. Not a full recolor:
|
|
// most roles (labels, casings, minor terrain fills, ...) are left at their
|
|
// theme defaults.
|
|
const OFFLINE_THEME = {
|
|
...protomaps_themes_base.namedTheme("light"),
|
|
background: "#f8f4f0",
|
|
earth: "#f8f4f0",
|
|
water: "#9ebdff",
|
|
wood_a: "#ace38c",
|
|
wood_b: "#ace38c",
|
|
buildings: "#dcd9d6",
|
|
highway: "#f2934a",
|
|
major: "#ffeeaa",
|
|
link: "#ffeeaa",
|
|
};
|
|
|
|
function offlineStyle() {
|
|
const pmtilesUrl = `${window.location.origin}/tiles/offline.pmtiles`;
|
|
|
|
return {
|
|
version: 8,
|
|
glyphs: `${window.location.origin}/vendor/fonts/{fontstack}/{range}.pbf`,
|
|
sources: {
|
|
offline: {
|
|
type: "vector",
|
|
url: `pmtiles://${pmtilesUrl}`,
|
|
attribution: "© OpenStreetMap contributors",
|
|
},
|
|
},
|
|
// lang: "en" pulls in labels_layers (see docs/architecture.adoc) — only
|
|
// the Latin-script glyph ranges are vendored, so labels using other
|
|
// scripts won't render offline.
|
|
layers: protomaps_themes_base.layers("offline", OFFLINE_THEME, { lang: "en" }),
|
|
};
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
mount(sidebar) {
|
|
this._container = document.createElement("div");
|
|
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);
|
|
});
|
|
});
|
|
|
|
onlyOneSidePanelOpen(this._panel, () => this._icon.classList.remove("active"));
|
|
|
|
sidebar.appendChild(this._container);
|
|
}
|
|
|
|
_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._panel.querySelectorAll(".basemap-panel__option").forEach((button) => {
|
|
button.classList.toggle("active", button.dataset.style === name);
|
|
});
|
|
}
|
|
}
|
|
|
|
const map = new MaplibreMap({
|
|
container: "map",
|
|
style: ONLINE_STYLE,
|
|
center: CENTER,
|
|
zoom: ZOOM,
|
|
});
|
|
|
|
map.addControl(new NavigationControl(), "bottom-right");
|
|
map.addControl(
|
|
new GeolocateControl({
|
|
positionOptions: { enableHighAccuracy: true },
|
|
trackUserLocation: true,
|
|
showAccuracyCircle: true,
|
|
}),
|
|
"bottom-right",
|
|
);
|
|
|
|
const sidebar = document.getElementById("sidebar");
|
|
|
|
const layerToggle = new LayerToggleControl(applyStyle);
|
|
layerToggle.mount(sidebar);
|
|
layerToggle.setActive("online");
|
|
|
|
new GpxPanelControl(map).mount(sidebar);
|
|
new RadarControl(map).mount(sidebar);
|
|
new GnssControl(map).mount(sidebar);
|
|
new LiveTrackPanelControl().mount(sidebar);
|
|
|
|
function applyStyle(name) {
|
|
layerToggle.setActive(name);
|
|
map.setStyle(name === "offline" ? offlineStyle() : ONLINE_STYLE);
|
|
}
|
|
|
|
// If a local PMTiles file is present, prefer it as the default style — its
|
|
// presence signals the app is meant to be used offline here.
|
|
fetch("/api/offline-tiles")
|
|
.then((response) => response.json())
|
|
.then(({ available }) => {
|
|
if (available) applyStyle("offline");
|
|
})
|
|
.catch(() => {
|
|
// Backend check failed; keep the online style.
|
|
});
|