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 reviving the cycleway snapping / sidepath zipping experiment #212

Merged
merged 4 commits into from
Mar 10, 2023
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
17 changes: 12 additions & 5 deletions osm2streets-js/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@ use serde::{Deserialize, Serialize};
use wasm_bindgen::prelude::*;

use osm2streets::{
osm, DebugStreets, IntersectionID, LaneID, MapConfig, Placement, RoadID, StreetNetwork,
Transformation,
osm, DebugStreets, IntersectionID, LaneID, MapConfig, Placement, RoadID, Sidepath,
StreetNetwork, Transformation,
};

#[derive(Serialize, Deserialize)]
pub struct ImportOptions {
debug_each_step: bool,
dual_carriageway_experiment: bool,
cycletrack_snapping_experiment: bool,
sidepath_zipping_experiment: bool,
inferred_sidewalks: bool,
osm2lanes: bool,
}
Expand Down Expand Up @@ -67,8 +67,8 @@ impl JsStreetNetwork {
transformations.retain(|t| !matches!(t, Transformation::CollapseShortRoads));
transformations.push(Transformation::MergeDualCarriageways);
}
if input.cycletrack_snapping_experiment {
transformations.push(Transformation::SnapCycleways);
if input.sidepath_zipping_experiment {
transformations.push(Transformation::ZipSidepaths);
transformations.push(Transformation::TrimDeadendCycleways);
transformations.push(Transformation::CollapseDegenerateIntersections);
}
Expand Down Expand Up @@ -256,6 +256,13 @@ impl JsStreetNetwork {
self.inner.collapse_intersection(i);
}
}

#[wasm_bindgen(js_name = zipSidepath)]
pub fn zip_sidepath(&mut self, road: usize) {
if let Some(sidepath) = Sidepath::new(&self.inner, RoadID(road)) {
sidepath.zip(&mut self.inner);
}
}
}

#[wasm_bindgen]
Expand Down
1 change: 1 addition & 0 deletions osm2streets/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ pub use self::lanes::{
get_lane_specs_ltr, BufferType, Direction, LaneSpec, LaneType, Placement,
NORMAL_LANE_THICKNESS, SIDEWALK_THICKNESS,
};
pub use self::operations::zip_sidepath::Sidepath;
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/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
mod collapse_intersection;
mod collapse_short_road;
mod update_geometry;
pub mod zip_sidepath;
202 changes: 202 additions & 0 deletions osm2streets/src/operations/zip_sidepath.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
use geom::{Distance, PolyLine};

use crate::{BufferType, Direction, IntersectionID, LaneSpec, LaneType, RoadID, StreetNetwork};

// We're only pattern matching on one type of parallel sidepath right now. This represents a single
// Road that's parallel to one or more main_roads.
//
// X--X
// S M
// S M
// S M
// S X
// S M
// S M
// X--X
//
// S is the sidepath segment. X are intersections. M are main roads -- note there are two matching
// up to this one sidepath. The '-'s are short connector roads between the two.
pub struct Sidepath {
sidepath: RoadID,
sidepath_center: PolyLine,
main_road_src_i: IntersectionID,
main_road_dst_i: IntersectionID,
main_roads: Vec<RoadID>,
}

