Get right to it: here’s the sample code
With the release of iOS 4, Apple has drastically decreased the complexity involved in rendering shapes, including routes, on a map. In fact, they’ve added an entire set of classes to MapKit specifically designed to help you add shapes to a map. These new classes and protocols are:
MKCircle
MKCircleView
MKMultiPoint
MKOverlayPathView
MKPolygon
MKPolygonView
MKPolyline
MKPolylineView
MKShape
MKOverlay
These classes make it incredibly easy to do what was once complex (And slightly buggy). Before when you would scroll or zoom in on a map with a route rendered in an MKAnnotationView, the route would have to temporarily be hidden because it was not scaled or scrolled along with with map. the MapKit framework now handles all this automatically.
So how do you add a route to a map using the updated MKMapView in iOS 4?
- Create a MKPolyline
- Add the polyline (as a MKOverlay) to the map
- Implement mapView:viewForOverlay: in your MKMapViewDelegate
- Initialize, set values on, and return a MKPolylineView from your updated map view delegate
Here’s some sample code that shows how you might create your MKPolyline:
-(void) loadRoute
{
NSString* filePath = [[NSBundle mainBundle] pathForResource:@”route” ofType:@”csv”];
NSString* fileContents = [NSString stringWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:nil];
NSArray* pointStrings = [fileContents componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];// while we create the route points, we will also be calculating the bounding box of our route
// so we can easily zoom in on it.
MKMapPoint northEastPoint;
MKMapPoint southWestPoint;// create a c array of points.
MKMapPoint* pointArr = malloc(sizeof(CLLocationCoordinate2D) * pointStrings.count);for(int idx = 0; idx < pointStrings.count; idx++)
{
// break the string down even further to latitude and longitude fields.
NSString* currentPointString = [pointStrings objectAtIndex:idx];
NSArray* latLonArr = [currentPointString componentsSeparatedByCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@","]];CLLocationDegrees latitude = [[latLonArr objectAtIndex:0] doubleValue];
CLLocationDegrees longitude = [[latLonArr objectAtIndex:1] doubleValue];// create our coordinate and add it to the correct spot in the array
CLLocationCoordinate2D coordinate = CLLocationCoordinate2DMake(latitude, longitude);MKMapPoint point = MKMapPointForCoordinate(coordinate);
//
// adjust the bounding box
//// if it is the first point, just use them, since we have nothing to compare to yet.
if (idx == 0) {
northEastPoint = point;
southWestPoint = point;
}
else
{
if (point.x > northEastPoint.x)
northEastPoint.x = point.x;
if(point.y > northEastPoint.y)
northEastPoint.y = point.y;
if (point.x < southWestPoint.x)
southWestPoint.x = point.x;
if (point.y < southWestPoint.y)
southWestPoint.y = point.y;
}pointArr[idx] = point;
}
// create the polyline based on the array of points.
self.routeLine = [MKPolyline polylineWithPoints:pointArr count:pointStrings.count];_routeRect = MKMapRectMake(southWestPoint.x, southWestPoint.y, northEastPoint.x - southWestPoint.x, northEastPoint.y - southWestPoint.y);
// clear the memory allocated earlier for the points
free(pointArr);}
Now that you’ve got your route’s polyline created, you need to let the map view display it by adding it as an overlay:
[self.mapView addOverlay:self.routeLine];
Adding the overlay alone will not render anything on the map. Your MKMapViewDelegate implementation must return an overlay for this route you’ve just added. Luckily, the new iOS 4 MapKit framework provides a default implementation of an MKOverlayView that is capable of rendering a line on a map. This class is called MKPolylineView, and this is how you might use it:
- (MKOverlayView *)mapView:(MKMapView *)mapView viewForOverlay:(id
)overlay
{
MKOverlayView* overlayView = nil;if(overlay == self.routeLine)
{
//if we have not yet created an overlay view for this overlay, create it now.
if(nil == self.routeLineView)
{
self.routeLineView = [[[MKPolylineView alloc] initWithPolyline:self.routeLine] autorelease];
self.routeLineView.fillColor = [UIColor redColor];
self.routeLineView.strokeColor = [UIColor redColor];
self.routeLineView.lineWidth = 3;
}overlayView = self.routeLineView;
}
return overlayView;
}
I hope this helps you use the new MapKit classes. Apple really has simplified rendering lines, routes and other shapes on the map, and these new classes and protocols really deprecate my previous articles on the topic, so long as you are targeting iOS 4 and above.
Here a sample project that uses the above code:
os4Maps
{ 31 } Comments
A working solution can be found here: http://github.com/mobilemelting/nvpolyline
This solution is especially targeted at iPhone OS versions prior to v.4.0
Although it can also be used in v.4.0
That’s very cool solutions but in real life app, how would you get the route location points between start and destination?
Marcin,
The points I used in this demo were obtained by using the core location manager. You could do something similar and then generate your list of points from the incoming data from the core location manager.
-Craig
Hello, this is great example!!
I’ve run build and analyze found leaks…
thank you I was freaking out when i updated my xcode to 4.0
Hi, great job!
One question: it’s possible to draw a route with two (or more) different colors?
thanks,
Matteo
Matteo,
You can probably split you route into two separate segments and use a different color for each.
Hi Craig. Read your previous polyline-posting which I implemented. But I couldn’t scroll the map after the line was drawn. Then I read your updated post and after some mixin it works like a charm.
Hi,
Good job! I was using the previously posting for route drawing mechanism, it was working fine on the all version below ios 4.0. As I switched my project to ios 4.0 I could not scroll map even I can zoom the app. Then I found new posting regarding this. I implemented that and it is working fine on the ios 4 devices. But what I want base SDK 4.0 and deployment target 3.1.2 for my app since I want app would run on the user device who have min version 3.1.2. After doing this when I tried to install this sample code on the device 3.1.2 and baseSDK 4.0 then this sample app installed but it crashes on the home screen. And in the console I am getting following error message.
dyld: Symbol not found: _OBJC_CLASS_$_MKPolyline
Referenced from: /var/mobile/Applications/211F44E6-B9AD-451B-A636-0147EE2AD38F/os4Maps.app/os4Maps
Expected in: /System/Library/Frameworks/MapKit.framework/MapKit
in /var/mobile/Applications/211F44E6-B9AD-451B-A636-0147EE2AD38F/os4Maps.app/os4Maps
Data Formatters temporarily unavailable, will re-try after a ‘continue’. (Not safe to call dlopen at this time.)
mi_cmd_stack_list_frames: Not enough frames in stack.
mi_cmd_stack_list_frames: Not enough frames in stack.
Can anybody suggest me pls how can I run this route drawing code for version 3.1.2 or above like 4.0 both.
Thanks
Anand,
these were additions to the SDK that came with the iOS4 update. You cannot use this code and MKPolyline classes if you want to run it on version 3.1.2, because they don’t support it. If you want to display routes on both versions 3 and 4, then you can use the aforementioned NVPolyline project: http://github.com/mobilemelting/nvpolyline
Craig thanks for this tutorial!
The points I used in this demo were obtained by using the core location manager. You could do something similar and then generate your list of points from the incoming data from the core location manager.
Please Admin how can I get points.
spent all day on this…..apple should hire you to write their docs…thanks!!
Thanks for the article. I have been struggling to use Overlays to display predefined map points on Augmented Reality. I am getting all points perfectly but getting frames overlapped with each other when they are close together. I have latitude, longitude for each map points. Could you please help me to create the overlayview for these point to get rid of overlapping windows please?
Thanks.
Great example thanks… I’ve been trying to fathom this out for weeks but with no joy! Apple only supply KMLViewer as sample code, and it’s so overloaded with KML stuff that it doesn’t help with the actual MKPolyline code. If only they used your example instead!
One thing if I may simplify it though… You create your own bounding rect for the map area when it’s already built into MapKit:
// zoom in on the route.
[self.mapView setVisibleMapRect:[self.routeLine boundingMapRect]];
Which removes about 10 lines of code for you
Thanks again!!
:-Joe
Also, you don’t need to convert the coordinates to MKPoints:
// create an array of points.
CLLocationCoordinate2D coordinateArray[pointStrings.count];
for(int idx = 0; idx < pointStrings.count; idx++)
{
// break the string down even further to latitude and longitude fields.
NSString* currentPointString = [pointStrings objectAtIndex:idx];
NSArray* latLonArr = [currentPointString componentsSeparatedByCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@","]];
CLLocationDegrees latitude = [[latLonArr objectAtIndex:0] doubleValue];
CLLocationDegrees longitude = [[latLonArr objectAtIndex:1] doubleValue];
// create our coordinate and add it to the correct spot in the array
CLLocationCoordinate2D coordinate = CLLocationCoordinate2DMake(latitude, longitude);
coordinateArray[idx] = coordinate;
}
// create the polyline based on the array of coordinates:
self.routeLine = [MKPolyline polylineWithCoordinates:coordinateArray count:pointStrings.count];
Hope this helps
Joe, nice one. I have also noticed this from KMLViewer example. Do you have any idea on how would you create OverLaysView for rectangles? See my complete question above just before your first response.
Thanks.
Hi,
I use your solution to draw routes in my MKMapView, but I’d like to add a gradient in it depending on the speed (coming from a CLLocationManager). Do you have any idea how to make it this way?
I posted something here: http://stackoverflow.com/questions/5479833/iphone-gradient-in-mapview-overlay showing a screenshot of what I’d like to have exactly…
Otherwise, great post and it helped me a lot, thanks!
Overlays are great as long as you don’t plan on moving them lets say with your finger across the screen. The refresh rate using method calls like setNeedsDisplay are much slower between redraws than using your previous route annotation implementation.
Summary from my experience -
Overlays:
+ placing objects between the map and annotations
+ iOS handles map changes/redraw method calls, including resizing views
- high redraw rates are hard to achieve (i.e. ability to drag an overlay smoothly across the screen which is drawn using CGContext methods)
Annotations (using your subview logic):
+ placing objects on the map for which the region/size is known
+ objects drawn stay the same pixel size by default
- having to manage map view region change events and annotation view visibility
- if you draw some minor distance outside of the original annotation view, iOS will no longer call your drawRect method after setNeedsDisplay. This is mostly likely due to some internal draw performance logic. An example using the route annotation subview project is posted as a comment here: http://spitzkoff.com/craig/?p=108&cpage=2#comment-509
Thanks for all the help Craig.
hi!
I have a problem: my route goes from australia to america, but I want that it passes from the ocean, not “from europe”… do you understand?
Well, the problem is that from the most east point to the most west point, there is an horizontal line… really ugly!
what can I do?
Thanks!
So far I have been able to include your code in my project and show your polylines where it belongs, still I am not totally clear about how to “refresh” the overlay view in order to show artifacts that have been programmatically added to it.
I just trying too implement your sample in my application. It is now showing the right position of my route on the Display (my route is in the center) but it does’n draw the route…, but if I load my route in your project it works… At the moment my Project is an Standard,Sattelite and Hybrid show of the map, but if I use your example to add to my code, after the build it shows the right position of my route, but it doesn’t draw the route… Can you help me please,so soon as possible?
Thanks!
I am having the same problem as ACFC. My app gets the points for a route from google. The response also contains the bounding rect. My app creates the rect and shows the map correctly and I add the overlay. The – (MKOverlayView *)mapView:(MKMapView *)mapView viewForOverlay:(id )overlay is called and I construct a MKOverlayPathView from the points. I have verified that the points are within the bounding rect. However, the route overlay does not draw on the map.
I have checked everything I can think of with no joy, any suggestions would be greatly appreciated. Thanks
hi all,
it’s normal that in ios5 not work?
the delegate method isn’t called
i put an nslog in a delegate method – (MKOverlayView *)mapView:(MKMapView *)mapView viewForOverlay:(id )overlay
but not work.. how can resolve?
thank you
Thanks for the article. It helped me a lot in my app which shows the route to the user.
One question though, on the map, if the two points are really far, it draws a straight line, which makes it look ugly because it does not follow the route. I believe there cannot be any solution to this – but just thought I’d mention it.
Thanks, again.
Why
MKMapPoint* pointArr = malloc(sizeof(CLLocationCoordinate2D) * pointStrings.count);
and not:
MKMapPoint* pointArr = malloc(sizeof(MKMapPoint) * pointStrings.count);
Would you agree that there is a one-to-one relationship between a MKOverlay and a MKPolylineView? If so, doesn’t this go against the re-use of views paradigm? Just curious.
This is otherwise a good tutorial except for one issue. The sample code on line 95 In os4MapViewController.m, _routeRect = MKMapRectMake(southWestPoint.x, southWestPoint.y, northEastPoint.x – southWestPoint.x, northEastPoint.y – southWestPoint.y); Xcode keeps telling me that “The left operand of ‘-’ is a garbage value”
Does anyone know how to fix it?
CLLocationDegrees longitude = [[latLonArr objectAtIndex:1] doubleValue];
throwing error on aboveLine;
perfect, thank you!
This is probably the most clean and workable solution out there. Thanks Craig!
{ 3 } Trackbacks
[...] wirklich trivial, aber ich habe in den weiten des Internet ein gutes und vorallem übersichtliches Beispiel gefunden, das mich zur korrekten Lösung geführt hat. Der Muster Code von Apple scheint mir da [...]
[...] am using the code in this link : http://spitzkoff.com/craig/?p=136 , I have changed it to use Google map API , here is my code [...]
[...] vamos a utilizar las clases MKPolyline y MKOverlayView. La forma de hacerlo la he obtenido de este blog, que es donde cuentan que desde iOS 4 es fácil pero que antes era complicado e infructuoso. El [...]
Post a Comment