Files
gods-eye-view/server/providers/mapillary/routes.js
T
Said TurkseverandClaude Opus 5.5 63280e22c0 fix(street-level): load the coverage in view, and name a rejected token
Tilted views: the visible box came from screen rays against the bare
ellipsoid and the 9 tiles kept were the ones nearest its centre, so a view
over high ground (Denver, 1,600 m up) or near the horizon dropped the
streets in front of the camera. At street zooms the rays now meet an
ellipsoid raised to the ground under the camera, hits beyond a range tied
to camera height are ignored, tilted views also keep the ground around the
camera, and tiles are ranked along the line of sight (nadir to screen
centre). Simulated with real Cesium ray maths over Sacramento and Denver
from 2 m to 1,000 m: the nearest visible ground and the screen centre are
always loaded.

Rejected token: Mapillary's 401/403 reached the panel as a raw "Mapillary
tiles HTTP 403" while the pill said ON, and every pan asked Mapillary again.
The proxy now answers a refusal as {keyRejected: true} and holds it for that
token (5 min), and a 429 with its Retry-After (held until then). The panel
reads KEY REJECTED, names MAPILLARY_CLIENT_TOKEN and Provider Settings,
gates its controls like KEY REQUIRED, requests nothing more, and clears the
message when the layer goes off. A rate limit keeps the drawn tiles and
refreshes once the wait is over.

Found in the IC8 review of #768 (P1 tile selection, P2 rejected token).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-05 15:12:03 +01:00

77 lines
2.9 KiB
JavaScript

import { fetchTile, TileRequestError, TileUpstreamError } from './tiles.js';
import { mapillaryToken } from './constants.js';
function sendJson(res, status, payload) {
res.statusCode = status;
res.setHeader('Content-Type', 'application/json; charset=utf-8');
res.setHeader('Cache-Control', 'no-store');
res.end(JSON.stringify(payload));
}
/** GET /api/mapillary/status — whether a token is configured, never its value. */
function handleStatus(req, res) {
if (req.method !== 'GET')
return sendJson(res, 405, { error: 'Method not allowed' });
sendJson(res, 200, { configured: Boolean(mapillaryToken()) });
}
/** GET /api/mapillary/tiles/coverage/{z}/{x}/{y} — cached protobuf tile. */
async function handleTile(req, res) {
if (req.method !== 'GET')
return sendJson(res, 405, { error: 'Method not allowed' });
const match = /^\/(coverage)\/(\d{1,2})\/(\d{1,6})\/(\d{1,6})$/.exec(
(req.url || '').split('?')[0],
);
if (!match)
return sendJson(res, 400, {
error: 'Tile path must be /coverage/{z}/{x}/{y}',
});
if (!mapillaryToken())
return sendJson(res, 503, { error: 'no_key', keyRequired: true });
const abandoned = new AbortController();
req.on?.('aborted', () => abandoned.abort());
res.on?.('close', () => abandoned.abort());
try {
const { bytes, source } = await fetchTile(
{ layer: match[1], z: match[2], x: match[3], y: match[4] },
{ signal: abandoned.signal },
);
if (res.writableEnded) return;
res.statusCode = bytes.length ? 200 : 204;
res.setHeader('Content-Type', 'application/x-protobuf');
res.setHeader('Cache-Control', 'public, max-age=3600');
res.setHeader('X-Gev-Cache', source);
res.end(bytes.length ? bytes : undefined);
} catch (error) {
if (res.writableEnded || abandoned.signal.aborted) return;
if (error instanceof TileRequestError)
return sendJson(res, error.status, { error: error.message });
if (error instanceof TileUpstreamError) {
// A rejected token is a key problem the panel can name, not a fault.
if (error.keyRejected)
return sendJson(res, 403, {
error: 'Mapillary rejected the access token',
keyRejected: true,
});
if (error.status === 429) {
const retryAfter = error.retryAfterSec || 60;
res.setHeader('Retry-After', String(retryAfter));
return sendJson(res, 429, {
error: 'Mapillary is rate-limiting tile requests',
retryAfter,
});
}
return sendJson(res, error.status >= 500 ? 502 : error.status, {
error: error.message,
});
}
sendJson(res, 502, { error: error?.message || 'Tile fetch failed' });
}
}
/** Attach every Mapillary route to a connect-style middleware stack. */
export function installMapillaryRoutes(middlewares) {
middlewares.use('/api/mapillary/status', handleStatus);
middlewares.use('/api/mapillary/tiles', handleTile);
}