Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Start handling highway=crossing nodes #247

Merged
merged 1 commit into from
Mar 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions osm2streets/src/intersection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ pub struct Intersection {
pub roads: Vec<RoadID>,
pub movements: Vec<Movement>,

pub crossing: Option<Crossing>,

// true if src_i matches this intersection (or the deleted/consolidated one, whatever)
// TODO Store start/end trim distance on _every_ road
#[serde(
Expand Down Expand Up @@ -88,6 +90,25 @@ pub enum IntersectionControl {
Construction,
}

/// When an Intersection is a pedestrian (and/or bike) crossing, represents details.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Crossing {
pub kind: CrossingKind,
/// Is there a pedestrian/traffic island/refuge?
pub has_island: bool,
}

#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
pub enum CrossingKind {
/// Controlled by a traffic signal
Signalized,
/// Often a zebra crossing. The semantics of which road user has priority is region-specific.
Marked,
/// No paint markings, but maybe still a de facto crossing due to a nearby curb cut or an
/// island on this crossing.
Unmarked,
}

/// The path that some group of adjacent lanes of traffic can take through an intersection.
pub type Movement = (RoadID, RoadID);

Expand Down Expand Up @@ -140,6 +161,7 @@ impl StreetNetwork {
// Filled out later
roads: Vec::new(),
movements: Vec::new(),
crossing: None,
trim_roads_for_merging: BTreeMap::new(),
},
);
Expand Down
3 changes: 2 additions & 1 deletion osm2streets/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ pub use self::geometry::{intersection_polygon, InputRoad};
pub(crate) use self::ids::RoadWithEndpoints;
pub use self::ids::{CommonEndpoint, IntersectionID, LaneID, RoadID};
pub use self::intersection::{
Intersection, IntersectionControl, IntersectionKind, Movement, TrafficConflict,
Crossing, CrossingKind, Intersection, IntersectionControl, IntersectionKind, Movement,
TrafficConflict,
};
pub use self::operations::zip_sidepath::Sidepath;
pub use self::render::Filter;
Expand Down
105 changes: 103 additions & 2 deletions osm2streets/src/render/intersection_markings.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
use anyhow::Result;
use geojson::Feature;
use geom::{Polygon, Ring};
use geom::{Distance, Line, PolyLine, Polygon, Ring};

use super::{serialize_features, Filter};
use crate::road::RoadEdge;
use crate::{Intersection, LaneType, StreetNetwork};
use crate::{CrossingKind, Intersection, LaneType, Road, StreetNetwork};

impl StreetNetwork {
pub fn to_intersection_markings_geojson(&self, filter: &Filter) -> Result<String> {
Expand All @@ -15,6 +15,25 @@ impl StreetNetwork {
f.set_property("type", "sidewalk corner");
features.push(f);
}

if let Some(ref crossing) = intersection.crossing {
match crossing.kind {
CrossingKind::Signalized | CrossingKind::Marked => {
for polygon in draw_zebra_crossing(self, intersection) {
let mut f = Feature::from(polygon.to_geojson(Some(&self.gps_bounds)));
f.set_property("type", "marked crossing line");
features.push(f);
}
}
CrossingKind::Unmarked => {
for polygon in draw_unmarked_crossing(self, intersection) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideas for rendering styles welcome, especially for unmarked crossings -- by definition it's quite unclear how to indicate them! Of course these are going to be region-dependent; I'm starting with something simple and we can add configurability later (along with region-specific lane colors, default widths)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably just a void with no road markings would suffice, unless there are regions where it’s normal for a centerline to continue uninterrupted past an unmarked crossing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tough to do today, because osm2streets doesn't know how to continue any road markings into the "intersection polygon" area that gets calculated. That's something @BudgieInWA was working on remedying by detecting "intersections" that're really just merges / forks. I'll keep it in mind as an idea, thank you!

let mut f = Feature::from(polygon.to_geojson(Some(&self.gps_bounds)));
f.set_property("type", "unmarked crossing outline");
features.push(f);
}
}
}
}
}
serialize_features(features)
}
Expand Down Expand Up @@ -109,3 +128,85 @@ fn make_sidewalk_corners(streets: &StreetNetwork, intersection: &Intersection) -
}
results
}

fn get_crossing_line_and_min_width(
streets: &StreetNetwork,
intersection: &Intersection,
) -> Option<(PolyLine, Distance)> {
// Find the pedestrian roads making up the crossing
let mut roads = Vec::new();
for r in &intersection.roads {
let road = &streets.roads[r];
if road.lane_specs_ltr.len() == 1 && road.lane_specs_ltr[0].lt.is_walkable() {
roads.push(road);
}
}
// TODO Look for examples
if roads.len() != 2 {
return None;
}

// Create the line connecting these two roads.
// TODO Subset the reference_lines by trim_start/end to get more detail
let pl = PolyLine::new(vec![
center_line_pointed_at(roads[0], intersection).last_pt(),
center_line_pointed_at(roads[1], intersection).last_pt(),
])
.ok()?;

let width = roads[0].total_width().min(roads[1].total_width());
Some((pl, width))
}

