2.6.1
Update wind and rain measures - now using Beaufort Scale
This commit is contained in:
+1
-1
@@ -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.' },
|
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.' },
|
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.' },
|
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).' },
|
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.' },
|
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.' },
|
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.' },
|
||||||
|
|||||||
@@ -116,45 +116,63 @@ export function checkHeatSpike(rows) {
|
|||||||
// wet + calm → rain icon (🌧️)
|
// wet + calm → rain icon (🌧️)
|
||||||
// windy + wet → storm icon (🌩️), and the wind/rain icons are suppressed
|
// windy + wet → storm icon (🌩️), and the wind/rain icons are suppressed
|
||||||
// for that hour so a single hour never shows two icons.
|
// 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_BLUSTERY = 25 / 2.237; // m/s = 25 mph - Strong Breeze (Force 6)
|
||||||
const GUST_STORM = 20; // m/s ≈ 45 mph - Beaufort Force 8+ threshold
|
const GUST_STORM = 39 / 2.237; // m/s = 39 mph - Gale (Force 8) - storm combo trigger
|
||||||
const PRECIP_WET = 5; // mm/h - meaningful rain
|
const PRECIP_MODERATE = 2.5; // mm/h - moderate rain threshold
|
||||||
const PRECIP_HEAVY = 10; // mm/h - heavy rain
|
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
|
// Wind category lookup from gust speed in m/s.
|
||||||
function beaufortForce(ms) {
|
// Uses simplified 6-tier scale matching common forecast language.
|
||||||
|
function windCategory(ms) {
|
||||||
const mph = ms * 2.237;
|
const mph = ms * 2.237;
|
||||||
if (mph >= 73) return { force: 12, name: 'Hurricane Force' };
|
if (mph >= 96) return { tier: 6, name: 'Violent Storm', advisory: 'Wind Advisory' };
|
||||||
if (mph >= 64) return { force: 11, name: 'Violent Storm' };
|
if (mph >= 74) return { tier: 5, name: 'Storm Force', advisory: 'Storm Warning' };
|
||||||
if (mph >= 55) return { force: 10, name: 'Storm Force' };
|
if (mph >= 55) return { tier: 4, name: 'Severe Gale', advisory: 'Gale Warning' };
|
||||||
if (mph >= 47) return { force: 9, name: 'Strong Gale' };
|
if (mph >= 39) return { tier: 3, name: 'Gale', advisory: 'Gale Warning' };
|
||||||
if (mph >= 39) return { force: 8, name: 'Gale' };
|
if (mph >= 32) return { tier: 2, name: 'Near Gale', advisory: 'Wind Advisory' };
|
||||||
if (mph >= 32) return { force: 7, name: 'Near Gale' };
|
return { tier: 1, name: 'Strong Breeze', advisory: null };
|
||||||
return { force: 6, name: 'Strong Breeze' };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const BEAUFORT_DESC = {
|
const WIND_DESC = {
|
||||||
6: 'Large branches in motion; umbrellas used with difficulty.',
|
1: 'Large branches in motion; umbrellas used with difficulty.',
|
||||||
7: 'Whole trees in motion; inconvenience felt when walking against the wind.',
|
2: 'Whole trees in motion; inconvenience felt when walking against the wind.',
|
||||||
8: 'Twigs break off trees and generally impedes progress.',
|
3: 'Twigs break off trees; progress on foot generally impeded.',
|
||||||
9: 'Slight structural damage possible — chimney pots and slates at risk.',
|
4: 'Slight structural damage possible — chimney pots and slates at risk.',
|
||||||
10: 'Trees uprooted; considerable structural damage expected.',
|
5: 'Trees uprooted; considerable structural damage expected.',
|
||||||
11: 'Very rarely experienced inland; widespread damage expected.',
|
6: 'Very rarely experienced inland; widespread damage — avoid all outdoor activity.',
|
||||||
12: 'Devastating conditions — avoid all outdoor activity.',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Banner colour scales with severity
|
// Rain intensity lookup from mm/h
|
||||||
function windBannerColors(force) {
|
function rainCategory(mmh) {
|
||||||
if (force >= 11) return { color: '#0c1020', textColor: '#c0d8ff' };
|
if (mmh >= PRECIP_TORRENTIAL) return { name: 'Torrential Rain', desc: 'extreme rainfall' };
|
||||||
if (force >= 9) return { color: '#101828', textColor: '#c0d4f0' };
|
if (mmh >= PRECIP_HEAVY) return { name: 'Heavy Rain', desc: 'heavy rainfall' };
|
||||||
if (force >= 7) return { color: '#141e2e', textColor: '#c0d0e8' };
|
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' };
|
return { color: '#1a2030', textColor: '#c0d8f0' };
|
||||||
}
|
}
|
||||||
|
|
||||||
const gustOf = r => r.gust ?? r.va ?? 0;
|
const gustOf = r => r.gust ?? r.va ?? 0;
|
||||||
const precipOf = r => r.precip ?? 0;
|
const precipOf = r => r.precip ?? 0;
|
||||||
const isWindyHour = r => { const g = gustOf(r); return isFinite(g) && g >= GUST_BLUSTERY; };
|
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);
|
const isStormHour = r => isWindyHour(r) && isWetHour(r);
|
||||||
|
|
||||||
export function checkWind(rows) {
|
export function checkWind(rows) {
|
||||||
@@ -165,19 +183,17 @@ export function checkWind(rows) {
|
|||||||
|
|
||||||
const maxGust = Math.max(...affected.map(gustOf).filter(isFinite));
|
const maxGust = Math.max(...affected.map(gustOf).filter(isFinite));
|
||||||
const mph = Math.round(maxGust * 2.237);
|
const mph = Math.round(maxGust * 2.237);
|
||||||
const { force, name } = beaufortForce(maxGust);
|
const { tier, name, advisory } = windCategory(maxGust);
|
||||||
const desc = BEAUFORT_DESC[force] || '';
|
const desc = WIND_DESC[tier] || '';
|
||||||
const { color, textColor } = windBannerColors(force);
|
const { color, textColor } = windBannerColors(tier);
|
||||||
|
|
||||||
const title = force >= 10 ? `${name} Winds`
|
const title = advisory ? `${advisory} — ${name}` : name;
|
||||||
: force >= 8 ? `${name} Warning`
|
|
||||||
: name;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: 'wind',
|
id: 'wind',
|
||||||
emoji: force >= 10 ? '🌪️' : '💨',
|
emoji: tier >= 5 ? '🌪️' : '💨',
|
||||||
title,
|
title,
|
||||||
message: `Gusts to ${mph} mph (Beaufort Force ${force}). ${desc}`,
|
message: `Gusts to ${mph} mph. ${desc}`,
|
||||||
color,
|
color,
|
||||||
textColor,
|
textColor,
|
||||||
type: 'weather',
|
type: 'weather',
|
||||||
@@ -193,20 +209,25 @@ export function checkWet(rows) {
|
|||||||
const isoRange = [affected[0].iso, affected[affected.length - 1].iso];
|
const isoRange = [affected[0].iso, affected[affected.length - 1].iso];
|
||||||
|
|
||||||
const maxPrecip = Math.max(...affected.map(precipOf).filter(isFinite));
|
const maxPrecip = Math.max(...affected.map(precipOf).filter(isFinite));
|
||||||
|
const { name: rainName } = rainCategory(maxPrecip);
|
||||||
|
|
||||||
|
const isTorrential = maxPrecip >= PRECIP_TORRENTIAL;
|
||||||
const isHeavy = maxPrecip >= PRECIP_HEAVY;
|
const isHeavy = maxPrecip >= PRECIP_HEAVY;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: 'wet',
|
id: 'wet',
|
||||||
emoji: '🌧️',
|
emoji: '🌧️',
|
||||||
title: 'Heavy Rain',
|
title: rainName,
|
||||||
message: isHeavy
|
message: isTorrential
|
||||||
? `Heavy rain expected — up to ${maxPrecip.toFixed(1)} mm/h at peak. Roads may flood; allow extra travel time.`
|
? `Torrential rain — up to ${maxPrecip.toFixed(1)} mm/h at peak. Flash flooding possible; avoid travel if you can.`
|
||||||
: `Wet spell — up to ${maxPrecip.toFixed(1)} mm/h rain at peak. Keep a brolly handy.`,
|
: 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',
|
color: '#15212e',
|
||||||
textColor: '#bcd6ee',
|
textColor: '#bcd6ee',
|
||||||
type: 'weather',
|
type: 'weather',
|
||||||
nightOnly: false,
|
nightOnly: false,
|
||||||
isoRange, // only show cell icon during the wet hours
|
isoRange,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,18 +240,19 @@ export function checkStorm(rows) {
|
|||||||
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 maxPrecip = Math.max(...affected.map(precipOf).filter(isFinite));
|
||||||
const mph = Math.round(maxGust * 2.237);
|
const mph = Math.round(maxGust * 2.237);
|
||||||
const { force, name } = beaufortForce(maxGust);
|
const { tier } = windCategory(maxGust);
|
||||||
const { color, textColor } = windBannerColors(force);
|
const { color, textColor } = windBannerColors(tier);
|
||||||
|
const windDesc = WIND_DESC[tier] || '';
|
||||||
|
|
||||||
const title = force >= 10 ? `${name} — Severe Storm`
|
const isTorrential = maxPrecip >= PRECIP_TORRENTIAL;
|
||||||
: force >= 8 ? `${name} with Rain`
|
const isHeavy = maxPrecip >= PRECIP_HEAVY;
|
||||||
: 'Storm Conditions';
|
const rainLabel = isTorrential ? 'torrential rain' : isHeavy ? 'heavy rain' : 'rain';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: 'storm',
|
id: 'storm',
|
||||||
emoji: '🌩️',
|
emoji: '🌩️',
|
||||||
title,
|
title: stormTitle(tier, maxPrecip),
|
||||||
message: `Gusts to ${mph} mph (Beaufort Force ${force}) with ${maxPrecip.toFixed(1)} mm/h rain. ${BEAUFORT_DESC[force] || ''} Take care outdoors.`,
|
message: `Gusts to ${mph} mph with ${rainLabel} (${maxPrecip.toFixed(1)} mm/h). ${windDesc}`,
|
||||||
color,
|
color,
|
||||||
textColor,
|
textColor,
|
||||||
type: 'weather',
|
type: 'weather',
|
||||||
|
|||||||
Reference in New Issue
Block a user