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