Drop pollen and AQI max for PAQI metric

* This was introduced as per #20 but no longer deemed necessary
* Fix up some comments
* Keep the PAQI documentation in `README.md`
This commit is contained in:
Paul van Tilburg 2022-06-05 21:47:12 +02:00
parent fb8236696d
commit 7d0cd4a822
Signed by: paul
GPG Key ID: C6DE073EDA9EEC4D
4 changed files with 15 additions and 82 deletions

View File

@ -136,20 +136,14 @@ position:
#### Combined metric PAQI #### Combined metric PAQI
The PAQI (pollen/air quality index) metric is a special combined metric. The PAQI (pollen/air quality index) metric is a special combined metric.
If selected, it not only merges items from the AQI and pollen metric into If selected, it merges items from the AQI and pollen metric into `PAQI` by
`PAQI` by selecting the maximum value for each hour, but it also yields the selecting the maximum value for each hour:
maximum forecast item for air quality index in `AQI_max` and for
pollen in `pollen_max` seperately (out the items that `PAQI` combined):
``` json ``` json
{ {
"lat": 52.0905169, "lat": 52.0905169,
"lon": 5.1109709, "lon": 5.1109709,
"time": 1652189065, "time": 1652189065,
"AQI_max": {
"time": 1652191200,
"value": 6.09
},
"PAQI": [ "PAQI": [
{ {
"time": 1652187600, "time": 1652187600,
@ -160,11 +154,7 @@ pollen in `pollen_max` seperately (out the items that `PAQI` combined):
"value": 6.09 "value": 6.09
}, },
... ...
], ]
"pollen_max": {
"time": 1652209200,
"value": 6
}
} }
``` ```

View File

