// Mock server: static UI + local tile backend (proxy -> OSM, disk-cached) // + routing backend (proxy -> local OSRM on :5000, NE-US extract). // The UI only ever talks to localhost; swap the upstream for a real // tileserver-gl / OSRM later without touching the frontend. const http = require('http'); const fs = require('fs'); const path = require('path'); const { Readable } = require('stream'); const PORT = 8077; const ROOT = __dirname; const OSRM = process.env.OSRM_URL || 'http://localhost:5000'; const PROFILE = { foot: 'walking', walk: 'walking', bike: 'cycling', cycle: 'cycling', car: 'driving', drive: 'driving', transit: 'driving' }; const CACHE = path.join(__dirname, '.tilecache'); fs.mkdirSync(CACHE, { recursive: true }); const mime = { '.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css', '.json': 'application/json', '.svg': 'image/svg+xml', '.png': 'image/png', }; http.createServer((req, res) => { const u = new URL(req.url, 'http://x'); // ---- routing backend (proxy -> local OSRM) ------------------------ // /route?mode=foot&points=lng,lat;lng,lat[;...] // /table?mode=foot&points=lng,lat;lng,lat[;...] if (u.pathname === '/route' || u.pathname === '/table') { const kind = u.pathname.slice(1); const mode = u.searchParams.get('mode') || 'foot'; const points = u.searchParams.get('points') || ''; const profile = PROFILE[mode] || 'driving'; let osrmPath = `/${kind}/v1/${profile}/${encodeURIComponent(points)}`; if (kind === 'route') osrmPath += '?overview=full&alternatives=false&steps=false'; else { // table: durations from the first point (the anchor) to every point const n = points.split(';').filter(Boolean).length; const targets = Array.from({ length: n }, (_, i) => i).join(','); osrmPath += `?annotations=duration,distance&sources=0&targets=${targets}`; } const fetchUp = fetch(OSRM + osrmPath).then(r => r.text()).catch(() => null); fetchUp.then(body => { res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Content-Type', 'application/json'); if (body == null) { res.writeHead(502); return res.end(JSON.stringify({ code: 'ProxyError' })); } res.writeHead(200); res.end(body); }); return; } // routing availability probe (so the UI can label router vs estimate) if (u.pathname === '/router-status') { const probe = '-71.06,42.35;-71.07,42.36'; // must span >0 distance for the health check fetch(OSRM + '/route/v1/walking/' + probe + '?overview=false').then(r => r.text()) .then(t => { let ok = false; try { ok = JSON.parse(t).code === 'Ok' && JSON.parse(t).routes?.[0]?.distance > 0; } catch (e) {} res.setHeader('Content-Type', 'application/json'); res.writeHead(200); res.end(JSON.stringify({ router: ok, osrm: OSRM })); }) .catch(() => { res.setHeader('Content-Type', 'application/json'); res.writeHead(200); res.end(JSON.stringify({ router: false, osrm: OSRM })); }); return; } // ---- tile backend ------------------------------------------------- if (u.pathname.startsWith('/tiles/')) { const parts = u.pathname.split('/'); const [z, x, y] = [parts[2], parts[3], (parts[4] || '').replace(/\.png$/, '')]; if (!/^\d+$/.test(z) || !/^\d+$/.test(x) || !/^\d+$/.test(y)) { res.writeHead(400); return res.end(); } const file = path.join(CACHE, `${z}/${x}`, `${y}.png`); const upstream = `https://tile.openstreetmap.org/${z}/${x}/${y}.png`; res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Content-Type', 'image/png'); res.setHeader('Cache-Control', 'public, max-age=86400'); if (fs.existsSync(file)) { res.writeHead(200); fs.createReadStream(file).pipe(res); return; } fs.mkdirSync(path.dirname(file), { recursive: true }); const fail = (code) => { if (!res.headersSent) { res.writeHead(code); res.end(); } else res.destroy(); }; fetch(upstream, { headers: { 'User-Agent': 'mapmock-dev/0.1 (local tile proxy)', 'Referer': `http://localhost:${PORT}/` } }) .then(r => { if (!r.ok) { return fail(r.status); } const body = Readable.fromWeb(r.body); body.on('error', () => fail(502)); res.writeHead(200); const ws = fs.createWriteStream(file, { flags: 'x' }); ws.on('error', () => {}); body.pipe(ws); body.pipe(res); }) .catch(() => fail(502)); return; } // ---- static ------------------------------------------------------- let p = u.pathname === '/' ? '/index.html' : u.pathname; const file = path.join(ROOT, p); if (!file.startsWith(ROOT)) { res.writeHead(403); return res.end(); } fs.readFile(file, (e, d) => { if (e) { res.writeHead(404); return res.end('not found'); } res.writeHead(200, { 'Content-Type': mime[path.extname(file)] || 'application/octet-stream' }); res.end(d); }); }).listen(PORT, '0.0.0.0', () => console.log(`map mock: http://localhost:${PORT} (tiles: /tiles/{z}/{x}/{y}.png)`));