backend/model/entity/
plant_layer.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
//! Contains the database implementation of the plant layer.

use std::cmp::max;

use crate::schema::sql_types::HeatmapColor as SqlHeatmapColor;
use chrono::NaiveDate;
use diesel::{
    debug_query,
    pg::Pg,
    sql_types::{Array, Date, Float, Integer, Uuid as SqlUuid},
    CombineDsl, ExpressionMethods, QueryDsl, QueryResult, QueryableByName,
};
use diesel_async::{AsyncPgConnection, RunQueryDsl};
use log::{debug, trace};
use uuid::Uuid;

use crate::{
    model::{
        dto::{RelationDto, RelationSearchParameters, RelationsDto},
        r#enum::{heatmap_color::HeatmapColor, spatial_relation_type::SpatialRelationType},
    },
    schema::spatial_relations,
};

/// A bounding box around the maps geometry.
#[derive(Debug, Clone, QueryableByName)]
struct BoundingBox {
    /// The lowest x value in the geometry.
    #[diesel(sql_type = Integer)]
    x_min: i32,
    /// The lowest y value in the geometry.
    #[diesel(sql_type = Integer)]
    y_min: i32,
    /// The highest x value in the geometry.
    #[diesel(sql_type = Integer)]
    x_max: i32,
    /// The highest y value in the geometry.
    #[diesel(sql_type = Integer)]
    y_max: i32,
}

/// Stores the score of a x,y coordinate on the heatmap.
#[derive(Debug, Clone, QueryableByName)]
struct HeatMapElement {
    /// The score on the heatmap.
    #[diesel(sql_type = SqlHeatmapColor)]
    color: HeatmapColor,
    /// The alpha on the heatmap.
    #[diesel(sql_type = Float)]
    relevance: f32,
    /// The x values of the score
    #[diesel(sql_type = Integer)]
    x: i32,
    /// The y values of the score.
    #[diesel(sql_type = Integer)]
    y: i32,
}

/// Generates a heatmap signaling ideal locations for planting the plant.
///
/// # Errors
/// * 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.
#[allow(
    clippy::cast_sign_loss,             // ok, because we will never reach number high enough where this will matter
    clippy::indexing_slicing,           // ok, because we know the size of the matrix using the maps bounding box
    clippy::cast_possible_truncation,   // ok, because ceil prevents invalid truncation
    clippy::too_many_arguments,         // ok, because all layer_ids are explicitly needed
)]
pub async fn heatmap(
    map_id: i32,
    plant_layer_id: Uuid,
    shade_layer_id: Uuid,
    hydrology_layer_id: Uuid,
    soil_layer_id: Uuid,
    plant_id: i32,
    date: NaiveDate,
    conn: &mut AsyncPgConnection,
) -> QueryResult<Vec<Vec<(HeatmapColor, f32)>>> {
    // Fetch the bounding box x and y values of the maps coordinates
    let bounding_box_query =
        diesel::sql_query("SELECT * FROM calculate_bbox($1)").bind::<Integer, _>(map_id);
    debug!("{}", debug_query::<Pg, _>(&bounding_box_query));
    let bounding_box = bounding_box_query.get_result::<BoundingBox>(conn).await?;

    let granularity = calculate_granularity(&bounding_box);

    // Fetch the heatmap
    let query =
        diesel::sql_query("SELECT * FROM calculate_heatmap($1, $2, $3, $4, $5, $6, $7, $8, $9)")
            .bind::<Integer, _>(map_id)
            .bind::<Array<SqlUuid>, _>(vec![
                plant_layer_id,
                shade_layer_id,
                hydrology_layer_id,
                soil_layer_id,
            ])
            .bind::<Integer, _>(plant_id)
            .bind::<Date, _>(date)
            .bind::<Integer, _>(granularity)
            .bind::<Integer, _>(bounding_box.x_min)
            .bind::<Integer, _>(bounding_box.y_min)
            .bind::<Integer, _>(bounding_box.x_max)
            .bind::<Integer, _>(bounding_box.y_max);
    debug!("{}", debug_query::<Pg, _>(&query));
    let result = query.load::<HeatMapElement>(conn).await?;

    // Convert the result to a matrix.
    // Matrix will be from 0..0 to ((x_max - x_min) / granularity)..((y_max - y_min) / granularity).
    let num_cols =
        (f64::from(bounding_box.x_max - bounding_box.x_min) / f64::from(granularity)).floor();
    let num_rows =
        (f64::from(bounding_box.y_max - bounding_box.y_min) / f64::from(granularity)).floor();
    let mut heatmap = vec![vec![(HeatmapColor::Green, 0.0); num_cols as usize]; num_rows as usize];
    for HeatMapElement {
        color,
        relevance,
        x,
        y,
    } in result
    {
        heatmap[y as usize][x as usize] = (color, relevance);
    }

    trace!("{heatmap:#?}");
    Ok(heatmap)
}

/// The number of values the resulting heatmap matrix should have.
const NUMBER_OF_SQUARES: f64 = 10000.0;

/// Calculate granularity so the number of scores calculated stays constant independent of map size.
fn calculate_granularity(bounding_box: &BoundingBox) -> i32 {
    let width = bounding_box.x_max - bounding_box.x_min;
    let height = bounding_box.y_max - bounding_box.y_min;

    // Mathematical reformulation:
    // width * height = number_of_squares * granularity^2
    // granularity = sqrt((width * height) / number_of_squares)
    #[allow(clippy::cast_possible_truncation)] // ok, because we don't care about exact values
    let granularity = (f64::from(width * height) / NUMBER_OF_SQUARES).sqrt() as i32;

    max(1, granularity)
}

/// Get all spatial relations of a certain plant.
///
/// # Errors
/// * If the SQL query fails.
pub async fn find_relations(
    search_query: RelationSearchParameters,
    conn: &mut AsyncPgConnection,
) -> QueryResult<RelationsDto> {
    let query = spatial_relations::table
        .select((spatial_relations::plant2, spatial_relations::relation))
        .filter(spatial_relations::plant1.eq(&search_query.plant_id))
        .union(
            spatial_relations::table
                .select((spatial_relations::plant1, spatial_relations::relation))
                .filter(spatial_relations::plant2.eq(&search_query.plant_id)),
        );
    debug!("{}", debug_query::<Pg, _>(&query));
    let relations = query
        .load::<(i32, SpatialRelationType)>(conn)
        .await?
        .into_iter()
        .map(|(id, relation)| RelationDto { id, relation })
        .collect();
    Ok(RelationsDto {
        id: search_query.plant_id,
        relations,
    })
}