Move the Active Fires hand-off up to 4,500 km

The heatmap now takes over above 4,500 km instead of 3,000 km, with
the cross-fade between 3,600 and 5,400 km. The markers band caps at
40,000 detections, which covers the ~40,000 in the padded view over
Africa at the low end of the fade, so weak fires still stay as markers.

To keep that rebuild near its old cost, markers outside the close band
read the anchor already built for each fire instead of the ground floor
cache: only the close band warms floors, so the lookup there always
missed. A 40,000-marker rebuild now costs ~55 ms, down from ~120 ms.
This commit is contained in:
German Khokhlov committed 2026-11-03 21:24:50 +04:00
1 parent 9a1bc34042
commit f608ae81e7
6 files changed
+40 -20

No files matched your search

+1 -1
View File
@@ -521,7 +521,7 @@ test('far zoom drapes one heatmap and the hand-off cross-fades it with the marke
assert.equal(markers.get(0).color.alpha, 1);
// Mid hand-off both are drawn, half each, in either band.
setHeight(3_000_000);
setHeight(4_500_000);
h.layer._applyHandoffFadeForTest();
assert.equal(added[0].alpha, 0.5);
assert.equal(markers.show, true);
+16 -2
View File
@@ -231,6 +231,19 @@ export function createModel({
return fire.position;
}
/**
* The anchor already built for a detection, without the floor lookup;
* for the far marker band, where floors are never warmed and the height
* difference is invisible, so tens of thousands of markers skip a cache
* read each per rebuild. Builds one through {@link firePosition} once.
* @param {Object} fire - Detection record.
* @returns {Cesium.Cartesian3}
*/
function fireBuiltPosition(fire) {
return fire.position ?? firePosition(fire);
}
/**
* Occlusion-test anchor for one detection. Returns the render position
* outright for anchors comfortably above the ellipsoid (the common case) and
@@ -238,11 +251,11 @@ export function createModel({
* NEVER feeds rendering: the datum-correct anchor from {@link firePosition}
* is what the sprite and the card are drawn at.
* @param {Object} fire - Detection record.
* @param {Cesium.Cartesian3} [position] Its render anchor, if at hand.
* @returns {Cesium.Cartesian3}
*/
function fireCullPosition(fire) {
const position = firePosition(fire);
function fireCullPosition(fire, position = firePosition(fire)) {
if (fire.positionHeight >= CULL_LIFT_THRESHOLD_M) return position;
if (!fire.cullPosition) {
fire.cullPosition = Cesium.Cartesian3.fromDegrees(
@@ -587,6 +600,7 @@ export function createModel({
sizeBucket,
glowSprite,
firePosition,
fireBuiltPosition,
fireCullPosition,
cellCullPosition,
applyHorizonCull,
+8 -8
View File
@@ -7,7 +7,7 @@ import * as Cesium from 'cesium';
export const REFRESH_INTERVAL_MS = 600_000;
/**
* LOD bands keyed by camera height. `heat` bands (above ~3,000 km) drape a
* LOD bands keyed by camera height. `heat` bands (above ~4,500 km) drape a
* Snap Map–style heatmap over the globe (firmsHeatRaster.js); their
* `gridDegrees` aggregation only feeds the "N FIRES" cards. `detections`
* bands render individual fire detections as glow-sprite billboards.
@@ -26,21 +26,21 @@ export const LOD_LEVELS = [
},
{
id: 'regional',
minHeight: 3000000,
minHeight: 4500000,
mode: 'heat',
gridDegrees: 1.0,
maxCells: 3600,
labelDistance: 8500000,
},
{
// Up to 20,000 markers: at the 2,400 km end of the hand-off the padded
// view over Africa, the densest region, holds ~21,000 detections, and a
// Up to 40,000 markers: at the 3,600 km end of the hand-off the padded
// view over Africa, the densest region, holds ~40,000 detections, and a
// tighter cap left the heatmap's weak spots dissolving into nothing.
// A rebuild of that many costs ~60 ms, only there.
id: 'local',
minHeight: 750000,
mode: 'detections',
maxDetections: 20000,
maxDetections: 40000,
labelDistance: 4500000,
},
{
@@ -55,7 +55,7 @@ export const LOD_LEVELS = [
/**
* Global heat raster for the `heat` bands: ≈0.09° (≈10 km) per pixel and a
* one-pixel Gaussian σ, so a lone fire is a spot about the size of its
* marker at the 3,000 km hand-off and the two cross-fade without a jump;
* marker near the hand-off and the two cross-fade without a jump;
* neighbours merge only when they nearly touch (σ 3 on a 2048 grid smeared
* each fire over ~200 km). Built once per data refresh or filter change,
* never per camera move.
@@ -69,12 +69,12 @@ export const HEAT_RASTER = Object.freeze({
/**
* Camera heights, in metres, over which the heatmap and the markers
* cross-fade around the 3,000 km band edge: markers alone below `from`,
* cross-fade around the 4,500 km band edge: markers alone below `from`,
* heatmap alone above `to`. Wider than the ±10 % band hysteresis, so
* whichever band is active inside it, both layers are already built.
*/
export const HANDOFF_FADE = Object.freeze({ from: 2_400_000, to: 3_600_000 });
export const HANDOFF_FADE = Object.freeze({ from: 3_600_000, to: 5_400_000 });
/**
* Trailing delay before a stale heatmap is rebuilt while one is already on
+7 -2
View File
@@ -354,10 +354,15 @@ export function createRendering({
}
const markers = [];
// Only the close band warms terrain floors (renderDetections).
const anchor =
lod.id === 'close'
? components.model.firePosition
: components.model.fireBuiltPosition;
for (const fire of candidates) {
const coreSize = components.model.frpPixelSize(fire.frp);
const position = components.model.firePosition(fire);
const cullPosition = components.model.fireCullPosition(fire);
const position = anchor(fire);
const cullPosition = components.model.fireCullPosition(fire, position);
const pickId = `firms-${fire.index}`;
layerState._pickIndexById.set(pickId, fire);
layerState._cullPositions.push(cullPosition);