From 875f4822035253a20d621a61fec9b31872868689 Mon Sep 17 00:00:00 2001 From: "Daniel J. Hofmann" Date: Wed, 26 Oct 2016 14:32:29 -0700 Subject: [PATCH] Refactors and improves the Sliproad Handler, resolves #3109 --- CHANGELOG.md | 1 + features/car/names.feature | 26 +- features/guidance/advanced-lanes.feature | 39 +- features/guidance/collapse-detail.feature | 35 + features/guidance/collapse.feature | 16 +- .../guidance/dedicated-turn-roads.feature | 417 ++++++++- include/extractor/guidance/intersection.hpp | 109 ++- .../guidance/intersection_generator.hpp | 6 + .../guidance/intersection_handler.hpp | 462 +++++++++- .../guidance/node_based_graph_walker.hpp | 59 +- .../extractor/guidance/sliproad_handler.hpp | 35 +- src/extractor/guidance/intersection.cpp | 36 +- .../guidance/intersection_handler.cpp | 448 +-------- src/extractor/guidance/motorway_handler.cpp | 4 +- .../guidance/node_based_graph_walker.cpp | 1 - src/extractor/guidance/sliproad_handler.cpp | 862 +++++++++++++----- 16 files changed, 1797 insertions(+), 759 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f0af0a8b7..5f6be4a47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ - Improved turn angle calculation, detecting offsets due to lanes / minor variations due to inaccuracies - Corrected the bearings returned for intermediate steps - requires reprocessing - Improved turn locations for collapsed turns + - Sliproad classification refinements: the situations we detect as Sliproads now resemble more closely the reality - Trip Plugin - changed internal behaviour to prefer the smallest lexicographic result over the largest one - Bugfixes diff --git a/features/car/names.feature b/features/car/names.feature index 7d3e6858b..f0f367e63 100644 --- a/features/car/names.feature +++ b/features/car/names.feature @@ -78,24 +78,24 @@ Feature: Car - Street names in instructions Scenario: Inner city expressway with on road Given the node map """ - a b c g - f - - - + a b . . . c g + `f . + ` + . + . d - - - + . + . + . e """ And the ways - | nodes | highway | name | name:pronunciation | - | abc | primary | road | roooaad | - | cg | primary | road | roooaad | - | bfd | trunk_link | | | - | cde | trunk | trunk | truank | + | nodes | highway | name | name:pronunciation | oneway | + | abc | primary | road | roooaad | | + | cg | primary | road | roooaad | | + | bfd | trunk_link | sliproad | | yes | + | cde | trunk | trunk | truank | yes | And the relations | type | way:from | way:to | node:via | restriction | diff --git a/features/guidance/advanced-lanes.feature b/features/guidance/advanced-lanes.feature index 201ec04e7..a61811224 100644 --- a/features/guidance/advanced-lanes.feature +++ b/features/guidance/advanced-lanes.feature @@ -5,13 +5,17 @@ Feature: Turn Lane Guidance Given the profile "car" Given a grid size of 3 meters + @sliproads Scenario: Separate Turn Lanes Given the node map """ e - a b c g - d + . + a ... b ..... c . g + ` . + `... d + . f """ @@ -41,8 +45,10 @@ Feature: Turn Lane Guidance Given the node map """ e - a b c g - d + a . . b . . . c g + ` . + ` . + ` d f """ @@ -67,21 +73,22 @@ Feature: Turn Lane Guidance | a,g | in,straight,straight | depart,new name straight,arrive | ,left:false straight:true right:false, | | a,f | in,cross,cross | depart,turn right,arrive | ,left:false straight:false right:true, | + @sliproads Scenario: Separate Turn Lanes Next to other turns Given the node map """ - e - a - - b.-.- - c-g - | ' 'd - | f - | - | - | - | - | - | - i - - h - - - j + . e + a . . b . . . c g + . ` . + . ` . + . d + . f + . + . + . + . + i . . h . . . j """ And the ways @@ -109,6 +116,7 @@ Feature: Turn Lane Guidance | a,j | in,turn,other,other | depart,turn right,turn left,arrive | ,,left:true right:false, | | a,i | in,turn,other,other | depart,turn right,turn right,arrive | ,,left:false right:true, | + @todo @2654 @none #https://github.com/Project-OSRM/osrm-backend/issues/2645 #http://www.openstreetmap.org/export#map=19/52.56054/13.32152 @@ -231,6 +239,7 @@ Feature: Turn Lane Guidance | a,j | ghough,market,market | depart,turn left,arrive | ,none:true straight:false straight:false straight:false, | | a,f | ghough,ghough,ghough | depart,continue slight left,arrive | ,none:true straight:true straight:false straight:false, | + Scenario: Check sliproad handler loop's exit condition, Issue #2896 # http://www.openstreetmap.org/way/198481519 Given the node locations diff --git a/features/guidance/collapse-detail.feature b/features/guidance/collapse-detail.feature index 49417c7ea..e623943ab 100644 --- a/features/guidance/collapse-detail.feature +++ b/features/guidance/collapse-detail.feature @@ -55,3 +55,38 @@ Feature: Collapse | waypoints | route | turns | | a,g | road,road,road | depart,continue uturn,arrive | | d,c | road,road,road | depart,continue uturn,arrive | + + Scenario: Forking before a turn (forky) + Given the node map + """ + g + . + c + a . . b .' + `d. + f e + """ + # note: check clooapse.feature for a similar test case where we do not + # classify the situation as Sliproad and therefore keep the fork inst. + + And the ways + | nodes | name | oneway | highway | + | ab | road | yes | primary | + | bd | road | yes | primary | + | bc | road | yes | primary | + | de | road | yes | primary | + | fd | cross | no | secondary | + | dc | cross | no | secondary | + | cg | cross | no | secondary | + + And the relations + | type | way:from | way:to | node:via | restriction | + | restriction | bd | dc | d | no_left_turn | + | restriction | bc | dc | c | no_right_turn | + + When I route I should get + | waypoints | route | turns | + | a,g | road,cross,cross | depart,turn left,arrive | + | a,e | road,road,road | depart,continue right,arrive | + # We should discuss whether the next item should be collapsed to depart,turn right,arrive. + | a,f | road,road,cross,cross | depart,continue slight right,turn right,arrive | diff --git a/features/guidance/collapse.feature b/features/guidance/collapse.feature index e3ff32917..4cf4c38da 100644 --- a/features/guidance/collapse.feature +++ b/features/guidance/collapse.feature @@ -701,12 +701,15 @@ Feature: Collapse Given the node map """ g - + . c - a b - d + a . . b .' + ` d. f e """ + # as it is right now we don't classify this as a sliproad, + # check collapse-detail.feature for a similar test case + # which removes the fork here due to it being a Sliproad. And the ways | nodes | name | oneway | highway | @@ -725,10 +728,9 @@ Feature: Collapse When I route I should get | waypoints | route | turns | - | a,g | road,cross,cross | depart,turn left,arrive | - | a,e | road,road,road | depart,continue straight,arrive | - # We should discuss whether the next item should be collapsed to depart,turn right,arrive. - | a,f | road,road,cross,cross | depart,continue straight,turn right,arrive | + | a,g | road,cross,cross | depart,fork left,arrive | + | a,e | road,road,road | depart,fork slight right,arrive | + | a,f | road,road,cross,cross | depart,fork slight right,turn right,arrive | Scenario: On-Off on Highway Given the node map diff --git a/features/guidance/dedicated-turn-roads.feature b/features/guidance/dedicated-turn-roads.feature index ce78a6187..8c0e27716 100644 --- a/features/guidance/dedicated-turn-roads.feature +++ b/features/guidance/dedicated-turn-roads.feature @@ -9,23 +9,23 @@ Feature: Slipways and Dedicated Turn Lanes Given the node map """ e - a b c d - h - - 1 - + a b . . c d + `h . + ` + 1 ` + . f - + . g """ And the ways - | nodes | highway | name | - | abc | trunk | first | - | cd | trunk | first | - | bhf | trunk_link | | - | cfg | primary | second | - | ec | primary | second | + | nodes | highway | name | oneway | + | abc | trunk | first | | + | cd | trunk | first | | + | bhf | trunk_link | | yes | + | cfg | primary | second | yes | + | ec | primary | second | | And the relations | type | way:from | way:to | node:via | restriction | @@ -51,12 +51,12 @@ Feature: Slipways and Dedicated Turn Lanes """ And the ways - | nodes | highway | name | maxspeed | - | abc | trunk | first | 70 | - | cd | trunk | first | 2 | - | bhf | trunk_link | | 2 | - | cfg | primary | second | 50 | - | ec | primary | second | 50 | + | nodes | highway | name | maxspeed | oneway | + | abc | trunk | first | 70 | | + | cd | trunk | first | 2 | | + | bhf | trunk_link | | 2 | yes | + | cfg | primary | second | 50 | yes | + | ec | primary | second | 50 | | And the relations | type | way:from | way:to | node:via | restriction | @@ -125,24 +125,24 @@ Feature: Slipways and Dedicated Turn Lanes Scenario: Inner city expressway with on road Given the node map """ - a b c g - f - - - + a b . . . c g + `f . + ` + . + . d - - - + . + . + . e """ And the ways - | nodes | highway | name | - | abc | primary | road | - | cg | primary | road | - | bfd | trunk_link | | - | cde | trunk | trunk | + | nodes | highway | name | oneway | + | abc | primary | road | | + | cg | primary | road | | + | bfd | trunk_link | | yes | + | cde | trunk | trunk | yes | And the relations | type | way:from | way:to | node:via | restriction | @@ -240,8 +240,8 @@ Feature: Slipways and Dedicated Turn Lanes | qe | secondary_link | Ettlinger Allee | | yes | When I route I should get - | waypoints | route | turns | ref | - | a,o | Schwarzwaldstrasse,Ettlinger Allee,Ettlinger Allee | depart,turn right,arrive | L561,L561, | + | waypoints | route | turns | ref | + | a,o | Schwarzwaldstrasse,Ettlinger Allee,Ettlinger Allee | depart,turn right,arrive | L561,L561, | Scenario: Traffic Lights everywhere #http://map.project-osrm.org/?z=18¢er=48.995336%2C8.383813&loc=48.995467%2C8.384548&loc=48.995115%2C8.382761&hl=en&alt=0 @@ -431,3 +431,354 @@ Feature: Slipways and Dedicated Turn Lanes When I route I should get | waypoints | route | turns | | a,i | road,road,road | depart,fork slight left,arrive | + + + # The following tests are current false positives / false negatives #3199 + + @sliproads + # http://www.openstreetmap.org/#map=19/52.59847/13.14815 + Scenario: Sliproad Detection + Given the node map + """ + a . . . + . . + b . . . . . . c . . . d + ` . . + e . . + ` . . + f . . + ` . . + g i + ` h . + """ + + And the ways + | nodes | highway | name | + | abefgh | residential | Nachtigallensteig | + | bcd | residential | Kiebitzsteig | + | cg | residential | Haenflingsteig | + | hid | residential | Waldkauzsteig | + + When I route I should get + | waypoints | route | turns | + | a,d | Nachtigallensteig,Kiebitzsteig,Kiebitzsteig | depart,turn left,arrive | + | a,h | Nachtigallensteig,Nachtigallensteig | depart,arrive | + + + @sliproads + Scenario: Not a obvious Sliproad + Given the node map + """ + d + . + s . a . . b . . c + ` . + ` e + .` + . ` + f g + """ + + And the ways + | nodes | highway | name | oneway | + | sabc | primary | sabc | | + | dbef | primary | dbef | yes | + | aeg | primary | aeg | yes | + + When I route I should get + | waypoints | route | turns | + | s,f | sabc,aeg,dbef,dbef | depart,turn right,turn right,arrive | + + @sliproads + Scenario: Through Street, not a Sliproad although obvious + Given the node map + """ + d + . + s . a . . b . . c + ` . + ` e + . ` + . ` + f g + """ + + And the ways + | nodes | highway | name | oneway | + | sabc | primary | sabc | | + | dbef | primary | dbef | yes | + | aeg | primary | aeg | yes | + + When I route I should get + | waypoints | route | turns | + | s,f | sabc,aeg,dbef,dbef | depart,turn right,turn right,arrive | + + @sliproads + Scenario: Sliproad target turn is restricted + Given the node map + """ + d + . + s . a . . . . b . . c + ` . + ` . + ` . + ` . + `. + e + .` + f ` + . ` g + """ + + And the ways + | nodes | highway | name | oneway | + | sa | primary | sabc | | + | abc | primary | sabc | | + | dbe | primary | dbef | yes | + | ef | primary | dbef | | + | ae | primary | aeg | yes | + | eg | primary | aeg | | + # the reason we have to split ways at e is that otherwise we can't handle restrictions via e + + And the relations + | type | way:from | way:to | node:via | restriction | + | restriction | ae | ef | e | no_right_turn | + + When I route I should get + | waypoints | route | turns | + | s,f | sabc,dbef,dbef | depart,turn right,arrive | + | s,g | sabc,aeg,aeg | depart,turn right,arrive | + + @sliproads + Scenario: Not a Sliproad, road not continuing straight + Given the node map + """ + d + . + s . a . . b . . c + ` . + ` e . . g + """ + + And the ways + | nodes | highway | name | oneway | + | sabc | primary | sabc | | + | dbe | primary | dbe | yes | + | aeg | primary | aeg | yes | + + When I route I should get + | waypoints | route | turns | + | s,c | sabc,sabc | depart,arrive | + | s,g | sabc,aeg,aeg | depart,turn right,arrive | + + @sliproads + Scenario: Intersection too far away with Traffic Light shortly after initial split + Given the node map + """ + d + . + s . a . . . . . . . . . . . . . t . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . b . . c + ` . . . . . . . . . . . . + ` . . . . . . . . . . . . + ` . . . . . . . . . . . . + ` . . . . . . . . . . . . + ` . . . . . . . . . . . . + ` . . . . . . . . . . . . + ` . . . + ` e + . + f + . + """ + + And the nodes + | node | highway | + | t | traffic_signals | + + And the ways + | nodes | highway | name | oneway | + | satbc | primary | sabc | | + | dbef | primary | dbef | yes | + | ae | primary | ae | yes | + + When I route I should get + | waypoints | route | turns | + | s,f | sabc,ae,dbef,dbef | depart,turn slight right,turn right,arrive | + + @sliproads + Scenario: Traffic Signal on Sliproad + Given the node map + """ + d + . + s . a . . . . . b . . c + ` . + ` . + ` . + t . + ` . + e + . + . + f + """ + + And the nodes + | node | highway | + | t | traffic_signals | + + And the ways + | nodes | highway | name | oneway | + | sabc | primary | sabc | | + | dbe | primary | dbe | yes | + | ef | primary | ef | | + | ate | primary | ate | yes | + + When I route I should get + | waypoints | route | turns | + | s,f | sabc,ef,ef | depart,turn right,arrive | + + @sliproads + Scenario: Sliproad tagged as link + Given the node map + """ + d + . + s . a . . . . . b . . c + ` . + ` . + ` . + ` . + ` . + e + . + . + f + """ + + And the ways + | nodes | highway | name | oneway | + | sabc | motorway | sabc | | + | dbef | motorway | dbef | yes | + | ae | motorway_link | ae | yes | + + When I route I should get + | waypoints | route | turns | + | s,f | sabc,dbef,dbef | depart,turn right,arrive | + + @sliproads + Scenario: Sliproad with same-ish names + Given the node map + """ + d + . + s . a . . b . . c + ` . + . e + .. + . + f + . + t + """ + + And the ways + | nodes | highway | name | ref | oneway | + | sabc | primary | main | | | + | dbe | primary | crossing | r0 | yes | + | eft | primary | crossing | r0;r1 | yes | + | af | primary | sliproad | | yes | + + When I route I should get + | waypoints | route | turns | + | s,t | main,crossing,crossing | depart,turn right,arrive | + + @sliproads + Scenario: Not a Sliproad, name mismatch + Given the node map + """ + d + . + s . a . . b . . c + ` . + . e + . . + .. + . + f + . + t + """ + + And the ways + | nodes | highway | name | oneway | + | sabc | primary | main | | + | dbe | primary | top | yes | + | ef | primary | bottom | yes | + | ft | primary | away | yes | + | af | primary | sliproad | yes | + + When I route I should get + | waypoints | route | turns | + | s,t | main,away,away | depart,turn right,arrive | + + @sliproads + Scenario: Not a Sliproad, low road priority + Given the node map + """ + d + . + s . a . . b . . c + ` . + . e + . . + .. + . + f + . + t + """ + + And the ways + # maxspeed otherwise service road will never be routed over and we won't see instructions + | nodes | highway | name | maxspeed | oneway | + | sabc | primary | main | 30 km/h | | + | dbe | primary | crossing | 30 km/h | yes | + | eft | primary | crossing | 30 km/h | yes | + | ft | primary | away | 30 km/h | yes | + | af | service | sliproad | 30 km/h | yes | + + When I route I should get + | waypoints | route | turns | + | s,t | main,away,away | depart,turn right,arrive | + + @sliproads + Scenario: Not a Sliproad, more than three roads at target intersection + Given the node map + """ + d + . + s . a . . b . . c + ` . + . e + . . + .. + . h + f . + . g + t + """ + + And the ways + | nodes | highway | name | oneway | + | sabc | primary | main | | + | dbe | primary | top | yes | + | eft | primary | bottom | yes | + | fh | primary | another | | + | fg | primary | another | | + | af | primary | sliproad | yes | + + When I route I should get + | waypoints | route | turns | + | s,g | main,sliproad,another,another | depart,turn right,turn left,arrive | diff --git a/include/extractor/guidance/intersection.hpp b/include/extractor/guidance/intersection.hpp index 02e1a5181..a58ef16fa 100644 --- a/include/extractor/guidance/intersection.hpp +++ b/include/extractor/guidance/intersection.hpp @@ -1,6 +1,9 @@ #ifndef OSRM_EXTRACTOR_GUIDANCE_INTERSECTION_HPP_ #define OSRM_EXTRACTOR_GUIDANCE_INTERSECTION_HPP_ +#include +#include +#include #include #include #include @@ -10,6 +13,11 @@ #include "util/node_based_graph.hpp" #include "util/typedefs.hpp" // EdgeID +#include +#include + +#include + namespace osrm { namespace extractor @@ -60,7 +68,7 @@ struct IntersectionViewData : IntersectionShapeData // A Connected Road is the internal representation of a potential turn. Internally, we require // full list of all connected roads to determine the outcome. // The reasoning behind is that even invalid turns can influence the perceived angles, or even -// instructions themselves. An pososible example can be described like this: +// instructions themselves. An possible example can be described like this: // // aaa(2)aa // a - bbbbb @@ -68,8 +76,7 @@ struct IntersectionViewData : IntersectionShapeData // // will not be perceived as a turn from (1) -> b, and as a U-turn from (1) -> (2). // In addition, they can influence whether a turn is obvious or not. b->(2) would also be no -// turn-operation, -// but rather a name change. +// turn-operation, but rather a name change. // // If this were a normal intersection with // @@ -103,6 +110,8 @@ struct ConnectedRoad final : IntersectionViewData // small helper function to print the content of a connected road std::string toString(const ConnectedRoad &road); +// Intersections are sorted roads: [0] being the UTurn road, then from sharp right to sharp left. + using IntersectionShape = std::vector; // Common operations shared among IntersectionView and Intersections. @@ -117,6 +126,84 @@ template struct EnableIntersectionOps return std::min_element(self()->begin(), self()->end(), comp); } + // Check validity of the intersection object. We assume a few basic properties every set of + // connected roads should follow throughout guidance pre-processing. This utility function + // allows checking intersections for validity + auto valid() const + { + if (self()->empty()) + return false; + + auto comp = [](const auto &lhs, const auto &rhs) { return lhs.CompareByAngle(rhs); }; + + const auto ordered = std::is_sorted(self()->begin(), self()->end(), comp); + + if (!ordered) + return false; + + const auto uturn = self()->operator[](0).angle < std::numeric_limits::epsilon(); + + if (!uturn) + return false; + + return true; + } + + // Given all possible turns which is the highest connected number of lanes per turn. + // This value is used for example during generation of intersections. + auto getHighestConnectedLaneCount(const util::NodeBasedDynamicGraph &graph) const + { + const std::function to_lane_count = + [&](const ConnectedRoad &road) { + return graph.GetEdgeData(road.eid).road_classification.GetNumberOfLanes(); + }; + + std::uint8_t max_lanes = 0; + const auto extract_maximal_value = [&max_lanes](std::uint8_t value) { + max_lanes = std::max(max_lanes, value); + return false; + }; + + const auto view = *self() | boost::adaptors::transformed(to_lane_count); + boost::range::find_if(view, extract_maximal_value); + return max_lanes; + } + + // Returns the UTurn road we took to arrive at this intersection. + const auto &getUTurnRoad() const { return self()->operator[](0); } + + // Returns the right-most road at this intersection. + const auto &getRightmostRoad() const + { + return self()->size() > 1 ? self()->operator[](1) : self()->getUTurnRoad(); + } + + // Returns the left-most road at this intersection. + const auto &getLeftmostRoad() const + { + return self()->size() > 1 ? self()->back() : self()->getUTurnRoad(); + } + + // Can this be skipped over? + auto isTrafficSignalOrBarrier() const { return self()->size() == 2; } + + // Checks if there is at least one road available (except UTurn road) on which to continue. + auto isDeadEnd() const + { + auto pred = [](const auto &road) { return road.entry_allowed; }; + return !std::any_of(self()->begin() + 1, self()->end(), pred); + } + + // Returns the number of roads we can enter at this intersection, respectively. + auto countEnterable() const + { + auto pred = [](const auto &road) { return road.entry_allowed; }; + return std::count_if(self()->begin(), self()->end(), pred); + } + + // Returns the number of roads we can not enter at this intersection, respectively. + auto countNonEnterable() const { return self()->size() - self()->countEnterable(); } + private: auto self() { return static_cast(this); } auto self() const { return static_cast(this); } @@ -126,28 +213,12 @@ struct IntersectionView final : std::vector, // EnableIntersectionOps // { using Base = std::vector; - - bool valid() const - { - return std::is_sorted(begin(), end(), std::mem_fn(&IntersectionViewData::CompareByAngle)); - }; }; struct Intersection final : std::vector, // EnableIntersectionOps // { using Base = std::vector; - - /* - * Check validity of the intersection object. We assume a few basic properties every set of - * connected roads should follow throughout guidance pre-processing. This utility function - * allows checking intersections for validity - */ - bool valid() const; - - // given all possible turns, which is the highest connected number of lanes per turn. This value - // is used, for example, during generation of intersections. - std::uint8_t getHighestConnectedLaneCount(const util::NodeBasedDynamicGraph &) const; }; } // namespace guidance diff --git a/include/extractor/guidance/intersection_generator.hpp b/include/extractor/guidance/intersection_generator.hpp index ea9e37177..bf7388243 100644 --- a/include/extractor/guidance/intersection_generator.hpp +++ b/include/extractor/guidance/intersection_generator.hpp @@ -35,6 +35,12 @@ class IntersectionGenerator const std::vector &node_info_list, const CompressedEdgeContainer &compressed_edge_container); + // For a source node `a` and a via edge `ab` creates an intersection at target `b`. + // + // a . . . b . . + // . + // . + // IntersectionView operator()(const NodeID nid, const EdgeID via_eid) const; /* diff --git a/include/extractor/guidance/intersection_handler.hpp b/include/extractor/guidance/intersection_handler.hpp index 19b79ab65..d68514db6 100644 --- a/include/extractor/guidance/intersection_handler.hpp +++ b/include/extractor/guidance/intersection_handler.hpp @@ -3,15 +3,22 @@ #include "extractor/guidance/intersection.hpp" #include "extractor/guidance/intersection_generator.hpp" +#include "extractor/guidance/node_based_graph_walker.hpp" #include "extractor/query_node.hpp" #include "extractor/suffix_table.hpp" +#include "util/coordinate_calculation.hpp" +#include "util/guidance/name_announcements.hpp" #include "util/name_table.hpp" #include "util/node_based_graph.hpp" +#include #include +#include #include +#include + namespace osrm { namespace extractor @@ -48,9 +55,7 @@ class IntersectionHandler const util::NameTable &name_table; const SuffixTable &street_name_suffix_table; const IntersectionGenerator &intersection_generator; - - // counts the number on allowed entry roads - std::size_t countValid(const Intersection &intersection) const; + const NodeBasedGraphWalker graph_walker; // for skipping traffic signal, distances etc. // Decide on a basic turn types TurnType::Enum findBasicTurnType(const EdgeID via_edge, const ConnectedRoad &candidate) const; @@ -59,7 +64,8 @@ class IntersectionHandler // determining whether there is a road that can be seen as obvious turn in the presence of many // other possible turns. The function will consider road categories and other inputs like the // turn angles. - std::size_t findObviousTurn(const EdgeID via_edge, const Intersection &intersection) const; + template // works with Intersection and IntersectionView + std::size_t findObviousTurn(const EdgeID via_edge, const IntersectionType &intersection) const; // Obvious turns can still take multiple forms. This function looks at the turn onto a road // candidate when coming from a via_edge and determines the best instruction to emit. @@ -83,9 +89,457 @@ class IntersectionHandler const std::size_t begin, const std::size_t end) const; + // Checks the intersection for a through street connected to `intersection[index]` bool isThroughStreet(const std::size_t index, const Intersection &intersection) const; + + // See `getNextIntersection` + struct IntersectionViewAndNode final + { + IntersectionView intersection; // < actual intersection + NodeID node; // < node at this intersection + }; + + // Skips over artificial intersections i.e. traffic lights, barriers etc. + // Returns the next non-artificial intersection and its node in the node based + // graph if an intersection could be found or none otherwise. + // + // a ... tl ... b .. c + // . + // . + // d + // + // ^ at + // ^ via + // + // For this scenario returns intersection at `b` and `b`. + boost::optional + getNextIntersection(const NodeID at, const EdgeID via) const; }; +// Impl. + +template // works with Intersection and IntersectionView +std::size_t IntersectionHandler::findObviousTurn(const EdgeID via_edge, + const IntersectionType &intersection) const +{ + using EdgeData = osrm::util::NodeBasedDynamicGraph::EdgeData; + using osrm::util::angularDeviation; + + // no obvious road + if (intersection.size() == 1) + return 0; + + // a single non u-turn is obvious + if (intersection.size() == 2) + return 1; + + const EdgeData &in_way_data = node_based_graph.GetEdgeData(via_edge); + + // the strategy for picking the most obvious turn involves deciding between + // an overall best candidate and a best candidate that shares the same name + // as the in road, i.e. a continue road + std::size_t best_option = 0; + double best_option_deviation = 180; + std::size_t best_continue = 0; + double best_continue_deviation = 180; + + /* helper functions */ + const auto IsContinueRoad = [&](const EdgeData &way_data) { + return !util::guidance::requiresNameAnnounced( + in_way_data.name_id, way_data.name_id, name_table, street_name_suffix_table); + }; + auto sameOrHigherPriority = [&in_way_data](const auto &way_data) { + return way_data.road_classification.GetPriority() <= + in_way_data.road_classification.GetPriority(); + }; + auto IsLowPriority = [](const auto &way_data) { + return way_data.road_classification.IsLowPriorityRoadClass(); + }; + // These two Compare functions are used for sifting out best option and continue + // candidates by evaluating all the ways in an intersection by what they share + // with the in way. Ideal candidates are of similar road class with the in way + // and are require relatively straight turns. + const auto RoadCompare = [&](const auto &lhs, const auto &rhs) { + const EdgeData &lhs_data = node_based_graph.GetEdgeData(lhs.eid); + const EdgeData &rhs_data = node_based_graph.GetEdgeData(rhs.eid); + const auto lhs_deviation = angularDeviation(lhs.angle, STRAIGHT_ANGLE); + const auto rhs_deviation = angularDeviation(rhs.angle, STRAIGHT_ANGLE); + + const bool rhs_same_classification = + rhs_data.road_classification == in_way_data.road_classification; + const bool lhs_same_classification = + lhs_data.road_classification == in_way_data.road_classification; + const bool rhs_same_or_higher_priority = sameOrHigherPriority(rhs_data); + const bool rhs_low_priority = IsLowPriority(rhs_data); + const bool lhs_same_or_higher_priority = sameOrHigherPriority(lhs_data); + const bool lhs_low_priority = IsLowPriority(lhs_data); + auto left_tie = std::tie(lhs.entry_allowed, + lhs_same_or_higher_priority, + rhs_low_priority, + rhs_deviation, + lhs_same_classification); + auto right_tie = std::tie(rhs.entry_allowed, + rhs_same_or_higher_priority, + lhs_low_priority, + lhs_deviation, + rhs_same_classification); + return left_tie > right_tie; + }; + const auto RoadCompareSameName = [&](const auto &lhs, const auto &rhs) { + const EdgeData &lhs_data = node_based_graph.GetEdgeData(lhs.eid); + const EdgeData &rhs_data = node_based_graph.GetEdgeData(rhs.eid); + const auto lhs_continues = IsContinueRoad(lhs_data); + const auto rhs_continues = IsContinueRoad(rhs_data); + const auto left_tie = std::tie(lhs.entry_allowed, lhs_continues); + const auto right_tie = std::tie(rhs.entry_allowed, rhs_continues); + return left_tie > right_tie || (left_tie == right_tie && RoadCompare(lhs, rhs)); + }; + + auto best_option_it = std::min_element(begin(intersection), end(intersection), RoadCompare); + + // min element should only return end() when vector is empty + BOOST_ASSERT(best_option_it != end(intersection)); + + best_option = std::distance(begin(intersection), best_option_it); + best_option_deviation = angularDeviation(intersection[best_option].angle, STRAIGHT_ANGLE); + const auto &best_option_data = node_based_graph.GetEdgeData(intersection[best_option].eid); + + // Unless the in way is also low priority, it is generally undesirable to + // indicate that a low priority road is obvious + if (IsLowPriority(best_option_data) && + best_option_data.road_classification != in_way_data.road_classification) + { + best_option = 0; + best_option_deviation = 180; + } + + // double check if the way with the lowest deviation from straight is still be better choice + const auto straightest = intersection.findClosestTurn(STRAIGHT_ANGLE); + if (straightest != best_option_it) + { + const EdgeData &straightest_data = node_based_graph.GetEdgeData(straightest->eid); + double straightest_data_deviation = angularDeviation(straightest->angle, STRAIGHT_ANGLE); + const auto deviation_diff = + std::abs(best_option_deviation - straightest_data_deviation) > FUZZY_ANGLE_DIFFERENCE; + const auto not_ramp_class = !straightest_data.road_classification.IsRampClass(); + const auto not_link_class = !straightest_data.road_classification.IsLinkClass(); + if (deviation_diff && !IsLowPriority(straightest_data) && not_ramp_class && + not_link_class && !IsContinueRoad(best_option_data)) + { + best_option = std::distance(begin(intersection), straightest); + best_option_deviation = + angularDeviation(intersection[best_option].angle, STRAIGHT_ANGLE); + } + } + + // No non-low priority roads? Declare no obvious turn + if (best_option == 0) + return 0; + + auto best_continue_it = + std::min_element(begin(intersection), end(intersection), RoadCompareSameName); + const auto best_continue_data = node_based_graph.GetEdgeData(best_continue_it->eid); + if (IsContinueRoad(best_continue_data) || + (in_way_data.name_id == EMPTY_NAMEID && best_continue_data.name_id == EMPTY_NAMEID)) + { + best_continue = std::distance(begin(intersection), best_continue_it); + best_continue_deviation = + angularDeviation(intersection[best_continue].angle, STRAIGHT_ANGLE); + } + + // if the best angle is going straight but the road is turning, declare no obvious turn + if (0 != best_continue && best_option != best_continue && + best_option_deviation < MAXIMAL_ALLOWED_NO_TURN_DEVIATION && + node_based_graph.GetEdgeData(intersection[best_continue].eid).road_classification == + best_option_data.road_classification) + { + return 0; + } + + // get a count of number of ways from that intersection that qualify to have + // continue instruction because they share a name with the approaching way + const std::int64_t continue_count = + count_if(++begin(intersection), end(intersection), [&](const auto &way) { + return IsContinueRoad(node_based_graph.GetEdgeData(way.eid)); + }); + const std::int64_t continue_count_valid = + count_if(++begin(intersection), end(intersection), [&](const auto &way) { + return IsContinueRoad(node_based_graph.GetEdgeData(way.eid)) && way.entry_allowed; + }); + + // checks if continue candidates are sharp turns + const bool all_continues_are_narrow = [&]() { + return std::count_if(begin(intersection), end(intersection), [&](const auto &road) { + const EdgeData &road_data = node_based_graph.GetEdgeData(road.eid); + const double &road_angle = angularDeviation(road.angle, STRAIGHT_ANGLE); + return IsContinueRoad(road_data) && (road_angle < NARROW_TURN_ANGLE); + }) == continue_count; + }(); + + // return true if the best_option candidate is more promising than the best_continue candidate + // otherwise return false, the best_continue candidate is more promising + const auto best_over_best_continue = [&]() { + // no continue road exists + if (best_continue == 0) + return true; + + // we have multiple continues and not all are narrow. This suggests that + // the continue candidates are ambiguous + if (!all_continues_are_narrow && (continue_count >= 2 && intersection.size() >= 4)) + return true; + + // if the best continue is not narrow and we also have at least 2 possible choices, the + // intersection size does not matter anymore + if (continue_count_valid >= 2 && best_continue_deviation >= 2 * NARROW_TURN_ANGLE) + return true; + + // continue data now most certainly exists + const auto &continue_data = node_based_graph.GetEdgeData(intersection[best_continue].eid); + + // best_continue is obvious by road class + if (obviousByRoadClass(in_way_data.road_classification, + continue_data.road_classification, + best_option_data.road_classification)) + return false; + + // best_option is obvious by road class + if (obviousByRoadClass(in_way_data.road_classification, + best_option_data.road_classification, + continue_data.road_classification)) + return true; + + // the best_option deviation is very straight and not a ramp + if (best_option_deviation < best_continue_deviation && + best_option_deviation < FUZZY_ANGLE_DIFFERENCE && + !best_option_data.road_classification.IsRampClass()) + return true; + + // the continue road is of a lower priority, while the road continues on the same priority + // with a better angle + if (best_option_deviation < best_continue_deviation && + in_way_data.road_classification == best_option_data.road_classification && + continue_data.road_classification.GetPriority() > + best_option_data.road_classification.GetPriority()) + return true; + + return false; + }(); + + if (best_over_best_continue) + { + // Find left/right deviation + // skipping over service roads + const std::size_t left_index = [&]() { + const auto index_candidate = (best_option + 1) % intersection.size(); + if (index_candidate == 0) + return index_candidate; + const auto &candidate_data = + node_based_graph.GetEdgeData(intersection[index_candidate].eid); + if (obviousByRoadClass(in_way_data.road_classification, + best_option_data.road_classification, + candidate_data.road_classification)) + return (index_candidate + 1) % intersection.size(); + else + return index_candidate; + + }(); + const auto right_index = [&]() { + BOOST_ASSERT(best_option > 0); + const auto index_candidate = best_option - 1; + if (index_candidate == 0) + return index_candidate; + const auto candidate_data = + node_based_graph.GetEdgeData(intersection[index_candidate].eid); + if (obviousByRoadClass(in_way_data.road_classification, + best_option_data.road_classification, + candidate_data.road_classification)) + return index_candidate - 1; + else + return index_candidate; + }(); + + const double left_deviation = + angularDeviation(intersection[left_index].angle, STRAIGHT_ANGLE); + const double right_deviation = + angularDeviation(intersection[right_index].angle, STRAIGHT_ANGLE); + + // return best_option candidate if it is nearly straight and distinct from the nearest other + // out + // way + if (best_option_deviation < MAXIMAL_ALLOWED_NO_TURN_DEVIATION && + std::min(left_deviation, right_deviation) > FUZZY_ANGLE_DIFFERENCE) + return best_option; + + const auto &left_data = node_based_graph.GetEdgeData(intersection[left_index].eid); + const auto &right_data = node_based_graph.GetEdgeData(intersection[right_index].eid); + + const bool obvious_to_left = + left_index == 0 || obviousByRoadClass(in_way_data.road_classification, + best_option_data.road_classification, + left_data.road_classification); + const bool obvious_to_right = + right_index == 0 || obviousByRoadClass(in_way_data.road_classification, + best_option_data.road_classification, + right_data.road_classification); + + // if the best_option turn isn't narrow, but there is a nearly straight turn, we don't + // consider the + // turn obvious + const auto check_narrow = [&intersection, best_option_deviation](const std::size_t index) { + return angularDeviation(intersection[index].angle, STRAIGHT_ANGLE) <= + FUZZY_ANGLE_DIFFERENCE && + (best_option_deviation > NARROW_TURN_ANGLE || intersection[index].entry_allowed); + }; + + // other narrow turns? + if (check_narrow(right_index) && !obvious_to_right) + return 0; + + if (check_narrow(left_index) && !obvious_to_left) + return 0; + + // checks if a given way in the intersection is distinct enough from the best_option + // candidate + const auto isDistinct = [&](const std::size_t index, const double deviation) { + /* + If the neighbor is not possible to enter, we allow for a lower + distinction rate. If the road category is smaller, its also adjusted. Only + roads of the same priority require the full distinction ratio. + */ + const auto &best_option_data = + node_based_graph.GetEdgeData(intersection[best_option].eid); + const auto adjusted_distinction_ratio = [&]() { + // not allowed competitors are easily distinct + if (!intersection[index].entry_allowed) + return 0.7 * DISTINCTION_RATIO; + // a bit less obvious are road classes + else if (in_way_data.road_classification == best_option_data.road_classification && + best_option_data.road_classification.GetPriority() < + node_based_graph.GetEdgeData(intersection[index].eid) + .road_classification.GetPriority()) + return 0.8 * DISTINCTION_RATIO; + // if road classes are the same, we use the full ratio + else + return DISTINCTION_RATIO; + }(); + return index == 0 || deviation / best_option_deviation >= adjusted_distinction_ratio || + (deviation <= NARROW_TURN_ANGLE && !intersection[index].entry_allowed); + }; + + const bool distinct_to_left = isDistinct(left_index, left_deviation); + const bool distinct_to_right = isDistinct(right_index, right_deviation); + // Well distinct turn that is nearly straight + if ((distinct_to_left || obvious_to_left) && (distinct_to_right || obvious_to_right)) + return best_option; + } + else + { + const auto &continue_data = node_based_graph.GetEdgeData(intersection[best_continue].eid); + if (std::abs(best_continue_deviation) < 1) + return best_continue; + + // check if any other similar best continues exist + std::size_t i, last = intersection.size(); + for (i = 1; i < last; ++i) + { + if (i == best_continue || !intersection[i].entry_allowed) + continue; + + const auto &turn_data = node_based_graph.GetEdgeData(intersection[i].eid); + const bool is_obvious_by_road_class = + obviousByRoadClass(in_way_data.road_classification, + continue_data.road_classification, + turn_data.road_classification); + + // if the main road is obvious by class, we ignore the current road as a potential + // prevention of obviousness + if (is_obvious_by_road_class) + continue; + + // continuation could be grouped with a straight turn and the turning road is a ramp + if (turn_data.road_classification.IsRampClass() && + best_continue_deviation < GROUP_ANGLE && + !continue_data.road_classification.IsRampClass()) + continue; + + // perfectly straight turns prevent obviousness + const auto turn_deviation = angularDeviation(intersection[i].angle, STRAIGHT_ANGLE); + if (turn_deviation < FUZZY_ANGLE_DIFFERENCE) + return 0; + + const auto deviation_ratio = turn_deviation / best_continue_deviation; + + // in comparison to normal deviations, a continue road can offer a smaller distinction + // ratio. Other roads close to the turn angle are not as obvious, if one road continues. + if (deviation_ratio < DISTINCTION_RATIO / 1.5) + return 0; + + /* in comparison to another continuing road, we need a better distinction. This prevents + situations where the turn is probably less obvious. An example are places that have a + road with the same name entering/exiting: + + d + / + / + a -- b + \ + \ + c + */ + + const auto same_name = !util::guidance::requiresNameAnnounced( + turn_data.name_id, continue_data.name_id, name_table, street_name_suffix_table); + + if (same_name && deviation_ratio < 1.5 * DISTINCTION_RATIO) + return 0; + } + + // Segregated intersections can result in us finding an obvious turn, even though its only + // obvious due to a very short segment in between. So if the segment coming in is very + // short, we check the previous intersection for other continues in the opposite bearing. + const auto node_at_intersection = node_based_graph.GetTarget(via_edge); + + const double constexpr MAX_COLLAPSE_DISTANCE = 30; + const auto distance_at_u_turn = intersection[0].segment_length; + if (distance_at_u_turn < MAX_COLLAPSE_DISTANCE) + { + // this request here actually goes against the direction of the ingoing edgeid. This can + // even reverse the direction. Since we don't want to compute actual turns but simply + // try to find whether there is a turn going to the opposite direction of our obvious + // turn, this should be alright. + NodeID new_node; + const auto previous_intersection = [&]() { + EdgeID turn_edge; + std::tie(new_node, turn_edge) = intersection_generator.SkipDegreeTwoNodes( + node_at_intersection, intersection[0].eid); + return intersection_generator.GetConnectedRoads(new_node, turn_edge); + }(); + + if (new_node != node_at_intersection) + { + const auto continue_road = intersection[best_continue]; + for (const auto &comparison_road : previous_intersection) + { + // since we look at the intersection in the wrong direction, a similar angle + // actually represents a near 180 degree different in bearings between the two + // roads. So if there is a road that is enterable in the opposite direction just + // prior, a turn is not obvious + const auto &turn_data = node_based_graph.GetEdgeData(comparison_road.eid); + if (angularDeviation(comparison_road.angle, STRAIGHT_ANGLE) > GROUP_ANGLE && + angularDeviation(comparison_road.angle, continue_road.angle) < + FUZZY_ANGLE_DIFFERENCE && + !turn_data.reversed && continue_data.CanCombineWith(turn_data)) + return 0; + } + } + } + + return best_continue; + } + + return 0; +} + } // namespace guidance } // namespace extractor } // namespace osrm diff --git a/include/extractor/guidance/node_based_graph_walker.hpp b/include/extractor/guidance/node_based_graph_walker.hpp index 2401ca93d..e477cbf33 100644 --- a/include/extractor/guidance/node_based_graph_walker.hpp +++ b/include/extractor/guidance/node_based_graph_walker.hpp @@ -4,6 +4,7 @@ #include "extractor/guidance/constants.hpp" #include "extractor/guidance/intersection_generator.hpp" #include "util/coordinate.hpp" +#include "util/coordinate_calculation.hpp" #include "util/node_based_graph.hpp" #include "util/typedefs.hpp" @@ -41,7 +42,7 @@ class NodeBasedGraphWalker boost::optional> TraverseRoad(NodeID starting_at_node_id, EdgeID following_edge_id, accumulator_type &accumulator, - const selector_type &selector); + const selector_type &selector) const; private: const util::NodeBasedDynamicGraph &node_based_graph; @@ -139,7 +140,7 @@ boost::optional> NodeBasedGraphWalker::TraverseRoad(NodeID current_node_id, EdgeID current_edge_id, accumulator_type &accumulator, - const selector_type &selector) + const selector_type &selector) const { /* * since graph hopping is used in many ways, we don't generate an adjusted intersection @@ -190,6 +191,60 @@ NodeBasedGraphWalker::TraverseRoad(NodeID current_node_id, return {}; } +struct SkipTrafficSignalBarrierRoadSelector +{ + boost::optional operator()(const NodeID, + const EdgeID, + const IntersectionView &intersection, + const util::NodeBasedDynamicGraph &) const + { + if (intersection.isTrafficSignalOrBarrier()) + { + return boost::make_optional(intersection[1].eid); + } + else + { + return boost::none; + } + } +}; + +struct DistanceToNextIntersectionAccumulator +{ + DistanceToNextIntersectionAccumulator( + const extractor::guidance::CoordinateExtractor &extractor_, + const util::NodeBasedDynamicGraph &graph_, + const double threshold) + : extractor{extractor_}, graph{graph_}, threshold{threshold} + { + } + + bool terminate() + { + if (distance > threshold) + { + too_far_away = true; + return true; + } + + return false; + } + + void update(const NodeID start, const EdgeID onto, const NodeID) + { + using namespace util::coordinate_calculation; + + const auto coords = extractor.GetForwardCoordinatesAlongRoad(start, onto); + distance += getLength(coords, &haversineDistance); + } + + const extractor::guidance::CoordinateExtractor &extractor; + const util::NodeBasedDynamicGraph &graph; + const double threshold; + bool too_far_away = false; + double distance = 0.; +}; + } // namespace guidance } // namespace extractor } // namespace osrm diff --git a/include/extractor/guidance/sliproad_handler.hpp b/include/extractor/guidance/sliproad_handler.hpp index a5a35eb1b..82e2e7d0b 100644 --- a/include/extractor/guidance/sliproad_handler.hpp +++ b/include/extractor/guidance/sliproad_handler.hpp @@ -11,6 +11,8 @@ #include +#include + namespace osrm { namespace extractor @@ -20,7 +22,7 @@ namespace guidance // Intersection handlers deal with all issues related to intersections. // They assign appropriate turn operations to the TurnOperations. -class SliproadHandler : public IntersectionHandler +class SliproadHandler final : public IntersectionHandler { public: SliproadHandler(const IntersectionGenerator &intersection_generator, @@ -40,6 +42,37 @@ class SliproadHandler : public IntersectionHandler Intersection operator()(const NodeID nid, const EdgeID via_eid, Intersection intersection) const override final; + + private: + boost::optional getObviousIndexWithSliproads(const EdgeID from, + const Intersection &intersection, + const NodeID at) const; + + // Next intersection from `start` onto `onto` is too far away for a Siproad scenario + bool nextIntersectionIsTooFarAway(const NodeID start, const EdgeID onto) const; + + // Through street: does a road continue with from's name at the intersection + bool isThroughStreet(const EdgeID from, const IntersectionView &intersection) const; + + // Does the road from `current` to `next` continue + bool roadContinues(const EdgeID current, const EdgeID next) const; + + // Is the area under the triangle a valid Sliproad triangle + bool isValidSliproadArea(const double max_area, const NodeID, const NodeID, const NodeID) const; + + // Is the Sliproad a link the both roads it shortcuts must not be links + bool isValidSliproadLink(const IntersectionViewData &sliproad, + const IntersectionViewData &first, + const IntersectionViewData &second) const; + + // Could a Sliproad reach this intersection? + static bool canBeTargetOfSliproad(const IntersectionView &intersection); + + // Scales a threshold based on the underlying road classification. + // Example: a 100 m threshold for a highway if different on living streets. + // The return value is guaranteed to not be larger than `threshold`. + static double scaledThresholdByRoadClass(const double max_threshold, + const RoadClassification &classification); }; } // namespace guidance diff --git a/src/extractor/guidance/intersection.cpp b/src/extractor/guidance/intersection.cpp index 9d71b76ec..18bafa9bb 100644 --- a/src/extractor/guidance/intersection.cpp +++ b/src/extractor/guidance/intersection.cpp @@ -1,13 +1,7 @@ #include "extractor/guidance/intersection.hpp" -#include -#include - -#include - -#include -#include #include +#include using osrm::util::angularDeviation; @@ -71,34 +65,6 @@ std::string toString(const ConnectedRoad &road) return result; } -bool Intersection::valid() const -{ - return !empty() && - std::is_sorted(begin(), end(), std::mem_fn(&ConnectedRoad::compareByAngle)) && - operator[](0).angle < std::numeric_limits::epsilon(); -} - -std::uint8_t -Intersection::getHighestConnectedLaneCount(const util::NodeBasedDynamicGraph &graph) const -{ - BOOST_ASSERT(valid()); // non empty() - - const std::function to_lane_count = - [&](const ConnectedRoad &road) { - return graph.GetEdgeData(road.eid).road_classification.GetNumberOfLanes(); - }; - - std::uint8_t max_lanes = 0; - const auto extract_maximal_value = [&max_lanes](std::uint8_t value) { - max_lanes = std::max(max_lanes, value); - return false; - }; - - const auto view = *this | boost::adaptors::transformed(to_lane_count); - boost::range::find_if(view, extract_maximal_value); - return max_lanes; -} - } // namespace guidance } // namespace extractor } // namespace osrm diff --git a/src/extractor/guidance/intersection_handler.cpp b/src/extractor/guidance/intersection_handler.cpp index d7ebe1b3c..c3b10008a 100644 --- a/src/extractor/guidance/intersection_handler.cpp +++ b/src/extractor/guidance/intersection_handler.cpp @@ -1,9 +1,8 @@ #include "extractor/guidance/intersection_handler.hpp" #include "extractor/guidance/constants.hpp" -#include "util/coordinate_calculation.hpp" -#include "util/log.hpp" #include "util/guidance/name_announcements.hpp" +#include "util/log.hpp" #include #include @@ -33,17 +32,11 @@ IntersectionHandler::IntersectionHandler(const util::NodeBasedDynamicGraph &node const IntersectionGenerator &intersection_generator) : node_based_graph(node_based_graph), node_info_list(node_info_list), name_table(name_table), street_name_suffix_table(street_name_suffix_table), - intersection_generator(intersection_generator) + intersection_generator(intersection_generator), + graph_walker(node_based_graph, intersection_generator) { } -std::size_t IntersectionHandler::countValid(const Intersection &intersection) const -{ - return std::count_if(intersection.begin(), intersection.end(), [](const ConnectedRoad &road) { - return road.entry_allowed; - }); -} - TurnType::Enum IntersectionHandler::findBasicTurnType(const EdgeID via_edge, const ConnectedRoad &road) const { @@ -371,423 +364,44 @@ bool IntersectionHandler::isThroughStreet(const std::size_t index, return false; } -std::size_t IntersectionHandler::findObviousTurn(const EdgeID via_edge, - const Intersection &intersection) const +boost::optional +IntersectionHandler::getNextIntersection(const NodeID at, const EdgeID via) const { - // no obvious road - if (intersection.size() == 1) - return 0; + // We use the intersection generator to jump over traffic signals, barriers. The intersection + // generater takes a starting node and a corresponding edge starting at this node. It returns + // the next non-artificial intersection writing as out param. the source node and the edge + // for which the target is the next intersection. + // + // . . + // a . . tl . . c . + // . . + // + // e0 ^ ^ e1 + // + // Starting at node `a` via edge `e0` the intersection generator returns the intersection at `c` + // writing `tl` (traffic signal) node and the edge `e1` which has the intersection as target. - // a single non u-turn is obvious - if (intersection.size() == 2) - return 1; + NodeID node = SPECIAL_NODEID; + EdgeID edge = SPECIAL_EDGEID; - const EdgeData &in_way_data = node_based_graph.GetEdgeData(via_edge); + std::tie(node, edge) = intersection_generator.SkipDegreeTwoNodes(at, via); + auto intersection = intersection_generator(node, edge); - // the strategy for picking the most obvious turn involves deciding between - // an overall best candidate and a best candidate that shares the same name - // as the in road, i.e. a continue road - std::size_t best_option = 0; - double best_option_deviation = 180; - std::size_t best_continue = 0; - double best_continue_deviation = 180; - - /* helper functions */ - const auto IsContinueRoad = [&](const EdgeData &way_data) { - return !util::guidance::requiresNameAnnounced( - in_way_data.name_id, way_data.name_id, name_table, street_name_suffix_table); - }; - auto sameOrHigherPriority = [&in_way_data](const auto &way_data) { - return way_data.road_classification.GetPriority() <= - in_way_data.road_classification.GetPriority(); - }; - auto IsLowPriority = [](const auto &way_data) { - return way_data.road_classification.IsLowPriorityRoadClass(); - }; - // These two Compare functions are used for sifting out best option and continue - // candidates by evaluating all the ways in an intersection by what they share - // with the in way. Ideal candidates are of similar road class with the in way - // and are require relatively straight turns. - const auto RoadCompare = [&](const ConnectedRoad &lhs, const ConnectedRoad &rhs) { - const EdgeData &lhs_data = node_based_graph.GetEdgeData(lhs.eid); - const EdgeData &rhs_data = node_based_graph.GetEdgeData(rhs.eid); - const auto lhs_deviation = angularDeviation(lhs.angle, STRAIGHT_ANGLE); - const auto rhs_deviation = angularDeviation(rhs.angle, STRAIGHT_ANGLE); - - const bool rhs_same_classification = - rhs_data.road_classification == in_way_data.road_classification; - const bool lhs_same_classification = - lhs_data.road_classification == in_way_data.road_classification; - const bool rhs_same_or_higher_priority = sameOrHigherPriority(rhs_data); - const bool rhs_low_priority = IsLowPriority(rhs_data); - const bool lhs_same_or_higher_priority = sameOrHigherPriority(lhs_data); - const bool lhs_low_priority = IsLowPriority(lhs_data); - auto left_tie = std::tie(lhs.entry_allowed, - lhs_same_or_higher_priority, - rhs_low_priority, - rhs_deviation, - lhs_same_classification); - auto right_tie = std::tie(rhs.entry_allowed, - rhs_same_or_higher_priority, - lhs_low_priority, - lhs_deviation, - rhs_same_classification); - return left_tie > right_tie; - }; - const auto RoadCompareSameName = [&](const ConnectedRoad &lhs, const ConnectedRoad &rhs) { - const EdgeData &lhs_data = node_based_graph.GetEdgeData(lhs.eid); - const EdgeData &rhs_data = node_based_graph.GetEdgeData(rhs.eid); - const auto lhs_continues = IsContinueRoad(lhs_data); - const auto rhs_continues = IsContinueRoad(rhs_data); - const auto left_tie = std::tie(lhs.entry_allowed, lhs_continues); - const auto right_tie = std::tie(rhs.entry_allowed, rhs_continues); - return left_tie > right_tie || (left_tie == right_tie && RoadCompare(lhs, rhs)); - }; - - auto best_option_it = std::min_element(begin(intersection), end(intersection), RoadCompare); - - // min element should only return end() when vector is empty - BOOST_ASSERT(best_option_it != end(intersection)); - - best_option = std::distance(begin(intersection), best_option_it); - best_option_deviation = angularDeviation(intersection[best_option].angle, STRAIGHT_ANGLE); - const auto &best_option_data = node_based_graph.GetEdgeData(intersection[best_option].eid); - - // Unless the in way is also low priority, it is generally undesirable to - // indicate that a low priority road is obvious - if (IsLowPriority(best_option_data) && - best_option_data.road_classification != in_way_data.road_classification) + // This should never happen, guard against nevertheless + if (node == SPECIAL_NODEID || edge == SPECIAL_EDGEID) { - best_option = 0; - best_option_deviation = 180; + return boost::none; } - // double check if the way with the lowest deviation from straight is still be better choice - const auto straightest = intersection.findClosestTurn(STRAIGHT_ANGLE); - if (straightest != best_option_it) + auto intersection_node = node_based_graph.GetTarget(edge); + + if (intersection.size() <= 2 || intersection.isTrafficSignalOrBarrier()) { - const EdgeData &straightest_data = node_based_graph.GetEdgeData(straightest->eid); - double straightest_data_deviation = angularDeviation(straightest->angle, STRAIGHT_ANGLE); - const auto deviation_diff = - std::abs(best_option_deviation - straightest_data_deviation) > FUZZY_ANGLE_DIFFERENCE; - const auto not_ramp_class = !straightest_data.road_classification.IsRampClass(); - const auto not_link_class = !straightest_data.road_classification.IsLinkClass(); - if (deviation_diff && !IsLowPriority(straightest_data) && not_ramp_class && - not_link_class && !IsContinueRoad(best_option_data)) - { - best_option = std::distance(begin(intersection), straightest); - best_option_deviation = - angularDeviation(intersection[best_option].angle, STRAIGHT_ANGLE); - } + return boost::none; } - // No non-low priority roads? Declare no obvious turn - if (best_option == 0) - return 0; - - auto best_continue_it = - std::min_element(begin(intersection), end(intersection), RoadCompareSameName); - const auto best_continue_data = node_based_graph.GetEdgeData(best_continue_it->eid); - if (IsContinueRoad(best_continue_data) || - (in_way_data.name_id == EMPTY_NAMEID && best_continue_data.name_id == EMPTY_NAMEID)) - { - best_continue = std::distance(begin(intersection), best_continue_it); - best_continue_deviation = - angularDeviation(intersection[best_continue].angle, STRAIGHT_ANGLE); - } - - // if the best angle is going straight but the road is turning, declare no obvious turn - if (0 != best_continue && best_option != best_continue && - best_option_deviation < MAXIMAL_ALLOWED_NO_TURN_DEVIATION && - node_based_graph.GetEdgeData(intersection[best_continue].eid).road_classification == - best_option_data.road_classification) - { - return 0; - } - - // get a count of number of ways from that intersection that qualify to have - // continue instruction because they share a name with the approaching way - const std::int64_t continue_count = - count_if(++begin(intersection), end(intersection), [&](const ConnectedRoad &way) { - return IsContinueRoad(node_based_graph.GetEdgeData(way.eid)); - }); - const std::int64_t continue_count_valid = - count_if(++begin(intersection), end(intersection), [&](const ConnectedRoad &way) { - return IsContinueRoad(node_based_graph.GetEdgeData(way.eid)) && way.entry_allowed; - }); - - // checks if continue candidates are sharp turns - const bool all_continues_are_narrow = [&]() { - return std::count_if( - begin(intersection), end(intersection), [&](const ConnectedRoad &road) { - const EdgeData &road_data = node_based_graph.GetEdgeData(road.eid); - const double &road_angle = angularDeviation(road.angle, STRAIGHT_ANGLE); - return IsContinueRoad(road_data) && (road_angle < NARROW_TURN_ANGLE); - }) == continue_count; - }(); - - // return true if the best_option candidate is more promising than the best_continue candidate - // otherwise return false, the best_continue candidate is more promising - const auto best_over_best_continue = [&]() { - // no continue road exists - if (best_continue == 0) - return true; - - // we have multiple continues and not all are narrow. This suggests that - // the continue candidates are ambiguous - if (!all_continues_are_narrow && (continue_count >= 2 && intersection.size() >= 4)) - return true; - - // if the best continue is not narrow and we also have at least 2 possible choices, the - // intersection size does not matter anymore - if (continue_count_valid >= 2 && best_continue_deviation >= 2 * NARROW_TURN_ANGLE) - return true; - - // continue data now most certainly exists - const auto &continue_data = node_based_graph.GetEdgeData(intersection[best_continue].eid); - - // best_continue is obvious by road class - if (obviousByRoadClass(in_way_data.road_classification, - continue_data.road_classification, - best_option_data.road_classification)) - return false; - - // best_option is obvious by road class - if (obviousByRoadClass(in_way_data.road_classification, - best_option_data.road_classification, - continue_data.road_classification)) - return true; - - // the best_option deviation is very straight and not a ramp - if (best_option_deviation < best_continue_deviation && - best_option_deviation < FUZZY_ANGLE_DIFFERENCE && - !best_option_data.road_classification.IsRampClass()) - return true; - - // the continue road is of a lower priority, while the road continues on the same priority - // with a better angle - if (best_option_deviation < best_continue_deviation && - in_way_data.road_classification == best_option_data.road_classification && - continue_data.road_classification.GetPriority() > - best_option_data.road_classification.GetPriority()) - return true; - - return false; - }(); - - if (best_over_best_continue) - { - // Find left/right deviation - // skipping over service roads - const std::size_t left_index = [&]() { - const auto index_candidate = (best_option + 1) % intersection.size(); - if (index_candidate == 0) - return index_candidate; - const auto &candidate_data = - node_based_graph.GetEdgeData(intersection[index_candidate].eid); - if (obviousByRoadClass(in_way_data.road_classification, - best_option_data.road_classification, - candidate_data.road_classification)) - return (index_candidate + 1) % intersection.size(); - else - return index_candidate; - - }(); - const auto right_index = [&]() { - BOOST_ASSERT(best_option > 0); - const auto index_candidate = best_option - 1; - if (index_candidate == 0) - return index_candidate; - const auto candidate_data = - node_based_graph.GetEdgeData(intersection[index_candidate].eid); - if (obviousByRoadClass(in_way_data.road_classification, - best_option_data.road_classification, - candidate_data.road_classification)) - return index_candidate - 1; - else - return index_candidate; - }(); - - const double left_deviation = - angularDeviation(intersection[left_index].angle, STRAIGHT_ANGLE); - const double right_deviation = - angularDeviation(intersection[right_index].angle, STRAIGHT_ANGLE); - - // return best_option candidate if it is nearly straight and distinct from the nearest other - // out - // way - if (best_option_deviation < MAXIMAL_ALLOWED_NO_TURN_DEVIATION && - std::min(left_deviation, right_deviation) > FUZZY_ANGLE_DIFFERENCE) - return best_option; - - const auto &left_data = node_based_graph.GetEdgeData(intersection[left_index].eid); - const auto &right_data = node_based_graph.GetEdgeData(intersection[right_index].eid); - - const bool obvious_to_left = - left_index == 0 || obviousByRoadClass(in_way_data.road_classification, - best_option_data.road_classification, - left_data.road_classification); - const bool obvious_to_right = - right_index == 0 || obviousByRoadClass(in_way_data.road_classification, - best_option_data.road_classification, - right_data.road_classification); - - // if the best_option turn isn't narrow, but there is a nearly straight turn, we don't - // consider the - // turn obvious - const auto check_narrow = [&intersection, best_option_deviation](const std::size_t index) { - return angularDeviation(intersection[index].angle, STRAIGHT_ANGLE) <= - FUZZY_ANGLE_DIFFERENCE && - (best_option_deviation > NARROW_TURN_ANGLE || intersection[index].entry_allowed); - }; - - // other narrow turns? - if (check_narrow(right_index) && !obvious_to_right) - return 0; - - if (check_narrow(left_index) && !obvious_to_left) - return 0; - - // checks if a given way in the intersection is distinct enough from the best_option - // candidate - const auto isDistinct = [&](const std::size_t index, const double deviation) { - /* - If the neighbor is not possible to enter, we allow for a lower - distinction rate. If the road category is smaller, its also adjusted. Only - roads of the same priority require the full distinction ratio. - */ - const auto &best_option_data = - node_based_graph.GetEdgeData(intersection[best_option].eid); - const auto adjusted_distinction_ratio = [&]() { - // not allowed competitors are easily distinct - if (!intersection[index].entry_allowed) - return 0.7 * DISTINCTION_RATIO; - // a bit less obvious are road classes - else if (in_way_data.road_classification == best_option_data.road_classification && - best_option_data.road_classification.GetPriority() < - node_based_graph.GetEdgeData(intersection[index].eid) - .road_classification.GetPriority()) - return 0.8 * DISTINCTION_RATIO; - // if road classes are the same, we use the full ratio - else - return DISTINCTION_RATIO; - }(); - return index == 0 || deviation / best_option_deviation >= adjusted_distinction_ratio || - (deviation <= NARROW_TURN_ANGLE && !intersection[index].entry_allowed); - }; - - const bool distinct_to_left = isDistinct(left_index, left_deviation); - const bool distinct_to_right = isDistinct(right_index, right_deviation); - // Well distinct turn that is nearly straight - if ((distinct_to_left || obvious_to_left) && (distinct_to_right || obvious_to_right)) - return best_option; - } - else - { - const auto &continue_data = node_based_graph.GetEdgeData(intersection[best_continue].eid); - if (std::abs(best_continue_deviation) < 1) - return best_continue; - - // check if any other similar best continues exist - std::size_t i, last = intersection.size(); - for (i = 1; i < last; ++i) - { - if (i == best_continue || !intersection[i].entry_allowed) - continue; - - const auto &turn_data = node_based_graph.GetEdgeData(intersection[i].eid); - const bool is_obvious_by_road_class = - obviousByRoadClass(in_way_data.road_classification, - continue_data.road_classification, - turn_data.road_classification); - - // if the main road is obvious by class, we ignore the current road as a potential - // prevention of obviousness - if (is_obvious_by_road_class) - continue; - - // continuation could be grouped with a straight turn and the turning road is a ramp - if (turn_data.road_classification.IsRampClass() && - best_continue_deviation < GROUP_ANGLE && - !continue_data.road_classification.IsRampClass()) - continue; - - // perfectly straight turns prevent obviousness - const auto turn_deviation = angularDeviation(intersection[i].angle, STRAIGHT_ANGLE); - if (turn_deviation < FUZZY_ANGLE_DIFFERENCE) - return 0; - - const auto deviation_ratio = turn_deviation / best_continue_deviation; - - // in comparison to normal deviations, a continue road can offer a smaller distinction - // ratio. Other roads close to the turn angle are not as obvious, if one road continues. - if (deviation_ratio < DISTINCTION_RATIO / 1.5) - return 0; - - /* in comparison to another continuing road, we need a better distinction. This prevents - situations where the turn is probably less obvious. An example are places that have a - road with the same name entering/exiting: - - d - / - / - a -- b - \ - \ - c - */ - - const auto same_name = !util::guidance::requiresNameAnnounced( - turn_data.name_id, continue_data.name_id, name_table, street_name_suffix_table); - - if (same_name && deviation_ratio < 1.5 * DISTINCTION_RATIO) - return 0; - } - - // Segregated intersections can result in us finding an obvious turn, even though its only - // obvious due to a very short segment in between. So if the segment coming in is very - // short, we check the previous intersection for other continues in the opposite bearing. - const auto node_at_intersection = node_based_graph.GetTarget(via_edge); - - const double constexpr MAX_COLLAPSE_DISTANCE = 30; - const auto distance_at_u_turn = intersection[0].segment_length; - if (distance_at_u_turn < MAX_COLLAPSE_DISTANCE) - { - // this request here actually goes against the direction of the ingoing edgeid. This can - // even reverse the direction. Since we don't want to compute actual turns but simply - // try to find whether there is a turn going to the opposite direction of our obvious - // turn, this should be alright. - NodeID new_node; - const auto previous_intersection = [&]() { - EdgeID turn_edge; - std::tie(new_node, turn_edge) = intersection_generator.SkipDegreeTwoNodes( - node_at_intersection, intersection[0].eid); - return intersection_generator.GetConnectedRoads(new_node, turn_edge); - }(); - - if (new_node != node_at_intersection) - { - const auto continue_road = intersection[best_continue]; - for (const auto &comparison_road : previous_intersection) - { - // since we look at the intersection in the wrong direction, a similar angle - // actually represents a near 180 degree different in bearings between the two - // roads. So if there is a road that is enterable in the opposite direction just - // prior, a turn is not obvious - const auto &turn_data = node_based_graph.GetEdgeData(comparison_road.eid); - if (angularDeviation(comparison_road.angle, STRAIGHT_ANGLE) > GROUP_ANGLE && - angularDeviation(comparison_road.angle, continue_road.angle) < - FUZZY_ANGLE_DIFFERENCE && - !turn_data.reversed && continue_data.CanCombineWith(turn_data)) - return 0; - } - } - } - - return best_continue; - } - - return 0; + return boost::make_optional( + IntersectionViewAndNode{std::move(intersection), intersection_node}); } } // namespace guidance diff --git a/src/extractor/guidance/motorway_handler.cpp b/src/extractor/guidance/motorway_handler.cpp index 0f0c80bdf..71642a3ad 100644 --- a/src/extractor/guidance/motorway_handler.cpp +++ b/src/extractor/guidance/motorway_handler.cpp @@ -193,7 +193,7 @@ Intersection MotorwayHandler::fromMotorway(const EdgeID via_eid, Intersection in assignFork(via_eid, intersection[3], intersection[2], intersection[1]); } - else if (countValid(intersection) > 0) // check whether turns exist at all + else if (intersection.countEnterable() > 0) // check whether turns exist at all { // FALLBACK, this should hopefully never be reached return fallback(std::move(intersection)); @@ -338,7 +338,7 @@ Intersection MotorwayHandler::fromMotorway(const EdgeID via_eid, Intersection in Intersection MotorwayHandler::fromRamp(const EdgeID via_eid, Intersection intersection) const { - auto num_valid_turns = countValid(intersection); + auto num_valid_turns = intersection.countEnterable(); // ramp straight into a motorway/ramp if (intersection.size() == 2 && num_valid_turns == 1) { diff --git a/src/extractor/guidance/node_based_graph_walker.cpp b/src/extractor/guidance/node_based_graph_walker.cpp index c09f3972a..8f5a235fd 100644 --- a/src/extractor/guidance/node_based_graph_walker.cpp +++ b/src/extractor/guidance/node_based_graph_walker.cpp @@ -1,5 +1,4 @@ #include "extractor/guidance/node_based_graph_walker.hpp" -#include "util/coordinate_calculation.hpp" #include using osrm::util::angularDeviation; diff --git a/src/extractor/guidance/sliproad_handler.cpp b/src/extractor/guidance/sliproad_handler.cpp index 2f60bb68d..e55d82f24 100644 --- a/src/extractor/guidance/sliproad_handler.cpp +++ b/src/extractor/guidance/sliproad_handler.cpp @@ -4,11 +4,14 @@ #include "util/coordinate_calculation.hpp" #include "util/guidance/name_announcements.hpp" +#include + +#include +#include #include #include -using EdgeData = osrm::util::NodeBasedDynamicGraph::EdgeData; using osrm::extractor::guidance::getTurnDirection; using osrm::util::angularDeviation; @@ -32,7 +35,8 @@ SliproadHandler::SliproadHandler(const IntersectionGenerator &intersection_gener { } -// included for interface reasons only +// The intersection has to connect a Sliproad, see the example scenario below: +// Intersection at `d`: Sliproad `bd` connecting `cd` and the road starting at `d`. bool SliproadHandler::canProcess(const NodeID /*nid*/, const EdgeID /*via_eid*/, const Intersection &intersection) const @@ -40,261 +44,699 @@ bool SliproadHandler::canProcess(const NodeID /*nid*/, return intersection.size() > 2; } +// Detect sliproad b-d in the following example: +// +// . +// e +// . +// . +// a ... b .... c . +// ` . +// ` . +// ` . +// d +// . +// +// ^ a nid +// ^ ab source_edge_id +// ^ b intersection Intersection SliproadHandler:: -operator()(const NodeID, const EdgeID source_edge_id, Intersection intersection) const +operator()(const NodeID /*nid*/, const EdgeID source_edge_id, Intersection intersection) const { + BOOST_ASSERT(intersection.size() > 2); + + // Potential splitting / start of a Sliproad (b) auto intersection_node_id = node_based_graph.GetTarget(source_edge_id); - // if there is no turn, there is no sliproad - if (intersection.size() <= 2) - return intersection; + // Road index prefering non-sliproads (bc) + auto obvious = getObviousIndexWithSliproads(source_edge_id, intersection, intersection_node_id); - const auto findNextIntersectionForRoad = - [&](const NodeID at_node, const ConnectedRoad &road, NodeID &output_node) { - auto intersection = intersection_generator(at_node, road.eid); - auto in_edge = road.eid; - // skip over traffic lights - // to prevent ending up in an endless loop, we remember all visited nodes. This is - // necessary, since merging of roads can actually create enterable loops of degree two - std::unordered_set visited_nodes; - auto node = at_node; - while (intersection.size() == 2 && visited_nodes.count(node) == 0) - { - visited_nodes.insert(node); - node = node_based_graph.GetTarget(in_edge); - if (node == at_node) - { - // we ended up in a loop without exit - output_node = SPECIAL_NODEID; - intersection.clear(); - return intersection; - } - in_edge = intersection[1].eid; - output_node = node_based_graph.GetTarget(in_edge); - intersection = intersection_generator(node, in_edge); - } - if (intersection.size() <= 2) - { - output_node = SPECIAL_NODEID; - intersection.clear(); - } - return intersection; - }; - - const std::size_t obvious_turn_index = [&]() -> std::size_t { - const auto index = findObviousTurn(source_edge_id, intersection); - if (index != 0) - return index; - else if (intersection.size() == 3 && intersection[1].instruction.type == TurnType::Fork) - { - // Forks themselves do not contain a `obvious` turn index. If we look at a fork that has - // a one-sided sliproad, however, the non-sliproad can be considered `obvious`. Here we - // assume that this could be the case and check for a potential sliproad/non-sliproad - // situation. - NodeID intersection_node_one = SPECIAL_NODEID, intersection_node_two = SPECIAL_NODEID; - const auto intersection_following_index_one = findNextIntersectionForRoad( - intersection_node_id, intersection[1], intersection_node_one); - const auto intersection_following_index_two = findNextIntersectionForRoad( - intersection_node_id, intersection[2], intersection_node_two); - // in case of broken roads, we return - if (intersection_following_index_one.empty() || - intersection_following_index_two.empty()) - return 0; - - // In case of loops at the end of the road, we will arrive back at the intersection - // itself. If that is the case, the road is obviously not a sliproad. - - // a sliproad has to enter a road without choice - const auto couldBeSliproad = [](const IntersectionView &intersection) { - if (intersection.size() != 3) - return false; - if ((intersection[1].entry_allowed && intersection[2].entry_allowed) || - intersection[0].entry_allowed) - return false; - return true; - }; - - if (couldBeSliproad(intersection_following_index_one) && - intersection_node_id != intersection_node_two) - return 2; - else if (couldBeSliproad(intersection_following_index_two) && - intersection_node_id != intersection_node_one) - return 1; - else - return 0; - } - else - return 0; - }(); - - if (obvious_turn_index == 0) - return intersection; - - const auto &next_road = intersection[obvious_turn_index]; - - const auto linkTest = [this, next_road](const ConnectedRoad &road) { - return !node_based_graph.GetEdgeData(road.eid).roundabout && road.entry_allowed && - angularDeviation(road.angle, STRAIGHT_ANGLE) <= 2 * NARROW_TURN_ANGLE && - !hasRoundaboutType(road.instruction) && - angularDeviation(next_road.angle, road.angle) > - std::numeric_limits::epsilon(); - }; - - bool hasNarrow = - std::find_if(intersection.begin(), intersection.end(), linkTest) != intersection.end(); - if (!hasNarrow) - return intersection; - - const auto source_edge_data = node_based_graph.GetEdgeData(source_edge_id); - // check whether the continue road is valid - const auto check_valid = [this, source_edge_data](const ConnectedRoad &road) { - const auto road_edge_data = node_based_graph.GetEdgeData(road.eid); - // Test to see if the source edge and the one we're looking at are the same road - - const auto same_name = !util::guidance::requiresNameAnnounced( - road_edge_data.name_id, source_edge_data.name_id, name_table, street_name_suffix_table); - - return road_edge_data.road_classification == source_edge_data.road_classification && - road_edge_data.name_id != EMPTY_NAMEID && source_edge_data.name_id != EMPTY_NAMEID && - same_name && road.entry_allowed; - }; - - if (!check_valid(next_road)) - return intersection; - - // Threshold check, if the intersection is too far away, don't bother continuing - const auto coordinate_extractor = intersection_generator.GetCoordinateExtractor(); - const auto next_road_length = util::coordinate_calculation::getLength( - coordinate_extractor.GetForwardCoordinatesAlongRoad( - node_based_graph.GetTarget(source_edge_id), next_road.eid), - &util::coordinate_calculation::haversineDistance); - if (next_road_length > MAX_SLIPROAD_THRESHOLD) + if (!obvious) { return intersection; } - auto next_intersection_node = node_based_graph.GetTarget(next_road.eid); - const auto next_road_next_intersection = - findNextIntersectionForRoad(intersection_node_id, next_road, next_intersection_node); + // Potential non-sliproad road (bc), leading to the intersection (c) the Sliproad (bd) shortcuts + const auto &main_road = intersection[*obvious]; - if (next_road_next_intersection.empty()) + // The road leading to the intersection (bc) has to continue from our source + if (!roadContinues(source_edge_id, main_road.eid)) + { return intersection; + } + + // Link-check for (bc) and later on (cd) which both are getting shortcutted by Sliproad + const auto is_potential_link = [this, main_road](const ConnectedRoad &road) { + if (!road.entry_allowed) + { + return false; + } + + // Prevent from starting in or going onto a roundabout + auto onto_roundabout = hasRoundaboutType(road.instruction); + + if (onto_roundabout) + { + return false; + } + + // Narrow turn angle for road (bd) and guard against data issues (overlapping roads) + auto is_narrow = angularDeviation(road.angle, STRAIGHT_ANGLE) <= 2 * NARROW_TURN_ANGLE; + auto same_angle = angularDeviation(main_road.angle, road.angle) // + <= std::numeric_limits::epsilon(); + + if (!is_narrow || same_angle) + { + return false; + } + + const auto &road_data = node_based_graph.GetEdgeData(road.eid); + + auto is_roundabout = road_data.roundabout; + + if (is_roundabout) + { + return false; + } + + return true; + }; + + if (!std::any_of(begin(intersection), end(intersection), is_potential_link)) + { + return intersection; + } + + // If the intersection is too far away, don't bother continuing + if (nextIntersectionIsTooFarAway(intersection_node_id, main_road.eid)) + { + return intersection; + } + + // Try to find the intersection at (c) which the Sliproad shortcuts + const auto main_road_intersection = getNextIntersection(intersection_node_id, main_road.eid); + + if (!main_road_intersection) + { + return intersection; + } + + if (main_road_intersection->intersection.isDeadEnd()) + { + return intersection; + } // If we are at a traffic loop at the end of a road, don't consider it a sliproad - if (intersection_node_id == next_intersection_node) - return intersection; - - std::unordered_set target_road_names; - - for (const auto &road : next_road_next_intersection) + if (intersection_node_id == main_road_intersection->node) { - const auto &target_data = node_based_graph.GetEdgeData(road.eid); - target_road_names.insert(target_data.name_id); + return intersection; } - for (auto &road : intersection) + std::vector target_road_name_ids; + target_road_name_ids.reserve(main_road_intersection->intersection.size()); + + for (const auto &road : main_road_intersection->intersection) { - if (linkTest(road)) + const auto &target_data = node_based_graph.GetEdgeData(road.eid); + target_road_name_ids.push_back(target_data.name_id); + } + + auto sliproad_found = false; + + // For all roads at the main intersection except the UTurn road: check Sliproad scenarios. + for (std::size_t road_index = 1, last = intersection.size(); road_index < last; ++road_index) + { + const auto index_left_of_main_road = (*obvious - 1) % intersection.size(); + const auto index_right_of_main_road = (*obvious + 1) % intersection.size(); + + // Be strict and require the Sliproad to be either left or right of the main road. + if (road_index != index_left_of_main_road && road_index != index_right_of_main_road) + continue; + + auto &sliproad = intersection[road_index]; // this is what we're checking and assigning to + const auto &sliproad_data = node_based_graph.GetEdgeData(sliproad.eid); + + // Intersection is orderd: 0 is UTurn, then from sharp right to sharp left. + // We already have an obvious index (bc) for going straight-ish. + const auto is_right_sliproad_turn = road_index < *obvious; + const auto is_left_sliproad_turn = road_index > *obvious; + + // Road at the intersection the main road leads onto which the sliproad arrives onto + const auto crossing_road = [&] { + if (is_left_sliproad_turn) + return main_road_intersection->intersection.getLeftmostRoad(); + + if (is_right_sliproad_turn) + return main_road_intersection->intersection.getRightmostRoad(); + + BOOST_ASSERT_MSG(false, "Sliproad is neither a left nor right of obvious main road"); + return main_road_intersection->intersection.getLeftmostRoad(); + }(); + + const auto &crossing_road_data = node_based_graph.GetEdgeData(crossing_road.eid); + + // The crossing road at the main road's intersection must not be incoming-only + if (crossing_road_data.reversed) { - EdgeID candidate_in = road.eid; - const auto target_intersection = [&](NodeID node) { - auto intersection = intersection_generator(node, candidate_in); - // skip over traffic lights - if (intersection.size() == 2) - { - node = node_based_graph.GetTarget(candidate_in); - candidate_in = intersection[1].eid; - intersection = intersection_generator(node, candidate_in); - } - return intersection; - }(intersection_node_id); + continue; + } - const auto link_data = node_based_graph.GetEdgeData(road.eid); - // Check if the road continues here - const bool is_through_street = - !target_intersection.empty() && link_data.name_id != EMPTY_NAMEID && - target_intersection.end() != - std::find_if( - target_intersection.begin() + 1, - target_intersection.end(), - [this, &link_data](const IntersectionViewData &road) { - const auto &road_edge_data = node_based_graph.GetEdgeData(road.eid); + // Discard service and other low priority roads - never Sliproad candidate + if (sliproad_data.road_classification.IsLowPriorityRoadClass()) + { + continue; + } - const auto same_name = - road_edge_data.name_id != EMPTY_NAMEID && - !util::guidance::requiresNameAnnounced(road_edge_data.name_id, - link_data.name_id, - name_table, - street_name_suffix_table); + // Incoming-only can never be a Sliproad + if (sliproad_data.reversed) + { + continue; + } - return same_name; - }); + // This is what we know so far: + // + // . + // e + // . + // . + // a ... b .... c . < `main_road_intersection` is intersection at `c` + // ` . + // ` . + // ` . + // d < `target_intersection` is intersection at `d` + // . `sliproad_edge_target` is node `d` + // e + // + // + // ^ `sliproad` is `bd` + // ^ `intersection` is intersection at `b` - // if the sliproad candidate is a through street, we cannot handle it as a sliproad - if (is_through_street) - continue; + if (!is_potential_link(sliproad)) + { + continue; + } - for (const auto &candidate_road : target_intersection) + // The Sliproad graph edge - in the following we use the graph walker to + // adjust this edge forward jumping over artificial intersections. + auto sliproad_edge = sliproad.eid; + + // Starting out at the intersection and going onto the Sliproad we skip artificial + // degree two intersections and limit the max hop count in doing so. + IntersectionFinderAccumulator intersection_finder{10, intersection_generator}; + const SkipTrafficSignalBarrierRoadSelector road_selector{}; + (void)graph_walker.TraverseRoad(intersection_node_id, // start node + sliproad_edge, // onto edge + intersection_finder, // accumulator + road_selector); // selector + + sliproad_edge = intersection_finder.via_edge_id; + const auto target_intersection = intersection_finder.intersection; + + // Constrain the Sliproad's target to sliproad, outgoing, incoming from main intersection + if (target_intersection.size() != 3) + { + continue; + } + + const NodeID sliproad_edge_target = node_based_graph.GetTarget(sliproad_edge); + + // Distinct triangle nodes `bcd` + if (intersection_node_id == main_road_intersection->node || + intersection_node_id == sliproad_edge_target || + main_road_intersection->node == sliproad_edge_target) + { + continue; + } + + // If the sliproad candidate is a through street, we cannot handle it as a sliproad. + if (isThroughStreet(sliproad_edge, target_intersection)) + { + continue; + } + + // The turn off of the Sliproad has to be obvious and a narrow turn + { + const auto index = findObviousTurn(sliproad_edge, target_intersection); + + if (index == 0) { - const auto &candidate_data = node_based_graph.GetEdgeData(candidate_road.eid); - if (target_road_names.count(candidate_data.name_id) > 0) - { - if (node_based_graph.GetTarget(candidate_road.eid) == next_intersection_node) - { - road.instruction.type = TurnType::Sliproad; - break; - } - else - { - const auto skip_traffic_light_intersection = intersection_generator( - node_based_graph.GetTarget(candidate_in), candidate_road.eid); - if (skip_traffic_light_intersection.size() == 2 && - node_based_graph.GetTarget(skip_traffic_light_intersection[1].eid) == - next_intersection_node) - { + continue; + } - road.instruction.type = TurnType::Sliproad; - break; - } - } + const auto onto = target_intersection[index]; + const auto angle_deviation = angularDeviation(onto.angle, STRAIGHT_ANGLE); + const auto is_narrow_turn = angle_deviation <= 2 * NARROW_TURN_ANGLE; + + if (!is_narrow_turn) + { + continue; + } + } + + // Check for curvature. Depending on the turn's direction at `c`. Scenario for right turn: + // + // a ... b .... c . a ... b .... c . a ... b .... c . + // ` . ` . . . . + // ` . . . . . + // ` . .. . . + // d d . d + // + // Sliproad Not a Sliproad + { + const auto &extractor = intersection_generator.GetCoordinateExtractor(); + + const NodeID start = intersection_node_id; // b + const EdgeID edge = sliproad_edge; // bd + + const auto coords = extractor.GetForwardCoordinatesAlongRoad(start, edge); + BOOST_ASSERT(coords.size() >= 2); + + // Now keep start and end coordinate fix and check for curvature + const auto start_coord = coords.front(); + const auto end_coord = coords.back(); + + const auto first = std::begin(coords) + 1; + const auto last = std::end(coords) - 1; + + auto snuggles = false; + + using namespace util::coordinate_calculation; + + // In addition, if it's a right/left turn we expect the rightmost/leftmost + // turn at `c` to be more or less ~90 degree for a Sliproad scenario. + double deviation_from_straight = 0; + + if (is_right_sliproad_turn) + { + snuggles = std::all_of(first, last, [=](auto each) { // + return !isCCW(start_coord, each, end_coord); + }); + + const auto rightmost = main_road_intersection->intersection.getRightmostRoad(); + deviation_from_straight = angularDeviation(rightmost.angle, STRAIGHT_ANGLE); + } + else if (is_left_sliproad_turn) + { + snuggles = std::all_of(first, last, [=](auto each) { // + return isCCW(start_coord, each, end_coord); + }); + + const auto leftmost = main_road_intersection->intersection.getLeftmostRoad(); + deviation_from_straight = angularDeviation(leftmost.angle, STRAIGHT_ANGLE); + } + + // The data modelling for small Sliproads is not reliable enough. + // Only check for curvature and ~90 degree when it makes sense to do so. + const constexpr auto MIN_LENGTH = 3.; + + const auto length = haversineDistance(node_info_list[intersection_node_id], + node_info_list[main_road_intersection->node]); + + const double perpendicular_angle = 90 + FUZZY_ANGLE_DIFFERENCE; + + if (length >= MIN_LENGTH) + { + if (!snuggles) + { + continue; + } + + if (deviation_from_straight > perpendicular_angle) + { + continue; + } + } + } + + // Check for area under triangle `bdc`. + // + // a ... b .... c . + // ` . + // ` . + // ` . + // d + // + const auto area_threshold = std::pow( + scaledThresholdByRoadClass(MAX_SLIPROAD_THRESHOLD, sliproad_data.road_classification), + 2.); + + if (!isValidSliproadArea(area_threshold, + intersection_node_id, + main_road_intersection->node, + sliproad_edge_target)) + { + continue; + } + + // Check all roads at `d` if one is connected to `c`, is so `bd` is Sliproad. + for (const auto &candidate_road : target_intersection) + { + const auto &candidate_data = node_based_graph.GetEdgeData(candidate_road.eid); + + // Name mismatch: check roads at `c` and `d` for same name + const auto name_mismatch = [&](const NameID road_name_id) { + const auto unnamed = road_name_id == EMPTY_NAMEID; + + return unnamed || + util::guidance::requiresNameAnnounced(road_name_id, // + candidate_data.name_id, // + name_table, // + street_name_suffix_table); // + + }; + + const auto candidate_road_name_mismatch = std::all_of(begin(target_road_name_ids), // + end(target_road_name_ids), // + name_mismatch); // + + if (candidate_road_name_mismatch) + { + continue; + } + + // If the Sliproad `bd` is a link, `bc` and `cd` must not be links. + if (!isValidSliproadLink(sliproad, main_road, candidate_road)) + { + continue; + } + + if (node_based_graph.GetTarget(candidate_road.eid) == main_road_intersection->node) + { + sliproad.instruction.type = TurnType::Sliproad; + sliproad_found = true; + break; + } + else + { + const auto skip_traffic_light_intersection = intersection_generator( + node_based_graph.GetTarget(sliproad_edge), candidate_road.eid); + if (skip_traffic_light_intersection.isTrafficSignalOrBarrier() && + node_based_graph.GetTarget(skip_traffic_light_intersection[1].eid) == + main_road_intersection->node) + { + + sliproad.instruction.type = TurnType::Sliproad; + sliproad_found = true; + break; } } } } - if (next_road.instruction.type == TurnType::Fork) + // Now in case we found a Sliproad and assigned the corresponding type to the road, + // it could be that the intersection from which the Sliproad splits off was a Fork before. + // In those cases the obvious non-Sliproad is now obvious and we discard the Fork turn type. + if (sliproad_found && main_road.instruction.type == TurnType::Fork) { - const auto &next_data = node_based_graph.GetEdgeData(next_road.eid); + const auto &source_edge_data = node_based_graph.GetEdgeData(source_edge_id); + const auto &main_road_data = node_based_graph.GetEdgeData(main_road.eid); - const auto same_name = - next_data.name_id != EMPTY_NAMEID && source_edge_data.name_id != EMPTY_NAMEID && - !util::guidance::requiresNameAnnounced( - next_data.name_id, source_edge_data.name_id, name_table, street_name_suffix_table); + const auto same_name = source_edge_data.name_id != EMPTY_NAMEID && // + main_road_data.name_id != EMPTY_NAMEID && // + !util::guidance::requiresNameAnnounced(source_edge_data.name_id, + main_road_data.name_id, + name_table, + street_name_suffix_table); // if (same_name) { - if (angularDeviation(next_road.angle, STRAIGHT_ANGLE) < 5) - intersection[obvious_turn_index].instruction.type = TurnType::Suppressed; + if (angularDeviation(main_road.angle, STRAIGHT_ANGLE) < 5) + intersection[*obvious].instruction.type = TurnType::Suppressed; else - intersection[obvious_turn_index].instruction.type = TurnType::Continue; - intersection[obvious_turn_index].instruction.direction_modifier = - getTurnDirection(intersection[obvious_turn_index].angle); + intersection[*obvious].instruction.type = TurnType::Continue; + intersection[*obvious].instruction.direction_modifier = + getTurnDirection(intersection[*obvious].angle); } - else if (next_data.name_id != EMPTY_NAMEID) + else if (main_road_data.name_id != EMPTY_NAMEID) { - intersection[obvious_turn_index].instruction.type = TurnType::NewName; - intersection[obvious_turn_index].instruction.direction_modifier = - getTurnDirection(intersection[obvious_turn_index].angle); + intersection[*obvious].instruction.type = TurnType::NewName; + intersection[*obvious].instruction.direction_modifier = + getTurnDirection(intersection[*obvious].angle); } else { - intersection[obvious_turn_index].instruction.type = TurnType::Suppressed; + intersection[*obvious].instruction.type = TurnType::Suppressed; } } return intersection; } +// Implementation details + +boost::optional SliproadHandler::getObviousIndexWithSliproads( + const EdgeID from, const Intersection &intersection, const NodeID at) const +{ + BOOST_ASSERT(from != SPECIAL_EDGEID); + BOOST_ASSERT(at != SPECIAL_NODEID); + + // If a turn is obvious without taking Sliproads into account use this + const auto index = findObviousTurn(from, intersection); + + if (index != 0) + { + return boost::make_optional(index); + } + + // Otherwise check if the road is forking into two and one of them is a Sliproad; + // then the non-Sliproad is the obvious one. + if (intersection.size() != 3) + { + return boost::none; + } + + const auto forking = intersection[1].instruction.type == TurnType::Fork && + intersection[2].instruction.type == TurnType::Fork; + + if (!forking) + { + return boost::none; + } + + const auto first = getNextIntersection(at, intersection.getRightmostRoad().eid); + const auto second = getNextIntersection(at, intersection.getLeftmostRoad().eid); + + if (!first || !second) + { + return boost::none; + } + + if (first->intersection.isDeadEnd() || second->intersection.isDeadEnd()) + { + return boost::none; + } + + // In case of loops at the end of the road, we will arrive back at the intersection + // itself. If that is the case, the road is obviously not a sliproad. + if (canBeTargetOfSliproad(first->intersection) && at != second->node) + { + return boost::make_optional(std::size_t{2}); + } + + if (canBeTargetOfSliproad(second->intersection) && at != first->node) + { + return boost::make_optional(std::size_t{1}); + } + + return boost::none; +} + +bool SliproadHandler::nextIntersectionIsTooFarAway(const NodeID start, const EdgeID onto) const +{ + BOOST_ASSERT(start != SPECIAL_NODEID); + BOOST_ASSERT(onto != SPECIAL_EDGEID); + + const auto &extractor = intersection_generator.GetCoordinateExtractor(); + + // Base max distance threshold on the current road class we're on + const auto &data = node_based_graph.GetEdgeData(onto); + const auto threshold = scaledThresholdByRoadClass(MAX_SLIPROAD_THRESHOLD, // <- scales down + data.road_classification); + + DistanceToNextIntersectionAccumulator accumulator{extractor, node_based_graph, threshold}; + const SkipTrafficSignalBarrierRoadSelector selector{}; + + (void)graph_walker.TraverseRoad(start, onto, accumulator, selector); + + return accumulator.too_far_away; +} + +bool SliproadHandler::isThroughStreet(const EdgeID from, const IntersectionView &intersection) const +{ + BOOST_ASSERT(from != SPECIAL_EDGEID); + BOOST_ASSERT(!intersection.empty()); + + const auto &edge_name_id = node_based_graph.GetEdgeData(from).name_id; + + auto first = begin(intersection) + 1; // Skip UTurn road + auto last = end(intersection); + + auto same_name = [&](const auto &road) { + const auto &road_name_id = node_based_graph.GetEdgeData(road.eid).name_id; + + return edge_name_id != EMPTY_NAMEID && // + road_name_id != EMPTY_NAMEID && // + !util::guidance::requiresNameAnnounced(edge_name_id, + road_name_id, + name_table, + street_name_suffix_table); // + }; + + return std::find_if(first, last, same_name) != last; +} + +bool SliproadHandler::roadContinues(const EdgeID current, const EdgeID next) const +{ + const auto ¤t_data = node_based_graph.GetEdgeData(current); + const auto &next_data = node_based_graph.GetEdgeData(next); + + auto same_road_category = current_data.road_classification == next_data.road_classification; + auto same_travel_mode = current_data.travel_mode == next_data.travel_mode; + + auto same_name = current_data.name_id != EMPTY_NAMEID && // + next_data.name_id != EMPTY_NAMEID && // + !util::guidance::requiresNameAnnounced(current_data.name_id, + next_data.name_id, + name_table, + street_name_suffix_table); // + + const auto continues = same_road_category && same_travel_mode && same_name; + return continues; +} + +bool SliproadHandler::isValidSliproadArea(const double max_area, + const NodeID a, + const NodeID b, + const NodeID c) const + +{ + using namespace util::coordinate_calculation; + + const auto first = node_info_list[a]; + const auto second = node_info_list[b]; + const auto third = node_info_list[c]; + + const auto length = haversineDistance(first, second); + const auto heigth = haversineDistance(second, third); + + const auto area = (length * heigth) / 2.; + + // Everything below is data issue - there are some weird situations where + // nodes are really close to each other and / or tagging ist just plain off. + const constexpr auto MIN_SLIPROAD_AREA = 3.; + + if (area < MIN_SLIPROAD_AREA || area > max_area) + { + return false; + } + + return true; +} + +bool SliproadHandler::isValidSliproadLink(const IntersectionViewData &sliproad, + const IntersectionViewData &first, + const IntersectionViewData &second) const +{ + // If the Sliproad is not a link we don't care + const auto &sliproad_data = node_based_graph.GetEdgeData(sliproad.eid); + if (!sliproad_data.road_classification.IsLinkClass()) + { + return true; + } + + // otherwise neither the first road leading to the intersection we shortcut + const auto &first_road_data = node_based_graph.GetEdgeData(first.eid); + if (first_road_data.road_classification.IsLinkClass()) + { + return false; + } + + // nor the second road coming from the intersection we shotcut must be links + const auto &second_road_data = node_based_graph.GetEdgeData(second.eid); + if (second_road_data.road_classification.IsLinkClass()) + { + return false; + } + + return true; +} + +bool SliproadHandler::canBeTargetOfSliproad(const IntersectionView &intersection) +{ + // Example to handle: + // . + // a . . b . + // ` . + // ` . + // c < intersection + // . + // + + // One outgoing two incoming + if (intersection.size() != 3) + { + return false; + } + + const auto backwards = intersection[0].entry_allowed; + const auto multiple_allowed = intersection[1].entry_allowed && intersection[2].entry_allowed; + + if (backwards || multiple_allowed) + { + return false; + } + + return true; +} + +double SliproadHandler::scaledThresholdByRoadClass(const double max_threshold, + const RoadClassification &classification) +{ + double factor = 1.0; + + switch (classification.GetPriority()) + { + case RoadPriorityClass::MOTORWAY: + factor = 1.0; + break; + case RoadPriorityClass::TRUNK: + factor = 0.8; + break; + case RoadPriorityClass::PRIMARY: + factor = 0.8; + break; + case RoadPriorityClass::SECONDARY: + factor = 0.6; + break; + case RoadPriorityClass::TERTIARY: + factor = 0.5; + break; + case RoadPriorityClass::MAIN_RESIDENTIAL: + factor = 0.4; + break; + case RoadPriorityClass::SIDE_RESIDENTIAL: + factor = 0.3; + break; + case RoadPriorityClass::LINK_ROAD: + factor = 0.3; + break; + case RoadPriorityClass::CONNECTIVITY: + factor = 0.1; + break; + + // What + case RoadPriorityClass::BIKE_PATH: + case RoadPriorityClass::FOOT_PATH: + default: + factor = 0.1; + } + + const auto scaled = max_threshold * factor; + + BOOST_ASSERT(scaled <= max_threshold); + return scaled; +} + } // namespace guidance } // namespace extractor } // namespace osrm