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 Average Speed as PathDetails to Response #1091
Conversation
lat = nodeAccess.getLat(baseNode); | ||
lng = nodeAccess.getLon(baseNode); | ||
for (; pointIndex < points.size(); pointIndex++) { | ||
if (lat == points.getLat(pointIndex) && lng == points.getLon(pointIndex)) { |
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.
Is it safe comparing coordinates without epsilon?
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 should be, since they come from the same source? I could add a delta, though. But it's good that you remind me, I wanted to check how the algorithm behaves for loops etc.
Thanks!
Maybe just run 100K routes for Germany via the Measurement class :) ? |
It might be interesting here or in a separate issue to get rid of this class entirely as we now could let jackson do this serialization |
Ah, I meant if the correct nodes are matched, just because at a crossing of the route it could be point=50 on the first way but point 270 on the way back, with waypoints we start the counting from the beginning for every path, but after every waypoint a new path starts, but we need to consider the points from the start to the end of the path to calculate the correct index, so maybe we have to remember the last index or something like that (not sure if this is understandable, not easy to explain without code) For example a route like this: https://graphhopper.com/maps/?point=49.368066%2C9.703674&point=49.396452%2C9.753628&point=49.375332%2C9.75071&point=49.38606%2C9.683933 When comparing only coordinates we might produce incorrect results when the routes cross.
👍 |
Sorry didn't yet look into the source code. Why do we need coordinate comparison? |
Not sure if we need it, I used this to calculate the interval, but now that you mention it, we could probably also count the number of points per edge and just sum it up. |
Yes, please :). To make this then more secure we need probably the same tests that you mention but matching is only good if we have no other choice |
I updated this PR so that the PathDetails are now stored similar to the instructions, so every PathDetail knows the number of points it contains. The benefit here is that when we run the simplification we don't have to update every PathDetail, but only the ones that are actually changed. When serializing I run a similar approach as the instruction to create actual intervals. I saw a "minor issue" close to waypoints. Sometime the start of the PathDetails move slightly when the waypoints are placed close to the beginning of an edge, where the PathDetails would change. Not sure what's the reason, but I think it could have something to do with VirtualEdges, but haven't checked, since I am not sure if it's worth the trouble, see the attached screenshots. The route is in Baden-Württemberg: http://localhost:8989/?point=48.596365%2C8.634396&point=48.586398%2C8.618098&point=48.578155%2C8.609955&details.average_speed=true&debug_details=true |
Thanks! Will have a look
I think it is important to understand the issue to judge if we should fix it or not. |
for (PathDetails pathDetails : details) { | ||
pathDetailsMap.put(pathDetails.getName(), pathDetails.getPathDetailsMap()); | ||
} | ||
jsonPath.put("details", pathDetailsMap); |
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.
Can't we use a Map when returning from ar.getPathDetails so that we could write jsonPath.put("details", ar.getPathDetails())? Or annotate PathDetails so that we get the JSON output that we want via jackson.
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.
Yes, makes sense 👍
public class PathDetailsFromEdges implements Path.EdgeVisitor{ | ||
|
||
int i; | ||
PathDetailsCalculator calc; |
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.
Would you mind to move everything inside the loop? This reads confusing.
calc = calculators.get(i); | ||
if (calc.edgeIsDifferentToLastEdge(edge)) { | ||
details.get(i).endInterval(numberOfPoints); | ||
details.get(i).startInterval(calc.getCurrentValue()); |
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 reads a bit strange that the index is used for calculators and details. Can we somehow assign one calculator to the PathDetails?
Also can we avoid stateful stuff like with the .reset call? (if possible at all)
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.
Don't you think that it makes sense to have calculator per detail? Essentially every calculator handles one PathDetail. So if you want to add a PathDetail in the Response, you have to pass the calculator to the PathMerger.
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.
But why should we use two independent lists? Why not e.g. setting the calculator as variable of PathDetails?
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.
Also can we avoid stateful stuff like with the .reset call? (if possible at all)
I don't like the reset call either. Mhm, we could create a new calculator for every Path. However, I think it makes sense in this case that the Calculator has a state and that we don't have to keep the state outside of the calculator. This allows us to handle complex states, using multiple variables in the calculator, but we don't have to care about it from the outside. WDYT?
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.
Why not create fresh calculators for every Path?
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.
Why not create fresh calculators for every Path?
On the other hand, why creating the overhead :)?
I think we could create a better setup here. In the GraphHopper.java class we could create a PathDetailsFactory that is initialized with the GHRequest. It reads the request and can create fresh calculators on request?
return pathDetails; | ||
} | ||
|
||
public void merge(PathDetails pD) { |
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.
Why do we need the merge method?
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.
To merge the PathDetails for every Path. We could also do this completely in the PathMerger class. At first I created it because PathDetails were a map and it made a lot of sense then. With the current code it could be removed and done in the PathMerger.
|
||
@Override | ||
public String getName() { | ||
return "average-speed"; |
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.
Potentially we should use the same name average_speed
like we use for the parameter in the URL?
@@ -86,8 +95,8 @@ public void doWork(PathWrapper altRsp, List<Path> paths, Translation tr) { | |||
} | |||
|
|||
for (Instruction i : il) { | |||
origPoints += i.getPoints().size(); |
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.
Was this a bug in the previous state of code?
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.
Yes. Well it was only used for logging, so it was not really bad, also it only appeared when having multiple Paths. I thought about creating an issue for it but then I thought it's not worth to create an issue for this.
List<PathDetails> details = new ArrayList<>(calculators.size()); | ||
for (PathDetailsCalculator calc : calculators) { | ||
details.add(new PathDetails(calc.getName())); | ||
calc.reset(); |
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.
Why do we have to call reset for the initialization?
this.pathDetails.get(i).merge(details.get(i)); | ||
} | ||
} | ||
} |
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.
See below: what is a 'merge'? And why not just adding PathDetails like the method says it?
Also instead of EMPTY_LIST and if+else I would prefer without the if and always do the same.
Ok, so I tried to dig into this issue a bit. I compared these two routes: I understood the issue now. There is one point in the resulting points that is reserved for the via point that cannot be calculated via sumarizing points of edges but has to be plainly added if there is a via point. |
I updated this PR. I added a way to simplify the Path for PathDetails+Instructions. So the simplification keeps the points in between the different Instructions and PathDetails. Optically, when using the Debugging view, the results look correct. I can also see that there is a difference of the response size if I pass I would have expected that the GHResponse contains the same amount of points as before if it only contains instructions. This is not true right now. We have less points, I am not exactly sure if this indicates an issue or not. I haven't found the reason yet. This is also the reason why the tests are failing right now. |
The merge method should be easier as we can just call pathDetails.setLast(otherDetail.getLast()) but I didn't get the simplification working easily. Would you have a look? |
int removed = douglasPeucker.simplify(pointList, nonConflictingStart, nonConflictingEnd); | ||
if (removed > 0) { | ||
for (int i = 0; i < toSimplify.size(); i++) { | ||
reduceNumberOfPoints(toSimplify.get(i), offset[i], removed, startIntervals[i], endIntervals[i] - removed); |
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.
Here I didn't understood what nonConflictingXY means ... I think we have to call this for all PathDetails even if no simplification is possible, as we need to change the pathDetail.first variable for all entries following a simplified PathDetail.
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.
The nonConflictingXY describes the index from where to where we can simplify without potentially remove a point that we might have to reference later.
I think we have to call this for all PathDetails even if no simplification is possible, as we need to change the pathDetail.first variable for all entries following a simplified PathDetail.
Yes this is true, since we don't store the length anymore, but the indexes we have to propagate our change trough all PathDetails, which is not very nice. That's btw the main reason why I decided earlier to only use length instead of indexes.
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.
Yes this is true, since we don't store the length anymore, but the indexes we have to propagate our change trough all PathDetails, which is not very nice. That's btw the main reason why I decided earlier to only use length instead of indexes.
Ok, I just checked, we actually don't necessarily have to do this. Right now the Simplification is completely based on the length and does not use the index of the toSimplify elements at all, since the length of elements is fixed until they are actually simplified, it still works.
If you check how the startInterval and endInterval are managed, you will see that we don't use the index, but only the length of the elements. However, it looks a bit like "magic" and you don't understand it on first sight, which I generally don't like. On the other hand the simplification is a bit more complex anyway so it might be ok, that the code is not completely understandable by just skimming over it.
endReached = true; | ||
} | ||
if (offset[toShiftIndex] >= toSimplify.get(toShiftIndex).size()) | ||
break; |
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.
should we just return pointList
?
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.
If possible I try to return at the end of a method
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.
Ok, I am fine with that :)
@@ -98,6 +98,16 @@ public PointList simplify() { | |||
if (removed > 0) { | |||
for (int i = 0; i < toSimplify.size(); i++) { | |||
reduceLength(toSimplify.get(i), offset[i], startIntervals[i], endIntervals[i] - removed); | |||
/* |
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.
As you can see, the tests succeed even if we don't do this. Also the result on the map with debug=true looks the same to me in both cases, so this seems unnecessary. As described in my other comment and I would remove it before we merge this. I just left it there so you can compare it.
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.
Ah, I think I have now fully understood the complexity, so when there are n PathDetails we do n simplifications and roughly n²/2 updates to the PathDetail object. The only alternative would be to sort the PathDetails by first index, create a dependency graph and then being to only update the PathDetails that are necessary
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.
But this is not necessary as a first version (and probably even later)
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.
Ah, I think I have now fully understood the complexity, so when there are n PathDetails we do n simplifications
No, we only have one PointList that we simplify. We only update the PahtDetails and Instructions. That's why I would like to remove the Points from the Instructions :).
roughly n²/2 updates to the PathDetail object
No, as I said, we don't need to propagate the changes. Everything works with this code commented out. At first I thought it would be necessary though.
The only alternative would be to sort the PathDetails by first index, create a dependency graph and then being to only update the PathDetails that are necessary
Instead of doing the simplififcation per PathDetails I tried to do it one run, which makes the process a bit more complicated but saves us a lot of computation (I think).
There is stilll a problem in the simplification (I think it is not sufficient to call reduceLength only for cases with
It is a bit strange that when you try this in the Measurement class against Germany with my minor changes there will be no cases for this problem. |
Thanks for finding this. It seems that this issue is fixed by uncommenting the loop which I thought that it would be unecessary. Apparently, the loop that I thought would not be needed is necessary :). When I uncomment the code, the glitch is fixed. I also found another issue, when a connection was not found, we should not calculate the PathDetails. Now the measurement for Baden-Württemberg and Bayern works. |
/* | ||
// TODO This is not needed, I left it here temporarily, has to be removed before merging | ||
if(listsToSimplify.get(i).get(0) instanceof PathDetail){ | ||
if (listsToSimplify.get(i).get(0) instanceof PathDetail) { |
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.
Why should it be necessary only for PathDetails?
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.
Ah, because for the remaining PathDetails we need to update the indices and instructions have the pointlist stored and do not need an update if the length changes
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.
Yes exactly 👍
@@ -377,7 +377,7 @@ public InstructionList calcInstructions(final Translation tr) { | |||
* @return List of PathDetails for this Path | |||
*/ | |||
public Map<String, List<PathDetail>> calcDetails(final PathDetailsBuilderFactory pathBuilderFactory, int previousIndex) { | |||
if (pathBuilderFactory == null) | |||
if (!isFound() || edgeIds.isEmpty() || pathBuilderFactory == null) |
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.
Hmmh, we should add an integration test and not calling PathMerger at all or wdyt?
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.
Also understand this now ;) and will add an integration test.
I think the edgeIds.isEmpty()
should not be here as a PathDetail could still be included even if the path has no length. So we have to do similar things that we do in calcInstructions when the path is empty but the finish instruction should be there.
Thanks! Another minor thing I noticed is that |
if (listsToSimplify.get(i).get(0) instanceof PathDetail) { | ||
List<PathDetail> pathDetails = listsToSimplify.get(i); | ||
for (int j = offset[i] + 1; j < pathDetails.size(); j++) { | ||
List<PathDetail> pathDetails = listsToSimplify.get(i); |
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 looks wrong, it can be either InstructionList or a list of PathDetail. It works, but I think we should not leave it like that. WDYT?
Mhm, hard to say. For osm users |
if (pathDetails.get(0) instanceof PathDetail) { | ||
for (int j = offsets[i] + 1; j < pathDetails.size(); j++) { | ||
PathDetail pd = pathDetails.get(j); | ||
PathDetail pd = (PathDetail) pathDetails.get(j); |
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.
ups, thanks!
Thanks @boldtrn for all the work - this is a nice new feature! |
Fixes #439.
I added the possibility to view details of the calculated as proposed in #439. It should be very easy to extend so we can add different details.
The calculation of the PathDetails is a bit similar to Instructions and maybe in the future we might find that it makes sense to merge them, but not now.
I haven't measured the performance yet, but I can do this if you like the general approach.
Simplification is disabled when adding PathDetails right now.
Right now I don't like two things:
Future improvements:
Change the way the Instructions are generated so that we generate the intervals for the instructions in the PathMerger. Then we could try to simplify points between PathDetails/Instructions without simplifying over the bounds of any of the two.