06 Navigating a route

Overview

In this tutorial you will learn how to implement route navigation and how to interpret the maneuvers given by the NavigationSDK. We also show you an easy way to simulate a navigation along the current route for testing.

Base

Use Tutorial05 as a starting point for this project.

getGuidanceInformation

To get maneuvers for our current guidance, we have to call [NSDK_Navigation getGuidanceInformation:error:] in our NavigationLoop. But we only have to call it when we are navigating a route. So we add an isNavigation flag to our NavigationLoop and corresponding accessor methods to determine the status of our app. We can do this in our NavigationLoop.m file.

@interface NavigationLoop () {
    BOOL _isNavigation;
}
@property UIMapView* uiMapView;
@end


@implementation NavigationLoop

- (instancetype)initWithMapView:(UIMapView*) mapView {
    self = [super init];
    if (self) {
         _isNavigation = NO;
        _uiMapView = mapView;
    }
    return self;
}

...

-(void) startNavigation {
  _isNavigation = YES;
}

-(void) stopNavigation {
  _isNavigation = NO;
}

-(BOOL) isNavigation {
  return _isNavigation;
}

...

Don't forget to declare the startNavigation, stopNavigation and isNavigation methods in the corresponding header file.

Then we can call [NavigationLoop startNavigation] if calculateTour returns no errors in our ViewController.

@implementation ViewController
...
- (void) calcNavigation {

    ...

    [NSDK_Navigation calculateTour: tour observer:self error:&err];
    if (err) {
      NSLog(@"calculateTour NSError returns:%@", err);
    } else {
      [_navigationLoop startNavigation];
    }
  }];
}

Now we can get the guidance information in the navigation loop. Because getGuidanceInformation is a potentially long running operation, we would usually push it to the NSDK_JobQueue. But since we are in our _run method already in one - there is no need for this. If we have no route, it makes no sense to get guidance information. So we check and return early from the loop. In the end of the function we check the getDestinationReached flag of the returned NSDK_NavigationInformation result to check if we have to stop the navigation.

@implementation NavigationLoop

...

- (void) _run {

    ...

    if (!_isNavigation) {
        return;
    }

    NSError * error;
    NSDK_NavigationInformation * nsdk_NavigationInformation = [NSDK_Navigation getGuidanceInformation:nsdk_gpsdata error:&error];

    if (error) {
        NSLog(@"getGuidanceInformation returns error:%@", error);
    }

    if (nsdk_NavigationInformation != nil) {
        if ([nsdk_NavigationInformation getDestinationReached]){
            [self stopNavigation];
        }
    }    
}

Rerouting

So whats happening if we deviate from the given route? Do we have to check and calculate a new route? No we don't have to, getGuidanceInformation will take care of that. If we deviate only a little from our current route, the call to getGuidanceInformation will pull us back to our current route. If we deviate a lot from our current route getGuidanceInformation will do a complete new routing to our destination.

Maneuver information

As an example we will implement a small View which shows the distance, the street name and an image representation of the next maneuver.

The View consists of three subviews: An UIImageView which shows an image representation of the next maneuver, a UITextField which shows the distance to the next maneuver and a UITextField with the street name of the next maneuver.

Ctrl-Drag the three UIViews as properties into the Header File:

@interface ViewController : UIViewController <NSDK_Observer>
...
@property (weak, nonatomic) IBOutlet UIImageView *uiImageViewArrow;
@property (weak, nonatomic) IBOutlet UITextField *uiTextFieldArrowCaption;
@property (weak, nonatomic) IBOutlet UITextField *uiTextFieldNextManeuver;

We write a method navigationUpdate:(NSDK_NavigationInformation*) where we will update the UITextField with the street name of the next maneuver.

@implementation ViewController

...

- (void) navigationUpdate:(NSDK_NavigationInformation*) navigationInformation {

    NSDK_ManeuverDescription * maneuver = [navigationInformation getNextManeuver];
    dispatch_async(dispatch_get_main_queue(), ^{
        [_uiTextFieldNextManeuver setText: [maneuver getStreetName]];
    });
}

Don't forget to declare this method in the header file.

Because we want to call this method from within our NavigationLoop we have to store the ViewController as a property:

