mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 18:34:02 +00:00
The CCTV layer kept every loaded camera live: each view change touched every icon, every notification rebuilt every camera's state, and the picker listed the whole catalog. With the default 4,000 cameras, a continental drag ran at 152-179 fps with 18-24 frames over 30 ms (worst 112 ms). It now runs at 235 fps with none. The layer now: - shows icons only on or near the screen, and when zoomed out keeps one camera per world-anchored cell (healthy feeds first), so panning keeps the same cameras; - grounds and draws coverage only for the cameras in view, the active camera and its coverage neighbours, retrying a camera at most twice per view regime; - reruns the view pass while the camera moves, paced by its own cost, and projects with one matrix per pass instead of a Cesium call per camera; - keeps the billboard buffer-usage hint fixed, so showing or hiding a few icons no longer rewrites the vertex data of every billboard; - lists the cameras in view (active camera first) in the picker, closing with the count of cameras outside the view, and builds the full camera list in the UI state only when it is read.
252 lines
7.5 KiB
JavaScript
252 lines
7.5 KiB
JavaScript
// src/data/cctvViewThinning.test.mjs — the CCTV view pass: only cameras on or
|
|
// near the screen show, a zoomed-out view keeps one camera per world-anchored
|
|
// cell, and the layer's UI state never rebuilds every camera for a listener
|
|
// that does not read the list.
|
|
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import * as Cesium from 'cesium';
|
|
import cctvLayer, {
|
|
_setCctvCoverageStateForTest,
|
|
_viewRecordIdsForTest,
|
|
refreshHorizonCulling,
|
|
} from './cctv.js';
|
|
|
|
const WIDTH = 800;
|
|
const HEIGHT = 600;
|
|
|
|
/**
|
|
* Records at the given [lon, lat] degrees near the origin, each drawn at the
|
|
* screen point `screenAt` gives it (default: the middle of the screen).
|
|
*/
|
|
function recordsAt(
|
|
lonLats,
|
|
screenAt = () => ({ x: WIDTH / 2, y: HEIGHT / 2 }),
|
|
) {
|
|
const screenByPosition = new Map();
|
|
const records = lonLats.map(([lon, lat], index) => {
|
|
const position = Cesium.Cartesian3.fromDegrees(lon, lat, 10);
|
|
screenByPosition.set(position, screenAt(index));
|
|
return {
|
|
camera: {
|
|
id: `cam-${index}`,
|
|
name: `Camera ${index}`,
|
|
city: 'Test',
|
|
lat,
|
|
lon,
|
|
headingDeg: 0,
|
|
pitchDeg: -18,
|
|
fovDeg: 44,
|
|
rangeM: 145,
|
|
mountHeightM: 8,
|
|
},
|
|
position,
|
|
billboard: { position, show: true },
|
|
coverageEntities: [],
|
|
projection: null,
|
|
};
|
|
});
|
|
return { records, screenByPosition };
|
|
}
|
|
|
|
function viewerAt(heightM, screenByPosition, shiftX = 0) {
|
|
return {
|
|
isDestroyed: () => false,
|
|
camera: {
|
|
positionWC: Cesium.Cartesian3.fromDegrees(0, 0, heightM),
|
|
positionCartographic: { latitude: 0, longitude: 0, height: heightM },
|
|
frustum: { fovy: Math.PI / 3 },
|
|
},
|
|
scene: {
|
|
canvas: { clientWidth: WIDTH, clientHeight: HEIGHT },
|
|
cartesianToCanvasCoordinates: (position) => {
|
|
const screen = screenByPosition.get(position);
|
|
return screen && { x: screen.x + shiftX, y: screen.y };
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
function shownIds(records) {
|
|
return records
|
|
.filter((record) => record.billboard.show)
|
|
.map((record) => record.camera.id)
|
|
.sort();
|
|
}
|
|
|
|
// Three cameras within ~110 m, and one ~22 km away.
|
|
const CLUSTER = [
|
|
[0, 0],
|
|
[0.001, 0],
|
|
[0.0005, 0.0005],
|
|
[0.2, 0],
|
|
];
|
|
|
|
test('zoomed out, cameras sharing a ground cell show one icon', () => {
|
|
const { records, screenByPosition } = recordsAt(CLUSTER);
|
|
_setCctvCoverageStateForTest({
|
|
viewer: viewerAt(20_000, screenByPosition),
|
|
records,
|
|
});
|
|
assert.equal(
|
|
refreshHorizonCulling(),
|
|
true,
|
|
'the first pass changes the view',
|
|
);
|
|
const shown = shownIds(records);
|
|
assert.equal(shown.length, 2, 'one for the cluster, one for the far camera');
|
|
assert.ok(shown.includes('cam-3'), 'a camera alone in its cell shows');
|
|
assert.deepEqual(_viewRecordIdsForTest().sort(), shown);
|
|
});
|
|
|
|
test('a pan keeps the same winners, and an unchanged view keeps its list', () => {
|
|
const { records, screenByPosition } = recordsAt(CLUSTER, (index) => ({
|
|
x: 100 + index * 7,
|
|
y: 300,
|
|
}));
|
|
_setCctvCoverageStateForTest({
|
|
viewer: viewerAt(20_000, screenByPosition),
|
|
records,
|
|
});
|
|
refreshHorizonCulling();
|
|
const before = shownIds(records);
|
|
const list = _viewRecordIdsForTest();
|
|
for (const shiftX of [3, 11, 20, 37]) {
|
|
_setCctvCoverageStateForTest({
|
|
viewer: viewerAt(20_000, screenByPosition, shiftX),
|
|
records,
|
|
});
|
|
refreshHorizonCulling();
|
|
assert.deepEqual(shownIds(records), before, `pan by ${shiftX} px`);
|
|
}
|
|
assert.equal(refreshHorizonCulling(), false, 'the same view changes nothing');
|
|
assert.deepEqual(_viewRecordIdsForTest(), list);
|
|
});
|
|
|
|
test('a small zoom within one cell size keeps the same winners', () => {
|
|
const { records, screenByPosition } = recordsAt(CLUSTER);
|
|
const shownAt = (heightM) => {
|
|
_setCctvCoverageStateForTest({
|
|
viewer: viewerAt(heightM, screenByPosition),
|
|
records,
|
|
});
|
|
refreshHorizonCulling();
|
|
return shownIds(records);
|
|
};
|
|
// Cells come in power-of-two metre steps: about 1.5 km and 1.6 km cells at
|
|
// these heights fall in the same step, so the same cameras win.
|
|
assert.deepEqual(shownAt(20_500), shownAt(20_000));
|
|
});
|
|
|
|
test('a healthy camera wins its cell over an unknown one', () => {
|
|
const { records, screenByPosition } = recordsAt(CLUSTER.slice(0, 2));
|
|
// Whichever camera the id order would pick, the healthy one must win.
|
|
for (const id of ['cam-0', 'cam-1']) {
|
|
_setCctvCoverageStateForTest({
|
|
viewer: viewerAt(20_000, screenByPosition),
|
|
records,
|
|
healthById: new Map([[id, { status: 'ok' }]]),
|
|
});
|
|
refreshHorizonCulling();
|
|
assert.deepEqual(shownIds(records), [id]);
|
|
}
|
|
});
|
|
|
|
test('the active camera always shows, even in a crowded cell', () => {
|
|
const { records, screenByPosition } = recordsAt(CLUSTER.slice(0, 3));
|
|
for (const active of records) {
|
|
_setCctvCoverageStateForTest({
|
|
viewer: viewerAt(20_000, screenByPosition),
|
|
records,
|
|
activeCameraId: active.camera.id,
|
|
});
|
|
refreshHorizonCulling();
|
|
assert.ok(active.billboard.show, `${active.camera.id} shows while active`);
|
|
}
|
|
});
|
|
|
|
test('close up, every camera near the screen shows and far-off ones hide', () => {
|
|
const { records, screenByPosition } = recordsAt(
|
|
[
|
|
[0, 0],
|
|
[0.0001, 0],
|
|
[0.0002, 0],
|
|
],
|
|
(index) =>
|
|
index === 2 ? { x: WIDTH * 3, y: HEIGHT / 2 } : { x: 100, y: 100 },
|
|
);
|
|
_setCctvCoverageStateForTest({
|
|
viewer: viewerAt(300, screenByPosition),
|
|
records,
|
|
});
|
|
refreshHorizonCulling();
|
|
assert.deepEqual(shownIds(records), ['cam-0', 'cam-1']);
|
|
});
|
|
|
|
test('a disabled layer skips the view pass', () => {
|
|
const { records, screenByPosition } = recordsAt(CLUSTER);
|
|
_setCctvCoverageStateForTest({
|
|
viewer: viewerAt(20_000, screenByPosition),
|
|
records,
|
|
enabled: false,
|
|
});
|
|
assert.equal(refreshHorizonCulling(), false);
|
|
assert.deepEqual(_viewRecordIdsForTest(), []);
|
|
});
|
|
|
|
test('UI state builds the full camera list only when it is read', () => {
|
|
const { records, screenByPosition } = recordsAt(CLUSTER.slice(0, 2));
|
|
_setCctvCoverageStateForTest({
|
|
viewer: viewerAt(300, screenByPosition),
|
|
records,
|
|
});
|
|
refreshHorizonCulling();
|
|
let built = 0;
|
|
for (const record of records) {
|
|
const camera = record.camera;
|
|
record.camera = new Proxy(camera, {
|
|
get(target, key) {
|
|
if (key === 'calibration') built += 1;
|
|
return target[key];
|
|
},
|
|
});
|
|
}
|
|
const first = cctvLayer.getUIState();
|
|
assert.equal(built, 0, 'listeners that skip `cameras` cost nothing');
|
|
assert.deepEqual(
|
|
first.cameraIndex.map((camera) => camera.id),
|
|
['cam-0', 'cam-1'],
|
|
);
|
|
assert.equal(
|
|
cctvLayer.getUIState().cameraIndex,
|
|
first.cameraIndex,
|
|
'the index keeps its identity while the view does',
|
|
);
|
|
assert.equal(first.cameras.length, 2);
|
|
assert.ok(built > 0, 'reading `cameras` builds it');
|
|
assert.equal(first.cameras, first.cameras, 'and builds it once');
|
|
});
|
|
|
|
test('the camera picker lists the view, with the active camera first', () => {
|
|
const { records, screenByPosition } = recordsAt(
|
|
[
|
|
[0, 0],
|
|
[0.0001, 0],
|
|
[0.0002, 0],
|
|
],
|
|
(index) => (index === 1 ? { x: WIDTH * 3, y: 0 } : { x: 100, y: 100 }),
|
|
);
|
|
_setCctvCoverageStateForTest({
|
|
viewer: viewerAt(300, screenByPosition),
|
|
records,
|
|
activeCameraId: 'cam-1',
|
|
});
|
|
refreshHorizonCulling();
|
|
const state = cctvLayer.getUIState();
|
|
assert.deepEqual(
|
|
state.cameraIndex.map((camera) => camera.id),
|
|
['cam-1', 'cam-0', 'cam-2'],
|
|
'the off-screen active camera leads, then what is in view',
|
|
);
|
|
assert.equal(state.cameras.length, 3, 'the full list is still every camera');
|
|
});
|