├── .gitignore ├── AGSClusterLayer ├── AGSCluster.h ├── AGSCluster.m ├── AGSClusterGrid.h ├── AGSClusterGrid.m ├── AGSClusterGridRow.h ├── AGSClusterGridRow.m ├── AGSClusterLayer.h ├── AGSClusterLayer.m ├── AGSClusterLayerRenderer.h ├── AGSClusterLayerRenderer.m ├── AGSClustering.h ├── AGSGDBFeature+AGSClustering.h ├── AGSGDBFeature+AGSClustering.m ├── AGSGraphic+AGSClustering.h ├── AGSGraphic+AGSClustering.m ├── NSArray+Utils.h └── NSArray+Utils.m ├── README.md ├── Sample ├── AGSCluster_int.h ├── ClusterLayerSample.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ └── contents.xcworkspacedata ├── Common_int.h └── Source │ ├── AGSSample-Info.plist │ ├── AGSSample-Prefix.pch │ ├── AGSSampleAppDelegate.h │ ├── AGSSampleAppDelegate.m │ ├── AGSSampleViewController.h │ ├── AGSSampleViewController.m │ ├── Data │ └── stops.geodatabase │ ├── Default-568h@2x.png │ ├── Default.png │ ├── Default@2x.png │ ├── Launch Screen.storyboard │ ├── NSObject+NFNotificationsProvider.h │ ├── NSObject+NFNotificationsProvider.m │ ├── en.lproj │ ├── InfoPlist.strings │ ├── MainStoryboard_iPad.storyboard │ └── MainStoryboard_iPhone.storyboard │ ├── icon.png │ ├── icon@2x.png │ └── main.m └── clusterlayer-plugin-ios.png /.gitignore: -------------------------------------------------------------------------------- 1 | # Taken from http://stackoverflow.com/questions/49478/git-ignore-file-for-xcode-projects 2 | ######################### 3 | # .gitignore file for Xcode4 / OS X Source projects 4 | # 5 | # NB: if you are storing "built" products, this WILL NOT WORK, 6 | # and you should use a different .gitignore (or none at all) 7 | # This file is for SOURCE projects, where there are many extra 8 | # files that we want to exclude 9 | # 10 | # For updates, see: http://stackoverflow.com/questions/49478/git-ignore-file-for-xcode-projects 11 | ######################### 12 | 13 | ##### 14 | # OS X temporary files that should never be committed 15 | 16 | .DS_Store 17 | *.swp 18 | *.lock 19 | profile 20 | 21 | 22 | #### 23 | # Xcode temporary files that should never be committed 24 | # 25 | # NB: NIB/XIB files still exist even on Storyboard projects, so we want this... 26 | 27 | *~.nib 28 | 29 | 30 | #### 31 | # Xcode build files - 32 | # 33 | # NB: slash on the end, so we only remove the FOLDER, not any files that were badly named "DerivedData" 34 | 35 | DerivedData/ 36 | 37 | # NB: slash on the end, so we only remove the FOLDER, not any files that were badly named "build" 38 | 39 | build/ 40 | 41 | 42 | ##### 43 | # Xcode private settings (window sizes, bookmarks, breakpoints, custom executables, smart groups) 44 | # 45 | # This is complicated: 46 | # 47 | # SOMETIMES you need to put this file in version control. 48 | # Apple designed it poorly - if you use "custom executables", they are 49 | # saved in this file. 50 | # 99% of projects do NOT use those, so they do NOT want to version control this file. 51 | # ..but if you're in the 1%, comment out the line "*.pbxuser" 52 | 53 | *.pbxuser 54 | *.mode1v3 55 | *.mode2v3 56 | *.perspectivev3 57 | # NB: also, whitelist the default ones, some projects need to use these 58 | !default.pbxuser 59 | !default.mode1v3 60 | !default.mode2v3 61 | !default.perspectivev3 62 | 63 | 64 | #### 65 | # Xcode 4 - semi-personal settings, often included in workspaces 66 | # 67 | # You can safely ignore the xcuserdata files - but do NOT ignore the files next to them 68 | # 69 | 70 | xcuserdata 71 | 72 | #### 73 | # XCode 4 workspaces - more detailed 74 | # 75 | # Workspaces are important! They are a core feature of Xcode - don't exclude them :) 76 | # 77 | # Workspace layout is quite spammy. For reference: 78 | # 79 | # (root)/ 80 | # (project-name).xcodeproj/ 81 | # project.pbxproj 82 | # project.xcworkspace/ 83 | # contents.xcworkspacedata 84 | # xcuserdata/ 85 | # (your name)/xcuserdatad/ 86 | # xcuserdata/ 87 | # (your name)/xcuserdatad/ 88 | # 89 | # 90 | # 91 | # Xcode 4 workspaces - SHARED 92 | # 93 | # This is UNDOCUMENTED (google: "developer.apple.com xcshareddata" - 0 results 94 | # But if you're going to kill personal workspaces, at least keep the shared ones... 95 | # 96 | # 97 | !xcshareddata 98 | 99 | #### 100 | # XCode 4 build-schemes 101 | # 102 | # PRIVATE ones are stored inside xcuserdata 103 | !xcschemes 104 | 105 | #### 106 | # Xcode 4 - Deprecated classes 107 | # 108 | # Allegedly, if you manually "deprecate" your classes, they get moved here. 109 | # 110 | # We're using source-control, so this is a "feature" that we do not want! 111 | 112 | *.moved-aside 113 | 114 | #### 115 | # Output for universal framework builds 116 | Products/ 117 | -------------------------------------------------------------------------------- /AGSClusterLayer/AGSCluster.h: -------------------------------------------------------------------------------- 1 | // 2 | // AGSCluster.h 3 | // Cluster Layer 4 | // 5 | // Created by Nicholas Furness on 3/25/14. 6 | // Copyright (c) 2014 ESRI. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | 12 | @interface AGSCluster : AGSGraphic 13 | 14 | //Number of features in the cluster 15 | @property (nonatomic, readonly) NSUInteger featureCount; 16 | 17 | //Unique key identifying the cluster item 18 | @property (nonatomic, readonly) id clusterItemKey; 19 | 20 | //Direct child clusters of this cluster 21 | @property (nonatomic, readonly) NSArray *childClusters; 22 | 23 | //All features, including (recursively) those of child-clusters 24 | @property (nonatomic, readonly) NSArray *features; 25 | 26 | //Coverage geometry of the cluster 27 | @property (nonatomic, readonly) AGSGeometry *coverage; 28 | 29 | //The coverage graphic of the cluster 30 | @property (nonatomic, readonly) AGSGraphic *coverageGraphic; 31 | 32 | //Envelope of the coverage 33 | @property (nonatomic, readonly) AGSEnvelope *envelope; 34 | 35 | @property (nonatomic, assign) BOOL showCoverage; 36 | 37 | //Initializes and returns an AGSCluster object for the given point 38 | +(AGSCluster *)clusterForPoint:(AGSPoint *)point; 39 | 40 | //Adds a list of AGSClusterItem objects on the cluster 41 | -(void)addItems:(NSArray *)items; 42 | 43 | //Removes all features and child clusters in the cluster 44 | -(void)removeAllItems; 45 | 46 | @end 47 | -------------------------------------------------------------------------------- /AGSClusterLayer/AGSCluster.m: -------------------------------------------------------------------------------- 1 | // 2 | // AGSCluster.m 3 | // Cluster Layer 4 | // 5 | // Created by Nicholas Furness on 3/25/14. 6 | // Copyright (c) 2014 ESRI. All rights reserved. 7 | // 8 | 9 | #import "AGSCluster.h" 10 | #import "AGSCluster_int.h" 11 | #import "NSArray+Utils.h" 12 | #import "AGSGraphic+AGSClustering.h" 13 | #import "Common_int.h" 14 | #import 15 | 16 | @interface AGSCluster () 17 | @property (nonatomic, assign, readwrite) NSUInteger featureCount; 18 | 19 | @property (nonatomic, strong) AGSClusterGrid *parentGrid; 20 | @property (nonatomic, strong) AGSCluster *parentCluster; 21 | 22 | @property (nonatomic, assign) NSUInteger clusterId; 23 | @property (nonatomic, assign) CGPoint cellCoordinate; 24 | 25 | @property (nonatomic, strong) NSMutableArray *_int_clusters; 26 | @property (nonatomic, strong) NSMutableArray *_int_features; 27 | 28 | @property (nonatomic, strong, readwrite) AGSGeometry *coverage; 29 | @property (nonatomic, strong, readwrite) AGSGraphic *coverageGraphic; 30 | 31 | @property (nonatomic, assign) BOOL shouldRecalculateGeometry; 32 | @property (nonatomic, assign) BOOL shouldRecalculateCoverageGeometry; 33 | 34 | @end 35 | 36 | @implementation AGSCluster 37 | @synthesize coverage = _coverage; 38 | @synthesize coverageGraphic = _coverageGraphic; 39 | 40 | #pragma mark - Initializers 41 | 42 | -(id)initWithPoint:(AGSPoint *)point { 43 | self = [self init]; 44 | if (self) { 45 | 46 | self.geometry = point; 47 | self._int_features = [NSMutableArray array]; 48 | self._int_clusters = [NSMutableArray array]; 49 | self.clusterId = [AGSCluster nextClusterId]; 50 | 51 | } 52 | return self; 53 | } 54 | 55 | #pragma mark - Convenience constructors 56 | +(AGSCluster *)clusterForPoint:(AGSPoint *)point { 57 | return [[AGSCluster alloc] initWithPoint:point]; 58 | } 59 | 60 | +(NSUInteger)nextClusterId { 61 | static NSUInteger clusterId = 0; 62 | return clusterId++; 63 | } 64 | 65 | #pragma mark - Override Properties 66 | -(NSUInteger)featureId { 67 | return self.clusterId; 68 | } 69 | 70 | #pragma mark - Properties 71 | -(id)clusterItemKey { 72 | return [NSString stringWithFormat:@"c%d", self.featureId]; 73 | } 74 | 75 | -(NSArray *)features { 76 | return self._int_features; 77 | } 78 | 79 | -(NSArray *)childClusters { 80 | return self._int_clusters; 81 | } 82 | 83 | -(NSUInteger)featureCount { 84 | return self.features.count; 85 | } 86 | 87 | -(void)setShouldRecalculateGeometry:(BOOL)isDirty { 88 | _shouldRecalculateGeometry = isDirty; 89 | if (_shouldRecalculateGeometry) { 90 | self.shouldRecalculateCoverageGeometry = YES; 91 | } 92 | } 93 | 94 | #pragma mark - Category Override 95 | -(BOOL)isCluster { 96 | return YES; 97 | } 98 | 99 | #pragma mark - Geometry and Coverage 100 | -(AGSGeometry *)geometry { 101 | if (self.shouldRecalculateGeometry) { 102 | [self recalculateCentroid]; 103 | } 104 | return super.geometry; 105 | } 106 | 107 | -(void)setGeometry:(AGSGeometry *)geometry { 108 | [super setGeometry:geometry]; 109 | self.shouldRecalculateGeometry = NO; 110 | } 111 | 112 | -(AGSEnvelope *)envelope { 113 | return self.coverage.envelope; 114 | } 115 | 116 | -(AGSGeometry *)coverage { 117 | if (self.shouldRecalculateCoverageGeometry) { 118 | [self recalculateCoverage]; 119 | } 120 | return _coverage; 121 | } 122 | 123 | -(void)setCoverage:(AGSGeometry *)coverage { 124 | _coverage = coverage; 125 | } 126 | 127 | -(AGSGraphic *)coverageGraphic { 128 | if (!_coverageGraphic) { 129 | _coverageGraphic = [AGSGraphic graphicWithGeometry:self.coverage symbol:nil attributes:nil]; 130 | objc_setAssociatedObject(_coverageGraphic, kClusterPayloadKey, self, OBJC_ASSOCIATION_ASSIGN); 131 | } 132 | return _coverageGraphic; 133 | } 134 | 135 | #pragma mark - Add and remove features 136 | -(void)addItems:(NSArray *)items { 137 | for (AGSClusterItem *item in items) { 138 | [self _addItem:item]; 139 | } 140 | } 141 | 142 | -(void)removeAllItems { 143 | [self._int_features removeAllObjects]; 144 | [self._int_clusters removeAllObjects]; 145 | self.shouldRecalculateGeometry = YES; 146 | [self recalculateCentroid]; 147 | } 148 | 149 | #pragma mark - Add and remove features (internal methods for iteration) 150 | -(void)_addItem:(AGSClusterItem *)item { 151 | if (item.isCluster) { 152 | AGSCluster *clusterItem = (AGSCluster *)item; 153 | clusterItem.parentCluster = self; 154 | [self._int_clusters addObject:item]; 155 | [self._int_features addObjectsFromArray:clusterItem.features]; 156 | } else { 157 | [self._int_features addObject:item]; 158 | } 159 | self.shouldRecalculateGeometry = YES; 160 | } 161 | 162 | -(void)_removeItem:(AGSClusterItem *)item { 163 | if (item.isCluster) { 164 | AGSCluster *clusterItem = (AGSCluster *)item; 165 | clusterItem.parentCluster = nil; 166 | [self._int_clusters removeObject:item]; 167 | [self._int_features removeObjectsInArray:clusterItem.features]; 168 | } else { 169 | [self._int_features removeObject:item]; 170 | } 171 | self.shouldRecalculateGeometry = YES; 172 | } 173 | 174 | #pragma mark - Centroid logic 175 | -(void) recalculateCentroid { 176 | if (!self.shouldRecalculateGeometry) return; 177 | 178 | AGSPoint *centroid = nil; 179 | NSArray *items = self.features; 180 | if (items.count == 1) { 181 | centroid = (AGSPoint *)((AGSClusterItem *)items[0]).geometry; 182 | } else if (items.count == 0) { 183 | centroid = [self.parentGrid cellCentroid:self.cellCoordinate]; 184 | } else { 185 | double xTotal = 0, yTotal = 0; 186 | AGSSpatialReference *ref = nil; 187 | for (AGSClusterItem *item in items) { 188 | AGSPoint *pt = (AGSPoint *)item.geometry; 189 | xTotal += pt.x; 190 | yTotal += pt.y; 191 | if (!ref) ref = pt.spatialReference; 192 | } 193 | centroid = [AGSPoint pointWithX:xTotal/items.count y:yTotal/items.count spatialReference:ref]; 194 | } 195 | self.geometry = centroid; 196 | } 197 | 198 | -(void) recalculateCoverage { 199 | if (!self.shouldRecalculateCoverageGeometry) return; 200 | 201 | if (self.features.count == 1) { 202 | self.coverage = self.geometry; 203 | } else if (self.features.count == 2) { 204 | AGSPoint *pt1 = (AGSPoint *)((AGSClusterItem *)self.features[0]).geometry; 205 | AGSPoint *pt2 = (AGSPoint *)((AGSClusterItem *)self.features[1]).geometry; 206 | AGSMutablePolyline *line = [[AGSMutablePolyline alloc] initWithSpatialReference:pt1.spatialReference]; 207 | [line addPathToPolyline]; 208 | [line addPoint:pt1 toPath:0]; 209 | [line addPoint:pt2 toPath:0]; 210 | self.coverage = line; 211 | } else { 212 | NSArray *allGeomsForCoverage = [self.features map:^id(id obj) { 213 | return ((AGSClusterItem *)obj).geometry; 214 | }]; 215 | AGSGeometry *geom = [[AGSGeometryEngine defaultGeometryEngine] unionGeometries:allGeomsForCoverage]; 216 | AGSGeometry *coverage = [[AGSGeometryEngine defaultGeometryEngine] convexHullForGeometry:geom]; 217 | self.coverage = coverage; 218 | } 219 | self.shouldRecalculateCoverageGeometry = NO; 220 | } 221 | 222 | #pragma mark - Description Override 223 | -(NSString *)description { 224 | return [NSString stringWithFormat:@"Cluster %d (%d features, %d child clusters)", self.clusterId, self._int_features.count, self._int_clusters.count]; 225 | } 226 | @end -------------------------------------------------------------------------------- /AGSClusterLayer/AGSClusterGrid.h: -------------------------------------------------------------------------------- 1 | // 2 | // AGSClusterGrid.h 3 | // Cluster Layer 4 | // 5 | // With much thanks to Leaflet.markercluster's DistanceGrid code: 6 | // https://github.com/Leaflet/Leaflet.markercluster/blob/master/src/DistanceGrid.js 7 | // 8 | // Created by Nicholas Furness on 3/24/14. 9 | // Copyright (c) 2014 ESRI. All rights reserved. 10 | // 11 | 12 | #import 13 | #import 14 | #import "AGSClustering.h" 15 | 16 | @interface AGSClusterGrid : NSObject 17 | 18 | //The cell size of this cluster grid 19 | @property (nonatomic, assign, readonly) NSUInteger cellSize; 20 | 21 | //Initializes an AGSClusterGrid object for the cluster layer with the given cell size 22 | -(id)initWithCellSize:(NSUInteger)cellSize forClusterLayer:(AGSClusterLayer *)clusterLayer; 23 | 24 | //Returns the centroid for the given cell co-ordinate 25 | -(AGSPoint *)cellCentroid:(CGPoint)cellCoord; 26 | @end 27 | -------------------------------------------------------------------------------- /AGSClusterLayer/AGSClusterGrid.m: -------------------------------------------------------------------------------- 1 | // 2 | // AGSClusterDistanceGrid.m 3 | // Cluster Layer 4 | // 5 | // Created by Nicholas Furness on 3/24/14. 6 | // Copyright (c) 2014 ESRI. All rights reserved. 7 | // 8 | 9 | #import "AGSClusterGrid.h" 10 | #import "AGSCluster.h" 11 | #import "AGSCluster_int.h" 12 | #import "AGSClusterGridRow.h" 13 | #import "Common_int.h" 14 | #import 15 | 16 | #define kAddFeaturesArrayKey @"__tempArrayKey" 17 | 18 | NSString * const AGSClusterGridClusteringNotification = kClusterGridClusteringNotification; 19 | NSString * const AGSClusterGridClusteredNotification = kClusterGridClusteredNotification; 20 | 21 | CGPoint getGridCoordForMapPoint(AGSPoint* pt, NSUInteger cellSize); 22 | AGSPoint* getGridCellCentroid(CGPoint cellCoord, NSUInteger cellSize); 23 | 24 | #pragma mark - Cluster Grid 25 | @interface AGSClusterGrid() 26 | @property (nonatomic, weak) AGSClusterLayer *owningClusterLayer; 27 | @property (nonatomic, strong) NSMutableDictionary *rows; 28 | @property (nonatomic, strong, readwrite) NSMutableArray *items; 29 | @property (nonatomic, assign, readwrite) NSUInteger cellSize; 30 | @end 31 | 32 | @implementation AGSClusterGrid 33 | @synthesize clusters = _clusters; 34 | @synthesize zoomLevel = _zoomLevel; 35 | @synthesize gridForNextZoomLevel = _gridForNextZoomLevel; 36 | @synthesize gridForPrevZoomLevel = _gridForPrevZoomLevel; 37 | 38 | -(id)initWithCellSize:(NSUInteger)cellSize forClusterLayer:(AGSClusterLayer *)clusterLayer { 39 | self = [self init]; 40 | if (self) { 41 | self.cellSize = cellSize; 42 | self.rows = [NSMutableDictionary dictionary]; 43 | self.items = [NSMutableArray array]; 44 | self.owningClusterLayer = clusterLayer; 45 | } 46 | return self; 47 | } 48 | 49 | -(void)addItems:(NSArray *)items { 50 | 51 | [self.items addObjectsFromArray:items]; 52 | [self clusterItems]; 53 | [self.gridForPrevZoomLevel addItems:self.clusters]; 54 | } 55 | 56 | -(void)removeAllItems { 57 | 58 | for (AGSClusterGridRow *row in [self.rows objectEnumerator]) { 59 | for (AGSCluster *cluster in [row.clusters objectEnumerator]) { 60 | [cluster removeAllItems]; 61 | } 62 | [row removeAllClusters]; 63 | } 64 | [self removeAllRows]; 65 | } 66 | 67 | - (NSMutableSet *)calculateClusters { 68 | 69 | NSArray *items = self.items; 70 | 71 | // Add each item to the clusters (creating new ones if necessary). 72 | NSMutableSet *clustersForItems = [NSMutableSet set]; 73 | for (AGSClusterItem *item in items) { 74 | // Find out what cluster this item should belong to. 75 | AGSCluster *cluster = [self clusterForItem:item]; 76 | 77 | // And track this item in an array associated with this cluster. 78 | NSMutableArray *itemsToAddToCluster = objc_getAssociatedObject(cluster, kAddFeaturesArrayKey); 79 | if (!itemsToAddToCluster) { 80 | itemsToAddToCluster = [NSMutableArray array]; 81 | objc_setAssociatedObject(cluster, kAddFeaturesArrayKey, itemsToAddToCluster, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 82 | } 83 | [itemsToAddToCluster addObject:item]; 84 | 85 | // Also track the clusters we've touched with these items. 86 | [clustersForItems addObject:cluster]; 87 | } 88 | return clustersForItems; 89 | } 90 | 91 | -(void)clusterItems { 92 | 93 | [[NSNotificationCenter defaultCenter] postNotificationName:kClusterGridClusteringNotification object:self]; 94 | 95 | NSDate *startTime = [NSDate date]; 96 | 97 | NSSet *clustersForItems = [self calculateClusters]; 98 | 99 | // Now go over the clusters we've touched and bulk add items to each individual cluster. 100 | NSUInteger totalFeatureCount = 0; 101 | for (AGSCluster *cluster in clustersForItems) { 102 | NSArray *itemsToAdd = objc_getAssociatedObject(cluster, kAddFeaturesArrayKey); 103 | [cluster addItems:itemsToAdd]; 104 | totalFeatureCount += cluster.featureCount; 105 | // Remove the temporary reference to the array that tracked the items to add to this cluster 106 | objc_setAssociatedObject(cluster, kAddFeaturesArrayKey, nil, OBJC_ASSOCIATION_ASSIGN); 107 | } 108 | 109 | NSTimeInterval clusteringDuration = -[startTime timeIntervalSinceNow]; 110 | 111 | [[NSNotificationCenter defaultCenter] postNotificationName:kClusterGridClusteredNotification object:self 112 | userInfo:@{ 113 | kClusterGridClusteredNotification_Key_FeatureCount: @(totalFeatureCount), 114 | kClusterGridClusteredNotification_Key_ClusterCount: @(self.clusters.count), 115 | kClusterGridClusteredNotification_Key_Duration: @(clusteringDuration), 116 | kClusterGridClusteredNotification_Key_ZoomLevel: self.zoomLevel 117 | }]; 118 | } 119 | 120 | -(AGSCluster *)clusterForItem:(AGSClusterItem *)item { 121 | AGSPoint *pt = (AGSPoint *)item.geometry; 122 | NSAssert(pt != nil, @"Graphic Geometry is NIL!"); 123 | 124 | // What cell (cluster) should this graphic go into? 125 | CGPoint gridCoord = getGridCoordForMapPoint(pt, self.cellSize); 126 | 127 | // Return the cluster 128 | return [self clusterForGridCoord:gridCoord atPoint:pt]; 129 | } 130 | 131 | -(AGSCluster *)clusterForGridCoord:(CGPoint)gridCoord atPoint:(AGSPoint *)point { 132 | // Find the cells along that row. 133 | AGSClusterGridRow *row = [self rowForGridCoord:gridCoord]; 134 | return [row clusterForGridCoord:gridCoord atPoint:point]; 135 | } 136 | 137 | -(AGSClusterGridRow *)rowForGridCoord:(CGPoint)gridCoord { 138 | AGSClusterGridRow *row = self.rows[@(gridCoord.y)]; 139 | if (!row) { 140 | row = [AGSClusterGridRow clusterGridRowForClusterGrid:self]; 141 | [self.rows setObject:row forKey:@(gridCoord.y)]; 142 | } 143 | return row; 144 | } 145 | 146 | -(AGSPoint *)cellCentroid:(CGPoint)cellCoord { 147 | return getGridCellCentroid(cellCoord, self.cellSize); 148 | } 149 | 150 | -(NSArray *)clusters { 151 | NSMutableArray *clusters = [NSMutableArray array]; 152 | for (AGSClusterGridRow *row in [self.rows objectEnumerator]) { 153 | for (AGSCluster *cluster in [row.clusters objectEnumerator]) { 154 | [clusters addObject:cluster]; 155 | } 156 | } 157 | return [NSArray arrayWithArray:clusters]; 158 | } 159 | 160 | -(void)removeAllRows { 161 | [self.rows removeAllObjects]; 162 | } 163 | 164 | -(NSString *)description { 165 | NSUInteger clusterCount = 0; 166 | NSUInteger featureCount = 0; 167 | NSUInteger loneFeatures = 0; 168 | for (AGSCluster *cluster in self.clusters) { 169 | if (cluster.features.count > 1) { 170 | clusterCount++; 171 | } else { 172 | loneFeatures++; 173 | } 174 | featureCount += cluster.features.count; 175 | } 176 | return [NSString stringWithFormat:@"Cluster Grid: %d features in %d clusters (with %d unclustered)", featureCount, clusterCount, loneFeatures]; 177 | } 178 | @end 179 | 180 | #pragma mark - Helper Methods 181 | CGPoint getGridCoordForMapPoint(AGSPoint* pt, NSUInteger cellSize) { 182 | return CGPointMake(floor(pt.x/cellSize), floor(pt.y/cellSize)); 183 | } 184 | 185 | AGSPoint* getGridCellCentroid(CGPoint cellCoord, NSUInteger cellSize) { 186 | return [AGSPoint pointWithX:(cellCoord.x * cellSize) + (cellSize/2) 187 | y:cellCoord.y * cellSize + (cellSize/2) 188 | spatialReference:[AGSSpatialReference webMercatorSpatialReference]]; 189 | } -------------------------------------------------------------------------------- /AGSClusterLayer/AGSClusterGridRow.h: -------------------------------------------------------------------------------- 1 | // 2 | // AGSClusterGridRow.h 3 | // Cluster Layer 4 | // 5 | // Created by Nicholas Furness on 4/7/14. 6 | // Copyright (c) 2014 ESRI. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | 12 | @class AGSClusterGrid; 13 | @class AGSCluster; 14 | 15 | @interface AGSClusterGridRow : NSObject 16 | 17 | //The cluster grid to which this row belongs to 18 | @property (nonatomic, weak) AGSClusterGrid *parentGrid; 19 | 20 | //Clusters in this grid row 21 | @property (nonatomic, strong, readonly) NSMutableDictionary *clusters; 22 | 23 | //Initializes and returns an AGSClusterGridRow object for the given cluster grid 24 | +(AGSClusterGridRow *)clusterGridRowForClusterGrid:(AGSClusterGrid *)grid; 25 | 26 | //Returns an AGSCluster object for the given grid co-ordinates and point 27 | -(AGSCluster *)clusterForGridCoord:(CGPoint)gridCoord atPoint:(AGSPoint *)point; 28 | -(void)removeAllClusters; 29 | @end 30 | 31 | -------------------------------------------------------------------------------- /AGSClusterLayer/AGSClusterGridRow.m: -------------------------------------------------------------------------------- 1 | // 2 | // AGSClusterGridRow.m 3 | // Cluster Layer 4 | // 5 | // Created by Nicholas Furness on 4/7/14. 6 | // Copyright (c) 2014 ESRI. All rights reserved. 7 | // 8 | 9 | #import "AGSClusterGridRow.h" 10 | #import "AGSCluster.h" 11 | #import "AGSCluster_int.h" 12 | #import "AGSClusterGrid.h" 13 | 14 | @interface AGSClusterGridRow () 15 | @property (nonatomic, strong, readwrite) NSMutableDictionary *clusters; 16 | @end 17 | 18 | @implementation AGSClusterGridRow 19 | +(AGSClusterGridRow *)clusterGridRowForClusterGrid:(AGSClusterGrid *)grid { 20 | return [[AGSClusterGridRow alloc] initForClusterGrid:grid]; 21 | } 22 | 23 | -(id)initForClusterGrid:(AGSClusterGrid *)grid { 24 | self = [super init]; 25 | if (self) { 26 | self.clusters = [NSMutableDictionary dictionary]; 27 | self.parentGrid = grid; 28 | } 29 | return self; 30 | } 31 | 32 | -(AGSCluster *)clusterForGridCoord:(CGPoint)gridCoord atPoint:(AGSPoint *)point{ 33 | AGSCluster *result = self.clusters[@(gridCoord.x)]; 34 | if (!result) { 35 | result = [AGSCluster clusterForPoint:point]; 36 | result.cellCoordinate = gridCoord; 37 | result.parentGrid = self.parentGrid; 38 | [self.clusters setObject:result forKey:@(gridCoord.x)]; 39 | } 40 | return result; 41 | } 42 | 43 | -(void)removeAllClusters { 44 | [self.clusters removeAllObjects]; 45 | } 46 | @end 47 | -------------------------------------------------------------------------------- /AGSClusterLayer/AGSClusterLayer.h: -------------------------------------------------------------------------------- 1 | // 2 | // AGSClusterLayer.h 3 | // Cluster Layer 4 | // 5 | // Created by Nicholas Furness on 3/24/14. 6 | // Copyright (c) 2014 ESRI. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "AGSClusterLayerRenderer.h" 11 | 12 | @interface AGSClusterLayer : AGSGraphicsLayer 13 | 14 | #pragma mark - Display 15 | // Global control over cluster visibility. 16 | @property (nonatomic, assign) BOOL clusteringEnabled; 17 | 18 | // The boolean value to toggle cluster coverage visibility 19 | @property (nonatomic, assign) BOOL showsClusterCoverages; 20 | 21 | #pragma mark - Configuration 22 | // The minimum number of features required to render as a cluster. 23 | @property (nonatomic, assign) NSUInteger minClusterCount; 24 | 25 | // The minimum scale beyond which clustering is not rendered. 26 | @property (nonatomic, assign) double minScaleForClustering; 27 | 28 | #pragma mark - State 29 | // The boolean value indicating whether clustering can be performed at the current scale 30 | @property (nonatomic, readonly) BOOL willClusterAtCurrentScale; 31 | 32 | // Returns an envelope which is the union of all cluster-coverage envelopes in the provided zoom level 33 | -(AGSEnvelope *)envelopeForClustersAtZoomLevel:(NSUInteger)zoomLevel; 34 | 35 | #pragma mark - Factory Methods 36 | // Initializes and returns a cluster layer for the provided feature layer 37 | +(AGSClusterLayer *)clusterLayerForFeatureLayer:(AGSFeatureLayer *)featureLayer; 38 | 39 | // Initializes and returns a cluster layer for the provided feature layer, cluster symbol block and coverage symbol block 40 | +(AGSClusterLayer *)clusterLayerForFeatureLayer:(AGSFeatureLayer *)featureLayer 41 | usingClusterSymbolBlock:(AGSClusterSymbolGeneratorBlock)clusterBlock 42 | coverageSymbolBlock:(AGSClusterSymbolGeneratorBlock)coverageBlock; 43 | 44 | // Initializes and returns a cluster layer for the provided graphics layer 45 | +(AGSClusterLayer *)clusterLayerForGraphicsLayer:(AGSGraphicsLayer *)graphicsLayer; 46 | 47 | // Initializes and returns a cluster layer for the provided graphics layer, cluster symbol block and coverage symbol block 48 | +(AGSClusterLayer *)clusterLayerForGraphicsLayer:(AGSGraphicsLayer *)graphicsLayer 49 | usingClusterSymbolBlock:(AGSClusterSymbolGeneratorBlock)clusterBlock 50 | coverageSymbolBlock:(AGSClusterSymbolGeneratorBlock)coverageBlock; 51 | 52 | +(AGSClusterLayer *)clusterLayerForFeatureTableLayer:(AGSFeatureTableLayer *)featureTableLayer; 53 | 54 | @end 55 | -------------------------------------------------------------------------------- /AGSClusterLayer/AGSClusterLayer.m: -------------------------------------------------------------------------------- 1 | // 2 | // AGSClusterLayer.m 3 | // Cluster Layer 4 | // 5 | // Created by Nicholas Furness on 3/24/14. 6 | // Copyright (c) 2014 ESRI. All rights reserved. 7 | // 8 | 9 | #import "AGSClustering.h" 10 | #import "AGSClusterLayer.h" 11 | #import "AGSClusterGrid.h" 12 | #import "AGSCluster.h" 13 | #import "Common_int.h" 14 | #import "NSArray+Utils.h" 15 | #import "AGSGraphic+AGSClustering.h" 16 | #import "AGSGDBFeature+AGSClustering.h" 17 | #import 18 | 19 | #pragma mark - Constants and Defines 20 | #define kClusterRenderBlockParameterKey @"clusterBlock" 21 | #define kCoverageRenderBlockParameterKey @"coverageBlock" 22 | 23 | #define kLODLevelScale @"scale" 24 | #define kLODLevelResolution @"resolution" 25 | #define kLODLevelZoomLevel @"level" 26 | #define kLODLevelCellSize @"cellSize" 27 | #define kLODLevelGrid @"grid" 28 | 29 | #define kDefaultMinClusterCount 2 30 | 31 | #define kBatchQueryOperationQueryKey @"__batchquery" 32 | 33 | NSString * const AGSClusterLayerClusteringProgressNotification = kClusterLayerClusteringNotification; 34 | NSString * const AGSClusterLayerClusteringProgressNotification_UserInfo_PercentComplete = kClusterLayerClusteringNotification_Key_PercentComplete; 35 | NSString * const AGSClusterLayerClusteringProgressNotification_UserInfo_TotalZoomLevels = kClusterLayerClusteringNotification_Key_TotalZoomLevels; 36 | NSString * const AGSClusterLayerClusteringProgressNotification_UserInfo_CompletedZoomLevels = kClusterLayerClusteringNotification_Key_ZoomLevelsClustered; 37 | NSString * const AGSClusterLayerClusteringProgressNotification_UserInfo_FeatureCount = kClusterLayerClusteringNotification_Key_FeatureCount; 38 | NSString * const AGSClusterLayerClusteringProgressNotification_UserInfo_Duration = kClusterLayerClusteringNotification_Key_Duration; 39 | 40 | NSString * const AGSClusterLayerDataLoadingErrorNotification = kClusterLayerDataLoadingErrorNotification; 41 | NSString * const AGSClusterLayerDataLoadingErrorNotification_UserInfo_Error = kClusterLayerDataLoadingErrorNotification_Key_Error; 42 | 43 | NSString * const AGSClusterLayerDataLoadingProgressNotification = kClusterLayerDataLoadingNotification; 44 | NSString * const AGSClusterLayerDataLoadingProgressNotification_UserInfo_PercentComplete = kClusterLayerDataLoadingNotification_Key_PercentComplete; 45 | NSString * const AGSClusterLayerDataLoadingProgressNotification_UserInfo_TotalRecordsToLoad = kClusterLayerDataLoadingNotification_Key_TotalRecords; 46 | NSString * const AGSClusterLayerDataLoadingProgressNotification_UserInfo_RecordsLoaded = kClusterLayerDataLoadingNotification_Key_LoadedCount; 47 | 48 | NSString * NSStringFromBool(BOOL boolValue) { 49 | return boolValue?@"Yes":@"No"; 50 | } 51 | 52 | #pragma mark - Internal properties 53 | @interface AGSClusterLayer () 54 | 55 | @property (nonatomic, strong) NSMutableDictionary *grids; 56 | @property (nonatomic, strong) NSArray *sortedGridKeys; 57 | @property (nonatomic, strong) AGSClusterGrid *gridForCurrentScale; 58 | @property (nonatomic, readonly) AGSClusterGrid *maxZoomLevelGrid; 59 | 60 | @property (nonatomic, strong) NSArray *lodData; 61 | 62 | @property (nonatomic, weak) AGSLayer *graphicsLayer; 63 | @property (nonatomic, strong) NSMutableDictionary *symbolGeneratorBlocks; 64 | @property (nonatomic, assign, readwrite) BOOL willClusterAtCurrentScale; 65 | 66 | @property (nonatomic, assign) BOOL loadsAllFeatures; 67 | 68 | @property (nonatomic, assign) BOOL mapViewLoaded; 69 | @property (nonatomic, assign) BOOL dataLoaded; 70 | @property (nonatomic, assign) BOOL initialized; 71 | 72 | @property (nonatomic, strong) NSNumber *maxZoomLevel; 73 | 74 | @property (nonatomic, strong) AGSGDBSyncTask *syncTask; 75 | @property (nonatomic, assign) NSUInteger maxRecordCount; 76 | @property (nonatomic, strong) NSMutableSet *openQueries; 77 | @property (nonatomic, assign) NSUInteger featureCountToLoad; 78 | @property (nonatomic, assign) NSUInteger totalFeatureCountToLoad; 79 | @property (nonatomic, strong) NSMutableArray *allFeatures; 80 | 81 | @property (nonatomic, strong) NSMutableArray *clusteringGrids; 82 | @property (nonatomic, strong) NSDate *clusteringStartTime; 83 | @end 84 | 85 | 86 | #pragma mark - Synthesizers 87 | @implementation AGSClusterLayer 88 | @synthesize willClusterAtCurrentScale = _willClusterAtCurrentScale; 89 | @synthesize initialized = _initialized; 90 | 91 | #pragma mark - Convenience Constructors 92 | 93 | +(AGSClusterLayer *)clusterLayerForFeatureLayer:(AGSFeatureLayer *)featureLayer { 94 | return [[AGSClusterLayer alloc] initWithFeatureLayer:featureLayer]; 95 | } 96 | 97 | +(AGSClusterLayer *)clusterLayerForFeatureLayer:(AGSFeatureLayer *)featureLayer 98 | usingClusterSymbolBlock:(AGSClusterSymbolGeneratorBlock)clusterBlock 99 | coverageSymbolBlock:(AGSClusterSymbolGeneratorBlock)coverageBlock { 100 | AGSClusterLayer *clusterLayer = [AGSClusterLayer clusterLayerForFeatureLayer:featureLayer]; 101 | if (clusterBlock) [clusterLayer.symbolGeneratorBlocks setObject:clusterBlock forKey:kClusterRenderBlockParameterKey]; 102 | if (coverageBlock) [clusterLayer.symbolGeneratorBlocks setObject:coverageBlock forKey:kCoverageRenderBlockParameterKey]; 103 | return clusterLayer; 104 | } 105 | 106 | +(AGSClusterLayer *)clusterLayerForGraphicsLayer:(AGSGraphicsLayer *)graphicsLayer { 107 | return [[AGSClusterLayer alloc] initWithGraphicsLayer:graphicsLayer]; 108 | } 109 | 110 | +(AGSClusterLayer *)clusterLayerForGraphicsLayer:(AGSGraphicsLayer *)graphicsLayer 111 | usingClusterSymbolBlock:(AGSClusterSymbolGeneratorBlock)clusterBlock 112 | coverageSymbolBlock:(AGSClusterSymbolGeneratorBlock)coverageBlock { 113 | AGSClusterLayer *clusterLayer = [AGSClusterLayer clusterLayerForGraphicsLayer:graphicsLayer]; 114 | if (clusterBlock) [clusterLayer.symbolGeneratorBlocks setObject:clusterBlock forKey:kClusterRenderBlockParameterKey]; 115 | if (coverageBlock) [clusterLayer.symbolGeneratorBlocks setObject:coverageBlock forKey:kCoverageRenderBlockParameterKey]; 116 | return clusterLayer; 117 | } 118 | 119 | +(AGSClusterLayer *)clusterLayerForFeatureTableLayer:(AGSFeatureTableLayer *)featureTableLayer { 120 | return [[AGSClusterLayer alloc] initWithFeatureTableLayer:featureTableLayer]; 121 | } 122 | 123 | 124 | #pragma mark - Initializers 125 | -(id)init { 126 | self = [super init]; 127 | if (self) { 128 | 129 | self.clusteringEnabled = YES; 130 | self.loadsAllFeatures = YES; 131 | self.calloutDelegate = self; 132 | self.minClusterCount = kDefaultMinClusterCount; 133 | self.symbolGeneratorBlocks = [NSMutableDictionary dictionary]; 134 | self.grids = [NSMutableDictionary dictionary]; 135 | self.lodData = [self defaultLodData]; 136 | self.openQueries = [NSMutableSet set]; 137 | self.allFeatures = [NSMutableArray array]; 138 | [self parseLodData]; 139 | } 140 | return self; 141 | } 142 | 143 | -(id)initWithGraphicsLayer:(AGSGraphicsLayer *)graphicsLayer { 144 | self = [self init]; 145 | if (self) { 146 | self.graphicsLayer = graphicsLayer; 147 | dispatch_async(dispatch_get_main_queue(), ^{ 148 | [self featureLayerLoaded:nil]; 149 | }); 150 | } 151 | return self; 152 | } 153 | 154 | -(id)initWithFeatureTableLayer:(AGSFeatureTableLayer *)featureTableLayer { 155 | self = [self init]; 156 | if (self) { 157 | self.graphicsLayer = featureTableLayer; 158 | dispatch_async(dispatch_get_main_queue(), ^{ 159 | [self featureLayerLoaded:nil]; 160 | }); 161 | } 162 | return self; 163 | } 164 | 165 | -(id)initWithFeatureLayer:(AGSFeatureLayer *)featureLayer { 166 | self = [self init]; 167 | if (self) { 168 | self.graphicsLayer = featureLayer; 169 | [[NSNotificationCenter defaultCenter] addObserver:self 170 | selector:@selector(featureLayerLoaded:) 171 | name:AGSLayerDidLoadNotification 172 | object:featureLayer]; 173 | [[NSNotificationCenter defaultCenter] addObserver:self 174 | selector:@selector(featureLayerFailedToLoad:) 175 | name:AGSLayerDidFailToLoadNotification 176 | object:featureLayer]; 177 | } 178 | return self; 179 | } 180 | 181 | #pragma mark - Internal Checks on the Graphics Layer Setup 182 | -(void)setGraphicsLayer:(AGSGraphicsLayer *)graphicsLayer { 183 | _graphicsLayer = graphicsLayer; 184 | 185 | if ([graphicsLayer isMemberOfClass:[AGSGraphicsLayer class]] && 186 | graphicsLayer && graphicsLayer.mapView && !graphicsLayer.mapView.loaded) { 187 | NSLog(@"Warning: The AGSMapView for the source GraphicsLayer is not yet loaded! Your layer will likely not cluster."); 188 | } 189 | } 190 | 191 | #pragma mark - Asynchronous Setup 192 | 193 | -(void)featureLayerLoaded:(NSNotification *)notification { 194 | AGSClusterSymbolGeneratorBlock clusterGenBlock = self.symbolGeneratorBlocks[kClusterRenderBlockParameterKey]; 195 | AGSClusterSymbolGeneratorBlock coverageGenBlock = self.symbolGeneratorBlocks[kCoverageRenderBlockParameterKey]; 196 | self.symbolGeneratorBlocks = nil; 197 | 198 | AGSRenderer *sourceRenderer = nil; 199 | if ([self.graphicsLayer isKindOfClass:[AGSFeatureLayer class]]) { 200 | sourceRenderer = ((AGSFeatureLayer *)self.graphicsLayer).renderer; 201 | } else if ([self.graphicsLayer isKindOfClass:[AGSFeatureTableLayer class]]) { 202 | sourceRenderer = ((AGSFeatureTableLayer *)self.graphicsLayer).renderer; 203 | } else if ([self.graphicsLayer isKindOfClass:[AGSGraphicsLayer class]]) { 204 | sourceRenderer = ((AGSGraphicsLayer *)self.graphicsLayer).renderer; 205 | } 206 | 207 | if (sourceRenderer != nil) { 208 | self.renderer = [[AGSClusterLayerRenderer alloc] initWithRenderer:sourceRenderer 209 | clusterSymbolBlock:clusterGenBlock 210 | coverageSymbolBlock:coverageGenBlock]; 211 | } else { 212 | NSLog(@"Source layer doesn't have a renderer that we could use. What did you pass in?!?"); 213 | } 214 | 215 | self.graphicsLayer.visible = !self.clusteringEnabled; 216 | 217 | if ([self.graphicsLayer isKindOfClass:[AGSFeatureLayer class]]) { 218 | if (self.loadsAllFeatures) { 219 | // Load all the data up-front. 220 | // We are then not dependent on the underlying feature layer for data updates. 221 | 222 | AGSFeatureLayer *featureLayer = (AGSFeatureLayer *)self.graphicsLayer; 223 | self.syncTask = [[AGSGDBSyncTask alloc] initWithURL:featureLayer.URL credential:featureLayer.credential]; 224 | __weak AGSFeatureLayer *weakFL = featureLayer; 225 | __weak AGSGDBSyncTask *weakTask = self.syncTask; 226 | __weak typeof(self) weakSelf = self; 227 | [self.syncTask setLoadCompletion:^(NSError *error) { 228 | if (error) { 229 | NSLog(@"Couldn't load FeatureServiceInfo: %@", error.localizedDescription); 230 | } else { 231 | weakSelf.maxRecordCount = weakTask.featureServiceInfo.maxRecordCount; 232 | if (weakSelf.maxRecordCount == 0) { 233 | weakSelf.maxRecordCount = 1000; 234 | } 235 | weakFL.queryDelegate = weakSelf; 236 | AGSQuery *q = [AGSQuery query]; 237 | q.whereClause = @"1=1"; 238 | [weakFL queryIds:q]; 239 | } 240 | weakSelf.syncTask = nil; 241 | }]; 242 | } else { 243 | // Load as the underlying feature layer updates its data. We build an accumulative copy of all the data. 244 | self.graphicsLayer.opacity = 0.5; 245 | 246 | [[NSNotificationCenter defaultCenter] addObserver:self 247 | selector:@selector(featuresLoaded:) 248 | name:AGSFeatureLayerDidLoadFeaturesNotification 249 | object:self.graphicsLayer]; 250 | } 251 | 252 | [self createBlankGridsForLods]; 253 | } else if ([self.graphicsLayer isKindOfClass:[AGSFeatureTableLayer class]]) { 254 | AGSFeatureTableLayer *layer = (AGSFeatureTableLayer *)self.graphicsLayer; 255 | AGSFeatureTable *table = layer.table; 256 | AGSQuery *q = [AGSQuery query]; 257 | q.whereClause = layer.definitionExpression; 258 | [table queryResultsWithParameters:q completion:^(NSArray *results, NSError *error) { 259 | [self addOrUpdateFeatures:results]; 260 | [self createBlankGridsForLods]; 261 | [self dataLoadCompleted]; 262 | }]; 263 | } else if ([self.graphicsLayer isKindOfClass:[AGSGraphicsLayer class]]) { 264 | AGSGraphicsLayer *sourceLayer = (AGSGraphicsLayer *)self.graphicsLayer; 265 | [self addOrUpdateFeatures:sourceLayer.graphics]; 266 | [self createBlankGridsForLods]; 267 | [self dataLoadCompleted]; 268 | } 269 | } 270 | 271 | -(void)featureLayerFailedToLoad:(NSNotification *)notification { 272 | AGSFeatureLayer *featureLayer = (AGSFeatureLayer *)self.graphicsLayer; 273 | NSLog(@"FeatureLayer failed to load: %@", [featureLayer.error localizedDescription]); 274 | 275 | [[NSNotificationCenter defaultCenter] postNotificationName:AGSClusterLayerDataLoadingErrorNotification 276 | object:self 277 | userInfo:@{kClusterLayerDataLoadingErrorNotification_Key_Error: featureLayer.error}]; 278 | } 279 | 280 | -(void)featureLayer:(AGSFeatureLayer *)featureLayer operation:(NSOperation *)op didQueryObjectIdsWithResults:(NSArray *)objectIds { 281 | NSUInteger count = 0; 282 | NSUInteger pageCount = 0; 283 | NSMutableArray *pagesToLoad = [NSMutableArray array]; 284 | NSMutableArray *currentObjectIds = nil; 285 | for (NSNumber *oid in objectIds) { 286 | if (count % self.maxRecordCount == 0) { 287 | currentObjectIds = [NSMutableArray array]; 288 | [pagesToLoad addObject:currentObjectIds]; 289 | pageCount++; 290 | } 291 | [currentObjectIds addObject:oid]; 292 | count++; 293 | } 294 | self.featureCountToLoad = count; 295 | self.totalFeatureCountToLoad = count; 296 | 297 | for (NSArray *featureIds in pagesToLoad) { 298 | AGSQuery *q = [AGSQuery query]; 299 | q.objectIds = featureIds; 300 | q.returnGeometry = YES; 301 | q.outSpatialReference = self.mapView.spatialReference; 302 | NSOperation *queryOp = [featureLayer queryFeatures:q]; 303 | objc_setAssociatedObject(queryOp, kBatchQueryOperationQueryKey, q, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 304 | [self.openQueries addObject:queryOp]; 305 | } 306 | } 307 | 308 | -(void)featureLayer:(AGSFeatureLayer *)featureLayer operation:(NSOperation *)op didFailQueryObjectIdsWithError:(NSError *)error { 309 | NSLog(@"Could not get object IDs to load, %@",[error localizedDescription]); 310 | } 311 | 312 | -(void)featureLayer:(AGSFeatureLayer *)featureLayer operation:(NSOperation *)op didQueryFeaturesWithFeatureSet:(AGSFeatureSet *)featureSet { 313 | [self addOrUpdateFeatures:featureSet.features]; 314 | [self continueLoadingFeatures:op]; 315 | } 316 | 317 | -(void)featureLayer:(AGSFeatureLayer *)featureLayer operation:(NSOperation *)op didFailQueryFeaturesWithError:(NSError *)error { 318 | NSLog(@"Could not load features, %@",[error localizedDescription]); 319 | [self continueLoadingFeatures:op]; 320 | } 321 | 322 | -(void)continueLoadingFeatures:(NSOperation *)op { 323 | AGSQuery *q = objc_getAssociatedObject(op, kBatchQueryOperationQueryKey); 324 | [self.openQueries removeObject:op]; 325 | self.featureCountToLoad -= q.objectIds.count; 326 | NSUInteger featuresLoaded = self.totalFeatureCountToLoad - self.featureCountToLoad; 327 | double percentComplete = 100.0f * featuresLoaded / self.totalFeatureCountToLoad; 328 | 329 | [[NSNotificationCenter defaultCenter] postNotificationName:kClusterLayerDataLoadingNotification 330 | object:self 331 | userInfo:@{kClusterLayerDataLoadingNotification_Key_TotalRecords: @(self.totalFeatureCountToLoad), 332 | kClusterLayerDataLoadingNotification_Key_LoadedCount: @(featuresLoaded), 333 | kClusterLayerDataLoadingNotification_Key_PercentComplete: @(percentComplete)}]; 334 | 335 | if (self.openQueries.count == 0) { 336 | [self dataLoadCompleted]; 337 | } 338 | } 339 | 340 | -(void)dataLoadCompleted { 341 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ 342 | self.dataLoaded = YES; 343 | [self rebuildClusterGrid]; 344 | dispatch_async(dispatch_get_main_queue(), ^{ 345 | [self refresh]; 346 | }); 347 | }); 348 | } 349 | 350 | -(void)addOrUpdateFeatures:(NSArray *)features { 351 | [self.allFeatures addObjectsFromArray:features]; 352 | } 353 | 354 | #pragma mark - Update Hooks 355 | -(void)featuresLoaded:(NSNotification *)notification { 356 | if ([notification.object isKindOfClass:[AGSFeatureLayer class]]) { 357 | AGSFeatureLayer *featureLayer = notification.object; 358 | [self addOrUpdateFeatures:featureLayer.graphics]; 359 | self.dataLoaded = YES; 360 | [self rebuildClusterGrid]; 361 | [self refresh]; 362 | } 363 | } 364 | 365 | -(NSString *)objectIdField { 366 | if ([self.graphicsLayer isKindOfClass:[AGSFeatureLayer class]]) { 367 | AGSFeatureLayer *featureLayer = (AGSFeatureLayer *)self.graphicsLayer; 368 | return featureLayer.objectIdField; 369 | } else { 370 | return @"FID"; 371 | } 372 | } 373 | 374 | -(void)mapDidUpdate:(AGSMapUpdateType)updateType 375 | { 376 | if (updateType == AGSMapUpdateTypeSpatialExtent) { 377 | self.mapViewLoaded = self.mapView.loaded; 378 | self.willClusterAtCurrentScale = self.mapView.mapScale > self.minScaleForClustering; 379 | if (self.initialized) { 380 | self.gridForCurrentScale = [self findGridForCurrentScale]; 381 | } 382 | } 383 | [super mapDidUpdate:updateType]; 384 | } 385 | 386 | #pragma mark - Map/Data Load tracking 387 | -(BOOL)initialized { 388 | if (!_initialized) { 389 | [self initializeIfPossible]; 390 | } 391 | return _initialized; 392 | } 393 | 394 | -(void)setInitialized:(BOOL)initialized { 395 | _initialized = initialized; 396 | if (_initialized) { 397 | self.gridForCurrentScale = [self findGridForCurrentScale]; 398 | } 399 | } 400 | 401 | -(void)initializeIfPossible { 402 | if (!_initialized) { 403 | if (self.mapViewLoaded && self.dataLoaded) { 404 | self.initialized = YES; 405 | } 406 | } 407 | } 408 | 409 | #pragma mark - Current Grid 410 | -(void)setGridForCurrentScale:(AGSClusterGrid *)gridForCurrentScale { 411 | if (_gridForCurrentScale != gridForCurrentScale) { 412 | _gridForCurrentScale = gridForCurrentScale; 413 | [self refresh]; 414 | } 415 | } 416 | 417 | #pragma mark - Helpers 418 | -(NSUInteger)findZoomLevelForScale:(double)scale { 419 | NSNumber *lastZoomLevel = nil; 420 | for (NSNumber *zoomLevel in [self.sortedGridKeys reverseObjectEnumerator]) { 421 | NSDictionary *d = self.grids[zoomLevel]; 422 | double scaleForZoom = [d[kLODLevelScale] doubleValue]; 423 | if (scale <= scaleForZoom) { 424 | // This is our ZoomLevel 425 | return [zoomLevel unsignedIntegerValue]; 426 | } 427 | lastZoomLevel = zoomLevel; 428 | } 429 | return [lastZoomLevel unsignedIntegerValue]; 430 | } 431 | 432 | -(NSUInteger)cellSizeForScale:(double)scale { 433 | return [self.grids[@([self findZoomLevelForScale:scale])][kLODLevelCellSize] unsignedIntegerValue]; 434 | } 435 | 436 | #pragma mark - Property Overrides 437 | -(void)setMapView:(AGSMapView *)mapView { 438 | [super setMapView:mapView]; 439 | self.mapViewLoaded = self.mapView.loaded; 440 | } 441 | 442 | #pragma mark - Properties 443 | -(void)setClusteringEnabled:(BOOL)clusteringEnabled { 444 | _clusteringEnabled = clusteringEnabled; 445 | [self refresh]; 446 | } 447 | 448 | -(void)setShowsClusterCoverages:(BOOL)showClusterCoverages { 449 | _showsClusterCoverages = showClusterCoverages; 450 | [self refresh]; 451 | } 452 | 453 | -(void)setWillClusterAtCurrentScale:(BOOL)willClusterAtCurrentScale { 454 | BOOL wasClusteringAtPreviousScale = _willClusterAtCurrentScale; 455 | if (willClusterAtCurrentScale != wasClusteringAtPreviousScale) { 456 | [self willChangeValueForKey:@"willClusterAtCurrentScale"]; 457 | } 458 | _willClusterAtCurrentScale = willClusterAtCurrentScale; 459 | if (willClusterAtCurrentScale != wasClusteringAtPreviousScale) { 460 | [self didChangeValueForKey:@"willClusterAtCurrentScale"]; 461 | [self refresh]; 462 | } 463 | } 464 | 465 | -(BOOL)willClusterAtCurrentScale { 466 | return _willClusterAtCurrentScale; 467 | } 468 | 469 | -(AGSEnvelope *)envelopeForClustersAtZoomLevel:(NSUInteger)zoomLevel { 470 | AGSMutableEnvelope *envelope = nil; 471 | AGSClusterGrid *gridToZoomTo = self.grids[@(zoomLevel)][kLODLevelGrid]; 472 | for (AGSCluster *cluster in gridToZoomTo.clusters) { 473 | AGSGeometry *coverage = cluster.coverageGraphic.geometry; 474 | if (!envelope) { 475 | envelope = [coverage.envelope mutableCopy]; 476 | } else { 477 | [envelope unionWithEnvelope:coverage.envelope]; 478 | } 479 | } 480 | return envelope; 481 | } 482 | 483 | -(AGSClusterGrid *)findGridForCurrentScale { 484 | return self.grids[@([self findZoomLevelForScale:self.mapView.mapScale])][kLODLevelGrid]; 485 | } 486 | 487 | -(AGSClusterGrid *)maxZoomLevelGrid { 488 | return self.grids[self.maxZoomLevel][kLODLevelGrid]; 489 | } 490 | 491 | +(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key { 492 | if ([key isEqualToString:@"willClusterAtCurrentScale"]) { 493 | return NO; 494 | } else { 495 | return [super automaticallyNotifiesObserversForKey:key]; 496 | } 497 | } 498 | 499 | #pragma mark - Layer Refresh 500 | -(void)refresh { 501 | if (!self.initialized) return; 502 | 503 | [self refreshClusters]; 504 | 505 | [super refresh]; 506 | } 507 | 508 | -(void)refreshClusters { 509 | [self removeAllGraphics]; 510 | [self renderClusters]; 511 | } 512 | 513 | -(void)clearClusters { 514 | [self removeAllGraphics]; 515 | [self.maxZoomLevelGrid removeAllItems]; 516 | } 517 | 518 | #pragma mark - Cluster Generation and Display 519 | -(void)rebuildClusterGrid { 520 | self.clusteringStartTime = [NSDate date]; 521 | self.clusteringGrids = [NSMutableArray arrayWithArray:[self.grids.allValues map:^id(id obj) { 522 | return obj[kLODLevelGrid]; 523 | }]]; 524 | 525 | AGSClusterGrid *grid = self.maxZoomLevelGrid; 526 | [grid addItems:self.allFeatures]; 527 | } 528 | 529 | -(void)gridClustered:(NSNotification *)notification { 530 | [self.clusteringGrids removeObject:notification.object]; 531 | 532 | NSUInteger gridsClustered = self.grids.count - self.clusteringGrids.count; 533 | NSTimeInterval clusteringDuration = -[self.clusteringStartTime timeIntervalSinceNow]; 534 | 535 | dispatch_async(dispatch_get_main_queue(), ^{ 536 | [[NSNotificationCenter defaultCenter] postNotificationName:kClusterLayerClusteringNotification 537 | object:self 538 | userInfo:@{ 539 | kClusterLayerClusteringNotification_Key_Duration: @(clusteringDuration), 540 | kClusterLayerClusteringNotification_Key_PercentComplete: @(100*gridsClustered/self.grids.count), 541 | kClusterLayerClusteringNotification_Key_FeatureCount: @(self.allFeatures.count), 542 | kClusterLayerClusteringNotification_Key_TotalZoomLevels: @(self.grids.count), 543 | kClusterLayerClusteringNotification_Key_ZoomLevelsClustered: @(self.grids.count - self.clusteringGrids.count) 544 | }]; 545 | }); 546 | } 547 | 548 | -(void)renderClusters { 549 | NSMutableArray *coverageGraphics = [NSMutableArray array]; 550 | NSMutableArray *clusterGraphics = [NSMutableArray array]; 551 | NSMutableArray *featureGraphics = [NSMutableArray array]; 552 | 553 | for (AGSCluster *cluster in self.gridForCurrentScale.clusters) { 554 | if (self.clusteringEnabled && 555 | self.mapView.mapScale > self.minScaleForClustering && 556 | cluster.featureCount >= self.minClusterCount) { 557 | // Draw as cluster. 558 | if ((self.showsClusterCoverages || cluster.showCoverage) && cluster.features.count > 2) { 559 | // Draw the coverage if need be 560 | AGSGraphic *coverageGraphic = cluster.coverageGraphic; 561 | coverageGraphic.symbol = [self.renderer symbolForFeature:coverageGraphic timeExtent:nil]; 562 | [coverageGraphics addObject:coverageGraphic]; 563 | } 564 | 565 | cluster.symbol = [self.renderer symbolForFeature:cluster timeExtent:nil]; 566 | [clusterGraphics addObject:cluster]; 567 | } else { 568 | // Draw as feature(s). 569 | if ([self.graphicsLayer isKindOfClass:[AGSFeatureTableLayer class]]) { 570 | for (AGSGDBFeature *feature in cluster.features) { 571 | AGSGraphic *g = feature.graphicForGDBFeature; 572 | g.symbol = [self.renderer symbolForFeature:feature timeExtent:nil]; 573 | [featureGraphics addObject:g]; 574 | } 575 | } else { 576 | for (AGSGraphic *feature in cluster.features) { 577 | feature.symbol = [self.renderer symbolForFeature:feature timeExtent:nil]; 578 | [featureGraphics addObject:feature]; 579 | } 580 | } 581 | } 582 | } 583 | 584 | [self addGraphics:coverageGraphics]; 585 | [self addGraphics:clusterGraphics]; 586 | [self addGraphics:featureGraphics]; 587 | } 588 | 589 | #pragma mark - UI/Callouts 590 | -(BOOL)callout:(AGSCallout *)callout willShowForFeature:(id)feature layer:(AGSLayer *)layer mapPoint:(AGSPoint *)mapPoint { 591 | [self handleCalloutChange:callout]; 592 | 593 | AGSCluster *cluster = [feature isMemberOfClass:[AGSCluster class]]?(AGSCluster *)feature:((AGSGraphic *)feature).owningCluster; 594 | 595 | if (cluster) { 596 | NSLog(@"%@ :: %d", cluster, cluster.featureCount); 597 | callout.title = @"Cluster"; 598 | callout.detail = [NSString stringWithFormat:@"Cluster contains %d features", cluster.features.count]; 599 | callout.accessoryButtonHidden = YES; 600 | callout.delegate = self; 601 | cluster.showCoverage = YES; 602 | [self refresh]; 603 | } else { 604 | callout.title = @"Ordinary feature"; 605 | callout.detail = @"Might have a bunch of attributes on it"; 606 | callout.accessoryButtonHidden = NO; 607 | } 608 | 609 | return YES; 610 | } 611 | 612 | -(void)calloutWillDismiss:(AGSCallout *)callout { 613 | [self handleCalloutChange:callout]; 614 | } 615 | 616 | -(void)handleCalloutChange:(AGSCallout *)callout { 617 | if (callout.representedFeature) { 618 | id feature = callout.representedFeature; 619 | AGSCluster *cluster = [feature isKindOfClass:[AGSCluster class]]?(AGSCluster *)feature:((AGSGraphic *)feature).owningCluster; 620 | if (cluster) { 621 | cluster.showCoverage = NO; 622 | [self refresh]; 623 | } 624 | } 625 | } 626 | 627 | #pragma mark - Build All Grids 628 | -(void)createBlankGridsForLods { 629 | CGFloat cellSizeInInches = 0.25; 630 | AGSUnits mapUnits = self.mapViewLoaded?AGSUnitsFromSpatialReference(self.mapView.spatialReference):AGSUnitsMeters; 631 | AGSUnits screenUnits = AGSUnitsInches; 632 | double cellSizeInMapUnits = AGSUnitsToUnits(cellSizeInInches, screenUnits, mapUnits); 633 | 634 | AGSClusterGrid *prevClusterGrid = nil; 635 | for (NSNumber *zoomLevel in self.sortedGridKeys) { 636 | NSMutableDictionary *d = self.grids[zoomLevel]; 637 | double scale = [d[kLODLevelScale] doubleValue]; 638 | NSUInteger cellSize = floor(cellSizeInMapUnits * scale); 639 | d[kLODLevelCellSize] = @(cellSize); 640 | 641 | AGSClusterGrid *gridForZoomLevel = [[AGSClusterGrid alloc] initWithCellSize:cellSize forClusterLayer:self]; 642 | gridForZoomLevel.zoomLevel = zoomLevel; 643 | 644 | prevClusterGrid.gridForNextZoomLevel = gridForZoomLevel; 645 | gridForZoomLevel.gridForPrevZoomLevel = prevClusterGrid; 646 | 647 | d[kLODLevelGrid] = gridForZoomLevel; 648 | 649 | [[NSNotificationCenter defaultCenter] addObserver:self 650 | selector:@selector(gridClustered:) 651 | name:AGSClusterGridClusteredNotification 652 | object:gridForZoomLevel]; 653 | 654 | prevClusterGrid = gridForZoomLevel; 655 | self.maxZoomLevel = zoomLevel; 656 | } 657 | } 658 | 659 | #pragma mark - Demo LODs 660 | -(void)parseLodData { 661 | for (NSDictionary *lodInfo in self.lodData) { 662 | [self.grids setObject:[NSMutableDictionary dictionaryWithDictionary:lodInfo] 663 | forKey:lodInfo[kLODLevelZoomLevel]]; 664 | } 665 | self.sortedGridKeys = [self.grids.allKeys sortedArrayUsingComparator:^NSComparisonResult(id obj1, id obj2) { 666 | return [((NSNumber *)obj1) compare:obj2]; 667 | }]; 668 | } 669 | 670 | -(NSArray *)defaultLodData { 671 | return @[ 672 | @{ 673 | @"level": @(0), 674 | @"resolution": @(156543.03392800014), 675 | @"scale": @(591657527.591555) 676 | }, 677 | @{ 678 | @"level": @(1), 679 | @"resolution": @(78271.51696399994), 680 | @"scale": @(295828763.795777) 681 | }, 682 | @{ 683 | @"level": @(2), 684 | @"resolution": @(39135.75848200009), 685 | @"scale": @(147914381.897889) 686 | }, 687 | @{ 688 | @"level": @(3), 689 | @"resolution": @(19567.87924099992), 690 | @"scale": @(73957190.948944) 691 | }, 692 | @{ 693 | @"level": @(4), 694 | @"resolution": @(9783.93962049996), 695 | @"scale": @(36978595.474472) 696 | }, 697 | @{ 698 | @"level": @(5), 699 | @"resolution": @(4891.96981024998), 700 | @"scale": @(18489297.737236) 701 | }, 702 | @{ 703 | @"level": @(6), 704 | @"resolution": @(2445.98490512499), 705 | @"scale": @(9244648.868618) 706 | }, 707 | @{ 708 | @"level": @(7), 709 | @"resolution": @(1222.992452562495), 710 | @"scale": @(4622324.434309) 711 | }, 712 | @{ 713 | @"level": @(8), 714 | @"resolution": @(611.4962262813797), 715 | @"scale": @(2311162.217155) 716 | }, 717 | @{ 718 | @"level": @(9), 719 | @"resolution": @(305.74811314055756), 720 | @"scale": @(1155581.108577) 721 | }, 722 | @{ 723 | @"level": @(10), 724 | @"resolution": @(152.87405657041106), 725 | @"scale": @(577790.554289) 726 | }, 727 | @{ 728 | @"level": @(11), 729 | @"resolution": @(76.43702828507324), 730 | @"scale": @(288895.277144) 731 | }, 732 | @{ 733 | @"level": @(12), 734 | @"resolution": @(38.21851414253662), 735 | @"scale": @(144447.638572) 736 | }, 737 | @{ 738 | @"level": @(13), 739 | @"resolution": @(19.10925707126831), 740 | @"scale": @(72223.819286) 741 | }, 742 | @{ 743 | @"level": @(14), 744 | @"resolution": @(9.554628535634155), 745 | @"scale": @(36111.909643) 746 | }, 747 | @{ 748 | @"level": @(15), 749 | @"resolution": @(4.77731426794937), 750 | @"scale": @(18055.954822) 751 | }, 752 | @{ 753 | @"level": @(16), 754 | @"resolution": @(2.388657133974685), 755 | @"scale": @(9027.977411) 756 | }, 757 | @{ 758 | @"level": @(17), 759 | @"resolution": @(1.1943285668550503), 760 | @"scale": @(4513.988705) 761 | }, 762 | @{ 763 | @"level": @(18), 764 | @"resolution": @(0.5971642835598172), 765 | @"scale": @(2256.994353) 766 | }, 767 | @{ 768 | @"level": @(19), 769 | @"resolution": @(0.29858214164761665), 770 | @"scale": @(1128.497176) 771 | } 772 | ]; 773 | } 774 | @end -------------------------------------------------------------------------------- /AGSClusterLayer/AGSClusterLayerRenderer.h: -------------------------------------------------------------------------------- 1 | // 2 | // AGSClusterLayerRenderer.h 3 | // Cluster Layer 4 | // 5 | // Created by Nicholas Furness on 3/25/14. 6 | // Copyright (c) 2014 ESRI. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @class AGSCluster; 12 | 13 | // A block with this signature will be passed a cluster and is expected to return an AGSSymbol. 14 | typedef AGSSymbol*(^AGSClusterSymbolGeneratorBlock)(AGSCluster *); 15 | 16 | @interface AGSClusterLayerRenderer : AGSSimpleRenderer 17 | 18 | //Initialize an AGSClusterLayerRenderer object with the provided renderer 19 | -(id)initWithRenderer:(AGSRenderer *)originalRenderer; 20 | 21 | //Initialize an AGSClusterLayerRenderer object with the provided renderer, cluster symbol block and coverage symbol block 22 | -(id)initWithRenderer:(AGSRenderer *)originalRenderer 23 | clusterSymbolBlock:(AGSClusterSymbolGeneratorBlock)clusterSymbolGenerator 24 | coverageSymbolBlock:(AGSClusterSymbolGeneratorBlock)coverageSymbolGenerator; 25 | 26 | 27 | 28 | @end -------------------------------------------------------------------------------- /AGSClusterLayer/AGSClusterLayerRenderer.m: -------------------------------------------------------------------------------- 1 | // 2 | // AGSClusterLayerRenderer.m 3 | // Cluster Layer 4 | // 5 | // Created by Nicholas Furness on 3/25/14. 6 | // Copyright (c) 2014 ESRI. All rights reserved. 7 | // 8 | 9 | #import "AGSClusterLayerRenderer.h" 10 | #import "AGSCluster.h" 11 | #import "Common_int.h" 12 | #import "AGSGraphic+AGSClustering.h" 13 | #import 14 | 15 | @interface AGSClusterLayerRenderer () 16 | @property (nonatomic, weak) AGSRenderer *originalRenderer; 17 | @property (nonatomic, copy) AGSClusterSymbolGeneratorBlock clusterGenBlock; 18 | @property (nonatomic, copy) AGSClusterSymbolGeneratorBlock coverageGenBlock; 19 | @end 20 | 21 | @implementation AGSClusterLayerRenderer 22 | -(id)initWithRenderer:(AGSRenderer *)originalRenderer { 23 | self = [self init]; 24 | if (self) { 25 | self.originalRenderer = originalRenderer; 26 | } 27 | return self; 28 | } 29 | 30 | -(id)initWithRenderer:(AGSRenderer *)originalRenderer 31 | clusterSymbolBlock:(AGSClusterSymbolGeneratorBlock)clusterSymbolGenerator 32 | coverageSymbolBlock:(AGSClusterSymbolGeneratorBlock)coverageSymbolGenerator 33 | { 34 | self = [self initWithRenderer:originalRenderer]; 35 | if (self) { 36 | if (clusterSymbolGenerator) { 37 | self.clusterGenBlock = clusterSymbolGenerator; 38 | } 39 | if (coverageSymbolGenerator) { 40 | self.coverageGenBlock = coverageSymbolGenerator; 41 | } 42 | } 43 | return self; 44 | } 45 | 46 | -(id)init { 47 | self = [super init]; 48 | if (self) { 49 | self.clusterGenBlock = ^(AGSCluster *cluster) { 50 | AGSCompositeSymbol *s = [AGSCompositeSymbol compositeSymbol]; 51 | 52 | NSUInteger innerSize = 24; 53 | NSUInteger borderSize = 6; 54 | NSUInteger fontSize = 14; 55 | 56 | UIColor *smallClusterColor = [UIColor colorWithRed:0.000 green:0.491 blue:0.000 alpha:1.000]; 57 | UIColor *mediumClusterColor = [UIColor colorWithRed:0.838 green:0.500 blue:0.000 alpha:1.000]; 58 | UIColor *largeClusterColor = [UIColor colorWithRed:0.615 green:0.178 blue:0.550 alpha:1.000]; 59 | UIColor *c = cluster.featureCount < 10?smallClusterColor:(cluster.featureCount < 100?mediumClusterColor:largeClusterColor); 60 | 61 | AGSSimpleMarkerSymbol *backgroundSymbol1 = [AGSSimpleMarkerSymbol simpleMarkerSymbolWithColor:[c colorWithAlphaComponent:0.7]]; 62 | backgroundSymbol1.outline = nil; 63 | backgroundSymbol1.size = CGSizeMake(innerSize + borderSize, innerSize + borderSize); 64 | 65 | AGSSimpleMarkerSymbol *backgroundSymbol2 = [AGSSimpleMarkerSymbol simpleMarkerSymbolWithColor:[[UIColor whiteColor] colorWithAlphaComponent:0.7]]; 66 | backgroundSymbol2.outline = nil; 67 | backgroundSymbol2.size = CGSizeMake(innerSize + (borderSize/2), innerSize + (borderSize/2)); 68 | 69 | AGSSimpleMarkerSymbol *backgroundSymbol3 = [AGSSimpleMarkerSymbol simpleMarkerSymbolWithColor:[c colorWithAlphaComponent:0.7]]; 70 | backgroundSymbol3.outline = nil; 71 | backgroundSymbol3.size = CGSizeMake(innerSize, innerSize); 72 | 73 | if (cluster.featureCount > 99) fontSize = fontSize * 0.8; 74 | AGSTextSymbol *countSymbol = [AGSTextSymbol textSymbolWithText:[NSString stringWithFormat:@"%d", cluster.featureCount] 75 | color:[UIColor whiteColor]]; 76 | countSymbol.fontSize = fontSize; 77 | [s addSymbol:backgroundSymbol1]; 78 | [s addSymbol:backgroundSymbol2]; 79 | [s addSymbol:backgroundSymbol3]; 80 | [s addSymbol:countSymbol]; 81 | 82 | return s; 83 | }; 84 | self.coverageGenBlock = ^(AGSCluster *cluster) { 85 | return [AGSSimpleFillSymbol simpleFillSymbolWithColor:[[UIColor orangeColor] colorWithAlphaComponent:0.3] 86 | outlineColor:[[UIColor orangeColor] colorWithAlphaComponent:0.7]]; 87 | }; 88 | } 89 | return self; 90 | } 91 | 92 | -(AGSSymbol *)symbolForFeature:(id)feature timeExtent:(AGSTimeExtent *)timeExtent { 93 | if ([feature isKindOfClass:[AGSCluster class]]) { 94 | return self.clusterGenBlock((AGSCluster *)feature); 95 | } 96 | 97 | AGSCluster *cluster = ((AGSGraphic *)feature).owningCluster; 98 | if (cluster != nil && 99 | [feature.geometry isKindOfClass:[AGSPolygon class]]) { 100 | return self.coverageGenBlock(cluster); 101 | } 102 | 103 | return [self.originalRenderer symbolForFeature:feature timeExtent:timeExtent]; 104 | } 105 | @end 106 | -------------------------------------------------------------------------------- /AGSClusterLayer/AGSClustering.h: -------------------------------------------------------------------------------- 1 | // 2 | // AGSClustering.h 3 | // Cluster Layer 4 | // 5 | // Created by Nicholas Furness on 4/10/14. 6 | // Copyright (c) 2014 ESRI. All rights reserved. 7 | // 8 | 9 | #import "AGSClusterLayer.h" 10 | #import "AGSClusterLayerRenderer.h" 11 | #import "AGSCluster.h" 12 | 13 | #ifndef ClusterLayerSample_AGSCL_h 14 | #define ClusterLayerSample_AGSCL_h 15 | 16 | #pragma mark - Notifications 17 | 18 | // AGSClusterLayerDataLoadingProgressNotification is posted periodically during data load. 19 | extern NSString * const AGSClusterLayerDataLoadingProgressNotification; 20 | extern NSString * const AGSClusterLayerDataLoadingProgressNotification_UserInfo_PercentComplete; 21 | extern NSString * const AGSClusterLayerDataLoadingProgressNotification_UserInfo_TotalRecordsToLoad; 22 | extern NSString * const AGSClusterLayerDataLoadingProgressNotification_UserInfo_RecordsLoaded; 23 | 24 | // AGSClusterLayerDataLoadingErrorNotification is posted if data fails to load. UserInfo contains an NSError object. 25 | extern NSString * const AGSClusterLayerDataLoadingErrorNotification; 26 | extern NSString * const AGSClusterLayerDataLoadingErrorNotification_UserInfo_Error; 27 | 28 | // AGSClusterLayerClusteringProgressNotification is posted each time a zoom level has clustered. When all zoom levels 29 | // have been clustered, the percentage will be 100. 30 | extern NSString * const AGSClusterLayerClusteringProgressNotification; 31 | extern NSString * const AGSClusterLayerClusteringProgressNotification_UserInfo_PercentComplete; 32 | extern NSString * const AGSClusterLayerClusteringProgressNotification_UserInfo_TotalZoomLevels; 33 | extern NSString * const AGSClusterLayerClusteringProgressNotification_UserInfo_CompletedZoomLevels; 34 | extern NSString * const AGSClusterLayerClusteringProgressNotification_UserInfo_FeatureCount; 35 | extern NSString * const AGSClusterLayerClusteringProgressNotification_UserInfo_Duration; 36 | 37 | // AGSClusterGridClusteredNotification is abstracted by the above AGSClusterLayerClusteringProgressNotification 38 | // It can be listened to, but exposes internal workings that are subject to change. Use at your own risk. 39 | extern NSString * const AGSClusterGridClusteringNotification; 40 | extern NSString * const AGSClusterGridClusteredNotification; 41 | 42 | #pragma mark - Protocols 43 | @protocol AGSClusterGridProvider 44 | @required 45 | @property (nonatomic, strong, readonly) NSArray *clusters; 46 | -(void)addItems:(NSArray *)items; 47 | -(void)removeAllItems; 48 | @end 49 | 50 | @protocol AGSZoomLevelClusterGridProvider 51 | @required 52 | @property (nonatomic, strong) NSNumber *zoomLevel; 53 | @property (nonatomic, strong) id gridForNextZoomLevel; 54 | @property (nonatomic, strong) id gridForPrevZoomLevel; 55 | @end 56 | 57 | #endif 58 | -------------------------------------------------------------------------------- /AGSClusterLayer/AGSGDBFeature+AGSClustering.h: -------------------------------------------------------------------------------- 1 | // 2 | // AGSGraphic+AGSClustering.h 3 | // Cluster Layer 4 | // 5 | // Created by Nicholas Furness on 3/28/14. 6 | // Copyright (c) 2014 ESRI. All rights reserved. 7 | // 8 | 9 | #import 10 | @class AGSCluster; 11 | 12 | @interface AGSGDBFeature (AGSClustering) 13 | 14 | //Determines whether the graphic is a cluster 15 | @property (nonatomic, readonly) BOOL isCluster; 16 | @property (nonatomic, readonly) BOOL isClusterCoverage; 17 | @property (nonatomic, readonly) AGSCluster *owningCluster; 18 | 19 | // Declare an attribute on the graphic that is an Unsigned Int. 20 | // Only set this if there is no appropriate "FID" attribute on the graphic. 21 | // It is only necessary to set this on Graphics in an AGSGraphicsLayer. Features in an 22 | // AGSFeatureLayer will automatically determine the right ID field to use. 23 | @property (nonatomic, strong) NSString *idAttributeName; 24 | 25 | -(id)clusterItemKey; 26 | 27 | @property (nonatomic, readonly) AGSGraphic *graphicForGDBFeature; 28 | @end 29 | -------------------------------------------------------------------------------- /AGSClusterLayer/AGSGDBFeature+AGSClustering.m: -------------------------------------------------------------------------------- 1 | // 2 | // AGSGraphic+AGSClustering.m 3 | // Cluster Layer 4 | // 5 | // Created by Nicholas Furness on 3/28/14. 6 | // Copyright (c) 2014 ESRI. All rights reserved. 7 | // 8 | 9 | #import "AGSGDBFeature+AGSClustering.h" 10 | #import 11 | #import "AGSCluster.h" 12 | #import "Common_int.h" 13 | 14 | #define kClusterGraphicForGDBFeatureKey @"__clusterGDBFeatureKey" 15 | 16 | @implementation AGSGDBFeature (AGSClustering) 17 | -(BOOL)isCluster { 18 | return NO; 19 | } 20 | 21 | -(BOOL)isClusterCoverage { 22 | return self.owningCluster != nil; 23 | } 24 | 25 | -(AGSCluster *)owningCluster { 26 | return objc_getAssociatedObject(self, kClusterPayloadKey); 27 | } 28 | 29 | -(NSString *)idAttributeName { 30 | return objc_getAssociatedObject(self, kClusterGraphicCustomAttributeKey); 31 | } 32 | 33 | -(AGSGraphic *)graphicForGDBFeature { 34 | AGSGraphic *g = objc_getAssociatedObject(self, kClusterGraphicForGDBFeatureKey); 35 | if (g == nil) { 36 | g = [AGSGraphic graphicWithFeature:self]; 37 | objc_setAssociatedObject(self, kClusterGraphicForGDBFeatureKey, g, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 38 | } 39 | return g; 40 | } 41 | 42 | -(void)setIdAttributeName:(NSString *)idAttributeName { 43 | objc_setAssociatedObject(self, kClusterGraphicCustomAttributeKey, idAttributeName, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 44 | } 45 | 46 | -(id)clusterItemKey { 47 | 48 | static NSString *oidFieldName = @"FID"; 49 | 50 | long long result = self.featureId; 51 | if (result == 0) { 52 | result = self.objectID; 53 | } 54 | 55 | if (result == 0) { 56 | if (self.idAttributeName) { 57 | @try { 58 | NSNumber *oid = [self attributeForKey:self.idAttributeName]; 59 | if (oid) { 60 | result = oid.unsignedIntegerValue; 61 | } 62 | } 63 | @catch (NSException *exception) { 64 | NSLog(@"If you set the 'idAttributeName' property, you MUST populate the AGSGDBFeature's attribute with an Unsigned Int > 0"); 65 | } 66 | } else { 67 | // No id, but we can get a OID field 68 | oidFieldName = ((AGSGDBFeatureTable *)self.table).objectIdField; 69 | NSNumber *oid = [self attributeForKey:oidFieldName]; 70 | if (oid) { 71 | result = oid.unsignedIntegerValue; 72 | } else { 73 | NSLog(@"Cannot find feature OID!"); 74 | } 75 | } 76 | } 77 | 78 | if (result == 0) { 79 | 80 | // If we could not recover, let's say so. 81 | NSLog(@"Feature ID 0!!"); 82 | UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Feature ID 0!!" 83 | message:nil 84 | delegate:nil 85 | cancelButtonTitle:@"OK" 86 | otherButtonTitles:nil]; 87 | [alert show]; 88 | } 89 | 90 | return [NSString stringWithFormat:@"f%lld", result]; 91 | } 92 | @end 93 | -------------------------------------------------------------------------------- /AGSClusterLayer/AGSGraphic+AGSClustering.h: -------------------------------------------------------------------------------- 1 | // 2 | // AGSGraphic+AGSClustering.h 3 | // Cluster Layer 4 | // 5 | // Created by Nicholas Furness on 3/28/14. 6 | // Copyright (c) 2014 ESRI. All rights reserved. 7 | // 8 | 9 | #import 10 | @class AGSCluster; 11 | 12 | @interface AGSGraphic (AGSClustering) 13 | 14 | //Determines whether the graphic is a cluster 15 | @property (nonatomic, readonly) BOOL isCluster; 16 | @property (nonatomic, readonly) BOOL isClusterCoverage; 17 | @property (nonatomic, readonly) AGSCluster *owningCluster; 18 | 19 | // Declare an attribute on the graphic that is an Unsigned Int. 20 | // Only set this if there is no appropriate "FID" attribute on the graphic. 21 | // It is only necessary to set this on Graphics in an AGSGraphicsLayer. Features in an 22 | // AGSFeatureLayer will automatically determine the right ID field to use. 23 | @property (nonatomic, strong) NSString *idAttributeName; 24 | 25 | -(id)clusterItemKey; 26 | @end 27 | -------------------------------------------------------------------------------- /AGSClusterLayer/AGSGraphic+AGSClustering.m: -------------------------------------------------------------------------------- 1 | // 2 | // AGSGraphic+AGSClustering.m 3 | // Cluster Layer 4 | // 5 | // Created by Nicholas Furness on 3/28/14. 6 | // Copyright (c) 2014 ESRI. All rights reserved. 7 | // 8 | 9 | #import "AGSGraphic+AGSClustering.h" 10 | #import 11 | #import "AGSCluster.h" 12 | #import "Common_int.h" 13 | 14 | @implementation AGSGraphic (AGSClustering) 15 | -(BOOL)isCluster { 16 | return NO; 17 | } 18 | 19 | -(BOOL)isClusterCoverage { 20 | return self.owningCluster != nil; 21 | } 22 | 23 | -(AGSCluster *)owningCluster { 24 | return objc_getAssociatedObject(self, kClusterPayloadKey); 25 | } 26 | 27 | -(NSString *)idAttributeName { 28 | return objc_getAssociatedObject(self, kClusterGraphicCustomAttributeKey); 29 | } 30 | 31 | -(void)setIdAttributeName:(NSString *)idAttributeName { 32 | objc_setAssociatedObject(self, kClusterGraphicCustomAttributeKey, idAttributeName, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 33 | } 34 | 35 | -(id)clusterItemKey { 36 | 37 | static NSString *oidFieldName = @"FID"; 38 | 39 | NSUInteger result = self.featureId; 40 | if (result == 0) { 41 | if (self.idAttributeName) { 42 | @try { 43 | NSNumber *oid = [self attributeForKey:self.idAttributeName]; 44 | if (oid) { 45 | result = oid.unsignedIntegerValue; 46 | } 47 | } 48 | @catch (NSException *exception) { 49 | NSLog(@"If you set the 'idAttributeName' property, you MUST populate the AGSGraphic's attribute with an Unsigned Int > 0"); 50 | } 51 | } else if (self.layer == nil || 52 | ![self.layer respondsToSelector:@selector(objectIdField)]) { 53 | // No featureId (we're doubtless not on a featureLayer). Try to recover 54 | @try { 55 | NSNumber *oid = [self attributeForKey:oidFieldName]; 56 | if (oid) { 57 | result = oid.unsignedIntegerValue; 58 | } 59 | } 60 | @catch (NSException *exception) { 61 | NSLog(@"Could not read FeatureID: %@", exception); 62 | } 63 | } else { 64 | // No id, but we can get a OID field 65 | oidFieldName = [((id)self.layer) objectIdField]; 66 | NSNumber *oid = [self attributeForKey:oidFieldName]; 67 | if (oid) { 68 | result = oid.unsignedIntegerValue; 69 | } else { 70 | NSLog(@"Cannot find feature OID!"); 71 | } 72 | } 73 | } 74 | 75 | if (result == 0) { 76 | 77 | // If we could not recover, let's say so. 78 | NSLog(@"Feature ID 0!!"); 79 | UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Feature ID 0!!" 80 | message:nil 81 | delegate:nil 82 | cancelButtonTitle:@"OK" 83 | otherButtonTitles:nil]; 84 | [alert show]; 85 | } 86 | 87 | return [NSString stringWithFormat:@"f%d", result]; 88 | } 89 | @end -------------------------------------------------------------------------------- /AGSClusterLayer/NSArray+Utils.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSArray+Utils.h 3 | // Cluster Layer 4 | // 5 | // Created by Nicholas Furness on 3/28/14. 6 | // With thanks to https://mikeash.com/pyblog/friday-qa-2009-08-14-practical-blocks.html 7 | 8 | #import 9 | 10 | @interface NSArray (Utils) 11 | - (NSArray *)map: (id (^)(id obj))block; 12 | @end 13 | -------------------------------------------------------------------------------- /AGSClusterLayer/NSArray+Utils.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSArray+Utils.m 3 | // Cluster Layer 4 | // 5 | // Created by Nicholas Furness on 3/28/14. 6 | // With thanks to https://mikeash.com/pyblog/friday-qa-2009-08-14-practical-blocks.html 7 | 8 | #import "NSArray+Utils.h" 9 | 10 | @implementation NSArray (Utils) 11 | - (NSArray *)map: (id (^)(id obj))block 12 | { 13 | NSMutableArray *new = [NSMutableArray array]; 14 | for(id obj in self) 15 | { 16 | id newObj = block(obj); 17 | [new addObject: newObj ? newObj : [NSNull null]]; 18 | } 19 | return new; 20 | } 21 | @end 22 | 23 | 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | clusterlayer-plugin-ios 2 | ======================= 3 | 4 | A cluster layer extension to the [ArcGIS Runtime for iOS](https://developers.arcgis.com/ios/). 5 | 6 | The gridding code is based heavily on [Leaflet.markercluster](https://github.com/Leaflet/Leaflet.markercluster/blob/master/src/DistanceGrid.js) 7 | 8 | ![App](clusterlayer-plugin-ios.png) 9 | 10 | ## Usage 11 | 1. Import `AGSClusterLayer.h` 12 | 2. Add an `AGSFeatureLayer` to the `AGSMapView` 13 | 3. Create an `AGSClusterLayer` with the `AGSFeatureLayer` and add it to the `AGSMapView` 14 | ``` ObjC 15 | #import "AGSClusterLayer.h" 16 | 17 | #define kGreyBasemap @"http://services.arcgisonline.com/ArcGIS/rest/services/Canvas/World_Light_Gray_Base/MapServer" 18 | #define kGreyBasemapRef @"http://services.arcgisonline.com/ArcGIS/rest/services/Canvas/World_Light_Gray_Reference/MapServer" 19 | 20 | - (void)viewDidLoad 21 | { 22 | [super viewDidLoad]; 23 | 24 | [self.mapView addMapLayer:[AGSTiledMapServiceLayer tiledMapServiceLayerWithURL:[NSURL URLWithString:kGreyBasemap]]]; 25 | 26 | AGSFeatureLayer *featureLayer = [AGSFeatureLayer featureServiceLayerWithURL:[NSURL URLWithString:kFeatureLayerURL] mode:AGSFeatureLayerModeOnDemand]; 27 | [self.mapView addMapLayer:featureLayer]; 28 | 29 | self.clusterLayer = [AGSClusterLayer clusterLayerForFeatureLayer:featureLayer]; 30 | [self.mapView addMapLayer:self.clusterLayer]; 31 | } 32 | ``` 33 | 34 | You can also use an `AGSGraphicsLayer` to load data into a cluster layer, but you need to do this once the `AGSMapView` has loaded, for example in the `mapViewDidLoad:` delegate method of `AGSMapViewLayerDelegate`: 35 | 36 | ``` ObjC 37 | -(void)mapViewDidLoad:(AGSMapView *)mapView { 38 | // Note, we need to add the GraphicsLayer after the AGSMapView has loaded so we know 39 | // there's a spatial reference we can use. You will see a warning in the console 40 | // logs if you don't. 41 | AGSGraphicsLayer *graphicsLayer = [AGSGraphicsLayer graphicsLayer]; 42 | graphicsLayer.renderer = [AGSSimpleRenderer simpleRendererWithSymbol:self.symbol]; 43 | [self.mapView addMapLayer:graphicsLayer]; 44 | 45 | [graphicsLayer addGraphics:[self generateRandomPointGraphics:10000 inEnvelope:self.mapView.visibleAreaEnvelope]]; 46 | 47 | // Now wrap it in an AGSClusterLayer. The original GraphicsLayer will be hidden in the map. 48 | self.graphicsClusterLayer = [AGSClusterLayer clusterLayerForGraphicsLayer:graphicsLayer]; 49 | [self.mapView addMapLayer:self.graphicsClusterLayer]; 50 | } 51 | ``` 52 | -------------------------------------------------------------------------------- /Sample/AGSCluster_int.h: -------------------------------------------------------------------------------- 1 | // 2 | // AGSCluster.h 3 | // Cluster Layer 4 | // 5 | // Created by Nicholas Furness on 3/25/14. 6 | // Copyright (c) 2014 ESRI. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | #import "AGSClusterGrid.h" 12 | 13 | @interface AGSCluster (_internal) 14 | @property (nonatomic, assign) CGPoint cellCoordinate; 15 | @property (nonatomic, strong) AGSClusterGrid *parentGrid; 16 | @end 17 | -------------------------------------------------------------------------------- /Sample/ClusterLayerSample.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | D906E0A818F316940015EAF2 /* AGSClusterGridRow.m in Sources */ = {isa = PBXBuildFile; fileRef = D906E0A718F316940015EAF2 /* AGSClusterGridRow.m */; }; 11 | D919A35F18E0B09800B140BF /* AGSClusterLayer.m in Sources */ = {isa = PBXBuildFile; fileRef = D919A35E18E0B09800B140BF /* AGSClusterLayer.m */; }; 12 | D919A36218E0B14700B140BF /* AGSClusterGrid.m in Sources */ = {isa = PBXBuildFile; fileRef = D919A36118E0B14700B140BF /* AGSClusterGrid.m */; }; 13 | D919A36518E203DD00B140BF /* AGSCluster.m in Sources */ = {isa = PBXBuildFile; fileRef = D919A36418E203DD00B140BF /* AGSCluster.m */; }; 14 | D977B2901971B1C3004E179E /* icon.png in Resources */ = {isa = PBXBuildFile; fileRef = D977B28E1971B1C3004E179E /* icon.png */; }; 15 | D977B2911971B1C3004E179E /* icon@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D977B28F1971B1C3004E179E /* icon@2x.png */; }; 16 | D98A0A7218F8666C00138B1C /* NSObject+NFNotificationsProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = D98A0A7118F8666C00138B1C /* NSObject+NFNotificationsProvider.m */; }; 17 | D9A8B11916BB27B9001653EE /* libc++.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = D9A8B11816BB27B9001653EE /* libc++.dylib */; }; 18 | D9A8B11B16BB27BF001653EE /* OpenGLES.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D9A8B11A16BB27BF001653EE /* OpenGLES.framework */; }; 19 | D9D1C8BE18E62D6400CD3CEF /* AGSGraphic+AGSClustering.m in Sources */ = {isa = PBXBuildFile; fileRef = D9D1C8BD18E62D6400CD3CEF /* AGSGraphic+AGSClustering.m */; }; 20 | D9D1C8C218E62DF000CD3CEF /* NSArray+Utils.m in Sources */ = {isa = PBXBuildFile; fileRef = D9D1C8C118E62DF000CD3CEF /* NSArray+Utils.m */; }; 21 | D9E0F74A18E232CC0009C771 /* AGSClusterLayerRenderer.m in Sources */ = {isa = PBXBuildFile; fileRef = D9E0F74918E232CC0009C771 /* AGSClusterLayerRenderer.m */; }; 22 | D9EFFAEB1C821E1400166983 /* Launch Screen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D9EFFAEA1C821E1400166983 /* Launch Screen.storyboard */; }; 23 | D9EFFAEE1C822ADE00166983 /* stops.geodatabase in Resources */ = {isa = PBXBuildFile; fileRef = D9EFFAED1C822ADE00166983 /* stops.geodatabase */; }; 24 | D9EFFAF11C822EF700166983 /* AGSGDBFeature+AGSClustering.m in Sources */ = {isa = PBXBuildFile; fileRef = D9EFFAF01C822EF700166983 /* AGSGDBFeature+AGSClustering.m */; }; 25 | D9F3D1AE1668271A00FB6A8D /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D9F3D1AD1668271A00FB6A8D /* UIKit.framework */; }; 26 | D9F3D1B01668271A00FB6A8D /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D9F3D1AF1668271A00FB6A8D /* Foundation.framework */; }; 27 | D9F3D1B21668271A00FB6A8D /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D9F3D1B11668271A00FB6A8D /* CoreGraphics.framework */; }; 28 | D9F3D1B81668271A00FB6A8D /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = D9F3D1B61668271A00FB6A8D /* InfoPlist.strings */; }; 29 | D9F3D1BA1668271A00FB6A8D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = D9F3D1B91668271A00FB6A8D /* main.m */; }; 30 | D9F3D1BE1668271A00FB6A8D /* AGSSampleAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = D9F3D1BD1668271A00FB6A8D /* AGSSampleAppDelegate.m */; }; 31 | D9F3D1C01668271A00FB6A8D /* Default.png in Resources */ = {isa = PBXBuildFile; fileRef = D9F3D1BF1668271A00FB6A8D /* Default.png */; }; 32 | D9F3D1C21668271A00FB6A8D /* Default@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D9F3D1C11668271A00FB6A8D /* Default@2x.png */; }; 33 | D9F3D1C41668271A00FB6A8D /* Default-568h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D9F3D1C31668271A00FB6A8D /* Default-568h@2x.png */; }; 34 | D9F3D1C71668271A00FB6A8D /* MainStoryboard_iPhone.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D9F3D1C51668271A00FB6A8D /* MainStoryboard_iPhone.storyboard */; }; 35 | D9F3D1CA1668271A00FB6A8D /* MainStoryboard_iPad.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D9F3D1C81668271A00FB6A8D /* MainStoryboard_iPad.storyboard */; }; 36 | D9F3D1CD1668271A00FB6A8D /* AGSSampleViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = D9F3D1CC1668271A00FB6A8D /* AGSSampleViewController.m */; }; 37 | D9F3D1DB166828A600FB6A8D /* CoreLocation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D9F3D1D3166828A600FB6A8D /* CoreLocation.framework */; }; 38 | D9F3D1DC166828A600FB6A8D /* CoreText.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D9F3D1D4166828A600FB6A8D /* CoreText.framework */; }; 39 | D9F3D1DE166828A600FB6A8D /* libz.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = D9F3D1D6166828A600FB6A8D /* libz.dylib */; }; 40 | D9F3D1DF166828A600FB6A8D /* MediaPlayer.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D9F3D1D7166828A600FB6A8D /* MediaPlayer.framework */; }; 41 | D9F3D1E0166828A600FB6A8D /* MobileCoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D9F3D1D8166828A600FB6A8D /* MobileCoreServices.framework */; }; 42 | D9F3D1E1166828A600FB6A8D /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D9F3D1D9166828A600FB6A8D /* QuartzCore.framework */; }; 43 | D9F3D1E2166828A600FB6A8D /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D9F3D1DA166828A600FB6A8D /* Security.framework */; }; 44 | D9F3D1E41668293000FB6A8D /* ArcGIS.bundle in Resources */ = {isa = PBXBuildFile; fileRef = D9F3D1E31668293000FB6A8D /* ArcGIS.bundle */; }; 45 | /* End PBXBuildFile section */ 46 | 47 | /* Begin PBXFileReference section */ 48 | D906E0A618F316940015EAF2 /* AGSClusterGridRow.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = AGSClusterGridRow.h; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; 49 | D906E0A718F316940015EAF2 /* AGSClusterGridRow.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = AGSClusterGridRow.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; 50 | D906E0A918F79B7B0015EAF2 /* AGSClustering.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = AGSClustering.h; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; 51 | D919A35D18E0B09800B140BF /* AGSClusterLayer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = AGSClusterLayer.h; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; 52 | D919A35E18E0B09800B140BF /* AGSClusterLayer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = AGSClusterLayer.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; 53 | D919A36018E0B14700B140BF /* AGSClusterGrid.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = AGSClusterGrid.h; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; 54 | D919A36118E0B14700B140BF /* AGSClusterGrid.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = AGSClusterGrid.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; 55 | D919A36318E203DD00B140BF /* AGSCluster.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = AGSCluster.h; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; 56 | D919A36418E203DD00B140BF /* AGSCluster.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = AGSCluster.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; 57 | D977B28E1971B1C3004E179E /* icon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = icon.png; sourceTree = ""; }; 58 | D977B28F1971B1C3004E179E /* icon@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "icon@2x.png"; sourceTree = ""; }; 59 | D98A0A7018F8666C00138B1C /* NSObject+NFNotificationsProvider.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSObject+NFNotificationsProvider.h"; sourceTree = ""; }; 60 | D98A0A7118F8666C00138B1C /* NSObject+NFNotificationsProvider.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSObject+NFNotificationsProvider.m"; sourceTree = ""; }; 61 | D9A8B11816BB27B9001653EE /* libc++.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = "libc++.dylib"; path = "usr/lib/libc++.dylib"; sourceTree = SDKROOT; }; 62 | D9A8B11A16BB27BF001653EE /* OpenGLES.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = OpenGLES.framework; path = System/Library/Frameworks/OpenGLES.framework; sourceTree = SDKROOT; }; 63 | D9D1C8BB18E5942F00CD3CEF /* AGSCluster_int.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = AGSCluster_int.h; sourceTree = SOURCE_ROOT; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; 64 | D9D1C8BC18E62D6400CD3CEF /* AGSGraphic+AGSClustering.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = "AGSGraphic+AGSClustering.h"; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; 65 | D9D1C8BD18E62D6400CD3CEF /* AGSGraphic+AGSClustering.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = "AGSGraphic+AGSClustering.m"; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; 66 | D9D1C8C018E62DF000CD3CEF /* NSArray+Utils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = "NSArray+Utils.h"; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; 67 | D9D1C8C118E62DF000CD3CEF /* NSArray+Utils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = "NSArray+Utils.m"; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; 68 | D9D1C8C318E6338300CD3CEF /* Common_int.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = Common_int.h; sourceTree = SOURCE_ROOT; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; 69 | D9E0F74818E232CC0009C771 /* AGSClusterLayerRenderer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = AGSClusterLayerRenderer.h; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; 70 | D9E0F74918E232CC0009C771 /* AGSClusterLayerRenderer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = AGSClusterLayerRenderer.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; 71 | D9EFFAEA1C821E1400166983 /* Launch Screen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = "Launch Screen.storyboard"; sourceTree = ""; }; 72 | D9EFFAED1C822ADE00166983 /* stops.geodatabase */ = {isa = PBXFileReference; lastKnownFileType = file; name = stops.geodatabase; path = Data/stops.geodatabase; sourceTree = ""; }; 73 | D9EFFAEF1C822EF700166983 /* AGSGDBFeature+AGSClustering.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "AGSGDBFeature+AGSClustering.h"; sourceTree = ""; }; 74 | D9EFFAF01C822EF700166983 /* AGSGDBFeature+AGSClustering.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "AGSGDBFeature+AGSClustering.m"; sourceTree = ""; }; 75 | D9F3D1A91668271A00FB6A8D /* AGSClusterLayer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AGSClusterLayer.app; sourceTree = BUILT_PRODUCTS_DIR; }; 76 | D9F3D1AD1668271A00FB6A8D /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; 77 | D9F3D1AF1668271A00FB6A8D /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; 78 | D9F3D1B11668271A00FB6A8D /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; 79 | D9F3D1B51668271A00FB6A8D /* AGSSample-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "AGSSample-Info.plist"; sourceTree = ""; }; 80 | D9F3D1B71668271A00FB6A8D /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 81 | D9F3D1B91668271A00FB6A8D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 82 | D9F3D1BB1668271A00FB6A8D /* AGSSample-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "AGSSample-Prefix.pch"; sourceTree = ""; }; 83 | D9F3D1BC1668271A00FB6A8D /* AGSSampleAppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AGSSampleAppDelegate.h; sourceTree = ""; }; 84 | D9F3D1BD1668271A00FB6A8D /* AGSSampleAppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AGSSampleAppDelegate.m; sourceTree = ""; }; 85 | D9F3D1BF1668271A00FB6A8D /* Default.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = Default.png; sourceTree = ""; }; 86 | D9F3D1C11668271A00FB6A8D /* Default@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Default@2x.png"; sourceTree = ""; }; 87 | D9F3D1C31668271A00FB6A8D /* Default-568h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Default-568h@2x.png"; sourceTree = ""; }; 88 | D9F3D1C61668271A00FB6A8D /* en */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = en; path = en.lproj/MainStoryboard_iPhone.storyboard; sourceTree = ""; }; 89 | D9F3D1C91668271A00FB6A8D /* en */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = en; path = en.lproj/MainStoryboard_iPad.storyboard; sourceTree = ""; }; 90 | D9F3D1CB1668271A00FB6A8D /* AGSSampleViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AGSSampleViewController.h; sourceTree = ""; }; 91 | D9F3D1CC1668271A00FB6A8D /* AGSSampleViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AGSSampleViewController.m; sourceTree = ""; }; 92 | D9F3D1D3166828A600FB6A8D /* CoreLocation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreLocation.framework; path = System/Library/Frameworks/CoreLocation.framework; sourceTree = SDKROOT; }; 93 | D9F3D1D4166828A600FB6A8D /* CoreText.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreText.framework; path = System/Library/Frameworks/CoreText.framework; sourceTree = SDKROOT; }; 94 | D9F3D1D6166828A600FB6A8D /* libz.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libz.dylib; path = usr/lib/libz.dylib; sourceTree = SDKROOT; }; 95 | D9F3D1D7166828A600FB6A8D /* MediaPlayer.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MediaPlayer.framework; path = System/Library/Frameworks/MediaPlayer.framework; sourceTree = SDKROOT; }; 96 | D9F3D1D8166828A600FB6A8D /* MobileCoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MobileCoreServices.framework; path = System/Library/Frameworks/MobileCoreServices.framework; sourceTree = SDKROOT; }; 97 | D9F3D1D9166828A600FB6A8D /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; 98 | D9F3D1DA166828A600FB6A8D /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; }; 99 | D9F3D1E31668293000FB6A8D /* ArcGIS.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; name = ArcGIS.bundle; path = $HOME/Library/SDKs/ArcGIS/iOS/ArcGIS.framework/Versions/Current/Resources/ArcGIS.bundle; sourceTree = ""; }; 100 | /* End PBXFileReference section */ 101 | 102 | /* Begin PBXFrameworksBuildPhase section */ 103 | D9F3D1A61668271A00FB6A8D /* Frameworks */ = { 104 | isa = PBXFrameworksBuildPhase; 105 | buildActionMask = 2147483647; 106 | files = ( 107 | D9A8B11B16BB27BF001653EE /* OpenGLES.framework in Frameworks */, 108 | D9A8B11916BB27B9001653EE /* libc++.dylib in Frameworks */, 109 | D9F3D1B21668271A00FB6A8D /* CoreGraphics.framework in Frameworks */, 110 | D9F3D1DB166828A600FB6A8D /* CoreLocation.framework in Frameworks */, 111 | D9F3D1B01668271A00FB6A8D /* Foundation.framework in Frameworks */, 112 | D9F3D1E1166828A600FB6A8D /* QuartzCore.framework in Frameworks */, 113 | D9F3D1AE1668271A00FB6A8D /* UIKit.framework in Frameworks */, 114 | D9F3D1DC166828A600FB6A8D /* CoreText.framework in Frameworks */, 115 | D9F3D1DF166828A600FB6A8D /* MediaPlayer.framework in Frameworks */, 116 | D9F3D1E0166828A600FB6A8D /* MobileCoreServices.framework in Frameworks */, 117 | D9F3D1DE166828A600FB6A8D /* libz.dylib in Frameworks */, 118 | D9F3D1E2166828A600FB6A8D /* Security.framework in Frameworks */, 119 | ); 120 | runOnlyForDeploymentPostprocessing = 0; 121 | }; 122 | /* End PBXFrameworksBuildPhase section */ 123 | 124 | /* Begin PBXGroup section */ 125 | D919A35C18E0B06300B140BF /* AGSClusterLayer */ = { 126 | isa = PBXGroup; 127 | children = ( 128 | D906E0A918F79B7B0015EAF2 /* AGSClustering.h */, 129 | D919A35D18E0B09800B140BF /* AGSClusterLayer.h */, 130 | D919A35E18E0B09800B140BF /* AGSClusterLayer.m */, 131 | D9E0F74818E232CC0009C771 /* AGSClusterLayerRenderer.h */, 132 | D9E0F74918E232CC0009C771 /* AGSClusterLayerRenderer.m */, 133 | D919A36018E0B14700B140BF /* AGSClusterGrid.h */, 134 | D919A36118E0B14700B140BF /* AGSClusterGrid.m */, 135 | D906E0A618F316940015EAF2 /* AGSClusterGridRow.h */, 136 | D906E0A718F316940015EAF2 /* AGSClusterGridRow.m */, 137 | D919A36318E203DD00B140BF /* AGSCluster.h */, 138 | D919A36418E203DD00B140BF /* AGSCluster.m */, 139 | D9D1C8BF18E62D6900CD3CEF /* Categories */, 140 | D9D1C8C318E6338300CD3CEF /* Common_int.h */, 141 | D9D1C8BB18E5942F00CD3CEF /* AGSCluster_int.h */, 142 | ); 143 | name = AGSClusterLayer; 144 | path = ../AGSClusterLayer; 145 | sourceTree = SOURCE_ROOT; 146 | }; 147 | D98A0A7318F8749A00138B1C /* Categories */ = { 148 | isa = PBXGroup; 149 | children = ( 150 | D98A0A7018F8666C00138B1C /* NSObject+NFNotificationsProvider.h */, 151 | D98A0A7118F8666C00138B1C /* NSObject+NFNotificationsProvider.m */, 152 | ); 153 | name = Categories; 154 | sourceTree = ""; 155 | }; 156 | D9D1C8BF18E62D6900CD3CEF /* Categories */ = { 157 | isa = PBXGroup; 158 | children = ( 159 | D9EFFAEF1C822EF700166983 /* AGSGDBFeature+AGSClustering.h */, 160 | D9EFFAF01C822EF700166983 /* AGSGDBFeature+AGSClustering.m */, 161 | D9D1C8BC18E62D6400CD3CEF /* AGSGraphic+AGSClustering.h */, 162 | D9D1C8BD18E62D6400CD3CEF /* AGSGraphic+AGSClustering.m */, 163 | D9D1C8C018E62DF000CD3CEF /* NSArray+Utils.h */, 164 | D9D1C8C118E62DF000CD3CEF /* NSArray+Utils.m */, 165 | ); 166 | name = Categories; 167 | sourceTree = ""; 168 | }; 169 | D9EFFAEC1C822AD300166983 /* Data */ = { 170 | isa = PBXGroup; 171 | children = ( 172 | D9EFFAED1C822ADE00166983 /* stops.geodatabase */, 173 | ); 174 | name = Data; 175 | sourceTree = ""; 176 | }; 177 | D9F3D19E1668271900FB6A8D = { 178 | isa = PBXGroup; 179 | children = ( 180 | D9F3D1B31668271A00FB6A8D /* Source */, 181 | D9F3D1AC1668271A00FB6A8D /* Frameworks */, 182 | D9F3D1AA1668271A00FB6A8D /* Products */, 183 | ); 184 | sourceTree = ""; 185 | }; 186 | D9F3D1AA1668271A00FB6A8D /* Products */ = { 187 | isa = PBXGroup; 188 | children = ( 189 | D9F3D1A91668271A00FB6A8D /* AGSClusterLayer.app */, 190 | ); 191 | name = Products; 192 | sourceTree = ""; 193 | }; 194 | D9F3D1AC1668271A00FB6A8D /* Frameworks */ = { 195 | isa = PBXGroup; 196 | children = ( 197 | D9F3D1B11668271A00FB6A8D /* CoreGraphics.framework */, 198 | D9F3D1D3166828A600FB6A8D /* CoreLocation.framework */, 199 | D9F3D1D4166828A600FB6A8D /* CoreText.framework */, 200 | D9F3D1AF1668271A00FB6A8D /* Foundation.framework */, 201 | D9A8B11816BB27B9001653EE /* libc++.dylib */, 202 | D9F3D1D6166828A600FB6A8D /* libz.dylib */, 203 | D9F3D1D7166828A600FB6A8D /* MediaPlayer.framework */, 204 | D9F3D1D8166828A600FB6A8D /* MobileCoreServices.framework */, 205 | D9A8B11A16BB27BF001653EE /* OpenGLES.framework */, 206 | D9F3D1D9166828A600FB6A8D /* QuartzCore.framework */, 207 | D9F3D1DA166828A600FB6A8D /* Security.framework */, 208 | D9F3D1AD1668271A00FB6A8D /* UIKit.framework */, 209 | ); 210 | name = Frameworks; 211 | sourceTree = ""; 212 | }; 213 | D9F3D1B31668271A00FB6A8D /* Source */ = { 214 | isa = PBXGroup; 215 | children = ( 216 | D919A35C18E0B06300B140BF /* AGSClusterLayer */, 217 | D9F3D1BC1668271A00FB6A8D /* AGSSampleAppDelegate.h */, 218 | D9F3D1BD1668271A00FB6A8D /* AGSSampleAppDelegate.m */, 219 | D9F3D1C51668271A00FB6A8D /* MainStoryboard_iPhone.storyboard */, 220 | D9F3D1C81668271A00FB6A8D /* MainStoryboard_iPad.storyboard */, 221 | D9F3D1CB1668271A00FB6A8D /* AGSSampleViewController.h */, 222 | D9F3D1CC1668271A00FB6A8D /* AGSSampleViewController.m */, 223 | D98A0A7318F8749A00138B1C /* Categories */, 224 | D9F3D1B41668271A00FB6A8D /* Supporting Files */, 225 | ); 226 | path = Source; 227 | sourceTree = SOURCE_ROOT; 228 | }; 229 | D9F3D1B41668271A00FB6A8D /* Supporting Files */ = { 230 | isa = PBXGroup; 231 | children = ( 232 | D9EFFAEC1C822AD300166983 /* Data */, 233 | D977B28E1971B1C3004E179E /* icon.png */, 234 | D977B28F1971B1C3004E179E /* icon@2x.png */, 235 | D9F3D1E31668293000FB6A8D /* ArcGIS.bundle */, 236 | D9F3D1B51668271A00FB6A8D /* AGSSample-Info.plist */, 237 | D9F3D1B61668271A00FB6A8D /* InfoPlist.strings */, 238 | D9F3D1B91668271A00FB6A8D /* main.m */, 239 | D9F3D1BB1668271A00FB6A8D /* AGSSample-Prefix.pch */, 240 | D9F3D1BF1668271A00FB6A8D /* Default.png */, 241 | D9F3D1C11668271A00FB6A8D /* Default@2x.png */, 242 | D9F3D1C31668271A00FB6A8D /* Default-568h@2x.png */, 243 | D9EFFAEA1C821E1400166983 /* Launch Screen.storyboard */, 244 | ); 245 | name = "Supporting Files"; 246 | sourceTree = ""; 247 | }; 248 | /* End PBXGroup section */ 249 | 250 | /* Begin PBXNativeTarget section */ 251 | D9F3D1A81668271A00FB6A8D /* AGSClusterLayerSample */ = { 252 | isa = PBXNativeTarget; 253 | buildConfigurationList = D9F3D1D01668271A00FB6A8D /* Build configuration list for PBXNativeTarget "AGSClusterLayerSample" */; 254 | buildPhases = ( 255 | D9F3D1A51668271A00FB6A8D /* Sources */, 256 | D9F3D1A61668271A00FB6A8D /* Frameworks */, 257 | D9F3D1A71668271A00FB6A8D /* Resources */, 258 | ); 259 | buildRules = ( 260 | ); 261 | dependencies = ( 262 | ); 263 | name = AGSClusterLayerSample; 264 | productName = Basemaps; 265 | productReference = D9F3D1A91668271A00FB6A8D /* AGSClusterLayer.app */; 266 | productType = "com.apple.product-type.application"; 267 | }; 268 | /* End PBXNativeTarget section */ 269 | 270 | /* Begin PBXProject section */ 271 | D9F3D1A01668271900FB6A8D /* Project object */ = { 272 | isa = PBXProject; 273 | attributes = { 274 | CLASSPREFIX = EQS; 275 | LastUpgradeCheck = 0820; 276 | ORGANIZATIONNAME = ESRI; 277 | }; 278 | buildConfigurationList = D9F3D1A31668271900FB6A8D /* Build configuration list for PBXProject "ClusterLayerSample" */; 279 | compatibilityVersion = "Xcode 3.2"; 280 | developmentRegion = English; 281 | hasScannedForEncodings = 0; 282 | knownRegions = ( 283 | en, 284 | ); 285 | mainGroup = D9F3D19E1668271900FB6A8D; 286 | productRefGroup = D9F3D1AA1668271A00FB6A8D /* Products */; 287 | projectDirPath = ""; 288 | projectRoot = ""; 289 | targets = ( 290 | D9F3D1A81668271A00FB6A8D /* AGSClusterLayerSample */, 291 | ); 292 | }; 293 | /* End PBXProject section */ 294 | 295 | /* Begin PBXResourcesBuildPhase section */ 296 | D9F3D1A71668271A00FB6A8D /* Resources */ = { 297 | isa = PBXResourcesBuildPhase; 298 | buildActionMask = 2147483647; 299 | files = ( 300 | D9F3D1B81668271A00FB6A8D /* InfoPlist.strings in Resources */, 301 | D9F3D1C01668271A00FB6A8D /* Default.png in Resources */, 302 | D9F3D1C21668271A00FB6A8D /* Default@2x.png in Resources */, 303 | D9F3D1C41668271A00FB6A8D /* Default-568h@2x.png in Resources */, 304 | D977B2901971B1C3004E179E /* icon.png in Resources */, 305 | D9EFFAEE1C822ADE00166983 /* stops.geodatabase in Resources */, 306 | D9F3D1C71668271A00FB6A8D /* MainStoryboard_iPhone.storyboard in Resources */, 307 | D9F3D1CA1668271A00FB6A8D /* MainStoryboard_iPad.storyboard in Resources */, 308 | D9F3D1E41668293000FB6A8D /* ArcGIS.bundle in Resources */, 309 | D9EFFAEB1C821E1400166983 /* Launch Screen.storyboard in Resources */, 310 | D977B2911971B1C3004E179E /* icon@2x.png in Resources */, 311 | ); 312 | runOnlyForDeploymentPostprocessing = 0; 313 | }; 314 | /* End PBXResourcesBuildPhase section */ 315 | 316 | /* Begin PBXSourcesBuildPhase section */ 317 | D9F3D1A51668271A00FB6A8D /* Sources */ = { 318 | isa = PBXSourcesBuildPhase; 319 | buildActionMask = 2147483647; 320 | files = ( 321 | D9D1C8C218E62DF000CD3CEF /* NSArray+Utils.m in Sources */, 322 | D906E0A818F316940015EAF2 /* AGSClusterGridRow.m in Sources */, 323 | D919A36518E203DD00B140BF /* AGSCluster.m in Sources */, 324 | D919A36218E0B14700B140BF /* AGSClusterGrid.m in Sources */, 325 | D98A0A7218F8666C00138B1C /* NSObject+NFNotificationsProvider.m in Sources */, 326 | D9F3D1BA1668271A00FB6A8D /* main.m in Sources */, 327 | D9EFFAF11C822EF700166983 /* AGSGDBFeature+AGSClustering.m in Sources */, 328 | D9F3D1BE1668271A00FB6A8D /* AGSSampleAppDelegate.m in Sources */, 329 | D9D1C8BE18E62D6400CD3CEF /* AGSGraphic+AGSClustering.m in Sources */, 330 | D919A35F18E0B09800B140BF /* AGSClusterLayer.m in Sources */, 331 | D9E0F74A18E232CC0009C771 /* AGSClusterLayerRenderer.m in Sources */, 332 | D9F3D1CD1668271A00FB6A8D /* AGSSampleViewController.m in Sources */, 333 | ); 334 | runOnlyForDeploymentPostprocessing = 0; 335 | }; 336 | /* End PBXSourcesBuildPhase section */ 337 | 338 | /* Begin PBXVariantGroup section */ 339 | D9F3D1B61668271A00FB6A8D /* InfoPlist.strings */ = { 340 | isa = PBXVariantGroup; 341 | children = ( 342 | D9F3D1B71668271A00FB6A8D /* en */, 343 | ); 344 | name = InfoPlist.strings; 345 | sourceTree = ""; 346 | }; 347 | D9F3D1C51668271A00FB6A8D /* MainStoryboard_iPhone.storyboard */ = { 348 | isa = PBXVariantGroup; 349 | children = ( 350 | D9F3D1C61668271A00FB6A8D /* en */, 351 | ); 352 | name = MainStoryboard_iPhone.storyboard; 353 | sourceTree = ""; 354 | }; 355 | D9F3D1C81668271A00FB6A8D /* MainStoryboard_iPad.storyboard */ = { 356 | isa = PBXVariantGroup; 357 | children = ( 358 | D9F3D1C91668271A00FB6A8D /* en */, 359 | ); 360 | name = MainStoryboard_iPad.storyboard; 361 | sourceTree = ""; 362 | }; 363 | /* End PBXVariantGroup section */ 364 | 365 | /* Begin XCBuildConfiguration section */ 366 | D9F3D1CE1668271A00FB6A8D /* Debug */ = { 367 | isa = XCBuildConfiguration; 368 | buildSettings = { 369 | ALWAYS_SEARCH_USER_PATHS = NO; 370 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 371 | CLANG_CXX_LIBRARY = "libc++"; 372 | CLANG_ENABLE_OBJC_ARC = YES; 373 | CLANG_WARN_BOOL_CONVERSION = YES; 374 | CLANG_WARN_CONSTANT_CONVERSION = YES; 375 | CLANG_WARN_EMPTY_BODY = YES; 376 | CLANG_WARN_ENUM_CONVERSION = YES; 377 | CLANG_WARN_INFINITE_RECURSION = YES; 378 | CLANG_WARN_INT_CONVERSION = YES; 379 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 380 | CLANG_WARN_UNREACHABLE_CODE = YES; 381 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 382 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 383 | COPY_PHASE_STRIP = NO; 384 | ENABLE_STRICT_OBJC_MSGSEND = YES; 385 | ENABLE_TESTABILITY = YES; 386 | GCC_C_LANGUAGE_STANDARD = gnu99; 387 | GCC_DYNAMIC_NO_PIC = NO; 388 | GCC_NO_COMMON_BLOCKS = YES; 389 | GCC_OPTIMIZATION_LEVEL = 0; 390 | GCC_PREPROCESSOR_DEFINITIONS = ( 391 | "DEBUG=1", 392 | "$(inherited)", 393 | ); 394 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 395 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 396 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 397 | GCC_WARN_UNDECLARED_SELECTOR = YES; 398 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 399 | GCC_WARN_UNUSED_FUNCTION = YES; 400 | GCC_WARN_UNUSED_VARIABLE = YES; 401 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 402 | ONLY_ACTIVE_ARCH = YES; 403 | SDKROOT = iphoneos; 404 | TARGETED_DEVICE_FAMILY = "1,2"; 405 | }; 406 | name = Debug; 407 | }; 408 | D9F3D1CF1668271A00FB6A8D /* Release */ = { 409 | isa = XCBuildConfiguration; 410 | buildSettings = { 411 | ALWAYS_SEARCH_USER_PATHS = NO; 412 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 413 | CLANG_CXX_LIBRARY = "libc++"; 414 | CLANG_ENABLE_OBJC_ARC = YES; 415 | CLANG_WARN_BOOL_CONVERSION = YES; 416 | CLANG_WARN_CONSTANT_CONVERSION = YES; 417 | CLANG_WARN_EMPTY_BODY = YES; 418 | CLANG_WARN_ENUM_CONVERSION = YES; 419 | CLANG_WARN_INFINITE_RECURSION = YES; 420 | CLANG_WARN_INT_CONVERSION = YES; 421 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 422 | CLANG_WARN_UNREACHABLE_CODE = YES; 423 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 424 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 425 | COPY_PHASE_STRIP = YES; 426 | ENABLE_STRICT_OBJC_MSGSEND = YES; 427 | GCC_C_LANGUAGE_STANDARD = gnu99; 428 | GCC_NO_COMMON_BLOCKS = YES; 429 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 430 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 431 | GCC_WARN_UNDECLARED_SELECTOR = YES; 432 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 433 | GCC_WARN_UNUSED_FUNCTION = YES; 434 | GCC_WARN_UNUSED_VARIABLE = YES; 435 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 436 | OTHER_CFLAGS = "-DNS_BLOCK_ASSERTIONS=1"; 437 | SDKROOT = iphoneos; 438 | TARGETED_DEVICE_FAMILY = "1,2"; 439 | VALIDATE_PRODUCT = YES; 440 | }; 441 | name = Release; 442 | }; 443 | D9F3D1D11668271A00FB6A8D /* Debug */ = { 444 | isa = XCBuildConfiguration; 445 | buildSettings = { 446 | ENABLE_BITCODE = NO; 447 | FRAMEWORK_SEARCH_PATHS = "$(HOME)/Library/SDKs/ArcGIS/iOS/**"; 448 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 449 | GCC_PREFIX_HEADER = "Source/AGSSample-Prefix.pch"; 450 | INFOPLIST_FILE = "Source/AGSSample-Info.plist"; 451 | ONLY_ACTIVE_ARCH = NO; 452 | OTHER_LDFLAGS = ( 453 | "-ObjC", 454 | "-framework", 455 | ArcGIS, 456 | "-l", 457 | "c++", 458 | ); 459 | PRODUCT_BUNDLE_IDENTIFIER = "com.esri.${PRODUCT_NAME:rfc1034identifier}"; 460 | PRODUCT_NAME = AGSClusterLayer; 461 | VALID_ARCHS = armv7; 462 | WRAPPER_EXTENSION = app; 463 | }; 464 | name = Debug; 465 | }; 466 | D9F3D1D21668271A00FB6A8D /* Release */ = { 467 | isa = XCBuildConfiguration; 468 | buildSettings = { 469 | ENABLE_BITCODE = NO; 470 | FRAMEWORK_SEARCH_PATHS = "$(HOME)/Library/SDKs/ArcGIS/iOS/**"; 471 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 472 | GCC_PREFIX_HEADER = "Source/AGSSample-Prefix.pch"; 473 | INFOPLIST_FILE = "Source/AGSSample-Info.plist"; 474 | OTHER_LDFLAGS = ( 475 | "-ObjC", 476 | "-framework", 477 | ArcGIS, 478 | "-l", 479 | "c++", 480 | ); 481 | PRODUCT_BUNDLE_IDENTIFIER = "com.esri.${PRODUCT_NAME:rfc1034identifier}"; 482 | PRODUCT_NAME = AGSClusterLayer; 483 | VALID_ARCHS = armv7; 484 | WRAPPER_EXTENSION = app; 485 | }; 486 | name = Release; 487 | }; 488 | /* End XCBuildConfiguration section */ 489 | 490 | /* Begin XCConfigurationList section */ 491 | D9F3D1A31668271900FB6A8D /* Build configuration list for PBXProject "ClusterLayerSample" */ = { 492 | isa = XCConfigurationList; 493 | buildConfigurations = ( 494 | D9F3D1CE1668271A00FB6A8D /* Debug */, 495 | D9F3D1CF1668271A00FB6A8D /* Release */, 496 | ); 497 | defaultConfigurationIsVisible = 0; 498 | defaultConfigurationName = Release; 499 | }; 500 | D9F3D1D01668271A00FB6A8D /* Build configuration list for PBXNativeTarget "AGSClusterLayerSample" */ = { 501 | isa = XCConfigurationList; 502 | buildConfigurations = ( 503 | D9F3D1D11668271A00FB6A8D /* Debug */, 504 | D9F3D1D21668271A00FB6A8D /* Release */, 505 | ); 506 | defaultConfigurationIsVisible = 0; 507 | defaultConfigurationName = Release; 508 | }; 509 | /* End XCConfigurationList section */ 510 | }; 511 | rootObject = D9F3D1A01668271900FB6A8D /* Project object */; 512 | } 513 | -------------------------------------------------------------------------------- /Sample/ClusterLayerSample.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Sample/Common_int.h: -------------------------------------------------------------------------------- 1 | // 2 | // Common_int.h 3 | // Cluster Layer 4 | // 5 | // Created by Nicholas Furness on 3/28/14. 6 | // Copyright (c) 2014 ESRI. All rights reserved. 7 | // 8 | 9 | #ifndef ClusterLayerSample_Common_h 10 | #define ClusterLayerSample_Common_h 11 | 12 | 13 | #pragma mark - DATA LOADING 14 | #define kClusterLayerDataLoadingNotification @"AGSCLusterLayerLoadProgressNotification" 15 | #define kClusterLayerDataLoadingNotification_Key_PercentComplete @"percentComplete" 16 | #define kClusterLayerDataLoadingNotification_Key_TotalRecords @"totalRecords" 17 | #define kClusterLayerDataLoadingNotification_Key_LoadedCount @"recordsLoaded" 18 | 19 | #define kClusterLayerDataLoadingErrorNotification @"AGSCLusterLayerLoadErrorNotification" 20 | #define kClusterLayerDataLoadingErrorNotification_Key_Error @"error" 21 | 22 | 23 | #pragma mark - LAYER CLUSTERING 24 | #define kClusterLayerClusteringNotification @"AGSClusterLayerClusteringNotification_Progress" 25 | #define kClusterLayerClusteringNotification_Key_Duration @"duration" 26 | #define kClusterLayerClusteringNotification_Key_PercentComplete @"percentComplete" 27 | #define kClusterLayerClusteringNotification_Key_FeatureCount @"featureCount" 28 | #define kClusterLayerClusteringNotification_Key_TotalZoomLevels @"totalLevels" 29 | #define kClusterLayerClusteringNotification_Key_ZoomLevelsClustered @"levelsComplete" 30 | 31 | 32 | 33 | #pragma mark - GRID CLUSTERING 34 | #define kClusterGridClusteringNotification @"AGSClusterGridNotification_StartClustering" 35 | 36 | #pragma mark - GRID CLUSTERED 37 | #define kClusterGridClusteredNotification @"AGSClusterGridNotification_EndClustering" 38 | #define kClusterGridClusteredNotification_Key_FeatureCount @"itemsClustered" 39 | #define kClusterGridClusteredNotification_Key_ClusterCount @"clusters" 40 | #define kClusterGridClusteredNotification_Key_Duration @"duration" 41 | #define kClusterGridClusteredNotification_Key_ZoomLevel @"zoomLevel" 42 | 43 | 44 | #define kClusterPayloadKey @"__cluster" 45 | #define kClusterGraphicCustomAttributeKey @"__clusterIDKey" 46 | 47 | typedef AGSGraphic AGSClusterItem; 48 | 49 | #endif 50 | -------------------------------------------------------------------------------- /Sample/Source/AGSSample-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | Cluster Layer 9 | CFBundleExecutable 10 | ${EXECUTABLE_NAME} 11 | CFBundleIcons 12 | 13 | CFBundlePrimaryIcon 14 | 15 | CFBundleIconFiles 16 | 17 | icon 18 | 19 | 20 | 21 | CFBundleIdentifier 22 | $(PRODUCT_BUNDLE_IDENTIFIER) 23 | CFBundleInfoDictionaryVersion 24 | 6.0 25 | CFBundleName 26 | ${PRODUCT_NAME} 27 | CFBundlePackageType 28 | APPL 29 | CFBundleShortVersionString 30 | 1.0 31 | CFBundleSignature 32 | ???? 33 | CFBundleVersion 34 | 1.0 35 | LSRequiresIPhoneOS 36 | 37 | UILaunchStoryboardName 38 | Launch Screen 39 | UIMainStoryboardFile 40 | MainStoryboard_iPhone 41 | UIMainStoryboardFile~ipad 42 | MainStoryboard_iPad 43 | UIRequiredDeviceCapabilities 44 | 45 | armv7 46 | 47 | UISupportedInterfaceOrientations 48 | 49 | UIInterfaceOrientationPortrait 50 | UIInterfaceOrientationLandscapeLeft 51 | UIInterfaceOrientationLandscapeRight 52 | 53 | UISupportedInterfaceOrientations~ipad 54 | 55 | UIInterfaceOrientationPortrait 56 | UIInterfaceOrientationPortraitUpsideDown 57 | UIInterfaceOrientationLandscapeLeft 58 | UIInterfaceOrientationLandscapeRight 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /Sample/Source/AGSSample-Prefix.pch: -------------------------------------------------------------------------------- 1 | // 2 | // Prefix header for all source files of the 'Basemaps' target in the 'Basemaps' project 3 | // 4 | 5 | #import 6 | 7 | #ifndef __IPHONE_5_0 8 | #warning "This project uses features only available in iOS SDK 5.0 and later." 9 | #endif 10 | 11 | #ifdef __OBJC__ 12 | #import 13 | #import 14 | #endif 15 | -------------------------------------------------------------------------------- /Sample/Source/AGSSampleAppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // EQSAppDelegate.h 3 | // Basemaps 4 | // 5 | // Created by Nicholas Furness on 11/29/12. 6 | // Copyright (c) 2012 ESRI. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface AGSSampleAppDelegate : UIResponder 12 | 13 | @property (strong, nonatomic) UIWindow *window; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /Sample/Source/AGSSampleAppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // EQSAppDelegate.m 3 | // Basemaps 4 | // 5 | // Created by Nicholas Furness on 11/29/12. 6 | // Copyright (c) 2012 ESRI. All rights reserved. 7 | // 8 | 9 | #import "AGSSampleAppDelegate.h" 10 | 11 | @implementation AGSSampleAppDelegate 12 | 13 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 14 | { 15 | // Override point for customization after application launch. 16 | return YES; 17 | } 18 | 19 | - (void)applicationWillResignActive:(UIApplication *)application 20 | { 21 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 22 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 23 | } 24 | 25 | - (void)applicationDidEnterBackground:(UIApplication *)application 26 | { 27 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 28 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 29 | } 30 | 31 | - (void)applicationWillEnterForeground:(UIApplication *)application 32 | { 33 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | - (void)applicationDidBecomeActive:(UIApplication *)application 37 | { 38 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 39 | } 40 | 41 | - (void)applicationWillTerminate:(UIApplication *)application 42 | { 43 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 44 | } 45 | 46 | @end 47 | -------------------------------------------------------------------------------- /Sample/Source/AGSSampleViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // EQSViewController.h 3 | // Basemaps 4 | // 5 | // Created by Nicholas Furness on 11/29/12. 6 | // Copyright (c) 2012 ESRI. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface AGSSampleViewController : UIViewController 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /Sample/Source/AGSSampleViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // EQSViewController.m 3 | // Basemaps 4 | // 5 | // Created by Nicholas Furness on 11/29/12. 6 | // Copyright (c) 2012 ESRI. All rights reserved. 7 | // 8 | 9 | #import "AGSSampleViewController.h" 10 | #import 11 | #import "AGSClustering.h" 12 | #import "NSObject+NFNotificationsProvider.h" 13 | 14 | #define kBasemap @"https://services.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer" 15 | #define kFeatureLayerURL @"https://services.arcgis.com/OfH668nDRN7tbJh0/arcgis/rest/services/stops/FeatureServer/0" 16 | #define kGeodatabaseName @"stops" 17 | 18 | @interface AGSSampleViewController () 19 | @property (weak, nonatomic) IBOutlet AGSMapView *mapView; 20 | @property (nonatomic, strong) AGSClusterLayer *clusterLayer; 21 | @property (nonatomic, strong) AGSClusterLayer *graphicsClusterLayer; 22 | @property (weak, nonatomic) IBOutlet UISwitch *coverageSwitch; 23 | @property (weak, nonatomic) IBOutlet UISwitch *clusteringSwitch; 24 | @property (weak, nonatomic) IBOutlet UILabel *clusteringStatusLabel; 25 | @property (weak, nonatomic) IBOutlet UILabel *clusteringFeedbackLabel; 26 | @property (weak, nonatomic) IBOutlet UIProgressView *dataLoadProgressView; 27 | @property (weak, nonatomic) IBOutlet UILabel *clusteringEnabledLabel; 28 | @end 29 | 30 | @implementation AGSSampleViewController 31 | 32 | -(double)randomDoubleBetween:(double)smallNumber and:(double)bigNumber { 33 | double diff = bigNumber - smallNumber; 34 | return (((double) (arc4random() % ((unsigned)RAND_MAX + 1)) / RAND_MAX) * diff) + smallNumber; 35 | } 36 | 37 | -(AGSPoint *)randomPointInEnvelope:(AGSEnvelope *)envelope { 38 | return [AGSPoint pointWithX:[self randomDoubleBetween:envelope.xmax and:envelope.xmin] 39 | y:[self randomDoubleBetween:envelope.ymin and:envelope.ymax] 40 | spatialReference:envelope.spatialReference]; 41 | } 42 | 43 | - (void)viewDidLoad 44 | { 45 | [super viewDidLoad]; 46 | // Do any additional setup after loading the view, typically from a nib. 47 | 48 | [self.mapView addMapLayer:[AGSTiledMapServiceLayer tiledMapServiceLayerWithURL:[NSURL URLWithString:kBasemap]]]; 49 | 50 | NSError *error = nil; 51 | AGSGDBGeodatabase *gdb = [AGSGDBGeodatabase geodatabaseWithName:kGeodatabaseName error:&error]; 52 | 53 | if (error) { 54 | NSLog(@"Error opening the geodatabase: %@", error); 55 | } else { 56 | if (gdb.featureTables.count > 0) { 57 | AGSFeatureTable *table = gdb.featureTables[0]; 58 | AGSFeatureTableLayer *layer = [[AGSFeatureTableLayer alloc] initWithFeatureTable:table]; 59 | [self.mapView addMapLayer:layer]; 60 | 61 | self.clusterLayer = [AGSClusterLayer clusterLayerForFeatureTableLayer:layer]; 62 | } 63 | } 64 | 65 | /// ******************************* 66 | /// Cluster Layer Setup 67 | 68 | if (self.clusterLayer == nil) { 69 | // Must add the source layer to connect it to its end point. 70 | AGSFeatureLayer *featureLayer = [AGSFeatureLayer featureServiceLayerWithURL:[NSURL URLWithString:kFeatureLayerURL] mode:AGSFeatureLayerModeOnDemand]; 71 | [self.mapView addMapLayer:featureLayer]; 72 | 73 | // Now wrap it in an AGSClusterLayer. The original FeatureLayer will be hidden in the map. 74 | self.clusterLayer = [AGSClusterLayer clusterLayerForFeatureLayer:featureLayer]; 75 | } 76 | 77 | [self.mapView addMapLayer:self.clusterLayer]; 78 | 79 | // Cluster layer config 80 | self.clusterLayer.minScaleForClustering = 15000; 81 | 82 | /// ******************************* 83 | 84 | 85 | 86 | 87 | AGSEnvelope *initialEnv = [AGSEnvelope envelopeWithXmin:-13743980.503617 88 | ymin:5553033.545344 89 | xmax:-13567177.320049 90 | ymax:5845311.308181 91 | spatialReference:[AGSSpatialReference webMercatorSpatialReference]]; 92 | [self.mapView zoomToEnvelope:initialEnv animated:NO]; 93 | 94 | // Cluster layer events - this is all UI stuff... 95 | [self.clusterLayer registerListener:self 96 | forNotifications:@{AGSClusterLayerDataLoadingProgressNotification: strSelector(dataLoadProgress:), 97 | AGSClusterLayerDataLoadingErrorNotification: strSelector(dataLoadError:), 98 | AGSClusterLayerClusteringProgressNotification: strSelector(clusteringProgress:)}]; 99 | 100 | [self.clusterLayer addObserver:self forKeyPath:@"willClusterAtCurrentScale" options:NSKeyValueObservingOptionNew context:nil]; 101 | [self.mapView addObserver:self forKeyPath:@"mapScale" options:NSKeyValueObservingOptionNew context:nil]; 102 | self.clusterLayer.showsClusterCoverages = self.coverageSwitch.on; 103 | 104 | 105 | self.mapView.layerDelegate = self; 106 | } 107 | 108 | -(void)mapViewDidLoad:(AGSMapView *)mapView { 109 | // Note, we need to add the GraphicsLayer after the AGSMapView has loaded so we know 110 | // there's a spatial reference we can use. You will see a warning in the console 111 | // logs if you don't. 112 | AGSSimpleMarkerSymbol *symbol = [AGSSimpleMarkerSymbol simpleMarkerSymbolWithColor:[[UIColor orangeColor] colorWithAlphaComponent:0.75]]; 113 | symbol.outline = nil; 114 | symbol.style = AGSSimpleMarkerSymbolStyleCircle; 115 | symbol.size = CGSizeMake(4, 4); 116 | 117 | AGSGraphicsLayer *graphicsLayer = [AGSGraphicsLayer graphicsLayer]; 118 | graphicsLayer.renderer = [AGSSimpleRenderer simpleRendererWithSymbol:symbol]; 119 | [self.mapView addMapLayer:graphicsLayer]; 120 | 121 | [graphicsLayer addGraphics:[self generateRandomPointGraphics:10000 inEnvelope:self.mapView.visibleAreaEnvelope]]; 122 | 123 | /// ******************************* 124 | /// Cluster Layer Setup 125 | 126 | // Now wrap it in an AGSClusterLayer. The original GraphicsLayer will be hidden in the map. 127 | self.graphicsClusterLayer = [AGSClusterLayer clusterLayerForGraphicsLayer:graphicsLayer]; 128 | self.graphicsClusterLayer.opacity = 0.6; 129 | [self.mapView insertMapLayer:self.graphicsClusterLayer atIndex:[self.mapView.mapLayers indexOfObject:self.clusterLayer]]; 130 | 131 | // Cluster layer config 132 | self.graphicsClusterLayer.minScaleForClustering = 15000; 133 | /// ******************************* 134 | } 135 | 136 | -(NSArray *)generateRandomPointGraphics:(NSUInteger)count inEnvelope:(AGSEnvelope *)envelope { 137 | return [self generateRandomPointGraphics:count withSymbol:nil inEnvelope:envelope]; 138 | } 139 | 140 | -(NSArray *)generateRandomPointGraphics:(NSUInteger)count withSymbol:(AGSSymbol *)symbol inEnvelope:(AGSEnvelope *)envelope { 141 | NSInteger currentOID = 1; 142 | NSMutableArray *output = [NSMutableArray arrayWithCapacity:count]; 143 | for (NSInteger i = 0; i < count; i++) { 144 | AGSPoint *newPoint = [self randomPointInEnvelope:envelope]; 145 | [output addObject:[AGSGraphic graphicWithGeometry:newPoint 146 | symbol:symbol 147 | attributes:@{ 148 | @"FID": @(currentOID) 149 | }]]; 150 | currentOID++; 151 | } 152 | return output; 153 | } 154 | 155 | -(void)dataLoadProgress:(NSNotification *)notification { 156 | 157 | double percentComplete = [notification.userInfo[AGSClusterLayerDataLoadingProgressNotification_UserInfo_PercentComplete] doubleValue]; 158 | NSUInteger featuresLoaded = [notification.userInfo[AGSClusterLayerDataLoadingProgressNotification_UserInfo_RecordsLoaded] unsignedIntegerValue]; 159 | NSUInteger totalFeaturesToLoad = [notification.userInfo[AGSClusterLayerDataLoadingProgressNotification_UserInfo_TotalRecordsToLoad] unsignedIntegerValue]; 160 | 161 | [self.dataLoadProgressView setProgress:percentComplete/100 animated:YES]; 162 | 163 | if (percentComplete == 100) { 164 | NSLog(@"Done loading %d features", featuresLoaded); 165 | self.dataLoadProgressView.backgroundColor = self.dataLoadProgressView.tintColor; 166 | self.dataLoadProgressView.tintColor = [UIColor colorWithRed:0.756 green:0.137 blue:0.173 alpha:1.000]; 167 | [self.dataLoadProgressView setProgress:0 animated:NO]; 168 | } 169 | 170 | self.clusteringFeedbackLabel.text = [NSString stringWithFormat:@"Loading data: %.2f%% complete (%d of %d features)", percentComplete, featuresLoaded, totalFeaturesToLoad]; 171 | } 172 | 173 | -(void)clusteringProgress:(NSNotification *)notification { 174 | 175 | double percentComplete = [notification.userInfo[AGSClusterLayerClusteringProgressNotification_UserInfo_PercentComplete] doubleValue]; 176 | NSTimeInterval duration = [notification.userInfo[AGSClusterLayerClusteringProgressNotification_UserInfo_Duration] doubleValue]; 177 | 178 | [self.dataLoadProgressView setProgress:percentComplete/100 animated:YES]; 179 | 180 | if (percentComplete == 100) { 181 | self.clusteringFeedbackLabel.text = [NSString stringWithFormat:@"Clustering complete in %.2fs", duration]; 182 | NSLog(@"Done clustering features in %.4fs", duration); 183 | [UIView animateWithDuration:0.2 184 | animations:^{ 185 | self.dataLoadProgressView.alpha = 0; 186 | } 187 | completion:^(BOOL finished) { 188 | self.dataLoadProgressView.hidden = YES; 189 | }]; 190 | } else { 191 | self.clusteringFeedbackLabel.text = [NSString stringWithFormat:@"Clustering zoom levels %.4fs", duration]; 192 | } 193 | } 194 | 195 | -(void)dataLoadError:(NSNotification *)notification { 196 | 197 | NSError *error = notification.userInfo[AGSClusterLayerDataLoadingErrorNotification_UserInfo_Error]; 198 | NSString *strError = error.localizedDescription; 199 | if (strError.length == 0) strError = error.localizedFailureReason; 200 | [[[UIAlertView alloc] initWithTitle:@"Cluster Load Error" 201 | message:strError 202 | delegate:nil 203 | cancelButtonTitle:@"OK" 204 | otherButtonTitles:nil] show]; 205 | } 206 | 207 | -(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { 208 | self.clusteringStatusLabel.text = [NSString stringWithFormat:@"%@ (1:%.2f)", self.clusterLayer.willClusterAtCurrentScale?@"Clustering":@"Not Clustering", self.mapView.mapScale]; 209 | } 210 | 211 | - (IBAction)toggleCoverages:(id)sender { 212 | self.clusterLayer.showsClusterCoverages = self.coverageSwitch.on; 213 | self.graphicsClusterLayer.showsClusterCoverages = self.coverageSwitch.on; 214 | } 215 | 216 | - (IBAction)toggleClustering { 217 | self.clusterLayer.clusteringEnabled = self.clusteringSwitch.on; 218 | self.graphicsClusterLayer.clusteringEnabled = self.clusteringSwitch.on; 219 | self.coverageSwitch.enabled = self.clusterLayer.clusteringEnabled; 220 | self.clusteringEnabledLabel.text = [NSString stringWithFormat:@"Clustering %@", self.clusterLayer.clusteringEnabled?@"Enabled":@"Disabled"]; 221 | } 222 | 223 | - (void)didReceiveMemoryWarning { 224 | [super didReceiveMemoryWarning]; 225 | // Dispose of any resources that can be recreated. 226 | } 227 | 228 | -(BOOL)prefersStatusBarHidden { 229 | return YES; 230 | } 231 | @end 232 | -------------------------------------------------------------------------------- /Sample/Source/Data/stops.geodatabase: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nixta/clusterlayer-plugin-ios/8c8a0da68fcb2893585ce17600f0bbd0d07c2e27/Sample/Source/Data/stops.geodatabase -------------------------------------------------------------------------------- /Sample/Source/Default-568h@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nixta/clusterlayer-plugin-ios/8c8a0da68fcb2893585ce17600f0bbd0d07c2e27/Sample/Source/Default-568h@2x.png -------------------------------------------------------------------------------- /Sample/Source/Default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nixta/clusterlayer-plugin-ios/8c8a0da68fcb2893585ce17600f0bbd0d07c2e27/Sample/Source/Default.png -------------------------------------------------------------------------------- /Sample/Source/Default@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nixta/clusterlayer-plugin-ios/8c8a0da68fcb2893585ce17600f0bbd0d07c2e27/Sample/Source/Default@2x.png -------------------------------------------------------------------------------- /Sample/Source/Launch Screen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 27 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /Sample/Source/NSObject+NFNotificationsProvider.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSObject+AGSNotificationsProvider.h 3 | // 4 | // Created by Nicholas Furness on 4/11/14. 5 | // Copyright (c) 2014 ESRI. All rights reserved. 6 | // 7 | 8 | #import 9 | 10 | #define strSelector(a) NSStringFromSelector(@selector(a)) 11 | 12 | @interface NSObject (NFNotificationsProvider) 13 | #pragma mark - Register listener on caller object 14 | -(void)registerListener:(id)listener forNotifications:(NSDictionary *)notificationSelectorsByName; 15 | -(void)unRegisterListener:(id)listener fromNotifications:(NSArray *)notificationNames; 16 | 17 | #pragma mark - Register caller as listener on defined object(s) 18 | -(void)registerAsListenerForNotifications:(NSDictionary *)notificationSelectorsByName onObjectOrObjects:(id)objectOrObjects; 19 | -(void)unRegisterAsListenerFromNotifications:(NSArray *)notificationNames onObjectOrObjects:(id)objectOrObjects; 20 | @end 21 | -------------------------------------------------------------------------------- /Sample/Source/NSObject+NFNotificationsProvider.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSObject+NFNotificationsProvider.m 3 | // 4 | // Created by Nicholas Furness on 4/11/14. 5 | // Copyright (c) 2014 ESRI. All rights reserved. 6 | // 7 | 8 | #import "NSObject+NFNotificationsProvider.h" 9 | 10 | @implementation NSObject (NFNotificationsProvider) 11 | #pragma mark - Registration of listeners to self 12 | -(void)registerListener:(id)listener forNotifications:(NSDictionary *)notificationSelectorsByName { 13 | [NSObject __registerListener:listener onObject:self forNotifications:notificationSelectorsByName]; 14 | } 15 | 16 | -(void)unRegisterListener:(id)listener fromNotifications:(NSArray *)notificationNames { 17 | [NSObject __unRegisterListener:listener onObject:self fromNotifications:notificationNames]; 18 | } 19 | 20 | #pragma mark - Registration of self as listener to object or objects 21 | -(void)registerAsListenerForNotifications:(NSDictionary *)notificationSelectorsByName onObjectOrObjects:(id)objectOrObjects { 22 | if (objectOrObjects) { 23 | for (id source in [objectOrObjects conformsToProtocol:@protocol(NSFastEnumeration)]?objectOrObjects:@[objectOrObjects]) { 24 | [NSObject __registerListener:self onObject:source forNotifications:notificationSelectorsByName]; 25 | } 26 | } else { 27 | [NSObject __registerListener:self onObject:nil forNotifications:notificationSelectorsByName]; 28 | } 29 | } 30 | 31 | -(void)unRegisterAsListenerFromNotifications:(NSArray *)notificationNames onObjectOrObjects:(id)objectOrObjects { 32 | if (objectOrObjects) { 33 | for (id source in [objectOrObjects conformsToProtocol:@protocol(NSFastEnumeration)]?objectOrObjects:@[objectOrObjects]) { 34 | [source __unRegisterListener:self onObject:source fromNotifications:notificationNames]; 35 | } 36 | } else { 37 | [NSObject __unRegisterListener:self onObject:nil fromNotifications:notificationNames]; 38 | } 39 | } 40 | 41 | #pragma mark - Internal Methods 42 | +(void)__registerListener:(id)listener onObject:(id)source forNotifications:(NSDictionary *)notificationSelectorsByName { 43 | for (id notificationName in notificationSelectorsByName) { 44 | [[NSNotificationCenter defaultCenter] addObserver:listener 45 | selector:NSSelectorFromString(notificationSelectorsByName[notificationName]) 46 | name:notificationName 47 | object:source]; 48 | } 49 | } 50 | 51 | +(void)__unRegisterListener:(id)listener onObject:(id)source fromNotifications:(NSArray *)notificationNames { 52 | for (id notificationName in notificationNames) { 53 | [[NSNotificationCenter defaultCenter] removeObserver:listener 54 | name:notificationName 55 | object:source]; 56 | } 57 | } 58 | @end 59 | -------------------------------------------------------------------------------- /Sample/Source/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | -------------------------------------------------------------------------------- /Sample/Source/en.lproj/MainStoryboard_iPad.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 66 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /Sample/Source/en.lproj/MainStoryboard_iPhone.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 76 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | -------------------------------------------------------------------------------- /Sample/Source/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nixta/clusterlayer-plugin-ios/8c8a0da68fcb2893585ce17600f0bbd0d07c2e27/Sample/Source/icon.png -------------------------------------------------------------------------------- /Sample/Source/icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nixta/clusterlayer-plugin-ios/8c8a0da68fcb2893585ce17600f0bbd0d07c2e27/Sample/Source/icon@2x.png -------------------------------------------------------------------------------- /Sample/Source/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // Basemaps 4 | // 5 | // Created by Nicholas Furness on 11/29/12. 6 | // Copyright (c) 2012 ESRI. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | #import "AGSSampleAppDelegate.h" 12 | 13 | int main(int argc, char *argv[]) 14 | { 15 | @autoreleasepool { 16 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AGSSampleAppDelegate class])); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /clusterlayer-plugin-ios.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nixta/clusterlayer-plugin-ios/8c8a0da68fcb2893585ce17600f0bbd0d07c2e27/clusterlayer-plugin-ios.png --------------------------------------------------------------------------------