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

Alternative Routes Support #3447

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open

Alternative Routes Support #3447

wants to merge 6 commits into from

Conversation

melonSkin76
Copy link

@melonSkin76 melonSkin76 commented Sep 18, 2022

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.


original_normal

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.


original_avoid_fail

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():

if (!road.SuitableForOptions(m_avoidRoutingOptions))
    return;

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:

  double CalcSegmentWeight(Segment const & segment, RoadGeometry const & road, Purpose purpose) const override
  {
    double result = CalcClimbSegment(purpose, segment, road, GetPedestrianClimbPenalty, this->GetStrategy());

    if (purpose == EdgeEstimator::Purpose::Weight)
    {
      if (!road.SuitableForOptions(EdgeEstimator::GetAvoidRoutingOptions()))
      {
        result += (24 * 60 * 60);
      }
    }

    return result;
  }

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:

RoutingOptions const routingOptions = RoutingOptions::LoadCarOptionsFromSettings();
router->SetEstimatorOptions(routingOptions.GetOptions());

Note that "SetEstimatorOptions" is a new method added to IndexRouter for setting the state of its member variable m_estimator.


new_avoid_toll

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:

EdgeEstimator::Strategy routingStrategy = EdgeEstimator::LoadRoutingStrategyFromSettings();
router->SetEstimatorStrategy(routingStrategy);

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():

  if (strategy == EdgeEstimator::Strategy::Shortest && purpose == EdgeEstimator::Purpose::Weight)
  {
    speedMpS = 1.0;
  }
  CHECK_GREATER(speedMpS, 0.0, ("from:", from.GetLatLon(), "to:", to.GetLatLon(), "speed:", speed));
  double const timeSec = distance / speedMpS;

  if (base::AlmostEqualAbs(distance, 0.0, 0.1) || strategy == EdgeEstimator::Strategy::Shortest)
    return timeSec;

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:

RouteWeight IndexGraph::CalculateEdgeWeight(EdgeEstimator::Purpose purpose, bool isOutgoing,
                                            Segment const & from, Segment const & to,
                                            std::optional<RouteWeight const> const & prevWeight) const
{
  auto const & segment = isOutgoing ? to : from;
  auto const & road = GetRoadGeometry(segment.GetFeatureId());

  auto const weight = RouteWeight(m_estimator->CalcSegmentWeight(segment, road, purpose));

  if (purpose == EdgeEstimator::Purpose::Weight)
  {
    if(m_estimator->GetStrategy() == EdgeEstimator::Strategy::Shortest)
    {
      return weight;
    }
  }

  auto const penalties = GetPenalties(purpose, isOutgoing ? from : to, isOutgoing ? to : from, prevWeight);

  return weight + penalties;
}

As a result, "GetPenalties" will not be called after weight calculations if we have the shortest path strategy.


original_short

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.


new_short

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:

  1. checking whether there is a turn
  2. calculating the cost of taking the turn

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

bool IsUTurn(Segment const & u, Segment const & v)
{
  return u.GetFeatureId() == v.GetFeatureId() && u.GetSegmentIdx() == v.GetSegmentIdx() &&
         u.IsForward() != v.IsForward();
}

Then, we mimicked the style of this U-turn-checking function and made a common turns check function that is slightly more complicated:

bool IndexGraph::IsTurn(Segment const & u, Segment const & v) const
{
  // Boundary check for segmentIdx
  if (u.GetSegmentIdx() == 0 && !(u.IsForward()))
  {
    return false;
  }

  if (v.GetSegmentIdx() == 0 && !(v.IsForward()))
  {
    return false;
  }

  auto geoU = GetRoadGeometry(u.GetFeatureId());
  auto startPointU = geoU.GetPoint(u.GetSegmentIdx());
  auto endPointU = geoU.GetPoint(u.IsForward() ? u.GetSegmentIdx() + 1: u.GetSegmentIdx() - 1);

  auto geoV = GetRoadGeometry(v.GetFeatureId());
  auto startPointV = geoV.GetPoint(v.GetSegmentIdx());
  auto endPointV = geoV.GetPoint(v.IsForward() ? v.GetSegmentIdx() + 1: v.GetSegmentIdx() - 1);

  if (!(endPointU == startPointV))
  {
    return false;
  }

  double vectorU[2] = {endPointU.m_lat - startPointU.m_lat, endPointU.m_lon - startPointU.m_lon};
  double vectorV[2] = {endPointV.m_lat - startPointV.m_lat, endPointV.m_lon - startPointV.m_lon};

  //dot product
  double dot = vectorU[0] * vectorV[0] + vectorU[1] * vectorV[1];

  //determinant
  double det = vectorU[0] * vectorV[1] - vectorU[1] * vectorV[0];

  //calculate the angle
  double angle = atan2(det, dot);

  //convert to degree value
  angle = angle * 180 / 3.141592;

  if (abs(angle) >= 15)
  {
    return true;
  }
  else
  {
    return false;
  }
}

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:

  double weightPenalty = 0.0;
  if (IsUTurn(u, v))
    weightPenalty += m_estimator->GetUTurnPenalty(purpose);

Now we added a new conditional statement after this:

double weightPenalty = 0.0;
  if (IsUTurn(u, v))
    weightPenalty += m_estimator->GetUTurnPenalty(purpose);

  if (IsTurn(u, v))
    weightPenalty += m_estimator->GetTurnPenalty(purpose);

where GetTurnPenalty is implemented for all child classes of EdgeEstimator. Below is an example from CarEstimator:

double CarEstimator::GetTurnPenalty(Purpose purpose) const
{
  if (purpose == EdgeEstimator::Purpose::Weight)
  {
    if (this->GetStrategy() == EdgeEstimator::Strategy::FewerTurns)
    {
      return 60 * 60;
    }
  }
  return 0.0;
}

Here is a simple comparison between the newly implemented "fewer turns" routing strategy with the original "fastest" routing strategy:

original_turns

The figure above shows a common fastest route in Hong Kong.


new_turns

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

  if (abs(angle) >= 15)
  {
    return true;
  }
  else
  {
    return false;
  }

We can change it to something like this:

  if (angle > 0)
  {
    // right turn
  }
  else
  {
    // left turn
  }

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.

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
@Jean-BaptisteC
Copy link
Member

Branch can be rebase to test changes ?

@lupolupp
Copy link

I didn’t understand yet, if it is planned to implement an option for users to choose between shortest and fastest route.
I made experience, that sometimes faster route is not really faster, because (I think) the algorithm calculates with the highest possible relating allowed speed. But often you can‘t drive the full allowed speed.
Second problem is, that fastest route sometimes is more complicated than one, what takes only few minutes more.
I often made this experience in areas I know by myself.

@rtsisyk
Copy link
Contributor

rtsisyk commented Nov 23, 2023

Is it ready for review? What is the current status here?

@patepelo patepelo added Routing Route building issues, e.g. valid route, valid ETA Route Planning Preview and plan your track labels Nov 23, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Route Planning Preview and plan your track Routing Route building issues, e.g. valid route, valid ETA
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants