From 2beef9be393e18b3ebba9b62182119166cd1a369 Mon Sep 17 00:00:00 2001 From: Rudy Celekli Date: Sat, 24 Oct 2026 00:23:11 -0400 Subject: [PATCH 1/3] fix(firms): drop the decimal when fire power rounds to 10 MW A 9.96 MW detection was labeled 10.0 MW because the one-decimal cutoff ran before rounding. --- src/data/firmsCards.test.mjs | 15 +++++++++++++++ src/data/firmsLabels.js | 11 +++++++++++ src/data/firmsLabels.test.mjs | 9 +++++++++ src/layers/firms/model.js | 8 +++++--- 4 files changed, 40 insertions(+), 3 deletions(-) diff --git a/src/data/firmsCards.test.mjs b/src/data/firmsCards.test.mjs index 4b423f687..a0ee83c01 100644 --- a/src/data/firmsCards.test.mjs +++ b/src/data/firmsCards.test.mjs @@ -53,6 +53,21 @@ test('buildFireCard: missing acquisition time omits the age segment', () => { assert.equal(card.details[0], 'high · N20'); }); +test('buildFireCard: power that rounds to 10 MW drops the decimal', () => { + const crossed = buildFireCard( + { fire: fire({ frp: 9.96 }), position: {} }, + NOW, + ); + assert.equal(crossed.title, '▲ 10 MW'); + const below = buildFireCard( + { fire: fire({ frp: 9.94 }), position: {} }, + NOW, + ); + assert.equal(below.title, '▲ 9.9 MW'); + const selected = buildSelectedFireCard(fire({ frp: 9.96 }), NOW); + assert.equal(selected.title, 'FIRE · 10 MW'); +}); + test('buildFireCard: SNPP satellite code renders as SNPP, weak fire is not red', () => { const card = buildFireCard({ fire: fire({ satellite: 'N', frp: 0.8, confidence: 0.3 }), position: {} }, NOW); assert.match(card.details[0], /SNPP$/); diff --git a/src/data/firmsLabels.js b/src/data/firmsLabels.js index 34b66b79a..b9e9d1765 100644 --- a/src/data/firmsLabels.js +++ b/src/data/firmsLabels.js @@ -39,6 +39,17 @@ export function accentForSeverity(stopName) { return ACCENT_RGB[stopName] || ACCENT_RGB.yellow; } +/** + * Fire radiative power as shown on a card: "9.9" below 10 MW, "10" once + * one-decimal rounding reaches 10. "10.0" would sit beside a whole "10". + * @param {number} frp Megawatts. + * @returns {string} + */ +export function formatFrpMw(frp) { + const digits = Number(Number(frp).toFixed(1)) >= 10 ? 0 : 1; + return Number(frp).toFixed(digits); +} + /** Raw FIRMS satellite code → short display name (N = Suomi NPP). */ export function satelliteShortName(satellite) { const s = String(satellite || '') diff --git a/src/data/firmsLabels.test.mjs b/src/data/firmsLabels.test.mjs index e0ebb2056..8573d7330 100644 --- a/src/data/firmsLabels.test.mjs +++ b/src/data/firmsLabels.test.mjs @@ -6,6 +6,7 @@ import { FIRMS_AMBIENT_COHORT_LIMIT, FIRMS_OVERLAY_SOURCE_ID, satelliteShortName, + formatFrpMw, } from './firmsLabels.js'; test('FIRMS formatting helpers retain the shipped severity palette', () => { @@ -15,6 +16,14 @@ test('FIRMS formatting helpers retain the shipped severity palette', () => { assert.equal(accentForSeverity('chartreuse'), accentForSeverity('yellow')); }); +test('FIRMS power that rounds to 10 MW drops the decimal', () => { + assert.equal(formatFrpMw(9.96), '10'); + assert.equal(formatFrpMw(9.94), '9.9'); + assert.equal(formatFrpMw(0.8), '0.8'); + assert.equal(formatFrpMw(1520.27), '1520'); + assert.equal(formatFrpMw(210.4), '210'); +}); + test('FIRMS satellite names retain the three VIIRS abbreviations', () => { assert.equal(satelliteShortName('N20'), 'N20'); assert.equal(satelliteShortName('N21'), 'N21'); diff --git a/src/layers/firms/model.js b/src/layers/firms/model.js index 226c32286..14b875bf8 100644 --- a/src/layers/firms/model.js +++ b/src/layers/firms/model.js @@ -4,6 +4,7 @@ import { satelliteShortName, fireDetectionKey, accentForSeverity, + formatFrpMw, } from '../../data/firmsLabels.js'; import { VIEW_PADDING, @@ -480,14 +481,15 @@ export function createModel({ } function formatFrp(frp) { - return frp >= 10 ? frp.toFixed(0) : frp.toFixed(1); + return formatFrpMw(frp); } /** formatFrp for a card, with the locale's decimal separator ("4,9"). */ function formatFrpLabel(frp) { - const digits = frp >= 10 ? 0 : 1; - return formatNumber(frp, { + const text = formatFrpMw(frp); + const digits = text.includes('.') ? 1 : 0; + return formatNumber(Number(text), { minimumFractionDigits: digits, maximumFractionDigits: digits, useGrouping: false, From 9f8ed74e7bbf741ad3d3fce36398a8625d9a00b5 Mon Sep 17 00:00:00 2001 From: Rudy Celekli Date: Sat, 24 Oct 2026 00:35:59 -0400 Subject: [PATCH 2/3] changelog: note the 10 MW fire-power label --- changelog.d/1354-firms-frp-ten-megawatts.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 changelog.d/1354-firms-frp-ten-megawatts.md diff --git a/changelog.d/1354-firms-frp-ten-megawatts.md b/changelog.d/1354-firms-frp-ten-megawatts.md new file mode 100644 index 000000000..394eba1df --- /dev/null +++ b/changelog.d/1354-firms-frp-ten-megawatts.md @@ -0,0 +1,3 @@ +- Drop the decimal when fire radiative power rounds to 10 MW. A 9.96 MW + detection was labeled 10.0 MW because the one-decimal cutoff ran before + rounding (Rudy Celekli, @rudycelekli, #1354). From 6b85990fec9f2db9d6db0edff700ff3bd3ee7133 Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Sat, 24 Oct 2026 22:17:56 -0700 Subject: [PATCH 3/3] docs: record PR #1354 display behavior --- docs/CURRENT-STATE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 3344a176d..e2dbe40be 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -4643,7 +4643,7 @@ silently demoting every later lookup for the session. multi-sensor datasheets publish a capability range, not the lens's current zoom, so those cameras keep their estimate and show `SPEC FOV min–max° (CAPABILITY)` beside it. Public camera state carries `fovSource`, `specFovMinDeg`, `specFovMaxDeg` and `spec` (#452). -- **FIRMS**: no ground clamping (zero 3D-tiles height sampling), ≤18 screen-decluttered ambient labels, click-to-inspect detail card, 2.5k/3k sprite budgets viewport-clipped by FRP. +- **FIRMS**: no ground clamping (zero 3D-tiles height sampling), ≤18 screen-decluttered ambient labels, click-to-inspect detail card, 2.5k/3k sprite budgets viewport-clipped by FRP. Fire-power labels share display rounding before localization: one decimal below the rounded 10 MW boundary, then whole megawatts (for example, 9.96 MW reads 10 MW). - **CCTV v2 foundation:** a pitched frustum wireframe (4 corner rays + far-cap rectangle) with a monitor plane at the frustum's far cap, retargeting the existing video/canvas texture pipeline. Stills are drawn at their source aspect, centered on the placeholder dark (`#01070c`), instead of being stretched to the 16:9 canvas. Manual calibration only —