Profile memory
Comment cleanup
This commit is contained in:
fraxle
2026-05-18 23:02:41 +01:00
parent dded468bec
commit 85c415d1f5
16 changed files with 504 additions and 581 deletions
+47 -47
View File
@@ -1,14 +1,14 @@
// ════════════════════════════════════════════════════════════════════════
// compute.js Build the per-hour display rows from the raw API data.
// ------------------------------------------------------------------------
// 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 }.
//
// 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:
// String slices (iso.slice(...)) for display & day grouping
// A true UTC Date (dt) for solarElevationDeg (which uses .getUTC*).
// ════════════════════════════════════════════════════════════════════════
// "2026-05-13T14:00" - no Z suffix. Two forms are used in each row:
// - String slices (iso.slice(...)) for display & day grouping
// - A true UTC Date (dt) for solarElevationDeg (which uses .getUTC*).
// ------------------------------------------------------------------------
import {
vaporPressureHpa, solarElevationDeg, calcTmrt, utciApprox,
@@ -20,7 +20,7 @@ import { windCompass8, uvSplit, cloudCategory, precipPenalty } from './utils.js'
export function buildHourlyRows({ forecast, airQuality, location, vehicleType, vehicleVent, buildingType }) {
const utcOffsetMs = (forecast?.utc_offset_seconds ?? 0) * 1000;
// Build a fast lookup map from the air quality hourly data: ISO string index.
// Build a fast lookup map from the air quality hourly data: ISO string - index.
// Normalise to "YYYY-MM-DDTHH" (13 chars) so forecast timestamps like
// "2026-05-17T14:00" match AQ timestamps like "2026-05-17T14".
const aqTimeMap = {};
@@ -56,17 +56,17 @@ export function buildHourlyRows({ forecast, airQuality, location, vehicleType, v
const soilT6 = h.soil_temperature_6cm ? h.soil_temperature_6cm[i] : null;
const soilM = h.soil_moisture_0_to_1cm ? h.soil_moisture_0_to_1cm[i] : null;
// Use direct_radiation (beam sunlight) + a fraction of diffuse for concrete.
// direct_radiation is zero on fully overcast days far more accurate than
// direct_radiation is zero on fully overcast days - far more accurate than
// shortwave_radiation which can be unreliably high even at 100% cloud cover.
// Diffuse (scattered light through cloud) contributes ~20% as much heat to
// a surface as direct beam, so we weight it accordingly.
const effectiveRad = dir + dif * 0.2;
const concreteT = calcConcreteTemp(Ta, effectiveRad, va);
// iso is a local wall-clock string e.g. "2026-05-13T14:00" (no Z).
// For display we slice the string directly no Date object needed.
// For display we slice the string directly - no Date object needed.
// For solarElevationDeg (which uses .getUTC* internally) we need the
// true UTC instant: treat the local time as UTC then subtract the offset.
// e.g. Brisbane UTC+10: local 14:00 parse as UTC 14:00 subtract 10h UTC 04:00
// e.g. Brisbane UTC+10: local 14:00 - parse as UTC 14:00 - subtract 10h - UTC 04:00 -
const dt = new Date(Date.parse(iso + 'Z') - utcOffsetMs);
const elev = solarElevationDeg(location.lat, location.lon, dt);
const vehicleT = calcVehicleInteriorTemp(Ta, glob, elev, vehicleType, vehicleVent);
@@ -78,7 +78,7 @@ export function buildHourlyRows({ forecast, airQuality, location, vehicleType, v
const compass = windCompass8(wd);
const { uvA, uvB } = uvSplit(uv, elev);
const cloudCat = cloudCategory(cc, ccLow, ccMid, ccHigh);
// Visibility from the main forecast API (metres km).
// Visibility from the main forecast API (metres - km).
const visKm = (() => { const v = h.visibility ? h.visibility[i] : null; return v != null ? v / 1000 : null; })();
const aqi = getAq('european_aqi', iso);
const grassPollen = getAq('grass_pollen', iso);
@@ -87,22 +87,22 @@ export function buildHourlyRows({ forecast, airQuality, location, vehicleType, v
const mugwortPollen= getAq('mugwort_pollen', iso);
const olivePollen = getAq('olive_pollen', iso);
const ragweedPollen= getAq('ragweed_pollen', iso);
// ── FUTURE FEATURE: Activity "What If" Modifier ───────────────────────────
// -- FUTURE FEATURE: Activity "What If" Modifier ---------------------------
// Add two extra columns driven by a user-selected activity level. These are
// intentionally kept SEPARATE from the core columns above so that baseline
// profile data stays consistent and comparable across profiles.
//
// The user picks an activity from a simple UI picker (no live data needed
// The user picks an activity from a simple UI picker (no live data needed -
// this is a forecast/planning tool, not a tracker):
// Resting Walking Cycling Running Sport/Intense
// Resting - Walking - Cycling - Running - Sport/Intense
//
// Two output columns only (keep it clean):
//
// adjustedSafeTime baseline UV safe exposure time × an activity multiplier.
// adjustedSafeTime - baseline UV safe exposure time - an activity multiplier.
// Higher activity = shorter safe time, because:
// metabolic heat raises core body temp
// sweating washes away sunscreen faster
// more skin blood flow = higher UV sensitivity
// - metabolic heat raises core body temp
// - sweating washes away sunscreen faster
// - more skin blood flow = higher UV sensitivity
// Suggested multipliers (tune with real data):
// Resting: 1.0 (no change)
// Walking: 0.85
@@ -110,22 +110,22 @@ export function buildHourlyRows({ forecast, airQuality, location, vehicleType, v
// Running: 0.60
// Sport: 0.50
//
// heatStressLevel a simple label: 'Low' | 'Moderate' | 'High' | 'Very High'
// heatStressLevel - a simple label: 'Low' | 'Moderate' | 'High' | 'Very High'
// Derived from UTCI + activity heat load. A runner at
// UTCI 28°C should read 'High' even if a resting person
// UTCI 28-C should read 'High' even if a resting person
// would read 'Moderate' at the same UTCI.
// Colour code in the UI: 🟢 🟡 🟠 🔴
// Colour code in the UI: - - - -
//
// Implementation sketch:
// 1. Accept `activityLevel` as a new param to buildHourlyRows() alongside
// 1. Accept 'activityLevel' as a new param to buildHourlyRows() alongside
// vehicleType, buildingType etc.
// 2. Define ACTIVITY_PRESETS in utils.js (multiplier + utciOffset per level).
// 3. Compute adjustedSafeTime = baseSafeTime * preset.multiplier
// 4. Compute heatStressLevel from (utci + preset.utciOffset) banded into labels.
// 5. Add both fields to the returned row object below.
// 6. In components.js, render these as optional columns that only appear when
// an activity other than 'Resting' is selected keeps the default table clean.
// ─────────────────────────────────────────────────────────────────────────────
// an activity other than 'Resting' is selected - keeps the default table clean.
// -----------------------------------------------------------------------------
return {
iso, dt, Ta, RH, dew, va, wd, gust, dir, dif, glob,
@@ -160,45 +160,45 @@ export function buildHourlyRows({ forecast, airQuality, location, vehicleType, v
day.rows.push(row);
});
// ── FUTURE FEATURE v2: Today Summary + Alert System ──────────────────────────
// -- FUTURE FEATURE v2: Today Summary + Alert System --------------------------
// After grouping rows into days, generate a per-day summary object that powers
// a stylish "Today at a Glance" panel shown above or below the main dial.
//
// The summary is NOT a live alert/push system it's a forecast digest that
// The summary is NOT a live alert/push system - it's a forecast digest that
// refreshes with the forecast data. Think of it as a smart briefing card.
//
// WHAT TO COMPUTE (per day, from that day's rows):
// Peak UTCI+P and time it occurs heat stress headline
// Min UTCI+P and time cold stress headline
// Peak UV index and time UV warning
// Max precipitation rate and time rain/ice warning
// Max vehicle cabin temp "dangerous to leave pets/children in car"
// Max pollen level + type pollen advisory
// Road condition risk (low air temp + precip ice risk)
// - Peak UTCI+P and time it occurs - heat stress headline
// - Min UTCI+P and time - cold stress headline
// - Peak UV index and time - UV warning
// - Max precipitation rate and time - rain/ice warning
// - Max vehicle cabin temp - "dangerous to leave pets/children in car"
// - Max pollen level + type - pollen advisory
// - Road condition risk (low air temp + precip - ice risk)
//
// ALERT CATEGORIES (each generates a styled warning card if threshold exceeded):
// 🌡️ Heat warning UTCI+P > 32°C
// 🥶 Cold warning UTCI+P < 0°C
// ☀️ UV warning UV index > 6
// 🌧️ Heavy rain precip > 4mm/h
// 🧊 Ice/road risk Ta < 3°C + any precip (or recent precip overnight)
// 🚗 Vehicle danger vehicleT > 35°C ("don't leave pets or children in car")
// 🌿 High pollen any pollen type > 50 grains/m³
// -- Heat warning UTCI+P > 32-C
// - Cold warning UTCI+P < 0-C
// -- UV warning UV index > 6
// -- Heavy rain precip > 4mm/h
// - Ice/road risk Ta < 3-C + any precip (or recent precip overnight)
// - Vehicle danger vehicleT > 35-C ("don't leave pets or children in car")
// - High pollen any pollen type > 50 grains/m-
//
// DESIGN NOTES:
// Cards should be concise one line of bold text + a short explanation
// Colour-coded to match the existing UTCI stress band palette
// Collapsible show top 2-3 alerts by default, expand for full list
// For today only (days[0]); optionally extend to day tabs in a later pass
// The "X°C above seasonal norm" historical context line (see app.js comment)
// - Cards should be concise - one line of bold text + a short explanation
// - Colour-coded to match the existing UTCI stress band palette
// - Collapsible - show top 2-3 alerts by default, expand for full list
// - For today only (days[0]); optionally extend to day tabs in a later pass
// - The "X-C above seasonal norm" historical context line (see app.js comment)
// could live here too, as a subtle subheading under the dial temperature
//
// Suggested return shape add to the return value below:
// Suggested return shape - add to the return value below:
// daySummaries: days.map(day => buildDaySummary(day.rows))
//
// where buildDaySummary() is a new helper in this file (or a separate
// summary.js module if it grows large).
// ─────────────────────────────────────────────────────────────────────────────
// -----------------------------------------------------------------------------
return { hourlyRows, days, utcOffsetMs };
}