112 lines
3.1 KiB
TypeScript
112 lines
3.1 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
|
import { sql } from "@/lib/db";
|
|
import { cacheGet, cacheSet, hashParams } from "@/lib/cache";
|
|
import { CATEGORY_IDS } from "@transportationer/shared";
|
|
import type { Poi } from "@transportationer/shared";
|
|
|
|
export const runtime = "nodejs";
|
|
|
|
export async function GET(req: NextRequest) {
|
|
const p = req.nextUrl.searchParams;
|
|
const city = p.get("city") ?? "berlin";
|
|
const category = p.get("category");
|
|
const bboxStr = p.get("bbox");
|
|
const cluster = p.get("cluster") === "true";
|
|
const zoom = parseInt(p.get("zoom") ?? "12", 10);
|
|
|
|
if (category && !CATEGORY_IDS.includes(category as any)) {
|
|
return NextResponse.json(
|
|
{ error: "Invalid category", code: "INVALID_CATEGORY" },
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
|
|
const cacheKey = `api:pois:${hashParams({ city, category, bbox: bboxStr, cluster, zoom })}`;
|
|
const cached = await cacheGet<Poi[]>(cacheKey);
|
|
if (cached) {
|
|
return NextResponse.json(cached, { headers: { "X-Cache": "HIT" } });
|
|
}
|
|
|
|
let bboxFilter = sql`TRUE`;
|
|
if (bboxStr) {
|
|
const parts = bboxStr.split(",").map(Number);
|
|
if (parts.length !== 4 || parts.some(isNaN)) {
|
|
return NextResponse.json(
|
|
{ error: "Invalid bbox", code: "INVALID_BBOX" },
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
const [minLng, minLat, maxLng, maxLat] = parts;
|
|
bboxFilter = sql`ST_Within(p.geom, ST_MakeEnvelope(${minLng}, ${minLat}, ${maxLng}, ${maxLat}, 4326))`;
|
|
}
|
|
|
|
let catFilter = sql`TRUE`;
|
|
if (category) {
|
|
catFilter = sql`p.category = ${category}`;
|
|
}
|
|
|
|
if (cluster && zoom < 14) {
|
|
const rows = await Promise.resolve(sql<{
|
|
count: number;
|
|
lng: number;
|
|
lat: number;
|
|
category: string;
|
|
}[]>`
|
|
SELECT
|
|
COUNT(*)::int AS count,
|
|
ST_X(ST_Centroid(ST_Collect(p.geom)))::float AS lng,
|
|
ST_Y(ST_Centroid(ST_Collect(p.geom)))::float AS lat,
|
|
p.category
|
|
FROM raw_pois p
|
|
WHERE p.city_slug = ${city}
|
|
AND ${catFilter}
|
|
AND ${bboxFilter}
|
|
GROUP BY
|
|
ROUND(ST_X(p.geom)::numeric, 2),
|
|
ROUND(ST_Y(p.geom)::numeric, 2),
|
|
p.category
|
|
LIMIT 2000
|
|
`);
|
|
await cacheSet(cacheKey, rows, "API_POIS");
|
|
return NextResponse.json(rows);
|
|
}
|
|
|
|
const rows = await Promise.resolve(sql<{
|
|
osm_id: string;
|
|
osm_type: string;
|
|
category: string;
|
|
subcategory: string;
|
|
name: string | null;
|
|
lng: number;
|
|
lat: number;
|
|
}[]>`
|
|
SELECT
|
|
p.osm_id::text,
|
|
p.osm_type,
|
|
p.category,
|
|
p.subcategory,
|
|
p.name,
|
|
ST_X(p.geom)::float AS lng,
|
|
ST_Y(p.geom)::float AS lat
|
|
FROM raw_pois p
|
|
WHERE p.city_slug = ${city}
|
|
AND ${catFilter}
|
|
AND ${bboxFilter}
|
|
LIMIT 5000
|
|
`);
|
|
|
|
const pois: Poi[] = rows.map((r) => ({
|
|
osmId: r.osm_id,
|
|
osmType: r.osm_type as "N" | "W" | "R",
|
|
category: r.category as Poi["category"],
|
|
subcategory: r.subcategory,
|
|
name: r.name,
|
|
lng: r.lng,
|
|
lat: r.lat,
|
|
}));
|
|
|
|
await cacheSet(cacheKey, pois, "API_POIS");
|
|
return NextResponse.json(pois, {
|
|
headers: { "Cache-Control": "public, s-maxage=300" },
|
|
});
|
|
}
|