impl Sidepath {
pub fn new(streets: &StreetNetwork, start: RoadID) -> Option<Self> {
const SHORT_ROAD_THRESHOLD: Distance = Distance::const_meters(10.0);

let sidepath_road = &streets.roads[&start];

// Look at other roads connected to both endpoints. One of them should be "very short."
let mut main_road_endpoints = Vec::new();
for i in sidepath_road.endpoints() {
let mut connector_candidates = Vec::new();
for road in streets.roads_per_intersection(i) {
if road.untrimmed_length() < SHORT_ROAD_THRESHOLD {
connector_candidates.push(road.id);
}
}
if connector_candidates.len() == 1 {
main_road_endpoints.push(streets.roads[&connector_candidates[0]].other_side(i));
}
}

if main_road_endpoints.len() != 2 {
return None;
}

// Often the main road parallel to this sidepath segment is just one road, but it might
// be more.
let main_road_src_i = main_road_endpoints[0];
let main_road_dst_i = main_road_endpoints[1];

// Find all main road segments "parallel to" this sidepath, by pathfinding between the
// main road intersections. We don't care about the order, but simple_path does. In
// case it's one-way for driving, try both.
if let Some(path) = streets
.simple_path(main_road_src_i, main_road_dst_i, &[LaneType::Driving])
.or_else(|| streets.simple_path(main_road_dst_i, main_road_src_i, &[LaneType::Driving]))
{
return Some(Self {
sidepath: sidepath_road.id,
sidepath_center: sidepath_road.center_line.clone(),
main_road_src_i,
main_road_dst_i,
main_roads: path.into_iter().map(|(r, _)| r).collect(),
});
}

None
}

pub fn debug(&self, streets: &mut StreetNetwork, label: String) {
streets.debug_road(self.sidepath, format!("sidepath {label}"));
streets.debug_intersection(self.main_road_src_i, format!("src_i of {label}"));
streets.debug_intersection(self.main_road_dst_i, format!("dst_i of {label}"));
for x in &self.main_roads {
streets.debug_road(*x, format!("main road along {label}"));
}
}

pub fn zip(self, streets: &mut StreetNetwork) {
assert!(streets.roads.contains_key(&self.sidepath));

// Remove the sidepath, but remember the lanes it contained
let mut sidepath_lanes = streets.remove_road(self.sidepath).lane_specs_ltr;

// TODO Preserve osm_ids

// TODO Re-evaluate this!
// The sidepath likely had shoulder lanes assigned to it by get_lane_specs_ltr, because we have
// many partially competing strategies for representing shared walking/cycling roads. Remove
// those.
if sidepath_lanes[0].lt == LaneType::Shoulder {
sidepath_lanes.remove(0);
}
if sidepath_lanes.last().as_ref().unwrap().lt == LaneType::Shoulder {
sidepath_lanes.pop();
}

// The sidepath was tagged as a separate way due to some kind of physical separation. We'll
// represent that with a buffer lane.
let buffer = LaneSpec {
// TODO Use https://wiki.openstreetmap.org/wiki/Proposed_features/separation if
// available
lt: LaneType::Buffer(BufferType::Planters),
dir: Direction::Fwd,
width: LaneSpec::typical_lane_width(LaneType::Buffer(BufferType::Planters)),
allowed_turns: Default::default(),
};

// For every main road segment corresponding to the sidepath, we need to insert these
// sidepath_lanes somewhere.
//
// - Fixing the direction of the lanes
// - Appending them on the left or right side (and "inside" the inferred sidewalk on the road)
// - Inserting the buffer
for r in self.main_roads {
let main_road = &streets.roads[&r];
// Which side is closer to the sidepath?
let (left, right) = main_road
.get_untrimmed_sides(streets.config.driving_side)
.unwrap();
// TODO georust has a way to check distance of linestrings. But for now, just check the
// middles
let snap_to_left = self.sidepath_center.middle().dist_to(left.middle())
< self.sidepath_center.middle().dist_to(right.middle());

// Does the sidepath point the same direction as this main road? We can use the left or
// right side, doesn't matter.
// TODO Check this logic very carefully; angles always lead to bugs. 90 is a very generous
// definition of parallel. But we have a binary decision to make, so maybe we should even
// use 180.
let oriented_same_way = self
.sidepath_center
.overall_angle()
.approx_eq(left.overall_angle(), 90.0);

// Where should we insert the sidepath lanes? If the main road already has a sidewalk,
// let's assume it should stay at the outermost part of the road. (That isn't always true,
// but it's an assumption we'll take for now.)
let insert_idx = if snap_to_left {
if main_road.lane_specs_ltr[0].lt.is_walkable() {
1
} else {
0
}
} else {
if main_road
.lane_specs_ltr
.last()
.as_ref()
.unwrap()
.lt
.is_walkable()
{
main_road.lane_specs_ltr.len() - 1
} else {
main_road.lane_specs_ltr.len()
}
};

streets.debug_road(r, format!("snap_to_left = {snap_to_left}, oriented_same_way = {oriented_same_way}, insert_idx = {insert_idx}"));

// This logic thankfully doesn't depend on driving side at all!
let mut insert_lanes = Vec::new();
for mut lane in sidepath_lanes.clone() {
if !oriented_same_way {
lane.dir = lane.dir.opposite();
}
insert_lanes.push(lane);
}
// TODO Do we ever need to reverse the order of the lanes?
let mut buffer_lane = buffer.clone();
if snap_to_left {
// TODO I'm not sure what direction the buffer lane should face. This is a very strong
// argument for Direction::Both.
buffer_lane.dir = insert_lanes.last().as_ref().unwrap().dir;
insert_lanes.push(buffer_lane);
} else {
buffer_lane.dir = insert_lanes[0].dir;
insert_lanes.insert(0, buffer_lane);
}

let main_road = streets.roads.get_mut(&r).unwrap();
splice_in(&mut main_road.lane_specs_ltr, insert_idx, insert_lanes);
}

// After this transformation, we should run CollapseDegenerateIntersections to handle the
// intersection where the side road originally crossed the sidepath, and TrimDeadendCycleways
// to clean up any small cycle connection roads.
}
}

