*points))success
65 | failure:(nullable void (^)(CKGeoPointOperation *operation, NSError *error))failure;
66 |
67 | @end
68 |
69 | NS_ASSUME_NONNULL_END
70 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
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 | ClusterKit is an elegant and efficiant clustering controller for maps. Its flexible architecture make it very customizable, you can use your own algorithm and even your own map provider.
29 |
30 | ## Features
31 |
32 | + Native supports of [**MapKit**](https://developer.apple.com/documentation/mapkit), [**GoogleMaps**](https://developers.google.com/maps/documentation/ios-sdk), [**Mapbox**](https://www.mapbox.com/ios-sdk/) and [**YandexMapKit**](https://tech.yandex.com/maps/mapkit/).
33 | + Comes with 2 clustering algorithms, a Grid Based Algorithm and a Non Hierarchical Distance Based Algorithm.
34 | + Annotations are stored in a [QuadTree](https://en.wikipedia.org/wiki/Quadtree) for efficient region queries.
35 | + Cluster center can be switched to **Centroid**, **Nearest Centroid**, **Bottom**.
36 | + Handles pin **selection** as well as **drag and dropping**.
37 | + Written in Objective-C with full **Swift** interop support.
38 |
39 | |MapKit|GoogleMaps|Mapbox|
40 | |---|---|---|
41 | ||||
42 |
43 | ## Installation & Usage
44 |
45 | **Please follow the [wiki](https://github.com/hulab/ClusterKit/wiki) for integration.**
46 |
47 | If you want to try it, simply run:
48 |
49 | ```
50 | pod try ClusterKit
51 | ```
52 |
53 | Or clone the repo and run `pod install` from the [Examples](Examples) directory first.
54 | > Provide the [Google API Key](https://console.developers.google.com) in the AppDelegate in order to try it with GoogleMaps.
55 |
56 | > Provide the [Mapbox Access Token](https://www.mapbox.com/studio/account/tokens/) in the AppDelegate in order to try it with Mapbox.
57 |
58 | > Provide the [Yandex API Key](https://developer.tech.yandex.ru/) in the AppDelegate in order to try it with YandexMapKit.
59 |
60 | ## Credits
61 |
62 | Assets by [Hugo des Gayets](https://dribbble.com/hugodesgayets).
63 |
64 | Thanks [@petropavel13](https://github.com/petropavel13) for the **YandexMapKit** integration.
65 |
66 | ## License
67 |
68 | ClusterKit is available under the MIT license. See the [LICENSE](LICENSE) file for more info.
69 |
--------------------------------------------------------------------------------
/Sources/ClusterKit/include/ClusterKit/CKQuadTree.h:
--------------------------------------------------------------------------------
1 | // CKQuadTree.h
2 | //
3 | // Copyright © 2017 Hulab. All rights reserved.
4 | //
5 | // Permission is hereby granted, free of charge, to any person obtaining a copy
6 | // of this software and associated documentation files (the "Software"), to deal
7 | // in the Software without restriction, including without limitation the rights
8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | // copies of the Software, and to permit persons to whom the Software is
10 | // furnished to do so, subject to the following conditions:
11 | //
12 | // The above copyright notice and this permission notice shall be included in
13 | // all copies or substantial portions of the Software.
14 | //
15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | // THE SOFTWARE.
22 |
23 | #import
24 |
25 | NS_ASSUME_NONNULL_BEGIN
26 |
27 | /// Default node capacity
28 | #define CK_QTREE_STDCAP 4
29 |
30 | typedef struct hb_qtree hb_qtree_t;
31 |
32 | /// :nodoc:
33 | FOUNDATION_EXPORT hb_qtree_t *hb_qtree_new(MKMapRect rect, NSUInteger cap);
34 | /// :nodoc:
35 | FOUNDATION_EXPORT void hb_qtree_free(hb_qtree_t *tree);
36 | /// :nodoc:
37 | FOUNDATION_EXPORT void hb_qtree_insert(hb_qtree_t *tree, id annotation);
38 | /// :nodoc:
39 | FOUNDATION_EXPORT void hb_qtree_remove(hb_qtree_t *tree, id annotation);
40 | /// :nodoc:
41 | FOUNDATION_EXPORT void hb_qtree_clear(hb_qtree_t *tree);
42 | /// :nodoc:
43 | FOUNDATION_EXPORT void hb_qtree_find_in_range(hb_qtree_t *tree, MKMapRect range, void(^find)(idannotation));
44 |
45 | /**
46 | A quadtree is a tree data structure in which each internal node has exactly four children.
47 | It is used to partition {@see MKMapRectWorld} by recursively subdividing it into four quadrants or regions.
48 |
49 | The quadtree represents a partition of space in two dimensions by decomposing the region into four equal quadrants, subquadrants,
50 | and so on with each leaf node containing annotation corresponding to a specific rect. Each node in the tree either has maximum
51 | four children. The height of quadtrees that follow this decomposition strategy (i.e. subdividing subquadrants as long as there is
52 | interesting data in the subquadrant for which more refinement is desired) is sensitive to and dependent on the spatial distribution
53 | of interesting areas in the space being decomposed. The region quadtree is a type of trie. Regions are subdivided until each leaf
54 | contains at most a single point.
55 | */
56 | @interface CKQuadTree : NSObject
57 |
58 | /**
59 | Initializes a CKQuadTree with the given annotations.
60 |
61 | @param annotations An annotations array.
62 |
63 | @return An initialized CKQuadTree.
64 | */
65 | - (instancetype)initWithAnnotations:(NSArray> *)annotations NS_DESIGNATED_INITIALIZER;
66 |
67 | @end
68 |
69 | NS_ASSUME_NONNULL_END
70 |
--------------------------------------------------------------------------------
/Examples/Example-data/CKGeoPointOperation.m:
--------------------------------------------------------------------------------
1 | // CKGeoPointOperation.m
2 | //
3 | // Copyright © 2017 Hulab. All rights reserved.
4 | //
5 | // Permission is hereby granted, free of charge, to any person obtaining a copy
6 | // of this software and associated documentation files (the "Software"), to deal
7 | // in the Software without restriction, including without limitation the rights
8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | // copies of the Software, and to permit persons to whom the Software is
10 | // furnished to do so, subject to the following conditions:
11 | //
12 | // The above copyright notice and this permission notice shall be included in
13 | // all copies or substantial portions of the Software.
14 | //
15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | // THE SOFTWARE.
22 |
23 | #import
24 | #import
25 |
26 | #import "CKGeoPointOperation.h"
27 |
28 | @implementation CKGeoPointOperation
29 |
30 | @synthesize points = _points;
31 | @synthesize error = _error;
32 |
33 | - (void)main {
34 | NSBundle *bundle = [NSBundle bundleForClass:[self class]];
35 | NSURL *URL = [bundle URLForResource:@"stations" withExtension:@"geojson"];
36 |
37 | NSData *data = [NSData dataWithContentsOfURL:URL];
38 | NSDictionary *geoJSON = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
39 |
40 | NSError *error = nil;
41 | _points = [GeoJSONSerialization shapesFromGeoJSONFeatureCollection:geoJSON error:&error];
42 | _error = error;
43 | }
44 |
45 | - (void)setCompletionBlock:(void (^)(void))completionBlock {
46 | if (!completionBlock) {
47 | [super setCompletionBlock:nil];
48 | } else {
49 | __weak typeof(self) weakSelf = self;
50 | [super setCompletionBlock:^ {
51 | completionBlock();
52 | [weakSelf setCompletionBlock:nil];
53 | }];
54 | }
55 | }
56 |
57 | - (void)setCompletionBlockWithSuccess:(void (^)(CKGeoPointOperation *operation, NSArray *points))success
58 | failure:(void (^)(CKGeoPointOperation *operation, NSError *error))failure {
59 |
60 | __weak __typeof(self) const weakSelf = self;
61 | self.completionBlock = ^{
62 | __typeof(self) const strongSelf = weakSelf; // Retain object, so we for sure have something to pass to the success and/or failure blocks.
63 | if(strongSelf) {
64 |
65 | if (strongSelf.error) {
66 | if (failure) {
67 | dispatch_async(strongSelf.failureCallbackQueue ?: dispatch_get_main_queue(), ^{
68 | failure(strongSelf, strongSelf.error);
69 | });
70 | }
71 | } else {
72 | dispatch_async(strongSelf.successCallbackQueue ?: dispatch_get_main_queue(), ^{
73 | success(strongSelf, strongSelf.points);
74 | });
75 | }
76 | }
77 | };
78 | }
79 |
80 | @end
81 |
--------------------------------------------------------------------------------
/Examples/Example-objc/CKAppDelegate.m:
--------------------------------------------------------------------------------
1 | // CKAppDelegate.m
2 | //
3 | // Copyright © 2017 Hulab. All rights reserved.
4 | //
5 | // Permission is hereby granted, free of charge, to any person obtaining a copy
6 | // of this software and associated documentation files (the "Software"), to deal
7 | // in the Software without restriction, including without limitation the rights
8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | // copies of the Software, and to permit persons to whom the Software is
10 | // furnished to do so, subject to the following conditions:
11 | //
12 | // The above copyright notice and this permission notice shall be included in
13 | // all copies or substantial portions of the Software.
14 | //
15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | // THE SOFTWARE.
22 |
23 | #import
24 | #import
25 | #import
26 |
27 | #import "CKAppDelegate.h"
28 |
29 | @interface CKAppDelegate ()
30 |
31 | @end
32 |
33 | @implementation CKAppDelegate
34 |
35 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
36 |
37 | [GMSServices provideAPIKey:@"<#Enter your google API key#>"];
38 | [MGLAccountManager setAccessToken:@"<#Enter your Mapbox access token#>"];
39 | [YMKMapKit setApiKey:@"<#Enter your YandexMapKit access token#>"];
40 |
41 | return YES;
42 | }
43 |
44 |
45 | - (void)applicationWillResignActive:(UIApplication *)application {
46 | // 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.
47 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
48 | }
49 |
50 |
51 | - (void)applicationDidEnterBackground:(UIApplication *)application {
52 | // 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.
53 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
54 | }
55 |
56 |
57 | - (void)applicationWillEnterForeground:(UIApplication *)application {
58 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
59 | }
60 |
61 |
62 | - (void)applicationDidBecomeActive:(UIApplication *)application {
63 | // 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.
64 | }
65 |
66 |
67 | - (void)applicationWillTerminate:(UIApplication *)application {
68 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
69 | }
70 |
71 |
72 | @end
73 |
--------------------------------------------------------------------------------
/Examples/Example-swift/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | // AppDelegate.swift
2 | //
3 | // Copyright © 2017 Hulab. All rights reserved.
4 | //
5 | // Permission is hereby granted, free of charge, to any person obtaining a copy
6 | // of this software and associated documentation files (the "Software"), to deal
7 | // in the Software without restriction, including without limitation the rights
8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | // copies of the Software, and to permit persons to whom the Software is
10 | // furnished to do so, subject to the following conditions:
11 | //
12 | // The above copyright notice and this permission notice shall be included in
13 | // all copies or substantial portions of the Software.
14 | //
15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | // THE SOFTWARE.
22 |
23 | import UIKit
24 | import GoogleMaps
25 | import Mapbox
26 | import YandexMapKit
27 |
28 | @UIApplicationMain
29 | class AppDelegate: UIResponder, UIApplicationDelegate {
30 |
31 | var window: UIWindow?
32 |
33 |
34 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
35 |
36 | GMSServices.provideAPIKey("<#Enter your google API key#>")
37 | MGLAccountManager.accessToken = "<#Enter your Mapbox access token#>"
38 | YMKMapKit.setApiKey("<#Enter your YandexMapKit access token#>")
39 |
40 | return true
41 | }
42 |
43 | func applicationWillResignActive(_ application: UIApplication) {
44 | // 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.
45 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
46 | }
47 |
48 | func applicationDidEnterBackground(_ application: UIApplication) {
49 | // 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.
50 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
51 | }
52 |
53 | func applicationWillEnterForeground(_ application: UIApplication) {
54 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
55 | }
56 |
57 | func applicationDidBecomeActive(_ application: UIApplication) {
58 | // 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.
59 | }
60 |
61 | func applicationWillTerminate(_ application: UIApplication) {
62 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
63 | }
64 |
65 |
66 | }
67 |
68 |
--------------------------------------------------------------------------------
/Examples/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "size" : "20x20",
5 | "idiom" : "iphone",
6 | "filename" : "iPhone - 20pt@2x.png",
7 | "scale" : "2x"
8 | },
9 | {
10 | "size" : "20x20",
11 | "idiom" : "iphone",
12 | "filename" : "iPhone - 20pt@3x.png",
13 | "scale" : "3x"
14 | },
15 | {
16 | "idiom" : "iphone",
17 | "size" : "29x29",
18 | "scale" : "1x"
19 | },
20 | {
21 | "size" : "29x29",
22 | "idiom" : "iphone",
23 | "filename" : "iPhone - 29pt@2x.png",
24 | "scale" : "2x"
25 | },
26 | {
27 | "size" : "29x29",
28 | "idiom" : "iphone",
29 | "filename" : "iPhone - 29pt@3x.png",
30 | "scale" : "3x"
31 | },
32 | {
33 | "size" : "40x40",
34 | "idiom" : "iphone",
35 | "filename" : "iPhone - 40pt@2x.png",
36 | "scale" : "2x"
37 | },
38 | {
39 | "size" : "40x40",
40 | "idiom" : "iphone",
41 | "filename" : "iPhone - 40pt@3x.png",
42 | "scale" : "3x"
43 | },
44 | {
45 | "idiom" : "iphone",
46 | "size" : "57x57",
47 | "scale" : "1x"
48 | },
49 | {
50 | "idiom" : "iphone",
51 | "size" : "57x57",
52 | "scale" : "2x"
53 | },
54 | {
55 | "size" : "60x60",
56 | "idiom" : "iphone",
57 | "filename" : "iPhone - 60pt@2x.png",
58 | "scale" : "2x"
59 | },
60 | {
61 | "size" : "60x60",
62 | "idiom" : "iphone",
63 | "filename" : "iPhone - 60pt@3x.png",
64 | "scale" : "3x"
65 | },
66 | {
67 | "size" : "20x20",
68 | "idiom" : "ipad",
69 | "filename" : "iPad - 20pt.png",
70 | "scale" : "1x"
71 | },
72 | {
73 | "size" : "20x20",
74 | "idiom" : "ipad",
75 | "filename" : "iPad - 20pt@2x.png",
76 | "scale" : "2x"
77 | },
78 | {
79 | "size" : "29x29",
80 | "idiom" : "ipad",
81 | "filename" : "iPad - 29pt.png",
82 | "scale" : "1x"
83 | },
84 | {
85 | "size" : "29x29",
86 | "idiom" : "ipad",
87 | "filename" : "iPad - 29pt@2x.png",
88 | "scale" : "2x"
89 | },
90 | {
91 | "size" : "40x40",
92 | "idiom" : "ipad",
93 | "filename" : "iPad - 40pt.png",
94 | "scale" : "1x"
95 | },
96 | {
97 | "size" : "40x40",
98 | "idiom" : "ipad",
99 | "filename" : "iPad - 40pt@2x.png",
100 | "scale" : "2x"
101 | },
102 | {
103 | "idiom" : "ipad",
104 | "size" : "50x50",
105 | "scale" : "1x"
106 | },
107 | {
108 | "idiom" : "ipad",
109 | "size" : "50x50",
110 | "scale" : "2x"
111 | },
112 | {
113 | "idiom" : "ipad",
114 | "size" : "72x72",
115 | "scale" : "1x"
116 | },
117 | {
118 | "idiom" : "ipad",
119 | "size" : "72x72",
120 | "scale" : "2x"
121 | },
122 | {
123 | "size" : "76x76",
124 | "idiom" : "ipad",
125 | "filename" : "iPad - 76pt.png",
126 | "scale" : "1x"
127 | },
128 | {
129 | "size" : "76x76",
130 | "idiom" : "ipad",
131 | "filename" : "iPad - 76pt@2x.png",
132 | "scale" : "2x"
133 | },
134 | {
135 | "size" : "83.5x83.5",
136 | "idiom" : "ipad",
137 | "filename" : "iPad - 83.5pt.png",
138 | "scale" : "2x"
139 | },
140 | {
141 | "idiom" : "ios-marketing",
142 | "size" : "1024x1024",
143 | "scale" : "1x"
144 | }
145 | ],
146 | "info" : {
147 | "version" : 1,
148 | "author" : "xcode"
149 | }
150 | }
--------------------------------------------------------------------------------
/Sources/ClusterKit/include/ClusterKit/CKMap.h:
--------------------------------------------------------------------------------
1 | // CKMap.h
2 | //
3 | // Copyright © 2017 Hulab. All rights reserved.
4 | //
5 | // Permission is hereby granted, free of charge, to any person obtaining a copy
6 | // of this software and associated documentation files (the "Software"), to deal
7 | // in the Software without restriction, including without limitation the rights
8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | // copies of the Software, and to permit persons to whom the Software is
10 | // furnished to do so, subject to the following conditions:
11 | //
12 | // The above copyright notice and this permission notice shall be included in
13 | // all copies or substantial portions of the Software.
14 | //
15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | // THE SOFTWARE.
22 |
23 | #import
24 | #import
25 |
26 | NS_ASSUME_NONNULL_BEGIN
27 |
28 | /**
29 | The CKMap protocol is used to provide cluster instructions and get informations from a map. To use this protocol, you adopt it in any custom objects that represent a map.
30 | @discussion An object that adopts this protocol must implement all methods and properties.
31 | */
32 | @protocol CKMap
33 |
34 | @required
35 |
36 | /**
37 | The cluster manager that gonna create, delete and move clusters.
38 | */
39 | @property (nonatomic,readonly) CKClusterManager *clusterManager;
40 |
41 | /**
42 | The area currently displayed by the map view.
43 | */
44 | @property (nonatomic,readonly) MKMapRect visibleMapRect;
45 |
46 | /**
47 | * Zoom uses an exponentional scale, where zoom 0 represents the entire world as a
48 | * 256 x 256 square. Each successive zoom level increases magnification by a factor of 2. So at
49 | * zoom level 1, the world is 512x512, and at zoom level 2, the entire world is 1024x1024.
50 | */
51 | @property (nonatomic,readonly) double zoom;
52 |
53 | /**
54 | Selects the specified cluster.
55 |
56 | @param cluster The cluster object to select.
57 | @param animated If YES, animates the view selection.
58 | */
59 | - (void)selectCluster:(CKCluster *)cluster animated:(BOOL)animated;
60 |
61 | /**
62 | Deselects the specified cluster.
63 |
64 | @param cluster The cluster object to deselect.
65 | @param animated If YES, animates the view deselection.
66 | */
67 | - (void)deselectCluster:(CKCluster *)cluster animated:(BOOL)animated;
68 |
69 | /**
70 | Removes clusters from the map.
71 |
72 | @param clusters The clusters array to remove.
73 | */
74 | - (void)removeClusters:(NSArray *)clusters;
75 |
76 | /**
77 | Adds clusters from the map.
78 |
79 | @param clusters The clusters array to add.
80 | */
81 | - (void)addClusters:(NSArray *)clusters;
82 |
83 | /**
84 | Perfoms the specidied cluster animations.
85 |
86 | @param animations The animations to perfom.
87 | @param completion A block object to be executed when the move sequence ends. This block has no return value and takes a single Boolean argument that indicates whether or not the moves actually finished before the completion handler was called. This parameter may be nil.
88 | */
89 | - (void)performAnimations:(NSArray *)animations completion:(void (^__nullable)(BOOL finished))completion;
90 |
91 | @end
92 |
93 | NS_ASSUME_NONNULL_END
94 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 | All notable changes to `ClusterKit` project will be documented in this file.
3 |
4 | ---
5 |
6 | ## [0.4.1](https://github.com/hulab/ClusterKit/releases/tag/0.4.1) - July 1, 2019
7 |
8 | ### Updated
9 |
10 | - **Mapbox**: 5.0 [#75](https://github.com/hulab/ClusterKit/pull/75)
11 |
12 | ## [0.4.0](https://github.com/hulab/ClusterKit/releases/tag/0.4.0) - February 11, 2019
13 |
14 | ### Added
15 |
16 | - **Yandex Map**: Thanks to [@petropavel13](https://github.com/petropavel13) for the **YandexMapKit** integration.
17 |
18 | ## [0.3.5](https://github.com/hulab/ClusterKit/releases/tag/0.3.5) - November 14, 2018
19 |
20 | ### Updated
21 |
22 | - **Mapbox**: 4.6
23 |
24 | ## [0.3.4](https://github.com/hulab/ClusterKit/releases/tag/0.3.4) - June 4, 2018
25 |
26 | ### Updated
27 |
28 | - **Mapbox**: 4.0
29 |
30 | ## [0.3.3](https://github.com/hulab/ClusterKit/releases/tag/0.3.3) - January 9, 2018
31 |
32 | ### Updated
33 |
34 | - **Mapbox**: 3.7
35 |
36 | ### Fixed
37 |
38 | - **CKClusterManager.m**: recursion introduced by Mapbox 3.7
39 | - **GMSMapView+ClusterKit.m**: Fix [#31](https://github.com/hulab/ClusterKit/issues/31) Disappearing cluster while crossing the antimerdian.
40 | - **MKMapView+ClusterKit.m**: Fix [#31](https://github.com/hulab/ClusterKit/issues/31) Disappearing cluster while crossing the antimerdian.
41 |
42 | ## [0.3.2](https://github.com/hulab/ClusterKit/releases/tag/0.3.2) - November 13, 2017
43 |
44 | ### Fixed
45 |
46 | - **CKCluster**: Fix annotation membership.
47 |
48 | ## [0.3.1](https://github.com/hulab/ClusterKit/releases/tag/0.3.1) - October 24, 2017
49 |
50 | ### Fixed
51 |
52 | - **CKClusterManager.m**: Fix annotation selection.
53 |
54 | ## [0.3.0](https://github.com/hulab/ClusterKit/releases/tag/0.3.0) - October 18, 2017
55 |
56 |
57 | > **Breaking changes**:
58 | >
59 | > + Your model do not need to adopt the `CKAnnotation` protocol anymore, only `MKAnnotation`.
60 | >
61 | > + For **GoogleMaps**: don't forget to update the `GMSMapView+ClusterKit` files.
62 |
63 |
64 | ### Fixed
65 |
66 | - **[Issue #23](https://github.com/hulab/ClusterKit/issues/23)**: Fix flickering pin on MapKit when updating clusters.
67 | Identical clusters are no more replaced and the clusters animation have been improved to be performed by batch.
68 |
69 | ### Added
70 |
71 | - **Mapbox**: ClusterKit is now compatible with [Mapbox](https://www.mapbox.com/).
72 |
73 | ### Removed
74 |
75 | - **CKAnnotation protocol**: CKAnnotation is no more accurate since we don't replace identical clusters.
76 |
77 | ### Updated
78 |
79 | - **CKCluster**: Compute the cluster bounds. Add cluster comparison methods.
80 |
81 | ## [0.2.0](https://github.com/hulab/ClusterKit/releases/tag/0.2.0) - July 24, 2017
82 |
83 | ### Added
84 |
85 | - **CKQuadTree.m**: Drag and Drop support.
86 |
87 | ### Fixed
88 |
89 | - **CKClusterManager.m**: Fix annotation selection/deselection.
90 |
91 | ## [0.1.3](https://github.com/hulab/ClusterKit/releases/tag/0.1.3) - July 5, 2017
92 |
93 | ### Updated
94 |
95 | - **CKCluster.m**: Make the annotation array plubic.
96 |
97 | ### Fixed
98 |
99 | - **CKCluster.m**: Fix empty cluster in annotation using `CKBottomCluster`.
100 |
101 | ## [0.1.2](https://github.com/hulab/ClusterKit/releases/tag/0.1.2) - May 3, 2017
102 |
103 | ### Updated
104 |
105 | - Examples: Use geojson files as data set.
106 |
107 | ### Fixed
108 |
109 | - **CKQuadTree.m**: Fix 180th meridian spanning.
110 |
111 | ## [0.1.1](https://github.com/hulab/ClusterKit/releases/tag/0.1.1) - February 1, 2017
112 |
113 | ### Updated
114 |
115 | - README.md
116 |
117 | ### Fixed
118 |
119 | - **CKNonHierarchicalDistanceBasedAlgorithm.m**: Fix cluster region on world bounds.
120 |
121 | ## [0.1.0](https://github.com/hulab/ClusterKit/releases/tag/0.1.0) - January 18, 2017
122 |
123 | First official release.
124 |
--------------------------------------------------------------------------------
/ClusterKit.xcodeproj/xcshareddata/xcschemes/ClusterKit.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
33 |
39 |
40 |
41 |
42 |
43 |
49 |
50 |
51 |
52 |
53 |
54 |
64 |
65 |
71 |
72 |
73 |
74 |
75 |
76 |
82 |
83 |
89 |
90 |
91 |
92 |
94 |
95 |
98 |
99 |
100 |
--------------------------------------------------------------------------------
/Tests/ClusterKitTests/CKGridBasedAlgorithmTest.m:
--------------------------------------------------------------------------------
1 | // CKGridBasedAlgorithmTest.m
2 | //
3 | // Copyright © 2017 Hulab. All rights reserved.
4 | //
5 | // Permission is hereby granted, free of charge, to any person obtaining a copy
6 | // of this software and associated documentation files (the "Software"), to deal
7 | // in the Software without restriction, including without limitation the rights
8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | // copies of the Software, and to permit persons to whom the Software is
10 | // furnished to do so, subject to the following conditions:
11 | //
12 | // The above copyright notice and this permission notice shall be included in
13 | // all copies or substantial portions of the Software.
14 | //
15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | // THE SOFTWARE.
22 |
23 | #import
24 | #import
25 | #import
26 |
27 | #import "CKAnnotation.h"
28 |
29 | @interface CKGridBasedAlgorithmTest : XCTestCase
30 | @property (nonatomic,strong) id tree;
31 | @end
32 |
33 | @implementation CKGridBasedAlgorithmTest
34 |
35 | - (void)setUp {
36 | [super setUp];
37 |
38 | NSMutableArray *annotations = [NSMutableArray array];
39 |
40 | for (double x = 0; x < MKMapSizeWorld.width; x += MKMapSizeWorld.width / 100) {
41 | for (double y = 0; y < MKMapSizeWorld.height; y += MKMapSizeWorld.height / 100) {
42 |
43 | MKMapPoint point = MKMapPointMake(x, y);
44 | CKAnnotation *annotation = [CKAnnotation new];
45 | annotation.coordinate = MKCoordinateForMapPoint(point);
46 | [annotations addObject:annotation];
47 | }
48 | }
49 |
50 | self.tree = [[CKQuadTree alloc] initWithAnnotations:annotations];
51 | }
52 |
53 | - (void)tearDown {
54 | // Put teardown code here. This method is called after the invocation of each test method in the class.
55 | [super tearDown];
56 | }
57 |
58 | - (void)testZoom1Performance {
59 |
60 | CKGridBasedAlgorithm *algorithm = [CKGridBasedAlgorithm new];
61 |
62 | [self measureBlock:^{
63 | NSArray *clusters = [algorithm clustersInRect:MKMapRectWorld zoom:1 tree:self.tree];
64 | XCTAssertTrue(clusters.count, @"No cluster");
65 | }];
66 | }
67 |
68 | - (void)testZoom2Performance {
69 |
70 | CKGridBasedAlgorithm *algorithm = [CKGridBasedAlgorithm new];
71 |
72 | [self measureBlock:^{
73 | NSArray *clusters = [algorithm clustersInRect:MKMapRectWorld zoom:2 tree:self.tree];
74 | XCTAssertTrue(clusters.count, @"No cluster");
75 | }];
76 | }
77 |
78 | - (void)testZoom4Performance {
79 |
80 | CKGridBasedAlgorithm *algorithm = [CKGridBasedAlgorithm new];
81 |
82 | [self measureBlock:^{
83 | NSArray *clusters = [algorithm clustersInRect:MKMapRectWorld zoom:4 tree:self.tree];
84 | XCTAssertTrue(clusters.count, @"No cluster");
85 | }];
86 | }
87 |
88 | - (void)testZoom8Performance {
89 |
90 | CKGridBasedAlgorithm *algorithm = [CKGridBasedAlgorithm new];
91 |
92 | [self measureBlock:^{
93 | NSArray *clusters = [algorithm clustersInRect:MKMapRectWorld zoom:8 tree:self.tree];
94 | XCTAssertTrue(clusters.count, @"No cluster");
95 | }];
96 | }
97 |
98 | - (void)testZoom16Performance {
99 |
100 | CKGridBasedAlgorithm *algorithm = [CKGridBasedAlgorithm new];
101 |
102 | [self measureBlock:^{
103 | NSArray *clusters = [algorithm clustersInRect:MKMapRectWorld zoom:16 tree:self.tree];
104 | XCTAssertTrue(clusters.count, @"No cluster");
105 | }];
106 | }
107 |
108 | @end
109 |
--------------------------------------------------------------------------------
/Tests/ClusterKitTests/CKQuadTreeTest.m:
--------------------------------------------------------------------------------
1 | // CKQuadTreeTest.m
2 | //
3 | // Copyright © 2017 Hulab. All rights reserved.
4 | //
5 | // Permission is hereby granted, free of charge, to any person obtaining a copy
6 | // of this software and associated documentation files (the "Software"), to deal
7 | // in the Software without restriction, including without limitation the rights
8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | // copies of the Software, and to permit persons to whom the Software is
10 | // furnished to do so, subject to the following conditions:
11 | //
12 | // The above copyright notice and this permission notice shall be included in
13 | // all copies or substantial portions of the Software.
14 | //
15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | // THE SOFTWARE.
22 |
23 | #import
24 | #import
25 |
26 | #import "CKAnnotation.h"
27 |
28 | @interface CKQuadTreeTest : XCTestCase
29 | @property (nonatomic,strong) NSArray *annotations;
30 | @property (nonatomic,assign) hb_qtree_t *tree;
31 | @end
32 |
33 | @implementation CKQuadTreeTest
34 |
35 | - (void)setUp {
36 | [super setUp];
37 | self.tree = hb_qtree_new(MKMapRectWorld, CK_QTREE_STDCAP);
38 |
39 | NSMutableArray *annotations = [NSMutableArray array];
40 |
41 | for (double x = 0; x < MKMapSizeWorld.width; x += MKMapSizeWorld.width / 100) {
42 | for (double y = 0; y < MKMapSizeWorld.height; y += MKMapSizeWorld.height / 100) {
43 |
44 | MKMapPoint point = MKMapPointMake(x, y);
45 | CKAnnotation *annotation = [CKAnnotation new];
46 | annotation.coordinate = MKCoordinateForMapPoint(point);
47 | [annotations addObject:annotation];
48 |
49 | hb_qtree_insert(self.tree, annotation);
50 | }
51 | }
52 | self.annotations = annotations.copy;
53 | }
54 |
55 | - (void)tearDown {
56 | hb_qtree_free(self.tree);
57 | [super tearDown];
58 | }
59 |
60 | - (void)testQueryPerformance {
61 | MKMapRect rect = MKMapRectInset(MKMapRectWorld, MKMapSizeWorld.width / 4, MKMapSizeWorld.height / 4);
62 |
63 | [self measureBlock:^{
64 | hb_qtree_find_in_range(self.tree, rect, ^(id _Nonnull annotation) {});
65 | }];
66 | }
67 |
68 | - (void)testQueryResult {
69 |
70 | NSMutableArray *annotations = [NSMutableArray array];
71 |
72 | MKMapRect nw, ne, sw, se;
73 |
74 | MKMapRectDivide(MKMapRectWorld, &nw, &ne, MKMapSizeWorld.width / 2, CGRectMaxXEdge);
75 | MKMapRectDivide(nw, &nw, &sw, MKMapSizeWorld.height / 2, CGRectMaxYEdge);
76 | MKMapRectDivide(ne, &ne, &se, MKMapSizeWorld.height / 2, CGRectMaxYEdge);
77 |
78 | hb_qtree_find_in_range(self.tree, nw, ^(id _Nonnull annotation) {
79 | [annotations addObject:annotation];
80 | });
81 |
82 | XCTAssertTrue(annotations.count == (self.annotations.count / 4), @"Tree should have find a quarter of annotations");
83 |
84 | hb_qtree_find_in_range(self.tree, ne, ^(id _Nonnull annotation) {
85 | [annotations addObject:annotation];
86 | });
87 |
88 | XCTAssertTrue(annotations.count == (self.annotations.count / 2), @"Tree should have find a quarter of annotations");
89 |
90 | hb_qtree_find_in_range(self.tree, sw, ^(id _Nonnull annotation) {
91 | [annotations addObject:annotation];
92 | });
93 |
94 | XCTAssertTrue(annotations.count == 3 * (self.annotations.count / 4), @"Tree should have find a quarter of annotations");
95 |
96 | hb_qtree_find_in_range(self.tree, se, ^(id _Nonnull annotation) {
97 | [annotations addObject:annotation];
98 | });
99 |
100 | XCTAssertTrue(annotations.count == self.annotations.count, @"Tree should have find a quarter of annotations");
101 | }
102 |
103 | @end
104 |
--------------------------------------------------------------------------------
/Tests/ClusterKitTests/CKNonHierarchicalDistanceBasedAlgorithmTest.m:
--------------------------------------------------------------------------------
1 | // CKNonHierarchicalDistanceBasedAlgorithmTest.m
2 | //
3 | // Copyright © 2017 Hulab. All rights reserved.
4 | //
5 | // Permission is hereby granted, free of charge, to any person obtaining a copy
6 | // of this software and associated documentation files (the "Software"), to deal
7 | // in the Software without restriction, including without limitation the rights
8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | // copies of the Software, and to permit persons to whom the Software is
10 | // furnished to do so, subject to the following conditions:
11 | //
12 | // The above copyright notice and this permission notice shall be included in
13 | // all copies or substantial portions of the Software.
14 | //
15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | // THE SOFTWARE.
22 |
23 | #import
24 | #import
25 | #import
26 |
27 | #import "CKAnnotation.h"
28 |
29 | @interface CKNonHierarchicalDistanceBasedAlgorithmTest : XCTestCase
30 | @property (nonatomic,strong) id tree;
31 | @end
32 |
33 | @implementation CKNonHierarchicalDistanceBasedAlgorithmTest
34 |
35 | - (void)setUp {
36 | [super setUp];
37 |
38 | NSMutableArray *annotations = [NSMutableArray array];
39 |
40 | for (double x = 0; x < MKMapSizeWorld.width; x += MKMapSizeWorld.width / 100) {
41 | for (double y = 0; y < MKMapSizeWorld.height; y += MKMapSizeWorld.height / 100) {
42 |
43 | MKMapPoint point = MKMapPointMake(x, y);
44 | CKAnnotation *annotation = [CKAnnotation new];
45 | annotation.coordinate = MKCoordinateForMapPoint(point);
46 | [annotations addObject:annotation];
47 | }
48 | }
49 |
50 | self.tree = [[CKQuadTree alloc] initWithAnnotations:annotations];
51 | }
52 |
53 | - (void)tearDown {
54 | // Put teardown code here. This method is called after the invocation of each test method in the class.
55 | [super tearDown];
56 | }
57 |
58 | - (void)testZoom1Performance {
59 |
60 | CKNonHierarchicalDistanceBasedAlgorithm *algorithm = [CKNonHierarchicalDistanceBasedAlgorithm new];
61 |
62 | [self measureBlock:^{
63 | NSArray *clusters = [algorithm clustersInRect:MKMapRectWorld zoom:1 tree:self.tree];
64 | XCTAssertTrue(clusters.count, @"No cluster");
65 | }];
66 | }
67 |
68 | - (void)testZoom2Performance {
69 |
70 | CKNonHierarchicalDistanceBasedAlgorithm *algorithm = [CKNonHierarchicalDistanceBasedAlgorithm new];
71 |
72 | [self measureBlock:^{
73 | NSArray *clusters = [algorithm clustersInRect:MKMapRectWorld zoom:2 tree:self.tree];
74 | XCTAssertTrue(clusters.count, @"No cluster");
75 | }];
76 | }
77 |
78 | - (void)testZoom4Performance {
79 |
80 | CKNonHierarchicalDistanceBasedAlgorithm *algorithm = [CKNonHierarchicalDistanceBasedAlgorithm new];
81 |
82 | [self measureBlock:^{
83 | NSArray *clusters = [algorithm clustersInRect:MKMapRectWorld zoom:4 tree:self.tree];
84 | XCTAssertTrue(clusters.count, @"No cluster");
85 | }];
86 | }
87 |
88 | - (void)testZoom8Performance {
89 |
90 | CKNonHierarchicalDistanceBasedAlgorithm *algorithm = [CKNonHierarchicalDistanceBasedAlgorithm new];
91 |
92 | [self measureBlock:^{
93 | NSArray *clusters = [algorithm clustersInRect:MKMapRectWorld zoom:8 tree:self.tree];
94 | XCTAssertTrue(clusters.count, @"No cluster");
95 | }];
96 | }
97 |
98 | - (void)testZoom16Performance {
99 |
100 | CKNonHierarchicalDistanceBasedAlgorithm *algorithm = [CKNonHierarchicalDistanceBasedAlgorithm new];
101 |
102 | [self measureBlock:^{
103 | NSArray *clusters = [algorithm clustersInRect:MKMapRectWorld zoom:16 tree:self.tree];
104 | XCTAssertTrue(clusters.count, @"No cluster");
105 | }];
106 | }
107 |
108 | @end
109 |
--------------------------------------------------------------------------------
/Examples/Example-objc/GoogleMaps/CKGoogleMapsViewController.m:
--------------------------------------------------------------------------------
1 | // CKGoogleMapsViewController.m
2 | //
3 | // Copyright © 2017 Hulab. All rights reserved.
4 | //
5 | // Permission is hereby granted, free of charge, to any person obtaining a copy
6 | // of this software and associated documentation files (the "Software"), to deal
7 | // in the Software without restriction, including without limitation the rights
8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | // copies of the Software, and to permit persons to whom the Software is
10 | // furnished to do so, subject to the following conditions:
11 | //
12 | // The above copyright notice and this permission notice shall be included in
13 | // all copies or substantial portions of the Software.
14 | //
15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | // THE SOFTWARE.
22 |
23 | #import
24 |
25 | #import
26 | #import
27 |
28 | #import "GMSMapView+ClusterKit.h"
29 |
30 | #import "CKGoogleMapsViewController.h"
31 |
32 | @interface CKGoogleMapsViewController ()
33 | @property (weak, nonatomic) IBOutlet GMSMapView *mapView;
34 | @end
35 |
36 | @implementation CKGoogleMapsViewController
37 |
38 | - (void)viewDidLoad {
39 | [super viewDidLoad];
40 |
41 | self.mapView.delegate = self;
42 | self.mapView.settings.compassButton = YES;
43 |
44 | CKNonHierarchicalDistanceBasedAlgorithm *algorithm = [CKNonHierarchicalDistanceBasedAlgorithm new];
45 | algorithm.cellSize = 200;
46 |
47 | self.mapView.clusterManager.algorithm = algorithm;
48 | self.mapView.clusterManager.marginFactor = 1;
49 | self.mapView.dataSource = self;
50 |
51 | [self loadData];
52 | }
53 |
54 | - (void)loadData {
55 | CKGeoPointOperation *operation = [[CKGeoPointOperation alloc] init];
56 |
57 | [operation setCompletionBlockWithSuccess:^(CKGeoPointOperation * _Nonnull operation, NSArray *points) {
58 | self.mapView.clusterManager.annotations = points;
59 | } failure:nil];
60 |
61 | [operation start];
62 | }
63 |
64 | #pragma mark
65 |
66 | - (GMSMarker *)mapView:(GMSMapView *)mapView markerForCluster:(CKCluster *)cluster {
67 | GMSMarker *marker = [GMSMarker markerWithPosition:cluster.coordinate];
68 | if(cluster.count > 1) {
69 | marker.icon = [UIImage imageNamed:@"cluster"];
70 | } else {
71 | marker.icon = [UIImage imageNamed:@"marker"];
72 | marker.title = cluster.title;
73 | marker.draggable = YES;
74 | }
75 |
76 | return marker;
77 | }
78 |
79 | #pragma mark - How To Update Clusters
80 |
81 | - (void)mapView:(GMSMapView *)mapView idleAtCameraPosition:(GMSCameraPosition *)position {
82 | [mapView.clusterManager updateClustersIfNeeded];
83 | }
84 |
85 | #pragma mark - How To Handle Selection/Deselection
86 |
87 | - (BOOL)mapView:(GMSMapView *)mapView didTapMarker:(GMSMarker *)marker {
88 | if (marker.cluster.count > 1) {
89 | UIEdgeInsets padding = UIEdgeInsetsMake(40, 20, 44, 20);
90 | GMSCameraUpdate *cameraUpdate = [GMSCameraUpdate fitCluster:marker.cluster withEdgeInsets:padding];
91 | [mapView animateWithCameraUpdate:cameraUpdate];
92 | return YES;
93 | }
94 | return NO;
95 | }
96 |
97 | - (UIView *)mapView:(GMSMapView *)mapView markerInfoWindow:(GMSMarker *)marker {
98 | [mapView.clusterManager selectAnnotation:marker.cluster.firstAnnotation animated:NO];
99 | return nil;
100 | }
101 |
102 | - (void)mapView:(GMSMapView *)mapView didCloseInfoWindowOfMarker:(GMSMarker *)marker {
103 | [mapView.clusterManager deselectAnnotation:marker.cluster.firstAnnotation animated:NO];
104 | }
105 |
106 | #pragma mark - How To Handle Drag and Drop
107 |
108 | - (void)mapView:(GMSMapView *)mapView didEndDraggingMarker:(GMSMarker *)marker {
109 | marker.cluster.firstAnnotation.coordinate = marker.position;
110 | }
111 |
112 | @end
113 |
--------------------------------------------------------------------------------
/Sources/ClusterKit/Algorithm/CKNonHierarchicalDistanceBasedAlgorithm.m:
--------------------------------------------------------------------------------
1 | // CKNonHierarchicalDistanceBasedAlgorithm.m
2 | //
3 | // Copyright © 2017 Hulab. All rights reserved.
4 | //
5 | // Permission is hereby granted, free of charge, to any person obtaining a copy
6 | // of this software and associated documentation files (the "Software"), to deal
7 | // in the Software without restriction, including without limitation the rights
8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | // copies of the Software, and to permit persons to whom the Software is
10 | // furnished to do so, subject to the following conditions:
11 | //
12 | // The above copyright notice and this permission notice shall be included in
13 | // all copies or substantial portions of the Software.
14 | //
15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | // THE SOFTWARE.
22 |
23 | #import
24 |
25 | @interface CKCandidate : NSObject
26 | @property (nonatomic) CGFloat distance;
27 | @property (nonatomic, strong) CKCluster *cluster;
28 | @end
29 |
30 | MKMapRect CKCreateRectFromSpan(CLLocationCoordinate2D center, double span);
31 |
32 | @implementation CKNonHierarchicalDistanceBasedAlgorithm
33 |
34 | - (instancetype)init {
35 | self = [super init];
36 | if (self) {
37 | self.cellSize = 100;
38 | }
39 | return self;
40 | }
41 |
42 | - (NSArray *)clustersInRect:(MKMapRect)rect zoom:(double)zoom tree:(id)tree {
43 |
44 | // The width and height of the square around a point that we'll consider later
45 | CGFloat zoomSpecificSpan = 100 * self.cellSize / pow(2, zoom + 8);
46 |
47 | NSMutableArray *clusters = [[NSMutableArray alloc] init];
48 |
49 | NSMapTable, CKCandidate *> *visited = [NSMapTable strongToStrongObjectsMapTable];
50 |
51 | @synchronized(tree) {
52 |
53 | NSArray *annotations = [tree annotationsInRect:rect];
54 |
55 | for (id annotation in annotations) {
56 |
57 | if ([visited objectForKey:annotation]) continue;
58 |
59 | CKCluster *cluster = [self clusterWithCoordinate:annotation.coordinate];
60 | [clusters addObject:cluster];
61 |
62 | MKMapRect clusterRect = CKCreateRectFromSpan(annotation.coordinate, zoomSpecificSpan);
63 | NSArray *neighbors = [tree annotationsInRect:clusterRect];
64 |
65 | for (id neighbor in neighbors) {
66 |
67 | CKCandidate *candidate = [visited objectForKey:neighbor];
68 |
69 | CGFloat distance = CKDistance(neighbor.coordinate, cluster.coordinate);
70 |
71 | if (candidate) {
72 | if (candidate.distance < distance) continue;
73 | [candidate.cluster removeAnnotation:neighbor];
74 | } else {
75 | candidate = [[CKCandidate alloc] init];
76 | [visited setObject:candidate forKey:neighbor];
77 | }
78 |
79 | candidate.cluster = cluster;
80 | candidate.distance = distance;
81 | [cluster addAnnotation:neighbor];
82 | }
83 | }
84 | }
85 |
86 | return clusters;
87 | }
88 |
89 | @end
90 |
91 | @implementation CKCandidate
92 |
93 | @end
94 |
95 | MKMapRect CKCreateRectFromSpan(CLLocationCoordinate2D center, CLLocationDegrees span) {
96 | double halfSpan = span / 2;
97 |
98 | CLLocationDegrees latitude = MIN(center.latitude + halfSpan, 90);
99 | CLLocationDegrees longitude = MAX(center.longitude - halfSpan, -180);
100 | MKMapPoint a = MKMapPointForCoordinate(CLLocationCoordinate2DMake(latitude, longitude));
101 |
102 | latitude = MAX(center.latitude - halfSpan, -90);
103 | longitude = MIN(center.longitude + halfSpan, 180);
104 | MKMapPoint b = MKMapPointForCoordinate(CLLocationCoordinate2DMake(latitude, longitude));
105 |
106 | return MKMapRectMake(MIN(a.x,b.x), MIN(a.y,b.y), ABS(a.x-b.x), ABS(a.y-b.y));
107 | }
108 |
--------------------------------------------------------------------------------
/Sources/ClusterKit/MapKit/MKMapView+ClusterKit.m:
--------------------------------------------------------------------------------
1 | // MKMapView+ClusterKit.m
2 | //
3 | // Copyright © 2017 Hulab. All rights reserved.
4 | //
5 | // Permission is hereby granted, free of charge, to any person obtaining a copy
6 | // of this software and associated documentation files (the "Software"), to deal
7 | // in the Software without restriction, including without limitation the rights
8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | // copies of the Software, and to permit persons to whom the Software is
10 | // furnished to do so, subject to the following conditions:
11 | //
12 | // The above copyright notice and this permission notice shall be included in
13 | // all copies or substantial portions of the Software.
14 | //
15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | // THE SOFTWARE.
22 |
23 | #if __has_include()
24 | #import
25 | #endif
26 |
27 | #import
28 | #import
29 |
30 | @implementation MKMapView (ClusterKit)
31 |
32 | #if __has_include()
33 |
34 | - (void)showCluster:(CKCluster *)cluster animated:(BOOL)animated {
35 | [self showCluster:cluster edgePadding:UIEdgeInsetsZero animated:animated];
36 | }
37 |
38 | - (void)showCluster:(CKCluster *)cluster edgePadding:(UIEdgeInsets)insets animated:(BOOL)animated {
39 | MKMapRect zoomRect = MKMapRectNull;
40 | for (id annotation in cluster) {
41 | zoomRect = MKMapRectByAddingPoint(zoomRect, MKMapPointForCoordinate(annotation.coordinate));
42 | }
43 | [self setVisibleMapRect:zoomRect edgePadding:insets animated:animated];
44 | }
45 |
46 | #endif
47 |
48 | #pragma mark -
49 |
50 | - (CKClusterManager *)clusterManager {
51 | CKClusterManager *clusterManager = objc_getAssociatedObject(self, @selector(clusterManager));
52 | if (!clusterManager) {
53 | clusterManager = [CKClusterManager new];
54 | clusterManager.map = self;
55 | objc_setAssociatedObject(self, @selector(clusterManager), clusterManager, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
56 | }
57 | return clusterManager;
58 | }
59 |
60 | - (double)zoom {
61 | return log2(360 * self.frame.size.width / (256 * self.region.span.longitudeDelta));
62 | }
63 |
64 | - (void)addClusters:(NSArray *)clusters {
65 | [self addAnnotations:clusters];
66 | }
67 |
68 | - (void)removeClusters:(NSArray *)clusters {
69 | [self removeAnnotations:clusters];
70 | }
71 |
72 | - (void)selectCluster:(CKCluster *)cluster animated:(BOOL)animated {
73 | if (![self.selectedAnnotations containsObject:cluster]) {
74 | [self selectAnnotation:cluster animated:animated];
75 | }
76 | }
77 |
78 | - (void)deselectCluster:(CKCluster *)cluster animated:(BOOL)animated {
79 | if ([self.selectedAnnotations containsObject:cluster]) {
80 | [self deselectAnnotation:cluster animated:animated];
81 | }
82 | }
83 |
84 | - (void)performAnimations:(NSArray *)animations completion:(void (^__nullable)(BOOL finished))completion {
85 |
86 | for (CKClusterAnimation *animation in animations) {
87 | animation.cluster.coordinate = animation.from;
88 | }
89 |
90 | void (^animationsBlock)(void) = ^{};
91 |
92 | for (CKClusterAnimation *animation in animations) {
93 | animationsBlock = ^{
94 | animationsBlock();
95 | animation.cluster.coordinate = animation.to;
96 | };
97 | }
98 |
99 | if ([self.clusterManager.delegate respondsToSelector:@selector(clusterManager:performAnimations:completion:)]) {
100 | [self.clusterManager.delegate clusterManager:self.clusterManager
101 | performAnimations:animationsBlock
102 | completion:^(BOOL finished) {
103 | if (completion) completion(finished);
104 | }];
105 | } else {
106 | #if __has_include()
107 | [UIView animateWithDuration:self.clusterManager.animationDuration
108 | delay:0
109 | options:self.clusterManager.animationOptions
110 | animations:animationsBlock
111 | completion:completion];
112 | #endif
113 | }
114 | }
115 |
116 | @end
117 |
--------------------------------------------------------------------------------
/Sources/GoogleMaps/GMSMapView+ClusterKit.h:
--------------------------------------------------------------------------------
1 | // GMSMapView+ClusterKit.h
2 | //
3 | // Copyright © 2017 Hulab. All rights reserved.
4 | //
5 | // Permission is hereby granted, free of charge, to any person obtaining a copy
6 | // of this software and associated documentation files (the "Software"), to deal
7 | // in the Software without restriction, including without limitation the rights
8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | // copies of the Software, and to permit persons to whom the Software is
10 | // furnished to do so, subject to the following conditions:
11 | //
12 | // The above copyright notice and this permission notice shall be included in
13 | // all copies or substantial portions of the Software.
14 | //
15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | // THE SOFTWARE.
22 |
23 | #import
24 | #import
25 | #import
26 |
27 | NS_ASSUME_NONNULL_BEGIN
28 |
29 | /**
30 | The GMSMapViewDataSource protocol is adopted by an object that mediates the GMSMapView’s data. The data source provides the markers that represent clusters on map.
31 | */
32 | @protocol GMSMapViewDataSource
33 |
34 | @optional
35 | /**
36 | Asks the data source for a marker that represent the given cluster.
37 |
38 | @param mapView A map view object requesting the marker.
39 | @param cluster The cluster to represent.
40 |
41 | @return An object inheriting from GMSMarker that the map view can use for the specified cluster.
42 | */
43 | - (__kindof GMSMarker *)mapView:(GMSMapView *)mapView markerForCluster:(CKCluster *)cluster;
44 |
45 | @end
46 |
47 | /**
48 | GMSMarker category adopting the CKAnnotation protocol.
49 | */
50 | @interface GMSMarker (ClusterKit)
51 |
52 | /**
53 | The cluster that the marker is related to.
54 | */
55 | @property (nonatomic, weak, nullable) CKCluster *cluster;
56 |
57 | @end
58 |
59 | /**
60 | GMSMapView category adopting the CKMap protocol.
61 | */
62 | @interface GMSMapView (ClusterKit)
63 |
64 | /**
65 | Data source instance that adopt the GMSMapViewDataSource.
66 | */
67 | @property(nonatomic, weak) IBOutlet id dataSource;
68 |
69 | /**
70 | Returns the marker representing the given cluster.
71 |
72 | @param cluster The cluster for which to return the corresponding marker.
73 |
74 | @return The value associated with cluster, or nil if no value is associated with cluster.
75 | */
76 | - (nullable __kindof GMSMarker *)markerForCluster:(CKCluster *)cluster;
77 |
78 | @end
79 |
80 | /**
81 | GMSCameraUpdate for modifying the camera to show the content of a cluster.
82 | */
83 | @interface GMSCameraUpdate (ClusterKit)
84 |
85 | /**
86 | Returns a GMSCameraUpdate that transforms the camera such that the specified cluster are centered on screen at the greatest possible zoom level. The bounds will have a default padding of 64 points.
87 | The returned camera update will set the camera's bearing and tilt to their default zero values (i.e., facing north and looking directly at the Earth).
88 |
89 | @param cluster The cluster to fit.
90 |
91 | @return The camera update that fit the given cluster.
92 | */
93 | + (GMSCameraUpdate *)fitCluster:(CKCluster *)cluster;
94 |
95 | /**
96 | This is similar to fitCluster: but allows specifying the padding (in points) in order to inset the bounding box from the view's edges.
97 |
98 | @param cluster The cluster to fit.
99 | @param padding The padding that inset the bounding box. If the requested padding is larger than the view size in either the vertical or horizontal direction the map will be maximally zoomed out.
100 |
101 | @return The camera update that fit the given cluster.
102 | */
103 | + (GMSCameraUpdate *)fitCluster:(CKCluster *)cluster
104 | withPadding:(CGFloat)padding;
105 |
106 | /**
107 | This is similar to fitCluster: but allows specifying edge insets in order to inset the bounding box from the view's edges.
108 |
109 | @param cluster The cluster to fit.
110 | @param edgeInsets The edge insets of the bounding box. If the requested edge insets are larger than the view size in either the vertical or horizontal direction the map will be maximally zoomed out.
111 |
112 | @return The camera update that fit the given cluster.
113 | */
114 | + (GMSCameraUpdate *)fitCluster:(CKCluster *)cluster
115 | withEdgeInsets:(UIEdgeInsets)edgeInsets;
116 |
117 | @end
118 |
119 | NS_ASSUME_NONNULL_END
120 |
--------------------------------------------------------------------------------
/Examples/Example-swift/GoogleMapsViewController.swift:
--------------------------------------------------------------------------------
1 | // GoogleMapsViewController.swift
2 | //
3 | // Copyright © 2017 Hulab. All rights reserved.
4 | //
5 | // Permission is hereby granted, free of charge, to any person obtaining a copy
6 | // of this software and associated documentation files (the "Software"), to deal
7 | // in the Software without restriction, including without limitation the rights
8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | // copies of the Software, and to permit persons to whom the Software is
10 | // furnished to do so, subject to the following conditions:
11 | //
12 | // The above copyright notice and this permission notice shall be included in
13 | // all copies or substantial portions of the Software.
14 | //
15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | // THE SOFTWARE.
22 |
23 | import UIKit
24 | import GoogleMaps
25 | import ClusterKit
26 | import ExampleData
27 |
28 | class GoogleMapsViewController: UIViewController, GMSMapViewDelegate, GMSMapViewDataSource {
29 |
30 | @IBOutlet weak var mapView: GMSMapView!
31 |
32 | override func viewDidLoad() {
33 | super.viewDidLoad()
34 |
35 | mapView.delegate = self
36 | mapView.settings.compassButton = true
37 |
38 | let algorithm = CKNonHierarchicalDistanceBasedAlgorithm()
39 | algorithm.cellSize = 200
40 |
41 | mapView.clusterManager.algorithm = algorithm
42 | mapView.clusterManager.marginFactor = 1
43 | mapView.dataSource = self
44 |
45 | let paris = CLLocationCoordinate2D(latitude: 48.853, longitude: 2.35)
46 | let update = GMSCameraUpdate.setTarget(paris)
47 | mapView.moveCamera(update)
48 |
49 | loadData()
50 | }
51 |
52 | override func didReceiveMemoryWarning() {
53 | super.didReceiveMemoryWarning()
54 | // Dispose of any resources that can be recreated.
55 | }
56 |
57 | func loadData() {
58 | let operation = CKGeoPointOperation()
59 |
60 | operation.setCompletionBlockWithSuccess({ (_, points) in
61 | self.mapView.clusterManager.annotations = points
62 | })
63 |
64 | operation.start()
65 | }
66 |
67 | // MARK: GMSMapViewDataSource
68 |
69 | func mapView(_ mapView: GMSMapView, markerFor cluster: CKCluster) -> GMSMarker {
70 | let marker = GMSMarker(position: cluster.coordinate)
71 |
72 | if cluster.count > 1 {
73 | marker.icon = UIImage(named: "cluster")
74 | } else {
75 | marker.icon = UIImage(named: "marker")
76 | marker.title = cluster.title
77 | marker.isDraggable = true
78 | }
79 |
80 | return marker;
81 | }
82 |
83 | // MARK: - How To Update Clusters
84 |
85 | func mapView(_ mapView: GMSMapView, idleAt position: GMSCameraPosition) {
86 | mapView.clusterManager.updateClustersIfNeeded()
87 | }
88 |
89 | // MARK: - How To Handle Selection/Deselection
90 |
91 | func mapView(_ mapView: GMSMapView, didTap marker: GMSMarker) -> Bool {
92 |
93 | if let cluster = marker.cluster, cluster.count > 1 {
94 |
95 | let padding = UIEdgeInsets.init(top: 40, left: 20, bottom: 44, right: 20)
96 | let cameraUpdate = GMSCameraUpdate.fit(cluster, with: padding)
97 | mapView.animate(with: cameraUpdate)
98 | return true
99 | }
100 | return false
101 | }
102 |
103 | public func mapView(_ mapView: GMSMapView, markerInfoWindow marker: GMSMarker) -> UIView? {
104 |
105 | if let annotation = marker.cluster?.firstAnnotation {
106 | mapView.clusterManager.selectAnnotation(annotation, animated: false)
107 | }
108 | return nil
109 | }
110 |
111 | func mapView(_ mapView: GMSMapView, didCloseInfoWindowOf marker: GMSMarker) {
112 |
113 | if let annotation = marker.cluster?.firstAnnotation {
114 | mapView.clusterManager.deselectAnnotation(annotation, animated: false)
115 | }
116 | }
117 |
118 | // MARK: - How To Handle Drag and Drop
119 |
120 | func mapView(_ mapView: GMSMapView, didEndDragging marker: GMSMarker) {
121 |
122 | if let annotation = marker.cluster?.firstAnnotation as? MKPointAnnotation {
123 | annotation.coordinate = marker.position
124 | }
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/Examples/Example-swift/YandexMapViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // YandexMapViewController.swift
3 | // Example-swift
4 | //
5 | // Created by petropavel on 24/01/2019.
6 | // Copyright © 2019 Hulab. All rights reserved.
7 | //
8 |
9 | import YandexMapKit
10 | import ClusterKit
11 | import ExampleData
12 |
13 | class YandexMapViewController: UIViewController, YMKMapViewDataSource,
14 | YMKMapLoadedListener,
15 | YMKMapCameraListener,
16 | YMKMapObjectTapListener,
17 | YMKMapObjectDragListener {
18 |
19 | @IBOutlet weak var mapView: YMKMapView!
20 |
21 | private var isMapLoaded = false
22 |
23 | override func viewDidLoad() {
24 | super.viewDidLoad()
25 |
26 | mapView.dataSource = self
27 |
28 | let map = mapView.mapWindow.map
29 |
30 | map.setMapLoadedListenerWith(self)
31 | map.addCameraListener(with: self)
32 |
33 | let algorithm = CKNonHierarchicalDistanceBasedAlgorithm()
34 | algorithm.cellSize = 200
35 |
36 | mapView.clusterManager.algorithm = algorithm
37 | mapView.clusterManager.marginFactor = 1
38 |
39 | let paris = YMKPoint(latitude: 48.853, longitude: 2.35)
40 |
41 | map.move(with: YMKCameraPosition(target: paris, zoom: 0, azimuth: 0, tilt: 0))
42 |
43 | loadData()
44 | }
45 |
46 | func loadData() {
47 | let operation = CKGeoPointOperation()
48 |
49 | operation.setCompletionBlockWithSuccess({ (_, points) in
50 | self.mapView.clusterManager.annotations = points
51 | })
52 |
53 | operation.start()
54 | }
55 |
56 | // MARK: YMKMapViewDataSource
57 |
58 | func mapView(_ mapView: YMKMapView, placemarkFor cluster: CKCluster) -> YMKPlacemarkMapObject {
59 | let point = YMKPoint(coordinate: cluster.coordinate)
60 | let placemark: YMKPlacemarkMapObject
61 |
62 | let mapObjects = mapView.mapWindow.map.mapObjects
63 |
64 | if cluster.count > 1 {
65 | placemark = mapObjects.addPlacemark(with: point, image: #imageLiteral(resourceName: "cluster"))
66 | } else {
67 | placemark = mapObjects.addPlacemark(with: point, image: #imageLiteral(resourceName: "marker"))
68 | }
69 |
70 | placemark.isDraggable = true
71 | placemark.setDragListenerWith(self)
72 | placemark.addTapListener(with: self)
73 |
74 | return placemark
75 | }
76 |
77 | // MARK: YMKMapLoadedListener
78 |
79 | func onMapLoaded(with statistics: YMKMapLoadStatistics) {
80 | isMapLoaded = true
81 | }
82 |
83 | // MARK: - How To Update Clusters
84 |
85 | // MARK: YMKMapCameraListener
86 |
87 | func onCameraPositionChanged(with map: YMKMap,
88 | cameraPosition: YMKCameraPosition,
89 | cameraUpdateSource: YMKCameraUpdateSource,
90 | finished: Bool) {
91 |
92 | guard isMapLoaded, finished else {
93 | return
94 | }
95 |
96 | mapView.clusterManager.updateClustersIfNeeded()
97 |
98 | // workaround (https://github.com/yandex/mapkit-ios-demo/issues/23)
99 | // we need to wait some time to get actual visibleRegion for clusterization
100 | // DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) {
101 | // self.mapView.clusterManager.updateClustersIfNeeded()
102 | // }
103 | }
104 |
105 | // MARK: - How To Handle Selection/Deselection
106 |
107 | // MARK: YMKMapObjectTapListener
108 |
109 | func onMapObjectTap(with mapObject: YMKMapObject, point: YMKPoint) -> Bool {
110 | guard let placemark = mapObject as? YMKPlacemarkMapObject,
111 | let cluster = placemark.cluster else {
112 | return false
113 | }
114 |
115 | let padding = UIEdgeInsets(top: 40, left: 20, bottom: 44, right: 20)
116 |
117 | if cluster.count > 1 {
118 | mapView.mapWindow.map.move(with: mapView.cameraPositionThatFits(cluster, edgePadding: padding),
119 | animationType: YMKAnimation(type: .smooth, duration: 0.5),
120 | cameraCallback: { completed in
121 | if completed {
122 | self.mapView.clusterManager.updateClustersIfNeeded()
123 | }
124 | })
125 | return true
126 | } else if let annotation = cluster.firstAnnotation {
127 | mapView.clusterManager.selectAnnotation(annotation, animated: true)
128 | return true
129 | }
130 |
131 | return false
132 | }
133 |
134 | // MARK: - How To Handle Drag and Drop
135 |
136 | // MARK: YMKMapObjectDragListener
137 |
138 | func onMapObjectDragStart(with mapObject: YMKMapObject) {
139 | // nothing
140 | }
141 |
142 | func onMapObjectDrag(with mapObject: YMKMapObject, point: YMKPoint) {
143 | // nothing
144 | }
145 |
146 | func onMapObjectDragEnd(with mapObject: YMKMapObject) {
147 | guard let placemark = mapObject as? YMKPlacemarkMapObject,
148 | let annotation = placemark.cluster?.firstAnnotation as? MKPointAnnotation else {
149 | return
150 | }
151 |
152 | annotation.coordinate = placemark.geometry.coordinate
153 | }
154 |
155 | }
156 |
157 | private extension YMKPoint {
158 | convenience init(coordinate: CLLocationCoordinate2D) {
159 | self.init(latitude: coordinate.latitude, longitude: coordinate.longitude)
160 | }
161 | }
162 |
--------------------------------------------------------------------------------
/Examples/Example-objc/YandexMapKit/CKYandexMapViewController.m:
--------------------------------------------------------------------------------
1 | //
2 | // CKYandexMapViewController.m
3 | // Example-objc
4 | //
5 | // Created by petropavel on 23/01/2019.
6 | // Copyright © 2019 Hulab. All rights reserved.
7 | //
8 |
9 | #import
10 |
11 | #import
12 | #import
13 |
14 | #import "YMKMapView+ClusterKit.h"
15 |
16 | #import "CKYandexMapViewController.h"
17 |
18 | @interface CKYandexMapViewController ()
23 |
24 | @property (weak, nonatomic) IBOutlet YMKMapView *mapView;
25 |
26 | @property (nonatomic) BOOL isMapLoaded;
27 |
28 | @end
29 |
30 | @implementation CKYandexMapViewController
31 |
32 | - (void)viewDidLoad {
33 | [super viewDidLoad];
34 |
35 | self.isMapLoaded = NO;
36 |
37 | YMKMap *map = self.mapView.mapWindow.map;
38 |
39 | self.mapView.dataSource = self;
40 | [map setMapLoadedListenerWithMapLoadedListener:self];
41 | [map addCameraListenerWithCameraListener:self];
42 |
43 | CKNonHierarchicalDistanceBasedAlgorithm *algorithm = [CKNonHierarchicalDistanceBasedAlgorithm new];
44 | algorithm.cellSize = 200;
45 |
46 | self.mapView.clusterManager.algorithm = algorithm;
47 | self.mapView.clusterManager.marginFactor = 1;
48 |
49 | [self loadData];
50 | }
51 |
52 | - (void)loadData {
53 | CKGeoPointOperation *operation = [[CKGeoPointOperation alloc] init];
54 |
55 | [operation setCompletionBlockWithSuccess:^(CKGeoPointOperation * _Nonnull operation, NSArray *points) {
56 | self.mapView.clusterManager.annotations = points;
57 | } failure:nil];
58 |
59 | [operation start];
60 | }
61 |
62 | #pragma mark
63 |
64 | - (YMKPlacemarkMapObject *)mapView:(YMKMapView *)mapView placemarkForCluster:(CKCluster *)cluster {
65 | YMKPlacemarkMapObject *placemark;
66 |
67 | CLLocationCoordinate2D clusterCoordinate = cluster.coordinate;
68 |
69 | YMKPoint *point = [YMKPoint pointWithLatitude:clusterCoordinate.latitude
70 | longitude:clusterCoordinate.longitude];
71 |
72 | if(cluster.count > 1) {
73 | placemark = [mapView.mapWindow.map.mapObjects addPlacemarkWithPoint:point
74 | image:[UIImage imageNamed:@"cluster"]];
75 | } else {
76 | placemark = [mapView.mapWindow.map.mapObjects addPlacemarkWithPoint:point
77 | image:[UIImage imageNamed:@"marker"]];
78 | placemark.draggable = YES;
79 | }
80 |
81 | [placemark addTapListenerWithTapListener:self];
82 | [placemark setDragListenerWithDragListener:self];
83 |
84 | return placemark;
85 | }
86 |
87 | #pragma mark
88 |
89 | - (void)onMapLoadedWithStatistics:(YMKMapLoadStatistics *)statistics {
90 | self.isMapLoaded = YES;
91 | }
92 |
93 | #pragma mark - How To Update Clusters
94 |
95 | #pragma mark
96 |
97 | - (void)onCameraPositionChangedWithMap:(YMKMap *)map
98 | cameraPosition:(YMKCameraPosition *)cameraPosition
99 | cameraUpdateSource:(YMKCameraUpdateSource)cameraUpdateSource
100 | finished:(BOOL)finished {
101 |
102 | if (self.isMapLoaded && finished) {
103 | [self.mapView.clusterManager updateClustersIfNeeded];
104 |
105 | // workaround (https://github.com/yandex/mapkit-ios-demo/issues/23)
106 | // we need to wait some time to get actual visibleRegion for clusterization
107 | // dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(100 * NSEC_PER_MSEC)), dispatch_get_main_queue(), ^{
108 | // [self.mapView.clusterManager updateClustersIfNeeded];
109 | // });
110 | }
111 | }
112 |
113 | #pragma mark - How To Handle Selection/Deselection
114 |
115 | #pragma mark
116 |
117 | - (BOOL)onMapObjectTapWithMapObject:(YMKMapObject *)mapObject point:(YMKPoint *)point {
118 | if ([mapObject isKindOfClass:[YMKPlacemarkMapObject class]]) {
119 | YMKPlacemarkMapObject *placemark = (YMKPlacemarkMapObject *)mapObject;
120 | UIEdgeInsets padding = UIEdgeInsetsMake(40, 20, 44, 20);
121 |
122 | YMKCameraPosition *cameraPosition = [self.mapView cameraPositionThatFits:placemark.cluster
123 | edgePadding:padding];\
124 |
125 | YMKAnimation *animation = [YMKAnimation animationWithType:YMKAnimationTypeSmooth
126 | duration:0.5f];
127 |
128 | [self.mapView.mapWindow.map moveWithCameraPosition:cameraPosition
129 | animationType:animation
130 | cameraCallback:^(BOOL completed) {
131 | if (completed) {
132 | [self.mapView.clusterManager updateClustersIfNeeded];
133 | }
134 | }];
135 |
136 | return YES;
137 | }
138 |
139 | return NO;
140 | }
141 |
142 | #pragma mark - How To Handle Drag and Drop
143 |
144 | #pragma mark
145 |
146 | - (void)onMapObjectDragStartWithMapObject:(YMKMapObject *)mapObject {
147 | // nothing
148 | }
149 |
150 | - (void)onMapObjectDragWithMapObject:(YMKMapObject *)mapObject point:(YMKPoint *)point {
151 | // nothing
152 | }
153 |
154 | - (void)onMapObjectDragEndWithMapObject:(YMKMapObject *)mapObject {
155 | if ([mapObject isKindOfClass:[YMKPlacemarkMapObject class]]) {
156 | YMKPlacemarkMapObject *placemark = (YMKPlacemarkMapObject *)mapObject;
157 | placemark.cluster.firstAnnotation.coordinate = placemark.geometry.coordinate;
158 | }
159 | }
160 |
161 | @end
162 |
--------------------------------------------------------------------------------
/Examples/Example-objc/MapKit/CKMapKitViewController.m:
--------------------------------------------------------------------------------
1 | // CKMapKitViewController.m
2 | //
3 | // Copyright © 2017 Hulab. All rights reserved.
4 | //
5 | // Permission is hereby granted, free of charge, to any person obtaining a copy
6 | // of this software and associated documentation files (the "Software"), to deal
7 | // in the Software without restriction, including without limitation the rights
8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | // copies of the Software, and to permit persons to whom the Software is
10 | // furnished to do so, subject to the following conditions:
11 | //
12 | // The above copyright notice and this permission notice shall be included in
13 | // all copies or substantial portions of the Software.
14 | //
15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | // THE SOFTWARE.
22 |
23 | #import
24 | #import
25 |
26 | #import "CKMapKitViewController.h"
27 |
28 | NSString * const CKMapViewDefaultAnnotationViewReuseIdentifier = @"annotation";
29 | NSString * const CKMapViewDefaultClusterAnnotationViewReuseIdentifier = @"cluster";
30 |
31 | @interface CKMapKitViewController ()
32 | @property (weak, nonatomic) IBOutlet MKMapView *mapView;
33 | @end
34 |
35 | @implementation CKMapKitViewController
36 |
37 | - (void)viewDidLoad {
38 | [super viewDidLoad];
39 |
40 | CKNonHierarchicalDistanceBasedAlgorithm *algorithm = [CKNonHierarchicalDistanceBasedAlgorithm new];
41 | algorithm.cellSize = 100;
42 |
43 | self.mapView.clusterManager.algorithm = algorithm;
44 | self.mapView.clusterManager.marginFactor = 1;
45 |
46 | [self loadData];
47 | }
48 |
49 | - (void)loadData {
50 | CKGeoPointOperation *operation = [[CKGeoPointOperation alloc] init];
51 |
52 | [operation setCompletionBlockWithSuccess:^(CKGeoPointOperation * _Nonnull operation, NSArray *points) {
53 | self.mapView.clusterManager.annotations = points;
54 | } failure:nil];
55 |
56 | [operation start];
57 | }
58 |
59 | #pragma mark
60 |
61 | - (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id)annotation {
62 | CKCluster *cluster = (CKCluster *)annotation;
63 |
64 | if (cluster.count > 1) {
65 | MKAnnotationView *view = [mapView dequeueReusableAnnotationViewWithIdentifier:CKMapViewDefaultClusterAnnotationViewReuseIdentifier];
66 | if (view) {
67 | return view;
68 | }
69 | return [[CKClusterView alloc] initWithAnnotation:cluster reuseIdentifier:CKMapViewDefaultClusterAnnotationViewReuseIdentifier];
70 | }
71 |
72 | MKAnnotationView *view = [mapView dequeueReusableAnnotationViewWithIdentifier:CKMapViewDefaultAnnotationViewReuseIdentifier];
73 | if (view) {
74 | return view;
75 | }
76 | return [[CKAnnotationView alloc] initWithAnnotation:cluster reuseIdentifier:CKMapViewDefaultAnnotationViewReuseIdentifier];
77 | }
78 |
79 | #pragma mark - How To Update Clusters
80 |
81 | - (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated {
82 | [mapView.clusterManager updateClustersIfNeeded];
83 | }
84 |
85 | #pragma mark - How To Handle Selection/Deselection
86 |
87 | - (void)mapView:(MKMapView *)mapView didSelectAnnotationView:(MKAnnotationView *)view {
88 | CKCluster *cluster = (CKCluster *)view.annotation;
89 |
90 | if (cluster.count > 1) {
91 | UIEdgeInsets edgePadding = UIEdgeInsetsMake(40, 20, 44, 20);
92 | [mapView showCluster:cluster edgePadding:edgePadding animated:YES];
93 | } else {
94 | [mapView.clusterManager selectAnnotation:cluster.firstAnnotation animated:NO];
95 | }
96 | }
97 |
98 | - (void)mapView:(MKMapView *)mapView didDeselectAnnotationView:(MKAnnotationView *)view {
99 | CKCluster *cluster = (CKCluster *)view.annotation;
100 | [mapView.clusterManager deselectAnnotation:cluster.firstAnnotation animated:NO];
101 | }
102 |
103 | #pragma mark - How To Handle Drag and Drop
104 |
105 | - (void)mapView:(MKMapView *)mapView annotationView:(MKAnnotationView *)view didChangeDragState:(MKAnnotationViewDragState)newState fromOldState:(MKAnnotationViewDragState)oldState {
106 | CKCluster *cluster = (CKCluster *)view.annotation;
107 |
108 | switch (newState) {
109 |
110 | case MKAnnotationViewDragStateEnding:
111 | cluster.firstAnnotation.coordinate = cluster.coordinate;
112 | view.dragState = MKAnnotationViewDragStateNone;
113 | [view setDragState:MKAnnotationViewDragStateNone animated:YES];
114 | break;
115 |
116 | case MKAnnotationViewDragStateCanceling:
117 | [view setDragState:MKAnnotationViewDragStateNone animated:YES];
118 | break;
119 |
120 | default:
121 | break;
122 | }
123 | }
124 |
125 | @end
126 |
127 | @implementation CKAnnotationView
128 |
129 | - (instancetype)initWithAnnotation:(id)annotation reuseIdentifier:(NSString *)reuseIdentifier {
130 | self = [super initWithAnnotation:annotation reuseIdentifier:reuseIdentifier];
131 | if (self) {
132 |
133 | self.canShowCallout = YES;
134 | self.draggable = YES;
135 | self.image = [UIImage imageNamed:@"marker"];
136 | }
137 | return self;
138 | }
139 |
140 |
141 | @end
142 |
143 | @implementation CKClusterView
144 |
145 | - (instancetype)initWithAnnotation:(id)annotation reuseIdentifier:(NSString *)reuseIdentifier {
146 | self = [super initWithAnnotation:annotation reuseIdentifier:reuseIdentifier];
147 | if (self) {
148 | self.image = [UIImage imageNamed:@"cluster"];
149 | }
150 | return self;
151 | }
152 |
153 | @end
154 |
--------------------------------------------------------------------------------
/Examples/Example-swift/MapKitViewController.swift:
--------------------------------------------------------------------------------
1 | // MapKitViewController.swift
2 | //
3 | // Copyright © 2017 Hulab. All rights reserved.
4 | //
5 | // Permission is hereby granted, free of charge, to any person obtaining a copy
6 | // of this software and associated documentation files (the "Software"), to deal
7 | // in the Software without restriction, including without limitation the rights
8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | // copies of the Software, and to permit persons to whom the Software is
10 | // furnished to do so, subject to the following conditions:
11 | //
12 | // The above copyright notice and this permission notice shall be included in
13 | // all copies or substantial portions of the Software.
14 | //
15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | // THE SOFTWARE.
22 |
23 | import UIKit
24 | import MapKit
25 | import ClusterKit
26 | import ExampleData
27 |
28 | public let CKMapViewDefaultAnnotationViewReuseIdentifier = "annotation"
29 | public let CKMapViewDefaultClusterAnnotationViewReuseIdentifier = "cluster"
30 |
31 | class MapKitViewController: UIViewController, MKMapViewDelegate {
32 |
33 | @IBOutlet weak var mapView: MKMapView!
34 |
35 | override func viewDidLoad() {
36 | super.viewDidLoad()
37 |
38 | let algorithm = CKNonHierarchicalDistanceBasedAlgorithm()
39 | algorithm.cellSize = 200
40 |
41 | mapView.clusterManager.algorithm = algorithm
42 | mapView.clusterManager.marginFactor = 1
43 |
44 | let paris = CLLocationCoordinate2D(latitude: 48.853, longitude: 2.35)
45 | mapView.setCenter(paris, animated: false)
46 |
47 | loadData()
48 | }
49 |
50 | override func didReceiveMemoryWarning() {
51 | super.didReceiveMemoryWarning()
52 | // Dispose of any resources that can be recreated.
53 | }
54 |
55 | func loadData() {
56 | let operation = CKGeoPointOperation()
57 |
58 | operation.setCompletionBlockWithSuccess({ (_, points) in
59 | self.mapView.clusterManager.annotations = points
60 | })
61 |
62 | operation.start()
63 | }
64 |
65 | // MARK: MKMapViewDelegate
66 |
67 | func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
68 | guard let cluster = annotation as? CKCluster else {
69 | return nil
70 | }
71 |
72 | if cluster.count > 1 {
73 | return mapView.dequeueReusableAnnotationView(withIdentifier: CKMapViewDefaultClusterAnnotationViewReuseIdentifier) ??
74 | CKClusterView(annotation: annotation, reuseIdentifier: CKMapViewDefaultClusterAnnotationViewReuseIdentifier)
75 | }
76 |
77 | return mapView.dequeueReusableAnnotationView(withIdentifier: CKMapViewDefaultAnnotationViewReuseIdentifier) ??
78 | CKAnnotationView(annotation: annotation, reuseIdentifier: CKMapViewDefaultAnnotationViewReuseIdentifier)
79 | }
80 |
81 | // MARK: - How To Update Clusters
82 |
83 | func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
84 | mapView.clusterManager.updateClustersIfNeeded()
85 | }
86 |
87 | // MARK: - How To Handle Selection/Deselection
88 |
89 | func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
90 | guard let cluster = view.annotation as? CKCluster else {
91 | return
92 | }
93 |
94 | if cluster.count > 1 {
95 | let edgePadding = UIEdgeInsets.init(top: 40, left: 20, bottom: 44, right: 20)
96 | mapView.show(cluster, edgePadding: edgePadding, animated: true)
97 | } else if let annotation = cluster.firstAnnotation {
98 | mapView.clusterManager.selectAnnotation(annotation, animated: false);
99 | }
100 | }
101 |
102 | func mapView(_ mapView: MKMapView, didDeselect view: MKAnnotationView) {
103 | guard let cluster = view.annotation as? CKCluster, cluster.count == 1 else {
104 | return
105 | }
106 |
107 | mapView.clusterManager.deselectAnnotation(cluster.firstAnnotation, animated: false);
108 | }
109 |
110 | // MARK: - How To Handle Drag and Drop
111 |
112 | func mapView(_ mapView: MKMapView, annotationView view: MKAnnotationView, didChange newState: MKAnnotationView.DragState, fromOldState oldState: MKAnnotationView.DragState) {
113 | guard let cluster = view.annotation as? CKCluster else {
114 | return
115 | }
116 |
117 | switch newState {
118 | case .ending:
119 |
120 | if let annotation = cluster.firstAnnotation as? MKPointAnnotation {
121 | annotation.coordinate = cluster.coordinate
122 | }
123 | view.setDragState(.none, animated: true)
124 |
125 | case .canceling:
126 | view.setDragState(.none, animated: true)
127 |
128 | default: break
129 |
130 | }
131 | }
132 | }
133 |
134 | class CKAnnotationView: MKAnnotationView {
135 |
136 | override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
137 | super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
138 | canShowCallout = true
139 | isDraggable = true
140 | image = UIImage(named: "marker")
141 | }
142 |
143 | required init?(coder aDecoder: NSCoder) {
144 | fatalError("Not implemented")
145 | }
146 | }
147 |
148 | class CKClusterView: MKAnnotationView {
149 |
150 | override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
151 | super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
152 | image = UIImage(named: "cluster")
153 | }
154 |
155 | required init?(coder aDecoder: NSCoder) {
156 | fatalError("Not implemented")
157 | }
158 | }
159 |
--------------------------------------------------------------------------------
/Sources/Mapbox/MGLMapView+ClusterKit.m:
--------------------------------------------------------------------------------
1 | // MGLMapView+ClusterKit.m
2 | //
3 | // Copyright © 2017 Hulab. All rights reserved.
4 | //
5 | // Permission is hereby granted, free of charge, to any person obtaining a copy
6 | // of this software and associated documentation files (the "Software"), to deal
7 | // in the Software without restriction, including without limitation the rights
8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | // copies of the Software, and to permit persons to whom the Software is
10 | // furnished to do so, subject to the following conditions:
11 | //
12 | // The above copyright notice and this permission notice shall be included in
13 | // all copies or substantial portions of the Software.
14 | //
15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | // THE SOFTWARE.
22 |
23 | #import
24 |
25 | #import "MGLMapView+ClusterKit.h"
26 |
27 | MGLCoordinateBounds MGLCoordinateIncludingCoordinate(MGLCoordinateBounds bounds, CLLocationCoordinate2D coordinate) {
28 |
29 | CLLocationCoordinate2D sw = (CLLocationCoordinate2D) {
30 | .latitude = MIN(bounds.sw.latitude, coordinate.latitude),
31 | .longitude = MIN(bounds.sw.longitude, coordinate.longitude)
32 | };
33 |
34 | CLLocationCoordinate2D ne = (CLLocationCoordinate2D) {
35 | .latitude = MAX(bounds.ne.latitude, coordinate.latitude),
36 | .longitude = MAX(bounds.ne.longitude, coordinate.longitude)
37 | };
38 | return MGLCoordinateBoundsMake(sw, ne);
39 | }
40 |
41 | @implementation CKCluster (Mapbox)
42 |
43 | @end
44 |
45 | @implementation MGLMapView (ClusterKit)
46 |
47 | - (MGLMapCamera *)cameraThatFitsCluster:(CKCluster *)cluster {
48 | return [self cameraThatFitsCluster:cluster edgePadding:UIEdgeInsetsZero];
49 | }
50 |
51 | - (MGLMapCamera *)cameraThatFitsCluster:(CKCluster *)cluster edgePadding:(UIEdgeInsets)insets {
52 | MGLCoordinateBounds bounds = MGLCoordinateBoundsMake(cluster.firstAnnotation.coordinate, cluster.firstAnnotation.coordinate);
53 |
54 | for (id annotation in cluster) {
55 | bounds = MGLCoordinateIncludingCoordinate(bounds, annotation.coordinate);
56 | }
57 |
58 | return [self cameraThatFitsCoordinateBounds:bounds edgePadding:insets];
59 | }
60 |
61 | #pragma mark -
62 |
63 | - (CKClusterManager *)clusterManager {
64 | CKClusterManager *clusterManager = objc_getAssociatedObject(self, @selector(clusterManager));
65 | if (!clusterManager) {
66 | clusterManager = [CKClusterManager new];
67 | clusterManager.map = self;
68 | objc_setAssociatedObject(self, @selector(clusterManager), clusterManager, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
69 | }
70 | return clusterManager;
71 | }
72 |
73 | - (double)zoom {
74 | MGLCoordinateBounds bounds = self.visibleCoordinateBounds;
75 | double longitudeDelta = bounds.ne.longitude - bounds.sw.longitude;
76 |
77 | // Handle antimeridian crossing
78 | if (longitudeDelta < 0) {
79 | longitudeDelta = 360 + bounds.ne.longitude - bounds.sw.longitude;
80 | }
81 |
82 | return log2(360 * self.frame.size.width / (256 * longitudeDelta));
83 | }
84 |
85 | - (MKMapRect)visibleMapRect {
86 | MGLCoordinateBounds bounds = self.visibleCoordinateBounds;
87 | MKMapPoint sw = MKMapPointForCoordinate(bounds.sw);
88 | MKMapPoint ne = MKMapPointForCoordinate(bounds.ne);
89 |
90 | double x = sw.x;
91 | double y = ne.y;
92 |
93 | double width = ne.x - sw.x;
94 | double height = sw.y - ne.y;
95 |
96 | // Handle 180th Meridian
97 | if (width < 0) {
98 | width = ne.x + MKMapSizeWorld.width - sw.x;
99 | }
100 | if (height < 0) {
101 | height = sw.y + MKMapSizeWorld.height - ne.y;
102 | }
103 |
104 | return MKMapRectMake(x, y, width, height);
105 | }
106 |
107 | - (void)addClusters:(NSArray *)clusters {
108 | [self addAnnotations:clusters];
109 | }
110 |
111 | - (void)removeClusters:(NSArray *)clusters {
112 | [self removeAnnotations:clusters];
113 | }
114 |
115 | - (void)selectCluster:(CKCluster *)cluster animated:(BOOL)animated {
116 | if (![self.selectedAnnotations containsObject:cluster]) {
117 | [self selectAnnotation:cluster animated:animated completionHandler:nil];
118 | }
119 | }
120 |
121 | - (void)deselectCluster:(CKCluster *)cluster animated:(BOOL)animated {
122 | if ([self.selectedAnnotations containsObject:cluster]) {
123 | [self deselectAnnotation:cluster animated:animated];
124 | }
125 | }
126 |
127 | - (void)performAnimations:(NSArray *)animations completion:(void (^__nullable)(BOOL finished))completion {
128 |
129 | for (CKClusterAnimation *animation in animations) {
130 | animation.cluster.coordinate = animation.from;
131 | }
132 |
133 | void (^animationsBlock)(void) = ^{};
134 |
135 | for (CKClusterAnimation *animation in animations) {
136 | animationsBlock = ^{
137 | animationsBlock();
138 | animation.cluster.coordinate = animation.to;
139 | };
140 | }
141 |
142 | if ([self.clusterManager.delegate respondsToSelector:@selector(clusterManager:performAnimations:completion:)]) {
143 | [self.clusterManager.delegate clusterManager:self.clusterManager
144 | performAnimations:animationsBlock
145 | completion:^(BOOL finished) {
146 | if (completion) completion(finished);
147 | }];
148 | } else {
149 | [UIView animateWithDuration:self.clusterManager.animationDuration
150 | delay:0
151 | options:self.clusterManager.animationOptions
152 | animations:animationsBlock
153 | completion:completion];
154 | }
155 | }
156 |
157 | @end
158 |
--------------------------------------------------------------------------------
/Examples/Example-swift/MapboxViewController.swift:
--------------------------------------------------------------------------------
1 | // MapboxViewController.swift
2 | //
3 | // Copyright © 2017 Hulab. All rights reserved.
4 | //
5 | // Permission is hereby granted, free of charge, to any person obtaining a copy
6 | // of this software and associated documentation files (the "Software"), to deal
7 | // in the Software without restriction, including without limitation the rights
8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | // copies of the Software, and to permit persons to whom the Software is
10 | // furnished to do so, subject to the following conditions:
11 | //
12 | // The above copyright notice and this permission notice shall be included in
13 | // all copies or substantial portions of the Software.
14 | //
15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | // THE SOFTWARE.
22 |
23 | import UIKit
24 | import Mapbox
25 | import ClusterKit
26 | import ExampleData
27 |
28 | class MapboxViewController: UIViewController, MGLMapViewDelegate {
29 |
30 | @IBOutlet weak var mapView: MGLMapView!
31 |
32 | override func viewDidLoad() {
33 | super.viewDidLoad()
34 |
35 | let algorithm = CKNonHierarchicalDistanceBasedAlgorithm()
36 | algorithm.cellSize = 200
37 |
38 | mapView.clusterManager.algorithm = algorithm
39 | mapView.clusterManager.marginFactor = 1
40 |
41 | let paris = CLLocationCoordinate2D(latitude: 48.853, longitude: 2.35)
42 | mapView.setCenter(paris, animated: false)
43 |
44 | loadData()
45 | }
46 |
47 | override func didReceiveMemoryWarning() {
48 | super.didReceiveMemoryWarning()
49 | // Dispose of any resources that can be recreated.
50 | }
51 |
52 |
53 | func loadData() {
54 | let operation = CKGeoPointOperation()
55 |
56 | operation.setCompletionBlockWithSuccess({ (_, points) in
57 | self.mapView.clusterManager.annotations = points
58 | })
59 |
60 | operation.start()
61 | }
62 |
63 | // MARK: MGLMapViewDelegate
64 |
65 | func mapView(_ mapView: MGLMapView, viewFor annotation: MGLAnnotation) -> MGLAnnotationView? {
66 | guard let cluster = annotation as? CKCluster else {
67 | return nil
68 | }
69 |
70 | if cluster.count > 1 {
71 | return mapView.dequeueReusableAnnotationView(withIdentifier: CKMapViewDefaultClusterAnnotationViewReuseIdentifier) ??
72 | MBXClusterView(annotation: annotation, reuseIdentifier: CKMapViewDefaultClusterAnnotationViewReuseIdentifier)
73 | }
74 |
75 | return mapView.dequeueReusableAnnotationView(withIdentifier: CKMapViewDefaultAnnotationViewReuseIdentifier) ??
76 | MBXAnnotationView(annotation: annotation, reuseIdentifier: CKMapViewDefaultAnnotationViewReuseIdentifier)
77 | }
78 |
79 | func mapView(_ mapView: MGLMapView, annotationCanShowCallout annotation: MGLAnnotation) -> Bool {
80 | guard let cluster = annotation as? CKCluster else {
81 | return true
82 | }
83 |
84 | return cluster.count == 1
85 | }
86 |
87 | // MARK: - How To Update Clusters
88 |
89 | func mapView(_ mapView: MGLMapView, regionDidChangeAnimated animated: Bool) {
90 | mapView.clusterManager.updateClustersIfNeeded()
91 | }
92 |
93 | // MARK: - How To Handle Selection/Deselection
94 |
95 | func mapView(_ mapView: MGLMapView, didSelect annotation: MGLAnnotation) {
96 | guard let cluster = annotation as? CKCluster else {
97 | return
98 | }
99 |
100 | if cluster.count > 1 {
101 |
102 | let edgePadding = UIEdgeInsets.init(top: 40, left: 20, bottom: 44, right: 20)
103 | let camera = mapView.cameraThatFitsCluster(cluster, edgePadding: edgePadding)
104 | mapView.setCamera(camera, animated: true)
105 |
106 | } else if let annotation = cluster.firstAnnotation {
107 | mapView.clusterManager.selectAnnotation(annotation, animated: false);
108 | }
109 | }
110 |
111 | func mapView(_ mapView: MGLMapView, didDeselect annotation: MGLAnnotation) {
112 | guard let cluster = annotation as? CKCluster, cluster.count == 1 else {
113 | return
114 | }
115 |
116 | mapView.clusterManager.deselectAnnotation(cluster.firstAnnotation, animated: false);
117 | }
118 |
119 | }
120 |
121 | // MARK: - Custom annotation view
122 |
123 | class MBXAnnotationView: MGLAnnotationView {
124 |
125 | var imageView: UIImageView!
126 |
127 | override init(annotation: MGLAnnotation?, reuseIdentifier: String?) {
128 | super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
129 | let image = UIImage(named: "marker")
130 | imageView = UIImageView(image: image)
131 | addSubview(imageView)
132 | frame = imageView.frame
133 |
134 | isDraggable = true
135 | centerOffset = CGVector(dx: 0.5, dy: 1)
136 | }
137 |
138 | required init?(coder aDecoder: NSCoder) {
139 | fatalError("Not implemented")
140 | }
141 |
142 | override func setDragState(_ dragState: MGLAnnotationViewDragState, animated: Bool) {
143 | super.setDragState(dragState, animated: animated)
144 |
145 | switch dragState {
146 | case .starting:
147 | UIView.animate(withDuration: 0.4, delay: 0, usingSpringWithDamping: 0.4, initialSpringVelocity: 0.5, options: [.curveLinear], animations: {
148 | self.transform = self.transform.scaledBy(x: 2, y: 2)
149 | })
150 | case .ending:
151 | UIView.animate(withDuration: 0.4, delay: 0, usingSpringWithDamping: 0.4, initialSpringVelocity: 0.5, options: [.curveLinear], animations: {
152 | self.transform = CGAffineTransform.identity
153 | })
154 | default:
155 | break
156 | }
157 | }
158 | }
159 |
160 | class MBXClusterView: MGLAnnotationView {
161 |
162 | var imageView: UIImageView!
163 |
164 | override init(annotation: MGLAnnotation?, reuseIdentifier: String?) {
165 | super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
166 | let image = UIImage(named: "cluster")
167 | imageView = UIImageView(image: image)
168 | addSubview(imageView)
169 | frame = imageView.frame
170 |
171 | centerOffset = CGVector(dx: 0.5, dy: 1)
172 | }
173 |
174 | required init?(coder aDecoder: NSCoder) {
175 | fatalError("Not implemented")
176 | }
177 |
178 | }
179 |
--------------------------------------------------------------------------------
/Examples/Example-objc/Mapbox/CKMapboxViewController.m:
--------------------------------------------------------------------------------
1 | // CKMapboxViewController.m
2 | //
3 | // Copyright © 2017 Hulab. All rights reserved.
4 | //
5 | // Permission is hereby granted, free of charge, to any person obtaining a copy
6 | // of this software and associated documentation files (the "Software"), to deal
7 | // in the Software without restriction, including without limitation the rights
8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | // copies of the Software, and to permit persons to whom the Software is
10 | // furnished to do so, subject to the following conditions:
11 | //
12 | // The above copyright notice and this permission notice shall be included in
13 | // all copies or substantial portions of the Software.
14 | //
15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | // THE SOFTWARE.
22 |
23 | #import
24 | #import
25 |
26 | #import "CKMapboxViewController.h"
27 |
28 | NSString * const MBXMapViewDefaultAnnotationViewReuseIdentifier = @"annotation";
29 | NSString * const MBXMapViewDefaultClusterAnnotationViewReuseIdentifier = @"cluster";
30 |
31 | @interface CKMapboxViewController ()
32 | @property (weak, nonatomic) IBOutlet MGLMapView *mapView;
33 | @end
34 |
35 | @implementation CKMapboxViewController
36 |
37 | - (void)viewDidLoad {
38 | [super viewDidLoad];
39 |
40 | CKNonHierarchicalDistanceBasedAlgorithm *algorithm = [CKNonHierarchicalDistanceBasedAlgorithm new];
41 | algorithm.cellSize = 200;
42 |
43 | self.mapView.clusterManager.algorithm = algorithm;
44 | self.mapView.clusterManager.marginFactor = 1;
45 |
46 | [self loadData];
47 | }
48 |
49 | - (void)loadData {
50 | CKGeoPointOperation *operation = [[CKGeoPointOperation alloc] init];
51 |
52 | [operation setCompletionBlockWithSuccess:^(CKGeoPointOperation * _Nonnull operation, NSArray *points) {
53 | self.mapView.clusterManager.annotations = points;
54 | } failure:nil];
55 |
56 | [operation start];
57 | }
58 |
59 | #pragma mark
60 |
61 | - (MGLAnnotationView *)mapView:(MGLMapView *)mapView viewForAnnotation:(id)annotation {
62 | CKCluster *cluster = (CKCluster *)annotation;
63 |
64 | if (cluster.count > 1) {
65 | MGLAnnotationView *view = [mapView dequeueReusableAnnotationViewWithIdentifier:MBXMapViewDefaultClusterAnnotationViewReuseIdentifier];
66 | if (view) {
67 | return view;
68 | }
69 | return [[MBXClusterView alloc] initWithAnnotation:cluster reuseIdentifier:MBXMapViewDefaultClusterAnnotationViewReuseIdentifier];
70 | }
71 |
72 | MGLAnnotationView *view = [mapView dequeueReusableAnnotationViewWithIdentifier:MBXMapViewDefaultAnnotationViewReuseIdentifier];
73 | if (view) {
74 | return view;
75 | }
76 | return [[MBXAnnotationView alloc] initWithAnnotation:cluster reuseIdentifier:MBXMapViewDefaultAnnotationViewReuseIdentifier];
77 | }
78 |
79 | - (BOOL)mapView:(MGLMapView *)mapView annotationCanShowCallout:(id)annotation {
80 | CKCluster *cluster = (CKCluster *)annotation;
81 | return cluster.count == 1;
82 | }
83 |
84 | #pragma mark - How To Update Clusters
85 |
86 | - (void)mapView:(MGLMapView *)mapView regionDidChangeAnimated:(BOOL)animated {
87 | [mapView.clusterManager updateClustersIfNeeded];
88 | }
89 |
90 | #pragma mark - How To Handle Selection/Deselection
91 |
92 | - (void)mapView:(MGLMapView *)mapView didSelectAnnotation:(nonnull id)annotation {
93 | CKCluster *cluster = (CKCluster *)annotation;
94 |
95 | if (cluster.count > 1) {
96 | UIEdgeInsets edgePadding = UIEdgeInsetsMake(40, 20, 44, 20);
97 | MGLMapCamera *camera = [mapView cameraThatFitsCluster:cluster edgePadding:edgePadding];
98 | [mapView setCamera:camera animated:YES];
99 | } else {
100 | [mapView.clusterManager selectAnnotation:cluster.firstAnnotation animated:NO];
101 | }
102 | }
103 |
104 | - (void)mapView:(MGLMapView *)mapView didDeselectAnnotation:(nonnull id)annotation{
105 | CKCluster *cluster = (CKCluster *)annotation;
106 | [mapView.clusterManager deselectAnnotation:cluster.firstAnnotation animated:NO];
107 | }
108 |
109 | @end
110 |
111 | @implementation MBXAnnotationView
112 |
113 | - (instancetype)initWithAnnotation:(id)annotation reuseIdentifier:(NSString *)reuseIdentifier {
114 | self = [super initWithAnnotation:annotation reuseIdentifier:reuseIdentifier];
115 | if (self) {
116 |
117 | UIImage *image = [UIImage imageNamed:@"marker"];
118 | self.imageView = [[UIImageView alloc] initWithImage:image];
119 | [self addSubview:self.imageView];
120 | self.frame = self.imageView.frame;
121 |
122 | self.draggable = YES;
123 | self.centerOffset = CGVectorMake(0.5, 1);
124 | }
125 | return self;
126 | }
127 |
128 |
129 | - (void)setDragState:(MGLAnnotationViewDragState)dragState animated:(BOOL)animated {
130 | [super setDragState:dragState animated:NO];
131 |
132 | switch (dragState) {
133 | case MGLAnnotationViewDragStateStarting: {
134 | [UIView animateWithDuration:.4 delay:0 usingSpringWithDamping:.4 initialSpringVelocity:.5 options:UIViewAnimationOptionCurveLinear animations:^{
135 | self.transform = CGAffineTransformScale(CGAffineTransformIdentity, 2, 2);
136 | } completion:nil];
137 | break;
138 | }
139 | case MGLAnnotationViewDragStateEnding: {
140 | self.transform = CGAffineTransformScale(CGAffineTransformIdentity, 2, 2);
141 | [UIView animateWithDuration:.4 delay:0 usingSpringWithDamping:.4 initialSpringVelocity:.5 options:UIViewAnimationOptionCurveLinear animations:^{
142 | self.transform = CGAffineTransformIdentity;
143 | } completion:nil];
144 | break;
145 | }
146 | default:
147 | break;
148 | }
149 | }
150 |
151 | @end
152 |
153 | @implementation MBXClusterView
154 |
155 | - (instancetype)initWithAnnotation:(id)annotation reuseIdentifier:(NSString *)reuseIdentifier {
156 | self = [super initWithAnnotation:annotation reuseIdentifier:reuseIdentifier];
157 | if (self) {
158 |
159 | UIImage *image = [UIImage imageNamed:@"cluster"];
160 | self.imageView = [[UIImageView alloc] initWithImage:image];
161 | [self addSubview:self.imageView];
162 | self.frame = self.imageView.frame;
163 |
164 | self.centerOffset = CGVectorMake(0.5, 1);
165 | }
166 | return self;
167 | }
168 |
169 | @end
170 |
--------------------------------------------------------------------------------
/Sources/ClusterKit/include/ClusterKit/CKCluster.h:
--------------------------------------------------------------------------------
1 | // CKCluster.h
2 | //
3 | // Copyright © 2017 Hulab. All rights reserved.
4 | //
5 | // Permission is hereby granted, free of charge, to any person obtaining a copy
6 | // of this software and associated documentation files (the "Software"), to deal
7 | // in the Software without restriction, including without limitation the rights
8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | // copies of the Software, and to permit persons to whom the Software is
10 | // furnished to do so, subject to the following conditions:
11 | //
12 | // The above copyright notice and this permission notice shall be included in
13 | // all copies or substantial portions of the Software.
14 | //
15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | // THE SOFTWARE.
22 |
23 | #import
24 | #import
25 |
26 | NS_ASSUME_NONNULL_BEGIN
27 |
28 | /**
29 | Compute the square euclidean distance in MapKit projection.
30 |
31 | @param from Distance from point.
32 | @param to Distance to point.
33 | @return Euclidean distance in MapKit projection.
34 | */
35 | MK_EXTERN double CKDistance(CLLocationCoordinate2D from, CLLocationCoordinate2D to);
36 |
37 | MK_EXTERN MKMapRect MKMapRectByAddingPoint(MKMapRect rect, MKMapPoint point);
38 |
39 | MK_EXTERN NSComparisonResult MKMapSizeCompare(MKMapSize size1, MKMapSize size2);
40 |
41 | @class CKCluster;
42 |
43 | #pragma mark - Cluster definitions
44 |
45 | /**
46 | CKCluster protocol that create a kind of CKCluster at the given coordinate.
47 | */
48 | @protocol CKCluster
49 |
50 | /**
51 | Instantiates a cluster at the given coordinate.
52 |
53 | @param coordinate The cluster coordinate.
54 | @return The newly-initialized cluster.
55 | */
56 | + (__kindof CKCluster *)clusterWithCoordinate:(CLLocationCoordinate2D)coordinate;
57 |
58 | @end
59 |
60 | /**
61 | The CKCluster object represents a group of annotation.
62 | */
63 | @interface CKCluster : NSObject
64 |
65 | /**
66 | Cluster coordinate.
67 | */
68 | @property (nonatomic, readonly) CLLocationCoordinate2D coordinate;
69 |
70 | /**
71 | Cluster annotation array.
72 | */
73 | @property (nonatomic, readonly, copy) NSArray> *annotations;
74 |
75 | /**
76 | The number of annotations in the cluster.
77 | */
78 | @property (nonatomic, readonly) NSUInteger count;
79 |
80 | /**
81 | The first annotation in the cluster.
82 | If the cluster is empty, returns nil.
83 | */
84 | @property (nonatomic, readonly, nullable) id firstAnnotation;
85 |
86 | /**
87 | The last annotation in the cluster.
88 | If the cluster is empty, returns nil.
89 | */
90 | @property (nonatomic, readonly, nullable) id lastAnnotation;
91 |
92 | /**
93 | Represents a rectangular bounding box on the Earth's projection.
94 | */
95 | @property (nonatomic, readonly) MKMapRect bounds;
96 |
97 | /**
98 | Adds a given annotation to the cluster, if it is not already a member.
99 |
100 | @param annotation The annotation to add.
101 | */
102 | - (void)addAnnotation:(id)annotation;
103 |
104 | /**
105 | Removes a given annotation from the cluster.
106 |
107 | @param annotation The annotation to remove.
108 | */
109 | - (void)removeAnnotation:(id)annotation;
110 |
111 | /**
112 | Returns the annotation at the given index.
113 | If index is beyond the end of the array (that is, if index is greater than or equal to the value returned by count), an NSRangeException is raised.
114 |
115 | @param index An annotation index within the bounds of the array.
116 | @return The annotation located at index.
117 | */
118 | - (id)annotationAtIndex:(NSUInteger)index;
119 |
120 | /**
121 | Returns a Boolean value that indicates whether a given annotation is present in the cluster.
122 | Starting at index 0, each annotation of the cluster is passed as an argument to an isEqual: message sent to the given annotation until a match is found or the end of the cluster is reached. Annotations are considered equal if isEqual: (declared in the NSObject protocol) returns YES.
123 |
124 | @param annotation An annotation.
125 | @return YES if the gievn annotation is present in the cluster, otherwise NO.
126 | */
127 | - (BOOL)containsAnnotation:(id)annotation;
128 |
129 | /**
130 | Returns the annotation at the specified index.
131 | This method has the same behavior as the annotationAtIndex: method.
132 | If index is beyond the end of the cluster (that is, if index is greater than or equal to the value returned by count), an NSRangeException is raised.
133 | You shouldn’t need to call this method directly. Instead, this method is called when accessing an annotation by index using subscripting.
134 |
135 | `id value = cluster[3]; // equivalent to [cluster annotationAtIndex:3]`
136 |
137 | @param index An index within the bounds of the cluster.
138 | @return The annotation located at index.
139 | */
140 | - (id)objectAtIndexedSubscript:(NSUInteger)index;
141 |
142 | /**
143 | Returns a Boolean value that indicates whether the receiver and a given cluster are equal.
144 |
145 | @param cluster The cluster to be compared to the receiver. May be nil, in which case this method returns NO.
146 | @return YES if the receiver and the given cluster are equal, otherwise NO.
147 | */
148 | - (BOOL)isEqualToCluster:(CKCluster *)cluster;
149 |
150 | /**
151 | Returns a Boolean value that indicates whether at least one annotion in the receiving cluster is also present in another given cluster.
152 |
153 | @param cluster The other cluster
154 | @return YES if at least one annotation in the receiving cluster is also present in other, otherwise NO.
155 | */
156 | - (BOOL)intersectsCluster:(CKCluster *)cluster;
157 |
158 | /**
159 | Returns a Boolean value that indicates whether every annotation in the receiving cluster is also present in another given cluster.
160 |
161 | @param cluster The cluster with which to compare the receiving cluster.
162 | @return YES if every annotation in the receiving cluster is also present in other, otherwise NO.
163 | */
164 | - (BOOL)isSubsetOfCluster:(CKCluster *)cluster;
165 |
166 | @end
167 |
168 | #pragma mark - Centroid Cluster
169 |
170 | /**
171 | Cluster with centroid coordinate.
172 | */
173 | @interface CKCentroidCluster : CKCluster
174 |
175 | @end
176 |
177 | #pragma mark - Nearest Centroid Cluster
178 |
179 | /**
180 | Cluster with coordinate at the nearest annotation from centroid.
181 | */
182 | @interface CKNearestCentroidCluster : CKCentroidCluster
183 |
184 | @end
185 |
186 | #pragma mark - Bottom Cluster
187 |
188 | /**
189 | Cluster with coordinate at the bottom annotion.
190 | */
191 | @interface CKBottomCluster : CKCluster
192 |
193 | @end
194 |
195 | NS_ASSUME_NONNULL_END
196 |
197 |
--------------------------------------------------------------------------------
/Sources/ClusterKit/include/ClusterKit/CKClusterManager.h:
--------------------------------------------------------------------------------
1 | // CKClusterManager.h
2 | //
3 | // Copyright © 2017 Hulab. All rights reserved.
4 | //
5 | // Permission is hereby granted, free of charge, to any person obtaining a copy
6 | // of this software and associated documentation files (the "Software"), to deal
7 | // in the Software without restriction, including without limitation the rights
8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | // copies of the Software, and to permit persons to whom the Software is
10 | // furnished to do so, subject to the following conditions:
11 | //
12 | // The above copyright notice and this permission notice shall be included in
13 | // all copies or substantial portions of the Software.
14 | //
15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | // THE SOFTWARE.
22 |
23 | #import
24 |
25 | #if __has_include()
26 | #import
27 | #endif
28 |
29 | #import
30 | #import
31 |
32 | NS_ASSUME_NONNULL_BEGIN
33 |
34 | FOUNDATION_EXTERN const double kCKMarginFactorWorld;
35 |
36 | @protocol CKMap;
37 | @class CKClusterManager;
38 |
39 | /**
40 | The delegate of a CKClusterManager object may adopt the CKClusterManagerDelegate protocol.
41 | Optional methods of the protocol allow the delegate to manage clustering and animations.
42 | */
43 | @protocol CKClusterManagerDelegate
44 |
45 | @optional
46 |
47 | /**
48 | Asks the delegate if the cluster manager should clusterized the given annotation.
49 |
50 | @param clusterManager The cluster manager object requesting this information.
51 | @param annotation The annotation to clusterized.
52 |
53 | @return Yes to permit clusterization of the given annotation.
54 | */
55 | - (BOOL)clusterManager:(CKClusterManager *)clusterManager shouldClusterAnnotation:(id)annotation;
56 |
57 | /**
58 | Tells the delegate to perform an animation.
59 |
60 | @param clusterManager The cluster manager object requesting the animation.
61 | @param animations A block object containing the animation. This block takes no parameters and has no return value. This parameter must not be NULL.
62 | @param completion A block object to be executed when the animation sequence ends. This block has no return value and takes a single Boolean argument
63 | that indicates whether or not the animations actually finished before the completion handler was called. If the duration of the
64 | animation is 0, this block is performed at the beginning of the next run loop cycle. This parameter may be NULL.
65 | */
66 | - (void)clusterManager:(CKClusterManager *)clusterManager performAnimations:(void (^)(void))animations completion:(void (^ __nullable)(BOOL finished))completion;
67 |
68 | @end
69 |
70 | @interface CKClusterManager : NSObject
71 |
72 | /**
73 | The total duration of the clusters animations, measured in seconds. If you specify a negative value or 0, the changes are made without animating them.
74 | */
75 | @property (nonatomic, assign) CGFloat animationDuration;
76 |
77 | #if __has_include()
78 | /**
79 | A mask of options indicating how you want to perform the animations. For a list of valid constants, @see UIViewAnimationOptions.
80 | */
81 | @property (nonatomic, assign) UIViewAnimationOptions animationOptions;
82 | #endif
83 |
84 | /**
85 | The cluster algorithm to use. @see CKClusterAlgorithm.
86 | */
87 | @property (nonatomic, strong) __kindof CKClusterAlgorithm *algorithm;
88 |
89 | /**
90 | A map object adopting the CKMap protocol.
91 | */
92 | @property (nonatomic, weak) id map;
93 |
94 | /**
95 | Delegate instance that adopt the CKClusterManagerDelegate protocol.
96 | */
97 | @property (nonatomic,weak) id delegate;
98 |
99 | /**
100 | The currently selected annotation.
101 | */
102 | @property (nonatomic, readonly) id selectedAnnotation;
103 |
104 | /**
105 | The current cluster array.
106 | */
107 | @property (nonatomic, readonly, copy) NSArray *clusters;
108 |
109 | /**
110 | The maximum zoom level for clustering, 20 by default.
111 | */
112 | @property (nonatomic) CGFloat maxZoomLevel;
113 |
114 | /**
115 | The clustering margin factor. kCKMarginFactorWorld by default.
116 | */
117 | @property (nonatomic) double marginFactor;
118 |
119 | /**
120 | The annotations to clusterize.
121 | */
122 | @property (nonatomic, copy) NSArray> *annotations;
123 |
124 | /**
125 | Adds an annotation.
126 |
127 | @param annotation The annotation to add.
128 | */
129 | - (void)addAnnotation:(id)annotation;
130 |
131 | /**
132 | Adds annotations.
133 |
134 | @param annotations Annotations to add.
135 | */
136 | - (void)addAnnotations:(NSArray> *)annotations;
137 |
138 | /**
139 | Removes an annotation.
140 |
141 | @param annotation The annotation to remove.
142 | */
143 | - (void)removeAnnotation:(id)annotation;
144 |
145 | /**
146 | Removes annotations.
147 |
148 | @param annotations Annotations to remove.
149 | */
150 | - (void)removeAnnotations:(NSArray> *)annotations;
151 |
152 | /**
153 | Selects an annotation. Look for the annotation in clusters and extract it if necessary.
154 |
155 | @param annotation The annotation to be selected.
156 | */
157 | - (void)selectAnnotation:(id)annotation animated:(BOOL)animated;
158 |
159 | /**
160 | Deselects an annotation.
161 |
162 | @param annotation The annotation to be deselected.
163 | */
164 | - (void)deselectAnnotation:(nullable id)annotation animated:(BOOL)animated;
165 |
166 | /**
167 | Updates displayed clusters.
168 | */
169 | - (void)updateClusters;
170 |
171 | /**
172 | Updates clusters if the area currently displayed has significantly moved.
173 | */
174 | - (void)updateClustersIfNeeded;
175 |
176 | @end
177 |
178 | /**
179 | CKClusterAnimation defines a cluster animation from a start coordinate to an end coordinate on a map.
180 | */
181 | @interface CKClusterAnimation : NSObject
182 |
183 | /**
184 | The cluster to move.
185 | */
186 | @property (nonatomic, readonly) CKCluster *cluster;
187 |
188 | /**
189 | The cluster starting point.
190 | */
191 | @property (nonatomic) CLLocationCoordinate2D from;
192 |
193 | /**
194 | The cluster ending point.
195 | */
196 | @property (nonatomic) CLLocationCoordinate2D to;
197 |
198 | /**
199 | Initializes an animation for the given cluster.
200 |
201 | @param cluster The cluster to animate.
202 | @param from The cluster starting point.
203 | @param to The cluster ending point.
204 | @return The initialized CKClusterAnimation object.
205 | */
206 | - (instancetype)initWithCluster:(CKCluster *)cluster from:(CLLocationCoordinate2D)from to:(CLLocationCoordinate2D)to NS_DESIGNATED_INITIALIZER;
207 |
208 | /**
209 | Creates an animation for the given cluster.
210 |
211 | @param cluster The cluster to animate.
212 | @param from The cluster starting point.
213 | @param to The cluster ending point.
214 | @return The initialized CKClusterAnimation object.
215 | */
216 | + (instancetype)animateCluster:(CKCluster *)cluster from:(CLLocationCoordinate2D)from to:(CLLocationCoordinate2D)to;
217 |
218 | /// :nodoc:
219 | - (instancetype)init NS_UNAVAILABLE;
220 | /// :nodoc:
221 | + (instancetype)new NS_UNAVAILABLE;
222 |
223 | @end
224 |
225 | NS_ASSUME_NONNULL_END
226 |
--------------------------------------------------------------------------------
/Sources/GoogleMaps/GMSMapView+ClusterKit.m:
--------------------------------------------------------------------------------
1 | // GMSMapView+ClusterKit.m
2 | //
3 | // Copyright © 2017 Hulab. All rights reserved.
4 | //
5 | // Permission is hereby granted, free of charge, to any person obtaining a copy
6 | // of this software and associated documentation files (the "Software"), to deal
7 | // in the Software without restriction, including without limitation the rights
8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | // copies of the Software, and to permit persons to whom the Software is
10 | // furnished to do so, subject to the following conditions:
11 | //
12 | // The above copyright notice and this permission notice shall be included in
13 | // all copies or substantial portions of the Software.
14 | //
15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | // THE SOFTWARE.
22 |
23 | #import
24 | #import "GMSMapView+ClusterKit.h"
25 |
26 | @implementation GMSMarker (ClusterKit)
27 |
28 | - (CKCluster *)cluster {
29 | return objc_getAssociatedObject(self, @selector(cluster));
30 | }
31 |
32 | - (void)setCluster:(CKCluster *)cluster {
33 | objc_setAssociatedObject(self, @selector(cluster), cluster, OBJC_ASSOCIATION_ASSIGN);
34 | }
35 |
36 | @end
37 |
38 | @interface GMSMapView ()
39 | @property (nonatomic,readonly) NSMapTable *markers;
40 | @end
41 |
42 | @implementation GMSMapView (ClusterKit)
43 |
44 | - (CKClusterManager *)clusterManager {
45 | CKClusterManager *clusterManager = objc_getAssociatedObject(self, @selector(clusterManager));
46 | if (!clusterManager) {
47 | clusterManager = [CKClusterManager new];
48 | clusterManager.map = self;
49 | objc_setAssociatedObject(self, @selector(clusterManager), clusterManager, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
50 | }
51 | return clusterManager;
52 | }
53 |
54 | - (id)dataSource {
55 | return objc_getAssociatedObject(self, @selector(dataSource));
56 | }
57 |
58 | - (void)setDataSource:(id)dataSource {
59 | objc_setAssociatedObject(self, @selector(dataSource), dataSource, OBJC_ASSOCIATION_ASSIGN);
60 | }
61 |
62 | - (NSMapTable *)markers {
63 | NSMapTable *markers = objc_getAssociatedObject(self, @selector(markers));
64 | if (!markers) {
65 | markers = [NSMapTable strongToStrongObjectsMapTable];
66 | objc_setAssociatedObject(self, @selector(markers), markers, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
67 | }
68 | return markers;
69 | }
70 |
71 | - (GMSMarker *)markerForCluster:(CKCluster*)cluster {
72 | return [self.markers objectForKey:cluster];
73 | }
74 |
75 | - (MKMapRect)visibleMapRect {
76 | GMSCoordinateBounds *bounds = [[GMSCoordinateBounds alloc] initWithRegion:self.projection.visibleRegion];
77 | MKMapPoint sw = MKMapPointForCoordinate(bounds.southWest);
78 | MKMapPoint ne = MKMapPointForCoordinate(bounds.northEast);
79 |
80 | double x = sw.x;
81 | double y = ne.y;
82 |
83 | double width = ne.x - sw.x;
84 | double height = sw.y - ne.y;
85 |
86 | // Handle antimeridian crossing
87 | if (width < 0) {
88 | width = ne.x + MKMapSizeWorld.width - sw.x;
89 | }
90 | if (height < 0) {
91 | height = sw.y + MKMapSizeWorld.height - ne.y;
92 | }
93 |
94 | return MKMapRectMake(x, y, width, height);
95 | }
96 |
97 | - (double)zoom {
98 | GMSCoordinateBounds *bounds = [[GMSCoordinateBounds alloc] initWithRegion:self.projection.visibleRegion];
99 | double longitudeDelta = bounds.northEast.longitude - bounds.southWest.longitude;
100 |
101 | // Handle antimeridian crossing
102 | if (longitudeDelta < 0) {
103 | longitudeDelta = 360 + bounds.northEast.longitude - bounds.southWest.longitude;
104 | }
105 |
106 | return log2(360 * self.frame.size.width / (256 * longitudeDelta));
107 | }
108 |
109 | - (void)addCluster:(CKCluster *)cluster {
110 | GMSMarker *marker = nil;
111 | if ([self.dataSource respondsToSelector:@selector(mapView:markerForCluster:)]) {
112 | marker = [self.dataSource mapView:self markerForCluster:cluster];
113 | } else {
114 | marker = [GMSMarker markerWithPosition:cluster.coordinate];
115 | if(cluster.count > 1) {
116 | marker.icon = [GMSMarker markerImageWithColor:[UIColor purpleColor]];
117 | }
118 | }
119 |
120 | marker.cluster = cluster;
121 | marker.zIndex = 1;
122 | marker.map = self;
123 | [self.markers setObject:marker forKey:cluster];
124 | }
125 |
126 | - (void)removeCluster:(CKCluster *)cluster {
127 | GMSMarker *marker = [self.markers objectForKey:cluster];
128 | marker.map = nil;
129 | [self.markers removeObjectForKey:cluster];
130 | }
131 |
132 | - (void)addClusters:(NSArray *)clusters {
133 | for (CKCluster *cluster in clusters) {
134 | [self addCluster:cluster];
135 | }
136 | }
137 |
138 | - (void)removeClusters:(NSArray *)clusters {
139 | for (CKCluster *cluster in clusters) {
140 | [self removeCluster:cluster];
141 | }
142 | }
143 |
144 | - (void)performAnimations:(NSArray *)animations completion:(void (^__nullable)(BOOL finished))completion {
145 |
146 | void (^animationsBlock)(void) = ^{};
147 |
148 | void (^completionBlock)(BOOL finished) = ^(BOOL finished){
149 | if (completion) completion(finished);
150 | };
151 |
152 | for (CKClusterAnimation *animation in animations) {
153 | GMSMarker *marker = [self.markers objectForKey:animation.cluster];
154 |
155 | marker.zIndex = 0;
156 | marker.position = animation.from;
157 |
158 | animationsBlock = ^{
159 | animationsBlock();
160 | marker.layer.latitude = animation.to.latitude;
161 | marker.layer.longitude = animation.to.longitude;
162 | };
163 |
164 | completionBlock = ^(BOOL finished){
165 | marker.zIndex = 1;
166 | completionBlock(finished);
167 | };
168 | }
169 |
170 | if ([self.clusterManager.delegate respondsToSelector:@selector(clusterManager:performAnimations:completion:)]) {
171 | [self.clusterManager.delegate clusterManager:self.clusterManager
172 | performAnimations:animationsBlock
173 | completion:completionBlock];
174 | } else {
175 | CAMediaTimingFunction *curve = nil;
176 | switch (self.clusterManager.animationOptions) {
177 | case UIViewAnimationOptionCurveEaseInOut:
178 | curve = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
179 | break;
180 | case UIViewAnimationOptionCurveEaseIn:
181 | curve = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
182 | break;
183 | case UIViewAnimationOptionCurveEaseOut:
184 | curve = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
185 | break;
186 | case UIViewAnimationOptionCurveLinear:
187 | curve = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
188 | break;
189 | default:
190 | curve = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionDefault];
191 | }
192 |
193 | [CATransaction begin];
194 | [CATransaction setAnimationDuration:self.clusterManager.animationDuration];
195 | [CATransaction setAnimationTimingFunction:curve];
196 | [CATransaction setCompletionBlock:^{
197 | completionBlock(YES);
198 | }];
199 | animationsBlock();
200 | [CATransaction commit];
201 | }
202 | }
203 |
204 | - (void)selectCluster:(CKCluster *)cluster animated:(BOOL)animated {
205 | GMSMarker *marker = [self.markers objectForKey:cluster];
206 | if (marker != self.selectedMarker) {
207 | marker.map = self;
208 | self.selectedMarker = marker;
209 | }
210 | }
211 |
212 | - (void)deselectCluster:(CKCluster *)cluster animated:(BOOL)animated {
213 | GMSMarker *marker = [self.markers objectForKey:cluster];
214 | if (marker == self.selectedMarker) {
215 | self.selectedMarker = nil;
216 | }
217 | }
218 |
219 | @end
220 |
221 | @implementation GMSCameraUpdate (ClusterKit)
222 |
223 | + (GMSCameraUpdate *)fitCluster:(CKCluster *)cluster {
224 | return [self fitCluster:cluster withPadding:64];
225 | }
226 |
227 | + (GMSCameraUpdate *)fitCluster:(CKCluster *)cluster withPadding:(CGFloat)padding {
228 | UIEdgeInsets edgeInsets = UIEdgeInsetsMake(padding, padding, padding, padding);
229 | return [self fitCluster:cluster withEdgeInsets:edgeInsets];
230 | }
231 |
232 | + (GMSCameraUpdate *)fitCluster:(CKCluster *)cluster withEdgeInsets:(UIEdgeInsets)edgeInsets {
233 | GMSCoordinateBounds *bounds = [[GMSCoordinateBounds alloc] initWithCoordinate:cluster.coordinate coordinate:cluster.coordinate];
234 |
235 | for (id marker in cluster) {
236 | bounds = [bounds includingCoordinate:marker.coordinate];
237 | }
238 | return [GMSCameraUpdate fitBounds:bounds withEdgeInsets:edgeInsets];
239 | }
240 |
241 | @end
242 |
--------------------------------------------------------------------------------
/Sources/ClusterKit/Tree/CKQuadTree.m:
--------------------------------------------------------------------------------
1 | // CKQuadTree.m
2 | //
3 | // Copyright © 2017 Hulab. All rights reserved.
4 | //
5 | // Permission is hereby granted, free of charge, to any person obtaining a copy
6 | // of this software and associated documentation files (the "Software"), to deal
7 | // in the Software without restriction, including without limitation the rights
8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | // copies of the Software, and to permit persons to whom the Software is
10 | // furnished to do so, subject to the following conditions:
11 | //
12 | // The above copyright notice and this permission notice shall be included in
13 | // all copies or substantial portions of the Software.
14 | //
15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | // THE SOFTWARE.
22 |
23 | #import
24 |
25 | /// Quadtree point
26 | typedef struct hb_qpoint {
27 | MKMapPoint point;
28 | __unsafe_unretained id annotation;
29 | struct hb_qpoint *next;
30 | } hb_qpoint_t;
31 |
32 | /// Quadtree node
33 | typedef struct hb_qnode {
34 | NSUInteger cap; ///< Capacity of the node
35 | NSUInteger cnt; ///< Number of point in the node
36 | MKMapRect bound; ///< Area covered by the node
37 | hb_qpoint_t *points; ///< Chained list of node's points
38 | struct hb_qnode *nw; ///< NW quadrant of the node
39 | struct hb_qnode *ne; ///< NE quadrant of the node
40 | struct hb_qnode *sw; ///< SW quadrant of the node
41 | struct hb_qnode *se; ///< SE quadrant of the node
42 | } hb_qnode_t;
43 |
44 | /// Quadtree container
45 | typedef struct hb_qtree {
46 | hb_qnode_t *root; ///< Root node
47 | } hb_qtree_t;
48 |
49 | static hb_qnode_t *hb_qnode_new(MKMapRect bound, NSUInteger capacity) {
50 | hb_qnode_t *n = malloc(sizeof(hb_qnode_t));
51 | memset(n, 0, sizeof(hb_qnode_t));
52 |
53 | n->bound = bound;
54 | n->cap = capacity;
55 | return n;
56 | }
57 |
58 | static void hb_qpoint_free(hb_qpoint_t *p) {
59 | if (p) {
60 | hb_qpoint_free(p->next);
61 | free(p);
62 | }
63 | }
64 |
65 | static void hb_qnode_free(hb_qnode_t *n) {
66 | hb_qpoint_free(n->points);
67 | n->cnt = 0;
68 |
69 | if(n->nw) {
70 | hb_qnode_free(n->nw);
71 | hb_qnode_free(n->ne);
72 | hb_qnode_free(n->sw);
73 | hb_qnode_free(n->se);
74 | }
75 | free(n);
76 | }
77 |
78 | static void add_(hb_qnode_t *n, hb_qpoint_t *p) {
79 | p->next = n->points;
80 | n->points = p;
81 | n->cnt++;
82 | }
83 |
84 | static bool drop_(hb_qnode_t *n, id a) {
85 |
86 | for (hb_qpoint_t *cur = n->points, *prev = NULL;
87 | cur != NULL;
88 | prev = cur, cur = cur->next) {
89 |
90 | if (cur->annotation == a) {
91 | if (prev == NULL) {
92 | n->points = cur->next;
93 | } else {
94 | prev->next = cur->next;
95 | }
96 | free(cur);
97 | n->cnt--;
98 | return true;
99 | }
100 | }
101 | return false;
102 | }
103 |
104 | static void subdivide_(hb_qnode_t *n) {
105 | MKMapRect bd = n->bound;
106 | MKMapRect nw;
107 | MKMapRect ne;
108 | MKMapRect sw;
109 | MKMapRect se;
110 |
111 | MKMapRectDivide(bd, &nw, &ne, MKMapRectGetWidth (bd) / 2, CGRectMaxXEdge);
112 | MKMapRectDivide(nw, &nw, &sw, MKMapRectGetHeight(nw) / 2, CGRectMaxYEdge);
113 | MKMapRectDivide(ne, &ne, &se, MKMapRectGetHeight(ne) / 2, CGRectMaxYEdge);
114 |
115 | n->nw = hb_qnode_new(nw, n->cap);
116 | n->ne = hb_qnode_new(ne, n->cap);
117 | n->sw = hb_qnode_new(sw, n->cap);
118 | n->se = hb_qnode_new(se, n->cap);
119 | }
120 |
121 | static bool hb_qnode_insert(hb_qnode_t *n, id a) {
122 | MKMapPoint point = MKMapPointForCoordinate(a.coordinate);
123 |
124 | if(!MKMapRectContainsPoint(n->bound, point)) return false;
125 |
126 | if(n->cnt < n->cap) {
127 | hb_qpoint_t *p = malloc(sizeof(hb_qpoint_t));
128 | p->annotation = a;
129 | p->point = point;
130 | add_(n, p);
131 | return true;
132 | }
133 |
134 | if(!n->nw) {
135 | subdivide_(n);
136 | }
137 |
138 | if(hb_qnode_insert(n->nw, a)) return true;
139 | if(hb_qnode_insert(n->ne, a)) return true;
140 | if(hb_qnode_insert(n->sw, a)) return true;
141 | if(hb_qnode_insert(n->se, a)) return true;
142 |
143 | return false;
144 | }
145 |
146 | static bool hb_qnode_remove(hb_qnode_t *n, id a) {
147 | if(drop_(n, a)) return true;
148 |
149 | if(n->nw) {
150 | if(hb_qnode_remove(n->nw, a)) return true;
151 | if(hb_qnode_remove(n->ne, a)) return true;
152 | if(hb_qnode_remove(n->sw, a)) return true;
153 | if(hb_qnode_remove(n->se, a)) return true;
154 | }
155 |
156 | return false;
157 | }
158 |
159 | static void hb_qnode_get_in_range(hb_qnode_t *n, MKMapRect range, void(^find)(idannotation)) {
160 |
161 | if(n->cnt) {
162 | if(!MKMapRectIntersectsRect(n->bound, range)) return;
163 |
164 | hb_qpoint_t *p = n->points;
165 | while (p) {
166 | if(MKMapRectContainsPoint(range, p->point)) {
167 | find(p->annotation);
168 | }
169 | p = p->next;
170 | }
171 | }
172 |
173 | if(n->nw) {
174 | hb_qnode_get_in_range(n->nw, range, find);
175 | hb_qnode_get_in_range(n->ne, range, find);
176 | hb_qnode_get_in_range(n->sw, range, find);
177 | hb_qnode_get_in_range(n->se, range, find);
178 | }
179 | }
180 |
181 | /* publics */
182 |
183 | hb_qtree_t *hb_qtree_new(MKMapRect rect, NSUInteger cap) {
184 | hb_qtree_t *t = malloc(sizeof(hb_qtree_t));
185 | t->root = hb_qnode_new(rect, cap);
186 | return t;
187 | }
188 |
189 | void hb_qtree_free(hb_qtree_t *t) {
190 | if(t->root) hb_qnode_free(t->root);
191 | free(t);
192 | }
193 |
194 | void hb_qtree_insert(hb_qtree_t *t, id annotation) {
195 | hb_qnode_insert(t->root, annotation);
196 | }
197 |
198 | void hb_qtree_remove(hb_qtree_t *t, id annotation) {
199 | hb_qnode_remove(t->root, annotation);
200 | }
201 |
202 | void hb_qtree_clear(hb_qtree_t *t) {
203 | MKMapRect bound = t->root->bound;
204 | NSUInteger cap = t->root->cap;
205 | hb_qnode_free(t->root);
206 | t->root = hb_qnode_new(bound, cap);
207 | }
208 |
209 | void hb_qtree_find_in_range(hb_qtree_t *t, MKMapRect range , void(^find)(idannotation)) {
210 | hb_qnode_get_in_range(t->root, range, find);
211 | }
212 |
213 | @interface CKQuadTree ()
214 | @property (nonatomic, copy) NSArray *annotations;
215 | @property (nonatomic, assign) hb_qtree_t *tree;
216 | @end
217 |
218 | @implementation CKQuadTree {
219 | BOOL _delegate_responds;
220 | }
221 |
222 | static void * const CKQuadTreeKVOContext = (void *)&CKQuadTreeKVOContext;
223 |
224 | @synthesize delegate = _delegate;
225 |
226 | - (instancetype)init {
227 | return [self initWithAnnotations:[NSArray array]];
228 | }
229 |
230 | - (instancetype)initWithAnnotations:(NSArray> *)annotations {
231 | self = [super init];
232 | if (self) {
233 | self.annotations = annotations;
234 |
235 | self.tree = hb_qtree_new(MKMapRectWorld, CK_QTREE_STDCAP);
236 |
237 | for (NSObject *annotation in annotations) {
238 | hb_qtree_insert(self.tree, annotation);
239 |
240 | [annotation addObserver:self
241 | forKeyPath:NSStringFromSelector(@selector(coordinate))
242 | options:NSKeyValueObservingOptionNew
243 | context:CKQuadTreeKVOContext];
244 | }
245 | }
246 | return self;
247 | }
248 |
249 | - (NSArray> *)annotationsInRect:(MKMapRect)rect {
250 | NSMutableArray *results = [NSMutableArray new];
251 |
252 | // For map rects that span the 180th meridian, we get the portion outside the world.
253 | if (MKMapRectSpans180thMeridian(rect)) {
254 |
255 | hb_qtree_find_in_range(self.tree, MKMapRectRemainder(rect), ^(id annotation) {
256 | if (!self->_delegate_responds || [self.delegate annotationTree:self shouldExtractAnnotation:annotation]) {
257 | [results addObject:annotation];
258 | }
259 | });
260 |
261 | rect = MKMapRectIntersection(rect, MKMapRectWorld);
262 | }
263 |
264 | hb_qtree_find_in_range(self.tree, rect, ^(id annotation) {
265 | if (!self->_delegate_responds || [self.delegate annotationTree:self shouldExtractAnnotation:annotation]) {
266 | [results addObject:annotation];
267 | }
268 | });
269 |
270 | return results;
271 | }
272 |
273 | - (void)setDelegate:(id)delegate {
274 | _delegate = delegate;
275 |
276 | //Cache whether the delegate responds to a selector
277 | _delegate_responds = [self.delegate respondsToSelector:@selector(annotationTree:shouldExtractAnnotation:)];
278 | }
279 |
280 | - (void)dealloc {
281 | for (NSObject *annotation in self.annotations) {
282 | [annotation removeObserver:self
283 | forKeyPath:NSStringFromSelector(@selector(coordinate))
284 | context:CKQuadTreeKVOContext];
285 | }
286 |
287 | hb_qtree_free(self.tree);
288 | }
289 |
290 | - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
291 |
292 | if (context == CKQuadTreeKVOContext) {
293 |
294 | if ([keyPath isEqualToString:NSStringFromSelector(@selector(coordinate))]) {
295 | hb_qtree_remove(self.tree, object);
296 | hb_qtree_insert(self.tree, object);
297 | }
298 |
299 | } else {
300 | [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
301 | }
302 | }
303 |
304 | @end
305 |
--------------------------------------------------------------------------------