From db3ac984cda5d99a9c555ad0456839edb6be49e3 Mon Sep 17 00:00:00 2001 From: Elie Habib Date: Sun, 5 Apr 2026 09:22:07 +0400 Subject: [PATCH] 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). --- scripts/seed-climate-disasters.mjs | 10 +++- tests/climate-disasters-seed.test.mjs | 82 ++++++++++++++++++--------- 2 files changed, 62 insertions(+), 30 deletions(-) diff --git a/scripts/seed-climate-disasters.mjs b/scripts/seed-climate-disasters.mjs index 409010e87..725eada24 100644 --- a/scripts/seed-climate-disasters.mjs +++ b/scripts/seed-climate-disasters.mjs @@ -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); diff --git a/tests/climate-disasters-seed.test.mjs b/tests/climate-disasters-seed.test.mjs index df98d5aad..7ef5bcf8d 100644 --- a/tests/climate-disasters-seed.test.mjs +++ b/tests/climate-disasters-seed.test.mjs @@ -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');