Generalized connectivity to use Spot and added BorderIsochrone (#668)

This commit is contained in:
Trevor Nederlof 2021-06-11 13:06:53 -04:00 committed by GitHub
parent 972f5b0187
commit 4e150dcab0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 266 additions and 105 deletions

View File

@ -6,7 +6,7 @@ use widgetry::{
SimpleState, State, TextExt, Transition, VerticalAlignment, Widget, SimpleState, State, TextExt, Transition, VerticalAlignment, Widget,
}; };
use crate::isochrone::{Isochrone, Options}; use crate::isochrone::{draw_isochrone, BorderIsochrone, Isochrone, Options};
use crate::viewer::{draw_star, HoverKey, HoverOnBuilding}; use crate::viewer::{draw_star, HoverKey, HoverOnBuilding};
use crate::App; use crate::App;
@ -23,8 +23,15 @@ impl FindAmenity {
.map(|at| Choice::new(at.to_string(), at)) .map(|at| Choice::new(at.to_string(), at))
.collect(), .collect(),
Box::new(move |at, ctx, app| { Box::new(move |at, ctx, app| {
let multi_isochrone = create_multi_isochrone(ctx, app, at, options); let multi_isochrone = create_multi_isochrone(ctx, app, at, options.clone());
return Transition::Replace(Results::new_state(ctx, app, multi_isochrone, at)); let border_isochrone = create_border_isochrone(ctx, app, options);
return Transition::Replace(Results::new_state(
ctx,
app,
multi_isochrone,
border_isochrone,
at,
));
}), }),
) )
} }
@ -48,6 +55,17 @@ fn create_multi_isochrone(
Isochrone::new(ctx, app, stores, options) Isochrone::new(ctx, app, stores, options)
} }
/// Draw an isochrone from every intersection border
fn create_border_isochrone(ctx: &mut EventCtx, app: &App, options: Options) -> BorderIsochrone {
let mut all_intersections = Vec::new();
for i in app.map.all_intersections() {
if i.is_border() {
all_intersections.push(i.id);
}
}
BorderIsochrone::new(ctx, app, all_intersections, options)
}
struct Results { struct Results {
draw: Drawable, draw: Drawable,
isochrone: Isochrone, isochrone: Isochrone,
@ -59,6 +77,7 @@ impl Results {
ctx: &mut EventCtx, ctx: &mut EventCtx,
app: &App, app: &App,
isochrone: Isochrone, isochrone: Isochrone,
border_isochrone: BorderIsochrone,
category: AmenityType, category: AmenityType,
) -> Box<dyn State<App>> { ) -> Box<dyn State<App>> {
let panel = Panel::new_builder(Widget::col(vec![ let panel = Panel::new_builder(Widget::col(vec![
@ -77,11 +96,27 @@ impl Results {
(Color::RED, "15 mins"), (Color::RED, "15 mins"),
], ],
), ),
ColorLegend::row(
ctx,
Color::rgb(0, 0, 0).alpha(0.3),
"< 15 mins from border (amenity could exist off map)",
),
])) ]))
.aligned(HorizontalAlignment::RightInset, VerticalAlignment::TopInset) .aligned(HorizontalAlignment::RightInset, VerticalAlignment::TopInset)
.build(ctx); .build(ctx);
let mut batch = isochrone.draw_isochrone(app); let mut batch = draw_isochrone(
app,
&border_isochrone.time_to_reach_building,
&border_isochrone.thresholds,
&border_isochrone.colors,
);
batch.append(draw_isochrone(
app,
&isochrone.time_to_reach_building,
&isochrone.thresholds,
&isochrone.colors,
));
for &start in &isochrone.start { for &start in &isochrone.start {
batch.append(draw_star(ctx, app.map.get_b(start))); batch.append(draw_star(ctx, app.map.get_b(start)));
} }

View File

