Handling MKMapView Annotation Pins on the Same Coordinate

10 January 2013
 

There is now an updated version of this post that reworks the solution from Objective-C to Swift 2.0.

A component of a project I am working on displays shop locations on a map. A problem arises when the shops are located at a shopping centre or mall – the shops are invariably geo-coded to the same latitude-longitude coordinates.

When the shops have the same coordinates, the annotations (pins) display in the exact same location on the map. This gives the appearance of there only being one pin, and indeed, the user can only tap one pin.

To overcome this, we implemented a routine to re-place the pins at new coordinates surrounding the contested coordinate.

First we group the annotations by coordinate.

for (id pin in annotations) {

    CLLocationCoordinate2D coordinate = pin.coordinate;
    NSValue *coordinateValue = [NSValue valueWithBytes:&coordinate objCType:@encode(CLLocationCoordinate2D)];

    NSMutableArray *annotationsAtLocation = coordinateValuesToAnnotations[coordinateValue];
    if (!annotationsAtLocation) {
        annotationsAtLocation = [NSMutableArray array];
        coordinateValuesToAnnotations[coordinateValue] = annotationsAtLocation;
    }

    [annotationsAtLocation addObject:pin];
}

This routine produces a dictionary keyed on NSValues containing a copy of the coordinate (you can’t key a NSDictionary on a c-type). The value of a entry in the dictionary is a NSArray of annotations at that coordinate.

You can see this only matches on exactly equal coordinates, but it would be relatively straightforward to group on coordinates that were close by calculating the distance between them.

Next we enumerate the dictionary looking for locations that have more than one annotation. When we find one, we reposition the annotations.

double distance = 3 * annotations.count / 2.0;
double radiansBetweenAnnotations = (M_PI * 2) / annotations.count;

for (int i = 0; i < annotations.count; i++) {

    double heading = radiansBetweenAnnotations * i;
    CLLocationCoordinate2D newCoordinate = [self calculateCoordinateFrom:coordinate onBearing:heading atDistance:distance];

    id  annotation = annotations[i];
    annotation.coordinate = newCoordinate;
}

The pins are arranged in a circle around the contested point by dividing the circle by the number of contesting annotations. You can see that the distance from the contested coordinate to the new coordinate is a function of the number of annotations contesting – if there are few pins contesting the coordinate, then we have space to place the pins close to the coordinate.

MKMapView Annotation Pins

Finally, the new coordinate is calculated using an implementation of the function from this excellent resource: Destination point given distance and bearing from start point

+ (CLLocationCoordinate2D)calculateCoordinateFrom:(CLLocationCoordinate2D)coordinate onBearing:(double)bearingInRadians atDistance:(double)distanceInMetres {

    double coordinateLatitudeInRadians = coordinate.latitude * M_PI / 180;
    double coordinateLongitudeInRadians = coordinate.longitude * M_PI / 180;

    double distanceComparedToEarth = distanceInMetres / 6378100;

    double resultLatitudeInRadians = asin(sin(coordinateLatitudeInRadians) * cos(distanceComparedToEarth) + cos(coordinateLatitudeInRadians) * sin(distanceComparedToEarth) * cos(bearingInRadians));
    double resultLongitudeInRadians = coordinateLongitudeInRadians + atan2(sin(bearingInRadians) * sin(distanceComparedToEarth) * cos(coordinateLatitudeInRadians), cos(distanceComparedToEarth) - sin(coordinateLatitudeInRadians) * sin(resultLatitudeInRadians));

    CLLocationCoordinate2D result;
    result.latitude = resultLatitudeInRadians * 180 / M_PI;
    result.longitude = resultLongitudeInRadians * 180 / M_PI;
    return result;
}

Search

Categories

Archives

Subscribe to Email Updates

Subscribe
 

We are a digital transformation consultancy. We help our clients succeed.

View Services