From e47b7fe28b5afc6c0c19473ef9cb14aacd205304 Mon Sep 17 00:00:00 2001 From: tiennm99 Date: Sun, 10 May 2026 00:40:47 +0700 Subject: [PATCH] fix(mapillary): always sub-sample small bbox + retry on transient errors --- src/lib/mapillary.js | 60 +++++++++++++++++++++----------------------- 1 file changed, 28 insertions(+), 32 deletions(-) diff --git a/src/lib/mapillary.js b/src/lib/mapillary.js index 3eb8796..e01e5f3 100644 --- a/src/lib/mapillary.js +++ b/src/lib/mapillary.js @@ -1,40 +1,34 @@ // Mapillary API utilities -// Maximum bbox area allowed by Mapillary API (square degrees). -// Mapillary tightened limits: keep area small enough to avoid 500 'reduce data' errors. -const MAX_BBOX_AREA = 0.001; +// Maximum bbox area allowed per Mapillary query (square degrees). +// Mapillary returns 500 'reduce data' / 'unknown error' on dense urban bboxes, +// so we keep each query small (~1km square) and rely on the retry loop to +// re-roll a new random sub-bbox if one query fails or returns empty. +const MAX_BBOX_AREA = 0.0001; -// Pick a random sub-bbox within the city bbox that fits Mapillary's area limit +// Pick a random sub-bbox within the city bbox that fits Mapillary's area limit. function getRandomSubBbox(bbox) { const [minLng, minLat, maxLng, maxLat] = bbox; const width = maxLng - minLng; const height = maxLat - minLat; - const area = width * height; - if (area <= MAX_BBOX_AREA) { - return bbox; - } - - // Calculate sub-bbox side length (square) that fits within the limit - const side = Math.sqrt(MAX_BBOX_AREA) * 0.9; // 10% margin for safety + // Always sub-sample even if the parent bbox is smaller than the cap — a + // smaller window keeps Mapillary happy in dense regions like central Hanoi. + const side = Math.sqrt(MAX_BBOX_AREA) * 0.9; // 10% safety margin const subWidth = Math.min(side, width); const subHeight = Math.min(side, height); - const startLng = minLng + Math.random() * (width - subWidth); - const startLat = minLat + Math.random() * (height - subHeight); + const startLng = minLng + Math.random() * Math.max(0, width - subWidth); + const startLat = minLat + Math.random() * Math.max(0, height - subHeight); return [startLng, startLat, startLng + subWidth, startLat + subHeight]; } -// Fetch images from a single bbox query +// Fetch images from a single bbox query. async function queryMapillary(accessToken, queryBbox) { const bboxString = queryBbox.join(','); const apiUrl = `https://graph.mapillary.com/images?access_token=${accessToken}&fields=id,thumb_2048_url,geometry,is_pano&limit=5&bbox=${bboxString}&is_pano=true`; - if (process.env.NODE_ENV === 'development') { - console.log('Mapillary query bbox:', bboxString); - } - const response = await fetch(apiUrl, { headers: { 'Accept': 'application/json' }, next: { revalidate: 0 } @@ -54,37 +48,39 @@ async function queryMapillary(accessToken, queryBbox) { return data.data || []; } -// Fetch images using city bbox, retrying with different sub-bboxes -const MAX_RETRIES = 5; +// Fetch panoramic images, retrying with a fresh sub-bbox on failure or empty +// result. Auth failures still abort early. +const MAX_RETRIES = 8; export async function fetchMapillaryImages(bbox) { const accessToken = process.env.MAPILLARY_ACCESS_TOKEN; - if (!accessToken) { throw new Error('MAPILLARY_ACCESS_TOKEN environment variable is not set'); } - for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) { - try { - const queryBbox = getRandomSubBbox(bbox); - const images = await queryMapillary(accessToken, queryBbox); + let lastError = null; + for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) { + const queryBbox = getRandomSubBbox(bbox); + try { + const images = await queryMapillary(accessToken, queryBbox); if (images.length > 0) { console.log(`Found ${images.length} images on attempt ${attempt}`); return { success: true, data: images }; } - - if (process.env.NODE_ENV === 'development') { - console.log(`Attempt ${attempt}/${MAX_RETRIES}: no images found, retrying...`); - } + console.log(`Attempt ${attempt}/${MAX_RETRIES}: no images, retrying with new sub-bbox`); } catch (error) { - console.error(`Attempt ${attempt}/${MAX_RETRIES} failed:`, error.message); - throw error; + // Auth issues are not transient — abort fast. + if (error.message === 'Mapillary authentication failed') throw error; + lastError = error; + console.error(`Attempt ${attempt}/${MAX_RETRIES} failed: ${error.message}; retrying with new sub-bbox`); } } return { success: false, - error: 'No street view images found in city bbox' + error: lastError + ? `No street view images found after ${MAX_RETRIES} attempts (last: ${lastError.message})` + : 'No street view images found in city bbox', }; }