diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 12bb8e26..e58beecf 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -925,7 +925,7 @@ checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "utils" version = "0.1.0" -source = "git+https://github.com/a-b-street/utils#5abc185bbe7c9d67c8a064e3357a6365688f4bec" +source = "git+https://github.com/a-b-street/utils#10d8bf23282fb601ffc3eeb78b5ea8b9b1acd496" dependencies = [ "anyhow", "fast_paths", diff --git a/backend/src/create.rs b/backend/src/create.rs index fe5dfcfa..78ece80d 100644 --- a/backend/src/create.rs +++ b/backend/src/create.rs @@ -21,6 +21,8 @@ struct Osm { railways: Vec, waterways: Vec, barrier_nodes: BTreeSet, + // Only represent one case of restricted turns (from, to) on a particular node + turn_restrictions: HashMap>, } impl OsmReader for Osm { @@ -74,6 +76,55 @@ impl OsmReader for Osm { } } } + + // https://wiki.openstreetmap.org/wiki/Relation:restriction describes many cases. Only handle + // the simplest: a banned turn involving exactly 2 ways and 1 node. + if tags.is("type", "restriction") + && tags.is_any( + "restriction", + vec![ + "no_right_turn", + "no_left_turn", + "no_u_turn", + "no_straight_on", + ], + ) + { + let mut from = None; + let mut via = None; + let mut to = None; + for (role, member) in members { + match member { + OsmID::Way(w) => { + if role == "from" && from.is_none() { + from = Some(*w); + } else if role == "to" && to.is_none() { + to = Some(*w); + } else { + // Some other case, bail out + return; + } + } + OsmID::Node(n) => { + if role == "via" && via.is_none() { + via = Some(*n); + } else { + return; + } + } + OsmID::Relation(_) => { + return; + } + } + } + + if let (Some(from), Some(via), Some(to)) = (from, via, to) { + self.turn_restrictions + .entry(via) + .or_insert_with(Vec::new) + .push((from, to)); + } + } } } @@ -96,6 +147,7 @@ pub fn create_from_osm( point: i.point, node: i.osm_node, roads: i.edges.into_iter().map(|e| RoadID(e.0)).collect(), + turn_restrictions: Vec::new(), }) .collect(); @@ -172,10 +224,40 @@ pub fn create_from_osm( }; map.impact = Some(Impact::new(&map)); + let graph = GraphSubset { + node_to_edge: graph.node_to_edge, + node_to_pt: graph.node_to_pt, + }; + + apply_existing_filters(&mut map, osm.barrier_nodes, &graph); + apply_turn_restrictions(&mut map, osm.turn_restrictions); + + let main_road_penalty = 1.0; + map.router_before = Some(Router::new( + &map.roads, + &map.modal_filters, + &map.directions, + main_road_penalty, + )); + + Ok(map) +} + +// Handles a partial borrow of Graph +struct GraphSubset { + node_to_edge: HashMap, + node_to_pt: HashMap, +} + +fn apply_existing_filters( + map: &mut MapModel, + barrier_nodes: BTreeSet, + graph: &GraphSubset, +) { // TODO Batch some or all of these initial edits? // Apply barriers on any surviving edges. RoadID and osm2graph::EdgeID are the same. - for node in osm.barrier_nodes { + for node in barrier_nodes { // If there's no surviving edge, then it was a barrier on something we don't consider a // road or on a road that was removed let Some(edge) = graph.node_to_edge.get(&node) else { @@ -186,7 +268,7 @@ pub fn create_from_osm( map.add_modal_filter(pt, Some(vec![RoadID(edge.0)]), FilterKind::NoEntry); } - // Look for roads tagged with restrictions + // Look for roads tagged with access restrictions let pedestrian_roads: BTreeSet = map .roads .iter() @@ -257,16 +339,34 @@ pub fn create_from_osm( map.original_modal_filters = map.modal_filters.clone(); map.undo_stack.clear(); map.redo_queue.clear(); +} - let main_road_penalty = 1.0; - map.router_before = Some(Router::new( - &map.roads, - &map.modal_filters, - &map.directions, - main_road_penalty, - )); +fn apply_turn_restrictions( + map: &mut MapModel, + mut turn_restrictions: HashMap>, +) { + for intersection in &mut map.intersections { + if let Some(list) = turn_restrictions.remove(&intersection.node) { + for (from_way, to_way) in list { + // One OSM way turns into multiple Roads. The restriction only makes sense on the + // Road connected to this intersection. So search only this intersection's roads. + let mut from = None; + let mut to = None; + for r in &intersection.roads { + let way = map.roads[r.0].way; + if way == from_way { + from = Some(*r); + } else if way == to_way { + to = Some(*r); + } + } - Ok(map) + if let (Some(from), Some(to)) = (from, to) { + intersection.turn_restrictions.push((from, to)); + } + } + } + } } fn is_road(tags: &Tags) -> bool { diff --git a/backend/src/geo_helpers.rs b/backend/src/geo_helpers.rs index b8c2c1f2..62bdd05c 100644 --- a/backend/src/geo_helpers.rs +++ b/backend/src/geo_helpers.rs @@ -214,6 +214,20 @@ pub fn make_arrow(line: Line, thickness: f64, double_ended: bool) -> Option Polygon { + let angle = angle_of_line(line); + Polygon::new( + LineString::new(vec![ + euclidean_destination_coord(line.start, angle - 90.0, thickness * 0.5), + euclidean_destination_coord(line.end, angle - 90.0, thickness * 0.5), + euclidean_destination_coord(line.end, angle + 90.0, thickness * 0.5), + euclidean_destination_coord(line.start, angle + 90.0, thickness * 0.5), + euclidean_destination_coord(line.start, angle - 90.0, thickness * 0.5), + ]), + Vec::new(), + ) +} + /// Create a polygon covering the world, minus a hole for the input polygon. Assumes the input is /// in WGS84 and has no holes itself. pub fn invert_polygon(wgs84_polygon: Polygon) -> Polygon { diff --git a/backend/src/lib.rs b/backend/src/lib.rs index 7a715e47..6024a61a 100644 --- a/backend/src/lib.rs +++ b/backend/src/lib.rs @@ -25,6 +25,7 @@ mod create; mod geo_helpers; mod impact; mod map_model; +mod movements; mod neighbourhood; mod render_cells; mod route; @@ -326,7 +327,11 @@ impl LTN { self.map .intersections .iter() - .map(|i| self.map.mercator.to_wgs84_gj(&i.point)) + .map(|i| { + let mut f = self.map.mercator.to_wgs84_gj(&i.point); + f.set_property("has_turn_restrictions", !i.turn_restrictions.is_empty()); + f + }) .collect::>(), )) .map_err(err_to_js)?) diff --git a/backend/src/map_model.rs b/backend/src/map_model.rs index 3b36a089..e0a1daa7 100644 --- a/backend/src/map_model.rs +++ b/backend/src/map_model.rs @@ -86,10 +86,11 @@ pub struct Road { pub struct Intersection { pub id: IntersectionID, - #[allow(unused)] pub node: osm_reader::NodeID, pub point: Point, pub roads: Vec, + /// (from, to) is not allowed. May be redundant with the road directions. + pub turn_restrictions: Vec<(RoadID, RoadID)>, } impl MapModel { @@ -581,17 +582,6 @@ impl MapModel { } directions } - - pub fn get_movements(&self, i: IntersectionID) -> GeoJson { - let mut features = Vec::new(); - - // TODO Temporary - for r in &self.get_i(i).roads { - features.push(self.mercator.to_wgs84_gj(&self.get_r(*r).linestring)); - } - - GeoJson::from(features) - } } impl Road { diff --git a/backend/src/movements.rs b/backend/src/movements.rs new file mode 100644 index 00000000..8ac7931d --- /dev/null +++ b/backend/src/movements.rs @@ -0,0 +1,80 @@ +use geo::{Euclidean, Length, Line, LineInterpolatePoint, Point, Polygon}; +use geojson::GeoJson; + +use crate::{ + geo_helpers::{make_arrow, thicken_line}, + Direction, IntersectionID, MapModel, Road, +}; + +impl MapModel { + pub fn get_movements(&self, i: IntersectionID) -> GeoJson { + let mut features = Vec::new(); + + let intersection = self.get_i(i); + for r1 in &intersection.roads { + for r2 in &intersection.roads { + // TODO Handle u-turns at dead-ends + if r1 == r2 { + continue; + } + let road1 = self.get_r(*r1); + let road2 = self.get_r(*r2); + + // If road1 is one-way, can we go towards i? + let ok1 = match self.directions[r1] { + Direction::BothWays => true, + Direction::Forwards => road1.dst_i == i, + Direction::Backwards => road1.src_i == i, + }; + // If road2 is one-way, can we go away from i? + let ok2 = match self.directions[r2] { + Direction::BothWays => true, + Direction::Forwards => road2.src_i == i, + Direction::Backwards => road2.dst_i == i, + }; + if !ok1 || !ok2 { + continue; + } + + // Is there a turn restriction between this pair? + if intersection.turn_restrictions.contains(&(*r1, *r2)) { + continue; + } + + let polygon = render_arrow(i, road1, road2); + features.push(self.mercator.to_wgs84_gj(&polygon)); + } + } + + GeoJson::from(features) + } +} + +fn render_arrow(i: IntersectionID, road1: &Road, road2: &Road) -> Polygon { + let line = Line::new( + pt_near_intersection(i, road1), + pt_near_intersection(i, road2), + ); + let thickness = 2.0; + let double_ended = false; + make_arrow(line, thickness, double_ended).unwrap_or_else(|| thicken_line(line, thickness)) +} + +fn pt_near_intersection(i: IntersectionID, road: &Road) -> Point { + // If the road is long enough, offset from the intersection this much + let distance_away = 10.0; + let len = road.linestring.length::(); + + if len > distance_away { + let pct = if road.src_i == i { + distance_away / len + } else { + 1.0 - (distance_away / len) + }; + return road.linestring.line_interpolate_point(pct).unwrap(); + } + + // If not, just take the other endpoint + let pct = if road.src_i == i { 1.0 } else { 0.0 }; + road.linestring.line_interpolate_point(pct).unwrap() +} diff --git a/web/src/DebugIntersectionsMode.svelte b/web/src/DebugIntersectionsMode.svelte index 9395b02e..128bcc9d 100644 --- a/web/src/DebugIntersectionsMode.svelte +++ b/web/src/DebugIntersectionsMode.svelte @@ -1,6 +1,7 @@ @@ -28,6 +31,11 @@ Choose project +
  • + ($mode = { mode: "network" })}> + Pick neighbourhood + +
  • Debug intersections
  • @@ -36,11 +44,14 @@
    ($mode = { mode: "network" })} /> +

    Purple intersections have some kind of turn restriction.

    + {#if movements.features.length > 0} -

    {movements.features.length} movements

    + + {/if}
    @@ -50,7 +61,12 @@ {...layerId("debug-intersections")} paint={{ "circle-radius": 15, - "circle-color": "black", + "circle-color": [ + "case", + ["get", "has_turn_restrictions"], + "purple", + "black", + ], }} manageHoverState hoverCursor="pointer" @@ -60,12 +76,20 @@ + + diff --git a/web/src/ImpactDetailMode.svelte b/web/src/ImpactDetailMode.svelte index e110d463..cb092bca 100644 --- a/web/src/ImpactDetailMode.svelte +++ b/web/src/ImpactDetailMode.svelte @@ -1,12 +1,11 @@ - -