it-swarm.com.ru

Маркерная кластеризация с Google Maps SDK для iOS?

Я использую Google Maps SDK в своем приложении для iOS, и мне нужно сгруппировать маркеры, которые очень близки друг к другу - в основном нужно использовать кластеризацию маркеров, как показано в прикрепленном URL-адресе. Мне удалось получить эту функциональность в SDK Android Maps, но я не нашел никакой библиотеки для SDK Google Maps для iOS.

Можете ли вы предложить какую-либо библиотеку для этого? Или предложить способ реализации собственной библиотеки для этого?

Marker_Clusterer_Full.png

( Источник этой картины)

36
sanjaydhakar

Чтобы понять основную концепцию этого решения с двойной картой, пожалуйста, посмотрите на это видео 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];
    }
}

Несколько заметок:

  • PointMapItem - это мой класс данных аннотаций (id<MKAnnotation>, если мы работали с Map Kit).
  • Здесь я использую свойство shouldBeMerged в PointMapItem, потому что есть некоторые аннотации, которые я не хочу объединять. Если вам это не нужно, удалите деталь, которая его использует, или установите для 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-ключ в AppDelegate.m.

Я буду работать с этой библиотекой в ​​течение следующих дней, я сообщу вам, если я найду некоторые ошибки.

EDIT # 1: Я много работал над кластерами в эти дни. Мой последний подход заключается в интеграции MKMapView, создании кластера в MKMapView (намного проще, чем в Google Maps SDK для iOS) и интеграции Google Maps Places в мой проект iOS .. При таком подходе производительность выше чем предыдущий.

EDIT # 2: Я не знаю, используете ли вы Realm или планируете ли вы использовать его, но они предоставляют действительно хорошее решение для кластеризации карт: https://realm.io/news/building-an -ios-cluster-map-view-in-target-c/

20
Tom

у меня есть приложение для решения этой проблемы, ниже приведен код

  1. зациклить все маркеры (nsdictionary) в массиве

  2. используйте 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


    }
2
chings228

Теперь это решается с помощью Google Maps IOS Utils . https://developers.google.com/maps/documentation/ios-sdk/utility/marker-clustering

0
Renetik