Make it easier to explore a category of amenities. Highlight the buildings, move panels. #393

This commit is contained in:
Dustin Carlino 2020-12-26 13:31:24 -08:00
parent cff51a46ea
commit c3f211ccfd
2 changed files with 96 additions and 82 deletions

View File

@ -49,7 +49,6 @@ impl Isochrone {
PathConstraints::Bike,
),
};
let draw = draw_isochrone(app, &time_to_reach_building).upload(ctx);
let mut amenities_reachable = MultiMap::new();
let mut population = 0;
@ -81,15 +80,17 @@ impl Isochrone {
}
}
Isochrone {
let mut i = Isochrone {
start,
options,
draw,
draw: Drawable::empty(ctx),
time_to_reach_building,
amenities_reachable,
population,
onstreet_parking_spots,
}
};
i.draw = i.draw_isochrone(app).upload(ctx);
i
}
pub fn path_to(&self, map: &Map, to: BuildingID) -> Option<Path> {
@ -109,71 +110,71 @@ impl Isochrone {
)?;
map.pathfind(req).ok()
}
}
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.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.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,
pub fn draw_isochrone(&self, app: &App) -> 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.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,
);
// 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!(),
// Calculate the cost from the start building to every other building in the map
for (b, cost) in &self.time_to_reach_building {
// What grid cell does the building belong to?
let pt = app.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();
}
}
batch
// 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

@ -5,7 +5,7 @@
//! See https://github.com/dabreegster/abstreet/issues/393 for more context.
use abstutil::prettyprint_usize;
use geom::{Distance, Duration, Pt2D};
use geom::{Distance, Duration};
use map_gui::tools::{
amenity_type, nice_map_name, open_browser, CityPicker, ColorLegend, PopupMsg,
};
@ -14,7 +14,7 @@ use map_model::connectivity::WalkingOptions;
use map_model::{Building, BuildingID};
use widgetry::table::{Col, Filter, Table};
use widgetry::{
lctrl, Btn, Checkbox, Choice, Color, DrawBaselayer, Drawable, EventCtx, GeomBatch, GfxCtx,
lctrl, Btn, Checkbox, Choice, Color, Drawable, EventCtx, GeomBatch, GfxCtx,
HorizontalAlignment, Key, Line, Outcome, Panel, RewriteColor, State, Text, TextExt, Transition,
VerticalAlignment, Widget,
};
@ -43,12 +43,12 @@ impl Viewer {
let options = Options::Walking(WalkingOptions::default());
let start = app.map.get_b(start);
let isochrone = Isochrone::new(ctx, app, start.id, options);
let highlight_start = draw_star(ctx, start.polygon.center());
let highlight_start = draw_star(ctx, start);
let panel = build_panel(ctx, app, start, &isochrone);
Box::new(Viewer {
panel,
highlight_start: highlight_start,
highlight_start: ctx.upload(highlight_start),
isochrone,
hovering_on_bldg: Cached::new(),
})
@ -77,7 +77,8 @@ impl State<App> for Viewer {
if ctx.normal_left_click() {
let start = app.map.get_b(hover_id);
self.isochrone = Isochrone::new(ctx, app, start.id, self.isochrone.options.clone());
self.highlight_start = draw_star(ctx, start.polygon.center());
let star = draw_star(ctx, start);
self.highlight_start = ctx.upload(star);
self.panel = build_panel(ctx, app, start, &self.isochrone);
// Any previous hover is from the perspective of the old `highlight_start`.
// Remove it so we don't have a dotted line to the previous isochrone's origin
@ -210,12 +211,10 @@ fn options_from_controls(panel: &Panel) -> Options {
}
}
fn draw_star(ctx: &mut EventCtx, center: Pt2D) -> Drawable {
ctx.upload(
GeomBatch::load_svg(ctx, "system/assets/tools/star.svg")
.centered_on(center)
.color(RewriteColor::ChangeAll(Color::BLACK)),
)
fn draw_star(ctx: &mut EventCtx, b: &Building) -> GeomBatch {
GeomBatch::load_svg(ctx, "system/assets/tools/star.svg")
.centered_on(b.polygon.center())
.color(RewriteColor::ChangeAll(Color::BLACK))
}
fn build_panel(ctx: &mut EventCtx, app: &App, start: &Building, isochrone: &Isochrone) -> Panel {
@ -346,6 +345,7 @@ struct ExploreAmenities {
category: String,
table: Table<App, Entry, ()>,
panel: Panel,
draw: Drawable,
}
struct Entry {
@ -363,6 +363,9 @@ impl ExploreAmenities {
isochrone: &Isochrone,
category: &str,
) -> Box<dyn State<App>> {
let mut batch = isochrone.draw_isochrone(app);
batch.append(draw_star(ctx, app.map.get_b(isochrone.start)));
let mut entries = Vec::new();
for b in isochrone.amenities_reachable.get(category) {
let bldg = app.map.get_b(*b);
@ -375,6 +378,8 @@ impl ExploreAmenities {
address: bldg.address.clone(),
duration_away: isochrone.time_to_reach_building[&bldg.id],
});
// Highlight the matching buildings
batch.push(Color::RED, bldg.polygon.clone());
}
}
}
@ -410,12 +415,14 @@ impl ExploreAmenities {
]),
table.render(ctx, app),
]))
.aligned(HorizontalAlignment::Center, VerticalAlignment::TopInset)
.build(ctx);
Box::new(ExploreAmenities {
category: category.to_string(),
table,
panel,
draw: ctx.upload(batch),
})
}
@ -429,6 +436,7 @@ impl ExploreAmenities {
]),
self.table.render(ctx, app),
]))
.aligned(HorizontalAlignment::Center, VerticalAlignment::TopInset)
.build(ctx);
new.restore(ctx, &self.panel);
self.panel = new;
@ -463,11 +471,16 @@ impl State<App> for ExploreAmenities {
Transition::Keep
}
fn draw_baselayer(&self) -> DrawBaselayer {
DrawBaselayer::PreviousState
}
fn draw(&self, g: &mut GfxCtx, _: &App) {
fn draw(&self, g: &mut GfxCtx, app: &App) {
g.redraw(&self.draw);
self.panel.draw(g);
if let Some(x) = self
.panel
.currently_hovering()
.and_then(|x| x.split(":").next())
.and_then(|x| x.parse::<usize>().ok())
{
g.draw_polygon(Color::CYAN, app.map.get_b(BuildingID(x)).polygon.clone());
}
}
}