More 'Home' Physics
More FAQs
More Text
This commit is contained in:
fraxle
2026-05-15 10:39:09 +01:00
parent 5b0e62d1b5
commit 01d939933a
7 changed files with 605 additions and 176 deletions
+287 -41
View File
@@ -37,10 +37,10 @@
"mainEntity": [ "mainEntity": [
{ {
"@type": "Question", "@type": "Question",
"name": "What is UTCI?", "name": "What is UTCI and why is it better than a standard feels-like temperature?",
"acceptedAnswer": { "acceptedAnswer": {
"@type": "Answer", "@type": "Answer",
"text": "The Universal Thermal Climate Index (UTCI) is a biometeorological standard that combines air temperature, humidity, wind speed, and solar radiation into a single felt-temperature value. It was developed by Bröde et al. (2012) and is used worldwide in heat-health warning systems." "text": "The Universal Thermal Climate Index (UTCI) is a peer-reviewed biometeorological standard developed by Bröde et al. (2012) and used in public health systems worldwide. Unlike simple feels-like values that only apply wind-chill or heat-index formulas, UTCI combines air temperature, humidity, wind speed, and the full solar radiation load using a physiological model of the human body. It accounts for mean radiant temperature from all surrounding surfaces, making it a much more accurate predictor of actual thermal comfort and heat stress."
} }
}, },
{ {
@@ -48,7 +48,95 @@
"name": "What is the SunScope soak-factor?", "name": "What is the SunScope soak-factor?",
"acceptedAnswer": { "acceptedAnswer": {
"@type": "Answer", "@type": "Answer",
"text": "The SunScope soak-factor is an original precipitation penalty formula that reduces the UTCI value based on rain intensity, snowfall, and wind speed — reflecting the extra chill of wet clothing and skin that standard UTCI does not account for." "text": "The SunScope soak-factor is an original precipitation penalty formula that reduces the UTCI felt-temperature value based on rain intensity, snowfall, and wind speed — reflecting the extra chill of wet clothing and skin that standard UTCI does not account for. The result is shown as UTCI+P."
}
},
{
"@type": "Question",
"name": "How hot does a car get in the sun?",
"acceptedAnswer": {
"@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."
}
},
{
"@type": "Question",
"name": "Does opening car windows reduce the temperature inside?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Yes, significantly. Open windows allow convective airflow to flush hot cabin air out continuously, reducing heat build-up by roughly 6080% compared to a sealed vehicle. SunScope's Vehicle column includes a Windows open toggle that switches the physics model to a ventilated state, showing you the estimated cabin temperature with windows cracked compared to fully sealed."
}
},
{
"@type": "Question",
"name": "Is it safe to leave a dog in a parked car?",
"acceptedAnswer": {
"@type": "Answer",
"text": "No — on any warm or sunny day a sealed vehicle heats up rapidly to temperatures that can cause heatstroke and death in animals within minutes. Dogs cannot cool themselves as efficiently as humans and are at risk at cabin temperatures above 32 °C. SunScope's Vehicle interior column gives an hour-by-hour estimate of cabin temperature so you can see at a glance whether conditions are safe."
}
},
{
"@type": "Question",
"name": "How do I keep my house cool in a heatwave without air conditioning?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Keep curtains and blinds closed during the day to block direct solar gain, then open windows once outdoor air drops below the indoor temperature — usually in the evening or early morning. SunScope's Home profile lets you select your building type (brick, modern, Victorian terrace, stone cottage, timber frame, flat, or conservatory) so the model reflects how your building actually heats up. Ticking Managed shows the effect of following this advice hour by hour — so you can see exactly when to open up and how much difference it makes."
}
},
{
"@type": "Question",
"name": "Why is my house hotter in the evening than during the day?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Building materials absorb heat slowly throughout the day and release it gradually over several hours — a property called thermal mass. How pronounced the effect is depends on construction: a stone cottage or Victorian terrace stays warm well into the night, while a timber frame new build or conservatory responds almost immediately to outdoor changes. SunScope's building type selector lets you match the model to your actual home so the indoor temperature lag it shows reflects your building rather than an average."
}
},
{
"@type": "Question",
"name": "How hot does pavement or concrete get in summer?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Exposed concrete and tarmac can be 1525 °C hotter than the surrounding air on a sunny day. Unlike grass they have no evaporative cooling and absorb a large fraction of solar radiation. This is a hazard for dogs on pavements, barefoot children, and cyclists. SunScope's Concrete surface column estimates this temperature using an energy-balance model driven by the real solar radiation forecast."
}
},
{
"@type": "Question",
"name": "What is the difference between UV-A and UV-B?",
"acceptedAnswer": {
"@type": "Answer",
"text": "UV-A has a longer wavelength and penetrates deeper into the skin. It is present throughout all daylight hours, passes through glass, and is the primary cause of long-term skin ageing. UV-B has a shorter wavelength, causes sunburn directly, and drives vitamin D production. UV-B intensity falls sharply at low solar elevations. SunScope shows both indices separately so you can make informed decisions about both immediate burn risk and long-term UV exposure."
}
},
{
"@type": "Question",
"name": "How long can I spend in the sun before I get sunburned?",
"acceptedAnswer": {
"@type": "Answer",
"text": "It depends on your skin type and the current UV index. SunScope's Burn time column estimates the minutes to one Minimal Erythemal Dose (MED) — the threshold for the onset of sunburn — based on the UV forecast and your Fitzpatrick skin type. Fair skin (type III) may burn in under 10 minutes at UV index 8+; darker skin can tolerate significantly longer exposure. Figures assume no sunscreen and direct unshaded exposure."
}
},
{
"@type": "Question",
"name": "What soil temperature do seeds need to germinate?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Most vegetable seeds need soil temperatures above 710 °C to germinate reliably, with warm-season crops like courgettes and beans requiring 1215 °C or more. SunScope shows both surface soil temperature and 6 cm depth temperature — the root zone where germination actually happens — so you can time sowing decisions accurately."
}
},
{
"@type": "Question",
"name": "How can I tell if the ground is too wet for a motorhome or caravan?",
"acceptedAnswer": {
"@type": "Answer",
"text": "SunScope's soil moisture column shows volumetric water content in m³/m³. A reading above 0.4 indicates saturated ground where a heavy vehicle is likely to sink or churn the surface. Below 0.2 is dry and firm. The Vehicle profile includes soil moisture by default for motorhome and caravan users assessing pitch suitability."
}
},
{
"@type": "Question",
"name": "What is the difference between UTCI and heat index?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Heat index is a simple table-based formula that adjusts air temperature for humidity only — it was designed for shade conditions and ignores wind and solar radiation. UTCI is a full physiological model accounting for all four factors simultaneously: air temperature, humidity, wind speed, and mean radiant temperature from solar radiation and surrounding surfaces. In sunny or windy conditions the two values can differ by 1020 °C. UTCI is the standard used in European heat-health warning systems."
} }
}, },
{ {
@@ -56,7 +144,23 @@
"name": "How accurate is SunScope?", "name": "How accurate is SunScope?",
"acceptedAnswer": { "acceptedAnswer": {
"@type": "Answer", "@type": "Answer",
"text": "SunScope uses Open-Meteo forecast data, which is sourced from ECMWF and national meteorological services. The UTCI calculation follows the peer-reviewed Bröde 2012 polynomial. The soak-factor is an original model and should be treated as an informed estimate rather than a scientific measurement." "text": "SunScope uses Open-Meteo forecast data sourced from ECMWF and national meteorological services. The UTCI calculation follows the peer-reviewed Bröde 2012 polynomial exactly. Derived columns (vehicle temperature, indoor temperature, concrete surface, UV) are physics-based estimates rather than measured values, and will vary based on local factors like building construction, shading, and surface colour."
}
},
{
"@type": "Question",
"name": "Does SunScope work outside the UK?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Yes — SunScope works for any location worldwide. Search for any city, town, or village using the search bar. The indoor temperature model offers seven building types — brick, modern insulated, Victorian terrace, stone cottage, timber frame, top-floor flat, and conservatory — so you can match the model to your actual building wherever you are. All other columns are fully location-agnostic."
}
},
{
"@type": "Question",
"name": "How far ahead does the forecast go?",
"acceptedAnswer": {
"@type": "Answer",
"text": "The free tier shows 3 days of hourly data. The forecast auto-refreshes every 5 minutes while the page is open, so the current hour always reflects the latest available data from Open-Meteo."
} }
} }
] ]
@@ -77,18 +181,79 @@
<h2>What is SunScope?</h2> <h2>What is SunScope?</h2>
<p> <p>
SunScope is a free outdoor thermal comfort forecast that goes beyond standard air temperature. SunScope is a free outdoor thermal comfort forecast that goes well beyond standard air
Most weather apps tell you it's 12 °C and leave you to guess whether that means t-shirt weather temperature. Most weather apps tell you it's 12 °C and leave you to guess whether that means
or a heavy coat. SunScope combines air temperature, humidity, wind speed, and solar radiation a t-shirt or a heavy coat. SunScope combines air temperature, humidity, wind speed, and solar
into a single <strong>felt temperature</strong> — so you know what to actually expect when you radiation into a single <strong>felt temperature</strong> — so you know what to actually expect
step outside. when you step outside.
</p> </p>
<p> <p>
It uses the <strong>Universal Thermal Climate Index (UTCI)</strong>, a peer-reviewed The core calculation is the <strong>Universal Thermal Climate Index (UTCI)</strong>, a
biometeorological standard developed by <a href="https://utci.org/" target="_blank" rel="noopener noreferrer">Bröde et al. (2012)</a> peer-reviewed biometeorological standard developed by
and used globally in heat-health warning systems and urban planning. Forecast data is sourced <a href="https://utci.org/" target="_blank" rel="noopener noreferrer">Bröde et al. (2012)</a>
in real time from <strong>Open-Meteo</strong>, a free and open-source weather API drawing on and used globally in heat-health warning systems and urban planning. On top of that, SunScope
ECMWF and national met services. SunScope is available for any location worldwide. layers its own <strong>soak-factor</strong> precipitation penalty — because UTCI alone doesn't
account for the extra chill of rain soaking through your clothes on a windy day. The combined
result is the <strong>UTCI+P</strong> score: a more honest, real-world comfort number.
</p>
<p>
Forecast data is sourced in real time from <strong>Open-Meteo</strong>, a free and open-source
weather API drawing on ECMWF and national meteorological services. The forecast refreshes
automatically every five minutes and is available for any location worldwide.
</p>
<h2>What else does SunScope calculate?</h2>
<p>
SunScope goes far beyond a single felt-temperature number. Depending on the profile you choose,
it can calculate and display a wide range of derived quantities, each built on the same
per-hour weather data:
</p>
<p>
<strong>Vehicle interior temperature.</strong> A physics-based model estimates how hot a sealed,
parked vehicle gets over time — combining heat conducted through the roof and body panels with
direct solar gain through the side windows. You can select your vehicle type (car, MPV, SUV,
motorhome, or caravan), each of which has its own albedo, glazing area, and insulation
characteristics. A <em>Windows open</em> toggle switches to a ventilated model where convective
loss is roughly five times higher, dramatically reducing cabin heat build-up — mirroring the
real-world effect of cracking the windows.
</p>
<p>
<strong>Indoor temperature.</strong> The <em>Indoors</em> column estimates the temperature
inside a building with windows closed and no air conditioning, simulating wall conduction,
window solar gain, and thermal mass hour by hour. You choose your building type from a dropdown
— brick, modern insulated, Victorian terrace, stone cottage, timber frame, top-floor flat, or
conservatory — and the physics model adjusts accordingly. Each type has its own insulation
level, thermal mass, and glazing characteristics, so a stone cottage and a conservatory behave
very differently on a hot day. Indoor temperatures typically peak 24 hours after the outdoor
peak, which is why a house can still feel stifling at 10 pm on a summer day. Tick
<em>Managed</em> alongside the dropdown to switch to the heatwave-advice model: curtains
closed by day to cut solar gain, windows opened whenever outdoor air is cooler than inside.
</p>
<p>
<strong>Urban concrete surface temperature.</strong> Concrete and tarmac absorb far more solar
energy than grass, and unlike grass they have no evaporative cooling. On a sunny summer
afternoon, exposed paving can run 1525 °C hotter than the surrounding air. SunScope models
this using a surface energy balance approach that accounts for solar absorption, surface
emissivity, and convective cooling by wind.
</p>
<p>
<strong>UV and sunburn.</strong> SunScope splits the solar UV spectrum into UV-A and UV-B
components. The burn-time column estimates how long before you reach one Minimal Erythemal
Dose (the clinical threshold for sunburn) based on the UV index and your Fitzpatrick skin type
— selected from a dropdown in the column bar.
</p>
<p>
<strong>Soil temperature and moisture.</strong> Surface and 6 cm soil temperatures are shown for
agricultural and outdoor-work planning. Soil moisture indicates whether the ground is workable
or saturated — particularly useful for motorhome and caravan pitching.
</p>
<p>
<strong>The sky scope.</strong> The large circular visualisation at the top of the page shows
the sky conditions for the currently selected hour. The sky colour smoothly interpolates through
a full set of elevation-keyed gradients — from deep pre-dawn navy through golden sunrise, bright
midday blue, warm sunset amber, and back to night — with a sun disc whose glow radius and
intensity scale with the actual shortwave radiation at that moment. It refreshes automatically
alongside the forecast.
</p> </p>
<h2>What is the SunScope soak-factor?</h2> <h2>What is the SunScope soak-factor?</h2>
@@ -182,15 +347,16 @@
<div class="profile-block"> <div class="profile-block">
<p class="profile-name">🏠 Home</p> <p class="profile-name">🏠 Home</p>
<p> <p>
For managing indoor comfort without air conditioning — the typical UK home scenario. For managing indoor comfort without air conditioning. The indoor temperature column is
The two indoor temperature columns are central: <strong>Indoors</strong> estimates the central: select your building type from the dropdown (UK brick, modern insulated, Victorian
ambient temperature inside a typical brick house with windows closed and no active cooling, terrace, stone cottage, timber frame, top-floor flat, or conservatory) and the physics model
accounting for wall conduction, window solar gain, and thermal mass (indoor temperatures adjusts for that building's insulation, thermal mass, and glazing characteristics. Tick
usually peak 24 hours after the outdoor peak). <strong>Managed indoors</strong> shows <strong>Managed</strong> to switch to the heatwave-advice model — curtains closed by day
what you could achieve by following standard heatwave advice — closing curtains to block to block solar gain, windows opened whenever outdoor air is cooler than inside.
direct solar gain and opening windows whenever the outdoor air is cooler than inside. Indoor temperatures typically peak 24 hours after the outdoor peak due to thermal mass,
Supporting columns include <strong>air temperature</strong>, <strong>relative humidity</strong> and the model reflects this lag for each building type. Supporting columns include
and <strong>dew point</strong> (important for condensation and mould risk in cooler months), <strong>air temperature</strong>, <strong>relative humidity</strong> and
<strong>dew point</strong> (important for condensation and mould risk in cooler months),
<strong>wind speed</strong> for ventilation decisions, <strong>cloud cover</strong>, <strong>wind speed</strong> for ventilation decisions, <strong>cloud cover</strong>,
and <strong>precipitation</strong> — so you know whether to open the windows or bring and <strong>precipitation</strong> — so you know whether to open the windows or bring
the washing in. the washing in.
@@ -227,8 +393,8 @@
<tr><td><strong>Soil moisture</strong></td><td>Volumetric water content of the top 1 cm of soil in m³/m³. Above 0.4 indicates saturated ground; below 0.2 is dry. Useful for assessing whether ground is workable, and for motorhome or caravan pitch suitability.</td></tr> <tr><td><strong>Soil moisture</strong></td><td>Volumetric water content of the top 1 cm of soil in m³/m³. Above 0.4 indicates saturated ground; below 0.2 is dry. Useful for assessing whether ground is workable, and for motorhome or caravan pitch suitability.</td></tr>
<tr><td><strong>Concrete °C</strong></td><td>Estimated temperature of sun-exposed urban 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.</td></tr> <tr><td><strong>Concrete °C</strong></td><td>Estimated temperature of sun-exposed urban 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.</td></tr>
<tr><td><strong>Vehicle °C</strong></td><td>Estimated ambient cabin temperature inside a sealed, parked vehicle — adjusted for your chosen vehicle type. Values above 35 °C are dangerous for children and pets; above 45 °C potentially fatal within minutes.</td></tr> <tr><td><strong>Vehicle °C</strong></td><td>Estimated ambient cabin temperature inside a sealed, parked vehicle — adjusted for your chosen vehicle type. Values above 35 °C are dangerous for children and pets; above 45 °C potentially fatal within minutes.</td></tr>
<tr><td><strong>Indoors °C</strong></td><td>Estimated temperature inside a typical UK brick house with windows closed and no active cooling. Accounts for wall conduction, window solar gain, and thermal mass — indoor peak typically lags the outdoor peak by 24 hours.</td></tr> <tr><td><strong>Indoors °C</strong></td><td>Estimated temperature inside a building with windows closed and no active cooling. Select your building type from the dropdown — brick, modern insulated, Victorian terrace, stone cottage, timber frame, top-floor flat, or conservatory. Each type has its own insulation, thermal mass, and glazing characteristics. Indoor peak typically lags the outdoor peak by 24 hours due to thermal mass.</td></tr>
<tr><td><strong>Managed °C</strong></td><td>Estimated indoor temperature following standard UK heatwave advice: curtains closed during the day to block solar gain, windows opened whenever outdoor air is cooler than inside. Shows the benefit of passive cooling over doing nothing.</td></tr> <tr><td><strong>Managed °C</strong></td><td>The same building model with two interventions applied: curtains closed by day to block solar gain, and windows opened whenever outdoor air is cooler than inside. Toggle with the Managed tick next to the building dropdown. Shows how much passive cooling can reduce indoor heat compared to doing nothing.</td></tr>
</tbody> </tbody>
</table> </table>
@@ -302,30 +468,110 @@
<h2>Frequently asked questions</h2> <h2>Frequently asked questions</h2>
<div class="faq-block"> <details class="faq-block">
<p class="faq-q">Why does SunScope sometimes show a much higher temperature than my regular weather app?</p> <summary class="faq-q">Why does SunScope sometimes show a much higher temperature than my regular weather app?</summary>
<p class="faq-a">On clear, sunny days direct solar radiation heats your skin significantly — sometimes adding 10 °C or more to what the air temperature alone would suggest. SunScope calculates Mean Radiant Temperature from shortwave radiation data, capturing this effect. Standard weather apps ignore it entirely.</p> <p class="faq-a">On clear, sunny days direct solar radiation heats your skin significantly — sometimes adding 10 °C or more to what the air temperature alone would suggest. SunScope calculates Mean Radiant Temperature from shortwave radiation data, capturing this effect. Standard weather apps ignore it entirely.</p>
</div> </details>
<div class="faq-block"> <details class="faq-block">
<p class="faq-q">Why does SunScope sometimes show a lower temperature than my regular weather app?</p> <summary class="faq-q">Why does SunScope sometimes show a lower temperature than my regular weather app?</summary>
<p class="faq-a">Wind chill and precipitation both reduce felt temperature. If it's raining or blustery, the UTCI+P value will be noticeably lower than the thermometer reading — which is exactly what your body experiences.</p> <p class="faq-a">Wind chill and precipitation both reduce felt temperature. If it's raining or blustery, the UTCI+P value will be noticeably lower than the thermometer reading — which is exactly what your body experiences.</p>
</div> </details>
<div class="faq-block"> <details class="faq-block">
<p class="faq-q">How far ahead does the forecast go?</p> <summary class="faq-q">What is UTCI and why is it better than a standard feels-like temperature?</summary>
<p class="faq-a">SunScope currently shows a 72-hour hourly forecast. Data refreshes automatically from Open-Meteo each time you load the page.</p> <p class="faq-a">The Universal Thermal Climate Index (UTCI) is a peer-reviewed biometeorological standard developed by Bröde et al. (2012) and used in public health systems worldwide. Unlike simple "feels like" values — which typically only apply a basic wind-chill or heat-index formula — UTCI combines air temperature, humidity, wind speed, and the full solar radiation load using a physiological model of the human body. It accounts for the mean radiant temperature your skin "sees" from all surrounding surfaces, not just the air around you. This makes it a much more accurate predictor of actual thermal comfort and heat stress.</p>
</div> </details>
<div class="faq-block"> <details class="faq-block">
<p class="faq-q">Is SunScope free?</p> <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>
</details>
<details class="faq-block">
<summary class="faq-q">Does opening car windows reduce the temperature inside?</summary>
<p class="faq-a">Yes, significantly. Open windows allow convective airflow to flush hot cabin air out continuously, reducing the heat build-up by roughly 6080% compared to a sealed vehicle. SunScope's Vehicle column has a Ventilation toggle that switches the physics model to a ventilated state, showing you the estimated cabin temperature with windows open compared to fully sealed.</p>
</details>
<details class="faq-block">
<summary class="faq-q">Is it safe to leave a dog in a parked car?</summary>
<p class="faq-a">No — on any warm or sunny day, even with cloud cover, a sealed vehicle heats up rapidly to temperatures that can cause heatstroke and death in animals within minutes. Dogs cannot cool themselves as efficiently as humans and are at risk at cabin temperatures above 32 °C. SunScope's Vehicle interior column gives an hour-by-hour estimate of cabin temperature: if the forecast shows values in amber (above 35 °C) or red (above 45 °C), the vehicle is not safe for a dog, cat, or young child, even for a few minutes.</p>
</details>
<details class="faq-block">
<summary class="faq-q">How do I keep my house cool in a heatwave without air conditioning?</summary>
<p class="faq-a">The standard approach is to keep curtains and blinds closed during the day to block direct solar gain through windows, then open windows once the outdoor air temperature drops below the indoor temperature — usually in the evening or early morning. SunScope's Home profile lets you select your building type (brick, modern, Victorian terrace, stone cottage, timber frame, flat, or conservatory) so the model reflects how your building actually heats up. Ticking Managed shows what you could achieve by following this advice, hour by hour — so you can see exactly when to open up for maximum benefit and how much difference it actually makes.</p>
</details>
<details class="faq-block">
<summary class="faq-q">Why is my house hotter in the evening than during the day?</summary>
<p class="faq-a">Building materials absorb heat slowly throughout the day and release it gradually over several hours — a property called thermal mass. How pronounced the effect is depends heavily on construction: a solid stone cottage or Victorian terrace has very high thermal mass and can stay warm well into the night, while a timber frame new build or conservatory responds almost immediately to outdoor temperature changes. SunScope's building type dropdown lets you match the model to your actual home, so the indoor temperature lag it shows reflects your building rather than an average.</p>
</details>
<details class="faq-block">
<summary class="faq-q">How hot does pavement or concrete get in summer?</summary>
<p class="faq-a">Exposed concrete and tarmac can be 1525 °C hotter than the surrounding air on a sunny day. Unlike grass, they have no evaporative cooling mechanism and their dark colour means they absorb a large fraction of incoming solar radiation. This is a real hazard for dogs walking on pavements, barefoot children, and cyclists — and it also radiates heat back upward, making urban areas feel significantly warmer than open countryside. SunScope's Concrete surface column estimates this temperature using an energy-balance model driven by the real solar radiation forecast.</p>
</details>
<details class="faq-block">
<summary class="faq-q">What is UV-A and UV-B and what is the difference?</summary>
<p class="faq-a">UV-A has a longer wavelength and penetrates deeper into the skin. It is present throughout all daylight hours, passes through glass, and is the primary cause of long-term skin ageing and contributes to some skin cancers. UV-B has a shorter wavelength, causes sunburn directly, and drives vitamin D production. UV-B intensity is much more angle-dependent: it is strongest when the sun is high and falls sharply in the early morning and late afternoon due to the longer atmospheric path the radiation has to travel. SunScope shows both indices separately so you can make informed decisions about both immediate burn risk and long-term UV exposure.</p>
</details>
<details class="faq-block">
<summary class="faq-q">How long can I spend in the sun before I get sunburned?</summary>
<p class="faq-a">It depends on your skin type and the current UV index. SunScope's Burn time column estimates the minutes to one Minimal Erythemal Dose (MED) — the clinical threshold for the onset of sunburn — based on the UV forecast and your Fitzpatrick skin type, selectable from the column bar. Fair skin (type III) may burn in under 10 minutes at UV index 8+; darker skin (type VVI) can tolerate significantly longer exposure. These figures assume no sunscreen and direct, unshaded exposure.</p>
</details>
<details class="faq-block">
<summary class="faq-q">When is it safe to spray crops or apply pesticides?</summary>
<p class="faq-a">Safe spraying requires low wind speeds (typically below 3 m/s), low temperatures to minimise evaporation and drift, and ideally no rain forecast within several hours. SunScope's Farming profile shows wind speed and direction, air temperature, relative humidity, dew point (to assess inversion risk and overnight settling conditions), and precipitation — giving you all the variables you need in one view without switching between apps.</p>
</details>
<details class="faq-block">
<summary class="faq-q">What soil temperature do seeds need to germinate?</summary>
<p class="faq-a">Most vegetable seeds need soil temperatures above 710 °C to germinate reliably, with warm-season crops like courgettes, beans, and sweetcorn requiring 1215 °C or more. SunScope shows both surface soil temperature and 6 cm depth temperature — the root zone where germination actually happens. The 6 cm figure lags behind the surface by several hours and gives a more realistic picture of what seeds in the ground are actually experiencing.</p>
</details>
<details class="faq-block">
<summary class="faq-q">How can I tell if the ground is too wet for a motorhome or caravan?</summary>
<p class="faq-a">SunScope's soil moisture column shows the volumetric water content of the top centimetre of soil in m³/m³. A reading above 0.4 indicates saturated ground where a heavy vehicle is likely to sink or churn the surface. Below 0.2 is dry and firm. The Vehicle profile includes soil moisture by default precisely for motorhome and caravan users who need to assess pitch suitability before arriving on site.</p>
</details>
<details class="faq-block">
<summary class="faq-q">How far ahead does the forecast go?</summary>
<p class="faq-a">The free tier shows 3 days of hourly data. The forecast auto-refreshes every 5 minutes while the page is open, so the current hour always reflects the latest available data from Open-Meteo.</p>
</details>
<details class="faq-block">
<summary class="faq-q">How accurate is SunScope?</summary>
<p class="faq-a">SunScope uses Open-Meteo forecast data sourced from ECMWF and national meteorological services — the same underlying models used by professional forecasters. The UTCI calculation follows the peer-reviewed Bröde 2012 polynomial exactly. The derived columns (vehicle temperature, indoor temperature, concrete surface, UV) are physics-based estimates rather than measured values, and should be treated as informed approximations. Real-world values will vary based on local factors like building construction, shading, surface colour, and vegetation.</p>
</details>
<details class="faq-block">
<summary class="faq-q">Is SunScope free?</summary>
<p class="faq-a">SunScope beta is currently free to use for any location worldwide. As the product develops, some features may move to a paid tier — but core forecasting will always be accessible.</p> <p class="faq-a">SunScope beta is currently free to use for any location worldwide. As the product develops, some features may move to a paid tier — but core forecasting will always be accessible.</p>
</div> </details>
<div class="faq-block"> <details class="faq-block">
<p class="faq-q">What data sources does SunScope use?</p> <summary class="faq-q">What data sources does SunScope use?</summary>
<p class="faq-a">Forecast data comes from <a href="https://open-meteo.com/" target="_blank" rel="noopener noreferrer">Open-Meteo</a>, which aggregates ECMWF, GFS, and national meteorological service models. The UTCI calculation follows the Bröde et al. 2012 polynomial. Geolocation search uses the Open-Meteo geocoding API.</p> <p class="faq-a">Forecast data comes from <a href="https://open-meteo.com/" target="_blank" rel="noopener noreferrer">Open-Meteo</a>, which aggregates ECMWF, GFS, and national meteorological service models. The UTCI calculation follows the Bröde et al. 2012 polynomial. Geolocation search uses the Open-Meteo geocoding API.</p>
</div> </details>
<details class="faq-block">
<summary class="faq-q">Does SunScope work outside the UK?</summary>
<p class="faq-a">Yes — SunScope works for any location worldwide. Search for any city, town, or village using the search bar. The indoor temperature model offers seven building types — brick, modern insulated, Victorian terrace, stone cottage, timber frame, top-floor flat, and conservatory — so you can pick the one that best matches your building wherever you are. All other columns (UTCI, vehicle, UV, soil, concrete) are fully location-agnostic.</p>
</details>
<details class="faq-block">
<summary class="faq-q">What is the difference between UTCI and heat index?</summary>
<p class="faq-a">Heat index is a simple table-based formula that adjusts air temperature for humidity only — it was designed for shade conditions and ignores wind and solar radiation entirely. UTCI is a full physiological model that accounts for all four factors simultaneously: air temperature, humidity, wind speed, and the mean radiant temperature from solar radiation and surrounding surfaces. In sunny, windy, or mixed conditions, the two values can differ by 1020 °C. UTCI is the standard used in European heat-health warning systems and urban heat research.</p>
</details>
<details class="faq-block">
<summary class="faq-q">Why does the sky display show the wrong time of day?</summary>
<p class="faq-a">The sky scope visualisation reflects the hour you have selected in the forecast table, not necessarily the current time. Click on a different hour row to see how the sky will look (or looked) at that time, with the sun's position, sky colour, and glow intensity all calculated from the actual solar elevation and radiation data for that hour.</p>
</details>
<h2>Get in touch</h2> <h2>Get in touch</h2>
<p> <p>
+38 -3
View File
@@ -115,21 +115,56 @@ em { color: #7a5c2a; font-style: italic; }
background: #fef8ea; background: #fef8ea;
border: 1px solid #e0d0a8; border: 1px solid #e0d0a8;
border-radius: 8px; border-radius: 8px;
padding: 1.2rem 1.4rem; margin-bottom: 0.5rem;
margin-bottom: 1rem; overflow: hidden;
transition: border-color 0.15s;
}
.faq-block[open] {
border-color: #c8922a;
} }
.faq-q { .faq-q {
list-style: none;
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 1rem 1.2rem;
font-weight: 600; font-weight: 600;
color: #1e1208; color: #1e1208;
margin-bottom: 0.4rem;
font-size: 0.97rem; font-size: 0.97rem;
cursor: pointer;
margin: 0;
user-select: none;
}
.faq-q::-webkit-details-marker { display: none; }
.faq-q::after {
content: '+';
font-size: 1.3rem;
font-weight: 300;
color: #c8922a;
flex-shrink: 0;
line-height: 1;
transition: transform 0.2s;
}
.faq-block[open] .faq-q::after {
transform: rotate(45deg);
}
.faq-block[open] .faq-q {
border-bottom: 1px solid #e0d0a8;
} }
.faq-a { .faq-a {
color: #5a3e1a; color: #5a3e1a;
font-size: 0.93rem; font-size: 0.93rem;
margin: 0; margin: 0;
padding: 0.9rem 1.2rem 1rem;
line-height: 1.65;
} }
.stress-table { .stress-table {
+75
View File
@@ -238,6 +238,81 @@
border-color: #c8922a; border-color: #c8922a;
} }
/* ── Vehicle group: select + windows-open checkbox fused together ── */
.col-toggle-group {
display: inline-flex;
align-items: stretch;
gap: 0;
}
/* Remove the right border + radius on the select so it butts up flush
with the checkbox label. When the label is absent (hide state), the
select is the only child and :last-child restores full pill rounding. */
.col-toggle-group .grouped-left {
border-right: none;
border-radius: 20px 0 0 20px;
}
.col-toggle-group .grouped-left:last-child {
border-right: 1.5px solid #c9b08a;
border-radius: 20px;
}
/* The checkbox pill — same height/padding as a col-toggle but styled as a label */
.col-toggle-vent {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 4px 10px 4px 9px;
font-size: 9px;
font-family: JetBrains Mono, monospace;
letter-spacing: 0.08em;
text-transform: uppercase;
cursor: pointer;
border: 1.5px solid #c9b08a;
border-left: 1px solid #d4c0a0; /* softer join line */
border-radius: 0 20px 20px 0;
background: #fef8ee;
color: #7a5c30;
user-select: none;
transition: background 0.15s, color 0.15s, border-color 0.15s;
white-space: nowrap;
}
.col-toggle-vent:hover {
border-color: #c8922a;
color: #6b4f2a;
background: #fef3dc;
}
.col-toggle-vent.on {
background: #c8922a;
border-color: #c8922a;
color: #fff;
}
/* Custom checkbox square inside the vent label */
.vent-checkbox {
display: inline-block;
width: 12px;
height: 12px;
border: 1.5px solid currentColor;
border-radius: 3px;
background: transparent;
flex-shrink: 0;
position: relative;
transition: background 0.12s;
}
.vent-checkbox.checked::after {
content: '';
position: absolute;
left: 2px;
top: -1px;
width: 5px;
height: 8px;
border: 2px solid currentColor;
border-top: none;
border-left: none;
transform: rotate(45deg);
}
/* ── 7. HOURLY TABLE ────────────────────────────────────────────────── */ /* ── 7. HOURLY TABLE ────────────────────────────────────────────────── */
+12
View File
@@ -99,6 +99,18 @@
color: #6b4f2a; color: #6b4f2a;
} }
.utci-about-link {
display: inline-block;
margin-left: 4px;
color: #c8922a;
font-weight: 600;
text-decoration: none;
white-space: nowrap;
}
.utci-about-link:hover {
text-decoration: underline;
}
.utci-footer { .utci-footer {
margin-top: 36px; margin-top: 36px;
padding-top: 20px; padding-top: 20px;
+116 -72
View File
@@ -27,7 +27,7 @@ import { vaporPressureHpa, solarElevationDeg, calcTmrt, utciApprox, calcConcrete
import { import {
utciCategory, precipPenalty, windCompass8, uvSplit, utciCategory, precipPenalty, windCompass8, uvSplit,
SKIN_TYPES, sunburnMinutes, burnLabel, SKIN_TYPES, sunburnMinutes, burnLabel,
VEHICLE_TYPES, VEHICLE_TYPES, BUILDING_TYPES,
cloudCategory, confidenceBand, moonGlyph, cloudCategory, confidenceBand, moonGlyph,
} from './utils.js'; } from './utils.js';
import { SkyScope, WindVane, CloudIcon, ScopeReticle, PrecipIcon } from './components.js'; import { SkyScope, WindVane, CloudIcon, ScopeReticle, PrecipIcon } from './components.js';
@@ -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(true); const [isPro, setIsPro] = useState(false);
// 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.
@@ -178,6 +178,16 @@ 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');
// Vehicle ventilation — true = windows open (high convective loss).
const [vehicleVent, setVehicleVent] = useState(false);
// Indoor: buildingType drives the physics preset; indoorManaged toggles the
// curtains+ventilation model on top. indoorMode gates column visibility:
// 'off' = hidden, 'on' = indoorT shown (managed or not depending on indoorManaged).
const [buildingType, setBuildingType] = useState('brick');
const [indoorManaged, setIndoorManaged] = useState(false);
const [indoorMode, setIndoorMode] = useState('off');
const searchTimeout = useRef(null); const searchTimeout = useRef(null);
// Refs for the two-scroller table layout (sticky-to-viewport header + // Refs for the two-scroller table layout (sticky-to-viewport header +
@@ -579,7 +589,7 @@ export function UTCIForecast() {
// dt kept for SkyScope / backward compat — same as dtUTC. // dt kept for SkyScope / backward compat — same as dtUTC.
const dt = dtUTC; const dt = dtUTC;
const elev = solarElevationDeg(location.lat, location.lon, dtUTC); const elev = solarElevationDeg(location.lat, location.lon, dtUTC);
const vehicleT = calcVehicleInteriorTemp(Ta, glob, elev, vehicleType); const vehicleT = calcVehicleInteriorTemp(Ta, glob, elev, vehicleType, vehicleVent);
const eh = vaporPressureHpa(Ta, RH); const eh = vaporPressureHpa(Ta, RH);
const Tmrt = calcTmrt(Ta, dir, dif, glob, elev); const Tmrt = calcTmrt(Ta, dir, dif, glob, elev);
const utci = utciApprox(Ta, Tmrt, va, eh); const utci = utciApprox(Ta, Tmrt, va, eh);
@@ -605,8 +615,8 @@ export function UTCIForecast() {
const TaArr = hourlyRows.map(r => r.Ta); const TaArr = hourlyRows.map(r => r.Ta);
const globArr = hourlyRows.map(r => r.glob); const globArr = hourlyRows.map(r => r.glob);
const elevArr = hourlyRows.map(r => r.elev); const elevArr = hourlyRows.map(r => r.elev);
const indoorTemps = calcIndoorTempPass(TaArr, globArr, elevArr); const indoorTemps = calcIndoorTempPass(TaArr, globArr, elevArr, buildingType);
const managedTemps = calcManagedIndoorTempPass(TaArr, globArr, elevArr); const managedTemps = calcManagedIndoorTempPass(TaArr, globArr, elevArr, buildingType);
hourlyRows.forEach((r, i) => { r.indoorT = indoorTemps[i]; r.managedT = managedTemps[i]; }); hourlyRows.forEach((r, i) => { r.indoorT = indoorTemps[i]; r.managedT = managedTemps[i]; });
} }
@@ -957,19 +967,48 @@ export function UTCIForecast() {
} }
setActiveProfile(key); setActiveProfile(key);
if (key !== 'custom') setVisibleCols({ ...profile.cols }); if (key !== 'custom') setVisibleCols({ ...profile.cols });
// Auto-enable indoor column for profiles that include it;
// reset to off for profiles that don't.
if (key !== 'custom') {
const hasIndoor = profile.cols['indoorT'] || profile.cols['managedT'];
setIndoorMode(hasIndoor ? 'on' : 'off');
setIndoorManaged(false);
}
}} }}
>${profile.icon} ${profile.label}${locked ? ' 🔒' : ''}</button>`; >${profile.icon} ${profile.label}${locked ? ' 🔒' : ''}</button>`;
})} })}
</div> </div>
<!-- <!--
COLUMN TOGGLES Pro only. Hidden entirely for free users COLUMN TOGGLES in exact table column order.
(the profile selector above provides enough control for them). Buttons visible to all users when profile includes that col.
Burn + Vehicle dropdowns always shown (free + pro).
Pro users see all toggles; free users see profile-filtered subset.
--> -->
${(FILTER_PROFILES[activeProfile]?.cols['burn'] || FILTER_PROFILES[activeProfile]?.cols['vehicleT']) && html` ${(isPro || FILTER_PROFILES[activeProfile]?.cols['burn'] || FILTER_PROFILES[activeProfile]?.cols['vehicleT'] || FILTER_PROFILES[activeProfile]?.cols['indoorT'] || FILTER_PROFILES[activeProfile]?.cols['managedT']) && html`
<div class="col-toggles"> <div class="col-toggles">
<span class="col-toggles-label">Columns:</span> <span class="col-toggles-label">Columns:</span>
${FILTER_PROFILES[activeProfile]?.cols['burn'] && html`
${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['rh']) && html`<button class=${`col-toggle${visibleCols.rh ? ' on' : ''}`} onClick=${() => toggleCol('rh')}>RH</button>`}
${isPro && (activeProfile === 'custom' || FILTER_PROFILES[activeProfile].cols['dew']) && html`<button class=${`col-toggle${visibleCols.dew ? ' on' : ''}`} onClick=${() => toggleCol('dew')}>Dew</button>`}
${isPro && (activeProfile === 'custom' || FILTER_PROFILES[activeProfile].cols['soilT']) && html`<button class=${`col-toggle${visibleCols.soilT ? ' on' : ''}`} onClick=${() => toggleCol('soilT')}>Soil °C</button>`}
${isPro && (activeProfile === 'custom' || FILTER_PROFILES[activeProfile].cols['soilT6']) && html`<button class=${`col-toggle${visibleCols.soilT6 ? ' on' : ''}`} onClick=${() => toggleCol('soilT6')}>Soil 6cm</button>`}
${isPro && (activeProfile === 'custom' || FILTER_PROFILES[activeProfile].cols['soilM']) && html`<button class=${`col-toggle${visibleCols.soilM ? ' on' : ''}`} onClick=${() => toggleCol('soilM')}>Soil moist</button>`}
${isPro && (activeProfile === 'custom' || FILTER_PROFILES[activeProfile].cols['concreteT']) && html`<button class=${`col-toggle${visibleCols.concreteT ? ' on' : ''}`} onClick=${() => toggleCol('concreteT')}>Concrete</button>`}
${isPro && (activeProfile === 'custom' || FILTER_PROFILES[activeProfile].cols['wind']) && html`<button class=${`col-toggle${visibleCols.wind ? ' on' : ''}`} onClick=${() => toggleCol('wind')}>Wind</button>`}
${isPro && (activeProfile === 'custom' || FILTER_PROFILES[activeProfile].cols['dir']) && html`<button class=${`col-toggle${visibleCols.dir ? ' on' : ''}`} onClick=${() => toggleCol('dir')}>Dir</button>`}
${isPro && (activeProfile === 'custom' || FILTER_PROFILES[activeProfile].cols['cloud']) && html`<button class=${`col-toggle${visibleCols.cloud ? ' on' : ''}`} onClick=${() => toggleCol('cloud')}>Cloud</button>`}
${isPro && (activeProfile === 'custom' || FILTER_PROFILES[activeProfile].cols['sun']) && html`<button class=${`col-toggle${visibleCols.sun ? ' on' : ''}`} onClick=${() => toggleCol('sun')}>Sun</button>`}
${isPro && (activeProfile === 'custom' || FILTER_PROFILES[activeProfile].cols['direct']) && html`<button class=${`col-toggle${visibleCols.direct ? ' on' : ''}`} onClick=${() => toggleCol('direct')}>Direct</button>`}
${isPro && (activeProfile === 'custom' || FILTER_PROFILES[activeProfile].cols['diffuse']) && html`<button class=${`col-toggle${visibleCols.diffuse ? ' on' : ''}`} onClick=${() => toggleCol('diffuse')}>Diffuse</button>`}
${isPro && (activeProfile === 'custom' || FILTER_PROFILES[activeProfile].cols['tmrt']) && html`<button class=${`col-toggle${visibleCols.tmrt ? ' on' : ''}`} onClick=${() => toggleCol('tmrt')}>Tmrt</button>`}
${isPro && (activeProfile === 'custom' || FILTER_PROFILES[activeProfile].cols['delta']) && html`<button class=${`col-toggle${visibleCols.delta ? ' on' : ''}`} onClick=${() => toggleCol('delta')}>Δ</button>`}
${isPro && (activeProfile === 'custom' || FILTER_PROFILES[activeProfile].cols['utci']) && html`<button class=${`col-toggle${visibleCols.utci ? ' on' : ''}`} onClick=${() => toggleCol('utci')}>UTCI</button>`}
${isPro && (activeProfile === 'custom' || FILTER_PROFILES[activeProfile].cols['uvA']) && html`<button class=${`col-toggle${visibleCols.uvA ? ' on' : ''}`} onClick=${() => toggleCol('uvA')}>UV-A</button>`}
${isPro && (activeProfile === 'custom' || FILTER_PROFILES[activeProfile].cols['uvB']) && html`<button class=${`col-toggle${visibleCols.uvB ? ' on' : ''}`} onClick=${() => toggleCol('uvB')}>UV-B</button>`}
${(activeProfile === 'custom' || FILTER_PROFILES[activeProfile]?.cols['burn']) && html`
<select <select
class=${`col-toggle burn-select${visibleCols.burn ? ' on' : ''}`} class=${`col-toggle burn-select${visibleCols.burn ? ' on' : ''}`}
value=${visibleCols.burn ? skinType : 'off'} value=${visibleCols.burn ? skinType : 'off'}
@@ -985,13 +1024,16 @@ export function UTCIForecast() {
${Object.entries(SKIN_TYPES).map(([k, v]) => html` ${Object.entries(SKIN_TYPES).map(([k, v]) => html`
<option key=${k} value=${k}>${v.name.split(' · ')[1]} skin</option>`)} <option key=${k} value=${k}>${v.name.split(' · ')[1]} skin</option>`)}
</select>`} </select>`}
${FILTER_PROFILES[activeProfile]?.cols['vehicleT'] && html`
${(activeProfile === 'custom' || FILTER_PROFILES[activeProfile]?.cols['vehicleT']) && html`
<span class="col-toggle-group">
<select <select
class=${`col-toggle burn-select${visibleCols.vehicleT ? ' on' : ''}`} class=${`col-toggle burn-select grouped-left${visibleCols.vehicleT ? ' on' : ''}`}
value=${visibleCols.vehicleT ? vehicleType : 'off'} value=${visibleCols.vehicleT ? vehicleType : 'off'}
onChange=${(e) => { onChange=${(e) => {
if (e.target.value === 'off') { if (e.target.value === 'off') {
setVisibleCols(prev => ({ ...prev, vehicleT: false })); setVisibleCols(prev => ({ ...prev, vehicleT: false }));
setVehicleVent(false);
} else { } else {
setVehicleType(e.target.value); setVehicleType(e.target.value);
setVisibleCols(prev => ({ ...prev, vehicleT: true })); setVisibleCols(prev => ({ ...prev, vehicleT: true }));
@@ -1000,48 +1042,54 @@ export function UTCIForecast() {
<option value="off">${visibleCols.vehicleT ? 'Hide Vehicle' : 'Vehicle'}</option> <option value="off">${visibleCols.vehicleT ? 'Hide Vehicle' : 'Vehicle'}</option>
${Object.entries(VEHICLE_TYPES).map(([k, v]) => html` ${Object.entries(VEHICLE_TYPES).map(([k, v]) => html`
<option key=${k} value=${k}>${v.name}</option>`)} <option key=${k} value=${k}>${v.name}</option>`)}
</select>`} </select>
</div>`} ${visibleCols.vehicleT && html`
<label
class=${`col-toggle-vent grouped-right${vehicleVent ? ' on' : ''}`}
title="Ventilation — open windows significantly reduce cabin heat build-up">
<input
type="checkbox"
checked=${vehicleVent}
onChange=${() => setVehicleVent(v => !v)}
style=${{ position: 'absolute', opacity: 0, width: 0, height: 0 }} />
<span class="vent-checkbox${vehicleVent ? ' checked' : ''}"></span>
Ventilation
</label>`}
</span>`}
${isPro && html` ${(activeProfile === 'custom' || FILTER_PROFILES[activeProfile].cols['indoorT'] || FILTER_PROFILES[activeProfile].cols['managedT']) && html`
<div class="col-toggles"> <span class="col-toggle-group">
<span class="col-toggles-label">Columns:</span> <select
${[ class=${`col-toggle burn-select grouped-left${indoorMode === 'on' ? ' on' : ''}`}
// Hour and UTCI+P are always-on — no toggle button for them. value=${indoorMode === 'on' ? buildingType : 'off'}
{ key: 'air', label: 'Air' }, onChange=${(e) => {
{ key: 'rh', label: 'RH' }, if (e.target.value === 'off') {
{ key: 'dew', label: 'Dew' }, setIndoorMode('off');
{ key: 'soilT', label: 'Soil °C' }, setIndoorManaged(false);
{ key: 'soilT6', label: 'Soil 6cm' }, } else {
{ key: 'soilM', label: 'Soil moist' }, setBuildingType(e.target.value);
{ key: 'concreteT', label: 'Concrete' }, setIndoorMode('on');
{ key: 'indoorT', label: 'Indoors' }, }
{ key: 'managedT', label: 'Managed' }, }}>
{ key: 'wind', label: 'Wind' }, <option value="off">${indoorMode === 'on' ? 'Hide Indoors' : 'Indoors'}</option>
{ key: 'dir', label: 'Dir' }, ${Object.entries(BUILDING_TYPES).map(([k, v]) => html`
{ key: 'cloud', label: 'Cloud' }, <option key=${k} value=${k}>${v.name}</option>`)}
{ key: 'sun', label: 'Sun' }, </select>
{ key: 'direct', label: 'Direct' }, ${indoorMode === 'on' && html`
{ key: 'diffuse', label: 'Diffuse' }, <label
{ key: 'tmrt', label: 'Tmrt' }, class=${`col-toggle-vent grouped-right${indoorManaged ? ' on' : ''}`}
{ key: 'delta', label: 'Δ' }, title="Managed: curtains closed by day, windows open when cooler outside">
{ key: 'utci', label: 'UTCI' }, <input
{ key: 'uvA', label: 'UV-A' }, type="checkbox"
{ key: 'uvB', label: 'UV-B' }, checked=${indoorManaged}
].filter(c => onChange=${() => setIndoorManaged(v => !v)}
activeProfile === 'custom' || !!FILTER_PROFILES[activeProfile].cols[c.key] style=${{ position: 'absolute', opacity: 0, width: 0, height: 0 }} />
).map(c => html` <span class="vent-checkbox${indoorManaged ? ' checked' : ''}"></span>
<button Managed
key=${c.key} </label>`}
class=${`col-toggle${visibleCols[c.key] ? ' on' : ''}`} </span>`}
onClick=${() => toggleCol(c.key)} ${isPro && (activeProfile === 'custom' || FILTER_PROFILES[activeProfile].cols['precip']) && html`<button class=${`col-toggle${visibleCols.precip ? ' on' : ''}`} onClick=${() => toggleCol('precip')}>Precip</button>`}
>${c.label}</button>`)}
${(activeProfile === 'custom' || FILTER_PROFILES[activeProfile].cols['precip']) && html`
<button
class=${`col-toggle${visibleCols.precip ? ' on' : ''}`}
onClick=${() => toggleCol('precip')}
>Precip</button>`}
</div>`} </div>`}
<!-- <!--
@@ -1084,8 +1132,8 @@ export function UTCIForecast() {
${visibleCols.uvB && html`<th class="col-info-th" onClick=${(e) => handleThClick('uvB', e)}>UV-B <span class="col-unit">est. idx</span></th>`} ${visibleCols.uvB && html`<th class="col-info-th" onClick=${(e) => handleThClick('uvB', e)}>UV-B <span class="col-unit">est. idx</span></th>`}
${visibleCols.burn && html`<th class="col-info-th" onClick=${(e) => handleThClick('burn', e)}>Burn <span class="col-unit">to MED</span></th>`} ${visibleCols.burn && html`<th class="col-info-th" onClick=${(e) => handleThClick('burn', e)}>Burn <span class="col-unit">to MED</span></th>`}
${visibleCols.vehicleT && html`<th class="col-info-th" onClick=${(e) => handleThClick('vehicleT', e)}>Vehicle <span class="col-unit">°C peak</span></th>`} ${visibleCols.vehicleT && html`<th class="col-info-th" onClick=${(e) => handleThClick('vehicleT', e)}>Vehicle <span class="col-unit">°C peak</span></th>`}
${visibleCols.indoorT && html`<th class="col-info-th" onClick=${(e) => handleThClick('indoorT', e)}>Indoors <span class="col-unit">°C est.</span></th>`} ${indoorMode === 'on' && !indoorManaged && html`<th class="col-info-th" onClick=${(e) => handleThClick('indoorT', e)}>Indoors <span class="col-unit">°C est.</span></th>`}
${visibleCols.managedT && html`<th class="col-info-th" onClick=${(e) => handleThClick('managedT', e)}>Managed <span class="col-unit">°C est.</span></th>`} ${indoorMode === 'on' && indoorManaged && html`<th class="col-info-th" onClick=${(e) => handleThClick('managedT', e)}>Managed <span class="col-unit">°C est.</span></th>`}
${visibleCols.precip && html`<th class="utci-tight-head col-info-th" onClick=${(e) => handleThClick('precip', e)}>Pcpt <span class="col-unit">mm/h</span></th>`} ${visibleCols.precip && html`<th class="utci-tight-head col-info-th" onClick=${(e) => handleThClick('precip', e)}>Pcpt <span class="col-unit">mm/h</span></th>`}
${visibleCols.utciP && html`<th class="col-info-th" onClick=${(e) => handleThClick('utciP', e)}>UTCI+P <span class="col-unit">°C adj.</span></th>`} ${visibleCols.utciP && html`<th class="col-info-th" onClick=${(e) => handleThClick('utciP', e)}>UTCI+P <span class="col-unit">°C adj.</span></th>`}
</tr> </tr>
@@ -1193,11 +1241,11 @@ export function UTCIForecast() {
<td style=${{ color: r.vehicleT != null && r.vehicleT > 45 ? '#c0392b' : r.vehicleT != null && r.vehicleT > 35 ? '#e67e22' : '#7f8c8d', fontWeight: 'bold', background: tempBg(r.vehicleT) }}> <td style=${{ color: r.vehicleT != null && r.vehicleT > 45 ? '#c0392b' : r.vehicleT != null && r.vehicleT > 35 ? '#e67e22' : '#7f8c8d', fontWeight: 'bold', background: tempBg(r.vehicleT) }}>
${r.vehicleT != null ? r.vehicleT.toFixed(1) : ''} ${r.vehicleT != null ? r.vehicleT.toFixed(1) : ''}
</td>`} </td>`}
${visibleCols.indoorT && html` ${indoorMode === 'on' && !indoorManaged && html`
<td style=${{ color: r.indoorT != null && r.indoorT > 32 ? '#c0392b' : r.indoorT != null && r.indoorT > 26 ? '#e67e22' : '#4a7a4a', fontWeight: 'bold', background: tempBg(r.indoorT) }}> <td style=${{ color: r.indoorT != null && r.indoorT > 32 ? '#c0392b' : r.indoorT != null && r.indoorT > 26 ? '#e67e22' : '#4a7a4a', fontWeight: 'bold', background: tempBg(r.indoorT) }}>
${r.indoorT != null ? r.indoorT.toFixed(1) : ''} ${r.indoorT != null ? r.indoorT.toFixed(1) : ''}
</td>`} </td>`}
${visibleCols.managedT && html` ${indoorMode === 'on' && indoorManaged && html`
<td style=${{ color: r.managedT != null && r.managedT > 32 ? '#c0392b' : r.managedT != null && r.managedT > 26 ? '#e67e22' : '#4a7a4a', fontWeight: 'bold', background: tempBg(r.managedT) }}> <td style=${{ color: r.managedT != null && r.managedT > 32 ? '#c0392b' : r.managedT != null && r.managedT > 26 ? '#e67e22' : '#4a7a4a', fontWeight: 'bold', background: tempBg(r.managedT) }}>
${r.managedT != null ? r.managedT.toFixed(1) : ''} ${r.managedT != null ? r.managedT.toFixed(1) : ''}
</td>`} </td>`}
@@ -1264,25 +1312,21 @@ export function UTCIForecast() {
<div class="utci-about"> <div class="utci-about">
<h2 class="utci-about-heading">What is SunScope?</h2> <h2 class="utci-about-heading">What is SunScope?</h2>
<p class="utci-about-text"> <p class="utci-about-text">
SunScope shows how the weather will actually <em>feel</em> on your body not just the air SunScope is a free hourly weather forecast built around <strong>felt temperature</strong>,
temperature. It uses the <strong>Universal Thermal Climate Index (UTCI)</strong>, a not just air temperature. It uses the <strong>Universal Thermal Climate Index (UTCI)</strong>
peer-reviewed biometeorological standard developed by Bröde et al. (2012) that combines the biometeorological standard used in heat-health warning systems worldwide to combine
air temperature, humidity, wind speed, and solar radiation into a single <em>felt air temperature, humidity, wind, and solar radiation into a single honest number. The
temperature</em>. On a calm, sunny winter day UTCI can read several degrees warmer than <strong>UTCI+P</strong> column adds an original rain and snow penalty so wet, windy days
the thermometer; on a grey, blustery day it can read far colder. Forecast data is read as cold as they feel.
sourced in real time from <strong>Open-Meteo</strong>, a free and open-source weather API,
and solar radiation is used to calculate Mean Radiant Temperature the heat your skin
absorbs from the sun making SunScope one of the most complete outdoor comfort forecasts
available for free.
</p> </p>
<p class="utci-about-text"> <p class="utci-about-text">
The <strong>UTCI+P</strong> column adds the <strong>SunScope soak-factor</strong>: an Beyond felt temperature, SunScope calculates <strong>vehicle cabin heat</strong> (choose
original precipitation penalty that accounts for the extra chill of rain and snow on your vehicle type; toggle windows open), <strong>indoor temperature</strong> (seven
exposed skin and wet clothing. Light drizzle reduces the felt temperature by around building types; managed heatwave mode), <strong>urban concrete surface temperature</strong>,
12 °C; heavy rain combined with wind can push it down by 78 °C. Snow carries an <strong>UV index and sunburn time</strong> by skin type, and <strong>soil temperature
additional penalty on top. The result is an honest, real-world comfort score for any and moisture</strong> for farming and motorhome use. Switch profiles to see the data
location worldwide simply search for your town or city and compare the hourly that matters for your situation or go Custom and build your own view.
forecast across the next 3 days (and up to 14 days with SunScope Extra). <a href="./about.html" class="utci-about-link">Learn more </a>
</p> </p>
</div> </div>
+26 -40
View File
@@ -7,13 +7,15 @@
// solarElevationDeg(lat, lon, dateUTC) NOAA simplified solar position // solarElevationDeg(lat, lon, dateUTC) NOAA simplified solar position
// calcTmrt(Ta, dirRad, diffRad, globalRad, solElev) Mean radiant temp // calcTmrt(Ta, dirRad, diffRad, globalRad, solElev) Mean radiant temp
// utciApprox(Ta, Tmrt, va10, ehPa) Bröde et al. 2012 polynomial // utciApprox(Ta, Tmrt, va10, ehPa) Bröde et al. 2012 polynomial
// calcVehicleInteriorTemp(Ta, globalRad, solElev, vehicleType) // calcVehicleInteriorTemp(Ta, globalRad, solElev, vehicleType, ventilated)
// calcIndoorTempPass(TaArr, globArr, elevArr, buildingType)
// calcManagedIndoorTempPass(TaArr, globArr, elevArr, buildingType)
// //
// Nothing in here should need editing unless the underlying science // Nothing in here should need editing unless the underlying science
// changes. All numbers are peer-reviewed constants or coefficients. // changes. All numbers are peer-reviewed constants or coefficients.
// ════════════════════════════════════════════════════════════════════════ // ════════════════════════════════════════════════════════════════════════
import { VEHICLE_TYPES } from './utils.js'; import { VEHICLE_TYPES, BUILDING_TYPES } from './utils.js';
// ═══════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════
// PHYSICAL CONSTANTS // PHYSICAL CONSTANTS
@@ -120,16 +122,13 @@ function _houseHeatLoad(Ta, globalRad, solElev) {
// Two-pass function: call with the full arrays of hourly Ta and globalRad. // Two-pass function: call with the full arrays of hourly Ta and globalRad.
// Returns an array of indoor temperatures, one per hour. // Returns an array of indoor temperatures, one per hour.
export function calcIndoorTempPass(TaArr, globArr, elevArr) { // buildingType must be a key of BUILDING_TYPES; defaults to 'brick'.
export function calcIndoorTempPass(TaArr, globArr, elevArr, buildingType = 'brick') {
const preset = BUILDING_TYPES[buildingType] || BUILDING_TYPES.brick;
const { uWall, lagHours, glazingRatio, gValue, orientFactor } = preset;
const n = TaArr.length; const n = TaArr.length;
const result = new Array(n); const result = new Array(n);
// Thermal resistance of the building envelope (°C per W/m² of heat load)
// Lower = faster response to outdoor changes. UK brick mid-stock ~0.45.
const uWall = 0.35; // W/m²K effective wall U-value
// Thermal mass time constant: heavier = longer lag.
// ~4 h lag for typical UK brick semi (expressed as exponential decay weight).
const lagHours = 4;
const alpha = 1 - Math.exp(-1 / lagHours); // per-hour blending weight const alpha = 1 - Math.exp(-1 / lagHours); // per-hour blending weight
// Seed indoor temp to first outdoor temp // Seed indoor temp to first outdoor temp
@@ -138,30 +137,22 @@ export function calcIndoorTempPass(TaArr, globArr, elevArr) {
for (let i = 0; i < n; i++) { for (let i = 0; i < n; i++) {
const Ta = TaArr[i] ?? Ti; const Ta = TaArr[i] ?? Ti;
const glob = globArr[i] ?? 0; const glob = globArr[i] ?? 0;
const solElev = elevArr[i] ?? 0;
// Solar gain through windows (W/m² effective) // Solar gain through windows (W/m² effective)
const glazingRatio = 0.16;
const gValue = 0.63;
const orientFactor = 0.50;
const solarGain = glob * glazingRatio * gValue * orientFactor; const solarGain = glob * glazingRatio * gValue * orientFactor;
// Conductive heat flow through walls: proportional to (Ta - Ti) // Conductive heat flow through walls: proportional to (Ta - Ti)
// uWall drives how quickly the indoor temp chases outdoor temp.
const conductionGain = uWall * (Ta - Ti); const conductionGain = uWall * (Ta - Ti);
// Target indoor temp this hour if there were no thermal mass: // Heat capacity proxy: how many °C does 1 W/m² raise the indoor air?
// Ti_instant = Ti + conduction + solar load / heat capacity proxy // Typical 90 m² house ≈ 0.15; conservatory much lower (less thermal mass).
// heatCap proxy: how many degrees does 1 W/m² raise the indoor air?
// For a typical 90 m² house, ~0.15 °C per W/m² effective.
const heatCapProxy = 0.15; const heatCapProxy = 0.15;
const Ti_instant = Ti + (conductionGain + solarGain) * heatCapProxy; const Ti_instant = Ti + (conductionGain + solarGain) * heatCapProxy;
// Apply thermal lag: blend toward Ti_instant slowly // Apply thermal lag: blend toward Ti_instant at the building's time constant
Ti = Ti + alpha * (Ti_instant - Ti); Ti = Ti + alpha * (Ti_instant - Ti);
// Can't be colder than outdoor (house doesn't actively cool) // Can't be colder than outdoor (house doesn't actively cool)
// Cap at 55 °C (physically implausible above this for a house interior)
result[i] = Math.max(Math.min(Ti, 55), Math.min(Ta, Ti)); result[i] = Math.max(Math.min(Ti, 55), Math.min(Ta, Ti));
} }
@@ -188,34 +179,25 @@ export function calcIndoorTempPass(TaArr, globArr, elevArr) {
// //
// Same thermal lag model as calcIndoorTempPass (4 h brick time constant). // Same thermal lag model as calcIndoorTempPass (4 h brick time constant).
// ═══════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════
export function calcManagedIndoorTempPass(TaArr, globArr, elevArr) { // buildingType must be a key of BUILDING_TYPES; defaults to 'brick'.
export function calcManagedIndoorTempPass(TaArr, globArr, elevArr, buildingType = 'brick') {
const preset = BUILDING_TYPES[buildingType] || BUILDING_TYPES.brick;
const { uWall, lagHours, glazingRatio, gValue, orientFactor, curtainBlock, ventAlpha } = preset;
const n = TaArr.length; const n = TaArr.length;
const result = new Array(n); const result = new Array(n);
const uWall = 0.35;
const lagHours = 4;
const alpha = 1 - Math.exp(-1 / lagHours); const alpha = 1 - Math.exp(-1 / lagHours);
// Curtain factor: blocks 80% of solar gain
const curtainBlock = 0.80;
// Ventilation blending weight per hour when windows are open
// ~2 ACH for a well-ventilated house → meaningful but not instant pull
const ventAlpha = 0.25;
let Ti = TaArr[0] ?? 15; let Ti = TaArr[0] ?? 15;
for (let i = 0; i < n; i++) { for (let i = 0; i < n; i++) {
const Ta = TaArr[i] ?? Ti; const Ta = TaArr[i] ?? Ti;
const glob = globArr[i] ?? 0; const glob = globArr[i] ?? 0;
// Solar gain — curtains block 80% // Solar gain — curtains block curtainBlock fraction
const glazingRatio = 0.16;
const gValue = 0.63;
const orientFactor = 0.50;
const solarGain = glob * glazingRatio * gValue * orientFactor * (1 - curtainBlock); const solarGain = glob * glazingRatio * gValue * orientFactor * (1 - curtainBlock);
// Wall conduction (unchanged) // Wall conduction (unchanged from unmanaged model)
const conductionGain = uWall * (Ta - Ti); const conductionGain = uWall * (Ta - Ti);
const heatCapProxy = 0.15; const heatCapProxy = 0.15;
@@ -265,7 +247,7 @@ export function calcManagedIndoorTempPass(TaArr, globArr, elevArr) {
// 3545 °C — dangerous for children/pets (hyperthermia risk) // 3545 °C — dangerous for children/pets (hyperthermia risk)
// > 45 °C — potentially fatal within minutes // > 45 °C — potentially fatal within minutes
// ═══════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════
export function calcVehicleInteriorTemp(Ta, globalRad, solElev, vehicleType = 'car') { export function calcVehicleInteriorTemp(Ta, globalRad, solElev, vehicleType = 'car', ventilated = false) {
if (globalRad == null || Ta == null) return null; if (globalRad == null || Ta == null) return null;
// Look up vehicle preset; fall back to a standard car if key unknown. // Look up vehicle preset; fall back to a standard car if key unknown.
@@ -303,8 +285,12 @@ 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 loss: varies by vehicle type (motorhomes insulated, cars not).
const Ti = Ta + totalGain / preset.hCabinLoss; // With windows open, convective loss 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;
// 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));
+31
View File
@@ -10,6 +10,7 @@
// sunburnMinutes(uv, skinType) minutes to MED // sunburnMinutes(uv, skinType) minutes to MED
// burnLabel(mins) formats burn time as "12m"/"1.5h" // burnLabel(mins) formats burn time as "12m"/"1.5h"
// VEHICLE_TYPES vehicle presets for cabin heat model // VEHICLE_TYPES vehicle presets for cabin heat model
// BUILDING_TYPES building presets for indoor heat model
// cloudCategory(total,low,mid,high) → 'clear'|'wispy'|'scattered'|'overcast' // cloudCategory(total,low,mid,high) → 'clear'|'wispy'|'scattered'|'overcast'
// confidenceBand(i) day-tab gradient + label // confidenceBand(i) day-tab gradient + label
// moonPhaseFraction(date) 0..1 synodic phase // moonPhaseFraction(date) 0..1 synodic phase
@@ -140,6 +141,36 @@ export const VEHICLE_TYPES = {
caravan: { name: 'Caravan (towed)', albedo: 0.40, glazingArea: 0.4, hCabinLoss: 1.0 }, caravan: { name: 'Caravan (towed)', albedo: 0.40, glazingArea: 0.4, hCabinLoss: 1.0 },
}; };
// ═══════════════════════════════════════════════════════════════════
// BUILDING TYPES — presets for the indoor temperature model.
// ───────────────────────────────────────────────────────────────────
// Each preset drives both calcIndoorTempPass and calcManagedIndoorTempPass.
//
// uWall W/m²K Effective envelope U-value. Higher = faster response
// to outdoor swings, less insulation.
// lagHours h Thermal mass time constant. Heavier construction = longer
// lag before indoor temp follows outdoor changes.
// glazingRatio — Fraction of floor area that is window. More glass = more
// solar gain in summer, more heat loss in winter.
// gValue — Solar heat gain coefficient of glazing. 0.63 = standard
// double glazing; 0.3 = modern low-e triple.
// orientFactor — Fraction of windows facing the sun at any given time.
// 0.5 = random orientation; 0.8 = south-facing conservatory.
// curtainBlock — Fraction of solar gain blocked when managed (curtains
// closed). Thick lined curtains ≈ 0.80; blinds ≈ 0.50.
// ventAlpha — Blending weight per hour when smart ventilation is open.
// Higher = more air changes per hour.
// ═══════════════════════════════════════════════════════════════════
export const BUILDING_TYPES = {
brick: { name: 'Brick (typical)', uWall: 0.35, lagHours: 4, glazingRatio: 0.16, gValue: 0.63, orientFactor: 0.50, curtainBlock: 0.80, ventAlpha: 0.25 },
modern: { name: 'Modern / Well insulated', uWall: 0.18, lagHours: 5, glazingRatio: 0.20, gValue: 0.30, orientFactor: 0.50, curtainBlock: 0.70, ventAlpha: 0.20 },
victorian: { name: 'Victorian Terrace', uWall: 0.55, lagHours: 5, glazingRatio: 0.12, gValue: 0.63, orientFactor: 0.50, curtainBlock: 0.80, ventAlpha: 0.30 },
stone: { name: 'Stone / Granite Cottage', uWall: 0.45, lagHours: 7, glazingRatio: 0.10, gValue: 0.63, orientFactor: 0.50, curtainBlock: 0.75, ventAlpha: 0.25 },
timber: { name: 'Timber Frame / New Build', uWall: 0.22, lagHours: 2, glazingRatio: 0.22, gValue: 0.35, orientFactor: 0.50, curtainBlock: 0.70, ventAlpha: 0.35 },
flat: { name: 'Top-floor Flat', uWall: 0.40, lagHours: 3, glazingRatio: 0.18, gValue: 0.63, orientFactor: 0.50, curtainBlock: 0.75, ventAlpha: 0.20 },
conservatory:{ name: 'Conservatory / Sun Room', uWall: 1.20, lagHours: 1, glazingRatio: 0.70, gValue: 0.72, orientFactor: 0.70, curtainBlock: 0.50, ventAlpha: 0.50 },
};
// ═══════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════
// CLOUD CATEGORY — pick one of 4 icon styles from low/mid/high split. // CLOUD CATEGORY — pick one of 4 icon styles from low/mid/high split.
// ─────────────────────────────────────────────────────────────────── // ───────────────────────────────────────────────────────────────────