diff --git a/CHANGELOG.md b/CHANGELOG.md index 37c48d4d12..f2c8e471a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -78,6 +78,7 @@ * CHANGED: Do not reclassify ferry connections when no hierarchies are to be generated [#4487](https://github.com/valhalla/valhalla/pull/4487) * ADDED: Added a config option to sort nodes spatially during graph building [#4455](https://github.com/valhalla/valhalla/pull/4455) * ADDED: Timezone info in route and matrix responses [#4491](https://github.com/valhalla/valhalla/pull/4491) + * ADDED: Support for `voiceInstructions` attribute in OSRM serializer via `voice_instructions` request parameter [#4506](https://github.com/valhalla/valhalla/pull/4506) * CHANGED: use pkg-config to find spatialite & geos and remove our cmake modules; upgraded conan's boost to 1.83.0 in the process [#4253](https://github.com/valhalla/valhalla/pull/4253) * ADDED: Added aggregation logic to filter stage of tile building [#4512](https://github.com/valhalla/valhalla/pull/4512) * UPDATED: tz to 2023d [#4519](https://github.com/valhalla/valhalla/pull/4519) diff --git a/docs/docs/api/turn-by-turn/api-reference.md b/docs/docs/api/turn-by-turn/api-reference.md index c2055973e2..6fdc13a4fe 100644 --- a/docs/docs/api/turn-by-turn/api-reference.md +++ b/docs/docs/api/turn-by-turn/api-reference.md @@ -271,12 +271,13 @@ Directions options should be specified at the top level of the JSON object. | `directions_type` | An enum with 3 values. | | `format` | Four options are available: | | `banner_instructions` | If the format is `osrm`, this boolean indicates if each step should have the additional `bannerInstructions` attribute, which can be displayed in some navigation system SDKs. | +| `voice_instructions` | If the format is `osrm`, this boolean indicates if each step should have the additional `voiceInstructions` attribute, which can be heard in some navigation system SDKs. | | `alternates` | A number denoting how many alternate routes should be provided. There may be no alternates or less alternates than the user specifies. Alternates are not yet supported on multipoint routes (that is, routes with more than 2 locations). They are also not supported on time dependent routes. | -For example a bus request with the result in Spanish using the OSRM (Open Source Routing Machine) format with the additional bannerInstructions in the steps would use the following json: +For example a bus request with the result in Spanish using the OSRM (Open Source Routing Machine) format with the additional bannerInstructions and voiceInstructions in the steps would use the following json: ```json -{"locations":[{"lat":40.730930,"lon":-73.991379},{"lat":40.749706,"lon":-73.991562}],"format":"osrm","costing":"bus","banner_instructions":true,"language":"es-ES"} +{"locations":[{"lat":40.730930,"lon":-73.991379},{"lat":40.749706,"lon":-73.991562}],"format":"osrm","costing":"bus","banner_instructions":true,"voice_instructions":true,"language":"es-ES"} ``` ##### Supported language tags diff --git a/proto/options.proto b/proto/options.proto index e7bdc3a478..b69f654c32 100644 --- a/proto/options.proto +++ b/proto/options.proto @@ -488,4 +488,5 @@ message Options { // or when CostMatrix is the selected matrix mode. bool banner_instructions = 55; // Whether to return bannerInstructions in the OSRM serializer response float elevation_interval = 56; // Interval for sampling elevation along the route path. [default = 0.0]; + bool voice_instructions = 57; // Whether to return voiceInstructions in the OSRM serializer response } diff --git a/src/tyr/route_serializer_osrm.cc b/src/tyr/route_serializer_osrm.cc index 540152216d..dcef6ca96c 100644 --- a/src/tyr/route_serializer_osrm.cc +++ b/src/tyr/route_serializer_osrm.cc @@ -96,6 +96,10 @@ const constexpr PointLL::first_type DOUGLAS_PEUCKER_THRESHOLDS[19] = { 2.6, // z18 }; +const constexpr double SECONDS_BEFORE_VERBAL_TRANSITION_ALERT_INSTRUCTION = 15.0; +const constexpr double SECONDS_BEFORE_VERBAL_PRE_TRANSITION_INSTRUCTION = 5.0; +const constexpr double APPROXIMATE_VERBAL_POSTRANSITION_LENGTH = 110; + inline double clamp(const double lat) { return std::max(std::min(lat, double(EPSG3857_MAX_LATITUDE)), double(-EPSG3857_MAX_LATITUDE)); } @@ -1424,6 +1428,147 @@ void maneuver_geometry(json::MapPtr& step, } } +// The idea is that the instructions come a fixed amount of seconds before the maneuver takes place. +// For whatever reasons, a distance in meters from the end of the maneuver needs to be provided +// though. When different speeds are used on the road, they all need to be taken into account. This +// function calculates the distance before the end of the maneuver by checking the elapsed_cost +// seconds of each edges and accumulates their distances until the seconds threshold is passed. The +// speed of this last edge is then used to subtract the distance so that the the seconds until the end +// are exactly the provided amount of seconds. +float distance_along_geometry(const valhalla::DirectionsLeg::Maneuver* prev_maneuver, + valhalla::odin::EnhancedTripLeg* etp, + const double distance, + const uint32_t target_seconds) { + uint32_t node_index = prev_maneuver->end_path_index(); + double end_node_elapsed_seconds = etp->node(node_index).cost().elapsed_cost().seconds(); + double begin_node_elapsed_seconds = + etp->node(prev_maneuver->begin_path_index()).cost().elapsed_cost().seconds(); + + // If the maneuver is too short, simply return its distance. + if (end_node_elapsed_seconds - begin_node_elapsed_seconds < target_seconds) { + return distance; + } + + float accumulated_distance_km = 0; + float previous_accumulated_distance_km = 0; + double accumulated_seconds = 0; + double previous_accumulated_seconds = 0; + // Find the node after which the instructions should be heard: + while (accumulated_seconds < target_seconds && node_index >= prev_maneuver->begin_path_index()) { + node_index -= 1; + // not really accumulating seconds ourselves, but it happens elsewhere: + previous_accumulated_seconds = accumulated_seconds; + accumulated_seconds = + end_node_elapsed_seconds - etp->node(node_index).cost().elapsed_cost().seconds(); + previous_accumulated_distance_km = accumulated_distance_km; + accumulated_distance_km += etp->GetCurrEdge(node_index)->length_km(); + } + // The node_index now indicates the node AFTER which the target_seconds will be reached + // we now have to subtract the surplus distance (based on seconds) of this edge from the + // accumulated_distance_km + auto surplus_percentage = + (accumulated_seconds - target_seconds) / (accumulated_seconds - previous_accumulated_seconds); + accumulated_distance_km -= + (accumulated_distance_km - previous_accumulated_distance_km) * surplus_percentage; + if (accumulated_distance_km * 1000 > distance) { + return distance; + } else { + return accumulated_distance_km * 1000; // in meters + } +} + +// Populate the voiceInstructions within a step. +json::ArrayPtr voice_instructions(const valhalla::DirectionsLeg::Maneuver* prev_maneuver, + const valhalla::DirectionsLeg::Maneuver& maneuver, + const double distance, + const uint32_t maneuver_index, + valhalla::odin::EnhancedTripLeg* etp) { + // voiceInstructions is an array, because there may be similar voice instructions. + // When the step is long enough, there may be multiple voice instructions. + json::ArrayPtr voice_instructions_array = json::array({}); + + // distanceAlongGeometry is the distance along the current step from where on this + // voice instruction should be played. It is measured from the end of the maneuver. + // Using the maneuver length (distance) as the distanceAlongGeometry plays + // right at the beginning of the maneuver. A distanceAlongGeometry of 10 is + // shortly (10 meters at the given speed) after the maneuver has started. + // The voice_instruction_beginning starts shortly after the beginning of the step. + // The voice_instruction_end starts shortly before the end of the step. + float distance_before_verbal_transition_alert_instruction = -1; + float distance_before_verbal_pre_transition_instruction = -1; + if (prev_maneuver) { + distance_before_verbal_transition_alert_instruction = + distance_along_geometry(prev_maneuver, etp, distance, + SECONDS_BEFORE_VERBAL_TRANSITION_ALERT_INSTRUCTION); + distance_before_verbal_pre_transition_instruction = + distance_along_geometry(prev_maneuver, etp, distance, + SECONDS_BEFORE_VERBAL_PRE_TRANSITION_INSTRUCTION); + if (maneuver_index == 1 && !prev_maneuver->verbal_pre_transition_instruction().empty()) { + // For depart maneuver, we always want to hear the verbal_pre_transition_instruction + // right at the beginning of the navigation. This is something like: + // Drive West on XYZ Street. + // This voice_instruction_start is only created once. It is always played, even when + // the maneuver would otherwise be too short. + json::MapPtr voice_instruction_start = json::map({}); + voice_instruction_start->emplace("distanceAlongGeometry", json::fixed_t{distance, 1}); + voice_instruction_start->emplace("announcement", + prev_maneuver->verbal_pre_transition_instruction()); + voice_instructions_array->emplace_back(std::move(voice_instruction_start)); + } else if (distance > distance_before_verbal_transition_alert_instruction + + APPROXIMATE_VERBAL_POSTRANSITION_LENGTH && + !prev_maneuver->verbal_post_transition_instruction().empty()) { + // In all other cases we want to play the verbal_post_transition_instruction shortly + // after the maneuver has started but only if there is sufficient time to play both + // the upcoming verbal_pre_transition_instruction and the verbal_post_transition_instruction + // itself. The approximation here is that the verbal_post_transition_instruction takes 100 + // meters to play + the 10 meters after the maneuver start which is added so that the + // instruction is not played directly on the intersection where the maneuver starts. + json::MapPtr voice_instruction_beginning = json::map({}); + voice_instruction_beginning->emplace("distanceAlongGeometry", json::fixed_t{distance - 10, 1}); + voice_instruction_beginning->emplace("announcement", + prev_maneuver->verbal_post_transition_instruction()); + voice_instructions_array->emplace_back(std::move(voice_instruction_beginning)); + } + } + + if (!maneuver.verbal_transition_alert_instruction().empty()) { + json::MapPtr voice_instruction_end = json::map({}); + if (maneuver_index == 1 && distance_before_verbal_transition_alert_instruction == distance) { + // For the depart maneuver we want to play both the verbal_post_transition_instruction and + // the verbal_transition_alert_instruction even if the maneuver is too short. + voice_instruction_end->emplace("distanceAlongGeometry", json::fixed_t{distance / 2, 1}); + } else { + // In all other cases we use distance_before_verbal_transition_alert_instruction value + // as it is capped to the maneuver length + voice_instruction_end + ->emplace("distanceAlongGeometry", + json::fixed_t{distance_before_verbal_transition_alert_instruction, 1}); + } + voice_instruction_end->emplace("announcement", maneuver.verbal_transition_alert_instruction()); + voice_instructions_array->emplace_back(std::move(voice_instruction_end)); + } + + if (!maneuver.verbal_pre_transition_instruction().empty()) { + json::MapPtr voice_instruction_end = json::map({}); + if (maneuver_index == 1 && distance_before_verbal_pre_transition_instruction >= distance / 2) { + // For the depart maneuver we want to play the verbal_post_transition_instruction, + // the verbal_transition_alert_instruction and + // the verbal_pre_transition_instruction even if the maneuver is too short. + voice_instruction_end->emplace("distanceAlongGeometry", json::fixed_t{distance / 4, 1}); + } else { + // In all other cases we use distance_before_verbal_pre_transition_instruction value + // as it is capped to the maneuver length + voice_instruction_end->emplace("distanceAlongGeometry", + json::fixed_t{distance_before_verbal_pre_transition_instruction, + 1}); + } + voice_instruction_end->emplace("announcement", maneuver.verbal_pre_transition_instruction()); + voice_instructions_array->emplace_back(std::move(voice_instruction_end)); + } + + return voice_instructions_array; +} + // Get the mode std::string get_mode(const valhalla::DirectionsLeg::Maneuver& maneuver, const bool arrive_maneuver, @@ -1700,6 +1845,19 @@ json::ArrayPtr serialize_legs(const google::protobuf::RepeatedPtrFieldemplace("voiceInstructions", + voice_instructions(prev_maneuver, maneuver, prev_distance, + maneuver_index, &etp)); + } + if (arrive_maneuver) { + step->emplace("voiceInstructions", + voice_instructions(prev_maneuver, maneuver, distance, maneuver_index, &etp)); + } + } + // Add junction_name if not the start maneuver std::string junction_name = get_sign_elements(sign.junction_names()); if (!depart_maneuver && !junction_name.empty()) { diff --git a/src/worker.cc b/src/worker.cc index 4c1ee7820c..7c7f33bf00 100644 --- a/src/worker.cc +++ b/src/worker.cc @@ -1196,6 +1196,10 @@ void from_json(rapidjson::Document& doc, Options::Action action, Api& api) { options.set_banner_instructions( rapidjson::get(doc, "/banner_instructions", options.banner_instructions())); + // whether to return voiceInstructions in OSRM serializer, default false + options.set_voice_instructions( + rapidjson::get(doc, "/voice_instructions", options.voice_instructions())); + // whether to include roundabout_exit maneuvers, default true auto roundabout_exits = rapidjson::get(doc, "/roundabout_exits", diff --git a/test/gurka/test_osrm_serializer.cc b/test/gurka/test_osrm_serializer.cc index a91155d54b..e7b3461a46 100644 --- a/test/gurka/test_osrm_serializer.cc +++ b/test/gurka/test_osrm_serializer.cc @@ -520,6 +520,186 @@ TEST(Standalone, HeadingNumberAutoRoute) { // clang-format on } +class VoiceInstructions : public ::testing::Test { +protected: + static gurka::map map; + + static void SetUpTestSuite() { + constexpr double gridsize_metres = 50; + + const std::string ascii_map = R"( + X Y + \ | --M--N + \ | __ -- ¯¯ + A----------BCD< + | ¯¯ -- __ + | --E--F + Z + )"; + + const gurka::ways ways = + {{"AB", {{"highway", "primary"}, {"maxspeed", "80"}, {"name", "10th Avenue SE"}}}, + {"BC", {{"highway", "primary"}, {"maxspeed", "50"}, {"name", "10th Avenue SE"}}}, + {"CD", {{"highway", "primary"}, {"maxspeed", "30"}, {"name", "10th Avenue SE"}}}, + {"CX", {{"highway", "primary"}, {"maxspeed", "30"}, {"name", "Sidestreet"}}}, + {"DMN", {{"highway", "primary"}, {"maxspeed", "30"}, {"name", "Heinrich Street"}}}, + {"DEF", {{"highway", "primary"}, {"maxspeed", "30"}, {"name", "Alfred Street"}}}, + {"YDZ", {{"highway", "primary"}, {"name", "Market Street"}, {"oneway", "yes"}}}}; + + const auto layout = gurka::detail::map_to_coordinates(ascii_map, gridsize_metres, {0.0, 0.0}); + + const std::unordered_map build_config{ + {"mjolnir.data_processing.use_admin_db", "false"}}; + + map = gurka::buildtiles(layout, ways, {}, {}, "test/data/osrm_serializer_voice", build_config); + } + + rapidjson::Document json_request(const std::string& from, const std::string& to) { + const std::string& request = + (boost::format( + R"({"locations":[{"lat":%s,"lon":%s},{"lat":%s,"lon":%s}],"costing":"auto","voice_instructions":true})") % + std::to_string(map.nodes.at(from).lat()) % std::to_string(map.nodes.at(from).lng()) % + std::to_string(map.nodes.at(to).lat()) % std::to_string(map.nodes.at(to).lng())) + .str(); + auto result = gurka::do_action(valhalla::Options::route, map, request); + return gurka::convert_to_json(result, Options::Format::Options_Format_osrm); + } +}; + +gurka::map VoiceInstructions::map = {}; + +TEST_F(VoiceInstructions, VoiceInstructionsPresent) { + auto json = json_request("A", "F"); + auto steps = json["routes"][0]["legs"][0]["steps"].GetArray(); + // Validate that each step has voiceInstructions with announcement and distanceAlongGeometry + for (int step = 0; step < steps.Size(); ++step) { + ASSERT_TRUE(steps[step].HasMember("voiceInstructions")); + ASSERT_TRUE(steps[step]["voiceInstructions"].IsArray()); + + EXPECT_GT(steps[step]["voiceInstructions"].Size(), 0); + for (int instr = 0; instr < steps[step]["voiceInstructions"].GetArray().Size(); ++instr) { + ASSERT_TRUE(steps[step]["voiceInstructions"][instr].HasMember("announcement")); + ASSERT_TRUE(steps[step]["voiceInstructions"][instr].HasMember("distanceAlongGeometry")); + } + } +} + +// depart_instruction +// +// 13 grids * 50m/grid = 650m +// => distanceAlongGeometry = 650m +// +// verbal_transition_alert_instruction +// +// The idea is that the instructions come a fixed amount of seconds before the maneuver takes place. +// For whatever reasons, a distance in meters from the end of the maneuver needs to be provided +// though. When different speeds are used on the road, they all need to be taken into account. +// +// CD: 50m / 30km/h = 50m * 3,600s / 30,000m = 50m * 0.12s/m = 6s +// BC: 50m / 50km/h = 50m * 3,600s / 50,000m = 50m * 0.072s/m = 3.6s +// SECONDS_BEFORE_VERBAL_TRANSITION_ALERT_INSTRUCTION = 15s +// AB: 15s - 6s - 3.6s = 5.4s +// 5.4s * 80 km/h = 5.4s * 80,000m / 3600s = 120m +// => distanceAlongGeometry = 120m + 50m + 50m = 220m +// +// verbal_pre_transition_instruction +// +// SECONDS_BEFORE_VERBAL_PRE_TRANSITION_INSTRUCTION = 5s +// CD: 5s * 30km/h = 5s * 30,000m / 3600s ~= 42m +TEST_F(VoiceInstructions, DistanceAlongGeometryVoiceInstructions) { + auto json = json_request("A", "D"); + auto steps = json["routes"][0]["legs"][0]["steps"].GetArray(); + + auto depart_instruction = steps[0]["voiceInstructions"][0].GetObject(); + EXPECT_STREQ( + depart_instruction["announcement"].GetString(), + "Drive east on 10th Avenue SE. Then, in 700 meters, You will arrive at your destination."); + EXPECT_EQ(depart_instruction["distanceAlongGeometry"].GetFloat(), 650.0); + auto verbal_transition_alert_instruction = steps[0]["voiceInstructions"][1].GetObject(); + EXPECT_STREQ(verbal_transition_alert_instruction["announcement"].GetString(), + "You will arrive at your destination."); + EXPECT_EQ(round(verbal_transition_alert_instruction["distanceAlongGeometry"].GetFloat()), 220); + auto verbal_pre_transition_instruction = steps[0]["voiceInstructions"][2].GetObject(); + EXPECT_STREQ(verbal_pre_transition_instruction["announcement"].GetString(), + "You have arrived at your destination."); + EXPECT_EQ(round(verbal_pre_transition_instruction["distanceAlongGeometry"].GetFloat()), 42); +} + +TEST_F(VoiceInstructions, ShortDepartVoiceInstructions) { + auto json = json_request("C", "F"); + auto steps = json["routes"][0]["legs"][0]["steps"].GetArray(); + + EXPECT_EQ(steps[0]["voiceInstructions"].Size(), 3); + + auto depart_instruction = steps[0]["voiceInstructions"][0].GetObject(); + EXPECT_STREQ(depart_instruction["announcement"].GetString(), + "Drive east on 10th Avenue SE. Then Bear right onto Alfred Street."); + EXPECT_EQ(depart_instruction["distanceAlongGeometry"].GetFloat(), 50.0); + auto verbal_transition_alert_instruction = steps[0]["voiceInstructions"][1].GetObject(); + EXPECT_STREQ(verbal_transition_alert_instruction["announcement"].GetString(), + "Bear right onto Alfred Street."); + EXPECT_EQ(verbal_transition_alert_instruction["distanceAlongGeometry"].GetFloat(), 25.0); + auto verbal_pre_transition_instruction = steps[0]["voiceInstructions"][2].GetObject(); + EXPECT_STREQ(verbal_pre_transition_instruction["announcement"].GetString(), + "Bear right onto Alfred Street."); + EXPECT_EQ(verbal_pre_transition_instruction["distanceAlongGeometry"].GetFloat(), 12.5); +} + +TEST_F(VoiceInstructions, ShortIntermediateStepVoiceInstructions) { + auto json = json_request("X", "Z"); + auto steps = json["routes"][0]["legs"][0]["steps"].GetArray(); + + EXPECT_EQ(steps[1]["voiceInstructions"].Size(), 2); // No verbal_post_transition_instruction + + auto verbal_transition_alert_instruction = steps[1]["voiceInstructions"][0].GetObject(); + EXPECT_STREQ(verbal_transition_alert_instruction["announcement"].GetString(), + "Turn right onto Market Street."); + EXPECT_EQ(verbal_transition_alert_instruction["distanceAlongGeometry"].GetFloat(), 50.0); + + auto verbal_pre_transition_instruction = steps[1]["voiceInstructions"][1].GetObject(); + EXPECT_STREQ(verbal_pre_transition_instruction["announcement"].GetString(), + "Turn right onto Market Street. Then You will arrive at your destination."); + // ~= 38.2 + EXPECT_GT(verbal_pre_transition_instruction["distanceAlongGeometry"].GetFloat(), 38); + EXPECT_LT(verbal_pre_transition_instruction["distanceAlongGeometry"].GetFloat(), 39); +} + +TEST_F(VoiceInstructions, AllVoiceInstructions) { + auto json = json_request("A", "F"); + auto steps = json["routes"][0]["legs"][0]["steps"].GetArray(); + + auto depart_instruction = steps[0]["voiceInstructions"][0].GetObject(); + EXPECT_STREQ(depart_instruction["announcement"].GetString(), + "Drive east on 10th Avenue SE. Then Bear right onto Alfred Street."); + EXPECT_EQ(depart_instruction["distanceAlongGeometry"].GetFloat(), 650.0); + + auto bear_right_instruction = steps[0]["voiceInstructions"][1].GetObject(); + EXPECT_STREQ(bear_right_instruction["announcement"].GetString(), "Bear right onto Alfred Street."); + EXPECT_EQ(round(bear_right_instruction["distanceAlongGeometry"].GetFloat()), 220); + + auto continue_instruction = steps[1]["voiceInstructions"][0].GetObject(); + EXPECT_STREQ(continue_instruction["announcement"].GetString(), "Continue for 900 meters."); + EXPECT_EQ(continue_instruction["distanceAlongGeometry"].GetFloat(), 847.0); + + auto arrive_instruction = steps[1]["voiceInstructions"][1].GetObject(); + EXPECT_STREQ(arrive_instruction["announcement"].GetString(), + "You will arrive at your destination."); + // ~= 125 + EXPECT_GT(arrive_instruction["distanceAlongGeometry"].GetFloat(), 124); + EXPECT_LT(arrive_instruction["distanceAlongGeometry"].GetFloat(), 126); + + auto final_arrive_instruction = steps[1]["voiceInstructions"][2].GetObject(); + EXPECT_STREQ(final_arrive_instruction["announcement"].GetString(), + "You have arrived at your destination."); + // ~= 42 + EXPECT_GT(final_arrive_instruction["distanceAlongGeometry"].GetFloat(), 41); + EXPECT_LT(final_arrive_instruction["distanceAlongGeometry"].GetFloat(), 43); + + auto last_instruction = steps[2]["voiceInstructions"][0].GetObject(); + EXPECT_STREQ(last_instruction["announcement"].GetString(), "You will arrive at your destination."); + EXPECT_EQ(last_instruction["distanceAlongGeometry"].GetFloat(), 0.0); +} + TEST(Standalone, BannerInstructions) { const std::string ascii_map = R"( A-------------1-B---X