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: i64, 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::now_v7(),
90            type_: *layer_type,
91            name: format!("{layer_type} Layer"),
92            map_id: result.id,
93            order_index,
94            marked_deleted: false,
95        };
96        let layer = Layer::create(result.id.into(), new_layer, &mut conn).await?;
97
98        // Immediately initialize a base layer image,
99        // because the frontend would always have to create one
100        // anyway.
101        if layer.type_ == LayerType::Base {
102            BaseLayerImages::create(
103                BaseLayerImageDto {
104                    id: Uuid::now_v7(),
105                    layer_id: layer.id,
106                    path: String::new(),
107                    rotation: 0,
108                    scale: 100,
109                    x: 0,
110                    y: 0,
111                },
112                &mut conn,
113            )
114            .await?;
115        }
116    }
117
118    Ok(result)
119}
120
121/// Update a map in the database.
122/// Checks if the map is owned by the requesting user.
123///
124/// # Errors
125/// If the connection to the database could not be established.
126/// If the requesting user is not the owner of the map.
127pub async fn update(
128    map_update: UpdateMapDto,
129    id: i64,
130    pool: &SharedPool,
131) -> Result<MapDto, ServiceError> {
132    let mut conn = pool.get().await?;
133
134    let result = Map::update(map_update, id, &mut conn).await?;
135    Ok(result)
136}
137
138/// Update a maps geometry in the database.
139/// Checks if the map is owned by the requesting user.
140///
141/// # Errors
142/// * If the connection to the database could not be established.
143/// * If the requesting user is not the owner of the map.
144pub async fn update_geometry(
145    map_update_geometry: UpdateMapGeometryDto,
146    id: i64,
147    pool: &SharedPool,
148) -> Result<MapDto, ServiceError> {
149    let mut conn = pool.get().await?;
150
151    let geometry_validation_result = is_valid_map_geometry(&map_update_geometry.geometry);
152    if let Some(error) = geometry_validation_result {
153        return Err(error);
154    }
155
156    let result = Map::update_geometry(map_update_geometry, id, &mut conn).await?;
157    Ok(result)
158}
159
160/// Soft-deletes a map from the database.
161/// Checks if the map is owned by the requesting user.
162///
163/// # Errors
164/// * If the connection to the database could not be established.
165/// * If the requesting user is not the owner of the map.
166pub async fn delete_by_id(id: i64, pool: &SharedPool) -> Result<MapDto, ServiceError> {
167    let mut conn = pool.get().await?;
168
169    let result = Map::mark_for_deletion(id, &mut conn).await?;
170
171    Ok(result)
172}
173
174/// Checks if a Polygon can be used as a maps geometry attribute.
175fn is_valid_map_geometry(geometry: &Polygon<Point>) -> Option<ServiceError> {
176    if geometry.rings.len() != 1 {
177        return Some(ServiceError {
178            status_code: StatusCode::BAD_REQUEST,
179            reason: "Map geometry must have exactly one ring".to_owned(),
180        });
181    }
182
183    let geometry_points_length = geometry.rings.get(0).unwrap_or(&Vec::new()).len();
184
185    if geometry_points_length < 3 + 1 {
186        return Some(ServiceError {
187            status_code: StatusCode::BAD_REQUEST,
188            reason: "Map geometry must be a polygon of at least three points.".to_owned(),
189        });
190    }
191
192    None
193}