Skip to content
This repository has been archived by the owner on Apr 24, 2024. It is now read-only.

Backend heatmap #566

Merged
merged 44 commits into from
Jul 16, 2023
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
863fec3
simply heatmap returning matrix of 0.5
kitzbergerg Jul 3, 2023
0db085b
only calculate score for intersection with map_polygon, add TODOs
kitzbergerg Jul 3, 2023
f3e4200
update utoipa API doc
kitzbergerg Jul 3, 2023
ba0839a
move heatmap to plant_layer
kitzbergerg Jul 3, 2023
fde4b78
add rustdoc
kitzbergerg Jul 3, 2023
0eaff84
return image from heatmap endpoint, throw error on non existant map i…
kitzbergerg Jul 6, 2023
81ecd3a
fix clippy errors
kitzbergerg Jul 6, 2023
c146127
set geometry of map in frontend
kitzbergerg Jul 6, 2023
5a75959
rename map_geom field to geometry
kitzbergerg Jul 6, 2023
bc75d9c
fix tests by adding geometry to maps
kitzbergerg Jul 6, 2023
7a0fe79
add test for heatmap
kitzbergerg Jul 6, 2023
32064ed
Merge branch 'master' into backend_heatmap
kitzbergerg Jul 6, 2023
878e39e
fix error in frontend
kitzbergerg Jul 6, 2023
fbe0f5f
use bounding box and granularity to calculate heatmap
kitzbergerg Jul 8, 2023
b0f9ce4
add colors to heatmap, fix index out of bounds
kitzbergerg Jul 8, 2023
1098e1f
add more detailed heatmap tests
kitzbergerg Jul 8, 2023
9530eba
fix clippy errors
kitzbergerg Jul 8, 2023
3a86842
add score based on plants relation on layer
kitzbergerg Jul 8, 2023
c839a35
refactor SQL queries and add better doc
kitzbergerg Jul 8, 2023
c8204ef
fix tests and add negative test
kitzbergerg Jul 8, 2023
0b5c826
add heatmap to changelog
kitzbergerg Jul 8, 2023
e4e0052
swap coordinate system to be (0,0) at top left
kitzbergerg Jul 9, 2023
fd28fba
adjust heatmap down.sql
kitzbergerg Jul 9, 2023
6a77ade
Merge branch 'master' into backend_heatmap
kitzbergerg Jul 11, 2023
e9070da
add test protocol for manual test to doc
kitzbergerg Jul 11, 2023
6ca9089
update test protocol
kitzbergerg Jul 11, 2023
b555522
update test protocol according to suggestions
kitzbergerg Jul 14, 2023
fb6c459
update frontend according to suggestions
kitzbergerg Jul 14, 2023
aaa1919
update heatmap sql according to suggestions
kitzbergerg Jul 14, 2023
c012c33
add more detailed heatmap endpoint description
kitzbergerg Jul 14, 2023
f302321
use integers instead of floats for bounding box and granularity
kitzbergerg Jul 14, 2023
0df1e8e
improve rustdoc error message on heatmap
kitzbergerg Jul 14, 2023
fae9e89
Merge branch 'master' of github.com:ElektraInitiative/PermaplanT into…
kitzbergerg Jul 14, 2023
7f7357a
improve rustdoc error message on heatmap in service
kitzbergerg Jul 14, 2023
21235f9
add doc about how to update the schema.patch file
kitzbergerg Jul 14, 2023
cd9e36a
fix mdbook error
kitzbergerg Jul 14, 2023
35078f2
add doc about the coordinate system
kitzbergerg Jul 15, 2023
bd2d722
move coordinate system doc to solution
kitzbergerg Jul 15, 2023
439ba82
allow for updating the maps geometry
kitzbergerg Jul 15, 2023
1646929
change values in SQL to floats
kitzbergerg Jul 15, 2023
a6ee00f
change from brown to grey for low scores in heatmap
kitzbergerg Jul 15, 2023
934edc6
fix clippy errors
kitzbergerg Jul 15, 2023
aa97b0d
Merge branch 'master' into backend_heatmap
kitzbergerg Jul 15, 2023
652904e
set geometry of UpdateMap as Option in typeshare
kitzbergerg Jul 15, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 78 additions & 1 deletion backend/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion backend/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ diesel = { version = "2.0.2", features = [
] }
diesel-async = { version = "0.2.2", features = ["deadpool", "postgres"] }
diesel-derive-enum = { version = "2.0.0-rc.0", features = ["postgres"] }
postgis_diesel = "2.1.0"
postgis_diesel = { version = "2.1.0", features = ["serde"] }

