refactor(alpr): accept normalized camera snapshots and source attribution

This commit is contained in:
Sameh Khamis committed 2026-09-13 17:33:48 -07:00
1 parent 4374c0fa60
commit 0feaab0a72
9 files changed
+307 -318

No files matched your search

+21 -3
View File
@@ -891,7 +891,15 @@ test('two factories keep their requests, records and destruction independent', a
const second = createAlprCamerasLayer({
services,
source: {
fetch: async () => ({ elements: [cameraNode(99)], stale: false }),
label: 'Test camera directory',
attribution: { name: 'Test directory' },
fetch: async () => ({
records: [
{ id: 'directory:99', latitude: 30.2672, longitude: -97.7431 },
],
stale: false,
saturated: false,
}),
},
});
const a = cameraHarness(first),
@@ -901,11 +909,21 @@ test('two factories keep their requests, records and destruction independent', a
await second.update();
first.destroy();
assert.equal(firstSignal.aborted, true);
resolveFirst({ elements: [cameraNode(42)], stale: false });
resolveFirst({
records: [normalizeAlprNode(cameraNode(42))],
stale: false,
saturated: false,
});
await pending;
assert.equal(first.getStats().count, 0);
assert.equal(second.getStats().count, 1);
assert.equal(b.source.entities.values[0].id, 'alpr:99');
assert.equal(b.source.entities.values[0].id, 'directory:99');
assert.equal(second.source, 'Test camera directory');
assert.ok(
b.source.entities.values[0].gevLabelModel.details.includes(
'Source: Test directory',
),
);
assert.equal(a.viewer.camera.moveEnd.numberOfListeners, 0);
assert.equal(a.viewer.scene.postRender.numberOfListeners, 0);
} finally {
+21 -25
View File
@@ -12,16 +12,24 @@ import {
boxContains,
destinationPointDeg,
alprRetryDelayMs,
isAlprSurveillanceType,
normalizeAlprNode,
buildOverpassQuery,
validateAlprSnapshot,
alprCreditMarkup,
} from './model.js';
import { createAlprPresentation } from './presentation.js';
/** Own one layer's requests, timers, records, and viewer subscriptions. */
/**
* Own one layer's requests, records, display and viewer subscriptions.
* A source fetches a bounded box with an AbortSignal and resolves
* { records, stale, saturated }. Records use stable string ids and latitude /
* longitude in degrees; optional directionDeg is a compass bearing [0, 360).
* Manufacturer, operator and cameraType are optional display fields.
* Source label and attribution { name, description, text, href } identify the
* actual provider; href must be HTTPS. Provider payload parsing stays in source.
*/
export function createAlprCamerasLayer({ source, services } = {}) {
if (typeof source?.fetch !== 'function')
throw new TypeError('ALPR requires a camera source');
const creditMarkup = alprCreditMarkup(source.attribution);
const { governorRequestRender } = services.render;
const { registerPickOwner, unregisterPickOwner } = services.picking;
@@ -67,7 +75,7 @@ export function createAlprCamerasLayer({ source, services } = {}) {
clearSelection,
updateSelectedAnchor,
installInteraction,
} = createAlprPresentation({ state, services });
} = createAlprPresentation({ state, services, source });
function setAlprStatus(status, error = null) {
if (state.status === status && state.error === error) return;
@@ -156,24 +164,14 @@ export function createAlprCamerasLayer({ source, services } = {}) {
setAlprStatus('loading');
renderRecords();
try {
const { elements, stale } = await source.fetch(
queryBox,
requestAbort.signal,
);
const snapshot = await source.fetch(queryBox, requestAbort.signal);
if (
requestAbort.signal.aborted ||
state.abort !== requestAbort ||
!state.enabled
)
return;
const records = [
...new Map(
elements
.map(normalizeAlprNode)
.filter(Boolean)
.map((record) => [record.id, record]),
).values(),
];
const { records, stale, saturated } = validateAlprSnapshot(snapshot);
state.records = records;
state.recordById = new Map(records.map((r) => [r.id, r]));
state.lastUpdate = Date.now();
@@ -181,8 +179,7 @@ export function createAlprCamerasLayer({ source, services } = {}) {
// Saturated when the QUERY truncated OR the render cap would hide cameras:
// either way the view holds more than the screen shows, and the user must
// be told rather than left with a count that disagrees with the map.
state.saturated =
elements.length >= QUERY_LIMIT || records.length > MAX_RENDERED;
state.saturated = saturated || records.length > MAX_RENDERED;
state.lastQueryBox = queryBox;
clearUnavailableRetry();
// Stale and saturated are independent facts; a cached response that also
@@ -206,7 +203,7 @@ export function createAlprCamerasLayer({ source, services } = {}) {
state.stale = Boolean(state.lastUpdate);
setAlprStatus(
'unavailable',
error?.message || 'Overpass temporarily unavailable',
error?.message || 'Camera source temporarily unavailable',
);
scheduleUnavailableRetry();
} finally {
@@ -223,17 +220,16 @@ export function createAlprCamerasLayer({ source, services } = {}) {
id: LAYER_ID,
name: 'ALPR Cameras',
icon: '📷',
source: 'OpenStreetMap · community mapped',
source: source.label || 'Mapped camera locations',
updateInterval: 0,
statsRefreshInterval: 1000,
init(viewer) {
if (state.viewer) throw new Error('ALPR layer is already initialized');
state.viewer = viewer;
state.dataSource = new Cesium.CustomDataSource('alpr-cameras');
state.credit = new Cesium.Credit(
'<span class="gev-alpr-credit">ALPR: <a href="https://www.openstreetmap.org/copyright" target="_blank" rel="noopener">© OpenStreetMap</a></span>',
true,
);
state.credit = creditMarkup
? new Cesium.Credit(creditMarkup, true)
: null;
viewer.dataSources.add(state.dataSource);
state.moveEndRemove =
viewer.camera.moveEnd.addEventListener(scheduleLoad);
+54
View File
@@ -155,3 +155,57 @@ export function buildOverpassQuery(south, west, north, east) {
`(${south},${west},${north},${east});out body ${QUERY_LIMIT};`
);
}
/** Validate a source-independent snapshot before replacing the accepted display. */
export function validateAlprSnapshot(snapshot) {
if (
!Array.isArray(snapshot?.records) ||
typeof snapshot.stale !== 'boolean' ||
typeof snapshot.saturated !== 'boolean'
) {
throw new TypeError('Camera source returned an invalid snapshot');
}
const ids = new Set();
for (const record of snapshot.records) {
if (
!record ||
typeof record.id !== 'string' ||
!record.id.trim() ||
ids.has(record.id) ||
!Number.isFinite(record.latitude) ||
Math.abs(record.latitude) > 90 ||
!Number.isFinite(record.longitude) ||
Math.abs(record.longitude) > 180 ||
(record.directionDeg != null &&
(!Number.isFinite(record.directionDeg) ||
record.directionDeg < 0 ||
record.directionDeg >= 360))
) {
throw new TypeError('Camera source returned an invalid record');
}
ids.add(record.id);
}
return snapshot;
}
/** Build a linked attribution from plain text and an HTTPS URL only. */
export function alprCreditMarkup(attribution) {
if (!attribution?.text || !attribution?.href) return null;
const href = new URL(attribution.href);
if (href.protocol !== 'https:' || href.username || href.password) {
throw new TypeError('Camera attribution requires a public HTTPS link');
}
const escape = (value) =>
String(value).replace(
/[&<>"']/g,
(char) =>
({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;',
})[char],
);
return `<span class="gev-alpr-credit">ALPR: <a href="${escape(href.href)}" target="_blank" rel="noopener">${escape(attribution.text)}</a></span>`;
}
+5 -4
View File
@@ -9,7 +9,7 @@ import {
} from './policy.js';
import { boxContains, destinationPointDeg } from './model.js';
export function createAlprPresentation({ state, services }) {
export function createAlprPresentation({ state, services, source }) {
const { governorRequestRender } = services.render;
const {
clearSelectedEntityContextForLayer,
@@ -130,7 +130,7 @@ export function createAlprPresentation({ state, services }) {
}
const entity = state.dataSource.entities.add(entityDef);
entity.gevTrackedId = record.id;
// The raw OSM datum has no elevation; it is not the clamped marker's
// The mapped camera datum has no elevation; it is not the clamped marker's
// visual anchor. Only the selected marker samples the rendered surface,
// at most once per second, through Cesium's public height APIs.
entity.gevAlprDisplayPosition = null;
@@ -141,7 +141,7 @@ export function createAlprPresentation({ state, services }) {
record.manufacturer ? `Manufacturer: ${record.manufacturer}` : null,
record.operator ? `Operator: ${record.operator}` : null,
record.cameraType ? `Type: ${record.cameraType}` : null,
'Source: OpenStreetMap',
`Source: ${source.attribution?.name || source.label || 'Camera source'}`,
].filter(Boolean),
accent: color.toCssColorString(),
};
@@ -150,7 +150,8 @@ export function createAlprPresentation({ state, services }) {
layerId: LAYER_ID,
dataSource: state.dataSource,
layerName: 'ALPR Cameras',
source: 'OpenStreetMap contributors (ODbL 1.0; community mapped)',
source:
source.attribution?.description || source.label || 'Camera source',
label: 'ALPR camera',
latitude: record.latitude,
longitude: record.longitude,
+30 -3
View File
@@ -2,8 +2,9 @@ import {
OVERPASS_URL,
MAX_VIEWPORT_DEGREES,
QUERY_SNAP_DEGREES,
QUERY_LIMIT,
} from './policy.js';
import { buildOverpassQuery } from './model.js';
import { buildOverpassQuery, normalizeAlprNode } from './model.js';
/** Construct the bounded OSM request adapter without starting a request. */
export function createOverpassAlprSource({
fetchImpl = (...args) => globalThis.fetch(...args),
@@ -33,6 +34,11 @@ export function createOverpassAlprSource({
signal,
});
if (!response.ok) {
try {
await response.body?.cancel();
} catch {
/* already closed */
}
const message =
response.status === 429
? 'Overpass rate-limited'
@@ -49,7 +55,28 @@ export function createOverpassAlprSource({
if (!Array.isArray(payload?.elements) || payload.remark) {
throw new Error('Overpass returned an incomplete camera response');
}
return { elements: payload.elements, stale };
return {
records: [
...new Map(
payload.elements
.slice(0, QUERY_LIMIT)
.map(normalizeAlprNode)
.filter(Boolean)
.map((record) => [record.id, record]),
).values(),
],
stale,
saturated: payload.elements.length >= QUERY_LIMIT,
};
}
return { fetch: fetchAlprNodes };
return {
fetch: fetchAlprNodes,
label: 'OpenStreetMap · community mapped',
attribution: {
name: 'OpenStreetMap',
description: 'OpenStreetMap contributors (ODbL 1.0; community mapped)',
text: '© OpenStreetMap',
href: 'https://www.openstreetmap.org/copyright',
},
};
}
+81
View File
@@ -1,6 +1,7 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { createOverpassAlprSource } from './source.js';
import { validateAlprSnapshot, alprCreditMarkup } from './model.js';
const box = { south: 30, west: -98, north: 30.1, east: -97.9 };
test('the source rejects invalid and unbounded queries before fetching', async () => {
let calls = 0;
@@ -34,3 +35,83 @@ test('cancellation during body parsing rejects even when the transport ignores i
});
await assert.rejects(source.fetch(box, abort.signal), { name: 'AbortError' });
});
test('Overpass normalization returns camera records and preserves limited/stale coverage', async () => {
const source = createOverpassAlprSource({
fetchImpl: async () => ({
ok: true,
headers: new Headers({ 'x-overpass-cache': 'STALE' }),
json: async () => ({
elements: [
{
type: 'node',
id: 12,
lat: 30,
lon: -98,
tags: {
'surveillance:type': 'camera;ALPR',
'camera:direction': '90',
},
},
],
}),
}),
});
const snapshot = await source.fetch(box);
assert.equal(snapshot.records[0].id, 'alpr:12');
assert.equal(snapshot.records[0].latitude, 30);
assert.equal(snapshot.records[0].directionDeg, 90);
assert.equal(snapshot.stale, true);
assert.equal(snapshot.saturated, false);
assert.equal('elements' in snapshot, false);
});
test('an unsuccessful source response releases its body before reporting an error', async () => {
let cancelled = 0;
const source = createOverpassAlprSource({
fetchImpl: async () => ({
ok: false,
status: 429,
body: {
async cancel() {
cancelled++;
},
},
}),
});
await assert.rejects(source.fetch(box), /rate-limited/);
assert.equal(cancelled, 1);
});
test('source snapshots reject malformed coordinates and duplicate identities', () => {
const record = { id: 'camera:1', latitude: 30, longitude: -98 };
for (const records of [
[{ ...record, latitude: NaN }],
[record, record],
[{ ...record, id: '' }],
]) {
assert.throws(
() => validateAlprSnapshot({ records, stale: false, saturated: false }),
/invalid record/,
);
}
assert.throws(
() => validateAlprSnapshot({ elements: [] }),
/invalid snapshot/,
);
});
test('provider attribution escapes markup and rejects executable links', () => {
assert.equal(alprCreditMarkup(null), null);
assert.throws(
() => alprCreditMarkup({ text: 'test', href: 'javascript:alert(1)' }),
/HTTPS/,
);
const html = alprCreditMarkup({
text: '<img onerror=alert(1)>',
href: 'https://example.org/?a=1&b=2',
});
assert.ok(html.includes('&lt;img onerror=alert(1)&gt;'));
assert.ok(html.includes('?a=1&amp;b=2'));
assert.ok(!html.includes('<img'));
});