Display a simple count of amenities reachable from a building. #393

This commit is contained in:
Dustin Carlino 2020-11-21 11:11:57 -08:00
parent bf9d34d1a3
commit 29da713903
4 changed files with 110 additions and 82 deletions

View File

@ -54,7 +54,7 @@ where
self.map.len() self.map.len()
} }
pub(crate) fn raw_map(&self) -> &BTreeMap<K, BTreeSet<V>> { pub fn borrow(&self) -> &BTreeMap<K, BTreeSet<V>> {
&self.map &self.map
} }

View File

@ -105,8 +105,7 @@ pub fn serialize_multimap<
map: &MultiMap<K, V>, map: &MultiMap<K, V>,
s: S, s: S,
) -> Result<S::Ok, S::Error> { ) -> Result<S::Ok, S::Error> {
// TODO maybe need to sort to have deterministic output map.borrow().iter().collect::<Vec<(_, _)>>().serialize(s)
map.raw_map().iter().collect::<Vec<(_, _)>>().serialize(s)
} }
/// Deserializes a MultiMap. /// Deserializes a MultiMap.

View File

@ -1,87 +1,111 @@
use std::collections::HashMap;
use abstutil::MultiMap;
use geom::{Duration, Polygon}; use geom::{Duration, Polygon};
use map_model::{connectivity, BuildingID}; use map_model::{connectivity, BuildingID};
use widgetry::{Color, Drawable, EventCtx, GeomBatch}; use widgetry::{Color, Drawable, EventCtx, GeomBatch};
use crate::app::App; use crate::app::App;
use crate::common::Grid; use crate::common::Grid;
use crate::helpers::amenity_type;
/// Represents the area reachable from a single building. /// Represents the area reachable from a single building.
pub struct Isochrone { pub struct Isochrone {
/// Colored polygon contours, uploaded to the GPU and ready for drawing /// Colored polygon contours, uploaded to the GPU and ready for drawing
pub draw: Drawable, pub draw: Drawable,
/* TODO This is a good place to store the buildings within 5 mins, 10 mins, etc /// How far away is each building from the start?
* and maybe some summary of the types of amenities within these ranges */ pub time_to_reach_building: HashMap<BuildingID, Duration>,
/// Per category of amenity (defined by helpers::amenity_type), what buildings have that?
pub amenities_reachable: MultiMap<&'static str, BuildingID>,
} }
impl Isochrone { impl Isochrone {
pub fn new(ctx: &mut EventCtx, app: &App, start: BuildingID) -> 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 let time_to_reach_building =
// in a 2D grid of costs. Use a 100x100 meter resolution. connectivity::all_costs_from(&app.primary.map, start, Duration::minutes(15));
let bounds = app.primary.map.get_bounds(); let draw = draw_isochrone(app, &time_to_reach_building).upload(ctx);
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<f64> = 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 let mut amenities_reachable = MultiMap::new();
for (b, cost) in for b in time_to_reach_building.keys() {
connectivity::all_costs_from(&app.primary.map, start, Duration::minutes(15)) let bldg = app.primary.map.get_b(*b);
{ for amenity in &bldg.amenities {
// What grid cell does the building belong to? if let Some(category) = amenity_type(&amenity.amenity_type) {
let pt = app.primary.map.get_b(b).polygon.center(); amenities_reachable.insert(category, bldg.id);
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!(),
} }
} }
Isochrone { Isochrone {
draw: batch.upload(ctx), draw,
time_to_reach_building,
amenities_reachable,
} }
} }
} }
fn draw_isochrone(app: &App, time_to_reach_building: &HashMap<BuildingID, Duration>) -> 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<f64> = 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
}

View File

@ -35,22 +35,27 @@ impl Viewer {
pub fn new(ctx: &mut EventCtx, app: &App, start: BuildingID) -> Box<dyn State<App>> { pub fn new(ctx: &mut EventCtx, app: &App, start: BuildingID) -> Box<dyn State<App>> {
let start = app.primary.map.get_b(start); let start = app.primary.map.get_b(start);
let isochrone = Isochrone::new(ctx, app, start.id);
let panel = Panel::new(Widget::col(vec![ let mut rows = Vec::new();
Widget::row(vec![ rows.push(Widget::row(vec![
Line("15-minute neighborhood explorer") Line("15-minute neighborhood explorer")
.small_heading() .small_heading()
.draw(ctx), .draw(ctx),
Btn::close(ctx), Btn::close(ctx),
]), ]));
Text::from_all(vec![ let mut txt = Text::from_all(vec![
Line("Starting from: ").secondary(), Line("Starting from: ").secondary(),
Line(&start.address), Line(&start.address),
]) ]);
.draw(ctx), for (amenity, buildings) in isochrone.amenities_reachable.borrow() {
])) txt.add(Line(format!("{}: {}", amenity, buildings.len())));
.aligned(HorizontalAlignment::Center, VerticalAlignment::Top) }
.build(ctx); 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. // Draw a star on the start building.
let highlight_start = GeomBatch::load_svg(ctx.prerender, "system/assets/tools/star.svg") let highlight_start = GeomBatch::load_svg(ctx.prerender, "system/assets/tools/star.svg")
@ -60,7 +65,7 @@ impl Viewer {
Box::new(Viewer { Box::new(Viewer {
panel, panel,
highlight_start: ctx.upload(highlight_start), highlight_start: ctx.upload(highlight_start),
isochrone: Isochrone::new(ctx, app, start.id), isochrone,
}) })
} }
} }