diff --git a/features/guidance/collapse.feature b/features/guidance/collapse.feature new file mode 100644 index 000000000..9cde9eb3f --- /dev/null +++ b/features/guidance/collapse.feature @@ -0,0 +1,347 @@ +@routing @guidance @collapsing +Feature: Collapse + + Background: + Given the profile "car" + Given a grid size of 20 meters + + Scenario: Segregated Intersection, Cross Belonging to Single Street + Given the node map + | | | i | l | | | + | | | | | | | + | d | | c | b | | a | + | e | | f | g | | h | + | | | | | | | + | | | j | k | | | + + And the ways + | nodes | highway | name | oneway | + | ab | primary | first | yes | + | bc | primary | first | yes | + | cd | primary | first | yes | + | ef | primary | first | yes | + | fg | primary | first | yes | + | gh | primary | first | yes | + | ic | primary | second | yes | + | bl | primary | second | yes | + | kg | primary | second | yes | + | fj | primary | second | yes | + | cf | primary | first | yes | + | gb | primary | first | yes | + + When I route I should get + | waypoints | route | turns | + | a,l | first,second,second | depart,turn right,arrive | + | a,d | first,first | depart,arrive | + | a,j | first,second,second | depart,turn left,arrive | + | a,h | first,first,first | depart,continue uturn,arrive | + | e,j | first,second,second | depart,turn right,arrive | + | e,h | first,first | depart,arrive | + | e,l | first,second,second | depart,turn left,arrive | + | e,d | first,first,first | depart,continue uturn,arrive | + | k,h | second,first,first | depart,turn right,arrive | + | k,l | second,second | depart,arrive | + | k,d | second,first,first | depart,turn left,arrive | + | k,j | second,second,second | depart,continue uturn,arrive | + | i,d | second,first,first | depart,turn right,arrive | + | i,j | second,second | depart,arrive | + | i,h | second,first,first | depart,turn left,arrive | + | i,l | second,second,second | depart,continue uturn,arrive | + + Scenario: Segregated Intersection, Cross Belonging to Correct Street + Given the node map + | | | i | l | | | + | | | | | | | + | d | | c | b | | a | + | e | | f | g | | h | + | | | | | | | + | | | j | k | | | + + And the ways + | nodes | highway | name | oneway | + | ab | primary | first | yes | + | bc | primary | first | yes | + | cd | primary | first | yes | + | ef | primary | first | yes | + | fg | primary | first | yes | + | gh | primary | first | yes | + | ic | primary | second | yes | + | bl | primary | second | yes | + | kg | primary | second | yes | + | fj | primary | second | yes | + | cf | primary | second | yes | + | gb | primary | second | yes | + + When I route I should get + | waypoints | route | turns | + | a,l | first,second,second | depart,turn right,arrive | + | a,d | first,first | depart,arrive | + | a,j | first,second,second | depart,turn left,arrive | + | a,h | first,first,first | depart,continue uturn,arrive | + | e,j | first,second,second | depart,turn right,arrive | + | e,h | first,first | depart,arrive | + | e,l | first,second,second | depart,turn left,arrive | + | e,d | first,first,first | depart,continue uturn,arrive | + | k,h | second,first,first | depart,turn right,arrive | + | k,l | second,second | depart,arrive | + | k,d | second,first,first | depart,turn left,arrive | + | k,j | second,second,second | depart,continue uturn,arrive | + | i,d | second,first,first | depart,turn right,arrive | + | i,j | second,second | depart,arrive | + | i,h | second,first,first | depart,turn left,arrive | + | i,l | second,second,second | depart,continue uturn,arrive | + + Scenario: Segregated Intersection, Cross Belonging to Mixed Streets + Given the node map + | | | i | l | | | + | | | | | | | + | d | | c | b | | a | + | e | | f | g | | h | + | | | | | | | + | | | j | k | | | + + And the ways + | nodes | highway | name | oneway | + | ab | primary | first | yes | + | bc | primary | second | yes | + | cd | primary | first | yes | + | ef | primary | first | yes | + | fg | primary | first | yes | + | gh | primary | first | yes | + | ic | primary | second | yes | + | bl | primary | second | yes | + | kg | primary | second | yes | + | fj | primary | second | yes | + | cf | primary | second | yes | + | gb | primary | first | yes | + + When I route I should get + | waypoints | route | turns | + | a,l | first,second,second | depart,turn right,arrive | + | a,d | first,first | depart,arrive | + | a,j | first,second,second | depart,turn left,arrive | + | a,h | first,first,first | depart,continue uturn,arrive | + | e,j | first,second,second | depart,turn right,arrive | + | e,h | first,first | depart,arrive | + | e,l | first,second,second | depart,turn left,arrive | + | e,d | first,first,first | depart,continue uturn,arrive | + | k,h | second,first,first | depart,turn right,arrive | + | k,l | second,second | depart,arrive | + | k,d | second,first,first | depart,turn left,arrive | + | k,j | second,second,second | depart,continue uturn,arrive | + | i,d | second,first,first | depart,turn right,arrive | + | i,j | second,second | depart,arrive | + | i,h | second,first,first | depart,turn left,arrive | + | i,l | second,second,second | depart,continue uturn,arrive | + + Scenario: Partly Segregated Intersection, Two Segregated Roads + Given the node map + | | g | | h | | + | | | | | | + | | | | | | + | c | | b | | a | + | d | | e | | f | + | | | | | | + | | | | | | + | | j | | i | | + + And the ways + | nodes | highway | name | oneway | + | ab | primary | first | yes | + | bc | primary | first | yes | + | de | primary | first | yes | + | ef | primary | first | yes | + | be | primary | first | no | + | gbh | primary | second | yes | + | iej | primary | second | yes | + + When I route I should get + | waypoints | route | turns | + | a,h | first,second,second | depart,turn right,arrive | + | a,c | first,first | depart,arrive | + | a,j | first,second,second | depart,turn left,arrive | + | a,f | first,first,first | depart,continue uturn,arrive | + | d,j | first,second,second | depart,turn right,arrive | + | d,f | first,first | depart,arrive | + | d,h | first,second,second | depart,turn left,arrive | + | d,c | first,first,first | depart,continue uturn,arrive | + | g,c | second,first,first | depart,turn right,arrive | + | g,j | second,second | depart,arrive | + | g,f | second,first,first | depart,turn left,arrive | + | g,h | second,second,second | depart,continue uturn,arrive | + | i,f | second,first,first | depart,turn right,arrive | + | i,h | second,second | depart,arrive | + | i,c | second,first,first | depart,turn left,arrive | + | i,j | second,second,second | depart,continue uturn,arrive | + + Scenario: Partly Segregated Intersection, Two Segregated Roads, Intersection belongs to Second + Given the node map + | | g | | h | | + | | | | | | + | | | | | | + | c | | b | | a | + | d | | e | | f | + | | | | | | + | | | | | | + | | j | | i | | + + And the ways + | nodes | highway | name | oneway | + | ab | primary | first | yes | + | bc | primary | first | yes | + | de | primary | first | yes | + | ef | primary | first | yes | + | be | primary | second | no | + | gbh | primary | second | yes | + | iej | primary | second | yes | + + When I route I should get + | waypoints | route | turns | + | a,h | first,second,second | depart,turn right,arrive | + | a,c | first,first | depart,arrive | + | a,j | first,second,second | depart,turn left,arrive | + | a,f | first,first,first | depart,continue uturn,arrive | + | d,j | first,second,second | depart,turn right,arrive | + | d,f | first,first | depart,arrive | + | d,h | first,second,second | depart,turn left,arrive | + | d,c | first,first,first | depart,continue uturn,arrive | + | g,c | second,first,first | depart,turn right,arrive | + | g,j | second,second | depart,arrive | + | g,f | second,first,first | depart,turn left,arrive | + | g,h | second,second,second | depart,continue uturn,arrive | + | i,f | second,first,first | depart,turn right,arrive | + | i,h | second,second | depart,arrive | + | i,c | second,first,first | depart,turn left,arrive | + | i,j | second,second,second | depart,continue uturn,arrive | + + Scenario: Segregated Intersection, Cross Belonging to Mixed Streets - Slight Angles + Given the node map + | | | i | l | | | + | | | | | | a | + | | | c | b | | h | + | d | | f | g | | | + | e | | | | | | + | | | j | k | | | + + And the ways + | nodes | highway | name | oneway | + | ab | primary | first | yes | + | bc | primary | second | yes | + | cd | primary | first | yes | + | ef | primary | first | yes | + | fg | primary | first | yes | + | gh | primary | first | yes | + | ic | primary | second | yes | + | bl | primary | second | yes | + | kg | primary | second | yes | + | fj | primary | second | yes | + | cf | primary | second | yes | + | gb | primary | first | yes | + + When I route I should get + | waypoints | route | turns | + | a,l | first,second,second | depart,turn right,arrive | + | a,d | first,first | depart,arrive | + | a,j | first,second,second | depart,turn left,arrive | + | a,h | first,first,first | depart,continue uturn,arrive | + | e,j | first,second,second | depart,turn right,arrive | + | e,h | first,first | depart,arrive | + | e,l | first,second,second | depart,turn left,arrive | + | e,d | first,first,first | depart,continue uturn,arrive | + | k,h | second,first,first | depart,turn right,arrive | + | k,l | second,second | depart,arrive | + | k,d | second,first,first | depart,turn left,arrive | + | k,j | second,second,second | depart,continue uturn,arrive | + | i,d | second,first,first | depart,turn right,arrive | + | i,j | second,second | depart,arrive | + | i,h | second,first,first | depart,turn left,arrive | + | i,l | second,second,second | depart,continue uturn,arrive | + + Scenario: Segregated Intersection, Cross Belonging to Mixed Streets - Slight Angles (2) + Given the node map + | | | i | l | | | + | | | | | | | + | | | c | b | | | + | d | | f | g | | a | + | e | | | | | h | + | | | j | k | | | + + And the ways + | nodes | highway | name | oneway | + | ab | primary | first | yes | + | bc | primary | second | yes | + | cd | primary | first | yes | + | ef | primary | first | yes | + | fg | primary | first | yes | + | gh | primary | first | yes | + | ic | primary | second | yes | + | bl | primary | second | yes | + | kg | primary | second | yes | + | fj | primary | second | yes | + | cf | primary | second | yes | + | gb | primary | first | yes | + + When I route I should get + | waypoints | route | turns | + | a,l | first,second,second | depart,turn right,arrive | + | a,d | first,first | depart,arrive | + | a,j | first,second,second | depart,turn left,arrive | + | a,h | first,first,first | depart,continue uturn,arrive | + | e,j | first,second,second | depart,turn right,arrive | + | e,h | first,first | depart,arrive | + | e,l | first,second,second | depart,turn left,arrive | + | e,d | first,first,first | depart,continue uturn,arrive | + | k,h | second,first,first | depart,turn right,arrive | + | k,l | second,second | depart,arrive | + | k,d | second,first,first | depart,turn left,arrive | + | k,j | second,second,second | depart,continue uturn,arrive | + | i,d | second,first,first | depart,turn right,arrive | + | i,j | second,second | depart,arrive | + | i,h | second,first,first | depart,turn left,arrive | + | i,l | second,second,second | depart,continue uturn,arrive | + + Scenario: Entering a segregated road + Given the node map + | | a | f | | | + | | | | | g | + | | b | e | | | + | | | | | | + | | | | | | + | c | d | | | | + + And the ways + | nodes | highway | name | oneway | + | abc | primary | first | yes | + | def | primary | first | yes | + | be | primary | first | no | + | ge | primary | second | no | + + When I route I should get + | waypoints | route | turns | + | d,c | first,first,first | depart,continue uturn,arrive | + | a,f | first,first,first | depart,continue uturn,arrive | + | a,g | first,second,second | depart,turn left,arrive | + | d,g | first,second,second | depart,turn right,arrive | + | g,f | second,first,first | depart,turn right,arrive | + | g,c | second,first,first | depart,end of road left,arrive | + + + Scenario: Do not collapse turning roads + Given the node map + | | | e | | | + | | | c | | d | + | a | | b | f | | + + And the ways + | nodes | highway | name | + | ab | primary | first | + | bc | primary | first | + | cd | primary | first | + | ce | primary | second | + | bf | primary | third | + + When I route I should get + | waypoints | route | turns | + | a,d | first,first,first,first | depart,continue left,continue right,arrive | + | a,e | first,second,second | depart,turn left,arrive | + | a,f | first,third,third | depart,new name straight,arrive | diff --git a/include/engine/api/route_api.hpp b/include/engine/api/route_api.hpp index 1907bee00..b8cbee215 100644 --- a/include/engine/api/route_api.hpp +++ b/include/engine/api/route_api.hpp @@ -132,6 +132,7 @@ class RouteAPI : public BaseAPI guidance::trimShortSegments(steps, leg_geometry); leg.steps = guidance::postProcess(std::move(steps)); + leg.steps = guidance::collapseTurns(std::move(leg.steps)); leg.steps = guidance::assignRelativeLocations(std::move(leg.steps), leg_geometry, phantoms.source_phantom, phantoms.target_phantom); diff --git a/include/engine/guidance/post_processing.hpp b/include/engine/guidance/post_processing.hpp index 32f6ee5ca..790d5af46 100644 --- a/include/engine/guidance/post_processing.hpp +++ b/include/engine/guidance/post_processing.hpp @@ -17,6 +17,13 @@ namespace guidance // passed as none-reference to modify in-place and move out again std::vector postProcess(std::vector steps); +// Multiple possible reasons can result in unnecessary/confusing instructions +// A prime example would be a segregated intersection. Turning around at this +// intersection would result in two instructions to turn left. +// Collapsing such turns into a single turn instruction, we give a clearer +// set of instructionst that is not cluttered by unnecessary turns/name changes. +std::vector collapseTurns(std::vector steps); + // trim initial/final segment of very short length. // This function uses in/out parameter passing to modify both steps and geometry in place. // We use this method since both steps and geometry are closely coupled logically but @@ -30,6 +37,9 @@ std::vector assignRelativeLocations(std::vector steps, const PhantomNode &source_node, const PhantomNode &target_node); +//remove steps invalidated by post-processing +std::vector removeNoTurnInstructions(std::vector steps); + // postProcess will break the connection between the leg geometry // for which a segment is supposed to represent exactly the coordinates // between routing maneuvers and the route steps itself. diff --git a/include/engine/guidance/toolkit.hpp b/include/engine/guidance/toolkit.hpp index e9cbcf5e3..f2fc3b4ac 100644 --- a/include/engine/guidance/toolkit.hpp +++ b/include/engine/guidance/toolkit.hpp @@ -4,6 +4,8 @@ #include "extractor/guidance/turn_instruction.hpp" #include "util/bearing.hpp" +#include + namespace osrm { namespace engine @@ -56,6 +58,12 @@ inline extractor::guidance::DirectionModifier angleToDirectionModifier(const dou return extractor::guidance::DirectionModifier::Left; } +inline double angularDeviation(const double angle, const double from) +{ + const double deviation = std::abs(angle - from); + return std::min(360 - deviation, deviation); +} + } // namespace guidance } // namespace engine } // namespace osrm diff --git a/src/engine/guidance/post_processing.cpp b/src/engine/guidance/post_processing.cpp index de1851599..7c931b73d 100644 --- a/src/engine/guidance/post_processing.cpp +++ b/src/engine/guidance/post_processing.cpp @@ -66,6 +66,13 @@ RouteStep forwardInto(RouteStep destination, const RouteStep &source) return destination; } +// invalidate a step and set its content to nothing +inline void invalidateStep(RouteStep &step) +{ + step = {}; + step.maneuver.instruction = TurnInstruction::NO_TURN(); +}; + void fixFinalRoundabout(std::vector &steps) { for (std::size_t propagation_index = steps.size() - 1; propagation_index > 0; @@ -196,8 +203,157 @@ void closeOffRoundabout(const bool on_roundabout, step.maneuver.instruction = TurnInstruction::NO_TURN(); } } + +// elongate a step by another. the data is added either at the front, or the back +RouteStep elongate(RouteStep step, const RouteStep &by_step) +{ + BOOST_ASSERT(step.mode == by_step.mode); + + step.duration += by_step.duration; + step.distance += by_step.distance; + + if (step.geometry_end == by_step.geometry_begin + 1) + { + step.geometry_end = by_step.geometry_end; + + // if we elongate in the back, we only need to copy the intersections to the beginning. + // the bearings remain the same, as the location of the turn doesn't change + step.maneuver.intersections.insert(step.maneuver.intersections.end(), + by_step.maneuver.intersections.begin(), + by_step.maneuver.intersections.end()); + } + else + { + BOOST_ASSERT(step.maneuver.waypoint_type == WaypointType::None && + by_step.maneuver.waypoint_type == WaypointType::None); + BOOST_ASSERT(by_step.geometry_end == step.geometry_begin + 1); + step.geometry_begin = by_step.geometry_begin; + + // elongating in the front changes the location of the maneuver + step.maneuver.location = by_step.maneuver.location; + step.maneuver.bearing_before = by_step.maneuver.bearing_before; + step.maneuver.bearing_after = by_step.maneuver.bearing_after; + step.maneuver.instruction = by_step.maneuver.instruction; + + step.maneuver.intersections.insert(step.maneuver.intersections.begin(), + by_step.maneuver.intersections.begin(), + by_step.maneuver.intersections.end()); + } + return step; +} + +// A check whether two instructions can be treated as one. This is only the case for very short +// maneuvers that can, in some form, be seen as one. The additional in_step is to find out about +// a possible u-turn. +inline bool collapsable(const RouteStep &step) +{ + const constexpr double MAX_COLLAPSE_DISTANCE = 25; + return step.distance < MAX_COLLAPSE_DISTANCE; +}; + +void collapseTurnAt(std::vector &steps, + const std::size_t two_back_index, + const std::size_t one_back_index, + const std::size_t step_index) +{ + const auto ¤t_step = steps[step_index]; + + const auto &one_back_step = steps[one_back_index]; + + const auto bearingsAreReversed = [](const double bearing_in, const double bearing_out) { + // Nearly perfectly reversed angles have a difference close to 180 degrees (straight) + return angularDeviation(bearing_in, bearing_out) > 170; + }; + + // Very Short New Name + if (TurnType::NewName == one_back_step.maneuver.instruction.type) + { + if (one_back_step.mode == steps[two_back_index].mode) + { + steps[two_back_index] = elongate(std::move(steps[two_back_index]), one_back_step); + // If the previous instruction asked to continue, the name change will have to + // be changed into a turn + invalidateStep(steps[one_back_index]); + + if (TurnType::Continue == current_step.maneuver.instruction.type) + steps[step_index].maneuver.instruction.type = TurnType::Turn; + } + } + // very short segment after turn + else if (TurnType::NewName == current_step.maneuver.instruction.type) + { + if (one_back_step.mode == current_step.mode) + { + steps[step_index] = elongate(std::move(steps[step_index]), steps[one_back_index]); + invalidateStep(steps[one_back_index]); + + if (TurnType::Continue == current_step.maneuver.instruction.type) + { + steps[step_index].maneuver.instruction.type = TurnType::Turn; + } + } + } + // Potential U-Turn + else if (bearingsAreReversed(one_back_step.maneuver.bearing_before, + current_step.maneuver.bearing_after)) + + { + // the simple case is a u-turn that changes directly into the in-name again + const bool direct_u_turn = steps[two_back_index].name == current_step.name; + + // however, we might also deal with a dual-collapse scenario in which we have to + // additionall collapse a name-change as well + const bool continues_with_name_change = + (step_index + 1 < steps.size()) && + (TurnType::NewName == steps[step_index + 1].maneuver.instruction.type); + const bool u_turn_with_name_change = + collapsable(current_step) && continues_with_name_change && + steps[step_index + 1].name == steps[two_back_index].name; + + if (direct_u_turn || u_turn_with_name_change) + { + steps[one_back_index] = elongate(std::move(steps[one_back_index]), steps[step_index]); + invalidateStep(steps[step_index]); + if (u_turn_with_name_change) + { + steps[one_back_index] = + elongate(std::move(steps[one_back_index]), steps[step_index + 1]); + invalidateStep(steps[step_index + 1]); // will be skipped due to the + // continue statement at the + // beginning of this function + } + + steps[one_back_index].name = steps[two_back_index].name; + steps[one_back_index].maneuver.instruction.type = TurnType::Continue; + steps[one_back_index].maneuver.instruction.direction_modifier = + DirectionModifier::UTurn; + } + } +} + } // namespace detail +// Post processing can invalidate some instructions. For example StayOnRoundabout +// is turned into exit counts. These instructions are removed by the following function + +std::vector removeNoTurnInstructions(std::vector steps) +{ + // finally clean up the post-processed instructions. + // Remove all invalid instructions from the set of instructions. + // An instruction is invalid, if its NO_TURN and has WaypointType::None. + // Two valid NO_TURNs exist in each leg in the form of Depart/Arrive + + // keep valid instructions + const auto not_is_valid = [](const RouteStep &step) { + return step.maneuver.instruction == TurnInstruction::NO_TURN() && + step.maneuver.waypoint_type == WaypointType::None; + }; + + boost::remove_erase_if(steps, not_is_valid); + + return steps; +} + // Every Step Maneuver consists of the information until the turn. // This list contains a set of instructions, called silent, which should // not be part of the final output. @@ -287,20 +443,65 @@ std::vector postProcess(std::vector steps) detail::fixFinalRoundabout(steps); } - // finally clean up the post-processed instructions. - // Remove all invalid instructions from the set of instructions. - // An instruction is invalid, if its NO_TURN and has WaypointType::None. - // Two valid NO_TURNs exist in each leg in the form of Depart/Arrive + return removeNoTurnInstructions(std::move(steps)); +} - // keep valid instructions - const auto not_is_valid = [](const RouteStep &step) { - return step.maneuver.instruction == TurnInstruction::NO_TURN() && - step.maneuver.waypoint_type == WaypointType::None; +// Post Processing to collapse unnecessary sets of combined instructions into a single one +std::vector collapseTurns(std::vector steps) +{ + // Get the previous non-invalid instruction + const auto getPreviousIndex = [&steps](std::size_t index) { + BOOST_ASSERT(index > 0); + --index; + while (index > 0 && steps[index].maneuver.instruction == TurnInstruction::NO_TURN()) + --index; + + return index; }; - boost::remove_erase_if(steps, not_is_valid); + // first and last instructions are waypoints that cannot be collapsed + for (std::size_t step_index = 2; step_index < steps.size(); ++step_index) + { + const auto ¤t_step = steps[step_index]; + const auto one_back_index = getPreviousIndex(step_index); - return steps; + // cannot collapse the depart instruction + if (one_back_index == 0 || current_step.maneuver.instruction == TurnInstruction::NO_TURN()) + continue; + + const auto &one_back_step = steps[one_back_index]; + const auto two_back_index = getPreviousIndex(one_back_index); + + // If we look at two consecutive name changes, we can check for a name oszillation. + // A name oszillation changes from name A shortly to name B and back to A. + // In these cases, the name change will be suppressed. + if (TurnType::NewName == current_step.maneuver.instruction.type && + TurnType::NewName == one_back_step.maneuver.instruction.type) + { + // valid due to step_index starting at 2 + const auto &coming_from_name = steps[step_index - 2].name; + if (current_step.name == coming_from_name) + { + if (current_step.mode == one_back_step.mode && + one_back_step.mode == steps[two_back_index].mode) + { + steps[two_back_index] = detail::elongate( + detail::elongate(std::move(steps[two_back_index]), steps[one_back_index]), + steps[step_index]); + detail::invalidateStep(steps[one_back_index]); + detail::invalidateStep(steps[step_index]); + } + // TODO discuss: we could think about changing the new-name to a pure notification + // about mode changes + } + } + else if (detail::collapsable(one_back_step)) + { + // check for one of the multiple collapse scenarios and, if possible, collapse the turn + detail::collapseTurnAt(steps, two_back_index, one_back_index, step_index); + } + } + return removeNoTurnInstructions(std::move(steps)); } void trimShortSegments(std::vector &steps, LegGeometry &geometry)