From 2a5724a489c51be89ac1dd473d73483f925917a9 Mon Sep 17 00:00:00 2001 From: kitzbergerg Date: Sat, 22 Jul 2023 12:38:28 +0200 Subject: [PATCH 001/157] #638 initial shadings endpoints --- .../2023-07-22-110000_shadings/down.sql | 4 + .../2023-07-22-110000_shadings/up.sql | 25 ++ backend/src/config/api_doc.rs | 45 +- backend/src/config/routes.rs | 29 +- backend/src/controller/mod.rs | 1 + backend/src/controller/shadings.rs | 181 ++++++++ backend/src/model/dto.rs | 2 + backend/src/model/dto/actions.rs | 209 +++++++-- backend/src/model/dto/shadings.rs | 135 ++++++ backend/src/model/dto/shadings_impl.rs | 53 +++ backend/src/model/entity.rs | 2 + backend/src/model/entity/shadings.rs | 42 ++ backend/src/model/entity/shadings_impl.rs | 97 +++++ backend/src/model/enum/shade.rs | 2 +- backend/src/schema.patch | 34 +- backend/src/service/mod.rs | 1 + backend/src/service/shadings.rs | 99 +++++ backend/src/test/mod.rs | 1 + backend/src/test/plantings.rs | 18 +- backend/src/test/shadings.rs | 395 ++++++++++++++++++ backend/src/test/util/data.rs | 54 ++- doc/backend/05updating_schema_patch.md | 5 +- 22 files changed, 1360 insertions(+), 74 deletions(-) create mode 100644 backend/migrations/2023-07-22-110000_shadings/down.sql create mode 100644 backend/migrations/2023-07-22-110000_shadings/up.sql create mode 100644 backend/src/controller/shadings.rs create mode 100644 backend/src/model/dto/shadings.rs create mode 100644 backend/src/model/dto/shadings_impl.rs create mode 100644 backend/src/model/entity/shadings.rs create mode 100644 backend/src/model/entity/shadings_impl.rs create mode 100644 backend/src/service/shadings.rs create mode 100644 backend/src/test/shadings.rs diff --git a/backend/migrations/2023-07-22-110000_shadings/down.sql b/backend/migrations/2023-07-22-110000_shadings/down.sql new file mode 100644 index 000000000..63d9b8550 --- /dev/null +++ b/backend/migrations/2023-07-22-110000_shadings/down.sql @@ -0,0 +1,4 @@ +-- This file should undo anything in `up.sql` +DROP TRIGGER check_shade_layer_type_before_insert_or_update ON plantings; +DROP FUNCTION check_shade_layer_type; +DROP TABLE shadings; diff --git a/backend/migrations/2023-07-22-110000_shadings/up.sql b/backend/migrations/2023-07-22-110000_shadings/up.sql new file mode 100644 index 000000000..109f259d0 --- /dev/null +++ b/backend/migrations/2023-07-22-110000_shadings/up.sql @@ -0,0 +1,25 @@ +-- Your SQL goes here +CREATE TABLE shadings ( + id uuid PRIMARY KEY, + layer_id integer NOT NULL, + shade_type shade NOT NULL, + geometry GEOMETRY (POLYGON, 4326) NOT NULL, + add_date date, + remove_date date, + FOREIGN KEY (layer_id) REFERENCES layers (id) ON DELETE CASCADE +); + +CREATE FUNCTION check_shade_layer_type() RETURNS trigger +LANGUAGE plpgsql +AS $$ +BEGIN + IF (SELECT type FROM layers WHERE id = NEW.layer_id) != 'shade' THEN + RAISE EXCEPTION 'Layer type must be "shade"'; + END IF; + RETURN NEW; +END; +$$; + +CREATE TRIGGER check_shade_layer_type_before_insert_or_update +BEFORE INSERT OR UPDATE ON shadings +FOR EACH ROW EXECUTE FUNCTION check_shade_layer_type(); diff --git a/backend/src/config/api_doc.rs b/backend/src/config/api_doc.rs index 8f83ee8db..6a27f525d 100644 --- a/backend/src/config/api_doc.rs +++ b/backend/src/config/api_doc.rs @@ -11,13 +11,18 @@ use super::auth::Config; use crate::{ controller::{ base_layer_image, config, layers, map, plant_layer, planting_suggestions, plantings, - plants, seed, + plants, seed, shadings, }, model::{ dto::{ plantings::{ - MovePlantingDto, NewPlantingDto, PlantingDto, TransformPlantingDto, - UpdatePlantingDto, + DeletePlantingDto, MovePlantingDto, NewPlantingDto, PlantingDto, + TransformPlantingDto, UpdateAddDatePlantingDto, UpdatePlantingDto, + UpdateRemoveDatePlantingDto, + }, + shadings::{ + DeleteShadingDto, NewShadingDto, ShadingDto, UpdateAddDateShadingDto, + UpdateRemoveDateShadingDto, UpdateShadingDto, UpdateValuesShadingDto, }, BaseLayerImageDto, ConfigDto, Coordinates, LayerDto, MapDto, NewLayerDto, NewMapDto, NewSeedDto, PageLayerDto, PageMapDto, PagePlantsSummaryDto, PageSeedDto, @@ -26,7 +31,7 @@ use crate::{ }, r#enum::{ privacy_option::PrivacyOption, quality::Quality, quantity::Quantity, - relation_type::RelationType, + relation_type::RelationType, shade::Shade, }, }, }; @@ -169,14 +174,43 @@ struct BaseLayerImagesApiDoc; PlantingDto, NewPlantingDto, UpdatePlantingDto, + DeletePlantingDto, TransformPlantingDto, - MovePlantingDto + MovePlantingDto, + UpdateAddDatePlantingDto, + UpdateRemoveDatePlantingDto ) ), modifiers(&SecurityAddon) )] struct PlantingsApiDoc; +/// Struct used by [`utoipa`] to generate `OpenApi` documentation for all shadings endpoints. +#[derive(OpenApi)] +#[openapi( + paths( + shadings::find, + shadings::create, + shadings::update, + shadings::delete + ), + components( + schemas( + ShadingDto, + NewShadingDto, + UpdateShadingDto, + DeleteShadingDto, + UpdateValuesShadingDto, + UpdateAddDateShadingDto, + UpdateRemoveDateShadingDto, + Shade + + ) + ), + modifiers(&SecurityAddon) +)] +struct ShadingsApiDoc; + /// Struct used by [`utoipa`] to generate `OpenApi` documentation for all suggestions endpoints. #[derive(OpenApi)] #[openapi( @@ -204,6 +238,7 @@ pub fn config(cfg: &mut web::ServiceConfig) { openapi.merge(PlantLayerApiDoc::openapi()); openapi.merge(BaseLayerImagesApiDoc::openapi()); openapi.merge(PlantingsApiDoc::openapi()); + openapi.merge(ShadingsApiDoc::openapi()); cfg.service(SwaggerUi::new("/doc/api/swagger/ui/{_:.*}").url("/doc/api/openapi.json", openapi)); } diff --git a/backend/src/config/routes.rs b/backend/src/config/routes.rs index b07661c7b..0fbd8a467 100644 --- a/backend/src/config/routes.rs +++ b/backend/src/config/routes.rs @@ -6,7 +6,7 @@ use actix_web_httpauth::middleware::HttpAuthentication; use crate::controller::{ base_layer_image, config, layers, map, plant_layer, planting_suggestions, plantings, plants, - seed, sse, + seed, shadings, sse, }; use super::auth::middleware::validator; @@ -41,13 +41,17 @@ pub fn config(cfg: &mut web::ServiceConfig) { .service(layers::create) .service(layers::delete) .service( - web::scope("/base/images") - .service(base_layer_image::create) - .service(base_layer_image::update) - .service(base_layer_image::delete), - ) - .service( - web::scope("/base/{layer_id}/images").service(base_layer_image::find), + web::scope("/base") + .service( + web::scope("/{layer_id}/images") + .service(base_layer_image::find), + ) + .service( + web::scope("/images") + .service(base_layer_image::create) + .service(base_layer_image::update) + .service(base_layer_image::delete), + ), ) .service( web::scope("/plants") @@ -63,6 +67,15 @@ pub fn config(cfg: &mut web::ServiceConfig) { .service(plantings::update) .service(plantings::delete), ), + ) + .service( + web::scope("/shade").service( + web::scope("/shadings") + .service(shadings::find) + .service(shadings::create) + .service(shadings::update) + .service(shadings::delete), + ), ), ), ) diff --git a/backend/src/controller/mod.rs b/backend/src/controller/mod.rs index 6834c867a..075271758 100644 --- a/backend/src/controller/mod.rs +++ b/backend/src/controller/mod.rs @@ -9,4 +9,5 @@ pub mod planting_suggestions; pub mod plantings; pub mod plants; pub mod seed; +pub mod shadings; pub mod sse; diff --git a/backend/src/controller/shadings.rs b/backend/src/controller/shadings.rs new file mode 100644 index 000000000..aebf98884 --- /dev/null +++ b/backend/src/controller/shadings.rs @@ -0,0 +1,181 @@ +//! `Shading` endpoints. + +use actix_web::{ + delete, get, patch, post, + web::{Data, Json, Path, Query}, + HttpResponse, Result, +}; +use uuid::Uuid; + +use crate::{ + config::auth::user_info::UserInfo, + config::data::AppDataInner, + model::dto::actions::{ + Action, CreateShadingActionPayload, DeleteShadingActionPayload, UpdateShadingActionPayload, + UpdateShadingAddDateActionPayload, UpdateShadingRemoveDateActionPayload, + }, +}; +use crate::{ + model::dto::shadings::{ + DeleteShadingDto, NewShadingDto, ShadingSearchParameters, UpdateShadingDto, + }, + service::shadings, +}; + +/// Endpoint for listing and filtering `Shading`. +/// +/// # Errors +/// * If the connection to the database could not be established. +#[utoipa::path( + context_path = "/api/maps/{map_id}/layers/shade/shadings", + params( + ("map_id" = i32, Path, description = "The id of the map the layer is on"), + ShadingSearchParameters + ), + responses( + (status = 200, description = "Find shadings", body = Vec) + ), + security( + ("oauth2" = []) + ) +)] +#[get("")] +pub async fn find( + search_params: Query, + app_data: Data, +) -> Result { + let response = shadings::find(search_params.into_inner(), &app_data).await?; + Ok(HttpResponse::Ok().json(response)) +} + +/// Endpoint for creating a new `Shading`. +/// +/// # Errors +/// * If the connection to the database could not be established. +#[utoipa::path( + context_path = "/api/maps/{map_id}/layers/shade/shadings", + params( + ("map_id" = i32, Path, description = "The id of the map the layer is on"), + ), + request_body = NewShadingDto, + responses( + (status = 201, description = "Create a shading", body = ShadingDto) + ), + security( + ("oauth2" = []) + ) +)] +#[post("")] +pub async fn create( + path: Path, + json: Json, + app_data: Data, + user_info: UserInfo, +) -> Result { + let new_shading = json.0; + let dto = shadings::create(new_shading.clone(), &app_data).await?; + + app_data + .broadcaster + .broadcast( + path.into_inner(), + Action::CreateShading(CreateShadingActionPayload::new( + dto.clone(), + user_info.id, + new_shading.action_id, + )), + ) + .await; + + Ok(HttpResponse::Created().json(dto)) +} + +/// Endpoint for updating a `Shading`. +/// +/// # Errors +/// * If the connection to the database could not be established. +#[utoipa::path( + context_path = "/api/maps/{map_id}/layers/shade/shadings", + params( + ("map_id" = i32, Path, description = "The id of the map the layer is on"), + ), + request_body = UpdateShadingDto, + responses( + (status = 200, description = "Update a shading", body = ShadingDto) + ), + security( + ("oauth2" = []) + ) +)] +#[patch("/{shading_id}")] +pub async fn update( + path: Path<(i32, Uuid)>, + json: Json, + app_data: Data, + user_info: UserInfo, +) -> Result { + let (map_id, shading_id) = path.into_inner(); + let update_shading = json.0; + + let shading = shadings::update(shading_id, update_shading.clone(), &app_data).await?; + + let action = match update_shading { + UpdateShadingDto::Update(action_dto) => Action::UpdateShading( + UpdateShadingActionPayload::new(shading.clone(), user_info.id, action_dto.action_id), + ), + UpdateShadingDto::UpdateAddDate(action_dto) => Action::UpdateShadingAddDate( + UpdateShadingAddDateActionPayload::new(&shading, user_info.id, action_dto.action_id), + ), + UpdateShadingDto::UpdateRemoveDate(action_dto) => Action::UpdateShadingRemoveDate( + UpdateShadingRemoveDateActionPayload::new(&shading, user_info.id, action_dto.action_id), + ), + }; + + app_data.broadcaster.broadcast(map_id, action).await; + + Ok(HttpResponse::Ok().json(shading)) +} + +/// Endpoint for deleting a `Shading`. +/// +/// # Errors +/// * If the connection to the database could not be established. +#[utoipa::path( + context_path = "/api/maps/{map_id}/layers/shade/shadings", + params( + ("map_id" = i32, Path, description = "The id of the map the layer is on"), + ), + request_body = DeleteShadingDto, + responses( + (status = 200, description = "Delete a shading") + ), + security( + ("oauth2" = []) + ) +)] +#[delete("/{shading_id}")] +pub async fn delete( + path: Path<(i32, Uuid)>, + json: Json, + app_data: Data, + user_info: UserInfo, +) -> Result { + let (map_id, shading_id) = path.into_inner(); + let delete_shading = json.0; + + shadings::delete_by_id(shading_id, &app_data).await?; + + app_data + .broadcaster + .broadcast( + map_id, + Action::DeleteShading(DeleteShadingActionPayload::new( + shading_id, + user_info.id, + delete_shading.action_id, + )), + ) + .await; + + Ok(HttpResponse::Ok().finish()) +} diff --git a/backend/src/model/dto.rs b/backend/src/model/dto.rs index edb5acf7e..501397d39 100644 --- a/backend/src/model/dto.rs +++ b/backend/src/model/dto.rs @@ -28,6 +28,8 @@ pub mod plantings; pub mod plantings_impl; pub mod plants_impl; pub mod seed_impl; +pub mod shadings; +pub mod shadings_impl; pub mod update_map_impl; /// Contains configuration the frontend needs to run. diff --git a/backend/src/model/dto/actions.rs b/backend/src/model/dto/actions.rs index 96e21d259..0872844c7 100644 --- a/backend/src/model/dto/actions.rs +++ b/backend/src/model/dto/actions.rs @@ -7,13 +7,14 @@ // Don't make the `new` functions const, there might come more fields in the future. #![allow(clippy::missing_const_for_fn)] -use crate::model::dto::plantings::PlantingDto; +use crate::model::{dto::plantings::PlantingDto, r#enum::shade::Shade}; use chrono::NaiveDate; +use postgis_diesel::types::{Point, Polygon}; use serde::Serialize; use typeshare::typeshare; use uuid::Uuid; -use super::BaseLayerImageDto; +use super::{shadings::ShadingDto, BaseLayerImageDto}; #[typeshare] #[derive(Debug, Serialize, Clone)] @@ -29,16 +30,28 @@ pub enum Action { MovePlanting(MovePlantActionPayload), /// An action used to broadcast transformation of a plant. TransformPlanting(TransformPlantActionPayload), + /// An action used to update the `add_date` of a plant. + UpdatePlantingAddDate(UpdatePlantingAddDateActionPayload), + /// An action used to update the `remove_date` of a plant. + UpdatePlantingRemoveDate(UpdatePlantingRemoveDateActionPayload), + + /// An action used to broadcast creation of a shading. + CreateShading(CreateShadingActionPayload), + /// An action used to broadcast deletion of a shading. + DeleteShading(DeleteShadingActionPayload), + /// An action used to broadcast movement of a shading. + UpdateShading(UpdateShadingActionPayload), + /// An action used to update the `add_date` of a shading. + UpdateShadingAddDate(UpdateShadingAddDateActionPayload), + /// An action used to update the `remove_date` of a shading. + UpdateShadingRemoveDate(UpdateShadingRemoveDateActionPayload), + /// An action used to broadcast creation of a baseLayerImage. CreateBaseLayerImage(CreateBaseLayerImageActionPayload), /// An action used to broadcast update of a baseLayerImage. UpdateBaseLayerImage(UpdateBaseLayerImageActionPayload), /// An action used to broadcast deletion of a baseLayerImage. DeleteBaseLayerImage(DeleteBaseLayerImageActionPayload), - /// An action used to update the `add_date` of a plant. - UpdatePlantingAddDate(UpdatePlantingAddDateActionPayload), - /// An action used to update the `remove_date` of a plant. - UpdatePlantingRemoveDate(UpdatePlantingRemoveDateActionPayload), } #[typeshare] @@ -164,45 +177,118 @@ impl TransformPlantActionPayload { #[typeshare] #[derive(Debug, Serialize, Clone)] -/// The payload of the [`Action::CreateBaseLayerImage`]. -/// This struct should always match [`BaseLayerImageDto`]. +/// The payload of the [`Action::UpdatePlantingAddDate`]. #[serde(rename_all = "camelCase")] -pub struct CreateBaseLayerImageActionPayload { +pub struct UpdatePlantingAddDateActionPayload { + user_id: Uuid, + action_id: Uuid, + id: Uuid, + add_date: Option, +} + +impl UpdatePlantingAddDateActionPayload { + #[must_use] + pub fn new(payload: PlantingDto, user_id: Uuid, action_id: Uuid) -> Self { + Self { + user_id, + action_id, + id: payload.id, + add_date: payload.add_date, + } + } +} + +#[typeshare] +#[derive(Debug, Serialize, Clone)] +/// The payload of the [`Action::UpdatePlantingRemoveDate`]. +#[serde(rename_all = "camelCase")] +pub struct UpdatePlantingRemoveDateActionPayload { + user_id: Uuid, + action_id: Uuid, + id: Uuid, + remove_date: Option, +} + +impl UpdatePlantingRemoveDateActionPayload { + #[must_use] + pub fn new(payload: PlantingDto, user_id: Uuid, action_id: Uuid) -> Self { + Self { + user_id, + action_id, + id: payload.id, + remove_date: payload.remove_date, + } + } +} + +#[typeshare] +#[derive(Debug, Serialize, Clone)] +/// The payload of the [`Action::CreateShading`]. +/// This struct should always match [`ShadingDto`]. +#[serde(rename_all = "camelCase")] +pub struct CreateShadingActionPayload { user_id: Uuid, action_id: Uuid, id: Uuid, layer_id: i32, - rotation: f32, - scale: f32, - path: String, + shade_type: Shade, + geometry: Polygon, + add_date: Option, + remove_date: Option, } -impl CreateBaseLayerImageActionPayload { +impl CreateShadingActionPayload { #[must_use] - pub fn new(payload: BaseLayerImageDto, user_id: Uuid, action_id: Uuid) -> Self { + pub fn new(payload: ShadingDto, user_id: Uuid, action_id: Uuid) -> Self { Self { user_id, action_id, id: payload.id, layer_id: payload.layer_id, - rotation: payload.rotation, - scale: payload.scale, - path: payload.path, + shade_type: payload.shade_type, + geometry: payload.geometry, + add_date: payload.add_date, + remove_date: payload.remove_date, } } } #[typeshare] #[derive(Debug, Serialize, Clone)] -/// The payload of the [`Action::DeleteBaseLayerImage`]. +/// The payload of the [`Action::UpdateShading`]. #[serde(rename_all = "camelCase")] -pub struct DeleteBaseLayerImageActionPayload { +pub struct UpdateShadingActionPayload { user_id: Uuid, action_id: Uuid, id: Uuid, + shade_type: Shade, + geometry: Polygon, } -impl DeleteBaseLayerImageActionPayload { +impl UpdateShadingActionPayload { + #[must_use] + pub fn new(payload: ShadingDto, user_id: Uuid, action_id: Uuid) -> Self { + Self { + user_id, + action_id, + id: payload.id, + shade_type: payload.shade_type, + geometry: payload.geometry, + } + } +} + +#[typeshare] +#[derive(Debug, Serialize, Clone)] +/// The payload of the [`Action::DeleteShading`]. +#[serde(rename_all = "camelCase")] +pub struct DeleteShadingActionPayload { + user_id: Uuid, + action_id: Uuid, + id: Uuid, +} + +impl DeleteShadingActionPayload { #[must_use] pub fn new(id: Uuid, user_id: Uuid, action_id: Uuid) -> Self { Self { @@ -215,9 +301,56 @@ impl DeleteBaseLayerImageActionPayload { #[typeshare] #[derive(Debug, Serialize, Clone)] -/// The payload of the [`Action::UpdateBaseLayerImage`]. +/// The payload of the [`Action::UpdateShadingAddDate`]. #[serde(rename_all = "camelCase")] -pub struct UpdateBaseLayerImageActionPayload { +pub struct UpdateShadingAddDateActionPayload { + user_id: Uuid, + action_id: Uuid, + id: Uuid, + add_date: Option, +} + +impl UpdateShadingAddDateActionPayload { + #[must_use] + pub fn new(payload: &ShadingDto, user_id: Uuid, action_id: Uuid) -> Self { + Self { + user_id, + action_id, + id: payload.id, + add_date: payload.add_date, + } + } +} + +#[typeshare] +#[derive(Debug, Serialize, Clone)] +/// The payload of the [`Action::UpdateShadingRemoveDate`]. +#[serde(rename_all = "camelCase")] +pub struct UpdateShadingRemoveDateActionPayload { + user_id: Uuid, + action_id: Uuid, + id: Uuid, + remove_date: Option, +} + +impl UpdateShadingRemoveDateActionPayload { + #[must_use] + pub fn new(payload: &ShadingDto, user_id: Uuid, action_id: Uuid) -> Self { + Self { + user_id, + action_id, + id: payload.id, + remove_date: payload.remove_date, + } + } +} + +#[typeshare] +#[derive(Debug, Serialize, Clone)] +/// The payload of the [`Action::CreateBaseLayerImage`]. +/// This struct should always match [`BaseLayerImageDto`]. +#[serde(rename_all = "camelCase")] +pub struct CreateBaseLayerImageActionPayload { user_id: Uuid, action_id: Uuid, id: Uuid, @@ -227,7 +360,7 @@ pub struct UpdateBaseLayerImageActionPayload { path: String, } -impl UpdateBaseLayerImageActionPayload { +impl CreateBaseLayerImageActionPayload { #[must_use] pub fn new(payload: BaseLayerImageDto, user_id: Uuid, action_id: Uuid) -> Self { Self { @@ -244,46 +377,50 @@ impl UpdateBaseLayerImageActionPayload { #[typeshare] #[derive(Debug, Serialize, Clone)] -/// The payload of the [`Action::UpdatePlantingAddDate`]. +/// The payload of the [`Action::DeleteBaseLayerImage`]. #[serde(rename_all = "camelCase")] -pub struct UpdatePlantingAddDateActionPayload { +pub struct DeleteBaseLayerImageActionPayload { user_id: Uuid, action_id: Uuid, id: Uuid, - add_date: Option, } -impl UpdatePlantingAddDateActionPayload { +impl DeleteBaseLayerImageActionPayload { #[must_use] - pub fn new(payload: PlantingDto, user_id: Uuid, action_id: Uuid) -> Self { + pub fn new(id: Uuid, user_id: Uuid, action_id: Uuid) -> Self { Self { user_id, action_id, - id: payload.id, - add_date: payload.add_date, + id, } } } #[typeshare] #[derive(Debug, Serialize, Clone)] -/// The payload of the [`Action::UpdatePlantingRemoveDate`]. +/// The payload of the [`Action::UpdateBaseLayerImage`]. #[serde(rename_all = "camelCase")] -pub struct UpdatePlantingRemoveDateActionPayload { +pub struct UpdateBaseLayerImageActionPayload { user_id: Uuid, action_id: Uuid, id: Uuid, - remove_date: Option, + layer_id: i32, + rotation: f32, + scale: f32, + path: String, } -impl UpdatePlantingRemoveDateActionPayload { +impl UpdateBaseLayerImageActionPayload { #[must_use] - pub fn new(payload: PlantingDto, user_id: Uuid, action_id: Uuid) -> Self { + pub fn new(payload: BaseLayerImageDto, user_id: Uuid, action_id: Uuid) -> Self { Self { user_id, action_id, id: payload.id, - remove_date: payload.remove_date, + layer_id: payload.layer_id, + rotation: payload.rotation, + scale: payload.scale, + path: payload.path, } } } diff --git a/backend/src/model/dto/shadings.rs b/backend/src/model/dto/shadings.rs new file mode 100644 index 000000000..09d557b23 --- /dev/null +++ b/backend/src/model/dto/shadings.rs @@ -0,0 +1,135 @@ +//! All DTOs associated with [`ShadingDto`]. + +use chrono::NaiveDate; +use postgis_diesel::types::{Point, Polygon}; +use serde::{Deserialize, Serialize}; +use typeshare::typeshare; +use utoipa::{IntoParams, ToSchema}; +use uuid::Uuid; + +use crate::model::r#enum::shade::Shade; + +/// Represents shade on a map. +#[typeshare] +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct ShadingDto { + /// The id of the shading. + pub id: Uuid, + /// The layer the shadings is on. + pub layer_id: i32, + /// The type/strength of shade. + pub shade_type: Shade, + /// The position of the shade on the map. + /// + /// E.g. `{"rings": [[{"x": 0.0,"y": 0.0},{"x": 1000.0,"y": 0.0},{"x": 1000.0,"y": 1000.0},{"x": 0.0,"y": 1000.0},{"x": 0.0,"y": 0.0}]],"srid": 4326}` + #[typeshare(serialized_as = "object")] + #[schema(value_type = Object)] + pub geometry: Polygon, + /// The date the shading was added to the map. + /// If None, the shading always existed. + pub add_date: Option, + /// The date the shading was removed from the map. + /// If None, the shading is still on the map. + pub remove_date: Option, +} + +/// Used to create a new shading. +#[typeshare] +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct NewShadingDto { + /// The id of the shading. + pub id: Option, + /// The plant layer the shadings is on. + pub layer_id: i32, + /// The type/strength of shade. + pub shade_type: Shade, + /// The position of the shade on the map. + /// + /// E.g. `{"rings": [[{"x": 0.0,"y": 0.0},{"x": 1000.0,"y": 0.0},{"x": 1000.0,"y": 1000.0},{"x": 0.0,"y": 1000.0},{"x": 0.0,"y": 0.0}]],"srid": 4326}` + #[typeshare(serialized_as = "object")] + #[schema(value_type = Object)] + pub geometry: Polygon, + /// The date the shading was added to the map. + /// If None, the shading always existed. + pub add_date: Option, + /// Id of the action (for identifying the action in the frontend). + pub action_id: Uuid, +} + +/// Used to differentiate between different update operations on shadings. +/// +/// Ordering of enum variants is important. +/// Serde will try to deserialize starting from the top. +#[typeshare] +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[serde(tag = "type", content = "content")] +pub enum UpdateShadingDto { + /// Update values of a shading. + Update(UpdateValuesShadingDto), + /// Change the `add_date` of a shading. + UpdateAddDate(UpdateAddDateShadingDto), + /// Change the `remove_date` of a shading. + UpdateRemoveDate(UpdateRemoveDateShadingDto), +} + +/// Used to update the values of an existing shading. +#[typeshare] +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +pub struct UpdateValuesShadingDto { + /// The type/strength of shade. + pub shade_type: Option, + /// The position of the shade on the map. + /// + /// E.g. `{"rings": [[{"x": 0.0,"y": 0.0},{"x": 1000.0,"y": 0.0},{"x": 1000.0,"y": 1000.0},{"x": 0.0,"y": 1000.0},{"x": 0.0,"y": 0.0}]],"srid": 4326}` + #[typeshare(serialized_as = "Option")] + #[schema(value_type = Option)] + pub geometry: Option>, + /// Id of the action (for identifying the action in the frontend). + pub action_id: Uuid, +} + +/// Used to change the `add_date` of a shading. +#[typeshare] +#[derive(Debug, Clone, Copy, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct UpdateAddDateShadingDto { + /// The date the shading was added to the map. + /// If None, the shading always existed. + pub add_date: Option, + /// Id of the action (for identifying the action in the frontend). + pub action_id: Uuid, +} + +/// Used to change the `remove_date` of a shading. +#[typeshare] +#[derive(Debug, Clone, Copy, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct UpdateRemoveDateShadingDto { + /// The date the shading was removed from the map. + /// If None, the shading is still on the map. + pub remove_date: Option, + /// Id of the action (for identifying the action in the frontend). + pub action_id: Uuid, +} + +/// Used to delete a shading. +/// The id of the shading is passed in the path. +#[typeshare] +#[derive(Debug, Clone, Copy, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct DeleteShadingDto { + /// Id of the action (for identifying the action in the frontend). + pub action_id: Uuid, +} + +/// Query parameters for searching shadings. +#[typeshare] +#[derive(Debug, Deserialize, IntoParams)] +pub struct ShadingSearchParameters { + /// The id of the layer the shading is placed on. + pub layer_id: Option, + /// Shadings that exist around this date are returned. + pub relative_to_date: NaiveDate, +} diff --git a/backend/src/model/dto/shadings_impl.rs b/backend/src/model/dto/shadings_impl.rs new file mode 100644 index 000000000..dfce29d3d --- /dev/null +++ b/backend/src/model/dto/shadings_impl.rs @@ -0,0 +1,53 @@ +//! Contains the implementations related to [`ShadingDto`]. + +use uuid::Uuid; + +use crate::model::entity::shadings::{Shading, UpdateShading}; + +use super::shadings::{NewShadingDto, ShadingDto, UpdateShadingDto}; + +impl From for ShadingDto { + fn from(entity: Shading) -> Self { + Self { + id: entity.id, + layer_id: entity.layer_id, + shade_type: entity.shade_type, + geometry: entity.geometry, + add_date: entity.add_date, + remove_date: entity.remove_date, + } + } +} + +impl From for Shading { + fn from(dto: NewShadingDto) -> Self { + Self { + id: dto.id.unwrap_or_else(Uuid::new_v4), + layer_id: dto.layer_id, + shade_type: dto.shade_type, + geometry: dto.geometry, + add_date: dto.add_date, + remove_date: None, + } + } +} + +impl From for UpdateShading { + fn from(dto: UpdateShadingDto) -> Self { + match dto { + UpdateShadingDto::Update(dto) => Self { + shade_type: dto.shade_type, + geometry: dto.geometry, + ..Default::default() + }, + UpdateShadingDto::UpdateAddDate(dto) => Self { + add_date: Some(dto.add_date), + ..Default::default() + }, + UpdateShadingDto::UpdateRemoveDate(dto) => Self { + remove_date: Some(dto.remove_date), + ..Default::default() + }, + } + } +} diff --git a/backend/src/model/entity.rs b/backend/src/model/entity.rs index 87f5d664d..326b3150d 100644 --- a/backend/src/model/entity.rs +++ b/backend/src/model/entity.rs @@ -8,6 +8,8 @@ pub mod plantings; pub mod plantings_impl; pub mod plants_impl; pub mod seed_impl; +pub mod shadings; +pub mod shadings_impl; use chrono::NaiveDate; use chrono::NaiveDateTime; diff --git a/backend/src/model/entity/shadings.rs b/backend/src/model/entity/shadings.rs new file mode 100644 index 000000000..92f73c8bb --- /dev/null +++ b/backend/src/model/entity/shadings.rs @@ -0,0 +1,42 @@ +//! All entities associated with [`Shading`]. + +use chrono::NaiveDate; +use diesel::{AsChangeset, Identifiable, Insertable, Queryable}; +use postgis_diesel::types::{Point, Polygon}; +use uuid::Uuid; + +use crate::{model::r#enum::shade::Shade, schema::shadings}; + +/// The `Shading` entity. +#[derive(Debug, Clone, Identifiable, Queryable, Insertable)] +#[diesel(table_name = shadings)] +pub struct Shading { + /// The id of the shading. + pub id: Uuid, + /// The plant layer the shadings is on. + pub layer_id: i32, + /// The type/strength of shade. + pub shade_type: Shade, + /// The position of the shade on the map. + pub geometry: Polygon, + /// The date the shading was added to the map. + /// If None, the shading always existed. + pub add_date: Option, + /// The date the shading was removed from the map. + /// If None, the shading is still on the map. + pub remove_date: Option, +} + +/// The `UpdateShading` entity. +#[derive(Debug, Clone, Default, AsChangeset)] +#[diesel(table_name = shadings)] +pub struct UpdateShading { + /// The type/strength of shade. + pub shade_type: Option, + /// The position of the shade on the map. + pub geometry: Option>, + /// The date the shading was added to the map. + pub add_date: Option>, + /// The date the shading was removed from the map. + pub remove_date: Option>, +} diff --git a/backend/src/model/entity/shadings_impl.rs b/backend/src/model/entity/shadings_impl.rs new file mode 100644 index 000000000..40cb2ba06 --- /dev/null +++ b/backend/src/model/entity/shadings_impl.rs @@ -0,0 +1,97 @@ +//! Contains the implementation of [`Shading`]. + +use chrono::NaiveDate; +use diesel::pg::Pg; +use diesel::{debug_query, BoolExpressionMethods, ExpressionMethods, QueryDsl, QueryResult}; +use diesel_async::{AsyncPgConnection, RunQueryDsl}; +use log::debug; +use uuid::Uuid; + +use crate::model::dto::shadings::{NewShadingDto, ShadingDto, UpdateShadingDto}; +use crate::model::entity::shadings::{Shading, UpdateShading}; +use crate::schema::shadings::{self, all_columns, layer_id}; + +/// Arguments for the database layer find shadings function. +pub struct FindShadingsParameters { + /// The id of the layer to find shadings for. + pub layer_id: Option, + /// First date in the time frame shadings are searched for. + pub from: NaiveDate, + /// Last date in the time frame shadings are searched for. + pub to: NaiveDate, +} + +impl Shading { + /// Get all shadings associated with the query. + /// + /// # Errors + /// * Unknown, diesel doesn't say why it might error. + pub async fn find( + search_parameters: FindShadingsParameters, + conn: &mut AsyncPgConnection, + ) -> QueryResult> { + let mut query = shadings::table.select(all_columns).into_boxed(); + + if let Some(id) = search_parameters.layer_id { + query = query.filter(layer_id.eq(id)); + } + + let shadings_added_before_date = shadings::add_date + .is_null() + .or(shadings::add_date.lt(search_parameters.to)); + let shadings_removed_after_date = shadings::remove_date + .is_null() + .or(shadings::remove_date.gt(search_parameters.from)); + + query = query.filter(shadings_added_before_date.and(shadings_removed_after_date)); + + debug!("{}", debug_query::(&query)); + + Ok(query + .load::(conn) + .await? + .into_iter() + .map(Into::into) + .collect()) + } + + /// Create a new shading in the database. + /// + /// # Errors + /// * If the `layer_id` references a layer that is not of type `plant`. + /// * Unknown, diesel doesn't say why it might error. + pub async fn create( + dto: NewShadingDto, + conn: &mut AsyncPgConnection, + ) -> QueryResult { + let shading = Self::from(dto); + let query = diesel::insert_into(shadings::table).values(&shading); + debug!("{}", debug_query::(&query)); + query.get_result::(conn).await.map(Into::into) + } + + /// Partially update a shading in the database. + /// + /// # Errors + /// * Unknown, diesel doesn't say why it might error. + pub async fn update( + shading_id: Uuid, + dto: UpdateShadingDto, + conn: &mut AsyncPgConnection, + ) -> QueryResult { + let shading = UpdateShading::from(dto); + let query = diesel::update(shadings::table.find(shading_id)).set(&shading); + debug!("{}", debug_query::(&query)); + query.get_result::(conn).await.map(Into::into) + } + + /// Delete the shading from the database. + /// + /// # Errors + /// * Unknown, diesel doesn't say why it might error. + pub async fn delete_by_id(id: Uuid, conn: &mut AsyncPgConnection) -> QueryResult { + let query = diesel::delete(shadings::table.find(id)); + debug!("{}", debug_query::(&query)); + query.execute(conn).await + } +} diff --git a/backend/src/model/enum/shade.rs b/backend/src/model/enum/shade.rs index 862aeb9d0..c5acbea0e 100644 --- a/backend/src/model/enum/shade.rs +++ b/backend/src/model/enum/shade.rs @@ -7,7 +7,7 @@ use utoipa::ToSchema; #[allow(clippy::missing_docs_in_private_items)] // TODO: See #97. #[typeshare] -#[derive(Serialize, Deserialize, DbEnum, Debug, ToSchema)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, DbEnum, ToSchema)] #[ExistingTypePath = "crate::schema::sql_types::Shade"] pub enum Shade { #[serde(rename = "no shade")] diff --git a/backend/src/schema.patch b/backend/src/schema.patch index 88f0f4345..83018fb56 100644 --- a/backend/src/schema.patch +++ b/backend/src/schema.patch @@ -1,15 +1,12 @@ -diff --git a/backend/src/schema.rs b/backend/src/schema.rs 2023-07-20 -index 54f26f46..68427977 100644 ---- a/backend/src/schema.rs -+++ b/backend/src/schema.rs -@@ -10,20 +10,12 @@ pub mod sql_types { - pub struct ExternalSource; +--- src/schema.rs 2023-07-22 11:22:05.218010765 +0200 ++++ src/schema_tmp.rs 2023-07-22 11:25:09.654549966 +0200 +@@ -11,20 +11,12 @@ #[derive(diesel::sql_types::SqlType)] #[diesel(postgres_type(name = "fertility"))] pub struct Fertility; -- #[derive(diesel::sql_types::SqlType)] + #[derive(diesel::sql_types::SqlType)] - #[diesel(postgres_type(name = "geography"))] - pub struct Geography; - @@ -17,13 +14,14 @@ index 54f26f46..68427977 100644 - #[diesel(postgres_type(name = "geometry"))] - pub struct Geometry; - - #[derive(diesel::sql_types::SqlType)] +- #[derive(diesel::sql_types::SqlType)] #[diesel(postgres_type(name = "growth_rate"))] pub struct GrowthRate; #[derive(diesel::sql_types::SqlType)] #[diesel(postgres_type(name = "herbaceous_or_woody"))] -@@ -100,16 +92,15 @@ diesel::table! { + pub struct HerbaceousOrWoody; +@@ -113,16 +105,15 @@ is_alternative -> Bool, } } @@ -41,3 +39,21 @@ index 54f26f46..68427977 100644 name -> Text, creation_date -> Date, deletion_date -> Nullable, +@@ -256,16 +247,16 @@ + plant_id -> Nullable, + owner_id -> Uuid, + } + } + + diesel::table! { ++ use postgis_diesel::sql_types::Geometry; + use postgis_diesel::sql_types::Geography; + use diesel::sql_types::*; + use super::sql_types::Shade; +- use super::sql_types::Geometry; + + shadings (id) { + id -> Uuid, + layer_id -> Int4, + shade_type -> Shade, + geometry -> Geometry, diff --git a/backend/src/service/mod.rs b/backend/src/service/mod.rs index c8ac6ec5c..dd8d81aed 100644 --- a/backend/src/service/mod.rs +++ b/backend/src/service/mod.rs @@ -7,4 +7,5 @@ pub mod plant_layer; pub mod plantings; pub mod plants; pub mod seed; +pub mod shadings; pub mod util; diff --git a/backend/src/service/shadings.rs b/backend/src/service/shadings.rs new file mode 100644 index 000000000..353be5811 --- /dev/null +++ b/backend/src/service/shadings.rs @@ -0,0 +1,99 @@ +//! Service layer for shadings. + +use actix_http::StatusCode; +use actix_web::web::Data; +use chrono::Days; +use uuid::Uuid; + +use crate::config::data::AppDataInner; +use crate::error::ServiceError; +use crate::model::dto::shadings::{ + NewShadingDto, ShadingDto, ShadingSearchParameters, UpdateShadingDto, +}; +use crate::model::dto::TimelinePage; +use crate::model::entity::shadings::Shading; +use crate::model::entity::shadings_impl::FindShadingsParameters; + +/// Time offset in days for loading shadings in the timeline. +pub const TIME_LINE_LOADING_OFFSET_DAYS: u64 = 356; + +/// Search shadings from the database. +/// +/// # Errors +/// If the connection to the database could not be established. +pub async fn find( + search_parameters: ShadingSearchParameters, + app_data: &Data, +) -> Result, ServiceError> { + let mut conn = app_data.pool.get().await?; + + let from = search_parameters + .relative_to_date + .checked_sub_days(Days::new(TIME_LINE_LOADING_OFFSET_DAYS)) + .ok_or_else(|| { + ServiceError::new( + StatusCode::BAD_REQUEST, + "Could not add days to relative_to_date".into(), + ) + })?; + + let to = search_parameters + .relative_to_date + .checked_add_days(Days::new(TIME_LINE_LOADING_OFFSET_DAYS)) + .ok_or_else(|| { + ServiceError::new( + StatusCode::BAD_REQUEST, + "Could not add days to relative_to_date".into(), + ) + })?; + + let search_parameters = FindShadingsParameters { + layer_id: search_parameters.layer_id, + from, + to, + }; + let result = Shading::find(search_parameters, &mut conn).await?; + + Ok(TimelinePage { + results: result, + from, + to, + }) +} + +/// Create a new shading in the database. +/// +/// # Errors +/// If the connection to the database could not be established. +pub async fn create( + dto: NewShadingDto, + app_data: &Data, +) -> Result { + let mut conn = app_data.pool.get().await?; + let result = Shading::create(dto, &mut conn).await?; + Ok(result) +} + +/// Update the shading in the database. +/// +/// # Errors +/// If the connection to the database could not be established. +pub async fn update( + id: Uuid, + dto: UpdateShadingDto, + app_data: &Data, +) -> Result { + let mut conn = app_data.pool.get().await?; + let result = Shading::update(id, dto, &mut conn).await?; + Ok(result) +} + +/// Delete the shading from the database. +/// +/// # Errors +/// If the connection to the database could not be established. +pub async fn delete_by_id(id: Uuid, app_data: &Data) -> Result<(), ServiceError> { + let mut conn = app_data.pool.get().await?; + let _ = Shading::delete_by_id(id, &mut conn).await?; + Ok(()) +} diff --git a/backend/src/test/mod.rs b/backend/src/test/mod.rs index 0e7ad24ad..a72938dbe 100644 --- a/backend/src/test/mod.rs +++ b/backend/src/test/mod.rs @@ -14,4 +14,5 @@ pub mod plant_layer_heatmap; mod planting_suggestions; mod plantings; mod seed; +mod shadings; pub mod util; diff --git a/backend/src/test/plantings.rs b/backend/src/test/plantings.rs index 40f18029b..8fbf28128 100644 --- a/backend/src/test/plantings.rs +++ b/backend/src/test/plantings.rs @@ -34,8 +34,8 @@ async fn test_can_search_plantings() { .await?; diesel::insert_into(crate::schema::layers::table) .values(vec![ - data::TestInsertableLayer::default(), - data::TestInsertableLayer { + data::TestInsertablePlantLayer::default(), + data::TestInsertablePlantLayer { id: -2, name: "Test Layer 2".to_owned(), is_alternative: true, @@ -102,7 +102,7 @@ async fn test_create_fails_with_invalid_layer() { .execute(conn) .await?; diesel::insert_into(crate::schema::layers::table) - .values(data::TestInsertableLayer { + .values(data::TestInsertablePlantLayer { type_: LayerType::Base, ..Default::default() }) @@ -152,7 +152,7 @@ async fn test_can_create_plantings() { .execute(conn) .await?; diesel::insert_into(crate::schema::layers::table) - .values(data::TestInsertableLayer::default()) + .values(data::TestInsertablePlantLayer::default()) .execute(conn) .await?; diesel::insert_into(crate::schema::plants::table) @@ -200,7 +200,7 @@ async fn test_can_update_plantings() { .execute(conn) .await?; diesel::insert_into(crate::schema::layers::table) - .values(data::TestInsertableLayer::default()) + .values(data::TestInsertablePlantLayer::default()) .execute(conn) .await?; diesel::insert_into(crate::schema::plants::table) @@ -253,7 +253,7 @@ async fn test_can_delete_planting() { .execute(conn) .await?; diesel::insert_into(crate::schema::layers::table) - .values(data::TestInsertableLayer::default()) + .values(data::TestInsertablePlantLayer::default()) .execute(conn) .await?; diesel::insert_into(crate::schema::plants::table) @@ -309,7 +309,7 @@ async fn test_removed_planting_outside_loading_offset_is_not_in_timeline() { .execute(conn) .await?; diesel::insert_into(crate::schema::layers::table) - .values(data::TestInsertableLayer::default()) + .values(data::TestInsertablePlantLayer::default()) .execute(conn) .await?; diesel::insert_into(crate::schema::plants::table) @@ -359,7 +359,7 @@ async fn test_removed_planting_inside_loading_offset_is_in_timeline() { .execute(conn) .await?; diesel::insert_into(crate::schema::layers::table) - .values(data::TestInsertableLayer::default()) + .values(data::TestInsertablePlantLayer::default()) .execute(conn) .await?; diesel::insert_into(crate::schema::plants::table) @@ -408,7 +408,7 @@ async fn test_added_planting_outside_loading_offset_is_not_in_timeline() { .execute(conn) .await?; diesel::insert_into(crate::schema::layers::table) - .values(data::TestInsertableLayer::default()) + .values(data::TestInsertablePlantLayer::default()) .execute(conn) .await?; diesel::insert_into(crate::schema::plants::table) diff --git a/backend/src/test/shadings.rs b/backend/src/test/shadings.rs new file mode 100644 index 000000000..4efa85ec1 --- /dev/null +++ b/backend/src/test/shadings.rs @@ -0,0 +1,395 @@ +//! Tests for [`crate::controller::shadings`]. + +use std::ops::Add; + +use actix_http::StatusCode; +use actix_web::{http::header, test}; +use chrono::{Days, NaiveDate}; +use diesel_async::{scoped_futures::ScopedFutureExt, RunQueryDsl}; +use uuid::Uuid; + +use crate::{ + model::{ + dto::{ + shadings::{ + DeleteShadingDto, NewShadingDto, ShadingDto, UpdateShadingDto, + UpdateValuesShadingDto, + }, + TimelinePage, + }, + r#enum::{layer_type::LayerType, shade::Shade}, + }, + service::shadings::TIME_LINE_LOADING_OFFSET_DAYS, + test::util::{data, dummy_map_polygons::small_rectangle}, +}; + +use crate::test::util::{init_test_app, init_test_database}; + +#[actix_rt::test] +async fn test_can_search_shadings() { + let pool = init_test_database(|conn| { + async { + diesel::insert_into(crate::schema::maps::table) + .values(data::TestInsertableMap::default()) + .execute(conn) + .await?; + diesel::insert_into(crate::schema::layers::table) + .values(vec![ + data::TestInsertableShadeLayer::default(), + data::TestInsertableShadeLayer { + id: -2, + name: "Test Layer 2".to_owned(), + is_alternative: true, + ..Default::default() + }, + ]) + .execute(conn) + .await?; + diesel::insert_into(crate::schema::shadings::table) + .values(vec![ + data::TestInsertableShading { + id: Uuid::new_v4(), + layer_id: -1, + ..Default::default() + }, + data::TestInsertableShading { + id: Uuid::new_v4(), + layer_id: -2, + ..Default::default() + }, + ]) + .execute(conn) + .await?; + Ok(()) + } + .scope_boxed() + }) + .await; + let (token, app) = init_test_app(pool.clone()).await; + + let resp = test::TestRequest::get() + .uri("/api/maps/-1/layers/shade/shadings?layer_id=-1&relative_to_date=2023-05-08") + .insert_header((header::AUTHORIZATION, token.clone())) + .send_request(&app) + .await; + assert_eq!(resp.status(), StatusCode::OK); + + let page: TimelinePage = test::read_body_json(resp).await; + assert_eq!(page.results.len(), 1); + + let resp = test::TestRequest::get() + .uri("/api/maps/-1/layers/shade/shadings?relative_to_date=2023-05-08") + .insert_header((header::AUTHORIZATION, token)) + .send_request(&app) + .await; + assert_eq!(resp.status(), StatusCode::OK); + + let page: TimelinePage = test::read_body_json(resp).await; + assert_eq!(page.results.len(), 2); +} + +#[actix_rt::test] +async fn test_create_fails_with_invalid_layer() { + let pool = init_test_database(|conn| { + async { + diesel::insert_into(crate::schema::maps::table) + .values(data::TestInsertableMap::default()) + .execute(conn) + .await?; + diesel::insert_into(crate::schema::layers::table) + .values(data::TestInsertableShadeLayer { + type_: LayerType::Base, + ..Default::default() + }) + .execute(conn) + .await?; + Ok(()) + } + .scope_boxed() + }) + .await; + let (token, app) = init_test_app(pool.clone()).await; + + let new_shading = NewShadingDto { + id: Some(Uuid::new_v4()), + action_id: Uuid::new_v4(), + shade_type: Shade::LightShade, + geometry: small_rectangle(), + layer_id: -1, + add_date: None, + }; + + let resp = test::TestRequest::post() + .uri("/api/maps/-1/layers/shade/shadings") + .insert_header((header::AUTHORIZATION, token)) + .set_json(new_shading) + .send_request(&app) + .await; + assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); +} + +#[actix_rt::test] +async fn test_can_create_shadings() { + let pool = init_test_database(|conn| { + async { + diesel::insert_into(crate::schema::maps::table) + .values(data::TestInsertableMap::default()) + .execute(conn) + .await?; + diesel::insert_into(crate::schema::layers::table) + .values(data::TestInsertableShadeLayer::default()) + .execute(conn) + .await?; + Ok(()) + } + .scope_boxed() + }) + .await; + let (token, app) = init_test_app(pool.clone()).await; + + let new_shading = NewShadingDto { + id: Some(Uuid::new_v4()), + action_id: Uuid::new_v4(), + layer_id: -1, + shade_type: Shade::LightShade, + geometry: small_rectangle(), + add_date: None, + }; + + let resp = test::TestRequest::post() + .uri("/api/maps/-1/layers/shade/shadings") + .insert_header((header::AUTHORIZATION, token)) + .set_json(new_shading) + .send_request(&app) + .await; + assert_eq!(resp.status(), StatusCode::CREATED); +} + +#[actix_rt::test] +async fn test_can_update_shadings() { + let shading_id = Uuid::new_v4(); + let pool = init_test_database(|conn| { + async { + diesel::insert_into(crate::schema::maps::table) + .values(data::TestInsertableMap::default()) + .execute(conn) + .await?; + diesel::insert_into(crate::schema::layers::table) + .values(data::TestInsertableShadeLayer::default()) + .execute(conn) + .await?; + diesel::insert_into(crate::schema::shadings::table) + .values(data::TestInsertableShading { + id: shading_id, + ..Default::default() + }) + .execute(conn) + .await?; + Ok(()) + } + .scope_boxed() + }) + .await; + let (token, app) = init_test_app(pool.clone()).await; + + let update_data = UpdateValuesShadingDto { + shade_type: Some(Shade::PermanentDeepShade), + geometry: None, + action_id: Uuid::new_v4(), + }; + let update_object = UpdateShadingDto::Update(update_data); + + let resp = test::TestRequest::patch() + .uri(&format!("/api/maps/-1/layers/shade/shadings/{shading_id}")) + .insert_header((header::AUTHORIZATION, token)) + .set_json(update_object) + .send_request(&app) + .await; + assert_eq!(resp.status(), StatusCode::OK); + + let shading: ShadingDto = test::read_body_json(resp).await; + assert_eq!(shading.shade_type, Shade::PermanentDeepShade); +} + +#[actix_rt::test] +async fn test_can_delete_shading() { + let shading_id = Uuid::new_v4(); + let pool = init_test_database(|conn| { + async { + diesel::insert_into(crate::schema::maps::table) + .values(data::TestInsertableMap::default()) + .execute(conn) + .await?; + diesel::insert_into(crate::schema::layers::table) + .values(data::TestInsertableShadeLayer::default()) + .execute(conn) + .await?; + diesel::insert_into(crate::schema::shadings::table) + .values(data::TestInsertableShading { + id: shading_id, + ..Default::default() + }) + .execute(conn) + .await?; + Ok(()) + } + .scope_boxed() + }) + .await; + let (token, app) = init_test_app(pool.clone()).await; + + let resp = test::TestRequest::delete() + .uri(&format!("/api/maps/-1/layers/shade/shadings/{shading_id}",)) + .insert_header((header::AUTHORIZATION, token.clone())) + .set_json(DeleteShadingDto { + action_id: Uuid::new_v4(), + }) + .send_request(&app) + .await; + assert_eq!(resp.status(), StatusCode::OK); + + let resp = test::TestRequest::get() + .uri("/api/maps/-1/layers/shade/shadings?relative_to_date=2023-05-08") + .insert_header((header::AUTHORIZATION, token)) + .send_request(&app) + .await; + assert_eq!(resp.status(), StatusCode::OK); + + let page: TimelinePage = test::read_body_json(resp).await; + assert_eq!(page.results.len(), 0); +} + +#[actix_rt::test] +async fn test_removed_shading_outside_loading_offset_is_not_in_timeline() { + let shading_id = Uuid::new_v4(); + let remove_date = NaiveDate::from_ymd_opt(2022, 1, 1).expect("date is valid"); + + let pool = init_test_database(|conn| { + async { + diesel::insert_into(crate::schema::maps::table) + .values(data::TestInsertableMap::default()) + .execute(conn) + .await?; + diesel::insert_into(crate::schema::layers::table) + .values(data::TestInsertableShadeLayer::default()) + .execute(conn) + .await?; + diesel::insert_into(crate::schema::shadings::table) + .values(data::TestInsertableShading { + id: shading_id, + remove_date: Some(remove_date), + ..Default::default() + }) + .execute(conn) + .await?; + Ok(()) + } + .scope_boxed() + }) + .await; + let (token, app) = init_test_app(pool.clone()).await; + + let resp = test::TestRequest::get() + .uri(&format!( + "/api/maps/-1/layers/shade/shadings?relative_to_date={}", + remove_date + .add(Days::new(TIME_LINE_LOADING_OFFSET_DAYS)) + .format("%Y-%m-%d"), + )) + .insert_header((header::AUTHORIZATION, token)) + .send_request(&app) + .await; + assert_eq!(resp.status(), StatusCode::OK); + + let page: TimelinePage = test::read_body_json(resp).await; + assert_eq!(page.results.len(), 0); +} + +#[actix_rt::test] +async fn test_removed_shading_inside_loading_offset_is_in_timeline() { + let shading_id = Uuid::new_v4(); + let remove_date = NaiveDate::from_ymd_opt(2022, 1, 1).expect("date is valid"); + + let pool = init_test_database(|conn| { + async { + diesel::insert_into(crate::schema::maps::table) + .values(data::TestInsertableMap::default()) + .execute(conn) + .await?; + diesel::insert_into(crate::schema::layers::table) + .values(data::TestInsertableShadeLayer::default()) + .execute(conn) + .await?; + diesel::insert_into(crate::schema::shadings::table) + .values(data::TestInsertableShading { + id: shading_id, + remove_date: Some(remove_date), + ..Default::default() + }) + .execute(conn) + .await?; + Ok(()) + } + .scope_boxed() + }) + .await; + let (token, app) = init_test_app(pool.clone()).await; + + let resp = test::TestRequest::get() + .uri(&format!( + "/api/maps/-1/layers/shade/shadings?relative_to_date={}", + remove_date.add(Days::new(1)).format("%Y-%m-%d"), + )) + .insert_header((header::AUTHORIZATION, token)) + .send_request(&app) + .await; + assert_eq!(resp.status(), StatusCode::OK); + + let page: TimelinePage = test::read_body_json(resp).await; + assert_eq!(page.results.len(), 1); +} + +#[actix_rt::test] +async fn test_added_shading_outside_loading_offset_is_not_in_timeline() { + let shading_id = Uuid::new_v4(); + let current_date = NaiveDate::from_ymd_opt(2022, 1, 1).expect("date is valid"); + let add_date = current_date.add(Days::new(TIME_LINE_LOADING_OFFSET_DAYS)); + + let pool = init_test_database(|conn| { + async { + diesel::insert_into(crate::schema::maps::table) + .values(data::TestInsertableMap::default()) + .execute(conn) + .await?; + diesel::insert_into(crate::schema::layers::table) + .values(data::TestInsertableShadeLayer::default()) + .execute(conn) + .await?; + diesel::insert_into(crate::schema::shadings::table) + .values(data::TestInsertableShading { + id: shading_id, + add_date: Some(add_date), + ..Default::default() + }) + .execute(conn) + .await?; + Ok(()) + } + .scope_boxed() + }) + .await; + let (token, app) = init_test_app(pool.clone()).await; + + let resp = test::TestRequest::get() + .uri(&format!( + "/api/maps/-1/layers/shade/shadings?relative_to_date={}", + current_date.format("%Y-%m-%d"), + )) + .insert_header((header::AUTHORIZATION, token)) + .send_request(&app) + .await; + assert_eq!(resp.status(), StatusCode::OK); + + let page: TimelinePage = test::read_body_json(resp).await; + assert_eq!(page.results.len(), 0); +} diff --git a/backend/src/test/util/data.rs b/backend/src/test/util/data.rs index 9003edb6b..d2bd64d41 100644 --- a/backend/src/test/util/data.rs +++ b/backend/src/test/util/data.rs @@ -5,9 +5,9 @@ use diesel::Insertable; use postgis_diesel::types::{Point, Polygon}; use uuid::Uuid; -use crate::model::r#enum::{layer_type::LayerType, privacy_option::PrivacyOption}; +use crate::model::r#enum::{layer_type::LayerType, privacy_option::PrivacyOption, shade::Shade}; -use super::dummy_map_polygons::tall_rectangle; +use super::dummy_map_polygons::{small_rectangle, tall_rectangle}; #[derive(Insertable)] #[diesel(table_name = crate::schema::maps)] @@ -45,7 +45,7 @@ impl Default for TestInsertableMap { #[derive(Insertable)] #[diesel(table_name = crate::schema::layers)] -pub struct TestInsertableLayer { +pub struct TestInsertablePlantLayer { pub id: i32, pub map_id: i32, pub type_: LayerType, @@ -53,7 +53,7 @@ pub struct TestInsertableLayer { pub is_alternative: bool, } -impl Default for TestInsertableLayer { +impl Default for TestInsertablePlantLayer { fn default() -> Self { Self { id: -1, @@ -65,6 +65,28 @@ impl Default for TestInsertableLayer { } } +#[derive(Insertable)] +#[diesel(table_name = crate::schema::layers)] +pub struct TestInsertableShadeLayer { + pub id: i32, + pub map_id: i32, + pub type_: LayerType, + pub name: String, + pub is_alternative: bool, +} + +impl Default for TestInsertableShadeLayer { + fn default() -> Self { + Self { + id: -1, + map_id: -1, + type_: LayerType::Shade, + name: "Test Layer 1".to_owned(), + is_alternative: false, + } + } +} + #[derive(Insertable)] #[diesel(table_name = crate::schema::plants)] pub struct TestInsertablePlant { @@ -118,3 +140,27 @@ impl Default for TestInsertablePlanting { } } } + +#[derive(Insertable)] +#[diesel(table_name = crate::schema::shadings)] +pub struct TestInsertableShading { + pub id: Uuid, + pub layer_id: i32, + pub shade_type: Shade, + pub geometry: Polygon, + pub add_date: Option, + pub remove_date: Option, +} + +impl Default for TestInsertableShading { + fn default() -> Self { + Self { + id: Uuid::default(), + layer_id: -1, + shade_type: Shade::NoShade, + geometry: small_rectangle(), + add_date: None, + remove_date: None, + } + } +} diff --git a/doc/backend/05updating_schema_patch.md b/doc/backend/05updating_schema_patch.md index 8499c44ec..00a39a7f3 100644 --- a/doc/backend/05updating_schema_patch.md +++ b/doc/backend/05updating_schema_patch.md @@ -17,7 +17,8 @@ This document explains how to update the `schema.patch` file used by diesel. You should now have a generated `schema.rs` in the backend src folder. 3. Copy the `schema.rs` file e.g. to `schema_tmp.rs`. -4. Run `` diff src/schema.rs `src/schema_tmp.rs` -U6 `` in the backend folder and save the result to the `src/schema.patch` file. -5. Add `patch_file = "src/schema.patch"` to the `diesel.toml` again. +4. Update the `schema_tmp.rs` to fit your needs. +5. Run `` diff src/schema.rs `src/schema_tmp.rs` -U6 `` in the backend folder and save the result to the `src/schema.patch` file. +6. Add `patch_file = "src/schema.patch"` to the `diesel.toml` again. From now on the newly generated patch file should be used by diesel. From 140f98f6ae0a63037d167e0da87f1cb730918874 Mon Sep 17 00:00:00 2001 From: kitzbergerg Date: Sat, 22 Jul 2023 12:52:04 +0200 Subject: [PATCH 002/157] refactor SQL calculate_heatmap to instead call function calculate_score --- .../2023-07-03-165000_heatmap/down.sql | 3 +- .../2023-07-03-165000_heatmap/up.sql | 70 ++++++++++++++----- backend/src/model/entity/plant_layer.rs | 19 ++--- 3 files changed, 65 insertions(+), 27 deletions(-) diff --git a/backend/migrations/2023-07-03-165000_heatmap/down.sql b/backend/migrations/2023-07-03-165000_heatmap/down.sql index 6316827e0..54ee815b7 100644 --- a/backend/migrations/2023-07-03-165000_heatmap/down.sql +++ b/backend/migrations/2023-07-03-165000_heatmap/down.sql @@ -1,7 +1,8 @@ -- This file should undo anything in `up.sql` DROP FUNCTION get_plant_relations; DROP FUNCTION calculate_score_from_relations; -DROP FUNCTION scale_score; DROP FUNCTION calculate_score; +DROP FUNCTION scale_score; +DROP FUNCTION calculate_heatmap; DROP FUNCTION calculate_bbox; ALTER TABLE maps DROP COLUMN geometry; diff --git a/backend/migrations/2023-07-03-165000_heatmap/up.sql b/backend/migrations/2023-07-03-165000_heatmap/up.sql index 2d4633edd..655aee06f 100644 --- a/backend/migrations/2023-07-03-165000_heatmap/up.sql +++ b/backend/migrations/2023-07-03-165000_heatmap/up.sql @@ -1,10 +1,10 @@ -- Your SQL goes here -ALTER TABLE maps ADD COLUMN geometry GEOMETRY(POLYGON, 4326) NOT NULL; +ALTER TABLE maps ADD COLUMN geometry GEOMETRY (POLYGON, 4326) NOT NULL; -- Calculate the bounding box of the map geometry. CREATE OR REPLACE FUNCTION calculate_bbox(map_id INTEGER) -RETURNS TABLE(x_min INTEGER, y_min INTEGER, x_max INTEGER, y_max INTEGER) AS $$ +RETURNS TABLE (x_min INTEGER, y_min INTEGER, x_max INTEGER, y_max INTEGER) AS $$ BEGIN RETURN QUERY SELECT @@ -26,17 +26,29 @@ $$ LANGUAGE plpgsql; -- Values where the plant should not be placed are close to 0. -- Values where the plant should be placed are close to 1. -- --- The resulting matrix does not contain valid (x,y) coordinates, instead (x,y) are simply the indices in the matrix. --- The (x,y) coordinate of the computed heatmap always starts at (0,0) no matter the boundaries of the map. --- To get valid coordinates the user would therefore need to move and scale the calculated heatmap by taking into account the boundaries of the map. +-- The resulting matrix does not contain valid (x,y) coordinates, +-- instead (x,y) are simply the indices in the matrix. +-- The (x,y) coordinate of the computed heatmap always starts at +-- (0,0) no matter the boundaries of the map. +-- To get valid coordinates the user would therefore need to move and scale the +-- calculated heatmap by taking into account the boundaries of the map. -- --- p_map_id ... the maps id --- p_layer_id ... the id of the plant layer --- p_plant_id ... the id of the plant for which to consider relations --- granularity ... the resolution of the map (float greater than 0) --- x_min, y_min, x_max, y_max ... the boundaries of the map -CREATE OR REPLACE FUNCTION calculate_score(p_map_id INTEGER, p_layer_id INTEGER, p_plant_id INTEGER, granularity INTEGER, x_min INTEGER, y_min INTEGER, x_max INTEGER, y_max INTEGER) -RETURNS TABLE(score REAL, x INTEGER, y INTEGER) AS $$ +-- p_map_id ... map id +-- p_layer_id ... id of the plant layer +-- p_plant_id ... id of the plant for which to consider relations +-- granularity ... resolution of the map (float greater than 0) +-- x_min,y_min,x_max,y_max ... boundaries of the map +CREATE OR REPLACE FUNCTION calculate_heatmap( + p_map_id INTEGER, + p_layer_id INTEGER, + p_plant_id INTEGER, + granularity INTEGER, + x_min INTEGER, + y_min INTEGER, + x_max INTEGER, + y_max INTEGER +) +RETURNS TABLE (score REAL, x INTEGER, y INTEGER) AS $$ DECLARE map_geometry GEOMETRY(POLYGON, 4326); cell GEOMETRY; @@ -75,8 +87,7 @@ BEGIN -- If the square is on the map calculate a score; otherwise set score to 0. IF ST_Intersects(cell, map_geometry) THEN - score := 0.5 + calculate_score_from_relations(p_layer_id, p_plant_id, x_pos, y_pos); - -- TODO: add additional checks like shade + score := calculate_score(p_map_id, p_layer_id, p_plant_id, x_pos, y_pos); score := scale_score(score); -- scale the score to be between 0 and 1 ELSE score := 0.0; @@ -99,8 +110,31 @@ BEGIN END; $$ LANGUAGE plpgsql; +-- Calculate a score for a certain position. +CREATE OR REPLACE FUNCTION calculate_score( + p_map_id INTEGER, + p_layer_id INTEGER, + p_plant_id INTEGER, + x_pos INTEGER, + y_pos INTEGER +) +RETURNS FLOAT AS $$ +DECLARE + plant_relation RECORD; + distance REAL; + weight REAL; + score REAL := 0; +BEGIN + score := 0.5 + calculate_score_from_relations(p_layer_id, p_plant_id, x_pos, y_pos); + + RETURN score; +END; +$$ LANGUAGE plpgsql; + -- Calculate a score using the plants relations and their distances. -CREATE OR REPLACE FUNCTION calculate_score_from_relations(p_layer_id INTEGER, p_plant_id INTEGER, x_pos INTEGER, y_pos INTEGER) +CREATE OR REPLACE FUNCTION calculate_score_from_relations( + p_layer_id INTEGER, p_plant_id INTEGER, x_pos INTEGER, y_pos INTEGER +) RETURNS FLOAT AS $$ DECLARE plant_relation RECORD; @@ -128,8 +162,10 @@ END; $$ LANGUAGE plpgsql; -- Get all relations for the plant on the specified layer. -CREATE OR REPLACE FUNCTION get_plant_relations(p_layer_id INTEGER, p_plant_id INTEGER) -RETURNS TABLE(x INTEGER, y INTEGER, relation relation_type) AS $$ +CREATE OR REPLACE FUNCTION get_plant_relations( + p_layer_id INTEGER, p_plant_id INTEGER +) +RETURNS TABLE (x INTEGER, y INTEGER, relation RELATION_TYPE) AS $$ BEGIN RETURN QUERY -- We only need x,y and type of relation to calculate a score. diff --git a/backend/src/model/entity/plant_layer.rs b/backend/src/model/entity/plant_layer.rs index 738be10c3..a65656fda 100644 --- a/backend/src/model/entity/plant_layer.rs +++ b/backend/src/model/entity/plant_layer.rs @@ -75,15 +75,16 @@ pub async fn heatmap( let bounding_box = bounding_box_query.get_result::(conn).await?; // Fetch the heatmap - let query = diesel::sql_query("SELECT * FROM calculate_score($1, $2, $3, $4, $5, $6, $7, $8)") - .bind::(map_id) - .bind::(layer_id) - .bind::(plant_id) - .bind::(GRANULARITY) - .bind::(bounding_box.x_min) - .bind::(bounding_box.y_min) - .bind::(bounding_box.x_max) - .bind::(bounding_box.y_max); + let query = + diesel::sql_query("SELECT * FROM calculate_heatmap($1, $2, $3, $4, $5, $6, $7, $8)") + .bind::(map_id) + .bind::(layer_id) + .bind::(plant_id) + .bind::(GRANULARITY) + .bind::(bounding_box.x_min) + .bind::(bounding_box.y_min) + .bind::(bounding_box.x_max) + .bind::(bounding_box.y_max); debug!("{}", debug_query::(&query)); let result = query.load::(conn).await?; From 8bbc9c0cf34ffd89a6cef20a8178d8ea3a567b64 Mon Sep 17 00:00:00 2001 From: kitzbergerg Date: Sat, 22 Jul 2023 13:14:45 +0200 Subject: [PATCH 003/157] use array to store layer ids at dedicated positions --- .../2023-07-03-165000_heatmap/up.sql | 20 ++++++++++--------- backend/src/model/entity/plant_layer.rs | 4 ++-- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/backend/migrations/2023-07-03-165000_heatmap/up.sql b/backend/migrations/2023-07-03-165000_heatmap/up.sql index 655aee06f..c457997ac 100644 --- a/backend/migrations/2023-07-03-165000_heatmap/up.sql +++ b/backend/migrations/2023-07-03-165000_heatmap/up.sql @@ -34,13 +34,13 @@ $$ LANGUAGE plpgsql; -- calculated heatmap by taking into account the boundaries of the map. -- -- p_map_id ... map id --- p_layer_id ... id of the plant layer +-- p_layer_ids ... ids of the layers -- p_plant_id ... id of the plant for which to consider relations -- granularity ... resolution of the map (float greater than 0) -- x_min,y_min,x_max,y_max ... boundaries of the map CREATE OR REPLACE FUNCTION calculate_heatmap( p_map_id INTEGER, - p_layer_id INTEGER, + p_layer_ids INTEGER [], p_plant_id INTEGER, granularity INTEGER, x_min INTEGER, @@ -59,10 +59,12 @@ DECLARE y_pos INTEGER; plant_relation RECORD; BEGIN - -- Makes sure the plant layer exists and fits to the map - IF NOT EXISTS (SELECT 1 FROM layers WHERE id = p_layer_id AND type = 'plants' AND map_id = p_map_id) THEN - RAISE EXCEPTION 'Layer with id % not found', p_layer_id; - END IF; + -- Makes sure the layers exists and fits to the map + FOR i IN 1..array_length(p_layer_ids, 1) LOOP + IF NOT EXISTS (SELECT 1 FROM layers WHERE id = p_layer_ids[i] AND map_id = p_map_id) THEN + RAISE EXCEPTION 'Layer with id % not found on map', p_layer_ids[i]; + END IF; + END LOOP; -- Makes sure the plant exists IF NOT EXISTS (SELECT 1 FROM plants WHERE id = p_plant_id) THEN RAISE EXCEPTION 'Plant with id % not found', p_plant_id; @@ -87,7 +89,7 @@ BEGIN -- If the square is on the map calculate a score; otherwise set score to 0. IF ST_Intersects(cell, map_geometry) THEN - score := calculate_score(p_map_id, p_layer_id, p_plant_id, x_pos, y_pos); + score := calculate_score(p_map_id, p_layer_ids, p_plant_id, x_pos, y_pos); score := scale_score(score); -- scale the score to be between 0 and 1 ELSE score := 0.0; @@ -113,7 +115,7 @@ $$ LANGUAGE plpgsql; -- Calculate a score for a certain position. CREATE OR REPLACE FUNCTION calculate_score( p_map_id INTEGER, - p_layer_id INTEGER, + p_layer_ids INTEGER [], p_plant_id INTEGER, x_pos INTEGER, y_pos INTEGER @@ -125,7 +127,7 @@ DECLARE weight REAL; score REAL := 0; BEGIN - score := 0.5 + calculate_score_from_relations(p_layer_id, p_plant_id, x_pos, y_pos); + score := 0.5 + calculate_score_from_relations(p_layer_ids[1], p_plant_id, x_pos, y_pos); RETURN score; END; diff --git a/backend/src/model/entity/plant_layer.rs b/backend/src/model/entity/plant_layer.rs index a65656fda..282a6450a 100644 --- a/backend/src/model/entity/plant_layer.rs +++ b/backend/src/model/entity/plant_layer.rs @@ -3,7 +3,7 @@ use diesel::{ debug_query, pg::Pg, - sql_types::{Float, Integer}, + sql_types::{Array, Float, Integer}, CombineDsl, ExpressionMethods, QueryDsl, QueryResult, QueryableByName, }; use diesel_async::{AsyncPgConnection, RunQueryDsl}; @@ -78,7 +78,7 @@ pub async fn heatmap( let query = diesel::sql_query("SELECT * FROM calculate_heatmap($1, $2, $3, $4, $5, $6, $7, $8)") .bind::(map_id) - .bind::(layer_id) + .bind::, _>(vec![layer_id]) .bind::(plant_id) .bind::(GRANULARITY) .bind::(bounding_box.x_min) From 1317e34043a29adbbaf9dcdf9199d163f8b8556d Mon Sep 17 00:00:00 2001 From: kitzbergerg Date: Sat, 22 Jul 2023 16:33:09 +0200 Subject: [PATCH 004/157] #638 use shadings in heatmap calculation --- .../2023-07-03-165000_heatmap/up.sql | 17 +-- .../2023-07-22-110000_shadings/down.sql | 20 ++++ .../2023-07-22-110000_shadings/up.sql | 71 +++++++++++- backend/src/model/dto.rs | 4 +- backend/src/model/dto/actions.rs | 10 +- backend/src/model/dto/shadings.rs | 6 +- backend/src/model/dto/shadings_impl.rs | 6 +- backend/src/model/entity/plant_layer.rs | 6 +- backend/src/model/entity/shadings.rs | 4 +- backend/src/schema.patch | 2 +- backend/src/service/map.rs | 2 +- backend/src/service/plant_layer.rs | 3 +- backend/src/test/plant_layer_heatmap.rs | 104 +++++++++++++++--- backend/src/test/shadings.rs | 8 +- backend/src/test/util/data.rs | 4 +- backend/src/test/util/dummy_map_polygons.rs | 31 ++++++ .../features/maps/routes/MapCreateForm.tsx | 6 +- 17 files changed, 250 insertions(+), 54 deletions(-) diff --git a/backend/migrations/2023-07-03-165000_heatmap/up.sql b/backend/migrations/2023-07-03-165000_heatmap/up.sql index c457997ac..6469926a4 100644 --- a/backend/migrations/2023-07-03-165000_heatmap/up.sql +++ b/backend/migrations/2023-07-03-165000_heatmap/up.sql @@ -51,7 +51,7 @@ CREATE OR REPLACE FUNCTION calculate_heatmap( RETURNS TABLE (score REAL, x INTEGER, y INTEGER) AS $$ DECLARE map_geometry GEOMETRY(POLYGON, 4326); - cell GEOMETRY; + point GEOMETRY; bbox GEOMETRY; num_cols INTEGER; num_rows INTEGER; @@ -81,14 +81,14 @@ BEGIN FOR i IN 0..num_cols-1 LOOP FOR j IN 0..num_rows-1 LOOP -- i and j do not represent coordinates. We need to adjust them to actual coordinates. - x_pos := x_min + (i * granularity); - y_pos := y_min + (j * granularity); + x_pos := x_min + (i * granularity) + (granularity / 2); + y_pos := y_min + (j * granularity) + (granularity / 2); - -- Make a square the same size as the granularity - cell := ST_Translate(ST_GeomFromEWKT('SRID=4326;POLYGON((0 0, ' || granularity || ' 0, ' || granularity || ' ' || granularity || ', 0 ' || granularity || ', 0 0))'), x_pos, y_pos); + -- Create a point from x_pos and y_pos + point := ST_SetSRID(ST_MakePoint(x_pos, y_pos), 4326); -- If the square is on the map calculate a score; otherwise set score to 0. - IF ST_Intersects(cell, map_geometry) THEN + IF ST_Intersects(point, map_geometry) THEN score := calculate_score(p_map_id, p_layer_ids, p_plant_id, x_pos, y_pos); score := scale_score(score); -- scale the score to be between 0 and 1 ELSE @@ -113,6 +113,7 @@ END; $$ LANGUAGE plpgsql; -- Calculate a score for a certain position. +-- p_layer_ids[1] ... plant layer CREATE OR REPLACE FUNCTION calculate_score( p_map_id INTEGER, p_layer_ids INTEGER [], @@ -120,7 +121,7 @@ CREATE OR REPLACE FUNCTION calculate_score( x_pos INTEGER, y_pos INTEGER ) -RETURNS FLOAT AS $$ +RETURNS REAL AS $$ DECLARE plant_relation RECORD; distance REAL; @@ -137,7 +138,7 @@ $$ LANGUAGE plpgsql; CREATE OR REPLACE FUNCTION calculate_score_from_relations( p_layer_id INTEGER, p_plant_id INTEGER, x_pos INTEGER, y_pos INTEGER ) -RETURNS FLOAT AS $$ +RETURNS REAL AS $$ DECLARE plant_relation RECORD; distance REAL; diff --git a/backend/migrations/2023-07-22-110000_shadings/down.sql b/backend/migrations/2023-07-22-110000_shadings/down.sql index 63d9b8550..c42ce75cc 100644 --- a/backend/migrations/2023-07-22-110000_shadings/down.sql +++ b/backend/migrations/2023-07-22-110000_shadings/down.sql @@ -1,4 +1,24 @@ -- This file should undo anything in `up.sql` +CREATE OR REPLACE FUNCTION calculate_score( + p_map_id INTEGER, + p_layer_ids INTEGER [], + p_plant_id INTEGER, + x_pos INTEGER, + y_pos INTEGER +) +RETURNS REAL AS $$ +DECLARE + plant_relation RECORD; + distance REAL; + weight REAL; + score REAL := 0; +BEGIN + score := 0.5 + calculate_score_from_relations(p_layer_ids[1], p_plant_id, x_pos, y_pos); + + RETURN score; +END; +$$ LANGUAGE plpgsql; + DROP TRIGGER check_shade_layer_type_before_insert_or_update ON plantings; DROP FUNCTION check_shade_layer_type; DROP TABLE shadings; diff --git a/backend/migrations/2023-07-22-110000_shadings/up.sql b/backend/migrations/2023-07-22-110000_shadings/up.sql index 109f259d0..e535e089f 100644 --- a/backend/migrations/2023-07-22-110000_shadings/up.sql +++ b/backend/migrations/2023-07-22-110000_shadings/up.sql @@ -2,7 +2,7 @@ CREATE TABLE shadings ( id uuid PRIMARY KEY, layer_id integer NOT NULL, - shade_type shade NOT NULL, + shade shade NOT NULL, geometry GEOMETRY (POLYGON, 4326) NOT NULL, add_date date, remove_date date, @@ -23,3 +23,72 @@ $$; CREATE TRIGGER check_shade_layer_type_before_insert_or_update BEFORE INSERT OR UPDATE ON shadings FOR EACH ROW EXECUTE FUNCTION check_shade_layer_type(); + +-- Calculate a score for a certain position. +-- p_layer_ids[1] ... plant layer +-- p_layer_ids[2] ... shade layer +CREATE OR REPLACE FUNCTION calculate_score( + p_map_id integer, + p_layer_ids integer [], + p_plant_id integer, + x_pos integer, + y_pos integer +) +RETURNS real AS $$ +DECLARE + plant_relation RECORD; + distance REAL; + weight REAL; + score REAL := 0; +BEGIN + score := 0.5 + + calculate_score_from_relations(p_layer_ids[1], p_plant_id, x_pos, y_pos) + + calculate_score_from_shadings(p_layer_ids[2], p_plant_id, x_pos, y_pos); + + RETURN score; +END; +$$ LANGUAGE plpgsql; + +-- Calculate a score using the shadings. +CREATE FUNCTION calculate_score_from_shadings( + p_layer_id integer, p_plant_id integer, x_pos integer, y_pos integer +) +RETURNS real AS $$ +DECLARE + point GEOMETRY; + plant_shade shade; + shading_shade shade; + all_values shade[]; + pos1 INTEGER; + pos2 INTEGER; +BEGIN + -- Get the preferred shade of the plant + SELECT shade INTO plant_shade + FROM plants + WHERE id = p_plant_id; + + -- Create a point from x_pos and y_pos + point := ST_SetSRID(ST_MakePoint(x_pos, y_pos), 4326); + -- Select the shading with the darkest shade that intersects the point + SELECT shade INTO shading_shade + FROM shadings + WHERE layer_id = p_layer_id AND ST_Intersects(geometry, point) + ORDER BY shade DESC + LIMIT 1; + + -- If there's no shading, return 0 + IF NOT FOUND OR plant_shade IS NULL THEN + RETURN 0; + END IF; + + -- Get all possible enum values + SELECT enum_range(NULL::shade) INTO all_values; + + -- Get the position of each enum value in the array + SELECT array_position(all_values, plant_shade) INTO pos1; + SELECT array_position(all_values, shading_shade) INTO pos2; + + -- Calculate the 'distance' to the preferred shade as a values between 0.5 and -0.5 + RETURN 0.5 - (abs(pos1 - pos2) / (ARRAY_LENGTH(all_values, 1) - 1)::REAL); +END; +$$ LANGUAGE plpgsql; diff --git a/backend/src/model/dto.rs b/backend/src/model/dto.rs index 501397d39..5377b4136 100644 --- a/backend/src/model/dto.rs +++ b/backend/src/model/dto.rs @@ -448,7 +448,9 @@ pub struct DeleteBaseLayerImageDto { #[derive(Debug, Deserialize, IntoParams)] pub struct HeatMapQueryParams { /// The id of the plant layer the planting will be planted on. - pub layer_id: i32, + pub plant_layer_id: i32, + /// The id of the shade layer the planting will be planted on. + pub shade_layer_id: i32, /// The id of the plant you want to plant. pub plant_id: i32, } diff --git a/backend/src/model/dto/actions.rs b/backend/src/model/dto/actions.rs index 0872844c7..66324f64d 100644 --- a/backend/src/model/dto/actions.rs +++ b/backend/src/model/dto/actions.rs @@ -231,7 +231,8 @@ pub struct CreateShadingActionPayload { action_id: Uuid, id: Uuid, layer_id: i32, - shade_type: Shade, + shade: Shade, + #[typeshare(serialized_as = "object")] geometry: Polygon, add_date: Option, remove_date: Option, @@ -245,7 +246,7 @@ impl CreateShadingActionPayload { action_id, id: payload.id, layer_id: payload.layer_id, - shade_type: payload.shade_type, + shade: payload.shade, geometry: payload.geometry, add_date: payload.add_date, remove_date: payload.remove_date, @@ -261,7 +262,8 @@ pub struct UpdateShadingActionPayload { user_id: Uuid, action_id: Uuid, id: Uuid, - shade_type: Shade, + shade: Shade, + #[typeshare(serialized_as = "object")] geometry: Polygon, } @@ -272,7 +274,7 @@ impl UpdateShadingActionPayload { user_id, action_id, id: payload.id, - shade_type: payload.shade_type, + shade: payload.shade, geometry: payload.geometry, } } diff --git a/backend/src/model/dto/shadings.rs b/backend/src/model/dto/shadings.rs index 09d557b23..463fa0e35 100644 --- a/backend/src/model/dto/shadings.rs +++ b/backend/src/model/dto/shadings.rs @@ -19,7 +19,7 @@ pub struct ShadingDto { /// The layer the shadings is on. pub layer_id: i32, /// The type/strength of shade. - pub shade_type: Shade, + pub shade: Shade, /// The position of the shade on the map. /// /// E.g. `{"rings": [[{"x": 0.0,"y": 0.0},{"x": 1000.0,"y": 0.0},{"x": 1000.0,"y": 1000.0},{"x": 0.0,"y": 1000.0},{"x": 0.0,"y": 0.0}]],"srid": 4326}` @@ -44,7 +44,7 @@ pub struct NewShadingDto { /// The plant layer the shadings is on. pub layer_id: i32, /// The type/strength of shade. - pub shade_type: Shade, + pub shade: Shade, /// The position of the shade on the map. /// /// E.g. `{"rings": [[{"x": 0.0,"y": 0.0},{"x": 1000.0,"y": 0.0},{"x": 1000.0,"y": 1000.0},{"x": 0.0,"y": 1000.0},{"x": 0.0,"y": 0.0}]],"srid": 4326}` @@ -79,7 +79,7 @@ pub enum UpdateShadingDto { #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct UpdateValuesShadingDto { /// The type/strength of shade. - pub shade_type: Option, + pub shade: Option, /// The position of the shade on the map. /// /// E.g. `{"rings": [[{"x": 0.0,"y": 0.0},{"x": 1000.0,"y": 0.0},{"x": 1000.0,"y": 1000.0},{"x": 0.0,"y": 1000.0},{"x": 0.0,"y": 0.0}]],"srid": 4326}` diff --git a/backend/src/model/dto/shadings_impl.rs b/backend/src/model/dto/shadings_impl.rs index dfce29d3d..c6dd9599f 100644 --- a/backend/src/model/dto/shadings_impl.rs +++ b/backend/src/model/dto/shadings_impl.rs @@ -11,7 +11,7 @@ impl From for ShadingDto { Self { id: entity.id, layer_id: entity.layer_id, - shade_type: entity.shade_type, + shade: entity.shade, geometry: entity.geometry, add_date: entity.add_date, remove_date: entity.remove_date, @@ -24,7 +24,7 @@ impl From for Shading { Self { id: dto.id.unwrap_or_else(Uuid::new_v4), layer_id: dto.layer_id, - shade_type: dto.shade_type, + shade: dto.shade, geometry: dto.geometry, add_date: dto.add_date, remove_date: None, @@ -36,7 +36,7 @@ impl From for UpdateShading { fn from(dto: UpdateShadingDto) -> Self { match dto { UpdateShadingDto::Update(dto) => Self { - shade_type: dto.shade_type, + shade: dto.shade, geometry: dto.geometry, ..Default::default() }, diff --git a/backend/src/model/entity/plant_layer.rs b/backend/src/model/entity/plant_layer.rs index 282a6450a..0bd17f273 100644 --- a/backend/src/model/entity/plant_layer.rs +++ b/backend/src/model/entity/plant_layer.rs @@ -64,7 +64,8 @@ struct HeatMapElement { )] pub async fn heatmap( map_id: i32, - layer_id: i32, + plant_layer_id: i32, + shade_layer_id: i32, plant_id: i32, conn: &mut AsyncPgConnection, ) -> QueryResult>> { @@ -78,7 +79,7 @@ pub async fn heatmap( let query = diesel::sql_query("SELECT * FROM calculate_heatmap($1, $2, $3, $4, $5, $6, $7, $8)") .bind::(map_id) - .bind::, _>(vec![layer_id]) + .bind::, _>(vec![plant_layer_id, shade_layer_id]) .bind::(plant_id) .bind::(GRANULARITY) .bind::(bounding_box.x_min) @@ -98,6 +99,7 @@ pub async fn heatmap( for HeatMapElement { score, x, y } in result { heatmap[y as usize][x as usize] = score; } + debug!("{heatmap:#?}"); Ok(heatmap) } diff --git a/backend/src/model/entity/shadings.rs b/backend/src/model/entity/shadings.rs index 92f73c8bb..307c75627 100644 --- a/backend/src/model/entity/shadings.rs +++ b/backend/src/model/entity/shadings.rs @@ -16,7 +16,7 @@ pub struct Shading { /// The plant layer the shadings is on. pub layer_id: i32, /// The type/strength of shade. - pub shade_type: Shade, + pub shade: Shade, /// The position of the shade on the map. pub geometry: Polygon, /// The date the shading was added to the map. @@ -32,7 +32,7 @@ pub struct Shading { #[diesel(table_name = shadings)] pub struct UpdateShading { /// The type/strength of shade. - pub shade_type: Option, + pub shade: Option, /// The position of the shade on the map. pub geometry: Option>, /// The date the shading was added to the map. diff --git a/backend/src/schema.patch b/backend/src/schema.patch index 83018fb56..c0afff0be 100644 --- a/backend/src/schema.patch +++ b/backend/src/schema.patch @@ -55,5 +55,5 @@ shadings (id) { id -> Uuid, layer_id -> Int4, - shade_type -> Shade, + shade -> Shade, geometry -> Geometry, diff --git a/backend/src/service/map.rs b/backend/src/service/map.rs index 2e40315ac..b880b2707 100644 --- a/backend/src/service/map.rs +++ b/backend/src/service/map.rs @@ -18,7 +18,7 @@ use crate::{ }; /// Defines which layers should be created when a new map is created. -const LAYER_TYPES: [LayerType; 2] = [LayerType::Base, LayerType::Plants]; +const LAYER_TYPES: [LayerType; 3] = [LayerType::Base, LayerType::Plants, LayerType::Shade]; /// Search maps from the database. /// diff --git a/backend/src/service/plant_layer.rs b/backend/src/service/plant_layer.rs index e1d67b2a7..0d45e12c0 100644 --- a/backend/src/service/plant_layer.rs +++ b/backend/src/service/plant_layer.rs @@ -32,7 +32,8 @@ pub async fn heatmap( let mut conn = app_data.pool.get().await?; let result = plant_layer::heatmap( map_id, - query_params.layer_id, + query_params.plant_layer_id, + query_params.shade_layer_id, query_params.plant_id, &mut conn, ) diff --git a/backend/src/test/plant_layer_heatmap.rs b/backend/src/test/plant_layer_heatmap.rs index 61bbcae97..676430178 100644 --- a/backend/src/test/plant_layer_heatmap.rs +++ b/backend/src/test/plant_layer_heatmap.rs @@ -17,12 +17,12 @@ use crate::{ error::ServiceError, model::{ entity::plant_layer::GRANULARITY, - r#enum::{layer_type::LayerType, privacy_option::PrivacyOption}, + r#enum::{layer_type::LayerType, privacy_option::PrivacyOption, shade::Shade}, }, test::util::{ dummy_map_polygons::{ rectangle_with_missing_bottom_left_corner, small_rectangle, - small_rectangle_with_non_0_xmin, tall_rectangle, + small_rectangle_with_non_0_xmin, small_square, tall_rectangle, }, init_test_app, init_test_database, }, @@ -49,13 +49,22 @@ async fn initial_db_values( .execute(conn) .await?; diesel::insert_into(crate::schema::layers::table) - .values(( - &crate::schema::layers::id.eq(-1), - &crate::schema::layers::map_id.eq(-1), - &crate::schema::layers::type_.eq(LayerType::Plants), - &crate::schema::layers::name.eq("Some name"), - &crate::schema::layers::is_alternative.eq(false), - )) + .values(vec![ + ( + &crate::schema::layers::id.eq(-1), + &crate::schema::layers::map_id.eq(-1), + &crate::schema::layers::type_.eq(LayerType::Plants), + &crate::schema::layers::name.eq("Some name"), + &crate::schema::layers::is_alternative.eq(false), + ), + ( + &crate::schema::layers::id.eq(-2), + &crate::schema::layers::map_id.eq(-1), + &crate::schema::layers::type_.eq(LayerType::Shade), + &crate::schema::layers::name.eq("Some name"), + &crate::schema::layers::is_alternative.eq(false), + ), + ]) .execute(conn) .await?; diesel::insert_into(crate::schema::plants::table) @@ -63,6 +72,7 @@ async fn initial_db_values( &crate::schema::plants::id.eq(-1), &crate::schema::plants::unique_name.eq("Testia testia"), &crate::schema::plants::common_name_en.eq(Some(vec![Some("T".to_owned())])), + &crate::schema::plants::shade.eq(Some(Shade::NoShade)), )) .execute(conn) .await?; @@ -76,7 +86,7 @@ async fn test_generate_heatmap_succeeds() { let (token, app) = init_test_app(pool.clone()).await; let resp = test::TestRequest::get() - .uri("/api/maps/-1/layers/plants/heatmap?plant_id=-1&layer_id=-1") + .uri("/api/maps/-1/layers/plants/heatmap?plant_id=-1&plant_layer_id=-1&shade_layer_id=-2") .insert_header((header::AUTHORIZATION, token)) .send_request(&app) .await; @@ -95,7 +105,7 @@ async fn test_check_heatmap_dimensionality_succeeds() { let (token, app) = init_test_app(pool.clone()).await; let resp = test::TestRequest::get() - .uri("/api/maps/-1/layers/plants/heatmap?plant_id=-1&layer_id=-1") + .uri("/api/maps/-1/layers/plants/heatmap?plant_id=-1&plant_layer_id=-1&shade_layer_id=-2") .insert_header((header::AUTHORIZATION, token)) .send_request(&app) .await; @@ -124,7 +134,7 @@ async fn test_check_heatmap_non_0_xmin_succeeds() { let (token, app) = init_test_app(pool.clone()).await; let resp = test::TestRequest::get() - .uri("/api/maps/-1/layers/plants/heatmap?plant_id=-1&layer_id=-1") + .uri("/api/maps/-1/layers/plants/heatmap?plant_id=-1&plant_layer_id=-1&shade_layer_id=-2") .insert_header((header::AUTHORIZATION, token)) .send_request(&app) .await; @@ -155,7 +165,7 @@ async fn test_heatmap_with_missing_corner_succeeds() { let (token, app) = init_test_app(pool.clone()).await; let resp = test::TestRequest::get() - .uri("/api/maps/-1/layers/plants/heatmap?plant_id=-1&layer_id=-1") + .uri("/api/maps/-1/layers/plants/heatmap?plant_id=-1&plant_layer_id=-1&shade_layer_id=-2") .insert_header((header::AUTHORIZATION, token)) .send_request(&app) .await; @@ -185,6 +195,56 @@ async fn test_heatmap_with_missing_corner_succeeds() { assert_eq!([64, 191, 64], bottom_right_pixel.0); } +#[actix_rt::test] +async fn test_heatmap_with_shadings_succeeds() { + let pool = init_test_database(|conn| { + async { + initial_db_values(conn, tall_rectangle()).await?; + diesel::insert_into(crate::schema::shadings::table) + .values(( + &crate::schema::shadings::id.eq(Uuid::new_v4()), + &crate::schema::shadings::layer_id.eq(-2), + &crate::schema::shadings::shade.eq(Shade::PermanentDeepShade), + &crate::schema::shadings::geometry.eq(small_square()), + )) + .execute(conn) + .await?; + Ok(()) + } + .scope_boxed() + }) + .await; + let (token, app) = init_test_app(pool.clone()).await; + + let resp = test::TestRequest::get() + .uri("/api/maps/-1/layers/plants/heatmap?plant_id=-1&plant_layer_id=-1&shade_layer_id=-2") + .insert_header((header::AUTHORIZATION, token)) + .send_request(&app) + .await; + + assert_eq!(resp.status(), StatusCode::OK); + assert_eq!( + resp.headers().get(header::CONTENT_TYPE), + Some(&header::HeaderValue::from_static("image/png")) + ); + let result = test::read_body(resp).await; + let result = &result.bytes().collect::, _>>().unwrap(); + let image = load_from_memory_with_format(result.as_slice(), image::ImageFormat::Png).unwrap(); + let image = image.as_rgb8().unwrap(); + assert_eq!( + ((500 / GRANULARITY) as u32, (1000 / GRANULARITY) as u32), + image.dimensions() + ); + + // (0,0) is be top left. + let top_left_pixel = image.get_pixel(1, 1); + let bottom_right_pixel = image.get_pixel(40, 80); + // The shading is the exact opposite of the plants preference, therefore the map will be grey. + assert_eq!([128, 128, 128], top_left_pixel.0); + // Green everywhere else. + assert_eq!([64, 191, 64], bottom_right_pixel.0); +} + #[actix_rt::test] async fn test_missing_entities_fails() { let pool = init_test_database(|conn| { @@ -195,23 +255,31 @@ async fn test_missing_entities_fails() { // Invalid map id let resp = test::TestRequest::get() - .uri("/api/maps/-2/layers/plants/heatmap?plant_id=-1&layer_id=-1") + .uri("/api/maps/-2/layers/plants/heatmap?plant_id=-1&plant_layer_id=-1&shade_layer_id=-2") .insert_header((header::AUTHORIZATION, token.clone())) .send_request(&app) .await; assert_eq!(resp.status(), StatusCode::NOT_FOUND); - // Invalid layer id + // Invalid plant id let resp = test::TestRequest::get() - .uri("/api/maps/-1/layers/plants/heatmap?plant_id=-2&layer_id=-1") + .uri("/api/maps/-1/layers/plants/heatmap?plant_id=-2&plant_layer_id=-1&shade_layer_id=-2") .insert_header((header::AUTHORIZATION, token.clone())) .send_request(&app) .await; assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); - // Invalid plant id + // Invalid plant layer id + let resp = test::TestRequest::get() + .uri("/api/maps/-1/layers/plants/heatmap?plant_id=-2&plant_layer_id=-5&shade_layer_id=-2") + .insert_header((header::AUTHORIZATION, token.clone())) + .send_request(&app) + .await; + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + + // Invalid shade layer id let resp = test::TestRequest::get() - .uri("/api/maps/-1/layers/plants/heatmap?plant_id=-1&layer_id=-2") + .uri("/api/maps/-1/layers/plants/heatmap?plant_id=-1&plant_layer_id=-1&shade_layer_id=-5") .insert_header((header::AUTHORIZATION, token)) .send_request(&app) .await; diff --git a/backend/src/test/shadings.rs b/backend/src/test/shadings.rs index 4efa85ec1..94251ca87 100644 --- a/backend/src/test/shadings.rs +++ b/backend/src/test/shadings.rs @@ -113,7 +113,7 @@ async fn test_create_fails_with_invalid_layer() { let new_shading = NewShadingDto { id: Some(Uuid::new_v4()), action_id: Uuid::new_v4(), - shade_type: Shade::LightShade, + shade: Shade::LightShade, geometry: small_rectangle(), layer_id: -1, add_date: None, @@ -151,7 +151,7 @@ async fn test_can_create_shadings() { id: Some(Uuid::new_v4()), action_id: Uuid::new_v4(), layer_id: -1, - shade_type: Shade::LightShade, + shade: Shade::LightShade, geometry: small_rectangle(), add_date: None, }; @@ -193,7 +193,7 @@ async fn test_can_update_shadings() { let (token, app) = init_test_app(pool.clone()).await; let update_data = UpdateValuesShadingDto { - shade_type: Some(Shade::PermanentDeepShade), + shade: Some(Shade::PermanentDeepShade), geometry: None, action_id: Uuid::new_v4(), }; @@ -208,7 +208,7 @@ async fn test_can_update_shadings() { assert_eq!(resp.status(), StatusCode::OK); let shading: ShadingDto = test::read_body_json(resp).await; - assert_eq!(shading.shade_type, Shade::PermanentDeepShade); + assert_eq!(shading.shade, Shade::PermanentDeepShade); } #[actix_rt::test] diff --git a/backend/src/test/util/data.rs b/backend/src/test/util/data.rs index d2bd64d41..92ecd8817 100644 --- a/backend/src/test/util/data.rs +++ b/backend/src/test/util/data.rs @@ -146,7 +146,7 @@ impl Default for TestInsertablePlanting { pub struct TestInsertableShading { pub id: Uuid, pub layer_id: i32, - pub shade_type: Shade, + pub shade: Shade, pub geometry: Polygon, pub add_date: Option, pub remove_date: Option, @@ -157,7 +157,7 @@ impl Default for TestInsertableShading { Self { id: Uuid::default(), layer_id: -1, - shade_type: Shade::NoShade, + shade: Shade::NoShade, geometry: small_rectangle(), add_date: None, remove_date: None, diff --git a/backend/src/test/util/dummy_map_polygons.rs b/backend/src/test/util/dummy_map_polygons.rs index 074071db7..497f24a27 100644 --- a/backend/src/test/util/dummy_map_polygons.rs +++ b/backend/src/test/util/dummy_map_polygons.rs @@ -132,3 +132,34 @@ pub fn rectangle_with_missing_bottom_left_corner() -> Polygon { }); serde_json::from_value(polygon).unwrap() } + +pub fn small_square() -> Polygon { + let polygon = json!({ + "rings": [ + [ + { + "x": 0.0, + "y": 0.0 + }, + { + "x": 20.0, + "y": 0.0 + }, + { + "x": 20.0, + "y": 20.0 + }, + { + "x": 0.0, + "y": 20.0 + }, + { + "x": 0.0, + "y": 0.0 + } + ] + ], + "srid": 4326 + }); + serde_json::from_value(polygon).unwrap() +} diff --git a/frontend/src/features/maps/routes/MapCreateForm.tsx b/frontend/src/features/maps/routes/MapCreateForm.tsx index c142b27a3..85e5b6e0d 100644 --- a/frontend/src/features/maps/routes/MapCreateForm.tsx +++ b/frontend/src/features/maps/routes/MapCreateForm.tsx @@ -130,9 +130,9 @@ export default function MapCreateForm() { rings: [ [ { x: 0.0, y: 0.0 }, - { x: 0.0, y: 10_000.0 }, - { x: 10_000.0, y: 10_000.0 }, - { x: 10_000.0, y: 0.0 }, + { x: 0.0, y: 1_000.0 }, + { x: 1_000.0, y: 1_000.0 }, + { x: 1_000.0, y: 0.0 }, { x: 0.0, y: 0.0 }, ], ], From ec75d26367df5f6786bbc224141c19582149c863 Mon Sep 17 00:00:00 2001 From: kitzbergerg Date: Sat, 22 Jul 2023 16:35:44 +0200 Subject: [PATCH 005/157] add shadings to changelog --- doc/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/CHANGELOG.md b/doc/CHANGELOG.md index 9f4df3fd7..bf70481fe 100644 --- a/doc/CHANGELOG.md +++ b/doc/CHANGELOG.md @@ -8,7 +8,7 @@ Syntax: `- short text describing the change _(Your Name)_` ## 0.3.0 - UNRELEASED -- _()_ +- backend: add shadings endpoints + heatmap _(Gabriel)_ - _()_ - _()_ - _()_ From e8297d6201d860ee0d09bac16c14e419fd30c8fc Mon Sep 17 00:00:00 2001 From: kitzbergerg Date: Sat, 22 Jul 2023 16:41:30 +0200 Subject: [PATCH 006/157] fix down.sql --- backend/migrations/2023-07-22-110000_shadings/down.sql | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/migrations/2023-07-22-110000_shadings/down.sql b/backend/migrations/2023-07-22-110000_shadings/down.sql index c42ce75cc..d7191a72b 100644 --- a/backend/migrations/2023-07-22-110000_shadings/down.sql +++ b/backend/migrations/2023-07-22-110000_shadings/down.sql @@ -1,4 +1,6 @@ -- This file should undo anything in `up.sql` +DROP FUNCTION calculate_score_from_shadings; + CREATE OR REPLACE FUNCTION calculate_score( p_map_id INTEGER, p_layer_ids INTEGER [], @@ -19,6 +21,6 @@ BEGIN END; $$ LANGUAGE plpgsql; -DROP TRIGGER check_shade_layer_type_before_insert_or_update ON plantings; +DROP TRIGGER check_shade_layer_type_before_insert_or_update ON shadings; DROP FUNCTION check_shade_layer_type; DROP TABLE shadings; From 1c822f1bf7098807aa6b902e74c0de4c299a11c0 Mon Sep 17 00:00:00 2001 From: kitzbergerg Date: Sat, 22 Jul 2023 17:25:26 +0200 Subject: [PATCH 007/157] move shade_layer to current and add gabriel as backend implementor --- backend/src/model/dto/actions.rs | 2 +- doc/usecases/{assigned => current}/shade_layer.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename doc/usecases/{assigned => current}/shade_layer.md (96%) diff --git a/backend/src/model/dto/actions.rs b/backend/src/model/dto/actions.rs index 66324f64d..e78eb1b2d 100644 --- a/backend/src/model/dto/actions.rs +++ b/backend/src/model/dto/actions.rs @@ -39,7 +39,7 @@ pub enum Action { CreateShading(CreateShadingActionPayload), /// An action used to broadcast deletion of a shading. DeleteShading(DeleteShadingActionPayload), - /// An action used to broadcast movement of a shading. + /// An action used to broadcast change of a shading. UpdateShading(UpdateShadingActionPayload), /// An action used to update the `add_date` of a shading. UpdateShadingAddDate(UpdateShadingAddDateActionPayload), diff --git a/doc/usecases/assigned/shade_layer.md b/doc/usecases/current/shade_layer.md similarity index 96% rename from doc/usecases/assigned/shade_layer.md rename to doc/usecases/current/shade_layer.md index 8777048b5..55edd45cc 100644 --- a/doc/usecases/assigned/shade_layer.md +++ b/doc/usecases/current/shade_layer.md @@ -6,7 +6,7 @@ - **Level:** User Goal - **Actors:** App User - **Brief:** The user can add, edit, move, remove and delete shade areas in their map in the shade layer and adjust the intensity. -- **Assignee:** Moritz +- **Assignee:** Moritz (Frontend), Gabriel (Backend) ## Scenarios From cf2d503d300a3ef6b0355b91c63e5ef14e7317ed Mon Sep 17 00:00:00 2001 From: kitzbergerg Date: Sun, 23 Jul 2023 12:10:22 +0200 Subject: [PATCH 008/157] update SQL doc according to suggestions --- backend/migrations/2023-07-03-165000_heatmap/up.sql | 2 +- backend/migrations/2023-07-22-110000_shadings/up.sql | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/migrations/2023-07-03-165000_heatmap/up.sql b/backend/migrations/2023-07-03-165000_heatmap/up.sql index 6469926a4..6cb9b0aa4 100644 --- a/backend/migrations/2023-07-03-165000_heatmap/up.sql +++ b/backend/migrations/2023-07-03-165000_heatmap/up.sql @@ -36,7 +36,7 @@ $$ LANGUAGE plpgsql; -- p_map_id ... map id -- p_layer_ids ... ids of the layers -- p_plant_id ... id of the plant for which to consider relations --- granularity ... resolution of the map (float greater than 0) +-- granularity ... resolution of the map (must be greater than 0) -- x_min,y_min,x_max,y_max ... boundaries of the map CREATE OR REPLACE FUNCTION calculate_heatmap( p_map_id INTEGER, diff --git a/backend/migrations/2023-07-22-110000_shadings/up.sql b/backend/migrations/2023-07-22-110000_shadings/up.sql index e535e089f..d0c4b1a93 100644 --- a/backend/migrations/2023-07-22-110000_shadings/up.sql +++ b/backend/migrations/2023-07-22-110000_shadings/up.sql @@ -49,7 +49,7 @@ BEGIN END; $$ LANGUAGE plpgsql; --- Calculate a score using the shadings. +-- Calculate a score between -0.5 and 0.5 using the shadings. CREATE FUNCTION calculate_score_from_shadings( p_layer_id integer, p_plant_id integer, x_pos integer, y_pos integer ) @@ -76,7 +76,7 @@ BEGIN ORDER BY shade DESC LIMIT 1; - -- If there's no shading, return 0 + -- If there's no shading, return 0 (meaning shadings do not affect the score) IF NOT FOUND OR plant_shade IS NULL THEN RETURN 0; END IF; @@ -88,7 +88,7 @@ BEGIN SELECT array_position(all_values, plant_shade) INTO pos1; SELECT array_position(all_values, shading_shade) INTO pos2; - -- Calculate the 'distance' to the preferred shade as a values between 0.5 and -0.5 + -- Calculate the 'distance' to the preferred shade as a values between -0.5 and 0.5 RETURN 0.5 - (abs(pos1 - pos2) / (ARRAY_LENGTH(all_values, 1) - 1)::REAL); END; $$ LANGUAGE plpgsql; From ee50d96ddf10a13ba0c7fe2566e7533139edba58 Mon Sep 17 00:00:00 2001 From: kitzbergerg Date: Sun, 23 Jul 2023 19:40:28 +0200 Subject: [PATCH 009/157] update SQL doc and uppercase integers --- backend/migrations/2023-07-03-165000_heatmap/up.sql | 6 ++++++ backend/migrations/2023-07-22-110000_shadings/up.sql | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/backend/migrations/2023-07-03-165000_heatmap/up.sql b/backend/migrations/2023-07-03-165000_heatmap/up.sql index 6cb9b0aa4..e8b2a6b41 100644 --- a/backend/migrations/2023-07-03-165000_heatmap/up.sql +++ b/backend/migrations/2023-07-03-165000_heatmap/up.sql @@ -113,7 +113,13 @@ END; $$ LANGUAGE plpgsql; -- Calculate a score for a certain position. +-- Values where the plant should not be placed are close to or smaller than 0. +-- Values where the plant should be placed are close to or larger than 1. +-- +-- p_map_id ... map id -- p_layer_ids[1] ... plant layer +-- p_plant_id ... id of the plant for which to consider relations +-- x_pos,y_pos ... coordinates on the map where to calculate the score CREATE OR REPLACE FUNCTION calculate_score( p_map_id INTEGER, p_layer_ids INTEGER [], diff --git a/backend/migrations/2023-07-22-110000_shadings/up.sql b/backend/migrations/2023-07-22-110000_shadings/up.sql index d0c4b1a93..ea200839c 100644 --- a/backend/migrations/2023-07-22-110000_shadings/up.sql +++ b/backend/migrations/2023-07-22-110000_shadings/up.sql @@ -25,8 +25,14 @@ BEFORE INSERT OR UPDATE ON shadings FOR EACH ROW EXECUTE FUNCTION check_shade_layer_type(); -- Calculate a score for a certain position. +-- Values where the plant should not be placed are close to or smaller than 0. +-- Values where the plant should be placed are close to or larger than 1. +-- +-- p_map_id ... map id -- p_layer_ids[1] ... plant layer -- p_layer_ids[2] ... shade layer +-- p_plant_id ... id of the plant for which to consider relations +-- x_pos,y_pos ... coordinates on the map where to calculate the score CREATE OR REPLACE FUNCTION calculate_score( p_map_id integer, p_layer_ids integer [], From 71d1e3ba2148582cb1c620f851aa002fec31ad0e Mon Sep 17 00:00:00 2001 From: kitzbergerg Date: Sun, 23 Jul 2023 19:58:52 +0200 Subject: [PATCH 010/157] add manual test for shading with heatmap to show that it works --- doc/tests/manual/protocol.md | 11 +- .../reports/230723_heatmap_with_shade.md | 238 ++++++++++++++++++ 2 files changed, 245 insertions(+), 4 deletions(-) create mode 100644 doc/tests/manual/reports/230723_heatmap_with_shade.md diff --git a/doc/tests/manual/protocol.md b/doc/tests/manual/protocol.md index db8539f5a..b54851e6a 100644 --- a/doc/tests/manual/protocol.md +++ b/doc/tests/manual/protocol.md @@ -114,14 +114,15 @@ - Description: Test whether the heatmap endpoints generates the image correctly. - Preconditions: - - Be on the map managment page. - - TODO! @kitzbergerg + - Be on the map management page. + - Data is inserted via the scraper (plants and plant relations) - Test Steps: 1. Create a map 2. Plant some plants with relations. - 3. TODO! @kitzbergerg + 3. Add other constraints such as shade or soil ph. + 4. Generate the heatmap. - Expected Result: - - [ ] Heatmap considers map polygon and plant relations. + - [ ] Heatmap considers map polygon and environmental constraints. - Actual Result: - Test Result: - Notes: @@ -224,6 +225,7 @@ - Notes: ## TC-013 - Base Layer + - Description: Check whether the maps background image is displayed correctly. - Preconditions: - [ ] A map has been created. @@ -244,6 +246,7 @@ - Notes: ## TC-014 - Grid + - Description: Display a point grid on the screen. - Preconditions: - [ ] User must be on the map screen. diff --git a/doc/tests/manual/reports/230723_heatmap_with_shade.md b/doc/tests/manual/reports/230723_heatmap_with_shade.md new file mode 100644 index 000000000..4ae54a265 --- /dev/null +++ b/doc/tests/manual/reports/230723_heatmap_with_shade.md @@ -0,0 +1,238 @@ +# Heatmap with Shade + +## General + +- Tester: Gabriel +- Date/Time: 23.07.2023 20:00 +- Duration: 15 min +- Commit/Tag: ee50d96ddf10a13ba0c7fe2566e7533139edba58 +- Planned tests: 1 +- Executed tests: **1** +- Passed tests: 1 +- Failed tests: 0 + +## Error Analysis + +## Closing remarks + +This test was executed to show how the heatmap can be generated without the frontend being fully implemented. + +## Testcases + +### TC-007 - Heatmap + +1. Get a clean database with all migrations: `cd backend && diesel database reset`. +2. Insert the plants and relations using the scraper (`cd scraper && npm run insert`) +3. Start the backend (`cd backend && cargo run`). +4. Create a map (via Postman, equivalent cURL below - remember to insert the token). + +```bash +curl --location 'http://localhost:8080/api/maps' \ +--header 'Authorization: Bearer ' \ +--header 'Content-Type: application/json' \ +--data '{ + "name": "Test1", + "creation_date": "2023-07-06", + "is_inactive": false, + "zoom_factor": 100, + "honors": 0, + "visits": 0, + "harvested": 0, + "privacy": "public", + "description": "", + "geometry": { + "rings": [ + [ + { + "x": 0.0, + "y": 0.0 + }, + { + "x": 100.0, + "y": 0.0 + }, + { + "x": 100.0, + "y": 100.0 + }, + { + "x": 50.0, + "y": 100.0 + }, + { + "x": 50.0, + "y": 50.0 + }, + { + "x": 0.0, + "y": 50.0 + }, + { + "x": 0.0, + "y": 0.0 + } + ] + ], + "srid": 4326 + } +}' +``` + +The result should be: + +```json +{ + "id": 1, + "name": "Test1", + "creation_date": "2023-07-06", + "deletion_date": null, + "last_visit": null, + "is_inactive": false, + "zoom_factor": 100, + "honors": 0, + "visits": 0, + "harvested": 0, + "privacy": "public", + "description": "", + "location": null, + "owner_id": "361c7c28-020f-4b31-84ea-cc629cc43180", + "geometry": { + "rings": [ + [ + { + "x": 0.0, + "y": 0.0, + "srid": 4326 + }, + { + "x": 100.0, + "y": 0.0, + "srid": 4326 + }, + { + "x": 100.0, + "y": 100.0, + "srid": 4326 + }, + { + "x": 50.0, + "y": 100.0, + "srid": 4326 + }, + { + "x": 50.0, + "y": 50.0, + "srid": 4326 + }, + { + "x": 0.0, + "y": 50.0, + "srid": 4326 + }, + { + "x": 0.0, + "y": 0.0, + "srid": 4326 + } + ] + ], + "srid": 4326 + } +} +``` + +5. Create a shading. + +```bash +curl --location 'http://localhost:8080/api/maps/1/layers/shade/shadings' \ +--header 'Content-Type: application/json' \ +--header 'Authorization: Bearer ' \ +--data '{ + "layerId": 3, + "shade": "no shade", + "geometry": { + "rings": [ + [ + { + "x": 0.0, + "y": 0.0 + }, + { + "x": 20.0, + "y": 0.0 + }, + { + "x": 20.0, + "y": 20.0 + }, + { + "x": 0.0, + "y": 20.0 + }, + { + "x": 0.0, + "y": 0.0 + } + ] + ], + "srid": 4326 + }, + "actionId": "00000000-0000-0000-0000-000000000000" +}' +``` + +The result should be: + +```json +{ + "id": "21c9ca45-5ff4-492a-a537-7eb64c134613", + "layerId": 3, + "shade": "no shade", + "geometry": { + "rings": [ + [ + { + "x": 0.0, + "y": 0.0, + "srid": 4326 + }, + { + "x": 20.0, + "y": 0.0, + "srid": 4326 + }, + { + "x": 20.0, + "y": 20.0, + "srid": 4326 + }, + { + "x": 0.0, + "y": 20.0, + "srid": 4326 + }, + { + "x": 0.0, + "y": 0.0, + "srid": 4326 + } + ] + ], + "srid": 4326 + }, + "addDate": null, + "removeDate": null +} +``` + +6. Execute the request. + +```bash +curl -o file.png --location 'http://localhost:8080/api/maps/1/layers/plants/heatmap?plant_id=1&plant_layer_id=1&shade_layer_id=3' \ +--header 'Authorization: Bearer ' +``` + +7. Verify: + +- The bottom left corner should be grey, everything else should be green. +- The top left corner should be greener than the rest as there is shade there and plant with id 1 likes shade. From 99f580a974477251369591023de5258e5f840109 Mon Sep 17 00:00:00 2001 From: kitzbergerg Date: Sun, 23 Jul 2023 21:37:06 +0200 Subject: [PATCH 011/157] add alpha channel to PNG --- .../2023-07-03-165000_heatmap/up.sql | 66 ++++++++++++------- .../2023-07-22-110000_shadings/up.sql | 37 +++++++---- backend/src/model/entity/plant_layer.rs | 11 ++-- backend/src/service/plant_layer.rs | 9 +-- backend/src/test/plant_layer_heatmap.rs | 20 +++--- 5 files changed, 89 insertions(+), 54 deletions(-) diff --git a/backend/migrations/2023-07-03-165000_heatmap/up.sql b/backend/migrations/2023-07-03-165000_heatmap/up.sql index e8b2a6b41..4d2274b38 100644 --- a/backend/migrations/2023-07-03-165000_heatmap/up.sql +++ b/backend/migrations/2023-07-03-165000_heatmap/up.sql @@ -21,11 +21,19 @@ BEGIN END; $$ LANGUAGE plpgsql; +CREATE TYPE score_w_alpha AS ( + score REAL, + alpha REAL +); -- Returns scores from 0-1 for each pixel of the map. -- Values where the plant should not be placed are close to 0. -- Values where the plant should be placed are close to 1. -- +-- Returns alpha from 0-1 for each pixel of the map. +-- Values where there is no relevant data are close to 0. +-- Values where there is relevant data are close to 1. +-- -- The resulting matrix does not contain valid (x,y) coordinates, -- instead (x,y) are simply the indices in the matrix. -- The (x,y) coordinate of the computed heatmap always starts at @@ -48,8 +56,9 @@ CREATE OR REPLACE FUNCTION calculate_heatmap( x_max INTEGER, y_max INTEGER ) -RETURNS TABLE (score REAL, x INTEGER, y INTEGER) AS $$ +RETURNS TABLE (score REAL, alpha REAL, x INTEGER, y INTEGER) AS $$ DECLARE + score_w_alpha score_w_alpha; map_geometry GEOMETRY(POLYGON, 4326); point GEOMETRY; bbox GEOMETRY; @@ -87,12 +96,15 @@ BEGIN -- Create a point from x_pos and y_pos point := ST_SetSRID(ST_MakePoint(x_pos, y_pos), 4326); - -- If the square is on the map calculate a score; otherwise set score to 0. + -- If the point is on the map calculate a score; otherwise set score to 0. IF ST_Intersects(point, map_geometry) THEN - score := calculate_score(p_map_id, p_layer_ids, p_plant_id, x_pos, y_pos); - score := scale_score(score); -- scale the score to be between 0 and 1 + score_w_alpha := calculate_score(p_map_id, p_layer_ids, p_plant_id, x_pos, y_pos); + score_w_alpha := scale_score(score_w_alpha); -- scale to be between 0 and 1 + score := score_w_alpha.score; + alpha := score_w_alpha.alpha; ELSE score := 0.0; + alpha := 0.0; END IF; x := i; @@ -104,15 +116,19 @@ BEGIN END; $$ LANGUAGE plpgsql; --- Scales the score to values 0-1. -CREATE OR REPLACE FUNCTION scale_score(input REAL) -RETURNS REAL AS $$ +-- Scales to values between 0 and 1. +CREATE OR REPLACE FUNCTION scale_score(input SCORE_W_ALPHA) +RETURNS SCORE_W_ALPHA AS $$ +DECLARE + score_w_alpha score_w_alpha; BEGIN - RETURN LEAST(GREATEST(input, 0), 1); + score_w_alpha.score := LEAST(GREATEST(input.score, 0), 1); + score_w_alpha.alpha := LEAST(GREATEST(input.alpha, 0), 1); + RETURN score_w_alpha; END; $$ LANGUAGE plpgsql; --- Calculate a score for a certain position. +-- Calculate a score and alpha for a certain position. -- Values where the plant should not be placed are close to or smaller than 0. -- Values where the plant should be placed are close to or larger than 1. -- @@ -127,30 +143,34 @@ CREATE OR REPLACE FUNCTION calculate_score( x_pos INTEGER, y_pos INTEGER ) -RETURNS REAL AS $$ +RETURNS SCORE_W_ALPHA AS $$ DECLARE - plant_relation RECORD; - distance REAL; - weight REAL; - score REAL := 0; + score_w_alpha score_w_alpha; + plants score_w_alpha; BEGIN - score := 0.5 + calculate_score_from_relations(p_layer_ids[1], p_plant_id, x_pos, y_pos); + plants := calculate_score_from_relations(p_layer_ids[1], p_plant_id, x_pos, y_pos); + + score_w_alpha.score := 0.5 + plants.score; + score_w_alpha.alpha := 0.2 + plants.alpha; - RETURN score; + RETURN score_w_alpha; END; $$ LANGUAGE plpgsql; --- Calculate a score using the plants relations and their distances. +-- Calculate score and alpha using the plants relations and their distances. CREATE OR REPLACE FUNCTION calculate_score_from_relations( p_layer_id INTEGER, p_plant_id INTEGER, x_pos INTEGER, y_pos INTEGER ) -RETURNS REAL AS $$ +RETURNS SCORE_W_ALPHA AS $$ DECLARE plant_relation RECORD; distance REAL; weight REAL; - score REAL := 0; + score_w_alpha score_w_alpha; BEGIN + score_w_alpha.score := 0.0; + score_w_alpha.alpha := 0.0; + FOR plant_relation IN (SELECT * FROM get_plant_relations(p_layer_id, p_plant_id)) LOOP -- calculate distance distance := sqrt((plant_relation.x - x_pos)^2 + (plant_relation.y - y_pos)^2); @@ -160,13 +180,15 @@ BEGIN -- update score based on relation IF plant_relation.relation = 'companion' THEN - score := score + 0.5 * weight; + score_w_alpha.score := score_w_alpha.score + 0.5 * weight; + score_w_alpha.alpha := score_w_alpha.alpha + 0.5 * weight; ELSE - score := score - 0.5 * weight; + score_w_alpha.score := score_w_alpha.score - 0.5 * weight; + score_w_alpha.alpha := score_w_alpha.alpha + 0.5 * weight; END IF; END LOOP; - RETURN score; + RETURN score_w_alpha; END; $$ LANGUAGE plpgsql; diff --git a/backend/migrations/2023-07-22-110000_shadings/up.sql b/backend/migrations/2023-07-22-110000_shadings/up.sql index ea200839c..b22b3c19a 100644 --- a/backend/migrations/2023-07-22-110000_shadings/up.sql +++ b/backend/migrations/2023-07-22-110000_shadings/up.sql @@ -24,7 +24,7 @@ CREATE TRIGGER check_shade_layer_type_before_insert_or_update BEFORE INSERT OR UPDATE ON shadings FOR EACH ROW EXECUTE FUNCTION check_shade_layer_type(); --- Calculate a score for a certain position. +-- Calculate a score and alpha for a certain position. -- Values where the plant should not be placed are close to or smaller than 0. -- Values where the plant should be placed are close to or larger than 1. -- @@ -40,26 +40,28 @@ CREATE OR REPLACE FUNCTION calculate_score( x_pos integer, y_pos integer ) -RETURNS real AS $$ +RETURNS score_w_alpha AS $$ DECLARE - plant_relation RECORD; - distance REAL; - weight REAL; - score REAL := 0; + score_w_alpha score_w_alpha; + plants score_w_alpha; + shades score_w_alpha; BEGIN - score := 0.5 - + calculate_score_from_relations(p_layer_ids[1], p_plant_id, x_pos, y_pos) - + calculate_score_from_shadings(p_layer_ids[2], p_plant_id, x_pos, y_pos); + plants := calculate_score_from_relations(p_layer_ids[1], p_plant_id, x_pos, y_pos); + shades := calculate_score_from_shadings(p_layer_ids[2], p_plant_id, x_pos, y_pos); - RETURN score; + score_w_alpha.score := 0.5 + plants.score + shades.score; + score_w_alpha.alpha := 0.2 + plants.alpha + shades.alpha; + + RETURN score_w_alpha; END; $$ LANGUAGE plpgsql; --- Calculate a score between -0.5 and 0.5 using the shadings. +-- Calculate score: Between -0.5 and 0.5 depending on shadings. +-- Calculate alpha: If there is shading set to 0.5; otherwise 0.0. CREATE FUNCTION calculate_score_from_shadings( p_layer_id integer, p_plant_id integer, x_pos integer, y_pos integer ) -RETURNS real AS $$ +RETURNS score_w_alpha AS $$ DECLARE point GEOMETRY; plant_shade shade; @@ -67,6 +69,7 @@ DECLARE all_values shade[]; pos1 INTEGER; pos2 INTEGER; + score_w_alpha score_w_alpha; BEGIN -- Get the preferred shade of the plant SELECT shade INTO plant_shade @@ -84,9 +87,12 @@ BEGIN -- If there's no shading, return 0 (meaning shadings do not affect the score) IF NOT FOUND OR plant_shade IS NULL THEN - RETURN 0; + score_w_alpha.score := 0.0; + score_w_alpha.alpha := 0.0; + RETURN score_w_alpha; END IF; + -- Get all possible enum values SELECT enum_range(NULL::shade) INTO all_values; @@ -95,6 +101,9 @@ BEGIN SELECT array_position(all_values, shading_shade) INTO pos2; -- Calculate the 'distance' to the preferred shade as a values between -0.5 and 0.5 - RETURN 0.5 - (abs(pos1 - pos2) / (ARRAY_LENGTH(all_values, 1) - 1)::REAL); + score_w_alpha.score := 0.5 - (abs(pos1 - pos2) / (ARRAY_LENGTH(all_values, 1) - 1)::REAL); + score_w_alpha.alpha := 0.5; + + RETURN score_w_alpha; END; $$ LANGUAGE plpgsql; diff --git a/backend/src/model/entity/plant_layer.rs b/backend/src/model/entity/plant_layer.rs index 0bd17f273..60dc4c8b8 100644 --- a/backend/src/model/entity/plant_layer.rs +++ b/backend/src/model/entity/plant_layer.rs @@ -43,6 +43,9 @@ struct HeatMapElement { /// The score on the heatmap. #[diesel(sql_type = Float)] score: f32, + /// The alpha on the heatmap. + #[diesel(sql_type = Float)] + alpha: f32, /// The x values of the score #[diesel(sql_type = Integer)] x: i32, @@ -68,7 +71,7 @@ pub async fn heatmap( shade_layer_id: i32, plant_id: i32, conn: &mut AsyncPgConnection, -) -> QueryResult>> { +) -> QueryResult>> { // 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::(map_id); @@ -95,9 +98,9 @@ pub async fn heatmap( (f64::from(bounding_box.x_max - bounding_box.x_min) / f64::from(GRANULARITY)).ceil(); let num_rows = (f64::from(bounding_box.y_max - bounding_box.y_min) / f64::from(GRANULARITY)).ceil(); - let mut heatmap = vec![vec![0.0; num_cols as usize]; num_rows as usize]; - for HeatMapElement { score, x, y } in result { - heatmap[y as usize][x as usize] = score; + let mut heatmap = vec![vec![(0.0, 0.0); num_cols as usize]; num_rows as usize]; + for HeatMapElement { score, alpha, x, y } in result { + heatmap[y as usize][x as usize] = (score, alpha); } debug!("{heatmap:#?}"); Ok(heatmap) diff --git a/backend/src/service/plant_layer.rs b/backend/src/service/plant_layer.rs index 0d45e12c0..55c483c90 100644 --- a/backend/src/service/plant_layer.rs +++ b/backend/src/service/plant_layer.rs @@ -4,7 +4,7 @@ use std::io::Cursor; use actix_http::StatusCode; use actix_web::web::Data; -use image::{ImageBuffer, Rgb}; +use image::{ImageBuffer, Rgba}; use crate::{ config::data::AppDataInner, @@ -50,19 +50,20 @@ pub async fn heatmap( clippy::indexing_slicing, // ok, because size of image is generated using matrix width and height clippy::cast_sign_loss // ok, because we only care about positive values )] -fn matrix_to_image(matrix: &Vec>) -> Result, ServiceError> { +fn matrix_to_image(matrix: &Vec>) -> Result, ServiceError> { let (width, height) = (matrix[0].len(), matrix.len()); let mut imgbuf = ImageBuffer::new(width as u32, height as u32); for (x, y, pixel) in imgbuf.enumerate_pixels_mut() { - let data = matrix[y as usize][x as usize]; + let (data, alpha) = matrix[y as usize][x as usize]; // The closer data is to 1 the green it gets. let red = data.mul_add(-128.0, 128.0); let green = data.mul_add(255.0 - 128.0, 128.0); let blue = data.mul_add(-128.0, 128.0); + let alpha = alpha * 255.0; - *pixel = Rgb([red as u8, green as u8, blue as u8]); + *pixel = Rgba([red as u8, green as u8, blue as u8, alpha as u8]); } let mut buffer: Vec = Vec::new(); diff --git a/backend/src/test/plant_layer_heatmap.rs b/backend/src/test/plant_layer_heatmap.rs index 676430178..f7f432a2c 100644 --- a/backend/src/test/plant_layer_heatmap.rs +++ b/backend/src/test/plant_layer_heatmap.rs @@ -118,7 +118,7 @@ async fn test_check_heatmap_dimensionality_succeeds() { let result = test::read_body(resp).await; let result = &result.bytes().collect::, _>>().unwrap(); let image = load_from_memory_with_format(result.as_slice(), image::ImageFormat::Png).unwrap(); - let image = image.as_rgb8().unwrap(); + let image = image.as_rgba8().unwrap(); assert_eq!( ((10 / GRANULARITY) as u32, (100 / GRANULARITY) as u32), image.dimensions() @@ -147,7 +147,7 @@ async fn test_check_heatmap_non_0_xmin_succeeds() { let result = test::read_body(resp).await; let result = &result.bytes().collect::, _>>().unwrap(); let image = load_from_memory_with_format(result.as_slice(), image::ImageFormat::Png).unwrap(); - let image = image.as_rgb8().unwrap(); + let image = image.as_rgba8().unwrap(); assert_eq!( ((90 / GRANULARITY) as u32, (100 / GRANULARITY) as u32), image.dimensions() @@ -178,7 +178,7 @@ async fn test_heatmap_with_missing_corner_succeeds() { let result = test::read_body(resp).await; let result = &result.bytes().collect::, _>>().unwrap(); let image = load_from_memory_with_format(result.as_slice(), image::ImageFormat::Png).unwrap(); - let image = image.as_rgb8().unwrap(); + let image = image.as_rgba8().unwrap(); assert_eq!( ((100 / GRANULARITY) as u32, (100 / GRANULARITY) as u32), image.dimensions() @@ -189,10 +189,10 @@ async fn test_heatmap_with_missing_corner_succeeds() { let top_right_pixel = image.get_pixel(8, 2); let bottom_left_pixel = image.get_pixel(2, 8); let bottom_right_pixel = image.get_pixel(8, 8); - assert_eq!([64, 191, 64], top_left_pixel.0); - assert_eq!([64, 191, 64], top_right_pixel.0); - assert_eq!([128, 128, 128], bottom_left_pixel.0); - assert_eq!([64, 191, 64], bottom_right_pixel.0); + assert_eq!([64, 191, 64, 51], top_left_pixel.0); + assert_eq!([64, 191, 64, 51], top_right_pixel.0); + assert_eq!([128, 128, 128, 0], bottom_left_pixel.0); + assert_eq!([64, 191, 64, 51], bottom_right_pixel.0); } #[actix_rt::test] @@ -230,7 +230,7 @@ async fn test_heatmap_with_shadings_succeeds() { let result = test::read_body(resp).await; let result = &result.bytes().collect::, _>>().unwrap(); let image = load_from_memory_with_format(result.as_slice(), image::ImageFormat::Png).unwrap(); - let image = image.as_rgb8().unwrap(); + let image = image.as_rgba8().unwrap(); assert_eq!( ((500 / GRANULARITY) as u32, (1000 / GRANULARITY) as u32), image.dimensions() @@ -240,9 +240,9 @@ async fn test_heatmap_with_shadings_succeeds() { let top_left_pixel = image.get_pixel(1, 1); let bottom_right_pixel = image.get_pixel(40, 80); // The shading is the exact opposite of the plants preference, therefore the map will be grey. - assert_eq!([128, 128, 128], top_left_pixel.0); + assert_eq!([128, 128, 128, 178], top_left_pixel.0); // Green everywhere else. - assert_eq!([64, 191, 64], bottom_right_pixel.0); + assert_eq!([64, 191, 64, 51], bottom_right_pixel.0); } #[actix_rt::test] From c4500cb37e132441ab49ca458cfdea2557a358a5 Mon Sep 17 00:00:00 2001 From: kitzbergerg Date: Sun, 23 Jul 2023 21:52:04 +0200 Subject: [PATCH 012/157] rename fields of score --- .../2023-07-03-165000_heatmap/up.sql | 81 +++++++++---------- .../2023-07-22-110000_shadings/up.sql | 38 +++++---- backend/src/model/entity/plant_layer.rs | 14 +++- backend/src/service/plant_layer.rs | 10 +-- 4 files changed, 73 insertions(+), 70 deletions(-) diff --git a/backend/migrations/2023-07-03-165000_heatmap/up.sql b/backend/migrations/2023-07-03-165000_heatmap/up.sql index 4d2274b38..405f492a5 100644 --- a/backend/migrations/2023-07-03-165000_heatmap/up.sql +++ b/backend/migrations/2023-07-03-165000_heatmap/up.sql @@ -21,18 +21,19 @@ BEGIN END; $$ LANGUAGE plpgsql; -CREATE TYPE score_w_alpha AS ( - score REAL, - alpha REAL +-- The score is defined as the preference and the relevance. +CREATE TYPE score AS ( + preference REAL, + relevance REAL ); -- Returns scores from 0-1 for each pixel of the map. --- Values where the plant should not be placed are close to 0. --- Values where the plant should be placed are close to 1. -- --- Returns alpha from 0-1 for each pixel of the map. --- Values where there is no relevant data are close to 0. --- Values where there is relevant data are close to 1. +-- Positions where the plant should not be placed have preference close to 0. +-- Positions where the plant should be placed have preference close to 1. +-- +-- Positions where there is no relevant data have relevance close to 0. +-- Positions where there is relevant data have relevance close to 1. -- -- The resulting matrix does not contain valid (x,y) coordinates, -- instead (x,y) are simply the indices in the matrix. @@ -56,9 +57,9 @@ CREATE OR REPLACE FUNCTION calculate_heatmap( x_max INTEGER, y_max INTEGER ) -RETURNS TABLE (score REAL, alpha REAL, x INTEGER, y INTEGER) AS $$ +RETURNS TABLE (preference REAL, relevance REAL, x INTEGER, y INTEGER) AS $$ DECLARE - score_w_alpha score_w_alpha; + score SCORE; map_geometry GEOMETRY(POLYGON, 4326); point GEOMETRY; bbox GEOMETRY; @@ -98,13 +99,13 @@ BEGIN -- If the point is on the map calculate a score; otherwise set score to 0. IF ST_Intersects(point, map_geometry) THEN - score_w_alpha := calculate_score(p_map_id, p_layer_ids, p_plant_id, x_pos, y_pos); - score_w_alpha := scale_score(score_w_alpha); -- scale to be between 0 and 1 - score := score_w_alpha.score; - alpha := score_w_alpha.alpha; + score := calculate_score(p_map_id, p_layer_ids, p_plant_id, x_pos, y_pos); + score := scale_score(score); -- scale to be between 0 and 1 + preference := score.preference; + relevance := score.relevance; ELSE - score := 0.0; - alpha := 0.0; + preference := 0.0; + relevance := 0.0; END IF; x := i; @@ -117,20 +118,18 @@ END; $$ LANGUAGE plpgsql; -- Scales to values between 0 and 1. -CREATE OR REPLACE FUNCTION scale_score(input SCORE_W_ALPHA) -RETURNS SCORE_W_ALPHA AS $$ +CREATE OR REPLACE FUNCTION scale_score(input SCORE) +RETURNS SCORE AS $$ DECLARE - score_w_alpha score_w_alpha; + score SCORE; BEGIN - score_w_alpha.score := LEAST(GREATEST(input.score, 0), 1); - score_w_alpha.alpha := LEAST(GREATEST(input.alpha, 0), 1); - RETURN score_w_alpha; + score.preference := LEAST(GREATEST(input.preference, 0), 1); + score.relevance := LEAST(GREATEST(input.relevance, 0), 1); + RETURN score; END; $$ LANGUAGE plpgsql; --- Calculate a score and alpha for a certain position. --- Values where the plant should not be placed are close to or smaller than 0. --- Values where the plant should be placed are close to or larger than 1. +-- Calculate score for a certain position. -- -- p_map_id ... map id -- p_layer_ids[1] ... plant layer @@ -143,33 +142,33 @@ CREATE OR REPLACE FUNCTION calculate_score( x_pos INTEGER, y_pos INTEGER ) -RETURNS SCORE_W_ALPHA AS $$ +RETURNS SCORE AS $$ DECLARE - score_w_alpha score_w_alpha; - plants score_w_alpha; + score SCORE; + plants SCORE; BEGIN plants := calculate_score_from_relations(p_layer_ids[1], p_plant_id, x_pos, y_pos); - score_w_alpha.score := 0.5 + plants.score; - score_w_alpha.alpha := 0.2 + plants.alpha; + score.preference := 0.5 + plants.preference; + score.relevance := 0.2 + plants.relevance; - RETURN score_w_alpha; + RETURN score; END; $$ LANGUAGE plpgsql; --- Calculate score and alpha using the plants relations and their distances. +-- Calculate score using the plants relations and their distances. CREATE OR REPLACE FUNCTION calculate_score_from_relations( p_layer_id INTEGER, p_plant_id INTEGER, x_pos INTEGER, y_pos INTEGER ) -RETURNS SCORE_W_ALPHA AS $$ +RETURNS SCORE AS $$ DECLARE plant_relation RECORD; distance REAL; weight REAL; - score_w_alpha score_w_alpha; + score SCORE; BEGIN - score_w_alpha.score := 0.0; - score_w_alpha.alpha := 0.0; + score.preference := 0.0; + score.relevance := 0.0; FOR plant_relation IN (SELECT * FROM get_plant_relations(p_layer_id, p_plant_id)) LOOP -- calculate distance @@ -180,15 +179,15 @@ BEGIN -- update score based on relation IF plant_relation.relation = 'companion' THEN - score_w_alpha.score := score_w_alpha.score + 0.5 * weight; - score_w_alpha.alpha := score_w_alpha.alpha + 0.5 * weight; + score.preference := score.preference + 0.5 * weight; + score.relevance := score.relevance + 0.5 * weight; ELSE - score_w_alpha.score := score_w_alpha.score - 0.5 * weight; - score_w_alpha.alpha := score_w_alpha.alpha + 0.5 * weight; + score.preference := score.preference - 0.5 * weight; + score.relevance := score.relevance + 0.5 * weight; END IF; END LOOP; - RETURN score_w_alpha; + RETURN score; END; $$ LANGUAGE plpgsql; diff --git a/backend/migrations/2023-07-22-110000_shadings/up.sql b/backend/migrations/2023-07-22-110000_shadings/up.sql index b22b3c19a..432ae9b65 100644 --- a/backend/migrations/2023-07-22-110000_shadings/up.sql +++ b/backend/migrations/2023-07-22-110000_shadings/up.sql @@ -24,9 +24,7 @@ CREATE TRIGGER check_shade_layer_type_before_insert_or_update BEFORE INSERT OR UPDATE ON shadings FOR EACH ROW EXECUTE FUNCTION check_shade_layer_type(); --- Calculate a score and alpha for a certain position. --- Values where the plant should not be placed are close to or smaller than 0. --- Values where the plant should be placed are close to or larger than 1. +-- Calculate score and relevance for a certain position. -- -- p_map_id ... map id -- p_layer_ids[1] ... plant layer @@ -40,28 +38,28 @@ CREATE OR REPLACE FUNCTION calculate_score( x_pos integer, y_pos integer ) -RETURNS score_w_alpha AS $$ +RETURNS score AS $$ DECLARE - score_w_alpha score_w_alpha; - plants score_w_alpha; - shades score_w_alpha; + score SCORE; + plants SCORE; + shades SCORE; BEGIN plants := calculate_score_from_relations(p_layer_ids[1], p_plant_id, x_pos, y_pos); shades := calculate_score_from_shadings(p_layer_ids[2], p_plant_id, x_pos, y_pos); - score_w_alpha.score := 0.5 + plants.score + shades.score; - score_w_alpha.alpha := 0.2 + plants.alpha + shades.alpha; + score.preference := 0.5 + plants.preference + shades.preference; + score.relevance := 0.2 + plants.relevance + shades.relevance; - RETURN score_w_alpha; + RETURN score; END; $$ LANGUAGE plpgsql; --- Calculate score: Between -0.5 and 0.5 depending on shadings. --- Calculate alpha: If there is shading set to 0.5; otherwise 0.0. +-- Calculate preference: Between -0.5 and 0.5 depending on shadings. +-- Calculate relevance: 0.5 if there is shading; otherwise 0.0. CREATE FUNCTION calculate_score_from_shadings( p_layer_id integer, p_plant_id integer, x_pos integer, y_pos integer ) -RETURNS score_w_alpha AS $$ +RETURNS score AS $$ DECLARE point GEOMETRY; plant_shade shade; @@ -69,7 +67,7 @@ DECLARE all_values shade[]; pos1 INTEGER; pos2 INTEGER; - score_w_alpha score_w_alpha; + score SCORE; BEGIN -- Get the preferred shade of the plant SELECT shade INTO plant_shade @@ -87,9 +85,9 @@ BEGIN -- If there's no shading, return 0 (meaning shadings do not affect the score) IF NOT FOUND OR plant_shade IS NULL THEN - score_w_alpha.score := 0.0; - score_w_alpha.alpha := 0.0; - RETURN score_w_alpha; + score.preference := 0.0; + score.relevance := 0.0; + RETURN score; END IF; @@ -101,9 +99,9 @@ BEGIN SELECT array_position(all_values, shading_shade) INTO pos2; -- Calculate the 'distance' to the preferred shade as a values between -0.5 and 0.5 - score_w_alpha.score := 0.5 - (abs(pos1 - pos2) / (ARRAY_LENGTH(all_values, 1) - 1)::REAL); - score_w_alpha.alpha := 0.5; + score.preference := 0.5 - (abs(pos1 - pos2) / (ARRAY_LENGTH(all_values, 1) - 1)::REAL); + score.relevance := 0.5; - RETURN score_w_alpha; + RETURN score; END; $$ LANGUAGE plpgsql; diff --git a/backend/src/model/entity/plant_layer.rs b/backend/src/model/entity/plant_layer.rs index 60dc4c8b8..43097b233 100644 --- a/backend/src/model/entity/plant_layer.rs +++ b/backend/src/model/entity/plant_layer.rs @@ -42,10 +42,10 @@ struct BoundingBox { struct HeatMapElement { /// The score on the heatmap. #[diesel(sql_type = Float)] - score: f32, + preference: f32, /// The alpha on the heatmap. #[diesel(sql_type = Float)] - alpha: f32, + relevance: f32, /// The x values of the score #[diesel(sql_type = Integer)] x: i32, @@ -99,8 +99,14 @@ pub async fn heatmap( let num_rows = (f64::from(bounding_box.y_max - bounding_box.y_min) / f64::from(GRANULARITY)).ceil(); let mut heatmap = vec![vec![(0.0, 0.0); num_cols as usize]; num_rows as usize]; - for HeatMapElement { score, alpha, x, y } in result { - heatmap[y as usize][x as usize] = (score, alpha); + for HeatMapElement { + preference, + relevance, + x, + y, + } in result + { + heatmap[y as usize][x as usize] = (preference, relevance); } debug!("{heatmap:#?}"); Ok(heatmap) diff --git a/backend/src/service/plant_layer.rs b/backend/src/service/plant_layer.rs index 55c483c90..2c2df7b7a 100644 --- a/backend/src/service/plant_layer.rs +++ b/backend/src/service/plant_layer.rs @@ -55,13 +55,13 @@ fn matrix_to_image(matrix: &Vec>) -> Result, ServiceErro let mut imgbuf = ImageBuffer::new(width as u32, height as u32); for (x, y, pixel) in imgbuf.enumerate_pixels_mut() { - let (data, alpha) = matrix[y as usize][x as usize]; + let (preference, relevance) = matrix[y as usize][x as usize]; // The closer data is to 1 the green it gets. - let red = data.mul_add(-128.0, 128.0); - let green = data.mul_add(255.0 - 128.0, 128.0); - let blue = data.mul_add(-128.0, 128.0); - let alpha = alpha * 255.0; + let red = preference.mul_add(-128.0, 128.0); + let green = preference.mul_add(255.0 - 128.0, 128.0); + let blue = preference.mul_add(-128.0, 128.0); + let alpha = relevance * 255.0; *pixel = Rgba([red as u8, green as u8, blue as u8, alpha as u8]); } From a570bb66cec7a689f512baa8e88a8d7475535df6 Mon Sep 17 00:00:00 2001 From: kitzbergerg Date: Sun, 23 Jul 2023 22:01:50 +0200 Subject: [PATCH 013/157] change heatmap gradient to red-green --- backend/src/controller/plant_layer.rs | 3 ++- backend/src/service/plant_layer.rs | 6 +++--- backend/src/test/plant_layer_heatmap.rs | 20 +++++++++---------- .../reports/230723_heatmap_with_shade.md | 4 ++-- 4 files changed, 17 insertions(+), 16 deletions(-) diff --git a/backend/src/controller/plant_layer.rs b/backend/src/controller/plant_layer.rs index c7a93beb6..3fc0dc952 100644 --- a/backend/src/controller/plant_layer.rs +++ b/backend/src/controller/plant_layer.rs @@ -14,7 +14,8 @@ use crate::{ /// Endpoint for generating a heatmap signaling ideal locations for planting the plant. /// -/// Grey pixels signal areas where the plant shouldn't be planted, while green areas signal ideal locations. +/// Red pixels signal areas where the plant shouldn't be planted, while green areas signal ideal locations. +/// The more transparent the location is the less data there is to support the claimed preference. /// /// The resulting heatmap does represent actual coordinates, meaning the pixel at (0,0) is not necessarily at coordinates (0,0). /// Instead the image has to be moved and scaled to fit inside the maps boundaries. diff --git a/backend/src/service/plant_layer.rs b/backend/src/service/plant_layer.rs index 2c2df7b7a..b5c42771b 100644 --- a/backend/src/service/plant_layer.rs +++ b/backend/src/service/plant_layer.rs @@ -58,9 +58,9 @@ fn matrix_to_image(matrix: &Vec>) -> Result, ServiceErro let (preference, relevance) = matrix[y as usize][x as usize]; // The closer data is to 1 the green it gets. - let red = preference.mul_add(-128.0, 128.0); - let green = preference.mul_add(255.0 - 128.0, 128.0); - let blue = preference.mul_add(-128.0, 128.0); + let red = preference.mul_add(-255.0, 255.0); + let green = preference * 255.0; + let blue = 0.0_f32; let alpha = relevance * 255.0; *pixel = Rgba([red as u8, green as u8, blue as u8, alpha as u8]); diff --git a/backend/src/test/plant_layer_heatmap.rs b/backend/src/test/plant_layer_heatmap.rs index f7f432a2c..721b36c59 100644 --- a/backend/src/test/plant_layer_heatmap.rs +++ b/backend/src/test/plant_layer_heatmap.rs @@ -122,7 +122,7 @@ async fn test_check_heatmap_dimensionality_succeeds() { assert_eq!( ((10 / GRANULARITY) as u32, (100 / GRANULARITY) as u32), image.dimensions() - ); // smaller by factor of 10 because of granularity + ); } #[actix_rt::test] @@ -155,7 +155,7 @@ async fn test_check_heatmap_non_0_xmin_succeeds() { } /// Test with a map geometry that excludes a corner. -/// The missing corner should be colored entirely in grey, as you cannot put plants there. +/// The missing corner should be transparent, as you cannot put plants there. #[actix_rt::test] async fn test_heatmap_with_missing_corner_succeeds() { let pool = init_test_database(|conn| { @@ -189,10 +189,10 @@ async fn test_heatmap_with_missing_corner_succeeds() { let top_right_pixel = image.get_pixel(8, 2); let bottom_left_pixel = image.get_pixel(2, 8); let bottom_right_pixel = image.get_pixel(8, 8); - assert_eq!([64, 191, 64, 51], top_left_pixel.0); - assert_eq!([64, 191, 64, 51], top_right_pixel.0); - assert_eq!([128, 128, 128, 0], bottom_left_pixel.0); - assert_eq!([64, 191, 64, 51], bottom_right_pixel.0); + assert_eq!([127, 127, 0, 51], top_left_pixel.0); + assert_eq!([127, 127, 0, 51], top_right_pixel.0); + assert_eq!([255, 0, 0, 0], bottom_left_pixel.0); + assert_eq!([127, 127, 0, 51], bottom_right_pixel.0); } #[actix_rt::test] @@ -239,10 +239,10 @@ async fn test_heatmap_with_shadings_succeeds() { // (0,0) is be top left. let top_left_pixel = image.get_pixel(1, 1); let bottom_right_pixel = image.get_pixel(40, 80); - // The shading is the exact opposite of the plants preference, therefore the map will be grey. - assert_eq!([128, 128, 128, 178], top_left_pixel.0); - // Green everywhere else. - assert_eq!([64, 191, 64, 51], bottom_right_pixel.0); + // The shading is the exact opposite of the plants preference, therefore the map will be red. + assert_eq!([255, 0, 0, 178], top_left_pixel.0); + // Yellow everywhere else. + assert_eq!([127, 127, 0, 51], bottom_right_pixel.0); } #[actix_rt::test] diff --git a/doc/tests/manual/reports/230723_heatmap_with_shade.md b/doc/tests/manual/reports/230723_heatmap_with_shade.md index 4ae54a265..bbbbf853d 100644 --- a/doc/tests/manual/reports/230723_heatmap_with_shade.md +++ b/doc/tests/manual/reports/230723_heatmap_with_shade.md @@ -234,5 +234,5 @@ curl -o file.png --location 'http://localhost:8080/api/maps/1/layers/plants/heat 7. Verify: -- The bottom left corner should be grey, everything else should be green. -- The top left corner should be greener than the rest as there is shade there and plant with id 1 likes shade. +- The bottom left corner should be transparent, everything else should be green. +- The top left corner should be green (as there is shade there and plant with id 1 likes shade); the rest should be yellow. From b8fe47a2708e7e31cc7f6ebd0e2b7bd309408ab5 Mon Sep 17 00:00:00 2001 From: kitzbergerg Date: Sun, 23 Jul 2023 22:02:49 +0200 Subject: [PATCH 014/157] change heatmap logging to trace --- backend/src/model/entity/plant_layer.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/backend/src/model/entity/plant_layer.rs b/backend/src/model/entity/plant_layer.rs index 43097b233..fc56d915d 100644 --- a/backend/src/model/entity/plant_layer.rs +++ b/backend/src/model/entity/plant_layer.rs @@ -7,7 +7,7 @@ use diesel::{ CombineDsl, ExpressionMethods, QueryDsl, QueryResult, QueryableByName, }; use diesel_async::{AsyncPgConnection, RunQueryDsl}; -use log::debug; +use log::{debug, trace}; use crate::{ model::{ @@ -108,7 +108,8 @@ pub async fn heatmap( { heatmap[y as usize][x as usize] = (preference, relevance); } - debug!("{heatmap:#?}"); + + trace!("{heatmap:#?}"); Ok(heatmap) } From 01fbc2556fa83c5d34d8e4bf833db2821d3f97c7 Mon Sep 17 00:00:00 2001 From: kitzbergerg Date: Sun, 23 Jul 2023 22:04:00 +0200 Subject: [PATCH 015/157] change default map size back to 1ha --- frontend/src/features/maps/routes/MapCreateForm.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/features/maps/routes/MapCreateForm.tsx b/frontend/src/features/maps/routes/MapCreateForm.tsx index 85e5b6e0d..c142b27a3 100644 --- a/frontend/src/features/maps/routes/MapCreateForm.tsx +++ b/frontend/src/features/maps/routes/MapCreateForm.tsx @@ -130,9 +130,9 @@ export default function MapCreateForm() { rings: [ [ { x: 0.0, y: 0.0 }, - { x: 0.0, y: 1_000.0 }, - { x: 1_000.0, y: 1_000.0 }, - { x: 1_000.0, y: 0.0 }, + { x: 0.0, y: 10_000.0 }, + { x: 10_000.0, y: 10_000.0 }, + { x: 10_000.0, y: 0.0 }, { x: 0.0, y: 0.0 }, ], ], From 07a1a050d6467bf5a4da296484252fe17dabd885 Mon Sep 17 00:00:00 2001 From: kitzbergerg Date: Sun, 23 Jul 2023 22:07:12 +0200 Subject: [PATCH 016/157] fix down.sql --- .../migrations/2023-07-03-165000_heatmap/down.sql | 1 + .../migrations/2023-07-22-110000_shadings/down.sql | 13 +++++++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/backend/migrations/2023-07-03-165000_heatmap/down.sql b/backend/migrations/2023-07-03-165000_heatmap/down.sql index 54ee815b7..9b5fe632a 100644 --- a/backend/migrations/2023-07-03-165000_heatmap/down.sql +++ b/backend/migrations/2023-07-03-165000_heatmap/down.sql @@ -4,5 +4,6 @@ DROP FUNCTION calculate_score_from_relations; DROP FUNCTION calculate_score; DROP FUNCTION scale_score; DROP FUNCTION calculate_heatmap; +DROP TYPE SCORE; DROP FUNCTION calculate_bbox; ALTER TABLE maps DROP COLUMN geometry; diff --git a/backend/migrations/2023-07-22-110000_shadings/down.sql b/backend/migrations/2023-07-22-110000_shadings/down.sql index d7191a72b..bf81441e1 100644 --- a/backend/migrations/2023-07-22-110000_shadings/down.sql +++ b/backend/migrations/2023-07-22-110000_shadings/down.sql @@ -8,14 +8,15 @@ CREATE OR REPLACE FUNCTION calculate_score( x_pos INTEGER, y_pos INTEGER ) -RETURNS REAL AS $$ +RETURNS SCORE AS $$ DECLARE - plant_relation RECORD; - distance REAL; - weight REAL; - score REAL := 0; + score SCORE; + plants SCORE; BEGIN - score := 0.5 + calculate_score_from_relations(p_layer_ids[1], p_plant_id, x_pos, y_pos); + plants := calculate_score_from_relations(p_layer_ids[1], p_plant_id, x_pos, y_pos); + + score.preference := 0.5 + plants.preference; + score.relevance := 0.2 + plants.relevance; RETURN score; END; From 5d16ad0a30cdc9525e80597b04164cc6dc299898 Mon Sep 17 00:00:00 2001 From: kitzbergerg Date: Tue, 25 Jul 2023 09:32:59 +0200 Subject: [PATCH 017/157] set SQL type formatting to uppercase --- backend/.sqlfluff | 2 +- .../2023-07-22-110000_shadings/up.sql | 34 +++++++++---------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/backend/.sqlfluff b/backend/.sqlfluff index 070b0a056..6aa7b483c 100644 --- a/backend/.sqlfluff +++ b/backend/.sqlfluff @@ -12,4 +12,4 @@ extended_capitalisation_policy = lower [sqlfluff:rules:capitalisation.literals] capitalisation_policy = lower [sqlfluff:rules:capitalisation.types] -extended_capitalisation_policy = lower +extended_capitalisation_policy = upper diff --git a/backend/migrations/2023-07-22-110000_shadings/up.sql b/backend/migrations/2023-07-22-110000_shadings/up.sql index 432ae9b65..3ff52c50e 100644 --- a/backend/migrations/2023-07-22-110000_shadings/up.sql +++ b/backend/migrations/2023-07-22-110000_shadings/up.sql @@ -1,15 +1,15 @@ -- Your SQL goes here CREATE TABLE shadings ( - id uuid PRIMARY KEY, - layer_id integer NOT NULL, - shade shade NOT NULL, + id UUID PRIMARY KEY, + layer_id INTEGER NOT NULL, + shade SHADE NOT NULL, geometry GEOMETRY (POLYGON, 4326) NOT NULL, - add_date date, - remove_date date, + add_date DATE, + remove_date DATE, FOREIGN KEY (layer_id) REFERENCES layers (id) ON DELETE CASCADE ); -CREATE FUNCTION check_shade_layer_type() RETURNS trigger +CREATE FUNCTION check_shade_layer_type() RETURNS TRIGGER LANGUAGE plpgsql AS $$ BEGIN @@ -32,13 +32,13 @@ FOR EACH ROW EXECUTE FUNCTION check_shade_layer_type(); -- p_plant_id ... id of the plant for which to consider relations -- x_pos,y_pos ... coordinates on the map where to calculate the score CREATE OR REPLACE FUNCTION calculate_score( - p_map_id integer, - p_layer_ids integer [], - p_plant_id integer, - x_pos integer, - y_pos integer + p_map_id INTEGER, + p_layer_ids INTEGER [], + p_plant_id INTEGER, + x_pos INTEGER, + y_pos INTEGER ) -RETURNS score AS $$ +RETURNS SCORE AS $$ DECLARE score SCORE; plants SCORE; @@ -57,14 +57,14 @@ $$ LANGUAGE plpgsql; -- Calculate preference: Between -0.5 and 0.5 depending on shadings. -- Calculate relevance: 0.5 if there is shading; otherwise 0.0. CREATE FUNCTION calculate_score_from_shadings( - p_layer_id integer, p_plant_id integer, x_pos integer, y_pos integer + p_layer_id INTEGER, p_plant_id INTEGER, x_pos INTEGER, y_pos INTEGER ) -RETURNS score AS $$ +RETURNS SCORE AS $$ DECLARE point GEOMETRY; - plant_shade shade; - shading_shade shade; - all_values shade[]; + plant_shade SHADE; + shading_shade SHADE; + all_values SHADE[]; pos1 INTEGER; pos2 INTEGER; score SCORE; From e6dc289e4b311f69db3c93e90785eb575b10b284 Mon Sep 17 00:00:00 2001 From: kitzbergerg Date: Tue, 25 Jul 2023 10:00:59 +0200 Subject: [PATCH 018/157] use typeshare.toml to map Polygon to ts object --- backend/src/model/dto.rs | 3 --- backend/src/model/dto/actions.rs | 2 -- backend/src/model/dto/shadings.rs | 3 --- backend/typeshare.toml | 1 + 4 files changed, 1 insertion(+), 8 deletions(-) diff --git a/backend/src/model/dto.rs b/backend/src/model/dto.rs index 5377b4136..70bb98fdf 100644 --- a/backend/src/model/dto.rs +++ b/backend/src/model/dto.rs @@ -239,7 +239,6 @@ pub struct MapDto { /// The geometry of the map. /// /// E.g. `{"rings": [[{"x": 0.0,"y": 0.0},{"x": 1000.0,"y": 0.0},{"x": 1000.0,"y": 1000.0},{"x": 0.0,"y": 1000.0},{"x": 0.0,"y": 0.0}]],"srid": 4326}` - #[typeshare(serialized_as = "object")] #[schema(value_type = Object)] pub geometry: Polygon, } @@ -275,7 +274,6 @@ pub struct NewMapDto { /// The geometry of the map. /// /// E.g. `{"rings": [[{"x": 0.0,"y": 0.0},{"x": 1000.0,"y": 0.0},{"x": 1000.0,"y": 1000.0},{"x": 0.0,"y": 1000.0},{"x": 0.0,"y": 0.0}]],"srid": 4326}` - #[typeshare(serialized_as = "object")] #[schema(value_type = Object)] pub geometry: Polygon, } @@ -295,7 +293,6 @@ pub struct UpdateMapDto { /// The geometry of the map. /// /// E.g. `{"rings": [[{"x": 0.0,"y": 0.0},{"x": 1000.0,"y": 0.0},{"x": 1000.0,"y": 1000.0},{"x": 0.0,"y": 1000.0},{"x": 0.0,"y": 0.0}]],"srid": 4326}` - #[typeshare(serialized_as = "Option")] #[schema(value_type = Option)] pub geometry: Option>, } diff --git a/backend/src/model/dto/actions.rs b/backend/src/model/dto/actions.rs index e78eb1b2d..209003783 100644 --- a/backend/src/model/dto/actions.rs +++ b/backend/src/model/dto/actions.rs @@ -232,7 +232,6 @@ pub struct CreateShadingActionPayload { id: Uuid, layer_id: i32, shade: Shade, - #[typeshare(serialized_as = "object")] geometry: Polygon, add_date: Option, remove_date: Option, @@ -263,7 +262,6 @@ pub struct UpdateShadingActionPayload { action_id: Uuid, id: Uuid, shade: Shade, - #[typeshare(serialized_as = "object")] geometry: Polygon, } diff --git a/backend/src/model/dto/shadings.rs b/backend/src/model/dto/shadings.rs index 463fa0e35..1ced338a5 100644 --- a/backend/src/model/dto/shadings.rs +++ b/backend/src/model/dto/shadings.rs @@ -23,7 +23,6 @@ pub struct ShadingDto { /// The position of the shade on the map. /// /// E.g. `{"rings": [[{"x": 0.0,"y": 0.0},{"x": 1000.0,"y": 0.0},{"x": 1000.0,"y": 1000.0},{"x": 0.0,"y": 1000.0},{"x": 0.0,"y": 0.0}]],"srid": 4326}` - #[typeshare(serialized_as = "object")] #[schema(value_type = Object)] pub geometry: Polygon, /// The date the shading was added to the map. @@ -48,7 +47,6 @@ pub struct NewShadingDto { /// The position of the shade on the map. /// /// E.g. `{"rings": [[{"x": 0.0,"y": 0.0},{"x": 1000.0,"y": 0.0},{"x": 1000.0,"y": 1000.0},{"x": 0.0,"y": 1000.0},{"x": 0.0,"y": 0.0}]],"srid": 4326}` - #[typeshare(serialized_as = "object")] #[schema(value_type = Object)] pub geometry: Polygon, /// The date the shading was added to the map. @@ -83,7 +81,6 @@ pub struct UpdateValuesShadingDto { /// The position of the shade on the map. /// /// E.g. `{"rings": [[{"x": 0.0,"y": 0.0},{"x": 1000.0,"y": 0.0},{"x": 1000.0,"y": 1000.0},{"x": 0.0,"y": 1000.0},{"x": 0.0,"y": 0.0}]],"srid": 4326}` - #[typeshare(serialized_as = "Option")] #[schema(value_type = Option)] pub geometry: Option>, /// Id of the action (for identifying the action in the frontend). diff --git a/backend/typeshare.toml b/backend/typeshare.toml index c2b8fc3a6..f497c62b9 100644 --- a/backend/typeshare.toml +++ b/backend/typeshare.toml @@ -1,3 +1,4 @@ [typescript.type_mappings] "NaiveDate" = "string" "Uuid" = "string" +"Polygon" = "object" From d2f8aec92a3a498b7e82c4c20f55306ca960992e Mon Sep 17 00:00:00 2001 From: kitzbergerg Date: Tue, 25 Jul 2023 12:26:46 +0200 Subject: [PATCH 019/157] changes according to suggestions, include light requirement in shade calculation --- .../2023-07-03-165000_heatmap/up.sql | 7 ++-- .../2023-07-22-110000_shadings/down.sql | 2 +- .../2023-07-22-110000_shadings/up.sql | 35 +++++++++++++++---- backend/src/test/plant_layer_heatmap.rs | 12 +++---- 4 files changed, 39 insertions(+), 17 deletions(-) diff --git a/backend/migrations/2023-07-03-165000_heatmap/up.sql b/backend/migrations/2023-07-03-165000_heatmap/up.sql index 405f492a5..8a30298d3 100644 --- a/backend/migrations/2023-07-03-165000_heatmap/up.sql +++ b/backend/migrations/2023-07-03-165000_heatmap/up.sql @@ -27,7 +27,7 @@ CREATE TYPE score AS ( relevance REAL ); --- Returns scores from 0-1 for each pixel of the map. +-- Returns preference from 0-1 and relevance from 0-1 for each pixel of the map. -- -- Positions where the plant should not be placed have preference close to 0. -- Positions where the plant should be placed have preference close to 1. @@ -35,7 +35,7 @@ CREATE TYPE score AS ( -- Positions where there is no relevant data have relevance close to 0. -- Positions where there is relevant data have relevance close to 1. -- --- The resulting matrix does not contain valid (x,y) coordinates, +-- The resulting matrix does not contain valid (x,y) map coordinates, -- instead (x,y) are simply the indices in the matrix. -- The (x,y) coordinate of the computed heatmap always starts at -- (0,0) no matter the boundaries of the map. @@ -118,6 +118,7 @@ END; $$ LANGUAGE plpgsql; -- Scales to values between 0 and 1. +-- TODO: Test different functions such as sigmoid and test performance CREATE OR REPLACE FUNCTION scale_score(input SCORE) RETURNS SCORE AS $$ DECLARE @@ -150,7 +151,7 @@ BEGIN plants := calculate_score_from_relations(p_layer_ids[1], p_plant_id, x_pos, y_pos); score.preference := 0.5 + plants.preference; - score.relevance := 0.2 + plants.relevance; + score.relevance := plants.relevance; RETURN score; END; diff --git a/backend/migrations/2023-07-22-110000_shadings/down.sql b/backend/migrations/2023-07-22-110000_shadings/down.sql index bf81441e1..3c6d10dbb 100644 --- a/backend/migrations/2023-07-22-110000_shadings/down.sql +++ b/backend/migrations/2023-07-22-110000_shadings/down.sql @@ -16,7 +16,7 @@ BEGIN plants := calculate_score_from_relations(p_layer_ids[1], p_plant_id, x_pos, y_pos); score.preference := 0.5 + plants.preference; - score.relevance := 0.2 + plants.relevance; + score.relevance := plants.relevance; RETURN score; END; diff --git a/backend/migrations/2023-07-22-110000_shadings/up.sql b/backend/migrations/2023-07-22-110000_shadings/up.sql index 3ff52c50e..5a3f7aaef 100644 --- a/backend/migrations/2023-07-22-110000_shadings/up.sql +++ b/backend/migrations/2023-07-22-110000_shadings/up.sql @@ -48,7 +48,7 @@ BEGIN shades := calculate_score_from_shadings(p_layer_ids[2], p_plant_id, x_pos, y_pos); score.preference := 0.5 + plants.preference + shades.preference; - score.relevance := 0.2 + plants.relevance + shades.relevance; + score.relevance := plants.relevance + shades.relevance; RETURN score; END; @@ -56,6 +56,7 @@ $$ LANGUAGE plpgsql; -- Calculate preference: Between -0.5 and 0.5 depending on shadings. -- Calculate relevance: 0.5 if there is shading; otherwise 0.0. +-- If the plant is guaranteed to die set preference to -100 and relevance to 1. CREATE FUNCTION calculate_score_from_shadings( p_layer_id INTEGER, p_plant_id INTEGER, x_pos INTEGER, y_pos INTEGER ) @@ -63,14 +64,15 @@ RETURNS SCORE AS $$ DECLARE point GEOMETRY; plant_shade SHADE; + plant_light_requirement light_requirement; shading_shade SHADE; all_values SHADE[]; pos1 INTEGER; pos2 INTEGER; score SCORE; BEGIN - -- Get the preferred shade of the plant - SELECT shade INTO plant_shade + -- Get the required light level and preferred shade level of the plant + SELECT light_requirement, shade INTO plant_light_requirement, plant_shade FROM plants WHERE id = p_plant_id; @@ -83,16 +85,35 @@ BEGIN ORDER BY shade DESC LIMIT 1; - -- If there's no shading, return 0 (meaning shadings do not affect the score) - IF NOT FOUND OR plant_shade IS NULL THEN + -- If there's no shading, then there is sun. + IF NOT FOUND THEN + shading_shade := 'no shade'; + END IF; + + -- Check if the plant can survive at the position. + -- If the plant can't survive set the score to -100 and relevance to 1. + IF plant_light_requirement IS NOT NULL AND + ( + (plant_light_requirement = 'full sun' AND shading_shade != 'no shade') OR + ( + plant_light_requirement = 'partial sun/shade' AND + (shading_shade = 'permanent shade' OR shading_shade = 'permanent deep shade') + ) + ) THEN + score.preference := -100; + score.relevance := 1; + RETURN score; + END IF; + + -- If there's no shading, return 0. + IF plant_shade IS NULL THEN score.preference := 0.0; score.relevance := 0.0; RETURN score; END IF; - -- Get all possible enum values - SELECT enum_range(NULL::shade) INTO all_values; + SELECT enum_range(NULL::SHADE) INTO all_values; -- Get the position of each enum value in the array SELECT array_position(all_values, plant_shade) INTO pos1; diff --git a/backend/src/test/plant_layer_heatmap.rs b/backend/src/test/plant_layer_heatmap.rs index 721b36c59..5862a2461 100644 --- a/backend/src/test/plant_layer_heatmap.rs +++ b/backend/src/test/plant_layer_heatmap.rs @@ -189,10 +189,10 @@ async fn test_heatmap_with_missing_corner_succeeds() { let top_right_pixel = image.get_pixel(8, 2); let bottom_left_pixel = image.get_pixel(2, 8); let bottom_right_pixel = image.get_pixel(8, 8); - assert_eq!([127, 127, 0, 51], top_left_pixel.0); - assert_eq!([127, 127, 0, 51], top_right_pixel.0); + assert_eq!([0, 255, 0, 127], top_left_pixel.0); + assert_eq!([0, 255, 0, 127], top_right_pixel.0); assert_eq!([255, 0, 0, 0], bottom_left_pixel.0); - assert_eq!([127, 127, 0, 51], bottom_right_pixel.0); + assert_eq!([0, 255, 0, 127], bottom_right_pixel.0); } #[actix_rt::test] @@ -240,9 +240,9 @@ async fn test_heatmap_with_shadings_succeeds() { let top_left_pixel = image.get_pixel(1, 1); let bottom_right_pixel = image.get_pixel(40, 80); // The shading is the exact opposite of the plants preference, therefore the map will be red. - assert_eq!([255, 0, 0, 178], top_left_pixel.0); - // Yellow everywhere else. - assert_eq!([127, 127, 0, 51], bottom_right_pixel.0); + assert_eq!([255, 0, 0, 127], top_left_pixel.0); + // Plant like other positions, therefore green. + assert_eq!([0, 255, 0, 127], bottom_right_pixel.0); } #[actix_rt::test] From 20cab0ed44635095040056cb1582110dacedf744 Mon Sep 17 00:00:00 2001 From: kitzbergerg Date: Tue, 25 Jul 2023 13:03:09 +0200 Subject: [PATCH 020/157] add test for light requirement --- .../2023-07-22-110000_shadings/up.sql | 23 +++++-- backend/src/test/plant_layer_heatmap.rs | 66 ++++++++++++++++++- 2 files changed, 81 insertions(+), 8 deletions(-) diff --git a/backend/migrations/2023-07-22-110000_shadings/up.sql b/backend/migrations/2023-07-22-110000_shadings/up.sql index 5a3f7aaef..bc5a7473a 100644 --- a/backend/migrations/2023-07-22-110000_shadings/up.sql +++ b/backend/migrations/2023-07-22-110000_shadings/up.sql @@ -64,7 +64,7 @@ RETURNS SCORE AS $$ DECLARE point GEOMETRY; plant_shade SHADE; - plant_light_requirement light_requirement; + plant_light_requirement light_requirement []; shading_shade SHADE; all_values SHADE[]; pos1 INTEGER; @@ -92,14 +92,23 @@ BEGIN -- Check if the plant can survive at the position. -- If the plant can't survive set the score to -100 and relevance to 1. - IF plant_light_requirement IS NOT NULL AND - ( - (plant_light_requirement = 'full sun' AND shading_shade != 'no shade') OR + IF plant_light_requirement IS NOT NULL AND ( ( - plant_light_requirement = 'partial sun/shade' AND - (shading_shade = 'permanent shade' OR shading_shade = 'permanent deep shade') + -- If the light_requirement contains 'full sun' the plant needs sun. + 'full sun' = ANY(plant_light_requirement) AND shading_shade NOT IN ('no shade', 'light shade') ) - ) THEN + OR + ( + -- If the light_requirement contains 'partial sun/shade' the plant is ok with a bit of sun or shade. + 'partial sun/shade' = ANY(plant_light_requirement) AND shading_shade NOT IN ('light shade', 'partial shade', 'permanent shade') + ) + OR + ( + -- If the light_requirement contains 'full shade' sun the plant needs shade. + 'full shade' = ANY(plant_light_requirement) AND shading_shade NOT IN ('permanent shade', 'permanent deep shade') + ) + ) + THEN score.preference := -100; score.relevance := 1; RETURN score; diff --git a/backend/src/test/plant_layer_heatmap.rs b/backend/src/test/plant_layer_heatmap.rs index 5862a2461..1b5d4fc48 100644 --- a/backend/src/test/plant_layer_heatmap.rs +++ b/backend/src/test/plant_layer_heatmap.rs @@ -17,7 +17,10 @@ use crate::{ error::ServiceError, model::{ entity::plant_layer::GRANULARITY, - r#enum::{layer_type::LayerType, privacy_option::PrivacyOption, shade::Shade}, + r#enum::{ + layer_type::LayerType, light_requirement::LightRequirement, + privacy_option::PrivacyOption, shade::Shade, + }, }, test::util::{ dummy_map_polygons::{ @@ -245,6 +248,67 @@ async fn test_heatmap_with_shadings_succeeds() { assert_eq!([0, 255, 0, 127], bottom_right_pixel.0); } +#[actix_rt::test] +async fn test_heatmap_with_shadings_and_light_requirement_succeeds() { + let pool = init_test_database(|conn| { + async { + initial_db_values(conn, tall_rectangle()).await?; + diesel::insert_into(crate::schema::plants::table) + .values(( + &crate::schema::plants::id.eq(-2), + &crate::schema::plants::unique_name.eq("Testia"), + &crate::schema::plants::common_name_en.eq(Some(vec![Some("T".to_owned())])), + &crate::schema::plants::shade.eq(Some(Shade::PermanentDeepShade)), + &crate::schema::plants::light_requirement + .eq(Some(vec![Some(LightRequirement::FullShade)])), + )) + .execute(conn) + .await?; + diesel::insert_into(crate::schema::shadings::table) + .values(( + &crate::schema::shadings::id.eq(Uuid::new_v4()), + &crate::schema::shadings::layer_id.eq(-2), + &crate::schema::shadings::shade.eq(Shade::PermanentDeepShade), + &crate::schema::shadings::geometry.eq(small_square()), + )) + .execute(conn) + .await?; + Ok(()) + } + .scope_boxed() + }) + .await; + let (token, app) = init_test_app(pool.clone()).await; + + let resp = test::TestRequest::get() + .uri("/api/maps/-1/layers/plants/heatmap?plant_id=-2&plant_layer_id=-1&shade_layer_id=-2") + .insert_header((header::AUTHORIZATION, token)) + .send_request(&app) + .await; + + assert_eq!(resp.status(), StatusCode::OK); + assert_eq!( + resp.headers().get(header::CONTENT_TYPE), + Some(&header::HeaderValue::from_static("image/png")) + ); + let result = test::read_body(resp).await; + let result = &result.bytes().collect::, _>>().unwrap(); + let image = load_from_memory_with_format(result.as_slice(), image::ImageFormat::Png).unwrap(); + let image = image.as_rgba8().unwrap(); + assert_eq!( + ((500 / GRANULARITY) as u32, (1000 / GRANULARITY) as u32), + image.dimensions() + ); + + // (0,0) is be top left. + let top_left_pixel = image.get_pixel(1, 1); + let bottom_right_pixel = image.get_pixel(40, 80); + // The shading is deep shade with is ok for the plant. + assert_eq!([0, 255, 0, 127], top_left_pixel.0); + // The plant can't grow in sun. + assert_eq!([255, 0, 0, 255], bottom_right_pixel.0); +} + #[actix_rt::test] async fn test_missing_entities_fails() { let pool = init_test_database(|conn| { From 94353068ef2942a15cdec3ae92332dd4a7e19557 Mon Sep 17 00:00:00 2001 From: kitzbergerg Date: Tue, 25 Jul 2023 15:00:53 +0200 Subject: [PATCH 021/157] add heatmap test for plant relations --- .../2023-07-03-165000_heatmap/up.sql | 7 +- backend/src/test/plant_layer_heatmap.rs | 75 ++++++++++++++++++- 2 files changed, 79 insertions(+), 3 deletions(-) diff --git a/backend/migrations/2023-07-03-165000_heatmap/up.sql b/backend/migrations/2023-07-03-165000_heatmap/up.sql index 8a30298d3..0a303c1cf 100644 --- a/backend/migrations/2023-07-03-165000_heatmap/up.sql +++ b/backend/migrations/2023-07-03-165000_heatmap/up.sql @@ -172,11 +172,14 @@ BEGIN score.relevance := 0.0; FOR plant_relation IN (SELECT * FROM get_plant_relations(p_layer_id, p_plant_id)) LOOP - -- calculate distance + -- calculate euclidean distance distance := sqrt((plant_relation.x - x_pos)^2 + (plant_relation.y - y_pos)^2); -- calculate weight based on distance - weight := 1 / (distance + 1); + -- weight decreases between 1 and 0 based on distance + -- distance is squared so it decreases faster the further away + -- weight is halved at 50 cm away + weight := 1 / (1 + (distance / 50)^2); -- update score based on relation IF plant_relation.relation = 'companion' THEN diff --git a/backend/src/test/plant_layer_heatmap.rs b/backend/src/test/plant_layer_heatmap.rs index 1b5d4fc48..92df0b7f6 100644 --- a/backend/src/test/plant_layer_heatmap.rs +++ b/backend/src/test/plant_layer_heatmap.rs @@ -19,10 +19,11 @@ use crate::{ entity::plant_layer::GRANULARITY, r#enum::{ layer_type::LayerType, light_requirement::LightRequirement, - privacy_option::PrivacyOption, shade::Shade, + privacy_option::PrivacyOption, relation_type::RelationType, shade::Shade, }, }, test::util::{ + data, dummy_map_polygons::{ rectangle_with_missing_bottom_left_corner, small_rectangle, small_rectangle_with_non_0_xmin, small_square, tall_rectangle, @@ -309,6 +310,78 @@ async fn test_heatmap_with_shadings_and_light_requirement_succeeds() { assert_eq!([255, 0, 0, 255], bottom_right_pixel.0); } +#[actix_rt::test] +async fn test_heatmap_with_plantings_succeeds() { + let pool = init_test_database(|conn| { + async { + initial_db_values(conn, tall_rectangle()).await?; + diesel::insert_into(crate::schema::plants::table) + .values(( + &crate::schema::plants::id.eq(-2), + &crate::schema::plants::unique_name.eq("Testia"), + &crate::schema::plants::common_name_en.eq(Some(vec![Some("T".to_owned())])), + )) + .execute(conn) + .await?; + diesel::insert_into(crate::schema::relations::table) + .values(vec![( + &crate::schema::relations::plant1.eq(-1), + &crate::schema::relations::plant2.eq(-2), + &crate::schema::relations::relation.eq(RelationType::Companion), + )]) + .execute(conn) + .await?; + diesel::insert_into(crate::schema::plantings::table) + .values(vec![data::TestInsertablePlanting { + id: Uuid::new_v4(), + layer_id: -1, + plant_id: -1, + x: 15, + y: 15, + ..Default::default() + }]) + .execute(conn) + .await?; + Ok(()) + } + .scope_boxed() + }) + .await; + let (token, app) = init_test_app(pool.clone()).await; + + let resp = test::TestRequest::get() + .uri("/api/maps/-1/layers/plants/heatmap?plant_id=-2&plant_layer_id=-1&shade_layer_id=-2") + .insert_header((header::AUTHORIZATION, token)) + .send_request(&app) + .await; + + assert_eq!(resp.status(), StatusCode::OK); + assert_eq!( + resp.headers().get(header::CONTENT_TYPE), + Some(&header::HeaderValue::from_static("image/png")) + ); + let result = test::read_body(resp).await; + let result = &result.bytes().collect::, _>>().unwrap(); + let image = load_from_memory_with_format(result.as_slice(), image::ImageFormat::Png).unwrap(); + let image = image.as_rgba8().unwrap(); + assert_eq!( + ((500 / GRANULARITY) as u32, (1000 / GRANULARITY) as u32), + image.dimensions() + ); + + // (0,0) is be top left. + let on_planting = image.get_pixel(1, 1); + let close_to_planting = image.get_pixel(2, 2); + let a_bit_away_from_planting = image.get_pixel(10, 10); + let far_away_from_planting = image.get_pixel(40, 80); + // The planting influences the map. + assert_eq!([0, 255, 0, 127], on_planting.0); + assert_eq!([9, 245, 0, 118], close_to_planting.0); + assert_eq!([110, 144, 0, 17], a_bit_away_from_planting.0); + // There is no influence on locations far away. + assert_eq!([127, 127, 0, 0], far_away_from_planting.0); +} + #[actix_rt::test] async fn test_missing_entities_fails() { let pool = init_test_database(|conn| { From d64613ca0cd8aedd09d7d36cbf4c14a62b86ac46 Mon Sep 17 00:00:00 2001 From: kitzbergerg Date: Tue, 25 Jul 2023 16:08:43 +0200 Subject: [PATCH 022/157] fix light requirement calculation and add test case --- .../2023-07-22-110000_shadings/up.sql | 40 +++++++------ backend/src/test/plant_layer_heatmap.rs | 58 ++++++++++++++++--- 2 files changed, 70 insertions(+), 28 deletions(-) diff --git a/backend/migrations/2023-07-22-110000_shadings/up.sql b/backend/migrations/2023-07-22-110000_shadings/up.sql index bc5a7473a..6555fd400 100644 --- a/backend/migrations/2023-07-22-110000_shadings/up.sql +++ b/backend/migrations/2023-07-22-110000_shadings/up.sql @@ -65,6 +65,7 @@ DECLARE point GEOMETRY; plant_shade SHADE; plant_light_requirement light_requirement []; + allowed_shades SHADE [] := '{}'; shading_shade SHADE; all_values SHADE[]; pos1 INTEGER; @@ -92,26 +93,27 @@ BEGIN -- Check if the plant can survive at the position. -- If the plant can't survive set the score to -100 and relevance to 1. - IF plant_light_requirement IS NOT NULL AND ( - ( - -- If the light_requirement contains 'full sun' the plant needs sun. - 'full sun' = ANY(plant_light_requirement) AND shading_shade NOT IN ('no shade', 'light shade') - ) - OR - ( - -- If the light_requirement contains 'partial sun/shade' the plant is ok with a bit of sun or shade. - 'partial sun/shade' = ANY(plant_light_requirement) AND shading_shade NOT IN ('light shade', 'partial shade', 'permanent shade') - ) - OR - ( - -- If the light_requirement contains 'full shade' sun the plant needs shade. - 'full shade' = ANY(plant_light_requirement) AND shading_shade NOT IN ('permanent shade', 'permanent deep shade') - ) - ) + IF plant_light_requirement IS NOT NULL THEN - score.preference := -100; - score.relevance := 1; - RETURN score; + IF 'full sun' = ANY(plant_light_requirement) + THEN + allowed_shades := allowed_shades || '{"no shade", "light shade"}'; + END IF; + IF 'partial sun/shade' = ANY(plant_light_requirement) + THEN + allowed_shades := allowed_shades || '{"light shade", "partial shade", "permanent shade"}'; + END IF; + IF 'full shade' = ANY(plant_light_requirement) + THEN + allowed_shades := allowed_shades || '{"permanent shade", "permanent deep shade"}'; + END IF; + + IF NOT (shading_shade = ANY(allowed_shades)) + THEN + score.preference := -100; + score.relevance := 1; + RETURN score; + END IF; END IF; -- If there's no shading, return 0. diff --git a/backend/src/test/plant_layer_heatmap.rs b/backend/src/test/plant_layer_heatmap.rs index 92df0b7f6..c527da503 100644 --- a/backend/src/test/plant_layer_heatmap.rs +++ b/backend/src/test/plant_layer_heatmap.rs @@ -255,14 +255,26 @@ async fn test_heatmap_with_shadings_and_light_requirement_succeeds() { async { initial_db_values(conn, tall_rectangle()).await?; diesel::insert_into(crate::schema::plants::table) - .values(( - &crate::schema::plants::id.eq(-2), - &crate::schema::plants::unique_name.eq("Testia"), - &crate::schema::plants::common_name_en.eq(Some(vec![Some("T".to_owned())])), - &crate::schema::plants::shade.eq(Some(Shade::PermanentDeepShade)), - &crate::schema::plants::light_requirement - .eq(Some(vec![Some(LightRequirement::FullShade)])), - )) + .values(vec![ + ( + &crate::schema::plants::id.eq(-2), + &crate::schema::plants::unique_name.eq("Testia"), + &crate::schema::plants::common_name_en.eq(Some(vec![Some("T".to_owned())])), + &crate::schema::plants::shade.eq(Some(Shade::PermanentDeepShade)), + &crate::schema::plants::light_requirement + .eq(Some(vec![Some(LightRequirement::FullShade)])), + ), + ( + &crate::schema::plants::id.eq(-3), + &crate::schema::plants::unique_name.eq("Testia testum"), + &crate::schema::plants::common_name_en.eq(Some(vec![Some("T".to_owned())])), + &crate::schema::plants::shade.eq(Some(Shade::LightShade)), + &crate::schema::plants::light_requirement.eq(Some(vec![ + Some(LightRequirement::Full), + Some(LightRequirement::Partial), + ])), + ), + ]) .execute(conn) .await?; diesel::insert_into(crate::schema::shadings::table) @@ -283,7 +295,7 @@ async fn test_heatmap_with_shadings_and_light_requirement_succeeds() { let resp = test::TestRequest::get() .uri("/api/maps/-1/layers/plants/heatmap?plant_id=-2&plant_layer_id=-1&shade_layer_id=-2") - .insert_header((header::AUTHORIZATION, token)) + .insert_header((header::AUTHORIZATION, token.clone())) .send_request(&app) .await; @@ -308,6 +320,34 @@ async fn test_heatmap_with_shadings_and_light_requirement_succeeds() { assert_eq!([0, 255, 0, 127], top_left_pixel.0); // The plant can't grow in sun. assert_eq!([255, 0, 0, 255], bottom_right_pixel.0); + + let resp = test::TestRequest::get() + .uri("/api/maps/-1/layers/plants/heatmap?plant_id=-3&plant_layer_id=-1&shade_layer_id=-2") + .insert_header((header::AUTHORIZATION, token)) + .send_request(&app) + .await; + + assert_eq!(resp.status(), StatusCode::OK); + assert_eq!( + resp.headers().get(header::CONTENT_TYPE), + Some(&header::HeaderValue::from_static("image/png")) + ); + let result = test::read_body(resp).await; + let result = &result.bytes().collect::, _>>().unwrap(); + let image = load_from_memory_with_format(result.as_slice(), image::ImageFormat::Png).unwrap(); + let image = image.as_rgba8().unwrap(); + assert_eq!( + ((500 / GRANULARITY) as u32, (1000 / GRANULARITY) as u32), + image.dimensions() + ); + + // (0,0) is be top left. + let top_left_pixel = image.get_pixel(1, 1); + let bottom_right_pixel = image.get_pixel(40, 80); + // The plant can't grow in deep shade. + assert_eq!([255, 0, 0, 255], top_left_pixel.0); + // The plant can grow in sun. + assert_eq!([63, 191, 0, 127], bottom_right_pixel.0); } #[actix_rt::test] From d05be159b0d11c0508edad5fa24b03132aff43f6 Mon Sep 17 00:00:00 2001 From: kitzbergerg Date: Tue, 25 Jul 2023 16:33:17 +0200 Subject: [PATCH 023/157] reduce influence of shading on preference --- .../migrations/2023-07-22-110000_shadings/up.sql | 2 +- backend/src/test/plant_layer_heatmap.rs | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/backend/migrations/2023-07-22-110000_shadings/up.sql b/backend/migrations/2023-07-22-110000_shadings/up.sql index 6555fd400..1171b8983 100644 --- a/backend/migrations/2023-07-22-110000_shadings/up.sql +++ b/backend/migrations/2023-07-22-110000_shadings/up.sql @@ -131,7 +131,7 @@ BEGIN SELECT array_position(all_values, shading_shade) INTO pos2; -- Calculate the 'distance' to the preferred shade as a values between -0.5 and 0.5 - score.preference := 0.5 - (abs(pos1 - pos2) / (ARRAY_LENGTH(all_values, 1) - 1)::REAL); + score.preference := (0.5 - (abs(pos1 - pos2) / (ARRAY_LENGTH(all_values, 1) - 1)::REAL)) / 2; score.relevance := 0.5; RETURN score; diff --git a/backend/src/test/plant_layer_heatmap.rs b/backend/src/test/plant_layer_heatmap.rs index c527da503..35331201d 100644 --- a/backend/src/test/plant_layer_heatmap.rs +++ b/backend/src/test/plant_layer_heatmap.rs @@ -193,10 +193,10 @@ async fn test_heatmap_with_missing_corner_succeeds() { let top_right_pixel = image.get_pixel(8, 2); let bottom_left_pixel = image.get_pixel(2, 8); let bottom_right_pixel = image.get_pixel(8, 8); - assert_eq!([0, 255, 0, 127], top_left_pixel.0); - assert_eq!([0, 255, 0, 127], top_right_pixel.0); + assert_eq!([63, 191, 0, 127], top_left_pixel.0); + assert_eq!([63, 191, 0, 127], top_right_pixel.0); assert_eq!([255, 0, 0, 0], bottom_left_pixel.0); - assert_eq!([0, 255, 0, 127], bottom_right_pixel.0); + assert_eq!([63, 191, 0, 127], bottom_right_pixel.0); } #[actix_rt::test] @@ -244,9 +244,9 @@ async fn test_heatmap_with_shadings_succeeds() { let top_left_pixel = image.get_pixel(1, 1); let bottom_right_pixel = image.get_pixel(40, 80); // The shading is the exact opposite of the plants preference, therefore the map will be red. - assert_eq!([255, 0, 0, 127], top_left_pixel.0); + assert_eq!([191, 63, 0, 127], top_left_pixel.0); // Plant like other positions, therefore green. - assert_eq!([0, 255, 0, 127], bottom_right_pixel.0); + assert_eq!([63, 191, 0, 127], bottom_right_pixel.0); } #[actix_rt::test] @@ -317,7 +317,7 @@ async fn test_heatmap_with_shadings_and_light_requirement_succeeds() { let top_left_pixel = image.get_pixel(1, 1); let bottom_right_pixel = image.get_pixel(40, 80); // The shading is deep shade with is ok for the plant. - assert_eq!([0, 255, 0, 127], top_left_pixel.0); + assert_eq!([63, 191, 0, 127], top_left_pixel.0); // The plant can't grow in sun. assert_eq!([255, 0, 0, 255], bottom_right_pixel.0); @@ -347,7 +347,7 @@ async fn test_heatmap_with_shadings_and_light_requirement_succeeds() { // The plant can't grow in deep shade. assert_eq!([255, 0, 0, 255], top_left_pixel.0); // The plant can grow in sun. - assert_eq!([63, 191, 0, 127], bottom_right_pixel.0); + assert_eq!([95, 159, 0, 127], bottom_right_pixel.0); } #[actix_rt::test] From da4b884452fd2e663fdcd73ac48943f6cc39c1f6 Mon Sep 17 00:00:00 2001 From: kitzbergerg Date: Tue, 25 Jul 2023 16:54:16 +0200 Subject: [PATCH 024/157] heatmap: add checks that layers are of correct type --- backend/migrations/2023-07-03-165000_heatmap/up.sql | 4 ++++ backend/migrations/2023-07-22-110000_shadings/up.sql | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/backend/migrations/2023-07-03-165000_heatmap/up.sql b/backend/migrations/2023-07-03-165000_heatmap/up.sql index 0a303c1cf..7acebacf3 100644 --- a/backend/migrations/2023-07-03-165000_heatmap/up.sql +++ b/backend/migrations/2023-07-03-165000_heatmap/up.sql @@ -168,6 +168,10 @@ DECLARE weight REAL; score SCORE; BEGIN + IF NOT EXISTS (SELECT 1 FROM layers WHERE id = p_layer_id AND type = 'plants') THEN + RAISE EXCEPTION 'Plant layer with id % not found', p_layer_id; + END IF; + score.preference := 0.0; score.relevance := 0.0; diff --git a/backend/migrations/2023-07-22-110000_shadings/up.sql b/backend/migrations/2023-07-22-110000_shadings/up.sql index 1171b8983..21495ff11 100644 --- a/backend/migrations/2023-07-22-110000_shadings/up.sql +++ b/backend/migrations/2023-07-22-110000_shadings/up.sql @@ -72,6 +72,10 @@ DECLARE pos2 INTEGER; score SCORE; BEGIN + IF NOT EXISTS (SELECT 1 FROM layers WHERE id = p_layer_id AND type = 'shade') THEN + RAISE EXCEPTION 'Shade layer with id % not found', p_layer_id; + END IF; + -- Get the required light level and preferred shade level of the plant SELECT light_requirement, shade INTO plant_light_requirement, plant_shade FROM plants From c05804cfdbc5b9afbadb2ce261910a874b03e421 Mon Sep 17 00:00:00 2001 From: kitzbergerg Date: Tue, 25 Jul 2023 17:31:02 +0200 Subject: [PATCH 025/157] heatmap: use sigmoid as scaling function --- .../2023-07-03-165000_heatmap/up.sql | 10 ++++++---- .../2023-07-22-110000_shadings/up.sql | 9 +++++---- backend/src/test/plant_layer_heatmap.rs | 20 +++++++++---------- 3 files changed, 21 insertions(+), 18 deletions(-) diff --git a/backend/migrations/2023-07-03-165000_heatmap/up.sql b/backend/migrations/2023-07-03-165000_heatmap/up.sql index 7acebacf3..38b054e60 100644 --- a/backend/migrations/2023-07-03-165000_heatmap/up.sql +++ b/backend/migrations/2023-07-03-165000_heatmap/up.sql @@ -118,14 +118,16 @@ END; $$ LANGUAGE plpgsql; -- Scales to values between 0 and 1. --- TODO: Test different functions such as sigmoid and test performance +-- +-- Preference input space: Any value. +-- Relevance input space: >=0 CREATE OR REPLACE FUNCTION scale_score(input SCORE) RETURNS SCORE AS $$ DECLARE score SCORE; BEGIN - score.preference := LEAST(GREATEST(input.preference, 0), 1); - score.relevance := LEAST(GREATEST(input.relevance, 0), 1); + score.preference := 1 / (1 + exp(-input.preference)); -- standard sigmoid so f(0)=0.5 + score.relevance := (2 / (1 + exp(-input.relevance))) - 1; -- modified sigmoid so f(0)=0 RETURN score; END; $$ LANGUAGE plpgsql; @@ -150,7 +152,7 @@ DECLARE BEGIN plants := calculate_score_from_relations(p_layer_ids[1], p_plant_id, x_pos, y_pos); - score.preference := 0.5 + plants.preference; + score.preference := plants.preference; score.relevance := plants.relevance; RETURN score; diff --git a/backend/migrations/2023-07-22-110000_shadings/up.sql b/backend/migrations/2023-07-22-110000_shadings/up.sql index 21495ff11..f779ead8a 100644 --- a/backend/migrations/2023-07-22-110000_shadings/up.sql +++ b/backend/migrations/2023-07-22-110000_shadings/up.sql @@ -47,7 +47,7 @@ BEGIN plants := calculate_score_from_relations(p_layer_ids[1], p_plant_id, x_pos, y_pos); shades := calculate_score_from_shadings(p_layer_ids[2], p_plant_id, x_pos, y_pos); - score.preference := 0.5 + plants.preference + shades.preference; + score.preference := plants.preference + shades.preference; score.relevance := plants.relevance + shades.relevance; RETURN score; @@ -56,7 +56,8 @@ $$ LANGUAGE plpgsql; -- Calculate preference: Between -0.5 and 0.5 depending on shadings. -- Calculate relevance: 0.5 if there is shading; otherwise 0.0. --- If the plant is guaranteed to die set preference to -100 and relevance to 1. +-- +-- If the plant would die at the position set preference=-100 and relevance=100. CREATE FUNCTION calculate_score_from_shadings( p_layer_id INTEGER, p_plant_id INTEGER, x_pos INTEGER, y_pos INTEGER ) @@ -96,7 +97,7 @@ BEGIN END IF; -- Check if the plant can survive at the position. - -- If the plant can't survive set the score to -100 and relevance to 1. + -- If the plant can't survive set preference=-100 and relevance=100. IF plant_light_requirement IS NOT NULL THEN IF 'full sun' = ANY(plant_light_requirement) @@ -115,7 +116,7 @@ BEGIN IF NOT (shading_shade = ANY(allowed_shades)) THEN score.preference := -100; - score.relevance := 1; + score.relevance := 100; RETURN score; END IF; END IF; diff --git a/backend/src/test/plant_layer_heatmap.rs b/backend/src/test/plant_layer_heatmap.rs index 35331201d..3c4bdfcf9 100644 --- a/backend/src/test/plant_layer_heatmap.rs +++ b/backend/src/test/plant_layer_heatmap.rs @@ -193,10 +193,10 @@ async fn test_heatmap_with_missing_corner_succeeds() { let top_right_pixel = image.get_pixel(8, 2); let bottom_left_pixel = image.get_pixel(2, 8); let bottom_right_pixel = image.get_pixel(8, 8); - assert_eq!([63, 191, 0, 127], top_left_pixel.0); - assert_eq!([63, 191, 0, 127], top_right_pixel.0); + assert_eq!([111, 143, 0, 62], top_left_pixel.0); + assert_eq!([111, 143, 0, 62], top_right_pixel.0); assert_eq!([255, 0, 0, 0], bottom_left_pixel.0); - assert_eq!([63, 191, 0, 127], bottom_right_pixel.0); + assert_eq!([111, 143, 0, 62], bottom_right_pixel.0); } #[actix_rt::test] @@ -244,9 +244,9 @@ async fn test_heatmap_with_shadings_succeeds() { let top_left_pixel = image.get_pixel(1, 1); let bottom_right_pixel = image.get_pixel(40, 80); // The shading is the exact opposite of the plants preference, therefore the map will be red. - assert_eq!([191, 63, 0, 127], top_left_pixel.0); + assert_eq!([143, 111, 0, 62], top_left_pixel.0); // Plant like other positions, therefore green. - assert_eq!([63, 191, 0, 127], bottom_right_pixel.0); + assert_eq!([111, 143, 0, 62], bottom_right_pixel.0); } #[actix_rt::test] @@ -317,7 +317,7 @@ async fn test_heatmap_with_shadings_and_light_requirement_succeeds() { let top_left_pixel = image.get_pixel(1, 1); let bottom_right_pixel = image.get_pixel(40, 80); // The shading is deep shade with is ok for the plant. - assert_eq!([63, 191, 0, 127], top_left_pixel.0); + assert_eq!([111, 143, 0, 62], top_left_pixel.0); // The plant can't grow in sun. assert_eq!([255, 0, 0, 255], bottom_right_pixel.0); @@ -347,7 +347,7 @@ async fn test_heatmap_with_shadings_and_light_requirement_succeeds() { // The plant can't grow in deep shade. assert_eq!([255, 0, 0, 255], top_left_pixel.0); // The plant can grow in sun. - assert_eq!([95, 159, 0, 127], bottom_right_pixel.0); + assert_eq!([119, 135, 0, 62], bottom_right_pixel.0); } #[actix_rt::test] @@ -415,9 +415,9 @@ async fn test_heatmap_with_plantings_succeeds() { let a_bit_away_from_planting = image.get_pixel(10, 10); let far_away_from_planting = image.get_pixel(40, 80); // The planting influences the map. - assert_eq!([0, 255, 0, 127], on_planting.0); - assert_eq!([9, 245, 0, 118], close_to_planting.0); - assert_eq!([110, 144, 0, 17], a_bit_away_from_planting.0); + assert_eq!([96, 158, 0, 62], on_planting.0); + assert_eq!([98, 156, 0, 57], close_to_planting.0); + assert_eq!([123, 131, 0, 8], a_bit_away_from_planting.0); // There is no influence on locations far away. assert_eq!([127, 127, 0, 0], far_away_from_planting.0); } From 22a68556ed538d183715b4b78adce307e5c96949 Mon Sep 17 00:00:00 2001 From: kitzbergerg Date: Wed, 26 Jul 2023 01:15:54 +0200 Subject: [PATCH 026/157] heatmap: performance test scripts initial commit --- backend/Cargo.toml | 3 + benchmarks/backend/README.md | 3 + benchmarks/backend/config/get_statistics.sh | 7 ++ benchmarks/backend/config/run_httperf.sh | 13 +++ benchmarks/backend/config/setup.sh | 16 +++ benchmarks/backend/heatmap/get_statistics.sh | 14 +++ .../backend/heatmap/insert_data-large_map.sh | 102 ++++++++++++++++++ .../backend/heatmap/insert_data-middle_map.sh | 102 ++++++++++++++++++ .../backend/heatmap/insert_data-small_map.sh | 102 ++++++++++++++++++ benchmarks/backend/heatmap/run_httperf.sh | 15 +++ benchmarks/backend/heatmap/setup.sh | 27 +++++ doc/backend/06performance_benchmarks.md | 26 +++++ 12 files changed, 430 insertions(+) create mode 100644 benchmarks/backend/README.md create mode 100755 benchmarks/backend/config/get_statistics.sh create mode 100755 benchmarks/backend/config/run_httperf.sh create mode 100755 benchmarks/backend/config/setup.sh create mode 100755 benchmarks/backend/heatmap/get_statistics.sh create mode 100755 benchmarks/backend/heatmap/insert_data-large_map.sh create mode 100755 benchmarks/backend/heatmap/insert_data-middle_map.sh create mode 100755 benchmarks/backend/heatmap/insert_data-small_map.sh create mode 100755 benchmarks/backend/heatmap/run_httperf.sh create mode 100755 benchmarks/backend/heatmap/setup.sh create mode 100644 doc/backend/06performance_benchmarks.md diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 3b448a2ef..ad67c6632 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -59,3 +59,6 @@ image = { version = "0.24.6", default-features = false, features = ["png"] } [dev-dependencies] jsonwebkey = { version = "0.3.5", features = ["generate", "jwt-convert"] } + +[profile.release] +debug = true diff --git a/benchmarks/backend/README.md b/benchmarks/backend/README.md new file mode 100644 index 000000000..dc9921a2f --- /dev/null +++ b/benchmarks/backend/README.md @@ -0,0 +1,3 @@ +# Backend Benchmarks + +Documentation about the backend benchmarks can be found [here](../../doc/backend/06performance_benchmarks.md). diff --git a/benchmarks/backend/config/get_statistics.sh b/benchmarks/backend/config/get_statistics.sh new file mode 100755 index 000000000..c38a3b159 --- /dev/null +++ b/benchmarks/backend/config/get_statistics.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env nu + +cd backend + +print "Requests:" +grep 'Total: connections ' httperf.log | awk '{print "total:", $3}' +grep 'Connection time \[ms\]: min' httperf.log | awk '{print "min:", $5, "ms\navg:", $7, "ms\nmax:", $9, "ms"}' diff --git a/benchmarks/backend/config/run_httperf.sh b/benchmarks/backend/config/run_httperf.sh new file mode 100755 index 000000000..46d8f0291 --- /dev/null +++ b/benchmarks/backend/config/run_httperf.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +# Get the OAuth2 access token +access_token=$(curl --request POST \ + --url 'https://auth.permaplant.net/realms/PermaplanT/protocol/openid-connect/token' \ + --header 'content-type: application/x-www-form-urlencoded' \ + --data grant_type=password \ + --data 'username=$1' \ + --data 'password=$2' \ + --data 'client_id=localhost' | jq -r .access_token) + +# Run httperf +httperf --server localhost --port 8080 --uri '/api/config' --num-conns 10000 --rate 100 --add-header="Authorization:Bearer ${access_token}\n" > backend/httperf.log 2>&1 diff --git a/benchmarks/backend/config/setup.sh b/benchmarks/backend/config/setup.sh new file mode 100755 index 000000000..ac14e55b1 --- /dev/null +++ b/benchmarks/backend/config/setup.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +# Start the database and run migrations +cd backend +docker run -d --name postgis -e POSTGRES_PASSWORD=permaplant -e POSTGRES_USER=permaplant -p 5432:5432 postgis/postgis:13-3.1 -c log_min_duration_statement=0 -c log_statement=all +sleep 10 +LC_ALL=C diesel setup +LC_ALL=C diesel migration run + +# Start the backend +cd ../backend +RUST_LOG="backend=warn,actix_web=warn" PERF=/usr/lib/linux-tools/5.4.0-153-generic/perf cargo flamegraph --open + +# Remove database +docker kill postgis +docker rm postgis diff --git a/benchmarks/backend/heatmap/get_statistics.sh b/benchmarks/backend/heatmap/get_statistics.sh new file mode 100755 index 000000000..e87206b3d --- /dev/null +++ b/benchmarks/backend/heatmap/get_statistics.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env nu + +cd backend + +grep ' UTC \[[0-9]*\] ' postgis.log | lines | split column -r '\[|\]' | group-by column2 | values | each {|pid| $pid | each {|el| $"($el.column1)[($el.column2)]($el.column3)" } } | flatten | save -f postgis_parsed.log + +print "SQL queries:" +grep 'LOG: execute s.*: SELECT \* FROM calculate_heatmap' postgis_parsed.log -A 2 | grep 'LOG: duration: .* ms$' | awk '{sum+=$7; count++; min=10000; if ($7<0+min) min=$7; if ($7>0+max) max=$7} END {print "total:", count, "\nmin:", min, "ms\navg:", sum/count, "ms\nmax:", max, "ms"}' + +print "" + +print "Requests:" +grep 'Total: connections ' httperf.log | awk '{print "total:", $3}' +grep 'Connection time \[ms\]: min' httperf.log | awk '{print "min:", $5, "ms\navg:", $7, "ms\nmax:", $9, "ms"}' diff --git a/benchmarks/backend/heatmap/insert_data-large_map.sh b/benchmarks/backend/heatmap/insert_data-large_map.sh new file mode 100755 index 000000000..7572df775 --- /dev/null +++ b/benchmarks/backend/heatmap/insert_data-large_map.sh @@ -0,0 +1,102 @@ +#!/usr/bin/env bash + +# Get the OAuth2 access token +username=$1 +password=$2 +access_token=$(curl --request POST \ + --url 'https://auth.permaplant.net/realms/PermaplanT/protocol/openid-connect/token' \ + --header 'content-type: application/x-www-form-urlencoded' \ + --data grant_type=password \ + --data "username=${username}" \ + --data "password=${password}" \ + --data 'client_id=localhost' | jq -r .access_token) + +# Create map +curl --location 'http://localhost:8080/api/maps' \ +--header "Authorization: Bearer ${access_token}" \ +--header 'Content-Type: application/json' \ +--data '{ + "name": "Test Map", + "creation_date": "2023-07-25", + "is_inactive": false, + "zoom_factor": 100, + "honors": 0, + "visits": 0, + "harvested": 0, + "privacy": "public", + "description": "", + "geometry": { + "rings": [ + [ + { + "x": 0.0, + "y": 0.0 + }, + { + "x": 10000.0, + "y": 0.0 + }, + { + "x": 10000.0, + "y": 10000.0 + }, + { + "x": 0.0, + "y": 10000.0 + }, + { + "x": 0.0, + "y": 0.0 + } + ] + ], + "srid": 4326 + } +}' + +# Create plantings +DATABASE_NAME="permaplant" +DATABASE_USER="permaplant" +PGPASSWORD=permaplant psql -h localhost -p 5432 -U $DATABASE_USER -d $DATABASE_NAME -c " +INSERT INTO plantings (id, layer_id, plant_id, x, y, width, height, rotation, scale_x, scale_y, add_date, remove_date) +VALUES + ('00000000-0000-0000-0000-000000000000', 2, 1, 15, 15, 0, 0, 0, 0, 0, DEFAULT, DEFAULT), + ('00000000-0000-0000-0000-000000000001', 2, 2, 20, 30, 0, 0, 0, 0, 0, DEFAULT, DEFAULT); +" + +# Create shadings +curl --location 'http://localhost:8080/api/maps/1/layers/shade/shadings' \ +--header "Authorization: Bearer ${access_token}" \ +--header 'Content-Type: application/json' \ +--data '{ + "layerId": 3, + "shade": "partial shade", + "geometry": { + "rings": [ + [ + { + "x": 0.0, + "y": 0.0 + }, + { + "x": 200.0, + "y": 0.0 + }, + { + "x": 200.0, + "y": 200.0 + }, + { + "x": 0.0, + "y": 200.0 + }, + { + "x": 0.0, + "y": 0.0 + } + ] + ], + "srid": 4326 + }, + "actionId": "00000000-0000-0000-0000-000000000000" +}' diff --git a/benchmarks/backend/heatmap/insert_data-middle_map.sh b/benchmarks/backend/heatmap/insert_data-middle_map.sh new file mode 100755 index 000000000..54613d2c8 --- /dev/null +++ b/benchmarks/backend/heatmap/insert_data-middle_map.sh @@ -0,0 +1,102 @@ +#!/usr/bin/env bash + +# Get the OAuth2 access token +username=$1 +password=$2 +access_token=$(curl --request POST \ + --url 'https://auth.permaplant.net/realms/PermaplanT/protocol/openid-connect/token' \ + --header 'content-type: application/x-www-form-urlencoded' \ + --data grant_type=password \ + --data "username=${username}" \ + --data "password=${password}" \ + --data 'client_id=localhost' | jq -r .access_token) + +# Create map +curl --location 'http://localhost:8080/api/maps' \ +--header "Authorization: Bearer ${access_token}" \ +--header 'Content-Type: application/json' \ +--data '{ + "name": "Test Map", + "creation_date": "2023-07-25", + "is_inactive": false, + "zoom_factor": 100, + "honors": 0, + "visits": 0, + "harvested": 0, + "privacy": "public", + "description": "", + "geometry": { + "rings": [ + [ + { + "x": 0.0, + "y": 0.0 + }, + { + "x": 1000.0, + "y": 0.0 + }, + { + "x": 1000.0, + "y": 1000.0 + }, + { + "x": 0.0, + "y": 1000.0 + }, + { + "x": 0.0, + "y": 0.0 + } + ] + ], + "srid": 4326 + } +}' + +# Create plantings +DATABASE_NAME="permaplant" +DATABASE_USER="permaplant" +PGPASSWORD=permaplant psql -h localhost -p 5432 -U $DATABASE_USER -d $DATABASE_NAME -c " +INSERT INTO plantings (id, layer_id, plant_id, x, y, width, height, rotation, scale_x, scale_y, add_date, remove_date) +VALUES + ('00000000-0000-0000-0000-000000000000', 2, 1, 15, 15, 0, 0, 0, 0, 0, DEFAULT, DEFAULT), + ('00000000-0000-0000-0000-000000000001', 2, 2, 20, 30, 0, 0, 0, 0, 0, DEFAULT, DEFAULT); +" + +# Create shadings +curl --location 'http://localhost:8080/api/maps/1/layers/shade/shadings' \ +--header "Authorization: Bearer ${access_token}" \ +--header 'Content-Type: application/json' \ +--data '{ + "layerId": 3, + "shade": "partial shade", + "geometry": { + "rings": [ + [ + { + "x": 0.0, + "y": 0.0 + }, + { + "x": 200.0, + "y": 0.0 + }, + { + "x": 200.0, + "y": 200.0 + }, + { + "x": 0.0, + "y": 200.0 + }, + { + "x": 0.0, + "y": 0.0 + } + ] + ], + "srid": 4326 + }, + "actionId": "00000000-0000-0000-0000-000000000000" +}' diff --git a/benchmarks/backend/heatmap/insert_data-small_map.sh b/benchmarks/backend/heatmap/insert_data-small_map.sh new file mode 100755 index 000000000..96e5b283f --- /dev/null +++ b/benchmarks/backend/heatmap/insert_data-small_map.sh @@ -0,0 +1,102 @@ +#!/usr/bin/env bash + +# Get the OAuth2 access token +username=$1 +password=$2 +access_token=$(curl --request POST \ + --url 'https://auth.permaplant.net/realms/PermaplanT/protocol/openid-connect/token' \ + --header 'content-type: application/x-www-form-urlencoded' \ + --data grant_type=password \ + --data "username=${username}" \ + --data "password=${password}" \ + --data 'client_id=localhost' | jq -r .access_token) + +# Create map +curl --location 'http://localhost:8080/api/maps' \ +--header "Authorization: Bearer ${access_token}" \ +--header 'Content-Type: application/json' \ +--data '{ + "name": "Test Map", + "creation_date": "2023-07-25", + "is_inactive": false, + "zoom_factor": 100, + "honors": 0, + "visits": 0, + "harvested": 0, + "privacy": "public", + "description": "", + "geometry": { + "rings": [ + [ + { + "x": 0.0, + "y": 0.0 + }, + { + "x": 100.0, + "y": 0.0 + }, + { + "x": 100.0, + "y": 100.0 + }, + { + "x": 0.0, + "y": 100.0 + }, + { + "x": 0.0, + "y": 0.0 + } + ] + ], + "srid": 4326 + } +}' + +# Create plantings +DATABASE_NAME="permaplant" +DATABASE_USER="permaplant" +PGPASSWORD=permaplant psql -h localhost -p 5432 -U $DATABASE_USER -d $DATABASE_NAME -c " +INSERT INTO plantings (id, layer_id, plant_id, x, y, width, height, rotation, scale_x, scale_y, add_date, remove_date) +VALUES + ('00000000-0000-0000-0000-000000000000', 2, 1, 15, 15, 0, 0, 0, 0, 0, DEFAULT, DEFAULT), + ('00000000-0000-0000-0000-000000000001', 2, 2, 20, 30, 0, 0, 0, 0, 0, DEFAULT, DEFAULT); +" + +# Create shadings +curl --location 'http://localhost:8080/api/maps/1/layers/shade/shadings' \ +--header "Authorization: Bearer ${access_token}" \ +--header 'Content-Type: application/json' \ +--data '{ + "layerId": 3, + "shade": "partial shade", + "geometry": { + "rings": [ + [ + { + "x": 0.0, + "y": 0.0 + }, + { + "x": 200.0, + "y": 0.0 + }, + { + "x": 200.0, + "y": 200.0 + }, + { + "x": 0.0, + "y": 200.0 + }, + { + "x": 0.0, + "y": 0.0 + } + ] + ], + "srid": 4326 + }, + "actionId": "00000000-0000-0000-0000-000000000000" +}' diff --git a/benchmarks/backend/heatmap/run_httperf.sh b/benchmarks/backend/heatmap/run_httperf.sh new file mode 100755 index 000000000..3eed06f9a --- /dev/null +++ b/benchmarks/backend/heatmap/run_httperf.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +# Get the OAuth2 access token +username=$1 +password=$2 +access_token=$(curl --request POST \ + --url 'https://auth.permaplant.net/realms/PermaplanT/protocol/openid-connect/token' \ + --header 'content-type: application/x-www-form-urlencoded' \ + --data grant_type=password \ + --data "username=${username}" \ + --data "password=${password}" \ + --data 'client_id=localhost' | jq -r .access_token) + +# Run httperf +httperf --server localhost --port 8080 --uri '/api/maps/1/layers/plants/heatmap?plant_id=1&plant_layer_id=2&shade_layer_id=3' --num-conns $3 --rate $4 --add-header="Authorization:Bearer ${access_token}\n" > backend/httperf.log 2>&1 diff --git a/benchmarks/backend/heatmap/setup.sh b/benchmarks/backend/heatmap/setup.sh new file mode 100755 index 000000000..9035961fc --- /dev/null +++ b/benchmarks/backend/heatmap/setup.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash + +# Start the database and run migrations +cd backend +docker run -d --name postgis -e POSTGRES_PASSWORD=permaplant -e POSTGRES_USER=permaplant -p 5432:5432 postgis/postgis:13-3.1 -c log_min_duration_statement=0 -c log_statement=all +sleep 10 +LC_ALL=C diesel setup +LC_ALL=C diesel migration run + +# Insert data +cd ../scraper && npm run insert +printf "\nNow run the backend in a second shell.\n" +read -n 1 -p "Press any key to continue!" +../benchmarks/backend/heatmap/insert_data-large_map.sh $1 $2 +printf "\n\nStop the backend.\n" +read -n 1 -p "Press any key to continue!" + +# Start the backend +cd ../backend +RUST_LOG="backend=warn,actix_web=warn" PERF=/usr/lib/linux-tools/5.4.0-153-generic/perf cargo flamegraph --open + +# Collect db logs +docker logs postgis > postgis.log 2>&1 + +# Remove database +docker kill postgis +docker rm postgis diff --git a/doc/backend/06performance_benchmarks.md b/doc/backend/06performance_benchmarks.md new file mode 100644 index 000000000..5c38772fd --- /dev/null +++ b/doc/backend/06performance_benchmarks.md @@ -0,0 +1,26 @@ +# Performance Benchmarks + +TODO: better documentation + +## Requirements + +docker +nushell +perf +flamegraph +httperf +standard linux tools like 'grep', 'awk' or 'psql' + +## Setup + +Add the following to the end of `Cargo.toml` + +```toml +[profile.release] +debug = true +``` + +## Scripts + +You can find the scripts in `benchmark/backend/`. +They are supposed to be run from the repos root folder. From 6d8d969ac0d5375caa2519bbffbc4e1075771120 Mon Sep 17 00:00:00 2001 From: kitzbergerg Date: Mon, 31 Jul 2023 11:40:13 +0200 Subject: [PATCH 027/157] update backend performance benchmark doc --- doc/backend/06performance_benchmarks.md | 66 +++++++++++++++++++++---- 1 file changed, 56 insertions(+), 10 deletions(-) diff --git a/doc/backend/06performance_benchmarks.md b/doc/backend/06performance_benchmarks.md index 5c38772fd..ea8b0a3ae 100644 --- a/doc/backend/06performance_benchmarks.md +++ b/doc/backend/06performance_benchmarks.md @@ -1,19 +1,20 @@ # Performance Benchmarks -TODO: better documentation - ## Requirements -docker -nushell -perf -flamegraph -httperf -standard linux tools like 'grep', 'awk' or 'psql' +The following tools are required to run the benchmarks: + +- docker +- [nushell](https://github.com/nushell/nushell) +- perf +- [flamegraph](https://github.com/flamegraph-rs/flamegraph) +- [httperf](https://github.com/httperf/httperf) +- standard linux tools like `grep`, `awk` or `psql` ## Setup -Add the following to the end of `Cargo.toml` +Add the following to the end of `Cargo.toml`. +This is necessary to ensure that perf can accurately track the function stack. ```toml [profile.release] @@ -23,4 +24,49 @@ debug = true ## Scripts You can find the scripts in `benchmark/backend/`. -They are supposed to be run from the repos root folder. +They are supposed to be run from the repositories root folder. + +The subfolders contain scripts to run performance benchmarks in specific endpoints. + +### `setup.sh` + +Execute this script like the following: +`./benchmarks/backend//setup.sh `. + +It will start the database and backend. +Depending on the endpoint it might execute `insert_data.sh` scripts to insert additional data into the database. + +The script might output instructions while executing. +Follow these instructions to ensure the benchmark works correctly. + +Parameters: + +- username: Your PermaplanT username. +- password: Your PermaplanT password. + +### `run_httperf.sh` + +Execute this script like the following: +`./benchmarks/backend//run_httperf.sh `. + +This script shall be run as soon as the `setup.sh` starts the backend via `cargo flamegraph`. +It will execute requests on the backend using httperf. + +Once this script finishes you can interrupt `setup.sh` via Ctrl+C. +Note that it might take 20min or longer to finish once interrupted. +Do not press Ctrl+C again, otherwise the generated flamegraph will not include all data. + +Parameters: + +- username: Your PermaplanT username. +- password: Your PermaplanT password. +- number_of_requests: The total number of requests httperf will execute. +- request_rate: How many requests will be executed per second. + +### `get_statistics.sh` + +Execute this script like the following: +`./benchmarks/backend//get_statistics.sh`. + +This script shall be executed once `setup.sh` finished. +It will parse the PostgreSQL logs and httperf logs to extract execution times. From 82dd3dd16a83fa7a8ad8198e0d979842dcc6b572 Mon Sep 17 00:00:00 2001 From: kitzbergerg Date: Mon, 31 Jul 2023 12:06:58 +0200 Subject: [PATCH 028/157] update backend performance benchmark scripts and doc --- benchmarks/backend/config/run_httperf.sh | 7 ++-- .../backend/heatmap/insert_data-large_map.sh | 3 +- .../backend/heatmap/insert_data-middle_map.sh | 3 +- .../backend/heatmap/insert_data-small_map.sh | 3 +- benchmarks/backend/heatmap/run_httperf.sh | 7 ++-- doc/backend/06performance_benchmarks.md | 35 ++++++++++++++++--- 6 files changed, 46 insertions(+), 12 deletions(-) diff --git a/benchmarks/backend/config/run_httperf.sh b/benchmarks/backend/config/run_httperf.sh index 46d8f0291..816e718e2 100755 --- a/benchmarks/backend/config/run_httperf.sh +++ b/benchmarks/backend/config/run_httperf.sh @@ -1,12 +1,15 @@ #!/usr/bin/env bash +username=$1 +password=$2 + # Get the OAuth2 access token access_token=$(curl --request POST \ --url 'https://auth.permaplant.net/realms/PermaplanT/protocol/openid-connect/token' \ --header 'content-type: application/x-www-form-urlencoded' \ --data grant_type=password \ - --data 'username=$1' \ - --data 'password=$2' \ + --data "username=${username}" \ + --data "password=${password}" \ --data 'client_id=localhost' | jq -r .access_token) # Run httperf diff --git a/benchmarks/backend/heatmap/insert_data-large_map.sh b/benchmarks/backend/heatmap/insert_data-large_map.sh index 7572df775..993e7c8d7 100755 --- a/benchmarks/backend/heatmap/insert_data-large_map.sh +++ b/benchmarks/backend/heatmap/insert_data-large_map.sh @@ -1,8 +1,9 @@ #!/usr/bin/env bash -# Get the OAuth2 access token username=$1 password=$2 + +# Get the OAuth2 access token access_token=$(curl --request POST \ --url 'https://auth.permaplant.net/realms/PermaplanT/protocol/openid-connect/token' \ --header 'content-type: application/x-www-form-urlencoded' \ diff --git a/benchmarks/backend/heatmap/insert_data-middle_map.sh b/benchmarks/backend/heatmap/insert_data-middle_map.sh index 54613d2c8..b3ce6f5e1 100755 --- a/benchmarks/backend/heatmap/insert_data-middle_map.sh +++ b/benchmarks/backend/heatmap/insert_data-middle_map.sh @@ -1,8 +1,9 @@ #!/usr/bin/env bash -# Get the OAuth2 access token username=$1 password=$2 + +# Get the OAuth2 access token access_token=$(curl --request POST \ --url 'https://auth.permaplant.net/realms/PermaplanT/protocol/openid-connect/token' \ --header 'content-type: application/x-www-form-urlencoded' \ diff --git a/benchmarks/backend/heatmap/insert_data-small_map.sh b/benchmarks/backend/heatmap/insert_data-small_map.sh index 96e5b283f..436dd0581 100755 --- a/benchmarks/backend/heatmap/insert_data-small_map.sh +++ b/benchmarks/backend/heatmap/insert_data-small_map.sh @@ -1,8 +1,9 @@ #!/usr/bin/env bash -# Get the OAuth2 access token username=$1 password=$2 + +# Get the OAuth2 access token access_token=$(curl --request POST \ --url 'https://auth.permaplant.net/realms/PermaplanT/protocol/openid-connect/token' \ --header 'content-type: application/x-www-form-urlencoded' \ diff --git a/benchmarks/backend/heatmap/run_httperf.sh b/benchmarks/backend/heatmap/run_httperf.sh index 3eed06f9a..483650188 100755 --- a/benchmarks/backend/heatmap/run_httperf.sh +++ b/benchmarks/backend/heatmap/run_httperf.sh @@ -1,8 +1,11 @@ #!/usr/bin/env bash -# Get the OAuth2 access token username=$1 password=$2 +number_of_requests=$3 +request_rate=$4 + +# Get the OAuth2 access token access_token=$(curl --request POST \ --url 'https://auth.permaplant.net/realms/PermaplanT/protocol/openid-connect/token' \ --header 'content-type: application/x-www-form-urlencoded' \ @@ -12,4 +15,4 @@ access_token=$(curl --request POST \ --data 'client_id=localhost' | jq -r .access_token) # Run httperf -httperf --server localhost --port 8080 --uri '/api/maps/1/layers/plants/heatmap?plant_id=1&plant_layer_id=2&shade_layer_id=3' --num-conns $3 --rate $4 --add-header="Authorization:Bearer ${access_token}\n" > backend/httperf.log 2>&1 +httperf --server localhost --port 8080 --uri '/api/maps/1/layers/plants/heatmap?plant_id=1&plant_layer_id=2&shade_layer_id=3' --num-conns $number_of_requests --rate $request_rate --add-header="Authorization:Bearer ${access_token}\n" > backend/httperf.log 2>&1 diff --git a/doc/backend/06performance_benchmarks.md b/doc/backend/06performance_benchmarks.md index ea8b0a3ae..5d8f28492 100644 --- a/doc/backend/06performance_benchmarks.md +++ b/doc/backend/06performance_benchmarks.md @@ -30,7 +30,7 @@ The subfolders contain scripts to run performance benchmarks in specific endpoin ### `setup.sh` -Execute this script like the following: +Execute like the following: `./benchmarks/backend//setup.sh `. It will start the database and backend. @@ -46,10 +46,10 @@ Parameters: ### `run_httperf.sh` -Execute this script like the following: +Execute like the following: `./benchmarks/backend//run_httperf.sh `. -This script shall be run as soon as the `setup.sh` starts the backend via `cargo flamegraph`. +This script shall be run as soon as the `setup.sh` starts the backend via `cargo flamegraph`. It will execute requests on the backend using httperf. Once this script finishes you can interrupt `setup.sh` via Ctrl+C. @@ -65,8 +65,33 @@ Parameters: ### `get_statistics.sh` -Execute this script like the following: +Execute like the following: `./benchmarks/backend//get_statistics.sh`. -This script shall be executed once `setup.sh` finished. +This script shall be executed once all previous scripts finished. It will parse the PostgreSQL logs and httperf logs to extract execution times. + +## Example run + +The following is a step by step guide on how to execute the benchmark for the heatmap: + +1. Insert + ```toml + [profile.release] + debug = true + ``` + into `Cargo.toml`. +2. Execute: `./benchmarks/backend/heatmap/setup.sh `. Do the following once the script gives the instructions. + - Start the backend in dev mode. + - Press Enter. + - Stop the backend. + - Press Enter. +3. Wait for the backend to start in release mode. +4. Once its started execute in a second shell: `./benchmarks/backend/heatmap/run_httperf.sh 10000 100` +5. Wait for httperf to finish. +6. Press Ctrl+C in the `setup.sh` shell. +7. Wait until `flamegraph.svg` was generated (this might take 20min or longer). Do not interrupt this step. +8. Execute: `./benchmarks/backend/heatmap/get_statistics.sh` +9. Collect results: + - flamgraph.svg + - Copy request execution times and query execution times from stdout of `get_statistics.sh` From 2578a840c34eadefd791eb846bad3f91a6cba359 Mon Sep 17 00:00:00 2001 From: kitzbergerg Date: Mon, 31 Jul 2023 14:56:50 +0200 Subject: [PATCH 029/157] use date in heatmap API --- backend/migrations/2023-07-03-165000_heatmap/up.sql | 4 ++++ backend/src/model/dto.rs | 3 +++ backend/src/model/entity/plant_layer.rs | 7 +++++-- backend/src/service/plant_layer.rs | 2 ++ 4 files changed, 14 insertions(+), 2 deletions(-) diff --git a/backend/migrations/2023-07-03-165000_heatmap/up.sql b/backend/migrations/2023-07-03-165000_heatmap/up.sql index ece1e6079..03755be43 100644 --- a/backend/migrations/2023-07-03-165000_heatmap/up.sql +++ b/backend/migrations/2023-07-03-165000_heatmap/up.sql @@ -40,15 +40,19 @@ CREATE TYPE score AS ( -- To get valid coordinates the user would therefore need to move and scale the -- calculated heatmap by taking into account the boundaries of the map. -- +-- View the API documentation via Swagger for additional information. +-- -- p_map_id ... map id -- p_layer_ids ... ids of the layers -- p_plant_id ... id of the plant for which to consider relations +-- p_date ... date at which to generate the heatmap -- granularity ... resolution of the map (must be greater than 0) -- x_min,y_min,x_max,y_max ... boundaries of the map CREATE OR REPLACE FUNCTION calculate_heatmap( p_map_id INTEGER, p_layer_ids INTEGER [], p_plant_id INTEGER, + p_date DATE, granularity INTEGER, x_min INTEGER, y_min INTEGER, diff --git a/backend/src/model/dto.rs b/backend/src/model/dto.rs index f9dc01066..da39c5ef2 100644 --- a/backend/src/model/dto.rs +++ b/backend/src/model/dto.rs @@ -456,6 +456,9 @@ pub struct HeatMapQueryParams { pub shade_layer_id: i32, /// The id of the plant you want to plant. pub plant_id: i32, + /// The date at which to generate the heatmap. + /// Will be set to the current date if `None`. + pub date: Option, } #[typeshare] diff --git a/backend/src/model/entity/plant_layer.rs b/backend/src/model/entity/plant_layer.rs index fc56d915d..ef5e3e90e 100644 --- a/backend/src/model/entity/plant_layer.rs +++ b/backend/src/model/entity/plant_layer.rs @@ -1,9 +1,10 @@ //! Contains the database implementation of the plant layer. +use chrono::NaiveDate; use diesel::{ debug_query, pg::Pg, - sql_types::{Array, Float, Integer}, + sql_types::{Array, Date, Float, Integer}, CombineDsl, ExpressionMethods, QueryDsl, QueryResult, QueryableByName, }; use diesel_async::{AsyncPgConnection, RunQueryDsl}; @@ -70,6 +71,7 @@ pub async fn heatmap( plant_layer_id: i32, shade_layer_id: i32, plant_id: i32, + date: NaiveDate, conn: &mut AsyncPgConnection, ) -> QueryResult>> { // Fetch the bounding box x and y values of the maps coordinates @@ -80,10 +82,11 @@ pub async fn heatmap( // Fetch the heatmap let query = - diesel::sql_query("SELECT * FROM calculate_heatmap($1, $2, $3, $4, $5, $6, $7, $8)") + diesel::sql_query("SELECT * FROM calculate_heatmap($1, $2, $3, $4, $5, $6, $7, $8, $9)") .bind::(map_id) .bind::, _>(vec![plant_layer_id, shade_layer_id]) .bind::(plant_id) + .bind::(date) .bind::(GRANULARITY) .bind::(bounding_box.x_min) .bind::(bounding_box.y_min) diff --git a/backend/src/service/plant_layer.rs b/backend/src/service/plant_layer.rs index b5c42771b..e842f8fcb 100644 --- a/backend/src/service/plant_layer.rs +++ b/backend/src/service/plant_layer.rs @@ -4,6 +4,7 @@ use std::io::Cursor; use actix_http::StatusCode; use actix_web::web::Data; +use chrono::Utc; use image::{ImageBuffer, Rgba}; use crate::{ @@ -35,6 +36,7 @@ pub async fn heatmap( query_params.plant_layer_id, query_params.shade_layer_id, query_params.plant_id, + query_params.date.unwrap_or_else(|| Utc::now().date_naive()), &mut conn, ) .await?; From 2cd8cdd18cdedd6201c6c27cacc75ba50c430a5b Mon Sep 17 00:00:00 2001 From: kitzbergerg Date: Mon, 31 Jul 2023 15:12:05 +0200 Subject: [PATCH 030/157] write SQL logic for date, write tests for heatmap date query --- .../2023-07-03-165000_heatmap/up.sql | 26 +++-- .../2023-07-22-110000_shadings/down.sql | 5 +- .../2023-07-22-110000_shadings/up.sql | 17 +++- backend/src/test/plant_layer_heatmap.rs | 99 +++++++++++++++++++ 4 files changed, 133 insertions(+), 14 deletions(-) diff --git a/backend/migrations/2023-07-03-165000_heatmap/up.sql b/backend/migrations/2023-07-03-165000_heatmap/up.sql index 03755be43..6ed27e414 100644 --- a/backend/migrations/2023-07-03-165000_heatmap/up.sql +++ b/backend/migrations/2023-07-03-165000_heatmap/up.sql @@ -45,14 +45,14 @@ CREATE TYPE score AS ( -- p_map_id ... map id -- p_layer_ids ... ids of the layers -- p_plant_id ... id of the plant for which to consider relations --- p_date ... date at which to generate the heatmap +-- date ... date at which to generate the heatmap -- granularity ... resolution of the map (must be greater than 0) -- x_min,y_min,x_max,y_max ... boundaries of the map CREATE OR REPLACE FUNCTION calculate_heatmap( p_map_id INTEGER, p_layer_ids INTEGER [], p_plant_id INTEGER, - p_date DATE, + date DATE, granularity INTEGER, x_min INTEGER, y_min INTEGER, @@ -101,7 +101,7 @@ BEGIN -- If the point is on the map calculate a score; otherwise set score to 0. IF ST_Intersects(point, map_geometry) THEN - score := calculate_score(p_map_id, p_layer_ids, p_plant_id, x_pos, y_pos); + score := calculate_score(p_map_id, p_layer_ids, p_plant_id, date, x_pos, y_pos); score := scale_score(score); -- scale to be between 0 and 1 preference := score.preference; relevance := score.relevance; @@ -139,11 +139,13 @@ $$ LANGUAGE plpgsql; -- p_map_id ... map id -- p_layer_ids[1] ... plant layer -- p_plant_id ... id of the plant for which to consider relations +-- date ... date at which to generate the heatmap -- x_pos,y_pos ... coordinates on the map where to calculate the score CREATE OR REPLACE FUNCTION calculate_score( p_map_id INTEGER, p_layer_ids INTEGER [], p_plant_id INTEGER, + date DATE, x_pos INTEGER, y_pos INTEGER ) @@ -152,7 +154,7 @@ DECLARE score SCORE; plants SCORE; BEGIN - plants := calculate_score_from_relations(p_layer_ids[1], p_plant_id, x_pos, y_pos); + plants := calculate_score_from_relations(p_layer_ids[1], p_plant_id, date, x_pos, y_pos); score.preference := plants.preference; score.relevance := plants.relevance; @@ -163,7 +165,11 @@ $$ LANGUAGE plpgsql; -- Calculate score using the plants relations and their distances. CREATE OR REPLACE FUNCTION calculate_score_from_relations( - p_layer_id INTEGER, p_plant_id INTEGER, x_pos INTEGER, y_pos INTEGER + p_layer_id INTEGER, + p_plant_id INTEGER, + date DATE, + x_pos INTEGER, + y_pos INTEGER ) RETURNS SCORE AS $$ DECLARE @@ -179,7 +185,7 @@ BEGIN score.preference := 0.0; score.relevance := 0.0; - FOR plant_relation IN (SELECT * FROM get_plant_relations(p_layer_id, p_plant_id)) LOOP + FOR plant_relation IN (SELECT * FROM get_plant_relations(p_layer_id, p_plant_id, date)) LOOP -- calculate euclidean distance distance := sqrt((plant_relation.x - x_pos)^2 + (plant_relation.y - y_pos)^2); @@ -205,7 +211,9 @@ $$ LANGUAGE plpgsql; -- Get all relations for the plant on the specified layer. CREATE OR REPLACE FUNCTION get_plant_relations( - p_layer_id INTEGER, p_plant_id INTEGER + p_layer_id INTEGER, + p_plant_id INTEGER, + date DATE ) RETURNS TABLE (x INTEGER, y INTEGER, relation RELATION_TYPE) AS $$ BEGIN @@ -226,6 +234,8 @@ BEGIN WHERE plant1 = p_plant_id AND r2.relation != 'neutral' ) relations ON plants.id = relations.plant - WHERE plantings.layer_id = p_layer_id; + WHERE plantings.layer_id = p_layer_id + AND (plantings.add_date IS NULL OR plantings.add_date <= date) + AND (plantings.remove_date IS NULL OR plantings.remove_date > date); END; $$ LANGUAGE plpgsql; diff --git a/backend/migrations/2023-07-22-110000_shadings/down.sql b/backend/migrations/2023-07-22-110000_shadings/down.sql index 3c6d10dbb..d92621c59 100644 --- a/backend/migrations/2023-07-22-110000_shadings/down.sql +++ b/backend/migrations/2023-07-22-110000_shadings/down.sql @@ -5,6 +5,7 @@ CREATE OR REPLACE FUNCTION calculate_score( p_map_id INTEGER, p_layer_ids INTEGER [], p_plant_id INTEGER, + date DATE, x_pos INTEGER, y_pos INTEGER ) @@ -13,9 +14,9 @@ DECLARE score SCORE; plants SCORE; BEGIN - plants := calculate_score_from_relations(p_layer_ids[1], p_plant_id, x_pos, y_pos); + plants := calculate_score_from_relations(p_layer_ids[1], p_plant_id, date, x_pos, y_pos); - score.preference := 0.5 + plants.preference; + score.preference := plants.preference; score.relevance := plants.relevance; RETURN score; diff --git a/backend/migrations/2023-07-22-110000_shadings/up.sql b/backend/migrations/2023-07-22-110000_shadings/up.sql index f779ead8a..06aadac8e 100644 --- a/backend/migrations/2023-07-22-110000_shadings/up.sql +++ b/backend/migrations/2023-07-22-110000_shadings/up.sql @@ -30,11 +30,13 @@ FOR EACH ROW EXECUTE FUNCTION check_shade_layer_type(); -- p_layer_ids[1] ... plant layer -- p_layer_ids[2] ... shade layer -- p_plant_id ... id of the plant for which to consider relations +-- date ... date at which to generate the heatmap -- x_pos,y_pos ... coordinates on the map where to calculate the score CREATE OR REPLACE FUNCTION calculate_score( p_map_id INTEGER, p_layer_ids INTEGER [], p_plant_id INTEGER, + date DATE, x_pos INTEGER, y_pos INTEGER ) @@ -44,8 +46,8 @@ DECLARE plants SCORE; shades SCORE; BEGIN - plants := calculate_score_from_relations(p_layer_ids[1], p_plant_id, x_pos, y_pos); - shades := calculate_score_from_shadings(p_layer_ids[2], p_plant_id, x_pos, y_pos); + plants := calculate_score_from_relations(p_layer_ids[1], p_plant_id, date, x_pos, y_pos); + shades := calculate_score_from_shadings(p_layer_ids[2], p_plant_id, date, x_pos, y_pos); score.preference := plants.preference + shades.preference; score.relevance := plants.relevance + shades.relevance; @@ -59,7 +61,11 @@ $$ LANGUAGE plpgsql; -- -- If the plant would die at the position set preference=-100 and relevance=100. CREATE FUNCTION calculate_score_from_shadings( - p_layer_id INTEGER, p_plant_id INTEGER, x_pos INTEGER, y_pos INTEGER + p_layer_id INTEGER, + p_plant_id INTEGER, + date DATE, + x_pos INTEGER, + y_pos INTEGER ) RETURNS SCORE AS $$ DECLARE @@ -87,7 +93,10 @@ BEGIN -- Select the shading with the darkest shade that intersects the point SELECT shade INTO shading_shade FROM shadings - WHERE layer_id = p_layer_id AND ST_Intersects(geometry, point) + WHERE layer_id = p_layer_id + AND (add_date IS NULL OR add_date <= date) + AND (remove_date IS NULL OR remove_date > date) + AND ST_Intersects(geometry, point) ORDER BY shade DESC LIMIT 1; diff --git a/backend/src/test/plant_layer_heatmap.rs b/backend/src/test/plant_layer_heatmap.rs index 3c4bdfcf9..8f9efaea5 100644 --- a/backend/src/test/plant_layer_heatmap.rs +++ b/backend/src/test/plant_layer_heatmap.rs @@ -422,6 +422,105 @@ async fn test_heatmap_with_plantings_succeeds() { assert_eq!([127, 127, 0, 0], far_away_from_planting.0); } +#[actix_rt::test] +async fn test_heatmap_with_deleted_planting_succeeds() { + let pool = init_test_database(|conn| { + async { + initial_db_values(conn, tall_rectangle()).await?; + diesel::insert_into(crate::schema::plants::table) + .values(( + &crate::schema::plants::id.eq(-2), + &crate::schema::plants::unique_name.eq("Testia"), + &crate::schema::plants::common_name_en.eq(Some(vec![Some("T".to_owned())])), + )) + .execute(conn) + .await?; + diesel::insert_into(crate::schema::relations::table) + .values(vec![( + &crate::schema::relations::plant1.eq(-1), + &crate::schema::relations::plant2.eq(-2), + &crate::schema::relations::relation.eq(RelationType::Companion), + )]) + .execute(conn) + .await?; + diesel::insert_into(crate::schema::plantings::table) + .values(vec![data::TestInsertablePlanting { + id: Uuid::new_v4(), + layer_id: -1, + plant_id: -1, + x: 15, + y: 15, + remove_date: Some(NaiveDate::from_ymd_opt(2023, 07, 30).unwrap()), + ..Default::default() + }]) + .execute(conn) + .await?; + Ok(()) + } + .scope_boxed() + }) + .await; + let (token, app) = init_test_app(pool.clone()).await; + + let resp = test::TestRequest::get() + .uri("/api/maps/-1/layers/plants/heatmap?plant_id=-2&plant_layer_id=-1&shade_layer_id=-2") + .insert_header((header::AUTHORIZATION, token.clone())) + .send_request(&app) + .await; + + assert_eq!(resp.status(), StatusCode::OK); + assert_eq!( + resp.headers().get(header::CONTENT_TYPE), + Some(&header::HeaderValue::from_static("image/png")) + ); + let result = test::read_body(resp).await; + let result = &result.bytes().collect::, _>>().unwrap(); + let image = load_from_memory_with_format(result.as_slice(), image::ImageFormat::Png).unwrap(); + let image = image.as_rgba8().unwrap(); + assert_eq!( + ((500 / GRANULARITY) as u32, (1000 / GRANULARITY) as u32), + image.dimensions() + ); + + // (0,0) is be top left. + let on_planting = image.get_pixel(1, 1); + let close_to_planting = image.get_pixel(2, 2); + let a_bit_away_from_planting = image.get_pixel(10, 10); + // The planting doesn't influences the map as it is deleted. + assert_eq!([127, 127, 0, 0], on_planting.0); + assert_eq!([127, 127, 0, 0], close_to_planting.0); + assert_eq!([127, 127, 0, 0], a_bit_away_from_planting.0); + + let resp = test::TestRequest::get() + .uri("/api/maps/-1/layers/plants/heatmap?plant_id=-2&plant_layer_id=-1&shade_layer_id=-2&date=2023-07-21") + .insert_header((header::AUTHORIZATION, token)) + .send_request(&app) + .await; + + assert_eq!(resp.status(), StatusCode::OK); + assert_eq!( + resp.headers().get(header::CONTENT_TYPE), + Some(&header::HeaderValue::from_static("image/png")) + ); + let result = test::read_body(resp).await; + let result = &result.bytes().collect::, _>>().unwrap(); + let image = load_from_memory_with_format(result.as_slice(), image::ImageFormat::Png).unwrap(); + let image = image.as_rgba8().unwrap(); + assert_eq!( + ((500 / GRANULARITY) as u32, (1000 / GRANULARITY) as u32), + image.dimensions() + ); + + // (0,0) is be top left. + let on_planting = image.get_pixel(1, 1); + let close_to_planting = image.get_pixel(2, 2); + let a_bit_away_from_planting = image.get_pixel(10, 10); + // The planting influences the map as we set the date back in the query. + assert_eq!([96, 158, 0, 62], on_planting.0); + assert_eq!([98, 156, 0, 57], close_to_planting.0); + assert_eq!([123, 131, 0, 8], a_bit_away_from_planting.0); +} + #[actix_rt::test] async fn test_missing_entities_fails() { let pool = init_test_database(|conn| { From dfbcfa13c6083262907bc4345d4a22f8236e604d Mon Sep 17 00:00:00 2001 From: kitzbergerg Date: Mon, 31 Jul 2023 15:29:51 +0200 Subject: [PATCH 031/157] move frontend benchmarks to own folder --- benchmarks/README.md | 57 +------------------ benchmarks/{ => frontend}/.gitignore | 0 benchmarks/frontend/README.md | 56 ++++++++++++++++++ benchmarks/{ => frontend}/package-lock.json | 0 benchmarks/{ => frontend}/package.json | 0 .../{ => frontend}/playwright.config.js | 0 .../tests/performance-audit.spec.js | 0 7 files changed, 58 insertions(+), 55 deletions(-) rename benchmarks/{ => frontend}/.gitignore (100%) create mode 100644 benchmarks/frontend/README.md rename benchmarks/{ => frontend}/package-lock.json (100%) rename benchmarks/{ => frontend}/package.json (100%) rename benchmarks/{ => frontend}/playwright.config.js (100%) rename benchmarks/{ => frontend}/tests/performance-audit.spec.js (100%) diff --git a/benchmarks/README.md b/benchmarks/README.md index 72615ccd5..e0667f488 100644 --- a/benchmarks/README.md +++ b/benchmarks/README.md @@ -1,56 +1,3 @@ -# PermaplanT Performance Audit +# Benchmarks -## Requirements - -- nodejs 19.4.0 -- npm - -## Installation and Usage - -1. Install dependencies - -```shell -npm install -``` - -2. Start the backend from the backend folder - -```shell -cargo run -``` - -3. Start the frontend from the frontend folder - -```shell -npm run dev -``` - -4. Run benchmarks - -```shell -npm run benchmark -``` - -## Benchmarking - -The benchmarking script runs a performance audit on a web page using [Lighthouse](https://github.com/GoogleChrome/lighthouse) and [Playwright](https://playwright.dev/). -The audit measures the performance of a web page by generating a performance score and then saves the results of the audit in `report` folder: - -- `-report.json` - the raw report of LightHouse for a single test case defined in `performance-audit.spec.js` -- `-lighthouse-results.csv` - the results of the audit for all test cases - -Metrics: - -- `First Contentful Paint (FCP)` - the time it takes for the browser to render the first bit of content on the page. Measured in milliseconds. -- `Interactive` - the time it takes for the page to become fully interactive. Measured in milliseconds. -- other metrics are described in the [Chrome Developers documentation](https://web.dev/performance-scoring/) - -Pages to audit are defined in `performance-audit.spec.js` file as individual test cases. -In order to add new pages to the audit, add a new test case to the file e.g.: - -```javascript -test("Another page", async () => { - const testname = "Another page"; - await audit(testname, "http://localhost:5173/another_page", results); -}); -``` +This folder contains scripts to run benchmarks for PermaplanT. diff --git a/benchmarks/.gitignore b/benchmarks/frontend/.gitignore similarity index 100% rename from benchmarks/.gitignore rename to benchmarks/frontend/.gitignore diff --git a/benchmarks/frontend/README.md b/benchmarks/frontend/README.md new file mode 100644 index 000000000..72615ccd5 --- /dev/null +++ b/benchmarks/frontend/README.md @@ -0,0 +1,56 @@ +# PermaplanT Performance Audit + +## Requirements + +- nodejs 19.4.0 +- npm + +## Installation and Usage + +1. Install dependencies + +```shell +npm install +``` + +2. Start the backend from the backend folder + +```shell +cargo run +``` + +3. Start the frontend from the frontend folder + +```shell +npm run dev +``` + +4. Run benchmarks + +```shell +npm run benchmark +``` + +## Benchmarking + +The benchmarking script runs a performance audit on a web page using [Lighthouse](https://github.com/GoogleChrome/lighthouse) and [Playwright](https://playwright.dev/). +The audit measures the performance of a web page by generating a performance score and then saves the results of the audit in `report` folder: + +- `-report.json` - the raw report of LightHouse for a single test case defined in `performance-audit.spec.js` +- `-lighthouse-results.csv` - the results of the audit for all test cases + +Metrics: + +- `First Contentful Paint (FCP)` - the time it takes for the browser to render the first bit of content on the page. Measured in milliseconds. +- `Interactive` - the time it takes for the page to become fully interactive. Measured in milliseconds. +- other metrics are described in the [Chrome Developers documentation](https://web.dev/performance-scoring/) + +Pages to audit are defined in `performance-audit.spec.js` file as individual test cases. +In order to add new pages to the audit, add a new test case to the file e.g.: + +```javascript +test("Another page", async () => { + const testname = "Another page"; + await audit(testname, "http://localhost:5173/another_page", results); +}); +``` diff --git a/benchmarks/package-lock.json b/benchmarks/frontend/package-lock.json similarity index 100% rename from benchmarks/package-lock.json rename to benchmarks/frontend/package-lock.json diff --git a/benchmarks/package.json b/benchmarks/frontend/package.json similarity index 100% rename from benchmarks/package.json rename to benchmarks/frontend/package.json diff --git a/benchmarks/playwright.config.js b/benchmarks/frontend/playwright.config.js similarity index 100% rename from benchmarks/playwright.config.js rename to benchmarks/frontend/playwright.config.js diff --git a/benchmarks/tests/performance-audit.spec.js b/benchmarks/frontend/tests/performance-audit.spec.js similarity index 100% rename from benchmarks/tests/performance-audit.spec.js rename to benchmarks/frontend/tests/performance-audit.spec.js From a6943455151ddb0356eb5f0a8dee3453dbf8f35b Mon Sep 17 00:00:00 2001 From: kitzbergerg Date: Mon, 31 Jul 2023 18:17:02 +0200 Subject: [PATCH 032/157] fine tune shading score calculation --- .../migrations/2023-07-22-110000_shadings/up.sql | 10 +++++----- backend/src/test/plant_layer_heatmap.rs | 14 +++++++------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/backend/migrations/2023-07-22-110000_shadings/up.sql b/backend/migrations/2023-07-22-110000_shadings/up.sql index 06aadac8e..545ea9a87 100644 --- a/backend/migrations/2023-07-22-110000_shadings/up.sql +++ b/backend/migrations/2023-07-22-110000_shadings/up.sql @@ -56,8 +56,8 @@ BEGIN END; $$ LANGUAGE plpgsql; --- Calculate preference: Between -0.5 and 0.5 depending on shadings. --- Calculate relevance: 0.5 if there is shading; otherwise 0.0. +-- Calculate preference: Between -1 and 1 depending on shadings. +-- Calculate relevance: 1 if there is shading; otherwise 0.0. -- -- If the plant would die at the position set preference=-100 and relevance=100. CREATE FUNCTION calculate_score_from_shadings( @@ -144,9 +144,9 @@ BEGIN SELECT array_position(all_values, plant_shade) INTO pos1; SELECT array_position(all_values, shading_shade) INTO pos2; - -- Calculate the 'distance' to the preferred shade as a values between -0.5 and 0.5 - score.preference := (0.5 - (abs(pos1 - pos2) / (ARRAY_LENGTH(all_values, 1) - 1)::REAL)) / 2; - score.relevance := 0.5; + -- Calculate the 'distance' to the preferred shade as a values between -1 and 1 + score.preference := (0.5 - (abs(pos1 - pos2) / (ARRAY_LENGTH(all_values, 1) - 1)::REAL)^0.5) * 2.0; + score.relevance := 1.0; RETURN score; END; diff --git a/backend/src/test/plant_layer_heatmap.rs b/backend/src/test/plant_layer_heatmap.rs index 8f9efaea5..20617a81e 100644 --- a/backend/src/test/plant_layer_heatmap.rs +++ b/backend/src/test/plant_layer_heatmap.rs @@ -193,10 +193,10 @@ async fn test_heatmap_with_missing_corner_succeeds() { let top_right_pixel = image.get_pixel(8, 2); let bottom_left_pixel = image.get_pixel(2, 8); let bottom_right_pixel = image.get_pixel(8, 8); - assert_eq!([111, 143, 0, 62], top_left_pixel.0); - assert_eq!([111, 143, 0, 62], top_right_pixel.0); + assert_eq!([68, 186, 0, 117], top_left_pixel.0); + assert_eq!([68, 186, 0, 117], top_right_pixel.0); assert_eq!([255, 0, 0, 0], bottom_left_pixel.0); - assert_eq!([111, 143, 0, 62], bottom_right_pixel.0); + assert_eq!([68, 186, 0, 117], bottom_right_pixel.0); } #[actix_rt::test] @@ -244,9 +244,9 @@ async fn test_heatmap_with_shadings_succeeds() { let top_left_pixel = image.get_pixel(1, 1); let bottom_right_pixel = image.get_pixel(40, 80); // The shading is the exact opposite of the plants preference, therefore the map will be red. - assert_eq!([143, 111, 0, 62], top_left_pixel.0); + assert_eq!([186, 68, 0, 117], top_left_pixel.0); // Plant like other positions, therefore green. - assert_eq!([111, 143, 0, 62], bottom_right_pixel.0); + assert_eq!([68, 186, 0, 117], bottom_right_pixel.0); } #[actix_rt::test] @@ -317,7 +317,7 @@ async fn test_heatmap_with_shadings_and_light_requirement_succeeds() { let top_left_pixel = image.get_pixel(1, 1); let bottom_right_pixel = image.get_pixel(40, 80); // The shading is deep shade with is ok for the plant. - assert_eq!([111, 143, 0, 62], top_left_pixel.0); + assert_eq!([68, 186, 0, 117], top_left_pixel.0); // The plant can't grow in sun. assert_eq!([255, 0, 0, 255], bottom_right_pixel.0); @@ -347,7 +347,7 @@ async fn test_heatmap_with_shadings_and_light_requirement_succeeds() { // The plant can't grow in deep shade. assert_eq!([255, 0, 0, 255], top_left_pixel.0); // The plant can grow in sun. - assert_eq!([119, 135, 0, 62], bottom_right_pixel.0); + assert_eq!([127, 127, 0, 117], bottom_right_pixel.0); } #[actix_rt::test] From a80db45faebccc226379a84ca8ba5efdb52e3aca Mon Sep 17 00:00:00 2001 From: kitzbergerg Date: Mon, 31 Jul 2023 21:54:41 +0200 Subject: [PATCH 033/157] execute manual test with newest commit --- doc/tests/manual/reports/230723_heatmap_with_shade.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/tests/manual/reports/230723_heatmap_with_shade.md b/doc/tests/manual/reports/230723_heatmap_with_shade.md index bbbbf853d..80346d94c 100644 --- a/doc/tests/manual/reports/230723_heatmap_with_shade.md +++ b/doc/tests/manual/reports/230723_heatmap_with_shade.md @@ -3,9 +3,9 @@ ## General - Tester: Gabriel -- Date/Time: 23.07.2023 20:00 +- Date/Time: 31.07.2023 21:50 - Duration: 15 min -- Commit/Tag: ee50d96ddf10a13ba0c7fe2566e7533139edba58 +- Commit/Tag: a8b0079cbdd638aea5600373434d8dddcca8e7e7 - Planned tests: 1 - Executed tests: **1** - Passed tests: 1 @@ -149,7 +149,7 @@ curl --location 'http://localhost:8080/api/maps/1/layers/shade/shadings' \ --header 'Authorization: Bearer ' \ --data '{ "layerId": 3, - "shade": "no shade", + "shade": "light shade", "geometry": { "rings": [ [ @@ -187,7 +187,7 @@ The result should be: { "id": "21c9ca45-5ff4-492a-a537-7eb64c134613", "layerId": 3, - "shade": "no shade", + "shade": "light shade", "geometry": { "rings": [ [ @@ -228,7 +228,7 @@ The result should be: 6. Execute the request. ```bash -curl -o file.png --location 'http://localhost:8080/api/maps/1/layers/plants/heatmap?plant_id=1&plant_layer_id=1&shade_layer_id=3' \ +curl -o file.png --location 'http://localhost:8080/api/maps/1/layers/plants/heatmap?plant_id=1&plant_layer_id=2&shade_layer_id=3' \ --header 'Authorization: Bearer ' ``` From aab1a337e68f122be995837fad575472b8d5b0d1 Mon Sep 17 00:00:00 2001 From: kitzbergerg Date: Mon, 31 Jul 2023 22:17:49 +0200 Subject: [PATCH 034/157] add more data for larger map benchmark --- backend/Cargo.toml | 1 + .../backend/heatmap/insert_data-large_map.sh | 626 +++++++++++++++++- .../backend/heatmap/insert_data-middle_map.sh | 171 ++++- benchmarks/backend/heatmap/setup.sh | 16 +- 4 files changed, 799 insertions(+), 15 deletions(-) diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 07c72d836..a4d4009ac 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -60,5 +60,6 @@ image = { version = "0.24.6", default-features = false, features = ["png"] } [dev-dependencies] jsonwebkey = { version = "0.3.5", features = ["generate", "jwt-convert"] } +# TODO: remove before merge [profile.release] debug = true diff --git a/benchmarks/backend/heatmap/insert_data-large_map.sh b/benchmarks/backend/heatmap/insert_data-large_map.sh index 993e7c8d7..ff9896849 100755 --- a/benchmarks/backend/heatmap/insert_data-large_map.sh +++ b/benchmarks/backend/heatmap/insert_data-large_map.sh @@ -59,10 +59,162 @@ curl --location 'http://localhost:8080/api/maps' \ DATABASE_NAME="permaplant" DATABASE_USER="permaplant" PGPASSWORD=permaplant psql -h localhost -p 5432 -U $DATABASE_USER -d $DATABASE_NAME -c " -INSERT INTO plantings (id, layer_id, plant_id, x, y, width, height, rotation, scale_x, scale_y, add_date, remove_date) -VALUES - ('00000000-0000-0000-0000-000000000000', 2, 1, 15, 15, 0, 0, 0, 0, 0, DEFAULT, DEFAULT), - ('00000000-0000-0000-0000-000000000001', 2, 2, 20, 30, 0, 0, 0, 0, 0, DEFAULT, DEFAULT); +INSERT INTO "plantings" ("id", "layer_id", "plant_id", "x", "y", "width", "height", "rotation", "scale_x", "scale_y", + "add_date", "remove_date") +VALUES ('00000000-0000-0000-0000-000000000000', 2, 4506, 0300, 1200, 0, 0, 0, 0, 0, null, null), -- sweet cherry + + ('00000000-0000-0000-0000-000000000001', 2, 4532, 0400, 0200, 0, 0, 0, 0, 0, null, null), -- european plum + + ('00000000-0000-0000-0000-000000000002', 2, 1658, 0000, 0000, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000003', 2, 1658, 0010, 0005, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000004', 2, 1658, 0300, 0010, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000005', 2, 1658, 0350, 0000, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000006', 2, 1658, 0400, 0000, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000007', 2, 1658, 0500, 0020, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000008', 2, 1658, 0520, 0015, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000009', 2, 1658, 0550, 0000, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000010', 2, 1658, 0600, 0015, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000011', 2, 1658, 0610, 0000, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000012', 2, 1658, 0620, 0015, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000013', 2, 1658, 0650, 0015, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000014', 2, 1658, 0800, 0015, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000015', 2, 1658, 0820, 0015, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000016', 2, 1658, 0880, 0015, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000017', 2, 1658, 0900, 0015, 0, 0, 0, 0, 0, null, null), -- sweet fern + + ('00000000-0000-0000-0000-000000000018', 2, 1658, 0080, 0600, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000019', 2, 1658, 0080, 0650, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000020', 2, 1658, 0080, 0700, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000021', 2, 1658, 0080, 0750, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000022', 2, 1658, 0080, 0800, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000023', 2, 1658, 0080, 0850, 0, 0, 0, 0, 0, null, null), -- sweet fern + + ('00000000-0000-0000-0000-000000000024', 2, 5557, 0600, 1050, 0, 0, 0, 0, 0, null, null), -- tomato + ('00000000-0000-0000-0000-000000000025', 2, 5557, 0650, 1050, 0, 0, 0, 0, 0, null, null), -- tomato + ('00000000-0000-0000-0000-000000000026', 2, 5557, 0700, 1050, 0, 0, 0, 0, 0, null, null), -- tomato + ('00000000-0000-0000-0000-000000000027', 2, 0355, 0740, 1050, 0, 0, 0, 0, 0, null, null), -- chives + ('00000000-0000-0000-0000-000000000028', 2, 0355, 0760, 1050, 0, 0, 0, 0, 0, null, null), -- chives + ('00000000-0000-0000-0000-000000000029', 2, 6247, 0550, 1030, 0, 0, 0, 0, 0, null, null), -- rhubarb + ('00000000-0000-0000-0000-000000000030', 2, 7708, 0550, 1000, 0, 0, 0, 0, 0, null, null), -- Iris germanica + ('00000000-0000-0000-0000-000000000031', 2, 7708, 0575, 1000, 0, 0, 0, 0, 0, null, null), -- Iris germanica + ('00000000-0000-0000-0000-000000000032', 2, 7708, 0600, 1000, 0, 0, 0, 0, 0, null, null), -- Iris germanica + ('00000000-0000-0000-0000-000000000033', 2, 7708, 0625, 1000, 0, 0, 0, 0, 0, null, null), -- Iris germanica + ('00000000-0000-0000-0000-000000000034', 2, 7708, 0650, 1000, 0, 0, 0, 0, 0, null, null), -- Iris germanica + ('00000000-0000-0000-0000-000000000035', 2, 7708, 0675, 1000, 0, 0, 0, 0, 0, null, null), -- Iris germanica + + ('00000000-0000-0000-0000-000000000036', 2, 5807, 0000, 1970, 0, 0, 0, 0, 0, null, null), -- Thuja plicata + ('00000000-0000-0000-0000-000000000037', 2, 5807, 0100, 1970, 0, 0, 0, 0, 0, null, null), -- Thuja plicata + ('00000000-0000-0000-0000-000000000038', 2, 5807, 0200, 1970, 0, 0, 0, 0, 0, null, null), -- Thuja plicata + ('00000000-0000-0000-0000-000000000039', 2, 5807, 0300, 1970, 0, 0, 0, 0, 0, null, null), -- Thuja plicata + ('00000000-0000-0000-0000-000000000040', 2, 5807, 0400, 1970, 0, 0, 0, 0, 0, null, null), -- Thuja plicata + ('00000000-0000-0000-0000-000000000041', 2, 5807, 0500, 1970, 0, 0, 0, 0, 0, null, null), -- Thuja plicata + ('00000000-0000-0000-0000-000000000042', 2, 5807, 0600, 1970, 0, 0, 0, 0, 0, null, null), -- Thuja plicata + ('00000000-0000-0000-0000-000000000043', 2, 5807, 0700, 1970, 0, 0, 0, 0, 0, null, null), -- Thuja plicata + + + + ('00000000-0000-0000-0000-000000000000', 2, 4506, 5300, 6200, 0, 0, 0, 0, 0, null, null), -- sweet cherry + + ('00000000-0000-0000-0000-000000000001', 2, 4532, 5400, 5200, 0, 0, 0, 0, 0, null, null), -- european plum + + ('00000000-0000-0000-0000-000000000002', 2, 1658, 5000, 5000, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000003', 2, 1658, 5010, 5005, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000004', 2, 1658, 5300, 5010, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000005', 2, 1658, 5350, 5000, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000006', 2, 1658, 5400, 5000, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000007', 2, 1658, 5500, 5020, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000008', 2, 1658, 5520, 5015, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000009', 2, 1658, 5550, 5000, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000010', 2, 1658, 5600, 5015, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000011', 2, 1658, 5610, 5000, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000012', 2, 1658, 5620, 5015, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000013', 2, 1658, 5650, 5015, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000014', 2, 1658, 5800, 5015, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000015', 2, 1658, 5820, 5015, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000016', 2, 1658, 5880, 5015, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000017', 2, 1658, 5900, 5015, 0, 0, 0, 0, 0, null, null), -- sweet fern + + ('00000000-0000-0000-0000-000000000018', 2, 1658, 5080, 5600, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000019', 2, 1658, 5080, 5650, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000020', 2, 1658, 5080, 5700, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000021', 2, 1658, 5080, 5750, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000022', 2, 1658, 5080, 5800, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000023', 2, 1658, 5080, 5850, 0, 0, 0, 0, 0, null, null), -- sweet fern + + ('00000000-0000-0000-0000-000000000024', 2, 5557, 5600, 6050, 0, 0, 0, 0, 0, null, null), -- tomato + ('00000000-0000-0000-0000-000000000025', 2, 5557, 5650, 6050, 0, 0, 0, 0, 0, null, null), -- tomato + ('00000000-0000-0000-0000-000000000026', 2, 5557, 5700, 6050, 0, 0, 0, 0, 0, null, null), -- tomato + ('00000000-0000-0000-0000-000000000027', 2, 0355, 5740, 6050, 0, 0, 0, 0, 0, null, null), -- chives + ('00000000-0000-0000-0000-000000000028', 2, 0355, 5760, 6050, 0, 0, 0, 0, 0, null, null), -- chives + ('00000000-0000-0000-0000-000000000029', 2, 6247, 5550, 6030, 0, 0, 0, 0, 0, null, null), -- rhubarb + ('00000000-0000-0000-0000-000000000030', 2, 7708, 5550, 6000, 0, 0, 0, 0, 0, null, null), -- Iris germanica + ('00000000-0000-0000-0000-000000000031', 2, 7708, 5575, 6000, 0, 0, 0, 0, 0, null, null), -- Iris germanica + ('00000000-0000-0000-0000-000000000032', 2, 7708, 5600, 6000, 0, 0, 0, 0, 0, null, null), -- Iris germanica + ('00000000-0000-0000-0000-000000000033', 2, 7708, 5625, 6000, 0, 0, 0, 0, 0, null, null), -- Iris germanica + ('00000000-0000-0000-0000-000000000034', 2, 7708, 5650, 6000, 0, 0, 0, 0, 0, null, null), -- Iris germanica + ('00000000-0000-0000-0000-000000000035', 2, 7708, 5675, 6000, 0, 0, 0, 0, 0, null, null), -- Iris germanica + + ('00000000-0000-0000-0000-000000000036', 2, 5807, 5000, 6970, 0, 0, 0, 0, 0, null, null), -- Thuja plicata + ('00000000-0000-0000-0000-000000000037', 2, 5807, 5100, 6970, 0, 0, 0, 0, 0, null, null), -- Thuja plicata + ('00000000-0000-0000-0000-000000000038', 2, 5807, 5200, 6970, 0, 0, 0, 0, 0, null, null), -- Thuja plicata + ('00000000-0000-0000-0000-000000000039', 2, 5807, 5300, 6970, 0, 0, 0, 0, 0, null, null), -- Thuja plicata + ('00000000-0000-0000-0000-000000000040', 2, 5807, 5400, 6970, 0, 0, 0, 0, 0, null, null), -- Thuja plicata + ('00000000-0000-0000-0000-000000000041', 2, 5807, 5500, 6970, 0, 0, 0, 0, 0, null, null), -- Thuja plicata + ('00000000-0000-0000-0000-000000000042', 2, 5807, 5600, 6970, 0, 0, 0, 0, 0, null, null), -- Thuja plicata + ('00000000-0000-0000-0000-000000000043', 2, 5807, 5700, 6970, 0, 0, 0, 0, 0, null, null), -- Thuja plicata + + + + ('00000000-0000-0000-0000-000000000000', 2, 4506, 8300, 8200, 0, 0, 0, 0, 0, null, null), -- sweet cherry + + ('00000000-0000-0000-0000-000000000001', 2, 4532, 8400, 8200, 0, 0, 0, 0, 0, null, null), -- european plum + + ('00000000-0000-0000-0000-000000000002', 2, 1658, 8000, 8000, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000003', 2, 1658, 8010, 8005, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000004', 2, 1658, 8300, 8010, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000005', 2, 1658, 8350, 8000, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000006', 2, 1658, 8400, 8000, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000007', 2, 1658, 8500, 8020, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000008', 2, 1658, 8520, 8015, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000009', 2, 1658, 8550, 8000, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000010', 2, 1658, 8600, 8015, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000011', 2, 1658, 8610, 8000, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000012', 2, 1658, 8620, 8015, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000013', 2, 1658, 8650, 8015, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000014', 2, 1658, 8800, 8015, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000015', 2, 1658, 8820, 8015, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000016', 2, 1658, 8880, 8015, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000017', 2, 1658, 8900, 8015, 0, 0, 0, 0, 0, null, null), -- sweet fern + + ('00000000-0000-0000-0000-000000000018', 2, 1658, 8080, 8600, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000019', 2, 1658, 8080, 8650, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000020', 2, 1658, 8080, 8700, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000021', 2, 1658, 8080, 8750, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000022', 2, 1658, 8080, 8800, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000023', 2, 1658, 8080, 8850, 0, 0, 0, 0, 0, null, null), -- sweet fern + + ('00000000-0000-0000-0000-000000000024', 2, 5557, 8600, 8050, 0, 0, 0, 0, 0, null, null), -- tomato + ('00000000-0000-0000-0000-000000000025', 2, 5557, 8650, 8050, 0, 0, 0, 0, 0, null, null), -- tomato + ('00000000-0000-0000-0000-000000000026', 2, 5557, 8700, 8050, 0, 0, 0, 0, 0, null, null), -- tomato + ('00000000-0000-0000-0000-000000000027', 2, 0355, 8740, 8050, 0, 0, 0, 0, 0, null, null), -- chives + ('00000000-0000-0000-0000-000000000028', 2, 0355, 8760, 8050, 0, 0, 0, 0, 0, null, null), -- chives + ('00000000-0000-0000-0000-000000000029', 2, 6247, 8550, 8030, 0, 0, 0, 0, 0, null, null), -- rhubarb + ('00000000-0000-0000-0000-000000000030', 2, 7708, 8550, 8000, 0, 0, 0, 0, 0, null, null), -- Iris germanica + ('00000000-0000-0000-0000-000000000031', 2, 7708, 8575, 8000, 0, 0, 0, 0, 0, null, null), -- Iris germanica + ('00000000-0000-0000-0000-000000000032', 2, 7708, 8600, 8000, 0, 0, 0, 0, 0, null, null), -- Iris germanica + ('00000000-0000-0000-0000-000000000033', 2, 7708, 8625, 8000, 0, 0, 0, 0, 0, null, null), -- Iris germanica + ('00000000-0000-0000-0000-000000000034', 2, 7708, 8650, 8000, 0, 0, 0, 0, 0, null, null), -- Iris germanica + ('00000000-0000-0000-0000-000000000035', 2, 7708, 8675, 8000, 0, 0, 0, 0, 0, null, null), -- Iris germanica + + ('00000000-0000-0000-0000-000000000036', 2, 5807, 8000, 8970, 0, 0, 0, 0, 0, null, null), -- Thuja plicata + ('00000000-0000-0000-0000-000000000037', 2, 5807, 8100, 8970, 0, 0, 0, 0, 0, null, null), -- Thuja plicata + ('00000000-0000-0000-0000-000000000038', 2, 5807, 8200, 8970, 0, 0, 0, 0, 0, null, null), -- Thuja plicata + ('00000000-0000-0000-0000-000000000039', 2, 5807, 8300, 8970, 0, 0, 0, 0, 0, null, null), -- Thuja plicata + ('00000000-0000-0000-0000-000000000040', 2, 5807, 8400, 8970, 0, 0, 0, 0, 0, null, null), -- Thuja plicata + ('00000000-0000-0000-0000-000000000041', 2, 5807, 8500, 8970, 0, 0, 0, 0, 0, null, null), -- Thuja plicata + ('00000000-0000-0000-0000-000000000042', 2, 5807, 8600, 8970, 0, 0, 0, 0, 0, null, null), -- Thuja plicata + ('00000000-0000-0000-0000-000000000043', 2, 5807, 8700, 8970, 0, 0, 0, 0, 0, null, null) -- Thuja plicata +; " # Create shadings @@ -80,11 +232,11 @@ curl --location 'http://localhost:8080/api/maps/1/layers/shade/shadings' \ "y": 0.0 }, { - "x": 200.0, + "x": 1000.0, "y": 0.0 }, { - "x": 200.0, + "x": 1000.0, "y": 200.0 }, { @@ -101,3 +253,465 @@ curl --location 'http://localhost:8080/api/maps/1/layers/shade/shadings' \ }, "actionId": "00000000-0000-0000-0000-000000000000" }' + +curl --location 'http://localhost:8080/api/maps/1/layers/shade/shadings' \ +--header "Authorization: Bearer ${access_token}" \ +--header 'Content-Type: application/json' \ +--data '{ + "layerId": 3, + "shade": "permanent shade", + "geometry": { + "rings": [ + [ + { + "x": 250.0, + "y": 50.0 + }, + { + "x": 400.0, + "y": 12.26 + }, + { + "x": 550.0, + "y": 50.0 + }, + { + "x": 612.26, + "y": 200.0 + }, + { + "x": 550.0, + "y": 350.0 + }, + { + "x": 400.0, + "y": 412.26 + }, + { + "x": 250.0, + "y": 350.0 + }, + { + "x": 187.74, + "y": 200.0 + }, + { + "x": 250.0, + "y": 50.0 + } + ] + ], + "srid": 4326 + }, + "actionId": "00000000-0000-0000-0000-000000000000" +}' + +curl --location 'http://localhost:8080/api/maps/1/layers/shade/shadings' \ +--header "Authorization: Bearer ${access_token}" \ +--header 'Content-Type: application/json' \ +--data '{ + "layerId": 3, + "shade": "permanent shade", + "geometry": { + "rings": [ + [ + { + "x": 0.0, + "y": 400.0 + }, + { + "x": 100.0, + "y": 400.0 + }, + { + "x": 100.0, + "y": 1000.0 + }, + { + "x": 0.0, + "y": 1000.0 + }, + { + "x": 0.0, + "y": 400.0 + } + ] + ], + "srid": 4326 + }, + "actionId": "00000000-0000-0000-0000-000000000000" +}' + +curl --location 'http://localhost:8080/api/maps/1/layers/shade/shadings' \ +--header "Authorization: Bearer ${access_token}" \ +--header 'Content-Type: application/json' \ +--data '{ + "layerId": 3, + "shade": "light shade", + "geometry": { + "rings": [ + [ + { + "x": 0.0, + "y": 900.0 + }, + { + "x": 800.0, + "y": 900.0 + }, + { + "x": 800.0, + "y": 1000.0 + }, + { + "x": 0.0, + "y": 1000.0 + }, + { + "x": 0.0, + "y": 900.0 + } + ] + ], + "srid": 4326 + }, + "actionId": "00000000-0000-0000-0000-000000000000" +}' + +curl --location 'http://localhost:8080/api/maps/1/layers/shade/shadings' \ +--header "Authorization: Bearer ${access_token}" \ +--header 'Content-Type: application/json' \ +--data '{ + "layerId": 3, + "shade": "light shade", + "geometry": { + "rings": [ + [ + { + "x": 150.0, + "y": 1050.0 + }, + { + "x": 300.0, + "y": 1012.26 + }, + { + "x": 450.0, + "y": 1050.0 + }, + { + "x": 512.26, + "y": 1200.0 + }, + { + "x": 450.0, + "y": 1350.0 + }, + { + "x": 300.0, + "y": 1412.26 + }, + { + "x": 150.0, + "y": 1350.0 + }, + { + "x": 87.74, + "y": 1200.0 + }, + { + "x": 150.0, + "y": 1050.0 + } + ] + ], + "srid": 4326 + }, + "actionId": "00000000-0000-0000-0000-000000000000" +}' + +curl --location 'http://localhost:8080/api/maps/1/layers/shade/shadings' \ +--header "Authorization: Bearer ${access_token}" \ +--header 'Content-Type: application/json' \ +--data '{ + "layerId": 3, + "shade": "permanent deep shade", + "geometry": { + "rings": [ + [ + { + "x": 0.0, + "y": 1800.0 + }, + { + "x": 800.0, + "y": 1800.0 + }, + { + "x": 800.0, + "y": 2000.0 + }, + { + "x": 0.0, + "y": 2000.0 + }, + { + "x": 0.0, + "y": 1800.0 + } + ] + ], + "srid": 4326 + }, + "actionId": "00000000-0000-0000-0000-000000000000" +}' + + + +curl --location 'http://localhost:8080/api/maps/1/layers/shade/shadings' \ +--header "Authorization: Bearer ${access_token}" \ +--header 'Content-Type: application/json' \ +--data '{ + "layerId": 3, + "shade": "partial shade", + "geometry": { + "rings": [ + [ + { + "x": 5000.0, + "y": 5000.0 + }, + { + "x": 6000.0, + "y": 5000 0.0 + }, + { + "x": 6000.0, + "y": 5200.0 + }, + { + "x": 5000.0, + "y": 5200.0 + }, + { + "x": 5000.0, + "y": 5000.0 + } + ] + ], + "srid": 4326 + }, + "actionId": "00000000-0000-0000-0000-000000000000" +}' + +curl --location 'http://localhost:8080/api/maps/1/layers/shade/shadings' \ +--header "Authorization: Bearer ${access_token}" \ +--header 'Content-Type: application/json' \ +--data '{ + "layerId": 3, + "shade": "permanent shade", + "geometry": { + "rings": [ + [ + { + "x": 5250.0, + "y": 5050.0 + }, + { + "x": 5400.0, + "y": 5012.26 + }, + { + "x": 5550.0, + "y": 5050.0 + }, + { + "x": 5612.26, + "y": 5200.0 + }, + { + "x": 5550.0, + "y": 5350.0 + }, + { + "x": 5400.0, + "y": 5412.26 + }, + { + "x": 5250.0, + "y": 5350.0 + }, + { + "x": 5187.74, + "y": 5200.0 + }, + { + "x": 5250.0, + "y": 5050.0 + } + ] + ], + "srid": 4326 + }, + "actionId": "00000000-0000-0000-0000-000000000000" +}' + +curl --location 'http://localhost:8080/api/maps/1/layers/shade/shadings' \ +--header "Authorization: Bearer ${access_token}" \ +--header 'Content-Type: application/json' \ +--data '{ + "layerId": 3, + "shade": "permanent shade", + "geometry": { + "rings": [ + [ + { + "x": 5000.0, + "y": 5400.0 + }, + { + "x": 5100.0, + "y": 5400.0 + }, + { + "x": 5100.0, + "y": 6000.0 + }, + { + "x": 5000.0, + "y": 6000.0 + }, + { + "x": 5000.0, + "y": 5400.0 + } + ] + ], + "srid": 4326 + }, + "actionId": "00000000-0000-0000-0000-000000000000" +}' + +curl --location 'http://localhost:8080/api/maps/1/layers/shade/shadings' \ +--header "Authorization: Bearer ${access_token}" \ +--header 'Content-Type: application/json' \ +--data '{ + "layerId": 3, + "shade": "light shade", + "geometry": { + "rings": [ + [ + { + "x": 5000.0, + "y": 5900.0 + }, + { + "x": 5800.0, + "y": 5900.0 + }, + { + "x": 5800.0, + "y": 6000.0 + }, + { + "x": 5000.0, + "y": 6000.0 + }, + { + "x": 5000.0, + "y": 5900.0 + } + ] + ], + "srid": 4326 + }, + "actionId": "00000000-0000-0000-0000-000000000000" +}' + +curl --location 'http://localhost:8080/api/maps/1/layers/shade/shadings' \ +--header "Authorization: Bearer ${access_token}" \ +--header 'Content-Type: application/json' \ +--data '{ + "layerId": 3, + "shade": "light shade", + "geometry": { + "rings": [ + [ + { + "x": 5150.0, + "y": 6050.0 + }, + { + "x": 5300.0, + "y": 6012.26 + }, + { + "x": 5450.0, + "y": 6050.0 + }, + { + "x": 5512.26, + "y": 6200.0 + }, + { + "x": 5450.0, + "y": 6350.0 + }, + { + "x": 5300.0, + "y": 6412.26 + }, + { + "x": 5150.0, + "y": 6350.0 + }, + { + "x": 5000 87.74, + "y": 6200.0 + }, + { + "x": 5150.0, + "y": 6050.0 + } + ] + ], + "srid": 4326 + }, + "actionId": "00000000-0000-0000-0000-000000000000" +}' + +curl --location 'http://localhost:8080/api/maps/1/layers/shade/shadings' \ +--header "Authorization: Bearer ${access_token}" \ +--header 'Content-Type: application/json' \ +--data '{ + "layerId": 3, + "shade": "permanent deep shade", + "geometry": { + "rings": [ + [ + { + "x": 5000.0, + "y": 6800.0 + }, + { + "x": 5800.0, + "y": 6800.0 + }, + { + "x": 5800.0, + "y": 7000.0 + }, + { + "x": 5000.0, + "y": 7000.0 + }, + { + "x": 5000.0, + "y": 6800.0 + } + ] + ], + "srid": 4326 + }, + "actionId": "00000000-0000-0000-0000-000000000000" +}' diff --git a/benchmarks/backend/heatmap/insert_data-middle_map.sh b/benchmarks/backend/heatmap/insert_data-middle_map.sh index b3ce6f5e1..827d87e0e 100755 --- a/benchmarks/backend/heatmap/insert_data-middle_map.sh +++ b/benchmarks/backend/heatmap/insert_data-middle_map.sh @@ -17,8 +17,8 @@ curl --location 'http://localhost:8080/api/maps' \ --header "Authorization: Bearer ${access_token}" \ --header 'Content-Type: application/json' \ --data '{ - "name": "Test Map", - "creation_date": "2023-07-25", + "name": "My Garden", + "creation_date": "2023-07-31", "is_inactive": false, "zoom_factor": 100, "honors": 0, @@ -59,10 +59,41 @@ curl --location 'http://localhost:8080/api/maps' \ DATABASE_NAME="permaplant" DATABASE_USER="permaplant" PGPASSWORD=permaplant psql -h localhost -p 5432 -U $DATABASE_USER -d $DATABASE_NAME -c " -INSERT INTO plantings (id, layer_id, plant_id, x, y, width, height, rotation, scale_x, scale_y, add_date, remove_date) -VALUES - ('00000000-0000-0000-0000-000000000000', 2, 1, 15, 15, 0, 0, 0, 0, 0, DEFAULT, DEFAULT), - ('00000000-0000-0000-0000-000000000001', 2, 2, 20, 30, 0, 0, 0, 0, 0, DEFAULT, DEFAULT); +INSERT INTO "plantings" ("id", "layer_id", "plant_id", "x", "y", "width", "height", "rotation", "scale_x", "scale_y", + "add_date", "remove_date") +VALUES ('00000000-0000-0000-0000-000000000001', 2, 4532, 0400, 0200, 0, 0, 0, 0, 0, null, null), -- european plum + + ('00000000-0000-0000-0000-000000000002', 2, 1658, 0000, 0000, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000003', 2, 1658, 0010, 0005, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000004', 2, 1658, 0300, 0010, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000005', 2, 1658, 0350, 0000, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000006', 2, 1658, 0400, 0000, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000007', 2, 1658, 0500, 0020, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000008', 2, 1658, 0520, 0015, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000009', 2, 1658, 0550, 0000, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000010', 2, 1658, 0600, 0015, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000011', 2, 1658, 0610, 0000, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000012', 2, 1658, 0620, 0015, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000013', 2, 1658, 0650, 0015, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000014', 2, 1658, 0800, 0015, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000015', 2, 1658, 0820, 0015, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000016', 2, 1658, 0880, 0015, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000017', 2, 1658, 0900, 0015, 0, 0, 0, 0, 0, null, null), -- sweet fern + + ('00000000-0000-0000-0000-000000000018', 2, 1658, 0080, 0600, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000019', 2, 1658, 0080, 0650, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000020', 2, 1658, 0080, 0700, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000021', 2, 1658, 0080, 0750, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000022', 2, 1658, 0080, 0800, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000023', 2, 1658, 0080, 0850, 0, 0, 0, 0, 0, null, null), -- sweet fern + + ('00000000-0000-0000-0000-000000000030', 2, 7708, 0550, 1000, 0, 0, 0, 0, 0, null, null), -- Iris germanica + ('00000000-0000-0000-0000-000000000031', 2, 7708, 0575, 1000, 0, 0, 0, 0, 0, null, null), -- Iris germanica + ('00000000-0000-0000-0000-000000000032', 2, 7708, 0600, 1000, 0, 0, 0, 0, 0, null, null), -- Iris germanica + ('00000000-0000-0000-0000-000000000033', 2, 7708, 0625, 1000, 0, 0, 0, 0, 0, null, null), -- Iris germanica + ('00000000-0000-0000-0000-000000000034', 2, 7708, 0650, 1000, 0, 0, 0, 0, 0, null, null), -- Iris germanica + ('00000000-0000-0000-0000-000000000035', 2, 7708, 0675, 1000, 0, 0, 0, 0, 0, null, null) -- Iris germanica +; " # Create shadings @@ -80,11 +111,11 @@ curl --location 'http://localhost:8080/api/maps/1/layers/shade/shadings' \ "y": 0.0 }, { - "x": 200.0, + "x": 1000.0, "y": 0.0 }, { - "x": 200.0, + "x": 1000.0, "y": 200.0 }, { @@ -101,3 +132,127 @@ curl --location 'http://localhost:8080/api/maps/1/layers/shade/shadings' \ }, "actionId": "00000000-0000-0000-0000-000000000000" }' + +curl --location 'http://localhost:8080/api/maps/1/layers/shade/shadings' \ +--header "Authorization: Bearer ${access_token}" \ +--header 'Content-Type: application/json' \ +--data '{ + "layerId": 3, + "shade": "permanent shade", + "geometry": { + "rings": [ + [ + { + "x": 250.0, + "y": 50.0 + }, + { + "x": 400.0, + "y": 12.26 + }, + { + "x": 550.0, + "y": 50.0 + }, + { + "x": 612.26, + "y": 200.0 + }, + { + "x": 550.0, + "y": 350.0 + }, + { + "x": 400.0, + "y": 412.26 + }, + { + "x": 250.0, + "y": 350.0 + }, + { + "x": 187.74, + "y": 200.0 + }, + { + "x": 250.0, + "y": 50.0 + } + ] + ], + "srid": 4326 + }, + "actionId": "00000000-0000-0000-0000-000000000000" +}' + +curl --location 'http://localhost:8080/api/maps/1/layers/shade/shadings' \ +--header "Authorization: Bearer ${access_token}" \ +--header 'Content-Type: application/json' \ +--data '{ + "layerId": 3, + "shade": "permanent shade", + "geometry": { + "rings": [ + [ + { + "x": 0.0, + "y": 400.0 + }, + { + "x": 100.0, + "y": 400.0 + }, + { + "x": 100.0, + "y": 1000.0 + }, + { + "x": 0.0, + "y": 1000.0 + }, + { + "x": 0.0, + "y": 400.0 + } + ] + ], + "srid": 4326 + }, + "actionId": "00000000-0000-0000-0000-000000000000" +}' + +curl --location 'http://localhost:8080/api/maps/1/layers/shade/shadings' \ +--header "Authorization: Bearer ${access_token}" \ +--header 'Content-Type: application/json' \ +--data '{ + "layerId": 3, + "shade": "light shade", + "geometry": { + "rings": [ + [ + { + "x": 0.0, + "y": 900.0 + }, + { + "x": 800.0, + "y": 900.0 + }, + { + "x": 800.0, + "y": 1000.0 + }, + { + "x": 0.0, + "y": 1000.0 + }, + { + "x": 0.0, + "y": 900.0 + } + ] + ], + "srid": 4326 + }, + "actionId": "00000000-0000-0000-0000-000000000000" +}' diff --git a/benchmarks/backend/heatmap/setup.sh b/benchmarks/backend/heatmap/setup.sh index 9035961fc..01665ba56 100755 --- a/benchmarks/backend/heatmap/setup.sh +++ b/benchmarks/backend/heatmap/setup.sh @@ -11,7 +11,21 @@ LC_ALL=C diesel migration run cd ../scraper && npm run insert printf "\nNow run the backend in a second shell.\n" read -n 1 -p "Press any key to continue!" -../benchmarks/backend/heatmap/insert_data-large_map.sh $1 $2 +read -n 1 -p "Do you want to benchmark a small, medium or large map? (s/m/l) " opt; +case $opt in + s|S) + echo "small map" + ../benchmarks/backend/heatmap/insert_data-small_map.sh $1 $2 + ;; + m|M) + echo "middle map" + ../benchmarks/backend/heatmap/insert_data-middle_map.sh $1 $2 + ;; + l|L) + echo "large map" + ../benchmarks/backend/heatmap/insert_data-large_map.sh $1 $2 + ;; +esac printf "\n\nStop the backend.\n" read -n 1 -p "Press any key to continue!" From 25656afe4926c003216cb30adc1116f891d09c4d Mon Sep 17 00:00:00 2001 From: kitzbergerg Date: Mon, 31 Jul 2023 23:05:58 +0200 Subject: [PATCH 035/157] add benchmark scripts for relations endpoint --- benchmarks/backend/heatmap/get_statistics.sh | 6 +- .../backend/relations/get_statistics.sh | 21 ++ benchmarks/backend/relations/insert_data.sh | 258 ++++++++++++++++++ benchmarks/backend/relations/run_httperf.sh | 29 ++ benchmarks/backend/relations/setup.sh | 27 ++ 5 files changed, 340 insertions(+), 1 deletion(-) create mode 100755 benchmarks/backend/relations/get_statistics.sh create mode 100755 benchmarks/backend/relations/insert_data.sh create mode 100755 benchmarks/backend/relations/run_httperf.sh create mode 100755 benchmarks/backend/relations/setup.sh diff --git a/benchmarks/backend/heatmap/get_statistics.sh b/benchmarks/backend/heatmap/get_statistics.sh index e87206b3d..d95f745f3 100755 --- a/benchmarks/backend/heatmap/get_statistics.sh +++ b/benchmarks/backend/heatmap/get_statistics.sh @@ -5,7 +5,11 @@ cd backend grep ' UTC \[[0-9]*\] ' postgis.log | lines | split column -r '\[|\]' | group-by column2 | values | each {|pid| $pid | each {|el| $"($el.column1)[($el.column2)]($el.column3)" } } | flatten | save -f postgis_parsed.log print "SQL queries:" -grep 'LOG: execute s.*: SELECT \* FROM calculate_heatmap' postgis_parsed.log -A 2 | grep 'LOG: duration: .* ms$' | awk '{sum+=$7; count++; min=10000; if ($7<0+min) min=$7; if ($7>0+max) max=$7} END {print "total:", count, "\nmin:", min, "ms\navg:", sum/count, "ms\nmax:", max, "ms"}' +grep 'LOG: execute s.*: SELECT \* FROM calculate_heatmap' postgis_parsed.log -A 2 + | grep 'LOG: duration: .* ms$' + | awk 'NR == 1 {count=1; sum=$7; min=$7; max=$7} + NR > 1 {count++; sum+=$7; if ($7<0+min) min=$7; if ($7>0+max) max=$7} + END {print "total:", count, "\nmin:", min, "ms\navg:", sum/count, "ms\nmax:", max, "ms"}' print "" diff --git a/benchmarks/backend/relations/get_statistics.sh b/benchmarks/backend/relations/get_statistics.sh new file mode 100755 index 000000000..84f9734f2 --- /dev/null +++ b/benchmarks/backend/relations/get_statistics.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env nu + +cd backend + +grep ' UTC \[[0-9]*\] ' postgis.log | lines | split column -r '\[|\]' | group-by column2 | values | each {|pid| $pid | each {|el| $"($el.column1)[($el.column2)]($el.column3)" } } | flatten | save -f postgis_parsed.log + +print "SQL queries:" +grep 'LOG: execute s.*: (SELECT "relations"\."plant2", "relations"\."relation" FROM' postgis_parsed.log -A 2 + | grep 'LOG: duration: .* ms$' + | awk 'NR == 1 {count=1; sum=$7; min=$7; max=$7} + NR > 1 {count++; sum+=$7; if ($7<0+min) min=$7; if ($7>0+max) max=$7} + END {print "total:", count, "\nmin:", min, "ms\navg:", sum/count, "ms\nmax:", max, "ms"}' + +print "" + +print "Requests:" +grep 'Connection time \[ms\]: min' httperf.log + | awk '{print $5, " ", $7, " ", $9}' + | awk 'NR == 1 {count=1; sum=$1; min=$1; max=$1} + NR > 1 {count++; sum+=$1; if ($1<0+min) min=$1; if ($1>0+max) max=$1} + END {print "total:", count, "\nmin:", min, "ms\navg:", sum/count, "ms\nmax:", max, "ms"}' diff --git a/benchmarks/backend/relations/insert_data.sh b/benchmarks/backend/relations/insert_data.sh new file mode 100755 index 000000000..827d87e0e --- /dev/null +++ b/benchmarks/backend/relations/insert_data.sh @@ -0,0 +1,258 @@ +#!/usr/bin/env bash + +username=$1 +password=$2 + +# Get the OAuth2 access token +access_token=$(curl --request POST \ + --url 'https://auth.permaplant.net/realms/PermaplanT/protocol/openid-connect/token' \ + --header 'content-type: application/x-www-form-urlencoded' \ + --data grant_type=password \ + --data "username=${username}" \ + --data "password=${password}" \ + --data 'client_id=localhost' | jq -r .access_token) + +# Create map +curl --location 'http://localhost:8080/api/maps' \ +--header "Authorization: Bearer ${access_token}" \ +--header 'Content-Type: application/json' \ +--data '{ + "name": "My Garden", + "creation_date": "2023-07-31", + "is_inactive": false, + "zoom_factor": 100, + "honors": 0, + "visits": 0, + "harvested": 0, + "privacy": "public", + "description": "", + "geometry": { + "rings": [ + [ + { + "x": 0.0, + "y": 0.0 + }, + { + "x": 1000.0, + "y": 0.0 + }, + { + "x": 1000.0, + "y": 1000.0 + }, + { + "x": 0.0, + "y": 1000.0 + }, + { + "x": 0.0, + "y": 0.0 + } + ] + ], + "srid": 4326 + } +}' + +# Create plantings +DATABASE_NAME="permaplant" +DATABASE_USER="permaplant" +PGPASSWORD=permaplant psql -h localhost -p 5432 -U $DATABASE_USER -d $DATABASE_NAME -c " +INSERT INTO "plantings" ("id", "layer_id", "plant_id", "x", "y", "width", "height", "rotation", "scale_x", "scale_y", + "add_date", "remove_date") +VALUES ('00000000-0000-0000-0000-000000000001', 2, 4532, 0400, 0200, 0, 0, 0, 0, 0, null, null), -- european plum + + ('00000000-0000-0000-0000-000000000002', 2, 1658, 0000, 0000, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000003', 2, 1658, 0010, 0005, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000004', 2, 1658, 0300, 0010, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000005', 2, 1658, 0350, 0000, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000006', 2, 1658, 0400, 0000, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000007', 2, 1658, 0500, 0020, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000008', 2, 1658, 0520, 0015, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000009', 2, 1658, 0550, 0000, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000010', 2, 1658, 0600, 0015, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000011', 2, 1658, 0610, 0000, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000012', 2, 1658, 0620, 0015, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000013', 2, 1658, 0650, 0015, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000014', 2, 1658, 0800, 0015, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000015', 2, 1658, 0820, 0015, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000016', 2, 1658, 0880, 0015, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000017', 2, 1658, 0900, 0015, 0, 0, 0, 0, 0, null, null), -- sweet fern + + ('00000000-0000-0000-0000-000000000018', 2, 1658, 0080, 0600, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000019', 2, 1658, 0080, 0650, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000020', 2, 1658, 0080, 0700, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000021', 2, 1658, 0080, 0750, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000022', 2, 1658, 0080, 0800, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-000000000023', 2, 1658, 0080, 0850, 0, 0, 0, 0, 0, null, null), -- sweet fern + + ('00000000-0000-0000-0000-000000000030', 2, 7708, 0550, 1000, 0, 0, 0, 0, 0, null, null), -- Iris germanica + ('00000000-0000-0000-0000-000000000031', 2, 7708, 0575, 1000, 0, 0, 0, 0, 0, null, null), -- Iris germanica + ('00000000-0000-0000-0000-000000000032', 2, 7708, 0600, 1000, 0, 0, 0, 0, 0, null, null), -- Iris germanica + ('00000000-0000-0000-0000-000000000033', 2, 7708, 0625, 1000, 0, 0, 0, 0, 0, null, null), -- Iris germanica + ('00000000-0000-0000-0000-000000000034', 2, 7708, 0650, 1000, 0, 0, 0, 0, 0, null, null), -- Iris germanica + ('00000000-0000-0000-0000-000000000035', 2, 7708, 0675, 1000, 0, 0, 0, 0, 0, null, null) -- Iris germanica +; +" + +# Create shadings +curl --location 'http://localhost:8080/api/maps/1/layers/shade/shadings' \ +--header "Authorization: Bearer ${access_token}" \ +--header 'Content-Type: application/json' \ +--data '{ + "layerId": 3, + "shade": "partial shade", + "geometry": { + "rings": [ + [ + { + "x": 0.0, + "y": 0.0 + }, + { + "x": 1000.0, + "y": 0.0 + }, + { + "x": 1000.0, + "y": 200.0 + }, + { + "x": 0.0, + "y": 200.0 + }, + { + "x": 0.0, + "y": 0.0 + } + ] + ], + "srid": 4326 + }, + "actionId": "00000000-0000-0000-0000-000000000000" +}' + +curl --location 'http://localhost:8080/api/maps/1/layers/shade/shadings' \ +--header "Authorization: Bearer ${access_token}" \ +--header 'Content-Type: application/json' \ +--data '{ + "layerId": 3, + "shade": "permanent shade", + "geometry": { + "rings": [ + [ + { + "x": 250.0, + "y": 50.0 + }, + { + "x": 400.0, + "y": 12.26 + }, + { + "x": 550.0, + "y": 50.0 + }, + { + "x": 612.26, + "y": 200.0 + }, + { + "x": 550.0, + "y": 350.0 + }, + { + "x": 400.0, + "y": 412.26 + }, + { + "x": 250.0, + "y": 350.0 + }, + { + "x": 187.74, + "y": 200.0 + }, + { + "x": 250.0, + "y": 50.0 + } + ] + ], + "srid": 4326 + }, + "actionId": "00000000-0000-0000-0000-000000000000" +}' + +curl --location 'http://localhost:8080/api/maps/1/layers/shade/shadings' \ +--header "Authorization: Bearer ${access_token}" \ +--header 'Content-Type: application/json' \ +--data '{ + "layerId": 3, + "shade": "permanent shade", + "geometry": { + "rings": [ + [ + { + "x": 0.0, + "y": 400.0 + }, + { + "x": 100.0, + "y": 400.0 + }, + { + "x": 100.0, + "y": 1000.0 + }, + { + "x": 0.0, + "y": 1000.0 + }, + { + "x": 0.0, + "y": 400.0 + } + ] + ], + "srid": 4326 + }, + "actionId": "00000000-0000-0000-0000-000000000000" +}' + +curl --location 'http://localhost:8080/api/maps/1/layers/shade/shadings' \ +--header "Authorization: Bearer ${access_token}" \ +--header 'Content-Type: application/json' \ +--data '{ + "layerId": 3, + "shade": "light shade", + "geometry": { + "rings": [ + [ + { + "x": 0.0, + "y": 900.0 + }, + { + "x": 800.0, + "y": 900.0 + }, + { + "x": 800.0, + "y": 1000.0 + }, + { + "x": 0.0, + "y": 1000.0 + }, + { + "x": 0.0, + "y": 900.0 + } + ] + ], + "srid": 4326 + }, + "actionId": "00000000-0000-0000-0000-000000000000" +}' diff --git a/benchmarks/backend/relations/run_httperf.sh b/benchmarks/backend/relations/run_httperf.sh new file mode 100755 index 000000000..559ba69b3 --- /dev/null +++ b/benchmarks/backend/relations/run_httperf.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash + +username=$1 +password=$2 +number_of_requests=$3 +request_rate=$4 + +# Get the OAuth2 access token +access_token=$(curl --request POST \ + --url 'https://auth.permaplant.net/realms/PermaplanT/protocol/openid-connect/token' \ + --header 'content-type: application/x-www-form-urlencoded' \ + --data grant_type=password \ + --data "username=${username}" \ + --data "password=${password}" \ + --data 'client_id=localhost' | jq -r .access_token) + +# Run httperf + +# Loop to send requests +for (( i=1; i<=$number_of_requests; i++ )) +do + # Generate a random number between 0 and 9810 (number of plants) + ID=$(($RANDOM % 9810)) + + # Execute httperf with the random ID and append to log file + httperf --server localhost --port 8080 --uri /api/maps/1/layers/plants/relations?map_id=1\&plant_id=$ID --num-conns 1 --add-header="Authorization:Bearer ${access_token}\n" >> backend/httperf.log 2>&1 + let "sleep_time = 1 / $request_rate" + sleep $sleep_time +done diff --git a/benchmarks/backend/relations/setup.sh b/benchmarks/backend/relations/setup.sh new file mode 100755 index 000000000..295d650ae --- /dev/null +++ b/benchmarks/backend/relations/setup.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash + +# Start the database and run migrations +cd backend +docker run -d --name postgis -e POSTGRES_PASSWORD=permaplant -e POSTGRES_USER=permaplant -p 5432:5432 postgis/postgis:13-3.1 -c log_min_duration_statement=0 -c log_statement=all +sleep 10 +LC_ALL=C diesel setup +LC_ALL=C diesel migration run + +# Insert data +cd ../scraper && npm run insert +printf "\nNow run the backend in a second shell.\n" +read -n 1 -p "Press any key to continue!" +../benchmarks/backend/relations/insert_data.sh $1 $2 +printf "\n\nStop the backend.\n" +read -n 1 -p "Press any key to continue!" + +# Start the backend +cd ../backend +RUST_LOG="backend=warn,actix_web=warn" PERF=/usr/lib/linux-tools/5.4.0-153-generic/perf cargo flamegraph --open + +# Collect db logs +docker logs postgis > postgis.log 2>&1 + +# Remove database +docker kill postgis +docker rm postgis From ef57c2277a86c0930b04c4529b40963007cdde65 Mon Sep 17 00:00:00 2001 From: kitzbergerg Date: Mon, 31 Jul 2023 23:08:19 +0200 Subject: [PATCH 036/157] update benchmark doc --- doc/backend/06performance_benchmarks.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/backend/06performance_benchmarks.md b/doc/backend/06performance_benchmarks.md index 5d8f28492..6dd286507 100644 --- a/doc/backend/06performance_benchmarks.md +++ b/doc/backend/06performance_benchmarks.md @@ -84,13 +84,14 @@ The following is a step by step guide on how to execute the benchmark for the he 2. Execute: `./benchmarks/backend/heatmap/setup.sh `. Do the following once the script gives the instructions. - Start the backend in dev mode. - Press Enter. + - Select map size: press `s`. - Stop the backend. - Press Enter. 3. Wait for the backend to start in release mode. 4. Once its started execute in a second shell: `./benchmarks/backend/heatmap/run_httperf.sh 10000 100` 5. Wait for httperf to finish. 6. Press Ctrl+C in the `setup.sh` shell. -7. Wait until `flamegraph.svg` was generated (this might take 20min or longer). Do not interrupt this step. +7. Wait until `flamegraph.svg` was generated (this might take 20min or longer). If you interrupt this step you have to rerun the benchmark. 8. Execute: `./benchmarks/backend/heatmap/get_statistics.sh` 9. Collect results: - flamgraph.svg From cee1ebf189454eb8320962f454ce2c6b7f6b34d4 Mon Sep 17 00:00:00 2001 From: kitzbergerg Date: Tue, 1 Aug 2023 14:37:11 +0200 Subject: [PATCH 037/157] fix large map data insertion --- .../backend/heatmap/insert_data-large_map.sh | 206 +++++++++--------- 1 file changed, 103 insertions(+), 103 deletions(-) diff --git a/benchmarks/backend/heatmap/insert_data-large_map.sh b/benchmarks/backend/heatmap/insert_data-large_map.sh index ff9896849..ab6bce3d3 100755 --- a/benchmarks/backend/heatmap/insert_data-large_map.sh +++ b/benchmarks/backend/heatmap/insert_data-large_map.sh @@ -113,107 +113,107 @@ VALUES ('00000000-0000-0000-0000-000000000000', 2, 4506, 0300, 1200, 0, 0, 0, 0, - ('00000000-0000-0000-0000-000000000000', 2, 4506, 5300, 6200, 0, 0, 0, 0, 0, null, null), -- sweet cherry - - ('00000000-0000-0000-0000-000000000001', 2, 4532, 5400, 5200, 0, 0, 0, 0, 0, null, null), -- european plum - - ('00000000-0000-0000-0000-000000000002', 2, 1658, 5000, 5000, 0, 0, 0, 0, 0, null, null), -- sweet fern - ('00000000-0000-0000-0000-000000000003', 2, 1658, 5010, 5005, 0, 0, 0, 0, 0, null, null), -- sweet fern - ('00000000-0000-0000-0000-000000000004', 2, 1658, 5300, 5010, 0, 0, 0, 0, 0, null, null), -- sweet fern - ('00000000-0000-0000-0000-000000000005', 2, 1658, 5350, 5000, 0, 0, 0, 0, 0, null, null), -- sweet fern - ('00000000-0000-0000-0000-000000000006', 2, 1658, 5400, 5000, 0, 0, 0, 0, 0, null, null), -- sweet fern - ('00000000-0000-0000-0000-000000000007', 2, 1658, 5500, 5020, 0, 0, 0, 0, 0, null, null), -- sweet fern - ('00000000-0000-0000-0000-000000000008', 2, 1658, 5520, 5015, 0, 0, 0, 0, 0, null, null), -- sweet fern - ('00000000-0000-0000-0000-000000000009', 2, 1658, 5550, 5000, 0, 0, 0, 0, 0, null, null), -- sweet fern - ('00000000-0000-0000-0000-000000000010', 2, 1658, 5600, 5015, 0, 0, 0, 0, 0, null, null), -- sweet fern - ('00000000-0000-0000-0000-000000000011', 2, 1658, 5610, 5000, 0, 0, 0, 0, 0, null, null), -- sweet fern - ('00000000-0000-0000-0000-000000000012', 2, 1658, 5620, 5015, 0, 0, 0, 0, 0, null, null), -- sweet fern - ('00000000-0000-0000-0000-000000000013', 2, 1658, 5650, 5015, 0, 0, 0, 0, 0, null, null), -- sweet fern - ('00000000-0000-0000-0000-000000000014', 2, 1658, 5800, 5015, 0, 0, 0, 0, 0, null, null), -- sweet fern - ('00000000-0000-0000-0000-000000000015', 2, 1658, 5820, 5015, 0, 0, 0, 0, 0, null, null), -- sweet fern - ('00000000-0000-0000-0000-000000000016', 2, 1658, 5880, 5015, 0, 0, 0, 0, 0, null, null), -- sweet fern - ('00000000-0000-0000-0000-000000000017', 2, 1658, 5900, 5015, 0, 0, 0, 0, 0, null, null), -- sweet fern - - ('00000000-0000-0000-0000-000000000018', 2, 1658, 5080, 5600, 0, 0, 0, 0, 0, null, null), -- sweet fern - ('00000000-0000-0000-0000-000000000019', 2, 1658, 5080, 5650, 0, 0, 0, 0, 0, null, null), -- sweet fern - ('00000000-0000-0000-0000-000000000020', 2, 1658, 5080, 5700, 0, 0, 0, 0, 0, null, null), -- sweet fern - ('00000000-0000-0000-0000-000000000021', 2, 1658, 5080, 5750, 0, 0, 0, 0, 0, null, null), -- sweet fern - ('00000000-0000-0000-0000-000000000022', 2, 1658, 5080, 5800, 0, 0, 0, 0, 0, null, null), -- sweet fern - ('00000000-0000-0000-0000-000000000023', 2, 1658, 5080, 5850, 0, 0, 0, 0, 0, null, null), -- sweet fern - - ('00000000-0000-0000-0000-000000000024', 2, 5557, 5600, 6050, 0, 0, 0, 0, 0, null, null), -- tomato - ('00000000-0000-0000-0000-000000000025', 2, 5557, 5650, 6050, 0, 0, 0, 0, 0, null, null), -- tomato - ('00000000-0000-0000-0000-000000000026', 2, 5557, 5700, 6050, 0, 0, 0, 0, 0, null, null), -- tomato - ('00000000-0000-0000-0000-000000000027', 2, 0355, 5740, 6050, 0, 0, 0, 0, 0, null, null), -- chives - ('00000000-0000-0000-0000-000000000028', 2, 0355, 5760, 6050, 0, 0, 0, 0, 0, null, null), -- chives - ('00000000-0000-0000-0000-000000000029', 2, 6247, 5550, 6030, 0, 0, 0, 0, 0, null, null), -- rhubarb - ('00000000-0000-0000-0000-000000000030', 2, 7708, 5550, 6000, 0, 0, 0, 0, 0, null, null), -- Iris germanica - ('00000000-0000-0000-0000-000000000031', 2, 7708, 5575, 6000, 0, 0, 0, 0, 0, null, null), -- Iris germanica - ('00000000-0000-0000-0000-000000000032', 2, 7708, 5600, 6000, 0, 0, 0, 0, 0, null, null), -- Iris germanica - ('00000000-0000-0000-0000-000000000033', 2, 7708, 5625, 6000, 0, 0, 0, 0, 0, null, null), -- Iris germanica - ('00000000-0000-0000-0000-000000000034', 2, 7708, 5650, 6000, 0, 0, 0, 0, 0, null, null), -- Iris germanica - ('00000000-0000-0000-0000-000000000035', 2, 7708, 5675, 6000, 0, 0, 0, 0, 0, null, null), -- Iris germanica - - ('00000000-0000-0000-0000-000000000036', 2, 5807, 5000, 6970, 0, 0, 0, 0, 0, null, null), -- Thuja plicata - ('00000000-0000-0000-0000-000000000037', 2, 5807, 5100, 6970, 0, 0, 0, 0, 0, null, null), -- Thuja plicata - ('00000000-0000-0000-0000-000000000038', 2, 5807, 5200, 6970, 0, 0, 0, 0, 0, null, null), -- Thuja plicata - ('00000000-0000-0000-0000-000000000039', 2, 5807, 5300, 6970, 0, 0, 0, 0, 0, null, null), -- Thuja plicata - ('00000000-0000-0000-0000-000000000040', 2, 5807, 5400, 6970, 0, 0, 0, 0, 0, null, null), -- Thuja plicata - ('00000000-0000-0000-0000-000000000041', 2, 5807, 5500, 6970, 0, 0, 0, 0, 0, null, null), -- Thuja plicata - ('00000000-0000-0000-0000-000000000042', 2, 5807, 5600, 6970, 0, 0, 0, 0, 0, null, null), -- Thuja plicata - ('00000000-0000-0000-0000-000000000043', 2, 5807, 5700, 6970, 0, 0, 0, 0, 0, null, null), -- Thuja plicata - - - - ('00000000-0000-0000-0000-000000000000', 2, 4506, 8300, 8200, 0, 0, 0, 0, 0, null, null), -- sweet cherry - - ('00000000-0000-0000-0000-000000000001', 2, 4532, 8400, 8200, 0, 0, 0, 0, 0, null, null), -- european plum - - ('00000000-0000-0000-0000-000000000002', 2, 1658, 8000, 8000, 0, 0, 0, 0, 0, null, null), -- sweet fern - ('00000000-0000-0000-0000-000000000003', 2, 1658, 8010, 8005, 0, 0, 0, 0, 0, null, null), -- sweet fern - ('00000000-0000-0000-0000-000000000004', 2, 1658, 8300, 8010, 0, 0, 0, 0, 0, null, null), -- sweet fern - ('00000000-0000-0000-0000-000000000005', 2, 1658, 8350, 8000, 0, 0, 0, 0, 0, null, null), -- sweet fern - ('00000000-0000-0000-0000-000000000006', 2, 1658, 8400, 8000, 0, 0, 0, 0, 0, null, null), -- sweet fern - ('00000000-0000-0000-0000-000000000007', 2, 1658, 8500, 8020, 0, 0, 0, 0, 0, null, null), -- sweet fern - ('00000000-0000-0000-0000-000000000008', 2, 1658, 8520, 8015, 0, 0, 0, 0, 0, null, null), -- sweet fern - ('00000000-0000-0000-0000-000000000009', 2, 1658, 8550, 8000, 0, 0, 0, 0, 0, null, null), -- sweet fern - ('00000000-0000-0000-0000-000000000010', 2, 1658, 8600, 8015, 0, 0, 0, 0, 0, null, null), -- sweet fern - ('00000000-0000-0000-0000-000000000011', 2, 1658, 8610, 8000, 0, 0, 0, 0, 0, null, null), -- sweet fern - ('00000000-0000-0000-0000-000000000012', 2, 1658, 8620, 8015, 0, 0, 0, 0, 0, null, null), -- sweet fern - ('00000000-0000-0000-0000-000000000013', 2, 1658, 8650, 8015, 0, 0, 0, 0, 0, null, null), -- sweet fern - ('00000000-0000-0000-0000-000000000014', 2, 1658, 8800, 8015, 0, 0, 0, 0, 0, null, null), -- sweet fern - ('00000000-0000-0000-0000-000000000015', 2, 1658, 8820, 8015, 0, 0, 0, 0, 0, null, null), -- sweet fern - ('00000000-0000-0000-0000-000000000016', 2, 1658, 8880, 8015, 0, 0, 0, 0, 0, null, null), -- sweet fern - ('00000000-0000-0000-0000-000000000017', 2, 1658, 8900, 8015, 0, 0, 0, 0, 0, null, null), -- sweet fern - - ('00000000-0000-0000-0000-000000000018', 2, 1658, 8080, 8600, 0, 0, 0, 0, 0, null, null), -- sweet fern - ('00000000-0000-0000-0000-000000000019', 2, 1658, 8080, 8650, 0, 0, 0, 0, 0, null, null), -- sweet fern - ('00000000-0000-0000-0000-000000000020', 2, 1658, 8080, 8700, 0, 0, 0, 0, 0, null, null), -- sweet fern - ('00000000-0000-0000-0000-000000000021', 2, 1658, 8080, 8750, 0, 0, 0, 0, 0, null, null), -- sweet fern - ('00000000-0000-0000-0000-000000000022', 2, 1658, 8080, 8800, 0, 0, 0, 0, 0, null, null), -- sweet fern - ('00000000-0000-0000-0000-000000000023', 2, 1658, 8080, 8850, 0, 0, 0, 0, 0, null, null), -- sweet fern - - ('00000000-0000-0000-0000-000000000024', 2, 5557, 8600, 8050, 0, 0, 0, 0, 0, null, null), -- tomato - ('00000000-0000-0000-0000-000000000025', 2, 5557, 8650, 8050, 0, 0, 0, 0, 0, null, null), -- tomato - ('00000000-0000-0000-0000-000000000026', 2, 5557, 8700, 8050, 0, 0, 0, 0, 0, null, null), -- tomato - ('00000000-0000-0000-0000-000000000027', 2, 0355, 8740, 8050, 0, 0, 0, 0, 0, null, null), -- chives - ('00000000-0000-0000-0000-000000000028', 2, 0355, 8760, 8050, 0, 0, 0, 0, 0, null, null), -- chives - ('00000000-0000-0000-0000-000000000029', 2, 6247, 8550, 8030, 0, 0, 0, 0, 0, null, null), -- rhubarb - ('00000000-0000-0000-0000-000000000030', 2, 7708, 8550, 8000, 0, 0, 0, 0, 0, null, null), -- Iris germanica - ('00000000-0000-0000-0000-000000000031', 2, 7708, 8575, 8000, 0, 0, 0, 0, 0, null, null), -- Iris germanica - ('00000000-0000-0000-0000-000000000032', 2, 7708, 8600, 8000, 0, 0, 0, 0, 0, null, null), -- Iris germanica - ('00000000-0000-0000-0000-000000000033', 2, 7708, 8625, 8000, 0, 0, 0, 0, 0, null, null), -- Iris germanica - ('00000000-0000-0000-0000-000000000034', 2, 7708, 8650, 8000, 0, 0, 0, 0, 0, null, null), -- Iris germanica - ('00000000-0000-0000-0000-000000000035', 2, 7708, 8675, 8000, 0, 0, 0, 0, 0, null, null), -- Iris germanica - - ('00000000-0000-0000-0000-000000000036', 2, 5807, 8000, 8970, 0, 0, 0, 0, 0, null, null), -- Thuja plicata - ('00000000-0000-0000-0000-000000000037', 2, 5807, 8100, 8970, 0, 0, 0, 0, 0, null, null), -- Thuja plicata - ('00000000-0000-0000-0000-000000000038', 2, 5807, 8200, 8970, 0, 0, 0, 0, 0, null, null), -- Thuja plicata - ('00000000-0000-0000-0000-000000000039', 2, 5807, 8300, 8970, 0, 0, 0, 0, 0, null, null), -- Thuja plicata - ('00000000-0000-0000-0000-000000000040', 2, 5807, 8400, 8970, 0, 0, 0, 0, 0, null, null), -- Thuja plicata - ('00000000-0000-0000-0000-000000000041', 2, 5807, 8500, 8970, 0, 0, 0, 0, 0, null, null), -- Thuja plicata - ('00000000-0000-0000-0000-000000000042', 2, 5807, 8600, 8970, 0, 0, 0, 0, 0, null, null), -- Thuja plicata - ('00000000-0000-0000-0000-000000000043', 2, 5807, 8700, 8970, 0, 0, 0, 0, 0, null, null) -- Thuja plicata + ('00000000-0000-0000-0000-100000000000', 2, 4506, 5300, 6200, 0, 0, 0, 0, 0, null, null), -- sweet cherry + + ('00000000-0000-0000-0000-100000000001', 2, 4532, 5400, 5200, 0, 0, 0, 0, 0, null, null), -- european plum + + ('00000000-0000-0000-0000-100000000002', 2, 1658, 5000, 5000, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-100000000003', 2, 1658, 5010, 5005, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-100000000004', 2, 1658, 5300, 5010, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-100000000005', 2, 1658, 5350, 5000, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-100000000006', 2, 1658, 5400, 5000, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-100000000007', 2, 1658, 5500, 5020, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-100000000008', 2, 1658, 5520, 5015, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-100000000009', 2, 1658, 5550, 5000, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-100000000010', 2, 1658, 5600, 5015, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-100000000011', 2, 1658, 5610, 5000, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-100000000012', 2, 1658, 5620, 5015, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-100000000013', 2, 1658, 5650, 5015, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-100000000014', 2, 1658, 5800, 5015, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-100000000015', 2, 1658, 5820, 5015, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-100000000016', 2, 1658, 5880, 5015, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-100000000017', 2, 1658, 5900, 5015, 0, 0, 0, 0, 0, null, null), -- sweet fern + + ('00000000-0000-0000-0000-100000000018', 2, 1658, 5080, 5600, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-100000000019', 2, 1658, 5080, 5650, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-100000000020', 2, 1658, 5080, 5700, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-100000000021', 2, 1658, 5080, 5750, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-100000000022', 2, 1658, 5080, 5800, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-100000000023', 2, 1658, 5080, 5850, 0, 0, 0, 0, 0, null, null), -- sweet fern + + ('00000000-0000-0000-0000-100000000024', 2, 5557, 5600, 6050, 0, 0, 0, 0, 0, null, null), -- tomato + ('00000000-0000-0000-0000-100000000025', 2, 5557, 5650, 6050, 0, 0, 0, 0, 0, null, null), -- tomato + ('00000000-0000-0000-0000-100000000026', 2, 5557, 5700, 6050, 0, 0, 0, 0, 0, null, null), -- tomato + ('00000000-0000-0000-0000-100000000027', 2, 0355, 5740, 6050, 0, 0, 0, 0, 0, null, null), -- chives + ('00000000-0000-0000-0000-100000000028', 2, 0355, 5760, 6050, 0, 0, 0, 0, 0, null, null), -- chives + ('00000000-0000-0000-0000-100000000029', 2, 6247, 5550, 6030, 0, 0, 0, 0, 0, null, null), -- rhubarb + ('00000000-0000-0000-0000-100000000030', 2, 7708, 5550, 6000, 0, 0, 0, 0, 0, null, null), -- Iris germanica + ('00000000-0000-0000-0000-100000000031', 2, 7708, 5575, 6000, 0, 0, 0, 0, 0, null, null), -- Iris germanica + ('00000000-0000-0000-0000-100000000032', 2, 7708, 5600, 6000, 0, 0, 0, 0, 0, null, null), -- Iris germanica + ('00000000-0000-0000-0000-100000000033', 2, 7708, 5625, 6000, 0, 0, 0, 0, 0, null, null), -- Iris germanica + ('00000000-0000-0000-0000-100000000034', 2, 7708, 5650, 6000, 0, 0, 0, 0, 0, null, null), -- Iris germanica + ('00000000-0000-0000-0000-100000000035', 2, 7708, 5675, 6000, 0, 0, 0, 0, 0, null, null), -- Iris germanica + + ('00000000-0000-0000-0000-100000000036', 2, 5807, 5000, 6970, 0, 0, 0, 0, 0, null, null), -- Thuja plicata + ('00000000-0000-0000-0000-100000000037', 2, 5807, 5100, 6970, 0, 0, 0, 0, 0, null, null), -- Thuja plicata + ('00000000-0000-0000-0000-100000000038', 2, 5807, 5200, 6970, 0, 0, 0, 0, 0, null, null), -- Thuja plicata + ('00000000-0000-0000-0000-100000000039', 2, 5807, 5300, 6970, 0, 0, 0, 0, 0, null, null), -- Thuja plicata + ('00000000-0000-0000-0000-100000000040', 2, 5807, 5400, 6970, 0, 0, 0, 0, 0, null, null), -- Thuja plicata + ('00000000-0000-0000-0000-100000000041', 2, 5807, 5500, 6970, 0, 0, 0, 0, 0, null, null), -- Thuja plicata + ('00000000-0000-0000-0000-100000000042', 2, 5807, 5600, 6970, 0, 0, 0, 0, 0, null, null), -- Thuja plicata + ('00000000-0000-0000-0000-100000000043', 2, 5807, 5700, 6970, 0, 0, 0, 0, 0, null, null), -- Thuja plicata + + + + ('00000000-0000-0000-0000-200000000000', 2, 4506, 8300, 8200, 0, 0, 0, 0, 0, null, null), -- sweet cherry + + ('00000000-0000-0000-0000-200000000001', 2, 4532, 8400, 8200, 0, 0, 0, 0, 0, null, null), -- european plum + + ('00000000-0000-0000-0000-200000000002', 2, 1658, 8000, 8000, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-200000000003', 2, 1658, 8010, 8005, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-200000000004', 2, 1658, 8300, 8010, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-200000000005', 2, 1658, 8350, 8000, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-200000000006', 2, 1658, 8400, 8000, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-200000000007', 2, 1658, 8500, 8020, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-200000000008', 2, 1658, 8520, 8015, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-200000000009', 2, 1658, 8550, 8000, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-200000000010', 2, 1658, 8600, 8015, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-200000000011', 2, 1658, 8610, 8000, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-200000000012', 2, 1658, 8620, 8015, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-200000000013', 2, 1658, 8650, 8015, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-200000000014', 2, 1658, 8800, 8015, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-200000000015', 2, 1658, 8820, 8015, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-200000000016', 2, 1658, 8880, 8015, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-200000000017', 2, 1658, 8900, 8015, 0, 0, 0, 0, 0, null, null), -- sweet fern + + ('00000000-0000-0000-0000-200000000018', 2, 1658, 8080, 8600, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-200000000019', 2, 1658, 8080, 8650, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-200000000020', 2, 1658, 8080, 8700, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-200000000021', 2, 1658, 8080, 8750, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-200000000022', 2, 1658, 8080, 8800, 0, 0, 0, 0, 0, null, null), -- sweet fern + ('00000000-0000-0000-0000-200000000023', 2, 1658, 8080, 8850, 0, 0, 0, 0, 0, null, null), -- sweet fern + + ('00000000-0000-0000-0000-200000000024', 2, 5557, 8600, 8050, 0, 0, 0, 0, 0, null, null), -- tomato + ('00000000-0000-0000-0000-200000000025', 2, 5557, 8650, 8050, 0, 0, 0, 0, 0, null, null), -- tomato + ('00000000-0000-0000-0000-200000000026', 2, 5557, 8700, 8050, 0, 0, 0, 0, 0, null, null), -- tomato + ('00000000-0000-0000-0000-200000000027', 2, 0355, 8740, 8050, 0, 0, 0, 0, 0, null, null), -- chives + ('00000000-0000-0000-0000-200000000028', 2, 0355, 8760, 8050, 0, 0, 0, 0, 0, null, null), -- chives + ('00000000-0000-0000-0000-200000000029', 2, 6247, 8550, 8030, 0, 0, 0, 0, 0, null, null), -- rhubarb + ('00000000-0000-0000-0000-200000000030', 2, 7708, 8550, 8000, 0, 0, 0, 0, 0, null, null), -- Iris germanica + ('00000000-0000-0000-0000-200000000031', 2, 7708, 8575, 8000, 0, 0, 0, 0, 0, null, null), -- Iris germanica + ('00000000-0000-0000-0000-200000000032', 2, 7708, 8600, 8000, 0, 0, 0, 0, 0, null, null), -- Iris germanica + ('00000000-0000-0000-0000-200000000033', 2, 7708, 8625, 8000, 0, 0, 0, 0, 0, null, null), -- Iris germanica + ('00000000-0000-0000-0000-200000000034', 2, 7708, 8650, 8000, 0, 0, 0, 0, 0, null, null), -- Iris germanica + ('00000000-0000-0000-0000-200000000035', 2, 7708, 8675, 8000, 0, 0, 0, 0, 0, null, null), -- Iris germanica + + ('00000000-0000-0000-0000-200000000036', 2, 5807, 8000, 8970, 0, 0, 0, 0, 0, null, null), -- Thuja plicata + ('00000000-0000-0000-0000-200000000037', 2, 5807, 8100, 8970, 0, 0, 0, 0, 0, null, null), -- Thuja plicata + ('00000000-0000-0000-0000-200000000038', 2, 5807, 8200, 8970, 0, 0, 0, 0, 0, null, null), -- Thuja plicata + ('00000000-0000-0000-0000-200000000039', 2, 5807, 8300, 8970, 0, 0, 0, 0, 0, null, null), -- Thuja plicata + ('00000000-0000-0000-0000-200000000040', 2, 5807, 8400, 8970, 0, 0, 0, 0, 0, null, null), -- Thuja plicata + ('00000000-0000-0000-0000-200000000041', 2, 5807, 8500, 8970, 0, 0, 0, 0, 0, null, null), -- Thuja plicata + ('00000000-0000-0000-0000-200000000042', 2, 5807, 8600, 8970, 0, 0, 0, 0, 0, null, null), -- Thuja plicata + ('00000000-0000-0000-0000-200000000043', 2, 5807, 8700, 8970, 0, 0, 0, 0, 0, null, null) -- Thuja plicata ; " @@ -483,7 +483,7 @@ curl --location 'http://localhost:8080/api/maps/1/layers/shade/shadings' \ }, { "x": 6000.0, - "y": 5000 0.0 + "y": 5000.0 }, { "x": 6000.0, @@ -666,7 +666,7 @@ curl --location 'http://localhost:8080/api/maps/1/layers/shade/shadings' \ "y": 6350.0 }, { - "x": 5000 87.74, + "x": 5087.74, "y": 6200.0 }, { From 62d7ed5bca98699495a0c889429cc6d6b0c7e1e7 Mon Sep 17 00:00:00 2001 From: kitzbergerg Date: Tue, 1 Aug 2023 14:37:29 +0200 Subject: [PATCH 038/157] calculate granularity dynamically depending on map size --- .../2023-07-03-165000_heatmap/up.sql | 4 +- backend/src/model/entity/plant_layer.rs | 30 ++++- backend/src/test/plant_layer_heatmap.rs | 116 ++++++------------ 3 files changed, 66 insertions(+), 84 deletions(-) diff --git a/backend/migrations/2023-07-03-165000_heatmap/up.sql b/backend/migrations/2023-07-03-165000_heatmap/up.sql index 6ed27e414..0cdd209ad 100644 --- a/backend/migrations/2023-07-03-165000_heatmap/up.sql +++ b/backend/migrations/2023-07-03-165000_heatmap/up.sql @@ -86,8 +86,8 @@ BEGIN SELECT geometry FROM maps WHERE id = p_map_id INTO STRICT map_geometry; -- Calculate the number of rows and columns based on the map's size and granularity - num_cols := CEIL((x_max - x_min) / granularity); -- Adjusted for granularity - num_rows := CEIL((y_max - y_min) / granularity); -- Adjusted for granularity + num_cols := FLOOR((x_max - x_min) / granularity); -- Adjusted for granularity + num_rows := FLOOR((y_max - y_min) / granularity); -- Adjusted for granularity -- Calculate the score for each pixel on the heatmap FOR i IN 0..num_cols-1 LOOP diff --git a/backend/src/model/entity/plant_layer.rs b/backend/src/model/entity/plant_layer.rs index ef5e3e90e..790ea62b6 100644 --- a/backend/src/model/entity/plant_layer.rs +++ b/backend/src/model/entity/plant_layer.rs @@ -1,5 +1,7 @@ //! Contains the database implementation of the plant layer. +use std::cmp::max; + use chrono::NaiveDate; use diesel::{ debug_query, @@ -18,9 +20,6 @@ use crate::{ schema::relations, }; -/// The resolution of the generated heatmap in cm. -pub const GRANULARITY: i32 = 10; - /// A bounding box around the maps geometry. #[derive(Debug, Clone, QueryableByName)] struct BoundingBox { @@ -80,6 +79,8 @@ pub async fn heatmap( debug!("{}", debug_query::(&bounding_box_query)); let bounding_box = bounding_box_query.get_result::(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)") @@ -87,7 +88,7 @@ pub async fn heatmap( .bind::, _>(vec![plant_layer_id, shade_layer_id]) .bind::(plant_id) .bind::(date) - .bind::(GRANULARITY) + .bind::(granularity) .bind::(bounding_box.x_min) .bind::(bounding_box.y_min) .bind::(bounding_box.x_max) @@ -98,9 +99,9 @@ pub async fn heatmap( // 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)).ceil(); + (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)).ceil(); + (f64::from(bounding_box.y_max - bounding_box.y_min) / f64::from(granularity)).floor(); let mut heatmap = vec![vec![(0.0, 0.0); num_cols as usize]; num_rows as usize]; for HeatMapElement { preference, @@ -116,6 +117,23 @@ pub async fn 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 relations of a certain plant. /// /// # Errors diff --git a/backend/src/test/plant_layer_heatmap.rs b/backend/src/test/plant_layer_heatmap.rs index 20617a81e..754632e64 100644 --- a/backend/src/test/plant_layer_heatmap.rs +++ b/backend/src/test/plant_layer_heatmap.rs @@ -15,12 +15,9 @@ use uuid::Uuid; use crate::{ error::ServiceError, - model::{ - entity::plant_layer::GRANULARITY, - r#enum::{ - layer_type::LayerType, light_requirement::LightRequirement, - privacy_option::PrivacyOption, relation_type::RelationType, shade::Shade, - }, + model::r#enum::{ + layer_type::LayerType, light_requirement::LightRequirement, privacy_option::PrivacyOption, + relation_type::RelationType, shade::Shade, }, test::util::{ data, @@ -123,10 +120,7 @@ async fn test_check_heatmap_dimensionality_succeeds() { let result = &result.bytes().collect::, _>>().unwrap(); let image = load_from_memory_with_format(result.as_slice(), image::ImageFormat::Png).unwrap(); let image = image.as_rgba8().unwrap(); - assert_eq!( - ((10 / GRANULARITY) as u32, (100 / GRANULARITY) as u32), - image.dimensions() - ); + assert_eq!((10, 100), image.dimensions()); } #[actix_rt::test] @@ -152,10 +146,7 @@ async fn test_check_heatmap_non_0_xmin_succeeds() { let result = &result.bytes().collect::, _>>().unwrap(); let image = load_from_memory_with_format(result.as_slice(), image::ImageFormat::Png).unwrap(); let image = image.as_rgba8().unwrap(); - assert_eq!( - ((90 / GRANULARITY) as u32, (100 / GRANULARITY) as u32), - image.dimensions() - ); + assert_eq!((90, 100), image.dimensions()); } /// Test with a map geometry that excludes a corner. @@ -183,16 +174,13 @@ async fn test_heatmap_with_missing_corner_succeeds() { let result = &result.bytes().collect::, _>>().unwrap(); let image = load_from_memory_with_format(result.as_slice(), image::ImageFormat::Png).unwrap(); let image = image.as_rgba8().unwrap(); - assert_eq!( - ((100 / GRANULARITY) as u32, (100 / GRANULARITY) as u32), - image.dimensions() - ); // (0,0) is be top left. - let top_left_pixel = image.get_pixel(2, 2); - let top_right_pixel = image.get_pixel(8, 2); - let bottom_left_pixel = image.get_pixel(2, 8); - let bottom_right_pixel = image.get_pixel(8, 8); + let (x_dim, y_dim) = image.dimensions(); + let top_left_pixel = image.get_pixel(0, 0); + let top_right_pixel = image.get_pixel(x_dim - 1, 0); + let bottom_left_pixel = image.get_pixel(0, y_dim - 1); + let bottom_right_pixel = image.get_pixel(x_dim - 1, y_dim - 1); assert_eq!([68, 186, 0, 117], top_left_pixel.0); assert_eq!([68, 186, 0, 117], top_right_pixel.0); assert_eq!([255, 0, 0, 0], bottom_left_pixel.0); @@ -235,14 +223,11 @@ async fn test_heatmap_with_shadings_succeeds() { let result = &result.bytes().collect::, _>>().unwrap(); let image = load_from_memory_with_format(result.as_slice(), image::ImageFormat::Png).unwrap(); let image = image.as_rgba8().unwrap(); - assert_eq!( - ((500 / GRANULARITY) as u32, (1000 / GRANULARITY) as u32), - image.dimensions() - ); // (0,0) is be top left. - let top_left_pixel = image.get_pixel(1, 1); - let bottom_right_pixel = image.get_pixel(40, 80); + let (x_dim, y_dim) = image.dimensions(); + let top_left_pixel = image.get_pixel(0, 0); + let bottom_right_pixel = image.get_pixel(x_dim - 1, y_dim - 1); // The shading is the exact opposite of the plants preference, therefore the map will be red. assert_eq!([186, 68, 0, 117], top_left_pixel.0); // Plant like other positions, therefore green. @@ -308,14 +293,11 @@ async fn test_heatmap_with_shadings_and_light_requirement_succeeds() { let result = &result.bytes().collect::, _>>().unwrap(); let image = load_from_memory_with_format(result.as_slice(), image::ImageFormat::Png).unwrap(); let image = image.as_rgba8().unwrap(); - assert_eq!( - ((500 / GRANULARITY) as u32, (1000 / GRANULARITY) as u32), - image.dimensions() - ); // (0,0) is be top left. - let top_left_pixel = image.get_pixel(1, 1); - let bottom_right_pixel = image.get_pixel(40, 80); + let (x_dim, y_dim) = image.dimensions(); + let top_left_pixel = image.get_pixel(0, 0); + let bottom_right_pixel = image.get_pixel(x_dim - 1, y_dim - 1); // The shading is deep shade with is ok for the plant. assert_eq!([68, 186, 0, 117], top_left_pixel.0); // The plant can't grow in sun. @@ -336,14 +318,11 @@ async fn test_heatmap_with_shadings_and_light_requirement_succeeds() { let result = &result.bytes().collect::, _>>().unwrap(); let image = load_from_memory_with_format(result.as_slice(), image::ImageFormat::Png).unwrap(); let image = image.as_rgba8().unwrap(); - assert_eq!( - ((500 / GRANULARITY) as u32, (1000 / GRANULARITY) as u32), - image.dimensions() - ); // (0,0) is be top left. - let top_left_pixel = image.get_pixel(1, 1); - let bottom_right_pixel = image.get_pixel(40, 80); + let (x_dim, y_dim) = image.dimensions(); + let top_left_pixel = image.get_pixel(0, 0); + let bottom_right_pixel = image.get_pixel(x_dim - 1, y_dim - 1); // The plant can't grow in deep shade. assert_eq!([255, 0, 0, 255], top_left_pixel.0); // The plant can grow in sun. @@ -376,8 +355,8 @@ async fn test_heatmap_with_plantings_succeeds() { id: Uuid::new_v4(), layer_id: -1, plant_id: -1, - x: 15, - y: 15, + x: 0, + y: 0, ..Default::default() }]) .execute(conn) @@ -404,20 +383,15 @@ async fn test_heatmap_with_plantings_succeeds() { let result = &result.bytes().collect::, _>>().unwrap(); let image = load_from_memory_with_format(result.as_slice(), image::ImageFormat::Png).unwrap(); let image = image.as_rgba8().unwrap(); - assert_eq!( - ((500 / GRANULARITY) as u32, (1000 / GRANULARITY) as u32), - image.dimensions() - ); - // (0,0) is be top left. - let on_planting = image.get_pixel(1, 1); - let close_to_planting = image.get_pixel(2, 2); - let a_bit_away_from_planting = image.get_pixel(10, 10); - let far_away_from_planting = image.get_pixel(40, 80); + let (x_dim, y_dim) = image.dimensions(); + let on_planting = image.get_pixel(0, 0); + let close_to_planting = image.get_pixel(1, 1); + let far_away_from_planting = image.get_pixel(x_dim - 1, y_dim - 1); // The planting influences the map. - assert_eq!([96, 158, 0, 62], on_planting.0); - assert_eq!([98, 156, 0, 57], close_to_planting.0); - assert_eq!([123, 131, 0, 8], a_bit_away_from_planting.0); + assert!(on_planting.0[0] < close_to_planting.0[0]); + assert!(on_planting.0[1] > close_to_planting.0[1]); + assert!(on_planting.0[3] > close_to_planting.0[3]); // There is no influence on locations far away. assert_eq!([127, 127, 0, 0], far_away_from_planting.0); } @@ -448,8 +422,8 @@ async fn test_heatmap_with_deleted_planting_succeeds() { id: Uuid::new_v4(), layer_id: -1, plant_id: -1, - x: 15, - y: 15, + x: 0, + y: 0, remove_date: Some(NaiveDate::from_ymd_opt(2023, 07, 30).unwrap()), ..Default::default() }]) @@ -477,19 +451,15 @@ async fn test_heatmap_with_deleted_planting_succeeds() { let result = &result.bytes().collect::, _>>().unwrap(); let image = load_from_memory_with_format(result.as_slice(), image::ImageFormat::Png).unwrap(); let image = image.as_rgba8().unwrap(); - assert_eq!( - ((500 / GRANULARITY) as u32, (1000 / GRANULARITY) as u32), - image.dimensions() - ); - // (0,0) is be top left. - let on_planting = image.get_pixel(1, 1); - let close_to_planting = image.get_pixel(2, 2); - let a_bit_away_from_planting = image.get_pixel(10, 10); + let (x_dim, y_dim) = image.dimensions(); + let on_planting = image.get_pixel(0, 0); + let close_to_planting = image.get_pixel(1, 1); + let far_away_from_planting = image.get_pixel(x_dim - 1, y_dim - 1); // The planting doesn't influences the map as it is deleted. assert_eq!([127, 127, 0, 0], on_planting.0); assert_eq!([127, 127, 0, 0], close_to_planting.0); - assert_eq!([127, 127, 0, 0], a_bit_away_from_planting.0); + assert_eq!([127, 127, 0, 0], far_away_from_planting.0); let resp = test::TestRequest::get() .uri("/api/maps/-1/layers/plants/heatmap?plant_id=-2&plant_layer_id=-1&shade_layer_id=-2&date=2023-07-21") @@ -506,19 +476,13 @@ async fn test_heatmap_with_deleted_planting_succeeds() { let result = &result.bytes().collect::, _>>().unwrap(); let image = load_from_memory_with_format(result.as_slice(), image::ImageFormat::Png).unwrap(); let image = image.as_rgba8().unwrap(); - assert_eq!( - ((500 / GRANULARITY) as u32, (1000 / GRANULARITY) as u32), - image.dimensions() - ); - // (0,0) is be top left. - let on_planting = image.get_pixel(1, 1); - let close_to_planting = image.get_pixel(2, 2); - let a_bit_away_from_planting = image.get_pixel(10, 10); + let on_planting = image.get_pixel(0, 0); + let close_to_planting = image.get_pixel(1, 1); // The planting influences the map as we set the date back in the query. - assert_eq!([96, 158, 0, 62], on_planting.0); - assert_eq!([98, 156, 0, 57], close_to_planting.0); - assert_eq!([123, 131, 0, 8], a_bit_away_from_planting.0); + assert!(on_planting.0[0] < close_to_planting.0[0]); + assert!(on_planting.0[1] > close_to_planting.0[1]); + assert!(on_planting.0[3] > close_to_planting.0[3]); } #[actix_rt::test] From 458d560b7fa3e83c9c5650fb1821779d3b22348a Mon Sep 17 00:00:00 2001 From: kitzbergerg Date: Tue, 1 Aug 2023 17:10:45 +0200 Subject: [PATCH 039/157] remove debug symbols for release mode --- backend/Cargo.toml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/backend/Cargo.toml b/backend/Cargo.toml index a4d4009ac..467c2b9d7 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -59,7 +59,3 @@ image = { version = "0.24.6", default-features = false, features = ["png"] } [dev-dependencies] jsonwebkey = { version = "0.3.5", features = ["generate", "jwt-convert"] } - -# TODO: remove before merge -[profile.release] -debug = true From bb1a7c805293e554f4cc62d34e2cb26ff5b6ecd2 Mon Sep 17 00:00:00 2001 From: kitzbergerg Date: Tue, 1 Aug 2023 17:17:39 +0200 Subject: [PATCH 040/157] update benchmark doc --- doc/backend/06performance_benchmarks.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/doc/backend/06performance_benchmarks.md b/doc/backend/06performance_benchmarks.md index 6dd286507..f905c40f2 100644 --- a/doc/backend/06performance_benchmarks.md +++ b/doc/backend/06performance_benchmarks.md @@ -21,6 +21,9 @@ This is necessary to ensure that perf can accurately track the function stack. debug = true ``` +The `setup.sh` scripts contain `PERF=/usr/lib/linux-tools/5.4.0-153-generic/perf` to set the location of perf. +Modify the path to point to your `perf` installation. + ## Scripts You can find the scripts in `benchmark/backend/`. @@ -88,7 +91,7 @@ The following is a step by step guide on how to execute the benchmark for the he - Stop the backend. - Press Enter. 3. Wait for the backend to start in release mode. -4. Once its started execute in a second shell: `./benchmarks/backend/heatmap/run_httperf.sh 10000 100` +4. Once its started execute in a second shell: `./benchmarks/backend/heatmap/run_httperf.sh 100 10` 5. Wait for httperf to finish. 6. Press Ctrl+C in the `setup.sh` shell. 7. Wait until `flamegraph.svg` was generated (this might take 20min or longer). If you interrupt this step you have to rerun the benchmark. From 7e9b805604a75d4de5f26a9308977e7b37255ca1 Mon Sep 17 00:00:00 2001 From: Moritz Schalk Date: Thu, 3 Aug 2023 16:59:05 +0200 Subject: [PATCH 041/157] map: start implementing heatmap frontend. --- .../features/map_planning/components/Map.tsx | 5 +++ .../layers/heatmap/HeatMapLayer.tsx | 45 +++++++++++++++++++ .../layers/heatmap/api/getHeatMap.ts | 24 ++++++++++ 3 files changed, 74 insertions(+) create mode 100644 frontend/src/features/map_planning/layers/heatmap/HeatMapLayer.tsx create mode 100644 frontend/src/features/map_planning/layers/heatmap/api/getHeatMap.ts diff --git a/frontend/src/features/map_planning/components/Map.tsx b/frontend/src/features/map_planning/components/Map.tsx index c56e5db84..c6775c670 100644 --- a/frontend/src/features/map_planning/components/Map.tsx +++ b/frontend/src/features/map_planning/components/Map.tsx @@ -22,6 +22,7 @@ import IconButton from '@/components/Button/IconButton'; import CancelConfirmationModal from '@/components/Modals/ExtendedModal'; import { FrontendOnlyLayerType } from '@/features/map_planning/layers/_frontend_only'; import { GridLayer } from '@/features/map_planning/layers/_frontend_only/grid/GridLayer'; +import { HeatMapLayer } from '@/features/map_planning/layers/heatmap/HeatMapLayer'; import { CombinedLayerType } from '@/features/map_planning/store/MapStoreTypes'; import { ReactComponent as GridIcon } from '@/icons/grid-dots.svg'; import { ReactComponent as RedoIcon } from '@/icons/redo.svg'; @@ -185,6 +186,10 @@ export const Map = ({ layers }: MapProps) => { opacity={untrackedState.layers.plants.opacity} listening={getSelectedLayerType() === LayerType.Plants} > + { + const { ...layerProps } = props; + + const mapId = useMapStore((state) => state.untrackedState.mapId); + const layerId = useMapStore((state) => state.trackedState.layers.plants.id); + const selectedPlantId = useMapStore( + (state) => state.untrackedState.layers.plants.selectedPlantForPlanting?.id, + ); + + const [imageResponse, setImageResponse] = useState(); + + useEffect(() => { + async function loadHeatmapImage() { + const response = await getHeatMap(mapId, layerId, selectedPlantId); + setImageResponse(response); + } + + if (!imageResponse) { + loadHeatmapImage(); + } + }); + + const image = useImageFromBlob({ + isLoading: false, + isError: imageResponse === null || imageResponse?.status !== 200, + data: imageResponse?.data, + fallbackImageSource: '', + }); + + return ( + + + + ); +}; diff --git a/frontend/src/features/map_planning/layers/heatmap/api/getHeatMap.ts b/frontend/src/features/map_planning/layers/heatmap/api/getHeatMap.ts new file mode 100644 index 000000000..bdab2f2bd --- /dev/null +++ b/frontend/src/features/map_planning/layers/heatmap/api/getHeatMap.ts @@ -0,0 +1,24 @@ +import { createAPI } from '@/config/axios'; + +/** + * Tells the backend to generate a heatmap. + * + * @param mapId The map for which the heatmap is generated. + * @param layerId Which plant layer should be considered. + * @param plantId The plant that is supposed to be added to the plant layer. + */ +export async function getHeatMap(mapId: number, layerId: number, plantId: number | undefined) { + if (!plantId) { + return null; + } + + const http = createAPI(); + + try { + return await http.get(`api/maps/${mapId}/layers/plants/heatmap`, { + params: { layer_id: layerId, plant_id: plantId }, + }); + } catch (error) { + throw error as Error; + } +} From 6cd40ad9175ff782f62ca29e59728d0920f3d4c9 Mon Sep 17 00:00:00 2001 From: kitzbergerg Date: Thu, 3 Aug 2023 17:24:58 +0200 Subject: [PATCH 042/157] update doc according to suggestions --- .../2023-07-03-165000_heatmap/up.sql | 6 ++-- doc/backend/06performance_benchmarks.md | 29 +++++++++++-------- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/backend/migrations/2023-07-03-165000_heatmap/up.sql b/backend/migrations/2023-07-03-165000_heatmap/up.sql index 0cdd209ad..e84e7ea20 100644 --- a/backend/migrations/2023-07-03-165000_heatmap/up.sql +++ b/backend/migrations/2023-07-03-165000_heatmap/up.sql @@ -89,7 +89,7 @@ BEGIN num_cols := FLOOR((x_max - x_min) / granularity); -- Adjusted for granularity num_rows := FLOOR((y_max - y_min) / granularity); -- Adjusted for granularity - -- Calculate the score for each pixel on the heatmap + -- Calculate the score for each point on the heatmap FOR i IN 0..num_cols-1 LOOP FOR j IN 0..num_rows-1 LOOP -- i and j do not represent coordinates. We need to adjust them to actual coordinates. @@ -128,8 +128,8 @@ RETURNS SCORE AS $$ DECLARE score SCORE; BEGIN - score.preference := 1 / (1 + exp(-input.preference)); -- standard sigmoid so f(0)=0.5 - score.relevance := (2 / (1 + exp(-input.relevance))) - 1; -- modified sigmoid so f(0)=0 + score.preference := 1 / (1 + exp(-input.preference)); -- standard sigmoid, so that f(0)=0.5 + score.relevance := (2 / (1 + exp(-input.relevance))) - 1; -- modified sigmoid, so that f(0)=0 RETURN score; END; $$ LANGUAGE plpgsql; diff --git a/doc/backend/06performance_benchmarks.md b/doc/backend/06performance_benchmarks.md index f905c40f2..f8e151af5 100644 --- a/doc/backend/06performance_benchmarks.md +++ b/doc/backend/06performance_benchmarks.md @@ -5,15 +5,17 @@ The following tools are required to run the benchmarks: - docker -- [nushell](https://github.com/nushell/nushell) -- perf -- [flamegraph](https://github.com/flamegraph-rs/flamegraph) +- [nushell](https://github.com/nushell/nushell) (e.g. via `cargo install nu`) +- perf (package might be called `linux-perf-5.10`) +- [flamegraph](https://github.com/flamegraph-rs/flamegraph) (cargo install flamegraph) - [httperf](https://github.com/httperf/httperf) -- standard linux tools like `grep`, `awk` or `psql` +- `jq` +- `psql` (part of PostgreSQL) +- standard linux tools like `grep`, `awk` etc. ## Setup -Add the following to the end of `Cargo.toml`. +Add the following to the end of `backend/Cargo.toml`. This is necessary to ensure that perf can accurately track the function stack. ```toml @@ -21,8 +23,10 @@ This is necessary to ensure that perf can accurately track the function stack. debug = true ``` -The `setup.sh` scripts contain `PERF=/usr/lib/linux-tools/5.4.0-153-generic/perf` to set the location of perf. -Modify the path to point to your `perf` installation. +The `benchmarks/backend/*/setup.sh` scripts contain `PERF=/usr/lib/linux-tools/5.4.0-153-generic/perf` to set the location of perf. +Depending on your distribution it might or might not be needed to change this. +On Debian no change is needed. +Otherwise modify the path to point to your `perf` installation. ## Scripts @@ -44,7 +48,7 @@ Follow these instructions to ensure the benchmark works correctly. Parameters: -- username: Your PermaplanT username. +- username: Your PermaplanT username for https://auth.permaplant.net. - password: Your PermaplanT password. ### `run_httperf.sh` @@ -61,10 +65,10 @@ Do not press Ctrl+C again, otherwise the generated flamegraph will not include a Parameters: -- username: Your PermaplanT username. +- username: Your PermaplanT username for https://auth.permaplant.net. - password: Your PermaplanT password. -- number_of_requests: The total number of requests httperf will execute. -- request_rate: How many requests will be executed per second. +- number_of_requests: The total number of requests httperf will execute (e.g. 10000). +- request_rate: How many requests will be executed per second (e.g. 100). ### `get_statistics.sh` @@ -94,7 +98,8 @@ The following is a step by step guide on how to execute the benchmark for the he 4. Once its started execute in a second shell: `./benchmarks/backend/heatmap/run_httperf.sh 100 10` 5. Wait for httperf to finish. 6. Press Ctrl+C in the `setup.sh` shell. -7. Wait until `flamegraph.svg` was generated (this might take 20min or longer). If you interrupt this step you have to rerun the benchmark. +7. Wait until `flamegraph.svg` was generated (this might take 20min or longer). + If you interrupt this step you have to rerun the benchmark. 8. Execute: `./benchmarks/backend/heatmap/get_statistics.sh` 9. Collect results: - flamgraph.svg From bf56bbd95824d9839b394bad8d70ddd89721ea0c Mon Sep 17 00:00:00 2001 From: Moritz Schalk Date: Sun, 6 Aug 2023 16:22:15 +0200 Subject: [PATCH 043/157] heatmap: fix broken image. --- .../layers/heatmap/HeatMapLayer.tsx | 35 +++++++++---------- .../layers/heatmap/api/getHeatMap.ts | 13 +++++-- 2 files changed, 27 insertions(+), 21 deletions(-) diff --git a/frontend/src/features/map_planning/layers/heatmap/HeatMapLayer.tsx b/frontend/src/features/map_planning/layers/heatmap/HeatMapLayer.tsx index fa0116aff..eb2929c61 100644 --- a/frontend/src/features/map_planning/layers/heatmap/HeatMapLayer.tsx +++ b/frontend/src/features/map_planning/layers/heatmap/HeatMapLayer.tsx @@ -1,9 +1,8 @@ import { getHeatMap } from '@/features/map_planning/layers/heatmap/api/getHeatMap'; import useMapStore from '@/features/map_planning/store/MapStore'; import { useImageFromBlob } from '@/features/nextcloud_integration/hooks/useImageFromBlob'; -import { AxiosResponse } from 'axios'; +import { useQuery } from '@tanstack/react-query'; import Konva from 'konva'; -import { useEffect, useState } from 'react'; import { Layer, Image } from 'react-konva'; type HeatMapLayerProps = Konva.LayerConfig; @@ -12,34 +11,34 @@ export const HeatMapLayer = (props: HeatMapLayerProps) => { const { ...layerProps } = props; const mapId = useMapStore((state) => state.untrackedState.mapId); - const layerId = useMapStore((state) => state.trackedState.layers.plants.id); + const plantLayerId = useMapStore((state) => state.trackedState.layers.plants.id); + const shadeLayerId = useMapStore((state) => state.trackedState.layers.shade.id); const selectedPlantId = useMapStore( (state) => state.untrackedState.layers.plants.selectedPlantForPlanting?.id, ); - const [imageResponse, setImageResponse] = useState(); - - useEffect(() => { - async function loadHeatmapImage() { - const response = await getHeatMap(mapId, layerId, selectedPlantId); - setImageResponse(response); - } - - if (!imageResponse) { - loadHeatmapImage(); - } + const { isLoading, isError, data } = useQuery({ + queryKey: ['heatmap', selectedPlantId], + queryFn: () => getHeatMap(mapId, plantLayerId, shadeLayerId, selectedPlantId), + cacheTime: 0, + staleTime: 0, + enabled: !!mapId && !!plantLayerId && !!shadeLayerId && !!selectedPlantId, }); const image = useImageFromBlob({ - isLoading: false, - isError: imageResponse === null || imageResponse?.status !== 200, - data: imageResponse?.data, + isLoading, + isError, + data: data?.data, fallbackImageSource: '', }); + if (selectedPlantId === undefined || data === undefined || isLoading || isError) { + return ; + } + return ( - + ); }; diff --git a/frontend/src/features/map_planning/layers/heatmap/api/getHeatMap.ts b/frontend/src/features/map_planning/layers/heatmap/api/getHeatMap.ts index bdab2f2bd..639ce6c2d 100644 --- a/frontend/src/features/map_planning/layers/heatmap/api/getHeatMap.ts +++ b/frontend/src/features/map_planning/layers/heatmap/api/getHeatMap.ts @@ -4,10 +4,16 @@ import { createAPI } from '@/config/axios'; * Tells the backend to generate a heatmap. * * @param mapId The map for which the heatmap is generated. - * @param layerId Which plant layer should be considered. + * @param plantLayerId Which plant layer should be considered? + * @param shadeLayerId Which shade layer should be used? * @param plantId The plant that is supposed to be added to the plant layer. */ -export async function getHeatMap(mapId: number, layerId: number, plantId: number | undefined) { +export async function getHeatMap( + mapId: number, + plantLayerId: number, + shadeLayerId: number, + plantId: number | undefined, +) { if (!plantId) { return null; } @@ -16,7 +22,8 @@ export async function getHeatMap(mapId: number, layerId: number, plantId: number try { return await http.get(`api/maps/${mapId}/layers/plants/heatmap`, { - params: { layer_id: layerId, plant_id: plantId }, + params: { plant_layer_id: plantLayerId, plant_id: plantId, shade_layer_id: shadeLayerId }, + responseType: 'blob', }); } catch (error) { throw error as Error; From 3057e6ca2ab4a018bec680b764795e18f5ad253b Mon Sep 17 00:00:00 2001 From: Moritz Schalk Date: Sun, 6 Aug 2023 18:02:15 +0200 Subject: [PATCH 044/157] heatmap: set scale according to map geometry. --- .../layers/heatmap/HeatMapLayer.tsx | 66 +++++++++++++++++-- 1 file changed, 60 insertions(+), 6 deletions(-) diff --git a/frontend/src/features/map_planning/layers/heatmap/HeatMapLayer.tsx b/frontend/src/features/map_planning/layers/heatmap/HeatMapLayer.tsx index eb2929c61..d40b4ba45 100644 --- a/frontend/src/features/map_planning/layers/heatmap/HeatMapLayer.tsx +++ b/frontend/src/features/map_planning/layers/heatmap/HeatMapLayer.tsx @@ -1,5 +1,6 @@ import { getHeatMap } from '@/features/map_planning/layers/heatmap/api/getHeatMap'; import useMapStore from '@/features/map_planning/store/MapStore'; +import { findMapById } from '@/features/maps/api/findMapById'; import { useImageFromBlob } from '@/features/nextcloud_integration/hooks/useImageFromBlob'; import { useQuery } from '@tanstack/react-query'; import Konva from 'konva'; @@ -7,6 +8,11 @@ import { Layer, Image } from 'react-konva'; type HeatMapLayerProps = Konva.LayerConfig; +type Geomerty = { + rings: Array>; + srid: string; +}; + export const HeatMapLayer = (props: HeatMapLayerProps) => { const { ...layerProps } = props; @@ -17,7 +23,11 @@ export const HeatMapLayer = (props: HeatMapLayerProps) => { (state) => state.untrackedState.layers.plants.selectedPlantForPlanting?.id, ); - const { isLoading, isError, data } = useQuery({ + const { + isLoading: heatmapIsLoading, + isError: heatmapIsError, + data: heatmapData, + } = useQuery({ queryKey: ['heatmap', selectedPlantId], queryFn: () => getHeatMap(mapId, plantLayerId, shadeLayerId, selectedPlantId), cacheTime: 0, @@ -25,20 +35,64 @@ export const HeatMapLayer = (props: HeatMapLayerProps) => { enabled: !!mapId && !!plantLayerId && !!shadeLayerId && !!selectedPlantId, }); + const { + isLoading: mapIsLoading, + isError: mapIsError, + data: mapData, + } = useQuery({ + queryKey: ['map', mapId], + queryFn: () => findMapById(mapId), + cacheTime: Infinity, + enabled: !!mapId, + }); + const image = useImageFromBlob({ - isLoading, - isError, - data: data?.data, + isLoading: heatmapIsLoading, + isError: heatmapIsError, + data: heatmapData?.data, fallbackImageSource: '', }); - if (selectedPlantId === undefined || data === undefined || isLoading || isError) { + if ( + selectedPlantId === undefined || + heatmapData === undefined || + heatmapIsLoading || + heatmapIsError || + mapIsLoading || + mapIsError || + mapData === undefined + ) { return ; } + // calculate map bounds + // we only need the first edge ring + const geometry = (mapData.geometry as Geomerty).rings[0]; + let minX = geometry[0].x; + let maxX = geometry[0].x; + let minY = geometry[0].y; + let maxY = geometry[0].y; + + for (const point of geometry) { + minX = Math.min(point.x, minX); + maxX = Math.max(point.x, maxX); + minY = Math.min(point.y, minY); + maxY = Math.max(point.y, maxY); + } + + const width = Math.abs(maxX - minX); + const height = Math.abs(maxY - minY); + return ( - + ); }; From 2467660c104fac15d2cec22a9082d04c4c58056c Mon Sep 17 00:00:00 2001 From: Moritz Schalk Date: Mon, 7 Aug 2023 13:30:16 +0200 Subject: [PATCH 045/157] heatmap: remove unused image property. --- .../map_planning/layers/heatmap/HeatMapLayer.tsx | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/frontend/src/features/map_planning/layers/heatmap/HeatMapLayer.tsx b/frontend/src/features/map_planning/layers/heatmap/HeatMapLayer.tsx index d40b4ba45..5f6b44fbb 100644 --- a/frontend/src/features/map_planning/layers/heatmap/HeatMapLayer.tsx +++ b/frontend/src/features/map_planning/layers/heatmap/HeatMapLayer.tsx @@ -85,14 +85,7 @@ export const HeatMapLayer = (props: HeatMapLayerProps) => { return ( - + ); }; From 23faa989dbd96c8390c81ffe2ca343c42b2074c6 Mon Sep 17 00:00:00 2001 From: Moritz Schalk Date: Mon, 7 Aug 2023 13:55:48 +0200 Subject: [PATCH 046/157] heat map: refactor component. --- .../layers/heatmap/HeatMapLayer.tsx | 125 +++++++++++++----- 1 file changed, 91 insertions(+), 34 deletions(-) diff --git a/frontend/src/features/map_planning/layers/heatmap/HeatMapLayer.tsx b/frontend/src/features/map_planning/layers/heatmap/HeatMapLayer.tsx index 5f6b44fbb..74ac37fd8 100644 --- a/frontend/src/features/map_planning/layers/heatmap/HeatMapLayer.tsx +++ b/frontend/src/features/map_planning/layers/heatmap/HeatMapLayer.tsx @@ -8,24 +8,69 @@ import { Layer, Image } from 'react-konva'; type HeatMapLayerProps = Konva.LayerConfig; -type Geomerty = { +/** + * Represents an array of closed edge loops. + */ +export type Geometry = { rings: Array>; srid: string; }; -export const HeatMapLayer = (props: HeatMapLayerProps) => { - const { ...layerProps } = props; +/** + * Contains additional geometry properties that need to be derived in the frontend. + */ +export type GeometryStats = { + minX: number; + minY: number; + maxX: number; + maxY: number; + width: number; + height: number; +}; - const mapId = useMapStore((state) => state.untrackedState.mapId); - const plantLayerId = useMapStore((state) => state.trackedState.layers.plants.id); - const shadeLayerId = useMapStore((state) => state.trackedState.layers.shade.id); - const selectedPlantId = useMapStore( - (state) => state.untrackedState.layers.plants.selectedPlantForPlanting?.id, - ); +/** + * Derive GeometryStats from a Geometry object. + * + * @param geometry The object for which GeometryStats should be generated. + */ +export function calculateGeometryStats(geometry: Geometry): GeometryStats { + const firstEdgeRing = geometry.rings[0]; + let minX = firstEdgeRing[0].x; + let maxX = firstEdgeRing[0].x; + let minY = firstEdgeRing[0].y; + let maxY = firstEdgeRing[0].y; + + for (const point of firstEdgeRing) { + minX = Math.min(point.x, minX); + maxX = Math.max(point.x, maxX); + minY = Math.min(point.y, minY); + maxY = Math.max(point.y, maxY); + } + const width = Math.abs(maxX - minX); + const height = Math.abs(maxY - minY); + + return { + minX, + minY, + maxX, + maxY, + width, + height, + }; +} + +export function useHeatmap( + mapId: number, + plantLayerId: number, + shadeLayerId: number, + selectedPlantId: number | undefined, +) { + // cacheTime and staleTime are zero to force an image reload if any parameter changes. + // Caching is not worth it in this case because the heatmap is no longer up to date if the user modified the plant layer. const { - isLoading: heatmapIsLoading, - isError: heatmapIsError, + isLoading, + isError, data: heatmapData, } = useQuery({ queryKey: ['heatmap', selectedPlantId], @@ -35,6 +80,30 @@ export const HeatMapLayer = (props: HeatMapLayerProps) => { enabled: !!mapId && !!plantLayerId && !!shadeLayerId && !!selectedPlantId, }); + const image = useImageFromBlob({ + isLoading, + isError, + data: heatmapData?.data, + fallbackImageSource: '', + }); + + return { + isLoading, + isError, + image, + }; +} + +export const HeatMapLayer = (props: HeatMapLayerProps) => { + const { ...layerProps } = props; + + const mapId = useMapStore((state) => state.untrackedState.mapId); + const plantLayerId = useMapStore((state) => state.trackedState.layers.plants.id); + const shadeLayerId = useMapStore((state) => state.trackedState.layers.shade.id); + const selectedPlantId = useMapStore( + (state) => state.untrackedState.layers.plants.selectedPlantForPlanting?.id, + ); + const { isLoading: mapIsLoading, isError: mapIsError, @@ -46,16 +115,14 @@ export const HeatMapLayer = (props: HeatMapLayerProps) => { enabled: !!mapId, }); - const image = useImageFromBlob({ + const { isLoading: heatmapIsLoading, isError: heatmapIsError, - data: heatmapData?.data, - fallbackImageSource: '', - }); + image, + } = useHeatmap(mapId, plantLayerId, shadeLayerId, selectedPlantId); if ( selectedPlantId === undefined || - heatmapData === undefined || heatmapIsLoading || heatmapIsError || mapIsLoading || @@ -65,27 +132,17 @@ export const HeatMapLayer = (props: HeatMapLayerProps) => { return ; } - // calculate map bounds - // we only need the first edge ring - const geometry = (mapData.geometry as Geomerty).rings[0]; - let minX = geometry[0].x; - let maxX = geometry[0].x; - let minY = geometry[0].y; - let maxY = geometry[0].y; - - for (const point of geometry) { - minX = Math.min(point.x, minX); - maxX = Math.max(point.x, maxX); - minY = Math.min(point.y, minY); - maxY = Math.max(point.y, maxY); - } - - const width = Math.abs(maxX - minX); - const height = Math.abs(maxY - minY); + const geometryStats = calculateGeometryStats(mapData.geometry as Geometry); return ( - + ); }; From dd55dcd89d48853bb617cae5646060179b383748 Mon Sep 17 00:00:00 2001 From: Moritz Schalk Date: Mon, 7 Aug 2023 14:42:09 +0200 Subject: [PATCH 047/157] heat map: refactor into different files & add tests. --- .../layers/heatmap/HeatMapLayer.tsx | 93 +------------------ .../layers/heatmap/hooks/useHeatmap.ts | 37 ++++++++ .../layers/heatmap/util/geometry.test.ts | 32 +++++++ .../layers/heatmap/util/geometry.ts | 52 +++++++++++ 4 files changed, 126 insertions(+), 88 deletions(-) create mode 100644 frontend/src/features/map_planning/layers/heatmap/hooks/useHeatmap.ts create mode 100644 frontend/src/features/map_planning/layers/heatmap/util/geometry.test.ts create mode 100644 frontend/src/features/map_planning/layers/heatmap/util/geometry.ts diff --git a/frontend/src/features/map_planning/layers/heatmap/HeatMapLayer.tsx b/frontend/src/features/map_planning/layers/heatmap/HeatMapLayer.tsx index 74ac37fd8..74d1d377a 100644 --- a/frontend/src/features/map_planning/layers/heatmap/HeatMapLayer.tsx +++ b/frontend/src/features/map_planning/layers/heatmap/HeatMapLayer.tsx @@ -1,99 +1,16 @@ -import { getHeatMap } from '@/features/map_planning/layers/heatmap/api/getHeatMap'; +import { useHeatmap } from '@/features/map_planning/layers/heatmap/hooks/useHeatmap'; +import { + calculateGeometryStats, + Geometry, +} from '@/features/map_planning/layers/heatmap/util/geometry'; import useMapStore from '@/features/map_planning/store/MapStore'; import { findMapById } from '@/features/maps/api/findMapById'; -import { useImageFromBlob } from '@/features/nextcloud_integration/hooks/useImageFromBlob'; import { useQuery } from '@tanstack/react-query'; import Konva from 'konva'; import { Layer, Image } from 'react-konva'; type HeatMapLayerProps = Konva.LayerConfig; -/** - * Represents an array of closed edge loops. - */ -export type Geometry = { - rings: Array>; - srid: string; -}; - -/** - * Contains additional geometry properties that need to be derived in the frontend. - */ -export type GeometryStats = { - minX: number; - minY: number; - maxX: number; - maxY: number; - width: number; - height: number; -}; - -/** - * Derive GeometryStats from a Geometry object. - * - * @param geometry The object for which GeometryStats should be generated. - */ -export function calculateGeometryStats(geometry: Geometry): GeometryStats { - const firstEdgeRing = geometry.rings[0]; - let minX = firstEdgeRing[0].x; - let maxX = firstEdgeRing[0].x; - let minY = firstEdgeRing[0].y; - let maxY = firstEdgeRing[0].y; - - for (const point of firstEdgeRing) { - minX = Math.min(point.x, minX); - maxX = Math.max(point.x, maxX); - minY = Math.min(point.y, minY); - maxY = Math.max(point.y, maxY); - } - - const width = Math.abs(maxX - minX); - const height = Math.abs(maxY - minY); - - return { - minX, - minY, - maxX, - maxY, - width, - height, - }; -} - -export function useHeatmap( - mapId: number, - plantLayerId: number, - shadeLayerId: number, - selectedPlantId: number | undefined, -) { - // cacheTime and staleTime are zero to force an image reload if any parameter changes. - // Caching is not worth it in this case because the heatmap is no longer up to date if the user modified the plant layer. - const { - isLoading, - isError, - data: heatmapData, - } = useQuery({ - queryKey: ['heatmap', selectedPlantId], - queryFn: () => getHeatMap(mapId, plantLayerId, shadeLayerId, selectedPlantId), - cacheTime: 0, - staleTime: 0, - enabled: !!mapId && !!plantLayerId && !!shadeLayerId && !!selectedPlantId, - }); - - const image = useImageFromBlob({ - isLoading, - isError, - data: heatmapData?.data, - fallbackImageSource: '', - }); - - return { - isLoading, - isError, - image, - }; -} - export const HeatMapLayer = (props: HeatMapLayerProps) => { const { ...layerProps } = props; diff --git a/frontend/src/features/map_planning/layers/heatmap/hooks/useHeatmap.ts b/frontend/src/features/map_planning/layers/heatmap/hooks/useHeatmap.ts new file mode 100644 index 000000000..299037f5f --- /dev/null +++ b/frontend/src/features/map_planning/layers/heatmap/hooks/useHeatmap.ts @@ -0,0 +1,37 @@ +import { getHeatMap } from '@/features/map_planning/layers/heatmap/api/getHeatMap'; +import { useImageFromBlob } from '@/features/nextcloud_integration/hooks/useImageFromBlob'; +import { useQuery } from '@tanstack/react-query'; + +export function useHeatmap( + mapId: number, + plantLayerId: number, + shadeLayerId: number, + selectedPlantId: number | undefined, +) { + // cacheTime and staleTime are zero to force an image reload if any parameter changes. + // Caching is not worth it in this case because the heatmap is no longer up to date if the user modified the plant layer. + const { + isLoading, + isError, + data: heatmapData, + } = useQuery({ + queryKey: ['heatmap', selectedPlantId], + queryFn: () => getHeatMap(mapId, plantLayerId, shadeLayerId, selectedPlantId), + cacheTime: 0, + staleTime: 0, + enabled: !!mapId && !!plantLayerId && !!shadeLayerId && !!selectedPlantId, + }); + + const image = useImageFromBlob({ + isLoading, + isError, + data: heatmapData?.data, + fallbackImageSource: '', + }); + + return { + isLoading, + isError, + image, + }; +} diff --git a/frontend/src/features/map_planning/layers/heatmap/util/geometry.test.ts b/frontend/src/features/map_planning/layers/heatmap/util/geometry.test.ts new file mode 100644 index 000000000..539a7ff90 --- /dev/null +++ b/frontend/src/features/map_planning/layers/heatmap/util/geometry.test.ts @@ -0,0 +1,32 @@ +import { + calculateGeometryStats, + Geometry, +} from '@/features/map_planning/layers/heatmap/util/geometry'; + +describe('Geometry', () => { + it('calculates stats from geometry', () => { + const geometry = { + srid: '1234', + rings: [ + [ + { x: 0, y: 0 }, + { x: 1, y: 0 }, + { x: 1, y: 1 }, + { x: 0, y: 1 }, + ], + [ + { x: 7, y: 8 }, + { x: 8, y: 10 }, + ], + ], + }; + + const geometryStats = calculateGeometryStats(geometry as Geometry); + expect(geometryStats.width).toEqual(1); + expect(geometryStats.height).toEqual(1); + expect(geometryStats.maxX).toEqual(1); + expect(geometryStats.maxY).toEqual(1); + expect(geometryStats.minX).toEqual(0); + expect(geometryStats.minY).toEqual(0); + }); +}); diff --git a/frontend/src/features/map_planning/layers/heatmap/util/geometry.ts b/frontend/src/features/map_planning/layers/heatmap/util/geometry.ts new file mode 100644 index 000000000..5d9762978 --- /dev/null +++ b/frontend/src/features/map_planning/layers/heatmap/util/geometry.ts @@ -0,0 +1,52 @@ +/** + * Represents an array of closed edge loops. + */ +export type Geometry = { + rings: Array>; + srid: string; +}; + +/** + * Contains additional geometry properties that need to be derived in the frontend. + */ +export type GeometryStats = { + minX: number; + minY: number; + maxX: number; + maxY: number; + width: number; + height: number; +}; + +/** + * Derive GeometryStats from a Geometry object. + * CAUTION: for simplicity reasons only the first edge loop will be considered. + * + * @param geometry The object for which GeometryStats should be generated. + */ +export function calculateGeometryStats(geometry: Geometry): GeometryStats { + const firstEdgeRing = geometry.rings[0]; + let minX = firstEdgeRing[0].x; + let maxX = firstEdgeRing[0].x; + let minY = firstEdgeRing[0].y; + let maxY = firstEdgeRing[0].y; + + for (const point of firstEdgeRing) { + minX = Math.min(point.x, minX); + maxX = Math.max(point.x, maxX); + minY = Math.min(point.y, minY); + maxY = Math.max(point.y, maxY); + } + + const width = Math.abs(maxX - minX); + const height = Math.abs(maxY - minY); + + return { + minX, + minY, + maxX, + maxY, + width, + height, + }; +} From 6d56edd2ff5508aa221664cc4ad024b4581ca035 Mon Sep 17 00:00:00 2001 From: Moritz Schalk Date: Mon, 7 Aug 2023 17:40:27 +0200 Subject: [PATCH 048/157] chore: add changelog. --- doc/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/CHANGELOG.md b/doc/CHANGELOG.md index 830abf66d..69807356f 100644 --- a/doc/CHANGELOG.md +++ b/doc/CHANGELOG.md @@ -37,7 +37,7 @@ Syntax: `- short text describing the change _(Your Name)_` - _()_ - _()_ - _()_ -- _()_ +- Display heatmap in the frontend _(Moritz)_ - _()_ - _()_ - _()_ From 32e5e98c60de6545407cbc92eca4633d11271ae5 Mon Sep 17 00:00:00 2001 From: Moritz Schalk Date: Tue, 8 Aug 2023 20:18:19 +0200 Subject: [PATCH 049/157] heat map: make non relevent areas transparent. --- backend/src/service/plant_layer.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/backend/src/service/plant_layer.rs b/backend/src/service/plant_layer.rs index e842f8fcb..f7a0c2870 100644 --- a/backend/src/service/plant_layer.rs +++ b/backend/src/service/plant_layer.rs @@ -59,11 +59,17 @@ fn matrix_to_image(matrix: &Vec>) -> Result, ServiceErro for (x, y, pixel) in imgbuf.enumerate_pixels_mut() { let (preference, relevance) = matrix[y as usize][x as usize]; - // The closer data is to 1 the green it gets. + // The closer data is to 1 the greener it gets. let red = preference.mul_add(-255.0, 255.0); let green = preference * 255.0; let blue = 0.0_f32; - let alpha = relevance * 255.0; + // For some reason every relevance value returned by the database is between + // (about) 0.5 and 1 while it should be between 0 and 1. + // + // Unfortunately I could not figure out why this is the case and therefore just + // rescaled the relevance value accordingly. + // - Moritz (badnames) + let alpha = (relevance - 0.5) * 512.0; *pixel = Rgba([red as u8, green as u8, blue as u8, alpha as u8]); } From e575763bc8a03f9ca68eae92ae2c94a55fe2c674 Mon Sep 17 00:00:00 2001 From: Moritz Schalk Date: Tue, 8 Aug 2023 23:49:21 +0200 Subject: [PATCH 050/157] migrations: add shade layers for old maps --- .pre-commit-config.yaml | 2 +- .../down.sql | 1 + .../up.sql | 17 +++++++++++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 backend/migrations/2023-08-08-200334_add_missing_shade_layers/down.sql create mode 100644 backend/migrations/2023-08-08-200334_add_missing_shade_layers/up.sql diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 27c06cfb3..cd2acf005 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -90,7 +90,7 @@ repos: rev: 23.7.0 hooks: - id: black - language_version: python3.9.2 + language_version: python3 - repo: https://github.com/PyCQA/flake8 rev: 6.0.0 hooks: diff --git a/backend/migrations/2023-08-08-200334_add_missing_shade_layers/down.sql b/backend/migrations/2023-08-08-200334_add_missing_shade_layers/down.sql new file mode 100644 index 000000000..cce261b49 --- /dev/null +++ b/backend/migrations/2023-08-08-200334_add_missing_shade_layers/down.sql @@ -0,0 +1 @@ +-- This migration can not be undone! diff --git a/backend/migrations/2023-08-08-200334_add_missing_shade_layers/up.sql b/backend/migrations/2023-08-08-200334_add_missing_shade_layers/up.sql new file mode 100644 index 000000000..51e34c770 --- /dev/null +++ b/backend/migrations/2023-08-08-200334_add_missing_shade_layers/up.sql @@ -0,0 +1,17 @@ +INSERT INTO layers (map_id, type, name, is_alternative) +SELECT + maps_without_shade_layer.id AS map_id, + 'shade' AS type, -- noqa: RF04 + 'Shade Layer' AS name, -- noqa: RF04 + false AS is_alternative +FROM ( + SELECT maps.id AS id + FROM maps + + EXCEPT + + SELECT maps.id + FROM maps + LEFT JOIN layers ON layers.map_id = maps.id + WHERE layers.type = 'shade' +) AS maps_without_shade_layer; From a22e9551e25eb6340fe5a9cb81eb2a900d2f711d Mon Sep 17 00:00:00 2001 From: Moritz Schalk Date: Wed, 9 Aug 2023 12:09:57 +0200 Subject: [PATCH 051/157] migration: enable rollback of new shade layers. --- .../2023-08-08-200334_add_missing_shade_layers/down.sql | 4 +++- .../2023-08-08-200334_add_missing_shade_layers/up.sql | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/backend/migrations/2023-08-08-200334_add_missing_shade_layers/down.sql b/backend/migrations/2023-08-08-200334_add_missing_shade_layers/down.sql index cce261b49..fac97de66 100644 --- a/backend/migrations/2023-08-08-200334_add_missing_shade_layers/down.sql +++ b/backend/migrations/2023-08-08-200334_add_missing_shade_layers/down.sql @@ -1 +1,3 @@ --- This migration can not be undone! +DELETE +FROM layers +WHERE layers.name = 'Shade Layer '; \ No newline at end of file diff --git a/backend/migrations/2023-08-08-200334_add_missing_shade_layers/up.sql b/backend/migrations/2023-08-08-200334_add_missing_shade_layers/up.sql index 51e34c770..0243ff873 100644 --- a/backend/migrations/2023-08-08-200334_add_missing_shade_layers/up.sql +++ b/backend/migrations/2023-08-08-200334_add_missing_shade_layers/up.sql @@ -2,7 +2,9 @@ INSERT INTO layers (map_id, type, name, is_alternative) SELECT maps_without_shade_layer.id AS map_id, 'shade' AS type, -- noqa: RF04 - 'Shade Layer' AS name, -- noqa: RF04 + -- Use and extra space to identify shade layers that were added using this migration script + -- so that the changes can be undone if necessary. + 'Shade Layer ' AS name, -- noqa: RF04 false AS is_alternative FROM ( SELECT maps.id AS id From d80614b5472057a2fa9dabdf3c9a02640ebb549f Mon Sep 17 00:00:00 2001 From: Moritz Schalk Date: Wed, 9 Aug 2023 12:16:57 +0200 Subject: [PATCH 052/157] migration: add new line at end of file. --- .../2023-08-08-200334_add_missing_shade_layers/down.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/migrations/2023-08-08-200334_add_missing_shade_layers/down.sql b/backend/migrations/2023-08-08-200334_add_missing_shade_layers/down.sql index fac97de66..db3cb5b21 100644 --- a/backend/migrations/2023-08-08-200334_add_missing_shade_layers/down.sql +++ b/backend/migrations/2023-08-08-200334_add_missing_shade_layers/down.sql @@ -1,3 +1,3 @@ DELETE FROM layers -WHERE layers.name = 'Shade Layer '; \ No newline at end of file +WHERE layers.name = 'Shade Layer '; From 7efd82f97c753fef8408083e4bd782db3972c1f0 Mon Sep 17 00:00:00 2001 From: Moritz Schalk Date: Wed, 9 Aug 2023 14:18:09 +0200 Subject: [PATCH 053/157] test: fix failing tests due to modified heat map endpoint. --- backend/src/test/plant_layer_heatmap.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/backend/src/test/plant_layer_heatmap.rs b/backend/src/test/plant_layer_heatmap.rs index 754632e64..a0cf3b627 100644 --- a/backend/src/test/plant_layer_heatmap.rs +++ b/backend/src/test/plant_layer_heatmap.rs @@ -181,10 +181,10 @@ async fn test_heatmap_with_missing_corner_succeeds() { let top_right_pixel = image.get_pixel(x_dim - 1, 0); let bottom_left_pixel = image.get_pixel(0, y_dim - 1); let bottom_right_pixel = image.get_pixel(x_dim - 1, y_dim - 1); - assert_eq!([68, 186, 0, 117], top_left_pixel.0); - assert_eq!([68, 186, 0, 117], top_right_pixel.0); + assert_eq!([68, 186, 0, 0], top_left_pixel.0); + assert_eq!([68, 186, 0, 0], top_right_pixel.0); assert_eq!([255, 0, 0, 0], bottom_left_pixel.0); - assert_eq!([68, 186, 0, 117], bottom_right_pixel.0); + assert_eq!([68, 186, 0, 0], bottom_right_pixel.0); } #[actix_rt::test] @@ -229,9 +229,9 @@ async fn test_heatmap_with_shadings_succeeds() { let top_left_pixel = image.get_pixel(0, 0); let bottom_right_pixel = image.get_pixel(x_dim - 1, y_dim - 1); // The shading is the exact opposite of the plants preference, therefore the map will be red. - assert_eq!([186, 68, 0, 117], top_left_pixel.0); + assert_eq!([186, 68, 0, 0], top_left_pixel.0); // Plant like other positions, therefore green. - assert_eq!([68, 186, 0, 117], bottom_right_pixel.0); + assert_eq!([68, 186, 0, 0], bottom_right_pixel.0); } #[actix_rt::test] @@ -299,7 +299,7 @@ async fn test_heatmap_with_shadings_and_light_requirement_succeeds() { let top_left_pixel = image.get_pixel(0, 0); let bottom_right_pixel = image.get_pixel(x_dim - 1, y_dim - 1); // The shading is deep shade with is ok for the plant. - assert_eq!([68, 186, 0, 117], top_left_pixel.0); + assert_eq!([68, 186, 0, 0], top_left_pixel.0); // The plant can't grow in sun. assert_eq!([255, 0, 0, 255], bottom_right_pixel.0); @@ -326,7 +326,7 @@ async fn test_heatmap_with_shadings_and_light_requirement_succeeds() { // The plant can't grow in deep shade. assert_eq!([255, 0, 0, 255], top_left_pixel.0); // The plant can grow in sun. - assert_eq!([127, 127, 0, 117], bottom_right_pixel.0); + assert_eq!([127, 127, 0, 0], bottom_right_pixel.0); } #[actix_rt::test] From 3b2691609f6e022603d663236340516ff5e86516 Mon Sep 17 00:00:00 2001 From: Moritz Schalk Date: Thu, 10 Aug 2023 18:03:41 +0200 Subject: [PATCH 054/157] test: fix test failing after heat map api change. --- backend/src/test/plant_layer_heatmap.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/backend/src/test/plant_layer_heatmap.rs b/backend/src/test/plant_layer_heatmap.rs index a0cf3b627..d42e16c84 100644 --- a/backend/src/test/plant_layer_heatmap.rs +++ b/backend/src/test/plant_layer_heatmap.rs @@ -475,14 +475,14 @@ async fn test_heatmap_with_deleted_planting_succeeds() { let result = test::read_body(resp).await; let result = &result.bytes().collect::, _>>().unwrap(); let image = load_from_memory_with_format(result.as_slice(), image::ImageFormat::Png).unwrap(); - let image = image.as_rgba8().unwrap(); + let image_rgba8 = image.as_rgba8().unwrap(); - let on_planting = image.get_pixel(0, 0); - let close_to_planting = image.get_pixel(1, 1); + let on_planting = image_rgba8.get_pixel(0, 0); + let close_to_planting = image_rgba8.get_pixel(1, 1); // The planting influences the map as we set the date back in the query. - assert!(on_planting.0[0] < close_to_planting.0[0]); - assert!(on_planting.0[1] > close_to_planting.0[1]); - assert!(on_planting.0[3] > close_to_planting.0[3]); + assert!(on_planting.0[0] <= close_to_planting.0[0]); + assert!(on_planting.0[1] >= close_to_planting.0[1]); + assert!(on_planting.0[3] >= close_to_planting.0[3]); } #[actix_rt::test] From 08caa841a1bbe12f1a92eab6b1df00b4037b1a42 Mon Sep 17 00:00:00 2001 From: Moritz Schalk Date: Thu, 10 Aug 2023 18:05:25 +0200 Subject: [PATCH 055/157] test: fix another test failing after heat map api change. --- backend/src/test/plant_layer_heatmap.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/src/test/plant_layer_heatmap.rs b/backend/src/test/plant_layer_heatmap.rs index d42e16c84..57586fd99 100644 --- a/backend/src/test/plant_layer_heatmap.rs +++ b/backend/src/test/plant_layer_heatmap.rs @@ -389,9 +389,9 @@ async fn test_heatmap_with_plantings_succeeds() { let close_to_planting = image.get_pixel(1, 1); let far_away_from_planting = image.get_pixel(x_dim - 1, y_dim - 1); // The planting influences the map. - assert!(on_planting.0[0] < close_to_planting.0[0]); - assert!(on_planting.0[1] > close_to_planting.0[1]); - assert!(on_planting.0[3] > close_to_planting.0[3]); + assert!(on_planting.0[0] <= close_to_planting.0[0]); + assert!(on_planting.0[1] >= close_to_planting.0[1]); + assert!(on_planting.0[3] >= close_to_planting.0[3]); // There is no influence on locations far away. assert_eq!([127, 127, 0, 0], far_away_from_planting.0); } From 43f3410b3af1fe4ad05f153eb4363a56d2299211 Mon Sep 17 00:00:00 2001 From: Moritz Schalk Date: Sun, 13 Aug 2023 10:32:47 +0200 Subject: [PATCH 056/157] sql: include feedback by felix. --- .../migrations/2023-07-03-165000_heatmap/up.sql | 14 +++++--------- .../migrations/2023-07-22-110000_shadings/down.sql | 6 +----- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/backend/migrations/2023-07-03-165000_heatmap/up.sql b/backend/migrations/2023-07-03-165000_heatmap/up.sql index e84e7ea20..682694576 100644 --- a/backend/migrations/2023-07-03-165000_heatmap/up.sql +++ b/backend/migrations/2023-07-03-165000_heatmap/up.sql @@ -74,7 +74,7 @@ BEGIN -- Makes sure the layers exists and fits to the map FOR i IN 1..array_length(p_layer_ids, 1) LOOP IF NOT EXISTS (SELECT 1 FROM layers WHERE id = p_layer_ids[i] AND map_id = p_map_id) THEN - RAISE EXCEPTION 'Layer with id % not found on map', p_layer_ids[i]; + RAISE EXCEPTION 'Layer with id % not found on map with id %', p_layer_ids[i], p_map_id; END IF; END LOOP; -- Makes sure the plant exists @@ -137,7 +137,7 @@ $$ LANGUAGE plpgsql; -- Calculate score for a certain position. -- -- p_map_id ... map id --- p_layer_ids[1] ... plant layer +-- p_layer_ids[1] ... plant layer (only the first array-element is used by the function) -- p_plant_id ... id of the plant for which to consider relations -- date ... date at which to generate the heatmap -- x_pos,y_pos ... coordinates on the map where to calculate the score @@ -151,15 +151,11 @@ CREATE OR REPLACE FUNCTION calculate_score( ) RETURNS SCORE AS $$ DECLARE - score SCORE; plants SCORE; BEGIN plants := calculate_score_from_relations(p_layer_ids[1], p_plant_id, date, x_pos, y_pos); - score.preference := plants.preference; - score.relevance := plants.relevance; - - RETURN score; + RETURN plants; END; $$ LANGUAGE plpgsql; @@ -198,11 +194,11 @@ BEGIN -- update score based on relation IF plant_relation.relation = 'companion' THEN score.preference := score.preference + 0.5 * weight; - score.relevance := score.relevance + 0.5 * weight; ELSE score.preference := score.preference - 0.5 * weight; - score.relevance := score.relevance + 0.5 * weight; END IF; + + score.relevance := score.relevance + 0.5 * weight; END LOOP; RETURN score; diff --git a/backend/migrations/2023-07-22-110000_shadings/down.sql b/backend/migrations/2023-07-22-110000_shadings/down.sql index d92621c59..7dbf48a1d 100644 --- a/backend/migrations/2023-07-22-110000_shadings/down.sql +++ b/backend/migrations/2023-07-22-110000_shadings/down.sql @@ -11,15 +11,11 @@ CREATE OR REPLACE FUNCTION calculate_score( ) RETURNS SCORE AS $$ DECLARE - score SCORE; plants SCORE; BEGIN plants := calculate_score_from_relations(p_layer_ids[1], p_plant_id, date, x_pos, y_pos); - score.preference := plants.preference; - score.relevance := plants.relevance; - - RETURN score; + RETURN plants; END; $$ LANGUAGE plpgsql; From af76c3a7de6c88cd6256d3fde54a30b786e949f7 Mon Sep 17 00:00:00 2001 From: Moritz Schalk Date: Mon, 14 Aug 2023 14:33:58 +0200 Subject: [PATCH 057/157] doc: add change requests by temmey. --- doc/backend/06performance_benchmarks.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/backend/06performance_benchmarks.md b/doc/backend/06performance_benchmarks.md index f8e151af5..f0bf04285 100644 --- a/doc/backend/06performance_benchmarks.md +++ b/doc/backend/06performance_benchmarks.md @@ -19,7 +19,7 @@ Add the following to the end of `backend/Cargo.toml`. This is necessary to ensure that perf can accurately track the function stack. ```toml -[profile.release] +[profile.bench] debug = true ``` @@ -31,16 +31,16 @@ Otherwise modify the path to point to your `perf` installation. ## Scripts You can find the scripts in `benchmark/backend/`. -They are supposed to be run from the repositories root folder. +They are supposed to be run from the repository's root folder. The subfolders contain scripts to run performance benchmarks in specific endpoints. ### `setup.sh` -Execute like the following: +Execute it as follows: `./benchmarks/backend//setup.sh `. -It will start the database and backend. +The database and backend have to be started manually. Depending on the endpoint it might execute `insert_data.sh` scripts to insert additional data into the database. The script might output instructions while executing. @@ -84,7 +84,7 @@ The following is a step by step guide on how to execute the benchmark for the he 1. Insert ```toml - [profile.release] + [profile.bench] debug = true ``` into `Cargo.toml`. From 1b10b3363a3943e233eb74d4a0e96694e2aac68b Mon Sep 17 00:00:00 2001 From: Moritz Schalk Date: Mon, 14 Aug 2023 14:37:52 +0200 Subject: [PATCH 058/157] doc: add db changes by temmey. --- backend/migrations/2023-07-03-165000_heatmap/up.sql | 5 +++-- backend/migrations/2023-07-22-110000_shadings/up.sql | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/backend/migrations/2023-07-03-165000_heatmap/up.sql b/backend/migrations/2023-07-03-165000_heatmap/up.sql index 682694576..c0a65f549 100644 --- a/backend/migrations/2023-07-03-165000_heatmap/up.sql +++ b/backend/migrations/2023-07-03-165000_heatmap/up.sql @@ -91,9 +91,10 @@ BEGIN -- Calculate the score for each point on the heatmap FOR i IN 0..num_cols-1 LOOP + -- i and j do not represent coordinates. We need to adjust them to actual coordinates. + x_pos := x_min + (i * granularity) + (granularity / 2); + FOR j IN 0..num_rows-1 LOOP - -- i and j do not represent coordinates. We need to adjust them to actual coordinates. - x_pos := x_min + (i * granularity) + (granularity / 2); y_pos := y_min + (j * granularity) + (granularity / 2); -- Create a point from x_pos and y_pos diff --git a/backend/migrations/2023-07-22-110000_shadings/up.sql b/backend/migrations/2023-07-22-110000_shadings/up.sql index 545ea9a87..83ec0dbe1 100644 --- a/backend/migrations/2023-07-22-110000_shadings/up.sql +++ b/backend/migrations/2023-07-22-110000_shadings/up.sql @@ -4,7 +4,7 @@ CREATE TABLE shadings ( layer_id INTEGER NOT NULL, shade SHADE NOT NULL, geometry GEOMETRY (POLYGON, 4326) NOT NULL, - add_date DATE, + add_date DATE DEFAULT now()::date, remove_date DATE, FOREIGN KEY (layer_id) REFERENCES layers (id) ON DELETE CASCADE ); From 3e18f6f3936fca08c8e821fe126f7d7b6496e1b8 Mon Sep 17 00:00:00 2001 From: Moritz Schalk Date: Mon, 14 Aug 2023 14:56:53 +0200 Subject: [PATCH 059/157] db: reverse changes that might have caused tests to fail. --- backend/migrations/2023-07-22-110000_shadings/up.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/migrations/2023-07-22-110000_shadings/up.sql b/backend/migrations/2023-07-22-110000_shadings/up.sql index 83ec0dbe1..545ea9a87 100644 --- a/backend/migrations/2023-07-22-110000_shadings/up.sql +++ b/backend/migrations/2023-07-22-110000_shadings/up.sql @@ -4,7 +4,7 @@ CREATE TABLE shadings ( layer_id INTEGER NOT NULL, shade SHADE NOT NULL, geometry GEOMETRY (POLYGON, 4326) NOT NULL, - add_date DATE DEFAULT now()::date, + add_date DATE, remove_date DATE, FOREIGN KEY (layer_id) REFERENCES layers (id) ON DELETE CASCADE ); From 642b057e3408ab717540beb6aa2dd6666f82d532 Mon Sep 17 00:00:00 2001 From: Moritz Schalk Date: Sat, 26 Aug 2023 16:57:24 +0200 Subject: [PATCH 060/157] heat map: make undefined check more specific. Co-authored-by: Jannis Adamek --- .../src/features/map_planning/layers/heatmap/api/getHeatMap.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/features/map_planning/layers/heatmap/api/getHeatMap.ts b/frontend/src/features/map_planning/layers/heatmap/api/getHeatMap.ts index 639ce6c2d..021199839 100644 --- a/frontend/src/features/map_planning/layers/heatmap/api/getHeatMap.ts +++ b/frontend/src/features/map_planning/layers/heatmap/api/getHeatMap.ts @@ -14,7 +14,7 @@ export async function getHeatMap( shadeLayerId: number, plantId: number | undefined, ) { - if (!plantId) { + if (plantId === undefined) { return null; } From 0d4b2922965b7f21f1394e36c7351f9af574ba1a Mon Sep 17 00:00:00 2001 From: Moritz Schalk Date: Sat, 26 Aug 2023 16:57:47 +0200 Subject: [PATCH 061/157] migrations: fix typo. Co-authored-by: Jannis Adamek --- .../2023-08-08-200334_add_missing_shade_layers/up.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/migrations/2023-08-08-200334_add_missing_shade_layers/up.sql b/backend/migrations/2023-08-08-200334_add_missing_shade_layers/up.sql index 0243ff873..130935b66 100644 --- a/backend/migrations/2023-08-08-200334_add_missing_shade_layers/up.sql +++ b/backend/migrations/2023-08-08-200334_add_missing_shade_layers/up.sql @@ -2,7 +2,7 @@ INSERT INTO layers (map_id, type, name, is_alternative) SELECT maps_without_shade_layer.id AS map_id, 'shade' AS type, -- noqa: RF04 - -- Use and extra space to identify shade layers that were added using this migration script + -- Use an extra space to identify shade layers that were added using this migration script -- so that the changes can be undone if necessary. 'Shade Layer ' AS name, -- noqa: RF04 false AS is_alternative From 3a3b58a1bf8485cffe9ef7951bde9ae7bb025d65 Mon Sep 17 00:00:00 2001 From: Moritz Schalk Date: Sun, 17 Dec 2023 12:30:45 +0100 Subject: [PATCH 062/157] Merge with current master. --- .devcontainer/Dockerfile | 2 +- .github/CONTRIBUTING.md | 29 +- .github/ISSUE_TEMPLATE/release.yml | 33 + .github/PULL_REQUEST_TEMPLATE.md | 12 +- .github/repo_analysis.ipynb | 194 +++ .gitignore | 4 +- .pre-commit-config.yaml | 9 +- .reuse/dep5 | 5 + Makefile | 322 ++-- README.md | 28 +- backend/.env.sample | 2 +- backend/.sqlfluff | 5 +- backend/Cargo.lock | 76 +- backend/Cargo.toml | 6 +- backend/Makefile | 25 +- backend/deny.toml | 281 ++++ .../2023-07-04-190000_base_layer/up.sql | 10 +- .../2023-07-12-143111_user_data/up.sql | 24 +- .../2023-07-21-085933_gain_blossoms/up.sql | 22 +- .../down.sql | 2 + .../2023-09-02-153556_archivable_seeds/up.sql | 2 + .../2023-09-20-073850_unique_seeds/down.sql | 5 + .../2023-09-20-073850_unique_seeds/up.sql | 11 + .../down.sql | 1 + .../up.sql | 1 + backend/src/config/api_doc.rs | 31 +- backend/src/config/routes.rs | 30 +- backend/src/controller/map.rs | 50 +- backend/src/controller/mod.rs | 1 - .../src/controller/planting_suggestions.rs | 61 - backend/src/controller/plantings.rs | 15 +- backend/src/controller/seed.rs | 59 +- backend/src/error.rs | 11 +- backend/src/model/dto.rs | 59 +- backend/src/model/dto/actions.rs | 195 ++- backend/src/model/dto/plantings.rs | 10 +- backend/src/model/dto/plantings_impl.rs | 50 +- backend/src/model/dto/seed_impl.rs | 24 + .../src/model/dto/update_map_geometry_impl.rs | 12 + backend/src/model/entity.rs | 11 + backend/src/model/entity/map_impl.rs | 21 +- backend/src/model/entity/plantings.rs | 4 + backend/src/model/entity/plantings_impl.rs | 66 +- backend/src/model/entity/plants_impl.rs | 62 +- backend/src/model/entity/seed_impl.rs | 140 +- .../src/model/enum/include_archived_seeds.rs | 19 + backend/src/model/enum/mod.rs | 1 + backend/src/schema.patch | 15 +- backend/src/service/map.rs | 62 +- backend/src/service/plantings.rs | 14 + backend/src/service/plants.rs | 34 +- backend/src/service/seed.rs | 58 +- backend/src/sse/broadcaster.rs | 37 +- backend/src/test/layers.rs | 2 +- backend/src/test/map.rs | 47 +- backend/src/test/mod.rs | 1 - backend/src/test/pagination.rs | 11 + backend/src/test/planting_suggestions.rs | 195 --- backend/src/test/plantings.rs | 2 + backend/src/test/seed.rs | 167 +- book.toml | 8 +- ci/Jenkinsfile | 140 +- ci/Jenkinsfile.release | 18 +- ci/build-scripts/build-schema.sh | 2 +- .../permaplant-rust/Dockerfile | 2 +- ci/scripts/check-changelog.sh | 15 + ci/scripts/check-pre-commit.sh | 13 + doc/CHANGELOG.md | 181 --- doc/architecture/04solution.md | 7 +- doc/architecture/08concepts.md | 5 +- doc/architecture/09decisions.md | 2 +- doc/architecture/12glossary.md | 172 +- doc/authors.md | 21 +- doc/backend/04http_header.md | 4 + doc/backend/05updating_schema_patch.md | 24 - doc/backend/README.md | 2 +- doc/changelog.md | 331 ++++ doc/ci.md | 80 +- doc/contrib/README.md | 71 +- doc/contrib/frontend.md | 95 +- doc/database/hierarchy.md | 66 +- doc/decisions/backend_mail_client.md | 2 +- doc/decisions/backend_orm_crate.md | 70 +- doc/decisions/database_plant_hierarchy.md | 4 +- .../down.sql | 8 - .../down.sql.md | 8 + .../up.sql | 17 - .../up.sql.md | 16 + .../example_queries.sql | 254 --- .../example_queries.sql.md | 254 +++ .../down.sql | 10 - .../2023-03-09-194135_plant_relations/up.sql | 52 - .../up.sql.md | 52 + .../2023-04-04-220813_taxons/down.sql | 7 - .../2023-04-04-220813_taxons/down.sql.md | 7 + .../2023-04-04-220813_taxons/up.sql | 15 - .../2023-04-04-220813_taxons/up.sql.md | 15 + .../down.sql | 2 - .../up.sql | 8 - .../up.sql.md | 8 + .../2023-04-07-112305_varieties/down.sql | 1 - .../2023-04-07-112305_varieties/up.sql | 11 - .../example_queries.sql | 280 ---- .../example_queries.sql.md | 280 ++++ .../frontend_architecture_pattern.md | 2 +- doc/decisions/frontend_keyhandling.md | 41 + doc/decisions/frontend_mocking_tool.md | 45 + doc/decisions/frontend_state_management.md | 4 + doc/decisions/frontend_timeline_concept.md | 40 + doc/development_setup.md | 18 +- doc/guidelines/backend.md | 1 + doc/guidelines/e2e.md | 94 ++ doc/guidelines/frontend-api-calls.md | 88 ++ doc/guidelines/frontend-keyhandling.md | 97 ++ doc/guidelines/frontend-locators.md | 5 + doc/guidelines/frontend-ui-usability.md | 182 +++ doc/guidelines/frontend.md | 31 +- doc/guidelines/i18n.md | 101 ++ doc/guidelines/ui.md | 56 - doc/meetings/2022_12_16.md | 46 - doc/meetings/2023_02_17.md | 31 - doc/meetings/2023_02_24.md | 42 - doc/meetings/2023_03_02.md | 89 -- doc/meetings/2023_03_15_gamification.md | 2 +- doc/meetings/2023_05_26.md | 106 -- doc/meetings/2023_07_18.md | 4 +- doc/meetings/2023_07_25.md | 2 +- doc/meetings/2023_07_31.md | 2 +- doc/meetings/2023_08_08.md | 3 +- doc/meetings/2023_08_15.md | 158 ++ doc/meetings/2023_08_22.md | 125 ++ doc/meetings/2023_08_28.md | 125 ++ doc/meetings/2023_09_04.md | 126 ++ doc/meetings/2023_09_11.md | 113 ++ doc/meetings/2023_09_18.md | 126 ++ doc/meetings/2023_09_25.md | 73 + doc/meetings/2023_10_02.md | 74 + doc/meetings/2023_10_09.md | 111 ++ doc/meetings/2023_10_16.md | 71 + doc/meetings/2023_10_23.md | 85 + doc/meetings/2023_10_30.md | 93 ++ doc/meetings/2023_11_06.md | 90 ++ doc/meetings/2023_11_13.md | 117 ++ doc/meetings/2023_11_20.md | 103 ++ doc/meetings/2023_11_27.md | 66 + doc/meetings/2023_12_04.md | 70 + doc/release.md | 6 +- doc/tests/README.md | 72 +- doc/tests/manual.md | 11 - doc/tests/manual/README.md | 64 - doc/tests/manual/protocol.md | 363 ++--- doc/tests/manual/reports/230915.md | 141 ++ doc/tests/manual/reports/231014.md | 110 ++ doc/tests/manual/reports/231111.md | 110 ++ doc/tests/manual/reports/231121.md | 85 + doc/tests/manual/reports/README.md | 9 + doc/tests/manual/reports/template.md | 38 + doc/usecases/README.md | 6 +- .../assigned/add_plant_relationships.md | 37 - doc/usecases/assigned/area_of_plants.md | 32 - doc/usecases/assigned/buy_seeds.md | 12 +- doc/usecases/assigned/calendar.md | 1 - doc/usecases/assigned/contributing_member.md | 32 - doc/usecases/assigned/copy_paste_selection.md | 25 - .../assigned/copy_paste_within_same_map.md | 61 + doc/usecases/assigned/create_layers.md | 22 + doc/usecases/assigned/diversity_score.md | 29 - doc/usecases/assigned/drawing_layer.md | 27 - doc/usecases/assigned/event_notfication.md | 29 - doc/usecases/assigned/experimental_results.md | 26 - doc/usecases/assigned/fertilization_layer.md | 1 + doc/usecases/assigned/geo_map.md | 23 - doc/usecases/assigned/habitats_layer.md | 27 - doc/usecases/assigned/hydrology_layer.md | 2 + doc/usecases/assigned/incomplete_groups.md | 27 - doc/usecases/assigned/infrastructure_layer.md | 34 - doc/usecases/assigned/ingredient_lists.md | 28 - doc/usecases/assigned/label_layer.md | 2 +- doc/usecases/assigned/landing_page.md | 31 - doc/usecases/assigned/landing_page_news.md | 36 - doc/usecases/assigned/landscape_layer.md | 6 +- doc/usecases/assigned/layers_alternatives.md | 36 - doc/usecases/assigned/map_collaboration.md | 5 +- doc/usecases/assigned/map_deletion.md | 2 +- doc/usecases/assigned/map_honors.md | 29 - .../assigned/map_specific_favorite_groups.md | 29 - doc/usecases/assigned/map_statistics.md | 27 - .../assigned/map_timeline_event_view.md | 34 - .../assigned/map_timeline_range_selection.md | 34 - doc/usecases/assigned/matchmaking.md | 27 - doc/usecases/assigned/measuring_distance.md | 4 +- .../assigned/membership_application.md | 58 - .../assigned/new_member_notification.md | 3 +- doc/usecases/assigned/nextcloud_circles.md | 1 - doc/usecases/assigned/offline.md | 39 - doc/usecases/assigned/paths_layer.md | 27 - doc/usecases/assigned/plant_lore.md | 24 - doc/usecases/assigned/rename_layers.md | 26 - .../assigned/review_plant_relationships.md | 29 - doc/usecases/assigned/reward_preview.md | 26 - doc/usecases/assigned/soil_layer.md | 38 - doc/usecases/assigned/suggest_plants.md | 44 - doc/usecases/assigned/terrain_layer.md | 31 - doc/usecases/assigned/todo_layer.md | 33 - doc/usecases/assigned/trees_layer.md | 31 - doc/usecases/assigned/warnings_layer.md | 12 +- doc/usecases/assigned/watering_layer.md | 30 - doc/usecases/assigned/winds_layer.md | 27 - doc/usecases/assigned/zones_layer.md | 1 + doc/usecases/current/base_layer.md | 45 - doc/usecases/current/entry_list_seeds.md | 26 - doc/usecases/current/gain_blossoms.md | 59 - doc/usecases/current/heat_map.md | 18 + doc/usecases/current/map_search.md | 32 - .../current/map_timeline_single_selection.md | 15 +- doc/usecases/current/plants_layer.md | 43 - doc/usecases/current/shade_layer.md | 27 - doc/usecases/done/entry_list_seeds.md | 25 + doc/usecases/done/guided_tour.md | 38 + doc/usecases/done/layers_visibility.md | 7 +- doc/usecases/done/login.md | 9 + doc/usecases/done/multi_select.md | 34 + doc/usecases/done/search_plants.md | 23 +- doc/usecases/done/translation.md | 4 + doc/usecases/done/zoom.md | 1 - .../draft/copy_paste_between_own_maps.md | 38 + .../draft/copy_paste_between_users.md | 38 + doc/usecases/draft/copy_paste_via_icons.md | 44 + doc/usecases/draft/diff.md | 26 + doc/usecases/draft/dimensioning_layer.md | 1 - doc/usecases/draft/map_to_pdf.md | 2 - doc/usecases/draft/map_webcam.md | 1 - doc/usecases/draft/remember_viewing_state.md | 59 + e2e/.env.sample | 5 + e2e/README.md | 251 ++- e2e/clean_db.py | 2 + e2e/conftest.py | 6 + e2e/e2e.sh | 2 +- e2e/features/base_layer.feature | 10 +- e2e/features/layer_visibility.feature | 10 +- e2e/features/map_creation.feature | 18 +- e2e/features/planting.feature | 4 +- e2e/features/planting_select.feature | 9 + e2e/features/search_plants.feature | 4 +- e2e/features/seeds.feature | 27 + e2e/features/timeline.feature | 24 +- e2e/features/undo_redo.feature | 11 +- e2e/install.sh | 1 + e2e/pages/abstract_page.py | 15 +- e2e/pages/home.py | 13 +- e2e/pages/inventory/create.py | 79 + e2e/pages/inventory/edit.py | 68 + e2e/pages/inventory/management.py | 60 + e2e/pages/maps/create.py | 11 +- e2e/pages/maps/edit.py | 2 +- e2e/pages/maps/management.py | 8 +- e2e/pages/maps/planting.py | 165 +- e2e/steps/common_steps.py | 43 +- e2e/steps/test_base_layer.py | 21 +- e2e/steps/test_frequent_steps.py | 0 e2e/steps/test_inventory.py | 125 ++ e2e/steps/test_layer_visibility.py | 12 +- e2e/steps/test_map_creation.py | 6 +- e2e/steps/test_planting.py | 4 +- e2e/steps/test_planting_select.py | 24 + e2e/steps/test_timeline.py | 22 +- e2e/steps/test_undo_redo.py | 18 +- e2e/test-reports/.gitignore | 2 - e2e/uninstall.sh | 1 + frontend/.gitignore | 2 +- frontend/.prettierignore | 2 +- frontend/index.html | 4 +- frontend/jest.config.ts | 1 + frontend/package-lock.json | 1390 ++++++++++++++--- frontend/package.json | 5 +- frontend/public/favicon.svg | 1 - frontend/public/permaplant-logo-2.svg | 32 - frontend/public/permaplant-logo-gray.svg | 72 - frontend/public/permaplant-logo.svg | 286 ---- frontend/public/plant.svg | 1 - frontend/src/App.tsx | 11 +- frontend/src/Providers.tsx | 15 +- frontend/src/__mocks__/styleMock.ts | 3 + frontend/src/assets/globe.svg | 1 - frontend/src/assets/permaplant-logo-2.svg | 32 - frontend/src/assets/permaplant-logo-dark.svg | 1 - frontend/src/assets/permaplant-logo-gray.svg | 72 - frontend/src/assets/permaplant-logo.svg | 286 ---- frontend/src/assets/planning.svg | 1 - frontend/src/assets/plant.svg | 1 - frontend/src/components/Button/ButtonLink.tsx | 14 +- .../components/Button/IconButton.stories.tsx | 2 +- frontend/src/components/Button/IconButton.tsx | 43 +- .../src/components/Button/SimpleButton.tsx | 16 +- .../__snapshots__/ButtonLink.test.tsx.snap | 4 +- .../__snapshots__/SimpleButton.test.tsx.snap | 8 +- .../src/components/Form/MarkdownEditor.tsx | 44 + .../components/Form/PaginatedSelectMenu.tsx | 9 +- .../src/components/Form/SearchInput.test.tsx | 152 +- frontend/src/components/Form/SearchInput.tsx | 108 +- frontend/src/components/Form/SelectMenu.tsx | 11 +- .../Form/SimpleFormInput.stories.tsx | 2 +- .../components/Form/SimpleFormInput.test.tsx | 2 +- .../src/components/Form/SimpleFormInput.tsx | 13 +- .../components/Form/SimpleFormTextArea.tsx | 58 + .../PaginatedSelectMenu.test.tsx.snap | 4 +- .../__snapshots__/SearchInput.test.tsx.snap | 13 +- .../__snapshots__/SelectMenu.test.tsx.snap | 4 +- .../SimpleFormInput.test.tsx.snap | 2 +- .../src/components/Layout/FullPageNav.tsx | 96 ++ frontend/src/components/Layout/Navbar.tsx | 122 +- .../LoadingSpinner/LoadingSpinner.tsx | 2 +- .../src/components/Modals/ExtendedModal.tsx | 2 +- frontend/src/components/Modals/ImageModal.tsx | 2 +- .../src/components/Modals/ModalContainer.tsx | 19 +- .../__snapshots__/SimpleModal.test.tsx.snap | 4 +- .../src/components/Slider/NamedSlider.tsx | 44 +- .../src/config/i18n/de/baseLayerForm.json | 16 +- frontend/src/config/i18n/de/common.json | 1 + frontend/src/config/i18n/de/grid.json | 3 - frontend/src/config/i18n/de/guidedTour.json | 104 +- frontend/src/config/i18n/de/index.ts | 14 +- .../config/i18n/de/plantAndSeedSearch.json | 4 + frontend/src/config/i18n/de/plantings.json | 11 +- frontend/src/config/i18n/de/polygon.json | 3 + frontend/src/config/i18n/de/readOnly.json | 3 + frontend/src/config/i18n/de/routes.json | 2 +- frontend/src/config/i18n/de/seeds.json | 99 +- .../src/config/i18n/de/toolboxTooltips.json | 6 + frontend/src/config/i18n/de/undoRedo.json | 4 +- .../src/config/i18n/en/baseLayerForm.json | 13 +- frontend/src/config/i18n/en/common.json | 1 + frontend/src/config/i18n/en/grid.json | 3 - frontend/src/config/i18n/en/guidedTour.json | 105 +- frontend/src/config/i18n/en/index.ts | 10 +- .../config/i18n/en/plantAndSeedSearch.json | 4 + frontend/src/config/i18n/en/plantings.json | 7 +- frontend/src/config/i18n/en/polygon.json | 3 + frontend/src/config/i18n/en/readOnly.json | 3 + frontend/src/config/i18n/en/routes.json | 2 +- frontend/src/config/i18n/en/seeds.json | 98 +- frontend/src/config/i18n/en/team.json | 2 +- .../src/config/i18n/en/toolboxTooltips.json | 6 + frontend/src/config/i18n/en/undoRedo.json | 4 +- frontend/src/config/keybindings/index.ts | 95 ++ .../src/config/keybindings/keybindings.json | 14 + frontend/src/features/auth/api/getAuthInfo.ts | 2 +- .../features/auth/components/LoginButton.tsx | 10 +- .../src/features/debugging/routes/Debug.tsx | 26 + .../landing_page/components/Features.tsx | 6 +- .../components/LanguageSwitcher.tsx | 6 +- .../landing_page/components/PhotoGallery.tsx | 4 +- .../features/landing_page/components/Team.tsx | 5 + .../map_planning/api/createPlanting.ts | 2 +- .../map_planning/api/createTourStatus.ts | 2 +- .../map_planning/api/deletePlanting.ts | 2 +- .../features/map_planning/api/gainBlossom.ts | 2 +- .../features/map_planning/api/getLayers.ts | 2 +- .../src/features/map_planning/api/getMap.ts | 13 + .../features/map_planning/api/getPlantings.ts | 2 +- .../map_planning/api/getTourStatus.ts | 2 +- .../features/map_planning/api/movePlanting.ts | 2 +- .../map_planning/api/transformPlanting.ts | 2 +- .../map_planning/api/updateAddDatePlanting.ts | 2 +- .../map_planning/api/updateMapGeometry.ts | 13 + .../api/updateRemoveDatePlanting.ts | 2 +- .../map_planning/api/updateTourStatus.ts | 2 +- .../map_planning/components/BaseStage.tsx | 290 +++- .../map_planning/components/EditorMap.tsx | 381 +++++ .../features/map_planning/components/Map.tsx | 260 --- .../components/image/NextcloudKonvaImage.tsx | 2 +- .../image/PublicNextcloudKonvaImage.tsx | 2 +- .../components/timeline/ItemSliderPicker.tsx | 311 ++++ .../components/timeline/Timeline.stories.tsx | 9 +- .../components/timeline/Timeline.tsx | 89 -- .../timeline/TimelineDatePicker.test.tsx | 152 ++ .../timeline/TimelineDatePicker.tsx | 316 ++++ .../components/timeline/styles/timeline.css | 9 + .../components/toolbar/LayerList.tsx | 154 +- .../toolbar/LayerListItem.stories.tsx | 14 + .../components/toolbar/LayerListItem.tsx | 110 ++ .../components/toolbar/Layers.stories.tsx | 14 - .../components/toolbar/Layers.tsx | 56 - .../components/toolbar/Toolbar.tsx | 6 +- .../hooks/useGetTimelineEvents.tsx | 71 + .../hooks/useSelectedLayerVisibility.test.ts | 127 ++ .../hooks/useSelectedLayerVisibility.ts | 21 + .../map_planning/hooks/useTourStatus.tsx | 26 +- .../layers/_frontend_only/grid/GridLayer.tsx | 11 +- .../_frontend_only/grid/groups/Grid.tsx | 12 +- .../_frontend_only/grid/groups/YardStick.tsx | 11 +- .../_frontend_only/grid/util/Constants.ts | 12 - .../map_planning/layers/base/BaseLayer.tsx | 84 +- .../layers/base/BaseMeasurementLayer.tsx | 54 - .../map_planning/layers/base/actions.ts | 46 +- .../layers/base/api/getBaseLayer.ts | 2 +- .../layers/base/api/updateBaseLayer.ts | 2 +- .../components/BaseLayerAttributeEditForm.tsx | 221 +++ .../base/components/BaseLayerRightToolbar.tsx | 227 +-- .../components/DistanceMeasurementModal.tsx | 91 ++ .../base/components/MapGeometryEditor.tsx | 136 ++ .../base/components/MapGeometryToolForm.tsx | 91 ++ .../map_planning/layers/plant/PlantsLayer.tsx | 302 +++- .../map_planning/layers/plant/actions.ts | 44 +- .../layers/plant/api/getRelations.ts | 2 +- .../plant/api/getSeasonalAvailablePlants.ts | 2 +- .../plant/components/ExtendedPlantDisplay.tsx | 2 +- .../plant/components/PlantAndSeedSearch.tsx | 138 ++ .../layers/plant/components/PlantCursor.tsx | 55 + .../layers/plant/components/PlantLabel.tsx | 28 +- .../components/PlantLayerLeftToolbar.tsx | 83 +- .../components/PlantLayerRelationsOverlay.tsx | 7 +- .../components/PlantLayerRightToolbar.tsx | 6 +- .../layers/plant/components/PlantListItem.tsx | 29 +- .../plant/components/PlantSearch.stories.tsx | 8 +- .../layers/plant/components/PlantSearch.tsx | 94 -- .../plant/components/PlantSuggestionList.tsx | 2 +- .../plant/components/PlantSuggestions.tsx | 9 +- .../components/PlantingAttributeEditForm.tsx | 185 ++- .../plant/components/PlantingElement.tsx | 123 +- .../layers/plant/components/SeedListItem.tsx | 57 + .../plant/hooks/useDeleteSelectedPlantings.ts | 27 + .../layers/plant/hooks/useFindPlantById.ts | 19 +- .../hooks/useFindPlantFromSeedCallback.ts | 37 + .../layers/plant/hooks/useFindSeedById.ts | 26 + .../layers/plant/hooks/usePlantSearch.ts | 13 +- .../layers/plant/hooks/useRelations.ts | 2 +- .../plant/hooks/useSeasonalAvailablePlants.ts | 8 +- .../layers/plant/hooks/useSeedSearch.ts | 40 + .../map_planning/routes/MapWrapper.tsx | 81 +- .../map_planning/store/MapStore.test.ts | 2 +- .../map_planning/store/MapStoreTypes.ts | 79 +- .../map_planning/store/RemoteActions.ts | 12 +- .../map_planning/store/TrackedMapStore.ts | 137 +- .../map_planning/store/UntrackedMapStore.ts | 182 ++- .../src/features/map_planning/store/utils.ts | 2 +- .../map_planning/store/zustand_get.test.ts | 45 + .../map_planning/types/PolygonTypes.ts | 45 + .../map_planning/types/layer-config.ts | 49 + .../features/map_planning/utils/Constants.ts | 3 - .../features/map_planning/utils/EditorTour.ts | 111 +- .../features/map_planning/utils/MapLabel.tsx | 13 +- .../map_planning/utils/PolygonUtils.test.ts | 241 +++ .../map_planning/utils/PolygonUtils.ts | 200 +++ .../utils/ReadOnlyModeContext.tsx | 39 + .../map_planning/utils/ShapesSelection.ts | 40 +- .../map_planning/utils/StageTransform.ts | 2 +- .../map_planning/utils/date-utils.test.ts | 33 + .../features/map_planning/utils/date-utils.ts | 14 + .../map_planning/utils/layer-utils.ts | 11 + .../map_planning/utils/planting-utils.test.ts | 42 + .../map_planning/utils/planting-utils.ts | 17 + frontend/src/features/maps/api/createMap.ts | 2 +- frontend/src/features/maps/api/findAllMaps.ts | 2 +- frontend/src/features/maps/api/findMapById.ts | 2 +- frontend/src/features/maps/api/updateMap.ts | 2 +- .../maps/components/CountingButton.tsx | 4 +- .../src/features/maps/components/MapCard.tsx | 7 +- .../features/maps/routes/MapCreateForm.tsx | 20 +- .../src/features/maps/routes/MapEditForm.tsx | 51 +- .../src/features/maps/routes/MapOverview.tsx | 6 +- .../nextcloud_integration/api/chat.ts | 195 +++ .../components/ConversationForm.tsx | 96 ++ .../components/FileSelector.tsx | 24 +- .../components/FileSelectorModal.tsx | 28 +- .../components/MessageList.tsx | 36 + .../components/NextcloudImage.tsx | 2 +- .../components/UploadFile.tsx | 3 +- .../nextcloud_integration/components/chat.tsx | 165 ++ .../hooks/useFileExists.ts | 23 + .../nextcloud_integration/hooks/useImage.ts | 4 +- .../hooks/useImageFromBlob.ts | 4 +- .../hooks/usePublicImage.tsx | 2 +- .../src/features/seeds/api/archiveSeed.ts | 12 + frontend/src/features/seeds/api/createSeed.ts | 4 +- frontend/src/features/seeds/api/deleteSeed.ts | 11 + frontend/src/features/seeds/api/editSeeds.ts | 12 + .../src/features/seeds/api/findAllPlants.ts | 2 +- .../src/features/seeds/api/findAllSeeds.ts | 2 +- .../src/features/seeds/api/findPlantById.ts | 11 +- .../src/features/seeds/api/findSeedById.ts | 11 +- .../src/features/seeds/api/searchPlants.ts | 2 +- .../seeds/components/CreateSeedForm.tsx | 167 +- .../seeds/components/SeedsOverviewList.tsx | 120 +- frontend/src/features/seeds/index.ts | 2 +- .../src/features/seeds/routes/CreateSeed.tsx | 44 +- .../src/features/seeds/routes/EditSeed.tsx | 110 ++ .../src/features/seeds/routes/SeedDetails.tsx | 92 -- .../src/features/seeds/routes/ViewSeeds.tsx | 79 +- .../features/seeds/store/CreateSeedStore.ts | 76 - frontend/src/features/toasts/groupedToast.tsx | 80 + frontend/src/hooks/useIsOnline.tsx | 43 + frontend/src/hooks/useKeyHandlers.tsx | 69 + frontend/src/hooks/useSafeAuth.tsx | 24 +- frontend/src/hooks/useTranslateQuality.ts | 24 + frontend/src/hooks/useTranslateQuantity.ts | 26 + frontend/src/icons/add.svg | 5 - frontend/src/icons/arrow.svg | 18 - frontend/src/icons/caret-down.svg | 4 - frontend/src/icons/caret-right.svg | 4 - frontend/src/icons/check.svg | 4 - frontend/src/icons/chevron-left.svg | 4 - frontend/src/icons/chevron-right.svg | 4 - frontend/src/icons/circle-dotted.svg | 15 - frontend/src/icons/close.svg | 5 - frontend/src/icons/copy.svg | 5 - frontend/src/icons/drag.svg | 13 - frontend/src/icons/drag_filling.svg | 13 - frontend/src/icons/edit.svg | 6 - frontend/src/icons/eye-off.svg | 6 - frontend/src/icons/eye.svg | 5 - frontend/src/icons/grid-dots.svg | 12 - frontend/src/icons/grid.svg | 13 - frontend/src/icons/heart.svg | 4 - frontend/src/icons/loader-quarter.svg | 6 - frontend/src/icons/move.svg | 35 - frontend/src/icons/photo-off.svg | 8 - frontend/src/icons/plant.svg | 6 - frontend/src/icons/redo.svg | 68 - frontend/src/icons/search.svg | 14 - frontend/src/icons/tags.svg | 6 - frontend/src/icons/trash.svg | 8 - frontend/src/icons/undo.svg | 64 - frontend/src/icons/user.svg | 5 - frontend/src/routes/index.ts | 31 +- frontend/src/routes/types.ts | 5 +- frontend/src/styles/globals.css | 9 +- frontend/src/styles/guidedTour.css | 94 +- frontend/src/svg/icons/archive-off.svg | 7 + frontend/src/svg/icons/eraser.svg | 5 + frontend/src/svg/icons/pencil-plus.svg | 7 + frontend/src/svg/icons/pointer.svg | 4 + frontend/src/svg/icons/search-reset.svg | 9 + frontend/src/utils/constants.ts | 18 + frontend/src/utils/enum.ts | 7 + frontend/src/utils/key-combinations.ts | 52 + frontend/src/utils/plant-naming.test.tsx | 220 +++ frontend/src/utils/plant-naming.tsx | 184 +++ frontend/src/utils/translated-enums.ts | 2 +- frontend/typedoc.json | 4 +- frontend/vite.config.ts | 1 + scraper/migrate_plant_detail_to_plant.sql | 8 - 542 files changed, 16781 insertions(+), 8039 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/release.yml create mode 100644 .github/repo_analysis.ipynb create mode 100644 backend/deny.toml create mode 100644 backend/migrations/2023-09-02-153556_archivable_seeds/down.sql create mode 100644 backend/migrations/2023-09-02-153556_archivable_seeds/up.sql create mode 100644 backend/migrations/2023-09-20-073850_unique_seeds/down.sql create mode 100644 backend/migrations/2023-09-20-073850_unique_seeds/up.sql create mode 100644 backend/migrations/2023-10-29-195901_planting_add_seed/down.sql create mode 100644 backend/migrations/2023-10-29-195901_planting_add_seed/up.sql delete mode 100644 backend/src/controller/planting_suggestions.rs create mode 100644 backend/src/model/dto/update_map_geometry_impl.rs create mode 100644 backend/src/model/enum/include_archived_seeds.rs delete mode 100644 backend/src/test/planting_suggestions.rs create mode 100755 ci/scripts/check-changelog.sh create mode 100755 ci/scripts/check-pre-commit.sh delete mode 100644 doc/backend/05updating_schema_patch.md create mode 100644 doc/changelog.md delete mode 100644 doc/decisions/example_migrations/normalized-plants-and-ranks/2023-04-07-130215_plant_relationships/down.sql create mode 100644 doc/decisions/example_migrations/normalized-plants-and-ranks/2023-04-07-130215_plant_relationships/down.sql.md delete mode 100644 doc/decisions/example_migrations/normalized-plants-and-ranks/2023-04-07-130215_plant_relationships/up.sql create mode 100644 doc/decisions/example_migrations/normalized-plants-and-ranks/2023-04-07-130215_plant_relationships/up.sql.md delete mode 100644 doc/decisions/example_migrations/normalized-plants-and-ranks/example_queries.sql create mode 100644 doc/decisions/example_migrations/normalized-plants-and-ranks/example_queries.sql.md delete mode 100644 doc/decisions/example_migrations/one-table-per-taxonomy/2023-03-09-194135_plant_relations/down.sql delete mode 100644 doc/decisions/example_migrations/one-table-per-taxonomy/2023-03-09-194135_plant_relations/up.sql create mode 100644 doc/decisions/example_migrations/one-table-per-taxonomy/2023-03-09-194135_plant_relations/up.sql.md delete mode 100644 doc/decisions/example_migrations/taxonomy-ranks-and-concrete-plants/2023-04-04-220813_taxons/down.sql create mode 100644 doc/decisions/example_migrations/taxonomy-ranks-and-concrete-plants/2023-04-04-220813_taxons/down.sql.md delete mode 100644 doc/decisions/example_migrations/taxonomy-ranks-and-concrete-plants/2023-04-04-220813_taxons/up.sql create mode 100644 doc/decisions/example_migrations/taxonomy-ranks-and-concrete-plants/2023-04-04-220813_taxons/up.sql.md delete mode 100644 doc/decisions/example_migrations/taxonomy-ranks-and-concrete-plants/2023-04-04-220921_plant_relationships/down.sql delete mode 100644 doc/decisions/example_migrations/taxonomy-ranks-and-concrete-plants/2023-04-04-220921_plant_relationships/up.sql create mode 100644 doc/decisions/example_migrations/taxonomy-ranks-and-concrete-plants/2023-04-04-220921_plant_relationships/up.sql.md delete mode 100644 doc/decisions/example_migrations/taxonomy-ranks-and-concrete-plants/2023-04-07-112305_varieties/down.sql delete mode 100644 doc/decisions/example_migrations/taxonomy-ranks-and-concrete-plants/2023-04-07-112305_varieties/up.sql delete mode 100644 doc/decisions/example_migrations/taxonomy-ranks-and-concrete-plants/example_queries.sql create mode 100644 doc/decisions/example_migrations/taxonomy-ranks-and-concrete-plants/example_queries.sql.md create mode 100644 doc/decisions/frontend_keyhandling.md create mode 100644 doc/decisions/frontend_mocking_tool.md create mode 100644 doc/decisions/frontend_timeline_concept.md create mode 100644 doc/guidelines/e2e.md create mode 100644 doc/guidelines/frontend-api-calls.md create mode 100644 doc/guidelines/frontend-keyhandling.md create mode 100644 doc/guidelines/frontend-ui-usability.md create mode 100644 doc/guidelines/i18n.md delete mode 100644 doc/guidelines/ui.md delete mode 100644 doc/meetings/2022_12_16.md delete mode 100644 doc/meetings/2023_02_17.md delete mode 100644 doc/meetings/2023_02_24.md delete mode 100644 doc/meetings/2023_03_02.md delete mode 100644 doc/meetings/2023_05_26.md create mode 100644 doc/meetings/2023_08_15.md create mode 100644 doc/meetings/2023_08_22.md create mode 100644 doc/meetings/2023_08_28.md create mode 100644 doc/meetings/2023_09_04.md create mode 100644 doc/meetings/2023_09_11.md create mode 100644 doc/meetings/2023_09_18.md create mode 100644 doc/meetings/2023_09_25.md create mode 100644 doc/meetings/2023_10_02.md create mode 100644 doc/meetings/2023_10_09.md create mode 100644 doc/meetings/2023_10_16.md create mode 100644 doc/meetings/2023_10_23.md create mode 100644 doc/meetings/2023_10_30.md create mode 100644 doc/meetings/2023_11_06.md create mode 100644 doc/meetings/2023_11_13.md create mode 100644 doc/meetings/2023_11_20.md create mode 100644 doc/meetings/2023_11_27.md create mode 100644 doc/meetings/2023_12_04.md delete mode 100644 doc/tests/manual.md create mode 100644 doc/tests/manual/reports/230915.md create mode 100644 doc/tests/manual/reports/231014.md create mode 100644 doc/tests/manual/reports/231111.md create mode 100644 doc/tests/manual/reports/231121.md create mode 100644 doc/tests/manual/reports/template.md delete mode 100644 doc/usecases/assigned/add_plant_relationships.md delete mode 100644 doc/usecases/assigned/area_of_plants.md delete mode 100644 doc/usecases/assigned/contributing_member.md delete mode 100644 doc/usecases/assigned/copy_paste_selection.md create mode 100644 doc/usecases/assigned/copy_paste_within_same_map.md create mode 100644 doc/usecases/assigned/create_layers.md delete mode 100644 doc/usecases/assigned/diversity_score.md delete mode 100644 doc/usecases/assigned/drawing_layer.md delete mode 100644 doc/usecases/assigned/event_notfication.md delete mode 100644 doc/usecases/assigned/experimental_results.md delete mode 100644 doc/usecases/assigned/geo_map.md delete mode 100644 doc/usecases/assigned/habitats_layer.md delete mode 100644 doc/usecases/assigned/incomplete_groups.md delete mode 100644 doc/usecases/assigned/infrastructure_layer.md delete mode 100644 doc/usecases/assigned/ingredient_lists.md delete mode 100644 doc/usecases/assigned/landing_page.md delete mode 100644 doc/usecases/assigned/landing_page_news.md delete mode 100644 doc/usecases/assigned/layers_alternatives.md delete mode 100644 doc/usecases/assigned/map_honors.md delete mode 100644 doc/usecases/assigned/map_specific_favorite_groups.md delete mode 100644 doc/usecases/assigned/map_statistics.md delete mode 100644 doc/usecases/assigned/map_timeline_event_view.md delete mode 100644 doc/usecases/assigned/map_timeline_range_selection.md delete mode 100644 doc/usecases/assigned/matchmaking.md delete mode 100644 doc/usecases/assigned/membership_application.md delete mode 100644 doc/usecases/assigned/offline.md delete mode 100644 doc/usecases/assigned/paths_layer.md delete mode 100644 doc/usecases/assigned/plant_lore.md delete mode 100644 doc/usecases/assigned/rename_layers.md delete mode 100644 doc/usecases/assigned/review_plant_relationships.md delete mode 100644 doc/usecases/assigned/reward_preview.md delete mode 100644 doc/usecases/assigned/soil_layer.md delete mode 100644 doc/usecases/assigned/suggest_plants.md delete mode 100644 doc/usecases/assigned/terrain_layer.md delete mode 100644 doc/usecases/assigned/todo_layer.md delete mode 100644 doc/usecases/assigned/trees_layer.md delete mode 100644 doc/usecases/assigned/watering_layer.md delete mode 100644 doc/usecases/assigned/winds_layer.md delete mode 100644 doc/usecases/current/base_layer.md delete mode 100644 doc/usecases/current/entry_list_seeds.md delete mode 100644 doc/usecases/current/gain_blossoms.md create mode 100644 doc/usecases/current/heat_map.md delete mode 100644 doc/usecases/current/map_search.md delete mode 100644 doc/usecases/current/plants_layer.md delete mode 100644 doc/usecases/current/shade_layer.md create mode 100644 doc/usecases/done/entry_list_seeds.md create mode 100644 doc/usecases/done/guided_tour.md create mode 100644 doc/usecases/done/multi_select.md create mode 100644 doc/usecases/draft/copy_paste_between_own_maps.md create mode 100644 doc/usecases/draft/copy_paste_between_users.md create mode 100644 doc/usecases/draft/copy_paste_via_icons.md create mode 100644 doc/usecases/draft/diff.md create mode 100644 doc/usecases/draft/remember_viewing_state.md create mode 100644 e2e/features/planting_select.feature create mode 100644 e2e/features/seeds.feature create mode 100644 e2e/pages/inventory/create.py create mode 100644 e2e/pages/inventory/edit.py create mode 100644 e2e/pages/inventory/management.py delete mode 100644 e2e/steps/test_frequent_steps.py create mode 100644 e2e/steps/test_inventory.py create mode 100644 e2e/steps/test_planting_select.py delete mode 100644 e2e/test-reports/.gitignore delete mode 100644 frontend/public/favicon.svg delete mode 100644 frontend/public/permaplant-logo-2.svg delete mode 100644 frontend/public/permaplant-logo-gray.svg delete mode 100644 frontend/public/permaplant-logo.svg delete mode 100644 frontend/public/plant.svg create mode 100644 frontend/src/__mocks__/styleMock.ts delete mode 100644 frontend/src/assets/globe.svg delete mode 100644 frontend/src/assets/permaplant-logo-2.svg delete mode 100644 frontend/src/assets/permaplant-logo-dark.svg delete mode 100644 frontend/src/assets/permaplant-logo-gray.svg delete mode 100644 frontend/src/assets/permaplant-logo.svg delete mode 100644 frontend/src/assets/planning.svg delete mode 100644 frontend/src/assets/plant.svg create mode 100644 frontend/src/components/Form/MarkdownEditor.tsx create mode 100644 frontend/src/components/Form/SimpleFormTextArea.tsx create mode 100644 frontend/src/components/Layout/FullPageNav.tsx delete mode 100644 frontend/src/config/i18n/de/grid.json create mode 100644 frontend/src/config/i18n/de/plantAndSeedSearch.json create mode 100644 frontend/src/config/i18n/de/polygon.json create mode 100644 frontend/src/config/i18n/de/readOnly.json create mode 100644 frontend/src/config/i18n/de/toolboxTooltips.json delete mode 100644 frontend/src/config/i18n/en/grid.json create mode 100644 frontend/src/config/i18n/en/plantAndSeedSearch.json create mode 100644 frontend/src/config/i18n/en/polygon.json create mode 100644 frontend/src/config/i18n/en/readOnly.json create mode 100644 frontend/src/config/i18n/en/toolboxTooltips.json create mode 100644 frontend/src/config/keybindings/index.ts create mode 100644 frontend/src/config/keybindings/keybindings.json create mode 100644 frontend/src/features/debugging/routes/Debug.tsx create mode 100644 frontend/src/features/map_planning/api/getMap.ts create mode 100644 frontend/src/features/map_planning/api/updateMapGeometry.ts create mode 100644 frontend/src/features/map_planning/components/EditorMap.tsx create mode 100644 frontend/src/features/map_planning/components/timeline/ItemSliderPicker.tsx delete mode 100644 frontend/src/features/map_planning/components/timeline/Timeline.tsx create mode 100644 frontend/src/features/map_planning/components/timeline/TimelineDatePicker.test.tsx create mode 100644 frontend/src/features/map_planning/components/timeline/TimelineDatePicker.tsx create mode 100644 frontend/src/features/map_planning/components/timeline/styles/timeline.css create mode 100644 frontend/src/features/map_planning/components/toolbar/LayerListItem.stories.tsx create mode 100644 frontend/src/features/map_planning/components/toolbar/LayerListItem.tsx delete mode 100644 frontend/src/features/map_planning/components/toolbar/Layers.stories.tsx delete mode 100644 frontend/src/features/map_planning/components/toolbar/Layers.tsx create mode 100644 frontend/src/features/map_planning/hooks/useGetTimelineEvents.tsx create mode 100644 frontend/src/features/map_planning/hooks/useSelectedLayerVisibility.test.ts create mode 100644 frontend/src/features/map_planning/hooks/useSelectedLayerVisibility.ts delete mode 100644 frontend/src/features/map_planning/layers/base/BaseMeasurementLayer.tsx create mode 100644 frontend/src/features/map_planning/layers/base/components/BaseLayerAttributeEditForm.tsx create mode 100644 frontend/src/features/map_planning/layers/base/components/DistanceMeasurementModal.tsx create mode 100644 frontend/src/features/map_planning/layers/base/components/MapGeometryEditor.tsx create mode 100644 frontend/src/features/map_planning/layers/base/components/MapGeometryToolForm.tsx create mode 100644 frontend/src/features/map_planning/layers/plant/components/PlantAndSeedSearch.tsx create mode 100644 frontend/src/features/map_planning/layers/plant/components/PlantCursor.tsx delete mode 100644 frontend/src/features/map_planning/layers/plant/components/PlantSearch.tsx create mode 100644 frontend/src/features/map_planning/layers/plant/components/SeedListItem.tsx create mode 100644 frontend/src/features/map_planning/layers/plant/hooks/useDeleteSelectedPlantings.ts create mode 100644 frontend/src/features/map_planning/layers/plant/hooks/useFindPlantFromSeedCallback.ts create mode 100644 frontend/src/features/map_planning/layers/plant/hooks/useFindSeedById.ts create mode 100644 frontend/src/features/map_planning/layers/plant/hooks/useSeedSearch.ts create mode 100644 frontend/src/features/map_planning/store/zustand_get.test.ts create mode 100644 frontend/src/features/map_planning/types/PolygonTypes.ts create mode 100644 frontend/src/features/map_planning/types/layer-config.ts create mode 100644 frontend/src/features/map_planning/utils/PolygonUtils.test.ts create mode 100644 frontend/src/features/map_planning/utils/PolygonUtils.ts create mode 100644 frontend/src/features/map_planning/utils/ReadOnlyModeContext.tsx create mode 100644 frontend/src/features/map_planning/utils/date-utils.test.ts create mode 100644 frontend/src/features/map_planning/utils/layer-utils.ts create mode 100644 frontend/src/features/map_planning/utils/planting-utils.test.ts create mode 100644 frontend/src/features/map_planning/utils/planting-utils.ts create mode 100644 frontend/src/features/nextcloud_integration/api/chat.ts create mode 100644 frontend/src/features/nextcloud_integration/components/ConversationForm.tsx create mode 100644 frontend/src/features/nextcloud_integration/components/MessageList.tsx create mode 100644 frontend/src/features/nextcloud_integration/components/chat.tsx create mode 100644 frontend/src/features/nextcloud_integration/hooks/useFileExists.ts create mode 100644 frontend/src/features/seeds/api/archiveSeed.ts create mode 100644 frontend/src/features/seeds/api/deleteSeed.ts create mode 100644 frontend/src/features/seeds/api/editSeeds.ts create mode 100644 frontend/src/features/seeds/routes/EditSeed.tsx delete mode 100644 frontend/src/features/seeds/routes/SeedDetails.tsx delete mode 100644 frontend/src/features/seeds/store/CreateSeedStore.ts create mode 100644 frontend/src/features/toasts/groupedToast.tsx create mode 100644 frontend/src/hooks/useIsOnline.tsx create mode 100644 frontend/src/hooks/useKeyHandlers.tsx create mode 100644 frontend/src/hooks/useTranslateQuality.ts create mode 100644 frontend/src/hooks/useTranslateQuantity.ts delete mode 100644 frontend/src/icons/add.svg delete mode 100644 frontend/src/icons/arrow.svg delete mode 100644 frontend/src/icons/caret-down.svg delete mode 100644 frontend/src/icons/caret-right.svg delete mode 100644 frontend/src/icons/check.svg delete mode 100644 frontend/src/icons/chevron-left.svg delete mode 100644 frontend/src/icons/chevron-right.svg delete mode 100644 frontend/src/icons/circle-dotted.svg delete mode 100644 frontend/src/icons/close.svg delete mode 100644 frontend/src/icons/copy.svg delete mode 100644 frontend/src/icons/drag.svg delete mode 100644 frontend/src/icons/drag_filling.svg delete mode 100644 frontend/src/icons/edit.svg delete mode 100644 frontend/src/icons/eye-off.svg delete mode 100644 frontend/src/icons/eye.svg delete mode 100644 frontend/src/icons/grid-dots.svg delete mode 100644 frontend/src/icons/grid.svg delete mode 100644 frontend/src/icons/heart.svg delete mode 100644 frontend/src/icons/loader-quarter.svg delete mode 100644 frontend/src/icons/move.svg delete mode 100644 frontend/src/icons/photo-off.svg delete mode 100644 frontend/src/icons/plant.svg delete mode 100644 frontend/src/icons/redo.svg delete mode 100644 frontend/src/icons/search.svg delete mode 100644 frontend/src/icons/tags.svg delete mode 100644 frontend/src/icons/trash.svg delete mode 100644 frontend/src/icons/undo.svg delete mode 100644 frontend/src/icons/user.svg create mode 100644 frontend/src/svg/icons/archive-off.svg create mode 100644 frontend/src/svg/icons/eraser.svg create mode 100644 frontend/src/svg/icons/pencil-plus.svg create mode 100644 frontend/src/svg/icons/pointer.svg create mode 100644 frontend/src/svg/icons/search-reset.svg create mode 100644 frontend/src/utils/constants.ts create mode 100644 frontend/src/utils/key-combinations.ts create mode 100644 frontend/src/utils/plant-naming.test.tsx create mode 100644 frontend/src/utils/plant-naming.tsx delete mode 100644 scraper/migrate_plant_detail_to_plant.sql diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 0e596db85..b2173ccfc 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,4 +1,4 @@ -FROM rust:1.67.1-slim +FROM rust:1.74-slim-bullseye ENV DEBIAN_FRONTEND=noninteractive ENV RUSTFLAGS="-C link-arg=-fuse-ld=lld" diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 953f3497e..564d66af9 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,32 +1,7 @@ # Contributing -If you are new, it is probably best if you first [write us](mailto:contact@permaplant.net). +If you didn't speak with us already, it is probably best if you first [write us](mailto:contact@permaplant.net). We use [GitHub](https://github.com/ElektraInitiative/PermaplanT/) to maintain this initiative. -Before you start working on anything, please make sure you have [pre-commit hooks](../doc/contrib/README.md#Hooks) set up. - -For any non-trival work, i.e. not only updates in docu or tests, there should be an underlying issue. -You can create such issues yourself. -Make sure the issue is: - -- assigned to you and -- ["In Progress" in our project](https://github.com/orgs/ElektraInitiative/projects/4). - -Once you created a PR, please request reviews, including also from @markus2330, who will usually also merge. - -Commit messages: - -- The first line in commit messages should be short. -- From the third line you can have more elaborate descriptions of the changes. -- Please refer to #issues/#PRs/@mention as useful. - -Branch names: - -- feature/ -- bug_fix/ -- documentation/ -- meeting_notes/ -- release/ - -If requested, and in any case before you start making fundamental changes, create a [decision](/doc/decisions/) before creating a code PR. +Continue reading in [here](../doc/contrib/README.md). diff --git a/.github/ISSUE_TEMPLATE/release.yml b/.github/ISSUE_TEMPLATE/release.yml new file mode 100644 index 000000000..bf9f0f1cc --- /dev/null +++ b/.github/ISSUE_TEMPLATE/release.yml @@ -0,0 +1,33 @@ +name: 🚀 Release +description: PermaplanT's release procedure +body: + - type: textarea + attributes: + label: Release + description: The procedure, please update the milestone link. + value: | + ### Prep for Release + + - [ ] [Finish release critical task](https://github.com/ElektraInitiative/PermaplanT/milestone/) + - [ ] fix _real_ security problems + unfortunately `npm audit --omit=dev fix` does [too much and too little](https://overreacted.io/npm-audit-broken-by-design/), + `cargo deny check` seems to be more helpful + - [ ] check/improve reformatting + - [ ] check if all issues labelled `release critical` are fixed + - [ ] update mergedDatasets.csv + - [ ] Merge PRs (@markus2330) + + ### Actual Release + + - [ ] check if all preps are done + - [ ] manually test dev.permaplant.net according to protocol + - [ ] build + - [ ] git tag -s vX.X.X + - [ ] git push --tags + + ### After Release + + - [ ] merge release PR to pump versions and new section in Changelog + - [ ] write announcement in [nextcloud](https://nextcloud.markus-raab.org/nextcloud/) + validations: + required: false diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index da8f2a8db..8d2a075e1 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -8,10 +8,10 @@ Check relevant points but **please do not remove entries**. These points need to be fulfilled for every PR. --> -- [ ] I added a line to [/doc/CHANGELOG.md](/doc/CHANGELOG.md) +- [ ] I added a line to [changelog.md](/doc/changelog.md) - [ ] The PR is rebased with current master. - [ ] Details of what you changed are in commit messages. -- [ ] References to issues, e.g. `close #X`, are in the commit messages. +- [ ] References to issues, e.g. `close #X`, are in the commit messages and changelog. - [ ] The buildserver is happy. -## TC-005 - Plant Search +## Plant Search - Description: Show a selection of plants if the search input is empty. -- Preconditions: - - No search input has been provided in the plant search. -- Test Steps: - 1. Navigate to the map page. - 2. Select a map. - 3. Select the plant layer in the right map menu. - 4. Push the search icon in the lower right menu. -- Expected Result: - - A selection of plants is shown to the user. +- Given I am on a map page with the plant layer active +- When I have an empty search box +- Then I can see various plants as results - Actual Result: - Test Result: - Notes: -## TC-006 - Plant Search - -- Description: Return expected results for a given search in the selected language. -- Preconditions: - - The search term "tomato" has been typed into the search field. -- Test Steps: - 1. Navigate to the map page. - 2. Select a map. - 3. Select the plant layer in the right map menu. - 4. Push the search icon in the lower right menu. - 5. Write "tomato" into the search field. -- Expected Result: - - The plants shown contain the string "tomato" in part of the datacolumns as outlined in the usecase document. +## Heatmap (NOT IMPLEMENTED) + +- Description: Test whether the heatmap endpoints generates the image correctly. +- Given I am on a map page with the plant layer active +- When I start planting a plant +- Then I see suitable places for that plant - Actual Result: - Test Result: - Notes: -## TC-007 - Plant Search - -- Description: Returns results for searches in the language that was selected. -- Preconditions: - - Language English has been selected -- Test Steps: - 1. Navigate to the map page. - 2. Select a map. - 3. Select the plant layer in the right map menu. - 4. Push the search icon in the lower right menu. - 5. Write "Tomato" and "Potato" into the search field. -- Expected Result: - - "Tomato" and "Potato" should be the first match. +## Layer opacity + +- Description: Check whether the opacity of a layer changes. +- Given I am on a map page with a base layer image configured +- When dragging the slider for the base layer in the layer section of the toolbar to 50% +- Then I can see the change in opacity of the base layer image +- When I change the size of the toolbar +- Then I can see the slider and the filling change in size proportionally. Therefore 50% of the slider should be filled. - Actual Result: - Test Result: - Notes: -## TC-008 - Heatmap +## Base Layer -- Description: Test whether the heatmap endpoints generates the image correctly. -- Preconditions: - - Be on the map editor page. - - Data is inserted via the scraper (plants and plant relations) -- Test Steps: - 1. Create a map - 2. Plant some plants with relations. - 3. Add other constraints such as shade or soil ph. - 4. Generate the heatmap. -- Expected Result: - - Heatmap considers map polygon and environmental constraints. +- Description: Check whether the maps background image is displayed correctly. +- Given I am on a map page with the base layer active +- When I select a base layer image +- Then I can see the base layer image on the canvas - Actual Result: - Test Result: - Notes: -## TC-009 - Timeline - -- Description: Change the date of the map to 'hide' plantings. -- Preconditions: - - User must be on the map planning screen. -- Test Steps: - 1. Add a planting to the map. - 2. Click on the date selection on the bottom of the screen. - 3. Navigate to a date in the past. - 4. Wait 1 second. -- Expected Result: - - The indicator was briefly blue, indicating a loading state. - - The indicator beside the input is green. - - The Date on the bottom/right corner of the screen shows a date in the past. - - The planting previously planted is gone. +## Grid + +- Description: Display a point grid on the screen. +- Given I am on a map page +- When I Zoom in or out +- Then the grid spacing is changing - Actual Result: - Test Result: - Notes: -## TC-010 - Timeline - -- Description: Change the date of the map to 'unhide' plantings. -- Preconditions: - - User must be on the map planning screen. -- Test Steps: - 1. Add a planting to the map. - 2. Click on the date selection on the bottom of the screen. - 3. Navigate to a date in the past. - 4. Wait 1 second. - 5. Navigate to today. -- Expected Result: - - The indicator was briefly blue, indicating a loading state. - - The indicator beside the input is green. - - The Date on the bottom/right corner of the screen shows the current day. - - The planting previously planted was gone while being in the past. - - The planting is visible again. +## Map Editor Guided Tour + +- Description: Check whether the Guided Tour leaves the Map Editor in its original state. +- Given I am on a map page +- When I do the Guided Tour +- Then after I have done the Guided Tour the map is the same as before - Actual Result: - Test Result: - Notes: -## TC-011 - Timeline - -- Description: Change the `add_date` of a planting to 'hide' it. -- Preconditions: - - User must be on the map planning screen. -- Test Steps: - 1. Add a planting to the map. - 2. Click on the planting. - 3. Click on the `Add Date` date selector in the left lower toolbar. - 4. Change the date to a date in the future. -- Expected Result: - - The indicator was briefly blue, indicating a loading state. - - The indicator beside the input is green. - - The planting previously planted is gone. +## Map Editor Guided Tour + +- Description: Guided Tour shows when not completed. +- Given I am on a map page +- When I have not completed the Guided Tour +- Then I can do the Guided Tour at any time - Actual Result: - Test Result: - Notes: -## TC-012 - Timeline - -- Description: Change the `remove_date` of a planting to 'hide' it. -- Preconditions: - - User must be on the map planning screen. - - The plants date has to be in the past. -- Test Steps: - 1. Add a planting to the map. - 2. Click on the planting. - 3. Click on the `Remove Date` date selector in the left lower toolbar. - 4. Change the date to today. -- Expected Result: - - The indicator was briefly blue, indicating a loading state. - - The indicator beside the input is green. - - The planting previously planted is gone. +## Map Editor Guided Tour + +- Description: Guided Tour only shows when not completed or explicitly cancelled. +- Given I am on a map page +- When I have not completed the Guided Tour +- Then I can interrupt the Tour at any time and come back later - Actual Result: - Test Result: - Notes: -## TC-013 - Timeline - -- Description: Change the `add_date` of a planting to 'unhide' it. -- Preconditions: - - User must be on the map planning screen. -- Test Steps: - 1. Add a planting to the map. - 2. Click on the planting. - 3. Click on the `Add Date` date selector in the left lower toolbar. - 4. Change the date to a date in the future. - 5. Wait one second - 6. Remove the date -- Expected Result: - - The indicator was briefly blue, indicating a loading state. - - The indicator beside the input is green. - - The planting previously planted was gone while its `add_date` was in the future. - - The planting is visible now with its `add_date` unset. +## Chat: Create conversation (NOT IMPLEMENTED) + +- Description: A conversation can be created +- Given I am on the chat page +- When I create a new conversation +- Then I can see the conversation in the conversation list - Actual Result: - Test Result: -- Notes: +- Notes: Currently only works with CORS disabled. -## TC-014 - Base Layer +## Chat: Send message (NOT IMPLEMENTED) -- Description: Check whether the maps background image is displayed correctly. -- Preconditions: - - A map has been created. - - The user has navigated to the map editor. - - The base layer has to be selected as the active layer. -- Test Steps: - 1. Select a base layer image. - 2. Set the base layer rotation to 45 degrees. - 3. Scale the base layer image to 50 px per meter. - 4. Close and reopen the current map. -- Expected Result: - - The selected base layer image is displayed after it has been selected. - - Applying rotation was successful (the image is rotated by 45 degrees). - - Applying scale was successful (the image is twice as large). - - The state of the base layer does not change when closing and reopening the map. +- Description: Send a message to a Nextcloud conversation. +- Given I am on the chat page +- When I select a conversation +- Then I can send messages in that conversation - Actual Result: - Test Result: -- Notes: +- Notes: Currently only works with CORS disabled. -## TC-015 - Grid +## Additional name on map: plant labels -- Description: Display a point grid on the screen. -- Preconditions: - - User must be in the map editor. -- Test Steps: - 1. Press the grid button in the left upper menu bar. - 2. Zoom all the way in. - 3. Zoom all the way out. -- Expected Result: - - The grid is not displayed anymore. - - Each press on the grid button toggles the grid off/on. - - Zooming in, grid spacing should switch from one meter to ten centimeters. - - Zooming out, grid spacing should switch ten centimeters to one meter to ten meter. +- Description: Test additional names being displayed properly. +- Given I am on the map page with the plants layer active +- When I create a new plant from a seed. +- Then I can see the additional name on the label when hovering over the plant. - Actual Result: - Test Result: -- Notes: +- Notes: The additional name must also be visible when a different account views the same map in parallel. -## TC-016 - Map Editor Guided Tour +## Additional name on map: left toolbar -- Description: Check whether the Guided Tour leaves the Map Editor in its original state. -- Preconditions: - - User must not have completed the Guided Tour prior. - - User must be on the map editor screen. -- Test Steps: - 1. Follow the Guided Tour until its end. - 2. Do every step exactly as stated. -- Expected Result: - - There are no plants on the map. - - The map date is set to the current date. - - Placement mode is not active. +- Description: Test additional names being displayed properly. +- Given I am on the map page with the plants layer active +- When I create a new plant from a seed. +- When I click on the new plant icon. +- Then I can see the additional name in the left toolbars heading. - Actual Result: - Test Result: -- Notes: +- Notes: The additional name must also be visible when a different account views the same map in parallel. -## TC-017 - Map Editor Guided Tour +## Additional name on map: updates -- Description: Guided Tour only shows when not completed or explicitly cancelled. -- Preconditions: - - User must not have completed the Guided Tour prior. - - User must be on the map editor screen. -- Test Steps: - 1. Leave the page by entering a different URL or using the browsers navigate back button. - 2. Return to the map editor screen. - 3. Use the cancel button on the Tour step or press ESC. - 4. Reload the page. -- Expected Result: - - The Guided Tour will show again after returning to the map editor screen. - - After the page reload, the Guided Tour will not be displayed. +- Description: Test additional names being displayed properly. +- Given I am on the map page with the plants layer active +- When I create a new plant from a seed. +- When I go to the inventory page and change the seeds name. +- Then I can see the changes in the plant label and left toolbar. - Actual Result: - Test Result: -- Notes: +- Notes: The additional name must also be visible when a different account views the same map. - diff --git a/doc/tests/manual/reports/230915.md b/doc/tests/manual/reports/230915.md new file mode 100644 index 000000000..5ab78e0f6 --- /dev/null +++ b/doc/tests/manual/reports/230915.md @@ -0,0 +1,141 @@ +# Manual Test Report (v0.3.3) + +## General + +- Tester: Aydan Namdar Ghazani +- Date/Time: 15 September 2023 +- Duration: ~10 minutes +- Commit/Tag: [af6b307](https://github.com/ElektraInitiative/PermaplanT/tree/604c8e73ab1209a3e0f62f0e6b3b5672bf8d9f11) +- Setup: dev.permaplant.net +- Planned tests: **11** +- Executed tests: **8** +- ✔️Passed tests: **5** +- ⚠️Problematic tests: **3** +- ❌Failed tests: **0** + +## [Error Analysis](../README.md#report-header) + +No errors occured during the test. + +## Closing remarks + +### How is the current state of the software? + +The software is in a good state. + +### Have the quality objectives been achieved? + +There were no defined quality objectives at this time. + +### What are the consequences drawn from the current state, including: how can future errors be avoided, how can the development process be improved? + +The test protocol can be improved. There is no need to write testcases for features that are not implemented yet. + +## Testcases + +## Plant Search + +- Description: Show a selection of plants if the search input is empty. +- Given I am on a map page with the plant layer active +- When I have an empty search box +- Then I can see various plants as results +- Test Result: ✔️ + +## Heatmap + +- Description: Test whether the heatmap endpoints generates the image correctly. +- Given I am on a map page with the plant layer active +- When I start planting a plant +- Then I see suitable places for that plant +- Test Result: Not Implemented + +## Base Layer + +- Description: Check whether the maps background image is displayed correctly. +- Given I am on a map page with the base layer active +- When I select a base layer image +- Then I can see the base layer image on the canvas +- Test Result: ✔️ + +## Grid + +- Description: Display a point grid on the screen. +- Given I am on a map page +- When I Zoom in or out +- Then the grid spacing is changing +- Test Result: ✔️ + +## Map Editor Guided Tour + +- Description: Check whether the Guided Tour leaves the Map Editor in its original state. +- Given I am on a map page +- When I do the Guided Tour +- Then after I have done the Guided Tour the map is the same as before +- Actual Result: +- Test Result: ⚠️ +- Notes: Was not able to test, since my user already performed the tour in the past + +## Map Editor Guided Tour + +- Description: Guided Tour shows when not completed. +- Given I am on a map page +- When I have not completed the Guided Tour +- Then I can do the Guided Tour at any time +- Actual Result: +- Test Result: ⚠️ +- Notes: Was not able to test, since my user already performed the tour in the past + +## Map Editor Guided Tour + +- Description: Guided Tour only shows when not completed or explicitly cancelled. +- Given I am on a map page +- When I have not completed the Guided Tour +- Then I can interrupt the Tour at any time and come back later +- Actual Result: +- Test Result: ⚠️ +- Notes: Was not able to test, since my user already performed the tour in the past + +## Edit seed + +- Description: Edit seed. +- Preconditions: + - User is on the view seed page. +- Test Steps: + 1. Press Button "Edit seed" + 2. Change an attribute of the seed. + 3. Submit the form. + 4. Repeat steps 1 through 3 for every seed attribute. +- Expected Result: + - The seed attributes updated successfully. +- Test Result: ✔️ + +## Delete seed + +- Description: Edit seed. +- Preconditions: + - User is on the view seed page. +- Test Steps: + 1. Press Button "Delete seed" +- Expected Result: + - The selected seed is no longer available. +- Test Result: ✔️ + +## Chat: Create conversation + +- Description: A conversation can be created +- Given I am on the chat page +- When I create a new conversation +- Then I can see the conversation in the conversation list +- Actual Result: +- Test Result: Not Implemented +- Notes: Currently only works with CORS disabled. + +## Chat: Send message + +- Description: Send a message to a Nextcloud conversation. +- Given I am on the chat page +- When I select a conversation +- Then I can send messages in that conversation +- Actual Result: +- Test Result: Not Implemented +- Notes: Currently only works with CORS disabled. diff --git a/doc/tests/manual/reports/231014.md b/doc/tests/manual/reports/231014.md new file mode 100644 index 000000000..f38fedc53 --- /dev/null +++ b/doc/tests/manual/reports/231014.md @@ -0,0 +1,110 @@ +# Manual Test Report (v0.3.4) + +## General + +- Tester: Aydan Namdar Ghazani +- Date/Time: 14. October 2023 +- Duration: ~5 minutes +- Commit/Tag: [1c3b54a](https://github.com/ElektraInitiative/PermaplanT/tree/1c3b54aa80c2fff938d7f9fe8cd662084c876873) +- Setup: dev.permaplant.net +- Planned tests: **8** +- Executed tests: **8** +- ✔️Passed tests: **8** +- ⚠️Problematic tests: **0** +- ❌Failed tests: **0** + +## [Error Analysis](../README.md#report-header) + +No errors occured during the test. + +## Closing remarks + +### How is the current state of the software? + +The software is in a good state. + +### Have the quality objectives been achieved? + +There were no defined quality objectives at this time. + +### What are the consequences drawn from the current state, including: how can future errors be avoided, how can the development process be improved? + +We should remove tests from the protocol which are not implemented yet (Heatmap, Chat) + +## Testcases + +## Plant Search + +- Description: Show a selection of plants if the search input is empty. +- Given I am on a map page with the plant layer active +- When I have an empty search box +- Then I can see various plants as results +- Test Result: ✔️ + +## Base Layer + +- Description: Check whether the maps background image is displayed correctly. +- Given I am on a map page with the base layer active +- When I select a base layer image +- Then I can see the base layer image on the canvas +- Test Result: ✔️ + +## Grid + +- Description: Display a point grid on the screen. +- Given I am on a map page +- When I Zoom in or out +- Then the grid spacing is changing +- Test Result: ✔️ + +## Map Editor Guided Tour + +- Description: Check whether the Guided Tour leaves the Map Editor in its original state. +- Given I am on a map page +- When I do the Guided Tour +- Then after I have done the Guided Tour the map is the same as before +- Actual Result: +- Test Result: ✔️ + +## Map Editor Guided Tour + +- Description: Guided Tour shows when not completed. +- Given I am on a map page +- When I have not completed the Guided Tour +- Then I can do the Guided Tour at any time +- Actual Result: +- Test Result: ✔️ + +## Map Editor Guided Tour + +- Description: Guided Tour only shows when not completed or explicitly cancelled. +- Given I am on a map page +- When I have not completed the Guided Tour +- Then I can interrupt the Tour at any time and come back later +- Actual Result: +- Test Result: ✔️ + +## Edit seed + +- Description: Edit seed. +- Preconditions: + - User is on the view seed page. +- Test Steps: + 1. Press Button "Edit seed" + 2. Change an attribute of the seed. + 3. Submit the form. + 4. Repeat steps 1 through 3 for every seed attribute. +- Expected Result: + - The seed attributes updated successfully. +- Test Result: ✔️ + +## Delete seed + +- Description: Edit seed. +- Preconditions: + - User is on the view seed page. +- Test Steps: + 1. Press Button "Delete seed" +- Expected Result: + - The selected seed is no longer available. +- Test Result: ✔️ diff --git a/doc/tests/manual/reports/231111.md b/doc/tests/manual/reports/231111.md new file mode 100644 index 000000000..2870fe7c8 --- /dev/null +++ b/doc/tests/manual/reports/231111.md @@ -0,0 +1,110 @@ +# Manual Test Report (v0.3.5) + +## General + +- Tester: Aydan Namdar Ghazani +- Date/Time: 11. November 2023 +- Duration: 5 minutes +- Commit/Tag: [42000aa](https://github.com/ElektraInitiative/PermaplanT/tree/42000aa868348e5a6bc8e5e3e54092794dce49aa) +- Setup: dev.permaplant.net +- Planned tests: **8** +- Executed tests: **8** +- ✔️Passed tests: **8** +- ⚠️Problematic tests: **0** +- ❌Failed tests: **0** + +## [Error Analysis](../README.md#report-header) + +No errors occured during the tests. + +## Closing remarks + +### How is the current state of the software? + +The software is in a good state. + +### Have the quality objectives been achieved? + +There were no defined quality objectives at this time. + +### What are the consequences drawn from the current state, including: how can future errors be avoided, how can the development process be improved? + +None + +## Testcases + +## Plant Search + +- Description: Show a selection of plants if the search input is empty. +- Given I am on a map page with the plant layer active +- When I have an empty search box +- Then I can see various plants as results +- Test Result: ✔️ + +## Base Layer + +- Description: Check whether the maps background image is displayed correctly. +- Given I am on a map page with the base layer active +- When I select a base layer image +- Then I can see the base layer image on the canvas +- Test Result: ✔️ + +## Grid + +- Description: Display a point grid on the screen. +- Given I am on a map page +- When I Zoom in or out +- Then the grid spacing is changing +- Test Result: ✔️ + +## Map Editor Guided Tour + +- Description: Check whether the Guided Tour leaves the Map Editor in its original state. +- Given I am on a map page +- When I do the Guided Tour +- Then after I have done the Guided Tour the map is the same as before +- Actual Result: +- Test Result: ✔️ + +## Map Editor Guided Tour + +- Description: Guided Tour shows when not completed. +- Given I am on a map page +- When I have not completed the Guided Tour +- Then I can do the Guided Tour at any time +- Actual Result: +- Test Result: ✔️ + +## Map Editor Guided Tour + +- Description: Guided Tour only shows when not completed or explicitly cancelled. +- Given I am on a map page +- When I have not completed the Guided Tour +- Then I can interrupt the Tour at any time and come back later +- Actual Result: +- Test Result: ✔️ + +## Edit seed + +- Description: Edit seed. +- Preconditions: + - User is on the view seed page. +- Test Steps: + 1. Press Button "Edit seed" + 2. Change an attribute of the seed. + 3. Submit the form. + 4. Repeat steps 1 through 3 for every seed attribute. +- Expected Result: + - The seed attributes updated successfully. +- Test Result: ✔️ + +## Delete seed + +- Description: Edit seed. +- Preconditions: + - User is on the view seed page. +- Test Steps: + 1. Press Button "Delete seed" +- Expected Result: + - The selected seed is no longer available. +- Test Result: ✔️ diff --git a/doc/tests/manual/reports/231121.md b/doc/tests/manual/reports/231121.md new file mode 100644 index 000000000..106b7310d --- /dev/null +++ b/doc/tests/manual/reports/231121.md @@ -0,0 +1,85 @@ +# Manual Test Report (v0.3.6) + +## General + +- Tester: Aydan Namdar Ghazani +- Date/Time: 21. November 2023 +- Duration: 5 minutes +- Commit/Tag: [671cab1](https://github.com/ElektraInitiative/PermaplanT/pull/1062/commits/671cab1866fc94f24202892b01dc96e571558429) +- Setup: localhost +- Planned tests: **6** +- Executed tests: **6** +- ✔️Passed tests: **6** +- ⚠️Problematic tests: **0** +- ❌Failed tests: **0** + +## [Error Analysis](../README.md#report-header) + +"Sorry I couldnt load your image" error occured on opening a fresh map. + +## Closing remarks + +### How is the current state of the software? + +The software is in a good state. The image loading error is a known bug, it still exists. + +### Have the quality objectives been achieved? + +There were no defined quality objectives at this time. + +### What are the consequences drawn from the current state, including: how can future errors be avoided, how can the development process be improved? + +None + +## Testcases + +## Plant Search + +- Description: Show a selection of plants if the search input is empty. +- Given I am on a map page with the plant layer active +- When I have an empty search box +- Then I can see various plants as results +- Test Result: ✔️ + +## Base Layer + +- Description: Check whether the maps background image is displayed correctly. +- Given I am on a map page with the base layer active +- When I select a base layer image +- Then I can see the base layer image on the canvas +- Test Result: ✔️ + +## Grid + +- Description: Display a point grid on the screen. +- Given I am on a map page +- When I Zoom in or out +- Then the grid spacing is changing +- Test Result: ✔️ + +## Map Editor Guided Tour + +- Description: Check whether the Guided Tour leaves the Map Editor in its original state. +- Given I am on a map page +- When I do the Guided Tour +- Then after I have done the Guided Tour the map is the same as before +- Actual Result: +- Test Result: ✔️ + +## Map Editor Guided Tour + +- Description: Guided Tour shows when not completed. +- Given I am on a map page +- When I have not completed the Guided Tour +- Then I can do the Guided Tour at any time +- Actual Result: +- Test Result: ✔️ + +## Map Editor Guided Tour + +- Description: Guided Tour only shows when not completed or explicitly cancelled. +- Given I am on a map page +- When I have not completed the Guided Tour +- Then I can interrupt the Tour at any time and come back later +- Actual Result: +- Test Result: ✔️ diff --git a/doc/tests/manual/reports/README.md b/doc/tests/manual/reports/README.md index e69de29bb..89f2c8cfa 100644 --- a/doc/tests/manual/reports/README.md +++ b/doc/tests/manual/reports/README.md @@ -0,0 +1,9 @@ +# Report + +A report is the execution of one test protocol. +It contains a report header at the beginning. +A report can chose to perform a subset of the test cases from the protocol, but should mention that some tests are skipped. + +## Naming convention + +The report should be named after todays date in this format YYMMDD. diff --git a/doc/tests/manual/reports/template.md b/doc/tests/manual/reports/template.md new file mode 100644 index 000000000..a786e3f70 --- /dev/null +++ b/doc/tests/manual/reports/template.md @@ -0,0 +1,38 @@ +## General + +- Tester: +- Date/Time: +- Duration: +- Commit/Tag: +- Setup: local build or dev.permaplant +- Planned tests: -**1** +- Executed tests: -**1** +- ✔️Passed tests: -**1** +- ⚠️Problematic tests: -**1** +- ❌Failed tests: -**1** + +## Error Analysis + +1. Identifying the error: Is it indeed a genuine software defect, or is it a faulty test case, incorrect test execution, etc.? +2. Has the error already been identified in previous tests? +3. Error specifictation + +- Class 1: Faulty specification +- Class 2: System crash +- Class 3: Essential functionality is faulty +- Class 4: Functional deviation or limitation +- Class 5: Minor deviation +- Class 6: Cosmetic issue + +4. Priority + +- Level 1: Immediate resolution +- Level 2: Fix in next version +- Level 3: Correction will be done opportunistically +- Level 4: Correction planning is still open + +## Closing remarks + +- How is the current state of the software? +- Have the quality objectives been achieved? +- What are the consequences drawn from the current state, including: how can future errors be avoided, how can the development process be improved? diff --git a/doc/usecases/README.md b/doc/usecases/README.md index c2ac0e706..755628503 100644 --- a/doc/usecases/README.md +++ b/doc/usecases/README.md @@ -4,9 +4,9 @@ This folder contains all use cases of PermaplanT. They are structured in folders by progress: -- `draft`: something is yet unclear about the use case -- `assigned`: use case could be implemented but is not part of current milestone -- `current`: use case is currently implemented (within milestone) +- `draft`: not yet ready for implementation +- `assigned`: use case is part of PermaplanT 1.0 +- `current`: use case is currently worked on - `done`: use case was successfully implemented ## Done diff --git a/doc/usecases/assigned/add_plant_relationships.md b/doc/usecases/assigned/add_plant_relationships.md deleted file mode 100644 index c44dcb855..000000000 --- a/doc/usecases/assigned/add_plant_relationships.md +++ /dev/null @@ -1,37 +0,0 @@ -# Use Case: Add Plant Relationships - -## Summary - -- **Scope:** Plants Layer -- **Level:** User Goal -- **Actors:** App User -- **Brief:** A new plant relationship is added to the system. -- **Assignee:** Ready to be assigned - -## Scenarios - -- **Precondition:** - - The user has opened the UI to propose a new relationship. -- **Main success scenario:** - - Details about the relationship are provided by the user. - - The two sides of the relationship can be - - Specific plants - - Taxonomic ranks which are not specific plants - - The type of the relationship can be - - Companion - - Antagonist - - Neutral - - Notes providing information on the why's and how's. -- **Alternative scenario:** - - The user creates a relationship which is only effective for one or more of her selected maps. - In this case there is no review process. - Map-specific relationships take precedence. -- **Error scenario:** - - The user tries to add a global relationship where another global with identical sides already exits. - The user is informed about this and can't create the relationship. - - The user tries to add a map-specific relationship where another map-specific with identical sides already exits. - The user is informed about this and can't create the relationship. -- **Postcondition:** - - A new relationship between plants is [reviewable by other users](../assigned/review_plant_relationships.md). - - A new relationship between plants is considered when suggesting alternatives. -- **Non-functional Constraints:** diff --git a/doc/usecases/assigned/area_of_plants.md b/doc/usecases/assigned/area_of_plants.md deleted file mode 100644 index 0d3e1b97f..000000000 --- a/doc/usecases/assigned/area_of_plants.md +++ /dev/null @@ -1,32 +0,0 @@ -# Use Case: Area of Plants - -## Summary - -- **Scope:** Plants Layer -- **Level:** User Goal -- **Actors:** App User -- **Brief:** The user can add, edit, move and delete plant areas elements in their map in the plants layer. -- **Assignee:** Ready to be assigned - -## Scenarios - -- **Precondition:** - The user has opened the app and has selected the plants layer. -- **Main success scenario:** - - While adding a plant, while holding the mouse, the user can draw a rectangle. - - The size of the arena and the number of plants is shown next to the mouse. - - When overlapping with other elements, it is visually indicated. -- **Alternative scenario:** - The user accidentally drew a wrong size of the area: - - and uses the app's undo function to correct the mistake - - or is able to change the size as wanted. -- **Error scenario:** - - The user attempts to add, move or edit a plant area element but the app is experiencing technical difficulties and is unable to complete the request, displaying an error message. - - There is an error in the app's plant relationship indication and the lines connecting the plants are not displayed correctly. In this case, the app displays an error message. -- **Postcondition:** - - The user's map includes the added, edited, moved or deleted plant area element as desired. - - If constraints are violated for the place where a plant was added or moved, warnings get added (or removed) to (from) the [warnings layer](../assigned/warnings_layer.md). -- **Non-functional Constraints:** - - Partial offline availability: editing attributes, especially for planting and harvesting - - Supports alternatives - - Performance: more than 10000 elements per year and per alternative should be usable without noticeable delays and acceptable memory overhead diff --git a/doc/usecases/assigned/buy_seeds.md b/doc/usecases/assigned/buy_seeds.md index 37303476b..b13697e35 100644 --- a/doc/usecases/assigned/buy_seeds.md +++ b/doc/usecases/assigned/buy_seeds.md @@ -6,23 +6,23 @@ - **Level:** User Goal - **Actors:** App User - **Brief:** The users buys missing seeds for the planned plants. -- **Assignee:** Ready to be assigned +- **Assignee:** Paul ## Scenarios - **Precondition:** - - [Already existing seeds are entered](../current/entry_list_seeds.md) - - [Plants are planted](../current/plants_layer.md) + - [Already existing seeds are entered](../done/entry_list_seeds.md) + - [Plants are planted](../done/plants_layer.md) - The user has opened the seed buying feature in the app. - **Main success scenario:** - The user gets a suggestion which seeds to buy based on planted seeds minus existing seeds. - The seeds are grouped by seeds that are not, partially or sufficiently stocked. - The user adjusts the amount for the already existing seeds if wanted. - - After confirming the purchase, the bought seeds get added to the [seed database](../current/entry_list_seeds.md). + - After confirming the purchase, the bought seeds get added to the [seed database](../done/entry_list_seeds.md). - **Alternative scenario:** - **Error scenario:** There is an error when the user attempts to purchase the seeds. - In this case, the [seed database](../current/entry_list_seeds.md) stays unmodified and the user gets notified about the error. + In this case, the [seed database](../done/entry_list_seeds.md) stays unmodified and the user gets notified about the error. - **Postcondition:** - The [seed database](../current/entry_list_seeds.md) contains all seeds as visible in the [map](../current/plants_layer.md). + The [seed database](../done/entry_list_seeds.md) contains all seeds as visible in the [map](../done/plants_layer.md). - **Non-functional Constraints:** diff --git a/doc/usecases/assigned/calendar.md b/doc/usecases/assigned/calendar.md index 496c2d633..9db829b9d 100644 --- a/doc/usecases/assigned/calendar.md +++ b/doc/usecases/assigned/calendar.md @@ -6,7 +6,6 @@ - **Level:** User Goal - **Actors:** App User - **Brief:** The user sees which of the planned plants are to be planted next. ("Saisonübersicht") -- **Status:** Assigned - **Assignee:** Samuel ## Scenarios diff --git a/doc/usecases/assigned/contributing_member.md b/doc/usecases/assigned/contributing_member.md deleted file mode 100644 index 2ee823aab..000000000 --- a/doc/usecases/assigned/contributing_member.md +++ /dev/null @@ -1,32 +0,0 @@ -# Use Case: Contributing Member - -## Summary - -- **Scope:** Membership -- **Level:** User Goal -- **Actors:** App User -- **Brief:** The user receives a membership without compensation via money through contributions. -- **Status:** Draft -- **Assignee:** Ready to be assigned - -## Scenarios - -- **Precondition:** - - The user already has an existing membership OR - - The user has currently no membership. -- **Main success scenario:** - - The user can receive permacoins through various activities in the app, like: - - gaining [Blossoms](../current/gain_blossoms.md) in the Expert Track. - - getting likes from other users on their reviews. - - getting [approvals](../assigned/review_plant_relationships.md) on their submitted [plant relationships](../assigned/add_plant_relationships.md). - - getting their comments on a map marked as helpful by the maps owner. - - recruiting new members. - - The user can see their exact count of current and previous permacoins in their profile. - - If the user applied for the contributing membership and when reaching a certain milestone with the amount of gathered permacoins, the user will be given the current calendar year of membership without compensation via money. - - Permacoins can not be transferred from the current into another year nor be exchanged to money in any way. -- **Alternative scenario:** - - The user does not gather enough permacoins and has to pay the full price for this years membership. -- **Error scenario:** -- **Postcondition:** - - The user has the permission to use the app and to be part of the community. -- **Non-functional Constraints:** diff --git a/doc/usecases/assigned/copy_paste_selection.md b/doc/usecases/assigned/copy_paste_selection.md deleted file mode 100644 index ef1ea31de..000000000 --- a/doc/usecases/assigned/copy_paste_selection.md +++ /dev/null @@ -1,25 +0,0 @@ -# Use Case: Copy & Paste of Selection - -## Summary - -- **Scope:** All Layers, except Base -- **Level:** User Goal -- **Actors:** App User -- **Brief:** The user can copy and paste a selection of elements, including succeeding crops, in their map -- **Status:** Merge Pending https://github.com/ElektraInitiative/PermaplanT/pull/356 -- **Assignee:** Ready to be assigned - -## Scenarios - -- **Precondition:** - The user has opened the app and has made a selection of elements that they want to copy and paste. -- **Main success scenario:** - The user successfully copies and pastes the selection of elements, within the same map, between own maps or between an own map and a map of another user. -- **Alternative scenario:** - The user accidentally pastes the selection in the wrong location and uses the app's undo function to correct the mistake. -- **Error scenario:** - User attempts to copy and paste a selection but the app is experiencing technical difficulties and is unable to complete the request, displaying an error message. -- **Postcondition:** - The user's map includes the copied and pasted selection of elements. -- **Non-functional Constraints:** - - Alternatives (selected elements depend on which alternative layer is selected) diff --git a/doc/usecases/assigned/copy_paste_within_same_map.md b/doc/usecases/assigned/copy_paste_within_same_map.md new file mode 100644 index 000000000..3e24380b5 --- /dev/null +++ b/doc/usecases/assigned/copy_paste_within_same_map.md @@ -0,0 +1,61 @@ +# Use Case: Copy & Paste Within the Same Map + +## Summary + +- **Scope:** All Layers, except Base +- **Level:** User Goal +- **Actors:** App User +- **Brief:** The user can copy and paste a selection of elements, including succeeding crops, within one map. +- **Status:** Ready to be Assigned + +## Scenarios + +- **Precondition:** + The user has opened the app on a key-controlled device and, on one of his maps, made a selection of elements that he wants to copy and paste into that same map. +- **Main success scenario:** + - The user copies the selection by pressing CTRL-C. + The user clicks anywhere on that same map. + The user pastes the copied selection into the map by pressing CTRL-V. + The pasted selection of elements is placed in the map at the user's last click position right before pasting. +- **Alternative scenarios:** + - The user pastes a copied selection without having clicked anywhere else on the map after copying. + The pasted selection is placed with a horizontal and vertical offset next to the copied selection. + - The user pastes a copied selection several times in a row. + The pasted selections are each placed with a horizontal and vertical offset next to each other. + - The user pastes a copied selection while having other element(s) currently selected. + The pasted selection is placed with a horizontal and vertical offset next to the currently selected element(s). + - The user pastes a copied selection having the wrong layer selected and a warning appears advising the correct layer. + The user selects the correct layer and successfully pastes that selection of elements into it. + - The user presses CTRL-C without having anything selected. + The user presses CTRL-V and no pasting happens because no elements have been copied. + - In a step #1, the user presses CTRL-C on a selection of elements. + In a step #2, The user unselects that selection by clicking anywhere else on the map. + The user presses CTRL-C without having anything selected. + The user presses CTRL-V. + The, in step #1, copied selection of elements is pasted into the map, placed where the user clicked in step #2 to unselect the copied elements. + - The user pastes a wrongly chosen selection of elements. + The user uses the app's undo function to revert the pasting. +- **Error scenarios:** + - The user attempts to copy and paste a selection but the app is experiencing technical difficulties and is unable to complete the request, displaying an error message. +- **Postconditions:** + - The user's map contains the copied and pasted selection of elements. +- **Non-functional Constraints:** + - Alternatives (selected elements depend on which alternative layer is selected) + - Any copy & pasted element should have its own UUID in the map, i.e. it's a unique Konva node on the current canvas. + - Left-click, middle-click and right-click should be accepted for setting the target position of the next pasting. + - The shortcuts for copying and pasting (CTRL-C and CTRL-V) should be stored in a central place where future keybindings will be added too. +- **Linked Use Cases:** + - [Copy & Paste of Selection Between a User's Own Maps](../draft/copy_paste_between_own_maps.md) + - [Copy & Paste of Selection Between Maps of Different Users](../draft/copy_paste_between_users.md) + - [Copy & Paste of Selection via Icons](../draft/copy_paste_via_icons.md) + +## Development Progress + +1. (this usecase) [Copy & Paste of Selection Within the Same Map](../assigned/copy_paste_within_same_map.md) + This usecase should be done before any other _Copy & Paste_-related usecase. + It will contain the core logic of copying and pasting. +2. [Copy & Paste of Selection via Icons](../draft/copy_paste_via_icons.md) + It can reuse everything implemented in the first usecase one-to-one. + Additionally it will add the logically-isolated possibility to copy and paste via icons plus the 'visibility-toggling' of the copy- and paste-icons' design. +3. [Copy & Paste of Selection Between a User's Own Maps](../draft/copy_paste_between_own_maps.md) / [Copy & Paste of Selection Between Maps of Different Users](../draft/copy_paste_between_users.md) + Here, the storing of the copied elements will have to be moved from the app's map store to the client side, i.e. the browser's local storage. diff --git a/doc/usecases/assigned/create_layers.md b/doc/usecases/assigned/create_layers.md new file mode 100644 index 000000000..c95690cf5 --- /dev/null +++ b/doc/usecases/assigned/create_layers.md @@ -0,0 +1,22 @@ +# Use Case: Create Layers + +## Summary + +- **Scope:** Drawing and Labels Layers +- **Level:** User Goal +- **Actors:** App User +- **Brief:** User can create new drawing and labels layers +- **Assignee:** Daniel + +## Scenarios + +- **Precondition:** + - User has opened the app. +- **Main success scenario:** + - The user can create a new drawing or labels layer. +- **Alternative scenario:** +- **Error scenario:** + - The user attempts to create the layer but the app is experiencing technical difficulties, e.g. too many layers were already created, and is unable to complete the request, displaying an error message. +- **Postcondition:** + - This new layer is created, will get a new subsequent number and is enabled. +- **Non-functional Constraints:** diff --git a/doc/usecases/assigned/diversity_score.md b/doc/usecases/assigned/diversity_score.md deleted file mode 100644 index 930f962d9..000000000 --- a/doc/usecases/assigned/diversity_score.md +++ /dev/null @@ -1,29 +0,0 @@ -# Use Case: Diversity Score - -## Summary - -- **Scope:** Diversity Score -- **Level:** User Goal -- **Actors:** App User -- **Brief:** A users map gets a diversity score depending on his usage of diversity and polyculture. -- **Assignee** Ready to be assigned - -## Scenarios - -- **Precondition:** - - The user has opened the app and is editing a map. -- **Main success scenario:** - - The user drags and drops various plants onto his map. - - The system calculates a diversity score after each plant operation based on how diverse the map is. - - Plants beneficial to the ecosystem (e.g. they are not problematic or provide food and shelter for wild life) or having many different plants increase the score, - while overusing a small set of plants (in relation to size of map) negatively affects the scoring. - - The user will be motivated to increase the diversity score and therefore optimizes the diversity and usage of polycultures on the map. -- **Alternative scenario:** -- **Error scenario:** - - The app incorrectly calculates the diversity score. - In this case the app will re-calculate the score after the next operation the user makes while the score is visible. -- **Postcondition:** - - The user finds the diversity score in the map overview and the map planner. - - The diversity score affects the order of the results in the [map search](../current/map_search.md). -- **Non-functional Constraints:**- - - Performance diff --git a/doc/usecases/assigned/drawing_layer.md b/doc/usecases/assigned/drawing_layer.md deleted file mode 100644 index 5d0a5bf45..000000000 --- a/doc/usecases/assigned/drawing_layer.md +++ /dev/null @@ -1,27 +0,0 @@ -# Use Case: Drawing Layer - -## Summary - -- **Scope:** Drawing Layer -- **Level:** User Goal -- **Actors:** App User -- **Brief:** User can add, edit, move, remove and delete custom drawings of any shape and color in their map in the drawing layer. -- **Assignee:** Giancarlo - -## Scenarios - -- **Precondition:** - The user has opened the app and has selected the drawing layer. -- **Main success scenario:** - - The user successfully adds, edits, moves, removes and deletes custom drawings of any form and color in the drawing layer. -- **Alternative scenario:** - - The user accidentally edits, moves or removes an element and uses undo to correct the mistake. - - The user accidentally adds an element and deletes it with the "delete" or "undo" functionality. -- **Error scenario:** - The user attempts to add, move or edit a drawing but the app is experiencing technical difficulties and is unable to complete the request, displaying an error message. -- **Postcondition:** - The user's map includes the added, edited, moved, removed or deleted drawings as desired. -- **Non-functional Constraints:** - - Offline availability - - New Layers can be created. - - Performance: at least 1000 elements per year per drawing layer should be usable without noticeable delays and acceptable memory overhead diff --git a/doc/usecases/assigned/event_notfication.md b/doc/usecases/assigned/event_notfication.md deleted file mode 100644 index 427cf0df1..000000000 --- a/doc/usecases/assigned/event_notfication.md +++ /dev/null @@ -1,29 +0,0 @@ -# Use Case: Event Notification - -## Summary - -- **Scope:** Notifications -- **Level:** User Goal -- **Actors:** - - Host: App User that hosts the event - - Nearby user: App User that is nearby the host - - User without location: App User that has no location -- **Brief:** The user gets a notification when another user has an event. -- **Assignee:** Ready to be assigned - -## Scenarios - -- **Precondition:** - - The host has opened the app and is logged in. - - The host and the nearby user are located nearby (requires location settings of both users, nearby is a defined radius). -- **Main success scenario:** - - The host announces an event (e.g. a "virtual welcome party" because planning of the map was finished) -- **Alternative scenario:** -- **Error scenario:** - The host does not have a location set: An error message is displayed, - which informs them that the location is required for sending invitations. -- **Postcondition:** - - Nearby user gets a notification which informs them about the event (e.g. invitation to visit the map virtually). - - The user without location gets a notification which informs them about the event (e.g. invitation to visit the map virtually) - with the additional text that they get the invitation because they don't have a location set. -- **Non-functional Constraints:** diff --git a/doc/usecases/assigned/experimental_results.md b/doc/usecases/assigned/experimental_results.md deleted file mode 100644 index 2fecf060e..000000000 --- a/doc/usecases/assigned/experimental_results.md +++ /dev/null @@ -1,26 +0,0 @@ -# Use Case: Experimental Results - -## Summary - -- **Scope:** Experimental Results -- **Level:** User Goal -- **Actors:** App User -- **Brief:** The user can give a feedback of the actual results for a map in a measuring cycle. -- **Assignee** Ready to be assigned - -## Scenarios - -- **Precondition:** - - The user has opened the app. - - The user has previously fully designed a map and implemented it in real life. -- **Main success scenario:** - - The user gets a number of simple recommended methods to measure amount and quality of their harvest, including a step-to-step guide to execute them. - - The user performs one or more of these methods or conducts their own advanced analysis. - - The user can input the results of the analysis in a feedback form of the map. - - The data gets saved and is used to adapt the parameters of the used entities in this map. -- **Alternative scenario:** -- **Error scenario:** - A technical error occurs, preventing the user from submitting the feedback form. In this case the system should display an error message and allow the user to try again. -- **Postcondition:** - The accumulated data from this and previous measuring cycles can be viewed as a report in a separate details screen of the map. -- **Non-functional Constraints:** diff --git a/doc/usecases/assigned/fertilization_layer.md b/doc/usecases/assigned/fertilization_layer.md index c89bd4ef7..844e2f0e8 100644 --- a/doc/usecases/assigned/fertilization_layer.md +++ b/doc/usecases/assigned/fertilization_layer.md @@ -15,6 +15,7 @@ - **Main success scenario:** - The user successfully adds, edits, moves and removes fertilizers in their map in the fertilization layer. - Fertilizers can be drawn using brushes of different sizes. + - The user can write a note which fertilization was used. - **Alternative scenario:** - The user accidentally edits, moves or removes an element and uses undo to correct the mistake. - The user accidentally adds an element and deletes it with the "delete" or "undo" functionality. diff --git a/doc/usecases/assigned/geo_map.md b/doc/usecases/assigned/geo_map.md deleted file mode 100644 index f61bbb6a3..000000000 --- a/doc/usecases/assigned/geo_map.md +++ /dev/null @@ -1,23 +0,0 @@ -# Use Case: GeoMap - -## Summary - -- **Scope:** GeoMap -- **Level:** User Goal -- **Actors:** App User -- **Brief:** The user can view sites/maps on a GeoMap -- **Assignee:** Samuel - -## Scenarios - -- **Precondition:** - - The user has navigated to the landing page. -- **Main success scenario:** - - The user can see maps of other users indicated by markers - - The user can view a picture of the map by clicking on a marker - - The user can view the link to a map by clicking on the marker - - The user can navigate to the map after clicking the corresponding link (if logged in) -- **Alternative scenario:** -- **Error scenario:** -- **Postcondition:** -- **Non-functional Constraints:** diff --git a/doc/usecases/assigned/habitats_layer.md b/doc/usecases/assigned/habitats_layer.md deleted file mode 100644 index c46bc8d1b..000000000 --- a/doc/usecases/assigned/habitats_layer.md +++ /dev/null @@ -1,27 +0,0 @@ -# Use Case: Habitats Layer - -## Summary - -- **Scope:** Habitats Layer -- **Level:** User Goal -- **Actors:** App User -- **Brief:** The user can add, edit, move, removed and delete habitats of animals to support their plants and ecosystem in their map in the habitats layer. - -## Scenarios - -- **Precondition:** - The user has opened the app and has selected the habitats layer. -- **Main success scenario:** - - The user successfully adds, edits, moves, removes and deletes aids in their map in the habitats layer. - For example: nesting aids, heaps of stones or leaves, perches etc. - This includes positioning the aids in the appropriate location. - - Habitats from deers and domesticated animals (like ducks and chicken) can be added by a big brush to draw on the habitats layer. -- **Alternative scenario:** - - The user accidentally edits, moves or removes an element and uses undo to correct the mistake. - - The user accidentally adds an element and deletes it with the "delete" or "undo" functionality. -- **Error scenario:** - The user attempts to add, move or edit an aid but the app is experiencing technical difficulties and is unable to complete the request, displaying an error message. -- **Postcondition:** - The user's map includes the added, edited, moved, removed or deleted habitats as desired. -- **Non-functional Constraints:** - - Performance: Map sizes with more than 1ha in 10000 raster elements (m²) and more than 500 elements per year should be usable without noticeable delays and acceptable memory overhead diff --git a/doc/usecases/assigned/hydrology_layer.md b/doc/usecases/assigned/hydrology_layer.md index 9bea2b325..b949f85d6 100644 --- a/doc/usecases/assigned/hydrology_layer.md +++ b/doc/usecases/assigned/hydrology_layer.md @@ -6,6 +6,8 @@ - **Level:** User Goal - **Actors:** App User - **Brief:** The user can add, edit, move, removed and delete moisture or dry areas in their map in the moisture layer. +- **Assignee:** Daniel +- **Note:** Lowest priority of the layers ## Scenarios diff --git a/doc/usecases/assigned/incomplete_groups.md b/doc/usecases/assigned/incomplete_groups.md deleted file mode 100644 index bb032a404..000000000 --- a/doc/usecases/assigned/incomplete_groups.md +++ /dev/null @@ -1,27 +0,0 @@ -# Use Case: Incomplete Groups - -## Summary - -- **Scope:** Plants Layer -- **Level:** User Goal -- **Actors:** App User -- **Brief:** The user is informed about what plant groups need more planning. -- **Status:** Assigned -- **Assignee:** Gabriel - -## Scenarios - -- **Precondition:** - - The user has opened the app - - The plants layer is visible - - At least one plant is placed - - At least to one of the placed plants a companion plant can be added. -- **Main success scenario:** - - Groups, where further companions are available, are visually highlighted. -- **Alternative scenario:** - - No groups have further companions available. - This is visually indicated. -- **Error scenario:** -- **Postcondition:** - - The user knows which groups need more attention. -- **Non-functional Constraints:** diff --git a/doc/usecases/assigned/infrastructure_layer.md b/doc/usecases/assigned/infrastructure_layer.md deleted file mode 100644 index 7b511b8a7..000000000 --- a/doc/usecases/assigned/infrastructure_layer.md +++ /dev/null @@ -1,34 +0,0 @@ -# Use Case: Infrastructure Layer - -## Summary - -- **Scope:** Infrastructure Layer -- **Level:** User Goal -- **Actors:** App User -- **Brief:** The user can add, edit, move and delete infrastructure elements in their plan. - -## Scenarios - -- **Precondition:** - The user has opened the app and has selected the infrastructure layer. -- **Main success scenario:** - The user successfully adds, edits, moves and deletes infrastructure elements in the infrastructure layer. - Infrastructure elements are, e.g.: - - outlets - - wifi spots - - taps - - water storage tanks - - irrigation systems - For placement: - - positioning the elements in the appropriate locations - - adjusting their properties - as needed. -- **Alternative scenario:** - - The user accidentally edits, moves or removes an element and uses undo to correct the mistake. - - The user accidentally adds an element and deletes it with the "delete" or "undo" functionality. -- **Error scenario:** - The user attempts to add, move or edit an infrastructure element but the app is experiencing technical difficulties and is unable to complete the request, displaying an error message. -- **Postcondition:** - The user's map includes the added, edited, moved or deleted infrastructure elements as desired. -- **Non-functional Constraints:** - - Performance: more than 500 elements per year should be usable without noticeable delays and acceptable memory overhead diff --git a/doc/usecases/assigned/ingredient_lists.md b/doc/usecases/assigned/ingredient_lists.md deleted file mode 100644 index 6883454f4..000000000 --- a/doc/usecases/assigned/ingredient_lists.md +++ /dev/null @@ -1,28 +0,0 @@ -# Use Case: Ingredient Lists - -## Summary - -- **Scope:** Ingredient Lists -- **Level:** User Goal -- **Actors:** App User -- **Brief:** The user can create lists to keep track of ingredients needed over the year. -- **Status:** Draft - -## Scenarios - -- **Precondition:** - - The user has opened the app and is editing a map. -- **Main success scenario:** - - The user has a list of fruits and vegetables that are needed continuously over the course of a timespan, e.g. a daily smoothie or salad. - - The user inputs the items into a form in the map editor and can set a name and picture for the ingredient list. - - The user is able to have multiple of those ingredient lists. - - The lists will display what ingredients are available on the map and which still need to be planted by the user. - - While harvesting or after harvesting (if the user has conserved some of it, e.g., stored fresh fruits, dried herbs), (s)he can mark ingredients as available over a specific timespan and unmark it once the harvest is over or storage is exhausted. - - The user is awarded a Blossom for having all necessary ingredients for any list in any month and additional ones for every subsequent month the list requirements are completely met. -- **Alternative scenario:** -- **Error scenario:** - The app does not mark a list item as (partially) harvested even though the user marked the relevant plant as harvested. - In this case, the user should try again to mark the plant as harvested to re-initialize the ingredient list check. -- **Postcondition:** - The user can keep track of needed ingredients and gets an overview of plants to plant on the map. -- **Non-functional Constraints:** diff --git a/doc/usecases/assigned/label_layer.md b/doc/usecases/assigned/label_layer.md index d0965fecf..c7ea3381d 100644 --- a/doc/usecases/assigned/label_layer.md +++ b/doc/usecases/assigned/label_layer.md @@ -6,7 +6,7 @@ - **Level:** User Goal - **Actors:** App User - **Brief:** The user adds labels to their map to identify different elements or areas -- **Assignee:** Giancarlo +- **Assignee:** Daniel ## Scenarios diff --git a/doc/usecases/assigned/landing_page.md b/doc/usecases/assigned/landing_page.md deleted file mode 100644 index 2b9cff704..000000000 --- a/doc/usecases/assigned/landing_page.md +++ /dev/null @@ -1,31 +0,0 @@ -# Use Case: Landing Page - -## Summary - -- **Scope:** Landing Page -- **Level:** User Goal -- **Actors:** App User -- **Brief:** The user can view sites/maps on a GeoMap and apply for a membership -- **Assignee:** Thorben - -## Scenarios - -- **Precondition:** - - The user has navigated to [permaplant.net](https://permaplant.net). -- **Main success scenario:** - - The user can interact with the displayed GeoMap - - The user can apply for a membership - - The user can contact us - - The user can read imprint (Impressum) - - The user can read about privacy (Datenschutz) - - The user can read about copyright - - The user can read latest [news and get an RSS/Atom feed of it](../assigned/landing_page_news.md) - - The user can login -- **Alternative scenario:** -- **Error scenario:** -- **Postcondition:** -- **Non-functional Constraints:** -- **Linked Use Cases:** - - [Membership Application](../assigned/membership_application.md) - - [GeoMap](../assigned/geo_map.md) - - [Login](../done/login.md) diff --git a/doc/usecases/assigned/landing_page_news.md b/doc/usecases/assigned/landing_page_news.md deleted file mode 100644 index 21061b07c..000000000 --- a/doc/usecases/assigned/landing_page_news.md +++ /dev/null @@ -1,36 +0,0 @@ -# Use Case: Landing Page News - -## Summary - -- **Scope:** Landing Page -- **Level:** User Goal -- **Actors:** App User -- **Brief:** The user can read news written in a Nextcloud chat component -- **Assignee:** Samuel - -## Scenarios - -- **Precondition:** - - The user has navigated to [permaplant.net](https://permaplant.net). -- **Main success scenario:** - The chat should look very similar to Nextcloud chat: - - highlight items when hovering - - group by date, show exact time (not 5 min ago) - Logged in users can: - - give reactions to posts - Chat features (in order of importance) - 1. show images - 2. text should be mark/copyable - 3. emoji - 4. reactions - 5. files - 6. reply - 7. directly share photo (take photo with phone) - 8. remove msg/reaction -- **Alternative scenario:** - - The user can subscribe the same content (chat) as RSS feed -- **Error scenario:** -- **Postcondition:** -- **Non-functional Constraints:** -- **Extends Use Case:** - - [Landing Page](../assigned/landing_page.md) diff --git a/doc/usecases/assigned/landscape_layer.md b/doc/usecases/assigned/landscape_layer.md index 9787e0eac..c22b3252d 100644 --- a/doc/usecases/assigned/landscape_layer.md +++ b/doc/usecases/assigned/landscape_layer.md @@ -6,20 +6,22 @@ - **Level:** User Goal - **Actors:** App User - **Brief:** User adds elements to their map using hand-drawn shapes or common shapes. -- **Assignee:** Moritz +- **Assignee:** Daniel ## Scenarios - **Precondition:** User has opened the app and selected the landscape layer. - **Main success scenario:** User successfully adds elements to their map using hand-drawn shapes or common shapes such as circles, ellipses, squares, and rectangles. - Element types include: + Element types include (icons from Nextcloud): - wall - plain - pond - (glas)houses - barns - plant beds + - circles + - doors - **Alternative scenario:** User accidentally adds a shape in the wrong location and uses the app's undo function to correct the mistake. - **Error scenario:** diff --git a/doc/usecases/assigned/layers_alternatives.md b/doc/usecases/assigned/layers_alternatives.md deleted file mode 100644 index 53bc75c50..000000000 --- a/doc/usecases/assigned/layers_alternatives.md +++ /dev/null @@ -1,36 +0,0 @@ -# Use Case: Layer Alternatives - -## Summary - -- **Scope:** All Layers -- **Level:** User Goal -- **Actors:** App User -- **Brief:** User selects and modifies alternatives of a layer -- **Status:** Assigned -- **Assignee:** Christoph (Database) - -## Scenarios - -- **Precondition:** - User has opened the app and a map. -- **Main success scenario:** - - - User chooses a layer to enable - - User duplicates the layer - - User gives the layer a name - - User modifies the layer - - User may select another alternative of the layer. - This change doesn't affect layer selections of other users (that work on the same map). - -- **Alternative scenario:** - - User accidentally duplicates the layer and deletes the duplicate with "delete layer" functionality - - User accidentally selects a wrong alternative as the current layer and undoes the action by selecting the correct layer -- **Error scenario:** - - If the user encounters technical issues or errors while using the layer alternatives, the platform should display an error message and allows the user to try again. -- **Postcondition:** - The user has successfully modified and selected an alternative of a layer. -- **Non-functional Constraints:** - - Performance: up to 10 alternatives should be fast to use - - Offline functionality depending on the layer -- **Note:** - - Layers that support alternatives, have this specified as **non-functional constraint**. diff --git a/doc/usecases/assigned/map_collaboration.md b/doc/usecases/assigned/map_collaboration.md index f17c2389d..9f9b6562d 100644 --- a/doc/usecases/assigned/map_collaboration.md +++ b/doc/usecases/assigned/map_collaboration.md @@ -6,7 +6,8 @@ - **Level:** User Goal - **Actors:** Map Creator, Collaborator - **Brief:** Collaborators plan on the map of a map creator. -- **Status:** Ready to be Assigned +- **Simplification:** Without alternatives. +- **Assignee:** Paul ## Scenarios @@ -16,7 +17,7 @@ - The map creator is able to open the map for collaboration. - The map creator chooses if the map is open to edit for every registered user or only through directly assigned permissions, in which case potential collaborators can be searched by their name and get an invitation to collaboration. - - The map creator can create [alternatives](../assigned/layers_alternatives.md) of existing layers and designate which can be edited by collaborators. + - The map creator can create [alternatives](../draft//layers_alternatives.md) of existing layers and designate which can be edited by collaborators. - Additionally, the map creator can leave a comment with additional information for collaborators, like e.g. a list of specific plants the map should contain. - A collaborator, optionally with direct permission if specified, can enter the map like it is one of their own. diff --git a/doc/usecases/assigned/map_deletion.md b/doc/usecases/assigned/map_deletion.md index 04652fae7..7cd91d3a2 100644 --- a/doc/usecases/assigned/map_deletion.md +++ b/doc/usecases/assigned/map_deletion.md @@ -6,7 +6,7 @@ - **Level:** User Goal - **Actors:** App User - **Brief:** The user deletes a map. -- **Assignee:** Thorben +- **Assignee:** Moritz ## Scenarios diff --git a/doc/usecases/assigned/map_honors.md b/doc/usecases/assigned/map_honors.md deleted file mode 100644 index eaee57c43..000000000 --- a/doc/usecases/assigned/map_honors.md +++ /dev/null @@ -1,29 +0,0 @@ -# Use Case: Map Honors - -## Summary - -- **Scope:** Map Honors -- **Level:** User Goal -- **Actors:** App User -- **Brief:** The users can give an honor to the whole map of another user they visit. -- **Assignee** Ready to be assigned - -## Scenarios - -- **Precondition:** - - The user has opened the app. - - The user is currently viewing the map of another user. -- **Main success scenario:** - - The user wants to show appreciation for the entirety of the currently visited map. - - The user uses the honor button to give an honor directly to the entire map. - - The amount of honors for that specific map increases. -- **Alternative scenario:** - - The user uses the honor button to withdraw a previously given honor on the currently visited map. - - The amount of honors for that specific map decreases. -- **Error scenario:** - A network error occurs, preventing the app from updating the honor count of the given map. - In this case, a warning message stating the error should be displayed and the user is prompted to try again. -- **Postcondition:** - Users can find the honor count next to the map in every instance of map selection as well as in the details screen of the map. - The amount of honors affects the order of the results in the [map search](../current/map_search.md). -- **Non-functional Constraints:** diff --git a/doc/usecases/assigned/map_specific_favorite_groups.md b/doc/usecases/assigned/map_specific_favorite_groups.md deleted file mode 100644 index c0a2709cf..000000000 --- a/doc/usecases/assigned/map_specific_favorite_groups.md +++ /dev/null @@ -1,29 +0,0 @@ -# Use Case: Map-Specific Favorite Groups - -## Summary - -- **Scope:** Plants Layer -- **Level:** User Goal -- **Actors:** App User -- **Brief:** The user changes a set of favorite plant groups per map. -- **Assignee:** Ready to be assigned - -## Scenarios - -- **Precondition:** - - The user is logged in to the app. - - The plants layer is selected. - - At least one plant is placed. -- **Main success scenario:** - - The user adds a plant or a plant group to her list of favorites. -- **Alternative scenario:** - - The user wants to reorder their list of favorite plant groups. - - The user wants to remove favorite plant groups. -- **Error scenario:** - - The user attempts to add a plant group that is already in their favorites. - - The user attempts to remove a group that is not in the favorites. -- **Postcondition:** - - The set of map-specific favorites has changed according to the changes the user made. - The state of these favorites can be seen in the plants layer. -- **Non-functional Constraints:** - - It must be clear that a favorite was added or removed. diff --git a/doc/usecases/assigned/map_statistics.md b/doc/usecases/assigned/map_statistics.md deleted file mode 100644 index 4602ac32f..000000000 --- a/doc/usecases/assigned/map_statistics.md +++ /dev/null @@ -1,27 +0,0 @@ -# Use Case: Map Statistics - -## Summary - -- **Scope:** Map Statistics -- **Level:** User Goal -- **Actors:** App User -- **Brief:** The user can view statistics about their garden and plants. -- **Assignee:** Ready to be assigned - -## Scenarios - -- **Precondition:** - The user has opened the app and has opened their map. -- **Main success scenario:** - The user views statistics about their garden and plants. - Examples of statistics include: - - Total number of plants in the garden - - Number of elements in each layer - - Total number of visits to the garden -- **Alternative scenario:** -- **Error scenario:** - - If the user encounters technical issues or errors while using the statistics, the platform should display an error message and allow the user to try again. -- **Postcondition:** - The user has successfully viewed statistics about their garden and plants. -- **Non-functional Constraints:** -- Alternatives (statistics depend on which alternative layers are selected) diff --git a/doc/usecases/assigned/map_timeline_event_view.md b/doc/usecases/assigned/map_timeline_event_view.md deleted file mode 100644 index 5b82d3c3a..000000000 --- a/doc/usecases/assigned/map_timeline_event_view.md +++ /dev/null @@ -1,34 +0,0 @@ -# Use Case: Map Timeline Event View - -## Summary - -- **Scope:** Map View -- **Level:** User Goal -- **Actors:** App User -- **Brief:** The user sees addition/removal of elements as events using the timeline feature. -- **Assignee:** Ready to be assigned - -## Scenarios - -- **Precondition:** - The user has opened the app and selected a map. -- **Main success scenario:** - - - The user sees events represented on the scrollbar of the timeline feature, allowing them to understand how the map changed over time. - Events include: - - addition of an element to the map - - deletion of an element from the map - - The timeline can be scaled, adjusting its start and end date, thus showing more events of the future and the past. - - The timeline can be filtered to only show one kind of event, e.g. only events related to additions of elements. - -- **Alternative scenario:** - - The user has not yet added/removed any elements, therefore no events are visible on the timeline. - - The timeline is scaled to encompass +/- three years, and the center is this present day. - - If no events are visible, a message is displayed that no events could be loaded. -- **Error scenario:** - There is an error in the timeline display or event loading functionality. - In this case, the app displays an error message and allows the user to try again. -- **Postcondition:** - The user successfully sees the events on the timeline scrollbar. -- **Non-functional Constraints:** - - Offline availability diff --git a/doc/usecases/assigned/map_timeline_range_selection.md b/doc/usecases/assigned/map_timeline_range_selection.md deleted file mode 100644 index 203b4162b..000000000 --- a/doc/usecases/assigned/map_timeline_range_selection.md +++ /dev/null @@ -1,34 +0,0 @@ -# Use Case: Map Timeline Range Selection - -## Summary - -- **Scope:** Map View -- **Level:** User Goal -- **Actors:** App User -- **Brief:** The user can view the map over a range of consecutive points in time by using the timeline feature. -- **Assignee:** Ready to be assigned - -## Scenarios - -- **Precondition:** - The user has opened the app and selected a map. -- **Main success scenario:** - - The user selects the timeline view and uses the scroll bar with a day/month/year granularity, or a date input that allows date range selection. - - Using the scroll bar, the user clicks and drags the mouse to span a range of dates. - - The user can only drag the selection up to the furthest event that happened in the past or the future. - - Dragging over the scroll bar hints which day/month/year represents the start and which the end of the selected date range. - - Finalizing the date selection updates the map to show the state of the garden over a range of points in time. - - Elements on the map that do not exist over the whole range of dates, appear grey. - - Elements on the map can be edited/moved/deleted, but not be added or removed. -- **Alternative scenario:** -- **Error scenario:** - - There is an error in the timeline display or navigation functionality. - In this case, the app displays an error message and allows the user to try again. - - The user tries to add/remove elements while being in the active date range. - In this case, the app displays an error message that adding/removing elements is not allowed. -- **Postcondition:** - The user successfully sees the map containing the elements in the desired range of dates. -- **Non-functional Constraints:** - - Offline availability only within the current year - - Memory usage (other years get unloaded after some time if they are not used) - - Performance diff --git a/doc/usecases/assigned/matchmaking.md b/doc/usecases/assigned/matchmaking.md deleted file mode 100644 index af5ee4a51..000000000 --- a/doc/usecases/assigned/matchmaking.md +++ /dev/null @@ -1,27 +0,0 @@ -# Use Case: Matchmaking - -## Summary - -- **Scope:** Matchmaking -- **Level:** User Goal -- **Actors:** App User -- **Brief:** The users can find other gardening enthusiasts to establish a gardening partnership. -- **Assignee:** Ready to be assigned - -## Scenarios - -- **Precondition:** - The user has opened the app and is on the matchmaking page. -- **Main success scenario:** - - The user creates a profile that includes their gardening preferences and experience. - - The user searches for other gardening enthusiasts. - - The user sends a request to potential gardening partners to discuss their gardening partnership. - - The user and their potential gardening partner successfully establish a gardening partnership. -- **Alternative scenario:** - - If the user is not satisfied with the results of their search, they can adjust their search parameters and try again. -- **Error scenario:** - - If the user encounters technical issues or errors while using the matchmaking, the platform should display an error message and allow the user to try again. -- **Postcondition:** - The user has successfully found gardening partners through the app and has established a successful gardening partnership. -- **Non-functional Constraints:** - - The matchmaking should also protect user privacy and data, including user profiles and messaging history. diff --git a/doc/usecases/assigned/measuring_distance.md b/doc/usecases/assigned/measuring_distance.md index 42156e62f..11a4f2a85 100644 --- a/doc/usecases/assigned/measuring_distance.md +++ b/doc/usecases/assigned/measuring_distance.md @@ -4,10 +4,10 @@ - **Scope:** All Layers - **Level:** User Goal -- **Actors:** App User +- **Actors:** User, App - **Brief:** The user can measure the distance between different elements in their map. - **Status:** Assigned -- **Simplification:** Measuring only of pixels (not via backend as described below) +- **Simplification:** Measuring only of pixels (not via backend as described in `Non-functional Constraints`) - **Assignee:** Moritz ## Scenarios diff --git a/doc/usecases/assigned/membership_application.md b/doc/usecases/assigned/membership_application.md deleted file mode 100644 index 2594d55aa..000000000 --- a/doc/usecases/assigned/membership_application.md +++ /dev/null @@ -1,58 +0,0 @@ -# Use Case: Membership Application - -## Summary - -- **Scope:** Membership -- **Level:** User Goal -- **Actors:** User (Membership Applicant), Administrator (Chairman) -- **Brief:** The user can apply for a membership (account) -- **Assignee:** Thorben (Frontend), Lukas (Keycloak Installation) - -## Scenarios - -- **Precondition:** - The user has opened the app and is not logged in. -- **Main success scenario:** - 1. The user can click an register button which leads them to Keycloak to fill out: - - username - - first+last name - - email address - - birthday - - biography - 2. The user can look at public maps (read-only and without seeing addresses!) - 3. The user apply for membership, with several steps: - 1. Which kind of membership is selected on landing page and "Apply for membership" clicked. - Can also be clicked without being registered before, then Step 1. of Main success scenario needs to be done. - 2. form page 1 - - salutation (Anrede) - - title (Title) (optional) - - billing address (Rechnungsadresse) - - country (Land) - - additional emails (E-Mails) (optional) - - telephone number (optional) - - website (optional) - - organisation (optional) - - permaculture experience (optional) - - user photo (uploaded and also used in Nextcloud) - 3. form page 2 - - if billing address or someone else's address should be used - - one or several photos of the site - - description of the site - - location of map(s) (not shown to default-roles-permaplant users) - 4. form page 3 - - owner (KontoinhaberIn) - - IBAN - - BIC - - how did you get to know about PermaplanT (optional text) - - username for who recruited (angeworben) (optional) - - privacy - 4. An admin: - - removes the bank account data - - assigns the "member" role in Keycloak - - changes quota in Nextcloud - 5. The user gets a notification via email -- **Alternative scenario:** -- **Error scenario:** -- **Postcondition:** - - The user can enjoy all of PermaplanT's features. -- **Non-functional Constraints:** diff --git a/doc/usecases/assigned/new_member_notification.md b/doc/usecases/assigned/new_member_notification.md index e39126b8f..2d59f83c9 100644 --- a/doc/usecases/assigned/new_member_notification.md +++ b/doc/usecases/assigned/new_member_notification.md @@ -8,7 +8,8 @@ - App User which is newly registered: "new user" - App User which existed beforehand: "existing user" - **Brief:** The user gets a notification when a new user joined. -- **Assignee:** Ready to be assigned +- **Assignee:** Samuel +- **Simplification:** Every user gets notified for every new member. ## Scenarios diff --git a/doc/usecases/assigned/nextcloud_circles.md b/doc/usecases/assigned/nextcloud_circles.md index 53ff039eb..52dad8fea 100644 --- a/doc/usecases/assigned/nextcloud_circles.md +++ b/doc/usecases/assigned/nextcloud_circles.md @@ -6,7 +6,6 @@ - **Level:** User Goal - **Actors:** App User - **Brief:** The user can communicate with members of shared maps. -- **Status:** Assigned - **Assignee:** Samuel ## Scenarios diff --git a/doc/usecases/assigned/offline.md b/doc/usecases/assigned/offline.md deleted file mode 100644 index 87f4d643c..000000000 --- a/doc/usecases/assigned/offline.md +++ /dev/null @@ -1,39 +0,0 @@ -# Use Case: Offline - -## Summary - -- **Scope:** Notifications -- **Level:** User Goal -- **Actors:** - - User: A user who wants to work offline. - Must have the permissions to lock. - - Other user: Other users who have write access to the same layers in the map - - Owner of the map. -- **Brief:** The user has some features offline while being in the garden without Internet access. -- **Assignee:** Paul - -## Scenarios - -- **Precondition:** - - The user has opened the app and is logged in. -- **Main success scenario:** - - The user presses an "offline" button. (1) - - Then the browser actually can be taken offline. (2) - - The user goes to the garden. - - The map can be edited on the layers that are marked suitable for offline functionality. - - The user comes back from the garden. - - After the work, the browser gets online. (3) - - The user presses the "offline" button again. (4) -- **Alternative scenario:** - - After a 8h timeout or if the owner of the map decides, the lock can be removed. - In this case, all data from the offline work is lost. -- **Error scenario:** - Could not go offline or online: An error message is displayed. -- **Postcondition:** - - All changes while being online are transferred. -- **Non-functional Constraints:** - It is clearly visible in which state the browser is: - 1. before going offline (browser is online) - 2. offline - 3. prepared for going online again (browser is online) - 4. online diff --git a/doc/usecases/assigned/paths_layer.md b/doc/usecases/assigned/paths_layer.md deleted file mode 100644 index 1b26b4c34..000000000 --- a/doc/usecases/assigned/paths_layer.md +++ /dev/null @@ -1,27 +0,0 @@ -# Use Case: Paths Layer - -## Summary - -- **Scope:** Paths Layer -- **Level:** User Goal -- **Actors:** App User -- **Brief:** The user can add, edit, move, remove and delete a connected network of paths and fences in their map in the paths layer. - -## Scenarios - -- **Precondition:** - The user has opened the app and has selected the paths layer. -- **Main success scenario:** - The user successfully adds, edits, moves, removes and deletes a connected network of paths and fences in their map in the paths layer with: - - the chosen path thickness (small way, ... large road) - - the chosen type (stepping stones, wood chips, gravel, sealed) - This includes positioning the connected network of paths and fences in the appropriate location. -- **Alternative scenario:** - - The user accidentally edits, moves or removes an element and uses undo to correct the mistake. - - The user accidentally adds an element and deletes it with the "delete" or "undo" functionality. -- **Error scenario:** - The user attempts to add, move or edit a connected network of paths and fences but the app is experiencing technical difficulties and is unable to complete the request, displaying an error message. -- **Postcondition:** - The user's map includes the added, edited, moved, removed or deleted connected network of paths and fences as desired. -- **Non-functional Constraints:** - - Performance: more than 100 elements per year should be usable without noticeable delays and acceptable memory overhead diff --git a/doc/usecases/assigned/plant_lore.md b/doc/usecases/assigned/plant_lore.md deleted file mode 100644 index a1d0ab3b8..000000000 --- a/doc/usecases/assigned/plant_lore.md +++ /dev/null @@ -1,24 +0,0 @@ -# Use Case: Plant Lore - -## Summary - -- **Scope:** Plant Lore -- **Level:** User Goal -- **Actors:** App User -- **Brief:** The user gets shown information of plants recommended for diversity. -- **Assignee** Ready to be assigned - -## Scenarios - -- **Precondition:** - - The user has opened the app and is editing a map. -- **Main success scenario:** - - The user hovers over the [for diversity recommended plant](../current/plants_layer.md). - - The user is shown a little box with additional information and interesting facts about that plant. - - The user is interested in the plant and places it on the map. -- **Alternative scenario:** -- **Error scenario:** - The app incorrectly displays the info box on the recommended plant. - In this case, the user should retry hovering over the plant to display the info box. -- **Postcondition:** -- **Non-functional Constraints:** diff --git a/doc/usecases/assigned/rename_layers.md b/doc/usecases/assigned/rename_layers.md deleted file mode 100644 index f289faccc..000000000 --- a/doc/usecases/assigned/rename_layers.md +++ /dev/null @@ -1,26 +0,0 @@ -# Use Case: Rename Layers - -## Summary - -- **Scope:** Created or Alternative Layers -- **Level:** User Goal -- **Actors:** App User -- **Brief:** User renames created or alternative layers -- **Status:** Assigned -- **Assignee:** Giancarlo - -## Scenarios - -- **Precondition:** - - User has opened the app and has at least one created or alternative layer. -- **Main success scenario:** - User successfully renames created or alternative layers by selecting the desired layer and entering a new name. -- **Alternative scenario:** - - The user accidentally renames a layer to a name that is already in use and uses the app's undo function to correct the mistake. -- **Error scenario:** - - The user attempts to rename the layer but the app is experiencing technical difficulties and is unable to complete the request, displaying an error message. - - The user attempts to rename the layer to a name that is already in use and the app is unable to complete the request, displaying an error message. - - The user attempts to rename the layer by entering a name that is longer than the maximum allowed length and the app is unable to complete the request, displaying an error message. -- **Postcondition:** - The user has changed the name of the selected layer. -- **Non-functional Constraints:** diff --git a/doc/usecases/assigned/review_plant_relationships.md b/doc/usecases/assigned/review_plant_relationships.md deleted file mode 100644 index d208e4766..000000000 --- a/doc/usecases/assigned/review_plant_relationships.md +++ /dev/null @@ -1,29 +0,0 @@ -# Use Case: Review Plant Relationships - -## Summary - -- **Scope:** Plants Layer -- **Level:** User Goal -- **Actors:** Reviewing User, Reviewed User, Administrator -- **Brief:** The quality of a user-created plant relationship is assessed by others. -- **Status:** Assigned -- **Assignee:** Benjamin - -## Scenarios - -- **Precondition:** - - The reviewing user is logged into the app. - - She is looking at a [plant relationship created by another user](add_plant_relationships.md), the reviewed user. -- **Main success scenario:** - - The reviewing user comments on the relationship to note something that seems relevant to her. - - The reviewing user approves the relationship because she agrees with it. -- **Alternative scenario:** - - The reviewing user only comments on the relationship but doesn't approve it and might suggest a different relationship (e.g. neutral). - - No changes are made to the relationships confidence score. - - One administrator decides on the outcome. -- **Error scenario:** -- **Postcondition:** - - The number of approvals are counted for the administrator to decide on the confidence score. - Relationships with higher confidence score will have a higher weight when [suggesting plants](../assigned/suggest_plants.md) or suggesting alternatives. - - On changes of the confidence score, the reviewed user gets notified about the updated score. -- **Non-functional Constraints:** diff --git a/doc/usecases/assigned/reward_preview.md b/doc/usecases/assigned/reward_preview.md deleted file mode 100644 index 02a34c39c..000000000 --- a/doc/usecases/assigned/reward_preview.md +++ /dev/null @@ -1,26 +0,0 @@ -# Use Case: Reward Preview - -## Summary - -- **Scope:** Reward Preview -- **Level:** User Goal -- **Actors:** App User -- **Brief:** A user can see a preview of future rewards for his efforts. -- **Assignee** Ready to be assigned - -## Scenarios - -- **Precondition:** - - The user has opened the app and is editing a map. - - The user has planted at least one plant on his map. -- **Main success scenario:** - - The dates for the blooming or harvesting periods of all planted plants will be marked on the [map timeline](../assigned/map_timeline_event_view.md). - - When viewing the map on such a date, the plants with an active blooming or harvesting period at that time will be marked on the map. -- **Alternative scenario:** - - Optionally, this can be disabled in the users' preferences. -- **Error scenario:** - The app incorrectly displays a plant as having an active blooming or harvesting period or does not display it for a plant that should have one. - In this case the user should reload the layer to let the system recalculate the conditions for displaying active blooming or harvesting periods. -- **Postcondition:** - The user always sees what personal benefits he gets from the plants on the map. -- **Non-functional Constraints:** diff --git a/doc/usecases/assigned/soil_layer.md b/doc/usecases/assigned/soil_layer.md deleted file mode 100644 index c49b9c002..000000000 --- a/doc/usecases/assigned/soil_layer.md +++ /dev/null @@ -1,38 +0,0 @@ -# Use Case: Soil Layer - -## Summary - -- **Scope:** pH Values Layer -- **Level:** User Goal -- **Actors:** App User -- **Brief:** The user can add, move, remove, delete and edit areas of pH values and soil weight class in their map using the soil layer -- **Assignee:** Moritz - -## Scenarios - -- **Precondition:** - The user has opened the app and has selected the soil layer. -- **Main success scenario:** - 1. First the user globally tells a pH topsoil value, soil weight and yield grade. - 2. Then the user successfully adds, moves, removes, deletes and edits: - - pH value areas for topsoil - - pH value areas for subsoil - - soil weight class - - yield grade - in their map using the soil layer, where it differs from the global value. - This includes positioning the pH value areas in the appropriate locations and adjusting their values as needed. - A big brush is used to draw on the soil layer. - 3. The user can check the values at individual spots by clicking on it. -- **Alternative scenario:** - - The user accidentally edits, moves or removes an element and uses undo to correct the mistake. - - The user accidentally adds an element and deletes it with the "delete" or "undo" functionality. -- **Error scenario:** - The user attempts to edit a soil weight class or pH value in a area that is not within the acceptable range and the app displays an error message. - The previous values stay unmodified. -- **Postcondition:** - The user's map includes the added, moved or edited pH value areas as desired. -- **Non-functional Constraints:** - - The app must clearly communicate to the user the constraints for editing pH values in the soil layer. -- **Notes:** - - pH values have one significant digit, not more (e.g. 4.5, 6.7, 8.4) - - Performance: Map sizes with more than 1ha in 100 raster elements (in 1a=100m²) per year with 4 values per raster element should be usable without noticeable delays and acceptable memory overhead diff --git a/doc/usecases/assigned/suggest_plants.md b/doc/usecases/assigned/suggest_plants.md deleted file mode 100644 index 736019058..000000000 --- a/doc/usecases/assigned/suggest_plants.md +++ /dev/null @@ -1,44 +0,0 @@ -# Use Case: Suggest Plants - -## Summary - -- **Scope:** Plants Layer -- **Level:** User Goal -- **Actors:** App User -- **Brief:** The user gets plant suggestions that she would not have thought of herself. - -## Scenarios - -- **Precondition:** - - The user has opened the app. - - The plants layer is enabled. - - Plant(s) or a position on the map are selected. -- **Main success scenario:** - - - A choice of plants that - - 1. work well with already placed plants and - 2. fit the environmental conditions - 3. have a high ecological value - - are presented to the user. - - - Within those suggestions the user can see what plants go well with each other. - - The user can see why these plants have a high ecological value. e.g.: - - Attracts wildlife - - Attracts pollinating insects - - Is a nitrogen fixer - - Is a cover crop - - Are less commonly planted - - etc. - -- **Alternative scenario:** - - No plant(s) or position are selected. - - Polyculture groups that fit the environmental conditions are suggested. -- **Error scenario:** - - In case of any technical errors the users is notified about these. - - When there are no matches due to too many constraints the user is informed that she has to remove a plant and try again. -- **Postcondition:** - - The user can consider plants she would have never though of herself. -- **Non-functional Constraints:** - - Performance diff --git a/doc/usecases/assigned/terrain_layer.md b/doc/usecases/assigned/terrain_layer.md deleted file mode 100644 index c0ae001a2..000000000 --- a/doc/usecases/assigned/terrain_layer.md +++ /dev/null @@ -1,31 +0,0 @@ -# Use Case: Terrain Layer - -## Summary - -- **Scope:** Terrain Layer -- **Level:** User Goal -- **Actors:** App User -- **Brief:** The user can add, edit, move and delete terrain levels in the terrain layer. - -## Scenarios - -- **Precondition:** - The user has opened the app and has selected the terrain layer. -- **Main success scenario:** - - - The user successfully sets the base level value for the terrain layer. - - The user successfully adds, edits, moves and deletes terrain elements in the terrain layer. - Terrain elements are, e.g.: - - **Hill:** A hill is a terrain element that is higher than the base level. - - **Valley:** A valley is a terrain element that is lower than the base level. - - Terrain elements can be drawn using brushes of different sizes. - - The height of the terrain elements should be represented by a color gradient. - -- **Alternative scenario:** - The user accidentally adds or edits an terrain element in the wrong location and uses the app's undo function to correct the mistake. -- **Error scenario:** - The user attempts to add, move or edit an terrain element but the app is experiencing technical difficulties and is unable to complete the request, displaying an error message. -- **Postcondition:** - The user's map includes the added, edited, moved or deleted terrain elements as desired. -- **Non-functional Constraints:** - - Performance: Map sizes with more than 1ha in 10000 raster elements (m²) per year should be usable without noticeable delays and acceptable memory overhead diff --git a/doc/usecases/assigned/todo_layer.md b/doc/usecases/assigned/todo_layer.md deleted file mode 100644 index 6ae9098bb..000000000 --- a/doc/usecases/assigned/todo_layer.md +++ /dev/null @@ -1,33 +0,0 @@ -# Use Case: Todo Layer - -## Summary - -- **Scope:** Todo Layer -- **Level:** User Goal -- **Actors:** App User -- **Brief:** The user adds todos to their map to remember what to do at certain locations -- **Assignee:** Samuel - -## Scenarios - -- **Precondition:** - The user has opened the app and selected the todo layer. -- **Main success scenario:** - The users successfully adds a task to their map with following content: - - location on the map - - title of card - - description - - end date - - assigned person - The task is displayed on the map and can be moved, edited, archived or removed at any time. -- **Alternative scenario:** - - The user accidentally adds a task in the wrong location and uses the app's undo function to correct the mistake. - - The user has no Board or List configured where cards should be added. -- **Error scenario:** - The user tries to add a task with a title of than 255 characters and the app prevents it by stopping any further insertions into the task input field. -- **Postcondition:** - The user's map includes tasks as desired. - The tasks are synchronized to the Nextcloud Deck named after the PermaplanT map. -- **Non-functional Constraints:** - - Offline availability - - Performance: more than 1000 elements per year and per layer should be usable without noticeable delays and acceptable memory overhead diff --git a/doc/usecases/assigned/trees_layer.md b/doc/usecases/assigned/trees_layer.md deleted file mode 100644 index b0b32ab88..000000000 --- a/doc/usecases/assigned/trees_layer.md +++ /dev/null @@ -1,31 +0,0 @@ -# Use Case: Trees Layer - -## Summary - -- **Scope:** Trees Layer -- **Level:** User Goal -- **Actors:** App User -- **Brief:** The user can add, edit, move, remove and delete forests, trees and, hedge bushes in their map. -- **Simplification:** For first version it is identical to the plants layer (so any plants can be planted) -- **Assignee:** Moritz - -## Scenarios - -- **Precondition:** - The user has opened the app and has selected the trees layer. -- **Main success scenario:** - The user successfully adds, edits, moves, removes and deletes forests, trees and, hedge bushes in their map in the trees layer. - This includes: - - positioning the stems (visible brown circle), - - sketching the shape (transparent green, also allows other stems and plants shine through), - - adjusting the height, and specifying the type of tree or bush. -- **Alternative scenario:** - - The user accidentally edits, moves or removes an element and uses undo to correct the mistake. - - The user accidentally adds an element and deletes it with the "delete" or "undo" functionality. -- **Error scenario:** - The user attempts to add or edit forests, trees and, hedge bushes but the app is experiencing technical difficulties and is unable to complete the request, displaying an error message. -- **Postcondition:** - The user's map includes the added, edited, moved, removed or deleted forests, trees and, hedge bushes as desired. -- **Non-functional Constraints:** - - Supports alternatives - - Performance: more than 500 elements per year and alternative should be usable without noticeable delays and acceptable memory overhead diff --git a/doc/usecases/assigned/warnings_layer.md b/doc/usecases/assigned/warnings_layer.md index bb91a493b..4ada8ff3a 100644 --- a/doc/usecases/assigned/warnings_layer.md +++ b/doc/usecases/assigned/warnings_layer.md @@ -6,14 +6,16 @@ - **Level:** User Goal - **Actors:** App User - **Brief:** The user can add, edit, move and hides warnings. -- **Assignee:** Samuel +- **Assignee:** Daniel ## Scenarios - **Precondition:** The user has opened the app and has selected the warnings layer. - **Main success scenario:** - - Warnings can be individual elements or connecting two elements (e.g. antagonists). + - Warnings can be individual elements or connecting two elements, when modifying elements on the map: + - shade mismatch + - nearby antagonists - The user successfully adds, edits, moves and hides warnings. - **Alternative scenario:** The user accidentally adds, edits, moves or hides the wrong warnings and uses the app's undo function to correct the mistake. @@ -24,4 +26,8 @@ - Performance: more than 10000 elements per year and per alternative should be usable without noticeable delays and acceptable memory overhead - **Note:** - it gets dynamically generated based on alternatives - - while doing drag and drop of [plants](../current/plants_layer.md), warnings are shown anyway. + - while doing drag and drop of [plants](../done/plants_layer.md), warnings are shown anyway. + +## Development + +- warnings are generated and stored in backend diff --git a/doc/usecases/assigned/watering_layer.md b/doc/usecases/assigned/watering_layer.md deleted file mode 100644 index 812c9108a..000000000 --- a/doc/usecases/assigned/watering_layer.md +++ /dev/null @@ -1,30 +0,0 @@ -# Use Case: Watering Layer - -## Summary - -- **Scope:** Watering Layer -- **Level:** User Goal -- **Actors:** App User -- **Brief:** The user can log watering events for parts of or the entire garden to track watering history and improve plant care. -- **Assignee:** Moritz - -## Scenarios - -- **Precondition:** - - The user has opened the app and has selected the watering layer. -- **Main success scenario:** - - The user logs a watering event: - - by selecting individual plants - - by brushing over the map - - for all parts of the map, which are open-air (area can be defined by where roofs etc. are) - - The app saves the watering event with the current date. - - The watering history is visible to the user, providing an overview of past watering events. -- **Alternative scenario:** - - The user accidentally adds, edits or moves an element and uses "undo" or "delete" functionality to correct the mistake. -- **Error scenario:** - - The user attempts to log a watering event with invalid data (e.g., negative water amount). -- **Postconition:** - - The watering history is updated, and the user can review the information to make informed decisions about watering plants in the future. -- **Non-functional Contstrains:** - - The watering management feature should be easy to use and understand. - - The feature should work offline, allowing users to log watering events without an internet connection and synchronize the data when the connection is reestablished. diff --git a/doc/usecases/assigned/winds_layer.md b/doc/usecases/assigned/winds_layer.md deleted file mode 100644 index 215448dd3..000000000 --- a/doc/usecases/assigned/winds_layer.md +++ /dev/null @@ -1,27 +0,0 @@ -# Use Case: Winds Layer - -## Summary - -- **Scope:** Wind Layer -- **Level:** User Goal -- **Actors:** App User -- **Brief:** The user can add, edit, move, remove and delete wind orientation and areas in their map using the wind layer. - -## Scenarios - -- **Precondition:** - The user has opened the app and has selected the winds layer. -- **Main success scenario:** - - The user successfully adds, edits, moves, remove and deletes wind areas in their map using the wind layer. - - This includes positioning the areas in areas of the landscape that are more windy and adjusting the strength of the wind. - - Wind areas can be added by a big brush to draw on the wind layer. - - Main wind orientation can be indicated via an arrow. -- **Alternative scenario:** - - The user accidentally edits, moves or removes an element and uses undo to correct the mistake. - - The user accidentally adds an element and deletes it with the "delete" or "undo" functionality. -- **Error scenario:** - The user attempts to add or edit a wind area but the app is experiencing technical difficulties and is unable to complete the request, displaying an error message. -- **Postcondition:** - The user's map includes the added, edited, moved, removed or deleted wind areas as desired. -- **Non-functional Constraints:** - - Performance: Map sizes with more than 1ha in 100 raster elements (in 1a=100m²) per year should be usable without noticeable delays and acceptable memory overhead diff --git a/doc/usecases/assigned/zones_layer.md b/doc/usecases/assigned/zones_layer.md index 8fbdb68fb..1731ccaa7 100644 --- a/doc/usecases/assigned/zones_layer.md +++ b/doc/usecases/assigned/zones_layer.md @@ -6,6 +6,7 @@ - **Level:** User Goal - **Actors:** App User - **Brief:** The user can add, edit, move, remove and delete zones in their map in the zones layer. +- **Assignee:** Daniel ## Scenarios diff --git a/doc/usecases/current/base_layer.md b/doc/usecases/current/base_layer.md deleted file mode 100644 index 0a02e66d2..000000000 --- a/doc/usecases/current/base_layer.md +++ /dev/null @@ -1,45 +0,0 @@ -# Use Case: Base Layer - -## Summary - -- **Scope:** Base Layer -- **Level:** User Goal -- **Actors:** App User -- **Brief:** User imports a picture to use as a background. -- **Status:** In Progress -- **Assignee:** Moritz - -## Scenarios - -- **Precondition:** - - User has opened the app and selected the base layer. - - The user has an orthophoto or site plan and knows the real length of a flat part of the orthophoto (e.g. length of house's roof). -- **Main success scenario:** - - User successfully imports an orthophoto or site plan to be used as a background by selecting the option to - - import a picture to Nextcloud or - - by choosing an image from Nextcloud. - - The user draws a polygon telling the app where the borders of this image are (these boarders are stored in the map and not subjective to "alternatives"). - - Georeferencing (of polygon): The user tells real lengths of lines (on flat land) so that we know how big this image in reality is (see use case "measuring distance"). - - The user chooses an orientation of the picture, i.e., rotate the image to where north is. - - The user chooses where north related to the screen is by rotating an north arrow (this rotates the image and the polygon together). -- **Alternative scenarios:** - 1. User selects an (additional) alternative image. - - The user scales the image, so that it fits to prior georeferencing. - - The user chooses an orientation of the picture, i.e., rotate the image to where north already is to fit prior north orientation - - The user can switch back to the original image. - 2. User accidentally replaces the ortophoto with another image or wrongly put a line in the polygon and presses undo to correct the mistake. - 3. The user later (after changes in other layers were already done) finds that the polygon, the orientation or the georeferencing contains a problem: - - The app automatically saves the current version of the map. - - The user corrects the polygon, the orientation or the georeferencing. - - The database gets rewritten with the new geometric data. - (No undo available but the user can load the previous version.) -- **Error scenario:** - - User attempts to import a file that is not a supported image format or is corrupted and the app displays an error message. - The user is prompted to choose a correct image in one of the supported formats instead. - - The orders polygon(s) do not close: the app displays an error message. - The user is prompted to close the polygon(s). -- **Postcondition:** The user's selected background image and borders are used for further planning. -- **Non-functional Constraints:** - - Support for multiple image formats - - Supports alternatives, see Alternative scenario 1 (but alternatives is **not supported** for border polygon/georeferencing and Alternative scenario 3.) - - Support for undo for most changes but not for Alternative scenario 3. diff --git a/doc/usecases/current/entry_list_seeds.md b/doc/usecases/current/entry_list_seeds.md deleted file mode 100644 index 4df25d831..000000000 --- a/doc/usecases/current/entry_list_seeds.md +++ /dev/null @@ -1,26 +0,0 @@ -# Use Case: Entry and List of Seeds - -## Summary - -- **Scope:** Seed Management -- **Level:** User Goal -- **Actors:** App User -- **Brief:** The users manage their seeds by adding, editing, and deleting them in the app. -- **Assignee:** Moritz (login/users), Giancarlo ([seed overview](https://github.com/ElektraInitiative/PermaplanT/issues/210)) - -## Scenarios - -- **Precondition:** - The user has opened the seed management feature in the app. -- **Main success scenario:** - - The user is able to add new seeds to their list. - - The user is able to view a list of seeds where plant name, additional name, harvest year, amount, and origin is visible. - - The user is able to edit or delete seeds of the list. -- **Alternative scenario:** - The user accidentally deletes a seed from their list and uses the undo feature to correct it. -- **Error scenario:** - There is an error when the user attempts to add a seed to their list, such as an invalid expiration date or a seed with the same name already existing. - In this case, the app validates while the user is typing and shows a validation error. -- **Postcondition:** - The user's seed list is updated with the added, deleted, or restored seeds. -- **Non-functional Constraints:** diff --git a/doc/usecases/current/gain_blossoms.md b/doc/usecases/current/gain_blossoms.md deleted file mode 100644 index c9127f0a9..000000000 --- a/doc/usecases/current/gain_blossoms.md +++ /dev/null @@ -1,59 +0,0 @@ -# Use Case: Gain Blossoms - -## Summary - -- **Scope:** Gain Blossoms -- **Level:** User Goal -- **Actors:** App User -- **Brief:** The user is awarded with Blossoms for achieving certain milestones. -- **Status:** Assigned -- **Assignee** Thorben - -## Scenarios - -- **Precondition:** - - The user has opened the app and uses it. -- **Main success scenario:** - - The user can see a list of incomplete milestones and their respective requirements. An user can reach a certain milestone by e.g.: - - change layers (visibility etc.) - - base layer: set base image - - change the date. - - planting their first plant. - - using an element from a new layer for the first time. - - uploading their first photo in the [photo layer](../assigned/photo_layer.md). - - harvesting a plant for the first time. - - opening their first map for collaboration. - - collaborating on a map from another user for the first time. - - Blossoms are grouped in different tracks by what goal they try to incentivize, e.g.: - - Beginners Track: leading new users through the basic features of PermaplanT. - - Seasonal Track: motivate users to completely plan out their map and keep it updated through the seasons. - - Completionist Track: rewarding users for enriching their own data with the results of their harvest. - - Expert Track: incentivize users to support the platform and its other users to gain a free membership. - - Further ideas: - - planting their first full group of companions. - - planting their first recommended diversity plant. - - submitting updated plant data. - - updating their plant relations with data from the harvest for the first time. - - reaching a specific [diversity score](../assigned/diversity_score.md) goal. - - gathering all ingredients for an ingredient list for the first time. - - buying their first batch of seeds. - - creating their first event. - - honoring a map from another user for the first time. - - posting their first comment on a map from another user. - - writing their first review of a map from another user. - - receiving their first honor from another user. - - receiving their first review from another user. - - having their first conversation with another user through [matchmaking](../assigned/matchmaking.md). - - adding their first new plant in the apps database. - - adding their first new seed in the apps database. - - Progress of the individual milestones is tracked independently and they can be accomplished in any order. - - The user is being kept updated on their progress when pursuing actions to complete a milestone. - - Upon completing a milestone, the user is informed of their achievement and is awarded the corresponding Blossom. - - Some Blossoms reset after a year to engage the user to continue the work in following seasons. -- **Alternative scenario:** -- **Error scenario:** - The user meets the criteria for a certain Blossom, but it will not be awarded due to an error in the app. - The Blossom will be awarded the next time its criteria will be checked. -- **Postcondition:** - The awarded Blossoms will be shown in the users profile with a number indicating the amount of times this blossom was earned in previous seasons. -- **Non-functional Constraints:** diff --git a/doc/usecases/current/heat_map.md b/doc/usecases/current/heat_map.md new file mode 100644 index 000000000..ea054b00e --- /dev/null +++ b/doc/usecases/current/heat_map.md @@ -0,0 +1,18 @@ +# Use Case: Heat Map + +- **Scope:** Plant Layer +- **Level:** User Goal +- **Actors:** App User +- **Brief:** The user gets a visual indication for good/bad spots on the map. +- **Assignee:** Moritz + +## Scenarios + +- **Precondition:** + The user has opened the plant layer. +- **Main success scenario:** + - The user selects an plant. + - The user gets a visual indication which parts of the map are ideal, okay and not suited for the plant to be placed. + - After placement gets aborted, the visual indication disappears. +- **Postcondition:** + None. diff --git a/doc/usecases/current/map_search.md b/doc/usecases/current/map_search.md deleted file mode 100644 index 0d28a6f68..000000000 --- a/doc/usecases/current/map_search.md +++ /dev/null @@ -1,32 +0,0 @@ -# Use Case: Map Search - -## Summary - -- **Scope:** Map Search -- **Level:** User Goal -- **Actors:** App User -- **Brief:** The user can search for maps within the app using keywords, including the ability to search for public maps created by other users. -- **Assignee:** Gabriel (Backend), Moritz (Frontend) - -## Scenarios - -- **Precondition:** - The user has opened the app and is on the map search page. -- **Main success scenario:** - - A list of all maps are shown, including both the user's own maps and public/protected maps created by other users. - For each map: - - A small image either of the photo or of its content is shown - - The name is shown - - The user enters a keyword into the search field and the app filters the list of maps that match the keyword. - - The user can then select a map to view it. -- **Alternative scenario:** - The user enters a keyword that does not match any maps in the app. - In this case, the app displays a message to the user indicating that no maps were found. -- **Error scenario:** - There is an error in the app's search functionality and the maps are not correctly retrieved. - In this case, the app displays an error message to the user and allows them to try again. -- **Postcondition:** - The user has successfully searched for a map and retrieved correct results within the app. -- **Non-functional Constraints:** - - The app's search functionality must be fast and efficient in order to provide a seamless user experience. - - The app must clearly communicate to the user whether their search was successful or not. diff --git a/doc/usecases/current/map_timeline_single_selection.md b/doc/usecases/current/map_timeline_single_selection.md index c7e0c8c8e..5da6f8ac3 100644 --- a/doc/usecases/current/map_timeline_single_selection.md +++ b/doc/usecases/current/map_timeline_single_selection.md @@ -6,18 +6,21 @@ - **Level:** User Goal - **Actors:** App User - **Brief:** The user can view the map at different points in time by using the timeline feature. -- **Assignee:** Paul +- **Assignee:** Daniel Steinkogler ## Scenarios - **Precondition:** - The user has opened the app and selected a map. - - The date field shows the current date. + - The timeline shows the current date. - **Main success scenario:** - - The user selects a different date in the date field. + - A timeline is presented to the user which allows them to select year, month and day. + - Year, month and day can be individually selected by scrolling, moving sliders or clicking on the elements. - This allows navigation to a different point in the past, present and future. - The map updates to show the state of the garden at the selected point in time (removing or adding elements accordingly). - Adding or removing elements is done on the selected date. + - The selected date is highlighted. + - The sliders are synchronized, i.e., if the user scrolls the day slider over the last day of the month, the month slider is updated to the next month. - **Alternative scenario:** - The user corrects the dates in the attributes of elements or presses undo to undo changes in the dates. - **Error scenario:** @@ -27,3 +30,9 @@ - Performance: data in up to 100 years should be fast to use - **Note:** - Single Date Selection must always be an exact date so we have a well-known date (reference point) when elements got added to the map. + +## Development Progress + +- Changing the map's date is currently implemented with a simple date picker in [Timeline.tsx and sub-components](https://github.com/ElektraInitiative/PermaplanT/blob/9927a132de09377baad47237f3048939f84c568b/frontend/src/features/map_planning/components/timeline/Timeline.tsx). + The related logic in the MapStore can be found [here](https://github.com/ElektraInitiative/PermaplanT/blob/9927a132de09377baad47237f3048939f84c568b/frontend/src/features/map_planning/store/UntrackedMapStore.ts#L177) +- the new version of the timeline should allow user to select date by scrolling or moving sliders diff --git a/doc/usecases/current/plants_layer.md b/doc/usecases/current/plants_layer.md deleted file mode 100644 index dd3853024..000000000 --- a/doc/usecases/current/plants_layer.md +++ /dev/null @@ -1,43 +0,0 @@ -# Use Case: Plants Layer - -## Summary - -- **Scope:** Plants Layer -- **Level:** User Goal -- **Actors:** App User -- **Brief:** The user can add, edit, move, remove, and delete plant elements in their map in the plants layer. -- **Assignee:** Paul (Frontend), Gabriel (Backend) - -## Scenarios - -- **Precondition:** - The user has opened the app and has selected the plants layer. -- **Main success scenario:** - - The user is presented with a list of seasonal plants (for the selected time of the year), in following groups: - - available seeds with early expiration days first - - diversity constraints: suggestions which not-yet-used plants fit next to the plant just added, or at a place selected (spatial and temporal) - - which were recently planted - - and lastly favourites of users - - While the user adds a plant, constraints are shown: - - The user gets a visual indication which parts of the map are ideal, okay and not suited for the plant to be placed - (based on e.g. previous plants, zones, pH value, moisture, animals or shadows). - - The user is able to view the relationships between the plants. - She does so by looking at the lines connecting the existing plants with a symbol around the mouse cursor indicating companion or antagonist. - - The user positions the plant element in the appropriate location in the map. - - When drawing an area of plants, the size of the area and the number of plants is shown next to the mouse. - - The user is able to move, edit (e.g. when planted, when harvested), remove (that it was removed from the garden) and delete (that it never existed) selected plants. - - The user adjusts the plant elements and their relationships as needed. -- **Alternative scenario:** - - The user accidentally edits, moves or removes an element and uses undo to correct the mistake. - - The user accidentally adds an element and deletes it with the "delete" or "undo" functionality. -- **Error scenario:** - - The user attempts to add, move or edit a plant element but the app is experiencing technical difficulties and is unable to complete the request, displaying an error message. - - There is an error in the app's plant relationship indication and the lines connecting the plants are not displayed correctly. In this case, the app displays an error message. -- **Postcondition:** - - The user's map includes the added, edited, moved, removed or deleted plant element as desired. - - If constraints are violated for the place where a plant was added or moved, warnings get added (or removed) to (from) the [warnings layer](../assigned/warnings_layer.md). -- **Non-functional Constraints:** - - Partial offline availability: editing attributes, especially for planting and harvesting - - Supports alternatives - - Performance: more than 10000 elements per year and per alternative should be usable without noticeable delays and acceptable memory overhead - - Annual plants automatically get removed after one year. diff --git a/doc/usecases/current/shade_layer.md b/doc/usecases/current/shade_layer.md deleted file mode 100644 index 55edd45cc..000000000 --- a/doc/usecases/current/shade_layer.md +++ /dev/null @@ -1,27 +0,0 @@ -# Use Case: Shade Layer - -## Summary - -- **Scope:** Shade Layer -- **Level:** User Goal -- **Actors:** App User -- **Brief:** The user can add, edit, move, remove and delete shade areas in their map in the shade layer and adjust the intensity. -- **Assignee:** Moritz (Frontend), Gabriel (Backend) - -## Scenarios - -- **Precondition:** - The user has opened the app and has selected the shade layer. -- **Main success scenario:** - The user successfully adds, edits, moves, removes and deletes shade areas in their map in the shade layer. - This includes positioning the indicators in areas of the landscape that receive more or less sun exposure and adjusting the intensity of the shade area. - Shade areas can be added by a big brush to draw on the shade layer. -- **Alternative scenario:** - - The user accidentally edits, moves or removes an element and uses undo to correct the mistake. - - The user accidentally adds an element and deletes it with the "delete" or "undo" functionality. -- **Error scenario:** - The user attempts to add or edit a shade area but the app is experiencing technical difficulties and is unable to complete the request, displaying an error message. -- **Postcondition:** - The user's map includes the added, edited, moved, removed or deleted shade area as desired. -- **Non-functional Constraints:** - - Performance: Map sizes with more than 1ha in 10000 raster elements (m²) per year should be usable without noticeable delays and acceptable memory overhead diff --git a/doc/usecases/done/entry_list_seeds.md b/doc/usecases/done/entry_list_seeds.md new file mode 100644 index 000000000..bf1fb47b1 --- /dev/null +++ b/doc/usecases/done/entry_list_seeds.md @@ -0,0 +1,25 @@ +# Use Case: Entry and List of Seeds + +- **Scope:** Seed Management +- **Level:** User Goal +- **Actors:** App User +- **Brief:** The users manage their seeds by adding, editing, searching, viewing and archiving them. +- **Assignee:** Moritz + +## Scenarios + +- **Precondition:** + The user has opened the seed management feature in the app. +- **Main success scenario:** + - The user is able to add new seeds to their list. + - The user is able to search for seeds using the full plant name. + - The user is able to view a list of seeds where the complete name, amount, quality, harvest year, and origin is visible. + - The user is able to edit or archive seeds of the list. + - The user is able to search for seeds with their complete name. +- **Alternative scenario:** + The user accidentally archives a seed from their list and uses the undo feature to correct it. +- **Error scenario:** + There is an error when the user attempts to add or edit a seed entry with invalid data such as an invalid price. + In this case, the app validates when saving the seed and shows a validation error. +- **Postcondition:** + The user's seed list corresponds exactly with the physical seeds in the user's seed box in reality. diff --git a/doc/usecases/done/guided_tour.md b/doc/usecases/done/guided_tour.md new file mode 100644 index 000000000..f9e73f1cb --- /dev/null +++ b/doc/usecases/done/guided_tour.md @@ -0,0 +1,38 @@ +# Use Case: Guided Tour + +## Summary + +- **Scope:** Gamification +- **Level:** User Goal +- **Actors:** User +- **Brief:** The user is presented with an introduction tour upon first encountering the map editor. +- **Status:** Done +- **Assignee:** Thorben + +## Scenarios + +- **Precondition:** + - The user is member and logged in. + - The user has created a map. + - The user has opened the map editor. + - The user has not previously completed the guided tour. +- **Main success scenario:** + The user is shown a guided tour, which explains the features of the map editor based on a small scenario. + The user will have to place and remove plants, use the timeline, see plant relations and use the undo feature in this scenario. + After completing the guided tour, the user is awarded a [Blossom](../draft/gain_blossoms.md). +- **Alternative scenario:** + The user does not want to do the guided tour and can cancel it during any step. + Upon cancel, the user decides to see it on their next visit or to cancel it indefinitely. +- **Error scenario:** + A network problem prevents the frontend from checking if the guided tour was already completed and will show an error message. +- **Postcondition:** + The guided tour is completed or canceled and won't be shown again next time. +- **Non-functional Constraints:** + +## Development Progress + +- Tour steps and display options are defined in their own [typescript file](https://github.com/ElektraInitiative/PermaplanT/blob/e4931dc6b4e1bbfaa48a6094a7c289f3cd2de57c/frontend/src/features/map_planning/utils/EditorTour.ts). +- CSS classes for the styling of the tour were added as a separate [CSS file](https://github.com/ElektraInitiative/PermaplanT/blob/e4931dc6b4e1bbfaa48a6094a7c289f3cd2de57c/frontend/src/styles/guidedTour.css). +- Guided tour is added to the application as part of the [map wrapper](https://github.com/ElektraInitiative/PermaplanT/blob/e4931dc6b4e1bbfaa48a6094a7c289f3cd2de57c/frontend/src/features/map_planning/routes/MapWrapper.tsx). +- The tour is started and event listeners are added in the [map component](https://github.com/ElektraInitiative/PermaplanT/blob/e4931dc6b4e1bbfaa48a6094a7c289f3cd2de57c/frontend/src/features/map_planning/components/Map.tsx). +- Follow-up Issue(s): [#710](https://github.com/ElektraInitiative/PermaplanT/issues/710). diff --git a/doc/usecases/done/layers_visibility.md b/doc/usecases/done/layers_visibility.md index 6630c5bb4..eaa3408ac 100644 --- a/doc/usecases/done/layers_visibility.md +++ b/doc/usecases/done/layers_visibility.md @@ -26,4 +26,9 @@ - **Alternative scenario:** - On activation of a layer, also the visibility gets turned on. - On activation of some layers, the visibility of others layer changes; e.g., - activation of the [moisture layer](../assigned/hydrology_layer.md) also sets the visibility of the [infrastructure layer](../assigned/infrastructure_layer.md) to on. + activation of the [moisture layer](../assigned/hydrology_layer.md) also sets the visibility of the [infrastructure layer](../draft/infrastructure_layer.md) to on. + +## Notes + +The layer opacity is a Konva feature and is implemented in [Map.tsx](https://github.com/ElektraInitiative/PermaplanT/tree/master/frontend/src/features/map_planning/components/Map.tsx). +The control elements for the opacity and visibility can be found in [LayerListItem.tsx](https://github.com/ElektraInitiative/PermaplanT/tree/master/frontend/src/features/map_planning/components/toolbar/LayerListItem.tsx) diff --git a/doc/usecases/done/login.md b/doc/usecases/done/login.md index 8e4490451..d81c87701 100644 --- a/doc/usecases/done/login.md +++ b/doc/usecases/done/login.md @@ -27,3 +27,12 @@ - **Non-functional Constraints:** - The login process must be secure to protect the user's personal information. - The app must clearly communicate to the user whether the login was successful or not. + +## Links + +- [Authentication Setup](../../research/nextcloud_integration.md) +- [Authentication Decision](../../decisions/auth.md) + +## Left-Overs + +- [Add an auth mechanism to the /api/updates/maps endpoint](https://github.com/ElektraInitiative/PermaplanT/issues/409) diff --git a/doc/usecases/done/multi_select.md b/doc/usecases/done/multi_select.md new file mode 100644 index 000000000..a04954b47 --- /dev/null +++ b/doc/usecases/done/multi_select.md @@ -0,0 +1,34 @@ +# Use Case: Multi-Select + +## Summary + +- **Scope:** All Layers, except Base +- **Level:** User Goal +- **Actors:** App User +- **Brief:** The user changes properties or deletes multi-selected plants. +- **Assignee:** Christoph N. + +## Scenarios + +- **Precondition:** + The user has made a selection of at least two plants. +- **Main success scenario:** + - The user sets the _Added on_ date. + Then he sets the _Removed on_ date. + - The user clicks on the deletion button and sees all selected plants disappear from the map. +- **Alternative scenario:** + - The user accidentally deletes the selected plants and uses the app's undo function to correct the mistake. + - The user accidentally sets a wrong date and uses the app's undo function to correct the mistake. +- **Error scenario:** + The app is experiencing technical difficulties and is unable to complete the request, displaying an error message. +- **Postcondition:** + - The _Added on_ date of all selected plants are changed. + - The _Removed on_ date of all selected plants are changed. + - All deleted plants are gone from the map as if they never existed. +- **Non-functional Constraints:** + - The user should be able to undo/redo setting the _Added on_ and _Removed On_ date of all selected plants. + - The user should be able to undo/redo the deletion of all selected plants. + +## Development Progress + +fully done diff --git a/doc/usecases/done/search_plants.md b/doc/usecases/done/search_plants.md index f495b3db6..ca4a2bf7d 100644 --- a/doc/usecases/done/search_plants.md +++ b/doc/usecases/done/search_plants.md @@ -22,27 +22,22 @@ - German common names - English common names - Attributes that describe the plant, especially `edible_uses`, so that people can search for `popcorn`. - - Results then get extended by the whole hierarchy below, e.g., a search for `Tomato` should include all cultivars. - - The results are ranked by: - 1. Exact matches, without additional letters before or after, e.g. the user wrote "fir", "fir" should be first hit - 2. Language settings, e.g., when typing "fi" on English setting "fir" should rank higher than German "Fichte" - (and the other way round) - 3. Names of a plant should be preferred (over text found in attributes or plants found of the hierarchy below). - - The resulting list is constructed (e.g., Tomatillo _Physalis philadelphica_): - - common names according to language settings (German or English), if available, then - - a hyphen `-` (if there was a common name), then - - unique name rendered as described in [hierarchy description](../../database/hierarchy.md) - - The matched part of the text should be bold. + - The results are ranked by how well the type text matches with the plant name, e.g. the user wrote "fir", "fir" should be first hit + - The plant name is rendered as described in [hierarchy description](../../database/hierarchy.md) - **Alternative scenario:** -- **Error scenario:** - No match can be found for what the user was searching for. A message will be displayed that nothing was found. +- **Error scenario:** - **Postcondition:** - The user has found the plant or a similar one to add to her map. - **Non-functional Constraints:** - Performance - If there is a possible match in the database, it should be included (regardless of language settings etc.) -# Follow-up +# Out-of-scope + +- The matched part of the text should be bold. +- Results then get extended by the whole hierarchy below, e.g., a search for `Tomato` should include all cultivars. +- We don't consider language for ranking so that the backend can be agnostic to language. -Follow-up issues were recorded and a note has been left in the use case document (#379, #433). +Further out-of-scope topics are documented in the closed issue #379. diff --git a/doc/usecases/done/translation.md b/doc/usecases/done/translation.md index 5b0553365..21182ecb9 100644 --- a/doc/usecases/done/translation.md +++ b/doc/usecases/done/translation.md @@ -24,3 +24,7 @@ - **Postcondition:** The app's interface and text are displayed in the selected language. - **Non-functional Constraints:** + +## Developers + +Read [doc/guidelines/i18n.md](../../guidelines/i18n.md) for further information. diff --git a/doc/usecases/done/zoom.md b/doc/usecases/done/zoom.md index ad1f6325c..f10f2a7c8 100644 --- a/doc/usecases/done/zoom.md +++ b/doc/usecases/done/zoom.md @@ -7,7 +7,6 @@ - **Actors:** App User - **Brief:** The user can zoom in and out on their landscape map to view details more clearly or get a better overview of their map. - **Status:** Done -- **Assignee:** Giancarlo ## Scenarios diff --git a/doc/usecases/draft/copy_paste_between_own_maps.md b/doc/usecases/draft/copy_paste_between_own_maps.md new file mode 100644 index 000000000..ad0e6e43d --- /dev/null +++ b/doc/usecases/draft/copy_paste_between_own_maps.md @@ -0,0 +1,38 @@ +# Use Case: Copy & Paste Between Own Maps + +## Summary + +- **Scope:** All Layers, except Base +- **Level:** User Goal +- **Actors:** App User +- **Brief:** The user can copy and paste a selection of elements, including succeeding crops, between his own maps. +- **Status:** Analysis +- **Assignee:** Draft + +## Scenarios + +- **Precondition:** + The user has opened the app on a key-controlled device and made a selection of elements on his own map _A_ that he wants to copy and paste into his own map _B_. +- **Main success scenario:** + - The user copies the selection on map _A_. + The user opens map _B_. + The user clicks anywhere on map _B_. + The user pastes the copied selection into map _B_. + The pasted selection of elements is placed in map _B_ at the user's last click position right before pasting. +- **Alternative scenarios:** + - Same as for [Copy & Paste of Selection Within the Same Map](../assigned/copy_paste_within_same_map.md#scenarios), just with two different maps of the same user + - The user logs in. + The user fails to paste the elements he copied in his previous session because they are removed from the 'copy-storage'. +- **Error scenarios:** + - The user attempts to copy and paste a selection but the app is experiencing technical difficulties and is unable to complete the request, displaying an error message. +- **Postconditions:** + - The user's map contains the copied and pasted selection of elements. + - The user's map _A_ still contains the same elements as before the copying and pasting into map _B_. + - The user's map _B_ contains the pasted selection of elements which he copied from his map _A_. +- **Non-functional Constraints:** + - Same as for [Copy & Paste of Selection Within the Same Map](../assigned/copy_paste_within_same_map.md#scenarios) + - For performance reasons and to minimize potential sources of error, the copied selection of elements should be persisted locally on the client side, i.e. in the browser's local storage. + - The new storage, i.e. the local storage, should be used for every _Copy & Paste_ scenario to store and retrieve the latest copied elements. + - To avoid inconsistencies of all sorts, the new storage with the copied elements in it, should not be persisted beyond a user's session, i.e. it should cleared upon the user's next login. +- **Linked Use Cases:** + - [Copy & Paste of Selection Within the Same Map](../assigned/copy_paste_within_same_map.md) diff --git a/doc/usecases/draft/copy_paste_between_users.md b/doc/usecases/draft/copy_paste_between_users.md new file mode 100644 index 000000000..364df4d0e --- /dev/null +++ b/doc/usecases/draft/copy_paste_between_users.md @@ -0,0 +1,38 @@ +# Use Case: Copy & Paste Between Users + +## Summary + +- **Scope:** All Layers, except Base +- **Level:** User Goal +- **Actors:** App User +- **Brief:** The user can copy and paste a selection of elements, including succeeding crops, between his own map and a map of another user. +- **Status:** Analysis +- **Assignee:** Draft + +## Scenarios + +- **Precondition:** + User A has opened the app on a key-controlled device and has made a selection of elements on one of his maps that he wants to copy and paste into the map of another user B. +- **Main success scenario:** + - User A copies the selection on his map. + User A opens a map of user B. + User A clicks anywhere on that map of user B. + User A pastes the copied selection into user B's map. + The pasted selection of elements is placed in user B's map at the position where user A's last click happened. +- **Alternative scenarios:** + - Same as for [Copy & Paste of Selection Within the Same Map](../assigned/copy_paste_within_same_map.md#scenarios), just with maps of two different users + - The user logs in. + The user fails to paste the elements he copied in his previous session because they are removed from the 'copy-storage'. +- **Error scenarios:** + - The user attempts to copy and paste a selection but the app is experiencing technical difficulties and is unable to complete the request, displaying an error message. +- **Postconditions:** + - The user's map contains the copied and pasted selection of elements. + - User A's map still contains the same elements as before the copying and pasting into user B's map. + - User B's map contains the pasted selection of elements which user A copied from one of his own maps. +- **Non-functional Constraints:** + - Same as for [Copy & Paste of Selection Within the Same Map](../assigned/copy_paste_within_same_map.md#scenarios) + - For performance reasons and to minimize potential sources of error, the copied selection of elements should be persisted locally on the client side, i.e. in the browser's local storage. + - The new storage, i.e. the local storage, should be used for every _Copy & Paste_ scenario to store and retrieve the latest copied elements. + - To avoid inconsistencies of all sorts, the new storage with the copied elements in it, should not be persisted beyond a user's session, i.e. it should cleared upon the user's next login. +- **Linked Use Cases:** + - [Copy & Paste of Selection Within the Same Map](../assigned/copy_paste_within_same_map.md) diff --git a/doc/usecases/draft/copy_paste_via_icons.md b/doc/usecases/draft/copy_paste_via_icons.md new file mode 100644 index 000000000..b33908bcf --- /dev/null +++ b/doc/usecases/draft/copy_paste_via_icons.md @@ -0,0 +1,44 @@ +# Use Case: Copy & Paste Via Icons + +## Summary + +- **Scope:** All Layers, except Base +- **Level:** User Goal +- **Actors:** App User +- **Brief:** The user can copy and paste a selection of elements, including succeeding crops, by using the appropriate icons in the toolbar. +- **Status:** Analysis +- **Assignee:** Draft + +## Scenarios + +- **Precondition:** + The user has opened the app and has made a selection of elements that he wants to copy and paste. +- **Main success scenario:** + - The user copies the selection by clicking on the _copy_ icon in the toolbar. + The user clicks anywhere on the map. + The user pastes the copied selection into the map by clicking on the _paste_ icon in the toolbar. + The pasted selection of elements is placed at the user's last click position right before pasting. +- **Alternative scenario:** + - Same as for [Copy & Paste of Selection Within the Same Map](../assigned/copy_paste_within_same_map.md#scenarios), just with clicking on the icons in the toolbar instead of using the keyboard shortcuts + - The user copies the selection by pressing CTRL-C. + The user clicks on the _paste_ icon to paste the copied selection into the map. + - The user copies the selection by clicking on the _copy_ icon. + The user pastes the copied selection into the map by pressing CTRL-V. +- **Intercondition:** + - The user sees the _copy_ icon as disabled and greyed out because he has no elements selected. + - The user sees the _paste_ icon as disabled and greyed out because he has not copied any element(s) in his current session yet. + - The user copies a selection of elements for the first time in his current session. + The user sees the _paste_ icon getting enabled and not being greyed out anymore. + - The user makes a selection of elements on the map, not having had anything selected right before. + The user sees the _copy_ icon getting enabled and not being greyed out anymore. +- **Error scenario:** + - The user attempts to copy and paste a selection but the app is experiencing technical difficulties and is unable to complete the request, displaying an error message. +- **Postcondition:** + - The user's map contains the copied and pasted selection of elements. + - The _copy_ icon is disabled and greyed out because after pasting every selection is gone. + - The _paste_ icon is enabled and not greyed out. +- **Non-functional Constraints:** + - Same as for [Copy & Paste of Selection Within the Same Map](../assigned/copy_paste_within_same_map.md#scenarios) + - The icons' Look and Feel should always signal their current state, i.e. if they are enabled or disabled. +- **Linked Use Cases:** + - [Copy & Paste of Selection Within the Same Map](../assigned/copy_paste_within_same_map.md) diff --git a/doc/usecases/draft/diff.md b/doc/usecases/draft/diff.md new file mode 100644 index 000000000..9d6aaf7a2 --- /dev/null +++ b/doc/usecases/draft/diff.md @@ -0,0 +1,26 @@ +# Use Case: Diff + +## Summary + +- **Scope:** All Layers, except Base +- **Level:** User Goal +- **Actors:** User, App +- **Brief:** The user can visualize differences between two dates. + +## Scenarios + +- **Precondition:** + The user has opened the app and has selected the desired layer (e.g. plants). +- **Main success scenario:** + - The user clicks on the first date in the timeline. + - The user double clicks on the second date in the timeline. + - Plants that were added or removed between these dates are marked visually. +- **Alternative scenario:** + - If no differences are available, a message is displayed. +- **Error scenario:** + - There is an error in the app's difference calculation. + In this case, the app displays an error message and allows the user to try again. +- **Postcondition:** + - Unless the user clicks on the timeline again, the user cannot add or remove plants. +- **Non-functional Constraints:** + - None. diff --git a/doc/usecases/draft/dimensioning_layer.md b/doc/usecases/draft/dimensioning_layer.md index ac9dcc090..9332b8590 100644 --- a/doc/usecases/draft/dimensioning_layer.md +++ b/doc/usecases/draft/dimensioning_layer.md @@ -6,7 +6,6 @@ - **Level:** User Goal - **Actors:** App User - **Brief:** The user can put dimensioning between elements. -- **Status:** Draft ## Scenarios diff --git a/doc/usecases/draft/map_to_pdf.md b/doc/usecases/draft/map_to_pdf.md index 5cfb93d1c..96cb14b33 100644 --- a/doc/usecases/draft/map_to_pdf.md +++ b/doc/usecases/draft/map_to_pdf.md @@ -6,8 +6,6 @@ - **Level:** User Goal - **Actors:** App User - **Brief:** Users can generate a PDF file of the map. -- **Status:** Assigned -- **Assignee:** Ramzan ## Scenarios diff --git a/doc/usecases/draft/map_webcam.md b/doc/usecases/draft/map_webcam.md index 25deb8be7..582fd4da1 100644 --- a/doc/usecases/draft/map_webcam.md +++ b/doc/usecases/draft/map_webcam.md @@ -6,7 +6,6 @@ - **Level:** User Goal - **Actors:** App User - **Brief:** The user can add, remove and view webcams in their map -- **Status:** Draft ## Scenarios diff --git a/doc/usecases/draft/remember_viewing_state.md b/doc/usecases/draft/remember_viewing_state.md new file mode 100644 index 000000000..d653234e2 --- /dev/null +++ b/doc/usecases/draft/remember_viewing_state.md @@ -0,0 +1,59 @@ +# Use Case: Remember Viewing State + +## Summary + +- **Scope:** Map +- **Level:** User Goal +- **Actors:** App User +- **Brief:** The map's visibility settings are restored from the user's last session. +- **Status:** Draft + +## Scenarios + +- **Precondition:** + The user is logged in and has opened a map. +- **Main success scenario:** + - The user selects layer _A_. + He zooms into the map. + He scrolls/drags the map's viewport. + He sets layer _B_ invisible. + He turns off the grid display. + He hides the plant labels. + He closes the browser, opens it again and logs in. + He opens the same map again which he edited in his previous session. + He finds all the changes and settings from the last session being restored: + - layer _A_ selected + - zoomed-in on the map + - all plants visible on the map's viewport + - layer _B_ invisible + - grid display turned off + - plant labels hidden +- **Alternative scenario:** + - The user does visibility changes on a map. + He navigates to the seeds page. + He uses browser-back to get back to the map. + All the visibility changes the user applied before, are restored. + - The user does visibility changes on map _A_. + He opens another map _B_ and changes some visibility settings there too. + He goes back to map _A_ and finds all the visibility changes he did on map _A_, still being applied. + He goes back to map _B_ and sees that all the visibility settings he changed on map _B_, are still being set. + - The user does visibility changes on a map in browser _A_. + He logs in in another browser _B_. + He opens the same map in browser _B_. + All the visibility changes the user applied to the map in browser _A_, are restored in browser _B_. +- **Error scenario:** + The app is experiencing technical difficulties and unable to restore one or more of the map's visibility settings, displaying an info message that the map's viewing state could not be fully restored. +- **Postcondition:** + - The map is shown with the same visibility settings it had when the user left it the last time, which includes: + - zoom + - map's (x,y)-offset in the viewport + - selected layer + - all layers' visibility + - grid display + - plant labels display +- **Non-functional Constraints:** + - Changes to maps while being in offline-mode, are not considered for restoring (for now). + - _Zustand_'s [_persist middleware_](https://docs.pmnd.rs/zustand/integrations/persisting-store-data) should be used to store a map's state in a storage of any kind - not only do we already use _Zustand_ as our global state management library, but it also supports de-/serialization of `Maps` and `Sets`. + _Persist_'s [_partialize_](https://docs.pmnd.rs/zustand/integrations/persisting-store-data#partialize) option should then be used to only store the 'restore-worthy' data of a map. + - Copy & Paste related data should not be restored, to not only reduce complexity of the storing/restoring- as well as the undo/redo-process, but also to prevent out-of-sync scenarios where copied elements are already deprecated/removed/outdated in the system/database. + Besides that, applications offering the recovery of Copy & Paste state in a new session are extremely rare and therefore something the user does not necessarily expect to get. diff --git a/e2e/.env.sample b/e2e/.env.sample index 74e6a95b0..2e5c5aed2 100644 --- a/e2e/.env.sample +++ b/e2e/.env.sample @@ -1,3 +1,8 @@ E2E_URL=localhost:5173 E2E_USERNAME=Adi E2E_PASSWORD=1234 +POSTGRES_DB=permaplant +POSTGRES_USER=permaplant +POSTGRES_PASSWORD=permaplant +DATABASE_URL=postgres://permaplant:permaplant@db/permaplant +DATABASE_PORT=5432 diff --git a/e2e/README.md b/e2e/README.md index cdbb1bd66..54dc05bbc 100644 --- a/e2e/README.md +++ b/e2e/README.md @@ -1,6 +1,21 @@ # PermaplanT E2E tests -All commands/scripts in this README are executed from this folder (/e2e). +## Table of Contents + +- [Directory structure](#directory-structure) +- [Coding Guidelines](#coding-guidelines) +- [Quickstart](#quickstart) + - [Docker](#docker) +- [Environment Variables](#environment-variables) +- [Test Results](#test-results) +- [Test Reports](#test-reports) +- [Cleanup](#cleanup) +- [Optional arguments](#optional-arguments) +- [Other Documentations](#other-documentations) + - [Playwright for Python](#playwright-for-python) + - [Pytest-playwright](#pytest-playwright) + - [Pytest-bdd](#pytest-bdd) + - [Pytest-xdist](#pytest-xdist) ## Directory structure @@ -10,47 +25,45 @@ All commands/scripts in this README are executed from this folder (/e2e). ├── steps The actual tests ├── conftest.py Global pytest fixtures, functions. ├── test-reports Pie charts, tables, runtime, etc. -├── test-results Screenshots, videos, etc. +├── test-results Screenshots, videos, etc. (generated on execution) ``` -## Environment Variables - -All environment variables are optional, since they have defaults. -For type details and defaults see [constants.py](pages/constants.py) +## [Coding Guidelines](../doc/guidelines/e2e.md) -- `E2E_URL` - The url where the app is running. - -- `E2E_USERNAME` - The username to login to permaplant. +## Quickstart -- `E2E_PASSWORD` - The password to login to permaplant. +Linux (Debian) is the OS under which these tests are developed and executed. +If you are confronting any issue on different OS, feel free to open a PR or contact someone. -## Quickstart +### Pre-requisites -- Make sure your app is running. +- All commands/scripts in this README are executed from this folder (`/e2e`). +- Make sure PermaplanT's frontend and backend is running. - Make sure the [ENV](#environment-variables) variables are set according to your desire. -- Make sure you have a virtual environment as this will install all python dependencies. +- Make sure you have a virtual environment as this will install all Python dependencies. +- Make sure `python` & `python3-pip` is installed -```sh -./install.sh -./e2e.sh -``` +### Linux -### Creating a virtual env +Create and activate a python virtual environment ```sh -sudo apt update -sudo apt install python3 -sudo apt install python3-venv python3 -m venv venv source venv/bin/activate +``` + +```sh ./install.sh ./e2e.sh ``` -### Inside Docker +For a more detailed execution have a look [here](#optional-arguments) + +### Windows + +Have a look inside install.sh and e2e.sh and perform the same steps in your windows terminal. + +### Docker Assuming your app and database is on your localhost network. @@ -62,175 +75,119 @@ docker run --network="host" permaplant-e2e ./e2e.sh The Jenkins pipeline performs exactly these two steps. So running this dockerfile locally should mirror CI. -### Optional arguments +## Environment Variables -Most of these are already set inside [e2e.sh](e2e.sh). -Nevertheless, if you maybe want to build your own script, here are some pytest arguments. +If any of the below mentioned variables are not set, the tests will fallback to defaults. -#### Parallelization +### E2E env variables -Use as many processes as your computer has CPU cores. +- `E2E_TIMEOUT` (default: `30000`) -```sh -python3 -m pytest -n auto -``` + The timeout (in ms) for a single action (click, navigate, etc.). -#### Retries +- `E2E_URL` (default: `localhost:5173`) -```sh -python3 -m pytest --retries 3 -``` + The url where the app is running. -#### Single test +- `E2E_USERNAME` (default: `Adi`) -```sh -python3 -m pytest steps/test_login_logout.py -``` + The username to login to permaplant. -#### Video capturing +- `E2E_PASSWORD` (default: `1234`) -```sh -python3 -m pytest --video on -``` + The password to login to permaplant. -Only on test failures. +- `BROWSER_LOGS` (default: not set) -```sh -python3 -m pytest --video retain-on-failure -``` + Writes console logs to console_logs.txt -#### Flaky tests +### Project env variables -If there is something suspicious going on. +Furthermore following variables are expected to be set in your environment and required by `clean_db.py`: -```sh -set -e; for i in `seq 10`;do echo "Running iteration $i"; python -m pytest -n auto; done -``` +- `POSTGRES_DB` (default: `permaplant`) +- `POSTGRES_USER` (default: `permaplant`) +- `POSTGRES_PASSWORD` (default: `permaplant`) +- `DATABASE_URL` (default: `postgres://permaplant:permaplant@db/permaplant`) +- `DATABASE_PORT` (default: `5432`) -### Cleanup +## Cleanup -Currently we need to use [clean_db.py](clean_db.py) after/before tests to make all test maps are deleted. +Currently we use [clean_db.py](clean_db.py) after or before tests to make sure all test maps are deleted. If we dont delete them, some tests will fail trying to create a map that already exists. -This is automatically done inside [e2e.sh](e2e.sh)`. - -## How to write tests - -### Guidelines - -Before developing E2E tests make sure you have read the [guidelines](https://github.com/ElektraInitiative/PermaplanT/doc/guidelines/frontend-testing.md). - -### General rules - -- Be consistent and minimalistic. - - Use the same vocabulary as playwright (click, visible, etc.) - - Avoid using multiple different verbs for the same actions, keep your vocabulary small and precise. - Use Playwrights vocabulary. - This means to prefix methods with the actions from playwright (e.g when calling `xyz.click()` from playwright inside that method, name the method `click_xyz()` not a mix of press, click, push etc.) - - Don't indent more than one time. - - Don't make a complicated call stack higher than two from a page object. -- Every test should be independent from other tests (concurrency). -- Name inputs or objects you create SUT (System under Test) so they are clearly marked as test artifacts. - -### A typical workflow - -The usual workflow consists of three steps: - -1. (`/features`) write a feature inside with one or multiple scenarios. -2. (`/pages`) implement the page object (if it doesn't already exist) in . -3. (`/steps`) create a function for each scenario step using the page objects. - -Lets go over these steps in more detail. - -### Writing .feature files - -Make sure to have a solid understanding about the Gherkin syntax, so you don't fall into common pitfalls. -Usually the syntax is not very strict but poor Gherkin will cascade into the later processes of testing and make everything more complicated. - -Avoid the following: - -- Multiple Given, When or Thens in one scenario -- Too verbose/detailed sentences -- Describing the implementation (press, enter, type etc.) - -If you are using more than one "Given-When-Then" You should probably split up your scenario. -Writing a feature file should not be underestimated. -Take your time in defining the problem. - -Here are some good guidelines. - -- https://automationpanda.com/2017/01/30/bdd-101-writing-good-gherkin/ +This is automatically done with [e2e.sh](e2e.sh)`. -- https://cucumber.io/docs/bdd/better-gherkin/ +## Test Results -### Implementing Page Objects +Test results are created after running the tests in `test-results/`. +This folder gets automatically created and deleted on test execution. +It will contain screenshots and videos about failed tests. -ALL tests are performed exclusively with pageobjects, where the selectors and functions are defined inside a class. -If you are not familiar with the page object model look [here](https://www.selenium.dev/documentation/test_practices/encouraged/page_object_models/) if you are familiar with the page object model, still have a look : ). -The pages are all instantiated globally inside `conftest.py` and injected into the step functions as arguments. -When using Playwright for python we are interacting with Playwrights [page class](https://playwright.dev/docs/api/class-page), which is basically representing a tab. -This Playwright page class is then instantiated as a singleton and used globally across all OUR page objects to perform actions on the webapp. -It is crucial to pay close attention what the side effects of OUR page object methods are as some methods will change the webpage and you will have to use a different page object after. +## Test Reports -**The Pagefactory does not exist yet** due to implementation difficulty and for flexible design at the start. -Just use the fixtures instead as they basically do the same. +Test reports are created at the end of tests in `test-reports/`. +There is a html and cucumber report. -### Creating steps with pytest-bdd +### Cucumber Report -When implementing the test steps you should ONLY be invoking methods of page objects to reach your goal. -Implementing complex functionalities inside tests should be STRICTLY avoided and implemented into the corresponding page objects. +Reports are locally inside `/test-reports`. +In CI you can find the cucumber report in +and the HTML report inside -Following the [testing strategy](https://github.com/ElektraInitiative/PermaplanT/tree/master/doc/tests) from PermaplanT: +- python3 -m pytest --cucumberjson=./cucumber.json -- Given should ARRANGE -- When should ACT -- Then should ASSERT +### Html Report -This will ensure the tests are simple and don't perform too much magic all over the place. +- python3 -m pytest --html=test-reports/report.html --self-contained-html -### A small note on pytest-xdist +## Optional arguments -[Pytest-xdist](https://pytest-xdist.readthedocs.io/en/latest/distribution.html) is used to parallelize the tests. -This is done to reduce the pipeline time, since many tests could make this stage take a long time at the end. -When developing tests always keep in mind that each scenario is running on a separate core and should not depend on results of other scenarios. -A scenario outlet will also start each scenario with one core. -Try to avoid too complex parallelization and we also probably don't need to assign and manage worker groups with additional xdist syntax. +If you dont like the default arguments in [e2e.sh](e2e.sh) you can execute the tests with pytest. -### Helpful tools +### Parallelization -#### [Playwright Test generator](https://playwright.dev/python/docs/codegen) +Use as many processes as your computer has CPU cores. -This is the fastest way to get some locators and see how playwright would write the tests. -This should under no circumstances be copy pasted and put into a test but rather help you write the page objects. +```sh +python3 -m pytest -n auto +``` -Launch the playwright codegen from the terminal and create code that you can later refactor into your pageobjects. +### Retries ```sh -playwright codegen http://localhost:5173/ +python3 -m pytest --retries 3 ``` -#### Reporting +### Single test -- python3 -m pytest --cucumberjson=./cucumber.json -- python3 -m pytest --gherkin-terminal-reporter +```sh +python3 -m pytest steps/test_login_logout.py +``` -#### Pytest-bdd Test generator +### Video capturing -- pytest-bdd generate features/login_logout.feature > steps/test_some_feature.py +```sh +python3 -m pytest --video on +``` -Only missing stuff: +Only on test failures. -- pytest --generate-missing --feature features steps/ +```sh +python3 -m pytest --video retain-on-failure +``` ## Other Documentations -### Playwright for Python Documentation +### [Playwright for Python](https://playwright.dev/python/docs/intro) -[https://playwright.dev/python/docs/intro](https://playwright.dev/python/docs/intro) +### [Pytest-playwright](https://playwright.dev/python/docs/test-runners) -### Pytest-bdd Documentation +### [Pytest-bdd](https://pypi.org/project/pytest-bdd/) -[https://pypi.org/project/pytest-bdd/](https://pypi.org/project/pytest-bdd/) +#### Async -If we ever need async functions they might be implemented in the future. +Async functions might be implemented in the future. [PR](https://github.com/pytest-dev/pytest-bdd/pull/629) -[about asyncio with pytest-bdd](https://github.com/pytest-dev/pytest-bdd/issues/223) +[About asyncio with pytest-bdd](https://github.com/pytest-dev/pytest-bdd/issues/223) + +### [Pytest-xdist](https://pytest-xdist.readthedocs.io/en/latest/index.html) diff --git a/e2e/clean_db.py b/e2e/clean_db.py index 325018d0d..234c837b4 100644 --- a/e2e/clean_db.py +++ b/e2e/clean_db.py @@ -33,6 +33,8 @@ def delete_maps_with_sut(dbname, user, password, host, port): # Delete rows with "SUT" in the name from the 'maps' table delete_query = "DELETE FROM maps WHERE name LIKE '%SUT%';" cursor.execute(delete_query) + delete_query = "DELETE FROM seeds WHERE name LIKE '%SUT%';" + cursor.execute(delete_query) # Commit the changes conn.commit() diff --git a/e2e/conftest.py b/e2e/conftest.py index 0552273e0..0240fb51a 100644 --- a/e2e/conftest.py +++ b/e2e/conftest.py @@ -1,3 +1,4 @@ +import uuid import pytest from dotenv import load_dotenv from playwright.sync_api import Page @@ -15,6 +16,11 @@ load_dotenv() +@pytest.fixture +def worker_uuid(worker_id): + return "-" + worker_id + "-" + str(uuid.uuid4()) + + @pytest.fixture def hp(page: Page) -> HomePage: return HomePage(page) diff --git a/e2e/e2e.sh b/e2e/e2e.sh index a5dd4f919..202ab71e0 100755 --- a/e2e/e2e.sh +++ b/e2e/e2e.sh @@ -1,4 +1,4 @@ #!/bin/bash -# Run the pytest command +python3 clean_db.py python3 -m pytest -n auto --reruns 2 --reruns-delay 5 --rerun-except AssertionError --video retain-on-failure --html=test-reports/report.html --self-contained-html --cucumberjson=test-reports/cucumber.json diff --git a/e2e/features/base_layer.feature b/e2e/features/base_layer.feature index 4cfcee460..b52d4c870 100644 --- a/e2e/features/base_layer.feature +++ b/e2e/features/base_layer.feature @@ -1,17 +1,17 @@ -Feature: PermaplanT base layer +Feature: Base layer As a user I want to be able to have a base layer Background: - Given I am on the SUTBaseLayer map page and I have selected the base layer + Given I am on the SUT-BaseLayer map page and I have selected the base layer Scenario: Successfully select a background image When I select a background image - Then /Photos/Birdie.jpg stays even when I leave SUTBaseLayer and come back later + Then /Photos/Birdie.jpg stays even when I leave SUT-BaseLayer and come back later Scenario: Successfully rotate a background image When I change the rotation of the image to 45 degrees - Then SUTBaseLayer image rotation is set to 45 degrees + Then SUT-BaseLayer image rotation is set to 45 degrees Scenario: Successfully scale a background image When I change the scale of the image to 120 percent - Then SUTBaseLayer image scale is set to 120 percent + Then SUT-BaseLayer image scale is set to 120 percent diff --git a/e2e/features/layer_visibility.feature b/e2e/features/layer_visibility.feature index 6427d4c0a..8613a99b9 100644 --- a/e2e/features/layer_visibility.feature +++ b/e2e/features/layer_visibility.feature @@ -1,15 +1,15 @@ -Feature: PermaplanT planting +Feature: Layer Visibility As a user I want to be able to change the layer visibilities Scenario: Successfully change plant layer visibility - Given I am on the SUTLayerVisibility map page and I have selected the plant layer + Given I am on the SUT-LayerVisibility map page and I have selected the plant layer When I plant something And I turn the plant layer visibility off Then all plants are invisible Scenario: Successfully change base layer visibility - Given I am on the SUTLayerVisibility map page and I have selected the base layer - And I have an empty canvas before + Given I am on the SUT-LayerVisibility map page and I have selected the base layer + And I capture a screenshot of the canvas When I select a background image And I turn the base layer visiblity off - Then the base layer is invisible + Then the canvas looks like the captured screenshot diff --git a/e2e/features/map_creation.feature b/e2e/features/map_creation.feature index 219a0a004..bc6ffe7ae 100644 --- a/e2e/features/map_creation.feature +++ b/e2e/features/map_creation.feature @@ -9,17 +9,17 @@ Feature: Map Creation Then I can successfully create without an error message Examples: | name | privacy | description | latitude | longitude | - | SUTPrivateMap | private | A private TestMap | 25 | 25 | - | SUTProtectedMap | protected | A protected TestMap | 1 | 1 | - | SUTPublicMap | public | A public TestMap | 33 | 33 | + | SUT-PrivateMap | private | A private TestMap | 25 | 25 | + | SUT-ProtectedMap | protected | A protected TestMap | 1 | 1 | + | SUT-PublicMap | public | A public TestMap | 33 | 33 | Scenario: Edit existing Map - Given I create a new map SUTEditMap - When I edit SUTEditMap to SUTEditedMap with EditedDescription - Then I can successfully save SUTEditedMap without an error message + Given I create a new map SUT-EditMap + When I edit SUT-EditMap to SUT-EditedMap with EditedDescription + Then I can successfully save SUT-EditedMap without an error message Scenario: Map already exists - Given I create a map SUTSameMap - When I try to create the same map SUTSameMap + Given I create a map SUT-SameMap + When I try to create the same map SUT-SameMap Then the app displays an error message - And my map SUTSameMap is not created + And my map SUT-SameMap is not created diff --git a/e2e/features/planting.feature b/e2e/features/planting.feature index 01048630a..82a08f507 100644 --- a/e2e/features/planting.feature +++ b/e2e/features/planting.feature @@ -2,8 +2,8 @@ Feature: Planting As a user I want to be able plant something on my map Background: - Given I am on the SUTPlanting map page and I have selected the plant layer + Given I am on the SUT-Planting map page and I have selected the plant layer Scenario: Successfully planting a plant When I place a tomato on the canvas - Then it stays on SUTPlanting even when I leave the page and come back later + Then it stays on SUT-Planting even when I leave the page and come back later diff --git a/e2e/features/planting_select.feature b/e2e/features/planting_select.feature new file mode 100644 index 000000000..8d1bb9988 --- /dev/null +++ b/e2e/features/planting_select.feature @@ -0,0 +1,9 @@ +Feature: Planting Select + As a user I want to be able to select multiple plants + + Background: + Given I am on the SUT-PlantingSelect map page and I have planted something + + Scenario: Successfully selecting plants + When I drag a select box over the canvas + Then the plant is selected diff --git a/e2e/features/search_plants.feature b/e2e/features/search_plants.feature index f6acc8ea1..ebf409725 100644 --- a/e2e/features/search_plants.feature +++ b/e2e/features/search_plants.feature @@ -1,8 +1,8 @@ -Feature: Planting Plant Search +Feature: Plant Search As a user I want to able to search for plants Background: - Given I am on the SUTSearching map page and I have selected the plant layer + Given I am on the SUT-Searching map page and I have selected the plant layer Scenario Outline: Searching for plants with exact/partial matches When I type into the search box diff --git a/e2e/features/seeds.feature b/e2e/features/seeds.feature new file mode 100644 index 000000000..2eac8d544 --- /dev/null +++ b/e2e/features/seeds.feature @@ -0,0 +1,27 @@ +Feature: Seed Creation + As a user I want to be able to create seeds + + Background: + Given I am on the seed management page + + Scenario: Successful seed creation + When I create a new seed + Then I can successfully create a new seed without an error message + + Scenario: Successful seed editing + When I create another new seed + And I edit this seed + Then the edited seed is saved and shown on the overview page + + Scenario: Successful seed search + When I search for a seed + Then I can see the seed in the overview page + + Scenario: Searching seed that does not exists + When I search for a seed that does not exist + Then the search result is empty + + Scenario: Archving a seed + When I try to archive a seed + Then the seed disapears + And I have the possiblity to restore it diff --git a/e2e/features/timeline.feature b/e2e/features/timeline.feature index 31ab6675d..ac9272998 100644 --- a/e2e/features/timeline.feature +++ b/e2e/features/timeline.feature @@ -2,9 +2,9 @@ Feature: Planting Timeline As a user I want to be able to track timelines of my plants Background: - Given I am on the SUTTimeline map page and I have planted something + Given I am on the SUT-Timeline map page and I have planted something - Scenario: Hide a plant by changing the map date + Scenario: Hide a plant by changing the map date by day When I change the map date to yesterday Then the plant disappears @@ -16,17 +16,23 @@ Feature: Planting Timeline When I change the plants removed date to yesterday Then the plant disappears - Scenario: Unhide a plant by changing the map date + Scenario: Unhide a plant by changing the map date by day When I change the plants added date to tomorrow And I change the map date to tomorrow Then the plant appears - Scenario: Unhide a plant by changing its added date - When I change the plants added date to yesterday - And I change the plants added date to tomorrow + Scenario: Hide a plant by changing the map date by month + When I change the map date to last month + Then the plant disappears + + Scenario: Unhide a plant by changing the map date by month + When I change the map date to next month Then the plant appears - Scenario: Unhide a plant by changing its removed date - When I change the plants removed date to yesterday - And I change the plants removed date to tomorrow + Scenario: Hide a plant by changing the map date by year + When I change the map date to last year + Then the plant disappears + + Scenario: Unhide a plant by changing the map date by year + When I change the map date to next year Then the plant appears diff --git a/e2e/features/undo_redo.feature b/e2e/features/undo_redo.feature index 8ec1cad30..2b483c7c3 100644 --- a/e2e/features/undo_redo.feature +++ b/e2e/features/undo_redo.feature @@ -1,8 +1,8 @@ -Feature: Planting Undo/Redo +Feature: Undo/Redo As a user I want to be able to undo and redo actions Background: - Given I am on the SUTUndoRedo map page and I have planted something + Given I am on the SUT-UndoRedo map page and I have planted something Scenario: Successful undo When I click undo @@ -10,5 +10,10 @@ Feature: Planting Undo/Redo Scenario: Successful redo after accidental undo When I accidentally clicked undo after planting one plant - And I click redo to get my plant back + And I click redo + Then I can see my plant on the canvas again + + Scenario: Successful undo deletion + When I delete the plant + And I click undo Then I can see my plant on the canvas again diff --git a/e2e/install.sh b/e2e/install.sh index c8bf4a619..8f456613c 100755 --- a/e2e/install.sh +++ b/e2e/install.sh @@ -1,3 +1,4 @@ #!/bin/bash + python3 -m pip install -r requirements.txt python3 -m playwright install chromium --with-deps diff --git a/e2e/pages/abstract_page.py b/e2e/pages/abstract_page.py index 219f7cdc1..7772981d8 100644 --- a/e2e/pages/abstract_page.py +++ b/e2e/pages/abstract_page.py @@ -1,9 +1,18 @@ import re +import os +import time from abc import ABC from e2e.pages.constants import E2E_TIMEOUT from playwright.sync_api import Page, expect +def log_errors(msg): + file1 = open("console_logs.txt", "a") # append mode + for arg in msg.args: + file1.write(f"{arg.json_value()}\n") + file1.flush() + + class AbstractPage(ABC): URL: str TITLE: str @@ -23,10 +32,14 @@ def load(self) -> None: ) self.dont_load_images_except_birdie() self._page.set_default_timeout(timeout=E2E_TIMEOUT) + + if "BROWSER_LOGS" in os.environ: + self._page.on("console", log_errors) + expect.set_options(timeout=E2E_TIMEOUT) response = self._page.goto(self.URL) assert response.status == 200 - self._page.wait_for_timeout(1000) + time.sleep(1) self.verify() self.click_english_translation() diff --git a/e2e/pages/home.py b/e2e/pages/home.py index 90c47bcc0..3fad15af8 100644 --- a/e2e/pages/home.py +++ b/e2e/pages/home.py @@ -2,6 +2,8 @@ from e2e.pages.constants import E2E_URL from e2e.pages.abstract_page import AbstractPage +from e2e.pages.maps.management import MapManagementPage +from e2e.pages.inventory.management import SeedManagementPage class HomePage(AbstractPage): @@ -17,6 +19,7 @@ def __init__(self, page: Page) -> None: self._logout_button = page.get_by_role("button", name="Log out") self._hello_message = page.get_by_text(self.HELLO_MSG, exact=True) self._map_management_button = page.get_by_role("button", name="Maps") + self._inventory_button = page.get_by_role("button", name="Inventory") def login_button_is_visible(self): expect(self._login_button).to_be_visible() @@ -25,7 +28,9 @@ def click_login_button(self): """ Clicks the login button which navigates to the `LoginPage`. + Waits for the backend to be ready before doing so. """ + self._page.get_by_test_id("login-button__is-loading").wait_for(state="detached") self._login_button.click() def click_logout_button(self): @@ -35,9 +40,15 @@ def click_logout_button(self): def hello_message_is_visible(self): expect(self._hello_message).to_be_visible() - def to_map_management_page(self): + def to_map_management_page(self) -> MapManagementPage: """ Navigates to `MapManagementPage`. """ self._map_management_button.click() self._page.wait_for_url("**/maps") + return MapManagementPage(self._page) + + def to_inventory_page(self) -> SeedManagementPage: + self._inventory_button.click() + self._page.wait_for_url("**/seeds") + return SeedManagementPage(self._page) diff --git a/e2e/pages/inventory/create.py b/e2e/pages/inventory/create.py new file mode 100644 index 000000000..3ae86da5c --- /dev/null +++ b/e2e/pages/inventory/create.py @@ -0,0 +1,79 @@ +from playwright.sync_api import Page +from e2e.pages.constants import E2E_URL +from e2e.pages.abstract_page import AbstractPage + + +class SeedCreatePage(AbstractPage): + """The seed creation page of permaplant""" + + URL: str = E2E_URL + "seeds/new" + TITLE: str = "PermaplanT" + + def __init__(self, page: Page): + self._page = page + self._plant_name = page.get_by_test_id("paginated-select-menu__Plant Name") + self._additional_name = page.get_by_label("Additional Name") + self._harvest_year = page.get_by_test_id("create-seed-form__harvest_year") + self._amount = page.get_by_test_id("select-menu__Amount-select") + self._best_by = page.get_by_test_id("create-seed-form__best_by") + self._origin = page.get_by_test_id("create-seed-form__origin") + self._quality = page.get_by_test_id("select-menu__Quality-select") + self._taste = page.get_by_test_id("create-seed-form__taste") + self._yield = page.get_by_test_id("create-seed-form__yield") + self._price = page.get_by_test_id("create-seed-form__price") + self._generation = page.get_by_test_id("create-seed-form__generation") + self._notes = page.get_by_test_id("create-seed-form__notest") + self._create_button = page.get_by_role("button", name="Create Seed") + + def create_a_seed( + self, + plant_name="Indian abelia (Abelia triflora)", + additional_name="SUT create", + harvest_year="2022", + amount="Enough", + best_by="2023-10-18", + origin="Origin SUT", + quality="Organic", + taste="Taste SUT", + _yield="5", + price="22", + generation="11", + ): + """ + Helper function to create a seed + Fills out fields and clicks create at the end + which navigate to the `SeedManagementPage` + """ + self._plant_name.click() + self._page.get_by_text(plant_name, exact=True).click() + + self._additional_name.fill(additional_name) + + self._harvest_year.fill(harvest_year) + + self._amount.click() + self._page.get_by_text(amount, exact=True).click() + + self._best_by.fill(best_by) + + self._origin.fill(origin) + + self._quality.click() + self._page.get_by_text(quality, exact=True).click() + + self._taste.fill(taste) + + self._yield.fill(_yield) + + self._price.fill(price) + + self._generation.fill(generation) + + self.click_create() + + def click_create(self): + """ + Creates seeds and navigates back to the `MapManagementPage` + """ + self._create_button.click() + self._page.wait_for_url("**/seeds") diff --git a/e2e/pages/inventory/edit.py b/e2e/pages/inventory/edit.py new file mode 100644 index 000000000..637816f81 --- /dev/null +++ b/e2e/pages/inventory/edit.py @@ -0,0 +1,68 @@ +from playwright.sync_api import Page +from e2e.pages.constants import E2E_URL +from e2e.pages.abstract_page import AbstractPage + + +class SeedEditPage(AbstractPage): + """The seed details page of permaplant""" + + URL: str = E2E_URL + "seeds/*/edit" + TITLE: str = "PermaplanT" + + def __init__(self, page: Page): + self._page = page + self._plant_name = page.get_by_test_id("paginated-select-menu__Plant Name") + self._additional_name = page.get_by_label("Additional Name") + self._harvest_year = page.get_by_test_id("create-seed-form__harvest_year") + self._amount = page.get_by_test_id("select-menu__Amount-select") + self._best_by = page.get_by_test_id("create-seed-form__best_by") + self._origin = page.get_by_test_id("create-seed-form__origin") + self._quality = page.get_by_test_id("select-menu__Quality-select") + self._taste = page.get_by_test_id("create-seed-form__taste") + self._yield = page.get_by_test_id("create-seed-form__yield") + self._price = page.get_by_test_id("create-seed-form__price") + self._generation = page.get_by_test_id("create-seed-form__generation") + self._notes = page.get_by_test_id("create-seed-form__notest") + self._edit_button = page.get_by_role("button", name="Edit seed") + + def set_plant_name(self): + pass + + def set_additional_name(self, name): + self._additional_name.fill(name) + + def set_harvest_year(self): + self._harvest_year.fill("2022") + + def set_best_by(self): + self._best_by.fill("2023-11-22") + + def set_origin(self, origin): + self._origin.fill(origin) + + def set_amount(self, amount: str): + self._amount.click() + self._page.get_by_text(amount, exact=True).click() + + def set_quality(self, quality: str): + self._quality.click() + self._page.get_by_text(quality, exact=True).click() + + def set_taste(self, taste: str): + self._taste.fill(taste) + + def set_yield(self, _yield: str): + self._yield.fill(_yield) + + def set_price(self, price: str): + self._price.fill(price) + + def set_generation(self, generation: str): + self._generation.fill(generation) + + def click_edit(self): + """ + Edits seeds and navigates back to the `MapManagementPage` + """ + self._edit_button.click() + self._page.wait_for_url("**/seeds/") diff --git a/e2e/pages/inventory/management.py b/e2e/pages/inventory/management.py new file mode 100644 index 000000000..f1f99c764 --- /dev/null +++ b/e2e/pages/inventory/management.py @@ -0,0 +1,60 @@ +from playwright.sync_api import Page, expect +from e2e.pages.constants import E2E_URL +from e2e.pages.abstract_page import AbstractPage +from e2e.pages.inventory.create import SeedCreatePage +from e2e.pages.inventory.edit import SeedEditPage + + +class SeedManagementPage(AbstractPage): + """The seed management page of permaplant""" + + URL: str = E2E_URL + "/seeds" + TITLE: str = "PermaplanT" + + def __init__(self, page: Page): + self._page = page + self._new_entry_button = page.get_by_role("button", name="New Entry") + self._search_field = page.get_by_test_id("search-input__input-field") + + def search(self, input): + self._search_field.fill(input) + + def delete_seed(self, seed_name): + self._page.get_by_role("row", name=seed_name).get_by_test_id( + "delete-seed-button" + ).click() + + def archive_seed(self, name): + """Archive the seed in the first row of the seeds table""" + self._page.get_by_role("row", name=name).get_by_test_id( + "seed-overview-list__archive-button" + ).click() + + def to_seed_create_page(self) -> SeedCreatePage: + """Navigates to `SeedCreatePage`""" + self._new_entry_button.click() + self._page.wait_for_url("**/seeds/create") + return SeedCreatePage(self._page) + + def to_seed_edit_page(self, seed_name) -> SeedEditPage: + """Navigates to `SeedEditPage`""" + self._page.get_by_role("row", name=seed_name).get_by_test_id( + "seed-overview-list__edit-button" + ).click() + self._page.wait_for_url("**/seeds/*/edit") + return SeedEditPage(self._page) + + def expect_first_row_cell_exists(self, cell_value): + """Expects a cell to exist on the seed management page in the first row of the table""" + expect( + self._page.get_by_role("cell", name=cell_value, exact=True).first + ).to_be_visible() + + def expect_first_row_cell_does_not_exist(self, cell_value): + """Expects a cell to exist on the seed management page in the first row of the table""" + expect( + self._page.get_by_role("cell", name=cell_value, exact=True).first + ).to_be_hidden() + + def expect_restore_button_to_be_visible(self): + self._page.get_by_role("button", name="Restore") diff --git a/e2e/pages/maps/create.py b/e2e/pages/maps/create.py index beca1d995..35844acd4 100644 --- a/e2e/pages/maps/create.py +++ b/e2e/pages/maps/create.py @@ -6,7 +6,7 @@ class MapCreatePage(AbstractPage): """The map creation page of permaplant""" - URL: str = E2E_URL + "/create" + URL: str = E2E_URL + "maps/create" TITLE: str = "PermaplanT" def __init__(self, page: Page): @@ -16,12 +16,13 @@ def __init__(self, page: Page): self._longitude = page.get_by_placeholder("Longitude") self._latitude = page.get_by_placeholder("Latitude") self._create_button = page.get_by_role("button", name="Create") + self._privacy_select = page.get_by_test_id("map-create-form__select-privacy") def create_a_map( self, mapname, - privacy=None, - description="SUTDescription", + privacy="private", + description="SUT-Description", latitude="1", longitude="1", ): @@ -31,7 +32,7 @@ def create_a_map( which navigate to the `MapManagementPage` """ self.fill_name(mapname) - # mcp.select_privacy(privacy) + self.select_privacy(privacy) self.fill_description(description) self.fill_latitude(latitude) self.fill_longitude(longitude) @@ -41,7 +42,7 @@ def fill_name(self, name: str): self._name.fill(name) def select_privacy(self, privacy: str): - self._page.locator("select").select_option(privacy) + self._privacy_select.select_option(privacy) def fill_description(self, description: str): self._description.fill(description) diff --git a/e2e/pages/maps/edit.py b/e2e/pages/maps/edit.py index 0b4de6f78..376e5efde 100644 --- a/e2e/pages/maps/edit.py +++ b/e2e/pages/maps/edit.py @@ -6,7 +6,7 @@ class MapEditPage(AbstractPage): """The map editing page of permaplant""" - URL: str = E2E_URL + "/edits" + URL: str = E2E_URL + "/edit" TITLE: str = "PermaplanT" def __init__(self, page: Page): diff --git a/e2e/pages/maps/management.py b/e2e/pages/maps/management.py index 4244fead1..abfa5dee2 100644 --- a/e2e/pages/maps/management.py +++ b/e2e/pages/maps/management.py @@ -1,6 +1,8 @@ from playwright.sync_api import Page, expect from e2e.pages.constants import E2E_URL from e2e.pages.abstract_page import AbstractPage +from e2e.pages.maps.create import MapCreatePage +from e2e.pages.maps.planting import MapPlantingPage class MapManagementPage(AbstractPage): @@ -13,10 +15,11 @@ def __init__(self, page: Page): self._page = page self._create_button = page.get_by_role("button", name="New Map") - def to_map_create_page(self): + def to_map_create_page(self) -> MapCreatePage: """Navigates to `MapCreatePage`""" self._create_button.click() self._page.wait_for_url("**/create") + return MapCreatePage(self._page) def to_map_edit_page(self, mapname: str): """Navigates to `MapEditPage`""" @@ -25,10 +28,11 @@ def to_map_edit_page(self, mapname: str): ).click() self._page.wait_for_url("**/edit") - def to_map_planting_page(self, mapname: str): + def to_map_planting_page(self, mapname: str) -> MapPlantingPage: """Navigates to `MapPlantingPage`""" self._page.get_by_text(mapname, exact=True).click() self._page.wait_for_url("**/maps/*") + return MapPlantingPage(self._page) def expect_mapname_is_visible(self, mapname: str): """Checks if the given map exists on the map management screen""" diff --git a/e2e/pages/maps/planting.py b/e2e/pages/maps/planting.py index cd25910cd..001834da5 100644 --- a/e2e/pages/maps/planting.py +++ b/e2e/pages/maps/planting.py @@ -1,4 +1,5 @@ import cv2 +import time import numpy as np from datetime import datetime, timedelta from playwright.sync_api import Page, expect, TimeoutError as PlaywrightTimeoutError @@ -15,34 +16,74 @@ class MapPlantingPage(AbstractPage): def __init__(self, page: Page): self._page = page self._screenshot = None + """ Selectors """ # Navbar self._map_management_button = page.get_by_role("button", name="Maps") + # Left side bar + self._added_date_state_idle = page.get_by_test_id( + "planting-attribute-edit-form__add-date-idle" + ) + self._removed_on_state_idle = page.get_by_test_id( + "planting-attribute-edit-form__removed-on-idle" + ) self._delete_plant_button = page.get_by_role("button", name="Delete Planting") + # Layer visibility - self._hide_plant_layer = page.get_by_test_id("plants-layer-visibility-icon") - self._hide_base_layer = page.get_by_test_id("base-layer-visibility-icon") + self._hide_plant_layer = page.get_by_test_id( + "layer-list-item__plants-layer-visibility-icon" + ) + self._hide_base_layer = page.get_by_test_id( + "layer-list-item__base-layer-visibility-icon" + ) + # Canvas - self._canvas = page.get_by_test_id("canvas") - self._close_selected_plant = page.get_by_test_id("canvas").get_by_role("button") - self._map_date = page.get_by_label("Change map date") + self._canvas = page.get_by_test_id("base-stage__canvas") + self._close_selected_plant = self._canvas.get_by_role("button") + + # Timeline + self._timeline_day_slider = page.get_by_test_id("timeline__day-slider") + self._timeline_month_slider = page.get_by_test_id("timeline__month-slider") + self._timeline_year_slider = page.get_by_test_id("timeline__year-slider") + self._timeline_idle = page.get_by_test_id("timeline__state-idle") + # Top left section - self._undo_button = page.get_by_test_id("undo-button") - self._redo_button = page.get_by_test_id("redo-button") + self._undo_button = page.get_by_test_id("map__undo-button") + self._redo_button = page.get_by_test_id("map__redo-button") + # Selected plant section self._plant_added_date = page.get_by_label("Added on") self._plant_removed_date = page.get_by_label("Removed on") + # Plant layer - self._base_layer_radio = page.get_by_test_id("base-layer-radio") - self._plant_layer_radio = page.get_by_test_id("plants-layer-radio") - self._plant_search_icon = page.get_by_test_id("plant-search-icon") - self._plant_search_input = page.get_by_test_id("plant-search-input") + self._base_layer_radio = page.get_by_test_id( + "layer-list-item__base-layer-radio" + ) + self._plant_layer_radio = page.get_by_test_id( + "layer-list-item__plants-layer-radio" + ) + self._plant_search_icon = page.get_by_test_id("plant-search__icon-button") + self._plant_search_input = page.get_by_test_id("plant-search__search-input") + # Base layer - self._background_select = page.get_by_test_id("baseBackgroundSelect") + self._background_image_file_path = page.get_by_test_id( + "base-layer-right-toolbar__background-input" + ) + self._background_image_file_path_idle = page.get_by_test_id( + "base-layer-attribute-edit-form__background-image-file-path-idle" + ) self._background_button = page.get_by_role("button", name="Choose an image") - self._rotation_input = page.get_by_test_id("rotation-input") - self._scale_input = page.get_by_test_id("scale-input") + self._rotation_input = page.get_by_test_id( + "base-layer-right-toolbar__rotation-input" + ) + self._rotation_input_idle = page.get_by_test_id( + "base-layer-attribute-edit-form__rotation-idle" + ) + self._scale_input = page.get_by_test_id("base-layer-right-toolbar__scale-input") + self._scale_input_idle = page.get_by_test_id( + "base-layer-attribute-edit-form__scale-idle" + ) """ACTIONS""" @@ -56,18 +97,47 @@ def check_plant_layer(self): def change_map_date_by_days(self, delta_days: int): """Changes the date by a given amount of days.""" - day = datetime.today() + timedelta(days=delta_days) - self._map_date.fill(day.strftime("%Y-%m-%d")) + if delta_days > 0: + for i in range(delta_days): + self._timeline_day_slider.press("ArrowRight") + else: + for i in range(abs(delta_days)): + self._timeline_day_slider.press("ArrowLeft") + self._timeline_idle.wait_for() + + def change_map_date_by_months(self, delta_months: int): + """Changes the date by a given amount of months.""" + if delta_months > 0: + for i in range(delta_months): + self._timeline_month_slider.press("ArrowRight") + else: + for i in range(abs(delta_months)): + self._timeline_month_slider.press("ArrowLeft") + self._timeline_idle.wait_for() + + def change_map_date_by_years(self, delta_years: int): + """Changes the date by a given amount of years.""" + if delta_years > 0: + for i in range(delta_years): + self._timeline_year_slider.press("ArrowRight") + else: + for i in range(abs(delta_years)): + self._timeline_year_slider.press("ArrowLeft") + self._timeline_idle.wait_for() def change_plant_added_date_by_days(self, delta_days: int): - """Changes the date by a given amount of days.""" + """Changes the date by a given amount of days and checks the spinner if possible.""" day = datetime.today() + timedelta(days=delta_days) self._plant_added_date.fill(day.strftime("%Y-%m-%d")) + if delta_days < 0: + self._added_date_state_idle.wait_for() def change_plant_removed_date_by_days(self, delta_days: int): - """Changes the date by a given amount of days.""" + """Changes the date by a given amount of days and checks the spinner if possible.""" day = datetime.today() + timedelta(days=delta_days) self._plant_removed_date.fill(day.strftime("%Y-%m-%d")) + if delta_days > 0: + self._removed_on_state_idle.wait_for() def fill_plant_search(self, plantname): """Clicks the search icon and types plantname into the plant search.""" @@ -76,21 +146,16 @@ def fill_plant_search(self, plantname): def fill_rotation(self, rotation): """Fills the rotation input according to rotation.""" self._rotation_input.fill(rotation) - # Wait for input to be processed and saved - self._page.wait_for_timeout(2000) + self._rotation_input_idle.wait_for() def fill_scaling(self, scaling): """Fills the scaling input according to scaling.""" self._scale_input.fill(scaling) - # Wait for input to be processed and saved - self._page.wait_for_timeout(2000) + self._scale_input_idle.wait_for() def close_tour(self): """ - Currently you get greeted with the tour. - In case you rerun the tests locally or if this gets removed - it is wrapped, so it does not fail the tests. - The timeout is also lowered to not slow down the tests by a lot. + Closes the tour window and catches the exception if the tour window does not exist. """ try: tour_close = self._page.get_by_label("Close Tour") @@ -110,15 +175,39 @@ def click_search_icon(self): def click_plant_from_search_results(self, plant_name): """Selects a plant by name from the search results.""" - self._page.get_by_test_id(plant_name + "-plant-search-result").click() + self._page.get_by_test_id("plant-list-item__" + plant_name).click() def click_on_canvas_middle(self): """Clicks in the middle of the canvas with a 300ms delay.""" + time.sleep(1) box = self._canvas.bounding_box() + self._page.mouse.move(box["x"] + box["width"] / 2, box["y"] + box["height"] / 2) self._page.mouse.click( box["x"] + box["width"] / 2, box["y"] + box["height"] / 2 ) - self._page.wait_for_timeout(300) + + def drag_select_box_over_canvas(self): + """Drags a select box over 75% of the canvas from top left to bottom right""" + time.sleep(1) + box = self._canvas.bounding_box() + x = box["x"] + y = box["y"] + width = box["width"] + height = box["height"] + self._page.mouse.move(x + width / 4, y + height / 4) + self._page.mouse.down() + self._page.mouse.move(x + (width / 4) * 3, y + (height / 4) * 3) + # https://playwright.dev/docs/input#drag-and-drop:~:text=()%3B-,NOTE,-If%20your%20page + # I dont know why, but it works only with a second mouse.move() + self._page.mouse.move(x + (width / 4) * 3, y + (height / 4) * 3) + self._page.mouse.up() + + def click_on_canvas_top_left(self): + """Clicks on the top left area of the canvas.""" + box = self._canvas.bounding_box() + self._page.mouse.click( + box["x"] + box["width"] / 4, box["y"] + box["height"] / 4 + ) def click_delete(self): """Deletes a planting by clicks on the delete button.""" @@ -140,17 +229,15 @@ def select_birdie_background(self): """ self._background_button.click() self._page.get_by_text("Birdie.jpg").click() - # Delay so image can be rendered on canvas - self._page.wait_for_timeout(2000) + self._background_image_file_path_idle.wait_for() def click_hide_base_layer(self): self._hide_base_layer.click() def click_hide_plant_layer(self): self._hide_plant_layer.click() - self._page.wait_for_timeout(5000) - def screenshot_canvas(self, timeout=300): + def screenshot_canvas(self, timeout=3): """ Takes a grayscale screenshot of the canvas. Has a default 300ms delay before taking the screenshot, @@ -158,20 +245,22 @@ def screenshot_canvas(self, timeout=300): Parameters ---------- - timeout : int, optional, default=500 + timeout : int, optional, default=3 in seconds Timeout in ms before taking the screenshot. Returns ------- opencv2 image object of the screenshot """ - self._page.wait_for_timeout(timeout) + time.sleep(timeout) buffer = self._canvas.screenshot() self._screenshot = cv2.imdecode( np.frombuffer(buffer, dtype=np.uint8), cv2.IMREAD_GRAYSCALE ) return self._screenshot + """ASSERTIONS""" + def expect_canvas_equals_last_screenshot(self, test_name, rtol=0, atol=5): """ This method compares the last taken screenshot of the canvas @@ -233,16 +322,14 @@ def expect_no_plant_on_canvas(self): def expect_search_result_is_visible(self, result): """Confirms that a search result is visible.""" - expect( - self._page.get_by_test_id(result + "-plant-search-result") - ).to_be_visible() + expect(self._page.get_by_test_id("plant-list-item__" + result)).to_be_visible() def expect_no_plants_found_text_is_visible(self): """Confirms that `no plants are found` is present.""" - expect(self._page.get_by_test_id("plant-search-results-empty")).to_be_visible() + expect(self._page.get_by_test_id("plant-search__empty-results")).to_be_visible() def expect_background_image(self, name): - expect(self._background_select).to_have_value(name) + expect(self._background_image_file_path).to_have_value(name) def expect_rotation_to_have_value(self, val): """Expects that the rotation is properly set.""" diff --git a/e2e/steps/common_steps.py b/e2e/steps/common_steps.py index 2976f9650..a6ee605d7 100644 --- a/e2e/steps/common_steps.py +++ b/e2e/steps/common_steps.py @@ -1,14 +1,31 @@ """ Frequently used steps in other tests """ - -from pytest_bdd import given, when, parsers - +from pytest_bdd import given, when, then, parsers +from playwright.sync_api import Page from e2e.pages.home import HomePage from e2e.pages.login import LoginPage from e2e.pages.maps.create import MapCreatePage from e2e.pages.maps.management import MapManagementPage from e2e.pages.maps.planting import MapPlantingPage +from e2e.pages.inventory.management import SeedManagementPage + + +@given("I am on the seed management page") +def smp(page: Page) -> SeedManagementPage: + """ + Login -> Seed Management Page + + Returns: + ------- + `SeedManagementPage` object + """ + hp = HomePage(page) + lp = LoginPage(page) + login(hp, lp) + smp = hp.to_inventory_page() + smp.verify() + return smp @given( @@ -24,7 +41,7 @@ def on_a_map_page_with_layer( mpp: MapPlantingPage, mapname, layer, - worker_id, + worker_uuid, ): """ Login -> create a map -> enter the map -> close tour -> @@ -38,8 +55,8 @@ def on_a_map_page_with_layer( hp.to_map_management_page() mmp.verify() mmp.to_map_create_page() - mcp.create_a_map(mapname + worker_id) - mmp.to_map_planting_page(mapname + worker_id) + mcp.create_a_map(mapname + worker_uuid) + mmp.to_map_planting_page(mapname + worker_uuid) mpp.verify() mpp.close_tour() if layer == "plant": @@ -52,11 +69,16 @@ def on_a_map_page_with_layer( @given(parsers.parse("I am on the {mapname} map page and I have planted something")) -def given_on_map_page_and_planted(hp, lp, mmp, mcp, mpp, mapname, worker_id): - on_a_map_page_with_layer(hp, lp, mmp, mcp, mpp, mapname, "plant", worker_id) +def given_on_map_page_and_planted(hp, lp, mmp, mcp, mpp, mapname, worker_uuid): + on_a_map_page_with_layer(hp, lp, mmp, mcp, mpp, mapname, "plant", worker_uuid) plant_a_tomato(mpp) +@given("I capture a screenshot of the canvas") +def canvas_first_screenshot(mpp: MapPlantingPage): + mpp.screenshot_canvas() + + def login(hp: HomePage, lp: LoginPage) -> HomePage: """ Login to permaplant and close the login notification. @@ -94,3 +116,8 @@ def plant_a_tomato(mpp: MapPlantingPage) -> MapPlantingPage: @when(parsers.parse("I select a background image")) def select_background_image(mpp: MapPlantingPage): mpp.select_birdie_background() + + +@then("the canvas looks like the captured screenshot") +def canvas_second_screenshot(mpp: MapPlantingPage, request): + mpp.expect_canvas_equals_last_screenshot(request.node.name) diff --git a/e2e/steps/test_base_layer.py b/e2e/steps/test_base_layer.py index 344ba40f4..ee2737b6a 100644 --- a/e2e/steps/test_base_layer.py +++ b/e2e/steps/test_base_layer.py @@ -12,53 +12,54 @@ parsers.parse("{image_name} stays even when I leave {map_name} and come back later") ) def background_image_stays( - mpp: MapPlantingPage, mmp: MapManagementPage, image_name, map_name, worker_id + mpp: MapPlantingPage, mmp: MapManagementPage, image_name, map_name, worker_uuid ): mpp.to_map_management_page() mmp.verify() - mmp.to_map_planting_page(map_name + worker_id) + mmp.to_map_planting_page(map_name + worker_uuid) + mpp.check_base_layer() mpp.expect_background_image(image_name) # Scenario 2: Successfully rotate the background image -# This test might be flaky, since it depends on the image to be loaded before -# the rotation is applied and to process the input before navigating away from the page. @when(parsers.parse("I change the rotation of the image to {val} degrees")) def change_rotation(mpp: MapPlantingPage, val): mpp.select_birdie_background() mpp.expect_background_image("/Photos/Birdie.jpg") + mpp.check_base_layer() mpp.fill_rotation(val) @then(parsers.parse("{map_name} image rotation is set to {val} degrees")) def rotation_gets_changed( - mpp: MapPlantingPage, mmp: MapManagementPage, map_name, val, worker_id + mpp: MapPlantingPage, mmp: MapManagementPage, map_name, val, worker_uuid ): mpp.to_map_management_page() mmp.verify() - mmp.to_map_planting_page(map_name + worker_id) + mmp.to_map_planting_page(map_name + worker_uuid) + mpp.check_base_layer() mpp.expect_rotation_to_have_value(val) # Scenario 3: Successfully scale the background image -# This test might be flaky, since it depends on the image to be loaded before -# the scaling is applied and to process the input before navigating away from the page. @when(parsers.parse("I change the scale of the image to {val} percent")) def change_scale(mpp: MapPlantingPage, val): mpp.select_birdie_background() mpp.expect_background_image("/Photos/Birdie.jpg") + mpp.check_base_layer() mpp.fill_scaling(val) @then(parsers.parse("{map_name} image scale is set to {val} percent")) def scale_gets_changed( - mpp: MapPlantingPage, mmp: MapManagementPage, map_name, val, worker_id + mpp: MapPlantingPage, mmp: MapManagementPage, map_name, val, worker_uuid ): mpp.to_map_management_page() mmp.verify() - mmp.to_map_planting_page(map_name + worker_id) + mmp.to_map_planting_page(map_name + worker_uuid) + mpp.check_base_layer() mpp.expect_scaling_to_have_value(val) diff --git a/e2e/steps/test_frequent_steps.py b/e2e/steps/test_frequent_steps.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/e2e/steps/test_inventory.py b/e2e/steps/test_inventory.py new file mode 100644 index 000000000..85b5b647b --- /dev/null +++ b/e2e/steps/test_inventory.py @@ -0,0 +1,125 @@ +from playwright.sync_api import Page +from pytest_bdd import scenario, when, then + +from e2e.pages.inventory.management import SeedManagementPage + + +@scenario("features/seeds.feature", "Successful seed creation") +def test_seeds(): + pass + + +@when("I create a new seed") +def provide_seed_details(page: Page): + smp = SeedManagementPage(page) + scp = smp.to_seed_create_page() + scp.create_a_seed(additional_name="SUT create") + + +@then("I can successfully create a new seed without an error message") +def create_seed_success(page: Page): + smp = SeedManagementPage(page) + smp.expect_alert_is_visible() + smp.expect_first_row_cell_exists("Indian abelia - SUT create (Abelia triflora)") + smp.expect_first_row_cell_exists("Enough") + smp.expect_first_row_cell_exists("Origin SUT") + + +@scenario("features/seeds.feature", "Successful seed editing") +def test_seed_editing(): + pass + + +@when("I create another new seed") +def create_seed_for_editing(page: Page): + smp = SeedManagementPage(page) + scp = smp.to_seed_create_page() + scp.create_a_seed(additional_name="SUT editing") + + +@when("I edit this seed") +def editing_seed(page: Page): + smp = SeedManagementPage(page) + sep = smp.to_seed_edit_page( + "Indian abelia - SUT editing (Abelia triflora) Enough Organic 2022 Origin SUT Edit seed Archive seed" + ) + sep.set_additional_name("SUT edited") + sep.set_amount("Not enough") + sep.set_quality("Not organic") + sep.set_origin("New origin SUT") + sep.click_edit() + + +@then("the edited seed is saved and shown on the overview page") +def edited_seed_success(page: Page): + smp = SeedManagementPage(page) + smp.expect_first_row_cell_exists("Indian abelia - SUT edited (Abelia triflora)") + smp.expect_first_row_cell_exists("Not enough") + smp.expect_first_row_cell_exists("Not organic") + smp.expect_first_row_cell_exists("New origin SUT") + + +@scenario("features/seeds.feature", "Successful seed search") +def test_seed_search(): + pass + + +@when("I search for a seed") +def search_seed(page: Page): + smp = SeedManagementPage(page) + scp = smp.to_seed_create_page() + scp.create_a_seed(additional_name="SUT search") + smp.search("SUT search") + + +@then("I can see the seed in the overview page") +def searched_seed_exists(page: Page): + smp = SeedManagementPage(page) + smp.expect_first_row_cell_exists("Indian abelia - SUT search (Abelia triflora)") + smp.expect_first_row_cell_exists("Enough") + smp.expect_first_row_cell_exists("Organic") + smp.expect_first_row_cell_exists("Origin SUT") + + +@scenario("features/seeds.feature", "Searching seed that does not exists") +def test_seed_search_not_existing_seed(): + pass + + +@when("I search for a seed that does not exist") +def search_seed_that_does_not_exist(page: Page): + smp = SeedManagementPage(page) + smp.search("AAAAAAAAA") + + +@then("the search result is empty") +def searched_seed_does_not_exist(page: Page): + smp = SeedManagementPage(page) + smp.expect_first_row_cell_exists("Sorry, I could not find this seed.") + + +@scenario("features/seeds.feature", "Archving a seed") +def test_seed_archive(): + pass + + +@when("I try to archive a seed") +def archive_seed(page: Page): + smp = SeedManagementPage(page) + scp = smp.to_seed_create_page() + scp.create_a_seed(additional_name="SUT archive") + smp.archive_seed( + "Indian abelia - SUT archive (Abelia triflora) Enough Organic 2022 Origin SUT Edit seed Archive seed" + ) + + +@then("the seed disapears") +def archived_seed_disapears(page: Page): + smp = SeedManagementPage(page) + smp.expect_first_row_cell_does_not_exist("SUT archive") + + +@then("I have the possiblity to restore it") +def positive_notification(page: Page): + smp = SeedManagementPage(page) + smp.expect_restore_button_to_be_visible() diff --git a/e2e/steps/test_layer_visibility.py b/e2e/steps/test_layer_visibility.py index 3b65fe51a..a8ba6c5ac 100644 --- a/e2e/steps/test_layer_visibility.py +++ b/e2e/steps/test_layer_visibility.py @@ -1,4 +1,4 @@ -from pytest_bdd import scenarios, given, when, then +from pytest_bdd import scenarios, when, then from e2e.pages.maps.planting import MapPlantingPage @@ -22,16 +22,6 @@ def plants_are_invisible(mpp: MapPlantingPage): # Scenario 2: Successfully change base layer visibility -@given("I have an empty canvas before") -def canvas_is_empty(mpp: MapPlantingPage): - mpp.screenshot_canvas() - - @when("I turn the base layer visiblity off") def turn_base_layer_invisibility_off(mpp: MapPlantingPage): mpp.click_hide_base_layer() - - -@then(("the base layer is invisible")) -def base_layer_is_invisible(mpp: MapPlantingPage, request): - mpp.expect_canvas_equals_last_screenshot(request.node.name) diff --git a/e2e/steps/test_map_creation.py b/e2e/steps/test_map_creation.py index 39e5aba9b..176b84bf0 100644 --- a/e2e/steps/test_map_creation.py +++ b/e2e/steps/test_map_creation.py @@ -35,7 +35,11 @@ def provide_map_details( ): mmp.to_map_create_page() mcp.create_a_map( - name, description=description, latitude=latitude, longitude=longitude + name, + privacy=privacy, + description=description, + latitude=latitude, + longitude=longitude, ) diff --git a/e2e/steps/test_planting.py b/e2e/steps/test_planting.py index 281819350..814f93550 100644 --- a/e2e/steps/test_planting.py +++ b/e2e/steps/test_planting.py @@ -12,8 +12,8 @@ @then( parsers.parse("it stays on {name} even when I leave the page and come back later") ) -def planting_persists(mmp: MapManagementPage, mpp: MapPlantingPage, name, worker_id): +def planting_persists(mmp: MapManagementPage, mpp: MapPlantingPage, name, worker_uuid): mpp.to_map_management_page() - mmp.to_map_planting_page(name + worker_id) + mmp.to_map_planting_page(name + worker_uuid) mpp.check_plant_layer() mpp.expect_plant_on_canvas("Tomato (Solanum lycopersicum)") diff --git a/e2e/steps/test_planting_select.py b/e2e/steps/test_planting_select.py new file mode 100644 index 000000000..c666fcd47 --- /dev/null +++ b/e2e/steps/test_planting_select.py @@ -0,0 +1,24 @@ +from pytest_bdd import scenarios, when, then + +from e2e.pages.maps.planting import MapPlantingPage + + +scenarios("features/planting_select.feature") + + +# Scenario: Successfully selecting plants + +# There is currently no real way to confirm +# how many plants you have selected with the +# select box, so this test only confirms that +# the plant name on the bottom left panel is visible + + +@when("I drag a select box over the canvas") +def drag_select_box(mpp: MapPlantingPage): + mpp.drag_select_box_over_canvas() + + +@then("the plant is selected") +def plant_is_selected(mpp: MapPlantingPage): + mpp.expect_plant_on_canvas("Tomato (Solanum lycopersicum)") diff --git a/e2e/steps/test_timeline.py b/e2e/steps/test_timeline.py index 5418f9089..9ac52deb9 100644 --- a/e2e/steps/test_timeline.py +++ b/e2e/steps/test_timeline.py @@ -10,8 +10,6 @@ # WHEN - - @when("I change the map date to yesterday") def change_map_date_to_yesterday(mpp: MapPlantingPage): mpp.change_map_date_by_days(-1) @@ -22,6 +20,26 @@ def change_map_date_to_tomorrow(mpp: MapPlantingPage): mpp.change_map_date_by_days(1) +@when("I change the map date to last year") +def change_map_date_to_last_year(mpp: MapPlantingPage): + mpp.change_map_date_by_years(-1) + + +@when("I change the map date to next year") +def change_map_date_to_next_year(mpp: MapPlantingPage): + mpp.change_map_date_by_years(1) + + +@when("I change the map date to last month") +def change_map_date_to_last_month(mpp: MapPlantingPage): + mpp.change_map_date_by_months(-1) + + +@when("I change the map date to next month") +def change_map_date_to_next_month(mpp: MapPlantingPage): + mpp.change_map_date_by_months(1) + + @when("I change the plants added date to tomorrow") def change_plants_added_date_to_tomorrow(mpp: MapPlantingPage): mpp.click_on_canvas_middle() diff --git a/e2e/steps/test_undo_redo.py b/e2e/steps/test_undo_redo.py index 5ffd6f6cf..9a74dad6d 100644 --- a/e2e/steps/test_undo_redo.py +++ b/e2e/steps/test_undo_redo.py @@ -5,7 +5,7 @@ scenarios("features/undo_redo.feature") -# Scenario 1: Successful undo +# Scenario: Successful undo @when("I click undo") @@ -18,7 +18,7 @@ def plant_is_gone(mpp: MapPlantingPage): mpp.expect_no_plant_on_canvas() -# Scenario 2: Successful redo +# Scenario: Successful redo @when("I accidentally clicked undo after planting one plant") @@ -26,12 +26,22 @@ def accidental_undo(mpp: MapPlantingPage): mpp.click_undo() -@when("I click redo to get my plant back") +@when("I click redo") def click_redo(mpp: MapPlantingPage): mpp.click_redo() - mpp.click_on_canvas_middle() @then("I can see my plant on the canvas again") def plant_is_back(mpp: MapPlantingPage): + # Click on top left to reset selection + mpp.click_on_canvas_top_left() mpp.expect_plant_on_canvas("Tomato (Solanum lycopersicum)") + + +# Scenario: Successful undo delete + + +@when("I delete the plant") +def delete_plant(mpp: MapPlantingPage): + mpp.click_on_canvas_middle() + mpp.click_delete() diff --git a/e2e/test-reports/.gitignore b/e2e/test-reports/.gitignore deleted file mode 100644 index 177fa595e..000000000 --- a/e2e/test-reports/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -cucumber.json -report.html diff --git a/e2e/uninstall.sh b/e2e/uninstall.sh index 9f9aee1de..553667f10 100755 --- a/e2e/uninstall.sh +++ b/e2e/uninstall.sh @@ -1,3 +1,4 @@ #!/bin/bash +rm -rf e2e/venv python3 -m playwright uninstall python3 -m pip uninstall -y -r requirements.txt diff --git a/frontend/.gitignore b/frontend/.gitignore index 248c378a6..8800f1e80 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -3,7 +3,7 @@ dist-ssr *.local # Auto generated -src/bindings/* +src/api_types/* src/generated/* # Editor directories and files diff --git a/frontend/.prettierignore b/frontend/.prettierignore index 962517f53..20f480443 100644 --- a/frontend/.prettierignore +++ b/frontend/.prettierignore @@ -2,6 +2,6 @@ **/node_modules **/dist **/*.css -src/bindings/definitions.ts +src/api_types/definitions.ts src/generated/docs storybook-static/ diff --git a/frontend/index.html b/frontend/index.html index b6683ca9c..b62e45d4d 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -5,10 +5,10 @@ - + - + diff --git a/frontend/jest.config.ts b/frontend/jest.config.ts index f7ee8b8ef..cf05ddfe6 100644 --- a/frontend/jest.config.ts +++ b/frontend/jest.config.ts @@ -10,6 +10,7 @@ const config: Config = { '^.+\\.svg$': '/src/__mocks__/svg.ts', '^@/(.*)$': '/src/$1', '^import.meta.env$': '/src/env.ts', + '\\.(css)$': '/src/__mocks__/styleMock.ts', }, watchPathIgnorePatterns: ['node_modules'], setupFilesAfterEnv: ['/jest.setup.ts'], diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 3ac008051..863629302 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,17 +1,18 @@ { "name": "permaplant", - "version": "0.3.3", + "version": "0.3.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "permaplant", - "version": "0.3.3", + "version": "0.3.7", "license": "MIT", "dependencies": { "@hookform/resolvers": "^3.1.1", "@tanstack/react-query": "^4.29.1", "@tanstack/react-query-devtools": "^4.29.19", + "@uiw/react-md-editor": "^3.24.1", "axios": "^1.2.3", "framer-motion": "^8.5.2", "i18next": "^22.4.14", @@ -105,9 +106,10 @@ } }, "node_modules/@adobe/css-tools": { - "version": "4.2.0", - "dev": true, - "license": "MIT" + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.3.1.tgz", + "integrity": "sha512-/62yikz7NLScCGAAST5SHdnjaDJQBDq0M2muyRTpf2VQhw6StBg2ALiu73zSJQ4fMVLA+0uBhBHAle7Wg+2kSg==", + "dev": true }, "node_modules/@ampproject/remapping": { "version": "2.2.1", @@ -314,8 +316,9 @@ } }, "node_modules/@babel/helper-environment-visitor": { - "version": "7.18.9", - "license": "MIT", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "engines": { "node": ">=6.9.0" } @@ -474,15 +477,17 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.19.4", - "license": "MIT", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.19.1", - "license": "MIT", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "engines": { "node": ">=6.9.0" } @@ -1918,9 +1923,10 @@ } }, "node_modules/@babel/register/node_modules/semver": { - "version": "5.7.1", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, - "license": "ISC", "bin": { "semver": "bin/semver" } @@ -1953,17 +1959,18 @@ } }, "node_modules/@babel/traverse": { - "version": "7.21.4", - "license": "MIT", + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.21.5.tgz", + "integrity": "sha512-AhQoI3YjWi6u/y/ntv7k48mcrCXmus0t79J9qPNlk/lAsFlCiJ047RmbfMOawySTHtywXhbXgpx/8nXMYd+oFw==", "dependencies": { "@babel/code-frame": "^7.21.4", - "@babel/generator": "^7.21.4", - "@babel/helper-environment-visitor": "^7.18.9", + "@babel/generator": "^7.21.5", + "@babel/helper-environment-visitor": "^7.21.5", "@babel/helper-function-name": "^7.21.0", "@babel/helper-hoist-variables": "^7.18.6", "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/parser": "^7.21.4", - "@babel/types": "^7.21.4", + "@babel/parser": "^7.21.5", + "@babel/types": "^7.21.5", "debug": "^4.1.0", "globals": "^11.1.0" }, @@ -1971,6 +1978,44 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/traverse/node_modules/@babel/generator": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", + "dependencies": { + "@babel/types": "^7.23.0", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/@babel/parser": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", + "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/traverse/node_modules/@babel/types": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", + "dependencies": { + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/types": { "version": "7.21.4", "license": "MIT", @@ -4461,9 +4506,10 @@ } }, "node_modules/@storybook/cli/node_modules/semver": { - "version": "7.5.0", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, - "license": "ISC", "dependencies": { "lru-cache": "^6.0.0" }, @@ -4761,9 +4807,10 @@ } }, "node_modules/@storybook/core-server/node_modules/semver": { - "version": "7.5.0", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, - "license": "ISC", "dependencies": { "lru-cache": "^6.0.0" }, @@ -4926,9 +4973,10 @@ } }, "node_modules/@storybook/manager-api/node_modules/semver": { - "version": "7.5.0", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, - "license": "ISC", "dependencies": { "lru-cache": "^6.0.0" }, @@ -5991,7 +6039,6 @@ }, "node_modules/@types/debug": { "version": "4.1.7", - "dev": true, "license": "MIT", "dependencies": { "@types/ms": "*" @@ -6070,6 +6117,14 @@ "@types/node": "*" } }, + "node_modules/@types/hast": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.7.tgz", + "integrity": "sha512-EVLigw5zInURhzfXUM65eixfadfsHKomGKUakToXo84t8gGIJuTcD2xooM2See7GyQ7DRtYjhCHnSUQez8JaLw==", + "dependencies": { + "@types/unist": "^2" + } + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.4", "dev": true, @@ -6159,7 +6214,6 @@ }, "node_modules/@types/mdast": { "version": "3.0.11", - "dev": true, "license": "MIT", "dependencies": { "@types/unist": "*" @@ -6187,7 +6241,6 @@ }, "node_modules/@types/ms": { "version": "0.7.31", - "dev": true, "license": "MIT" }, "node_modules/@types/node": { @@ -6231,6 +6284,11 @@ "version": "4.0.0", "license": "MIT" }, + "node_modules/@types/parse5": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/parse5/-/parse5-6.0.3.tgz", + "integrity": "sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==" + }, "node_modules/@types/prettier": { "version": "2.7.2", "dev": true, @@ -6241,6 +6299,11 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/prismjs": { + "version": "1.26.2", + "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.2.tgz", + "integrity": "sha512-/r7Cp7iUIk7gts26mHXD66geUC+2Fo26TZYjQK6Nr4LDfi6lmdRmMqM0oPwfiMhUwoBAOFe8GstKi2pf6hZvwA==" + }, "node_modules/@types/prop-types": { "version": "15.7.5", "license": "MIT" @@ -6256,8 +6319,9 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "18.0.37", - "license": "MIT", + "version": "18.2.33", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.33.tgz", + "integrity": "sha512-v+I7S+hu3PIBoVkKGpSYYpiBT1ijqEzWpzQD62/jm4K74hPpSP7FF9BnKG6+fg2+62weJYkkBWDJlZt5JO/9hg==", "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -6265,9 +6329,10 @@ } }, "node_modules/@types/react-dom": { - "version": "18.0.11", + "version": "18.2.14", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.14.tgz", + "integrity": "sha512-V835xgdSVmyQmI1KLV2BEIUgqEuinxp9O4G6g3FqO/SqLac049E53aysv0oEFD2kHfejeKU+ZqL2bcFWj9gLAQ==", "dev": true, - "license": "MIT", "dependencies": { "@types/react": "*" } @@ -6345,7 +6410,6 @@ }, "node_modules/@types/unist": { "version": "2.0.6", - "dev": true, "license": "MIT" }, "node_modules/@types/uuid": { @@ -6411,9 +6475,10 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { - "version": "7.5.0", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, - "license": "ISC", "dependencies": { "lru-cache": "^6.0.0" }, @@ -6547,9 +6612,10 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.5.0", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, - "license": "ISC", "dependencies": { "lru-cache": "^6.0.0" }, @@ -6602,9 +6668,10 @@ } }, "node_modules/@typescript-eslint/utils/node_modules/semver": { - "version": "7.5.0", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, - "license": "ISC", "dependencies": { "lru-cache": "^6.0.0" }, @@ -6636,6 +6703,63 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@uiw/copy-to-clipboard": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/@uiw/copy-to-clipboard/-/copy-to-clipboard-1.0.16.tgz", + "integrity": "sha512-IXR+N363nLTR3ilklmM+B0nk774jVE/muOrBYt4Rdww/Pf3uP9XHyv2x6YZrbDh29F7w9BkzQyB8QF6WDShmJA==" + }, + "node_modules/@uiw/react-markdown-preview": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@uiw/react-markdown-preview/-/react-markdown-preview-4.1.16.tgz", + "integrity": "sha512-O8WS/xECPLsZqneg4IoaGA/L8ycWtMvNhK9iJtTsqcvc6l5Oq+maAyae8toMz3HIcpIP46Ml6vU/VsTnYkk63w==", + "dependencies": { + "@babel/runtime": "^7.17.2", + "@uiw/copy-to-clipboard": "~1.0.12", + "react-markdown": "~8.0.0", + "rehype-attr": "~2.1.0", + "rehype-autolink-headings": "~6.1.1", + "rehype-ignore": "^1.0.1", + "rehype-prism-plus": "1.6.3", + "rehype-raw": "^6.1.1", + "rehype-rewrite": "~3.0.6", + "rehype-slug": "~5.1.0", + "remark-gfm": "~3.0.1", + "unist-util-visit": "^4.1.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@uiw/react-markdown-preview/node_modules/unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@uiw/react-md-editor": { + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/@uiw/react-md-editor/-/react-md-editor-3.24.1.tgz", + "integrity": "sha512-nLm1lOwMGRlCbRj0ythFO7RfmUm+Zd7pQOYsBKRlbB9oxCnpjsW+ioQQ2XiAfkUM7aBf/pFSE/GQ63k08nWucA==", + "dependencies": { + "@babel/runtime": "^7.14.6", + "@uiw/react-markdown-preview": "^4.1.14", + "rehype": "~12.0.1", + "rehype-prism-plus": "~1.6.1" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/@vitejs/plugin-react": { "version": "2.2.0", "dev": true, @@ -7112,8 +7236,9 @@ } }, "node_modules/axios": { - "version": "1.3.5", - "license": "MIT", + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz", + "integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==", "dependencies": { "follow-redirects": "^1.15.0", "form-data": "^4.0.0", @@ -7346,7 +7471,6 @@ }, "node_modules/bail": { "version": "2.0.2", - "dev": true, "license": "MIT", "funding": { "type": "github", @@ -7381,6 +7505,15 @@ ], "license": "MIT" }, + "node_modules/bcp-47-match": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/bcp-47-match/-/bcp-47-match-2.0.3.tgz", + "integrity": "sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/better-opn": { "version": "2.1.1", "dev": true, @@ -7491,6 +7624,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" + }, "node_modules/boxen": { "version": "5.1.2", "dev": true, @@ -7791,7 +7929,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001480", + "version": "1.0.30001561", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001561.tgz", + "integrity": "sha512-NTt0DNoKe958Q0BE0j0c1V9jbUzhBxHIEJy7asmGrpE0yG63KTV7PLHPnK2E1O9RsQrQ081I3NLuXGS6zht3cw==", "funding": [ { "type": "opencollective", @@ -7805,8 +7945,7 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ], - "license": "CC-BY-4.0" + ] }, "node_modules/canvas": { "version": "2.11.2", @@ -7825,7 +7964,6 @@ }, "node_modules/ccount": { "version": "2.0.1", - "dev": true, "license": "MIT", "funding": { "type": "github", @@ -7854,13 +7992,39 @@ }, "node_modules/character-entities": { "version": "2.0.2", - "dev": true, "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/charenc": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", @@ -8060,6 +8224,15 @@ "node": ">= 0.8" } }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/commander": { "version": "2.20.3", "dev": true, @@ -8308,8 +8481,9 @@ } }, "node_modules/crypto-js": { - "version": "4.1.1", - "license": "MIT" + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" }, "node_modules/crypto-random-string": { "version": "2.0.0", @@ -8319,6 +8493,11 @@ "node": ">=8" } }, + "node_modules/css-selector-parser": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/css-selector-parser/-/css-selector-parser-1.4.1.tgz", + "integrity": "sha512-HYPSb7y/Z7BNDCOrakL4raGO2zltZkbeXyAd6Tg9obzix6QhzxCotdBl6VT0Dv4vZfJGVz3WL/xaEI9Ly3ul0g==" + }, "node_modules/css.escape": { "version": "1.5.1", "dev": true, @@ -8435,7 +8614,6 @@ }, "node_modules/decode-named-character-reference": { "version": "1.0.2", - "dev": true, "license": "MIT", "dependencies": { "character-entities": "^2.0.0" @@ -8587,7 +8765,6 @@ }, "node_modules/dequal": { "version": "2.0.3", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -8663,7 +8840,6 @@ }, "node_modules/diff": { "version": "5.1.0", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" @@ -8688,6 +8864,18 @@ "node": ">=8" } }, + "node_modules/direction": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/direction/-/direction-2.0.1.tgz", + "integrity": "sha512-9S6m9Sukh1cZNknO1CWAr2QAWsbKLafQiyM5gZ7VgXHeuaoUwffKN4q6NC4A/Mf9iiPlOXQEKW/Mv/mh9/3YFA==", + "bin": { + "direction": "cli.js" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/dlv": { "version": "1.1.3", "dev": true, @@ -9950,7 +10138,6 @@ }, "node_modules/extend": { "version": "3.0.2", - "dev": true, "license": "MIT" }, "node_modules/extract-zip": { @@ -10875,105 +11062,362 @@ "dev": true, "license": "ISC" }, - "node_modules/he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "bin": { - "he": "bin/he" - } - }, - "node_modules/hey-listen": { - "version": "1.0.8", - "license": "MIT" - }, - "node_modules/hoist-non-react-statics": { - "version": "3.3.2", - "license": "BSD-3-Clause", + "node_modules/hast-util-from-parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-7.1.2.tgz", + "integrity": "sha512-Nz7FfPBuljzsN3tCQ4kCBKqdNhQE2l0Tn+X1ubgKBPRoiDIu1mL08Cfw4k7q71+Duyaw7DXDN+VTAp4Vh3oCOw==", "dependencies": { - "react-is": "^16.7.0" + "@types/hast": "^2.0.0", + "@types/unist": "^2.0.0", + "hastscript": "^7.0.0", + "property-information": "^6.0.0", + "vfile": "^5.0.0", + "vfile-location": "^4.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/hoist-non-react-statics/node_modules/react-is": { - "version": "16.13.1", - "license": "MIT" - }, - "node_modules/hosted-git-info": { - "version": "2.8.9", - "dev": true, - "license": "ISC" - }, - "node_modules/hot-patcher": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hot-patcher/-/hot-patcher-2.0.0.tgz", - "integrity": "sha512-rwJ0ZqSFgm+5oD0KiGBVinyPWRJESRSsHlEWDzZjyOe/OfhD9tynHqUyUIGX2fWuV+BihW4nXxeoZRJVHid64w==" + "node_modules/hast-util-has-property": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/hast-util-has-property/-/hast-util-has-property-2.0.1.tgz", + "integrity": "sha512-X2+RwZIMTMKpXUzlotatPzWj8bspCymtXH3cfG3iQKV+wPF53Vgaqxi/eLqGck0wKq1kS9nvoB1wchbCPEL8sg==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } }, - "node_modules/hpack.js": { - "version": "2.1.6", - "dev": true, - "license": "MIT", + "node_modules/hast-util-heading-rank": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/hast-util-heading-rank/-/hast-util-heading-rank-2.1.1.tgz", + "integrity": "sha512-iAuRp+ESgJoRFJbSyaqsfvJDY6zzmFoEnL1gtz1+U8gKtGGj1p0CVlysuUAUjq95qlZESHINLThwJzNGmgGZxA==", "dependencies": { - "inherits": "^2.0.1", - "obuf": "^1.0.0", - "readable-stream": "^2.0.1", - "wbuf": "^1.1.0" + "@types/hast": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/hpack.js/node_modules/isarray": { - "version": "1.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/hpack.js/node_modules/readable-stream": { - "version": "2.3.8", - "dev": true, - "license": "MIT", + "node_modules/hast-util-is-element": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-2.1.3.tgz", + "integrity": "sha512-O1bKah6mhgEq2WtVMk+Ta5K7pPMqsBBlmzysLdcwKVrqzZQ0CHqUPiIVspNhAG1rvxpvJjtGee17XfauZYKqVA==", "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" + "@types/hast": "^2.0.0", + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/hpack.js/node_modules/safe-buffer": { - "version": "5.1.2", - "dev": true, - "license": "MIT" - }, - "node_modules/hpack.js/node_modules/string_decoder": { - "version": "1.1.1", - "dev": true, - "license": "MIT", + "node_modules/hast-util-parse-selector": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-3.1.1.tgz", + "integrity": "sha512-jdlwBjEexy1oGz0aJ2f4GKMaVKkA9jwjr4MjAAI22E5fM/TXVZHuS5OpONtdeIkRKqAaryQ2E9xNQxijoThSZA==", "dependencies": { - "safe-buffer": "~5.1.0" + "@types/hast": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/html-encoding-sniffer": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "whatwg-encoding": "^2.0.0" + "node_modules/hast-util-raw": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-7.2.3.tgz", + "integrity": "sha512-RujVQfVsOrxzPOPSzZFiwofMArbQke6DJjnFfceiEbFh7S05CbPt0cYN+A5YeD3pso0JQk6O1aHBnx9+Pm2uqg==", + "dependencies": { + "@types/hast": "^2.0.0", + "@types/parse5": "^6.0.0", + "hast-util-from-parse5": "^7.0.0", + "hast-util-to-parse5": "^7.0.0", + "html-void-elements": "^2.0.0", + "parse5": "^6.0.0", + "unist-util-position": "^4.0.0", + "unist-util-visit": "^4.0.0", + "vfile": "^5.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" }, - "engines": { - "node": ">=12" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/html-escaper": { - "version": "2.0.2", - "dev": true, - "license": "MIT" + "node_modules/hast-util-raw/node_modules/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==" }, - "node_modules/html-parse-stringify": { - "version": "3.0.1", - "license": "MIT", + "node_modules/hast-util-raw/node_modules/unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", "dependencies": { - "void-elements": "3.1.0" - } - }, + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-select": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/hast-util-select/-/hast-util-select-5.0.5.tgz", + "integrity": "sha512-QQhWMhgTFRhCaQdgTKzZ5g31GLQ9qRb1hZtDPMqQaOhpLBziWcshUS0uCR5IJ0U1jrK/mxg35fmcq+Dp/Cy2Aw==", + "dependencies": { + "@types/hast": "^2.0.0", + "@types/unist": "^2.0.0", + "bcp-47-match": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "css-selector-parser": "^1.0.0", + "direction": "^2.0.0", + "hast-util-has-property": "^2.0.0", + "hast-util-to-string": "^2.0.0", + "hast-util-whitespace": "^2.0.0", + "not": "^0.1.0", + "nth-check": "^2.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "unist-util-visit": "^4.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-select/node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/hast-util-select/node_modules/unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-html": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-8.0.4.tgz", + "integrity": "sha512-4tpQTUOr9BMjtYyNlt0P50mH7xj0Ks2xpo8M943Vykljf99HW6EzulIoJP1N3eKOSScEHzyzi9dm7/cn0RfGwA==", + "dependencies": { + "@types/hast": "^2.0.0", + "@types/unist": "^2.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-raw": "^7.0.0", + "hast-util-whitespace": "^2.0.0", + "html-void-elements": "^2.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-html/node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/hast-util-to-parse5": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-7.1.0.tgz", + "integrity": "sha512-YNRgAJkH2Jky5ySkIqFXTQiaqcAtJyVE+D5lkN6CdtOqrnkLfGYYrEcKuHOJZlp+MwjSwuD3fZuawI+sic/RBw==", + "dependencies": { + "@types/hast": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-parse5/node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/hast-util-to-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hast-util-to-string/-/hast-util-to-string-2.0.0.tgz", + "integrity": "sha512-02AQ3vLhuH3FisaMM+i/9sm4OXGSq1UhOOCpTLLQtHdL3tZt7qil69r8M8iDkZYyC0HCFylcYoP+8IO7ddta1A==", + "dependencies": { + "@types/hast": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-2.0.1.tgz", + "integrity": "sha512-nAxA0v8+vXSBDt3AnRUNjyRIQ0rD+ntpbAp4LnPkumc5M9yUbSMa4XDU9Q6etY4f1Wp4bNgvc1yjiZtsTTrSng==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-7.2.0.tgz", + "integrity": "sha512-TtYPq24IldU8iKoJQqvZOuhi5CyCQRAbvDOX0x1eW6rsHSxa/1i2CCiptNTotGHJ3VoHRGmqiv6/D3q113ikkw==", + "dependencies": { + "@types/hast": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^3.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript/node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "bin": { + "he": "bin/he" + } + }, + "node_modules/hey-listen": { + "version": "1.0.8", + "license": "MIT" + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "license": "MIT" + }, + "node_modules/hosted-git-info": { + "version": "2.8.9", + "dev": true, + "license": "ISC" + }, + "node_modules/hot-patcher": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hot-patcher/-/hot-patcher-2.0.0.tgz", + "integrity": "sha512-rwJ0ZqSFgm+5oD0KiGBVinyPWRJESRSsHlEWDzZjyOe/OfhD9tynHqUyUIGX2fWuV+BihW4nXxeoZRJVHid64w==" + }, + "node_modules/hpack.js": { + "version": "2.1.6", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + } + }, + "node_modules/hpack.js/node_modules/isarray": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/hpack.js/node_modules/readable-stream": { + "version": "2.3.8", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/hpack.js/node_modules/safe-buffer": { + "version": "5.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/hpack.js/node_modules/string_decoder": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "license": "MIT", + "dependencies": { + "void-elements": "3.1.0" + } + }, "node_modules/html-tags": { "version": "3.3.1", "dev": true, @@ -10985,6 +11429,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/html-void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-2.0.1.tgz", + "integrity": "sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/http-deceiver": { "version": "1.2.7", "dev": true, @@ -11253,6 +11706,11 @@ "dev": true, "license": "ISC" }, + "node_modules/inline-style-parser": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz", + "integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==" + }, "node_modules/internal-slot": { "version": "1.0.5", "dev": true, @@ -11295,6 +11753,28 @@ "node": ">=8" } }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-arguments": { "version": "1.1.1", "dev": true, @@ -11366,7 +11846,6 @@ }, "node_modules/is-buffer": { "version": "2.0.5", - "dev": true, "funding": [ { "type": "github", @@ -11421,6 +11900,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-deflate": { "version": "1.0.0", "dev": true, @@ -11497,6 +11985,15 @@ "node": ">=0.10.0" } }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-map": { "version": "2.0.2", "dev": true, @@ -11588,7 +12085,6 @@ }, "node_modules/is-plain-obj": { "version": "4.1.0", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -13387,9 +13883,10 @@ "license": "MIT" }, "node_modules/jest-snapshot/node_modules/semver": { - "version": "7.5.0", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, - "license": "ISC", "dependencies": { "lru-cache": "^6.0.0" }, @@ -14141,7 +14638,6 @@ }, "node_modules/longest-streak": { "version": "3.1.0", - "dev": true, "license": "MIT", "funding": { "type": "github", @@ -14223,7 +14719,6 @@ }, "node_modules/markdown-table": { "version": "3.0.3", - "dev": true, "license": "MIT", "funding": { "type": "github", @@ -14281,7 +14776,6 @@ }, "node_modules/mdast-util-find-and-replace": { "version": "2.2.2", - "dev": true, "license": "MIT", "dependencies": { "@types/mdast": "^3.0.0", @@ -14296,7 +14790,6 @@ }, "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { "version": "5.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -14307,7 +14800,6 @@ }, "node_modules/mdast-util-from-markdown": { "version": "1.3.0", - "dev": true, "license": "MIT", "dependencies": { "@types/mdast": "^3.0.0", @@ -14330,7 +14822,6 @@ }, "node_modules/mdast-util-gfm": { "version": "2.0.2", - "dev": true, "license": "MIT", "dependencies": { "mdast-util-from-markdown": "^1.0.0", @@ -14348,7 +14839,6 @@ }, "node_modules/mdast-util-gfm-autolink-literal": { "version": "1.0.3", - "dev": true, "license": "MIT", "dependencies": { "@types/mdast": "^3.0.0", @@ -14363,7 +14853,6 @@ }, "node_modules/mdast-util-gfm-footnote": { "version": "1.0.2", - "dev": true, "license": "MIT", "dependencies": { "@types/mdast": "^3.0.0", @@ -14377,7 +14866,6 @@ }, "node_modules/mdast-util-gfm-strikethrough": { "version": "1.0.3", - "dev": true, "license": "MIT", "dependencies": { "@types/mdast": "^3.0.0", @@ -14390,7 +14878,6 @@ }, "node_modules/mdast-util-gfm-table": { "version": "1.0.7", - "dev": true, "license": "MIT", "dependencies": { "@types/mdast": "^3.0.0", @@ -14405,7 +14892,6 @@ }, "node_modules/mdast-util-gfm-task-list-item": { "version": "1.0.2", - "dev": true, "license": "MIT", "dependencies": { "@types/mdast": "^3.0.0", @@ -14418,7 +14904,6 @@ }, "node_modules/mdast-util-phrasing": { "version": "3.0.1", - "dev": true, "license": "MIT", "dependencies": { "@types/mdast": "^3.0.0", @@ -14429,9 +14914,55 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mdast-util-to-hast": { + "version": "12.3.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-12.3.0.tgz", + "integrity": "sha512-pits93r8PhnIoU4Vy9bjW39M2jJ6/tdHyja9rrot9uujkN7UTU9SDnE6WNJz/IGyQk3XHX6yNNtrBH6cQzm8Hw==", + "dependencies": { + "@types/hast": "^2.0.0", + "@types/mdast": "^3.0.0", + "mdast-util-definitions": "^5.0.0", + "micromark-util-sanitize-uri": "^1.1.0", + "trim-lines": "^3.0.0", + "unist-util-generated": "^2.0.0", + "unist-util-position": "^4.0.0", + "unist-util-visit": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast/node_modules/mdast-util-definitions": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-5.1.2.tgz", + "integrity": "sha512-8SVPMuHqlPME/z3gqVwWY4zVXn8lqKv/pAhC57FuJ40ImXyBpmO5ukh98zB2v7Blql2FiHjHv9LVztSIqjY+MA==", + "dependencies": { + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "unist-util-visit": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast/node_modules/unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/mdast-util-to-markdown": { "version": "1.5.0", - "dev": true, "license": "MIT", "dependencies": { "@types/mdast": "^3.0.0", @@ -14450,7 +14981,6 @@ }, "node_modules/mdast-util-to-markdown/node_modules/unist-util-visit": { "version": "4.1.2", - "dev": true, "license": "MIT", "dependencies": { "@types/unist": "^2.0.0", @@ -14464,7 +14994,6 @@ }, "node_modules/mdast-util-to-string": { "version": "3.2.0", - "dev": true, "license": "MIT", "dependencies": { "@types/mdast": "^3.0.0" @@ -14522,7 +15051,6 @@ }, "node_modules/micromark": { "version": "3.1.0", - "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -14556,7 +15084,6 @@ }, "node_modules/micromark-core-commonmark": { "version": "1.0.6", - "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -14589,7 +15116,6 @@ }, "node_modules/micromark-extension-gfm": { "version": "2.0.1", - "dev": true, "license": "MIT", "dependencies": { "micromark-extension-gfm-autolink-literal": "^1.0.0", @@ -14608,7 +15134,6 @@ }, "node_modules/micromark-extension-gfm-autolink-literal": { "version": "1.0.3", - "dev": true, "license": "MIT", "dependencies": { "micromark-util-character": "^1.0.0", @@ -14624,7 +15149,6 @@ }, "node_modules/micromark-extension-gfm-footnote": { "version": "1.1.0", - "dev": true, "license": "MIT", "dependencies": { "micromark-core-commonmark": "^1.0.0", @@ -14643,7 +15167,6 @@ }, "node_modules/micromark-extension-gfm-strikethrough": { "version": "1.0.5", - "dev": true, "license": "MIT", "dependencies": { "micromark-util-chunked": "^1.0.0", @@ -14660,7 +15183,6 @@ }, "node_modules/micromark-extension-gfm-table": { "version": "1.0.5", - "dev": true, "license": "MIT", "dependencies": { "micromark-factory-space": "^1.0.0", @@ -14676,7 +15198,6 @@ }, "node_modules/micromark-extension-gfm-tagfilter": { "version": "1.0.2", - "dev": true, "license": "MIT", "dependencies": { "micromark-util-types": "^1.0.0" @@ -14688,7 +15209,6 @@ }, "node_modules/micromark-extension-gfm-task-list-item": { "version": "1.0.4", - "dev": true, "license": "MIT", "dependencies": { "micromark-factory-space": "^1.0.0", @@ -14704,7 +15224,6 @@ }, "node_modules/micromark-factory-destination": { "version": "1.0.0", - "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -14724,7 +15243,6 @@ }, "node_modules/micromark-factory-label": { "version": "1.0.2", - "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -14745,7 +15263,6 @@ }, "node_modules/micromark-factory-space": { "version": "1.0.0", - "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -14764,7 +15281,6 @@ }, "node_modules/micromark-factory-title": { "version": "1.0.2", - "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -14786,7 +15302,6 @@ }, "node_modules/micromark-factory-whitespace": { "version": "1.0.0", - "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -14807,7 +15322,6 @@ }, "node_modules/micromark-util-character": { "version": "1.1.0", - "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -14826,7 +15340,6 @@ }, "node_modules/micromark-util-chunked": { "version": "1.0.0", - "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -14844,7 +15357,6 @@ }, "node_modules/micromark-util-classify-character": { "version": "1.0.0", - "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -14864,7 +15376,6 @@ }, "node_modules/micromark-util-combine-extensions": { "version": "1.0.0", - "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -14883,7 +15394,6 @@ }, "node_modules/micromark-util-decode-numeric-character-reference": { "version": "1.0.0", - "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -14901,7 +15411,6 @@ }, "node_modules/micromark-util-decode-string": { "version": "1.0.2", - "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -14922,7 +15431,6 @@ }, "node_modules/micromark-util-encode": { "version": "1.0.1", - "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -14937,7 +15445,6 @@ }, "node_modules/micromark-util-html-tag-name": { "version": "1.1.0", - "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -14952,7 +15459,6 @@ }, "node_modules/micromark-util-normalize-identifier": { "version": "1.0.0", - "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -14970,7 +15476,6 @@ }, "node_modules/micromark-util-resolve-all": { "version": "1.0.0", - "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -14988,7 +15493,6 @@ }, "node_modules/micromark-util-sanitize-uri": { "version": "1.1.0", - "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -15008,7 +15512,6 @@ }, "node_modules/micromark-util-subtokenize": { "version": "1.0.2", - "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -15029,7 +15532,6 @@ }, "node_modules/micromark-util-symbol": { "version": "1.0.1", - "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -15044,7 +15546,6 @@ }, "node_modules/micromark-util-types": { "version": "1.0.2", - "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -15203,7 +15704,6 @@ }, "node_modules/mri": { "version": "1.2.0", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -15380,9 +15880,10 @@ } }, "node_modules/normalize-package-data/node_modules/semver": { - "version": "5.7.1", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, - "license": "ISC", "bin": { "semver": "bin/semver" } @@ -15403,6 +15904,11 @@ "node": ">=0.10.0" } }, + "node_modules/not": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/not/-/not-0.1.0.tgz", + "integrity": "sha512-5PDmaAsVfnWUgTUbJ3ERwn7u79Z0dYxN9ErxCpVJJqe2RK0PJ3z+iFUxuqjwtlDDegXvtWoxD/3Fzxox7tFGWA==" + }, "node_modules/npm-run-path": { "version": "4.0.1", "dev": true, @@ -15425,6 +15931,17 @@ "set-blocking": "^2.0.0" } }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, "node_modules/nwsapi": { "version": "2.2.4", "dev": true, @@ -15734,6 +16251,25 @@ "node": ">=6" } }, + "node_modules/parse-entities": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.1.tgz", + "integrity": "sha512-SWzvYcSJh4d/SGLIOQfZ/CoNv6BTlI6YEQ7Nj82oDVnRpwe/Z/F1EMx42x3JAOwGBlCjeCH0BRJQbQ/opHL17w==", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/parse-json": { "version": "5.2.0", "license": "MIT", @@ -15750,6 +16286,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse-numeric-range": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/parse-numeric-range/-/parse-numeric-range-1.3.0.tgz", + "integrity": "sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ==" + }, "node_modules/parse5": { "version": "7.1.2", "dev": true, @@ -16215,6 +16756,15 @@ "version": "16.13.1", "license": "MIT" }, + "node_modules/property-information": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.3.0.tgz", + "integrity": "sha512-gVNZ74nqhRMiIUYWGQdosYetaKc83x8oT41a0LlV3AAFCAZwCpg4vmGkq8t34+cUhp3cnM4XDiU/7xlgK7HGrg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "dev": true, @@ -16686,12 +17236,70 @@ "version": "4.2.1", "license": "Hippocratic-2.1", "dependencies": { - "@react-leaflet/core": "^2.1.0" + "@react-leaflet/core": "^2.1.0" + }, + "peerDependencies": { + "leaflet": "^1.9.0", + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, + "node_modules/react-markdown": { + "version": "8.0.7", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-8.0.7.tgz", + "integrity": "sha512-bvWbzG4MtOU62XqBx3Xx+zB2raaFFsq4mYiAzfjXJMEz2sixgeAfraA3tvzULF02ZdOMUOKTBFFaZJDDrq+BJQ==", + "dependencies": { + "@types/hast": "^2.0.0", + "@types/prop-types": "^15.0.0", + "@types/unist": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^2.0.0", + "prop-types": "^15.0.0", + "property-information": "^6.0.0", + "react-is": "^18.0.0", + "remark-parse": "^10.0.0", + "remark-rehype": "^10.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-object": "^0.4.0", + "unified": "^10.0.0", + "unist-util-visit": "^4.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=16", + "react": ">=16" + } + }, + "node_modules/react-markdown/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + }, + "node_modules/react-markdown/node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/react-markdown/node_modules/unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" }, - "peerDependencies": { - "leaflet": "^1.9.0", - "react": "^18.0.0", - "react-dom": "^18.0.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, "node_modules/react-oidc-context": { @@ -17050,6 +17658,21 @@ "node": ">=8" } }, + "node_modules/refractor": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/refractor/-/refractor-4.8.1.tgz", + "integrity": "sha512-/fk5sI0iTgFYlmVGYVew90AoYnNMP6pooClx/XKqyeeCQXrL0Kvgn8V0VEht5ccdljbzzF1i3Q213gcntkRExg==", + "dependencies": { + "@types/hast": "^2.0.0", + "@types/prismjs": "^1.0.0", + "hastscript": "^7.0.0", + "parse-entities": "^4.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/regenerate": { "version": "1.4.2", "dev": true, @@ -17140,6 +17763,245 @@ "jsesc": "bin/jsesc" } }, + "node_modules/rehype": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/rehype/-/rehype-12.0.1.tgz", + "integrity": "sha512-ey6kAqwLM3X6QnMDILJthGvG1m1ULROS9NT4uG9IDCuv08SFyLlreSuvOa//DgEvbXx62DS6elGVqusWhRUbgw==", + "dependencies": { + "@types/hast": "^2.0.0", + "rehype-parse": "^8.0.0", + "rehype-stringify": "^9.0.0", + "unified": "^10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-attr": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/rehype-attr/-/rehype-attr-2.1.4.tgz", + "integrity": "sha512-iAeaL5JyF4XxkcvWzpi/0SAF7iV7qOTaHS56tJuEsXziQc3+PEmMn65kV8OFgbO9mRVY7J1fRC/aLvot1PsNkg==", + "dependencies": { + "unified": "~10.1.1", + "unist-util-visit": "~4.1.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/rehype-attr/node_modules/unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-autolink-headings": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/rehype-autolink-headings/-/rehype-autolink-headings-6.1.1.tgz", + "integrity": "sha512-NMYzZIsHM3sA14nC5rAFuUPIOfg+DFmf9EY1YMhaNlB7+3kK/ZlE6kqPfuxr1tsJ1XWkTrMtMoyHosU70d35mA==", + "dependencies": { + "@types/hast": "^2.0.0", + "extend": "^3.0.0", + "hast-util-has-property": "^2.0.0", + "hast-util-heading-rank": "^2.0.0", + "hast-util-is-element": "^2.0.0", + "unified": "^10.0.0", + "unist-util-visit": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-autolink-headings/node_modules/unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-ignore": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/rehype-ignore/-/rehype-ignore-1.0.5.tgz", + "integrity": "sha512-JQXS5eDwXaYKwB8JEYFJJA/YvGi0sSNUOYuiURMtuPTg8tuWHFB91JMYLbImH1FyvyGQM4fIBqNMAPB50WR2Bw==", + "dependencies": { + "hast-util-select": "^5.0.5", + "unified": "^10.1.2", + "unist-util-visit": "^4.1.2" + }, + "engines": { + "node": "^14.13.1 || >=16.0.0" + } + }, + "node_modules/rehype-ignore/node_modules/unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-parse": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/rehype-parse/-/rehype-parse-8.0.5.tgz", + "integrity": "sha512-Ds3RglaY/+clEX2U2mHflt7NlMA72KspZ0JLUJgBBLpRddBcEw3H8uYZQliQriku22NZpYMfjDdSgHcjxue24A==", + "dependencies": { + "@types/hast": "^2.0.0", + "hast-util-from-parse5": "^7.0.0", + "parse5": "^6.0.0", + "unified": "^10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-parse/node_modules/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==" + }, + "node_modules/rehype-prism-plus": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/rehype-prism-plus/-/rehype-prism-plus-1.6.3.tgz", + "integrity": "sha512-F6tn376zimnvy+xW0bSnryul+rvVL7NhDIkavc9kAuzDx5zIZW04A6jdXPkcFBhojcqZB8b6pHt6CLqiUx+Tbw==", + "dependencies": { + "hast-util-to-string": "^2.0.0", + "parse-numeric-range": "^1.3.0", + "refractor": "^4.8.0", + "rehype-parse": "^8.0.2", + "unist-util-filter": "^4.0.0", + "unist-util-visit": "^4.0.0" + } + }, + "node_modules/rehype-prism-plus/node_modules/unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-raw": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-6.1.1.tgz", + "integrity": "sha512-d6AKtisSRtDRX4aSPsJGTfnzrX2ZkHQLE5kiUuGOeEoLpbEulFF4hj0mLPbsa+7vmguDKOVVEQdHKDSwoaIDsQ==", + "dependencies": { + "@types/hast": "^2.0.0", + "hast-util-raw": "^7.2.0", + "unified": "^10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-rewrite": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/rehype-rewrite/-/rehype-rewrite-3.0.6.tgz", + "integrity": "sha512-REDTNCvsKcAazy8IQWzKp66AhSUDSOIKssSCqNqCcT9sN7JCwAAm3mWGTUdUzq80ABuy8d0D6RBwbnewu1aY1g==", + "dependencies": { + "hast-util-select": "~5.0.1", + "unified": "~10.1.1", + "unist-util-visit": "~4.1.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/rehype-rewrite/node_modules/unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-slug": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/rehype-slug/-/rehype-slug-5.1.0.tgz", + "integrity": "sha512-Gf91dJoXneiorNEnn+Phx97CO7oRMrpi+6r155tTxzGuLtm+QrI4cTwCa9e1rtePdL4i9tSO58PeSS6HWfgsiw==", + "dependencies": { + "@types/hast": "^2.0.0", + "github-slugger": "^2.0.0", + "hast-util-has-property": "^2.0.0", + "hast-util-heading-rank": "^2.0.0", + "hast-util-to-string": "^2.0.0", + "unified": "^10.0.0", + "unist-util-visit": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-slug/node_modules/github-slugger": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", + "integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==" + }, + "node_modules/rehype-slug/node_modules/unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-stringify": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-9.0.4.tgz", + "integrity": "sha512-Uk5xu1YKdqobe5XpSskwPvo1XeHUUucWEQSl8hTrXt5selvca1e8K1EZ37E6YoZ4BT8BCqCdVfQW7OfHfthtVQ==", + "dependencies": { + "@types/hast": "^2.0.0", + "hast-util-to-html": "^8.0.0", + "unified": "^10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/remark-external-links": { "version": "8.0.0", "dev": true, @@ -17158,7 +18020,6 @@ }, "node_modules/remark-gfm": { "version": "3.0.1", - "dev": true, "license": "MIT", "dependencies": { "@types/mdast": "^3.0.0", @@ -17171,6 +18032,35 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/remark-parse": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-10.0.2.tgz", + "integrity": "sha512-3ydxgHa/ZQzG8LvC7jTXccARYDcRld3VfcgIIFs7bI6vbRSxJJmzgLEIIoYKyrfhaY+ujuWaf/PJiMZXoiCXgw==", + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-from-markdown": "^1.0.0", + "unified": "^10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-10.1.0.tgz", + "integrity": "sha512-EFmR5zppdBp0WQeDVZ/b66CWJipB2q2VLNFMabzDSGR66Z2fQii83G5gTBbgGEnEEA0QRussvrFHxk1HWGJskw==", + "dependencies": { + "@types/hast": "^2.0.0", + "@types/mdast": "^3.0.0", + "mdast-util-to-hast": "^12.1.0", + "unified": "^10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/remark-slug": { "version": "6.1.0", "dev": true, @@ -17402,7 +18292,6 @@ }, "node_modules/sade": { "version": "1.8.1", - "dev": true, "license": "MIT", "dependencies": { "mri": "^1.1.0" @@ -17489,8 +18378,9 @@ "license": "MIT" }, "node_modules/semver": { - "version": "6.3.0", - "license": "ISC", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "bin": { "semver": "bin/semver.js" } @@ -18054,6 +18944,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/stringify-entities": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.3.tgz", + "integrity": "sha512-BP9nNHMhhfcMbiuQKCqMjhDP5yBCAxsPu4pHFFzJ6Alo9dZgY4VLDPutXqIjpRiMoKdp7Av85Gr73Q5uH9k7+g==", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/stringify-object": { "version": "3.3.0", "dev": true, @@ -18129,6 +19032,14 @@ "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==" }, + "node_modules/style-to-object": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.4.4.tgz", + "integrity": "sha512-HYNoHZa2GorYNyqiCaBgsxvcJIn7OHq6inEga+E6Ke3m5JkoqpQbnFssk4jwe+K7AhGa2fcha4wSOf1Kn01dMg==", + "dependencies": { + "inline-style-parser": "0.1.1" + } + }, "node_modules/stylis": { "version": "4.1.3", "license": "MIT" @@ -18589,9 +19500,10 @@ } }, "node_modules/tough-cookie": { - "version": "4.1.2", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", + "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", "dev": true, - "license": "BSD-3-Clause", "dependencies": { "psl": "^1.1.33", "punycode": "^2.1.1", @@ -18615,9 +19527,17 @@ "dev": true, "license": "MIT" }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/trough": { "version": "2.1.0", - "dev": true, "license": "MIT", "funding": { "type": "github", @@ -18691,9 +19611,10 @@ } }, "node_modules/ts-jest/node_modules/semver": { - "version": "7.5.0", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, - "license": "ISC", "dependencies": { "lru-cache": "^6.0.0" }, @@ -19020,7 +19941,6 @@ }, "node_modules/unified": { "version": "10.1.2", - "dev": true, "license": "MIT", "dependencies": { "@types/unist": "^2.0.0", @@ -19047,9 +19967,27 @@ "node": ">=8" } }, + "node_modules/unist-util-filter": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/unist-util-filter/-/unist-util-filter-4.0.1.tgz", + "integrity": "sha512-RynicUM/vbOSTSiUK+BnaK9XMfmQUh6gyi7L6taNgc7FIf84GukXVV3ucGzEN/PhUUkdP5hb1MmXc+3cvPUm5Q==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.0.0" + } + }, + "node_modules/unist-util-generated": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unist-util-generated/-/unist-util-generated-2.0.1.tgz", + "integrity": "sha512-qF72kLmPxAw0oN2fwpWIqbXAVyEqUzDHMsbtPvOudIlUzXYFIeQIuxXQCRCFh22B7cixvU0MG7m3MW8FTq/S+A==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/unist-util-is": { "version": "5.2.1", - "dev": true, "license": "MIT", "dependencies": { "@types/unist": "^2.0.0" @@ -19059,9 +19997,20 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/unist-util-position": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.4.tgz", + "integrity": "sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/unist-util-stringify-position": { "version": "3.0.3", - "dev": true, "license": "MIT", "dependencies": { "@types/unist": "^2.0.0" @@ -19087,7 +20036,6 @@ }, "node_modules/unist-util-visit-parents": { "version": "5.1.3", - "dev": true, "license": "MIT", "dependencies": { "@types/unist": "^2.0.0", @@ -19310,7 +20258,6 @@ }, "node_modules/uvu": { "version": "0.5.6", - "dev": true, "license": "MIT", "dependencies": { "dequal": "^2.0.0", @@ -19327,7 +20274,6 @@ }, "node_modules/uvu/node_modules/kleur": { "version": "4.1.5", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -19370,7 +20316,6 @@ }, "node_modules/vfile": { "version": "5.3.7", - "dev": true, "license": "MIT", "dependencies": { "@types/unist": "^2.0.0", @@ -19383,9 +20328,21 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/vfile-location": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-4.1.0.tgz", + "integrity": "sha512-YF23YMyASIIJXpktBa4vIGLJ5Gs88UB/XePgqPmTa7cDA+JeO3yclbpheQYCHjVHBn/yePzrXuygIL+xbvRYHw==", + "dependencies": { + "@types/unist": "^2.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/vfile-message": { "version": "3.1.4", - "dev": true, "license": "MIT", "dependencies": { "@types/unist": "^2.0.0", @@ -19670,6 +20627,15 @@ "minimalistic-assert": "^1.0.0" } }, + "node_modules/web-namespaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/web-streams-polyfill": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", @@ -19860,9 +20826,10 @@ } }, "node_modules/word-wrap": { - "version": "1.2.3", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -20394,9 +21361,9 @@ } }, "node_modules/zod": { - "version": "3.21.4", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz", - "integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==", + "version": "3.22.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", + "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -20425,7 +21392,6 @@ }, "node_modules/zwitch": { "version": "2.0.4", - "dev": true, "license": "MIT", "funding": { "type": "github", diff --git a/frontend/package.json b/frontend/package.json index 4b9e8eaa2..cf3848a49 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,13 +1,13 @@ { "name": "permaplant", - "version": "0.3.3", + "version": "0.3.7", "license": "MIT", "scripts": { "dev": "vite", "build": "tsc && vite build", "preview": "vite preview", "https-preview": "serve dist", - "generate-api-types": "cd ../backend && typeshare ./ --lang=typescript --output-file=../frontend/src/bindings/definitions.ts", + "generate-api-types": "cd ../backend && typeshare ./ --lang=typescript --output-file=../frontend/src/api_types/definitions.ts", "format": "prettier --write .", "format:check": "prettier --check .", "lint": "eslint --max-warnings=0 .", @@ -20,6 +20,7 @@ "@hookform/resolvers": "^3.1.1", "@tanstack/react-query": "^4.29.1", "@tanstack/react-query-devtools": "^4.29.19", + "@uiw/react-md-editor": "^3.24.1", "axios": "^1.2.3", "framer-motion": "^8.5.2", "i18next": "^22.4.14", diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg deleted file mode 100644 index 34be69fc7..000000000 --- a/frontend/public/favicon.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/frontend/public/permaplant-logo-2.svg b/frontend/public/permaplant-logo-2.svg deleted file mode 100644 index c5764592d..000000000 --- a/frontend/public/permaplant-logo-2.svg +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - PermaplanT - - - diff --git a/frontend/public/permaplant-logo-gray.svg b/frontend/public/permaplant-logo-gray.svg deleted file mode 100644 index 85093ed72..000000000 --- a/frontend/public/permaplant-logo-gray.svg +++ /dev/null @@ -1,72 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/frontend/public/permaplant-logo.svg b/frontend/public/permaplant-logo.svg deleted file mode 100644 index b9ae0cef2..000000000 --- a/frontend/public/permaplant-logo.svg +++ /dev/null @@ -1,286 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/frontend/public/plant.svg b/frontend/public/plant.svg deleted file mode 100644 index 9900b3ef0..000000000 --- a/frontend/public/plant.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 9b719b6fc..d02a10c23 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -2,9 +2,10 @@ import NavContainer from './components/Layout/NavContainer'; import { useSafeAuth } from './hooks/useSafeAuth'; import Pages from './routes/Pages'; import './styles/guidedTour.css'; +import { errorToastGrouped, infoToastGrouped } from '@/features/toasts/groupedToast'; import { useEffect } from 'react'; import { useTranslation } from 'react-i18next'; -import { toast, ToastContainer } from 'react-toastify'; +import { ToastContainer } from 'react-toastify'; import 'react-toastify/dist/ReactToastify.css'; import 'shepherd.js/dist/css/shepherd.css'; @@ -26,14 +27,14 @@ const useAuthEffect = () => { useEffect(() => { if (auth.error) { console.error(auth.error.message); - toast.error(t('auth:error_failed_authentication'), { autoClose: false }); + errorToastGrouped(t('auth:error_failed_authentication'), { autoClose: false }); } switch (auth.activeNavigator) { case 'signinSilent': - toast.info(t('auth:signing_in')); + infoToastGrouped(t('auth:signing_in')); break; case 'signoutRedirect': - toast.info(t('auth:signing_out')); + infoToastGrouped(t('auth:signing_out')); } }, [auth, t]); @@ -42,7 +43,7 @@ const useAuthEffect = () => { useEffect(() => { if (isAuth) { - toast.info(`${t('auth:hello')} ${preferredUsername}`, { icon: false }); + infoToastGrouped(`${t('auth:hello')} ${preferredUsername}`, { icon: false }); } }, [isAuth, t, preferredUsername]); }; diff --git a/frontend/src/Providers.tsx b/frontend/src/Providers.tsx index 8b874d069..85ddf680e 100644 --- a/frontend/src/Providers.tsx +++ b/frontend/src/Providers.tsx @@ -1,10 +1,18 @@ import { getAuthInfo } from './features/auth'; +import { errorToastGrouped } from '@/features/toasts/groupedToast'; import { QueryCache, QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import { ReactNode } from 'react'; import { AuthProvider } from 'react-oidc-context'; import { BrowserRouter } from 'react-router-dom'; -import { toast } from 'react-toastify'; + +declare module '@tanstack/query-core' { + interface QueryMeta { + autoClose?: false | number; + errorMessage?: string; + toastId?: string; + } +} interface ProviderProps { children: ReactNode; @@ -14,7 +22,10 @@ const queryClient = new QueryClient({ queryCache: new QueryCache({ onError: (error, query) => { if (query.meta?.errorMessage && typeof query.meta.errorMessage === 'string') { - toast.error(query.meta.errorMessage); + errorToastGrouped(query.meta.errorMessage, { + autoClose: query.meta.autoClose, + toastId: query.meta.toastId, + }); } }, }), diff --git a/frontend/src/__mocks__/styleMock.ts b/frontend/src/__mocks__/styleMock.ts new file mode 100644 index 000000000..d729c4088 --- /dev/null +++ b/frontend/src/__mocks__/styleMock.ts @@ -0,0 +1,3 @@ +// This file serves as mock for css modules as described in https://jestjs.io/docs/webpack +module.exports = {}; +export {}; diff --git a/frontend/src/assets/globe.svg b/frontend/src/assets/globe.svg deleted file mode 100644 index e2e639038..000000000 --- a/frontend/src/assets/globe.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/frontend/src/assets/permaplant-logo-2.svg b/frontend/src/assets/permaplant-logo-2.svg deleted file mode 100644 index c5764592d..000000000 --- a/frontend/src/assets/permaplant-logo-2.svg +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - PermaplanT - - - diff --git a/frontend/src/assets/permaplant-logo-dark.svg b/frontend/src/assets/permaplant-logo-dark.svg deleted file mode 100644 index 34be69fc7..000000000 --- a/frontend/src/assets/permaplant-logo-dark.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/frontend/src/assets/permaplant-logo-gray.svg b/frontend/src/assets/permaplant-logo-gray.svg deleted file mode 100644 index 85093ed72..000000000 --- a/frontend/src/assets/permaplant-logo-gray.svg +++ /dev/null @@ -1,72 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/frontend/src/assets/permaplant-logo.svg b/frontend/src/assets/permaplant-logo.svg deleted file mode 100644 index b9ae0cef2..000000000 --- a/frontend/src/assets/permaplant-logo.svg +++ /dev/null @@ -1,286 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/frontend/src/assets/planning.svg b/frontend/src/assets/planning.svg deleted file mode 100644 index 5dc2b5651..000000000 --- a/frontend/src/assets/planning.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/frontend/src/assets/plant.svg b/frontend/src/assets/plant.svg deleted file mode 100644 index 9900b3ef0..000000000 --- a/frontend/src/assets/plant.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/frontend/src/components/Button/ButtonLink.tsx b/frontend/src/components/Button/ButtonLink.tsx index 3114524dc..b49a7aca6 100644 --- a/frontend/src/components/Button/ButtonLink.tsx +++ b/frontend/src/components/Button/ButtonLink.tsx @@ -1,3 +1,4 @@ +import { MouseEventHandler } from 'react'; import { Link } from 'react-router-dom'; interface ButtonLinkProps { @@ -7,15 +8,22 @@ interface ButtonLinkProps { to: string; /** The className styling of the button. */ className?: string; + /** Clickhandler for the button. */ + onClick?: MouseEventHandler; } /** * A styled link that has properties of a button. */ -const ButtonLink = ({ title, to, className = '' }: ButtonLinkProps) => { +const ButtonLink = ({ title, to, className = '', onClick }: ButtonLinkProps) => { return ( - - + + ); }; diff --git a/frontend/src/components/Button/IconButton.stories.tsx b/frontend/src/components/Button/IconButton.stories.tsx index 57597aa5f..3eacb7f90 100644 --- a/frontend/src/components/Button/IconButton.stories.tsx +++ b/frontend/src/components/Button/IconButton.stories.tsx @@ -1,5 +1,5 @@ import IconButton, { ButtonVariant } from './IconButton'; -import { ReactComponent as PlantIcon } from '@/icons/plant.svg'; +import { ReactComponent as PlantIcon } from '@/svg/icons/plant.svg'; import { reactRouterDecorator } from '@/utils/stories/react-router-decorators'; import type { Meta, StoryObj } from '@storybook/react'; diff --git a/frontend/src/components/Button/IconButton.tsx b/frontend/src/components/Button/IconButton.tsx index c6f4bdf71..ef64445af 100644 --- a/frontend/src/components/Button/IconButton.tsx +++ b/frontend/src/components/Button/IconButton.tsx @@ -1,6 +1,10 @@ interface IconButtonProps extends React.ButtonHTMLAttributes { /** The variant specifies the look of the button. */ variant?: ButtonVariant; + /** Active icons (e.g. Grid shown) are rendered in highlighted fashion. */ + renderAsActive?: boolean; + /** Toolbox icons receive additional styling. */ + isToolboxIcon?: boolean; } export enum ButtonVariant { @@ -8,28 +12,43 @@ export enum ButtonVariant { secondary, } +const variantStyles = { + [ButtonVariant.primary]: + 'hover:text-primary-500 dark:hover:text-primary-500 hover:stroke-primary-500 dark:hover:stroke-primary-500 active:stroke-primary-200 dark:active:stroke-primary-200 hover:fill-primary-500 dark:hover:fill-primary-500 active:fill-primary-200 dark:active:fill-primary-200 focus:ring-primary-300 ', + [ButtonVariant.secondary]: + 'hover:text-secondary-500 dark:hover:text-secondary-500 hover:stroke-secondary-500 dark:hover:stroke-secondary-500 active:stroke-secondary-200 dark:active:stroke-secondary-200 hover:fill-secondary-500 dark:hover:fill-secondary-500 active:fill-secondary-200 dark:active:fill-secondary-200 focus:ring-secondary-300 ', +}; + /** * A clickable icon. * @param props.variant The variant specifies the look of the button. * @param props All React props for buttons can be applied. */ -export default function IconButton({ variant = ButtonVariant.primary, ...props }: IconButtonProps) { - const colors = { - [ButtonVariant.primary]: - 'hover:text-primary-500 dark:hover:text-primary-500 hover:stroke-primary-500 dark:hover:stroke-primary-500 active:stroke-primary-200 dark:active:stroke-primary-200 hover:fill-primary-500 dark:hover:fill-primary-500 active:fill-primary-200 dark:active:fill-primary-200 focus:ring-primary-300 ', - [ButtonVariant.secondary]: - 'hover:text-secondary-500 dark:hover:text-secondary-500 hover:stroke-secondary-500 dark:hover:stroke-secondary-500 active:stroke-secondary-200 dark:active:stroke-secondary-200 hover:fill-secondary-500 dark:hover:fill-secondary-500 active:fill-secondary-200 dark:active:fill-secondary-200 focus:ring-secondary-300 ', - }; - const className = - 'inline-flex h-6 w-6 justify-center rounded-lg items-center text-sm font-medium focus:outline-none focus:ring-4 focus:ring-blue-300 stroke-neutral-800 dark:stroke-neutral-800-dark fill-neutral-800 dark:fill-neutral-800-dark' + - ' disabled:stroke-neutral-500 dark:disabled:stroke-neutral-500-dark ' + +export default function IconButton({ + variant = ButtonVariant.primary, + renderAsActive = false, + isToolboxIcon = false, + ...props +}: IconButtonProps) { + const defaultIconStyles = + 'inline-flex h-6 w-6 justify-center rounded-lg items-center text-sm font-medium focus:outline-none focus:ring-1 focus:ring-secondary-100 dark:focus:ring-secondary-500 focus:border-0 dark:focus:border-0 stroke-neutral-800 dark:stroke-neutral-800-dark fill-neutral-800 dark:fill-neutral-800-dark' + + ' disabled:stroke-neutral-500 dark:disabled:stroke-neutral-500-dark disabled:fill-neutral-500 dark:disabled:fill-neutral-500-dark disabled:cursor-not-allowed' + ' ' + - colors[variant]; + variantStyles[variant]; + + const activeIcon = renderAsActive + ? 'fill-primary-500 dark:fill-primary-400 stroke-primary-500 dark:stroke-primary-400' + : ''; + const toolboxIcon = isToolboxIcon + ? 'mx-1 my-2 first-of-type:ml-2 last-of-type:mr-2 h-8 w-8 p-1 border border-neutral-500 hover:bg-neutral-200 dark:hover:bg-neutral-800 dark:hover:stroke-primary-400 active:stroke-primary-400 active:fill-primary-400' + : ''; + const additionalClasses = props.className ? props.className : ''; + return ( ); } diff --git a/frontend/src/components/Button/SimpleButton.tsx b/frontend/src/components/Button/SimpleButton.tsx index f4cad5f1d..290c87ce9 100644 --- a/frontend/src/components/Button/SimpleButton.tsx +++ b/frontend/src/components/Button/SimpleButton.tsx @@ -11,17 +11,17 @@ export enum ButtonVariant { dangerBase, } -const variantToClassName = { +const variantStyles = { [ButtonVariant.primaryBase]: - 'bg-primary-500 dark:bg-primary-300 border-primary-500 hover:bg-primary-600 dark:hover:bg-primary-200 active:bg-primary-900 text-primary-50 dark:text-primary-700', + 'bg-primary-500 dark:bg-primary-300 border-primary-500 hover:bg-primary-600 dark:hover:bg-primary-200 active:bg-primary-700 dark:active:bg-primary-400 text-primary-50 dark:text-primary-700', [ButtonVariant.secondaryBase]: - 'bg-secondary-500 dark:bg-secondary-300 border-secondary-500 hover:bg-secondary-600 dark:hover:bg-secondary-200 active:bg-secondary-900 text-secondary-50 dark:text-secondary-700', + 'bg-secondary-500 dark:bg-secondary-300 border-secondary-500 hover:bg-secondary-600 dark:hover:bg-secondary-200 active:bg-secondary-900 text-secondary-50 dark:text-secondary-700 dark:focus:ring-secondary-100', [ButtonVariant.primaryContainer]: - 'bg-primary-200 dark:bg-primary-600 border-primary-500 hover:bg-primary-200 dark:hover:bg-primary-600 active:bg-primary-900 text-primary-900 dark:text-primary-200', + 'bg-primary-200 dark:bg-primary-600 border-primary-500 hover:bg-primary-400 dark:hover:bg-primary-400 active:bg-primary-500 dark:active:bg-primary-500 text-primary-900 dark:text-primary-200', [ButtonVariant.secondaryContainer]: - 'bg-secondary-200 dark:bg-secondary-600 border-secondary-500 hover:bg-secondary-200 dark:hover:bg-secondary-600 active:bg-secondary-900 text-secondary-900 dark:text-secondary-200', + 'bg-secondary-200 dark:bg-secondary-600 border-secondary-500 hover:bg-secondary-400 dark:hover:bg-secondary-400 active:bg-secondary-900 dark:active:bg-secondary-900 text-secondary-900 dark:text-secondary-200', [ButtonVariant.dangerBase]: - 'bg-red-500 dark:bg-red-300 border-red-500 hover:bg-red-600 dark:hover:bg-red-200 active:bg-red-900 text-red-50 dark:text-red-700', + 'bg-red-500 dark:bg-red-600 border-red-500 dark:border-red-500 active:border-red-900 dark:active:border-red-800 hover:bg-red-600 dark:hover:bg-red-500 active:bg-red-900 dark:active:bg-red-800 text-red-50', }; /** @@ -33,8 +33,8 @@ export default function SimpleButton({ ...props }: SimpleButtonProps) { const className = - 'button focus:outline-none focus:ring-4 focus:ring-blue-300 border ' + - variantToClassName[variant]; + 'button disabled:bg-neutral-300 disabled:border-neutral-300 disabled:text-neutral-500 dark:disabled:bg-neutral-300-dark dark:disabled:border-neutral-300-dark dark:disabled:text-neutral-500-dark disabled:cursor-not-allowed focus:outline-none focus:ring-2 focus:ring-secondary-200 dark:focus:ring-secondary-300 border ' + + variantStyles[variant]; return (