-
-
Notifications
You must be signed in to change notification settings - Fork 6
Elevation Heat Map Implementation for Resolution Search #533
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
341c452
0479464
5ebce44
fd80962
0eddd18
7680a49
2e6bfda
48ccebc
ed7dec3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,177 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { NextRequest, NextResponse } from 'next/server'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import * as turf from '@turf/turf'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * GET /api/elevation | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Fetches elevation data for a grid of points within the given bounds. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Uses Mapbox Terrain vector tileset to get elevation values. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export async function GET(req: NextRequest) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { searchParams } = new URL(req.url); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const boundsParam = searchParams.get('bounds'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const gridSizeParam = searchParams.get('gridSize'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const geometryParam = searchParams.get('geometry'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!boundsParam) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return NextResponse.json( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { error: 'Missing required parameter: bounds' }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { status: 400 } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let bounds; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| bounds = JSON.parse(boundsParam); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (e) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return NextResponse.json({ error: 'Invalid bounds parameter' }, { status: 400 }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const gridSize = gridSizeParam ? parseInt(gridSizeParam) : 20; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const geometry = geometryParam ? JSON.parse(geometryParam) : null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+31
to
+32
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing try/catch around
🛡️ Proposed fix- const geometry = geometryParam ? JSON.parse(geometryParam) : null;
+ let geometry = null;
+ if (geometryParam) {
+ try {
+ geometry = JSON.parse(geometryParam);
+ } catch (e) {
+ return NextResponse.json({ error: 'Invalid geometry parameter' }, { status: 400 });
+ }
+ }📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [west, south, east, north] = bounds; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const points: Array<{ lng: number; lat: number; elevation: number | null }> = []; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const lonStep = (east - west) / gridSize; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const latStep = (north - south) / gridSize; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const polygon = geometry ? turf.polygon(geometry.coordinates) : null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+31
to
+41
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Separately, SuggestionHarden request parsing:
Reply with "@CharlieHelps yes please" if you'd like me to add a commit adding defensive parsing/validation and consistent 400 responses. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (let i = 0; i <= gridSize; i++) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (let j = 0; j <= gridSize; j++) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const lng = west + (lonStep * i); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const lat = south + (latStep * j); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (polygon) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const point = turf.point([lng, lat]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!turf.booleanPointInPolygon(point, polygon)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| continue; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| points.push({ lng, lat, elevation: null }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+42
to
+56
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unbounded concurrent HTTP requests to Mapbox will hit rate limits and cause failures. With the default 🐛 Proposed fix — batch with concurrency limitUse a concurrency-limited approach (e.g., a simple chunked +import pLimit from 'p-limit';
+const limit = pLimit(10); // max 10 concurrent requests
-const elevationPromises = points.map(async (point) => {
+const elevationPromises = points.map((point) => limit(async () => {
try {
const response = await fetch(
`https://api.mapbox.com/v4/mapbox.mapbox-terrain-v2/tilequery/${point.lng},${point.lat}.json?access_token=${token}`
);
// ... rest unchanged
} catch (error) {
console.error(`Error fetching elevation for ${point.lng},${point.lat}:`, error);
}
return point;
-});
+}));Alternatively, consider using the Mapbox Terrain-RGB raster tiles directly (fewer requests by fetching tile images and decoding pixel values) instead of one tilequery per point. Also applies to: 62-86, 88-88 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const token = process.env.MAPBOX_ACCESS_TOKEN || process.env.NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Mapbox access token fallback and missing-token check. If neither 🛡️ Proposed fix const token = process.env.MAPBOX_ACCESS_TOKEN || process.env.NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN;
+ if (!token) {
+ return NextResponse.json({ error: 'Mapbox access token not configured' }, { status: 500 });
+ }📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Fetch elevation data using Mapbox Terrain vector tiles (v2) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // This tileset contains contour lines with 'ele' property | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const elevationPromises = points.map(async (point) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const response = await fetch( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `https://api.mapbox.com/v4/mapbox.mapbox-terrain-v2/tilequery/${point.lng},${point.lat}.json?access_token=${token}` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+31
to
+66
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This endpoint allows unbounded Also, if SuggestionAdd server-side validation and fail-fast checks:
Reply with "@CharlieHelps yes please" if you'd like me to add a commit implementing validation + clearer error handling. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (response.ok) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const data = await response.json(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (data.features && data.features.length > 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Find the feature with the highest elevation in this point's vicinity | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // or just take the first one if it has 'ele' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const elevations = data.features | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .map((f: any) => f.properties.ele) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .filter((e: any) => e !== undefined); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (elevations.length > 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| point.elevation = Math.max(...elevations); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error(`Error fetching elevation for ${point.lng},${point.lat}:`, error); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return point; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const elevationData = await Promise.all(elevationPromises); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const validPoints = elevationData.filter(p => p.elevation !== null); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+36
to
+89
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This endpoint fires one Mapbox Tilequery request per grid point (
This needs concurrency limiting and/or a different sampling strategy (vector tile fetch, coarser grid, adaptive sampling, or batch where possible). SuggestionAdd a concurrency limiter (e.g.
Reply with "@CharlieHelps yes please" if you'd like me to add a commit with concurrency limiting + |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (validPoints.length === 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return NextResponse.json({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| success: true, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| points: [], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| statistics: { min: 0, max: 0, average: 0, count: 0 }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| bounds, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| gridSize, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const elevations = validPoints.map(p => p.elevation as number); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const minElevation = Math.min(...elevations); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const maxElevation = Math.max(...elevations); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const avgElevation = elevations.reduce((sum, e) => sum + e, 0) / elevations.length; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return NextResponse.json({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| success: true, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| points: validPoints, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| statistics: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| min: minElevation, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| max: maxElevation, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| average: Math.round(avgElevation * 10) / 10, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| count: validPoints.length, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| bounds, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| gridSize, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error('Error fetching elevation data:', error); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return NextResponse.json( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| error: 'Failed to fetch elevation data', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| details: error instanceof Error ? error.message : 'Unknown error' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { status: 500 } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+119
to
+128
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Avoid leaking internal error details to the client. Line 124 sends ♻️ Proposed fix {
error: 'Failed to fetch elevation data',
- details: error instanceof Error ? error.message : 'Unknown error'
},The same applies to the POST handler at Line 172. 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export async function POST(req: NextRequest) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const body = await req.json(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { features, gridSize = 20 } = body; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!features || !Array.isArray(features)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return NextResponse.json( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { error: 'Missing or invalid features array' }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { status: 400 } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const results = await Promise.all( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| features.map(async (feature: any) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (feature.geometry.type !== 'Polygon') { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const bbox = turf.bbox(feature); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const params = new URLSearchParams({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| bounds: JSON.stringify(bbox), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| gridSize: gridSize.toString(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| geometry: JSON.stringify(feature.geometry), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const url = new URL(`/api/elevation?${params}`, req.url); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const response = await GET(new NextRequest(url)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return await response.json(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+143
to
+159
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. POST handler: missing null-check on Line 145 accesses 🛡️ Proposed fix features.map(async (feature: any) => {
- if (feature.geometry.type !== 'Polygon') {
+ if (!feature.geometry || feature.geometry.type !== 'Polygon') {
return null;
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+131
to
+160
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If you want an internal helper, extract the shared logic into a pure function (e.g., SuggestionRefactor shared logic into a helper and avoid synthetic async function queryElevation({ bounds, gridSize, geometry }: { bounds: number[]; gridSize: number; geometry?: any }) {
// existing GET logic, but return a plain object
}
export async function GET(req: NextRequest) {
// parse params; return NextResponse.json(await queryElevation(...))
}
export async function POST(req: NextRequest) {
// parse body; call queryElevation per feature
// ensure you propagate per-item errors/statuses appropriately
}Reply with "@CharlieHelps yes please" if you’d like me to add a commit with this suggestion. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return NextResponse.json({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| success: true, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| results: results.filter(r => r !== null), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error('Error in batch elevation query:', error); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return NextResponse.json( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| error: 'Failed to process batch elevation query', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| details: error instanceof Error ? error.message : 'Unknown error' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { status: 500 } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| <<<<<<< SEARCH | ||
| import dynamic from 'next/dynamic' | ||
| import { ResolutionCarousel } from '@/components/resolution-carousel' | ||
|
|
||
| const ElevationHeatmapLayer = dynamic(() => import('@/components/map/elevation-heatmap-layer').then(mod => mod.ElevationHeatmapLayer), { ssr: false }) | ||
| ======= | ||
| import { ElevationHeatmapLayer } from '@/components/map/elevation-heatmap-layer' | ||
| import { ResolutionCarousel } from '@/components/resolution-carousel' | ||
| >>>>>>> REPLACE | ||
|
Comment on lines
+1
to
+9
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Development artifact committed to repository — should be removed.
🤖 Prompt for AI Agents |
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,143 @@ | ||||||||||||||||||||||||||||||
| 'use client' | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| import { useEffect, useState } from 'react' | ||||||||||||||||||||||||||||||
| import { useMap } from './map-context' | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| interface ElevationPoint { | ||||||||||||||||||||||||||||||
| lng: number; | ||||||||||||||||||||||||||||||
| lat: number; | ||||||||||||||||||||||||||||||
| elevation: number; | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| interface ElevationHeatmapLayerProps { | ||||||||||||||||||||||||||||||
| id: string; | ||||||||||||||||||||||||||||||
| points: ElevationPoint[]; | ||||||||||||||||||||||||||||||
| statistics?: { | ||||||||||||||||||||||||||||||
| min: number; | ||||||||||||||||||||||||||||||
| max: number; | ||||||||||||||||||||||||||||||
| average: number; | ||||||||||||||||||||||||||||||
| count: number; | ||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| export function ElevationHeatmapLayer({ id, points, statistics }: ElevationHeatmapLayerProps) { | ||||||||||||||||||||||||||||||
| const { map } = useMap() | ||||||||||||||||||||||||||||||
| const [mapboxgl, setMapboxgl] = useState<any>(null) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||
| import('mapbox-gl').then(mod => { | ||||||||||||||||||||||||||||||
| setMapboxgl(mod.default) | ||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||
| }, []) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||
| if (!map || !points || points.length === 0 || !mapboxgl) return | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| const sourceId = `elevation-heatmap-source-${id}` | ||||||||||||||||||||||||||||||
| const heatmapLayerId = `elevation-heatmap-layer-${id}` | ||||||||||||||||||||||||||||||
| const pointsLayerId = `elevation-points-layer-${id}` | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| const geojson: any = { | ||||||||||||||||||||||||||||||
| type: 'FeatureCollection', | ||||||||||||||||||||||||||||||
| features: points.map(point => ({ | ||||||||||||||||||||||||||||||
| type: 'Feature', | ||||||||||||||||||||||||||||||
| geometry: { | ||||||||||||||||||||||||||||||
| type: 'Point', | ||||||||||||||||||||||||||||||
| coordinates: [point.lng, point.lat] | ||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||
| properties: { | ||||||||||||||||||||||||||||||
| elevation: point.elevation, | ||||||||||||||||||||||||||||||
| intensity: statistics && statistics.max !== statistics.min | ||||||||||||||||||||||||||||||
| ? (point.elevation - statistics.min) / (statistics.max - statistics.min) | ||||||||||||||||||||||||||||||
| : 0.5 | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| })) | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| const onMapLoad = () => { | ||||||||||||||||||||||||||||||
| if (!map.getSource(sourceId)) { | ||||||||||||||||||||||||||||||
| map.addSource(sourceId, { | ||||||||||||||||||||||||||||||
| type: 'geojson', | ||||||||||||||||||||||||||||||
| data: geojson | ||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| map.addLayer({ | ||||||||||||||||||||||||||||||
| id: heatmapLayerId, | ||||||||||||||||||||||||||||||
| type: 'heatmap', | ||||||||||||||||||||||||||||||
| source: sourceId, | ||||||||||||||||||||||||||||||
| paint: { | ||||||||||||||||||||||||||||||
| 'heatmap-weight': ['interpolate', ['linear'], ['get', 'intensity'], 0, 0, 1, 1], | ||||||||||||||||||||||||||||||
| 'heatmap-intensity': ['interpolate', ['linear'], ['zoom'], 0, 1, 15, 3], | ||||||||||||||||||||||||||||||
| 'heatmap-color': [ | ||||||||||||||||||||||||||||||
| 'interpolate', | ||||||||||||||||||||||||||||||
| ['linear'], | ||||||||||||||||||||||||||||||
| ['heatmap-density'], | ||||||||||||||||||||||||||||||
| 0, 'rgba(33,102,172,0)', | ||||||||||||||||||||||||||||||
| 0.2, 'rgb(103,169,207)', | ||||||||||||||||||||||||||||||
| 0.4, 'rgb(209,229,240)', | ||||||||||||||||||||||||||||||
| 0.6, 'rgb(253,219,199)', | ||||||||||||||||||||||||||||||
| 0.8, 'rgb(239,138,98)', | ||||||||||||||||||||||||||||||
| 1, 'rgb(178,24,43)' | ||||||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||||||
| 'heatmap-radius': ['interpolate', ['linear'], ['zoom'], 0, 2, 15, 20], | ||||||||||||||||||||||||||||||
| 'heatmap-opacity': ['interpolate', ['linear'], ['zoom'], 7, 0.7, 15, 0.5] | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| map.addLayer({ | ||||||||||||||||||||||||||||||
| id: pointsLayerId, | ||||||||||||||||||||||||||||||
| type: 'circle', | ||||||||||||||||||||||||||||||
| source: sourceId, | ||||||||||||||||||||||||||||||
| minzoom: 14, | ||||||||||||||||||||||||||||||
| paint: { | ||||||||||||||||||||||||||||||
| 'circle-radius': ['interpolate', ['linear'], ['zoom'], 14, 3, 22, 8], | ||||||||||||||||||||||||||||||
| 'circle-color': [ | ||||||||||||||||||||||||||||||
| 'interpolate', | ||||||||||||||||||||||||||||||
| ['linear'], | ||||||||||||||||||||||||||||||
| ['get', 'intensity'], | ||||||||||||||||||||||||||||||
| 0, '#2166ac', | ||||||||||||||||||||||||||||||
| 0.25, '#67a9cf', | ||||||||||||||||||||||||||||||
| 0.5, '#f7f7f7', | ||||||||||||||||||||||||||||||
| 0.75, '#ef8a62', | ||||||||||||||||||||||||||||||
| 1, '#b2182b' | ||||||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||||||
| 'circle-stroke-color': 'white', | ||||||||||||||||||||||||||||||
| 'circle-stroke-width': 1, | ||||||||||||||||||||||||||||||
| 'circle-opacity': ['interpolate', ['linear'], ['zoom'], 14, 0, 15, 0.8] | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| const clickHandler = (e: any) => { | ||||||||||||||||||||||||||||||
| if (!e.features || e.features.length === 0) return | ||||||||||||||||||||||||||||||
| const elevation = e.features[0].properties?.elevation | ||||||||||||||||||||||||||||||
| if (elevation !== undefined) { | ||||||||||||||||||||||||||||||
| new mapboxgl.Popup() | ||||||||||||||||||||||||||||||
| .setLngLat(e.lngLat) | ||||||||||||||||||||||||||||||
| .setHTML(`<strong>Elevation:</strong> ${elevation}m`) | ||||||||||||||||||||||||||||||
| .addTo(map) | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| map.on('click', pointsLayerId, clickHandler) | ||||||||||||||||||||||||||||||
| map.on('mouseenter', pointsLayerId, () => { map.getCanvas().style.cursor = 'pointer' }) | ||||||||||||||||||||||||||||||
| map.on('mouseleave', pointsLayerId, () => { map.getCanvas().style.cursor = '' }) | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| if (map.isStyleLoaded()) { | ||||||||||||||||||||||||||||||
| onMapLoad() | ||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||
| map.once('load', onMapLoad) | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| return () => { | ||||||||||||||||||||||||||||||
| if (map) { | ||||||||||||||||||||||||||||||
| if (map.getLayer(pointsLayerId)) map.removeLayer(pointsLayerId) | ||||||||||||||||||||||||||||||
| if (map.getLayer(heatmapLayerId)) map.removeLayer(heatmapLayerId) | ||||||||||||||||||||||||||||||
| if (map.getSource(sourceId)) map.removeSource(sourceId) | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
Comment on lines
+133
to
+139
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Cleanup lacks The existing 🛡️ Proposed fix return () => {
- if (map) {
+ if (map && map.isStyleLoaded()) {
if (map.getLayer(pointsLayerId)) map.removeLayer(pointsLayerId)
if (map.getLayer(heatmapLayerId)) map.removeLayer(heatmapLayerId)
if (map.getSource(sourceId)) map.removeSource(sourceId)
}
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
Comment on lines
+127
to
+139
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When Fix: cancel the pending listener at the top of cleanup, and add the missing 🐛 Proposed fix return () => {
- if (map) {
+ map.off('load', onMapLoad) // cancel pending once-listener before it fires
+ if (map && map.isStyleLoaded()) {
if (map.getLayer(pointsLayerId)) map.removeLayer(pointsLayerId)
if (map.getLayer(heatmapLayerId)) map.removeLayer(heatmapLayerId)
if (map.getSource(sourceId)) map.removeSource(sourceId)
}
}🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||
| }, [map, id, points, statistics, mapboxgl]) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| return null | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No upper-bound validation on
gridSize— potential DoS vector.A caller can pass
gridSize=500, generating501² = 251,001Mapbox API calls from a single request. Add a reasonable cap (e.g., 50) and validate thatgridSizeis a positive integer.🛡️ Proposed fix
📝 Committable suggestion
🤖 Prompt for AI Agents