Skip to content

09 Alternatives

Overview

This tutorial will show you how to calculate a route with two alternative route proposals.

Base

You can use your results from the previous Tutorial08 as a base for our new project.

Route overview

In this tutorial we implement a new UIViewController with name RouteInfoViewController showing the complete route and some metrics. The RouteInfoViewController will be displayed right after the route calculation.

Since we like to show our route with two alternative routes, we put three RouteInfoViewController into a UITabBarController. This enables the user to switch between the alternatives.

Enabling alternative routes

For enabling alternative routes in the SDK, you have to set the appropriate RouteCalculationType NSDK_ROUTE_ALTERNATIVE. You can do this in your AppDelegate:

@implementation AppDelegate

...

- (void) setRouteOptions {
    NSDK_RouteOptions * options = [NSDK_Navigation getRoutingOptions:nil];

    ...

    [options setRouteCalculationType:NSDK_ROUTE_ALTERNATIVE];

Handling alternative routes

In this tutorial we show a mechanism to switch between all given alternatives. The following SDK methods are helpful for us:

  • [NSDK_Navigation getAlternativeRouteCount:] - receive the amount of alternative routes (including the default, currently <= 3)
  • [NSDK_Navigation getAlternativeRouteActive:] - receive the currently active alternative route
  • [NSDK_Navigation setAlternativeRouteActive:error:] - set the active alternative route (also highlights the active route trace on the map)
  • [NSDK_Navigation getRouteInformationWithAlternative:error:] - obtain the RouteInformation for the specified alternative route
  • [NSDK_Navigation getTrafficDelayOnRouteWithCompleteRoute:alternativeRouteId:error:] - obtain the delay caused by traffic on the selected alternative route

Because there is a lot of boilerplate UI Code in this tutorial, we show and describe only excerpts of the code we use in the Tutorial09 XCode project. Therefore we recommend to copy these files to your project:

RouteInfoTabBarController.m, RouteInfoTabBarController.h
RouteInfoViewController.m, RouteInfoViewController.h

Furthermore it is recommended that you copy the complete "RouteInfoController" and "Route Info Tab Bar Controller" scene (including the three attached Navigation Controller) from the Tutorial09 XCode sample project storyboard to your storyboard.

Your storyboard should look like in the following picture:

Set the "Custom class" of the "RouteInfoController" scene to "RouteInfoViewController" and of the "Route Info Tab Bar Controller" scene to "RouteInfoTabBarController"

Create a Segue with name segueToRouteInfoTabBar between the ViewController and the UITabBarController. In our [ViewController calcNavigation]method we no longer start the Navigation instantly, but show the UITabBarController with the alternative routes. The following code triggers the aforementioned segue.

@implementation ViewController
...
- (void) showRouteInfoController {
    [self performSegueWithIdentifier: @"segueToRouteInfoTabBar" sender: self];
}

- (void) calcNavigation {

    NSDK_JobQueue * jobQueue = [NSDK_JobQueue sharedInstance];
    [jobQueue pushWithBlock:^{

        NSDK_WayPoint * start = [Helper getCurrentWayPointWithFallbackKarlsruhe];
        NSDK_WayPoint *  end = [[NSDK_WayPoint alloc] initWithX:[_destination  getX] y:[_destination getY]];

        NSError * err = NULL;
        NSDK_Tour * tour = [[NSDK_Tour alloc] initWithWayPoint:start];
        [tour addStation:end error:nil];

        [NSDK_Navigation calculateTour: tour observer:self error:&err];
        if (err) {
            NSLog(@"ViewController calculateTour NSError returns:%@", err);
        } else {
            //call 'searchTrafficInformation' here, because in the RouteInfoController we want to get the traffic delay
            NSDK_Position * startPosition = [[NSDK_Position alloc] initWithX:[start getX] y:[start getY]];
            [NSDK_Navigation searchTrafficInformationWithPosition:startPosition observer:nil error:&err];
            if (err) {
                NSLog(@"ViewController searchTrafficInformationWithPosition NSError returns:%@", err);
            }

            dispatch_async(dispatch_get_main_queue(), ^{
                [self showRouteInfoController];
            });
        }
    }];
}

As you can see in the code above, we added a call to [NSDK_Navigation searchTrafficInformationWithPosition:]. That's because we have successfully calculated a route a few lines before and the SDK is so smart to fetch the traffic situation for the calculated route.

Create a new IBAction method for actually starting the navigation:

@implementation ViewController
...
- (IBAction)unwindToMainViewControllerStartNavigation:(UIStoryboardSegue*)sender {
    if (_gpsSimulationIsActive) {
        [[LocationManagerShared sharedManager] startGPSSimulation];
    }
    [_navigationLoop startNavigation];
}

In the RouteInfoViewController scene in the storyboard we have connected a button (the button with the green arrow) with this method (again through the "Exit" Icon in Interface Builder).

We have to revise our unwindToMainViewController and unwindToMainViewControllerSimulation methods a bit. That's because the iOS runtime often runs in View hierarchy issues when we switch to another UIViewController (or call a segue) within these methods. Therefore we just set a flag that the navigation calculation should be started (and after the calculation is finished, the segue to the RouteInfoViewController should be triggered) - and actually check the flag and do the work in our viewDidAppear method.

@interface ViewController ()
...
@property BOOL shallCalcNavigation;
@end

@implementation ViewController
...
- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];

    ...

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


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


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

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

