diff --git a/Example/TSClusterMapView/ClusterDemo/AnnotationViews/TSDemoClusteredAnnotationView.m b/Example/TSClusterMapView/ClusterDemo/AnnotationViews/TSDemoClusteredAnnotationView.m index a0a8f52..b6f35bc 100644 --- a/Example/TSClusterMapView/ClusterDemo/AnnotationViews/TSDemoClusteredAnnotationView.m +++ b/Example/TSClusterMapView/ClusterDemo/AnnotationViews/TSDemoClusteredAnnotationView.m @@ -12,22 +12,36 @@ blue:((float)(rgbValue & 0xFF))/255.0 alpha:1.0] #import "TSDemoClusteredAnnotationView.h" +#import "CDMapViewController.h" @implementation TSDemoClusteredAnnotationView -- (id)initWithAnnotation:(id)annotation reuseIdentifier:(NSString *)reuseIdentifier { +- (id)initWithAnnotation:(ADClusterAnnotation *)annotation reuseIdentifier:(NSString *)reuseIdentifier { self = [super initWithAnnotation:annotation reuseIdentifier:reuseIdentifier]; if (self) { // Initialization code - self.image = [UIImage imageNamed:@"ClusterAnnotation"]; - self.frame = CGRectMake(0, 0, self.image.size.width, self.image.size.height); - self.label = [[UILabel alloc] initWithFrame:self.frame]; + + if ([annotation.cluster.groupID isEqualToString:CDStreetLightJsonFile]) { + self.image = [UIImage imageNamed:@"ClusterAnnotationYellow"]; + self.label.textColor = UIColorFromRGB(0xf6d262); + } + else if ([annotation.cluster.groupID isEqualToString:CDToiletJsonFile]) { + self.image = [UIImage imageNamed:@"ClusterAnnotationGreen"]; + self.label.textColor = UIColorFromRGB(0x6fc99d); + } + else { + NSLog(@"Error Grouping: %@, %@, %@", annotation, annotation.cluster, annotation.cluster.groupID); + self.image = [UIImage imageNamed:@"ClusterAnnotation"]; + self.label.textColor = UIColorFromRGB(0x009fd6); + } + + self.frame = CGRectMake(0, 0, self.image.size.width, self.image.size.height); + self.label.frame = self.frame; self.label.textAlignment = NSTextAlignmentCenter; self.label.font = [UIFont systemFontOfSize:10]; - self.label.textColor = UIColorFromRGB(0x009fd6); self.label.center = CGPointMake(self.image.size.width/2, self.image.size.height*.43); self.centerOffset = CGPointMake(0, -self.frame.size.height/2); @@ -46,6 +60,20 @@ - (void)clusteringAnimation { NSUInteger count = clusterAnnotation.clusterCount; self.label.text = [self numberLabelText:count]; + + if ([clusterAnnotation.cluster.groupID isEqualToString:CDStreetLightJsonFile]) { + self.image = [UIImage imageNamed:@"ClusterAnnotationYellow"]; + self.label.textColor = UIColorFromRGB(0xf6d262); + } + else if ([clusterAnnotation.cluster.groupID isEqualToString:CDToiletJsonFile]) { + self.image = [UIImage imageNamed:@"ClusterAnnotationGreen"]; + self.label.textColor = UIColorFromRGB(0x6fc99d); + } + else { + NSLog(@"Error Grouping: %@, %@, %@", clusterAnnotation, clusterAnnotation.cluster, clusterAnnotation.cluster.groupID); + self.image = [UIImage imageNamed:@"ClusterAnnotation"]; + self.label.textColor = UIColorFromRGB(0x009fd6); + } } - (NSString *)numberLabelText:(float)count { diff --git a/Example/TSClusterMapView/ClusterDemo/Resources/Images.xcassets/BathroomAnnotationGreen.imageset/BathroomAnnotationGreen@3x.png b/Example/TSClusterMapView/ClusterDemo/Resources/Images.xcassets/BathroomAnnotationGreen.imageset/BathroomAnnotationGreen@3x.png new file mode 100644 index 0000000..95e1fad Binary files /dev/null and b/Example/TSClusterMapView/ClusterDemo/Resources/Images.xcassets/BathroomAnnotationGreen.imageset/BathroomAnnotationGreen@3x.png differ diff --git a/Example/TSClusterMapView/ClusterDemo/Resources/Images.xcassets/BathroomAnnotationGreen.imageset/Contents.json b/Example/TSClusterMapView/ClusterDemo/Resources/Images.xcassets/BathroomAnnotationGreen.imageset/Contents.json new file mode 100644 index 0000000..453f0d9 --- /dev/null +++ b/Example/TSClusterMapView/ClusterDemo/Resources/Images.xcassets/BathroomAnnotationGreen.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "BathroomAnnotationGreen@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/TSClusterMapView/ClusterDemo/Resources/Images.xcassets/ClusterAnnotationGreen.imageset/ClusterAnnotationGreen@3x.png b/Example/TSClusterMapView/ClusterDemo/Resources/Images.xcassets/ClusterAnnotationGreen.imageset/ClusterAnnotationGreen@3x.png new file mode 100644 index 0000000..0946987 Binary files /dev/null and b/Example/TSClusterMapView/ClusterDemo/Resources/Images.xcassets/ClusterAnnotationGreen.imageset/ClusterAnnotationGreen@3x.png differ diff --git a/Example/TSClusterMapView/ClusterDemo/Resources/Images.xcassets/ClusterAnnotationGreen.imageset/Contents.json b/Example/TSClusterMapView/ClusterDemo/Resources/Images.xcassets/ClusterAnnotationGreen.imageset/Contents.json new file mode 100644 index 0000000..faff65b --- /dev/null +++ b/Example/TSClusterMapView/ClusterDemo/Resources/Images.xcassets/ClusterAnnotationGreen.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "ClusterAnnotationGreen@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/TSClusterMapView/ClusterDemo/Resources/Images.xcassets/ClusterAnnotationYellow.imageset/ClusterAnnotationYellow@3x.png b/Example/TSClusterMapView/ClusterDemo/Resources/Images.xcassets/ClusterAnnotationYellow.imageset/ClusterAnnotationYellow@3x.png new file mode 100644 index 0000000..1398846 Binary files /dev/null and b/Example/TSClusterMapView/ClusterDemo/Resources/Images.xcassets/ClusterAnnotationYellow.imageset/ClusterAnnotationYellow@3x.png differ diff --git a/Example/TSClusterMapView/ClusterDemo/Resources/Images.xcassets/ClusterAnnotationYellow.imageset/Contents.json b/Example/TSClusterMapView/ClusterDemo/Resources/Images.xcassets/ClusterAnnotationYellow.imageset/Contents.json new file mode 100644 index 0000000..e88f8a9 --- /dev/null +++ b/Example/TSClusterMapView/ClusterDemo/Resources/Images.xcassets/ClusterAnnotationYellow.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "ClusterAnnotationYellow@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/TSClusterMapView/ClusterDemo/Resources/Images.xcassets/StreetLightAnnotationYellow.imageset/Contents.json b/Example/TSClusterMapView/ClusterDemo/Resources/Images.xcassets/StreetLightAnnotationYellow.imageset/Contents.json new file mode 100644 index 0000000..e09797a --- /dev/null +++ b/Example/TSClusterMapView/ClusterDemo/Resources/Images.xcassets/StreetLightAnnotationYellow.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "StreetLightAnnotationYellow@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/TSClusterMapView/ClusterDemo/Resources/Images.xcassets/StreetLightAnnotationYellow.imageset/StreetLightAnnotationYellow@3x.png b/Example/TSClusterMapView/ClusterDemo/Resources/Images.xcassets/StreetLightAnnotationYellow.imageset/StreetLightAnnotationYellow@3x.png new file mode 100644 index 0000000..9081840 Binary files /dev/null and b/Example/TSClusterMapView/ClusterDemo/Resources/Images.xcassets/StreetLightAnnotationYellow.imageset/StreetLightAnnotationYellow@3x.png differ diff --git a/Example/TSClusterMapView/ClusterDemo/ViewControllers/CDMapViewController.h b/Example/TSClusterMapView/ClusterDemo/ViewControllers/CDMapViewController.h index 9ded412..19681d4 100644 --- a/Example/TSClusterMapView/ClusterDemo/ViewControllers/CDMapViewController.h +++ b/Example/TSClusterMapView/ClusterDemo/ViewControllers/CDMapViewController.h @@ -9,6 +9,12 @@ #import #import "TSClusterMapView.h" +static NSString * const CDStreetLightJsonFile = @"CDStreetlights"; +static NSString * const kStreetLightAnnotationImage = @"StreetLightAnnotationYellow"; + +static NSString * const CDToiletJsonFile = @"CDToilets"; +static NSString * const kBathroomAnnotationImage = @"BathroomAnnotationGreen"; + @interface CDMapViewController : UIViewController @property (strong, nonatomic) IBOutlet TSClusterMapView * mapView; diff --git a/Example/TSClusterMapView/ClusterDemo/ViewControllers/CDMapViewController.m b/Example/TSClusterMapView/ClusterDemo/ViewControllers/CDMapViewController.m index f76ef78..e0f0c54 100644 --- a/Example/TSClusterMapView/ClusterDemo/ViewControllers/CDMapViewController.m +++ b/Example/TSClusterMapView/ClusterDemo/ViewControllers/CDMapViewController.m @@ -12,11 +12,7 @@ #import "TSStreetLightAnnotation.h" #import "TSDemoClusteredAnnotationView.h" -static NSString * const CDStreetLightJsonFile = @"CDStreetlights"; -static NSString * const kStreetLightAnnotationImage = @"StreetLightAnnotation"; -static NSString * const CDToiletJsonFile = @"CDToilets"; -static NSString * const kBathroomAnnotationImage = @"BathroomAnnotation"; @interface CDMapViewController () @@ -82,7 +78,7 @@ - (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id)annotation { +- (MKAnnotationView *)mapView:(TSClusterMapView *)mapView viewForClusterAnnotation:(ADClusterAnnotation *)annotation { TSDemoClusteredAnnotationView * view = (TSDemoClusteredAnnotationView *)[mapView dequeueReusableAnnotationViewWithIdentifier:NSStringFromClass([TSDemoClusteredAnnotationView class])]; if (!view) { @@ -156,6 +152,11 @@ - (BOOL)mapView:(TSClusterMapView *)mapView shouldRepositionAnnotations:(NSArray - (void)tabBar:(UITabBar *)tabBar didSelectItem:(UITabBarItem *)item { + [self resetsStepperValues]; +} + +- (void)resetsStepperValues { + if (_tabBar.selectedItem == _bathroomTabBarItem) { _stepper.value = _bathroomAnnotationsAdded.count; _stepper.minimumValue = 0; @@ -176,31 +177,31 @@ - (IBAction)addAll:(id)sender { if (_tabBar.selectedItem == _bathroomTabBarItem) { NSLog(@"Adding All %@", CDToiletJsonFile); - [_mapView addClusteredAnnotations:_bathroomAnnotations]; + [_mapView addClusteredAnnotations:_bathroomAnnotations toGroup:CDToiletJsonFile]; _bathroomAnnotationsAdded = [NSMutableArray arrayWithArray:_bathroomAnnotations]; - _stepper.value = _bathroomAnnotationsAdded.count; } else if (_tabBar.selectedItem == _streetLightsTabBarItem) { NSLog(@"Adding All %@", CDStreetLightJsonFile); - [_mapView addClusteredAnnotations:_streetLightAnnotations]; + [_mapView addClusteredAnnotations:_streetLightAnnotations toGroup:CDStreetLightJsonFile]; _streetLightAnnotationsAdded = [NSMutableArray arrayWithArray:_streetLightAnnotations]; - _stepper.value = _streetLightAnnotationsAdded.count; } [self refreshBadges]; + + [self resetsStepperValues]; } - (IBAction)removeAll:(id)sender { if (_tabBar.selectedItem == _bathroomTabBarItem) { - [_mapView removeAnnotations:_bathroomAnnotationsAdded]; + [_mapView removeAnnotations:_bathroomAnnotationsAdded fromGroup:CDToiletJsonFile]; [_bathroomAnnotationsAdded removeAllObjects]; NSLog(@"Removing All %@", CDToiletJsonFile); } else if (_tabBar.selectedItem == _streetLightsTabBarItem) { - [_mapView removeAnnotations:_streetLightAnnotationsAdded]; + [_mapView removeAnnotations:_streetLightAnnotationsAdded fromGroup:CDStreetLightJsonFile]; [_streetLightAnnotationsAdded removeAllObjects]; NSLog(@"Removing All %@", CDStreetLightJsonFile); @@ -253,7 +254,7 @@ - (void)addNewBathroom { TSBathroomAnnotation *annotation = [_bathroomAnnotations objectAtIndex:_bathroomAnnotationsAdded.count]; [_bathroomAnnotationsAdded addObject:annotation]; - [_mapView addClusteredAnnotation:annotation]; + [_mapView addClusteredAnnotation:annotation toGroup:CDToiletJsonFile]; } - (void)addNewStreetLight { @@ -267,7 +268,7 @@ - (void)addNewStreetLight { TSStreetLightAnnotation *annotation = [_streetLightAnnotations objectAtIndex:_streetLightAnnotationsAdded.count]; [_streetLightAnnotationsAdded addObject:annotation]; - [_mapView addClusteredAnnotation:annotation]; + [_mapView addClusteredAnnotation:annotation toGroup:CDStreetLightJsonFile]; } - (void)removeLastBathroom { @@ -276,7 +277,7 @@ - (void)removeLastBathroom { TSBathroomAnnotation *annotation = [_bathroomAnnotationsAdded lastObject]; [_bathroomAnnotationsAdded removeObject:annotation]; - [_mapView removeAnnotation:annotation]; + [_mapView removeAnnotation:annotation fromGroup:CDToiletJsonFile]; } - (void)removeLastStreetLight { @@ -285,7 +286,7 @@ - (void)removeLastStreetLight { TSStreetLightAnnotation *annotation = [_streetLightAnnotationsAdded lastObject]; [_streetLightAnnotationsAdded removeObject:annotation]; - [_mapView removeAnnotation:annotation]; + [_mapView removeAnnotation:annotation fromGroup:CDStreetLightJsonFile]; } - (IBAction)segmentedControlValueChanged:(id)sender { diff --git a/Pod/Classes/ADClusterAnnotation.h b/Pod/Classes/ADClusterAnnotation.h index 3a86208..7b4d726 100755 --- a/Pod/Classes/ADClusterAnnotation.h +++ b/Pod/Classes/ADClusterAnnotation.h @@ -28,7 +28,7 @@ typedef NS_ENUM(NSUInteger, ADClusterAnnotationType) { @property (nonatomic, copy) NSString *title; @property (nonatomic, copy) NSString *subtitle; -@property (readonly, nonatomic) BOOL offscreen; +@property (readonly, nonatomic) BOOL offMap; /*! * @discussion Type of annotation, cluster or single. @@ -53,7 +53,7 @@ typedef NS_ENUM(NSUInteger, ADClusterAnnotationType) { /*! * @discussion This array contains the MKAnnotation objects represented by this annotation */ -@property (weak, nonatomic, readonly) NSArray > * originalAnnotations; +@property (weak, nonatomic, readonly) NSSet > * originalAnnotations; /*! * @discussion Number of annotations represented by the annotation diff --git a/Pod/Classes/ADClusterAnnotation.m b/Pod/Classes/ADClusterAnnotation.m index 097925e..7129bf9 100755 --- a/Pod/Classes/ADClusterAnnotation.m +++ b/Pod/Classes/ADClusterAnnotation.m @@ -17,7 +17,7 @@ - (id)init { self = [super init]; if (self) { _cluster = nil; - self.coordinate = [self offscreenCoordinate]; + self.coordinate = [self offMapCoordinate]; _shouldBeRemovedAfterAnimation = NO; _title = @"Title"; } @@ -52,32 +52,32 @@ - (void)setCoordinate:(CLLocationCoordinate2D)coordinate { - (void)reset { self.cluster = nil; - self.coordinate = [self offscreenCoordinate]; + self.coordinate = [self offMapCoordinate]; } - (void)shouldReset { self.cluster = nil; - self.coordinatePreAnimation = [self offscreenCoordinate]; + self.coordinatePreAnimation = [self offMapCoordinate]; } -- (CLLocationCoordinate2D)offscreenCoordinate { +- (CLLocationCoordinate2D)offMapCoordinate { - CLLocationCoordinate2D coordinate = CLLocationCoordinate2DMake(MAXFLOAT, MAXFLOAT); + CLLocationCoordinate2D coordinate;// = CLLocationCoordinate2DMake(MAXFLOAT, MAXFLOAT); - if (!SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"8.0")) { +// if (!SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"8.0")) { coordinate = CLLocationCoordinate2DMake(85.0, 179.0); // this coordinate puts the annotation on the top right corner of the map. We use this instead of kCLLocationCoordinate2DInvalid so that we don't mess with MapKit's KVO weird behaviour that removes from the map the annotations whose coordinate was set to kCLLocationCoordinate2DInvalid. - } +// } return coordinate; } -- (BOOL)offscreen { - CLLocationCoordinate2D offscreen = [self offscreenCoordinate]; - return (self.coordinate.latitude == offscreen.latitude && self.coordinate.longitude == offscreen.longitude); +- (BOOL)offMap { + CLLocationCoordinate2D offMapCoordinate = [self offMapCoordinate]; + return (self.coordinate.latitude == offMapCoordinate.latitude && self.coordinate.longitude == offMapCoordinate.longitude); } -- (NSArray > *)originalAnnotations { +- (NSSet > *)originalAnnotations { NSAssert(self.cluster != nil, @"This annotation should have a cluster assigned!"); return self.cluster.originalAnnotations; } diff --git a/Pod/Classes/ADMapCluster.h b/Pod/Classes/ADMapCluster.h index 4c10162..58b5198 100755 --- a/Pod/Classes/ADMapCluster.h +++ b/Pod/Classes/ADMapCluster.h @@ -26,9 +26,9 @@ typedef void(^KdtreeCompletionBlock)(ADMapCluster *mapCluster); @property (nonatomic, readonly) NSInteger depth; -@property (readonly) NSMutableArray > *originalAnnotations; +@property (readonly) NSSet > *originalAnnotations; -@property (readonly) NSMutableArray *originalMapPointAnnotations; +@property (readonly, strong) NSSet *originalMapPointAnnotations; @property (readonly) NSString *title; @@ -46,24 +46,38 @@ typedef void(^KdtreeCompletionBlock)(ADMapCluster *mapCluster); @property (readonly) NSSet *clustersWithAnnotations; +@property (strong, nonatomic) NSString *groupID; + +@property (strong, nonatomic) NSArray *rootClusters; + +- (id)initWithRootClusters:(NSArray *)clusters; + +- (ADMapCluster *)rootClusterForID:(NSString *)groupID; + +- (instancetype)rebuildWithAnnotations:(NSSet *)annotations mapView:(TSClusterMapView *)mapView completion:(KdtreeCompletionBlock)completion; + +- (BOOL)overlapsClusterOnMap:(ADMapCluster *)cluster annotationViewMapRectSize:(MKMapRect)annotationViewRect; + /*! * @discussion Creates a KD-tree of clusters http://en.wikipedia.org/wiki/K-d_tree * @param annotations Set of ADMapPointAnnotation objects * @param mapView The ADClusterMapView that will send the delegate callback + * @param groupID The key associated with the group of annotations * @param completion A new ADMapCluster object. */ -+ (void)rootClusterForAnnotations:(NSSet *)annotations mapView:(TSClusterMapView *)mapView completion:(KdtreeCompletionBlock)completion ; ++ (ADMapCluster *)rootClusterForAnnotations:(NSSet *)annotations mapView:(TSClusterMapView *)mapView groupID:(NSString *)groupID completion:(KdtreeCompletionBlock)completion ; /*! * @discussion Creates a KD-tree of clusters http://en.wikipedia.org/wiki/K-d_tree * @param annotations Set of ADMapPointAnnotation objects + * @param groupID The key associated with the group of annotations * @param gamma Descrimination power * @param clusterTitle Title of cluster * @param showSubtitle A Boolean to show subtitle from titles of children * @param completion A new ADMapCluster object. */ -+ (void)rootClusterForAnnotations:(NSSet *)annotations centerWeight:(double)gamma title:(NSString *)clusterTitle showSubtitle:(BOOL)showSubtitle completion:(KdtreeCompletionBlock)completion ; ++ (ADMapCluster *)rootClusterForAnnotations:(NSSet *)annotations groupID:(NSString *)groupID centerWeight:(double)gamma title:(NSString *)clusterTitle showSubtitle:(BOOL)showSubtitle completion:(KdtreeCompletionBlock)completion ; /*! * @discussion Adds a single map point annotation to an existing KD-tree map cluster root diff --git a/Pod/Classes/ADMapCluster.m b/Pod/Classes/ADMapCluster.m index 142f9ee..02ddf3e 100755 --- a/Pod/Classes/ADMapCluster.m +++ b/Pod/Classes/ADMapCluster.m @@ -14,6 +14,8 @@ #define ADMapClusterDiscriminationPrecision 1E-4 +static NSString * const kTSClusterMapViewRootMultiClusterID = @"kTSClusterMapViewRootMultiClusterID-Private"; + @interface ADMapCluster () @property (nonatomic, strong) ADMapCluster *leftChild; @@ -30,18 +32,38 @@ @implementation ADMapCluster #pragma mark - Init -+ (void)rootClusterForAnnotations:(NSSet *)annotations mapView:(TSClusterMapView *)mapView completion:(KdtreeCompletionBlock)completion { +- (instancetype)rebuildWithAnnotations:(NSSet *)annotations mapView:(TSClusterMapView *)mapView completion:(KdtreeCompletionBlock)completion { + + return [ADMapCluster rootClusterForAnnotations:annotations mapView:mapView groupID:self.groupID completion:^(ADMapCluster *mapCluster) { + + mapCluster.parentCluster = self.parentCluster; + + if (self.parentCluster.leftChild == self) { + self.parentCluster.leftChild = mapCluster; + } + else { + self.parentCluster.rightChild = mapCluster; + } + if (completion) { + completion(mapCluster); + } + }]; +} + ++ (instancetype)rootClusterForAnnotations:(NSSet *)annotations mapView:(TSClusterMapView *)mapView groupID:(NSString *)groupID completion:(KdtreeCompletionBlock)completion { [mapView mapView:mapView willBeginBuildingClusterTreeForMapPoints:annotations]; - [ADMapCluster rootClusterForAnnotations:annotations centerWeight:mapView.clusterDiscrimination title:mapView.clusterTitle showSubtitle:mapView.clusterShouldShowSubtitle completion:^(ADMapCluster *mapCluster) { + return [ADMapCluster rootClusterForAnnotations:annotations groupID:groupID centerWeight:mapView.clusterDiscrimination title:mapView.clusterTitle showSubtitle:mapView.clusterShouldShowSubtitle completion:^(ADMapCluster *mapCluster) { [mapView mapView:mapView didFinishBuildingClusterTreeForMapPoints:annotations]; - completion(mapCluster); + if (completion) { + completion(mapCluster); + } }]; } -+ (void)rootClusterForAnnotations:(NSSet *)annotations centerWeight:(double)gamma title:(NSString *)clusterTitle showSubtitle:(BOOL)showSubtitle completion:(KdtreeCompletionBlock)completion { ++ (instancetype)rootClusterForAnnotations:(NSSet *)annotations groupID:(NSString *)groupID centerWeight:(double)gamma title:(NSString *)clusterTitle showSubtitle:(BOOL)showSubtitle completion:(KdtreeCompletionBlock)completion { // KDTree //NSLog(@"Computing KD-tree for %lu annotations...", (unsigned long)annotations.count); @@ -65,16 +87,20 @@ + (void)rootClusterForAnnotations:(NSSet *)annotations } - ADMapCluster * cluster = [[ADMapCluster alloc] initWithAnnotations:annotations atDepth:0 inMapRect:boundaries gamma:gamma clusterTitle:clusterTitle showSubtitle:showSubtitle parentCluster:nil rootCluster:nil]; + ADMapCluster * cluster = [[ADMapCluster alloc] initWithAnnotations:annotations groupID:(NSString *)groupID atDepth:0 inMapRect:boundaries gamma:gamma clusterTitle:clusterTitle showSubtitle:showSubtitle parentCluster:nil rootCluster:nil]; //NSLog(@"Computation done !"); - completion(cluster); + if (completion) { + completion(cluster); + } + return cluster; } -- (id)initWithAnnotations:(NSSet *)annotations atDepth:(NSInteger)depth inMapRect:(MKMapRect)mapRect gamma:(double)gamma clusterTitle:(NSString *)clusterTitle showSubtitle:(BOOL)showSubtitle parentCluster:(ADMapCluster *)parentCluster rootCluster:(ADMapCluster *)rootCluster { +- (id)initWithAnnotations:(NSSet *)annotations groupID:(NSString *)groupID atDepth:(NSInteger)depth inMapRect:(MKMapRect)mapRect gamma:(double)gamma clusterTitle:(NSString *)clusterTitle showSubtitle:(BOOL)showSubtitle parentCluster:(ADMapCluster *)parentCluster rootCluster:(ADMapCluster *)rootCluster { self = [super init]; if (self) { + _originalMapPointAnnotations = [annotations copy]; _depth = depth; _mapRect = mapRect; _clusterTitle = clusterTitle; @@ -83,6 +109,7 @@ - (id)initWithAnnotations:(NSSet *)annotations atDepth: _parentCluster = parentCluster; _clusterCount = annotations.count; _progress = 0; + _groupID = groupID; if (depth == 0) { rootCluster = self; @@ -123,15 +150,132 @@ - (id)initWithAnnotations:(NSSet *)annotations atDepth: MKMapRect rightMapRect = [ADMapCluster boundariesForAnnotations:splitAnnotations[1]]; if (splitAnnotations[0]) { - _leftChild = [[ADMapCluster alloc] initWithAnnotations:splitAnnotations[0] atDepth:depth+1 inMapRect:leftMapRect gamma:gamma clusterTitle:clusterTitle showSubtitle:showSubtitle parentCluster:self rootCluster:rootCluster]; + _leftChild = [[ADMapCluster alloc] initWithAnnotations:splitAnnotations[0] groupID:(NSString *)groupID atDepth:depth+1 inMapRect:leftMapRect gamma:gamma clusterTitle:clusterTitle showSubtitle:showSubtitle parentCluster:self rootCluster:rootCluster]; } - _rightChild = [[ADMapCluster alloc] initWithAnnotations:splitAnnotations[1] atDepth:depth+1 inMapRect:rightMapRect gamma:gamma clusterTitle:clusterTitle showSubtitle:showSubtitle parentCluster:self rootCluster:rootCluster]; + _rightChild = [[ADMapCluster alloc] initWithAnnotations:splitAnnotations[1] groupID:(NSString *)groupID atDepth:depth+1 inMapRect:rightMapRect gamma:gamma clusterTitle:clusterTitle showSubtitle:showSubtitle parentCluster:self rootCluster:rootCluster]; } } return self; } + +#pragma mark - Multi Tree + +- (id)initWithRootClusters:(NSArray *)clusters { + self = [super init]; + if (self) { + self.groupID = kTSClusterMapViewRootMultiClusterID; + self.rootClusters = clusters; + + _clusterCount = 0; + + for (ADMapCluster *cluster in clusters) { + cluster.parentCluster = self; + _clusterCount += cluster.clusterCount; + + if (MKMapRectIsEmpty(_mapRect)) { + _mapRect = cluster.mapRect; + continue; + } + _mapRect = MKMapRectUnion(_mapRect, cluster.mapRect); + } + + _depth = 0; + _clusterTitle = clusters.firstObject.title; + _showSubtitle = clusters.firstObject.showSubtitle; + _gamma = clusters.firstObject.gamma; + _progress = 0; + + self.annotation = nil; + + NSMutableDictionary *mutableDict = [[NSMutableDictionary alloc] init]; + NSMutableSet *annotations = [[NSMutableSet alloc] initWithCapacity:clusters.count]; + + for (ADMapCluster *cluster in clusters) { + cluster.parentCluster = self; + ADClusterAnnotation *tempAnnotation = [[ADClusterAnnotation alloc] init]; + tempAnnotation.coordinate = cluster.clusterCoordinate; + ADMapPointAnnotation *mapPoint = [[ADMapPointAnnotation alloc] initWithAnnotation:tempAnnotation]; + [annotations addObject:mapPoint]; + + [mutableDict setObject:cluster forKey:mapPoint]; + } + + + MKMapPoint centerMapPoint = [self meanCoordinateForAnnotations:annotations gamma:_gamma]; + _clusterCoordinate = MKCoordinateForMapPoint(centerMapPoint); + + NSArray *splitAnnotations = [self splitAnnotations:annotations centerPoint:centerMapPoint]; + NSArray *left = splitAnnotations.firstObject; + NSArray *right = splitAnnotations.lastObject; + + NSMutableArray *leftChildren = [[NSMutableArray alloc] init]; + for (ADMapPointAnnotation *mapPoint in left) { + ADMapCluster *cluster = [mutableDict objectForKey:mapPoint]; + if (cluster) { + [leftChildren addObject:cluster]; + } + else { + NSLog(@"Lost cluster"); + } + } + + NSMutableArray *rightChildren = [[NSMutableArray alloc] init]; + for (ADMapPointAnnotation *mapPoint in right) { + ADMapCluster *cluster = [mutableDict objectForKey:mapPoint]; + if (cluster) { + [rightChildren addObject:cluster]; + } + else { + NSLog(@"Lost cluster"); + } + } + + if (leftChildren.count) { + if (leftChildren.count == 1) { + _leftChild = leftChildren.firstObject; + } + else { + _leftChild = [[ADMapCluster alloc] initWithRootClusters:leftChildren]; + } + } + + if (rightChildren.count) { + if (rightChildren.count == 1) { + _rightChild = rightChildren.firstObject; + } + else { + _rightChild = [[ADMapCluster alloc] initWithRootClusters:rightChildren]; + } + } + } + return self; +} + +- (ADMapCluster *)rootClusterForID:(NSString *)groupID { + + if ([self.groupID isEqualToString:groupID]) { + return self; + } + + for (ADMapCluster *cluster in self.children) { + + if ([cluster.groupID isEqualToString:groupID]) { + return cluster; + } + + if ([cluster.groupID isEqualToString:kTSClusterMapViewRootMultiClusterID]) { + ADMapCluster *foundCluster = [cluster rootClusterForID:groupID]; + if (foundCluster) { + return foundCluster; + } + } + } + + return nil; +} + #pragma mark Tree Mapping - (NSArray *>*)splitAnnotations:(NSSet *)annotations centerPoint:(MKMapPoint)center { @@ -288,12 +432,12 @@ - (void)mapView:(TSClusterMapView *)mapView addAnnotation:(ADMapPointAnnotation } } - NSMutableSet *annotationsToRecalculate = [[NSMutableSet alloc] initWithArray:closestCluster.originalMapPointAnnotations]; + NSMutableSet *annotationsToRecalculate = [closestCluster.originalMapPointAnnotations mutableCopy];//[[NSMutableSet alloc] initWithArray:closestCluster.originalMapPointAnnotations]; [annotationsToRecalculate addObject:mapPointAnnotation]; closestCluster.clusterCount = 0; - [ADMapCluster rootClusterForAnnotations:annotationsToRecalculate mapView:mapView completion:^(ADMapCluster *mapCluster) { + [ADMapCluster rootClusterForAnnotations:annotationsToRecalculate mapView:mapView groupID:self.groupID completion:^(ADMapCluster *mapCluster) { if (closestCluster.parentCluster.rightChild == closestCluster) { closestCluster.parentCluster.rightChild = mapCluster; } @@ -323,12 +467,12 @@ - (void)mapView:(TSClusterMapView *)mapView removeAnnotation:(id)a //Go up two cluster to ensure a more complete result ADMapCluster *clusterParent = clusterToRemove.parentCluster.parentCluster; - NSMutableSet *annotationsToRecalculate = [[NSMutableSet alloc] initWithArray:clusterParent.originalMapPointAnnotations]; + NSMutableSet *annotationsToRecalculate = [clusterParent.originalMapPointAnnotations mutableCopy]; [annotationsToRecalculate removeObject:clusterToRemove.annotation]; clusterParent.clusterCount = 0; - [ADMapCluster rootClusterForAnnotations:annotationsToRecalculate mapView:mapView completion:^(ADMapCluster *mapCluster) { + [ADMapCluster rootClusterForAnnotations:annotationsToRecalculate mapView:mapView groupID:self.groupID completion:^(ADMapCluster *mapCluster) { if (clusterParent.parentCluster.rightChild == clusterParent) { clusterParent.parentCluster.rightChild = mapCluster; @@ -375,18 +519,15 @@ - (NSString *)subtitle { return nil; } -- (NSMutableArray > *)originalAnnotations { - NSMutableArray * originalAnnotations = [[NSMutableArray alloc] initWithCapacity:1]; - if (self.annotation) { - [originalAnnotations addObject:self.annotation.annotation]; - } else { - if (_leftChild) { - [originalAnnotations addObjectsFromArray:_leftChild.originalAnnotations]; - } - if (_rightChild) { - [originalAnnotations addObjectsFromArray:_rightChild.originalAnnotations]; - } +- (NSSet > *)originalAnnotations { + + NSSet *originalMapPoints = self.originalMapPointAnnotations; + NSMutableSet * originalAnnotations = [[NSMutableSet alloc] initWithCapacity:originalMapPoints.count]; + + for (ADMapPointAnnotation *mapPointAnn in originalMapPoints) { + [originalAnnotations addObject:mapPointAnn.annotation]; } + return originalAnnotations; } @@ -420,21 +561,21 @@ - (NSString *)subtitle { } -- (NSMutableArray *)originalMapPointAnnotations { - NSMutableArray * originalAnnotations = [[NSMutableArray alloc] initWithCapacity:1]; - - if (self.annotation) { - [originalAnnotations addObject:self.annotation]; - } - - if (_leftChild) { - [originalAnnotations addObjectsFromArray:_leftChild.originalMapPointAnnotations]; - } - if (_rightChild) { - [originalAnnotations addObjectsFromArray:_rightChild.originalMapPointAnnotations]; - } - return originalAnnotations; -} +//- (NSSet *)originalMapPointAnnotations { +// NSMutableSet * originalAnnotations = [[NSMutableSet alloc] initWithCapacity:1]; +// +// if (self.annotation) { +// [originalAnnotations addObject:self.annotation]; +// } +// +// if (_leftChild) { +// [originalAnnotations unionSet:_leftChild.originalMapPointAnnotations]; +// } +// if (_rightChild) { +// [originalAnnotations unionSet:_rightChild.originalMapPointAnnotations]; +// } +// return originalAnnotations; +//} - (NSMutableSet *)clustersWithAnnotations { @@ -508,7 +649,7 @@ - (void)setClusterCount:(NSInteger)clusterCount { for (ADMapCluster * child in children) { if (!overlap && !MKMapRectIsEmpty(annotationSizeRect) && children.count == 2) { - if ([child overlapsClusterOnMap:[children lastObject] annotationViewMapRectSize:annotationSizeRect]) { + if (child.depth != 0 && [child overlapsClusterOnMap:[children lastObject] annotationViewMapRectSize:annotationSizeRect]) { [clusters addObject:cluster]; break; } @@ -638,9 +779,17 @@ - (BOOL)overlapsClusterOnMap:(ADMapCluster *)cluster annotationViewMapRectSize:( #pragma mark Tree Relations - (BOOL)isAncestorOf:(ADMapCluster *)mapCluster { - return _depth < mapCluster.depth && (_leftChild == mapCluster || _rightChild == mapCluster || [_leftChild isAncestorOf:mapCluster] || [_rightChild isAncestorOf:mapCluster]); + + BOOL sameGroup = [self.groupID isEqualToString:kTSClusterMapViewRootMultiClusterID] || [mapCluster.groupID isEqualToString:self.groupID]; + + if (!sameGroup) { + return NO; + } + + return sameGroup && _depth < mapCluster.depth && (_leftChild == mapCluster || _rightChild == mapCluster || [_leftChild isAncestorOf:mapCluster] || [_rightChild isAncestorOf:mapCluster]); } + - (BOOL)isRootClusterForAnnotation:(id)annotation { return _annotation.annotation == annotation || [_leftChild isRootClusterForAnnotation:annotation] || [_rightChild isRootClusterForAnnotation:annotation]; } diff --git a/Pod/Classes/ADMapPointAnnotation.h b/Pod/Classes/ADMapPointAnnotation.h index ac1ec17..323f60b 100755 --- a/Pod/Classes/ADMapPointAnnotation.h +++ b/Pod/Classes/ADMapPointAnnotation.h @@ -12,7 +12,7 @@ /** * Do not subclass. This is a wrapper to give annotations added to cluster a map point. */ -@interface ADMapPointAnnotation : NSObject +@interface ADMapPointAnnotation : NSObject @property (nonatomic, readonly) MKMapPoint mapPoint; diff --git a/Pod/Classes/ADMapPointAnnotation.m b/Pod/Classes/ADMapPointAnnotation.m index a1bd622..a7814d4 100755 --- a/Pod/Classes/ADMapPointAnnotation.m +++ b/Pod/Classes/ADMapPointAnnotation.m @@ -19,4 +19,26 @@ - (id)initWithAnnotation:(id)annotation { return self; } +- (id)copyWithZone:(NSZone *)zone { + + return [[[self class] alloc] initWithAnnotation:_annotation]; +} + +- (BOOL)isEqual:(ADMapPointAnnotation *)other +{ + if (other == self) { + return YES; + } else { + if ([other isKindOfClass:[self class]]) { + return [self.annotation isEqual:other.annotation]; + } + return NO; + } +} + +- (NSUInteger)hash +{ + return [@(self.mapPoint.x) hash] ^ [@(self.mapPoint.y) hash] ^ [self.annotation hash]; +} + @end diff --git a/Pod/Classes/CLLocation+Utilities.h b/Pod/Classes/CLLocation+Utilities.h index a25374e..ea71e70 100644 --- a/Pod/Classes/CLLocation+Utilities.h +++ b/Pod/Classes/CLLocation+Utilities.h @@ -13,10 +13,14 @@ BOOL CLLocationCoordinate2DIsApproxEqual(CLLocationCoordinate2D coord1, CLLocationCoordinate2D coord2, float epsilon); +double CLLocationCoordinate2DBearingRadians(CLLocationCoordinate2D coord1, CLLocationCoordinate2D coord2); + CLLocationCoordinate2D CLLocationCoordinate2DOffset(CLLocationCoordinate2D coord, double x, double y); CLLocationCoordinate2D CLLocationCoordinate2DRoundedLonLat(CLLocationCoordinate2D coord, int decimalPlace); +CLLocationCoordinate2D CLLocationCoordinate2DMidPoint(CLLocationCoordinate2D coord1, CLLocationCoordinate2D coord2); + BOOL MKMapRectSizeIsEqual(MKMapRect rect1, MKMapRect rect2); BOOL MKMapRectApproxEqual(MKMapRect rect1, MKMapRect rect2); diff --git a/Pod/Classes/CLLocation+Utilities.m b/Pod/Classes/CLLocation+Utilities.m index 6830cef..bce363f 100644 --- a/Pod/Classes/CLLocation+Utilities.m +++ b/Pod/Classes/CLLocation+Utilities.m @@ -15,6 +15,45 @@ BOOL CLLocationCoordinate2DIsApproxEqual(CLLocationCoordinate2D coord1, CLLocati fabs(coord1.longitude - coord2.longitude) < epsilon); } +double CLLocationCoordinate2DBearingRadians(CLLocationCoordinate2D coord1, CLLocationCoordinate2D coord2) { + + double lat1 = coord1.latitude; + double lon1 = coord1.longitude; + + double lat2 = coord2.latitude; + double lon2 = coord2.longitude; + + double y = sin(lon2-lon1)*cos(lat2); //SIN(lon2-lon1)*COS(lat2) + double x = cos(lat1)*sin(lat2)-sin(lat1)*cos(lat2)*cos(lon2-lon1); + double bearing = atan2(y, x); + + return bearing; +} + +CLLocationCoordinate2D CLLocationCoordinate2DMidPoint(CLLocationCoordinate2D coord1, CLLocationCoordinate2D coord2) { + + CLLocationCoordinate2D midPoint; + + double lon1 = coord1.longitude * M_PI / 180; + double lon2 = coord2.longitude * M_PI / 180; + + double lat1 = coord1.latitude * M_PI / 180; + double lat2 = coord2.latitude * M_PI / 180; + + double dLon = lon2 - lon1; + + double x = cos(lat2) * cos(dLon); + double y = cos(lat2) * sin(dLon); + + double lat3 = atan2( sin(lat1) + sin(lat2), sqrt((cos(lat1) + x) * (cos(lat1) + x) + y * y) ); + double lon3 = lon1 + atan2(y, cos(lat1) + x); + + midPoint.latitude = lat3 * 180 / M_PI; + midPoint.longitude = lon3 * 180 / M_PI; + + return midPoint; +} + CLLocationCoordinate2D CLLocationCoordinate2DOffset(CLLocationCoordinate2D coord, double x, double y) { return CLLocationCoordinate2DMake(coord.latitude + y, coord.longitude + x); } diff --git a/Pod/Classes/TSClusterAnnotationView.m b/Pod/Classes/TSClusterAnnotationView.m index d17cc79..a149761 100644 --- a/Pod/Classes/TSClusterAnnotationView.m +++ b/Pod/Classes/TSClusterAnnotationView.m @@ -77,6 +77,9 @@ - (void)setAnnotation:(id)annotation { - (void)animateView { + if ([NSOperationQueue mainQueue] != [NSOperationQueue currentQueue]) { + NSLog(@"NotMain"); + } if ([_addedView isKindOfClass:[TSRefreshedAnnotationView class]]) { [(TSRefreshedAnnotationView*)_addedView clusteringAnimation]; } diff --git a/Pod/Classes/TSClusterMapView.h b/Pod/Classes/TSClusterMapView.h index 607fcfe..107538d 100755 --- a/Pod/Classes/TSClusterMapView.h +++ b/Pod/Classes/TSClusterMapView.h @@ -26,7 +26,7 @@ extern NSString * const KDTreeClusteringProgress; * @param annotation The object representing the annotation that is about to be displayed. * @return The annotation view to display for the specified annotation or nil if you want to display a standard annotation view. */ -- (MKAnnotationView *)mapView:(TSClusterMapView *)mapView viewForClusterAnnotation:(id )annotation; +- (MKAnnotationView *)mapView:(TSClusterMapView *)mapView viewForClusterAnnotation:(ADClusterAnnotation *)annotation; /*! * @discussion MapView will begin creating Kd-tree from new annotations. Use this delegate to alert the user of a refresh for large data sets with long build times. @@ -102,6 +102,11 @@ typedef NS_ENUM(NSInteger, ADClusterBufferSize) { @interface TSClusterMapView : MKMapView +- (void)addClusteredAnnotation:(id)annotation toGroup:(NSString *)groupID; +- (void)addClusteredAnnotations:(NSArray > *)annotations toGroup:(NSString *)groupID; +- (void)removeAnnotations:(NSArray > *)annotations fromGroup:(NSString *)groupID; +- (void)removeAnnotation:(id)annotation fromGroup:(NSString *)groupID; + /*! * @discussion Adds an annotation to the map and clusters if needed (threadsafe). Only rebuilds entire cluster tree if there are less than 1000 clustered annotations or the annotation coordinate is an outlier from current clustered data set. * @param annotation The annotation to be added to map diff --git a/Pod/Classes/TSClusterMapView.m b/Pod/Classes/TSClusterMapView.m index 7f91c65..89f103e 100755 --- a/Pod/Classes/TSClusterMapView.m +++ b/Pod/Classes/TSClusterMapView.m @@ -17,11 +17,12 @@ #define DATA_REFRESH_MAX 1000 static NSString * const kTSClusterAnnotationViewID = @"kTSClusterAnnotationViewID-private"; +static NSString * const kTSClusterMapViewRootClusterID = @"kTSClusterMapViewRootClusterID-private"; NSString * const KDTreeClusteringProgress = @"KDTreeClusteringProgress"; @interface TSClusterMapView () - +@property (strong, nonatomic) NSMutableDictionary >*> *annotationsBygroupID; @end @@ -51,6 +52,8 @@ - (void)initHelpers { [self setDefaults]; + _annotationsBygroupID = [[NSMutableDictionary alloc] init]; + _clusterAnnotationsPool = [[NSMutableSet alloc] init]; _preClusterOperationQueue = [[NSOperationQueue alloc] init]; @@ -145,15 +148,24 @@ - (NSUInteger)numberOfClusters { } - (void)needsRefresh { - - [self createKDTreeAndCluster:_clusterableAnnotationsAdded]; + [self buildKDTreeAndCluster]; } #pragma mark - Add/Remove Annotations +- (NSMutableSet> *)clusterableAnnotationsAdded { + + NSMutableSet *mutableSet = [[NSMutableSet alloc] init]; + + for (NSMutableSet *set in self.annotationsBygroupID.allValues) { + [mutableSet unionSet:set]; + } + + return mutableSet; +} - (void)addAnnotation:(id)annotation { - if (![_clusterableAnnotationsAdded containsObject:annotation]) { + if (![self.clusterableAnnotationsAdded containsObject:annotation]) { [super addAnnotation:annotation]; } } @@ -161,47 +173,57 @@ - (void)addAnnotation:(id)annotation { - (void)addAnnotations:(NSArray > *)annotations { NSMutableSet *annotationsToAdd = [NSMutableSet setWithArray:annotations]; - [annotationsToAdd minusSet:_clusterableAnnotationsAdded]; + [annotationsToAdd minusSet:self.clusterableAnnotationsAdded]; if (annotationsToAdd.count) { [super addAnnotations:annotationsToAdd.allObjects]; } } -- (void)addClusteredAnnotation:(id)annotation { +#pragma mark - Multi Tree + +- (void)addClusteredAnnotation:(id)annotation toGroup:(NSString *)groupID { BOOL refresh = NO; - if (_clusterableAnnotationsAdded.count < DATA_REFRESH_MAX) { + NSMutableSet *annotationsForTree = self.annotationsBygroupID[groupID]; + + if (annotationsForTree.count < DATA_REFRESH_MAX) { refresh = YES; } - [self addClusteredAnnotation:annotation clusterTreeRefresh:refresh]; + [self addClusteredAnnotation:annotation toGroup:(NSString *)groupID clusterTreeRefresh:refresh]; + } -- (void)addClusteredAnnotation:(id)annotation clusterTreeRefresh:(BOOL)refresh { +- (void)addClusteredAnnotation:annotation toGroup:(NSString *)groupID clusterTreeRefresh:(BOOL)refresh { + - if (!annotation || [_clusterableAnnotationsAdded containsObject:annotation]) { + NSMutableSet *annotationsForTree = self.annotationsBygroupID[groupID]; + + if (!annotation || [annotationsForTree containsObject:annotation]) { return; } - if (_clusterableAnnotationsAdded) { - [_clusterableAnnotationsAdded addObject:annotation]; + if (annotationsForTree) { + [annotationsForTree addObject:annotation]; } else { - _clusterableAnnotationsAdded = [[NSMutableSet alloc] initWithObjects:annotation, nil]; + annotationsForTree = [[NSMutableSet alloc] initWithObjects:annotation, nil]; + self.annotationsBygroupID[groupID] = annotationsForTree; } - if (refresh || _treeOperationQueue.operationCount > 10) { - [self needsRefresh]; + ADMapCluster *rootForID = [_rootMapCluster rootClusterForID:groupID]; + + if (!rootForID || refresh || _treeOperationQueue.operationCount > 10) { + [self buildKDTreeAndClusterWithGroupID:groupID]; return; } - __weak TSClusterMapView *weakSelf = self; [_treeOperationQueue addOperationWithBlock:^{ //Attempt to insert in existing root cluster - will fail if small data set or an outlier - [_rootMapCluster mapView:self addAnnotation:[[ADMapPointAnnotation alloc] initWithAnnotation:annotation] completion:^(BOOL added) { + [rootForID mapView:self addAnnotation:[[ADMapPointAnnotation alloc] initWithAnnotation:annotation] completion:^(BOOL added) { TSClusterMapView *strongSelf = weakSelf; @@ -209,51 +231,59 @@ - (void)addClusteredAnnotation:(id)annotation clusterTreeRefresh:( [strongSelf clusterVisibleMapRectForceRefresh:YES]; } else { - [strongSelf needsRefresh]; + [strongSelf buildKDTreeAndClusterWithGroupID:groupID]; } }]; }]; } -- (void)addClusteredAnnotations:(NSArray > *)annotations { +- (void)addClusteredAnnotations:(NSArray > *)annotations toGroup:(NSString *)groupID { if (!annotations || !annotations.count) { return; } - NSInteger count = _clusterableAnnotationsAdded.count; + NSMutableSet *annotationsForTree = self.annotationsBygroupID[groupID]; + NSMutableSet *addSet = [NSMutableSet setWithArray:annotations]; + + NSInteger preCount = annotationsForTree.count; - if (_clusterableAnnotationsAdded) { - [_clusterableAnnotationsAdded unionSet:[NSSet setWithArray:annotations]]; + if (!annotationsForTree) { + annotationsForTree = addSet; + self.annotationsBygroupID[groupID] = annotationsForTree; } else { - _clusterableAnnotationsAdded = [[NSMutableSet alloc] initWithArray:annotations]; + [annotationsForTree unionSet:addSet]; } - if (count != _clusterableAnnotationsAdded.count) { - [self needsRefresh]; + if (preCount != annotationsForTree.count) { + [self buildKDTreeAndClusterWithGroupID:groupID]; } } -- (void)removeAnnotation:(id)annotation { +- (void)removeAnnotation:(id)annotation fromGroup:(NSString *)groupID { if (!annotation) { return; } - if ([_clusterableAnnotationsAdded containsObject:annotation]) { - [_clusterableAnnotationsAdded removeObject:annotation]; + + NSMutableSet *annotationsForTree = self.annotationsBygroupID[groupID]; + + if ([annotationsForTree containsObject:annotation]) { + [annotationsForTree removeObject:annotation]; //Small data set just rebuild - if (_clusterableAnnotationsAdded.count < DATA_REFRESH_MAX || _treeOperationQueue.operationCount > 10) { - [self needsRefresh]; + if (annotationsForTree.count < DATA_REFRESH_MAX || _treeOperationQueue.operationCount > 10 || annotationsForTree.count == 0) { + [self buildKDTreeAndClusterWithGroupID:groupID]; } else { + ADMapCluster *rootForID = [_rootMapCluster rootClusterForID:groupID]; __weak TSClusterMapView *weakSelf = self; [_treeOperationQueue addOperationWithBlock:^{ - [weakSelf.rootMapCluster mapView:self removeAnnotation:annotation completion:^(BOOL removed) { + [rootForID mapView:self removeAnnotation:annotation completion:^(BOOL removed) { TSClusterMapView *strongSelf = weakSelf; @@ -261,7 +291,7 @@ - (void)removeAnnotation:(id)annotation { [strongSelf clusterVisibleMapRectForceRefresh:YES]; } else { - [strongSelf needsRefresh]; + [strongSelf buildKDTreeAndClusterWithGroupID:groupID]; } }]; }]; @@ -271,23 +301,56 @@ - (void)removeAnnotation:(id)annotation { [super removeAnnotation:annotation]; } -- (void)removeAnnotations:(NSArray > *)annotations { +- (void)removeAnnotations:(NSArray > *)annotations fromGroup:(NSString *)groupID { if (!annotations) { return; } - NSUInteger previousCount = _clusterableAnnotationsAdded.count; + NSMutableSet *annotationsForTree = self.annotationsBygroupID[groupID]; + + if (!annotationsForTree) { + return; + } + + NSUInteger previousCount = annotationsForTree.count; NSSet *set = [NSSet setWithArray:annotations]; - [_clusterableAnnotationsAdded minusSet:set]; + [annotationsForTree minusSet:set]; - if (_clusterableAnnotationsAdded.count != previousCount) { - [self needsRefresh]; + if (annotationsForTree.count != previousCount) { + [self buildKDTreeAndClusterWithGroupID:groupID]; } [super removeAnnotations:annotations]; } +/////// + +- (void)addClusteredAnnotation:(id)annotation { + + [self addClusteredAnnotation:annotation toGroup:kTSClusterMapViewRootClusterID]; +} + +- (void)addClusteredAnnotation:(id)annotation clusterTreeRefresh:(BOOL)refresh { + + [self addClusteredAnnotation:annotation toGroup:kTSClusterMapViewRootClusterID clusterTreeRefresh:refresh]; +} + +- (void)addClusteredAnnotations:(NSArray > *)annotations { + + [self addClusteredAnnotations:annotations toGroup:kTSClusterMapViewRootClusterID]; +} + +- (void)removeAnnotation:(id)annotation { + + [self removeAnnotation:annotation fromGroup:kTSClusterMapViewRootClusterID]; +} + +- (void)removeAnnotations:(NSArray > *)annotations { + + [self removeAnnotations:annotations fromGroup:kTSClusterMapViewRootClusterID]; +} + #pragma mark - Annotations - (void)refreshClusterAnnotation:(ADClusterAnnotation *)annotation { @@ -300,7 +363,7 @@ - (void)refreshClusterAnnotation:(ADClusterAnnotation *)annotation { - (NSArray *)visibleClusterAnnotations { NSMutableArray * displayedAnnotations = [[NSMutableArray alloc] init]; for (ADClusterAnnotation * annotation in [_clusterAnnotationsPool copy]) { - if (!annotation.offscreen) { + if (!annotation.offMap) { [displayedAnnotations addObject:annotation]; } } @@ -312,7 +375,7 @@ - (void)refreshClusterAnnotation:(ADClusterAnnotation *)annotation { NSMutableSet *set = [NSMutableSet setWithArray:[super annotations]]; [set minusSet:self.clusterAnnotations]; - [set unionSet:_clusterableAnnotationsAdded]; + [set unionSet:self.clusterableAnnotationsAdded]; return set.allObjects; } @@ -447,37 +510,115 @@ - (void)forwardInvocation:(NSInvocation *)anInvocation { #pragma mark - Clustering -- (void)createKDTreeAndCluster:(NSSet > *)annotations { +- (void)buildKDTreeAndClusterWithGroupID:(NSString *)groupID { - if (!annotations) { + NSMutableDictionary >*> *annotationsForTrees = [_annotationsBygroupID copy]; + + if (!annotationsForTrees.allKeys.count) { return; } - annotations = [annotations copy]; + annotationsForTrees = [annotationsForTrees copy]; + + ADMapCluster *clusterToReplace = [self.rootMapCluster rootClusterForID:groupID]; + NSSet *annotations = [annotationsForTrees[groupID] copy]; [_treeOperationQueue cancelAllOperations]; __weak TSClusterMapView *weakSelf = self; [_treeOperationQueue addOperationWithBlock:^{ - // use wrapper annotations that expose a MKMapPoint property instead of a CLLocationCoordinate2D property + TSClusterMapView *strongSelf = weakSelf; + NSMutableSet * mapPointAnnotations = [[NSMutableSet alloc] initWithCapacity:annotations.count]; + for (id annotation in annotations) { ADMapPointAnnotation * mapPointAnnotation = [[ADMapPointAnnotation alloc] initWithAnnotation:annotation]; [mapPointAnnotations addObject:mapPointAnnotation]; } - [ADMapCluster rootClusterForAnnotations:mapPointAnnotations mapView:self completion:^(ADMapCluster *mapCluster) { + if (!clusterToReplace) { - TSClusterMapView *strongSelf = weakSelf; + NSArray *allRootClusters = strongSelf.rootMapCluster.rootClusters; - strongSelf.rootMapCluster = mapCluster; + if (!self.rootMapCluster.originalMapPointAnnotations.count && !allRootClusters.count) { + [strongSelf buildKDTreeAndCluster]; + return; + } + + if (!allRootClusters.count) { + allRootClusters = @[self.rootMapCluster]; + } + + ADMapCluster *newCluster = [ADMapCluster rootClusterForAnnotations:mapPointAnnotations mapView:self groupID:groupID completion:nil]; + strongSelf.rootMapCluster = [[ADMapCluster alloc] initWithRootClusters:[allRootClusters arrayByAddingObject:newCluster]]; + [strongSelf clusterVisibleMapRectForceRefresh:YES]; + return; + } + + [clusterToReplace rebuildWithAnnotations:mapPointAnnotations mapView:strongSelf completion:^(ADMapCluster *mapCluster) { + + if ([strongSelf.rootMapCluster.groupID isEqualToString:groupID]) { + strongSelf.rootMapCluster = mapCluster; + } [strongSelf clusterVisibleMapRectForceRefresh:YES]; }]; }]; } +- (void)buildKDTreeAndCluster { + + if (!_annotationsBygroupID.allKeys.count) { + return; + } + + NSMutableDictionary >*> *annotationsForTrees = [_annotationsBygroupID copy]; + + [_treeOperationQueue cancelAllOperations]; + + __weak TSClusterMapView *weakSelf = self; + [_treeOperationQueue addOperationWithBlock:^{ + + NSMutableArray *allRootClusters = [[NSMutableArray alloc] initWithCapacity:annotationsForTrees.allKeys.count]; + + for (NSString *key in annotationsForTrees.allKeys) { + + NSMutableSet *annotations = [annotationsForTrees[key] copy]; + + if (annotations.count == 0) { + continue; + } + // use wrapper annotations that expose a MKMapPoint property instead of a CLLocationCoordinate2D property + NSMutableSet * mapPointAnnotations = [[NSMutableSet alloc] initWithCapacity:annotations.count]; + + for (id annotation in annotations) { + ADMapPointAnnotation * mapPointAnnotation = [[ADMapPointAnnotation alloc] initWithAnnotation:annotation]; + [mapPointAnnotations addObject:mapPointAnnotation]; + } + + ADMapCluster *rootCluster = [ADMapCluster rootClusterForAnnotations:mapPointAnnotations mapView:self groupID:key completion:nil]; + [allRootClusters addObject:rootCluster]; + } + + + TSClusterMapView *strongSelf = weakSelf; + + if (allRootClusters.count <= 1) { + strongSelf.rootMapCluster = allRootClusters.firstObject; + + if (!strongSelf.rootMapCluster) { + strongSelf.rootMapCluster = [ADMapCluster rootClusterForAnnotations:nil mapView:self groupID:nil completion:nil]; + } + } + else { + strongSelf.rootMapCluster = [[ADMapCluster alloc] initWithRootClusters:allRootClusters]; + } + + [strongSelf clusterVisibleMapRectForceRefresh:YES]; + }]; +} + - (void)initAnnotationPools:(NSUInteger)numberOfAnnotationsInPool { @@ -517,6 +658,8 @@ - (void)initAnnotationPools:(NSUInteger)numberOfAnnotationsInPool { [super addAnnotations:toAdd]; }]; } + + NSLog(@"%i", _clusterAnnotationsPool.count); } - (BOOL)shouldNotAnimate { @@ -529,13 +672,9 @@ - (void)splitClusterToOriginal:(ADClusterAnnotation *)clusterAnnotation { return; } - NSDictionary *groupedRoundedLatLonAnnotations = [TSClusterOperation groupAnnotationsByLocationValue:[NSSet setWithArray:clusterAnnotation.cluster.originalAnnotations]]; - - if (groupedRoundedLatLonAnnotations.allKeys.count == 1) { - if ([_clusterDelegate respondsToSelector:@selector(mapView:shouldForceSplitClusterAnnotation:)]) { - if (![_clusterDelegate mapView:self shouldForceSplitClusterAnnotation:clusterAnnotation]) { - return; - } + if ([_clusterDelegate respondsToSelector:@selector(mapView:shouldForceSplitClusterAnnotation:)]) { + if (![_clusterDelegate mapView:self shouldForceSplitClusterAnnotation:clusterAnnotation]) { + return; } } @@ -706,7 +845,7 @@ - (void)mapView:(MKMapView *)mapView annotationView:(MKAnnotationView *)view did - (void)selectAnnotation:(id)annotation animated:(BOOL)animated { - if ([_clusterableAnnotationsAdded containsObject:annotation]) { + if ([self.clusterableAnnotationsAdded containsObject:annotation]) { for (ADClusterAnnotation *clusterAnnotation in self.visibleClusterAnnotations) { if ([clusterAnnotation.originalAnnotations containsObject:annotation]) { [super selectAnnotation:clusterAnnotation animated:animated]; @@ -760,7 +899,7 @@ - (MKAnnotationView *)refreshAnnotationViewForAnnotation:(id)annot // only leaf clusters have annotations if (((ADClusterAnnotation *)annotation).type == ADClusterAnnotationTypeLeaf) { - annotation = [((ADClusterAnnotation *)annotation).originalAnnotations firstObject]; + annotation = [((ADClusterAnnotation *)annotation).originalAnnotations anyObject]; if ([_clusterDelegate respondsToSelector:@selector(mapView:viewForAnnotation:)]) { delegateAnnotationView = [_clusterDelegate mapView:self viewForAnnotation:annotation]; } @@ -775,7 +914,7 @@ - (MKAnnotationView *)refreshAnnotationViewForAnnotation:(id)annot } //If dequeued it won't have an annotation set; - if (!delegateAnnotationView.annotation) { + if (delegateAnnotationView.annotation != annotation) { delegateAnnotationView.annotation = annotation; } @@ -816,7 +955,7 @@ - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { MKCoordinateRegion region = MKCoordinateRegionForMapRect(zoomTo); - if (zoomTo.size.width < 3000) { + if (zoomTo.size.width < 3000 || zoomTo.size.height < 3000) { float ratio = self.camera.altitude/self.visibleMapRect.size.width; diff --git a/Pod/Classes/TSClusterOperation.m b/Pod/Classes/TSClusterOperation.m index 2f3c63a..7a28516 100644 --- a/Pod/Classes/TSClusterOperation.m +++ b/Pod/Classes/TSClusterOperation.m @@ -18,6 +18,14 @@ #import "TSClusterMapView.h" #import "TSRefreshedAnnotationView.h" +@interface AwaitingMatch : NSObject +@property (nonatomic, strong) ADMapCluster *cluster; +@property (nonatomic, strong) ADClusterAnnotation *annotation; +@end + +@implementation AwaitingMatch +@end + @interface TSClusterOperation () @property (weak, nonatomic) TSClusterMapView *mapView; @@ -28,6 +36,13 @@ @interface TSClusterOperation () @property (nonatomic, strong) NSMutableSet *annotationPool; @property (nonatomic, strong) NSMutableSet *poolAnnotationRemoval; +@property (nonatomic, readonly) NSSet * unmatchedOffMapAnnotations; +@property (nonatomic, readonly) NSSet * unmatchedAnnotations; +@property (nonatomic, readonly) NSSet * matchedAnnotations; +@property (nonatomic, readonly) NSMutableSet * parentClustersMatched; + +@property (nonatomic, strong) NSMutableSet *removeAfterAnimation; + @property (nonatomic, strong) ADMapCluster *splitCluster; @end @@ -92,154 +107,100 @@ - (instancetype)initWithMapView:(TSClusterMapView *)mapView splitCluster:(ADMapC return self; } +#pragma mark - Annotation Groups -#pragma mark - Full Cluster Operation - -- (void)clusterInMapRect:(MKMapRect)clusteredMapRect { - - if (!_rootMapCluster.clusterCount) { - [self resetAll]; - return; - } - - NSUInteger maxNumberOfClusters = _numberOfClusters; - - MKMapRect annotationViewSize = [self mapRectAnnotationViewSize]; +- (NSSet *)unmatchedOffMapAnnotations { - //If there is no size available to the clustering operation use a grid to keep from cluttering - if (MKMapRectIsEmpty(annotationViewSize)) { - maxNumberOfClusters = [self calculateNumberByGrid:clusteredMapRect]; - } - - BOOL shouldOverlap = NO;//(_mapView.camera.altitude <= 400); - - //Try and account for camera pitch which distorts clustering calculations - if (_mapView.camera.pitch > 50) { - shouldOverlap = YES; - clusteredMapRect = _mapView.visibleMapRect; - } - - //Clusters that need to be visible after the animation - NSSet *clustersToShowOnMap = [_rootMapCluster find:maxNumberOfClusters childrenInMapRect:clusteredMapRect annotationViewSize:annotationViewSize allowOverlap:shouldOverlap]; - - if (self.isCancelled) { - if (_finishedBlock) { - _finishedBlock(clusteredMapRect, NO, nil); - } - return; - } - - //Sort out the current annotations to get an idea of what you're working with - NSMutableSet *offscreenAnnotations = [[NSMutableSet alloc] initWithCapacity:_annotationPool.count]; + NSMutableSet *offMapAnnotations = [[NSMutableSet alloc] initWithCapacity:_annotationPool.count]; for (ADClusterAnnotation *annotation in _annotationPool) { - if (annotation.offscreen) { - [offscreenAnnotations addObject:annotation]; + if (annotation.offMap && !annotation.cluster) { + [offMapAnnotations addObject:annotation]; } } - NSMutableSet *unmatchedAnnotations = [[NSMutableSet alloc] initWithCapacity:_annotationPool.count]; + return offMapAnnotations; +} + +- (NSSet *)unmatchedAnnotations { + + NSMutableSet *unmatchedAnnotations = [[NSMutableSet alloc] initWithCapacity:_annotationPool.count]; for (ADClusterAnnotation *annotation in _annotationPool) { if (!annotation.cluster) { [unmatchedAnnotations addObject:annotation]; } } + return unmatchedAnnotations; +} + +- (NSSet *)matchedAnnotations { - NSMutableSet *matchedAnnotations = [[NSMutableSet alloc] initWithSet:_annotationPool]; - [matchedAnnotations minusSet:unmatchedAnnotations]; - + NSMutableSet *matchedAnnotations = [[NSMutableSet alloc] initWithSet:_annotationPool]; + [matchedAnnotations minusSet:self.unmatchedAnnotations]; - // - NSMutableSet *unMatchedClusters = [[NSMutableSet alloc] initWithSet:clustersToShowOnMap]; + return matchedAnnotations; +} + +- (NSMutableSet *)matchChildren:(NSSet *)children annotation:(ADClusterAnnotation *)annotation { - //There will be only one annotation after clustering in so we want to know if the parent cluster was already matched to an annotation - NSMutableSet *parentClustersMatched = [[NSMutableSet alloc] initWithCapacity:_numberOfClusters]; + NSMutableSet *childrenToMatch = [children mutableCopy]; + NSMutableSet *stillNeedsMatch = [[NSMutableSet alloc] initWithCapacity:children.count]; + //Choose any child cluster that needs to be shown and assign it to the existing annotation so that it stays on the map and moves to the new location to represent the child cluster + ADMapCluster *cluster = [children anyObject]; + annotation.cluster = cluster; + annotation.coordinatePreAnimation = annotation.coordinate; + [childrenToMatch removeObject:cluster]; - //These will be the annotations that converge to a point and will no longer be needed - NSMutableSet *removeAfterAnimation = [[NSMutableSet alloc] initWithCapacity:_numberOfClusters]; + //There should be more than one child if it splits so we'll need to grab unused annotations. + //Clusterless offMap annotations will then start at the annotation on screen's point and split to the child coordinate. - //These will be leftovers that didn't have any annotations available to match at the time. - //Some annotations should become free after further sorting and matching. - //At the end any unmatched annotations will be used. - NSMutableSet *stillNeedsMatch = [[NSMutableSet alloc] initWithCapacity:10]; - - if (self.isCancelled) { - if (_finishedBlock) { - _finishedBlock(clusteredMapRect, NO, nil); + NSMutableSet *offMap = [self.unmatchedOffMapAnnotations mutableCopy]; + for (ADMapCluster *cluster in childrenToMatch) { + ADClusterAnnotation *clusterlessAnnotation = [offMap anyObject]; + if (clusterlessAnnotation) { + [offMap removeObject:clusterlessAnnotation]; + clusterlessAnnotation.cluster = cluster; + clusterlessAnnotation.coordinatePreAnimation = annotation.coordinate; + } + else { + //Ran out of annotations off screen we'll come back after more have been sorted and reassign one that is available + AwaitingMatch *unmatched = [[AwaitingMatch alloc] init]; + unmatched.cluster = cluster; + unmatched.annotation = annotation; + [stillNeedsMatch addObject:unmatched]; } - return; } - //Go through annotations that already have clusters and try and match them to new clusters - for (ADClusterAnnotation *annotation in matchedAnnotations) { - - NSMutableSet *children = [annotation.cluster findChildrenForClusterInSet:clustersToShowOnMap]; - - //Found children - //These will start at cluster and split to their respective cluster coordinates - if (children.count) { - - ADMapCluster *cluster = [children anyObject]; - annotation.cluster = cluster; - annotation.coordinatePreAnimation = annotation.coordinate; - - [children removeObject:cluster]; - [unMatchedClusters removeObject:cluster]; - - //There should be more than one child if it splits so we'll need to grab unused annotations. - //Clusterless offscreen annotations will then start at the annotation on screen's point and split to the child coordinate. - for (ADMapCluster *cluster in children) { - ADClusterAnnotation *clusterlessAnnotation = [offscreenAnnotations anyObject]; - - if (clusterlessAnnotation) { - clusterlessAnnotation.cluster = cluster; - clusterlessAnnotation.coordinatePreAnimation = annotation.coordinate; - - [unmatchedAnnotations removeObject:clusterlessAnnotation]; - [offscreenAnnotations removeObject:clusterlessAnnotation]; - - [unMatchedClusters removeObject:cluster]; - } - else { - //Ran out of annotations off screen we'll come back after more have been sorted and reassign one that is available - [stillNeedsMatch addObject:@[cluster, annotation]]; - } - } - - continue; - } - - - ADMapCluster *cluster = [annotation.cluster findAncestorForClusterInSet:clustersToShowOnMap]; + //Returns the children that couldn't be matched do to not enough available annotations at the time + return stillNeedsMatch; +} + +- (ADMapCluster *)matchedClusterForAnnotation:(ADClusterAnnotation *)annotation inSet:(NSSet *)clustersToShowOnMap { + + ADMapCluster *cluster = [annotation.cluster findAncestorForClusterInSet:clustersToShowOnMap]; + + //Found an ancestor + //These will start as individual annotations and converge into a single annotation during animation + if (cluster) { + annotation.cluster = cluster; + annotation.coordinatePreAnimation = annotation.coordinate; - //Found an ancestor - //These will start as individual annotations and converge into a single annotation during animation - if (cluster) { - annotation.cluster = cluster; - annotation.coordinatePreAnimation = annotation.coordinate; - - [unMatchedClusters removeObject:cluster]; - - if ([parentClustersMatched containsObject:cluster]) { - [removeAfterAnimation addObject:annotation]; - } - - [parentClustersMatched addObject:cluster]; - - continue; + if ([_parentClustersMatched containsObject:cluster]) { + [_removeAfterAnimation addObject:annotation]; } - //No ancestor or child found - //This will happen when the annotation is no longer in the visible map rect and - //the section of the cluster tree does not include this annotation - [unmatchedAnnotations addObject:annotation]; - [annotation shouldReset]; + [_parentClustersMatched addObject:cluster]; + return cluster; } - //Find annotations for remaining unmatched clusters - //If there are available nearby, set the available annotation to animate to cluster position and take over. - //After a full tree refresh all annotations will be unmatched but coordinates still may match up or be close by. + return cluster; +} + +/** + If there are available nearby, set the available annotation to animate to cluster position and take over. After a full tree refresh all annotations will be unmatched but coordinates still may match up or be close by. +*/ +- (void)matchLeftoverOnscreenAnnotations:(NSMutableSet *)unMatchedClusters { + for (ADMapCluster *cluster in [unMatchedClusters copy]) { - ADClusterAnnotation *annotation; MKMapRect mRect = _mapView.visibleMapRect; @@ -248,8 +209,8 @@ - (void)clusterInMapRect:(MKMapRect)clusteredMapRect { //Don't want annotations flying across the map CLLocationDistance min = MKMetersBetweenMapPoints(eastMapPoint, westMapPoint)/2; - NSMutableSet *unmatchedOnScreen = [NSMutableSet setWithSet:unmatchedAnnotations]; - [unmatchedOnScreen minusSet:offscreenAnnotations]; + NSMutableSet *unmatchedOnScreen = [NSMutableSet setWithSet:self.unmatchedAnnotations]; + [unmatchedOnScreen minusSet:self.unmatchedOffMapAnnotations ]; for (ADClusterAnnotation *checkAnnotation in unmatchedOnScreen) { //Could be same @@ -272,46 +233,142 @@ - (void)clusterInMapRect:(MKMapRect)clusteredMapRect { annotation.popInAnimation = NO; //already visible don't animate appearance } - else if (offscreenAnnotations.count) { - annotation = [offscreenAnnotations anyObject]; + else if (self.unmatchedOffMapAnnotations.count) { + annotation = [self.unmatchedOffMapAnnotations anyObject]; annotation.coordinatePreAnimation = cluster.clusterCoordinate; annotation.popInAnimation = YES; //Not visible animate appearance } else { - NSLog(@"Not enough annotations?!"); + //NSLog(@"Not enough annotations?!"); break; } annotation.cluster = cluster; - [unmatchedAnnotations removeObject:annotation]; - [offscreenAnnotations removeObject:annotation]; [unMatchedClusters removeObject:cluster]; } +} + +#pragma mark - Full Cluster Operation + +- (NSSet *)clustersToShowOnMap:(MKMapRect)clusteredMapRect { + + NSUInteger maxNumberOfClusters = _numberOfClusters; + + MKMapRect annotationViewSize = [self mapRectAnnotationViewSize]; + //If there is no size available to the clustering operation use a grid to keep from cluttering + if (MKMapRectIsEmpty(annotationViewSize)) { + maxNumberOfClusters = [self calculateNumberByGrid:clusteredMapRect]; + } + + BOOL shouldOverlap = NO;//(_mapView.camera.altitude <= 400); + + //Try and account for camera pitch which distorts clustering calculations + if (_mapView.camera.pitch > 50) { + shouldOverlap = YES; + clusteredMapRect = _mapView.visibleMapRect; + } + + return [_rootMapCluster find:maxNumberOfClusters childrenInMapRect:clusteredMapRect annotationViewSize:annotationViewSize allowOverlap:shouldOverlap]; +} + +- (void)clusterInMapRect:(MKMapRect)clusteredMapRect { + + //NSLog(@"1 %@", [NSOperationQueue currentQueue].name); + + if (!_rootMapCluster.clusterCount) { + [self resetAll]; + return; + } + + //Clusters that need to be visible after the animation + NSSet *clustersToShowOnMap = [self clustersToShowOnMap:clusteredMapRect]; + + //NSLog(@"2 %@", [NSOperationQueue currentQueue].name); + + if (self.isCancelled) { + if (_finishedBlock) { + _finishedBlock(clusteredMapRect, NO, nil); + } + return; + } + + + //Begin with all clusters to show on the map + NSMutableSet *unMatchedClusters = [[NSMutableSet alloc] initWithSet:clustersToShowOnMap]; + + //There will be only one annotation after clustering in so we want to know if the parent cluster was already matched to an annotation + _parentClustersMatched = [[NSMutableSet alloc] initWithCapacity:_numberOfClusters]; + + + //These will be the annotations that converge to a point and will no longer be needed + _removeAfterAnimation = [[NSMutableSet alloc] initWithCapacity:_numberOfClusters]; + + //These will be leftovers that didn't have any annotations available to match at the time. + //Some annotations should become free after further sorting and matching. + //At the end any unmatched annotations will be used. + NSMutableSet *stillNeedsMatch = [[NSMutableSet alloc] initWithCapacity:10]; + + //Go through annotations that already have clusters and try and match them to new clusters + for (ADClusterAnnotation *annotation in self.matchedAnnotations) { + + NSSet *children = [annotation.cluster findChildrenForClusterInSet:clustersToShowOnMap]; + + if (children.count) { + //Found children + //These will start at cluster and split to their respective cluster coordinates + NSSet *unmatchedChildren = [self matchChildren:children annotation:annotation]; + + //Unmatched children will be paired later when more annotations become free + [stillNeedsMatch unionSet:unmatchedChildren]; + [unMatchedClusters minusSet:children]; + continue; + } + + + ADMapCluster *cluster = [self matchedClusterForAnnotation:annotation inSet:clustersToShowOnMap]; + + if (cluster) { + //Found an ancestor + //These will start as individual annotations and converge into a single annotation during animation + [unMatchedClusters removeObject:cluster]; + continue; + } + + //No ancestor or child found + //This will happen when the annotation is no longer in the visible map rect and + //the section of the cluster tree does not include this annotation + [annotation shouldReset]; + } + + + //NSLog(@"3 %@", [NSOperationQueue currentQueue].name); + //Find annotations for remaining unmatched clusters + [self matchLeftoverOnscreenAnnotations:unMatchedClusters]; + + //NSLog(@"4 %@", [NSOperationQueue currentQueue].name); //Still need unmatched for a split into multiple from cluster if (stillNeedsMatch.count) { - for (NSArray *array in stillNeedsMatch) { - ADClusterAnnotation *clusterlessAnnotation = [unmatchedAnnotations anyObject]; + for (AwaitingMatch *awaitingMatch in stillNeedsMatch) { + ADClusterAnnotation *clusterlessAnnotation = [self.unmatchedAnnotations anyObject]; if (clusterlessAnnotation) { - clusterlessAnnotation.cluster = array[0]; - clusterlessAnnotation.coordinatePreAnimation = ((ADClusterAnnotation *)array[1]).coordinate; - - [unmatchedAnnotations removeObject:clusterlessAnnotation]; - [offscreenAnnotations removeObject:clusterlessAnnotation]; - [unMatchedClusters removeObject:clusterlessAnnotation.cluster]; + clusterlessAnnotation.cluster = awaitingMatch.cluster; + clusterlessAnnotation.coordinatePreAnimation = awaitingMatch.annotation.coordinate; + } + else { + [unMatchedClusters addObject:awaitingMatch.cluster]; } } } - matchedAnnotations = [NSMutableSet setWithSet:_annotationPool]; - [matchedAnnotations minusSet:unmatchedAnnotations]; - if (unMatchedClusters.count) { NSLog(@"Unmatched Clusters!?"); } + //NSLog(@"5 %@", [NSOperationQueue currentQueue].name); + for (ADClusterAnnotation * annotation in _annotationPool) { if (annotation.cluster) { annotation.coordinatePostAnimation = annotation.cluster.clusterCoordinate; @@ -319,15 +376,24 @@ - (void)clusterInMapRect:(MKMapRect)clusteredMapRect { } //Create a circle around coordinate to display all single annotations that overlap - [self mutateCoordinatesOfClashingAnnotations:matchedAnnotations]; + [self mutateCoordinatesOfClashingAnnotations:self.matchedAnnotations]; + + ADClusterAnnotation *annotationToSelect = [self annotationToSelect]; + //NSLog(@"6 %@", [NSOperationQueue currentQueue].name); + [[NSOperationQueue mainQueue] addOperationWithBlock:^{ + [self executeAnimationAndSelectAnnotation:annotationToSelect]; + }]; +} + +- (ADClusterAnnotation *)annotationToSelect { ADClusterAnnotation *selectedAnnotation = [_mapView.selectedAnnotations firstObject]; ADClusterAnnotation *annotationToSelect; if (selectedAnnotation && [selectedAnnotation isKindOfClass:[ADClusterAnnotation class]]) { - for (ADClusterAnnotation *annotation in matchedAnnotations) { + for (ADClusterAnnotation *annotation in self.matchedAnnotations) { if (annotation.cluster == selectedAnnotation.cluster || [annotation.cluster isAncestorOf:selectedAnnotation.cluster]) { annotationToSelect = annotation; break; @@ -335,7 +401,7 @@ - (void)clusterInMapRect:(MKMapRect)clusteredMapRect { if ((annotation.type == ADClusterAnnotationTypeCluster && CLLocationCoordinate2DIsApproxEqual(annotation.coordinate, selectedAnnotation.coordinate, .000001)) || - ![removeAfterAnimation containsObject:annotation]) { + ![_removeAfterAnimation containsObject:annotation]) { annotationToSelect = annotation; } } @@ -351,87 +417,99 @@ - (void)clusterInMapRect:(MKMapRect)clusteredMapRect { annotationToSelect = nil; } + return annotationToSelect; +} + +- (void)executeAnimationAndSelectAnnotation:(ADClusterAnnotation *)annotationToSelect { - [[NSOperationQueue mainQueue] addOperationWithBlock:^{ - - //Make sure they are in the offscreen position - for (ADClusterAnnotation *annotation in unmatchedAnnotations) { - [annotation reset]; - } - - //Make sure we close callout of cluster if needed - NSArray *selectedAnnotations = _mapView.selectedAnnotations; - for (ADClusterAnnotation *annotation in selectedAnnotations) { - if ([annotation isKindOfClass:[ADClusterAnnotation class]]) { - if ((annotation.type == ADClusterAnnotationTypeCluster && - !CLLocationCoordinate2DIsApproxEqual(annotation.coordinate, annotation.coordinatePreAnimation, .000001)) || - [removeAfterAnimation containsObject:annotation]) { - [_mapView deselectAnnotation:annotation animated:NO]; - } + NSArray *selectedAnnotations = _mapView.selectedAnnotations; + ADClusterAnnotation *selectedAnnotation = [selectedAnnotations firstObject]; + + //Make sure they are in the offMap position + for (ADClusterAnnotation *annotation in self.unmatchedAnnotations) { + [annotation reset]; + } + + //Make sure we close callout of cluster if needed + for (ADClusterAnnotation *annotation in selectedAnnotations) { + if ([annotation isKindOfClass:[ADClusterAnnotation class]]) { + if ((annotation.type == ADClusterAnnotationTypeCluster && + !CLLocationCoordinate2DIsApproxEqual(annotation.coordinate, annotation.coordinatePreAnimation, .000001)) || + [_removeAfterAnimation containsObject:annotation]) { + [_mapView deselectAnnotation:annotation animated:NO]; } } - - //Set pre animation position - for (ADClusterAnnotation *annotation in _annotationPool) { - if (CLLocationCoordinate2DIsValid(annotation.coordinatePreAnimation)) { - annotation.coordinate = annotation.coordinatePreAnimation; - } + } + + //Set pre animation position + for (ADClusterAnnotation *annotation in _annotationPool) { + if (CLLocationCoordinate2DIsValid(annotation.coordinatePreAnimation)) { + annotation.coordinate = annotation.coordinatePreAnimation; + } + } + + + for (ADClusterAnnotation * annotation in _annotationPool) { + //Get the new or cached view from delegate + if (annotation.cluster && annotation.needsRefresh) { + [_mapView refreshClusterAnnotation:annotation]; } - + //Pre animation setup for popInAnimation + if (annotation.popInAnimation && _mapView.clusterAppearanceAnimated) { + CGAffineTransform t = CGAffineTransformMakeScale(0.001, 0.001); + t = CGAffineTransformTranslate(t, 0, -annotation.annotationView.frame.size.height); + annotation.annotationView.transform = t; + } + } + + //Selected if needed + if (annotationToSelect) { + [_mapView selectAnnotation:annotationToSelect animated:YES]; + } + else if (selectedAnnotation) { + [_mapView deselectAnnotation:selectedAnnotation animated:NO]; + } + + TSClusterAnimationOptions *options = _mapView.clusterAnimationOptions; + [UIView animateWithDuration:options.duration delay:0.0 usingSpringWithDamping:options.springDamping initialSpringVelocity:options.springVelocity options:options.viewAnimationOptions animations:^{ for (ADClusterAnnotation * annotation in _annotationPool) { - //Get the new or cached view from delegate - if (annotation.cluster && annotation.needsRefresh) { - [_mapView refreshClusterAnnotation:annotation]; + if (annotation.cluster) { + annotation.coordinate = annotation.coordinatePostAnimation; + [annotation.annotationView animateView]; } - - //Pre animation setup for popInAnimation if (annotation.popInAnimation && _mapView.clusterAppearanceAnimated) { - CGAffineTransform t = CGAffineTransformMakeScale(0.001, 0.001); - t = CGAffineTransformTranslate(t, 0, -annotation.annotationView.frame.size.height); - annotation.annotationView.transform = t; + annotation.annotationView.transform = CGAffineTransformIdentity; + annotation.popInAnimation = NO; } } + } completion:^(BOOL finished) { - //Selected if needed + for (ADClusterAnnotation * annotation in _annotationPool) { + if (annotation.cluster) { + annotation.coordinate = annotation.coordinatePostAnimation; + [annotation.annotationView animateView]; + annotation.annotationView.transform = CGAffineTransformIdentity; + annotation.popInAnimation = NO; + } + } + + //Make sure selected if was previously offMap if (annotationToSelect) { [_mapView selectAnnotation:annotationToSelect animated:YES]; } - else if (selectedAnnotation) { - [_mapView deselectAnnotation:selectedAnnotation animated:NO]; + + //Need to be removed after clustering they are no longer needed + for (ADClusterAnnotation *annotation in _removeAfterAnimation) { + [annotation reset]; } - TSClusterAnimationOptions *options = _mapView.clusterAnimationOptions; - [UIView animateWithDuration:options.duration delay:0.0 usingSpringWithDamping:options.springDamping initialSpringVelocity:options.springVelocity options:options.viewAnimationOptions animations:^{ - for (ADClusterAnnotation * annotation in _annotationPool) { - if (annotation.cluster) { - annotation.coordinate = annotation.coordinatePostAnimation; - [annotation.annotationView animateView]; - } - if (annotation.popInAnimation && _mapView.clusterAppearanceAnimated) { - annotation.annotationView.transform = CGAffineTransformIdentity; - annotation.popInAnimation = NO; - } - } - } completion:^(BOOL finished) { - - //Make sure selected if was previously offscreen - if (annotationToSelect) { - [_mapView selectAnnotation:annotationToSelect animated:YES]; - } - - //Need to be removed after clustering they are no longer needed - for (ADClusterAnnotation *annotation in removeAfterAnimation) { - [annotation reset]; - } - - //If the number of clusters wanted on screen was reduced we can adjust the annotation pool accordingly to speed things up - NSSet *toRemove = [self poolAnnotationsToRemove:_numberOfClusters freeAnnotations:[unmatchedAnnotations setByAddingObjectsFromSet:removeAfterAnimation]]; - - if (_finishedBlock) { - _finishedBlock(clusteredMapRect, YES, toRemove); - } - }]; + //If the number of clusters wanted on screen was reduced we can adjust the annotation pool accordingly to speed things up + NSSet *toRemove = [self poolAnnotationsToRemove:_numberOfClusters freeAnnotations:[self.unmatchedAnnotations setByAddingObjectsFromSet:_removeAfterAnimation]]; + + if (_finishedBlock) { + _finishedBlock(_clusteringRect, YES, toRemove); + } }]; } @@ -475,12 +553,12 @@ - (void)splitSingleCluster:(ADMapCluster *)cluster { } else { annotation = [unmatchedAnnotations anyObject]; + [unmatchedAnnotations removeObject:annotation]; } annotation.cluster = leafCluster; annotation.coordinatePreAnimation = cluster.clusterCoordinate; - [unmatchedAnnotations removeObject:annotation]; [matchedAnnotations addObject:annotation]; } @@ -511,7 +589,9 @@ - (void)splitSingleCluster:(ADMapCluster *)cluster { [UIView animateWithDuration:options.duration delay:0.0 usingSpringWithDamping:options.springDamping initialSpringVelocity:options.springVelocity options:options.viewAnimationOptions animations:^{ for (ADClusterAnnotation * annotation in matchedAnnotations) { annotation.coordinate = annotation.coordinatePostAnimation; - [annotation.annotationView animateView]; + if (annotation.cluster) { + [annotation.annotationView animateView]; + } if (annotation.popInAnimation && _mapView.clusterAppearanceAnimated) { annotation.annotationView.transform = CGAffineTransformIdentity; @@ -555,7 +635,7 @@ - (MKMapRect)mapRectForRect:(CGRect)rect { //Get Hypotenuse then calculate xA*xA + xB*xB = xC*xC = distance CLLocationDistance distance = MKMetersBetweenMapPoints(MKMapPointForCoordinate(topLeft), MKMapPointForCoordinate(bottomRight)); double x = sqrt(distance*distance/(rect.size.width*rect.size.width + rect.size.height*rect.size.height)); - + CLLocationCoordinate2D translated = [self translateCoord:topLeft MetersLat:-x*rect.size.height MetersLong:x*rect.size.width]; MKMapPoint topLeftPoint = MKMapPointForCoordinate(topLeft); @@ -671,7 +751,7 @@ - (NSUInteger)calculateNumberByGrid:(MKMapRect)clusteredMapRect { - (void)mutateCoordinatesOfClashingAnnotations:(NSSet *)annotations { - NSDictionary *coordinateValuesToAnnotations = [TSClusterOperation groupClusterAnnotationsByLocationValue:annotations]; + NSDictionary *coordinateValuesToAnnotations = [TSClusterOperation groupLeafAnnotationsByLocationValue:annotations]; for (NSValue *coordinateValue in coordinateValuesToAnnotations.allKeys) { NSMutableArray *outletsAtLocation = coordinateValuesToAnnotations[coordinateValue]; @@ -681,18 +761,51 @@ - (void)mutateCoordinatesOfClashingAnnotations:(NSSet *) [self repositionAnnotations:outletsAtLocation toAvoidClashAtCoordinate:coordinate]; } } + + annotations = [annotations filteredSetUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(ADClusterAnnotation * evaluatedObject, NSDictionary * _Nullable bindings) { + + return evaluatedObject.type == ADClusterAnnotationTypeCluster && evaluatedObject.cluster; + }]]; + + for (ADClusterAnnotation *annotation in annotations) { + for (ADClusterAnnotation *compareAnnotation in annotations) { + if (compareAnnotation == annotation || [compareAnnotation.cluster.groupID isEqualToString:annotation.cluster.groupID]) { + continue; + } + + if ([annotation.cluster overlapsClusterOnMap:compareAnnotation.cluster annotationViewMapRectSize:[self mapRectAnnotationViewSize]]) { + [self repositionAnnotations:@[annotation, compareAnnotation] toAvoidClashAtCoordinate:CLLocationCoordinate2DMidPoint(annotation.cluster.clusterCoordinate, compareAnnotation.cluster.clusterCoordinate)]; + } + } + } +} + ++ (NSDictionary *>*)groupLeafAnnotationsByLocationValue:(NSSet *)annotations { + + annotations = [annotations filteredSetUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(ADClusterAnnotation * evaluatedObject, NSDictionary * _Nullable bindings) { + + return evaluatedObject.type == ADClusterAnnotationTypeLeaf && evaluatedObject.cluster; + }]]; + + return [self groupClusterByLocationValue:annotations]; } + (NSDictionary *>*)groupClusterAnnotationsByLocationValue:(NSSet *)annotations { + annotations = [annotations filteredSetUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(ADClusterAnnotation * evaluatedObject, NSDictionary * _Nullable bindings) { + + return evaluatedObject.type == ADClusterAnnotationTypeCluster && evaluatedObject.cluster; + }]]; + + return [self groupClusterByLocationValue:annotations]; +} + ++ (NSDictionary *>*)groupClusterByLocationValue:(NSSet *)annotations { + NSMutableDictionary *result = [NSMutableDictionary dictionary]; for (ADClusterAnnotation *pin in annotations) { - if (!pin.cluster || pin.type == ADClusterAnnotationTypeCluster) { - continue; - } - CLLocationCoordinate2D coordinate = CLLocationCoordinate2DRoundedLonLat(pin.cluster.clusterCoordinate, 5); NSValue *coordinateValue = [NSValue valueWithBytes:&coordinate objCType:@encode(CLLocationCoordinate2D)]; @@ -704,10 +817,12 @@ - (void)mutateCoordinatesOfClashingAnnotations:(NSSet *) [annotationsAtLocation addObject:pin]; } + return result; } + + (NSDictionary > *>*)groupAnnotationsByLocationValue:(NSSet >*)annotations { NSMutableDictionary *result = [NSMutableDictionary dictionary]; @@ -736,15 +851,50 @@ - (void)repositionAnnotations:(NSArray *)annotations toAv } } - double distance = 3 * annotations.count / 2.0; + MKMapRect mapViewRect = _mapView.visibleMapRect; + + CLLocationDistance width = MKMetersBetweenMapPoints(mapViewRect.origin, MKMapPointMake(mapViewRect.origin.x + mapViewRect.size.width, mapViewRect.origin.y)); + CLLocationDistance height = MKMetersBetweenMapPoints(mapViewRect.origin, MKMapPointMake(mapViewRect.origin.x, mapViewRect.origin.y + mapViewRect.size.height)); + CLLocationDistance minHeightWidth = MIN(width, height); + + + MKMapRect annotationRect = [self mapRectAnnotationViewSize]; + + CLLocationDistance distance = minHeightWidth/8; + + if (!MKMapRectIsNull(annotationRect)) { + + MKMapPoint originPoint = annotationRect.origin; + MKMapPoint sizePoint = MKMapPointMake(annotationRect.origin.x + annotationRect.size.width, annotationRect.origin.y + annotationRect.size.height); + distance = MKMetersBetweenMapPoints(originPoint, sizePoint); + } + + CLLocationDistance maxRadius = minHeightWidth - (2 * distance); + double radiansBetweenAnnotations = (M_PI * 2) / annotations.count; + double radius = (distance * annotations.count)/(2 * M_PI); + + radius = MIN(maxRadius, radius); + + if (annotations.count == 2 && !CLLocationCoordinate2DIsApproxEqual(annotations.firstObject.cluster.clusterCoordinate, annotations.lastObject.cluster.clusterCoordinate, .00001) ) { + + + for (ADClusterAnnotation *annotation in annotations) { + double bearing = CLLocationCoordinate2DBearingRadians(coordinate, annotation.cluster.clusterCoordinate); + CLLocationCoordinate2D newCoordinate = [TSClusterOperation calculateCoordinateFrom:coordinate onBearing:bearing atDistance:radius]; + + annotation.coordinatePostAnimation = newCoordinate; + } + + return; + } int i = 0; for (ADClusterAnnotation *annotation in annotations) { - double heading = radiansBetweenAnnotations * i; - CLLocationCoordinate2D newCoordinate = [TSClusterOperation calculateCoordinateFrom:coordinate onBearing:heading atDistance:distance]; + double bearing = radiansBetweenAnnotations * i; + CLLocationCoordinate2D newCoordinate = [TSClusterOperation calculateCoordinateFrom:coordinate onBearing:bearing atDistance:radius]; annotation.coordinatePostAnimation = newCoordinate; diff --git a/Pod/Classes/TSRefreshedAnnotationView.m b/Pod/Classes/TSRefreshedAnnotationView.m index f59cfc0..60a4ee1 100644 --- a/Pod/Classes/TSRefreshedAnnotationView.m +++ b/Pod/Classes/TSRefreshedAnnotationView.m @@ -25,6 +25,10 @@ - (id)initWithAnnotation:(id)annotation reuseIdentifier:(NSString - (void)clusteringAnimation { //Subclass and add your cluster view updates to be animated here + + if ([NSOperationQueue mainQueue] != [NSOperationQueue currentQueue]) { + NSLog(@"NotMain"); + } }