Sorry, you need to enable JavaScript to visit this website.

Search Along a Route

Overview

This tutorial shows how to use the TomTom Maps SDK for iOS to create an application that helps a user find points of interest along a planned route.

It shows how to use:

  • The TomTom Map SDK module to display a map, including markers with custom icons and balloons.
  • The TomTom Routing SDK module to plan routes with and without waypoints.
  • The TomTom Search SDK module to search for points of interest (POIs) and to geocode map positions.

An end user can start interacting with the application by planning a route with departure and destination points:

  • One long press on the map sets a departure point.
  • A second long press sets a destination point and draws a route between those two points on the map.

When the route is visible on the map, the user can type a POI name or category into a search field or click on any of the predefined POI category buttons (gas station, restaurant, ATM). The map displays markers for POIs that match the user's request. The user can add one of the displayed POIs to their planned route by tapping the marker on the map and choosing the "Add to my route" button inside the marker balloon that is then displayed. The route is recalculated and redrawn to include the selected point.

Prerequisites

  1. Make sure that the CocoaPods dependency manager is installed on your computer:https://cocoapods.org.
  2. Create a new Single View App project named SearchAlongARoute.
  3. Go to the console and create a pod file by executing a pod init command.
  4. Copy and paste SDK module dependencies into the newly created Podfile:
    pod 'TomTomOnlineSDKMaps', ‘2.3.15’
    pod 'TomTomOnlineSDKSearch', ‘2.3.15’
    pod 'TomTomOnlineSDKRouting', ‘2.3.15’
    pod 'TomTomOnlineSDKMapsUIExtensions', ‘2.3.15’

    Install the SDK modules by typing pod install from the console. After this is finished close your project and open *.xcworkspace file with XCode.

  5. If you don't have an API key follow these steps:

    Create an API key now

    To create a new API key, first log in or register on the portal.

    Next, create a new application in your Dashboard:

    Choose all the APIs:

    You are now set up. Click on your newly created app and copy your API key:

  6. Copy and paste your API key into a Info.plistfile.
    <key>OnlineMap.Key</key>
    <string>YOUR_KEY_GOES_HERE</string>
    <key>OnlineSearch.Key</key>
    <string>YOUR_KEY_GOES_HERE</string>
    <key>OnlineRouting.Key</key>
    <string>YOUR_KEY_GOES_HERE</string>

Map initialization

To initialize a TomTom map add a View object to a Main.storyboard file and configure it with a custom class named TTMapView.

Run your application. You should see a map.

The TomTom map handles zooming, panning, rotating and double tapping gestures. In this application you need to add additional map interactions including location handling, long map press events, drawing routes and markers.

Now add #import statements inside the ViewController.m file:

#import <TomTomOnlineSDKSearch/TomTomOnlineSDKSearch.h>
#import <TomTomOnlineSDKRouting/TomTomOnlineSDKRouting.h>
#import <TomTomOnlineSDKMapsUIExtensions/TomTomOnlineSDKMapsUIExtensions.h>

Your ViewController class must conform to the protocols:

  • TTMapViewDelegate
  • TTAnnotationDelegate
  • TTAlongRouteSearchDelegate

Add an initTomTomServices method to the ViewController class where the Maps API modules are initialized. At the same time, add a initUIViews method where User Interface (UI) elements are initialized.

#import "ViewController.h"
#import <TomTomOnlineSDKSearch/TomTomOnlineSDKSearch.h>
#import <TomTomOnlineSDKRouting/TomTomOnlineSDKRouting.h>
#import <TomTomOnlineSDKMapsUIExtensions/TomTomOnlineSDKMapsUIExtensions.h>

@interface ViewController() <TTMapViewDelegate, TTAnnotationDelegate, TTAlongRouteSearchDelegate>

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    [self initTomTomServices];
    [self initUIViews];
}

- (void)initTomTomServices {
}

- (void)initUIViews {
}

@end

Add a private property for the TomTom map inside the ViewController interface:

@property(weak,nonatomic) IBOutlet TTMapView* tomtomMap;

and connect it with the added earlier TTMapView from the Main.storyboard file.

Drawing a route on the map

Drawing a route on the map requires that the following additional properties be defined in the ViewController interface:

  • TTRoute
  • TTReverseGeocoder
  • TTAlongRouteSearch – used in later part of this tutorial
  • Departure coordinates
  • Destination coordinates
  • Waypoint coordinates
  • TTFullRoute
