sinoptik/src/main.rs

218 lines
6.2 KiB
Rust

//! 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)]
use std::sync::{Arc, Mutex};
use color_eyre::Result;
use rocket::http::ContentType;
use rocket::response::content::Custom;
use rocket::serde::json::Json;
use rocket::tokio::time::Instant;
use rocket::tokio::{self, select};
use rocket::{get, routes, State};
use std::f64::consts::PI;
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;
/// Handler for retrieving the forecast for an address.
#[get("/forecast?<address>&<metrics>")]
async fn forecast_address(
address: String,
metrics: Vec<Metric>,
maps_handle: &State<MapsHandle>,
) -> Option<Json<Forecast>> {
let position = resolve_address(address).await?;
let forecast = forecast(position, metrics, maps_handle).await;
Some(Json(forecast))
}
/// Handler for retrieving the forecast for a geocoded position.
#[get("/forecast?<lat>&<lon>&<metrics>", rank = 2)]
async fn forecast_geo(
lat: f64,
lon: f64,
metrics: Vec<Metric>,
maps_handle: &State<MapsHandle>,
) -> Json<Forecast> {
let position = Position::new(lat, lon);
let forecast = forecast(position, metrics, maps_handle).await;
Json(forecast)
}
fn deg2rad(x: f64)->f64 {
let y: f64 = x * PI / 180.0;
return y;
}
/// Mercator projection from https://stackoverflow.com/questions/18838915/convert-lat-lon-to-pixel-coordinate
fn mercator_y(lat: f64)->f64 {
return f64::ln(f64::tan(lat / 2.0 + PI / 4.0));
}
/// 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?<address>&<metric>")]
async fn show_map(
address: String,
metric: Metric,
maps_handle: &State<MapsHandle>,
) -> Option<Custom<Vec<u8>>> {
use image::{GenericImage, Rgba};
use std::io::Cursor;
let position = resolve_address(address).await?;
let lat = position.lat;
let lon = position.lon;
let now = Instant::now();
let maps = maps_handle.lock().expect("Maps handle lock was poisoned");
let mut image = match metric {
Metric::PAQI => maps.pollen_at(now)?,
Metric::Pollen => maps.pollen_at(now)?,
Metric::UVI => maps.uvi_at(now)?,
_ => return None, // Unsupported metric
};
// GPS coordinates from Google Maps
let vlissingen_lat: f64 = deg2rad(51.44);
let vlissingen_lon: f64 = deg2rad(3.57);
let vlissingen_x: u32 = 84;
let vlissingen_y: u32 = 745;
let winschoten_lat: f64 = deg2rad(53.14);
let winschoten_lon: f64 = deg2rad(7.04);
let winschoten_x: u32 = 729;
let winschoten_y: u32 = 185;
let lauwersoog_lat: f64 = deg2rad(53.40);
let lauwersoog_lon: f64 = deg2rad(6.22);
let lauwersoog_x: u32 = 566;
let lauwersoog_y: u32 = 111;
let enschede_lat: f64 = deg2rad(52.22);
let enschede_lon: f64 = deg2rad(6.90);
let enschede_x: u32 = 694;
let enschede_y: u32 = 494;
let ref_x1 = vlissingen_x;
let ref_lon1 = vlissingen_lon;
let ref_y1 = vlissingen_y;
let ref_lat1 = vlissingen_lat;
let ref_x2 = enschede_x;
let ref_lon2 = enschede_lon;
let ref_y2 = lauwersoog_y;
let ref_lat2 = lauwersoog_lat;
let y_min = mercator_y(ref_lat1);
let y_max = mercator_y(ref_lat2);
let x_factor = ((ref_x2 - ref_x1) as f64) / (ref_lon2 - ref_lon1);
let y_factor = ((ref_y1 - ref_y2) as f64) / (y_max - y_min);
println!("x_factor: {}, y_factor: {}", x_factor, y_factor);
println!("y_min: {}, y_max: {}, lat: {}", y_min * y_factor, y_max * y_factor, mercator_y(deg2rad(lat)) * y_factor);
let mut x_f64 = (deg2rad(lon) - ref_lon1) * x_factor + ref_x1 as f64;
let mut y_f64 = (y_max - mercator_y(deg2rad(lat))) * y_factor + ref_y2 as f64;
println!("x: {}, y: {}", x_f64, y_f64);
if x_f64 < 0.0 {
x_f64 = 0.0;
}
if x_f64 > (image.width() - 1) as f64 {
x_f64 = (image.width() - 1) as f64;
}
if y_f64 < 0.0 {
y_f64 = 0.0;
}
if y_f64 > (image.height() - 1) as f64 {
y_f64 = (image.height() - 1) as f64;
}
println!("bounded to x: {}, y: {}", x_f64, y_f64);
for px in ref_x1 - 5..=ref_x1 + 5 {
for py in ref_y1 - 5..=ref_y1 + 5 {
image.put_pixel(px, py, Rgba::from([0x00, 0xff, 0xff, 0x70]));
}
}
for px in ref_x2 - 5..=ref_x2 + 5 {
for py in ref_y2 - 5..=ref_y2 + 5 {
image.put_pixel(px, py, Rgba::from([0xff, 0x00, 0xff, 0x70]));
}
}
for py in 0..(image.height() - 1) {
image.put_pixel(x_f64 as u32, py, Rgba::from([0x00, 0x00, 0x00, 0x70]));
}
for px in 0..(image.width() - 1) {
image.put_pixel(px, y_f64 as u32, Rgba::from([0x00, 0x00, 0x00, 0x70]));
}
// Encode the image as PNG image data.
// FIXME: This encoding call blocks the worker thread!
let mut image_data = Cursor::new(Vec::new());
image
.write_to(
&mut image_data,
image::ImageOutputFormat::from(image::ImageFormat::Png),
)
.ok()?;
Some(Custom(ContentType::PNG, image_data.into_inner()))
}
/// Starts the main maps refresh loop and sets up and launches Rocket.
///
/// See [`maps::run`] for the maps refresh loop.
#[rocket::main]
async fn main() -> Result<()> {
color_eyre::install()?;
let maps = Maps::new();
let maps_handle = Arc::new(Mutex::new(maps));
let maps_updater = tokio::spawn(maps::run(Arc::clone(&maps_handle)));
let rocket = rocket::build()
.manage(maps_handle)
.mount("/", routes![forecast_address, forecast_geo, show_map])
.ignite()
.await?;
let shutdown = rocket.shutdown();
select! {
result = rocket.launch() => {
result?
}
result = maps_updater => {
shutdown.notify();
result?
}
}
Ok(())
}