To understand the underlying concept of this double map solution, please have a look at this WWDC 2011 video (from 22'30). The Map kit code is directly extracted from this video except a few things that I described in a few notes. The Google Map SDK solution is just an adaptation.
Main idea: a map is hidden and holds every single annotation, including the merged ones (allAnnotationMapView
in my code). Another is visible and shows only the cluster's annotations or the annotation if it's single (mapView in my code).
Second main idea: I divide the visible map (plus a margin) into squares, and every annotation in a specific square are merged into one annotation.
The code I use for Google Maps SDK (Please note that I wrote this when markers
property were available on GMSMapView class. It's not anymore but you can keep track of all the marker you add in your own array, and use this array instead of calling mapView.markers):
- (void)loadView {
[super loadView];
self.mapView = [[GMSMapView alloc] initWithFrame:self.view.frame];
self.mapView.delegate = self;
self.allAnnotationMapView = [[GMSMapView alloc] initWithFrame:self.view.frame]; // can't be zero or you'll have weard results (I don't remember exactly why)
self.view = self.mapView;
UIPinchGestureRecognizer* pinchRecognizer = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(didZoom:)];
[pinchRecognizer setDelegate:self];
[self.mapView addGestureRecognizer:pinchRecognizer];
}
- (void)didZoom:(UIGestureRecognizer*)gestureRecognizer {
if (gestureRecognizer.state == UIGestureRecognizerStateEnded){
[self updateVisibleAnnotations];
}
}
- (float)distanceFrom:(CGPoint)point1 to:(CGPoint)point2 {
CGFloat xDist = (point2.x - point1.x);
CGFloat yDist = (point2.y - point1.y);
return sqrt((xDist * xDist) + (yDist * yDist));
}
- (NSSet *)annotationsInRect:(CGRect)rect forMapView:(GMSMapView *)mapView {
GMSProjection *projection = self.mapView.projection; //always take self.mapView because it is the only one zoomed on screen
CLLocationCoordinate2D southWestCoordinates = [projection coordinateForPoint:CGPointMake(rect.origin.x, rect.origin.y + rect.size.height)];
CLLocationCoordinate2D northEastCoordinates = [projection coordinateForPoint:CGPointMake(rect.origin.x + rect.size.width, rect.origin.y)];
NSMutableSet *annotations = [NSMutableSet set];
for (GMSMarker *marker in mapView.markers) {
if (marker.position.latitude < southWestCoordinates.latitude || marker.position.latitude >= northEastCoordinates.latitude) {
continue;
}
if (marker.position.longitude < southWestCoordinates.longitude || marker.position.longitude >= northEastCoordinates.longitude) {
continue;
}
[annotations addObject:marker.userData];
}
return annotations;
}
- (GMSMarker *)viewForAnnotation:(PointMapItem *)item forMapView:(GMSMapView *)mapView{
for (GMSMarker *marker in mapView.markers) {
if (marker.userData == item) {
return marker;
}
}
return nil;
}
- (void)updateVisibleAnnotations {
static float marginFactor = 1.0f;
static float bucketSize = 100.0f;
CGRect visibleMapRect = self.view.frame;
CGRect adjustedVisibleMapRect = CGRectInset(visibleMapRect, -marginFactor * visibleMapRect.size.width, -marginFactor * visibleMapRect.size.height);
double startX = CGRectGetMinX(adjustedVisibleMapRect);
double startY = CGRectGetMinY(adjustedVisibleMapRect);
double endX = CGRectGetMaxX(adjustedVisibleMapRect);
double endY = CGRectGetMaxY(adjustedVisibleMapRect);
CGRect gridMapRect = CGRectMake(0, 0, bucketSize, bucketSize);
gridMapRect.origin.y = startY;
while(CGRectGetMinY(gridMapRect) <= endY) {
gridMapRect.origin.x = startX;
while (CGRectGetMinX(gridMapRect) <= endX) {
NSSet *allAnnotationsInBucket = [self annotationsInRect:gridMapRect forMapView:self.allAnnotationMapView];
NSSet *visibleAnnotationsInBucket = [self annotationsInRect:gridMapRect forMapView:self.mapView];
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 addAnnotation:item inMapView:self.mapView animated:NO];
}
if(filteredAnnotationsInBucket.count > 0) {
PointMapItem *annotationForGrid = (PointMapItem *)[self annotationInGrid:gridMapRect usingAnnotations:filteredAnnotationsInBucket];
[filteredAnnotationsInBucket removeObject:annotationForGrid];
annotationForGrid.containedAnnotations = [filteredAnnotationsInBucket allObjects];
[self removeAnnotation:annotationForGrid inMapView:self.mapView];
[self addAnnotation:annotationForGrid inMapView:self.mapView animated:NO];
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 removeAnnotation:annotation inMapView:self.mapView];
}];
}
}
}
gridMapRect.origin.x += bucketSize;
}
gridMapRect.origin.y += bucketSize;
}
}
- (PointMapItem *)annotationInGrid:(CGRect)gridMapRect usingAnnotations:(NSSet *)annotations {
NSSet *visibleAnnotationsInBucket = [self annotationsInRect:gridMapRect forMapView:self.mapView];
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];
}
CGPoint centerMapPoint = CGPointMake(CGRectGetMidX(gridMapRect), CGRectGetMidY(gridMapRect));
NSArray *sortedAnnotations = [[annotations allObjects] sortedArrayUsingComparator:^(id obj1, id obj2) {
CGPoint mapPoint1 = [self.mapView.projection pointForCoordinate:((PointMapItem *)obj1).coordinate];
CGPoint mapPoint2 = [self.mapView.projection pointForCoordinate:((PointMapItem *)obj2).coordinate];
CLLocationDistance distance1 = [self distanceFrom:mapPoint1 to:centerMapPoint];
CLLocationDistance distance2 = [self distanceFrom:mapPoint2 to:centerMapPoint];
if (distance1 < distance2) {
return NSOrderedAscending;
}
else if (distance1 > distance2) {
return NSOrderedDescending;
}
return NSOrderedSame;
}];
return [sortedAnnotations objectAtIndex:0];
return nil;
}
- (void)addAnnotation:(PointMapItem *)item inMapView:(GMSMapView *)mapView {
[self addAnnotation:item inMapView:mapView animated:YES];
}
- (void)addAnnotation:(PointMapItem *)item inMapView:(GMSMapView *)mapView animated:(BOOL)animated {
GMSMarker *marker = [[GMSMarker alloc] init];
GMSMarkerAnimation animation = kGMSMarkerAnimationNone;
if (animated) {
animation = kGMSMarkerAnimationPop;
}
marker.appearAnimation = animation;
marker.title = item.title;
marker.icon = [[AnnotationsViewUtils getInstance] imageForItem:item];
marker.position = item.coordinate;
marker.map = mapView;
marker.userData = item;
// item.associatedMarker = marker;
}
- (void)addAnnotations:(NSArray *)items inMapView:(GMSMapView *)mapView {
[self addAnnotations:items inMapView:mapView animated:YES];
}
- (void)addAnnotations:(NSArray *)items inMapView:(GMSMapView *)mapView animated:(BOOL)animated {
for (PointMapItem *item in items) {
[self addAnnotation:item inMapView:mapView];
}
}
- (void)removeAnnotation:(PointMapItem *)item inMapView:(GMSMapView *)mapView {
// Try to make that work because it avoid loopigng through all markers each time we just want to delete one...
// Plus, your associatedMarker property should be weak to avoid memory cycle because userData hold strongly the item
// GMSMarker *marker = item.associatedMarker;
// marker.map = nil;
for (GMSMarker *marker in mapView.markers) {
if (marker.userData == item) {
marker.map = nil;
}
}
}
- (void)removeAnnotations:(NSArray *)items inMapView:(GMSMapView *)mapView {
for (PointMapItem *item in items) {
[self removeAnnotation:item inMapView:mapView];
}
}
A few notes:
PointMapItem
is my annotation data class (id<MKAnnotation>
if we were working with Map kit).
- Here I use a
shouldBeMerged
property on PointMapItem
because there are some annotations I don't want to merge. If you do not need this, remove the part that is using it or set shouldBeMerged
to YES for all your annotations. Though, you should probably keep the class testing if you don't want to merge user location!
- When you want to add annotations, add them to the hidden
allAnnotationMapView
and call updateVisibleAnnotation
. updateVisibleAnnotation
method is responsible for choosing which annotations to merge and which to show. It will then add the annotation to mapView
which is visible.
For Map Kit I use the following code:
- (void)didZoom:(UIGestureRecognizer*)gestureRecognizer {
if (gestureRecognizer.state == UIGestureRecognizerStateEnded){
[self updateVisibleAnnotations];
}
}
- (void)updateVisibleAnnotations {
static f
与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…