Route ground legs along real roads via an OSRM directions proxy
- GET /api/directions proxies OSRM (OSRM_URL env, default public demo server) with validation, Leaflet-order geometry, 24h capped cache - Map draws straight lines first, then upgrades ground legs to road-following polylines with routed km (marked road) as directions arrive; silent fallback to great-circle on failure; air legs unchanged - README documents OSRM_URL
This commit is contained in:
@@ -9,6 +9,7 @@ import tripsRoutes from './routes/trips.js';
|
||||
import entriesRoutes from './routes/entries.js';
|
||||
import geocodeRoutes from './routes/geocode.js';
|
||||
import airportsRoutes from './routes/airports.js';
|
||||
import directionsRoutes from './routes/directions.js';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const PUBLIC_DIR = path.join(__dirname, '..', '..', 'public');
|
||||
@@ -51,6 +52,7 @@ export function createApp(options = {}) {
|
||||
app.use('/api', requireAuth, entriesRoutes(db)); // /trips/:id/entries + /entries/:id
|
||||
app.use('/api/geocode', requireAuth, geocodeRoutes(db));
|
||||
app.use('/api/airports', requireAuth, airportsRoutes());
|
||||
app.use('/api/directions', requireAuth, directionsRoutes());
|
||||
|
||||
// JSON 404 for any unmatched /api route.
|
||||
app.use('/api', (req, res) => {
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import express from 'express';
|
||||
|
||||
const USER_AGENT = 'trip-plan-app/0.1 (self-hosted)';
|
||||
const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
||||
const MAX_CACHE_ENTRIES = 500;
|
||||
|
||||
// Parses a "lat,lng" query param into {lat, lng}, or null if malformed/out of range.
|
||||
function parseLatLng(value) {
|
||||
if (typeof value !== 'string') return null;
|
||||
const parts = value.split(',');
|
||||
if (parts.length !== 2) return null;
|
||||
const lat = Number(parts[0]);
|
||||
const lng = Number(parts[1]);
|
||||
if (!Number.isFinite(lat) || !Number.isFinite(lng)) return null;
|
||||
if (lat < -90 || lat > 90 || lng < -180 || lng > 180) return null;
|
||||
return { lat, lng };
|
||||
}
|
||||
|
||||
function round1(n) {
|
||||
return Math.round(n * 10) / 10;
|
||||
}
|
||||
|
||||
export default function directionsRoutes() {
|
||||
const router = express.Router();
|
||||
const cache = new Map(); // "lat,lng;lat,lng" (rounded) -> { at, data }
|
||||
|
||||
// GET /api/directions?from=lat,lng&to=lat,lng
|
||||
router.get('/', async (req, res) => {
|
||||
const from = parseLatLng(req.query.from);
|
||||
const to = parseLatLng(req.query.to);
|
||||
if (!from || !to) {
|
||||
return res.status(400).json({ error: 'from and to must be lat,lng' });
|
||||
}
|
||||
|
||||
const osrmUrl = process.env.OSRM_URL || 'https://router.project-osrm.org';
|
||||
const key = `${from.lat.toFixed(5)},${from.lng.toFixed(5)};${to.lat.toFixed(5)},${to.lng.toFixed(5)}`;
|
||||
|
||||
const cached = cache.get(key);
|
||||
if (cached && Date.now() - cached.at < CACHE_TTL_MS) {
|
||||
return res.status(200).json(cached.data);
|
||||
}
|
||||
|
||||
const url = `${osrmUrl}/route/v1/driving/${from.lng},${from.lat};${to.lng},${to.lat}?overview=full&geometries=geojson`;
|
||||
let data;
|
||||
try {
|
||||
const upstream = await fetch(url, {
|
||||
headers: { 'User-Agent': USER_AGENT, Accept: 'application/json' },
|
||||
});
|
||||
if (!upstream.ok) throw new Error(`upstream status ${upstream.status}`);
|
||||
data = await upstream.json();
|
||||
} catch {
|
||||
return res.status(502).json({ error: 'directions unavailable' });
|
||||
}
|
||||
|
||||
const route = data && data.code === 'Ok' && Array.isArray(data.routes) ? data.routes[0] : null;
|
||||
if (!route) {
|
||||
return res.status(502).json({ error: 'directions unavailable' });
|
||||
}
|
||||
|
||||
const result = {
|
||||
km: round1(route.distance / 1000),
|
||||
geometry: route.geometry.coordinates.map(([lng, lat]) => [lat, lng]),
|
||||
};
|
||||
|
||||
if (cache.size >= MAX_CACHE_ENTRIES) {
|
||||
const oldestKey = cache.keys().next().value;
|
||||
cache.delete(oldestKey);
|
||||
}
|
||||
cache.set(key, { at: Date.now(), data: result });
|
||||
|
||||
res.status(200).json(result);
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
Reference in New Issue
Block a user