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