2026-04-10 12:49:19 -04:00
import { DateTime } from '../../vendor/auto/luxon.mjs' ;
const LOOKAHEAD _HOURS = 6 ;
const METERS _PER _MILE = 1609.344 ;
const KPH _PER _MPH = 1.609344 ;
const SEVERITY _RANK = {
Extreme : 2 ,
Severe : 1 ,
} ;
const RULE _PRIORITY = {
2026-04-13 14:38:23 -04:00
tropical : 6 ,
2026-04-10 12:49:19 -04:00
thunderstorm : 5 ,
freezing : 4 ,
snow : 3 ,
rain : 2 ,
wind : 1 ,
} ;
const WEATHER _CODES = {
freezing : new Set ( [ 56 , 57 , 66 , 67 ] ) ,
snow : new Set ( [ 71 , 73 , 75 , 77 , 85 , 86 ] ) ,
thunderstorm : new Set ( [ 95 , 96 , 99 ] ) ,
rain : new Set ( [ 51 , 53 , 55 , 61 , 63 , 65 , 80 , 81 , 82 ] ) ,
} ;
const thresholds = {
lowVisibilitySevere : 5 * METERS _PER _MILE ,
lowVisibilityExtreme : 2 * METERS _PER _MILE ,
gustSevere : 20 * KPH _PER _MPH ,
gustExtreme : 35 * KPH _PER _MPH ,
highWindSevere : 40 * KPH _PER _MPH ,
highWindExtreme : 55 * KPH _PER _MPH ,
2026-04-13 14:38:23 -04:00
tropicalWindSevere : 50 ,
tropicalWindExtreme : 63 ,
tropicalGustSevere : 75 ,
tropicalGustExtreme : 90 ,
tropicalPressureSevere : 1002 ,
tropicalPressureExtreme : 998 ,
2026-04-10 12:49:19 -04:00
freezingTempC : 1 ,
} ;
2026-04-13 14:38:23 -04:00
const buildDerivedHazard = ( {
id ,
severity ,
description ,
priority ,
event = 'Severe Weather Alert' ,
} ) => ( {
2026-04-10 12:49:19 -04:00
id ,
priority ,
properties : {
2026-04-13 14:38:23 -04:00
event ,
2026-04-10 12:49:19 -04:00
severity ,
urgency : 'Expected' ,
description : ` This is a derived local alert based on forecast conditions. ${ description } ` ,
} ,
} ) ;
const getUpcomingHours = ( weatherParameters ) => {
const zone = weatherParameters ? . timeZone || 'UTC' ;
const now = DateTime . now ( ) . setZone ( zone ) ;
const end = now . plus ( { hours : LOOKAHEAD _HOURS } ) ;
const allHours = Object . values ( weatherParameters ? . forecast ? ? { } )
. flatMap ( ( day ) => day ? . hours ? ? [ ] )
. map ( ( hour ) => ( {
... hour ,
forecastTime : DateTime . fromISO ( hour . time , { zone } ) ,
} ) )
. filter ( ( hour ) => hour . forecastTime . isValid )
. sort ( ( a , b ) => a . forecastTime . toMillis ( ) - b . forecastTime . toMillis ( ) ) ;
return allHours . filter ( ( hour ) => hour . forecastTime >= now && hour . forecastTime <= end ) ;
} ;
const getWorstHour = ( hours , evaluator ) => hours . reduce ( ( worst , hour ) => {
const candidate = evaluator ( hour ) ;
if ( ! candidate ) return worst ;
if ( ! worst ) return candidate ;
if ( SEVERITY _RANK [ candidate . severity ] > SEVERITY _RANK [ worst . severity ] ) return candidate ;
return worst ;
} , null ) ;
const evaluateThunderstorm = ( hours ) => getWorstHour ( hours , ( hour ) => {
const code = Number ( hour . weather _code ? ? 0 ) ;
if ( ! WEATHER _CODES . thunderstorm . has ( code ) ) return null ;
if ( code === 96 || code === 99 ) {
return {
severity : 'Extreme' ,
description : 'Thunderstorms with hail are possible in the next several hours and may create dangerous outdoor conditions.' ,
} ;
}
return {
severity : 'Severe' ,
description : 'Thunderstorms are possible in the next several hours and may create hazardous outdoor conditions.' ,
} ;
} ) ;
const evaluateFreezing = ( hours ) => getWorstHour ( hours , ( hour ) => {
const code = Number ( hour . weather _code ? ? 0 ) ;
if ( ! WEATHER _CODES . freezing . has ( code ) ) return null ;
const temperature = hour . temperature _2m ? ? Number . POSITIVE _INFINITY ;
if ( temperature > thresholds . freezingTempC ) return null ;
const visibility = hour . visibility ? ? Number . POSITIVE _INFINITY ;
const gusts = hour . wind _gusts _10m ? ? 0 ;
const isExtreme = visibility <= thresholds . lowVisibilityExtreme || gusts >= thresholds . gustExtreme ;
return {
severity : isExtreme ? 'Extreme' : 'Severe' ,
description : isExtreme
? 'Freezing precipitation with poor visibility or strong gusts is expected in the next several hours and may create dangerous travel conditions.'
: 'Freezing precipitation is expected in the next several hours and may create slippery travel conditions.' ,
} ;
} ) ;
const evaluateSnow = ( hours ) => getWorstHour ( hours , ( hour ) => {
const code = Number ( hour . weather _code ? ? 0 ) ;
if ( ! WEATHER _CODES . snow . has ( code ) ) return null ;
const visibility = hour . visibility ? ? Number . POSITIVE _INFINITY ;
const gusts = hour . wind _gusts _10m ? ? 0 ;
if ( visibility <= thresholds . lowVisibilitySevere && gusts >= thresholds . gustSevere ) {
return {
severity : visibility <= thresholds . lowVisibilityExtreme ? 'Extreme' : 'Severe' ,
description : visibility <= thresholds . lowVisibilityExtreme
? 'Snow, poor visibility, and gusty winds are expected in the next several hours and may create dangerous travel conditions.'
: 'Snow, reduced visibility, and gusty winds are expected in the next several hours and may create hazardous travel conditions.' ,
} ;
}
return null ;
} ) ;
const evaluateRain = ( hours ) => getWorstHour ( hours , ( hour ) => {
const code = Number ( hour . weather _code ? ? 0 ) ;
const visibility = hour . visibility ? ? Number . POSITIVE _INFINITY ;
const gusts = hour . wind _gusts _10m ? ? 0 ;
const hasRain = WEATHER _CODES . rain . has ( code ) || ( hour . rain ? ? 0 ) > 0 || ( hour . showers ? ? 0 ) > 0 ;
if ( ! hasRain ) return null ;
if ( visibility <= thresholds . lowVisibilityExtreme && gusts >= thresholds . gustExtreme ) {
return {
severity : 'Extreme' ,
description : 'Heavy rain, very low visibility, and strong gusts are expected in the next several hours and may create dangerous travel conditions.' ,
} ;
}
if ( visibility <= thresholds . lowVisibilitySevere && gusts >= thresholds . gustSevere ) {
return {
severity : 'Severe' ,
description : 'Heavy rain, reduced visibility, and gusty winds are expected in the next several hours and may create hazardous travel conditions.' ,
} ;
}
return null ;
} ) ;
2026-04-13 14:38:23 -04:00
const evaluateTropical = ( hours ) => getWorstHour ( hours , ( hour ) => {
const code = Number ( hour . weather _code ? ? 0 ) ;
const sustainedWind = hour . wind _speed _10m ? ? 0 ;
const gusts = hour . wind _gusts _10m ? ? 0 ;
const pressureHpa = hour . pressure _msl ? ? Number . POSITIVE _INFINITY ;
const visibility = hour . visibility ? ? Number . POSITIVE _INFINITY ;
const hasRain = WEATHER _CODES . rain . has ( code ) || ( hour . rain ? ? 0 ) > 0 || ( hour . showers ? ? 0 ) > 0 ;
if ( ! hasRain ) return null ;
const meetsExtreme = (
( sustainedWind >= thresholds . tropicalWindExtreme || gusts >= thresholds . tropicalGustExtreme )
&& pressureHpa <= thresholds . tropicalPressureExtreme
) ;
if ( meetsExtreme ) {
return {
severity : 'Extreme' ,
description : visibility <= thresholds . lowVisibilitySevere
? 'Tropical storm conditions are expected in the next several hours, with very strong winds, heavy rain, poor visibility, and dangerous travel conditions.'
: 'Tropical storm conditions are expected in the next several hours, with very strong winds, heavy rain, and dangerous travel conditions.' ,
} ;
}
const meetsSevere = (
( sustainedWind >= thresholds . tropicalWindSevere || gusts >= thresholds . tropicalGustSevere )
&& pressureHpa <= thresholds . tropicalPressureSevere
) ;
if ( meetsSevere ) {
return {
severity : 'Severe' ,
description : 'Tropical storm conditions are possible in the next several hours, including heavy rain, strong winds, and dangerous travel conditions.' ,
} ;
}
return null ;
} ) ;
2026-04-10 12:49:19 -04:00
const evaluateWind = ( hours ) => getWorstHour ( hours , ( hour ) => {
const gusts = hour . wind _gusts _10m ? ? 0 ;
if ( gusts >= thresholds . highWindExtreme ) {
return {
severity : 'Extreme' ,
description : 'Very strong wind gusts are expected in the next several hours and may create dangerous conditions for travel and outdoor activity.' ,
} ;
}
if ( gusts >= thresholds . highWindSevere ) {
return {
severity : 'Severe' ,
description : 'Strong wind gusts are expected in the next several hours and may create hazardous conditions for travel and outdoor activity.' ,
} ;
}
return null ;
} ) ;
const deriveHazards = ( weatherParameters ) => {
const upcomingHours = getUpcomingHours ( weatherParameters ) ;
if ( upcomingHours . length === 0 ) return [ ] ;
2026-04-13 14:38:23 -04:00
const tropicalCandidate = evaluateTropical ( upcomingHours ) ;
2026-04-10 12:49:19 -04:00
const thunderstormCandidate = evaluateThunderstorm ( upcomingHours ) ;
const freezingCandidate = evaluateFreezing ( upcomingHours ) ;
const snowCandidate = evaluateSnow ( upcomingHours ) ;
const rainCandidate = evaluateRain ( upcomingHours ) ;
const windCandidate = evaluateWind ( upcomingHours ) ;
const candidates = [
2026-04-13 14:38:23 -04:00
tropicalCandidate && buildDerivedHazard ( {
id : 'derived-severe-weather-alert-tropical' ,
priority : RULE _PRIORITY . tropical ,
event : 'Tropical Storm Alert' ,
... tropicalCandidate ,
} ) ,
2026-04-10 12:49:19 -04:00
thunderstormCandidate && buildDerivedHazard ( {
id : 'derived-severe-weather-alert-thunderstorm' ,
priority : RULE _PRIORITY . thunderstorm ,
... thunderstormCandidate ,
} ) ,
freezingCandidate && buildDerivedHazard ( {
id : 'derived-severe-weather-alert-freezing' ,
priority : RULE _PRIORITY . freezing ,
... freezingCandidate ,
} ) ,
snowCandidate && buildDerivedHazard ( {
id : 'derived-severe-weather-alert-snow' ,
priority : RULE _PRIORITY . snow ,
... snowCandidate ,
} ) ,
rainCandidate && buildDerivedHazard ( {
id : 'derived-severe-weather-alert-rain' ,
priority : RULE _PRIORITY . rain ,
... rainCandidate ,
} ) ,
windCandidate && buildDerivedHazard ( {
id : 'derived-severe-weather-alert-wind' ,
priority : RULE _PRIORITY . wind ,
... windCandidate ,
} ) ,
] . filter ( Boolean ) ;
if ( candidates . length === 0 ) return [ ] ;
candidates . sort ( ( a , b ) => {
const severityDiff = SEVERITY _RANK [ b . properties . severity ] - SEVERITY _RANK [ a . properties . severity ] ;
if ( severityDiff !== 0 ) return severityDiff ;
return b . priority - a . priority ;
} ) ;
return [ candidates [ 0 ] ] ;
} ;
export default deriveHazards ;