3.1.0.0
Vehicle Speed Calcs Extra At A Glance ranges New Driver profile
This commit is contained in:
@@ -1214,6 +1214,10 @@
|
|||||||
.col-toggle-group--expanded {
|
.col-toggle-group--expanded {
|
||||||
flex: 0 0 calc((100% - 12px) / 3 * 2 + 6px);
|
flex: 0 0 calc((100% - 12px) / 3 * 2 + 6px);
|
||||||
}
|
}
|
||||||
|
/* Vehicle group fuses three children (select + speed + vent) — full row. */
|
||||||
|
.col-toggle-group--expanded.col-toggle-group--triple {
|
||||||
|
flex: 0 0 100%;
|
||||||
|
}
|
||||||
.col-toggle-group .cs-wrap {
|
.col-toggle-group .cs-wrap {
|
||||||
flex: 1 1 0;
|
flex: 1 1 0;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|||||||
@@ -1682,6 +1682,7 @@
|
|||||||
color: #4a3420;
|
color: #4a3420;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
white-space: pre-line; /* honour \n so multi-range windows stack one per line */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Alert state - draws attention without being aggressive */
|
/* Alert state - draws attention without being aggressive */
|
||||||
|
|||||||
+33
-8
@@ -26,7 +26,7 @@ import htm from '../vendor/htm.js';
|
|||||||
import {
|
import {
|
||||||
utciCategory, UTCI_BANDS, bandGradient,
|
utciCategory, UTCI_BANDS, bandGradient,
|
||||||
SKIN_TYPES, sunburnMinutes, burnLabel,
|
SKIN_TYPES, sunburnMinutes, burnLabel,
|
||||||
VEHICLE_TYPES, BUILDING_TYPES,
|
VEHICLE_TYPES, VEHICLE_SPEEDS, BUILDING_TYPES,
|
||||||
confidenceBand, moonGlyph, skyFillForElev,
|
confidenceBand, moonGlyph, skyFillForElev,
|
||||||
} from './utils.js';
|
} from './utils.js';
|
||||||
import { SkyScope, WindVane, CloudIcon, ScopeReticle, PrecipIcon, CustomSelect, VentPill } from './components.js';
|
import { SkyScope, WindVane, CloudIcon, ScopeReticle, PrecipIcon, CustomSelect, VentPill } from './components.js';
|
||||||
@@ -190,6 +190,7 @@ export function UTCIForecast() {
|
|||||||
skinType, setSkinType,
|
skinType, setSkinType,
|
||||||
vehicleType, setVehicleType,
|
vehicleType, setVehicleType,
|
||||||
vehicleVent, setVehicleVent,
|
vehicleVent, setVehicleVent,
|
||||||
|
vehicleSpeed, setVehicleSpeed,
|
||||||
outdoorsVariant, setOutdoorsVariantAndSave,
|
outdoorsVariant, setOutdoorsVariantAndSave,
|
||||||
buildingType, setBuildingType,
|
buildingType, setBuildingType,
|
||||||
indoorManaged, setIndoorManaged,
|
indoorManaged, setIndoorManaged,
|
||||||
@@ -451,6 +452,7 @@ export function UTCIForecast() {
|
|||||||
outdoorsVariant,
|
outdoorsVariant,
|
||||||
skinType,
|
skinType,
|
||||||
visibleCols,
|
visibleCols,
|
||||||
|
vehicleSpeed,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Human-readable date for the "Day at a glance" heading, e.g. "Mon 2nd June 2026".
|
// Human-readable date for the "Day at a glance" heading, e.g. "Mon 2nd June 2026".
|
||||||
@@ -875,7 +877,7 @@ export function UTCIForecast() {
|
|||||||
/>
|
/>
|
||||||
</span>`}
|
</span>`}
|
||||||
${showVehicle && html`
|
${showVehicle && html`
|
||||||
<span class=${'col-toggle-group' + (vehicleOn ? ' col-toggle-group--expanded' : '')}>
|
<span class=${'col-toggle-group' + (vehicleOn ? ' col-toggle-group--expanded col-toggle-group--triple' : '')}>
|
||||||
<${CustomSelect}
|
<${CustomSelect}
|
||||||
value=${vehicleType} isOn=${vehicleOn} grpClass="grp-felt"
|
value=${vehicleType} isOn=${vehicleOn} grpClass="grp-felt"
|
||||||
hideLabel="Vehicle" hidingLabel="Vehicle"
|
hideLabel="Vehicle" hidingLabel="Vehicle"
|
||||||
@@ -887,9 +889,16 @@ export function UTCIForecast() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
${vehicleOn && html`
|
${vehicleOn && html`
|
||||||
|
<${CustomSelect}
|
||||||
|
value=${vehicleSpeed} isOn=${true} noHide=${true} grpClass="grp-felt"
|
||||||
|
buttonLabel=${(VEHICLE_SPEEDS[vehicleSpeed] ?? VEHICLE_SPEEDS.static).name}
|
||||||
|
groupedLeft=${true} isLastChild=${false}
|
||||||
|
options=${Object.entries(VEHICLE_SPEEDS).map(([k, v]) => ({ value: k, label: v.name }))}
|
||||||
|
onChange=${v => setVehicleSpeed(v)}
|
||||||
|
/>
|
||||||
<${VentPill} checked=${vehicleVent} onChange=${() => setVehicleVent(v => !v)}
|
<${VentPill} checked=${vehicleVent} onChange=${() => setVehicleVent(v => !v)}
|
||||||
grpClass="grp-felt" label="Ventilation"
|
grpClass="grp-felt" label="Ventilation"
|
||||||
title="Open windows significantly reduce cabin heat build-up" />`}
|
title="Open windows significantly reduce cabin heat build-up. Speed sets cabin airflow from forward motion." />`}
|
||||||
</span>`}
|
</span>`}
|
||||||
${showIndoor && html`
|
${showIndoor && html`
|
||||||
<span class=${'col-toggle-group' + (indoorOn ? ' col-toggle-group--expanded' : '')}>
|
<span class=${'col-toggle-group' + (indoorOn ? ' col-toggle-group--expanded' : '')}>
|
||||||
@@ -941,7 +950,7 @@ export function UTCIForecast() {
|
|||||||
/>
|
/>
|
||||||
</span>`}
|
</span>`}
|
||||||
${(isPro ? (activeProfile === 'custom' || activeProfile === 'showall' || activeCols['vehicleT']) : activeCols['vehicleT']) && html`
|
${(isPro ? (activeProfile === 'custom' || activeProfile === 'showall' || activeCols['vehicleT']) : activeCols['vehicleT']) && html`
|
||||||
<span class=${'col-toggle-group' + (visibleCols.vehicleT ? ' col-toggle-group--expanded' : '')}>
|
<span class=${'col-toggle-group' + (visibleCols.vehicleT ? ' col-toggle-group--expanded col-toggle-group--triple' : '')}>
|
||||||
<${CustomSelect}
|
<${CustomSelect}
|
||||||
value=${vehicleType}
|
value=${vehicleType}
|
||||||
isOn=${visibleCols.vehicleT}
|
isOn=${visibleCols.vehicleT}
|
||||||
@@ -965,12 +974,23 @@ export function UTCIForecast() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
${visibleCols.vehicleT && html`
|
${visibleCols.vehicleT && html`
|
||||||
|
<${CustomSelect}
|
||||||
|
value=${vehicleSpeed}
|
||||||
|
isOn=${true}
|
||||||
|
noHide=${true}
|
||||||
|
grpClass="grp-felt"
|
||||||
|
buttonLabel=${(VEHICLE_SPEEDS[vehicleSpeed] ?? VEHICLE_SPEEDS.static).name}
|
||||||
|
groupedLeft=${true}
|
||||||
|
isLastChild=${false}
|
||||||
|
options=${Object.entries(VEHICLE_SPEEDS).map(([k, v]) => ({ value: k, label: v.name }))}
|
||||||
|
onChange=${(v) => setVehicleSpeed(v)}
|
||||||
|
/>
|
||||||
<${VentPill}
|
<${VentPill}
|
||||||
checked=${vehicleVent}
|
checked=${vehicleVent}
|
||||||
onChange=${() => setVehicleVent(v => !v)}
|
onChange=${() => setVehicleVent(v => !v)}
|
||||||
grpClass="grp-felt"
|
grpClass="grp-felt"
|
||||||
label="Ventilation"
|
label="Ventilation"
|
||||||
title="Ventilation — open windows significantly reduce cabin heat build-up"
|
title="Ventilation — open windows significantly reduce cabin heat build-up. Speed sets cabin airflow from forward motion."
|
||||||
/>`}
|
/>`}
|
||||||
</span>`}
|
</span>`}
|
||||||
${(isPro ? (activeProfile === 'custom' || activeProfile === 'showall' || activeCols['indoorT'] || activeCols['managedT']) : (activeCols['indoorT'] || activeCols['managedT'])) && html`
|
${(isPro ? (activeProfile === 'custom' || activeProfile === 'showall' || activeCols['indoorT'] || activeCols['managedT']) : (activeCols['indoorT'] || activeCols['managedT'])) && html`
|
||||||
@@ -1562,15 +1582,20 @@ export function UTCIForecast() {
|
|||||||
<span class="insight-label">${label}</span>
|
<span class="insight-label">${label}</span>
|
||||||
<span class="insight-value">${value}</span>
|
<span class="insight-value">${value}</span>
|
||||||
</div>`;
|
</div>`;
|
||||||
const comfortItem = glanceSummary.find(i => i.label === 'Comfortable window');
|
// Only the outdoors profile reorders items around the heat block
|
||||||
const restSummary = glanceSummary.filter(i => i.label !== 'Comfortable window');
|
// (it has a 'Peak felt temp' anchor). Other profiles render in order.
|
||||||
|
const hasFeltAnchor = glanceSummary.some(i => i.label === 'Peak felt temp');
|
||||||
|
const comfortItem = hasFeltAnchor ? glanceSummary.find(i => i.label === 'Comfortable window') : null;
|
||||||
|
const drivingItem = hasFeltAnchor ? glanceSummary.find(i => i.label === 'Good driving time') : null;
|
||||||
|
const restSummary = glanceSummary.filter(i => !(hasFeltAnchor && (i.label === 'Comfortable window' || i.label === 'Good driving time')));
|
||||||
return [
|
return [
|
||||||
...restSummary.flatMap(item => [
|
...restSummary.flatMap(item => [
|
||||||
renderRow(item),
|
renderRow(item),
|
||||||
// After the peak/heat block, drop in the comfortable window
|
// After the peak/heat block, drop in good driving time then the comfortable window
|
||||||
...(item.label === 'Peak felt temp'
|
...(item.label === 'Peak felt temp'
|
||||||
? [
|
? [
|
||||||
...heatEvents.map(e => renderRow(e, 'ev-')),
|
...heatEvents.map(e => renderRow(e, 'ev-')),
|
||||||
|
...(drivingItem ? [renderRow(drivingItem)] : []),
|
||||||
...(comfortItem ? [renderRow(comfortItem)] : []),
|
...(comfortItem ? [renderRow(comfortItem)] : []),
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
|
|||||||
+67
-30
@@ -2,7 +2,7 @@
|
|||||||
// compute.js - Build the per-hour display rows from the raw API data.
|
// compute.js - Build the per-hour display rows from the raw API data.
|
||||||
//
|
//
|
||||||
// Pure-ish function: feed in (forecast, airQuality, location, vehicleType,
|
// Pure-ish function: feed in (forecast, airQuality, location, vehicleType,
|
||||||
// vehicleVent, buildingType) and get back { hourlyRows, days, utcOffsetMs }.
|
// vehicleVent, vehicleSpeed, buildingType) and get back { hourlyRows, days, utcOffsetMs }.
|
||||||
//
|
//
|
||||||
// Open-Meteo with timezone=auto returns local wall-clock strings like
|
// 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:
|
// "2026-05-13T14:00" - no Z suffix. Two forms are used in each row:
|
||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
import { windCompass8, uvSplit, cloudCategory, precipPenalty, sunburnMinutes, burnLabel } from './utils.js';
|
import { windCompass8, uvSplit, cloudCategory, precipPenalty, sunburnMinutes, burnLabel } from './utils.js';
|
||||||
import { UTCI_ENVIRONMENTS } from './config.js';
|
import { UTCI_ENVIRONMENTS } from './config.js';
|
||||||
|
|
||||||
export function buildHourlyRows({ forecast, airQuality, location, vehicleType, vehicleVent, buildingType, utciEnv }) {
|
export function buildHourlyRows({ forecast, airQuality, location, vehicleType, vehicleVent, vehicleSpeed, buildingType, utciEnv }) {
|
||||||
const env = UTCI_ENVIRONMENTS[utciEnv] ?? UTCI_ENVIRONMENTS.open;
|
const env = UTCI_ENVIRONMENTS[utciEnv] ?? UTCI_ENVIRONMENTS.open;
|
||||||
const utcOffsetMs = (forecast?.utc_offset_seconds ?? 0) * 1000;
|
const utcOffsetMs = (forecast?.utc_offset_seconds ?? 0) * 1000;
|
||||||
|
|
||||||
@@ -77,7 +77,7 @@ export function buildHourlyRows({ forecast, airQuality, location, vehicleType, v
|
|||||||
// a surface as direct beam, so we weight it accordingly.
|
// a surface as direct beam, so we weight it accordingly.
|
||||||
const effectiveRad = dir + dif * 0.2;
|
const effectiveRad = dir + dif * 0.2;
|
||||||
// concreteT is now stamped in the two-pass section below (thermal lag).
|
// concreteT is now stamped in the two-pass section below (thermal lag).
|
||||||
const vehicleT = calcVehicleInteriorTemp(Ta, glob, elev, vehicleType, vehicleVent);
|
const vehicleT = calcVehicleInteriorTemp(Ta, glob, elev, vehicleType, vehicleVent, vehicleSpeed);
|
||||||
const eh = vaporPressureHpa(Ta, RH);
|
const eh = vaporPressureHpa(Ta, RH);
|
||||||
// Apply environment modifier - adjust solar inputs and air temp for shaded environments.
|
// Apply environment modifier - adjust solar inputs and air temp for shaded environments.
|
||||||
const TaEnv = Ta + env.taOffset;
|
const TaEnv = Ta + env.taOffset;
|
||||||
@@ -377,7 +377,7 @@ export function computeWhyFeelsLike(row, env) {
|
|||||||
// variant - active sub-variant key e.g. "running", "beach" (or null)
|
// variant - active sub-variant key e.g. "running", "beach" (or null)
|
||||||
// skinType - Fitzpatrick skin type key for UV burn time
|
// skinType - Fitzpatrick skin type key for UV burn time
|
||||||
// ------------------------------------------------------------------------
|
// ------------------------------------------------------------------------
|
||||||
export function computeGlanceSummary(todayRows, profile, variant, skinType, cols = null) {
|
export function computeGlanceSummary(todayRows, profile, variant, skinType, cols = null, vehicleSpeed = 'static') {
|
||||||
if (!todayRows || todayRows.length === 0) return [];
|
if (!todayRows || todayRows.length === 0) return [];
|
||||||
const show = (key) => !cols || !!cols[key];
|
const show = (key) => !cols || !!cols[key];
|
||||||
|
|
||||||
@@ -406,18 +406,35 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols
|
|||||||
(r.precipProb != null && (best == null || r.precipProb > best.precipProb)) ? r : best, null);
|
(r.precipProb != null && (best == null || r.precipProb > best.precipProb)) ? r : best, null);
|
||||||
const maxRainProb = peakRainRow ? (peakRainRow.precipProb ?? 0) : 0;
|
const maxRainProb = peakRainRow ? (peakRainRow.precipProb ?? 0) : 0;
|
||||||
|
|
||||||
// Longest run of rows that satisfy a predicate.
|
// All contiguous runs of rows that satisfy a predicate. Each run is
|
||||||
const longestWindow = (rows, cond) => {
|
// { start, end, len }. Used so an "at a glance" window can report more than
|
||||||
let best = null, cur = null;
|
// one range - e.g. good driving in the morning AND again in the evening.
|
||||||
|
const allWindows = (rows, cond) => {
|
||||||
|
const out = [];
|
||||||
|
let cur = null;
|
||||||
for (const r of rows) {
|
for (const r of rows) {
|
||||||
if (cond(r)) {
|
if (cond(r)) {
|
||||||
cur = cur ? { start: cur.start, end: r, len: cur.len + 1 } : { start: r, end: r, len: 1 };
|
cur = cur ? { start: cur.start, end: r, len: cur.len + 1 } : { start: r, end: r, len: 1 };
|
||||||
if (!best || cur.len > best.len) best = { ...cur };
|
} else if (cur) {
|
||||||
} else {
|
out.push(cur); cur = null;
|
||||||
cur = null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return best;
|
if (cur) out.push(cur);
|
||||||
|
return out;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Format the most significant windows as "h – h", one range per line, in
|
||||||
|
// chronological order. Caps at `max` ranges (longest kept) so a split day
|
||||||
|
// reads cleanly. Returns null when there are no windows. The newline is
|
||||||
|
// preserved by `white-space: pre-line` on .insight-value.
|
||||||
|
const formatWindows = (wins, max = 2) => {
|
||||||
|
if (!wins || wins.length === 0) return null;
|
||||||
|
return [...wins]
|
||||||
|
.sort((a, b) => b.len - a.len)
|
||||||
|
.slice(0, max)
|
||||||
|
.sort((a, b) => (a.start.iso < b.start.iso ? -1 : 1))
|
||||||
|
.map(w => `${hhmm(w.start.iso)} – ${hhmmEnd(w.end.iso)}`)
|
||||||
|
.join('\n');
|
||||||
};
|
};
|
||||||
|
|
||||||
const pollenLabel = (v) => {
|
const pollenLabel = (v) => {
|
||||||
@@ -470,9 +487,35 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols
|
|||||||
alert: maxRainProb >= 60,
|
alert: maxRainProb >= 60,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ── Good Driving Time ──────────────────────────────────────────────────
|
||||||
|
// Shown whenever a road speed (not Static) is selected and the vehicle cabin
|
||||||
|
// column is active - so it appears in both the Vehicle and Driver profiles.
|
||||||
|
// Longest run of hours, across the full day since lorries run day and night,
|
||||||
|
// that are NOT poor driving conditions: a hot cabin (>29 °C), heavy rain,
|
||||||
|
// snow/ice, fog/thick mist, or gale-force wind.
|
||||||
|
const GALE_MS = 39 / 2.237; // 39 mph gust = Gale (Force 8)
|
||||||
|
const drivingShown = show('vehicleT') && vehicleSpeed !== 'static';
|
||||||
|
const drivingWins = drivingShown
|
||||||
|
? allWindows(todayRows, r => {
|
||||||
|
const gustMs = r.gust ?? r.va;
|
||||||
|
return (r.vehicleT == null || r.vehicleT <= 29) && // cabin not dangerously hot
|
||||||
|
(r.precip == null || r.precip < 4) && // not heavy rain (mm/h)
|
||||||
|
(r.snow == null || r.snow === 0) && // no snow
|
||||||
|
(r.Ta == null || r.Ta > 1) && // no ice risk
|
||||||
|
(r.visKm == null || r.visKm >= 4) && // not fog / thick mist
|
||||||
|
(gustMs == null || gustMs < GALE_MS); // not gale-force wind
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
const drivingItem = {
|
||||||
|
icon: '🚚',
|
||||||
|
label: 'Good driving time',
|
||||||
|
value: formatWindows(drivingWins) ?? 'Drive with care',
|
||||||
|
alert: drivingWins.length === 0,
|
||||||
|
};
|
||||||
|
|
||||||
// ── Farming ────────────────────────────────────────────────────────────
|
// ── Farming ────────────────────────────────────────────────────────────
|
||||||
if (profile === 'farming') {
|
if (profile === 'farming') {
|
||||||
const fieldWindow = longestWindow(dayRows, r =>
|
const fieldWins = allWindows(dayRows, r =>
|
||||||
r.utciAdj >= 8 && r.utciAdj <= 32 && r.precipProb < 30 && r.va < 12
|
r.utciAdj >= 8 && r.utciAdj <= 32 && r.precipProb < 30 && r.va < 12
|
||||||
);
|
);
|
||||||
const soilWarmRow = todayRows.find(r => r.soilT0 != null && r.soilT0 >= 10);
|
const soilWarmRow = todayRows.find(r => r.soilT0 != null && r.soilT0 >= 10);
|
||||||
@@ -483,10 +526,8 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols
|
|||||||
{
|
{
|
||||||
icon: '⏱',
|
icon: '⏱',
|
||||||
label: 'Best field work',
|
label: 'Best field work',
|
||||||
value: fieldWindow
|
value: formatWindows(fieldWins) ?? 'No suitable window',
|
||||||
? `${hhmm(fieldWindow.start.iso)} – ${hhmmEnd(fieldWindow.end.iso)}`
|
alert: fieldWins.length === 0,
|
||||||
: 'No suitable window',
|
|
||||||
alert: !fieldWindow,
|
|
||||||
},
|
},
|
||||||
...(show('soilT') ? [{
|
...(show('soilT') ? [{
|
||||||
icon: '🌱',
|
icon: '🌱',
|
||||||
@@ -530,6 +571,7 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols
|
|||||||
}] : []),
|
}] : []),
|
||||||
...(show('precipProb') ? [rainItem] : []),
|
...(show('precipProb') ? [rainItem] : []),
|
||||||
...lightningItem,
|
...lightningItem,
|
||||||
|
...(drivingShown ? [drivingItem] : []),
|
||||||
{
|
{
|
||||||
icon: '🌤',
|
icon: '🌤',
|
||||||
label: 'Best travel comfort',
|
label: 'Best travel comfort',
|
||||||
@@ -547,7 +589,7 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols
|
|||||||
if (profile === 'home') {
|
if (profile === 'home') {
|
||||||
const peakIndoor = peakRow('indoorT');
|
const peakIndoor = peakRow('indoorT');
|
||||||
const peakManaged = peakRow('managedT');
|
const peakManaged = peakRow('managedT');
|
||||||
const ventWindow = longestWindow(todayRows, r =>
|
const ventWins = allWindows(todayRows, r =>
|
||||||
r.Ta != null && r.indoorT != null && r.Ta < r.indoorT && r.precipProb < 20
|
r.Ta != null && r.indoorT != null && r.Ta < r.indoorT && r.precipProb < 20
|
||||||
);
|
);
|
||||||
const peakAqi = Math.max(0, ...todayRows.map(r => r.aqi ?? 0));
|
const peakAqi = Math.max(0, ...todayRows.map(r => r.aqi ?? 0));
|
||||||
@@ -569,9 +611,7 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols
|
|||||||
...(show('indoorT') ? [{
|
...(show('indoorT') ? [{
|
||||||
icon: '🪟',
|
icon: '🪟',
|
||||||
label: 'Open windows',
|
label: 'Open windows',
|
||||||
value: ventWindow
|
value: formatWindows(ventWins) ?? 'Keep closed',
|
||||||
? `${hhmm(ventWindow.start.iso)} – ${hhmmEnd(ventWindow.end.iso)}`
|
|
||||||
: 'Keep closed',
|
|
||||||
alert: false,
|
alert: false,
|
||||||
}] : []),
|
}] : []),
|
||||||
...lightningItem,
|
...lightningItem,
|
||||||
@@ -592,7 +632,7 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols
|
|||||||
|
|
||||||
// ── Activities - running / cycling ─────────────────────────────────────
|
// ── Activities - running / cycling ─────────────────────────────────────
|
||||||
if (variant === 'running' || variant === 'cycling') {
|
if (variant === 'running' || variant === 'cycling') {
|
||||||
const coolWindow = longestWindow(dayRows, r =>
|
const coolWins = allWindows(dayRows, r =>
|
||||||
r.utciAdj >= 5 && r.utciAdj <= 22 && r.precipProb < 30
|
r.utciAdj >= 5 && r.utciAdj <= 22 && r.precipProb < 30
|
||||||
);
|
);
|
||||||
const peakUvRow = peakRow('uv');
|
const peakUvRow = peakRow('uv');
|
||||||
@@ -606,10 +646,8 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols
|
|||||||
{
|
{
|
||||||
icon: '⏱',
|
icon: '⏱',
|
||||||
label: `Best ${variant} window`,
|
label: `Best ${variant} window`,
|
||||||
value: coolWindow
|
value: formatWindows(coolWins) ?? 'No cool window today',
|
||||||
? `${hhmm(coolWindow.start.iso)} – ${hhmmEnd(coolWindow.end.iso)}`
|
alert: coolWins.length === 0,
|
||||||
: 'No cool window today',
|
|
||||||
alert: !coolWindow,
|
|
||||||
},
|
},
|
||||||
...(show('precipProb') ? [rainItem] : []),
|
...(show('precipProb') ? [rainItem] : []),
|
||||||
...lightningItem,
|
...lightningItem,
|
||||||
@@ -635,7 +673,7 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Outdoors - beach, park, events etc. (and fallback) ─────────────────
|
// ── Outdoors - beach, park, events etc. (and fallback) ─────────────────
|
||||||
const comfortWindow = longestWindow(dayRows, r =>
|
const comfortWins = allWindows(dayRows, r =>
|
||||||
r.utciAdj >= 9 && r.utciAdj <= 26 && r.precipProb < 30
|
r.utciAdj >= 9 && r.utciAdj <= 26 && r.precipProb < 30
|
||||||
);
|
);
|
||||||
const peakFelt = peakRow('utciAdj');
|
const peakFelt = peakRow('utciAdj');
|
||||||
@@ -647,13 +685,12 @@ export function computeGlanceSummary(todayRows, profile, variant, skinType, cols
|
|||||||
: null;
|
: null;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
...(drivingShown ? [drivingItem] : []),
|
||||||
{
|
{
|
||||||
icon: '🌤',
|
icon: '🌤',
|
||||||
label: 'Comfortable window',
|
label: 'Comfortable window',
|
||||||
value: comfortWindow
|
value: formatWindows(comfortWins) ?? 'No comfortable window',
|
||||||
? `${hhmm(comfortWindow.start.iso)} – ${hhmmEnd(comfortWindow.end.iso)}`
|
alert: comfortWins.length === 0,
|
||||||
: 'No comfortable window',
|
|
||||||
alert: !comfortWindow,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: '🌡',
|
icon: '🌡',
|
||||||
|
|||||||
+4
-1
@@ -108,6 +108,7 @@ export const OUTDOORS_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 } },
|
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 } },
|
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 } },
|
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 } },
|
||||||
};
|
};
|
||||||
|
|
||||||
// Pollen type options shown in the pulldown selector.
|
// Pollen type options shown in the pulldown selector.
|
||||||
@@ -158,6 +159,7 @@ export const VARIANT_DEFAULT_ENV = {
|
|||||||
construction:'urban',
|
construction:'urban',
|
||||||
office: 'urban',
|
office: 'urban',
|
||||||
windowcleaning:'urban',
|
windowcleaning:'urban',
|
||||||
|
driver: 'urban',
|
||||||
};
|
};
|
||||||
|
|
||||||
// Left-to-right order of the profile buttons in the top filter bar.
|
// Left-to-right order of the profile buttons in the top filter bar.
|
||||||
@@ -185,6 +187,7 @@ export const variantIcons = {
|
|||||||
market: '🏪',
|
market: '🏪',
|
||||||
windowcleaning: '🪟',
|
windowcleaning: '🪟',
|
||||||
office: '🏢',
|
office: '🏢',
|
||||||
|
driver: '🚚',
|
||||||
};
|
};
|
||||||
|
|
||||||
// Which variant keys appear in the Activities dropdown.
|
// Which variant keys appear in the Activities dropdown.
|
||||||
@@ -194,7 +197,7 @@ export const activityVariantKeys = ['dogwalk', 'running', 'cycling', 'hiking', '
|
|||||||
export const placeVariantKeys = ['urban', 'park', 'forest', 'beach', 'events', 'festival', 'airport'];
|
export const placeVariantKeys = ['urban', 'park', 'forest', 'beach', 'events', 'festival', 'airport'];
|
||||||
|
|
||||||
// Which variant keys appear in the Work dropdown.
|
// Which variant keys appear in the Work dropdown.
|
||||||
export const workVariantKeys = ['farming', 'construction', 'market', 'windowcleaning', 'office'];
|
export const workVariantKeys = ['farming', 'construction', 'market', 'windowcleaning', 'driver', 'office'];
|
||||||
|
|
||||||
// Tooltip text shown when the user clicks/hovers a table column header.
|
// Tooltip text shown when the user clicks/hovers a table column header.
|
||||||
export const COL_DESCRIPTIONS = {
|
export const COL_DESCRIPTIONS = {
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import {
|
|||||||
activityVariantKeys, placeVariantKeys, workVariantKeys,
|
activityVariantKeys, placeVariantKeys, workVariantKeys,
|
||||||
UTCI_ENVIRONMENTS, VARIANT_DEFAULT_ENV,
|
UTCI_ENVIRONMENTS, VARIANT_DEFAULT_ENV,
|
||||||
} from '../config.js';
|
} from '../config.js';
|
||||||
import { utciCategory } from '../utils.js';
|
import { utciCategory, VEHICLE_SPEEDS } from '../utils.js';
|
||||||
import { buildHourlyRows, aggregateRows } from '../compute.js';
|
import { buildHourlyRows, aggregateRows } from '../compute.js';
|
||||||
import { useForecast } from './useForecast.js';
|
import { useForecast } from './useForecast.js';
|
||||||
import { useColumnPopup } from './useColumnPopup.js';
|
import { useColumnPopup } from './useColumnPopup.js';
|
||||||
@@ -202,6 +202,14 @@ export function useAppState() {
|
|||||||
try { localStorage.setItem('sunscope_vehicle_vent', v ? '1' : '0'); } catch (e) { /* ignore */ }
|
try { localStorage.setItem('sunscope_vehicle_vent', v ? '1' : '0'); } catch (e) { /* ignore */ }
|
||||||
setVehicleVent(v);
|
setVehicleVent(v);
|
||||||
};
|
};
|
||||||
|
// Road-speed class (key into VEHICLE_SPEEDS): 'static' | 'urban' | 'aroad' | 'motorway'.
|
||||||
|
const [vehicleSpeed, setVehicleSpeed] = useState(() => {
|
||||||
|
try { return localStorage.getItem('sunscope_vehicle_speed') || 'static'; } catch (e) { return 'static'; }
|
||||||
|
});
|
||||||
|
const setVehicleSpeedAndSave = (v) => {
|
||||||
|
try { localStorage.setItem('sunscope_vehicle_speed', v); } catch (e) { /* ignore */ }
|
||||||
|
setVehicleSpeed(v);
|
||||||
|
};
|
||||||
|
|
||||||
const [outdoorsVariant, setOutdoorsVariant] = useState(() => {
|
const [outdoorsVariant, setOutdoorsVariant] = useState(() => {
|
||||||
try { return localStorage.getItem('sunscope_outdoors_variant') || 'urban'; } catch (e) { return 'urban'; }
|
try { return localStorage.getItem('sunscope_outdoors_variant') || 'urban'; } catch (e) { return 'urban'; }
|
||||||
@@ -220,6 +228,12 @@ export function useAppState() {
|
|||||||
// Auto-set UTCI environment modifier to match the selected variant.
|
// Auto-set UTCI environment modifier to match the selected variant.
|
||||||
const defaultEnv = VARIANT_DEFAULT_ENV[v] ?? 'open';
|
const defaultEnv = VARIANT_DEFAULT_ENV[v] ?? 'open';
|
||||||
setUtciEnvAndSave(defaultEnv);
|
setUtciEnvAndSave(defaultEnv);
|
||||||
|
// The Driver profile is about cab comfort - default the vehicle model to a
|
||||||
|
// truck cab travelling at A-road speed.
|
||||||
|
if (v === 'driver') {
|
||||||
|
setVehicleTypeAndSave('truck');
|
||||||
|
setVehicleSpeedAndSave('aroad');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const activeCols = activeProfile === 'outdoors'
|
const activeCols = activeProfile === 'outdoors'
|
||||||
@@ -458,7 +472,9 @@ export function useAppState() {
|
|||||||
|
|
||||||
// ── 9. COMPUTATION ───────────────────────────────────────────────────
|
// ── 9. COMPUTATION ───────────────────────────────────────────────────
|
||||||
const { hourlyRows, days, utcOffsetMs } = buildHourlyRows({
|
const { hourlyRows, days, utcOffsetMs } = buildHourlyRows({
|
||||||
forecast, airQuality, location, vehicleType, vehicleVent, buildingType, utciEnv,
|
forecast, airQuality, location, vehicleType, vehicleVent,
|
||||||
|
vehicleSpeed: (VEHICLE_SPEEDS[vehicleSpeed] ?? VEHICLE_SPEEDS.static).mph,
|
||||||
|
buildingType, utciEnv,
|
||||||
});
|
});
|
||||||
|
|
||||||
const visible = days[selectedDay]?.rows || [];
|
const visible = days[selectedDay]?.rows || [];
|
||||||
@@ -600,6 +616,7 @@ export function useAppState() {
|
|||||||
skinType, setSkinType,
|
skinType, setSkinType,
|
||||||
vehicleType, setVehicleType: setVehicleTypeAndSave,
|
vehicleType, setVehicleType: setVehicleTypeAndSave,
|
||||||
vehicleVent, setVehicleVent: setVehicleVentAndSave,
|
vehicleVent, setVehicleVent: setVehicleVentAndSave,
|
||||||
|
vehicleSpeed, setVehicleSpeed: setVehicleSpeedAndSave,
|
||||||
outdoorsVariant, setOutdoorsVariantAndSave,
|
outdoorsVariant, setOutdoorsVariantAndSave,
|
||||||
buildingType, setBuildingType: setBuildingTypeAndSave,
|
buildingType, setBuildingType: setBuildingTypeAndSave,
|
||||||
indoorManaged, setIndoorManaged: setIndoorManagedAndSave,
|
indoorManaged, setIndoorManaged: setIndoorManagedAndSave,
|
||||||
|
|||||||
+26
-32
@@ -11,7 +11,7 @@
|
|||||||
// calcConcreteTempPass(arrays...) thermal-lag pass over full hourly arrays
|
// calcConcreteTempPass(arrays...) thermal-lag pass over full hourly arrays
|
||||||
// calcIndoorTempPass(TaArr, globArr, elevArr, buildingType) passive indoor temp
|
// calcIndoorTempPass(TaArr, globArr, elevArr, buildingType) passive indoor temp
|
||||||
// calcManagedIndoorTempPass(TaArr, globArr, elevArr, buildingType) managed indoor temp
|
// calcManagedIndoorTempPass(TaArr, globArr, elevArr, buildingType) managed indoor temp
|
||||||
// calcVehicleInteriorTemp(Ta, globalRad, solElev, vehicleType, ventilated)
|
// calcVehicleInteriorTemp(Ta, globalRad, solElev, vehicleType, ventilated, speedMph)
|
||||||
// vaporPressureHpa(Ta, RH) Magnus formula - hPa
|
// vaporPressureHpa(Ta, RH) Magnus formula - hPa
|
||||||
// solarElevationDeg(lat, lon, dateUTC) NOAA simplified solar position (-)
|
// solarElevationDeg(lat, lon, dateUTC) NOAA simplified solar position (-)
|
||||||
// calcTmrt(Ta, dirRad, diffRad, globalRad, solElev) Mean radiant temperature
|
// calcTmrt(Ta, dirRad, diffRad, globalRad, solElev) Mean radiant temperature
|
||||||
@@ -396,17 +396,24 @@ export function calcManagedIndoorTempPass(TaArr, globArr, elevArr, buildingType
|
|||||||
// 35-45 -C - dangerous for children/pets (hyperthermia risk)
|
// 35-45 -C - dangerous for children/pets (hyperthermia risk)
|
||||||
// > 45 -C - potentially fatal within minutes
|
// > 45 -C - potentially fatal within minutes
|
||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
export function calcVehicleInteriorTemp(Ta, globalRad, solElev, vehicleType = 'car', ventilated = false) {
|
export function calcVehicleInteriorTemp(Ta, globalRad, solElev, vehicleType = 'car', ventilated = false, speedMph = 0) {
|
||||||
if (globalRad == null || Ta == null) return null;
|
if (globalRad == null || Ta == null) return null;
|
||||||
|
|
||||||
// Look up vehicle preset; fall back to a standard car if key unknown.
|
// Look up vehicle preset; fall back to a standard car if key unknown.
|
||||||
const preset = VEHICLE_TYPES[vehicleType] || VEHICLE_TYPES.car;
|
const preset = VEHICLE_TYPES[vehicleType] || VEHICLE_TYPES.car;
|
||||||
|
|
||||||
|
// Road speed in m/s. speedMph = 0 is "Static" (parked) and reproduces the
|
||||||
|
// original stationary model exactly; higher speeds scrub the shell with
|
||||||
|
// forced airflow and (windows down) flush the cabin toward ambient.
|
||||||
|
const vMs = Math.max(0, speedMph) * 0.447; // mph -> m/s
|
||||||
|
|
||||||
// -- 1. Panel conduction ------------------------------------------
|
// -- 1. Panel conduction ------------------------------------------
|
||||||
const panelAbsorbed = globalRad * (1 - preset.albedo); // W/m- absorbed by bodywork
|
const panelAbsorbed = globalRad * (1 - preset.albedo); // W/m- absorbed by bodywork
|
||||||
// Panel surface temp: absorbed solar / convective loss to outside air
|
// Panel surface temp: absorbed solar / convective loss to outside air.
|
||||||
// hOut ~10 W/m-K (light breeze over panel surface even when parked)
|
// Parked, a light breeze over the panels gives hOut ~10 W/m-K. Once moving,
|
||||||
const hOut = 10;
|
// forced convection rises with road speed (same sqrt form as the surface
|
||||||
|
// model), so the bodywork runs progressively closer to ambient.
|
||||||
|
const hOut = 10 + 5.5 * Math.sqrt(vMs);
|
||||||
const panelSurfaceTemp = Ta + panelAbsorbed / hOut;
|
const panelSurfaceTemp = Ta + panelAbsorbed / hOut;
|
||||||
// Conductive gain into cabin. Cars are thin metal + trim; motorhomes and
|
// Conductive gain into cabin. Cars are thin metal + trim; motorhomes and
|
||||||
// caravans use insulated sandwich panels, so their bodyU is much lower.
|
// caravans use insulated sandwich panels, so their bodyU is much lower.
|
||||||
@@ -437,10 +444,14 @@ export function calcVehicleInteriorTemp(Ta, globalRad, solElev, vehicleType = 'c
|
|||||||
const totalGain = conductionGain + glazingGain;
|
const totalGain = conductionGain + glazingGain;
|
||||||
// Cabin heat rejection: an effective blend of leakage, internal air volume,
|
// Cabin heat rejection: an effective blend of leakage, internal air volume,
|
||||||
// and surfaces exchanging heat with the outside. With windows open it is
|
// and surfaces exchanging heat with the outside. With windows open it is
|
||||||
// roughly 5- higher - air moves freely
|
// roughly 5x higher when parked - air moves freely through the cabin,
|
||||||
// through the cabin, flushing heat out and capping interior temperature much
|
// flushing heat out and capping interior temperature much closer to ambient.
|
||||||
// closer to ambient. Cabin temp still rises a little due to panel/roof solar gain.
|
// On the move the through-draught multiplies this further (a 70 mph open
|
||||||
const effectiveHLoss = ventilated ? preset.hCabinLoss * 5 : preset.hCabinLoss;
|
// window flushes the cabin almost to ambient). Sealed but moving rejects a
|
||||||
|
// little faster too, because the cooler shell pulls cabin heat out.
|
||||||
|
const speedFactor = 1 + vMs / 12; // grows with road speed (windows-open draught)
|
||||||
|
const lossMult = ventilated ? 5 * speedFactor : 1 + vMs / 40;
|
||||||
|
const effectiveHLoss = preset.hCabinLoss * lossMult;
|
||||||
const thermalMass = preset.thermalMass ?? 1;
|
const thermalMass = preset.thermalMass ?? 1;
|
||||||
const solarRise = (totalGain / effectiveHLoss) * thermalMass;
|
const solarRise = (totalGain / effectiveHLoss) * thermalMass;
|
||||||
|
|
||||||
@@ -493,30 +504,13 @@ export function calcVehicleInteriorTemp(Ta, globalRad, solElev, vehicleType = 'c
|
|||||||
//
|
//
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
// -- FUTURE FEATURE: Vehicle-at-speed thermal model ----------------------------
|
// -- Vehicle-at-speed thermal model (IMPLEMENTED) ------------------------------
|
||||||
// Idea: extend calcVehicleInteriorTemp (or add a companion function) to model
|
// calcVehicleInteriorTemp now takes a speedMph argument (Static / 20 / 50 / 70
|
||||||
// cabin temperature for a vehicle travelling at speed, not just parked.
|
// mph in the UI). Forced convection over the shell scales with road speed, and
|
||||||
|
// windows-open through-flow scales further with speed, so a moving cabin runs
|
||||||
|
// cooler than the same parked car. speedMph = 0 reproduces the static model.
|
||||||
//
|
//
|
||||||
// Key physics differences from the static model:
|
// -- CYCLIST AT SPEED (FUTURE) ------------------------------------------------
|
||||||
// - Forced convection over the shell scales with vehicle speed (v-), so
|
|
||||||
// hOut rises significantly - shell cools much faster than when parked.
|
|
||||||
// - Above ~30 mph the vehicle's own forward motion dominates airflow, so
|
|
||||||
// ambient wind direction becomes largely irrelevant (simplifies the model).
|
|
||||||
// - Speed classes to model: urban (~20 mph), dual carriageway (~50 mph),
|
|
||||||
// motorway (~70 mph) - each with a derived hOut multiplier.
|
|
||||||
// - Windows-open behaviour changes completely at speed: at 70 mph open
|
|
||||||
// windows create high-velocity through-flow, dramatically cutting cabin
|
|
||||||
// temp vs. the sealed-car case (but much less pleasant than AC!).
|
|
||||||
// - Roof and bonnet solar gain stays the same; side-glass gain is unchanged.
|
|
||||||
// - AC-off vs AC-on would be the primary user toggle alongside speed class.
|
|
||||||
//
|
|
||||||
// Suggested signature:
|
|
||||||
// calcVehicleInteriorTempAtSpeed(Ta, globalRad, solElev, vehicleType, speedMph, windowsOpen)
|
|
||||||
//
|
|
||||||
// This would be a useful companion output column (e.g. "Vehicle (moving)")
|
|
||||||
// for road-trip planning, dog-in-car safety at a rest stop vs. motorway, etc.
|
|
||||||
//
|
|
||||||
// -- CYCLIST AT SPEED (companion to the above) --------------------------------
|
|
||||||
// A cyclist generates their own headwind, so the felt temperature (UTCI) is
|
// A cyclist generates their own headwind, so the felt temperature (UTCI) is
|
||||||
// very different from a stationary person - even without ambient wind.
|
// very different from a stationary person - even without ambient wind.
|
||||||
// Could share the same speed-class approach as the vehicle model:
|
// Could share the same speed-class approach as the vehicle model:
|
||||||
|
|||||||
@@ -176,10 +176,26 @@ export const VEHICLE_TYPES = {
|
|||||||
car: { name: 'Car / Hatchback', albedo: 0.25, glazingArea: 1.0, bodyU: 4.0, hCabinLoss: 20, thermalMass: 1.00, retainedWarmth: false, internalGain: 0.0 },
|
car: { name: 'Car / Hatchback', albedo: 0.25, glazingArea: 1.0, bodyU: 4.0, hCabinLoss: 20, thermalMass: 1.00, retainedWarmth: false, internalGain: 0.0 },
|
||||||
mpv: { name: 'MPV / People Carrier', albedo: 0.25, glazingArea: 1.3, bodyU: 4.0, hCabinLoss: 20, thermalMass: 1.00, retainedWarmth: false, internalGain: 0.0 },
|
mpv: { name: 'MPV / People Carrier', albedo: 0.25, glazingArea: 1.3, bodyU: 4.0, hCabinLoss: 20, thermalMass: 1.00, retainedWarmth: false, internalGain: 0.0 },
|
||||||
suv: { name: 'SUV / 4x4', albedo: 0.22, glazingArea: 1.1, bodyU: 3.8, hCabinLoss: 19, thermalMass: 0.95, retainedWarmth: false, internalGain: 0.0 },
|
suv: { name: 'SUV / 4x4', albedo: 0.22, glazingArea: 1.1, bodyU: 3.8, hCabinLoss: 19, thermalMass: 0.95, retainedWarmth: false, internalGain: 0.0 },
|
||||||
|
truck: { name: 'Truck / HGV Cab', albedo: 0.30, glazingArea: 1.2, bodyU: 3.5, hCabinLoss: 18, thermalMass: 0.90, retainedWarmth: false, internalGain: 0.0 },
|
||||||
motorhome: { name: 'Motorhome / Campervan', albedo: 0.55, glazingArea: 0.25, bodyU: 0.9, hCabinLoss: 12, thermalMass: 0.35, retainedWarmth: true, internalGain: 1.2 },
|
motorhome: { name: 'Motorhome / Campervan', albedo: 0.55, glazingArea: 0.25, bodyU: 0.9, hCabinLoss: 12, thermalMass: 0.35, retainedWarmth: true, internalGain: 1.2 },
|
||||||
caravan: { name: 'Caravan (towed)', albedo: 0.60, glazingArea: 0.22, bodyU: 0.8, hCabinLoss: 11, thermalMass: 0.35, retainedWarmth: true, internalGain: 1.2 },
|
caravan: { name: 'Caravan (towed)', albedo: 0.60, glazingArea: 0.22, bodyU: 0.8, hCabinLoss: 11, thermalMass: 0.35, retainedWarmth: true, internalGain: 1.2 },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
// VEHICLE SPEEDS - road-speed classes for the cabin heat model.
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
// mph feeds calcVehicleInteriorTemp's speedMph argument. 'static' (0 mph)
|
||||||
|
// is a parked vehicle and reproduces the original stationary model exactly;
|
||||||
|
// higher speeds scrub the shell with forced airflow and (windows down) flush
|
||||||
|
// the cabin toward ambient.
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
export const VEHICLE_SPEEDS = {
|
||||||
|
static: { name: 'Static', mph: 0 },
|
||||||
|
urban: { name: 'Urban 20mph', mph: 20 },
|
||||||
|
aroad: { name: 'A-road 50mph', mph: 50 },
|
||||||
|
motorway:{ name: 'Motorway 70mph', mph: 70 },
|
||||||
|
};
|
||||||
|
|
||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
// BUILDING TYPES - presets for the indoor temperature model.
|
// BUILDING TYPES - presets for the indoor temperature model.
|
||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
|
|||||||
@@ -108,5 +108,12 @@
|
|||||||
"basic": 16,
|
"basic": 16,
|
||||||
"farming": 2
|
"farming": 2
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"2026-06-27": {
|
||||||
|
"visits": 7,
|
||||||
|
"profiles": {
|
||||||
|
"farming": 3,
|
||||||
|
"basic": 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user