2.6.1
Update wind and rain measures - now using Beaufort Scale
This commit is contained in:
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user