use std::collections::{BTreeMap, HashMap}; use std::io::Cursor; use std::rc::Rc; use std::time::Instant; use gotham::{ helpers::http::response::create_response, hyper::{Body, Response}, state::{FromState, State}, }; use geo::{LineString, Rect}; use mime::IMAGE_PNG; use http::StatusCode; use cairo::{ImageSurface, Context}; use crate::{tile_style, AppState, 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.)) } } struct Shape { style: &'static tile_style::Style, path: LineString, excludes: Rc>>, } pub fn get_tile(state: State) -> (State, Response) { let mut times = vec![Instant::now()]; let te = TileExtractor::borrow_from(&state); let zoom = te.zoom; let bounds = te.to_rect(); let app_state = AppState::borrow_from(&state); 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); ctx.set_line_join(cairo::LineJoin::Round); ctx.set_line_cap(cairo::LineCap::Butt); // background ctx.set_source_rgba(0.5, 0.5, 0.5, 1.); ctx.fill().unwrap(); let pctx = PaintContext { ctx, w: w as f64, h: h as f64, bounds, }; let mut shapes: BTreeMap> = BTreeMap::new(); times.push(Instant::now()); let ways_result = app_state.with_db(|db| db.query("SELECT geo, attrs FROM osm_ways WHERE box(polygon(pclose(geo))) && $1::box", &[ &bounds.grow(1.2) ]).unwrap() ); times.push(Instant::now()); for row in ways_result { let tags = if let serde_json::Value::Object(tags) = row.get(1) { tags.into_iter() .filter_map(|(k, v)| if let serde_json::Value::String(s) = v { Some((k, s)) } else { None }) .collect::>() } else { continue; }; tile_style::find(&zoom, &tags) .map(|style| { let path: LineString = row.get(0); let shape = Shape { style, path: path.clone(), excludes: Rc::new(vec![]) }; shapes.entry(style.z_index) .or_default() .push(shape); }); } times.push(Instant::now()); let multis_result = app_state.with_db(|db| { let multi_members = db.query("SELECT id, m_role, path(m_geo) FROM osm_multipolygon_members WHERE box(m_geo) && $1::box", &[ &bounds.grow(1.2) ]).unwrap(); let mut members_by_id: HashMap)>> = HashMap::new(); for row in multi_members { members_by_id.entry(row.get(0)) .or_default() .push((row.get(1), row.get(2))); } let ids: Vec = members_by_id.keys() .cloned() .collect(); let multis = db.query("SELECT id, attrs FROM osm_multipolygons WHERE id=ANY($1)", &[&ids]) .unwrap(); multis.into_iter().map(move |row| { let id = row.get(0); let attrs = row.get(1); let members = members_by_id.remove(&id).unwrap(); (id, attrs, members) }) }); times.push(Instant::now()); let mut multis_count = 0usize; for (_id, attrs, members) in multis_result { multis_count += 1; let tags = if let serde_json::Value::Object(tags) = attrs { tags.into_iter() .filter_map(|(k, v)| if let serde_json::Value::String(s) = v { Some((k, s)) } else { None }) .collect::>() } else { continue; }; tile_style::find(&zoom, &tags) .map(|style| { let filter_members = |target_role| members.iter().filter_map(move |(m_role, m_geo)| { if m_role == target_role { Some(m_geo) } else { None } }); let inners: Rc> = Rc::new( filter_members("inner") .cloned() .collect() ); for path in filter_members("outer") { // TODO: expensive? let inners = inners.clone(); let shape = Shape { style, path: path.clone(), excludes: inners }; shapes.entry(style.z_index) .or_default() .push(shape); } }); } times.push(Instant::now()); for shapes in shapes.values() { for Shape { style, path, excludes } in shapes { pctx.ctx.save().unwrap(); if path.is_closed() { style.fill.map(|(r, g, b, a)| { pctx.ctx.set_source_rgba(r, g, b, a); pctx.create_path(path); pctx.ctx.close_path(); if ! excludes.is_empty() { pctx.ctx.clip_preserve(); for exclude in excludes.iter() { pctx.create_path(exclude); pctx.ctx.close_path(); } } pctx.ctx.fill().unwrap(); }); } pctx.ctx.restore().unwrap(); pctx.ctx.save().unwrap(); style.stroke.map(|(size, (r, g, b, a))| { pctx.ctx.set_line_width(size as f64 / 600. / pctx.bounds.width()); pctx.ctx.set_source_rgba(r, g, b, a); pctx.create_path(path); pctx.ctx.stroke().unwrap(); }); pctx.ctx.restore().unwrap(); } } times.push(Instant::now()); let mut buffer = Cursor::new(vec![]); s.write_to_png(&mut buffer).unwrap(); times.push(Instant::now()); let res = create_response(&state, StatusCode::OK, IMAGE_PNG, buffer.into_inner()); times.push(Instant::now()); let mut last_time = None; let mut periods = vec![]; for time in times { if let Some(last_time) = last_time { periods.push(time - last_time); } last_time = Some(time); } println!("get_tile@{} {} ways {} multipolygons {} shapes {:?}", zoom, "unknown" /*ways_result.len()*/, multis_count, shapes.values().map(|shapes| shapes.len()).sum::(), periods); (state, res) } struct PaintContext { ctx: Context, bounds: Rect, w: f64, h: f64, } impl PaintContext { pub fn lonlat_to_xy(&self, lon: f64, lat: f64) -> (f64, f64) { ((lon - self.bounds.min().x) * self.w / self.bounds.width(), (self.bounds.max().y - lat) * self.h / self.bounds.height()) } pub fn create_path(&self, path: &LineString) { for (i, point) in path.points_iter().enumerate() { let (x, y) = self.lonlat_to_xy(point.x(), point.y()); if i == 0 { self.ctx.move_to(x, y); } else { self.ctx.line_to(x, y); } } } }