Fix wet, windy and storms
This commit is contained in:
fraxle
2026-06-02 15:40:05 +01:00
parent 099f2924d3
commit 91ff762c5f
4 changed files with 145 additions and 13 deletions
+17 -1
View File
@@ -81,13 +81,29 @@ export function getLensOverlaySVG(event, cx, cy, lensR) {
+ '<path d="M ' + (cx-lensR+10) + ' ' + (cy+45) + ' Q ' + (cx-10) + ' ' + (cy+30) + ' ' + (cx+30) + ' ' + (cy+45) + ' Q ' + (cx+65) + ' ' + (cy+60) + ' ' + (cx+lensR-10) + ' ' + (cy+45) + '" fill="none" stroke="rgba(255,100,10,0.5)" stroke-width="2" /></g>';
}
if (id === 'storm') {
if (id === 'wet') {
const sl = [-55,-30,-5,20,45,65].map(function(x){
return '<line x1="' + (cx+x) + '" y1="' + (cy-lensR+10) + '" x2="' + (cx+x-15) + '" y2="' + (cy+lensR-10) + '" stroke="rgba(140,180,220,0.6)" stroke-width="1" stroke-linecap="round" />';
}).join('');
return '<g clip-path="url(#scope-lens-clip)" opacity="0.45">' + sl + '</g>';
}
if (id === 'wind') {
const wl = [-30,5,40].map(function(y){
return '<path d="M ' + (cx-lensR+12) + ' ' + (cy+y) + ' q 40 -14 80 0 q 30 10 ' + (lensR-10) + ' -2" fill="none" stroke="rgba(170,200,230,0.55)" stroke-width="1.5" stroke-linecap="round" />';
}).join('');
return '<g clip-path="url(#scope-lens-clip)" opacity="0.45">' + wl + '</g>';
}
if (id === 'storm') {
// Driving rain streaks plus a forked lightning bolt.
const sl = [-55,-30,-5,20,45,65].map(function(x){
return '<line x1="' + (cx+x) + '" y1="' + (cy-lensR+10) + '" x2="' + (cx+x-22) + '" y2="' + (cy+lensR-10) + '" stroke="rgba(140,180,220,0.55)" stroke-width="1" stroke-linecap="round" />';
}).join('');
const bolt = '<path d="M ' + (cx+6) + ' ' + (cy-lensR+18) + ' L ' + (cx-14) + ' ' + (cy+8) + ' L ' + (cx-2) + ' ' + (cy+8) + ' L ' + (cx-18) + ' ' + (cy+lensR-14) + ' L ' + (cx+18) + ' ' + (cy-2) + ' L ' + (cx+4) + ' ' + (cy-2) + ' Z" fill="rgba(255,224,120,0.7)" stroke="rgba(255,200,60,0.8)" stroke-width="1" stroke-linejoin="round" />';
return '<g clip-path="url(#scope-lens-clip)" opacity="0.5">' + sl + bolt + '</g>';
}
if (id === 'frost') {
const fc = [[-50,40],[0,55],[50,40],[-30,65],[30,65]].map(function(p){
var dx=p[0], dy=p[1];
+99 -10
View File
@@ -85,6 +85,13 @@ export function checkHeatSpike(rows) {
const maxTemp = Math.max(...rows.map(r => r.Ta).filter(isFinite));
if (maxTemp < 30) return null;
const severity = maxTemp >= 36 ? 'extreme' : maxTemp >= 33 ? 'severe' : 'notable';
// Only flag the hours that are actually at/above the heat threshold.
const affected = rows.filter(r => isFinite(r.Ta) && r.Ta >= 30);
const isoRange = affected.length
? [affected[0].iso, affected[affected.length - 1].iso]
: undefined;
const msgs = {
notable: `Temperatures reaching ${maxTemp.toFixed(1)}°C — above seasonal norms. Stay hydrated and avoid prolonged sun exposure.`,
severe: `Heat warning: ${maxTemp.toFixed(1)}°C expected today. Risk of heat exhaustion for vulnerable people — keep cool and hydrated.`,
@@ -99,31 +106,112 @@ export function checkHeatSpike(rows) {
textColor: '#ffd0b0',
type: 'weather',
nightOnly: false,
isoRange, // only show cell icon during the hot hours
};
}
export function checkStorm(rows) {
const maxGust = Math.max(...rows.map(r => r.gust ?? r.va ?? 0).filter(isFinite));
const maxPrecip = Math.max(...rows.map(r => r.precip ?? 0).filter(isFinite));
if (maxGust < 15 && maxPrecip < 5) return null;
const isStorm = maxGust >= 20 || maxPrecip >= 10;
// Wind and rain are detected separately so each icon lands only on the hours
// that genuinely have that condition. They are mutually exclusive per hour:
// windy + dry → wind icon (💨)
// 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) - matches the table's gust highlight
const GUST_STORM = 20; // m/s ≈ 45 mph - storm-force
const PRECIP_WET = 5; // mm/h - meaningful rain
const PRECIP_HEAVY = 10; // mm/h - heavy rain
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 isStormHour = r => isWindyHour(r) && isWetHour(r);
export function checkWind(rows) {
// Windy hours that are NOT also wet (those are storms instead).
const affected = rows.filter(r => isWindyHour(r) && !isStormHour(r));
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 mph = Math.round(maxGust * 2.237);
const isStorm = maxGust >= GUST_STORM;
return {
id: 'storm',
emoji: '🌩️',
title: isStorm ? 'Storm Conditions Forecast' : 'Blustery & Wet Today',
id: 'wind',
emoji: '💨',
title: isStorm ? 'Storm-Force Winds' : 'Blustery Conditions',
message: isStorm
? `Storm-level conditions expected — gusts to ${Math.round(maxGust * 2.237)} mph with heavy precipitation. Take care outdoors.`
: `Unsettled day ahead — windy with gusts to ${Math.round(maxGust * 2.237)} mph and ${maxPrecip.toFixed(1)} mm/h rain at peak.`,
? `Storm-force gusts to ${mph} mph expected — secure loose objects and take care outdoors.`
: `Windy spell — gusts to ${mph} mph at peak. Hold onto hats and secure loose items outdoors.`,
color: '#1a2030',
textColor: '#c0d8f0',
type: 'weather',
nightOnly: false,
isoRange, // only show cell icon during the windy hours
};
}
export function checkWet(rows) {
// Wet hours that are NOT also windy (those are storms instead).
const affected = rows.filter(r => isWetHour(r) && !isStormHour(r));
if (affected.length === 0) return null;
const isoRange = [affected[0].iso, affected[affected.length - 1].iso];
const maxPrecip = Math.max(...affected.map(precipOf).filter(isFinite));
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.`,
color: '#15212e',
textColor: '#bcd6ee',
type: 'weather',
nightOnly: false,
isoRange, // only show cell icon during the wet hours
};
}
export function checkStorm(rows) {
// Storm hours = windy AND wet at the same time.
const affected = rows.filter(isStormHour);
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 maxPrecip = Math.max(...affected.map(precipOf).filter(isFinite));
const mph = Math.round(maxGust * 2.237);
const isSevere = maxGust >= GUST_STORM || maxPrecip >= PRECIP_HEAVY;
return {
id: 'storm',
emoji: '🌩️',
title: isSevere ? 'Severe Storm' : 'Storm Conditions',
message: isSevere
? `Severe storm — driving rain to ${maxPrecip.toFixed(1)} mm/h and gusts to ${mph} mph. Avoid travel if you can.`
: `Stormy spell — wind and rain together, gusts to ${mph} mph with ${maxPrecip.toFixed(1)} mm/h rain. Take care outdoors.`,
color: '#141a26',
textColor: '#c0d0e8',
type: 'weather',
nightOnly: false,
isoRange, // only show cell icon during the stormy hours
};
}
export function checkFrost(rows) {
const minTemp = Math.min(...rows.map(r => r.Ta).filter(isFinite));
if (minTemp > 2) return null;
// Only flag the hours that are actually at/below the frost threshold.
const affected = rows.filter(r => isFinite(r.Ta) && r.Ta <= 2);
const isoRange = affected.length
? [affected[0].iso, affected[affected.length - 1].iso]
: undefined;
return {
id: 'frost',
emoji: '❄️',
@@ -135,5 +223,6 @@ export function checkFrost(rows) {
textColor: '#c8e8ff',
type: 'weather',
nightOnly: false,
isoRange, // only show cell icon during the frosty hours
};
}