112 lines
3.4 KiB
TypeScript
112 lines
3.4 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
|
import { sql } from "@/lib/db";
|
|
import { fetchIsochrone } from "@/lib/valhalla";
|
|
import { getValhallaQueue, getValhallaTransitQueue } from "@/lib/queue";
|
|
import { nextTuesdayDeparture } from "@transportationer/shared";
|
|
|
|
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";
|
|
const departureDate = mode === "transit" ? nextTuesdayDeparture() : null;
|
|
|
|
// 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 departure_date IS NOT DISTINCT FROM ${departureDate}
|
|
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) {
|
|
const raw = cached[0].result;
|
|
const result = typeof raw === "string" ? JSON.parse(raw as string) : raw;
|
|
return NextResponse.json({ ...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.
|
|
// Check the queue that owns the requested mode's instance.
|
|
const rebuildQueue = mode === "transit" ? getValhallaTransitQueue() : getValhallaQueue();
|
|
const activeValhalla = await rebuildQueue.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.
|
|
// Use an explicit ::jsonb cast so PostgreSQL receives a text parameter and
|
|
// parses it as JSON itself. Without the cast, postgres.js infers the JSONB
|
|
// column type and re-encodes the string as a JSONB string literal.
|
|
await Promise.resolve(sql`
|
|
INSERT INTO isochrone_cache (origin_geom, travel_mode, contours_min, departure_date, result)
|
|
VALUES (
|
|
ST_SetSRID(ST_MakePoint(${lng}, ${lat}), 4326),
|
|
${mode},
|
|
${contours},
|
|
${departureDate},
|
|
${JSON.stringify(geojson)}::jsonb
|
|
)
|
|
`);
|
|
|
|
return NextResponse.json({ ...geojson, cached: false });
|
|
}
|