Compare commits
7 Commits
Author | SHA1 | Date |
---|---|---|
Paul van Tilburg | b61eb63cdf | |
Paul van Tilburg | e5e006dc00 | |
Paul van Tilburg | a0c4e0da77 | |
Paul van Tilburg | 34b63ec94d | |
Paul van Tilburg | 5a23e83b7f | |
Paul van Tilburg | e2d1a1d9df | |
Paul van Tilburg | 29b79c720d |
15
CHANGELOG.md
15
CHANGELOG.md
|
@ -7,7 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
## [0.2.0] - 2022-5-07
|
## [0.2.1] - 2002-05-08
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
* Add tests for the merge functionality of the combined provider (PAQI)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
* Filter out old item/samples in combined provider (PAQI)
|
||||||
|
|
||||||
|
## [0.2.0] - 2022-05-07
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
@ -18,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
|
|
||||||
Initial release.
|
Initial release.
|
||||||
|
|
||||||
[Unreleased]: https://git.luon.net/paul/sinoptik/compare/v0.2.0...HEAD
|
[Unreleased]: https://git.luon.net/paul/sinoptik/compare/v0.2.1...HEAD
|
||||||
|
[0.2.1]: https://git.luon.net/paul/sinoptik/compare/v0.2.0...v0.2.1
|
||||||
[0.2.0]: https://git.luon.net/paul/sinoptik/compare/v0.1.0...v0.2.0
|
[0.2.0]: https://git.luon.net/paul/sinoptik/compare/v0.1.0...v0.2.0
|
||||||
[0.1.0]: https://git.luon.net/paul/sinoptik/commits/tag/v0.1.0
|
[0.1.0]: https://git.luon.net/paul/sinoptik/commits/tag/v0.1.0
|
||||||
|
|
|
@ -2129,7 +2129,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sinoptik"
|
name = "sinoptik"
|
||||||
version = "0.2.0"
|
version = "0.2.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"assert_float_eq",
|
"assert_float_eq",
|
||||||
"assert_matches",
|
"assert_matches",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "sinoptik"
|
name = "sinoptik"
|
||||||
version = "0.2.0"
|
version = "0.2.1"
|
||||||
authors = [
|
authors = [
|
||||||
"Admar Schoonen <admar@luon.net",
|
"Admar Schoonen <admar@luon.net",
|
||||||
"Paul van Tilburg <paul@luon.net>"
|
"Paul van Tilburg <paul@luon.net>"
|
||||||
|
|
|
@ -148,8 +148,8 @@ pub(crate) async fn forecast(
|
||||||
providers::combined::get(position, metric, maps_handle).await
|
providers::combined::get(position, metric, maps_handle).await
|
||||||
{
|
{
|
||||||
forecast.paqi = Some(paqi);
|
forecast.paqi = Some(paqi);
|
||||||
forecast.aqi_max = aqi_max;
|
forecast.aqi_max = Some(aqi_max);
|
||||||
forecast.pollen_max = pollen_max;
|
forecast.pollen_max = Some(pollen_max);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Metric::PM10 => forecast.pm10 = providers::luchtmeetnet::get(position, metric).await,
|
Metric::PM10 => forecast.pm10 = providers::luchtmeetnet::get(position, metric).await,
|
||||||
|
|
|
@ -294,7 +294,7 @@ impl MapsRefresh for MapsHandle {
|
||||||
/// A Buienradar map sample.
|
/// A Buienradar map sample.
|
||||||
///
|
///
|
||||||
/// This represents a value at a given time.
|
/// This represents a value at a given time.
|
||||||
#[derive(Clone, Debug, Serialize)]
|
#[derive(Clone, Debug, PartialEq, Serialize)]
|
||||||
#[serde(crate = "rocket::serde")]
|
#[serde(crate = "rocket::serde")]
|
||||||
pub(crate) struct Sample {
|
pub(crate) struct Sample {
|
||||||
/// The time(stamp) of the forecast.
|
/// The time(stamp) of the forecast.
|
||||||
|
@ -308,6 +308,13 @@ pub(crate) struct Sample {
|
||||||
pub(crate) score: u8,
|
pub(crate) score: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Sample {
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) fn new(time: DateTime<Utc>, score: u8) -> Self {
|
||||||
|
Self { time, score }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Builds a scoring histogram for the map key.
|
/// Builds a scoring histogram for the map key.
|
||||||
fn map_key_histogram() -> MapKeyHistogram {
|
fn map_key_histogram() -> MapKeyHistogram {
|
||||||
MAP_KEY
|
MAP_KEY
|
||||||
|
|
|
@ -36,7 +36,7 @@ struct Row {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The Buienradar API precipitation data item.
|
/// The Buienradar API precipitation data item.
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||||
#[serde(crate = "rocket::serde", try_from = "Row")]
|
#[serde(crate = "rocket::serde", try_from = "Row")]
|
||||||
pub(crate) struct Item {
|
pub(crate) struct Item {
|
||||||
/// The time(stamp) of the forecast.
|
/// The time(stamp) of the forecast.
|
||||||
|
|
|
@ -14,7 +14,7 @@ use crate::position::Position;
|
||||||
use crate::Metric;
|
use crate::Metric;
|
||||||
|
|
||||||
/// The combined data item.
|
/// The combined data item.
|
||||||
#[derive(Clone, Debug, Serialize)]
|
#[derive(Clone, Debug, PartialEq, Serialize)]
|
||||||
#[serde(crate = "rocket::serde")]
|
#[serde(crate = "rocket::serde")]
|
||||||
pub(crate) struct Item {
|
pub(crate) struct Item {
|
||||||
/// The time(stamp) of the forecast.
|
/// The time(stamp) of the forecast.
|
||||||
|
@ -25,6 +25,13 @@ pub(crate) struct Item {
|
||||||
value: f32,
|
value: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Item {
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) fn new(time: DateTime<Utc>, value: f32) -> Self {
|
||||||
|
Self { time, value }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Merges pollen samples and AQI items into combined items.
|
/// Merges pollen samples and AQI items into combined items.
|
||||||
///
|
///
|
||||||
/// The merging drops items from either the pollen samples or from the AQI items if they are not
|
/// The merging drops items from either the pollen samples or from the AQI items if they are not
|
||||||
|
@ -39,14 +46,15 @@ pub(crate) struct Item {
|
||||||
fn merge(
|
fn merge(
|
||||||
pollen_samples: Vec<BuienradarSample>,
|
pollen_samples: Vec<BuienradarSample>,
|
||||||
aqi_items: Vec<LuchtmeetnetItem>,
|
aqi_items: Vec<LuchtmeetnetItem>,
|
||||||
) -> Option<(
|
) -> Option<(Vec<Item>, BuienradarSample, LuchtmeetnetItem)> {
|
||||||
Vec<Item>,
|
|
||||||
Option<BuienradarSample>,
|
|
||||||
Option<LuchtmeetnetItem>,
|
|
||||||
)> {
|
|
||||||
let mut pollen_samples = pollen_samples;
|
let mut pollen_samples = pollen_samples;
|
||||||
let mut aqi_items = aqi_items;
|
let mut aqi_items = aqi_items;
|
||||||
|
|
||||||
|
// Only retain samples/items that have timestamps that are at least half an hour ago.
|
||||||
|
let now = Utc::now();
|
||||||
|
pollen_samples.retain(|smp| smp.time.signed_duration_since(now).num_seconds() > -1800);
|
||||||
|
aqi_items.retain(|item| item.time.signed_duration_since(now).num_seconds() > -1800);
|
||||||
|
|
||||||
// Align the iterators based on the (hourly) timestamps!
|
// Align the iterators based on the (hourly) timestamps!
|
||||||
let pollen_first_time = pollen_samples.first()?.time;
|
let pollen_first_time = pollen_samples.first()?.time;
|
||||||
let aqi_first_time = aqi_items.first()?.time;
|
let aqi_first_time = aqi_items.first()?.time;
|
||||||
|
@ -73,14 +81,18 @@ fn merge(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the maximum sample/item of each series.
|
// Find the maximum sample/item of each series.
|
||||||
|
// Note: Unwrapping is possible because each series has at least an item otherwise `.first`
|
||||||
|
// would have failed above.
|
||||||
let pollen_max = pollen_samples
|
let pollen_max = pollen_samples
|
||||||
.iter()
|
.iter()
|
||||||
.max_by_key(|sample| sample.score)
|
.max_by_key(|sample| sample.score)
|
||||||
.cloned();
|
.cloned()
|
||||||
|
.unwrap();
|
||||||
let aqi_max = aqi_items
|
let aqi_max = aqi_items
|
||||||
.iter()
|
.iter()
|
||||||
.max_by_key(|item| (item.value * 1_000.0) as u32)
|
.max_by_key(|item| (item.value * 1_000.0) as u32)
|
||||||
.cloned();
|
.cloned()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
// Combine the samples with items by taking the maximum of pollen sample score and AQI item
|
// Combine the samples with items by taking the maximum of pollen sample score and AQI item
|
||||||
// value.
|
// value.
|
||||||
|
@ -123,11 +135,7 @@ pub(crate) async fn get(
|
||||||
position: Position,
|
position: Position,
|
||||||
metric: Metric,
|
metric: Metric,
|
||||||
maps_handle: &MapsHandle,
|
maps_handle: &MapsHandle,
|
||||||
) -> Option<(
|
) -> Option<(Vec<Item>, BuienradarSample, LuchtmeetnetItem)> {
|
||||||
Vec<Item>,
|
|
||||||
Option<BuienradarSample>,
|
|
||||||
Option<LuchtmeetnetItem>,
|
|
||||||
)> {
|
|
||||||
if metric != Metric::PAQI {
|
if metric != Metric::PAQI {
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
|
@ -136,3 +144,110 @@ pub(crate) async fn get(
|
||||||
|
|
||||||
merge(pollen_items?, aqi_items?)
|
merge(pollen_items?, aqi_items?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use chrono::{Duration, Timelike};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn merge() {
|
||||||
|
let t_now = Utc::now()
|
||||||
|
.with_second(0)
|
||||||
|
.unwrap()
|
||||||
|
.with_nanosecond(0)
|
||||||
|
.unwrap();
|
||||||
|
let t_m2 = t_now.checked_sub_signed(Duration::days(1)).unwrap();
|
||||||
|
let t_m1 = t_now.checked_sub_signed(Duration::hours(2)).unwrap();
|
||||||
|
let t_0 = t_now.checked_add_signed(Duration::minutes(12)).unwrap();
|
||||||
|
let t_1 = t_now.checked_add_signed(Duration::minutes(72)).unwrap();
|
||||||
|
let t_2 = t_now.checked_add_signed(Duration::minutes(132)).unwrap();
|
||||||
|
|
||||||
|
let pollen_samples = Vec::from([
|
||||||
|
BuienradarSample::new(t_m2, 4),
|
||||||
|
BuienradarSample::new(t_m1, 5),
|
||||||
|
BuienradarSample::new(t_0, 1),
|
||||||
|
BuienradarSample::new(t_1, 3),
|
||||||
|
BuienradarSample::new(t_2, 2),
|
||||||
|
]);
|
||||||
|
let aqi_items = Vec::from([
|
||||||
|
LuchtmeetnetItem::new(t_m2, 4.0),
|
||||||
|
LuchtmeetnetItem::new(t_m1, 5.0),
|
||||||
|
LuchtmeetnetItem::new(t_0, 1.1),
|
||||||
|
LuchtmeetnetItem::new(t_1, 2.9),
|
||||||
|
LuchtmeetnetItem::new(t_2, 2.4),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Perform a normal merge.
|
||||||
|
let merged = super::merge(pollen_samples.clone(), aqi_items.clone());
|
||||||
|
assert!(merged.is_some());
|
||||||
|
let (paqi, max_pollen, max_aqi) = merged.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
paqi,
|
||||||
|
Vec::from([
|
||||||
|
Item::new(t_0, 1.1),
|
||||||
|
Item::new(t_1, 3.0),
|
||||||
|
Item::new(t_2, 2.4),
|
||||||
|
])
|
||||||
|
);
|
||||||
|
assert_eq!(max_pollen, BuienradarSample::new(t_1, 3));
|
||||||
|
assert_eq!(max_aqi, LuchtmeetnetItem::new(t_1, 2.9));
|
||||||
|
|
||||||
|
// The pollen samples are shifted, i.e. one hour in the future.
|
||||||
|
let shifted_pollen_samples = pollen_samples[2..]
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.map(|mut item| {
|
||||||
|
item.time = item.time.checked_add_signed(Duration::hours(1)).unwrap();
|
||||||
|
item
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let merged = super::merge(shifted_pollen_samples, aqi_items.clone());
|
||||||
|
assert!(merged.is_some());
|
||||||
|
let (paqi, max_pollen, max_aqi) = merged.unwrap();
|
||||||
|
assert_eq!(paqi, Vec::from([Item::new(t_1, 2.9), Item::new(t_2, 3.0),]));
|
||||||
|
assert_eq!(max_pollen, BuienradarSample::new(t_2, 3));
|
||||||
|
assert_eq!(max_aqi, LuchtmeetnetItem::new(t_1, 2.9));
|
||||||
|
|
||||||
|
// The AQI items are shifted, i.e. one hour in the future.
|
||||||
|
let shifted_aqi_items = aqi_items[2..]
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.map(|mut item| {
|
||||||
|
item.time = item.time.checked_add_signed(Duration::hours(1)).unwrap();
|
||||||
|
item
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let merged = super::merge(pollen_samples.clone(), shifted_aqi_items);
|
||||||
|
assert!(merged.is_some());
|
||||||
|
let (paqi, max_pollen, max_aqi) = merged.unwrap();
|
||||||
|
assert_eq!(paqi, Vec::from([Item::new(t_1, 3.0), Item::new(t_2, 2.9),]));
|
||||||
|
assert_eq!(max_pollen, BuienradarSample::new(t_1, 3));
|
||||||
|
assert_eq!(max_aqi, LuchtmeetnetItem::new(t_2, 2.9));
|
||||||
|
|
||||||
|
// Merging fails because the samples/items are too far (6 hours) apart.
|
||||||
|
let shifted_aqi_items = aqi_items
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.map(|mut item| {
|
||||||
|
item.time = item.time.checked_add_signed(Duration::hours(6)).unwrap();
|
||||||
|
item
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let merged = super::merge(pollen_samples.clone(), shifted_aqi_items);
|
||||||
|
assert_eq!(merged, None);
|
||||||
|
|
||||||
|
// The pollen samples list is empty, or everything is too old.
|
||||||
|
let merged = super::merge(Vec::new(), aqi_items.clone());
|
||||||
|
assert_eq!(merged, None);
|
||||||
|
let merged = super::merge(pollen_samples[0..2].to_vec(), aqi_items.clone());
|
||||||
|
assert_eq!(merged, None);
|
||||||
|
|
||||||
|
// The AQI items list is empty, or everything is too old.
|
||||||
|
let merged = super::merge(pollen_samples.clone(), Vec::new());
|
||||||
|
assert_eq!(merged, None);
|
||||||
|
let merged = super::merge(pollen_samples, aqi_items[0..2].to_vec());
|
||||||
|
assert_eq!(merged, None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -24,7 +24,7 @@ struct Container {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The Luchtmeetnet API data item.
|
/// The Luchtmeetnet API data item.
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||||
#[serde(crate = "rocket::serde")]
|
#[serde(crate = "rocket::serde")]
|
||||||
pub(crate) struct Item {
|
pub(crate) struct Item {
|
||||||
/// The time(stamp) of the forecast.
|
/// The time(stamp) of the forecast.
|
||||||
|
@ -40,6 +40,13 @@ pub(crate) struct Item {
|
||||||
pub(crate) value: f32,
|
pub(crate) value: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Item {
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) fn new(time: DateTime<Utc>, value: f32) -> Self {
|
||||||
|
Self { time, value }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Retrieves the Luchtmeetnet forecasted items for the provided position and metric.
|
/// Retrieves the Luchtmeetnet forecasted items for the provided position and metric.
|
||||||
///
|
///
|
||||||
/// It supports the following metrics:
|
/// It supports the following metrics:
|
||||||
|
|
Loading…
Reference in New Issue