treeadvisor/server/src/trees.rs

184 lines
6.8 KiB
Rust

use std::collections::HashMap;
use std::io::Cursor;
use gotham::{
helpers::http::response::create_response,
hyper::{Body, Response},
state::{FromState, State},
};
use geo::{Point, Rect};
use serde::Serialize;
use mime::{APPLICATION_JSON, IMAGE_PNG};
use http::StatusCode;
use chrono::{Local, NaiveDate};
use cairo::{ImageSurface, Context};
use crate::{AppState, AreaExtractor, IdExtractor, TileExtractor};
trait Grow {
fn grow(&self, factor: f64) -> Self;
}
impl Grow for Rect<f64> {
fn grow(&self, factor: f64) -> Self {
let c = self.center();
let w = self.width();
let h = self.height();
Rect::new((c.x - factor * w / 2., c.y - factor * h / 2.),
(c.x + factor * w / 2., c.y + factor * h / 2.))
}
}
#[derive(Debug, Serialize)]
struct Tree {
id: String,
coords: [f64; 2],
german: Option<String>,
botanic: Option<String>,
details: Option<HashMap<String, String>>,
planted: Option<NaiveDate>,
score: f64,
}
pub fn get_trees(state: State) -> (State, Response<Body>) {
let pe = AreaExtractor::borrow_from(&state);
let app_state = AppState::borrow_from(&state);
let result = app_state.with_db(|db| {
db.query("SELECT id, coord, botanic, german, planted, score FROM trees WHERE coord <@ $1::box ORDER BY coord <-> center($1::box) ASC LIMIT 2000", &[
&pe.to_rect()
]).unwrap()
}).into_iter().map(|row| {
let point: Point<f64> = row.get(1);
Tree {
id: row.get(0),
coords: [point.x(), point.y()],
botanic: row.get(2),
details: None,
german: row.get(3),
planted: row.get(4),
score: row.get(5),
}
}).collect::<Vec<_>>();
let body = serde_json::to_string(&result).unwrap();
let res = create_response(&state, StatusCode::OK, APPLICATION_JSON, body);
(state, res)
}
const PFAF_COLS: &[&str] = &[
"Latin name", "Common name", "Family", "Synonyms",
"Known hazards", "Range", "Habitat", "Rating",
"Medicinal Rating", "Habit", "Height", "Width",
"Hardyness", "FrostTender", "In leaf", "Scented",
"Flowering time", "Seed ripens", "Flower Type", "Pollinators",
"Self-fertile", "Nitrogen fixer", "Wildlife", "Soil",
"Well-drained", "pH", "Shade", "Moisture",
"Saline", "Wind", "Edible uses", "Medicinal",
"Uses notes", "In cultivation?", "Cultivation details", "Growth rate",
"Pollution", "Propagation 1", "Poor soil", "Drought",
"Woodland", "Meadow", "Wall", "Acid",
"Alkaline", "Heavy clay", "Cultivars", "Cultivars in cultivation",
"Pull-out", "Last update", "Record checked", "SiteSpecificNotes",
"Deciduous/Evergreen", "Author", "Botanical references"
];
pub fn get_tree(state: State) -> (State, Response<Body>) {
let ie = IdExtractor::borrow_from(&state);
let app_state = AppState::borrow_from(&state);
let result = app_state.with_db(|db| {
match db.query("SELECT id, coord, botanic, botanic_pfaf, german, planted, score FROM trees WHERE id=$1 LIMIT 1", &[&ie.id]).unwrap()
.into_iter()
.next() {
None => None,
Some(row) => {
let botanic_pfaf: Option<String> = row.get(3);
botanic_pfaf.and_then(|botanic_pfaf| {
let mut query = format!("SELECT ");
for (i, col) in PFAF_COLS.iter().enumerate() {
query = format!("{}{}\"{}\"::text", query, if i == 0 { "" } else { ", " }, col);
}
query = format!("{} FROM \"PlantsForAFuture\" WHERE \"Latin name\"=$1 LIMIT 1", query);
db.query(query.as_str(), &[&botanic_pfaf]).unwrap()
.into_iter()
.next()
}).map(|pfaf_row| {
let mut details: HashMap<String, String> = HashMap::with_capacity(PFAF_COLS.len());
for (i, col) in PFAF_COLS.iter().enumerate() {
if let Some(value) = pfaf_row.get(i) {
details.insert(col.to_string(), value);
}
}
let point: Point<f64> = row.get(1);
Tree {
id: row.get(0),
coords: [point.x(), point.y()],
botanic: row.get(2),
german: row.get(4),
details: Some(details),
planted: row.get(5),
score: row.get(6),
}
})
}
}
});
let res = match result {
Some(result) => {
let body = serde_json::to_string(&result).unwrap();
create_response(&state, StatusCode::OK, APPLICATION_JSON, body)
}
None =>
create_response(&state, StatusCode::NOT_FOUND, APPLICATION_JSON, "{}"),
};
(state, res)
}
pub fn get_heatmap(state: State) -> (State, Response<Body>) {
let rect = TileExtractor::borrow_from(&state).to_rect();
let app_state = AppState::borrow_from(&state);
let result = app_state.with_db(|db| {
db.query("SELECT coord, score, planted FROM trees WHERE coord <@ $1::box AND SCORE IS NOT NULL", &[
&rect.grow(1.2)
]).unwrap()
});
const TILE_SIZE: i32 = 256;
let (w, h) = (TILE_SIZE, TILE_SIZE);
let s = ImageSurface::create(cairo::Format::ARgb32, w, h).unwrap();
let ctx = Context::new(&s).unwrap();
ctx.set_antialias(cairo::Antialias::Fast);
for row in result {
let point: Point<f64> = row.get(0);
let score: f64 = row.get(1);
let planted: Option<NaiveDate> = row.get(2);
let age = planted.map(|planted| ((Local::today().naive_local() - planted).num_days()) / 365);
let (r, g, b) = temperature(1. - score);
ctx.set_source_rgba(r, g, b, 0.5);
let radius = 1.5 + (0.4 + age.unwrap_or(50) as f64 / 80.).min(1.4) * 0.02 / rect.width();
ctx.arc((point.x() - rect.min().x) * (w as f64) / rect.width(), (rect.max().y - point.y()) * (h as f64) / rect.height(),
radius, 0., 2. * core::f64::consts::PI);
ctx.fill().unwrap();
}
let mut buffer = Cursor::new(vec![]);
s.write_to_png(&mut buffer).unwrap();
let res = create_response(&state, StatusCode::OK, IMAGE_PNG, buffer.into_inner());
(state, res)
}
/// 0.0: green, 0.4: yellow, 0.8: red, 1.0: purple
fn temperature(mut x: f64) -> (f64, f64, f64) {
x = x.max(0.).min(1.);
const MAX: f64 = 0.95;
if x < 0.4 {
(MAX * x / 0.4, MAX, 0.0)
} else if x < 0.8 {
(MAX, MAX - MAX * (x - 0.4) / 0.4, 0.0)
} else {
(MAX, 0.0, MAX * (x - 0.8) / 0.2)
}
}