From 2e1f3b7c4866ecd628e3f323e145fe1b80f3b873 Mon Sep 17 00:00:00 2001 From: fraxle Date: Mon, 11 May 2026 09:03:18 +0100 Subject: [PATCH] Initial commit: Sunscope UTCI forecast site --- .gitignore | 19 + about.html | 171 ++++++++ assets/about.css | 154 +++++++ assets/index-CD_5RYSA.css | 1 + assets/index-cxJ3Sbx3.js | 1 + index.html | 15 + robots.txt | 4 + sitemap.xml | 13 + utci-forecast.jsx | 874 ++++++++++++++++++++++++++++++++++++++ 9 files changed, 1252 insertions(+) create mode 100644 .gitignore create mode 100644 about.html create mode 100644 assets/about.css create mode 100644 assets/index-CD_5RYSA.css create mode 100644 assets/index-cxJ3Sbx3.js create mode 100644 index.html create mode 100644 robots.txt create mode 100644 sitemap.xml create mode 100644 utci-forecast.jsx diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..216ca29 --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +# OS / editor cruft +.DS_Store +Thumbs.db +*.swp +*~ +.vscode/ +.idea/ + +# Node / build (in case you add tooling later) +node_modules/ +dist/ +.cache/ +*.log + +# Env / secrets +.env +.env.* +*.key +*.pem diff --git a/about.html b/about.html new file mode 100644 index 0000000..ce07876 --- /dev/null +++ b/about.html @@ -0,0 +1,171 @@ + + + + + + About SunScope · The True Temperature + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Home + About +
+ +
+

About SunScope

+

The True Temperature — what the weather actually feels like on your body.

+ +

What is SunScope?

+

+ SunScope is a free outdoor thermal comfort forecast that goes beyond standard air temperature. + Most weather apps tell you it's 12 °C and leave you to guess whether that means t-shirt weather + or a heavy coat. SunScope combines air temperature, humidity, wind speed, and solar radiation + into a single felt temperature — so you know what to actually expect when you + step outside. +

+

+ It uses the Universal Thermal Climate Index (UTCI), a peer-reviewed + biometeorological standard developed by Bröde et al. (2012) + and used globally in heat-health warning systems and urban planning. Forecast data is sourced + in real time from Open-Meteo, a free and open-source weather API drawing on + ECMWF and national met services. SunScope is available for any location worldwide. +

+ +

What is the SunScope soak-factor?

+

+ Standard UTCI does not account for precipitation — it tells you how cold the wind and sun make + you feel, but not the extra chill of rain soaking your clothing or snow settling on your skin. + The SunScope soak-factor fills that gap with an original penalty formula. +

+

+ Light drizzle (under 1 mm/hr) reduces the felt temperature by roughly 1–2 °C. Heavy rain + combined with strong wind can push it down by 7–8 °C. Snowfall carries an additional penalty + on top of any rain adjustment. The result is shown in the UTCI+P column — + a more honest, real-world comfort score for anyone heading outdoors. +

+ +

Reading the stress bands

+

SunScope maps every UTCI+P value to a thermal stress band so you can read conditions at a glance:

+ + + + + + + + + + + + + + + + + +
UTCI rangeBandWhat it means outdoors
Below −27 °CPolarSurvivable only with expedition-grade kit; do not go outside unprepared
−27 to −13 °CArcticVery strong cold stress; full winter protection needed
−13 to 0 °CFreezingStrong cold stress; warm layers essential
0 to 9 °CColdModerate cold stress; coat and gloves advised
9 to 18 °CChilledSlight cold stress; comfortable with a light jacket
18 to 26 °CComfortableNo thermal stress; ideal outdoor conditions
26 to 32 °CMod heatModerate heat stress; stay hydrated
32 to 38 °CStrong heatStrong heat stress; limit strenuous activity
38 to 46 °CV. strong heatVery strong heat stress; seek shade and cool fluids
Above 46 °CExtreme heatExtreme heat stress; dangerous for outdoor exposure
+ +

Frequently asked questions

+ +
+

Why does SunScope sometimes show a much higher temperature than my regular weather app?

+

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.

+
+ +
+

Why does SunScope sometimes show a lower temperature than my regular weather app?

+

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.

+
+ +
+

How far ahead does the forecast go?

+

SunScope currently shows a 72-hour hourly forecast. Data refreshes automatically from Open-Meteo each time you load the page.

+
+ +
+

Is SunScope free?

+

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.

+
+ +
+

What data sources does SunScope use?

+

Forecast data comes from Open-Meteo, 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.

+
+ +

Get in touch

+

+ SunScope is an independent project. If you have feedback, spotted a bug, or are interested in + using SunScope data for your business or website, feel free to reach out via + hello@sunscope.net. +