@interface NavigationLoop () {
    BOOL _isNavigation;
}
@property UIMapView* uiMapView;
@property ViewController * viewController;
@end

@implementation NavigationLoop

- (instancetype)initWithMapView:(UIMapView*) mapView viewController:(ViewController*) viewController {
    self = [super init];
    if (self) {
         _isNavigation = NO;
        _uiMapView = mapView;
        _viewController = viewController;
    }
    return self;
}

...

  - (void) _run {

    ...

    if (nsdk_NavigationInformation != nil) {
        dispatch_async(dispatch_get_main_queue(), ^{
            [_viewController navigationUpdate:nsdk_NavigationInformation];
        });

        if ([nsdk_NavigationInformation getDestinationReached]){
            [self stopNavigation];
        }
    }
  }

Don't forget to adapt the init Method declaration in the header file and to revise the call of the initWithMapView: Method in the ViewController.

Attention

Don't update the GUI directly from the NavigationLoop. Only update the user interface from the GUI thread (aka main thread). Therefore all GUI processing calls have to be wrapped like above (dispatch_async(dispatch_get_main_queue(), ...)

When you start the app at this stage of the tutorial and start a navigation, you should see the street name in the top of the screen.

To update the UIImageView with a maneuver arrow, we have to interpret the NSDK_ManeuverDescription given by [NSDK_NavigationInformation getNextManeuver] and the NSDK_CrossingView given by [NSDK_NavigationInformation getNextManeuverCrossingView].

Maneuvers are described by three properties, implemented as enums. First, NSDK_ManeuverType describes the kind of maneuver we are dealing with (simple turn, entering a motorway or a roundabout for example). Second, the NSDK_ManeuverDirection describes which direction the maneuver has. Third, you get a rough estimation of the angle of a maneuver from the NSDK_ManeuverWeight. For extended information on the next crossing/junction you need to access the NSDK_CrossingView information with [NSDK_NavigationInformation getNextManeuverCrossingView].

We implement an example how to map these informations into a maneuver arrow in a new method getImageNameFromManeuver: in the ViewController. The returned NSString contains the image name of the arrow. You will find all example arrows in the res folder of Tutorial06. In addition we set the distance to the maneuver as the text of the second UITextField. To get a human readable string of [NSDK_NavigationInformation getDistToNextManeuver] we pass it to [NSDK_Navigation getLocalizedDistanceString:].

@implementation ViewController

...

- (void) navigationUpdate:(NSDK_NavigationInformation*) navigationInformation {

    NSDK_ManeuverDescription * maneuver = [navigationInformation getNextManeuver];

    NSString * imageName = [self getImageNameFromManeuver:maneuver crossingView:[navigationInformation getNextManeuverCrossingView]];
    NSString * localizedDistanceString = [NSDK_Navigation getLocalizedDistanceString:[navigationInformation getDistToNextManeuver] quantify:YES allowNow:YES now:@"now" error:nil];
    [_uiTextFieldNextManeuver setText: [maneuver getStreetName]];
    [_uiImageViewArrow setImage:[UIImage imageNamed:imageName]];
    [_uiTextFieldArrowCaption setText:localizedDistanceString];
}


