使用google maps SDK for iOS进行标记聚类?

san*_*kar 37 google-maps google-maps-sdk-ios

我在我的iOS应用程序中使用Google Maps SDK,我需要对彼此非常接近的标记进行分组 - 基本上需要使用标记聚类,如附加网址中所示.我能够在Android地图SDK中获得此功能,但我找不到iOS Google Maps SDK的任何库.

你可以为此建议任何图书馆吗?或者建议一种为此实现自定义库的方法?

Marker_Clusterer_Full.png

(此图片来源)

Aur*_*rte 26

要了解此双地图解决方案的基本概念,请查看此WWDC 2011视频(从22'30开始).地图套件代码直接从该视频中提取,除了我在几个笔记中描述的一些内容.Google Map SDK解决方案只是一种改编.

主要思想:隐藏地图并保存每一个注释,包括合并的(allAnnotationMapView在我的代码中).另一个是可见的,只显示集群的注释或注释,如果它是单一的(我的代码中的mapView).

第二个主要思想:我将可见地图(加上边距)划分为正方形,并将特定正方形中的每个注释合并为一个注释.

我用于Google Maps SDK的代码(请注意,当我markers在GMSMapView类上提供属性时,我写了这个.它不再是你可以跟踪你在自己的数组中添加的所有标记,并使用这个数组而不是调用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];
    }
}
Run Code Online (Sandbox Code Playgroud)

几点说明:

  • PointMapItem是我的注释数据类(id<MKAnnotation>如果我们使用Map kit).
  • 这里我使用了一个shouldBeMerged属性,PointMapItem因为有一些我不想合并的注释.如果您不需要,请删除正在使用它的部分,或者shouldBeMerged为所有注释设置为YES.但是,如果您不想合并用户位置,您应该继续进行类测试!
  • 如果要添加注释,请将它们添加到隐藏allAnnotationMapView和调用updateVisibleAnnotation.updateVisibleAnnotationmethod负责选择要合并的注释和要显示的注释.然后它将添加mapView可见的注释.

对于Map Kit,我使用以下代码:

- (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];
}
Run Code Online (Sandbox Code Playgroud)

两者都应该可以正常工作,但如果您有任何疑问,请随时提出!

  • 对于未来的读者:请注意我在GMSMapView类上提供`markers`属性时写的这个.它不再是最新版本的SDK了.但是你可以通过将它们放在你自己的数组中来跟踪你添加的所有标记,并使用这个数组而不是调用mapView.markers. (3认同)
  • 非常感谢你分享这个!我想了解一下PointMapItem应该是什么样子.如果有人想知道:https://gist.github.com/plu/b3835e6afe7538fada15 (2认同)
  • @Aurelien Porte在代码中添加注释可能对尝试理解和使用它的人非常有帮助,但是感谢您与社区共享源代码. (2认同)

Tom*_*Tom 20

经过长时间的研究,我终于找到了一个做这件事的好人.

非常感谢DDRBoxman.

检查他的github:https://github.com/DDRBoxman/google-maps-ios-utils

他最近推出了一些代码示例.

当我想运行他的项目时,我遇到了一些问题.我刚刚删除了Google Maps SDK,并按照完整的Google教程来整合Google Maps SDK.然后,没有更多的问题,我能够运行该应用程序.不要忘记将API KEY放在AppDelegate.m中.

我将在接下来的几天使用这个库,如果我发现一些错误,我会通知你.

编辑#1:这些天我在集群上工作了很多.我的最终方法是集成MKMapView,在MKMapView上创建集群(比在Google Maps SDK for iOS上更容易),并将Google Maps Places集成到我的iOS项目中.这种方法的性能比前一种方法更好.

编辑#2:我不知道您是否使用Realm或者您是否打算使用它,但它们为地图聚类提供了一个非常好的解决方案:https://realm.io/news/building-an-ios-clustered-map -view合目标c /