+ +
+ + + + + diff --git a/assets/about.css b/assets/about.css new file mode 100644 index 0000000..9987af8 --- /dev/null +++ b/assets/about.css @@ -0,0 +1,154 @@ +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } + +body { + font-family: 'Manrope', sans-serif; + background: #f5edd6; + color: #1e1208; + line-height: 1.7; + min-height: 100vh; +} + +a { color: #c8922a; text-decoration: none; } +a:hover { text-decoration: underline; } + +header { + display: flex; + align-items: center; + justify-content: flex-end; + padding: 18px 32px; + gap: 28px; + border-bottom: 1px solid #e0d0a8; +} + +header a { + font-family: 'JetBrains Mono', monospace; + font-size: 16px; + letter-spacing: 0.10em; + text-transform: uppercase; + color: #b09870; + text-decoration: none; + border-bottom: 1px solid transparent; + padding-bottom: 2px; + transition: color 0.2s, border-color 0.2s; +} +header a:hover { color: #c8922a; border-bottom-color: rgba(200,146,42,0.5); } +header a.active { color: #c8922a; } + +.logo { + margin-right: auto; + font-family: 'Fraunces', serif; + font-weight: 900; + font-size: 1.6rem; + color: #1e1208; + letter-spacing: -0.02em; + text-transform: none; +} +.logo-sun { color: #c8922a; font-weight: 900; font-style: normal; } +.logo-scope { font-style: italic; font-weight: 900; color: #1e1208; } + +main { + max-width: 780px; + margin: 0 auto; + padding: 3rem 1.5rem 5rem; +} + +h1 { + font-family: 'Fraunces', serif; + font-weight: 900; + font-size: 2.2rem; + color: #1e1208; + margin-bottom: 0.3rem; + line-height: 1.15; +} + +.tagline { + font-family: 'Fraunces', serif; + font-style: italic; + font-size: 1.05rem; + color: #7a5c2a; + margin-bottom: 2.5rem; +} + +h2 { + font-family: 'Fraunces', serif; + font-weight: 900; + font-size: 1.2rem; + color: #1e1208; + margin: 2.5rem 0 0.7rem; + padding-bottom: 0.3rem; + border-bottom: 2px solid #c9b08a; +} + +p { + margin-bottom: 1.1rem; + color: #3a2a12; + font-size: 0.97rem; +} + +strong { color: #1e1208; font-weight: 600; } +em { color: #7a5c2a; font-style: italic; } + +.faq-block { + background: #fef8ea; + border: 1px solid #e0d0a8; + border-radius: 8px; + padding: 1.2rem 1.4rem; + margin-bottom: 1rem; +} + +.faq-q { + font-weight: 600; + color: #1e1208; + margin-bottom: 0.4rem; + font-size: 0.97rem; +} + +.faq-a { + color: #5a3e1a; + font-size: 0.93rem; + margin: 0; +} + +.stress-table { + width: 100%; + border-collapse: collapse; + margin: 1.2rem 0 1.5rem; + font-size: 0.9rem; +} + +.stress-table th { + background: #ede4cc; + color: #5a3e1a; + font-weight: 600; + padding: 0.5rem 0.8rem; + text-align: left; + border-bottom: 1px solid #c9b08a; +} + +.stress-table td { + padding: 0.45rem 0.8rem; + border-bottom: 1px solid #ede4cc; + color: #3a2a12; +} + +.swatch { + display: inline-block; + width: 12px; height: 12px; + border-radius: 3px; + margin-right: 6px; + vertical-align: middle; +} + +footer { + text-align: center; + padding: 2rem 1.5rem; + font-size: 0.82rem; + color: #b09870; + border-top: 1px solid #e0d0a8; +} + +@media (max-width: 600px) { + h1 { font-size: 1.6rem; } + main { padding: 2rem 1rem 4rem; } + header { padding: 14px 16px; } +} diff --git a/assets/index-CD_5RYSA.css b/assets/index-CD_5RYSA.css new file mode 100644 index 0000000..430e9da --- /dev/null +++ b/assets/index-CD_5RYSA.css @@ -0,0 +1 @@ +@import"https://fonts.bunny.net/css2?family=Fraunces:opsz,wght@9..144,300;9..144,500;9..144,700;9..144,900&family=JetBrains+Mono:wght@400;600&family=Manrope:wght@400;500;600;700&display=swap";*,*:before,*:after{box-sizing:border-box;margin:0;padding:0}body{background:#f5edd6}.utci-topnav{position:absolute;top:0;left:0;right:0;z-index:10;display:flex;align-items:center;justify-content:flex-end;padding:18px 32px;gap:28px}.utci-topnav a{font-family:JetBrains Mono,monospace;font-size:16px;letter-spacing:.1em;text-transform:uppercase;color:#b09870;text-decoration:none;border-bottom:1px solid transparent;padding-bottom:2px;transition:color .2s,border-color .2s}.utci-topnav a:hover{color:#c8922a;border-bottom-color:#c8922a80}.utci-app{font-family:Manrope,sans-serif;color:#1e1208;background:#f5edd6;min-height:100vh;padding:52px 28px 32px;position:relative;overflow-x:hidden}.lens-bloom-a{position:fixed;top:-200px;left:-200px;width:700px;height:700px;border-radius:50%;background:radial-gradient(ellipse,rgba(255,210,60,.18) 0%,transparent 65%);pointer-events:none;z-index:0}.lens-bloom-b{position:fixed;top:-80px;right:-140px;width:520px;height:420px;background:radial-gradient(ellipse,rgba(200,146,42,.13) 0%,transparent 68%);pointer-events:none;z-index:0}.utci-shell{position:relative;z-index:1;max-width:1400px;margin:0 auto}.utci-header{display:grid;grid-template-columns:1fr auto 1fr;gap:32px;align-items:center;padding-bottom:32px;border-bottom:2px solid #c9b08a;margin-bottom:32px}.header-right{display:flex;flex-direction:column;align-items:flex-end;justify-content:center}.utci-title{font-family:Fraunces,serif;font-weight:900;font-variation-settings:"opsz" 144;font-size:clamp(48px,6vw,80px);line-height:.9;letter-spacing:-.03em;color:#1e1208}.utci-title .title-sun{color:#c8922a;font-weight:900;font-style:normal}.utci-title .title-scope{font-style:italic;font-weight:900;color:#1e1208}.utci-title .title-beta{font-size:.38em;text-transform:none;font-weight:400;letter-spacing:.05em;vertical-align:super;color:#b09870}.utci-tagline{font-family:Fraunces,serif;font-style:italic;font-size:17px;color:#7a5c2a;margin-top:14px}.utci-subtitle{font-family:JetBrains Mono,monospace;font-size:11px;text-transform:uppercase;letter-spacing:.18em;margin-top:10px;color:#b09870}.utci-subtitle-link{color:#c8922a;text-decoration:none;border-bottom:1px solid rgba(200,146,42,.4);transition:border-color .2s,color .2s}.utci-subtitle-link:hover{color:#a06e18;border-bottom-color:#a06e18}.utci-current-loc{font-family:Fraunces,serif;font-style:italic;font-size:18px;margin-top:12px;color:#1e1208}.utci-loc-coords{font-family:JetBrains Mono,monospace;font-style:normal;font-size:11px;color:#b09870;margin-left:10px;letter-spacing:.08em}.scope-ring{width:220px;height:220px;display:block}.utci-search-wrap{position:relative;width:300px}.utci-search-label{font-family:JetBrains Mono,monospace;font-size:10px;text-transform:uppercase;letter-spacing:.18em;color:#b09870;display:block;margin-bottom:7px}.utci-search{width:100%;padding:12px 16px;border:1.5px solid #c9b08a;background:#fffcf2;font-family:Manrope,sans-serif;font-size:14px;color:#1e1208;outline:none;transition:border-color .2s,box-shadow .2s}.utci-search::placeholder{color:#c0a880}.utci-search:focus{border-color:#c8922a;box-shadow:0 0 0 3px #c8922a24}.utci-results{position:absolute;top:100%;left:0;right:0;background:#fffcf2;border:1.5px solid #c9b08a;border-top:none;max-height:260px;overflow-y:auto;z-index:50;box-shadow:0 8px 24px #6446141f}.utci-result{padding:11px 16px;cursor:pointer;border-bottom:1px solid #ede4cc;font-size:14px;transition:background .12s;border-left:3px solid transparent}.utci-result:hover{background:#fef3dc;border-left-color:#c8922a}.utci-result-meta{font-size:11px;color:#b09870;font-family:JetBrains Mono,monospace;margin-top:3px;letter-spacing:.06em}.utci-searching{margin-top:7px;font-size:10px;color:#b09870;font-family:JetBrains Mono,monospace;letter-spacing:.14em;text-transform:uppercase}.utci-day-tabs{display:flex;margin-bottom:18px;border-bottom:2px solid #d4c0a0}.utci-day-tab{padding:12px 24px;cursor:pointer;font-family:JetBrains Mono,monospace;font-size:10px;text-transform:uppercase;letter-spacing:.14em;background:transparent;border:none;color:#b09870;border-bottom:2px solid transparent;margin-bottom:-2px;transition:color .2s,border-color .2s}.utci-day-tab:hover{color:#6b4f2a}.utci-day-tab.active{color:#c8922a;border-bottom-color:#c8922a}.utci-day-date{font-family:Fraunces,serif;font-style:italic;font-size:13px;text-transform:none;letter-spacing:0;display:block;margin-top:3px;color:inherit}.utci-table-wrap{overflow-x:auto;border:1.5px solid #c9b08a;background:#fffcf2}.col-toggles{display:flex;flex-wrap:wrap;gap:6px;align-items:center;margin-bottom:10px;padding:10px 14px;background:#fdf8ee;border:1.5px solid #d4c0a0}.col-toggles-label{font-family:JetBrains Mono,monospace;font-size:9px;text-transform:uppercase;letter-spacing:.18em;color:#b09870;margin-right:4px;white-space:nowrap}.col-toggle{padding:4px 11px;cursor:pointer;border:1.5px solid #c9b08a;background:#f5edd6;color:#9a7d5a;font-family:JetBrains Mono,monospace;font-size:9px;text-transform:uppercase;letter-spacing:.1em;transition:all .15s;-webkit-user-select:none;user-select:none}.col-toggle:hover{border-color:#c8922a;color:#6b4f2a;background:#fef3dc}.col-toggle.on{background:#c8922a;border-color:#c8922a;color:#fff}.utci-table{width:100%;border-collapse:collapse;font-size:14px}.utci-table th{background:#f0e4c4;color:#6b4f2a;font-family:JetBrains Mono,monospace;font-size:10px;text-transform:uppercase;letter-spacing:.14em;font-weight:600;padding:14px 12px;text-align:right;white-space:nowrap;position:sticky;top:0;border-bottom:2px solid #c9b08a}.utci-table th:first-child,.utci-table td:first-child{text-align:left}.utci-table th .col-unit{display:block;color:#c8922a;font-size:9px;margin-top:2px}.utci-table tbody tr{border-bottom:1px solid #ede4cc;transition:background .1s}.utci-table tbody tr:hover{background:#fef8ea}.utci-table tbody tr.is-night td{color:#c0a880}.utci-table td{padding:10px 12px;text-align:right;font-family:JetBrains Mono,monospace;font-variant-numeric:tabular-nums;white-space:nowrap;color:#4a3420}.utci-table tbody tr.is-now{background:#fff8e4!important;box-shadow:inset 3px 0 #c8922a}.utci-table tbody tr.is-now td{color:#1e1208}.now-pip{display:inline-block;width:6px;height:6px;border-radius:50%;background:#c8922a;margin-right:7px;vertical-align:middle;box-shadow:0 0 8px #c8922a73}.utci-time{font-family:Fraunces,serif!important;font-weight:500;font-size:16px;color:#1e1208!important}.utci-cell{display:inline-block;padding:4px 12px;border-radius:3px;font-weight:600;min-width:60px;text-align:center;font-size:14px}.utci-band-label{font-family:JetBrains Mono,monospace;font-size:10px;text-transform:uppercase;letter-spacing:.08em}.utci-status{padding:14px 18px;background:#f0e4c4;color:#7a5c2a;font-family:JetBrains Mono,monospace;font-size:11px;text-align:center;letter-spacing:.12em;text-transform:uppercase;border:1.5px solid #c9b08a;margin-bottom:18px}.utci-legend{margin-top:22px;padding:18px 20px;background:#fdf8ee;border:1.5px solid #c9b08a}.utci-legend-label{font-family:JetBrains Mono,monospace;font-size:10px;text-transform:uppercase;letter-spacing:.18em;color:#b09870;margin-bottom:12px;display:block;font-weight:600}.utci-legend-row{display:flex;flex-wrap:wrap;gap:4px}.utci-legend-item{padding:6px 12px;font-family:JetBrains Mono,monospace;font-size:10px;text-transform:uppercase;letter-spacing:.06em;flex:1;min-width:110px;text-align:center;opacity:.9}.utci-about{margin-top:28px;padding:24px 28px;background:#fdf8ee;border:1.5px solid #c9b08a}.utci-about-heading{font-family:Fraunces,serif;font-size:18px;font-weight:700;color:#1e1208;margin-bottom:14px;letter-spacing:-.01em}.utci-about-text{font-family:Manrope,sans-serif;font-size:14px;line-height:1.75;color:#4a3420;margin-bottom:12px;max-width:860px}.utci-about-text:last-child{margin-bottom:0}.utci-about-text strong{color:#1e1208;font-weight:600}.utci-about-text em{font-style:italic;color:#6b4f2a}.utci-footer{margin-top:36px;padding-top:20px;border-top:1px solid #d4c0a0;font-family:JetBrains Mono,monospace;font-size:10px;text-transform:uppercase;letter-spacing:.12em;color:#c0a880;line-height:2.2}.utci-footer em{font-family:Fraunces,serif;font-style:italic;text-transform:none;letter-spacing:0;font-size:14px;color:#7a5c2a}@media (max-width: 800px){.utci-header{grid-template-columns:1fr;text-align:center}.header-right{align-items:center}.utci-search-wrap{width:100%}.scope-ring{width:180px;height:180px;margin:0 auto}} diff --git a/assets/index-cxJ3Sbx3.js b/assets/index-cxJ3Sbx3.js new file mode 100644 index 0000000..06ac34d --- /dev/null +++ b/assets/index-cxJ3Sbx3.js @@ -0,0 +1 @@ +(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const s of document.querySelectorAll('link[rel="modulepreload"]'))_(s);new MutationObserver(s=>{for(const i of s)if(i.type==="childList")for(const f of i.addedNodes)f.tagName==="LINK"&&f.rel==="modulepreload"&&_(f)}).observe(document,{childList:!0,subtree:!0});function n(s){const i={};return s.integrity&&(i.integrity=s.integrity),s.referrerPolicy&&(i.referrerPolicy=s.referrerPolicy),s.crossOrigin==="use-credentials"?i.credentials="include":s.crossOrigin==="anonymous"?i.credentials="omit":i.credentials="same-origin",i}function _(s){if(s.ep)return;s.ep=!0;const i=n(s);fetch(s.href,i)}})();var Z,N,Re,L,ke,Le,je,te,V,E,Oe,le,re,ce,J={},Y=[],Xe=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i,ee=Array.isArray;function U(e,t){for(var n in t)e[n]=t[n];return e}function se(e){e&&e.parentNode&&e.parentNode.removeChild(e)}function Ze(e,t,n){var _,s,i,f={};for(i in t)i=="key"?_=t[i]:i=="ref"?s=t[i]:f[i]=t[i];if(arguments.length>2&&(f.children=arguments.length>3?Z.call(arguments,2):n),typeof e=="function"&&e.defaultProps!=null)for(i in e.defaultProps)f[i]===void 0&&(f[i]=e.defaultProps[i]);return q(e,f,_,s,null)}function q(e,t,n,_,s){var i={type:e,props:t,key:n,ref:_,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:s??++Re,__i:-1,__u:0};return s==null&&N.vnode!=null&&N.vnode(i),i}function j(e){return e.children}function G(e,t){this.props=e,this.context=t}function O(e,t){if(t==null)return e.__?O(e.__,e.__i+1):null;for(var n;tt&&L.sort(je),e=L.shift(),t=L.length,et(e)}finally{L.length=Q.__r=0}}function Ee(e,t,n,_,s,i,f,r,l,c,d){var o,p,m,b,w,h,y,k=_&&_.__k||Y,C=t.length;for(l=tt(n,t,k,l,C),o=0;o0?f=e.__k[i]=q(f.type,f.props,f.key,f.ref?f.ref:null,f.__v):e.__k[i]=f,l=i+p,f.__=e,f.__b=e.__b+1,r=null,(c=f.__i=nt(f,n,l,o))!=-1&&(o--,(r=n[c])&&(r.__u|=2)),r==null||r.__v==null?(c==-1&&(s>d?p--:sl?p--:p++,f.__u|=4))):e.__k[i]=null;if(o)for(i=0;i(d?1:0)){for(s=n-1,i=n+1;s>=0||i=0?s--:i++])!=null&&!(2&c.__u)&&r==c.key&&l==c.type)return f}return-1}function Ne(e,t,n){t[0]=="-"?e.setProperty(t,n??""):e[t]=n==null?"":typeof n!="number"||Xe.test(t)?n:n+"px"}function z(e,t,n,_,s){var i,f;e:if(t=="style")if(typeof n=="string")e.style.cssText=n;else{if(typeof _=="string"&&(e.style.cssText=_=""),_)for(t in _)n&&t in n||Ne(e.style,t,"");if(n)for(t in n)_&&n[t]==_[t]||Ne(e.style,t,n[t])}else if(t[0]=="o"&&t[1]=="n")i=t!=(t=t.replace(Oe,"$1")),f=t.toLowerCase(),t=f in e||t=="onFocusOut"||t=="onFocusIn"?f.slice(2):t.slice(2),e.l||(e.l={}),e.l[t+i]=n,n?_?n[E]=_[E]:(n[E]=le,e.addEventListener(t,i?ce:re,i)):e.removeEventListener(t,i?ce:re,i);else{if(s=="http://www.w3.org/2000/svg")t=t.replace(/xlink(H|:h)/,"h").replace(/sName$/,"s");else if(t!="width"&&t!="height"&&t!="href"&&t!="list"&&t!="form"&&t!="tabIndex"&&t!="download"&&t!="rowSpan"&&t!="colSpan"&&t!="role"&&t!="popover"&&t in e)try{e[t]=n??"";break e}catch{}typeof n=="function"||(n==null||n===!1&&t[4]!="-"?e.removeAttribute(t):e.setAttribute(t,t=="popover"&&n==1?"":n))}}function Me(e){return function(t){if(this.l){var n=this.l[t.type+e];if(t[V]==null)t[V]=le++;else if(t[V]0?e:ee(e)?e.map(Ve):U({},e)}function ot(e,t,n,_,s,i,f,r,l){var c,d,o,p,m,b,w,h=n.props||J,y=t.props,k=t.type;if(k=="svg"?s="http://www.w3.org/2000/svg":k=="math"?s="http://www.w3.org/1998/Math/MathML":s||(s="http://www.w3.org/1999/xhtml"),i!=null){for(c=0;c=n.__.length&&n.__.push({}),n.__[e]}function F(e){return X=1,at(Je,e)}function at(e,t,n){var _=ue(W++,2);if(_.t=e,!_.__c&&(_.__=[Je(void 0,t),function(r){var l=_.__N?_.__N[0]:_.__[0],c=_.t(l,r);l!==c&&(_.__N=[c,_.__[1]],_.__c.setState({}))}],_.__c=P,!P.__f)){var s=function(r,l,c){if(!_.__c.__H)return!0;var d=_.__c.__H.__.filter(function(p){return p.__c});if(d.every(function(p){return!p.__N}))return!i||i.call(this,r,l,c);var o=_.__c.props!==r;return d.some(function(p){if(p.__N){var m=p.__[0];p.__=p.__N,p.__N=void 0,m!==p.__[0]&&(o=!0)}}),i&&i.call(this,r,l,c)||o};P.__f=!0;var i=P.shouldComponentUpdate,f=P.componentWillUpdate;P.componentWillUpdate=function(r,l,c){if(this.__e){var d=i;i=void 0,s(r,l,c),i=d}f&&f.call(this,r,l,c)},P.shouldComponentUpdate=s}return _.__N||_.__}function Ae(e,t){var n=ue(W++,3);!$.__s&&Ke(n.__H,t)&&(n.__=e,n.u=t,P.__H.__h.push(n))}function lt(e){return X=5,st(function(){return{current:e}},[])}function st(e,t){var n=ue(W++,7);return Ke(n.__H,t)&&(n.__=e(),n.__H=t,n.__h=e),n.__}function _t(){for(var e;e=Ge.shift();){var t=e.__H;if(e.__P&&t)try{t.__h.some(K),t.__h.some(ae),t.__h=[]}catch(n){t.__h=[],$.__e(n,e.__v)}}}$.__b=function(e){P=null,Te&&Te(e)},$.__=function(e,t){e&&t.__k&&t.__k.__m&&(e.__m=t.__k.__m),Ie&&Ie(e,t)},$.__r=function(e){Se&&Se(e),W=0;var t=(P=e.__c).__H;t&&(ne===P?(t.__h=[],P.__h=[],t.__.some(function(n){n.__N&&(n.__=n.__N),n.u=n.__N=void 0})):(t.__h.some(K),t.__h.some(ae),t.__h=[],W=0)),ne=P},$.diffed=function(e){Pe&&Pe(e);var t=e.__c;t&&t.__H&&(t.__H.__h.length&&(Ge.push(t)!==1&&Ce===$.requestAnimationFrame||((Ce=$.requestAnimationFrame)||dt)(_t)),t.__H.__.some(function(n){n.u&&(n.__H=n.u),n.u=void 0})),ne=P=null},$.__c=function(e,t){t.some(function(n){try{n.__h.some(K),n.__h=n.__h.filter(function(_){return!_.__||ae(_)})}catch(_){t.some(function(s){s.__h&&(s.__h=[])}),t=[],$.__e(_,n.__v)}}),$e&&$e(e,t)},$.unmount=function(e){De&&De(e);var t,n=e.__c;n&&n.__H&&(n.__H.__.some(function(_){try{K(_)}catch(s){t=s}}),n.__H=void 0,t&&$.__e(t,n.__v))};var He=typeof requestAnimationFrame=="function";function dt(e){var t,n=function(){clearTimeout(_),He&&cancelAnimationFrame(t),setTimeout(e)},_=setTimeout(n,35);He&&(t=requestAnimationFrame(n))}function K(e){var t=P,n=e.__c;typeof n=="function"&&(e.__c=void 0,n()),P=t}function ae(e){var t=P;e.__c=e.__(),P=t}function Ke(e,t){return!e||e.length!==t.length||t.some(function(n,_){return n!==e[_]})}function Je(e,t){return typeof t=="function"?t(e):t}const Fe=5670374419e-17,Ue=.97,ut=.7,ht=.23;function ft(e,t){return 6.105*Math.exp(17.27*e/(237.7+e))*(t/100)}function mt(e,t,n){const _=Date.UTC(n.getUTCFullYear(),0,0),s=n-_,i=Math.floor(s/864e5),f=n.getUTCHours()+n.getUTCMinutes()/60+n.getUTCSeconds()/3600,r=2*Math.PI/365*(i-1+(f-12)/24),l=229.18*(75e-6+.001868*Math.cos(r)-.032077*Math.sin(r)-.014615*Math.cos(2*r)-.040849*Math.sin(2*r)),c=.006918-.399912*Math.cos(r)+.070257*Math.sin(r)-.006758*Math.cos(2*r)+907e-6*Math.sin(2*r)-.002697*Math.cos(3*r)+.00148*Math.sin(3*r),d=l+4*t,p=((f*60+d)/4-180)*Math.PI/180,m=e*Math.PI/180,b=Math.sin(m)*Math.sin(c)+Math.cos(m)*Math.cos(c)*Math.cos(p),w=Math.acos(Math.max(-1,Math.min(1,b)));return(Math.PI/2-w)*(180/Math.PI)}function pt(e,t,n,_,s){const i=e+273.15;let f=0;if(s>0){const o=s;f=.308*Math.cos(Math.PI/180*o*(.998-o*o/5e4))}let r=0;s>1&&(r=t/Math.sin(s*Math.PI/180),r=Math.min(r,1100));const l=ut*(f*r+.5*n+.5*ht*_),c=Ue*Fe*Math.pow(i,4);return Math.pow((l+c)/(Ue*Fe),.25)-273.15}function yt(e,t,n,_){const s=Math.max(.5,Math.min(17,n)),i=t-e,f=_/10,r=e,l=s,c=i,d=f,o=r*r,p=o*r,m=p*r,b=m*r,w=b*r,h=l*l,y=h*l,k=y*l,C=k*l,D=C*l,g=c*c,M=g*c,T=M*c,I=T*c,A=I*c,v=d*d,u=v*d,x=u*d,S=x*d,H=S*d;return r+.607562052+-.0227712343*r+.000806470249*o+-.000154271372*p+-324651735e-14*m+732602852e-16*b+135959073e-17*w+-2.2583652*l+.0880326035*r*l+.00216844454*o*l+-153347087e-13*p*l+-572983704e-15*m*l+-255090145e-17*b*l+-.751269505*h+-.00408350271*r*h+-521670675e-13*o*h+194544667e-14*p*h+114099531e-16*m*h+.158137256*y+-657263143e-13*r*y+222697524e-15*o*y+-416117031e-16*p*y+-.0127762753*k+966891875e-14*r*k+252785852e-17*o*k+.000456306672*C+-174202546e-15*r*C+-591491269e-14*D+.398374029*c+.000183945314*r*c+-.00017375451*o*c+-760781159e-15*p*c+377830287e-16*m*c+543079673e-18*b*c+-.0200518269*l*c+.000892859837*r*l*c+345433048e-14*o*l*c+-377925774e-15*p*l*c+-169699377e-17*m*l*c+.000169992415*h*c+-499204314e-13*r*h*c+247417178e-15*o*h*c+107596466e-16*p*h*c+849242932e-13*y*c+135191328e-14*r*y*c+-621531254e-17*o*y*c+-499410301e-14*k*c+-189489258e-16*r*k*c+815300114e-16*C*c+.00075504309*g+-565095215e-13*r*g+-452166564e-15*o*g+246688878e-16*p*g+242674348e-18*m*g+.00015454725*l*g+52411097e-13*r*l*g+-875874982e-16*o*l*g+-150743064e-17*p*l*g+-156236307e-13*h*g+-133895614e-15*r*h*g+249709824e-17*o*h*g+651711721e-15*y*g+194960053e-17*r*y*g+-100361113e-16*k*g+-121206673e-13*M+-21820366e-14*r*M+751269482e-17*o*M+979063848e-19*p*M+125006734e-14*l*M+-181584736e-17*r*l*M+-352197671e-18*o*l*M+-33651463e-15*h*M+135908359e-18*r*h*M+41703262e-17*y*M+-130369025e-17*T+413908461e-18*r*T+922652254e-20*o*T+-508220384e-17*l*T+-224730961e-19*r*l*T+117139133e-18*h*T+662154879e-18*I+40386326e-20*r*I+195087203e-20*l*I+-473602469e-20*A+5.12733497*d+-.312788561*r*d+-.0196701861*o*d+.00099969087*p*d+951738512e-14*m*d+-466426341e-15*b*d+.548050612*l*d+-.00330552823*r*l*d+-.0016411944*o*l*d+-516670694e-14*p*l*d+952692432e-15*m*l*d+-.0429223622*h*d+.00500845667*r*h*d+100601257e-14*o*h*d+-181748644e-14*p*h*d+-.00125813502*y*d+-.000179330391*r*y*d+234994441e-14*o*y*d+.000129735808*k*d+12906487e-13*r*k*d+-228558686e-14*C*d+-.0369476348*c*d+.00162325322*r*c*d+-31427968e-12*o*c*d+259835559e-14*p*c*d+-477136523e-16*m*c*d+.0086420339*l*c*d+-.000687405181*r*l*c*d+-913863872e-14*o*l*c*d+515916806e-15*p*l*c*d+-359217476e-13*h*c*d+328696511e-13*r*h*c*d+-710542454e-15*o*h*c*d+-1243823e-11*y*c*d+-7385844e-15*r*y*c*d+220609296e-15*k*c*d+-.00073246918*g*d+-187381964e-13*r*g*d+480925239e-14*o*g*d+-87549204e-15*p*g*d+27786293e-12*l*g*d+-506004592e-14*r*l*g*d+114325367e-15*o*l*g*d+253016723e-14*h*g*d+-172857035e-16*r*h*g*d+-395079398e-16*y*g*d+-359413173e-15*M*d+704388046e-15*r*M*d+-189309167e-16*o*M*d+-479768731e-15*l*M*d+796079978e-17*r*l*M*d+162897058e-17*h*M*d+394367674e-16*T*d+-118566247e-17*r*T*d+334678041e-18*l*T*d+-115606447e-18*I*d+-2.80626406*v+.548712484*r*v+-.0039942841*o*v+-.000954009191*p*v+193090978e-13*m*v+-.308806365*l*v+.0116952364*r*l*v+.000495271903*o*l*v+-190710882e-13*p*l*v+.00210787756*h*v+-.000698445738*r*h*v+230109073e-13*o*h*v+.00041785659*y*v+-127043871e-13*r*y*v+-304620472e-14*k*v+.0514507424*c*v+-.00432510997*r*c*v+899281156e-13*o*c*v+-714663943e-15*p*c*v+-.000266016305*l*c*v+.000263789586*r*l*c*v+-701199003e-14*o*l*c*v+-.000106823306*h*c*v+361341136e-14*r*h*c*v+229748967e-15*y*c*v+.000304788893*g*v+-642070836e-13*r*g*v+116257971e-14*o*g*v+768023384e-14*l*g*v+-547446896e-15*r*l*g*v+-35993791e-15*h*g*v+-436497725e-14*M*v+168737969e-15*r*M*v+267489271e-16*l*M*v+323926897e-17*T*v+-.0353874123*u+-.22120119*r*u+.0155126038*o*u+-.000263917279*p*u+.0453433455*l*u+-.00432943862*r*l*u+.000145389826*o*l*u+.00021750861*h*u+-666724702e-13*r*h*u+33321714e-12*y*u+-.00226921615*c*u+.000380261982*r*c*u+-545314314e-17*o*c*u+-.000796355448*l*c*u+253458034e-13*r*l*c*u+-631223658e-14*h*c*u+.000302122035*g*u+-477403547e-14*r*g*u+173825715e-14*l*g*u+-409087898e-15*M*u+.614155345*x+-.0616755931*r*x+.00133374846*o*x+.00355375387*l*x+-.000513027851*r*l*x+.000102449757*h*x+-.00148526421*c*x+-411469183e-13*r*c*x+-680434415e-14*l*c*x+-977675906e-14*g*x+.0882773108*S+-.00301859306*r*S+.00104452989*l*S+.000247090539*c*S+.00148348065*H}function oe(e){return e<-40?{label:"Extreme cold",bg:"#1a1438",fg:"#fff"}:e<-27?{label:"Very strong cold",bg:"#23408f",fg:"#fff"}:e<-13?{label:"Arctic",bg:"#3f73c4",fg:"#fff"}:e<0?{label:"Freezing",bg:"#7eb0e0",fg:"#1a1612"}:e<9?{label:"Cold",bg:"#bcd9ec",fg:"#1a1612"}:e<18?{label:"Chilled",bg:"#c8dcc0",fg:"#1a1612"}:e<26?{label:"Comfortable",bg:"#6ab05a",fg:"#fff"}:e<32?{label:"Moderate heat",bg:"#e8c547",fg:"#1a1612"}:e<38?{label:"Strong heat",bg:"#dc8a3a",fg:"#1a1612"}:e<46?{label:"Very strong heat",bg:"#c44a3a",fg:"#fff"}:{label:"Extreme heat",bg:"#7a1a1a",fg:"#fff"}}function gt({value:e,cat:t,loading:n}){const f=Array.from({length:36},(m,b)=>{const h=(b*10-90)*Math.PI/180,y=b%9===0,k=b%3===0,C=y?72:k?78:82;return{x1:100+86*Math.cos(h),y1:100+86*Math.sin(h),x2:100+C*Math.cos(h),y2:100+C*Math.sin(h),major:y,medium:k}}),r=[{min:-40,max:-27,color:"#23408f"},{min:-27,max:-13,color:"#3f73c4"},{min:-13,max:0,color:"#7eb0e0"},{min:0,max:9,color:"#bcd9ec"},{min:9,max:18,color:"#c8dcc0"},{min:18,max:26,color:"#6ab05a"},{min:26,max:32,color:"#e8c547"},{min:32,max:38,color:"#dc8a3a"},{min:38,max:46,color:"#c44a3a"},{min:46,max:50,color:"#7a1a1a"}];function l(m,b){const h=(135+m*270)*Math.PI/180;return[100+b*Math.cos(h),100+b*Math.sin(h)]}function c(m){const b=Math.min(1,Math.max(0,(m.min+10)/60)),w=Math.min(1,Math.max(0,(m.max+10)/60)),h=68,[y,k]=l(b,h),[C,D]=l(w,h),g=(w-b)*270>180?1:0;return`M ${y} ${k} A ${h} ${h} 0 ${g} 1 ${C} ${D}`}let d=100,o=154;if(e!=null){const w=(135+Math.min(1,Math.max(0,(e+10)/60))*270)*Math.PI/180;d=100+54*Math.cos(w),o=100+54*Math.sin(w)}const p=t?t.bg:"#c8922a";return a("svg",{viewBox:"0 0 200 200",className:"scope-ring","aria-label":"Current UTCI scope readout",children:[a("defs",{children:a("radialGradient",{id:"scope-bg-glow",cx:"50%",cy:"50%",r:"50%",children:[a("stop",{offset:"0%","stop-color":p,"stop-opacity":"0.12"}),a("stop",{offset:"100%","stop-color":p,"stop-opacity":"0"})]})}),a("circle",{cx:100,cy:100,r:"96",fill:"url(#scope-bg-glow)"}),r.map((m,b)=>a("path",{d:c(m),fill:"none",stroke:m.color,"stroke-width":"4",opacity:"0.4","stroke-linecap":"butt"},b)),a("circle",{cx:100,cy:100,r:86,fill:"none",stroke:"#c9b08a","stroke-width":"1.5"}),f.map((m,b)=>a("line",{x1:m.x1,y1:m.y1,x2:m.x2,y2:m.y2,stroke:m.major?"#c8922a":m.medium?"#c9b08a":"#e0d0b0","stroke-width":m.major?1.5:.75},b)),a("line",{x1:17,y1:100,x2:68,y2:100,stroke:"#d4b896","stroke-width":"0.75"}),a("line",{x1:132,y1:100,x2:183,y2:100,stroke:"#d4b896","stroke-width":"0.75"}),a("line",{x1:100,y1:17,x2:100,y2:68,stroke:"#d4b896","stroke-width":"0.75"}),a("line",{x1:100,y1:132,x2:100,y2:183,stroke:"#d4b896","stroke-width":"0.75"}),a("circle",{cx:100,cy:100,r:"58",fill:"none",stroke:"#e8d8c0","stroke-width":"0.75"}),a("circle",{cx:100,cy:100,r:"32",fill:"none",stroke:"#e8d8c0","stroke-width":"0.5"}),[[-1,-1],[1,-1],[-1,1],[1,1]].map(([m,b],w)=>a("g",{children:[a("line",{x1:100+m*72,y1:100+b*72,x2:100+m*60,y2:100+b*72,stroke:"#c9b08a","stroke-width":"1"}),a("line",{x1:100+m*72,y1:100+b*72,x2:100+m*72,y2:100+b*60,stroke:"#c9b08a","stroke-width":"1"})]},w)),e!=null&&a(j,{children:[a("line",{x1:100,y1:100,x2:d,y2:o,stroke:"#c8922a","stroke-width":"3","stroke-linecap":"round",opacity:"0.18"}),a("line",{x1:100,y1:100,x2:d,y2:o,stroke:"#c8922a","stroke-width":"1.5","stroke-linecap":"round",opacity:"0.95"})]}),a("circle",{cx:100,cy:100,r:"5.5",fill:"#f5edd6",stroke:"#c8922a","stroke-width":"1.5"}),a("circle",{cx:100,cy:100,r:"2.5",fill:"#c8922a"}),n?a("text",{x:100,y:105,"text-anchor":"middle",fill:"#b09870","font-size":"11","font-family":"monospace",children:"· · ·"}):e!=null?a(j,{children:[a("text",{x:100,y:92,"text-anchor":"middle",fill:"#1e1208","font-size":"26","font-family":"Fraunces, serif","font-weight":"700",children:[e.toFixed(1),"°"]}),a("text",{x:100,y:107,"text-anchor":"middle",fill:"#9a7d5a","font-size":"6.5","font-family":"JetBrains Mono, monospace","letter-spacing":"2",children:"UTCI NOW"}),a("text",{x:100,y:119,"text-anchor":"middle",fill:p,"font-size":"7","font-family":"JetBrains Mono, monospace","letter-spacing":"0.8",children:t.label.toUpperCase()})]}):a("text",{x:100,y:105,"text-anchor":"middle",fill:"#b09870","font-size":"8","font-family":"JetBrains Mono, monospace","letter-spacing":"1.2",children:"AWAITING"})]})}function bt(e,t,n){let _=0;if(e>0){const s=Math.min(7,1.4*Math.pow(e,.55)+e*.28),i=1+Math.min(.35,n*.025);_+=s*i}return t>0&&(_+=Math.min(6,2.2+t*1.6)),-Math.round(_*10)/10}function vt(){var v;const[e,t]=F({name:"Pangbourne, Berkshire",lat:51.4839,lon:-1.0725,country:"GB"}),[n,_]=F(null),[s,i]=F(!1),[f,r]=F(null),[l,c]=F(""),[d,o]=F([]),[p,m]=F(!1),[b,w]=F(0),[h,y]=F({hour:!0,air:!0,rh:!0,wind:!0,cloud:!1,sun:!1,direct:!1,diffuse:!1,tmrt:!1,delta:!1,utci:!1,utciP:!0,precip:!0}),k=u=>y(x=>({...x,[u]:!x[u]})),C=lt(null);Ae(()=>{if(l.length<2){o([]);return}C.current&&clearTimeout(C.current),C.current=setTimeout(async()=>{m(!0);try{const x=await(await fetch(`https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(l)}&count=6&language=en&format=json`)).json();o(x.results||[])}catch{o([])}finally{m(!1)}},300)},[l]),Ae(()=>{async function u(){i(!0),r(null);try{const x=`https://api.open-meteo.com/v1/forecast?latitude=${e.lat}&longitude=${e.lon}&hourly=temperature_2m,relative_humidity_2m,wind_speed_10m,direct_radiation,diffuse_radiation,shortwave_radiation,cloud_cover,precipitation,snowfall&wind_speed_unit=ms&timezone=auto&forecast_days=3`,S=await fetch(x);if(!S.ok)throw new Error(`Open-Meteo HTTP ${S.status}`);_(await S.json())}catch(x){r(x.message)}finally{i(!1)}}u()},[e]);const D=n?n.hourly.time.map((u,x)=>{const S=n.hourly.temperature_2m[x],H=n.hourly.relative_humidity_2m[x],R=n.hourly.wind_speed_10m[x],B=n.hourly.direct_radiation[x]||0,he=n.hourly.diffuse_radiation[x]||0,fe=n.hourly.shortwave_radiation[x]||0,Ye=n.hourly.cloud_cover[x],me=n.hourly.precipitation[x]||0,pe=n.hourly.snowfall[x]||0,ye=new Date(u),ge=mt(e.lat,e.lon,ye),be=ft(S,H),ve=pt(S,B,he,fe,ge),xe=yt(S,ve,R,be),Qe=xe+bt(me,pe,R);return{iso:u,dt:ye,Ta:S,RH:H,va:R,dir:B,dif:he,glob:fe,cc:Ye,precip:me,snow:pe,elev:ge,Tmrt:ve,utci:xe,utciAdj:Qe,eh:be}}):[],g=[];D.forEach(u=>{const x=u.iso.slice(0,10);let S=g.find(H=>H.key===x);S||(S={key:x,date:new Date(u.iso),rows:[]},g.push(S)),S.rows.push(u)});const M=((v=g[b])==null?void 0:v.rows)||[],T=new Date,I=D.length>0?D.find(u=>T.toDateString()===u.dt.toDateString()&&T.getHours()===u.dt.getHours())??D.reduce((u,x)=>Math.abs(x.dt-T)c(u.target.value)}),d.length>0&&a("div",{className:"utci-results",children:d.map(u=>a("div",{className:"utci-result",onClick:()=>{t({name:`${u.name}${u.admin1?", "+u.admin1:""}`,lat:u.latitude,lon:u.longitude,country:u.country_code}),c(""),o([]),w(0)},children:[a("div",{children:[u.name,u.admin1?`, ${u.admin1}`:""]}),a("div",{className:"utci-result-meta",children:[u.country," · ",u.latitude.toFixed(2),"°, ",u.longitude.toFixed(2),"°"]})]},`${u.id}-${u.latitude}`))}),p&&a("div",{className:"utci-searching",children:"Searching…"})]})})]}),f&&a("div",{className:"utci-status",style:{borderColor:"#3a1010",color:"#c44a3a"},children:["⚠ ",f]}),s&&!f&&a("div",{className:"utci-status",children:"Acquiring forecast data…"}),n&&g.length>0&&a(j,{children:[a("div",{className:"utci-day-tabs",children:g.map((u,x)=>a("button",{className:`utci-day-tab ${x===b?"active":""}`,onClick:()=>w(x),children:[x===0?"Today":x===1?"Tomorrow":"Day after",a("span",{className:"utci-day-date",children:u.date.toLocaleDateString("en-GB",{weekday:"long",day:"numeric",month:"short"})})]},u.key))}),a("div",{className:"col-toggles",children:[a("span",{className:"col-toggles-label",children:"Columns:"}),[{key:"hour",label:"Hour"},{key:"air",label:"Air"},{key:"rh",label:"RH"},{key:"wind",label:"Wind"},{key:"cloud",label:"Cloud"},{key:"sun",label:"Sun"},{key:"direct",label:"Direct"},{key:"diffuse",label:"Diffuse"},{key:"tmrt",label:"Tmrt"},{key:"delta",label:"Δ"},{key:"utci",label:"UTCI"},{key:"precip",label:"Precip"},{key:"utciP",label:"UTCI+P"}].map(u=>a("button",{className:`col-toggle${h[u.key]?" on":""}`,onClick:()=>k(u.key),children:u.label},u.key))]}),a("div",{className:"utci-table-wrap",children:a("table",{className:"utci-table",children:[a("thead",{children:a("tr",{children:[h.hour&&a("th",{children:"Hour"}),h.air&&a("th",{children:["Air ",a("span",{className:"col-unit",children:"°C"})]}),h.rh&&a("th",{children:["RH ",a("span",{className:"col-unit",children:"%"})]}),h.wind&&a("th",{children:["Wind ",a("span",{className:"col-unit",children:"m/s"})]}),h.cloud&&a("th",{children:["Cloud ",a("span",{className:"col-unit",children:"%"})]}),h.sun&&a("th",{children:["Sun ",a("span",{className:"col-unit",children:"elev°"})]}),h.direct&&a("th",{children:["Direct ",a("span",{className:"col-unit",children:"W/m²"})]}),h.diffuse&&a("th",{children:["Diffuse ",a("span",{className:"col-unit",children:"W/m²"})]}),h.tmrt&&a("th",{children:["Tmrt ",a("span",{className:"col-unit",children:"°C"})]}),h.delta&&a("th",{children:["Δ ",a("span",{className:"col-unit",children:"UTCI−Air"})]}),h.utci&&a("th",{children:["UTCI ",a("span",{className:"col-unit",children:"°C felt"})]}),h.precip&&a("th",{children:["Precip ",a("span",{className:"col-unit",children:"mm/h"})]}),h.utciP&&a("th",{children:["UTCI+P ",a("span",{className:"col-unit",children:"°C adj."})]})]})}),a("tbody",{children:M.map(u=>{const x=oe(u.utci),S=u.elev<0,H=T.toDateString()===u.dt.toDateString()&&T.getHours()===u.dt.getHours(),R=u.utci-u.Ta;return a("tr",{className:[S?"is-night":"",H?"is-now":""].join(" "),children:[h.hour&&a("td",{className:"utci-time",children:[H&&a("span",{className:"now-pip"}),u.dt.toLocaleTimeString("en-GB",{hour:"2-digit",minute:"2-digit"})]}),h.air&&a("td",{children:u.Ta.toFixed(1)}),h.rh&&a("td",{children:Math.round(u.RH)}),h.wind&&a("td",{children:u.va.toFixed(1)}),h.cloud&&a("td",{children:Math.round(u.cc)}),h.sun&&a("td",{children:u.elev>0?u.elev.toFixed(1):"—"}),h.direct&&a("td",{children:Math.round(u.dir)}),h.diffuse&&a("td",{children:Math.round(u.dif)}),h.tmrt&&a("td",{children:u.Tmrt.toFixed(1)}),h.delta&&a("td",{style:{color:R>3?"#c8601a":R<-3?"#3f73c4":"#9a7d5a",fontWeight:600},children:[R>0?"+":"",R.toFixed(1)]}),h.utci&&a("td",{children:a("span",{className:"utci-cell",style:{background:x.bg,color:x.fg},children:u.utci.toFixed(1)})}),h.precip&&a("td",{style:{color:u.snow>0?"#6090c8":u.precip>0?"#5090b0":"#c0a880"},children:u.snow>0?"❅ "+u.snow.toFixed(1)+"cm":u.precip>0?u.precip.toFixed(1):"—"}),h.utciP&&(()=>{const B=oe(u.utciAdj);return a("td",{style:{background:"rgba(180,215,250,0.10)"},children:a("span",{className:"utci-cell",style:{background:B.bg,color:B.fg},children:u.utciAdj.toFixed(1)})})})()]},u.iso)})})]})}),a("div",{className:"utci-legend",children:[a("span",{className:"utci-legend-label",children:"Thermal stress bands"}),a("div",{className:"utci-legend-row",children:[{l:"-27 to -13 Arctic",bg:"#3f73c4",fg:"#fff"},{l:"-13 to 0 Freezing",bg:"#7eb0e0",fg:"#111"},{l:"0 to 9 Cold",bg:"#bcd9ec",fg:"#111"},{l:"9 to 18 Chilled",bg:"#c8dcc0",fg:"#111"},{l:"18 to 26 Comfortable",bg:"#6ab05a",fg:"#fff"},{l:"26 to 32 Mod heat",bg:"#e8c547",fg:"#111"},{l:"32 to 38 Strong heat",bg:"#dc8a3a",fg:"#111"},{l:"38 to 46 V. strong",bg:"#c44a3a",fg:"#fff"},{l:"> 46 Extreme heat",bg:"#7a1a1a",fg:"#fff"}].map((u,x)=>a("span",{className:"utci-legend-item",style:{background:u.bg,color:u.fg},children:u.l},x))})]})]}),a("div",{className:"utci-about",children:[a("h2",{className:"utci-about-heading",children:"What is SunScope?"}),a("p",{className:"utci-about-text",children:["SunScope shows how the weather will actually ",a("em",{children:"feel"})," on your body — not just the air temperature. It uses the ",a("strong",{children:"Universal Thermal Climate Index (UTCI)"}),", a peer-reviewed biometeorological standard developed by Bröde et al. (2012) that combines air temperature, humidity, wind speed, and solar radiation into a single ",a("em",{children:"felt temperature"}),". On a calm, sunny winter day UTCI can read several degrees warmer than the thermometer; on a grey, blustery day it can read far colder. Forecast data is sourced in real time from ",a("strong",{children:"Open-Meteo"}),", 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."]}),a("p",{className:"utci-about-text",children:["The ",a("strong",{children:"UTCI+P"})," column adds the ",a("strong",{children:"SunScope soak-factor"}),": an original precipitation penalty that accounts for the extra chill of rain and snow on exposed skin and wet clothing. Light drizzle reduces the felt temperature by around 1–2 °C; heavy rain combined with wind can push it down by 7–8 °C. Snow carries an additional penalty on top. The result is a honest, real-world comfort score for any location worldwide — simply search for your town or city and compare the three-day hourly forecast."]})]}),a("div",{className:"utci-footer",children:[a("em",{children:"Reading the table."})," A large positive Δ means your body is absorbing far more heat than the air temperature alone suggests — typically due to direct solar radiation. On clear sunny days this gap can exceed 10°C even at modest air temperatures."]})]})]})}ct(a(vt,{}),document.getElementById("root")); diff --git a/index.html b/index.html new file mode 100644 index 0000000..c84bcf1 --- /dev/null +++ b/index.html @@ -0,0 +1,15 @@ + + + + + + + SunScope · The True Temperature + + + + + +
+ + diff --git a/robots.txt b/robots.txt new file mode 100644 index 0000000..4c3cd61 --- /dev/null +++ b/robots.txt @@ -0,0 +1,4 @@ +User-agent: * +Allow: / + +Sitemap: https://sunscope.net/sitemap.xml diff --git a/sitemap.xml b/sitemap.xml new file mode 100644 index 0000000..07ec53a --- /dev/null +++ b/sitemap.xml @@ -0,0 +1,13 @@ + + + + https://sunscope.net/ + daily + 1.0 + + + https://sunscope.net/about + monthly + 0.8 + + diff --git a/utci-forecast.jsx b/utci-forecast.jsx new file mode 100644 index 0000000..7a7db8d --- /dev/null +++ b/utci-forecast.jsx @@ -0,0 +1,874 @@ +import { useState, useEffect, useRef } from 'preact/hooks'; +import './styles.css'; + +// ═══════════════════════════════════════════════════════════════════ +// PHYSICAL CONSTANTS +// ═══════════════════════════════════════════════════════════════════ +const SIGMA = 5.670374419e-8; +const EPSILON_P = 0.97; +const A_K = 0.7; +const ALBEDO_GRASS = 0.23; + +// ═══════════════════════════════════════════════════════════════════ +// VAPOR PRESSURE (Magnus formula → hPa) +// ═══════════════════════════════════════════════════════════════════ +function vaporPressureHpa(Ta, RH) { + const es = 6.105 * Math.exp((17.27 * Ta) / (237.7 + Ta)); + return es * (RH / 100); +} + +// ═══════════════════════════════════════════════════════════════════ +// SOLAR POSITION (NOAA simplified; returns elevation in degrees) +// ═══════════════════════════════════════════════════════════════════ +function solarElevationDeg(lat, lon, dateUTC) { + const start = Date.UTC(dateUTC.getUTCFullYear(), 0, 0); + const diff = dateUTC - start; + const DOY = Math.floor(diff / 86400000); + const hourUTC = + dateUTC.getUTCHours() + + dateUTC.getUTCMinutes() / 60 + + dateUTC.getUTCSeconds() / 3600; + + const gamma = ((2 * Math.PI) / 365) * (DOY - 1 + (hourUTC - 12) / 24); + + const eqtime = + 229.18 * + (0.000075 + + 0.001868 * Math.cos(gamma) - + 0.032077 * Math.sin(gamma) - + 0.014615 * Math.cos(2 * gamma) - + 0.040849 * Math.sin(2 * gamma)); + + const decl = + 0.006918 - + 0.399912 * Math.cos(gamma) + + 0.070257 * Math.sin(gamma) - + 0.006758 * Math.cos(2 * gamma) + + 0.000907 * Math.sin(2 * gamma) - + 0.002697 * Math.cos(3 * gamma) + + 0.00148 * Math.sin(3 * gamma); + + const timeOffset = eqtime + 4 * lon; + const tst = hourUTC * 60 + timeOffset; + const ha = (((tst / 4) - 180) * Math.PI) / 180; + + const latRad = (lat * Math.PI) / 180; + const cosZenith = + Math.sin(latRad) * Math.sin(decl) + + Math.cos(latRad) * Math.cos(decl) * Math.cos(ha); + const zenith = Math.acos(Math.max(-1, Math.min(1, cosZenith))); + return (Math.PI / 2 - zenith) * (180 / Math.PI); +} + +// ═══════════════════════════════════════════════════════════════════ +// MEAN RADIANT TEMPERATURE +// ═══════════════════════════════════════════════════════════════════ +function calcTmrt(Ta, dirRad, diffRad, globalRad, solElev) { + const TaK = Ta + 273.15; + let fp = 0; + if (solElev > 0) { + const h = solElev; + fp = 0.308 * Math.cos((Math.PI / 180) * h * (0.998 - (h * h) / 50000)); + } + let DNI = 0; + if (solElev > 1) { + DNI = dirRad / Math.sin((solElev * Math.PI) / 180); + DNI = Math.min(DNI, 1100); + } + const Sshort = A_K * (fp * DNI + 0.5 * diffRad + 0.5 * ALBEDO_GRASS * globalRad); + const Slong = EPSILON_P * SIGMA * Math.pow(TaK, 4); + const TmrtK = Math.pow((Sshort + Slong) / (EPSILON_P * SIGMA), 0.25); + return TmrtK - 273.15; +} + +// ═══════════════════════════════════════════════════════════════════ +// UTCI POLYNOMIAL APPROXIMATION (Bröde et al. 2012, 210 terms) +// ═══════════════════════════════════════════════════════════════════ +function utciApprox(Ta, Tmrt, va10, ehPa) { + const va = Math.max(0.5, Math.min(17, va10)); + const D_Tmrt = Tmrt - Ta; + const Pa = ehPa / 10; + + const T = Ta, V = va, D = D_Tmrt, P = Pa; + const T2=T*T, T3=T2*T, T4=T3*T, T5=T4*T, T6=T5*T; + const V2=V*V, V3=V2*V, V4=V3*V, V5=V4*V, V6=V5*V; + const D2=D*D, D3=D2*D, D4=D3*D, D5=D4*D, D6=D5*D; + const P2=P*P, P3=P2*P, P4=P3*P, P5=P4*P, P6=P5*P; + + return T + + 6.07562052e-1 + + -2.27712343e-2 * T + + 8.06470249e-4 * T2 + + -1.54271372e-4 * T3 + + -3.24651735e-6 * T4 + + 7.32602852e-8 * T5 + + 1.35959073e-9 * T6 + + -2.25836520e0 * V + + 8.80326035e-2 * T*V + + 2.16844454e-3 * T2*V + + -1.53347087e-5 * T3*V + + -5.72983704e-7 * T4*V + + -2.55090145e-9 * T5*V + + -7.51269505e-1 * V2 + + -4.08350271e-3 * T*V2 + + -5.21670675e-5 * T2*V2 + + 1.94544667e-6 * T3*V2 + + 1.14099531e-8 * T4*V2 + + 1.58137256e-1 * V3 + + -6.57263143e-5 * T*V3 + + 2.22697524e-7 * T2*V3 + + -4.16117031e-8 * T3*V3 + + -1.27762753e-2 * V4 + + 9.66891875e-6 * T*V4 + + 2.52785852e-9 * T2*V4 + + 4.56306672e-4 * V5 + + -1.74202546e-7 * T*V5 + + -5.91491269e-6 * V6 + + 3.98374029e-1 * D + + 1.83945314e-4 * T*D + + -1.73754510e-4 * T2*D + + -7.60781159e-7 * T3*D + + 3.77830287e-8 * T4*D + + 5.43079673e-10* T5*D + + -2.00518269e-2 * V*D + + 8.92859837e-4 * T*V*D + + 3.45433048e-6 * T2*V*D + + -3.77925774e-7 * T3*V*D + + -1.69699377e-9 * T4*V*D + + 1.69992415e-4 * V2*D + + -4.99204314e-5 * T*V2*D + + 2.47417178e-7 * T2*V2*D + + 1.07596466e-8 * T3*V2*D + + 8.49242932e-5 * V3*D + + 1.35191328e-6 * T*V3*D + + -6.21531254e-9 * T2*V3*D + + -4.99410301e-6 * V4*D + + -1.89489258e-8 * T*V4*D + + 8.15300114e-8 * V5*D + + 7.55043090e-4 * D2 + + -5.65095215e-5 * T*D2 + + -4.52166564e-7 * T2*D2 + + 2.46688878e-8 * T3*D2 + + 2.42674348e-10* T4*D2 + + 1.54547250e-4 * V*D2 + + 5.24110970e-6 * T*V*D2 + + -8.75874982e-8 * T2*V*D2 + + -1.50743064e-9 * T3*V*D2 + + -1.56236307e-5 * V2*D2 + + -1.33895614e-7 * T*V2*D2 + + 2.49709824e-9 * T2*V2*D2 + + 6.51711721e-7 * V3*D2 + + 1.94960053e-9 * T*V3*D2 + + -1.00361113e-8 * V4*D2 + + -1.21206673e-5 * D3 + + -2.18203660e-7 * T*D3 + + 7.51269482e-9 * T2*D3 + + 9.79063848e-11* T3*D3 + + 1.25006734e-6 * V*D3 + + -1.81584736e-9 * T*V*D3 + + -3.52197671e-10* T2*V*D3 + + -3.36514630e-8 * V2*D3 + + 1.35908359e-10* T*V2*D3 + + 4.17032620e-10* V3*D3 + + -1.30369025e-9 * D4 + + 4.13908461e-10* T*D4 + + 9.22652254e-12* T2*D4 + + -5.08220384e-9 * V*D4 + + -2.24730961e-11* T*V*D4 + + 1.17139133e-10* V2*D4 + + 6.62154879e-10* D5 + + 4.03863260e-13* T*D5 + + 1.95087203e-12* V*D5 + + -4.73602469e-12* D6 + + 5.12733497e0 * P + + -3.12788561e-1 * T*P + + -1.96701861e-2 * T2*P + + 9.99690870e-4 * T3*P + + 9.51738512e-6 * T4*P + + -4.66426341e-7 * T5*P + + 5.48050612e-1 * V*P + + -3.30552823e-3 * T*V*P + + -1.64119440e-3 * T2*V*P + + -5.16670694e-6 * T3*V*P + + 9.52692432e-7 * T4*V*P + + -4.29223622e-2 * V2*P + + 5.00845667e-3 * T*V2*P + + 1.00601257e-6 * T2*V2*P + + -1.81748644e-6 * T3*V2*P + + -1.25813502e-3 * V3*P + + -1.79330391e-4 * T*V3*P + + 2.34994441e-6 * T2*V3*P + + 1.29735808e-4 * V4*P + + 1.29064870e-6 * T*V4*P + + -2.28558686e-6 * V5*P + + -3.69476348e-2 * D*P + + 1.62325322e-3 * T*D*P + + -3.14279680e-5 * T2*D*P + + 2.59835559e-6 * T3*D*P + + -4.77136523e-8 * T4*D*P + + 8.64203390e-3 * V*D*P + + -6.87405181e-4 * T*V*D*P + + -9.13863872e-6 * T2*V*D*P + + 5.15916806e-7 * T3*V*D*P + + -3.59217476e-5 * V2*D*P + + 3.28696511e-5 * T*V2*D*P + + -7.10542454e-7 * T2*V2*D*P + + -1.24382300e-5 * V3*D*P + + -7.38584400e-9 * T*V3*D*P + + 2.20609296e-7 * V4*D*P + + -7.32469180e-4 * D2*P + + -1.87381964e-5 * T*D2*P + + 4.80925239e-6 * T2*D2*P + + -8.75492040e-8 * T3*D2*P + + 2.77862930e-5 * V*D2*P + + -5.06004592e-6 * T*V*D2*P + + 1.14325367e-7 * T2*V*D2*P + + 2.53016723e-6 * V2*D2*P + + -1.72857035e-8 * T*V2*D2*P + + -3.95079398e-8 * V3*D2*P + + -3.59413173e-7 * D3*P + + 7.04388046e-7 * T*D3*P + + -1.89309167e-8 * T2*D3*P + + -4.79768731e-7 * V*D3*P + + 7.96079978e-9 * T*V*D3*P + + 1.62897058e-9 * V2*D3*P + + 3.94367674e-8 * D4*P + + -1.18566247e-9 * T*D4*P + + 3.34678041e-10* V*D4*P + + -1.15606447e-10* D5*P + + -2.80626406e0 * P2 + + 5.48712484e-1 * T*P2 + + -3.99428410e-3 * T2*P2 + + -9.54009191e-4 * T3*P2 + + 1.93090978e-5 * T4*P2 + + -3.08806365e-1 * V*P2 + + 1.16952364e-2 * T*V*P2 + + 4.95271903e-4 * T2*V*P2 + + -1.90710882e-5 * T3*V*P2 + + 2.10787756e-3 * V2*P2 + + -6.98445738e-4 * T*V2*P2 + + 2.30109073e-5 * T2*V2*P2 + + 4.17856590e-4 * V3*P2 + + -1.27043871e-5 * T*V3*P2 + + -3.04620472e-6 * V4*P2 + + 5.14507424e-2 * D*P2 + + -4.32510997e-3 * T*D*P2 + + 8.99281156e-5 * T2*D*P2 + + -7.14663943e-7 * T3*D*P2 + + -2.66016305e-4 * V*D*P2 + + 2.63789586e-4 * T*V*D*P2 + + -7.01199003e-6 * T2*V*D*P2 + + -1.06823306e-4 * V2*D*P2 + + 3.61341136e-6 * T*V2*D*P2 + + 2.29748967e-7 * V3*D*P2 + + 3.04788893e-4 * D2*P2 + + -6.42070836e-5 * T*D2*P2 + + 1.16257971e-6 * T2*D2*P2 + + 7.68023384e-6 * V*D2*P2 + + -5.47446896e-7 * T*V*D2*P2 + + -3.59937910e-8 * V2*D2*P2 + + -4.36497725e-6 * D3*P2 + + 1.68737969e-7 * T*D3*P2 + + 2.67489271e-8 * V*D3*P2 + + 3.23926897e-9 * D4*P2 + + -3.53874123e-2 * P3 + + -2.21201190e-1 * T*P3 + + 1.55126038e-2 * T2*P3 + + -2.63917279e-4 * T3*P3 + + 4.53433455e-2 * V*P3 + + -4.32943862e-3 * T*V*P3 + + 1.45389826e-4 * T2*V*P3 + + 2.17508610e-4 * V2*P3 + + -6.66724702e-5 * T*V2*P3 + + 3.33217140e-5 * V3*P3 + + -2.26921615e-3 * D*P3 + + 3.80261982e-4 * T*D*P3 + + -5.45314314e-9 * T2*D*P3 + + -7.96355448e-4 * V*D*P3 + + 2.53458034e-5 * T*V*D*P3 + + -6.31223658e-6 * V2*D*P3 + + 3.02122035e-4 * D2*P3 + + -4.77403547e-6 * T*D2*P3 + + 1.73825715e-6 * V*D2*P3 + + -4.09087898e-7 * D3*P3 + + 6.14155345e-1 * P4 + + -6.16755931e-2 * T*P4 + + 1.33374846e-3 * T2*P4 + + 3.55375387e-3 * V*P4 + + -5.13027851e-4 * T*V*P4 + + 1.02449757e-4 * V2*P4 + + -1.48526421e-3 * D*P4 + + -4.11469183e-5 * T*D*P4 + + -6.80434415e-6 * V*D*P4 + + -9.77675906e-6 * D2*P4 + + 8.82773108e-2 * P5 + + -3.01859306e-3 * T*P5 + + 1.04452989e-3 * V*P5 + + 2.47090539e-4 * D*P5 + + 1.48348065e-3 * P6; +} + +// ═══════════════════════════════════════════════════════════════════ +// UTCI STRESS BANDS +// ═══════════════════════════════════════════════════════════════════ +function utciCategory(u) { + if (u < -40) return { label: 'Extreme cold', bg: '#1a1438', fg: '#fff' }; + if (u < -27) return { label: 'Very strong cold', bg: '#23408f', fg: '#fff' }; + if (u < -13) return { label: 'Arctic', bg: '#3f73c4', fg: '#fff' }; + if (u < 0) return { label: 'Freezing', bg: '#7eb0e0', fg: '#1a1612' }; + if (u < 9) return { label: 'Cold', bg: '#bcd9ec', fg: '#1a1612' }; + if (u < 18) return { label: 'Chilled', bg: '#c8dcc0', fg: '#1a1612' }; + if (u < 26) return { label: 'Comfortable', bg: '#6ab05a', fg: '#fff' }; + if (u < 32) return { label: 'Moderate heat', bg: '#e8c547', fg: '#1a1612' }; + if (u < 38) return { label: 'Strong heat', bg: '#dc8a3a', fg: '#1a1612' }; + if (u < 46) return { label: 'Very strong heat', bg: '#c44a3a', fg: '#fff' }; + return { label: 'Extreme heat', bg: '#7a1a1a', fg: '#fff' }; +} + +// ═══════════════════════════════════════════════════════════════════ +// SCOPE RETICLE — shows current UTCI like a precision instrument +// (uses only basic SVG primitives for maximum compatibility) +// ═══════════════════════════════════════════════════════════════════ +function ScopeReticle({ value, cat, loading }) { + const cx = 100, cy = 100, R = 86; + + // 36 tick marks around the ring + const ticks = Array.from({ length: 36 }, (_, i) => { + const deg = i * 10 - 90; + const rad = (deg * Math.PI) / 180; + const major = i % 9 === 0; + const medium = i % 3 === 0; + const r2 = major ? R - 14 : medium ? R - 8 : R - 4; + return { + x1: cx + R * Math.cos(rad), y1: cy + R * Math.sin(rad), + x2: cx + r2 * Math.cos(rad), y2: cy + r2 * Math.sin(rad), + major, medium, + }; + }); + + // Stress band arc segments: UTCI -40..50 → 135°..405° (270° sweep) + const stressBands = [ + { min: -40, max: -27, color: '#23408f' }, + { min: -27, max: -13, color: '#3f73c4' }, + { min: -13, max: 0, color: '#7eb0e0' }, + { min: 0, max: 9, color: '#bcd9ec' }, + { min: 9, max: 18, color: '#c8dcc0' }, + { min: 18, max: 26, color: '#6ab05a' }, + { min: 26, max: 32, color: '#e8c547' }, + { min: 32, max: 38, color: '#dc8a3a' }, + { min: 38, max: 46, color: '#c44a3a' }, + { min: 46, max: 50, color: '#7a1a1a' }, + ]; + + function fracToXY(frac, r) { + const deg = 135 + frac * 270; + const rad = (deg * Math.PI) / 180; + return [cx + r * Math.cos(rad), cy + r * Math.sin(rad)]; + } + + function bandArcPath(band) { + const f1 = Math.min(1, Math.max(0, (band.min + 10) / 60)); + const f2 = Math.min(1, Math.max(0, (band.max + 10) / 60)); + const arcR = R - 18; + const [x1, y1] = fracToXY(f1, arcR); + const [x2, y2] = fracToXY(f2, arcR); + const large = (f2 - f1) * 270 > 180 ? 1 : 0; + return `M ${x1} ${y1} A ${arcR} ${arcR} 0 ${large} 1 ${x2} ${y2}`; + } + + // Needle position + let needleX = cx, needleY = cy + 54; + if (value != null) { + const frac = Math.min(1, Math.max(0, (value + 10) / 60)); + const deg = 135 + frac * 270; + const rad = (deg * Math.PI) / 180; + needleX = cx + 54 * Math.cos(rad); + needleY = cy + 54 * Math.sin(rad); + } + + const glowColor = cat ? cat.bg : '#c8922a'; + + return ( + + + + + + + + + {/* Ambient glow */} + + + {/* Stress band arcs */} + {stressBands.map((b, i) => ( + + ))} + + {/* Outer ring */} + + + {/* Tick marks */} + {ticks.map((t, i) => ( + + ))} + + {/* Crosshair lines */} + + + + + + {/* Inner rings */} + + + + {/* Corner bracket markers */} + {[[-1,-1],[1,-1],[-1,1],[1,1]].map(([sx, sy], i) => ( + + + + + ))} + + {/* Needle */} + {value != null && ( + <> + + + + )} + + {/* Centre pivot */} + + + + {/* Readout text */} + {loading ? ( + · · · + ) : value != null ? ( + <> + + {value.toFixed(1)}° + + + UTCI NOW + + + {cat.label.toUpperCase()} + + + ) : ( + + AWAITING + + )} + + ); +} + +// ═══════════════════════════════════════════════════════════════════ +// MAIN COMPONENT +// ═══════════════════════════════════════════════════════════════════ + +// ═══════════════════════════════════════════════════════════════════ +// PRECIPITATION PENALTY (applied on top of UTCI) +// Rain → evaporative + wet-clothing cooling, amplified by wind +// Snow → heavier conductive penalty on top of any rain +// ═══════════════════════════════════════════════════════════════════ +function precipPenalty(precipMm, snowCmH, windMs) { + let penalty = 0; + if (precipMm > 0) { + // Scales: drizzle 0.1mm→-1.2°, moderate 2mm→-3.8°, heavy 8mm→-6.5° + const base = Math.min(7, 1.4 * Math.pow(precipMm, 0.55) + precipMm * 0.28); + // Wind amplifies wet chill (up to +35% at gale force) + const windMult = 1 + Math.min(0.35, windMs * 0.025); + penalty += base * windMult; + } + if (snowCmH > 0) { + // Snow adds extra penalty on top (wet snow especially brutal) + penalty += Math.min(6, 2.2 + snowCmH * 1.6); + } + return -Math.round(penalty * 10) / 10; +} + +export default function UTCIForecast() { + const [location, setLocation] = useState({ + name: 'Pangbourne, Berkshire', + lat: 51.4839, + lon: -1.0725, + country: 'GB', + }); + const [forecast, setForecast] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [searchQuery, setSearchQuery] = useState(''); + const [searchResults, setSearchResults] = useState([]); + const [searching, setSearching] = useState(false); + const [selectedDay, setSelectedDay] = useState(0); + const [visibleCols, setVisibleCols] = useState({ + hour: true, air: true, rh: true, wind: true, + cloud: false, sun: false, direct: false, diffuse: false, + tmrt: false, delta: false, utci: false, utciP: true, + precip: true, + }); + const toggleCol = (col) => setVisibleCols(prev => ({ ...prev, [col]: !prev[col] })); + const searchTimeout = useRef(null); + + // Geocoding search + useEffect(() => { + if (searchQuery.length < 2) { setSearchResults([]); return; } + if (searchTimeout.current) clearTimeout(searchTimeout.current); + searchTimeout.current = setTimeout(async () => { + setSearching(true); + try { + const r = await fetch( + `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(searchQuery)}&count=6&language=en&format=json` + ); + const j = await r.json(); + setSearchResults(j.results || []); + } catch { setSearchResults([]); } + finally { setSearching(false); } + }, 300); + }, [searchQuery]); + + // Forecast fetch + useEffect(() => { + async function load() { + setLoading(true); setError(null); + try { + const url = + `https://api.open-meteo.com/v1/forecast` + + `?latitude=${location.lat}&longitude=${location.lon}` + + `&hourly=temperature_2m,relative_humidity_2m,wind_speed_10m,` + + `direct_radiation,diffuse_radiation,shortwave_radiation,cloud_cover,` + + `precipitation,snowfall` + + `&wind_speed_unit=ms&timezone=auto&forecast_days=3`; + const r = await fetch(url); + if (!r.ok) throw new Error(`Open-Meteo HTTP ${r.status}`); + setForecast(await r.json()); + } catch (e) { setError(e.message); } + finally { setLoading(false); } + } + load(); + }, [location]); + + // Compute hourly rows + const hourlyRows = forecast ? forecast.hourly.time.map((iso, i) => { + const Ta = forecast.hourly.temperature_2m[i]; + const RH = forecast.hourly.relative_humidity_2m[i]; + const va = forecast.hourly.wind_speed_10m[i]; + const dir = forecast.hourly.direct_radiation[i] || 0; + const dif = forecast.hourly.diffuse_radiation[i] || 0; + const glob = forecast.hourly.shortwave_radiation[i] || 0; + const cc = forecast.hourly.cloud_cover[i]; + const precip = forecast.hourly.precipitation[i] || 0; + const snow = forecast.hourly.snowfall[i] || 0; + const dt = new Date(iso); + const elev = solarElevationDeg(location.lat, location.lon, dt); + const eh = vaporPressureHpa(Ta, RH); + const Tmrt = calcTmrt(Ta, dir, dif, glob, elev); + const utci = utciApprox(Ta, Tmrt, va, eh); + const utciAdj = utci + precipPenalty(precip, snow, va); + return { iso, dt, Ta, RH, va, dir, dif, glob, cc, precip, snow, elev, Tmrt, utci, utciAdj, eh }; + }) : []; + + // Group by day + const days = []; + hourlyRows.forEach(row => { + const key = row.iso.slice(0, 10); + let day = days.find(d => d.key === key); + if (!day) { day = { key, date: new Date(row.iso), rows: [] }; days.push(day); } + day.rows.push(row); + }); + + const visible = days[selectedDay]?.rows || []; + + // Current moment — for reticle + row highlight + const now = new Date(); + const currentRow = hourlyRows.length > 0 + ? (hourlyRows.find(row => + now.toDateString() === row.dt.toDateString() && + now.getHours() === row.dt.getHours() + ) ?? hourlyRows.reduce((best, row) => + Math.abs(row.dt - now) < Math.abs(best.dt - now) ? row : best)) + : null; + const currentCat = currentRow + ? utciCategory(currentRow.utciAdj) + : { bg: '#4a4228', fg: '#ede4cc', label: 'No data' }; + + + return ( +
+ +
+
+ +
+ +
+ +
+

SUNScope beta

+
See the sun the way your body does.
+
Universal Thermal Climate Index · Bröde 2012 · Open-Meteo · SunScope soak-factor
+
+ ↳ {location.name} + + {location.lat.toFixed(3)}°, {location.lon.toFixed(3)}° + +
+
+ +
+ +
+ +
+
+ + setSearchQuery(e.target.value)} + /> + {searchResults.length > 0 && ( +
+ {searchResults.map((r) => ( +
{ + setLocation({ + name: `${r.name}${r.admin1 ? ', ' + r.admin1 : ''}`, + lat: r.latitude, + lon: r.longitude, + country: r.country_code, + }); + setSearchQuery(''); + setSearchResults([]); + setSelectedDay(0); + }} + > +
{r.name}{r.admin1 ? `, ${r.admin1}` : ''}
+
+ {r.country} · {r.latitude.toFixed(2)}°, {r.longitude.toFixed(2)}° +
+
+ ))} +
+ )} + {searching &&
Searching…
} +
+
+ +
+ + {error && ( +
+ ⚠ {error} +
+ )} + {loading && !error && ( +
Acquiring forecast data…
+ )} + + {forecast && days.length > 0 && ( + <> +
+ {days.map((d, i) => ( + + ))} +
+ +
+ Columns: + {[ + { key: 'hour', label: 'Hour' }, + { key: 'air', label: 'Air' }, + { key: 'rh', label: 'RH' }, + { key: 'wind', label: 'Wind' }, + { key: 'cloud', label: 'Cloud' }, + { key: 'sun', label: 'Sun' }, + { key: 'direct', label: 'Direct' }, + { key: 'diffuse', label: 'Diffuse' }, + { key: 'tmrt', label: 'Tmrt' }, + { key: 'delta', label: 'Δ' }, + { key: 'utci', label: 'UTCI' }, + { key: 'precip', label: 'Precip' }, + { key: 'utciP', label: 'UTCI+P' }, + ].map(c => ( + + ))} +
+ +
+ + + + {visibleCols.hour && } + {visibleCols.air && } + {visibleCols.rh && } + {visibleCols.wind && } + {visibleCols.cloud && } + {visibleCols.sun && } + {visibleCols.direct && } + {visibleCols.diffuse && } + {visibleCols.tmrt && } + {visibleCols.delta && } + {visibleCols.utci && } + {visibleCols.precip && } + {visibleCols.utciP && } + + + + {visible.map((r) => { + const cat = utciCategory(r.utci); + const isNight = r.elev < 0; + const isNow = + now.toDateString() === r.dt.toDateString() && + now.getHours() === r.dt.getHours(); + const delta = r.utci - r.Ta; + return ( + + {visibleCols.hour && } + {visibleCols.air && } + {visibleCols.rh && } + {visibleCols.wind && } + {visibleCols.cloud && } + {visibleCols.sun && } + {visibleCols.direct && } + {visibleCols.diffuse && } + {visibleCols.tmrt && } + {visibleCols.delta && } + {visibleCols.utci && } + {visibleCols.precip && } + {visibleCols.utciP && (() => { + const adjCat = utciCategory(r.utciAdj); + return ; + })()} + + ); + })} + +
HourAir °CRH %Wind m/sCloud %Sun elev°Direct W/m²Diffuse W/m²Tmrt °CΔ UTCI−AirUTCI °C feltPrecip mm/hUTCI+P °C adj.
+ {isNow && } + {r.dt.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' })} + {r.Ta.toFixed(1)}{Math.round(r.RH)}{r.va.toFixed(1)}{Math.round(r.cc)}{r.elev > 0 ? r.elev.toFixed(1) : '—'}{Math.round(r.dir)}{Math.round(r.dif)}{r.Tmrt.toFixed(1)} 3 ? '#c8601a' : delta < -3 ? '#3f73c4' : '#9a7d5a', + fontWeight: 600, + }}> + {delta > 0 ? '+' : ''}{delta.toFixed(1)} + + + {r.utci.toFixed(1)} + + 0 ? '#6090c8' : r.precip > 0 ? '#5090b0' : '#c0a880' }}> + {r.snow > 0 ? '❅ ' + r.snow.toFixed(1) + 'cm' : r.precip > 0 ? r.precip.toFixed(1) : '—'} + + + {r.utciAdj.toFixed(1)} + +
+
+ +
+ Thermal stress bands +
+ {[ + { l: '-27 to -13 Arctic', bg: '#3f73c4', fg: '#fff' }, + { l: '-13 to 0 Freezing', bg: '#7eb0e0', fg: '#111' }, + { l: '0 to 9 Cold', bg: '#bcd9ec', fg: '#111' }, + { l: '9 to 18 Chilled', bg: '#c8dcc0', fg: '#111' }, + { l: '18 to 26 Comfortable', bg: '#6ab05a', fg: '#fff' }, + { l: '26 to 32 Mod heat', bg: '#e8c547', fg: '#111' }, + { l: '32 to 38 Strong heat', bg: '#dc8a3a', fg: '#111' }, + { l: '38 to 46 V. strong', bg: '#c44a3a', fg: '#fff' }, + { l: '> 46 Extreme heat', bg: '#7a1a1a', fg: '#fff' }, + ].map((b, i) => ( + + {b.l} + + ))} +
+
+ + )} + + +
+

What is SunScope?

+

+ SunScope shows how the weather will actually feel on your body — not just the air + temperature. It uses the Universal Thermal Climate Index (UTCI), a + peer-reviewed biometeorological standard developed by Bröde et al. (2012) that combines + air temperature, humidity, wind speed, and solar radiation into a single felt + temperature. On a calm, sunny winter day UTCI can read several degrees warmer than + the thermometer; on a grey, blustery day it can read far colder. Forecast data is + sourced in real time from Open-Meteo, 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. +

+

+ The UTCI+P column adds the SunScope soak-factor: an + original precipitation penalty that accounts for the extra chill of rain and snow on + exposed skin and wet clothing. Light drizzle reduces the felt temperature by around + 1–2 °C; heavy rain combined with wind can push it down by 7–8 °C. Snow carries an + additional penalty on top. The result is a honest, real-world comfort score for any + location worldwide — simply search for your town or city and compare the three-day + hourly forecast. +

+
+
+ Reading the table. A large positive Δ means your body is absorbing + far more heat than the air temperature alone suggests — typically due to direct solar radiation. + On clear sunny days this gap can exceed 10°C even at modest air temperatures. +
+ +
+
+ ); +}