diff --git a/backend/src/crossings.rs b/backend/src/crossings.rs index 3a5af50..fa01189 100644 --- a/backend/src/crossings.rs +++ b/backend/src/crossings.rs @@ -12,7 +12,16 @@ impl Speedwalk { pub fn connect_all_crossings(&self, include_crossing_no: bool) -> CreateNewGeometry { info!("Finding crossings to connect"); let mut crossings = Vec::new(); - for (id, node) in &self.derived_nodes { + let mut half_crossings = Vec::new(); + for (crossing_id, node) in &self.derived_nodes { + if !(node.is_crossing() || (include_crossing_no && node.is_explicit_crossing_no())) { + continue; + } + let ways = node + .way_ids + .iter() + .map(|w| &self.derived_ways[w]) + .collect::>(); // When do we generate a crossing way from a node? Have tried a few heuristics here: // // - when it's not part of a crossing way already (but then nodes on driveways are @@ -25,22 +34,49 @@ impl Speedwalk { // - if the node is only attached to one way (in the middle), it needs a crossing // - if the node is attached to two ways AND those ways are nearly // parallel/anti-parallel, then it needs a crossing - if node.is_crossing() || (include_crossing_no && node.is_explicit_crossing_no()) { - let ways = node - .way_ids - .iter() - .map(|w| &self.derived_ways[w]) - .collect::>(); - if ways.len() == 1 && ways[0].kind.is_road() { - crossings.push(*id); - } else if ways.len() == 2 - && ways[0].kind.is_road() - && ways[1].kind.is_road() - && nearly_parallel(&ways[0].linestring, &ways[1].linestring, 10.0) - { - crossings.push(*id); + if ways.len() == 1 && ways[0].kind.is_road() { + crossings.push(*crossing_id); + continue; + } else if ways.len() == 2 + && ways[0].kind.is_road() + && ways[1].kind.is_road() + && nearly_parallel(&ways[0].linestring, &ways[1].linestring, 10.0) + { + crossings.push(*crossing_id); + continue; + } + + // There are also cases where the crossing node is already connected to a sidewalk or + // footway on one side, and we only need to generate a new way on the other side. + let mut roads = Vec::new(); + let mut crossings = Vec::new(); + let mut other = Vec::new(); + for way in ways { + if way.kind.is_road() { + roads.push(way); + } else if way.kind == Kind::Crossing { + crossings.push(way); + } else if way.kind == Kind::Other { + // If the crossing node is an endpoint of this footway, then this is indeed a + // half-crossing. Otherwise, it's likely a mistagged crossing way (that may + // need to be split). + if let Some((idx, _)) = way + .node_ids + .iter() + .enumerate() + .find(|(_, n)| **n == *crossing_id) + { + if idx == 0 || idx == way.node_ids.len() - 1 { + other.push(way); + } + } } } + // TODO This may be too restrictive, but it's a start for cases like + // https://www.openstreetmap.org/node/12270311650 + if crossings.is_empty() && roads.len() >= 1 && other.len() == 1 { + half_crossings.push((*crossing_id, roads[0], other[0])); + } } info!( @@ -59,6 +95,8 @@ impl Speedwalk { let mut new_crossings = Vec::new(); let mut insert_new_nodes = HashMap::new(); for crossing_node_id in crossings { + // TODO tmp, to just see the new half-crossings + break; let project_away_meters = 10.0; let crossing_node = &self.derived_nodes[&crossing_node_id]; @@ -108,6 +146,54 @@ impl Speedwalk { .push((endpt2, Tags::empty())); } + info!("Generating {} half-crossings", half_crossings.len()); + for (crossing_node_id, road, existing_footway) in half_crossings { + let project_away_meters = 10.0; + + let crossing_node = &self.derived_nodes[&crossing_node_id]; + let crossing_pt = crossing_node.pt; + + // Make two perpendicular lines at the node + // TODO It might be more straightforward to figure out the incoming angle of + // existing_footway and continue in that direction + let angle = angle_of_pt_on_line(&road.linestring, crossing_pt); + let mut candidates = Vec::new(); + for offset in [-90.0, 90.0] { + let test_line = Line::new( + crossing_pt, + project_away(crossing_pt, angle + offset, project_away_meters), + ); + if let Some((sidewalk, endpt)) = find_sidewalk_hit(&closest_sidewalk, test_line) { + candidates.push((sidewalk, endpt)); + } + } + + // Which one is the missing side? Check the distance between this candidate endpoint + // and the existing footway + if let Some((sidewalk, endpt)) = candidates.into_iter().max_by_key(|(_, endpt)| { + to_cm(Euclidean.distance(&existing_footway.linestring, &Point::from(*endpt))) + }) { + let mut new_tags = Tags::empty(); + new_tags.insert("highway", "footway"); + new_tags.insert("footway", "crossing"); + // Store OSM reference: use crossing node ID (primary) and road way ID (always available as fallback) + new_tags.insert("tmp:osm_node_id", format!("node/{}", crossing_node_id.0)); + // TODO Plumb road id + //new_tags.insert("tmp:osm_way_id", format!("way/{}", road_way_id.0)); + // Copy one tag from the crossing node to the new crossing way + if let Some(value) = crossing_node.tags.get("crossing") { + new_tags.insert("crossing", value); + } + + new_crossings.push((LineString::new(vec![endpt, crossing_pt]), new_tags)); + + insert_new_nodes + .entry(sidewalk) + .or_insert_with(Vec::new) + .push((endpt, Tags::empty())); + } + } + CreateNewGeometry { new_ways: new_crossings, new_kind: Kind::Crossing, @@ -177,3 +263,7 @@ pub fn shortest_rotation(angle1: f64, angle2: f64) -> f64 { fn nearly_parallel(ls1: &LineString, ls2: &LineString, epsilon_degrees: f64) -> bool { shortest_rotation(average_angle(ls1), average_angle(ls2)).abs() < epsilon_degrees } + +fn to_cm(x: f64) -> usize { + (x * 100.0).round() as usize +}