feat(fires): Snap Map-style heat raster and its globe imagery drape

Detections are binned on a 2048x1024 equirectangular grid weighted
1 + log10(1 + FRP), Gaussian-blurred (longitude wraps), normalized on a log
scale against the 99.5th percentile and mapped blue -> red with an alpha
ramp that is fully transparent below the floor. heatImagery drapes the bytes
as one full-globe SingleTileImageryProvider on the active imagery host.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
German KhokhlovandClaude Opus 5.5 committed 2026-10-29 17:05:14 +04:00
1 parent 887b261195
commit b778a43f1d
4 files changed
+407

No files matched your search

+71
View File
@@ -0,0 +1,71 @@
/**
* Drapes the Active Fires heat raster over the globe as one full-extent
* imagery tile on the active imagery host (globe or 3D-tiles), like the wind
* layer's scalar field. Owns the canvas and the imagery layer; nothing else
* in the fires layer touches imagery.
*/
const defaultCanvas = (width, height) => {
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
return canvas;
};
export function createHeatImagery({
cesium,
getHost,
createCanvas = defaultCanvas,
}) {
let raster = null;
let layer = null;
let collection = null;
let show = true;
function detach() {
if (layer && collection) collection.remove(layer, true);
layer = null;
collection = null;
}
function attach() {
detach();
if (!raster) return;
const host = getHost?.() ?? { collection: null, kind: 'none' };
if (!host.collection) return;
const canvas = createCanvas(raster.width, raster.height);
const ctx = canvas.getContext('2d');
const image = ctx.createImageData(raster.width, raster.height);
image.data.set(raster.rgba);
ctx.putImageData(image, 0, 0);
const provider = new cesium.SingleTileImageryProvider({
url: canvas.toDataURL('image/png'),
tileWidth: raster.width,
tileHeight: raster.height,
rectangle: cesium.Rectangle.MAX_VALUE,
});
collection = host.collection;
layer = collection.addImageryProvider(provider);
layer.show = show;
collection.raiseToTop?.(layer);
}
return {
setRaster(next) {
raster = next;
attach();
},
setShow(value) {
show = Boolean(value);
if (layer) layer.show = show;
},
rehome() {
const host = getHost?.();
if ((host?.collection ?? null) !== collection) attach();
},
destroy() {
detach();
raster = null;
},
};
}
+87
View File
@@ -0,0 +1,87 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { createHeatImagery } from './heatImagery.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('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);
});