3.1.0.0
Vehicle Speed Calcs Extra At A Glance ranges New Driver profile
This commit is contained in:
+67
-30
@@ -2,7 +2,7 @@
|
||||
// compute.js - Build the per-hour display rows from the raw API data.
|
||||
//
|
||||
// Pure-ish function: feed in (forecast, airQuality, location, vehicleType,
|
||||
// vehicleVent, buildingType) and get back { hourlyRows, days, utcOffsetMs }.
|
||||
// vehicleVent, vehicleSpeed, buildingType) and get back { hourlyRows, days, utcOffsetMs }.
|
||||
//
|
||||
// Open-Meteo with timezone=auto returns local wall-clock strings like
|
||||
// "2026-05-13T14:00" - no Z suffix. Two forms are used in each row:
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
import { windCompass8, uvSplit, cloudCategory, precipPenalty, sunburnMinutes, burnLabel } from './utils.js';
|
||||
import { UTCI_ENVIRONMENTS } from './config.js';
|
||||
|
||||
export function buildHourlyRows({ forecast, airQuality, location, vehicleType, vehicleVent, buildingType, utciEnv }) {
|
||||
export function buildHourlyRows({ forecast, airQuality, location, vehicleType, vehicleVent, vehicleSpeed, buildingType, utciEnv }) {
|
||||
const env = UTCI_ENVIRONMENTS[utciEnv] ?? UTCI_ENVIRONMENTS.open;
|
||||
const utcOffsetMs = (forecast?.utc_offset_seconds ?? 0) * 1000;
|
||||
|
||||
@@ -77,7 +77,7 @@ export function buildHourlyRows({ forecast, airQuality, location, vehicleType, v
|
||||
// a surface as direct beam, so we weight it accordingly.
|
||||
const effectiveRad = dir + dif * 0.2;
|
||||
// concreteT is now stamped in the two-pass section below (thermal lag).
|
||||
const vehicleT = calcVehicleInteriorTemp(Ta, glob, elev, vehicleType, vehicleVent);
|
||||
const vehicleT = calcVehicleInteriorTemp(Ta, glob, elev, vehicleType, vehicleVent, vehicleSpeed);
|
||||
const eh = vaporPressureHpa(Ta, RH);
|
||||
// Apply environment modifier - adjust solar inputs and air temp for shaded environments.
|
||||
const TaEnv = Ta + env.taOffset;
|
||||
@@ -377,7 +377,7 @@ export function computeWhyFeelsLike(row, env) {
|
||||
// variant - active sub-variant key e.g. "running", "beach" (or null)
|
||||
// skinType - Fitzpatrick skin type key for UV burn time
|
||||
// ------------------------------------------------------------------------
|
||||
export function computeGlanceSummary(todayRows, profile, variant, skinType, cols = null) {
|
||||
export function computeGlanceSummary(todayRows, profile, variant, skinType, cols = null, vehicleSpeed = 'static') {
|
||||
if (!todayRows || todayRows.length === 0) return [];
|
||||
const show = (key) => !cols || !!cols[key];
|
||||
|
||||
@@ -406,18 +406,35 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols
|
||||
(r.precipProb != null && (best == null || r.precipProb > best.precipProb)) ? r : best, null);
|
||||
const maxRainProb = peakRainRow ? (peakRainRow.precipProb ?? 0) : 0;
|
||||
|
||||
// Longest run of rows that satisfy a predicate.
|
||||
const longestWindow = (rows, cond) => {
|
||||
let best = null, cur = null;
|
||||
// All contiguous runs of rows that satisfy a predicate. Each run is
|
||||
// { start, end, len }. Used so an "at a glance" window can report more than
|
||||
// one range - e.g. good driving in the morning AND again in the evening.
|
||||
const allWindows = (rows, cond) => {
|
||||
const out = [];
|
||||
let cur = null;
|
||||
for (const r of rows) {
|
||||
if (cond(r)) {
|
||||
cur = cur ? { start: cur.start, end: r, len: cur.len + 1 } : { start: r, end: r, len: 1 };
|
||||
if (!best || cur.len > best.len) best = { ...cur };
|
||||
} else {
|
||||
cur = null;
|
||||
} else if (cur) {
|
||||
out.push(cur); cur = null;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
if (cur) out.push(cur);
|
||||
return out;
|
||||
};
|
||||
|
||||
// Format the most significant windows as "h – h", one range per line, in
|
||||
// chronological order. Caps at `max` ranges (longest kept) so a split day
|
||||
// reads cleanly. Returns null when there are no windows. The newline is
|
||||
// preserved by `white-space: pre-line` on .insight-value.
|
||||
const formatWindows = (wins, max = 2) => {
|
||||
if (!wins || wins.length === 0) return null;
|
||||
return [...wins]
|
||||
.sort((a, b) => b.len - a.len)
|
||||
.slice(0, max)
|
||||
.sort((a, b) => (a.start.iso < b.start.iso ? -1 : 1))
|
||||
.map(w => `${hhmm(w.start.iso)} – ${hhmmEnd(w.end.iso)}`)
|
||||
.join('\n');
|
||||
};
|
||||
|
||||
const pollenLabel = (v) => {
|
||||
@@ -470,9 +487,35 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols
|
||||
alert: maxRainProb >= 60,
|
||||
};
|
||||
|
||||
// ── Good Driving Time ──────────────────────────────────────────────────
|
||||
// Shown whenever a road speed (not Static) is selected and the vehicle cabin
|
||||
// column is active - so it appears in both the Vehicle and Driver profiles.
|
||||
// Longest run of hours, across the full day since lorries run day and night,
|
||||
// that are NOT poor driving conditions: a hot cabin (>29 °C), heavy rain,
|
||||
// snow/ice, fog/thick mist, or gale-force wind.
|
||||
const GALE_MS = 39 / 2.237; // 39 mph gust = Gale (Force 8)
|
||||
const drivingShown = show('vehicleT') && vehicleSpeed !== 'static';
|
||||
const drivingWins = drivingShown
|
||||
? allWindows(todayRows, r => {
|
||||
const gustMs = r.gust ?? r.va;
|
||||
return (r.vehicleT == null || r.vehicleT <= 29) && // cabin not dangerously hot
|
||||
(r.precip == null || r.precip < 4) && // not heavy rain (mm/h)
|
||||
(r.snow == null || r.snow === 0) && // no snow
|
||||
(r.Ta == null || r.Ta > 1) && // no ice risk
|
||||
(r.visKm == null || r.visKm >= 4) && // not fog / thick mist
|
||||
(gustMs == null || gustMs < GALE_MS); // not gale-force wind
|
||||
})
|
||||
: [];
|
||||
const drivingItem = {
|
||||
icon: '🚚',
|
||||
label: 'Good driving time',
|
||||
value: formatWindows(drivingWins) ?? 'Drive with care',
|
||||
alert: drivingWins.length === 0,
|
||||
};
|
||||
|
||||
// ── Farming ────────────────────────────────────────────────────────────
|
||||
if (profile === 'farming') {
|
||||
const fieldWindow = longestWindow(dayRows, r =>
|
||||
const fieldWins = allWindows(dayRows, r =>
|
||||
r.utciAdj >= 8 && r.utciAdj <= 32 && r.precipProb < 30 && r.va < 12
|
||||
);
|
||||
const soilWarmRow = todayRows.find(r => r.soilT0 != null && r.soilT0 >= 10);
|
||||
@@ -483,10 +526,8 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols
|
||||
{
|
||||
icon: '⏱',
|
||||
label: 'Best field work',
|
||||
value: fieldWindow
|
||||
? `${hhmm(fieldWindow.start.iso)} – ${hhmmEnd(fieldWindow.end.iso)}`
|
||||
: 'No suitable window',
|
||||
alert: !fieldWindow,
|
||||
value: formatWindows(fieldWins) ?? 'No suitable window',
|
||||
alert: fieldWins.length === 0,
|
||||
},
|
||||
...(show('soilT') ? [{
|
||||
icon: '🌱',
|
||||
@@ -530,6 +571,7 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols
|
||||
}] : []),
|
||||
...(show('precipProb') ? [rainItem] : []),
|
||||
...lightningItem,
|
||||
...(drivingShown ? [drivingItem] : []),
|
||||
{
|
||||
icon: '🌤',
|
||||
label: 'Best travel comfort',
|
||||
@@ -547,7 +589,7 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols
|
||||
if (profile === 'home') {
|
||||
const peakIndoor = peakRow('indoorT');
|
||||
const peakManaged = peakRow('managedT');
|
||||
const ventWindow = longestWindow(todayRows, r =>
|
||||
const ventWins = allWindows(todayRows, r =>
|
||||
r.Ta != null && r.indoorT != null && r.Ta < r.indoorT && r.precipProb < 20
|
||||
);
|
||||
const peakAqi = Math.max(0, ...todayRows.map(r => r.aqi ?? 0));
|
||||
@@ -569,9 +611,7 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols
|
||||
...(show('indoorT') ? [{
|
||||
icon: '🪟',
|
||||
label: 'Open windows',
|
||||
value: ventWindow
|
||||
? `${hhmm(ventWindow.start.iso)} – ${hhmmEnd(ventWindow.end.iso)}`
|
||||
: 'Keep closed',
|
||||
value: formatWindows(ventWins) ?? 'Keep closed',
|
||||
alert: false,
|
||||
}] : []),
|
||||
...lightningItem,
|
||||
@@ -592,7 +632,7 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols
|
||||
|
||||
// ── Activities - running / cycling ─────────────────────────────────────
|
||||
if (variant === 'running' || variant === 'cycling') {
|
||||
const coolWindow = longestWindow(dayRows, r =>
|
||||
const coolWins = allWindows(dayRows, r =>
|
||||
r.utciAdj >= 5 && r.utciAdj <= 22 && r.precipProb < 30
|
||||
);
|
||||
const peakUvRow = peakRow('uv');
|
||||
@@ -606,10 +646,8 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols
|
||||
{
|
||||
icon: '⏱',
|
||||
label: `Best ${variant} window`,
|
||||
value: coolWindow
|
||||
? `${hhmm(coolWindow.start.iso)} – ${hhmmEnd(coolWindow.end.iso)}`
|
||||
: 'No cool window today',
|
||||
alert: !coolWindow,
|
||||
value: formatWindows(coolWins) ?? 'No cool window today',
|
||||
alert: coolWins.length === 0,
|
||||
},
|
||||
...(show('precipProb') ? [rainItem] : []),
|
||||
...lightningItem,
|
||||
@@ -635,7 +673,7 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols
|
||||
}
|
||||
|
||||
// ── Outdoors - beach, park, events etc. (and fallback) ─────────────────
|
||||
const comfortWindow = longestWindow(dayRows, r =>
|
||||
const comfortWins = allWindows(dayRows, r =>
|
||||
r.utciAdj >= 9 && r.utciAdj <= 26 && r.precipProb < 30
|
||||
);
|
||||
const peakFelt = peakRow('utciAdj');
|
||||
@@ -647,13 +685,12 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols
|
||||
: null;
|
||||
|
||||
return [
|
||||
...(drivingShown ? [drivingItem] : []),
|
||||
{
|
||||
icon: '🌤',
|
||||
label: 'Comfortable window',
|
||||
value: comfortWindow
|
||||
? `${hhmm(comfortWindow.start.iso)} – ${hhmmEnd(comfortWindow.end.iso)}`
|
||||
: 'No comfortable window',
|
||||
alert: !comfortWindow,
|
||||
value: formatWindows(comfortWins) ?? 'No comfortable window',
|
||||
alert: comfortWins.length === 0,
|
||||
},
|
||||
{
|
||||
icon: '🌡',
|
||||
|
||||
Reference in New Issue
Block a user