├── Resources ├── mapbox.gif ├── mapkit.gif ├── git_banner.png └── googlemaps.gif ├── Examples ├── Assets.xcassets │ ├── Contents.json │ ├── apple.imageset │ │ ├── apple.png │ │ ├── apple@2x.png │ │ ├── apple@3x.png │ │ └── Contents.json │ ├── cluster.imageset │ │ ├── cluster.png │ │ ├── cluster@2x.png │ │ ├── cluster@3x.png │ │ └── Contents.json │ ├── google.imageset │ │ ├── google.png │ │ ├── google@2x.png │ │ ├── google@3x.png │ │ └── Contents.json │ ├── marker.imageset │ │ ├── marker.png │ │ ├── marker@2x.png │ │ ├── marker@3x.png │ │ └── Contents.json │ ├── yandex.imageset │ │ ├── yandex.png │ │ ├── yandex@2x.png │ │ ├── yandex@3x.png │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── iPad - 20pt.png │ │ ├── iPad - 29pt.png │ │ ├── iPad - 40pt.png │ │ ├── iPad - 76pt.png │ │ ├── iPad - 20pt@2x.png │ │ ├── iPad - 29pt@2x.png │ │ ├── iPad - 40pt@2x.png │ │ ├── iPad - 76pt@2x.png │ │ ├── iPad - 83.5pt.png │ │ ├── iPhone - 20pt@2x.png │ │ ├── iPhone - 20pt@3x.png │ │ ├── iPhone - 29pt@2x.png │ │ ├── iPhone - 29pt@3x.png │ │ ├── iPhone - 40pt@2x.png │ │ ├── iPhone - 40pt@3x.png │ │ ├── iPhone - 60pt@2x.png │ │ ├── iPhone - 60pt@3x.png │ │ └── Contents.json │ └── mapbox-logo-white.imageset │ │ ├── mapbox-logo-white.png │ │ ├── mapbox-logo-white@2x.png │ │ ├── mapbox-logo-white@3x.png │ │ └── Contents.json ├── Examples.xcodeproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── Example-objc │ ├── YandexMapKit │ │ ├── CKYandexMapViewController.h │ │ └── CKYandexMapViewController.m │ ├── main.m │ ├── GoogleMaps │ │ ├── CKGoogleMapsViewController.h │ │ └── CKGoogleMapsViewController.m │ ├── CKAppDelegate.h │ ├── MapKit │ │ ├── CKMapKitViewController.h │ │ └── CKMapKitViewController.m │ ├── Mapbox │ │ ├── CKMapboxViewController.h │ │ └── CKMapboxViewController.m │ ├── Info.plist │ ├── Base.lproj │ │ └── LaunchScreen.storyboard │ └── CKAppDelegate.m ├── Example-swift │ ├── Example-swift-Bridging-Header.h │ ├── Info.plist │ ├── Base.lproj │ │ └── LaunchScreen.storyboard │ ├── AppDelegate.swift │ ├── GoogleMapsViewController.swift │ ├── YandexMapViewController.swift │ ├── MapKitViewController.swift │ └── MapboxViewController.swift ├── Podfile ├── Example-data │ ├── Info.plist │ ├── ExampleData.h │ ├── CKGeoPointOperation.h │ └── CKGeoPointOperation.m └── Podfile.lock ├── .gitignore ├── ClusterKit.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ └── ClusterKit.xcscheme ├── Sources ├── ClusterKit │ ├── include │ │ ├── module.modulemap │ │ └── ClusterKit │ │ │ ├── ClusterKit.h │ │ │ ├── CKGridBasedAlgorithm.h │ │ │ ├── CKNonHierarchicalDistanceBasedAlgorithm.h │ │ │ ├── MKMapView+ClusterKit.h │ │ │ ├── CKClusterAlgorithm.h │ │ │ ├── CKAnnotationTree.h │ │ │ ├── CKQuadTree.h │ │ │ ├── CKMap.h │ │ │ ├── CKCluster.h │ │ │ └── CKClusterManager.h │ ├── Info.plist │ ├── Algorithm │ │ ├── CKClusterAlgorithm.m │ │ ├── CKGridBasedAlgorithm.m │ │ └── CKNonHierarchicalDistanceBasedAlgorithm.m │ ├── MapKit │ │ └── MKMapView+ClusterKit.m │ └── Tree │ │ └── CKQuadTree.m ├── Mapbox │ ├── MGLMapView+ClusterKit.h │ └── MGLMapView+ClusterKit.m ├── YandexMapKit │ └── YMKMapView+ClusterKit.h └── GoogleMaps │ ├── GMSMapView+ClusterKit.h │ └── GMSMapView+ClusterKit.m ├── Tests └── ClusterKitTests │ ├── Info.plist │ ├── CKAnnotation.h │ ├── CKAnnotation.m │ ├── CKGridBasedAlgorithmTest.m │ ├── CKQuadTreeTest.m │ └── CKNonHierarchicalDistanceBasedAlgorithmTest.m ├── Package.swift ├── LICENSE ├── .travis.yml ├── ClusterKit.podspec ├── README.md └── CHANGELOG.md /Resources/mapbox.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hulab/ClusterKit/HEAD/Resources/mapbox.gif -------------------------------------------------------------------------------- /Resources/mapkit.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hulab/ClusterKit/HEAD/Resources/mapkit.gif -------------------------------------------------------------------------------- /Resources/git_banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hulab/ClusterKit/HEAD/Resources/git_banner.png -------------------------------------------------------------------------------- /Resources/googlemaps.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hulab/ClusterKit/HEAD/Resources/googlemaps.gif -------------------------------------------------------------------------------- /Examples/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Examples/Assets.xcassets/apple.imageset/apple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hulab/ClusterKit/HEAD/Examples/Assets.xcassets/apple.imageset/apple.png -------------------------------------------------------------------------------- /Examples/Assets.xcassets/apple.imageset/apple@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hulab/ClusterKit/HEAD/Examples/Assets.xcassets/apple.imageset/apple@2x.png -------------------------------------------------------------------------------- /Examples/Assets.xcassets/apple.imageset/apple@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hulab/ClusterKit/HEAD/Examples/Assets.xcassets/apple.imageset/apple@3x.png -------------------------------------------------------------------------------- /Examples/Assets.xcassets/cluster.imageset/cluster.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hulab/ClusterKit/HEAD/Examples/Assets.xcassets/cluster.imageset/cluster.png -------------------------------------------------------------------------------- /Examples/Assets.xcassets/google.imageset/google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hulab/ClusterKit/HEAD/Examples/Assets.xcassets/google.imageset/google.png -------------------------------------------------------------------------------- /Examples/Assets.xcassets/marker.imageset/marker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hulab/ClusterKit/HEAD/Examples/Assets.xcassets/marker.imageset/marker.png -------------------------------------------------------------------------------- /Examples/Assets.xcassets/yandex.imageset/yandex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hulab/ClusterKit/HEAD/Examples/Assets.xcassets/yandex.imageset/yandex.png -------------------------------------------------------------------------------- /Examples/Assets.xcassets/google.imageset/google@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hulab/ClusterKit/HEAD/Examples/Assets.xcassets/google.imageset/google@2x.png -------------------------------------------------------------------------------- /Examples/Assets.xcassets/google.imageset/google@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hulab/ClusterKit/HEAD/Examples/Assets.xcassets/google.imageset/google@3x.png -------------------------------------------------------------------------------- /Examples/Assets.xcassets/marker.imageset/marker@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hulab/ClusterKit/HEAD/Examples/Assets.xcassets/marker.imageset/marker@2x.png -------------------------------------------------------------------------------- /Examples/Assets.xcassets/marker.imageset/marker@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hulab/ClusterKit/HEAD/Examples/Assets.xcassets/marker.imageset/marker@3x.png -------------------------------------------------------------------------------- /Examples/Assets.xcassets/yandex.imageset/yandex@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hulab/ClusterKit/HEAD/Examples/Assets.xcassets/yandex.imageset/yandex@2x.png -------------------------------------------------------------------------------- /Examples/Assets.xcassets/yandex.imageset/yandex@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hulab/ClusterKit/HEAD/Examples/Assets.xcassets/yandex.imageset/yandex@3x.png -------------------------------------------------------------------------------- /Examples/Assets.xcassets/cluster.imageset/cluster@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hulab/ClusterKit/HEAD/Examples/Assets.xcassets/cluster.imageset/cluster@2x.png -------------------------------------------------------------------------------- /Examples/Assets.xcassets/cluster.imageset/cluster@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hulab/ClusterKit/HEAD/Examples/Assets.xcassets/cluster.imageset/cluster@3x.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /.swiftpm 4 | /Packages 5 | /*.xcodeproj 6 | xcuserdata 7 | Carthage 8 | Examples/Examples.xcworkspace 9 | Examples/Pods 10 | -------------------------------------------------------------------------------- /Examples/Assets.xcassets/AppIcon.appiconset/iPad - 20pt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hulab/ClusterKit/HEAD/Examples/Assets.xcassets/AppIcon.appiconset/iPad - 20pt.png -------------------------------------------------------------------------------- /Examples/Assets.xcassets/AppIcon.appiconset/iPad - 29pt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hulab/ClusterKit/HEAD/Examples/Assets.xcassets/AppIcon.appiconset/iPad - 29pt.png -------------------------------------------------------------------------------- /Examples/Assets.xcassets/AppIcon.appiconset/iPad - 40pt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hulab/ClusterKit/HEAD/Examples/Assets.xcassets/AppIcon.appiconset/iPad - 40pt.png -------------------------------------------------------------------------------- /Examples/Assets.xcassets/AppIcon.appiconset/iPad - 76pt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hulab/ClusterKit/HEAD/Examples/Assets.xcassets/AppIcon.appiconset/iPad - 76pt.png -------------------------------------------------------------------------------- /Examples/Assets.xcassets/AppIcon.appiconset/iPad - 20pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hulab/ClusterKit/HEAD/Examples/Assets.xcassets/AppIcon.appiconset/iPad - 20pt@2x.png -------------------------------------------------------------------------------- /Examples/Assets.xcassets/AppIcon.appiconset/iPad - 29pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hulab/ClusterKit/HEAD/Examples/Assets.xcassets/AppIcon.appiconset/iPad - 29pt@2x.png -------------------------------------------------------------------------------- /Examples/Assets.xcassets/AppIcon.appiconset/iPad - 40pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hulab/ClusterKit/HEAD/Examples/Assets.xcassets/AppIcon.appiconset/iPad - 40pt@2x.png -------------------------------------------------------------------------------- /Examples/Assets.xcassets/AppIcon.appiconset/iPad - 76pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hulab/ClusterKit/HEAD/Examples/Assets.xcassets/AppIcon.appiconset/iPad - 76pt@2x.png -------------------------------------------------------------------------------- /Examples/Assets.xcassets/AppIcon.appiconset/iPad - 83.5pt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hulab/ClusterKit/HEAD/Examples/Assets.xcassets/AppIcon.appiconset/iPad - 83.5pt.png -------------------------------------------------------------------------------- /Examples/Assets.xcassets/AppIcon.appiconset/iPhone - 20pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hulab/ClusterKit/HEAD/Examples/Assets.xcassets/AppIcon.appiconset/iPhone - 20pt@2x.png -------------------------------------------------------------------------------- /Examples/Assets.xcassets/AppIcon.appiconset/iPhone - 20pt@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hulab/ClusterKit/HEAD/Examples/Assets.xcassets/AppIcon.appiconset/iPhone - 20pt@3x.png -------------------------------------------------------------------------------- /Examples/Assets.xcassets/AppIcon.appiconset/iPhone - 29pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hulab/ClusterKit/HEAD/Examples/Assets.xcassets/AppIcon.appiconset/iPhone - 29pt@2x.png -------------------------------------------------------------------------------- /Examples/Assets.xcassets/AppIcon.appiconset/iPhone - 29pt@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hulab/ClusterKit/HEAD/Examples/Assets.xcassets/AppIcon.appiconset/iPhone - 29pt@3x.png -------------------------------------------------------------------------------- /Examples/Assets.xcassets/AppIcon.appiconset/iPhone - 40pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hulab/ClusterKit/HEAD/Examples/Assets.xcassets/AppIcon.appiconset/iPhone - 40pt@2x.png -------------------------------------------------------------------------------- /Examples/Assets.xcassets/AppIcon.appiconset/iPhone - 40pt@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hulab/ClusterKit/HEAD/Examples/Assets.xcassets/AppIcon.appiconset/iPhone - 40pt@3x.png -------------------------------------------------------------------------------- /Examples/Assets.xcassets/AppIcon.appiconset/iPhone - 60pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hulab/ClusterKit/HEAD/Examples/Assets.xcassets/AppIcon.appiconset/iPhone - 60pt@2x.png -------------------------------------------------------------------------------- /Examples/Assets.xcassets/AppIcon.appiconset/iPhone - 60pt@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hulab/ClusterKit/HEAD/Examples/Assets.xcassets/AppIcon.appiconset/iPhone - 60pt@3x.png -------------------------------------------------------------------------------- /Examples/Assets.xcassets/mapbox-logo-white.imageset/mapbox-logo-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hulab/ClusterKit/HEAD/Examples/Assets.xcassets/mapbox-logo-white.imageset/mapbox-logo-white.png -------------------------------------------------------------------------------- /Examples/Assets.xcassets/mapbox-logo-white.imageset/mapbox-logo-white@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hulab/ClusterKit/HEAD/Examples/Assets.xcassets/mapbox-logo-white.imageset/mapbox-logo-white@2x.png -------------------------------------------------------------------------------- /Examples/Assets.xcassets/mapbox-logo-white.imageset/mapbox-logo-white@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hulab/ClusterKit/HEAD/Examples/Assets.xcassets/mapbox-logo-white.imageset/mapbox-logo-white@3x.png -------------------------------------------------------------------------------- /Examples/Examples.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ClusterKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Sources/ClusterKit/include/module.modulemap: -------------------------------------------------------------------------------- 1 | module ClusterKit { 2 | umbrella header "ClusterKit/ClusterKit.h" 3 | export * 4 | 5 | explicit module MapKit { 6 | header "ClusterKit/MKMapView+ClusterKit.h" 7 | export * 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /ClusterKit.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Examples/Example-objc/YandexMapKit/CKYandexMapViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // CKYandexMapViewController.h 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 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface CKYandexMapViewController : UIViewController 14 | 15 | @end 16 | 17 | NS_ASSUME_NONNULL_END 18 | -------------------------------------------------------------------------------- /Examples/Example-objc/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // ClusterKit-objc 4 | // 5 | // Created by Maxime Epain on 15/12/2016. 6 | // Copyright © 2016 Hulab. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "CKAppDelegate.h" 11 | 12 | int main(int argc, char * argv[]) { 13 | @autoreleasepool { 14 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([CKAppDelegate class])); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Examples/Example-swift/Example-swift-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Example-swift-Bridging-Header.h 3 | // Examples 4 | // 5 | // Created by petropavel on 24/01/2019. 6 | // Copyright © 2019 Hulab. All rights reserved. 7 | // 8 | 9 | #ifndef Example_swift_Bridging_Header_h 10 | #define Example_swift_Bridging_Header_h 11 | 12 | #import "GMSMapView+ClusterKit.h" 13 | #import "YMKMapView+ClusterKit.h" 14 | 15 | #endif /* Example_swift_Bridging_Header_h */ 16 | -------------------------------------------------------------------------------- /Examples/Podfile: -------------------------------------------------------------------------------- 1 | source 'https://github.com/CocoaPods/Specs.git' 2 | platform :ios, '9.0' 3 | use_frameworks! 4 | 5 | pod 'ClusterKit', :path => '../.' 6 | pod 'ClusterKit/Mapbox', :path => '../.' 7 | 8 | target 'Example-data' do 9 | pod 'GeoJSONSerialization' 10 | end 11 | 12 | target 'Example-objc' do 13 | pod 'GoogleMaps' 14 | pod 'YandexMapKit' 15 | end 16 | 17 | target 'Example-swift' do 18 | pod 'GoogleMaps' 19 | pod 'YandexMapKit' 20 | end 21 | 22 | -------------------------------------------------------------------------------- /Examples/Assets.xcassets/apple.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "apple.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "apple@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "apple@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /Examples/Assets.xcassets/google.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "google.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "google@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "google@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /Examples/Assets.xcassets/marker.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "marker.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "marker@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "marker@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /Examples/Assets.xcassets/yandex.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "yandex.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "yandex@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "yandex@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /Examples/Assets.xcassets/cluster.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "cluster.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "cluster@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "cluster@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /Examples/Assets.xcassets/mapbox-logo-white.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "mapbox-logo-white.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "mapbox-logo-white@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "mapbox-logo-white@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /Tests/ClusterKitTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Examples/Example-data/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSPrincipalClass 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Sources/ClusterKit/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSPrincipalClass 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.2 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "ClusterKit", 6 | platforms: [ 7 | .macOS(.v10_15), .iOS(.v9), .tvOS(.v13) 8 | ], 9 | products: [ 10 | // Products define the executables and libraries a package produces, and make them visible to other packages. 11 | .library( 12 | name: "ClusterKit", 13 | targets: ["ClusterKit"]) 14 | ], 15 | dependencies: [ 16 | // Dependencies declare other packages that this package depends on. 17 | ], 18 | targets: [ 19 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 20 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 21 | .target( 22 | name: "ClusterKit" 23 | ), 24 | ] 25 | ) 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2017 Hulab. All rights reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Examples/Example-swift/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UIRequiresFullScreen 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | UIInterfaceOrientationLandscapeRight 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /Examples/Example-objc/GoogleMaps/CKGoogleMapsViewController.h: -------------------------------------------------------------------------------- 1 | // CKGoogleMapsViewController.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 | @interface CKGoogleMapsViewController : UIViewController 26 | 27 | @end 28 | -------------------------------------------------------------------------------- /Examples/Example-objc/CKAppDelegate.h: -------------------------------------------------------------------------------- 1 | // CKAppDelegate.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 | @interface CKAppDelegate : UIResponder 26 | 27 | @property (strong, nonatomic) UIWindow *window; 28 | 29 | @end 30 | 31 | -------------------------------------------------------------------------------- /Tests/ClusterKitTests/CKAnnotation.h: -------------------------------------------------------------------------------- 1 | // CKAnnotation.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 | @interface CKAnnotation : NSObject 26 | 27 | @property (nonatomic, readwrite) CLLocationCoordinate2D coordinate; 28 | 29 | @end 30 | -------------------------------------------------------------------------------- /Tests/ClusterKitTests/CKAnnotation.m: -------------------------------------------------------------------------------- 1 | // CKAnnotation.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 "CKAnnotation.h" 24 | 25 | @implementation CKAnnotation 26 | 27 | - (NSString *)title { 28 | return [NSString stringWithFormat:@"(%f, %f)", self.coordinate.latitude, self.coordinate.longitude]; 29 | } 30 | 31 | @end 32 | -------------------------------------------------------------------------------- /Examples/Example-objc/MapKit/CKMapKitViewController.h: -------------------------------------------------------------------------------- 1 | // CKMapKitViewController.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 | @interface CKMapKitViewController : UIViewController 26 | 27 | @end 28 | 29 | @interface CKAnnotationView : MKAnnotationView 30 | 31 | @end 32 | 33 | @interface CKClusterView : MKAnnotationView 34 | 35 | @end 36 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | osx_image: xcode10.2 3 | env: 4 | global: 5 | - FRAMEWORK_NAME=ClusterKit 6 | 7 | before_install: 8 | - brew update 9 | - brew outdated carthage || brew upgrade carthage 10 | 11 | install: 12 | - gem install cocoapods 13 | - gem install xcpretty --no-document --quiet 14 | 15 | script: 16 | - set -o pipefail && xcodebuild -project ClusterKit.xcodeproj -scheme $FRAMEWORK_NAME -sdk iphonesimulator12.2 -configuration Release -destination "platform=iOS Simulator,name=iPhone 8" clean build test | xcpretty -c 17 | - pod lib lint --quick 18 | 19 | before_deploy: 20 | - carthage build --no-skip-current 21 | - carthage archive $FRAMEWORK_NAME 22 | 23 | deploy: 24 | provider: releases 25 | api_key: 26 | secure: SQh6dfiety7M8KOnIeRAE4f1m1YXswch/8t/3/AEXymp5v35o2DUQ0P9zLyJlwuOLF9zPrT10Po2p34K8DYClNeA+6n/f3ptGozLtXEm2Whl99i/Kq54H6v1XbiRTUR61t5qjTGqxQnR/POYSQAYU+gwt9/pQ5l3YVIrfeUIppgy8VQi0WzknzeEHOvdld9GvgJ5hetQNXctmMYGFTfYpqfUbAajcLhfvic/qrhZMmhPwCf5FLIbtkRxOcImqATHVtZ0JW/mXEDb5eatsfP1JpQJ9pclC/5K+1DWfGv+AA3QbSOod4JuDnJG9JYwWBjGmtKd+4TuiO8HzawlydpK3gE3vVBOSQB5EI646f2AlQaxzWZCCplFBsIbpGamTcv3KJ+Xy4lsT7AFfH6aqDQsspoZhgFbImSYrKJVD/rypmS7dsKknPjUk0w0z3D+cxkvCZ+VC4TkQW5IIvQoiGXP03Yqg3zF4jUhZywEDXFVYJqLIZpoVL8P8zAp8PMTkULMZCGsSkH+vFOzxa1DwK+BY1VVqocCF0tkothG+WrWeH0Cf1qBhYRortPnwgvx6ae7lOJyfAJLKXDHTY8R0y2CWm4mrAGKb/9/c+OVzK4Csw01oKJ4DFGiIi9AwNbaP0/U6b0ppGmLBB6YoL9RFDRzqsENWS/aiG9CGMRskw2GUsU= 27 | file: $FRAMEWORK_NAME.framework.zip 28 | skip_cleanup: true 29 | on: 30 | repo: hulab/ClusterKit 31 | tags: true 32 | -------------------------------------------------------------------------------- /Examples/Example-data/ExampleData.h: -------------------------------------------------------------------------------- 1 | // ExampleData.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 | //! Project version number for Example-data. 26 | FOUNDATION_EXPORT double Example_dataVersionNumber; 27 | 28 | //! Project version string for Example-data. 29 | FOUNDATION_EXPORT const unsigned char Example_dataVersionString[]; 30 | 31 | #import 32 | 33 | 34 | -------------------------------------------------------------------------------- /Examples/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - ClusterKit (0.5.0): 3 | - ClusterKit/Core (= 0.5.0) 4 | - ClusterKit/Core (0.5.0) 5 | - ClusterKit/Mapbox (0.5.0): 6 | - ClusterKit/Core 7 | - Mapbox-iOS-SDK (~> 5.0) 8 | - GeoJSONSerialization (0.0.4) 9 | - GoogleMaps (3.1.0): 10 | - GoogleMaps/Maps (= 3.1.0) 11 | - GoogleMaps/Base (3.1.0) 12 | - GoogleMaps/Maps (3.1.0): 13 | - GoogleMaps/Base 14 | - Mapbox-iOS-SDK (5.9.0): 15 | - MapboxMobileEvents (= 0.10.2) 16 | - MapboxMobileEvents (0.10.2) 17 | - YandexMapKit (3.3.1): 18 | - YandexRuntime (~> 3.3) 19 | - YandexRuntime (3.3.1) 20 | 21 | DEPENDENCIES: 22 | - ClusterKit (from `../.`) 23 | - ClusterKit/Mapbox (from `../.`) 24 | - GeoJSONSerialization 25 | - GoogleMaps 26 | - YandexMapKit 27 | 28 | SPEC REPOS: 29 | https://github.com/CocoaPods/Specs.git: 30 | - GeoJSONSerialization 31 | - GoogleMaps 32 | - Mapbox-iOS-SDK 33 | - MapboxMobileEvents 34 | - YandexMapKit 35 | - YandexRuntime 36 | 37 | EXTERNAL SOURCES: 38 | ClusterKit: 39 | :path: "../." 40 | 41 | SPEC CHECKSUMS: 42 | ClusterKit: 8a8abf1c4fb506a6ece9b211a9bfc249ff4a29ec 43 | GeoJSONSerialization: 55a3d24fe9af26508e3af76873114b39b13ba479 44 | GoogleMaps: 5c13302e6fe6bb6e686b267196586b91cd594225 45 | Mapbox-iOS-SDK: a5915700ec84bc1a7f8b3e746d474789e35b7956 46 | MapboxMobileEvents: 2bc0ca2eedb627b73cf403258dce2b2fa98074a6 47 | YandexMapKit: d91d278f55fa5cced0a946b9ba33431fb5a9665d 48 | YandexRuntime: d7aae40396f41c9b23b17fee1fe6164b886e3679 49 | 50 | PODFILE CHECKSUM: 4e905150e743aaf8c850979e34e1433bebd7a815 51 | 52 | COCOAPODS: 1.9.3 53 | -------------------------------------------------------------------------------- /Examples/Example-objc/Mapbox/CKMapboxViewController.h: -------------------------------------------------------------------------------- 1 | // CKMapboxViewController.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 | @interface CKMapboxViewController : UIViewController 26 | 27 | @end 28 | 29 | @interface MBXAnnotationView : MGLAnnotationView 30 | 31 | @property (nonatomic, strong) UIImageView *imageView; 32 | 33 | @end 34 | 35 | @interface MBXClusterView : MGLAnnotationView 36 | 37 | @property (nonatomic, strong) UIImageView *imageView; 38 | 39 | @end 40 | -------------------------------------------------------------------------------- /Examples/Example-objc/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | NSAppTransportSecurity 24 | 25 | NSExceptionDomains 26 | 27 | opendata.paris.fr 28 | 29 | NSIncludesSubdomains 30 | 31 | NSTemporaryExceptionAllowsInsecureHTTPLoads 32 | 33 | NSTemporaryExceptionMinimumTLSVersion 34 | TLSv1.1 35 | 36 | 37 | 38 | UILaunchStoryboardName 39 | LaunchScreen 40 | UIMainStoryboardFile 41 | Main 42 | UIRequiredDeviceCapabilities 43 | 44 | armv7 45 | 46 | UIRequiresFullScreen 47 | 48 | UISupportedInterfaceOrientations 49 | 50 | UIInterfaceOrientationPortrait 51 | UIInterfaceOrientationLandscapeLeft 52 | UIInterfaceOrientationLandscapeRight 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /Sources/ClusterKit/include/ClusterKit/ClusterKit.h: -------------------------------------------------------------------------------- 1 | // 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 | 25 | //! Project version number for ClusterKit. 26 | FOUNDATION_EXPORT double ClusterKitVersionNumber; 27 | 28 | //! Project version string for ClusterKit. 29 | FOUNDATION_EXPORT const unsigned char ClusterKitVersionString[]; 30 | 31 | #import 32 | #import 33 | #import 34 | #import 35 | #import 36 | #import 37 | #import 38 | 39 | 40 | -------------------------------------------------------------------------------- /Examples/Example-objc/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /Examples/Example-swift/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /Sources/Mapbox/MGLMapView+ClusterKit.h: -------------------------------------------------------------------------------- 1 | // MGLMapView+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 | MK_EXTERN MGLCoordinateBounds MGLCoordinateIncludingCoordinate(MGLCoordinateBounds bounds, CLLocationCoordinate2D coordinate); 30 | 31 | /** 32 | Convinient extension to make all MGLShape complying to the `MKAnnotation` and make the usable in ClusterKit 33 | */ 34 | @interface MGLShape (ClusterKit) 35 | 36 | @end 37 | 38 | @interface CKCluster (Mapbox) 39 | 40 | @end 41 | 42 | @interface MGLMapView (ClusterKit) 43 | 44 | - (MGLMapCamera *)cameraThatFitsCluster:(CKCluster *)cluster; 45 | 46 | - (MGLMapCamera *)cameraThatFitsCluster:(CKCluster *)cluster edgePadding:(UIEdgeInsets)insets; 47 | 48 | @end 49 | 50 | NS_ASSUME_NONNULL_END 51 | -------------------------------------------------------------------------------- /Sources/YandexMapKit/YMKMapView+ClusterKit.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import 4 | 5 | NS_ASSUME_NONNULL_BEGIN 6 | 7 | /** 8 | The YMKMapViewDataSource protocol is adopted by an object that mediates the YMKMap’™s data. The data source provides the placemarks that represent clusters on map. 9 | */ 10 | @protocol YMKMapViewDataSource 11 | 12 | @optional 13 | /** 14 | Asks the data source for a marker that represent the given cluster. 15 | 16 | @param mapView A map view object requesting the marker. 17 | @param cluster The cluster to represent. 18 | 19 | @return An object inheriting from GMSMarker that the map view can use for the specified cluster. 20 | */ 21 | - (__kindof YMKPlacemarkMapObject *)mapView:(YMKMapView *)mapView placemarkForCluster:(CKCluster *)cluster; 22 | 23 | @end 24 | 25 | /** 26 | YMKPlacemarkMapObject category adopting the CKAnnotation protocol. 27 | */ 28 | @interface YMKPlacemarkMapObject (ClusterKit) 29 | 30 | /** 31 | The cluster that the marker is related to. 32 | */ 33 | @property (nonatomic, weak, nullable) CKCluster *cluster; 34 | 35 | @end 36 | 37 | @interface YMKMapView (ClusterKit) 38 | 39 | /** 40 | Data source instance that adopt the YMKMapViewDataSource. 41 | */ 42 | @property(nonatomic, weak) IBOutlet id dataSource; 43 | 44 | /** 45 | Returns the placemark representing the given cluster. 46 | 47 | @param cluster The cluster for which to return the corresponding placemark. 48 | 49 | @return The value associated with cluster, or nil if no value is associated with cluster. 50 | */ 51 | - (nullable __kindof YMKPlacemarkMapObject *)placemarkForCluster:(CKCluster *)cluster; 52 | 53 | - (YMKCameraPosition *)cameraPositionThatFits:(CKCluster *)cluster; 54 | 55 | - (YMKCameraPosition *)cameraPositionThatFits:(CKCluster *)cluster edgePadding:(UIEdgeInsets)insets; 56 | 57 | @end 58 | 59 | @interface YMKPoint (ClusterKit) 60 | 61 | @property (nonatomic, readonly) CLLocationCoordinate2D coordinate; 62 | 63 | @end 64 | 65 | NS_ASSUME_NONNULL_END 66 | -------------------------------------------------------------------------------- /Sources/ClusterKit/include/ClusterKit/CKGridBasedAlgorithm.h: -------------------------------------------------------------------------------- 1 | // CKGridBasedAlgorithm.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 | /** 26 | A simple grid-based clustering algorithm with O(n) performance. 27 | 28 | The great advantage of grid-based clustering is its significant reduction of the computational complexity, 29 | especially for clustering very large data sets. The grid-based clustering approach differs from the conventional 30 | clustering algorithms in that it is concerned not with the data points but with the value space that surrounds 31 | the data points. 32 | 33 | This grid-based implementation consists of the following the steps: 34 | 35 | 1. Iterate througth the annotations found in the given rect. 36 | 2. Associate each annotation to a grid cell. The rect is partitioned in a finite number of cells using the cell 37 | size property at the given zoom level. 38 | 3. Annotation are added to a centroid cluster {@see CKCentroidCluster} by default. 39 | */ 40 | @interface CKGridBasedAlgorithm : CKClusterAlgorithm 41 | 42 | /** 43 | The grid cell size. 44 | */ 45 | @property (nonatomic) CGFloat cellSize; 46 | 47 | @end 48 | -------------------------------------------------------------------------------- /Sources/ClusterKit/include/ClusterKit/CKNonHierarchicalDistanceBasedAlgorithm.h: -------------------------------------------------------------------------------- 1 | // CKNonHierarchicalDistanceBasedAlgorithm.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 | /** 26 | A simple clustering algorithm with O(nlog n) performance. 27 | 28 | Non-hierarchical distance analysis aims to find a grouping of annotations which minimises the distance 29 | between an annotation and its cluster. These algorithm will iteratively assign annotations to different 30 | groups while searching for the optimal distance. 31 | 32 | 1. Iterate througth the annotations that are not yet clusterized found in the given rect. 33 | 2. Create a cluster with the center of the annotation. 34 | 3. Add all items that are within a certain distance to the cluster. 35 | 4. Move any items out of an existing cluster if they are closer to another cluster. 36 | 37 | CKNonHierarchicalDistanceBasedAlgorithm is an objective-c implementation of the non-hierarchical distance 38 | based clustering algorithm used by Google maps. 39 | @see https://github.com/googlemaps/android-maps-utils/blob/master/library/src/com/google/maps/android/clustering/algo/NonHierarchicalDistanceBasedAlgorithm.java 40 | */ 41 | @interface CKNonHierarchicalDistanceBasedAlgorithm : CKClusterAlgorithm 42 | 43 | /** 44 | Cell size around a point. 45 | */ 46 | @property (nonatomic) CGFloat cellSize; 47 | 48 | @end 49 | -------------------------------------------------------------------------------- /Sources/ClusterKit/Algorithm/CKClusterAlgorithm.m: -------------------------------------------------------------------------------- 1 | // CKClusterAlgorithm.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 | @implementation CKClusterAlgorithm { 26 | Class _clusterClass; 27 | } 28 | 29 | - (instancetype)init { 30 | self = [super init]; 31 | if (self) { 32 | _clusterClass = [CKCluster class]; 33 | } 34 | return self; 35 | } 36 | 37 | - (NSArray *)clustersInRect:(MKMapRect)rect zoom:(double)zoom tree:(id)tree { 38 | NSArray *annotations = [tree annotationsInRect:rect]; 39 | NSMutableArray *clusters = [NSMutableArray arrayWithCapacity:annotations.count]; 40 | 41 | for (id annotation in annotations) { 42 | CKCluster *cluster = [self clusterWithCoordinate:annotation.coordinate]; 43 | [cluster addAnnotation:annotation]; 44 | [clusters addObject:cluster]; 45 | } 46 | return clusters; 47 | } 48 | 49 | @end 50 | 51 | @implementation CKClusterAlgorithm (CKCluster) 52 | 53 | - (void)registerClusterClass:(Class)clusterClass { 54 | NSAssert([clusterClass conformsToProtocol:@protocol(CKCluster)], @"Can only register class conforming to CKCluster."); 55 | _clusterClass = clusterClass; 56 | } 57 | 58 | - (CKCluster *)clusterWithCoordinate:(CLLocationCoordinate2D)coordinate { 59 | return [_clusterClass clusterWithCoordinate:coordinate]; 60 | } 61 | 62 | @end 63 | -------------------------------------------------------------------------------- /Sources/ClusterKit/include/ClusterKit/MKMapView+ClusterKit.h: -------------------------------------------------------------------------------- 1 | // MKMapView+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 | MKMapView category adopting the CKMap protocol. 31 | */ 32 | @interface MKMapView (ClusterKit) 33 | 34 | #if __has_include() 35 | 36 | /** 37 | Shows the specified cluster centered on screen at the greatest possible zoom level. 38 | 39 | @param cluster The cluster to show. 40 | @param animated Specify YES if you want the map view to animate the transition to the cluster rectangle or NO if you want the map to center on the specified cluster immediately. 41 | */ 42 | - (void)showCluster:(CKCluster *)cluster animated:(BOOL)animated; 43 | 44 | /** 45 | Shows the specified cluster centered on screen at the greatest possible zoom level with the given edge padding. 46 | 47 | @param cluster The cluster to show. 48 | @param insets The amount of additional space (measured in screen points) to make visible around the specified rectangle. 49 | @param animated Specify YES if you want the map view to animate the transition to the cluster rectangle or NO if you want the map to center on the specified cluster immediately. 50 | */ 51 | - (void)showCluster:(CKCluster *)cluster edgePadding:(UIEdgeInsets)insets animated:(BOOL)animated; 52 | 53 | #endif 54 | @end 55 | 56 | NS_ASSUME_NONNULL_END 57 | -------------------------------------------------------------------------------- /Sources/ClusterKit/include/ClusterKit/CKClusterAlgorithm.h: -------------------------------------------------------------------------------- 1 | // CKClusterAlgorithm.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 | CKClusterAlgorithm represents a cluster algorithm parent class. 31 | */ 32 | @interface CKClusterAlgorithm : NSObject 33 | 34 | /** 35 | Returns an array of clusters for the given map rect at a certain zoom. 36 | 37 | @param rect The map rect in which the clusters will be computed. 38 | @param zoom The zoom value at which the clusters will be computed. 39 | @param tree The tree where containing the annotations. 40 | 41 | @return The list of cluster. 42 | */ 43 | - (NSArray *)clustersInRect:(MKMapRect)rect zoom:(double)zoom tree:(id)tree; 44 | 45 | @end 46 | 47 | /** 48 | CKClusterAlgorithm for CKCluster class registration. 49 | The algorithm will use the registrated class to instantiate a cluster. 50 | */ 51 | @interface CKClusterAlgorithm (CKCluster) 52 | 53 | /** 54 | Registers a CKCluster class initializer. 55 | 56 | @param clusterClass The CKCluster class initializer. 57 | */ 58 | - (void)registerClusterClass:(Class)clusterClass; 59 | 60 | /** 61 | Instantiates a cluster using the registered class. 62 | 63 | @param coordinate The cluster coordinate. 64 | @return The newly-initialized cluster. 65 | */ 66 | - (__kindof CKCluster *)clusterWithCoordinate:(CLLocationCoordinate2D)coordinate; 67 | 68 | @end 69 | 70 | NS_ASSUME_NONNULL_END 71 | -------------------------------------------------------------------------------- /ClusterKit.podspec: -------------------------------------------------------------------------------- 1 | 2 | Pod::Spec.new do |s| 3 | s.name = "ClusterKit" 4 | s.version = "0.5.0" 5 | s.summary = "ClusterKit is a map clustering framework targeting MapKit, Google Maps, Mapbox and YandexMapKit." 6 | 7 | s.description = <<-DESC 8 | ClusterKit is an efficient clustering framework with the following features: 9 | - Native supports of MapKit, GoogleMaps, Mapbox and YandexMapKit. 10 | - Comes with 2 clustering algorithms, a Grid Based Algorithm and a Non Hierarchical Distance Based Algorithm. Other algorithms can easily be integrated. 11 | - Annotations are stored in a QuadTree for efficient region queries. 12 | - Cluster center can be switched to Centroid, Nearest Centroid, Bottom. 13 | - Handles pin selection as well as drag and dropping. 14 | - Written in Objective-C with full Swift interop support. 15 | DESC 16 | 17 | s.homepage = "https://github.com/hulab/ClusterKit" 18 | # s.screenshots = "www.example.com/screenshots_1", "www.example.com/screenshots_2" 19 | s.license = 'MIT' 20 | s.author = { "Hulab" => "info@mapstr.com" } 21 | s.source = { :git => "https://github.com/hulab/ClusterKit.git", :tag => s.version.to_s } 22 | s.social_media_url = 'https://twitter.com/mapstr_app' 23 | 24 | s.platform = :ios, '7.0' 25 | s.requires_arc = true 26 | s.default_subspecs = 'Core' 27 | 28 | s.subspec 'Core' do |ss| 29 | ss.frameworks = 'MapKit' 30 | ss.source_files = 'Sources/ClusterKit/**/*.{h,m}' 31 | 32 | ss.test_spec do |test_spec| 33 | test_spec.source_files = 'Tests/ClusterKitTests/*.{h,m}' 34 | end 35 | end 36 | 37 | # Like GoogleMaps sdk (googlemaps/google-maps-ios-utils#23) YandexMapKit is statically built, 38 | # which mean we can't use them as subspec dependency yet. Better to keep both 39 | # GoogleMaps and YandexMapKit commented until both of them are dynamically built! 40 | 41 | # s.subspec 'GoogleMaps' do |ss| 42 | # ss.platform = :ios, '8.0' 43 | # ss.dependency 'ClusterKit/Core' 44 | # ss.dependency 'GoogleMaps', '~> 2.7' 45 | # ss.source_files = 'Sources/GoogleMaps' 46 | # end 47 | 48 | # s.subspec 'YandexMapKit' do |ss| 49 | # ss.platform = :ios, '9.0' 50 | # ss.dependency 'ClusterKit/Core' 51 | # ss.dependency 'YandexMapKit', '~> 3.2' 52 | # ss.source_files = 'Sources/YandexMapKit' 53 | # end 54 | 55 | s.subspec 'Mapbox' do |ss| 56 | ss.platform = :ios, '9.0' 57 | ss.dependency 'ClusterKit/Core' 58 | ss.dependency 'Mapbox-iOS-SDK', '~> 5.0' 59 | ss.source_files = 'Sources/Mapbox' 60 | end 61 | 62 | end 63 | -------------------------------------------------------------------------------- /Sources/ClusterKit/Algorithm/CKGridBasedAlgorithm.m: -------------------------------------------------------------------------------- 1 | // CKGridBasedAlgorithm.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 | @implementation CKGridBasedAlgorithm 26 | 27 | - (instancetype)init { 28 | self = [super init]; 29 | if (self) { 30 | self.cellSize = 100; 31 | [self registerClusterClass:[CKCentroidCluster class]]; 32 | } 33 | return self; 34 | } 35 | 36 | - (NSArray *)clustersInRect:(MKMapRect)rect zoom:(double)zoom tree:(id)tree { 37 | NSMutableDictionary *clusters = [[NSMutableDictionary alloc] init]; 38 | 39 | @synchronized(tree) { 40 | NSArray *annotations = [tree annotationsInRect:rect]; 41 | 42 | // Divide the whole map into a numCells x numCells grid and assign annotations to them. 43 | long numCells = (long)ceil(256 * pow(2, zoom) / self.cellSize); 44 | 45 | for (id annotation in annotations) { 46 | 47 | MKMapPoint point = MKMapPointForCoordinate(annotation.coordinate); 48 | NSUInteger col = numCells * point.x / MKMapSizeWorld.width; 49 | NSUInteger row = numCells * point.y / MKMapSizeWorld.height; 50 | 51 | NSNumber *key = @(numCells * row + col); 52 | CKCluster *cluster = clusters[key]; 53 | if (!cluster) { 54 | cluster = [self clusterWithCoordinate:annotation.coordinate]; 55 | clusters[key] = cluster; 56 | } 57 | [cluster addAnnotation:annotation]; 58 | } 59 | } 60 | 61 | return clusters.allValues; 62 | } 63 | 64 | @end 65 | -------------------------------------------------------------------------------- /Sources/ClusterKit/include/ClusterKit/CKAnnotationTree.h: -------------------------------------------------------------------------------- 1 | // CKAnnotationTree.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 | @protocol CKAnnotationTree; 30 | 31 | /** 32 | The delegate of a CKAnnotationTree object may adopt the KPAnnotationTreeDelegate protocol. 33 | Optional method of the protocol allow the delegate to manage annotation extraction. 34 | */ 35 | @protocol CKAnnotationTreeDelegate 36 | 37 | @optional 38 | 39 | /** 40 | Asks the delegate if the annotation tree should extract the given annotation. 41 | 42 | @param annotationTree The annotation tree object requesting this information. 43 | @param annotation The annotation to extract. 44 | 45 | @return Yes to permit the extraction of the given annotation. 46 | */ 47 | - (BOOL)annotationTree:(id)annotationTree shouldExtractAnnotation:(id)annotation; 48 | 49 | @end 50 | 51 | /** 52 | The annotation tree protocol. 53 | */ 54 | @protocol CKAnnotationTree 55 | 56 | @property (nonatomic, weak) id delegate; 57 | 58 | /** 59 | The tree's annotation set. 60 | */ 61 | @property (nonatomic, readonly) NSArray> *annotations; 62 | 63 | /** 64 | Initializes a KPAnnotationTree object. 65 | 66 | @param annotations An annotations array. 67 | 68 | @return The initialized KPAnnotationTree object. 69 | */ 70 | - (instancetype)initWithAnnotations:(NSArray> *)annotations; 71 | 72 | /** 73 | Extracts annotations from a rect. 74 | 75 | @param rect The map rect. 76 | 77 | @return The annotation array. 78 | */ 79 | - (NSArray> *)annotationsInRect:(MKMapRect)rect; 80 | 81 | @end 82 | 83 | NS_ASSUME_NONNULL_END 84 | -------------------------------------------------------------------------------- /Examples/Example-data/CKGeoPointOperation.h: -------------------------------------------------------------------------------- 1 | // CKGeoPointOperation.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 | Operation to retrieve geopoints from the embed geojson file. 31 | */ 32 | @interface CKGeoPointOperation : NSOperation 33 | 34 | /** 35 | The callback dispatch queue on success. If `NULL` (default), the main queue is used. 36 | 37 | The queue is retained while this operation is living 38 | */ 39 | @property (nonatomic, assign) dispatch_queue_t successCallbackQueue; 40 | 41 | /** 42 | The callback dispatch queue on failure. If `NULL` (default), the main queue is used. 43 | 44 | The queue is retained while this operation is living 45 | */ 46 | @property (nonatomic, assign) dispatch_queue_t failureCallbackQueue; 47 | 48 | /** 49 | The geopoints found in the geojson file. 50 | */ 51 | @property (nonatomic, nullable, readonly) NSArray *points; 52 | 53 | /** 54 | The error, if any, that occurred during execution of the operation. 55 | */ 56 | @property (nonatomic, nullable, readonly) NSError *error; 57 | 58 | /** 59 | Sets the `completionBlock` property with a block that executes either the specified success or failure block, depending on the state of the operation. 60 | 61 | @param success The block to be executed on the completion of a successful operation. This block has no return value and takes two arguments: the receiver operation and the resulted geopoints. 62 | @param failure The block to be executed on the completion of an unsuccessful operation. This block has no return value and takes two arguments: the receiver operation and the error that occurred during the execution of the operation. 63 | */ 64 | - (void)setCompletionBlockWithSuccess:(void (^)(CKGeoPointOperation *operation, NSArray *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 | CI Status 8 | 9 | 10 | Version 11 | 12 | 13 | License 14 | 15 | 16 | Platform 17 | 18 | 19 | Swift Package Manager 20 | 21 | 22 | Carthage 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 | |![MapKit](Resources/mapkit.gif)|![GoogleMaps](Resources/googlemaps.gif)|![Mapbox](Resources/mapbox.gif)| 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 | --------------------------------------------------------------------------------