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}