From 41acfcd5b93cb8eebf499bd2cd21486fd25947b6 Mon Sep 17 00:00:00 2001 From: fraxle Date: Thu, 4 Jun 2026 16:20:31 +0100 Subject: [PATCH] 2.6.1 Update wind and rain measures - now using Beaufort Scale --- assets/js/config.js | 2 +- assets/js/events/weather-checks.js | 124 +++++++++++++++++------------ 2 files changed, 74 insertions(+), 52 deletions(-) diff --git a/assets/js/config.js b/assets/js/config.js index 4864ccd..6ea8f74 100644 --- a/assets/js/config.js +++ b/assets/js/config.js @@ -202,7 +202,7 @@ export const COL_DESCRIPTIONS = { air: { title: 'Air Temperature', desc: 'Measured air temperature at 2 m above the ground. This is the standard thermometer reading — it does not account for sun, wind, or humidity.' }, rh: { title: 'Relative Humidity', desc: 'How much moisture the air holds relative to its maximum capacity at that temperature. High RH makes warm days feel stickier and cold days feel rawer.' }, dew: { title: 'Dew Point', desc: 'The temperature at which air becomes saturated and moisture begins to condense. A useful measure of absolute humidity — above 16 °C it starts to feel muggy; above 21 °C it feels oppressive.' }, - wind: { title: 'Wind Speed', desc: 'Mean wind speed at 10 m in mph, with peak gust in brackets where significantly higher. Gust colour indicates strength: amber 25-39 mph, orange 40-54 mph, red 55+ mph. Wind dramatically increases heat loss from exposed skin — the basis of wind chill.' }, + wind: { title: 'Wind Speed', desc: 'Mean wind speed at 10 m in mph, with peak gust in brackets where significantly higher. Gust colour indicates Beaufort scale severity: amber = Near Gale (32+ mph), orange = Gale (39+ mph), red = Severe Gale or above (55+ mph). Banners above the table classify conditions from Strong Breeze through to Violent Storm using standard wind categories. Wind dramatically increases heat loss from exposed skin — the basis of wind chill.' }, dir: { title: 'Wind Direction', desc: 'The compass direction the wind is blowing from, shown as an arrow and abbreviated label (e.g. SW = south-westerly).' }, cloud: { title: 'Cloud Cover', desc: 'Total cloud cover as a percentage of the sky. High cloud blocks solar radiation and reduces both daytime heating and overnight cooling.' }, sun: { title: 'Sun Elevation', desc: 'The angle of the sun above the horizon in degrees. Below 0° the sun has set. The higher the elevation, the more intense the solar radiation reaching the ground.' }, diff --git a/assets/js/events/weather-checks.js b/assets/js/events/weather-checks.js index 2713934..a44ce16 100644 --- a/assets/js/events/weather-checks.js +++ b/assets/js/events/weather-checks.js @@ -116,45 +116,63 @@ export function checkHeatSpike(rows) { // wet + calm → rain icon (🌧️) // windy + wet → storm icon (🌩️), and the wind/rain icons are suppressed // for that hour so a single hour never shows two icons. -const GUST_BLUSTERY = 25 / 2.237; // m/s (= 25 mph) - Beaufort Force 6 threshold -const GUST_STORM = 20; // m/s ≈ 45 mph - Beaufort Force 8+ threshold -const PRECIP_WET = 5; // mm/h - meaningful rain -const PRECIP_HEAVY = 10; // mm/h - heavy rain +const GUST_BLUSTERY = 25 / 2.237; // m/s = 25 mph - Strong Breeze (Force 6) +const GUST_STORM = 39 / 2.237; // m/s = 39 mph - Gale (Force 8) - storm combo trigger +const PRECIP_MODERATE = 2.5; // mm/h - moderate rain threshold +const PRECIP_HEAVY = 7.6; // mm/h - heavy rain (>0.3 in/hr) +const PRECIP_TORRENTIAL = 20; // mm/h - torrential / very heavy -// Beaufort scale lookup - force number and name from gust speed in m/s -function beaufortForce(ms) { +// Wind category lookup from gust speed in m/s. +// Uses simplified 6-tier scale matching common forecast language. +function windCategory(ms) { const mph = ms * 2.237; - if (mph >= 73) return { force: 12, name: 'Hurricane Force' }; - if (mph >= 64) return { force: 11, name: 'Violent Storm' }; - if (mph >= 55) return { force: 10, name: 'Storm Force' }; - if (mph >= 47) return { force: 9, name: 'Strong Gale' }; - if (mph >= 39) return { force: 8, name: 'Gale' }; - if (mph >= 32) return { force: 7, name: 'Near Gale' }; - return { force: 6, name: 'Strong Breeze' }; + if (mph >= 96) return { tier: 6, name: 'Violent Storm', advisory: 'Wind Advisory' }; + if (mph >= 74) return { tier: 5, name: 'Storm Force', advisory: 'Storm Warning' }; + if (mph >= 55) return { tier: 4, name: 'Severe Gale', advisory: 'Gale Warning' }; + if (mph >= 39) return { tier: 3, name: 'Gale', advisory: 'Gale Warning' }; + if (mph >= 32) return { tier: 2, name: 'Near Gale', advisory: 'Wind Advisory' }; + return { tier: 1, name: 'Strong Breeze', advisory: null }; } -const BEAUFORT_DESC = { - 6: 'Large branches in motion; umbrellas used with difficulty.', - 7: 'Whole trees in motion; inconvenience felt when walking against the wind.', - 8: 'Twigs break off trees and generally impedes progress.', - 9: 'Slight structural damage possible — chimney pots and slates at risk.', - 10: 'Trees uprooted; considerable structural damage expected.', - 11: 'Very rarely experienced inland; widespread damage expected.', - 12: 'Devastating conditions — avoid all outdoor activity.', +const WIND_DESC = { + 1: 'Large branches in motion; umbrellas used with difficulty.', + 2: 'Whole trees in motion; inconvenience felt when walking against the wind.', + 3: 'Twigs break off trees; progress on foot generally impeded.', + 4: 'Slight structural damage possible — chimney pots and slates at risk.', + 5: 'Trees uprooted; considerable structural damage expected.', + 6: 'Very rarely experienced inland; widespread damage — avoid all outdoor activity.', }; -// Banner colour scales with severity -function windBannerColors(force) { - if (force >= 11) return { color: '#0c1020', textColor: '#c0d8ff' }; - if (force >= 9) return { color: '#101828', textColor: '#c0d4f0' }; - if (force >= 7) return { color: '#141e2e', textColor: '#c0d0e8' }; - return { color: '#1a2030', textColor: '#c0d8f0' }; +// Rain intensity lookup from mm/h +function rainCategory(mmh) { + if (mmh >= PRECIP_TORRENTIAL) return { name: 'Torrential Rain', desc: 'extreme rainfall' }; + if (mmh >= PRECIP_HEAVY) return { name: 'Heavy Rain', desc: 'heavy rainfall' }; + if (mmh >= PRECIP_MODERATE) return { name: 'Moderate Rain', desc: 'moderate rainfall' }; + return { name: 'Rain', desc: 'light rain' }; +} + +// Combined wind+rain event naming +function stormTitle(windTier, rainMmh) { + const isHeavyRain = rainMmh >= PRECIP_HEAVY; + if (windTier >= 5) return 'Severe Storm'; + if (windTier >= 4) return isHeavyRain ? 'Windstorm with Heavy Rain' : 'Windstorm with Rain'; + if (windTier >= 3) return isHeavyRain ? 'Gales with Heavy Rain' : 'Gales with Rain'; + if (windTier >= 2) return isHeavyRain ? 'Blustery Heavy Rain' : 'Blustery Rain'; + return isHeavyRain ? 'Blustery Showers' : 'Blustery Rain'; +} + +// Banner colour scales with wind severity +function windBannerColors(tier) { + if (tier >= 5) return { color: '#0c1020', textColor: '#c0d8ff' }; + if (tier >= 4) return { color: '#101828', textColor: '#c0d4f0' }; + if (tier >= 2) return { color: '#141e2e', textColor: '#c0d0e8' }; + return { color: '#1a2030', textColor: '#c0d8f0' }; } const gustOf = r => r.gust ?? r.va ?? 0; const precipOf = r => r.precip ?? 0; const isWindyHour = r => { const g = gustOf(r); return isFinite(g) && g >= GUST_BLUSTERY; }; -const isWetHour = r => { const p = precipOf(r); return isFinite(p) && p >= PRECIP_WET; }; +const isWetHour = r => { const p = precipOf(r); return isFinite(p) && p >= PRECIP_MODERATE; }; const isStormHour = r => isWindyHour(r) && isWetHour(r); export function checkWind(rows) { @@ -165,19 +183,17 @@ export function checkWind(rows) { const maxGust = Math.max(...affected.map(gustOf).filter(isFinite)); const mph = Math.round(maxGust * 2.237); - const { force, name } = beaufortForce(maxGust); - const desc = BEAUFORT_DESC[force] || ''; - const { color, textColor } = windBannerColors(force); + const { tier, name, advisory } = windCategory(maxGust); + const desc = WIND_DESC[tier] || ''; + const { color, textColor } = windBannerColors(tier); - const title = force >= 10 ? `${name} Winds` - : force >= 8 ? `${name} Warning` - : name; + const title = advisory ? `${advisory} — ${name}` : name; return { id: 'wind', - emoji: force >= 10 ? '🌪️' : '💨', + emoji: tier >= 5 ? '🌪️' : '💨', title, - message: `Gusts to ${mph} mph (Beaufort Force ${force}). ${desc}`, + message: `Gusts to ${mph} mph. ${desc}`, color, textColor, type: 'weather', @@ -193,20 +209,25 @@ export function checkWet(rows) { const isoRange = [affected[0].iso, affected[affected.length - 1].iso]; const maxPrecip = Math.max(...affected.map(precipOf).filter(isFinite)); - const isHeavy = maxPrecip >= PRECIP_HEAVY; + const { name: rainName } = rainCategory(maxPrecip); + + const isTorrential = maxPrecip >= PRECIP_TORRENTIAL; + const isHeavy = maxPrecip >= PRECIP_HEAVY; return { id: 'wet', emoji: '🌧️', - title: 'Heavy Rain', - message: isHeavy - ? `Heavy rain expected — up to ${maxPrecip.toFixed(1)} mm/h at peak. Roads may flood; allow extra travel time.` - : `Wet spell — up to ${maxPrecip.toFixed(1)} mm/h rain at peak. Keep a brolly handy.`, + title: rainName, + message: isTorrential + ? `Torrential rain — up to ${maxPrecip.toFixed(1)} mm/h at peak. Flash flooding possible; avoid travel if you can.` + : isHeavy + ? `Heavy rain — up to ${maxPrecip.toFixed(1)} mm/h at peak. Roads may flood; allow extra travel time.` + : `Moderate rain expected — up to ${maxPrecip.toFixed(1)} mm/h at peak. Keep a brolly handy.`, color: '#15212e', textColor: '#bcd6ee', type: 'weather', nightOnly: false, - isoRange, // only show cell icon during the wet hours + isoRange, }; } @@ -216,21 +237,22 @@ export function checkStorm(rows) { if (affected.length === 0) return null; const isoRange = [affected[0].iso, affected[affected.length - 1].iso]; - const maxGust = Math.max(...affected.map(gustOf).filter(isFinite)); + const maxGust = Math.max(...affected.map(gustOf).filter(isFinite)); const maxPrecip = Math.max(...affected.map(precipOf).filter(isFinite)); - const mph = Math.round(maxGust * 2.237); - const { force, name } = beaufortForce(maxGust); - const { color, textColor } = windBannerColors(force); + const mph = Math.round(maxGust * 2.237); + const { tier } = windCategory(maxGust); + const { color, textColor } = windBannerColors(tier); + const windDesc = WIND_DESC[tier] || ''; - const title = force >= 10 ? `${name} — Severe Storm` - : force >= 8 ? `${name} with Rain` - : 'Storm Conditions'; + const isTorrential = maxPrecip >= PRECIP_TORRENTIAL; + const isHeavy = maxPrecip >= PRECIP_HEAVY; + const rainLabel = isTorrential ? 'torrential rain' : isHeavy ? 'heavy rain' : 'rain'; return { id: 'storm', emoji: '🌩️', - title, - message: `Gusts to ${mph} mph (Beaufort Force ${force}) with ${maxPrecip.toFixed(1)} mm/h rain. ${BEAUFORT_DESC[force] || ''} Take care outdoors.`, + title: stormTitle(tier, maxPrecip), + message: `Gusts to ${mph} mph with ${rainLabel} (${maxPrecip.toFixed(1)} mm/h). ${windDesc}`, color, textColor, type: 'weather',