# Other
serde_json = "1.0.95"
Expand All @@ -54,6 +54,7 @@ uuid = { version = "1.3.2", features = ["serde", "v4"] }
log = "0.4.17"
env_logger = "0.10.0"
futures = "0.3.28"
image = { version = "0.24.6", default-features = false, features = ["png"] }


[dev-dependencies]
Expand Down
7 changes: 7 additions & 0 deletions backend/migrations/2023-07-03-165000_heatmap/down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
-- 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 calculate_bbox;
ALTER TABLE maps DROP COLUMN geometry;
144 changes: 144 additions & 0 deletions backend/migrations/2023-07-03-165000_heatmap/up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
-- Your SQL goes here
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 REAL, y_min REAL, x_max REAL, y_max REAL) AS $$
BEGIN
RETURN QUERY
SELECT
CAST(ST_XMin(bbox) AS REAL) AS x_min,
CAST(ST_YMin(bbox) AS REAL) AS y_min,
CAST(ST_XMax(bbox) AS REAL) AS x_max,
CAST(ST_YMax(bbox) AS REAL) AS y_max
FROM (
SELECT
ST_Envelope(geometry) AS bbox
FROM maps
WHERE id = map_id
) AS subquery;
END;
$$ LANGUAGE plpgsql;


-- Returns scores from 0-1 for each pixel of the map. It should never return values greater than 1.
kitzbergerg marked this conversation as resolved.
Show resolved Hide resolved
-- Values where the plant should not be placed are close to 0.
-- Values where the plant should be placed are close to 1.
-- The score at position x=0,y=0 is at the position x=x_min,y=y_min.
kitzbergerg marked this conversation as resolved.
Show resolved Hide resolved
CREATE OR REPLACE FUNCTION calculate_score(p_map_id INTEGER, p_layer_id INTEGER, p_plant_id INTEGER, granularity REAL, x_min REAL, y_min REAL, x_max REAL, y_max REAL)
kitzbergerg marked this conversation as resolved.
Show resolved Hide resolved
RETURNS TABLE(score REAL, x INTEGER, y INTEGER) AS $$
DECLARE
map_geometry GEOMETRY(POLYGON, 4326);
cell GEOMETRY;
bbox GEOMETRY;
num_cols INTEGER;
num_rows INTEGER;
x_pos REAL;
y_pos REAL;
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 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;
END IF;

-- INTO STRICT makes sure the map exists. Does have to be explicitly checked as bounding box calculation would error anyways.
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

-- Calculate the score for each pixel 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.
x_pos := x_min + (i * granularity);
y_pos := y_min + (j * granularity);

-- 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);

-- 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 := scale_score(score); -- scale the score to be between 0 and 1
ELSE
score := 0.0;
END IF;

x := i;
y := j;

RETURN NEXT;
END LOOP;
END LOOP;
END;
$$ LANGUAGE plpgsql;

-- Scales the score to values 0-1.
CREATE OR REPLACE FUNCTION scale_score(input REAL)
RETURNS REAL AS $$
BEGIN
RETURN LEAST(GREATEST(input, 0), 1);
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 REAL, y_pos REAL)
RETURNS FLOAT AS $$
DECLARE
plant_relation RECORD;
distance REAL;
weight REAL;
score REAL := 0;
BEGIN
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);

-- calculate weight based on distance
weight := 1 / (distance + 1);

