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::{auth::user_info::UserInfo, 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, MapCollaborator},
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    user_info: UserInfo,
39) -> Result<Page<MapDto>, ServiceError> {
40    let mut conn = pool.get().await?;
41    let collaborating_in = MapCollaborator::find_by_user_id(user_info.id, &mut conn).await?;
42    let result = Map::find(
43        search_parameters,
44        page_parameters,
45        &mut conn,
46        user_info,
47        collaborating_in,
48    )
49    .await?;
50    Ok(result)
51}
52
53/// Find a map by id in the database.
54///
55/// # Errors
56/// If the connection to the database could not be established.
57pub async fn find_by_id(id: i32, pool: &SharedPool) -> Result<MapDto, ServiceError> {
58    let mut conn = pool.get().await?;
59    let result = Map::find_by_id(id, &mut conn).await?;
60    Ok(result)
61}
62
63/// Create a new map in the database.
64///
65/// # Errors
66/// If the connection to the database could not be established.
67pub async fn create(
68    new_map: NewMapDto,
69    user_id: Uuid,
70    pool: &SharedPool,
71) -> Result<MapDto, ServiceError> {
72    let mut conn = pool.get().await?;
73
74    if Map::is_name_taken(&new_map.name, &mut conn).await? {
75        return Err(ServiceError::new(
76            StatusCode::CONFLICT,
77            "Map name already taken",
78        ));
79    }
80
81    let geometry_validation_result = is_valid_map_geometry(&new_map.geometry);
82    if let Some(error) = geometry_validation_result {
83        return Err(error);
84    }
85
86    let result = Map::create(new_map, user_id, &mut conn).await?;
87    for (layer_type, order_index) in LAYER_TYPES.iter().zip(0..) {
88        let new_layer = LayerDto {
89            id: Uuid::new_v4(),
90            type_: *layer_type,
91            name: format!("{layer_type} Layer"),
92            is_alternative: false,
93            map_id: result.id,
94            order_index,
95            marked_deleted: false,
96        };
97        let layer = Layer::create(result.id, new_layer, &mut conn).await?;
98
99        // Immediately initialize a base layer image,
100        // because the frontend would always have to create one
101        // anyway.
102        if layer.type_ == LayerType::Base {
103            BaseLayerImages::create(
104                BaseLayerImageDto {
105                    id: Uuid::new_v4(),
106                    layer_id: layer.id,
107                    path: String::new(),
108                    rotation: 0.0,
109                    scale: 100.0,
110                    x: 0,
111                    y: 0,
112                },
113                &mut conn,
114            )
115            .await?;
116        }
117    }
118
119    Ok(result)
120}
121
122/// Update a map in the database.
123/// Checks if the map is owned by the requesting user.
124///
125/// # Errors
126/// If the connection to the database could not be established.
127/// If the requesting user is not the owner of the map.
128pub async fn update(
129    map_update: UpdateMapDto,
130    id: i32,
131    pool: &SharedPool,
132) -> Result<MapDto, ServiceError> {
133    let mut conn = pool.get().await?;
134
135    let result = Map::update(map_update, id, &mut conn).await?;
136    Ok(result)
137}
138
139/// Update a maps geometry in the database.
140/// Checks if the map is owned by the requesting user.
141///
142/// # Errors
143/// * If the connection to the database could not be established.
144/// * If the requesting user is not the owner of the map.
145pub async fn update_geometry(
146    map_update_geometry: UpdateMapGeometryDto,
147    id: i32,
148    pool: &SharedPool,
149) -> Result<MapDto, ServiceError> {
150    let mut conn = pool.get().await?;
151
152    let geometry_validation_result = is_valid_map_geometry(&map_update_geometry.geometry);
153    if let Some(error) = geometry_validation_result {
154        return Err(error);
155    }
156
157    let result = Map::update_geometry(map_update_geometry, id, &mut conn).await?;
158    Ok(result)
159}
160
161/// Soft-deletes a map from the database.
162/// Checks if the map is owned by the requesting user.
163///
164/// # Errors
165/// * If the connection to the database could not be established.
166/// * If the requesting user is not the owner of the map.
167pub async fn delete_by_id(id: i32, pool: &SharedPool) -> Result<MapDto, ServiceError> {
168    let mut conn = pool.get().await?;
169
170    let result = Map::mark_for_deletion(id, &mut conn).await?;
171
172    Ok(result)
173}
174
175/// Checks if a Polygon can be used as a maps geometry attribute.
176fn is_valid_map_geometry(geometry: &Polygon<Point>) -> Option<ServiceError> {
177    if geometry.rings.len() != 1 {
178        return Some(ServiceError {
179            status_code: StatusCode::BAD_REQUEST,
180            reason: "Map geometry must have exactly one ring".to_owned(),
181        });
182    }
183
184    let geometry_points_length = geometry.rings.get(0).unwrap_or(&Vec::new()).len();
185
186    if geometry_points_length < 3 + 1 {
187        return Some(ServiceError {
188            status_code: StatusCode::BAD_REQUEST,
189            reason: "Map geometry must be a polygon of at least three points.".to_owned(),
190        });
191    }
192
193    None
194}