Question

I've got quite a lot of pins to put on my map so I think it would be a nice idea to cluster those annotations. I'm not really sure how to achieve this on iPhone, I was able to work something out with google maps and some javascript examples. But iPhone uses its mkmapview and I have no idea how to cluster annotations in there.

Any ideas or frameworks that you know and are good? Thanks.

Was it helpful?

Solution

Since this is a very common problem and i needed a solution i have wrote a custom subclass of MKMapView which supports clustering. Then i made it available open source! You can get it here: https://github.com/yinkou/OCMapView.

It manages the clustering of the annotations and you can handle their views by yourself. You don't have to do anything but to copy the OCMapView folder to your project, create a MKMapView in your nib and set its class to OCMapView. (Or create and delegate it in code like a regular MKMapView)

OTHER TIPS

You don't necessarily need to use a 3rd party framework because since iOS 4.2, MKMapView has a method called - (NSSet *)annotationsInMapRect:(MKMapRect)mapRect which you can use to do your clustering.

Check out the WWDC11 Session video 'Visualizing Information Geographically with MapKit'. About half way through it explains how to do it. But I'll summarize the concept for you:

  • Use Two maps (second map is never added to the view hierarchy)
  • Second map contains all annotations (again, it's never drawn)
  • Divide map area into a grid of squares
  • Use -annotationsInMapRect method to get annotation data from invisible map
  • Visible map builds its annotations from this data from invisible map

enter image description here

Fortunately, you don't need 3rd party framework's anymore. iOS 11 has native clustering support.

You need to implement mapView:clusterAnnotationForMemberAnnotations: method.

Get more details in the Apple example: https://developer.apple.com/sample-code/wwdc/2017/MapKit-Sample.zip

By using Apple demo code it's easy to implement clustering concept in our code. Reference link

Simply we can use following code for the Clustering

Steps to implement clustering

Step1 : The important thing is for clustering we use two mapviews(allAnnotationsMapView, ), One is for reference(allAnnotationsMapView).

@property (nonatomic, strong) MKMapView *allAnnotationsMapView;
@property (nonatomic, strong) IBOutlet MKMapView *mapView;

In viewDidLoad

    _allAnnotationsMapView = [[MKMapView alloc] initWithFrame:CGRectZero];

Step2 : Add all annotations to the _allAnnotationsMapView, In below _photos are the annotations array.

        [_allAnnotationsMapView addAnnotations:_photos];
        [self updateVisibleAnnotations];

Step3 : Add below methods for clustering, in this PhotoAnnotation is the custom annotation. MapViewDelegate methods

 - (void)mapView:(MKMapView *)aMapView regionDidChangeAnimated:(BOOL)animated {

    [self updateVisibleAnnotations];
}

- (void)mapView:(MKMapView *)aMapView didAddAnnotationViews:(NSArray *)views {

    for (MKAnnotationView *annotationView in views) {
        if (![annotationView.annotation isKindOfClass:[PhotoAnnotation class]]) {
            continue;
        }

        PhotoAnnotation *annotation = (PhotoAnnotation *)annotationView.annotation;

        if (annotation.clusterAnnotation != nil) {
            // animate the annotation from it's old container's coordinate, to its actual coordinate
            CLLocationCoordinate2D actualCoordinate = annotation.coordinate;
            CLLocationCoordinate2D containerCoordinate = annotation.clusterAnnotation.coordinate;

            // since it's displayed on the map, it is no longer contained by another annotation,
            // (We couldn't reset this in -updateVisibleAnnotations because we needed the reference to it here
            // to get the containerCoordinate)
            annotation.clusterAnnotation = nil;

            annotation.coordinate = containerCoordinate;

            [UIView animateWithDuration:0.3 animations:^{
                annotation.coordinate = actualCoordinate;
            }];
        }
    }
}

clustering Handling methods

    - (id<MKAnnotation>)annotationInGrid:(MKMapRect)gridMapRect usingAnnotations:(NSSet *)annotations {

    // first, see if one of the annotations we were already showing is in this mapRect
    NSSet *visibleAnnotationsInBucket = [self.mapView annotationsInMapRect:gridMapRect];
    NSSet *annotationsForGridSet = [annotations objectsPassingTest:^BOOL(id obj, BOOL *stop) {
        BOOL returnValue = ([visibleAnnotationsInBucket containsObject:obj]);
        if (returnValue)
        {
            *stop = YES;
        }
        return returnValue;
    }];

    if (annotationsForGridSet.count != 0) { 
        return [annotationsForGridSet anyObject];
    }

    // otherwise, sort the annotations based on their distance from the center of the grid square,
    // then choose the one closest to the center to show
    MKMapPoint centerMapPoint = MKMapPointMake(MKMapRectGetMidX(gridMapRect), MKMapRectGetMidY(gridMapRect));
    NSArray *sortedAnnotations = [[annotations allObjects] sortedArrayUsingComparator:^(id obj1, id obj2) {
        MKMapPoint mapPoint1 = MKMapPointForCoordinate(((id<MKAnnotation>)obj1).coordinate);
        MKMapPoint mapPoint2 = MKMapPointForCoordinate(((id<MKAnnotation>)obj2).coordinate);

        CLLocationDistance distance1 = MKMetersBetweenMapPoints(mapPoint1, centerMapPoint);
        CLLocationDistance distance2 = MKMetersBetweenMapPoints(mapPoint2, centerMapPoint);

        if (distance1 < distance2) {
            return NSOrderedAscending;
        } else if (distance1 > distance2) {
            return NSOrderedDescending;
        }

        return NSOrderedSame;
    }];

    PhotoAnnotation *photoAnn = sortedAnnotations[0];
    NSLog(@"lat long %f %f", photoAnn.coordinate.latitude, photoAnn.coordinate.longitude);

    return sortedAnnotations[0];
}

- (void)updateVisibleAnnotations {

    // This value to controls the number of off screen annotations are displayed.
    // A bigger number means more annotations, less chance of seeing annotation views pop in but decreased performance.
    // A smaller number means fewer annotations, more chance of seeing annotation views pop in but better performance.
    static float marginFactor = 2.0;

    // Adjust this roughly based on the dimensions of your annotations views.
    // Bigger numbers more aggressively coalesce annotations (fewer annotations displayed but better performance).
    // Numbers too small result in overlapping annotations views and too many annotations on screen.
    static float bucketSize = 60.0;

    // find all the annotations in the visible area + a wide margin to avoid popping annotation views in and out while panning the map.
    MKMapRect visibleMapRect = [self.mapView visibleMapRect];
    MKMapRect adjustedVisibleMapRect = MKMapRectInset(visibleMapRect, -marginFactor * visibleMapRect.size.width, -marginFactor * visibleMapRect.size.height);

    // determine how wide each bucket will be, as a MKMapRect square
    CLLocationCoordinate2D leftCoordinate = [self.mapView convertPoint:CGPointZero toCoordinateFromView:self.view];
    CLLocationCoordinate2D rightCoordinate = [self.mapView convertPoint:CGPointMake(bucketSize, 0) toCoordinateFromView:self.view];
    double gridSize = MKMapPointForCoordinate(rightCoordinate).x - MKMapPointForCoordinate(leftCoordinate).x;
    MKMapRect gridMapRect = MKMapRectMake(0, 0, gridSize, gridSize);

    // condense annotations, with a padding of two squares, around the visibleMapRect
    double startX = floor(MKMapRectGetMinX(adjustedVisibleMapRect) / gridSize) * gridSize;
    double startY = floor(MKMapRectGetMinY(adjustedVisibleMapRect) / gridSize) * gridSize;
    double endX = floor(MKMapRectGetMaxX(adjustedVisibleMapRect) / gridSize) * gridSize;
    double endY = floor(MKMapRectGetMaxY(adjustedVisibleMapRect) / gridSize) * gridSize;

    // for each square in our grid, pick one annotation to show
    gridMapRect.origin.y = startY;
    while (MKMapRectGetMinY(gridMapRect) <= endY) {
        gridMapRect.origin.x = startX;

        while (MKMapRectGetMinX(gridMapRect) <= endX) {
            NSSet *allAnnotationsInBucket = [self.allAnnotationsMapView annotationsInMapRect:gridMapRect];
            NSSet *visibleAnnotationsInBucket = [self.mapView annotationsInMapRect:gridMapRect];

            // we only care about PhotoAnnotations
            NSMutableSet *filteredAnnotationsInBucket = [[allAnnotationsInBucket objectsPassingTest:^BOOL(id obj, BOOL *stop) {
                return ([obj isKindOfClass:[PhotoAnnotation class]]);
            }] mutableCopy];

            if (filteredAnnotationsInBucket.count > 0) {
                PhotoAnnotation *annotationForGrid = (PhotoAnnotation *)[self annotationInGrid:gridMapRect usingAnnotations:filteredAnnotationsInBucket];

                [filteredAnnotationsInBucket removeObject:annotationForGrid];

                // give the annotationForGrid a reference to all the annotations it will represent
                annotationForGrid.containedAnnotations = [filteredAnnotationsInBucket allObjects];

                [self.mapView addAnnotation:annotationForGrid];

                for (PhotoAnnotation *annotation in filteredAnnotationsInBucket) {
                    // give all the other annotations a reference to the one which is representing them
                    annotation.clusterAnnotation = annotationForGrid;
                    annotation.containedAnnotations = nil;

                    // remove annotations which we've decided to cluster
                    if ([visibleAnnotationsInBucket containsObject:annotation]) {
                        CLLocationCoordinate2D actualCoordinate = annotation.coordinate;
                        [UIView animateWithDuration:0.3 animations:^{
                            annotation.coordinate = annotation.clusterAnnotation.coordinate;
                        } completion:^(BOOL finished) {
                            annotation.coordinate = actualCoordinate;
                            [self.mapView removeAnnotation:annotation];
                        }];
                    }
                }
            }

            gridMapRect.origin.x += gridSize;
        }

        gridMapRect.origin.y += gridSize;
    }
}

By following above steps we can achieve clustering on mapview, it is not necessary to use any third party code or framework. Please check the Apple sample code here. Please let me know if you have any doubts on this.

Have you looked at ADClusterMapView ? https://github.com/applidium/ADClusterMapView

It does precisely just this.

I just wanted to clustering pins, just showing its number. The following one https://www.cocoacontrols.com/controls/qtree-objc fits my expectations.

I recently forked off of ADClusterMapView mentioned in another answer and resolved many, if not all, of the issues associated with the project. It's a kd-tree algorithm and animates the clustering.

It's available open source here https://github.com/ashare80/TSClusterMapView

Try this framework (XMapView.framework); it now supports iOS 8.

This framework doesn't need you to change your current project structure and it can directly be used to your MKMapView. There is a zip file. It gives you an example to cluster 200 pins at once. After I tested it in an iPod I found it is very smooth.

http://www.xuliu.info/xMapView.html

This library supports:

  1. clustering different categories
  2. clustering all categories
  3. setting up your own cluster radius and so on
  4. hide or show a certain of categories
  5. individually handle and control each pin in the map

There is a pretty cool and well maintained library for both Objective-C and Swift here: https://github.com/bigfish24/ABFRealmMapView

It does clustering really well and also handles large amounts of points due to its integration with Realm.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top