Weight NWS weather-code mapping by precipitation probability
NWS text like 'Slight Chance Showers And Thunderstorms' (~17-24%) was mapping straight to WMO 95 / thunderstorm icon, over-stating a mere chance of storms. shortForecastToWmo now takes the period's precip probability (plus chance-like wording) and downgrades low-chance storms to light showers (80) and low-chance rain to light rain (61), keeping real/severe storms at 95. This also stops the false thunderstorm alert banner for low-chance days. Today/Monday for Rogers County now render as showers instead of storms.
This commit is contained in:
parent
32f749444e
commit
ebe81b6bfa
@ -57,14 +57,33 @@ export function parseWind(s) {
|
|||||||
return Math.max(...nums.map(Number))
|
return Math.max(...nums.map(Number))
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Map an NWS shortForecast string to a WMO code for icons/severity. */
|
/**
|
||||||
export function shortForecastToWmo(text) {
|
* Map an NWS shortForecast string to a WMO code for icons/severity.
|
||||||
|
* @param {string} text - NWS shortForecast text, e.g. "Slight Chance Showers And Thunderstorms"
|
||||||
|
* @param {number|null} prob - precipitation probability (0-100) for that period,
|
||||||
|
* so low-chance wording does not render as a full weather condition.
|
||||||
|
*/
|
||||||
|
export function shortForecastToWmo(text, prob = null) {
|
||||||
const t = (text || '').toLowerCase()
|
const t = (text || '').toLowerCase()
|
||||||
if (/thunderstorm|tstm|severe thunder/.test(t)) return /\bsevere\b/.test(t) ? 96 : 95
|
const chanceLike = () =>
|
||||||
if (/freez|x-ice|ice pellets/.test(t) && /rain|drizzle|shower/.test(t)) return 66
|
(prob != null && prob < 30) ||
|
||||||
if (/snow|blizzard|flurr|sleet|snow shower/.test(t)) return /heavy|blizzard/.test(t) ? 75 : 71
|
/\bslight chance\b|chance of|scattered|isolated|fewer chance|\bchance\b/.test(t)
|
||||||
if (/freez|ice/.test(t)) return 66
|
|
||||||
if (/rain|shower|drizzle|sprinkl/.test(t)) return /heavy|strong|torrential/.test(t) ? 65 : /slight|isolated|scattered/.test(t) ? 61 : 63
|
if (/thunderstorm|tstm/.test(t)) {
|
||||||
|
// An actual severe/thunderstorm is a real condition; a mere *chance* of
|
||||||
|
// one is just light showers until something organises.
|
||||||
|
const severe = /\bsevere\b|\bstrong\b|warning|watch/.test(t)
|
||||||
|
if (severe) return 95
|
||||||
|
if (chanceLike()) return 80 // light rain showers, not a full thunderstorm
|
||||||
|
return 95
|
||||||
|
}
|
||||||
|
if (/freez|x-ice|ice pellets|freezing/.test(t) && /rain|drizzle|shower/.test(t)) return 66
|
||||||
|
if (/snow|blizzard|flurr|snow shower/.test(t)) return /heavy|blizzard/.test(t) ? 75 : 71
|
||||||
|
if (/rain|shower|drizzle|sprinkl/.test(t)) {
|
||||||
|
if (/heavy|strong|torrential/.test(t)) return 65
|
||||||
|
if (chanceLike()) return 61
|
||||||
|
return 63
|
||||||
|
}
|
||||||
if (/fog|haze|smoke|brume/.test(t)) return 45
|
if (/fog|haze|smoke|brume/.test(t)) return 45
|
||||||
if (/partly (cloudy|sunny)/.test(t)) return 2
|
if (/partly (cloudy|sunny)/.test(t)) return 2
|
||||||
if (/overcast|covering|cloudy/.test(t)) return 3
|
if (/overcast|covering|cloudy/.test(t)) return 3
|
||||||
@ -123,7 +142,7 @@ function buildDaily(periods, tz, units) {
|
|||||||
else d.lows.push(c)
|
else d.lows.push(c)
|
||||||
}
|
}
|
||||||
d.precip.push(prob(pd))
|
d.precip.push(prob(pd))
|
||||||
d.codes.push(shortForecastToWmo(pd.shortForecast))
|
d.codes.push(shortForecastToWmo(pd.shortForecast, prob(pd)))
|
||||||
const ws = parseWind(pd.windSpeed)
|
const ws = parseWind(pd.windSpeed)
|
||||||
if (ws != null) d.winds.push(ws)
|
if (ws != null) d.winds.push(ws)
|
||||||
}
|
}
|
||||||
@ -166,7 +185,7 @@ export function normalizeNWS(src) {
|
|||||||
temperature_2m: temp(cur.temperature, units),
|
temperature_2m: temp(cur.temperature, units),
|
||||||
apparent_temperature: apparent,
|
apparent_temperature: apparent,
|
||||||
relative_humidity_2m: cur.relativeHumidity?.value ?? null,
|
relative_humidity_2m: cur.relativeHumidity?.value ?? null,
|
||||||
weather_code: shortForecastToWmo(cur.shortForecast),
|
weather_code: shortForecastToWmo(cur.shortForecast, prob(cur)),
|
||||||
wind_speed_10m: ws != null ? speed(ws, units) : null,
|
wind_speed_10m: ws != null ? speed(ws, units) : null,
|
||||||
wind_direction_10m: compassToDeg(cur.windDirection),
|
wind_direction_10m: compassToDeg(cur.windDirection),
|
||||||
wind_gusts_10m: null,
|
wind_gusts_10m: null,
|
||||||
@ -182,7 +201,7 @@ export function normalizeNWS(src) {
|
|||||||
time.push(localTimeString(pd.startTime, tz))
|
time.push(localTimeString(pd.startTime, tz))
|
||||||
temperature_2m.push(temp(pd.temperature, units))
|
temperature_2m.push(temp(pd.temperature, units))
|
||||||
precipitation_probability.push(prob(pd))
|
precipitation_probability.push(prob(pd))
|
||||||
weather_code.push(shortForecastToWmo(pd.shortForecast))
|
weather_code.push(shortForecastToWmo(pd.shortForecast, prob(pd)))
|
||||||
const ws = parseWind(pd.windSpeed)
|
const ws = parseWind(pd.windSpeed)
|
||||||
wind_speed_10m.push(ws != null ? speed(ws, units) : null)
|
wind_speed_10m.push(ws != null ? speed(ws, units) : null)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,12 +18,26 @@ describe('nws helpers', () => {
|
|||||||
expect(shortForecastToWmo('Mostly Sunny')).toBe(2)
|
expect(shortForecastToWmo('Mostly Sunny')).toBe(2)
|
||||||
expect(shortForecastToWmo('Partly Cloudy')).toBe(2)
|
expect(shortForecastToWmo('Partly Cloudy')).toBe(2)
|
||||||
expect(shortForecastToWmo('Overcast')).toBe(3)
|
expect(shortForecastToWmo('Overcast')).toBe(3)
|
||||||
expect(shortForecastToWmo('Slight Chance Showers And Thunderstorms')).toBe(95)
|
|
||||||
expect(shortForecastToWmo('Heavy Rain')).toBe(65)
|
expect(shortForecastToWmo('Heavy Rain')).toBe(65)
|
||||||
expect(shortForecastToWmo('Light Snow')).toBe(71)
|
expect(shortForecastToWmo('Light Snow')).toBe(71)
|
||||||
expect(shortForecastToWmo('Areas of Fog')).toBe(45)
|
expect(shortForecastToWmo('Areas of Fog')).toBe(45)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('downgrades low-chance thunderstorms so they are not shown as storms', () => {
|
||||||
|
// The word matters less than the raw probability.
|
||||||
|
// 17% "slight chance" of showers/thunderstorms -> light rain showers, NOT a storm.
|
||||||
|
expect(shortForecastToWmo('Slight Chance Showers And Thunderstorms')).toBe(80)
|
||||||
|
expect(shortForecastToWmo('Slight Chance Showers And Thunderstorms', 17)).toBe(80)
|
||||||
|
expect(shortForecastToWmo('Thunderstorms', 20)).toBe(80)
|
||||||
|
expect(shortForecastToWmo('Isolated Thunderstorms', 20)).toBe(80)
|
||||||
|
// A real storm (wordy + high probability) stays a storm.
|
||||||
|
expect(shortForecastToWmo('Thunderstorms', 70)).toBe(95)
|
||||||
|
expect(shortForecastToWmo('Severe Thunderstorm Warning', 80)).toBe(95)
|
||||||
|
// Low-chance plain rain downgrades to light rain too.
|
||||||
|
expect(shortForecastToWmo('Chance of Rain', 15)).toBe(61)
|
||||||
|
expect(shortForecastToWmo('Showers And Thunderstorms', 50)).toBe(95)
|
||||||
|
})
|
||||||
|
|
||||||
it('compassToDeg maps compass points to degrees', () => {
|
it('compassToDeg maps compass points to degrees', () => {
|
||||||
expect(compassToDeg('N')).toBe(0)
|
expect(compassToDeg('N')).toBe(0)
|
||||||
expect(compassToDeg('NE')).toBe(45)
|
expect(compassToDeg('NE')).toBe(45)
|
||||||
@ -69,6 +83,10 @@ describe('normalizeNWS (recorded live fixture for Rogers County, OK)', () => {
|
|||||||
expect(Math.max(...data.daily.temperature_2m_max)).toBeLessThan(115)
|
expect(Math.max(...data.daily.temperature_2m_max)).toBeLessThan(115)
|
||||||
expect(data.daily.temperature_2m_min[0]).toBe(75)
|
expect(data.daily.temperature_2m_min[0]).toBe(75)
|
||||||
expect(data.daily.precipitation_probability_max[0]).toBe(24)
|
expect(data.daily.precipitation_probability_max[0]).toBe(24)
|
||||||
|
// Today/Monday are only ~17-24% "slight chance" of storms -> light showers.
|
||||||
|
expect(data.daily.weather_code[0]).toBe(80)
|
||||||
|
expect(data.daily.weather_code[1]).toBe(80)
|
||||||
|
expect(data.current.weather_code).toBe(2) // "Mostly Sunny" hour
|
||||||
})
|
})
|
||||||
|
|
||||||
it('builds a realistic current from the first hourly period', () => {
|
it('builds a realistic current from the first hourly period', () => {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user