treeadvisor/server/src/trees.rs

168 lines
6.0 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;
use serde::Serialize;
use mime::{APPLICATION_JSON, IMAGE_PNG};
use http::StatusCode;
use chrono::NaiveDate;
use cairo::{ImageSurface, Context};
use crate::{AppState, AreaExtractor, IdExtractor};
#[derive(Debug, Serialize)]
struct Tree {
id: String,
coords: [f64; 2],
german: Option<String>,
botanic: Option<String>,
details: Option<HashMap<String, String>>,
planted: Option<NaiveDate>,
}
pub fn get_trees(state: State) -> (State, Response<Body>) {
let pe = AreaExtractor::borrow_from(&state);
let app_state = AppState::borrow_from(&state);
let result = {
let mut db = app_state.db.lock().unwrap();
db.query("SELECT id, coord, botanic, german, planted 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),
}
}).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 = {
let mut db = app_state.db.lock().unwrap();
let row = db.query("SELECT id, coord, botanic, botanic_pfaf, german, planted FROM trees WHERE id=$1 LIMIT 1", &[&ie.id]).unwrap()
.into_iter()
.next();
if let Some(row) = row {
let botanic_pfaf: &str = row.get(3);
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);
let pfaf_row = db.query(query.as_str(), &[&botanic_pfaf]).unwrap()
.into_iter()
.next();
pfaf_row.map(|pfaf_row| {
let point: Point<f64> = row.get(1);
let mut details: HashMap<String, String> = HashMap::with_capacity(PFAF_COLS.len());
for (i, col) in PFAF_COLS.iter().enumerate() {
details.insert(col.to_string(), pfaf_row.get(i));
}
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),
}
})
} else {
None
}
};
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 pe = AreaExtractor::borrow_from(&state);
let app_state = AppState::borrow_from(&state);
let result = {
let mut db = app_state.db.lock().unwrap();
db.query("SELECT coord, score FROM trees WHERE coord <@ $1::box AND SCORE IS NOT NULL", &[
&pe.grow(0.2).to_rect()
]).unwrap()
};
const TILE_SIZE: i32 = 256;
// let (w, h) = if pe.w() / 2. > pe.h() {
// (TILE_MAX / 2, (TILE_MAX as f64 * pe.h() / pe.w()) as i32)
// } else {
// (((TILE_MAX as f64 / 2. * pe.w() / pe.h()) as i32), TILE_MAX)
// };
let (w, h) = (TILE_SIZE, TILE_SIZE);
// println!("{}x{} ({}x{})", w, h, pe.w(), pe.h());
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 (r, g, b) = temperature(1. - score);
ctx.set_source_rgba(r, g, b, 0.1);
let radius = 1.5 + 0.03 / pe.w();
// println!("radius: {}", radius);
ctx.arc((point.x() - pe.x1) * (w as f64) / pe.w(), (pe.y2 - point.y()) * (h as f64) / pe.h(),
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.5: yellow, 1.0: red
fn temperature(mut x: f64) -> (f64, f64, f64) {
x = x.max(0.).min(1.);
if x < 0.5 {
(2.0 * x, 1.0, 0.0)
} else {
(1.0, 2.0 - 2.0 * x, 0.0)
}
}