diff --git a/assets/js/app.js b/assets/js/app.js
index 39937e2..d5b9cb9 100644
--- a/assets/js/app.js
+++ b/assets/js/app.js
@@ -27,6 +27,7 @@ import {
utciCategory, UTCI_BANDS, bandGradient,
SKIN_TYPES, sunburnMinutes, burnLabel,
VEHICLE_TYPES, VEHICLE_SPEEDS, BUILDING_TYPES,
+ FUR_COLORS, pawBurnRiskLabel,
confidenceBand, moonGlyph, skyFillForElev,
} from './utils.js';
import { SkyScope, WindVane, CloudIcon, ScopeReticle, PrecipIcon, CustomSelect, VentPill } from './components.js';
@@ -199,6 +200,7 @@ export function UTCIForecast() {
vehicleSpeed, setVehicleSpeed,
outdoorsVariant, setOutdoorsVariantAndSave,
buildingType, setBuildingType,
+ furColor, setFurColor,
indoorManaged, setIndoorManaged,
indoorMode, setIndoorMode,
pollenType, setPollenTypeAndSave,
@@ -337,9 +339,9 @@ export function UTCIForecast() {
// Columns that mark the start of a logical group - used to draw a faint
// vertical border separating groups in the forecast table.
const GROUP_ORDER = {
- felt: ['utciP', 'vehicleT', 'indoorT', 'managedT', 'burn', 'utci', 'delta', 'tmrt'],
- surface: ['concreteT', 'soilT', 'soilT6', 'soilM'],
- ambient: ['shadeT', 'air', 'rh', 'dew'],
+ felt: ['utciP', 'vehicleT', 'indoorT', 'managedT', 'petHomeT', 'burn', 'utci', 'delta', 'tmrt'],
+ surface: ['concreteT', 'soilT', 'soilT6', 'soilM', 'furSurfaceT', 'pawT'],
+ ambient: ['shadeT', 'petShadeT', 'air', 'rh', 'dew'],
precip: ['precip', 'precipProb', 'lightning'],
sky: ['cloud', 'vis'],
wind: ['wind', 'dir'],
@@ -1093,6 +1095,30 @@ export function UTCIForecast() {
title="Managed: curtains closed by day, windows open when cooler outside"
/>`}
`}
+ ${(isPro ? (activeProfile === 'custom' || activeProfile === 'showall' || activeCols['furSurfaceT']) : activeCols['furSurfaceT']) && html`
+
+ <${CustomSelect}
+ value=${furColor}
+ isOn=${visibleCols.furSurfaceT}
+ grpClass="grp-surface"
+ hideLabel="Fur Colour"
+ hidingLabel="Hide Fur Colour"
+ groupedLeft=${true}
+ isLastChild=${true}
+ options=${Object.entries(FUR_COLORS).map(([k, v]) => ({ value: k, label: v.name }))}
+ onChange=${(v) => {
+ if (v === 'off') {
+ setVisibleCols(prev => ({ ...prev, furSurfaceT: false }));
+ } else {
+ setFurColor(v);
+ setVisibleCols(prev => ({ ...prev, furSurfaceT: true }));
+ }
+ }}
+ />
+ `}
+ ${(isPro ? (activeProfile === 'custom' || activeProfile === 'showall' || activeCols['pawT']) : activeCols['pawT']) && html``}
+ ${(isPro ? (activeProfile === 'custom' || activeProfile === 'showall' || activeCols['petShadeT']) : activeCols['petShadeT']) && html``}
+ ${(isPro ? (activeProfile === 'custom' || activeProfile === 'showall' || activeCols['petHomeT']) : activeCols['petHomeT']) && html``}
${(isPro ? (activeProfile === 'custom' || activeProfile === 'showall' || activeCols['burn']) : activeCols['burn']) && html`
<${CustomSelect}
value=${skinType}
@@ -1326,15 +1352,19 @@ export function UTCIForecast() {
${visibleCols.vehicleT && html`
handleThClick('vehicleT', e)} onMouseEnter=${(e) => handleThEnter('vehicleT', e)} onMouseLeave=${handleThLeave}>Vehicle °C peak | `}
${indoorMode === 'on' && !indoorManaged && html` handleThClick('indoorT', e)} onMouseEnter=${(e) => handleThEnter('indoorT', e)} onMouseLeave=${handleThLeave}>Indoors °C est. | `}
${indoorMode === 'on' && indoorManaged && html` handleThClick('managedT', e)} onMouseEnter=${(e) => handleThEnter('managedT', e)} onMouseLeave=${handleThLeave}>Managed °C est. | `}
+ ${visibleCols.petHomeT && html` handleThClick('petHomeT', e)} onMouseEnter=${(e) => handleThEnter('petHomeT', e)} onMouseLeave=${handleThLeave}>Pet Home °C est. | `}
${visibleCols.burn && html` handleThClick('burn', e)} onMouseEnter=${(e) => handleThEnter('burn', e)} onMouseLeave=${handleThLeave}>Burn to MED | `}
${visibleCols.utci && html` handleThClick('utci', e)} onMouseEnter=${(e) => handleThEnter('utci', e)} onMouseLeave=${handleThLeave}>UTCI °C felt | `}
${visibleCols.delta && html` handleThClick('delta', e)} onMouseEnter=${(e) => handleThEnter('delta', e)} onMouseLeave=${handleThLeave}>Δ UTCI−Air | `}
${visibleCols.tmrt && html` handleThClick('tmrt', e)} onMouseEnter=${(e) => handleThEnter('tmrt', e)} onMouseLeave=${handleThLeave}>Tmrt °C | `}
${visibleCols.concreteT && html` handleThClick('concreteT', e)} onMouseEnter=${(e) => handleThEnter('concreteT', e)} onMouseLeave=${handleThLeave}>Concrete °C surface | `}
+ ${visibleCols.furSurfaceT && html` handleThClick('furSurfaceT', e)} onMouseEnter=${(e) => handleThEnter('furSurfaceT', e)} onMouseLeave=${handleThLeave}>Fur °C surface | `}
+ ${visibleCols.pawT && html` handleThClick('pawT', e)} onMouseEnter=${(e) => handleThEnter('pawT', e)} onMouseLeave=${handleThLeave}>Paw °C surface | `}
${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.petShadeT && html` handleThClick('petShadeT', e)} onMouseEnter=${(e) => handleThEnter('petShadeT', e)} onMouseLeave=${handleThLeave}>Pet 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 | `}
@@ -1491,6 +1521,10 @@ export function UTCIForecast() {
${fmt(r.managedT)}${u('°C')}
| `}
+ ${visibleCols.petHomeT && html`
+
+ ${fmt(r.indoorT)}${u('°C')}
+ | `}
${visibleCols.burn && html`
0 ? (burnMins < 30 ? '#c44a3a' : '#c8601a') : '#4a3218', background: burnBg(burnMins, r.uv) }}>
${burnLabel(burnMins)}
@@ -1511,6 +1545,16 @@ export function UTCIForecast() {
|
${fmt(r.concreteT)}${u('°C')}
| `}
+ ${visibleCols.furSurfaceT && html`
+
+ ${fmt(r.furSurfaceT)}${u('°C')}
+ | `}
+ ${visibleCols.pawT && (() => {
+ const risk = pawBurnRiskLabel(r.concreteT);
+ return html`
+ ${fmt(r.concreteT)}${u('°C')}
+ | `;
+ })()}
${visibleCols.soilT && html`
${r.soilT0 != null ? fmt(r.soilT0) + u('°C') : '—'} | `}
${visibleCols.soilT6 && html`
@@ -1518,6 +1562,10 @@ export function UTCIForecast() {
${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.petShadeT && 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/compute.js b/assets/js/compute.js
index 10b9809..fd28ea6 100644
--- a/assets/js/compute.js
+++ b/assets/js/compute.js
@@ -2,7 +2,8 @@
// compute.js - Build the per-hour display rows from the raw API data.
//
// Pure-ish function: feed in (forecast, airQuality, location, vehicleType,
-// vehicleVent, vehicleSpeed, buildingType) and get back { hourlyRows, days, utcOffsetMs }.
+// vehicleVent, vehicleSpeed, buildingType, furColor) 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:
@@ -14,11 +15,12 @@ import {
vaporPressureHpa, solarElevationDeg, calcTmrt, utciApprox,
calcConcreteTempPass, calcVehicleInteriorTemp,
calcIndoorTempPass, calcManagedIndoorTempPass, calcShadeAirTemp,
+ calcFurSurfaceTempPass,
} from './physics.js';
-import { windCompass8, uvSplit, cloudCategory, precipPenalty, sunburnMinutes, burnLabel } from './utils.js';
+import { windCompass8, uvSplit, cloudCategory, precipPenalty, sunburnMinutes, burnLabel, FUR_COLORS } from './utils.js';
import { UTCI_ENVIRONMENTS, CROP_CALENDAR } from './config.js';
-export function buildHourlyRows({ forecast, airQuality, location, vehicleType, vehicleVent, vehicleSpeed, buildingType, utciEnv }) {
+export function buildHourlyRows({ forecast, airQuality, location, vehicleType, vehicleVent, vehicleSpeed, buildingType, furColor, utciEnv }) {
const env = UTCI_ENVIRONMENTS[utciEnv] ?? UTCI_ENVIRONMENTS.open;
const utcOffsetMs = (forecast?.utc_offset_seconds ?? 0) * 1000;
@@ -185,6 +187,11 @@ export function buildHourlyRows({ forecast, airQuality, location, vehicleType, v
const indoorTemps = calcIndoorTempPass(TaArr, globArr, elevArr, buildingType);
const managedTemps = calcManagedIndoorTempPass(TaArr, globArr, elevArr, buildingType);
hourlyRows.forEach((r, i) => { r.indoorT = indoorTemps[i]; r.managedT = managedTemps[i]; });
+
+ // Fur surface temperature with thermal lag - Pets profile.
+ const furAlbedo = (FUR_COLORS[furColor] || FUR_COLORS.brown).albedo;
+ const furTemps = calcFurSurfaceTempPass(TaArr, radArr, vaArr, elevArr, furAlbedo);
+ hourlyRows.forEach((r, i) => { r.furSurfaceT = furTemps[i]; });
}
// Group those hourly rows into days for the day tabs.
@@ -673,6 +680,56 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols
];
}
+ // ── Pets (cats / small dogs) ────────────────────────────────────────────
+ if (profile === 'pets') {
+ const peakFur = peakRow('furSurfaceT');
+ const peakShade = peakRow('shadeT');
+ const peakIndoor = peakRow('indoorT');
+ const maxPollen = Math.max(0, ...todayRows.map(r => r.grassPollen ?? 0));
+ const peakAqi = Math.max(0, ...todayRows.map(r => r.aqi ?? 0));
+ const carDanger = todayRows.find(r => r.vehicleT != null && r.vehicleT >= 29);
+
+ return [
+ ...(show('furSurfaceT') ? [{
+ icon: '🐾',
+ label: 'Sunsoak for pet',
+ value: peakFur ? `${Math.round(peakFur.furSurfaceT)}° at ${hhmm(peakFur.iso)}` : '-',
+ alert: !!(peakFur && peakFur.furSurfaceT >= 45),
+ }] : []),
+ ...(show('petShadeT') ? [{
+ icon: '🌳',
+ label: 'Shade for pet',
+ value: peakShade ? `${Math.round(peakShade.shadeT)}° at ${hhmm(peakShade.iso)}` : '-',
+ alert: !!(peakShade && peakShade.shadeT >= 26),
+ }] : []),
+ ...(show('petHomeT') ? [{
+ icon: '🏠',
+ label: 'Pet at home',
+ value: peakIndoor ? `${Math.round(peakIndoor.indoorT)}° at ${hhmm(peakIndoor.iso)}` : '-',
+ alert: !!(peakIndoor && peakIndoor.indoorT >= 26),
+ }] : []),
+ ...(show('vehicleT') ? [{
+ icon: '🧒',
+ label: 'Kids/pets in car',
+ value: carDanger ? `Unsafe from ${hhmm(carDanger.iso)}` : 'Safe all day',
+ alert: !!carDanger,
+ }] : []),
+ ...lightningItem,
+ ...(show('aqi') ? [{
+ icon: '💨',
+ label: 'Air quality',
+ value: peakAqi > 0 ? aqiLabel(peakAqi) : '-',
+ alert: peakAqi >= 60,
+ }] : []),
+ ...(show('pollen') && maxPollen >= 10 ? [{
+ icon: '🌼',
+ label: 'Pollen',
+ value: pollenLabel(maxPollen),
+ alert: maxPollen >= 50,
+ }] : []),
+ ];
+ }
+
// ── Activities - running / cycling ─────────────────────────────────────
if (variant === 'running' || variant === 'cycling') {
const coolWins = allWindows(dayRows, r =>
diff --git a/assets/js/config.js b/assets/js/config.js
index 0529fd8..fcf504a 100644
--- a/assets/js/config.js
+++ b/assets/js/config.js
@@ -29,46 +29,52 @@ 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, 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 },
+ 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, furSurfaceT: false, pawT: false, petShadeT: false, petHomeT: false },
},
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, 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 },
+ 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, furSurfaceT: false, pawT: false, petShadeT: false, petHomeT: false },
},
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, 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 },
+ 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, furSurfaceT: false, pawT: false, petShadeT: false, petHomeT: false },
+ },
+ pets: {
+ label: 'Pets',
+ icon: '🐾',
+ scene: "url('assets/images/profiles/pets.png') center / cover no-repeat, linear-gradient(160deg,#e0c0a0,#f0ddc8)",
+ 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: false, precip: true, precipProb: true, lightning: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: true, indoorT: false, managedT: false, vis: false, aqi: true, pollen: true, furSurfaceT: true, pawT: true, petShadeT: true, petHomeT: true },
},
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, 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 },
+ 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, furSurfaceT: false, pawT: false, petShadeT: false, petHomeT: false },
},
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, 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 },
+ 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, furSurfaceT: false, pawT: false, petShadeT: false, petHomeT: 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, 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 },
+ 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, furSurfaceT: false, pawT: false, petShadeT: false, petHomeT: 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, 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 },
+ 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, furSurfaceT: false, pawT: false, petShadeT: false, petHomeT: false },
},
custom: {
label: 'Custom',
@@ -76,39 +82,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, 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 },
+ 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, furSurfaceT: false, pawT: false, petShadeT: false, petHomeT: 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, 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 } },
+ 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, furSurfaceT: false, pawT: false, petShadeT: false, petHomeT: 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, furSurfaceT: false, pawT: false, petShadeT: false, petHomeT: 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 } },
+ 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, furSurfaceT: false, pawT: false, petShadeT: false, petHomeT: 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 } },
+ 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, furSurfaceT: false, pawT: false, petShadeT: false, petHomeT: 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, 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 } },
+ 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, furSurfaceT: false, pawT: false, petShadeT: false, petHomeT: 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, 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 } },
+ 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, furSurfaceT: false, pawT: false, petShadeT: false, petHomeT: 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, 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 } },
+ 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, furSurfaceT: false, pawT: false, petShadeT: false, petHomeT: 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, 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 } },
+ 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, furSurfaceT: false, pawT: false, petShadeT: false, petHomeT: 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, furSurfaceT: false, pawT: false, petShadeT: false, petHomeT: 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, furSurfaceT: false, pawT: false, petShadeT: false, petHomeT: 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, 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 } },
+ 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, furSurfaceT: false, pawT: false, petShadeT: false, petHomeT: 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, furSurfaceT: false, pawT: false, petShadeT: false, petHomeT: 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, furSurfaceT: false, pawT: false, petShadeT: false, petHomeT: 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, furSurfaceT: false, pawT: false, petShadeT: false, petHomeT: false } },
};
// Pollen type options shown in the pulldown selector.
@@ -170,7 +176,7 @@ export const VARIANT_DEFAULT_ENV = {
};
// Left-to-right order of the profile buttons in the top filter bar.
-export const profileButtonOrder = ['basic', 'home', 'vehicle', 'alltemps', 'showall', 'custom'];
+export const profileButtonOrder = ['basic', 'home', 'vehicle', 'pets', 'alltemps', 'showall', 'custom'];
// Emoji glyph per place/activity variant.
export const variantIcons = {
@@ -239,6 +245,10 @@ export const COL_DESCRIPTIONS = {
vis: { title: 'Visibility', desc: 'Horizontal visibility in kilometres, sourced from the CAMS air quality model. Values below 1 km indicate fog or very thick haze; below 10 km suggests mist, smoke, or significant pollution. Relevant for driving, flying, and photography.' },
aqi: { title: 'Air Quality Index', desc: 'European Air Quality Index (0–100+), combining PM2.5, PM10, ozone, nitrogen dioxide, and sulphur dioxide. 0–20 = Good; 20–40 = Fair; 40–60 = Moderate; 60–80 = Poor; 80–100 = Very Poor; 100+ = Extremely Poor. Sourced from Copernicus CAMS.' },
pollen: { title: 'Pollen', desc: 'Pollen concentration in grains per cubic metre for the selected pollen type, sourced from the Copernicus CAMS pollen forecast. Low = <10; Moderate = 10–50; High = 50–200; Very High = 200+. Values vary by species and season.' },
+ furSurfaceT: { title: 'Fur Temp', desc: 'Estimated fur surface temperature a cat or small dog experiences in direct sun, calibrated for a thin/short coat rather than a heavily-insulated breed. Runs hotter than air temperature on sunny, still days — set fur colour with the selector alongside this column.' },
+ pawT: { title: 'Paw Temp', desc: 'Estimated ground/pavement surface temperature for paw contact, colour-coded by burn risk: green below 40 °C (safe), amber 40–52 °C (discomfort / possible burn on prolonged contact), red above 52 °C (can burn within 60 seconds). If you can\'t hold the back of your hand on the surface for 7 seconds, it\'s too hot to walk your pet.' },
+ petShadeT: { title: 'Pet Shade', desc: 'Shaded air temperature for a resting pet, using the same shade estimate as the Shade column but with a lower, pet-specific alert threshold — small pets have far less capacity to dump heat than humans.' },
+ petHomeT: { title: 'Pet Home', desc: 'Indoor temperature estimate for a pet left home alone, using the same model as the Indoors column but with a lower, pet-specific alert threshold (26 °C) rather than the human heatwave guidance threshold.' },
};
// Seasonal crop calendar for the Farming "At a glance" sow/harvest advice.
diff --git a/assets/js/hooks/useAppState.js b/assets/js/hooks/useAppState.js
index 72f22c1..8c7a525 100644
--- a/assets/js/hooks/useAppState.js
+++ b/assets/js/hooks/useAppState.js
@@ -280,6 +280,13 @@ export function useAppState() {
try { localStorage.setItem('sunscope_building_type', v); } catch (e) { /* ignore */ }
setBuildingType(v);
};
+ const [furColor, setFurColor] = useState(() => {
+ try { return localStorage.getItem('sunscope_fur_color') || 'brown'; } catch (e) { return 'brown'; }
+ });
+ const setFurColorAndSave = (v) => {
+ try { localStorage.setItem('sunscope_fur_color', v); } catch (e) { /* ignore */ }
+ setFurColor(v);
+ };
const [indoorManaged, setIndoorManaged] = useState(() => {
try { return localStorage.getItem('sunscope_indoor_managed') === '1'; } catch (e) { return false; }
});
@@ -500,7 +507,7 @@ export function useAppState() {
const { hourlyRows, days, utcOffsetMs } = buildHourlyRows({
forecast, airQuality, location, vehicleType, vehicleVent,
vehicleSpeed: (VEHICLE_SPEEDS[vehicleSpeed] ?? VEHICLE_SPEEDS.static).mph,
- buildingType, utciEnv,
+ buildingType, furColor, utciEnv,
});
const visible = days[selectedDay]?.rows || [];
@@ -645,6 +652,7 @@ export function useAppState() {
vehicleSpeed, setVehicleSpeed: setVehicleSpeedAndSave,
outdoorsVariant, setOutdoorsVariantAndSave,
buildingType, setBuildingType: setBuildingTypeAndSave,
+ furColor, setFurColor: setFurColorAndSave,
indoorManaged, setIndoorManaged: setIndoorManagedAndSave,
indoorMode, setIndoorMode: setIndoorModeAndSave,
pollenType, setPollenTypeAndSave,
diff --git a/assets/js/physics.js b/assets/js/physics.js
index cc2e429..edb907e 100644
--- a/assets/js/physics.js
+++ b/assets/js/physics.js
@@ -526,40 +526,51 @@ export function calcShadeAirTemp(Ta, effectiveRad, va, elev, shade) {
return Ta + Math.max(-4, Math.min(5, adj));
}
-// -- FUTURE FEATURE v2: Pets Profile - Fur Temperature & Heat Stress ----------
-// Dogs and cats experience heat very differently from humans:
-//
-// FUR SURFACE TEMPERATURE
-// Dark/thick fur absorbs solar radiation and can run significantly hotter
-// than air temperature - similar in principle to calcConcreteTemp() but
-// with fur-specific albedo values:
-// Black fur: albedo ~0.05 (almost all radiation absorbed)
-// Brown fur: albedo ~0.15
-// Golden fur: albedo ~0.25
-// White fur: albedo ~0.40
-// Add a calcFurSurfaceTemp(Ta, globalRad, windSpeed, furAlbedo) function
-// mirroring calcConcreteTemp but with appropriate convective coefficients
-// for fur (lower hc than concrete - fur insulates).
-//
-// PAW BURN RISK
-// Paw pads are highly sensitive to surface temperature. Use calcConcreteTemp()
-// output directly - the existing concrete surface temp is already the right
-// number. Threshold guidance:
-// < 40 -C - safe
-// 40-52 -C - discomfort / possible burn (hold-your-hand-for-7-seconds test)
-// > 52 -C - burns within 60 seconds
-// Display as a simple traffic-light column in the Pets profile.
-//
-// BREED-SPECIFIC HEAT STRESS
-// Brachycephalic breeds (Bulldogs, Pugs, French Bulldogs, Persians) have
-// severely impaired thermoregulation - their safe UTCI ceiling is much lower.
-// A breed selector (Normal / Brachycephalic / Senior) applies a risk multiplier
-// to the UTCI thresholds, similar to the planned activity modifier.
-//
-// SUGGESTED COLUMNS FOR PETS PROFILE
-// Hour | Air Temp | UTCI+P | Fur Surface Temp | Paw Burn Risk | Shade Advised
-//
-// -----------------------------------------------------------------------------
+// -- Pets Profile - Fur Surface Temperature -----------------------------------
+// Cats and small dogs experience solar heat differently from bare pavement:
+// a coat absorbs radiation like calcConcreteTemp()'s slab, but a thin/short
+// coat over a living, blood-perfused body insulates less than a heavy double
+// coat and never bakes as hot as dead concrete. furAlbedo varies by coat
+// colour (FUR_COLORS in utils.js): black ~0.05, brown ~0.15, golden ~0.25,
+// white ~0.40. Paw burn risk reuses calcConcreteTemp() output directly (the
+// ground/pavement surface temp is already the right number for paw contact):
+// < 40 -C - safe
+// 40-52 -C - discomfort / possible burn (hold-your-hand-for-7-seconds test)
+// > 52 -C - burns within 60 seconds
+// A breed-specific multiplier (brachycephalic/senior) is deliberately not
+// modelled - this profile targets cats and small dogs as the reference animal.
+// -------------------------------------------------------------------------
+export function calcFurSurfaceTemp(Ta, globalRad, windSpeed, solElev, furAlbedo = 0.15) {
+ if (globalRad == null || Ta == null) return null;
+ const elevDeg = (solElev != null) ? Math.max(5, solElev) : 45;
+ const angleCorrection = Math.sin(elevDeg * Math.PI / 180);
+ const absorbed = globalRad * (1 - furAlbedo) * angleCorrection;
+ // Lower convective coefficient than bare concrete (hc=10+4.5*sqrt(v)) -
+ // trapped air in the coat insulates, running the surface hotter for a
+ // given wind speed than pavement, but not as insulated as a thick double
+ // coat, so it sits between "bare skin" and "heavy fur".
+ const hc = 7 + 3.5 * Math.sqrt(Math.max(windSpeed || 0, 0));
+ const Ts = Ta + absorbed / hc;
+ // Floor at air temp; cap below concrete's 85-C ceiling - a thin coat over
+ // a living body doesn't bake as hot as dead pavement. No wet-fur/rain
+ // evaporative term yet (future refinement, mirrors calcConcreteTemp's).
+ return Math.max(Ta, Math.min(Ts, 70));
+}
+
+export const LAG_HOURS_FUR = 0.4; // ~24 min - low thermal mass, animal moves in/out of sun
+
+export function calcFurSurfaceTempPass(TaArr, radArr, vaArr, elevArr, furAlbedo = 0.15) {
+ const n = TaArr.length;
+ const result = new Array(n);
+ const alpha = 1 - Math.exp(-1 / LAG_HOURS_FUR);
+ let Tf = calcFurSurfaceTemp(TaArr[0], radArr[0], vaArr[0], elevArr[0], furAlbedo) ?? TaArr[0];
+ for (let i = 0; i < n; i++) {
+ const target = calcFurSurfaceTemp(TaArr[i], radArr[i], vaArr[i], elevArr[i], furAlbedo) ?? TaArr[i];
+ Tf = Tf + alpha * (target - Tf);
+ result[i] = Math.max(TaArr[i], Tf);
+ }
+ return result;
+}
// -- Vehicle-at-speed thermal model (IMPLEMENTED) ------------------------------
// calcVehicleInteriorTemp now takes a speedMph argument (Static / 20 / 50 / 70
diff --git a/assets/js/utils.js b/assets/js/utils.js
index 0bc84c0..18c20f7 100644
--- a/assets/js/utils.js
+++ b/assets/js/utils.js
@@ -14,6 +14,8 @@
// burnLabel(mins) formats burn time as "12m" / "1.5h"
// VEHICLE_TYPES vehicle presets for cabin heat model
// BUILDING_TYPES building presets for indoor heat model
+// FUR_COLORS fur presets for the fur surface temp model
+// pawBurnRiskLabel(concreteT) - 'Safe'|'Caution'|'Danger' paw contact risk
// cloudCategory(total,low,mid,high) - 'clear'|'wispy'|'scattered'|'overcast'
// confidenceBand(i) day-tab gradient colour + label
// moonPhaseFraction(date) 0..1 synodic phase fraction
@@ -232,6 +234,32 @@ export const BUILDING_TYPES = {
office: { name: 'Office Block', uWall: 0.25, lagHours: 2, glazingRatio: 0.45, gValue: 0.55, orientFactor: 0.65, curtainBlock: 0.50, ventAlpha: 0.15, solarScale: 0.055, baseTemp: 18.5, internalGain: 2.5, retainedScale: 0.30 },
};
+// -------------------------------------------------------------------
+// FUR COLORS - presets for the fur surface temperature model (Pets profile).
+// -------------------------------------------------------------------
+// albedo - how much solar radiation the coat reflects (0 = absorbs almost
+// all of it, 1 = reflects almost all of it). Feeds calcFurSurfaceTemp.
+// -------------------------------------------------------------------
+export const FUR_COLORS = {
+ black: { name: 'Black / Dark', albedo: 0.05 },
+ brown: { name: 'Brown / Tabby', albedo: 0.15 },
+ golden: { name: 'Golden / Tan', albedo: 0.25 },
+ white: { name: 'White / Pale', albedo: 0.40 },
+};
+
+// -------------------------------------------------------------------
+// PAW BURN RISK - traffic-light label from ground/pavement surface temp.
+// -------------------------------------------------------------------
+// Uses concreteT directly - the "7-second hand test" pavement-burn
+// guidance already documented on calcConcreteTemp in physics.js.
+// -------------------------------------------------------------------
+export function pawBurnRiskLabel(concreteT) {
+ if (concreteT == null) return null;
+ if (concreteT < 40) return 'Safe';
+ if (concreteT < 52) return 'Caution';
+ return 'Danger';
+}
+
// -------------------------------------------------------------------
// CLOUD CATEGORY - pick one of 4 icon styles from low/mid/high split.
// -------------------------------------------------------------------