Skip to content

Drawing polyines or routes on a MKMapView (as an MKAnnotationView) – Part 2

It turns out there is a better way to display routes on a map than the last example I gave. The problem with my last example is that the route exists in a layer above the map view, resulting in the line of the route being drawn on top of any annotation views. So pins, photos and anything else expressed as an MKAnnotation would be drawn over by the routes in the layer above the map view.

The solution to this problem then is to draw the lines as usual, but in a custom MKAnnoationView. This view will be set to not clip its own subviews, and will then have a subview that lies outside the frame of the annotation view, and this subview will draw the lines. The route annotation view needs a subview to do the rendering which will always be positioned at the full frame size and origin of the map. This way the MKAnnotationView can be smaller than the route, but it  always draws in the internal subview, which is the size of the map view.

So… long story short: the ability to put a route on a MKMapView via a custom MKAnnotation. You can download the sample code here.

//
// CSRouteView.m
// testMapp
//
// Created by Craig on 8/18/09.
// Copyright Craig Spitzkoff 2009. All rights reserved.
//
#import "CSRouteView.h"
#import "CSRouteAnnotation.h"
// this is an internally used view to CSRouteView. The CSRouteView needs a subview that does not get clipped to always
// be positioned at the full frame size and origin of the map. This way the view can be smaller than the route, but it
// always draws in the internal subview, which is the size of the map view.
@interface CSRouteViewInternal : UIView
{
// route view which added this as a subview.
CSRouteView* _routeView;
}
@property (nonatomic, retain) CSRouteView* routeView;
@end
@implementation CSRouteViewInternal
@synthesize routeView = _routeView;
-(void) drawRect:(CGRect) rect
{
CSRouteAnnotation* routeAnnotation = (CSRouteAnnotation*)self.routeView.annotation;

// 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 != routeAnnotation.points && routeAnnotation.points.count > 0)
{
CGContextRef context = UIGraphicsGetCurrentContext();

if(nil == routeAnnotation.lineColor)
routeAnnotation.lineColor = [UIColor blueColor]; // setting the property instead of the member variable will automatically reatin it.

CGContextSetStrokeColorWithColor(context, routeAnnotation.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 < routeAnnotation.points.count; idx++) { CLLocation* location = [routeAnnotation.points objectAtIndex:idx]; CGPoint point = [self.routeView.mapView convertCoordinate:location.coordinate toPointToView:self]; NSLog(@"Point: %lf, %lf", point.x, point.y); if(idx == 0) { // move to the first point CGContextMoveToPoint(context, point.x, point.y); } else { CGContextAddLineToPoint(context, point.x, point.y); } } CGContextStrokePath(context); // debug. Draw the line around our view. /* CGContextMoveToPoint(context, 0, 0); CGContextAddLineToPoint(context, 0, self.frame.size.height); CGContextAddLineToPoint(context, self.frame.size.width, self.frame.size.height); CGContextAddLineToPoint(context, self.frame.size.width, 0); CGContextAddLineToPoint(context, 0, 0); CGContextStrokePath(context); */ } } -(id) init { self = [super init]; self.backgroundColor = [UIColor clearColor]; self.clipsToBounds = NO; return self; } -(void) dealloc { self.routeView = nil; [super dealloc]; } @end @implementation CSRouteView @synthesize mapView = _mapView; - (id)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { self.backgroundColor = [UIColor clearColor]; // do not clip the bounds. We need the CSRouteViewInternal to be able to render the route, regardless of where the // actual annotation view is displayed. self.clipsToBounds = NO; // create the internal route view that does the rendering of the route. _internalRouteView = [[CSRouteViewInternal alloc] init]; _internalRouteView.routeView = self; [self addSubview:_internalRouteView]; } return self; } -(void) setMapView:(MKMapView*) mapView { [_mapView release]; _mapView = [mapView retain]; [self regionChanged]; } -(void) regionChanged { NSLog(@"Region Changed"); // move the internal route view. CGPoint origin = CGPointMake(0, 0); origin = [_mapView convertPoint:origin toView:self]; _internalRouteView.frame = CGRectMake(origin.x, origin.y, _mapView.frame.size.width, _mapView.frame.size.height); [_internalRouteView setNeedsDisplay]; } - (void)dealloc { [_mapView release]; [_internalRouteView release]; [super dealloc]; } @end

{ 16 } Comments

  1. Nik | March 15, 2010 at 6:30 pm | Permalink

    Hi craig,

    I’am glad there are such good developers to help all of us with less experience.
    Your code was really helpful and very well documented.

    I had a little problem, which i would like to mention.
    When testing my app on a device and navigating on the map the app crashes unexpectedly.

    I use a dictionary to keep the route annotations but i think there is a problem with the autorelease issue…

    I solved the problem by retaining the CSRouteAnnotations and releasing them on the dealloc method manually.

    What do you think about it?

  2. Ankush | April 5, 2010 at 8:11 am | Permalink

    Hi,

    Thanks for creating such a nice application.
    I have used your code for my application and facing a problem i.e. when I plot the route from “San Jose” to “New York”, it works fine but when I try to zoom in or zoom out, then close the application without waiting for the zoom effect. After that I re-launch the application immediately my application crashes!!! :((

    What I figured out is that when I zoom-in or zoom-out multiple drawRect events are pushed for CSRouteView from setCenter method which are still pending for the execution, when i close the application and relaunch it I somehow gets the same process back again and my application try to handle the remaining events then shuts down.

    Please help me to resolve this issue.

  3. 知识 | May 27, 2010 at 11:07 am | Permalink

    顶~~~~好文章知识分享 我会经常来的252

  4. Vijay | June 3, 2010 at 4:35 am | Permalink

    I am having a problem with dragging pin when overlapped with other pin. It got stuck after lifting the pin.

  5. adriaan | June 17, 2010 at 6:20 am | Permalink

    It sounds like I have the same issues as @Charlie Mezak and @FMulder.
    What I’m trying to do is to display an evolving route. Most of it’s static but the last coordinate is extrapolated based (as if something is moving at a certain speed). I am trying to accomplish this with two routes – one for the static component and one for the extrapolated bit.

    The problem I’m having is as Charlie said that “The RouteView is initializing with a non-origin frame” which is causing the extrapolated route to get cropped in certain situations. And because I keep redrawing/extrapolating this every second, it gets re-cropped every second.

    Does anyone have an idea on how to get the RouteView to initialize correctly, i.e. aligned with the frame of the mapview?

  6. Chris | June 26, 2010 at 12:44 pm | Permalink

    Thanks allot for your code.

    Is there anybody out there who managed displaying routes with allot of single points?
    This will be extremely slow – and blocks the mainthread, too.

    Any Ideas?

  7. Merlin | July 3, 2010 at 4:14 pm | Permalink

    I’m using the code from Matt Arturi’s comment to fix the clipping problem.

    One thing though – for nudging to work it should move latitude not by 0.00017 (which is fine only at the map scale in this example), but by 1% of the span:

    newCenter.latitude = mapper.region.center.latitude – mapper.region.span.latitudeDelta/100;

    If you move by 0.00017 then in some zoom levels that might be quite visible, and in some zoom levels it might not move at all and not protect the clipping.

  8. Joerg | July 13, 2010 at 12:51 am | Permalink

    You might want to have a look at 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
    Hope this helps.

  9. Jassi | August 13, 2010 at 3:34 am | Permalink

    I am creating a line between two points on a map. The line is displayed but the problem is that whenever I am zooming the map , the app crashes. This happens in 8 GB iPhone and not on 16 GB. Is it a memory issue or what?

    I am submitting a snippet of code along with.
    Please help me.

    drawImage.frame = [tileContainerView frame];
    drawImage.tag = 555;
    UIGraphicsBeginImageContext(tileContainerView.frame.size);
    CGContextSetLineCap(UIGraphicsGetCurrentContext(), kCGLineCapRound);
    //CGContextSetLineWidth(UIGraphicsGetCurrentContext(), 3.0);
    //CGContextSetRGBStrokeColor(UIGraphicsGetCurrentContext(), 1.0, 0.0, 0.0, 1.0);
    CGContextBeginPath(UIGraphicsGetCurrentContext());
    CGContextMoveToPoint(UIGraphicsGetCurrentContext(), mapXFrom,mapYFrom+22);
    CGContextAddLineToPoint(UIGraphicsGetCurrentContext(),(mapXFrom +mapXTo)/2,(mapYFrom+ mapYTo+22)/2);
    CGContextStrokePath(UIGraphicsGetCurrentContext());
    CGContextFlush(UIGraphicsGetCurrentContext());

    drawImage.image = UIGraphicsGetImageFromCurrentImageContext();
    [self addSubview:drawImage];
    [self setUserInteractionEnabled:TRUE];
    UIGraphicsEndImageContext();

  10. McAndy | August 26, 2010 at 3:18 pm | Permalink

    I’m completely new to this topic and here’s my question: How do you download a route from google earth or google maps with longitudes and latitudes? – Is there a tool for that?

  11. Valerio | February 12, 2011 at 12:16 pm | Permalink

    Thank you Craig,
    It helped me a lot.
    I have a question. I’d like to draw a line on the map, then refreshing the view, by removing the old line and putting the new one with other points.
    How should I proceed?
    Thanks
    Valerio

  12. michael | April 15, 2011 at 2:33 pm | Permalink

    I have a problem using your implementation. The drawRect method in CSRouteView.m will not be called if you do the following:

    1) CSRouteAnnotation.m (initWithPoints method) – shift the _center coordinate:


    // the center point is the average of the max and mins
    _center.latitude = (minLat-1.5) + _span.latitudeDelta / 2;
    _center.longitude = (minLon-1.5) + _span.longitudeDelta / 2;

    This mimics placing points some distance outside of the view originally created.

    2) for visualizing, change the background color of CSRouteView from clear to gray in initWithFrame method.

    self.backgroundColor = [UIColor lightGrayColor];

    —–
    Now run the app. You will notice as you zoom/move the map around the gray background view doesn’t always consume the entire screen because of the frame constraints and probably functions as desired.
    Now pan over to the route location which is at its original coordinates (up and to the right of starting location). Zoom in so the route takes up the whole screen (from left to right). What you will notice is that the drawRect method is no longer called, the route disappears, the push pins remain, and the origin value calculated in CSRouteView.regionChanged is very different that what it was originally – probably what is causing drawRect to not be called by iOS.

    The problem seems to be that this implementation is only good for static points and is unable to handle points created some distance away from the views original frame.

    Any ideas on how to support this?

  13. joe | September 12, 2011 at 5:16 am | Permalink

    Hi michael, I’m experiencing the same problem. Did you manage to solve it? Would really appreciate if you could share.. I found that when the grey background disappears from the screen, the origin coordinates in regionChanged becomes a huge negative value.

    CGPoint origin = CGPointMake(0, 0);
    origin = [_mapView convertPoint:origin toView:self];

    It seems that the routeView did not update accordingly when the map is zoomed in/out or panned. Any help?

  14. rohith sreeram | February 20, 2012 at 2:25 am | Permalink

    i wanted to knw is there any method which automatically returns an array of (lattitude,longitude) between 2 points of a map, if this is available please let me know

  15. rohith sreeram | February 20, 2012 at 2:27 am | Permalink

    i wanted to know is there any method which will return an array of (lattitude,longitude) between to points of a map ??

  16. Stephen | March 30, 2012 at 2:05 pm | Permalink

    Hi, I am trying to get this code to work in an application, however I am very new to objective C, so I am not sure if I completely understand when drawRect is happening. I can’t find any calls to it so I believe some sort of event is triggering it, but it is never triggered in my application.

{ 4 } Trackbacks

  1. […] I should credit Craig for his example of drawing lines on a MKMapView (http://spitzkoff.com/craig/?p=108), that’s where you can find part 1 and part 2. This post is a kind of teaser what I’m […]

  2. […] App Store submissions later, if my app ever gets that far. I googled a bit and found Graig’s excellent blog post and sample project on how to draw polylines on a […]

  3. […] this blog post and sample code is an excellent guide to putting not only routes, but quite a lot of other things onto a MapKit […]

  4. […] have seen in this tutorial Drawing polyines or routes on a MKMapView that how to draw driving direction using core graphics…but in the sample code it has […]

Post a Comment

Your email is never published nor shared. Required fields are marked *