web-dev-qa-db-ja.com

iOS用GoogleマップSDKを使用したマーカークラスタリング?

IOSアプリでGoogle Maps SDKを使用しており、互いに非常に近いマーカーをグループ化する必要があります。基本的に、添付のURLに示されているようなマーカークラスタリングを使用する必要があります。 Android maps SDKでこの機能を使用できますが、iOS Google Maps SDKのライブラリが見つかりませんでした。

このライブラリを提案していただけますか? またはこれにカスタムライブラリを実装する方法を提案しますか?

Marker_Clusterer_Full.png

ソース この写真の)

37
sanjaydhakar

このダブルマップソリューションの基本概念を理解するには、これをご覧ください WWDC 2011ビデオ (22'30から)。マップキットコードは、このビデオから直接抽出されています。ただし、いくつかのメモで説明したいくつかの点を除きます。 Google Map SDKソリューションは単なる適応です。

主なアイデア:マップは非表示であり、マージされた注釈(私のコードではallAnnotationMapView)を含むすべての注釈を保持します。もう1つは表示され、クラスターの注釈または単一の注釈(コードではmapView)のみを表示します。

2番目の主なアイデア:可視マップ(およびマージン)を正方形に分割し、特定の正方形のすべての注釈を1つの注釈にマージします。

Google Maps SDKに使用するコード(GMSMapViewクラスでmarkersプロパティが使用可能になったときにこれを作成したことに注意してください。これはもうありませんが、独自の配列に追加したすべてのマーカーを追跡して使用できます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>マップキットを使用していた場合)。
  • ここでは、shouldBeMergedPointMapItemプロパティを使用します。これは、マージしたくない注釈があるためです。これが必要ない場合は、それを使用している部分を削除するか、すべての注釈に対して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];
}

どちらも正常に機能するはずですが、質問がある場合は、お気軽にお問い合わせください!

26
Aurelien Porte

長時間の研究の後、私はついにこれを行った素晴らしい男を見つけました。

ありがとうDDRBoxman

彼のgithubを確認してください: https://github.com/DDRBoxman/google-maps-ios-utils

彼は最近、いくつかのコードサンプルをプッシュしました。

彼のプロジェクトを実行したいとき、いくつかの問題がありました。 Google Maps SDKを削除し、完全なGoogleチュートリアルに従ってGoogle Maps SDKを統合しました。その後、これ以上の問題はなく、アプリを実行できました。 API KEYをAppDelegate.mに入れることを忘れないでください。

次の数日間、このlibを使用します。バグが見つかった場合はお知らせします。

EDIT#1:私は最近クラスターで多くのことをしました。私の最後のアプローチは、MKMapViewを統合し、MKMapViewにクラスターを作成し(iOS用のGoogle Maps SDKで行うよりもはるかに簡単)、Google Maps PlacesをiOSプロジェクトに統合することです。このアプローチのパフォーマンスは、前のアプローチよりも優れています。

EDIT#2:Realmを使用しているか、それを使用する予定かはわかりませんが、マップクラスタリングに非常に優れたソリューションを提供します:- https://realm.io/news/building-an-ios-clustered-map-view-in-objective-c/

20
Tom

私はこの問題を処理するアプリを持っています、以下はコードです

  1. 配列内のすべてのマーカー(nsdictionary)をループします

  2. gmsmapview.projectionを使用してCGPointを取得し、マーカーをグループ化する必要があるかどうかを確認します

3 iは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


    }
2
chings228

これは、Googleマップで解決されましたIOS Utils。 https://developers.google.com/maps/documentation/ios-sdk/utility/marker-clustering

0
Renetik