Skip to content

Commit

Permalink
Make the isochrone use time, not distance, as a cost function. And
Browse files Browse the repository at this point in the history
floodfill using Dijkstra's, instead of computing loads of paths.  #393
  • Loading branch information
dabreegster committed Nov 19, 2020
1 parent 5a5aaae commit d33d051
Show file tree
Hide file tree
Showing 5 changed files with 50 additions and 32 deletions.
23 changes: 14 additions & 9 deletions game/src/devtools/fifteen_min/isochrone.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use geom::{Distance, Polygon};
use geom::{Duration, Polygon};
use map_model::{connectivity, BuildingID};
use widgetry::{Color, Drawable, EventCtx, GeomBatch};

Expand All @@ -19,8 +19,8 @@ impl Isochrone {
// 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 distances, but the contour crate needs f64, so
// just store the number of meters.
// 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,
Expand All @@ -36,27 +36,32 @@ impl Isochrone {
((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_meters();
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,
Distance::miles(0.5).inner_meters(),
Distance::miles(3.0).inner_meters(),
Distance::miles(6.0).inner_meters(),
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::BLACK.alpha(0.5),
Color::GREEN.alpha(0.5),
Color::BLUE.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()
Expand Down
34 changes: 16 additions & 18 deletions map_model/src/connectivity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ use std::collections::{HashMap, HashSet};

use petgraph::graphmap::DiGraphMap;

use geom::Distance;
use geom::Duration;

pub use crate::pathfind::driving_cost;
use crate::{BuildingID, LaneID, Map, PathConstraints, PathRequest};
pub use crate::pathfind::{build_graph_for_pedestrians, driving_cost, WalkingNode};
use crate::{BuildingID, LaneID, Map, PathConstraints};

/// Calculate the srongy 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;
Expand Down Expand Up @@ -46,23 +46,21 @@ pub fn find_scc(map: &Map, constraints: PathConstraints) -> (HashSet<LaneID>, Ha
(largest_group, disconnected)
}

/// Starting from one building, calculate the cost to all others.
// TODO Also take a PathConstraints and use different cost functions based on that -- maybe just
// total time?
pub fn all_costs_from(map: &Map, start: BuildingID) -> HashMap<BuildingID, Distance> {
/// Starting from one building, calculate the cost to all others. If a destination isn't reachable,
/// it won't be included in the results.
pub fn all_costs_from(map: &Map, start: BuildingID) -> HashMap<BuildingID, Duration> {
// TODO This is hardcoded to walking; take a PathConstraints.
let graph = build_graph_for_pedestrians(map);
let start = WalkingNode::closest(map.get_b(start).sidewalk_pos, map);
let cost_per_node = petgraph::algo::dijkstra(&graph, start, None, |(_, _, cost)| *cost);

// Assign every building a cost based on which end of the sidewalk it's closest to
// TODO We could try to get a little more accurate by accounting for the distance from that
// end of the sidewalk to the building
let mut results = HashMap::new();
let start = map.get_b(start).sidewalk_pos;
// TODO This is SO inefficient. Flood out and mark off buildings as we go. Granularity of lane
// makes more sense.
for b in map.all_buildings() {
if let Some(path) = map.pathfind(PathRequest {
start,
end: b.sidewalk_pos,
constraints: PathConstraints::Pedestrian,
}) {
// TODO Distance isn't an interesting thing to show at all, we want the path cost
// (probably in time)
results.insert(b.id, path.total_length());
if let Some(seconds) = cost_per_node.get(&WalkingNode::closest(b.sidewalk_pos, map)) {
results.insert(b.id, Duration::seconds(*seconds as f64));
}
}
results
Expand Down
23 changes: 18 additions & 5 deletions map_model/src/pathfind/dijkstra.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,24 @@ pub fn pathfind(req: PathRequest, map: &Map) -> Option<Path> {
return Some(Path::new(map, steps, req.end.dist_along(), Vec::new()));
}

let graph = build_graph_for_vehicles(map, req.constraints);
calc_path(graph, req, map)
}

pub fn build_graph_for_vehicles(
map: &Map,
constraints: PathConstraints,
) -> DiGraphMap<LaneID, TurnID> {
// TODO Handle zones.
let mut graph: DiGraphMap<LaneID, TurnID> = DiGraphMap::new();
for l in map.all_lanes() {
if req.constraints.can_use(l, map) {
for turn in map.get_turns_for(l.id, req.constraints) {
if constraints.can_use(l, map) {
for turn in map.get_turns_for(l.id, constraints) {
graph.add_edge(turn.id.src, turn.id.dst, turn.id);
}
}
}

calc_path(graph, req, map)
graph
}

pub fn pathfind_avoiding_lanes(
Expand Down Expand Up @@ -76,7 +83,8 @@ fn calc_path(graph: DiGraphMap<LaneID, TurnID>, req: PathRequest, map: &Map) ->
}

// TODO Not happy this works so differently
fn pathfind_walking(req: PathRequest, map: &Map) -> Option<Vec<WalkingNode>> {

pub fn build_graph_for_pedestrians(map: &Map) -> DiGraphMap<WalkingNode, usize> {
let mut graph: DiGraphMap<WalkingNode, usize> = DiGraphMap::new();
for l in map.all_lanes() {
if l.is_walkable() {
Expand All @@ -98,6 +106,11 @@ fn pathfind_walking(req: PathRequest, map: &Map) -> Option<Vec<WalkingNode>> {
}
}
}
graph
}

fn pathfind_walking(req: PathRequest, map: &Map) -> Option<Vec<WalkingNode>> {
let graph = build_graph_for_pedestrians(map);

let closest_start = WalkingNode::closest(req.start, map);
let closest_end = WalkingNode::closest(req.end, map);
Expand Down
1 change: 1 addition & 0 deletions map_model/src/pathfind/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use abstutil::Timer;
use geom::{Distance, PolyLine, EPSILON_DIST};

pub use self::ch::ContractionHierarchyPathfinder;
pub use self::dijkstra::build_graph_for_pedestrians;
pub use self::driving::driving_cost;
pub use self::walking::{walking_cost, WalkingNode};
use crate::{
Expand Down
1 change: 1 addition & 0 deletions map_model/src/pathfind/walking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,7 @@ fn transit_input_graph(
}
}

/// The cost is time in seconds, rounded to a usize
pub fn walking_cost(dist: Distance) -> usize {
let walking_speed = Speed::meters_per_second(1.34);
let time = dist / walking_speed;
Expand Down

0 comments on commit d33d051

Please sign in to comment.