-(NSString*) getImageNameFromManeuver:(NSDK_ManeuverDescription*) description crossingView:(NSDK_CrossingView*) crossingView {

    NSDK_ManeuverArrow arrow = [NSDK_Navigation getManeuverArrow:description crossing:crossingView error:nil];

    switch (arrow) {
        case None:
            return @"arw_fwd.png";
        case Left:
            return @"arw_left.png";
        case Right:
            return @"arw_right.png";
        case FowardLeft:
            return @"arw_fwd_left.png";
        case FowardRight:
            return @"arw_fwd_right.png";
        case UTurnLeft:
            return @"arw_uturn_left.png";
        case UTurnRight:
            return @"arw_uturn_right.png";
        case WeakLeft:
            return @"arw_fwd_left_weak.png";
        case WeakRight:
            return @"arw_fwd_right_weak.png";
        case RoundaboutRight045:
            return @"arw_rb_right1.png";
        case RoundaboutRight090:
            return @"arw_rb_right2.png";
        case RoundaboutRight135:
            return @"arw_rb_right3.png";
        case RoundaboutRight180:
            return @"arw_rb_right4.png";
        case RoundaboutRight225:
            return @"arw_rb_right5.png";
        case RoundaboutRight270:
            return @"arw_rb_right6.png";
        case RoundaboutRight315:
            return @"arw_rb_right7.png";
        case RoundaboutLeft045:
            return @"arw_rb_left7.png";
        case RoundaboutLeft090:
            return @"arw_rb_left6.png";
        case RoundaboutLeft135:
            return @"arw_rb_left5.png";
        case RoundaboutLeft180:
            return @"arw_rb_left4.png";
        case RoundaboutLeft225:
            return @"arw_rb_left3.png";
        case RoundaboutLeft270:
            return @"arw_rb_left2.png";
        case RoundaboutLeft315:
            return @"arw_rb_left1.png";
        case Foward:
            return @"arw_fwd.png";
        case LeaveMotorWayLeft:
            return @"arw_mw_left.png";
        case LeaveMotorWayRight:
            return @"arw_mw_right.png";
        case Destination:
            return @"arw_destination.png";
        default:
            return @"arw_fwd.png";
    }
}

Route metrics

To inform the user about the arrival, we add the following information at the bottom of the screen:

  • Estimated Arrival time
  • Estimated duration until Arrival
  • Distance to the destination

We open our storyboard File and put three labels at the bottom of the ViewController. We connect the labels with the following new members in ViewController.h

@property (weak, nonatomic) IBOutlet UILabel *uiLabelEta;
@property (weak, nonatomic) IBOutlet UILabel *uiLabelDuration;
@property (weak, nonatomic) IBOutlet UILabel *uiLabelDistance;

We set the values of these labels in our navigationUpdate function. Again, we get the appropriate values from the NSDK_NavigationInformation object:

- (void) navigationUpdate:(NSDK_NavigationInformation*) navigationInformation {
    ...
    // Estimated time of arrival
    long secondsToDestination = [navigationInformation getTimeToDestination];
    NSDate * date = [NSDate dateWithTimeIntervalSinceNow:secondsToDestination];
    NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
    [formatter setDateFormat:@"HH:mm"];
    _uiLabelEta.text = [formatter stringFromDate:date];

    // Duration
    long h = secondsToDestination / 3600;
    long m = (secondsToDestination / 60) % 60;
    long s = secondsToDestination % 60;
    _uiLabelDuration.text = [NSString stringWithFormat:@"%ld:%02ld:%02ld", h, m, s];

    // Distance
    NSString * distanceString = [NSDK_Navigation getLocalizedDistanceString:[navigationInformation getDistToDestination] quantify:YES error:nil];
    _uiLabelDistance.text = distanceString;
}

!!! note: The getLocalizedDistanceStringfunction returns a formatted String including the appropriate unit ("km" or "m" for example).

Simulate driving

For easy testing, the NavigationSDK brings its own simulation mechanism. To use it we simply generate a simulation file by calling [NSDK_Navigation createGPSSimulationWithPath:] after a route calculation. We then close our current GPSDevice by calling [[NSDK_GPSManager sharedInstance] closeGPSDevice:] and reopen a simulated GPSDevice with [[NSDK_GPSManager sharedInstance] openSimulatedGPSDevice:(BOOL) simFile:(NSString*) error:(NSError**)]].

@implementation LocationManagerShared

...

- (void) startGPSSimulation {
    _gpsSimulationIsActive = YES;

    NSError * error;
    NSString * tmpSimFileWithPath = [Helper getWritablePathWithFilename:@"sim.tmp"];
    [NSDK_Navigation createGPSSimulationWithPath:tmpSimFileWithPath error:&error];
    [[NSDK_GPSManager sharedInstance] closeGPSDevice:&error];
    [[NSDK_GPSManager sharedInstance] openSimulatedGPSDevice:false simFile:tmpSimFileWithPath error:&error];
}


