fix(osm): stop using public Overpass by default (#648) (#742)

Traffic roads come from OpenFreeMap vector tiles, or TomTom flow tiles with a
key (Hybrid by default). Military areas use OpenFreeMap polygons with a bundled
worldwide name index; wide views show named points. ALPR reads an hourly
OpenStreetMap extract. Cockpit place context and country, state and US county
outlines use bundled Natural Earth and US Census data. Overpass is used only
when an operator configures their own endpoint.

Also: camera arrivals no longer land below ground, keyless terrain retries on
429, bounded relation geometry for configured Overpass, one shared
OpenStreetMap credit, and separate tile and API transports for embedders.

Co-authored-by: theGaryLarson <[email protected]>
Co-authored-by: Milan Khanal <[email protected]>
This commit is contained in:
authored and GitHub committed 2026-09-28 19:07:57 -05:00
1 parent 81eb44340d
commit f4fcf267c6
200 files changed
+26665 -1977

No files matched your search

+42 -92
View File
@@ -1,97 +1,47 @@
import { tilesForBounds } from '../../data/tomtomTiles.js';
import { isDrivableFlowSegment } from './roadModes.js';
import { createVectorTileSource } from '../../sources/vectorTiles.js';
import { clipTileLine } from '../../sources/openFreeMap.js';
import { tileToBBox } from '../../data/tomtomTiles.js';
import { decodeFlowTile } from './flowDecode.js';
/** Own one decoded flow cache and its session counters. */
/** Own one bounded decoded flow cache; every request still passes the server tile budget. */
export function createFlowTileSource({
fetchImpl = (...args) => globalThis.fetch(...args),
} = {}) {
const DECODE_CACHE_TTL_MS = 120_000;
/** @const {number} Max decoded tiles kept in memory before oldest-entry eviction. */
const DECODE_CACHE_MAX_ENTRIES = 64;
/**
* Decoded-tile cache keyed by "z/x/y".
* @type {Map<string, {at:number, segments:Array}>}
*/
const _decodeCache = new Map();
/** @type {number} Session count of tile requests issued to the proxy (decode-cache misses). */
let _tilesFetched = 0;
/** Insert into the decode cache with oldest-entry eviction. */
function cacheSet(key, entry) {
if (
!_decodeCache.has(key) &&
_decodeCache.size >= DECODE_CACHE_MAX_ENTRIES
) {
const oldest = _decodeCache.keys().next().value;
_decodeCache.delete(oldest);
}
_decodeCache.set(key, entry);
}
/**
* Fetch + decode all flow tiles covering the given bounds.
*
* Tiles are fetched from the local proxy in parallel; each decoded tile is
* cached in memory for 120 s (keyed z/x/y), so repeat calls for the same
* viewport are free. Partial tile failures return the segments that DID
* decode (last-good philosophy); the promise rejects only when every tile
* failed (e.g. keyless 503, aborted signal, proxy down).
*
* @param {{south:number, west:number, north:number, east:number}} bounds - Degrees.
* @param {Object} [opts]
* @param {AbortSignal} [opts.signal] - Abort signal (camera moved / layer disabled).
* @param {number} [opts.zoom=12] - Flow tile zoom level.
* @returns {Promise<Array<{coords:number[][], trafficLevel:number, roadType:string, closure:boolean}>>}
* Flat array of flow segments across all covering tiles.
*/
async function fetchFlowForBounds(bounds, { signal, zoom = 12 } = {}) {
signal?.throwIfAborted();
const tiles = tilesForBounds(bounds, zoom);
if (tiles.length === 0) return [];
const now = Date.now();
const results = await Promise.allSettled(
tiles.map(async ({ z, x, y }) => {
const key = `${z}/${x}/${y}`;
const cached = _decodeCache.get(key);
if (cached && now - cached.at < DECODE_CACHE_TTL_MS)
return cached.segments;
_tilesFetched += 1;
const res = await fetchImpl(`/api/tomtom/flow/${z}/${x}/${y}.pbf`, {
signal,
});
if (!res.ok) throw new Error(`flow tile ${key}: HTTP ${res.status}`);
const segments = decodeFlowTile(await res.arrayBuffer(), z, x, y);
signal?.throwIfAborted();
cacheSet(key, { at: Date.now(), segments });
return segments;
}),
);
signal?.throwIfAborted();
const fulfilled = results.filter((r) => r.status === 'fulfilled');
if (fulfilled.length === 0) {
throw results[0].reason instanceof Error
? results[0].reason
: new Error('flow fetch failed');
}
return fulfilled.flatMap((r) => r.value);
}
/**
* Session diagnostics for `getStats()` surfaces.
* @returns {{tilesFetched:number}} Count of tile requests issued to the proxy
* this session (decode-cache hits excluded).
*/
function getFlowSessionStats() {
return { tilesFetched: _tilesFetched };
}
/** Clear the decode cache (tests + layer teardown). Session stats persist. */
function resetFlowTileCache() {
_decodeCache.clear();
}
return { fetchFlowForBounds, getFlowSessionStats, resetFlowTileCache };
const tiles = createVectorTileSource({
template: '/api/tomtom/flow/{z}/{x}/{y}.pbf',
allowedOrigin: 'http://localhost',
decode: (bytes, z, x, y) =>
decodeFlowTile(bytes, z, x, y, { strict: true })
.filter(isDrivableFlowSegment)
.flatMap((segment) =>
clipTileLine(segment.coords, tileToBBox(z, x, y)).map((coords) => ({
...segment,
coords,
})),
),
fetchImpl,
ttlMs: 120_000,
maxTiles: 16,
});
let partial = false;
return {
async fetchFlowForBounds(bounds, { signal, zoom = 12 } = {}) {
const result = await tiles.fetchBounds(bounds, {
zoom,
signal,
tiles: bounds.coverage?.coarse,
});
partial = result.partial;
if (bounds.coverage) return result.tiles.flat();
return result.tiles.flat().flatMap((segment) =>
clipTileLine(segment.coords, bounds).map((coords) => ({
...segment,
coords,
})),
);
},
getFlowSessionStats: () => ({ ...tiles.getStats(), partial }),
resetFlowTileCache: () => tiles.clear(),
};
}