Yield pollen and AQI max for PAQI metric (closes: #20)
* Make the combined provider keep track of the AQI and pollen maximum value * Extend the `Forecast` struct with the `aqi_max` and `pollen_max` fields * Fill the `aqi_max` and `pollen_max` fields when the PAQI metric is selected * Update the documentation * Extend the tests
This commit is contained in:
parent
7feae97ee2
commit
5972697cf1
39
README.md
39
README.md
|
@ -105,8 +105,8 @@ position:
|
|||
|
||||
```json
|
||||
{
|
||||
"lat": 34.567890,
|
||||
"lon": 1.234567,
|
||||
"lat": 52.0905169,
|
||||
"lon": 5.1109709,
|
||||
"time": 1645800043,
|
||||
"UVI": [
|
||||
{
|
||||
|
@ -133,6 +133,41 @@ 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
|
||||
24-hour maximum forecast item for air quality index in `AQI_max` and for
|
||||
pollen in `pollen_max` seperately:
|
||||
|
||||
``` json
|
||||
{
|
||||
"lat": 52.0905169,
|
||||
"lon": 5.1109709,
|
||||
"time": 1645800043,
|
||||
"AQI_max": {
|
||||
"time": 1652022000,
|
||||
"value": 6.65
|
||||
},
|
||||
"PAQI": [
|
||||
{
|
||||
"time": 1651951457,
|
||||
"value": 6.04
|
||||
},
|
||||
{
|
||||
"time": 1651955057,
|
||||
"value": 6.04
|
||||
},
|
||||
...
|
||||
],
|
||||
"pollen_max": {
|
||||
"time": 1652034257,
|
||||
"value": 6
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Map API endpoint
|
||||
|
||||
The `/map` API endpoint basically only exists for debugging purposes. Given an
|
||||
|
|
|
@ -33,6 +33,10 @@ 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>>,
|
||||
|
@ -53,6 +57,10 @@ 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>>,
|
||||
|
@ -136,7 +144,13 @@ 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 => {
|
||||
forecast.paqi = providers::combined::get(position, metric, maps_handle).await
|
||||
if let Some((paqi, pollen_max, aqi_max)) =
|
||||
providers::combined::get(position, metric, maps_handle).await
|
||||
{
|
||||
forecast.paqi = Some(paqi);
|
||||
forecast.aqi_max = aqi_max;
|
||||
forecast.pollen_max = pollen_max;
|
||||
}
|
||||
}
|
||||
Metric::PM10 => forecast.pm10 = providers::luchtmeetnet::get(position, metric).await,
|
||||
Metric::Pollen => {
|
||||
|
|
|
@ -140,11 +140,13 @@ 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);
|
||||
|
||||
|
@ -158,11 +160,13 @@ 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(_)));
|
||||
}
|
||||
|
@ -180,11 +184,13 @@ 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);
|
||||
|
||||
|
@ -198,11 +204,13 @@ 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(_)));
|
||||
}
|
||||
|
|
|
@ -27,16 +27,23 @@ pub(crate) struct Item {
|
|||
|
||||
/// Merges pollen samples and AQI items into combined items.
|
||||
///
|
||||
/// This 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 stating series, thus lining them before they
|
||||
/// are combined.
|
||||
/// 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
|
||||
/// before they are combined.
|
||||
///
|
||||
/// Returns [`None`] if there are no pollen samples, if there are no AQI items, or if
|
||||
/// lining them up fails.
|
||||
/// 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.
|
||||
fn merge(
|
||||
pollen_samples: Vec<BuienradarSample>,
|
||||
aqi_items: Vec<LuchtmeetnetItem>,
|
||||
) -> Option<Vec<Item>> {
|
||||
) -> Option<(
|
||||
Vec<Item>,
|
||||
Option<BuienradarSample>,
|
||||
Option<LuchtmeetnetItem>,
|
||||
)> {
|
||||
let mut pollen_samples = pollen_samples;
|
||||
let mut aqi_items = aqi_items;
|
||||
|
||||
|
@ -65,6 +72,16 @@ fn merge(
|
|||
aqi_items.drain(..idx);
|
||||
}
|
||||
|
||||
// Find the maximum sample/item of each series.
|
||||
let pollen_max = pollen_samples
|
||||
.iter()
|
||||
.max_by_key(|sample| sample.score)
|
||||
.cloned();
|
||||
let aqi_max = aqi_items
|
||||
.iter()
|
||||
.max_by_key(|item| (item.value * 1_000.0) as u32)
|
||||
.cloned();
|
||||
|
||||
// Combine the samples with items by taking the maximum of pollen sample score and AQI item
|
||||
// value.
|
||||
let items = pollen_samples
|
||||
|
@ -78,18 +95,23 @@ fn merge(
|
|||
})
|
||||
.collect();
|
||||
|
||||
Some(items)
|
||||
Some((items, pollen_max, aqi_max))
|
||||
}
|
||||
|
||||
/// 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`] if retrieving data from either the Buienradar or the Luchtmeetnet provider
|
||||
/// fails or if they cannot be combined.
|
||||
/// 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.
|
||||
///
|
||||
/// 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.
|
||||
#[cached(
|
||||
time = 1800,
|
||||
|
@ -101,7 +123,11 @@ pub(crate) async fn get(
|
|||
position: Position,
|
||||
metric: Metric,
|
||||
maps_handle: &MapsHandle,
|
||||
) -> Option<Vec<Item>> {
|
||||
) -> Option<(
|
||||
Vec<Item>,
|
||||
Option<BuienradarSample>,
|
||||
Option<LuchtmeetnetItem>,
|
||||
)> {
|
||||
if metric != Metric::PAQI {
|
||||
return None;
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue