mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
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]>
This commit is contained in:
1 parent
0d41b6be54
commit
fa5710bcf2
46 files changed
+9935
-32
No files matched your search
@@ -0,0 +1,99 @@
|
||||
import path from 'node:path';
|
||||
import { promises as fsp } from 'node:fs';
|
||||
import {
|
||||
SPRITE_DISK_DIR,
|
||||
SPRITE_DISK_TTL_MS,
|
||||
SPRITE_MAX_BYTES,
|
||||
SPRITE_SOURCE_ROOT,
|
||||
} from './constants.js';
|
||||
import { isSignValue } from '../../../src/layers/mapillary/values.js';
|
||||
|
||||
/** A taxonomy value: lower-case segments joined by `--`, words by `-`. */
|
||||
const VALUE_GRAMMAR =
|
||||
/^[a-z0-9]+(?:-[a-z0-9]+)*(?:--[a-z0-9]+(?:-[a-z0-9]+)*)*$/;
|
||||
|
||||
/** @type {Map<string, Promise<Buffer|null>>} */
|
||||
const _inFlight = new Map();
|
||||
|
||||
function spritePath(value) {
|
||||
return path.join(SPRITE_DISK_DIR, `${value}.svg`);
|
||||
}
|
||||
|
||||
async function readDisk(value) {
|
||||
try {
|
||||
const file = spritePath(value);
|
||||
const stat = await fsp.stat(file);
|
||||
if (Date.now() - stat.mtimeMs > SPRITE_DISK_TTL_MS) return null;
|
||||
return await fsp.readFile(file);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchSprite(value) {
|
||||
const pkg = isSignValue(value) ? 'package_signs' : 'package_objects';
|
||||
const response = await fetch(`${SPRITE_SOURCE_ROOT}/${pkg}/${value}.svg`, {
|
||||
signal: AbortSignal.timeout(20_000),
|
||||
});
|
||||
if (response.status === 404) {
|
||||
await response.body?.cancel().catch(() => {});
|
||||
return null;
|
||||
}
|
||||
if (!response.ok) {
|
||||
await response.body?.cancel().catch(() => {});
|
||||
throw new Error(`sprite source HTTP ${response.status}`);
|
||||
}
|
||||
const bytes = Buffer.from(await response.arrayBuffer());
|
||||
if (
|
||||
bytes.length > SPRITE_MAX_BYTES ||
|
||||
!bytes.subarray(0, 512).toString().includes('<svg')
|
||||
)
|
||||
return null;
|
||||
const file = spritePath(value);
|
||||
fsp
|
||||
.mkdir(path.dirname(file), { recursive: true })
|
||||
.then(() => fsp.writeFile(file, bytes))
|
||||
.catch(() => {});
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/mapillary/sprite/<value>.svg — the Mapillary icon for a taxonomy
|
||||
* value, fetched once from the MIT sprite source and cached on disk.
|
||||
*/
|
||||
export async function handleSprite(req, res) {
|
||||
const match = /^\/([a-z0-9-]+)\.svg$/.exec((req.url || '').split('?')[0]);
|
||||
const value = match?.[1];
|
||||
if (!value || !VALUE_GRAMMAR.test(value)) {
|
||||
res.statusCode = 400;
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.end(JSON.stringify({ error: 'Invalid sprite value' }));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
let bytes = await readDisk(value);
|
||||
if (!bytes) {
|
||||
let pending = _inFlight.get(value);
|
||||
if (!pending) {
|
||||
pending = fetchSprite(value).finally(() => _inFlight.delete(value));
|
||||
_inFlight.set(value, pending);
|
||||
}
|
||||
bytes = await pending;
|
||||
}
|
||||
if (!bytes) {
|
||||
res.statusCode = 404;
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.setHeader('Cache-Control', 'public, max-age=3600');
|
||||
res.end(JSON.stringify({ error: 'No sprite for value' }));
|
||||
return;
|
||||
}
|
||||
res.statusCode = 200;
|
||||
res.setHeader('Content-Type', 'image/svg+xml; charset=utf-8');
|
||||
res.setHeader('Cache-Control', 'public, max-age=86400');
|
||||
res.end(bytes);
|
||||
} catch (error) {
|
||||
res.statusCode = 502;
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.end(JSON.stringify({ error: error?.message || 'Sprite unavailable' }));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user