diff --git a/src/routes/weather/week/[location]/+page.svelte b/src/routes/weather/week/[location]/+page.svelte index ec2bb8e..df8e566 100644 --- a/src/routes/weather/week/[location]/+page.svelte +++ b/src/routes/weather/week/[location]/+page.svelte @@ -1,38 +1,20 @@ @@ -767,1075 +114,34 @@
- -
-
- {#if fetchedDaily} - {#each fetchedDaily.dailyDates as time, index (index)} - {@const selected = time.getDate() === selectedDay.getDate()} - {@const tempMax = fetchedDaily.daily.temperature_2m_max[index]} - {@const tempMin = fetchedDaily.daily.temperature_2m_min[index]} - {@const wCode = fetchedDaily.daily.weather_code[index]} - {@const sunDuration = fetchedDaily.daily.sunshine_duration[index]} - {@const precipSum = fetchedDaily.daily.precipitation_sum[index]} - {@const windMax = fetchedDaily.daily.windspeed_10m_max[index]} - {@const gustMax = fetchedDaily.daily.windgusts_10m_max[index]} - {@const windDir = fetchedDaily.daily.winddirection_10m_dominant[index]} - {#if tempMax != null && !isNaN(tempMax)} - - {/if} - {/each} - {/if} -
-
- - {#if fetchedHourly} - {@const hourly = fetchedHourly.hourly} - {@const dates = fetchedHourly.hourlyDates} - {@const dayIdx = getDayIndices(dates, selectedDay)} - {@const numCols = dayIdx.length} - {@const svgW = numCols * SVG_UNIT} - {@const maxPrecip = Math.max( - ...dayIdx.map((i) => hourly.precipitation[i]).filter((v) => v != null && !isNaN(v)), - 0.1 - )} - {@const dayTemps = dayIdx - .map((i) => hourly.temperature_2m[i]) - .filter((t) => t != null && !isNaN(t))} - {@const minTemp = dayTemps.length ? Math.min(...dayTemps) : 0} - {@const maxTemp = dayTemps.length ? Math.max(...dayTemps) : 10} - {@const iconPx = getIconSize(hourlyInterval)} - -
-

- {selectedDay.toLocaleDateString('en-GB', { weekday: 'long' })} - Hourly -

-
- 3h - - 1h -
-
- - {#if numCols > 0} - -
- - - - - {#each dayIdx as _ (_.toString())} - - {/each} - - - - - - {#each dayIdx as idx (idx)} - {@const date = dates[idx]} - {@const isNow = isCurrentHour(date)} - - {/each} - - - - - - {#each dayIdx as idx (idx)} - {@const wCode = hourly.weather_code[idx]} - {@const date = dates[idx]} - {@const daytime = isDaytimeHour(date.getHours())} - {@const isNow = isCurrentHour(date)} - - {/each} - - - - - - - - - - - - {#each dayIdx as idx (idx)} - {@const val = hourly.precipitation[idx]} - {@const date = dates[idx]} - {@const isNow = isCurrentHour(date)} - - {/each} - - - - - - {#each dayIdx as idx (idx)} - {@const prob = hourly.precipitation_probability[idx]} - {@const date = dates[idx]} - {@const isNow = isCurrentHour(date)} - - {/each} - - - - - - {#each dayIdx as idx (idx)} - {@const val = hourly.precipitation[idx]} - {@const date = dates[idx]} - {@const isNow = isCurrentHour(date)} - - {/each} - - - - - - {#each dayIdx as idx (idx)} - {@const windDir = hourly.winddirection_10m[idx]} - {@const date = dates[idx]} - {@const isNow = isCurrentHour(date)} - - {/each} - - - - - - {#each dayIdx as idx (idx)} - {@const temp = hourly.apparent_temperature[idx]} - {@const date = dates[idx]} - {@const isNow = isCurrentHour(date)} - - {/each} - - - - - - {#each dayIdx as idx (idx)} - {@const hum = hourly.relative_humidity_2m[idx]} - {@const date = dates[idx]} - {@const isNow = isCurrentHour(date)} - - {/each} - - - - - - {#each dayIdx as idx (idx)} - {@const wind = hourly.windspeed_10m[idx]} - {@const date = dates[idx]} - {@const isNow = isCurrentHour(date)} - - {/each} - - - - - - {#each dayIdx as idx (idx)} - {@const dp = hourly.dew_point_2m[idx]} - {@const date = dates[idx]} - {@const isNow = isCurrentHour(date)} - - {/each} - - -
Hourly weather details for {location.name}
- {pad(date.getHours())}00 -
- - - - - - - -
- {params.temperature_unit === 'celsius' ? '°C' : '°F'} - -
- - - - {#each dayIdx as idx, i (idx)} - {@const t = hourly.temperature_2m[idx]} - - {/each} - - - - - {#each dayIdx as idx, i (idx)} - {@const cloud = hourly.cloud_cover[idx]} - - {/each} - - - - - - - - - -
- {#each dayIdx as idx, i (idx)} - {@const t = hourly.temperature_2m[idx]} - {@const rangeT = maxTemp - minTemp || 1} - {@const yPct = - ((1 - ((t ?? minTemp) - minTemp) / rangeT) * 0.7 + 0.05) * 100} - - {t?.toFixed(0) ?? '-'}° - - {/each} -
-
-
-
- {hourlyInterval}h - {params.precipitation_unit === 'mm' ? 'mm' : 'in'} -
-
-
- {#if val > 0} -
- {/if} -
- {val > 0 ? val.toFixed(1) : ''} -
- % - - {prob != null ? prob.toFixed(0) + '%' : '-'} -
- - - - - {#if val > 0} - - - - {/if} -
- - - - - {#if windDir != null && !isNaN(windDir)} -
- - - -
- {/if} -
- - - - - {temp != null ? temp.toFixed(0) + '°' : '-'} -
- - - - - {hum != null ? hum.toFixed(0) + '%' : '-'} -
-
- - - - {params.wind_speed_unit === 'kmh' ? 'km/h' : params.wind_speed_unit} -
-
- {wind?.toFixed(0) ?? '-'} -
- - - - - {dp != null ? dp.toFixed(0) + '°' : '-'} -
-
- {/if} + {/if} - - {#if fetchedDaily} - {@const sunriseTs = fetchedDaily.daily.sunrise[selectedDayIndex]} - {@const sunsetTs = fetchedDaily.daily.sunset[selectedDayIndex]} - {#if sunriseTs && sunsetTs} - {@const sunrise = new Date(sunriseTs * 1000)} - {@const sunset = new Date(sunsetTs * 1000)} -
-
- - - - {pad(sunrise.getHours())}:{pad(sunrise.getMinutes())} -
-
- - - - {pad(sunset.getHours())}:{pad(sunset.getMinutes())} -
-
- {/if} + + + {#if fetchedHourly} + {/if} - -
- -
- - {#if showDetailedCharts} -
-
-

- {selectedDay.toLocaleDateString('en-GB', { weekday: 'long' })} - - {selectedDay.getDate() === new Date(today.getTime() - 24 * 60 * 60 * 1000).getDate() - ? ' (Yesterday)' - : ''} - {selectedDay.getDate() === today.getDate() ? ' (Today)' : ''} - {selectedDay.getDate() === new Date(today.getTime() + 24 * 60 * 60 * 1000).getDate() - ? ' (Tomorrow)' - : ''} - -

- -
- - - {#each chartOptions as option, i (i)} - - {/each} - - -
- -
-
- {/if} -
- - -
-
-
- {#if params.models && params.models.length > 0} - {@const modelValue = params.models[0]} - { - if (params.models && val) { - params.models = [val]; - } - }} - > - {modelSelected?.label} - - {#each models as mo (mo.value)} - {mo.label} - {/each} - - - - {/if} -
-
+ { + params.models = [model]; + }} + />
- - diff --git a/src/routes/weather/week/[location]/DailyCards.svelte b/src/routes/weather/week/[location]/DailyCards.svelte new file mode 100644 index 0000000..c640aec --- /dev/null +++ b/src/routes/weather/week/[location]/DailyCards.svelte @@ -0,0 +1,232 @@ + + +
+
+ {#if daily} + {#each daily.dailyDates as time, index (index)} + {@const selected = time.getDate() === selectedDay.getDate()} + {@const tempMax = daily.daily.temperature_2m_max[index]} + {@const tempMin = daily.daily.temperature_2m_min[index]} + {@const wCode = daily.daily.weather_code[index]} + {@const sunDuration = daily.daily.sunshine_duration[index]} + {@const precipSum = daily.daily.precipitation_sum[index]} + {@const windMax = daily.daily.windspeed_10m_max[index]} + {@const gustMax = daily.daily.windgusts_10m_max[index]} + {@const windDir = daily.daily.winddirection_10m_dominant[index]} + {#if tempMax != null && !isNaN(tempMax)} + + {/if} + {/each} + {/if} +
+
+ + diff --git a/src/routes/weather/week/[location]/HourlyTable.svelte b/src/routes/weather/week/[location]/HourlyTable.svelte new file mode 100644 index 0000000..4297c95 --- /dev/null +++ b/src/routes/weather/week/[location]/HourlyTable.svelte @@ -0,0 +1,625 @@ + + +
+

+ {selectedDay.toLocaleDateString('en-GB', { weekday: 'long' })} – Hourly +

+
+ 3h + + 1h +
+
+ +{#if numCols > 0} + {@const hourly = data.hourly} + {@const dates = data.hourlyDates} +
+ + + + + {#each dayIdx as _ (_.toString())} + + {/each} + + + + + + {#each dayIdx as idx (idx)} + {@const date = dates[idx]} + {@const now = isCurrentHour(date, today)} + + {/each} + + + + + + {#each dayIdx as idx (idx)} + {@const wCode = hourly.weather_code[idx]} + {@const date = dates[idx]} + {@const daytime = isDaytimeHour(date.getHours())} + {@const now = isCurrentHour(date, today)} + + {/each} + + + + + + {#each dayIdx as idx (idx)} + {@const temp = hourly.temperature_2m[idx]} + {@const date = dates[idx]} + {@const now = isCurrentHour(date, today)} + {@const bg = getColor(temp, String(units.temperature_unit))} + {@const fg = getTextColorForTemp(temp, String(units.temperature_unit))} + + {/each} + + + + + + {#each dayIdx as idx (idx)} + {@const temp = hourly.apparent_temperature[idx]} + {@const date = dates[idx]} + {@const now = isCurrentHour(date, today)} + {@const bg = getColor(temp, String(units.temperature_unit))} + {@const fg = getTextColorForTemp(temp ?? 0, String(units.temperature_unit))} + + {/each} + + + + + + {#each dayIdx as idx (idx)} + {@const cloud = hourly.cloud_cover[idx]} + {@const date = dates[idx]} + {@const now = isCurrentHour(date, today)} + + {/each} + + + + + + {#each dayIdx as idx (idx)} + {@const precip = hourly.precipitation[idx]} + {@const prob = hourly.precipitation_probability[idx]} + {@const date = dates[idx]} + {@const now = isCurrentHour(date, today)} + + {/each} + + + + + + {#each dayIdx as idx (idx)} + {@const wind = hourly.windspeed_10m[idx]} + {@const windDir = hourly.winddirection_10m[idx]} + {@const date = dates[idx]} + {@const now = isCurrentHour(date, today)} + + {/each} + + + + + + {#each dayIdx as idx (idx)} + {@const hum = hourly.relative_humidity_2m[idx]} + {@const date = dates[idx]} + {@const now = isCurrentHour(date, today)} + + {/each} + + +
Hourly weather details for {locationName}
+ {pad(date.getHours())}00 +
+ + + + + + + +
+
+ + + + {tempUnit} +
+
+ {temp != null ? temp.toFixed(0) + '°' : '-'} +
+
+ Feels + {tempUnit} +
+
+ {temp != null ? temp.toFixed(0) + '°' : '-'} +
+
+ + + + % +
+
+ {cloud != null ? cloud.toFixed(0) : '-'} +
+
+ + + + {precipUnit} +
+
+
+ {#if precip > 0} +
+ {/if} + + {#if precip > 0} + {precip.toFixed(1)} + {:else if prob != null && prob > 0} + {prob}% + {/if} + +
+
+ + + + {windUnit} +
+
+ {#if windDir != null && !isNaN(windDir)} +
+ + + +
+ {/if} + {wind?.toFixed(0) ?? '-'} +
+
+ + + + % +
+
+ {hum != null ? hum.toFixed(0) : '-'} +
+
+{/if} + + diff --git a/src/routes/weather/week/[location]/MeteogramCharts.svelte b/src/routes/weather/week/[location]/MeteogramCharts.svelte new file mode 100644 index 0000000..0c87627 --- /dev/null +++ b/src/routes/weather/week/[location]/MeteogramCharts.svelte @@ -0,0 +1,595 @@ + + +
+ +
+ +{#if showCharts} +
+
+

+ {selectedDay.toLocaleDateString('en-GB', { weekday: 'long' })} + + {getDayLabel(selectedDay, today) !== + selectedDay.toLocaleDateString('en-GB', { weekday: 'long' }) + ? ` (${getDayLabel(selectedDay, today)})` + : ''} + +

+ +
+ + + {#each chartOptions as option, i (i)} + + {/each} + + +
+ +
+
+{/if} + + diff --git a/src/routes/weather/week/[location]/ModelSelector.svelte b/src/routes/weather/week/[location]/ModelSelector.svelte new file mode 100644 index 0000000..5ef3c89 --- /dev/null +++ b/src/routes/weather/week/[location]/ModelSelector.svelte @@ -0,0 +1,45 @@ + + +
+
+ { + if (val) onModelChange(val); + }} + > + + {modelLabel} + + + {#each models as mo (mo.value)} + {mo.label} + {/each} + + + +
+
diff --git a/src/routes/weather/week/[location]/SunInfo.svelte b/src/routes/weather/week/[location]/SunInfo.svelte new file mode 100644 index 0000000..f93a009 --- /dev/null +++ b/src/routes/weather/week/[location]/SunInfo.svelte @@ -0,0 +1,57 @@ + + +{#if sunrise && sunset} +
+
+ + + + {pad(sunrise.getHours())}:{pad(sunrise.getMinutes())} +
+
+ + + + {pad(sunset.getHours())}:{pad(sunset.getMinutes())} +
+
+{/if} + + diff --git a/src/routes/weather/week/[location]/types.ts b/src/routes/weather/week/[location]/types.ts new file mode 100644 index 0000000..e4f4396 --- /dev/null +++ b/src/routes/weather/week/[location]/types.ts @@ -0,0 +1,89 @@ +import type { MarkArea, WeekDailyData, WeekHourlyData } from '$lib/services/weather'; + +export interface WeatherUnits { + temperature_unit: string; + wind_speed_unit: string; + precipitation_unit: string; +} + +export interface FetchedHourly { + hourly: WeekHourlyData; + utc_offset_seconds: number; + timestamps: number[]; + hourlyDates: Date[]; + markAreas: MarkArea[]; +} + +export interface FetchedDaily { + daily: WeekDailyData; + dailyDates: Date[]; +} + +export function getTempUnit(units: WeatherUnits): string { + return units.temperature_unit === 'celsius' ? '°C' : '°F'; +} + +export function getWindUnit(units: WeatherUnits): string { + return units.wind_speed_unit === 'kmh' ? 'km/h' : units.wind_speed_unit; +} + +export function getPrecipUnit(units: WeatherUnits): string { + return units.precipitation_unit === 'mm' ? 'mm' : 'in'; +} + +export function getTextColorForTemp(temp: number, unit: string): string { + const threshold = unit === 'celsius' ? { low: -13, high: 40 } : { low: 7, high: 104 }; + return temp < threshold.low || temp >= threshold.high ? 'white' : 'black'; +} + +export function getWindArrowRotation(deg: number): string { + return `rotate(${deg}deg)`; +} + +export function getWindDirectionLabel(deg: number): string { + const dirs = [ + 'N', + 'NNE', + 'NE', + 'ENE', + 'E', + 'ESE', + 'SE', + 'SSE', + 'S', + 'SSW', + 'SW', + 'WSW', + 'W', + 'WNW', + 'NW', + 'NNW' + ]; + return dirs[Math.round(deg / 22.5) % 16]; +} + +export function getDayLabel(date: Date, today: Date): string { + const MS_PER_DAY = 24 * 3600 * 1000; + const diff = Math.round( + (new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime() - + new Date(today.getFullYear(), today.getMonth(), today.getDate()).getTime()) / + MS_PER_DAY + ); + if (diff === 0) return 'Today'; + if (diff === 1) return 'Tomorrow'; + if (diff === -1) return 'Yesterday'; + return `${date.getMonth() + 1}-${date.getDate()}`; +} + +export function isDaytimeHour(hour: number): boolean { + return hour >= 6 && hour < 21; +} + +export function isCurrentHour(date: Date, now: Date): boolean { + return ( + date.getDate() === now.getDate() && + date.getMonth() === now.getMonth() && + date.getFullYear() === now.getFullYear() && + date.getHours() === now.getHours() + ); +}