- DESIGN.md: full design incl. driving-trip requirements (R1-R4), stays model, focus mode, mobile, provenance rules - SURVEY.md: open-source landscape - mock/: interaction mock (Florence itinerary, focus mode, stays, region stops, mobile layout) - router/: Go module (stdlib-only) with Router interface (Valhalla + OSRM backends), stop_cost, optimize_stops, corridor, routectl CLI, bench (5 real NE-corridor tasks, 26 checks passing), integration tests, and setup-osrm.sh for the self-hosted router - osm/: NH+MA+CT+NY PBFs (gitignored) + setup artifacts
59 lines
2.6 KiB
JavaScript
59 lines
2.6 KiB
JavaScript
// Mock server: static UI + local tile backend (proxy -> OSM, disk-cached).
|
|
// The UI only ever talks to localhost; swap the upstream for a real
|
|
// tileserver-gl 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 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');
|
|
|
|
// ---- 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)`));
|