backend/service/
plant_layer.rs

1//! Service layer for plant layer.
2
3use std::io::Cursor;
4
5use actix_http::StatusCode;
6use chrono::Utc;
7use image::{ImageBuffer, Rgba};
8
9use crate::{
10    config::data::SharedPool,
11    error::ServiceError,
12    model::{
13        dto::{HeatMapQueryParams, RelationSearchParameters, RelationsDto},
14        entity::plant_layer,
15        r#enum::heatmap_color::HeatmapColor,
16    },
17    service::application_settings,
18};
19
20use super::application_settings::HeatmapColors;
21
22/// Generates a heatmap signaling ideal locations for planting the plant.
23/// The return values are raw bytes of an PNG image.
24///
25/// # Errors
26/// * If the connection to the database could not be established.
27/// * If no map with id `map_id` exists.
28/// * 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.
29/// * If no plant with id `plant_id` exists.
30/// * If the image could not be parsed to bytes.
31pub async fn heatmap(
32    map_id: i32,
33    query_params: HeatMapQueryParams,
34    pool: &SharedPool,
35) -> Result<Vec<u8>, ServiceError> {
36    let mut conn = pool.get().await?;
37    let result = plant_layer::heatmap(
38        map_id,
39        query_params.plant_layer_id,
40        query_params.shade_layer_id,
41        query_params.hydrology_layer_id,
42        query_params.soil_layer_id,
43        query_params.plant_id,
44        query_params.date.unwrap_or_else(|| Utc::now().date_naive()),
45        &mut conn,
46    )
47    .await?;
48
49    let heatmap_colors = application_settings::get_heatmap_colors(&mut conn).await?;
50    let buffer = matrix_to_image(&result, heatmap_colors)?;
51
52    Ok(buffer)
53}
54
55/// Parses the matrix of scores with values 0-1 to raw bytes of a PNG image.
56#[allow(
57    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
58    clippy::indexing_slicing,           // ok, because size of image is generated using matrix width and height
59    clippy::cast_sign_loss              // ok, because we only care about positive values
60)]
61fn matrix_to_image(
62    matrix: &[Vec<(HeatmapColor, f32)>],
63    colors: HeatmapColors,
64) -> Result<Vec<u8>, ServiceError> {
65    let (width, height) = (matrix[0].len(), matrix.len());
66    let mut imgbuf = ImageBuffer::new(width as u32, height as u32);
67
68    let (red, orange, green, black) = colors;
69
70    for (x, y, pixel) in imgbuf.enumerate_pixels_mut() {
71        let (color, relevance) = &matrix[y as usize][x as usize];
72        let alpha = (relevance * 255.0) as u8;
73
74        *pixel = match color {
75            HeatmapColor::Red => Rgba([red.0, red.1, red.2, alpha]),
76            HeatmapColor::Orange => Rgba([orange.0, orange.1, orange.2, alpha]),
77            HeatmapColor::Green => Rgba([green.0, green.1, green.2, alpha]),
78            HeatmapColor::Black => Rgba([black.0, black.1, black.2, alpha]),
79        };
80    }
81
82    let mut buffer: Vec<u8> = Vec::new();
83    imgbuf
84        .write_to(&mut Cursor::new(&mut buffer), image::ImageOutputFormat::Png)
85        .map_err(|err| ServiceError::new(StatusCode::INTERNAL_SERVER_ERROR, &err.to_string()))?;
86    Ok(buffer)
87}
88
89/// Get spatial relations of a certain plant.
90///
91/// # Errors
92/// * If the connection to the database could not be established.
93/// * If the SQL query failed.
94pub async fn find_relations(
95    search_query: RelationSearchParameters,
96    pool: &SharedPool,
97) -> Result<RelationsDto, ServiceError> {
98    let mut conn = pool.get().await?;
99    let result = plant_layer::find_relations(search_query, &mut conn).await?;
100    Ok(result)
101}