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

Cache roads per intersection. #93 #98

Merged
merged 8 commits into from
Oct 2, 2022
7 changes: 7 additions & 0 deletions osm2streets-js/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,13 @@ impl JsStreetNetwork {
.map(|x| JsValue::from(JsDebugStreets { inner: x.clone() }))
.collect()
}

#[wasm_bindgen(js_name = debugClockwiseOrderingGeojson)]
pub fn debug_clockwise_ordering_geojson(&self) -> String {
self.inner
.debug_clockwise_ordering_geojson(&mut Timer::throwaway())
.unwrap()
}
}

#[wasm_bindgen]
Expand Down
110 changes: 93 additions & 17 deletions osm2streets/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,28 +159,45 @@ impl StreetNetwork {
}
}

// TODO Might be better to maintain this instead of doing a search everytime.
pub fn roads_per_intersection(&self, i: osm::NodeID) -> Vec<OriginalRoad> {
let mut results = Vec::new();
for id in self.roads.keys() {
if id.i1 == i || id.i2 == i {
results.push(*id);
}
pub fn insert_road(&mut self, id: OriginalRoad, road: Road) {
self.roads.insert(id, road);
for i in [id.i1, id.i2] {
self.intersections.get_mut(&i).unwrap().roads.push(id);
self.sort_roads(i);
}
results
}

pub fn new_osm_node_id(&self, start: i64) -> osm::NodeID {
assert!(start < 0);
// Slow, but deterministic.
let mut osm_node_id = start;
loop {
if self.intersections.keys().any(|i| i.0 == osm_node_id) {
osm_node_id -= 1;
} else {
return osm::NodeID(osm_node_id);
pub fn remove_road(&mut self, id: &OriginalRoad) -> Road {
// Since the roads are already sorted, removing doesn't hurt anything
self.intersections
.get_mut(&id.i1)
.unwrap()
.roads
.retain(|r| r != id);
self.intersections
.get_mut(&id.i2)
.unwrap()
.roads
.retain(|r| r != id);
self.roads.remove(id).unwrap()
}

pub fn retain_roads<F: Fn(&OriginalRoad, &Road) -> bool>(&mut self, f: F) {
let mut remove = Vec::new();
for (id, r) in &self.roads {
if !f(id, r) {
remove.push(*id);
}
}
for id in remove {
self.remove_road(&id);
}
}

// This always returns roads oriented in clockwise order around the intersection
// TODO Consider not cloning. Many callers will have to change
pub fn roads_per_intersection(&self, i: osm::NodeID) -> Vec<OriginalRoad> {
self.intersections[&i].roads.clone()
}

/// (Intersection polygon, polygons for roads, list of labeled polygons to debug)
Expand Down Expand Up @@ -286,6 +303,60 @@ impl StreetNetwork {
.push((self.roads[&r].untrimmed_road_geometry().0, label.into()));
}
}

// Restore the invariant that an intersection's roads are ordered clockwise
//
// TODO This doesn't handle trim_roads_for_merging
Copy link
Contributor Author

Choose a reason for hiding this comment

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

In cases where we remove a short road and merge intersections, sorting around a "center" point gets kind of funny. geometry/algorithm.rs takes care to try and handle that. I didn't port the same logic here yet.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think the best way to determine the order of roads in those multi-node intersections from scratch is to use the OSM graph structure. First sorting the nodes (that are the Road endpoints) then by bearing of the first OSM segment. #98 (comment)

This would require keeping the OSM graph around in some capacity, at least within multi-node intersections.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I agree with keeping the graph structure around somehow. Preserving turn restrictions when we merge gets very complex, and a possibly simpler approach is to use the original graph and just ask questions about it. Keeping the graph also means we can map back to objects in OSM more directly.

I don't know how this would work, so shelving the idea for the moment, but you've definitely got me thinking...

Copy link
Collaborator

Choose a reason for hiding this comment

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

I like "Asking questions about it". In fact, load and leave the osm graph completely in place. Every road should get it's own copy of the geom (like when you merge similar ways currently) which can be promptly offset to account for placement. Then further tweaked if needed (like sausage links). (Think of an editor, where the graph is literally drawn on the map. One could point at a lane to edit the way.)

(Roads and intersections need to have IDs independant of the OSM ids they reference, [and they need to know their own ID, so we don't need to pass pairs eveywhere])

Asking questions about roads is how I'm thinking about movements too. It's like I want to iterate flows of traffic clockwise around the intersection. A way could yield a single (oneway) flow, or commonly two. An intersection provides an iter_cw_flows() -> Flow, where Flow is a view onto a subset of lanes on a Road (that all go in the same direction).

I think that's how the type system helps me out here: There exists a whole lot of data about a whole lot of lanes, laid out in memory in one way or another, I want to slice it up differently, with differently typed structs, for different tasks. It's like the HashMap Entry type.

Keeping an immutable copy of the OSM graph and making the Street graph independent will no doubt become GH Issues to discuss and target soon.

fn sort_roads(&mut self, i: osm::NodeID) {
let intersection = self.intersections.get_mut(&i).unwrap();

// (ID, polyline pointing to the intersection, sorting point that's filled out later)
let mut road_centers = Vec::new();
let mut endpoints_for_center = Vec::new();
for r in &intersection.roads {
let road = &self.roads[r];
// road.center_pts is unadjusted; it doesn't handle unequal widths yet. But that
// shouldn't matter for sorting.
let center_pl = if r.i1 == i {
PolyLine::must_new(road.osm_center_points.clone()).reversed()
} else if r.i2 == i {
PolyLine::must_new(road.osm_center_points.clone())
} else {
panic!("Incident road {r} doesn't have an endpoint at {i}");
};
endpoints_for_center.push(center_pl.last_pt());

road_centers.push((*r, center_pl, Pt2D::zero()));
}
// In most cases, this will just be the same point repeated a few times, so Pt2D::center is a
// no-op. But when we have pretrimmed roads, this is much closer to the real "center" of the
// polygon we're attempting to create.
let intersection_center = Pt2D::center(&endpoints_for_center);

// Sort the road polylines in clockwise order around the center. This is subtle --
// https://a-b-street.github.io/docs/tech/map/geometry/index.html#sorting-revisited. When we
// get this wrong, the resulting polygon looks like a "bowtie," because the order of the
// intersection polygon's points follows this clockwise ordering of roads.
//
// We could use the point on each road center line farthest from the intersection center. But
// when some of the roads bend around, this produces incorrect ordering. Try walking along that
// center line a distance equal to the _shortest_ road.
let shortest_center = road_centers
.iter()
.map(|(_, pl, _)| pl.length())
.min()
.unwrap();
for (_, pl, sorting_pt) in &mut road_centers {
*sorting_pt = pl.must_dist_along(pl.length() - shortest_center).0;
}
road_centers.sort_by_key(|(_, _, sorting_pt)| {
sorting_pt
.angle_to(intersection_center)
.normalized_degrees() as i64
});

intersection.roads = road_centers.into_iter().map(|(r, _, _)| r).collect();
}
}

// Mutations and supporting queries
Expand Down Expand Up @@ -544,6 +615,10 @@ pub struct Intersection {
pub control: ControlType,
pub elevation: Distance,

/// All roads connected to this intersection. They may be incoming or outgoing relative to this
/// intersection. They're ordered clockwise aroundd the intersection.
pub roads: Vec<OriginalRoad>,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is there a good type that has an insert_at to take advantage of the fact that roads is already sorted?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Like https://doc.rust-lang.org/std/vec/struct.Vec.html#method.insert ? When we're adding a new road, we could calculate its position in the list and add it, or just recalculate the ordering for the intersection. It'll depend on the sorting algorithm

Copy link
Collaborator

Choose a reason for hiding this comment

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

Right. Performance doesn't really matter, so Vec works if we want to do it the "find and insert" way.


// true if src_i matches this intersection (or the deleted/consolidated one, whatever)
pub trim_roads_for_merging: BTreeMap<(osm::WayID, bool), Pt2D>,
}
Expand All @@ -555,6 +630,7 @@ impl Intersection {
complexity,
control,
// Filled out later
roads: Vec::new(),
elevation: Distance::ZERO,
trim_roads_for_merging: BTreeMap::new(),
}
Expand Down
29 changes: 29 additions & 0 deletions osm2streets/src/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,35 @@ impl StreetNetwork {
let output = serde_json::to_string_pretty(&obj)?;
Ok(output)
}

/// For an intersection, show the clockwise ordering of roads around it
pub fn debug_clockwise_ordering_geojson(&self, timer: &mut Timer) -> Result<String> {
let initial_map = crate::initial::InitialMap::new(self, timer);

let mut pairs = Vec::new();

for (i, intersection) in &self.intersections {
for (idx, r) in intersection.roads.iter().enumerate() {
let pl = &initial_map.roads[r].trimmed_center_pts;
let pt = if r.i1 == *i {
pl.first_pt()
} else {
pl.last_pt()
};
pairs.push((
pt.to_geojson(Some(&self.gps_bounds)),
make_props(&[(
"label",
format!("{} / {}", idx + 1, intersection.roads.len()).into(),
)]),
));
}
}

let obj = geom::geometries_with_properties_to_geojson(pairs);
let output = serde_json::to_string_pretty(&obj)?;
Ok(output)
}
}

impl DebugStreets {
Expand Down
11 changes: 6 additions & 5 deletions osm2streets/src/transform/collapse_intersections.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ pub fn collapse_intersection(streets: &mut StreetNetwork, i: NodeID) {
assert_eq!(roads.len(), 2);
let mut r1 = roads[0];
let mut r2 = roads[1];
assert_ne!(r1, r2);

// We'll keep r1's way ID, so it's a little more convenient for debugging to guarantee r1 is
// the longer piece.
Expand All @@ -157,11 +158,11 @@ pub fn collapse_intersection(streets: &mut StreetNetwork, i: NodeID) {
}

info!("Collapsing degenerate {}", i);
streets.intersections.remove(&i).unwrap();
// We could be more careful merging percent_incline and osm_tags, but in practice, it doesn't
// matter for the short segments we're merging.
let mut new_road = streets.roads.remove(&r1).unwrap();
let mut road2 = streets.roads.remove(&r2).unwrap();
let mut new_road = streets.remove_road(&r1);
let mut road2 = streets.remove_road(&r2);
streets.intersections.remove(&i).unwrap();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Order matters! remove_road cleans up the two intersections, so they need to both exist until the end


// There are 4 cases, easy to understand on paper. Preserve the original direction of r1
let (new_i1, new_i2) = if r1.i2 == r2.i1 {
Expand Down Expand Up @@ -195,7 +196,7 @@ pub fn collapse_intersection(streets: &mut StreetNetwork, i: NodeID) {
i1: new_i1,
i2: new_i2,
};
streets.roads.insert(new_r1, new_road);
streets.insert_road(new_r1, new_road);

// We may need to fix up turn restrictions. r1 and r2 both become new_r1.
let rewrite = |x: &OriginalRoad| *x == r1 || *x == r2;
Expand Down Expand Up @@ -242,7 +243,7 @@ pub fn trim_deadends(streets: &mut StreetNetwork) {
}

for r in remove_roads {
streets.roads.remove(&r).unwrap();
streets.remove_road(&r);
}
for i in remove_intersections {
streets.delete_intersection(i);
Expand Down
28 changes: 15 additions & 13 deletions osm2streets/src/transform/merge_short_road.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ impl StreetNetwork {
if !self.intersections.contains_key(&short.i1)
|| !self.intersections.contains_key(&short.i2)
{
self.roads.remove(&short).unwrap();
self.remove_road(&short);
bail!(
"One endpoint of {} has already been deleted, skipping",
short
Expand Down Expand Up @@ -95,17 +95,10 @@ impl StreetNetwork {
.extend(trim_roads_for_merging);
}

self.roads.remove(&short).unwrap();
self.remove_road(&short);

// Arbitrarily keep i1 and destroy i2. If the intersection types differ, upgrade the
// surviving interesting.
{
// Don't use delete_intersection; we're manually fixing up connected roads
let i = self.intersections.remove(&i2).unwrap();
if i.control == ControlType::TrafficSignal {
self.intersections.get_mut(&i1).unwrap().control = ControlType::TrafficSignal;
}
}
// Arbitrarily keep i1 and destroy i2. Don't actually remove the intersection until later;
// remove_road needs the intersection to exist

// Fix up all roads connected to i2. Delete them and create a new copy; the ID changes,
// since one intersection changes.
Expand All @@ -115,7 +108,7 @@ impl StreetNetwork {
let mut new_to_old = BTreeMap::new();
for r in self.roads_per_intersection(i2) {
deleted.push(r);
let road = self.roads.remove(&r).unwrap();
let road = self.remove_road(&r);
let mut new_id = r;
if r.i1 == i2 {
new_id.i1 = i1;
Expand All @@ -133,10 +126,19 @@ impl StreetNetwork {
old_to_new.insert(r, new_id);
new_to_old.insert(new_id, r);

self.roads.insert(new_id, road);
self.insert_road(new_id, road);
created.push(new_id);
}

// If the intersection types differ, upgrade the surviving interesting.
{
// Don't use delete_intersection; we're manually fixing up connected roads
let i = self.intersections.remove(&i2).unwrap();
if i.control == ControlType::TrafficSignal {
self.intersections.get_mut(&i1).unwrap().control = ControlType::TrafficSignal;
}
}

// If we're deleting the target of a simple restriction somewhere, update it.
for (from_id, road) in &mut self.roads {
let mut fix_trs = Vec::new();
Expand Down
4 changes: 2 additions & 2 deletions osm2streets/src/transform/remove_disconnected.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,15 @@ pub fn remove_disconnected_roads(streets: &mut StreetNetwork) {
for p in partitions.iter().skip(1) {
for id in p {
info!("Removing {} because it's disconnected from most roads", id);
streets.roads.remove(id).unwrap();
streets.remove_road(id);
next_roads.remove(id.i1, *id);
next_roads.remove(id.i2, *id);
}
}

// Also remove cul-de-sacs here. TODO Support them properly, but for now, they mess up parking
// hint matching (loop PolyLine) and pathfinding later.
streets.roads.retain(|id, _| id.i1 != id.i2);
streets.retain_roads(|id, _| id.i1 != id.i2);

// Remove intersections without any roads
streets
Expand Down
2 changes: 1 addition & 1 deletion osm2streets/src/transform/sausage_links.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ fn fix(streets: &mut StreetNetwork, id1: OriginalRoad, id2: OriginalRoad) {
assert!(streets.roads.contains_key(&id2));

// Arbitrarily remove the 2nd
let mut road2 = streets.roads.remove(&id2).unwrap();
let mut road2 = streets.remove_road(&id2);
// And modify the 1st
let road1 = streets.roads.get_mut(&id1).unwrap();

Expand Down
2 changes: 1 addition & 1 deletion osm2streets/src/transform/snappy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ pub fn snap_cycleways(streets: &mut StreetNetwork) {
snapped_ids.push(cycleway_id);

// Remove the separate cycleway
let deleted_cycleway = streets.roads.remove(&cycleway_id).unwrap();
let deleted_cycleway = streets.remove_road(&cycleway_id);

// Add it as an attribute to the roads instead
for (road_id, dir) in roads {
Expand Down
13 changes: 11 additions & 2 deletions street-explorer/www/js/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,8 @@ class TestCase {

importButton.innerText = "Importing OSM data...";

const drivingSide = app.getImportSettings().drivingSideForNewImports || "Right";
const drivingSide =
app.getImportSettings().drivingSideForNewImports || "Right";

importOSM("Imported area", app, osmInput, drivingSide, true);
const bounds = app.layers
Expand Down Expand Up @@ -196,7 +197,7 @@ function importOSM(groupName, app, osmXML, drivingSide, addOSMLayer) {
dual_carriageway_experiment: !!importSettings.dualCarriagewayExperiment,
cycletrack_snapping_experiment:
!!importSettings.cycletrackSnappingExperiment,
inferred_sidewalks: importSettings.sidewalks === 'infer',
inferred_sidewalks: importSettings.sidewalks === "infer",
osm2lanes: !!importSettings.osm2lanes,
});
var group = new LayerGroup(groupName, app.map);
Expand All @@ -212,6 +213,9 @@ function importOSM(groupName, app, osmXML, drivingSide, addOSMLayer) {
"Lane markings",
makeLaneMarkingsLayer(network.toLaneMarkingsGeojson())
);
group.addLazyLayer("Debug road ordering", () =>
makeDebugLayer(network.debugClockwiseOrderingGeojson())
);
// TODO Graphviz hits `ReferenceError: can't access lexical declaration 'graph' before initialization`

const numDebugSteps = network.getDebugSteps().length;
Expand All @@ -235,6 +239,11 @@ function importOSM(groupName, app, osmXML, drivingSide, addOSMLayer) {
group.addLazyLayer("Lane markings", () =>
makeLaneMarkingsLayer(step.getNetwork().toLaneMarkingsGeojson())
);
// TODO Can we disable by default in a group? This one is very noisy, but
// could be useful to inspect
/*group.addLazyLayer("Debug road ordering", () =>
makeDebugLayer(step.getNetwork().debugClockwiseOrderingGeojson())
);*/

const debugGeojson = step.toDebugGeojson();
if (debugGeojson) {
Expand Down
Loading