From 672191497a58fbf0e2b1ee1fad5543fb7c6d0b13 Mon Sep 17 00:00:00 2001 From: Daniel Slay <99130682+danielslay86@users.noreply.github.com> Date: Fri, 25 Sep 2026 16:31:47 -0400 Subject: [PATCH 1/5] feat(cctv): color live video cameras apart from stills --- docs/CURRENT-STATE.md | 2 +- src/layers/cctv/policy.js | 3 +++ src/layers/cctv/rendering.js | 19 ++++++++++++------- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 105fdd37f..734e722d7 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -4359,7 +4359,7 @@ silently demoting every later lookup for the session. ground clamp now lifts the cap _center_ only so the wireframe stays a true pyramid welded to the plane (was a flattened fan / the ~47.5 m divergence — RESOLVED), re-selecting the active camera is a no-op (killed a click-flash), and texture swaps gate on canvas content (killed a - periodic white flash). Coverage is now **metro-wide: 250 cameras** (`CCTV_AUSTIN_MAX_SOURCES` + periodic white flash). Camera icons are cyan for stills, violet for cameras with a live video feed, and amber for the active camera. Coverage is now **metro-wide: 250 cameras** (`CCTV_AUSTIN_MAX_SOURCES` default 36 → 250, hard bound 300), filtered to `camera_status === TURNED_ON` (~815 live of 1,003 rows). City packs (2026-07-04): Caltrans (districts 4/7/11/3 — SF, LA, San Diego, Sacramento; cap 300) and TfL London JamCams (cap 250) join Austin (cap 250) as keyless default diff --git a/src/layers/cctv/policy.js b/src/layers/cctv/policy.js index cf09ebe64..7ba4e1231 100644 --- a/src/layers/cctv/policy.js +++ b/src/layers/cctv/policy.js @@ -410,6 +410,9 @@ export const IDLE_CAMERA_COLOR = export const ACTIVE_CAMERA_COLOR = Cesium.Color.fromCssColorString('#ffd97a').withAlpha(0.95); +export const LIVE_CAMERA_COLOR = + Cesium.Color.fromCssColorString('#b48cff').withAlpha(0.88); + export const IDLE_COVERAGE_COLOR = Cesium.Color.fromCssColorString('#2fe0ff').withAlpha(0.24); diff --git a/src/layers/cctv/rendering.js b/src/layers/cctv/rendering.js index 6601d938b..d10368d40 100644 --- a/src/layers/cctv/rendering.js +++ b/src/layers/cctv/rendering.js @@ -4,6 +4,7 @@ import { isHeadingEstimated } from './headingConfidence.js'; import { ACTIVE_CAMERA_COLOR, IDLE_CAMERA_COLOR, + LIVE_CAMERA_COLOR, ACTIVE_COVERAGE_CENTER, IDLE_COVERAGE_CENTER_MUTED, ACTIVE_COVERAGE_CENTER_DEPTHFAIL, @@ -18,6 +19,15 @@ export function createRendering({ parts, source, }) { + /** Icon color: active wins, then live-capable video feeds, then idle stills. */ + function cameraColorFor(record) { + if (record.camera.id === layerState._activeCameraId) + return ACTIVE_CAMERA_COLOR; + if (parts.model.isVideoFeedType(record.camera.feedType)) + return LIVE_CAMERA_COLOR; + return IDLE_CAMERA_COLOR; + } + const { focusNowMs, getFocusTarget, @@ -58,10 +68,7 @@ export function createRendering({ ), cameraDistanceFor: (position) => Cesium.Cartesian3.distance(camera.positionWC, position), - baseColorFor: (record) => - record.camera.id === layerState._activeCameraId - ? ACTIVE_CAMERA_COLOR - : IDLE_CAMERA_COLOR, + baseColorFor: cameraColorFor, }); layerState._activeFocusStyleCount = result.activeCount; } @@ -208,9 +215,7 @@ export function createRendering({ for (const record of layerState._records) { const isActive = record.camera.id === activeId; if (record.billboard) { - record.billboard.color = isActive - ? ACTIVE_CAMERA_COLOR - : IDLE_CAMERA_COLOR; + record.billboard.color = cameraColorFor(record); record.billboard.scale = isActive ? 1.25 : 1.0; // disableDepthTestDistance stays POSITIVE_INFINITY for every billboard // (set at creation) — see the field-test far-zoom submerge fix there. From 548e7f23d118f82b66bbccb30fb2a9c81c4a8993 Mon Sep 17 00:00:00 2001 From: Daniel Slay Date: Sat, 17 Oct 2026 15:10:49 -0400 Subject: [PATCH 2/5] docs(changelog): credit the live camera color --- changelog.d/1200-cctv-live-icon-color.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 changelog.d/1200-cctv-live-icon-color.md diff --git a/changelog.d/1200-cctv-live-icon-color.md b/changelog.d/1200-cctv-live-icon-color.md new file mode 100644 index 000000000..5f39fad1f --- /dev/null +++ b/changelog.d/1200-cctv-live-icon-color.md @@ -0,0 +1,5 @@ +- Color CCTV cameras with live video violet. Cameras with an HLS, MP4 or WebM + feed were the same cyan as stills, so a live DelDOT or Statens vegvesen + camera could only be found by opening it. Stills stay cyan and the active + camera stays amber, and focus de-emphasis fades the violet the same way + (Danielslay86, #1200). From 8e635c9a3ab2d925b102d035fab786a632ad772c Mon Sep 17 00:00:00 2001 From: Daniel Slay Date: Mon, 19 Oct 2026 10:50:22 -0400 Subject: [PATCH 3/5] test(cctv): pin the camera icon colors; say video feed, not live --- changelog.d/1200-cctv-live-icon-color.md | 10 +++---- docs/CURRENT-STATE.md | 2 +- src/data/cctv.test.mjs | 36 ++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 6 deletions(-) diff --git a/changelog.d/1200-cctv-live-icon-color.md b/changelog.d/1200-cctv-live-icon-color.md index 5f39fad1f..c17c1760d 100644 --- a/changelog.d/1200-cctv-live-icon-color.md +++ b/changelog.d/1200-cctv-live-icon-color.md @@ -1,5 +1,5 @@ -- Color CCTV cameras with live video violet. Cameras with an HLS, MP4 or WebM - feed were the same cyan as stills, so a live DelDOT or Statens vegvesen - camera could only be found by opening it. Stills stay cyan and the active - camera stays amber, and focus de-emphasis fades the violet the same way - (Danielslay86, #1200). +- Color CCTV cameras with a video feed violet. Cameras with an HLS, MP4 or + WebM feed were the same cyan as stills, so a DelDOT or Statens vegvesen + video camera could only be found by opening it. Stills stay cyan and the + active camera stays amber, and focus de-emphasis fades the violet the same + way (Danielslay86, #1200). \ No newline at end of file diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 734e722d7..fb3291080 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -4359,7 +4359,7 @@ silently demoting every later lookup for the session. ground clamp now lifts the cap _center_ only so the wireframe stays a true pyramid welded to the plane (was a flattened fan / the ~47.5 m divergence — RESOLVED), re-selecting the active camera is a no-op (killed a click-flash), and texture swaps gate on canvas content (killed a - periodic white flash). Camera icons are cyan for stills, violet for cameras with a live video feed, and amber for the active camera. Coverage is now **metro-wide: 250 cameras** (`CCTV_AUSTIN_MAX_SOURCES` + periodic white flash). Camera icons are cyan for stills, violet for cameras with a video feed, and amber for the active camera. Coverage is now **metro-wide: 250 cameras** (`CCTV_AUSTIN_MAX_SOURCES` default 36 → 250, hard bound 300), filtered to `camera_status === TURNED_ON` (~815 live of 1,003 rows). City packs (2026-07-04): Caltrans (districts 4/7/11/3 — SF, LA, San Diego, Sacramento; cap 300) and TfL London JamCams (cap 250) join Austin (cap 250) as keyless default diff --git a/src/data/cctv.test.mjs b/src/data/cctv.test.mjs index ca6a3c11a..e102b081d 100644 --- a/src/data/cctv.test.mjs +++ b/src/data/cctv.test.mjs @@ -77,6 +77,11 @@ import { CCTV_FOCUS_REQUEST_EVENT, activateCctvCameraFromWorldClick, } from '../cctvFocusRequest.js'; +import { + ACTIVE_CAMERA_COLOR, + IDLE_CAMERA_COLOR, + LIVE_CAMERA_COLOR, +} from '../layers/cctv/policy.js' const CCTV_PRESENTATION_SOURCE = fs.readFileSync( path.join( @@ -360,6 +365,37 @@ test('default COVERAGE refresh materializes the active and visible camera frustu } }); +test('camera icons are violet for video feeds, cyan for stills, amber when active', () => { + const record = (id, feedType) => ({ + camera: { ...UNCLAMPED_CAMERA, id, feedType, groundElevationM: 0 }, + billboard: {}, + coverageEntities: [], + projection: null, + }); + const idleVideo = record('idle-hls', 'hls'); + const idleStill = record('idle-image', 'image'); + const activeVideo = record('active-hls', 'hls'); + const viewer = { entities: { add: (entity) => entity } }; + + try { + // Disabled keeps the active video camera from opening a real projection; + // icon colors are assigned either way. + _setCctvCoverageStateForTest({ + viewer, + records: [idleVideo, idleStill, activeVideo], + activeCameraId: activeVideo.camera.id, + enabled: false, + coverageMode: 'off', + }); + refreshCoverageStyles(); + assert.equal(idleVideo.billboard.color, LIVE_CAMERA_COLOR); + assert.equal(idleStill.billboard.color, IDLE_CAMERA_COLOR); + assert.equal(activeVideo.billboard.color, ACTIVE_CAMERA_COLOR); + } finally { + _setCctvCoverageStateForTest({ enabled: false }); + } +}); + test('geometry drain pacing yields to tracked and cockpit camera ownership', () => { assert.deepEqual(cctvGeometryDrainPacing(), { batchSize: 4, delayMs: 120 }); assert.deepEqual( From d80683c44b9ea2eb8856b0f3fb8b0977a8e8b28f Mon Sep 17 00:00:00 2001 From: Said Turksever Date: Mon, 19 Oct 2026 16:05:38 +0100 Subject: [PATCH 4/5] docs(changelog): end the #1200 fragment with a newline Co-Authored-By: Claude Opus 5.5 --- changelog.d/1200-cctv-live-icon-color.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/1200-cctv-live-icon-color.md b/changelog.d/1200-cctv-live-icon-color.md index c17c1760d..30d67e1d4 100644 --- a/changelog.d/1200-cctv-live-icon-color.md +++ b/changelog.d/1200-cctv-live-icon-color.md @@ -2,4 +2,4 @@ WebM feed were the same cyan as stills, so a DelDOT or Statens vegvesen video camera could only be found by opening it. Stills stay cyan and the active camera stays amber, and focus de-emphasis fades the violet the same - way (Danielslay86, #1200). \ No newline at end of file + way (Danielslay86, #1200). From 29fb756a5c4fcb590f20ca02da343129c03e97d2 Mon Sep 17 00:00:00 2001 From: Said Turksever Date: Mon, 19 Oct 2026 16:50:21 +0100 Subject: [PATCH 5/5] test(cctv): cover the focus de-emphasis icon colors refreshCctvFocusStyles passes cameraColorFor as baseColorFor, but only refreshCoverageStyles was tested. The new rendering.test.mjs runs the focus pass through createRendering with stubbed focus services and checks that an idle video camera fades from violet, a still from cyan and the active camera from amber. It fails if the focus callback goes back to the old active/idle check. Also adds the missing semicolon on the policy.js import in cctv.test.mjs. Co-Authored-By: Claude Opus 5.5 --- scripts/format-scope.json | 1 + src/data/cctv.test.mjs | 2 +- src/layers/cctv/rendering.test.mjs | 73 ++++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 src/layers/cctv/rendering.test.mjs diff --git a/scripts/format-scope.json b/scripts/format-scope.json index 4230c23ae..2d2ddb361 100644 --- a/scripts/format-scope.json +++ b/scripts/format-scope.json @@ -557,6 +557,7 @@ "src/data/cctvNswSource.test.mjs", "src/data/cctvTxdotSource.test.mjs", "src/layers/cctv/source.test.mjs", + "src/layers/cctv/rendering.test.mjs", "src/data/alprCameras.js", "src/data/alprCameras.test.mjs", "src/data/dataCredits.test.mjs", diff --git a/src/data/cctv.test.mjs b/src/data/cctv.test.mjs index e102b081d..dafaeb5f6 100644 --- a/src/data/cctv.test.mjs +++ b/src/data/cctv.test.mjs @@ -81,7 +81,7 @@ import { ACTIVE_CAMERA_COLOR, IDLE_CAMERA_COLOR, LIVE_CAMERA_COLOR, -} from '../layers/cctv/policy.js' +} from '../layers/cctv/policy.js'; const CCTV_PRESENTATION_SOURCE = fs.readFileSync( path.join( diff --git a/src/layers/cctv/rendering.test.mjs b/src/layers/cctv/rendering.test.mjs new file mode 100644 index 000000000..c4b490dd2 --- /dev/null +++ b/src/layers/cctv/rendering.test.mjs @@ -0,0 +1,73 @@ +// src/layers/cctv/rendering.test.mjs +// Icon colors under focus de-emphasis (#1200): the focus pass fades each icon +// from the same base color the coverage refresh assigns, so a video-feed camera +// stays violet while it dims instead of falling back to the idle cyan. +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { createRendering } from './rendering.js'; +import { + ACTIVE_CAMERA_COLOR, + IDLE_CAMERA_COLOR, + LIVE_CAMERA_COLOR, +} from './policy.js'; +import { isVideoFeedType } from '../../sources/cctvTypes.js'; + +const FOCUS_FACTOR = 0.5; + +function assertFadedFrom(actual, base, label) { + assert.ok(actual, `${label}: color written`); + assert.deepEqual( + [actual.red, actual.green, actual.blue], + [base.red, base.green, base.blue], + `${label}: hue`, + ); + assert.equal(actual.alpha, base.alpha * FOCUS_FACTOR, `${label}: alpha`); +} + +test('focus de-emphasis fades each icon from its own color', () => { + const record = (id, feedType) => ({ + camera: { id, feedType }, + // No position, so the pass skips screen projection; the focus stub below + // decides the factor on its own. + billboard: { show: true }, + }); + const idleVideo = record('idle-hls', 'hls'); + const idleStill = record('idle-image', 'image'); + const activeVideo = record('active-hls', 'hls'); + const state = { + _enabled: true, + _viewer: { scene: {}, camera: {} }, + _records: [idleVideo, idleStill, activeVideo], + _activeCameraId: activeVideo.camera.id, + _activeFocusStyleCount: 0, + _lastFocusStyleAt: -Infinity, + }; + const focus = { + focusNowMs: (nowMs) => nowMs, + getFocusTarget: () => ({}), + focusPassIsNeeded: () => true, + advanceSpriteFocus: () => ({ + factor: FOCUS_FACTOR, + active: true, + transitioning: false, + }), + focusAlphaNeedsWrite: () => true, + }; + const { refreshCctvFocusStyles } = createRendering({ + state, + services: { focus }, + parts: { model: { isVideoFeedType } }, + source: {}, + }); + + refreshCctvFocusStyles(1000); + + assertFadedFrom(idleVideo.billboard.color, LIVE_CAMERA_COLOR, 'idle video'); + assertFadedFrom(idleStill.billboard.color, IDLE_CAMERA_COLOR, 'idle still'); + assertFadedFrom( + activeVideo.billboard.color, + ACTIVE_CAMERA_COLOR, + 'active video', + ); + assert.equal(state._activeFocusStyleCount, 3); +});