Files
Flight-Ops/lib/mapStyles.ts

100 lines
3.8 KiB
TypeScript

// 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<boolean> | null = null;
export function localAssetsAvailable(): Promise<boolean> {
if (!assetsProbePromise) {
assetsProbePromise = fetch(MANIFEST_URL, { cache: 'no-store' })
.then((res) => res.ok)
.catch(() => false);
}
return assetsProbePromise;
}
type Source = Record<string, unknown>;
// 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<string, unknown>) };
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<string, unknown> = { ...(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;
}