Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Grouped/Ungroup MKAnnotation depending on the zoom level (And keep it fast)

I have a mapview that has a lot of annotations, most of them are very close to each other. What I want to do is similar to the Photo app on iOS where annotations are grouped when they are too close to each other and whenever you zoom out, they un grouped if they are too far apart.

I have seen this question already but the answer given isn't really what I was looking for.

I'm looking for either a library or a algorithm that I could implement myself.

like image 462
Pier-Olivier Thibault Avatar asked Aug 20 '11 13:08

Pier-Olivier Thibault


2 Answers

One of Apple's WWDC 2011 session videos shows how to do exactly this. Go to https://developer.apple.com/videos/wwdc/2011/ (must be registered developer) and scroll to the video titled "Visualizing Information Geographically with MapKit". The basic idea is to use an offscreen map view to hold all of your annotations, and copy them to the onscreen map view as needed, making sure that you're not trying to show too many at once. It even does the nifty animation with the annotations as you zoom.

like image 138
shawkinaw Avatar answered Oct 15 '22 02:10

shawkinaw


Have a look here in order to see the full answer. It contains both implementations for MapKit and Google Maps. The code is inspired by WWDC 2011 video and works very well in my app.

I post the code for MapKit here anyway but there are a few useful remarks in my other answer.

- (void)didZoom:(UIGestureRecognizer*)gestureRecognizer {
    if (gestureRecognizer.state == UIGestureRecognizerStateEnded){
        [self updateVisibleAnnotations];
    }
}
- (void)updateVisibleAnnotations {
    static float marginFactor = 2.0f;
    static float bucketSize = 50.0f;
    MKMapRect visibleMapRect = [self.mapView visibleMapRect];
    MKMapRect adjustedVisibleMapRect = MKMapRectInset(visibleMapRect, -marginFactor * visibleMapRect.size.width, -marginFactor * visibleMapRect.size.height);

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

    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;

    gridMapRect.origin.y = startY;
    while(MKMapRectGetMinY(gridMapRect) <= endY) {
        gridMapRect.origin.x = startX;
        while (MKMapRectGetMinX(gridMapRect) <= endX) {
            NSSet *allAnnotationsInBucket = [self.allAnnotationMapView annotationsInMapRect:gridMapRect];
            NSSet *visibleAnnotationsInBucket = [self.mapView annotationsInMapRect:gridMapRect];

            NSMutableSet *filteredAnnotationsInBucket = [[allAnnotationsInBucket objectsPassingTest:^BOOL(id obj, BOOL *stop) {
                BOOL isPointMapItem = [obj isKindOfClass:[PointMapItem class]];
                BOOL shouldBeMerged = NO;
                if (isPointMapItem) {
                    PointMapItem *pointItem = (PointMapItem *)obj;
                    shouldBeMerged = pointItem.shouldBeMerged;
                }
                return shouldBeMerged;
            }] mutableCopy];
            NSSet *notMergedAnnotationsInBucket = [allAnnotationsInBucket objectsPassingTest:^BOOL(id obj, BOOL *stop) {
                BOOL isPointMapItem = [obj isKindOfClass:[PointMapItem class]];
                BOOL shouldBeMerged = NO;
                if (isPointMapItem) {
                    PointMapItem *pointItem = (PointMapItem *)obj;
                    shouldBeMerged = pointItem.shouldBeMerged;
                }
                return isPointMapItem && !shouldBeMerged;
            }];
            for (PointMapItem *item in notMergedAnnotationsInBucket) {
                [self.mapView addAnnotation:item];
            }

            if(filteredAnnotationsInBucket.count > 0) {
                PointMapItem *annotationForGrid = (PointMapItem *)[self annotationInGrid:gridMapRect usingAnnotations:filteredAnnotationsInBucket];
                [filteredAnnotationsInBucket removeObject:annotationForGrid];
                annotationForGrid.containedAnnotations = [filteredAnnotationsInBucket allObjects];
                [self.mapView addAnnotation:annotationForGrid];
                //force reload of the image because it's not done if annotationForGrid is already present in the bucket!!
                MKAnnotationView* annotationView = [self.mapView viewForAnnotation:annotationForGrid];
                NSString *imageName = [AnnotationsViewUtils imageNameForItem:annotationForGrid selected:NO];
                UILabel *countLabel = [[UILabel alloc] initWithFrame:CGRectMake(15, 2, 8, 8)];
                [countLabel setFont:[UIFont fontWithName:POINT_FONT_NAME size:10]];
                [countLabel setTextColor:[UIColor whiteColor]];
                [annotationView addSubview:countLabel];
                imageName = [AnnotationsViewUtils imageNameForItem:annotationForGrid selected:NO];
                annotationView.image = [UIImage imageNamed:imageName];

                if (filteredAnnotationsInBucket.count > 0){
                    [self.mapView deselectAnnotation:annotationForGrid animated:NO];
                }
                for (PointMapItem *annotation in filteredAnnotationsInBucket) {
                    [self.mapView deselectAnnotation:annotation animated:NO];
                    annotation.clusterAnnotation = annotationForGrid;
                    annotation.containedAnnotations = nil;
                    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;
    }
}

- (id<MKAnnotation>)annotationInGrid:(MKMapRect)gridMapRect usingAnnotations:(NSSet *)annotations {
    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];
    }
    MKMapPoint centerMapPoint = MKMapPointMake(MKMapRectGetMinX(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;
    }];
    return [sortedAnnotations objectAtIndex:0];
} 
like image 22
Aurelien Porte Avatar answered Oct 15 '22 00:10

Aurelien Porte