- (void) stopGPSSimulation {
    NSError * error;
    [[NSDK_GPSManager sharedInstance] closeGPSDevice:&error];
    [[NSDK_GPSManager sharedInstance] openGPSDevice:NO error:&error];
    _gpsSimulationIsActive = NO;
}

Don't forget to put the appropriate function declarations into the header file!

Attention

When simulate GPS location, don't push locations to the SDK by calling [[NSDK_GPSManager sharedInstance] pushLocation:]! Only get the current position with [[NSDK_GPSManager sharedInstance] getCurrentPosition:]. The simulation will step to the next position with every call to getCurrentPosition.

Therefore we introduced a property gpsSimulationIsActive in the code above. We use this property to check, if we're allowed to push a new location into the SDK:

@interface LocationManagerShared : NSObject <CLLocationManagerDelegate>

...

@property BOOL gpsSimulationIsActive;
@implementation LocationManagerShared

...

- (void) pushLocationInSDK:(CLLocation*) location {
    if (!_gpsSimulationIsActive) {
        NSDK_JobQueue * jobQueue = [NSDK_JobQueue sharedInstance];
        [jobQueue pushWithBlock:^{
            [[NSDK_GPSManager sharedInstance] pushLocation:location error:nil];
        }];
    }
}

To start a simulation, we will introduce a UIButton in our SearchViewController (The "S" in the button means "Simulation" ).

We make an IBAction method which will be later connected with the UIButton. We also have to adapt the existing unwindToMainViewController: method to stop an ongoing GPS Simulation.

@interface ViewController ()
...
@property BOOL gpsSimulationIsActive;
@end
@implementation ViewController

...

- (IBAction)unwindToMainViewControllerSimulation:(UIStoryboardSegue*)sender {
    _gpsSimulationIsActive = YES;

    if (_destination != nil ) {
        [self initProgressView];
        [self calcNavigation];

        [[NSDK_JobQueue sharedInstance] pushWithBlock:^{
            [[LocationManagerShared sharedManager] startGPSSimulation];
        }];
    }
}

- (IBAction)unwindToMainViewController:(UIStoryboardSegue*)sender {

    if (_gpsSimulationIsActive) {
        [[LocationManagerShared sharedManager]  stopGPSSimulation];
    }
    _gpsSimulationIsActive = NO;

    if (_destination != nil ) {
        [self initProgressView];
        [self calcNavigation];
    }
}

Open Main.storyboard and Ctrl-Drag the new UIButton to the Exit Icon and choose the unwindToMainViewControllerSimulation: method in the popup menu.

Start the app and have fun watching the CCP following the calculated route!

Autozoom

Manual zoom levels hide many informations while driving. We need good zoom levels according to our current speed and navigation situation. We provide you with good default zoom levels while driving if you add a doAutoZoom call to your NavigationLoop.

...

@implementation NavigationLoop

    ...

    - (void) _run {
      ...

      NSError * error;
      NSDK_NavigationInformation * nsdk_NavigationInformation = [NSDK_Navigation getGuidanceInformation:nsdk_gpsdata error:&error];

      if (error) {
         NSLog(@"getGuidanceInformation returns error:%@", error);
      }

      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];

        // map orientation in driving direction
        [_uiMapView setOrientation:270 + [nsdk_gpsdata getCourse]];

        // ccp in the lower third of the screen
        [_uiMapView setFixPoint: [[NSDK_Position alloc] initWithX:0 y:-39]];


        dispatch_async(dispatch_get_main_queue(), ^{
            [_viewController navigationUpdate:nsdk_NavigationInformation];
        });

        if ([nsdk_NavigationInformation getDestinationReached]){
          [self stopNavigation];
        }
      }
   }

To configure the Autozoom you can have a look at the following NavigationSDK methods.

Config method Description
[setAutoZoomSpeedScales] Set the auto zoom scales for the given speeds for tracking and navigation mode
[setAutoZoomScaleMargins] Set the auto zoom scale margins in 2d and 3d
[setAutoZoomSteppingValues] Set the auto zoom linear approximation stepping values

As you can see in the code above, we also added calls to [NSDK_UIMapView setOrientation:] and [NSDK_UIMapView setFixPoint:] for setting the map orientation in driving direction and display the current car position not in the middle of the map, but in the lower third.