2
0

CCHMapClusterController.m 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  1. //
  2. // CCHMapClusterController.m
  3. // CCHMapClusterController
  4. //
  5. // Copyright (C) 2013 Claus Höfele
  6. //
  7. // Permission is hereby granted, free of charge, to any person obtaining a copy
  8. // of this software and associated documentation files (the "Software"), to deal
  9. // in the Software without restriction, including without limitation the rights
  10. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  11. // copies of the Software, and to permit persons to whom the Software is
  12. // furnished to do so, subject to the following conditions:
  13. //
  14. // The above copyright notice and this permission notice shall be included in
  15. // all copies or substantial portions of the Software.
  16. //
  17. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  18. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  19. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  20. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  21. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  22. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  23. // THE SOFTWARE.
  24. //
  25. // Based on https://github.com/MarcoSero/MSMapClustering by MarcoSero/WWDC 2011
  26. #import "CCHMapClusterController.h"
  27. #import "CCHMapClusterControllerDebugPolygon.h"
  28. #import "CCHMapClusterControllerUtils.h"
  29. #import "CCHMapClusterAnnotation.h"
  30. #import "CCHMapClusterControllerDelegate.h"
  31. #import "CCHMapViewDelegateProxy.h"
  32. #import "CCHCenterOfMassMapClusterer.h"
  33. #import "CCHFadeInOutMapAnimator.h"
  34. #import "CCHMapClusterOperation.h"
  35. #import "CCHMapTree.h"
  36. #define NODE_CAPACITY 10
  37. #define WORLD_MIN_LAT -85
  38. #define WORLD_MAX_LAT 85
  39. #define WORLD_MIN_LON -180
  40. #define WORLD_MAX_LON 180
  41. #define fequal(a, b) (fabs((a) - (b)) < __FLT_EPSILON__)
  42. @interface CCHMapClusterController()<MKMapViewDelegate>
  43. @property (nonatomic) NSMutableSet *allAnnotations;
  44. @property (nonatomic) CCHMapTree *allAnnotationsMapTree;
  45. @property (nonatomic) CCHMapTree *visibleAnnotationsMapTree;
  46. @property (nonatomic) NSOperationQueue *backgroundQueue;
  47. @property (nonatomic) MKMapView *mapView;
  48. @property (nonatomic) CCHMapViewDelegateProxy *mapViewDelegateProxy;
  49. @property (nonatomic) id<MKAnnotation> annotationToSelect;
  50. @property (nonatomic) CCHMapClusterAnnotation *mapClusterAnnotationToSelect;
  51. @property (nonatomic) MKCoordinateSpan regionSpanBeforeChange;
  52. @property (nonatomic, getter = isRegionChanging) BOOL regionChanging;
  53. @property (nonatomic) id<CCHMapClusterer> strongClusterer;
  54. @property (nonatomic) id<CCHMapAnimator> strongAnimator;
  55. @end
  56. @implementation CCHMapClusterController
  57. - (instancetype)initWithMapView:(MKMapView *)mapView
  58. {
  59. self = [super init];
  60. if (self) {
  61. _marginFactor = 0.5;
  62. _cellSize = 60;
  63. _maxZoomLevelForClustering = DBL_MAX;
  64. _mapView = mapView;
  65. _allAnnotations = [NSMutableSet new];
  66. _allAnnotationsMapTree = [[CCHMapTree alloc] initWithNodeCapacity:NODE_CAPACITY minLatitude:WORLD_MIN_LAT maxLatitude:WORLD_MAX_LAT minLongitude:WORLD_MIN_LON maxLongitude:WORLD_MAX_LON];
  67. _visibleAnnotationsMapTree = [[CCHMapTree alloc] initWithNodeCapacity:NODE_CAPACITY minLatitude:WORLD_MIN_LAT maxLatitude:WORLD_MAX_LAT minLongitude:WORLD_MIN_LON maxLongitude:WORLD_MAX_LON];
  68. _backgroundQueue = [[NSOperationQueue alloc] init];
  69. _backgroundQueue.maxConcurrentOperationCount = 1; // sync access to allAnnotationsMapTree & visibleAnnotationsMapTree
  70. if ([mapView.delegate isKindOfClass:CCHMapViewDelegateProxy.class]) {
  71. CCHMapViewDelegateProxy *delegateProxy = (CCHMapViewDelegateProxy *)mapView.delegate;
  72. [delegateProxy addDelegate:self];
  73. _mapViewDelegateProxy = delegateProxy;
  74. } else {
  75. _mapViewDelegateProxy = [[CCHMapViewDelegateProxy alloc] initWithMapView:mapView delegate:self];
  76. }
  77. // Keep strong reference to default instance because public property is weak
  78. id<CCHMapClusterer> clusterer = [[CCHCenterOfMassMapClusterer alloc] init];
  79. _clusterer = clusterer;
  80. _strongClusterer = clusterer;
  81. id<CCHMapAnimator> animator = [[CCHFadeInOutMapAnimator alloc] init];
  82. _animator = animator;
  83. _strongAnimator = animator;
  84. [self setReuseExistingClusterAnnotations:YES];
  85. }
  86. return self;
  87. }
  88. - (NSSet *)annotations
  89. {
  90. return self.allAnnotations.copy;
  91. }
  92. - (void)setClusterer:(id<CCHMapClusterer>)clusterer
  93. {
  94. _clusterer = clusterer;
  95. self.strongClusterer = nil;
  96. }
  97. - (void)setAnimator:(id<CCHMapAnimator>)animator
  98. {
  99. _animator = animator;
  100. self.strongAnimator = nil;
  101. }
  102. - (double)zoomLevel
  103. {
  104. MKCoordinateRegion region = self.mapView.region;
  105. return CCHMapClusterControllerZoomLevelForRegion(region.center.longitude, region.span.longitudeDelta, self.mapView.bounds.size.width);
  106. }
  107. - (void)cancelAllClusterOperations
  108. {
  109. NSOperationQueue *backgroundQueue = self.backgroundQueue;
  110. for (NSOperation *operation in backgroundQueue.operations) {
  111. if ([operation isKindOfClass:CCHMapClusterOperation.class]) {
  112. [operation cancel];
  113. }
  114. }
  115. }
  116. - (void)addAnnotations:(NSArray *)annotations withCompletionHandler:(void (^)(void))completionHandler
  117. {
  118. [self cancelAllClusterOperations];
  119. [self.allAnnotations addObjectsFromArray:annotations];
  120. [self.backgroundQueue addOperationWithBlock:^{
  121. BOOL updated = [self.allAnnotationsMapTree addAnnotations:annotations];
  122. dispatch_async(dispatch_get_main_queue(), ^{
  123. if (updated && !self.isRegionChanging) {
  124. [self updateAnnotationsWithCompletionHandler:completionHandler];
  125. } else if (completionHandler) {
  126. completionHandler();
  127. }
  128. });
  129. }];
  130. }
  131. - (void)removeAnnotations:(NSArray *)annotations withCompletionHandler:(void (^)(void))completionHandler
  132. {
  133. [self cancelAllClusterOperations];
  134. [self.allAnnotations minusSet:[NSSet setWithArray:annotations]];
  135. [self.backgroundQueue addOperationWithBlock:^{
  136. BOOL updated = [self.allAnnotationsMapTree removeAnnotations:annotations];
  137. dispatch_async(dispatch_get_main_queue(), ^{
  138. if (updated && !self.isRegionChanging) {
  139. [self updateAnnotationsWithCompletionHandler:completionHandler];
  140. } else if (completionHandler) {
  141. completionHandler();
  142. }
  143. });
  144. }];
  145. }
  146. - (void)updateAnnotationsWithCompletionHandler:(void (^)(void))completionHandler
  147. {
  148. [self cancelAllClusterOperations];
  149. CCHMapClusterOperation *operation = [[CCHMapClusterOperation alloc] initWithMapView:self.mapView
  150. cellSize:self.cellSize
  151. marginFactor:self.marginFactor
  152. reuseExistingClusterAnnotations:self.reuseExistingClusterAnnotations
  153. maxZoomLevelForClustering:self.maxZoomLevelForClustering
  154. minUniqueLocationsForClustering:self.minUniqueLocationsForClustering];
  155. operation.allAnnotationsMapTree = self.allAnnotationsMapTree;
  156. operation.visibleAnnotationsMapTree = self.visibleAnnotationsMapTree;
  157. operation.clusterer = self.clusterer;
  158. operation.animator = self.animator;
  159. operation.clusterControllerDelegate = self.delegate;
  160. operation.clusterController = self;
  161. if (completionHandler) {
  162. operation.completionBlock = ^{
  163. dispatch_async(dispatch_get_main_queue(), ^{
  164. completionHandler();
  165. });
  166. };
  167. };
  168. [self.backgroundQueue addOperation:operation];
  169. // Debugging
  170. if (self.isDebuggingEnabled) {
  171. double cellMapSize = [CCHMapClusterOperation cellMapSizeForCellSize:self.cellSize withMapView:self.mapView];
  172. MKMapRect gridMapRect = [CCHMapClusterOperation gridMapRectForMapRect:self.mapView.visibleMapRect withCellMapSize:cellMapSize marginFactor:self.marginFactor];
  173. [self updateDebugPolygonsInGridMapRect:gridMapRect withCellMapSize:cellMapSize];
  174. }
  175. }
  176. - (void)updateDebugPolygonsInGridMapRect:(MKMapRect)gridMapRect withCellMapSize:(double)cellMapSize
  177. {
  178. MKMapView *mapView = self.mapView;
  179. // Remove old polygons
  180. for (id<MKOverlay> overlay in mapView.overlays) {
  181. if ([overlay isKindOfClass:CCHMapClusterControllerDebugPolygon.class]) {
  182. CCHMapClusterControllerDebugPolygon *debugPolygon = (CCHMapClusterControllerDebugPolygon *)overlay;
  183. if (debugPolygon.mapClusterController == self) {
  184. [mapView removeOverlay:overlay];
  185. }
  186. }
  187. }
  188. // Add polygons outlining each cell
  189. CCHMapClusterControllerEnumerateCells(gridMapRect, cellMapSize, ^(MKMapRect cellMapRect) {
  190. // cellMapRect.origin.x -= MKMapSizeWorld.width; // fixes issue when view port spans 180th meridian
  191. MKMapPoint points[4];
  192. points[0] = MKMapPointMake(MKMapRectGetMinX(cellMapRect), MKMapRectGetMinY(cellMapRect));
  193. points[1] = MKMapPointMake(MKMapRectGetMaxX(cellMapRect), MKMapRectGetMinY(cellMapRect));
  194. points[2] = MKMapPointMake(MKMapRectGetMaxX(cellMapRect), MKMapRectGetMaxY(cellMapRect));
  195. points[3] = MKMapPointMake(MKMapRectGetMinX(cellMapRect), MKMapRectGetMaxY(cellMapRect));
  196. CCHMapClusterControllerDebugPolygon *debugPolygon = (CCHMapClusterControllerDebugPolygon *)[CCHMapClusterControllerDebugPolygon polygonWithPoints:points count:4];
  197. debugPolygon.mapClusterController = self;
  198. [mapView addOverlay:debugPolygon];
  199. });
  200. }
  201. - (void)deselectAllAnnotations
  202. {
  203. NSArray *selectedAnnotations = self.mapView.selectedAnnotations;
  204. for (id<MKAnnotation> selectedAnnotation in selectedAnnotations) {
  205. [self.mapView deselectAnnotation:selectedAnnotation animated:YES];
  206. }
  207. }
  208. - (void)selectAnnotation:(id<MKAnnotation>)annotation andZoomToRegionWithLatitudinalMeters:(CLLocationDistance)latitudinalMeters longitudinalMeters:(CLLocationDistance)longitudinalMeters
  209. {
  210. // Check for valid annotation
  211. BOOL existingAnnotation = [self.annotations containsObject:annotation];
  212. NSAssert(existingAnnotation, @"Invalid annotation - can only select annotations previously added by calling addAnnotations:withCompletionHandler:");
  213. if (!existingAnnotation) {
  214. return;
  215. }
  216. // Deselect annotations
  217. [self deselectAllAnnotations];
  218. // Zoom to annotation
  219. self.annotationToSelect = annotation;
  220. MKCoordinateRegion region = MKCoordinateRegionMakeWithDistance(annotation.coordinate, latitudinalMeters, longitudinalMeters);
  221. [self.mapView setRegion:region animated:YES];
  222. if (CCHMapClusterControllerCoordinateEqualToCoordinate(region.center, self.mapView.centerCoordinate)) {
  223. // Manually call update methods because region won't change
  224. [self mapView:self.mapView regionWillChangeAnimated:YES];
  225. [self mapView:self.mapView regionDidChangeAnimated:YES];
  226. }
  227. }
  228. #pragma mark - Map view proxied delegate methods
  229. - (void)mapView:(MKMapView *)mapView didAddAnnotationViews:(NSArray *)annotationViews
  230. {
  231. // Animate annotations that get added
  232. [self.animator mapClusterController:self didAddAnnotationViews:annotationViews];
  233. }
  234. - (void)mapView:(MKMapView *)mapView regionWillChangeAnimated:(BOOL)animated
  235. {
  236. self.regionSpanBeforeChange = mapView.region.span;
  237. self.regionChanging = YES;
  238. }
  239. - (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated
  240. {
  241. self.regionChanging = NO;
  242. // Deselect all annotations when zooming in/out. Longitude delta will not change
  243. // unless zoom changes (in contrast to latitude delta).
  244. BOOL hasZoomed = !fequal(mapView.region.span.longitudeDelta, self.regionSpanBeforeChange.longitudeDelta);
  245. if (hasZoomed) {
  246. [self deselectAllAnnotations];
  247. }
  248. // Update annotations
  249. [self updateAnnotationsWithCompletionHandler:^{
  250. if (self.annotationToSelect) {
  251. // Map has zoomed to selected annotation; search for cluster annotation that contains this annotation
  252. CCHMapClusterAnnotation *mapClusterAnnotation = CCHMapClusterControllerClusterAnnotationForAnnotation(self.mapView, self.annotationToSelect, mapView.visibleMapRect);
  253. self.annotationToSelect = nil;
  254. if (CCHMapClusterControllerCoordinateEqualToCoordinate(self.mapView.centerCoordinate, mapClusterAnnotation.coordinate)) {
  255. // Select immediately since region won't change
  256. [self.mapView selectAnnotation:mapClusterAnnotation animated:YES];
  257. } else {
  258. // Actual selection happens in next call to mapView:regionDidChangeAnimated:
  259. self.mapClusterAnnotationToSelect = mapClusterAnnotation;
  260. // Dispatch async to avoid calling regionDidChangeAnimated immediately
  261. dispatch_async(dispatch_get_main_queue(), ^{
  262. // No zooming, only panning. Otherwise, annotation might change to a different cluster annotation
  263. [self.mapView setCenterCoordinate:mapClusterAnnotation.coordinate animated:NO];
  264. });
  265. }
  266. } else if (self.mapClusterAnnotationToSelect) {
  267. // Map has zoomed to annotation
  268. [self.mapView selectAnnotation:self.mapClusterAnnotationToSelect animated:YES];
  269. self.mapClusterAnnotationToSelect = nil;
  270. }
  271. }];
  272. }
  273. @end