From 6a7b89e436f282356375f6dff667ec8ffb0c1a30 Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Wed, 14 Oct 2026 19:21:57 -0700 Subject: [PATCH] test(firms): cover both coordinate bounds and blank values on each axis Documents the adapter's skip rule and cross-references the two checks, which sit in separate packages (provider parser and fire layer). --- src/data/firms-coordinate-evidence.test.mjs | 38 +++++++++++++++++++++ src/data/firmsAdapt.js | 5 +-- src/data/firmsCsv.js | 2 ++ 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/src/data/firms-coordinate-evidence.test.mjs b/src/data/firms-coordinate-evidence.test.mjs index 494b38c18..d60a7ee37 100644 --- a/src/data/firms-coordinate-evidence.test.mjs +++ b/src/data/firms-coordinate-evidence.test.mjs @@ -29,3 +29,41 @@ test('missing positions do not become fire detections at zero coordinates', () = ], ); }); + +test('both axes keep their exact bounds and drop values just past them', () => { + const row = (lat, lon) => `${lat},${lon},2026-07-16,1200,n,1`; + const csv = [ + 'latitude,longitude,acq_date,acq_time,confidence,frp', + row(90, 180), + row(-90, -180), + row(90.000001, 0), + row(-90.000001, 0), + row(0, 180.000001), + row(0, -180.000001), + row(' ', 5), + row(5, '\t'), + ].join('\n'); + assert.deepEqual( + parseFirmsCsv(csv).map((r) => [r.lat, r.lon]), + [ + [90, 180], + [-90, -180], + ], + ); + const kept = adaptFirmsRecords([ + { lat: 90, lon: 180 }, + { lat: '-90', lon: '-180' }, + { lat: -90.000001, lon: 0 }, + { lat: 0, lon: -180.000001 }, + { lat: 5, lon: ' ' }, + { lat: 5, lon: undefined }, + { lat: [5], lon: 5 }, + ]); + assert.deepEqual( + kept.map((r) => [r.lat, r.lon]), + [ + [90, 180], + [-90, -180], + ], + ); +}); diff --git a/src/data/firmsAdapt.js b/src/data/firmsAdapt.js index 3e3f27631..312838e80 100644 --- a/src/data/firmsAdapt.js +++ b/src/data/firmsAdapt.js @@ -7,8 +7,8 @@ */ /** - * Map proxy fire records into internal fire records. Records with - * non-finite coordinates are skipped; `index` is the post-skip position + * Map proxy fire records into internal fire records. Records with missing, + * non-numeric or out-of-range (WGS84) coordinates are skipped; `index` is the post-skip position * (it keys pick ids and context-store ids, so it must stay sequential). * `contextEntity`/`position` start null and are lazily filled by the layer. * @param {?Array} records - /api/firms `fires` array. @@ -119,6 +119,7 @@ function finiteNumber(value) { return Number.isFinite(number) ? number : 0; } +// Same rule as parseFirmsCsv: a blank or non-numeric value is not a zero. function hasCoordinateValue(value) { return ( typeof value === 'number' || diff --git a/src/data/firmsCsv.js b/src/data/firmsCsv.js index c89d10a0a..aafeac50b 100644 --- a/src/data/firmsCsv.js +++ b/src/data/firmsCsv.js @@ -105,6 +105,8 @@ export function parseFirmsCsv(text) { if (parts.length < header.length) continue; // malformed row — skip const lat = Number(parts[iLat]); const lon = Number(parts[iLon]); + // Blank or out-of-range positions are dropped, never read as 0 (see + // hasCoordinateValue in firmsAdapt.js for the proxy-record side). if ( !parts[iLat].trim() || !parts[iLon].trim() ||