From 5b0e62d1b5cd225f43040bc7df12e9feb62ec011 Mon Sep 17 00:00:00 2001
From: fraxle
+ SunScope organises its columns into profiles β 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, or switch to Custom to + pick individual columns yourself. +
+ +ποΈ Urban
++ 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 + air temperature, wind speed and direction (useful for + finding sheltered routes), cloud cover, UV-A index and + burn time for sun protection, UTCI+P for overall felt + comfort, precipitation, and concrete surface temperature + β the heat radiating up from pavement that standard forecasts completely ignore. +
+πΎ Farming
++ Built for anyone working the land β farmers, growers, and market gardeners. Decision-making + in agriculture is driven by conditions at ground level, not just the air. The Farming profile + shows air temperature, relative humidity and + dew point (critical for frost risk assessment and disease pressure on crops), + wind speed and direction (essential for safe spraying windows), + cloud cover and sun elevation for light levels, + UTCI for heat stress on the person working outdoors, + precipitation, and the three soil columns: + surface soil temperature, soil temperature at 6 cm + (the root zone for most seedlings), and soil moisture β so you know + whether the ground is workable or waterlogged before you head out. +
+β΅ Sailing
++ 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: + air temperature (hypothermia is a real risk β water conducts heat away from + the body twenty-five times faster than air), dew point (condensation on + sails and equipment is a practical concern at dawn and dusk), wind speed + with gusts, wind direction, cloud cover, and + sun elevation for visibility and glare. UV exposure is significantly higher + on open water due to reflection off the surface, so UV-A index and + burn time are included. UTCI+P captures the combined + misery of wet, windy conditions, and precipitation rounds out the picture + for passage planning. +
+π Vehicle
++ Covers the parked and travelling vehicle experience β especially useful for anyone with + children or pets, motorhome travellers, and caravan owners. The headline column is + vehicle interior temperature: 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). Wind speed is included because it matters + significantly for high-sided vehicles and caravans. Cloud cover and + air temperature give context, precipitation covers + driving conditions, and UTCI+P tells you what it will feel like when + you step outside. Soil moisture is included specifically for motorhome + and caravan users β values above 0.4 mΒ³/mΒ³ indicate saturated ground where pitching + or manoeuvring is likely to cause problems. +
+π Home
++ For managing indoor comfort without air conditioning β the typical UK home scenario. + The two indoor temperature columns are central: Indoors estimates the + ambient temperature inside a typical brick house with windows closed and no active cooling, + accounting for wall conduction, window solar gain, and thermal mass (indoor temperatures + usually peak 2β4 hours after the outdoor peak). Managed indoors shows + what you could achieve by following standard heatwave advice β closing curtains to block + direct solar gain and opening windows whenever the outdoor air is cooler than inside. + Supporting columns include air temperature, relative humidity + and dew point (important for condensation and mould risk in cooler months), + wind speed for ventilation decisions, cloud cover, + and precipitation β so you know whether to open the windows or bring + the washing in. +
+Every column in the SunScope forecast table is described below.
+ +| Column | What it shows |
|---|---|
| Air Β°C | Standard air temperature at 2 m above ground β the thermometer reading. Does not account for sun, wind, or humidity. |
| RH % | Relative humidity: how much moisture the air holds as a percentage of its maximum capacity at that temperature. High RH makes warm days feel stickier and cold days feel rawer. |
| Dew Point | The temperature at which air becomes saturated and moisture begins to condense. Above 16 Β°C it starts to feel muggy; above 21 Β°C it feels oppressive. A better measure of absolute humidity than RH. |
| Wind | Mean wind speed at 10 m, with peak gust in brackets where significantly higher. Wind is the primary driver of wind chill β a key ingredient in the UTCI calculation. |
| Dir | The compass direction the wind is blowing from, shown as an arrow and abbreviated label. Useful for route planning and spray timing. |
| Cloud | Total cloud cover as a percentage of sky. High cloud blocks solar radiation, reducing daytime heating and UV exposure. |
| Sun Β° | The angle of the sun above the horizon in degrees. Below 0Β° the sun has set. The higher the elevation, the more intense the radiation reaching the ground. |
| Direct W/mΒ² | Shortwave solar radiation arriving as a direct beam from the sun. The primary driver of skin heating on sunny days. |
| Diffuse W/mΒ² | Solar radiation scattered from all directions across the sky. Present even under cloud cover; contributes to the overall solar heat load. |
| Tmrt Β°C | Mean Radiant Temperature β the temperature your skin effectively "sees" from all surrounding surfaces and the sun combined. Can exceed air temperature by 20β30 Β°C on a sunny day; this is why shade feels so much cooler. |
| UTCIβAir Ξ | The gap between the UTCI felt temperature and the plain air temperature. A large positive value means solar radiation is adding significant heat stress beyond what the thermometer shows. |
| UTCI Β°C | Universal Thermal Climate Index β the raw felt temperature combining air temp, humidity, wind, and solar radiation. Does not include precipitation effects. |
| UV-A | Estimated UV-A radiation index. UV-A penetrates deeper into the skin and contributes to long-term ageing and some skin cancers, even through glass. Present throughout daylight hours. |
| UV-B | Estimated UV-B radiation index. Causes sunburn and drives vitamin D production. Intensity falls sharply at low solar elevations due to the longer atmospheric path length. |
| Burn time | Estimated time to reach one Minimal Erythemal Dose (MED) β the threshold for sunburn β based on the UV index and your selected Fitzpatrick skin type. A guide, not a medical measurement. |
| UTCI+P Β°C | SunScope's adjusted felt temperature: UTCI plus the soak-factor penalty for rain and snow. Wet clothing and skin can reduce the effective felt temperature by up to 8 Β°C in heavy, windy rain. |
| Precip mm | Expected rainfall or snowfall in mm per hour. Even light drizzle meaningfully reduces felt temperature when combined with wind. |
| Soil Β°C surface | Temperature of the soil at the surface (0 cm depth). Most seeds germinate above 7β10 Β°C. Useful for sowing decisions and frost assessment. |
| Soil Β°C 6 cm | Soil temperature at 6 cm depth β the root zone for many crops and seedlings. Lags behind the surface by several hours. |
| Soil moisture | 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. |
| Concrete Β°C | Estimated temperature of sun-exposed urban 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. |
| Vehicle Β°C | 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. |
| Indoors Β°C | 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 2β4 hours. |
| Managed Β°C | 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. |
The UTCI standard was developed with a natural grass surface as its reference ground β the diff --git a/assets/about.css b/assets/about.css index 9987af8..4de2574 100644 --- a/assets/about.css +++ b/assets/about.css @@ -88,6 +88,29 @@ p { strong { color: #1e1208; font-weight: 600; } em { color: #7a5c2a; font-style: italic; } +.profile-block { + background: #fef8ea; + border: 1px solid #e0d0a8; + border-left: 4px solid #c8922a; + border-radius: 8px; + padding: 1.1rem 1.4rem; + margin-bottom: 1rem; +} + +.profile-name { + font-family: 'Fraunces', serif; + font-weight: 900; + font-size: 1rem; + color: #1e1208; + margin-bottom: 0.5rem; +} + +.col-ref-table td:first-child { + white-space: nowrap; + width: 160px; + color: #5a3e1a; +} + .faq-block { background: #fef8ea; border: 1px solid #e0d0a8; diff --git a/assets/js/app.js b/assets/js/app.js index 740bb37..2a803a7 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -27,6 +27,7 @@ import { vaporPressureHpa, solarElevationDeg, calcTmrt, utciApprox, calcConcrete import { utciCategory, precipPenalty, windCompass8, uvSplit, SKIN_TYPES, sunburnMinutes, burnLabel, + VEHICLE_TYPES, cloudCategory, confidenceBand, moonGlyph, } from './utils.js'; import { SkyScope, WindVane, CloudIcon, ScopeReticle, PrecipIcon } from './components.js'; @@ -55,6 +56,7 @@ export function UTCIForecast() { const [searchResults, setSearchResults] = useState([]); // geocoding dropdown const [searching, setSearching] = useState(false); // search-in-flight flag const [selectedDay, setSelectedDay] = useState(0); // which day tab is active + const [now, setNow] = useState(new Date()); // ticks every 5 min const [proPromptDay, setProPromptDay] = useState(null); // locked day clicked β show upsell card const [proPromptSource, setProPromptSource] = useState('day'); // 'day' | 'custom' @@ -111,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. @@ -125,30 +127,30 @@ 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 }, }, + 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: false, cloud: true, sun: false, 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: true, vehicleT: false, indoorT: false, managedT: false }, - }, - farming: { - label: 'Farming', - icon: 'πΎ', - cols: { hour: true, air: true, rh: true, dew: true, wind: true, dir: false, cloud: true, sun: true, direct: true, diffuse: true, tmrt: false, delta: false, utci: false, uvA: false, uvB: false, burn: false, utciP: true, precip: true, soilT: true, soilT6: true, soilM: true, concreteT: false, vehicleT: false, indoorT: false, managedT: false }, - }, - sailing: { - label: 'Sailing', - icon: 'β΅', - cols: { hour: true, air: false, 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 }, + 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: false, dir: false, cloud: true, sun: false, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: true, uvB: true, burn: true, utciP: false, precip: false, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: true, indoorT: false, managedT: false }, + cols: { hour: true, air: true, rh: false, dew: false, wind: true, dir: false, cloud: true, sun: false, direct: false, diffuse: false, tmrt: false, delta: false, utci: false, uvA: false, uvB: false, burn: false, utciP: true, precip: true, soilT: false, soilT6: false, soilM: true, concreteT: false, vehicleT: true, indoorT: false, managedT: false }, }, - 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: false, soilT: false, soilT6: false, soilM: false, concreteT: false, vehicleT: false, indoorT: true, managedT: true }, + 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 }, }, alltemps: { label: 'All Temps', @@ -168,19 +170,14 @@ export function UTCIForecast() { // Which columns appear in the hourly table by default. // true = visible on first load (and the only ones free users see) // false = hidden by default (Pro users can toggle these on) - const [visibleCols, setVisibleCols] = useState({ - 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, - }); + const [visibleCols, setVisibleCols] = useState({ ...FILTER_PROFILES.basic.cols }); const toggleCol = (col) => setVisibleCols(prev => ({ ...prev, [col]: !prev[col] })); // Skin type for the sunburn-time column. Fitzpatrick II is typical UK fair. const [skinType, setSkinType] = useState('II'); + + // Vehicle type for the cabin heat column. 'car' is the default preset. + const [vehicleType, setVehicleType] = useState('car'); const searchTimeout = useRef(null); // Refs for the two-scroller table layout (sticky-to-viewport header + @@ -224,7 +221,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. Modelled from solar gain through body panels and side windows. Dangerous for children and pets above 35 Β°C; potentially fatal above 45 Β°C.' }, + 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.' }, 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.' }, }; @@ -455,7 +452,7 @@ export function UTCIForecast() { if (ro) ro.disconnect(); window.removeEventListener('resize', sync); }; - }, [forecast, visibleCols, selectedDay, skinType]); + }, [forecast, visibleCols, selectedDay, skinType, vehicleType]); // Geocoding search useEffect(() => { @@ -503,6 +500,37 @@ export function UTCIForecast() { load(); }, [location]); + // βββ AUTO-REFRESH β tick every 5 minutes βββββββββββββββββββββββββββββ + // Updates `now` so the scope always shows the current hour's data. + // Also refetches the forecast so fresh API data comes in automatically. + useEffect(() => { + const FIVE_MIN = 5 * 60 * 1000; + const id = setInterval(() => { + setNow(new Date()); + // Trigger a fresh forecast fetch by nudging location identity. + // We do this via a separate load rather than touching location state + // (which would reset other things), so we call load() directly. + async function refresh() { + try { + const url = + `https://api.open-meteo.com/v1/forecast` + + `?latitude=${location.lat}&longitude=${location.lon}` + + `&hourly=temperature_2m,relative_humidity_2m,dew_point_2m,` + + `wind_speed_10m,wind_direction_10m,wind_gusts_10m,` + + `direct_radiation,diffuse_radiation,shortwave_radiation,` + + `cloud_cover,cloud_cover_low,cloud_cover_mid,cloud_cover_high,` + + `uv_index,precipitation,snowfall,` + + `soil_temperature_0cm,soil_temperature_6cm,soil_moisture_0_to_1cm` + + `&wind_speed_unit=ms&timezone=auto&forecast_days=14`; + const r = await fetch(url); + if (r.ok) setForecast(await r.json()); + } catch (_) { /* silently ignore refresh errors */ } + } + refresh(); + }, FIVE_MIN); + return () => clearInterval(id); + }, [location]); + // βββ COMPUTATION βββββββββββββββββββββββββββββββββββββββββββββββββββββ // Take the raw API arrays and stitch them into one object per hour, // calculating UTCI + soak-factor for each row. This is what gets @@ -551,7 +579,7 @@ export function UTCIForecast() { // dt kept for SkyScope / backward compat β same as dtUTC. const dt = dtUTC; const elev = solarElevationDeg(location.lat, location.lon, dtUTC); - const vehicleT = calcVehicleInteriorTemp(Ta, glob, elev); + const vehicleT = calcVehicleInteriorTemp(Ta, glob, elev, vehicleType); const eh = vaporPressureHpa(Ta, RH); const Tmrt = calcTmrt(Ta, dir, dif, glob, elev); const utci = utciApprox(Ta, Tmrt, va, eh); @@ -592,7 +620,6 @@ export function UTCIForecast() { }); const visible = days[selectedDay]?.rows || []; - const now = new Date(); // nowLocalISO: current moment in location-local time as "YYYY-MM-DDTHH" // Used to match against r.iso (which is already a local wall-clock string). const nowLocalISO = new Date(now.getTime() + utcOffsetMs) @@ -653,6 +680,7 @@ export function UTCIForecast() { loading=${loading} elev=${currentRow?.elev ?? 0} dt=${currentRow?.dt ?? new Date()} + glob=${currentRow?.glob ?? 0} /> @@ -938,6 +966,43 @@ export function UTCIForecast() { COLUMN TOGGLES β Pro only. Hidden entirely for free users (the profile selector above provides enough control for them). --> + ${(FILTER_PROFILES[activeProfile]?.cols['burn'] || FILTER_PROFILES[activeProfile]?.cols['vehicleT']) && html` +