diff --git a/src/main.rs b/src/main.rs index 3c4a214..d3d88c8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,7 +9,6 @@ use std::sync::{Arc, Mutex}; -use chrono::Utc; use color_eyre::Result; use rocket::http::ContentType; use rocket::response::content::Custom; @@ -19,7 +18,7 @@ use rocket::{get, routes, State}; pub(crate) use self::forecast::Metric; use self::forecast::{forecast, Forecast}; -pub(crate) use self::maps::{Maps, MapsHandle}; +pub(crate) use self::maps::{mark_map, Maps, MapsHandle}; use self::position::{resolve_address, Position}; pub(crate) mod forecast; @@ -59,13 +58,13 @@ async fn forecast_geo( /// /// Note: This handler is mosly used for debugging purposes! #[get("/map?
&")] -async fn show_map_address( +async fn map_address( address: String, metric: Metric, maps_handle: &State, ) -> Option>> { let position = resolve_address(address).await?; - let image_data = draw_position(position, metric, maps_handle).await; + let image_data = mark_map(position, metric, maps_handle).await; image_data.map(|id| Custom(ContentType::PNG, id)) } @@ -74,68 +73,18 @@ async fn show_map_address( /// /// Note: This handler is mosly used for debugging purposes! #[get("/map?&&", rank = 2)] -async fn show_map_geo( +async fn map_geo( lat: f64, lon: f64, metric: Metric, maps_handle: &State, ) -> Option>> { let position = Position::new(lat, lon); - let image_data = draw_position(position, metric, maps_handle).await; + let image_data = mark_map(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> { - use image::{GenericImage, Rgba}; - use std::io::Cursor; - - let maps_handle = Arc::clone(maps_handle); - tokio::task::spawn_blocking(move || { - let now = Utc::now(); - 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])); - } - } - - // 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() -} - /// Starts the main maps refresh loop and sets up and launches Rocket. /// /// See [`maps::run`] for the maps refresh loop. @@ -151,12 +100,7 @@ async fn main() -> Result<()> { .manage(maps_handle) .mount( "/", - routes![ - forecast_address, - forecast_geo, - show_map_address, - show_map_geo - ], + routes![forecast_address, forecast_geo, map_address, map_geo], ) .ignite() .await?; diff --git a/src/maps.rs b/src/maps.rs index b5a12f8..932bdab 100644 --- a/src/maps.rs +++ b/src/maps.rs @@ -9,12 +9,13 @@ use std::sync::{Arc, Mutex}; use chrono::serde::ts_seconds; use chrono::{DateTime, Duration, Utc}; -use image::{DynamicImage, GenericImageView, ImageFormat, Pixel, Rgb, Rgba}; +use image::{DynamicImage, GenericImage, GenericImageView, ImageFormat, Pixel, Rgb, Rgba}; use reqwest::Url; use rocket::serde::Serialize; use rocket::tokio; use rocket::tokio::time::sleep; +use crate::forecast::Metric; use crate::position::Position; /// A handle to access the in-memory cached maps. @@ -149,33 +150,22 @@ impl Maps { } } - /// Returns the pollen map for the given instant. + /// Returns a current pollen map that marks the provided position. /// - /// This returns [`None`] if the maps are not in the cache yet, or if `instant` is too far in the - /// future with respect to the cached maps. - pub(crate) fn pollen_at(&self, instant: DateTime) -> Option { - let duration = instant.signed_duration_since(self.pollen_stamp); - let offset = (duration.num_seconds() / POLLEN_MAP_INTERVAL) as u32; - // Check if out of bounds. - if offset >= POLLEN_MAP_COUNT { - return None; - } - - self.pollen.as_ref().map(|maps| { - let width = maps.width() / POLLEN_MAP_COUNT; - - maps.crop_imm(offset * width, 0, width, maps.height()) - }) - } - - /// Projects the provided geocoded position to a coordinate on a pollen map. - /// - /// This returns [`None`] if the maps are not in the cache yet. - pub(crate) fn pollen_project(&self, position: Position) -> Option<(u32, u32)> { + /// This returns [`None`] if the maps are not in the cache yet, there is no matching map for + /// the current moment or if the provided position is not within the bounds of the map. + pub(crate) fn pollen_mark(&self, position: Position) -> Option { self.pollen.as_ref().and_then(|maps| { - let map = maps.view(0, 0, maps.width() / POLLEN_MAP_COUNT, maps.height()); + let map = map_at( + maps, + self.pollen_stamp, + POLLEN_MAP_INTERVAL, + POLLEN_MAP_COUNT, + Utc::now(), + )?; + let coords = project(&map, POLLEN_MAP_REF_POINTS, position)?; - project(&*map, POLLEN_MAP_REF_POINTS, position) + Some(mark(map, coords)) }) } @@ -184,9 +174,10 @@ impl Maps { /// This returns [`None`] if the maps are not in the cache yet. /// Otherwise, it returns [`Some`] with a list of pollen sample, one for each map /// in the series of maps. - pub(crate) fn pollen_sample(&self, position: Position) -> Option> { + pub(crate) fn pollen_samples(&self, position: Position) -> Option> { self.pollen.as_ref().and_then(|maps| { - let coords = self.pollen_project(position)?; + let map = maps.view(0, 0, maps.width() / UVI_MAP_COUNT, maps.height()); + let coords = project(&*map, POLLEN_MAP_REF_POINTS, position)?; sample( maps, @@ -198,33 +189,22 @@ impl Maps { }) } - /// Returns the UV index map for the given instant. + /// Returns a current UV index map that marks the provided position. /// - /// This returns [`None`] if the maps are not in the cache yet, or if `instant` is too far in - /// the future with respect to the cached maps. - pub(crate) fn uvi_at(&self, instant: DateTime) -> Option { - let duration = instant.signed_duration_since(self.uvi_stamp); - let offset = (duration.num_seconds() / UVI_MAP_INTERVAL) as u32; - // Check if out of bounds. - if offset >= UVI_MAP_COUNT { - return None; - } - - self.uvi.as_ref().map(|maps| { - let width = maps.width() / UVI_MAP_COUNT; - - maps.crop_imm(offset * width, 0, width, maps.height()) - }) - } - - /// Projects the provided geocoded position to a coordinate on an UV index map. - /// - /// This returns [`None`] if the maps are not in the cache yet. - pub(crate) fn uvi_project(&self, position: Position) -> Option<(u32, u32)> { + /// This returns [`None`] if the maps are not in the cache yet, there is no matching map for + /// the current moment or if the provided position is not within the bounds of the map. + pub(crate) fn uvi_mark(&self, position: Position) -> Option { self.uvi.as_ref().and_then(|maps| { - let map = maps.view(0, 0, maps.width() / UVI_MAP_COUNT, maps.height()); + let map = map_at( + maps, + self.uvi_stamp, + UVI_MAP_INTERVAL, + UVI_MAP_COUNT, + Utc::now(), + )?; + let coords = project(&map, POLLEN_MAP_REF_POINTS, position)?; - project(&*map, UVI_MAP_REF_POINTS, position) + Some(mark(map, coords)) }) } @@ -233,10 +213,10 @@ impl Maps { /// This returns [`None`] if the maps are not in the cache yet. /// Otherwise, it returns [`Some`] with a list of UV index sample, one for each map /// in the series of maps. - #[allow(dead_code)] - pub(crate) fn uvi_sample(&self, position: Position) -> Option> { + pub(crate) fn uvi_samples(&self, position: Position) -> Option> { self.uvi.as_ref().and_then(|maps| { - let coords = self.uvi_project(position)?; + let map = maps.view(0, 0, maps.width() / UVI_MAP_COUNT, maps.height()); + let coords = project(&*map, UVI_MAP_REF_POINTS, position)?; sample( maps, @@ -446,6 +426,42 @@ async fn retrieve_uvi_maps() -> Option<(DynamicImage, DateTime)> { retrieve_image(url).await } +/// Returns the map for the given instant. +/// +/// This returns [`None`] if `instant` is too far in the future with respect to the number of +/// cached maps. +fn map_at( + maps: &DynamicImage, + maps_stamp: DateTime, + interval: i64, + count: u32, + instant: DateTime, +) -> Option { + let duration = instant.signed_duration_since(maps_stamp); + let offset = (duration.num_seconds() / interval) as u32; + // Check if out of bounds. + if offset >= UVI_MAP_COUNT { + return None; + } + let width = maps.width() / count; + + Some(maps.crop_imm(offset * width, 0, width, maps.height())) +} + +/// Marks the provided coordinates on the map using a horizontal and vertical line. +fn mark(mut map: DynamicImage, coords: (u32, u32)) -> DynamicImage { + let (x, y) = coords; + + for py in 0..map.height() { + map.put_pixel(x, py, Rgba::from([0x00, 0x00, 0x00, 0x70])); + } + for px in 0..map.width() { + map.put_pixel(px, y, Rgba::from([0x00, 0x00, 0x00, 0x70])); + } + + map +} + /// Projects the provided geocoded position to a coordinate on a map. /// /// This uses two reference points and a Mercator projection on the y-coordinates of those points @@ -479,6 +495,43 @@ fn project( } } +/// Returns the data of a map with a crosshair drawn on it for the given position. +/// +/// The map that is used is determined by the provided metric. +pub(crate) async fn mark_map( + position: Position, + metric: Metric, + maps_handle: &MapsHandle, +) -> Option> { + use std::io::Cursor; + + let maps_handle = Arc::clone(maps_handle); + tokio::task::spawn_blocking(move || { + let maps = maps_handle.lock().expect("Maps handle lock was poisoned"); + let image = match metric { + Metric::PAQI => maps.pollen_mark(position), + Metric::Pollen => maps.pollen_mark(position), + Metric::UVI => maps.uvi_mark(position), + _ => return None, // Unsupported metric + }?; + drop(maps); + + // 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() +} + /// Runs a loop that keeps refreshing the maps when necessary. /// /// Use [`MapsRefresh`] trait methods on `maps_handle` to check whether each maps type needs to be diff --git a/src/providers/buienradar.rs b/src/providers/buienradar.rs index 4760558..22b4aee 100644 --- a/src/providers/buienradar.rs +++ b/src/providers/buienradar.rs @@ -181,7 +181,7 @@ async fn get_pollen(position: Position, maps_handle: &MapsHandle) -> Option Option