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 not camera footage or plate records. City-scale queries use the existing Overpass
provider, with capped results, retry, cached-data and incomplete-coverage notices. provider, with capped results, retry, cached-data and incomplete-coverage notices.
Its `./layers/alpr` entry exposes a per-instance layer factory and bounded source 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. render services. Selected cards and Data attribution identify OpenStreetMap.
Layer state and the existing voice layer tools include `alpr-cameras`. Layer state and the existing voice layer tools include `alpr-cameras`.
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "gods-eye-view", "name": "gods-eye-view",
"private": true, "private": true,
"version": "0.1.1", "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", "type": "module",
"license": "MIT", "license": "MIT",
"author": "Bilawal Sidhu", "author": "Bilawal Sidhu",
+91 -281
View File
@@ -10,47 +10,27 @@
"src/data/localGeojsonCore.js", "src/data/localGeojsonCore.js",
"src/data/localGeojsonLod.js" "src/data/localGeojsonLod.js"
], ],
"external": [ "external": ["cesium"]
"cesium"
]
}, },
"application": { "application": {
"exports": [ "exports": ["./application"],
"./application" "modules": ["src/app/application.js"],
],
"modules": [
"src/app/application.js"
],
"external": [] "external": []
}, },
"viewer": { "viewer": {
"exports": [ "exports": ["./application/viewer"],
"./application/viewer" "modules": ["src/app/viewer.js"],
], "external": ["cesium"]
"modules": [
"src/app/viewer.js"
],
"external": [
"cesium"
]
}, },
"vite-build": { "vite-build": {
"runtime": "node", "runtime": "node",
"exports": [ "exports": ["./build/vite"],
"./build/vite" "modules": ["build/vite.js"],
], "external": ["vite-plugin-cesium"]
"modules": [
"build/vite.js"
],
"external": [
"vite-plugin-cesium"
]
}, },
"live-providers": { "live-providers": {
"runtime": "node", "runtime": "node",
"exports": [ "exports": ["./server/providers/live"],
"./server/providers/live"
],
"modules": [ "modules": [
"server/providers/live.js", "server/providers/live.js",
"server/providers/common/http.js", "server/providers/common/http.js",
@@ -65,24 +45,16 @@
"src/data/aisStreamAdapter.js", "src/data/aisStreamAdapter.js",
"src/data/aisWatchdog.js" "src/data/aisWatchdog.js"
], ],
"external": [ "external": ["ws"]
"ws"
]
}, },
"adsb-lol-source": { "adsb-lol-source": {
"exports": [ "exports": ["./sources/adsb-lol"],
"./sources/adsb-lol" "modules": ["src/data/adsbLolFallback.js"],
],
"modules": [
"src/data/adsbLolFallback.js"
],
"external": [] "external": []
}, },
"place-providers": { "place-providers": {
"runtime": "node", "runtime": "node",
"exports": [ "exports": ["./server/providers/places"],
"./server/providers/places"
],
"modules": [ "modules": [
"server/providers/places.js", "server/providers/places.js",
"server/providers/places/google.js", "server/providers/places/google.js",
@@ -97,19 +69,13 @@
"external": [] "external": []
}, },
"place-payloads": { "place-payloads": {
"exports": [ "exports": ["./sources/places"],
"./sources/places" "modules": ["src/data/placeProviderPayloads.js"],
],
"modules": [
"src/data/placeProviderPayloads.js"
],
"external": [] "external": []
}, },
"space-providers": { "space-providers": {
"runtime": "node", "runtime": "node",
"exports": [ "exports": ["./server/providers/space"],
"./server/providers/space"
],
"modules": [ "modules": [
"server/providers/space.js", "server/providers/space.js",
"server/providers/space/celestrak.js", "server/providers/space/celestrak.js",
@@ -120,19 +86,13 @@
"external": [] "external": []
}, },
"space-requests": { "space-requests": {
"exports": [ "exports": ["./sources/space"],
"./sources/space" "modules": ["src/data/spaceProviderRequests.js"],
],
"modules": [
"src/data/spaceProviderRequests.js"
],
"external": [] "external": []
}, },
"terrain-provider": { "terrain-provider": {
"runtime": "node", "runtime": "node",
"exports": [ "exports": ["./server/providers/terrain"],
"./server/providers/terrain"
],
"modules": [ "modules": [
"server/providers/terrain.js", "server/providers/terrain.js",
"src/data/terrainHeightsProxy.js" "src/data/terrainHeightsProxy.js"
@@ -140,50 +100,30 @@
"external": [] "external": []
}, },
"terrain-source": { "terrain-source": {
"exports": [ "exports": ["./sources/terrain"],
"./sources/terrain" "modules": ["src/data/terrainHeightsProxy.js"],
],
"modules": [
"src/data/terrainHeightsProxy.js"
],
"external": [] "external": []
}, },
"traffic-provider": { "traffic-provider": {
"runtime": "node", "runtime": "node",
"exports": [ "exports": ["./server/providers/traffic"],
"./server/providers/traffic" "modules": ["server/providers/traffic.js", "src/data/tomtomTiles.js"],
],
"modules": [
"server/providers/traffic.js",
"src/data/tomtomTiles.js"
],
"external": [] "external": []
}, },
"traffic-source": { "traffic-source": {
"exports": [ "exports": ["./sources/traffic"],
"./sources/traffic" "modules": ["src/data/tomtomTiles.js"],
],
"modules": [
"src/data/tomtomTiles.js"
],
"external": [] "external": []
}, },
"firms-provider": { "firms-provider": {
"runtime": "node", "runtime": "node",
"exports": [ "exports": ["./server/providers/firms"],
"./server/providers/firms" "modules": ["server/providers/firms.js", "src/data/firmsCsv.js"],
],
"modules": [
"server/providers/firms.js",
"src/data/firmsCsv.js"
],
"external": [] "external": []
}, },
"gbfs-provider": { "gbfs-provider": {
"runtime": "node", "runtime": "node",
"exports": [ "exports": ["./server/providers/gbfs"],
"./server/providers/gbfs"
],
"modules": [ "modules": [
"server/providers/gbfs.js", "server/providers/gbfs.js",
"server/providers/common/http.js", "server/providers/common/http.js",
@@ -192,19 +132,13 @@
"external": [] "external": []
}, },
"gbfs-source": { "gbfs-source": {
"exports": [ "exports": ["./sources/gbfs"],
"./sources/gbfs" "modules": ["src/data/gbfsSource.js"],
],
"modules": [
"src/data/gbfsSource.js"
],
"external": [] "external": []
}, },
"cctv-provider": { "cctv-provider": {
"runtime": "node", "runtime": "node",
"exports": [ "exports": ["./server/providers/cctv"],
"./server/providers/cctv"
],
"modules": [ "modules": [
"server/providers/cctv.js", "server/providers/cctv.js",
"server/providers/cctv/catalog.js", "server/providers/cctv/catalog.js",
@@ -221,9 +155,7 @@
}, },
"radio-provider": { "radio-provider": {
"runtime": "node", "runtime": "node",
"exports": [ "exports": ["./server/providers/radio"],
"./server/providers/radio"
],
"modules": [ "modules": [
"server/providers/radio.js", "server/providers/radio.js",
"server/providers/radio/catalog.js", "server/providers/radio/catalog.js",
@@ -237,9 +169,7 @@
}, },
"overpass-provider": { "overpass-provider": {
"runtime": "node", "runtime": "node",
"exports": [ "exports": ["./server/providers/overpass"],
"./server/providers/overpass"
],
"modules": [ "modules": [
"server/providers/common/geo.js", "server/providers/common/geo.js",
"server/providers/common/http.js", "server/providers/common/http.js",
@@ -258,9 +188,7 @@
}, },
"military-installations-provider": { "military-installations-provider": {
"runtime": "node", "runtime": "node",
"exports": [ "exports": ["./server/providers/military-installations"],
"./server/providers/military-installations"
],
"modules": [ "modules": [
"server/providers/common/http.js", "server/providers/common/http.js",
"server/providers/common/query.js", "server/providers/common/query.js",
@@ -277,9 +205,7 @@
}, },
"regional-provider": { "regional-provider": {
"runtime": "node", "runtime": "node",
"exports": [ "exports": ["./server/providers/regional"],
"./server/providers/regional"
],
"modules": [ "modules": [
"server/providers/common/http.js", "server/providers/common/http.js",
"server/providers/common/query.js", "server/providers/common/query.js",
@@ -298,9 +224,7 @@
}, },
"openai-provider": { "openai-provider": {
"runtime": "node", "runtime": "node",
"exports": [ "exports": ["./server/providers/openai"],
"./server/providers/openai"
],
"modules": [ "modules": [
"server/providers/common/rate-limit.js", "server/providers/common/rate-limit.js",
"server/providers/common/request.js", "server/providers/common/request.js",
@@ -320,9 +244,7 @@
}, },
"standalone-key-setup": { "standalone-key-setup": {
"runtime": "node", "runtime": "node",
"exports": [ "exports": ["./server/standalone/key-setup"],
"./server/standalone/key-setup"
],
"modules": [ "modules": [
"scripts/pinokio-environment.mjs", "scripts/pinokio-environment.mjs",
"server/providers/common/source-root.js", "server/providers/common/source-root.js",
@@ -333,9 +255,7 @@
"external": [] "external": []
}, },
"place-search": { "place-search": {
"exports": [ "exports": ["./search"],
"./search"
],
"modules": [ "modules": [
"src/search/index.js", "src/search/index.js",
"src/search/placeSearch.js", "src/search/placeSearch.js",
@@ -345,27 +265,17 @@
"external": [] "external": []
}, },
"panel-disclosure": { "panel-disclosure": {
"exports": [ "exports": ["./ui/panels"],
"./ui/panels" "modules": ["src/ui/panelDisclosure.js"],
],
"modules": [
"src/ui/panelDisclosure.js"
],
"external": [] "external": []
}, },
"surface-keyboard": { "surface-keyboard": {
"exports": [ "exports": ["./ui/surfaces"],
"./ui/surfaces" "modules": ["src/ui/surfaceKeyboard.js"],
],
"modules": [
"src/ui/surfaceKeyboard.js"
],
"external": [] "external": []
}, },
"panel-rail-layout": { "panel-rail-layout": {
"exports": [ "exports": ["./ui/layout"],
"./ui/layout"
],
"modules": [ "modules": [
"src/ui/panelRails.js", "src/ui/panelRails.js",
"src/ui/leftPanelRail.js", "src/ui/leftPanelRail.js",
@@ -377,9 +287,7 @@
"external": [] "external": []
}, },
"visual-input": { "visual-input": {
"exports": [ "exports": ["./ui/input"],
"./ui/input"
],
"modules": [ "modules": [
"src/ui/visualInput.js", "src/ui/visualInput.js",
"src/ui/applicationShortcuts.js", "src/ui/applicationShortcuts.js",
@@ -388,19 +296,12 @@
"external": [] "external": []
}, },
"display-controls": { "display-controls": {
"exports": [ "exports": ["./ui/display"],
"./ui/display" "modules": ["src/ui/displayControls.js"],
],
"modules": [
"src/ui/displayControls.js"
],
"external": [] "external": []
}, },
"visual-effects": { "visual-effects": {
"exports": [ "exports": ["./ui/effects", "./ui/effects/bloom"],
"./ui/effects",
"./ui/effects/bloom"
],
"modules": [ "modules": [
"src/ui/effects.js", "src/ui/effects.js",
"src/ui/visualEffects.js", "src/ui/visualEffects.js",
@@ -413,14 +314,10 @@
"src/styles/noir.js", "src/styles/noir.js",
"src/styles/snow.js" "src/styles/snow.js"
], ],
"external": [ "external": ["cesium"]
"cesium"
]
}, },
"map-source-controls": { "map-source-controls": {
"exports": [ "exports": ["./ui/maps"],
"./ui/maps"
],
"modules": [ "modules": [
"src/ui/mapSource.js", "src/ui/mapSource.js",
"src/ui/mapSourceControls.js", "src/ui/mapSourceControls.js",
@@ -430,10 +327,7 @@
"external": [] "external": []
}, },
"layer-panel": { "layer-panel": {
"exports": [ "exports": ["./ui/layers", "./ui/layers/feedback"],
"./ui/layers",
"./ui/layers/feedback"
],
"modules": [ "modules": [
"src/ui/layers.js", "src/ui/layers.js",
"src/ui/layerPanel.js", "src/ui/layerPanel.js",
@@ -444,9 +338,7 @@
"external": [] "external": []
}, },
"location-controls": { "location-controls": {
"exports": [ "exports": ["./ui/location"],
"./ui/location"
],
"modules": [ "modules": [
"src/ui/location.js", "src/ui/location.js",
"src/ui/locationControls.js", "src/ui/locationControls.js",
@@ -457,9 +349,7 @@
"external": [] "external": []
}, },
"radio-controls": { "radio-controls": {
"exports": [ "exports": ["./ui/radio"],
"./ui/radio"
],
"modules": [ "modules": [
"src/ui/radio.js", "src/ui/radio.js",
"src/ui/radioControls.js", "src/ui/radioControls.js",
@@ -470,9 +360,7 @@
"external": [] "external": []
}, },
"cctv-controls": { "cctv-controls": {
"exports": [ "exports": ["./ui/cctv"],
"./ui/cctv"
],
"modules": [ "modules": [
"src/ui/cctv.js", "src/ui/cctv.js",
"src/ui/cctvBindings.js", "src/ui/cctvBindings.js",
@@ -484,10 +372,7 @@
"external": [] "external": []
}, },
"context-controls": { "context-controls": {
"exports": [ "exports": ["./ui/context", "./ui/context/policy"],
"./ui/context",
"./ui/context/policy"
],
"modules": [ "modules": [
"src/ui/context.js", "src/ui/context.js",
"src/ui/contextActions.js", "src/ui/contextActions.js",
@@ -529,14 +414,10 @@
"src/cockpitVisionPolicy.js", "src/cockpitVisionPolicy.js",
"src/ui/panelRailGeometry.js" "src/ui/panelRailGeometry.js"
], ],
"external": [ "external": ["cesium"]
"cesium"
]
}, },
"scene-controls": { "scene-controls": {
"exports": [ "exports": ["./ui/scenes"],
"./ui/scenes"
],
"modules": [ "modules": [
"src/ui/scenes.js", "src/ui/scenes.js",
"src/ui/sceneControls.js", "src/ui/sceneControls.js",
@@ -545,9 +426,7 @@
"external": [] "external": []
}, },
"ui-shell": { "ui-shell": {
"exports": [ "exports": ["./ui/shell"],
"./ui/shell"
],
"modules": [ "modules": [
"src/bloom.js", "src/bloom.js",
"src/cctvFocusPolicy.js", "src/cctvFocusPolicy.js",
@@ -644,14 +523,10 @@
"src/app/stateChannel.js", "src/app/stateChannel.js",
"src/ui/frameRateMonitor.js" "src/ui/frameRateMonitor.js"
], ],
"external": [ "external": ["cesium"]
"cesium"
]
}, },
"ui-styles": { "ui-styles": {
"exports": [ "exports": ["./ui/styles"],
"./ui/styles"
],
"modules": [ "modules": [
"src/ui/styles.js", "src/ui/styles.js",
"style.css", "style.css",
@@ -678,9 +553,7 @@
"external": [] "external": []
}, },
"scenes": { "scenes": {
"exports": [ "exports": ["./scenes"],
"./scenes"
],
"modules": [ "modules": [
"src/scenes/director.js", "src/scenes/director.js",
"src/scenes/recipes.js", "src/scenes/recipes.js",
@@ -692,14 +565,10 @@
"src/ui/scenePresentation.js", "src/ui/scenePresentation.js",
"src/contextModePolicy.js" "src/contextModePolicy.js"
], ],
"external": [ "external": ["cesium"]
"cesium"
]
}, },
"live-observations": { "live-observations": {
"exports": [ "exports": ["./sources/live"],
"./sources/live"
],
"modules": [ "modules": [
"src/sources/live/aircraft.js", "src/sources/live/aircraft.js",
"src/sources/live/contract.js", "src/sources/live/contract.js",
@@ -710,9 +579,7 @@
"external": [] "external": []
}, },
"civil-flights": { "civil-flights": {
"exports": [ "exports": ["./layers/flights"],
"./layers/flights"
],
"modules": [ "modules": [
"src/data/aircraftClass.js", "src/data/aircraftClass.js",
"src/data/aircraftIcons.js", "src/data/aircraftIcons.js",
@@ -743,14 +610,10 @@
"src/layers/flights/testing.js", "src/layers/flights/testing.js",
"src/layers/flights/tracking.js" "src/layers/flights/tracking.js"
], ],
"external": [ "external": ["cesium"]
"cesium"
]
}, },
"military-flights": { "military-flights": {
"exports": [ "exports": ["./layers/military"],
"./layers/military"
],
"modules": [ "modules": [
"src/data/aircraftClass.js", "src/data/aircraftClass.js",
"src/data/aircraftIcons.js", "src/data/aircraftIcons.js",
@@ -778,14 +641,10 @@
"src/layers/military/testing.js", "src/layers/military/testing.js",
"src/layers/military/tracking.js" "src/layers/military/tracking.js"
], ],
"external": [ "external": ["cesium"]
"cesium"
]
}, },
"aircraft-mechanics": { "aircraft-mechanics": {
"exports": [ "exports": ["./aircraft"],
"./aircraft"
],
"modules": [ "modules": [
"src/data/aircraftClass.js", "src/data/aircraftClass.js",
"src/data/aircraftIcons.js", "src/data/aircraftIcons.js",
@@ -805,14 +664,10 @@
"src/layers/aircraft/classification.js", "src/layers/aircraft/classification.js",
"src/layers/aircraft/index.js" "src/layers/aircraft/index.js"
], ],
"external": [ "external": ["cesium"]
"cesium"
]
}, },
"vessel-layer": { "vessel-layer": {
"exports": [ "exports": ["./layers/vessels"],
"./layers/vessels"
],
"modules": [ "modules": [
"src/layers/vessels/cards.js", "src/layers/vessels/cards.js",
"src/layers/vessels/evidence.js", "src/layers/vessels/evidence.js",
@@ -830,27 +685,19 @@
"src/data/iconOrientation.js", "src/data/iconOrientation.js",
"src/data/vesselLabels.js" "src/data/vesselLabels.js"
], ],
"external": [ "external": ["cesium"]
"cesium"
]
}, },
"earthquakes": { "earthquakes": {
"exports": [ "exports": ["./layers/earthquakes"],
"./layers/earthquakes"
],
"modules": [ "modules": [
"src/layers/earthquakes/index.js", "src/layers/earthquakes/index.js",
"src/layers/earthquakes/model.js", "src/layers/earthquakes/model.js",
"src/layers/earthquakes/source.js" "src/layers/earthquakes/source.js"
], ],
"external": [ "external": ["cesium"]
"cesium"
]
}, },
"fire-layer": { "fire-layer": {
"exports": [ "exports": ["./layers/firms"],
"./layers/firms"
],
"modules": [ "modules": [
"src/data/firmsAdapt.js", "src/data/firmsAdapt.js",
"src/data/firmsLabels.js", "src/data/firmsLabels.js",
@@ -869,14 +716,10 @@
"src/layers/firms/state.js", "src/layers/firms/state.js",
"src/layers/firms/viewport.js" "src/layers/firms/viewport.js"
], ],
"external": [ "external": ["cesium"]
"cesium"
]
}, },
"satellites-layer": { "satellites-layer": {
"exports": [ "exports": ["./layers/satellites"],
"./layers/satellites"
],
"modules": [ "modules": [
"src/data/issPass.js", "src/data/issPass.js",
"src/data/satelliteClass.js", "src/data/satelliteClass.js",
@@ -895,15 +738,10 @@
"src/layers/satellites/testing.js", "src/layers/satellites/testing.js",
"src/layers/satellites/tracking.js" "src/layers/satellites/tracking.js"
], ],
"external": [ "external": ["cesium", "satellite.js"]
"cesium",
"satellite.js"
]
}, },
"launches-layer": { "launches-layer": {
"exports": [ "exports": ["./layers/launches"],
"./layers/launches"
],
"modules": [ "modules": [
"src/layers/launches/camera.js", "src/layers/launches/camera.js",
"src/layers/launches/controls.js", "src/layers/launches/controls.js",
@@ -925,14 +763,10 @@
"src/layers/launches/state.js", "src/layers/launches/state.js",
"src/layers/launches/testing.js" "src/layers/launches/testing.js"
], ],
"external": [ "external": ["cesium"]
"cesium"
]
}, },
"installations-layer": { "installations-layer": {
"exports": [ "exports": ["./layers/installations"],
"./layers/installations"
],
"modules": [ "modules": [
"src/data/installationFeedback.js", "src/data/installationFeedback.js",
"src/data/militaryInstallationData.js", "src/data/militaryInstallationData.js",
@@ -948,14 +782,10 @@
"src/layers/installations/state.js", "src/layers/installations/state.js",
"src/layers/installations/viewport.js" "src/layers/installations/viewport.js"
], ],
"external": [ "external": ["cesium"]
"cesium"
]
}, },
"awareness-layer": { "awareness-layer": {
"exports": [ "exports": ["./layers/awareness"],
"./layers/awareness"
],
"modules": [ "modules": [
"src/cockpitMath.js", "src/cockpitMath.js",
"src/data/iconOrientation.js", "src/data/iconOrientation.js",
@@ -978,14 +808,10 @@
"src/layers/awareness/testing.js", "src/layers/awareness/testing.js",
"src/ui/panelRailGeometry.js" "src/ui/panelRailGeometry.js"
], ],
"external": [ "external": ["cesium"]
"cesium"
]
}, },
"traffic-layer": { "traffic-layer": {
"exports": [ "exports": ["./layers/traffic"],
"./layers/traffic"
],
"modules": [ "modules": [
"src/data/flowMatch.js", "src/data/flowMatch.js",
"src/data/tomtomTiles.js", "src/data/tomtomTiles.js",
@@ -1010,16 +836,10 @@
"src/layers/traffic/timing.js", "src/layers/traffic/timing.js",
"src/layers/traffic/viewport.js" "src/layers/traffic/viewport.js"
], ],
"external": [ "external": ["cesium", "pbf", "@mapbox/vector-tile"]
"cesium",
"pbf",
"@mapbox/vector-tile"
]
}, },
"bikeshare-layer": { "bikeshare-layer": {
"exports": [ "exports": ["./layers/bikeshare"],
"./layers/bikeshare"
],
"modules": [ "modules": [
"src/layers/bikeshare/controls.js", "src/layers/bikeshare/controls.js",
"src/layers/bikeshare/index.js", "src/layers/bikeshare/index.js",
@@ -1036,14 +856,10 @@
"src/layers/bikeshare/testing.js", "src/layers/bikeshare/testing.js",
"src/layers/bikeshare/viewport.js" "src/layers/bikeshare/viewport.js"
], ],
"external": [ "external": ["cesium"]
"cesium"
]
}, },
"cctv-layer": { "cctv-layer": {
"exports": [ "exports": ["./layers/cctv"],
"./layers/cctv"
],
"modules": [ "modules": [
"src/data/cctvCards.js", "src/data/cctvCards.js",
"src/data/cctvGizmo.js", "src/data/cctvGizmo.js",
@@ -1076,14 +892,10 @@
"src/layers/cctv/testing.js", "src/layers/cctv/testing.js",
"src/overlays/worldOverlayTokens.js" "src/overlays/worldOverlayTokens.js"
], ],
"external": [ "external": ["cesium"]
"cesium"
]
}, },
"alpr-cameras": { "alpr-cameras": {
"exports": [ "exports": ["./layers/alpr"],
"./layers/alpr"
],
"modules": [ "modules": [
"src/layers/alpr/index.js", "src/layers/alpr/index.js",
"src/layers/alpr/model.js", "src/layers/alpr/model.js",
@@ -1091,8 +903,6 @@
"src/layers/alpr/presentation.js", "src/layers/alpr/presentation.js",
"src/layers/alpr/source.js" "src/layers/alpr/source.js"
], ],
"external": [ "external": ["cesium"]
"cesium"
]
} }
} }
+21 -3
View File
@@ -891,7 +891,15 @@ test('two factories keep their requests, records and destruction independent', a
const second = createAlprCamerasLayer({ const second = createAlprCamerasLayer({
services, services,
source: { 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), const a = cameraHarness(first),
@@ -901,11 +909,21 @@ test('two factories keep their requests, records and destruction independent', a
await second.update(); await second.update();
first.destroy(); first.destroy();
assert.equal(firstSignal.aborted, true); assert.equal(firstSignal.aborted, true);
resolveFirst({ elements: [cameraNode(42)], stale: false }); resolveFirst({
records: [normalizeAlprNode(cameraNode(42))],
stale: false,
saturated: false,
});
await pending; await pending;
assert.equal(first.getStats().count, 0); assert.equal(first.getStats().count, 0);
assert.equal(second.getStats().count, 1); 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.camera.moveEnd.numberOfListeners, 0);
assert.equal(a.viewer.scene.postRender.numberOfListeners, 0); assert.equal(a.viewer.scene.postRender.numberOfListeners, 0);
} finally { } finally {
+21 -25
View File
@@ -12,16 +12,24 @@ import {
boxContains, boxContains,
destinationPointDeg, destinationPointDeg,
alprRetryDelayMs, alprRetryDelayMs,
isAlprSurveillanceType, validateAlprSnapshot,
normalizeAlprNode, alprCreditMarkup,
buildOverpassQuery,
} from './model.js'; } from './model.js';
import { createAlprPresentation } from './presentation.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 } = {}) { export function createAlprCamerasLayer({ source, services } = {}) {
if (typeof source?.fetch !== 'function') if (typeof source?.fetch !== 'function')
throw new TypeError('ALPR requires a camera source'); throw new TypeError('ALPR requires a camera source');
const creditMarkup = alprCreditMarkup(source.attribution);
const { governorRequestRender } = services.render; const { governorRequestRender } = services.render;
const { registerPickOwner, unregisterPickOwner } = services.picking; const { registerPickOwner, unregisterPickOwner } = services.picking;
@@ -67,7 +75,7 @@ export function createAlprCamerasLayer({ source, services } = {}) {
clearSelection, clearSelection,
updateSelectedAnchor, updateSelectedAnchor,
installInteraction, installInteraction,
} = createAlprPresentation({ state, services }); } = createAlprPresentation({ state, services, source });
function setAlprStatus(status, error = null) { function setAlprStatus(status, error = null) {
if (state.status === status && state.error === error) return; if (state.status === status && state.error === error) return;
@@ -156,24 +164,14 @@ export function createAlprCamerasLayer({ source, services } = {}) {
setAlprStatus('loading'); setAlprStatus('loading');
renderRecords(); renderRecords();
try { try {
const { elements, stale } = await source.fetch( const snapshot = await source.fetch(queryBox, requestAbort.signal);
queryBox,
requestAbort.signal,
);
if ( if (
requestAbort.signal.aborted || requestAbort.signal.aborted ||
state.abort !== requestAbort || state.abort !== requestAbort ||
!state.enabled !state.enabled
) )
return; return;
const records = [ const { records, stale, saturated } = validateAlprSnapshot(snapshot);
...new Map(
elements
.map(normalizeAlprNode)
.filter(Boolean)
.map((record) => [record.id, record]),
).values(),
];
state.records = records; state.records = records;
state.recordById = new Map(records.map((r) => [r.id, r])); state.recordById = new Map(records.map((r) => [r.id, r]));
state.lastUpdate = Date.now(); 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: // 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 // 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. // be told rather than left with a count that disagrees with the map.
state.saturated = state.saturated = saturated || records.length > MAX_RENDERED;
elements.length >= QUERY_LIMIT || records.length > MAX_RENDERED;
state.lastQueryBox = queryBox; state.lastQueryBox = queryBox;
clearUnavailableRetry(); clearUnavailableRetry();
// Stale and saturated are independent facts; a cached response that also // 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); state.stale = Boolean(state.lastUpdate);
setAlprStatus( setAlprStatus(
'unavailable', 'unavailable',
error?.message || 'Overpass temporarily unavailable', error?.message || 'Camera source temporarily unavailable',
); );
scheduleUnavailableRetry(); scheduleUnavailableRetry();
} finally { } finally {
@@ -223,17 +220,16 @@ export function createAlprCamerasLayer({ source, services } = {}) {
id: LAYER_ID, id: LAYER_ID,
name: 'ALPR Cameras', name: 'ALPR Cameras',
icon: '📷', icon: '📷',
source: 'OpenStreetMap · community mapped', source: source.label || 'Mapped camera locations',
updateInterval: 0, updateInterval: 0,
statsRefreshInterval: 1000, statsRefreshInterval: 1000,
init(viewer) { init(viewer) {
if (state.viewer) throw new Error('ALPR layer is already initialized'); if (state.viewer) throw new Error('ALPR layer is already initialized');
state.viewer = viewer; state.viewer = viewer;
state.dataSource = new Cesium.CustomDataSource('alpr-cameras'); state.dataSource = new Cesium.CustomDataSource('alpr-cameras');
state.credit = new Cesium.Credit( state.credit = creditMarkup
'<span class="gev-alpr-credit">ALPR: <a href="https://www.openstreetmap.org/copyright" target="_blank" rel="noopener">© OpenStreetMap</a></span>', ? new Cesium.Credit(creditMarkup, true)
true, : null;
);
viewer.dataSources.add(state.dataSource); viewer.dataSources.add(state.dataSource);
state.moveEndRemove = state.moveEndRemove =
viewer.camera.moveEnd.addEventListener(scheduleLoad); 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};` `(${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'; } from './policy.js';
import { boxContains, destinationPointDeg } from './model.js'; import { boxContains, destinationPointDeg } from './model.js';
export function createAlprPresentation({ state, services }) { export function createAlprPresentation({ state, services, source }) {
const { governorRequestRender } = services.render; const { governorRequestRender } = services.render;
const { const {
clearSelectedEntityContextForLayer, clearSelectedEntityContextForLayer,
@@ -130,7 +130,7 @@ export function createAlprPresentation({ state, services }) {
} }
const entity = state.dataSource.entities.add(entityDef); const entity = state.dataSource.entities.add(entityDef);
entity.gevTrackedId = record.id; 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, // visual anchor. Only the selected marker samples the rendered surface,
// at most once per second, through Cesium's public height APIs. // at most once per second, through Cesium's public height APIs.
entity.gevAlprDisplayPosition = null; entity.gevAlprDisplayPosition = null;
@@ -141,7 +141,7 @@ export function createAlprPresentation({ state, services }) {
record.manufacturer ? `Manufacturer: ${record.manufacturer}` : null, record.manufacturer ? `Manufacturer: ${record.manufacturer}` : null,
record.operator ? `Operator: ${record.operator}` : null, record.operator ? `Operator: ${record.operator}` : null,
record.cameraType ? `Type: ${record.cameraType}` : null, record.cameraType ? `Type: ${record.cameraType}` : null,
'Source: OpenStreetMap', `Source: ${source.attribution?.name || source.label || 'Camera source'}`,
].filter(Boolean), ].filter(Boolean),
accent: color.toCssColorString(), accent: color.toCssColorString(),
}; };
@@ -150,7 +150,8 @@ export function createAlprPresentation({ state, services }) {
layerId: LAYER_ID, layerId: LAYER_ID,
dataSource: state.dataSource, dataSource: state.dataSource,
layerName: 'ALPR Cameras', layerName: 'ALPR Cameras',
source: 'OpenStreetMap contributors (ODbL 1.0; community mapped)', source:
source.attribution?.description || source.label || 'Camera source',
label: 'ALPR camera', label: 'ALPR camera',
latitude: record.latitude, latitude: record.latitude,
longitude: record.longitude, longitude: record.longitude,
+30 -3
View File
@@ -2,8 +2,9 @@ import {
OVERPASS_URL, OVERPASS_URL,
MAX_VIEWPORT_DEGREES, MAX_VIEWPORT_DEGREES,
QUERY_SNAP_DEGREES, QUERY_SNAP_DEGREES,
QUERY_LIMIT,
} from './policy.js'; } from './policy.js';
import { buildOverpassQuery } from './model.js'; import { buildOverpassQuery, normalizeAlprNode } from './model.js';
/** Construct the bounded OSM request adapter without starting a request. */ /** Construct the bounded OSM request adapter without starting a request. */
export function createOverpassAlprSource({ export function createOverpassAlprSource({
fetchImpl = (...args) => globalThis.fetch(...args), fetchImpl = (...args) => globalThis.fetch(...args),
@@ -33,6 +34,11 @@ export function createOverpassAlprSource({
signal, signal,
}); });
if (!response.ok) { if (!response.ok) {
try {
await response.body?.cancel();
} catch {
/* already closed */
}
const message = const message =
response.status === 429 response.status === 429
? 'Overpass rate-limited' ? 'Overpass rate-limited'
@@ -49,7 +55,28 @@ export function createOverpassAlprSource({
if (!Array.isArray(payload?.elements) || payload.remark) { if (!Array.isArray(payload?.elements) || payload.remark) {
throw new Error('Overpass returned an incomplete camera response'); 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 test from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { createOverpassAlprSource } from './source.js'; import { createOverpassAlprSource } from './source.js';
import { validateAlprSnapshot, alprCreditMarkup } from './model.js';
const box = { south: 30, west: -98, north: 30.1, east: -97.9 }; const box = { south: 30, west: -98, north: 30.1, east: -97.9 };
test('the source rejects invalid and unbounded queries before fetching', async () => { test('the source rejects invalid and unbounded queries before fetching', async () => {
let calls = 0; 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' }); 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'));
});