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,
};
use crate::isochrone::{Isochrone, Options};
use crate::isochrone::{draw_isochrone, BorderIsochrone, Isochrone, Options};
use crate::viewer::{draw_star, HoverKey, HoverOnBuilding};
use crate::App;
@ -23,8 +23,15 @@ impl FindAmenity {
.map(|at| Choice::new(at.to_string(), at))
.collect(),
Box::new(move |at, ctx, app| {
let multi_isochrone = create_multi_isochrone(ctx, app, at, options);
return Transition::Replace(Results::new_state(ctx, app, multi_isochrone, at));
let multi_isochrone = create_multi_isochrone(ctx, app, at, options.clone());
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)
}
/// 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 {
draw: Drawable,
isochrone: Isochrone,
@ -59,6 +77,7 @@ impl Results {
ctx: &mut EventCtx,
app: &App,
isochrone: Isochrone,
border_isochrone: BorderIsochrone,
category: AmenityType,
) -> Box<dyn State<App>> {
let panel = Panel::new_builder(Widget::col(vec![
@ -77,11 +96,27 @@ impl Results {
(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)
.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 {
batch.append(draw_star(ctx, app.map.get_b(start)));
}

View File

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

View File

@ -1,11 +1,12 @@
use std::collections::{HashMap, HashSet};
use abstutil::MultiMap;
use connectivity::Spot;
use geom::{Duration, Polygon};
use map_gui::tools::Grid;
use map_model::{
connectivity, AmenityType, BuildingID, BuildingType, LaneType, Map, Path, PathConstraints,
PathRequest,
connectivity, AmenityType, BuildingID, BuildingType, IntersectionID, LaneType, Map, Path,
PathConstraints, PathRequest,
};
use widgetry::{Color, Drawable, EventCtx, GeomBatch};
@ -19,6 +20,10 @@ pub struct 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>,
/// Per category of amenity, what buildings have that?
@ -40,11 +45,7 @@ pub enum Options {
impl Options {
/// 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.
pub fn times_from_buildings(
self,
map: &Map,
starts: Vec<BuildingID>,
) -> HashMap<BuildingID, Duration> {
pub fn times_from(self, map: &Map, starts: Vec<Spot>) -> HashMap<BuildingID, Duration> {
match self {
Options::Walking(opts) => {
connectivity::all_walking_costs_from(map, starts, Duration::minutes(15), opts)
@ -66,9 +67,8 @@ impl Isochrone {
start: Vec<BuildingID>,
options: Options,
) -> Isochrone {
let time_to_reach_building = options
.clone()
.times_from_buildings(&app.map, start.clone());
let spot_starts = start.iter().map(|b_id| Spot::Building(*b_id)).collect();
let time_to_reach_building = options.clone().times_from(&app.map, spot_starts);
let mut amenities_reachable = MultiMap::new();
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 {
start,
options,
draw: Drawable::empty(ctx),
thresholds,
colors,
time_to_reach_building,
amenities_reachable,
population,
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
}
@ -133,73 +153,111 @@ impl Isochrone {
all_paths.min_by_key(|path| path.total_length())
}
}
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,
pub fn draw_isochrone(
app: &App,
time_to_reach_building: &HashMap<BuildingID, Duration>,
thresholds: &[f64],
colors: &[Color],
) -> 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,
);
// 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
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();
}
// 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 {
if let Ok(poly) = Polygon::from_geojson(&p) {
batch.push(color, poly.scale(resolution_m));
}
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_home::FindHome;
use crate::isochrone::{Isochrone, Options};
use crate::isochrone::{draw_isochrone, Isochrone, Options};
use crate::App;
/// This is the UI state for exploring the isochrone/walkshed from a single building.
@ -450,7 +450,12 @@ impl ExploreAmenities {
isochrone: &Isochrone,
category: AmenityType,
) -> 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])));
let mut entries = Vec::new();

View File

@ -1,5 +1,6 @@
// TODO Possibly these should be methods on Map.
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
use std::collections::{BinaryHeap, HashMap, HashSet};
@ -10,10 +11,18 @@ use geom::Duration;
pub use self::walking::{all_walking_costs_from, WalkingOptions};
use crate::pathfind::{build_graph_for_vehicles, zone_cost};
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;
#[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
/// 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
@ -51,12 +60,12 @@ pub fn find_scc(map: &Map, constraints: PathConstraints) -> (HashSet<LaneID>, Ha
(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
/// away.
pub fn all_vehicle_costs_from(
map: &Map,
starts: Vec<BuildingID>,
starts: Vec<Spot>,
time_limit: Duration,
constraints: PathConstraints,
) -> 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
// straightforward. In the common case it'll be fine, but some buildings are isolated from the
// graph by some sidewalks.
let mut bldg_to_road = HashMap::new();
for b in map.all_buildings() {
if constraints == PathConstraints::Car {
@ -78,12 +88,32 @@ pub fn all_vehicle_costs_from(
}
let mut queue: BinaryHeap<Item> = BinaryHeap::new();
for b in starts {
if let Some(start_road) = bldg_to_road.get(&b).cloned() {
queue.push(Item {
cost: Duration::ZERO,
node: start_road,
});
for spot in starts {
match spot {
Spot::Building(b_id) => {
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 crate::connectivity::Spot;
use crate::pathfind::{zone_cost, WalkingNode};
use crate::{BuildingID, LaneType, Map, PathConstraints, Traversable};
use crate::{BuildingID, Lane, LaneType, Map, PathConstraints, Traversable};
#[derive(Clone)]
pub struct WalkingOptions {
@ -64,24 +65,57 @@ impl Ord for Item {
/// the results will always be empty.
pub fn all_walking_costs_from(
map: &Map,
starts: Vec<BuildingID>,
starts: Vec<Spot>,
time_limit: Duration,
opts: WalkingOptions,
) -> HashMap<BuildingID, Duration> {
if !opts.allow_shoulders
&& starts
.iter()
.all(|b| map.get_l(map.get_b(*b).sidewalk()).lane_type == LaneType::Shoulder)
{
return HashMap::new();
let mut queue: BinaryHeap<Item> = BinaryHeap::new();
for spot in starts {
match spot {
Spot::Building(b_id) => {
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();
for b in starts {
queue.push(Item {
cost: Duration::ZERO,
node: WalkingNode::closest(map.get_b(b).sidewalk_pos, map),
});
if !opts.allow_shoulders {
let mut shoulder_endpoint = Vec::new();
for q in &queue {
if let WalkingNode::SidewalkEndpoint(dir_r, _) = q.node {
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();