import { Map as MaplibreMap, NavigationControl, GeolocateControl, Popup, 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 { MeasureControl } from "/js/measure.js"; import { LAYERS_ICON } from "/js/icons.js"; import { notifySidePanelOpen, onlyOneSidePanelOpen } from "/js/panels.js"; import HomeButton from "/vendor/npmap-home-button/home-button.esm.js"; import OverviewMap from "/vendor/npmap-overview/overview.esm.js"; import * as maplibregl from "/vendor/maplibre-gl/maplibre-gl.mjs"; 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); // Nudged from protomaps-themes-base's stock "light" theme towards the // National Park Service's own "Park Tiles" cartography — a warm, // Harpers-Ferry-Center-derived look. Not their actual map: a handful of // values below (marked) are the genuine paint colors pulled from a public // Park Tiles style JSON // (nps.gov/lib/npmap.js/style/ck72fwp2642dv07o7tbqinvz4.json, the "Imagery" // variant — the only one of their four styles served from a plain, // unauthenticated URL; "Standard"/"Slate"/"Light" resolve through Mapbox // Studio's private API with an access token, which isn't something to // piggyback on even though it's technically visible in their own JS bundle). // The rest are reasoned to match that same family (warm cream ground, olive // greens, muted blue) rather than sourced. Not a full recolor: labels, // casings, and roles with no clean real-color anchor are left at theme // defaults. // // Recreating the color choices is legitimate — that's not the kind of thing // a data license restricts (see docs/architecture.adoc's "Offline maps" // section) — what stays off-limits is their actual tile *service*: its // OSM/terrain layers are licensed through Mapbox Atlas, a commercial, // non-sublicensable data service scoped to NPS. const OFFLINE_THEME = { ...protomaps_themes_base.namedTheme("light"), background: "#f9f6ef", // real: Park Tiles' own background, hsl(40, 37%, 97%) earth: "#f9f6ef", water: "#a3cedf", // Brown/khaki, not green: in Park Tiles' own Imagery style, green is // specifically the protected-area/park color (below), not a general land // cover fill — actual forest there is just the aerial photo underneath, // which this offline style has no equivalent of. Using that same vivid // green for ordinary wood/scrub as well (an earlier version of this // theme did) made everything read as "green," not "brown with green // parks" — reversed here to match the real distinction. wood_a: "#c9bd9c", wood_b: "#bcae8a", scrub_a: "#d9cfb0", scrub_b: "#cfc29d", park_a: "#c8e2a0", park_b: "#b3e378", // real: Park Tiles' protected-area/park-point green sand: "#f0e4c4", beach: "#f0e4c4", buildings: "#ded7cc", pedestrian: "#e9e2d8", highway: "#fdefbe", // real: Park Tiles' primary/highway road color major: "#fef7dd", // real: Park Tiles' secondary road color minor: "#fffdf8", link: "#fef7dd", boundaries: "#546619", // real: Park Tiles' park-boundary line color }; 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); } // Public wrapper so a second entry point (MapLayersButton, a map corner // control) can open/close the same panel without reaching into a // "private" method by convention only. toggle() { this._togglePanel(); } setActive(name) { this._panel.querySelectorAll(".basemap-panel__option").forEach((button) => { button.classList.toggle("active", button.dataset.style === name); }); } // Called once it's known there's no offline map to switch to (see the // /api/offline-tiles check below) — an Online/Offline switch doesn't make // sense to show at all when Offline was never going to work. remove() { this._container.remove(); this._panel.hidden = true; } } // A small map-corner button (grouped with Home/zoom, see below) that's a // second entry point to the same panel LayerToggleControl already manages // in the page's own sidebar — matching the National Park Service's own // map control layout, which keeps zoom/home/base-map together on the map // rather than split between the map and a separate side rail. class MapLayersButton { constructor(layerToggle) { this._layerToggle = layerToggle; } onAdd() { this._container = document.createElement("div"); this._container.className = "maplibregl-ctrl maplibregl-ctrl-group"; const button = document.createElement("button"); button.type = "button"; button.title = "Base map"; button.setAttribute("aria-label", "Base map"); button.innerHTML = LAYERS_ICON; button.addEventListener("click", () => this._layerToggle.toggle()); this._container.appendChild(button); return this._container; } onRemove() { this._container.remove(); } getDefaultPosition() { return "top-left"; } } // A passive zoom-level readout, sitting in the same stack as the buttons // around it — the National Park Service's own control layout has one (the // small numbered square next to their zoom buttons). Not a button: no // click behavior, just a live display, so it's a plain div rather than a //