Tracing routes with MapKit

05/22/2012 § 23 Comments


Presenting a map to the user is a common feature of mobile apps. And very often this feature comes with an additional requirement: to trace the route from the current user location to some arbitrary destination. The thing is, most apps accomplish this last requirement by adding a button to the right navigation item that opens up google maps on the browser. But usually this is not the best user experience.

Most developers don’t know this (and I was one of them not too long ago), but it is possible to use the MKMapView to easily render paths between to locations. There isn’t however (for now) any native APIs that magically handle this kind of drawing.

iOS handles routes using MKOverlay objects (just like it handles pins using MKAnnotation). There is a native MKOverlay class called MKPolyline which consists of an array of CLLocationCoordinate2D structures that MKMapView knows how to draw.

The thing is: We know only two locations (coordinates). The current one (our origin) and the place’s location (the destination). AND we need all the coordinates in between these two end locations describing a smooth path following the roads and streets considering traffic and so on, in order to properly create the MKPolyline object and add that to the map.

This is where Google Directions API comes in. Google offers an API (both JSON and XML) that among other options let’s you specify two locations and returns a complex set of information containing all sorts of data, like routes (with alternatives), waypoints, distance and directions (instructions). At first, you might look to the documentation and think that you may need to write a parser, iterate through the structure and grab what you need. That is exactly what you need to do, but not as difficult as it seems. The information we are looking for is available as a string named overview_polyline available under the route tag. Just grab that.

If you are using JSON (the recommended output), there are a lot of third-party libraries out there that represents a JSON string as native data structures such as NSArray, NSDictionary and NSString. Now if you are really lazy (and smart), then you use some sort of library like AFNetworking to handle requests and get for free JSON parsing right on the response callback.

Almost every step of the process is a piece of cake until here. The MapKit has a native overlay view that knows how to display a route. The route is given to you for free and with almost no efforts by Google and AFNetworking provides you automatic parsing of the response Google sent you.

The only remaining detail is: Google Directions API gives us a string representing the route and we need an array of CLLocationCoordinate2D structures.

Fortunately the Encoded Polyline Algorithm Format used by google is fully described in the docs and an Objective-C implementation was made available by Ankit Srivastava on stackoverflow.

For those lazy guys who are in a hurry, good news: There is a code snippet below for every point of our discussion.

