fix(cctv): surface heading confidence — estimated bearings are now visible

Every pack sets headingConfidence on each camera, but nothing consumed it:
a bearing synthesized by fallbackHeadingFromId (a hash of the id string,
no geographic input) rendered exactly like a surveyed one — 2,827 of 3,997
cameras in a default catalog. Fixes #639.

Two consumers, plus a passthrough:

- HUD: the heading token is tagged when the bearing is synthetic —
  `HDG 194° (ESTIMATED)` — so a hash never reads as a measured facing.
- Wireframe: a low-confidence camera draws its coverage frustum with dashed
  polylines instead of solid ones. Colors, widths, and the active/idle
  emphasis are unchanged; the dash is the only signal.
- Public camera state gains `headingConfidence` (the raw pack flag) and
  `headingEstimated` (the derived bit) so UI consumers never re-derive it.

A human-vouched pose always wins over the pack flag: a manually saved
calibration (calSource:'manual') or a curated catalog entry
(poseSource:'curated') is never presented as estimated, mirroring the CAL
badge states in calibration.js.

- src/layers/cctv/headingConfidence.js: pure helpers (isHeadingEstimated,
  headingHudToken)
- src/layers/cctv/headingConfidence.test.mjs: confidence normalization,
  calibration/curation overrides, HUD token formatting
- src/layers/cctv/presentation.js: HUD token + public-state passthrough
- src/layers/cctv/rendering.js: dashed materials for estimated bearings
This commit is contained in:
bassem chagra committed 2026-09-17 10:20:26 +02:00
1 parent 0d41b6be54
commit 5f27f6edf4
4 files changed
+144 -15

No files matched your search

+47
View File
@@ -0,0 +1,47 @@
/**
* @module headingConfidence
*
* Consumers for the catalog's per-camera `headingConfidence` flag (#639).
*
* Packs whose feed publishes no facing get a synthetic bearing from
* `fallbackHeadingFromId` — a hash of the id string, with no geographic
* input — and are marked `headingConfidence: 'low'`. Until now nothing read
* the flag, so a guessed bearing rendered exactly like a surveyed one. These
* helpers make the guess visible: the HUD heading token gains an
* `(ESTIMATED)` tag and the coverage wireframe draws dashed instead of solid.
*
* A human-vouched pose always wins over the pack flag: a manually saved
* calibration (`calSource:'manual'`) or a hand-authored catalog entry
* (`poseSource:'curated'`) is never presented as estimated, mirroring the
* CAL badge states in calibration.js.
*/
/**
* Whether a camera's bearing is a synthetic guess that should present as
* provisional.
* @param {{headingConfidence?: string|null, calSource?: string|null,
* poseSource?: string|null}} camera - Catalog camera (or public state).
* @returns {boolean}
*/
export function isHeadingEstimated(camera) {
if (!camera) return false;
if (camera.calSource === 'manual') return false;
if (camera.poseSource === 'curated') return false;
return (
String(camera.headingConfidence || '')
.trim()
.toLowerCase() === 'low'
);
}
/**
* HUD heading token: `HDG 194°`, tagged `(ESTIMATED)` when the bearing is
* synthetic — so a hash can never read as a surveyed facing.
* @param {{headingDeg?: number, headingConfidence?: string|null,
* calSource?: string|null, poseSource?: string|null}} camera
* @returns {string}
*/
export function headingHudToken(camera) {
const hdg = Math.round(Number(camera?.headingDeg) || 0);
return `HDG ${hdg}°${isHeadingEstimated(camera) ? ' (ESTIMATED)' : ''}`;
}