fix(climate): broaden natural event filter to accept EONET sources (#2718)

* fix(climate): broaden natural event filter to accept EONET sources

The isClimateNaturalEvent filter only accepted events with sourceName
"NASA FIRMS" or "GDACS", but natural:events:v1 has EONET events with
source IDs (e.g. EONET_2931). Result: 23 raw events, 0 matched.

Fix: accept EONET IDs and any event with a sourceName/URL for climate
categories (floods, wildfires, volcanoes, drought). Severe storms
remain GDACS-only.

* fix(review): add volcano/drought type mapping, remove source whitelist guard

P1: mapNaturalType had no cases for volcanoes/drought, returning ''
which caused mapNaturalEvent to discard them.

P1: mapNaturalEvent guarded source !== 'GDACS' && source !== 'NASA FIRMS',
dropping all EONET/OTHER events even after isClimateNaturalEvent passed them.
Changed to !source (only reject truly unknown sources).

P2: Added full pipeline test for EONET volcano, drought, and flood events.
Moved NHC rejection test to isClimateNaturalEvent (correct filter layer).
This commit is contained in:
Elie Habib
2026-04-05 09:22:07 +04:00
committed by GitHub
parent 4985bb4978
commit db3ac984cd
2 changed files with 62 additions and 30 deletions

View File

@@ -99,15 +99,19 @@ function getNaturalSourceMeta(event) {
const id = String(event?.id || '');
if (name === 'nasa firms' || name.startsWith('firms') || url.includes('firms.modaps.')) return { source: 'NASA FIRMS' };
if (name === 'gdacs' || name.startsWith('gdacs') || url.includes('gdacs.org') || id.startsWith('gdacs-')) return { source: 'GDACS' };
if (url.includes('eonet.') || id.startsWith('EONET_') || name.startsWith('eonet')) return { source: 'EONET' };
if (name || url) return { source: 'OTHER' };
return null;
}
const CLIMATE_CATEGORIES = new Set(['floods', 'wildfires', 'volcanoes', 'drought']);
function isClimateNaturalEvent(event) {
if (!event || typeof event !== 'object') return false;
const sourceMeta = getNaturalSourceMeta(event);
if (!sourceMeta) return false;
if (event.category === 'floods' || event.category === 'wildfires') return true;
if (CLIMATE_CATEGORIES.has(event.category)) return true;
if (event.category !== 'severeStorms') return false;
if (sourceMeta.source !== 'GDACS') return false;
@@ -120,6 +124,8 @@ function mapNaturalType(event) {
if (event.category === 'floods') return 'flood';
if (event.category === 'wildfires') return 'wildfire';
if (event.category === 'severeStorms') return 'cyclone';
if (event.category === 'volcanoes') return 'volcano';
if (event.category === 'drought') return 'drought';
return '';
}
@@ -333,7 +339,7 @@ function mapNaturalEvent(event) {
if (!type) return null;
const source = mapNaturalSource(event);
if (source !== 'GDACS' && source !== 'NASA FIRMS') return null;
if (!source) return null;
const severity = mapNaturalSeverity(event, source);
const status = mapNaturalStatus(event, severity);
const lat = Number(event.lat);

View File

@@ -38,23 +38,13 @@ describe('seed-climate-disasters helpers', () => {
assert.equal(getReliefWebAppname(), null);
});
it('only reuses GDACS or NASA FIRMS items from natural:events:v1', () => {
assert.equal(
isClimateNaturalEvent({ category: 'floods', sourceName: 'GDACS', id: 'gdacs-FL-123' }),
true,
);
assert.equal(
isClimateNaturalEvent({ category: 'wildfires', sourceName: 'NASA FIRMS', id: 'EONET_1' }),
true,
);
assert.equal(
isClimateNaturalEvent({ category: 'severeStorms', sourceName: 'NHC', stormName: 'Alfred', id: 'nhc-AL01-1' }),
false,
);
assert.equal(
isClimateNaturalEvent({ category: 'wildfires', sourceName: 'Volcanic Ash Advisory', id: 'EONET_2' }),
false,
);
it('accepts climate events from known sources and rejects unrecognized ones', () => {
assert.equal(isClimateNaturalEvent({ category: 'floods', sourceName: 'GDACS', id: 'gdacs-FL-123' }), true);
assert.equal(isClimateNaturalEvent({ category: 'wildfires', sourceName: 'NASA FIRMS', id: 'EONET_1' }), true);
assert.equal(isClimateNaturalEvent({ category: 'wildfires', sourceName: 'Volcanic Ash Advisory', id: 'EONET_2' }), true);
assert.equal(isClimateNaturalEvent({ category: 'volcanoes', sourceName: '', id: 'EONET_3' }), true);
assert.equal(isClimateNaturalEvent({ category: 'severeStorms', sourceName: 'NHC', stormName: 'Alfred', id: 'nhc-AL01-1' }), false);
assert.equal(isClimateNaturalEvent({ category: 'floods', sourceName: '', id: '' }), false);
});
it('preserves supported natural-event provenance and rejects unsupported rows', () => {
@@ -89,21 +79,57 @@ describe('seed-climate-disasters helpers', () => {
assert.equal(gdacsEvent.source, 'GDACS');
assert.equal(gdacsEvent.severity, 'red');
// NHC severe storms are filtered by isClimateNaturalEvent (GDACS-only),
// not by mapNaturalEvent. mapNaturalEvent accepts any known source.
assert.equal(
mapNaturalEvent({
id: 'nhc-AL01-1',
category: 'severeStorms',
title: 'Tropical Storm Alfred',
sourceName: 'NHC',
sourceUrl: 'https://www.nhc.noaa.gov/',
date: 1_700_000_000_000,
lat: 20,
lon: -70,
}),
null,
isClimateNaturalEvent({ category: 'severeStorms', sourceName: 'NHC', stormName: 'Alfred', id: 'nhc-AL01-1' }),
false,
);
});
it('maps EONET-sourced volcano and drought events through the full pipeline', () => {
const volcanoEvent = mapNaturalEvent({
id: 'EONET_5001',
category: 'volcanoes',
title: 'Etna eruption',
sourceName: 'SIVolcano',
sourceUrl: 'https://volcano.si.edu/',
date: 1_700_000_000_000,
lat: 37.75,
lon: 14.99,
});
assert.ok(volcanoEvent, 'EONET volcano should not be dropped');
assert.equal(volcanoEvent.type, 'volcano');
assert.equal(volcanoEvent.source, 'EONET');
const droughtEvent = mapNaturalEvent({
id: 'EONET_5002',
category: 'drought',
title: 'East Africa drought',
sourceName: 'FEWS NET',
sourceUrl: 'https://fews.net/',
date: 1_700_000_000_000,
lat: 1.0,
lon: 38.0,
});
assert.ok(droughtEvent, 'EONET drought should not be dropped');
assert.equal(droughtEvent.type, 'drought');
const eonetFlood = mapNaturalEvent({
id: 'EONET_5003',
category: 'floods',
title: 'Flooding in Bangladesh',
sourceName: '',
sourceUrl: 'https://eonet.gsfc.nasa.gov/',
date: 1_700_000_000_000,
lat: 23.8,
lon: 90.4,
});
assert.ok(eonetFlood, 'EONET flood should not be dropped');
assert.equal(eonetFlood.type, 'flood');
assert.equal(eonetFlood.source, 'EONET');
});
it('derives country codes from coordinates when natural-event text lacks a country', () => {
assert.equal(findCountryCodeByCoordinates(35.6762, 139.6503), 'JP');