@ -1,8 +1,10 @@
use std::collections::HashMap; use std::collections::HashMap;
use crate::App;
use abstutil::{prettyprint_usize, Counter, Timer}; use abstutil::{prettyprint_usize, Counter, Timer};
use geom::Percent; use geom::Percent;
use map_gui::tools::PopupMsg; use map_gui::tools::PopupMsg;
use map_model::connectivity::Spot;
use map_model::{AmenityType, BuildingID}; use map_model::{AmenityType, BuildingID};
use widgetry::{ use widgetry::{
Color, Drawable, EventCtx, GeomBatch, GfxCtx, HorizontalAlignment, Key, Line, Panel, Color, Drawable, EventCtx, GeomBatch, GfxCtx, HorizontalAlignment, Key, Line, Panel,
@ -10,7 +12,6 @@ use widgetry::{
}; };
use crate::isochrone::Options; use crate::isochrone::Options;
use crate::App;
/// Ask what types of amenities are necessary to be within a walkshed, then rank every house with /// Ask what types of amenities are necessary to be within a walkshed, then rank every house with
/// how many of those needs are satisfied. /// how many of those needs are satisfied.
@ -98,12 +99,10 @@ fn score_houses(
let mut stores = Vec::new(); let mut stores = Vec::new();
for b in map.all_buildings() { for b in map.all_buildings() {
if b.has_amenity(category) { if b.has_amenity(category) {
stores.push(b.id); stores.push(Spot::Building(b.id));
} }
} }
options.clone().times_from(map, stores)
// Then find all buildings reachable from any of those starting points
options.clone().times_from_buildings(map, stores)
}) { }) {
for (b, _) in times { for (b, _) in times {
satisfied_per_bldg.inc(b); satisfied_per_bldg.inc(b);

View File

@ -1,11 +1,12 @@
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use abstutil::MultiMap; use abstutil::MultiMap;
use connectivity::Spot;
use geom::{Duration, Polygon}; use geom::{Duration, Polygon};
use map_gui::tools::Grid; use map_gui::tools::Grid;
use map_model::{ use map_model::{
connectivity, AmenityType, BuildingID, BuildingType, LaneType, Map, Path, PathConstraints, connectivity, AmenityType, BuildingID, BuildingType, IntersectionID, LaneType, Map, Path,
PathRequest, PathConstraints, PathRequest,
}; };
use widgetry::{Color, Drawable, EventCtx, GeomBatch}; use widgetry::{Color, Drawable, EventCtx, GeomBatch};
@ -19,6 +20,10 @@ pub struct Isochrone {
pub options: Options, pub options: Options,
/// 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,
/// Thresholds used to draw the isochrone
pub thresholds: Vec<f64>,
/// Colors used to draw the isochrone
pub colors: Vec<Color>,
/// How far away is each building from the start? /// How far away is each building from the start?
pub time_to_reach_building: HashMap<BuildingID, Duration>, pub time_to_reach_building: HashMap<BuildingID, Duration>,
/// Per category of amenity, what buildings have that? /// Per category of amenity, what buildings have that?
@ -40,11 +45,7 @@ pub enum Options {
impl Options { impl Options {
/// Calculate the quickest time to reach buildings across the map from any of the starting /// Calculate the quickest time to reach buildings across the map from any of the starting
/// points, subject to the walking/biking settings configured in these Options. /// points, subject to the walking/biking settings configured in these Options.
pub fn times_from_buildings( pub fn times_from(self, map: &Map, starts: Vec<Spot>) -> HashMap<BuildingID, Duration> {
self,
map: &Map,
starts: Vec<BuildingID>,
) -> HashMap<BuildingID, Duration> {
match self { match self {
Options::Walking(opts) => { Options::Walking(opts) => {
connectivity::all_walking_costs_from(map, starts, Duration::minutes(15), opts) connectivity::all_walking_costs_from(map, starts, Duration::minutes(15), opts)
@ -66,9 +67,8 @@ impl Isochrone {
start: Vec<BuildingID>, start: Vec<BuildingID>,
options: Options, options: Options,
) -> Isochrone { ) -> Isochrone {
let time_to_reach_building = options let spot_starts = start.iter().map(|b_id| Spot::Building(*b_id)).collect();
.clone() let time_to_reach_building = options.clone().times_from(&app.map, spot_starts);
.times_from_buildings(&app.map, start.clone());
let mut amenities_reachable = MultiMap::new(); let mut amenities_reachable = MultiMap::new();
let mut population = 0; let mut population = 0;
@ -101,16 +101,36 @@ impl Isochrone {
} }
} }
// 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 mut i = Isochrone { let mut i = Isochrone {
start, start,
options, options,
draw: Drawable::empty(ctx), draw: Drawable::empty(ctx),
thresholds,
colors,
time_to_reach_building, time_to_reach_building,
amenities_reachable, amenities_reachable,
population, population,
onstreet_parking_spots, onstreet_parking_spots,
}; };
i.draw = i.draw_isochrone(app).upload(ctx);
i.draw =
draw_isochrone(app, &i.time_to_reach_building, &i.thresholds, &i.colors).upload(ctx);
i i
} }
@ -133,73 +153,111 @@ impl Isochrone {
all_paths.min_by_key(|path| path.total_length()) all_paths.min_by_key(|path| path.total_length())
} }
}
pub fn draw_isochrone(&self, app: &App) -> GeomBatch { pub fn draw_isochrone(
// To generate the polygons covering areas between 0-5 mins, 5-10 mins, etc, we have to feed app: &App,
// in a 2D grid of costs. Use a 100x100 meter resolution. time_to_reach_building: &HashMap<BuildingID, Duration>,
let bounds = app.map.get_bounds(); thresholds: &[f64],
let resolution_m = 100.0; colors: &[Color],
// The costs we're storing are currenly durations, but the contour crate needs f64, so ) -> GeomBatch {
// just store the number of seconds. // To generate the polygons covering areas between 0-5 mins, 5-10 mins, etc, we have to feed
let mut grid: Grid<f64> = Grid::new( // in a 2D grid of costs. Use a 100x100 meter resolution.
(bounds.width() / resolution_m).ceil() as usize, let bounds = app.map.get_bounds();
(bounds.height() / resolution_m).ceil() as usize, let resolution_m = 100.0;
0.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,
); );
// Don't add! If two buildings map to the same cell, we should pick a finer resolution.
grid.data[idx] = cost.inner_seconds();
}
// Calculate the cost from the start building to every other building in the map let smooth = false;
for (b, cost) in &self.time_to_reach_building { let c = contour::ContourBuilder::new(grid.width as u32, grid.height as u32, smooth);
// What grid cell does the building belong to? let mut batch = GeomBatch::new();
let pt = app.map.get_b(*b).polygon.center(); // The last feature returned will be larger than the last threshold value. We don't want to
let idx = grid.idx( // display that at all. zip() will omit this last pair, since colors.len() ==
((pt.x() - bounds.min_x) / resolution_m) as usize, // thresholds.len() - 1.
((pt.y() - bounds.min_y) / resolution_m) as usize, //
); // TODO Actually, this still isn't working. I think each polygon is everything > the
// Don't add! If two buildings map to the same cell, we should pick a finer resolution. // threshold, not everything between two thresholds?
grid.data[idx] = cost.inner_seconds(); for (feature, color) in c
} .contours(&grid.data, &thresholds)
.unwrap()
// Generate polygons covering the contour line where the cost in the grid crosses these .into_iter()
// threshold values. .zip(colors)
let thresholds = vec![ {
0.1, match feature.geometry.unwrap().value {
Duration::minutes(5).inner_seconds(), geojson::Value::MultiPolygon(polygons) => {
Duration::minutes(10).inner_seconds(), for p in polygons {
Duration::minutes(15).inner_seconds(), if let Ok(poly) = Polygon::from_geojson(&p) {
]; batch.push(*color, poly.scale(resolution_m));
// 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 {
if let Ok(poly) = Polygon::from_geojson(&p) {
batch.push(color, poly.scale(resolution_m));
}
} }
} }
_ => unreachable!(),
} }
_ => unreachable!(),
} }
}
batch batch
}
/// Represents the area reachable from all intersections on the map border
pub struct BorderIsochrone {
/// The center of the isochrone (can be multiple points)
pub start: Vec<IntersectionID>,
/// The options used to generate this isochrone
pub options: Options,
/// Colored polygon contours, uploaded to the GPU and ready for drawing
pub draw: Drawable,
/// Thresholds used to draw the isochrone
pub thresholds: Vec<f64>,
/// Colors used to draw the isochrone
pub colors: Vec<Color>,
/// How far away is each building from the start?
pub time_to_reach_building: HashMap<BuildingID, Duration>,
}
impl BorderIsochrone {
pub fn new(
ctx: &mut EventCtx,
app: &App,
start: Vec<IntersectionID>,
options: Options,
) -> BorderIsochrone {
let spot_starts = start.iter().map(|i_id| Spot::Border(*i_id)).collect();
let time_to_reach_building = options.clone().times_from(&app.map, spot_starts);
// Generate a single polygon showing 15 minutes from the border
let thresholds = vec![0.1, Duration::minutes(15).inner_seconds()];
// Use one color for the entire polygon
let colors = vec![Color::rgb(0, 0, 0).alpha(0.3)];
let mut i = BorderIsochrone {
start,
options,
draw: Drawable::empty(ctx),
thresholds,
colors,
time_to_reach_building,
};
i.draw =
draw_isochrone(app, &i.time_to_reach_building, &i.thresholds, &i.colors).upload(ctx);
i
} }
} }

