From 682c6ff3f721078ef65c261369fb0dd0ec787d34 Mon Sep 17 00:00:00 2001 From: Dustin Carlino Date: Thu, 1 Dec 2022 23:45:05 +0000 Subject: [PATCH] Refactor the road edge view construct, and use it for a much simpler intersection geometry algorithm after trimming road centers. #136 This needs a little more work to keep intersections minimal --- osm2streets/src/geometry/algorithm.rs | 214 ++++---------------------- osm2streets/src/render.rs | 51 +----- osm2streets/src/road.rs | 45 ++++++ 3 files changed, 85 insertions(+), 225 deletions(-) diff --git a/osm2streets/src/geometry/algorithm.rs b/osm2streets/src/geometry/algorithm.rs index 3a76a1b3..6ca200d6 100644 --- a/osm2streets/src/geometry/algorithm.rs +++ b/osm2streets/src/geometry/algorithm.rs @@ -2,10 +2,10 @@ use std::collections::BTreeMap; use anyhow::Result; -use abstutil::wraparound_get; use geom::{Circle, Distance, InfiniteLine, PolyLine, Polygon, Pt2D, Ring, EPSILON_DIST}; use super::Results; +use crate::road::RoadEdge; use crate::{IntersectionID, Road, RoadID}; const DEGENERATE_INTERSECTION_HALF_LENGTH: Distance = Distance::const_meters(2.5); @@ -84,9 +84,9 @@ pub fn intersection_polygon( } if !trim_roads_for_merging.is_empty() { - pretrimmed_geometry(results, &road_lines) + finalize_intersection_polygon(results, &road_lines) } else if let Some(result) = on_off_ramp(results.clone(), road_lines.clone()) { - Ok(result) + result } else { generalized_trim_back(results, &road_lines) } @@ -235,127 +235,7 @@ fn generalized_trim_back(mut results: Results, input_road_lines: &[RoadLine]) -> results.roads.get_mut(&id).unwrap().trimmed_center_line = pl; } - calculate_polygon(results, input_road_lines) -} - -fn calculate_polygon(mut results: Results, input_road_lines: &[RoadLine]) -> Result { - let mut endpoints: Vec = Vec::new(); - for idx in 0..input_road_lines.len() as isize { - let (id, fwd_pl, back_pl) = { - let r = wraparound_get(input_road_lines, idx); - (r.id, &r.fwd_pl, &r.back_pl) - }; - // TODO Ahhh these names are confusing. Adjacent to the fwd_pl, but it's a back pl. - let adj_back_pl = &wraparound_get(input_road_lines, idx + 1).fwd_pl; - let adj_fwd_pl = &wraparound_get(input_road_lines, idx - 1).back_pl; - - let r = &results.roads[&id]; - - // Include collisions between polylines of adjacent roads, so the polygon doesn't cover area - // not originally covered by the thick road bands. - // Always take the second_half here to handle roads that intersect at multiple points. - // TODO Should maybe do reversed() to fwd_pl here too. And why not make all the lines - // passed in point AWAY from the intersection instead? - if fwd_pl.length() >= EPSILON_DIST * 3.0 && adj_fwd_pl.length() >= EPSILON_DIST * 3.0 { - if let Some((hit, _)) = fwd_pl - .second_half()? - .intersection(&adj_fwd_pl.second_half()?) - { - endpoints.push(hit); - } - } else { - warn!( - "Excluding collision between original polylines of {id} and something, because \ - stuff's too short" - ); - } - - // Shift those final centers out again to find the main endpoints for the polygon. - if r.dst_i == results.intersection_id { - endpoints.push(r.trimmed_center_line.shift_right(r.half_width())?.last_pt()); - endpoints.push(r.trimmed_center_line.shift_left(r.half_width())?.last_pt()); - } else { - endpoints.push(r.trimmed_center_line.shift_left(r.half_width())?.first_pt()); - endpoints.push( - r.trimmed_center_line - .shift_right(r.half_width())? - .first_pt(), - ); - } - - if back_pl.length() >= EPSILON_DIST * 3.0 && adj_back_pl.length() >= EPSILON_DIST * 3.0 { - if let Some((hit, _)) = back_pl - .second_half()? - .intersection(&adj_back_pl.second_half()?) - { - endpoints.push(hit); - } - } else { - warn!( - "Excluding collision between original polylines of {id} and something, because \ - stuff's too short" - ); - } - } - - // There are bad polygons caused by weird short roads. As a temporary workaround, detect cases - // where polygons dramatically double back on themselves and force the polygon to proceed - // around its center. - let main_result = close_off_polygon(Pt2D::approx_dedupe(endpoints, Distance::meters(0.1))); - let mut deduped = main_result.clone(); - deduped.pop(); - deduped.sort_by_key(|pt| pt.to_hashable()); - deduped = Pt2D::approx_dedupe(deduped, Distance::meters(0.1)); - let center = Pt2D::center(&deduped); - deduped.sort_by_key(|pt| pt.angle_to(center).normalized_degrees() as i64); - deduped = Pt2D::approx_dedupe(deduped, Distance::meters(0.1)); - deduped = close_off_polygon(deduped); - - results.intersection_polygon = if main_result.len() == deduped.len() { - Ring::must_new(main_result).into_polygon() - } else { - warn!( - "{}'s polygon has weird repeats, forcibly removing points", - results.intersection_id - ); - Ring::must_new(deduped).into_polygon() - }; - - // TODO Or always sort points? Helps some cases, hurts other for downtown Seattle. - /*endpoints.sort_by_key(|pt| pt.to_hashable()); - endpoints = Pt2D::approx_dedupe(endpoints, Distance::meters(0.1)); - let center = Pt2D::center(&endpoints); - endpoints.sort_by_key(|pt| pt.angle_to(center).normalized_degrees() as i64); - close_off_polygon(endpoints)*/ - - Ok(results) -} - -fn pretrimmed_geometry(mut results: Results, road_lines: &[RoadLine]) -> Result { - let mut endpoints: Vec = Vec::new(); - for r in road_lines { - let r = &results.roads[&r.id]; - // Shift those final centers out again to find the main endpoints for the polygon. - if r.dst_i == results.intersection_id { - endpoints.push(r.trimmed_center_line.shift_right(r.half_width())?.last_pt()); - endpoints.push(r.trimmed_center_line.shift_left(r.half_width())?.last_pt()); - } else { - endpoints.push(r.trimmed_center_line.shift_left(r.half_width())?.first_pt()); - endpoints.push( - r.trimmed_center_line - .shift_right(r.half_width())? - .first_pt(), - ); - } - } - - // TODO Do all of the crazy deduping that calculate_polygon does? - results.intersection_polygon = Ring::new(close_off_polygon(Pt2D::approx_dedupe( - endpoints, - Distance::meters(0.1), - )))? - .into_polygon(); - Ok(results) + finalize_intersection_polygon(results, input_road_lines) } fn deadend(mut results: Results, road_lines: &[RoadLine]) -> Result { @@ -395,7 +275,6 @@ fn deadend(mut results: Results, road_lines: &[RoadLine]) -> Result { // After trimming the center points, the two sides of the road may be at different // points, so shift the center out again to find the endpoints. - // TODO Refactor with generalized_trim_back. let mut endpts = vec![pl_b.last_pt(), pl_a.last_pt()]; if r.dst_i == results.intersection_id { endpts.push(trimmed.shift_right(r.half_width())?.last_pt()); @@ -404,20 +283,13 @@ fn deadend(mut results: Results, road_lines: &[RoadLine]) -> Result { endpts.push(trimmed.shift_left(r.half_width())?.first_pt()); endpts.push(trimmed.shift_right(r.half_width())?.first_pt()); } + endpts.push(endpts[0]); - endpts.dedup(); - results.intersection_polygon = Ring::must_new(close_off_polygon(endpts)).into_polygon(); + let ring = Ring::deduping_new(endpts)?; + results.intersection_polygon = ring.into_polygon(); Ok(results) } -fn close_off_polygon(mut pts: Vec) -> Vec { - if pts.last().unwrap().approx_eq(pts[0], Distance::meters(0.1)) { - pts.pop(); - } - pts.push(pts[0]); - pts -} - // The lines all end at the intersection struct Piece { id: RoadID, @@ -430,7 +302,7 @@ struct Piece { // The normal generalized_trim_back approach produces huge intersections when 3 roads meet at // certain angles. It usually happens for highway on/off ramps. Try something different here. In // lieu of proper docs, see https://twitter.com/CarlinoDustin/status/1290799086036111360. -fn on_off_ramp(mut results: Results, road_lines: Vec) -> Option { +fn on_off_ramp(mut results: Results, road_lines: Vec) -> Option> { if road_lines.len() != 3 { return None; } @@ -454,7 +326,7 @@ fn on_off_ramp(mut results: Results, road_lines: Vec) -> Option) -> Option) -> Option Result { + let mut sorted_roads = Vec::new(); + // Just use RoadLines to remember the sorting order + for line in input_road_lines { + sorted_roads.push(&results.roads[&line.id]); + } + + let mut endpts = Vec::new(); + // TODO Look at pairs. If the points already match in a corner, ignore, but otherwise calculate the intersection again + for edge in RoadEdge::calculate(sorted_roads, results.intersection_id) { + endpts.push(edge.pl.last_pt()); } - /*for (idx, pt) in endpoints.iter().enumerate() { - debug.push((format!("{}", idx), Circle::new(*pt, Distance::meters(2.0)).to_polygon())); - }*/ - - endpoints.sort_by_key(|pt| pt.to_hashable()); - endpoints.dedup(); - let center = Pt2D::center(&endpoints); - endpoints.sort_by_key(|pt| pt.angle_to(center).normalized_degrees() as i64); - endpoints.dedup(); - results.intersection_polygon = Ring::must_new(close_off_polygon(endpoints)).into_polygon(); - Some(results) + endpts.push(endpts[0]); + let ring = Ring::deduping_new(endpts)?; + results.intersection_polygon = ring.into_polygon(); + Ok(results) } diff --git a/osm2streets/src/render.rs b/osm2streets/src/render.rs index f73901b6..fcfbacbe 100644 --- a/osm2streets/src/render.rs +++ b/osm2streets/src/render.rs @@ -6,9 +6,8 @@ use std::path::Path; use anyhow::Result; use geom::{ArrowCap, Distance, Line, PolyLine, Polygon, Ring}; -use crate::{ - DebugStreets, Direction, DrivingSide, Intersection, LaneSpec, LaneType, RoadID, StreetNetwork, -}; +use crate::road::RoadEdge; +use crate::{DebugStreets, Direction, DrivingSide, Intersection, LaneType, StreetNetwork}; impl StreetNetwork { /// Saves the plain GeoJSON rendering to a file. @@ -398,47 +397,13 @@ fn make_props(list: &[(&str, serde_json::Value)]) -> serde_json::Map Vec { - #[derive(Clone)] - struct Edge { - road: RoadID, - // Pointed into the intersection - pl: PolyLine, - lane: LaneSpec, - } - - // Get the left and right edge of each road, pointed into the intersection. All sorted - // clockwise - // TODO Use the road view idea instead. Or just refactor this. - let mut edges = Vec::new(); - for road in streets.roads_per_intersection(intersection.id) { - let mut left = Edge { - road: road.id, - pl: road - .trimmed_center_line - .must_shift_left(road.total_width() / 2.0), - lane: road.lane_specs_ltr[0].clone(), - }; - let mut right = Edge { - road: road.id, - pl: road - .trimmed_center_line - .must_shift_right(road.total_width() / 2.0), - lane: road.lane_specs_ltr.last().unwrap().clone(), - }; - if road.dst_i == intersection.id { - edges.push(right); - edges.push(left); - } else { - left.pl = left.pl.reversed(); - right.pl = right.pl.reversed(); - edges.push(left); - edges.push(right); - } - } - - // Look at every adjacent pair - let mut results = Vec::new(); + // Look at every adjacent pair of edges + let mut edges = RoadEdge::calculate( + streets.roads_per_intersection(intersection.id), + intersection.id, + ); edges.push(edges[0].clone()); + let mut results = Vec::new(); for pair in edges.windows(2) { let one = &pair[0]; let two = &pair[1]; diff --git a/osm2streets/src/road.rs b/osm2streets/src/road.rs index 4daf4019..9c92b237 100644 --- a/osm2streets/src/road.rs +++ b/osm2streets/src/road.rs @@ -270,3 +270,48 @@ impl StreetNetwork { id } } + +/// The edge of a road, pointed into some intersection +#[derive(Clone)] +pub(crate) struct RoadEdge { + pub road: RoadID, + /// Pointed into the intersection + pub pl: PolyLine, + pub lane: LaneSpec, +} + +impl RoadEdge { + /// Get the left and right edge of each road, pointed into the intersection. All sorted + /// clockwise. No repetitions -- to iterate over all adjacent pairs, the caller must repeat the + /// first edge + // TODO Maybe returning an iterator over pairs of these is more useful + pub fn calculate(sorted_roads: Vec<&Road>, i: IntersectionID) -> Vec { + let mut edges = Vec::new(); + for road in sorted_roads { + let mut left = RoadEdge { + road: road.id, + pl: road + .trimmed_center_line + .must_shift_left(road.total_width() / 2.0), + lane: road.lane_specs_ltr[0].clone(), + }; + let mut right = RoadEdge { + road: road.id, + pl: road + .trimmed_center_line + .must_shift_right(road.total_width() / 2.0), + lane: road.lane_specs_ltr.last().unwrap().clone(), + }; + if road.dst_i == i { + edges.push(right); + edges.push(left); + } else { + left.pl = left.pl.reversed(); + right.pl = right.pl.reversed(); + edges.push(left); + edges.push(right); + } + } + edges + } +}