55

I want to draw a route between two locations on the map. Something like a tour guide. When the tourist clicks another location, I want to be able to draw a route; as well as, inform about the distance from the current location.

I am aware of sites on the Internet which tell how to draw polylines on map. But, most of the examples had a preloaded .csv file with various coordinates.

Is there an alternative way to get the coordinates from Google or any other provider, as the location is selected dynamically.

If NO, how do I get the information for intermediate coordinates?

Does iOS 6 provide any direct way for this problem?

Cœur
  • 37,241
  • 25
  • 195
  • 267
PRN
  • 575
  • 1
  • 5
  • 9

10 Answers10

62

The following viewDidLoad will (1) set two locations, (2) remove all the previous annotations, and (3) call user defined helper functions (to get route points and draw the route).

-(void)viewDidLoad
{
    [super viewDidLoad];

    // Origin Location.
    CLLocationCoordinate2D loc1;
    loc1.latitude = 29.0167;
    loc1.longitude = 77.3833;
    Annotation *origin = [[Annotation alloc] initWithTitle:@"loc1" subTitle:@"Home1" andCoordinate:loc1];
    [objMapView addAnnotation:origin];

    // Destination Location.
    CLLocationCoordinate2D loc2;
    loc2.latitude = 19.076000;
    loc2.longitude = 72.877670;
    Annotation *destination = [[Annotation alloc] initWithTitle:@"loc2" subTitle:@"Home2" andCoordinate:loc2];
    [objMapView addAnnotation:destination];

    if(arrRoutePoints) // Remove all annotations
        [objMapView removeAnnotations:[objMapView annotations]];

    arrRoutePoints = [self getRoutePointFrom:origin to:destination];
    [self drawRoute];
    [self centerMap];
}

The following is the MKMapViewDelegate method, which draws overlay (iOS 4.0 and later).

/* MKMapViewDelegate Meth0d -- for viewForOverlay*/
- (MKOverlayView*)mapView:(MKMapView*)theMapView viewForOverlay:(id <MKOverlay>)overlay
{
    MKPolylineView *view = [[MKPolylineView alloc] initWithPolyline:objPolyline];
    view.fillColor = [UIColor blackColor];
    view.strokeColor = [UIColor blackColor];
    view.lineWidth = 4;
    return view;
}

The following function will get both the locations and prepare URL to get all the route points. And of course, will call stringWithURL.

/* This will get the route coordinates from the Google API. */
- (NSArray*)getRoutePointFrom:(Annotation *)origin to:(Annotation *)destination
{
    NSString* saddr = [NSString stringWithFormat:@"%f,%f", origin.coordinate.latitude, origin.coordinate.longitude];
    NSString* daddr = [NSString stringWithFormat:@"%f,%f", destination.coordinate.latitude, destination.coordinate.longitude];

    NSString* apiUrlStr = [NSString stringWithFormat:@"http://maps.google.com/maps?output=dragdir&saddr=%@&daddr=%@", saddr, daddr];
    NSURL* apiUrl = [NSURL URLWithString:apiUrlStr];

    NSError *error;
    NSString *apiResponse = [NSString stringWithContentsOfURL:apiUrl encoding:NSUTF8StringEncoding error:&error];
    NSString* encodedPoints = [apiResponse stringByMatching:@"points:\\\"([^\\\"]*)\\\"" capture:1L];

    return [self decodePolyLine:[encodedPoints mutableCopy]];
}

The following code is the real magic (decoder for the response we got from the API). I would not modify that code unless I know what I am doing :)

