diff --git a/server/src/main.rs b/server/src/main.rs index 6fe3859..1b51616 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -24,6 +24,7 @@ use serde::Deserialize; mod area; mod trees; +mod tile_style; mod tiles; #[derive(Clone, StateData)] diff --git a/server/src/tile_style.rs b/server/src/tile_style.rs new file mode 100644 index 0000000..b700416 --- /dev/null +++ b/server/src/tile_style.rs @@ -0,0 +1,86 @@ +use std::collections::HashMap; + +pub enum Selector { + MinZoom(i32), + MaxZoom(i32), + HasTag(&'static str), + TagEquals(&'static str, &'static str), + And(&'static [Selector]), + Or(&'static [Selector]), +} + +impl Selector { + fn matches(&self, zoom: &i32, tags: &HashMap) -> bool { + match self { + Selector::MinZoom(min_zoom) => + zoom >= min_zoom, + Selector::MaxZoom(max_zoom) => + zoom <= max_zoom, + Selector::HasTag(name) => + tags.contains_key(*name), + Selector::TagEquals(name, value) => + tags.get(*name).map(|s| s.as_str()) == Some(*value), + Selector::And(selectors) => + selectors.iter().all(|selector| selector.matches(zoom, tags)), + Selector::Or(selectors) => + selectors.iter().any(|selector| selector.matches(zoom, tags)), + } + } +} + +type Color = (f64, f64, f64, f64); + +pub struct Style { + pub z_index: i32, + pub stroke: Option<(f64, Color)>, + pub fill: Option, +} + +impl Style { + const DEFAULT: Style = Style { + z_index: 0, + stroke: None, + fill: None, + }; +} + +const STYLES: &[(Selector, Style)] = &[ + (Selector::HasTag("tunnel"), + Style { z_index: -10, ..Style::DEFAULT }), + + (Selector::TagEquals("highway", "motorway"), + Style { z_index: 10, stroke: Some((60., (1., 1., 1., 1.))), ..Style::DEFAULT }), + (Selector::TagEquals("highway", "trunk"), + Style { z_index: 9, stroke: Some((40., (1., 1., 1., 1.))), ..Style::DEFAULT }), + (Selector::TagEquals("highway", "primary"), + Style { z_index: 8, stroke: Some((35., (1., 1., 1., 1.))), ..Style::DEFAULT }), + (Selector::And(&[Selector::TagEquals("highway", "secondary"), Selector::MinZoom(12)]), + Style { z_index: 7, stroke: Some((30., (1., 1., 1., 1.))), ..Style::DEFAULT }), + (Selector::And(&[Selector::TagEquals("highway", "tertiary"), Selector::MinZoom(14)]), + Style { z_index: 6, stroke: Some((20., (1., 1., 1., 1.))), ..Style::DEFAULT }), + (Selector::And(&[Selector::TagEquals("highway", "residential"), Selector::MinZoom(16)]), + Style { z_index: 5, stroke: Some((15., (1., 1., 1., 1.))), ..Style::DEFAULT }), + (Selector::And(&[Selector::TagEquals("highway", "living_street"), Selector::MinZoom(16)]), + Style { z_index: 4, stroke: Some((12., (1., 1., 1., 1.))), ..Style::DEFAULT }), + (Selector::And(&[Selector::HasTag("highway"), Selector::MinZoom(18)]), + Style { z_index: 3, stroke: Some((10., (1., 1., 1., 1.))), ..Style::DEFAULT }), + + (Selector::And(&[Selector::HasTag("railway"), Selector::MinZoom(10)]), + Style { z_index: 20, stroke: Some((3., (0., 0., 0., 1.))), ..Style::DEFAULT }), + + (Selector::Or(&[Selector::HasTag("waterway"), Selector::TagEquals("natural", "water")]), + Style { z_index: -2, fill: Some((0.5, 0.5, 0.7, 1.)), ..Style::DEFAULT }), + + (Selector::And(&[Selector::HasTag("building"), Selector::MinZoom(17)]), + Style { z_index: 30, fill: Some((0.7, 0.7, 0.7, 1.)), stroke: Some((1., (0., 0., 0., 1.))), ..Style::DEFAULT }), +]; + +pub fn find(zoom: &i32, tags: &HashMap) -> Option<&'static Style> { + for (selector, style) in STYLES { + if selector.matches(zoom, tags) { + return Some(&style); + } + } + + None +} diff --git a/server/src/tiles.rs b/server/src/tiles.rs index ec76a94..62c117c 100644 --- a/server/src/tiles.rs +++ b/server/src/tiles.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::collections::{BTreeMap, HashMap}; use std::io::Cursor; use gotham::{ helpers::http::response::create_response, @@ -11,7 +11,7 @@ use mime::{APPLICATION_JSON, IMAGE_PNG}; use http::StatusCode; use chrono::{Local, NaiveDate}; use cairo::{ImageSurface, Context}; -use crate::{AppState, AreaExtractor, IdExtractor, TileExtractor}; +use crate::{tile_style, AppState, AreaExtractor, IdExtractor, TileExtractor}; trait Grow { fn grow(&self, factor: f64) -> Self; @@ -27,11 +27,16 @@ impl Grow for Rect { } } +struct Shape { + style: &'static tile_style::Style, + path: LineString, +} + pub fn get_tile(state: State) -> (State, Response) { let te = TileExtractor::borrow_from(&state); let zoom = te.zoom; let bounds = te.to_rect(); - println!("get_tile {:?}", bounds); + println!("get_tile@{} {:?}", zoom, bounds); let app_state = AppState::borrow_from(&state); let result = { let mut db = app_state.db.lock().unwrap(); @@ -40,11 +45,16 @@ pub fn get_tile(state: State) -> (State, Response) { ]).unwrap() }; - const TILE_SIZE: i32 = 512; + 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, @@ -52,9 +62,9 @@ pub fn get_tile(state: State) -> (State, Response) { h: h as f64, bounds, }; - + + let mut shapes: BTreeMap> = BTreeMap::new(); for row in result { - let path: LineString = row.get(0); let tags = if let serde_json::Value::Object(tags) = row.get(1) { tags.into_iter() @@ -67,18 +77,45 @@ pub fn get_tile(state: State) -> (State, Response) { } else { continue; }; - - pctx.ctx.save().unwrap(); - if tags.contains_key(&"tunnel".to_string()) { - // ignore tunnels - } if tags.contains_key(&"highway".to_string()) { - draw_highway(&pctx, &path, &tags); - } else if tags.contains_key(&"waterway".to_string()) || tags.get(&"natural".to_string()).map(|s| s.as_str()) == Some("water") { - draw_waterway(&pctx, &path, &tags); - } else if zoom > 14 && tags.contains_key(&"building".to_string()) { - draw_building(&pctx, &path, &tags); + + tile_style::find(&zoom, &tags) + .map(|style| { + let path: LineString = row.get(0); + let shape = Shape { style, path }; + shapes.entry(style.z_index) + .or_default() + .push(shape); + }); + } + + for shapes in shapes.values() { + for Shape { style, path } in shapes { + pctx.ctx.save().unwrap(); + + // TODO: multipolygon relations + 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(); + 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(); } - pctx.ctx.restore().unwrap(); } let mut buffer = Cursor::new(vec![]); @@ -111,48 +148,3 @@ impl PaintContext { } } } - -fn draw_highway(pctx: &PaintContext, path: &LineString, tags: &HashMap) { - // println!("highway: {:?} {:?}", path, tags); - let size = match tags.get("highway").map(|s| s.as_str()) { - Some("highway") => 60, - Some("trunk") => 40, - Some("primary") => 25, - Some("secondary") => 15, - Some("tertiary") => 10, - Some("residential") => 8, - Some("living_street") => 5, - Some(highway) => { - println!("highway: {}", highway); - 3 - } - None => unreachable!(), - }; - pctx.ctx.set_line_width(size as f64 / 350. / pctx.bounds.width()); - pctx.ctx.set_source_rgba(1., 1., 1., (size as f64 / 10.).min(1.)); - pctx.create_path(path); - pctx.ctx.stroke().unwrap(); -} - -fn draw_waterway(pctx: &PaintContext, path: &LineString, tags: &HashMap) { - pctx.ctx.set_source_rgba(0.2, 0.2, 0.8, 1.0); - - pctx.create_path(path); - if path.is_closed() { - pctx.ctx.close_path(); - pctx.ctx.fill().unwrap(); - } else { - pctx.ctx.stroke().unwrap(); - } -} - -fn draw_building(pctx: &PaintContext, path: &LineString, tags: &HashMap) { - pctx.create_path(path); - pctx.ctx.close_path(); - - pctx.ctx.set_source_rgba(0.4, 0.4, 0.4, 0.8); - pctx.ctx.fill().unwrap(); - - pctx.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0); - pctx.ctx.stroke().unwrap(); -} diff --git a/server/static/app.js b/server/static/app.js index d88daae..1631522 100644 --- a/server/static/app.js +++ b/server/static/app.js @@ -1,8 +1,10 @@ const TREES_MIN_ZOOM = 17; var map = L.map('map').setView([51.05, 13.75], TREES_MIN_ZOOM, { + // maxZoom: 20, }); +L.tileLayer('/tiles/{z}/{x}/{y}/tile.png', { attribution: '© OpenStreetMap contributors', maxZoom: 30, }).addTo(map);