@ -33,10 +33,6 @@ pub(crate) struct Forecast {
#[serde(rename = "AQI", skip_serializing_if = "Option::is_none")] #[serde(rename = "AQI", skip_serializing_if = "Option::is_none")]
aqi: Option<Vec<LuchtmeetnetItem>>, aqi: Option<Vec<LuchtmeetnetItem>>,
/// The maximum air quality index value (when asked for PAQI).
#[serde(rename = "AQI_max", skip_serializing_if = "Option::is_none")]
aqi_max: Option<LuchtmeetnetItem>,
/// The NO₂ concentration (when asked for). /// The NO₂ concentration (when asked for).
#[serde(rename = "NO2", skip_serializing_if = "Option::is_none")] #[serde(rename = "NO2", skip_serializing_if = "Option::is_none")]
no2: Option<Vec<LuchtmeetnetItem>>, no2: Option<Vec<LuchtmeetnetItem>>,
@ -57,10 +53,6 @@ pub(crate) struct Forecast {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pollen: Option<Vec<BuienradarSample>>, pollen: Option<Vec<BuienradarSample>>,
/// The maximum pollen in the air (when asked for PAQI).
#[serde(skip_serializing_if = "Option::is_none")]
pollen_max: Option<BuienradarSample>,
/// The precipitation (when asked for). /// The precipitation (when asked for).
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
precipitation: Option<Vec<BuienradarItem>>, precipitation: Option<Vec<BuienradarItem>>,
@ -144,13 +136,7 @@ pub(crate) async fn forecast(
Metric::NO2 => forecast.no2 = providers::luchtmeetnet::get(position, metric).await, Metric::NO2 => forecast.no2 = providers::luchtmeetnet::get(position, metric).await,
Metric::O3 => forecast.o3 = providers::luchtmeetnet::get(position, metric).await, Metric::O3 => forecast.o3 = providers::luchtmeetnet::get(position, metric).await,
Metric::PAQI => { Metric::PAQI => {
if let Some((paqi, pollen_max, aqi_max)) = forecast.paqi = providers::combined::get(position, metric, maps_handle).await
providers::combined::get(position, metric, maps_handle).await
{
forecast.paqi = Some(paqi);
forecast.aqi_max = Some(aqi_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,
Metric::Pollen => { Metric::Pollen => {

View File

@ -154,13 +154,11 @@ mod tests {
assert_f64_near!(json["lon"].as_f64().unwrap(), 5.478633); assert_f64_near!(json["lon"].as_f64().unwrap(), 5.478633);
assert_matches!(json["time"], JsonValue::Number(_)); assert_matches!(json["time"], JsonValue::Number(_));
assert_matches!(json.get("AQI"), None); assert_matches!(json.get("AQI"), None);
assert_matches!(json.get("AQI_max"), None);
assert_matches!(json.get("NO2"), None); assert_matches!(json.get("NO2"), None);
assert_matches!(json.get("O3"), None); assert_matches!(json.get("O3"), None);
assert_matches!(json.get("PAQI"), None); assert_matches!(json.get("PAQI"), None);
assert_matches!(json.get("PM10"), None); assert_matches!(json.get("PM10"), None);
assert_matches!(json.get("pollen"), None); assert_matches!(json.get("pollen"), None);
assert_matches!(json.get("pollen_max"), None);
assert_matches!(json.get("precipitation"), None); assert_matches!(json.get("precipitation"), None);
assert_matches!(json.get("UVI"), None); assert_matches!(json.get("UVI"), None);
@ -174,13 +172,11 @@ mod tests {
assert_f64_near!(json["lon"].as_f64().unwrap(), 5.478633); assert_f64_near!(json["lon"].as_f64().unwrap(), 5.478633);
assert_matches!(json["time"], JsonValue::Number(_)); assert_matches!(json["time"], JsonValue::Number(_));
assert_matches!(json.get("AQI"), Some(JsonValue::Array(_))); assert_matches!(json.get("AQI"), Some(JsonValue::Array(_)));
assert_matches!(json.get("AQI_max"), Some(JsonValue::Object(_)));
assert_matches!(json.get("NO2"), Some(JsonValue::Array(_))); assert_matches!(json.get("NO2"), Some(JsonValue::Array(_)));
assert_matches!(json.get("O3"), Some(JsonValue::Array(_))); assert_matches!(json.get("O3"), Some(JsonValue::Array(_)));
assert_matches!(json.get("PAQI"), Some(JsonValue::Array(_))); assert_matches!(json.get("PAQI"), Some(JsonValue::Array(_)));
assert_matches!(json.get("PM10"), Some(JsonValue::Array(_))); assert_matches!(json.get("PM10"), Some(JsonValue::Array(_)));
assert_matches!(json.get("pollen"), Some(JsonValue::Array(_))); assert_matches!(json.get("pollen"), Some(JsonValue::Array(_)));
assert_matches!(json.get("pollen_max"), Some(JsonValue::Object(_)));
assert_matches!(json.get("precipitation"), Some(JsonValue::Array(_))); assert_matches!(json.get("precipitation"), Some(JsonValue::Array(_)));
assert_matches!(json.get("UVI"), Some(JsonValue::Array(_))); assert_matches!(json.get("UVI"), Some(JsonValue::Array(_)));
} }
@ -198,13 +194,11 @@ mod tests {
assert_f64_near!(json["lon"].as_f64().unwrap(), 5.5); assert_f64_near!(json["lon"].as_f64().unwrap(), 5.5);
assert_matches!(json["time"], JsonValue::Number(_)); assert_matches!(json["time"], JsonValue::Number(_));
assert_matches!(json.get("AQI"), None); assert_matches!(json.get("AQI"), None);
assert_matches!(json.get("AQI_max"), None);
assert_matches!(json.get("NO2"), None); assert_matches!(json.get("NO2"), None);
assert_matches!(json.get("O3"), None); assert_matches!(json.get("O3"), None);
assert_matches!(json.get("PAQI"), None); assert_matches!(json.get("PAQI"), None);
assert_matches!(json.get("PM10"), None); assert_matches!(json.get("PM10"), None);
assert_matches!(json.get("pollen"), None); assert_matches!(json.get("pollen"), None);
assert_matches!(json.get("pollen_max"), None);
assert_matches!(json.get("precipitation"), None); assert_matches!(json.get("precipitation"), None);
assert_matches!(json.get("UVI"), None); assert_matches!(json.get("UVI"), None);
@ -218,13 +212,11 @@ mod tests {
assert_f64_near!(json["lon"].as_f64().unwrap(), 5.5); assert_f64_near!(json["lon"].as_f64().unwrap(), 5.5);
assert_matches!(json["time"], JsonValue::Number(_)); assert_matches!(json["time"], JsonValue::Number(_));
assert_matches!(json.get("AQI"), Some(JsonValue::Array(_))); assert_matches!(json.get("AQI"), Some(JsonValue::Array(_)));
assert_matches!(json.get("AQI_max"), Some(JsonValue::Object(_)));
assert_matches!(json.get("NO2"), Some(JsonValue::Array(_))); assert_matches!(json.get("NO2"), Some(JsonValue::Array(_)));
assert_matches!(json.get("O3"), Some(JsonValue::Array(_))); assert_matches!(json.get("O3"), Some(JsonValue::Array(_)));
assert_matches!(json.get("PAQI"), Some(JsonValue::Array(_))); assert_matches!(json.get("PAQI"), Some(JsonValue::Array(_)));
assert_matches!(json.get("PM10"), Some(JsonValue::Array(_))); assert_matches!(json.get("PM10"), Some(JsonValue::Array(_)));
assert_matches!(json.get("pollen"), Some(JsonValue::Array(_))); assert_matches!(json.get("pollen"), Some(JsonValue::Array(_)));
assert_matches!(json.get("pollen_max"), Some(JsonValue::Object(_)));
assert_matches!(json.get("precipitation"), Some(JsonValue::Array(_))); assert_matches!(json.get("precipitation"), Some(JsonValue::Array(_)));
assert_matches!(json.get("UVI"), Some(JsonValue::Array(_))); assert_matches!(json.get("UVI"), Some(JsonValue::Array(_)));
} }

View File

@ -35,18 +35,15 @@ impl Item {
/// 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
/// stamped with half an hour of the first item of the latest starting series, thus lining them /// stamped within an hour of the first item of the latest starting series, thus lining them
/// before they are combined. /// before they are combined.
/// ///
/// This function also finds the maximum pollen sample and AQI item.
///
/// Returns [`None`] if there are no pollen samples, if there are no AQI items, or if /// Returns [`None`] if there are no pollen samples, if there are no AQI items, or if
/// lining them up fails. Returns [`None`] for the maximum pollen sample or maximum AQI item /// lining them up fails.
/// if there are no samples or items.
fn merge( fn merge(
pollen_samples: Vec<BuienradarSample>, pollen_samples: Vec<BuienradarSample>,
aqi_items: Vec<LuchtmeetnetItem>, aqi_items: Vec<LuchtmeetnetItem>,
) -> Option<(Vec<Item>, BuienradarSample, LuchtmeetnetItem)> { ) -> Option<Vec<Item>> {
let mut pollen_samples = pollen_samples; let mut pollen_samples = pollen_samples;
let mut aqi_items = aqi_items; let mut aqi_items = aqi_items;
@ -80,23 +77,6 @@ fn merge(
aqi_items.drain(..idx); aqi_items.drain(..idx);
} }
// Find the maximum sample/item of each series.
// Note 1: Unwrapping is possible because each series has at least an item otherwise `.first`
// would have failed above.
// Note 2: Ensure that the maximum sample/item is in scope of the time range covered by the
// combined items.
let zip_len = std::cmp::min(pollen_samples.len(), aqi_items.len());
let pollen_max = pollen_samples[..zip_len]
.iter()
.max_by_key(|sample| sample.score)
.cloned()
.unwrap();
let aqi_max = aqi_items[..zip_len]
.iter()
.max_by_key(|item| (item.value * 1_000.0) as u32)
.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.
let items = pollen_samples let items = pollen_samples
@ -110,21 +90,16 @@ fn merge(
}) })
.collect(); .collect();
Some((items, pollen_max, aqi_max)) Some(items)
} }
/// Retrieves the combined forecasted items for the provided position and metric. /// Retrieves the combined forecasted items for the provided position and metric.
/// ///
/// Besides the combined items, it also yields the maxium pollen sample and AQI item.
/// Note that the maximum values are calculated before combining them, so the time stamp
/// corresponds to the one in the original series, not to a timestamp of an item after merging.
///
/// It supports the following metric: /// It supports the following metric:
/// * [`Metric::PAQI`] /// * [`Metric::PAQI`]
/// ///
/// Returns [`None`] for the combined items if retrieving data from either the Buienradar or the /// Returns [`None`] for the combined items if retrieving data from either the Buienradar or the
/// Luchtmeetnet provider fails or if they cannot be combined. Returns [`None`] for the maxiumum /// Luchtmeetnet provider fails or if they cannot be combined.
/// pollen sample or AQI item if there are no samples or items.
/// ///
/// If the result is [`Some`], it will be cached for 30 minutes for the the given position and /// If the result is [`Some`], it will be cached for 30 minutes for the the given position and
/// metric. /// metric.
@ -138,7 +113,7 @@ pub(crate) async fn get(
position: Position, position: Position,
metric: Metric, metric: Metric,
maps_handle: &MapsHandle, maps_handle: &MapsHandle,
) -> Option<(Vec<Item>, BuienradarSample, LuchtmeetnetItem)> { ) -> Option<Vec<Item>> {
if metric != Metric::PAQI { if metric != Metric::PAQI {
return None; return None;
}; };
@ -185,7 +160,7 @@ mod tests {
// Perform a normal merge. // Perform a normal merge.
let merged = super::merge(pollen_samples.clone(), aqi_items.clone()); let merged = super::merge(pollen_samples.clone(), aqi_items.clone());
assert!(merged.is_some()); assert!(merged.is_some());
let (paqi, max_pollen, max_aqi) = merged.unwrap(); let paqi = merged.unwrap();
assert_eq!( assert_eq!(
paqi, paqi,
Vec::from([ Vec::from([
@ -194,8 +169,6 @@ mod tests {
Item::new(t_2, 2.4), 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. // The pollen samples are shifted, i.e. one hour in the future.
let shifted_pollen_samples = pollen_samples[2..] let shifted_pollen_samples = pollen_samples[2..]
@ -208,10 +181,8 @@ mod tests {
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let merged = super::merge(shifted_pollen_samples, aqi_items.clone()); let merged = super::merge(shifted_pollen_samples, aqi_items.clone());
assert!(merged.is_some()); assert!(merged.is_some());
let (paqi, max_pollen, max_aqi) = merged.unwrap(); let paqi = merged.unwrap();
assert_eq!(paqi, Vec::from([Item::new(t_1, 2.9), Item::new(t_2, 3.0)])); 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. // The AQI items are shifted, i.e. one hour in the future.
let shifted_aqi_items = aqi_items[2..] let shifted_aqi_items = aqi_items[2..]
@ -224,25 +195,19 @@ mod tests {
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let merged = super::merge(pollen_samples.clone(), shifted_aqi_items); let merged = super::merge(pollen_samples.clone(), shifted_aqi_items);
assert!(merged.is_some()); assert!(merged.is_some());
let (paqi, max_pollen, max_aqi) = merged.unwrap(); let paqi = merged.unwrap();
assert_eq!(paqi, Vec::from([Item::new(t_1, 3.0), Item::new(t_2, 2.9)])); 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));
// The maximum sample/item should not be later then the interval the PAQI items cover. // The maximum sample/item should not be later then the interval the PAQI items cover.
let merged = super::merge(pollen_samples[..3].to_vec(), aqi_items.clone()); let merged = super::merge(pollen_samples[..3].to_vec(), aqi_items.clone());
assert!(merged.is_some()); assert!(merged.is_some());
let (paqi, max_pollen, max_aqi) = merged.unwrap(); let paqi = merged.unwrap();
assert_eq!(paqi, Vec::from([Item::new(t_0, 1.1)])); assert_eq!(paqi, Vec::from([Item::new(t_0, 1.1)]));
assert_eq!(max_pollen, BuienradarSample::new(t_0, 1));
assert_eq!(max_aqi, LuchtmeetnetItem::new(t_0, 1.1));
let merged = super::merge(pollen_samples.clone(), aqi_items[..3].to_vec()); let merged = super::merge(pollen_samples.clone(), aqi_items[..3].to_vec());
assert!(merged.is_some()); assert!(merged.is_some());
let (paqi, max_pollen, max_aqi) = merged.unwrap(); let paqi = merged.unwrap();
assert_eq!(paqi, Vec::from([Item::new(t_0, 1.1)])); assert_eq!(paqi, Vec::from([Item::new(t_0, 1.1)]));
assert_eq!(max_pollen, BuienradarSample::new(t_0, 1));
assert_eq!(max_aqi, LuchtmeetnetItem::new(t_0, 1.1));
// Merging fails because the samples/items are too far (6 hours) apart. // Merging fails because the samples/items are too far (6 hours) apart.
let shifted_aqi_items = aqi_items let shifted_aqi_items = aqi_items