feat: v0.4.0 — self-host OFM styles/fonts/sprites (tiles stay remote) + top-bar style switcher defaulting to Fiord
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
#!/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 });
|
||||
}
|
||||
Reference in New Issue
Block a user