forked from paul/sinoptik
Introduce the Position struct; add position module
* Use `Position` everywhere instead of latitude/longitude float values * Implement `Partial`, `Eq` and `Hash` for `Position` so it can part of a cache key * Drop the `cache_key` helper function * Rename the `address_position` function to `resolve_address` * Add methods on `Position` for formatting latitude/longitude with a given precision (used for URL parameters in providers)
This commit is contained in:
parent
b2f63db6b4
commit
3a48f234e9
|
@ -6,6 +6,7 @@
|
|||
use rocket::serde::Serialize;
|
||||
|
||||
use crate::maps::MapsHandle;
|
||||
use crate::position::Position;
|
||||
use crate::providers;
|
||||
use crate::providers::buienradar::Item as BuienradarItem;
|
||||
use crate::providers::luchtmeetnet::Item as LuchtmeetnetItem;
|
||||
|
@ -112,12 +113,11 @@ impl Metric {
|
|||
///
|
||||
/// The provided list `metrics` determines what will be included in the forecast.
|
||||
pub(crate) async fn forecast(
|
||||
lat: f64,
|
||||
lon: f64,
|
||||
position: Position,
|
||||
metrics: Vec<Metric>,
|
||||
_maps_handle: &MapsHandle,
|
||||
) -> Forecast {
|
||||
let mut forecast = Forecast::new(lat, lon);
|
||||
let mut forecast = Forecast::new(position);
|
||||
|
||||
// Expand the `All` metric if present, deduplicate otherwise.
|
||||
let mut metrics = metrics;
|
||||
|
@ -131,14 +131,14 @@ pub(crate) async fn forecast(
|
|||
match metric {
|
||||
// This should have been expanded to all the metrics matched below.
|
||||
Metric::All => unreachable!("The all metric should have been expanded"),
|
||||
Metric::AQI => forecast.aqi = providers::luchtmeetnet::get(lat, lon, metric).await,
|
||||
Metric::NO2 => forecast.no2 = providers::luchtmeetnet::get(lat, lon, metric).await,
|
||||
Metric::O3 => forecast.o3 = providers::luchtmeetnet::get(lat, lon, metric).await,
|
||||
Metric::AQI => forecast.aqi = 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::PAQI => forecast.paqi = Some(()),
|
||||
Metric::PM10 => forecast.pm10 = providers::luchtmeetnet::get(lat, lon, metric).await,
|
||||
Metric::PM10 => forecast.pm10 = providers::luchtmeetnet::get(position, metric).await,
|
||||
Metric::Pollen => forecast.pollen = Some(()),
|
||||
Metric::Precipitation => {
|
||||
forecast.precipitation = providers::buienradar::get(lat, lon, metric).await
|
||||
forecast.precipitation = providers::buienradar::get(position, metric).await
|
||||
}
|
||||
Metric::UVI => forecast.uvi = Some(()),
|
||||
}
|
||||
|
|
47
src/main.rs
47
src/main.rs
|
@ -13,53 +13,21 @@
|
|||
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use cached::proc_macro::cached;
|
||||
use color_eyre::Result;
|
||||
use geocoding::{Forward, Openstreetmap, Point};
|
||||
use rocket::serde::json::Json;
|
||||
use rocket::tokio::{self, select};
|
||||
use rocket::{get, routes, State};
|
||||
|
||||
pub(crate) use self::forecast::{forecast, Forecast, Metric};
|
||||
pub(crate) use self::forecast::Metric;
|
||||
use self::forecast::{forecast, Forecast};
|
||||
pub(crate) use self::maps::{Maps, MapsHandle};
|
||||
use self::position::{resolve_address, Position};
|
||||
|
||||
pub(crate) mod forecast;
|
||||
pub(crate) mod maps;
|
||||
pub(crate) mod position;
|
||||
pub(crate) mod providers;
|
||||
|
||||
/// Caching key helper function that can be used by providers.
|
||||
///
|
||||
/// This is necessary because `f64` does not implement `Eq` nor `Hash`, which is required by
|
||||
/// the caching implementation.
|
||||
fn cache_key(lat: f64, lon: f64, metric: Metric) -> (i32, i32, Metric) {
|
||||
let lat_key = (lat * 10_000.0) as i32;
|
||||
let lon_key = (lon * 10_000.0) as i32;
|
||||
|
||||
(lat_key, lon_key, metric)
|
||||
}
|
||||
|
||||
/// Retrieves the geocoded position for the given address.
|
||||
///
|
||||
/// Returns [`Some`] with tuple of latitude and longitude. Returns [`None`] if the address could
|
||||
/// not be geocoded or the OpenStreetMap Nomatim API could not be contacted.
|
||||
///
|
||||
/// If the result is [`Some`] it will be cached. Only the 100 least-recently used address
|
||||
/// will be cached.
|
||||
#[cached(size = 100)]
|
||||
async fn address_position(address: String) -> Option<(f64, f64)> {
|
||||
println!("🌍 Geocoding the position of the address: {}", address);
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let osm = Openstreetmap::new();
|
||||
let points: Vec<Point<f64>> = osm.forward(&address).ok()?;
|
||||
|
||||
// The `geocoding` API always returns (longitude, latitude) as (x, y).
|
||||
points.get(0).map(|point| (point.y(), point.x()))
|
||||
})
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
}
|
||||
|
||||
/// Handler for retrieving the forecast for an address.
|
||||
#[get("/forecast?<address>&<metrics>")]
|
||||
async fn forecast_address(
|
||||
|
@ -67,8 +35,8 @@ async fn forecast_address(
|
|||
metrics: Vec<Metric>,
|
||||
maps_handle: &State<MapsHandle>,
|
||||
) -> Option<Json<Forecast>> {
|
||||
let (lat, lon) = address_position(address).await?;
|
||||
let forecast = forecast(lat, lon, metrics, maps_handle).await;
|
||||
let position = resolve_address(address).await?;
|
||||
let forecast = forecast(position, metrics, maps_handle).await;
|
||||
|
||||
Some(Json(forecast))
|
||||
}
|
||||
|
@ -81,7 +49,8 @@ async fn forecast_geo(
|
|||
metrics: Vec<Metric>,
|
||||
maps_handle: &State<MapsHandle>,
|
||||
) -> Json<Forecast> {
|
||||
let forecast = forecast(lat, lon, metrics, maps_handle).await;
|
||||
let position = Position::new(lat, lon);
|
||||
let forecast = forecast(position, metrics, maps_handle).await;
|
||||
|
||||
Json(forecast)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,103 @@
|
|||
//! Positions in the geographic coordinate system.
|
||||
//!
|
||||
//! This module contains everything related to geographic coordinate system functionality.
|
||||
|
||||
use std::hash::Hash;
|
||||
|
||||
use cached::proc_macro::cached;
|
||||
use geocoding::{Forward, Openstreetmap, Point};
|
||||
use rocket::tokio;
|
||||
|
||||
/// A (geocoded) position.
|
||||
///
|
||||
/// This is used for measuring and communication positions directly on the Earth as latitude and
|
||||
/// longitude.
|
||||
///
|
||||
/// # Position equivalence and hashing
|
||||
///
|
||||
/// For caching purposes we need to check equivalence between two positions. If the positions match
|
||||
/// up to the 5th decimal, we consider them the same (see [`Position::lat_as_i32`] and
|
||||
/// [`Position::lon_as_i32`]).
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub(crate) struct Position {
|
||||
pub(crate) lat: f64,
|
||||
pub(crate) lon: f64,
|
||||
}
|
||||
|
||||
impl Position {
|
||||
/// Creates a new (geocoded) position.
|
||||
pub(crate) fn new(lat: f64, lon: f64) -> Self {
|
||||
Self { lat, lon }
|
||||
}
|
||||
|
||||
/// Returns the latitude as an integer.
|
||||
///
|
||||
/// This is achieved by multiplying it by `10_000` and rounding it. Thus, this gives a
|
||||
/// precision of 5 decimals.
|
||||
fn lat_as_i32(&self) -> i32 {
|
||||
(self.lat * 10_000.0).round() as i32
|
||||
}
|
||||
|
||||
/// Returns the longitude as an integer.
|
||||
///
|
||||
/// This is achieved by multiplying it by `10_000` and rounding it. Thus, this gives a
|
||||
/// precision of 5 decimals.
|
||||
fn lon_as_i32(&self) -> i32 {
|
||||
(self.lon * 10_000.0).round() as i32
|
||||
}
|
||||
|
||||
/// Returns the latitude as a string with the given precision.
|
||||
pub(crate) fn lat_as_str(&self, precision: usize) -> String {
|
||||
format!("{:.*}", precision, self.lat)
|
||||
}
|
||||
|
||||
/// Returns the longitude as a string with the given precision.
|
||||
pub(crate) fn lon_as_str(&self, precision: usize) -> String {
|
||||
format!("{:.*}", precision, self.lon)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Point<f64>> for Position {
|
||||
fn from(point: &Point<f64>) -> Self {
|
||||
// The `geocoding` API always returns (longitude, latitude) as (x, y).
|
||||
Position::new(point.y(), point.x())
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for Position {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
// Floats cannot be hashed. Use the 5-decimal precision integer representation of the
|
||||
// coordinates instead.
|
||||
self.lat_as_i32().hash(state);
|
||||
self.lon_as_i32().hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Position {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.lat_as_i32() == other.lat_as_i32() && self.lon_as_i32() == other.lon_as_i32()
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Position {}
|
||||
|
||||
/// Resolves the geocoded position for a given address.
|
||||
///
|
||||
/// Returns [`None`] if the address could not be geocoded or the OpenStreetMap Nomatim API could
|
||||
/// not be contacted.
|
||||
///
|
||||
/// If the result is [`Some`], it will be cached.
|
||||
/// Note that only the 100 least recently used addresses will be cached.
|
||||
#[cached(size = 100)]
|
||||
pub(crate) async fn resolve_address(address: String) -> Option<Position> {
|
||||
println!("🌍 Geocoding the position of the address: {}", address);
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let osm = Openstreetmap::new();
|
||||
let points: Vec<Point<f64>> = osm.forward(&address).ok()?;
|
||||
|
||||
points.get(0).map(Position::from)
|
||||
})
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
}
|
|
@ -12,7 +12,8 @@ use csv::ReaderBuilder;
|
|||
use reqwest::Url;
|
||||
use rocket::serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{cache_key, Metric};
|
||||
use crate::position::Position;
|
||||
use crate::Metric;
|
||||
|
||||
/// The base URL for the Buienradar API.
|
||||
const BUIENRADAR_BASE_URL: &str = "https://gpsgadget.buienradar.nl/data/raintext";
|
||||
|
@ -93,20 +94,15 @@ fn convert_value(v: u16) -> f32 {
|
|||
///
|
||||
/// If the result is [`Some`] it will be cached for 5 minutes for the the given position and
|
||||
/// metric.
|
||||
#[cached(
|
||||
time = 300,
|
||||
convert = "{ cache_key(lat, lon, metric) }",
|
||||
key = "(i32, i32, Metric)",
|
||||
option = true
|
||||
)]
|
||||
pub(crate) async fn get(lat: f64, lon: f64, metric: Metric) -> Option<Vec<Item>> {
|
||||
#[cached(time = 300, option = true)]
|
||||
pub(crate) async fn get(position: Position, metric: Metric) -> Option<Vec<Item>> {
|
||||
if metric != Metric::Precipitation {
|
||||
return None;
|
||||
}
|
||||
let mut url = Url::parse(BUIENRADAR_BASE_URL).unwrap();
|
||||
url.query_pairs_mut()
|
||||
.append_pair("lat", &format!("{:.02}", lat))
|
||||
.append_pair("lon", &format!("{:.02}", lon));
|
||||
.append_pair("lat", &position.lat_as_str(2))
|
||||
.append_pair("lon", &position.lon_as_str(2));
|
||||
|
||||
println!("▶️ Retrieving Buienradar data from: {url}");
|
||||
let response = reqwest::get(url).await.ok()?;
|
||||
|
|
|
@ -8,7 +8,8 @@ use chrono::{DateTime, Utc};
|
|||
use reqwest::Url;
|
||||
use rocket::serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{cache_key, Metric};
|
||||
use crate::position::Position;
|
||||
use crate::Metric;
|
||||
|
||||
/// The base URL for the Luchtmeetnet API.
|
||||
const LUCHTMEETNET_BASE_URL: &str = "https://api.luchtmeetnet.nl/open_api/concentrations";
|
||||
|
@ -52,13 +53,8 @@ pub(crate) struct Item {
|
|||
///
|
||||
/// If the result is [`Some`] it will be cached for 30 minutes for the the given position and
|
||||
/// metric.
|
||||
#[cached(
|
||||
time = 1800,
|
||||
convert = "{ cache_key(lat, lon, metric) }",
|
||||
key = "(i32, i32, Metric)",
|
||||
option = true
|
||||
)]
|
||||
pub(crate) async fn get(lat: f64, lon: f64, metric: Metric) -> Option<Vec<Item>> {
|
||||
#[cached(time = 1800, option = true)]
|
||||
pub(crate) async fn get(position: Position, metric: Metric) -> Option<Vec<Item>> {
|
||||
let formula = match metric {
|
||||
Metric::AQI => "lki",
|
||||
Metric::NO2 => "no2",
|
||||
|
@ -69,8 +65,8 @@ pub(crate) async fn get(lat: f64, lon: f64, metric: Metric) -> Option<Vec<Item>>
|
|||
let mut url = Url::parse(LUCHTMEETNET_BASE_URL).unwrap();
|
||||
url.query_pairs_mut()
|
||||
.append_pair("formula", formula)
|
||||
.append_pair("latitude", &format!("{:.05}", lat))
|
||||
.append_pair("longitude", &format!("{:.05}", lon));
|
||||
.append_pair("latitude", &position.lat_as_str(5))
|
||||
.append_pair("longitude", &position.lon_as_str(5));
|
||||
|
||||
println!("▶️ Retrieving Luchtmeetnet data from: {url}");
|
||||
let response = reqwest::get(url).await.ok()?;
|
||||
|
|
Loading…
Reference in New Issue