Skip to content

Commit 6d3a91b

Browse files
authored
Merge pull request #71 from thekid/feature/aggregate-weather
Aggregate weather for entries when importing
2 parents 3f68637 + a60779c commit 6d3a91b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+455
-11
lines changed

src/main/handlebars/feed.handlebars

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
{{/each}}
4040
</ul>
4141
{{count views '' '(Eine Ansicht)' '(# Ansichten)'}}
42+
{{> partials/weather in=.}}
4243
</div>
4344

4445
{{> partials/images in=. first=@first}}

src/main/handlebars/journey.handlebars

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ parent: feed
6464
{{/each}}
6565
</ul>
6666
{{count views '' '(Eine Ansicht)' '(# Ansichten)'}}
67+
{{> partials/weather in=.}}
6768
</div>
6869
{{> partials/images in=.}}
6970

src/main/handlebars/layout.handlebars

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,26 @@
442442
color: orange;
443443
}
444444
445+
.meta {
446+
margin-block: .25rem -.5rem;
447+
color: var(--meta-color);
448+
449+
a {
450+
color: var(--meta-color);
451+
}
452+
453+
.weather {
454+
display: flex;
455+
gap: .25rem;
456+
457+
img {
458+
width: 1lh;
459+
height: 1lh;
460+
filter: drop-shadow(.125rem .125rem .125rem rgb(0 0 0 / .5));
461+
}
462+
}
463+
}
464+
445465
.cards {
446466
margin-top: 1rem;
447467
}
@@ -460,10 +480,6 @@
460480
}
461481
}
462482
463-
.meta, .meta a {
464-
color: var(--meta-color);
465-
}
466-
467483
ul.locations {
468484
display: inline;
469485
padding-left: 0;

src/main/php/de/thekid/dialog/Helpers.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
*
88
* - range <from> <until>
99
* - range-rel <from> <until>
10+
* - temperature <min> <max>
1011
* - size-class <size>
1112
* - route <entry>
1213
* - dataset <meta-inf>
@@ -23,7 +24,7 @@ public function helpers() {
2324
yield 'range' => function($node, $context, $options) {
2425
$from= date($options['format'], strtotime($options[0]));
2526
$until= date($options['format'], strtotime($options[1]));
26-
return $from === $until ? $from : $from.' - '.$until;
27+
return $from === $until ? $from : $from.' '.$until;
2728
};
2829
yield 'range-rel' => function($node, $context, $options) {
2930
$from= strtotime($options[0]);
@@ -33,6 +34,13 @@ public function helpers() {
3334
if ($time > $until) return 'passed';
3435
return 'current';
3536
};
37+
yield 'temperature' => function($node, $context, $options) {
38+
$diff= abs($options[0] - $options[1]);
39+
return $diff <= ($options['tolerance'] ?? 1)
40+
? sprintf('%.1f', ($options[0] + $options[1]) / 2)
41+
: $options[0].''.$options[1]
42+
;
43+
};
3644
yield 'size-class' => function($node, $context, $options) {
3745
$s= (int)$options[0];
3846
return match {
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php namespace de\thekid\dialog;
2+
3+
use lang\IllegalArgumentException;
4+
use util\{Date, TimeZone, URI};
5+
use webservices\rest\Endpoint;
6+
7+
/**
8+
* Open-Meteo is an open-source weather API and offers free access for non-commercial use.
9+
*
10+
* @see https://github.com/open-meteo/open-meteo
11+
*/
12+
class OpenMeteo {
13+
private $base;
14+
private $auth= [];
15+
private $endpoints= [];
16+
17+
public function __construct(string|URI $base) {
18+
$this->base= $base instanceof URI ? $base : new URI($base);
19+
}
20+
21+
/** Returns a given API endpoint */
22+
protected function endpoint(string $kind): Endpoint {
23+
return $this->endpoints[$kind]??= new Endpoint($this->base->using()
24+
->host($kind.'.'.$this->base->host())
25+
->create()
26+
);
27+
}
28+
29+
public function lookup(string|float $lat, string|float $lon, Date $start, ?Date $end= null, ?TimeZone $tz= null): array<string, mixed> {
30+
$params= $this->auth + [
31+
'latitude' => $lat,
32+
'longitude' => $lon,
33+
'start_date' => $start->toString('Y-m-d'),
34+
'end_date' => ($end ?? $start)->toString('Y-m-d'),
35+
'timezone' => ($tz ?? $start->getTimeZone())->name(),
36+
'daily' => ['sunrise', 'sunset'],
37+
'hourly' => ['weather_code', 'apparent_temperature'],
38+
];
39+
return $this->endpoint('archive-api')->resource('archive')->get($params)->match([
40+
200 => fn($r) => $r->value(),
41+
400 => fn($r) => throw new IllegalArgumentException($r->content()),
42+
]);
43+
}
44+
}

src/main/php/de/thekid/dialog/import/Content.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
11
<?php namespace de\thekid\dialog\import;
22

33
use de\thekid\dialog\processing\Files;
4+
use util\Date;
45

56
/** Imports contents */
67
class Content extends Source {
78

89
public function entryFrom(Description $description): array<string, mixed> {
10+
$date= $description->meta['date'];
911
return [
1012
'slug' => $this->name(),
1113
'parent' => $this->parent(),
1214
'date' => $description->meta['date'],
1315
'title' => $description->meta['title'],
1416
'keywords' => $description->meta['keywords'] ?? [],
1517
'content' => $description->content,
16-
'locations' => [...$description->locations($description->meta['date']->getTimeZone())],
18+
'locations' => [...$description->locations(($date instanceof Date ? $date : new Date($date))->getTimeZone())],
1719
'is' => ['content' => true],
1820
];
1921
}

src/main/php/de/thekid/dialog/import/Cover.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
11
<?php namespace de\thekid\dialog\import;
22

33
use de\thekid\dialog\processing\Files;
4+
use util\Date;
45

56
/** Imports the cover image */
67
class Cover extends Source {
78

89
public function entryFrom(Description $description): array<string, mixed> {
10+
$date= $description->meta['date'];
911
return [
1012
'slug' => '@cover',
1113
'parent' => '~',
1214
'date' => $description->meta['date'],
1315
'title' => $description->meta['title'],
1416
'keywords' => $description->meta['keywords'] ?? [],
1517
'content' => $description->content,
16-
'locations' => [...$description->locations($description->meta['date']->getTimeZone())],
18+
'locations' => [...$description->locations(($date instanceof Date ? $date : new Date($date)->getTimeZone())],
1719
'is' => ['cover' => true],
1820
];
1921
}

src/main/php/de/thekid/dialog/import/Journey.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
use de\thekid\dialog\processing\Files;
44
use io\File;
5+
use util\Date;
56

67
/** Imports journeys */
78
class Journey extends Source {
@@ -32,13 +33,14 @@ private function childrenIn(Files $files): iterable {
3233
}
3334

3435
public function entryFrom(Description $description): array<string, mixed> {
36+
$date= $description->meta['from'];
3537
return [
3638
'slug' => $this->name(),
3739
'date' => $description->meta['from'],
3840
'title' => $description->meta['title'],
3941
'keywords' => $description->meta['keywords'] ?? [],
4042
'content' => $description->content,
41-
'locations' => [...$description->locations($description->meta['from']->getTimeZone())],
43+
'locations' => [...$description->locations(($date instanceof Date ? $date : new Date($date))->getTimeZone())],
4244
'is' => [
4345
'journey' => true,
4446
'from' => $description->meta['from'],
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?php namespace de\thekid\dialog\import;
2+
3+
use de\thekid\dialog\OpenMeteo;
4+
use lang\FormatException;
5+
use util\{Date, Dates, TimeZone, TimeInterval};
6+
use webservices\rest\Endpoint;
7+
8+
/** Aggregates weather for entries using OpenMeteo */
9+
class LookupWeather extends Task {
10+
private $weather= new OpenMeteo('https://open-meteo.com/v1');
11+
12+
public function __construct(private array<string, mixed> $entry, private array<mixed> $images) { }
13+
14+
public function execute(Endpoint $api) {
15+
$weather= [];
16+
$min= $max= null;
17+
foreach ($this->entry['locations'] as $location) {
18+
$tz= new TimeZone($location['timezone']);
19+
20+
// Infer date range from first and last images
21+
$dates= [];
22+
foreach ($this->images as $image) {
23+
$dates[]= new Date(strtr($image['meta']['dateTime'], ['+00:00' => '']), $tz);
24+
}
25+
usort($dates, fn($a, $b) => $b->compareTo($a));
26+
$first= current($dates);
27+
$last= end($dates);
28+
29+
// Filter hourly weather for the duration of the images
30+
$result= $this->weather->lookup($location['lat'], $location['lon'], $first, $last, $tz);
31+
$start= array_search(Dates::truncate($first, TimeInterval::$HOURS)->toString('Y-m-d\TH:i'), $result['hourly']['time']);
32+
$end= array_search(Dates::truncate($last, TimeInterval::$HOURS)->toString('Y-m-d\TH:i'), $result['hourly']['time']);
33+
34+
// Determine most common weather codes and temperature range
35+
$codes= array_count_values(array_slice($result['hourly']['weather_code'], $start, 1 + ($end - $start)));
36+
$temp= array_slice($result['hourly']['apparent_temperature'], $start, 1 + ($end - $start));
37+
$min= null === $min ? min($temp) : min($min, min($temp));
38+
$max= null === $max ? max($temp) : max($max, max($temp));
39+
40+
arsort($codes);
41+
foreach ($codes as $code => $count) {
42+
$weather[$code]??= 0;
43+
$weather[$code]+= $count;
44+
}
45+
46+
yield $location['name'] => sprintf('#%02d @ %.1f-%.1f °C', key($codes), min($temp), max($temp));
47+
}
48+
49+
arsort($weather);
50+
return [
51+
'code' => sprintf('%02d', key($weather)),
52+
'min' => $min,
53+
'max' => $max,
54+
];
55+
}
56+
57+
public function description(): string { return 'Looking up weather'; }
58+
}

src/main/php/de/thekid/dialog/import/Source.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ public function synchronize(Files $files) {
7676

7777
if (isset($updated)) {
7878
$changes['locations']= yield new LookupLocationInfos($changes);
79+
$changes['weather']= yield new LookupWeather($changes, $this->entry['images'] ?? []);
7980
$changes['published']= time();
8081
yield new PublishEntry($this->entry['slug'], $changes);
8182
}

0 commit comments

Comments
 (0)