Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve the 15m isochrone view, by coloring roads without any buildings #1075

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/fifteen_min/src/amenities_details.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ impl ExploreAmenitiesDetails {
let mut batch = draw_isochrone(
&app.map,
&isochrone.time_to_reach_building,
&isochrone.time_to_reach_empty_road,
&isochrone.thresholds,
&isochrone.colors,
);
Expand Down
2 changes: 2 additions & 0 deletions apps/fifteen_min/src/from_amenity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ impl FromAmenity {
batch.append(draw_isochrone(
&app.map,
&border_isochrone.time_to_reach_building,
&border_isochrone.time_to_reach_empty_road,
&border_isochrone.thresholds,
&border_isochrone.colors,
));
Expand All @@ -71,6 +72,7 @@ impl FromAmenity {
batch.append(draw_isochrone(
&app.map,
&isochrone.time_to_reach_building,
&isochrone.time_to_reach_empty_road,
&isochrone.thresholds,
&isochrone.colors,
));
Expand Down
26 changes: 20 additions & 6 deletions apps/fifteen_min/src/isochrone.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use geom::Duration;
use map_gui::tools::draw_isochrone;
use map_model::{
connectivity, AmenityType, BuildingID, BuildingType, IntersectionID, LaneType, Map, Path,
PathConstraints, PathRequest,
PathConstraints, PathRequest, RoadID,
};
use widgetry::mapspace::{ToggleZoomed, ToggleZoomedBuilder};
use widgetry::{Color, EventCtx};
Expand All @@ -27,6 +27,7 @@ pub struct Isochrone {
pub colors: Vec<Color>,
/// How far away is each building from the start?
pub time_to_reach_building: HashMap<BuildingID, Duration>,
pub time_to_reach_empty_road: HashMap<RoadID, Duration>,
/// Per category of amenity, what buildings have that?
pub amenities_reachable: MultiMap<AmenityType, BuildingID>,
/// How many people live in the returned area, according to estimates included in the map (from
Expand Down Expand Up @@ -60,9 +61,14 @@ pub enum MovementOptions {
}

impl MovementOptions {
/// 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(self, map: &Map, starts: Vec<Spot>) -> HashMap<BuildingID, Duration> {
/// Calculate the quickest time to reach buildings (and roads without buildings) across the map
/// from any of the starting points, subject to the walking/biking settings configured in these
/// Options.
pub fn times_from(
self,
map: &Map,
starts: Vec<Spot>,
) -> (HashMap<BuildingID, Duration>, HashMap<RoadID, Duration>) {
match self {
MovementOptions::Walking(opts) => {
connectivity::all_walking_costs_from(map, starts, Duration::minutes(15), opts)
Expand All @@ -85,10 +91,12 @@ impl Isochrone {
options: Options,
) -> Isochrone {
let spot_starts = start.iter().map(|b_id| Spot::Building(*b_id)).collect();
let time_to_reach_building = options.movement.clone().times_from(&app.map, spot_starts);
let (time_to_reach_building, time_to_reach_empty_road) =
options.movement.clone().times_from(&app.map, spot_starts);

let mut amenities_reachable = MultiMap::new();
let mut population = 0;
// TODO We could return all reachable roads and simplify this
let mut all_roads = HashSet::new();
for b in time_to_reach_building.keys() {
let bldg = app.map.get_b(*b);
Expand Down Expand Up @@ -133,6 +141,7 @@ impl Isochrone {
thresholds,
colors,
time_to_reach_building,
time_to_reach_empty_road,
amenities_reachable,
population,
onstreet_parking_spots,
Expand All @@ -141,6 +150,7 @@ impl Isochrone {
i.draw = ToggleZoomedBuilder::from(draw_isochrone(
&app.map,
&i.time_to_reach_building,
&i.time_to_reach_empty_road,
&i.thresholds,
&i.colors,
))
Expand Down Expand Up @@ -182,6 +192,7 @@ pub struct BorderIsochrone {
pub colors: Vec<Color>,
/// How far away is each building from the start?
pub time_to_reach_building: HashMap<BuildingID, Duration>,
pub time_to_reach_empty_road: HashMap<RoadID, Duration>,
}

impl BorderIsochrone {
Expand All @@ -192,7 +203,8 @@ impl BorderIsochrone {
options: Options,
) -> BorderIsochrone {
let spot_starts = start.iter().map(|i_id| Spot::Border(*i_id)).collect();
let time_to_reach_building = options.movement.clone().times_from(&app.map, spot_starts);
let (time_to_reach_building, time_to_reach_empty_road) =
options.movement.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()];
Expand All @@ -207,11 +219,13 @@ impl BorderIsochrone {
thresholds,
colors,
time_to_reach_building,
time_to_reach_empty_road,
};

i.draw = ToggleZoomedBuilder::from(draw_isochrone(
&app.map,
&i.time_to_reach_building,
&i.time_to_reach_empty_road,
&i.thresholds,
&i.colors,
))
Expand Down
2 changes: 1 addition & 1 deletion apps/fifteen_min/src/score_homes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ fn score_houses_by_one_match(
(
category,
stores,
movement_opts.clone().times_from(map, spots),
movement_opts.clone().times_from(map, spots).0,
)
})
{
Expand Down
21 changes: 19 additions & 2 deletions map_gui/src/tools/heatmap.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::collections::HashMap;

use geom::{Bounds, Duration, Histogram, Polygon, Pt2D, Statistic};
use map_model::{BuildingID, Map};
use geom::{Bounds, Distance, Duration, Histogram, Polygon, Pt2D, Statistic};
use map_model::{BuildingID, Map, RoadID};
use widgetry::tools::{ColorLegend, ColorScale};
use widgetry::{
Choice, Color, EventCtx, GeomBatch, Panel, RoundedF64, Spinner, TextExt, Toggle, Widget,
Expand Down Expand Up @@ -295,6 +295,7 @@ impl<T: Copy> Grid<T> {
pub fn draw_isochrone(
map: &Map,
time_to_reach_building: &HashMap<BuildingID, Duration>,
time_to_reach_empty_road: &HashMap<RoadID, Duration>,
thresholds: &[f64],
colors: &[Color],
) -> GeomBatch {
Expand All @@ -321,6 +322,22 @@ pub fn draw_isochrone(
grid.data[idx] = cost.inner_seconds();
}

for (r, cost) in time_to_reach_empty_road {
// Walk along the road in half-steps of our resolution
let buffer_ends = Distance::ZERO;
for (pt, _) in map
.get_r(*r)
.center_pts
.step_along(Distance::meters(resolution_m / 2.0), buffer_ends)
{
let idx = grid.idx(
((pt.x() - bounds.min_x) / resolution_m) as usize,
((pt.y() - bounds.min_y) / resolution_m) as usize,
);
grid.data[idx] = cost.inner_seconds();
}
}

let smooth = false;
let contour_builder =
contour::ContourBuilder::new(grid.width as u32, grid.height as u32, smooth);
Expand Down
67 changes: 45 additions & 22 deletions map_model/src/connectivity/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// TODO Possibly these should be methods on Map.

use abstutil::MultiMap;
use serde::{Deserialize, Serialize};
use std::collections::{BinaryHeap, HashMap, HashSet};

Expand All @@ -10,7 +11,9 @@ use geom::Duration;

pub use self::walking::{all_walking_costs_from, WalkingOptions};
pub use crate::pathfind::{vehicle_cost, WalkingNode};
use crate::{BuildingID, DirectedRoadID, IntersectionID, LaneID, Map, PathConstraints};
use crate::{
Building, BuildingID, DirectedRoadID, IntersectionID, LaneID, Map, PathConstraints, RoadID,
};

mod walking;

Expand Down Expand Up @@ -57,30 +60,42 @@ pub fn find_scc(map: &Map, constraints: PathConstraints) -> (HashSet<LaneID>, Ha
(largest_group, disconnected)
}

fn bldg_to_dir_road(
map: &Map,
b: &Building,
constraints: PathConstraints,
) -> Option<DirectedRoadID> {
let pos = if constraints == PathConstraints::Car {
b.driving_connection(map)?.0
} else if constraints == PathConstraints::Bike {
b.biking_connection(map)?.0
} else {
return None;
};
Some(map.get_l(pos.lane()).get_directed_parent())
}

/// 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.
///
/// Costs for roads will only be filled out for roads with no buildings along them. The cost will
/// be the same for the entire road, which may be misleading for long roads.
pub fn all_vehicle_costs_from(
map: &Map,
starts: Vec<Spot>,
time_limit: Duration,
constraints: PathConstraints,
) -> HashMap<BuildingID, Duration> {
) -> (HashMap<BuildingID, Duration>, HashMap<RoadID, Duration>) {
assert!(constraints != PathConstraints::Pedestrian);
// 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();
let mut dir_road_to_bldgs = MultiMap::new();
for b in map.all_buildings() {
if constraints == PathConstraints::Car {
if let Some((pos, _)) = b.driving_connection(map) {
bldg_to_road.insert(b.id, map.get_l(pos.lane()).get_directed_parent());
}
} else if constraints == PathConstraints::Bike {
if let Some((pos, _)) = b.biking_connection(map) {
bldg_to_road.insert(b.id, map.get_l(pos.lane()).get_directed_parent());
}
if let Some(dr) = bldg_to_dir_road(map, b, constraints) {
dir_road_to_bldgs.insert(dr, b.id);
}
}

Expand All @@ -89,7 +104,7 @@ pub fn all_vehicle_costs_from(
for spot in starts {
match spot {
Spot::Building(b_id) => {
if let Some(start_road) = bldg_to_road.get(&b_id).cloned() {
if let Some(start_road) = bldg_to_dir_road(map, map.get_b(b_id), constraints) {
queue.push(PriorityQueueItem {
cost: Duration::ZERO,
value: start_road,
Expand Down Expand Up @@ -120,15 +135,29 @@ pub fn all_vehicle_costs_from(
}
}

let mut cost_per_node: HashMap<DirectedRoadID, Duration> = HashMap::new();
let mut visited_nodes = HashSet::new();
let mut bldg_results = HashMap::new();
let mut road_results = HashMap::new();

while let Some(current) = queue.pop() {
if cost_per_node.contains_key(&current.value) {
if visited_nodes.contains(&current.value) {
continue;
}
if current.cost > time_limit {
continue;
}
cost_per_node.insert(current.value, current.cost);
visited_nodes.insert(current.value);

let mut any = false;
for b in dir_road_to_bldgs.get(current.value) {
any = true;
bldg_results.insert(*b, current.cost);
}
if !any {
road_results
.entry(current.value.road)
.or_insert(current.cost);
}

for mvmnt in map.get_movements_for(current.value, constraints) {
if let Some(cost) =
Expand All @@ -142,11 +171,5 @@ pub fn all_vehicle_costs_from(
}
}

let mut results = HashMap::new();
for (b, road) in bldg_to_road {
if let Some(duration) = cost_per_node.get(&road).cloned() {
results.insert(b, duration);
}
}
results
(bldg_results, road_results)
}
30 changes: 21 additions & 9 deletions map_model/src/connectivity/walking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use geom::{Duration, Speed};

use crate::connectivity::Spot;
use crate::pathfind::{zone_cost, WalkingNode};
use crate::{BuildingID, Lane, LaneType, Map, PathConstraints, PathStep};
use crate::{BuildingID, Lane, LaneType, Map, PathConstraints, PathStep, RoadID};

#[derive(Clone)]
pub struct WalkingOptions {
Expand Down Expand Up @@ -35,18 +35,21 @@ impl WalkingOptions {
}
}

/// Starting from some initial buildings, calculate the cost to all others. If a destination isn't
/// reachable, it won't be included in the results. Ignore results greater than the time_limit
/// away.
/// Starting from some initial buildings, calculate the cost to other buildings and roads. If a
/// destination isn't reachable, it won't be included in the results. Ignore results greater than
/// the time_limit away.
///
/// If all of the start buildings are on the shoulder of a road and `!opts.allow_shoulders`, then
/// the results will always be empty.
///
/// Costs for roads will only be filled out for roads with no buildings along them. The cost will
/// be the same for the entire road, which may be misleading for long roads.
pub fn all_walking_costs_from(
map: &Map,
starts: Vec<Spot>,
time_limit: Duration,
opts: WalkingOptions,
) -> HashMap<BuildingID, Duration> {
) -> (HashMap<BuildingID, Duration>, HashMap<RoadID, Duration>) {
let mut queue: BinaryHeap<PriorityQueueItem<Duration, WalkingNode>> = BinaryHeap::new();

for spot in starts {
Expand Down Expand Up @@ -102,7 +105,7 @@ pub fn all_walking_costs_from(
}
}
if shoulder_endpoint.into_iter().all(|x| x) {
return HashMap::new();
return (HashMap::new(), HashMap::new());
}
}

Expand All @@ -111,7 +114,8 @@ pub fn all_walking_costs_from(
sidewalk_to_bldgs.insert(b.sidewalk(), b.id);
}

let mut results = HashMap::new();
let mut bldg_results = HashMap::new();
let mut road_results = HashMap::new();

let mut visited_nodes = HashSet::new();
while let Some(current) = queue.pop() {
Expand Down Expand Up @@ -146,7 +150,9 @@ pub fn all_walking_costs_from(
// this out properly, so that's why the order of graph nodes visited matters and we're
// doing this work here.
if !visited_nodes.contains(&cross_to_node) {
let mut any = false;
for b in sidewalk_to_bldgs.get(lane.id) {
any = true;
let bldg_dist_along = map.get_b(*b).sidewalk_pos.dist_along();
let dist_to_bldg = if is_dst_i {
// Crossing from the end of the sidewalk to the beginning
Expand All @@ -156,9 +162,15 @@ pub fn all_walking_costs_from(
};
let bldg_cost = current.cost + dist_to_bldg / speed;
if bldg_cost <= time_limit {
results.insert(*b, bldg_cost);
bldg_results.insert(*b, bldg_cost);
}
}
if !any {
// We could add the cost to cross this road or not. Funny things may happen
// with long roads. Also, if we've visited this road before in the opposite
// direction, don't overwrite -- keep the lower cost.
road_results.entry(lane.id.road).or_insert(current.cost);
}

queue.push(PriorityQueueItem {
cost: current.cost + sidewalk_len / speed,
Expand Down Expand Up @@ -188,5 +200,5 @@ pub fn all_walking_costs_from(
}
}

results
(bldg_results, road_results)
}