Tik op een dag in de verwachtingsstrook voor een uuroverzicht van
temperatuur, regen en wind (richting + snelheid), om de 2 uur, 12
punten per dag. Sluit na 20 seconden vanzelf of bij een tik op de
overlay zelf.
Backend: Open-Meteo levert hourly-data in dezelfde call als daily
(geen extra verzoek, dezelfde 60min-cache). Nieuwe HourlyForecastMapper
groepeert per kalenderdatum en bemonstert om het andere uur, robuust
voor de zomertijddag (23/25 uur). OpenMeteoClient::dailyForecast()
hernoemd naar forecast(), geeft nu {daily, hourly} terug. Elke dag in
de forecast-array krijgt een hourly-lijst.
Frontend: temperatuurlijn hergebruikt sparklinePath (nu ook als
sparklinePoints voor exacte uitlijning met de 12-koloms grid), regen
als kleine staafjes, windpijl (nieuw icoon) wijst waarheen de wind
waait. 20s-balk via CSS transition met reflow-restart. Servicewerker
naar v5.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
75 lines
2.5 KiB
PHP
75 lines
2.5 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Weerscherm;
|
|
|
|
/**
|
|
* Zet het "hourly"-blok van Open-Meteo om naar uurpunten per kalenderdatum, om het andere uur.
|
|
*
|
|
* Groepeert eerst chronologisch per datum in de gegeven tijdzone, en neemt daarna om het andere
|
|
* punt (index 0, 2, 4, …) binnen die dag. Dat is robuust voor de zomertijddag: een dag met 23 of 25
|
|
* uren levert vanzelf iets minder of meer dan 12 punten op, zonder speciale wissel-logica.
|
|
*/
|
|
final class HourlyForecastMapper
|
|
{
|
|
private const STEP = 2;
|
|
|
|
/**
|
|
* @param array<string, mixed> $hourly
|
|
* @return array<string, list<array{at: string, temp: ?float, precip_mm: ?float, wind_kmh: ?float, wind_dir: ?int}>>
|
|
* sleutel is de kalenderdatum (Y-m-d) in $tz
|
|
*/
|
|
public static function mapByDate(array $hourly, \DateTimeZone $tz): array
|
|
{
|
|
$times = is_array($hourly['time'] ?? null) ? $hourly['time'] : [];
|
|
|
|
$perDate = [];
|
|
foreach ($times as $i => $iso) {
|
|
if (!is_string($iso)) {
|
|
continue;
|
|
}
|
|
$at = self::parse($iso, $tz);
|
|
if ($at === null) {
|
|
continue;
|
|
}
|
|
$perDate[$at->format('Y-m-d')][] = [
|
|
'at' => $at->format(\DATE_ATOM),
|
|
'temp' => self::floatOrNull($hourly['temperature_2m'][$i] ?? null),
|
|
'precip_mm' => self::floatOrNull($hourly['precipitation'][$i] ?? null),
|
|
'wind_kmh' => self::floatOrNull($hourly['wind_speed_10m'][$i] ?? null),
|
|
'wind_dir' => self::intOrNull($hourly['wind_direction_10m'][$i] ?? null),
|
|
];
|
|
}
|
|
|
|
$out = [];
|
|
foreach ($perDate as $date => $points) {
|
|
$sampled = [];
|
|
foreach (array_values($points) as $index => $point) {
|
|
if ($index % self::STEP === 0) {
|
|
$sampled[] = $point;
|
|
}
|
|
}
|
|
$out[$date] = $sampled;
|
|
}
|
|
return $out;
|
|
}
|
|
|
|
/** Open-Meteo levert lokale tijd zonder offset, bv. "2026-09-14T00:00". */
|
|
private static function parse(string $iso, \DateTimeZone $tz): ?\DateTimeImmutable
|
|
{
|
|
$dt = \DateTimeImmutable::createFromFormat('Y-m-d\TH:i', $iso, $tz);
|
|
return $dt === false ? null : $dt;
|
|
}
|
|
|
|
private static function floatOrNull(mixed $v): ?float
|
|
{
|
|
return is_int($v) || is_float($v) ? (float) $v : null;
|
|
}
|
|
|
|
private static function intOrNull(mixed $v): ?int
|
|
{
|
|
return is_int($v) || is_float($v) ? (int) round($v) : null;
|
|
}
|
|
}
|