mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 02:14:01 +00:00
Merge pull request #643 from ch-bas/fix/heading-confidence-consumers
fix(cctv): surface heading confidence — estimated bearings are now visible (fixes #639)
This commit is contained in:
7 files changed
+159
-15
No files matched your search
@@ -1,5 +1,11 @@
|
||||
# Changelog
|
||||
|
||||
- CCTV cameras whose bearing is a guess now say so. Packs mark bearings derived
|
||||
from a hash of the camera id as `headingConfidence: 'low'`, but nothing read the
|
||||
flag, so roughly 70% of a default catalog rendered like surveyed facings. The HUD
|
||||
now reads `HDG n° (ESTIMATED)` and the coverage wireframe draws dashed; manual
|
||||
calibrations and curated poses are never marked estimated (bassem chagra, #643).
|
||||
|
||||
- Report which upstream declined a Street Traffic road load. The layer row now
|
||||
reads `Overpass rate-limited`, `Overpass timed out`, or
|
||||
`Overpass refused the road query (HTTP 406)` instead of a general
|
||||
|
||||
@@ -3384,6 +3384,12 @@ silently demoting every later lookup for the session.
|
||||
- **The DENSE chip reports the dense LOAD, not the catalog param.** The param flips synchronously while the Starlink shell takes seconds to arrive over a chunked load, and CelesTrak 502s that feed regularly. So the chip reads `DENSE ···` (busy, disabled) while loading, ACTIVE only once dense points are actually on screen, and `DENSE ✕` with the reason on hover when the load fails — a failure also reverts `catalog` to `core`, drops any partial chunk, and leaves the chip clickable to retry. A load is judged by points added, not by HTTP status: a 200 carrying an empty body, a passed-through HTML error page, or only TLEs the core catalog already owns fails with the same revert semantics as a 502. Any explicit request for `core` clears a latched error even when the mode does not change, so a Space Missions restore of an already-core snapshot never leaves the user with a failure they did not cause. Because the load settles asynchronously, the layer pushes a re-render through the optional `setRowControlsListener()` hook; nothing else would repaint that row before the 5-minute catalog refresh, so the count and legend would otherwise sit stale.
|
||||
- **A dependency owner takes the row with it.** Space Missions borrows this layer for TLE lookup with `showPoints:false`; while points are hidden the layer returns empty row controls, so the legend never describes an empty sky and the chip cannot accept a write that the owner's restore would silently revert.
|
||||
- The detection-overlay record cache (`_detectionObjects`) is cleared with the catalog on every rebuild: it stamps id/class at creation only, and a rebuild can re-tag a satellite when a partial CelesTrak outage changes which group wins dedupe.
|
||||
- **CCTV estimated bearings:** a camera whose pack marks `headingConfidence: 'low'` (the id-hash
|
||||
`fallbackHeadingFromId` bearing) shows `HDG n° (ESTIMATED)` in the HUD and draws its coverage
|
||||
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).
|
||||
- **FIRMS**: no ground clamping (zero 3D-tiles height sampling), ≤18 screen-decluttered ambient labels, click-to-inspect detail card, 2.5k/3k sprite budgets viewport-clipped by FRP.
|
||||
- **CCTV v2 foundation:** a pitched
|
||||
frustum wireframe (4 corner rays + far-cap rectangle) with a monitor plane at the frustum's
|
||||
|
||||
@@ -1072,6 +1072,7 @@
|
||||
"src/layers/cctv/geometry.js",
|
||||
"src/layers/cctv/geometryQueue.js",
|
||||
"src/layers/cctv/ground.js",
|
||||
"src/layers/cctv/headingConfidence.js",
|
||||
"src/layers/cctv/health.js",
|
||||
"src/layers/cctv/hover.js",
|
||||
"src/layers/cctv/index.js",
|
||||
@@ -1488,6 +1489,7 @@
|
||||
"src/layers/cctv/geometry.js",
|
||||
"src/layers/cctv/geometryQueue.js",
|
||||
"src/layers/cctv/ground.js",
|
||||
"src/layers/cctv/headingConfidence.js",
|
||||
"src/layers/cctv/health.js",
|
||||
"src/layers/cctv/hover.js",
|
||||
"src/layers/cctv/index.js",
|
||||
@@ -2127,6 +2129,7 @@
|
||||
"src/layers/cctv/geometry.js",
|
||||
"src/layers/cctv/geometryQueue.js",
|
||||
"src/layers/cctv/ground.js",
|
||||
"src/layers/cctv/headingConfidence.js",
|
||||
"src/layers/cctv/health.js",
|
||||
"src/layers/cctv/hover.js",
|
||||
"src/layers/cctv/index.js",
|
||||
|
||||
@@ -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)' : ''}`;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
// src/layers/cctv/headingConfidence.test.mjs
|
||||
// Bearing-provenance consumers (#639): a pack's headingConfidence:'low'
|
||||
// (a hashed synthetic bearing) must present as estimated — unless a human
|
||||
// vouched for the pose via manual calibration or a curated catalog entry.
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { isHeadingEstimated, headingHudToken } from './headingConfidence.js';
|
||||
|
||||
test('low confidence marks the bearing estimated', () => {
|
||||
assert.equal(isHeadingEstimated({ headingConfidence: 'low' }), true);
|
||||
});
|
||||
|
||||
test('confidence matching is trimmed and case-insensitive', () => {
|
||||
assert.equal(isHeadingEstimated({ headingConfidence: ' LOW ' }), true);
|
||||
assert.equal(isHeadingEstimated({ headingConfidence: 'Low' }), true);
|
||||
});
|
||||
|
||||
test('medium, high, missing, and absent cameras are not estimated', () => {
|
||||
assert.equal(isHeadingEstimated({ headingConfidence: 'medium' }), false);
|
||||
assert.equal(isHeadingEstimated({ headingConfidence: 'high' }), false);
|
||||
assert.equal(isHeadingEstimated({}), false);
|
||||
assert.equal(isHeadingEstimated(null), false);
|
||||
assert.equal(isHeadingEstimated(undefined), false);
|
||||
});
|
||||
|
||||
test('a manually saved calibration overrides a low pack flag', () => {
|
||||
assert.equal(
|
||||
isHeadingEstimated({ headingConfidence: 'low', calSource: 'manual' }),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test('a curated catalog entry overrides a low pack flag', () => {
|
||||
assert.equal(
|
||||
isHeadingEstimated({ headingConfidence: 'low', poseSource: 'curated' }),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test('HUD token tags a synthetic bearing and rounds the degrees', () => {
|
||||
assert.equal(
|
||||
headingHudToken({ headingDeg: 194.4, headingConfidence: 'low' }),
|
||||
'HDG 194° (ESTIMATED)',
|
||||
);
|
||||
});
|
||||
|
||||
test('HUD token stays untagged for surveyed and calibrated bearings', () => {
|
||||
assert.equal(
|
||||
headingHudToken({ headingDeg: 67.5, headingConfidence: 'high' }),
|
||||
'HDG 68°',
|
||||
);
|
||||
assert.equal(
|
||||
headingHudToken({
|
||||
headingDeg: 225,
|
||||
headingConfidence: 'low',
|
||||
calSource: 'manual',
|
||||
}),
|
||||
'HDG 225°',
|
||||
);
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
import { CCTV_AMBIENT_CARD_MAX } from '../../data/cctvLod.js';
|
||||
import { ACTIVE_FRAME_REFRESH_MS, IDLE_FRAME_REFRESH_MS } from './policy.js';
|
||||
import { headingHudToken, isHeadingEstimated } from './headingConfidence.js';
|
||||
|
||||
export function createPresentation({
|
||||
state: layerState,
|
||||
@@ -35,7 +36,9 @@ export function createPresentation({
|
||||
return [
|
||||
`${active.camera.city.toUpperCase()} CCTV`,
|
||||
`${active.camera.name.toUpperCase()}`,
|
||||
`HDG ${Math.round(active.camera.headingDeg)}°`,
|
||||
// A synthetic bearing (headingConfidence 'low', no human calibration)
|
||||
// is tagged so a hashed guess never reads as a surveyed facing (#639).
|
||||
headingHudToken(active.camera),
|
||||
`FOV ${Math.round(active.camera.fovDeg)}°`,
|
||||
`COVERAGE ${area.toFixed(2)}km²`,
|
||||
overlapCount > 0 ? `OVERLAP ${overlapCount} cams` : 'ISOLATED VIEW',
|
||||
@@ -76,6 +79,11 @@ export function createPresentation({
|
||||
lat: camera.lat,
|
||||
lon: camera.lon,
|
||||
headingDeg: camera.headingDeg,
|
||||
// Bearing provenance (#639): the pack's confidence flag plus the derived
|
||||
// "is this a synthetic guess" bit (calibration-aware), so UI consumers
|
||||
// never have to re-derive it.
|
||||
headingConfidence: camera.headingConfidence || null,
|
||||
headingEstimated: isHeadingEstimated(camera),
|
||||
pitchDeg: camera.pitchDeg,
|
||||
fovDeg: camera.fovDeg,
|
||||
rangeM: camera.rangeM,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as Cesium from 'cesium';
|
||||
import { horizonOccluder } from '../../data/iconOrientation.js';
|
||||
import { isHeadingEstimated } from './headingConfidence.js';
|
||||
import {
|
||||
ACTIVE_CAMERA_COLOR,
|
||||
IDLE_CAMERA_COLOR,
|
||||
@@ -233,6 +234,15 @@ export function createRendering({
|
||||
);
|
||||
|
||||
const inVisibleSet = coverageVisible.has(record.camera.id);
|
||||
// A synthetic bearing (headingConfidence 'low', no human calibration)
|
||||
// draws its wireframe dashed so a guessed facing is visibly provisional
|
||||
// rather than rendering identically to a surveyed one (#639). Colors,
|
||||
// widths, and the active/idle emphasis are unchanged.
|
||||
const bearingEstimated = isHeadingEstimated(record.camera);
|
||||
const lineMaterial = (color) =>
|
||||
bearingEstimated
|
||||
? new Cesium.PolylineDashMaterialProperty({ color })
|
||||
: color;
|
||||
for (const entity of record.coverageEntities || []) {
|
||||
// The frustum wireframe is part of the projection representation —
|
||||
// force it on for the active camera and let it read through geometry
|
||||
@@ -247,13 +257,15 @@ export function createRendering({
|
||||
// active camera keeps its width/alpha emphasis in both schemes.
|
||||
const hue = viewshedOn ? record.viewshedColors : null;
|
||||
if (entity._coverageRole === 'cap') {
|
||||
entity.polyline.material = hue
|
||||
? isActive
|
||||
? hue.lineActive
|
||||
: hue.line
|
||||
: isActive
|
||||
? ACTIVE_COVERAGE_CENTER
|
||||
: IDLE_COVERAGE_CENTER_MUTED;
|
||||
entity.polyline.material = lineMaterial(
|
||||
hue
|
||||
? isActive
|
||||
? hue.lineActive
|
||||
: hue.line
|
||||
: isActive
|
||||
? ACTIVE_COVERAGE_CENTER
|
||||
: IDLE_COVERAGE_CENTER_MUTED,
|
||||
);
|
||||
entity.polyline.width = isActive ? 2.2 : 1.0;
|
||||
entity.polyline.depthFailMaterial = planeShowing
|
||||
? hue
|
||||
@@ -261,13 +273,15 @@ export function createRendering({
|
||||
: ACTIVE_COVERAGE_CENTER_DEPTHFAIL
|
||||
: undefined;
|
||||
} else {
|
||||
entity.polyline.material = hue
|
||||
? isActive
|
||||
? hue.lineActive
|
||||
: hue.line.withAlpha(0.6)
|
||||
: isActive
|
||||
? ACTIVE_COVERAGE_EDGE
|
||||
: IDLE_COVERAGE_EDGE_MUTED;
|
||||
entity.polyline.material = lineMaterial(
|
||||
hue
|
||||
? isActive
|
||||
? hue.lineActive
|
||||
: hue.line.withAlpha(0.6)
|
||||
: isActive
|
||||
? ACTIVE_COVERAGE_EDGE
|
||||
: IDLE_COVERAGE_EDGE_MUTED,
|
||||
);
|
||||
entity.polyline.width = isActive ? 1.8 : 0.9;
|
||||
entity.polyline.depthFailMaterial = planeShowing
|
||||
? hue
|
||||
|
||||
Reference in new issue
Block a user