// Insert all of `insert` at `idx` in `target`
fn splice_in<T>(target: &mut Vec<T>, idx: usize, insert: Vec<T>) {
let tail = target.split_off(idx);
target.extend(insert);
target.extend(tail);
}
2 changes: 1 addition & 1 deletion osm2streets/src/road.rs
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ impl Road {
for spec in &self.lane_specs_ltr {
if spec.lt == LaneType::Biking {
bike = true;
} else if spec.lt != LaneType::Shoulder {
} else if !spec.lt.is_walkable() {
return false;
}
}
Expand Down
4 changes: 0 additions & 4 deletions osm2streets/src/transform/collapse_intersections.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,6 @@ fn should_collapse(road1: &Road, road2: &Road) -> Result<()> {
_ => bail!("one of the placements isn't consistent"),
}

if road1.is_cycleway() && road2.is_cycleway() {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This had no effect. Even if it was false, we would choose to collapse.

return Ok(());
}

Ok(())
}

Expand Down
14 changes: 7 additions & 7 deletions osm2streets/src/transform/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ use crate::StreetNetwork;
mod collapse_intersections;
mod collapse_short_road;
mod dual_carriageways;
mod parallel_sidepaths;
mod remove_disconnected;
mod sausage_links;
mod separate_cycletracks;

/// An in-place transformation of a `StreetNetwork`.
pub enum Transformation {
TrimDeadendCycleways,
SnapCycleways,
ZipSidepaths,
RemoveDisconnectedRoads,
CollapseShortRoads,
CollapseDegenerateIntersections,
Expand Down Expand Up @@ -43,8 +43,8 @@ impl Transformation {
// Not working yet
if false {
let mut prepend = vec![
Transformation::SnapCycleways,
// More dead-ends can be created after snapping cycleways. But also, snapping can be
Transformation::ZipSidepaths,
// More dead-ends can be created after zipping sidepaths. But also, zipping can be
// easier to do after trimming some dead-ends. So... just run it twice.
Transformation::TrimDeadendCycleways,
Transformation::RemoveDisconnectedRoads,
Expand All @@ -61,7 +61,7 @@ impl Transformation {
fn name(&self) -> &'static str {
match self {
Transformation::TrimDeadendCycleways => "trim dead-end cycleways",
Transformation::SnapCycleways => "snap separate cycleways",
Transformation::ZipSidepaths => "zip parallel sidepaths",
Transformation::RemoveDisconnectedRoads => "remove disconnected roads",
Transformation::CollapseShortRoads => "collapse short roads",
Transformation::CollapseDegenerateIntersections => "collapse degenerate intersections",
Expand All @@ -76,8 +76,8 @@ impl Transformation {
Transformation::TrimDeadendCycleways => {
collapse_intersections::trim_deadends(streets);
}
Transformation::SnapCycleways => {
separate_cycletracks::snap_cycleways(streets);
Transformation::ZipSidepaths => {
parallel_sidepaths::zip_sidepaths(streets);
}
Transformation::RemoveDisconnectedRoads => {
remove_disconnected::remove_disconnected_roads(streets);
Expand Down
19 changes: 19 additions & 0 deletions osm2streets/src/transform/parallel_sidepaths.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
use crate::{Sidepath, StreetNetwork};

/// Find sidepath segments that exist as separate objects, parallel to a main road. Zip (or "snap")
/// them into the main road, inserting a buffer lane to represent the physical division.
pub fn zip_sidepaths(streets: &mut StreetNetwork) {
let mut sidepaths = Vec::new();
for r in streets.roads.values() {
// TODO Or footpath
if r.is_cycleway() {
sidepaths.extend(Sidepath::new(streets, r.id));
}
}

for (idx, sidepath) in sidepaths.into_iter().enumerate() {
streets.maybe_start_debug_step(format!("snap sidepath {idx}"));
sidepath.debug(streets, idx.to_string());
sidepath.zip(streets);
}
}
Loading