fn draw_zebra_crossing(streets: &StreetNetwork, intersection: &Intersection) -> Vec<Polygon> {
let mut results = Vec::new();
let Some((line, total_width)) = get_crossing_line_and_min_width(streets, intersection) else {
return results;
};

// Pretty arbitrary parameters
let width = 0.8 * total_width;
let thickness = Distance::meters(0.15);
let step_size = 3.0 * thickness;
let buffer_ends = step_size;
for (pt1, angle) in line.step_along(step_size, buffer_ends) {
// Project away an arbitrary amount
let pt2 = pt1.project_away(Distance::meters(1.0), angle);
results.push(perp_line(Line::must_new(pt1, pt2), width).make_polygons(thickness));
}

results
}

fn draw_unmarked_crossing(streets: &StreetNetwork, intersection: &Intersection) -> Vec<Polygon> {
let mut results = Vec::new();
let Some((line, total_width)) = get_crossing_line_and_min_width(streets, intersection) else {
return results;
};

let width = 0.8 * total_width;
let thickness = Distance::meters(0.15);

for shift in [width / 2.0, -width / 2.0] {
if let Ok(pl) = line.shift_either_direction(shift) {
results.push(pl.make_polygons(thickness));
}
}

results
}

fn center_line_pointed_at(road: &Road, intersection: &Intersection) -> PolyLine {
if road.dst_i == intersection.id {
road.center_line.clone()
} else {
road.center_line.reversed()
}
}

// this always does it at pt1
fn perp_line(l: Line, length: Distance) -> Line {
let pt1 = l.shift_right(length / 2.0).pt1();
let pt2 = l.shift_left(length / 2.0).pt1();
Line::must_new(pt1, pt2)
}
1 change: 1 addition & 0 deletions osm2streets/src/render/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ impl StreetNetwork {
);
f.set_property("intersection_kind", format!("{:?}", intersection.kind));
f.set_property("control", format!("{:?}", intersection.control));
f.set_property("crossing", serde_json::to_value(&intersection.crossing)?);
f.set_property(
"movements",
Value::Array(
Expand Down
26 changes: 18 additions & 8 deletions streets_reader/src/extract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::collections::HashMap;
use abstutil::Tags;
use geom::{HashablePt2D, Pt2D};
use osm2streets::osm::{NodeID, OsmID, RelationID, WayID};
use osm2streets::{osm, Direction, RestrictionType};
use osm2streets::{osm, Crossing, CrossingKind, Direction, RestrictionType};

use crate::osm_reader::{Node, Relation, Way};
use crate::MapConfig;
Expand All @@ -22,8 +22,7 @@ pub struct OsmExtract {
/// 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>,
pub crossings: HashMap<HashablePt2D, Crossing>,
}

impl OsmExtract {
Expand All @@ -36,7 +35,7 @@ impl OsmExtract {

traffic_signals: HashMap::new(),
cycleway_stop_lines: Vec::new(),
signalized_crossings: Vec::new(),
crossings: HashMap::new(),
}
}

Expand All @@ -53,10 +52,21 @@ impl OsmExtract {
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());
if node.tags.is("highway", "crossing") || node.tags.is("railway", "crossing") {
let kind = match node.tags.get("crossing").map(|x| x.as_str()) {
Some("traffic_signals") => CrossingKind::Signalized,
Some("uncontrolled" | "marked") => CrossingKind::Marked,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link

@1ec5 1ec5 Mar 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kind of. The history around crossing classification is quite muddled and quickly devolves into a blame game. Some mappers strongly believe uncontrolled should mean marked/signposted/unsignalized. Others point out that, in some regions such as the UK and U.S., uncontrolled was historically largely used on unmarked crossings, due to plain language in both dialects of English. I think it’s fair to say that the former faction is winning, but only because the latter has moved on to crossing:markings=* and crossing:signals=*. At this point, a new renderer or router would be better off supporting crossing:markings=* and crossing:signals=*, which are less ambiguous in practice and cover the unmarked/signalized case.

Some("unmarked") => CrossingKind::Unmarked,
// TODO Look into these cases
_ => CrossingKind::Unmarked,
};
self.crossings.insert(
node.pt.to_hashable(),
Crossing {
kind,
has_island: node.tags.is("crossing:island", "yes"),
},
);
}
}

Expand Down
12 changes: 7 additions & 5 deletions streets_reader/src/split_ways.rs
Original file line number Diff line number Diff line change
Expand Up @@ -286,12 +286,14 @@ pub fn split_up_roads(
}
}

timer.start_iter(
"match signalized crossings",
input.signalized_crossings.len(),
);
for pt in input.signalized_crossings {
timer.start_iter("match crossings", input.crossings.len());
for (pt, crossing) in input.crossings {
timer.next();

if let Some(i) = pt_to_intersection_id.get(&pt) {
streets.intersections.get_mut(&i).unwrap().crossing = Some(crossing);
}

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
Expand Down
Loading