Drawing polyines or routes on a MKMapView (Using Map Kit on the iPhone)
by Craig on Apr.12, 2009, under Mobile Development, Software Development
Apple recently released the 3.0 Beta of their iPhone SDK. One of the most exciting new items in this SDK for me was the addition of the MapKit framework. This new mapping component would allow developers to add maps to their applications that have similar performance and functionality to the Google Maps application that ships with the iPhone.
Unfortunately, there are a few useful pieces missing from the new map SDK; the most glaring of which (to me) is the built in ability to draw routes on a map. This is easily solved though by placing a custom UIView over the map that acts as the map delegate, and knows how to take a series of CLLocation coordinates and plot them on the map, regardless of the location the user pans to or how far they have zoomed in.
I have included the code of this custom class below, as well as some tips on how to use it. There are two things to keep in mind though when looking at this code:
- It is in no way optimized, meaning the bulk of the drawRect functionality executes, regardless if the polyline being rendered is completely off screen.
- The Map Kit SDK is still in Beta, and is subject to change. This sample was written targeting the second beta of the 3.0 iPhone SDK. I do not anticipate huge changes in the SDK, but future releases may break the code listed below, or (hopefully) make the code below completely uneccessary.
Going through the code, you can see when this new CSMapRouteLayerView is initialized with an array of CLLocations and a MapView, the view adds itself as the subview to the MKMapView and registers as its delegate. It then, based on the points that were passed in, uses the map to determine the region that would result in the full path of the route’s line being displayed. The map is then zoomed to this region that contains the while route.
The drawRect functionality is pretty straightforward; for every geographic point in the array, it uses the map to determine the pixel coordinates of that point, and then draws a line to that point from the previous point (in the case of the first point, it just moves the pen to that location and nothing is drawn till the next point).
One downside to this approach, and you can see this in the MKMapViewDelegate handlers in the route layer view, is that when the user decides to scroll or zoom the map, we must temporarily disable the display of the route. This is because during the transition, the lines will appear to be rendered at the wrong location. As soon as the region is done changing, we can bring our polyline back onto the map.
The sample data used for this project is a CSV file with some Latitude/Longitude pairs. The applications main view controller does the work of opening up this file, parsing out the points, and sending it to the initialization of our route layer view. I have not copied the code for this below, but you can see it if you download the sample project.
Here’s the important code… you can download the sample project here: mapLines Sample Project
//
// CSMapRouteLayerView.h
// mapLines
#import
#import
@interface CSMapRouteLayerView : UIView
{
MKMapView* _mapView;
NSArray* _points;
UIColor* _lineColor;
}
-(id) initWithRoute:(NSArray*)routePoints mapView:(MKMapView*)mapView;
@property (nonatomic, retain) NSArray* points;
@property (nonatomic, retain) MKMapView* mapView;
@property (nonatomic, retain) UIColor* lineColor;
@end
//
// CSMapRouteLayerView.m
// mapLines
#import "CSMapRouteLayerView.h"
@implementation CSMapRouteLayerView
@synthesize mapView = _mapView;
@synthesize points = _points;
@synthesize lineColor = _lineColor;
-(id) initWithRoute:(NSArray*)routePoints mapView:(MKMapView*)mapView
{
self = [super initWithFrame:CGRectMake(0, 0, mapView.frame.size.width, mapView.frame.size.height)];
[self setBackgroundColor:[UIColor clearColor]];
[self setMapView:mapView];
[self setPoints:routePoints];
// determine the extents of the trip points that were passed in, and zoom in to that area.
CLLocationDegrees maxLat = -90;
CLLocationDegrees maxLon = -180;
CLLocationDegrees minLat = 90;
CLLocationDegrees minLon = 180;
for(int idx = 0; idx < self.points.count; idx++)
{
CLLocation* currentLocation = [self.points objectAtIndex:idx];
if(currentLocation.coordinate.latitude > maxLat)
maxLat = currentLocation.coordinate.latitude;
if(currentLocation.coordinate.latitude < minLat)
minLat = currentLocation.coordinate.latitude;
if(currentLocation.coordinate.longitude > maxLon)
maxLon = currentLocation.coordinate.longitude;
if(currentLocation.coordinate.longitude < minLon)
minLon = currentLocation.coordinate.longitude;
}
MKCoordinateRegion region;
region.center.latitude = (maxLat + minLat) / 2;
region.center.longitude = (maxLon + minLon) / 2;
region.span.latitudeDelta = maxLat - minLat;
region.span.longitudeDelta = maxLon - minLon;
[self.mapView setRegion:region];
[self.mapView setDelegate:self];
[self.mapView addSubview:self];
return self;
}
- (void)drawRect:(CGRect)rect
{
// only draw our lines if we're not int he moddie of a transition and we
// acutally have some points to draw.
if(!self.hidden && nil != self.points && self.points.count > 0)
{
CGContextRef context = UIGraphicsGetCurrentContext();
if(nil == self.lineColor)
self.lineColor = [UIColor blueColor];
CGContextSetStrokeColorWithColor(context, self.lineColor.CGColor);
CGContextSetRGBFillColor(context, 0.0, 0.0, 1.0, 1.0);
// Draw them with a 2.0 stroke width so they are a bit more visible.
CGContextSetLineWidth(context, 2.0);
for(int idx = 0; idx < self.points.count; idx++)
{
CLLocation* location = [self.points objectAtIndex:idx];
CGPoint point = [_mapView convertCoordinate:location.coordinate toPointToView:self];
if(idx == 0)
{
// move to the first point
CGContextMoveToPoint(context, point.x, point.y);
}
else
{
CGContextAddLineToPoint(context, point.x, point.y);
}
}
CGContextStrokePath(context);
}
}
#pragma mark mapView delegate functions
- (void)mapView:(MKMapView *)mapView regionWillChangeAnimated:(BOOL)animated
{
// turn off the view of the route as the map is chaning regions. This prevents
// the line from being displayed at an incorrect positoin on the map during the
// transition.
self.hidden = YES;
}
- (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated
{
// re-enable and re-poosition the route display.
self.hidden = NO;
[self setNeedsDisplay];
}
-(void) dealloc
{
[_points release];
[_mapView release];
[super dealloc];
}
@end
16 Comments for this entry
1 Trackback or Pingback for this entry
-
The Reluctant Blogger :: Using MKAnnotation, MKPinAnnotationView and creating a custom MKAnnotationView
May 16th, 2009 on 2:08 pm[...] My last experiment with the MKMapKit was an example of how to use the MKMapView to display the line of a route on the map. [...]
April 16th, 2009 on 12:39 pm
Great! Thanks for this useful example. This saved my project
Dirk
April 22nd, 2009 on 7:07 am
Nice work, was thinking about doing this myself – glad to see someone got there before me
April 27th, 2009 on 12:06 am
very good example , thank u thanks a lot , this has given me good understanding over maps api.
April 30th, 2009 on 1:22 am
Thanks for such a good maps overview.
May 10th, 2009 on 10:33 am
Excellent, thank you. Question: is there a way to use core location or google or some other api to provide two points on the map and get the resulting set of latitude/longitude pairs for the route?
May 11th, 2009 on 3:17 am
Thanks Popeto. What I think you’re trying to do is pretty straightforward, if you intend to track a user’s current position, and draw the route representing their track.
What you’d need to do is register your class as the delegate with the CLLocationManager and make sure your class implements the CLLocationManagerDelegate functions. Whenever the location manager calls your function
locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation
you can use the newLocation.coordinate.latitude and longitude field. Given the example code attached to this post, you could add these new coordinates to the array of points, and tell the map view to refresh by calling setNeedsDisplay.I hoep this helps. Let me know if you need a more concrete example.
May 19th, 2009 on 1:24 pm
Great article, works a treat!
I use a custom UIImage instead of the pins, when I use the code it changes them back to pushpins…. any pointers here?
May 19th, 2009 on 1:36 pm
Also I get a funky issue when using the code above to find the bounds and then carry out the SetRegion. The map zooms but it has the Google grey grid view and doesn’t load the map up!
I have logged a bug with Apple….
May 19th, 2009 on 1:51 pm
Hi Lee,
I go over using custom views instead of the pins in this newer article: http://www.arlingtondev.com/thoughts/?p=81
Let me know if you’re still having issues after reading through that sample. Thanks!
May 19th, 2009 on 1:52 pm
Are you sure your device or simulator has network connectivity when you are seeing the grey grid view? Do you see the route line rendered on the grid view? Can you access the same maps in the actual google maps application on your device?
May 20th, 2009 on 8:46 am
This example is very helpful for me to understand the concepts of Map..thanks a lot..
May 20th, 2009 on 10:45 am
Yes the maps load fine if I scroll away and then back so network connectivity is there!
May 30th, 2009 on 1:53 am
Hey..
This is a wonderful work by you. But i expected Map Kit will come with something to draw poly lines. However you did it.
One question. I found you converting coordinate to pint and drawing lines through that point. Good. But i want to do the other way. I now convertPoint to Coordinate method is there with mapkit. I wrote the touchBegan: event method in “CSMapRouteLayerView” class , but touch is not firing teh method. I want to show user the place when he touch any particular point on the blue path.
Any idea?
June 9th, 2009 on 5:10 pm
Nice clean implementation. I think there has to be a better way to avoid coordinate recalculations though when scrolling around the map, coordinate lookups should only be necessary when changing zoom levels.
June 11th, 2009 on 8:54 am
Thanks for the sample code. I have the same question as Popeto (I think you misunderstood his question):
Given 2 locations, e.g. train station and hotel, are you aware of a method to retrieve the intermediate points between them, using 3.0 MapKit or Google Map API? It’s for route guidance application on iPhone.
June 11th, 2009 on 7:36 pm
Very nice! I really appreciate it mate
Is it possible to draw an arc between two points on a map ? Let’s say for example a geodesic arc ?