Подтвердить что ты не робот

Кластеризация кластеров с помощью Google Maps SDK для iOS?

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

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

Marker_Clusterer_Full.png

(Источник на этом рисунке)

4b9b3361

Ответ 1

Чтобы понять базовую концепцию этого решения с двойной картой, просмотрите видео WWDC 2011 (с 22:30). Код комплекта карты непосредственно извлекается из этого видео, за исключением нескольких вещей, которые я описал в нескольких заметках. Решение Google Map SDK - это просто адаптация.

Основная идея: карта скрыта и содержит каждую отдельную аннотацию, включая объединенные (allAnnotationMapView в моем коде). Другое видно и показывает только аннотации кластера или аннотации, если они одиночные (mapView в моем коде).

Вторая основная идея. Я разделяю видимую карту (плюс поле) на квадраты, и каждая аннотация в определенном квадрате объединяется в одну аннотацию.

Код, который я использую для SDK Google Maps (обратите внимание, что я написал это, когда свойство 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).
  • Здесь я использую свойство 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 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];
}

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

Ответ 2

После долгих часов исследований я наконец нашел замечательного парня, который это сделал.

Большое спасибо вам DDRBoxman.

Проверьте его github: https://github.com/DDRBoxman/google-maps-ios-utils

Недавно он нажал образец кода.

Когда я хотел запустить свой проект, у меня были некоторые проблемы. Я просто удалил SDK Google Maps и выполнил полный учебник Google по интеграции SDK Google Maps. Тогда, больше никаких проблем, я смог запустить приложение. Не забудьте добавить свой API-ключ в AppDelegate.m.

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

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

РЕДАКТИРОВАТЬ № 2: я не знаю, используете ли вы Realm или планируете использовать его, но они обеспечивают действительно хорошее решение для кластеризации карт: https://realm.io/news/building-an-ios-clustered-map-view-in-objective-c/

Ответ 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


    }