Skip to content

Commit

Permalink
Start representing and extracting explicit stop lines, with many
Browse files Browse the repository at this point in the history
problems. #165
  • Loading branch information
dabreegster committed Jan 4, 2023
1 parent 2b89cc1 commit b0b40da
Show file tree
Hide file tree
Showing 11 changed files with 210 additions and 29 deletions.
4 changes: 3 additions & 1 deletion osm2streets/src/geometry/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(),
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion osm2streets/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down
1 change: 1 addition & 0 deletions osm2streets/src/operations/collapse_short_road.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
67 changes: 65 additions & 2 deletions osm2streets/src/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
41 changes: 39 additions & 2 deletions osm2streets/src/road.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,42 @@ pub struct Road {
pub complicated_turn_restrictions: Vec<(RoadID, RoadID)>,

pub lane_specs_ltr: Vec<LaneSpec>,

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<Distance>,
/// 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<Distance>,
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 {
Expand Down Expand Up @@ -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);
Expand Down
10 changes: 0 additions & 10 deletions osm2streets/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
2 changes: 2 additions & 0 deletions street-explorer/js/layers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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), {
Expand Down
38 changes: 30 additions & 8 deletions streets_reader/src/extract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,37 +13,51 @@ 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<Pt2D>, Tags)>,
/// Traffic signals to the direction they apply
pub traffic_signals: HashMap<HashablePt2D, Direction>,
pub osm_node_ids: HashMap<HashablePt2D, NodeID>,
/// (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<HashablePt2D, Option<Direction>>,
pub cycleway_stop_lines: Vec<(HashablePt2D, Option<Direction>)>,
/// Pedestrian crossings with a traffic signal, with unknown direction
pub signalized_crossings: Vec<HashablePt2D>,
}

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(),
}
}

pub fn handle_node(&mut self, id: NodeID, node: &Node) {
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
Expand Down Expand Up @@ -190,3 +204,11 @@ impl OsmExtract {
true
}
}

fn parse_dir(x: Option<&String>) -> Option<Direction> {
match x.map(|x| x.as_str()) {
Some("forward") => Some(Direction::Fwd),
Some("backward") => Some(Direction::Back),
_ => None,
}
}
70 changes: 67 additions & 3 deletions streets_reader/src/split_ways.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion tests/src/bristol_contraflow_cycleway/geometry.json
Original file line number Diff line number Diff line change
Expand Up @@ -2263,7 +2263,7 @@
"type": "Polygon"
},
"properties": {
"control": "Signalled",
"control": "Signed",
"id": 21,
"intersection_kind": "Connection",
"movements": [
Expand Down
Loading

0 comments on commit b0b40da

Please sign in to comment.