mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
152 lines
4.9 KiB
JavaScript
152 lines
4.9 KiB
JavaScript
/**
|
|
* Adapter from /api/firms proxy records (src/data/firmsCsv.js shape) to the
|
|
* internal fire-record shape rendered by firmsHeatmap.js. Pure — no Cesium,
|
|
* so the mapping is unit-testable. The normalization helpers previously
|
|
* lived module-private in firmsHeatmap.js and moved here with the live-data
|
|
* swap (2026-07-16).
|
|
*/
|
|
|
|
/**
|
|
* 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.
|
|
* @returns {Array<Object>} Internal fire records.
|
|
*/
|
|
export function adaptFirmsRecords(records) {
|
|
const fires = [];
|
|
if (!Array.isArray(records)) return fires;
|
|
const acqCache = new Map();
|
|
for (const record of records) {
|
|
const lat = Number(record?.lat);
|
|
const lon = Number(record?.lon);
|
|
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,
|
|
lon,
|
|
frp: finiteNumber(record.frp),
|
|
confidence: normalizeConfidence(record.confidence),
|
|
brightness: finiteNumber(record.brightness),
|
|
night: record.daynight === 'N',
|
|
acqMs: parseAcquisitionMs(record.acqDate, record.acqTime, acqCache),
|
|
sensor: normalizeSensor(record.instrument),
|
|
satellite: typeof record.satellite === 'string' ? record.satellite : '',
|
|
contextEntity: null,
|
|
position: null,
|
|
});
|
|
}
|
|
return fires;
|
|
}
|
|
|
|
/**
|
|
* Normalize FIRMS confidence to 0..1. VIIRS reports the categorical strings
|
|
* low/nominal/high (`l`/`n`/`h`); MODIS reports 0..100 numeric.
|
|
* @param {*} value - Raw confidence property.
|
|
* @returns {number} Confidence in 0..1.
|
|
*/
|
|
export function normalizeConfidence(value) {
|
|
if (typeof value === 'string') {
|
|
const text = value.trim().toLowerCase();
|
|
if (text === 'low' || text === 'l') return 0.3;
|
|
if (text === 'nominal' || text === 'n') return 0.6;
|
|
if (text === 'high' || text === 'h') return 0.9;
|
|
const numeric = Number(text);
|
|
return Number.isFinite(numeric) ? clamp01(numeric / 100) : 0;
|
|
}
|
|
const numeric = Number(value);
|
|
return Number.isFinite(numeric) ? clamp01(numeric / 100) : 0;
|
|
}
|
|
|
|
/**
|
|
* Parse acq_date ("YYYY-MM-DD") + acq_time (unpadded "HHMM", UTC) into epoch
|
|
* ms. Memoized per unique date:time pair — granule timestamps repeat heavily.
|
|
* @param {*} date - acq_date property.
|
|
* @param {*} time - acq_time property.
|
|
* @param {Map<string, number>} [cache] - Per-batch memo cache.
|
|
* @returns {number} Epoch milliseconds, or 0 when unparseable.
|
|
*/
|
|
export function parseAcquisitionMs(date, time, cache = new Map()) {
|
|
if (typeof date !== 'string' || date.length < 10) return 0;
|
|
const key = `${date}:${time ?? ''}`;
|
|
const cached = cache.get(key);
|
|
if (cached !== undefined) return cached;
|
|
|
|
// Same calendar as the CSV gate. Date.UTC would otherwise roll 30 February
|
|
// into March, and the fire's age would describe a detection that never happened.
|
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) {
|
|
cache.set(key, 0);
|
|
return 0;
|
|
}
|
|
const timeText = String(time ?? '').trim();
|
|
if (!/^\d{1,4}$/.test(timeText)) {
|
|
cache.set(key, 0);
|
|
return 0;
|
|
}
|
|
const hhmm = timeText.padStart(4, '0');
|
|
const year = Number(date.slice(0, 4));
|
|
const month = Number(date.slice(5, 7));
|
|
const day = Number(date.slice(8, 10));
|
|
const hours = Number(hhmm.slice(0, 2));
|
|
const minutes = Number(hhmm.slice(2, 4));
|
|
if (
|
|
month < 1 ||
|
|
month > 12 ||
|
|
day < 1 ||
|
|
day > 31 ||
|
|
hours > 23 ||
|
|
minutes > 59
|
|
) {
|
|
cache.set(key, 0);
|
|
return 0;
|
|
}
|
|
const ms = Date.UTC(year, month - 1, day, hours, minutes);
|
|
const parsed = new Date(ms);
|
|
const result =
|
|
parsed.getUTCFullYear() === year &&
|
|
parsed.getUTCMonth() === month - 1 &&
|
|
parsed.getUTCDate() === day
|
|
? ms
|
|
: 0;
|
|
cache.set(key, result);
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Map sensor/instrument strings to a short display sensor name.
|
|
* @param {*} value - Raw instrument property.
|
|
* @returns {string}
|
|
*/
|
|
export function normalizeSensor(value) {
|
|
const text = String(value || '').toUpperCase();
|
|
if (text.includes('VIIRS')) return 'VIIRS';
|
|
if (text.includes('MODIS')) return 'MODIS';
|
|
return text ? text.slice(0, 12) : '';
|
|
}
|
|
|
|
function clamp01(value) {
|
|
return Math.max(0, Math.min(1, value));
|
|
}
|
|
|
|
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() !== '')
|
|
);
|
|
}
|