├── Images ├── demo.gif ├── demo.png ├── logo.png └── logo.sketch ├── docs ├── img │ ├── gh.png │ ├── carat.png │ └── dash.png ├── docsets │ ├── Cluster.tgz │ └── Cluster.docset │ │ └── Contents │ │ ├── Resources │ │ ├── docSet.dsidx │ │ └── Documents │ │ │ ├── img │ │ │ ├── gh.png │ │ │ ├── carat.png │ │ │ └── dash.png │ │ │ ├── badge.svg │ │ │ ├── js │ │ │ └── jazzy.js │ │ │ ├── css │ │ │ └── highlight.css │ │ │ ├── Extensions.html │ │ │ ├── Enums.html │ │ │ ├── Protocols.html │ │ │ ├── Functions.html │ │ │ ├── Extensions │ │ │ └── CLLocationCoordinate2D.html │ │ │ ├── undocumented.json │ │ │ └── Classes │ │ │ └── Annotation.html │ │ └── Info.plist ├── badge.svg ├── js │ └── jazzy.js ├── css │ ├── highlight.css │ └── jazzy.css ├── Extensions.html ├── Enums.html ├── Protocols.html ├── Functions.html ├── Extensions │ └── CLLocationCoordinate2D.html ├── undocumented.json └── Classes │ ├── Annotation.html │ └── ClusterAnnotation.html ├── Example ├── Assets.xcassets │ ├── Contents.json │ ├── me.imageset │ │ ├── me.pdf │ │ └── Contents.json │ ├── pin.imageset │ │ ├── pin.pdf │ │ └── Contents.json │ ├── pin2.imageset │ │ ├── pin2.pdf │ │ └── Contents.json │ └── AppIcon.appiconset │ │ └── Contents.json ├── AppDelegate.swift ├── AnnotationView.swift ├── Info.plist ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Extensions.swift ├── DispatchQueue+Once.swift └── ViewController.swift ├── Cluster.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ ├── xcbaselines │ └── F4AF970320F5EF1C008DB4E5.xcbaseline │ │ ├── 103EDAB3-2096-425C-9D63-A5E97089DBA2.plist │ │ └── Info.plist │ └── xcschemes │ ├── Cluster.xcscheme │ └── Example.xcscheme ├── Sources ├── AnnotationContainer.swift ├── Cluster.h ├── Atomic.swift ├── Info.plist ├── Extensions.swift ├── QuadTree.swift ├── Annotation.swift └── AnnotationGrouper.swift ├── .travis.yml ├── Cluster.podspec ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── ISSUE_TEMPLATE.md ├── CODE_OF_CONDUCT.md └── CONTRIBUTING.md ├── Tests └── Info.plist ├── LICENSE ├── Package.swift ├── .gitignore └── CHANGELOG.md /Images/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ml-archive/Cluster/master/Images/demo.gif -------------------------------------------------------------------------------- /Images/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ml-archive/Cluster/master/Images/demo.png -------------------------------------------------------------------------------- /Images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ml-archive/Cluster/master/Images/logo.png -------------------------------------------------------------------------------- /docs/img/gh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ml-archive/Cluster/master/docs/img/gh.png -------------------------------------------------------------------------------- /Images/logo.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ml-archive/Cluster/master/Images/logo.sketch -------------------------------------------------------------------------------- /docs/img/carat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ml-archive/Cluster/master/docs/img/carat.png -------------------------------------------------------------------------------- /docs/img/dash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ml-archive/Cluster/master/docs/img/dash.png -------------------------------------------------------------------------------- /docs/docsets/Cluster.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ml-archive/Cluster/master/docs/docsets/Cluster.tgz -------------------------------------------------------------------------------- /Example/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/me.imageset/me.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ml-archive/Cluster/master/Example/Assets.xcassets/me.imageset/me.pdf -------------------------------------------------------------------------------- /Example/Assets.xcassets/pin.imageset/pin.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ml-archive/Cluster/master/Example/Assets.xcassets/pin.imageset/pin.pdf -------------------------------------------------------------------------------- /Example/Assets.xcassets/pin2.imageset/pin2.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ml-archive/Cluster/master/Example/Assets.xcassets/pin2.imageset/pin2.pdf -------------------------------------------------------------------------------- /docs/docsets/Cluster.docset/Contents/Resources/docSet.dsidx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ml-archive/Cluster/master/docs/docsets/Cluster.docset/Contents/Resources/docSet.dsidx -------------------------------------------------------------------------------- /docs/docsets/Cluster.docset/Contents/Resources/Documents/img/gh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ml-archive/Cluster/master/docs/docsets/Cluster.docset/Contents/Resources/Documents/img/gh.png -------------------------------------------------------------------------------- /docs/docsets/Cluster.docset/Contents/Resources/Documents/img/carat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ml-archive/Cluster/master/docs/docsets/Cluster.docset/Contents/Resources/Documents/img/carat.png -------------------------------------------------------------------------------- /docs/docsets/Cluster.docset/Contents/Resources/Documents/img/dash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ml-archive/Cluster/master/docs/docsets/Cluster.docset/Contents/Resources/Documents/img/dash.png -------------------------------------------------------------------------------- /Cluster.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/Assets.xcassets/me.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "me.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/pin2.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "pin2.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/pin.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "pin.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } -------------------------------------------------------------------------------- /Cluster.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Sources/AnnotationContainer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnnotationContainer.swift 3 | // Cluster 4 | // 5 | // Created by Nick Trienens on 2/14/20. 6 | // Copyright © 2020 efremidze. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import MapKit 11 | 12 | protocol AnnotationsContainer { 13 | func add(_ annotation: MKAnnotation) -> Bool 14 | func remove(_ annotation: MKAnnotation) -> Bool 15 | func annotations(in rect: MKMapRect) -> [MKAnnotation] 16 | } 17 | -------------------------------------------------------------------------------- /Sources/Cluster.h: -------------------------------------------------------------------------------- 1 | // 2 | // Cluster.h 3 | // Cluster 4 | // 5 | // Created by Lasha Efremidze on 4/13/17. 6 | // Copyright © 2017 efremidze. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for Cluster. 12 | FOUNDATION_EXPORT double ClusterVersionNumber; 13 | 14 | //! Project version string for Cluster. 15 | FOUNDATION_EXPORT const unsigned char ClusterVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /Example/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Example 4 | // 5 | // Created by Lasha Efremidze on 4/13/17. 6 | // Copyright © 2017 efremidze. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | // Override point for customization after application launch. 18 | return true 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /Sources/Atomic.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Atomic.swift 3 | // Cluster 4 | // 5 | // Created by Lasha Efremidze on 10/10/19. 6 | // Copyright © 2019 efremidze. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | @propertyWrapper 12 | class Atomic { 13 | private let queue = DispatchQueue(label: "Atomic serial queue", attributes: .concurrent) 14 | 15 | private var _value: Value 16 | 17 | init(wrappedValue value: Value) { 18 | self._value = value 19 | } 20 | 21 | var wrappedValue: Value { 22 | get { return queue.sync { _value } } 23 | set { queue.async(flags: .barrier) { self._value = newValue } } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /docs/docsets/Cluster.docset/Contents/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleIdentifier 6 | com.jazzy.cluster 7 | CFBundleName 8 | Cluster 9 | DocSetPlatformFamily 10 | cluster 11 | isDashDocset 12 | 13 | dashIndexFilePath 14 | index.html 15 | isJavaScriptEnabled 16 | 17 | DashDocSetFamily 18 | dashtoc 19 | 20 | 21 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: osx 2 | osx_image: xcode11.2 3 | env: 4 | global: 5 | - PROJECT=Cluster.xcodeproj 6 | - SCHEME="Cluster" 7 | - EXAMPLE_SCHEME="Example" 8 | matrix: 9 | - DESTINATION="OS=13.2,name=iPhone 11 Pro" SCHEME="$SCHEME" 10 | - DESTINATION="OS=11.4,name=iPhone X" SCHEME="$SCHEME" 11 | - DESTINATION="OS=10.3.1,name=iPhone 7 Plus" SCHEME="$SCHEME" 12 | script: 13 | - set -o pipefail 14 | 15 | - xcodebuild -project "$PROJECT" -scheme "$SCHEME" -destination "$DESTINATION" -configuration Release ONLY_ACTIVE_ARCH=NO ENABLE_TESTABILITY=YES test | xcpretty; 16 | - xcodebuild -project "$PROJECT" -scheme "$EXAMPLE_SCHEME" -destination "$DESTINATION" -configuration Debug ONLY_ACTIVE_ARCH=NO build | xcpretty; 17 | -------------------------------------------------------------------------------- /Cluster.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'Cluster' 3 | s.version = '3.0.3' 4 | s.summary = 'Map Clustering Library' 5 | s.homepage = 'https://github.com/efremidze/Cluster' 6 | s.license = { :type => 'MIT', :file => 'LICENSE' } 7 | s.author = { 'efremidze' => 'efremidzel@hotmail.com' } 8 | s.documentation_url = 'https://efremidze.github.io/Cluster/' 9 | s.source = { :git => 'https://github.com/efremidze/Cluster.git', :tag => s.version.to_s } 10 | s.ios.deployment_target = '8.0' 11 | s.swift_version = '5.0' 12 | s.source_files = 'Sources/*.swift' 13 | s.test_spec 'Tests' do |test_spec| 14 | test_spec.source_files = 'Tests/*.swift' 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /Cluster.xcodeproj/xcshareddata/xcbaselines/F4AF970320F5EF1C008DB4E5.xcbaseline/103EDAB3-2096-425C-9D63-A5E97089DBA2.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | classNames 6 | 7 | Tests 8 | 9 | testMultipleOperations() 10 | 11 | com.apple.XCTPerformanceMetric_WallClockTime 12 | 13 | baselineAverage 14 | 0.0153 15 | baselineIntegrationDisplayName 16 | Local Baseline 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ### Checklist 4 | - [ ] I've tested my changes. 5 | - [ ] I've read the [Contribution Guidelines](CONTRIBUTING.md). 6 | - [ ] I've updated the documentation if necessary. 7 | 8 | ### Motivation and Context 9 | 10 | 11 | 12 | 13 | 14 | ### Description 15 | 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Description** 11 | Describe your issue in detail. 12 | 13 | **Screenshots** 14 | If applicable, add screenshots to help explain your problem. 15 | 16 | **Smartphone** 17 | - Device: [e.g. iPhone6] 18 | - OS: [e.g. iOS8.1] 19 | - Version: [e.g. 22] 20 | 21 | **Checklist** 22 | - [ ] I updated Cluster to the latest version. 23 | - [ ] I read the [Contribution Guidelines](https://github.com/efremidze/Cluster/blob/master/.github/CONTRIBUTING.md). 24 | - [ ] I read the [documentation](https://github.com/efremidze/Cluster). 25 | - [ ] I searched for [existing GitHub issues](https://github.com/efremidze/Cluster/issues). 26 | -------------------------------------------------------------------------------- /Tests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 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 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ### New Issue Checklist 4 | 5 | 6 | - [ ] I updated Cluster to the latest version. 7 | - [ ] I read the [Contribution Guidelines](https://github.com/efremidze/Cluster/blob/master/.github/CONTRIBUTING.md). 8 | - [ ] I read the [documentation](https://github.com/efremidze/Cluster). 9 | - [ ] I searched for [existing GitHub issues](https://github.com/efremidze/Cluster/issues). 10 | 11 | ### Issue Description 12 | 13 | 14 | 15 | ### Environment 16 | 17 | - **iOS Version**: [INSERT iOS VERSION HERE] 18 | - **Device(s)**: [INSERT DEVICE(S) HERE] 19 | -------------------------------------------------------------------------------- /Sources/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 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | 22 | **Checklist** 23 | - [ ] I updated Cluster to the latest version. 24 | - [ ] I read the [Contribution Guidelines](https://github.com/efremidze/Cluster/blob/master/.github/CONTRIBUTING.md). 25 | - [ ] I read the [documentation](https://github.com/efremidze/Cluster). 26 | - [ ] I searched for [existing GitHub issues](https://github.com/efremidze/Cluster/issues). 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Lasha Efremidze 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/badge.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | documentation 17 | 18 | 19 | documentation 20 | 21 | 22 | 56% 23 | 24 | 25 | 56% 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /docs/docsets/Cluster.docset/Contents/Resources/Documents/badge.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | documentation 17 | 18 | 19 | documentation 20 | 21 | 22 | 56% 23 | 24 | 25 | 56% 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.0 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Cluster", 8 | platforms: [ 9 | .iOS(.v8) 10 | ], 11 | products: [ 12 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 13 | .library( 14 | name: "Cluster", 15 | targets: ["Cluster"]), 16 | ], 17 | dependencies: [ 18 | // Dependencies declare other packages that this package depends on. 19 | // .package(url: /* package url */, from: "1.0.0"), 20 | ], 21 | targets: [ 22 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 23 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 24 | .target( 25 | name: "Cluster", 26 | dependencies: [], 27 | path: "Sources" 28 | ), 29 | .testTarget( 30 | name: "ClusterTests", 31 | dependencies: ["Cluster"], 32 | path: "Tests" 33 | ) 34 | ] 35 | ) 36 | -------------------------------------------------------------------------------- /Cluster.xcodeproj/xcshareddata/xcbaselines/F4AF970320F5EF1C008DB4E5.xcbaseline/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | runDestinationsByUUID 6 | 7 | 103EDAB3-2096-425C-9D63-A5E97089DBA2 8 | 9 | localComputer 10 | 11 | busSpeedInMHz 12 | 100 13 | cpuCount 14 | 1 15 | cpuKind 16 | Intel Core i5 17 | cpuSpeedInMHz 18 | 2700 19 | logicalCPUCoresPerPackage 20 | 4 21 | modelCode 22 | MacBookPro12,1 23 | physicalCPUCoresPerPackage 24 | 2 25 | platformIdentifier 26 | com.apple.platform.macosx 27 | 28 | targetArchitecture 29 | x86_64 30 | targetDevice 31 | 32 | modelCode 33 | iPhone10,4 34 | platformIdentifier 35 | com.apple.platform.iphonesimulator 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /Example/AnnotationView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnnotationView.swift 3 | // Example 4 | // 5 | // Created by Lasha Efremidze on 10/9/18. 6 | // Copyright © 2018 efremidze. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MapKit 11 | import Cluster 12 | 13 | class CountClusterAnnotationView: ClusterAnnotationView { 14 | override func configure() { 15 | super.configure() 16 | 17 | guard let annotation = annotation as? ClusterAnnotation else { return } 18 | let count = annotation.annotations.count 19 | let diameter = radius(for: count) * 2 20 | self.frame.size = CGSize(width: diameter, height: diameter) 21 | self.layer.cornerRadius = self.frame.width / 2 22 | self.layer.masksToBounds = true 23 | self.layer.borderColor = UIColor.white.cgColor 24 | self.layer.borderWidth = 1.5 25 | } 26 | 27 | func radius(for count: Int) -> CGFloat { 28 | if count < 5 { 29 | return 12 30 | } else if count < 10 { 31 | return 16 32 | } else { 33 | return 20 34 | } 35 | } 36 | } 37 | 38 | class ImageCountClusterAnnotationView: ClusterAnnotationView { 39 | lazy var once: Void = { [unowned self] in 40 | self.countLabel.frame.size.width -= 6 41 | self.countLabel.frame.origin.x += 3 42 | self.countLabel.frame.origin.y -= 6 43 | }() 44 | override func layoutSubviews() { 45 | super.layoutSubviews() 46 | 47 | _ = once 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /docs/js/jazzy.js: -------------------------------------------------------------------------------- 1 | window.jazzy = {'docset': false} 2 | if (typeof window.dash != 'undefined') { 3 | document.documentElement.className += ' dash' 4 | window.jazzy.docset = true 5 | } 6 | if (navigator.userAgent.match(/xcode/i)) { 7 | document.documentElement.className += ' xcode' 8 | window.jazzy.docset = true 9 | } 10 | 11 | // On doc load, toggle the URL hash discussion if present 12 | $(document).ready(function() { 13 | if (!window.jazzy.docset) { 14 | var linkToHash = $('a[href="' + window.location.hash +'"]'); 15 | linkToHash.trigger("click"); 16 | } 17 | }); 18 | 19 | // On token click, toggle its discussion and animate token.marginLeft 20 | $(".token").click(function(event) { 21 | if (window.jazzy.docset) { 22 | return; 23 | } 24 | var link = $(this); 25 | var animationDuration = 300; 26 | var tokenOffset = "15px"; 27 | var original = link.css('marginLeft') == tokenOffset; 28 | link.animate({'margin-left':original ? "0px" : tokenOffset}, animationDuration); 29 | $content = link.parent().parent().next(); 30 | $content.slideToggle(animationDuration); 31 | 32 | // Keeps the document from jumping to the hash. 33 | var href = $(this).attr('href'); 34 | if (history.pushState) { 35 | history.pushState({}, '', href); 36 | } else { 37 | location.hash = href; 38 | } 39 | event.preventDefault(); 40 | }); 41 | 42 | // Dumb down quotes within code blocks that delimit strings instead of quotations 43 | // https://github.com/realm/jazzy/issues/714 44 | $("code q").replaceWith(function () { 45 | return ["\"", $(this).contents(), "\""]; 46 | }); 47 | -------------------------------------------------------------------------------- /docs/docsets/Cluster.docset/Contents/Resources/Documents/js/jazzy.js: -------------------------------------------------------------------------------- 1 | window.jazzy = {'docset': false} 2 | if (typeof window.dash != 'undefined') { 3 | document.documentElement.className += ' dash' 4 | window.jazzy.docset = true 5 | } 6 | if (navigator.userAgent.match(/xcode/i)) { 7 | document.documentElement.className += ' xcode' 8 | window.jazzy.docset = true 9 | } 10 | 11 | // On doc load, toggle the URL hash discussion if present 12 | $(document).ready(function() { 13 | if (!window.jazzy.docset) { 14 | var linkToHash = $('a[href="' + window.location.hash +'"]'); 15 | linkToHash.trigger("click"); 16 | } 17 | }); 18 | 19 | // On token click, toggle its discussion and animate token.marginLeft 20 | $(".token").click(function(event) { 21 | if (window.jazzy.docset) { 22 | return; 23 | } 24 | var link = $(this); 25 | var animationDuration = 300; 26 | var tokenOffset = "15px"; 27 | var original = link.css('marginLeft') == tokenOffset; 28 | link.animate({'margin-left':original ? "0px" : tokenOffset}, animationDuration); 29 | $content = link.parent().parent().next(); 30 | $content.slideToggle(animationDuration); 31 | 32 | // Keeps the document from jumping to the hash. 33 | var href = $(this).attr('href'); 34 | if (history.pushState) { 35 | history.pushState({}, '', href); 36 | } else { 37 | location.hash = href; 38 | } 39 | event.preventDefault(); 40 | }); 41 | 42 | // Dumb down quotes within code blocks that delimit strings instead of quotations 43 | // https://github.com/realm/jazzy/issues/714 44 | $("code q").replaceWith(function () { 45 | return ["\"", $(this).contents(), "\""]; 46 | }); 47 | -------------------------------------------------------------------------------- /Example/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 | UIStatusBarHidden 34 | 35 | UISupportedInterfaceOrientations 36 | 37 | UIInterfaceOrientationPortrait 38 | 39 | UISupportedInterfaceOrientations~ipad 40 | 41 | UIInterfaceOrientationPortrait 42 | UIInterfaceOrientationPortraitUpsideDown 43 | UIInterfaceOrientationLandscapeLeft 44 | UIInterfaceOrientationLandscapeRight 45 | 46 | UIViewControllerBasedStatusBarAppearance 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xcuserstate 23 | 24 | ## Obj-C/Swift specific 25 | *.hmap 26 | *.ipa 27 | *.dSYM.zip 28 | *.dSYM 29 | 30 | ## Playgrounds 31 | timeline.xctimeline 32 | playground.xcworkspace 33 | 34 | # Swift Package Manager 35 | # 36 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 37 | # Packages/ 38 | .build/ 39 | 40 | # CocoaPods 41 | # 42 | # We recommend against adding the Pods directory to your .gitignore. However 43 | # you should judge for yourself, the pros and cons are mentioned at: 44 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 45 | # 46 | # Pods/ 47 | 48 | # Carthage 49 | # 50 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 51 | # Carthage/Checkouts 52 | 53 | Carthage/Build 54 | 55 | # fastlane 56 | # 57 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 58 | # screenshots whenever they are needed. 59 | # For more information about the recommended setup visit: 60 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 61 | 62 | fastlane/report.xml 63 | fastlane/Preview.html 64 | fastlane/screenshots 65 | fastlane/test_output 66 | /.DS_Store 67 | -------------------------------------------------------------------------------- /Example/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 | -------------------------------------------------------------------------------- /Example/Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Extensions.swift 3 | // Cluster 4 | // 5 | // Created by Lasha Efremidze on 7/8/17. 6 | // Copyright © 2017 efremidze. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MapKit 11 | 12 | extension UIImage { 13 | 14 | func filled(with color: UIColor) -> UIImage { 15 | UIGraphicsBeginImageContextWithOptions(size, false, scale) 16 | color.setFill() 17 | guard let context = UIGraphicsGetCurrentContext() else { return self } 18 | context.translateBy(x: 0, y: size.height) 19 | context.scaleBy(x: 1.0, y: -1.0); 20 | context.setBlendMode(CGBlendMode.normal) 21 | let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height) 22 | guard let mask = self.cgImage else { return self } 23 | context.clip(to: rect, mask: mask) 24 | context.fill(rect) 25 | let newImage = UIGraphicsGetImageFromCurrentImageContext()! 26 | UIGraphicsEndImageContext() 27 | return newImage 28 | } 29 | 30 | static let pin = UIImage(named: "pin")?.filled(with: .green) 31 | static let pin2 = UIImage(named: "pin2")?.filled(with: .green) 32 | static let me = UIImage(named: "me")?.filled(with: .blue) 33 | 34 | } 35 | 36 | extension UIColor { 37 | class var green: UIColor { return UIColor(red: 76 / 255, green: 217 / 255, blue: 100 / 255, alpha: 1) } 38 | class var blue: UIColor { return UIColor(red: 0, green: 122 / 255, blue: 1, alpha: 1) } 39 | } 40 | 41 | extension MKMapView { 42 | func annotationView(of type: T.Type, annotation: MKAnnotation?, reuseIdentifier: String) -> T { 43 | guard let annotationView = dequeueReusableAnnotationView(withIdentifier: reuseIdentifier) as? T else { 44 | return type.init(annotation: annotation, reuseIdentifier: reuseIdentifier) 45 | } 46 | annotationView.annotation = annotation 47 | return annotationView 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Example/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /Example/DispatchQueue+Once.swift: -------------------------------------------------------------------------------- 1 | // DispatchQueue+Once.swift 2 | // Cluster 3 | // 4 | // Created by Nick Trienens on 2/6/20 5 | // Copyright (c) 2020 Fuzzproductions All rights reserved. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension DispatchQueue { 11 | private static var _onceTracker = [String]() 12 | 13 | class func once( 14 | file: String = #file, 15 | function: String = #function, 16 | line: Int = #line, 17 | block: () -> Void 18 | ) { 19 | let token = "\(file):\(function):\(line)" 20 | once(token: token, block: block) 21 | } 22 | 23 | /** 24 | Executes a block of code, associated with a unique token, only once. The code is thread safe and will 25 | only execute the code once even in the presence of multithreaded calls. 26 | 27 | - parameter token: A unique reverse DNS style name such as com.vectorform. or a GUID 28 | - parameter block: Block to execute once 29 | */ 30 | class func once( 31 | token: String, 32 | block: () -> Void 33 | ) { 34 | objc_sync_enter(self) 35 | defer { objc_sync_exit(self) } 36 | 37 | guard !_onceTracker.contains(token) else { return } 38 | 39 | _onceTracker.append(token) 40 | block() 41 | } 42 | 43 | /** 44 | Executes a block of code, associated with a unique token, only once. The code is thread safe and will 45 | only execute the code once even in the presence of multithreaded calls. 46 | 47 | - parameter token: A unique reverse DNS style name such as com.vectorform. or a GUID 48 | - parameter block: Block to execute once 49 | */ 50 | class func once( 51 | in interval: Double, 52 | token: String? = nil, 53 | file: String = #file, 54 | function: String = #function, 55 | line: Int = #line, 56 | block: () -> Void 57 | ) { 58 | objc_sync_enter(self) 59 | defer { objc_sync_exit(self) } 60 | 61 | let tokenToUse = token ?? "\(file):\(function):\(line)" 62 | 63 | guard !_onceTracker.contains(tokenToUse) else { return } 64 | 65 | _onceTracker.append(tokenToUse) 66 | Timer.scheduledTimer(withTimeInterval: interval, repeats: false) { _ in 67 | _onceTracker.removing(tokenToUse) 68 | } 69 | block() 70 | } 71 | } 72 | 73 | extension Array where Iterator.Element: Equatable { 74 | 75 | mutating func removing(_ item: Iterator.Element) { 76 | if let ind = firstIndex(of: item) { 77 | remove(at: ind) 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at efremidzel@hotmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to _Cluster_ 2 | 3 | The following is a set of guidelines for contributing to _Cluster_ on GitHub. 4 | 5 | > Above all, thank you for your interest in the project and for taking the time to contribute! 👍 6 | 7 | ## Asking Questions 8 | 9 | We don't use GitHub as a support forum. 10 | For any usage questions that are not specific to the project itself, 11 | please ask on [Stack Overflow](https://stackoverflow.com) instead. 12 | By doing so, you'll be more likely to quickly solve your problem, 13 | and you'll allow anyone else with the same question to find the answer. 14 | This also allows maintainers to focus on improving the project for others. 15 | 16 | ## Reporting Other Issues 17 | 18 | A great way to contribute to the project 19 | is to send a detailed issue when you encounter a problem. 20 | We always appreciate a well-written, thorough bug report. 21 | 22 | Check that the project issues database 23 | doesn't already include that problem or suggestion before submitting an issue. 24 | If you find a match, add a quick "+1" or "I have this problem too." 25 | Doing this helps prioritize the most common problems and requests. 26 | 27 | Before submitting a new GitHub issue, please make sure to 28 | 29 | - Check out the [documentation](https://github.com/efremidze/Cluster). 30 | - Read the usage guide on [the README](https://github.com/efremidze/Cluster/#usage). 31 | - Search for [existing GitHub issues](https://github.com/efremidze/Cluster/issues). 32 | 33 | If the above doesn't help, please [submit an issue](https://github.com/efremidze/Cluster/issues) on GitHub. 34 | 35 | ## I want to contribute to _Cluster_ 36 | 37 | ### Prerequisites 38 | 39 | To develop _Cluster_, you will need to use an Xcode version compatible with the Swift version specified in the [README](https://github.com/efremidze/Cluster/#requirements). 40 | 41 | ### Checking out the repository 42 | 43 | - Click the “Fork” button in the upper right corner of repo 44 | - Clone your fork: 45 | - `git clone https://github.com//Cluster.git` 46 | - Create a new branch to work on: 47 | - `git checkout -b ` 48 | - A good name for a branch describes the thing you’ll be working on, e.g. `voice-over`, `fix-font-size`, etc. 49 | 50 | That’s it! Now you’re ready to work on _Cluster_. Open the `Cluster.xcworkspace` workspace to start coding. 51 | 52 | ### Things to keep in mind 53 | 54 | - Please do not change the minimum iOS version 55 | - Always document new public methods and properties 56 | 57 | ### Testing your local changes 58 | 59 | Before opening a pull request, please make sure your changes don't break things. 60 | 61 | - The framework and example project should build without warnings 62 | - The example project should run without issues. 63 | 64 | ### Submitting the PR 65 | 66 | When the coding is done and you’ve finished testing your changes, you are ready to submit the PR to the [main repo](https://github.com/efremidze/Cluster). Some best practices are: 67 | 68 | - Use a descriptive title 69 | - Link the issues that are related to your PR in the body 70 | 71 | ## Code of Conduct 72 | 73 | Help us keep _Cluster_ open and inclusive. Please read and follow our [Code of Conduct](CODE_OF_CONDUCT.md). 74 | 75 | ## License 76 | 77 | This project is licensed under the terms of the MIT license. See the [LICENSE](LICENSE) file. 78 | 79 | _These contribution guidelines were adapted from [_fastlane_](https://github.com/fastlane/fastlane) guides._ 80 | -------------------------------------------------------------------------------- /Cluster.xcodeproj/xcshareddata/xcschemes/Cluster.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 | -------------------------------------------------------------------------------- /Cluster.xcodeproj/xcshareddata/xcschemes/Example.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 34 | 40 | 41 | 42 | 43 | 44 | 50 | 51 | 52 | 53 | 54 | 55 | 65 | 67 | 73 | 74 | 75 | 76 | 77 | 78 | 84 | 86 | 92 | 93 | 94 | 95 | 97 | 98 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /Sources/Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Extensions.swift 3 | // Cluster 4 | // 5 | // Created by Lasha Efremidze on 4/15/17. 6 | // Copyright © 2017 efremidze. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import MapKit 11 | 12 | extension MKMapRect { 13 | init(minX: Double, minY: Double, maxX: Double, maxY: Double) { 14 | self.init(x: minX, y: minY, width: abs(maxX - minX), height: abs(maxY - minY)) 15 | } 16 | init(x: Double, y: Double, width: Double, height: Double) { 17 | self.init(origin: MKMapPoint(x: x, y: y), size: MKMapSize(width: width, height: height)) 18 | } 19 | func contains(_ coordinate: CLLocationCoordinate2D) -> Bool { 20 | return self.contains(MKMapPoint(coordinate)) 21 | } 22 | 23 | public func exapnd(scale: Double) -> MKMapRect { 24 | let newWidth = size.width * scale 25 | let newHeight = size.width * scale 26 | return MKMapRect(origin: MKMapPoint(x: origin.x + (newWidth - size.width)/2, y: origin.y + (newHeight - size.height)/2), size: MKMapSize(width: newWidth, height: newHeight)) 27 | 28 | } 29 | } 30 | 31 | let CLLocationCoordinate2DMax = CLLocationCoordinate2D(latitude: 90, longitude: 180) 32 | let MKMapPointMax = MKMapPoint(CLLocationCoordinate2DMax) 33 | 34 | extension CLLocationCoordinate2D: Hashable { 35 | public func hash(into hasher: inout Hasher) { 36 | hasher.combine(latitude) 37 | hasher.combine(longitude) 38 | } 39 | } 40 | 41 | public func ==(lhs: CLLocationCoordinate2D, rhs: CLLocationCoordinate2D) -> Bool { 42 | return lhs.latitude == rhs.latitude && lhs.longitude == rhs.longitude 43 | } 44 | 45 | extension Double { 46 | var zoomLevel: Double { 47 | let maxZoomLevel = log2(MKMapSize.world.width / 256) // 20 48 | let zoomLevel = floor(log2(self) + 0.5) // negative 49 | return max(0, maxZoomLevel + zoomLevel) // max - current 50 | } 51 | } 52 | 53 | private let radiusOfEarth: Double = 6372797.6 54 | 55 | extension CLLocationCoordinate2D { 56 | func coordinate(onBearingInRadians bearing: Double, atDistanceInMeters distance: Double) -> CLLocationCoordinate2D { 57 | let distRadians = distance / radiusOfEarth // earth radius in meters 58 | 59 | let lat1 = latitude * .pi / 180 60 | let lon1 = longitude * .pi / 180 61 | 62 | let lat2 = asin(sin(lat1) * cos(distRadians) + cos(lat1) * sin(distRadians) * cos(bearing)) 63 | let lon2 = lon1 + atan2(sin(bearing) * sin(distRadians) * cos(lat1), cos(distRadians) - sin(lat1) * sin(lat2)) 64 | 65 | return CLLocationCoordinate2D(latitude: lat2 * 180 / .pi, longitude: lon2 * 180 / .pi) 66 | } 67 | var location: CLLocation { 68 | return CLLocation(latitude: latitude, longitude: longitude) 69 | } 70 | func distance(from coordinate: CLLocationCoordinate2D) -> CLLocationDistance { 71 | return location.distance(from: coordinate.location) 72 | } 73 | } 74 | 75 | extension Array where Element: MKAnnotation { 76 | func subtracted(_ other: [Element]) -> [Element] { 77 | return filter { item in !other.contains { $0.isEqual(item) } } 78 | } 79 | mutating func subtract(_ other: [Element]) { 80 | self = self.subtracted(other) 81 | } 82 | mutating func add(_ other: [Element]) { 83 | self.append(contentsOf: other) 84 | } 85 | @discardableResult 86 | mutating func remove(_ item: Element) -> Element? { 87 | return firstIndex { $0.isEqual(item) }.map { remove(at: $0) } 88 | } 89 | } 90 | 91 | extension MKPolyline { 92 | convenience init(mapRect: MKMapRect) { 93 | let points = [ 94 | MKMapPoint(x: mapRect.minX, y: mapRect.minY), 95 | MKMapPoint(x: mapRect.maxX, y: mapRect.minY), 96 | MKMapPoint(x: mapRect.maxX, y: mapRect.maxY), 97 | MKMapPoint(x: mapRect.minX, y: mapRect.maxY), 98 | MKMapPoint(x: mapRect.minX, y: mapRect.minY) 99 | ] 100 | self.init(points: points, count: points.count) 101 | } 102 | } 103 | 104 | extension OperationQueue { 105 | static var serial: OperationQueue { 106 | let queue = OperationQueue() 107 | queue.name = "com.cluster.serialQueue" 108 | queue.maxConcurrentOperationCount = 1 109 | return queue 110 | } 111 | func addBlockOperation(_ block: @escaping (BlockOperation) -> Void) { 112 | let operation = BlockOperation() 113 | operation.addExecutionBlock { [weak operation] in 114 | guard let operation = operation else { return } 115 | block(operation) 116 | } 117 | self.addOperation(operation) 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /docs/css/highlight.css: -------------------------------------------------------------------------------- 1 | /* Credit to https://gist.github.com/wataru420/2048287 */ 2 | .highlight { 3 | /* Comment */ 4 | /* Error */ 5 | /* Keyword */ 6 | /* Operator */ 7 | /* Comment.Multiline */ 8 | /* Comment.Preproc */ 9 | /* Comment.Single */ 10 | /* Comment.Special */ 11 | /* Generic.Deleted */ 12 | /* Generic.Deleted.Specific */ 13 | /* Generic.Emph */ 14 | /* Generic.Error */ 15 | /* Generic.Heading */ 16 | /* Generic.Inserted */ 17 | /* Generic.Inserted.Specific */ 18 | /* Generic.Output */ 19 | /* Generic.Prompt */ 20 | /* Generic.Strong */ 21 | /* Generic.Subheading */ 22 | /* Generic.Traceback */ 23 | /* Keyword.Constant */ 24 | /* Keyword.Declaration */ 25 | /* Keyword.Pseudo */ 26 | /* Keyword.Reserved */ 27 | /* Keyword.Type */ 28 | /* Literal.Number */ 29 | /* Literal.String */ 30 | /* Name.Attribute */ 31 | /* Name.Builtin */ 32 | /* Name.Class */ 33 | /* Name.Constant */ 34 | /* Name.Entity */ 35 | /* Name.Exception */ 36 | /* Name.Function */ 37 | /* Name.Namespace */ 38 | /* Name.Tag */ 39 | /* Name.Variable */ 40 | /* Operator.Word */ 41 | /* Text.Whitespace */ 42 | /* Literal.Number.Float */ 43 | /* Literal.Number.Hex */ 44 | /* Literal.Number.Integer */ 45 | /* Literal.Number.Oct */ 46 | /* Literal.String.Backtick */ 47 | /* Literal.String.Char */ 48 | /* Literal.String.Doc */ 49 | /* Literal.String.Double */ 50 | /* Literal.String.Escape */ 51 | /* Literal.String.Heredoc */ 52 | /* Literal.String.Interpol */ 53 | /* Literal.String.Other */ 54 | /* Literal.String.Regex */ 55 | /* Literal.String.Single */ 56 | /* Literal.String.Symbol */ 57 | /* Name.Builtin.Pseudo */ 58 | /* Name.Variable.Class */ 59 | /* Name.Variable.Global */ 60 | /* Name.Variable.Instance */ 61 | /* Literal.Number.Integer.Long */ } 62 | .highlight .c { 63 | color: #999988; 64 | font-style: italic; } 65 | .highlight .err { 66 | color: #a61717; 67 | background-color: #e3d2d2; } 68 | .highlight .k { 69 | color: #000000; 70 | font-weight: bold; } 71 | .highlight .o { 72 | color: #000000; 73 | font-weight: bold; } 74 | .highlight .cm { 75 | color: #999988; 76 | font-style: italic; } 77 | .highlight .cp { 78 | color: #999999; 79 | font-weight: bold; } 80 | .highlight .c1 { 81 | color: #999988; 82 | font-style: italic; } 83 | .highlight .cs { 84 | color: #999999; 85 | font-weight: bold; 86 | font-style: italic; } 87 | .highlight .gd { 88 | color: #000000; 89 | background-color: #ffdddd; } 90 | .highlight .gd .x { 91 | color: #000000; 92 | background-color: #ffaaaa; } 93 | .highlight .ge { 94 | color: #000000; 95 | font-style: italic; } 96 | .highlight .gr { 97 | color: #aa0000; } 98 | .highlight .gh { 99 | color: #999999; } 100 | .highlight .gi { 101 | color: #000000; 102 | background-color: #ddffdd; } 103 | .highlight .gi .x { 104 | color: #000000; 105 | background-color: #aaffaa; } 106 | .highlight .go { 107 | color: #888888; } 108 | .highlight .gp { 109 | color: #555555; } 110 | .highlight .gs { 111 | font-weight: bold; } 112 | .highlight .gu { 113 | color: #aaaaaa; } 114 | .highlight .gt { 115 | color: #aa0000; } 116 | .highlight .kc { 117 | color: #000000; 118 | font-weight: bold; } 119 | .highlight .kd { 120 | color: #000000; 121 | font-weight: bold; } 122 | .highlight .kp { 123 | color: #000000; 124 | font-weight: bold; } 125 | .highlight .kr { 126 | color: #000000; 127 | font-weight: bold; } 128 | .highlight .kt { 129 | color: #445588; } 130 | .highlight .m { 131 | color: #009999; } 132 | .highlight .s { 133 | color: #d14; } 134 | .highlight .na { 135 | color: #008080; } 136 | .highlight .nb { 137 | color: #0086B3; } 138 | .highlight .nc { 139 | color: #445588; 140 | font-weight: bold; } 141 | .highlight .no { 142 | color: #008080; } 143 | .highlight .ni { 144 | color: #800080; } 145 | .highlight .ne { 146 | color: #990000; 147 | font-weight: bold; } 148 | .highlight .nf { 149 | color: #990000; } 150 | .highlight .nn { 151 | color: #555555; } 152 | .highlight .nt { 153 | color: #000080; } 154 | .highlight .nv { 155 | color: #008080; } 156 | .highlight .ow { 157 | color: #000000; 158 | font-weight: bold; } 159 | .highlight .w { 160 | color: #bbbbbb; } 161 | .highlight .mf { 162 | color: #009999; } 163 | .highlight .mh { 164 | color: #009999; } 165 | .highlight .mi { 166 | color: #009999; } 167 | .highlight .mo { 168 | color: #009999; } 169 | .highlight .sb { 170 | color: #d14; } 171 | .highlight .sc { 172 | color: #d14; } 173 | .highlight .sd { 174 | color: #d14; } 175 | .highlight .s2 { 176 | color: #d14; } 177 | .highlight .se { 178 | color: #d14; } 179 | .highlight .sh { 180 | color: #d14; } 181 | .highlight .si { 182 | color: #d14; } 183 | .highlight .sx { 184 | color: #d14; } 185 | .highlight .sr { 186 | color: #009926; } 187 | .highlight .s1 { 188 | color: #d14; } 189 | .highlight .ss { 190 | color: #990073; } 191 | .highlight .bp { 192 | color: #999999; } 193 | .highlight .vc { 194 | color: #008080; } 195 | .highlight .vg { 196 | color: #008080; } 197 | .highlight .vi { 198 | color: #008080; } 199 | .highlight .il { 200 | color: #009999; } 201 | -------------------------------------------------------------------------------- /docs/docsets/Cluster.docset/Contents/Resources/Documents/css/highlight.css: -------------------------------------------------------------------------------- 1 | /* Credit to https://gist.github.com/wataru420/2048287 */ 2 | .highlight { 3 | /* Comment */ 4 | /* Error */ 5 | /* Keyword */ 6 | /* Operator */ 7 | /* Comment.Multiline */ 8 | /* Comment.Preproc */ 9 | /* Comment.Single */ 10 | /* Comment.Special */ 11 | /* Generic.Deleted */ 12 | /* Generic.Deleted.Specific */ 13 | /* Generic.Emph */ 14 | /* Generic.Error */ 15 | /* Generic.Heading */ 16 | /* Generic.Inserted */ 17 | /* Generic.Inserted.Specific */ 18 | /* Generic.Output */ 19 | /* Generic.Prompt */ 20 | /* Generic.Strong */ 21 | /* Generic.Subheading */ 22 | /* Generic.Traceback */ 23 | /* Keyword.Constant */ 24 | /* Keyword.Declaration */ 25 | /* Keyword.Pseudo */ 26 | /* Keyword.Reserved */ 27 | /* Keyword.Type */ 28 | /* Literal.Number */ 29 | /* Literal.String */ 30 | /* Name.Attribute */ 31 | /* Name.Builtin */ 32 | /* Name.Class */ 33 | /* Name.Constant */ 34 | /* Name.Entity */ 35 | /* Name.Exception */ 36 | /* Name.Function */ 37 | /* Name.Namespace */ 38 | /* Name.Tag */ 39 | /* Name.Variable */ 40 | /* Operator.Word */ 41 | /* Text.Whitespace */ 42 | /* Literal.Number.Float */ 43 | /* Literal.Number.Hex */ 44 | /* Literal.Number.Integer */ 45 | /* Literal.Number.Oct */ 46 | /* Literal.String.Backtick */ 47 | /* Literal.String.Char */ 48 | /* Literal.String.Doc */ 49 | /* Literal.String.Double */ 50 | /* Literal.String.Escape */ 51 | /* Literal.String.Heredoc */ 52 | /* Literal.String.Interpol */ 53 | /* Literal.String.Other */ 54 | /* Literal.String.Regex */ 55 | /* Literal.String.Single */ 56 | /* Literal.String.Symbol */ 57 | /* Name.Builtin.Pseudo */ 58 | /* Name.Variable.Class */ 59 | /* Name.Variable.Global */ 60 | /* Name.Variable.Instance */ 61 | /* Literal.Number.Integer.Long */ } 62 | .highlight .c { 63 | color: #999988; 64 | font-style: italic; } 65 | .highlight .err { 66 | color: #a61717; 67 | background-color: #e3d2d2; } 68 | .highlight .k { 69 | color: #000000; 70 | font-weight: bold; } 71 | .highlight .o { 72 | color: #000000; 73 | font-weight: bold; } 74 | .highlight .cm { 75 | color: #999988; 76 | font-style: italic; } 77 | .highlight .cp { 78 | color: #999999; 79 | font-weight: bold; } 80 | .highlight .c1 { 81 | color: #999988; 82 | font-style: italic; } 83 | .highlight .cs { 84 | color: #999999; 85 | font-weight: bold; 86 | font-style: italic; } 87 | .highlight .gd { 88 | color: #000000; 89 | background-color: #ffdddd; } 90 | .highlight .gd .x { 91 | color: #000000; 92 | background-color: #ffaaaa; } 93 | .highlight .ge { 94 | color: #000000; 95 | font-style: italic; } 96 | .highlight .gr { 97 | color: #aa0000; } 98 | .highlight .gh { 99 | color: #999999; } 100 | .highlight .gi { 101 | color: #000000; 102 | background-color: #ddffdd; } 103 | .highlight .gi .x { 104 | color: #000000; 105 | background-color: #aaffaa; } 106 | .highlight .go { 107 | color: #888888; } 108 | .highlight .gp { 109 | color: #555555; } 110 | .highlight .gs { 111 | font-weight: bold; } 112 | .highlight .gu { 113 | color: #aaaaaa; } 114 | .highlight .gt { 115 | color: #aa0000; } 116 | .highlight .kc { 117 | color: #000000; 118 | font-weight: bold; } 119 | .highlight .kd { 120 | color: #000000; 121 | font-weight: bold; } 122 | .highlight .kp { 123 | color: #000000; 124 | font-weight: bold; } 125 | .highlight .kr { 126 | color: #000000; 127 | font-weight: bold; } 128 | .highlight .kt { 129 | color: #445588; } 130 | .highlight .m { 131 | color: #009999; } 132 | .highlight .s { 133 | color: #d14; } 134 | .highlight .na { 135 | color: #008080; } 136 | .highlight .nb { 137 | color: #0086B3; } 138 | .highlight .nc { 139 | color: #445588; 140 | font-weight: bold; } 141 | .highlight .no { 142 | color: #008080; } 143 | .highlight .ni { 144 | color: #800080; } 145 | .highlight .ne { 146 | color: #990000; 147 | font-weight: bold; } 148 | .highlight .nf { 149 | color: #990000; } 150 | .highlight .nn { 151 | color: #555555; } 152 | .highlight .nt { 153 | color: #000080; } 154 | .highlight .nv { 155 | color: #008080; } 156 | .highlight .ow { 157 | color: #000000; 158 | font-weight: bold; } 159 | .highlight .w { 160 | color: #bbbbbb; } 161 | .highlight .mf { 162 | color: #009999; } 163 | .highlight .mh { 164 | color: #009999; } 165 | .highlight .mi { 166 | color: #009999; } 167 | .highlight .mo { 168 | color: #009999; } 169 | .highlight .sb { 170 | color: #d14; } 171 | .highlight .sc { 172 | color: #d14; } 173 | .highlight .sd { 174 | color: #d14; } 175 | .highlight .s2 { 176 | color: #d14; } 177 | .highlight .se { 178 | color: #d14; } 179 | .highlight .sh { 180 | color: #d14; } 181 | .highlight .si { 182 | color: #d14; } 183 | .highlight .sx { 184 | color: #d14; } 185 | .highlight .sr { 186 | color: #009926; } 187 | .highlight .s1 { 188 | color: #d14; } 189 | .highlight .ss { 190 | color: #990073; } 191 | .highlight .bp { 192 | color: #999999; } 193 | .highlight .vc { 194 | color: #008080; } 195 | .highlight .vg { 196 | color: #008080; } 197 | .highlight .vi { 198 | color: #008080; } 199 | .highlight .il { 200 | color: #009999; } 201 | -------------------------------------------------------------------------------- /Sources/QuadTree.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QuadTree.swift 3 | // Cluster 4 | // 5 | // Created by Lasha Efremidze on 5/6/17. 6 | // Copyright © 2017 efremidze. All rights reserved. 7 | // 8 | 9 | import MapKit 10 | 11 | class QuadTreeNode { 12 | 13 | enum NodeType { 14 | case leaf 15 | case `internal`(children: Children) 16 | } 17 | 18 | struct Children: Sequence { 19 | let northWest: QuadTreeNode 20 | let northEast: QuadTreeNode 21 | let southWest: QuadTreeNode 22 | let southEast: QuadTreeNode 23 | 24 | init(parentNode: QuadTreeNode) { 25 | let mapRect = parentNode.rect 26 | northWest = QuadTreeNode(rect: MKMapRect(minX: mapRect.minX, minY: mapRect.minY, maxX: mapRect.midX, maxY: mapRect.midY)) 27 | northEast = QuadTreeNode(rect: MKMapRect(minX: mapRect.midX, minY: mapRect.minY, maxX: mapRect.maxX, maxY: mapRect.midY)) 28 | southWest = QuadTreeNode(rect: MKMapRect(minX: mapRect.minX, minY: mapRect.midY, maxX: mapRect.midX, maxY: mapRect.maxY)) 29 | southEast = QuadTreeNode(rect: MKMapRect(minX: mapRect.midX, minY: mapRect.midY, maxX: mapRect.maxX, maxY: mapRect.maxY)) 30 | } 31 | 32 | struct ChildrenIterator: IteratorProtocol { 33 | private var index = 0 34 | private let children: Children 35 | 36 | init(children: Children) { 37 | self.children = children 38 | } 39 | 40 | mutating func next() -> QuadTreeNode? { 41 | defer { index += 1 } 42 | switch index { 43 | case 0: return children.northWest 44 | case 1: return children.northEast 45 | case 2: return children.southWest 46 | case 3: return children.southEast 47 | default: return nil 48 | } 49 | } 50 | } 51 | 52 | public func makeIterator() -> ChildrenIterator { 53 | return ChildrenIterator(children: self) 54 | } 55 | } 56 | 57 | var annotations = [MKAnnotation]() 58 | let rect: MKMapRect 59 | var type: NodeType = .leaf 60 | 61 | static let maxPointCapacity = 8 62 | 63 | init(rect: MKMapRect) { 64 | self.rect = rect 65 | } 66 | 67 | } 68 | 69 | extension QuadTreeNode: AnnotationsContainer { 70 | 71 | @discardableResult 72 | func add(_ annotation: MKAnnotation) -> Bool { 73 | guard rect.contains(annotation.coordinate) else { return false } 74 | 75 | switch type { 76 | case .leaf: 77 | annotations.append(annotation) 78 | // if the max capacity was reached, become an internal node 79 | if annotations.count == QuadTreeNode.maxPointCapacity { 80 | subdivide() 81 | } 82 | case .internal(let children): 83 | // pass the point to one of the children 84 | for child in children where child.add(annotation) { 85 | return true 86 | } 87 | 88 | assertionFailure("rect.contains evaluted to true, but none of the children added the annotation") 89 | } 90 | return true 91 | } 92 | 93 | @discardableResult 94 | func remove(_ annotation: MKAnnotation) -> Bool { 95 | guard rect.contains(annotation.coordinate) else { return false } 96 | 97 | _ = annotations.map { $0.coordinate }.firstIndex(of: annotation.coordinate).map { annotations.remove(at: $0) } 98 | 99 | switch type { 100 | case .leaf: break 101 | case .internal(let children): 102 | // pass the point to one of the children 103 | for child in children where child.remove(annotation) { 104 | return true 105 | } 106 | 107 | assertionFailure("rect.contains evaluted to true, but none of the children removed the annotation") 108 | } 109 | return true 110 | } 111 | 112 | private func subdivide() { 113 | switch type { 114 | case .leaf: 115 | type = .internal(children: Children(parentNode: self)) 116 | case .internal: 117 | preconditionFailure("Calling subdivide on an internal node") 118 | } 119 | } 120 | 121 | func annotations(in rect: MKMapRect) -> [MKAnnotation] { 122 | guard self.rect.intersects(rect) else { return [] } 123 | 124 | var result = [MKAnnotation]() 125 | 126 | for annotation in annotations where rect.contains(annotation.coordinate) { 127 | result.append(annotation) 128 | } 129 | 130 | switch type { 131 | case .leaf: break 132 | case .internal(let children): 133 | for childNode in children { 134 | result.append(contentsOf: childNode.annotations(in: rect)) 135 | } 136 | } 137 | 138 | return result 139 | } 140 | 141 | } 142 | 143 | public class QuadTree: AnnotationsContainer { 144 | 145 | let root: QuadTreeNode 146 | 147 | public init(rect: MKMapRect) { 148 | self.root = QuadTreeNode(rect: rect) 149 | } 150 | 151 | @discardableResult 152 | public func add(_ annotation: MKAnnotation) -> Bool { 153 | return root.add(annotation) 154 | } 155 | 156 | @discardableResult 157 | public func remove(_ annotation: MKAnnotation) -> Bool { 158 | return root.remove(annotation) 159 | } 160 | 161 | public func annotations(in rect: MKMapRect) -> [MKAnnotation] { 162 | return root.annotations(in: rect) 163 | } 164 | 165 | } 166 | -------------------------------------------------------------------------------- /Sources/Annotation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Annotation.swift 3 | // Cluster 4 | // 5 | // Created by Lasha Efremidze on 4/15/17. 6 | // Copyright © 2017 efremidze. All rights reserved. 7 | // 8 | 9 | import MapKit 10 | 11 | open class Annotation: MKPointAnnotation { 12 | // @available(swift, obsoleted: 6.0, message: "Please migrate to StyledClusterAnnotationView.") 13 | open var style: ClusterAnnotationStyle? 14 | 15 | // public convenience init(coordinate: CLLocationCoordinate2D) { 16 | // self.init(__coordinate: coordinate) 17 | // } 18 | public convenience init(coordinate: CLLocationCoordinate2D) { 19 | self.init() 20 | self.coordinate = coordinate 21 | } 22 | } 23 | 24 | open class ClusterAnnotation: Annotation { 25 | open var annotations: [MKAnnotation] 26 | 27 | open override func isEqual(_ object: Any?) -> Bool { 28 | guard let object = object as? ClusterAnnotation else { return false } 29 | 30 | if self === object { 31 | return true 32 | } 33 | 34 | if coordinate != object.coordinate { 35 | return false 36 | } 37 | 38 | if annotations.count != object.annotations.count { 39 | return false 40 | } 41 | 42 | return annotations.map { $0.coordinate } == object.annotations.map { $0.coordinate } 43 | } 44 | public init(annotations: [MKAnnotation] = [MKAnnotation]()) { 45 | self.annotations = annotations 46 | } 47 | 48 | public convenience init(annotations: [MKAnnotation] = [MKAnnotation](), coordinate: CLLocationCoordinate2D) { 49 | self.init(annotations: annotations) 50 | self.coordinate = coordinate 51 | 52 | 53 | } 54 | } 55 | 56 | /** 57 | The view associated with your cluster annotations. 58 | */ 59 | open class ClusterAnnotationView: MKAnnotationView { 60 | open lazy var countLabel: UILabel = { 61 | let label = UILabel() 62 | label.autoresizingMask = [.flexibleWidth, .flexibleHeight] 63 | label.backgroundColor = .clear 64 | label.font = .boldSystemFont(ofSize: 13) 65 | label.textColor = .white 66 | label.textAlignment = .center 67 | label.adjustsFontSizeToFitWidth = true 68 | label.minimumScaleFactor = 0.5 69 | label.baselineAdjustment = .alignCenters 70 | self.addSubview(label) 71 | return label 72 | }() 73 | 74 | open override var annotation: MKAnnotation? { 75 | didSet { 76 | configure() 77 | } 78 | } 79 | 80 | open func configure() { 81 | guard let annotation = annotation as? ClusterAnnotation else { return } 82 | let count = annotation.annotations.count 83 | countLabel.text = "\(count)" 84 | } 85 | } 86 | 87 | /** 88 | The style of the cluster annotation view. 89 | */ 90 | public enum ClusterAnnotationStyle { 91 | /** 92 | Displays the annotations as a circle. 93 | 94 | - `color`: The color of the annotation circle 95 | - `radius`: The radius of the annotation circle 96 | */ 97 | case color(UIColor, radius: CGFloat) 98 | 99 | /** 100 | Displays the annotation as an image. 101 | */ 102 | case image(UIImage?) 103 | } 104 | 105 | /** 106 | A cluster annotation view that supports styles. 107 | */ 108 | open class StyledClusterAnnotationView: ClusterAnnotationView { 109 | 110 | /** 111 | The style of the cluster annotation view. 112 | */ 113 | public var style: ClusterAnnotationStyle 114 | 115 | /** 116 | Initializes and returns a new cluster annotation view. 117 | 118 | - Parameters: 119 | - annotation: The annotation object to associate with the new view. 120 | - reuseIdentifier: If you plan to reuse the annotation view for similar types of annotations, pass a string to identify it. Although you can pass nil if you do not intend to reuse the view, reusing annotation views is generally recommended. 121 | - style: The cluster annotation style to associate with the new view. 122 | 123 | - Returns: The initialized cluster annotation view. 124 | */ 125 | public init(annotation: MKAnnotation?, reuseIdentifier: String?, style: ClusterAnnotationStyle) { 126 | self.style = style 127 | super.init(annotation: annotation, reuseIdentifier: reuseIdentifier) 128 | configure() 129 | } 130 | 131 | required public init?(coder aDecoder: NSCoder) { 132 | fatalError("init(coder:) has not been implemented") 133 | } 134 | 135 | open override func configure() { 136 | guard let annotation = annotation as? ClusterAnnotation else { return } 137 | 138 | switch style { 139 | case let .image(image): 140 | backgroundColor = .clear 141 | self.image = image 142 | case let .color(color, radius): 143 | let count = annotation.annotations.count 144 | backgroundColor = color 145 | var diameter = radius * 2 146 | switch count { 147 | case _ where count < 8: 148 | diameter *= 0.6 149 | case _ where count < 16: 150 | diameter *= 0.8 151 | default: break 152 | } 153 | frame = CGRect(origin: frame.origin, size: CGSize(width: diameter, height: diameter)) 154 | countLabel.text = "\(count)" 155 | } 156 | } 157 | 158 | override open func layoutSubviews() { 159 | super.layoutSubviews() 160 | 161 | if case .color = style { 162 | layer.masksToBounds = true 163 | layer.cornerRadius = image == nil ? bounds.width / 2 : 0 164 | countLabel.frame = bounds 165 | } 166 | } 167 | 168 | } 169 | -------------------------------------------------------------------------------- /Sources/AnnotationGrouper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnnotationGrouper.swift 3 | // Example 4 | // 5 | // Created by Nick Trienens on 2/14/20. 6 | // Copyright © 2020 efremidze. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import MapKit 11 | 12 | extension MKAnnotation { 13 | static func == (lhs: MKAnnotation, rhs: MKAnnotation) -> Bool { 14 | return lhs.coordinate == rhs.coordinate 15 | } 16 | } 17 | 18 | public class AnnotationGrouper: AnnotationsContainer { 19 | var annotations = [MKAnnotation]() 20 | var groups = [AnnatationGroup]() 21 | var updateSinceLastRanging = true 22 | 23 | var rangedSet = [SortingPin]() 24 | 25 | @discardableResult 26 | public func add(_ annotation: MKAnnotation) -> Bool { 27 | if annotations.contains(where: { $0.coordinate == annotation.coordinate }) { return false } 28 | 29 | annotations.append(annotation) 30 | updateSinceLastRanging = true 31 | return true 32 | } 33 | 34 | @discardableResult 35 | public func remove(_ annotation: MKAnnotation) -> Bool { 36 | annotations.remove(annotation) 37 | updateSinceLastRanging = true 38 | return true 39 | } 40 | 41 | public func annotations(in rect: MKMapRect) -> [MKAnnotation] { 42 | return annotations 43 | } 44 | 45 | func clusteredAnnotations( zoomScale: Double, 46 | minCountForClustering: Int, 47 | mapCenter: CLLocationCoordinate2D, 48 | maxZoomLevel: Int, 49 | delegate: ClusterManagerDelegate?) -> [MKAnnotation] { 50 | 51 | let clusteringRange = 1.0/zoomScale * 10.0 52 | 53 | if updateSinceLastRanging { 54 | rangedSet = annotations.map { pin -> SortingPin in 55 | let sorted = annotations.compactMap { second -> SortingPin? in 56 | guard !pin.isEqual(second) else { return nil } 57 | let distance = pin.coordinate.distance(from: second.coordinate) 58 | return SortingPin( annotation: second, distance: distance) 59 | } 60 | return SortingPin(annotation: pin, nearby: sorted) 61 | } 62 | .sorted { $0.annotation.coordinate.distance(from: mapCenter) < $1.annotation.coordinate.distance(from: mapCenter) } 63 | 64 | updateSinceLastRanging = false 65 | } 66 | var usedPins = [SortingPin]() 67 | var createdClusters = [AnnatationGroup]() 68 | var singleAnnotations = [MKAnnotation]() 69 | var protectedAnnotations = [MKAnnotation]() 70 | 71 | 72 | for pin in rangedSet { 73 | if zoomScale.zoomLevel > Double(maxZoomLevel) { 74 | singleAnnotations.append(pin.annotation) 75 | continue 76 | } 77 | 78 | if !(delegate?.shouldClusterAnnotation(pin.annotation) ?? true) { 79 | protectedAnnotations.append(pin.annotation) 80 | usedPins.append(pin) 81 | continue 82 | } 83 | if usedPins.contains(pin) { 84 | continue 85 | } 86 | 87 | let clustingPins = pin.nearby.filter { proposed in 88 | let taken = usedPins.contains(where: { $0.annotation.coordinate == proposed.annotation.coordinate }) 89 | return proposed.distance < clusteringRange && !taken 90 | 91 | } 92 | 93 | if clustingPins.count <= minCountForClustering { 94 | singleAnnotations.append(pin.annotation) 95 | usedPins.append(pin) 96 | 97 | clustingPins.forEach { 98 | singleAnnotations.append($0.annotation) 99 | } 100 | } else { 101 | let newGroup = AnnatationGroup() 102 | newGroup.annotations = clustingPins.map { $0.annotation } + [pin.annotation] 103 | usedPins.append(pin) 104 | clustingPins.forEach { 105 | usedPins.append($0) 106 | singleAnnotations.subtract([$0.annotation]) 107 | } 108 | createdClusters.append(newGroup) 109 | } 110 | } 111 | 112 | return protectedAnnotations + singleAnnotations + createdClusters.map( { ClusterAnnotation(annotations: $0.annotations, coordinate: $0.coordinate()) }) 113 | } 114 | } 115 | 116 | struct SortingPin: Equatable { 117 | static func == (lhs: SortingPin, rhs: SortingPin) -> Bool { 118 | lhs.annotation.isEqual(rhs.annotation) 119 | } 120 | 121 | let annotation: MKAnnotation 122 | let nearby: [SortingPin] 123 | let distance: Double 124 | 125 | init( annotation: MKAnnotation, nearby: [SortingPin] = [SortingPin](), distance: Double = .greatestFiniteMagnitude) { 126 | self.annotation = annotation 127 | self.nearby = nearby 128 | self.distance = distance 129 | } 130 | } 131 | 132 | class AnnatationGroup: Equatable { 133 | static func == (lhs: AnnatationGroup, rhs: AnnatationGroup) -> Bool { 134 | lhs.id == rhs.id 135 | } 136 | 137 | let id = UUID().uuidString 138 | var annotations = [MKAnnotation]() 139 | 140 | 141 | func coordinate() -> CLLocationCoordinate2D { 142 | let coordinates = annotations.map { $0.coordinate } 143 | let totals = coordinates.reduce((latitude: 0.0, longitude: 0.0)) { ($0.latitude + $1.latitude, $0.longitude + $1.longitude) } 144 | return CLLocationCoordinate2D(latitude: totals.latitude / Double(coordinates.count), longitude: totals.longitude / Double(coordinates.count)) 145 | } 146 | 147 | } 148 | -------------------------------------------------------------------------------- /docs/Extensions.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Extensions Reference 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 |

Cluster Docs (56% documented)

18 |
19 |
20 |
21 | 26 |
27 |
28 | 90 |
91 |
92 |
93 |

Extensions

94 |

The following extensions are available globally.

95 | 96 |
97 |
98 |
99 |
    100 |
  • 101 |
    102 | 103 | 104 | 105 | CLLocationCoordinate2D 106 | 107 |
    108 |
    109 |
    110 |
    111 |
    112 |
    113 | 114 | See more 115 |
    116 |
    117 |

    Declaration

    118 |
    119 |

    Swift

    120 |
    struct CLLocationCoordinate2D
    121 | 122 |
    123 |
    124 |
    125 |
    126 |
  • 127 |
128 |
129 |
130 |
131 | 135 |
136 |
137 | 138 | 139 | 140 | -------------------------------------------------------------------------------- /docs/docsets/Cluster.docset/Contents/Resources/Documents/Extensions.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Extensions Reference 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 |

Cluster Docs (56% documented)

18 |
19 |
20 |
21 | 26 |
27 |
28 | 90 |
91 |
92 |
93 |

Extensions

94 |

The following extensions are available globally.

95 | 96 |
97 |
98 |
99 |
    100 |
  • 101 |
    102 | 103 | 104 | 105 | CLLocationCoordinate2D 106 | 107 |
    108 |
    109 |
    110 |
    111 |
    112 |
    113 | 114 | See more 115 |
    116 |
    117 |

    Declaration

    118 |
    119 |

    Swift

    120 |
    struct CLLocationCoordinate2D
    121 | 122 |
    123 |
    124 |
    125 |
    126 |
  • 127 |
128 |
129 |
130 |
131 | 135 |
136 |
137 | 138 | 139 | 140 | -------------------------------------------------------------------------------- /docs/Enums.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Enumerations Reference 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 |

Cluster Docs (56% documented)

18 |
19 |
20 |
21 | 26 |
27 |
28 | 90 |
91 |
92 |
93 |

Enumerations

94 |

The following enumerations are available globally.

95 | 96 |
97 |
98 |
99 |
    100 |
  • 101 |
    102 | 103 | 104 | 105 | ClusterAnnotationStyle 106 | 107 |
    108 |
    109 |
    110 |
    111 |
    112 |
    113 |

    The style of the cluster annotation view.

    114 | 115 | See more 116 |
    117 |
    118 |

    Declaration

    119 |
    120 |

    Swift

    121 |
    public enum ClusterAnnotationStyle
    122 | 123 |
    124 |
    125 |
    126 |
    127 |
  • 128 |
129 |
130 |
131 |
132 | 136 |
137 |
138 | 139 | 140 | 141 | -------------------------------------------------------------------------------- /docs/Protocols.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Protocols Reference 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 |

Cluster Docs (56% documented)

18 |
19 |
20 |
21 | 26 |
27 |
28 | 90 |
91 |
92 |
93 |

Protocols

94 |

The following protocols are available globally.

95 | 96 |
97 |
98 |
99 |
    100 |
  • 101 |
    102 | 103 | 104 | 105 | ClusterManagerDelegate 106 | 107 |
    108 |
    109 |
    110 |
    111 |
    112 |
    113 |

    Undocumented

    114 | 115 | See more 116 |
    117 |
    118 |

    Declaration

    119 |
    120 |

    Swift

    121 |
    public protocol ClusterManagerDelegate : AnyObject
    122 | 123 |
    124 |
    125 |
    126 |
    127 |
  • 128 |
129 |
130 |
131 |
132 | 136 |
137 |
138 | 139 | 140 | 141 | -------------------------------------------------------------------------------- /docs/docsets/Cluster.docset/Contents/Resources/Documents/Enums.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Enumerations Reference 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 |

Cluster Docs (56% documented)

18 |
19 |
20 |
21 | 26 |
27 |
28 | 90 |
91 |
92 |
93 |

Enumerations

94 |

The following enumerations are available globally.

95 | 96 |
97 |
98 |
99 |
    100 |
  • 101 |
    102 | 103 | 104 | 105 | ClusterAnnotationStyle 106 | 107 |
    108 |
    109 |
    110 |
    111 |
    112 |
    113 |

    The style of the cluster annotation view.

    114 | 115 | See more 116 |
    117 |
    118 |

    Declaration

    119 |
    120 |

    Swift

    121 |
    public enum ClusterAnnotationStyle
    122 | 123 |
    124 |
    125 |
    126 |
    127 |
  • 128 |
129 |
130 |
131 |
132 | 136 |
137 |
138 | 139 | 140 | 141 | -------------------------------------------------------------------------------- /docs/docsets/Cluster.docset/Contents/Resources/Documents/Protocols.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Protocols Reference 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 |

Cluster Docs (56% documented)

18 |
19 |
20 |
21 | 26 |
27 |
28 | 90 |
91 |
92 |
93 |

Protocols

94 |

The following protocols are available globally.

95 | 96 |
97 |
98 |
99 |
    100 |
  • 101 |
    102 | 103 | 104 | 105 | ClusterManagerDelegate 106 | 107 |
    108 |
    109 |
    110 |
    111 |
    112 |
    113 |

    Undocumented

    114 | 115 | See more 116 |
    117 |
    118 |

    Declaration

    119 |
    120 |

    Swift

    121 |
    public protocol ClusterManagerDelegate : AnyObject
    122 | 123 |
    124 |
    125 |
    126 |
    127 |
  • 128 |
129 |
130 |
131 |
132 | 136 |
137 |
138 | 139 | 140 | 141 | -------------------------------------------------------------------------------- /docs/Functions.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Functions Reference 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 |

Cluster Docs (56% documented)

18 |
19 |
20 |
21 | 26 |
27 |
28 | 90 |
91 |
92 |
93 |

Functions

94 |

The following functions are available globally.

95 | 96 |
97 |
98 |
99 |
    100 |
  • 101 |
    102 | 103 | 104 | 105 | ==(_:_:) 106 | 107 |
    108 |
    109 |
    110 |
    111 |
    112 |
    113 |

    Undocumented

    114 | 115 |
    116 |
    117 |

    Declaration

    118 |
    119 |

    Swift

    120 |
    public func == (lhs: CLLocationCoordinate2D, rhs: CLLocationCoordinate2D) -> Bool
    121 | 122 |
    123 |
    124 |
    125 |
    126 |
  • 127 |
128 |
129 |
130 |
131 | 135 |
136 |
137 | 138 | 139 | 140 | -------------------------------------------------------------------------------- /docs/docsets/Cluster.docset/Contents/Resources/Documents/Functions.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Functions Reference 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 |

Cluster Docs (56% documented)

18 |
19 |
20 |
21 | 26 |
27 |
28 | 90 |
91 |
92 |
93 |

Functions

94 |

The following functions are available globally.

95 | 96 |
97 |
98 |
99 |
    100 |
  • 101 |
    102 | 103 | 104 | 105 | ==(_:_:) 106 | 107 |
    108 |
    109 |
    110 |
    111 |
    112 |
    113 |

    Undocumented

    114 | 115 |
    116 |
    117 |

    Declaration

    118 |
    119 |

    Swift

    120 |
    public func == (lhs: CLLocationCoordinate2D, rhs: CLLocationCoordinate2D) -> Bool
    121 | 122 |
    123 |
    124 |
    125 |
    126 |
  • 127 |
128 |
129 |
130 |
131 | 135 |
136 |
137 | 138 | 139 | 140 | -------------------------------------------------------------------------------- /docs/Extensions/CLLocationCoordinate2D.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CLLocationCoordinate2D Extension Reference 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 |

Cluster Docs (56% documented)

18 |
19 |
20 |
21 | 26 |
27 |
28 | 90 |
91 |
92 |
93 |

CLLocationCoordinate2D

94 |
95 |
96 |
struct CLLocationCoordinate2D
97 | 98 |
99 |
100 | 101 |
102 |
103 |
104 |
    105 |
  • 106 |
    107 | 108 | 109 | 110 | hash(into:) 111 | 112 |
    113 |
    114 |
    115 |
    116 |
    117 |
    118 | 119 |
    120 |
    121 |

    Declaration

    122 |
    123 |

    Swift

    124 |
    public func hash(into hasher: inout Hasher)
    125 | 126 |
    127 |
    128 |
    129 |
    130 |
  • 131 |
132 |
133 |
134 |
135 | 139 |
140 |
141 | 142 | 143 | 144 | -------------------------------------------------------------------------------- /docs/docsets/Cluster.docset/Contents/Resources/Documents/Extensions/CLLocationCoordinate2D.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CLLocationCoordinate2D Extension Reference 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 |

Cluster Docs (56% documented)

18 |
19 |
20 |
21 | 26 |
27 |
28 | 90 |
91 |
92 |
93 |

CLLocationCoordinate2D

94 |
95 |
96 |
struct CLLocationCoordinate2D
97 | 98 |
99 |
100 | 101 |
102 |
103 |
104 |
    105 |
  • 106 |
    107 | 108 | 109 | 110 | hash(into:) 111 | 112 |
    113 |
    114 |
    115 |
    116 |
    117 |
    118 | 119 |
    120 |
    121 |

    Declaration

    122 |
    123 |

    Swift

    124 |
    public func hash(into hasher: inout Hasher)
    125 | 126 |
    127 |
    128 |
    129 |
    130 |
  • 131 |
132 |
133 |
134 |
135 | 139 |
140 |
141 | 142 | 143 | 144 | -------------------------------------------------------------------------------- /Example/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Example 4 | // 5 | // Created by Lasha Efremidze on 4/13/17. 6 | // Copyright © 2017 efremidze. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MapKit 11 | import Cluster 12 | 13 | class ViewController: UIViewController { 14 | 15 | @IBOutlet weak var mapView: MKMapView! { 16 | didSet { 17 | mapView.region = .init(center: region.center, span: .init(latitudeDelta: region.delta, longitudeDelta: region.delta)) 18 | } 19 | } 20 | @IBOutlet weak var segmentedControl: UISegmentedControl! 21 | 22 | lazy var manager: ClusterManager = { [unowned self] in 23 | let manager = ClusterManager() 24 | manager.delegate = self 25 | manager.minCountForClustering = 3 26 | manager.clusterPosition = .average 27 | return manager 28 | }() 29 | 30 | let region = (center: CLLocationCoordinate2D(latitude: 37.787994, longitude: -122.407437), delta: 0.031) 31 | 32 | override func viewDidLoad() { 33 | super.viewDidLoad() 34 | 35 | manager.add(MeAnnotation(coordinate: region.center)) 36 | addAnnotations() 37 | } 38 | 39 | @IBAction func addAnnotations(_ sender: UIButton? = nil) { 40 | // Add annotations to the manager. 41 | let annotations: [Annotation] = (0..<100).map { i in 42 | let annotation = Annotation() 43 | annotation.coordinate = CLLocationCoordinate2D(latitude: region.center.latitude + drand48() * region.delta - region.delta / 2, longitude: region.center.longitude + drand48() * region.delta - region.delta / 2) 44 | return annotation 45 | } 46 | manager.add(annotations) 47 | manager.reload(mapView: mapView) 48 | } 49 | 50 | @IBAction func removeAnnotations(_ sender: UIButton? = nil) { 51 | manager.removeAll() 52 | manager.reload(mapView: mapView) 53 | } 54 | 55 | @IBAction func valueChanged(_ sender: UISegmentedControl) { 56 | removeAnnotations() 57 | addAnnotations() 58 | } 59 | 60 | } 61 | 62 | extension ViewController: MKMapViewDelegate { 63 | 64 | func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? { 65 | if let annotation = annotation as? ClusterAnnotation { 66 | let index = segmentedControl.selectedSegmentIndex 67 | let identifier = "Cluster\(index)" 68 | let selection = Selection(rawValue: index)! 69 | return mapView.annotationView(selection: selection, annotation: annotation, reuseIdentifier: identifier) 70 | } else if let annotation = annotation as? MeAnnotation { 71 | let identifier = "Me" 72 | let annotationView = mapView.annotationView(of: MKAnnotationView.self, annotation: annotation, reuseIdentifier: identifier) 73 | annotationView.image = .me 74 | return annotationView 75 | } else { 76 | let identifier = "Pin" 77 | let annotationView = mapView.annotationView(of: MKPinAnnotationView.self, annotation: annotation, reuseIdentifier: identifier) 78 | annotationView.pinTintColor = .green 79 | return annotationView 80 | } 81 | } 82 | 83 | func mapViewDidChangeVisibleRegion(_ mapView: MKMapView) { 84 | clusterNeedsReload() 85 | } 86 | 87 | 88 | func clusterNeedsReload(){ 89 | DispatchQueue.once(in: 0.3) { 90 | manager.reload(mapView: mapView) { finished in 91 | print(finished) 92 | } 93 | } 94 | } 95 | 96 | func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) { 97 | clusterNeedsReload() 98 | } 99 | 100 | func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) { 101 | guard let annotation = view.annotation else { return } 102 | 103 | if let cluster = annotation as? ClusterAnnotation { 104 | var zoomRect = MKMapRect.null 105 | for annotation in cluster.annotations { 106 | let annotationPoint = MKMapPoint(annotation.coordinate) 107 | let pointRect = MKMapRect(x: annotationPoint.x, y: annotationPoint.y, width: 0, height: 0) 108 | if zoomRect.isNull { 109 | zoomRect = pointRect 110 | } else { 111 | zoomRect = zoomRect.union(pointRect) 112 | } 113 | } 114 | zoomRect = zoomRect.exapnd(scale: 1.1) 115 | mapView.setVisibleMapRect(zoomRect, animated: true) 116 | } 117 | } 118 | 119 | func mapView(_ mapView: MKMapView, didAdd views: [MKAnnotationView]) { 120 | views.forEach { $0.alpha = 0 } 121 | UIView.animate(withDuration: 0.35, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 0, options: [], animations: { 122 | views.forEach { $0.alpha = 1 } 123 | }, completion: nil) 124 | } 125 | 126 | } 127 | 128 | extension ViewController: ClusterManagerDelegate { 129 | 130 | func cellSize(for zoomLevel: Double) -> Double? { 131 | print(zoomLevel) 132 | switch zoomLevel { 133 | case 13...15: 134 | return 64 135 | case 16...18: 136 | return 52 137 | case 19...: 138 | return 26 139 | default: 140 | return 88 141 | } 142 | return nil // default 143 | } 144 | 145 | func shouldClusterAnnotation(_ annotation: MKAnnotation) -> Bool { 146 | return !(annotation is MeAnnotation) 147 | } 148 | 149 | } 150 | 151 | extension ViewController { 152 | enum Selection: Int { 153 | case count, imageCount, image 154 | } 155 | } 156 | 157 | extension MKMapView { 158 | func annotationView(selection: ViewController.Selection, annotation: MKAnnotation?, reuseIdentifier: String) -> MKAnnotationView { 159 | switch selection { 160 | case .count: 161 | let annotationView = self.annotationView(of: CountClusterAnnotationView.self, annotation: annotation, reuseIdentifier: reuseIdentifier) 162 | annotationView.countLabel.backgroundColor = .green 163 | return annotationView 164 | case .imageCount: 165 | let annotationView = self.annotationView(of: ImageCountClusterAnnotationView.self, annotation: annotation, reuseIdentifier: reuseIdentifier) 166 | annotationView.countLabel.textColor = .green 167 | annotationView.image = .pin2 168 | return annotationView 169 | case .image: 170 | let annotationView = self.annotationView(of: MKAnnotationView.self, annotation: annotation, reuseIdentifier: reuseIdentifier) 171 | annotationView.image = .pin 172 | return annotationView 173 | } 174 | } 175 | } 176 | 177 | class MeAnnotation: Annotation {} 178 | -------------------------------------------------------------------------------- /docs/undocumented.json: -------------------------------------------------------------------------------- 1 | { 2 | "warnings": [ 3 | { 4 | "file": "/Users/home/Documents/Cluster/Sources/Annotation.swift", 5 | "line": 11, 6 | "symbol": "Annotation", 7 | "symbol_kind": "source.lang.swift.decl.class", 8 | "warning": "undocumented" 9 | }, 10 | { 11 | "file": "/Users/home/Documents/Cluster/Sources/Annotation.swift", 12 | "line": 13, 13 | "symbol": "Annotation.style", 14 | "symbol_kind": "source.lang.swift.decl.var.instance", 15 | "warning": "undocumented" 16 | }, 17 | { 18 | "file": "/Users/home/Documents/Cluster/Sources/Annotation.swift", 19 | "line": 15, 20 | "symbol": "Annotation.init(coordinate:)", 21 | "symbol_kind": "source.lang.swift.decl.function.method.instance", 22 | "warning": "undocumented" 23 | }, 24 | { 25 | "file": "/Users/home/Documents/Cluster/Sources/Annotation.swift", 26 | "line": 21, 27 | "symbol": "ClusterAnnotation", 28 | "symbol_kind": "source.lang.swift.decl.class", 29 | "warning": "undocumented" 30 | }, 31 | { 32 | "file": "/Users/home/Documents/Cluster/Sources/Annotation.swift", 33 | "line": 22, 34 | "symbol": "ClusterAnnotation.annotations", 35 | "symbol_kind": "source.lang.swift.decl.var.instance", 36 | "warning": "undocumented" 37 | }, 38 | { 39 | "file": "/Users/home/Documents/Cluster/Sources/Annotation.swift", 40 | "line": 24, 41 | "symbol": "ClusterAnnotation.isEqual(_:)", 42 | "symbol_kind": "source.lang.swift.decl.function.method.instance", 43 | "warning": "undocumented" 44 | }, 45 | { 46 | "file": "/Users/home/Documents/Cluster/Sources/Annotation.swift", 47 | "line": 47, 48 | "symbol": "ClusterAnnotationView.countLabel", 49 | "symbol_kind": "source.lang.swift.decl.var.instance", 50 | "warning": "undocumented" 51 | }, 52 | { 53 | "file": "/Users/home/Documents/Cluster/Sources/Annotation.swift", 54 | "line": 61, 55 | "symbol": "ClusterAnnotationView.annotation", 56 | "symbol_kind": "source.lang.swift.decl.var.instance", 57 | "warning": "undocumented" 58 | }, 59 | { 60 | "file": "/Users/home/Documents/Cluster/Sources/Annotation.swift", 61 | "line": 67, 62 | "symbol": "ClusterAnnotationView.configure()", 63 | "symbol_kind": "source.lang.swift.decl.function.method.instance", 64 | "warning": "undocumented" 65 | }, 66 | { 67 | "file": "/Users/home/Documents/Cluster/Sources/Annotation.swift", 68 | "line": 118, 69 | "symbol": "StyledClusterAnnotationView.init(coder:)", 70 | "symbol_kind": "source.lang.swift.decl.function.method.instance", 71 | "warning": "undocumented" 72 | }, 73 | { 74 | "file": "/Users/home/Documents/Cluster/Sources/Annotation.swift", 75 | "line": 122, 76 | "symbol": "StyledClusterAnnotationView.configure()", 77 | "symbol_kind": "source.lang.swift.decl.function.method.instance", 78 | "warning": "undocumented" 79 | }, 80 | { 81 | "file": "/Users/home/Documents/Cluster/Sources/Annotation.swift", 82 | "line": 145, 83 | "symbol": "StyledClusterAnnotationView.layoutSubviews()", 84 | "symbol_kind": "source.lang.swift.decl.function.method.instance", 85 | "warning": "undocumented" 86 | }, 87 | { 88 | "file": "/Users/home/Documents/Cluster/Sources/Cluster.swift", 89 | "line": null, 90 | "symbol": "ClusterManagerDelegate", 91 | "symbol_kind": "source.lang.swift.decl.extension", 92 | "warning": "undocumented" 93 | }, 94 | { 95 | "file": "/Users/home/Documents/Cluster/Sources/Cluster.swift", 96 | "line": 12, 97 | "symbol": "ClusterManagerDelegate", 98 | "symbol_kind": "source.lang.swift.decl.protocol", 99 | "warning": "undocumented" 100 | }, 101 | { 102 | "file": "/Users/home/Documents/Cluster/Sources/Cluster.swift", 103 | "line": 44, 104 | "symbol": "ClusterManager", 105 | "symbol_kind": "source.lang.swift.decl.class", 106 | "warning": "undocumented" 107 | }, 108 | { 109 | "file": "/Users/home/Documents/Cluster/Sources/Cluster.swift", 110 | "line": 141, 111 | "symbol": "ClusterManager.delegate", 112 | "symbol_kind": "source.lang.swift.decl.var.instance", 113 | "warning": "undocumented" 114 | }, 115 | { 116 | "file": "/Users/home/Documents/Cluster/Sources/Cluster.swift", 117 | "line": 143, 118 | "symbol": "ClusterManager.init()", 119 | "symbol_kind": "source.lang.swift.decl.function.method.instance", 120 | "warning": "undocumented" 121 | }, 122 | { 123 | "file": "/Users/home/Documents/Cluster/Sources/Cluster.swift", 124 | "line": 249, 125 | "symbol": "ClusterManager.clusteredAnnotations(zoomScale:visibleMapRect:operation:)", 126 | "symbol_kind": "source.lang.swift.decl.function.method.instance", 127 | "warning": "undocumented" 128 | }, 129 | { 130 | "file": "/Users/home/Documents/Cluster/Sources/Cluster.swift", 131 | "line": 377, 132 | "symbol": "ClusterManager.display(mapView:toAdd:toRemove:)", 133 | "symbol_kind": "source.lang.swift.decl.function.method.instance", 134 | "warning": "undocumented" 135 | }, 136 | { 137 | "file": "/Users/home/Documents/Cluster/Sources/Extensions.swift", 138 | "line": 34, 139 | "symbol": "==(_:_:)", 140 | "symbol_kind": "source.lang.swift.decl.function.free", 141 | "warning": "undocumented" 142 | }, 143 | { 144 | "file": "/Users/home/Documents/Cluster/Sources/QuadTree.swift", 145 | "line": 149, 146 | "symbol": "QuadTree", 147 | "symbol_kind": "source.lang.swift.decl.class", 148 | "warning": "undocumented" 149 | }, 150 | { 151 | "file": "/Users/home/Documents/Cluster/Sources/QuadTree.swift", 152 | "line": 153, 153 | "symbol": "QuadTree.init(rect:)", 154 | "symbol_kind": "source.lang.swift.decl.function.method.instance", 155 | "warning": "undocumented" 156 | }, 157 | { 158 | "file": "/Users/home/Documents/Cluster/Sources/QuadTree.swift", 159 | "line": 158, 160 | "symbol": "QuadTree.add(_:)", 161 | "symbol_kind": "source.lang.swift.decl.function.method.instance", 162 | "warning": "undocumented" 163 | }, 164 | { 165 | "file": "/Users/home/Documents/Cluster/Sources/QuadTree.swift", 166 | "line": 163, 167 | "symbol": "QuadTree.remove(_:)", 168 | "symbol_kind": "source.lang.swift.decl.function.method.instance", 169 | "warning": "undocumented" 170 | }, 171 | { 172 | "file": "/Users/home/Documents/Cluster/Sources/QuadTree.swift", 173 | "line": 167, 174 | "symbol": "QuadTree.annotations(in:)", 175 | "symbol_kind": "source.lang.swift.decl.function.method.instance", 176 | "warning": "undocumented" 177 | } 178 | ], 179 | "source_directory": "/Users/home/Documents/Cluster" 180 | } -------------------------------------------------------------------------------- /docs/docsets/Cluster.docset/Contents/Resources/Documents/undocumented.json: -------------------------------------------------------------------------------- 1 | { 2 | "warnings": [ 3 | { 4 | "file": "/Users/home/Documents/Cluster/Sources/Annotation.swift", 5 | "line": 11, 6 | "symbol": "Annotation", 7 | "symbol_kind": "source.lang.swift.decl.class", 8 | "warning": "undocumented" 9 | }, 10 | { 11 | "file": "/Users/home/Documents/Cluster/Sources/Annotation.swift", 12 | "line": 13, 13 | "symbol": "Annotation.style", 14 | "symbol_kind": "source.lang.swift.decl.var.instance", 15 | "warning": "undocumented" 16 | }, 17 | { 18 | "file": "/Users/home/Documents/Cluster/Sources/Annotation.swift", 19 | "line": 15, 20 | "symbol": "Annotation.init(coordinate:)", 21 | "symbol_kind": "source.lang.swift.decl.function.method.instance", 22 | "warning": "undocumented" 23 | }, 24 | { 25 | "file": "/Users/home/Documents/Cluster/Sources/Annotation.swift", 26 | "line": 21, 27 | "symbol": "ClusterAnnotation", 28 | "symbol_kind": "source.lang.swift.decl.class", 29 | "warning": "undocumented" 30 | }, 31 | { 32 | "file": "/Users/home/Documents/Cluster/Sources/Annotation.swift", 33 | "line": 22, 34 | "symbol": "ClusterAnnotation.annotations", 35 | "symbol_kind": "source.lang.swift.decl.var.instance", 36 | "warning": "undocumented" 37 | }, 38 | { 39 | "file": "/Users/home/Documents/Cluster/Sources/Annotation.swift", 40 | "line": 24, 41 | "symbol": "ClusterAnnotation.isEqual(_:)", 42 | "symbol_kind": "source.lang.swift.decl.function.method.instance", 43 | "warning": "undocumented" 44 | }, 45 | { 46 | "file": "/Users/home/Documents/Cluster/Sources/Annotation.swift", 47 | "line": 47, 48 | "symbol": "ClusterAnnotationView.countLabel", 49 | "symbol_kind": "source.lang.swift.decl.var.instance", 50 | "warning": "undocumented" 51 | }, 52 | { 53 | "file": "/Users/home/Documents/Cluster/Sources/Annotation.swift", 54 | "line": 61, 55 | "symbol": "ClusterAnnotationView.annotation", 56 | "symbol_kind": "source.lang.swift.decl.var.instance", 57 | "warning": "undocumented" 58 | }, 59 | { 60 | "file": "/Users/home/Documents/Cluster/Sources/Annotation.swift", 61 | "line": 67, 62 | "symbol": "ClusterAnnotationView.configure()", 63 | "symbol_kind": "source.lang.swift.decl.function.method.instance", 64 | "warning": "undocumented" 65 | }, 66 | { 67 | "file": "/Users/home/Documents/Cluster/Sources/Annotation.swift", 68 | "line": 118, 69 | "symbol": "StyledClusterAnnotationView.init(coder:)", 70 | "symbol_kind": "source.lang.swift.decl.function.method.instance", 71 | "warning": "undocumented" 72 | }, 73 | { 74 | "file": "/Users/home/Documents/Cluster/Sources/Annotation.swift", 75 | "line": 122, 76 | "symbol": "StyledClusterAnnotationView.configure()", 77 | "symbol_kind": "source.lang.swift.decl.function.method.instance", 78 | "warning": "undocumented" 79 | }, 80 | { 81 | "file": "/Users/home/Documents/Cluster/Sources/Annotation.swift", 82 | "line": 145, 83 | "symbol": "StyledClusterAnnotationView.layoutSubviews()", 84 | "symbol_kind": "source.lang.swift.decl.function.method.instance", 85 | "warning": "undocumented" 86 | }, 87 | { 88 | "file": "/Users/home/Documents/Cluster/Sources/Cluster.swift", 89 | "line": null, 90 | "symbol": "ClusterManagerDelegate", 91 | "symbol_kind": "source.lang.swift.decl.extension", 92 | "warning": "undocumented" 93 | }, 94 | { 95 | "file": "/Users/home/Documents/Cluster/Sources/Cluster.swift", 96 | "line": 12, 97 | "symbol": "ClusterManagerDelegate", 98 | "symbol_kind": "source.lang.swift.decl.protocol", 99 | "warning": "undocumented" 100 | }, 101 | { 102 | "file": "/Users/home/Documents/Cluster/Sources/Cluster.swift", 103 | "line": 44, 104 | "symbol": "ClusterManager", 105 | "symbol_kind": "source.lang.swift.decl.class", 106 | "warning": "undocumented" 107 | }, 108 | { 109 | "file": "/Users/home/Documents/Cluster/Sources/Cluster.swift", 110 | "line": 141, 111 | "symbol": "ClusterManager.delegate", 112 | "symbol_kind": "source.lang.swift.decl.var.instance", 113 | "warning": "undocumented" 114 | }, 115 | { 116 | "file": "/Users/home/Documents/Cluster/Sources/Cluster.swift", 117 | "line": 143, 118 | "symbol": "ClusterManager.init()", 119 | "symbol_kind": "source.lang.swift.decl.function.method.instance", 120 | "warning": "undocumented" 121 | }, 122 | { 123 | "file": "/Users/home/Documents/Cluster/Sources/Cluster.swift", 124 | "line": 249, 125 | "symbol": "ClusterManager.clusteredAnnotations(zoomScale:visibleMapRect:operation:)", 126 | "symbol_kind": "source.lang.swift.decl.function.method.instance", 127 | "warning": "undocumented" 128 | }, 129 | { 130 | "file": "/Users/home/Documents/Cluster/Sources/Cluster.swift", 131 | "line": 377, 132 | "symbol": "ClusterManager.display(mapView:toAdd:toRemove:)", 133 | "symbol_kind": "source.lang.swift.decl.function.method.instance", 134 | "warning": "undocumented" 135 | }, 136 | { 137 | "file": "/Users/home/Documents/Cluster/Sources/Extensions.swift", 138 | "line": 34, 139 | "symbol": "==(_:_:)", 140 | "symbol_kind": "source.lang.swift.decl.function.free", 141 | "warning": "undocumented" 142 | }, 143 | { 144 | "file": "/Users/home/Documents/Cluster/Sources/QuadTree.swift", 145 | "line": 149, 146 | "symbol": "QuadTree", 147 | "symbol_kind": "source.lang.swift.decl.class", 148 | "warning": "undocumented" 149 | }, 150 | { 151 | "file": "/Users/home/Documents/Cluster/Sources/QuadTree.swift", 152 | "line": 153, 153 | "symbol": "QuadTree.init(rect:)", 154 | "symbol_kind": "source.lang.swift.decl.function.method.instance", 155 | "warning": "undocumented" 156 | }, 157 | { 158 | "file": "/Users/home/Documents/Cluster/Sources/QuadTree.swift", 159 | "line": 158, 160 | "symbol": "QuadTree.add(_:)", 161 | "symbol_kind": "source.lang.swift.decl.function.method.instance", 162 | "warning": "undocumented" 163 | }, 164 | { 165 | "file": "/Users/home/Documents/Cluster/Sources/QuadTree.swift", 166 | "line": 163, 167 | "symbol": "QuadTree.remove(_:)", 168 | "symbol_kind": "source.lang.swift.decl.function.method.instance", 169 | "warning": "undocumented" 170 | }, 171 | { 172 | "file": "/Users/home/Documents/Cluster/Sources/QuadTree.swift", 173 | "line": 167, 174 | "symbol": "QuadTree.annotations(in:)", 175 | "symbol_kind": "source.lang.swift.decl.function.method.instance", 176 | "warning": "undocumented" 177 | } 178 | ], 179 | "source_directory": "/Users/home/Documents/Cluster" 180 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change log 2 | 3 | ## [Version 3.0.3](https://github.com/efremidze/Cluster/releases/tag/3.0.3) 4 | Released on 2019-11-18 5 | 6 | - Reverted Atomic Property Wrapper 7 | 8 | ## [Version 3.0.2](https://github.com/efremidze/Cluster/releases/tag/3.0.2) 9 | Released on 2019-10-11 10 | 11 | - Added Atomic Property Wrapper 12 | 13 | ## [Version 3.0.1](https://github.com/efremidze/Cluster/releases/tag/3.0.1) 14 | Released on 2019-09-29 15 | 16 | - SPM Support 17 | 18 | ## [Version 3.0.0](https://github.com/efremidze/Cluster/releases/tag/3.0.0) 19 | Released on 2019-04-24 20 | 21 | - Swift 5.0 support 22 | 23 | ## [Version 2.4.9](https://github.com/efremidze/Cluster/releases/tag/2.4.9) 24 | Released on 2019-04-01 25 | 26 | - Memory management improvements 27 | 28 | ## [Version 2.4.8](https://github.com/efremidze/Cluster/releases/tag/2.4.8) 29 | Released on 2019-03-18 30 | 31 | - Improved error handling 32 | 33 | ## [Version 2.4.7](https://github.com/efremidze/Cluster/releases/tag/2.4.7) 34 | Released on 2019-03-14 35 | 36 | - Performance improvements 37 | 38 | ## [Version 2.4.6](https://github.com/efremidze/Cluster/releases/tag/2.4.6) 39 | Released on 2019-03-07 40 | 41 | - Performance improvements 42 | 43 | ## [Version 2.4.5](https://github.com/efremidze/Cluster/releases/tag/2.4.5) 44 | Released on 2019-03-07 45 | 46 | - Fixed distributeAnnotations bug 47 | 48 | ## [Version 2.4.4](https://github.com/efremidze/Cluster/releases/tag/2.4.4) 49 | Released on 2019-02-11 50 | 51 | - Refactored Clustering 52 | 53 | ## [Version 2.4.3](https://github.com/efremidze/Cluster/releases/tag/2.4.3) 54 | Released on 2019-01-28 55 | 56 | - Reintroduced StyledClusterAnnotationView 57 | 58 | ## [Version 2.4.2](https://github.com/efremidze/Cluster/releases/tag/2.4.2) 59 | Released on 2019-01-07 60 | 61 | - Updated README 62 | 63 | ## [Version 2.4.1](https://github.com/efremidze/Cluster/releases/tag/2.4.1) 64 | Released on 2018-12-02 65 | 66 | - Fixed crash in QuadTreeNode annotations 67 | - Added dispatch queues 68 | 69 | ## [Version 2.4.0](https://github.com/efremidze/Cluster/releases/tag/2.4.0) 70 | Released on 2018-10-18 71 | 72 | - Swift 4.2 Support 73 | 74 | ## [Version 2.3.0](https://github.com/efremidze/Cluster/releases/tag/2.3.0) 75 | Released on 2018-10-17 76 | 77 | - Fixed race condition 78 | - Added ClusterManagerDelegate 79 | - Refactored CusterAnnotationView 80 | - Improved example 81 | 82 | ## [Version 2.2.9](https://github.com/efremidze/Cluster/releases/tag/2.2.9) 83 | Released on 2018-09-23 84 | 85 | - Added code coverage 86 | - Bug fixes 87 | 88 | ## [Version 2.2.8](https://github.com/efremidze/Cluster/releases/tag/2.2.8) 89 | Released on 2018-08-21 90 | 91 | - Fixed remove annotations 92 | 93 | ## [Version 2.2.7](https://github.com/efremidze/Cluster/releases/tag/2.2.7) 94 | Released on 2018-07-10 95 | 96 | - Added tests 97 | 98 | ## [Version 2.2.6](https://github.com/efremidze/Cluster/releases/tag/2.2.6) 99 | Released on 2018-07-07 100 | 101 | - Fixed Swift 3.3 bug 102 | 103 | ## [Version 2.2.5](https://github.com/efremidze/Cluster/releases/tag/2.2.5) 104 | Released on 2018-06-15 105 | 106 | - Updated configuration 107 | 108 | ## [Version 2.2.4](https://github.com/efremidze/Cluster/releases/tag/2.2.4) 109 | Released on 2018-06-15 110 | 111 | - Added same coordinate handling options 112 | 113 | ## [Version 2.2.3](https://github.com/efremidze/Cluster/releases/tag/2.2.3) 114 | Released on 2018-05-18 115 | 116 | - Updated Podspec 117 | 118 | ## [Version 2.2.2](https://github.com/efremidze/Cluster/releases/tag/2.2.2) 119 | Released on 2018-05-06 120 | 121 | - Added documentation 122 | 123 | ## [Version 2.2.1](https://github.com/efremidze/Cluster/releases/tag/2.2.1) 124 | Released on 2018-05-03 125 | 126 | - Crash fix 127 | 128 | ## [Version 2.2.0](https://github.com/efremidze/Cluster/releases/tag/2.2.0) 129 | Released on 2018-04-12 130 | 131 | - Comparison improvements 132 | 133 | ## [Version 2.1.5](https://github.com/efremidze/Cluster/releases/tag/2.1.5) 134 | Released on 2018-04-04 135 | 136 | - Fixed queuing 137 | 138 | ## [Version 2.1.4](https://github.com/efremidze/Cluster/releases/tag/2.1.4) 139 | Released on 2018-04-03 140 | 141 | - Moved clustering to background thread 142 | 143 | ## [Version 2.1.3](https://github.com/efremidze/Cluster/releases/tag/2.1.3) 144 | Released on 2018-03-30 145 | 146 | - Swift 4.1 Support 147 | 148 | ## [Version 2.1.2](https://github.com/efremidze/Cluster/releases/tag/2.1.2) 149 | Released on 2018-02-05 150 | 151 | - Comparison fix 152 | 153 | ## [Version 2.1.1](https://github.com/efremidze/Cluster/releases/tag/2.1.1) 154 | Released on 2018-01-28 155 | 156 | - Comparison improvement 157 | 158 | ## [Version 2.1.0](https://github.com/efremidze/Cluster/releases/tag/2.1.0) 159 | Released on 2017-12-01 160 | 161 | - Bug fixes 162 | - Renamed ClusterAnnotationType to ClusterAnnotationStyle 163 | 164 | ## [Version 2.0.2](https://github.com/efremidze/Cluster/releases/tag/2.0.2) 165 | Released on 2017-11-28 166 | 167 | - Bug fixes 168 | 169 | ## [Version 2.0.1](https://github.com/efremidze/Cluster/releases/tag/2.0.1) 170 | Released on 2017-10-17 171 | 172 | - Added custom cell sizing 173 | - Added annotation center aligning 174 | - Fixed annotations moving when panning 175 | 176 | ## [Version 2.0.0](https://github.com/efremidze/Cluster/releases/tag/2.0.0) 177 | Released on 2017-09-20 178 | 179 | - Updated to Swift 4 180 | 181 | ## [Version 1.0.10](https://github.com/efremidze/Cluster/releases/tag/1.0.10) 182 | Released on 2017-09-11 183 | 184 | - Changed Set to Array 185 | 186 | ## [Version 1.0.9](https://github.com/efremidze/Cluster/releases/tag/1.0.9) 187 | Released on 2017-08-17 188 | 189 | - Added option to retain invisible annotations 190 | 191 | ## [Version 1.0.8](https://github.com/efremidze/Cluster/releases/tag/1.0.8) 192 | Released on 2017-08-09 193 | 194 | - Added multiple manager support 195 | - Fixed overlapping annotations 196 | - Removed multithreading 197 | 198 | ## [Version 1.0.7](https://github.com/efremidze/Cluster/releases/tag/1.0.7) 199 | Released on 2017-05-31 200 | 201 | - Fixed grouping issue 202 | 203 | ## [Version 1.0.6](https://github.com/efremidze/Cluster/releases/tag/1.0.6) 204 | Released on 2017-05-08 205 | 206 | - Bug fixes 207 | 208 | ## [Version 1.0.5](https://github.com/efremidze/Cluster/releases/tag/1.0.5) 209 | Released on 2017-05-08 210 | 211 | - Added zoom level 212 | 213 | ## [Version 1.0.4](https://github.com/efremidze/Cluster/releases/tag/1.0.4) 214 | Released on 2017-05-06 215 | 216 | - Added annotation removal 217 | 218 | ## [Version 1.0.3](https://github.com/efremidze/Cluster/releases/tag/1.0.3) 219 | Released on 2017-04-29 220 | 221 | - Added image support 222 | 223 | ## [Version 1.0.2](https://github.com/efremidze/Cluster/releases/tag/1.0.2) 224 | Released on 2017-04-19 225 | 226 | - Added documentation 227 | 228 | ## [Version 1.0.1](https://github.com/efremidze/Cluster/releases/tag/1.0.1) 229 | Released on 2017-04-18 230 | 231 | - Bug fixes 232 | 233 | ## [Version 1.0.0](https://github.com/efremidze/Cluster/releases/tag/1.0.0) 234 | Released on 2017-04-13 235 | 236 | - Created Project 237 | -------------------------------------------------------------------------------- /Example/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 43 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /docs/Classes/Annotation.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Annotation Class Reference 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 |

Cluster Docs (56% documented)

18 |
19 |
20 |
21 | 26 |
27 |
28 | 90 |
91 |
92 |
93 |

Annotation

94 |
95 |
96 |
open class Annotation : MKPointAnnotation
97 | 98 |
99 |
100 |

Undocumented

101 | 102 |
103 |
104 |
105 |
    106 |
  • 107 |
    108 | 109 | 110 | 111 | style 112 | 113 |
    114 |
    115 |
    116 |
    117 |
    118 |
    119 |

    Undocumented

    120 | 121 |
    122 |
    123 |

    Declaration

    124 |
    125 |

    Swift

    126 |
    open var style: ClusterAnnotationStyle?
    127 | 128 |
    129 |
    130 |
    131 |
    132 |
  • 133 |
  • 134 |
    135 | 136 | 137 | 138 | init(coordinate:) 139 | 140 |
    141 |
    142 |
    143 |
    144 |
    145 |
    146 |

    Undocumented

    147 | 148 |
    149 |
    150 |

    Declaration

    151 |
    152 |

    Swift

    153 |
    public convenience init(coordinate: CLLocationCoordinate2D)
    154 | 155 |
    156 |
    157 |
    158 |
    159 |
  • 160 |
161 |
162 |
163 |
164 | 168 |
169 |
170 | 171 | 172 | 173 | -------------------------------------------------------------------------------- /docs/docsets/Cluster.docset/Contents/Resources/Documents/Classes/Annotation.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Annotation Class Reference 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 |

Cluster Docs (56% documented)

18 |
19 |
20 |
21 | 26 |
27 |
28 | 90 |
91 |
92 |
93 |

Annotation

94 |
95 |
96 |
open class Annotation : MKPointAnnotation
97 | 98 |
99 |
100 |

Undocumented

101 | 102 |
103 |
104 |
105 |
    106 |
  • 107 |
    108 | 109 | 110 | 111 | style 112 | 113 |
    114 |
    115 |
    116 |
    117 |
    118 |
    119 |

    Undocumented

    120 | 121 |
    122 |
    123 |

    Declaration

    124 |
    125 |

    Swift

    126 |
    open var style: ClusterAnnotationStyle?
    127 | 128 |
    129 |
    130 |
    131 |
    132 |
  • 133 |
  • 134 |
    135 | 136 | 137 | 138 | init(coordinate:) 139 | 140 |
    141 |
    142 |
    143 |
    144 |
    145 |
    146 |

    Undocumented

    147 | 148 |
    149 |
    150 |

    Declaration

    151 |
    152 |

    Swift

    153 |
    public convenience init(coordinate: CLLocationCoordinate2D)
    154 | 155 |
    156 |
    157 |
    158 |
    159 |
  • 160 |
161 |
162 |
163 |
164 | 168 |
169 |
170 | 171 | 172 | 173 | -------------------------------------------------------------------------------- /docs/Classes/ClusterAnnotation.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ClusterAnnotation Class Reference 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 |

Cluster Docs (56% documented)

18 |
19 |
20 |
21 | 26 |
27 |
28 | 90 |
91 |
92 |
93 |

ClusterAnnotation

94 |
95 |
96 |
open class ClusterAnnotation : Annotation
97 | 98 |
99 |
100 |

Undocumented

101 | 102 |
103 |
104 |
105 |
    106 |
  • 107 |
    108 | 109 | 110 | 111 | annotations 112 | 113 |
    114 |
    115 |
    116 |
    117 |
    118 |
    119 |

    Undocumented

    120 | 121 |
    122 |
    123 |

    Declaration

    124 |
    125 |

    Swift

    126 |
    open var annotations: [MKAnnotation]
    127 | 128 |
    129 |
    130 |
    131 |
    132 |
  • 133 |
  • 134 |
    135 | 136 | 137 | 138 | isEqual(_:) 139 | 140 |
    141 |
    142 |
    143 |
    144 |
    145 |
    146 |

    Undocumented

    147 | 148 |
    149 |
    150 |

    Declaration

    151 |
    152 |

    Swift

    153 |
    open override func isEqual(_ object: Any?) -> Bool
    154 | 155 |
    156 |
    157 |
    158 |
    159 |
  • 160 |
161 |
162 |
163 |
164 | 168 |
169 |
170 | 171 | 172 | 173 | -------------------------------------------------------------------------------- /docs/css/jazzy.css: -------------------------------------------------------------------------------- 1 | html, body, div, span, h1, h3, h4, p, a, code, em, img, ul, li, table, tbody, tr, td { 2 | background: transparent; 3 | border: 0; 4 | margin: 0; 5 | outline: 0; 6 | padding: 0; 7 | vertical-align: baseline; } 8 | 9 | body { 10 | background-color: #f2f2f2; 11 | font-family: Helvetica, freesans, Arial, sans-serif; 12 | font-size: 14px; 13 | -webkit-font-smoothing: subpixel-antialiased; 14 | word-wrap: break-word; } 15 | 16 | h1, h2, h3 { 17 | margin-top: 0.8em; 18 | margin-bottom: 0.3em; 19 | font-weight: 100; 20 | color: black; } 21 | 22 | h1 { 23 | font-size: 2.5em; } 24 | 25 | h2 { 26 | font-size: 2em; 27 | border-bottom: 1px solid #e2e2e2; } 28 | 29 | h4 { 30 | font-size: 13px; 31 | line-height: 1.5; 32 | margin-top: 21px; } 33 | 34 | h5 { 35 | font-size: 1.1em; } 36 | 37 | h6 { 38 | font-size: 1.1em; 39 | color: #777; } 40 | 41 | .section-name { 42 | color: gray; 43 | display: block; 44 | font-family: Helvetica; 45 | font-size: 22px; 46 | font-weight: 100; 47 | margin-bottom: 15px; } 48 | 49 | pre, code { 50 | font: 0.95em Menlo, monospace; 51 | color: #777; 52 | word-wrap: normal; } 53 | 54 | p code, li code { 55 | background-color: #eee; 56 | padding: 2px 4px; 57 | border-radius: 4px; } 58 | 59 | a { 60 | color: #0088cc; 61 | text-decoration: none; } 62 | 63 | ul { 64 | padding-left: 15px; } 65 | 66 | li { 67 | line-height: 1.8em; } 68 | 69 | img { 70 | max-width: 100%; } 71 | 72 | blockquote { 73 | margin-left: 0; 74 | padding: 0 10px; 75 | border-left: 4px solid #ccc; } 76 | 77 | .content-wrapper { 78 | margin: 0 auto; 79 | width: 980px; } 80 | 81 | header { 82 | font-size: 0.85em; 83 | line-height: 26px; 84 | background-color: #414141; 85 | position: fixed; 86 | width: 100%; 87 | z-index: 1; } 88 | header img { 89 | padding-right: 6px; 90 | vertical-align: -4px; 91 | height: 16px; } 92 | header a { 93 | color: #fff; } 94 | header p { 95 | float: left; 96 | color: #999; } 97 | header .header-right { 98 | float: right; 99 | margin-left: 16px; } 100 | 101 | #breadcrumbs { 102 | background-color: #f2f2f2; 103 | height: 27px; 104 | padding-top: 17px; 105 | position: fixed; 106 | width: 100%; 107 | z-index: 1; 108 | margin-top: 26px; } 109 | #breadcrumbs #carat { 110 | height: 10px; 111 | margin: 0 5px; } 112 | 113 | .sidebar { 114 | background-color: #f9f9f9; 115 | border: 1px solid #e2e2e2; 116 | overflow-y: auto; 117 | overflow-x: hidden; 118 | position: fixed; 119 | top: 70px; 120 | bottom: 0; 121 | width: 230px; 122 | word-wrap: normal; } 123 | 124 | .nav-groups { 125 | list-style-type: none; 126 | background: #fff; 127 | padding-left: 0; } 128 | 129 | .nav-group-name { 130 | border-bottom: 1px solid #e2e2e2; 131 | font-size: 1.1em; 132 | font-weight: 100; 133 | padding: 15px 0 15px 20px; } 134 | .nav-group-name > a { 135 | color: #333; } 136 | 137 | .nav-group-tasks { 138 | margin-top: 5px; } 139 | 140 | .nav-group-task { 141 | font-size: 0.9em; 142 | list-style-type: none; 143 | white-space: nowrap; } 144 | .nav-group-task a { 145 | color: #888; } 146 | 147 | .main-content { 148 | background-color: #fff; 149 | border: 1px solid #e2e2e2; 150 | margin-left: 246px; 151 | position: absolute; 152 | overflow: hidden; 153 | padding-bottom: 60px; 154 | top: 70px; 155 | width: 734px; } 156 | .main-content p, .main-content a, .main-content code, .main-content em, .main-content ul, .main-content table, .main-content blockquote { 157 | margin-bottom: 1em; } 158 | .main-content p { 159 | line-height: 1.8em; } 160 | .main-content section .section:first-child { 161 | margin-top: 0; 162 | padding-top: 0; } 163 | .main-content section .task-group-section .task-group:first-of-type { 164 | padding-top: 10px; } 165 | .main-content section .task-group-section .task-group:first-of-type .section-name { 166 | padding-top: 15px; } 167 | .main-content section .heading:before { 168 | content: ""; 169 | display: block; 170 | padding-top: 70px; 171 | margin: -70px 0 0; } 172 | 173 | .section { 174 | padding: 0 25px; } 175 | 176 | .highlight { 177 | background-color: #eee; 178 | padding: 10px 12px; 179 | border: 1px solid #e2e2e2; 180 | border-radius: 4px; 181 | overflow-x: auto; } 182 | 183 | .declaration .highlight { 184 | overflow-x: initial; 185 | padding: 0 40px 40px 0; 186 | margin-bottom: -25px; 187 | background-color: transparent; 188 | border: none; } 189 | 190 | .section-name { 191 | margin: 0; 192 | margin-left: 18px; } 193 | 194 | .task-group-section { 195 | padding-left: 6px; 196 | border-top: 1px solid #e2e2e2; } 197 | 198 | .task-group { 199 | padding-top: 0px; } 200 | 201 | .task-name-container a[name]:before { 202 | content: ""; 203 | display: block; 204 | padding-top: 70px; 205 | margin: -70px 0 0; } 206 | 207 | .item { 208 | padding-top: 8px; 209 | width: 100%; 210 | list-style-type: none; } 211 | .item a[name]:before { 212 | content: ""; 213 | display: block; 214 | padding-top: 70px; 215 | margin: -70px 0 0; } 216 | .item code { 217 | background-color: transparent; 218 | padding: 0; } 219 | .item .token { 220 | padding-left: 3px; 221 | margin-left: 15px; 222 | font-size: 11.9px; } 223 | .item .declaration-note { 224 | font-size: .85em; 225 | color: gray; 226 | font-style: italic; } 227 | 228 | .pointer-container { 229 | border-bottom: 1px solid #e2e2e2; 230 | left: -23px; 231 | padding-bottom: 13px; 232 | position: relative; 233 | width: 110%; } 234 | 235 | .pointer { 236 | background: #f9f9f9; 237 | border-left: 1px solid #e2e2e2; 238 | border-top: 1px solid #e2e2e2; 239 | height: 12px; 240 | left: 21px; 241 | top: -7px; 242 | -webkit-transform: rotate(45deg); 243 | -moz-transform: rotate(45deg); 244 | -o-transform: rotate(45deg); 245 | transform: rotate(45deg); 246 | position: absolute; 247 | width: 12px; } 248 | 249 | .height-container { 250 | display: none; 251 | left: -25px; 252 | padding: 0 25px; 253 | position: relative; 254 | width: 100%; 255 | overflow: hidden; } 256 | .height-container .section { 257 | background: #f9f9f9; 258 | border-bottom: 1px solid #e2e2e2; 259 | left: -25px; 260 | position: relative; 261 | width: 100%; 262 | padding-top: 10px; 263 | padding-bottom: 5px; } 264 | 265 | .aside, .language { 266 | padding: 6px 12px; 267 | margin: 12px 0; 268 | border-left: 5px solid #dddddd; 269 | overflow-y: hidden; } 270 | .aside .aside-title, .language .aside-title { 271 | font-size: 9px; 272 | letter-spacing: 2px; 273 | text-transform: uppercase; 274 | padding-bottom: 0; 275 | margin: 0; 276 | color: #aaa; 277 | -webkit-user-select: none; } 278 | .aside p:last-child, .language p:last-child { 279 | margin-bottom: 0; } 280 | 281 | .language { 282 | border-left: 5px solid #cde9f4; } 283 | .language .aside-title { 284 | color: #4b8afb; } 285 | 286 | .aside-warning { 287 | border-left: 5px solid #ff6666; } 288 | .aside-warning .aside-title { 289 | color: #ff0000; } 290 | 291 | .graybox { 292 | border-collapse: collapse; 293 | width: 100%; } 294 | .graybox p { 295 | margin: 0; 296 | word-break: break-word; 297 | min-width: 50px; } 298 | .graybox td { 299 | border: 1px solid #e2e2e2; 300 | padding: 5px 25px 5px 10px; 301 | vertical-align: middle; } 302 | .graybox tr td:first-of-type { 303 | text-align: right; 304 | padding: 7px; 305 | vertical-align: top; 306 | word-break: normal; 307 | width: 40px; } 308 | 309 | .slightly-smaller { 310 | font-size: 0.9em; } 311 | 312 | #footer { 313 | position: absolute; 314 | bottom: 10px; 315 | margin-left: 25px; } 316 | #footer p { 317 | margin: 0; 318 | color: #aaa; 319 | font-size: 0.8em; } 320 | 321 | html.dash header, html.dash #breadcrumbs, html.dash .sidebar { 322 | display: none; } 323 | html.dash .main-content { 324 | width: 980px; 325 | margin-left: 0; 326 | border: none; 327 | width: 100%; 328 | top: 0; 329 | padding-bottom: 0; } 330 | html.dash .height-container { 331 | display: block; } 332 | html.dash .item .token { 333 | margin-left: 0; } 334 | html.dash .content-wrapper { 335 | width: auto; } 336 | html.dash #footer { 337 | position: static; } 338 | --------------------------------------------------------------------------------