mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 19:34:01 +00:00
Check that fires keep their observation sum and draw one marker kind across Navara LOD changes
This commit is contained in:
1 parent
c45ec634ee
commit
1910a3b2d5
1 file changed
+192
@@ -0,0 +1,192 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import * as geo from '../../geo/index.js';
|
||||
import { createFakeEngine } from '../../globe/testing/fakeEngine.js';
|
||||
import { createFirmsHeatmapLayer } from './index.js';
|
||||
import { createFirmsNavaraRenderer } from './render/navara.js';
|
||||
import { unregisterSpriteCollection } from '../../data/spriteOrder.js';
|
||||
|
||||
const settle = async () => {
|
||||
for (let i = 0; i < 5; i++)
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
};
|
||||
const FIRE = { lat: 38.5, lon: -121.5 };
|
||||
// One cluster a few hundred metres across: one cell from orbit, sprites at the fire.
|
||||
const records = Array.from({ length: 9 }, (_, i) => ({
|
||||
lat: FIRE.lat + (i % 3) * 0.002,
|
||||
lon: FIRE.lon + Math.floor(i / 3) * 0.002,
|
||||
frp: 10 + i * 7,
|
||||
confidence: 'h',
|
||||
brightness: 330,
|
||||
daynight: 'D',
|
||||
acqDate: '2026-01-01',
|
||||
acqTime: '1200',
|
||||
instrument: 'VIIRS',
|
||||
}));
|
||||
|
||||
function installCanvasFakes() {
|
||||
const saved = { document: globalThis.document, Image: globalThis.Image };
|
||||
const context = {
|
||||
createRadialGradient: () => ({ addColorStop() {} }),
|
||||
fillRect() {},
|
||||
drawImage() {},
|
||||
};
|
||||
globalThis.document = {
|
||||
createElement: () => ({
|
||||
width: 0,
|
||||
height: 0,
|
||||
getContext: () => context,
|
||||
toDataURL() {
|
||||
return `data:image/png;base64,${this.width}`;
|
||||
},
|
||||
}),
|
||||
};
|
||||
globalThis.Image = class {
|
||||
set src(url) {
|
||||
this.width = this.height = Number(url.split(',')[1]);
|
||||
queueMicrotask(() => this.onload?.());
|
||||
}
|
||||
};
|
||||
return () => Object.assign(globalThis, saved);
|
||||
}
|
||||
|
||||
function fixture() {
|
||||
const restore = installCanvasFakes();
|
||||
const engine = createFakeEngine({ kind: 'navara' });
|
||||
const pose = (height) =>
|
||||
engine.fake.setPose({
|
||||
longitude: geo.math.toRadians(FIRE.lon + 0.002),
|
||||
latitude: geo.math.toRadians(FIRE.lat + 0.002),
|
||||
height,
|
||||
heading: 0,
|
||||
pitch: -Math.PI / 2,
|
||||
roll: 0,
|
||||
});
|
||||
pose(12_000_000);
|
||||
const effects = [];
|
||||
const native = {
|
||||
view: {
|
||||
camera: {},
|
||||
registerEffect() {},
|
||||
addEffect(config) {
|
||||
const handle = { config, update() {}, delete() {} };
|
||||
effects.push(handle);
|
||||
return handle;
|
||||
},
|
||||
},
|
||||
EffectDesc: class {},
|
||||
NavaraEffect: class {},
|
||||
};
|
||||
// Cells the layer hands to the ground display for its current build.
|
||||
let cells = [];
|
||||
const noop = () => {};
|
||||
const layer = createFirmsHeatmapLayer({
|
||||
id: 'local-firms',
|
||||
name: 'Fires',
|
||||
feed: { getSnapshot: async () => ({ fires: records }) },
|
||||
services: {
|
||||
render: { governorRequestRender: noop },
|
||||
picking: {
|
||||
registerPickOwner: noop,
|
||||
unregisterPickOwner: noop,
|
||||
resolvePickId: () => null,
|
||||
isOwnedByOtherLayer: () => false,
|
||||
},
|
||||
focus: { requestWorldFocus: noop },
|
||||
context: {
|
||||
selectEntityContext: noop,
|
||||
clearSelectedEntityContextForLayer: noop,
|
||||
getContextStore: () => new Map(),
|
||||
registerEntityContext: noop,
|
||||
},
|
||||
anchors: {
|
||||
fireAnchorHeight: () => 0,
|
||||
warmFireAnchorFloors: async () => false,
|
||||
},
|
||||
},
|
||||
overlayHost: { setEntries: noop, setVisible: noop, clearSource: noop },
|
||||
renderer: {
|
||||
create: ({ engine: target, id }) => {
|
||||
const renderer = createFirmsNavaraRenderer(target, { ...native, id });
|
||||
const { addCell, clearCells } = renderer;
|
||||
renderer.addCell = (cell) => (cells.push(cell), addCell(cell));
|
||||
renderer.clearCells = () => ((cells = []), clearCells());
|
||||
return renderer;
|
||||
},
|
||||
},
|
||||
});
|
||||
layer.init(null, { engine });
|
||||
let clock = 0;
|
||||
const realNow = performance.now;
|
||||
performance.now = () => clock;
|
||||
return {
|
||||
engine,
|
||||
layer,
|
||||
pose,
|
||||
cells: () => cells,
|
||||
// Ground cells actually drawn by the shared display.
|
||||
drawnCells: () => effects[0]?.config.gevFirmsGround.owner.state.count ?? 0,
|
||||
// Fire sprites actually drawn (shown instances of the engine set).
|
||||
drawnSprites() {
|
||||
const set = engine.fake.moverSet('firms:local-firms');
|
||||
if (!set) return 0;
|
||||
let shown = 0;
|
||||
for (let i = 0; i < set.store.count; i++) shown += set.store.shows[i];
|
||||
return shown;
|
||||
},
|
||||
async step() {
|
||||
clock += 1000; // past the LOD throttle
|
||||
engine.fake.frame();
|
||||
await settle();
|
||||
},
|
||||
cleanup() {
|
||||
layer.destroy();
|
||||
unregisterSpriteCollection('local-firms');
|
||||
performance.now = realNow;
|
||||
restore();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test('edge:local-firms: orbit to the fire keeps the observation sum and never doubles a marker across LOD changes', async () => {
|
||||
const f = fixture();
|
||||
try {
|
||||
await f.layer.enable();
|
||||
await f.step();
|
||||
const sum = (list) => list.reduce((n, c) => n + c.properties.count, 0);
|
||||
assert.equal(
|
||||
sum(f.cells()),
|
||||
records.length,
|
||||
'orbit cells hold every observation',
|
||||
);
|
||||
assert.equal(f.drawnCells(), 1);
|
||||
assert.equal(f.drawnSprites(), 0);
|
||||
for (const round of [1, 2]) {
|
||||
// Down to the fire: detections replace the cell, one sprite per observation.
|
||||
f.pose(20_000);
|
||||
await f.step();
|
||||
assert.equal(
|
||||
f.drawnSprites(),
|
||||
records.length,
|
||||
`round ${round} at the fire`,
|
||||
);
|
||||
assert.equal(
|
||||
f.drawnCells(),
|
||||
0,
|
||||
`round ${round}: no cell under the sprites`,
|
||||
);
|
||||
// Back to orbit: the cell returns with the same sum, sprites go.
|
||||
f.pose(12_000_000);
|
||||
await f.step();
|
||||
assert.equal(sum(f.cells()), records.length);
|
||||
assert.equal(f.drawnCells(), 1, `round ${round} in orbit`);
|
||||
assert.equal(
|
||||
f.drawnSprites(),
|
||||
0,
|
||||
`round ${round}: no sprite over the cell`,
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
f.cleanup();
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user