@property(strong,nonatomic) TTRoute*route;
@property(strong,nonatomic) TTReverseGeocoder *reverseGeocoder;
@property(strong,nonatomic) TTAlongRouteSearch *alongRouteSearch;
@property TTFullRoute*fullRoute;
@property CLLocationCoordinate2D departurePosition;
@property CLLocationCoordinate2D destinationPosition;
@property CLLocationCoordinate2D wayPointPosition;
@property TTAnnotationImage* departureImage;

Initialize required fields and services inside the initTomTomServices method. Your ViewController class is used here as a delegate receiving an event notification from the TomTom Map, Routing and Search services.

- (void)initTomTomServices {
    self.tomtomMap.delegate = self;
    self.tomtomMap.annotationManager.delegate = self;
    self.reverseGeocoder = [[TTReverseGeocoder alloc] init];
    self.route = [[TTRoute alloc] init];
    self.alongRouteSearch = [[TTAlongRouteSearch alloc] init];
    self.alongRouteSearch.delegate = self;

    self.departurePosition = kCLLocationCoordinate2DInvalid;
    self.destinationPosition = kCLLocationCoordinate2DInvalid;
    self.wayPointPosition = kCLLocationCoordinate2DInvalid;
}

Initialize an icon for a departure map annotation inside the initUIViews method.You also need to add an icon named ic_map_route_departure to the Assets.xcassets directory.

self.departureImage = [TTAnnotationImage createPNGWithName:@"ic_map_route_departure"];

You need a clearMap function, where all the markers and the route are removed from the map.

- (void)clearMap {
    [self.tomtomMap.routeManager removeAllRoutes];
    [self.tomtomMap.annotationManager removeAllAnnotations];
    self.departurePosition = kCLLocationCoordinate2DInvalid;
    self.destinationPosition = kCLLocationCoordinate2DInvalid;
    self.wayPointPosition = kCLLocationCoordinate2DInvalid;
    self.fullRoute = nil;
}

Now add an implementation to the method didLongPress that handles long press events on the map.

- (BOOL)isDestinationPositionSet {
    return CLLocationCoordinate2DIsValid(self.destinationPosition);
}

- (BOOL)isDeparturePositionSet {
    return CLLocationCoordinate2DIsValid(self.departurePosition);
}

- (void)mapView:(TTMapView *)mapView didLongPress:(CLLocationCoordinate2D)coordinate {
    if ([self isDeparturePositionSet] && [self isDestinationPositionSet]) {
        [self clearMap];
    } else {
        [self handleLongPress:coordinate];
    }
}

- (void)handleLongPress:(CLLocationCoordinate2D)coordinate {
    TTReverseGeocoderQuery *query = [[TTReverseGeocoderQueryBuilder createWithCLLocationCoordinate2D:coordinate] build];
    
    [self.reverseGeocoder reverseGeocoderWithQuery:query completionHandle:^(TTReverseGeocoderResponse *response, TTResponseError *error) {
        if (response.result.addresses.count > 0) {
            TTReverseGeocoderFullAddress *firstAddress = response.result.addresses.firstObject;
            NSString *address = firstAddress.address.freeformAddress ? firstAddress.address.freeformAddress : @"  ";
            [self processGeocoderResponse:firstAddress.position address:address];
        }
    }];
}

- (void)processGeocoderResponse:(CLLocationCoordinate2D)geocodedPosition address:(NSString *)address {
    if (!CLLocationCoordinate2DIsValid(self.departurePosition)) {
        self.departurePosition = geocodedPosition;
        [self createAndDisplayMarkerAtPosition:self.departurePosition withAnnotationImage:self.departureImage andBalloonText:address];
    } else {
        self.destinationPosition = geocodedPosition;
        [self drawRouteWithDeparture:self.departurePosition andDestination:self.destinationPosition];
    }
}

The didLongPress function calls a reverseGeocoder's reverseGeocoderWithQuery method. This method checks if a road can be found in the place where a long press is registered on the map.

If the reverse geocoding call is successful and contains valid results, the effect of the long press depends on context:

  • The first long press on the map sets the departurePosition object.
  • A second long press sets the destinationPosition object and draws a route on the map.
  • A third long press removes any destination and departure markers and the route from the map.

Next add functions to send a route request to the Routing API and to draw a route from the response on the map.

- (void)drawRouteWithDeparture:(CLLocationCoordinate2D)departure andDestination:(CLLocationCoordinate2D)destination {
    [self drawRouteWithDeparture:departure andDestination:destination andWayPoint:kCLLocationCoordinate2DInvalid];
}

