Merge pull request #1044 from rudycelekli/contrib/gods-023-firms-coordinate-evidence-20261011-reviewed

fix(firms): reject missing and out-of-range detection coordinates
This commit is contained in:
Sameh Khamis authored and GitHub committed 2026-10-14 19:43:39 -07:00
commit 84bfba8d39
5 files changed
+103 -4

No files matched your search

+2
View File
@@ -36,6 +36,8 @@
- Director timeline seeks cross the dateline along the short longitude arc.
- Fire detections with missing or impossible positions are skipped while genuine zero coordinates remain valid.
- Frequently reused terrain batches retain their cached heights ahead of unused cells.
- Recent Imagery’s exported `setBox` and `searchHls` reject missing, blank or
+2
View File
@@ -1429,6 +1429,8 @@ accessible name remains "Don't show this again". Control behavior is unchanged.
## FIRMS source status
FIRMS ingestion and adaptation require explicit finite coordinates within WGS84 ranges; genuine zero coordinates remain valid and admitted detection indices stay sequential.
The FIRMS proxy records source success after appending its rows. If aggregation
throws, that source reports failure without a contradictory success entry.
The existing per-record append continues to support large feeds; sequential
@@ -0,0 +1,69 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { parseFirmsCsv } from './firmsCsv.js';
import { adaptFirmsRecords } from './firmsAdapt.js';
test('missing positions do not become fire detections at zero coordinates', () => {
const csv =
'latitude,longitude,acq_date,acq_time,confidence,frp\n,1,2026-07-16,1200,n,1\n1, ,2026-07-16,1200,n,1\n95,1,2026-07-16,1200,n,1\n1,181,2026-07-16,1200,n,1\n0,0,2026-07-16,1200,n,1\n-90,-180,2026-07-16,1200,n,1';
assert.deepEqual(
parseFirmsCsv(csv).map((r) => [r.lat, r.lon]),
[
[0, 0],
[-90, -180],
],
);
const records = [
{ lat: null, lon: 1 },
{ lat: '', lon: 1 },
{ lat: false, lon: 1 },
{ lat: 95, lon: 1 },
{ lat: 1, lon: 181 },
{ lat: '0', lon: 0 },
{ lat: -90, lon: -180 },
];
assert.deepEqual(
adaptFirmsRecords(records).map((r) => [r.index, r.lat, r.lon]),
[
[0, 0, 0],
[1, -90, -180],
],
);
});
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],
],
);
});
+19 -3
View File
@@ -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<Object>} records - /api/firms `fires` array.
@@ -21,7 +21,15 @@ export function adaptFirmsRecords(records) {
for (const record of records) {
const lat = Number(record?.lat);
const lon = Number(record?.lon);
if (!Number.isFinite(lat) || !Number.isFinite(lon)) continue;
if (
!hasCoordinateValue(record?.lat) ||
!hasCoordinateValue(record?.lon) ||
!Number.isFinite(lat) ||
!Number.isFinite(lon) ||
Math.abs(lat) > 90 ||
Math.abs(lon) > 180
)
continue;
fires.push({
index: fires.length,
lat,
@@ -110,3 +118,11 @@ function finiteNumber(value) {
const number = Number(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' ||
(typeof value === 'string' && value.trim() !== '')
);
}
+11 -1
View File
@@ -105,7 +105,17 @@ export function parseFirmsCsv(text) {
if (parts.length < header.length) continue; // malformed row — skip
const lat = Number(parts[iLat]);
const lon = Number(parts[iLon]);
if (!Number.isFinite(lat) || !Number.isFinite(lon)) continue;
// 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() ||
!Number.isFinite(lat) ||
!Number.isFinite(lon) ||
Math.abs(lat) > 90 ||
Math.abs(lon) > 180
)
continue;
records.push({
lat,