1.6.0
Fix and enhance the concrete calculation Minify the about.css file
This commit is contained in:
+1
-1
@@ -27,7 +27,7 @@
|
|||||||
<link rel="preconnect" href="https://fonts.bunny.net" />
|
<link rel="preconnect" href="https://fonts.bunny.net" />
|
||||||
<link href="https://fonts.bunny.net/css2?family=Fraunces:ital,wght@0,900;1,300&family=Manrope:wght@400;600&family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet" />
|
<link href="https://fonts.bunny.net/css2?family=Fraunces:ital,wght@0,900;1,300&family=Manrope:wght@400;600&family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet" />
|
||||||
|
|
||||||
<link rel="stylesheet" href="./assets/about.css">
|
<link rel="stylesheet" href="./assets/about.min.css">
|
||||||
|
|
||||||
<!-- Structured data for AEO / AI search -->
|
<!-- Structured data for AEO / AI search -->
|
||||||
<script type="application/ld+json">
|
<script type="application/ld+json">
|
||||||
|
|||||||
@@ -53,10 +53,11 @@
|
|||||||
margin-top: 14px;
|
margin-top: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* "↳ Pangbourne, Berkshire" */
|
/* "↳ Location, Somewhere" */
|
||||||
.utci-current-loc {
|
.utci-current-loc {
|
||||||
font-family: Fraunces, serif;
|
font-family: Fraunces, serif;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
|
font-weight: 800;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
color: #1e1208;
|
color: #1e1208;
|
||||||
@@ -70,7 +71,7 @@
|
|||||||
color: #b09870;
|
color: #b09870;
|
||||||
display: block;
|
display: block;
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
margin-left: 18px;
|
margin-left: 15px;
|
||||||
letter-spacing: 0.08em;
|
letter-spacing: 0.08em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+33
-13
@@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
vaporPressureHpa, solarElevationDeg, calcTmrt, utciApprox,
|
vaporPressureHpa, solarElevationDeg, calcTmrt, utciApprox,
|
||||||
calcConcreteTemp, calcVehicleInteriorTemp,
|
calcConcreteTempPass, calcVehicleInteriorTemp,
|
||||||
calcIndoorTempPass, calcManagedIndoorTempPass,
|
calcIndoorTempPass, calcManagedIndoorTempPass,
|
||||||
} from './physics.js';
|
} from './physics.js';
|
||||||
import { windCompass8, uvSplit, cloudCategory, precipPenalty } from './utils.js';
|
import { windCompass8, uvSplit, cloudCategory, precipPenalty } from './utils.js';
|
||||||
@@ -55,20 +55,23 @@ export function buildHourlyRows({ forecast, airQuality, location, vehicleType, v
|
|||||||
const soilT0 = h.soil_temperature_0cm ? h.soil_temperature_0cm[i] : null;
|
const soilT0 = h.soil_temperature_0cm ? h.soil_temperature_0cm[i] : null;
|
||||||
const soilT6 = h.soil_temperature_6cm ? h.soil_temperature_6cm[i] : null;
|
const soilT6 = h.soil_temperature_6cm ? h.soil_temperature_6cm[i] : null;
|
||||||
const soilM = h.soil_moisture_0_to_1cm ? h.soil_moisture_0_to_1cm[i] : null;
|
const soilM = h.soil_moisture_0_to_1cm ? h.soil_moisture_0_to_1cm[i] : null;
|
||||||
|
// iso is a local wall-clock string e.g. "2026-05-13T14:00" (no Z).
|
||||||
|
// For display we slice the string directly - no Date object needed.
|
||||||
|
// For solarElevationDeg (which uses .getUTC* internally) we need the
|
||||||
|
// true UTC instant: treat the local time as UTC then subtract the offset.
|
||||||
|
// e.g. Brisbane UTC+10: local 14:00 - parse as UTC 14:00 - subtract 10h - UTC 04:00 -
|
||||||
|
// iso is a local wall-clock string e.g. "2026-05-13T14:00" (no Z).
|
||||||
|
// For solarElevationDeg (which uses .getUTC* internally) we need the
|
||||||
|
// true UTC instant: treat the local time as UTC then subtract the offset.
|
||||||
|
const dt = new Date(Date.parse(iso + 'Z') - utcOffsetMs);
|
||||||
|
const elev = solarElevationDeg(location.lat, location.lon, dt);
|
||||||
// Use direct_radiation (beam sunlight) + a fraction of diffuse for concrete.
|
// Use direct_radiation (beam sunlight) + a fraction of diffuse for concrete.
|
||||||
// direct_radiation is zero on fully overcast days - far more accurate than
|
// direct_radiation is zero on fully overcast days - far more accurate than
|
||||||
// shortwave_radiation which can be unreliably high even at 100% cloud cover.
|
// shortwave_radiation which can be unreliably high even at 100% cloud cover.
|
||||||
// Diffuse (scattered light through cloud) contributes ~20% as much heat to
|
// Diffuse (scattered light through cloud) contributes ~20% as much heat to
|
||||||
// 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;
|
||||||
const concreteT = calcConcreteTemp(Ta, effectiveRad, va);
|
// concreteT is now stamped in the two-pass section below (thermal lag).
|
||||||
// iso is a local wall-clock string e.g. "2026-05-13T14:00" (no Z).
|
|
||||||
// For display we slice the string directly - no Date object needed.
|
|
||||||
// For solarElevationDeg (which uses .getUTC* internally) we need the
|
|
||||||
// true UTC instant: treat the local time as UTC then subtract the offset.
|
|
||||||
// e.g. Brisbane UTC+10: local 14:00 - parse as UTC 14:00 - subtract 10h - UTC 04:00 -
|
|
||||||
const dt = new Date(Date.parse(iso + 'Z') - utcOffsetMs);
|
|
||||||
const elev = solarElevationDeg(location.lat, location.lon, dt);
|
|
||||||
const vehicleT = calcVehicleInteriorTemp(Ta, glob, elev, vehicleType, vehicleVent);
|
const vehicleT = calcVehicleInteriorTemp(Ta, glob, elev, vehicleType, vehicleVent);
|
||||||
const eh = vaporPressureHpa(Ta, RH);
|
const eh = vaporPressureHpa(Ta, RH);
|
||||||
const Tmrt = calcTmrt(Ta, dir, dif, glob, elev);
|
const Tmrt = calcTmrt(Ta, dir, dif, glob, elev);
|
||||||
@@ -132,20 +135,37 @@ 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, snow,
|
precip, snow,
|
||||||
soilT0, soilT6, soilM, concreteT, vehicleT,
|
soilT0, soilT6, soilM, vehicleT,
|
||||||
|
effectiveRad,
|
||||||
elev, Tmrt, utci, utciAdj, eh, compass,
|
elev, Tmrt, utci, utciAdj, eh, compass,
|
||||||
visKm, aqi,
|
visKm, aqi,
|
||||||
grassPollen, birchPollen, alderPollen, mugwortPollen, olivePollen, ragweedPollen,
|
grassPollen, birchPollen, alderPollen, mugwortPollen, olivePollen, ragweedPollen,
|
||||||
};
|
};
|
||||||
}) : [];
|
}) : [];
|
||||||
|
|
||||||
// Two-pass indoor temperature: needs the full hourly arrays so thermal
|
// Two-pass calculations: concrete thermal lag + indoor temperature.
|
||||||
// lag can look back at previous hours. Run after hourlyRows is built,
|
// Both need the full hourly arrays so they can look back at previous
|
||||||
// then stamp each row with its indoorT value.
|
// 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);
|
||||||
const globArr = hourlyRows.map(r => r.glob);
|
const globArr = hourlyRows.map(r => r.glob);
|
||||||
const elevArr = hourlyRows.map(r => r.elev);
|
const elevArr = hourlyRows.map(r => r.elev);
|
||||||
|
const radArr = hourlyRows.map(r => r.effectiveRad);
|
||||||
|
const vaArr = hourlyRows.map(r => r.va);
|
||||||
|
const uvArr = hourlyRows.map(r => r.uv);
|
||||||
|
const cloudCatArr = hourlyRows.map(r => r.cloudCat);
|
||||||
|
const soilMArr = hourlyRows.map(r => r.soilM);
|
||||||
|
const precipArr = hourlyRows.map(r => r.precip);
|
||||||
|
const snowArr = hourlyRows.map(r => r.snow);
|
||||||
|
|
||||||
|
// Concrete surface temperature with thermal lag (1.5 h time constant).
|
||||||
|
// A slab baking in the sun retains heat when cloud rolls in, and takes
|
||||||
|
// a couple of hours of sunshine to fully heat up from a cold start.
|
||||||
|
const concreteTemps = calcConcreteTempPass(
|
||||||
|
TaArr, radArr, vaArr, elevArr, uvArr, cloudCatArr, soilMArr, precipArr, snowArr
|
||||||
|
);
|
||||||
|
hourlyRows.forEach((r, i) => { r.concreteT = concreteTemps[i]; });
|
||||||
|
|
||||||
const indoorTemps = calcIndoorTempPass(TaArr, globArr, elevArr, buildingType);
|
const indoorTemps = calcIndoorTempPass(TaArr, globArr, elevArr, buildingType);
|
||||||
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]; });
|
||||||
|
|||||||
+181
-16
@@ -6,7 +6,9 @@
|
|||||||
//
|
//
|
||||||
// Exports (in order of appearance):
|
// Exports (in order of appearance):
|
||||||
// SIGMA, EPSILON_P, A_K, ALBEDO_GRASS, ALBEDO_CONCRETE (radiation constants)
|
// SIGMA, EPSILON_P, A_K, ALBEDO_GRASS, ALBEDO_CONCRETE (radiation constants)
|
||||||
// calcConcreteTemp(Ta, globalRad, windSpeed) urban surface temp
|
// LAG_HOURS_CONCRETE slab thermal time constant
|
||||||
|
// calcConcreteTemp(Ta, globalRad, windSpeed, ...) urban surface temp (instantaneous target)
|
||||||
|
// 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)
|
||||||
@@ -42,26 +44,189 @@ export const ALBEDO_CONCRETE = 0.30;
|
|||||||
// CONCRETE SURFACE TEMPERATURE (Urban profile)
|
// CONCRETE SURFACE TEMPERATURE (Urban profile)
|
||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
// Estimates the surface temperature of exposed concrete using a
|
// Estimates the surface temperature of exposed concrete using a
|
||||||
// simplified energy-balance approach:
|
// multi-factor energy-balance approach. Six physical effects are
|
||||||
// - Absorbed solar = globalRad - (1 - albedo)
|
// modelled beyond the basic solar-gain / convective-loss pair:
|
||||||
// - No latent heat (no evaporation - concrete is dry)
|
|
||||||
// - Convective loss to air proportional to wind speed
|
|
||||||
// - Result is clamped to a physically plausible range
|
|
||||||
//
|
//
|
||||||
// This is what matters for contact heat stress in cities - the UTCI
|
// 1. SUN ANGLE CORRECTION
|
||||||
// standard uses grass, which runs ~5-15 -C cooler than urban concrete
|
// At low solar elevation the sun hits the surface obliquely,
|
||||||
// on a sunny day because grass sweats (transpires).
|
// spreading energy over a larger area. A sin(elev) factor
|
||||||
|
// reduces absorbed solar proportionally. Clamped at a 5-deg
|
||||||
|
// minimum so the result stays finite near the horizon.
|
||||||
|
//
|
||||||
|
// 2. CLOUD TYPE TRANSMITTANCE
|
||||||
|
// Cloud cover category drives a transmittance multiplier.
|
||||||
|
// Low cloud (stratus) is far more opaque than high cirrus:
|
||||||
|
// clear 1.00 - full beam reaches the surface
|
||||||
|
// wispy 0.92 - cirrus barely attenuates
|
||||||
|
// scattered 0.72 - broken cumulus, significant blocking
|
||||||
|
// overcast 0.28 - thick stratus, mostly diffuse remains
|
||||||
|
// The raw effectiveRad already dims with cloud cover from the
|
||||||
|
// API, but this adds the qualitative distinction between cloud
|
||||||
|
// types that the single radiation number does not capture.
|
||||||
|
//
|
||||||
|
// 3. UV CLARITY FACTOR
|
||||||
|
// UV index is a proxy for atmospheric clarity beyond cloud cover -
|
||||||
|
// aerosols, haze, and humidity all reduce it. A UV of 8+ indicates
|
||||||
|
// a very clean, dry atmosphere with maximum direct-beam intensity.
|
||||||
|
// Normalised to a 0.85-1.00 range so it modulates rather than
|
||||||
|
// dominates. No UV data defaults to neutral (1.0).
|
||||||
|
//
|
||||||
|
// 4. EVAPORATIVE COOLING FROM SOIL MOISTURE
|
||||||
|
// Wet concrete loses heat via evaporation. Soil moisture at 0-1cm
|
||||||
|
// is used as a proxy for surface wetness (0 = bone dry, 1 = fully
|
||||||
|
// saturated). A saturated surface loses up to ~8-C relative to
|
||||||
|
// the dry case - consistent with published wet-pavement studies.
|
||||||
|
//
|
||||||
|
// 5. RAIN-WET SURFACE
|
||||||
|
// Active precipitation forces surface wetness regardless of soil
|
||||||
|
// moisture data. Above 0.5 mm/h the surface is considered fully
|
||||||
|
// wet and the maximum evaporative penalty applies.
|
||||||
|
//
|
||||||
|
// 6. SNOW COVER
|
||||||
|
// Snow on concrete insulates the slab from solar gain AND strongly
|
||||||
|
// reflects incoming radiation (albedo ~0.80 for fresh snow vs 0.30
|
||||||
|
// for bare concrete). When snowfall is active or lying snow is
|
||||||
|
// implied (snow > 0), absorbed radiation is cut by 85% and a small
|
||||||
|
// insulating offset is applied instead.
|
||||||
|
//
|
||||||
|
// Colour thresholds (same as before - surface contact risk):
|
||||||
|
// < Ta - should not occur (clamped)
|
||||||
|
// Ta - 45 -C - warm but bearable contact
|
||||||
|
// 45 - 60 -C - pain threshold for bare skin contact
|
||||||
|
// > 60 -C - burns on contact (relevant for paws / bare feet)
|
||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
export function calcConcreteTemp(Ta, globalRad, windSpeed) {
|
export function calcConcreteTemp(Ta, globalRad, windSpeed, solElev, uv, cloudCat, soilM, precip, snow) {
|
||||||
if (globalRad == null || Ta == null) return null;
|
if (globalRad == null || Ta == null) return null;
|
||||||
const absorbed = globalRad * (1 - ALBEDO_CONCRETE); // W/m-
|
|
||||||
// Convective heat transfer coefficient: ~10 W/m-K still air, rises with wind
|
// -- 6. Snow short-circuit --------------------------------------------
|
||||||
// (10 reflects realistic natural convection; 5 was too low and ran too hot)
|
// Snow-covered concrete behaves like a white reflective insulator.
|
||||||
|
// Absorbed solar collapses; slab temp stays close to air temp.
|
||||||
|
if (snow != null && snow > 0) {
|
||||||
|
const snowAbsorbed = globalRad * (1 - 0.80); // fresh snow albedo ~0.80
|
||||||
|
const hcSnow = 10 + 4.5 * Math.sqrt(Math.max(windSpeed || 0, 0));
|
||||||
|
const Ts = Ta + snowAbsorbed / hcSnow;
|
||||||
|
return Math.max(Ta - 1, Math.min(Ts, 40)); // snow-covered slab rarely exceeds 40-C
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- 1. Sun angle correction ------------------------------------------
|
||||||
|
// Low-angle sun spreads energy across a larger surface area.
|
||||||
|
// sin(elev) = 1.0 at 90-deg (overhead), ~0.17 at 10-deg (grazing).
|
||||||
|
// Default to sin(45-deg) ~0.71 when elevation is unknown.
|
||||||
|
const elevDeg = (solElev != null) ? Math.max(5, solElev) : 45;
|
||||||
|
const angleCorrection = Math.sin(elevDeg * Math.PI / 180);
|
||||||
|
|
||||||
|
// -- 2. Cloud type transmittance --------------------------------------
|
||||||
|
// Modulates beam quality beyond what raw radiation already captures.
|
||||||
|
const cloudTransmit = cloudCat === 'clear' ? 1.00
|
||||||
|
: cloudCat === 'wispy' ? 0.92
|
||||||
|
: cloudCat === 'scattered' ? 0.72
|
||||||
|
: /* overcast */ 0.28;
|
||||||
|
|
||||||
|
// -- 3. UV clarity factor --------------------------------------------
|
||||||
|
// UV index as atmospheric-clarity proxy. Scaled to 0.85-1.00 range.
|
||||||
|
// uv=0 (night or heavy cloud) - neutral 1.0 (no adjustment needed,
|
||||||
|
// radiation already near-zero). uv=8+ - max clarity bonus of 1.0.
|
||||||
|
const uvClarity = (uv && uv > 0)
|
||||||
|
? 0.85 + 0.15 * Math.min(1, uv / 8)
|
||||||
|
: 1.0;
|
||||||
|
|
||||||
|
// -- Absorbed solar with all modifiers --------------------------------
|
||||||
|
const absorbed = globalRad * (1 - ALBEDO_CONCRETE)
|
||||||
|
* angleCorrection
|
||||||
|
* cloudTransmit
|
||||||
|
* uvClarity;
|
||||||
|
|
||||||
|
// -- Convective loss --------------------------------------------------
|
||||||
const hc = 10 + 4.5 * Math.sqrt(Math.max(windSpeed || 0, 0));
|
const hc = 10 + 4.5 * Math.sqrt(Math.max(windSpeed || 0, 0));
|
||||||
// Surface temp: Ta + solar gain / convective loss
|
|
||||||
|
// -- Dry surface temperature ------------------------------------------
|
||||||
const Ts = Ta + absorbed / hc;
|
const Ts = Ta + absorbed / hc;
|
||||||
// Clamp: can't be cooler than air, cap at 85 -C (melting asphalt territory)
|
|
||||||
return Math.max(Ta, Math.min(Ts, 85));
|
// -- 4 + 5. Evaporative cooling ---------------------------------------
|
||||||
|
// Rain-wet surface overrides soil moisture - surface is fully saturated.
|
||||||
|
const surfaceWet = (precip != null && precip >= 0.5)
|
||||||
|
? 1.0
|
||||||
|
: Math.max(0, Math.min(1, soilM ?? 0));
|
||||||
|
// Max evaporative delta ~8-C at full saturation (published wet-pavement data).
|
||||||
|
const evapCooling = surfaceWet * 8;
|
||||||
|
|
||||||
|
const TsCooled = Ts - evapCooling;
|
||||||
|
|
||||||
|
// -- Final clamp: no cooler than air, no hotter than 85-C -------------
|
||||||
|
return Math.max(Ta, Math.min(TsCooled, 85));
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
// CONCRETE THERMAL LAG TIME CONSTANT
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
// A standard urban pavement slab (~100 mm thick, exposed top surface,
|
||||||
|
// air below via sub-base) has moderate thermal mass. Real-world
|
||||||
|
// measurement studies put the e-folding time constant at 1.5-2 h for
|
||||||
|
// this geometry - we use 1.5 h as representative of the thin end of
|
||||||
|
// typical footway construction (block paving, tarmac-over-hardcore).
|
||||||
|
//
|
||||||
|
// If this ever needs to vary by surface type, pull it into a
|
||||||
|
// SURFACE_TYPES preset object mirroring BUILDING_TYPES in utils.js.
|
||||||
|
// For now a single named constant keeps the intent obvious.
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
export const LAG_HOURS_CONCRETE = 1.5;
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
// CONCRETE SURFACE TEMPERATURE - THERMAL LAG PASS
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
// Wraps calcConcreteTemp in the same exponential-blending pattern used
|
||||||
|
// by calcIndoorTempPass. Instead of snapping to the instantaneous
|
||||||
|
// target each hour, the slab temperature blends toward it at a rate
|
||||||
|
// controlled by LAG_HOURS_CONCRETE.
|
||||||
|
//
|
||||||
|
// Effect in practice:
|
||||||
|
// - A slab baking at 55-C when cloud rolls in will still read ~48-C
|
||||||
|
// an hour later and ~44-C two hours later - not instantly 22-C.
|
||||||
|
// - A cold slab at dawn takes 2-3 hours of strong sun to fully heat.
|
||||||
|
// - Post-rain cool-down persists into the next hour even if it stops.
|
||||||
|
//
|
||||||
|
// Call AFTER building hourlyRows (same pattern as calcIndoorTempPass).
|
||||||
|
// Returns a Float64-like plain Array of concrete temps, one per hour.
|
||||||
|
//
|
||||||
|
// Arguments are parallel arrays (one value per forecast hour):
|
||||||
|
// TaArr - air temperature (-C)
|
||||||
|
// radArr - effectiveRad (dir + dif*0.2) (W/m-)
|
||||||
|
// vaArr - wind speed (m/s)
|
||||||
|
// elevArr - solar elevation (degrees)
|
||||||
|
// uvArr - UV index
|
||||||
|
// cloudCatArr - cloud category string
|
||||||
|
// soilMArr - soil moisture 0-1cm (0-1 fraction)
|
||||||
|
// precipArr - precipitation (mm/h)
|
||||||
|
// snowArr - snowfall (cm/h)
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
export function calcConcreteTempPass(TaArr, radArr, vaArr, elevArr, uvArr, cloudCatArr, soilMArr, precipArr, snowArr) {
|
||||||
|
const n = TaArr.length;
|
||||||
|
const result = new Array(n);
|
||||||
|
const alpha = 1 - Math.exp(-1 / LAG_HOURS_CONCRETE);
|
||||||
|
|
||||||
|
// Seed from the first hours instantaneous value so we start somewhere
|
||||||
|
// physically reasonable rather than zero.
|
||||||
|
let Tc = calcConcreteTemp(
|
||||||
|
TaArr[0], radArr[0], vaArr[0],
|
||||||
|
elevArr[0], uvArr[0], cloudCatArr[0],
|
||||||
|
soilMArr[0], precipArr[0], snowArr[0]
|
||||||
|
) ?? TaArr[0];
|
||||||
|
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
const target = calcConcreteTemp(
|
||||||
|
TaArr[i], radArr[i], vaArr[i],
|
||||||
|
elevArr[i], uvArr[i], cloudCatArr[i],
|
||||||
|
soilMArr[i], precipArr[i], snowArr[i]
|
||||||
|
) ?? TaArr[i];
|
||||||
|
|
||||||
|
// Blend slab temp toward this hours target.
|
||||||
|
Tc = Tc + alpha * (target - Tc);
|
||||||
|
|
||||||
|
// Slab cannot be cooler than air (no active cooling mechanism).
|
||||||
|
result[i] = Math.max(TaArr[i], Tc);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ function cleanDist() {
|
|||||||
path.join(DIST, 'index.html'),
|
path.join(DIST, 'index.html'),
|
||||||
path.join(DIST, 'about.html'),
|
path.join(DIST, 'about.html'),
|
||||||
path.join(ASSETS, '_tmp_flat.css'),
|
path.join(ASSETS, '_tmp_flat.css'),
|
||||||
|
path.join(ASSETS, 'about.min.css'),
|
||||||
];
|
];
|
||||||
for (const f of targets) {
|
for (const f of targets) {
|
||||||
try { fs.unlinkSync(f); } catch (_) { /* ok if missing */ }
|
try { fs.unlinkSync(f); } catch (_) { /* ok if missing */ }
|
||||||
@@ -145,12 +146,22 @@ async function build() {
|
|||||||
console.log(' -> ' + htmlFile);
|
console.log(' -> ' + htmlFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Copy standalone CSS files that are not part of the main bundle
|
// 5. Minify standalone CSS files that are not part of the main bundle
|
||||||
for (const f of ['about.css']) {
|
// src name -> output name
|
||||||
const src = path.join(ROOT, 'assets', f);
|
const standaloneCSS = [['about.css', 'about.min.css']];
|
||||||
|
for (const [srcName, outName] of standaloneCSS) {
|
||||||
|
const src = path.join(ROOT, 'assets', srcName);
|
||||||
if (fs.existsSync(src)) {
|
if (fs.existsSync(src)) {
|
||||||
copyFile(src, path.join(ASSETS, f));
|
fs.mkdirSync(ASSETS, { recursive: true });
|
||||||
console.log(' -> assets/' + f);
|
await esbuild.build({
|
||||||
|
entryPoints: [src],
|
||||||
|
bundle: false,
|
||||||
|
minify: true,
|
||||||
|
outfile: path.join(ASSETS, outName),
|
||||||
|
logLevel: 'warning',
|
||||||
|
});
|
||||||
|
const sz = fs.statSync(path.join(ASSETS, outName)).size;
|
||||||
|
console.log(' -> assets/' + outName + ' (' + (sz / 1024).toFixed(1) + ' KB minified)');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Generated
+1
-1
@@ -8,7 +8,7 @@
|
|||||||
"name": "sunscope",
|
"name": "sunscope",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"esbuild": "^0.21.0"
|
"esbuild": "^0.21.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/aix-ppc64": {
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
|
|||||||
+1
-1
@@ -8,6 +8,6 @@
|
|||||||
"build:watch": "node --watch build.js"
|
"build:watch": "node --watch build.js"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"esbuild": "^0.21.0"
|
"esbuild": "^0.21.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user