- (NSMutableArray *)decodePolyLine:(NSMutableString *)encodedString
{
    [encodedString replaceOccurrencesOfString:@"\\\\" withString:@"\\"
                                  options:NSLiteralSearch
                                    range:NSMakeRange(0, [encodedString length])];
    NSInteger len = [encodedString 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 = [encodedString 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 = [encodedString 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];
        printf("\n[%f,", [latitude doubleValue]);
        printf("%f]", [longitude doubleValue]);
        CLLocation *loc = [[CLLocation alloc] initWithLatitude:[latitude floatValue] longitude:[longitude floatValue]];
        [array addObject:loc];
    }
    return array;
}

This function will draw a route and will add an overlay.

- (void)drawRoute
{
    int numPoints = [arrRoutePoints count];
    if (numPoints > 1)
    {
        CLLocationCoordinate2D* coords = malloc(numPoints * sizeof(CLLocationCoordinate2D));
        for (int i = 0; i < numPoints; i++)
        {
            CLLocation* current = [arrRoutePoints objectAtIndex:i];
            coords[i] = current.coordinate;
        }

        self.objPolyline = [MKPolyline polylineWithCoordinates:coords count:numPoints];
        free(coords);

        [objMapView addOverlay:objPolyline];
        [objMapView setNeedsDisplay];
    }
}

The following code will center align the map.

- (void)centerMap
{
    MKCoordinateRegion region;

    CLLocationDegrees maxLat = -90;
    CLLocationDegrees maxLon = -180;
    CLLocationDegrees minLat = 90;
    CLLocationDegrees minLon = 180;

    for(int idx = 0; idx < arrRoutePoints.count; idx++)
    {
        CLLocation* currentLocation = [arrRoutePoints 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;
    }

    region.center.latitude     = (maxLat + minLat) / 2;
    region.center.longitude    = (maxLon + minLon) / 2;
    region.span.latitudeDelta  = maxLat - minLat;
    region.span.longitudeDelta = maxLon - minLon;

    [objMapView setRegion:region animated:YES];
}

I hope this would help someone.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
viral
  • 4,168
  • 5
  • 43
  • 68
  • Hope your draw route method works fine. I used almost same function like the one named decodePolyLine in yours. I got the array of CLLocations but I was stuck after that because the method "polylineWithCordinates" take an arrray of CLLocationCordinate2D and I didn't know how to convert my array to CLLocationCordinate2D array. I'll try your draw route method tomorrow at my work place. By the way could you explain this line 'CLLocationCoordinate2D* coords = malloc(numPoints * sizeof(CLLocationCoordinate2D));' – nr5 Dec 16 '12 at 09:30
  • 1
    @rd4code `CLLocationCoordinate2D* coords = malloc(numPoints * sizeof(CLLocationCoordinate2D));` -- It allocates chunk in memory equal to the size of our `arrRoutePoints`, with each of the index object as `CLLocationCoordinate2D`. Hope it's clear to you. – viral Dec 17 '12 at 04:47
  • your method worked today :). thanks and thanks for the explanation. . Also I want to ask if there's another way creating the CLLocationCoordinate2D array ? I mean if the malloc necessary ? – nr5 Dec 17 '12 at 05:09
  • 1
    @rd4code `malloc` is used just to avoid `CFArray`, that's all. Without that, you would have use `CFArrays` -- It is just simplifies. Nothing else. – viral Dec 17 '12 at 06:30
24

This is a tricky one. There is no way to do that with MapKit: it's easy enough to draw lines when you know the coordinates, but MapKit won't give you access to the roads or other routing information. I'd say you need to call an external API to get your data.

I've been playing with cloudmade.com API. The vector stream server should return what you need, and then you can draw that over your map. However, discrepancies between the Google maps and the OSM maps used by cloudmade may make you want to use cloudmade maps all the way: they have an equivalent to MapKit.

P.S.: Other mapping providers - Google, Bing, etc. may also provide equivalent data feeds. I've just been looking at OSM/Cloudmade recently.

P.P.S.: None of this is trivial newbie stuff! Best of luck!

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Andiih
  • 12,285
  • 10
  • 57
  • 88
12

Andiih's got it right. MapKit won't let you do that. Unfortunately, Google won't let you do what you want to do either.

When Apple announced MapKit and all, they also explicitly stated that any navigational applications would be BYOM: Bring Your Own Maps, so any navigation application uses their own set of mapping tools.

Google's Terms of Service restrict you from even displaying routes on top of their maps:

http://code.google.com/intl/de/apis/maps/iphone/terms.html

License Restrictions:

10.9 use the Service or Content with any products, systems, or applications for or in connection with:

(a) 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;

(b) any systems or functions for automatic or autonomous control of vehicle behavior; or

(c) dispatch, fleet management, business asset tracking, or similar enterprise applications (the Google Maps API can be used to track assets (such as cars, buses or other vehicles) as long as the tracking application is made available to the public without charge. For example, you may offer a free, public Maps API Implementation that displays real-time public transit or other transportation status information.

Sadly, this includes what you would like to do. Hopefully one day MapKit will be expanded to allow such features... although unlikely.

Good luck.

Daniel Amitay
  • 6,677
  • 7
  • 36
  • 43
  • 1
    PRN doesn't say he's trying to do anything enterprise or turn-by-turn : just that he wants to overlay directions. This is something you can do with the Google Maps Javascript API (I think), but not MapKit. You are obviously more familiar with the Google API than me - if its not turn-by-turn is there any way of getting road vectors out ? [see this question has got me interested now!] – Andiih May 15 '10 at 06:51
  • 1
    it's not turn by turn but it¡s route guidance, isn't it? – Daniel Apr 13 '12 at 18:19
5

You might want to have a look at https://github.com/leviathan/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.

leviathan
  • 11,080
  • 5
  • 42
  • 40
  • Hi Colins, First of all sorry for the delayed reply...but with the example mentioned above, the co-ordinates are hardcoded within the application,what i want is to get the co-ordinates as and when the location is selected...not something where the co-ordinates are preloaded. For instance,a tourist could pick any random location in a city, and should be provided the route based on his/her current co-ordinates. This selected location's co-ordinate may not be preloaded. – PRN Jul 29 '10 at 06:19
  • PRN please head over to the polyline GitHub project and open an issue report (http://github.com/mobilemelting/nvpolyline/issues). I will try to incorporate a solution for your problem in the project. – leviathan Jul 29 '10 at 07:23
4

Obtaining and drawing a route on the map is super easy with iOS 7 API:

MKDirectionsRequest *directionsRequest = [[MKDirectionsRequest alloc] init];

// Set the origin of the route to be current user location
[directionsRequest setSource:[MKMapItem mapItemForCurrentLocation]];

// Set the destination point of the route
CLLocationCoordinate2D destinationCoordinate = CLLocationCoordinate2DMake(34.0872, 76.235);
MKPlacemark *destinationPlacemark = [[MKPlacemark alloc] initWithCoordinate:destinationCoordinate addressDictionary:nil];
[directionsRequest setDestination:[[MKMapItem alloc] initWithPlacemark:destinationPlacemark]];

MKDirections *directions = [[MKDirections alloc] initWithRequest:directionsRequest];

// Requesting route information from Apple Map services
[directions calculateDirectionsWithCompletionHandler:^(MKDirectionsResponse *response, NSError *error) {
    if (error) {
        NSLog(@"Cannot calculate directions: %@",[error localizedDescription]);
    } else {
        // Displaying the route on the map
        MKRoute *route = [response.routes firstObject];
        [mapView addOverlay:route.polyline];
    }
}];
nalexn
  • 10,615
  • 6
  • 44
  • 48
4

Try MapKit-Route-Directions (GitHub).

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
iOS_User
  • 1,372
  • 5
  • 21
  • 35
3

MapQuest has an SDK that's a drop-in replacement for MapKit. It's currently in beta, but under active development.

It allows overlays, routing, and geocoding.

MapQuest iOS Maps API

Fabian
  • 6,973
  • 2
  • 26
  • 27
3

Just to clarify, it looks like there are a couple of things being discussed. One is a way to get the vertices of a route and the other is to draw an overlay on the map using those vertices. I know the MapQuest APIs, so I have some links for those below - Google and Bing have equivalents I think.

1) Getting vertices of a route
If you're looking for the new coordinates of a route to draw a route overlay, you can either use a web service call to a routing web service - I'm assuming you're using JavaScript here to display the map. If you're using native code, you can still hit a web service or you can use a native call (that is, the MapQuest iPhone SDK has a native route call in it).

Most of the directions services should return the "shapepoints" of the route so that you can draw.

Here are some examples using MapQuest- Directions Web Service to get shapepoints (see the Shape return object) - http://www.mapquestapi.com/directions/

2) Drawing an overlay
Once you have your vertices, you need to draw them. I think most JavaScript map APIs will have an overlay class of some sort. Here's the MapQuest one: http://developer.mapquest.com/web/documentation/sdk/javascript/v7.0/overlays#line

3) Doing it with one call
MapQuest also have some convenience functions to make the route call and draw the line for you - I can't post more than two links! So go to the link above and look for "routing" in the navigation bar on the left.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Roman
  • 61
  • 3
