From b0b40daa5cc474830b5e45365c5adfe6c4ae81b5 Mon Sep 17 00:00:00 2001 From: Dustin Carlino Date: Wed, 4 Jan 2023 11:41:58 +0000 Subject: [PATCH] Start representing and extracting explicit stop lines, with many problems. #165 --- osm2streets/src/geometry/mod.rs | 4 +- osm2streets/src/lib.rs | 2 +- .../src/operations/collapse_short_road.rs | 1 + osm2streets/src/render.rs | 67 +++++++++++++++++- osm2streets/src/road.rs | 41 ++++++++++- osm2streets/src/types.rs | 10 --- street-explorer/js/layers.js | 2 + streets_reader/src/extract.rs | 38 +++++++--- streets_reader/src/split_ways.rs | 70 ++++++++++++++++++- .../bristol_contraflow_cycleway/geometry.json | 2 +- tests/src/leeds_cycleway/geometry.json | 2 +- 11 files changed, 210 insertions(+), 29 deletions(-) diff --git a/osm2streets/src/geometry/mod.rs b/osm2streets/src/geometry/mod.rs index 89196aa4..9bd2d972 100644 --- a/osm2streets/src/geometry/mod.rs +++ b/osm2streets/src/geometry/mod.rs @@ -20,7 +20,7 @@ use anyhow::Result; use geom::{Distance, PolyLine, Polygon, Pt2D, Ring}; use crate::road::RoadEdge; -use crate::{IntersectionID, RoadID}; +use crate::{IntersectionID, RoadID, StopLine}; // For anyone considering removing this indirection in the future: it's used to recalculate one or // two intersections at a time in A/B Street's edit mode. Within just this repo, it does seem @@ -75,6 +75,8 @@ impl InputRoad { trim_end: Distance::ZERO, turn_restrictions: Vec::new(), complicated_turn_restrictions: Vec::new(), + stop_line_start: StopLine::dummy(), + stop_line_end: StopLine::dummy(), } } } diff --git a/osm2streets/src/lib.rs b/osm2streets/src/lib.rs index 6bf057ba..93564b31 100644 --- a/osm2streets/src/lib.rs +++ b/osm2streets/src/lib.rs @@ -21,7 +21,7 @@ pub use self::lanes::{ get_lane_specs_ltr, BufferType, Direction, LaneSpec, LaneType, NORMAL_LANE_THICKNESS, SIDEWALK_THICKNESS, }; -pub use self::road::Road; +pub use self::road::{Road, StopLine, TrafficInterruption}; pub use self::transform::Transformation; pub use self::types::{DrivingSide, MapConfig, NamePerLanguage}; diff --git a/osm2streets/src/operations/collapse_short_road.rs b/osm2streets/src/operations/collapse_short_road.rs index e402bbb8..958c7c43 100644 --- a/osm2streets/src/operations/collapse_short_road.rs +++ b/osm2streets/src/operations/collapse_short_road.rs @@ -96,6 +96,7 @@ impl StreetNetwork { // If the intersection types differ, upgrade the surviving interesting. if destroy_i.control == IntersectionControl::Signalled { self.intersections.get_mut(&keep_i).unwrap().control = IntersectionControl::Signalled; + // TODO Propagate to stop lines } // Remember the merge diff --git a/osm2streets/src/render.rs b/osm2streets/src/render.rs index f96518ae..b3aa1750 100644 --- a/osm2streets/src/render.rs +++ b/osm2streets/src/render.rs @@ -8,8 +8,8 @@ use geom::{ArrowCap, Distance, Line, PolyLine, Polygon, Ring}; use crate::road::RoadEdge; use crate::{ - DebugStreets, Direction, DrivingSide, Intersection, IntersectionID, LaneID, LaneType, Movement, - StreetNetwork, + DebugStreets, Direction, DrivingSide, Intersection, IntersectionID, LaneID, LaneSpec, LaneType, + Movement, Road, StreetNetwork, }; impl StreetNetwork { @@ -175,6 +175,16 @@ impl StreetNetwork { } } + // Stop line distances are relative to the direction of the road, not the lane! + for (lane, center) in road.lane_specs_ltr.iter().zip(lane_centers.iter()) { + for (polygon, kind) in draw_stop_lines(lane, center, road) { + pairs.push(( + polygon.to_geojson(gps_bounds), + make_props(&[("type", kind.into())]), + )); + } + } + // Below renderings need lane centers to point in the direction of the lane for (lane, center) in road.lane_specs_ltr.iter().zip(lane_centers.iter_mut()) { if lane.dir == Direction::Back { @@ -491,3 +501,56 @@ fn make_sidewalk_corners(streets: &StreetNetwork, intersection: &Intersection) - } results } + +fn draw_stop_lines( + lane: &LaneSpec, + center: &PolyLine, + road: &Road, +) -> Vec<(Polygon, &'static str)> { + let mut results = Vec::new(); + + if !matches!( + lane.lt, + LaneType::Driving | LaneType::Bus | LaneType::Biking + ) { + return results; + } + let thickness = Distance::meters(0.5); + + let stop_line = if lane.dir == Direction::Fwd { + &road.stop_line_end + } else { + &road.stop_line_start + }; + + // The vehicle line + if let Some(dist) = stop_line.vehicle_distance { + if let Ok((pt, angle)) = center.dist_along(dist) { + results.push(( + Line::must_new( + pt.project_away(lane.width / 2.0, angle.rotate_degs(90.0)), + pt.project_away(lane.width / 2.0, angle.rotate_degs(-90.0)), + ) + .make_polygons(thickness), + "vehicle stop line", + )); + } + } + + if let Some(dist) = stop_line.bike_distance { + if let Ok((pt, angle)) = center.dist_along(dist) { + results.push(( + Line::must_new( + pt.project_away(lane.width / 2.0, angle.rotate_degs(90.0)), + pt.project_away(lane.width / 2.0, angle.rotate_degs(-90.0)), + ) + .make_polygons(thickness), + "bike stop line", + )); + } + } + + // TODO Change the rendering based on interruption too + + results +} diff --git a/osm2streets/src/road.rs b/osm2streets/src/road.rs index ce52f6c7..383ea82c 100644 --- a/osm2streets/src/road.rs +++ b/osm2streets/src/road.rs @@ -54,6 +54,42 @@ pub struct Road { pub complicated_turn_restrictions: Vec<(RoadID, RoadID)>, pub lane_specs_ltr: Vec, + + pub stop_line_start: StopLine, + pub stop_line_end: StopLine, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct StopLine { + /// Relative to the road's reference_line. Stop lines at the start of the road will have low + /// values, and at the end will have values closer to the reference_line's length. This is only + /// set when the stop line is explicitly specified; it's never inferred. + pub vehicle_distance: Option, + /// If there is an advanced stop line for cyclists different than the vehicle position, this + /// specifies it. This must be farther along than the vehicle_distance (smaller for start, + /// larger for end). The bike box covers the interval between the two. + pub bike_distance: Option, + pub interruption: TrafficInterruption, +} + +/// How a lane of travel is interrupted, as it meets another or ends. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] +pub enum TrafficInterruption { + Uninterrupted, + Yield, + Stop, + Signal, + DeadEnd, +} + +impl StopLine { + pub fn dummy() -> Self { + Self { + vehicle_distance: None, + bike_distance: None, + interruption: TrafficInterruption::Uninterrupted, + } + } } impl Road { @@ -107,15 +143,16 @@ impl Road { trim_end: Distance::ZERO, turn_restrictions: Vec::new(), complicated_turn_restrictions: Vec::new(), - lane_specs_ltr, + stop_line_start: StopLine::dummy(), + stop_line_end: StopLine::dummy(), }; result.update_center_line(config.driving_side); // TODO delay this until trim_start and trim_end are calculated result } - /// Calculates and sets the center_line from reference_line, reference_line_placement + /// Calculates and sets the center_line from reference_line and reference_line_placement. /// (and TODO trim_start, trim_end). pub fn update_center_line(&mut self, driving_side: DrivingSide) { self.center_line = self.get_untrimmed_center_line(driving_side); diff --git a/osm2streets/src/types.rs b/osm2streets/src/types.rs index a87a9cce..625c47e5 100644 --- a/osm2streets/src/types.rs +++ b/osm2streets/src/types.rs @@ -94,13 +94,3 @@ pub enum DrivingSide { Right, Left, } - -/// How a lane of travel is interrupted, as it meets another or ends. -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] -pub enum TrafficInterruption { - Uninterrupted, - Yield, - Stop, - Signal, - DeadEnd, -} diff --git a/street-explorer/js/layers.js b/street-explorer/js/layers.js index 30967543..5f9f0a02 100644 --- a/street-explorer/js/layers.js +++ b/street-explorer/js/layers.js @@ -149,6 +149,8 @@ export const makeLaneMarkingsLayer = (text) => { "lane arrow": "white", "buffer edge": "white", "buffer stripe": "white", + "vehicle stop line": "white", + "bike stop line": "green", }; return new L.geoJSON(JSON.parse(text), { diff --git a/streets_reader/src/extract.rs b/streets_reader/src/extract.rs index 93d82477..25eeca52 100644 --- a/streets_reader/src/extract.rs +++ b/streets_reader/src/extract.rs @@ -13,23 +13,30 @@ pub struct OsmExtract { /// Note there may be multiple entries here with the same WayID. Effectively those have been /// partly pre-split. pub roads: Vec<(WayID, Vec, Tags)>, - /// Traffic signals to the direction they apply - pub traffic_signals: HashMap, pub osm_node_ids: HashMap, /// (ID, restriction type, from way ID, via node ID, to way ID) pub simple_turn_restrictions: Vec<(RestrictionType, WayID, NodeID, WayID)>, /// (relation ID, from way ID, via way ID, to way ID) pub complicated_turn_restrictions: Vec<(RelationID, WayID, WayID, WayID)>, + + /// Traffic signals and bike stop lines, with an optional direction they apply to + pub traffic_signals: HashMap>, + pub cycleway_stop_lines: Vec<(HashablePt2D, Option)>, + /// Pedestrian crossings with a traffic signal, with unknown direction + pub signalized_crossings: Vec, } impl OsmExtract { pub fn new() -> Self { Self { roads: Vec::new(), - traffic_signals: HashMap::new(), osm_node_ids: HashMap::new(), simple_turn_restrictions: Vec::new(), complicated_turn_restrictions: Vec::new(), + + traffic_signals: HashMap::new(), + cycleway_stop_lines: Vec::new(), + signalized_crossings: Vec::new(), } } @@ -37,13 +44,20 @@ impl OsmExtract { self.osm_node_ids.insert(node.pt.to_hashable(), id); if node.tags.is(osm::HIGHWAY, "traffic_signals") { - let dir = if node.tags.is("traffic_signals:direction", "backward") { - Direction::Back - } else { - Direction::Fwd - }; + let dir = parse_dir(node.tags.get("traffic_signals:direction")); self.traffic_signals.insert(node.pt.to_hashable(), dir); } + + if node.tags.is("cycleway", "asl") { + let dir = parse_dir(node.tags.get("direction")); + self.cycleway_stop_lines.push((node.pt.to_hashable(), dir)); + } + + // TODO Maybe restricting to traffic_signals is too much. But we definitely don't want to + // use crossing=unmarked to infer stop lines + if node.tags.is("highway", "crossing") && node.tags.is("crossing", "traffic_signals") { + self.signalized_crossings.push(node.pt.to_hashable()); + } } // Returns true if the way was added as a road @@ -190,3 +204,11 @@ impl OsmExtract { true } } + +fn parse_dir(x: Option<&String>) -> Option { + match x.map(|x| x.as_str()) { + Some("forward") => Some(Direction::Fwd), + Some("backward") => Some(Direction::Back), + _ => None, + } +} diff --git a/streets_reader/src/split_ways.rs b/streets_reader/src/split_ways.rs index 4b8f8255..073c25c8 100644 --- a/streets_reader/src/split_ways.rs +++ b/streets_reader/src/split_ways.rs @@ -4,6 +4,7 @@ use abstutil::{Counter, Timer}; use geom::{HashablePt2D, PolyLine, Pt2D}; use osm2streets::{ Direction, IntersectionControl, IntersectionID, IntersectionKind, Road, RoadID, StreetNetwork, + TrafficInterruption, }; use super::OsmExtract; @@ -48,6 +49,7 @@ pub fn split_up_roads( let control = if osm_ids.is_empty() { IntersectionControl::Uncontrolled } else if input.traffic_signals.remove(&hash_pt).is_some() { + // This is a node; don't expect a direction IntersectionControl::Signalled } else { // TODO default to uncontrolled, guess StopSign as a transform @@ -202,24 +204,86 @@ pub fn split_up_roads( for (pt, dir) in input.traffic_signals { if let Some(r) = pt_to_road.get(&pt) { // The road might've crossed the boundary and been clipped - if let Some(road) = streets.roads.get(r) { - // Example: https://www.openstreetmap.org/node/26734224 - if road.highway_type != "construction" { + if let Some(road) = streets.roads.get_mut(r) { + // On a one-way road, specifying direction is redundant, so infer from there too + if let Some(dir) = dir.or_else(|| road.oneway_for_driving()) { + // Update the intersection control type let i = if dir == Direction::Fwd { road.dst_i } else { road.src_i }; let i = streets.intersections.get_mut(&i).unwrap(); + // TODO Maybe we should do this later, as a consequence of TrafficInterruption + // on incoming roads? if !i.is_map_edge() { i.control = IntersectionControl::Signalled; } + + // Specify the explicit vehicle stop line + if let Some((dist, _)) = road.reference_line.dist_along_of_point(pt.to_pt2d()) { + let stop_line = if dir == Direction::Fwd { + &mut road.stop_line_end + } else { + &mut road.stop_line_start + }; + stop_line.vehicle_distance = Some(dist); + stop_line.interruption = TrafficInterruption::Signal; + } + // TODO If dist_along_of_point fails, it's because we smoothed the line. This + // is a great reason to instead just find the closest point on the line and + // then the distance. } + // TODO What should we do more generally with traffic signals on ways that don't + // specify a direction? } } } timer.stop("match traffic signals to intersections"); + // Do the same for cycleway ASLs + for (pt, dir) in input.cycleway_stop_lines { + if let Some(road) = pt_to_road.get(&pt).and_then(|r| streets.roads.get_mut(r)) { + if let Some(dir) = dir { + if let Some((dist, _)) = road.reference_line.dist_along_of_point(pt.to_pt2d()) { + let stop_line = if dir == Direction::Fwd { + &mut road.stop_line_end + } else { + &mut road.stop_line_start + }; + stop_line.bike_distance = Some(dist); + + // Inherit the interruption type from the intersection + let i = if dir == Direction::Fwd { + road.dst_i + } else { + road.src_i + }; + if streets.intersections[&i].control == IntersectionControl::Signalled { + stop_line.interruption = TrafficInterruption::Signal; + } + } + } + } + } + + for pt in input.signalized_crossings { + if let Some(road) = pt_to_road.get(&pt).and_then(|r| streets.roads.get_mut(r)) { + if let Some((dist, _)) = road.reference_line.dist_along_of_point(pt.to_pt2d()) { + // We don't know the direction. Arbitrarily snap to the start or end if it's within + // 30% of the length. If it's in the middle 40%, it might be a mid-block crossing? + let pct = dist / road.reference_line.length(); + if pct < 0.3 { + road.stop_line_start.vehicle_distance = Some(dist); + road.stop_line_start.interruption = TrafficInterruption::Signal; + } else if pct > 0.7 { + road.stop_line_end.vehicle_distance = Some(dist); + road.stop_line_end.interruption = TrafficInterruption::Signal; + } + } + } + } + timer.start("calculate intersection movements"); let intersection_ids: Vec<_> = streets.intersections.keys().cloned().collect(); for i in intersection_ids { diff --git a/tests/src/bristol_contraflow_cycleway/geometry.json b/tests/src/bristol_contraflow_cycleway/geometry.json index bebea314..15fe90ac 100644 --- a/tests/src/bristol_contraflow_cycleway/geometry.json +++ b/tests/src/bristol_contraflow_cycleway/geometry.json @@ -2263,7 +2263,7 @@ "type": "Polygon" }, "properties": { - "control": "Signalled", + "control": "Signed", "id": 21, "intersection_kind": "Connection", "movements": [ diff --git a/tests/src/leeds_cycleway/geometry.json b/tests/src/leeds_cycleway/geometry.json index 9886da1e..4bb95850 100644 --- a/tests/src/leeds_cycleway/geometry.json +++ b/tests/src/leeds_cycleway/geometry.json @@ -25741,7 +25741,7 @@ "type": "Polygon" }, "properties": { - "control": "Signalled", + "control": "Signed", "id": 185, "intersection_kind": "Connection", "movements": [