-
Notifications
You must be signed in to change notification settings - Fork 80
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
Conversation
There was a problem hiding this 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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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))) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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));
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code tennis! :)
There was a problem hiding this 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() |
There was a problem hiding this comment.
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))) |
There was a problem hiding this comment.
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?
Good point @matthieun - that was my plan for today :) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this 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(); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
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:
Intersection at end (and also at the end) of the way:
Test Results:
Ran the changes on a shard that contains such intersections and visually verified correct stitching and connectivity.
In doubt: Contributing Guidelines