Skip to content

Add turn movements#207

Draft
BudgieInWA wants to merge 4 commits into
mainfrom
turn-movements
Draft

Add turn movements#207
BudgieInWA wants to merge 4 commits into
mainfrom
turn-movements

Conversation

@BudgieInWA

Copy link
Copy Markdown
Collaborator

We don't need all of this before merging, but here's the plan so far:

  • Rename Movement -> Turn (road level), add TurnMovement (lane level)
  • Calculate TurnMovements for degenerate (2 road) intersections
  • Calculate movements for IntersectionKind::Connections with more than two roads.
  • Do some default allocations for arbitrarily complex intersections.
  • Add TurnKind to determine what kind of lane markings the TurnMovements get.
  • Render "turn guide" lane markings for "turn" turns.
  • Render ordinary lane markings for "continuation" turns (ideally, reusing the normal logic).

This is making some good progress towards lane level movements. In fact, some movements are calculated and drawn! I added the movement centrelines in post, the left edge of the movements are being drawn:

image

We need angled road ends, #95, to draw these markings properly.

@dabreegster dabreegster left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Really cool to see this!

Not that I'm likely to play with the JOSM side soon, but is https://github.com/BudgieInWA/JOSM2Streets/ still up-to-date, or are there new rendering goodies corresponding to this?


