web-dev-qa-db-fra.com

Regroupement de marqueurs avec Google Maps SDK pour iOS?

J'utilise Google Maps SDK dans mon application iOS et je dois regrouper les marqueurs très proches les uns des autres. Pour ce faire, nous devons utiliser le regroupement de marqueurs comme dans l'URL jointe. Je suis en mesure d’obtenir cette fonctionnalité dans le SDK de cartes Android, mais je n’ai trouvé aucune bibliothèque pour le SDK iOS Google Maps.

Pouvez-vous s'il vous plaît suggérer une bibliothèque pour cela? Ou suggère-t-il un moyen d'implémenter une bibliothèque personnalisée?

Marker_Clusterer_Full.png

( Source de cette image)

36
sanjaydhakar

Pour comprendre le concept sous-jacent de cette solution à double carte, veuillez regarder cette vidéo WWDC 2011 (à partir de 22'30). Le code du kit cartographique est directement extrait de cette vidéo, à l'exception de quelques éléments que j'ai décrits dans quelques notes. La solution Google Map SDK n’est qu’une adaptation.

Idée principale: une carte est masquée et contient toutes les annotations, y compris les annotations fusionnées (allAnnotationMapView dans mon code). Une autre est visible et affiche uniquement les annotations du cluster ou l'annotation si elle est unique (mapView dans mon code).

Deuxième idée principale: je divise la carte visible (plus une marge) en carrés et chaque annotation d'un carré spécifique est fusionnée en une seule annotation.

Le code que j'utilise pour Google Maps SDK (veuillez noter que j'ai écrit ceci lorsque la propriété markers était disponible dans la classe GMSMapView. Ce n'est plus le cas mais vous pouvez garder une trace de tous les marqueurs que vous avez ajoutés dans votre propre tableau et utiliser ce tableau au lieu d'appeler 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];
    }
}

Quelques notes:

  • PointMapItem est ma classe de données d'annotation (id<MKAnnotation> si nous travaillions avec Map Kit).
  • Ici, j'utilise une propriété shouldBeMerged sur PointMapItem car il y a des annotations que je ne souhaite pas fusionner. Si vous n'en avez pas besoin, supprimez la partie qui l'utilise ou définissez shouldBeMerged sur YES pour toutes vos annotations. Cependant, vous devriez probablement garder la classe test si vous ne voulez pas fusionner l'emplacement de l'utilisateur!
  • Lorsque vous souhaitez ajouter des annotations, ajoutez-les à la allAnnotationMapView masquée et appelez updateVisibleAnnotation. La méthode updateVisibleAnnotation permet de choisir les annotations à fusionner et à afficher. Il ajoutera ensuite l'annotation à mapView qui est visible.

Pour Map Kit, j'utilise le code suivant:

- (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];
}

Les deux devraient fonctionner correctement, mais si vous avez des questions, n'hésitez pas à demander!

26
Aurelien Porte

Après de longues heures de recherche, j'ai finalement trouvé un gars merveilleux qui a fait cela.

Merci beaucoup à vous DDRBoxman.

Vérifiez son github: https://github.com/DDRBoxman/google-maps-ios-utils

Il a récemment poussé un échantillon de code. 

Quand j'ai voulu exécuter son projet, j'ai eu quelques problèmes. Je viens de supprimer le SDK Google Maps et de suivre le didacticiel complet de Google pour intégrer le SDK Google Maps. Ensuite, plus de problèmes, j'ai pu exécuter l'application . N'oubliez pas de mettre votre clé API dans AppDelegate.m.

Je vais travailler avec cette bibliothèque pour les jours suivants, je vous ferai savoir si je trouve des bugs.

EDIT # 1: J'ai beaucoup travaillé sur les clusters ces jours-ci. Ma dernière approche consiste à intégrer un MKMapView, à créer le cluster sur un MKMapView (beaucoup plus facile que de le faire sur le SDK Google Maps pour iOS) et à intégrer Google Maps Places à mon projet iOS . Les performances sont meilleures avec cette approche. que le précédent.

EDIT # 2: Je ne sais pas si vous utilisez Realm ou si vous prévoyez de l’utiliser, mais ils constituent une très bonne solution pour le clustering de cartes: https://realm.io/news/building-an -ios-clustered-map-view-in-objective-c/

20
Tom

j'ai une application gérer ce problème, ci-dessous est le code

  1. boucle tous les marqueurs (nsdictionary) dans un tableau

  2. utilisez gmsmapview.projection pour obtenir CGPoint afin de déterminer si le marqueur doit être regroupé 

3 J'utilise 100 points pour tester et le temps de réponse est assez satisfait.

4 la carte sera redessinée si la différence de niveau de zoom est supérieure à 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

Ceci est maintenant résolu par Google Maps IOS Utils . https://developers.google.com/maps/documentation/ios-sdk/utility/marker-clustering

0
Renetik