mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 02:14:01 +00:00
refactor(alpr): accept normalized camera snapshots and source attribution
This commit is contained in:
1 parent
4374c0fa60
commit
0feaab0a72
9 files changed
+307
-318
No files matched your search
@@ -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
@@ -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
@@ -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"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
@@ -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);
|
||||||
|
|||||||
@@ -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) =>
|
||||||
|
({
|
||||||
|
'&': '&',
|
||||||
|
'<': '<',
|
||||||
|
'>': '>',
|
||||||
|
'"': '"',
|
||||||
|
"'": ''',
|
||||||
|
})[char],
|
||||||
|
);
|
||||||
|
return `<span class="gev-alpr-credit">ALPR: <a href="${escape(href.href)}" target="_blank" rel="noopener">${escape(attribution.text)}</a></span>`;
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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',
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
@@ -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('<img onerror=alert(1)>'));
|
||||||
|
assert.ok(html.includes('?a=1&b=2'));
|
||||||
|
assert.ok(!html.includes('<img'));
|
||||||
|
});
|
||||||
Reference in new issue
Block a user