fifteen/apps/web/app/api/tiles/hidden-gems/[...tile]/route.ts

66 lines
2.1 KiB
TypeScript

import { NextRequest, NextResponse } from "next/server";
import { sql } from "@/lib/db";
export const runtime = "nodejs";
export async function GET(
req: NextRequest,
{ params }: { params: Promise<{ tile: string[] }> },
) {
const { tile } = await params;
if (tile.length !== 3) {
return new NextResponse("Invalid tile path", { status: 400 });
}
const z = parseInt(tile[0], 10);
const x = parseInt(tile[1], 10);
const y = parseInt(tile[2], 10);
if ([z, x, y].some(isNaN)) {
return new NextResponse("Invalid tile coordinates", { status: 400 });
}
const city = req.nextUrl.searchParams.get("city") ?? "";
if (!city) return new NextResponse("Missing city", { status: 400 });
try {
const rows = await Promise.resolve(sql<{ mvt: Uint8Array }[]>`
WITH
envelope AS (SELECT ST_TileEnvelope(${z}, ${x}, ${y}) AS env),
city_info AS (SELECT COALESCE(resolution_m, 200) AS resolution_m FROM cities WHERE slug = ${city})
SELECT ST_AsMVT(t, 'hidden-gems', 4096, 'geom') AS mvt
FROM (
SELECT
ST_AsMVTGeom(
ST_Expand(ST_Transform(gp.geom, 3857), ci.resolution_m::float / 2),
e.env,
4096, 0, true
) AS geom,
ROUND(gp.hidden_gem_score * 100)::int AS score
FROM grid_points gp
CROSS JOIN envelope e
CROSS JOIN city_info ci
WHERE gp.city_slug = ${city}
AND gp.hidden_gem_score IS NOT NULL
AND ST_Intersects(
ST_Transform(gp.geom, 3857),
ST_Expand(e.env, ci.resolution_m::float / 2)
)
) t
WHERE t.geom IS NOT NULL
`);
const buf = rows[0]?.mvt;
const data = buf ? new Uint8Array(buf) : new Uint8Array(0);
return new NextResponse(data, {
headers: {
"Content-Type": "application/x-protobuf",
"Cache-Control": "public, max-age=3600",
"Access-Control-Allow-Origin": "*",
},
});
} catch (err) {
console.error("[tiles/hidden-gems] Error:", err);
return new NextResponse("Internal Server Error", { status: 500 });
}
}