fix(cctv): show a camera facing 359.6° as HDG 0°, not HDG 360°

The CCTV HUD heading token and the camera panel's meta line rounded the
bearing without wrapping, so a camera facing 359.5° or more (a feed
bearing, or a heading calibrated just west of north) read "HDG 360°".
Both now wrap after rounding: 359.6° reads "HDG 0°", 359.4° "HDG 359°".
This commit is contained in:
Aditya committed 2026-10-28 15:06:07 +05:30
1 parent a4d3701c3c
commit 940050d4e5
5 files changed
+39 -3

No files matched your search

+3 -1
View File
@@ -4788,7 +4788,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
+2 -1
View File
@@ -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 });
@@ -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' }),
+24
View File
@@ -47,6 +47,30 @@ function fixture(t) {
return { controls, requests };
}
test('the panel meta line reads a bearing that rounds to north as HDG 0°', (t) => {
const { controls } = fixture(t);
controls.actions.setPanelCollapsed = () => {};
controls._cctvMeta = element();
const meta = (headingDeg) => {
const camera = {
id: 'a',
city: 'Austin',
headingDeg,
fovDeg: 74,
rangeM: 700,
};
controls._renderCctvState({
enabled: true,
activeCameraId: camera.id,
activeCamera: camera,
cameras: [camera],
});
return controls._cctvMeta.textContent;
};
assert.match(meta(359.4), /· HDG 359° ·/);
assert.match(meta(359.6), /· HDG 0° ·/);
});
test('a late image completion cannot replace a newer camera preview', (t) => {
const { controls, requests } = fixture(t);
controls._queueCctvFrame('first.jpg', 'a', true);
+1 -1
View File
@@ -171,7 +171,7 @@ export function _renderCctvState(state) {
'{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,