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
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
`PAQI` by selecting the maximum value for each hour, but it also yields the
maximum forecast item for air quality index in `AQI_max` and for
pollen in `pollen_max` seperately (out the items that `PAQI` combined):
If selected, it merges items from the AQI and pollen metric into `PAQI` by
selecting the maximum value for each hour:
``` json
{
"lat": 52.0905169,
"lon": 5.1109709,
"time": 1652189065,
"AQI_max": {
"time": 1652191200,
"value": 6.09
},
"PAQI": [
{
"time": 1652187600,
@ -160,11 +154,7 @@ pollen in `pollen_max` seperately (out the items that `PAQI` combined):
"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")]
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).
#[serde(rename = "NO2", skip_serializing_if = "Option::is_none")]
no2: Option<Vec<LuchtmeetnetItem>>,
@ -57,10 +53,6 @@ pub(crate) struct Forecast {
#[serde(skip_serializing_if = "Option::is_none")]
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).
#[serde(skip_serializing_if = "Option::is_none")]
precipitation: Option<Vec<BuienradarItem>>,
@ -144,13 +136,7 @@ pub(crate) async fn forecast(
Metric::NO2 => forecast.no2 = providers::luchtmeetnet::get(position, metric).await,
Metric::O3 => forecast.o3 = providers::luchtmeetnet::get(position, metric).await,
Metric::PAQI => {
if let Some((paqi, pollen_max, aqi_max)) =
providers::combined::get(position, metric, maps_handle).await
{
forecast.paqi = Some(paqi);
forecast.aqi_max = Some(aqi_max);
forecast.pollen_max = Some(pollen_max);
}
forecast.paqi = providers::combined::get(position, metric, maps_handle).await
}
Metric::PM10 => forecast.pm10 = providers::luchtmeetnet::get(position, metric).await,
Metric::Pollen => {

View File

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

View File

@ -35,18 +35,15 @@ impl Item {
/// 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
/// 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.
///
/// 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
/// lining them up fails. Returns [`None`] for the maximum pollen sample or maximum AQI item
/// if there are no samples or items.
/// lining them up fails.
fn merge(
pollen_samples: Vec<BuienradarSample>,
aqi_items: Vec<LuchtmeetnetItem>,
) -> Option<(Vec<Item>, BuienradarSample, LuchtmeetnetItem)> {
) -> Option<Vec<Item>> {
let mut pollen_samples = pollen_samples;
let mut aqi_items = aqi_items;
@ -80,23 +77,6 @@ fn merge(
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
// value.
let items = pollen_samples
@ -110,21 +90,16 @@ fn merge(
})
.collect();
Some((items, pollen_max, aqi_max))
Some(items)
}
/// 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:
/// * [`Metric::PAQI`]
///
/// 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
/// pollen sample or AQI item if there are no samples or items.
/// Luchtmeetnet provider fails or if they cannot be combined.
///
/// If the result is [`Some`], it will be cached for 30 minutes for the the given position and
/// metric.
@ -138,7 +113,7 @@ pub(crate) async fn get(
position: Position,
metric: Metric,
maps_handle: &MapsHandle,
) -> Option<(Vec<Item>, BuienradarSample, LuchtmeetnetItem)> {
) -> Option<Vec<Item>> {
if metric != Metric::PAQI {
return None;
};
@ -185,7 +160,7 @@ mod tests {
// 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();
let paqi = merged.unwrap();
assert_eq!(
paqi,
Vec::from([
@ -194,8 +169,6 @@ mod tests {
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..]
@ -208,10 +181,8 @@ mod tests {
.collect::<Vec<_>>();
let merged = super::merge(shifted_pollen_samples, aqi_items.clone());
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!(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..]
@ -224,25 +195,19 @@ mod tests {
.collect::<Vec<_>>();
let merged = super::merge(pollen_samples.clone(), shifted_aqi_items);
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!(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.
let merged = super::merge(pollen_samples[..3].to_vec(), aqi_items.clone());
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!(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());
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!(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.
let shifted_aqi_items = aqi_items