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

Fixing edge connectivity for OSM ways with differing layer tag values #220

Merged
merged 3 commits into from
Sep 21, 2018

Conversation

MikeGost
Copy link
Contributor

Description:

In the existing implementation, we would only way-section two ways if they had the same layer tag value. However, this ignored a very common OSM use-case where two ways could be connected, but have two distinct layer tag values. As an example, imagine a trunk-link of layer 0 linking a trunk road of layer 1 to another road of layer 0 - a very common case for most on/off ramps. This PR updates the way-sectioning logic to allow sectioning if two ways have different layer tag values, as long as the intersection is at the end point of one of the ways. This is a clear distinction, to avoid sectioning anything that is intersecting at a non-endpoint, since this is often times a mapping error. As an example, imagine an overpass road of layer 1 sharing a shape point with an underlying residential road of layer 0. If we way-section at a non-end point, we would end up connecting these two, when in reality there is no way to drive from the overpass to the residential road beneath it. It's worth calling out - that the existing logic may still have some corner cases that exist. However, these changes align the Atlas more closely with the OSM data model.

Potential Impact:

This will have an impact on any consumer of the atlas with the changes. Specifically, the proper connectivity is now ensured with ways that are connected at endpoints and having different layer tag values.

Unit Test Approach:

Added 2 new unit tests and renamed an existing one to capture behavior more accurately. The test suite is now comprised of three cases where two ways with different layer tag values are intersecting. The intersections occur at the beginning, middle and end of the way. When the intersection happens at the start or end of the way, the proper connectivity is ensured. Whereas, if the intersection is during a non-end point between the two ways of different layer tag values, then we ensure they are not connected. The two unit tests both fail without the changes and pass afterwards. Below are the screenshots of the behavior the unit tests are testing:

Intersection at start of the way:
intersectionatstart

Intersection at end (and also at the end) of the way:
intersectionatend

Test Results:

Ran the changes on a shard that contains such intersections and visually verified correct stitching and connectivity.

In doubt: Contributing Guidelines

jklamer
jklamer previously approved these changes Sep 19, 2018
Copy link
Contributor

@jklamer jklamer left a comment

Choose a reason for hiding this comment

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

Looks good! Nice unit testing.

if (locationIsPartOfAnIntersectingEdgeOfTheSameLayer(shapePoint, line))
{
addPointToNodeList(shapePoint, nodesForEdge);
}

// 4. Check if there is an intersecting edge, of a different layer at this
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: In the comment I believe this is described as case 3b?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point, I'll update the comment to reflect the 3b

// Find all intersecting edges
.stream(this.rawAtlas.linesContaining(location,
target -> target.getIdentifier() != line.getIdentifier()
&& isAtlasEdge(target) && target.asPolyLine().contains(location)))
Copy link
Contributor

Choose a reason for hiding this comment

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

does this.rawAtlas.lineContaining() also return lines where the location isn't a shape point but just somewhere along a segment?

Copy link
Collaborator

Choose a reason for hiding this comment

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

It only looks at the shape points. But @MikeGost can correct me here, I think location is only a start/end point, so it should match with a shape point, right?

Copy link
Contributor Author

@MikeGost MikeGost Sep 19, 2018

Choose a reason for hiding this comment

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

@matthieun is correct, the lineContaining() call only looks at the underlying PolyLine shape points. The location is the intersection location - I will rename it to make that more clear. And then in the anyMatch block, we make sure that the intersection happens at the start/end point only, not somewhere in between.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Correction, the location is just a shape point, with a possible intersection that we are checking for.

