backend/model/entity/
plant_layer.rs

1//! Contains the database implementation of the plant layer.
2
3use std::cmp::max;
4
5use crate::schema::sql_types::HeatmapColor as SqlHeatmapColor;
6use chrono::NaiveDate;
7use diesel::{
8    debug_query,
9    pg::Pg,
10    sql_types::{Array, Date, Float, Integer, Uuid as SqlUuid},
11    CombineDsl, ExpressionMethods, QueryDsl, QueryResult, QueryableByName,
12};
13use diesel_async::{AsyncPgConnection, RunQueryDsl};
14use log::{debug, trace};
15use uuid::Uuid;
16
17use crate::{
18    model::{
19        dto::{RelationDto, RelationSearchParameters, RelationsDto},
20        r#enum::{heatmap_color::HeatmapColor, spatial_relation_type::SpatialRelationType},
21    },
22    schema::spatial_relations,
23};
24
25/// A bounding box around the maps geometry.
26#[derive(Debug, Clone, QueryableByName)]
27struct BoundingBox {
28    /// The lowest x value in the geometry.
29    #[diesel(sql_type = Integer)]
30    x_min: i32,
31    /// The lowest y value in the geometry.
32    #[diesel(sql_type = Integer)]
33    y_min: i32,
34    /// The highest x value in the geometry.
35    #[diesel(sql_type = Integer)]
36    x_max: i32,
37    /// The highest y value in the geometry.
38    #[diesel(sql_type = Integer)]
39    y_max: i32,
40}
41
42/// Stores the score of a x,y coordinate on the heatmap.
43#[derive(Debug, Clone, QueryableByName)]
44struct HeatMapElement {
45    /// The score on the heatmap.
46    #[diesel(sql_type = SqlHeatmapColor)]
47    color: HeatmapColor,
48    /// The alpha on the heatmap.
49    #[diesel(sql_type = Float)]
50    relevance: f32,
51    /// The x values of the score
52    #[diesel(sql_type = Integer)]
53    x: i32,
54    /// The y values of the score.
55    #[diesel(sql_type = Integer)]
56    y: i32,
57}
58
59/// Generates a heatmap signaling ideal locations for planting the plant.
60///
61/// # Errors
62/// * If no map with id `map_id` exists.
63/// * 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.
64/// * If no plant with id `plant_id` exists.
65#[allow(
66    clippy::cast_sign_loss,             // ok, because we will never reach number high enough where this will matter
67    clippy::indexing_slicing,           // ok, because we know the size of the matrix using the maps bounding box
68    clippy::cast_possible_truncation,   // ok, because ceil prevents invalid truncation
69    clippy::too_many_arguments,         // ok, because all layer_ids are explicitly needed
70)]
71pub async fn heatmap(
72    map_id: i32,
73    plant_layer_id: Uuid,
74    shade_layer_id: Uuid,
75    hydrology_layer_id: Uuid,
76    soil_layer_id: Uuid,
77    plant_id: i32,
78    date: NaiveDate,
79    conn: &mut AsyncPgConnection,
80) -> QueryResult<Vec<Vec<(HeatmapColor, f32)>>> {
81    // Fetch the bounding box x and y values of the maps coordinates
82    let bounding_box_query =
83        diesel::sql_query("SELECT * FROM calculate_bbox($1)").bind::<Integer, _>(map_id);
84    debug!("{}", debug_query::<Pg, _>(&bounding_box_query));
85    let bounding_box = bounding_box_query.get_result::<BoundingBox>(conn).await?;
86
87    let granularity = calculate_granularity(&bounding_box);
88
89    // Fetch the heatmap
90    let query =
91        diesel::sql_query("SELECT * FROM calculate_heatmap($1, $2, $3, $4, $5, $6, $7, $8, $9)")
92            .bind::<Integer, _>(map_id)
93            .bind::<Array<SqlUuid>, _>(vec![
94                plant_layer_id,
95                shade_layer_id,
96                hydrology_layer_id,
97                soil_layer_id,
98            ])
99            .bind::<Integer, _>(plant_id)
100            .bind::<Date, _>(date)
101            .bind::<Integer, _>(granularity)
102            .bind::<Integer, _>(bounding_box.x_min)
103            .bind::<Integer, _>(bounding_box.y_min)
104            .bind::<Integer, _>(bounding_box.x_max)
105            .bind::<Integer, _>(bounding_box.y_max);
106    debug!("{}", debug_query::<Pg, _>(&query));
107    let result = query.load::<HeatMapElement>(conn).await?;
108
109    // Convert the result to a matrix.
110    // Matrix will be from 0..0 to ((x_max - x_min) / granularity)..((y_max - y_min) / granularity).
111    let num_cols =
112        (f64::from(bounding_box.x_max - bounding_box.x_min) / f64::from(granularity)).floor();
113    let num_rows =
114        (f64::from(bounding_box.y_max - bounding_box.y_min) / f64::from(granularity)).floor();
115    let mut heatmap = vec![vec![(HeatmapColor::Green, 0.0); num_cols as usize]; num_rows as usize];
116    for HeatMapElement {
117        color,
118        relevance,
119        x,
120        y,
121    } in result
122    {
123        heatmap[y as usize][x as usize] = (color, relevance);
124    }
125
126    trace!("{heatmap:#?}");
127    Ok(heatmap)
128}
129
130/// The number of values the resulting heatmap matrix should have.
131const NUMBER_OF_SQUARES: f64 = 10000.0;
132
133/// Calculate granularity so the number of scores calculated stays constant independent of map size.
134fn calculate_granularity(bounding_box: &BoundingBox) -> i32 {
135    let width = bounding_box.x_max - bounding_box.x_min;
136    let height = bounding_box.y_max - bounding_box.y_min;
137
138    // Mathematical reformulation:
139    // width * height = number_of_squares * granularity^2
140    // granularity = sqrt((width * height) / number_of_squares)
141    #[allow(clippy::cast_possible_truncation)] // ok, because we don't care about exact values
142    let granularity = (f64::from(width * height) / NUMBER_OF_SQUARES).sqrt() as i32;
143
144    max(1, granularity)
145}
146
147/// Get all spatial relations of a certain plant.
148///
149/// # Errors
150/// * If the SQL query fails.
151pub async fn find_relations(
152    search_query: RelationSearchParameters,
153    conn: &mut AsyncPgConnection,
154) -> QueryResult<RelationsDto> {
155    let query = spatial_relations::table
156        .select((spatial_relations::plant2, spatial_relations::relation))
157        .filter(spatial_relations::plant1.eq(&search_query.plant_id))
158        .union(
159            spatial_relations::table
160                .select((spatial_relations::plant1, spatial_relations::relation))
161                .filter(spatial_relations::plant2.eq(&search_query.plant_id)),
162        );
163    debug!("{}", debug_query::<Pg, _>(&query));
164    let relations = query
165        .load::<(i32, SpatialRelationType)>(conn)
166        .await?
167        .into_iter()
168        .map(|(id, relation)| RelationDto { id, relation })
169        .collect();
170    Ok(RelationsDto {
171        id: search_query.plant_id,
172        relations,
173    })
174}