backend/service/
map.rs

1//! Service layer for maps.
2
3use actix_http::StatusCode;
4use postgis_diesel::types::{Point, Polygon};
5use uuid::Uuid;
6
7use crate::{
8    config::data::SharedPool,
9    error::ServiceError,
10    model::{
11        dto::{
12            base_layer_images::BaseLayerImageDto, layers::LayerDto, MapDto, MapSearchParameters,
13            NewMapDto, Page, PageParameters, UpdateMapDto, UpdateMapGeometryDto,
14        },
15        entity::{base_layer_images::BaseLayerImages, layers::Layer, Map},
16        r#enum::layer_type::LayerType,
17    },
18};
19
20/// Defines which layers should be created when a new map is created.
21const LAYER_TYPES: [LayerType; 6] = [
22    LayerType::Base,
23    LayerType::Drawing,
24    LayerType::Soiltexture,
25    LayerType::Hydrology,
26    LayerType::Shade,
27    LayerType::Plants,
28];
29
30/// Search maps from the database.
31///
32/// # Errors
33/// If the connection to the database could not be established.
34pub async fn find(
35    search_parameters: MapSearchParameters,
36    page_parameters: PageParameters,
37    pool: &SharedPool,
38) -> Result<Page<MapDto>, ServiceError> {
39    let mut conn = pool.get().await?;
40    let result = Map::find(search_parameters, page_parameters, &mut conn).await?;
41    Ok(result)
42}
43
44/// Find a map by id in the database.
45///
46/// # Errors
47/// If the connection to the database could not be established.
48pub async fn find_by_id(id: i32, pool: &SharedPool) -> Result<MapDto, ServiceError> {
49    let mut conn = pool.get().await?;
50    let result = Map::find_by_id(id, &mut conn).await?;
51    Ok(result)
52}
53
54/// Create a new map in the database.
55///
56/// # Errors
57/// If the connection to the database could not be established.
58pub async fn create(
59    new_map: NewMapDto,
60    user_id: Uuid,
61    pool: &SharedPool,
62) -> Result<MapDto, ServiceError> {
63    let mut conn = pool.get().await?;
64
65    if Map::is_name_taken(&new_map.name, &mut conn).await? {
66        return Err(ServiceError::new(
67            StatusCode::CONFLICT,
68            "Map name already taken",
69        ));
70    }
71
72    let geometry_validation_result = is_valid_map_geometry(&new_map.geometry);
73    if let Some(error) = geometry_validation_result {
74        return Err(error);
75    }
76
77    let result = Map::create(new_map, user_id, &mut conn).await?;
78    for (layer_type, order_index) in LAYER_TYPES.iter().zip(0..) {
79        let new_layer = LayerDto {
80            id: Uuid::new_v4(),
81            type_: *layer_type,
82            name: format!("{layer_type} Layer"),
83            is_alternative: false,
84            map_id: result.id,
85            order_index,
86            marked_deleted: false,
87        };
88        let layer = Layer::create(result.id, new_layer, &mut conn).await?;
89
90        // Immediately initialize a base layer image,
91        // because the frontend would always have to create one
92        // anyway.
93        if layer.type_ == LayerType::Base {
94            BaseLayerImages::create(
95                BaseLayerImageDto {
96                    id: Uuid::new_v4(),
97                    layer_id: layer.id,
98                    path: String::new(),
99                    rotation: 0.0,
100                    scale: 100.0,
101                    x: 0,
102                    y: 0,
103                },
104                &mut conn,
105            )
106            .await?;
107        }
108    }
109
110    Ok(result)
111}
112
113/// Update a map in the database.
114/// Checks if the map is owned by the requesting user.
115///
116/// # Errors
117/// If the connection to the database could not be established.
118/// If the requesting user is not the owner of the map.
119pub async fn update(
120    map_update: UpdateMapDto,
121    id: i32,
122    user_id: Uuid,
123    pool: &SharedPool,
124) -> Result<MapDto, ServiceError> {
125    let mut conn = pool.get().await?;
126    let map = Map::find_by_id(id, &mut conn).await?;
127    if map.created_by != user_id {
128        return Err(ServiceError {
129            status_code: StatusCode::FORBIDDEN,
130            reason: "no permission to update data".to_owned(),
131        });
132    }
133    let result = Map::update(map_update, id, &mut conn).await?;
134    Ok(result)
135}
136
137/// Update a maps geometry in the database.
138/// Checks if the map is owned by the requesting user.
139///
140/// # Errors
141/// * If the connection to the database could not be established.
142/// * If the requesting user is not the owner of the map.
143pub async fn update_geometry(
144    map_update_geometry: UpdateMapGeometryDto,
145    id: i32,
146    user_id: Uuid,
147    pool: &SharedPool,
148) -> Result<MapDto, ServiceError> {
149    let mut conn = pool.get().await?;
150    let map = Map::find_by_id(id, &mut conn).await?;
151    if map.created_by != user_id {
152        return Err(ServiceError {
153            status_code: StatusCode::FORBIDDEN,
154            reason: "no permission to update geometry".to_owned(),
155        });
156    }
157
158    let geometry_validation_result = is_valid_map_geometry(&map_update_geometry.geometry);
159    if let Some(error) = geometry_validation_result {
160        return Err(error);
161    }
162
163    let result = Map::update_geometry(map_update_geometry, id, &mut conn).await?;
164    Ok(result)
165}
166
167/// Soft-deletes a map from the database.
168/// Checks if the map is owned by the requesting user.
169///
170/// # Errors
171/// * If the connection to the database could not be established.
172/// * If the requesting user is not the owner of the map.
173pub async fn delete_by_id(
174    id: i32,
175    user_id: Uuid,
176    pool: &SharedPool,
177) -> Result<MapDto, ServiceError> {
178    let mut conn = pool.get().await?;
179
180    let map = Map::find_by_id(id, &mut conn).await?;
181    if map.created_by != user_id {
182        return Err(ServiceError {
183            status_code: StatusCode::FORBIDDEN,
184            reason: "no permission to delete map".to_owned(),
185        });
186    }
187
188    let result = Map::mark_for_deletion(id, &mut conn).await?;
189
190    Ok(result)
191}
192
193/// Checks if a Polygon can be used as a maps geometry attribute.
194fn is_valid_map_geometry(geometry: &Polygon<Point>) -> Option<ServiceError> {
195    if geometry.rings.len() != 1 {
196        return Some(ServiceError {
197            status_code: StatusCode::BAD_REQUEST,
198            reason: "Map geometry must have exactly one ring".to_owned(),
199        });
200    }
201
202    let geometry_points_length = geometry.rings.get(0).unwrap_or(&Vec::new()).len();
203
204    if geometry_points_length < 3 + 1 {
205        return Some(ServiceError {
206            status_code: StatusCode::BAD_REQUEST,
207            reason: "Map geometry must be a polygon of at least three points.".to_owned(),
208        });
209    }
210
211    None
212}