diff --git a/changelog.d/1426-fire-intensity-filter.md b/changelog.d/1426-fire-intensity-filter.md index c1d06643e..4c02ccf20 100644 --- a/changelog.d/1426-fire-intensity-filter.md +++ b/changelog.d/1426-fire-intensity-filter.md @@ -4,6 +4,6 @@ log-scale minimum-FRP slider whose track is the ramp, an All / Nominal+ / High confidence switch and an "N of M shown" counter to the layer row. Far zoom shows a Snap Map–style heatmap in the same colors instead of squares, - empty where nothing burns. The filter applies once before rendering, so markers, cards, the heatmap, context and the top-fires query agree; share + one marker-sized spot per fire, empty where nothing burns. The filter applies once before rendering, so markers, cards, the heatmap, context and the top-fires query agree; share links keep it (`w` token: `f`, `c`), and `get_active_fires` takes matching `min_frp_mw` and `confidence` arguments. Default 0 MW / All (gekh, #1426). diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index dabc074ae..5be2a98dc 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -2185,9 +2185,10 @@ accents read that ramp; confidence and brightness do not affect the color. At far zoom (camera above ~3,000 km) the layer drapes a Snap Map–style heatmap instead of individual markers: the shown detections are binned on a -global 2048 × 1024 grid, each weighted by its FRP in megawatts (so heat is +global 4096 × 2048 grid (≈10 km per pixel), each weighted by its FRP in megawatts (so heat is radiated power, and raising the FRP slider does not dim areas where strong fires -burn), Gaussian-blurred, +burn), Gaussian-blurred by one pixel so each fire stays a spot about the size +of its marker, normalized on a log scale against the 99.5th percentile and mapped through the same hues, from a bright red edge through orange and gold to a pale white-hot core. The edge starts more than half opaque, so regions far below diff --git a/src/layers/firms/heatImagery.test.mjs b/src/layers/firms/heatImagery.test.mjs index 0fca7b97f..4b6403298 100644 --- a/src/layers/firms/heatImagery.test.mjs +++ b/src/layers/firms/heatImagery.test.mjs @@ -1,6 +1,8 @@ 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 = []; @@ -85,3 +87,26 @@ test('destroy removes the layer', () => { h.imagery.destroy(); assert.equal(h.layers.length, 0); }); + +test('the shipped raster paints a lone weak fire as a marker-sized 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 <= 80, + `spot ${Math.round(painted.length * kmPerPx)} km wide`, + ); +}); diff --git a/src/layers/firms/policy.js b/src/layers/firms/policy.js index faa55bbc3..9de42cfa4 100644 --- a/src/layers/firms/policy.js +++ b/src/layers/firms/policy.js @@ -49,14 +49,17 @@ export const LOD_LEVELS = [ ]; /** - * Global heat raster for the `heat` bands: ≈0.18° per pixel, Gaussian σ in - * pixels. Built once per data refresh or filter change, never per camera move. + * 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 as wide as its marker + * at the 3,000 km hand-off and neighbours merge only when they really touch + * (2048 px with σ 3 smeared each fire over ~200 km). Built once per data + * refresh or filter change, never per camera move. */ export const HEAT_RASTER = Object.freeze({ - width: 2048, - height: 1024, - sigmaPx: 3, + width: 4096, + height: 2048, + sigmaPx: 1, }); /**