(WordPress sucks when it comes to presenting source code, but there is a “View Source” button that lets you copy the code and properly paste it! But just in case you wish to read the code I have also attached a file here 😉

  • Create the Map View
_mapView = [[MKMapView alloc] initWithFrame:self.view.bounds];
_mapView.showsUserLocation = YES;
_mapView.delegate = self;
[self.view addSubview:_mapView];
  • Once you have the current location, define the map region you want to be visible:
MKCoordinateRegion viewRegion = MKCoordinateRegionMakeWithDistance(self. location.coordinate, REGION_SIZE, REGION_SIZE);
MKCoordinateRegion adjustedRegion = [_mapView regionThatFits:viewRegion]; [_mapView setRegion:adjustedRegion animated:NO];
  • Also request Google Directions API to retrieve the route:

AFHTTPClient *_httpClient = [AFHTTPClient clientWithBaseURL:[NSURL URLWithString:@"http://maps.googleapis.com/"]];
[_httpClient registerHTTPOperationClass: [AFJSONRequestOperation class]];

NSMutableDictionary *parameters = [[NSMutableDictionary alloc] init];
[parameters setObject:[NSString stringWithFormat:@"%f,%f", location.coordinate.latitude, location.coordinate.longitude] forKey:@"origin"];
[parameters setObject:[NSString stringWithFormat:@"%f,%f", endLocation.coordinate.latitude, endLocation.coordinate.longitude] forKey:@"destination"];
[parameters setObject:@"true" forKey:@"sensor"];

NSMutableURLRequest *request = [_httpClient requestWithMethod:@"GET" path: @"maps/api/directions/json" parameters:parameters];
request.cachePolicy = NSURLRequestReloadIgnoringLocalCacheData;

AFHTTPRequestOperation *operation = [_httpClient HTTPRequestOperationWithRequest:request success:^(AFHTTPRequestOperation *operation, id response) {
	NSInteger statusCode = operation.response.statusCode;
	if (statusCode == 200) {
	 [self parseResponse:response];

	} else {

	}
} failure:^(AFHTTPRequestOperation *operation, NSError *error) { }];

[_httpClient enqueueHTTPRequestOperation:operation];

  • Get what you need:
- (void)parseResponse:(NSDictionary *)response {
 NSArray *routes = [response objectForKey:@"routes"];
 NSDictionary *route = [routes lastObject];
 if (route) {
 NSString *overviewPolyline = [[route objectForKey: @"overview_polyline"] objectForKey:@"points"];
 _path = [self decodePolyLine:overviewPolyline];
 }
}
  • And use the code provided by Ankit Srivastava:
-(NSMutableArray *)decodePolyLine:(NSString *)encodedStr {
 NSMutableString *encoded = [[NSMutableString alloc] initWithCapacity:[encodedStr length]];
 [encoded appendString:encodedStr];
 [encoded replaceOccurrencesOfString:@"\\\\" withString:@"\\"
 options:NSLiteralSearch
 range:NSMakeRange(0, [encoded length])];
 NSInteger len = [encoded length];
 NSInteger index = 0;
 NSMutableArray *array = [[NSMutableArray alloc] init];
 NSInteger lat=0;
 NSInteger lng=0;
 while (index < len) {
 NSInteger b;
 NSInteger shift = 0;
 NSInteger result = 0;
 do {
 b = [encoded characterAtIndex:index++] - 63;
 result |= (b & 0x1f) << shift;
 shift += 5;
 } while (b >= 0x20);
 NSInteger dlat = ((result & 1) ? ~(result >> 1) : (result >> 1));
 lat += dlat;
 shift = 0;
 result = 0;
 do {
 b = [encoded characterAtIndex:index++] - 63;
 result |= (b & 0x1f) << shift;
 shift += 5;
 } while (b >= 0x20);
 NSInteger dlng = ((result & 1) ? ~(result >> 1) : (result >> 1));
 lng += dlng;
 NSNumber *latitude = [[NSNumber alloc] initWithFloat:lat * 1e-5];
 NSNumber *longitude = [[NSNumber alloc] initWithFloat:lng * 1e-5];

CLLocation *location = [[CLLocation alloc] initWithLatitude:[latitude floatValue] longitude:[longitude floatValue]];
 [array addObject:location];
 }

return array;
}
  • Create the MKPolyline annotation:
NSInteger numberOfSteps = _path.count;

CLLocationCoordinate2D coordinates[numberOfSteps];
for (NSInteger index = 0; index < numberOfSteps; index++) {
 CLLocation *location = [_path objectAtIndex:index];
 CLLocationCoordinate2D coordinate = location.coordinate;

 coordinates[index] = coordinate;
}

MKPolyline *polyLine = [MKPolyline polylineWithCoordinates:coordinates count:numberOfSteps];
[_mapView addOverlay:polyLine];
  • And make it visible on the map view:
- (MKOverlayView *)mapView:(MKMapView *)mapView viewForOverlay:(id <MKOverlay>)overlay {
 MKPolylineView *polylineView = [[MKPolylineView alloc] initWithPolyline:overlay];
 polylineView.strokeColor = [UIColor redColor];
 polylineView.lineWidth = 1.0;

 return polylineView;
}

Please note the code snippets provided on this post doesn’t have any error handling neither are optimized. Remember to fix these issues before copying them to your application.

Tagged: , , , , , , , , , ,

§ 23 Responses to Tracing routes with MapKit

  • david says:

    Hi,

    I’ve tried the code and I’m getting an exception.
    The problem is in the method parseResponse

    – (void)parseResponse:(NSDictionary *)response {
    NSArray *routes = [response objectForKey:@”routes”];
    NSDictionary *route = [routes lastObject];
    if (route) {
    NSString *overviewPolyline = [[route objectForKey: @”overview_polyline”] objectForKey:@”points”];
    _path = [self decodePolyLine:overviewPolyline];
    }
    }

    sending the message objectForKey to the response object causes an “unrecognized selector sent to instance”

    I’ve tried to copy and paste the URL called by the application into a browser and seems to receive a correct answer. Here is a link FYI.
    http://maps.googleapis.com/maps/api/directions/json?sensor=true&destination=41.380000%2C2.180000&origin=41.380000%2C2.120000

    Any help will be much appreciated.

    • Hi David,

      in this tutorial I was using AFNetworking and JSONKit.

      AFNetworking automatically locates and uses JSONKit (and some other known JSON parsers) when a request is made (through the AFNetworking API).

      If you did that, the response object should be an NSDictionary. Otherwise it can be a string or a HTTPResponse object of some kind.

      Greetings,
      Cezar

      • david says:

        Thanks for the quick response. I did not do it before. I have placed the JSONKit files into my project, but the error keeps happening 😦

      • david says:

        Just to let you know, it worked after I added:

        [_httpClient setDefaultHeader:@”Accept” value:@”application/json”];

        right after the line:
        [_httpClient registerHTTPOperationClass: [AFJSONRequestOperation class]];

  • is this example stored some where? It will be easy for the new babies for learning thanks.

  • Jesus says:

    Please help me, the code runs without errors or warnings but it don’t display route. Where do i put function map view?

    • the mapView is not a function, but an object. You should add it as a subview of your controller (generally).

      • Jesus says:

        Ok, the map and coordinates are displayed fine, but the route is not displayed and I don’t know why could you help me?
        I’m trying to learn about IOS doing some working code.

  • Miquel says:

    Just writing to thank you (and Ankit Srivastava)!

    It helped me a lot and it just worked. As a side note, I used MKNetworkKit instead of AFNetworking, but both look pretty similar (at least the code related to requesting data to the Google API).

  • Thank you!, but, what about the Usage Limits (https://developers.google.com/maps/documentation/directions/#Limits)?, when Google says: “Note: the Directions API may only be used in conjunction with displaying results on a Google map; using Directions data without displaying a map for which directions data was requested is prohibited.”, so, what up with iOS6 and Apple Maps?

    • Hi Juan,

      You are – unfortunately – right. According to the Directions API’s usage limits, you shouldn’t use it with iOS 6.

      (I wrote this post before iOS 6 was released =)

    • kineticgravatar says:

      I know this post is old, but I wanted to mention that before anyone out there gets discouraged when they read that the data has to be used with Google Maps, there is now the Google Maps SDK for iOS, which has polylines too!

      https://developers.google.com/maps/documentation/ios/lines

      • The Terms of service do not allow you to draw lines to present routes or turn by turn navigation :
        https://developers.google.com/maps/terms
        (e) No Modification of Links. You must not modify, replace, obscure, or otherwise hinder the functioning of links to Google or third party websites provided in the Content. For the avoidance of doubt, titles for place results must link to the applicable URL provided in the result, unless the title is intended to be selected only for purposes of navigation by an end user accessing your Maps API Implementation from a device with appropriately space-constraining user interface options. In these cases, the title linking to the Google-provided URL must be displayed as the top and primary link on the subsequent landing page or user interface component.

      • I am not sure I quite followed your thoughts. What those Link restrictions have to do with drawing routes?

  • Codec Major says:

    Thank you so much!!
    Very useful 😀

  • shengjia says:

    Hi,

    I have to mention that there is an error from method “decodePolyline” by Ankit Srivastava.

    We should not do the code following:

    [encoded replaceOccurrencesOfString:@”\\\\” withString:@”\\”
    options:NSLiteralSearch
    range:NSMakeRange(0, [encoded length])];

  • prag says:

    Very informative article..
    Does the JSON response contains traffic information.If yes, Please tell me the Key to extract it.
    Thanks

  • is this even legal to draw directions on maps in iOS apps using googles api ?
    (c) No Navigation, Autonomous Vehicle Control, or Enterprise Applications. You must not use the Service or Content with any products, systems, or applications for or in connection with any of the following:

    (i) real time navigation or route guidance, including but not limited to turn-by-turn route guidance that is synchronized to the position of a user’s sensor-enabled device.
    From TOS – Google’s Map API

Leave a reply to Cezar Augustus Signori Cancel reply

What’s this?

You are currently reading Tracing routes with MapKit at iOS Guy.

meta