1

To update this question, there is no need to external apk since iOS7.

Here a very simple and effective solution :

http://technet.weblineindia.com/mobile/draw-route-between-2-points-on-map-with-ios7-mapkit-api/2/

I know the question was about iOS 6 but I believe that this solution will be useful for many people.

The only thing missing in this solution is implementing the following delegate method to display the beginning and ending pin

-(MKAnnotationView *) mapView:(MKMapView *)mapView viewForAnnotation:(id<MKAnnotation>)annotation
Omaty
  • 425
  • 5
  • 14
0

As per 2019, with iOS12 or iOS13, it's quite easy to draw a route between two points. Heres is a typical code of mine, that draws a route between two POIs, a CLLocation and a CLPlacemark, that you can easily adapt to your context.

/// The map view to use to draw the route
lazy var mapView: MKMapView = {
    let m = MKMapView()
    m.delegate = self

    // Attach to the controller view
    view.addSubview(m)
    // Don't forget to setup its details
    return m
}()

/// Having the source:
let from: CLLocation

/// And the destination
let to: CLPlacemark


/// Compute and draw the route between the two POI.
func getAndDrawRoute() {

    /// Get the route, driving

    let ok = route(from: from, to: to, transportType: .automobile) { routes, error in
        if let error = error {
            // *** Handle the error here
            print("\(type(of: self)).\(#function): *** Error: \(error)")

            // blah blah
            return
        }

        // Get the route among the multiple possibilities. Here we take the first one to keep this sniper short

        guard let route = routes.first else {
            // *** Handle the error: no route exits
            print("\(type(of: self)).\(#function): *** Warning: no route exits")
            // blah blah
            return 
        }

        // Route exists
        print("Found the route: \(route)")

        // Draw it
        self.mapView.draw(route: route)
    }
}

