问题描述
我在我的 iOS 应用程序中使用 Google Maps SDK,我需要对彼此非常接近的标记进行分组 - 基本上需要使用标记聚类,如附加 url 中所示.我能够在 Android 地图 SDK 中获得此功能,但我没有找到任何适用于 iOS Google Maps SDK 的库.
I am using Google Maps SDK in my iOS app, and I need to group markers which are very close to each other - basically need to use marker clustering like its shown in the attached url. I am able to get this functionality in the Android maps SDK, but I didn't find any library for the iOS Google Maps SDK.
你能为此推荐任何图书馆吗?或者建议一种为此实现自定义库的方法?
Can you please suggest any library for this? Or suggest a way to implement a custom library for this?
(这张图片的来源)>
(Source of this picture)
推荐答案
要了解这个双地图解决方案的底层概念,请看这个WWDC 2011 视频(来自 22'30).除了我在一些注释中描述的一些内容外,地图套件代码直接从该视频中提取.Google Map SDK 解决方案只是一种改编.
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.
主要思想:地图被隐藏并保存每个注释,包括合并的注释(在我的代码中为 allAnnotationMapView
).另一个是可见的,仅显示集群的注释或注释(如果它是单个的)(我的代码中的 mapView).
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.
我用于 Google Maps SDK 的代码(请注意,当 markers
属性在 GMSMapView 类上可用时,我编写了此代码.它不再存在,但您可以跟踪添加到您的自己的数组,并使用这个数组而不是调用 mapView.markers):
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];
}
}
一些注意事项:
PointMapItem
是我的注释数据类(id
,如果我们使用的是 Map kit).- 这里我在
PointMapItem
上使用了一个shouldBeMerged
属性,因为有些注释我不想合并.如果您不需要它,请删除正在使用它的部分或将所有注释的shouldBeMerged
设置为 YES.不过,如果您不想合并用户位置,您可能应该保持类测试! - 当你想添加注解时,将它们添加到隐藏的
allAnnotationMapView
并调用updateVisibleAnnotation
.updateVisibleAnnotation
方法负责选择要合并的注释和显示的注释.然后它将注释添加到可见的mapView
.
PointMapItem
is my annotation data class (id<MKAnnotation>
if we were working with Map kit).- Here I use a
shouldBeMerged
property onPointMapItem
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 setshouldBeMerged
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 callupdateVisibleAnnotation
.updateVisibleAnnotation
method is responsible for choosing which annotations to merge and which to show. It will then add the annotation tomapView
which is visible.
对于 Map Kit,我使用以下代码:
For Map Kit I use the following code:
- (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];
}
两者都应该可以正常工作,但如果您有任何问题,请随时提出!
Both should work fine, but if you have any question, feel free to ask!
这篇关于使用适用于 iOS 的谷歌地图 SDK 进行标记聚类?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!