Clicking a LiveTrack session now fetches and draws its track (line + latest position). Garmin's own API 403s any plain server-side request (Cloudflare bot protection), so LiveTrackClient fetches the session page instead and pulls the track out of the server-rendered data embedded in it. The webhook now captures the session's token too, since the id alone isn't enough to fetch anything.
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(map).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.
|
|
});
|