diff --git a/osm2streets-js/src/lib.rs b/osm2streets-js/src/lib.rs index 870588eb..6fc91b40 100644 --- a/osm2streets-js/src/lib.rs +++ b/osm2streets-js/src/lib.rs @@ -14,7 +14,7 @@ use osm2streets::{ pub struct ImportOptions { debug_each_step: bool, dual_carriageway_experiment: bool, - cycletrack_snapping_experiment: bool, + sidepath_zipping_experiment: bool, inferred_sidewalks: bool, osm2lanes: bool, } @@ -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); } diff --git a/osm2streets/src/lib.rs b/osm2streets/src/lib.rs index a922bd33..54a2aa49 100644 --- a/osm2streets/src/lib.rs +++ b/osm2streets/src/lib.rs @@ -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}; diff --git a/osm2streets/src/operations/mod.rs b/osm2streets/src/operations/mod.rs index 8c174c75..6c9dba2b 100644 --- a/osm2streets/src/operations/mod.rs +++ b/osm2streets/src/operations/mod.rs @@ -3,3 +3,4 @@ mod collapse_intersection; mod collapse_short_road; mod update_geometry; +pub mod zip_sidepath; diff --git a/osm2streets/src/operations/zip_sidepath.rs b/osm2streets/src/operations/zip_sidepath.rs new file mode 100644 index 00000000..7cafd47f --- /dev/null +++ b/osm2streets/src/operations/zip_sidepath.rs @@ -0,0 +1,248 @@ +use std::collections::BTreeSet; + +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, + connector_src_i: Option, + connector_dst_i: Option, +} + +impl Sidepath { + pub fn new(streets: &StreetNetwork, start: RoadID) -> Option { + 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.id != sidepath_road.id && road.center_line.length() < SHORT_ROAD_THRESHOLD { + connector_candidates.push(road.id); + } + } + if connector_candidates.len() == 1 { + let connector = connector_candidates[0]; + main_road_endpoints + .push((streets.roads[&connector].other_side(i), Some(connector))); + } else if connector_candidates.is_empty() { + // Maybe this intersection has been merged already. Use it directly. + main_road_endpoints.push((i, None)); + } + } + + 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, connector_src_i) = main_road_endpoints[0]; + let (main_road_dst_i, connector_dst_i) = main_road_endpoints[1]; + // It may be none at all, when the main road intersection gets merged + if main_road_src_i == main_road_dst_i { + return None; + } + + // 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(), + connector_src_i, + connector_dst_i, + }); + } + + 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}")); + } + if let Some(r) = self.connector_src_i { + streets.debug_road(r, format!("src_i connector of {label}")); + } + if let Some(r) = self.connector_dst_i { + streets.debug_road(r, format!("dst_i connector of {label}")); + } + } + + pub fn zip(self, streets: &mut StreetNetwork) { + // The caller may find a bunch of these and zip, which sometimes could delete one of the + // located pieces + if !streets.roads.contains_key(&self.sidepath) { + return; + } + + // 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/cycleway: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 + let mut intersections = BTreeSet::new(); + 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); + + intersections.extend(main_road.endpoints()); + } + + // Recalculate geometry along all of the main roads we just thickened + for i in intersections { + streets.update_i(i); + } + + // 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. + // + // ALTERNATIVELY, remove the connector segments immediately. + if let Some(r) = self.connector_src_i { + if streets.roads.contains_key(&r) { + // Ignore errors + let _ = streets.collapse_short_road(r); + } + } + if let Some(r) = self.connector_dst_i { + if streets.roads.contains_key(&r) { + let _ = streets.collapse_short_road(r); + } + } + } +} + +// Insert all of `insert` at `idx` in `target` +fn splice_in(target: &mut Vec, idx: usize, insert: Vec) { + let tail = target.split_off(idx); + target.extend(insert); + target.extend(tail); +} diff --git a/osm2streets/src/transform/mod.rs b/osm2streets/src/transform/mod.rs index 628e544d..aa186fab 100644 --- a/osm2streets/src/transform/mod.rs +++ b/osm2streets/src/transform/mod.rs @@ -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, @@ -43,9 +43,9 @@ 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 - // easier to do after trimming some dead-ends. So... just run it twice. + Transformation::ZipSidepaths, + // More dead-ends can be created after zipping. But also, zipping can be easier to + // do after trimming some dead-ends. So... just run it twice. Transformation::TrimDeadendCycleways, Transformation::RemoveDisconnectedRoads, ]; @@ -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", @@ -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); diff --git a/osm2streets/src/transform/parallel_sidepaths.rs b/osm2streets/src/transform/parallel_sidepaths.rs new file mode 100644 index 00000000..9286fbe9 --- /dev/null +++ b/osm2streets/src/transform/parallel_sidepaths.rs @@ -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); + } +} diff --git a/osm2streets/src/transform/separate_cycletracks.rs b/osm2streets/src/transform/separate_cycletracks.rs deleted file mode 100644 index a30d7cde..00000000 --- a/osm2streets/src/transform/separate_cycletracks.rs +++ /dev/null @@ -1,265 +0,0 @@ -use std::collections::BTreeSet; - -use geom::{Distance, PolyLine}; - -use crate::{BufferType, Direction, IntersectionID, LaneSpec, LaneType, RoadID, StreetNetwork}; - -/// Find cycleway segments that exist as separate objects, parallel to a main road. Merge (or -/// "snap") them into the main road, inserting a buffer lane to represent the physical division. -pub fn snap_cycleways(streets: &mut StreetNetwork) { - for cycleway in find_cycleways(streets) { - streets.maybe_start_debug_step(format!("snap cycleway {}", cycleway.debug_idx)); - cycleway.debug(streets); - snap(streets, cycleway); - } -} - -// We're only pattern matching on one type of separate cycleway right now. This represents a single -// Road that's parallel to one or more main_roads. -// -// X--X -// C M -// C M -// C M -// C X -// C M -// C M -// X--X -// -// C is the cycleway segment. X are intersections. M are main roads -- note there are two matching -// up to this one cycleway. The '-'s are short connector roads between the two. -struct Cycleway { - cycleway: RoadID, - cycleway_center: PolyLine, - // Just to distinguish different cycleways when debugging - debug_idx: usize, - main_road_src_i: IntersectionID, - main_road_dst_i: IntersectionID, - main_roads: Vec, - connector_src_i: Option, - connector_dst_i: Option, -} - -impl Cycleway { - fn debug(&self, streets: &mut StreetNetwork) { - streets.debug_road(self.cycleway, format!("cycleway {}", self.debug_idx)); - streets.debug_intersection(self.main_road_src_i, format!("src_i of {}", self.debug_idx)); - streets.debug_intersection(self.main_road_dst_i, format!("dst_i of {}", self.debug_idx)); - for x in &self.main_roads { - streets.debug_road(*x, format!("main road along {}", self.debug_idx)); - } - if let Some(r) = self.connector_src_i { - streets.debug_road(r, format!("src_i connector of {}", self.debug_idx)); - } - if let Some(r) = self.connector_dst_i { - streets.debug_road(r, format!("dst_i connector of {}", self.debug_idx)); - } - } -} - -fn find_cycleways(streets: &StreetNetwork) -> Vec { - const SHORT_ROAD_THRESHOLD: Distance = Distance::const_meters(10.0); - - let mut cycleways = Vec::new(); - for cycleway_road in streets.roads.values() { - if cycleway_road.is_cycleway() { - // 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 cycleway_road.endpoints() { - let mut connector_candidates = Vec::new(); - for road in streets.roads_per_intersection(i) { - if road.id != cycleway_road.id - && road.center_line.length() < SHORT_ROAD_THRESHOLD - { - connector_candidates.push(road.id); - } - } - if connector_candidates.len() == 1 { - let connector = connector_candidates[0]; - main_road_endpoints - .push((streets.roads[&connector].other_side(i), Some(connector))); - } else if connector_candidates.is_empty() { - // Maybe this intersection has been merged already. Use it directly. - main_road_endpoints.push((i, None)); - } - } - - if main_road_endpoints.len() == 2 { - // Often the main road parallel to this cycleway segment is just one road, but it - // might be more. - let (main_road_src_i, connector_src_i) = main_road_endpoints[0]; - let (main_road_dst_i, connector_dst_i) = main_road_endpoints[1]; - // It may be none at all, when the main road intersection gets merged - if main_road_src_i == main_road_dst_i { - continue; - } - // Find all main road segments "parallel to" this cycleway, 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]) - }) - { - cycleways.push(Cycleway { - cycleway: cycleway_road.id, - cycleway_center: cycleway_road.center_line.clone(), - debug_idx: cycleways.len(), - main_road_src_i, - main_road_dst_i, - main_roads: path.into_iter().map(|(r, _)| r).collect(), - connector_src_i, - connector_dst_i, - }); - } - } - } - } - cycleways -} - -fn snap(streets: &mut StreetNetwork, input: Cycleway) { - // This analysis shouldn't modify other cycleways when it works on one - //assert!(streets.roads.contains_key(&input.cycleway)); - // TODO Not true anymore; sometimes the short connector segment is a cycleway and matches stuff - // here incorrectly, but then a previous pass deletes it - if !streets.roads.contains_key(&input.cycleway) { - return; - } - - // Remove the cycleway, but remember the lanes it contained - let mut cycleway_lanes = streets.remove_road(input.cycleway).lane_specs_ltr; - - // TODO Preserve osm_ids - - // The cycleway 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 cycleway_lanes[0].lt == LaneType::Shoulder { - cycleway_lanes.remove(0); - } - if cycleway_lanes.last().as_ref().unwrap().lt == LaneType::Shoulder { - cycleway_lanes.pop(); - } - - // The cycleway 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/cycleway: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 cycleway, we need to insert these - // cycleway_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 - let mut intersections = BTreeSet::new(); - for r in input.main_roads { - let main_road = &streets.roads[&r]; - // Which side is closer to the cycleway? - 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 = input.cycleway_center.middle().dist_to(left.middle()) - < input.cycleway_center.middle().dist_to(right.middle()); - - // Does the cycleway 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 = input - .cycleway_center - .overall_angle() - .approx_eq(left.overall_angle(), 90.0); - - // Where should we insert the cycleway 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 cycleway_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); - - intersections.extend(main_road.endpoints()); - } - - // Recalculate geometry along all of the main roads we just thickened - for i in intersections { - streets.update_i(i); - } - - // After this transformation, we should run CollapseDegenerateIntersections to handle the - // intersection where the side road originally crossed the cycleway, and TrimDeadendCycleways - // to clean up any small cycle connection roads. - // - // ALTERNATIVELY, remove the connector segments immediately. - if let Some(r) = input.connector_src_i { - if streets.roads.contains_key(&r) { - // Ignore errors - let _ = streets.collapse_short_road(r); - } - } - if let Some(r) = input.connector_dst_i { - if streets.roads.contains_key(&r) { - let _ = streets.collapse_short_road(r); - } - } -} - -// Insert all of `insert` at `idx` in `target` -fn splice_in(target: &mut Vec, idx: usize, insert: Vec) { - let tail = target.split_off(idx); - target.extend(insert); - target.extend(tail); -} diff --git a/street-explorer/index.html b/street-explorer/index.html index bf0c209d..53b87897 100644 --- a/street-explorer/index.html +++ b/street-explorer/index.html @@ -45,8 +45,8 @@

StreetExplorer

dual carriageway experiment