From 37e3777fcd11377e338e2b3463a3eea4420a9327 Mon Sep 17 00:00:00 2001 From: Kajari Ghosh Date: Sat, 7 Apr 2018 22:20:59 -0400 Subject: [PATCH] Add support for annotations=distances in MLD This commit brings feature parity with CH for the `table` pluging. --- features/testbot/distance_matrix.feature | 96 +++- features/testbot/duration_matrix.feature | 22 + features/testbot/multi_level_routing.feature | 34 ++ features/testbot/traffic_speeds.feature | 2 +- .../routing_algorithms/many_to_many.hpp | 7 +- .../routing_algorithms/routing_base_mld.hpp | 201 ++++++- include/util/query_heap.hpp | 2 + profiles/testbot.lua | 12 +- src/engine/plugins/table.cpp | 8 - .../direct_shortest_path.cpp | 9 + .../routing_algorithms/many_to_many_ch.cpp | 3 +- .../routing_algorithms/many_to_many_mld.cpp | 509 +++++++++++++++--- test/nodejs/table.js | 3 +- 13 files changed, 793 insertions(+), 115 deletions(-) diff --git a/features/testbot/distance_matrix.feature b/features/testbot/distance_matrix.feature index 104727350..57cce0b44 100644 --- a/features/testbot/distance_matrix.feature +++ b/features/testbot/distance_matrix.feature @@ -1,4 +1,4 @@ -@matrix @testbot @ch +@matrix @testbot Feature: Basic Distance Matrix # note that results of travel distance are in metres @@ -21,6 +21,7 @@ Feature: Basic Distance Matrix | a | 0 | 100+-1 | | b | 100+-1 | 0 | + @ch Scenario: Testbot - Travel distance matrix of minimal network with toll exclude Given the query options | exclude | toll | @@ -45,6 +46,7 @@ Feature: Basic Distance Matrix | c | | | 0 | 100+-1 | | d | | | 100+-1 | 0 | + @ch Scenario: Testbot - Travel distance matrix of minimal network with motorway exclude Given the query options | exclude | motorway | @@ -66,8 +68,8 @@ Feature: Basic Distance Matrix | | a | b | c | d | | a | 0 | 300+-2 | 100+-2 | 200+-2 | - - Scenario: Testbot - Travel distance matrix of minimal network disconnected motorway exclude + @ch + Scenario: Testbot - Travel distance matrix of minimal network disconnected motorway exclude Given the query options | exclude | motorway | And the extract extra arguments "--small-component-size 4" @@ -88,7 +90,7 @@ Feature: Basic Distance Matrix | | a | b | e | | a | 0 | 50+-1 | | - + @ch Scenario: Testbot - Travel distance matrix of minimal network with motorway and toll excludes Given the query options | exclude | motorway,toll | @@ -212,6 +214,13 @@ Feature: Basic Distance Matrix | be | | cf | + When I route I should get + | from | to | distance | + | e | a | 200m +- 1 | + | e | b | 100m +- 1 | + | f | a | 300m +- 1 | + | f | b | 200m +- 1 | + When I request a travel distance matrix I should get | | a | b | e | f | | a | 0 | 100+-1 | 200+-1 | 300+-1 | @@ -255,7 +264,6 @@ Feature: Basic Distance Matrix | e | 200+-1 | 100+-1 | 0 | 100+-1 | | f | 300+-1 | 200+-1 | 100+-1 | 0 | - Scenario: Testbot - Travel distance 3x2 matrix Given the node map """ @@ -445,10 +453,21 @@ Feature: Basic Distance Matrix | 7 | 300+-5 | 200+-5 | 600+-5 | 500+-5 | 900+-5 | 800+-5 | 0 | 1100+-5 | | 8 | 400+-5 | 300+-5 | 700+-5 | 600+-5 | 1000+-5 | 900+-5 | 100+-5 | 0 | + When I request a travel distance matrix I should get + | | 1 | + | 1 | 0 | + | 2 | 100+-5 | + | 3 | 900+-5 | + | 4 | 1000+-5 | + | 5 | 600+-5 | + | 6 | 700+-5 | + | 7 | 300+-5 | + | 8 | 400+-5 | + Scenario: Testbot - Travel distance matrix with ties Given the node map """ - a b + a b c d """ @@ -466,21 +485,26 @@ Feature: Basic Distance Matrix When I route I should get | from | to | route | distance | - | a | b | ab,ab | 300m +- 1 | + | a | b | ab,ab | 450m | | a | c | ac,ac | 200m | - | a | d | ab,bd,bd | 500m +- 1 | + | a | d | ac,dc,dc | 500m +- 1 | When I request a travel distance matrix I should get | | a | b | c | d | - | a | 0 | 300+-2 | 200+-2 | 500+-2 | + | a | 0 | 450+-2 | 200+-2 | 500+-2 | When I request a travel distance matrix I should get | | a | | a | 0 | - | b | 300+-2 | + | b | 450+-2 | | c | 200+-2 | | d | 500+-2 | + When I request a travel distance matrix I should get + | | a | c | + | a | 0 | 200+-2 | + | c | 200+-2 | 0 | + # Check rounding errors Scenario: Testbot - Long distances in tables @@ -492,8 +516,58 @@ Feature: Basic Distance Matrix And the ways | nodes | - | abcd | + | abcd | When I request a travel distance matrix I should get | | a | b | c | d | | a | 0 | 1000+-3 | 2000+-3 | 3000+-3 | + + + Scenario: Testbot - OneToMany vs ManyToOne + Given the node map + """ + a b + c + """ + + And the ways + | nodes | oneway | + | ab | yes | + | ac | | + | bc | | + + When I request a travel distance matrix I should get + | | a | b | + | b | 240.4 | 0 | + + When I request a travel distance matrix I should get + | | a | + | a | 0 | + | b | 240.4 | + + Scenario: Testbot - Varying distances between nodes + Given the node map + """ + a b c d + + e + + + + f + """ + + And the ways + | nodes | oneway | + | feabcd | yes | + | ec | | + | fd | | + + When I request a travel distance matrix I should get + | | a | b | c | d | e | f | + | a | 0 | 100+-1 | 300+-1 | 650+-1 | 1930+-1 | 1533+-1 | + | b | 760+-1 | 0 | 200+-1 | 550+-1 | 1830+-1 | 1433+-1 | + | c | 560+-2 | 660+-2 | 0 | 350+-1 | 1630+-1 | 1233+-1 | + | d | 1480+-2 | 1580+-1 | 1780+-1 | 0 | 1280+-1 | 883+-1 | + | e | 200+-2 | 300+-2 | 500+-1 | 710+-1 | 0 | 1593+-1 | + | f | 597+-1 | 696+-1 | 896+-1 | 1108+-1 | 400+-3 | 0 | diff --git a/features/testbot/duration_matrix.feature b/features/testbot/duration_matrix.feature index f93ea7936..c6977dfab 100644 --- a/features/testbot/duration_matrix.feature +++ b/features/testbot/duration_matrix.feature @@ -488,3 +488,25 @@ Feature: Basic Duration Matrix | b | 1 | | c | 15 | | d | 10 | + + Scenario: Testbot - OneToMany vs ManyToOne + Given the node map + """ + a b + c + """ + + And the ways + | nodes | oneway | + | ab | yes | + | ac | | + | bc | | + + When I request a travel time matrix I should get + | | a | b | + | b | 24.1 | 0 | + + When I request a travel time matrix I should get + | | a | + | a | 0 | + | b | 24.1 | diff --git a/features/testbot/multi_level_routing.feature b/features/testbot/multi_level_routing.feature index 739faf088..1fd0ab399 100644 --- a/features/testbot/multi_level_routing.feature +++ b/features/testbot/multi_level_routing.feature @@ -106,6 +106,40 @@ Feature: Multi level routing | l | 144.7 | 60 | | o | 124.7 | 0 | + + When I request a travel distance matrix I should get + | | a | f | l | o | + | a | 0+-2 | 2287+-2 | 1443+-2 | 1243+-2 | + | f | 2284+-2 | 0+-2 | 1241+-2 | 1443+-2 | + | l | 1443+-2 | 1244+-2 | 0+-2 | 600+-2 | + | o | 1243+-2 | 1444+-2 | 600+-2 | 0+-2 | + + When I request a travel distance matrix I should get + | | a | f | l | o | + | a | 0 | 2287.2+-2 | 1443+-2 | 1243+-2 | + + When I request a travel distance matrix I should get + | | a | + | a | 0 | + | f | 2284.5+-2 | + | l | 1443.1 | + | o | 1243 | + + When I request a travel distance matrix I should get + | | a | f | l | o | + | a | 0 | 2287+-2 | 1443+-2 | 1243+-2 | + | o | 1243 | 1444+-2 | 600+-2 | 0+-2 | + + + When I request a travel distance matrix I should get + | | a | o | + | a | 0+-2 | 1243+-2 | + | f | 2284+-2 | 1443+-2 | + | l | 1443+-2 | 600+-2 | + | o | 1243+-2 | 0+-2 | + + + Scenario: Testbot - Multi level routing: horizontal road Given the node map """ diff --git a/features/testbot/traffic_speeds.feature b/features/testbot/traffic_speeds.feature index 4e2b4ea26..f5f2198f3 100644 --- a/features/testbot/traffic_speeds.feature +++ b/features/testbot/traffic_speeds.feature @@ -54,7 +54,7 @@ Feature: Traffic - speeds | a | d | ad,ad | 27 km/h | 1275.7,0 | 1 | | d | c | dc,dc | 36 km/h | 956.8,0 | 0 | | g | b | fb,fb | 36 km/h | 164.7,0 | 0 | - | a | g | ad,df,fb,fb | 30 km/h | 1275.7,487.5,304.7,0 | 1:0:0 | + | a | g | ad,df,fb,fb | 30 km/h | 1295.7,487.5,304.7,0 | 1:0:0 | Scenario: Weighting based on speed file weights, ETA based on file durations diff --git a/include/engine/routing_algorithms/many_to_many.hpp b/include/engine/routing_algorithms/many_to_many.hpp index bef14874c..cd5fe9c1c 100644 --- a/include/engine/routing_algorithms/many_to_many.hpp +++ b/include/engine/routing_algorithms/many_to_many.hpp @@ -15,24 +15,25 @@ namespace engine { namespace routing_algorithms { - namespace { struct NodeBucket { NodeID middle_node; NodeID parent_node; + bool from_clique_arc; unsigned column_index; // a column in the weight/duration matrix EdgeWeight weight; EdgeDuration duration; NodeBucket(NodeID middle_node, NodeID parent_node, + bool from_clique_arc, unsigned column_index, EdgeWeight weight, EdgeDuration duration) - : middle_node(middle_node), parent_node(parent_node), column_index(column_index), - weight(weight), duration(duration) + : middle_node(middle_node), parent_node(parent_node), from_clique_arc(from_clique_arc), + column_index(column_index), weight(weight), duration(duration) { } diff --git a/include/engine/routing_algorithms/routing_base_mld.hpp b/include/engine/routing_algorithms/routing_base_mld.hpp index 53d285432..0070be1b4 100644 --- a/include/engine/routing_algorithms/routing_base_mld.hpp +++ b/include/engine/routing_algorithms/routing_base_mld.hpp @@ -54,7 +54,7 @@ inline bool checkParentCellRestriction(CellID, const PhantomNodes &) { return tr // Restricted search (Args is LevelID, CellID): // * use the fixed level for queries -// * check if the node cell is the same as the specified parent onr +// * check if the node cell is the same as the specified parent template inline LevelID getNodeQueryLevel(const MultiLevelPartition &, NodeID, LevelID level, CellID) { @@ -65,6 +65,61 @@ inline bool checkParentCellRestriction(CellID cell, LevelID, CellID parent) { return cell == parent; } + +// Unrestricted search with a single phantom node (Args is const PhantomNode &): +// * use partition.GetQueryLevel to find the node query level +// * allow to traverse all cells +template +inline LevelID getNodeQueryLevel(const MultiLevelPartition &partition, + const NodeID node, + const PhantomNode &phantom_node) +{ + auto highest_diffrent_level = [&partition, node](const SegmentID &phantom_node) { + if (phantom_node.enabled) + return partition.GetHighestDifferentLevel(phantom_node.id, node); + return INVALID_LEVEL_ID; + }; + + const auto node_level = std::min(highest_diffrent_level(phantom_node.forward_segment_id), + highest_diffrent_level(phantom_node.reverse_segment_id)); + + return node_level; +} + +// Unrestricted search with a single phantom node and a vector of phantom nodes: +// * use partition.GetQueryLevel to find the node query level +// * allow to traverse all cells +template +inline LevelID getNodeQueryLevel(const MultiLevelPartition &partition, + NodeID node, + const std::vector &phantom_nodes, + const std::size_t phantom_index, + const std::vector &phantom_indices) +{ + auto min_level = [&partition, node](const PhantomNode &phantom_node) { + + const auto &forward_segment = phantom_node.forward_segment_id; + const auto forward_level = + forward_segment.enabled ? partition.GetHighestDifferentLevel(node, forward_segment.id) + : INVALID_LEVEL_ID; + + const auto &reverse_segment = phantom_node.reverse_segment_id; + const auto reverse_level = + reverse_segment.enabled ? partition.GetHighestDifferentLevel(node, reverse_segment.id) + : INVALID_LEVEL_ID; + + return std::min(forward_level, reverse_level); + }; + + // Get minimum level over all phantoms of the highest different level with respect to node + // This is equivalent to min_{∀ source, target} partition.GetQueryLevel(source, node, target) + auto result = min_level(phantom_nodes[phantom_index]); + for (const auto &index : phantom_indices) + { + result = std::min(result, min_level(phantom_nodes[index])); + } + return result; +} } // Heaps only record for each node its predecessor ("parent") on the shortest path. @@ -74,6 +129,46 @@ inline bool checkParentCellRestriction(CellID cell, LevelID, CellID parent) using PackedEdge = std::tuple; using PackedPath = std::vector; +template +inline void retrievePackedPathFromSingleManyToManyHeap( + const SearchEngineData::ManyToManyQueryHeap &heap, const NodeID middle, OutIter out) +{ + + NodeID current = middle; + NodeID parent = heap.GetData(current).parent; + + while (current != parent) + { + const auto &data = heap.GetData(current); + + if (DIRECTION == FORWARD_DIRECTION) + { + *out = std::make_tuple(parent, current, data.from_clique_arc); + ++out; + } + else if (DIRECTION == REVERSE_DIRECTION) + { + *out = std::make_tuple(current, parent, data.from_clique_arc); + ++out; + } + + current = parent; + parent = heap.GetData(parent).parent; + } +} + +template +inline PackedPath retrievePackedPathFromSingleManyToManyHeap( + const SearchEngineData::ManyToManyQueryHeap &heap, const NodeID middle) +{ + + PackedPath packed_path; + retrievePackedPathFromSingleManyToManyHeap( + heap, middle, std::back_inserter(packed_path)); + + return packed_path; +} + template inline void retrievePackedPathFromSingleHeap(const SearchEngineData::QueryHeap &heap, const NodeID middle, @@ -351,6 +446,21 @@ UnpackedPath search(SearchEngineData &engine_working_data, // Get packed path as edges {from node ID, to node ID, from_clique_arc} auto packed_path = retrievePackedPathFromHeap(forward_heap, reverse_heap, middle); + // if (!packed_path.empty()) + // { + // std::cout << "packed_path: "; + // for (auto edge : packed_path) + // { + // std::cout << std::get<0>(edge) << ","; + // } + // std::cout << std::get<1>(packed_path.back()); + // std::cout << std::endl; + // } + // else + // { + // std::cout << "no packed_path!" << std::endl; + // } + // Beware the edge case when start, middle, end are all the same. // In this case we return a single node, no edges. We also don't unpack. const NodeID source_node = !packed_path.empty() ? std::get<0>(packed_path.front()) : middle; @@ -410,7 +520,96 @@ UnpackedPath search(SearchEngineData &engine_working_data, unpacked_edges.insert(unpacked_edges.end(), subpath_edges.begin(), subpath_edges.end()); } } + // std::cout << "unpacked_nodes: "; + // for (auto node : unpacked_nodes) + // { + // std::cout << node << ", "; + // } + // std::cout << std::endl; + return std::make_tuple(weight, std::move(unpacked_nodes), std::move(unpacked_edges)); +} +// With (s, middle, t) we trace back the paths middle -> s and middle -> t. +// This gives us a packed path (node ids) from the base graph around s and t, +// and overlay node ids otherwise. We then have to unpack the overlay clique +// edges by recursively descending unpacking the path down to the base graph. + +using UnpackedNodes = std::vector; +using UnpackedEdges = std::vector; +using UnpackedPath = std::tuple; + +template +UnpackedPath +unpackPathAndCalculateDistance(SearchEngineData &engine_working_data, + const DataFacade &facade, + typename SearchEngineData::QueryHeap &forward_heap, + typename SearchEngineData::QueryHeap &reverse_heap, + const bool force_loop_forward, + const bool force_loop_reverse, + EdgeWeight weight_upper_bound, + PackedPath packed_path, + NodeID middle, + Args... args) +{ + EdgeWeight weight = weight_upper_bound; + const auto &partition = facade.GetMultiLevelPartition(); + const NodeID source_node = !packed_path.empty() ? std::get<0>(packed_path.front()) : middle; + + // Unpack path + std::vector unpacked_nodes; + std::vector unpacked_edges; + unpacked_nodes.reserve(packed_path.size()); + unpacked_edges.reserve(packed_path.size()); + + unpacked_nodes.push_back(source_node); + + for (auto const &packed_edge : packed_path) + { + NodeID source, target; + bool overlay_edge; + std::tie(source, target, overlay_edge) = packed_edge; + if (!overlay_edge) + { // a base graph edge + unpacked_nodes.push_back(target); + unpacked_edges.push_back(facade.FindEdge(source, target)); + } + else + { // an overlay graph edge + LevelID level = getNodeQueryLevel(partition, source, args...); + CellID parent_cell_id = partition.GetCell(level, source); + BOOST_ASSERT(parent_cell_id == partition.GetCell(level, target)); + + LevelID sublevel = level - 1; + + // Here heaps can be reused, let's go deeper! + forward_heap.Clear(); + reverse_heap.Clear(); + forward_heap.Insert(source, 0, {source}); + reverse_heap.Insert(target, 0, {target}); + + // TODO: when structured bindings will be allowed change to + // auto [subpath_weight, subpath_source, subpath_target, subpath] = ... + EdgeWeight subpath_weight; + std::vector subpath_nodes; + std::vector subpath_edges; + std::tie(subpath_weight, subpath_nodes, subpath_edges) = search(engine_working_data, + facade, + forward_heap, + reverse_heap, + force_loop_forward, + force_loop_reverse, + weight_upper_bound, + sublevel, + parent_cell_id); + BOOST_ASSERT(!subpath_edges.empty()); + BOOST_ASSERT(subpath_nodes.size() > 1); + BOOST_ASSERT(subpath_nodes.front() == source); + BOOST_ASSERT(subpath_nodes.back() == target); + unpacked_nodes.insert( + unpacked_nodes.end(), std::next(subpath_nodes.begin()), subpath_nodes.end()); + unpacked_edges.insert(unpacked_edges.end(), subpath_edges.begin(), subpath_edges.end()); + } + } return std::make_tuple(weight, std::move(unpacked_nodes), std::move(unpacked_edges)); } diff --git a/include/util/query_heap.hpp b/include/util/query_heap.hpp index e547ac973..f4b9e626a 100644 --- a/include/util/query_heap.hpp +++ b/include/util/query_heap.hpp @@ -226,12 +226,14 @@ class QueryHeap Data &GetData(NodeID node) { const auto index = node_index.peek_index(node); + BOOST_ASSERT((int)index >= 0 && (int)index < (int)inserted_nodes.size()); return inserted_nodes[index].data; } Data const &GetData(NodeID node) const { const auto index = node_index.peek_index(node); + BOOST_ASSERT((int)index >= 0 && (int)index < (int)inserted_nodes.size()); return inserted_nodes[index].data; } diff --git a/profiles/testbot.lua b/profiles/testbot.lua index 69f84c27f..ed2e9ade6 100644 --- a/profiles/testbot.lua +++ b/profiles/testbot.lua @@ -5,7 +5,7 @@ -- Secondary road: 18km/h = 18000m/3600s = 100m/20s -- Tertiary road: 12km/h = 12000m/3600s = 100m/30s -api_version = 3 +api_version = 4 function setup() return { @@ -14,7 +14,7 @@ function setup() max_speed_for_map_matching = 30/3.6, --km -> m/s weight_name = 'duration', process_call_tagless_node = false, - uturn_penalty = 20, + u_turn_penalty = 20, traffic_light_penalty = 7, -- seconds use_turn_restrictions = true }, @@ -128,13 +128,15 @@ function process_way (profile, way, result) end function process_turn (profile, turn) - if turn.direction_modifier == direction_modifier.uturn then - turn.duration = profile.properties.uturn_penalty - turn.weight = profile.properties.uturn_penalty + if turn.is_u_turn then + turn.duration = turn.duration + profile.properties.u_turn_penalty + turn.weight = turn.weight + profile.properties.u_turn_penalty end if turn.has_traffic_light then turn.duration = turn.duration + profile.properties.traffic_light_penalty end + + io.write("after penalty turn.duration: ", turn.duration, "turn.weight: ", turn.weight, "\n") end return { diff --git a/src/engine/plugins/table.cpp b/src/engine/plugins/table.cpp index d33d2c991..e10b5461a 100644 --- a/src/engine/plugins/table.cpp +++ b/src/engine/plugins/table.cpp @@ -85,14 +85,6 @@ Status TablePlugin::HandleRequest(const RoutingAlgorithmsInterface &algorithms, bool request_distance = params.annotations & api::TableParameters::AnnotationsType::Distance; bool request_duration = params.annotations & api::TableParameters::AnnotationsType::Duration; - if (request_distance && !algorithms.SupportsDistanceAnnotationType()) - { - return Error("NotImplemented", - "The distance annotations calculation is not implemented for the chosen " - "search algorithm.", - result); - } - auto result_tables_pair = algorithms.ManyToManySearch( snapped_phantoms, params.sources, params.destinations, request_distance, request_duration); diff --git a/src/engine/routing_algorithms/direct_shortest_path.cpp b/src/engine/routing_algorithms/direct_shortest_path.cpp index 08ca3249e..4c6435ade 100644 --- a/src/engine/routing_algorithms/direct_shortest_path.cpp +++ b/src/engine/routing_algorithms/direct_shortest_path.cpp @@ -74,6 +74,15 @@ InternalRouteResult directShortestPathSearch(SearchEngineData &e auto &reverse_heap = *engine_working_data.reverse_heap_1; insertNodesInHeaps(forward_heap, reverse_heap, phantom_nodes); + std::cout << "source_phantom.forward_segment_id.id: " + << phantom_nodes.source_phantom.forward_segment_id.id + << " source_phantom.reverse_segment_id.id: " + << phantom_nodes.source_phantom.reverse_segment_id.id << std::endl; + std::cout << "target_phantom.forward_segment_id.id: " + << phantom_nodes.target_phantom.forward_segment_id.id + << " target_phantom.reverse_segment_id.id: " + << phantom_nodes.target_phantom.reverse_segment_id.id << std::endl; + // TODO: when structured bindings will be allowed change to // auto [weight, source_node, target_node, unpacked_edges] = ... EdgeWeight weight = INVALID_EDGE_WEIGHT; diff --git a/src/engine/routing_algorithms/many_to_many_ch.cpp b/src/engine/routing_algorithms/many_to_many_ch.cpp index 8ee02b5ec..8c59c9173 100644 --- a/src/engine/routing_algorithms/many_to_many_ch.cpp +++ b/src/engine/routing_algorithms/many_to_many_ch.cpp @@ -148,10 +148,11 @@ void backwardRoutingStep(const DataFacade &facade, const auto target_weight = query_heap.GetKey(node); const auto target_duration = query_heap.GetData(node).duration; const auto parent = query_heap.GetData(node).parent; + const bool INVALID_CLIQUE_ARC_TYPE = false; // Store settled nodes in search space bucket search_space_with_buckets.emplace_back( - node, parent, column_index, target_weight, target_duration); + node, parent, INVALID_CLIQUE_ARC_TYPE, column_index, target_weight, target_duration); relaxOutgoingEdges( facade, node, target_weight, target_duration, query_heap, phantom_node); diff --git a/src/engine/routing_algorithms/many_to_many_mld.cpp b/src/engine/routing_algorithms/many_to_many_mld.cpp index 05a8a28d6..0461e116f 100644 --- a/src/engine/routing_algorithms/many_to_many_mld.cpp +++ b/src/engine/routing_algorithms/many_to_many_mld.cpp @@ -1,5 +1,5 @@ #include "engine/routing_algorithms/many_to_many.hpp" -#include "engine/routing_algorithms/routing_base.hpp" +#include "engine/routing_algorithms/routing_base_mld.hpp" #include #include @@ -19,22 +19,8 @@ namespace routing_algorithms namespace mld { -template -inline LevelID getNodeQueryLevel(const MultiLevelPartition &partition, - const NodeID node, - const PhantomNode &phantom_node) -{ - auto highest_diffrent_level = [&partition, node](const SegmentID &phantom_node) { - if (phantom_node.enabled) - return partition.GetHighestDifferentLevel(phantom_node.id, node); - return INVALID_LEVEL_ID; - }; - - const auto node_level = std::min(highest_diffrent_level(phantom_node.forward_segment_id), - highest_diffrent_level(phantom_node.reverse_segment_id)); - - return node_level; -} +using PackedEdge = std::tuple; +using PackedPath = std::vector; template inline LevelID getNodeQueryLevel(const MultiLevelPartition &partition, @@ -50,38 +36,6 @@ inline LevelID getNodeQueryLevel(const MultiLevelPartition &partition, return node_level; } -template -inline LevelID getNodeQueryLevel(const MultiLevelPartition &partition, - NodeID node, - const std::vector &phantom_nodes, - const std::size_t phantom_index, - const std::vector &phantom_indices) -{ - auto min_level = [&partition, node](const PhantomNode &phantom_node) { - - const auto &forward_segment = phantom_node.forward_segment_id; - const auto forward_level = - forward_segment.enabled ? partition.GetHighestDifferentLevel(node, forward_segment.id) - : INVALID_LEVEL_ID; - - const auto &reverse_segment = phantom_node.reverse_segment_id; - const auto reverse_level = - reverse_segment.enabled ? partition.GetHighestDifferentLevel(node, reverse_segment.id) - : INVALID_LEVEL_ID; - - return std::min(forward_level, reverse_level); - }; - - // Get minimum level over all phantoms of the highest different level with respect to node - // This is equivalent to min_{∀ source, target} partition.GetQueryLevel(source, node, target) - auto result = min_level(phantom_nodes[phantom_index]); - for (const auto &index : phantom_indices) - { - result = std::min(result, min_level(phantom_nodes[index])); - } - return result; -} - template void relaxOutgoingEdges(const DataFacade &facade, const NodeID node, @@ -125,8 +79,10 @@ void relaxOutgoingEdges(const DataFacade &facade, { query_heap.Insert(to, to_weight, {node, true, to_duration}); } - else if (std::tie(to_weight, to_duration) < - std::tie(query_heap.GetKey(to), query_heap.GetData(to).duration)) + else if (std::tie(to_weight, to_duration, node) < + std::tie(query_heap.GetKey(to), + query_heap.GetData(to).duration, + query_heap.GetData(to).parent)) { query_heap.GetData(to) = {node, true, to_duration}; query_heap.DecreaseKey(to, to_weight); @@ -155,8 +111,10 @@ void relaxOutgoingEdges(const DataFacade &facade, { query_heap.Insert(to, to_weight, {node, true, to_duration}); } - else if (std::tie(to_weight, to_duration) < - std::tie(query_heap.GetKey(to), query_heap.GetData(to).duration)) + else if (std::tie(to_weight, to_duration, node) < + std::tie(query_heap.GetKey(to), + query_heap.GetData(to).duration, + query_heap.GetData(to).parent)) { query_heap.GetData(to) = {node, true, to_duration}; query_heap.DecreaseKey(to, to_weight); @@ -198,8 +156,10 @@ void relaxOutgoingEdges(const DataFacade &facade, query_heap.Insert(to, to_weight, {node, false, to_duration}); } // Found a shorter Path -> Update weight and set new parent - else if (std::tie(to_weight, to_duration) < - std::tie(query_heap.GetKey(to), query_heap.GetData(to).duration)) + else if (std::tie(to_weight, to_duration, node) < + std::tie(query_heap.GetKey(to), + query_heap.GetData(to).duration, + query_heap.GetData(to).parent)) { query_heap.GetData(to) = {node, false, to_duration}; query_heap.DecreaseKey(to, to_weight); @@ -217,11 +177,13 @@ oneToManySearch(SearchEngineData &engine_working_data, const DataFacade &facade, const std::vector &phantom_nodes, std::size_t phantom_index, - const std::vector &phantom_indices) + const std::vector &phantom_indices, + const bool calculate_distance) { std::vector weights(phantom_indices.size(), INVALID_EDGE_WEIGHT); std::vector durations(phantom_indices.size(), MAXIMAL_EDGE_DURATION); - std::vector distances(phantom_indices.size(), INVALID_EDGE_DISTANCE); + std::vector distances_table; + std::vector middle_nodes_table(phantom_indices.size(), SPECIAL_NODEID); // Collect destination (source) nodes into a map std::unordered_multimap> @@ -289,6 +251,7 @@ oneToManySearch(SearchEngineData &engine_working_data, { weights[index] = path_weight; durations[index] = path_duration; + middle_nodes_table[index] = node; } // Remove node from destinations list @@ -306,12 +269,15 @@ oneToManySearch(SearchEngineData &engine_working_data, // Update single node paths update_values(node, initial_weight, initial_duration); + query_heap.Insert(node, initial_weight, {node, initial_duration}); + // Place adjacent nodes into heap for (auto edge : facade.GetAdjacentEdgeRange(node)) { const auto &data = facade.GetEdgeData(edge); - if ((DIRECTION == FORWARD_DIRECTION) ? facade.IsForwardEdge(edge) - : facade.IsBackwardEdge(edge)) + if ((DIRECTION == FORWARD_DIRECTION ? facade.IsForwardEdge(edge) + : facade.IsBackwardEdge(edge)) && + !query_heap.WasInserted(facade.GetTarget(edge))) { const auto turn_id = data.turn_id; const auto node_id = DIRECTION == FORWARD_DIRECTION ? node : facade.GetTarget(edge); @@ -330,28 +296,35 @@ oneToManySearch(SearchEngineData &engine_working_data, if (DIRECTION == FORWARD_DIRECTION) { - if (phantom_node.IsValidForwardSource()) + { insert_node(phantom_node.forward_segment_id.id, -phantom_node.GetForwardWeightPlusOffset(), -phantom_node.GetForwardDuration()); + } if (phantom_node.IsValidReverseSource()) + { insert_node(phantom_node.reverse_segment_id.id, -phantom_node.GetReverseWeightPlusOffset(), -phantom_node.GetReverseDuration()); + } } else if (DIRECTION == REVERSE_DIRECTION) { if (phantom_node.IsValidForwardTarget()) + { insert_node(phantom_node.forward_segment_id.id, phantom_node.GetForwardWeightPlusOffset(), phantom_node.GetForwardDuration()); + } if (phantom_node.IsValidReverseTarget()) + { insert_node(phantom_node.reverse_segment_id.id, phantom_node.GetReverseWeightPlusOffset(), phantom_node.GetReverseDuration()); + } } } @@ -376,7 +349,127 @@ oneToManySearch(SearchEngineData &engine_working_data, phantom_indices); } - return std::make_pair(durations, distances); + if (calculate_distance) + { + // Initialize unpacking heaps + engine_working_data.InitializeOrClearFirstThreadLocalStorage( + facade.GetNumberOfNodes(), facade.GetMaxBorderNodeID() + 1); + + distances_table.resize(phantom_indices.size(), INVALID_EDGE_DISTANCE); + + for (unsigned location = 0; location < phantom_indices.size(); ++location) + { + // Get the "middle" node that is the last node of a path + const NodeID middle_node_id = middle_nodes_table[location]; + if (middle_node_id == SPECIAL_NODEID) // takes care of one-ways + { + continue; + } + + // Retrieve the packed path from the heap + PackedPath packed_path = mld::retrievePackedPathFromSingleManyToManyHeap( + query_heap, middle_node_id); + + // ... and reverse it to have packed edges in the correct order, + if (DIRECTION == FORWARD_DIRECTION) + { + std::reverse(packed_path.begin(), packed_path.end()); + } + + // ... unpack path + auto &forward_heap = *engine_working_data.forward_heap_1; + auto &reverse_heap = *engine_working_data.reverse_heap_1; + EdgeWeight weight = INVALID_EDGE_WEIGHT; + std::vector unpacked_nodes; + std::vector unpacked_edges; + + std::tie(weight, unpacked_nodes, unpacked_edges) = + unpackPathAndCalculateDistance(engine_working_data, + facade, + forward_heap, + reverse_heap, + DO_NOT_FORCE_LOOPS, + DO_NOT_FORCE_LOOPS, + INVALID_EDGE_WEIGHT, + packed_path, + middle_node_id, + phantom_nodes, + phantom_index, + phantom_indices); + + // Accumulate the path length without the last node + auto annotation = 0.0; + + BOOST_ASSERT(!unpacked_nodes.empty()); + for (auto node = unpacked_nodes.begin(), last_node = std::prev(unpacked_nodes.end()); + node != last_node; + ++node) + { + annotation += computeEdgeDistance(facade, *node); + } + + // ... and add negative source and positive target offsets + // ⚠ for REVERSE_DIRECTION original source and target phantom nodes are swapped + // Get source and target phantom nodes + // * 1-to-N: source is a single index, target is the corresponding from the indices list + // * N-to-1: source is the corresponding from the indices list, target is a single index + auto source_phantom_index = phantom_index; + auto target_phantom_index = phantom_indices[location]; + if (DIRECTION == REVERSE_DIRECTION) + { + std::swap(source_phantom_index, target_phantom_index); + } + const auto &source_phantom = phantom_nodes[source_phantom_index]; + const auto &target_phantom = phantom_nodes[target_phantom_index]; + const NodeID source_node = unpacked_nodes.front(); + const NodeID target_node = unpacked_nodes.back(); + + EdgeDistance source_offset = 0., target_offset = 0.; + if (source_phantom.IsValidForwardSource() && + source_phantom.forward_segment_id.id == source_node) + { + // ............ <-- calculateEGBAnnotation returns distance from 0 + // to 3 + // -->s <-- subtract offset to start at source + // ......... <-- want this distance as result + // entry 0---1---2---3--- <-- 3 is exit node + source_offset = source_phantom.GetForwardDistance(); + } + else if (source_phantom.IsValidReverseSource() && + source_phantom.reverse_segment_id.id == source_node) + { + // ............ <-- calculateEGBAnnotation returns distance from 0 to 3 + // s<------- <-- subtract offset to start at source + // ... <-- want this distance + // entry 0---1---2---3 <-- 3 is exit node + source_offset = source_phantom.GetReverseDistance(); + } + if (target_phantom.IsValidForwardTarget() && + target_phantom.forward_segment_id.id == target_node) + { + // ............ <-- calculateEGBAnnotation returns distance from 0 + // to 3 + // ++>t <-- add offset to get to target + // ................ <-- want this distance as result + // entry 0---1---2---3--- <-- 3 is exit node + target_offset = target_phantom.GetForwardDistance(); + } + else if (target_phantom.IsValidReverseTarget() && + target_phantom.reverse_segment_id.id == target_node) + { + // ............ <-- calculateEGBAnnotation returns distance from 0 + // to 3 + // <++t <-- add offset to get from target + // ................ <-- want this distance as result + // entry 0---1---2---3--- <-- 3 is exit node + target_offset = target_phantom.GetReverseDistance(); + } + + distances_table[location] = -source_offset + annotation + target_offset; + } + } + + return std::make_pair(durations, distances_table); } // @@ -391,6 +484,7 @@ void forwardRoutingStep(const DataFacade &facade, const std::vector &search_space_with_buckets, std::vector &weights_table, std::vector &durations_table, + std::vector &middle_nodes_table, const PhantomNode &phantom_node) { const auto node = query_heap.DeleteMin(); @@ -427,6 +521,7 @@ void forwardRoutingStep(const DataFacade &facade, { current_weight = new_weight; current_duration = new_duration; + middle_nodes_table[location] = node; } } @@ -445,10 +540,11 @@ void backwardRoutingStep(const DataFacade &facade, const auto target_weight = query_heap.GetKey(node); const auto target_duration = query_heap.GetData(node).duration; const auto parent = query_heap.GetData(node).parent; + const auto from_clique_arc = query_heap.GetData(node).from_clique_arc; // Store settled nodes in search space bucket search_space_with_buckets.emplace_back( - node, parent, column_idx, target_weight, target_duration); + node, parent, from_clique_arc, column_idx, target_weight, target_duration); const auto &partition = facade.GetMultiLevelPartition(); const auto maximal_level = partition.GetNumberOfLevels() - 1; @@ -457,13 +553,226 @@ void backwardRoutingStep(const DataFacade &facade, facade, node, target_weight, target_duration, query_heap, phantom_node, maximal_level); } +template +void retrievePackedPathFromSearchSpace(NodeID middle_node_id, + const unsigned column_idx, + const std::vector &search_space_with_buckets, + PackedPath &path) +{ + auto bucket_list = std::equal_range(search_space_with_buckets.begin(), + search_space_with_buckets.end(), + middle_node_id, + NodeBucket::ColumnCompare(column_idx)); + + BOOST_ASSERT_MSG(std::distance(bucket_list.first, bucket_list.second) == 1, + "The pointers are not pointing to the same element."); + + NodeID current_node_id = middle_node_id; + + while (bucket_list.first->parent_node != current_node_id && + bucket_list.first != search_space_with_buckets.end()) + { + const auto parent_node_id = bucket_list.first->parent_node; + + const auto from = DIRECTION == FORWARD_DIRECTION ? current_node_id : parent_node_id; + const auto to = DIRECTION == FORWARD_DIRECTION ? parent_node_id : current_node_id; + path.emplace_back(std::make_tuple(from, to, bucket_list.first->from_clique_arc)); + + current_node_id = parent_node_id; + bucket_list = std::equal_range(search_space_with_buckets.begin(), + search_space_with_buckets.end(), + current_node_id, + NodeBucket::ColumnCompare(column_idx)); + + BOOST_ASSERT_MSG(std::distance(bucket_list.first, bucket_list.second) == 1, + "The pointers are not pointing to the same element."); + } +} + +template +void calculateDistances(typename SearchEngineData::ManyToManyQueryHeap &query_heap, + const DataFacade &facade, + const std::vector &phantom_nodes, + const std::vector &target_indices, + const unsigned row_idx, + const std::size_t source_index, + const unsigned number_of_sources, + const unsigned number_of_targets, + const std::vector &search_space_with_buckets, + std::vector &distances_table, + const std::vector &middle_nodes_table, + SearchEngineData &engine_working_data) +{ + engine_working_data.InitializeOrClearFirstThreadLocalStorage(facade.GetNumberOfNodes(), + facade.GetMaxBorderNodeID() + 1); + + for (unsigned column_idx = 0; column_idx < number_of_targets; ++column_idx) + { + // Step 1: Get source and target phantom nodes that were used in the bucketed search + auto source_phantom_index = source_index; + auto target_phantom_index = target_indices[column_idx]; + const auto &source_phantom = phantom_nodes[source_phantom_index]; + const auto &target_phantom = phantom_nodes[target_phantom_index]; + + const auto location = DIRECTION == FORWARD_DIRECTION + ? row_idx * number_of_targets + column_idx + : row_idx + column_idx * number_of_sources; + + if (source_phantom_index == target_phantom_index) + { + distances_table[location] = 0.0; + continue; + } + + NodeID middle_node_id = middle_nodes_table[location]; + + if (middle_node_id == SPECIAL_NODEID) // takes care of one-ways + { + distances_table[location] = INVALID_EDGE_DISTANCE; + continue; + } + + // Step 2: Find path from source to middle node + PackedPath packed_path = + mld::retrievePackedPathFromSingleManyToManyHeap(query_heap, middle_node_id); + + if (DIRECTION == FORWARD_DIRECTION) + { + std::reverse(packed_path.begin(), packed_path.end()); + } + + auto &forward_heap = *engine_working_data.forward_heap_1; + auto &reverse_heap = *engine_working_data.reverse_heap_1; + EdgeWeight weight = INVALID_EDGE_WEIGHT; + std::vector unpacked_nodes_from_source; + std::vector unpacked_edges; + std::tie(weight, unpacked_nodes_from_source, unpacked_edges) = + unpackPathAndCalculateDistance(engine_working_data, + facade, + forward_heap, + reverse_heap, + DO_NOT_FORCE_LOOPS, + DO_NOT_FORCE_LOOPS, + INVALID_EDGE_WEIGHT, + packed_path, + middle_node_id, + source_phantom); + + // Step 3: Find path from middle to target node + packed_path.clear(); + retrievePackedPathFromSearchSpace( + middle_node_id, column_idx, search_space_with_buckets, packed_path); + + if (DIRECTION == REVERSE_DIRECTION) + { + std::reverse(packed_path.begin(), packed_path.end()); + } + + std::vector unpacked_nodes_to_target; + std::tie(weight, unpacked_nodes_to_target, unpacked_edges) = + unpackPathAndCalculateDistance(engine_working_data, + facade, + forward_heap, + reverse_heap, + DO_NOT_FORCE_LOOPS, + DO_NOT_FORCE_LOOPS, + INVALID_EDGE_WEIGHT, + packed_path, + middle_node_id, + target_phantom); + + if (DIRECTION == REVERSE_DIRECTION) + { + std::swap(unpacked_nodes_to_target, unpacked_nodes_from_source); + } + + // Step 4: Compute annotation value along the path nodes without the target node + auto annotation = 0.0; + + for (auto node = unpacked_nodes_from_source.begin(), + last_node = std::prev(unpacked_nodes_from_source.end()); + node != last_node; + ++node) + { + annotation += computeEdgeDistance(facade, *node); + } + + for (auto node = unpacked_nodes_to_target.begin(), + last_node = std::prev(unpacked_nodes_to_target.end()); + node != last_node; + ++node) + { + annotation += computeEdgeDistance(facade, *node); + } + + // Step 5: Get phantom node offsets and compute the annotation value + EdgeDistance source_offset = 0., target_offset = 0.; + { + // ⚠ for REVERSE_DIRECTION original source and target phantom nodes are swapped + if (DIRECTION == REVERSE_DIRECTION) + { + std::swap(source_phantom_index, target_phantom_index); + } + const auto &source_phantom = phantom_nodes[source_phantom_index]; + const auto &target_phantom = phantom_nodes[target_phantom_index]; + + NodeID source_node = unpacked_nodes_from_source.front(); + NodeID target_node = unpacked_nodes_to_target.back(); + + if (source_phantom.IsValidForwardSource() && + source_phantom.forward_segment_id.id == source_node) + { + // ............ <-- calculateEGBAnnotation returns distance from 0 + // to 3 + // -->s <-- subtract offset to start at source + // ......... <-- want this distance as result + // entry 0---1---2---3--- <-- 3 is exit node + source_offset = source_phantom.GetForwardDistance(); + } + else if (source_phantom.IsValidReverseSource() && + source_phantom.reverse_segment_id.id == source_node) + { + // ............ <-- calculateEGBAnnotation returns distance from 0 to 3 + // s<------- <-- subtract offset to start at source + // ... <-- want this distance + // entry 0---1---2---3 <-- 3 is exit node + source_offset = source_phantom.GetReverseDistance(); + } + + if (target_phantom.IsValidForwardTarget() && + target_phantom.forward_segment_id.id == target_node) + { + // ............ <-- calculateEGBAnnotation returns distance from 0 + // to 3 + // ++>t <-- add offset to get to target + // ................ <-- want this distance as result + // entry 0---1---2---3--- <-- 3 is exit node + target_offset = target_phantom.GetForwardDistance(); + } + else if (target_phantom.IsValidReverseTarget() && + target_phantom.reverse_segment_id.id == target_node) + { + // ............ <-- calculateEGBAnnotation returns distance from 0 + // to 3 + // <++t <-- add offset to get from target + // ................ <-- want this distance as result + // entry 0---1---2---3--- <-- 3 is exit node + target_offset = target_phantom.GetReverseDistance(); + } + } + + distances_table[location] = -source_offset + annotation + target_offset; + } +} + template std::pair, std::vector> manyToManySearch(SearchEngineData &engine_working_data, const DataFacade &facade, const std::vector &phantom_nodes, const std::vector &source_indices, - const std::vector &target_indices) + const std::vector &target_indices, + const bool calculate_distance) { const auto number_of_sources = source_indices.size(); const auto number_of_targets = target_indices.size(); @@ -471,7 +780,8 @@ manyToManySearch(SearchEngineData &engine_working_data, std::vector weights_table(number_of_entries, INVALID_EDGE_WEIGHT); std::vector durations_table(number_of_entries, MAXIMAL_EDGE_DURATION); - std::vector distances_table(number_of_entries, MAXIMAL_EDGE_DURATION); + std::vector distances_table; + std::vector middle_nodes_table(number_of_entries, SPECIAL_NODEID); std::vector search_space_with_buckets; @@ -479,22 +789,22 @@ manyToManySearch(SearchEngineData &engine_working_data, for (std::uint32_t column_idx = 0; column_idx < target_indices.size(); ++column_idx) { const auto index = target_indices[column_idx]; - const auto &phantom = phantom_nodes[index]; + const auto &target_phantom = phantom_nodes[index]; engine_working_data.InitializeOrClearManyToManyThreadLocalStorage( facade.GetNumberOfNodes(), facade.GetMaxBorderNodeID() + 1); auto &query_heap = *(engine_working_data.many_to_many_heap); if (DIRECTION == FORWARD_DIRECTION) - insertTargetInHeap(query_heap, phantom); + insertTargetInHeap(query_heap, target_phantom); else - insertSourceInHeap(query_heap, phantom); + insertSourceInHeap(query_heap, target_phantom); // explore search space while (!query_heap.Empty()) { backwardRoutingStep( - facade, column_idx, query_heap, search_space_with_buckets, phantom); + facade, column_idx, query_heap, search_space_with_buckets, target_phantom); } } @@ -504,18 +814,19 @@ manyToManySearch(SearchEngineData &engine_working_data, // Find shortest paths from sources to all accessible nodes for (std::uint32_t row_idx = 0; row_idx < source_indices.size(); ++row_idx) { - const auto index = source_indices[row_idx]; - const auto &phantom = phantom_nodes[index]; + const auto source_index = source_indices[row_idx]; + const auto &source_phantom = phantom_nodes[source_index]; // Clear heap and insert source nodes engine_working_data.InitializeOrClearManyToManyThreadLocalStorage( facade.GetNumberOfNodes(), facade.GetMaxBorderNodeID() + 1); + auto &query_heap = *(engine_working_data.many_to_many_heap); if (DIRECTION == FORWARD_DIRECTION) - insertSourceInHeap(query_heap, phantom); + insertSourceInHeap(query_heap, source_phantom); else - insertTargetInHeap(query_heap, phantom); + insertTargetInHeap(query_heap, source_phantom); // Explore search space while (!query_heap.Empty()) @@ -528,7 +839,25 @@ manyToManySearch(SearchEngineData &engine_working_data, search_space_with_buckets, weights_table, durations_table, - phantom); + middle_nodes_table, + source_phantom); + } + + if (calculate_distance) + { + distances_table.resize(number_of_entries, INVALID_EDGE_DISTANCE); + calculateDistances(query_heap, + facade, + phantom_nodes, + target_indices, // source_indices + row_idx, + source_index, + number_of_sources, + number_of_targets, + search_space_with_buckets, + distances_table, + middle_nodes_table, + engine_working_data); } } @@ -559,31 +888,45 @@ manyToManySearch(SearchEngineData &engine_working_data, const bool calculate_distance, const bool calculate_duration) { - (void)calculate_distance; // flag stub to use for calculating distances in matrix in mld in the - // future (void)calculate_duration; // flag stub to use for calculating distances in matrix in mld in the // future if (source_indices.size() == 1) { // TODO: check if target_indices.size() == 1 and do a bi-directional search - return mld::oneToManySearch( - engine_working_data, facade, phantom_nodes, source_indices.front(), target_indices); + return mld::oneToManySearch(engine_working_data, + facade, + phantom_nodes, + source_indices.front(), + target_indices, + calculate_distance); } if (target_indices.size() == 1) { - return mld::oneToManySearch( - engine_working_data, facade, phantom_nodes, target_indices.front(), source_indices); + return mld::oneToManySearch(engine_working_data, + facade, + phantom_nodes, + target_indices.front(), + source_indices, + calculate_distance); } if (target_indices.size() < source_indices.size()) { - return mld::manyToManySearch( - engine_working_data, facade, phantom_nodes, target_indices, source_indices); + return mld::manyToManySearch(engine_working_data, + facade, + phantom_nodes, + target_indices, + source_indices, + calculate_distance); } - return mld::manyToManySearch( - engine_working_data, facade, phantom_nodes, source_indices, target_indices); + return mld::manyToManySearch(engine_working_data, + facade, + phantom_nodes, + source_indices, + target_indices, + calculate_distance); } } // namespace routing_algorithms diff --git a/test/nodejs/table.js b/test/nodejs/table.js index b053443a7..220added1 100644 --- a/test/nodejs/table.js +++ b/test/nodejs/table.js @@ -225,8 +225,7 @@ tables.forEach(function(annotation) { annotations: [annotation.slice(0,-1)] }; osrm.table(options, function(err, response) { - if (annotation === 'durations') assert.equal(response[annotation].length, 2); - else assert.error(response, 'NotImplemented'); + assert.equal(response[annotation].length, 2); }); }); });