fix(fires): paint aggregated heat cells warm by density, not by strongest FRP

Coloring each far-zoom cell with the ramp stop of its strongest detection
turned a burning region into a blue / teal / lime mosaic: neighbouring
cells flipped hue on one outlier and the cold stops did not read as fire.
Cells say where it burns, so they go back to the heat-normalized score
(density, night share, max FRP) on the warm end of the ramp — lime, amber,
red — with the cell card accent matching. Single detections keep the full
six-stop FRP ramp.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
German KhokhlovandClaude Fable 5.1 committed 2026-10-27 19:19:40 +04:00
1 parent 1dad6b658a
commit 887b261195
4 files changed
+39 -15

No files matched your search

+28 -9
View File
@@ -137,15 +137,33 @@ export function createModel({
}
/**
* Fill color for an aggregated heat cell: the ramp color of its strongest
* detection, so cells, markers and the slider legend read on one scale.
* @param {number} maxFrp - Strongest FRP in the cell, megawatts.
* Severity stop for an aggregated heat cell from its heat-normalized score
* (density, night share and strongest FRP together). Cells say *where* it
* burns, so they stay on the warm end of the ramp — lime, amber, red — and
* read as one field instead of a per-cell mosaic; the cold stops belong to
* single detections, which say *how hard* each one burns.
* @param {number} normalized - 0..1 heat score (sqrt-normalized to the view).
* @returns {{name: string, minFrp: number, color: Cesium.Color}}
*/
function cellColorStop(normalized) {
const name =
normalized > 0.72 ? 'red' : normalized > 0.42 ? 'amber' : 'lime';
return (
DETECTION_COLOR_STOPS.find((stop) => stop.name === name) ||
DETECTION_COLOR_STOPS[0]
);
}
/**
* Fill color for an aggregated heat cell.
* @param {number} normalized - 0..1 heat score (see {@link cellColorStop}).
* @param {number} alpha - Fill alpha (heat-normalized by the caller).
* @returns {Cesium.Color}
*/
function heatColor(maxFrp, alpha) {
return frpColorStop(maxFrp).color.withAlpha(alpha);
function heatColor(normalized, alpha) {
return cellColorStop(normalized).color.withAlpha(alpha);
}
/**
@@ -476,14 +494,14 @@ export function createModel({
}
/**
* Cell card accent: the ramp stop of the cell's strongest detection,
* Cell card accent: the warm severity stop of the cell's heat score,
* mirroring heatColor.
* @param {number} maxFrp - Strongest FRP in the cell, megawatts.
* @param {number} normalized - 0..1 heat score.
* @returns {string} "r, g, b" accent string.
*/
function cellAccent(maxFrp) {
return accentForSeverity(frpColorStop(maxFrp).name);
function cellAccent(normalized) {
return accentForSeverity(cellColorStop(normalized).name);
}
/** Coordinate line like "30.512°N 75.831°E". */
@@ -590,6 +608,7 @@ export function createModel({
cellIntersectsBounds,
heatScore,
heatColor,
cellColorStop,
frpColorStop,
detectionColorStop,
frpPixelSize,
+3 -2
View File
@@ -144,7 +144,7 @@ export function createRendering({
const score = components.model.heatScore(cell);
const normalized = Math.min(1, Math.sqrt(score / maxScore));
const alpha = 0.16 + normalized * 0.5;
const color = components.model.heatColor(cell.maxFrp, alpha);
const color = components.model.heatColor(normalized, alpha);
const centerLon = cell.lonCell + lod.gridDegrees / 2;
const centerLat = cell.latCell + lod.gridDegrees / 2;
const position = Cesium.Cartesian3.fromDegrees(centerLon, centerLat, 0);
@@ -170,12 +170,13 @@ export function createRendering({
});
// Cells arrive heat-sorted, so candidate order doubles as label priority.
// Accent is resolved here because `normalized` only exists in this walk.
layerState._labelCandidates.push({
position,
cullPosition: components.model.cellCullPosition(centerLon, centerLat),
fire: null,
cell,
accent: components.model.cellAccent(cell.maxFrp),
accent: components.model.cellAccent(normalized),
});
}