fifteen/apps/web/app/api/isochrones/route.ts
2026-03-01 21:59:44 +01:00

101 lines
2.7 KiB
TypeScript

import { NextRequest, NextResponse } from "next/server";
import { sql } from "@/lib/db";
import { fetchIsochrone } from "@/lib/valhalla";
import { getValhallaQueue } from "@/lib/queue";
export const runtime = "nodejs";
const CACHE_TOLERANCE_M = 50;
export async function POST(req: NextRequest) {
let body: unknown;
try {
body = await req.json();
} catch {
return NextResponse.json(
{ error: "Invalid JSON body", code: "INVALID_BODY" },
{ status: 400 },
);
}
const { lng, lat, travelMode, contourMinutes } = body as Record<
string,
unknown
>;
if (typeof lng !== "number" || typeof lat !== "number") {
return NextResponse.json(
{ error: "lng and lat must be numbers", code: "INVALID_COORDS" },
{ status: 400 },
);
}
const contours: number[] = Array.isArray(contourMinutes)
? (contourMinutes as number[])
: [5, 10, 15];
const mode = typeof travelMode === "string" ? travelMode : "walking";
// Check PostGIS isochrone cache
const cached = await Promise.resolve(sql<{ result: object }[]>`
SELECT result
FROM isochrone_cache
WHERE travel_mode = ${mode}
AND contours_min = ${contours}
AND ST_DWithin(
origin_geom::geography,
ST_SetSRID(ST_MakePoint(${lng}, ${lat}), 4326)::geography,
${CACHE_TOLERANCE_M}
)
ORDER BY created_at DESC
LIMIT 1
`);
if (cached.length > 0) {
return NextResponse.json({ ...cached[0].result, cached: true });
}
// Refuse to call valhalla_service while tiles are being rebuilt —
// the service is stopped during the build and requests would hang or fail.
const activeValhalla = await getValhallaQueue().getActiveCount();
if (activeValhalla > 0) {
return NextResponse.json(
{ error: "Routing engine is rebuilding, please try again shortly.", code: "VALHALLA_REBUILDING" },
{ status: 503, headers: { "Retry-After": "60" } },
);
}
// Fetch from local Valhalla
let geojson: object;
try {
geojson = await fetchIsochrone({
lng,
lat,
travelMode: mode,
contourMinutes: contours,
polygons: true,
});
} catch (err) {
return NextResponse.json(
{
error: "Routing engine unavailable",
code: "VALHALLA_ERROR",
detail: err instanceof Error ? err.message : "unknown",
},
{ status: 503 },
);
}
// Store in PostGIS cache
await Promise.resolve(sql`
INSERT INTO isochrone_cache (origin_geom, travel_mode, contours_min, result)
VALUES (
ST_SetSRID(ST_MakePoint(${lng}, ${lat}), 4326),
${mode},
${contours},
${JSON.stringify(geojson)}
)
`);
return NextResponse.json({ ...geojson, cached: false });
}