Files
gods-eye-view/src/tooling/precomputeCctvHeights.test.mjs
T

366 lines
11 KiB
JavaScript

import assert from 'node:assert/strict';
import test from 'node:test';
import {
batches,
cellKey,
mergeCameraSamples,
mergeSidecar,
needsSampling,
nominalPose,
parseArgs,
planCells,
sampleCell,
samplingPoints,
withDeadline,
plausibleHeight,
} from '../../scripts/precompute-cctv-heights.mjs';
import {
planeSupportPoints,
poseHash,
SUPPORT_KEYS,
} from '../data/cctvFootprint.js';
const pose = {
lat: 30,
lon: -95,
headingDeg: 45,
pitchDeg: -17,
fovDeg: 74,
rangeM: 145,
mountHeightM: 8,
};
const sampledAt = '2026-09-13T12:00:00.000Z';
const camera = (id, overrides = {}) => ({
id,
sourceKind: 'test',
pose: { ...pose, ...overrides },
});
test('nominal poses are hashed exactly as served, never clamped', () => {
assert.deepEqual(nominalPose({ id: 'a', ...pose }), pose);
// The server join and the client hash the same raw served values, so a
// range outside the client's display clamp still round-trips untouched.
for (const rangeM of [20, 120, 145, 3000]) {
const result = nominalPose({ id: 'a', ...pose, rangeM });
assert.equal(result.rangeM, rangeM);
assert.equal(poseHash(result), poseHash({ ...pose, rangeM }));
}
assert.throws(() => nominalPose({ ...pose, rangeM: null }), /invalid rangeM/);
assert.throws(() => nominalPose({ ...pose, lat: 91 }), /invalid coordinates/);
assert.equal(pose.rangeM, 145);
});
test('cells have stable per-row longitude widths and handle boundaries', () => {
assert.equal(cellKey({ lat: 0.001, lon: 0.001 }), '4500,9000');
assert.equal(cellKey({ lat: 0.019, lon: 0.001 }), '4500,9000');
assert.equal(cellKey({ lat: 0.021, lon: 0.001 }).split(',')[0], '4501');
assert.equal(cellKey({ lat: -0.001, lon: 0.001 }).split(',')[0], '4499');
const column = (lat, lon) => Number(cellKey({ lat, lon }).split(',')[1]);
assert.equal(column(0.001, 2) - column(0.001, 0), 100);
assert.equal(column(60.001, 2) - column(60.001, 0), 50);
assert.equal(cellKey({ lat: 30, lon: 180 }), cellKey({ lat: 30, lon: -180 }));
for (const lat of [-90, 90])
assert.match(cellKey({ lat, lon: 0 }), /^\d+,\d+$/);
assert.throws(() => cellKey({ lat: NaN, lon: 0 }), /Invalid/);
});
test('plans order cells numerically and preserve camera order within cells', () => {
const cameras = [
camera('north', { lat: 50 }),
camera('east', { lon: -90 }),
camera('a'),
camera('b'),
];
const plan = planCells(cameras);
assert.deepEqual(
plan.map((cell) => cell.cameras.map((item) => item.id)),
[['a', 'b'], ['east'], ['north']],
);
const sparse = planCells([
camera('ten', { lat: -89.79 }),
camera('two', { lat: -89.95 }),
]);
assert.deepEqual(
sparse.map((cell) => cell.cameras[0].id),
['two', 'ten'],
);
assert.deepEqual(
cameras.map((item) => item.id),
['north', 'east', 'a', 'b'],
);
});
test('batches cap 16 cameras at 160 points, including partial and empty batches', () => {
const items = Array.from({ length: 35 }, (_, i) => i);
const result = batches(items);
assert.deepEqual(
result.map((batch) => batch.length),
[16, 16, 3],
);
assert.deepEqual(result.flat(), items);
assert.deepEqual(batches([], 4), []);
for (const size of [0, -1, 1.5, Infinity])
assert.throws(() => batches(items, size));
});
test('plausibility preserves ellipsoid heights without null or string coercion', () => {
for (const height of [-150, -10.25, 0, 123.456, 9000])
assert.equal(plausibleHeight(height), height);
for (const height of [
-150.001,
undefined,
null,
NaN,
Infinity,
-Infinity,
'0',
false,
])
assert.equal(plausibleHeight(height), null);
});
test('resume requires both ok status and the current shared pose hash', () => {
const entry = { status: 'ok', poseHash: poseHash(pose) };
assert.equal(needsSampling(entry, pose), false);
for (const key of Object.keys(pose))
assert.equal(needsSampling(entry, { ...pose, [key]: pose[key] + 1 }), true);
for (const prior of [
undefined,
{},
{ ...entry, status: 'miss' },
{ poseHash: entry.poseHash },
])
assert.equal(needsSampling(prior, pose), true);
// A complete-looking entry with missing supports is resampled.
assert.equal(needsSampling({ ...entry, misses: ['tr'] }, pose), true);
assert.equal(needsSampling({ ...entry, misses: [] }, pose), false);
});
test('entries preserve support ordering, partial misses and retry successes', () => {
const first = mergeCameraSamples(
pose,
[0, 10, null, 12, 13, 14, 15, 16, 17, -151],
null,
1,
sampledAt,
);
assert.equal(first.status, 'ok');
assert.equal(first.mountGroundM, 0);
assert.deepEqual(Object.keys(first.supports), SUPPORT_KEYS);
assert.deepEqual(first.misses, ['bm', 'tr']);
assert.equal(first.supports.bm, null);
const retried = mergeCameraSamples(
pose,
[undefined, null, 11, null, null, null, null, null, null, 18],
first,
2,
sampledAt,
);
assert.deepEqual(
retried.supports,
Object.fromEntries(SUPPORT_KEYS.map((key, i) => [key, i + 10])),
);
assert.deepEqual(retried.misses, []);
assert.equal(retried.mountGroundM, 0);
assert.equal(retried.attempts, 2);
assert.equal(retried.sampledAt, sampledAt);
// A partial first pass is 'ok' (the mount landed) but still resamples so
// the missing supports get another chance.
assert.equal(needsSampling(first, pose), first.misses.length > 0);
});
test('a missing mount stores no heights and stale poses cannot supply retry heights', () => {
const missing = mergeCameraSamples(
pose,
[-151, ...Array(9).fill(10)],
null,
3,
sampledAt,
);
assert.equal(missing.status, 'miss');
assert.deepEqual(missing.misses, ['mount']);
assert.equal(Object.hasOwn(missing, 'mountGroundM'), false);
assert.equal(Object.hasOwn(missing, 'supports'), false);
const previous = mergeCameraSamples(
{ ...pose, rangeM: 200 },
Array(10).fill(99),
null,
1,
sampledAt,
);
const entry = mergeCameraSamples(pose, [], previous, 2, sampledAt);
assert.equal(entry.status, 'miss');
assert.deepEqual(entry.misses, ['mount', ...SUPPORT_KEYS]);
});
test('sidecar merging sorts ids, preserves unselected entries and replaces stale heights', () => {
const old = {
cameras: { z: { status: 'ok' }, a: { status: 'ok', mountGroundM: 20 } },
};
const result = mergeSidecar(
old,
{ b: { status: 'ok' }, a: { status: 'miss' } },
sampledAt,
);
assert.deepEqual(Object.keys(result.cameras), ['a', 'b', 'z']);
assert.deepEqual(result.cameras.a, { status: 'miss' });
assert.equal(old.cameras.a.mountGroundM, 20);
assert.equal(result.schemaVersion, 1);
assert.equal(result.provider, 'google-3d-tiles');
assert.equal(result.heightReference, 'WGS84-ellipsoid');
assert.equal(result.generatedAt, sampledAt);
assert.deepEqual(mergeSidecar(null, {}, sampledAt).cameras, {});
});
test('CLI supports combined pilot filters and rejects malformed flags', () => {
assert.deepEqual(
parseArgs(['--kinds', 'a, b', '--limit', '40', '--dry-run']),
{ limit: 40, kinds: ['a', 'b'], dryRun: true },
);
assert.equal(parseArgs(['--limit', '0']).limit, 0);
assert.equal(parseArgs([]).limit, Infinity);
for (const args of [
['--limit'],
['--limit', '-1'],
['--limit', '1.5'],
['--limit', '1e3'],
['--kinds'],
['--kinds', 'a,'],
['--kinds', '--dry-run'],
['--unknown'],
])
assert.throws(() => parseArgs(args));
});
test('sampling uses the shared footprint directly and rejects invalid geometry', () => {
const footprint = planeSupportPoints(pose);
const expected = [
footprint.mount,
...SUPPORT_KEYS.map((key) => footprint.supports[key]),
].map(({ lat, lon }) => ({ lat, lon }));
assert.deepEqual(samplingPoints(pose), expected);
assert.equal(expected.length, 10);
assert.ok(
expected.every(
(point) => Number.isFinite(point.lat) && Number.isFinite(point.lon),
),
);
assert.throws(
() => samplingPoints({ ...pose, lat: NaN }),
/invalid coordinates/,
);
});
test('failed calls recycle before serial retries in batches of 4, then singly', async () => {
const cameras = Array.from({ length: 16 }, (_, i) => camera(String(i)));
const sizes = [];
const seen = new Map();
let active = 0;
let needsRecycle = false;
let recycled = 0;
const result = await sampleCell(
cameras,
{
async sample(batch) {
assert.equal(active, 0);
assert.equal(needsRecycle, false);
active += 1;
sizes.push(batch.length);
await Promise.resolve();
active -= 1;
for (const item of batch)
seen.set(item.id, (seen.get(item.id) || 0) + 1);
if (sizes.length === 1) {
needsRecycle = true;
throw new Error('timeout');
}
return batch.map((item) =>
seen.get(item.id) === 2 ? [] : Array(10).fill(25),
);
},
async recycle() {
assert.equal(active, 0);
needsRecycle = false;
recycled += 1;
},
},
() => sampledAt,
);
assert.deepEqual(sizes, [16, 4, 4, 4, 4, ...Array(16).fill(1)]);
assert.equal(recycled, 1);
for (const entry of Object.values(result)) {
assert.equal(entry.status, 'ok');
assert.equal(entry.attempts, 3);
}
});
test('only failed cameras retry; partial successes survive and mount misses stop after three attempts', async () => {
const calls = [];
const result = await sampleCell(
[camera('ok'), camera('partial'), camera('miss')],
{
async sample(batch) {
calls.push(batch.map((item) => item.id));
return batch.map((item) => {
if (item.id === 'miss') return [];
if (item.id === 'partial')
return calls.length === 1
? [10, ...Array(8).fill(11), null]
: [null, ...Array(8).fill(null), 12];
return Array(10).fill(10);
});
},
async recycle() {
assert.fail('No call failed');
},
},
() => sampledAt,
);
assert.deepEqual(calls, [
['ok', 'partial', 'miss'],
['partial', 'miss'],
['miss'],
]);
assert.equal(result.ok.attempts, 1);
assert.equal(result.partial.attempts, 2);
assert.deepEqual(result.partial.misses, []);
assert.equal(result.partial.supports.tr, 12);
assert.equal(result.miss.status, 'miss');
assert.equal(result.miss.attempts, 3);
});
test('deadline rejects hung calls and passes successful or rejected operations through', async () => {
assert.equal(await withDeadline(Promise.resolve(42)), 42);
await assert.rejects(
withDeadline(Promise.reject(new Error('provider failed'))),
/provider failed/,
);
await assert.rejects(withDeadline(new Promise(() => {}), 5), /timed out/);
});
test('a resumed camera keeps its valid heights when the retry only fails', async () => {
const prior = mergeCameraSamples(
pose,
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
undefined,
1,
't0',
);
const partial = {
...prior,
supports: { ...prior.supports, tr: null },
misses: ['tr'],
};
const sampler = {
sample: async (batch) => batch.map(() => Array(10).fill(undefined)),
recycle: async () => {},
};
const entries = await sampleCell([{ id: 'cam', pose }], sampler, () => 't1', {
cam: partial,
});
assert.equal(entries.cam.status, 'ok');
assert.equal(entries.cam.mountGroundM, 1);
assert.equal(entries.cam.supports.bl, 2);
assert.deepEqual(entries.cam.misses, ['tr']);
});