Fix timezones
This commit is contained in:
+41
-16
@@ -358,6 +358,19 @@ export function UTCIForecast() {
|
|||||||
// Take the raw API arrays and stitch them into one object per hour,
|
// Take the raw API arrays and stitch them into one object per hour,
|
||||||
// calculating UTCI + soak-factor for each row. This is what gets
|
// calculating UTCI + soak-factor for each row. This is what gets
|
||||||
// displayed in the table.
|
// displayed in the table.
|
||||||
|
// Open-Meteo with timezone=auto returns local wall-clock strings like
|
||||||
|
// "2026-05-13T14:00" — no Z, no offset suffix. We need two things:
|
||||||
|
// 1. The wall-clock hour for display & day grouping (just slice the string)
|
||||||
|
// 2. The true UTC instant for solarElevationDeg (which uses .getUTC* internally)
|
||||||
|
// Strategy: treat the ISO string as UTC (append Z), which gives a Date whose
|
||||||
|
// UTC hours equal the local wall-clock hour. Then ADD the utc_offset_seconds
|
||||||
|
// to shift it to the real UTC instant. e.g. Brisbane UTC+10: "14:00" local
|
||||||
|
// → parse as UTC 14:00 → add 10h → UTC 00:00 next day? No — subtract.
|
||||||
|
// Brisbane local 14:00 = UTC 04:00, offset = +10h, so UTC = local - offset.
|
||||||
|
// Date.parse("2026-05-13T14:00Z") = ms for UTC 14:00
|
||||||
|
// Subtract offset (+10h = 36000000ms) → UTC 04:00. Correct.
|
||||||
|
const utcOffsetMs = (forecast?.utc_offset_seconds ?? 0) * 1000;
|
||||||
|
|
||||||
const hourlyRows = forecast ? forecast.hourly.time.map((iso, i) => {
|
const hourlyRows = forecast ? forecast.hourly.time.map((iso, i) => {
|
||||||
const h = forecast.hourly;
|
const h = forecast.hourly;
|
||||||
const Ta = h.temperature_2m[i];
|
const Ta = h.temperature_2m[i];
|
||||||
@@ -379,8 +392,15 @@ export function UTCIForecast() {
|
|||||||
const soilT0 = h.soil_temperature_0cm ? h.soil_temperature_0cm[i] : null;
|
const soilT0 = h.soil_temperature_0cm ? h.soil_temperature_0cm[i] : null;
|
||||||
const soilT6 = h.soil_temperature_6cm ? h.soil_temperature_6cm[i] : null;
|
const soilT6 = h.soil_temperature_6cm ? h.soil_temperature_6cm[i] : null;
|
||||||
const soilM = h.soil_moisture_0_to_1cm ? h.soil_moisture_0_to_1cm[i] : null;
|
const soilM = h.soil_moisture_0_to_1cm ? h.soil_moisture_0_to_1cm[i] : null;
|
||||||
const dt = new Date(iso);
|
// iso is a local wall-clock string e.g. "2026-05-13T14:00" (no Z).
|
||||||
const elev = solarElevationDeg(location.lat, location.lon, dt);
|
// For display we slice the string directly — no Date object needed.
|
||||||
|
// For solarElevationDeg (which uses .getUTC* internally) we need the
|
||||||
|
// true UTC instant: treat the local time as UTC then subtract the offset.
|
||||||
|
// e.g. Brisbane UTC+10: local 14:00 → parse as UTC 14:00 → subtract 10h → UTC 04:00 ✓
|
||||||
|
const dtUTC = new Date(Date.parse(iso + 'Z') - utcOffsetMs);
|
||||||
|
// dt kept for SkyScope / backward compat — same as dtUTC.
|
||||||
|
const dt = dtUTC;
|
||||||
|
const elev = solarElevationDeg(location.lat, location.lon, dtUTC);
|
||||||
const eh = vaporPressureHpa(Ta, RH);
|
const eh = vaporPressureHpa(Ta, RH);
|
||||||
const Tmrt = calcTmrt(Ta, dir, dif, glob, elev);
|
const Tmrt = calcTmrt(Ta, dir, dif, glob, elev);
|
||||||
const utci = utciApprox(Ta, Tmrt, va, eh);
|
const utci = utciApprox(Ta, Tmrt, va, eh);
|
||||||
@@ -404,17 +424,19 @@ export function UTCIForecast() {
|
|||||||
hourlyRows.forEach(row => {
|
hourlyRows.forEach(row => {
|
||||||
const key = row.iso.slice(0, 10);
|
const key = row.iso.slice(0, 10);
|
||||||
let day = days.find(d => d.key === key);
|
let day = days.find(d => d.key === key);
|
||||||
if (!day) { day = { key, date: new Date(row.iso), rows: [] }; days.push(day); }
|
if (!day) { day = { key, rows: [] }; days.push(day); }
|
||||||
day.rows.push(row);
|
day.rows.push(row);
|
||||||
});
|
});
|
||||||
|
|
||||||
const visible = days[selectedDay]?.rows || [];
|
const visible = days[selectedDay]?.rows || [];
|
||||||
const now = new Date();
|
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)
|
||||||
|
.toISOString().slice(0, 13); // "YYYY-MM-DDTHH"
|
||||||
const currentRow = hourlyRows.length > 0
|
const currentRow = hourlyRows.length > 0
|
||||||
? (hourlyRows.find(row =>
|
? (hourlyRows.find(row => row.iso.slice(0, 13) === nowLocalISO)
|
||||||
now.toDateString() === row.dt.toDateString() &&
|
?? hourlyRows.reduce((best, row) =>
|
||||||
now.getHours() === row.dt.getHours()
|
|
||||||
) ?? hourlyRows.reduce((best, row) =>
|
|
||||||
Math.abs(row.dt - now) < Math.abs(best.dt - now) ? row : best))
|
Math.abs(row.dt - now) < Math.abs(best.dt - now) ? row : best))
|
||||||
: null;
|
: null;
|
||||||
const currentCat = currentRow
|
const currentCat = currentRow
|
||||||
@@ -541,9 +563,12 @@ export function UTCIForecast() {
|
|||||||
const band = confidenceBand(i);
|
const band = confidenceBand(i);
|
||||||
const locked = !isPro && i >= FREE_DAYS;
|
const locked = !isPro && i >= FREE_DAYS;
|
||||||
const isActive = i === selectedDay;
|
const isActive = i === selectedDay;
|
||||||
|
// d.key is "YYYY-MM-DD" in location-local time — parse as UTC so
|
||||||
|
// toLocaleDateString with timeZone:'UTC' reads the correct weekday/date.
|
||||||
|
const dDate = new Date(d.key + 'T00:00Z');
|
||||||
const dayName = i === 0 ? 'Today'
|
const dayName = i === 0 ? 'Today'
|
||||||
: i === 1 ? 'Tomorrow'
|
: i === 1 ? 'Tomorrow'
|
||||||
: d.date.toLocaleDateString('en-GB', { weekday: 'short' });
|
: dDate.toLocaleDateString('en-GB', { weekday: 'short', timeZone: 'UTC' });
|
||||||
return html`
|
return html`
|
||||||
<button
|
<button
|
||||||
key=${d.key}
|
key=${d.key}
|
||||||
@@ -576,7 +601,7 @@ export function UTCIForecast() {
|
|||||||
<span style=${{ position: 'absolute', top: '3px', right: '5px', fontSize: '10px', opacity: 0.75 }}>🔒</span>`}
|
<span style=${{ position: 'absolute', top: '3px', right: '5px', fontSize: '10px', opacity: 0.75 }}>🔒</span>`}
|
||||||
${dayName}
|
${dayName}
|
||||||
<span class="utci-day-date" style=${{ color: '#5a3f24' }}>
|
<span class="utci-day-date" style=${{ color: '#5a3f24' }}>
|
||||||
${d.date.toLocaleDateString('en-GB', { day: 'numeric', month: 'short' })}
|
${dDate.toLocaleDateString('en-GB', { day: 'numeric', month: 'short', timeZone: 'UTC' })}
|
||||||
</span>
|
</span>
|
||||||
</button>`;
|
</button>`;
|
||||||
})}
|
})}
|
||||||
@@ -591,9 +616,9 @@ export function UTCIForecast() {
|
|||||||
form when you have one.
|
form when you have one.
|
||||||
-->
|
-->
|
||||||
${proPromptDay !== null && days[proPromptDay] && (() => {
|
${proPromptDay !== null && days[proPromptDay] && (() => {
|
||||||
const promptDate = days[proPromptDay].date;
|
const promptDate = new Date(days[proPromptDay].key + 'T00:00Z');
|
||||||
const dayName = promptDate.toLocaleDateString('en-GB', { weekday: 'long' });
|
const dayName = promptDate.toLocaleDateString('en-GB', { weekday: 'long', timeZone: 'UTC' });
|
||||||
const dayLong = promptDate.toLocaleDateString('en-GB', { weekday: 'long', day: 'numeric', month: 'short' });
|
const dayLong = promptDate.toLocaleDateString('en-GB', { weekday: 'long', day: 'numeric', month: 'short', timeZone: 'UTC' });
|
||||||
return html`
|
return html`
|
||||||
<div style=${{
|
<div style=${{
|
||||||
margin: '12px 0',
|
margin: '12px 0',
|
||||||
@@ -857,18 +882,18 @@ export function UTCIForecast() {
|
|||||||
${visible.map((r) => {
|
${visible.map((r) => {
|
||||||
const cat = utciCategory(r.utci);
|
const cat = utciCategory(r.utci);
|
||||||
const isNight = r.elev < 0;
|
const isNight = r.elev < 0;
|
||||||
const isNow =
|
const isNow = r.iso.slice(0, 13) === nowLocalISO;
|
||||||
now.toDateString() === r.dt.toDateString() &&
|
|
||||||
now.getHours() === r.dt.getHours();
|
|
||||||
const delta = r.utci - r.Ta;
|
const delta = r.utci - r.Ta;
|
||||||
const adjCat = utciCategory(r.utciAdj);
|
const adjCat = utciCategory(r.utciAdj);
|
||||||
|
// r.iso is the local wall-clock string from the API — slice it directly.
|
||||||
|
const localHHMM = r.iso.slice(11, 16);
|
||||||
return html`
|
return html`
|
||||||
<tr key=${r.iso}
|
<tr key=${r.iso}
|
||||||
class=${`${isNight ? 'is-night' : ''} ${isNow ? 'is-now' : ''}`.trim()}>
|
class=${`${isNight ? 'is-night' : ''} ${isNow ? 'is-now' : ''}`.trim()}>
|
||||||
<td class="utci-time">
|
<td class="utci-time">
|
||||||
<span style=${{ display: 'inline-flex', alignItems: 'center', gap: '7px', verticalAlign: 'middle' }}>
|
<span style=${{ display: 'inline-flex', alignItems: 'center', gap: '7px', verticalAlign: 'middle' }}>
|
||||||
<${SkyScope} elev=${r.elev} dt=${r.dt} size=${30} />
|
<${SkyScope} elev=${r.elev} dt=${r.dt} size=${30} />
|
||||||
<span>${r.dt.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' })}</span>
|
<span>${localHHMM}</span>
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
${visibleCols.air && html`<td>${r.Ta.toFixed(1)}</td>`}
|
${visibleCols.air && html`<td>${r.Ta.toFixed(1)}</td>`}
|
||||||
|
|||||||
Reference in New Issue
Block a user