ltn/logic/
shortcuts.rs

1use std::collections::{BTreeSet, HashSet};
2
3use abstutil::{Counter, Timer};
4use map_gui::tools::ColorNetwork;
5use map_model::{
6    DirectedRoadID, IntersectionID, LaneID, Map, PathConstraints, PathRequest, PathStepV2, PathV2,
7    Pathfinder, Position, RoadID,
8};
9use widgetry::GeomBatch;
10
11use crate::{App, Cell, Neighbourhood};
12
13pub struct Shortcuts {
14    pub paths: Vec<PathV2>,
15    pub count_per_road: Counter<RoadID>,
16    pub count_per_intersection: Counter<IntersectionID>,
17}
18
19impl Shortcuts {
20    // For temporary use
21    pub fn empty() -> Self {
22        Self {
23            paths: Vec::new(),
24            count_per_road: Counter::new(),
25            count_per_intersection: Counter::new(),
26        }
27    }
28
29    pub fn new(map: &Map, neighbourhood: &Neighbourhood, timer: &mut Timer) -> Self {
30        // The overall approach: look for all possible paths from an entrance to an exit, only if they
31        // connect to different major roads.
32        //
33        // But an entrance and exit to _what_? If we try to route from the entrance to one cell to the
34        // exit of another, then the route will make strange U-turns and probably use the perimeter. By
35        // definition, two cells aren't reachable without using the perimeter. So restrict our search
36        // to pairs of entrances/exits in the _same_ cell.
37        let mut requests = Vec::new();
38
39        for cell in &neighbourhood.cells {
40            let entrances = find_entrances_or_exits(map, neighbourhood, cell, true);
41            let exits = find_entrances_or_exits(map, neighbourhood, cell, false);
42
43            for entrance in &entrances {
44                for exit in &exits {
45                    // Most of the time, an entrance/exit connects to only "one" major road. But where
46                    // a road name changes, or two major roads meet, we'll have multiple. If two
47                    // corners meet and share one main road, still consider that a shortcut -- it might
48                    // be a "false positive" or there could be some legitimate reason for a driver to
49                    // attempt the shortcut.
50                    if entrance.major_road_names != exit.major_road_names {
51                        requests.push(PathRequest::vehicle(
52                            Position::start(entrance.lane),
53                            Position::end(exit.lane, map),
54                            PathConstraints::Car,
55                        ));
56                    }
57                }
58            }
59        }
60
61        // Short-circuit for performance. This happens for "degenerate" neighbourhoods without any
62        // internal roads, usually near the map edge, between dual carriageways, etc.
63        if requests.is_empty() {
64            return Self::empty();
65        }
66
67        let mut params = map.routing_params_respecting_modal_filters();
68
69        // Restrict the pathfinding to the interior of the neighbourhood only. Don't allow using
70        // perimeter roads or leaving and re-entering at all.
71        //
72        // The point of this view is to show possible detours people might try to take in response to
73        // one filter. Note the original "demand model" input is bogus anyway; it's all possible
74        // entrances and exits to the neighbourhood, without regards for the larger path somebody
75        // actually wants to take.
76        params.only_use_roads = neighbourhood.interior_roads.clone();
77
78        // Also can't use private roads
79        for r in &neighbourhood.interior_roads {
80            if !crate::is_driveable(map.get_r(*r), map) {
81                params.avoid_roads.insert(*r);
82            }
83        }
84
85        // TODO Perf: when would it be worth creating a CH? Especially if we could subset just this
86        // part of the graph, it'd probably be helpful.
87        let pathfinder = Pathfinder::new_dijkstra(map, params, vec![PathConstraints::Car], timer);
88        let paths: Vec<PathV2> = timer
89            .parallelize(
90                "calculate paths between entrances and exits",
91                requests,
92                |req| pathfinder.pathfind_v2(req, map),
93            )
94            .into_iter()
95            .flatten()
96            .collect();
97
98        // TODO Rank the likeliness of each shortcut by
99        // 1) Calculating a path between similar start/endpoints -- travelling along the perimeter,
100        //    starting and ending on a specific road that makes sense. (We have to pick the 'direction'
101        //    along the perimeter roads that's sensible.)
102        // 2) Comparing that time to the time for cutting through
103
104        Shortcuts::from_paths(neighbourhood, paths)
105    }
106
107    pub fn from_paths(neighbourhood: &Neighbourhood, paths: Vec<PathV2>) -> Self {
108        // How many shortcuts pass through each street?
109        let mut count_per_road = Counter::new();
110        let mut count_per_intersection = Counter::new();
111        for path in &paths {
112            for step in path.get_steps() {
113                match step {
114                    PathStepV2::Along(dr) => {
115                        if neighbourhood.interior_roads.contains(&dr.road) {
116                            count_per_road.inc(dr.road);
117                        }
118                    }
119                    PathStepV2::Movement(m) => {
120                        if neighbourhood.interior_intersections.contains(&m.parent) {
121                            count_per_intersection.inc(m.parent);
122                        }
123                    }
124                    // Car paths don't make contraflow movements
125                    _ => unreachable!(),
126                }
127            }
128        }
129
130        Self {
131            paths,
132            count_per_road,
133            count_per_intersection,
134        }
135    }
136
137    pub fn quiet_and_total_streets(&self, neighbourhood: &Neighbourhood) -> (usize, usize) {
138        let quiet_streets = neighbourhood
139            .interior_roads
140            .iter()
141            .filter(|r| self.count_per_road.get(**r) == 0)
142            .count();
143        let total_streets = neighbourhood.interior_roads.len();
144        (quiet_streets, total_streets)
145    }
146
147    pub fn subset(&self, neighbourhood: &Neighbourhood, r: RoadID) -> Self {
148        let paths = self
149            .paths
150            .iter()
151            .filter(|path| path.crosses_road(r))
152            .cloned()
153            .collect();
154        Self::from_paths(neighbourhood, paths)
155    }
156
157    pub fn draw_heatmap(&self, app: &App) -> GeomBatch {
158        let mut colorer = ColorNetwork::no_fading(app);
159        colorer.ranked_roads(self.count_per_road.clone(), &app.cs.good_to_bad_red);
160        // TODO These two will be on different scales, which may look weird
161        colorer.ranked_intersections(self.count_per_intersection.clone(), &app.cs.good_to_bad_red);
162        colorer.draw.unzoomed
163    }
164}
165
166struct EntryExit {
167    // Really this is a DirectedRoadID, but since the pathfinding request later needs to know
168    // lanes, just use this
169    lane: LaneID,
170    major_road_names: BTreeSet<String>,
171}
172
173fn find_entrances_or_exits(
174    map: &Map,
175    neighbourhood: &Neighbourhood,
176    cell: &Cell,
177    entrances: bool,
178) -> Vec<EntryExit> {
179    let mut entry_exits = Vec::new();
180    for i in &cell.borders {
181        let major_road_names = find_major_road_names(map, neighbourhood, *i);
182        let mut seen: HashSet<DirectedRoadID> = HashSet::new();
183        let lanes = if entrances {
184            map.get_i(*i).get_outgoing_lanes(map, PathConstraints::Car)
185        } else {
186            map.get_i(*i).get_incoming_lanes(map, PathConstraints::Car)
187        };
188        for l in lanes {
189            let dr = map.get_l(l).get_directed_parent();
190            if !seen.contains(&dr) && cell.roads.contains_key(&dr.road) {
191                entry_exits.push(EntryExit {
192                    lane: l,
193                    major_road_names: major_road_names.clone(),
194                });
195                seen.insert(dr);
196            }
197        }
198    }
199    entry_exits
200}
201
202fn find_major_road_names(
203    map: &Map,
204    neighbourhood: &Neighbourhood,
205    i: IntersectionID,
206) -> BTreeSet<String> {
207    let mut names = BTreeSet::new();
208    for r in &map.get_i(i).roads {
209        if !neighbourhood.interior_roads.contains(r) {
210            names.insert(map.get_r(*r).get_name(None));
211        }
212    }
213    names
214}