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 { 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 protocol = new pmtiles.Protocol(); 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 = offlineTheme(protomaps_themes_base.namedTheme("light")); const pmtilesUrl = `${window.location.origin}/tiles/offline.pmtiles`; return { version: 8, sources: { offline: { type: "vector", url: `pmtiles://${pmtilesUrl}`, attribution: "© OpenStreetMap contributors", }, }, layers: protomaps_themes_base.layers("offline", theme), }; } // 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: await onlineStyle(), 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); async function applyStyle(name) { layerToggle.setActive(name); map.setStyle(name === "offline" ? offlineStyle() : await onlineStyle()); } // 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. });