mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
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:
5 files changed
+103
-4
No files matched your search
@@ -36,6 +36,8 @@
|
|||||||
|
|
||||||
- Director timeline seeks cross the dateline along the short longitude arc.
|
- 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.
|
- Frequently reused terrain batches retain their cached heights ahead of unused cells.
|
||||||
|
|
||||||
- Recent Imagery’s exported `setBox` and `searchHls` reject missing, blank or
|
- Recent Imagery’s exported `setBox` and `searchHls` reject missing, blank or
|
||||||
|
|||||||
@@ -1429,6 +1429,8 @@ accessible name remains "Don't show this again". Control behavior is unchanged.
|
|||||||
|
|
||||||
## FIRMS source status
|
## 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
|
The FIRMS proxy records source success after appending its rows. If aggregation
|
||||||
throws, that source reports failure without a contradictory success entry.
|
throws, that source reports failure without a contradictory success entry.
|
||||||
The existing per-record append continues to support large feeds; sequential
|
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
@@ -7,8 +7,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Map proxy fire records into internal fire records. Records with
|
* Map proxy fire records into internal fire records. Records with missing,
|
||||||
* non-finite coordinates are skipped; `index` is the post-skip position
|
* 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).
|
* (it keys pick ids and context-store ids, so it must stay sequential).
|
||||||
* `contextEntity`/`position` start null and are lazily filled by the layer.
|
* `contextEntity`/`position` start null and are lazily filled by the layer.
|
||||||
* @param {?Array<Object>} records - /api/firms `fires` array.
|
* @param {?Array<Object>} records - /api/firms `fires` array.
|
||||||
@@ -21,7 +21,15 @@ export function adaptFirmsRecords(records) {
|
|||||||
for (const record of records) {
|
for (const record of records) {
|
||||||
const lat = Number(record?.lat);
|
const lat = Number(record?.lat);
|
||||||
const lon = Number(record?.lon);
|
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({
|
fires.push({
|
||||||
index: fires.length,
|
index: fires.length,
|
||||||
lat,
|
lat,
|
||||||
@@ -110,3 +118,11 @@ function finiteNumber(value) {
|
|||||||
const number = Number(value);
|
const number = Number(value);
|
||||||
return Number.isFinite(number) ? number : 0;
|
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
@@ -105,7 +105,17 @@ export function parseFirmsCsv(text) {
|
|||||||
if (parts.length < header.length) continue; // malformed row — skip
|
if (parts.length < header.length) continue; // malformed row — skip
|
||||||
const lat = Number(parts[iLat]);
|
const lat = Number(parts[iLat]);
|
||||||
const lon = Number(parts[iLon]);
|
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({
|
records.push({
|
||||||
lat,
|
lat,
|
||||||
|
|||||||
Reference in new issue
Block a user