// Map style registry + rewriting of OpenFreeMap styles for self-hosted assets. // Styles, fonts and sprites are served locally from /public (populated by // scripts/download-map-assets.mjs); only vector tiles (/planet) and the ne2_shaded // raster tiles stay remote on tiles.openfreemap.org. export interface MapStyle { id: string; label: string; } export const STYLES: MapStyle[] = [ { id: 'liberty', label: 'Liberty' }, { id: 'bright', label: 'Bright' }, { id: 'positron', label: 'Positron' }, { id: 'dark', label: 'Dark' }, { id: 'fiord', label: 'Fiord' }, ]; export const DEFAULT_STYLE_ID = 'fiord'; // localStorage key for the persisted style choice. export const STYLE_STORAGE_KEY = 'flight-ops-style'; const OFM_FONTS_PREFIX = 'https://tiles.openfreemap.org/fonts/'; const OFM_SPRITE_URL = 'https://tiles.openfreemap.org/sprites/ofm_f384/ofm'; // Sprite files are extracted flat into public/sprites/, so the sprite base URL // (MapLibre appends .json / @2x.png) points straight at them. export const LOCAL_SPRITE_URL = '/sprites/ofm'; // OFM styles ship no attribution; ODbL requires visible OSM credit. export const ATTRIBUTION = '© OpenStreetMap contributors © OpenFreeMap'; export function isStyleId(id: string): boolean { return STYLES.some((s) => s.id === id); } export function styleUrl(id: string): string { return `/styles/${id}.json`; } // Remote fallback base for styles when the local asset pack is absent (fresh clone // without `npm run assets:download` / no volume mount). The raw OFM styles already // point glyphs/sprites at remote URLs, so they work as-is — only vector tiles were // ever remote. A future self-hosted tile pack plugs into the same availability // mechanism below (the manifest describes the whole local asset set). export const REMOTE_STYLE_BASE = 'https://tiles.openfreemap.org/styles'; export function styleUrlFor(id: string, localAssetsAvailable: boolean): string { return localAssetsAvailable ? `/styles/${id}.json` : `${REMOTE_STYLE_BASE}/${id}`; } // The manifest is written LAST by scripts/download-map-assets.mjs, so its presence // means the whole pack (styles + fonts + sprites — and any future asset class) is in // place. Probed once per page life via a module-level cached promise: any non-200 or // network error → false (fall back to remote styles). const MANIFEST_URL = '/map-assets-manifest.json'; let assetsProbePromise: Promise | null = null; export function localAssetsAvailable(): Promise { if (!assetsProbePromise) { assetsProbePromise = fetch(MANIFEST_URL, { cache: 'no-store' }) .then((res) => res.ok) .catch(() => false); } return assetsProbePromise; } type Source = Record; // Takes a raw OFM style object and returns a new one with glyphs/sprite pointed at the // self-hosted copies under /fonts and /sprites. Only those two fields are rewritten — // the vector tile url (/planet) and raster tiles (/natural_earth/...) stay remote. // Sources without an attribution get one added (ODbL requires visible OSM credit). export function rewriteStyle(raw: unknown): unknown { if (typeof raw !== 'object' || raw === null) return raw; const style = { ...(raw as Record) }; const glyphs = style.glyphs; if (typeof glyphs === 'string' && glyphs.startsWith(OFM_FONTS_PREFIX)) { style.glyphs = '/fonts/' + glyphs.slice(OFM_FONTS_PREFIX.length); } if (style.sprite === OFM_SPRITE_URL) { style.sprite = LOCAL_SPRITE_URL; } const sources = style.sources; if (sources && typeof sources === 'object') { const next: Record = { ...(sources as Source) }; for (const [id, src] of Object.entries(next)) { if (src && typeof src === 'object' && !('attribution' in (src as Source))) { next[id] = { ...(src as Source), attribution: ATTRIBUTION }; } } style.sources = next; } return style; }