3.5.9
Updated pet calcs to be more accurate Fixed some layout issues Updated profile popup
This commit is contained in:
+63
-13
@@ -383,6 +383,17 @@
|
|||||||
cursor: help;
|
cursor: help;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Hidden reference copy of the Activities tab (see ConfigPanel.js) — stays
|
||||||
|
in normal flow so its natural width sets the dropdown's fixed size, but
|
||||||
|
is never seen or reachable. */
|
||||||
|
.profile-scroll-sizer {
|
||||||
|
visibility: hidden;
|
||||||
|
height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.profile-scroll-sizer .profile-scroll { pointer-events: none; }
|
||||||
|
|
||||||
/* Single scrollable row of profile cards */
|
/* Single scrollable row of profile cards */
|
||||||
.profile-scroll {
|
.profile-scroll {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -586,11 +597,9 @@
|
|||||||
visibility 0s linear 0s;
|
visibility 0s linear 0s;
|
||||||
}
|
}
|
||||||
.profile-dropdown-body {
|
.profile-dropdown-body {
|
||||||
max-width: 900px;
|
|
||||||
margin: 0 auto;
|
|
||||||
max-height: calc(100vh - 24px);
|
max-height: calc(100vh - 24px);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 12px 24px 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
.profile-dropdown-close {
|
.profile-dropdown-close {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -618,6 +627,10 @@
|
|||||||
top: var(--profile-anchor-top, 0px);
|
top: var(--profile-anchor-top, 0px);
|
||||||
bottom: auto;
|
bottom: auto;
|
||||||
max-height: calc(100vh - var(--profile-anchor-top, 0px) - 16px);
|
max-height: calc(100vh - var(--profile-anchor-top, 0px) - 16px);
|
||||||
|
width: fit-content;
|
||||||
|
min-width: 380px;
|
||||||
|
max-width: min(880px, calc(100vw - 32px));
|
||||||
|
margin: 0 auto;
|
||||||
border: 1.5px solid #c9b08a;
|
border: 1.5px solid #c9b08a;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
overflow: hidden; /* keeps the scrolling body inside the rounded corners */
|
overflow: hidden; /* keeps the scrolling body inside the rounded corners */
|
||||||
@@ -663,7 +676,7 @@
|
|||||||
border: 2px solid #d4c0a0;
|
border: 2px solid #d4c0a0;
|
||||||
border-radius: 9px;
|
border-radius: 9px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
box-shadow: inset 0 -1px 0 rgba(0,0,0,0.05), 0 1px 3px rgba(0,0,0,0.04);
|
box-shadow: inset 0 -1px 0 rgba(0,0,0,0.05), 0 2px 5px rgba(0,0,0,0.08);
|
||||||
transition: filter 0.2s, border-color 0.2s, box-shadow 0.2s;
|
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
|
||||||
@@ -724,13 +737,18 @@
|
|||||||
letter-spacing: 0.04em;
|
letter-spacing: 0.04em;
|
||||||
color: #9a6a1a;
|
color: #9a6a1a;
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
opacity: 0.7;
|
padding: 4px 10px;
|
||||||
transition: opacity 0.2s, transform 0.2s, color 0.2s;
|
background: rgba(200,146,42,0.12);
|
||||||
|
border: 1px solid rgba(200,146,42,0.35);
|
||||||
|
border-radius: 999px;
|
||||||
|
transition: background 0.2s, border-color 0.2s, color 0.2s, transform 0.2s;
|
||||||
}
|
}
|
||||||
.active-profile-row:hover .active-profile-row-edit,
|
.active-profile-row:hover .active-profile-row-edit,
|
||||||
.active-profile-row:focus-visible .active-profile-row-edit {
|
.active-profile-row:focus-visible .active-profile-row-edit {
|
||||||
opacity: 1;
|
|
||||||
color: #c8922a;
|
color: #c8922a;
|
||||||
|
background: rgba(200,146,42,0.2);
|
||||||
|
border-color: #c8922a;
|
||||||
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Border + text accent per thermal-model group, reusing the same colour
|
/* Border + text accent per thermal-model group, reusing the same colour
|
||||||
@@ -1108,9 +1126,12 @@
|
|||||||
z-index: 6; /* above sticky header (z-index 5) */
|
z-index: 6; /* above sticky header (z-index 5) */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Left fade — warm cream fading to transparent rightward */
|
/* Left fade — warm cream fading to transparent rightward.
|
||||||
|
Offset by the pinned Hour column's width (--hour-col-w, set by JS in
|
||||||
|
useTableScroll.js) so the fade sits over the first column that
|
||||||
|
actually scrolls, not over the Hour column itself. */
|
||||||
.utci-table-wrap::before {
|
.utci-table-wrap::before {
|
||||||
left: 0;
|
left: var(--hour-col-w, 0px);
|
||||||
background: linear-gradient(to right, rgba(240,228,196,0.92) 0%, transparent 100%);
|
background: linear-gradient(to right, rgba(240,228,196,0.92) 0%, transparent 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1137,7 +1158,7 @@
|
|||||||
transition: opacity 0.2s ease;
|
transition: opacity 0.2s ease;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
.utci-table-wrap .utci-scroll-chevron.left { left: 6px; }
|
.utci-table-wrap .utci-scroll-chevron.left { left: calc(var(--hour-col-w, 0px) + 6px); }
|
||||||
.utci-table-wrap .utci-scroll-chevron.right { right: 6px; }
|
.utci-table-wrap .utci-scroll-chevron.right { right: 6px; }
|
||||||
|
|
||||||
.utci-table-wrap.scroll-fade-left .utci-scroll-chevron.left { opacity: 1; }
|
.utci-table-wrap.scroll-fade-left .utci-scroll-chevron.left { opacity: 1; }
|
||||||
@@ -1255,6 +1276,34 @@
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── PINNED HOUR COLUMN ──────────────────────────────────────────────────
|
||||||
|
The Hour/time column stays put while the rest of the table scrolls
|
||||||
|
horizontally, mirroring the fixed label column on the day tabs strip.
|
||||||
|
The body scroller genuinely scrolls (overflow-x: auto) so a plain
|
||||||
|
sticky left:0 works there. The header track instead gets shifted by
|
||||||
|
JS via translate3d to mirror the body's scrollLeft - so its first
|
||||||
|
cells get an equal-and-opposite translate3d applied by JS (see
|
||||||
|
handleBodyScroll in useTableScroll.js) to cancel that shift and stay
|
||||||
|
visually pinned at the left edge. */
|
||||||
|
.utci-table tbody td:first-child {
|
||||||
|
position: sticky;
|
||||||
|
left: 0;
|
||||||
|
z-index: 2;
|
||||||
|
background: #fffcf2;
|
||||||
|
}
|
||||||
|
.utci-table tbody tr:hover td:first-child {
|
||||||
|
background: #fef8ea;
|
||||||
|
}
|
||||||
|
.utci-table tbody tr.is-now td:first-child {
|
||||||
|
background: #fff8e4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.utci-table thead th:first-child {
|
||||||
|
position: relative;
|
||||||
|
z-index: 3;
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
/* The "°C" / "m/s" etc. unit underneath each heading */
|
/* The "°C" / "m/s" etc. unit underneath each heading */
|
||||||
.utci-table th .col-unit {
|
.utci-table th .col-unit {
|
||||||
display: block;
|
display: block;
|
||||||
@@ -1965,6 +2014,7 @@
|
|||||||
letter-spacing: 0.07em;
|
letter-spacing: 0.07em;
|
||||||
color: #7a5c2a;
|
color: #7a5c2a;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.forecast-view-toggle {
|
.forecast-view-toggle {
|
||||||
@@ -1980,7 +2030,7 @@
|
|||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
padding: 6px 14px;
|
padding: 6px 8px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border: 1.5px solid #c9b08a;
|
border: 1.5px solid #c9b08a;
|
||||||
background: #f5edd6;
|
background: #f5edd6;
|
||||||
@@ -2021,7 +2071,7 @@
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr auto 1fr;
|
grid-template-columns: 1fr auto 1fr;
|
||||||
align-items: end;
|
align-items: end;
|
||||||
padding: 0 14px;
|
padding: 0;
|
||||||
}
|
}
|
||||||
.fvt-toolbar-left {
|
.fvt-toolbar-left {
|
||||||
grid-column: 1;
|
grid-column: 1;
|
||||||
@@ -2050,7 +2100,7 @@
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
justify-items: center;
|
justify-items: center;
|
||||||
row-gap: 6px;
|
row-gap: 6px;
|
||||||
padding: 6px 10px 0;
|
padding: 6px 0 10px;
|
||||||
}
|
}
|
||||||
.fvt-toolbar-left {
|
.fvt-toolbar-left {
|
||||||
grid-column: 1;
|
grid-column: 1;
|
||||||
|
|||||||
@@ -269,6 +269,24 @@ export function ConfigPanel({
|
|||||||
>${label}</button>
|
>${label}</button>
|
||||||
`)}
|
`)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Invisible, always-rendered copy of the Activities cards. It has no
|
||||||
|
visual presence (height:0, hidden) but stays in normal flow so its
|
||||||
|
natural width feeds the dropdown's fit-content sizing — this keeps
|
||||||
|
the popup a constant width across all four tabs instead of
|
||||||
|
resizing as the user switches tabs, using Activities (the widest
|
||||||
|
tab) as that fixed reference size. -->
|
||||||
|
<div class="profile-scroll-sizer" aria-hidden="true">
|
||||||
|
<div class="profile-scroll">
|
||||||
|
${activityOptions.map((opt) => html`
|
||||||
|
<span key=${'sizer-' + opt.value} class="profile-btn" tabIndex="-1">
|
||||||
|
<span class="profile-btn-scene"></span>
|
||||||
|
<span class="profile-btn-label">${opt.name}</span>
|
||||||
|
</span>
|
||||||
|
`)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="profile-scroll-wrap" ref=${profileWrapRef}>
|
<div class="profile-scroll-wrap" ref=${profileWrapRef}>
|
||||||
<span class="profile-scroll-chevron left" aria-hidden="true">‹</span>
|
<span class="profile-scroll-chevron left" aria-hidden="true">‹</span>
|
||||||
<span class="profile-scroll-chevron right" aria-hidden="true">›</span>
|
<span class="profile-scroll-chevron right" aria-hidden="true">›</span>
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
|
|
||||||
import { h, Fragment } from '../../vendor/preact.js';
|
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, petCategory, 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 { calcVehicleInteriorTempPass } 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';
|
||||||
@@ -134,7 +134,7 @@ export function DayTabs({
|
|||||||
// only need the derived main metric to compute the day-tab numbers and to
|
// only need the derived main metric to compute the day-tab numbers and to
|
||||||
// label the "what temps am I looking at" row below.
|
// label the "what temps am I looking at" row below.
|
||||||
const {
|
const {
|
||||||
isHomeOrOffice, isVehicleProfile, mainConfigKey, mainField, secondaryField, mainLabel,
|
isHomeOrOffice, isVehicleProfile, isPetsProfile, mainConfigKey, mainField, secondaryField, mainLabel,
|
||||||
mainMetricLabel, mainMetricDesc, secondaryMetricLabel, secondaryMetricDesc,
|
mainMetricLabel, mainMetricDesc, secondaryMetricLabel, secondaryMetricDesc,
|
||||||
} = deriveProfileMain(activeProfile, outdoorsVariant);
|
} = deriveProfileMain(activeProfile, outdoorsVariant);
|
||||||
|
|
||||||
@@ -302,12 +302,18 @@ export function DayTabs({
|
|||||||
// • Otherwise rain wins, then cloud, then a clear "sunny"/cold colour.
|
// • Otherwise rain wins, then cloud, then a clear "sunny"/cold colour.
|
||||||
const mix = (a, b, t) => a.map((v, i) => Math.round(v + (b[i] - v) * Math.max(0, Math.min(1, t))));
|
const mix = (a, b, t) => a.map((v, i) => Math.round(v + (b[i] - v) * Math.max(0, Math.min(1, t))));
|
||||||
const hex2rgb = (hx) => { const n = parseInt(hx.replace('#', ''), 16); return [(n >> 16) & 255, (n >> 8) & 255, n & 255]; };
|
const hex2rgb = (hx) => { const n = parseInt(hx.replace('#', ''), 16); return [(n >> 16) & 255, (n >> 8) & 255, n & 255]; };
|
||||||
const tBand = dayHi !== null ? utciCategory(dayHi) : { bg: '#f8e554' };
|
const catFn = isPetsProfile ? petCategory : utciCategory;
|
||||||
|
const tBand = dayHi !== null ? catFn(dayHi) : { bg: '#f8e554' };
|
||||||
|
|
||||||
const ccVals = repRows.map(r => r.cc).filter(v => isFinite(v));
|
const ccVals = repRows.map(r => r.cc).filter(v => isFinite(v));
|
||||||
const avgCloud = ccVals.length ? ccVals.reduce((s, v) => s + v, 0) / ccVals.length : 0;
|
const avgCloud = ccVals.length ? ccVals.reduce((s, v) => s + v, 0) / ccVals.length : 0;
|
||||||
|
|
||||||
const isHot = dayHi !== null && dayHi >= 29; // Hot band and above
|
// Hot/cold cutoffs below are calibrated against UTCI_BANDS; for the
|
||||||
|
// Pets profile (furSurfaceT, judged against the shifted PET_BANDS)
|
||||||
|
// the same cutoffs are re-projected onto the pet scale so the
|
||||||
|
// "hot hue" / "plain cold" branches kick in at the same relative
|
||||||
|
// point in each band, not the same absolute degree.
|
||||||
|
const isHot = dayHi !== null && dayHi >= (isPetsProfile ? 35 : 29); // Hot band and above
|
||||||
const isDanger = tBand.solid === true;
|
const isDanger = tBand.solid === true;
|
||||||
|
|
||||||
// Home/indoor/vehicle tabs show a modelled temperature, not the
|
// Home/indoor/vehicle tabs show a modelled temperature, not the
|
||||||
@@ -321,7 +327,9 @@ export function DayTabs({
|
|||||||
} else if (isHot) {
|
} else if (isHot) {
|
||||||
// Keep the heat hue; cloud only mutes it a touch (orange/red never
|
// Keep the heat hue; cloud only mutes it a touch (orange/red never
|
||||||
// greens) and rain does not override heat.
|
// greens) and rain does not override heat.
|
||||||
const heatFactor = dayHi >= 39 ? 0.15 : dayHi >= 34 ? 0.30 : 0.45;
|
const heatFactor = isPetsProfile
|
||||||
|
? (dayHi >= 49 ? 0.15 : dayHi >= 43 ? 0.30 : 0.45)
|
||||||
|
: (dayHi >= 39 ? 0.15 : dayHi >= 34 ? 0.30 : 0.45);
|
||||||
const t = Math.max(0, Math.min(1, (avgCloud - 30) / 70)) * heatFactor;
|
const t = Math.max(0, Math.min(1, (avgCloud - 30) / 70)) * heatFactor;
|
||||||
rgb = mix(hex2rgb(tBand.bg), [170, 162, 152], t);
|
rgb = mix(hex2rgb(tBand.bg), [170, 162, 152], t);
|
||||||
} else if (daySnow >= 0.1) {
|
} else if (daySnow >= 0.1) {
|
||||||
@@ -333,7 +341,7 @@ export function DayTabs({
|
|||||||
} else if (avgCloud >= 30) {
|
} else if (avgCloud >= 30) {
|
||||||
// Cloud: light → dark grey with cover (pure grey, no temperature hue).
|
// Cloud: light → dark grey with cover (pure grey, no temperature hue).
|
||||||
rgb = mix([205, 206, 208], [110, 115, 122], Math.max(0, Math.min(1, (avgCloud - 30) / 70)));
|
rgb = mix([205, 206, 208], [110, 115, 122], Math.max(0, Math.min(1, (avgCloud - 30) / 70)));
|
||||||
} else if (dayHi !== null && dayHi < 10) {
|
} else if (dayHi !== null && dayHi < (isPetsProfile ? 2 : 10)) {
|
||||||
// Clear & cold: keep the cold temperature blue.
|
// Clear & cold: keep the cold temperature blue.
|
||||||
rgb = hex2rgb(tBand.bg);
|
rgb = hex2rgb(tBand.bg);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -199,7 +199,7 @@ export function buildHourlyRows({ forecast, airQuality, location, vehicleType, v
|
|||||||
|
|
||||||
// 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, uvArr, cloudCatArr);
|
||||||
hourlyRows.forEach((r, i) => { r.furSurfaceT = furTemps[i]; });
|
hourlyRows.forEach((r, i) => { r.furSurfaceT = furTemps[i]; });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -630,7 +630,7 @@ export function useAppState() {
|
|||||||
tableCanScrollRight,
|
tableCanScrollRight,
|
||||||
handleBodyScroll,
|
handleBodyScroll,
|
||||||
} = useTableScroll({
|
} = useTableScroll({
|
||||||
headTableRef, bodyTableRef, bodyScrollRef, headTrackRef,
|
headTableRef, bodyTableRef, bodyScrollRef, headTrackRef, tableWrapRef,
|
||||||
forecast, visibleCols, selectedDay, skinType, vehicleType,
|
forecast, visibleCols, selectedDay, skinType, vehicleType,
|
||||||
indoorMode, indoorManaged, showDecimals, showUnits,
|
indoorMode, indoorManaged, showDecimals, showUnits,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export function useTableScroll({
|
|||||||
bodyTableRef,
|
bodyTableRef,
|
||||||
bodyScrollRef,
|
bodyScrollRef,
|
||||||
headTrackRef,
|
headTrackRef,
|
||||||
|
tableWrapRef,
|
||||||
forecast,
|
forecast,
|
||||||
visibleCols,
|
visibleCols,
|
||||||
selectedDay,
|
selectedDay,
|
||||||
@@ -111,6 +112,15 @@ export function useTableScroll({
|
|||||||
const body = bodyScrollRef.current;
|
const body = bodyScrollRef.current;
|
||||||
if (!track || !body) return;
|
if (!track || !body) return;
|
||||||
track.style.transform = `translate3d(${-body.scrollLeft}px, 0, 0)`;
|
track.style.transform = `translate3d(${-body.scrollLeft}px, 0, 0)`;
|
||||||
|
// The Hour column's header cells (one per thead row) need to stay
|
||||||
|
// pinned to the left edge even though the track they live in is
|
||||||
|
// being shifted above - cancel that shift with an equal-and-opposite
|
||||||
|
// translate so they hold still while the rest of the header scrolls.
|
||||||
|
const headTable = headTableRef.current;
|
||||||
|
if (headTable) {
|
||||||
|
const pinnedCells = headTable.querySelectorAll('thead th:first-child');
|
||||||
|
pinnedCells.forEach(c => { c.style.transform = `translate3d(${body.scrollLeft}px, 0, 0)`; });
|
||||||
|
}
|
||||||
updateTableScrollIndicators();
|
updateTableScrollIndicators();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -198,6 +208,12 @@ export function useTableScroll({
|
|||||||
// column N in the body, no drift as you scroll right.
|
// column N in the body, no drift as you scroll right.
|
||||||
headTable.style.width = `${totalWidth}px`;
|
headTable.style.width = `${totalWidth}px`;
|
||||||
bodyTable.style.width = `${totalWidth}px`;
|
bodyTable.style.width = `${totalWidth}px`;
|
||||||
|
// Expose the pinned Hour column's width as a CSS var so the left
|
||||||
|
// scroll-fade indicator can start after it, over the first column
|
||||||
|
// that actually scrolls, rather than over the pinned column itself.
|
||||||
|
if (tableWrapRef && tableWrapRef.current) {
|
||||||
|
tableWrapRef.current.style.setProperty('--hour-col-w', `${finalW[0]}px`);
|
||||||
|
}
|
||||||
// Re-apply current horizontal offset so column alignment survives.
|
// Re-apply current horizontal offset so column alignment survives.
|
||||||
handleBodyScroll();
|
handleBodyScroll();
|
||||||
};
|
};
|
||||||
|
|||||||
+19
-5
@@ -620,11 +620,25 @@ export function calcShadeAirTemp(Ta, effectiveRad, va, elev, shade) {
|
|||||||
// A breed-specific multiplier (brachycephalic/senior) is deliberately not
|
// A breed-specific multiplier (brachycephalic/senior) is deliberately not
|
||||||
// modelled - this profile targets cats and small dogs as the reference animal.
|
// modelled - this profile targets cats and small dogs as the reference animal.
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
export function calcFurSurfaceTemp(Ta, globalRad, windSpeed, solElev, furAlbedo = 0.15) {
|
export function calcFurSurfaceTemp(Ta, globalRad, windSpeed, solElev, furAlbedo = 0.15, uv, cloudCat) {
|
||||||
if (globalRad == null || Ta == null) return null;
|
if (globalRad == null || Ta == null) return null;
|
||||||
const elevDeg = (solElev != null) ? Math.max(5, solElev) : 45;
|
const elevDeg = (solElev != null) ? Math.max(5, solElev) : 45;
|
||||||
const angleCorrection = Math.sin(elevDeg * Math.PI / 180);
|
const angleCorrection = Math.sin(elevDeg * Math.PI / 180);
|
||||||
const absorbed = globalRad * (1 - furAlbedo) * angleCorrection;
|
// Same cloud/UV-clarity derating calcConcreteTemp() applies on top of the
|
||||||
|
// raw radiation figure - both models share the same input array
|
||||||
|
// (hourlyRows.effectiveRad), so without these fur was absorbing the full
|
||||||
|
// beam while concrete was further discounted for haze/cloud, making fur
|
||||||
|
// read hotter than pavement on anything but a clear sky (backwards from
|
||||||
|
// the "never bakes as hot as dead concrete" intent above).
|
||||||
|
const cloudTransmit = cloudCat === 'clear' ? 1.00
|
||||||
|
: cloudCat === 'wispy' ? 0.92
|
||||||
|
: cloudCat === 'scattered' ? 0.72
|
||||||
|
: cloudCat === 'overcast' ? 0.28
|
||||||
|
: 1.00;
|
||||||
|
const uvClarity = (uv && uv > 0)
|
||||||
|
? 0.85 + 0.15 * Math.min(1, uv / 8)
|
||||||
|
: 1.0;
|
||||||
|
const absorbed = globalRad * (1 - furAlbedo) * angleCorrection * cloudTransmit * uvClarity;
|
||||||
// Lower convective coefficient than bare concrete (hc=10+4.5*sqrt(v)) -
|
// Lower convective coefficient than bare concrete (hc=10+4.5*sqrt(v)) -
|
||||||
// trapped air in the coat insulates, running the surface hotter for a
|
// trapped air in the coat insulates, running the surface hotter for a
|
||||||
// given wind speed than pavement, but not as insulated as a thick double
|
// given wind speed than pavement, but not as insulated as a thick double
|
||||||
@@ -639,13 +653,13 @@ export function calcFurSurfaceTemp(Ta, globalRad, windSpeed, solElev, furAlbedo
|
|||||||
|
|
||||||
export const LAG_HOURS_FUR = 0.4; // ~24 min - low thermal mass, animal moves in/out of sun
|
export const LAG_HOURS_FUR = 0.4; // ~24 min - low thermal mass, animal moves in/out of sun
|
||||||
|
|
||||||
export function calcFurSurfaceTempPass(TaArr, radArr, vaArr, elevArr, furAlbedo = 0.15) {
|
export function calcFurSurfaceTempPass(TaArr, radArr, vaArr, elevArr, furAlbedo = 0.15, uvArr, cloudCatArr) {
|
||||||
const n = TaArr.length;
|
const n = TaArr.length;
|
||||||
const result = new Array(n);
|
const result = new Array(n);
|
||||||
const alpha = 1 - Math.exp(-1 / LAG_HOURS_FUR);
|
const alpha = 1 - Math.exp(-1 / LAG_HOURS_FUR);
|
||||||
let Tf = calcFurSurfaceTemp(TaArr[0], radArr[0], vaArr[0], elevArr[0], furAlbedo) ?? TaArr[0];
|
let Tf = calcFurSurfaceTemp(TaArr[0], radArr[0], vaArr[0], elevArr[0], furAlbedo, uvArr?.[0], cloudCatArr?.[0]) ?? TaArr[0];
|
||||||
for (let i = 0; i < n; i++) {
|
for (let i = 0; i < n; i++) {
|
||||||
const target = calcFurSurfaceTemp(TaArr[i], radArr[i], vaArr[i], elevArr[i], furAlbedo) ?? TaArr[i];
|
const target = calcFurSurfaceTemp(TaArr[i], radArr[i], vaArr[i], elevArr[i], furAlbedo, uvArr?.[i], cloudCatArr?.[i]) ?? TaArr[i];
|
||||||
Tf = Tf + alpha * (target - Tf);
|
Tf = Tf + alpha * (target - Tf);
|
||||||
result[i] = Math.max(TaArr[i], Tf);
|
result[i] = Math.max(TaArr[i], Tf);
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-7
@@ -173,14 +173,25 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"2026-07-29": {
|
"2026-07-29": {
|
||||||
"visits": 32,
|
"visits": 44,
|
||||||
"profiles": {
|
"profiles": {
|
||||||
"vehicle": 9,
|
"vehicle": 16,
|
||||||
"home": 9,
|
"home": 13,
|
||||||
"basic": 7,
|
"basic": 12,
|
||||||
"showall": 1,
|
"showall": 5,
|
||||||
"pets": 3,
|
"pets": 9,
|
||||||
"alltemps": 2
|
"alltemps": 6,
|
||||||
|
"farming": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"2026-07-30": {
|
||||||
|
"visits": 6,
|
||||||
|
"profiles": {
|
||||||
|
"farming": 3,
|
||||||
|
"home": 4,
|
||||||
|
"basic": 2,
|
||||||
|
"vehicle": 4,
|
||||||
|
"pets": 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user