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 { 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, botanic: Option, details: Option>, planted: Option, } pub fn get_trees(state: State) -> (State, Response) { 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 = 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::>(); 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) { let ie = IdExtractor::borrow_from(&state); let app_state = AppState::borrow_from(&state); let result = { let mut db = app_state.db.lock().unwrap(); match db.query("SELECT id, coord, botanic, botanic_pfaf, german, planted FROM trees WHERE id=$1 LIMIT 1", &[&ie.id]).unwrap() .into_iter() .next() { None => None, Some(row) => { let botanic_pfaf: Option = 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 = 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 = 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), } }) } } }; 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) { let rect = TileExtractor::borrow_from(&state).to_rect(); let app_state = AppState::borrow_from(&state); let result = { let mut db = app_state.db.lock().unwrap(); 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 = row.get(0); let score: f64 = row.get(1); let planted: Option = 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) } }