-
-
Notifications
You must be signed in to change notification settings - Fork 854
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
Alternative Routes Support #3447
base: master
Are you sure you want to change the base?
Conversation
same as title
1. Changed the indent to 2 spaces 2. Moved the instantiations of RoutingOptions member variable of EdgeEstimator to its constructor, so we no longer need to create a new RoutingOptions object before each CalcSegmentWeight() call 3. Added the similar mechanisms to BicycleEstimator and PedestrianEstimator
1. Moved the routing option setting to the asynchronous router 2. Developed the strategy feature for edge estimator and index graph 3. Finished implemented the shortest path strategy 4. Additionally separated the extra weights of avoid options from ETA calculations
Added new strategy "Fewer Turns" with the corresponding turn check for connected segments u and v
Also added the UI for adjusting avoid routing options for desktop
Also made some minor modifications to the routing calculations in the backend
Branch can be rebase to test changes ? |
I didn’t understand yet, if it is planned to implement an option for users to choose between shortest and fastest route. |
Is it ready for review? What is the current status here? |
I have made a report on my changes and the logic behind them. Please let me know if you have any questions or suggestions.
Alternative Routes: What, How, and Why
This is the page that documenting the key features developed in the Alternative Routes Project for Organic Maps during GSoC 2022. In this report, the author is going to go through the key features one by one while discussing the problems behind the scene, how we coped with the problems, and why we did it in these ways.
Feature 1: Quantified Weight Calculations for Avoid Routing Options
Problem
As some of our users pointed out: Sometimes when a specific toll road is unavoidable, the current "avoid toll roads" option turns useless because no possible routes can be generated based on the current algorithm.
The figure above shows a good example from the user: When trying to reach the southern island in Hong Kong, all the available routes have toll roads.
When "avoid toll roads" option is enabled, no routes can be generated.
The reason behind this is that we are removing all the road candidates with toll when building the route. More specifically, in /routing/index_graph.cpp, we are enabling the avoid mechanisms by conditionally return early in IndexGraph::GetNeighboringEdges() and IndexGraph::GetSegmentCandidateForRoadPoint():
Consequently, roads of the type to avoid will never be taken into consideration for route building, thus making it impossible to generate routes when some specific roads are unavoidable.
Solution
In order to cope with this problem, we came up with the idea of quantifying the avoid routing options. In other words, we take the roads of target type into consideration, but we add extra weight to them. We removed the "candidate dropping" process mentioned above in IndexGraph::GetNeighboringEdges() and IndexGraph::GetSegmentCandidateForRoadPoint() and we added some conditional statements to CalcSegmentWeight() overrides in edge_estimator.cpp. Below is the example for pedestrian estimator:
EdgeEstimator::GetAvoidRoutingOptions() is a new method added to the EdgeEstimator class. In the new implementation, we make the EdgeEstimator to hold the current "state" of avoid routing options. This is done by adding a new member variable "Routing Options m_avoidRoutingOptions" and its corresponding methods. As a result, when estimating the road weight in the route building process, the modified edge estimator can check the current avoid routing options and add extra weight to the results of calculations.
In addition, how is this member variable initialized? In fact, the preparations for this start before the entire route calculations begin. More specifically, the state of EdgeEstimator is set before calling router->CalculateRoute() in async_router.cpp:
Note that "SetEstimatorOptions" is a new method added to IndexRouter for setting the state of its member variable m_estimator.
With the new implementation, the router can still generate routes when we cannot completely avoid a specific type of road. Nevertheless, the route generated minimizes the number of target road type encounters. In this case, as you can see in the figure above, the route takes a detour such that only one charge is needed halfway (which is two if we do not select "avoid toll roads").
Rationale
Why we are making the EdgeEstimator to hold the state of current avoid routing option? This is because the weight calculations for candidate roads happen here. We do not need to implement any additional message passing mechanism before calling CalcSegmentWeight(). Furthermore, there is a one-to-one mapping between IndexRouter and EdgeEstimator (m_estimator is the unique member variable of IndexRouter). So, we do not need to worry about the avoid routing option state in our weight calculations is out of sync with the router.
Why we are only considering the during the number of toll charges in case the cost of one toll road can be higher than multiple toll roads combined? Well, this issue has been discussed before. Different countries have different regulations and we have not come up with a universal idea especially when OSM may not even have the corresponding data.
Future Improvements
As you can see in the earlier section, the "extra weight" value we added to the road to avoid is quite arbitrary: "24 * 60 * 60" -- the time of a day. The logic behind this is straightforward: we are trying to make the weight of roads to avoid "large." But how large? This is probably a question worth further discussions. Maybe we can some dynamic road condition analyses mechanisms here so different roads to avoid have different weights.
Feature 2: The Routing Strategy Feature and Shortest Path
Problem
As stated in the project idea: "The primary limitation of Organic Maps is that only one route variant can be provided by the planner." So, apart from building the fastest route, we also need to have some other routing strategies. The easiest one is "shortest path."
Solution
So, how did we enable the the routing strategy feature? Our solution is pretty much the same as enabling the avoid routing options -- we added a routing strategy "state" for EdgeEstimator, which is a new member variable "Strategy m_strategy." And "Strategy" is a newly defined enumeration class similar to the Road class in routing_options.hpp. Just like the avoid routing options mentioned previously, the initialization of routing strategy is finished before the routing process begins in async_router.cpp:
where methods like "SetEstimatorStrategy()" are also added to IndexRouter class for strategy related operations. Moreover, we also added methods "LoadRoutingStrategyFromSettings()" and "SaveRoutingStrategyToSettings()" which mimic the operations of "LoadRoutingStrategyFromSettings()" and "SaveRoutingStrategyToSettings()" in EdgeEstimator.
As for the implementation of shortest path strategy, our solution is also very straightforward -- eliminate all the extra weight and penalties in weight calculation and make the estimated time proportional with the distance of traveling. Through observations of the original segment weight calculations, we found out that all the extra weight (except those added later for avoid options) are added in CalcClimbSegment() in edge_estimator.cpp. So, we inserted two conditional statements into double CalcClimbSegment():
So, if we have "strategy == EdgeEstimator::Strategy::Shortest" when calculating the segment weight, then the return value of CalcClimbSegment() is simply the distance of road divided by 1.0, making the segment weight proportional with the distance of the road.
Furthermore, we also dismissed the penalty calculations (which is different from weight calculations) in "RouteWeight IndexGraph::CalculateEdgeWeight()" in index_graph.cpp:
As a result, "GetPenalties" will not be called after weight calculations if we have the shortest path strategy.
One example of a "fastest" route in Hong Kong. As you can see in the figure above, the planner tries to make the route go along as many "primary" roads as possible. This strategy takes some detours but the ETA is shorter due to the fact that primary roads have higher speed limit.
With "shortest" strategy enabled, as you can see in the figure above, the planner made a significantly shorted route. But now that we are having more "secondary" roads along the way, the ETA tends to be longer than the previous strategy.
Rationale
The author thinks most of the controversial problems have been discussed earlier in the Solution section. If you have any other problems in this regard, please contact the author.
Future Improvements
When the router is trying to evaluate which road to pick for the route, it seems right now we have two values to support the evaluation -- "weight" and "Penalty." However, the boundary between the two is not very clear from the author's perspective. We have been struggling with which value we should change we implementing avoid routing options and new strategies. "Weight" seems to account for the calculations of climbing penalties of some vehicles, while "Penalty" is responsible for U turns, ferries, and some other malicious factors. Nevertheless, there is no clear definition of the two so far. So, maybe in the future we can re-organize the calculations for weight and penalties so that the route planning process is more efficient and is easier to understand.
Feature 3: Turns Check and the Fewer Turns Strategy
Problem
Taking turns takes time, especially for cars. However, our current implementation does not have a complete mechanism for calculating the penalties of taking turns (We do have U-turn checks and penalties implemented, but we do not have those for normal turns). Besides, "fewer turns" routing strategy is frequently requested by the users. So, it is necessary to implement the functions of:
Solution
How did we implement the function of checking whether there is a turn? We found out that there has already been a similar U-turn checking function in the code base (index_graph.cpp):
Then, we mimicked the style of this U-turn-checking function and made a common turns check function that is slightly more complicated:
A brief explanation of the turn checking process: First, we fetch the latitude and longitude of the starting/ending points of segments u(from) and v(to). Second, using the coordinates of the 4 points, we calculate the vectors of segment u and segment v. Afterwards, we can calculate the dot product and the determinant of the two vectors. Then, the angle between the two vectors can be calculated by taking arctan (check this for references). Last but not least, we check the size of the angle to determine whether we are taking turns between the segments u and v.
How do we calculate the penalties of taking these turns after finding them? We also implemented this in the same way as adding penalties to U-turns. In IndexGraph::GetPenalties(), previously we have:
Now we added a new conditional statement after this:
where GetTurnPenalty is implemented for all child classes of EdgeEstimator. Below is an example from CarEstimator:
Here is a simple comparison between the newly implemented "fewer turns" routing strategy with the original "fastest" routing strategy:
The figure above shows a common fastest route in Hong Kong.
When "fewer turns" strategy enabled, as we can see in the figure above, the router makes the route dramatically "straighter." But the new route is also longer due to the detours.
Rationale
You may have noticed this in the very beginning of the code of IndexGraph::IsTurn(): Why we are doing boundary checks before all the turn calculations? The reason behind this is that, in some situations, though very rare, a segment goes backwards AND has ID = 0, making it impossible to get the valid end point coordinates due to negative overflows. Therefore, we need to make sure that the calculations would proceed without this problem.
Another issue is that, since we already have a "IsTurn()" function that may cover the situations of "IsUTurn()," why don't we delete the "IsUTurn()" and it's usages? This is because "IsTurn()" does not exactly covers "IsUTurn()." If you check the code of "IsUTurn()" carefully you will find out that it works in a different way: it checks whether we are moving on the same "edge" in opposite directions. Moreover, IsUTurn() seems to be used by a variety of other functionalities outside this project. Deleting it may cause unexpected troubles. Meanwhile, keeping IsUTurn may also benefit the route building to some extent -- we are adding "double penalties" to U-turns compared with normal turns. So, the router may not try to take more time-consuming U-turns to avoid normal turns.
Future Improvements
One simple and meaningful improvement is to specify the "turns" into "left turns" and "right turns." This can be done by modify the angle judgement part in "IsTurn":
We can change it to something like this:
This is true because the return value of atan2() in is of range [-pi, +pi]. After converting the radians to degrees we have
-180 <= angle <= 180
In the meantime, how we determine there is an angle can also be improved. Currently, we are simply comparing the angle with 15 degree. But in real life this can be complicated (imagine a crooked road with no crossings has a 30 degree angle). So, maybe in the future we can implement a dynamic turn judging mechanism which decides whether there is a turn by analyzing more road geometry data.
The penalty value for taking turns is another very interesting topic. As showed in the previous section, we are using "60 * 60" for the turns penalty in case we have fewer turns strategy enabled. We set it to be this value because we would like to prioritize the avoid routing options over routing strategies (24 * 60 * 60 additional weight for roads to avoid). But apparently we can make the penalty values better-designed by taking the specific road condition into calculation.