View File

@ -21,7 +21,7 @@ use widgetry::{
use crate::find_amenities::FindAmenity; use crate::find_amenities::FindAmenity;
use crate::find_home::FindHome; use crate::find_home::FindHome;
use crate::isochrone::{Isochrone, Options}; use crate::isochrone::{draw_isochrone, Isochrone, Options};
use crate::App; use crate::App;
/// This is the UI state for exploring the isochrone/walkshed from a single building. /// This is the UI state for exploring the isochrone/walkshed from a single building.
@ -450,7 +450,12 @@ impl ExploreAmenities {
isochrone: &Isochrone, isochrone: &Isochrone,
category: AmenityType, category: AmenityType,
) -> Box<dyn State<App>> { ) -> Box<dyn State<App>> {
let mut batch = isochrone.draw_isochrone(app); let mut batch = draw_isochrone(
app,
&isochrone.time_to_reach_building,
&isochrone.thresholds,
&isochrone.colors,
);
batch.append(draw_star(ctx, app.map.get_b(isochrone.start[0]))); batch.append(draw_star(ctx, app.map.get_b(isochrone.start[0])));
let mut entries = Vec::new(); let mut entries = Vec::new();

View File

@ -1,5 +1,6 @@
// TODO Possibly these should be methods on Map. // TODO Possibly these should be methods on Map.
use serde::{Deserialize, Serialize};
use std::cmp::Ordering; use std::cmp::Ordering;
use std::collections::{BinaryHeap, HashMap, HashSet}; use std::collections::{BinaryHeap, HashMap, HashSet};
@ -10,10 +11,18 @@ use geom::Duration;
pub use self::walking::{all_walking_costs_from, WalkingOptions}; pub use self::walking::{all_walking_costs_from, WalkingOptions};
use crate::pathfind::{build_graph_for_vehicles, zone_cost}; use crate::pathfind::{build_graph_for_vehicles, zone_cost};
pub use crate::pathfind::{vehicle_cost, WalkingNode}; pub use crate::pathfind::{vehicle_cost, WalkingNode};
use crate::{BuildingID, DirectedRoadID, LaneID, Map, PathConstraints, PathRequest}; use crate::{
BuildingID, DirectedRoadID, IntersectionID, LaneID, Map, PathConstraints, PathRequest,
};
mod walking; mod walking;
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum Spot {
Building(BuildingID),
Border(IntersectionID),
}
/// Calculate the strongly connected components (SCC) of the part of the map accessible by /// Calculate the strongly connected components (SCC) of the part of the map accessible by
/// constraints (ie, the graph of sidewalks or driving+bike lanes). The largest component is the /// constraints (ie, the graph of sidewalks or driving+bike lanes). The largest component is the
/// "main" graph; the rest is disconnected. Returns (lanes in the largest "main" component, all /// "main" graph; the rest is disconnected. Returns (lanes in the largest "main" component, all
@ -51,12 +60,12 @@ pub fn find_scc(map: &Map, constraints: PathConstraints) -> (HashSet<LaneID>, Ha
(largest_group, disconnected) (largest_group, disconnected)
} }
/// Starting from some initial buildings, calculate the cost to all others. If a destination isn't /// Starting from some initial spot, calculate the cost to all buildings. If a destination isn't
/// reachable, it won't be included in the results. Ignore results greater than the time_limit /// reachable, it won't be included in the results. Ignore results greater than the time_limit
/// away. /// away.
pub fn all_vehicle_costs_from( pub fn all_vehicle_costs_from(
map: &Map, map: &Map,
starts: Vec<BuildingID>, starts: Vec<Spot>,
time_limit: Duration, time_limit: Duration,
constraints: PathConstraints, constraints: PathConstraints,
) -> HashMap<BuildingID, Duration> { ) -> HashMap<BuildingID, Duration> {
@ -64,6 +73,7 @@ pub fn all_vehicle_costs_from(
// TODO We have a graph of DirectedRoadIDs, but mapping a building to one isn't // TODO We have a graph of DirectedRoadIDs, but mapping a building to one isn't
// straightforward. In the common case it'll be fine, but some buildings are isolated from the // straightforward. In the common case it'll be fine, but some buildings are isolated from the
// graph by some sidewalks. // graph by some sidewalks.
let mut bldg_to_road = HashMap::new(); let mut bldg_to_road = HashMap::new();
for b in map.all_buildings() { for b in map.all_buildings() {
if constraints == PathConstraints::Car { if constraints == PathConstraints::Car {
@ -78,12 +88,32 @@ pub fn all_vehicle_costs_from(
} }
let mut queue: BinaryHeap<Item> = BinaryHeap::new(); let mut queue: BinaryHeap<Item> = BinaryHeap::new();
for b in starts {
if let Some(start_road) = bldg_to_road.get(&b).cloned() { for spot in starts {
queue.push(Item { match spot {
cost: Duration::ZERO, Spot::Building(b_id) => {
node: start_road, if let Some(start_road) = bldg_to_road.get(&b_id).cloned() {
}); queue.push(Item {
cost: Duration::ZERO,
node: start_road,
});
}
}
Spot::Border(i_id) => {
let intersection = map.get_i(i_id);
let incoming_lanes = intersection.get_incoming_lanes(map, constraints);
let mut outgoing_lanes = intersection.get_outgoing_lanes(map, constraints);
let mut all_lanes = incoming_lanes;
all_lanes.append(&mut outgoing_lanes);
for l_id in all_lanes {
queue.push(Item {
cost: Duration::ZERO,
node: map.get_l(l_id).get_directed_parent(),
});
}
}
} }
} }

View File

@ -3,8 +3,9 @@ use std::collections::{BinaryHeap, HashMap};
use geom::{Duration, Speed}; use geom::{Duration, Speed};
use crate::connectivity::Spot;
use crate::pathfind::{zone_cost, WalkingNode}; use crate::pathfind::{zone_cost, WalkingNode};
use crate::{BuildingID, LaneType, Map, PathConstraints, Traversable}; use crate::{BuildingID, Lane, LaneType, Map, PathConstraints, Traversable};
#[derive(Clone)] #[derive(Clone)]
pub struct WalkingOptions { pub struct WalkingOptions {
@ -64,24 +65,57 @@ impl Ord for Item {
/// the results will always be empty. /// the results will always be empty.
pub fn all_walking_costs_from( pub fn all_walking_costs_from(
map: &Map, map: &Map,
starts: Vec<BuildingID>, starts: Vec<Spot>,
time_limit: Duration, time_limit: Duration,
opts: WalkingOptions, opts: WalkingOptions,
) -> HashMap<BuildingID, Duration> { ) -> HashMap<BuildingID, Duration> {
if !opts.allow_shoulders let mut queue: BinaryHeap<Item> = BinaryHeap::new();
&& starts
.iter() for spot in starts {
.all(|b| map.get_l(map.get_b(*b).sidewalk()).lane_type == LaneType::Shoulder) match spot {
{ Spot::Building(b_id) => {
return HashMap::new(); queue.push(Item {
cost: Duration::ZERO,
node: WalkingNode::closest(map.get_b(b_id).sidewalk_pos, map),
});
}
Spot::Border(i_id) => {
let intersection = map.get_i(i_id);
let incoming_lanes = intersection.incoming_lanes.clone();
let mut outgoing_lanes = intersection.outgoing_lanes.clone();
let mut all_lanes = incoming_lanes;
all_lanes.append(&mut outgoing_lanes);
let walkable_lanes: Vec<&Lane> = all_lanes
.into_iter()
.map(|l_id| map.get_l(l_id))
.filter(|l| l.is_walkable())
.collect();
for lane in walkable_lanes {
queue.push(Item {
cost: Duration::ZERO,
node: WalkingNode::SidewalkEndpoint(
lane.get_directed_parent(),
lane.src_i == i_id,
),
});
}
}
}
} }
let mut queue: BinaryHeap<Item> = BinaryHeap::new(); if !opts.allow_shoulders {
for b in starts { let mut shoulder_endpoint = Vec::new();
queue.push(Item { for q in &queue {
cost: Duration::ZERO, if let WalkingNode::SidewalkEndpoint(dir_r, _) = q.node {
node: WalkingNode::closest(map.get_b(b).sidewalk_pos, map), let lanes = &map.get_r(dir_r.id).lanes_ltr;
}); for (_, _, lane_type) in lanes {
shoulder_endpoint.push(lane_type == &LaneType::Shoulder)
}
}
}
if shoulder_endpoint.into_iter().all(|x| x) {
return HashMap::new();
}
} }
let mut cost_per_node: HashMap<WalkingNode, Duration> = HashMap::new(); let mut cost_per_node: HashMap<WalkingNode, Duration> = HashMap::new();