Speed up the 'find perfect home' tool by starting Dijkstra's floodfill with multiple sources. Only implemented for walking right now.

This commit is contained in:
Dustin Carlino 2021-05-06 09:20:20 -07:00
parent b29a613b42
commit 8d6a184a1f
4 changed files with 62 additions and 55 deletions

View File

@ -1,6 +1,6 @@
use std::collections::{HashMap, HashSet};
use std::collections::HashMap;
use abstutil::{Counter, Parallelism, Timer};
use abstutil::{prettyprint_usize, Counter, Parallelism, Timer};
use geom::Percent;
use map_gui::tools::PopupMsg;
use map_model::{AmenityType, BuildingID};
@ -89,33 +89,28 @@ fn score_houses(
options: Options,
timer: &mut Timer,
) -> HashMap<BuildingID, Percent> {
let num_categories = amenities.len();
let mut satisfied_per_bldg: Counter<BuildingID> = Counter::new();
// There may be more clever ways to calculate this, but we'll take the brute-force approach of
// calculating the walkshed from every matching business.
let num_categories = amenities.len();
for category in amenities {
let mut stores: HashSet<BuildingID> = HashSet::new();
for b in app.map.all_buildings() {
if b.has_amenity(category) {
stores.insert(b.id);
let map = &app.map;
for times in timer.parallelize(
"find houses close to amenities",
Parallelism::Fastest,
amenities,
|category| {
// For each category, find all matching stores
let mut stores = Vec::new();
for b in map.all_buildings() {
if b.has_amenity(category) {
stores.push(b.id);
}
}
}
let mut houses: HashSet<BuildingID> = HashSet::new();
let map = &app.map;
for times in timer.parallelize(
&format!("find houses close to {}", category),
Parallelism::Fastest,
stores.into_iter().collect(),
|b| options.clone().time_to_reach_building(map, b),
) {
for (b, _) in times {
houses.insert(b);
}
}
for b in houses {
// Then find all buildings reachable from any of those starting points
options.clone().times_from_buildings(map, stores)
},
) {
for (b, _) in times {
satisfied_per_bldg.inc(b);
}
}
@ -140,12 +135,22 @@ impl Results {
scores: HashMap<BuildingID, Percent>,
amenities: Vec<AmenityType>,
) -> Box<dyn State<App>> {
// TODO Show imperfect matches with different colors.
let mut batch = GeomBatch::new();
let mut count = 0;
for (b, pct) in scores {
if pct == Percent::int(100) {
batch.push(Color::RED, app.map.get_b(b).polygon.clone());
count += 1;
}
}
let panel = Panel::new(Widget::col(vec![
Line("Results for your walkable home")
.small_heading()
.into_widget(ctx),
// TODO Adjust text to say bikeshed, or otherwise reflect the options chosen
"Here are all of the matching houses.".text_widget(ctx),
format!("{} houses match", prettyprint_usize(count)).text_widget(ctx),
format!(
"Containing at least 1 of each: {}",
amenities
@ -164,14 +169,6 @@ impl Results {
.aligned(HorizontalAlignment::RightInset, VerticalAlignment::TopInset)
.build(ctx);
// TODO Show imperfect matches with different colors.
let mut batch = GeomBatch::new();
for (b, pct) in scores {
if pct == Percent::int(100) {
batch.push(Color::RED, app.map.get_b(b).polygon.clone());
}
}
SimpleState::new(
panel,
Box::new(Results {

View File

@ -38,18 +38,19 @@ pub enum Options {
}
impl Options {
pub fn time_to_reach_building(
// TODO doc
pub fn times_from_buildings(
self,
map: &Map,
start: BuildingID,
starts: Vec<BuildingID>,
) -> HashMap<BuildingID, Duration> {
match self {
Options::Walking(opts) => {
connectivity::all_walking_costs_from(map, start, Duration::minutes(15), opts)
connectivity::all_walking_costs_from(map, starts, Duration::minutes(15), opts)
}
Options::Biking => connectivity::all_vehicle_costs_from(
map,
start,
starts,
Duration::minutes(15),
PathConstraints::Bike,
),
@ -59,7 +60,7 @@ impl Options {
impl Isochrone {
pub fn new(ctx: &mut EventCtx, app: &App, start: BuildingID, options: Options) -> Isochrone {
let time_to_reach_building = options.clone().time_to_reach_building(&app.map, start);
let time_to_reach_building = options.clone().times_from_buildings(&app.map, vec![start]);
let mut amenities_reachable = MultiMap::new();
let mut population = 0;

View File

@ -50,11 +50,12 @@ pub fn find_scc(map: &Map, constraints: PathConstraints) -> (HashSet<LaneID>, Ha
(largest_group, disconnected)
}
/// Starting from one building, 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 all others. 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,
start: BuildingID,
starts: Vec<BuildingID>,
time_limit: Duration,
constraints: PathConstraints,
) -> HashMap<BuildingID, Duration> {
@ -77,7 +78,8 @@ pub fn all_vehicle_costs_from(
}
}
if let Some(start_road) = bldg_to_road.get(&start) {
// TODO Use all starting points
if let Some(start_road) = bldg_to_road.get(&starts[0]) {
let graph = build_graph_for_vehicles(map, constraints);
let cost_per_road = petgraph::algo::dijkstra(&graph, *start_road, None, |(_, _, mvmnt)| {
vehicle_cost(mvmnt.from, *mvmnt, constraints, map.routing_params(), map)

View File

@ -56,27 +56,34 @@ impl Ord for Item {
}
}
/// Starting from one building, 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 all others. If a destination isn't
/// reachable, it won't be included in the results. Ignore results greater than the time_limit
/// away.
///
/// If the start building is on the shoulder of a road and `!opts.allow_shoulders`, then the
/// results will always be empty.
/// If all of the start buildings are on the shoulder of a road and `!opts.allow_shoulders`, then
/// the results will always be empty.
pub fn all_walking_costs_from(
map: &Map,
start: BuildingID,
starts: Vec<BuildingID>,
time_limit: Duration,
opts: WalkingOptions,
) -> HashMap<BuildingID, Duration> {
let start_lane = map.get_l(map.get_b(start).sidewalk_pos.lane());
if start_lane.lane_type == LaneType::Shoulder && !opts.allow_shoulders {
return HashMap::new();
if !opts.allow_shoulders {
if 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();
queue.push(Item {
cost: Duration::ZERO,
node: WalkingNode::closest(map.get_b(start).sidewalk_pos, map),
});
for b in starts {
queue.push(Item {
cost: Duration::ZERO,
node: WalkingNode::closest(map.get_b(b).sidewalk_pos, map),
});
}
let mut cost_per_node: HashMap<WalkingNode, Duration> = HashMap::new();
while let Some(current) = queue.pop() {