diff --git a/CHANGELOG.md b/CHANGELOG.md index 752164074..cdb1a3b31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,8 @@ - libOSRM now creates an own watcher thread then used in shared memory mode to listen for data updates - Tools: - Added osrm-extract-conditionals tool for checking conditional values in OSM data + - Trip Plugin + - Added a new feature that finds the optimal route given a list of waypoints, a source and a destination. This does not return a roundtrip and instead returns a one way optimal route from the fixed source to the destination points. # 5.5.1 - Changes from 5.5.0 diff --git a/docs/http.md b/docs/http.md index 0f33ae821..db1c4831a 100644 --- a/docs/http.md +++ b/docs/http.md @@ -175,7 +175,7 @@ In addition to the [general options](#general-options) the following options are |annotations |`true`, `false` (default) |Returns additional metadata for each coordinate along the route geometry. | |geometries |`polyline` (default), `polyline6`, `geojson` |Returned route geometry format (influences overview and per step) | |overview |`simplified` (default), `full`, `false` |Add overview geometry either full, simplified according to highest zoom level it could be display on, or not at all.| -|continue\_straight |`default` (default), `true`, `false` |Forces the route to keep going straight at waypoints constraining uturns there even if it would be faster. Default value depends on the profile. | +|continue\_straight |`default` (default), `true`, `false` |Forces the route to keep going straight at waypoints constraining uturns there even if it would be faster. Default value depends on the profile. | \* Please note that even if an alternative route is requested, a result cannot be guaranteed. @@ -311,27 +311,62 @@ All other properties might be undefined. ### Trip service -The trip plugin solves the Traveling Salesman Problem using a greedy heuristic (farthest-insertion algorithm). -The returned path does not have to be the fastest path, as TSP is NP-hard it is only an approximation. -Note that if the input coordinates can not be joined by a single trip (e.g. the coordinates are on several disconnected islands) -multiple trips for each connected component are returned. +The trip plugin solves the Traveling Salesman Problem using a greedy heuristic (farthest-insertion algorithm) for 10 or more waypoints and uses brute force for less than 10 waypoints. +The returned path does not have to be the fastest path. As TSP is NP-hard it only returns an approximation. +Note that all input coordinates have to be connected for the trip service to work. ```endpoint -GET /trip/v1/{profile}/{coordinates}?steps={true|false}&geometries={polyline|polyline6|geojson}&overview={simplified|full|false}&annotations={true|false}' +GET /trip/v1/{profile}/{coordinates}?roundtrip={true|false}&source{any|first}&destination{any|last}&steps={true|false}&geometries={polyline|polyline6|geojson}&overview={simplified|full|false}&annotations={true|false}' ``` In addition to the [general options](#general-options) the following options are supported for this service: |Option |Values |Description | |------------|------------------------------------------------|---------------------------------------------------------------------------| +|roundtrip |`true` (default), `false` |Return route is a roundtrip | +|source |`any` (default), `first` |Return route starts at `any` or `first` coordinate | +|destination |`any` (default), `last` |Return route ends at `any` or `last` coordinate | |steps |`true`, `false` (default) |Return route instructions for each trip | |annotations |`true`, `false` (default) |Returns additional metadata for each coordinate along the route geometry. | |geometries |`polyline` (default), `polyline6`, `geojson` |Returned route geometry format (influences overview and per step) | |overview |`simplified` (default), `full`, `false` |Add overview geometry either full, simplified according to highest zoom level it could be display on, or not at all.| -**Response** +**Fixing Start and End Points** -- `code` if the request was successful `Ok` otherwise see the service dependent and general status codes. +It is possible to explicitely set the start or end coordinate of the trip. +When source is set to `first`, the first coordinate is used as start coordinate of the trip in the output. When destination is set to `last`, the last coordinate will be used as destination of the trip in the returned output. If you specify `any`, any of the coordinates can be used as the first or last coordinate in the output. + +However, if `source=any&destination=any` the returned round-trip will still start at the first input coordinate by default. + +Currently, not all combinations of `roundtrip`, `source` and `destination` are supported. +Right now, the following combinations are possible: + +| roundtrip | source | destination | supported | +| :-- | :-- | :-- | :-- | +| true | first | last | **yes** | +| true | first | any | **yes** | +| true | any | last | **yes** | +| true | any | any | **yes** | +| false | first | last | **yes** | +| false | first | any | no | +| false | any | last | no | +| false | any | any | no | + +#### Example Requests + +```curl +# Round trip in Berlin with three stops: +curl 'http://router.project-osrm.org/trip/v1/driving/13.388860,52.517037;13.397634,52.529407;13.428555,52.523219' +``` + +```curl +# Round trip in Berlin with four stops, starting at the first stop, ending at the last: +curl 'http://router.project-osrm.org/trip/v1/driving/13.388860,52.517037;13.397634,52.529407;13.428555,52.523219;13.418555,52.523215?source=first&destination=last' +``` + +#### Response + +- `code`: if the request was successful `Ok` otherwise see the service dependent and general status codes. - `waypoints`: Array of `Waypoint` objects representing all waypoints in input order. Each `Waypoint` object has the following additional properties: - `trips_index`: Index to `trips` of the sub-trip the point was matched to. - `waypoint_index`: Index of the point in the trip. @@ -341,7 +376,8 @@ In case of error the following `code`s are supported in addition to the general | Type | Description | |-------------------|---------------------| -| `NoTrips` | No trips found. | +| `NoTrips` | No trips found because input coordinates are not connected.| +| `NotImplemented` | This request is not supported | All other properties might be undefined. diff --git a/features/step_definitions/trip.js b/features/step_definitions/trip.js index 7e52c5d22..5a216acc6 100644 --- a/features/step_definitions/trip.js +++ b/features/step_definitions/trip.js @@ -24,7 +24,6 @@ module.exports = function () { } else if (row[k]) { params[match[1]] = [row[k]]; } - got[k] = row[k]; } } @@ -35,11 +34,11 @@ module.exports = function () { } if (headers.has('status')) { - got.status = json.status.toString(); + got.status = json.code; } if (headers.has('message')) { - got.message = json.status_message; + got.message = json.message; } if (headers.has('geometry')) { @@ -59,6 +58,7 @@ module.exports = function () { var subTrips; var trip_durations; + var trip_distance; if (res.statusCode === 200) { if (headers.has('trips')) { subTrips = json.trips.filter(t => !!t).map(t => t.legs).map(tl => Array.prototype.concat.apply([], tl.map((sl, i) => { @@ -74,13 +74,19 @@ module.exports = function () { }))); trip_durations = all_durations.map( a => a.reduce(add, 0)); } + if(headers.has('distance')) { + var all_distance = json.trips.filter(t => !!t).map(t => t.legs).map(tl => Array.prototype.concat.apply([], tl.map(sl => { + return sl.distance; + }))); + trip_distance = all_distance.map( a => a.reduce(add, 0)); + } } var ok = true, encodedResult = '', extendedTarget = ''; - row.trips.split(',').forEach((sub, si) => { + if (json.trips) row.trips.split(',').forEach((sub, si) => { if (si >= subTrips.length) { ok = false; } else { @@ -105,10 +111,10 @@ module.exports = function () { got.via_points = row.via_points; } else { got.trips = encodedResult; - got.trips = extendedTarget; } got.durations = trip_durations; + got.distance = trip_distance; for (var key in row) { if (this.FuzzyMatch.match(got[key], row[key])) { @@ -144,6 +150,19 @@ module.exports = function () { waypoints.push(node); }); got = { waypoints: row.waypoints }; + + if (row.source) { + params.source = got.source = row.source; + } + + if (row.destination) { + params.destination = got.destination = row.destination; + } + + if (row.hasOwnProperty('roundtrip')) { //roundtrip is a boolean so row.roundtrip alone doesn't work as a check here + params.roundtrip = got.roundtrip = row.roundtrip; + } + this.requestTrip(waypoints, params, afterRequest); } else { throw new Error('*** no waypoints'); diff --git a/features/testbot/trip.feature b/features/testbot/trip.feature index 743224c67..c254f8d75 100644 --- a/features/testbot/trip.feature +++ b/features/testbot/trip.feature @@ -5,7 +5,25 @@ Feature: Basic trip planning Given the profile "testbot" Given a grid size of 10 meters - Scenario: Testbot - Trip planning with less than 10 nodes + Scenario: Testbot - Trip: Roundtrip with one waypoint + Given the node map + """ + a b + c d + """ + + And the ways + | nodes | + | ab | + | bc | + | cb | + | da | + + When I plan a trip I should get + | waypoints | trips | + | a | aa | + + Scenario: Testbot - Trip: Roundtrip with waypoints (less than 10) Given the node map """ a b @@ -24,7 +42,7 @@ Feature: Basic trip planning | a,b,c,d | abcda | 7.6 | | d,b,c,a | dbcad | 7.6 | - Scenario: Testbot - Trip planning with more than 10 nodes + Scenario: Testbot - Trip: Roundtrip waypoints (more than 10) Given the node map """ a b c d @@ -37,7 +55,6 @@ Feature: Basic trip planning | nodes | | ab | | bc | - | cb | | de | | ef | | fg | @@ -48,12 +65,85 @@ Feature: Basic trip planning | kl | | la | + When I plan a trip I should get + | waypoints | trips | + | a,b,c,d,e,f,g,h,i,j,k,l | alkjihgfedcba | + + Scenario: Testbot - Trip: Roundtrip FS waypoints (more than 10) + Given the node map + """ + a b c d + l e + k f + j i h g + """ + + And the ways + | nodes | + | ab | + | bc | + | de | + | ef | + | fg | + | gh | + | hi | + | ij | + | jk | + | kl | + | la | + + When I plan a trip I should get + | waypoints | source | trips | + | a,b,c,d,e,f,g,h,i,j,k,l | first | alkjihgfedcba | + + Scenario: Testbot - Trip: Roundtrip FE waypoints (more than 10) + Given the query options + | source | last | + Given the node map + """ + a b c d + l e + k f + j i h g + """ + + And the ways + | nodes | + | ab | + | bc | + | de | + | ef | + | fg | + | gh | + | hi | + | ij | + | jk | + | kl | + | la | When I plan a trip I should get | waypoints | trips | - | a,b,c,d,e,f,g,h,i,j,k,l | cbalkjihgfedc | + | a,b,c,d,e,f,g,h,i,j,k,l | lkjihgfedcbal | - Scenario: Testbot - Trip planning with multiple scc + Scenario: Testbot - Trip: Unroutable roundtrip with waypoints (less than 10) + Given the node map + """ + a b + + d c + """ + + And the ways + | nodes | + | ab | + | dc | + + When I plan a trip I should get + | waypoints | status | message | + | a,b,c,d | NoTrips | No trip visiting all destinations possible. | + + + Scenario: Testbot - Trip: Unroutable roundtrip with waypoints (more than 10) Given the node map """ a b c d @@ -62,7 +152,7 @@ Feature: Basic trip planning j i h g q m n - p o + p o """ And the ways @@ -85,13 +175,106 @@ Feature: Basic trip planning | pq | | qm | + When I plan a trip I should get + | waypoints | status | message | + | a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p | NoTrips | No trip visiting all destinations possible. | + +# Test TFSE + Scenario: Testbot - Trip: TFSE with errors + Given the node map + """ + a b + + d c + """ + + And the ways + | nodes | + | ab | + | dc | + + When I plan a trip I should get + | waypoints | source | destination | roundtrip | status | message | + | a,b,c,d | first | last | false | NoTrips | No trip visiting all destinations possible. | + + Scenario: Testbot - Trip: FSE with waypoints (less than 10) + Given the node map + """ + a b + + c + e d + """ + + And the ways + | nodes | + | ab | + | ac | + | ad | + | ae | + | bc | + | bd | + | be | + | cd | + | ce | + | de | When I plan a trip I should get - | waypoints | trips | - | a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p | defghijklabcd,mnopm | + | waypoints | source | destination |roundtrip | trips | durations | distance | + | a,b,d,e,c | first | last | false | abedc | 8.200000000000001 | 81.6 | - # Test single node in each component #1850 - Scenario: Testbot - Trip planning with less than 10 nodes + + Scenario: Testbot - Trip: FSE with waypoints (more than 10) + Given the node map + """ + a b c d e f g h i j k + """ + + And the ways + | nodes | + | ab | + | bc | + | cd | + | de | + | ef | + | fg | + | gh | + | hi | + | ij | + | jk | + + When I plan a trip I should get + | waypoints | source | destination | roundtrip | trips | durations | distance | + | a,b,c,d,e,h,i,j,k,g,f | first | last | false | abcdeghijkf | 15 | 149.8 | + + Scenario: Testbot - Trip: FSE roundtrip with waypoints (less than 10) + Given the node map + """ + a b + + c + e d + """ + + And the ways + | nodes | + | ab | + | ac | + | ad | + | ae | + | bc | + | bd | + | be | + | cd | + | ce | + | de | + + When I plan a trip I should get + | waypoints | source | destination | roundtrip | trips | + | a,b,d,e,c | first | last | true | abedca | + + + Scenario: Testbot - Trip: midway points in isoldated roads should return no trips Given the node map """ a 1 b @@ -105,10 +288,10 @@ Feature: Basic trip planning | cd | When I plan a trip I should get - | waypoints | trips | - | 1,2 | | + | waypoints | status | message | + | 1,2 | NoTrips | No trip visiting all destinations possible. | - Scenario: Testbot - Repeated Coordinate + Scenario: Testbot - Trip: Repeated Coordinate Given the node map """ a b @@ -123,7 +306,7 @@ Feature: Basic trip planning | a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a | | - Scenario: Testbot - Trip with geometry details of geojson + Scenario: Testbot - Trip: geometry details of geojson Given the query options | geometries | geojson | @@ -145,7 +328,7 @@ Feature: Basic trip planning | a,b,c,d | abcda | 7.6 | 1,1,1.00009,1,1,0.99991,1.00009,1,1,1,1.00009,0.99991,1,1 | | d,b,c,a | dbcad | 7.6 | 1.00009,0.99991,1,1,1.00009,1,1,0.99991,1.00009,1,1,1,1.00009,0.99991 | - Scenario: Testbot - Trip with geometry details of polyline + Scenario: Testbot - Trip: geometry details of polyline Given the query options | geometries | polyline | @@ -167,7 +350,7 @@ Feature: Basic trip planning | a,b,c,d | abcda | 7.6 | 1,1,1,1.00009,0.99991,1,1,1.00009,1,1,0.99991,1.00009,1,1 | | d,b,c,a | dbcad | 7.6 | 0.99991,1.00009,1,1,1,1.00009,0.99991,1,1,1.00009,1,1,0.99991,1.00009 | - Scenario: Testbot - Trip with geometry details of polyline6 + Scenario: Testbot - Trip: geometry details of polyline6 Given the query options | geometries | polyline6 | @@ -187,4 +370,4 @@ Feature: Basic trip planning When I plan a trip I should get | waypoints | trips | durations | geometry | | a,b,c,d | abcda | 7.6 | 1,1,1,1.00009,0.99991,1,1,1.00009,1,1,0.99991,1.00009,1,1 | - | d,b,c,a | dbcad | 7.6 | 0.99991,1.00009,1,1,1,1.00009,0.99991,1,1,1.00009,1,1,0.99991,1.00009 | + | d,b,c,a | dbcad | 7.6 | 0.99991,1.00009,1,1,1,1.00009,0.99991,1,1,1.00009,1,1,0.99991,1.00009 | \ No newline at end of file diff --git a/include/engine/api/trip_parameters.hpp b/include/engine/api/trip_parameters.hpp index 53c9f4274..0164dbb02 100644 --- a/include/engine/api/trip_parameters.hpp +++ b/include/engine/api/trip_parameters.hpp @@ -30,6 +30,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "engine/api/route_parameters.hpp" +#include #include namespace osrm @@ -47,7 +48,33 @@ namespace api */ struct TripParameters : public RouteParameters { - // bool IsValid() const; Falls back to base class + TripParameters() = default; + enum class SourceType + { + Any, + First + }; + enum class DestinationType + { + Any, + Last + }; + + template + TripParameters(SourceType source_, + DestinationType destination_, + bool roundtrip_, + Args &&... args_) + : RouteParameters{std::forward(args_)...}, source{source_}, destination{destination_}, + roundtrip{roundtrip_} + { + } + + SourceType source = SourceType::Any; + DestinationType destination = DestinationType::Any; + bool roundtrip = true; + + bool IsValid() const { return RouteParameters::IsValid(); } }; } } diff --git a/include/engine/plugins/trip.hpp b/include/engine/plugins/trip.hpp index 8d726c2ea..d189b2d47 100644 --- a/include/engine/plugins/trip.hpp +++ b/include/engine/plugins/trip.hpp @@ -36,7 +36,8 @@ class TripPlugin final : public BasePlugin InternalRouteResult ComputeRoute(const std::shared_ptr facade, const std::vector &phantom_node_list, - const std::vector &trip) const; + const std::vector &trip, + const bool roundtrip) const; public: explicit TripPlugin(const int max_locations_trip_) diff --git a/include/engine/trip/trip_brute_force.hpp b/include/engine/trip/trip_brute_force.hpp index 650f2a667..cd7cece31 100644 --- a/include/engine/trip/trip_brute_force.hpp +++ b/include/engine/trip/trip_brute_force.hpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include @@ -25,62 +26,76 @@ namespace trip EdgeWeight ReturnDistance(const util::DistTableWrapper &dist_table, const std::vector &location_order, const EdgeWeight min_route_dist, - const std::size_t component_size) + const std::size_t number_of_locations) { EdgeWeight route_dist = 0; - std::size_t i = 0; - while (i < location_order.size() && (route_dist < min_route_dist)) + std::size_t current_index = 0; + while (current_index < location_order.size() && (route_dist < min_route_dist)) { - route_dist += dist_table(location_order[i], location_order[(i + 1) % component_size]); - BOOST_ASSERT_MSG(dist_table(location_order[i], location_order[(i + 1) % component_size]) != + + std::size_t next_index = (current_index + 1) % number_of_locations; + auto edge_weight = dist_table(location_order[current_index], location_order[next_index]); + + // If the edge_weight is very large (INVALID_EDGE_WEIGHT) then the algorithm will not choose + // this edge in final minimal path. So instead of computing all the permutations after this + // large edge, discard this edge right here and don't consider the path after this edge. + if (edge_weight == INVALID_EDGE_WEIGHT) + { + return INVALID_EDGE_WEIGHT; + } + else + { + route_dist += edge_weight; + } + + // This boost assert should not be reached if TFSE table + BOOST_ASSERT_MSG(dist_table(location_order[current_index], location_order[next_index]) != INVALID_EDGE_WEIGHT, "invalid route found"); - ++i; + ++current_index; } return route_dist; } // computes the route by computing all permutations and selecting the shortest -template -std::vector BruteForceTrip(const NodeIDIterator start, - const NodeIDIterator end, - const std::size_t number_of_locations, +std::vector BruteForceTrip(const std::size_t number_of_locations, const util::DistTableWrapper &dist_table) { - (void)number_of_locations; // unused - - const auto component_size = std::distance(start, end); - - std::vector perm(start, end); - std::vector route = perm; + // set initial order in which nodes are visited to 0, 1, 2, 3, ... + std::vector node_order(number_of_locations); + std::iota(std::begin(node_order), std::end(node_order), 0); + std::vector route = node_order; EdgeWeight min_route_dist = INVALID_EDGE_WEIGHT; // check length of all possible permutation of the component ids - - BOOST_ASSERT_MSG(perm.size() > 0, "no permutation given"); - BOOST_ASSERT_MSG(*(std::max_element(std::begin(perm), std::end(perm))) < number_of_locations, + BOOST_ASSERT_MSG(node_order.size() > 0, "no order permutation given"); + BOOST_ASSERT_MSG(*(std::max_element(std::begin(node_order), std::end(node_order))) < + number_of_locations, + "invalid node id"); + BOOST_ASSERT_MSG(*(std::min_element(std::begin(node_order), std::end(node_order))) >= 0, "invalid node id"); - BOOST_ASSERT_MSG(*(std::min_element(std::begin(perm), std::end(perm))) >= 0, "invalid node id"); do { - const auto new_distance = ReturnDistance(dist_table, perm, min_route_dist, component_size); + const auto new_distance = + ReturnDistance(dist_table, node_order, min_route_dist, number_of_locations); // we can use `<` instead of `<=` here, since all distances are `!=` INVALID_EDGE_WEIGHT // In case we really sum up to invalid edge weight for all permutations, keeping the very // first one is fine too. if (new_distance < min_route_dist) { min_route_dist = new_distance; - route = perm; + route = node_order; } - } while (std::next_permutation(std::begin(perm), std::end(perm))); + } while (std::next_permutation(std::begin(node_order), std::end(node_order))); return route; } -} -} -} + +} // namespace trip +} // namespace engine +} // namespace osrm #endif // TRIP_BRUTE_FORCE_HPP diff --git a/include/engine/trip/trip_farthest_insertion.hpp b/include/engine/trip/trip_farthest_insertion.hpp index 1089c0301..cd95d75c7 100644 --- a/include/engine/trip/trip_farthest_insertion.hpp +++ b/include/engine/trip/trip_farthest_insertion.hpp @@ -50,8 +50,11 @@ GetShortestRoundTrip(const NodeID new_loc, const auto dist_to = dist_table(new_loc, *to_node); const auto trip_dist = dist_from + dist_to - dist_table(*from_node, *to_node); - BOOST_ASSERT_MSG(dist_from != INVALID_EDGE_WEIGHT, "distance has invalid edge weight"); - BOOST_ASSERT_MSG(dist_to != INVALID_EDGE_WEIGHT, "distance has invalid edge weight"); + // If the edge_weight is very large (INVALID_EDGE_WEIGHT) then the algorithm will not choose + // this edge in final minimal path. So instead of computing all the permutations after this + // large edge, discard this edge right here and don't consider the path after this edge. + if (dist_from == INVALID_EDGE_WEIGHT || dist_to == INVALID_EDGE_WEIGHT) + continue; // This is not neccessarily true: // Lets say you have an edge (u, v) with duration 100. If you place a coordinate exactly in // the middle of the segment yielding (u, v'), the adjusted duration will be 100 * 0.5 = 50. @@ -72,18 +75,14 @@ GetShortestRoundTrip(const NodeID new_loc, return std::make_pair(min_trip_distance, next_insert_point_candidate); } -template // given two initial start nodes, find a roundtrip route using the farthest insertion algorithm std::vector FindRoute(const std::size_t &number_of_locations, - const std::size_t &component_size, - const NodeIDIterator &start, - const NodeIDIterator &end, const util::DistTableWrapper &dist_table, const NodeID &start1, const NodeID &start2) { - BOOST_ASSERT_MSG(number_of_locations >= component_size, - "component size bigger than total number of locations"); + BOOST_ASSERT_MSG(number_of_locations * number_of_locations == dist_table.size(), + "number_of_locations and dist_table size do not match"); std::vector route; route.reserve(number_of_locations); @@ -96,22 +95,21 @@ std::vector FindRoute(const std::size_t &number_of_locations, route.push_back(start1); route.push_back(start2); - // add all other nodes missing (two nodes are already in the initial start trip) - for (std::size_t j = 2; j < component_size; ++j) + // two nodes are already in the initial start trip, so we need to add all other nodes + for (std::size_t added_nodes = 2; added_nodes < number_of_locations; ++added_nodes) { - auto farthest_distance = std::numeric_limits::min(); auto next_node = -1; NodeIDIter next_insert_point; - // find unvisited loc i that is the farthest away from all other visited locs - for (auto i = start; i != end; ++i) + // find unvisited node that is the farthest away from all other visited locs + for (std::size_t id = 0; id < number_of_locations; ++id) { // find the shortest distance from i to all visited nodes - if (!visited[*i]) + if (!visited[id]) { const auto insert_candidate = - GetShortestRoundTrip(*i, dist_table, number_of_locations, route); + GetShortestRoundTrip(id, dist_table, number_of_locations, route); BOOST_ASSERT_MSG(insert_candidate.first != INVALID_EDGE_WEIGHT, "shortest round trip is invalid"); @@ -121,7 +119,7 @@ std::vector FindRoute(const std::size_t &number_of_locations, if (insert_candidate.first > farthest_distance) { farthest_distance = insert_candidate.first; - next_node = *i; + next_node = id; next_insert_point = insert_candidate.second; } } @@ -136,10 +134,7 @@ std::vector FindRoute(const std::size_t &number_of_locations, return route; } -template -std::vector FarthestInsertionTrip(const NodeIDIterator &start, - const NodeIDIterator &end, - const std::size_t number_of_locations, +std::vector FarthestInsertionTrip(const std::size_t number_of_locations, const util::DistTableWrapper &dist_table) { ////////////////////////////////////////////////////////////////////////////////////////////////// @@ -158,57 +153,29 @@ std::vector FarthestInsertionTrip(const NodeIDIterator &start, // Guard against dist_table being empty therefore max_element returning the end iterator. BOOST_ASSERT(dist_table.size() > 0); - const auto component_size = std::distance(start, end); - BOOST_ASSERT(component_size >= 0); + BOOST_ASSERT_MSG(number_of_locations * number_of_locations == dist_table.size(), + "number_of_locations and dist_table size do not match"); - auto max_from = -1; - auto max_to = -1; - - if (static_cast(component_size) == number_of_locations) - { - // find the pair of location with the biggest distance and make the pair the initial start - // trip. Skipping over the very first element (0,0), we make sure not to end up with the - // same start/end in the special case where all entries are the same. - const auto index = - std::distance(std::begin(dist_table), - std::max_element(std::begin(dist_table) + 1, std::end(dist_table))); - max_from = index / number_of_locations; - max_to = index % number_of_locations; - } - else - { - auto max_dist = std::numeric_limits::min(); - - for (auto x = start; x != end; ++x) - { - for (auto y = start; y != end; ++y) - { - // don't repeat coordinates - if (*x == *y) - continue; - - const auto xy_dist = dist_table(*x, *y); - // SCC decomposition done correctly? - BOOST_ASSERT(xy_dist != INVALID_EDGE_WEIGHT); - - if (xy_dist >= max_dist) - { - max_dist = xy_dist; - max_from = *x; - max_to = *y; - } - } - } - } + // find the pair of location with the biggest distance and make the pair the initial start + // trip. Skipping over the very first element (0,0), we make sure not to end up with the + // same start/end in the special case where all entries are the same. + const auto index_of_farthest_distance = std::distance( + std::begin(dist_table), std::max_element(std::begin(dist_table) + 1, std::end(dist_table))); + // distance table is a nxn matrix with the distance(u,v) in column u and row v + // but the distance table is stored in an 1D array of distances + // to get the actual (u,v), get the row by dividing and the column by computing modulo n + NodeID max_from = index_of_farthest_distance / number_of_locations; + NodeID max_to = index_of_farthest_distance % number_of_locations; BOOST_ASSERT(max_from >= 0); BOOST_ASSERT(max_to >= 0); BOOST_ASSERT_MSG(static_cast(max_from) < number_of_locations, "start node"); BOOST_ASSERT_MSG(static_cast(max_to) < number_of_locations, "start node"); - return FindRoute(number_of_locations, component_size, start, end, dist_table, max_from, max_to); -} -} -} + return FindRoute(number_of_locations, dist_table, max_from, max_to); } +} // namespace trip +} // namespace engine +} // namespace osrm + #endif // TRIP_FARTHEST_INSERTION_HPP diff --git a/include/server/api/trip_parameter_grammar.hpp b/include/server/api/trip_parameter_grammar.hpp index 30809a398..76105da07 100644 --- a/include/server/api/trip_parameter_grammar.hpp +++ b/include/server/api/trip_parameter_grammar.hpp @@ -4,6 +4,7 @@ #include "server/api/route_parameters_grammar.hpp" #include "engine/api/trip_parameters.hpp" +#include #include namespace osrm @@ -15,6 +16,7 @@ namespace api namespace { +namespace ph = boost::phoenix; namespace qi = boost::spirit::qi; } @@ -26,12 +28,37 @@ struct TripParametersGrammar final : public RouteParametersGrammar + source_type[ph::bind(&engine::api::TripParameters::source, qi::_r1) = qi::_1]; + + destination_rule = + qi::lit("destination=") > + destination_type[ph::bind(&engine::api::TripParameters::destination, qi::_r1) = qi::_1]; + root_rule = BaseGrammar::query_rule(qi::_r1) > -qi::lit(".json") > - -('?' > (BaseGrammar::base_rule(qi::_r1)) % '&'); + -('?' > (roundtrip_rule(qi::_r1) | source_rule(qi::_r1) | + destination_rule(qi::_r1) | BaseGrammar::base_rule(qi::_r1)) % + '&'); } private: + qi::rule source_rule; + qi::rule destination_rule; + qi::rule roundtrip_rule; qi::rule root_rule; + + qi::symbols source_type; + qi::symbols destination_type; }; } } diff --git a/include/util/dist_table_wrapper.hpp b/include/util/dist_table_wrapper.hpp index 143bd56ac..56c750244 100644 --- a/include/util/dist_table_wrapper.hpp +++ b/include/util/dist_table_wrapper.hpp @@ -46,6 +46,18 @@ template class DistTableWrapper return table_[index]; } + void SetValue(NodeID from, NodeID to, EdgeWeight value) + { + BOOST_ASSERT_MSG(from < number_of_nodes_, "from ID is out of bound"); + BOOST_ASSERT_MSG(to < number_of_nodes_, "to ID is out of bound"); + + const auto index = from * number_of_nodes_ + to; + + BOOST_ASSERT_MSG(index < table_.size(), "index is out of bound"); + + table_[index] = value; + } + ConstIterator begin() const { return std::begin(table_); } Iterator begin() { return std::begin(table_); } diff --git a/include/util/matrix_graph_wrapper.hpp b/include/util/matrix_graph_wrapper.hpp index 3dc97972e..7889a6301 100644 --- a/include/util/matrix_graph_wrapper.hpp +++ b/include/util/matrix_graph_wrapper.hpp @@ -13,8 +13,7 @@ namespace util { // This Wrapper provides all methods that are needed for extractor::TarjanSCC, when the graph is -// given in a -// matrix representation (e.g. as output from a distance table call) +// given in a matrix representation (e.g. as output from a distance table call) template class MatrixGraphWrapper { diff --git a/include/util/typedefs.hpp b/include/util/typedefs.hpp index 87c0f2d3d..b68e9c5cb 100644 --- a/include/util/typedefs.hpp +++ b/include/util/typedefs.hpp @@ -60,6 +60,8 @@ using NameID = std::uint32_t; using EdgeWeight = std::int32_t; using TurnPenalty = std::int16_t; // turn penalty in 100ms units +static const std::size_t INVALID_INDEX = std::numeric_limits::max(); + using LaneID = std::uint8_t; static const LaneID INVALID_LANEID = std::numeric_limits::max(); using LaneDataID = std::uint16_t; diff --git a/src/engine/plugins/trip.cpp b/src/engine/plugins/trip.cpp index 73f4980de..746fbab71 100644 --- a/src/engine/plugins/trip.cpp +++ b/src/engine/plugins/trip.cpp @@ -1,7 +1,5 @@ #include "engine/plugins/trip.hpp" -#include "extractor/tarjan_scc.hpp" - #include "engine/api/trip_api.hpp" #include "engine/api/trip_parameters.hpp" #include "engine/trip/trip_brute_force.hpp" @@ -16,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -28,129 +27,150 @@ namespace engine namespace plugins { -// Object to hold all strongly connected components (scc) of a graph -// to access all graphs with component ID i, get the iterators by: -// auto start = std::begin(scc_component.component) + scc_component.range[i]; -// auto end = std::begin(scc_component.component) + scc_component.range[i+1]; -struct SCC_Component +bool IsStronglyConnectedComponent(const util::DistTableWrapper &result_table) { - // in_component: all NodeIDs sorted by component ID - // in_range: index where a new component starts - // - // example: NodeID 0, 1, 2, 4, 5 are in component 0 - // NodeID 3, 6, 7, 8 are in component 1 - // => in_component = [0, 1, 2, 4, 5, 3, 6, 7, 8] - // => in_range = [0, 5] - SCC_Component(std::vector in_component_nodes, std::vector in_range) - : component(std::move(in_component_nodes)), range(std::move(in_range)) - { - BOOST_ASSERT_MSG(component.size() > 0, "there's no scc component"); - BOOST_ASSERT_MSG(*std::max_element(range.begin(), range.end()) == component.size(), - "scc component ranges are out of bound"); - BOOST_ASSERT_MSG(*std::min_element(range.begin(), range.end()) == 0, - "invalid scc component range"); - BOOST_ASSERT_MSG(std::is_sorted(std::begin(range), std::end(range)), - "invalid component ranges"); - } - - std::size_t GetNumberOfComponents() const - { - BOOST_ASSERT_MSG(range.size() > 0, "there's no range"); - return range.size() - 1; - } - - const std::vector component; - std::vector range; -}; - -// takes the number of locations and its duration matrix, -// identifies and splits the graph in its strongly connected components (scc) -// and returns an SCC_Component -SCC_Component SplitUnaccessibleLocations(const std::size_t number_of_locations, - const util::DistTableWrapper &result_table) -{ - - if (std::find(std::begin(result_table), std::end(result_table), INVALID_EDGE_WEIGHT) == - std::end(result_table)) - { - // whole graph is one scc - std::vector location_ids(number_of_locations); - std::iota(std::begin(location_ids), std::end(location_ids), 0); - std::vector range = {0, location_ids.size()}; - return SCC_Component(std::move(location_ids), std::move(range)); - } - - // Run TarjanSCC - auto wrapper = std::make_shared>(result_table.GetTable(), - number_of_locations); - auto scc = extractor::TarjanSCC>(wrapper); - scc.Run(); - - const auto number_of_components = scc.GetNumberOfComponents(); - - std::vector range_insertion; - std::vector range; - range_insertion.reserve(number_of_components); - range.reserve(number_of_components); - - std::vector components(number_of_locations, 0); - - std::size_t prefix = 0; - for (std::size_t j = 0; j < number_of_components; ++j) - { - range_insertion.push_back(prefix); - range.push_back(prefix); - prefix += scc.GetComponentSize(j); - } - // senitel - range.push_back(components.size()); - - for (std::size_t i = 0; i < number_of_locations; ++i) - { - components[range_insertion[scc.GetComponentID(i)]] = i; - ++range_insertion[scc.GetComponentID(i)]; - } - - return SCC_Component(std::move(components), std::move(range)); + return std::find(std::begin(result_table), std::end(result_table), INVALID_EDGE_WEIGHT) == + std::end(result_table); } +bool IsSupportedParameterCombination(const bool fixed_start, + const bool fixed_end, + const bool roundtrip) +{ + if (fixed_start && fixed_end && !roundtrip) + { + return true; + } + else if (roundtrip) + { + return true; + } + else + { + return false; + } +} + +// given the node order in which to visit, compute the actual route (with geometry, travel time and +// so on) and return the result InternalRouteResult TripPlugin::ComputeRoute(const std::shared_ptr facade, const std::vector &snapped_phantoms, - const std::vector &trip) const + const std::vector &trip, + const bool roundtrip) const { InternalRouteResult min_route; - // given he final trip, compute total duration and return the route and location permutation + // given the final trip, compute total duration and return the route and location permutation PhantomNodes viapoint; - const auto start = std::begin(trip); - const auto end = std::end(trip); + // computes a roundtrip from the nodes in trip - for (auto it = start; it != end; ++it) + for (auto node = trip.begin(); node < trip.end() - 1; ++node) { - const auto from_node = *it; - // if from_node is the last node, compute the route from the last to the first location - const auto to_node = std::next(it) != end ? *std::next(it) : *start; + const auto from_node = *node; + const auto to_node = *std::next(node); viapoint = PhantomNodes{snapped_phantoms[from_node], snapped_phantoms[to_node]}; min_route.segment_end_coordinates.emplace_back(viapoint); } - BOOST_ASSERT(min_route.segment_end_coordinates.size() == trip.size()); + + // return back to the first node if it is a round trip + if (roundtrip) + { + viapoint = PhantomNodes{snapped_phantoms[trip.back()], snapped_phantoms[trip.front()]}; + min_route.segment_end_coordinates.emplace_back(viapoint); + // trip comes out to be something like 0 1 4 3 2 0 + BOOST_ASSERT(min_route.segment_end_coordinates.size() == trip.size()); + } + else + { + // trip comes out to be something like 0 1 4 3 2, so the sizes don't match + BOOST_ASSERT(min_route.segment_end_coordinates.size() == trip.size() - 1); + } shortest_path(facade, min_route.segment_end_coordinates, {false}, min_route); - BOOST_ASSERT_MSG(min_route.shortest_path_length < INVALID_EDGE_WEIGHT, "unroutable route"); return min_route; } +void ManipulateTableForFSE(const std::size_t source_id, + const std::size_t destination_id, + util::DistTableWrapper &result_table) +{ + // ****************** Change Table ************************* + // The following code manipulates the table and produces the new table for + // Trip with Fixed Start and End (TFSE). In the example the source is a + // and destination is c. The new table forces the roundtrip to start at + // source and end at destination by virtually squashing them together. + // This way the brute force and the farthest insertion algorithms don't + // have to be modified, and instead we can just pass a modified table to + // return a non-roundtrip "optimal" route from a start node to an end node. + + // Original Table // New Table + // a b c d e // a b c d e + // a 0 15 36 34 30 // a 0 15 10000 34 30 + // b 15 0 25 30 34 // b 10000 0 25 30 34 + // c 36 25 0 18 32 // c 0 10000 0 10000 10000 + // d 34 30 18 0 15 // d 10000 30 18 0 15 + // e 30 34 32 15 0 // e 10000 34 32 15 0 + + // change parameters.source column + // set any node to source to impossibly high numbers so it will never + // try to use any node->source in the middle of the "optimal path" + for (std::size_t i = 0; i < result_table.GetNumberOfNodes(); i++) + { + if (i == source_id) + continue; + result_table.SetValue(i, source_id, INVALID_EDGE_WEIGHT); + } + + // change parameters.destination row + // set destination to anywhere else to impossibly high numbers so it will + // never try to use destination->any node in the middle of the "optimal path" + for (std::size_t i = 0; i < result_table.GetNumberOfNodes(); i++) + { + if (i == destination_id) + continue; + result_table.SetValue(destination_id, i, INVALID_EDGE_WEIGHT); + } + + // set destination->source to zero so rountrip treats source and + // destination as one location + result_table.SetValue(destination_id, source_id, 0); + + // set source->destination as very high number so algorithm is forced + // to find another path to get to destination + result_table.SetValue(source_id, destination_id, INVALID_EDGE_WEIGHT); + + //********* End of changes to table ************************************* +} + Status TripPlugin::HandleRequest(const std::shared_ptr facade, const api::TripParameters ¶meters, util::json::Object &json_result) const { BOOST_ASSERT(parameters.IsValid()); + const auto number_of_locations = parameters.coordinates.size(); + + std::size_t source_id = INVALID_INDEX; + std::size_t destination_id = INVALID_INDEX; + if (parameters.source == api::TripParameters::SourceType::First) + { + source_id = 0; + } + if (parameters.destination == api::TripParameters::DestinationType::Last) + { + BOOST_ASSERT(number_of_locations > 0); + destination_id = number_of_locations - 1; + } + bool fixed_start = (source_id == 0); + bool fixed_end = (destination_id == number_of_locations - 1); + if (!IsSupportedParameterCombination(fixed_start, fixed_end, parameters.roundtrip)) + { + return Error("NotImplemented", "This request is not supported", json_result); + } // enforce maximum number of locations for performance reasons - if (max_locations_trip > 0 && - static_cast(parameters.coordinates.size()) > max_locations_trip) + if (max_locations_trip > 0 && static_cast(number_of_locations) > max_locations_trip) { return Error("TooBig", "Too many trip coordinates", json_result); } @@ -161,21 +181,27 @@ Status TripPlugin::HandleRequest(const std::shared_ptr= parameters.coordinates.size() || + destination_id >= parameters.coordinates.size())) + { + return Error("InvalidValue", "Invalid source or destination value.", json_result); + } auto snapped_phantoms = SnapPhantomNodes(phantom_node_pairs); - const auto number_of_locations = snapped_phantoms.size(); + BOOST_ASSERT(snapped_phantoms.size() == number_of_locations); // compute the duration table of all phantom nodes - const auto result_table = util::DistTableWrapper( + auto result_table = util::DistTableWrapper( duration_table(facade, snapped_phantoms, {}, {}), number_of_locations); if (result_table.size() == 0) @@ -187,56 +213,49 @@ Status TripPlugin::HandleRequest(const std::shared_ptr> trips; - trips.reserve(scc.GetNumberOfComponents()); - // run Trip computation for every SCC - for (std::size_t k = 0; k < scc.GetNumberOfComponents(); ++k) + if (!IsStronglyConnectedComponent(result_table)) { - const auto component_size = scc.range[k + 1] - scc.range[k]; - - BOOST_ASSERT_MSG(component_size > 0, "invalid component size"); - - std::vector scc_route; - auto route_begin = std::begin(scc.component) + scc.range[k]; - auto route_end = std::begin(scc.component) + scc.range[k + 1]; - - if (component_size > 1) - { - - if (component_size < BF_MAX_FEASABLE) - { - scc_route = - trip::BruteForceTrip(route_begin, route_end, number_of_locations, result_table); - } - else - { - scc_route = trip::FarthestInsertionTrip( - route_begin, route_end, number_of_locations, result_table); - } - } - else - { - scc_route = std::vector(route_begin, route_end); - } - - trips.push_back(std::move(scc_route)); - } - if (trips.empty()) - { - return Error("NoTrips", "Cannot find trips", json_result); + return Error("NoTrips", "No trip visiting all destinations possible.", json_result); } - // compute all round trip routes - std::vector routes; - routes.reserve(trips.size()); - for (const auto &trip : trips) + if (fixed_start && fixed_end) { - routes.push_back(ComputeRoute(facade, snapped_phantoms, trip)); + ManipulateTableForFSE(source_id, destination_id, result_table); } + std::vector trip; + trip.reserve(number_of_locations); + // get an optimized order in which the destinations should be visited + if (number_of_locations < BF_MAX_FEASABLE) + { + trip = trip::BruteForceTrip(number_of_locations, result_table); + } + else + { + trip = trip::FarthestInsertionTrip(number_of_locations, result_table); + } + + // rotate result such that roundtrip starts at node with index 0 + // thist first if covers scenarios: !fixed_end || fixed_start || (fixed_start && fixed_end) + if (!fixed_end || fixed_start) + { + auto desired_start_index = std::find(std::begin(trip), std::end(trip), 0); + BOOST_ASSERT(desired_start_index != std::end(trip)); + std::rotate(std::begin(trip), desired_start_index, std::end(trip)); + } + else if (fixed_end && !fixed_start && parameters.roundtrip) + { + auto desired_start_index = std::find(std::begin(trip), std::end(trip), destination_id); + BOOST_ASSERT(desired_start_index != std::end(trip)); + std::rotate(std::begin(trip), desired_start_index, std::end(trip)); + } + + // get the route when visiting all destinations in optimized order + InternalRouteResult route = ComputeRoute(facade, snapped_phantoms, trip, parameters.roundtrip); + + // get api response + const std::vector> trips = {trip}; + const std::vector routes = {route}; api::TripAPI trip_api{*facade, parameters}; trip_api.MakeResponse(trips, routes, snapped_phantoms, json_result); @@ -244,4 +263,4 @@ Status TripPlugin::HandleRequest(const std::shared_ptr().value; + BOOST_CHECK_EQUAL(code, "Ok"); + + const auto &waypoints = result.values.at("waypoints").get().values; + BOOST_CHECK_EQUAL(waypoints.size(), params.coordinates.size()); + + const auto &trips = result.values.at("trips").get().values; + BOOST_CHECK_EQUAL(trips.size(), 1); + + for (const auto &waypoint : waypoints) + { + const auto &waypoint_object = waypoint.get(); + + const auto location = waypoint_object.values.at("location").get().values; + const auto longitude = location[0].get().value; + const auto latitude = location[1].get().value; + BOOST_CHECK(longitude >= -180. && longitude <= 180.); + BOOST_CHECK(latitude >= -90. && latitude <= 90.); + + const auto trip = waypoint_object.values.at("trips_index").get().value; + const auto pos = waypoint_object.values.at("waypoint_index").get().value; + BOOST_CHECK(trip >= 0 && trip < trips.size()); + BOOST_CHECK(pos >= 0 && pos < waypoints.size()); + } +} + +BOOST_AUTO_TEST_CASE(test_tfse_2) +{ + const auto args = get_args(); + auto osrm = getOSRM(args.at(0)); + + using namespace osrm; + + const auto locations = get_locations_in_big_component(); + + TripParameters params; + params.coordinates.push_back(locations.at(0)); + params.coordinates.push_back(locations.at(2)); + params.coordinates.push_back(locations.at(1)); + + params.source = TripParameters::SourceType::First; + params.destination = TripParameters::DestinationType::Last; + params.roundtrip = false; + + json::Object result; + const auto rc = osrm.Trip(params, result); + BOOST_CHECK(rc == Status::Ok); + + const auto code = result.values.at("code").get().value; + BOOST_CHECK_EQUAL(code, "Ok"); + + const auto &waypoints = result.values.at("waypoints").get().values; + BOOST_CHECK_EQUAL(waypoints.size(), params.coordinates.size()); + + const auto &trips = result.values.at("trips").get().values; + BOOST_CHECK_EQUAL(trips.size(), 1); + + for (const auto &waypoint : waypoints) + { + const auto &waypoint_object = waypoint.get(); + + const auto location = waypoint_object.values.at("location").get().values; + const auto longitude = location[0].get().value; + const auto latitude = location[1].get().value; + BOOST_CHECK(longitude >= -180. && longitude <= 180.); + BOOST_CHECK(latitude >= -90. && latitude <= 90.); + + const auto trip = waypoint_object.values.at("trips_index").get().value; + const auto pos = waypoint_object.values.at("waypoint_index").get().value; + BOOST_CHECK(trip >= 0 && trip < trips.size()); + BOOST_CHECK(pos >= 0 && pos < waypoints.size()); + } +} + +void ResetParams(const Locations &locations, osrm::TripParameters ¶ms) +{ + params = osrm::TripParameters(); + params.coordinates.push_back(locations.at(0)); + params.coordinates.push_back(locations.at(1)); + params.coordinates.push_back(locations.at(2)); +} +void CheckNotImplemented(const osrm::OSRM &osrm, osrm::TripParameters ¶ms) +{ + osrm::json::Object result; + auto rc = osrm.Trip(params, result); + BOOST_REQUIRE(rc == osrm::Status::Error); + auto code = result.values.at("code").get().value; + BOOST_CHECK_EQUAL(code, "NotImplemented"); +} + +void CheckOk(const osrm::OSRM &osrm, osrm::TripParameters ¶ms) +{ + osrm::json::Object result; + auto rc = osrm.Trip(params, result); + BOOST_REQUIRE(rc == osrm::Status::Ok); + auto code = result.values.at("code").get().value; + BOOST_CHECK_EQUAL(code, "Ok"); +} + +BOOST_AUTO_TEST_CASE(test_tfse_illegal_parameters) +{ + const auto args = get_args(); + auto osrm = getOSRM(args.at(0)); + using namespace osrm; + + const auto locations = get_locations_in_big_component(); + auto params = osrm::TripParameters(); + + // one parameter set + ResetParams(locations, params); + params.roundtrip = false; + CheckNotImplemented(osrm, params); + + // two parameter set + ResetParams(locations, params); + params.source = TripParameters::SourceType::Any; + params.roundtrip = false; + CheckNotImplemented(osrm, params); + + ResetParams(locations, params); + params.source = TripParameters::SourceType::First; + params.roundtrip = false; + CheckNotImplemented(osrm, params); + + ResetParams(locations, params); + params.destination = TripParameters::DestinationType::Any; + params.roundtrip = false; + CheckNotImplemented(osrm, params); + + ResetParams(locations, params); + params.destination = TripParameters::DestinationType::Last; + params.roundtrip = false; + CheckNotImplemented(osrm, params); + + // three parameters set + params.source = TripParameters::SourceType::Any; + params.destination = TripParameters::DestinationType::Any; + params.roundtrip = false; + CheckNotImplemented(osrm, params); + + params.source = TripParameters::SourceType::Any; + params.destination = TripParameters::DestinationType::Last; + params.roundtrip = false; + CheckNotImplemented(osrm, params); + + params.source = TripParameters::SourceType::First; + params.destination = TripParameters::DestinationType::Any; + params.roundtrip = false; + CheckNotImplemented(osrm, params); +} + +BOOST_AUTO_TEST_CASE(test_tfse_legal_parameters) +{ + const auto args = get_args(); + auto osrm = getOSRM(args.at(0)); + using namespace osrm; + const auto locations = get_locations_in_big_component(); + json::Object result; + TripParameters params; + + // no parameter set + ResetParams(locations, params); + CheckOk(osrm, params); + + // one parameter set + ResetParams(locations, params); + params.roundtrip = true; + CheckOk(osrm, params); + + ResetParams(locations, params); + params.source = TripParameters::SourceType::First; + CheckOk(osrm, params); + + ResetParams(locations, params); + params.source = TripParameters::SourceType::Any; + CheckOk(osrm, params); + + ResetParams(locations, params); + params.destination = TripParameters::DestinationType::Any; + CheckOk(osrm, params); + + ResetParams(locations, params); + params.destination = TripParameters::DestinationType::Last; + CheckOk(osrm, params); + + // two parameter set + ResetParams(locations, params); + params.destination = TripParameters::DestinationType::Last; + params.roundtrip = true; + CheckOk(osrm, params); + + ResetParams(locations, params); + params.source = TripParameters::SourceType::First; + params.roundtrip = true; + CheckOk(osrm, params); + + ResetParams(locations, params); + params.source = TripParameters::SourceType::First; + params.destination = TripParameters::DestinationType::Any; + CheckOk(osrm, params); + + ResetParams(locations, params); + params.source = TripParameters::SourceType::Any; + params.destination = TripParameters::DestinationType::Last; + CheckOk(osrm, params); + + ResetParams(locations, params); + params.source = TripParameters::SourceType::First; + params.destination = TripParameters::DestinationType::Last; + CheckOk(osrm, params); + + ResetParams(locations, params); + params.source = TripParameters::SourceType::Any; + params.roundtrip = true; + CheckOk(osrm, params); + + ResetParams(locations, params); + params.destination = TripParameters::DestinationType::Any; + params.roundtrip = true; + CheckOk(osrm, params); + + ResetParams(locations, params); + params.source = TripParameters::SourceType::Any; + params.destination = TripParameters::DestinationType::Any; + CheckOk(osrm, params); + + // three parameter set + params.source = TripParameters::SourceType::Any; + params.destination = TripParameters::DestinationType::Any; + params.roundtrip = true; + CheckOk(osrm, params); + + params.source = TripParameters::SourceType::First; + params.destination = TripParameters::DestinationType::Last; + params.roundtrip = false; + CheckOk(osrm, params); + + params.source = TripParameters::SourceType::Any; + params.destination = TripParameters::DestinationType::Last; + params.roundtrip = true; + CheckOk(osrm, params); + + params.source = TripParameters::SourceType::First; + params.destination = TripParameters::DestinationType::Any; + params.roundtrip = true; + CheckOk(osrm, params); + + params.source = TripParameters::SourceType::First; + params.destination = TripParameters::DestinationType::Last; + params.roundtrip = true; + CheckOk(osrm, params); +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/unit_tests/server/parameters_parser.cpp b/unit_tests/server/parameters_parser.cpp index eff892267..72168a0b9 100644 --- a/unit_tests/server/parameters_parser.cpp +++ b/unit_tests/server/parameters_parser.cpp @@ -495,9 +495,43 @@ BOOST_AUTO_TEST_CASE(valid_trip_urls) reference_1.coordinates = coords_1; auto result_1 = parseParameters("1,2;3,4"); BOOST_CHECK(result_1); - CHECK_EQUAL_RANGE(reference_1.bearings, result_1->bearings); CHECK_EQUAL_RANGE(reference_1.radiuses, result_1->radiuses); CHECK_EQUAL_RANGE(reference_1.coordinates, result_1->coordinates); + + TripParameters reference_2{}; + reference_2.coordinates = coords_1; + reference_2.source = TripParameters::SourceType::First; + reference_2.destination = TripParameters::DestinationType::Last; + auto result_2 = parseParameters("1,2;3,4?source=first&destination=last"); + BOOST_CHECK(result_2); + CHECK_EQUAL_RANGE(reference_2.radiuses, result_2->radiuses); + CHECK_EQUAL_RANGE(reference_2.coordinates, result_2->coordinates); + + // check supported source/destination/rountrip combinations + auto param_fse_r = + parseParameters("1,2;3,4?source=first&destination=last&roundtrip=true"); + BOOST_CHECK(param_fse_r->IsValid()); + auto param_fse_nr_ = + parseParameters("1,2;3,4?source=first&destination=last&roundtrip=false"); + BOOST_CHECK(param_fse_nr_->IsValid()); + auto param_fs_r = parseParameters("1,2;3,4?source=first&roundtrip=true"); + BOOST_CHECK(param_fs_r->IsValid()); + auto param_fs_nr = parseParameters("1,2;3,4?source=first&roundtrip=false"); + BOOST_CHECK(param_fs_nr->IsValid()); + auto param_fe_r = parseParameters("1,2;3,4?destination=last&roundtrip=true"); + BOOST_CHECK(param_fe_r->IsValid()); + auto param_fe_nr = parseParameters("1,2;3,4?destination=last&roundtrip=false"); + BOOST_CHECK(param_fe_nr->IsValid()); + auto param_r = parseParameters("1,2;3,4?roundtrip=true"); + BOOST_CHECK(param_r->IsValid()); + auto param_nr = parseParameters("1,2;3,4?roundtrip=false"); + BOOST_CHECK(param_nr->IsValid()); + + auto param_fail_1 = + testInvalidOptions("1,2;3,4?source=blubb&destination=random"); + BOOST_CHECK_EQUAL(param_fail_1, 15UL); + auto param_fail_2 = testInvalidOptions("1,2;3,4?source=first&destination=nah"); + BOOST_CHECK_EQUAL(param_fail_2, 33UL); } BOOST_AUTO_TEST_SUITE_END()