diff --git a/changelog.d/1441-cctv-heading-north.md b/changelog.d/1441-cctv-heading-north.md new file mode 100644 index 000000000..d65e5c2d8 --- /dev/null +++ b/changelog.d/1441-cctv-heading-north.md @@ -0,0 +1,3 @@ +- Show a CCTV camera facing 359.5° or more as HDG 0°. The HUD heading token + and the camera panel rounded the bearing without wrapping, so a camera + facing 359.6° read "HDG 360°" (Aditya, @Rohilalala, #1441). diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 24fda95a1..500e6c406 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -4930,7 +4930,9 @@ silently demoting every later lookup for the session. wireframe dashed; colours, widths and active/idle emphasis are unchanged. A manual calibration (`calSource: 'manual'`) or curated pose (`poseSource: 'curated'`) is never presented as estimated, matching the CAL badge. Public camera state carries `headingConfidence` and - `headingEstimated` (`src/layers/cctv/headingConfidence.js`, #639). + `headingEstimated` (`src/layers/cctv/headingConfidence.js`, #639). The HUD token and the + camera panel's meta line wrap the bearing after rounding to whole degrees, so a camera + facing 359.5° or more reads `HDG 0°` rather than `HDG 360°`. - **CCTV identified hardware:** a camera whose source or seed names a hardware `model` that resolves in `src/layers/cctv/modelSpecs.js` (vendored CC0 records from the CCTV Camera Database) gets the model's specs appended to the HUD meta line. Only fixed single-lens diff --git a/src/layers/cctv/headingConfidence.js b/src/layers/cctv/headingConfidence.js index 26a877558..fad0867de 100644 --- a/src/layers/cctv/headingConfidence.js +++ b/src/layers/cctv/headingConfidence.js @@ -44,7 +44,8 @@ export function isHeadingEstimated(camera) { * @returns {string} */ export function headingHudToken(camera) { - const hdg = Math.round(Number(camera?.headingDeg) || 0); + // 359.5° and above rounds to north; modulo keeps it from printing 360°. + const hdg = Math.round(Number(camera?.headingDeg) || 0) % 360; return isHeadingEstimated(camera) ? t('HDG {value}° (ESTIMATED)', { value: hdg }) : t('HDG {value}°', { value: hdg }); diff --git a/src/layers/cctv/headingConfidence.test.mjs b/src/layers/cctv/headingConfidence.test.mjs index 513bf0d1b..3cf673861 100644 --- a/src/layers/cctv/headingConfidence.test.mjs +++ b/src/layers/cctv/headingConfidence.test.mjs @@ -44,6 +44,15 @@ test('HUD token tags a synthetic bearing and rounds the degrees', () => { ); }); +test('HUD token reads a bearing that rounds to north as 0°, not 360°', () => { + assert.equal(headingHudToken({ headingDeg: 359.4 }), 'HDG 359°'); + assert.equal(headingHudToken({ headingDeg: 359.6 }), 'HDG 0°'); + assert.equal( + headingHudToken({ headingDeg: 359.5, headingConfidence: 'low' }), + 'HDG 0° (ESTIMATED)', + ); +}); + test('HUD token stays untagged for surveyed and calibrated bearings', () => { assert.equal( headingHudToken({ headingDeg: 67.5, headingConfidence: 'high' }), diff --git a/src/ui/cctvPanel.test.mjs b/src/ui/cctvPanel.test.mjs index 1675d6119..a3dbef786 100644 --- a/src/ui/cctvPanel.test.mjs +++ b/src/ui/cctvPanel.test.mjs @@ -490,3 +490,26 @@ test('the scene summary types in, then holds the whole text', async (t) => { await nextTick(); assert.equal(byId('cctv-summary').textContent, text); }); + +test('the panel meta line reads a bearing that rounds to north as HDG 0°', () => { + const meta = (headingDeg) => { + const camera = { + id: 'a', + city: 'Austin', + headingDeg, + fovDeg: 74, + rangeM: 700, + }; + return presentCctvPanel( + { + enabled: true, + activeCameraId: camera.id, + activeCamera: camera, + cameras: [camera], + }, + true, + ).meta; + }; + assert.match(meta(359.4), /· HDG 359° ·/); + assert.match(meta(359.6), /· HDG 0° ·/); +}); diff --git a/src/ui/cctvPresentation.js b/src/ui/cctvPresentation.js index c5daf7163..cd298e108 100644 --- a/src/ui/cctvPresentation.js +++ b/src/ui/cctvPresentation.js @@ -107,7 +107,7 @@ function metaText(state, enabled, activeCamera, catalogCount) { '{city} · HDG {heading} · FOV {fov} · RANGE {range}m · {projection}{calBadge} · {provider}{status}', { city: activeCamera.city, - heading: `${Math.round(activeCamera.headingDeg)}°`, + heading: `${Math.round(activeCamera.headingDeg) % 360}°`, fov: `${Math.round(activeCamera.fovDeg)}°`, range: Math.round(activeCamera.rangeM), projection: projLabel,