From 29da713903404c8e2e7c1831c4d71e3a17daa732 Mon Sep 17 00:00:00 2001 From: Dustin Carlino Date: Sat, 21 Nov 2020 11:11:57 -0800 Subject: [PATCH] Display a simple count of amenities reachable from a building. #393 --- abstutil/src/collections.rs | 2 +- abstutil/src/serde.rs | 3 +- game/src/devtools/fifteen_min/isochrone.rs | 150 ++++++++++++--------- game/src/devtools/fifteen_min/mod.rs | 37 ++--- 4 files changed, 110 insertions(+), 82 deletions(-) diff --git a/abstutil/src/collections.rs b/abstutil/src/collections.rs index efa27b1bd0..0bd0ca1303 100644 --- a/abstutil/src/collections.rs +++ b/abstutil/src/collections.rs @@ -54,7 +54,7 @@ where self.map.len() } - pub(crate) fn raw_map(&self) -> &BTreeMap> { + pub fn borrow(&self) -> &BTreeMap> { &self.map } diff --git a/abstutil/src/serde.rs b/abstutil/src/serde.rs index 9968ba33ab..b425011a4d 100644 --- a/abstutil/src/serde.rs +++ b/abstutil/src/serde.rs @@ -105,8 +105,7 @@ pub fn serialize_multimap< map: &MultiMap, s: S, ) -> Result { - // TODO maybe need to sort to have deterministic output - map.raw_map().iter().collect::>().serialize(s) + map.borrow().iter().collect::>().serialize(s) } /// Deserializes a MultiMap. diff --git a/game/src/devtools/fifteen_min/isochrone.rs b/game/src/devtools/fifteen_min/isochrone.rs index b6bf6afdf3..8e56c2819b 100644 --- a/game/src/devtools/fifteen_min/isochrone.rs +++ b/game/src/devtools/fifteen_min/isochrone.rs @@ -1,87 +1,111 @@ +use std::collections::HashMap; + +use abstutil::MultiMap; use geom::{Duration, Polygon}; use map_model::{connectivity, BuildingID}; use widgetry::{Color, Drawable, EventCtx, GeomBatch}; use crate::app::App; use crate::common::Grid; +use crate::helpers::amenity_type; /// Represents the area reachable from a single building. pub struct Isochrone { /// Colored polygon contours, uploaded to the GPU and ready for drawing pub draw: Drawable, - /* TODO This is a good place to store the buildings within 5 mins, 10 mins, etc - * and maybe some summary of the types of amenities within these ranges */ + /// How far away is each building from the start? + pub time_to_reach_building: HashMap, + /// Per category of amenity (defined by helpers::amenity_type), what buildings have that? + pub amenities_reachable: MultiMap<&'static str, BuildingID>, } impl Isochrone { pub fn new(ctx: &mut EventCtx, app: &App, start: BuildingID) -> Isochrone { - // To generate the polygons covering areas between 0-5 mins, 5-10 mins, etc, we have to feed - // in a 2D grid of costs. Use a 100x100 meter resolution. - let bounds = app.primary.map.get_bounds(); - let resolution_m = 100.0; - // The costs we're storing are currenly durations, but the contour crate needs f64, so - // just store the number of seconds. - let mut grid: Grid = Grid::new( - (bounds.width() / resolution_m).ceil() as usize, - (bounds.height() / resolution_m).ceil() as usize, - 0.0, - ); + let time_to_reach_building = + connectivity::all_costs_from(&app.primary.map, start, Duration::minutes(15)); + let draw = draw_isochrone(app, &time_to_reach_building).upload(ctx); - // Calculate the cost from the start building to every other building in the map - for (b, cost) in - connectivity::all_costs_from(&app.primary.map, start, Duration::minutes(15)) - { - // What grid cell does the building belong to? - let pt = app.primary.map.get_b(b).polygon.center(); - let idx = grid.idx( - ((pt.x() - bounds.min_x) / resolution_m) as usize, - ((pt.y() - bounds.min_y) / resolution_m) as usize, - ); - // Don't add! If two buildings map to the same cell, we should pick a finer resolution. - grid.data[idx] = cost.inner_seconds(); - } - - // Generate polygons covering the contour line where the cost in the grid crosses these - // threshold values. - let thresholds = vec![ - 0.1, - Duration::minutes(5).inner_seconds(), - Duration::minutes(10).inner_seconds(), - Duration::minutes(15).inner_seconds(), - ]; - // And color the polygon for each threshold - let colors = vec![ - Color::GREEN.alpha(0.5), - Color::ORANGE.alpha(0.5), - Color::RED.alpha(0.5), - ]; - let smooth = false; - let c = contour::ContourBuilder::new(grid.width as u32, grid.height as u32, smooth); - let mut batch = GeomBatch::new(); - // The last feature returned will be larger than the last threshold value. We don't want to - // display that at all. zip() will omit this last pair, since colors.len() == - // thresholds.len() - 1. - // - // TODO Actually, this still isn't working. I think each polygon is everything > the - // threshold, not everything between two thresholds? - for (feature, color) in c - .contours(&grid.data, &thresholds) - .unwrap() - .into_iter() - .zip(colors) - { - match feature.geometry.unwrap().value { - geojson::Value::MultiPolygon(polygons) => { - for p in polygons { - batch.push(color, Polygon::from_geojson(&p).scale(resolution_m)); - } + let mut amenities_reachable = MultiMap::new(); + for b in time_to_reach_building.keys() { + let bldg = app.primary.map.get_b(*b); + for amenity in &bldg.amenities { + if let Some(category) = amenity_type(&amenity.amenity_type) { + amenities_reachable.insert(category, bldg.id); } - _ => unreachable!(), } } Isochrone { - draw: batch.upload(ctx), + draw, + time_to_reach_building, + amenities_reachable, } } } + +fn draw_isochrone(app: &App, time_to_reach_building: &HashMap) -> GeomBatch { + // To generate the polygons covering areas between 0-5 mins, 5-10 mins, etc, we have to feed + // in a 2D grid of costs. Use a 100x100 meter resolution. + let bounds = app.primary.map.get_bounds(); + let resolution_m = 100.0; + // The costs we're storing are currenly durations, but the contour crate needs f64, so + // just store the number of seconds. + let mut grid: Grid = Grid::new( + (bounds.width() / resolution_m).ceil() as usize, + (bounds.height() / resolution_m).ceil() as usize, + 0.0, + ); + + // Calculate the cost from the start building to every other building in the map + for (b, cost) in time_to_reach_building { + // What grid cell does the building belong to? + let pt = app.primary.map.get_b(*b).polygon.center(); + let idx = grid.idx( + ((pt.x() - bounds.min_x) / resolution_m) as usize, + ((pt.y() - bounds.min_y) / resolution_m) as usize, + ); + // Don't add! If two buildings map to the same cell, we should pick a finer resolution. + grid.data[idx] = cost.inner_seconds(); + } + + // Generate polygons covering the contour line where the cost in the grid crosses these + // threshold values. + let thresholds = vec![ + 0.1, + Duration::minutes(5).inner_seconds(), + Duration::minutes(10).inner_seconds(), + Duration::minutes(15).inner_seconds(), + ]; + // And color the polygon for each threshold + let colors = vec![ + Color::GREEN.alpha(0.5), + Color::ORANGE.alpha(0.5), + Color::RED.alpha(0.5), + ]; + let smooth = false; + let c = contour::ContourBuilder::new(grid.width as u32, grid.height as u32, smooth); + let mut batch = GeomBatch::new(); + // The last feature returned will be larger than the last threshold value. We don't want to + // display that at all. zip() will omit this last pair, since colors.len() == + // thresholds.len() - 1. + // + // TODO Actually, this still isn't working. I think each polygon is everything > the + // threshold, not everything between two thresholds? + for (feature, color) in c + .contours(&grid.data, &thresholds) + .unwrap() + .into_iter() + .zip(colors) + { + match feature.geometry.unwrap().value { + geojson::Value::MultiPolygon(polygons) => { + for p in polygons { + batch.push(color, Polygon::from_geojson(&p).scale(resolution_m)); + } + } + _ => unreachable!(), + } + } + + batch +} diff --git a/game/src/devtools/fifteen_min/mod.rs b/game/src/devtools/fifteen_min/mod.rs index b25e04db79..7f501c2275 100644 --- a/game/src/devtools/fifteen_min/mod.rs +++ b/game/src/devtools/fifteen_min/mod.rs @@ -35,22 +35,27 @@ impl Viewer { pub fn new(ctx: &mut EventCtx, app: &App, start: BuildingID) -> Box> { let start = app.primary.map.get_b(start); + let isochrone = Isochrone::new(ctx, app, start.id); - let panel = Panel::new(Widget::col(vec![ - Widget::row(vec![ - Line("15-minute neighborhood explorer") - .small_heading() - .draw(ctx), - Btn::close(ctx), - ]), - Text::from_all(vec![ - Line("Starting from: ").secondary(), - Line(&start.address), - ]) - .draw(ctx), - ])) - .aligned(HorizontalAlignment::Center, VerticalAlignment::Top) - .build(ctx); + let mut rows = Vec::new(); + rows.push(Widget::row(vec![ + Line("15-minute neighborhood explorer") + .small_heading() + .draw(ctx), + Btn::close(ctx), + ])); + let mut txt = Text::from_all(vec![ + Line("Starting from: ").secondary(), + Line(&start.address), + ]); + for (amenity, buildings) in isochrone.amenities_reachable.borrow() { + txt.add(Line(format!("{}: {}", amenity, buildings.len()))); + } + rows.push(txt.draw(ctx)); + + let panel = Panel::new(Widget::col(rows)) + .aligned(HorizontalAlignment::Center, VerticalAlignment::Top) + .build(ctx); // Draw a star on the start building. let highlight_start = GeomBatch::load_svg(ctx.prerender, "system/assets/tools/star.svg") @@ -60,7 +65,7 @@ impl Viewer { Box::new(Viewer { panel, highlight_start: ctx.upload(highlight_start), - isochrone: Isochrone::new(ctx, app, start.id), + isochrone, }) } }