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 { TrackProfilePanel } from "/js/track_profile.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 = ``; 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"); const profilePanel = new TrackProfilePanel(map); profilePanel.mount(); new GpxPanelControl(map, profilePanel).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. });