Calculation updates
This commit is contained in:
+40
-37
@@ -56,7 +56,7 @@
|
||||
"name": "How hot does a car get in the sun?",
|
||||
"acceptedAnswer": {
|
||||
"@type": "Answer",
|
||||
"text": "A sealed car in direct sunlight can reach cabin temperatures 20–30 °C above outside air temperature within 30–45 minutes. On a 25 °C day with strong sun, cabin air can hit 50–55 °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 20–30 °C above outside air temperature within 30–45 minutes. On a 25 °C day with strong sun, cabin air can hit 50–55 °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
|
||||
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
|
||||
using the row of buttons above the forecast table. The <strong>Outdoors</strong> profile
|
||||
includes a sub-dropdown in the Columns bar to choose between Beach, Festival, Events,
|
||||
Naturist, and Winter Sports. Switch to <strong>Custom</strong> to pick individual columns
|
||||
yourself.
|
||||
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 Urban, Beach, Events,
|
||||
Festival, Winter Sports, and Naturist. Everyday and public-good profiles are free;
|
||||
specialist planning views and Custom columns are part of SunScope Extra.
|
||||
</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">
|
||||
<p class="profile-name">🌾 Farming</p>
|
||||
<p>
|
||||
@@ -313,7 +298,7 @@
|
||||
</div>
|
||||
|
||||
<div class="profile-block">
|
||||
<p class="profile-name">⛵ Sailing</p>
|
||||
<p class="profile-name">⛵ Sailing · Extra</p>
|
||||
<p>
|
||||
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:
|
||||
@@ -336,7 +321,10 @@
|
||||
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
|
||||
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 25–35 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
|
||||
<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
|
||||
@@ -366,11 +354,22 @@
|
||||
</div>
|
||||
|
||||
<div class="profile-block">
|
||||
<p class="profile-name">🌤️ Outdoors</p>
|
||||
<p class="profile-name">🌤️ Places</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
|
||||
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>
|
||||
<strong>Beach</strong> — UV exposure at the coast is significantly higher than inland:
|
||||
@@ -383,7 +382,7 @@
|
||||
for day planning.
|
||||
</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
|
||||
<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
|
||||
@@ -402,7 +401,7 @@
|
||||
— relevant for anyone standing or sitting on sun-exposed hard standing for extended periods.
|
||||
</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
|
||||
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
|
||||
@@ -411,7 +410,7 @@
|
||||
cover</strong>, and <strong>precipitation</strong>.
|
||||
</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
|
||||
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
|
||||
@@ -476,7 +475,7 @@
|
||||
on city surfaces — something the standard UTCI value alone won't tell you.
|
||||
</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.
|
||||
</p>
|
||||
|
||||
@@ -489,9 +488,10 @@
|
||||
</p>
|
||||
<p>
|
||||
The calculation combines two physical processes. The first is <strong>body panel
|
||||
conduction</strong>: metal body panels (roof, doors, bonnet) absorb solar radiation and
|
||||
conduct heat into the cabin regardless of the sun's position in the sky. This is the slow,
|
||||
relentless background heat that builds even on overcast days. The second is
|
||||
conduction</strong>: body panels absorb solar radiation and conduct heat into the cabin
|
||||
regardless of the sun's position in the sky. Thin metal car panels pass much more of this
|
||||
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
|
||||
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
|
||||
@@ -500,10 +500,13 @@
|
||||
you heats up steadily while you remain unaware of how hot the environment has become.
|
||||
</p>
|
||||
<p>
|
||||
Wind speed has almost no effect on a sealed vehicle and is not factored in. The figure shown
|
||||
represents a steady-state cabin temperature reached after approximately 45–60 minutes of
|
||||
parking. Values above 35 °C are shown in amber (dangerous for children and pets); above
|
||||
45 °C in red (potentially fatal within minutes).
|
||||
Wind speed has almost no effect on a sealed vehicle and is not factored in unless the
|
||||
Ventilation toggle is enabled. For cars, the figure represents the cabin temperature after
|
||||
roughly 45–60 minutes of parking. For motorhomes and caravans, the model allows for lower
|
||||
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>
|
||||
|
||||
<h2>Reading the stress bands</h2>
|
||||
@@ -546,7 +549,7 @@
|
||||
|
||||
<details class="faq-block">
|
||||
<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 20–30 °C above outside air temperature within 30–45 minutes. On a 25 °C day with strong sun, cabin air can hit 50–55 °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 20–30 °C above outside air temperature within 30–45 minutes. On a 25 °C day with strong sun, cabin air can hit 50–55 °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 class="faq-block">
|
||||
|
||||
+66
-37
@@ -113,7 +113,7 @@ export function UTCIForecast() {
|
||||
// FLIP THE `false` BELOW TO `true` TO PREVIEW THE PRO EXPERIENCE.
|
||||
// When this is wired to real billing/auth, replace `useState(false)`
|
||||
// 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 🔒.
|
||||
// Bump this number if you want to give free users more access.
|
||||
@@ -127,57 +127,56 @@ export function UTCIForecast() {
|
||||
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 },
|
||||
},
|
||||
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: {
|
||||
label: 'Home',
|
||||
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 },
|
||||
},
|
||||
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: {
|
||||
label: 'Vehicle',
|
||||
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 },
|
||||
},
|
||||
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: {
|
||||
label: 'Farming',
|
||||
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 },
|
||||
},
|
||||
outdoors: {
|
||||
label: 'Outdoors',
|
||||
icon: '🌤️',
|
||||
// Default cols match the first variant (beach). Switching variant updates visibleCols.
|
||||
sailing: {
|
||||
label: 'Sailing',
|
||||
icon: '⛵',
|
||||
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 },
|
||||
},
|
||||
alltemps: {
|
||||
label: 'All Temps',
|
||||
label: 'Temps',
|
||||
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 },
|
||||
},
|
||||
custom: {
|
||||
label: 'Custom',
|
||||
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 },
|
||||
},
|
||||
};
|
||||
|
||||
// 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.
|
||||
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 } },
|
||||
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 } },
|
||||
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 } },
|
||||
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 } },
|
||||
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', 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
|
||||
@@ -195,8 +194,8 @@ export function UTCIForecast() {
|
||||
// Vehicle type for the cabin heat column. 'car' is the default preset.
|
||||
const [vehicleType, setVehicleType] = useState('car');
|
||||
|
||||
// Outdoors profile sub-variant (beach / festival / events / naturist)
|
||||
const [outdoorsVariant, setOutdoorsVariant] = useState('beach');
|
||||
// Places profile sub-variant (urban / beach / events / festival / wintersports / naturist)
|
||||
const [outdoorsVariant, setOutdoorsVariant] = useState('urban');
|
||||
|
||||
// Vehicle ventilation — true = windows open (high convective loss).
|
||||
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.' },
|
||||
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 15–25 °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 2–4 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.' },
|
||||
};
|
||||
@@ -839,6 +838,36 @@ export function UTCIForecast() {
|
||||
const promptDate = new Date(days[proPromptDay].key + 'T00:00Z');
|
||||
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 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`
|
||||
<div style=${{
|
||||
margin: '12px 0',
|
||||
@@ -863,9 +892,7 @@ export function UTCIForecast() {
|
||||
marginBottom: '6px',
|
||||
lineHeight: 1.25,
|
||||
}}>
|
||||
🔒 ${proPromptSource === 'custom'
|
||||
? 'Custom columns are part of SunScope Extra'
|
||||
: `${dayName}'s forecast is part of SunScope Extra`}
|
||||
🔒 ${promptCopy.title}
|
||||
</div>
|
||||
<div style=${{
|
||||
fontFamily: 'Manrope, sans-serif',
|
||||
@@ -873,9 +900,7 @@ export function UTCIForecast() {
|
||||
color: '#4a3420',
|
||||
lineHeight: 1.65,
|
||||
}}>
|
||||
${proPromptSource === 'custom'
|
||||
? '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.'}
|
||||
${promptCopy.detail}
|
||||
</div>
|
||||
<div style=${{
|
||||
fontFamily: 'JetBrains Mono, monospace',
|
||||
@@ -962,31 +987,30 @@ export function UTCIForecast() {
|
||||
|
||||
<!--
|
||||
FILTER PROFILE SELECTOR — presets shown to all users.
|
||||
Custom is Pro-only: free users see it locked with a 🔒
|
||||
and clicking it triggers the same upsell prompt as locked days.
|
||||
Extra profiles are shown in place with a gentle 🔒 and clicking
|
||||
them triggers the same upsell prompt as locked days.
|
||||
The bottom border is removed only when the col-toggles bar
|
||||
follows (Pro users), so the two bars merge into one panel.
|
||||
-->
|
||||
<div class="filter-profiles" style=${{ borderBottom: isPro ? 'none' : '' }}>
|
||||
<span class="filter-profiles-label">Profile:</span>
|
||||
${Object.entries(FILTER_PROFILES).map(([key, profile]) => {
|
||||
const isCustom = key === 'custom';
|
||||
const locked = isCustom && !isPro;
|
||||
const locked = profile.proOnly && !isPro;
|
||||
return html`
|
||||
<button
|
||||
key=${key}
|
||||
class=${`profile-btn${activeProfile === key ? ' active' : ''}${locked ? ' locked' : ''}`}
|
||||
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=${() => {
|
||||
if (locked) {
|
||||
setProPromptSource('custom');
|
||||
setProPromptSource(`profile:${key}`);
|
||||
setProPromptDay(0);
|
||||
return;
|
||||
}
|
||||
setActiveProfile(key);
|
||||
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 });
|
||||
} else if (key !== 'custom') {
|
||||
setVisibleCols({ ...profile.cols });
|
||||
@@ -1020,11 +1044,16 @@ export function UTCIForecast() {
|
||||
value=${outdoorsVariant}
|
||||
onChange=${(e) => {
|
||||
const v = e.target.value;
|
||||
if (OUTDOORS_VARIANTS[v]?.proOnly && !isPro) {
|
||||
setProPromptSource(`variant:${v}`);
|
||||
setProPromptDay(0);
|
||||
return;
|
||||
}
|
||||
setOutdoorsVariant(v);
|
||||
setVisibleCols({ ...OUTDOORS_VARIANTS[v].cols });
|
||||
}}>
|
||||
${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>`}
|
||||
|
||||
${isPro && (activeProfile === 'custom' || FILTER_PROFILES[activeProfile].cols['air']) && html`<button class=${`col-toggle${visibleCols.air ? ' on' : ''}`} onClick=${() => toggleCol('air')}>Air</button>`}
|
||||
|
||||
+25
-7
@@ -239,8 +239,13 @@ export function calcManagedIndoorTempPass(TaArr, globArr, elevArr, buildingType
|
||||
// so it adds to ambient cabin temp, not direct radiant load.
|
||||
// Above 60° the sun mostly hits the roof; below 10° it reflects.
|
||||
//
|
||||
// Wind is ignored (vehicle is sealed). No evaporative cooling.
|
||||
// Steady-state reached after ~45–60 min of parking.
|
||||
// Wind is ignored unless ventilation is enabled. No evaporative cooling.
|
||||
// Cars warm quickly; motorhomes and caravans are treated as insulated living
|
||||
// spaces with 25–35 mm sandwich panels, so panel heat gain is much smaller
|
||||
// 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:
|
||||
// < 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)
|
||||
const hOut = 10;
|
||||
const panelSurfaceTemp = Ta + panelAbsorbed / hOut;
|
||||
// Conductive gain into cabin: panel-to-cabin air, ~4 W/m²K through metal + trim
|
||||
const hCabin = 4;
|
||||
// Conductive gain into cabin. Cars are thin metal + trim; motorhomes and
|
||||
// caravans use insulated sandwich panels, so their bodyU is much lower.
|
||||
const hCabin = preset.bodyU ?? 4;
|
||||
const conductionGain = hCabin * (panelSurfaceTemp - Ta); // W/m²
|
||||
|
||||
// ── 2. Side-window glazing gain (angle-dependent) ─────────────────
|
||||
@@ -285,12 +291,24 @@ export function calcVehicleInteriorTemp(Ta, globalRad, solElev, vehicleType = 'c
|
||||
// ── Combine into cabin air temperature ───────────────────────────
|
||||
// Total heat input per m² of cabin surface
|
||||
const totalGain = conductionGain + glazingGain;
|
||||
// Cabin heat loss: varies by vehicle type (motorhomes insulated, cars not).
|
||||
// With windows open, convective loss is roughly 5× higher — air moves freely
|
||||
// Cabin heat rejection: an effective blend of leakage, internal air volume,
|
||||
// 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
|
||||
// closer to ambient. Cabin temp still rises a little due to panel/roof solar gain.
|
||||
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
|
||||
return Math.max(Ta, Math.min(Ti, 90));
|
||||
|
||||
+16
-10
@@ -127,18 +127,24 @@ export function burnLabel(mins) {
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// VEHICLE TYPES — presets for the cabin heat model.
|
||||
// ───────────────────────────────────────────────────────────────────
|
||||
// Each entry tweaks the three physical levers in calcVehicleInteriorTemp:
|
||||
// albedo — how much solar the bodywork reflects (0 = black, 1 = mirror)
|
||||
// glazingArea — relative side-window glass area (1.0 = typical car)
|
||||
// hCabinLoss — how easily heat escapes the cabin (W/m²K); lower = hotter
|
||||
// Motorhomes have thicker insulation → lower loss
|
||||
// Each entry tweaks the physical levers in calcVehicleInteriorTemp:
|
||||
// albedo — how much solar the bodywork reflects (0 = black, 1 = mirror)
|
||||
// glazingArea — relative sun-exposed glass area (1.0 = typical car)
|
||||
// bodyU — effective body/panel conductance into the cabin. Cars are
|
||||
// thin metal/glass boxes; motorhomes/caravans have insulated
|
||||
// sandwich panels, commonly around 25–35 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 = {
|
||||
car: { name: 'Car / Hatchback', albedo: 0.25, glazingArea: 1.0, hCabinLoss: 2.0 },
|
||||
mpv: { name: 'MPV / People Carrier', albedo: 0.25, glazingArea: 1.3, hCabinLoss: 2.0 },
|
||||
suv: { name: 'SUV / 4×4', albedo: 0.22, glazingArea: 1.1, hCabinLoss: 1.9 },
|
||||
motorhome: { name: 'Motorhome / Campervan', albedo: 0.35, glazingArea: 0.6, hCabinLoss: 1.2 },
|
||||
caravan: { name: 'Caravan (towed)', albedo: 0.40, glazingArea: 0.4, hCabinLoss: 1.0 },
|
||||
car: { name: 'Car / Hatchback', albedo: 0.25, glazingArea: 1.0, bodyU: 4.0, hCabinLoss: 20, thermalMass: 1.00, retainedWarmth: false, internalGain: 0.0 },
|
||||
mpv: { name: 'MPV / People Carrier', albedo: 0.25, glazingArea: 1.3, bodyU: 4.0, hCabinLoss: 20, thermalMass: 1.00, retainedWarmth: false, internalGain: 0.0 },
|
||||
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.55, glazingArea: 0.25, bodyU: 0.9, hCabinLoss: 12, thermalMass: 0.35, retainedWarmth: true, internalGain: 1.2 },
|
||||
caravan: { name: 'Caravan (towed)', albedo: 0.60, glazingArea: 0.22, bodyU: 0.8, hCabinLoss: 11, thermalMass: 0.35, retainedWarmth: true, internalGain: 1.2 },
|
||||
};
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
Reference in New Issue
Block a user