- (void)drawRouteWithDeparture:(CLLocationCoordinate2D)departure andDestination:(CLLocationCoordinate2D)destination andWayPoint:(CLLocationCoordinate2D)wayPoint {
    TTRouteQuery *query = [self createRouteQueryWithOrigin:departure andDestination:destination andWayPoint:wayPoint];
    [self.route planRouteWithQuery:query completionHandler:^(TTRouteResult *result, TTResponseError *error) {
        if (result.routes.count > 0) {
            [self addActiveRouteToMap:result.routes.firstObject];
        }
    }];
}
- (TTRouteQuery *)createRouteQueryWithOrigin:(CLLocationCoordinate2D)origin andDestination:(CLLocationCoordinate2D)destination andWayPoint:(CLLocationCoordinate2D)wayPoint {
    TTRouteQueryBuilder *builder = [TTRouteQueryBuilder createWithDest:destination andOrig:origin];
    if (CLLocationCoordinate2DIsValid(wayPoint)) {
        [builder withWayPoints:@[[NSValue value:&wayPoint withObjCType:@encode(CLLocationCoordinate2D)]]];
    }
    return [builder build];
}

- (void)addActiveRouteToMap:(TTFullRoute *)route {
    [self.tomtomMap.routeManager removeAllRoutes];
    self.fullRoute = route;
    if (!CLLocationCoordinate2DIsValid(self.wayPointPosition)) {
        [self.tomtomMap.annotationManager removeAllAnnotations];
    }
    TTMapRoute *mapRoute = [TTMapRoute routeWithCoordinatesData:self.fullRoute imageStart:TTMapRoute.defaultImageDeparture imageEnd:TTMapRoute.defaultImageDestination];
    [self.tomtomMap.routeManager addRoute:mapRoute];
    mapRoute.active = YES;
}

The createRouteQuery method returns a routeQuery object:

  • With additional waypoints (if the wayPoint field is a valid 2D coordinate).
  • Without additional waypoints (if the wayPoint field is not a valid 2D coordinate).

The drawRouteWithDeparture method calls the Routing API. If the response is successful, an active route is drawn on the map.

Add a createAndDisplayMarkerAtPosition method to display a departure position marker if the destination position is not set:

- (void)createAndDisplayMarkerAtPosition:(CLLocationCoordinate2D)coords withAnnotationImage:(TTAnnotationImage *)image andBalloonText:(NSString *)text {
    self.positionsPoisInfo[[self coordinatesToString:coords]] = text;
    [self.tomtomMap.annotationManager addAnnotation:[TTAnnotation annotationWithCoordinate:coords annotationImage:image anchor:TTAnnotationAnchorCenter type:TTAnnotationTypeFocal]];
}

Now add dictionary property where you can store coordinates with a balloon text to display when any map marker is tapped.

@property NSMutableDictionary *positionsPoisInfo;

Make sure that you initialize the dictionary inside the initUIViews method:

self.positionsPoisInfo = [[NSMutableDictionary alloc] init];

Now you can draw the route on the map by using long presses in chosen locations.

Searching for POIs along the route

Modify the Main.storyboard file by adding a SearchBar control. Add constraints to the SearchBar control to make sure that it will be displayed correctly on all of the devices. Make sure that the Top Space constraint is connected to the TomtomMap View.

Now add constraints to the TomtomMap View. You only need to add constraints to the top, left and right sides since a bottom space is already constrained to search bar.

In the next step you need to make sure that the SearchBar which you just added is always above a keyboard when it is visible.

First add two constant fields above the interface declaration in the ViewController.m file:

static const int KEYBOARD_SHOW_MULTIPLIER = -1;
static const int KEYBOARD_HIDE_MULTIPLIER = 1;

and two properties inside the ViewController class:

@property(weak, nonatomic) IBOutlet NSLayoutConstraint *bottomConstraint;
@property Boolean keyboardShown;

A bottomConstraint property is adjusted with the size of the keyboard shown on the phone screen when the user starts typing inside the SearchBar. A keyboardShown property is used to store information if keyboard is currently visible.

Make sure that the bottomConstraint property field is connected with a SearchBar bottom constraint from the Main.storyboard file.

Next add an initKeyboardNotificationEvents method where keyboard events are assigned to methods executed whenever a specific event is triggered.

- (void)initKeyboardNotificationEvents {
    NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
    [nc addObserver:self selector:@selector(keyboardWillShow:) name:UIKeyboardWillShowNotification object:nil];
    [nc addObserver:self selector:@selector(keyboardWillHide:) name:UIKeyboardWillHideNotification object:nil];
    [nc addObserver:self selector:@selector(keyboardDidShow:) name:UIKeyboardDidShowNotification object:nil];
    [nc addObserver:self selector:@selector(keyboardDidHide:) name:UIKeyboardDidHideNotification object:nil];
}

