//! Service layer for plant layer.
use std::io::Cursor;
use actix_http::StatusCode;
use chrono::Utc;
use image::{ImageBuffer, Rgba};
use crate::{
config::data::SharedPool,
error::ServiceError,
model::{
dto::{HeatMapQueryParams, RelationSearchParameters, RelationsDto},
entity::plant_layer,
r#enum::heatmap_color::HeatmapColor,
},
service::application_settings,
};
use super::application_settings::HeatmapColors;
/// Generates a heatmap signaling ideal locations for planting the plant.
/// The return values are raw bytes of an PNG image.
///
/// # Errors
/// * If the connection to the database could not be established.
/// * If no map with id `map_id` exists.
/// * If no layer with id `layer_id` exists, if the layer is not a plant layer or if the layer is not part of the map.
/// * If no plant with id `plant_id` exists.
/// * If the image could not be parsed to bytes.
pub async fn heatmap(
map_id: i32,
query_params: HeatMapQueryParams,
pool: &SharedPool,
) -> Result<Vec<u8>, ServiceError> {
let mut conn = pool.get().await?;
let result = plant_layer::heatmap(
map_id,
query_params.plant_layer_id,
query_params.shade_layer_id,
query_params.hydrology_layer_id,
query_params.soil_layer_id,
query_params.plant_id,
query_params.date.unwrap_or_else(|| Utc::now().date_naive()),
&mut conn,
)
.await?;
let heatmap_colors = application_settings::get_heatmap_colors(&mut conn).await?;
let buffer = matrix_to_image(&result, heatmap_colors)?;
Ok(buffer)
}
/// Parses the matrix of scores with values 0-1 to raw bytes of a PNG image.
#[allow(
clippy::cast_possible_truncation, // ok, because size of matrix shouldn't ever be larger than u32 and casting to u8 in image should remove floating point values
clippy::indexing_slicing, // ok, because size of image is generated using matrix width and height
clippy::cast_sign_loss // ok, because we only care about positive values
)]
fn matrix_to_image(
matrix: &[Vec<(HeatmapColor, f32)>],
colors: HeatmapColors,
) -> Result<Vec<u8>, ServiceError> {
let (width, height) = (matrix[0].len(), matrix.len());
let mut imgbuf = ImageBuffer::new(width as u32, height as u32);
let (red, orange, green, black) = colors;
for (x, y, pixel) in imgbuf.enumerate_pixels_mut() {
let (color, relevance) = &matrix[y as usize][x as usize];
let alpha = (relevance * 255.0) as u8;
*pixel = match color {
HeatmapColor::Red => Rgba([red.0, red.1, red.2, alpha]),
HeatmapColor::Orange => Rgba([orange.0, orange.1, orange.2, alpha]),
HeatmapColor::Green => Rgba([green.0, green.1, green.2, alpha]),
HeatmapColor::Black => Rgba([black.0, black.1, black.2, alpha]),
};
}
let mut buffer: Vec<u8> = Vec::new();
imgbuf
.write_to(&mut Cursor::new(&mut buffer), image::ImageOutputFormat::Png)
.map_err(|err| ServiceError::new(StatusCode::INTERNAL_SERVER_ERROR, &err.to_string()))?;
Ok(buffer)
}
/// Get spatial relations of a certain plant.
///
/// # Errors
/// * If the connection to the database could not be established.
/// * If the SQL query failed.
pub async fn find_relations(
search_query: RelationSearchParameters,
pool: &SharedPool,
) -> Result<RelationsDto, ServiceError> {
let mut conn = pool.get().await?;
let result = plant_layer::find_relations(search_query, &mut conn).await?;
Ok(result)
}