feat: mark which pois were used for scoring in the UI
This commit is contained in:
parent
09d23e6666
commit
d0b29278bd
3 changed files with 51 additions and 12 deletions
|
|
@ -8,6 +8,13 @@ const CACHE_TOLERANCE_M = 50;
|
|||
|
||||
type CacheFeature = { geometry: object; properties: { contour: number } };
|
||||
|
||||
// Which raw travel modes feed into each synthetic persona mode for scoring.
|
||||
const DETAIL_MODES: Record<string, string[]> = {
|
||||
cyclist: ["walking", "cycling", "transit"],
|
||||
cycling_walk: ["walking", "cycling"],
|
||||
transit_walk: ["walking", "transit"],
|
||||
};
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
console.log("[reachable-pois] GET called:", req.nextUrl.search);
|
||||
const { searchParams } = req.nextUrl;
|
||||
|
|
@ -29,6 +36,9 @@ export async function GET(req: NextRequest) {
|
|||
mode === "transit_walk" ? "transit" :
|
||||
mode;
|
||||
|
||||
// Raw travel modes that feed into this mode's scoring (for scored POI lookup).
|
||||
const detailModes = DETAIL_MODES[mode] ?? [mode];
|
||||
|
||||
// Use the identical contours array that the display isochrone route caches with,
|
||||
// so the exact-match lookup is guaranteed to find the entry once isochroneData
|
||||
// is set on the client (which is our fetch trigger).
|
||||
|
|
@ -76,13 +86,34 @@ export async function GET(req: NextRequest) {
|
|||
return NextResponse.json({ pois: [], _debug: "no_polygon" }, { headers: { "Cache-Control": "no-store" } });
|
||||
}
|
||||
|
||||
const pois = await Promise.resolve(sql<{ category: string; subcategory: string; name: string | null; lat: number; lng: number }[]>`
|
||||
SELECT category, subcategory, name,
|
||||
ST_Y(geom)::float AS lat, ST_X(geom)::float AS lng
|
||||
FROM raw_pois
|
||||
WHERE city_slug = ${city}
|
||||
AND ST_Intersects(geom, ST_SetSRID(ST_GeomFromGeoJSON(${JSON.stringify(polygon)}), 4326))
|
||||
ORDER BY category, subcategory, name
|
||||
const pois = await Promise.resolve(sql<{
|
||||
category: string; subcategory: string; name: string | null;
|
||||
lat: number; lng: number; scored: boolean;
|
||||
}[]>`
|
||||
WITH nearest_gp AS (
|
||||
SELECT id AS grid_point_id
|
||||
FROM grid_points
|
||||
WHERE city_slug = ${city}
|
||||
ORDER BY geom <-> ST_SetSRID(ST_MakePoint(${lng}, ${lat}), 4326)
|
||||
LIMIT 1
|
||||
),
|
||||
scored_ids AS (
|
||||
SELECT DISTINCT gpd.nearest_poi_id
|
||||
FROM grid_poi_details gpd
|
||||
CROSS JOIN nearest_gp ng
|
||||
WHERE gpd.grid_point_id = ng.grid_point_id
|
||||
AND gpd.travel_mode = ANY(${detailModes})
|
||||
AND gpd.nearest_poi_id IS NOT NULL
|
||||
)
|
||||
SELECT
|
||||
p.category, p.subcategory, p.name,
|
||||
ST_Y(p.geom)::float AS lat, ST_X(p.geom)::float AS lng,
|
||||
(s.nearest_poi_id IS NOT NULL) AS scored
|
||||
FROM raw_pois p
|
||||
LEFT JOIN scored_ids s ON p.osm_id = s.nearest_poi_id
|
||||
WHERE p.city_slug = ${city}
|
||||
AND ST_Intersects(p.geom, ST_SetSRID(ST_GeomFromGeoJSON(${JSON.stringify(polygon)}), 4326))
|
||||
ORDER BY p.category, p.subcategory, p.name
|
||||
`);
|
||||
|
||||
if (pois.length === 0) {
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ export default function HomePage() {
|
|||
const [pinEstateScorePercentile, setPinEstateScorePercentile] = useState<number | null>(null);
|
||||
|
||||
// Reachable POI pins
|
||||
type ReachablePoi = { category: string; subcategory: string; name: string | null; lat: number; lng: number };
|
||||
type ReachablePoi = { category: string; subcategory: string; name: string | null; lat: number; lng: number; scored: boolean };
|
||||
const [reachablePois, setReachablePois] = useState<ReachablePoi[]>([]);
|
||||
const [showPois, setShowPois] = useState(true);
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ export type BaseOverlay = "accessibility" | "estate-value" | "hidden-gem";
|
|||
export type ReachablePoi = {
|
||||
category: string;
|
||||
subcategory: string;
|
||||
scored?: boolean;
|
||||
name: string | null;
|
||||
lat: number;
|
||||
lng: number;
|
||||
|
|
@ -503,7 +504,7 @@ export function MapView({
|
|||
features: reachablePois.map((poi) => ({
|
||||
type: "Feature",
|
||||
geometry: { type: "Point", coordinates: [poi.lng, poi.lat] },
|
||||
properties: { category: poi.category, subcategory: poi.subcategory, name: poi.name },
|
||||
properties: { category: poi.category, subcategory: poi.subcategory, name: poi.name, scored: poi.scored ?? false },
|
||||
})),
|
||||
};
|
||||
|
||||
|
|
@ -527,10 +528,17 @@ export function MapView({
|
|||
paint: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
"circle-color": colorMatch as any,
|
||||
"circle-radius": 6,
|
||||
// Scored POIs (in KNN set) are larger and fully opaque;
|
||||
// out-of-KNN POIs are smaller and faded.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
"circle-radius": ["case", ["get", "scored"], 6, 4] as any,
|
||||
"circle-stroke-color": "#ffffff",
|
||||
"circle-stroke-width": 1.5,
|
||||
"circle-opacity": 0.85,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
"circle-stroke-width": ["case", ["get", "scored"], 1.5, 1] as any,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
"circle-opacity": ["case", ["get", "scored"], 0.9, 0.5] as any,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
"circle-stroke-opacity": ["case", ["get", "scored"], 0.9, 0.5] as any,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue