diff --git a/about.html b/about.html index 9252dcd..40e63b1 100644 --- a/about.html +++ b/about.html @@ -104,6 +104,134 @@ a more honest, real-world comfort score for anyone heading outdoors.

+

Forecast profiles

+

+ 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. +

+
+ +

Column reference

+

Every column in the SunScope forecast table is described below.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ColumnWhat it shows
Air Β°CStandard 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 PointThe 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.
WindMean 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.
DirThe compass direction the wind is blowing from, shown as an arrow and abbreviated label. Useful for route planning and spray timing.
CloudTotal 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 Β°CMean 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 Β°CUniversal Thermal Climate Index β€” the raw felt temperature combining air temp, humidity, wind, and solar radiation. Does not include precipitation effects.
UV-AEstimated 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-BEstimated 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 timeEstimated 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 Β°CSunScope'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 mmExpected rainfall or snowfall in mm per hour. Even light drizzle meaningfully reduces felt temperature when combined with wind.
Soil Β°C surfaceTemperature 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 cmSoil temperature at 6 cm depth β€” the root zone for many crops and seedlings. Lags behind the surface by several hours.
Soil moistureVolumetric 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 Β°CEstimated 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 Β°CEstimated 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 Β°CEstimated 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 Β°CEstimated 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.
+

Urban heat: concrete surface temperature

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` +

+ Columns: + ${FILTER_PROFILES[activeProfile]?.cols['burn'] && html` + `} + ${FILTER_PROFILES[activeProfile]?.cols['vehicleT'] && html` + `} +
`} + ${isPro && html`
Columns: @@ -950,7 +1015,6 @@ export function UTCIForecast() { { key: 'soilT6', label: 'Soil 6cm' }, { key: 'soilM', label: 'Soil moist' }, { key: 'concreteT', label: 'Concrete' }, - { key: 'vehicleT', label: 'Vehicle' }, { key: 'indoorT', label: 'Indoors' }, { key: 'managedT', label: 'Managed' }, { key: 'wind', label: 'Wind' }, @@ -973,23 +1037,6 @@ export function UTCIForecast() { onClick=${() => toggleCol(c.key)} >${c.label}`)} - ${(activeProfile === 'custom' || FILTER_PROFILES[activeProfile].cols['burn']) && html` - `} - ${(activeProfile === 'custom' || FILTER_PROFILES[activeProfile].cols['precip']) && html`