-
-
Notifications
You must be signed in to change notification settings - Fork 8
Implement Elevation Heat Map for Resolution Search #544
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 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,21 +21,20 @@ import { BotMessage } from '@/components/message' | |
| import { SearchSection } from '@/components/search-section' | ||
| import SearchRelated from '@/components/search-related' | ||
| import { GeoJsonLayer } from '@/components/map/geojson-layer' | ||
| import { ElevationHeatmapLayer } from '@/components/map/elevation-heatmap-layer' | ||
| import { ResolutionCarousel } from '@/components/resolution-carousel' | ||
| import { ResolutionImage } from '@/components/resolution-image' | ||
| import { CopilotDisplay } from '@/components/copilot-display' | ||
| import RetrieveSection from '@/components/retrieve-section' | ||
| import { VideoSearchSection } from '@/components/video-search-section' | ||
| import { MapQueryHandler } from '@/components/map/map-query-handler' | ||
|
|
||
| // Define the type for related queries | ||
| type RelatedQueries = { | ||
| items: { query: string }[] | ||
| } | ||
|
|
||
| async function submit(formData?: FormData, skip?: boolean) { | ||
| 'use server' | ||
|
|
||
| const aiState = getMutableAIState<typeof AI>() | ||
| const uiStream = createStreamableUI() | ||
| const isGenerating = createStreamableValue(true) | ||
|
|
@@ -65,10 +64,8 @@ async function submit(formData?: FormData, skip?: boolean) { | |
|
|
||
| const mapboxBuffer = file_mapbox ? await file_mapbox.arrayBuffer() : null; | ||
| const mapboxDataUrl = mapboxBuffer ? `data:${file_mapbox.type};base64,${Buffer.from(mapboxBuffer).toString('base64')}` : null; | ||
|
|
||
| const googleBuffer = file_google ? await file_google.arrayBuffer() : null; | ||
| const googleDataUrl = googleBuffer ? `data:${file_google.type};base64,${Buffer.from(googleBuffer).toString('base64')}` : null; | ||
|
|
||
| const buffer = await file.arrayBuffer(); | ||
| const dataUrl = `data:${file.type};base64,${Buffer.from(buffer).toString('base64')}`; | ||
|
|
||
|
|
@@ -94,15 +91,14 @@ async function submit(formData?: FormData, skip?: boolean) { | |
| { id: nanoid(), role: 'user', content, type: 'input' } | ||
| ] | ||
| }); | ||
| messages.push({ role: 'user', content }); | ||
|
|
||
| messages.push({ role: 'user', content }); | ||
| const summaryStream = createStreamableValue<string>('Analyzing map view...'); | ||
| const groupeId = nanoid(); | ||
|
|
||
| async function processResolutionSearch() { | ||
| try { | ||
| const streamResult = await resolutionSearch(messages, timezone, drawnFeatures, location); | ||
|
|
||
| let fullSummary = ''; | ||
| for await (const partialObject of streamResult.partialObjectStream) { | ||
| if (partialObject.summary) { | ||
|
|
@@ -123,6 +119,33 @@ async function submit(formData?: FormData, skip?: boolean) { | |
| ); | ||
| } | ||
|
|
||
| let elevationPoints = undefined; | ||
| let elevationStatistics = undefined; | ||
| if (analysisResult.elevationData?.requested && analysisResult.elevationData.bounds) { | ||
| try { | ||
| const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000'; | ||
| const elevationResponse = await fetch( | ||
| `${baseUrl}/api/elevation?bounds=${JSON.stringify(analysisResult.elevationData.bounds)}&gridSize=${analysisResult.elevationData.gridSize || 20}` | ||
| ); | ||
|
Comment on lines
+128
to
+134
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. Building the elevation API URL by embedding raw Also, using Prefer calling the route via a relative URL from server code and encode query params explicitly. SuggestionUse a relative URL and const params = new URLSearchParams({
bounds: JSON.stringify(analysisResult.elevationData.bounds),
gridSize: String(analysisResult.elevationData.gridSize ?? 20),
});
const elevationResponse = await fetch(`/api/elevation?${params.toString()}`);If you need absolute URLs (e.g., for non-Next fetch contexts), derive it from Reply with "@CharlieHelps yes please" if you’d like me to add a commit with this suggestion. |
||
| if (elevationResponse.ok) { | ||
| const elevationData = await elevationResponse.json(); | ||
| if (elevationData.points && elevationData.points.length > 0) { | ||
| elevationPoints = elevationData.points; | ||
| elevationStatistics = elevationData.statistics; | ||
| uiStream.append( | ||
| <ElevationHeatmapLayer | ||
| id={groupeId} | ||
| points={elevationData.points} | ||
| statistics={elevationData.statistics} | ||
| /> | ||
| ); | ||
| } | ||
| } | ||
| } catch (error) { | ||
| console.error('Error fetching elevation data:', error); | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| } | ||
|
|
||
| messages.push({ role: 'assistant', content: analysisResult.summary || 'Analysis complete.' }); | ||
|
|
||
| const sanitizedMessages: CoreMessage[] = messages.map((m: any) => { | ||
|
|
@@ -147,7 +170,9 @@ async function submit(formData?: FormData, skip?: boolean) { | |
| } | ||
| return m | ||
| }); | ||
|
|
||
| const relatedQueries = await querySuggestor(uiStream, sanitizedMessages); | ||
|
|
||
| uiStream.append( | ||
| <Section title="Follow-up"> | ||
| <FollowupPanel /> | ||
|
|
@@ -173,7 +198,9 @@ async function submit(formData?: FormData, skip?: boolean) { | |
| ...analysisResult, | ||
| image: dataUrl, | ||
| mapboxImage: mapboxDataUrl, | ||
| googleImage: googleDataUrl | ||
| googleImage: googleDataUrl, | ||
| elevationPoints, | ||
| elevationStatistics | ||
| }), | ||
| type: 'resolution_search_result' | ||
| }, | ||
|
|
@@ -231,7 +258,7 @@ async function submit(formData?: FormData, skip?: boolean) { | |
| const definition = userInput.toLowerCase().trim() === 'what is a planet computer?' | ||
| ? `A planet computer is a proprietary environment aware system that interoperates weather forecasting, mapping and scheduling using cutting edge multi-agents to streamline automation and exploration on a planet. Available for our Pro and Enterprise customers. [QCX Pricing](https://www.queue.cx/#pricing)` | ||
| : `QCX-Terra is a model garden of pixel level precision geospatial foundational models for efficient land feature predictions from satellite imagery. Available for our Pro and Enterprise customers. [QCX Pricing] (https://www.queue.cx/#pricing)`; | ||
|
|
||
| const content = JSON.stringify(Object.fromEntries(formData!)); | ||
| const type = 'input'; | ||
| const groupeId = nanoid(); | ||
|
|
@@ -434,6 +461,7 @@ async function submit(formData?: FormData, skip?: boolean) { | |
| let toolOutputs: ToolResultPart[] = [] | ||
| let errorOccurred = false | ||
| const streamText = createStreamableValue<string>() | ||
|
|
||
| uiStream.update(<Spinner />) | ||
|
|
||
| while ( | ||
|
|
@@ -504,9 +532,7 @@ async function submit(formData?: FormData, skip?: boolean) { | |
| <FollowupPanel /> | ||
| </Section> | ||
| ) | ||
|
|
||
| await new Promise(resolve => setTimeout(resolve, 500)) | ||
|
|
||
| aiState.done({ | ||
| ...aiState.get(), | ||
| messages: [ | ||
|
|
@@ -549,9 +575,7 @@ async function submit(formData?: FormData, skip?: boolean) { | |
|
|
||
| async function clearChat() { | ||
| 'use server' | ||
|
|
||
| const aiState = getMutableAIState<typeof AI>() | ||
|
|
||
| aiState.done({ | ||
| chatId: nanoid(), | ||
| messages: [] | ||
|
|
@@ -589,10 +613,12 @@ export const AI = createAI<AIState, UIState>({ | |
| 'use server' | ||
|
|
||
| const aiState = getAIState() as AIState | ||
|
|
||
| if (aiState) { | ||
| const uiState = getUIStateFromAIState(aiState) | ||
| return uiState | ||
| } | ||
|
|
||
| return initialUIState | ||
| }, | ||
| onSetAIState: async ({ state }) => { | ||
|
|
@@ -605,8 +631,8 @@ export const AI = createAI<AIState, UIState>({ | |
| const { chatId, messages } = state | ||
| const createdAt = new Date() | ||
| const path = `/search/${chatId}` | ||
|
|
||
| let title = 'Untitled Chat' | ||
|
|
||
| if (messages.length > 0) { | ||
| const firstMessageContent = messages[0].content | ||
| if (typeof firstMessageContent === 'string') { | ||
|
|
@@ -655,13 +681,15 @@ export const AI = createAI<AIState, UIState>({ | |
| title, | ||
| messages: updatedMessages | ||
| } | ||
|
|
||
| await saveChat(chat, actualUserId) | ||
| } | ||
| }, | ||
| }) | ||
|
|
||
| export const getUIStateFromAIState = (aiState: AIState): UIState => { | ||
| const chatId = aiState.chatId | ||
| const isSharePage = aiState.isSharePage | ||
|
|
||
| return aiState.messages | ||
| .map((message, index) => { | ||
| const { role, content, id, type, name } = message | ||
|
|
@@ -687,6 +715,7 @@ export const getUIStateFromAIState = (aiState: AIState): UIState => { | |
| } catch (e) { | ||
| messageContent = content | ||
| } | ||
|
|
||
| return { | ||
| id, | ||
| component: ( | ||
|
|
@@ -741,24 +770,20 @@ export const getUIStateFromAIState = (aiState: AIState): UIState => { | |
| } | ||
| case 'resolution_search_result': { | ||
| const analysisResult = JSON.parse(content as string); | ||
| const geoJson = analysisResult.geoJson as FeatureCollection; | ||
| const image = analysisResult.image as string; | ||
| const mapboxImage = analysisResult.mapboxImage as string; | ||
| const googleImage = analysisResult.googleImage as string; | ||
|
|
||
| return { | ||
| id, | ||
| component: ( | ||
| <> | ||
| <Section title="response"> | ||
| <ResolutionCarousel | ||
| mapboxImage={mapboxImage} | ||
| googleImage={googleImage} | ||
| initialImage={image} | ||
| /> | ||
| {geoJson && ( | ||
| <GeoJsonLayer id={id} data={geoJson} /> | ||
| )} | ||
| </> | ||
| </Section> | ||
| ) | ||
| } | ||
|
Comment on lines
774
to
797
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.
The same gap applies to 🐛 Proposed fix — render layers from persisted state case 'resolution_search_result': {
const analysisResult = JSON.parse(content as string);
const image = analysisResult.image as string;
const mapboxImage = analysisResult.mapboxImage as string;
const googleImage = analysisResult.googleImage as string;
+ const elevationPoints = analysisResult.elevationPoints;
+ const elevationStatistics = analysisResult.elevationStatistics;
+ const geoJson = analysisResult.geoJson;
return {
id,
component: (
<Section title="response">
<ResolutionCarousel
mapboxImage={mapboxImage}
googleImage={googleImage}
initialImage={image}
/>
+ {geoJson && (
+ <GeoJsonLayer id={id} data={geoJson as FeatureCollection} />
+ )}
+ {elevationPoints && elevationPoints.length > 0 && (
+ <ElevationHeatmapLayer
+ id={id}
+ points={elevationPoints}
+ statistics={elevationStatistics}
+ />
+ )}
</Section>
)
}
}🤖 Prompt for AI Agents |
||
| } | ||
|
|
@@ -776,7 +801,6 @@ export const getUIStateFromAIState = (aiState: AIState): UIState => { | |
| ) { | ||
| const mapUrl = toolOutput.mcp_response?.mapUrl; | ||
| const placeName = toolOutput.mcp_response?.location?.place_name; | ||
|
|
||
| return { | ||
| id, | ||
| component: ( | ||
|
|
@@ -799,6 +823,7 @@ export const getUIStateFromAIState = (aiState: AIState): UIState => { | |
| JSON.stringify(toolOutput) | ||
| ) | ||
| searchResults.done(JSON.stringify(toolOutput)) | ||
|
|
||
| switch (name) { | ||
| case 'search': | ||
| return { | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,92 @@ | ||||||||||||||||||||||||||||||||
| import { NextRequest, NextResponse } from 'next/server'; | ||||||||||||||||||||||||||||||||
| import { generateCoordinateGrid, decodeMapboxTerrainRGB, getElevationStats } from '@/lib/utils/elevation'; | ||||||||||||||||||||||||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
|
Comment on lines
+1
to
+3
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.
Leaving this mismatch increases confusion and makes it harder to reason about correctness. SuggestionEither remove If keeping Tilequery/contours, tighten naming/comments so the API clearly returns contour-based elevations. Reply with "@CharlieHelps yes please" if you’d like me to add a commit with this suggestion. |
||||||||||||||||||||||||||||||||
| export const runtime = 'edge'; | ||||||||||||||||||||||||||||||||
|
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.
🛠️ Proposed fix-export const runtime = 'edge';
+// Use default Node.js runtime for long-running Mapbox fan-out requests🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| export async function GET(req: NextRequest) { | ||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||
| const { searchParams } = new URL(req.url); | ||||||||||||||||||||||||||||||||
| const boundsParam = searchParams.get('bounds'); | ||||||||||||||||||||||||||||||||
| const gridSizeParam = searchParams.get('gridSize'); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| if (!boundsParam) { | ||||||||||||||||||||||||||||||||
| return NextResponse.json( | ||||||||||||||||||||||||||||||||
| { error: 'Missing required parameter: bounds' }, | ||||||||||||||||||||||||||||||||
| { status: 400 } | ||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| const bounds = JSON.parse(boundsParam) as [number, number, number, number]; // [west, south, east, north] | ||||||||||||||||||||||||||||||||
| const gridSize = gridSizeParam ? parseInt(gridSizeParam) : 20; | ||||||||||||||||||||||||||||||||
|
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.
If Proposed fix+ let bounds: [number, number, number, number];
+ try {
+ bounds = JSON.parse(boundsParam);
+ } catch {
+ return NextResponse.json({ error: 'Invalid bounds format' }, { status: 400 });
+ }
+ if (!Array.isArray(bounds) || bounds.length !== 4 || bounds.some(v => typeof v !== 'number' || !isFinite(v))) {
+ return NextResponse.json({ error: 'bounds must be an array of 4 finite numbers [west, south, east, north]' }, { status: 400 });
+ }
- const bounds = JSON.parse(boundsParam) as [number, number, number, number];
- const gridSize = gridSizeParam ? parseInt(gridSizeParam) : 20;
+ const gridSize = gridSizeParam ? parseInt(gridSizeParam, 10) : 20;
+ if (!Number.isFinite(gridSize) || gridSize < 2 || gridSize > 50) {
+ return NextResponse.json({ error: 'gridSize must be an integer between 2 and 50' }, { status: 400 });
+ }📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
|
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, SuggestionTreat bad inputs as 400s and clamp let bounds: [number, number, number, number];
try {
const parsed = JSON.parse(boundsParam);
if (!Array.isArray(parsed) || parsed.length !== 4 || parsed.some(n => typeof n !== 'number')) {
return NextResponse.json({ error: 'Invalid bounds' }, { status: 400 });
}
bounds = parsed as [number, number, number, number];
} catch {
return NextResponse.json({ error: 'Invalid bounds JSON' }, { status: 400 });
}
const requestedGridSize = gridSizeParam ? Number.parseInt(gridSizeParam, 10) : 20;
const gridSize = Number.isFinite(requestedGridSize)
? Math.min(50, Math.max(5, requestedGridSize))
: 20;Reply with "@CharlieHelps yes please" if you’d like me to add a commit with this suggestion. |
||||||||||||||||||||||||||||||||
| // Generate grid points | ||||||||||||||||||||||||||||||||
| // generateCoordinateGrid expects bounds: [west, south, east, north] | ||||||||||||||||||||||||||||||||
| // But check the implementation I just wrote: | ||||||||||||||||||||||||||||||||
| // export function generateCoordinateGrid(bounds: [number, number, number, number], gridSize: number = 20) | ||||||||||||||||||||||||||||||||
| // inside: const [west, south, east, north] = bounds; | ||||||||||||||||||||||||||||||||
| // Implementation seems consistent. | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| const points = generateCoordinateGrid(bounds, gridSize); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // Fetch elevation data | ||||||||||||||||||||||||||||||||
| const mapboxToken = process.env.NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN; | ||||||||||||||||||||||||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||||||||||||||||||||||||||||||||
| if (!mapboxToken) { | ||||||||||||||||||||||||||||||||
| return NextResponse.json( | ||||||||||||||||||||||||||||||||
| { error: 'Mapbox access token not configured' }, | ||||||||||||||||||||||||||||||||
| { status: 500 } | ||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // Mapbox Tilequery API allows multiple coordinates? No, it's one by one or comma separated for features, but for terrain-rgb tilequery usually returns value at point. | ||||||||||||||||||||||||||||||||
| // The user snippet did fetch per point. I will do parallel fetch with limit. | ||||||||||||||||||||||||||||||||
| // Since runtime is edge, we can do parallel fetches. | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // Batching strategy: Mapbox Tilequery API supports multiple queries? | ||||||||||||||||||||||||||||||||
| // "Retrieve features from vector tiles based on a longitude and latitude." | ||||||||||||||||||||||||||||||||
| // It doesn't support batch points in one request. We must make N requests. | ||||||||||||||||||||||||||||||||
| // N = 20*20 = 400 requests. This might be too many for browser fetch limit or edge limit. | ||||||||||||||||||||||||||||||||
| // But let's try. Maybe reduce grid size default if needed. 400 is a lot. | ||||||||||||||||||||||||||||||||
| // User snippet uses 20x20. | ||||||||||||||||||||||||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // Limit concurrency to avoid hitting rate limits or fetch errors | ||||||||||||||||||||||||||||||||
| const batchSize = 10; | ||||||||||||||||||||||||||||||||
| const results = []; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| for (let i = 0; i < points.length; i += batchSize) { | ||||||||||||||||||||||||||||||||
| const batch = points.slice(i, i + batchSize); | ||||||||||||||||||||||||||||||||
| const batchPromises = batch.map(async (point) => { | ||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||
| const url = `https://api.mapbox.com/v4/mapbox.mapbox-terrain-v2/tilequery/${point.lng},${point.lat}.json?layers=contour&limit=1&access_token=${mapboxToken}`; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| const response = await fetch(url); | ||||||||||||||||||||||||||||||||
| if (!response.ok) return { ...point, elevation: 0 }; | ||||||||||||||||||||||||||||||||
|
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. Failed/rate-limited requests silently default to Line 62 defaults non-OK responses to Either mark failed points distinctly (e.g., Proposed fix- if (!response.ok) return { ...point, elevation: 0 };
+ if (!response.ok) return { ...point, elevation: null };
// ...
} catch (e) {
console.error(e);
- return { ...point, elevation: 0 };
+ return { ...point, elevation: null };
}
// ...
- const validPoints = results.filter(p => p.elevation !== undefined);
+ const validPoints = results.filter(p => p.elevation !== null && p.elevation !== undefined);Also applies to: 77-77 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| const data = await response.json(); | ||||||||||||||||||||||||||||||||
| const elevation = data.features && data.features.length > 0 ? data.features[0].properties?.ele : 0; | ||||||||||||||||||||||||||||||||
| return { ...point, elevation }; | ||||||||||||||||||||||||||||||||
| } catch (e) { | ||||||||||||||||||||||||||||||||
| console.error(e); | ||||||||||||||||||||||||||||||||
| return { ...point, elevation: 0 }; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| const batchResults = await Promise.all(batchPromises); | ||||||||||||||||||||||||||||||||
| results.push(...batchResults); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| const validPoints = results.filter(p => p.elevation !== undefined); | ||||||||||||||||||||||||||||||||
| const statistics = getElevationStats(validPoints); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
|
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 route makes up to You need either a different data source (e.g., raster SuggestionMinimum viable mitigation:
Example adjustment: if (!response.ok) return { ...point, elevation: null };
// ...
const elevation = /* extracted */;
return { ...point, elevation: typeof elevation === 'number' ? elevation : null };
const validPoints = results.filter(p => typeof p.elevation === 'number');Longer-term: consider sampling Mapbox Reply with "@CharlieHelps yes please" if you’d like me to add a commit with this suggestion. |
||||||||||||||||||||||||||||||||
| return NextResponse.json({ | ||||||||||||||||||||||||||||||||
| points: validPoints, | ||||||||||||||||||||||||||||||||
| statistics | ||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| } 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 } | ||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
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.
🧩 Analysis chain
🏁 Script executed:
rg -n "gridSize" --type=ts -C4Repository: QueueLab/QCX
Length of output: 4728
🏁 Script executed:
Repository: QueueLab/QCX
Length of output: 38
🏁 Script executed:
Repository: QueueLab/QCX
Length of output: 5199
🏁 Script executed:
Repository: QueueLab/QCX
Length of output: 226
🏁 Script executed:
Repository: QueueLab/QCX
Length of output: 226
🏁 Script executed:
cat -n app/api/elevation/route.ts | head -80Repository: QueueLab/QCX
Length of output: 3936
Add gridSize ceiling and URL-encode bounds parameter to prevent runaway Mapbox API costs.
The LLM-generated
gridSizeinanalysisResult.elevationData.gridSizeis forwarded unchecked across three files with no validation layer. Since the elevation API generates gridSize² coordinate points and makes one Mapbox Tilequery API call per point, an uncapped gridSize creates quadratic cost: gridSize=100 → 10,000 calls; gridSize=500 → 250,000 calls.Additionally,
JSON.stringify(bounds)is embedded in the URL withoutencodeURIComponent, allowing unencoded characters like{,",:to be sent as query parameters.🛡️ Proposed fix — cap gridSize and encode the query parameter
🤖 Prompt for AI Agents