使用适用于 iOS 的谷歌地图 SDK 进行标记聚类?
Posted
技术标签:
【中文标题】使用适用于 iOS 的谷歌地图 SDK 进行标记聚类?【英文标题】:Marker clustering with google maps SDK for iOS? 【发布时间】:2013-12-09 03:33:13 【问题描述】:我在我的 ios 应用程序中使用 Google Maps SDK,我需要对彼此非常接近的标记进行分组 - 基本上需要使用标记聚类,如附加 url 中所示。我能够在 android 地图 SDK 中获得此功能,但我没有找到任何适用于 iOS 谷歌地图 SDK 的库。
您能为此推荐任何图书馆吗? 或者建议一种为此实现自定义库的方法?
(这张照片的Source)
【问题讨论】:
好奇 google SDK 是否比 MapKit 更好... 目前这个功能已经在 Android Google Maps SDK 而不是 iOS Google Maps SDK 中实现也很奇怪(讽刺和讽刺)。真是巧合哈哈。 我首先在 Apple Maps 中实现了相同的应用程序(地图上大约 500 个图钉),然后切换到 Google Maps!结果:即使我使用相同的 1 KB gif 图像作为谷歌地图的 pin 图像,它也非常慢 -> 由于谷歌地图没有的 dequeueReusableAnnotationViewWithIdentifier 函数,Apple 地图的性能非常糟糕! 【参考方案1】:要了解此双地图解决方案的基本概念,请查看此WWDC 2011 video(从 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];
几点说明:
PointMapItem
是我的注释数据类(id<MKAnnotation>
,如果我们使用 Map kit)。
这里我在PointMapItem
上使用shouldBeMerged
属性,因为有些注释我不想合并。如果您不需要它,请删除正在使用它的部分或将所有注释的 shouldBeMerged
设置为 YES。不过,如果您不想合并用户位置,您可能应该继续进行课程测试!
当你想添加注释时,将它们添加到隐藏的allAnnotationMapView
并调用updateVisibleAnnotation
。 updateVisibleAnnotation
方法负责选择要合并和显示哪些注释。然后它将注释添加到可见的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];
两者都应该可以正常工作,但如果您有任何问题,请随时提问!
【讨论】:
非常感谢您分享这个!我花了一点时间弄清楚 PointMapItem 应该是什么样子。如果其他人想知道:gist.github.com/plu/b3835e6afe7538fada15 对于未来的读者:请注意,我是在 GMSMapView 类上提供markers
属性时写的。它不再是最新版本的 SDK。但是您可以通过将它们放入您自己的数组中来跟踪您添加的所有标记,并使用此数组而不是调用 mapView.markers。
我可以使用 KMPointMapItem 类 gist.github.com/plu/b3835e6afe7538fada15 代替 PointMapItem 吗?
@Aurelien Porte 在你的代码中添加 cmets 对那些试图理解和使用它的人来说真的很有帮助,但感谢你与社区分享你的源代码。
上面的许多问题都没有回答@AurelienPorte 你能把我们链接到缺少的类,比如 PointMapItem 吗?【参考方案2】:
经过长时间的研究,我终于找到了一个很棒的人。
非常感谢您DDRBoxman。
查看他的github:https://github.com/DDRBoxman/google-maps-ios-utils
他最近推送了一些代码示例。
当我想运行他的项目时,我遇到了一些问题。我刚刚删除了 Google Maps SDK 并按照完整的 Google 教程来集成 Google Maps SDK。然后,没有更多问题,我能够运行该应用程序。 不要忘记将您的 API KEY 放入 AppDelegate.m。
我将在接下来的几天里使用这个库,如果我发现一些错误,我会告诉你。
EDIT #1:这些天我在集群上做了很多工作。我的最后一种方法是集成 MKMapView,在 MKMapView 上创建集群(比在 iOS 版 Google Maps SDK 上更容易)并将 Google Maps Places 集成到我的 iOS 项目中。 这种方法的性能比前一种方法更好。
编辑 #2:我不知道您是否使用 Realm,或者您是否打算使用它,但它们为地图聚类提供了一个非常好的解决方案:https://realm.io/news/building-an-ios-clustered-map-view-in-objective-c/
【讨论】:
每次重绘后我都有很大的滞后。像2秒的延迟。不太好用。那是在 iPhone 4s 上 是的,正如我在编辑#1 中所说,我停止使用谷歌地图进行聚类。将 MKMapView 与 Google Places API 结合使用确实更快。 干得好!谷歌地图确实做得很好。该项目提供了一个可用的原型来进行地图聚类。您可以更改其集群渲染器算法以提高性能。对于我的修改,我只渲染地图视图端口内部或附近的集群,而不是渲染所有集群。 如何区分轻按标记和轻按集群?为什么[clusterManager_.items count]
总是 0?
我想检查 cluterManager 项目是否包含被点击的标记点(标记),但它似乎是空的 [clusterManager_.items count]
总是 0..【参考方案3】:
我有一个应用程序处理这个问题,下面是代码
循环数组中的所有标记(nsdictionary)
使用 gmsmapview.projection 获取 CGPoint 以确定标记是否应该组合在一起
3 我用100分测试,响应时间很满意。
4如果缩放级别差异超过0.5,地图将重绘;
-(float)distance :(CGPoint)pointA point:(CGPoint) pointB
return sqrt( (pow((pointA.x - pointB.x),2) + pow((pointA.y-pointB.y),2)));
-(void)mapView:(GMSMapView *)mapView didChangeCameraPosition:(GMSCameraPosition *)position
float currentZoomLevel = mapView.camera.zoom;
if (fabs(currentZoomLevel- lastZoomLevel_)>0.5)
lastZoomLevel_ = currentZoomLevel;
markersGroupArray_ = [[NSMutableArray alloc] init];
for (NSDictionary *photo in photoArray_)
float coordx = [[photo objectForKey:@"coordx"]floatValue];
float coordy = [[photo objectForKey:@"coordy"] floatValue];
CLLocationCoordinate2D coord = CLLocationCoordinate2DMake(coordx, coordy);
CGPoint currentPoint = [mapView.projection pointForCoordinate:coord];
if ([markersGroupArray_ count] == 0)
NSMutableArray *array = [[NSMutableArray alloc] initWithObjects:photo, nil];
[markersGroupArray_ addObject:array];
else
bool flag_groupadded = false;
int counter= 0;
for (NSMutableArray *array in markersGroupArray_)
for (NSDictionary *marker in array)
float mcoordx = [[marker objectForKey:@"coordx"]floatValue];
float mcoordy = [[marker objectForKey:@"coordy"]floatValue];
CLLocationCoordinate2D mcoord = CLLocationCoordinate2DMake(mcoordx, mcoordy);
CGPoint mpt = [mapView.projection pointForCoordinate:mcoord];
if ([self distance:mpt point:currentPoint] <30)
flag_groupadded = YES;
break;
if (flag_groupadded)
break;
counter++;
if (flag_groupadded)
if ([markersGroupArray_ count]>counter)
NSMutableArray *groupArray = [markersGroupArray_ objectAtIndex:counter];
[groupArray insertObject:photo atIndex:0];
[markersGroupArray_ replaceObjectAtIndex:counter withObject:groupArray];
else if (!flag_groupadded)
NSMutableArray * array = [[NSMutableArray alloc]initWithObjects:photo, nil];
[markersGroupArray_ addObject:array];
// for loop for photoArray
// display group point
[mapView clear];
photoMarkers_ = [[NSMutableArray alloc] init];
for (NSArray *array in markersGroupArray_)
NSLog(@"arry count %d",[array count]);
NSDictionary *item = [array objectAtIndex:0];
float coordx = [[item objectForKey:@"coordx"]floatValue];
float coordy = [[item objectForKey:@"coordy"] floatValue];
CLLocationCoordinate2D coord = CLLocationCoordinate2DMake(coordx, coordy);
GMSMarker *marker = [[GMSMarker alloc] init];
marker.position = coord;
marker.map = mapView;
[photoMarkers_ addObject:marker];
marker = nil;
NSLog(@"markers %@",photoMarkers_);
// zoomlevel diffference thersold
【讨论】:
请检查此链接我想显示标记动画。请看我的问题***.com/questions/23541930/… @chings228 在你的代码中添加 cmets 对那些试图理解和使用它的人来说真的很有帮助,但感谢你与社区分享你的源代码。 @chings228 如果可以,请将 cmets 放入您的代码中,这将非常有帮助!谢谢以上是关于使用适用于 iOS 的谷歌地图 SDK 进行标记聚类?的主要内容,如果未能解决你的问题,请参考以下文章
用于 IOS 的谷歌分析 SDK 不再跟踪屏幕或事件。曾经工作
使用markerclusterer v3获取谷歌地图范围内的标记列表