diff --git a/CCHMapClusterController/CCHMapClusterController.m b/CCHMapClusterController/CCHMapClusterController.m index d6ee90c..38e4def 100644 --- a/CCHMapClusterController/CCHMapClusterController.m +++ b/CCHMapClusterController/CCHMapClusterController.m @@ -185,6 +185,7 @@ - (void)updateAnnotationsWithCompletionHandler:(void (^)())completionHandler operation.animator = self.animator; operation.clusterControllerDelegate = self.delegate; operation.clusterController = self; + operation.clusterMethod = ClusterMethodDistanceBased; if (completionHandler) { operation.completionBlock = ^{ diff --git a/CCHMapClusterController/CCHMapClusterOperation.h b/CCHMapClusterController/CCHMapClusterOperation.h index 13ae232..7ef1776 100644 --- a/CCHMapClusterController/CCHMapClusterOperation.h +++ b/CCHMapClusterController/CCHMapClusterOperation.h @@ -33,6 +33,13 @@ @protocol CCHMapAnimator; @protocol CCHMapClusterControllerDelegate; + +typedef enum { + ClusterMethodGridBased, + ClusterMethodDistanceBased +} CCHClusterMethod; + + @interface CCHMapClusterOperation : NSOperation @property (nonatomic) CCHMapTree *allAnnotationsMapTree; @@ -41,6 +48,7 @@ @property (nonatomic) id animator; @property (nonatomic, weak) id clusterControllerDelegate; @property (nonatomic, weak) CCHMapClusterController *clusterController; +@property (nonatomic) CCHClusterMethod clusterMethod; - (instancetype)initWithMapView:(MKMapView *)mapView cellSize:(double)cellSize marginFactor:(double)marginFactor reuseExistingClusterAnnotations:(BOOL)reuseExistingClusterAnnotation maxZoomLevelForClustering:(double)maxZoomLevelForClustering minUniqueLocationsForClustering:(NSUInteger)minUniqueLocationsForClustering; diff --git a/CCHMapClusterController/CCHMapClusterOperation.m b/CCHMapClusterController/CCHMapClusterOperation.m index 19a722d..eeb321a 100644 --- a/CCHMapClusterController/CCHMapClusterOperation.m +++ b/CCHMapClusterController/CCHMapClusterOperation.m @@ -37,6 +37,7 @@ @interface CCHMapClusterOperation() @property (nonatomic) MKMapView *mapView; +@property (nonatomic) double cellSize; @property (nonatomic) double cellMapSize; @property (nonatomic) double marginFactor; @property (nonatomic) MKMapRect mapViewVisibleMapRect; @@ -56,12 +57,15 @@ @implementation CCHMapClusterOperation @synthesize executing = _executing; @synthesize finished = _finished; +@synthesize clusterMethod; + - (instancetype)initWithMapView:(MKMapView *)mapView cellSize:(double)cellSize marginFactor:(double)marginFactor reuseExistingClusterAnnotations:(BOOL)reuseExistingClusterAnnotation maxZoomLevelForClustering:(double)maxZoomLevelForClustering minUniqueLocationsForClustering:(NSUInteger)minUniqueLocationsForClustering { self = [super init]; if (self) { _mapView = mapView; + _cellSize = cellSize; _cellMapSize = [self.class cellMapSizeForCellSize:cellSize withMapView:mapView]; _marginFactor = marginFactor; _mapViewVisibleMapRect = mapView.visibleMapRect; @@ -97,20 +101,237 @@ + (MKMapRect)gridMapRectForMapRect:(MKMapRect)mapRect withCellMapSize:(double)ce return gridMapRect; } + +- (MKZoomScale) currentZoomScale +{ + CGSize screenSize = self.mapView.bounds.size; + MKMapRect mapRect = self.mapView.visibleMapRect; + MKZoomScale zoomScale = screenSize.width / mapRect.size.width; + return zoomScale; +} + + ++ (double) distanceSquared:(MKMapPoint)a b:(MKMapPoint)b { + return (a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y); +} + + ++ (MKMapRect) createBoundsFromSpan:(MKMapPoint)p span:(double)span { + double halfSpan = span / 2; + return MKMapRectMake(p.x - halfSpan, p.y - halfSpan, span, span); +} + + + - (void)start +{ + switch (self.clusterMethod) { + case ClusterMethodGridBased: + [self start_grid]; + break; + case ClusterMethodDistanceBased: + [self start_distance]; + break; + }; +} + + + +// A simple clustering algorithm with O(nlog n) performance. Resulting clusters are not +// hierarchical. Contributed by GitHub user __amishjake__. +// Steps: +// 1. Iterate over candidate annotations. +// 2. Create/re-use a CCHMapClusterAnnotation with the center of the annotation. +// 3. Add all annotations that are within a certain distance to the cluster. +// 4. Move any annotations out of an existing cluster if they are closer to another cluster. +// 5. Remove those items from the list of candidate clusters. +// 6. Add/remove items on the actual map as in the grid algorithm. +// +- (void) start_distance +{ + self.executing = YES; + + BOOL respondsToSelector = [_clusterControllerDelegate respondsToSelector:@selector(mapClusterController:willReuseMapClusterAnnotation:)]; + + double zoomLevel = CCHMapClusterControllerZoomLevelForRegion(self.mapViewRegion.center.longitude, self.mapViewRegion.span.longitudeDelta, self.mapViewWidth); + BOOL disableClustering = (zoomLevel > self.maxZoomLevelForClustering); + + // Zoom scale * MK distance = screen points + MKZoomScale zoomScale = [self currentZoomScale]; + + // The width and height of the square around a point that we'll consider later + double zoomSpecificSpan = _cellSize / zoomScale; + + // Annotations we've already looked at for a starting point for a cluster + NSMutableSet *visitedCandidates = [[NSMutableSet alloc] init]; + + // The MKAnnotations (single POIs and clusters alike) we want on display + NSMutableSet *results = [[NSMutableSet alloc] init]; + + // Map a single POI MKAnnotation to its distance (NSNumber*) from its cluster (if added to one yet) + NSMapTable *distanceToCluster = [NSMapTable strongToStrongObjectsMapTable]; + + // Map a single POI MKAnnotation to its cluster annotation (if added to one yet) + NSMapTable *itemToCluster = [NSMapTable strongToStrongObjectsMapTable]; + + // Existing annotations to re-use + NSMutableSet *annotationsToReuse = [NSMutableSet setWithSet:[_visibleAnnotationsMapTree annotationsInMapRect:self.mapView.visibleMapRect]]; + //NSMutableSet *annotationsToReuse = [NSMutableSet setWithSet:[_visibleAnnotationsMapTree annotations]]; + + // The CCHMapClusterAnnotations that are being reused + NSMutableSet *reusedAnnotations = [NSMutableSet setWithSet:[_visibleAnnotationsMapTree annotations]]; + + @synchronized(_allAnnotationsMapTree) { + + // Iterate over all POIs we know about + for (id candidate in _allAnnotationsMapTree.annotations) { + + if ([visitedCandidates containsObject:candidate]) { + // Candidate is already part of another cluster. + continue; + } + + // searchBounds is a rectangle of size zoomSpecificSpan (map x and y, + // not LatLng), centered on the candidate item's point + MKMapPoint point = MKMapPointForCoordinate(candidate.coordinate); + MKMapRect searchBounds = [self.class createBoundsFromSpan:point span:zoomSpecificSpan]; + + // Get list of MKAnnotations in that bounds + NSSet *clusterItems = [_allAnnotationsMapTree annotationsInMapRect:searchBounds]; + + CCHMapClusterAnnotation *cluster; + + if (_reuseExistingClusterAnnotations) { + // Check if an existing cluster annotation can be reused + cluster = CCHMapClusterControllerFindVisibleAnnotation(clusterItems, annotationsToReuse); + + // For unique locations, coordinate has to match as well + //if (annotationForCell && annotationSetsAreUniqueLocations) { + // BOOL coordinateMatches = fequal(coordinate.latitude, annotationForCell.coordinate.latitude) && fequal(coordinate.longitude, annotationForCell.coordinate.longitude); + // annotationForCell = coordinateMatches ? annotationForCell : nil; + //} + } + + if (cluster == nil) { + // Create new cluster annotation + cluster = [[CCHMapClusterAnnotation alloc] init]; + cluster.mapClusterController = _clusterController; + cluster.delegate = _clusterControllerDelegate; + } + else { + [reusedAnnotations addObject:cluster]; + [annotationsToReuse removeObject:cluster]; + } + + // Cluster takes coordinate of first found annotation + cluster.coordinate = candidate.coordinate; + cluster.annotations = [[NSSet alloc] init]; + + if (clusterItems.count == 1) { + // Only the current marker is in range. Just add the single item to the results. + cluster.annotations = [cluster.annotations setByAddingObject:candidate]; + [results addObject:cluster]; + [visitedCandidates addObject:candidate]; + [distanceToCluster setObject:[NSNumber numberWithDouble:0.0] forKey:candidate]; + continue; + } + + // Add the cluster of several annotations to the results + [results addObject:cluster]; + + // Iterate items in the bounds box + for (id clusterItem in clusterItems) { + + // This item may already be associated with another cluster, + // in which case we can know its distance from that cluster + NSNumber *existingDistance = [distanceToCluster objectForKey:clusterItem]; + + // Get distance from the new cluster location we're working on + double distance = [self.class distanceSquared:MKMapPointForCoordinate(clusterItem.coordinate) b:MKMapPointForCoordinate(candidate.coordinate)]; + + if (existingDistance != nil) { + // Item already belongs to another cluster. Check if it's closer to this cluster. + if ([existingDistance doubleValue] <= distance) { + // next clusterItem + continue; + } + // Remove from previous cluster. + CCHMapClusterAnnotation *prevCluster = [itemToCluster objectForKey:clusterItem]; + if (prevCluster != nil) { + NSMutableSet *set = [NSMutableSet setWithSet:prevCluster.annotations]; + [set minusSet:[NSSet setWithObject:clusterItem]]; + prevCluster.annotations = set; + } + + } + + // Record new distance + [distanceToCluster setObject:[NSNumber numberWithDouble:distance] forKey:clusterItem]; + + // Add item to the cluster we're working on + cluster.annotations = [cluster.annotations setByAddingObject:clusterItem]; + + // Update mapping in our item-to-cluster map. + [itemToCluster setObject:cluster forKey:clusterItem]; + } + + // Mark all of them visited so we don't start considering them again + [visitedCandidates addObjectsFromArray:[clusterItems allObjects]]; + } + } + + // Tell client to reconfigure all annotations that we are re-using + for (CCHMapClusterAnnotation* anno in reusedAnnotations) { + dispatch_async(dispatch_get_main_queue(), ^{ + anno.title = nil; + anno.subtitle = nil; + if (respondsToSelector) { + [_clusterControllerDelegate mapClusterController:_clusterController willReuseMapClusterAnnotation:anno]; + } + }); + } + + // Figure out difference between new and old clusters + NSSet *annotationsBeforeAsSet = CCHMapClusterControllerClusterAnnotationsForAnnotations(self.mapViewAnnotations, self.clusterController); + NSMutableSet *annotationsToKeep = [NSMutableSet setWithSet:annotationsBeforeAsSet]; + [annotationsToKeep intersectSet:results]; + NSMutableSet *annotationsToAddAsSet = [NSMutableSet setWithSet:results]; + [annotationsToAddAsSet minusSet:annotationsToKeep]; + NSArray *annotationsToAdd = [annotationsToAddAsSet allObjects]; + NSMutableSet *annotationsToRemoveAsSet = [NSMutableSet setWithSet:annotationsBeforeAsSet]; + [annotationsToRemoveAsSet minusSet:results]; + NSArray *annotationsToRemove = [annotationsToRemoveAsSet allObjects]; + + // Show cluster annotations on map + [_visibleAnnotationsMapTree removeAnnotations:annotationsToRemove]; + [_visibleAnnotationsMapTree addAnnotations:annotationsToAdd]; + dispatch_async(dispatch_get_main_queue(), ^{ + [self.mapView addAnnotations:annotationsToAdd]; + [self.animator mapClusterController:self.clusterController willRemoveAnnotations:annotationsToRemove withCompletionHandler:^{ + [self.mapView removeAnnotations:annotationsToRemove]; + + self.executing = NO; + self.finished = YES; + }]; + }); +} + + +// Original Grid-based algorithm +- (void)start_grid { self.executing = YES; - + double zoomLevel = CCHMapClusterControllerZoomLevelForRegion(self.mapViewRegion.center.longitude, self.mapViewRegion.span.longitudeDelta, self.mapViewWidth); BOOL disableClustering = (zoomLevel > self.maxZoomLevelForClustering); BOOL respondsToSelector = [_clusterControllerDelegate respondsToSelector:@selector(mapClusterController:willReuseMapClusterAnnotation:)]; - + // For each cell in the grid, pick one cluster annotation to show MKMapRect gridMapRect = [self.class gridMapRectForMapRect:self.mapViewVisibleMapRect withCellMapSize:self.cellMapSize marginFactor:self.marginFactor]; NSMutableSet *clusters = [NSMutableSet set]; CCHMapClusterControllerEnumerateCells(gridMapRect, _cellMapSize, ^(MKMapRect cellMapRect) { NSSet *allAnnotationsInCell = [_allAnnotationsMapTree annotationsInMapRect:cellMapRect]; - + if (allAnnotationsInCell.count > 0) { BOOL annotationSetsAreUniqueLocations; NSArray *annotationSets; @@ -164,9 +385,10 @@ - (void)start [visibleAnnotationsInCell removeObject:annotationForCell]; annotationForCell.annotations = annotationSet; dispatch_async(dispatch_get_main_queue(), ^{ - if (annotationSetsAreUniqueLocations) { + // 3/24/15 + //if (annotationSetsAreUniqueLocations) { annotationForCell.coordinate = coordinate; - } + //} annotationForCell.title = nil; annotationForCell.subtitle = nil; if (respondsToSelector) {