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-19 16:45:37 +01:00
|
|
|
use chrono::Utc;
|
2022-02-12 15:58:56 +01:00
|
|
|
use color_eyre::Result;
|
2022-02-15 17:05:12 +01:00
|
|
|
use rocket::http::ContentType;
|
|
|
|
use rocket::response::content::Custom;
|
2022-02-12 12:25:05 +01:00
|
|
|
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 14:15:59 +01:00
|
|
|
pub(crate) use self::forecast::Metric;
|
|
|
|
use self::forecast::{forecast, Forecast};
|
2022-02-15 13:14:01 +01:00
|
|
|
pub(crate) use self::maps::{Maps, MapsHandle};
|
2022-02-15 14:15:59 +01:00
|
|
|
use self::position::{resolve_address, Position};
|
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;
|
2022-02-15 14:15:59 +01:00
|
|
|
pub(crate) mod position;
|
2022-02-13 12:45:27 +01:00
|
|
|
pub(crate) mod providers;
|
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-15 14:15:59 +01:00
|
|
|
let position = resolve_address(address).await?;
|
|
|
|
let forecast = forecast(position, 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> {
|
2022-02-15 14:15:59 +01:00
|
|
|
let position = Position::new(lat, lon);
|
|
|
|
let forecast = forecast(position, metrics, maps_handle).await;
|
2022-02-12 12:25:05 +01:00
|
|
|
|
|
|
|
Json(forecast)
|
|
|
|
}
|
|
|
|
|
2022-02-18 23:18:50 +01:00
|
|
|
/// Handler for showing the current map with the geocoded position of an address for a specific
|
|
|
|
/// metric.
|
2022-02-15 17:05:12 +01:00
|
|
|
///
|
|
|
|
/// Note: This handler is mosly used for debugging purposes!
|
2022-02-17 22:25:13 +01:00
|
|
|
#[get("/map?<address>&<metric>")]
|
2022-02-18 23:18:50 +01:00
|
|
|
async fn show_map_address(
|
2022-02-17 22:25:13 +01:00
|
|
|
address: String,
|
2022-02-15 17:05:12 +01:00
|
|
|
metric: Metric,
|
|
|
|
maps_handle: &State<MapsHandle>,
|
|
|
|
) -> Option<Custom<Vec<u8>>> {
|
2022-02-18 23:18:50 +01:00
|
|
|
let position = resolve_address(address).await?;
|
|
|
|
let image_data = draw_position(position, metric, maps_handle).await;
|
|
|
|
|
|
|
|
image_data.map(|id| Custom(ContentType::PNG, id))
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Handler for showing the current map with the geocoded position for a specific metric.
|
|
|
|
///
|
|
|
|
/// Note: This handler is mosly used for debugging purposes!
|
|
|
|
#[get("/map?<lat>&<lon>&<metric>", rank = 2)]
|
|
|
|
async fn show_map_geo(
|
|
|
|
lat: f64,
|
|
|
|
lon: f64,
|
|
|
|
metric: Metric,
|
|
|
|
maps_handle: &State<MapsHandle>,
|
|
|
|
) -> Option<Custom<Vec<u8>>> {
|
|
|
|
let position = Position::new(lat, lon);
|
|
|
|
let image_data = draw_position(position, metric, maps_handle).await;
|
|
|
|
|
|
|
|
image_data.map(|id| Custom(ContentType::PNG, id))
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Draws a crosshair on a map for the given position.
|
|
|
|
///
|
|
|
|
/// The map that is used is determined by the metric.
|
|
|
|
// FIXME: Maybe move this to the `maps` module?
|
|
|
|
async fn draw_position(
|
|
|
|
position: Position,
|
|
|
|
metric: Metric,
|
|
|
|
maps_handle: &MapsHandle,
|
|
|
|
) -> Option<Vec<u8>> {
|
2022-02-15 17:05:12 +01:00
|
|
|
use image::{GenericImage, Rgba};
|
|
|
|
use std::io::Cursor;
|
|
|
|
|
2022-02-18 23:18:50 +01:00
|
|
|
let maps_handle = Arc::clone(maps_handle);
|
|
|
|
tokio::task::spawn_blocking(move || {
|
2022-02-19 16:45:37 +01:00
|
|
|
let now = Utc::now();
|
2022-02-18 23:18:50 +01:00
|
|
|
let maps = maps_handle.lock().expect("Maps handle lock was poisoned");
|
|
|
|
let (mut image, coords) = match metric {
|
|
|
|
Metric::PAQI => (maps.pollen_at(now)?, maps.pollen_project(position)),
|
|
|
|
Metric::Pollen => (maps.pollen_at(now)?, maps.pollen_project(position)),
|
|
|
|
Metric::UVI => (maps.uvi_at(now)?, maps.uvi_project(position)),
|
|
|
|
_ => return None, // Unsupported metric
|
|
|
|
};
|
|
|
|
drop(maps);
|
|
|
|
|
|
|
|
if let Some((x, y)) = coords {
|
|
|
|
for py in 0..(image.height() - 1) {
|
|
|
|
image.put_pixel(x, py, Rgba::from([0x00, 0x00, 0x00, 0x70]));
|
|
|
|
}
|
|
|
|
|
|
|
|
for px in 0..(image.width() - 1) {
|
|
|
|
image.put_pixel(px, y, Rgba::from([0x00, 0x00, 0x00, 0x70]));
|
|
|
|
}
|
2022-02-18 23:00:58 +01:00
|
|
|
}
|
2022-02-15 17:05:12 +01:00
|
|
|
|
2022-02-18 23:18:50 +01:00
|
|
|
// Encode the image as PNG image data.
|
|
|
|
let mut image_data = Cursor::new(Vec::new());
|
|
|
|
image
|
|
|
|
.write_to(
|
|
|
|
&mut image_data,
|
|
|
|
image::ImageOutputFormat::from(image::ImageFormat::Png),
|
|
|
|
)
|
|
|
|
.ok()?;
|
|
|
|
|
|
|
|
Some(image_data.into_inner())
|
|
|
|
})
|
|
|
|
.await
|
|
|
|
.ok()
|
|
|
|
.flatten()
|
2022-02-15 17:05:12 +01:00
|
|
|
}
|
|
|
|
|
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-18 23:18:50 +01:00
|
|
|
.mount(
|
|
|
|
"/",
|
|
|
|
routes![
|
|
|
|
forecast_address,
|
|
|
|
forecast_geo,
|
|
|
|
show_map_address,
|
|
|
|
show_map_geo
|
|
|
|
],
|
|
|
|
)
|
2022-02-12 15:58:56 +01:00
|
|
|
.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
|
|
|
}
|