Files
gods-eye-view/server/providers/mapillary/routes.js
T
Said TurkseverandClaude Fable 5.1 fa5710bcf2 feat(mapillary): street-level layer core — coverage, sequences, viewer bridge, query engine
Camera-driven Mapillary coverage (z0–5 overview points from orbit, z11–14
ground-clamped sequence lines below 60 km), per-sequence image cones, an
embedded MapillaryJS viewer bridge with a camera-follow mode, sprite icons
for map features, and a query engine that turns a plan into a streamed
z14 tile fan-out. Registers an on-globe CC BY-SA 4.0 credit while enabled.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-09-23 15:44:32 +01:00

80 lines
2.9 KiB
JavaScript

import { fetchTile, TileRequestError, TileUpstreamError } from './tiles.js';
import { handleFeatureQuery } from './features.js';
import { handlePlan } from './planner.js';
import { handleSprite } from './sprites.js';
import {
anthropicConfigured,
mapillaryToken,
plannerModel,
QUERY_MAX_TILES,
FEATURE_TILE_ZOOM,
} 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 — what this install can do, never a value. */
function handleStatus(req, res) {
if (req.method !== 'GET')
return sendJson(res, 405, { error: 'Method not allowed' });
sendJson(res, 200, {
configured: Boolean(mapillaryToken()),
planner: anthropicConfigured(),
plannerModel: anthropicConfigured() ? plannerModel() : null,
limits: { maxTiles: QUERY_MAX_TILES, featureZoom: FEATURE_TILE_ZOOM },
});
}
/** GET /api/mapillary/tiles/{layer}/{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|points|signs)\/(\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 /{layer}/{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)
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);
middlewares.use('/api/mapillary/features', handleFeatureQuery);
middlewares.use('/api/mapillary/plan', handlePlan);
middlewares.use('/api/mapillary/sprite', handleSprite);
}