mirror of
https://github.com/a-b-street/abstreet.git
synced 2024-11-24 09:24:26 +03:00
Make it easier to explore a category of amenities. Highlight the buildings, move panels. #393
This commit is contained in:
parent
cff51a46ea
commit
c3f211ccfd
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user