mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-15 15:52:21 +00:00
The far-zoom heatmap now uses the ½× grid (4096 × 2048, ≈10 km per pixel, one-pixel blur), so a lone fire's spot matches its marker at the 3,000 km hand-off. The spot size slider and its share option are gone. Markers and heatmap are both kept built at every zoom and cross-fade by camera height between 2,400 and 3,600 km on every frame, so a fast zoom never shows a beat with neither. The heatmap fades by imagery alpha: hiding a Cesium imagery layer unmaps it from the globe tiles, and showing it again re-mapped them late, which was the gap at the band edge. Under the markers the heatmap is built 150 ms after the data or filter changes.
129 lines
3.9 KiB
JavaScript
129 lines
3.9 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { createHeatImagery } from './heatImagery.js';
|
|
import { buildHeatRaster } from '../../data/firmsHeatRaster.js';
|
|
import { HEAT_RASTER } from './policy.js';
|
|
|
|
function harness() {
|
|
const layers = [];
|
|
const removed = [];
|
|
const collection = {
|
|
addImageryProvider(provider) {
|
|
const layer = { provider, show: true, alpha: 1 };
|
|
layers.push(layer);
|
|
return layer;
|
|
},
|
|
remove(layer, destroy) {
|
|
layers.splice(layers.indexOf(layer), 1);
|
|
removed.push({ layer, destroy });
|
|
},
|
|
raiseToTop() {},
|
|
};
|
|
let host = { collection, kind: 'globe' };
|
|
const cesium = {
|
|
Rectangle: { MAX_VALUE: { max: true } },
|
|
SingleTileImageryProvider: class {
|
|
constructor(options) {
|
|
this.options = options;
|
|
}
|
|
},
|
|
};
|
|
const createCanvas = (w, h) => ({
|
|
width: w,
|
|
height: h,
|
|
getContext: () => ({
|
|
createImageData: (cw, ch) => ({
|
|
data: new Uint8ClampedArray(cw * ch * 4),
|
|
}),
|
|
putImageData() {},
|
|
}),
|
|
toDataURL: () => 'data:image/png;base64,AAAA',
|
|
});
|
|
const imagery = createHeatImagery({
|
|
cesium,
|
|
getHost: () => host,
|
|
createCanvas,
|
|
});
|
|
return { imagery, layers, removed, collection, setHost: (h) => (host = h) };
|
|
}
|
|
|
|
const raster = { width: 4, height: 2, rgba: new Uint8ClampedArray(32) };
|
|
|
|
test('setRaster drapes one full-globe tile and replaces it on the next build', () => {
|
|
const h = harness();
|
|
h.imagery.setRaster(raster);
|
|
assert.equal(h.layers.length, 1);
|
|
assert.deepEqual(h.layers[0].provider.options.rectangle, { max: true });
|
|
assert.equal(h.layers[0].provider.options.tileWidth, 4);
|
|
h.imagery.setRaster(raster);
|
|
assert.equal(h.layers.length, 1, 'never two heat layers');
|
|
assert.equal(h.removed[0].destroy, true);
|
|
});
|
|
|
|
test('setShow toggles without rebuilding', () => {
|
|
const h = harness();
|
|
h.imagery.setRaster(raster);
|
|
const layer = h.layers[0];
|
|
h.imagery.setShow(false);
|
|
assert.equal(layer.show, false);
|
|
h.imagery.setShow(true);
|
|
assert.equal(h.layers[0], layer);
|
|
assert.equal(layer.show, true);
|
|
});
|
|
|
|
test('setOpacity fades the same layer and survives a rebuild', () => {
|
|
const h = harness();
|
|
h.imagery.setRaster(raster);
|
|
const layer = h.layers[0];
|
|
h.imagery.setOpacity(0);
|
|
assert.equal(layer.alpha, 0);
|
|
assert.equal(layer.show, true, 'a faded layer stays mapped');
|
|
h.imagery.setOpacity(0.25);
|
|
assert.equal(h.layers[0], layer);
|
|
assert.equal(layer.alpha, 0.25);
|
|
h.imagery.setRaster(raster);
|
|
assert.equal(h.layers[0].alpha, 0.25, 'a rebuilt layer keeps the fade');
|
|
h.imagery.setOpacity(7);
|
|
assert.equal(h.layers[0].alpha, 1);
|
|
});
|
|
|
|
test('no imagery host keeps nothing draped; rehome brings it back', () => {
|
|
const h = harness();
|
|
h.setHost({ collection: null, kind: 'none' });
|
|
h.imagery.setRaster(raster);
|
|
assert.equal(h.layers.length, 0);
|
|
h.setHost({ collection: h.collection, kind: 'globe' });
|
|
h.imagery.rehome();
|
|
assert.equal(h.layers.length, 1);
|
|
});
|
|
|
|
test('destroy removes the layer', () => {
|
|
const h = harness();
|
|
h.imagery.setRaster(raster);
|
|
h.imagery.destroy();
|
|
assert.equal(h.layers.length, 0);
|
|
});
|
|
|
|
test('the shipped raster paints a lone weak fire as a distinct spot', () => {
|
|
// A hot cluster elsewhere sets the scale, as Africa does on real data.
|
|
const fires = [{ lat: 0, lon: 0, frp: 5 }];
|
|
for (let i = 0; i < 40; i++) {
|
|
fires.push({ lat: -10 + (i % 5) * 0.1, lon: 25 + i * 0.05, frp: 300 });
|
|
}
|
|
const r = buildHeatRaster(fires, HEAT_RASTER);
|
|
const y = Math.floor((90 / 180) * r.height);
|
|
const painted = [];
|
|
for (let x = 0; x < r.width; x++) {
|
|
const lon = ((x + 0.5) / r.width) * 360 - 180;
|
|
if (Math.abs(lon) < 5 && r.rgba[(y * r.width + x) * 4 + 3] > 0) {
|
|
painted.push(x);
|
|
}
|
|
}
|
|
const kmPerPx = 40075 / r.width;
|
|
assert.ok(painted.length > 0, 'the weak fire still shows');
|
|
assert.ok(
|
|
painted.length * kmPerPx <= 100,
|
|
`spot ${Math.round(painted.length * kmPerPx)} km wide`,
|
|
);
|
|
});
|