mirror of
https://github.com/tiennm99/vngeoguessr.git
synced 2026-10-15 16:15:23 +00:00
fix(mapillary): port viguessr random-point + per-city delta strategy
Replaces adaptive shrink/grow bbox loop with viguessr's proven approach: random point inside city bbox → fixed-size sub-bbox of side 2*delta → re-roll position on empty/error. Small fixed bbox keeps Mapillary query cost under their 'reduce data' cap; dart-throw eventually lands on a pano-rich window. - src/lib/game.js: add per-city mapillaryDelta (HN=0.003, others=0.005) and export cityDeltas alongside existing cityBboxes - src/lib/mapillary.js: rewrite fetchMapillaryImages(bbox, delta) with 10-attempt position-only retry; revert to thumb_original_url; limit=3 - src/app/api/new-game/route.js: pass cityDeltas[cityCode] to the fetcher Source: https://github.com/luuvanduc1999/viguessr (assets/js/game.js)
This commit is contained in:
1 parent
a3a0667774
commit
f12fef855d
5 files changed
+346
-82
No files matched your search
+176
@@ -0,0 +1,176 @@
|
||||
---
|
||||
phase: 1
|
||||
title: "Per-city Delta + Random-Point Fetch"
|
||||
status: pending
|
||||
priority: P1
|
||||
effort: "30m"
|
||||
dependencies: []
|
||||
---
|
||||
|
||||
# Phase 1: Per-city Delta + Random-Point Fetch
|
||||
|
||||
## Overview
|
||||
|
||||
Replace the adaptive bbox loop in `src/lib/mapillary.js` with viguessr's random-point dart-throw using a per-city fixed `delta`. Add `mapillaryDelta` to each `CITIES` entry. Update `new-game` route to pass delta and use `thumb_original_url`.
|
||||
|
||||
## Related Code Files
|
||||
|
||||
- Modify: `src/lib/game.js` — add `mapillaryDelta` field on each `CITIES` entry; export `cityDeltas`
|
||||
- Rewrite: `src/lib/mapillary.js` — random-point + fixed-delta + retry-position
|
||||
- Modify: `src/app/api/new-game/route.js` — import `cityDeltas`, pass to fetch, switch back to `thumb_original_url`
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### 1. `src/lib/game.js`
|
||||
|
||||
Inside each `CITIES.<code>` object, add a numeric field `mapillaryDelta`:
|
||||
|
||||
```js
|
||||
HN: { ..., mapillaryDelta: 0.003 },
|
||||
TPHCM: { ..., mapillaryDelta: 0.005 },
|
||||
DL: { ..., mapillaryDelta: 0.005 },
|
||||
DH: { ..., mapillaryDelta: 0.005 },
|
||||
DN: { ..., mapillaryDelta: 0.005 }, // disabled but populated
|
||||
```
|
||||
|
||||
Below the existing `cityBboxes` export, add:
|
||||
|
||||
```js
|
||||
export const cityDeltas = Object.fromEntries(
|
||||
Object.values(CITIES).map(city => [city.code, city.mapillaryDelta])
|
||||
);
|
||||
```
|
||||
|
||||
### 2. `src/lib/mapillary.js` (full rewrite)
|
||||
|
||||
```js
|
||||
// Mapillary API utilities
|
||||
//
|
||||
// Strategy ported from viguessr (https://github.com/luuvanduc1999/viguessr):
|
||||
// pick a random point inside the city bbox, query a small fixed-size sub-bbox
|
||||
// (side = 2*delta) centered on that point, and re-roll the point on empty
|
||||
// results or transient 5xx. The fixed small size keeps Mapillary query cost
|
||||
// below their cap; the dart-throw eventually lands on a pano-rich window.
|
||||
|
||||
const MAX_RETRIES = 10;
|
||||
|
||||
export async function fetchMapillaryImages(bbox, delta) {
|
||||
const accessToken = process.env.MAPILLARY_ACCESS_TOKEN;
|
||||
if (!accessToken) {
|
||||
throw new Error('MAPILLARY_ACCESS_TOKEN environment variable is not set');
|
||||
}
|
||||
|
||||
const [minLng, minLat, maxLng, maxLat] = bbox;
|
||||
let lastError = null;
|
||||
|
||||
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
||||
const lat = Math.random() * (maxLat - minLat) + minLat;
|
||||
const lng = Math.random() * (maxLng - minLng) + minLng;
|
||||
const queryBbox = [
|
||||
(lng - delta).toFixed(4),
|
||||
(lat - delta).toFixed(4),
|
||||
(lng + delta).toFixed(4),
|
||||
(lat + delta).toFixed(4),
|
||||
].join(',');
|
||||
const apiUrl = `https://graph.mapillary.com/images?access_token=${accessToken}&fields=id,thumb_original_url,geometry,is_pano&limit=3&bbox=${queryBbox}&is_pano=true`;
|
||||
|
||||
try {
|
||||
const response = await fetch(apiUrl, {
|
||||
headers: { Accept: 'application/json' },
|
||||
next: { revalidate: 0 },
|
||||
});
|
||||
|
||||
if (response.status === 401) {
|
||||
throw new Error('Mapillary authentication failed');
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const body = await response.text().catch(() => '<unreadable>');
|
||||
lastError = `${response.status}: ${body.slice(0, 200)}`;
|
||||
console.error(`Attempt ${attempt}/${MAX_RETRIES}: Mapillary ${response.status}; re-rolling`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const images = data.data || [];
|
||||
const panos = images.filter(img => img.is_pano);
|
||||
|
||||
if (panos.length > 0) {
|
||||
console.log(`Found ${panos.length} panos on attempt ${attempt}`);
|
||||
return { success: true, data: panos };
|
||||
}
|
||||
|
||||
console.log(`Attempt ${attempt}/${MAX_RETRIES}: no panos; re-rolling`);
|
||||
} catch (error) {
|
||||
if (error.message === 'Mapillary authentication failed') {
|
||||
throw error;
|
||||
}
|
||||
lastError = error.message;
|
||||
console.error(`Attempt ${attempt}/${MAX_RETRIES} threw: ${error.message}; re-rolling`);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: lastError
|
||||
? `No panos after ${MAX_RETRIES} attempts (last: ${lastError})`
|
||||
: 'No panos found',
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
Removed:
|
||||
- `MAX_BBOX_AREA` / `MIN_BBOX_AREA` / `GROW_FACTOR` / `SHRINK_FACTOR`
|
||||
- `getRandomSubBbox` helper (replaced inline)
|
||||
- Adaptive shrink/grow loop
|
||||
|
||||
### 3. `src/app/api/new-game/route.js`
|
||||
|
||||
Update imports + call:
|
||||
|
||||
```js
|
||||
// Before:
|
||||
import { cityNames, cityBboxes } from '../../../lib/game.js';
|
||||
const bbox = cityBboxes[cityCode];
|
||||
const imageResult = await fetchMapillaryImages(bbox);
|
||||
const imageUrl = selectedImage.thumb_2048_url;
|
||||
|
||||
// After:
|
||||
import { cityNames, cityBboxes, cityDeltas } from '../../../lib/game.js';
|
||||
const bbox = cityBboxes[cityCode];
|
||||
const delta = cityDeltas[cityCode];
|
||||
const imageResult = await fetchMapillaryImages(bbox, delta);
|
||||
const imageUrl = selectedImage.thumb_original_url;
|
||||
```
|
||||
|
||||
Add a guard for missing delta:
|
||||
|
||||
```js
|
||||
if (!delta) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: `No Mapillary delta configured for city: ${cityCode}`,
|
||||
}, { status: 400 });
|
||||
}
|
||||
```
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [ ] `npm run build` passes
|
||||
- [ ] HN: 3 sample requests → ≥2 success
|
||||
- [ ] TPHCM: 3 sample requests → ≥2 success
|
||||
- [ ] DL: 3 sample requests → ≥2 success
|
||||
- [ ] DH: 3 sample requests → ≥2 success
|
||||
- [ ] No 'reduce data' 500s in production logs after deploy
|
||||
- [ ] `grep "MAX_BBOX_AREA\|GROW_FACTOR\|SHRINK_FACTOR" src/` empty
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
- **Risk**: viguessr's HN delta=0.003 (666m) might still occasionally hit 'reduce data' if Mapillary tightens further. **Mitigation**: 10 retries; if persistent, drop HN delta to 0.002.
|
||||
- **Risk**: Per-city delta requires a value for each enabled city; missing entry crashes. **Mitigation**: explicit guard in route; default fallback to 0.005 if undefined.
|
||||
- **Risk**: `thumb_original_url` may be deprecated. **Mitigation**: viguessr is shipping it in production today; if Mapillary deprecates, swap to `thumb_2048_url`.
|
||||
- **Risk**: Rare cities with sparse pano coverage (DH 666m windows) might exhaust retries. **Mitigation**: keep delta=0.005 (1.1km) for non-HN cities.
|
||||
|
||||
## Rollback
|
||||
|
||||
Single commit; revert HEAD to restore current adaptive code. Mapillary deploys with no schema/state changes, so rollback is instant.
|
||||
@@ -0,0 +1,94 @@
|
||||
---
|
||||
title: "Port viguessr Mapillary strategy"
|
||||
description: "Replace adaptive shrink/grow bbox loop with viguessr's proven approach: per-city fixed `delta` (half-bbox-side) + random-point dart-throw + simple retry-with-new-point. Fixes 'reduce data' 500s in HN/HCM dense areas."
|
||||
status: pending
|
||||
priority: P1
|
||||
effort: 30m
|
||||
created: 2026-05-10
|
||||
source: https://github.com/luuvanduc1999/viguessr (assets/js/game.js, obfuscated)
|
||||
blockedBy: []
|
||||
blocks: []
|
||||
---
|
||||
|
||||
# Port viguessr Mapillary strategy
|
||||
|
||||
## Why
|
||||
|
||||
Current `src/lib/mapillary.js` adaptive bbox sizing fails consistently in dense HN/HCM — Mapillary returns 500 'reduce data' when query bbox costs too much to scan. Adaptive grow on empty results worsens this.
|
||||
|
||||
viguessr (a sibling vngeoguessr project) ships a working implementation for the same cities. Their approach: **per-city fixed `delta`**, query a small bbox centered on a random point, retry only by re-rolling the random point (never resizing the bbox). The small fixed bbox keeps Mapillary query cost under their cap; the dart-throw eventually finds pano-rich windows.
|
||||
|
||||
## Source decoded (from obfuscated game.js)
|
||||
|
||||
```js
|
||||
boundingBoxVN = {
|
||||
'HN': [105.77, 20.96, 105.88, 21.05, delta=0.003],
|
||||
'TPHCM': [106.62, 10.71, 106.75, 10.83, delta=0.005],
|
||||
'HP': [106.65, 20.80, 106.75, 20.90, delta=0.05],
|
||||
'ND': [106.00, 20.35, 106.25, 20.50, delta=0.005],
|
||||
'DN': [108.17, 16.00, 108.25, 16.10, delta=0.005],
|
||||
};
|
||||
|
||||
async function getRandomMapillaryImage() {
|
||||
const lat = Math.random() * (maxLat - minLat) + minLat;
|
||||
const lng = Math.random() * (maxLong - minLong) + minLong;
|
||||
const bbox = [lng-delta, lat-delta, lng+delta, lat+delta].map(n => n.toFixed(4)).join(',');
|
||||
const url = `https://graph.mapillary.com/images?access_token=${TOKEN}&fields=id,thumb_original_url,geometry,is_pano&limit=3&bbox=${bbox}&is_pano=true`;
|
||||
const json = await (await fetch(url)).json();
|
||||
if (json.data?.length) {
|
||||
const panos = json.data.filter(d => d.is_pano);
|
||||
return panos.length ? panos[rand(panos.length)] : json.data[0];
|
||||
}
|
||||
return getRandomMapillaryImage(); // re-roll, same delta
|
||||
}
|
||||
```
|
||||
|
||||
## What changes locally
|
||||
|
||||
### `src/lib/game.js` (CITIES schema)
|
||||
|
||||
Add `mapillaryDelta` field to each enabled `CITIES` entry:
|
||||
|
||||
| Code | New `mapillaryDelta` | Source rationale |
|
||||
|---|---|---|
|
||||
| HN | `0.003` | viguessr value |
|
||||
| TPHCM | `0.005` | viguessr value |
|
||||
| DL | `0.005` | not in viguessr; use TPHCM-class default |
|
||||
| DH | `0.005` | not in viguessr; use TPHCM-class default |
|
||||
| DN | `0.005` (still disabled) | viguessr value, future-proof |
|
||||
|
||||
Export a `cityDeltas` map alongside existing `cityBboxes`/`cityNames`/`cityCenters`.
|
||||
|
||||
### `src/lib/mapillary.js` (rewrite)
|
||||
|
||||
Replace adaptive loop with viguessr-style: random point inside city bbox, fixed-size sub-bbox of side `2*delta`, re-roll position on empty/error.
|
||||
|
||||
Signature change: `fetchMapillaryImages(bbox, delta)`.
|
||||
|
||||
### `src/app/api/new-game/route.js`
|
||||
|
||||
- Import `cityDeltas` from `lib/game.js`.
|
||||
- Pass `cityDeltas[cityCode]` to `fetchMapillaryImages`.
|
||||
- Use `selectedImage.thumb_original_url` (revert from `thumb_2048_url`).
|
||||
|
||||
## Phases
|
||||
|
||||
| # | Phase | Effort | Status |
|
||||
|---|---|---|---|
|
||||
| 01 | [Per-city Delta + Random-Point Fetch](./phase-01-per-city-delta-random-point-fetch.md) | 30m | pending |
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- HN, TPHCM, DL, DH all return panoramic image on first request (sample ≥ 3 attempts each).
|
||||
- No `MAX_BBOX_AREA` / `MIN_BBOX_AREA` / shrink/grow logic remains in `src/lib/mapillary.js`.
|
||||
- `npm run build` passes.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Token rotation or rate-limit handling
|
||||
- Caching successful responses
|
||||
- Adding more cities (DN remains disabled, future work)
|
||||
|
||||
## Manual cleanup after success
|
||||
|
||||
- Remove the diagnostic `console.error` of Mapillary URL/body once confirmed stable (or keep as observability).
|
||||
@@ -1,6 +1,6 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { cityNames, cityBboxes } from '../../../lib/game.js';
|
||||
import { cityNames, cityBboxes, cityDeltas } from '../../../lib/game.js';
|
||||
import { fetchMapillaryImages } from '../../../lib/mapillary.js';
|
||||
import { storeGameSession } from '../../../lib/session.js';
|
||||
|
||||
@@ -30,17 +30,24 @@ export async function GET(request) {
|
||||
try {
|
||||
console.log('Getting images for city:', cityName);
|
||||
|
||||
// Get city bbox
|
||||
// Get city bbox + per-city Mapillary delta (half-side of query window)
|
||||
const bbox = cityBboxes[cityCode];
|
||||
const delta = cityDeltas[cityCode];
|
||||
if (!bbox) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: `No bbox found for city: ${cityCode}`
|
||||
}, { status: 400 });
|
||||
}
|
||||
if (!delta) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: `No Mapillary delta configured for city: ${cityCode}`
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
// Fetch images from city bbox
|
||||
const imageResult = await fetchMapillaryImages(bbox);
|
||||
// Fetch images from city bbox via random-point dart-throw
|
||||
const imageResult = await fetchMapillaryImages(bbox, delta);
|
||||
|
||||
if (!imageResult.success) {
|
||||
return NextResponse.json({
|
||||
@@ -60,8 +67,8 @@ export async function GET(request) {
|
||||
lng: selectedImage.geometry.coordinates[0]
|
||||
};
|
||||
|
||||
// Use the thumbnail URL for now - Mapillary requires special handling for full panoramas
|
||||
const imageUrl = selectedImage.thumb_2048_url;
|
||||
// Use original-resolution panorama URL for PhotoSphere viewer
|
||||
const imageUrl = selectedImage.thumb_original_url;
|
||||
|
||||
// Create or update game session
|
||||
const currentSessionId = sessionId || generateSessionId();
|
||||
|
||||
@@ -7,6 +7,7 @@ export const CITIES = {
|
||||
name: 'Ha Noi',
|
||||
center: [21.0285, 105.8542],
|
||||
bbox: [105.77, 20.96, 105.88, 21.05],
|
||||
mapillaryDelta: 0.003,
|
||||
enabled: true
|
||||
},
|
||||
DN: {
|
||||
@@ -14,6 +15,7 @@ export const CITIES = {
|
||||
name: 'Da Nang',
|
||||
center: [16.0544, 108.2022],
|
||||
bbox: [108.17, 16, 108.25, 16.1],
|
||||
mapillaryDelta: 0.005,
|
||||
enabled: false
|
||||
},
|
||||
TPHCM: {
|
||||
@@ -21,6 +23,7 @@ export const CITIES = {
|
||||
name: 'Ho Chi Minh',
|
||||
center: [10.8231, 106.6297],
|
||||
bbox: [106.62, 10.71, 106.75, 10.83],
|
||||
mapillaryDelta: 0.005,
|
||||
enabled: true
|
||||
},
|
||||
DL: {
|
||||
@@ -28,6 +31,7 @@ export const CITIES = {
|
||||
name: 'Da Lat',
|
||||
center: [11.9404, 108.4583],
|
||||
bbox: [108.38, 11.89, 108.50, 12.00],
|
||||
mapillaryDelta: 0.005,
|
||||
enabled: true
|
||||
},
|
||||
DH: {
|
||||
@@ -35,6 +39,7 @@ export const CITIES = {
|
||||
name: 'Duc Hoa (Long An)',
|
||||
center: [10.8888, 106.3825],
|
||||
bbox: [106.35, 10.85, 106.45, 10.95],
|
||||
mapillaryDelta: 0.005,
|
||||
enabled: true
|
||||
}
|
||||
};
|
||||
@@ -53,6 +58,12 @@ export const cityBboxes = Object.fromEntries(
|
||||
Object.values(CITIES).map(city => [city.code, city.bbox])
|
||||
);
|
||||
|
||||
// Per-city Mapillary query bbox half-side (degrees). Smaller for dense urban
|
||||
// areas to keep Mapillary query cost under their 'reduce data' cap.
|
||||
export const cityDeltas = Object.fromEntries(
|
||||
Object.values(CITIES).map(city => [city.code, city.mapillaryDelta])
|
||||
);
|
||||
|
||||
// Cities list for UI components (only enabled cities)
|
||||
export const cities = Object.values(CITIES)
|
||||
.filter(city => city.enabled)
|
||||
|
||||
+52
-76
@@ -1,97 +1,73 @@
|
||||
// Mapillary API utilities
|
||||
//
|
||||
// Strategy ported from viguessr (https://github.com/luuvanduc1999/viguessr):
|
||||
// pick a random point inside the city bbox, query a small fixed-size sub-bbox
|
||||
// (side = 2*delta) centered on that point, and re-roll the point on empty
|
||||
// results or transient 5xx. The fixed small size keeps Mapillary query cost
|
||||
// below their cap; the dart-throw eventually lands on a pano-rich window.
|
||||
|
||||
// Bounding-box area range (square degrees) used by the adaptive query loop.
|
||||
// - Too big in dense urban regions → Mapillary returns 500 'reduce data'.
|
||||
// - Too small in sparse regions → no panoramic images returned.
|
||||
// Loop shrinks on 500 errors and grows on empty results to converge on a
|
||||
// workable size per city.
|
||||
const MIN_BBOX_AREA = 0.00005; // ~250m square
|
||||
const MAX_BBOX_AREA = 0.003; // ~5.5km square
|
||||
const MAX_RETRIES = 10;
|
||||
|
||||
// Pick a random sub-bbox of the requested area within the parent bbox.
|
||||
function getRandomSubBbox(bbox, area) {
|
||||
const [minLng, minLat, maxLng, maxLat] = bbox;
|
||||
const width = maxLng - minLng;
|
||||
const height = maxLat - minLat;
|
||||
|
||||
const side = Math.sqrt(area) * 0.9; // 10% safety margin
|
||||
const subWidth = Math.min(side, width);
|
||||
const subHeight = Math.min(side, height);
|
||||
|
||||
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 panoramic images from a single bbox query.
|
||||
// Returns { ok, images, retryable, error } so the loop can decide adaptive action.
|
||||
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&limit=1&bbox=${bboxString}&is_pano=true`;
|
||||
|
||||
const response = await fetch(apiUrl, {
|
||||
headers: { 'Accept': 'application/json' },
|
||||
next: { revalidate: 0 }
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const body = await response.text().catch(() => '<unreadable>');
|
||||
if (response.status === 401) {
|
||||
throw new Error('Mapillary authentication failed');
|
||||
}
|
||||
return { ok: false, retryable: true, error: `${response.status}: ${body.slice(0, 200)}` };
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return { ok: true, images: data.data || [] };
|
||||
}
|
||||
|
||||
const MAX_RETRIES = 15;
|
||||
const GROW_FACTOR = 1.6;
|
||||
const SHRINK_FACTOR = 0.5;
|
||||
|
||||
export async function fetchMapillaryImages(bbox) {
|
||||
export async function fetchMapillaryImages(bbox, delta) {
|
||||
const accessToken = process.env.MAPILLARY_ACCESS_TOKEN;
|
||||
if (!accessToken) {
|
||||
throw new Error('MAPILLARY_ACCESS_TOKEN environment variable is not set');
|
||||
}
|
||||
|
||||
// Start at the geometric mean of the size range — a reasonable middle ground.
|
||||
let area = Math.sqrt(MIN_BBOX_AREA * MAX_BBOX_AREA);
|
||||
const [minLng, minLat, maxLng, maxLat] = bbox;
|
||||
let lastError = null;
|
||||
|
||||
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
||||
const queryBbox = getRandomSubBbox(bbox, area);
|
||||
let result;
|
||||
const lat = Math.random() * (maxLat - minLat) + minLat;
|
||||
const lng = Math.random() * (maxLng - minLng) + minLng;
|
||||
const queryBbox = [
|
||||
(lng - delta).toFixed(4),
|
||||
(lat - delta).toFixed(4),
|
||||
(lng + delta).toFixed(4),
|
||||
(lat + delta).toFixed(4),
|
||||
].join(',');
|
||||
const apiUrl = `https://graph.mapillary.com/images?access_token=${accessToken}&fields=id,thumb_original_url,geometry,is_pano&limit=3&bbox=${queryBbox}&is_pano=true`;
|
||||
|
||||
try {
|
||||
result = await queryMapillary(accessToken, queryBbox);
|
||||
const response = await fetch(apiUrl, {
|
||||
headers: { Accept: 'application/json' },
|
||||
next: { revalidate: 0 },
|
||||
});
|
||||
|
||||
if (response.status === 401) {
|
||||
throw new Error('Mapillary authentication failed');
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const body = await response.text().catch(() => '<unreadable>');
|
||||
lastError = `${response.status}: ${body.slice(0, 200)}`;
|
||||
console.error(`Attempt ${attempt}/${MAX_RETRIES}: Mapillary ${response.status}; re-rolling`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const images = data.data || [];
|
||||
const panos = images.filter(img => img.is_pano);
|
||||
|
||||
if (panos.length > 0) {
|
||||
console.log(`Found ${panos.length} panos on attempt ${attempt}`);
|
||||
return { success: true, data: panos };
|
||||
}
|
||||
|
||||
console.log(`Attempt ${attempt}/${MAX_RETRIES}: no panos; re-rolling`);
|
||||
} catch (error) {
|
||||
// Auth failures are not transient — abort fast.
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (result.ok && result.images.length > 0) {
|
||||
console.log(`Found ${result.images.length} images on attempt ${attempt} (area=${area.toFixed(5)})`);
|
||||
return { success: true, data: result.images };
|
||||
}
|
||||
|
||||
if (!result.ok) {
|
||||
// 'reduce data' / 'unknown error' — shrink and retry.
|
||||
console.error(`Attempt ${attempt}/${MAX_RETRIES} 500 (area=${area.toFixed(5)}): ${result.error}`);
|
||||
lastError = result.error;
|
||||
area = Math.max(MIN_BBOX_AREA, area * SHRINK_FACTOR);
|
||||
} else {
|
||||
// OK but empty — grow and retry to widen the search area.
|
||||
console.log(`Attempt ${attempt}/${MAX_RETRIES} no panos (area=${area.toFixed(5)})`);
|
||||
area = Math.min(MAX_BBOX_AREA, area * GROW_FACTOR);
|
||||
if (error.message === 'Mapillary authentication failed') {
|
||||
throw error;
|
||||
}
|
||||
lastError = error.message;
|
||||
console.error(`Attempt ${attempt}/${MAX_RETRIES} threw: ${error.message}; re-rolling`);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: lastError
|
||||
? `No street view images found after ${MAX_RETRIES} attempts (last: ${lastError})`
|
||||
: 'No street view images found in city bbox',
|
||||
? `No panos after ${MAX_RETRIES} attempts (last: ${lastError})`
|
||||
: 'No panos found',
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user