07 Maneuver announcements
Overview
In this tutorial, we will show how to generate maneuver announcements from the data given by the 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 iOS 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 [NSDK_Navigation getGuidanceInformation:error:] 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 NSDK_ManeuverGeneratorSettings. It has four members which can be changed by their setters:
setOutputType:(int)
Sets the output type to TTS (0) or SAMPLES (1)setSeparator:(char)
Sets the decimal separator for numbersetSpeakStreetNames:(boolean)
When set and output type is TTS, street names will be integrated into the output text.setSpeakSignPosts(boolean)
When set and output type is TTS, sign posts will be integrated into the output text.
The settings can be set by calling [NSDK_Navigation setManeuverGeneratorSettings:error:] .
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 [NSDK_Navigation getManeuverText:]
outside the navigation loop is not recommended and can lead to unwanted behaviour!
The maneuver text will be generated by the function:
[NSDK_Navigation getManeuverText:stringRetriever:error:].
The method has two input parameters:
- The NSDK_NavigationInformation info, which provides the next and second next maneuver data
- The NSDK_StringRetriever
The NSDK_StringRetriever protocol declares the function getText:
, which must be implemented. 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 NSDK_StringRetriever implementation.
This approach has the big advantage, 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)
with the correspondent ID.
In our example, we create a new class StringRetriever which implements NSDK_StringRetriever. We implement the getText:(int)
function with the following:
#import "StringRetriever.h" @implementation StringRetriever -(NSString*) getText:(const NSInteger) sdkId { NSString * retval = nil; switch(sdkId) { case NSDK_MG_guidance_border: retval = NSLocalizedString(@"guidance_border", nil); break; case NSDK_MG_guidance_continue: retval = NSLocalizedString(@"guidance_continue", nil); break; case NSDK_MG_guidance_distMan1: retval = NSLocalizedString(@"guidance_distMan1", nil); break; case NSDK_MG_guidance_distMan1ThenMan2: retval = NSLocalizedString(@"guidance_distMan1ThenMan2", nil); break; case NSDK_MG_guidance_endOfStreetMan: retval = NSLocalizedString(@"guidance_endOfStreetMan", nil); break; case NSDK_MG_guidance_ferryEnter: retval = NSLocalizedString(@"guidance_ferryEnter", nil); break; case NSDK_MG_guidance_ferryExit: retval = NSLocalizedString(@"guidance_ferryExit", nil); break; case NSDK_MG_guidance_followRoad: retval = NSLocalizedString(@"guidance_followRoad", nil); break; case NSDK_MG_guidance_holdHalfLeft: retval = NSLocalizedString(@"guidance_holdHalfLeft", nil); break; case NSDK_MG_guidance_holdHalfRight: retval = NSLocalizedString(@"guidance_holdHalfRight", nil); break; case NSDK_MG_guidance_holdStraight: retval = NSLocalizedString(@"guidance_holdStraight", nil); break; case NSDK_MG_guidance_motorroadEnter: retval = NSLocalizedString(@"guidance_motorroadEnter", nil); break; case NSDK_MG_guidance_motorroadExit: retval = NSLocalizedString(@"guidance_motorroadExit", nil); break; case NSDK_MG_guidance_motorwayEnter: retval = NSLocalizedString(@"guidance_motorwayEnter", nil); break; case NSDK_MG_guidance_motorwayExit: retval = NSLocalizedString(@"guidance_motorwayExit", nil); break; case NSDK_MG_guidance_motorwayTurnHalfLeft: retval = NSLocalizedString(@"guidance_motorwayTurnHalfLeft", nil); break; case NSDK_MG_guidance_motorwayTurnHalfRight: retval = NSLocalizedString(@"guidance_motorwayTurnHalfRight", nil); break; case NSDK_MG_guidance_motorwayTurnLeft: retval = NSLocalizedString(@"guidance_motorwayTurnLeft", nil); break; case NSDK_MG_guidance_motorwayTurnRight: retval = NSLocalizedString(@"guidance_motorwayTurnRight", nil); break; case NSDK_MG_guidance_now: retval = NSLocalizedString(@"guidance_now", nil); break; case NSDK_MG_guidance_nowMan1: retval = NSLocalizedString(@"guidance_nowMan1", nil); break; case NSDK_MG_guidance_nowMan1ThenMan2: retval = NSLocalizedString(@"guidance_nowMan1ThenMan2", nil); break; case NSDK_MG_guidance_roundaboutExit1: retval = NSLocalizedString(@"guidance_roundaboutExit1", nil); break; case NSDK_MG_guidance_roundaboutExit10: retval = NSLocalizedString(@"guidance_roundaboutExit10", nil); break; case NSDK_MG_guidance_roundaboutExit11: retval = NSLocalizedString(@"guidance_roundaboutExit11", nil); break; case NSDK_MG_guidance_roundaboutExit12: retval = NSLocalizedString(@"guidance_roundaboutExit12", nil); break; case NSDK_MG_guidance_roundaboutExit2: retval = NSLocalizedString(@"guidance_roundaboutExit2", nil); break; case NSDK_MG_guidance_roundaboutExit3: retval = NSLocalizedString(@"guidance_roundaboutExit3", nil); break; case NSDK_MG_guidance_roundaboutExit4: retval = NSLocalizedString(@"guidance_roundaboutExit4", nil); break; case NSDK_MG_guidance_roundaboutExit5: retval = NSLocalizedString(@"guidance_roundaboutExit5", nil); break; case NSDK_MG_guidance_roundaboutExit6: retval = NSLocalizedString(@"guidance_roundaboutExit6", nil); break; case NSDK_MG_guidance_roundaboutExit7: retval = NSLocalizedString(@"guidance_roundaboutExit7", nil); break; case NSDK_MG_guidance_roundaboutExit8: retval = NSLocalizedString(@"guidance_roundaboutExit8", nil); break; case NSDK_MG_guidance_roundaboutExit9: retval = NSLocalizedString(@"guidance_roundaboutExit9", nil); break; case NSDK_MG_guidance_startAreaExit: retval = NSLocalizedString(@"guidance_startAreaExit", nil); break; case NSDK_MG_guidance_stopover: retval = NSLocalizedString(@"guidance_stopover", nil); break; case NSDK_MG_guidance_straightLeft: retval = NSLocalizedString(@"guidance_straightLeft", nil); break; case NSDK_MG_guidance_straightRight: retval = NSLocalizedString(@"guidance_straightRight", nil); break; case NSDK_MG_guidance_streetDirection: retval = NSLocalizedString(@"guidance_streetDirection", nil); break; case NSDK_MG_guidance_streetDirectionSep: retval = NSLocalizedString(@"guidance_streetDirectionSep", nil); break; case NSDK_MG_guidance_targetAreaEnter: retval = NSLocalizedString(@"guidance_targetAreaEnter", nil); break; case NSDK_MG_guidance_targetAreaEnterRouteList: retval = NSLocalizedString(@"guidance_targetAreaEnterRouteList", nil); break; case NSDK_MG_guidance_targetReached: retval = NSLocalizedString(@"guidance_targetReached", nil); break; case NSDK_MG_guidance_tunnelAfter: retval = NSLocalizedString(@"guidance_tunnelAfter", nil); break; case NSDK_MG_guidance_tunnelIn: retval = NSLocalizedString(@"guidance_tunnelIn", nil); break; case NSDK_MG_guidance_turnAround: retval = NSLocalizedString(@"guidance_turnAround", nil); break; case NSDK_MG_guidance_turnLeft: retval = NSLocalizedString(@"guidance_turnLeft", nil); break; case NSDK_MG_guidance_turnRight: retval = NSLocalizedString(@"guidance_turnRight", nil); break; case NSDK_MG_guidance_uTurn: retval = NSLocalizedString(@"guidance_uTurn", nil); break; } return retval; } @end
Don't forget the protocol declaration NSDK_StringRetriever
in the header file:
#import <Foundation/Foundation.h> #import "navicoreFramework/NSDK_Navigation.h" @interface StringRetriever : NSObject <NSDK_StringRetriever> -(NSString*) getText:(NSInteger) id; @end
In iOS you can use, NSLocalizedString
which is a Foundation macro that returns a localized version of a string. This macro expects a "Strings File", with name Localizable.strings
. So create a new "Strings file" in XCode and verify that it's included it in your App bundle (this is the case when the file shows up in the "Copy Bundle Resources" in your "Build Phases")
"guidance_continue" = "continue"; "guidance_turnLeft" = "turn left"; "guidance_turnRight" = "turn right"; "guidance_straightLeft" = "take sharp left turn"; "guidance_straightRight" = "take sharp right turn"; "guidance_holdHalfLeft" = "keep left"; "guidance_holdHalfRight" = "keep right"; "guidance_holdStraight" = "keep driving straight ahead"; "guidance_roundaboutExit1" = "leave roundabout at first exit"; "guidance_roundaboutExit2" = "leave roundabout at second exit"; "guidance_roundaboutExit3" = "leave roundabout at third exit"; "guidance_roundaboutExit4" = "leave roundabout at fourth exit"; "guidance_roundaboutExit5" = "leave roundabout at fifth exit"; "guidance_roundaboutExit6" = "leave roundabout at sixth exit"; "guidance_roundaboutExit7" = "leave roundabout at seventh exit"; "guidance_roundaboutExit8" = "leave roundabout at eighth exit"; "guidance_roundaboutExit9" = "leave roundabout at ninth exit"; "guidance_roundaboutExit10" = "leave roundabout at tenth exit"; "guidance_roundaboutExit11" = "leave roundabout at eleventh exit"; "guidance_roundaboutExit12" = "leave roundabout at twelfth exit"; "guidance_uTurn" = "If possible, turn around"; "guidance_turnAround" = "turn around"; "guidance_targetReached" = "you will have reached your destination!"; "guidance_motorwayTurnRight" = "turn right then join the motorway"; "guidance_motorwayTurnLeft" = "turn left, then join the motorway"; "guidance_motorwayTurnHalfRight" = "keep right, then join the motorway"; "guidance_motorwayTurnHalfLeft" = "keep left, then join the motorway"; "guidance_motorwayEnter" = "join the motorway"; "guidance_motorwayExit" = "leave the motorway"; "guidance_stopover" = "Stopover point"; "guidance_targetAreaEnter" = "you have reached the destination area."; "guidance_targetAreaEnterRouteList" = "You have reached the destination area."; "guidance_startAreaExit" = "You have left the start area."; "guidance_border" = "border crossing"; "guidance_motorroadEnter" = "join the highway"; "guidance_motorroadExit" = "leave the highway"; "guidance_ferryEnter" = "and drive onto the ferry"; "guidance_ferryExit" = "and drive off the ferry"; "guidance_distMan1ThenMan2" = "in |DISTANCE |TUNNEL |MANOEUVRE1 |MANOEUVREADDON |STREETNAME |SIGNPOST |, then |IMMEDIATELY |MANOEUVRE2 |MANOEUVREADDON"; "guidance_distMan1" = "in |DISTANCE |TUNNEL |MANOEUVRE1 |MANOEUVREADDON |STREETNAME |SIGNPOST"; "guidance_followRoad" = "follow the direction of the road"; "guidance_tunnelAfter" = "after the tunnel"; "guidance_endOfStreetMan" = "At the end of the road |DISTANCE |TUNNEL |MANOEUVRE1 |MANOEUVREADDON |STREETNAME |SIGNPOST"; "guidance_tunnelIn" = "in the tunnel"; "guidance_streetDirection" = "towards"; "guidance_streetDirectionSep" = "onto"; "guidance_nowMan1" = "now |TUNNEL |MANOEUVRE1 |MANOEUVREADDON |STREETNAME"; "guidance_nowMan1ThenMan2" = "now |TUNNEL |MANOEUVRE1 |MANOEUVREADDON |, then |IMMEDIATELY |MANOEUVRE2 |MANOEUVREADDON"; "guidance_now" = "now"; "route_calc_failed" = "Route calculation failed!"; "route_calc_progress_title" = "Calculating route";
So, if the generator requests a string with the ID NSDK_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 Localizable.strings file as we do in our example, you can get the strings elsewhere, they only must return the correct sentence. We have strings for a number of languages, so there is nothing to do beside creating a Localizable.strings file in language specific folders for a quick start with localized data.
Output of the announcement text through the iOS text to speech engine
To output text as TTS, we use a little helper class called TTSEngine to initialize and finalize the TTS engine, to get the maneuver text and to output this text through the iOS TTS engine.
The TTSEngine class looks like this:
#import "TTSEngine.h" #import "StringRetriever.h" @interface TTSEngine () @property (strong, nonatomic) AVSpeechSynthesizer *synthesizer; @property (strong, nonatomic) StringRetriever * stringRetriever; @property (strong, nonatomic) AVAudioSession * avAudioSession; @end @implementation TTSEngine - (instancetype)init { self = [super init]; if (self) { _avAudioSession = [AVAudioSession sharedInstance]; [_avAudioSession setCategory:AVAudioSessionCategoryPlayback withOptions:AVAudioSessionCategoryOptionInterruptSpokenAudioAndMixWithOthers|AVAudioSessionCategoryOptionDuckOthers error:nil]; _synthesizer = [[AVSpeechSynthesizer alloc] init]; _synthesizer.delegate = self; _stringRetriever = [[StringRetriever alloc] init]; NSDK_ManeuverGeneratorSettings * settings = [[NSDK_ManeuverGeneratorSettings alloc] init]; [settings setOutputType:0]; [settings setSpeakStreetNames:YES]; NSError * error; [NSDK_Navigation setManeuverGeneratorSettings:settings error:&error]; } return self; } - (void) update:(NSDK_NavigationInformation*) info { NSError * error; NSString * maneuverText = [NSDK_Navigation getManeuverText:info stringRetriever:self.stringRetriever error:&error]; if (error) { NSLog(@"getManeuverText returns error:%@", error); } else { if ([maneuverText length] > 0){ NSLog(@"getManeuverText returns text:%@", maneuverText); [self speak:maneuverText]; } } } - (void) speak:(NSString*) text { AVSpeechUtterance *nextUtterance = [[AVSpeechUtterance alloc] initWithString:text]; nextUtterance.voice = [AVSpeechSynthesisVoice voiceWithLanguage:@"en-US"]; [_avAudioSession setActive: YES error: nil]; [self.synthesizer speakUtterance:nextUtterance]; } - (void)speechSynthesizer:(AVSpeechSynthesizer *)synthesizer didFinishSpeechUtterance:(AVSpeechUtterance *)utterance { NSError * error; [_avAudioSession setActive: NO withOptions: AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error: &error]; } @end
And here are the visible methods in TTSEngine.h:
#import <Foundation/Foundation.h> #import <AVFoundation/AVFoundation.h> #import "navicoreFramework/NSDK_Navigation.h" @interface TTSEngine : NSObject <AVSpeechSynthesizerDelegate> - (void) update:(NSDK_NavigationInformation*) info; - (void) speak:(NSString*) text; @end
In the NavigationLoop class, the [TTSEngine update:] function is called in the _run method.
@interface NavigationLoop () { BOOL _isNavigation; } ... @property TTSEngine *ttsEngine; @end @implementation NavigationLoop - (instancetype)initWithMapView:(UIMapView*) mapView viewController:(ViewController*) viewController { self = [super init]; if (self) { ... _ttsEngine = [[TTSEngine alloc] init]; } return self; } ... - (void) _run { ... if (nsdk_NavigationInformation != nil) { // add autozoom for current NavigationInformation and current gps // use smooth steps for zooming [_uiMapView doAutoZoom:nsdk_NavigationInformation gpsData:nsdk_gpsdata doSmoothZoom:YES error:&error]; dispatch_async(dispatch_get_main_queue(), ^{ [_viewController navigationUpdate:nsdk_NavigationInformation]; }); if ([nsdk_NavigationInformation getDestinationReached]){ [self stopNavigation]; } [self.ttsEngine update:nsdk_NavigationInformation]; } }
When running the app, you should hear the spoken Navigation informations.