Calculation updates

This commit is contained in:
fraxle
2026-05-15 13:46:11 +01:00
parent ef96d5f1c8
commit 153029c645
4 changed files with 147 additions and 91 deletions
+40 -37
View File
@@ -56,7 +56,7 @@
"name": "How hot does a car get in the sun?", "name": "How hot does a car get in the sun?",
"acceptedAnswer": { "acceptedAnswer": {
"@type": "Answer", "@type": "Answer",
"text": "A sealed car in direct sunlight can reach cabin temperatures 2030 °C above outside air temperature within 3045 minutes. On a 25 °C day with strong sun, cabin air can hit 5055 °C. SunScope models this hour by hour using the actual solar radiation forecast, accounting for body panel conduction and window solar gain. Values above 35 °C are dangerous for children and pets; above 45 °C can be fatal within minutes." "text": "A sealed car in direct sunlight can reach cabin temperatures 2030 °C above outside air temperature within 3045 minutes. On a 25 °C day with strong sun, cabin air can hit 5055 °C. SunScope models this hour by hour using the actual solar radiation forecast, accounting for body panel conduction and window solar gain. Motorhomes and caravans are treated separately as insulated occupied living spaces, with reflective panels, retained warmth, and slower heat response. Values above 35 °C are dangerous for children and pets; above 45 °C can be fatal within minutes."
} }
}, },
{ {
@@ -274,27 +274,12 @@
SunScope organises its columns into <strong>profiles</strong> — curated sets of data tailored SunScope organises its columns into <strong>profiles</strong> — curated sets of data tailored
to different activities and use cases. Rather than showing every column at once, each profile to different activities and use cases. Rather than showing every column at once, each profile
surfaces the information that actually matters for that situation. You can switch profiles surfaces the information that actually matters for that situation. You can switch profiles
using the row of buttons above the forecast table. The <strong>Outdoors</strong> profile using the row of buttons above the forecast table. The <strong>Places</strong> profile
includes a sub-dropdown in the Columns bar to choose between Beach, Festival, Events, includes a sub-dropdown in the Columns bar to choose between Urban, Beach, Events,
Naturist, and Winter Sports. Switch to <strong>Custom</strong> to pick individual columns Festival, Winter Sports, and Naturist. Everyday and public-good profiles are free;
yourself. specialist planning views and Custom columns are part of SunScope Extra.
</p> </p>
<div class="profile-block">
<p class="profile-name">🏙️ Urban</p>
<p>
Designed for anyone moving around a city on foot — commuting, shopping, sightseeing.
The urban environment creates its own microclimate: buildings channel wind into gusts down
narrow streets, concrete absorbs heat all day and radiates it back long after sunset, and
the lack of vegetation means there's no evaporative cooling. The Urban profile shows
<strong>air temperature</strong>, <strong>wind speed and direction</strong> (useful for
finding sheltered routes), <strong>cloud cover</strong>, <strong>UV-A index</strong> and
<strong>burn time</strong> for sun protection, <strong>UTCI+P</strong> for overall felt
comfort, <strong>precipitation</strong>, and <strong>concrete surface temperature</strong>
— the heat radiating up from pavement that standard forecasts completely ignore.
</p>
</div>
<div class="profile-block"> <div class="profile-block">
<p class="profile-name">🌾 Farming</p> <p class="profile-name">🌾 Farming</p>
<p> <p>
@@ -313,7 +298,7 @@
</div> </div>
<div class="profile-block"> <div class="profile-block">
<p class="profile-name">⛵ Sailing</p> <p class="profile-name">⛵ Sailing · Extra</p>
<p> <p>
Wind is everything on the water, and the conditions out at sea can be dramatically different Wind is everything on the water, and the conditions out at sea can be dramatically different
from what the shore feels like. The Sailing profile leads with what matters most: from what the shore feels like. The Sailing profile leads with what matters most:
@@ -336,7 +321,10 @@
children or pets, motorhome travellers, and caravan owners. The headline column is children or pets, motorhome travellers, and caravan owners. The headline column is
<strong>vehicle interior temperature</strong>: a physics-based estimate of how hot the <strong>vehicle interior temperature</strong>: a physics-based estimate of how hot the
cabin gets when parked and sealed, adjusted for your chosen vehicle type (car, MPV, SUV, cabin gets when parked and sealed, adjusted for your chosen vehicle type (car, MPV, SUV,
motorhome, or caravan). <strong>Wind speed</strong> is included because it matters motorhome, or caravan). Motorhomes and caravans are treated as insulated occupied living
spaces, with reflective sandwich panels roughly 2535 mm thick, less sun-exposed glass
than a car, and retained warmth from previous hours, occupants, appliances, and background
heating. <strong>Wind speed</strong> is included because it matters
significantly for high-sided vehicles and caravans. <strong>Cloud cover</strong> and significantly for high-sided vehicles and caravans. <strong>Cloud cover</strong> and
<strong>air temperature</strong> give context, <strong>precipitation</strong> covers <strong>air temperature</strong> give context, <strong>precipitation</strong> covers
driving conditions, and <strong>UTCI+P</strong> tells you what it will feel like when driving conditions, and <strong>UTCI+P</strong> tells you what it will feel like when
@@ -366,11 +354,22 @@
</div> </div>
<div class="profile-block"> <div class="profile-block">
<p class="profile-name">🌤️ Outdoors</p> <p class="profile-name">🌤️ Places</p>
<p> <p>
A single profile covering five distinct outdoor leisure activities, each with its own A single profile covering six distinct place and activity contexts, each with its own
tailored column set. Select the activity from the dropdown in the Columns bar after tailored column set. Select the activity from the dropdown in the Columns bar after
activating the Outdoors profile. activating the Places profile. Urban, Beach, and Events are free; Festival, Winter
Sports, and Naturist are part of SunScope Extra.
</p>
<p>
<strong>Urban</strong> — designed for anyone moving around a city on foot: commuting,
shopping, sightseeing. The urban environment creates its own microclimate: buildings
channel wind into gusts down narrow streets, concrete absorbs heat all day and radiates
it back long after sunset, and the lack of vegetation means there's no evaporative
cooling. The Urban variant shows <strong>air temperature</strong>, <strong>wind speed
and direction</strong>, <strong>cloud cover</strong>, <strong>UV-A index</strong>,
<strong>burn time</strong>, <strong>UTCI+P</strong>, <strong>precipitation</strong>,
and <strong>concrete surface temperature</strong>.
</p> </p>
<p> <p>
<strong>Beach</strong> — UV exposure at the coast is significantly higher than inland: <strong>Beach</strong> — UV exposure at the coast is significantly higher than inland:
@@ -383,7 +382,7 @@
for day planning. for day planning.
</p> </p>
<p> <p>
<strong>Festival</strong> — multi-day outdoor events live or die by two things: the <strong>Festival · Extra</strong> — multi-day outdoor events live or die by two things: the
weather comfort and the state of the ground. The Festival variant focuses on weather comfort and the state of the ground. The Festival variant focuses on
<strong>UTCI+P</strong> for how cold and wet conditions will actually feel, <strong>wind <strong>UTCI+P</strong> for how cold and wet conditions will actually feel, <strong>wind
speed and direction</strong> for tent and stage exposure, <strong>dew point</strong> for speed and direction</strong> for tent and stage exposure, <strong>dew point</strong> for
@@ -402,7 +401,7 @@
— relevant for anyone standing or sitting on sun-exposed hard standing for extended periods. — relevant for anyone standing or sitting on sun-exposed hard standing for extended periods.
</p> </p>
<p> <p>
<strong>Naturist</strong> — with no clothing buffer between skin and environment, every <strong>Naturist · Extra</strong> — with no clothing buffer between skin and environment, every
weather variable matters more. Wind chill at even modest speeds becomes significant, so weather variable matters more. Wind chill at even modest speeds becomes significant, so
both the raw <strong>UTCI</strong> and <strong>UTCI+P</strong> are shown. Both <strong>UV-A both the raw <strong>UTCI</strong> and <strong>UTCI+P</strong> are shown. Both <strong>UV-A
</strong> and <strong>UV-B</strong> are included — full-body UV exposure is a serious </strong> and <strong>UV-B</strong> are included — full-body UV exposure is a serious
@@ -411,7 +410,7 @@
cover</strong>, and <strong>precipitation</strong>. cover</strong>, and <strong>precipitation</strong>.
</p> </p>
<p> <p>
<strong>Winter Sports</strong> — for skiers, snowboarders, and mountain walkers. UV <strong>Winter Sports · Extra</strong> — for skiers, snowboarders, and mountain walkers. UV
intensity increases by roughly 10% for every 1,000 m of altitude, and snow reflects up intensity increases by roughly 10% for every 1,000 m of altitude, and snow reflects up
to 80% of UV back upward — making mountain environments one of the highest-risk UV to 80% of UV back upward — making mountain environments one of the highest-risk UV
exposures outside a solarium. Both <strong>UV-A</strong> and <strong>UV-B</strong> are exposures outside a solarium. Both <strong>UV-A</strong> and <strong>UV-B</strong> are
@@ -476,7 +475,7 @@
on city surfaces — something the standard UTCI value alone won't tell you. on city surfaces — something the standard UTCI value alone won't tell you.
</p> </p>
<p> <p>
The column is enabled by default in the <strong>Urban</strong> profile and colour-coded: The column is enabled by default in the <strong>Places → Urban</strong> variant and colour-coded:
values above 30 °C are shown in amber, above 40 °C in red. values above 30 °C are shown in amber, above 40 °C in red.
</p> </p>
@@ -489,9 +488,10 @@
</p> </p>
<p> <p>
The calculation combines two physical processes. The first is <strong>body panel The calculation combines two physical processes. The first is <strong>body panel
conduction</strong>: metal body panels (roof, doors, bonnet) absorb solar radiation and conduction</strong>: body panels absorb solar radiation and conduct heat into the cabin
conduct heat into the cabin regardless of the sun's position in the sky. This is the slow, regardless of the sun's position in the sky. Thin metal car panels pass much more of this
relentless background heat that builds even on overcast days. The second is heat through than the insulated sandwich panels used in many motorhomes and caravans. This
is the slow, relentless background heat that builds even on overcast days. The second is
<strong>side-window solar gain</strong>: when the sun is at an elevation where its rays <strong>side-window solar gain</strong>: when the sun is at an elevation where its rays
cut through the side glass rather than striking the roof or reflecting off at a shallow cut through the side glass rather than striking the roof or reflecting off at a shallow
angle, additional heat enters the cabin. Because the occupant is modelled as sitting out of angle, additional heat enters the cabin. Because the occupant is modelled as sitting out of
@@ -500,10 +500,13 @@
you heats up steadily while you remain unaware of how hot the environment has become. you heats up steadily while you remain unaware of how hot the environment has become.
</p> </p>
<p> <p>
Wind speed has almost no effect on a sealed vehicle and is not factored in. The figure shown Wind speed has almost no effect on a sealed vehicle and is not factored in unless the
represents a steady-state cabin temperature reached after approximately 4560 minutes of Ventilation toggle is enabled. For cars, the figure represents the cabin temperature after
parking. Values above 35 °C are shown in amber (dangerous for children and pets); above roughly 4560 minutes of parking. For motorhomes and caravans, the model allows for lower
45 °C in red (potentially fatal within minutes). heat gain, slower response from insulated panels, and retained occupied-space warmth, so
the estimate stays much closer to the real living-space temperature on cool or moderate
sunny days. Values above 35 °C are shown in amber (dangerous for children and pets);
above 45 °C in red (potentially fatal within minutes).
</p> </p>
<h2>Reading the stress bands</h2> <h2>Reading the stress bands</h2>
@@ -546,7 +549,7 @@
<details class="faq-block"> <details class="faq-block">
<summary class="faq-q">How hot does a car get in the sun?</summary> <summary class="faq-q">How hot does a car get in the sun?</summary>
<p class="faq-a">A sealed car in direct sunlight can reach cabin temperatures 2030 °C above outside air temperature within 3045 minutes. On a 25 °C day with strong sun, cabin air can hit 5055 °C. SunScope models this hour by hour using the actual solar radiation forecast for your location, accounting for body panel conduction and window solar gain. Select the Vehicle profile and choose your vehicle type to see the estimate. Values above 35 °C are dangerous for children and pets; above 45 °C can be fatal within minutes.</p> <p class="faq-a">A sealed car in direct sunlight can reach cabin temperatures 2030 °C above outside air temperature within 3045 minutes. On a 25 °C day with strong sun, cabin air can hit 5055 °C. SunScope models this hour by hour using the actual solar radiation forecast for your location, accounting for body panel conduction and window solar gain. Select the Vehicle profile and choose your vehicle type to see the estimate. Motorhomes and caravans use a separate insulated occupied-space model, so they warm much less aggressively than cars in heat, but can also stay several degrees warmer than outside air on cool sunny days. Values above 35 °C are dangerous for children and pets; above 45 °C can be fatal within minutes.</p>
</details> </details>
<details class="faq-block"> <details class="faq-block">
+66 -37
View File
@@ -113,7 +113,7 @@ export function UTCIForecast() {
// FLIP THE `false` BELOW TO `true` TO PREVIEW THE PRO EXPERIENCE. // FLIP THE `false` BELOW TO `true` TO PREVIEW THE PRO EXPERIENCE.
// When this is wired to real billing/auth, replace `useState(false)` // When this is wired to real billing/auth, replace `useState(false)`
// with a check against the logged-in user. // with a check against the logged-in user.
const [isPro, setIsPro] = useState(false); const [isPro, setIsPro] = useState(true);
// How many days the free tier shows. Days beyond this get a 🔒. // How many days the free tier shows. Days beyond this get a 🔒.
// Bump this number if you want to give free users more access. // Bump this number if you want to give free users more access.
@@ -127,57 +127,56 @@ export function UTCIForecast() {
icon: '🌡️', icon: '🌡️',
cols: { hour: true, air: true, rh: false, dew: false, wind: true, dir: false, cloud: true, sun: false, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: false, uvB: false, burn: false, utciP: true, precip: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false }, cols: { hour: true, air: true, rh: false, dew: false, wind: true, dir: false, cloud: true, sun: false, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: false, uvB: false, burn: false, utciP: true, precip: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false },
}, },
outdoors: {
label: 'Places',
icon: '🌤️',
// Default cols match the first variant (urban). Switching variant updates visibleCols.
cols: { hour: true, air: true, rh: false, dew: false, wind: true, dir: true, cloud: true, sun: false, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: true, uvB: false, burn: true, utciP: true, precip: true, soilT: false, soilT6: false, soilM: false, concreteT: true, vehicleT: false, indoorT: false, managedT: false },
},
home: { home: {
label: 'Home', label: 'Home',
icon: '🏠', icon: '🏠',
cols: { hour: true, air: true, rh: true, dew: true, wind: true, dir: false, cloud: true, sun: false, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: false, uvB: false, burn: false, utciP: false, precip: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: true, managedT: true }, cols: { hour: true, air: true, rh: true, dew: true, wind: true, dir: false, cloud: true, sun: false, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: false, uvB: false, burn: false, utciP: false, precip: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: true, managedT: true },
}, },
urban: {
label: 'Urban',
icon: '🏙️',
cols: { hour: true, air: true, rh: false, dew: false, wind: true, dir: true, cloud: true, sun: false, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: true, uvB: false, burn: true, utciP: true, precip: true, soilT: false, soilT6: false, soilM: false, concreteT: true, vehicleT: false, indoorT: false, managedT: false },
},
vehicle: { vehicle: {
label: 'Vehicle', label: 'Vehicle',
icon: '🚗', icon: '🚗',
cols: { hour: true, air: true, rh: false, dew: false, wind: true, dir: false, cloud: true, sun: false, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: false, uvB: false, burn: false, utciP: true, precip: true, soilT: false, soilT6: false, soilM: true, concreteT: false, vehicleT: true, indoorT: false, managedT: false }, cols: { hour: true, air: true, rh: false, dew: false, wind: true, dir: false, cloud: true, sun: false, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: false, uvB: false, burn: false, utciP: true, precip: true, soilT: false, soilT6: false, soilM: true, concreteT: false, vehicleT: true, indoorT: false, managedT: false },
}, },
sailing: {
label: 'Sailing',
icon: '⛵',
cols: { hour: true, air: true, rh: false, dew: true, wind: true, dir: true, cloud: true, sun: true, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: true, uvB: false, burn: true, utciP: true, precip: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false },
},
farming: { farming: {
label: 'Farming', label: 'Farming',
icon: '🌾', icon: '🌾',
cols: { hour: true, air: true, rh: true, dew: true, wind: true, dir: true, cloud: true, sun: true, direct: false, diffuse: false, tmrt: false, delta: false, utci: true, uvA: false, uvB: false, burn: false, utciP: false, precip: true, soilT: true, soilT6: true, soilM: true, concreteT: false, vehicleT: false, indoorT: false, managedT: false }, cols: { hour: true, air: true, rh: true, dew: true, wind: true, dir: true, cloud: true, sun: true, direct: false, diffuse: false, tmrt: false, delta: false, utci: true, uvA: false, uvB: false, burn: false, utciP: false, precip: true, soilT: true, soilT6: true, soilM: true, concreteT: false, vehicleT: false, indoorT: false, managedT: false },
}, },
outdoors: { sailing: {
label: 'Outdoors', label: 'Sailing',
icon: '🌤️', icon: '',
// Default cols match the first variant (beach). Switching variant updates visibleCols. proOnly: true,
cols: { hour: true, air: true, rh: false, dew: true, wind: true, dir: true, cloud: true, sun: true, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: true, uvB: false, burn: true, utciP: true, precip: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false }, cols: { hour: true, air: true, rh: false, dew: true, wind: true, dir: true, cloud: true, sun: true, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: true, uvB: false, burn: true, utciP: true, precip: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false },
}, },
alltemps: { alltemps: {
label: 'All Temps', label: 'Temps',
icon: '🌡️', icon: '🌡️',
proOnly: true,
cols: { hour: true, air: true, rh: false, dew: false, wind: false, dir: false, cloud: false, sun: false, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: false, uvB: false, burn: false, utciP: true, precip: false, soilT: true, soilT6: false, soilM: false, concreteT: true, vehicleT: true, indoorT: true, managedT: true }, cols: { hour: true, air: true, rh: false, dew: false, wind: false, dir: false, cloud: false, sun: false, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: false, uvB: false, burn: false, utciP: true, precip: false, soilT: true, soilT6: false, soilM: false, concreteT: true, vehicleT: true, indoorT: true, managedT: true },
}, },
custom: { custom: {
label: 'Custom', label: 'Custom',
icon: '⚙️', icon: '⚙️',
proOnly: true,
cols: { hour: true, air: true, rh: false, dew: false, wind: true, dir: false, cloud: false, sun: false, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: false, uvB: false, burn: false, utciP: true, precip: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false }, cols: { hour: true, air: true, rh: false, dew: false, wind: true, dir: false, cloud: false, sun: false, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: false, uvB: false, burn: false, utciP: true, precip: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false },
}, },
}; };
// Outdoors sub-variants — each has its own column set. // Places sub-variants — each has its own column set.
// Selecting a variant applies its cols to visibleCols. // Selecting a variant applies its cols to visibleCols.
const OUTDOORS_VARIANTS = { const OUTDOORS_VARIANTS = {
urban: { name: 'Urban', cols: { hour: true, air: true, rh: false, dew: false, wind: true, dir: true, cloud: true, sun: false, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: true, uvB: false, burn: true, utciP: true, precip: true, soilT: false, soilT6: false, soilM: false, concreteT: true, vehicleT: false, indoorT: false, managedT: false } },
beach: { name: 'Beach', cols: { hour: true, air: true, rh: false, dew: true, wind: true, dir: true, cloud: true, sun: true, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: true, uvB: true, burn: true, utciP: true, precip: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false } }, beach: { name: 'Beach', cols: { hour: true, air: true, rh: false, dew: true, wind: true, dir: true, cloud: true, sun: true, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: true, uvB: true, burn: true, utciP: true, precip: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false } },
festival: { name: 'Festival', cols: { hour: true, air: true, rh: false, dew: true, wind: true, dir: true, cloud: true, sun: false, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: true, uvB: false, burn: true, utciP: true, precip: true, soilT: false, soilT6: false, soilM: true, concreteT: false, vehicleT: false, indoorT: false, managedT: false } },
events: { name: 'Events', cols: { hour: true, air: true, rh: false, dew: false, wind: true, dir: true, cloud: true, sun: false, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: true, uvB: false, burn: true, utciP: true, precip: true, soilT: false, soilT6: false, soilM: false, concreteT: true, vehicleT: false, indoorT: false, managedT: false } }, events: { name: 'Events', cols: { hour: true, air: true, rh: false, dew: false, wind: true, dir: true, cloud: true, sun: false, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: true, uvB: false, burn: true, utciP: true, precip: true, soilT: false, soilT6: false, soilM: false, concreteT: true, vehicleT: false, indoorT: false, managedT: false } },
naturist: { name: 'Naturist', cols: { hour: true, air: true, rh: false, dew: true, wind: true, dir: false, cloud: true, sun: true, direct: false, diffuse: false, tmrt: false, delta: false, utci: true, uvA: true, uvB: true, burn: true, utciP: true, precip: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false } }, festival: { name: 'Festival', proOnly: true, cols: { hour: true, air: true, rh: false, dew: true, wind: true, dir: true, cloud: true, sun: false, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: true, uvB: false, burn: true, utciP: true, precip: true, soilT: false, soilT6: false, soilM: true, concreteT: false, vehicleT: false, indoorT: false, managedT: false } },
wintersports: { name: 'Winter Sports', cols: { hour: true, air: true, rh: false, dew: false, wind: true, dir: true, cloud: true, sun: true, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: true, uvB: true, burn: true, utciP: true, precip: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false } }, wintersports: { name: 'Winter Sports', proOnly: true, cols: { hour: true, air: true, rh: false, dew: false, wind: true, dir: true, cloud: true, sun: true, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: true, uvB: true, burn: true, utciP: true, precip: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false } },
naturist: { name: 'Naturist', proOnly: true, cols: { hour: true, air: true, rh: false, dew: true, wind: true, dir: false, cloud: true, sun: true, direct: false, diffuse: false, tmrt: false, delta: false, utci: true, uvA: true, uvB: true, burn: true, utciP: true, precip: true, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: false, managedT: false } },
}; };
// Current active filter profile // Current active filter profile
@@ -195,8 +194,8 @@ export function UTCIForecast() {
// Vehicle type for the cabin heat column. 'car' is the default preset. // Vehicle type for the cabin heat column. 'car' is the default preset.
const [vehicleType, setVehicleType] = useState('car'); const [vehicleType, setVehicleType] = useState('car');
// Outdoors profile sub-variant (beach / festival / events / naturist) // Places profile sub-variant (urban / beach / events / festival / wintersports / naturist)
const [outdoorsVariant, setOutdoorsVariant] = useState('beach'); const [outdoorsVariant, setOutdoorsVariant] = useState('urban');
// Vehicle ventilation — true = windows open (high convective loss). // Vehicle ventilation — true = windows open (high convective loss).
const [vehicleVent, setVehicleVent] = useState(false); const [vehicleVent, setVehicleVent] = useState(false);
@@ -250,7 +249,7 @@ export function UTCIForecast() {
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: 'Volumetric water content of the top 1 cm of soil (m³/m³). Values above 0.4 suggest saturated ground; below 0.2 indicates dry conditions.' }, soilM: { title: 'Soil Moisture', desc: 'Volumetric water content of the top 1 cm of soil (m³/m³). Values above 0.4 suggest saturated ground; below 0.2 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 1525 °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 1525 °C above air temperature on sunny days.' },
vehicleT: { title: 'Vehicle Interior', desc: 'Estimated peak ambient cabin temperature inside a sealed, parked vehicle. Adjusted for your selected vehicle type — albedo, glazing area, and insulation all affect the result. 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 with retained warmth, lower glass gain, and slower heat response. Dangerous for children and pets above 35 °C; potentially fatal above 45 °C.' },
indoorT: { title: 'Indoors', desc: 'Estimated ambient temperature inside a typical UK brick house with windows closed and no air conditioning. Accounts for wall conduction, window solar gain, and thermal mass — indoor temperatures typically peak 24 hours after the outdoor peak.' }, indoorT: { title: 'Indoors', desc: 'Estimated ambient temperature inside a typical UK brick house with windows closed and no air conditioning. Accounts for wall conduction, window solar gain, and thermal mass — indoor temperatures typically peak 24 hours after the outdoor peak.' },
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.' },
}; };
@@ -839,6 +838,36 @@ export function UTCIForecast() {
const promptDate = new Date(days[proPromptDay].key + 'T00:00Z'); const promptDate = new Date(days[proPromptDay].key + 'T00:00Z');
const dayName = promptDate.toLocaleDateString('en-GB', { weekday: 'long', timeZone: 'UTC' }); const dayName = promptDate.toLocaleDateString('en-GB', { weekday: 'long', timeZone: 'UTC' });
const dayLong = promptDate.toLocaleDateString('en-GB', { weekday: 'long', day: 'numeric', month: 'short', timeZone: 'UTC' }); const dayLong = promptDate.toLocaleDateString('en-GB', { weekday: 'long', day: 'numeric', month: 'short', timeZone: 'UTC' });
const extraPromptCopy = {
'profile:sailing': {
title: 'Sailing is part of SunScope Extra',
detail: 'Extra adds specialist planning views for higher-commitment trips, including wind, exposure, UV, and wet-weather comfort for water conditions.',
},
'profile:alltemps': {
title: 'Temps is part of SunScope Extra',
detail: 'Extra unlocks the comparison view for air, soil, concrete, vehicle, and indoor temperatures in one place.',
},
'profile:custom': {
title: 'Custom columns are part of SunScope Extra',
detail: 'Extra lets you choose exactly which columns appear: mix and match Air, Dew, Soil, UV, UTCI and more to build your perfect view.',
},
'variant:festival': {
title: 'Festival planning is part of SunScope Extra',
detail: 'Extra adds multi-day comfort, ground condition, exposure, and rain planning for higher-stakes outdoor trips.',
},
'variant:wintersports': {
title: 'Winter Sports is part of SunScope Extra',
detail: 'Extra adds specialist exposure planning for snow, glare, wind, UV reflection, and cold-weather comfort.',
},
'variant:naturist': {
title: 'Naturist is part of SunScope Extra',
detail: 'Extra adds specialist skin-exposure planning with UV, wind, humidity, precipitation, and felt-temperature detail.',
},
};
const promptCopy = extraPromptCopy[proPromptSource] || {
title: `${dayName}'s forecast is part of SunScope Extra`,
detail: 'Extra unlocks the full 14-day forecast, customisable columns, specialist profiles, and an ad-free view.',
};
return html` return html`
<div style=${{ <div style=${{
margin: '12px 0', margin: '12px 0',
@@ -863,9 +892,7 @@ export function UTCIForecast() {
marginBottom: '6px', marginBottom: '6px',
lineHeight: 1.25, lineHeight: 1.25,
}}> }}>
🔒 ${proPromptSource === 'custom' 🔒 ${promptCopy.title}
? 'Custom columns are part of SunScope Extra'
: `${dayName}'s forecast is part of SunScope Extra`}
</div> </div>
<div style=${{ <div style=${{
fontFamily: 'Manrope, sans-serif', fontFamily: 'Manrope, sans-serif',
@@ -873,9 +900,7 @@ export function UTCIForecast() {
color: '#4a3420', color: '#4a3420',
lineHeight: 1.65, lineHeight: 1.65,
}}> }}>
${proPromptSource === 'custom' ${promptCopy.detail}
? 'Pro lets you choose exactly which columns appear — mix and match Air, Dew, Soil, UV, UTCI and more to build your perfect view.'
: 'Pro unlocks the full 14-day forecast, customisable columns, and an ad-free view.'}
</div> </div>
<div style=${{ <div style=${{
fontFamily: 'JetBrains Mono, monospace', fontFamily: 'JetBrains Mono, monospace',
@@ -962,31 +987,30 @@ export function UTCIForecast() {
<!-- <!--
FILTER PROFILE SELECTOR presets shown to all users. FILTER PROFILE SELECTOR presets shown to all users.
Custom is Pro-only: free users see it locked with a 🔒 Extra profiles are shown in place with a gentle 🔒 and clicking
and clicking it triggers the same upsell prompt as locked days. them triggers the same upsell prompt as locked days.
The bottom border is removed only when the col-toggles bar The bottom border is removed only when the col-toggles bar
follows (Pro users), so the two bars merge into one panel. follows (Pro users), so the two bars merge into one panel.
--> -->
<div class="filter-profiles" style=${{ borderBottom: isPro ? 'none' : '' }}> <div class="filter-profiles" style=${{ borderBottom: isPro ? 'none' : '' }}>
<span class="filter-profiles-label">Profile:</span> <span class="filter-profiles-label">Profile:</span>
${Object.entries(FILTER_PROFILES).map(([key, profile]) => { ${Object.entries(FILTER_PROFILES).map(([key, profile]) => {
const isCustom = key === 'custom'; const locked = profile.proOnly && !isPro;
const locked = isCustom && !isPro;
return html` return html`
<button <button
key=${key} key=${key}
class=${`profile-btn${activeProfile === key ? ' active' : ''}${locked ? ' locked' : ''}`} class=${`profile-btn${activeProfile === key ? ' active' : ''}${locked ? ' locked' : ''}`}
style=${{ opacity: locked ? 0.6 : 1, cursor: locked ? 'not-allowed' : 'pointer', position: 'relative' }} style=${{ opacity: locked ? 0.6 : 1, cursor: locked ? 'not-allowed' : 'pointer', position: 'relative' }}
title=${locked ? '🔒 Custom columns are part of SunScope Extra' : ''} title=${locked ? `🔒 ${profile.label} is part of SunScope Extra` : ''}
onClick=${() => { onClick=${() => {
if (locked) { if (locked) {
setProPromptSource('custom'); setProPromptSource(`profile:${key}`);
setProPromptDay(0); setProPromptDay(0);
return; return;
} }
setActiveProfile(key); setActiveProfile(key);
if (key === 'outdoors') { if (key === 'outdoors') {
// Apply the current variant's cols (default: beach on first switch) // Apply the current variant's cols (default: urban on first switch)
setVisibleCols({ ...OUTDOORS_VARIANTS[outdoorsVariant].cols }); setVisibleCols({ ...OUTDOORS_VARIANTS[outdoorsVariant].cols });
} else if (key !== 'custom') { } else if (key !== 'custom') {
setVisibleCols({ ...profile.cols }); setVisibleCols({ ...profile.cols });
@@ -1020,11 +1044,16 @@ export function UTCIForecast() {
value=${outdoorsVariant} value=${outdoorsVariant}
onChange=${(e) => { onChange=${(e) => {
const v = e.target.value; const v = e.target.value;
if (OUTDOORS_VARIANTS[v]?.proOnly && !isPro) {
setProPromptSource(`variant:${v}`);
setProPromptDay(0);
return;
}
setOutdoorsVariant(v); setOutdoorsVariant(v);
setVisibleCols({ ...OUTDOORS_VARIANTS[v].cols }); setVisibleCols({ ...OUTDOORS_VARIANTS[v].cols });
}}> }}>
${Object.entries(OUTDOORS_VARIANTS).map(([k, v]) => html` ${Object.entries(OUTDOORS_VARIANTS).map(([k, v]) => html`
<option key=${k} value=${k}>${v.name}</option>`)} <option key=${k} value=${k}>${v.name}${v.proOnly && !isPro ? ' 🔒' : ''}</option>`)}
</select>`} </select>`}
${isPro && (activeProfile === 'custom' || FILTER_PROFILES[activeProfile].cols['air']) && html`<button class=${`col-toggle${visibleCols.air ? ' on' : ''}`} onClick=${() => toggleCol('air')}>Air</button>`} ${isPro && (activeProfile === 'custom' || FILTER_PROFILES[activeProfile].cols['air']) && html`<button class=${`col-toggle${visibleCols.air ? ' on' : ''}`} onClick=${() => toggleCol('air')}>Air</button>`}
+25 -7
View File
@@ -239,8 +239,13 @@ export function calcManagedIndoorTempPass(TaArr, globArr, elevArr, buildingType
// 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. // Above 60° the sun mostly hits the roof; below 10° it reflects.
// //
// Wind is ignored (vehicle is sealed). No evaporative cooling. // Wind is ignored unless ventilation is enabled. No evaporative cooling.
// Steady-state reached after ~4560 min of parking. // Cars warm quickly; motorhomes and caravans are treated as insulated living
// spaces with 2535 mm sandwich panels, so panel heat gain is much smaller
// and the interior response is slower than a car cabin. Because they are
// occupied living spaces, they also retain warmth from previous hours, people,
// appliances, and background heating; without that, cool sunny days are
// under-estimated badly.
// //
// 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
@@ -259,8 +264,9 @@ export function calcVehicleInteriorTemp(Ta, globalRad, solElev, vehicleType = 'c
// hOut ~10 W/m²K (light breeze over panel surface even when parked) // hOut ~10 W/m²K (light breeze over panel surface even when parked)
const hOut = 10; const hOut = 10;
const panelSurfaceTemp = Ta + panelAbsorbed / hOut; const panelSurfaceTemp = Ta + panelAbsorbed / hOut;
// Conductive gain into cabin: panel-to-cabin air, ~4 W/m²K through metal + trim // Conductive gain into cabin. Cars are thin metal + trim; motorhomes and
const hCabin = 4; // caravans use insulated sandwich panels, so their bodyU is much lower.
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. Side-window glazing gain (angle-dependent) ─────────────────
@@ -285,12 +291,24 @@ export function calcVehicleInteriorTemp(Ta, globalRad, solElev, vehicleType = 'c
// ── 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;
// Cabin heat loss: varies by vehicle type (motorhomes insulated, cars not). // Cabin heat rejection: an effective blend of leakage, internal air volume,
// With windows open, convective loss is roughly 5× higher — air moves freely // and surfaces exchanging heat with the outside. With windows open it is
// roughly 5× higher — air moves freely
// through the cabin, flushing heat out and capping interior temperature much // through the cabin, flushing heat out and capping interior temperature much
// closer to ambient. Cabin temp still rises a little due to panel/roof solar gain. // closer to ambient. Cabin temp still rises a little due to panel/roof solar gain.
const effectiveHLoss = ventilated ? preset.hCabinLoss * 5 : preset.hCabinLoss; const effectiveHLoss = ventilated ? preset.hCabinLoss * 5 : preset.hCabinLoss;
const Ti = Ta + totalGain / effectiveHLoss; const thermalMass = preset.thermalMass ?? 1;
const solarRise = (totalGain / effectiveHLoss) * thermalMass;
// Motorhomes/caravans behave more like small insulated rooms than parked
// cars. This term captures retained living-space warmth: strongest on cool
// days, tapering away as outdoor air warms, and reduced when ventilated.
const retainedWarmth = preset.retainedWarmth
? Math.max(0, 8 - 0.25 * Ta) * (ventilated ? 0.35 : 1)
: 0;
const internalGain = (preset.internalGain ?? 0) * (ventilated ? 0.35 : 1);
const Ti = Ta + solarRise + retainedWarmth + internalGain;
// Clamp: can't be cooler than outside air; physical cap at 90 °C // Clamp: can't be cooler than outside air; physical cap at 90 °C
return Math.max(Ta, Math.min(Ti, 90)); return Math.max(Ta, Math.min(Ti, 90));
+15 -9
View File
@@ -127,18 +127,24 @@ export function burnLabel(mins) {
// ═══════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════
// VEHICLE TYPES — presets for the cabin heat model. // VEHICLE TYPES — presets for the cabin heat model.
// ─────────────────────────────────────────────────────────────────── // ───────────────────────────────────────────────────────────────────
// Each entry tweaks the three 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 side-window glass area (1.0 = typical car) // glazingArea — relative sun-exposed glass area (1.0 = typical car)
// hCabinLoss — how easily heat escapes the cabin (W/m²K); lower = hotter // bodyU — effective body/panel conductance into the cabin. Cars are
// Motorhomes have thicker insulation → lower loss // thin metal/glass boxes; motorhomes/caravans have insulated
// sandwich panels, commonly around 2535 mm thick.
// hCabinLoss — effective heat rejection/infiltration from the cabin air.
// thermalMass — lower values mean the interior warms more slowly in the hour.
// retainedWarmth — occupied insulated living spaces hold heat from previous
// hours, people, appliances, and background heating.
// 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, hCabinLoss: 2.0 }, car: { name: 'Car / Hatchback', albedo: 0.25, glazingArea: 1.0, bodyU: 4.0, hCabinLoss: 20, thermalMass: 1.00, retainedWarmth: false, internalGain: 0.0 },
mpv: { name: 'MPV / People Carrier', albedo: 0.25, glazingArea: 1.3, hCabinLoss: 2.0 }, mpv: { name: 'MPV / People Carrier', albedo: 0.25, glazingArea: 1.3, bodyU: 4.0, hCabinLoss: 20, thermalMass: 1.00, retainedWarmth: false, internalGain: 0.0 },
suv: { name: 'SUV / 4×4', albedo: 0.22, glazingArea: 1.1, hCabinLoss: 1.9 }, suv: { name: 'SUV / 4x4', albedo: 0.22, glazingArea: 1.1, bodyU: 3.8, hCabinLoss: 19, thermalMass: 0.95, retainedWarmth: false, internalGain: 0.0 },
motorhome: { name: 'Motorhome / Campervan', albedo: 0.35, glazingArea: 0.6, hCabinLoss: 1.2 }, motorhome: { name: 'Motorhome / Campervan', albedo: 0.55, glazingArea: 0.25, bodyU: 0.9, hCabinLoss: 12, thermalMass: 0.35, retainedWarmth: true, internalGain: 1.2 },
caravan: { name: 'Caravan (towed)', albedo: 0.40, glazingArea: 0.4, hCabinLoss: 1.0 }, caravan: { name: 'Caravan (towed)', albedo: 0.60, glazingArea: 0.22, bodyU: 0.8, hCabinLoss: 11, thermalMass: 0.35, retainedWarmth: true, internalGain: 1.2 },
}; };
// ═══════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════