Attention

Don't call [NSDK_Navigation getGuidanceInformation] before the user has chosen the alternative route. If you do nevertheless, the SDK will discard the alternative routes!

We stop our NavigationLoop when we leave our ViewController. We can stop the loop whenever a Segue will be performed:

@implementation ViewController

...

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
    [_navigationLoop stopNavigation];
}

Now connect the UILabels and the UIMapView of the RouteInfoViewController scene to the corresponding IBOutlets in the header file. So Ctrl-Drag to the appropriate property.

#import <UIKit/UIKit.h>
#import "navicoreFramework/NSDK_Navigation.h"
#import "UIMapView.h"


@interface RouteInfoViewController : UIViewController

@property NSInteger routeAlternativeId;

@property (weak, nonatomic) IBOutlet UIMapView *uiMapView;
@property (weak, nonatomic) IBOutlet UILabel *labelDestinationDist;
@property (weak, nonatomic) IBOutlet UILabel *labelDestinationTime;
@property (weak, nonatomic) IBOutlet UILabel *labelTrafficDelay;
@property (weak, nonatomic) IBOutlet UILabel *labelToll;

@end

In RouteInfoViewController we show from left to right

  • The distance to destination
  • The estimated time of arrival
  • The delay caused by traffic
  • The covered distance on toll roads

All this information about the current route can be obtained by calling [NSDK_Navigation getRouteInformation:]. The [NSDK_RouteInformation] class can give you several infos like distance, duration, toll information and restrictions on your current route. To access the traffic delay, we simply call [NSDK_Navigation getTrafficDelayOnRouteWithCompleteRoute:alternativeRouteId:error:] . Like in Tutorial06 we format the distances with [NSDK_RouteInformation getLocalizedDistanceString:quantify:error:].

@implementation RouteInfoViewController

- (void) viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    NSLog(@"RouteInfoViewController routeAlternativeId:%ld viewWillAppear called", (long)_routeAlternativeId);
    [NSDK_Navigation setAlternativeRouteActive:(int)_routeAlternativeId error:nil];
    [self setLabelText];
}

