Marcador de clustering con google maps SDK para iOS?

Estoy usando el SDK de Google Maps en mi aplicación para iOS, y necesito agrupar marcadores que estén muy cerca el uno del otro; básicamente, necesitan usar la agrupación de marcadores como se muestra en la url adjunta. Puedo obtener esta funcionalidad en el SDK de mapas de Android, pero no encontré ninguna biblioteca para el iOS SDK de Google Maps.

¿Puedes sugerir alguna biblioteca para esto? ¿O sugerir una forma de implementar una biblioteca personalizada para esto?

Marker_Clusterer_Full.png

( Fuente de esta imagen)

Para comprender el concepto subyacente de esta solución de doble mapa, eche un vistazo a este video de la WWDC 2011 (desde las 22’30). El código del kit de mapas se extrae directamente de este video, excepto algunas cosas que describí en algunas notas. La solución Google Map SDK es solo una adaptación.

Idea principal : un mapa está oculto y contiene cada anotación, incluidas las fusionadas ( allAnnotationMapView en mi código). Otro es visible y muestra solo las anotaciones del clúster o la anotación si es único (mapView en mi código).

Segunda idea principal : divido el mapa visible (más un margen) en cuadrados, y cada anotación en un cuadrado específico se fusionan en una sola anotación.

El código que uso para el SDK de Google Maps (tenga en cuenta que escribí esto cuando la propiedad de markers estaba disponible en la clase GMSMapView. Ya no, pero puede hacer un seguimiento de todos los marcadores que agrega en su propia matriz y usar esta matriz en lugar de llamar 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]; } } 

Algunas notas:

  • PointMapItem es mi clase de datos de anotación ( id si estuviéramos trabajando con Map kit).
  • Aquí uso una propiedad shouldBeMerged en PointMapItem porque hay algunas anotaciones que no quiero fusionar. Si no necesita esto, elimine la parte que lo está usando o configure shouldBeMerged en YES para todas sus anotaciones. Sin embargo, probablemente deberías mantener la prueba de clase si no deseas combinar la ubicación del usuario.
  • Cuando desee agregar anotaciones, agréguelos al allAnnotationMapView oculto y llame a updateVisibleAnnotation . updateVisibleAnnotation método updateVisibleAnnotation es responsable de elegir qué anotaciones fusionar y cuáles mostrar. Luego agregará la anotación a mapView que es visible.

Para Map Kit utilizo el siguiente código:

 - (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)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)obj1).coordinate); MKMapPoint mapPoint2 = MKMapPointForCoordinate(((id)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]; } 

Ambos deberían funcionar bien, pero si tienes alguna pregunta, ¡no dudes en preguntar!

Después de largas horas de investigación, finalmente encontré a un tipo maravilloso que hizo esto.

Muchas gracias a ti DDRBoxman .

Comprueba su github: https://github.com/DDRBoxman/google-maps-ios-utils

Recientemente presionó algunos ejemplos de código.

Cuando quise ejecutar su proyecto, tuve algunos problemas. Acabo de eliminar el SDK de Google Maps y seguir el tutorial completo de Google para integrar el SDK de Google Maps. Entonces, sin más problemas, pude ejecutar la aplicación. No olvides poner tu API KEY en AppDelegate.m.

Trabajaré con esta lib para los días siguientes, te avisaré si encuentro algunos errores.

EDIT # 1 : trabajé mucho en clusters en estos días. Mi enfoque final es integrar un MKMapView, crear el clúster en un MKMapView (mucho más fácil que hacerlo en el SDK de Google Maps para iOS) e integrar Google Maps Places en mi proyecto de iOS. El rendimiento es mejor con este enfoque que el anterior.

EDIT # 2 : No sé si usa Realm o si planea usarlo, pero proporcionan una solución realmente buena para la agrupación de mapas: https://realm.io/news/building-an-ios-clustered-map -view-in-objective-c /

Tengo una aplicación que maneja este problema, a continuación se muestra el código

  1. bucle todos los marcadores (nsdictionary) en una matriz

  2. utilice gmsmapview.projection para obtener CGPoint con el fin de averiguar si el marcador debe agruparse

3 uso 100 puntos para probar y el tiempo de respuesta está bastante satisfecho.

4 el mapa se volverá a dibujar si la diferencia de nivel de zoom es superior a 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 } 

Esto ahora está resuelto por Google Maps IOS Utils. https://developers.google.com/maps/documentation/ios-sdk/utility/marker-clustering