Take a third stab at finding rat runs in a neighborhood. This new

definition finds all entrances and exits to a neighborhood, grouped by
the name of the perimeter road they connect to. Then the shortest
driving path between every pair is found, and the results are sorted to
emphasize paths that spend most of the time inside the neighborhood.

This new version doesn't crash and seems to produce reasonable results
everywhere I've looked.

It doesn't yet pay attention to the modal filters people have created,
or summarize which roads are likely to be quiet or not.
This commit is contained in:
Dustin Carlino 2021-12-19 11:24:11 +00:00
parent 904ba3c249
commit 5542632d61
3 changed files with 153 additions and 266 deletions

View File

@ -6,16 +6,16 @@ use widgetry::{
VerticalAlignment, Widget,
};
use super::rat_runs::{find_rat_runs, RatRun};
use super::rat_runs::{find_rat_runs, RatRuns};
use super::Neighborhood;
use crate::app::{App, Transition};
pub struct BrowseRatRuns {
panel: Panel,
rat_runs: Vec<RatRun>,
rat_runs: RatRuns,
current_idx: usize,
draw_paths: ToggleZoomed,
draw_path: ToggleZoomed,
neighborhood: Neighborhood,
}
@ -25,12 +25,19 @@ impl BrowseRatRuns {
app: &App,
neighborhood: Neighborhood,
) -> Box<dyn State<App>> {
let rat_runs = find_rat_runs(&app.primary.map, &neighborhood, &app.session.modal_filters);
let rat_runs = ctx.loading_screen("find rat runs", |_, timer| {
find_rat_runs(
&app.primary.map,
&neighborhood,
&app.session.modal_filters,
timer,
)
});
let mut state = BrowseRatRuns {
panel: Panel::empty(ctx),
rat_runs,
current_idx: 0,
draw_paths: ToggleZoomed::empty(ctx),
draw_path: ToggleZoomed::empty(ctx),
neighborhood,
};
state.recalculate(ctx, app);
@ -38,7 +45,7 @@ impl BrowseRatRuns {
}
fn recalculate(&mut self, ctx: &mut EventCtx, app: &App) {
if self.rat_runs.is_empty() {
if self.rat_runs.paths.is_empty() {
self.panel = Panel::new_builder(Widget::col(vec![
ctx.style()
.btn_outline
@ -52,15 +59,13 @@ impl BrowseRatRuns {
return;
}
let current = &self.rat_runs[self.current_idx];
self.panel = Panel::new_builder(Widget::col(vec![
ctx.style()
.btn_outline
.text("Back to editing modal filters")
.hotkey(Key::Escape)
.build_def(ctx),
Line("Warning: placeholder results")
Line("Warning: preliminary results")
.fg(Color::RED)
.into_widget(ctx),
Widget::row(vec![
@ -71,49 +76,49 @@ impl BrowseRatRuns {
.hotkey(Key::LeftArrow)
.build_widget(ctx, "previous rat run"),
Text::from(
Line(format!("{}/{}", self.current_idx + 1, self.rat_runs.len())).secondary(),
Line(format!(
"{}/{}",
self.current_idx + 1,
self.rat_runs.paths.len()
))
.secondary(),
)
.into_widget(ctx)
.centered_vert(),
ctx.style()
.btn_next()
.disabled(self.current_idx == self.rat_runs.len() - 1)
.disabled(self.current_idx == self.rat_runs.paths.len() - 1)
.hotkey(Key::RightArrow)
.build_widget(ctx, "next rat run"),
]),
Text::from_multiline(vec![
Line(format!("Ratio: {:.2}", current.time_ratio())),
Line(format!(
"Shortcut takes: {}",
current.shortcut_path.get_cost()
)),
Line(format!(
"Fastest path takes: {}",
current.fastest_path.get_cost()
)),
])
.into_widget(ctx),
]))
.aligned(HorizontalAlignment::Left, VerticalAlignment::Top)
.build(ctx);
// TODO Transforming into PathV1 seems like a particularly unnecessary step. Time to come
// up with a native v2 drawing?
let mut draw_paths = ToggleZoomed::builder();
for (path, color) in [
(current.shortcut_path.clone(), Color::RED),
(current.fastest_path.clone(), Color::BLUE),
] {
if let Ok(path) = path.into_v1(&app.primary.map) {
if let Some(pl) = path.trace(&app.primary.map) {
// TODO This produces a really buggy shape sometimes!
let shape = pl.make_arrow(3.0 * NORMAL_LANE_THICKNESS, ArrowCap::Triangle);
draw_paths.unzoomed.push(color.alpha(0.8), shape.clone());
draw_paths.zoomed.push(color.alpha(0.5), shape);
}
}
let mut draw_path = ToggleZoomed::builder();
let color = Color::RED;
let path = &self.rat_runs.paths[self.current_idx];
if let Some(pl) = path.trace(&app.primary.map) {
// TODO This produces a really buggy shape sometimes!
let shape = pl.make_arrow(3.0 * NORMAL_LANE_THICKNESS, ArrowCap::Triangle);
draw_path.unzoomed.push(color.alpha(0.8), shape.clone());
draw_path.zoomed.push(color.alpha(0.5), shape);
draw_path
.unzoomed
.append(map_gui::tools::start_marker(ctx, pl.first_pt(), 2.0));
draw_path
.zoomed
.append(map_gui::tools::start_marker(ctx, pl.first_pt(), 0.5));
draw_path
.unzoomed
.append(map_gui::tools::goal_marker(ctx, pl.last_pt(), 2.0));
draw_path
.zoomed
.append(map_gui::tools::goal_marker(ctx, pl.last_pt(), 0.5));
}
self.draw_paths = draw_paths.build(ctx);
self.draw_path = draw_path.build(ctx);
}
}
@ -157,6 +162,6 @@ impl State<App> for BrowseRatRuns {
self.neighborhood.labels.draw(g, app);
}
self.draw_paths.draw(g);
self.draw_path.draw(g);
}
}

View File

@ -1,255 +1,139 @@
use std::cmp::Ordering;
use std::collections::{BTreeSet, BinaryHeap, HashMap, HashSet};
use std::collections::HashSet;
use geom::Duration;
use abstutil::Timer;
use map_model::{
connectivity, DirectedRoadID, DrivingSide, IntersectionID, Map, MovementID, PathConstraints,
PathRequest, PathV2, TurnType,
DirectedRoadID, IntersectionID, LaneID, Map, Path, PathConstraints, PathRequest, PathStep,
Position,
};
use super::{ModalFilters, Neighborhood};
pub struct RatRun {
pub shortcut_path: PathV2,
/// May be the same as the shortcut
pub fastest_path: PathV2,
pub struct RatRuns {
pub paths: Vec<Path>,
}
/// Ideally this returns every possible path through the neighborhood between two borders. Doesn't
/// work correctly yet.
pub fn find_rat_runs(
map: &Map,
neighborhood: &Neighborhood,
modal_filters: &ModalFilters,
) -> Vec<RatRun> {
let mut results: Vec<RatRun> = Vec::new();
for i in &neighborhood.borders {
let mut started_from: HashSet<DirectedRoadID> = HashSet::new();
for l in map.get_i(*i).get_outgoing_lanes(map, PathConstraints::Car) {
let dr = map.get_l(l).get_directed_parent();
if !started_from.contains(&dr)
&& neighborhood.orig_perimeter.interior.contains(&dr.road)
{
started_from.insert(dr);
results.extend(find_rat_runs_from(
map,
dr,
&neighborhood.borders,
modal_filters,
timer: &mut Timer,
) -> RatRuns {
let entrances = find_entrances(map, neighborhood);
let exits = find_exits(map, neighborhood);
// Look for all possible paths from an entrance to an exit, only if they connect to different
// major roads.
let mut requests = Vec::new();
for entrance in &entrances {
for exit in &exits {
if entrance.major_road_name != exit.major_road_name {
requests.push(PathRequest::vehicle(
Position::start(entrance.lane),
Position::end(exit.lane, map),
PathConstraints::Car,
));
}
}
}
results.sort_by(|a, b| a.time_ratio().partial_cmp(&b.time_ratio()).unwrap());
results
}
fn find_rat_runs_from(
map: &Map,
start: DirectedRoadID,
borders: &BTreeSet<IntersectionID>,
modal_filters: &ModalFilters,
) -> Vec<RatRun> {
// If there's a filter where we're starting, we can't go anywhere
if modal_filters.roads.contains_key(&start.road) {
return Vec::new();
}
let mut paths: Vec<Path> = timer
.parallelize(
"calculate paths between entrances and exits",
requests,
|req| map.pathfind(req),
)
.into_iter()
.flatten()
.collect();
let mut results = Vec::new();
let mut back_refs = HashMap::new();
let mut queue: BinaryHeap<Item> = BinaryHeap::new();
queue.push(Item {
node: start,
cost: Duration::ZERO,
// Some paths wind up partly using perimeter roads (or even things outside the neighborhood
// entirely). Sort by "worse" paths that spend more time inside.
paths.sort_by_key(|path| {
let mut roads_inside = 0;
let mut roads_outside = 0;
for step in path.get_steps() {
if let PathStep::Lane(l) = step {
if neighborhood.orig_perimeter.interior.contains(&l.road) {
roads_inside += 1;
} else {
roads_outside += 1;
}
}
}
let pct = (roads_outside as f64) / (roads_outside + roads_inside) as f64;
// f64 isn't Ord, just approximate by 1/10th of a percent
(pct * 1000.0) as usize
});
let mut visited = HashSet::new();
while let Some(current) = queue.pop() {
if visited.contains(&current.node) {
continue;
}
visited.insert(current.node);
// TODO Heatmap of roads used (any direction)
// If we found a border, then stitch together the path
let dst_i = current.node.dst_i(map);
if borders.contains(&dst_i) {
let mut at = current.node;
let mut path = vec![at];
while let Some(prev) = back_refs.get(&at).cloned() {
path.push(prev);
at = prev;
RatRuns { paths }
}
struct EntryExit {
// TODO Really this is a DirectedRoadID, but since pathfinding later needs to know lanes, just
// use this
lane: LaneID,
major_road_name: String,
}
fn find_entrances(map: &Map, neighborhood: &Neighborhood) -> Vec<EntryExit> {
let mut entrances = Vec::new();
for i in &neighborhood.borders {
if let Some(major_road_name) = find_major_road_name(map, neighborhood, *i) {
let mut seen: HashSet<DirectedRoadID> = HashSet::new();
for l in map.get_i(*i).get_outgoing_lanes(map, PathConstraints::Car) {
let dr = map.get_l(l).get_directed_parent();
if !seen.contains(&dr) && neighborhood.orig_perimeter.interior.contains(&dr.road) {
entrances.push(EntryExit {
lane: l,
major_road_name: major_road_name.clone(),
});
seen.insert(dr);
}
}
path.push(start);
path.reverse();
results.push(RatRun::new(map, path, current.cost));
// Keep searching for more
continue;
}
}
entrances
}
for mvmnt in map.get_movements_for(current.node, PathConstraints::Car) {
// Can't cross filters
if modal_filters.roads.contains_key(&mvmnt.to.road)
|| modal_filters
.intersections
.get(&mvmnt.parent)
.map(|filter| !filter.allows_turn(mvmnt.from.road, mvmnt.to.road))
.unwrap_or(false)
{
continue;
fn find_exits(map: &Map, neighborhood: &Neighborhood) -> Vec<EntryExit> {
let mut exits = Vec::new();
for i in &neighborhood.borders {
if let Some(major_road_name) = find_major_road_name(map, neighborhood, *i) {
let mut seen: HashSet<DirectedRoadID> = HashSet::new();
for l in map.get_i(*i).get_incoming_lanes(map, PathConstraints::Car) {
let dr = map.get_l(l).get_directed_parent();
if !seen.contains(&dr) && neighborhood.orig_perimeter.interior.contains(&dr.road) {
exits.push(EntryExit {
lane: l,
major_road_name: major_road_name.clone(),
});
seen.insert(dr);
}
}
// If we've already visited the destination, don't add it again. We don't want to
// update back_refs -- because this must be a higher-cost path to a place we've already
// visited.
if visited.contains(&mvmnt.to) {
continue;
}
queue.push(Item {
cost: current.cost
+ connectivity::vehicle_cost(
mvmnt.from,
mvmnt,
PathConstraints::Car,
map.routing_params(),
map,
),
node: mvmnt.to,
});
back_refs.insert(mvmnt.to, mvmnt.from);
}
}
results
exits
}
#[derive(PartialEq, Eq)]
struct Item {
cost: Duration,
node: DirectedRoadID,
}
impl PartialOrd for Item {
fn partial_cmp(&self, other: &Item) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Item {
fn cmp(&self, other: &Item) -> Ordering {
// BinaryHeap is a max-heap, so reverse the comparison to get smallest times first.
let ord = other.cost.cmp(&self.cost);
if ord != Ordering::Equal {
return ord;
}
self.node.cmp(&other.node)
}
}
impl RatRun {
fn new(map: &Map, mut path: Vec<DirectedRoadID>, mut cost: Duration) -> RatRun {
// The rat run starts and ends at a road just inside the neighborhood. To "motivate" using
// the shortcut, find an entry and exit road just outside the neighborhood to calculate a
// fastest path.
let entry = cheap_entry(map, path[0]);
let exit = cheap_exit(map, *path.last().unwrap());
path.insert(0, entry.from);
path.push(exit.to);
// Adjust the cost for the new roads
// TODO Or just make a PathV2 method to do this?
cost += connectivity::vehicle_cost(
entry.from,
entry,
PathConstraints::Car,
map.routing_params(),
map,
);
cost += connectivity::vehicle_cost(
// TODO This is an abuse of vehicle_cost! It should just take the MovementID and always
// use from... and something else should add the cost of the final road
exit.to,
exit,
PathConstraints::Car,
map.routing_params(),
map,
);
let req =
PathRequest::between_directed_roads(map, entry.from, exit.to, PathConstraints::Car)
.unwrap();
let shortcut_path = PathV2::from_roads(
path,
req.clone(),
cost,
// TODO We're assuming there are no uber turns. Seems unlikely in the interior of a
// neighborhood!
Vec::new(),
map,
);
let fastest_path = map.pathfind_v2(req).unwrap();
// TODO If the path matches up, double check the cost does too, since we may calculate it
// differently...
RatRun {
shortcut_path,
fastest_path,
fn find_major_road_name(
map: &Map,
neighborhood: &Neighborhood,
i: IntersectionID,
) -> Option<String> {
let mut names = Vec::new();
for r in &map.get_i(i).roads {
if neighborhood.perimeter.contains(r) {
names.push(map.get_r(*r).get_name(None));
}
}
/// The ratio of the shortcut's time to the fastest path's time. Smaller values mean the
/// shortcut is more desirable.
pub fn time_ratio(&self) -> f64 {
// TODO Not sure why yet, just avoid crashing
if self.fastest_path.get_cost() == Duration::ZERO {
return 1.0;
}
self.shortcut_path.get_cost() / self.fastest_path.get_cost()
}
}
/// Find a movement that leads into the neighborhood at the first road in a rat-run
fn cheap_entry(map: &Map, to: DirectedRoadID) -> MovementID {
let cheap_turn_type = if map.get_config().driving_side == DrivingSide::Right {
TurnType::Right
names.sort();
names.dedup();
// TODO If the major road changes names or we found a corner, bail out
if names.len() == 1 {
names.pop()
} else {
TurnType::Left
};
map.get_i(to.src_i(map))
.movements
.values()
.filter(|mvmnt| mvmnt.id.to == to)
.min_by_key(|mvmnt| {
if mvmnt.turn_type == cheap_turn_type {
0
} else if mvmnt.turn_type == TurnType::Straight {
1
} else {
2
}
})
.unwrap()
.id
}
/// Find a movement that leads out of the neighborhood at the last road in a rat-run
fn cheap_exit(map: &Map, from: DirectedRoadID) -> MovementID {
let cheap_turn_type = if map.get_config().driving_side == DrivingSide::Right {
TurnType::Right
} else {
TurnType::Left
};
map.get_i(from.dst_i(map))
.movements
.values()
.filter(|mvmnt| mvmnt.id.from == from)
.min_by_key(|mvmnt| {
if mvmnt.turn_type == cheap_turn_type {
0
} else if mvmnt.turn_type == TurnType::Straight {
1
} else {
2
}
})
.unwrap()
.id
None
}
}

View File

@ -45,10 +45,8 @@ impl Viewer {
.build_def(ctx),
ctx.style()
.btn_outline
.text("Browse rat-runs")
.text("Examine rat-runs")
.hotkey(Key::R)
.disabled(true)
.disabled_tooltip("Still being prototyped")
.build_def(ctx),
ctx.style()
.btn_outline
@ -110,7 +108,7 @@ impl State<App> for Viewer {
)]
}));
}
"Browse rat-runs" => {
"Examine rat-runs" => {
return Transition::ConsumeState(Box::new(|state, ctx, app| {
let state = state.downcast::<Viewer>().ok().unwrap();
vec![super::rat_run_viewer::BrowseRatRuns::new_state(