07 Maneuver announcements
Overview
In this tutorial, we will show how to generate maneuver announcements from the data given by getGuidanceInfo.
Base
You can use your results from Tutorial06
as a base for our new project.
Maneuver announcement basics
In the last tutorial, we introduced a control which shows the next maneuver as a sign. In this tutorial, we will create a speakable maneuver text which will be outputted by the Android text to speech engine.
Maneuver text generation settings
Maneuver text generating is done internally by the maneuver text generator. It converts the guidance information you get when calling NavigationSDK.getGuidanceInformation() to a human readable text with current maneuver hints. This text can then be easily outputted by a text to speech engine (TTS). The generator can even produce output which is intended to play static wave files, but we will not go into detail of this functionality. For now, we will output readable text for a given TTS engine. The announcement text can optionally be completed with the current street name and sign post information. Also, the decimal delimiter sign can be set (for outputting numbers).
The settings are done by the class ManeuverGeneratorSettings. It has four members which can be changed by their setters:
setOutputType(int outputType)
Sets the output type to TTS (0) or SAMPLES (1)setSeparator(char separator)
Sets the decimal separator for numbersetSpeakStreetNames(boolean speakStreetNames)
When set and output type is TTS, street names will be integrated into the output text.setSpeakSignPosts(boolean speakSignPosts)
When set and output type is TTS, sign posts will be integrated into the output text.
The settings can be set by calling setManeuverGeneratorSettings(ManeuverGeneratorSettings settings)
.
Maneuver text generation
Important
The maneuver text is only generated when a navigation is active. Also, the generator takes care of the timings and the number of announcements. It only delivers a text when it is the right time for an announcement. Every announcement will only be generated once, further calls to the function in the same state will return an empty text. Because of this, calling of NavigationSDK.getManeuverText()
outside the navigation loop is not recommended and can lead to unwanted behaviour!
The maneuver text will be generated by the function:
NavigationSDK.getManeuverText(NavigationInformation info, StringRetriever textRetriever)
.
The method has two input parameters:
- The NavigationInformation info, which provides the next and second next maneuver data
- The StringRetriever
The StringRetriever class provides the function `getText(int id), which must be overwritten by the user. This function is needed by the maneuver generator for two purposes:
- To get the grammar for a distinct maneuver sentence
- To get particular sentences for a maneuver
The maneuver generator builds up an announcement text by checking which maneuvers will be next. From this information, it tries to get the grammar string from the StringRetriever. Such a grammar looks like "in |DISTANCE |TUNNEL |MANOEUVRE1 |MANOEUVREADDON |STREETNAME |SIGNPOST |, then |IMMEDIATELY |MANOEUVRE2 |MANOEUVREADDON". The fields will be replaced by the correct sentences, which also will be filled by the StringRetriever class.
The advantage of this approach is that the generator doesn't need any information about localization. All relevant localized strings and even the localized grammar of the sentence are stored outside the generator. The generator itself knows only IDs and gets the correct text by calling getText(int id)
with the corresponding ID.
In our example, we create a new class ManeuverStringRetriever which is a subclass of StringRetriever. We override the getText()
function with the following:
public class ManeuverStringRetriever extends StringRetriever { @Override public String getText(int sdkId) { String retval = ""; switch(sdkId) { case SDKManeuverTextID.SDK_MG_guidance_border: retval = Application.getContext().getString(R.string.guidance_border); break; case SDKManeuverTextID.SDK_MG_guidance_continue: retval = Application.getContext().getString(R.string.guidance_continue); break; case SDKManeuverTextID.SDK_MG_guidance_distMan1: retval = Application.getContext().getString(R.string.guidance_distMan1); break; case SDKManeuverTextID.SDK_MG_guidance_distMan1ThenMan2: retval = Application.getContext().getString(R.string.guidance_distMan1ThenMan2); break; case SDKManeuverTextID.SDK_MG_guidance_endOfStreetMan: retval = Application.getContext().getString(R.string.guidance_endOfStreetMan); break; case SDKManeuverTextID.SDK_MG_guidance_ferryEnter: retval = Application.getContext().getString(R.string.guidance_ferryEnter); break; case SDKManeuverTextID.SDK_MG_guidance_ferryExit: retval = Application.getContext().getString(R.string.guidance_ferryExit); break; case SDKManeuverTextID.SDK_MG_guidance_followRoad: retval = Application.getContext().getString(R.string.guidance_followRoad); break; case SDKManeuverTextID.SDK_MG_guidance_holdHalfLeft: retval = Application.getContext().getString(R.string.guidance_holdHalfLeft); break; case SDKManeuverTextID.SDK_MG_guidance_holdHalfRight: retval = Application.getContext().getString(R.string.guidance_holdHalfRight); break; case SDKManeuverTextID.SDK_MG_guidance_holdStraight: retval = Application.getContext().getString(R.string.guidance_holdStraight); break; case SDKManeuverTextID.SDK_MG_guidance_motorroadEnter: retval = Application.getContext().getString(R.string.guidance_motorroadEnter); break; case SDKManeuverTextID.SDK_MG_guidance_motorroadExit: retval = Application.getContext().getString(R.string.guidance_motorroadExit); break; case SDKManeuverTextID.SDK_MG_guidance_motorwayEnter: retval = Application.getContext().getString(R.string.guidance_motorwayEnter); break; case SDKManeuverTextID.SDK_MG_guidance_motorwayExit: retval = Application.getContext().getString(R.string.guidance_motorwayExit); break; case SDKManeuverTextID.SDK_MG_guidance_motorwayTurnHalfLeft: retval = Application.getContext().getString(R.string.guidance_motorwayTurnHalfLeft); break; case SDKManeuverTextID.SDK_MG_guidance_motorwayTurnHalfRight: retval = Application.getContext().getString(R.string.guidance_motorwayTurnHalfRight); break; case SDKManeuverTextID.SDK_MG_guidance_motorwayTurnLeft: retval = Application.getContext().getString(R.string.guidance_motorwayTurnLeft); break; case SDKManeuverTextID.SDK_MG_guidance_motorwayTurnRight: retval = Application.getContext().getString(R.string.guidance_motorwayTurnRight); break; case SDKManeuverTextID.SDK_MG_guidance_now: retval = Application.getContext().getString(R.string.guidance_now); break; case SDKManeuverTextID.SDK_MG_guidance_nowMan1: retval = Application.getContext().getString(R.string.guidance_nowMan1); break; case SDKManeuverTextID.SDK_MG_guidance_nowMan1ThenMan2: retval = Application.getContext().getString(R.string.guidance_nowMan1ThenMan2); break; case SDKManeuverTextID.SDK_MG_guidance_roundaboutExit1: retval = Application.getContext().getString(R.string.guidance_roundaboutExit1); break; case SDKManeuverTextID.SDK_MG_guidance_roundaboutExit10: retval = Application.getContext().getString(R.string.guidance_roundaboutExit10); break; case SDKManeuverTextID.SDK_MG_guidance_roundaboutExit11: retval = Application.getContext().getString(R.string.guidance_roundaboutExit11); break; case SDKManeuverTextID.SDK_MG_guidance_roundaboutExit12: retval = Application.getContext().getString(R.string.guidance_roundaboutExit12); break; case SDKManeuverTextID.SDK_MG_guidance_roundaboutExit2: retval = Application.getContext().getString(R.string.guidance_roundaboutExit2); break; case SDKManeuverTextID.SDK_MG_guidance_roundaboutExit3: retval = Application.getContext().getString(R.string.guidance_roundaboutExit3); break; case SDKManeuverTextID.SDK_MG_guidance_roundaboutExit4: retval = Application.getContext().getString(R.string.guidance_roundaboutExit4); break; case SDKManeuverTextID.SDK_MG_guidance_roundaboutExit5: retval = Application.getContext().getString(R.string.guidance_roundaboutExit5); break; case SDKManeuverTextID.SDK_MG_guidance_roundaboutExit6: retval = Application.getContext().getString(R.string.guidance_roundaboutExit6); break; case SDKManeuverTextID.SDK_MG_guidance_roundaboutExit7: retval = Application.getContext().getString(R.string.guidance_roundaboutExit7); break; case SDKManeuverTextID.SDK_MG_guidance_roundaboutExit8: retval = Application.getContext().getString(R.string.guidance_roundaboutExit8); break; case SDKManeuverTextID.SDK_MG_guidance_roundaboutExit9: retval = Application.getContext().getString(R.string.guidance_roundaboutExit9); break; case SDKManeuverTextID.SDK_MG_guidance_startAreaExit: retval = Application.getContext().getString(R.string.guidance_startAreaExit); break; case SDKManeuverTextID.SDK_MG_guidance_stopover: retval = Application.getContext().getString(R.string.guidance_stopover); break; case SDKManeuverTextID.SDK_MG_guidance_straightLeft: retval = Application.getContext().getString(R.string.guidance_straightLeft); break; case SDKManeuverTextID.SDK_MG_guidance_straightRight: retval = Application.getContext().getString(R.string.guidance_straightRight); break; case SDKManeuverTextID.SDK_MG_guidance_streetDirection: retval = Application.getContext().getString(R.string.guidance_streetDirection); break; case SDKManeuverTextID.SDK_MG_guidance_streetDirectionSep: retval = Application.getContext().getString(R.string.guidance_streetDirectionSep); break; case SDKManeuverTextID.SDK_MG_guidance_targetAreaEnter: retval = Application.getContext().getString(R.string.guidance_targetAreaEnter); break; case SDKManeuverTextID.SDK_MG_guidance_targetAreaEnterRouteList: retval = Application.getContext().getString(R.string.guidance_targetAreaEnterRouteList); break; case SDKManeuverTextID.SDK_MG_guidance_targetReached: retval = Application.getContext().getString(R.string.guidance_targetReached); break; case SDKManeuverTextID.SDK_MG_guidance_tunnelAfter: retval = Application.getContext().getString(R.string.guidance_tunnelAfter); break; case SDKManeuverTextID.SDK_MG_guidance_tunnelIn: retval = Application.getContext().getString(R.string.guidance_tunnelIn); break; case SDKManeuverTextID.SDK_MG_guidance_turnAround: retval = Application.getContext().getString(R.string.guidance_turnAround); break; case SDKManeuverTextID.SDK_MG_guidance_turnLeft: retval = Application.getContext().getString(R.string.guidance_turnLeft); break; case SDKManeuverTextID.SDK_MG_guidance_turnRight: retval = Application.getContext().getString(R.string.guidance_turnRight); break; case SDKManeuverTextID.SDK_MG_guidance_uTurn: retval = Application.getContext().getString(R.string.guidance_uTurn); break; } return retval; } }
The getText()
function returns text from the string resources dependent on the given id.
In Android, you can easily use this pattern for localizing text, because Android itself takes care of the correct strings.xml file, dependent on the current language. You only have to provide the localized strings with the correct name in the correspondent strings.xml files.
Extend the strings.xml in our example with the following:
... <string name="guidance_continue">continue</string> <string name="guidance_turnLeft">turn left</string> <string name="guidance_turnRight">turn right</string> <string name="guidance_straightLeft">take sharp left turn</string> <string name="guidance_straightRight">take sharp right turn</string> <string name="guidance_holdHalfLeft">keep left</string> <string name="guidance_holdHalfRight">keep right</string> <string name="guidance_holdStraight">keep driving straight ahead</string> <string name="guidance_roundaboutExit1">leave roundabout at first exit</string> <string name="guidance_roundaboutExit2">leave roundabout at second exit</string> <string name="guidance_roundaboutExit3">leave roundabout at third exit</string> <string name="guidance_roundaboutExit4">leave roundabout at fourth exit</string> <string name="guidance_roundaboutExit5">leave roundabout at fifth exit</string> <string name="guidance_roundaboutExit6">leave roundabout at sixth exit</string> <string name="guidance_roundaboutExit7">leave roundabout at seventh exit</string> <string name="guidance_roundaboutExit8">leave roundabout at eighth exit</string> <string name="guidance_roundaboutExit9">leave roundabout at ninth exit</string> <string name="guidance_roundaboutExit10">leave roundabout at tenth exit</string> <string name="guidance_roundaboutExit11">leave roundabout at eleventh exit</string> <string name="guidance_roundaboutExit12">leave roundabout at twelfth exit</string> <string name="guidance_uTurn">If possible, turn around</string> <string name="guidance_turnAround">turn around</string> <string name="guidance_targetReached">you will have reached your destination!</string> <string name="guidance_motorwayTurnRight">turn right then join the motorway</string> <string name="guidance_motorwayTurnLeft">turn left, then join the motorway</string> <string name="guidance_motorwayTurnHalfRight">keep right, then join the motorway</string> <string name="guidance_motorwayTurnHalfLeft">keep left, then join the motorway</string> <string name="guidance_motorwayEnter">join the motorway</string> <string name="guidance_motorwayExit">leave the motorway</string> <string name="guidance_stopover">Stopover point</string> <string name="guidance_targetAreaEnter">you have reached the destination area.</string> <string name="guidance_targetAreaEnterRouteList">You have reached the destination area.</string> <string name="guidance_startAreaExit">You have left the start area.</string> <string name="guidance_border">border crossing</string> <string name="guidance_motorroadEnter">join the highway</string> <string name="guidance_motorroadExit">leave the highway</string> <string name="guidance_ferryEnter">and drive onto the ferry</string> <string name="guidance_ferryExit">and drive off the ferry</string> <string name="guidance_distMan1ThenMan2">in |DISTANCE |TUNNEL |MANOEUVRE1 |MANOEUVREADDON |STREETNAME |SIGNPOST |, then |IMMEDIATELY |MANOEUVRE2 |MANOEUVREADDON</string> <string name="guidance_distMan1">in |DISTANCE |TUNNEL |MANOEUVRE1 |MANOEUVREADDON |STREETNAME |SIGNPOST</string> <string name="guidance_followRoad">follow the direction of the road</string> <string name="guidance_tunnelAfter">after the tunnel</string> <string name="guidance_endOfStreetMan">At the end of the road |DISTANCE |TUNNEL |MANOEUVRE1 |MANOEUVREADDON |STREETNAME |SIGNPOST</string> <string name="guidance_tunnelIn">in the tunnel</string> <string name="guidance_streetDirection">towards</string> <string name="guidance_streetDirectionSep">onto</string> <string name="guidance_nowMan1">now |TUNNEL |MANOEUVRE1 |MANOEUVREADDON |STREETNAME</string> <string name="guidance_nowMan1ThenMan2">now |TUNNEL |MANOEUVRE1 |MANOEUVREADDON |, then |IMMEDIATELY |MANOEUVRE2 |MANOEUVREADDON</string> <string name="guidance_now">now</string> ...
So, if the generator requests a string with the ID SDK_MG_guidance_holdHalfLeft, we will return "keep left".
Output format of the generated maneuver text
The generator is able to generate two output formats:
- A "normal" text for TTS output
- A text with all samples that have to be played for the current maneuver (we will not cover this functionality in this tutorial)
The first format is self explaining, the output will be a text like "in 250m, turn right onto Sunset Boulevard". You can pipe it directly to a TTS engine. The second format is used for the maneuver announcement via pre-recorded samples. The output will be a list of sample names in the correct order.
You don't have to use the strings.xml as we do in our example, you can get the strings elsewhere, they only have to return the correct sentence. We have strings for a number of languages, so there is nothing to do beside filling the strings.xml for a quick start with localized data.
Output of the announcement text through the android text to speech engine
To output text as TTS, we use a little helper class called TTSEngine. This class helps to initialize and finalize the Android engine, to get the maneuver text and to output this text through the Android TTS engine.
The TTSEngine class is derived from NavigationControl to allow easy integration in our NavigationLoop and looks like this:
package com.ptvag.navigation.tutorial; public class TTSEngine implements NavigationControl { private Context mContext; private boolean mIsInitialized; private TextToSpeech mTTS = null; private StringRetriever retriever = new ManeuverStringRetriever(); public TTSEngine(Context context) { mContext = context; mIsInitialized = false; ManeuverGeneratorSettings settings = new ManeuverGeneratorSettings(); settings.setOutputType(0); settings.setSpeakStreetNames(true); NavigationSDK.setManeuverGeneratorSettings(settings); mTTS = new TextToSpeech(mContext, new TextToSpeech.OnInitListener() { @Override public void onInit(int i) { mTTS.setLanguage(Locale.US); mIsInitialized = true; } }); } @Override public void onGPS(GPSData gps) { } @Override public void onNavigation(final NavigationInformation info, final GPSData gps) { String text = NavigationSDK.getManeuverText(info, retriever); if(text.length() > 0) { speak(text); } } @Override public void onNavigationStarted(RouteInformation info) { } @Override public void onNavigationStopped() { } public void shutdown() { // Don't forget to shutdown tts! if (mTTS != null) { mTTS.stop(); mTTS.shutdown(); } } }
It starts and stops the engine, gets the maneuver text when update() is called and speaks the generated text.
To retrieve the sentences out of the strings.xml, we add the following code to the Application class.
public class Application extends android.app.Application { private static Application instance; ... public Application() { instance = this; } public static Context getContext() { return instance; } ... }
The engine will be created in the MainActivity and added to the NavigationLoop by adding it to the list of NavigationControls.
... public class MainActivity extends DrawerActivity implements OnInitializeListener { private MapView mapView; private NavigationLoop navigationLoop; private TTSEngine ttsEngine; ... @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); inflateNavigationMenu(R.menu.drawer_menu); ttsEngine = new TTSEngine(this); ... navigationLoop.addControl(ttsEngine); ... } ... }
In the NavigationLoop class, the TTsEngine.onNavigation() function is called in the run() method. It is safe to do this in a separate thread like we do.