mirror of
https://github.com/a-b-street/abstreet.git
synced 2024-12-25 07:25:47 +03:00
Display a simple count of amenities reachable from a building. #393
This commit is contained in:
parent
bf9d34d1a3
commit
29da713903
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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.
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
@ -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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user