-
Notifications
You must be signed in to change notification settings - Fork 151
Added ClusterMethodDistanceBased #83
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess in objective c we follow the naming convention to use camel case or method names? |
||
| { | ||
| 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); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is never read in your code. We should not ignore this property. |
||
|
|
||
| // 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<MKAnnotation> candidate in _allAnnotationsMapTree.annotations) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This might not be the most performant way. Imagine we have tons of annotations placed over the whole globe. We'd iterate over all of them when ever we change the visible map rect for a small area. In the grid based algorithm we only consider annotations in a specific area around the visible map rect to speed up performance. I guess here we could do the same? |
||
|
|
||
| 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 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not quite sure if this works as expected. I'd start simple and first make the algorithm work without reusing annotations. Then in the next iteration we can check how to adapt the old re-use logic to the new algo |
||
| 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<MKAnnotation> 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) { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of having this type property I would rather create two classed called CCHMapClusterOperationDistanceBased and CCHMapClusterOperationGridBased