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

Add turn movements #207

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft

Add turn movements #207

wants to merge 4 commits into from

Conversation

BudgieInWA
Copy link
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.

Copy link
Contributor

@dabreegster dabreegster left a comment

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
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
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
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
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
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
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

// Lanes through intersections
for inter in self.intersections.values() {
for Turn { movements, .. } in &inter.turns {
let Some(movements) = movements else { continue };
Copy link
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
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).

@@ -304,6 +335,10 @@ impl StreetNetwork {

areas
}

fn lane(&self, lane: LaneID) -> Option<&LaneSpec> {
Copy link
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?

pairs.push((polygon.to_geojson(Some(&self.gps_bounds)), make_props(&[])));
}
}
if pairs.is_empty() {
Copy link
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
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.

@@ -467,6 +515,23 @@ impl Road {
Ok((left, right))
}

pub fn intersection_at(&self, end: RoadEnd) -> IntersectionID {
Copy link
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
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
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
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!

// Lanes through intersections
for inter in self.intersections.values() {
for Turn { movements, .. } in &inter.turns {
let Some(movements) = movements else { continue };
Copy link
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 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
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.

pairs.push((polygon.to_geojson(Some(&self.gps_bounds)), make_props(&[])));
}
}
if pairs.is_empty() {
Copy link
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.

@@ -467,6 +515,23 @@ impl Road {
Ok((left, right))
}

pub fn intersection_at(&self, end: RoadEnd) -> IntersectionID {
Copy link
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
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
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
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
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
Copy link

oneil512 commented Jun 9, 2024

Hi, is this still being considered?

@dabreegster
Copy link
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?

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.

3 participants