- (void) setLabelText {
    NSError * error;
    NSDK_RouteInformation * routeInformation = [NSDK_Navigation getRouteInformationWithAlternative:_routeAlternativeId error:&error];

    //Distance
    NSString * distanceString = [NSDK_Navigation getLocalizedDistanceString:[routeInformation getLength] quantify:NO error:&error];
    _labelDestinationDist.text = distanceString;

    //Arrival time
    NSDate * date = [NSDate dateWithTimeIntervalSinceNow:[routeInformation getDuration]];
    NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
    [formatter setDateFormat:@"HH:mm"];
    NSLog(@"RouteInfoViewController RouteInformation arrival date:%@", [formatter stringFromDate:date]);
    _labelDestinationTime.text = [formatter stringFromDate:date];

    //traffic delay
    int trafficTime = [NSDK_Navigation getTrafficDelayOnRouteWithCompleteRoute:YES alternativeRouteId:_routeAlternativeId error:&error];
    int hours = trafficTime / 3600;
    //remaining seconds
    trafficTime = trafficTime - hours * 3600;
    int minutes = trafficTime / 60;
    NSString *trafficDelay = [NSString stringWithFormat:@"+%dh %dmin", hours, minutes];
    _labelTrafficDelay.text = trafficDelay;
    NSLog(@"RouteInfoViewController TrafficDelay hours:%d minutes:%d", hours, minutes);

    //toll length
    int tollLength = [[routeInformation getTollInformation] getTotalLengthTruckToll];
    _labelToll.text = [NSDK_Navigation getLocalizedDistanceString:tollLength quantify:NO error:&error];
}

...
As you can see, we call the setLabelText method from the viewWillAppear method, not from the viewDidLoad method. That's because we want to respect the freshest traffic delay information whenever the user switches between the different alternative routes within the UITabBarController. The viewDidLoad method is called only once by the runtime, no matter how often the user switches the view within the UITabBarController.

Furthermore we set the active route in the SDK with the call to [NSDK_Navigation setAlternativeRouteActive:error:].

And last, here is the essential code for instantiating and initializing the RouteInfoViewControllers within the RouteInfoTabBarController:

@implementation RouteInfoTabBarController
...

- (void)viewDidLoad {
    [super viewDidLoad];

    int routeCount = [NSDK_Navigation getAlternativeRouteCount:nil];

    NSMutableArray *tabbarViewControllers = [NSMutableArray arrayWithArray: [self viewControllers]];
    if (routeCount < 3) {
        [tabbarViewControllers removeObjectAtIndex:2];
    }
    if (routeCount < 2) {
        [tabbarViewControllers removeObjectAtIndex:1];
    }

    [self setViewControllers: tabbarViewControllers ];

    [self setRouteIdOnController:0];
    [self setRouteIdOnController:1];
    [self setRouteIdOnController:2];
}


- (void) setRouteIdOnController:(int) idx {

    if (idx >= [[self viewControllers] count]) {
        return;
    }

    NSError * error;
    NSDK_RouteInformation * routeInformation = [NSDK_Navigation getRouteInformationWithAlternative:idx error:&error];

    int seconds = [routeInformation getDuration];
    int hour = seconds / 3600;
    int mins = (seconds % 3600) / 60;
    NSString * str = [NSString stringWithFormat:@"%dH %dMIN", hour, mins];

    UITabBarItem * item = [self.tabBar.items objectAtIndex:idx];
    [item setTitle:NSLocalizedString(str, @"comment")];
    [item setTitlePositionAdjustment:UIOffsetMake(0, -10)];
    [item setTitleTextAttributes:[NSDictionary dictionaryWithObjectsAndKeys:
                                            [UIFont boldSystemFontOfSize:16], UITextAttributeFont, nil]
                                  forState:UIControlStateNormal];

    //add RouteInfoViewController to NavigationController
    RouteInfoViewController * routeInfoViewController = [self.storyboard instantiateViewControllerWithIdentifier:@"routeInfoControllerId"];
    UINavigationController * navigationController = (UINavigationController*)[[self viewControllers] objectAtIndex:idx];
    [navigationController addChildViewController:routeInfoViewController];

    routeInfoViewController.routeAlternativeId = idx;
}

When the SDK delivers less than three routes, we have to check the actual number of routes and remove the superfluous Controller from the UITabBarController. Remember that we initially defined 3 child controllers (UINavigationController) for the UITabBarController.

This is our final result:

Implications on navigation

Starting a navigation with an alternative route (i.e. bigger than 0) has implications to navigation. Because each of the alternative routes is not the arithmetical optimal route, there is a risk that rerouting will switch back to the optimal route. Therefore a special mechanism is implemented: The chosen route will be stored during the whole navigation. Its underlying segments get a small preference while rerouting. This should lead to a good trade-off finding reasonable reroutes, but also most likely keeps the chosen alternative route.