iPhone MKMapView Annotation Clustering
Asked Answered
W

9

18

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.

Wadai answered 18/10, 2011 at 0:1 Comment(3)
github.com/stars?direction=desc&sort=created&q=clusterAirbrush
@brian.clear: well you queried link show nothing.Onagraceous
@AdilSoomro Well, it shows a search result to your personal stared github projects. In my case I was lucky to find github.com/choefele/CCHMapClusterController and github.com/thoughtbot/TBAnnotationClusteringThoer
B
13

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)

Buckbuckaroo answered 20/10, 2011 at 21:23 Comment(2)
The class mentioned in this answer has been abandoned by the OP. Last change is over a year ago.Myrticemyrtie
Hi, I'm the developer. Well it kinda was. I currently working on a swift port. :)Buckbuckaroo
H
40

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

Homeland answered 15/3, 2013 at 17:19 Comment(5)
where can i find the code demo-ed in this WWDC videoSpeedwell
I implemented this and it's OK...other clustering algorithms like quadtree or maybe optics probably display better but would be more complex to implementStasny
demo code is available at developer.apple.com/library/ios/samplecode/PhotoMap/…Mamiemamma
I downloaded the demo code from developer.apple.com/library/ios/samplecode/PhotoMap/… but I found that when I am rotating and zooming the map randomly it got stuck. please if you have any other demo then help me.Upstanding
Here is the rotating problem solution: https://mcmap.net/q/668738/-annotation-clustering-in-iphone-mkmapviewKulturkampf
B
16

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

Beerbohm answered 25/8, 2017 at 9:55 Comment(2)
How do you disable it in runtime? do you know how?Calefaction
@Calefaction Set the clusteringIdentifier of your annotation views to nilJodhpur
B
13

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)

Buckbuckaroo answered 20/10, 2011 at 21:23 Comment(2)
The class mentioned in this answer has been abandoned by the OP. Last change is over a year ago.Myrticemyrtie
Hi, I'm the developer. Well it kinda was. I currently working on a swift port. :)Buckbuckaroo
T
8

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.

Tune answered 22/6, 2015 at 8:53 Comment(0)
B
2

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

It does precisely just this.

Brummett answered 7/6, 2013 at 15:20 Comment(1)
I was very disappointed about this project. Make sure to read all the unsolved GitHub issues before doing too many changes in your code to support it. I didn't and wish I had...Supination
S
2

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

Supernormal answered 13/6, 2015 at 15:42 Comment(0)
R
1

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

Roman answered 27/1, 2015 at 6:19 Comment(1)
While this link may answer the question, it is better to include the essential parts of the answer here and provide the link for reference. Link-only answers can become invalid if the linked page changes.Electrotype
E
0

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
Evette answered 14/4, 2015 at 3:38 Comment(0)
B
0

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.

Bricklayer answered 3/10, 2015 at 0:29 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.