Execute the initKeyboardNotificationEvents inside the viewDidLoad method.

[self initKeyboardNotificationEvents];

Add required methods:

- (void)keyboardWillShow:(NSNotification *)notification {
    if (!self.keyboardShown) {
        [self adjustHeight:YES withNotification:notification];
    }
}

- (void)keyboardWillHide:(NSNotification *)notification {
    if (self.keyboardShown) {
        [self adjustHeight:NO withNotification:notification];
    }
}

- (void)keyboardDidShow:(NSNotification *)notification {
    self.keyboardShown = YES;
}

- (void)keyboardDidHide:(NSNotification *)notification {
    self.keyboardShown = NO;
}

- (void)adjustHeight:(Boolean)show withNotification:(NSNotification *)notification {
    NSDictionary *userInfo = [notification userInfo];
    CGRect keyboardFrame = [userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
    NSTimeInterval animationDuration = [userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
    CGFloat changeInHeight = CGRectGetHeight(keyboardFrame) * (show ? KEYBOARD_SHOW_MULTIPLIER : KEYBOARD_HIDE_MULTIPLIER);
    [UIView animateWithDuration:animationDuration animations:^{
        self.bottomConstraint.constant += changeInHeight;
    }];
}

Methods keyboardDidShow and keyboardDidHide are used to set the keyboardShown field with a Boolean value holding information about a current keyboard state. Inside a keyboardWillShow and a keyboardWillHide methods, the keyboard height is adjusted with the value of a keyboard frame rectangle height.

Optionally implement a single tap on the map event, where you can hide keyboard if it is visible:

- (void)mapView:(TTMapView *_Nonnull)mapView didSingleTap:(CLLocationCoordinate2D)coordinate {
    [self.view endEditing:YES];
}

Add a UISearchBarDelegate protocol declaration to the ViewController interface inside the ViewController.m file and a SearchBar outlet as property inside this interface. Connect your outlet property with the SearchBar from the Main.storyboard file.

@property(weak,nonatomic) IBOutlet UISearchBar *searchBar;

Designate a current ViewController object as a delegate for the newly added searchBar property by adding this line inside the initUIViews method:

self.searchBar.delegate = self;

This will allow you to receive and react to the searchBar event notifications. Implement a method searchBarSearchButtonClicked to be executed whenever a search bar search button is pressed.

In the next step add implementation of the method executed whenever the search bar button is clicked.

- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar {
    if (self.fullRoute) {
        [self searchAlongTheRoute:self.searchBar.text];
    }
}
- (void)searchAlongTheRoute:(NSString *)searchText {
    TTAlongRouteSearchQuery *alongRouteSearchQuery = [[[[TTAlongRouteSearchQueryBuilder alloc] initWithTerm:searchText withRoute:self.fullRoute withMaxDetourTime:1000] withLimit:10] build];
    [self.alongRouteSearch searchWithQuery:alongRouteSearchQuery];
}

The searchAlongTheRoute function in this object executes a search query for the provided search term along the route visible on the map.

Add a completedWithResponse method executed when a response is received from the TomTom search module. Make sure to declare that your ViewController class conforms to the TTAlongRouteSearchDelegate protocol.

- (void)search:(TTAlongRouteSearch *)search completedWithResponse:(TTAlongRouteSearchResponse *)response {
    [self.tomtomMap.annotationManager removeAllAnnotations];
    [self.positionsPoisInfo removeAllObjects];
    
    for (TTAlongRouteSearchResult *result in response.results) {
        NSString *markerString = [NSString stringWithFormat:@"%@, %@", result.poi.name, result.address.freeformAddress];
        [self createAndDisplayMarkerAtPosition:result.position withAnnotationImage:TTAnnotation.defaultAnnotationImage andBalloonText:markerString];
    }
    [self.tomtomMap zoomToAllAnnotations];
}

The createAndDisplayMarkerAtPosition method then adds a map marker in the position returned by the search query, including the name and address of the POI.

Adding custom marker balloons

Add a new User Interface View file named CustomAnnotationView. Open the newly added CustomAnnotationView.xib file and configure the size of the View component inside this file.

Add labels where a point of interest name and an address can be displayed. Additionally add a button to include point of interest marker to the route displayed on map.

Now create a new Cocoa Touch Class file named CustomAnnotationView as a subclass of UIView. Make sure that your CustomAnnotationView conforms to a TTCalloutView protocol by modifying a CustomAnnotationView.h file.

#import <UIKit/UIKit.h>
#import <TomTomOnlineSDKMaps/TomTomOnlineSDKMaps.h>
@interface CustomAnnotationView : UIView <TTCalloutView>

@end

Configure a custom class for the main View component in the CustomViewController.xib file.

Inside the CustomAnnotationView.h file add a WayPointAddedDelegate protocol with a setWayPoint method defined inside. The class implementing this protocol receives notifications from a button click inside the marker balloon. Additionally define properties and methods inside the CustomAnnotationView interface.

#import <UIKit/UIKit.h>
#import <TomTomOnlineSDKMaps/TomTomOnlineSDKMaps.h>

@protocol WayPointAddedDelegate

- (void) setWayPoint:(TTAnnotation* )annotation;

@end

@interface CustomAnnotationView : UIView <TTCalloutView>

@property(nonatomic, strong) TTAnnotation *annotation;
@property(strong, nonatomic) IBOutlet UILabel *poiName;
@property(strong, nonatomic) IBOutlet UILabel *poiAddress;
@property(strong, nonatomic) id myDelegate;
- (IBAction)addWayPoint:(id)sender;

@end

Connect outlets with previously added labels and the addWayPointmethod for the Touch up inside action performed on the add waypoint button.

Now add an implementation of the CustomAnnotationView class inside the CustomAnnotationView.m file

#import "CustomAnnotationView.h"

@implementation CustomAnnotationView

- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    self = [super initWithCoder:aDecoder];
    return self;
}

- (IBAction)addWayPoint:(id)sender {
    [self.myDelegate setWayPoint:self.annotation];
}

@end

When a class implements the WayPointAddedDelegate protocol it must define a setWayPoint method which is executed whenever a button inside marker balloon is clicked.

Import CustomAnnotationView.h inside the ViewController.m file.

#import"CustomAnnotationView.h"

Add the WayPointAddedDelegate protocol declaration to the ViewController class and implement a setWaypoint method

- (void)setWayPoint:(TTAnnotation *)annotation {
    self.wayPointPosition = [annotation coordinate];
    [self.tomtomMap.annotationManager deselectAnnotation];
    [self drawRouteWithDeparture:self.departurePosition andDestination:self.destinationPosition andWayPoint:self.wayPointPosition];
}

In the last step overwrite default behavior of the viewForSelectedAnnotation method where custom annotation is created for each point of interest along a route.

- (UIView <TTCalloutView> *)annotationManager:(id<TTAnnotationManager>)manager viewForSelectedAnnotation:(TTAnnotation *)selectedAnnotation {
    NSString *selectedCoordinatesString = [self coordinatesToString:selectedAnnotation.coordinate];
    if ([selectedCoordinatesString isEqualToString:[self coordinatesToString:self.departurePosition]]) {
        return [[TTCalloutViewSimple alloc] init];
    } else {
        return [self createCustomAnnotation:selectedCoordinatesString];
    }
}

- (UIView <TTCalloutView> *)createCustomAnnotation:(NSString *)coordinatesString {
    CustomAnnotationView <TTCalloutView> *customAnnotation = [[NSBundle.mainBundle loadNibNamed:@"CustomAnnotationView" owner:self options:nil] firstObject];
    NSArray *annotationStringArray = [self.positionsPoisInfo[coordinatesString] componentsSeparatedByString:@","];
    customAnnotation.poiName.text = annotationStringArray[0];
    customAnnotation.poiAddress.text = annotationStringArray[1];
    customAnnotation.myDelegate = self;
    return customAnnotation;
}

- (NSString *)coordinatesToString:(CLLocationCoordinate2D)coords {
    return [NSString stringWithFormat:@"%@,%@", [@(coords.latitude) stringValue], [@(coords.longitude) stringValue]];
}

Inside the createCustomAnnotation method a CustomAnnotationView object is created, and labels for this object are populated with the information about a found point of interest. Next this annotation is displayed for each marker visible on the map.

Now you should have a fully working application where you can:

  • Display a map.
  • Create a route between 2 points.
  • Display points of interest.
  • Add a single POI to your route.

The additional styling, shortcut buttons, and help screen in the application screenshots are not a part of this tutorial. You can find them, along with all the icons and images used in this tutorial, in the application posted on Github.

Summary

This tutorial explained how to create a sample application that searches for and displays points of interest(POIs) along a route, then recalculates the route to include one of those POIs.

This application can be extended with other TomTom Maps SDK functions, such as displaying information about traffic and travel distances.

Happy coding!

Example application

The full application, including additional layout changes and improvements, is visible below. At the bottom of the screen there are three optional buttons that can be used for quick searches for gas stations, restaurants, and ATMs. There is also a help button in the top right corner along with a clear button to remove the route and any markers from the map.