ltn/logic/turn_restrictions.rs
1use std::collections::HashSet;
2
3use geom::{Polygon, Pt2D};
4use map_model::{IntersectionID, Map, RoadID};
5use osm2streets::{Direction, RestrictionType};
6
7/// An attempt to standardise language around turn restrictions.
8/// NOTE AT PRESENT THIS IS ASPIRATIONAL - DO NOT ASSUME THAT THE RELEVANT CODE ADHERES TO THESE RULES
9/// Future refactoring should migrate to these conventions.
10///
11/// Summary:
12/// -------
13/// ```notrust
14/// {connected} == { permitted ∪ opposing_oneway ∪ restricted_turn }
15///
16/// {possible_turns} == { connected - opposing_oneways } == { permitted + restricted_turns }
17/// ```
18///
19/// Details:
20/// -------
21/// When moving (or attempting to move) from "RoadA" to "RoadB" the follow terms should be used:
22/// "from_r" = RoadA
23/// "target_r" = RoadB
24/// "connected" = RoadB will be a member of "connected" if RoadB share a common intersection with RoadA, or
25/// is part of a shared complicated turn with RoadA. The legality of driving from RoadA to RoadB
26/// is not a concern for "connected". "Connected" is the superset of all the other categories
27/// listed here.
28/// "permitted" = RoadB is a member of "permitted", if RoadB is a member of "connected" and it is legal to
29/// drive from RoadA to RoadB. Lane-level restrictions are not considered, so as long as some
30/// route from one or more driving Lanes in RoadA to one or more Lanes in RoadB then RoadB is
31/// considered "permitted".
32/// "opposing_oneways" = RoadB is oneway for driving, and driving from RoadA to RoadB would result in driving the
33/// wrong way along RoadB.
34/// "restricted_turns" = RoadB will be a member of "restricted_turns" if all of these are true:
35/// a) RoadB is a member of "connected"
36/// b) There is explicitly tagged turn restriction which prohibits traffic turning from
37/// RoadA to RoadB, OR there is an explicitly tagged turn restriction which mandates
38/// traffic from RoadA must turn onto a different road to RoadB.
39/// c) RoadB is not a member of "opposing_oneways"
40/// "possible_turns" = These are turns that would be possible if all turn restrictions where removed.
41///
42/// Notes:
43/// -----
44/// * RoadA will NOT be a member of any of the groups connected, permitted, opposing_oneway, restricted_turn
45/// even if a no U-turns restriction exists
46/// * In reality a road/turn maybe signposted by both turn restrictions and oneway restrictions.
47/// Following (OSM practise)[https://wiki.openstreetmap.org/wiki/Relation:restriction#When_to_map] it is not
48/// necessary mark turn restrictions when they are already implied by opposing oneway restrictions. We treat
49/// "banned_turn" and "opposing_oneway" as mutually exclusive.
50///
51/// Discouraged terms:
52/// -----------------
53/// "prohibited_turn" => use "restricted_turn" instead.
54/// "banned_turns" => use "restricted_turn" instead where practical. "Banned" is used elsewhere in A/BStreet,
55/// (ie `RestrictionType::BanTurns`) but within the LTN tool "restricted" is preferred, as it
56/// is more consistent with the `road.restricted_turns` and `road.complicated_turn_restrictions`
57/// as well the general OSM tagging.
58/// "src_r" and "dst_r" => use "from_r" and "target_r" instead. ("src_r" and "dst_r" are too similar to
59/// `road.src_i` and `road.dst_i` which are conceptually very different).
60pub struct FocusedTurns {
61 pub from_r: RoadID,
62 pub i: IntersectionID,
63 pub hull: Polygon,
64 pub possible_t: HashSet<RoadID>,
65 pub restricted_t: HashSet<RoadID>,
66}
67
68impl FocusedTurns {
69 pub fn new(r: RoadID, clicked_pt: Pt2D, map: &Map) -> Self {
70 let dst_i = map.get_r(r).dst_i;
71 let src_i = map.get_r(r).src_i;
72
73 let dst_m = clicked_pt.fast_dist(map.get_i(dst_i).polygon.center());
74 let src_m = clicked_pt.fast_dist(map.get_i(src_i).polygon.center());
75
76 // Find the closest intersection
77 let i = if dst_m > src_m { src_i } else { dst_i };
78
79 let restricted_t = restricted_destination_roads(map, r, Some(i));
80 let possible_t = possible_destination_roads(map, r, Some(i));
81 let hull = hull_around_focused_turns(map, r, &possible_t, &restricted_t);
82
83 FocusedTurns {
84 from_r: r,
85 i,
86 hull,
87 possible_t,
88 restricted_t,
89 }
90 }
91}
92
93fn hull_around_focused_turns(
94 map: &Map,
95 r: RoadID,
96 permitted_t: &HashSet<RoadID>,
97 restricted_t: &HashSet<RoadID>,
98) -> Polygon {
99 let mut all_pt: Vec<Pt2D> = Vec::new();
100
101 let mut all_r = HashSet::from([r]);
102 all_r.extend(permitted_t);
103 all_r.extend(restricted_t);
104
105 for other_r in all_r {
106 all_pt.extend(
107 map.get_r(other_r)
108 .get_thick_polygon()
109 .get_outer_ring()
110 .clone()
111 .into_points(),
112 );
113 }
114
115 // TODO the `200` value seems to work for some cases. But it is arbitrary and there is no science
116 // behind its the value. Need to work out what is an appropriate value _and why_.
117 Polygon::concave_hull(all_pt, 200).unwrap_or(Polygon::dummy())
118}
119
120/// Returns all roads that are possible destinations from the given "from_road" where the turn is currently
121/// prohibited by a turn restriction.
122pub fn restricted_destination_roads(
123 map: &Map,
124 from_road_id: RoadID,
125 i: Option<IntersectionID>,
126) -> HashSet<RoadID> {
127 let candidate_roads = possible_destination_roads(map, from_road_id, i);
128
129 let from_road = map.get_r(from_road_id);
130 let mut restricted_destinations: HashSet<RoadID> = HashSet::new();
131
132 for (restriction, target_r) in &from_road.turn_restrictions {
133 if *restriction == RestrictionType::BanTurns && candidate_roads.contains(target_r) {
134 restricted_destinations.insert(*target_r);
135 }
136 }
137 for (via, target_r) in &from_road.complicated_turn_restrictions {
138 if candidate_roads.contains(via) {
139 restricted_destinations.insert(*via);
140 restricted_destinations.insert(*target_r);
141 }
142 }
143 restricted_destinations
144}
145
146/// checks that an Intersection ID is connected to a RoadID. Returns `true` if connected, `false` otherwise.
147fn verify_intersection(map: &Map, r: RoadID, i: IntersectionID) -> bool {
148 let road = map.get_r(r);
149 road.dst_i == i || road.src_i == i
150}
151
152/// Returns a HashSet of all roads which are connected by driving from RoadID.
153/// This accounts for oneway restrictions, but not turn restrictions. eg:
154///
155/// - If a oneway restriction on either the 'from_road' or the 'target_road' would prevent driving from
156/// source to destination, then 'target_road' it will NOT be included in the result.
157/// - If a turn restriction exists and is the only thing that would prevent driving from 'from_road' or the
158/// 'target_road', then the 'target_road' will still be included in the result.
159///
160/// `i` is Optional. If `i` is `Some` then, it must be connected to `from_r`. It is used to filter
161/// the results to return only the destination roads that connect to `i`.
162///
163// TODO highlighting possible destinations for complicated turns (at present both sections of existing
164// complicated_turn_restrictions are included). However possible future complicated turns are not detected.
165//
166// TODO Rework `possible_destination_roads()` and `restricted_destination_roads()` to a extra function that
167// returns a tupple `(permitted, opposing_oneway, restricted_turn)`
168pub fn possible_destination_roads(
169 map: &Map,
170 from_r: RoadID,
171 i: Option<IntersectionID>,
172) -> HashSet<RoadID> {
173 if let Some(unverified_i) = i {
174 if !verify_intersection(map, from_r, unverified_i) {
175 panic!(
176 "IntersectionID {:?}, does not connect to RoadID {:?}",
177 unverified_i, from_r
178 );
179 }
180 }
181
182 let from_road = map.get_r(from_r);
183 let mut target_roads: HashSet<RoadID> = HashSet::new();
184
185 let one_way = from_road.oneway_for_driving();
186
187 if one_way != Some(Direction::Fwd) && Some(from_road.dst_i) != i {
188 for r in &map.get_i(from_road.src_i).roads {
189 if from_road.id != *r && is_road_drivable_from_i(&map, *r, from_road.src_i) {
190 target_roads.insert(*r);
191 }
192 }
193 }
194
195 if one_way != Some(Direction::Back) && Some(from_road.src_i) != i {
196 for r in &map.get_i(from_road.dst_i).roads {
197 if from_road.id != *r && is_road_drivable_from_i(&map, *r, from_road.dst_i) {
198 target_roads.insert(*r);
199 }
200 }
201 }
202 target_roads
203}
204
205fn is_road_drivable_from_i(map: &Map, target_r: RoadID, i: IntersectionID) -> bool {
206 let road = map.get_r(target_r);
207 let one_way = road.oneway_for_driving();
208
209 return road.is_driveable()
210 && ((road.src_i == i && one_way != Some(Direction::Back))
211 || (road.dst_i == i && one_way != Some(Direction::Fwd)));
212}
213
214#[cfg(test)]
215mod tests {
216 use super::{possible_destination_roads, restricted_destination_roads, FocusedTurns};
217 use geom::Pt2D;
218 use map_model::{IntersectionID, RoadID};
219 use std::collections::HashSet;
220 use tests::{get_test_file_path, import_map};
221
222 #[test]
223 fn test_focused_turn_restriction() {
224 // Test that the correct intersection is selected when creating a FocusTurns object
225
226 // Get example map
227 let file_name = get_test_file_path(String::from("input/turn_restriction_ltn_boundary.osm"));
228 let map = import_map(file_name.unwrap());
229
230 let r = RoadID(11);
231 let road = map.get_r(r);
232 // south west
233 let click_pt_1 = Pt2D::new(192.5633, 215.7847);
234 let expected_i_1 = 3;
235 // north east
236 let click_pt_2 = Pt2D::new(214.7931, 201.7212);
237 let expects_i_2 = 13;
238
239 for (click_pt, i_id) in [(click_pt_1, expected_i_1), (click_pt_2, expects_i_2)] {
240 let ft = FocusedTurns::new(r, click_pt, &map);
241
242 println!("ft.i {:?}", ft.i);
243 assert_eq!(ft.i, IntersectionID(i_id));
244 assert!([road.src_i, road.dst_i].contains(&ft.i));
245 }
246 }
247
248 #[test]
249 fn test_destination_roads() {
250 // Get example map
251 let file_name = get_test_file_path(String::from("input/turn_restriction_ltn_boundary.osm"));
252 let map = import_map(file_name.unwrap());
253
254 // hard coded values for "turn_restriction_ltn_boundary"
255 let from_r = RoadID(11);
256 let from_road = map.get_r(from_r);
257 // Expected possible turns for either intersection
258 let expected_possible_all_r = vec![3usize, 4, 9, 12]
259 .into_iter()
260 .map(|n| RoadID(n))
261 .collect::<HashSet<_>>();
262 // Expected possible turns via `from_r.dst_i`
263 let expected_possible_for_dst_i = vec![9usize, 12]
264 .into_iter()
265 .map(|n| RoadID(n))
266 .collect::<HashSet<_>>();
267 // Expected possible turns via `from_r.src_i`
268 let expected_possible_for_src_i = vec![3usize, 4]
269 .into_iter()
270 .map(|n| RoadID(n))
271 .collect::<HashSet<_>>();
272
273 // Three test cases
274 for (i, expected) in [
275 (None, expected_possible_all_r),
276 (Some(from_road.dst_i), expected_possible_for_dst_i),
277 (Some(from_road.src_i), expected_possible_for_src_i),
278 ] {
279 let actual_vec = possible_destination_roads(&map, from_r, i);
280 let mut actual = HashSet::<RoadID>::new();
281 actual.extend(actual_vec.iter());
282
283 for target_r in actual.iter() {
284 println!("destination_roads, src_r {}, dst_r = {}", from_r, target_r);
285 }
286 assert_eq!(actual, expected);
287 }
288 }
289
290 #[test]
291 fn test_destination_roads_connected_one_ways() {
292 struct TurnRestrictionTestCase {
293 pub input_file: String,
294 pub from_r: RoadID,
295 pub possible_for_dst_i: HashSet<RoadID>,
296 pub possible_for_src_i: HashSet<RoadID>,
297 pub restricted_for_dst_i: HashSet<RoadID>,
298 pub restricted_for_src_i: HashSet<RoadID>,
299 }
300
301 let test_cases = [
302 TurnRestrictionTestCase {
303 input_file: String::from("input/false_positive_u_turns.osm"),
304 // north end is dst according to JOSM
305 from_r: RoadID(5),
306 // Can continue on north bound left-hand lane past central barrier
307 possible_for_dst_i: HashSet::from([RoadID(1)]),
308 // Cannot continue on southbound right-hand (opposing oneway) past barrier RoadID(3)
309 // but this is already restricted by virtue of being oneway
310 restricted_for_dst_i: HashSet::new(),
311 // Can continue south onto Tyne bridge (RoadID(0))
312 // Right turn would prevent turing onto right on Pilgrim Street North bound (RoadID(2))
313 possible_for_src_i: HashSet::from([RoadID(2), RoadID(0)]),
314 // Cannot turn right onto Pilgrim Street North bound (RoadID(2)).
315 // Also cannot go backward up southbound ramp (RoadID(4) which is a oneway.
316 restricted_for_src_i: HashSet::from([RoadID(2)]),
317 },
318 TurnRestrictionTestCase {
319 input_file: String::from("input/false_positive_u_turns.osm"),
320 // north end is src according to JOSM
321 from_r: RoadID(0),
322 // Off the edge of the map
323 possible_for_dst_i: HashSet::new(),
324 // Off the edge of the map
325 restricted_for_dst_i: HashSet::new(),
326 // Can continue south onto Tyne bridge
327 possible_for_src_i: HashSet::from([RoadID(5), RoadID(2)]),
328 // Cannot turn right onto Pilgrim Street North bound - Cannot go backward up southbound oneway ramp (RoadID(4))
329 restricted_for_src_i: HashSet::new(),
330 },
331 ];
332
333 for tc in test_cases {
334 // Get example map
335 let file_name = get_test_file_path(tc.input_file.clone());
336 let map = import_map(file_name.unwrap());
337
338 // Three combinations of road/intersection for each test case
339 for (i, expected_possible, expected_restricted) in [
340 (
341 Some(map.get_r(tc.from_r).dst_i),
342 tc.possible_for_dst_i,
343 tc.restricted_for_dst_i,
344 ),
345 (
346 Some(map.get_r(tc.from_r).src_i),
347 tc.possible_for_src_i,
348 tc.restricted_for_src_i,
349 ),
350 // (None,
351 // tc.permitted_dst_i.union(tc.permitted_src_i).collect::<HashSet<_>>(),
352 // tc.prohibited_dst_i.union(tc.prohibited_src_i).collect::<HashSet<_>>()
353 // )
354 ] {
355 let actual_possible = possible_destination_roads(&map, tc.from_r, i);
356 let actual_restricted = restricted_destination_roads(&map, tc.from_r, i);
357
358 println!("r={:?}, i={:?}, file={:?}", &tc.from_r, i, &tc.input_file);
359 for target_r in actual_possible.iter() {
360 println!(
361 "destination_roads, src_r {}, dst_r = {}",
362 tc.from_r, target_r
363 );
364 }
365 assert_eq!(actual_restricted, expected_restricted);
366 assert_eq!(actual_possible, expected_possible);
367 }
368 }
369 }
370}