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
|
||||
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
@@ -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
@@ -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"]
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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) =>
|
||||
({
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": ''',
|
||||
})[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';
|
||||
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,
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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('<img onerror=alert(1)>'));
|
||||
assert.ok(html.includes('?a=1&b=2'));
|
||||
assert.ok(!html.includes('<img'));
|
||||
});
|
||||
Reference in new issue
Block a user