match intersection.kind {
IntersectionKind::Connection => {
for turn in intersection.turns.iter_mut() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
for turn in intersection.turns.iter_mut() {
for turn in &mut intersection.turns {

Optional, not really sure what style is preferable. If we later need to do anything else with the iterator, like .enumerate(), then how you have it is nicer (because we'd switch back to it)


/// A specific path that a vehicle can take through a turn.
///
/// Lane numbers are counted from the perspective of the direction of the turn.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This stumped me for a few minutes; can we add an example or clarify the comment? LaneIDs normally index into lanes_ltr. Normally whatever direction we're thinking about a road, the lane ID is "absolute" -- 0 means the left side of the road based on its OSM-defined orientation. It feels like here we have a sudden explosion of possibilities, if we point from the right side of one road to the left side of another in the backwards direction, for instance...

And actually now I'm wondering if this refers to LtrLaneNum and not LaneID and am more confused :\

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

D'oh, this is an outdated comment. You're right, it was referring to LtrLaneNum, trying to clarify about Forward/Backward variants.

}

/// Limitations: ignores both ways lanes
pub fn default(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Getting tests of some form here is important IMHO. We could maybe make unit tests if we can succinctly express input and output, or maybe it's better to do our usual snapshot+diff testing approach, and get more rendering into Street Explorer

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I think unit tests is a great idea here, and covering at least some of these code paths in our snapshot tests is important too.

I keep finding myself wanting to create "unit test" like snapshot tests (with very simple, contrived inputs), instead of the large "integration tests" that our current snapshot tests are. Maybe that will be worthwhile at some point, but unit testing these kinds of functions is an easier way to to address my want!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hmm... nothing would stop us from drawing something synthetic in JOSM and adding the .osm to tests/ like we normally do

Comment thread osm2streets/src/output.rs
// Lanes through intersections
for inter in self.intersections.values() {
for Turn { movements, .. } in &inter.turns {
let Some(movements) = movements else { continue };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

OK I actually hadn't understood the point of let-else till this; very cool!

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Yep, a bunch of nesting can be replaced with early returns! The else block can run any code, as long as it diverges (doesn't run to the end).

Comment thread osm2streets/src/output.rs
areas
}

fn lane(&self, lane: LaneID) -> Option<&LaneSpec> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Could we move to road.rs in the impl StreetNetwork block?

Comment thread osm2streets/src/render.rs
pairs.push((polygon.to_geojson(Some(&self.gps_bounds)), make_props(&[])));
}
}
if pairs.is_empty() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

In general, when should we expect turns to exist, but not movements? Right now update_i does both. Is this temporary before we have movements defined in all cases?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Yes, that was a temporary measure. I don't know where it will land though. I think we will be able to calculate movements any time we calculate turns, so we will be able to remove the Option.

I was playing with using an Option in places where data is only present after some step of the algorithm, instead of using *::dummy() kind of values.

Comment thread osm2streets/src/road.rs
Ok((left, right))
}

pub fn intersection_at(&self, end: RoadEnd) -> IntersectionID {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

These new helpers are nice; there's probably many places throughout the code where they can come in handy!

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Using RoadEnd has been indispensable while dealing with the possibility space explosion in crate::movements, and these helpers remove a lot of noise from those implementation sites that make it possible to see the (still very complicated) logic there.


/// A specific path that a vehicle can take through a turn.
///
/// Lane numbers are counted from the perspective of the direction of the turn.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

D'oh, this is an outdated comment. You're right, it was referring to LtrLaneNum, trying to clarify about Forward/Backward variants.

}

/// Limitations: ignores both ways lanes
pub fn default(

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I think unit tests is a great idea here, and covering at least some of these code paths in our snapshot tests is important too.

I keep finding myself wanting to create "unit test" like snapshot tests (with very simple, contrived inputs), instead of the large "integration tests" that our current snapshot tests are. Maybe that will be worthwhile at some point, but unit testing these kinds of functions is an easier way to to address my want!

Comment thread osm2streets/src/output.rs
// Lanes through intersections
for inter in self.intersections.values() {
for Turn { movements, .. } in &inter.turns {
let Some(movements) = movements else { continue };

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Yep, a bunch of nesting can be replaced with early returns! The else block can run any code, as long as it diverges (doesn't run to the end).

Comment thread osm2streets/src/render.rs
Comment on lines -69 to +75
"movements",
"turns",
serde_json::Value::Array(
intersection
.movements
.turns
.iter()
.map(|(a, b)| format!("{a} -> {b}").into())
.map(|Turn { from, to, .. }| format!("{from} -> {to}").into())

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I need to add TurnMovements to the test output to observe these changes.

Comment thread osm2streets/src/render.rs
pairs.push((polygon.to_geojson(Some(&self.gps_bounds)), make_props(&[])));
}
}
if pairs.is_empty() {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Yes, that was a temporary measure. I don't know where it will land though. I think we will be able to calculate movements any time we calculate turns, so we will be able to remove the Option.

I was playing with using an Option in places where data is only present after some step of the algorithm, instead of using *::dummy() kind of values.

Comment thread osm2streets/src/road.rs
Ok((left, right))
}

pub fn intersection_at(&self, end: RoadEnd) -> IntersectionID {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Using RoadEnd has been indispensable while dealing with the possibility space explosion in crate::movements, and these helpers remove a lot of noise from those implementation sites that make it possible to see the (still very complicated) logic there.

Comment on lines +140 to +141
path: PolyLine::new(vec![src_pt, dst_pt])
.unwrap_or_else(|_| PolyLine::must_new(vec![src_pt, Pt2D::new(0.0, 0.0), dst_pt])),

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This is where I would want to create a nice curved path, instead of just jamming two points in.

After working with it, it might be better to remove the geometry aspect from here and calculate it later, or on the fly. That matches up better with where geometry of other kinds is calculated.

Also, the or_else polyline sometimes crashes when the first one does, but not always!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

On-the-fly geom could be nice, yeah, and would have reduce file size requirements maybe. (But it's not a clear-cut tradeoff; lazily calculating in a UI later can be sluggish.)

Also, the or_else polyline sometimes crashes when the first one does, but not always!

I actually didn't look at this carefully; inserting (0, 0) in the middle is an attempt to make the total line length larger? In what kind of cases is the above PolyLine::new(vec![src_pt, dst_pt]) failing -- when the two points are very close together? What should we do in those cases -- maybe it's valid to just have no movement geometry?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Yes, when the points are close together. Given our current approach, we shouldn't have any zero length movements, but I think it happens coincidently sometimes.

I'm going to experiment with removing path from TurnMovement so that turns and movement are only concerned with connectivity for now (and don't have to be recalculated if trim distances change, or something like that). I want to think about storing lane centerlines and movement paths in the same way.

@BudgieInWA

Copy link
Copy Markdown
Collaborator Author

Really cool to see this!

Not that I'm likely to play with the JOSM side soon, but is BudgieInWA/JOSM2Streets still up-to-date, or are there new rendering goodies corresponding to this?

The JOSM2Streets build embeds StreetNetwork.jar, which in turn embeds the rust lib. If the osm2streets java classes haven't changed their API, then you can build your own StreetNetwork.jar and replace the copy in the JOSM2Streets repo before building. You'd need to do that for these changes, I haven't pushed up the new JAR yet.

@oneil512

oneil512 commented Jun 9, 2024

Copy link
Copy Markdown

Hi, is this still being considered?

@dabreegster

Copy link
Copy Markdown
Contributor

I don't think Ben or I have had time to work on this, so not anytime soon. Are you interested in picking it up?

@paaspaas00

Copy link
Copy Markdown

Hi @BudgieInWA is this being worked on?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants