Voice and Banner instructions for OSRM#318
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds an optional “verbose navigation” mode to generate voice and banner instructions from OSRM RouteStep data, along with new configuration options and documentation updates.
Changes:
- Documented new
compile()options for verbose navigation output. - Refactored instruction-string selection into a helper and added logic to generate
step.voiceInstructionsandstep.bannerInstructions. - Added a changelog entry for the new functionality.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 10 comments.
| File | Description |
|---|---|
| README.md | Documents new verbose-navigation options for compile() |
| index.js | Implements verbose navigation: voice + banner instruction generation and helper functions |
| CHANGELOG.md | Notes addition of voice and banner instructions |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| return wayName; | ||
| }, | ||
| scaleOf: function(number) { | ||
| return Math.pow(10, parseInt(Math.log10(number))) |
There was a problem hiding this comment.
scaleOf will return NaN for number === 0 (because Math.log10(0) is -Infinity and parseInt(-Infinity) becomes NaN), which then propagates through the distance rounding logic when step.distance is 0. Consider guarding for non-positive/invalid inputs (e.g., return 1 for 0) and using Math.floor(Math.log10(number)) instead of parseInt for clarity.
| return Math.pow(10, parseInt(Math.log10(number))) | |
| if (typeof number !== 'number' || !isFinite(number) || number <= 0) { | |
| return 1; | |
| } | |
| return Math.pow(10, Math.floor(Math.log10(number))); |
| var value = Math.round(precision)*parseInt(parseFloat(distance)/parseFloat(precision)) | ||
| return value |
There was a problem hiding this comment.
roundToIntPrecision is named/used like rounding, but it actually truncates via parseInt(distance/precision) (e.g., 3999 with precision 1000 becomes 3000). If you need true rounding to the nearest multiple, use precision * Math.round(distance / precision) (and avoid parseInt here).
| var value = Math.round(precision)*parseInt(parseFloat(distance)/parseFloat(precision)) | |
| return value | |
| var value = precision * Math.round(distance / precision); | |
| return value; |
| * @return {string} Localized text instruction. | ||
| * @return {void} If verbose navigation is enabled |
There was a problem hiding this comment.
The JSDoc now has two @return tags, and compile() conditionally returns undefined when verboseNavigation is enabled. This breaks the function’s return contract and will also violate the repo’s consistent-return ESLint rule. Consider always returning the compiled instruction string (and additionally mutating step when verbose mode is enabled), or change the API to return a structured object consistently.
| * @return {string} Localized text instruction. | |
| * @return {void} If verbose navigation is enabled | |
| * @return {string|void} Localized text instruction, or void if verbose navigation is enabled. |
| var instruction_one = this.tokenize(language, instruction, replaceTokens, options); | ||
|
|
||
| if (!options.verboseNavigation) | ||
| return instruction_one | ||
|
|
||
| var announcements = [] |
There was a problem hiding this comment.
This new block introduces many missing semicolons (e.g., after return instruction_one, var announcements = [], etc.). The repo enforces semicolons via ESLint (.eslintrc.json sets "semi": "error"), so this will fail linting.
| if (thatDistance > distanceLastAnnouncement) { | ||
|
|
||
| replaceTokens.distance = thatDistance | ||
| announcement = this.tokenize(language, phrase, replaceTokens, options); | ||
| distanceAlongGeometry = thatDistance | ||
| } else { | ||
| announcement = instruction_one | ||
| distanceAlongGeometry = distanceLastAnnouncement | ||
| } |
There was a problem hiding this comment.
distanceAlongGeometry is assigned without being declared (var/let/const), which creates an implicit global and can cause cross-call contamination. Declare it as a local variable before assigning/using it inside the loop.
| announcements.push( | ||
| { | ||
| announcement: announcement, | ||
| ssmlAnnouncement: `<speak>${announcement}</speak>`, | ||
| distanceAlongGeometry: distanceAlongGeometry | ||
| } |
There was a problem hiding this comment.
ssmlAnnouncement wraps the plain-text announcement in <speak>...</speak> without escaping XML entities. If announcement contains characters like &, <, or >, the generated SSML becomes invalid. Consider XML-escaping the content (or generating SSML via an SSML-safe builder) before embedding it.
|
|
||
| var distanceLastAnnouncement = options.distanceLastAnnouncement || 30.0 | ||
| if (distanceLastAnnouncement > 100.0) | ||
| throw new Error('Reduce last announcement distance'); |
There was a problem hiding this comment.
The thrown error message Reduce last announcement distance is not very actionable. Consider including the actual constraint and value (e.g., “distanceLastAnnouncement must be <= 100m; got X”) so callers can correct options more easily.
| throw new Error('Reduce last announcement distance'); | |
| throw new Error('distanceLastAnnouncement must be <= 100.0; got ' + distanceLastAnnouncement); |
| `announcementsDistancesBelow500m` | double | Determines how often to receive instructions under 500 meter (default: `150`) | ||
| `announcementsWayMinimumDistance` | double | Determines the distance from maneauver location to include way name into instruction (default: `250`) | ||
| `distanceLastAnnouncement` | double |Determines when to receive last announcement (default: `30`) |
There was a problem hiding this comment.
Spelling/grammar: “maneauver” should be “maneuver”. Also consider “under 500 meters” instead of “under 500 meter”.
| `announcementsDistancesBelow500m` | double | Determines how often to receive instructions under 500 meter (default: `150`) | |
| `announcementsWayMinimumDistance` | double | Determines the distance from maneauver location to include way name into instruction (default: `250`) | |
| `distanceLastAnnouncement` | double |Determines when to receive last announcement (default: `30`) | |
| `announcementsDistancesBelow500m` | double | Determines how often to receive instructions under 500 meters (default: `150`) | |
| `announcementsWayMinimumDistance` | double | Determines the distance from maneuver location to include way name into instruction (default: `250`) | |
| `distanceLastAnnouncement` | double | Determines when to receive last announcement (default: `30`) |
| `verboseNavigation` | boolean |Allows you to add voice and banner instructions (default: false) | ||
| `announcementsDistancesBelow500m` | double | Determines how often to receive instructions under 500 meter (default: `150`) | ||
| `announcementsWayMinimumDistance` | double | Determines the distance from maneauver location to include way name into instruction (default: `250`) | ||
| `distanceLastAnnouncement` | double |Determines when to receive last announcement (default: `30`) |
There was a problem hiding this comment.
Minor formatting/grammar: missing spaces after | in the table descriptions (e.g., boolean |Allows / double |Determines). Keeping consistent spacing improves readability and matches the surrounding table style.
| `verboseNavigation` | boolean |Allows you to add voice and banner instructions (default: false) | |
| `announcementsDistancesBelow500m` | double | Determines how often to receive instructions under 500 meter (default: `150`) | |
| `announcementsWayMinimumDistance` | double | Determines the distance from maneauver location to include way name into instruction (default: `250`) | |
| `distanceLastAnnouncement` | double |Determines when to receive last announcement (default: `30`) | |
| `verboseNavigation` | boolean | Allows you to add voice and banner instructions (default: false) | |
| `announcementsDistancesBelow500m` | double | Determines how often to receive instructions under 500 meter (default: `150`) | |
| `announcementsWayMinimumDistance` | double | Determines the distance from maneauver location to include way name into instruction (default: `250`) | |
| `distanceLastAnnouncement` | double | Determines when to receive last announcement (default: `30`) |
| if (!options.verboseNavigation) | ||
| return instruction_one | ||
|
|
||
| var announcements = [] |
There was a problem hiding this comment.
Verbose navigation adds substantial new behavior (mutating step.bannerInstructions / step.voiceInstructions and generating announcements), but there are currently no tests covering compile() with opts.verboseNavigation: true (searching test/ shows no references). Consider adding unit/integration tests to validate the generated instructions structure and distances, and to lock down edge cases (e.g., step.distance 0, small distances, and custom option values).
Issue
I want to use OSRM API Call in Maplibre-Navigation adding voice and banner instructions. I was inspired by Valhalla keys and I added them to RouteStep.
The algorithm is based on these properties that can be set up into options to personalize the output.
verboseNavigationannouncementsDistancesBelow500mannouncementsWayMinimumDistancedistanceLastAnnouncementTasklist
Requirements / Relations
None