diff --git a/assets/js/app.js b/assets/js/app.js index 64eadc9..c225a9e 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -339,7 +339,7 @@ export function UTCIForecast() { const GROUP_ORDER = { felt: ['utciP', 'vehicleT', 'indoorT', 'managedT', 'burn', 'utci', 'delta', 'tmrt'], surface: ['concreteT', 'soilT', 'soilT6', 'soilM'], - ambient: ['air', 'rh', 'dew'], + ambient: ['shadeT', 'air', 'rh', 'dew'], precip: ['precip', 'precipProb', 'lightning'], sky: ['cloud', 'vis'], wind: ['wind', 'dir'], @@ -1066,6 +1066,7 @@ export function UTCIForecast() { ${isPro && (activeProfile === 'custom' || activeProfile === 'showall' || activeCols['soilT']) && html``} ${isPro && (activeProfile === 'custom' || activeProfile === 'showall' || activeCols['soilT6']) && html``} ${isPro && (activeProfile === 'custom' || activeProfile === 'showall' || activeCols['soilM']) && html``} + ${isPro && (activeProfile === 'custom' || activeProfile === 'showall' || activeCols['shadeT']) && html``} ${isPro && (activeProfile === 'custom' || activeProfile === 'showall' || activeCols['air']) && html``} ${isPro && (activeProfile === 'custom' || activeProfile === 'showall' || activeCols['rh']) && html``} ${isPro && (activeProfile === 'custom' || activeProfile === 'showall' || activeCols['dew']) && html``} @@ -1279,6 +1280,7 @@ export function UTCIForecast() { ${visibleCols.soilT && html` handleThClick('soilT', e)} onMouseEnter=${(e) => handleThEnter('soilT', e)} onMouseLeave=${handleThLeave}>Soil °C surface`} ${visibleCols.soilT6 && html` handleThClick('soilT6', e)} onMouseEnter=${(e) => handleThEnter('soilT6', e)} onMouseLeave=${handleThLeave}>Soil 6cm °C root`} ${visibleCols.soilM && html` handleThClick('soilM', e)} onMouseEnter=${(e) => handleThEnter('soilM', e)} onMouseLeave=${handleThLeave}>Soil moist %`} + ${visibleCols.shadeT && html` handleThClick('shadeT', e)} onMouseEnter=${(e) => handleThEnter('shadeT', e)} onMouseLeave=${handleThLeave}>Shade °C`} ${visibleCols.air && html` handleThClick('air', e)} onMouseEnter=${(e) => handleThEnter('air', e)} onMouseLeave=${handleThLeave}>Air °C`} ${visibleCols.rh && html` handleThClick('rh', e)} onMouseEnter=${(e) => handleThEnter('rh', e)} onMouseLeave=${handleThLeave}>RH %`} ${visibleCols.dew && html` handleThClick('dew', e)} onMouseEnter=${(e) => handleThEnter('dew', e)} onMouseLeave=${handleThLeave}>Dew °C`} @@ -1461,6 +1463,7 @@ export function UTCIForecast() { ${r.soilT6 != null ? fmt(r.soilT6) + u('°C') : '—'}`} ${visibleCols.soilM && html` { const sm = soilMoistureBg(r.soilM); return { color: sm.fg, background: sm.bg }; })()}>${r.soilM != null ? fmt(r.soilM * 100, 1) + u('%') : '—'}`} + ${visibleCols.shadeT && html`${r.shadeT != null ? fmt(r.shadeT) + u('°C') : '—'}`} ${visibleCols.air && html`${fmt(r.Ta)}${u('°C')}`} ${visibleCols.rh && html`${Math.round(r.RH)}${u('%')}`} ${visibleCols.dew && html`${fmt(r.dew)}${u('°C')}`} diff --git a/assets/js/components/DayTabs.js b/assets/js/components/DayTabs.js index 1b90be9..5d41be3 100644 --- a/assets/js/components/DayTabs.js +++ b/assets/js/components/DayTabs.js @@ -313,9 +313,9 @@ export function DayTabs({ const utcipVals = d.rows.map(r => r.utciAdj).filter(v => isFinite(v)); const dayHi = utcipVals.length ? Math.round(Math.max(...utcipVals)) : null; const dayLo = utcipVals.length ? Math.round(Math.min(...utcipVals)) : null; - const airVals = d.rows.map(r => r.Ta).filter(v => isFinite(v)); - const airHi = airVals.length ? Math.round(Math.max(...airVals)) : null; - const airLo = airVals.length ? Math.round(Math.min(...airVals)) : null; + const shadeVals = d.rows.map(r => r.shadeT).filter(v => isFinite(v)); + const shadeHi = shadeVals.length ? Math.round(Math.max(...shadeVals)) : null; + const shadeLo = shadeVals.length ? Math.round(Math.min(...shadeVals)) : null; // Day-tab weather icon - use core daylight rows (elev > 10°) where // available, falling back to any above-horizon rows, then all rows. @@ -415,12 +415,12 @@ export function DayTabs({ }}> ${dayHi}° - ${airHi !== null && html`${airHi}°`} + ${shadeHi !== null && html`${shadeHi}°`} ${dayLo}° - ${airLo !== null && html`${airLo}°`} + ${shadeLo !== null && html`${shadeLo}°`} `} `; diff --git a/assets/js/compute.js b/assets/js/compute.js index eedc52c..2bd59a9 100644 --- a/assets/js/compute.js +++ b/assets/js/compute.js @@ -13,7 +13,7 @@ import { vaporPressureHpa, solarElevationDeg, calcTmrt, utciApprox, calcConcreteTempPass, calcVehicleInteriorTemp, - calcIndoorTempPass, calcManagedIndoorTempPass, + calcIndoorTempPass, calcManagedIndoorTempPass, calcShadeAirTemp, } from './physics.js'; import { windCompass8, uvSplit, cloudCategory, precipPenalty, sunburnMinutes, burnLabel } from './utils.js'; import { UTCI_ENVIRONMENTS } from './config.js'; @@ -77,6 +77,9 @@ 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). + // Shade air temp: the air you'd feel in this Solar Model's typical shade + // (building shadow, beach umbrella, canopy...). Uses the env's shade block. + const shadeT = calcShadeAirTemp(Ta, effectiveRad, va, elev, env.shade); 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. @@ -147,7 +150,7 @@ export function buildHourlyRows({ forecast, airQuality, location, vehicleType, v cc, ccLow, ccMid, ccHigh, cloudCat, uv, uvA, uvB, precip, precipProb, lightning, cape, snow, - soilT0, soilT6, soilM, vehicleT, + soilT0, soilT6, soilM, vehicleT, shadeT, effectiveRad, elev, Tmrt, utci, utciAdj, eh, compass, visKm, aqi, @@ -258,7 +261,7 @@ export function buildHourlyRows({ forecast, airQuality, location, vehicleType, v // interval <= 1 returns the input untouched (zero behaviour change). // ------------------------------------------------------------------------ const AGG_MEAN = [ - 'Ta', 'RH', 'dew', 'va', 'dir', 'dif', 'cc', 'ccLow', 'ccMid', 'ccHigh', + 'Ta', 'shadeT', 'RH', 'dew', 'va', 'dir', 'dif', 'cc', 'ccLow', 'ccMid', 'ccHigh', 'soilT0', 'soilT6', 'soilM', 'Tmrt', 'eh', 'visKm', 'aqi', 'effectiveRad', 'grassPollen', 'birchPollen', 'alderPollen', 'mugwortPollen', 'olivePollen', 'ragweedPollen', ]; diff --git a/assets/js/config.js b/assets/js/config.js index b90c245..2a06801 100644 --- a/assets/js/config.js +++ b/assets/js/config.js @@ -29,46 +29,46 @@ export const FILTER_PROFILES = { label: 'Basic', icon: '🌡️', scene: "url('assets/images/profiles/basic.png') center / cover no-repeat, linear-gradient(160deg,#b8d8e8,#daeef8)", - cols: { hour: true, air: true, rh: true, dew: false, wind: true, dir: false, cloud: true, sun: false, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: false, uvB: false, burn: false, utciP: true, precip: true, precipProb: true, lightning: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false, vis: false, aqi: false, pollen: true }, + cols: { hour: true, air: true, shadeT: true, rh: true, dew: false, wind: true, dir: false, cloud: true, sun: false, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: false, uvB: false, burn: false, utciP: true, precip: true, precipProb: true, lightning: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false, vis: false, aqi: false, pollen: true }, }, home: { label: 'Home', icon: '🏠', scene: "url('assets/images/profiles/home.png') center / cover no-repeat, linear-gradient(160deg,#b0d890,#d0ecb8)", - cols: { hour: true, air: true, rh: true, dew: true, wind: true, dir: false, cloud: true, sun: false, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: false, uvB: false, burn: false, utciP: false, precip: true, precipProb: true, lightning: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: true, managedT: true, vis: false, aqi: true, pollen: true }, + cols: { hour: true, air: true, shadeT: false, rh: true, dew: true, wind: true, dir: false, cloud: true, sun: false, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: false, uvB: false, burn: false, utciP: false, precip: true, precipProb: true, lightning: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: true, managedT: true, vis: false, aqi: true, pollen: true }, }, vehicle: { label: 'Vehicle', icon: '🚗', scene: "url('assets/images/profiles/vehicle.png') center / cover no-repeat, linear-gradient(160deg,#a8bcc8,#ccd8e4)", - cols: { hour: true, air: true, rh: false, dew: false, wind: true, dir: false, cloud: true, sun: false, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: false, uvB: false, burn: false, utciP: true, precip: true, precipProb: true, lightning: true, soilT: false, soilT6: false, soilM: true, concreteT: false, vehicleT: true, indoorT: false, managedT: false, vis: true, aqi: false, pollen: false }, + cols: { hour: true, air: true, shadeT: false, rh: false, dew: false, wind: true, dir: false, cloud: true, sun: false, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: false, uvB: false, burn: false, utciP: true, precip: true, precipProb: true, lightning: true, soilT: false, soilT6: false, soilM: true, concreteT: false, vehicleT: true, indoorT: false, managedT: false, vis: true, aqi: false, pollen: false }, }, farming: { label: 'Farming', icon: '🌾', scene: "url('assets/images/profiles/farming.png') center / cover no-repeat, linear-gradient(160deg,#d8bc90,#ece0b8)", - cols: { hour: true, air: true, rh: true, dew: true, wind: true, dir: true, cloud: true, sun: true, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: false, uvB: false, burn: false, utciP: true, precip: true, precipProb: true, lightning: true, soilT: true, soilT6: true, soilM: true, concreteT: false, vehicleT: false, indoorT: false, managedT: false, vis: false, aqi: true, pollen: true }, + cols: { hour: true, air: true, shadeT: true, rh: true, dew: true, wind: true, dir: true, cloud: true, sun: true, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: false, uvB: false, burn: false, utciP: true, precip: true, precipProb: true, lightning: true, soilT: true, soilT6: true, soilM: true, concreteT: false, vehicleT: false, indoorT: false, managedT: false, vis: false, aqi: true, pollen: true }, }, outdoors: { label: 'Places', icon: '🌤️', scene: "url('assets/images/profiles/places.png') center / cover no-repeat, linear-gradient(160deg,#98ccc0,#c4e4dc)", // Default cols match the first variant (urban). Switching variant updates visibleCols. - cols: { hour: true, air: true, rh: false, dew: false, wind: true, dir: true, cloud: true, sun: false, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: true, uvB: false, burn: true, utciP: true, precip: true, precipProb: true, lightning: true, soilT: false, soilT6: false, soilM: false, concreteT: true, vehicleT: false, indoorT: false, managedT: false, vis: true, aqi: true, pollen: false }, + cols: { hour: true, air: true, shadeT: true, rh: false, dew: false, wind: true, dir: true, cloud: true, sun: false, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: true, uvB: false, burn: true, utciP: true, precip: true, precipProb: true, lightning: true, soilT: false, soilT6: false, soilM: false, concreteT: true, vehicleT: false, indoorT: false, managedT: false, vis: true, aqi: true, pollen: false }, }, alltemps: { label: 'Temps', icon: '🌡️', scene: "url('assets/images/profiles/temps.png') center / cover no-repeat, linear-gradient(160deg,#d8b0b0,#ecd0c8)", proOnly: true, - cols: { hour: true, air: true, rh: false, dew: false, wind: false, dir: false, cloud: false, sun: false, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: false, uvB: false, burn: false, utciP: true, precip: false, precipProb: false, lightning: false, soilT: true, soilT6: false, soilM: false, concreteT: true, vehicleT: true, indoorT: true, managedT: true, vis: false, aqi: false, pollen: false }, + cols: { hour: true, air: true, shadeT: true, rh: false, dew: false, wind: false, dir: false, cloud: false, sun: false, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: false, uvB: false, burn: false, utciP: true, precip: false, precipProb: false, lightning: false, soilT: true, soilT6: false, soilM: false, concreteT: true, vehicleT: true, indoorT: true, managedT: true, vis: false, aqi: false, pollen: false }, }, showall: { label: 'Show All', icon: '⚙️', scene: "url('assets/images/profiles/all.png') center / cover no-repeat, linear-gradient(160deg,#b4b4d4,#d4d4ec)", proOnly: true, - cols: { hour: true, air: true, rh: true, dew: true, wind: true, dir: true, cloud: true, sun: true, direct: true, diffuse: true, tmrt: true, delta: true, utci: true, uvA: true, uvB: true, burn: true, utciP: true, precip: true, precipProb: true, lightning: true, soilT: true, soilT6: true, soilM: true, concreteT: true, vehicleT: true, indoorT: true, managedT: true, vis: true, aqi: true, pollen: true }, + cols: { hour: true, air: true, shadeT: true, rh: true, dew: true, wind: true, dir: true, cloud: true, sun: true, direct: true, diffuse: true, tmrt: true, delta: true, utci: true, uvA: true, uvB: true, burn: true, utciP: true, precip: true, precipProb: true, lightning: true, soilT: true, soilT6: true, soilM: true, concreteT: true, vehicleT: true, indoorT: true, managedT: true, vis: true, aqi: true, pollen: true }, }, custom: { label: 'Custom', @@ -76,39 +76,39 @@ export const FILTER_PROFILES = { scene: "url('assets/images/profiles/custom.png') center / cover no-repeat, linear-gradient(160deg,#d8b0cc,#ecd0e4)", proOnly: true, // Blank slate - only Hour is on. User enables whichever columns they want. - cols: { hour: true, air: false, rh: false, dew: false, wind: false, dir: false, cloud: false, sun: false, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: false, uvB: false, burn: false, utciP: false, precip: false, precipProb: false, lightning: false, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false, vis: false, aqi: false, pollen: false }, + cols: { hour: true, air: false, shadeT: false, rh: false, dew: false, wind: false, dir: false, cloud: false, sun: false, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: false, uvB: false, burn: false, utciP: false, precip: false, precipProb: false, lightning: false, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false, vis: false, aqi: false, pollen: false }, }, }; // Places sub-variants - each has its own column set. // Selecting a variant applies its cols to visibleCols. export const OUTDOORS_VARIANTS = { - urban: { name: 'Urban', scene: "url('assets/images/profiles/urban.png') center / cover no-repeat, linear-gradient(160deg,#c2d4cc,#e2ece6)", cols: { hour: true, air: true, rh: false, dew: false, wind: true, dir: true, cloud: true, sun: false, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: true, uvB: false, burn: true, utciP: true, precip: true, precipProb: true, lightning: true, soilT: false, soilT6: false, soilM: false, concreteT: true, vehicleT: false, indoorT: false, managedT: false, vis: true, aqi: true, pollen: false } }, - beach: { name: 'Beach', scene: "url('assets/images/profiles/beach.png') center / cover no-repeat, linear-gradient(160deg,#c2d4cc,#e2ece6)", cols: { hour: true, air: true, rh: false, dew: true, wind: true, dir: true, cloud: true, sun: true, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: true, uvB: true, burn: true, utciP: true, precip: true, precipProb: true, lightning: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false, vis: true, aqi: false, pollen: false } }, - events: { name: 'Events', scene: "url('assets/images/profiles/events.png') center / cover no-repeat, linear-gradient(160deg,#c2d4cc,#e2ece6)", cols: { hour: true, air: true, rh: false, dew: false, wind: true, dir: true, cloud: true, sun: false, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: true, uvB: false, burn: true, utciP: true, precip: true, precipProb: true, lightning: true, soilT: false, soilT6: false, soilM: false, concreteT: true, vehicleT: false, indoorT: false, managedT: false, vis: false, aqi: false, pollen: true } }, - festival: { name: 'Festival', scene: "url('assets/images/profiles/festival.png') center / cover no-repeat, linear-gradient(160deg,#c2d4cc,#e2ece6)", cols: { hour: true, air: true, rh: false, dew: true, wind: true, dir: true, cloud: true, sun: false, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: true, uvB: false, burn: true, utciP: true, precip: true, precipProb: true, lightning: true, soilT: false, soilT6: false, soilM: true, concreteT: false, vehicleT: false, indoorT: false, managedT: false, vis: false, aqi: true, pollen: true } }, - wintersports: { name: 'Winter Sports', proOnly: true, scene: "url('assets/images/profiles/winter.png') center / cover no-repeat, linear-gradient(160deg,#c2d4cc,#e2ece6)", cols: { hour: true, air: true, rh: false, dew: false, wind: true, dir: true, cloud: true, sun: true, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: true, uvB: true, burn: true, utciP: true, precip: true, precipProb: true, lightning: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false, vis: true, aqi: false, pollen: false } }, - naturist: { name: 'Naturist', proOnly: true, scene: "url('assets/images/profiles/naturist.png') center / cover no-repeat, linear-gradient(160deg,#c2d4cc,#e2ece6)", cols: { hour: true, air: true, rh: false, dew: true, wind: true, dir: false, cloud: true, sun: true, direct: false, diffuse: false, tmrt: false, delta: false, utci: true, uvA: true, uvB: true, burn: true, utciP: true, precip: true, precipProb: true, lightning: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false, vis: false, aqi: true, pollen: true } }, - sailing: { name: 'Sailing', proOnly: true, scene: "url('assets/images/profiles/sailing.png') center / cover no-repeat, linear-gradient(160deg,#c2d4cc,#e2ece6)", cols: { hour: true, air: true, rh: false, dew: true, wind: true, dir: true, cloud: true, sun: true, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: true, uvB: false, burn: true, utciP: true, precip: true, precipProb: true, lightning: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false, vis: true, aqi: false, pollen: false } }, + urban: { name: 'Urban', scene: "url('assets/images/profiles/urban.png') center / cover no-repeat, linear-gradient(160deg,#c2d4cc,#e2ece6)", cols: { hour: true, air: true, shadeT: true, rh: false, dew: false, wind: true, dir: true, cloud: true, sun: false, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: true, uvB: false, burn: true, utciP: true, precip: true, precipProb: true, lightning: true, soilT: false, soilT6: false, soilM: false, concreteT: true, vehicleT: false, indoorT: false, managedT: false, vis: true, aqi: true, pollen: false } }, + beach: { name: 'Beach', scene: "url('assets/images/profiles/beach.png') center / cover no-repeat, linear-gradient(160deg,#c2d4cc,#e2ece6)", cols: { hour: true, air: true, shadeT: true, rh: false, dew: true, wind: true, dir: true, cloud: true, sun: true, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: true, uvB: true, burn: true, utciP: true, precip: true, precipProb: true, lightning: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false, vis: true, aqi: false, pollen: false } }, + events: { name: 'Events', scene: "url('assets/images/profiles/events.png') center / cover no-repeat, linear-gradient(160deg,#c2d4cc,#e2ece6)", cols: { hour: true, air: true, shadeT: true, rh: false, dew: false, wind: true, dir: true, cloud: true, sun: false, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: true, uvB: false, burn: true, utciP: true, precip: true, precipProb: true, lightning: true, soilT: false, soilT6: false, soilM: false, concreteT: true, vehicleT: false, indoorT: false, managedT: false, vis: false, aqi: false, pollen: true } }, + festival: { name: 'Festival', scene: "url('assets/images/profiles/festival.png') center / cover no-repeat, linear-gradient(160deg,#c2d4cc,#e2ece6)", cols: { hour: true, air: true, shadeT: true, rh: false, dew: true, wind: true, dir: true, cloud: true, sun: false, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: true, uvB: false, burn: true, utciP: true, precip: true, precipProb: true, lightning: true, soilT: false, soilT6: false, soilM: true, concreteT: false, vehicleT: false, indoorT: false, managedT: false, vis: false, aqi: true, pollen: true } }, + wintersports: { name: 'Winter Sports', proOnly: true, scene: "url('assets/images/profiles/winter.png') center / cover no-repeat, linear-gradient(160deg,#c2d4cc,#e2ece6)", cols: { hour: true, air: true, shadeT: false, rh: false, dew: false, wind: true, dir: true, cloud: true, sun: true, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: true, uvB: true, burn: true, utciP: true, precip: true, precipProb: true, lightning: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false, vis: true, aqi: false, pollen: false } }, + naturist: { name: 'Naturist', proOnly: true, scene: "url('assets/images/profiles/naturist.png') center / cover no-repeat, linear-gradient(160deg,#c2d4cc,#e2ece6)", cols: { hour: true, air: true, shadeT: true, rh: false, dew: true, wind: true, dir: false, cloud: true, sun: true, direct: false, diffuse: false, tmrt: false, delta: false, utci: true, uvA: true, uvB: true, burn: true, utciP: true, precip: true, precipProb: true, lightning: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false, vis: false, aqi: true, pollen: true } }, + sailing: { name: 'Sailing', proOnly: true, scene: "url('assets/images/profiles/sailing.png') center / cover no-repeat, linear-gradient(160deg,#c2d4cc,#e2ece6)", cols: { hour: true, air: true, shadeT: false, rh: false, dew: true, wind: true, dir: true, cloud: true, sun: true, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: true, uvB: false, burn: true, utciP: true, precip: true, precipProb: true, lightning: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false, vis: true, aqi: false, pollen: false } }, // Places - free - park: { name: 'Park / Picnic', scene: "url('assets/images/profiles/park.png') center / cover no-repeat, linear-gradient(160deg,#c2d4cc,#e2ece6)", cols: { hour: true, air: true, rh: false, dew: false, wind: true, dir: false, cloud: true, sun: false, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: false, uvB: false, burn: true, utciP: true, precip: true, precipProb: true, lightning: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false, vis: false, aqi: false, pollen: true } }, - forest: { name: 'Forest / Woodland', scene: "url('assets/images/profiles/forest.png') center / cover no-repeat, linear-gradient(160deg,#c2d4cc,#e2ece6)", cols: { hour: true, air: true, rh: false, dew: false, wind: true, dir: false, cloud: true, sun: false, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: false, uvB: false, burn: true, utciP: true, precip: true, precipProb: true, lightning: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false, vis: true, aqi: true, pollen: true } }, - airport: { name: 'Airport / Travel', scene: "url('assets/images/profiles/airport.png') center / cover no-repeat, linear-gradient(160deg,#c2d4cc,#e2ece6)", cols: { hour: true, air: true, rh: false, dew: false, wind: true, dir: true, cloud: true, sun: false, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: false, uvB: false, burn: false, utciP: false, precip: true, precipProb: true, lightning: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false, vis: true, aqi: false, pollen: false } }, + park: { name: 'Park / Picnic', scene: "url('assets/images/profiles/park.png') center / cover no-repeat, linear-gradient(160deg,#c2d4cc,#e2ece6)", cols: { hour: true, air: true, shadeT: true, rh: false, dew: false, wind: true, dir: false, cloud: true, sun: false, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: false, uvB: false, burn: true, utciP: true, precip: true, precipProb: true, lightning: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false, vis: false, aqi: false, pollen: true } }, + forest: { name: 'Forest / Woodland', scene: "url('assets/images/profiles/forest.png') center / cover no-repeat, linear-gradient(160deg,#c2d4cc,#e2ece6)", cols: { hour: true, air: true, shadeT: false, rh: false, dew: false, wind: true, dir: false, cloud: true, sun: false, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: false, uvB: false, burn: true, utciP: true, precip: true, precipProb: true, lightning: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false, vis: true, aqi: true, pollen: true } }, + airport: { name: 'Airport / Travel', scene: "url('assets/images/profiles/airport.png') center / cover no-repeat, linear-gradient(160deg,#c2d4cc,#e2ece6)", cols: { hour: true, air: true, shadeT: false, rh: false, dew: false, wind: true, dir: true, cloud: true, sun: false, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: false, uvB: false, burn: false, utciP: false, precip: true, precipProb: true, lightning: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false, vis: true, aqi: false, pollen: false } }, // Places - Pro - construction: { name: 'Construction', proOnly: true, scene: "url('assets/images/profiles/construction.png') center / cover no-repeat, linear-gradient(160deg,#c2d4cc,#e2ece6)", cols: { hour: true, air: true, rh: false, dew: false, wind: true, dir: true, cloud: true, sun: false, direct: false, diffuse: false, tmrt: false, delta: false, utci: true, uvA: false, uvB: false, burn: false, utciP: true, precip: true, precipProb: true, lightning: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false, vis: true, aqi: true, pollen: false } }, + construction: { name: 'Construction', proOnly: true, scene: "url('assets/images/profiles/construction.png') center / cover no-repeat, linear-gradient(160deg,#c2d4cc,#e2ece6)", cols: { hour: true, air: true, shadeT: true, rh: false, dew: false, wind: true, dir: true, cloud: true, sun: false, direct: false, diffuse: false, tmrt: false, delta: false, utci: true, uvA: false, uvB: false, burn: false, utciP: true, precip: true, precipProb: true, lightning: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false, vis: true, aqi: true, pollen: false } }, // Activities - free - cycling: { name: 'Cycling', scene: "url('assets/images/profiles/cycling.png') center / cover no-repeat, linear-gradient(160deg,#c2d4cc,#e2ece6)", cols: { hour: true, air: true, rh: false, dew: false, wind: true, dir: true, cloud: true, sun: false, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: false, uvB: false, burn: true, utciP: true, precip: true, precipProb: true, lightning: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false, vis: true, aqi: false, pollen: false } }, - running: { name: 'Running', scene: "url('assets/images/profiles/running.png') center / cover no-repeat, linear-gradient(160deg,#c2d4cc,#e2ece6)", cols: { hour: true, air: true, rh: true, dew: false, wind: true, dir: false, cloud: true, sun: false, direct: false, diffuse: false, tmrt: false, delta: false, utci: true, uvA: false, uvB: false, burn: true, utciP: true, precip: true, precipProb: true, lightning: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false, vis: false, aqi: false, pollen: true } }, - dogwalk: { name: 'Dog Walking', scene: "url('assets/images/profiles/walking.png') center / cover no-repeat, linear-gradient(160deg,#c2d4cc,#e2ece6)", cols: { hour: true, air: true, rh: false, dew: false, wind: true, dir: false, cloud: true, sun: false, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: false, uvB: false, burn: false, utciP: true, precip: true, precipProb: true, lightning: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false, vis: false, aqi: false, pollen: true } }, + cycling: { name: 'Cycling', scene: "url('assets/images/profiles/cycling.png') center / cover no-repeat, linear-gradient(160deg,#c2d4cc,#e2ece6)", cols: { hour: true, air: true, shadeT: false, rh: false, dew: false, wind: true, dir: true, cloud: true, sun: false, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: false, uvB: false, burn: true, utciP: true, precip: true, precipProb: true, lightning: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false, vis: true, aqi: false, pollen: false } }, + running: { name: 'Running', scene: "url('assets/images/profiles/running.png') center / cover no-repeat, linear-gradient(160deg,#c2d4cc,#e2ece6)", cols: { hour: true, air: true, shadeT: false, rh: true, dew: false, wind: true, dir: false, cloud: true, sun: false, direct: false, diffuse: false, tmrt: false, delta: false, utci: true, uvA: false, uvB: false, burn: true, utciP: true, precip: true, precipProb: true, lightning: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false, vis: false, aqi: false, pollen: true } }, + dogwalk: { name: 'Dog Walking', scene: "url('assets/images/profiles/walking.png') center / cover no-repeat, linear-gradient(160deg,#c2d4cc,#e2ece6)", cols: { hour: true, air: true, shadeT: false, rh: false, dew: false, wind: true, dir: false, cloud: true, sun: false, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: false, uvB: false, burn: false, utciP: true, precip: true, precipProb: true, lightning: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false, vis: false, aqi: false, pollen: true } }, // Activities - Pro - hiking: { name: 'Hiking', proOnly: true, scene: "url('assets/images/profiles/hiking.png') center / cover no-repeat, linear-gradient(160deg,#c2d4cc,#e2ece6)", cols: { hour: true, air: true, rh: false, dew: false, wind: true, dir: true, cloud: true, sun: true, direct: false, diffuse: false, tmrt: false, delta: false, utci: true, uvA: true, uvB: true, burn: true, utciP: true, precip: true, precipProb: true, lightning: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false, vis: true, aqi: false, pollen: false } }, - photography: { name: 'Photography', proOnly: true, scene: "url('assets/images/profiles/photography.png') center / cover no-repeat, linear-gradient(160deg,#c2d4cc,#e2ece6)", cols: { hour: true, air: true, rh: false, dew: false, wind: false, dir: false, cloud: true, sun: true, direct: true, diffuse: true, tmrt: false, delta: false, utci: false, uvA: false, uvB: false, burn: false, utciP: false, precip: true, precipProb: true, lightning: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false, vis: true, aqi: false, pollen: false } }, - fishing: { name: 'Fishing', proOnly: true, scene: "url('assets/images/profiles/fishing.png') center / cover no-repeat, linear-gradient(160deg,#c2d4cc,#e2ece6)", cols: { hour: true, air: true, rh: false, dew: true, wind: true, dir: true, cloud: true, sun: false, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: false, uvB: false, burn: false, utciP: true, precip: true, precipProb: true, lightning: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false, vis: true, aqi: false, pollen: false } }, + hiking: { name: 'Hiking', proOnly: true, scene: "url('assets/images/profiles/hiking.png') center / cover no-repeat, linear-gradient(160deg,#c2d4cc,#e2ece6)", cols: { hour: true, air: true, shadeT: false, rh: false, dew: false, wind: true, dir: true, cloud: true, sun: true, direct: false, diffuse: false, tmrt: false, delta: false, utci: true, uvA: true, uvB: true, burn: true, utciP: true, precip: true, precipProb: true, lightning: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false, vis: true, aqi: false, pollen: false } }, + photography: { name: 'Photography', proOnly: true, scene: "url('assets/images/profiles/photography.png') center / cover no-repeat, linear-gradient(160deg,#c2d4cc,#e2ece6)", cols: { hour: true, air: true, shadeT: true, rh: false, dew: false, wind: false, dir: false, cloud: true, sun: true, direct: true, diffuse: true, tmrt: false, delta: false, utci: false, uvA: false, uvB: false, burn: false, utciP: false, precip: true, precipProb: true, lightning: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false, vis: true, aqi: false, pollen: false } }, + fishing: { name: 'Fishing', proOnly: true, scene: "url('assets/images/profiles/fishing.png') center / cover no-repeat, linear-gradient(160deg,#c2d4cc,#e2ece6)", cols: { hour: true, air: true, shadeT: true, rh: false, dew: true, wind: true, dir: true, cloud: true, sun: false, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: false, uvB: false, burn: false, utciP: true, precip: true, precipProb: true, lightning: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false, vis: true, aqi: false, pollen: false } }, // Work variants - market: { name: 'Market Trading', proOnly: true, scene: "url('assets/images/profiles/market.png') center / cover no-repeat, linear-gradient(160deg,#c2d4cc,#e2ece6)", cols: { hour: true, air: true, rh: false, dew: false, wind: true, dir: true, cloud: true, sun: false, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: false, uvB: false, burn: false, utciP: true, precip: true, precipProb: true, lightning: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false, vis: true, aqi: false, pollen: false } }, - windowcleaning: { name: 'Window Cleaning', proOnly: true, scene: "url('assets/images/profiles/window.png') center / cover no-repeat, linear-gradient(160deg,#c2d4cc,#e2ece6)", cols: { hour: true, air: true, rh: false, dew: false, wind: true, dir: true, cloud: true, sun: false, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: false, uvB: false, burn: false, utciP: true, precip: true, precipProb: true, lightning: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false, vis: true, aqi: false, pollen: false } }, - office: { name: 'Office', proOnly: true, scene: "url('assets/images/profiles/office.png') center / cover no-repeat, linear-gradient(160deg,#c2d4cc,#e2ece6)", cols: { hour: true, air: true, rh: true, dew: false, wind: false, dir: false, cloud: true, sun: true, direct: true, diffuse: false, tmrt: false, delta: false, utci: false, uvA: false, uvB: false, burn: false, utciP: true, precip: true, precipProb: true, lightning: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: true, vis: false, aqi: true, pollen: false } }, - driver: { name: 'Driver / Trucker', proOnly: true, scene: "url('assets/images/profiles/vehicle.png') center / cover no-repeat, linear-gradient(160deg,#c2d4cc,#e2ece6)", cols: { hour: true, air: true, rh: false, dew: false, wind: true, dir: true, cloud: true, sun: false, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: true, uvB: false, burn: false, utciP: false, precip: true, precipProb: true, lightning: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: true, indoorT: false, managedT: false, vis: true, aqi: false, pollen: false } }, + market: { name: 'Market Trading', proOnly: true, scene: "url('assets/images/profiles/market.png') center / cover no-repeat, linear-gradient(160deg,#c2d4cc,#e2ece6)", cols: { hour: true, air: true, shadeT: true, rh: false, dew: false, wind: true, dir: true, cloud: true, sun: false, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: false, uvB: false, burn: false, utciP: true, precip: true, precipProb: true, lightning: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false, vis: true, aqi: false, pollen: false } }, + windowcleaning: { name: 'Window Cleaning', proOnly: true, scene: "url('assets/images/profiles/window.png') center / cover no-repeat, linear-gradient(160deg,#c2d4cc,#e2ece6)", cols: { hour: true, air: true, shadeT: false, rh: false, dew: false, wind: true, dir: true, cloud: true, sun: false, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: false, uvB: false, burn: false, utciP: true, precip: true, precipProb: true, lightning: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false, vis: true, aqi: false, pollen: false } }, + office: { name: 'Office', proOnly: true, scene: "url('assets/images/profiles/office.png') center / cover no-repeat, linear-gradient(160deg,#c2d4cc,#e2ece6)", cols: { hour: true, air: true, shadeT: false, rh: true, dew: false, wind: false, dir: false, cloud: true, sun: true, direct: true, diffuse: false, tmrt: false, delta: false, utci: false, uvA: false, uvB: false, burn: false, utciP: true, precip: true, precipProb: true, lightning: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: true, vis: false, aqi: true, pollen: false } }, + driver: { name: 'Driver / Trucker', proOnly: true, scene: "url('assets/images/profiles/vehicle.png') center / cover no-repeat, linear-gradient(160deg,#c2d4cc,#e2ece6)", cols: { hour: true, air: true, shadeT: false, rh: false, dew: false, wind: true, dir: true, cloud: true, sun: false, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: true, uvB: false, burn: false, utciP: false, precip: true, precipProb: true, lightning: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: true, indoorT: false, managedT: false, vis: true, aqi: false, pollen: false } }, }; // Pollen type options shown in the pulldown selector. @@ -130,15 +130,21 @@ export const POLLEN_TYPES = { // taOffset - offset applied to air temperature in degrees C (evapotranspiration etc.) // label - short display name // note - shown in column header tooltip to flag modified calculation +// shade - per-environment microclimate inputs for the Shade air-temp +// column (see calcShadeAirTemp in physics.js): +// offset base air-temp delta sitting in this env's shade (-C) +// shelter 0-1 wind shelter (high = enclosed, calm pocket) +// sun 0-~1.2 how sun-baked the surroundings are +// type short label of the typical shade for the tooltip export const UTCI_ENVIRONMENTS = { - open: { shortLabel: null, label: 'Grass / Open Field', dirFactor: 1.00, difFactor: 1.00, globFactor: 1.00, taOffset: 0.0, note: null }, - urban: { shortLabel: 'City', label: 'Town / City', dirFactor: 0.70, difFactor: 0.75, globFactor: 0.82, taOffset: 2.5, note: 'Urban heat island estimate - UHI raises air temp ~2–3 °C, building canyons reduce direct and diffuse solar load, reflections from concrete and glass partially offset sky-view reduction' }, - beach: { shortLabel: 'Beach', label: 'Beach / Lakeside', dirFactor: 1.12, difFactor: 1.06, globFactor: 1.10, taOffset: 0.0, note: 'Reflected radiation from water and sand surfaces increases effective solar load - no shade, higher UV dose' }, - river: { shortLabel: 'Riverbank', label: 'River / Riverbank', dirFactor: 0.50, difFactor: 0.60, globFactor: 0.55, taOffset: -2.5, note: 'Shaded riverbank estimate - canopy and valley walls reduce direct sun, evaporative cooling from water surface lowers apparent air temp' }, - forest: { shortLabel: 'Forest', label: 'Forest / Woodland', dirFactor: 0.15, difFactor: 0.40, globFactor: 0.20, taOffset: -2.0, note: 'Canopy-shaded estimate - direct radiation reduced 85%, air temp adjusted for evapotranspiration cooling' }, - openwater: { shortLabel: 'Open Water', label: 'Open Water', dirFactor: 1.14, difFactor: 1.08, globFactor: 1.12, taOffset: 0.0, note: 'Open water - full solar exposure with additional radiation reflected from the water surface, no shade or shelter, no ground heat contribution' }, - alpine: { shortLabel: 'Alpine', label: 'High Altitude / Alpine', dirFactor: 1.15, difFactor: 1.10, globFactor: 1.12, taOffset: -1.0, note: 'Alpine estimate assuming approx. 2000 m elevation - radiation enhanced ~15% through thinner atmosphere, adjusted for exposed ridge conditions' }, - desert: { shortLabel: 'Desert', label: 'Desert / Arid', dirFactor: 1.22, difFactor: 1.10, globFactor: 1.18, taOffset: 3.5, note: 'Extreme radiation load from high albedo sand and bare ground - ground-level heat significantly raises effective felt temperature' }, + open: { shortLabel: null, label: 'Grass / Open Field', dirFactor: 1.00, difFactor: 1.00, globFactor: 1.00, taOffset: 0.0, note: null, shade: { offset: 0.5, shelter: 0.45, sun: 0.50, type: 'hedge / scattered trees' } }, + urban: { shortLabel: 'City', label: 'Town / City', dirFactor: 0.70, difFactor: 0.75, globFactor: 0.82, taOffset: 2.5, note: 'Urban heat island estimate - UHI raises air temp ~2–3 °C, building canyons reduce direct and diffuse solar load, reflections from concrete and glass partially offset sky-view reduction', shade: { offset: 1.5, shelter: 0.75, sun: 0.85, type: 'building shadow' } }, + beach: { shortLabel: 'Beach', label: 'Beach / Lakeside', dirFactor: 1.12, difFactor: 1.06, globFactor: 1.10, taOffset: 0.0, note: 'Reflected radiation from water and sand surfaces increases effective solar load - no shade, higher UV dose', shade: { offset: 0.5, shelter: 0.25, sun: 0.95, type: 'beach umbrella' } }, + river: { shortLabel: 'Riverbank', label: 'River / Riverbank', dirFactor: 0.50, difFactor: 0.60, globFactor: 0.55, taOffset: -2.5, note: 'Shaded riverbank estimate - canopy and valley walls reduce direct sun, evaporative cooling from water surface lowers apparent air temp', shade: { offset: -1.5, shelter: 0.55, sun: 0.40, type: 'trees & bushes' } }, + forest: { shortLabel: 'Forest', label: 'Forest / Woodland', dirFactor: 0.15, difFactor: 0.40, globFactor: 0.20, taOffset: -2.0, note: 'Canopy-shaded estimate - direct radiation reduced 85%, air temp adjusted for evapotranspiration cooling', shade: { offset: -1.0, shelter: 0.80, sun: 0.20, type: 'tree canopy' } }, + openwater: { shortLabel: 'Open Water', label: 'Open Water', dirFactor: 1.14, difFactor: 1.08, globFactor: 1.12, taOffset: 0.0, note: 'Open water - full solar exposure with additional radiation reflected from the water surface, no shade or shelter, no ground heat contribution', shade: { offset: 0.0, shelter: 0.10, sun: 0.25, type: 'boat panels' } }, + alpine: { shortLabel: 'Alpine', label: 'High Altitude / Alpine', dirFactor: 1.15, difFactor: 1.10, globFactor: 1.12, taOffset: -1.0, note: 'Alpine estimate assuming approx. 2000 m elevation - radiation enhanced ~15% through thinner atmosphere, adjusted for exposed ridge conditions', shade: { offset: -0.5, shelter: 0.35, sun: 0.40, type: 'trees & rocks' } }, + desert: { shortLabel: 'Desert', label: 'Desert / Arid', dirFactor: 1.22, difFactor: 1.10, globFactor: 1.18, taOffset: 3.5, note: 'Extreme radiation load from high albedo sand and bare ground - ground-level heat significantly raises effective felt temperature', shade: { offset: 2.5, shelter: 0.40, sun: 1.00, type: 'cactus / rockface' } }, }; // Maps OUTDOORS_VARIANTS keys to a default UTCI_ENVIRONMENTS key. @@ -203,6 +209,7 @@ export const workVariantKeys = ['farming', 'construction', 'market', 'windowclea export const COL_DESCRIPTIONS = { hour: { title: 'Hour', desc: 'Local wall-clock time for this forecast row. Each row covers one hour.' }, air: { title: 'Air Temperature', desc: 'Measured air temperature at 2 m above the ground. This is the standard thermometer reading — it does not account for sun, wind, or humidity.' }, + shadeT: { title: 'Shade Air', desc: 'Estimated air temperature sitting in the typical shade of the selected Solar Model — building shadow, beach umbrella, tree canopy, boat panels, and so on. Sheltered, sun-warmed surroundings push it above the official open-ground Air figure on calm sunny hours; wind and cloud pull it back toward it. This is an estimate, not a measurement, and differs from the standardised 2 m Air reading.' }, rh: { title: 'Relative Humidity', desc: 'How much moisture the air holds relative to its maximum capacity at that temperature. High RH makes warm days feel stickier and cold days feel rawer.' }, dew: { title: 'Dew Point', desc: 'The temperature at which air becomes saturated and moisture begins to condense. A useful measure of absolute humidity — above 16 °C it starts to feel muggy; above 21 °C it feels oppressive.' }, wind: { title: 'Wind Speed', desc: 'Mean wind speed at 10 m in mph, with peak gust in brackets where significantly higher. Gust colour indicates Beaufort scale severity: amber = Near Gale (32+ mph), orange = Gale (39+ mph), red = Severe Gale or above (55+ mph). Banners above the table classify conditions from Strong Breeze through to Violent Storm using standard wind categories. Wind dramatically increases heat loss from exposed skin — the basis of wind chill.' }, diff --git a/assets/js/export.js b/assets/js/export.js index 7ab3ae4..3aad3e1 100644 --- a/assets/js/export.js +++ b/assets/js/export.js @@ -53,8 +53,9 @@ const COLS = [ ['UTCI', '°C', 'right', 7, '0.0'], ['Δ UTCI−Air', '°C', 'right', 9, '0.0'], ['Tmrt', '°C', 'right', 7, '0.0'], - // Air (3) + // Air (4) ['Air Temp', '°C', 'right', 8, '0.0'], + ['Shade Air', '°C', 'right', 9, '0.0'], ['Dew Point', '°C', 'right', 9, '0.0'], ['Humidity', '%', 'right', 8, '0' ], // UV & Solar (7) @@ -95,13 +96,13 @@ const COLS = [ ['Ragweed', 'g/m³', 'right', 10, '0' ], ]; -const TOTAL_COLS = COLS.length; // 41 +const TOTAL_COLS = COLS.length; // 42 // Groups: [label, span, bgKey] const GROUPS = [ ['Time & Sun', 2, 'timeSun'], ['Felt Temperature', 5, 'felt' ], - ['Air', 3, 'air' ], + ['Air', 4, 'air' ], ['UV & Solar', 7, 'uvSolar'], ['Surfaces', 6, 'surf' ], ['Wind', 3, 'wind' ], @@ -213,54 +214,55 @@ function buildForecastSheet(X, rows, locationName, dateLabel, skinType) { setNum(X, ws, rowIdx, 6, r.Tmrt, { bg: tempBg(r.Tmrt), fg: P.espresso, sz: 10, numFmt: '0.0', borderHex: P.line }); // Air - setNum(X, ws, rowIdx, 7, r.Ta, { bg: tempBg(r.Ta), fg: P.espresso, sz: 10, numFmt: '0.0', borderHex: P.line }); - setNum(X, ws, rowIdx, 8, r.dew, { ...def(), numFmt: '0.0' }); - setNum(X, ws, rowIdx, 9, r.RH, { ...def(), numFmt: '0' }); + setNum(X, ws, rowIdx, 7, r.Ta, { bg: tempBg(r.Ta), fg: P.espresso, sz: 10, numFmt: '0.0', borderHex: P.line }); + setNum(X, ws, rowIdx, 8, r.shadeT, { bg: tempBg(r.shadeT), fg: P.espresso, sz: 10, numFmt: '0.0', borderHex: P.line }); + setNum(X, ws, rowIdx, 9, r.dew, { ...def(), numFmt: '0.0' }); + setNum(X, ws, rowIdx, 10, r.RH, { ...def(), numFmt: '0' }); // UV & Solar const uvFg = r.uv >= 8 ? P.alert : r.uv >= 3 ? P.warn : P.warmDark; - setNum(X, ws, rowIdx, 10, r.uv, { ...def(), fg: uvFg, numFmt: '0.0' }); - setNum(X, ws, rowIdx, 11, r.uvA, { ...def(), numFmt: '0.0' }); - setNum(X, ws, rowIdx, 12, r.uvB, { ...def(), numFmt: '0.00' }); + setNum(X, ws, rowIdx, 11, r.uv, { ...def(), fg: uvFg, numFmt: '0.0' }); + setNum(X, ws, rowIdx, 12, r.uvA, { ...def(), numFmt: '0.0' }); + setNum(X, ws, rowIdx, 13, r.uvB, { ...def(), numFmt: '0.00' }); const burnMins = sunburnMinutes(r.uv, skinType); const burnStr = r.uv > 0 ? burnLabel(burnMins) : '—'; const burnFg = r.uv > 0 ? (burnMins < 30 ? P.alert : P.warn) : P.muted; - setStr(X, ws, rowIdx, 13, burnStr, { ...def(), fg: burnFg, align: 'right' }); - setNum(X, ws, rowIdx, 14, r.dir, { ...def(), numFmt: '0' }); - setNum(X, ws, rowIdx, 15, r.dif, { ...def(), numFmt: '0' }); - setNum(X, ws, rowIdx, 16, r.glob, { ...def(), numFmt: '0' }); + setStr(X, ws, rowIdx, 14, burnStr, { ...def(), fg: burnFg, align: 'right' }); + setNum(X, ws, rowIdx, 15, r.dir, { ...def(), numFmt: '0' }); + setNum(X, ws, rowIdx, 16, r.dif, { ...def(), numFmt: '0' }); + setNum(X, ws, rowIdx, 17, r.glob, { ...def(), numFmt: '0' }); // Surfaces - setNum(X, ws, rowIdx, 17, r.vehicleT, { bg: tempBg(r.vehicleT), fg: P.espresso, sz: 10, numFmt: '0.0', borderHex: P.line }); - setNum(X, ws, rowIdx, 18, r.indoorT, { bg: tempBg(r.indoorT), fg: P.espresso, sz: 10, numFmt: '0.0', borderHex: P.line }); - setNum(X, ws, rowIdx, 19, r.concreteT, { bg: tempBg(r.concreteT), fg: P.espresso, sz: 10, numFmt: '0.0', borderHex: P.line }); - setNum(X, ws, rowIdx, 20, r.soilT0, { ...def(), numFmt: '0.0' }); - setNum(X, ws, rowIdx, 21, r.soilT6, { ...def(), numFmt: '0.0' }); - setNum(X, ws, rowIdx, 22, r.soilM != null ? Math.round(r.soilM * 100) : null, { ...def(), numFmt: '0' }); + setNum(X, ws, rowIdx, 18, r.vehicleT, { bg: tempBg(r.vehicleT), fg: P.espresso, sz: 10, numFmt: '0.0', borderHex: P.line }); + setNum(X, ws, rowIdx, 19, r.indoorT, { bg: tempBg(r.indoorT), fg: P.espresso, sz: 10, numFmt: '0.0', borderHex: P.line }); + setNum(X, ws, rowIdx, 20, r.concreteT, { bg: tempBg(r.concreteT), fg: P.espresso, sz: 10, numFmt: '0.0', borderHex: P.line }); + setNum(X, ws, rowIdx, 21, r.soilT0, { ...def(), numFmt: '0.0' }); + setNum(X, ws, rowIdx, 22, r.soilT6, { ...def(), numFmt: '0.0' }); + setNum(X, ws, rowIdx, 23, r.soilM != null ? Math.round(r.soilM * 100) : null, { ...def(), numFmt: '0' }); // Wind - setNum(X, ws, rowIdx, 23, r.va, { ...def(), numFmt: '0.0' }); - setNum(X, ws, rowIdx, 24, r.gust, { ...def(), numFmt: '0.0' }); - setStr(X, ws, rowIdx, 25, r.compass?.label ?? (r.wd != null ? `${Math.round(r.wd)}°` : '—'), { ...def(), align: 'center' }); + setNum(X, ws, rowIdx, 24, r.va, { ...def(), numFmt: '0.0' }); + setNum(X, ws, rowIdx, 25, r.gust, { ...def(), numFmt: '0.0' }); + setStr(X, ws, rowIdx, 26, r.compass?.label ?? (r.wd != null ? `${Math.round(r.wd)}°` : '—'), { ...def(), align: 'center' }); // Sky & Precip - setNum(X, ws, rowIdx, 26, r.cc, { ...def(), numFmt: '0' }); - setNum(X, ws, rowIdx, 27, r.ccLow, { ...def(), numFmt: '0' }); - setNum(X, ws, rowIdx, 28, r.ccMid, { ...def(), numFmt: '0' }); - setNum(X, ws, rowIdx, 29, r.ccHigh, { ...def(), numFmt: '0' }); - setNum(X, ws, rowIdx, 30, r.precip > 0 ? r.precip : null, { ...def(), fg: r.precip > 2 ? P.blue : P.warmDark, numFmt: '0.00' }); - setNum(X, ws, rowIdx, 31, r.precipProb, { ...def(), fg: r.precipProb >= 70 ? P.blue : P.warmDark, numFmt: '0' }); - setNum(X, ws, rowIdx, 32, r.snow > 0 ? r.snow : null, { ...def(), numFmt: '0.00' }); - setNum(X, ws, rowIdx, 33, r.visKm, { ...def(), numFmt: '0.0' }); + setNum(X, ws, rowIdx, 27, r.cc, { ...def(), numFmt: '0' }); + setNum(X, ws, rowIdx, 28, r.ccLow, { ...def(), numFmt: '0' }); + setNum(X, ws, rowIdx, 29, r.ccMid, { ...def(), numFmt: '0' }); + setNum(X, ws, rowIdx, 30, r.ccHigh, { ...def(), numFmt: '0' }); + setNum(X, ws, rowIdx, 31, r.precip > 0 ? r.precip : null, { ...def(), fg: r.precip > 2 ? P.blue : P.warmDark, numFmt: '0.00' }); + setNum(X, ws, rowIdx, 32, r.precipProb, { ...def(), fg: r.precipProb >= 70 ? P.blue : P.warmDark, numFmt: '0' }); + setNum(X, ws, rowIdx, 33, r.snow > 0 ? r.snow : null, { ...def(), numFmt: '0.00' }); + setNum(X, ws, rowIdx, 34, r.visKm, { ...def(), numFmt: '0.0' }); // Air Quality - setNum(X, ws, rowIdx, 34, r.aqi, { ...def(), fg: r.aqi >= 50 ? P.alert : P.warmDark, numFmt: '0' }); - setNum(X, ws, rowIdx, 35, r.grassPollen, { ...def(), numFmt: '0' }); - setNum(X, ws, rowIdx, 36, r.birchPollen, { ...def(), numFmt: '0' }); - setNum(X, ws, rowIdx, 37, r.alderPollen, { ...def(), numFmt: '0' }); - setNum(X, ws, rowIdx, 38, r.mugwortPollen, { ...def(), numFmt: '0' }); - setNum(X, ws, rowIdx, 39, r.olivePollen, { ...def(), numFmt: '0' }); - setNum(X, ws, rowIdx, 40, r.ragweedPollen, { ...def(), numFmt: '0' }); + setNum(X, ws, rowIdx, 35, r.aqi, { ...def(), fg: r.aqi >= 50 ? P.alert : P.warmDark, numFmt: '0' }); + setNum(X, ws, rowIdx, 36, r.grassPollen, { ...def(), numFmt: '0' }); + setNum(X, ws, rowIdx, 37, r.birchPollen, { ...def(), numFmt: '0' }); + setNum(X, ws, rowIdx, 38, r.alderPollen, { ...def(), numFmt: '0' }); + setNum(X, ws, rowIdx, 39, r.mugwortPollen, { ...def(), numFmt: '0' }); + setNum(X, ws, rowIdx, 40, r.olivePollen, { ...def(), numFmt: '0' }); + setNum(X, ws, rowIdx, 41, r.ragweedPollen, { ...def(), numFmt: '0' }); }); // ── Sheet metadata ──────────────────────────────────────────────────── diff --git a/assets/js/physics.js b/assets/js/physics.js index 9cb4fad..248c7aa 100644 --- a/assets/js/physics.js +++ b/assets/js/physics.js @@ -12,6 +12,7 @@ // calcIndoorTempPass(TaArr, globArr, elevArr, buildingType) passive indoor temp // calcManagedIndoorTempPass(TaArr, globArr, elevArr, buildingType) managed indoor temp // calcVehicleInteriorTemp(Ta, globalRad, solElev, vehicleType, ventilated, speedMph) +// calcShadeAirTemp(Ta, effectiveRad, va, elev, shade) per-env shade air temp // vaporPressureHpa(Ta, RH) Magnus formula - hPa // solarElevationDeg(lat, lon, dateUTC) NOAA simplified solar position (-) // calcTmrt(Ta, dirRad, diffRad, globalRad, solElev) Mean radiant temperature @@ -469,6 +470,53 @@ export function calcVehicleInteriorTemp(Ta, globalRad, solElev, vehicleType = 'c return Math.max(Ta, Math.min(Ti, 90)); } +// ------------------------------------------------------------------- +// SHADE AIR TEMPERATURE (per-environment microclimate) +// ------------------------------------------------------------------- +// Estimates the AIR temperature you'd actually experience sitting in +// the typical shade of the selected Solar Model - NOT the felt temp. +// +// The official Air column is the standardised open-ground, 2 m, +// shaded-screen value for a ~2 km model grid cell. A real sitting +// spot is a sheltered sun-trap: surrounding surfaces (walls, sand, +// rock) re-radiate heat into the still air pocket, while wind mixing +// pulls the pocket back toward the official reading. The kind of +// shelter and the heat of the surroundings differ by environment, +// which is what the per-env `shade` block captures. +// +// IMPORTANT: this is an air-temperature nudge, deliberately gentle. +// The radiant load of direct sun is already handled by Tmrt/SunSoak, +// so we do NOT re-add it here - that would double-count the sun. +// +// shade.offset - base air-temp delta for this environment's shade (-C). +// Warmer for urban/desert (heat island, baking ground), +// cooler for river/forest (evaporative, canopy shade). +// shade.shelter - 0-1 wind shelter. High = enclosed (canyon, canopy), +// so wind mixing has little effect. Low = breezy +// (open water, single beach umbrella). +// shade.sun - 0-~1.2 surface solar gain. How sun-baked the +// surroundings are: hot concrete/sand high, shaded +// forest floor / cool water low. Drives how much the +// still air warms on a sunny hour. +// +// Behaviour: a calm sunny hour in a sheltered, sun-baked environment +// reads a degree or two above official Air; a windy or overcast hour +// collapses back toward it. +// ------------------------------------------------------------------- +export const SHADE_K_SUN = 1.5; // surface-warming strength (per full-sun, fully sun-baked) +export const SHADE_K_WIND = 0.15; // wind-mixing strength (per m/s, fully exposed) + +export function calcShadeAirTemp(Ta, effectiveRad, va, elev, shade) { + if (Ta == null || !shade) return Ta ?? null; + const rad = Math.max(0, Math.min(1, (effectiveRad || 0) / 800)); // 0-1, saturates ~800 W/m- + const daytime = (elev != null && elev > 0) ? 1 : 0; + const sunTerm = SHADE_K_SUN * (shade.sun ?? 0) * rad * daytime; + const windTerm = SHADE_K_WIND * (1 - (shade.shelter ?? 0)) * Math.max(0, va || 0); + const adj = (shade.offset ?? 0) + sunTerm - windTerm; + // Keep the nudge sane - this is air temp, not a felt-temp delta. + return Ta + Math.max(-5, Math.min(6, adj)); +} + // -- FUTURE FEATURE v2: Pets Profile - Fur Temperature & Heat Stress ---------- // Dogs and cats experience heat very differently from humans: //