#!/usr/bin/env node // Downloads OpenFreeMap map assets and writes them into public/ so the app is // self-contained at runtime: all 5 styles (rewritten to local glyphs/sprite), // the full Noto Sans font set, and the sprite sheet. Vector tiles (/planet) and // the ne2_shaded raster stay remote — this script never touches tile data. // // Run manually (not part of tests/CI): node scripts/download-map-assets.mjs // Idempotent: overwrites existing files in public/. import { execFileSync } from 'node:child_process'; import { mkdirSync, mkdtempSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { STYLES, rewriteStyle } from '../lib/mapStyles.ts'; const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const publicDir = path.join(root, 'public'); // The OFM CDN returns 403 to non-browser user agents. const UA = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36'; async function download(url, destFile) { const res = await fetch(url, { headers: { 'user-agent': UA } }); if (!res.ok) throw new Error(`GET ${url} → HTTP ${res.status}`); writeFileSync(destFile, Buffer.from(await res.arrayBuffer())); return statSync(destFile).size; } function dirSize(dir) { let total = 0; for (const entry of readdirSync(dir, { withFileTypes: true })) { const p = path.join(dir, entry.name); total += entry.isDirectory() ? dirSize(p) : statSync(p).size; } return total; } function extractTarball(tarball, destDir, stripComponents) { mkdirSync(destDir, { recursive: true }); execFileSync('tar', ['-xzf', tarball, `--strip-components=${stripComponents}`, '-C', destDir], { stdio: 'inherit' }); } const tmp = mkdtempSync(path.join(os.tmpdir(), 'ofm-assets-')); try { // 1. Styles: fetch each raw OFM style JSON, rewrite glyphs/sprite to local paths, // add OSM attribution to sources, write to public/styles/{id}.json. const stylesDir = path.join(publicDir, 'styles'); mkdirSync(stylesDir, { recursive: true }); for (const { id } of STYLES) { const url = `https://tiles.openfreemap.org/styles/${id}`; const res = await fetch(url, { headers: { 'user-agent': UA } }); if (!res.ok) throw new Error(`GET ${url} → HTTP ${res.status}`); const raw = JSON.parse(await res.text()); writeFileSync(path.join(stylesDir, `${id}.json`), JSON.stringify(rewriteStyle(raw))); console.log(`style ${id}: written (${JSON.stringify(rewriteStyle(raw)).length} bytes)`); } // 2. Fonts: tarball layout is ofm/{Noto Sans X}/{range}.pbf → strip the leading // "ofm/" so fontstack paths in styles (/fonts/Noto Sans X/...) resolve directly. const fontsTar = path.join(tmp, 'ofm.tar.gz'); console.log(`downloading fonts tarball…`); await download('https://assets.openfreemap.com/fonts/ofm.tar.gz', fontsTar); extractTarball(fontsTar, path.join(publicDir, 'fonts'), 1); // 3. Sprites: tarball layout is ofm_f384/ofm{.json,.png,@2x.*} → strip the leading // "ofm_f384/" so files sit flat in public/sprites/ (sprite url is /sprites/ofm). const spritesTar = path.join(tmp, 'ofm_f384.tar.gz'); console.log(`downloading sprite tarball…`); await download('https://assets.openfreemap.com/sprites/ofm_f384.tar.gz', spritesTar); extractTarball(spritesTar, path.join(publicDir, 'sprites'), 1); // Sanity checks: fail loudly if the tarball structure changed. const fonts = readdirSync(path.join(publicDir, 'fonts')); for (const dir of ['Noto Sans Regular', 'Noto Sans Italic', 'Noto Sans Bold']) { if (!fonts.includes(dir)) throw new Error(`unexpected font layout: missing "${dir}" in public/fonts`); } const sprites = readdirSync(path.join(publicDir, 'sprites')); for (const f of ['ofm.json', 'ofm.png', 'ofm@2x.png']) { if (!sprites.includes(f)) throw new Error(`unexpected sprite layout: missing "${f}" in public/sprites`); } const fmt = (n) => `${(n / 1048576).toFixed(1)} MB`; console.log(`done. public/styles ${fmt(dirSize(stylesDir))}, public/fonts ${fmt(dirSize(path.join(publicDir, 'fonts')))}, public/sprites ${fmt(dirSize(path.join(publicDir, 'sprites')))}`); } finally { rmSync(tmp, { recursive: true, force: true }); }