Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OSRM Serializer: Adds voice instructions #4506

Merged
merged 24 commits into from
Feb 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
f652e38
OSRM serializer: Implements voiceInstructions
eikes Dec 9, 2023
f924b50
Apply clang-format
eikes Dec 10, 2023
c72cd99
Makes proper use of pre- and post- transition instructions in voice i…
eikes Dec 10, 2023
bae792d
Use start voice instructions and fix distances for voice instructions
eikes Dec 10, 2023
a9d8005
OSRM serializer with voiceInstructions: Adds tests
eikes Dec 28, 2023
17b50e9
Adds distance_along_geometry function to determine how far before the…
eikes Jan 8, 2024
f379d3b
Adds well documented voice instructions test for distanceAlongGeometr…
eikes Jan 11, 2024
79394e4
Adds CHANGELOG entry
eikes Jan 12, 2024
8635e1d
Merge branch 'master' into voice-instructions
eikes Jan 12, 2024
52e3253
Adds voice_instructions param to turn-by-turn directions API document…
eikes Jan 12, 2024
f35eae6
Apply clang-format
eikes Jan 12, 2024
15ac327
Add comments, ensure distance_along_geometry cannot be longer than di…
eikes Jan 15, 2024
7237a34
Calculate distance_along_geometry by interpolating the meters along t…
eikes Jan 16, 2024
49bebe5
Apply clang format again :D
eikes Jan 16, 2024
f426530
Merge branch 'master' into voice-instructions
eikes Jan 17, 2024
5b62b8a
Merge branch 'master' into voice-instructions
eikes Jan 22, 2024
64268c5
Merge branch 'master' into voice-instructions
nilsnolde Jan 23, 2024
1d6be86
Merge branch 'master' into voice-instructions
eikes Jan 24, 2024
568414a
Replace magic number with named const
eikes Jan 24, 2024
7813517
Merge branch 'master' into voice-instructions
eikes Jan 25, 2024
d9d50d0
Merge branch 'master' into voice-instructions
eikes Jan 27, 2024
a78ccc5
Merge branch 'master' into voice-instructions
eikes Feb 9, 2024
eb1106d
Tyr OSRM serializer voiceInstructions: Use both verbal_transition_ale…
eikes Feb 10, 2024
35d4e3a
Uncomment accidentally commented tests
eikes Feb 11, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
5 changes: 3 additions & 2 deletions docs/docs/api/turn-by-turn/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -271,12 +271,13 @@ Directions options should be specified at the top level of the JSON object.
| `directions_type` | An enum with 3 values. <ul><li>`none` indicating no maneuvers or instructions should be returned.</li><li>`maneuvers` indicating that only maneuvers be returned.</li><li>`instructions` indicating that maneuvers with instructions should be returned (this is the default if not specified).</li></ul> |
| `format` | Four options are available: <ul><li>`json` is default valhalla routing directions JSON format</li><li>`gpx` returns the route as a GPX (GPS exchange format) XML track</li><li>`osrm` creates a OSRM compatible route directions JSON</li><li>`pbf` formats the result using protocol buffers</li></ul> |
| `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
Expand Down
1 change: 1 addition & 0 deletions proto/options.proto
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
158 changes: 158 additions & 0 deletions src/tyr/route_serializer_osrm.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
Expand Down Expand Up @@ -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});
eikes marked this conversation as resolved.
Show resolved Hide resolved
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,
Expand Down Expand Up @@ -1700,6 +1845,19 @@ json::ArrayPtr serialize_legs(const google::protobuf::RepeatedPtrField<valhalla:
}
}

// Add voice instructions if the user requested them
if (options.voice_instructions()) {
if (prev_step) {
prev_step->emplace("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()) {
Expand Down
4 changes: 4 additions & 0 deletions src/worker.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1196,6 +1196,10 @@ void from_json(rapidjson::Document& doc, Options::Action action, Api& api) {
options.set_banner_instructions(
rapidjson::get<bool>(doc, "/banner_instructions", options.banner_instructions()));

// whether to return voiceInstructions in OSRM serializer, default false
options.set_voice_instructions(
rapidjson::get<bool>(doc, "/voice_instructions", options.voice_instructions()));

// whether to include roundabout_exit maneuvers, default true
auto roundabout_exits =
rapidjson::get<bool>(doc, "/roundabout_exits",
Expand Down