/**
Route from a source to the destination locations.

- Parameters:
    - from: The source location;
    - toPlacemark: The destination `MKPlaceMark`;
    - transportType: The transport type;
    - completion: The completion closure.

- Returns: `true` if the route can be traced, or false if the user's position is not yet available

*/
public func route(from source: CLLocation, toMKPlacemark destinationPlacemark: MKPlacemark, transportType: MKDirectionsTransportType, completion: @escaping RouteCompletion) {

    let sourcePlacemark = MKPlacemark(coordinate: source.coordinate)

    let sourceMapItem = MKMapItem(placemark: sourcePlacemark)
    let destinationMapItem = MKMapItem(placemark: destinationPlacemark)

    let directionRequest = MKDirections.Request()
    directionRequest.source = sourceMapItem
    directionRequest.destination = destinationMapItem
    directionRequest.transportType = transportType

    // Calculate the direction
    let directions = MKDirections(request: directionRequest)

    // And get the routes
    directions.calculate { response, error in

        guard let response = response else {
            if let error = error {
                print("\(type(of: self)).\(#function): *** Error: \(error.localizedDescription)")
            }
            completion(nil, error)
            return
        }

        completion(response.routes, nil)
    }
}

/// Adds the route overlay
public func draw(route: MKRoute) {

    mapView.addOverlay(route.polyline)
}

/// Renders the overlays, inclusion the route
public func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
    // If you want to draw a circle to fit your specifics
    if overlay is MKCircle {
        let renderer = MKCircleRenderer(overlay: overlay)
        renderer.fillColor      = UIColor.blue.withAlphaComponent(0.1)
        renderer.strokeColor    = .blue
        renderer.lineWidth      = 1
        return renderer
    }

    // If you want to draw a route (polyline) to fit your specifics
    if overlay is MKPolyline {

        let renderer = MKPolylineRenderer(overlay: overlay)

        renderer.strokeColor    = UIColor.init(displayP3Red: 0.15, green: 0.5, blue: 1, alpha: 0.9)

        renderer.lineWidth      = 10.0

        return renderer
    }

    return MKOverlayRenderer(overlay: overlay)
}
Stéphane de Luca
  • 12,745
  • 9
  • 57
  • 95