2022-02-12 12:25:05 +01:00
|
|
|
//! Service that provides today's weather forecast for air quality, rain and UV metrics.
|
|
|
|
//!
|
|
|
|
//! This is useful if you want to prepare for going outside and need to know what happens in the
|
|
|
|
//! near future or later today.
|
|
|
|
|
|
|
|
#![warn(
|
|
|
|
clippy::all,
|
|
|
|
missing_debug_implementations,
|
|
|
|
rust_2018_idioms,
|
|
|
|
rustdoc::broken_intra_doc_links
|
|
|
|
)]
|
|
|
|
#![deny(missing_docs)]
|
|
|
|
|
2022-02-13 11:16:19 +01:00
|
|
|
use std::sync::{Arc, Mutex};
|
2022-02-12 21:35:58 +01:00
|
|
|
|
2022-02-14 21:06:31 +01:00
|
|
|
use cached::proc_macro::cached;
|
2022-02-12 15:58:56 +01:00
|
|
|
use color_eyre::Result;
|
2022-02-12 12:25:05 +01:00
|
|
|
use geocoding::{Forward, Openstreetmap, Point};
|
|
|
|
use rocket::serde::json::Json;
|
2022-02-12 15:58:56 +01:00
|
|
|
use rocket::tokio::{self, select};
|
2022-02-15 13:14:01 +01:00
|
|
|
use rocket::{get, routes, State};
|
2022-02-12 15:58:56 +01:00
|
|
|
|
2022-02-15 13:14:01 +01:00
|
|
|
pub(crate) use self::forecast::{forecast, Forecast, Metric};
|
|
|
|
pub(crate) use self::maps::{Maps, MapsHandle};
|
2022-02-12 15:58:56 +01:00
|
|
|
|
2022-02-15 13:14:01 +01:00
|
|
|
pub(crate) mod forecast;
|
2022-02-13 12:45:27 +01:00
|
|
|
pub(crate) mod maps;
|
|
|
|
pub(crate) mod providers;
|
2022-02-12 12:25:05 +01:00
|
|
|
|
2022-02-14 21:06:31 +01:00
|
|
|
/// 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)
|
|
|
|
}
|
|
|
|
|
2022-02-12 12:25:05 +01:00
|
|
|
/// Retrieves the geocoded position for the given address.
|
2022-02-14 21:04:25 +01:00
|
|
|
///
|
|
|
|
/// 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.
|
2022-02-14 21:40:07 +01:00
|
|
|
///
|
|
|
|
/// If the result is [`Some`] it will be cached. Only the 100 least-recently used address
|
|
|
|
/// will be cached.
|
2022-02-14 21:04:25 +01:00
|
|
|
#[cached(size = 100)]
|
2022-02-12 15:57:48 +01:00
|
|
|
async fn address_position(address: String) -> Option<(f64, f64)> {
|
2022-02-14 21:04:25 +01:00
|
|
|
println!("🌍 Geocoding the position of the address: {}", address);
|
2022-02-12 15:57:48 +01:00
|
|
|
tokio::task::spawn_blocking(move || {
|
|
|
|
let osm = Openstreetmap::new();
|
|
|
|
let points: Vec<Point<f64>> = osm.forward(&address).ok()?;
|
|
|
|
|
2022-02-14 21:04:25 +01:00
|
|
|
// The `geocoding` API always returns (longitude, latitude) as (x, y).
|
|
|
|
points.get(0).map(|point| (point.y(), point.x()))
|
2022-02-12 15:57:48 +01:00
|
|
|
})
|
|
|
|
.await
|
|
|
|
.ok()
|
|
|
|
.flatten()
|
2022-02-12 12:25:05 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Handler for retrieving the forecast for an address.
|
|
|
|
#[get("/forecast?<address>&<metrics>")]
|
2022-02-12 21:35:58 +01:00
|
|
|
async fn forecast_address(
|
|
|
|
address: String,
|
|
|
|
metrics: Vec<Metric>,
|
|
|
|
maps_handle: &State<MapsHandle>,
|
|
|
|
) -> Option<Json<Forecast>> {
|
2022-02-12 15:57:48 +01:00
|
|
|
let (lat, lon) = address_position(address).await?;
|
2022-02-12 21:35:58 +01:00
|
|
|
let forecast = forecast(lat, lon, metrics, maps_handle).await;
|
2022-02-12 12:25:05 +01:00
|
|
|
|
|
|
|
Some(Json(forecast))
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Handler for retrieving the forecast for a geocoded position.
|
|
|
|
#[get("/forecast?<lat>&<lon>&<metrics>", rank = 2)]
|
2022-02-12 21:35:58 +01:00
|
|
|
async fn forecast_geo(
|
|
|
|
lat: f64,
|
|
|
|
lon: f64,
|
|
|
|
metrics: Vec<Metric>,
|
|
|
|
maps_handle: &State<MapsHandle>,
|
|
|
|
) -> Json<Forecast> {
|
|
|
|
let forecast = forecast(lat, lon, metrics, maps_handle).await;
|
2022-02-12 12:25:05 +01:00
|
|
|
|
|
|
|
Json(forecast)
|
|
|
|
}
|
|
|
|
|
2022-02-12 15:58:56 +01:00
|
|
|
/// Starts the main maps refresh loop and sets up and launches Rocket.
|
2022-02-13 11:22:22 +01:00
|
|
|
///
|
|
|
|
/// See [`maps::run`] for the maps refresh loop.
|
2022-02-12 15:58:56 +01:00
|
|
|
#[rocket::main]
|
|
|
|
async fn main() -> Result<()> {
|
|
|
|
color_eyre::install()?;
|
|
|
|
|
2022-02-12 21:35:58 +01:00
|
|
|
let maps = Maps::new();
|
|
|
|
let maps_handle = Arc::new(Mutex::new(maps));
|
|
|
|
let maps_updater = tokio::spawn(maps::run(Arc::clone(&maps_handle)));
|
|
|
|
|
2022-02-12 15:58:56 +01:00
|
|
|
let rocket = rocket::build()
|
2022-02-12 21:35:58 +01:00
|
|
|
.manage(maps_handle)
|
2022-02-12 15:58:56 +01:00
|
|
|
.mount("/", routes![forecast_address, forecast_geo])
|
|
|
|
.ignite()
|
|
|
|
.await?;
|
|
|
|
let shutdown = rocket.shutdown();
|
|
|
|
|
|
|
|
select! {
|
|
|
|
result = rocket.launch() => {
|
|
|
|
result?
|
|
|
|
}
|
|
|
|
result = maps_updater => {
|
|
|
|
shutdown.notify();
|
|
|
|
result?
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
2022-02-12 12:25:05 +01:00
|
|
|
}
|