3.5.5
Update desktop profile hover button Recalibrated vehicle ventilation calcs
This commit is contained in:
+60
-9
@@ -455,6 +455,43 @@
|
|||||||
}
|
}
|
||||||
.profile-dropdown-close:hover { color: #1e1208; background: rgba(176, 152, 112, 0.16); }
|
.profile-dropdown-close:hover { color: #1e1208; background: rgba(176, 152, 112, 0.16); }
|
||||||
|
|
||||||
|
/* Tablet + desktop: the floating bottom button is mobile-only, so the panel
|
||||||
|
hangs off the bottom edge of the active-profile-row instead — dropping
|
||||||
|
down out of that row and shrinking back into it on close.
|
||||||
|
--profile-anchor-top is the row's live viewport bottom, set by
|
||||||
|
ConfigPanel.js. Phones (<=640px) keep the bottom sheet untouched. */
|
||||||
|
@media (min-width: 641px) {
|
||||||
|
.profile-dropdown {
|
||||||
|
top: var(--profile-anchor-top, 0px);
|
||||||
|
bottom: auto;
|
||||||
|
max-height: calc(100vh - var(--profile-anchor-top, 0px) - 16px);
|
||||||
|
border: 1.5px solid #c9b08a;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden; /* keeps the scrolling body inside the rounded corners */
|
||||||
|
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.18);
|
||||||
|
transform-origin: 50% 0;
|
||||||
|
}
|
||||||
|
.profile-dropdown-body {
|
||||||
|
max-height: calc(100vh - var(--profile-anchor-top, 0px) - 16px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Too little room under the row for the content — revert to the mobile
|
||||||
|
bottom sheet (set by ConfigPanel.js) so the panel gets the full height. */
|
||||||
|
.profile-dropdown.is-bottom-sheet {
|
||||||
|
top: auto;
|
||||||
|
bottom: 0;
|
||||||
|
max-height: calc(100vh - 24px);
|
||||||
|
border: none;
|
||||||
|
border-top: 1.5px solid #c9b08a;
|
||||||
|
border-radius: 12px 12px 0 0;
|
||||||
|
box-shadow: 0 -10px 28px rgba(0, 0, 0, 0.18);
|
||||||
|
transform-origin: 50% 100%;
|
||||||
|
}
|
||||||
|
.profile-dropdown.is-bottom-sheet .profile-dropdown-body {
|
||||||
|
max-height: calc(100vh - 24px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Read-only "what am I looking at" row above the day tabs — states the
|
/* Read-only "what am I looking at" row above the day tabs — states the
|
||||||
active profile and the thermal basis (SunSoak env / vehicle / building /
|
active profile and the thermal basis (SunSoak env / vehicle / building /
|
||||||
fur colour) driving the numbers below. Clicking/Enter opens the same
|
fur colour) driving the numbers below. Clicking/Enter opens the same
|
||||||
@@ -465,15 +502,16 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding: 2px 16px;
|
padding: 8px 16px;
|
||||||
margin-bottom: 6px;
|
margin-bottom: 4px;
|
||||||
/* --row-bg is set inline per the exact selected option (see ROW_GRADIENTS
|
/* --row-bg is set inline per the exact selected option (see ROW_GRADIENTS
|
||||||
in DayTabs.js) — falls back to a flat parchment tone if unset. */
|
in DayTabs.js) — falls back to a flat parchment tone if unset. */
|
||||||
background: var(--row-bg, #fdf8ee);
|
background: var(--row-bg, #fdf8ee);
|
||||||
border: 1.5px solid #d4c0a0;
|
border: 2px solid #d4c0a0;
|
||||||
border-radius: 6px;
|
border-radius: 9px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: filter 0.15s, border-color 0.15s;
|
box-shadow: inset 0 -1px 0 rgba(0,0,0,0.05), 0 1px 3px rgba(0,0,0,0.04);
|
||||||
|
transition: filter 0.2s, border-color 0.2s, box-shadow 0.2s;
|
||||||
}
|
}
|
||||||
/* Remove alpha from the default fallback color and any potential inherited
|
/* Remove alpha from the default fallback color and any potential inherited
|
||||||
semi-transparent backgrounds while keeping the hue. */
|
semi-transparent backgrounds while keeping the hue. */
|
||||||
@@ -494,13 +532,16 @@
|
|||||||
}
|
}
|
||||||
.active-profile-row:hover,
|
.active-profile-row:hover,
|
||||||
.active-profile-row:focus-visible {
|
.active-profile-row:focus-visible {
|
||||||
filter: brightness(1.06);
|
filter: brightness(1.04);
|
||||||
border-color: #c8922a;
|
border-color: #c8922a;
|
||||||
|
box-shadow: inset 0 -1px 0 rgba(0,0,0,0.05), 0 3px 8px rgba(200,146,42,0.18);
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Breathing room between each "PROFILE" / "VIEWING" label and its value. */
|
||||||
.active-profile-row-item {
|
.active-profile-row-item {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: baseline;
|
||||||
gap: 7px;
|
gap: 7px;
|
||||||
}
|
}
|
||||||
.active-profile-row-label {
|
.active-profile-row-label {
|
||||||
@@ -521,12 +562,22 @@
|
|||||||
}
|
}
|
||||||
.active-profile-row-sep { color: #c0a880; font-size: 16px; }
|
.active-profile-row-sep { color: #c0a880; font-size: 16px; }
|
||||||
.active-profile-row-edit {
|
.active-profile-row-edit {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
font-family: Manrope, sans-serif;
|
font-family: Manrope, sans-serif;
|
||||||
font-size: 13px;
|
font-size: 12.5px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
letter-spacing: 0.04em;
|
letter-spacing: 0.04em;
|
||||||
color: #9a6a1a;
|
color: #9a6a1a;
|
||||||
margin-left: 4px;
|
margin-left: 8px;
|
||||||
|
opacity: 0.7;
|
||||||
|
transition: opacity 0.2s, transform 0.2s, color 0.2s;
|
||||||
|
}
|
||||||
|
.active-profile-row:hover .active-profile-row-edit,
|
||||||
|
.active-profile-row:focus-visible .active-profile-row-edit {
|
||||||
|
opacity: 1;
|
||||||
|
color: #c8922a;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Border + text accent per thermal-model group, reusing the same colour
|
/* Border + text accent per thermal-model group, reusing the same colour
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ export function ConfigPanel({
|
|||||||
}) {
|
}) {
|
||||||
const profileScrollRef = useRef(null);
|
const profileScrollRef = useRef(null);
|
||||||
const profileWrapRef = useRef(null);
|
const profileWrapRef = useRef(null);
|
||||||
|
const dropdownRef = useRef(null);
|
||||||
|
|
||||||
const { mainConfigKey, mainLabel } = deriveProfileMain(activeProfile, outdoorsVariant);
|
const { mainConfigKey, mainLabel } = deriveProfileMain(activeProfile, outdoorsVariant);
|
||||||
|
|
||||||
@@ -77,6 +78,34 @@ export function ConfigPanel({
|
|||||||
return () => document.removeEventListener('keydown', onKey);
|
return () => document.removeEventListener('keydown', onKey);
|
||||||
}, [open, onClose]);
|
}, [open, onClose]);
|
||||||
|
|
||||||
|
// From tablet up the panel drops *out of* the active-profile-row rather
|
||||||
|
// than sliding up from the floating button (which is mobile-only), so it
|
||||||
|
// has to be anchored to that row's live bottom edge — it's sticky on
|
||||||
|
// desktop and scrolls away on tablet, hence remeasuring on scroll/resize.
|
||||||
|
// Mobile (<=640px) ignores the var entirely and keeps the bottom sheet.
|
||||||
|
useEffect(() => {
|
||||||
|
const el = dropdownRef.current;
|
||||||
|
if (!el) return;
|
||||||
|
const measure = () => {
|
||||||
|
const row = document.querySelector('.active-profile-row');
|
||||||
|
const top = row ? Math.max(0, row.getBoundingClientRect().bottom) : 0;
|
||||||
|
el.style.setProperty('--profile-anchor-top', `${Math.round(top)}px`);
|
||||||
|
// Not enough room below the row for the panel's content? Fall back to
|
||||||
|
// the mobile bottom-sheet layout, which gets the full viewport height.
|
||||||
|
const body = el.querySelector('.profile-dropdown-body');
|
||||||
|
const content = body ? body.scrollHeight : 0;
|
||||||
|
el.classList.toggle('is-bottom-sheet', content > window.innerHeight - top - 16);
|
||||||
|
};
|
||||||
|
measure();
|
||||||
|
if (!open) return;
|
||||||
|
window.addEventListener('scroll', measure, true);
|
||||||
|
window.addEventListener('resize', measure);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('scroll', measure, true);
|
||||||
|
window.removeEventListener('resize', measure);
|
||||||
|
};
|
||||||
|
}, [open, activeTab, activeProfile, outdoorsVariant]);
|
||||||
|
|
||||||
// Drag-to-scroll + fade edges for the horizontal profile card row.
|
// Drag-to-scroll + fade edges for the horizontal profile card row.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const el = profileScrollRef.current;
|
const el = profileScrollRef.current;
|
||||||
@@ -226,7 +255,7 @@ export function ConfigPanel({
|
|||||||
return html`
|
return html`
|
||||||
<${Fragment}>
|
<${Fragment}>
|
||||||
<div class=${`profile-dropdown-overlay${open ? ' is-open' : ''}`} onClick=${onClose}></div>
|
<div class=${`profile-dropdown-overlay${open ? ' is-open' : ''}`} onClick=${onClose}></div>
|
||||||
<div class=${`profile-dropdown${open ? ' is-open' : ''}`} role="dialog" aria-modal="true" aria-label="Profile and configuration" aria-hidden=${!open}>
|
<div ref=${dropdownRef} class=${`profile-dropdown${open ? ' is-open' : ''}`} role="dialog" aria-modal="true" aria-label="Profile and configuration" aria-hidden=${!open}>
|
||||||
<button class="profile-dropdown-close" aria-label="Close" onClick=${onClose}>×</button>
|
<button class="profile-dropdown-close" aria-label="Close" onClick=${onClose}>×</button>
|
||||||
<div class="profile-dropdown-body">
|
<div class="profile-dropdown-body">
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ import { h, Fragment } from '../../vendor/preact.js';
|
|||||||
import htm from '../../vendor/htm.js';
|
import htm from '../../vendor/htm.js';
|
||||||
import { confidenceBand, utciCategory, VEHICLE_SPEEDS, VEHICLE_TYPES, BUILDING_TYPES, FUR_COLORS } from '../utils.js';
|
import { confidenceBand, utciCategory, VEHICLE_SPEEDS, VEHICLE_TYPES, BUILDING_TYPES, FUR_COLORS } from '../utils.js';
|
||||||
import { FREE_DAYS, UTCI_ENVIRONMENTS, deriveProfileMain, FILTER_PROFILES, variantIcons } from '../config.js';
|
import { FREE_DAYS, UTCI_ENVIRONMENTS, deriveProfileMain, FILTER_PROFILES, variantIcons } from '../config.js';
|
||||||
import { calcVehicleInteriorTemp } from '../physics.js';
|
import { calcVehicleInteriorTempPass } from '../physics.js';
|
||||||
import { CloudIcon, PrecipIcon, HouseIcon, CarIcon, FogIcon, IceIcon } from '../components.js';
|
import { CloudIcon, PrecipIcon, HouseIcon, CarIcon, FogIcon, IceIcon } from '../components.js';
|
||||||
import { SubscribeModal } from './SubscribeModal.js';
|
import { SubscribeModal } from './SubscribeModal.js';
|
||||||
|
|
||||||
@@ -222,9 +222,12 @@ export function DayTabs({
|
|||||||
let shadeHi = null, shadeLo = null;
|
let shadeHi = null, shadeLo = null;
|
||||||
if (isVehicleProfile) {
|
if (isVehicleProfile) {
|
||||||
const speedMph = (VEHICLE_SPEEDS[vehicleSpeed] ?? VEHICLE_SPEEDS.static).mph;
|
const speedMph = (VEHICLE_SPEEDS[vehicleSpeed] ?? VEHICLE_SPEEDS.static).mph;
|
||||||
const ventVals = d.rows
|
// Run the lagged pass over the whole day, not per-hour equilibrium,
|
||||||
.map(r => calcVehicleInteriorTemp(r.Ta, r.glob, r.elev, vehicleType, true, speedMph))
|
// so this hi/lo is directly comparable with the vehicleT column.
|
||||||
.filter(v => isFinite(v));
|
const ventVals = calcVehicleInteriorTempPass(
|
||||||
|
d.rows.map(r => r.Ta), d.rows.map(r => r.glob), d.rows.map(r => r.elev),
|
||||||
|
d.rows.map(r => r.va), vehicleType, true, speedMph
|
||||||
|
).filter(v => v != null && isFinite(v));
|
||||||
shadeHi = ventVals.length ? Math.round(Math.max(...ventVals)) : null;
|
shadeHi = ventVals.length ? Math.round(Math.max(...ventVals)) : null;
|
||||||
shadeLo = ventVals.length ? Math.round(Math.min(...ventVals)) : null;
|
shadeLo = ventVals.length ? Math.round(Math.min(...ventVals)) : null;
|
||||||
} else if (secondaryField) {
|
} else if (secondaryField) {
|
||||||
|
|||||||
@@ -23,13 +23,7 @@ import { Wordmark } from './Wordmark.js';
|
|||||||
const html = htm.bind(h);
|
const html = htm.bind(h);
|
||||||
|
|
||||||
const SUBSCRIBE_URL = 'https://buy.stripe.com/9B63cw7vl8k15Ei9DQd7q00';
|
const SUBSCRIBE_URL = 'https://buy.stripe.com/9B63cw7vl8k15Ei9DQd7q00';
|
||||||
// TODO: replace with the one-time Payment Link created in the Stripe
|
const SUBSCRIBE_URL_ONEOFF = 'https://buy.stripe.com/14A14odTJ6bT0jY4jwd7q02';
|
||||||
// Dashboard (see plan Part 3) - a flat-rate product with a few selectable
|
|
||||||
// price options (e.g. £3 / £5 / £10), since Stripe Payment Links don't
|
|
||||||
// support true customer-chosen amounts. Configure its after-payment
|
|
||||||
// redirect to https://sunscope.net/?session_id={CHECKOUT_SESSION_ID}
|
|
||||||
// the same way the monthly link should be.
|
|
||||||
const SUBSCRIBE_URL_ONEOFF = 'https://buy.stripe.com/REPLACE_WITH_ONEOFF_PAYMENT_LINK';
|
|
||||||
const MANAGE_URL = 'https://billing.stripe.com/p/login/9B63cw7vl8k15Ei9DQd7q00';
|
const MANAGE_URL = 'https://billing.stripe.com/p/login/9B63cw7vl8k15Ei9DQd7q00';
|
||||||
|
|
||||||
export function SubscribeModal({ title, detail, onClose, openRestore }) {
|
export function SubscribeModal({ title, detail, onClose, openRestore }) {
|
||||||
|
|||||||
+14
-5
@@ -13,7 +13,7 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
vaporPressureHpa, solarElevationDeg, calcTmrt, utciApprox,
|
vaporPressureHpa, solarElevationDeg, calcTmrt, utciApprox,
|
||||||
calcConcreteTempPass, calcVehicleInteriorTemp,
|
calcConcreteTempPass, calcVehicleInteriorTempPass,
|
||||||
calcIndoorTempPass, calcManagedIndoorTempPass, calcShadeAirTemp,
|
calcIndoorTempPass, calcManagedIndoorTempPass, calcShadeAirTemp,
|
||||||
calcFurSurfaceTempPass,
|
calcFurSurfaceTempPass,
|
||||||
} from './physics.js';
|
} from './physics.js';
|
||||||
@@ -88,7 +88,7 @@ export function buildHourlyRows({ forecast, airQuality, location, vehicleType, v
|
|||||||
// (building shadow, beach umbrella, canopy...). Shares SunSoak's TaEnv
|
// (building shadow, beach umbrella, canopy...). Shares SunSoak's TaEnv
|
||||||
// baseline and env-reduced radiation, so the two columns stay consistent.
|
// baseline and env-reduced radiation, so the two columns stay consistent.
|
||||||
const shadeT = calcShadeAirTemp(TaEnv, dirEnv + difEnv * 0.2, va, elev, env.shade);
|
const shadeT = calcShadeAirTemp(TaEnv, dirEnv + difEnv * 0.2, va, elev, env.shade);
|
||||||
const vehicleT = calcVehicleInteriorTemp(Ta, glob, elev, vehicleType, vehicleVent, vehicleSpeed);
|
// vehicleT is stamped in the two-pass section below (thermal lag).
|
||||||
const eh = vaporPressureHpa(Ta, RH);
|
const eh = vaporPressureHpa(Ta, RH);
|
||||||
const Tmrt = calcTmrt(TaEnv, dirEnv, difEnv, globEnv, elev);
|
const Tmrt = calcTmrt(TaEnv, dirEnv, difEnv, globEnv, elev);
|
||||||
const utci = utciApprox(TaEnv, Tmrt, va, eh);
|
const utci = utciApprox(TaEnv, Tmrt, va, eh);
|
||||||
@@ -153,7 +153,7 @@ export function buildHourlyRows({ forecast, airQuality, location, vehicleType, v
|
|||||||
cc, ccLow, ccMid, ccHigh, cloudCat,
|
cc, ccLow, ccMid, ccHigh, cloudCat,
|
||||||
uv, uvA, uvB,
|
uv, uvA, uvB,
|
||||||
precip, precipProb, lightning, cape, snow,
|
precip, precipProb, lightning, cape, snow,
|
||||||
soilT0, soilT6, soilM, vehicleT, shadeT,
|
soilT0, soilT6, soilM, shadeT,
|
||||||
effectiveRad,
|
effectiveRad,
|
||||||
elev, Tmrt, utci, utciAdj, eh, compass,
|
elev, Tmrt, utci, utciAdj, eh, compass,
|
||||||
visKm, aqi,
|
visKm, aqi,
|
||||||
@@ -161,8 +161,8 @@ export function buildHourlyRows({ forecast, airQuality, location, vehicleType, v
|
|||||||
};
|
};
|
||||||
}) : [];
|
}) : [];
|
||||||
|
|
||||||
// Two-pass calculations: concrete thermal lag + indoor temperature.
|
// Two-pass calculations: concrete thermal lag + indoor + vehicle cabin.
|
||||||
// Both need the full hourly arrays so they can look back at previous
|
// All need the full hourly arrays so they can look back at previous
|
||||||
// hours. Run after hourlyRows is built, then stamp each row.
|
// hours. Run after hourlyRows is built, then stamp each row.
|
||||||
if (hourlyRows.length > 0) {
|
if (hourlyRows.length > 0) {
|
||||||
const TaArr = hourlyRows.map(r => r.Ta);
|
const TaArr = hourlyRows.map(r => r.Ta);
|
||||||
@@ -188,6 +188,15 @@ export function buildHourlyRows({ forecast, airQuality, location, vehicleType, v
|
|||||||
const managedTemps = calcManagedIndoorTempPass(TaArr, globArr, elevArr, buildingType);
|
const managedTemps = calcManagedIndoorTempPass(TaArr, globArr, elevArr, buildingType);
|
||||||
hourlyRows.forEach((r, i) => { r.indoorT = indoorTemps[i]; r.managedT = managedTemps[i]; });
|
hourlyRows.forEach((r, i) => { r.indoorT = indoorTemps[i]; r.managedT = managedTemps[i]; });
|
||||||
|
|
||||||
|
// Vehicle cabin temperature with thermal lag. A parked vehicle climbs
|
||||||
|
// toward the hour's equilibrium rather than jumping to it, so a van that
|
||||||
|
// has been in the sun since morning reads hotter than the same van an
|
||||||
|
// hour after parking. vaArr feeds ambient wind into shell convection.
|
||||||
|
const vehicleTemps = calcVehicleInteriorTempPass(
|
||||||
|
TaArr, globArr, elevArr, vaArr, vehicleType, vehicleVent, vehicleSpeed
|
||||||
|
);
|
||||||
|
hourlyRows.forEach((r, i) => { r.vehicleT = vehicleTemps[i]; });
|
||||||
|
|
||||||
// Fur surface temperature with thermal lag - Pets profile.
|
// Fur surface temperature with thermal lag - Pets profile.
|
||||||
const furAlbedo = (FUR_COLORS[furColor] || FUR_COLORS.brown).albedo;
|
const furAlbedo = (FUR_COLORS[furColor] || FUR_COLORS.brown).albedo;
|
||||||
const furTemps = calcFurSurfaceTempPass(TaArr, radArr, vaArr, elevArr, furAlbedo);
|
const furTemps = calcFurSurfaceTempPass(TaArr, radArr, vaArr, elevArr, furAlbedo);
|
||||||
|
|||||||
+1
-1
@@ -256,7 +256,7 @@ export const COL_DESCRIPTIONS = {
|
|||||||
soilT6: { title: 'Soil Temp 6 cm', desc: 'Temperature of the soil at 6 cm depth, the root zone for many crops and seedlings. Lags behind surface temperature by several hours.' },
|
soilT6: { title: 'Soil Temp 6 cm', desc: 'Temperature of the soil at 6 cm depth, the root zone for many crops and seedlings. Lags behind surface temperature by several hours.' },
|
||||||
soilM: { title: 'Soil Moisture', desc: 'Water saturation of the top 1 cm of soil as a percentage. Above 40% suggests saturated ground; below 20% indicates dry conditions.' },
|
soilM: { title: 'Soil Moisture', desc: 'Water saturation of the top 1 cm of soil as a percentage. Above 40% suggests saturated ground; below 20% indicates dry conditions.' },
|
||||||
concreteT: { title: 'Concrete Surface', desc: 'Estimated temperature of sun-exposed urban concrete or paving. Concrete absorbs more solar energy than grass and cannot cool itself through evaporation — surface temps can run 15–25 °C above air temperature on sunny days.' },
|
concreteT: { title: 'Concrete Surface', desc: 'Estimated temperature of sun-exposed urban concrete or paving. Concrete absorbs more solar energy than grass and cannot cool itself through evaporation — surface temps can run 15–25 °C above air temperature on sunny days.' },
|
||||||
vehicleT: { title: 'Vehicle Interior', desc: 'Estimated ambient cabin temperature inside a parked vehicle. Cars heat rapidly through thin body panels and glass; motorhomes and caravans are modelled as insulated occupied living spaces with retained warmth, lower glass gain, and slower heat response. Dangerous for children and pets above 35 °C; potentially fatal above 45 °C.' },
|
vehicleT: { title: 'Vehicle Interior', desc: 'Estimated ambient cabin temperature inside a parked vehicle. Cars heat rapidly through thin body panels and glass; motorhomes and caravans are modelled as insulated occupied living spaces that warm more slowly but hold heat longer, with gain through the windscreen and rooflights and a little retained living-space warmth. Accounts for ambient wind over the bodywork, road speed, and whether the windows are open. Dangerous for children and pets above 35 °C; potentially fatal above 45 °C.' },
|
||||||
indoorT: { title: 'Indoors', desc: 'Estimated ambient temperature inside a selected building type with windows closed and no air conditioning. Accounts for retained warmth, window solar gain, internal gains, and thermal lag without repeatedly accumulating solar heat hour after hour.' },
|
indoorT: { title: 'Indoors', desc: 'Estimated ambient temperature inside a selected building type with windows closed and no air conditioning. Accounts for retained warmth, window solar gain, internal gains, and thermal lag without repeatedly accumulating solar heat hour after hour.' },
|
||||||
managedT: { title: 'Managed Indoors', desc: 'Estimated indoor temperature with curtains closed and windows opened when outdoor air is cooler than inside — the standard UK heatwave advice. Curtains block most direct solar gain; smart ventilation pulls the temperature down during cooler periods.' },
|
managedT: { title: 'Managed Indoors', desc: 'Estimated indoor temperature with curtains closed and windows opened when outdoor air is cooler than inside — the standard UK heatwave advice. Curtains block most direct solar gain; smart ventilation pulls the temperature down during cooler periods.' },
|
||||||
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.' },
|
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.' },
|
||||||
|
|||||||
+121
-45
@@ -11,7 +11,8 @@
|
|||||||
// 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, speedMph)
|
// calcVehicleInteriorTemp(Ta, globalRad, solElev, vehicleType, ventilated, speedMph, windMps) equilibrium cabin temp
|
||||||
|
// calcVehicleInteriorTempPass(TaArr, globArr, elevArr, vaArr, vehicleType, ventilated, speedMph) lagged cabin temp
|
||||||
// calcShadeAirTemp(TaEnv, effRadEnv, va, elev, shade) per-env shade air temp
|
// calcShadeAirTemp(TaEnv, effRadEnv, va, elev, shade) per-env shade air temp
|
||||||
// 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 (-)
|
||||||
@@ -375,92 +376,115 @@ export function calcManagedIndoorTempPass(TaArr, globArr, elevArr, buildingType
|
|||||||
// fleet; dark paint ~0.10, silver/white ~0.40).
|
// fleet; dark paint ~0.10, silver/white ~0.40).
|
||||||
// Panel surface temp - conductive gain into cabin air.
|
// Panel surface temp - conductive gain into cabin air.
|
||||||
//
|
//
|
||||||
// 2. SIDE-WINDOW GLAZING GAIN (sun-angle dependent)
|
// 2. VERTICAL GLAZING GAIN (sun-angle dependent)
|
||||||
// When solar elevation is between ~10- and ~60-, the sun's rays
|
// When solar elevation is between ~10- and ~60-, the sun's rays
|
||||||
// cut through the side glass at an angle that allows significant
|
// cut through the side glass and windscreen at an angle that allows
|
||||||
// transmission into the cabin (rather than hitting the roof or
|
// significant transmission into the cabin. This warms the cabin air
|
||||||
// reflecting off at a shallow angle). This warms the cabin air
|
|
||||||
// but the occupant is modelled as NOT sitting in the beam -
|
// but the occupant is modelled as NOT sitting in the beam -
|
||||||
// so it adds to ambient cabin temp, not direct radiant load.
|
// so it adds to ambient cabin temp, not direct radiant load.
|
||||||
// Above 60- the sun mostly hits the roof; below 10- it reflects.
|
// Outside that window a diffuse floor still applies: scattered sky
|
||||||
|
// light enters the glass at any sun angle, so the tent function is
|
||||||
|
// clamped rather than switched off (an unclamped tent produced a
|
||||||
|
// discontinuity where a LOWER sun gave MORE glazing gain).
|
||||||
//
|
//
|
||||||
// Wind is ignored unless ventilation is enabled. No evaporative cooling.
|
// 3. HORIZONTAL GLAZING GAIN (rooflights / panoramic roofs)
|
||||||
|
// Rooflights collect most strongly when the sun is high - the exact
|
||||||
|
// condition under which the vertical-glass term is tailing off. Scales
|
||||||
|
// with sin(elevation) and uses a lower transmission (0.40) because
|
||||||
|
// rooflights are typically smoked/tinted acrylic rather than clear glass.
|
||||||
|
//
|
||||||
|
// Ambient wind scrubs the bodywork whether or not the windows are open, so
|
||||||
|
// it feeds the shell convection coefficient in every case; road speed does
|
||||||
|
// the same job and the larger of the two wins. No evaporative cooling.
|
||||||
// Cars warm quickly; motorhomes and caravans are treated as insulated living
|
// Cars warm quickly; motorhomes and caravans are treated as insulated living
|
||||||
// spaces with 25-35 mm sandwich panels, so panel heat gain is much smaller
|
// spaces with 25-35 mm sandwich panels, so panel heat gain is much smaller.
|
||||||
// and the interior response is slower than a car cabin. Because they are
|
// Their interiors are SLOWER, not COOLER - the insulation that keeps heat out
|
||||||
// occupied living spaces, they also retain warmth from previous hours, people,
|
// also keeps it in, so a closed-up van ends up about as hot as a closed-up car
|
||||||
// appliances, and background heating; without that, cool sunny days are
|
// once it has had a few hours to get there. Because they are occupied living
|
||||||
// under-estimated badly.
|
// spaces, they also retain a little warmth from previous hours, people,
|
||||||
|
// appliances, and background heating.
|
||||||
//
|
//
|
||||||
// Colour thresholds in the table:
|
// Colour thresholds in the table:
|
||||||
// < 35 -C - warm but tolerable for short periods
|
// < 35 -C - warm but tolerable for short periods
|
||||||
// 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, speedMph = 0) {
|
export function calcVehicleInteriorTemp(Ta, globalRad, solElev, vehicleType = 'car', ventilated = false, speedMph = 0, windMps = 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
|
// Road speed in m/s. speedMph = 0 is "Static" (parked); higher speeds scrub
|
||||||
// original stationary model exactly; higher speeds scrub the shell with
|
// the shell with forced airflow and (windows down) flush the cabin toward
|
||||||
// forced airflow and (windows down) flush the cabin toward ambient.
|
// ambient.
|
||||||
const vMs = Math.max(0, speedMph) * 0.447; // mph -> m/s
|
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.
|
||||||
// Parked, a light breeze over the panels gives hOut ~10 W/m-K. Once moving,
|
// Convection over the shell is driven by whichever airflow is stronger -
|
||||||
// forced convection rises with road speed (same sqrt form as the surface
|
// the ambient wind or the car's own road speed. A parked vehicle in a
|
||||||
// model), so the bodywork runs progressively closer to ambient.
|
// 15 mph wind has bodywork far closer to ambient than one in still air,
|
||||||
const hOut = 10 + 5.5 * Math.sqrt(vMs);
|
// so ambient wind must not be ignored just because the windows are shut.
|
||||||
|
// The 1.5 m/s floor is not fudge: hOut is steep near calm, and a reported
|
||||||
|
// wind of 0 m/s does not mean still air at the bodywork - thermal plumes off
|
||||||
|
// hot panels and ordinary gustiness keep it moving. Without the floor a dead
|
||||||
|
// calm hour sent a sealed car to ~67 -C. It only bites below ~3 mph, so it
|
||||||
|
// leaves the calibration anchors untouched.
|
||||||
|
const vShell = Math.max(1.5, vMs, Math.max(0, windMps || 0));
|
||||||
|
const hOut = 10 + 5.5 * Math.sqrt(vShell);
|
||||||
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.
|
||||||
const hCabin = preset.bodyU ?? 4;
|
const hCabin = preset.bodyU ?? 4;
|
||||||
const conductionGain = hCabin * (panelSurfaceTemp - Ta); // W/m-
|
const conductionGain = hCabin * (panelSurfaceTemp - Ta); // W/m-
|
||||||
|
|
||||||
// -- 2. Side-window glazing gain (angle-dependent) -----------------
|
// -- 2. Vertical glazing gain (angle-dependent) --------------------
|
||||||
// Glazing transmission for auto glass ~0.70; scaled by vehicle glazing area.
|
// Glazing transmission for auto glass ~0.70; scaled by vehicle glazing area.
|
||||||
|
// Tent function peaks at 35- elevation, where the sun cuts squarely through
|
||||||
|
// side glass and windscreen, and tapers either side. It is clamped at 0.30
|
||||||
|
// (= the 0.15 diffuse floor once the 0.5 not-in-beam factor is applied) so
|
||||||
|
// scattered sky light always gets in. Without that clamp the gain fell to
|
||||||
|
// zero at 10- and 60- and then jumped back up outside the range.
|
||||||
const tau = 0.70 * preset.glazingArea;
|
const tau = 0.70 * preset.glazingArea;
|
||||||
let glazingGain = 0;
|
const tent = (solElev != null && solElev > 0)
|
||||||
if (solElev != null && solElev > 10 && solElev < 60) {
|
? Math.max(0, 1 - Math.abs(solElev - 35) / 25)
|
||||||
// Scale factor: peaks around 30-40- elevation (sun cuts squarely
|
: 0;
|
||||||
// through side glass), tapers off toward 10- (shallow/reflected)
|
const glazingGain = tau * globalRad * Math.max(0.30, tent) * 0.5; // occupant not in beam
|
||||||
// and 60- (sun increasingly hitting roof not side glass).
|
|
||||||
// Use a simple tent function peaking at 35-.
|
// -- 3. Horizontal glazing gain (rooflights, panoramic roof) -------
|
||||||
const peak = 35;
|
// Rooflights collect in proportion to sin(elevation), so they peak at midday
|
||||||
const halfWidth = 25; // degrees either side
|
// just as the vertical-glass term is falling away. Smoked acrylic transmits
|
||||||
const factor = Math.max(0, 1 - Math.abs(solElev - peak) / halfWidth);
|
// roughly 0.40 rather than the 0.70 of clear auto glass.
|
||||||
// Diffuse radiation also enters through glass regardless of angle
|
const roofGain = (solElev != null && solElev > 0)
|
||||||
glazingGain = tau * globalRad * factor * 0.5; // occupant not in beam - 50% ambient
|
? 0.40 * (preset.roofGlazing ?? 0) * globalRad * Math.sin(solElev * Math.PI / 180)
|
||||||
} else {
|
: 0;
|
||||||
// Outside the side-window zone: diffuse only (scattered sky light)
|
|
||||||
glazingGain = tau * (globalRad * 0.15); // ~15% diffuse fraction
|
|
||||||
}
|
|
||||||
|
|
||||||
// -- Combine into cabin air temperature ---------------------------
|
// -- Combine into cabin air temperature ---------------------------
|
||||||
// Total heat input per m- of cabin surface
|
// Total heat input per m- of cabin surface
|
||||||
const totalGain = conductionGain + glazingGain;
|
const totalGain = conductionGain + glazingGain + roofGain;
|
||||||
// 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. Opening the windows when
|
||||||
// roughly 5x higher when parked - air moves freely through the cabin,
|
// parked multiplies it by the preset's ventMult - a car with every window
|
||||||
// flushing heat out and capping interior temperature much closer to ambient.
|
// down flushes far harder per unit volume than a van with two windows and a
|
||||||
|
// rooflight open, so that multiplier is per-vehicle, not a shared constant.
|
||||||
// On the move the through-draught multiplies this further (a 70 mph open
|
// On the move the through-draught multiplies this further (a 70 mph open
|
||||||
// window flushes the cabin almost to ambient). Sealed but moving rejects a
|
// window flushes the cabin almost to ambient). Sealed but moving rejects a
|
||||||
// little faster too, because the cooler shell pulls cabin heat out.
|
// little faster too, because the cooler shell pulls cabin heat out.
|
||||||
const speedFactor = 1 + vMs / 12; // grows with road speed (windows-open draught)
|
const speedFactor = 1 + vMs / 12; // grows with road speed (windows-open draught)
|
||||||
const lossMult = ventilated ? 5 * speedFactor : 1 + vMs / 40;
|
const lossMult = ventilated ? (preset.ventMult ?? 4) * speedFactor : 1 + vMs / 40;
|
||||||
const effectiveHLoss = preset.hCabinLoss * lossMult;
|
const effectiveHLoss = preset.hCabinLoss * lossMult;
|
||||||
const thermalMass = preset.thermalMass ?? 1;
|
const solarRise = totalGain / effectiveHLoss;
|
||||||
const solarRise = (totalGain / effectiveHLoss) * thermalMass;
|
|
||||||
|
|
||||||
// Motorhomes/caravans behave more like small insulated rooms than parked
|
// Motorhomes/caravans behave more like small insulated rooms than parked
|
||||||
// cars. This term captures retained living-space warmth: strongest on cool
|
// cars. This term captures retained living-space warmth: strongest on cool
|
||||||
// days, tapering away as outdoor air warms, and reduced when ventilated.
|
// days, tapering away as outdoor air warms, and reduced when ventilated.
|
||||||
|
// It used to be much larger (8 - 0.25*Ta) because it was silently standing
|
||||||
|
// in for solar gain the model was throwing away; now that the glazing terms
|
||||||
|
// are right it only has to cover occupancy and residual warmth.
|
||||||
const retainedWarmth = preset.retainedWarmth
|
const retainedWarmth = preset.retainedWarmth
|
||||||
? Math.max(0, 8 - 0.25 * Ta) * (ventilated ? 0.35 : 1)
|
? Math.max(0, 5 - 0.20 * Ta) * (ventilated ? 0.35 : 1)
|
||||||
: 0;
|
: 0;
|
||||||
const internalGain = (preset.internalGain ?? 0) * (ventilated ? 0.35 : 1);
|
const internalGain = (preset.internalGain ?? 0) * (ventilated ? 0.35 : 1);
|
||||||
|
|
||||||
@@ -470,6 +494,57 @@ export function calcVehicleInteriorTemp(Ta, globalRad, solElev, vehicleType = 'c
|
|||||||
return Math.max(Ta, Math.min(Ti, 90));
|
return Math.max(Ta, Math.min(Ti, 90));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
// VEHICLE INTERIOR TEMPERATURE - TWO-PASS (thermal lag)
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
// calcVehicleInteriorTemp above returns the EQUILIBRIUM cabin temperature
|
||||||
|
// for one hour's conditions - where the interior would settle if those
|
||||||
|
// conditions held. Real cabins take time to get there, so this pass relaxes
|
||||||
|
// toward that target with a per-vehicle time constant, exactly as
|
||||||
|
// calcConcreteTempPass and calcIndoorTempPass do.
|
||||||
|
//
|
||||||
|
// This replaces an earlier 'thermalMass' multiplier that scaled the
|
||||||
|
// equilibrium rise down (motorhomes ran at 0.35x). That conflated two
|
||||||
|
// different things: thermal mass delays how fast you reach equilibrium, it
|
||||||
|
// does not lower the equilibrium itself. A van parked in the sun since
|
||||||
|
// breakfast is close to equilibrium by mid-afternoon, so the multiplier
|
||||||
|
// under-predicted every long parked spell - badly enough that a ventilated
|
||||||
|
// motorhome came out barely a degree above ambient at peak sun.
|
||||||
|
//
|
||||||
|
// Cars use a short constant (~0.5 h - a car is hot within the half hour);
|
||||||
|
// motorhomes and caravans a longer one (~1.5-1.7 h) reflecting their larger
|
||||||
|
// air volume and heavier interior fit-out.
|
||||||
|
//
|
||||||
|
// vaArr is ambient wind in m/s (Open-Meteo wind_speed_10m with
|
||||||
|
// wind_speed_unit=ms) and may be null/omitted, in which case still air is
|
||||||
|
// assumed. speedMph is the selected road-speed class, not per-hour data.
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
export function calcVehicleInteriorTempPass(TaArr, globArr, elevArr, vaArr, vehicleType = 'car', ventilated = false, speedMph = 0) {
|
||||||
|
const preset = VEHICLE_TYPES[vehicleType] || VEHICLE_TYPES.car;
|
||||||
|
const n = TaArr.length;
|
||||||
|
const result = new Array(n);
|
||||||
|
const alpha = 1 - Math.exp(-1 / (preset.lagHours ?? 0.5));
|
||||||
|
|
||||||
|
// Seed at the first hour's air temp - a vehicle left overnight has
|
||||||
|
// equalised with the outside air.
|
||||||
|
let Ti = TaArr[0] ?? 15;
|
||||||
|
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
const Ta = TaArr[i];
|
||||||
|
if (Ta == null) { result[i] = null; continue; }
|
||||||
|
const target = calcVehicleInteriorTemp(
|
||||||
|
Ta, globArr[i] ?? 0, elevArr[i], vehicleType, ventilated, speedMph,
|
||||||
|
vaArr ? (vaArr[i] ?? 0) : 0
|
||||||
|
);
|
||||||
|
if (target == null) { result[i] = null; continue; }
|
||||||
|
Ti = Ti + alpha * (target - Ti);
|
||||||
|
// Can't be cooler than outside air; physical cap at 90 -C.
|
||||||
|
result[i] = Math.max(Ta, Math.min(Ti, 90));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
// SHADE AIR TEMPERATURE (per-environment microclimate)
|
// SHADE AIR TEMPERATURE (per-environment microclimate)
|
||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
@@ -576,7 +651,8 @@ export function calcFurSurfaceTempPass(TaArr, radArr, vaArr, elevArr, furAlbedo
|
|||||||
// calcVehicleInteriorTemp now takes a speedMph argument (Static / 20 / 50 / 70
|
// calcVehicleInteriorTemp now takes a speedMph argument (Static / 20 / 50 / 70
|
||||||
// mph in the UI). Forced convection over the shell scales with road speed, and
|
// 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
|
// 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.
|
// cooler than the same parked car. speedMph = 0 is the parked case - which is
|
||||||
|
// no longer "no airflow", since ambient wind now drives shell convection too.
|
||||||
//
|
//
|
||||||
// -- CYCLIST AT SPEED (FUTURE) ------------------------------------------------
|
// -- CYCLIST AT SPEED (FUTURE) ------------------------------------------------
|
||||||
// A cyclist generates their own headwind, so the felt temperature (UTCI) is
|
// A cyclist generates their own headwind, so the felt temperature (UTCI) is
|
||||||
|
|||||||
+22
-8
@@ -209,23 +209,37 @@ export function burnLabel(mins) {
|
|||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
// Each entry tweaks the physical levers in calcVehicleInteriorTemp:
|
// Each entry tweaks the physical levers in calcVehicleInteriorTemp:
|
||||||
// albedo - how much solar the bodywork reflects (0 = black, 1 = mirror)
|
// albedo - how much solar the bodywork reflects (0 = black, 1 = mirror)
|
||||||
// glazingArea - relative sun-exposed glass area (1.0 = typical car)
|
// glazingArea - relative VERTICAL glass area (1.0 = typical car). A coachbuilt
|
||||||
|
// motorhome has a huge near-vertical windscreen plus cab side
|
||||||
|
// windows, so it is far from the 0.25 once assumed here.
|
||||||
|
// roofGlazing - relative HORIZONTAL glass area (rooflights / Heki hatches /
|
||||||
|
// panoramic glass roof). Gains from these peak at high sun,
|
||||||
|
// which is exactly when the vertical-glass term is tailing off.
|
||||||
// bodyU - effective body/panel conductance into the cabin. Cars are
|
// bodyU - effective body/panel conductance into the cabin. Cars are
|
||||||
// thin metal/glass boxes; motorhomes/caravans have insulated
|
// thin metal/glass boxes; motorhomes/caravans have insulated
|
||||||
// sandwich panels, commonly around 25-35 mm thick.
|
// sandwich panels, commonly around 25-35 mm thick.
|
||||||
// hCabinLoss - effective heat rejection/infiltration from the cabin air.
|
// hCabinLoss - effective heat rejection/infiltration from the cabin air.
|
||||||
// thermalMass - lower values mean the interior warms more slowly in the hour.
|
// Motorhomes/caravans reject heat more SLOWLY than cars (better
|
||||||
|
// sealed, smaller aperture per unit volume), which is why a
|
||||||
|
// closed-up van gets as hot as a car despite its insulation.
|
||||||
|
// ventMult - how much opening the windows multiplies hCabinLoss when
|
||||||
|
// parked. A car with all windows down flushes far more
|
||||||
|
// effectively per unit volume than a van with two windows and
|
||||||
|
// a rooflight open, so this is not a shared constant.
|
||||||
|
// lagHours - interior thermal time constant. This is a DELAY on reaching
|
||||||
|
// the hour's equilibrium, not a reduction of it - see the note
|
||||||
|
// in calcVehicleInteriorTempPass.
|
||||||
// retainedWarmth - occupied insulated living spaces hold heat from previous
|
// retainedWarmth - occupied insulated living spaces hold heat from previous
|
||||||
// hours, people, appliances, and background heating.
|
// hours, people, appliances, and background heating.
|
||||||
// internalGain - small living-space warmth boost when closed up.
|
// internalGain - small living-space warmth boost when closed up.
|
||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
export const VEHICLE_TYPES = {
|
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.00, roofGlazing: 0.00, bodyU: 4.0, hCabinLoss: 11.0, ventMult: 4.0, lagHours: 0.5, 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.30, roofGlazing: 0.02, bodyU: 4.0, hCabinLoss: 11.5, ventMult: 4.0, lagHours: 0.6, 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.10, roofGlazing: 0.02, bodyU: 3.8, hCabinLoss: 11.0, ventMult: 4.0, lagHours: 0.6, 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 },
|
truck: { name: 'Truck / HGV Cab', albedo: 0.30, glazingArea: 1.20, roofGlazing: 0.00, bodyU: 3.5, hCabinLoss: 10.5, ventMult: 4.0, lagHours: 0.7, 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.85, roofGlazing: 0.06, bodyU: 0.9, hCabinLoss: 6.5, ventMult: 2.5, lagHours: 1.5, 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.55, roofGlazing: 0.07, bodyU: 0.8, hCabinLoss: 6.2, ventMult: 2.4, lagHours: 1.7, retainedWarmth: true, internalGain: 1.2 },
|
||||||
};
|
};
|
||||||
|
|
||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
|
|||||||
@@ -232,7 +232,8 @@ async function build() {
|
|||||||
|
|
||||||
// 8. Copy static files
|
// 8. Copy static files
|
||||||
console.log(' Copying static files...');
|
console.log(' Copying static files...');
|
||||||
for (const f of ['robots.txt', 'sitemap.xml', 'og-image.png', 'favicon.svg', 'restore.php', 'track.php', 'stats.php']) {
|
for (const f of ['robots.txt', 'sitemap.xml', 'og-image.png', 'favicon.svg', 'restore.php', 'track.php', 'stats.php',
|
||||||
|
'verify-session.php', 'check-subscription.php', 'dev-unlock.php', 'secrets.local.php']) {
|
||||||
const src = path.join(ROOT, f);
|
const src = path.join(ROOT, f);
|
||||||
if (fs.existsSync(src)) {
|
if (fs.existsSync(src)) {
|
||||||
copyFile(src, path.join(DIST, f));
|
copyFile(src, path.join(DIST, f));
|
||||||
|
|||||||
@@ -158,5 +158,18 @@
|
|||||||
"alltemps": 2,
|
"alltemps": 2,
|
||||||
"showall": 2
|
"showall": 2
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"2026-07-25": {
|
||||||
|
"visits": 7,
|
||||||
|
"profiles": {
|
||||||
|
"home": 10,
|
||||||
|
"vehicle": 7,
|
||||||
|
"pets": 5,
|
||||||
|
"basic": 5,
|
||||||
|
"alltemps": 3,
|
||||||
|
"showall": 1,
|
||||||
|
"custom": 1,
|
||||||
|
"farming": 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user