-- update score based on relation
IF plant_relation.relation = 'companion' THEN
score := score + 0.5 * weight;
ELSE
score := score - 0.5 * weight;
END IF;
END LOOP;

RETURN score;
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 $$
BEGIN
RETURN QUERY
-- We only need x,y and type of relation to calculate a score.
SELECT plantings.x, plantings.y, relations.relation
FROM plantings
JOIN plants ON plantings.plant_id = plants.id
JOIN (
-- We need UNION as the relation is bidirectional.
SELECT plant1 AS plant, r1.relation
FROM relations r1
WHERE plant2 = p_plant_id
AND r1.relation != 'neutral'
UNION
SELECT plant2 AS plant, r2.relation
FROM relations r2
WHERE plant1 = p_plant_id
AND r2.relation != 'neutral'
) relations ON plants.id = relations.plant
WHERE plantings.layer_id = p_layer_id;
END;
$$ LANGUAGE plpgsql;
29 changes: 16 additions & 13 deletions backend/src/config/api_doc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

use actix_web::web;
use utoipa::{
openapi::security::{AuthorizationCode, Flow, OAuth2, Password, Scopes, SecurityScheme},
openapi::security::{AuthorizationCode, Flow, OAuth2, Scopes, SecurityScheme},
Modify, OpenApi,
};
use utoipa_swagger_ui::SwaggerUi;
Expand All @@ -16,11 +16,14 @@ use crate::{
MovePlantingDto, NewPlantingDto, PlantingDto, TransformPlantingDto,
UpdatePlantingDto,
},
ConfigDto, LayerDto, MapDto, NewLayerDto, NewMapDto, NewSeedDto, PageLayerDto,
PageMapDto, PagePlantsSummaryDto, PageSeedDto, PlantsSummaryDto, RelationDto,
RelationsDto, SeedDto,
ConfigDto, Coordinates, LayerDto, MapDto, NewLayerDto, NewMapDto, NewSeedDto,
PageLayerDto, PageMapDto, PagePlantsSummaryDto, PageSeedDto, PlantsSummaryDto,
RelationDto, RelationsDto, SeedDto,
},
r#enum::{
privacy_options::PrivacyOptions, quality::Quality, quantity::Quantity,
relation_type::RelationType,
},
r#enum::{quality::Quality, quantity::Quantity, relation_type::RelationType},
},
};

Expand Down Expand Up @@ -81,6 +84,8 @@ struct PlantsApiDoc;
PageMapDto,
MapDto,
NewMapDto,
PrivacyOptions,
Coordinates
)
),
modifiers(&SecurityAddon)
Expand Down Expand Up @@ -111,6 +116,7 @@ struct LayerApiDoc;
#[derive(OpenApi)]
#[openapi(
paths(
plant_layer::heatmap,
plant_layer::find_relations
),
components(
Expand Down Expand Up @@ -186,14 +192,11 @@ impl Modify for SecurityAddon {
let components = openapi.components.as_mut().unwrap();

let config = &Config::get().openid_configuration;
let oauth2 = OAuth2::new([
Flow::AuthorizationCode(AuthorizationCode::new(
config.authorization_endpoint.clone(),
config.token_endpoint.clone(),
Scopes::new(),
)),
Flow::Password(Password::new(config.token_endpoint.clone(), Scopes::new())),
]);
let oauth2 = OAuth2::new([Flow::AuthorizationCode(AuthorizationCode::new(
config.authorization_endpoint.clone(),
config.token_endpoint.clone(),
Scopes::new(),
))]);
components.add_security_scheme("oauth2", SecurityScheme::OAuth2(oauth2));
}
}
1 change: 1 addition & 0 deletions backend/src/config/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ pub fn config(cfg: &mut web::ServiceConfig) {
.service(layers::delete)
.service(
web::scope("/plants")
.service(plant_layer::heatmap)
.service(plant_layer::find_relations)
.service(
web::scope("/suggestions").service(planting_suggestions::find),
Expand Down
Loading