1.5
Code refactoring Profile fixes Table fixes Animation additions
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
// weather-checks.js — Weather-derived event detectors.
|
||||
//
|
||||
// Computed in real-time from the forecast data.
|
||||
// Each checker function receives (rows [, location]) and returns an event
|
||||
// object or null. rows = today's hourlyRows array.
|
||||
// location = { lat, lon, name, country }.
|
||||
//
|
||||
// To add a new weather event: write a checkXxx(rows, location) function
|
||||
// below, export it, and add it to the checks[] array inside
|
||||
// getActiveEvents() (in events.js).
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
|
||||
export function checkStargazing(rows) {
|
||||
// Great stargazing: mostly clear night hours with low cloud
|
||||
const nightRows = rows.filter(r => r.elev < -5);
|
||||
if (nightRows.length < 3) return null;
|
||||
const avgCloud = nightRows.reduce((s, r) => s + r.cc, 0) / nightRows.length;
|
||||
if (avgCloud > 30) return null;
|
||||
return {
|
||||
id: 'stargazing',
|
||||
emoji: '⭐',
|
||||
title: 'Great Stargazing Tonight',
|
||||
message: `Clear skies expected overnight at ${nightRows.length} hours with average ${Math.round(avgCloud)}% cloud — ideal conditions for stargazing.`,
|
||||
color: '#080e1a',
|
||||
textColor: '#c8dcff',
|
||||
type: 'weather',
|
||||
nightOnly: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function checkSunset(rows, location) {
|
||||
// Find the actual sunset window: the LAST contiguous run of rows where
|
||||
// solar elevation is in the golden/civil-twilight band (-3° to 8°).
|
||||
// This distinguishes sunset from sunrise (which is the first such run).
|
||||
const twilightIndices = rows
|
||||
.map((r, i) => ({ r, i }))
|
||||
.filter(({ r }) => r.elev > -3 && r.elev < 8);
|
||||
if (twilightIndices.length === 0) return null;
|
||||
|
||||
// Split into runs separated by gaps (midday gap separates sunrise from sunset)
|
||||
const runs = [];
|
||||
let run = [twilightIndices[0]];
|
||||
for (let k = 1; k < twilightIndices.length; k++) {
|
||||
if (twilightIndices[k].i === twilightIndices[k - 1].i + 1) {
|
||||
run.push(twilightIndices[k]);
|
||||
} else {
|
||||
runs.push(run);
|
||||
run = [twilightIndices[k]];
|
||||
}
|
||||
}
|
||||
runs.push(run);
|
||||
|
||||
// Use the LAST run (sunset). If only one run exists it's either sunrise-only
|
||||
// or a single dusk window — use it but we'll label generically.
|
||||
const sunsetRun = runs[runs.length - 1];
|
||||
const sunsetRows = sunsetRun.map(({ r }) => r);
|
||||
const isSunrise = runs.length === 1 && sunsetRows[0].elev < sunsetRows[sunsetRows.length - 1].elev;
|
||||
// If sun is rising through the band this is a sunrise window, not sunset — skip.
|
||||
if (isSunrise) return null;
|
||||
|
||||
const avgCloud = sunsetRows.reduce((s, r) => s + r.cc, 0) / sunsetRows.length;
|
||||
const avgLowCloud = sunsetRows.reduce((s, r) => s + (r.ccLow || 0), 0) / sunsetRows.length;
|
||||
if (avgLowCloud > 25) return null;
|
||||
if (avgCloud < 5 || avgCloud > 75) return null;
|
||||
|
||||
// Store the ISO time range so getCellTagEvents can limit the icon to those hours
|
||||
const firstISO = sunsetRun[0].r.iso;
|
||||
const lastISO = sunsetRun[sunsetRun.length - 1].r.iso;
|
||||
|
||||
return {
|
||||
id: 'perfect-sunset',
|
||||
emoji: '🌅',
|
||||
title: 'Spectacular Sunset Conditions',
|
||||
message: `Low cloud is clear near the horizon but high cloud will scatter the light — conditions look ideal for a vivid sunset near ${location.name}.`,
|
||||
color: '#3a1a00',
|
||||
textColor: '#ffe0b0',
|
||||
type: 'weather',
|
||||
nightOnly: false,
|
||||
isoRange: [firstISO, lastISO], // only show cell icon during these hours
|
||||
};
|
||||
}
|
||||
|
||||
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';
|
||||
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.`,
|
||||
extreme: `Extreme heat alert: ${maxTemp.toFixed(1)}°C forecast. Risk of heat stroke — avoid outdoor activity during peak hours.`,
|
||||
};
|
||||
return {
|
||||
id: 'heat-spike',
|
||||
emoji: '🔥',
|
||||
title: severity === 'extreme' ? 'Extreme Heat Alert' : severity === 'severe' ? 'Heat Warning' : 'Heat Spike Today',
|
||||
message: msgs[severity],
|
||||
color: severity === 'extreme' ? '#3a0000' : severity === 'severe' ? '#4a1000' : '#5a2000',
|
||||
textColor: '#ffd0b0',
|
||||
type: 'weather',
|
||||
nightOnly: false,
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
return {
|
||||
id: 'storm',
|
||||
emoji: '🌩️',
|
||||
title: isStorm ? 'Storm Conditions Forecast' : 'Blustery & Wet Today',
|
||||
message: isStorm
|
||||
? `Storm-level conditions expected — gusts to ${maxGust.toFixed(0)} m/s with heavy precipitation. Take care outdoors.`
|
||||
: `Unsettled day ahead — windy with gusts to ${maxGust.toFixed(0)} m/s and ${maxPrecip.toFixed(1)} mm/h rain at peak.`,
|
||||
color: '#1a2030',
|
||||
textColor: '#c0d8f0',
|
||||
type: 'weather',
|
||||
nightOnly: false,
|
||||
};
|
||||
}
|
||||
|
||||
export function checkFrost(rows) {
|
||||
const minTemp = Math.min(...rows.map(r => r.Ta).filter(isFinite));
|
||||
if (minTemp > 2) return null;
|
||||
return {
|
||||
id: 'frost',
|
||||
emoji: '❄️',
|
||||
title: minTemp <= 0 ? 'Freezing Conditions' : 'Frost Risk Tonight',
|
||||
message: minTemp <= 0
|
||||
? `Temperatures dropping to ${minTemp.toFixed(1)}°C — ice on roads and surfaces is likely. Allow extra travel time.`
|
||||
: `Temperatures near freezing tonight (${minTemp.toFixed(1)}°C) — frost possible on exposed surfaces and vehicles.`,
|
||||
color: '#0a1a2a',
|
||||
textColor: '#c8e8ff',
|
||||
type: 'weather',
|
||||
nightOnly: false,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user