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

+3 -1
View File
@@ -4,7 +4,9 @@ The optional **ALPR Cameras** layer shows community-mapped OpenStreetMap locatio
not camera footage or plate records. City-scale queries use the existing Overpass
provider, with capped results, retry, cached-data and incomplete-coverage notices.
Its `./layers/alpr` entry exposes a per-instance layer factory and bounded source
adapter. The standalone registration supplies selection, picking, terrain and
adapter. Sources return normalized records, stale/coverage flags and their own
attribution; rendering does not parse Overpass payloads. The standalone
registration supplies selection, picking, terrain and
render services. Selected cards and Data attribution identify OpenStreetMap.
Layer state and the existing voice layer tools include `alpr-cameras`.
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "gods-eye-view",
"private": true,
"version": "0.1.1",
"description": "A real-time intelligence console for planet Earth — photorealistic 3D globe, live aircraft/ships/satellites/earthquakes/CCTV, and hands-free voice control. Runs in a browser.",
"description": "A real-time intelligence console for planet Earth \u2014 photorealistic 3D globe, live aircraft/ships/satellites/earthquakes/CCTV, and hands-free voice control. Runs in a browser.",
"type": "module",
"license": "MIT",
"author": "Bilawal Sidhu",
+91 -281
View File
@@ -10,47 +10,27 @@
"src/data/localGeojsonCore.js",
"src/data/localGeojsonLod.js"
],
"external": [
"cesium"
]
"external": ["cesium"]
},
"application": {
"exports": [
"./application"
],
"modules": [
"src/app/application.js"
],
"exports": ["./application"],
"modules": ["src/app/application.js"],
"external": []
},
"viewer": {
"exports": [
"./application/viewer"
],
"modules": [
"src/app/viewer.js"
],
"external": [
"cesium"
]
"exports": ["./application/viewer"],
"modules": ["src/app/viewer.js"],
"external": ["cesium"]
},
"vite-build": {
"runtime": "node",
"exports": [
"./build/vite"
],
"modules": [
"build/vite.js"
],
"external": [
"vite-plugin-cesium"
]
"exports": ["./build/vite"],
"modules": ["build/vite.js"],
"external": ["vite-plugin-cesium"]
},
"live-providers": {
"runtime": "node",
"exports": [
"./server/providers/live"
],
"exports": ["./server/providers/live"],
"modules": [
"server/providers/live.js",
"server/providers/common/http.js",
@@ -65,24 +45,16 @@
"src/data/aisStreamAdapter.js",
"src/data/aisWatchdog.js"
],
"external": [
"ws"
]
"external": ["ws"]
},
"adsb-lol-source": {
"exports": [
"./sources/adsb-lol"
],
"modules": [
"src/data/adsbLolFallback.js"
],
"exports": ["./sources/adsb-lol"],
"modules": ["src/data/adsbLolFallback.js"],
"external": []
},
"place-providers": {
"runtime": "node",
"exports": [
"./server/providers/places"
],
"exports": ["./server/providers/places"],
"modules": [
"server/providers/places.js",
"server/providers/places/google.js",
@@ -97,19 +69,13 @@
"external": []
},
"place-payloads": {
"exports": [
"./sources/places"
],
"modules": [
"src/data/placeProviderPayloads.js"
],
"exports": ["./sources/places"],
"modules": ["src/data/placeProviderPayloads.js"],
"external": []
},
"space-providers": {
"runtime": "node",
"exports": [
"./server/providers/space"
],
"exports": ["./server/providers/space"],
"modules": [
"server/providers/space.js",
"server/providers/space/celestrak.js",
@@ -120,19 +86,13 @@
"external": []
},
"space-requests": {
"exports": [
"./sources/space"
],
"modules": [
"src/data/spaceProviderRequests.js"
],
"exports": ["./sources/space"],
"modules": ["src/data/spaceProviderRequests.js"],
"external": []
},
"terrain-provider": {
"runtime": "node",
"exports": [
"./server/providers/terrain"
],
"exports": ["./server/providers/terrain"],
"modules": [
"server/providers/terrain.js",
"src/data/terrainHeightsProxy.js"
@@ -140,50 +100,30 @@
"external": []
},
"terrain-source": {
"exports": [
"./sources/terrain"
],
"modules": [
"src/data/terrainHeightsProxy.js"
],
"exports": ["./sources/terrain"],
"modules": ["src/data/terrainHeightsProxy.js"],
"external": []
},
"traffic-provider": {
"runtime": "node",
"exports": [
"./server/providers/traffic"
],
"modules": [
"server/providers/traffic.js",
"src/data/tomtomTiles.js"
],
"exports": ["./server/providers/traffic"],
"modules": ["server/providers/traffic.js", "src/data/tomtomTiles.js"],
"external": []
},
"traffic-source": {
"exports": [
"./sources/traffic"
],
"modules": [
"src/data/tomtomTiles.js"
],
"exports": ["./sources/traffic"],
"modules": ["src/data/tomtomTiles.js"],
"external": []
},
"firms-provider": {
"runtime": "node",
"exports": [
"./server/providers/firms"
],
"modules": [
"server/providers/firms.js",
"src/data/firmsCsv.js"
],
"exports": ["./server/providers/firms"],
"modules": ["server/providers/firms.js", "src/data/firmsCsv.js"],
"external": []
},
"gbfs-provider": {
"runtime": "node",
"exports": [
"./server/providers/gbfs"
],
"exports": ["./server/providers/gbfs"],
"modules": [
"server/providers/gbfs.js",
"server/providers/common/http.js",
@@ -192,19 +132,13 @@
"external": []
},
"gbfs-source": {
"exports": [
"./sources/gbfs"
],
"modules": [
"src/data/gbfsSource.js"
],
"exports": ["./sources/gbfs"],
"modules": ["src/data/gbfsSource.js"],
"external": []
},
"cctv-provider": {
"runtime": "node",
"exports": [
"./server/providers/cctv"
],
"exports": ["./server/providers/cctv"],
"modules": [
"server/providers/cctv.js",
"server/providers/cctv/catalog.js",
@@ -221,9 +155,7 @@
},
"radio-provider": {
"runtime": "node",
"exports": [
"./server/providers/radio"
],
"exports": ["./server/providers/radio"],
"modules": [
"server/providers/radio.js",
"server/providers/radio/catalog.js",
@@ -237,9 +169,7 @@
},
"overpass-provider": {
"runtime": "node",
"exports": [
"./server/providers/overpass"
],
"exports": ["./server/providers/overpass"],
"modules": [
"server/providers/common/geo.js",
"server/providers/common/http.js",
@@ -258,9 +188,7 @@
},
"military-installations-provider": {
"runtime": "node",
"exports": [
"./server/providers/military-installations"
],
"exports": ["./server/providers/military-installations"],
"modules": [
"server/providers/common/http.js",
"server/providers/common/query.js",
@@ -277,9 +205,7 @@
},
"regional-provider": {
"runtime": "node",
"exports": [
"./server/providers/regional"
],
"exports": ["./server/providers/regional"],
"modules": [
"server/providers/common/http.js",
"server/providers/common/query.js",
@@ -298,9 +224,7 @@
},
"openai-provider": {
"runtime": "node",
"exports": [
"./server/providers/openai"
],
"exports": ["./server/providers/openai"],
"modules": [
"server/providers/common/rate-limit.js",
"server/providers/common/request.js",
@@ -320,9 +244,7 @@
},
"standalone-key-setup": {
"runtime": "node",
"exports": [
"./server/standalone/key-setup"
],
"exports": ["./server/standalone/key-setup"],
"modules": [
"scripts/pinokio-environment.mjs",
"server/providers/common/source-root.js",
@@ -333,9 +255,7 @@
"external": []
},
"place-search": {
"exports": [
"./search"
],
"exports": ["./search"],
"modules": [
"src/search/index.js",
"src/search/placeSearch.js",
@@ -345,27 +265,17 @@
"external": []
},
"panel-disclosure": {
"exports": [
"./ui/panels"
],
"modules": [
"src/ui/panelDisclosure.js"
],
"exports": ["./ui/panels"],
"modules": ["src/ui/panelDisclosure.js"],
"external": []
},
"surface-keyboard": {
"exports": [
"./ui/surfaces"
],
"modules": [
"src/ui/surfaceKeyboard.js"
],
"exports": ["./ui/surfaces"],
"modules": ["src/ui/surfaceKeyboard.js"],
"external": []
},
"panel-rail-layout": {
"exports": [
"./ui/layout"
],
"exports": ["./ui/layout"],
"modules": [
"src/ui/panelRails.js",
"src/ui/leftPanelRail.js",
@@ -377,9 +287,7 @@
"external": []
},
"visual-input": {
"exports": [
"./ui/input"
],
"exports": ["./ui/input"],
"modules": [
"src/ui/visualInput.js",
"src/ui/applicationShortcuts.js",
@@ -388,19 +296,12 @@
"external": []
},
"display-controls": {
"exports": [
"./ui/display"
],
"modules": [
"src/ui/displayControls.js"
],
"exports": ["./ui/display"],
"modules": ["src/ui/displayControls.js"],
"external": []
},
"visual-effects": {
"exports": [
"./ui/effects",
"./ui/effects/bloom"
],
"exports": ["./ui/effects", "./ui/effects/bloom"],
"modules": [
"src/ui/effects.js",
"src/ui/visualEffects.js",
@@ -413,14 +314,10 @@
"src/styles/noir.js",
"src/styles/snow.js"
],
"external": [
"cesium"
]
"external": ["cesium"]
},
"map-source-controls": {
"exports": [
"./ui/maps"
],
"exports": ["./ui/maps"],
"modules": [
"src/ui/mapSource.js",
"src/ui/mapSourceControls.js",
@@ -430,10 +327,7 @@
"external": []
},
"layer-panel": {
"exports": [
"./ui/layers",
"./ui/layers/feedback"
],
"exports": ["./ui/layers", "./ui/layers/feedback"],
"modules": [
"src/ui/layers.js",
"src/ui/layerPanel.js",
@@ -444,9 +338,7 @@
"external": []
},
"location-controls": {
"exports": [
"./ui/location"
],
"exports": ["./ui/location"],
"modules": [
"src/ui/location.js",
"src/ui/locationControls.js",
@@ -457,9 +349,7 @@
"external": []
},
"radio-controls": {
"exports": [
"./ui/radio"
],
"exports": ["./ui/radio"],
"modules": [
"src/ui/radio.js",
"src/ui/radioControls.js",
@@ -470,9 +360,7 @@
"external": []
},
"cctv-controls": {
"exports": [
"./ui/cctv"
],
"exports": ["./ui/cctv"],
"modules": [
"src/ui/cctv.js",
"src/ui/cctvBindings.js",
@@ -484,10 +372,7 @@
"external": []
},
"context-controls": {
"exports": [
"./ui/context",
"./ui/context/policy"
],
"exports": ["./ui/context", "./ui/context/policy"],
"modules": [
"src/ui/context.js",
"src/ui/contextActions.js",
@@ -529,14 +414,10 @@
"src/cockpitVisionPolicy.js",
"src/ui/panelRailGeometry.js"
],
"external": [
"cesium"
]
"external": ["cesium"]
},
"scene-controls": {
"exports": [
"./ui/scenes"
],
"exports": ["./ui/scenes"],
"modules": [
"src/ui/scenes.js",
"src/ui/sceneControls.js",
@@ -545,9 +426,7 @@
"external": []
},
"ui-shell": {
"exports": [
"./ui/shell"
],
"exports": ["./ui/shell"],
"modules": [
"src/bloom.js",
"src/cctvFocusPolicy.js",
@@ -644,14 +523,10 @@
"src/app/stateChannel.js",
"src/ui/frameRateMonitor.js"
],
"external": [
"cesium"
]
"external": ["cesium"]
},
"ui-styles": {
"exports": [
"./ui/styles"
],
"exports": ["./ui/styles"],
"modules": [
"src/ui/styles.js",
"style.css",
@@ -678,9 +553,7 @@
"external": []
},
"scenes": {
"exports": [
"./scenes"
],
"exports": ["./scenes"],
"modules": [
"src/scenes/director.js",
"src/scenes/recipes.js",
@@ -692,14 +565,10 @@
"src/ui/scenePresentation.js",
"src/contextModePolicy.js"
],
"external": [
"cesium"
]
"external": ["cesium"]
},
"live-observations": {
"exports": [
"./sources/live"
],
"exports": ["./sources/live"],
"modules": [
"src/sources/live/aircraft.js",
"src/sources/live/contract.js",
@@ -710,9 +579,7 @@
"external": []
},
"civil-flights": {
"exports": [
"./layers/flights"
],
"exports": ["./layers/flights"],
"modules": [
"src/data/aircraftClass.js",
"src/data/aircraftIcons.js",
@@ -743,14 +610,10 @@
"src/layers/flights/testing.js",
"src/layers/flights/tracking.js"
],
"external": [
"cesium"
]
"external": ["cesium"]
},
"military-flights": {
"exports": [
"./layers/military"
],
"exports": ["./layers/military"],
"modules": [
"src/data/aircraftClass.js",
"src/data/aircraftIcons.js",
@@ -778,14 +641,10 @@
"src/layers/military/testing.js",
"src/layers/military/tracking.js"
],
"external": [
"cesium"
]
"external": ["cesium"]
},
"aircraft-mechanics": {
"exports": [
"./aircraft"
],
"exports": ["./aircraft"],
"modules": [
"src/data/aircraftClass.js",
"src/data/aircraftIcons.js",
@@ -805,14 +664,10 @@
"src/layers/aircraft/classification.js",
"src/layers/aircraft/index.js"
],
"external": [
"cesium"
]
"external": ["cesium"]
},
"vessel-layer": {
"exports": [
"./layers/vessels"
],
"exports": ["./layers/vessels"],
"modules": [
"src/layers/vessels/cards.js",
"src/layers/vessels/evidence.js",
@@ -830,27 +685,19 @@
"src/data/iconOrientation.js",
"src/data/vesselLabels.js"
],
"external": [
"cesium"
]
"external": ["cesium"]
},
"earthquakes": {
"exports": [
"./layers/earthquakes"
],
"exports": ["./layers/earthquakes"],
"modules": [
"src/layers/earthquakes/index.js",
"src/layers/earthquakes/model.js",
"src/layers/earthquakes/source.js"
],
"external": [
"cesium"
]
"external": ["cesium"]
},
"fire-layer": {
"exports": [
"./layers/firms"
],
"exports": ["./layers/firms"],
"modules": [
"src/data/firmsAdapt.js",
"src/data/firmsLabels.js",
@@ -869,14 +716,10 @@
"src/layers/firms/state.js",
"src/layers/firms/viewport.js"
],
"external": [
"cesium"
]
"external": ["cesium"]
},
"satellites-layer": {
"exports": [
"./layers/satellites"
],
"exports": ["./layers/satellites"],
"modules": [
"src/data/issPass.js",
"src/data/satelliteClass.js",
@@ -895,15 +738,10 @@
"src/layers/satellites/testing.js",
"src/layers/satellites/tracking.js"
],
"external": [
"cesium",
"satellite.js"
]
"external": ["cesium", "satellite.js"]
},
"launches-layer": {
"exports": [
"./layers/launches"
],
"exports": ["./layers/launches"],
"modules": [
"src/layers/launches/camera.js",
"src/layers/launches/controls.js",
@@ -925,14 +763,10 @@
"src/layers/launches/state.js",
"src/layers/launches/testing.js"
],
"external": [
"cesium"
]
"external": ["cesium"]
},
"installations-layer": {
"exports": [
"./layers/installations"
],
"exports": ["./layers/installations"],
"modules": [
"src/data/installationFeedback.js",
"src/data/militaryInstallationData.js",
@@ -948,14 +782,10 @@
"src/layers/installations/state.js",
"src/layers/installations/viewport.js"
],
"external": [
"cesium"
]
"external": ["cesium"]
},
"awareness-layer": {
"exports": [
"./layers/awareness"
],
"exports": ["./layers/awareness"],
"modules": [
"src/cockpitMath.js",
"src/data/iconOrientation.js",
@@ -978,14 +808,10 @@
"src/layers/awareness/testing.js",
"src/ui/panelRailGeometry.js"
],
"external": [
"cesium"
]
"external": ["cesium"]
},
"traffic-layer": {
"exports": [
"./layers/traffic"
],
"exports": ["./layers/traffic"],
"modules": [
"src/data/flowMatch.js",
"src/data/tomtomTiles.js",
@@ -1010,16 +836,10 @@
"src/layers/traffic/timing.js",
"src/layers/traffic/viewport.js"
],
"external": [
"cesium",
"pbf",
"@mapbox/vector-tile"
]
"external": ["cesium", "pbf", "@mapbox/vector-tile"]
},
"bikeshare-layer": {
"exports": [
"./layers/bikeshare"
],
"exports": ["./layers/bikeshare"],
"modules": [
"src/layers/bikeshare/controls.js",
"src/layers/bikeshare/index.js",
@@ -1036,14 +856,10 @@
"src/layers/bikeshare/testing.js",
"src/layers/bikeshare/viewport.js"
],
"external": [
"cesium"
]
"external": ["cesium"]
},
"cctv-layer": {
"exports": [
"./layers/cctv"
],
"exports": ["./layers/cctv"],
"modules": [
"src/data/cctvCards.js",
"src/data/cctvGizmo.js",
@@ -1076,14 +892,10 @@
"src/layers/cctv/testing.js",
"src/overlays/worldOverlayTokens.js"
],
"external": [
"cesium"
]
"external": ["cesium"]
},
"alpr-cameras": {
"exports": [
"./layers/alpr"
],
"exports": ["./layers/alpr"],
"modules": [
"src/layers/alpr/index.js",
"src/layers/alpr/model.js",
@@ -1091,8 +903,6 @@
"src/layers/alpr/presentation.js",
"src/layers/alpr/source.js"
],
"external": [
"cesium"
]
"external": ["cesium"]
}
}
+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'));
});