return Iterables
// Find all intersecting edges
.stream(this.rawAtlas.linesContaining(location,
target -> target.getIdentifier() != line.getIdentifier()
Copy link
Contributor

Choose a reason for hiding this comment

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

do you have to worry about the reverse edge here or are these line just the OSM ways navigable or not?

Copy link
Collaborator

Choose a reason for hiding this comment

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

This way-sectioning phase works on a "raw" atlas that only contains lines, points and relations. This step is what determines what becomes an edge and a line, and that creates the reverse edges. At that point, there are no reverse edges yet.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's correct - no need to worry about reverse edges here. We only see Lines, that will become Edges, at this point and are trying to establish all Node locations. Once we have that, we will section this Line to create forward and reverse Edges, as needed, based on the tagging.

final PolyLine candidatePolyline = candidate.asPolyLine();
final boolean intersectionIsAtEndPoint = candidatePolyline.first()
.equals(location) || candidatePolyline.last().equals(location);
return edgesOnDifferentLayers && intersectionIsAtEndPoint;
Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure if you're trying to absolutely squeeze performance here, but if you want to avoid doing

final boolean intersectionIsAtEndPoint = candidatePolyline.first()
                             .equals(location) || candidatePolyline.last().equals(location);

unless necessary you can squeeze it into

return edgesOnDifferentLayers && (candidatePolyline.first()
                             .equals(location) || candidatePolyline.last().equals(location));

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point, I actually had that at first, and then changed it back to the current version for readability. It probably makes sense to change it back. Thanks for the catch!

Copy link
Contributor

@lucaspcram lucaspcram Sep 19, 2018

Choose a reason for hiding this comment

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

This is true, but worst case (assuming the compiler does not do final variable optimizations) it's only 2 extra byte code instructions (an istore_ and an iload_ for the intersectionIsAtEndPoint variable).

I think having the name of what that predicate represents (intersectionIsAtEndPoint) is useful for readability - especially if someone else needs to dig back into the code in the future.

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'll change it back!

Copy link
Contributor

Choose a reason for hiding this comment

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

Code tennis! :)

matthieun
matthieun previously approved these changes Sep 19, 2018
Copy link
Collaborator

@matthieun matthieun left a comment

Choose a reason for hiding this comment

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

Nice fix @MikeGost! Would be nice to test on some shards to make sure it does not introduce new hotspots (if not done already)

return Iterables
// Find all intersecting edges
.stream(this.rawAtlas.linesContaining(location,
target -> target.getIdentifier() != line.getIdentifier()
Copy link
Collaborator

Choose a reason for hiding this comment

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

This way-sectioning phase works on a "raw" atlas that only contains lines, points and relations. This step is what determines what becomes an edge and a line, and that creates the reverse edges. At that point, there are no reverse edges yet.

// Find all intersecting edges
.stream(this.rawAtlas.linesContaining(location,
target -> target.getIdentifier() != line.getIdentifier()
&& isAtlasEdge(target) && target.asPolyLine().contains(location)))
Copy link
Collaborator

Choose a reason for hiding this comment

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

It only looks at the shape points. But @MikeGost can correct me here, I think location is only a start/end point, so it should match with a shape point, right?

@MikeGost
Copy link
Contributor Author

Good point @matthieun - that was my plan for today :)

@MikeGost MikeGost dismissed stale reviews from matthieun and jklamer via 5c463d5 September 19, 2018 16:31
lucaspcram
lucaspcram previously approved these changes Sep 19, 2018
Copy link
Contributor

@lucaspcram lucaspcram left a comment

Choose a reason for hiding this comment

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

Looks good to me @MikeGost , nice testing.

I had a slightly different opinion than you and @jklamer on one of the changes, but it's not that big a deal.

Copy link
Contributor

@AlexHsieh22 AlexHsieh22 left a comment

Choose a reason for hiding this comment

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

a few minor suggestions, overall LGTM!

{
final long layerValue = LayerTag.getTaggedOrImpliedValue(candidate, 0L);
final boolean edgesOnDifferentLayers = targetLayerValue != layerValue;
final PolyLine candidatePolyline = candidate.asPolyLine();
Copy link
Contributor

Choose a reason for hiding this comment

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

not sure what the compiler will do in this case, but does it make sense to return early if edgesOnDifferentLyaers is false?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Gave this some thought during original implementation. I don't think the performance improvement would be that great, considering that the intersection logic is fairly trivial (two equality checks for Location class). My thought was to weight readability over minor performance. Will consider changing if I see this misbehaving on the cluster :)

{
final long targetLayerValue = LayerTag.getTaggedOrImpliedValue(line, 0L);

return Iterables
Copy link
Contributor

Choose a reason for hiding this comment

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

This code to return a stream of intersecting edges (or even a list) seems reusable. Thoughts on pulling it out into a helper function?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Same comment here. Going to keep as is for readability here.

@matthieun matthieun merged commit 2b343dc into osmlab:dev Sep 21, 2018
@matthieun matthieun added this to the 5.1.13 milestone Oct 17, 2018
@MikeGost MikeGost deleted the subAtlas branch November 8, 2018 20:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants