├── gemfile ├── Resources └── bento_animation3.gif ├── BentoMapExample ├── Assets │ ├── Assets.xcassets │ │ ├── Contents.json │ │ ├── redHexagon.imageset │ │ │ ├── redHexagon.png │ │ │ ├── redHexagon@2x.png │ │ │ ├── redHexagon@3x.png │ │ │ └── Contents.json │ │ ├── blueStretch.imageset │ │ │ ├── blueStretch.png │ │ │ ├── blueStretch@2x.png │ │ │ ├── blueStretch@3x.png │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── Info.plist │ └── Base.lproj │ │ └── LaunchScreen.storyboard ├── App Delegate │ └── AppDelegate.swift ├── App │ ├── CoreGraphicsViewController.swift │ ├── MainMenuTableViewController.swift │ ├── QuadTree+SampleData.swift │ └── MapKitViewController.swift └── Annotations │ ├── Annotations.swift │ └── AnnotationView.swift ├── .swiftlint.yml ├── BentoMap.xcodeproj ├── project.xcworkspace │ └── contents.xcworkspacedata ├── xcshareddata │ └── xcschemes │ │ └── BentoMap.xcscheme └── project.pbxproj ├── BentoMap ├── Box.swift ├── Resources │ ├── BentoMap.h │ └── Info.plist ├── Extensions │ ├── CGPoint+BentoCoordinate.swift │ ├── MKMapPoint+BentoCoordinate.swift │ ├── CLLocationCoordinate2D+BentoCoordinate.swift │ ├── CGRect+BentoRect.swift │ ├── CollectionTypeExtensions.swift │ └── MKMapRect+BentoRect.swift ├── OptionalOr.swift ├── QuadrantWrapper.swift ├── OrdinalNodes.swift ├── QuadTreeResult.swift ├── QuadTreeNode.swift ├── BentoBox.swift └── QuadTree.swift ├── BentoMapTests ├── Resources │ └── Info.plist ├── OptionalOrTests.swift ├── QuadTreeTests.swift └── BentoBoxTests.swift ├── BentoMap.podspec ├── LICENSE ├── .travis.yml ├── .gitignore ├── Gemfile.lock ├── CONTRIBUTING.md └── README.md /gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem 'cocoapods', '~> 1.1' 4 | -------------------------------------------------------------------------------- /Resources/bento_animation3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rightpoint/BentoMap/HEAD/Resources/bento_animation3.gif -------------------------------------------------------------------------------- /BentoMapExample/Assets/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /BentoMapExample/Assets/Assets.xcassets/redHexagon.imageset/redHexagon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rightpoint/BentoMap/HEAD/BentoMapExample/Assets/Assets.xcassets/redHexagon.imageset/redHexagon.png -------------------------------------------------------------------------------- /BentoMapExample/Assets/Assets.xcassets/blueStretch.imageset/blueStretch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rightpoint/BentoMap/HEAD/BentoMapExample/Assets/Assets.xcassets/blueStretch.imageset/blueStretch.png -------------------------------------------------------------------------------- /BentoMapExample/Assets/Assets.xcassets/redHexagon.imageset/redHexagon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rightpoint/BentoMap/HEAD/BentoMapExample/Assets/Assets.xcassets/redHexagon.imageset/redHexagon@2x.png -------------------------------------------------------------------------------- /BentoMapExample/Assets/Assets.xcassets/redHexagon.imageset/redHexagon@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rightpoint/BentoMap/HEAD/BentoMapExample/Assets/Assets.xcassets/redHexagon.imageset/redHexagon@3x.png -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | statement_position: 2 | statement_mode: uncuddled_else 3 | 4 | trailing_comma: 5 | mandatory_comma: true 6 | 7 | disabled_rules: 8 | - line_length 9 | - variable_name 10 | 11 | -------------------------------------------------------------------------------- /BentoMapExample/Assets/Assets.xcassets/blueStretch.imageset/blueStretch@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rightpoint/BentoMap/HEAD/BentoMapExample/Assets/Assets.xcassets/blueStretch.imageset/blueStretch@2x.png -------------------------------------------------------------------------------- /BentoMapExample/Assets/Assets.xcassets/blueStretch.imageset/blueStretch@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rightpoint/BentoMap/HEAD/BentoMapExample/Assets/Assets.xcassets/blueStretch.imageset/blueStretch@3x.png -------------------------------------------------------------------------------- /BentoMap.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /BentoMap/Box.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Box.swift 3 | // BentoMap 4 | // 5 | // Created by Michael Skiba on 2/17/16. 6 | // Copyright © 2016 Raizlabs. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// A workaround because Swift structs do not 12 | /// allow recursive value types 13 | class Box { 14 | 15 | var value: T 16 | 17 | init(value: T) { 18 | self.value = value 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /BentoMapExample/Assets/Assets.xcassets/redHexagon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "redHexagon.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "redHexagon@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "redHexagon@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /BentoMap/Resources/BentoMap.h: -------------------------------------------------------------------------------- 1 | // 2 | // BentoMap.h 3 | // BentoMap 4 | // 5 | // Created by Michael Skiba on 7/6/16. 6 | // Copyright © 2016 Raizlabs. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for BentoBox. 12 | FOUNDATION_EXPORT double BentoMapVersionNumber; 13 | 14 | //! Project version string for BentoBox. 15 | FOUNDATION_EXPORT const unsigned char BentoMapVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /BentoMap/Extensions/CGPoint+BentoCoordinate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGPoint+BentoCoordinate.swift 3 | // BentoMap 4 | // 5 | // Created by Matthew Buckley on 8/27/16. 6 | // Copyright © 2016 Raizlabs. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension CGPoint: BentoCoordinate { 12 | 13 | public var coordX: CGFloat { 14 | get { return x } 15 | set { x = newValue } 16 | } 17 | 18 | public var coordY: CGFloat { 19 | get { return y } 20 | set { y = newValue } 21 | } 22 | 23 | public init(coordX: CGFloat, coordY: CGFloat) { 24 | self.init(x: coordX, y: coordY) 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /BentoMap/OptionalOr.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OptionalOr.swift 3 | // BentoMap 4 | // 5 | // Created by Michael Skiba on 7/7/16. 6 | // Copyright © 2016 Raizlabs. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | infix operator ||?: LogicalDisjunctionPrecedence 12 | 13 | func ||? (lhs: Bool?, rhs: Bool?) -> Bool { 14 | return (lhs ?? false) || (rhs ?? false) 15 | } 16 | 17 | func ||? (lhs: Bool, rhs: Bool?) -> Bool { 18 | return lhs || (rhs ?? false) 19 | } 20 | 21 | func ||? (lhs: Bool?, rhs: Bool) -> Bool { 22 | return (lhs ?? false) || rhs 23 | } 24 | 25 | func ||? (lhs: Bool, rhs: Bool) -> Bool { 26 | return lhs || rhs 27 | } 28 | -------------------------------------------------------------------------------- /BentoMap/Extensions/MKMapPoint+BentoCoordinate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MKMapPoint+BentoCoordinate.swift 3 | // BentoMap 4 | // 5 | // Created by Matthew Buckley on 8/27/16. 6 | // Copyright © 2016 Raizlabs. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import MapKit 11 | 12 | extension MKMapPoint: BentoCoordinate { 13 | 14 | public var coordX: CGFloat { 15 | get { return CGFloat(x) } 16 | set { x = Double(newValue) } 17 | } 18 | 19 | public var coordY: CGFloat { 20 | get { return CGFloat(y) } 21 | set { y = Double(newValue) } 22 | } 23 | 24 | public init(coordX: CGFloat, coordY: CGFloat) { 25 | self.init(x: Double(coordX), y: Double(coordY)) 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /BentoMapExample/App Delegate/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // BentoMapExample 4 | // 5 | // Created by Michael Skiba on 7/6/16. 6 | // Copyright © 2016 Raizlabs. 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: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 17 | 18 | window = UIWindow(frame: UIScreen.main.bounds) 19 | 20 | window?.rootViewController = UINavigationController.init(rootViewController: MainMenuTableViewController()) 21 | window?.makeKeyAndVisible() 22 | return true 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /BentoMapTests/Resources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /BentoMap/Extensions/CLLocationCoordinate2D+BentoCoordinate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CLLocationCoordinate2D+BentoCoordinate.swift 3 | // BentoMap 4 | // 5 | // Created by Matthew Buckley on 8/27/16. 6 | // Copyright © 2016 Raizlabs. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CoreLocation 11 | 12 | extension CLLocationCoordinate2D: BentoCoordinate { 13 | 14 | public var coordX: CGFloat { 15 | get { return CGFloat(latitude) } 16 | set { latitude = CLLocationDegrees(newValue) } 17 | } 18 | 19 | public var coordY: CGFloat { 20 | get { return CGFloat(longitude) } 21 | set { longitude = CLLocationDegrees(newValue) } 22 | } 23 | 24 | public init(coordX: CGFloat, coordY: CGFloat) { 25 | self.init(latitude: CLLocationDegrees(coordX), longitude: CLLocationDegrees(coordY)) 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /BentoMap/Resources/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 | 0.3.3 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /BentoMap.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'BentoMap' 3 | s.version = '0.3.3' 4 | s.summary = 'Map Clustering for Swift.' 5 | 6 | s.description = <<-DESC 7 | A library to handle storing map annotations in a Quadtrees, and for returning 8 | clusters of annotations to allow easy grouping of map items based on proximity. 9 | DESC 10 | 11 | s.homepage = 'https://github.com/Raizlabs/BentoMap' 12 | s.license = { :type => 'MIT', :file => 'LICENSE' } 13 | s.author = { 'Michael Skiba' => 'mike.skiba@raizlabs.com', 'Rob Visentin' => 'rob.visentin@raizlabs.com', 'Matt Buckley' => 'matt.buckley@raizlabs.com' } 14 | s.source = { :git => 'https://github.com/Raizlabs/BentoMap.git', :tag => s.version.to_s } 15 | s.social_media_url = 'https://twitter.com/atelierclkwrk' 16 | 17 | s.ios.deployment_target = '8.0' 18 | 19 | s.source_files = 'BentoMap/**/*' 20 | 21 | s.frameworks = 'Foundation', 'MapKit' 22 | 23 | end 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Raizlabs 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 | -------------------------------------------------------------------------------- /BentoMap/Extensions/CGRect+BentoRect.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGRect+BentoRect.swift 3 | // BentoMap 4 | // 5 | // Created by Matthew Buckley on 8/27/16. 6 | // Copyright © 2016 Raizlabs. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension CGRect: BentoRect { 12 | 13 | public func containsCoordinate(_ c: BentoCoordinate) -> Bool { 14 | let point = CGPoint(x: c.coordX, y: c.coordY) 15 | return self.contains(point) 16 | } 17 | 18 | public func divide(_ percent: CGFloat, edge: CGRectEdge) -> (CGRect, CGRect) { 19 | let amount: CGFloat 20 | switch edge { 21 | case .maxXEdge, .minXEdge: 22 | amount = size.width / 2.0 23 | case .maxYEdge, .minYEdge: 24 | amount = size.height / 2.0 25 | } 26 | 27 | let (slice, remainder) = divided(atDistance: amount, from: edge) 28 | return (slice: slice, remainder: remainder) 29 | } 30 | 31 | public func unionWith(_ other: CGRect) -> CGRect { 32 | return union(other) 33 | } 34 | 35 | public init(originCoordinate origin: BentoCoordinate, size: CGSize) { 36 | self.init(origin: CGPoint(x: origin.coordX, y: origin.coordY), size: size) 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /BentoMap/QuadrantWrapper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QuadrantWrapper.swift 3 | // BentoMap 4 | // 5 | // Created by Michael Skiba on 7/5/16. 6 | // Copyright © 2016 Raizlabs. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct QuadrantWrapper { 12 | 13 | /// top left node 14 | var northWest: Quadrant 15 | 16 | /// top right node 17 | var northEast: Quadrant 18 | 19 | /// bottom left node 20 | var southWest: Quadrant 21 | 22 | /// bottom right node 23 | var southEast: Quadrant 24 | 25 | /** 26 | Applies a closure to each quadrant. 27 | 28 | - parameter converter: a closure that takes a Quadrant type 29 | and returns another type specified in the function signature. 30 | 31 | - returns: a new QuadrantWrapper containing a new type. 32 | */ 33 | func map(_ converter: (Quadrant) -> NewQuadrant) -> QuadrantWrapper { 34 | return QuadrantWrapper(northWest: converter(northWest), 35 | northEast: converter(northEast), 36 | southWest: converter(southWest), 37 | southEast: converter(southEast)) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | env: 2 | global: 3 | - LC_CTYPE=en_US.UTF-8 4 | 5 | branches: 6 | only: 7 | - develop 8 | - master 9 | 10 | os: osx 11 | language: objective-c 12 | matrix: 13 | exclude: 14 | - os: osx 15 | include: 16 | - osx_image: xcode8 17 | env: 18 | - XCODE_ACTION="clean test" 19 | XCODE_TOOLCHAIN="swift" 20 | XCODE_DESTINATION="platform=iOS Simulator,id=237D7619-4A4E-4624-8925-67A42A8A690A" 21 | - osx_image: xcode8 22 | env: 23 | - JOB=CARTHAGE 24 | script: 25 | - brew update 1> /dev/null 2> /dev/null 26 | - brew outdated carthage || brew upgrade carthage 27 | - carthage build --no-skip-current 28 | - test -d Carthage/Build/iOS/BentoMap.framework || exit 1 29 | - osx_image: xcode8 30 | env: 31 | - JOB=COCOAPODS 32 | script: 33 | - bundle exec pod lib lint --sources="https://github.com/Raizlabs/BentoMap" 34 | 35 | before_script: 36 | - killall "Simulator" || echo "No matching processes belonging to you were found" 37 | 38 | script: 39 | - travis_retry xcodebuild ${XCODE_ACTION} 40 | -toolchain ${XCODE_TOOLCHAIN} 41 | -project BentoMap.xcodeproj 42 | -scheme BentoMap 43 | -destination "${XCODE_DESTINATION}" 44 | -------------------------------------------------------------------------------- /BentoMapExample/Assets/Assets.xcassets/blueStretch.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "resizing" : { 5 | "mode" : "3-part-horizontal", 6 | "center" : { 7 | "mode" : "stretch", 8 | "width" : 12 9 | }, 10 | "cap-insets" : { 11 | "right" : 10, 12 | "left" : 10 13 | } 14 | }, 15 | "idiom" : "universal", 16 | "filename" : "blueStretch.png", 17 | "scale" : "1x" 18 | }, 19 | { 20 | "resizing" : { 21 | "mode" : "3-part-horizontal", 22 | "center" : { 23 | "mode" : "stretch", 24 | "width" : 18 25 | }, 26 | "cap-insets" : { 27 | "right" : 23, 28 | "left" : 23 29 | } 30 | }, 31 | "idiom" : "universal", 32 | "filename" : "blueStretch@2x.png", 33 | "scale" : "2x" 34 | }, 35 | { 36 | "resizing" : { 37 | "mode" : "3-part-horizontal", 38 | "center" : { 39 | "mode" : "stretch", 40 | "width" : 26 41 | }, 42 | "cap-insets" : { 43 | "right" : 35, 44 | "left" : 35 45 | } 46 | }, 47 | "idiom" : "universal", 48 | "filename" : "blueStretch@3x.png", 49 | "scale" : "3x" 50 | } 51 | ], 52 | "info" : { 53 | "version" : 1, 54 | "author" : "xcode" 55 | } 56 | } -------------------------------------------------------------------------------- /BentoMapExample/Assets/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 | 0.3.3 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /BentoMapExample/Assets/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 | -------------------------------------------------------------------------------- /BentoMapExample/App/CoreGraphicsViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CoreGraphicsViewController.swift 3 | // BentoMap 4 | // 5 | // Created by Matthew Buckley on 9/5/16. 6 | // Copyright © 2016 Raizlabs. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import BentoMap 11 | 12 | class CoreGraphicsViewController: UIViewController { 13 | 14 | override func viewWillLayoutSubviews() { 15 | super.viewWillLayoutSubviews() 16 | let gridData = QuadTree.sampleGridData(forContainerRect: view.frame) 17 | 18 | let map: BentoBox = BentoBox(root: view.frame) 19 | let mapView = UIView(frame: map.root) 20 | view.addSubview(mapView) 21 | mapView.backgroundColor = .white 22 | 23 | let clusterResults = gridData.clusteredDataWithinMapRect(map.root, 24 | zoomScale: 1.0, 25 | cellSize: 64) 26 | 27 | for result in clusterResults { 28 | switch result { 29 | case .single(let node): 30 | let node = UIView(frame: CGRect(origin: node.originCoordinate, size: CGSize(width: 5.0, height: 5.0))) 31 | node.backgroundColor = .blue 32 | node.layer.cornerRadius = 2.5 33 | view.addSubview(node) 34 | case .multiple(let nodes): 35 | for node in nodes { 36 | let node = UIView(frame: CGRect(origin: node.originCoordinate, size: CGSize(width: 5.0, height: 5.0))) 37 | node.layer.cornerRadius = 2.5 38 | view.addSubview(node) 39 | node.backgroundColor = .blue 40 | } 41 | } 42 | } 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /BentoMapExample/App/MainMenuTableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainMenuTableViewController.swift 3 | // BentoMap 4 | // 5 | // Created by Matthew Buckley on 9/5/16. 6 | // Copyright © 2016 Raizlabs. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class MainMenuTableViewController: UITableViewController { 12 | 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | 16 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: "reuseIdentifier") 17 | tableView.separatorStyle = .none 18 | } 19 | 20 | // MARK: - Table view data source 21 | 22 | override func numberOfSections(in tableView: UITableView) -> Int { 23 | return 1 24 | } 25 | 26 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 27 | return 2 28 | } 29 | 30 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 31 | guard let cell = tableView.dequeueReusableCell(withIdentifier: "reuseIdentifier") else { 32 | return UITableViewCell() 33 | } 34 | if indexPath.row == 0 { 35 | cell.textLabel?.text = "MapKit Example" 36 | } 37 | else if indexPath.row == 1 { 38 | cell.textLabel?.text = "CoreGraphics Example" 39 | } 40 | cell.accessoryType = .disclosureIndicator 41 | return cell 42 | } 43 | 44 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 45 | if indexPath.row == 0 { 46 | navigationController?.pushViewController(MapKitViewController(), animated: true) 47 | } 48 | else if indexPath.row == 1 { 49 | navigationController?.pushViewController(CoreGraphicsViewController(), animated: true) 50 | } 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /BentoMapTests/OptionalOrTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OptionalOrTests.swift 3 | // BentoMap 4 | // 5 | // Created by Michael Skiba on 7/7/16. 6 | // Copyright © 2016 Raizlabs. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import BentoMap 11 | 12 | class OptionalOrTests: XCTestCase { 13 | func testOptionalOr() { 14 | let truePairs: [(Bool?, Bool?)] = [ 15 | (true, true), 16 | (true, false), 17 | (false, true), 18 | (true, nil), 19 | (nil, true), 20 | ] 21 | for (lhs, rhs) in truePairs { 22 | XCTAssert(lhs ||? rhs, "Should be true") 23 | if let unwrappedLeft = lhs { 24 | XCTAssert(unwrappedLeft ||? rhs, "Should be true") 25 | } 26 | if let unwrappedRight = rhs { 27 | XCTAssert(lhs ||? unwrappedRight, "Should be true") 28 | } 29 | if let unwrappedLeft = lhs, let unwrappedRight = rhs { 30 | XCTAssert(unwrappedLeft ||? unwrappedRight, "Should be true") 31 | } 32 | } 33 | 34 | let falsePairs: [(Bool?, Bool?)] = [ 35 | (false, false), 36 | (nil, false), 37 | (false, nil), 38 | (nil, nil), 39 | ] 40 | for (lhs, rhs) in falsePairs { 41 | XCTAssertFalse(lhs ||? rhs, "Should be false") 42 | if let unwrappedLeft = lhs { 43 | XCTAssertFalse(unwrappedLeft ||? rhs, "Should be false") 44 | } 45 | if let unwrappedRight = rhs { 46 | XCTAssertFalse(lhs ||? unwrappedRight, "Should be false") 47 | } 48 | if let unwrappedLeft = lhs, let unwrappedRight = rhs { 49 | XCTAssertFalse(unwrappedLeft ||? unwrappedRight, "Should be false") 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /BentoMap/Extensions/CollectionTypeExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionTypeExtensions.swift 3 | // BentoMap 4 | // 5 | // Created by Rob Visentin on 8/1/16. 6 | // Copyright © 2016 Raizlabs. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import MapKit 11 | 12 | public extension Collection where Iterator.Element: BentoCoordinate { 13 | 14 | func bentoBox() -> BentoBox { 15 | 16 | let coordinates: [BentoCoordinate] = flatMap({return $0 as BentoCoordinate}) 17 | 18 | return BentoBox(root: bb(coordinates, rectType: Rect.self)) 19 | } 20 | 21 | } 22 | 23 | public extension Collection { 24 | 25 | func bb(_ coords: [BentoCoordinate], rectType: Rect.Type) -> Rect { 26 | var min: CGPoint = CGPoint(x: CGFloat.greatestFiniteMagnitude, y: CGFloat.greatestFiniteMagnitude) 27 | var max: CGPoint = CGPoint.zero 28 | 29 | for point in coords { 30 | 31 | if point.coordX < min.coordX { 32 | min.coordX = point.coordX 33 | } 34 | if point.coordX > max.coordX { 35 | max.coordX = point.coordX 36 | } 37 | if point.coordY < min.coordY { 38 | min.coordY = point.coordY 39 | } 40 | if point.coordY > max.coordY { 41 | max.coordY = point.coordY 42 | } 43 | } 44 | 45 | return Rect(originCoordinate: min, size: CGSize(width: max.x - min.x, height: max.y - min.y)) 46 | } 47 | } 48 | 49 | public extension Collection where Iterator.Element: CoordinateProvider { 50 | 51 | func boundingBox() -> BentoBox { 52 | let boundingBox: [BentoCoordinate] = map({ $0.coordinate }) 53 | 54 | return BentoBox(root: bb(boundingBox, rectType: Rect.self)) 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /BentoMapExample/Assets/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 | "info" : { 90 | "version" : 1, 91 | "author" : "xcode" 92 | } 93 | } -------------------------------------------------------------------------------- /BentoMap/Extensions/MKMapRect+BentoRect.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MKMapRect+BentoRect.swift 3 | // BentoMap 4 | // 5 | // Created by Matthew Buckley on 8/27/16. 6 | // Copyright © 2016 Raizlabs. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import MapKit 11 | 12 | extension MKMapRect: BentoRect { 13 | 14 | public var minX: CGFloat { 15 | return CGFloat(MKMapRectGetMinX(self)) 16 | } 17 | 18 | public var minY: CGFloat { 19 | return CGFloat(MKMapRectGetMinY(self)) 20 | } 21 | 22 | public var maxX: CGFloat { 23 | return CGFloat(MKMapRectGetMaxX(self)) 24 | } 25 | 26 | public var maxY: CGFloat { 27 | return CGFloat(MKMapRectGetMaxY(self)) 28 | } 29 | 30 | public func containsCoordinate(_ c: BentoCoordinate) -> Bool { 31 | let originCoordinate = MKMapPoint(x: Double(c.coordX), y: Double(c.coordY)) 32 | return MKMapRectContainsPoint(self, originCoordinate) 33 | } 34 | 35 | public func divide(_ percent: CGFloat, edge: CGRectEdge) -> (MKMapRect, MKMapRect) { 36 | let amount: Double 37 | switch edge { 38 | case .maxXEdge, .minXEdge: 39 | amount = size.width / 2.0 40 | case .maxYEdge, .minYEdge: 41 | amount = size.height / 2.0 42 | } 43 | 44 | let slice = UnsafeMutablePointer.allocate(capacity: 1) 45 | defer { 46 | slice.deinitialize() 47 | } 48 | let remainder = UnsafeMutablePointer.allocate(capacity: 1) 49 | defer { 50 | remainder.deinitialize() 51 | } 52 | MKMapRectDivide(self, slice, remainder, amount, edge) 53 | return (slice: slice[0], remainder: remainder[0]) 54 | } 55 | 56 | public func unionWith(_ other: MKMapRect) -> MKMapRect { 57 | return MKMapRectUnion(self, other) 58 | } 59 | 60 | public init(originCoordinate origin: BentoCoordinate, size: CGSize) { 61 | self.init(origin: MKMapPoint(x: Double(origin.coordX), y: Double(origin.coordY)), size: MKMapSize(width: Double(size.width), height: Double(size.height))) 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (2.3.5) 5 | activesupport (4.2.7.1) 6 | i18n (~> 0.7) 7 | json (~> 1.7, >= 1.7.7) 8 | minitest (~> 5.1) 9 | thread_safe (~> 0.3, >= 0.3.4) 10 | tzinfo (~> 1.1) 11 | claide (1.0.1) 12 | cocoapods (1.2.0) 13 | activesupport (>= 4.0.2, < 5) 14 | claide (>= 1.0.1, < 2.0) 15 | cocoapods-core (= 1.2.0) 16 | cocoapods-deintegrate (>= 1.0.1, < 2.0) 17 | cocoapods-downloader (>= 1.1.3, < 2.0) 18 | cocoapods-plugins (>= 1.0.0, < 2.0) 19 | cocoapods-search (>= 1.0.0, < 2.0) 20 | cocoapods-stats (>= 1.0.0, < 2.0) 21 | cocoapods-trunk (>= 1.1.2, < 2.0) 22 | cocoapods-try (>= 1.1.0, < 2.0) 23 | colored (~> 1.2) 24 | escape (~> 0.0.4) 25 | fourflusher (~> 2.0.1) 26 | gh_inspector (~> 1.0) 27 | molinillo (~> 0.5.5) 28 | nap (~> 1.0) 29 | ruby-macho (~> 0.2.5) 30 | xcodeproj (>= 1.4.1, < 2.0) 31 | cocoapods-core (1.2.0) 32 | activesupport (>= 4.0.2, < 5) 33 | fuzzy_match (~> 2.0.4) 34 | nap (~> 1.0) 35 | cocoapods-deintegrate (1.0.1) 36 | cocoapods-downloader (1.1.3) 37 | cocoapods-plugins (1.0.0) 38 | nap 39 | cocoapods-search (1.0.0) 40 | cocoapods-stats (1.0.0) 41 | cocoapods-trunk (1.1.2) 42 | nap (>= 0.8, < 2.0) 43 | netrc (= 0.7.8) 44 | cocoapods-try (1.1.0) 45 | colored (1.2) 46 | escape (0.0.4) 47 | fourflusher (2.0.1) 48 | fuzzy_match (2.0.4) 49 | gh_inspector (1.0.3) 50 | i18n (0.8.0) 51 | json (1.8.6) 52 | minitest (5.10.1) 53 | molinillo (0.5.6) 54 | nanaimo (0.2.3) 55 | nap (1.1.0) 56 | netrc (0.7.8) 57 | ruby-macho (0.2.6) 58 | thread_safe (0.3.5) 59 | tzinfo (1.2.2) 60 | thread_safe (~> 0.1) 61 | xcodeproj (1.4.2) 62 | CFPropertyList (~> 2.3.3) 63 | activesupport (>= 3) 64 | claide (>= 1.0.1, < 2.0) 65 | colored (~> 1.2) 66 | nanaimo (~> 0.2.3) 67 | 68 | PLATFORMS 69 | ruby 70 | 71 | DEPENDENCIES 72 | cocoapods (~> 1.1) 73 | 74 | BUNDLED WITH 75 | 1.14.4 76 | -------------------------------------------------------------------------------- /BentoMap/OrdinalNodes.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OrdinalNodes.swift 3 | // BentoMap 4 | // 5 | // Created by Michael Skiba on 2/17/16. 6 | // Copyright © 2016 Raizlabs. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct OrdinalNodes { 12 | 13 | /// A type that exposes all 4 quadrants of a given QuadTree 14 | fileprivate typealias QuadTreeWrapper = QuadrantWrapper> 15 | 16 | // Recursive structs require a workaround class type 17 | // because it is impossible at runtime to calculate 18 | // how much memory must be allocated 19 | fileprivate var quadrants: Box 20 | 21 | /// The NorthWest quadrant of the QuadTree's root. 22 | var northWest: QuadTree { 23 | get { 24 | return quadrants.value.northWest 25 | } 26 | set { 27 | quadrants.value.northWest = newValue 28 | } 29 | } 30 | 31 | /// The NorthEast quadrant of the QuadTree's root. 32 | var northEast: QuadTree { 33 | get { 34 | return quadrants.value.northEast 35 | } 36 | set { 37 | quadrants.value.northEast = newValue 38 | } 39 | } 40 | 41 | /// The SouthWest quadrant of the QuadTree's root. 42 | var southWest: QuadTree { 43 | get { 44 | return quadrants.value.southWest 45 | } 46 | set { 47 | quadrants.value.southWest = newValue 48 | } 49 | } 50 | 51 | /// The SouthEast quadrant of the QuadTree's root. 52 | var southEast: QuadTree { 53 | get { 54 | return quadrants.value.southEast 55 | } 56 | set { 57 | quadrants.value.southEast = newValue 58 | } 59 | } 60 | 61 | init(northWest: QuadTree, 62 | northEast: QuadTree, 63 | southWest: QuadTree, 64 | southEast: QuadTree) { 65 | quadrants = Box(value: QuadrantWrapper(northWest: northWest, 66 | northEast: northEast, 67 | southWest: southWest, 68 | southEast: southEast)) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /BentoMapExample/Annotations/Annotations.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Annotations.swift 3 | // BentoMap 4 | // 5 | // Created by Michael Skiba on 7/7/16. 6 | // Copyright © 2016 Raizlabs. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import MapKit 11 | import BentoMap 12 | 13 | class BaseAnnotation: NSObject, MKAnnotation { 14 | let originCoordinate: MKMapPoint 15 | let root: MKMapRect 16 | var coordinate: CLLocationCoordinate2D { 17 | return MKCoordinateForMapPoint(originCoordinate) 18 | } 19 | 20 | init(originCoordinate: MKMapPoint, root: MKMapRect) { 21 | self.originCoordinate = originCoordinate 22 | self.root = root 23 | } 24 | 25 | static func makeAnnotation(_ result: QuadTreeResult) -> BaseAnnotation { 26 | let annotation: BaseAnnotation 27 | switch result { 28 | case let .single(node: node): 29 | annotation = SingleAnnotation(originCoordinate: result.originCoordinate, 30 | annotationNumber: node.content, 31 | root: result.contentRect) 32 | case let .multiple(nodes: nodes): 33 | annotation = ClusterAnnotation(originCoordinate: result.originCoordinate, 34 | annotationNumbers: nodes.map({ $0.content }), 35 | root: result.contentRect) 36 | } 37 | return annotation 38 | } 39 | } 40 | 41 | final class SingleAnnotation: BaseAnnotation { 42 | 43 | let annotationNumber: Int 44 | var title: String? 45 | 46 | init(originCoordinate: MKMapPoint, annotationNumber: Int, root: MKMapRect) { 47 | self.annotationNumber = annotationNumber 48 | super.init(originCoordinate: originCoordinate, root: root) 49 | title = "Point #\(annotationNumber)" 50 | } 51 | 52 | } 53 | 54 | final class ClusterAnnotation: BaseAnnotation { 55 | 56 | let annotationNumbers: [Int] 57 | 58 | init(originCoordinate: MKMapPoint, annotationNumbers: [Int], root: MKMapRect) { 59 | self.annotationNumbers = annotationNumbers.sorted() 60 | super.init(originCoordinate: originCoordinate, root: root) 61 | } 62 | 63 | } 64 | 65 | func == (lhs: BaseAnnotation, rhs: BaseAnnotation) -> Bool { 66 | if let lSingle = lhs as? SingleAnnotation, 67 | let rSingle = rhs as? SingleAnnotation { 68 | return lSingle.annotationNumber == rSingle.annotationNumber 69 | } 70 | else if let lMulti = lhs as? ClusterAnnotation, 71 | let rMulti = rhs as? ClusterAnnotation { 72 | return lMulti.annotationNumbers == rMulti.annotationNumbers 73 | } 74 | return false 75 | } 76 | -------------------------------------------------------------------------------- /BentoMap/QuadTreeResult.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QuadTreeResult.swift 3 | // BentoMap 4 | // 5 | // Created by Michael Skiba on 2/17/16. 6 | // Copyright © 2016 Raizlabs. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import MapKit 11 | 12 | public enum QuadTreeResult { 13 | 14 | case single(node: QuadTreeNode) 15 | case multiple(nodes: [QuadTreeNode]) 16 | 17 | /// The average of the origin points of all the nodes 18 | /// contained in the QuadTree. 19 | public var originCoordinate: Coordinate { 20 | let originCoordinate: Coordinate 21 | switch self { 22 | case let .single(node): 23 | originCoordinate = node.originCoordinate 24 | case let .multiple(nodes): 25 | var x: CGFloat = 0.0 26 | var y: CGFloat = 0.0 27 | for node in nodes { 28 | x += node.originCoordinate.coordX 29 | y += node.originCoordinate.coordY 30 | } 31 | x /= CGFloat(nodes.count) 32 | y /= CGFloat(nodes.count) 33 | originCoordinate = Coordinate(coordX: x, coordY: y) 34 | } 35 | return originCoordinate 36 | } 37 | 38 | /// The smallest possible rectangle that contains the node(s) contained in this QuadTree. 39 | public var contentRect: Rect { 40 | let origin: Coordinate 41 | let size: CGSize 42 | switch self { 43 | case let .single(node: node): 44 | origin = node.originCoordinate 45 | size = CGSize() 46 | case let .multiple(nodes: nodes): 47 | var minCoordinate = CGPoint(x: CGFloat.greatestFiniteMagnitude, y: CGFloat.greatestFiniteMagnitude) 48 | var maxCoordinate = CGPoint(x: -CGFloat.greatestFiniteMagnitude, y: -CGFloat.greatestFiniteMagnitude) 49 | for node in nodes { 50 | minCoordinate.x = min(minCoordinate.coordX, node.originCoordinate.coordX) 51 | minCoordinate.y = min(minCoordinate.coordY, node.originCoordinate.coordY) 52 | maxCoordinate.x = max(maxCoordinate.coordX, node.originCoordinate.coordX) 53 | maxCoordinate.y = max(maxCoordinate.coordY, node.originCoordinate.coordY) 54 | } 55 | origin = Coordinate(coordX: minCoordinate.coordX, coordY: minCoordinate.coordY) 56 | // slightly pad the size to make sure all nodes are contained 57 | size = CGSize(width: abs(minCoordinate.x - maxCoordinate.x) + 0.001, 58 | height: abs(minCoordinate.y - maxCoordinate.y) + 0.001) 59 | } 60 | return Rect(originCoordinate: origin, size: size) 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /BentoMap/QuadTreeNode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QuadTreeNode.swift 3 | // BentoMap 4 | // 5 | // Created by Michael Skiba on 2/17/16. 6 | // Copyright © 2016 Raizlabs. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol BentoCoordinate { 12 | 13 | /// The horizontal location of the coordinate 14 | var coordX: CGFloat { get set } 15 | 16 | /// The vertical location of the coordinate 17 | var coordY: CGFloat { get set } 18 | 19 | init(coordX: CGFloat, coordY: CGFloat) 20 | } 21 | 22 | public protocol BentoRect { 23 | 24 | /// The horizontal max of the rectangle. 25 | var maxX: CGFloat { get } 26 | 27 | /// The vertical max of the rectangle. 28 | var maxY: CGFloat { get } 29 | 30 | /// The horizontal min of the rectangle. 31 | var minX: CGFloat { get } 32 | 33 | /// The vertical min of the rectangle. 34 | var minY: CGFloat { get } 35 | 36 | /** 37 | A test to determine if this rectangle contains 38 | a given coordinate. 39 | 40 | - parameter c: a coordinate. 41 | 42 | - returns: a Bool indicating whether this rectangle contains the coordinate passed in. 43 | */ 44 | func containsCoordinate(_ c: BentoCoordinate) -> Bool 45 | 46 | /** 47 | Divides this rectangle at a given percentage 48 | on the axis of the supplied edge. 49 | 50 | - parameter percent: the percentage at which to divide this rectangle. 51 | - parameter edge: the edge from which to divide this rectangle. 52 | 53 | - returns: a tuple containing a rectangle that is the percentage requested, 54 | and a rectangle that is the remainder. 55 | */ 56 | func divide(_ percent: CGFloat, edge: CGRectEdge) -> (Self, Self) 57 | 58 | /** 59 | * Computes the geometric union of this rectangle with the supplied rectangle. 60 | * 61 | * - parameter other: a rectangle. 62 | */ 63 | func unionWith(_ other: Self) -> Self 64 | 65 | init(originCoordinate: BentoCoordinate, size: CGSize) 66 | } 67 | 68 | public struct QuadTreeNode { 69 | 70 | /// The location of this node in the map's coordinate space. 71 | public var originCoordinate: Coordinate 72 | 73 | /// The data associated with this node. 74 | public var content: NodeData 75 | 76 | public init(originCoordinate: Coordinate, content: NodeData) { 77 | self.originCoordinate = originCoordinate 78 | self.content = content 79 | } 80 | 81 | } 82 | 83 | public protocol CoordinateProvider { 84 | 85 | /// The type of coordinate associated with the conforming object. 86 | associatedtype CoordinateType: BentoCoordinate 87 | 88 | /// The coordinate associated with the conforming object. 89 | var coordinate: CoordinateType { get } 90 | 91 | } 92 | 93 | extension QuadTreeNode: CoordinateProvider { 94 | 95 | public typealias CoordinateType = Coordinate 96 | 97 | /// The origin coordinate of the QuadTree. 98 | public var coordinate: Coordinate { 99 | return originCoordinate 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /CONTRIBUTING.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 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at . All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ -------------------------------------------------------------------------------- /BentoMap/BentoBox.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BentoBox.swift 3 | // BentoMap 4 | // 5 | // Created by Michael Skiba on 2/17/16. 6 | // Copyright © 2016 Raizlabs. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct BentoBox { 12 | 13 | /// The root node of the tree. Can be thought of 14 | /// as the container map of the sub-rectangles 15 | public var root: Rect 16 | 17 | public init(minPoint: Coordinate, maxPoint: Coordinate) { 18 | 19 | let minX = min(minPoint.coordX, maxPoint.coordX) 20 | let minY = min(minPoint.coordY, maxPoint.coordY) 21 | let maxX = max(minPoint.coordX, maxPoint.coordX) 22 | let maxY = max(minPoint.coordY, maxPoint.coordY) 23 | 24 | root = Rect(originCoordinate: Coordinate(coordX: minX, coordY: minY), 25 | size: CGSize(width: maxX - minX, height: maxY - minY)) 26 | } 27 | 28 | public init(root: Rect) { 29 | self.root = root 30 | } 31 | 32 | } 33 | 34 | // MARK: Public API 35 | 36 | public extension BentoBox { 37 | 38 | /// The coordinate at the top left corner of the root rect 39 | public var minCoordinate: Coordinate { 40 | return Coordinate(coordX: root.minX, coordY: root.minY) 41 | } 42 | 43 | /// The coordinate at the bottom right corner of the root rect 44 | public var maxCoordinate: Coordinate { 45 | return Coordinate(coordX: root.maxX, coordY: root.maxY) 46 | } 47 | 48 | } 49 | 50 | extension BentoBox { 51 | 52 | /** 53 | Returns a Bool value for whether the root contains 54 | the coordinate passed in 55 | 56 | - parameter coordinate: a BentoCoordinate 57 | 58 | - returns: whether the root contains 59 | the coordinate passed in 60 | */ 61 | func containsCoordinate(_ coordinate: BentoCoordinate) -> Bool { 62 | // we aren't using MKMapRectContainsPoint(mapRect, pt) because it will test true for points that 63 | // are exatly on either edge, which means that a point exact on an edge may be counted in multiple boxes 64 | let width = root.maxX - root.minX 65 | let height = root.maxY - root.minY 66 | 67 | let isContained = coordinate.coordX >= root.minX && 68 | coordinate.coordX < (root.minX + width) && 69 | coordinate.coordY >= root.minY && 70 | coordinate.coordY < (root.minY + height) 71 | return isContained 72 | } 73 | 74 | /** 75 | Returns a Bool value for whether the root intersects with the 76 | root of the bentoBox passed in 77 | 78 | - parameter bentoBox: a BentoBox 79 | 80 | - returns: whether the root intersects with the 81 | root of the bentoBox passed in 82 | */ 83 | func intersectsBentoBox(_ bentoBox: BentoBox) -> Bool { 84 | return root.minX < bentoBox.root.maxX && root.maxX > bentoBox.root.minX && 85 | root.minY < bentoBox.root.maxY && root.maxY > bentoBox.root.minY 86 | } 87 | 88 | /// A wrapper around the 4 child nodes of the root 89 | var quadrants: QuadrantWrapper { 90 | let (north, south) = root.divide(0.5, edge: .minYEdge) 91 | let (northWest, northEast) = north.divide(0.5, edge: .minXEdge) 92 | let (southWest, southEast) = south.divide(0.5, edge: .minXEdge) 93 | 94 | return QuadrantWrapper(northWest: northWest, northEast: northEast, southWest: southWest, southEast: southEast).map(BentoBox.init) 95 | } 96 | 97 | @discardableResult func union(_ other: BentoBox) -> Rect { 98 | return root.unionWith(other.root) 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /BentoMapExample/App/QuadTree+SampleData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QuadTree+sampleMapData.swift 3 | // BentoMap 4 | // 5 | // Created by Michael Skiba on 7/6/16. 6 | // Copyright © 2016 Raizlabs. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import MapKit 11 | import BentoMap 12 | 13 | extension QuadTree { 14 | 15 | static var sampleData: QuadTree { 16 | var samples = QuadTree(bentoBox: BentoBox(minPoint: MKMapPointForCoordinate(CLLocationCoordinate2D.minCoord), maxPoint: MKMapPointForCoordinate(CLLocationCoordinate2D.maxCoord)), bucketCapacity: 5) 17 | let randomData = (1...5000).map { count in 18 | return QuadTreeNode(originCoordinate: MKMapPointForCoordinate(CLLocationCoordinate2D.randomCoordinate()), content: count) 19 | } 20 | for node in randomData { 21 | samples.insertNode(node) 22 | } 23 | return samples 24 | } 25 | 26 | static func sampleGridData(forContainerRect containerRect: CGRect) -> QuadTree { 27 | let minPoint = CGPoint(x: containerRect.minX, y: containerRect.minY) 28 | let maxPoint = CGPoint(x: containerRect.maxX, y: containerRect.maxY) 29 | var samples = QuadTree(bentoBox: BentoBox(minPoint: minPoint, maxPoint: maxPoint), bucketCapacity: 5) 30 | let root = QuadTreeNode(originCoordinate: CGPoint.zero, content: 1000) 31 | samples.insertNode(root) 32 | for count in 1...500 { 33 | let node = QuadTreeNode(originCoordinate: CGPoint.randomPoint(withinRect: containerRect), content: count) 34 | samples.insertNode(node) 35 | } 36 | return samples 37 | } 38 | 39 | } 40 | 41 | private extension CGPoint { 42 | 43 | static func randomPoint(withinRect rect: CGRect) -> CGPoint { 44 | let x = Double.cappedRandom(min: Double(rect.minX), max: Double(rect.maxX)) 45 | let y = Double.cappedRandom(min: Double(rect.minY), max: Double(rect.maxY)) 46 | return CGPoint(x: x, y: y) 47 | } 48 | 49 | } 50 | 51 | private extension CLLocationCoordinate2D { 52 | enum Coordinates { 53 | static let bostonLatitude: Double = 42.3584300 54 | static let bostonLongitude: Double = -71.0597700 55 | static let adjustment: Double = 0.5 56 | static let minLatitude: Double = Coordinates.bostonLatitude - Coordinates.adjustment 57 | static let minLongitude: Double = Coordinates.bostonLongitude - Coordinates.adjustment 58 | static let maxLatitude: Double = Coordinates.bostonLatitude + Coordinates.adjustment 59 | static let maxLongitude: Double = Coordinates.bostonLongitude + Coordinates.adjustment 60 | } 61 | 62 | static let minCoord = CLLocationCoordinate2D(latitude: Coordinates.minLatitude, 63 | longitude: Coordinates.minLongitude) 64 | static let maxCoord = CLLocationCoordinate2D(latitude: Coordinates.maxLatitude, 65 | longitude: Coordinates.maxLongitude) 66 | 67 | static func randomCoordinate() -> CLLocationCoordinate2D { 68 | let lat = Double.cappedRandom(min: Coordinates.minLatitude, 69 | max: Coordinates.maxLatitude) 70 | let long = Double.cappedRandom(min: Coordinates.minLongitude, 71 | max: Coordinates.maxLongitude) 72 | return CLLocationCoordinate2D(latitude: lat, longitude: long) 73 | } 74 | } 75 | 76 | private extension Double { 77 | static func cappedRandom(min minValue: Double, max maxValue: Double) -> Double { 78 | let exponent: Double = 10000.0 79 | let diff = UInt32(abs(minValue - maxValue) * exponent) 80 | let randomNumber = Double(arc4random_uniform(diff)) / exponent 81 | return min(minValue, maxValue) + randomNumber 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /BentoMap.xcodeproj/xcshareddata/xcschemes/BentoMap.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 | 66 | 72 | 73 | 74 | 75 | 76 | 77 | 83 | 84 | 90 | 91 | 92 | 93 | 95 | 96 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BentoMap 2 | > Map Clustering for Swift 3 | 4 | [![Build Status](https://travis-ci.org/Raizlabs/BentoMap.svg?branch=develop)](https://travis-ci.org/Raizlabs/BentoMap) 5 | [![Version](https://img.shields.io/cocoapods/v/BentoMap.svg?style=flat)](http://cocoapods.org/pods/BentoMap) 6 | [![License](https://img.shields.io/cocoapods/l/BentoMap.svg?style=flat)](http://cocoapods.org/pods/BentoMap) 7 | [![Platform](https://img.shields.io/cocoapods/p/BentoMap.svg?style=flat)](http://cocoapods.org/pods/BentoMap) 8 | [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) 9 | 10 | BentoMap is an Swift implementation of [quadtrees][wiki] for map annotation clustering, and storage. It can also allow other 2d coordinate data to conform to a protocol and be added into `BentoBox` containers. 11 | 12 | For more information, check out the [Raizlabs Developer Blog][rl]. The Android equivalent, known as "Marker Clustering," is [documented here][mk]. 13 | 14 | [wiki]: https://en.wikipedia.org/wiki/Quadtree 15 | [rl]: https://www.raizlabs.com/dev/2016/08/introducing-bentomap/ 16 | [mk]: https://developers.google.com/maps/documentation/android-api/utility/marker-clustering 17 | 18 | ![BentoMap](Resources/bento_animation3.gif) 19 | 20 | ## Features 21 | 22 | - [x] Store annotation data in QuadTrees 23 | - [x] Fetch annotations in a region, with a clustering threshold 24 | - [x] Protocols for storing other data types 25 | 26 | ## Requirements 27 | 28 | - iOS 9.0+ 29 | - Xcode 8.0 30 | 31 | ## Installation 32 | 33 | #### CocoaPods 34 | BentoMap is available through [CocoaPods][cp]. To install it, simply add the following line to your Podfile: 35 | 36 | ```ruby 37 | pod 'BentoMap' 38 | ``` 39 | 40 | #### Carthage 41 | Create a `Cartfile` that lists the framework and run `carthage update`. Follow the [instructions][carthage] to add `$(SRCROOT)/Carthage/Build/iOS/BentoMap.framework` to an iOS project. 42 | 43 | ```ogdl 44 | github "Raizlabs/BentoMap" 45 | ``` 46 | 47 | #### Manually 48 | 1. Download all of the `.swift` files in `BentoMap/` and `BentoMap/Extensions/` and drop them into your project. 49 | 2. Congratulations! 50 | 51 | 52 | [cp]: http://cocoapods.org/ 53 | [carthage]: https://github.com/Carthage/Carthage#if-youre-building-for-ios 54 | 55 | ## Usage example 56 | 57 | To see a full implementation of loading data into a map view, check out the [example project][ex]. 58 | 59 | ### Inserting Data 60 | 61 | ```swift 62 | import BentoMap 63 | 64 | static var sampleData: QuadTree { 65 | var samples = QuadTree(bentoBox: BentoBox(minPoint: MKMapPointForCoordinate(CLLocationCoordinate2D.minCoord), maxPoint: MKMapPointForCoordinate(CLLocationCoordinate2D.maxCoord)), bucketCapacity: 5) 66 | let randomData = (1...5000).map { count in 67 | return QuadTreeNode(originCoordinate: MKMapPointForCoordinate(CLLocationCoordinate2D.randomCoordinate()), content: count) 68 | } 69 | for node in randomData { 70 | samples.insertNode(node) 71 | } 72 | return samples 73 | } 74 | 75 | ``` 76 | 77 | ### Updating a Map View 78 | 79 | ```swift 80 | func updateAnnotations(inMapView mapView: MKMapView, 81 | forMapRect root: MKMapRect) { 82 | guard !mapView.frame.isEmpty && !MKMapRectIsEmpty(root) else { 83 | mapView.removeAnnotations(mapView.annotations) 84 | return 85 | } 86 | let zoomScale = Double(mapView.frame.width) / root.size.width 87 | let clusterResults = mapData.clusteredDataWithinMapRect(root, 88 | zoomScale: zoomScale, 89 | cellSize: Double(MapKitViewController.cellSize)) 90 | let newAnnotations = clusterResults.map(BaseAnnotation.makeAnnotation) 91 | 92 | let oldAnnotations = mapView.annotations.flatMap({ $0 as? BaseAnnotation }) 93 | 94 | let toRemove = oldAnnotations.filter { annotation in 95 | return !newAnnotations.contains { newAnnotation in 96 | return newAnnotation == annotation 97 | } 98 | } 99 | 100 | mapView.removeAnnotations(toRemove) 101 | 102 | let toAdd = newAnnotations.filter { annotation in 103 | return !oldAnnotations.contains { oldAnnotation in 104 | return oldAnnotation == annotation 105 | } 106 | } 107 | 108 | mapView.addAnnotations(toAdd) 109 | } 110 | ``` 111 | 112 | [ex]: https://github.com/Raizlabs/BentoMap/blob/develop/BentoMapExample/App/MapKitViewController.swift 113 | 114 | ## Contributing 115 | 116 | Issues and pull requests are welcome! Please ensure that you have the latest [SwiftLint][sl] installed before committing and that there are no style warnings generated when building. 117 | 118 | Contributors are expected to abide by the [Contributor Covenant Code of Conduct][cc]. 119 | 120 | [sl]: https://github.com/realm/SwiftLint 121 | [cc]: https://github.com/Raizlabs/BentoMap/blob/develop/CONTRIBUTING.md 122 | 123 | ## License 124 | 125 | BentoMap is available under the MIT license. See the `LICENSE` file for more info. 126 | 127 | ## Authors 128 | 129 | - Michael Skiba: , [@atelierclkwrk][mstw] 130 | - Rob Visentin: 131 | - Matt Buckley: , [@mattthousand][mbtw] 132 | 133 | [mstw]: https://twitter.com/atelierclkwrk 134 | [mbtw]: https://twitter.com/mattthousand 135 | -------------------------------------------------------------------------------- /BentoMapExample/App/MapKitViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MapKitViewController.swift 3 | // BentoMapExample 4 | // 5 | // Created by Michael Skiba on 7/6/16. 6 | // Copyright © 2016 Raizlabs. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MapKit 11 | import BentoMap 12 | 13 | final class MapKitViewController: UIViewController { 14 | 15 | static let cellSize: CGFloat = 64 16 | // Used to make sure the map is nicely padded on the edges, and visible annotations 17 | // aren't hidden under the navigation bar 18 | static let mapInsets = UIEdgeInsets(top: cellSize, left: (cellSize / 2), bottom: (cellSize / 2), right: (cellSize / 2)) 19 | 20 | let mapData = QuadTree.sampleData 21 | var mapView: MKMapView! { 22 | return view as? MKMapView 23 | } 24 | 25 | override func loadView() { 26 | let mapView = MKMapView() 27 | mapView.delegate = self 28 | view = mapView 29 | } 30 | 31 | override func viewDidLoad() { 32 | super.viewDidLoad() 33 | navigationItem.title = NSLocalizedString("BentoBox", 34 | comment: "BentoBox navbar title") 35 | } 36 | 37 | override func viewDidAppear(_ animated: Bool) { 38 | super.viewDidAppear(animated) 39 | let zoomRect = mapView.mapRectThatFits(mapData.bentoBox.root, edgePadding: type(of: self).mapInsets) 40 | mapView.setVisibleMapRect(zoomRect, 41 | edgePadding: type(of: self).mapInsets, 42 | animated: false) 43 | } 44 | 45 | } 46 | 47 | extension MapKitViewController: MKMapViewDelegate { 48 | 49 | func mapView(_ mapView: MKMapView, 50 | viewFor annotation: MKAnnotation) -> MKAnnotationView? { 51 | let annotationView: MKAnnotationView 52 | if let single = annotation as? SingleAnnotation { 53 | annotationView = mapView.dequeueAnnotationView(for: single) as SingleAnnotationView 54 | } 55 | else if let cluster = annotation as? ClusterAnnotation { 56 | annotationView = mapView.dequeueAnnotationView(for: cluster) as ClusterAnnotationView 57 | } 58 | else { 59 | fatalError("Unexpected annotation type found") 60 | } 61 | return annotationView 62 | } 63 | 64 | func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) { 65 | if let cluster = view as? ClusterAnnotationView, let zoomRect = cluster.typedAnnotation?.root { 66 | let adjustedZoom = mapView.mapRectThatFits(zoomRect, edgePadding: type(of: self).mapInsets) 67 | mapView.setVisibleMapRect(adjustedZoom, 68 | edgePadding: type(of: self).mapInsets, 69 | animated: true) 70 | } 71 | else { 72 | view.setSelected(true, animated: true) 73 | } 74 | } 75 | 76 | func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) { 77 | updateAnnotations(inMapView: mapView, 78 | forMapRect: mapView.visibleMapRect) 79 | } 80 | 81 | } 82 | 83 | private extension MapKitViewController { 84 | 85 | func updateAnnotations(inMapView mapView: MKMapView, 86 | forMapRect root: MKMapRect) { 87 | guard !mapView.frame.isEmpty && !MKMapRectIsEmpty(root) else { 88 | mapView.removeAnnotations(mapView.annotations) 89 | return 90 | } 91 | let zoomScale = Double(mapView.frame.width) / root.size.width 92 | let clusterResults = mapData.clusteredDataWithinMapRect(root, 93 | zoomScale: zoomScale, 94 | cellSize: Double(MapKitViewController.cellSize)) 95 | let newAnnotations = clusterResults.map(BaseAnnotation.makeAnnotation) 96 | 97 | let oldAnnotations = mapView.annotations.flatMap({ $0 as? BaseAnnotation }) 98 | 99 | let toRemove = oldAnnotations.filter { annotation in 100 | return !newAnnotations.contains { newAnnotation in 101 | return newAnnotation == annotation 102 | } 103 | } 104 | 105 | let snapshots: [UIView] = toRemove.flatMap { annotation in 106 | guard let annotationView = mapView.view(for: annotation), 107 | let snapshot = annotationView.snapshotView(afterScreenUpdates: false), 108 | mapView.frame.intersects(annotationView.frame) == true else { 109 | return nil 110 | } 111 | snapshot.frame = annotationView.frame 112 | mapView.insertSubview(snapshot, aboveSubview: annotationView) 113 | return snapshot 114 | } 115 | 116 | UIView.animate(withDuration: 0.2, animations: { 117 | for snapshot in snapshots { 118 | snapshot.transform = CGAffineTransform(scaleX: 0.01, y: 0.01) 119 | snapshot.layer.opacity = 0 120 | } 121 | }, 122 | completion: { _ in 123 | for snapshot in snapshots { 124 | snapshot.removeFromSuperview() 125 | } 126 | }) 127 | 128 | mapView.removeAnnotations(toRemove) 129 | 130 | let toAdd = newAnnotations.filter { annotation in 131 | return !oldAnnotations.contains { oldAnnotation in 132 | return oldAnnotation == annotation 133 | } 134 | } 135 | 136 | mapView.addAnnotations(toAdd) 137 | } 138 | 139 | } 140 | -------------------------------------------------------------------------------- /BentoMapExample/Annotations/AnnotationView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnnotationView.swift 3 | // BentoMap 4 | // 5 | // Created by Michael Skiba on 1/10/17. 6 | // Copyright © 2017 Raizlabs. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MapKit 11 | 12 | private extension CGSize { 13 | static let minSize: CGSize = CGSize(width: 32, height: 32) 14 | } 15 | 16 | extension MKAnnotationView { 17 | 18 | static var classReuseIdentifier: String { 19 | return String(describing: self) 20 | } 21 | 22 | } 23 | 24 | extension MKMapView { 25 | 26 | func dequeueAnnotationView(for annotation: AnnotationView.AnnotationType, 27 | withIdentifier: String = AnnotationView.classReuseIdentifier) -> AnnotationView where AnnotationView: TypedAnnotationView { 28 | let view: AnnotationView 29 | if let annotationView = dequeueReusableAnnotationView(withIdentifier: AnnotationView.classReuseIdentifier) as? AnnotationView { 30 | annotationView.annotation = annotation 31 | view = annotationView 32 | } 33 | else { 34 | view = AnnotationView.init(annotation: annotation, reuseIdentifier: AnnotationView.classReuseIdentifier) 35 | } 36 | return view 37 | } 38 | 39 | } 40 | 41 | protocol TypedAnnotationView { 42 | 43 | associatedtype AnnotationType: MKAnnotation 44 | 45 | var annotation: MKAnnotation? { get set } 46 | 47 | func updateAnnotation() 48 | } 49 | 50 | extension TypedAnnotationView { 51 | 52 | var typedAnnotation: AnnotationType? { 53 | get { 54 | return annotation as? AnnotationType 55 | } 56 | set { 57 | annotation = newValue 58 | updateAnnotation() 59 | } 60 | } 61 | 62 | } 63 | 64 | class AnimatedAnnotationView: MKAnnotationView { 65 | 66 | override func didMoveToSuperview() { 67 | super.didMoveToSuperview() 68 | transform = CGAffineTransform(scaleX: 0.01, y: 0.01) 69 | layer.opacity = 0 70 | let animation = { 71 | self.transform = .identity 72 | self.layer.opacity = 1 73 | } 74 | UIView.animate(withDuration: 0.4, 75 | delay: 0, 76 | usingSpringWithDamping: 0.5, 77 | initialSpringVelocity: 1.0, 78 | options: [.curveEaseInOut], 79 | animations: animation, 80 | completion: nil) 81 | } 82 | 83 | } 84 | 85 | class SingleAnnotationView: AnimatedAnnotationView, TypedAnnotationView { 86 | 87 | typealias AnnotationType = SingleAnnotation 88 | 89 | override var annotation: MKAnnotation? { 90 | didSet { 91 | updateAnnotation() 92 | } 93 | } 94 | 95 | override init(annotation: MKAnnotation?, reuseIdentifier: String?) { 96 | super.init(annotation: annotation, reuseIdentifier: reuseIdentifier) 97 | image = #imageLiteral(resourceName: "redHexagon") 98 | canShowCallout = true 99 | updateAnnotation() 100 | } 101 | 102 | required init?(coder aDecoder: NSCoder) { 103 | fatalError("init(coder:) has not been implemented") 104 | } 105 | 106 | func updateAnnotation() { 107 | } 108 | 109 | } 110 | 111 | class ClusterAnnotationView: AnimatedAnnotationView, TypedAnnotationView { 112 | 113 | typealias AnnotationType = ClusterAnnotation 114 | let label: UILabel 115 | 116 | override var annotation: MKAnnotation? { 117 | didSet { 118 | updateAnnotation() 119 | } 120 | } 121 | 122 | override init(annotation: MKAnnotation?, reuseIdentifier: String?) { 123 | label = UILabel() 124 | label.font = UIFont.boldSystemFont(ofSize: 12) 125 | label.textColor = .white 126 | label.layer.shouldRasterize = true 127 | label.layer.rasterizationScale = UIScreen.main.scale 128 | label.layer.masksToBounds = true 129 | label.layer.shadowColor = UIColor.black.cgColor 130 | label.layer.shadowOffset = CGSize(width: 0, height: 0) 131 | label.layer.shadowRadius = 0.5 132 | label.layer.shadowOpacity = 1 133 | 134 | super.init(annotation: annotation, reuseIdentifier: reuseIdentifier) 135 | 136 | image = #imageLiteral(resourceName: "blueStretch") 137 | 138 | addSubview(label) 139 | label.translatesAutoresizingMaskIntoConstraints = false 140 | NSLayoutConstraint.activate([ 141 | NSLayoutConstraint(item: label, attribute: .centerX, relatedBy: .equal, toItem: self, attribute: .centerX, multiplier: 1, constant: 0), 142 | NSLayoutConstraint(item: label, attribute: .centerY, relatedBy: .equal, toItem: self, attribute: .centerY, multiplier: 1, constant: 0), 143 | ]) 144 | 145 | updateAnnotation() 146 | } 147 | 148 | required init?(coder aDecoder: NSCoder) { 149 | fatalError("init(coder:) has not been implemented") 150 | } 151 | 152 | func updateAnnotation() { 153 | label.text = typedAnnotation.map { "\($0.annotationNumbers.count)" } 154 | 155 | var size = label.systemLayoutSizeFitting(.minSize, 156 | withHorizontalFittingPriority: UILayoutPriorityFittingSizeLevel, 157 | verticalFittingPriority: UILayoutPriorityFittingSizeLevel) 158 | 159 | size.width += 8 160 | size.height += 8 161 | 162 | if size.width < CGSize.minSize.width { 163 | size.width = CGSize.minSize.width 164 | } 165 | if size.height < CGSize.minSize.height { 166 | size.height = CGSize.minSize.height 167 | } 168 | 169 | frame = CGRect(origin: CGPoint(), size: size) 170 | } 171 | 172 | } 173 | -------------------------------------------------------------------------------- /BentoMap/QuadTree.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QuadTree.swift 3 | // BentoMap 4 | // 5 | // Created by Michael Skiba on 2/17/16. 6 | // Copyright © 2016 Raizlabs. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | //following example code and text from https://robots.thoughtbot.com/how-to-handle-large-amounts-of-data-on-maps 12 | public struct QuadTree { 13 | 14 | /// The individual nodes that compose the QuadTree 15 | public var ordinalNodes: OrdinalNodes? 16 | 17 | /// The rectangular map specified by the QuadTree 18 | var root: BentoBox 19 | 20 | /// The number of coordinates or points that an individual node may contain 21 | public let bucketCapacity: Int 22 | public var points = [QuadTreeNode]() 23 | 24 | public init(bentoBox: BentoBox, bucketCapacity: Int) { 25 | precondition(bucketCapacity > 0, "Bucket capacity must be greater than 0") 26 | self.root = bentoBox 27 | self.bucketCapacity = bucketCapacity 28 | } 29 | 30 | } 31 | 32 | public extension QuadTree { 33 | 34 | /// Recursively computes the bounding box of the points in the quad tree in O(n) time. 35 | public var bentoBox: BentoBox { 36 | let boundingBox: BentoBox = points.boundingBox() 37 | 38 | if let ordinals = ordinalNodes { 39 | boundingBox.union(ordinals.northWest.bentoBox) 40 | boundingBox.union(ordinals.northEast.bentoBox) 41 | boundingBox.union(ordinals.southWest.bentoBox) 42 | boundingBox.union(ordinals.southEast.bentoBox) 43 | } 44 | 45 | return boundingBox 46 | } 47 | 48 | /** 49 | Computes clusters of data based on the zoomscale 50 | and cell size passed in. 51 | 52 | - parameter root: the root bentoBox of the Quadtree (used 53 | to compute the bounding rectangle for the map. 54 | - parameter zoomScale: used to calculate scale factor relative to the cell size passed in. 55 | - parameter cellSize: the desired size for the "clustering region" for each individual bucket.i 56 | 57 | - returns: an array of quadtree results for each cell that contains nodes. 58 | */ 59 | public func clusteredDataWithinMapRect(_ root: Rect, zoomScale: Double, cellSize: Double) -> [QuadTreeResult] { 60 | 61 | let scaleFactor: Double 62 | 63 | // Prevents divide by zero errors from cropping up if handed bad data 64 | if cellSize == 0 || zoomScale == 0 { 65 | scaleFactor = 1 66 | } 67 | else { 68 | scaleFactor = zoomScale / cellSize 69 | } 70 | 71 | let stepSize = CGFloat(1.0 / scaleFactor) 72 | 73 | // normalizes the cell grid to make sure the first column is aligned to the step size to prevent jitter 74 | let minX = (root.minX - root.minX.remainder(dividingBy: stepSize)) - stepSize 75 | // normalizes the cell grid to make sure the last column is added to the buckets 76 | let maxX = (root.maxX - root.maxY.remainder(dividingBy: stepSize)) + stepSize 77 | // normalizes the cell grid to make sure the first row is aligned to the step size to prevent jitter 78 | let minY = (root.minY - root.minY.remainder(dividingBy: stepSize)) - stepSize 79 | // normalizes the cell grid to make sure the last row is added to the buckets 80 | let maxY = (root.maxY - root.maxY.remainder(dividingBy: stepSize)) + stepSize 81 | 82 | var result = [QuadTreeResult]() 83 | 84 | let mapStep = CGSize(width: Double(stepSize), height: Double(stepSize)) 85 | for x in stride(from: minX, through: maxX, by: stepSize) { 86 | for y in stride(from: minY, through: maxY, by: stepSize) { 87 | let cellRectangle = Rect(originCoordinate: Coordinate(coordX: x, coordY: y), size: mapStep) 88 | let nodes = nodesInRange(BentoBox(root: cellRectangle)) 89 | 90 | switch nodes.count { 91 | case 0: 92 | continue 93 | case 1: 94 | if let first = nodes.first { 95 | result.append(.single(node: first)) 96 | } 97 | default: 98 | result.append(.multiple(nodes: nodes)) 99 | } 100 | } 101 | } 102 | return result 103 | } 104 | 105 | /** 106 | Inserts a node if the node fits in the 107 | bucket's coordinate space and if the bucket 108 | capacty has not been reached. If the bucket 109 | capacity has been reached, it subdivides the 110 | Quadtree (if required) and inserts the node 111 | into the appropriate subnode. 112 | 113 | - parameter node: the node to insert. 114 | 115 | - returns: a Bool indicating success or failure of the insertion. 116 | */ 117 | @discardableResult public mutating func insertNode(_ node: QuadTreeNode) -> Bool { 118 | guard root.containsCoordinate(node.originCoordinate) else { 119 | return false 120 | } 121 | 122 | if points.count < bucketCapacity { 123 | points.append(node) 124 | return true 125 | } 126 | 127 | if ordinalNodes == nil { 128 | subdivide() 129 | } 130 | 131 | return ordinalNodes?.northWest.insertNode(node) ||? 132 | ordinalNodes?.northEast.insertNode(node) ||? 133 | ordinalNodes?.southWest.insertNode(node) ||? 134 | ordinalNodes?.southEast.insertNode(node) 135 | } 136 | 137 | } 138 | 139 | private extension QuadTree { 140 | 141 | /** 142 | Divides the QuadTree into quadrants of equal dimension. 143 | */ 144 | mutating func subdivide() { 145 | let trees = root.quadrants.map { quadrant in 146 | QuadTree(bentoBox: quadrant, bucketCapacity: self.bucketCapacity) 147 | } 148 | 149 | ordinalNodes = OrdinalNodes(northWest: trees.northWest, 150 | northEast: trees.northEast, 151 | southWest: trees.southWest, 152 | southEast: trees.southEast) 153 | } 154 | 155 | /** 156 | Computes the collection of nodes contained within a box's 157 | coordinate space. 158 | 159 | - parameter range: the containing box. 160 | 161 | - returns: a collection of nodes. 162 | */ 163 | func nodesInRange(_ range: BentoBox) -> [QuadTreeNode] { 164 | var nodes = [QuadTreeNode]() 165 | 166 | guard root.intersectsBentoBox(range) else { 167 | return nodes 168 | } 169 | 170 | nodes += points.filter { point in 171 | range.containsCoordinate(point.originCoordinate) 172 | } 173 | 174 | if let ordinals = ordinalNodes { 175 | nodes += ordinals.northWest.nodesInRange(range) 176 | nodes += ordinals.northEast.nodesInRange(range) 177 | nodes += ordinals.southWest.nodesInRange(range) 178 | nodes += ordinals.southEast.nodesInRange(range) 179 | } 180 | 181 | return nodes 182 | } 183 | 184 | } 185 | -------------------------------------------------------------------------------- /BentoMapTests/QuadTreeTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QuadTreeTests.swift 3 | // BentoMapTests 4 | // 5 | // Created by Michael Skiba on 7/6/16. 6 | // Copyright © 2016 Raizlabs. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import MapKit 11 | @testable import BentoMap 12 | 13 | class QuadTreeTests: XCTestCase { 14 | 15 | override func setUp() { 16 | super.setUp() 17 | } 18 | 19 | override func tearDown() { 20 | super.tearDown() 21 | } 22 | 23 | func testQuadTreeInitialization() { 24 | let box = BentoBox(root: MKMapRectWorld) 25 | let quadTree = QuadTree(bentoBox: box, bucketCapacity: 5) 26 | 27 | XCTAssert(MKMapRectEqualToRect(box.root, quadTree.root.root), "The bounding box should equal the initalized bounding box") 28 | XCTAssert(quadTree.bucketCapacity == 5, "The bucket capacity passed in is the bucket capacity used") 29 | XCTAssertNil(quadTree.ordinalNodes, "The initalized nodes should be empty") 30 | XCTAssertTrue(quadTree.points.isEmpty, "The bucket of points should be empty") 31 | } 32 | 33 | fileprivate func resultRectTester(_ result: QuadTreeResult) { 34 | let contentRect = result.contentRect 35 | switch result { 36 | case let .single(node: node): 37 | XCTAssert(MKMapPointEqualToPoint(contentRect.origin, node.originCoordinate), "Single nodes should have an origin equal to the node's point") 38 | XCTAssert(MKMapSizeEqualToSize(MKMapSize(), contentRect.size), "single nodes should have a zero-size content size") 39 | case let .multiple(nodes: nodes): 40 | for node in nodes { 41 | XCTAssert(MKMapRectContainsPoint(contentRect, node.originCoordinate), "Every node should be contained in the map content rect") 42 | } 43 | } 44 | } 45 | 46 | func testQuadTreeInsertion() { 47 | 48 | let bentoBox = BentoBox(root: MKMapRect(origin: MKMapPoint(), size: MKMapSize(width: 5000, height: 5000))) 49 | var quadTree = QuadTree(bentoBox: bentoBox, bucketCapacity: 5) 50 | var i = 0 51 | for x in stride(from: 0, to: 5000, by: 50) { 52 | for y in stride(from: 0, to: 5000, by: 50) { 53 | let originCoordinate = MKMapPoint(x: Double(x), y: Double(y)) 54 | let node = QuadTreeNode(originCoordinate: originCoordinate, content: i) 55 | quadTree.insertNode(node) 56 | i += 1 57 | } 58 | } 59 | 60 | let unclusteredNodes = quadTree.clusteredDataWithinMapRect(bentoBox.root, zoomScale: 1, cellSize: 50) 61 | for point in unclusteredNodes { 62 | XCTAssert(Int(point.originCoordinate.coordX) % 50 == 0, "all map point coords should be divisible by 50") 63 | XCTAssert(Int(point.originCoordinate.coordY) % 50 == 0, "all map point coords should be divisible by 50") 64 | resultRectTester(point) 65 | } 66 | XCTAssertTrue(unclusteredNodes.count == 10000, "This should return 10k clusters") 67 | let clusteredNodes = quadTree.clusteredDataWithinMapRect(bentoBox.root, zoomScale: 1, cellSize: 500) 68 | XCTAssertTrue(clusteredNodes.count == 100, "This should return 100 clusters") 69 | var totalNodeCount = 0 70 | for cluster in clusteredNodes { 71 | XCTAssert(Int(cluster.originCoordinate.coordX - 225) % 500 == 0, "all map point coords should be divisible by 500 after centering/ point was \(cluster.originCoordinate.coordX)") 72 | XCTAssert(Int(cluster.originCoordinate.coordY - 225) % 500 == 0, "all map point coords should be divisible by 500 after centering. point was \(cluster.originCoordinate.coordY)") 73 | resultRectTester(cluster) 74 | switch cluster { 75 | case .single: 76 | totalNodeCount += 1 77 | case let .multiple(nodes: nodes): 78 | XCTAssert(nodes.count == 100, "Each cluster contains 100 nodes") 79 | totalNodeCount += nodes.count 80 | } 81 | } 82 | XCTAssertTrue(totalNodeCount == 10000, "All nodes should add up to 10k nodes") 83 | 84 | XCTAssertFalse(quadTree.insertNode(QuadTreeNode(originCoordinate: MKMapPoint(x: Double(5002), y: Double(5002)), content: 1))) 85 | let unclusteredNodes2 = quadTree.clusteredDataWithinMapRect(bentoBox.root, zoomScale: 1, cellSize: 50) 86 | XCTAssertTrue(unclusteredNodes2.count == 10000, "This should return 10k clusters as an out of bounds cluster shouldn't insert") 87 | 88 | // making sure divide by zero avoidance works 89 | let testZero = quadTree.clusteredDataWithinMapRect(MKMapRectMake(0, 0, 5, 5), zoomScale: 0, cellSize: 0) 90 | XCTAssertTrue(testZero.count == 1, "This should return 100 clusters") 91 | } 92 | 93 | func testClustering() { 94 | let bentoBox = BentoBox(root: MKMapRect(origin: MKMapPoint(), size: MKMapSize(width: 11, height: 11))) 95 | var quadTree = QuadTree(bentoBox: bentoBox, bucketCapacity: 5) 96 | var i = 0 97 | for x in 0...10 { 98 | for y in 0...10 { 99 | let originCoordinate = MKMapPoint(x: Double(x), y: Double(y)) 100 | let node = QuadTreeNode(originCoordinate: originCoordinate, content: i) 101 | quadTree.insertNode(node) 102 | i += 1 103 | } 104 | } 105 | 106 | let rect = MKMapRect(origin: MKMapPoint(), size: MKMapSize(width: 9, height: 9)) 107 | let unclusteredNodes = quadTree.clusteredDataWithinMapRect(rect, zoomScale: 1, cellSize: 10) 108 | XCTAssertEqual(unclusteredNodes.count, 4, "there should be four clusters in this data set") 109 | guard let cluster = unclusteredNodes.first else { 110 | XCTFail("First node should be non nil") 111 | return 112 | } 113 | let nodeCenter: MKMapPoint 114 | switch cluster { 115 | case .multiple(nodes: let nodes): 116 | XCTAssertEqual(nodes.count, 100, "there should be 100 nodes in the cluster") 117 | let reducedNodeCenter = nodes.reduce(MKMapPoint()) { total, current in 118 | return MKMapPoint(x: total.x + current.coordinate.x, y: total.y + current.coordinate.y) 119 | } 120 | nodeCenter = MKMapPoint(x: reducedNodeCenter.x / Double(nodes.count), 121 | y: reducedNodeCenter.y / Double(nodes.count)) 122 | case .single(node: _): 123 | XCTFail("node should be a multiple node") 124 | return 125 | } 126 | XCTAssertEqualWithAccuracy(nodeCenter.x, 4.5, accuracy: 0.0001) 127 | XCTAssertEqualWithAccuracy(nodeCenter.y, 4.5, accuracy: 0.0001) 128 | XCTAssertEqualWithAccuracy(nodeCenter.x, cluster.originCoordinate.x, accuracy: 0.0001) 129 | XCTAssertEqualWithAccuracy(nodeCenter.x, cluster.originCoordinate.y, accuracy: 0.0001) 130 | XCTAssertEqualWithAccuracy(cluster.contentRect.minX, 0, accuracy: 0.001) 131 | XCTAssertEqualWithAccuracy(cluster.contentRect.minY, 0, accuracy: 0.001) 132 | XCTAssertEqualWithAccuracy(cluster.contentRect.maxX, 9, accuracy: 0.001) 133 | XCTAssertEqualWithAccuracy(cluster.contentRect.maxY, 9, accuracy: 0.001) 134 | } 135 | 136 | } 137 | -------------------------------------------------------------------------------- /BentoMapTests/BentoBoxTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BentoMapTests.swift 3 | // BentoMapTests 4 | // 5 | // Created by Michael Skiba on 7/5/16. 6 | // Copyright © 2016 Raizlabs. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import MapKit 11 | @testable import BentoMap 12 | 13 | class MapKitTests: XCTestCase { 14 | 15 | override func setUp() { 16 | super.setUp() 17 | } 18 | 19 | override func tearDown() { 20 | super.tearDown() 21 | } 22 | 23 | func testBentoBoxCreation() { 24 | 25 | let root = MKMapRectWorld 26 | 27 | let rootNodeBentoBox = BentoBox(root: root) 28 | 29 | XCTAssert(MKMapRectEqualToRect(root, rootNodeBentoBox.root), "The bounding box's map rect should be equal to the input map rect") 30 | 31 | let maxCoord = CLLocationCoordinate2D(latitude: 30, longitude: 60) 32 | let minCoord = CLLocationCoordinate2D(latitude: 20, longitude: 40) 33 | 34 | let coordBentoBox = BentoBox(minPoint: minCoord, maxPoint: maxCoord) 35 | 36 | let minLat = CGFloat(min(minCoord.coordX, maxCoord.coordX)) 37 | XCTAssertEqualWithAccuracy(coordBentoBox.root.minX, minLat, accuracy: 0.001, "The bounding box's min latitude \(coordBentoBox.root.minX) should equal the smallest latitude passed in \(minLat)") 38 | 39 | let maxLat = max(minCoord.coordX, maxCoord.coordX) 40 | XCTAssertEqualWithAccuracy(coordBentoBox.maxCoordinate.coordX, maxLat, accuracy: 0.001, "The bounding box's max latitude \(coordBentoBox.maxCoordinate.coordX) should equal the largest latitude passed in \(maxLat)") 41 | 42 | let minLong = min(minCoord.coordY, maxCoord.coordY) 43 | XCTAssertEqualWithAccuracy(coordBentoBox.minCoordinate.coordY, minLong, accuracy: 0.001, "The bounding box's min longitude \(coordBentoBox.minCoordinate.coordY) should equal the smallest longitude passed in \(minLong)") 44 | 45 | let maxLong = max(minCoord.coordY, maxCoord.coordY) 46 | XCTAssertEqualWithAccuracy(coordBentoBox.maxCoordinate.coordY, maxLong, accuracy: 0.001, "The bounding box's max longitude \(coordBentoBox.minCoordinate.coordY) should equal the largest longitude passed in \(minLong)") 47 | } 48 | 49 | func testBentoBoxCoordinates() { 50 | let coordBentoBox = BentoBox(minPoint: CLLocationCoordinate2D(latitude: 30, longitude: 60), 51 | maxPoint: CLLocationCoordinate2D(latitude: 20, longitude: 40)) 52 | 53 | let maxCoordinate = MKMapPoint(x: Double(coordBentoBox.maxCoordinate.coordX), y: Double(coordBentoBox.maxCoordinate.coordY)) 54 | let minCoordinate = MKMapPoint(x: Double(coordBentoBox.minCoordinate.coordX), y: Double(coordBentoBox.minCoordinate.coordY)) 55 | 56 | // the min or higher is inside the bounding box 57 | XCTAssert(coordBentoBox.containsCoordinate(minCoordinate.offset(latitude: 0.1, longitude: 0.1))) 58 | XCTAssert(coordBentoBox.containsCoordinate(minCoordinate.offset(longitude: 0.1))) 59 | XCTAssert(coordBentoBox.containsCoordinate(minCoordinate.offset(latitude: 0.1))) 60 | XCTAssert(coordBentoBox.containsCoordinate(minCoordinate)) 61 | // slightly lower than the map is inside the mounding box 62 | XCTAssert(coordBentoBox.containsCoordinate(maxCoordinate.offset(latitude: -0.1, longitude: -0.1))) 63 | // the max or higher is outside the bounding box 64 | XCTAssertFalse(coordBentoBox.containsCoordinate(maxCoordinate)) 65 | XCTAssertFalse(coordBentoBox.containsCoordinate(maxCoordinate.offset(longitude: -0.1))) 66 | XCTAssertFalse(coordBentoBox.containsCoordinate(maxCoordinate.offset(latitude: -0.1))) 67 | XCTAssertFalse(coordBentoBox.containsCoordinate(maxCoordinate.offset(latitude: 0.1, longitude: 0.1))) 68 | XCTAssertFalse(coordBentoBox.containsCoordinate(maxCoordinate.offset(longitude: 0.1))) 69 | XCTAssertFalse(coordBentoBox.containsCoordinate(maxCoordinate.offset(latitude: 0.1))) 70 | // lower than the min is outside the bounding box 71 | XCTAssertFalse(coordBentoBox.containsCoordinate(minCoordinate.offset(latitude: -0.1, longitude: -0.1))) 72 | XCTAssertFalse(coordBentoBox.containsCoordinate(minCoordinate.offset(longitude: -0.1))) 73 | XCTAssertFalse(coordBentoBox.containsCoordinate(minCoordinate.offset(latitude: -0.1))) 74 | 75 | // the middle point is inside the bounding box 76 | let midCoord = MKMapPoint(x: Double(maxCoordinate.coordX + minCoordinate.coordX) / 2.0, 77 | y: Double(maxCoordinate.coordY + minCoordinate.coordY) / 2.0) 78 | XCTAssert(coordBentoBox.containsCoordinate(midCoord)) 79 | } 80 | 81 | func testBentoBoxIntersection() { 82 | let rect = MKMapRectMake(0, 0, 500, 500) 83 | let intersectingRect = MKMapRectMake(250, 250, 500, 500) 84 | let nonIntersectingRect = MKMapRectMake(500, 0, 500, 500) 85 | 86 | let bentoBox = BentoBox(root: rect) 87 | let intersectingBox = BentoBox(root: intersectingRect) 88 | let nonIntersectingBox = BentoBox(root: nonIntersectingRect) 89 | 90 | XCTAssert(bentoBox.intersectsBentoBox(intersectingBox)) 91 | XCTAssertFalse(bentoBox.intersectsBentoBox(nonIntersectingBox)) 92 | } 93 | 94 | func testQuadrants() { 95 | let baseRect = MKMapRectMake(0, 0, 500, 500) 96 | let nwRect = MKMapRectMake(0, 0, 250, 250) 97 | let neRect = MKMapRectMake(250, 0, 250, 250) 98 | let swRect = MKMapRectMake(0, 250, 250, 250) 99 | let seRect = MKMapRectMake(250, 250, 250, 250) 100 | 101 | let quadrants = BentoBox(root: baseRect).quadrants 102 | 103 | XCTAssert(MKMapRectEqualToRect(quadrants.northWest.root, nwRect)) 104 | XCTAssert(MKMapRectEqualToRect(quadrants.northEast.root, neRect)) 105 | XCTAssert(MKMapRectEqualToRect(quadrants.southWest.root, swRect)) 106 | XCTAssert(MKMapRectEqualToRect(quadrants.southEast.root, seRect)) 107 | } 108 | 109 | func testCollectionTypeExtensions() { 110 | let points = [ 111 | MKMapPoint(x: 25.0, y: 10956.7), 112 | MKMapPoint(x: 1894.5, y: 22897.5550), 113 | MKMapPoint(x: 25278.445, y: 156.339), 114 | MKMapPoint(x: 17603.8472, y: 2456.7), 115 | ] 116 | 117 | let mapPointBentoBox: BentoBox = points.bentoBox() 118 | 119 | let minCoordinate = mapPointBentoBox.minCoordinate 120 | let maxCoordinate = mapPointBentoBox.maxCoordinate 121 | 122 | XCTAssert(MKMapPointEqualToPoint(minCoordinate, MKMapPoint(x: 25.0, y: 156.339))) 123 | XCTAssert(MKMapPointEqualToPoint(maxCoordinate, MKMapPoint(x: 25278.445, y: 22897.5550))) 124 | 125 | let coords = [ 126 | CLLocationCoordinate2D(latitude: 34.6790, longitude: 28.2847), 127 | CLLocationCoordinate2D(latitude: -34.6790, longitude: 88.1349), 128 | CLLocationCoordinate2D(latitude: 61.9471, longitude: -14.1887), 129 | CLLocationCoordinate2D(latitude: 1.2898, longitude: 42.4277), 130 | ] 131 | 132 | let coordinateBentoBox: BentoBox = coords.bentoBox() 133 | 134 | XCTAssertEqualWithAccuracy(coordinateBentoBox.minCoordinate.coordX, -34.6790, accuracy: 1e-4) 135 | XCTAssertEqualWithAccuracy(coordinateBentoBox.minCoordinate.coordY, -14.1887, accuracy: 1e-4) 136 | XCTAssertEqualWithAccuracy(coordinateBentoBox.maxCoordinate.coordX, 61.9471, accuracy: 1e-4) 137 | XCTAssertEqualWithAccuracy(coordinateBentoBox.maxCoordinate.coordY, 88.1349, accuracy: 1e-4) 138 | } 139 | } 140 | 141 | class CoreGraphicsTests: XCTestCase { 142 | 143 | override func setUp() { 144 | super.setUp() 145 | } 146 | 147 | override func tearDown() { 148 | super.tearDown() 149 | } 150 | 151 | func testBentoBoxCreationForCGRect() { 152 | 153 | let origin = CGPoint.zero 154 | let gridSize = CGSize(width: 1000.0, height: 1000.0) 155 | let root = CGRect(origin: origin, size: gridSize) 156 | 157 | let rootNodeBentoBox = BentoBox(root: root) 158 | 159 | XCTAssertEqual(root, rootNodeBentoBox.root, "The bounding box's map rect should be equal to the input map rect") 160 | 161 | let maxCoord = CGPoint(x: 30.0, y: 60.0) 162 | let minCoord = CGPoint(x: 20.0, y: 40.0) 163 | 164 | let coordBentoBox = BentoBox(minPoint: minCoord, maxPoint: maxCoord) 165 | 166 | let minLat = CGFloat(min(minCoord.coordX, maxCoord.coordX)) 167 | XCTAssertEqualWithAccuracy(coordBentoBox.root.minX, minLat, accuracy: 0.001, "The bounding box's min x \(coordBentoBox.root.minX) should equal the smallest x passed in \(minLat)") 168 | 169 | let maxLat = max(minCoord.coordX, maxCoord.coordX) 170 | XCTAssertEqualWithAccuracy(coordBentoBox.maxCoordinate.coordX, maxLat, accuracy: 0.001, "The bounding box's max x \(coordBentoBox.maxCoordinate.coordX) should equal the largest x passed in \(maxLat)") 171 | 172 | let minLong = min(minCoord.coordY, maxCoord.coordY) 173 | XCTAssertEqualWithAccuracy(coordBentoBox.minCoordinate.coordY, minLong, accuracy: 0.001, "The bounding box's min y \(coordBentoBox.minCoordinate.coordY) should equal the smallest y passed in \(minLong)") 174 | 175 | let maxLong = max(minCoord.coordY, maxCoord.coordY) 176 | XCTAssertEqualWithAccuracy(coordBentoBox.maxCoordinate.coordY, maxLong, accuracy: 0.001, "The bounding box's max y \(coordBentoBox.minCoordinate.coordY) should equal the largest y passed in \(minLong)") 177 | } 178 | 179 | func testBentoBoxCoordinates() { 180 | let coordBentoBox = BentoBox(minPoint: CGPoint(x: 30, y: 60), 181 | maxPoint: CGPoint(x: 20, y: 40)) 182 | 183 | let maxCoordinate = CGPoint(x: coordBentoBox.maxCoordinate.coordX, y: coordBentoBox.maxCoordinate.coordY) 184 | let minCoordinate = CGPoint(x: coordBentoBox.minCoordinate.coordX, y: coordBentoBox.minCoordinate.coordY) 185 | 186 | // the min or higher is inside the bounding box 187 | XCTAssert(coordBentoBox.containsCoordinate(minCoordinate.offset(x: 0.1, y: 0.1))) 188 | XCTAssert(coordBentoBox.containsCoordinate(minCoordinate.offset(y: 0.1))) 189 | XCTAssert(coordBentoBox.containsCoordinate(minCoordinate.offset(x: 0.1))) 190 | XCTAssert(coordBentoBox.containsCoordinate(minCoordinate)) 191 | // slightly lower than the map is inside the mounding box 192 | XCTAssert(coordBentoBox.containsCoordinate(maxCoordinate.offset(x: -0.1, y: -0.1))) 193 | // the max or higher is outside the bounding box 194 | XCTAssertFalse(coordBentoBox.containsCoordinate(maxCoordinate)) 195 | XCTAssertFalse(coordBentoBox.containsCoordinate(maxCoordinate.offset(y: -0.1))) 196 | XCTAssertFalse(coordBentoBox.containsCoordinate(maxCoordinate.offset(x: -0.1))) 197 | XCTAssertFalse(coordBentoBox.containsCoordinate(maxCoordinate.offset(x: 0.1, y: 0.1))) 198 | XCTAssertFalse(coordBentoBox.containsCoordinate(maxCoordinate.offset(y: 0.1))) 199 | XCTAssertFalse(coordBentoBox.containsCoordinate(maxCoordinate.offset(x: 0.1))) 200 | // lower than the min is outside the bounding box 201 | XCTAssertFalse(coordBentoBox.containsCoordinate(minCoordinate.offset(x: -0.1, y: -0.1))) 202 | XCTAssertFalse(coordBentoBox.containsCoordinate(minCoordinate.offset(y: -0.1))) 203 | XCTAssertFalse(coordBentoBox.containsCoordinate(minCoordinate.offset(x: -0.1))) 204 | 205 | // the middle point is inside the bounding box 206 | let midCoord = CGPoint(x: (maxCoordinate.coordX + minCoordinate.coordX) / 2.0, 207 | y: (maxCoordinate.coordY + minCoordinate.coordY) / 2.0) 208 | XCTAssert(coordBentoBox.containsCoordinate(midCoord)) 209 | } 210 | 211 | func testBentoBoxIntersection() { 212 | let rect = CGRect(x: 0, y: 0, width: 500, height: 500) 213 | let intersectingRect = CGRect(x: 250, y: 250, width: 500, height: 500) 214 | let nonIntersectingRect = CGRect(x: 500, y: 0, width: 500, height: 500) 215 | 216 | let bentoBox = BentoBox(root: rect) 217 | let intersectingBox = BentoBox(root: intersectingRect) 218 | let nonIntersectingBox = BentoBox(root: nonIntersectingRect) 219 | 220 | XCTAssert(bentoBox.intersectsBentoBox(intersectingBox)) 221 | XCTAssertFalse(bentoBox.intersectsBentoBox(nonIntersectingBox)) 222 | } 223 | 224 | func testQuadrants() { 225 | let baseRect = CGRect(x: 0, y: 0, width: 500, height: 500) 226 | let nwRect = CGRect(x: 0, y: 0, width: 250, height: 250) 227 | let neRect = CGRect(x: 250, y: 0, width: 250, height: 250) 228 | let swRect = CGRect(x: 0, y: 250, width: 250, height: 250) 229 | let seRect = CGRect(x: 250, y: 250, width: 250, height: 250) 230 | 231 | let quadrants = BentoBox(root: baseRect).quadrants 232 | 233 | XCTAssert(quadrants.northWest.root.equalTo(nwRect)) 234 | XCTAssert(quadrants.northEast.root.equalTo(neRect)) 235 | XCTAssert(quadrants.southWest.root.equalTo(swRect)) 236 | XCTAssert(quadrants.southEast.root.equalTo(seRect)) 237 | } 238 | 239 | func testCollectionTypeExtensions() { 240 | let points = [ 241 | CGPoint(x: 25.0, y: 10956.7), 242 | CGPoint(x: 1894.5, y: 22897.5550), 243 | CGPoint(x: 25278.445, y: 156.339), 244 | CGPoint(x: 17603.8472, y: 2456.7), 245 | ] 246 | 247 | let mapPointBentoBox: BentoBox = points.bentoBox() 248 | 249 | let minCoordinate = mapPointBentoBox.minCoordinate 250 | let maxCoordinate = mapPointBentoBox.maxCoordinate 251 | 252 | XCTAssert(minCoordinate.equalTo(CGPoint(x: 25.0, y: 156.339))) 253 | XCTAssert(maxCoordinate.equalTo(CGPoint(x: 25278.445, y: 22897.5550))) 254 | 255 | let coords = [ 256 | CGPoint(x: 34.6790, y: 28.2847), 257 | CGPoint(x: -34.6790, y: 88.1349), 258 | CGPoint(x: 61.9471, y: -14.1887), 259 | CGPoint(x: 1.2898, y: 42.4277), 260 | ] 261 | 262 | let coordinateBentoBox: BentoBox = coords.bentoBox() 263 | 264 | XCTAssertEqualWithAccuracy(coordinateBentoBox.minCoordinate.coordX, -34.6790, accuracy: 1e-4) 265 | XCTAssertEqualWithAccuracy(coordinateBentoBox.minCoordinate.coordY, -14.1887, accuracy: 1e-4) 266 | XCTAssertEqualWithAccuracy(coordinateBentoBox.maxCoordinate.coordX, 61.9471, accuracy: 1e-4) 267 | XCTAssertEqualWithAccuracy(coordinateBentoBox.maxCoordinate.coordY, 88.1349, accuracy: 1e-4) 268 | } 269 | } 270 | 271 | private extension MKMapPoint { 272 | func offset(latitude latitudeOffset: Double = 0, longitude longitudeOffset: Double = 0) -> MKMapPoint { 273 | return MKMapPoint(x: x + latitudeOffset, y: y + longitudeOffset) 274 | } 275 | } 276 | 277 | private extension CGPoint { 278 | func offset(x xOffset: CGFloat = 0.0, y yOffset: CGFloat = 0.0) -> CGPoint { 279 | return CGPoint(x: x + xOffset, y: y + yOffset) 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /BentoMap.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 7228026E1D2E9D9100C637A6 /* Annotations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7228026D1D2E9D9100C637A6 /* Annotations.swift */; }; 11 | 723753081D2D9D6D002A1BC2 /* BentoMap.h in Headers */ = {isa = PBXBuildFile; fileRef = 723753071D2D9D6D002A1BC2 /* BentoMap.h */; settings = {ATTRIBUTES = (Public, ); }; }; 12 | 7237530F1D2D9D6D002A1BC2 /* BentoMap.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 723753041D2D9D6D002A1BC2 /* BentoMap.framework */; }; 13 | 723753251D2D9DA2002A1BC2 /* BentoBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7237531E1D2D9DA2002A1BC2 /* BentoBox.swift */; }; 14 | 723753261D2D9DA2002A1BC2 /* Box.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7237531F1D2D9DA2002A1BC2 /* Box.swift */; }; 15 | 723753271D2D9DA2002A1BC2 /* OrdinalNodes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 723753201D2D9DA2002A1BC2 /* OrdinalNodes.swift */; }; 16 | 723753281D2D9DA2002A1BC2 /* QuadrantWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 723753211D2D9DA2002A1BC2 /* QuadrantWrapper.swift */; }; 17 | 723753291D2D9DA2002A1BC2 /* QuadTree.swift in Sources */ = {isa = PBXBuildFile; fileRef = 723753221D2D9DA2002A1BC2 /* QuadTree.swift */; }; 18 | 7237532A1D2D9DA2002A1BC2 /* QuadTreeNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 723753231D2D9DA2002A1BC2 /* QuadTreeNode.swift */; }; 19 | 7237532B1D2D9DA2002A1BC2 /* QuadTreeResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 723753241D2D9DA2002A1BC2 /* QuadTreeResult.swift */; }; 20 | 7237532E1D2D9DB5002A1BC2 /* BentoBoxTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7237532C1D2D9DB5002A1BC2 /* BentoBoxTests.swift */; }; 21 | 7237532F1D2D9DB5002A1BC2 /* QuadTreeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7237532D1D2D9DB5002A1BC2 /* QuadTreeTests.swift */; }; 22 | 723753391D2D9E75002A1BC2 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 723753381D2D9E75002A1BC2 /* AppDelegate.swift */; }; 23 | 7237533B1D2D9E75002A1BC2 /* MapKitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7237533A1D2D9E75002A1BC2 /* MapKitViewController.swift */; }; 24 | 723753401D2D9E75002A1BC2 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7237533F1D2D9E75002A1BC2 /* Assets.xcassets */; }; 25 | 723753431D2D9E75002A1BC2 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 723753411D2D9E75002A1BC2 /* LaunchScreen.storyboard */; }; 26 | 723753481D2D9E80002A1BC2 /* BentoMap.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 723753041D2D9D6D002A1BC2 /* BentoMap.framework */; }; 27 | 723753491D2D9E80002A1BC2 /* BentoMap.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 723753041D2D9D6D002A1BC2 /* BentoMap.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 28 | 7237534E1D2DA0CF002A1BC2 /* QuadTree+SampleData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7237534D1D2DA0CF002A1BC2 /* QuadTree+SampleData.swift */; }; 29 | 72383AB51D2ECAD00071EDE2 /* OptionalOr.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72383AB41D2ECAD00071EDE2 /* OptionalOr.swift */; }; 30 | 72383AB71D2ECAF70071EDE2 /* OptionalOrTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72383AB61D2ECAF70071EDE2 /* OptionalOrTests.swift */; }; 31 | 72D679441E252AD40032902B /* AnnotationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72D679431E252AD40032902B /* AnnotationView.swift */; }; 32 | B50426AF1D71D710008BEF76 /* CGRect+BentoRect.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50426AE1D71D710008BEF76 /* CGRect+BentoRect.swift */; }; 33 | B50426B41D71F35C008BEF76 /* MKMapPoint+BentoCoordinate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50426B31D71F35C008BEF76 /* MKMapPoint+BentoCoordinate.swift */; }; 34 | B50426B61D71F775008BEF76 /* CGPoint+BentoCoordinate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50426B51D71F775008BEF76 /* CGPoint+BentoCoordinate.swift */; }; 35 | B50426B81D71FA21008BEF76 /* MKMapRect+BentoRect.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50426B71D71FA21008BEF76 /* MKMapRect+BentoRect.swift */; }; 36 | B50426BA1D7203C9008BEF76 /* CLLocationCoordinate2D+BentoCoordinate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50426B91D7203C9008BEF76 /* CLLocationCoordinate2D+BentoCoordinate.swift */; }; 37 | B539B33E1D7E35D60089AFE6 /* CoreGraphicsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B539B33D1D7E35D60089AFE6 /* CoreGraphicsViewController.swift */; }; 38 | B539B3401D7E35ED0089AFE6 /* MainMenuTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B539B33F1D7E35ED0089AFE6 /* MainMenuTableViewController.swift */; }; 39 | D4C3839B1D4F9DD500556EBE /* CollectionTypeExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4C3839A1D4F9DD500556EBE /* CollectionTypeExtensions.swift */; }; 40 | /* End PBXBuildFile section */ 41 | 42 | /* Begin PBXContainerItemProxy section */ 43 | 723753101D2D9D6D002A1BC2 /* PBXContainerItemProxy */ = { 44 | isa = PBXContainerItemProxy; 45 | containerPortal = 723752FB1D2D9D6D002A1BC2 /* Project object */; 46 | proxyType = 1; 47 | remoteGlobalIDString = 723753031D2D9D6D002A1BC2; 48 | remoteInfo = BentoMap; 49 | }; 50 | 7237534A1D2D9E80002A1BC2 /* PBXContainerItemProxy */ = { 51 | isa = PBXContainerItemProxy; 52 | containerPortal = 723752FB1D2D9D6D002A1BC2 /* Project object */; 53 | proxyType = 1; 54 | remoteGlobalIDString = 723753031D2D9D6D002A1BC2; 55 | remoteInfo = BentoMap; 56 | }; 57 | /* End PBXContainerItemProxy section */ 58 | 59 | /* Begin PBXCopyFilesBuildPhase section */ 60 | 7237534C1D2D9E80002A1BC2 /* Embed Frameworks */ = { 61 | isa = PBXCopyFilesBuildPhase; 62 | buildActionMask = 2147483647; 63 | dstPath = ""; 64 | dstSubfolderSpec = 10; 65 | files = ( 66 | 723753491D2D9E80002A1BC2 /* BentoMap.framework in Embed Frameworks */, 67 | ); 68 | name = "Embed Frameworks"; 69 | runOnlyForDeploymentPostprocessing = 0; 70 | }; 71 | /* End PBXCopyFilesBuildPhase section */ 72 | 73 | /* Begin PBXFileReference section */ 74 | 7228026D1D2E9D9100C637A6 /* Annotations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = Annotations.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 75 | 723753041D2D9D6D002A1BC2 /* BentoMap.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = BentoMap.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 76 | 723753071D2D9D6D002A1BC2 /* BentoMap.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = BentoMap.h; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; 77 | 723753091D2D9D6D002A1BC2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 78 | 7237530E1D2D9D6D002A1BC2 /* BentoMapTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BentoMapTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 79 | 723753151D2D9D6D002A1BC2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 80 | 7237531E1D2D9DA2002A1BC2 /* BentoBox.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = BentoBox.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 81 | 7237531F1D2D9DA2002A1BC2 /* Box.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = Box.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 82 | 723753201D2D9DA2002A1BC2 /* OrdinalNodes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = OrdinalNodes.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 83 | 723753211D2D9DA2002A1BC2 /* QuadrantWrapper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = QuadrantWrapper.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 84 | 723753221D2D9DA2002A1BC2 /* QuadTree.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = QuadTree.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 85 | 723753231D2D9DA2002A1BC2 /* QuadTreeNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = QuadTreeNode.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 86 | 723753241D2D9DA2002A1BC2 /* QuadTreeResult.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = QuadTreeResult.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 87 | 7237532C1D2D9DB5002A1BC2 /* BentoBoxTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = BentoBoxTests.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 88 | 7237532D1D2D9DB5002A1BC2 /* QuadTreeTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QuadTreeTests.swift; sourceTree = ""; }; 89 | 723753361D2D9E75002A1BC2 /* BentoMapExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = BentoMapExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 90 | 723753381D2D9E75002A1BC2 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 91 | 7237533A1D2D9E75002A1BC2 /* MapKitViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapKitViewController.swift; sourceTree = ""; }; 92 | 7237533F1D2D9E75002A1BC2 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 93 | 723753421D2D9E75002A1BC2 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 94 | 723753441D2D9E75002A1BC2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 95 | 7237534D1D2DA0CF002A1BC2 /* QuadTree+SampleData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = "QuadTree+SampleData.swift"; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 96 | 72383AB41D2ECAD00071EDE2 /* OptionalOr.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = OptionalOr.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 97 | 72383AB61D2ECAF70071EDE2 /* OptionalOrTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = OptionalOrTests.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 98 | 72D679431E252AD40032902B /* AnnotationView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnnotationView.swift; sourceTree = ""; }; 99 | B50426AE1D71D710008BEF76 /* CGRect+BentoRect.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = "CGRect+BentoRect.swift"; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 100 | B50426B31D71F35C008BEF76 /* MKMapPoint+BentoCoordinate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = "MKMapPoint+BentoCoordinate.swift"; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 101 | B50426B51D71F775008BEF76 /* CGPoint+BentoCoordinate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = "CGPoint+BentoCoordinate.swift"; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 102 | B50426B71D71FA21008BEF76 /* MKMapRect+BentoRect.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = "MKMapRect+BentoRect.swift"; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 103 | B50426B91D7203C9008BEF76 /* CLLocationCoordinate2D+BentoCoordinate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = "CLLocationCoordinate2D+BentoCoordinate.swift"; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 104 | B539B33D1D7E35D60089AFE6 /* CoreGraphicsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = CoreGraphicsViewController.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 105 | B539B33F1D7E35ED0089AFE6 /* MainMenuTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = MainMenuTableViewController.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 106 | D4C3839A1D4F9DD500556EBE /* CollectionTypeExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = CollectionTypeExtensions.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 107 | /* End PBXFileReference section */ 108 | 109 | /* Begin PBXFrameworksBuildPhase section */ 110 | 723753001D2D9D6D002A1BC2 /* Frameworks */ = { 111 | isa = PBXFrameworksBuildPhase; 112 | buildActionMask = 2147483647; 113 | files = ( 114 | ); 115 | runOnlyForDeploymentPostprocessing = 0; 116 | }; 117 | 7237530B1D2D9D6D002A1BC2 /* Frameworks */ = { 118 | isa = PBXFrameworksBuildPhase; 119 | buildActionMask = 2147483647; 120 | files = ( 121 | 7237530F1D2D9D6D002A1BC2 /* BentoMap.framework in Frameworks */, 122 | ); 123 | runOnlyForDeploymentPostprocessing = 0; 124 | }; 125 | 723753331D2D9E75002A1BC2 /* Frameworks */ = { 126 | isa = PBXFrameworksBuildPhase; 127 | buildActionMask = 2147483647; 128 | files = ( 129 | 723753481D2D9E80002A1BC2 /* BentoMap.framework in Frameworks */, 130 | ); 131 | runOnlyForDeploymentPostprocessing = 0; 132 | }; 133 | /* End PBXFrameworksBuildPhase section */ 134 | 135 | /* Begin PBXGroup section */ 136 | 723752FA1D2D9D6D002A1BC2 = { 137 | isa = PBXGroup; 138 | children = ( 139 | 723753061D2D9D6D002A1BC2 /* BentoMap */, 140 | 723753371D2D9E75002A1BC2 /* BentoMapExample */, 141 | 723753121D2D9D6D002A1BC2 /* BentoMapTests */, 142 | 723753051D2D9D6D002A1BC2 /* Products */, 143 | ); 144 | sourceTree = ""; 145 | }; 146 | 723753051D2D9D6D002A1BC2 /* Products */ = { 147 | isa = PBXGroup; 148 | children = ( 149 | 723753041D2D9D6D002A1BC2 /* BentoMap.framework */, 150 | 7237530E1D2D9D6D002A1BC2 /* BentoMapTests.xctest */, 151 | 723753361D2D9E75002A1BC2 /* BentoMapExample.app */, 152 | ); 153 | name = Products; 154 | sourceTree = ""; 155 | }; 156 | 723753061D2D9D6D002A1BC2 /* BentoMap */ = { 157 | isa = PBXGroup; 158 | children = ( 159 | B50426B21D71F2C8008BEF76 /* Extensions */, 160 | 723E206E1D2EBB9C00CF44FA /* Resources */, 161 | 7237531E1D2D9DA2002A1BC2 /* BentoBox.swift */, 162 | 7237531F1D2D9DA2002A1BC2 /* Box.swift */, 163 | 72383AB41D2ECAD00071EDE2 /* OptionalOr.swift */, 164 | 723753201D2D9DA2002A1BC2 /* OrdinalNodes.swift */, 165 | 723753221D2D9DA2002A1BC2 /* QuadTree.swift */, 166 | 723753231D2D9DA2002A1BC2 /* QuadTreeNode.swift */, 167 | 723753241D2D9DA2002A1BC2 /* QuadTreeResult.swift */, 168 | 723753211D2D9DA2002A1BC2 /* QuadrantWrapper.swift */, 169 | ); 170 | path = BentoMap; 171 | sourceTree = ""; 172 | }; 173 | 723753121D2D9D6D002A1BC2 /* BentoMapTests */ = { 174 | isa = PBXGroup; 175 | children = ( 176 | 723E20731D2EC00400CF44FA /* Resources */, 177 | 7237532C1D2D9DB5002A1BC2 /* BentoBoxTests.swift */, 178 | 72383AB61D2ECAF70071EDE2 /* OptionalOrTests.swift */, 179 | 7237532D1D2D9DB5002A1BC2 /* QuadTreeTests.swift */, 180 | ); 181 | path = BentoMapTests; 182 | sourceTree = ""; 183 | }; 184 | 723753371D2D9E75002A1BC2 /* BentoMapExample */ = { 185 | isa = PBXGroup; 186 | children = ( 187 | 723E20701D2EBBD000CF44FA /* Annotations */, 188 | 723E20721D2EBFF500CF44FA /* App */, 189 | 723E206F1D2EBBC000CF44FA /* App Delegate */, 190 | 723E20711D2EBC9200CF44FA /* Assets */, 191 | ); 192 | path = BentoMapExample; 193 | sourceTree = ""; 194 | }; 195 | 723E206E1D2EBB9C00CF44FA /* Resources */ = { 196 | isa = PBXGroup; 197 | children = ( 198 | 723753071D2D9D6D002A1BC2 /* BentoMap.h */, 199 | 723753091D2D9D6D002A1BC2 /* Info.plist */, 200 | ); 201 | path = Resources; 202 | sourceTree = ""; 203 | }; 204 | 723E206F1D2EBBC000CF44FA /* App Delegate */ = { 205 | isa = PBXGroup; 206 | children = ( 207 | 723753381D2D9E75002A1BC2 /* AppDelegate.swift */, 208 | ); 209 | path = "App Delegate"; 210 | sourceTree = ""; 211 | }; 212 | 723E20701D2EBBD000CF44FA /* Annotations */ = { 213 | isa = PBXGroup; 214 | children = ( 215 | 7228026D1D2E9D9100C637A6 /* Annotations.swift */, 216 | 72D679431E252AD40032902B /* AnnotationView.swift */, 217 | ); 218 | path = Annotations; 219 | sourceTree = ""; 220 | }; 221 | 723E20711D2EBC9200CF44FA /* Assets */ = { 222 | isa = PBXGroup; 223 | children = ( 224 | 7237533F1D2D9E75002A1BC2 /* Assets.xcassets */, 225 | 723753441D2D9E75002A1BC2 /* Info.plist */, 226 | 723753411D2D9E75002A1BC2 /* LaunchScreen.storyboard */, 227 | ); 228 | path = Assets; 229 | sourceTree = ""; 230 | }; 231 | 723E20721D2EBFF500CF44FA /* App */ = { 232 | isa = PBXGroup; 233 | children = ( 234 | B539B33D1D7E35D60089AFE6 /* CoreGraphicsViewController.swift */, 235 | B539B33F1D7E35ED0089AFE6 /* MainMenuTableViewController.swift */, 236 | 7237533A1D2D9E75002A1BC2 /* MapKitViewController.swift */, 237 | 7237534D1D2DA0CF002A1BC2 /* QuadTree+SampleData.swift */, 238 | ); 239 | path = App; 240 | sourceTree = ""; 241 | }; 242 | 723E20731D2EC00400CF44FA /* Resources */ = { 243 | isa = PBXGroup; 244 | children = ( 245 | 723753151D2D9D6D002A1BC2 /* Info.plist */, 246 | ); 247 | path = Resources; 248 | sourceTree = ""; 249 | }; 250 | B50426B21D71F2C8008BEF76 /* Extensions */ = { 251 | isa = PBXGroup; 252 | children = ( 253 | B50426B51D71F775008BEF76 /* CGPoint+BentoCoordinate.swift */, 254 | B50426AE1D71D710008BEF76 /* CGRect+BentoRect.swift */, 255 | B50426B91D7203C9008BEF76 /* CLLocationCoordinate2D+BentoCoordinate.swift */, 256 | D4C3839A1D4F9DD500556EBE /* CollectionTypeExtensions.swift */, 257 | B50426B31D71F35C008BEF76 /* MKMapPoint+BentoCoordinate.swift */, 258 | B50426B71D71FA21008BEF76 /* MKMapRect+BentoRect.swift */, 259 | ); 260 | path = Extensions; 261 | sourceTree = ""; 262 | }; 263 | /* End PBXGroup section */ 264 | 265 | /* Begin PBXHeadersBuildPhase section */ 266 | 723753011D2D9D6D002A1BC2 /* Headers */ = { 267 | isa = PBXHeadersBuildPhase; 268 | buildActionMask = 2147483647; 269 | files = ( 270 | 723753081D2D9D6D002A1BC2 /* BentoMap.h in Headers */, 271 | ); 272 | runOnlyForDeploymentPostprocessing = 0; 273 | }; 274 | /* End PBXHeadersBuildPhase section */ 275 | 276 | /* Begin PBXNativeTarget section */ 277 | 723753031D2D9D6D002A1BC2 /* BentoMap */ = { 278 | isa = PBXNativeTarget; 279 | buildConfigurationList = 723753181D2D9D6D002A1BC2 /* Build configuration list for PBXNativeTarget "BentoMap" */; 280 | buildPhases = ( 281 | 72B69E881DE5EB7D00272BC1 /* SwiftLint */, 282 | 723752FF1D2D9D6D002A1BC2 /* Sources */, 283 | 723753001D2D9D6D002A1BC2 /* Frameworks */, 284 | 723753011D2D9D6D002A1BC2 /* Headers */, 285 | 723753021D2D9D6D002A1BC2 /* Resources */, 286 | ); 287 | buildRules = ( 288 | ); 289 | dependencies = ( 290 | ); 291 | name = BentoMap; 292 | productName = BentoMap; 293 | productReference = 723753041D2D9D6D002A1BC2 /* BentoMap.framework */; 294 | productType = "com.apple.product-type.framework"; 295 | }; 296 | 7237530D1D2D9D6D002A1BC2 /* BentoMapTests */ = { 297 | isa = PBXNativeTarget; 298 | buildConfigurationList = 7237531B1D2D9D6D002A1BC2 /* Build configuration list for PBXNativeTarget "BentoMapTests" */; 299 | buildPhases = ( 300 | 7237530A1D2D9D6D002A1BC2 /* Sources */, 301 | 7237530B1D2D9D6D002A1BC2 /* Frameworks */, 302 | 7237530C1D2D9D6D002A1BC2 /* Resources */, 303 | 723753311D2D9E26002A1BC2 /* SwiftLint */, 304 | ); 305 | buildRules = ( 306 | ); 307 | dependencies = ( 308 | 723753111D2D9D6D002A1BC2 /* PBXTargetDependency */, 309 | ); 310 | name = BentoMapTests; 311 | productName = BentoMapTests; 312 | productReference = 7237530E1D2D9D6D002A1BC2 /* BentoMapTests.xctest */; 313 | productType = "com.apple.product-type.bundle.unit-test"; 314 | }; 315 | 723753351D2D9E75002A1BC2 /* BentoMapExample */ = { 316 | isa = PBXNativeTarget; 317 | buildConfigurationList = 723753451D2D9E75002A1BC2 /* Build configuration list for PBXNativeTarget "BentoMapExample" */; 318 | buildPhases = ( 319 | 723753321D2D9E75002A1BC2 /* Sources */, 320 | 723753331D2D9E75002A1BC2 /* Frameworks */, 321 | 723753341D2D9E75002A1BC2 /* Resources */, 322 | 7237534C1D2D9E80002A1BC2 /* Embed Frameworks */, 323 | ); 324 | buildRules = ( 325 | ); 326 | dependencies = ( 327 | 7237534B1D2D9E80002A1BC2 /* PBXTargetDependency */, 328 | ); 329 | name = BentoMapExample; 330 | productName = BentoMapExample; 331 | productReference = 723753361D2D9E75002A1BC2 /* BentoMapExample.app */; 332 | productType = "com.apple.product-type.application"; 333 | }; 334 | /* End PBXNativeTarget section */ 335 | 336 | /* Begin PBXProject section */ 337 | 723752FB1D2D9D6D002A1BC2 /* Project object */ = { 338 | isa = PBXProject; 339 | attributes = { 340 | LastSwiftUpdateCheck = 0730; 341 | LastUpgradeCheck = 0810; 342 | ORGANIZATIONNAME = Raizlabs; 343 | TargetAttributes = { 344 | 723753031D2D9D6D002A1BC2 = { 345 | CreatedOnToolsVersion = 7.3.1; 346 | LastSwiftMigration = 0810; 347 | }; 348 | 7237530D1D2D9D6D002A1BC2 = { 349 | CreatedOnToolsVersion = 7.3.1; 350 | LastSwiftMigration = 0810; 351 | }; 352 | 723753351D2D9E75002A1BC2 = { 353 | CreatedOnToolsVersion = 7.3.1; 354 | LastSwiftMigration = 0810; 355 | }; 356 | }; 357 | }; 358 | buildConfigurationList = 723752FE1D2D9D6D002A1BC2 /* Build configuration list for PBXProject "BentoMap" */; 359 | compatibilityVersion = "Xcode 3.2"; 360 | developmentRegion = English; 361 | hasScannedForEncodings = 0; 362 | knownRegions = ( 363 | en, 364 | Base, 365 | ); 366 | mainGroup = 723752FA1D2D9D6D002A1BC2; 367 | productRefGroup = 723753051D2D9D6D002A1BC2 /* Products */; 368 | projectDirPath = ""; 369 | projectRoot = ""; 370 | targets = ( 371 | 723753031D2D9D6D002A1BC2 /* BentoMap */, 372 | 7237530D1D2D9D6D002A1BC2 /* BentoMapTests */, 373 | 723753351D2D9E75002A1BC2 /* BentoMapExample */, 374 | ); 375 | }; 376 | /* End PBXProject section */ 377 | 378 | /* Begin PBXResourcesBuildPhase section */ 379 | 723753021D2D9D6D002A1BC2 /* Resources */ = { 380 | isa = PBXResourcesBuildPhase; 381 | buildActionMask = 2147483647; 382 | files = ( 383 | ); 384 | runOnlyForDeploymentPostprocessing = 0; 385 | }; 386 | 7237530C1D2D9D6D002A1BC2 /* Resources */ = { 387 | isa = PBXResourcesBuildPhase; 388 | buildActionMask = 2147483647; 389 | files = ( 390 | ); 391 | runOnlyForDeploymentPostprocessing = 0; 392 | }; 393 | 723753341D2D9E75002A1BC2 /* Resources */ = { 394 | isa = PBXResourcesBuildPhase; 395 | buildActionMask = 2147483647; 396 | files = ( 397 | 723753431D2D9E75002A1BC2 /* LaunchScreen.storyboard in Resources */, 398 | 723753401D2D9E75002A1BC2 /* Assets.xcassets in Resources */, 399 | ); 400 | runOnlyForDeploymentPostprocessing = 0; 401 | }; 402 | /* End PBXResourcesBuildPhase section */ 403 | 404 | /* Begin PBXShellScriptBuildPhase section */ 405 | 723753311D2D9E26002A1BC2 /* SwiftLint */ = { 406 | isa = PBXShellScriptBuildPhase; 407 | buildActionMask = 2147483647; 408 | files = ( 409 | ); 410 | inputPaths = ( 411 | ); 412 | name = SwiftLint; 413 | outputPaths = ( 414 | ); 415 | runOnlyForDeploymentPostprocessing = 0; 416 | shellPath = /bin/sh; 417 | shellScript = "if which swiftlint >/dev/null; then\nswiftlint\nelse\necho \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi"; 418 | }; 419 | 72B69E881DE5EB7D00272BC1 /* SwiftLint */ = { 420 | isa = PBXShellScriptBuildPhase; 421 | buildActionMask = 2147483647; 422 | files = ( 423 | ); 424 | inputPaths = ( 425 | ); 426 | name = SwiftLint; 427 | outputPaths = ( 428 | ); 429 | runOnlyForDeploymentPostprocessing = 0; 430 | shellPath = /bin/sh; 431 | shellScript = "if which swiftlint >/dev/null; then\n swiftlint || echo \"warning: SwiftLint failed with exit code $?. Is SwiftLint installed and up to date?\"\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi"; 432 | }; 433 | /* End PBXShellScriptBuildPhase section */ 434 | 435 | /* Begin PBXSourcesBuildPhase section */ 436 | 723752FF1D2D9D6D002A1BC2 /* Sources */ = { 437 | isa = PBXSourcesBuildPhase; 438 | buildActionMask = 2147483647; 439 | files = ( 440 | 7237532A1D2D9DA2002A1BC2 /* QuadTreeNode.swift in Sources */, 441 | 7237532B1D2D9DA2002A1BC2 /* QuadTreeResult.swift in Sources */, 442 | 72383AB51D2ECAD00071EDE2 /* OptionalOr.swift in Sources */, 443 | B50426B61D71F775008BEF76 /* CGPoint+BentoCoordinate.swift in Sources */, 444 | B50426B41D71F35C008BEF76 /* MKMapPoint+BentoCoordinate.swift in Sources */, 445 | D4C3839B1D4F9DD500556EBE /* CollectionTypeExtensions.swift in Sources */, 446 | B50426B81D71FA21008BEF76 /* MKMapRect+BentoRect.swift in Sources */, 447 | 723753251D2D9DA2002A1BC2 /* BentoBox.swift in Sources */, 448 | B50426BA1D7203C9008BEF76 /* CLLocationCoordinate2D+BentoCoordinate.swift in Sources */, 449 | 723753281D2D9DA2002A1BC2 /* QuadrantWrapper.swift in Sources */, 450 | 723753291D2D9DA2002A1BC2 /* QuadTree.swift in Sources */, 451 | 723753261D2D9DA2002A1BC2 /* Box.swift in Sources */, 452 | 723753271D2D9DA2002A1BC2 /* OrdinalNodes.swift in Sources */, 453 | B50426AF1D71D710008BEF76 /* CGRect+BentoRect.swift in Sources */, 454 | ); 455 | runOnlyForDeploymentPostprocessing = 0; 456 | }; 457 | 7237530A1D2D9D6D002A1BC2 /* Sources */ = { 458 | isa = PBXSourcesBuildPhase; 459 | buildActionMask = 2147483647; 460 | files = ( 461 | 7237532E1D2D9DB5002A1BC2 /* BentoBoxTests.swift in Sources */, 462 | 72383AB71D2ECAF70071EDE2 /* OptionalOrTests.swift in Sources */, 463 | 7237532F1D2D9DB5002A1BC2 /* QuadTreeTests.swift in Sources */, 464 | ); 465 | runOnlyForDeploymentPostprocessing = 0; 466 | }; 467 | 723753321D2D9E75002A1BC2 /* Sources */ = { 468 | isa = PBXSourcesBuildPhase; 469 | buildActionMask = 2147483647; 470 | files = ( 471 | 7237534E1D2DA0CF002A1BC2 /* QuadTree+SampleData.swift in Sources */, 472 | B539B3401D7E35ED0089AFE6 /* MainMenuTableViewController.swift in Sources */, 473 | 7237533B1D2D9E75002A1BC2 /* MapKitViewController.swift in Sources */, 474 | 72D679441E252AD40032902B /* AnnotationView.swift in Sources */, 475 | B539B33E1D7E35D60089AFE6 /* CoreGraphicsViewController.swift in Sources */, 476 | 7228026E1D2E9D9100C637A6 /* Annotations.swift in Sources */, 477 | 723753391D2D9E75002A1BC2 /* AppDelegate.swift in Sources */, 478 | ); 479 | runOnlyForDeploymentPostprocessing = 0; 480 | }; 481 | /* End PBXSourcesBuildPhase section */ 482 | 483 | /* Begin PBXTargetDependency section */ 484 | 723753111D2D9D6D002A1BC2 /* PBXTargetDependency */ = { 485 | isa = PBXTargetDependency; 486 | target = 723753031D2D9D6D002A1BC2 /* BentoMap */; 487 | targetProxy = 723753101D2D9D6D002A1BC2 /* PBXContainerItemProxy */; 488 | }; 489 | 7237534B1D2D9E80002A1BC2 /* PBXTargetDependency */ = { 490 | isa = PBXTargetDependency; 491 | target = 723753031D2D9D6D002A1BC2 /* BentoMap */; 492 | targetProxy = 7237534A1D2D9E80002A1BC2 /* PBXContainerItemProxy */; 493 | }; 494 | /* End PBXTargetDependency section */ 495 | 496 | /* Begin PBXVariantGroup section */ 497 | 723753411D2D9E75002A1BC2 /* LaunchScreen.storyboard */ = { 498 | isa = PBXVariantGroup; 499 | children = ( 500 | 723753421D2D9E75002A1BC2 /* Base */, 501 | ); 502 | name = LaunchScreen.storyboard; 503 | path = .; 504 | sourceTree = ""; 505 | }; 506 | /* End PBXVariantGroup section */ 507 | 508 | /* Begin XCBuildConfiguration section */ 509 | 723753161D2D9D6D002A1BC2 /* Debug */ = { 510 | isa = XCBuildConfiguration; 511 | buildSettings = { 512 | ALWAYS_SEARCH_USER_PATHS = NO; 513 | CLANG_ANALYZER_NONNULL = YES; 514 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 515 | CLANG_CXX_LIBRARY = "libc++"; 516 | CLANG_ENABLE_MODULES = YES; 517 | CLANG_ENABLE_OBJC_ARC = YES; 518 | CLANG_WARN_BOOL_CONVERSION = YES; 519 | CLANG_WARN_CONSTANT_CONVERSION = YES; 520 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 521 | CLANG_WARN_EMPTY_BODY = YES; 522 | CLANG_WARN_ENUM_CONVERSION = YES; 523 | CLANG_WARN_INFINITE_RECURSION = YES; 524 | CLANG_WARN_INT_CONVERSION = YES; 525 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 526 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 527 | CLANG_WARN_UNREACHABLE_CODE = YES; 528 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 529 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 530 | COPY_PHASE_STRIP = NO; 531 | CURRENT_PROJECT_VERSION = 1; 532 | DEBUG_INFORMATION_FORMAT = dwarf; 533 | ENABLE_STRICT_OBJC_MSGSEND = YES; 534 | ENABLE_TESTABILITY = YES; 535 | GCC_C_LANGUAGE_STANDARD = gnu99; 536 | GCC_DYNAMIC_NO_PIC = NO; 537 | GCC_NO_COMMON_BLOCKS = YES; 538 | GCC_OPTIMIZATION_LEVEL = 0; 539 | GCC_PREPROCESSOR_DEFINITIONS = ( 540 | "DEBUG=1", 541 | "$(inherited)", 542 | ); 543 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 544 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 545 | GCC_WARN_UNDECLARED_SELECTOR = YES; 546 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 547 | GCC_WARN_UNUSED_FUNCTION = YES; 548 | GCC_WARN_UNUSED_VARIABLE = YES; 549 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 550 | MTL_ENABLE_DEBUG_INFO = YES; 551 | ONLY_ACTIVE_ARCH = YES; 552 | SDKROOT = iphoneos; 553 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 554 | TARGETED_DEVICE_FAMILY = "1,2"; 555 | VERSIONING_SYSTEM = "apple-generic"; 556 | VERSION_INFO_PREFIX = ""; 557 | }; 558 | name = Debug; 559 | }; 560 | 723753171D2D9D6D002A1BC2 /* Release */ = { 561 | isa = XCBuildConfiguration; 562 | buildSettings = { 563 | ALWAYS_SEARCH_USER_PATHS = NO; 564 | CLANG_ANALYZER_NONNULL = YES; 565 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 566 | CLANG_CXX_LIBRARY = "libc++"; 567 | CLANG_ENABLE_MODULES = YES; 568 | CLANG_ENABLE_OBJC_ARC = YES; 569 | CLANG_WARN_BOOL_CONVERSION = YES; 570 | CLANG_WARN_CONSTANT_CONVERSION = YES; 571 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 572 | CLANG_WARN_EMPTY_BODY = YES; 573 | CLANG_WARN_ENUM_CONVERSION = YES; 574 | CLANG_WARN_INFINITE_RECURSION = YES; 575 | CLANG_WARN_INT_CONVERSION = YES; 576 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 577 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 578 | CLANG_WARN_UNREACHABLE_CODE = YES; 579 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 580 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 581 | COPY_PHASE_STRIP = NO; 582 | CURRENT_PROJECT_VERSION = 1; 583 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 584 | ENABLE_NS_ASSERTIONS = NO; 585 | ENABLE_STRICT_OBJC_MSGSEND = YES; 586 | GCC_C_LANGUAGE_STANDARD = gnu99; 587 | GCC_NO_COMMON_BLOCKS = YES; 588 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 589 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 590 | GCC_WARN_UNDECLARED_SELECTOR = YES; 591 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 592 | GCC_WARN_UNUSED_FUNCTION = YES; 593 | GCC_WARN_UNUSED_VARIABLE = YES; 594 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 595 | MTL_ENABLE_DEBUG_INFO = NO; 596 | SDKROOT = iphoneos; 597 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 598 | TARGETED_DEVICE_FAMILY = "1,2"; 599 | VALIDATE_PRODUCT = YES; 600 | VERSIONING_SYSTEM = "apple-generic"; 601 | VERSION_INFO_PREFIX = ""; 602 | }; 603 | name = Release; 604 | }; 605 | 723753191D2D9D6D002A1BC2 /* Debug */ = { 606 | isa = XCBuildConfiguration; 607 | buildSettings = { 608 | CLANG_ENABLE_MODULES = YES; 609 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; 610 | DEFINES_MODULE = YES; 611 | DYLIB_COMPATIBILITY_VERSION = 1; 612 | DYLIB_CURRENT_VERSION = 1; 613 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 614 | INFOPLIST_FILE = BentoMap/Resources/Info.plist; 615 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 616 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 617 | PRODUCT_BUNDLE_IDENTIFIER = com.raizlabs.BentoMap; 618 | PRODUCT_NAME = "$(TARGET_NAME)"; 619 | SKIP_INSTALL = YES; 620 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 621 | SWIFT_VERSION = 3.0; 622 | }; 623 | name = Debug; 624 | }; 625 | 7237531A1D2D9D6D002A1BC2 /* Release */ = { 626 | isa = XCBuildConfiguration; 627 | buildSettings = { 628 | CLANG_ENABLE_MODULES = YES; 629 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; 630 | DEFINES_MODULE = YES; 631 | DYLIB_COMPATIBILITY_VERSION = 1; 632 | DYLIB_CURRENT_VERSION = 1; 633 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 634 | INFOPLIST_FILE = BentoMap/Resources/Info.plist; 635 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 636 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 637 | PRODUCT_BUNDLE_IDENTIFIER = com.raizlabs.BentoMap; 638 | PRODUCT_NAME = "$(TARGET_NAME)"; 639 | SKIP_INSTALL = YES; 640 | SWIFT_VERSION = 3.0; 641 | }; 642 | name = Release; 643 | }; 644 | 7237531C1D2D9D6D002A1BC2 /* Debug */ = { 645 | isa = XCBuildConfiguration; 646 | buildSettings = { 647 | INFOPLIST_FILE = BentoMapTests/Resources/Info.plist; 648 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 649 | PRODUCT_BUNDLE_IDENTIFIER = com.raizlabs.BentoMapTests; 650 | PRODUCT_NAME = "$(TARGET_NAME)"; 651 | SWIFT_VERSION = 3.0; 652 | }; 653 | name = Debug; 654 | }; 655 | 7237531D1D2D9D6D002A1BC2 /* Release */ = { 656 | isa = XCBuildConfiguration; 657 | buildSettings = { 658 | INFOPLIST_FILE = BentoMapTests/Resources/Info.plist; 659 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 660 | PRODUCT_BUNDLE_IDENTIFIER = com.raizlabs.BentoMapTests; 661 | PRODUCT_NAME = "$(TARGET_NAME)"; 662 | SWIFT_VERSION = 3.0; 663 | }; 664 | name = Release; 665 | }; 666 | 723753461D2D9E75002A1BC2 /* Debug */ = { 667 | isa = XCBuildConfiguration; 668 | buildSettings = { 669 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 670 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 671 | INFOPLIST_FILE = BentoMapExample/Assets/Info.plist; 672 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 673 | PRODUCT_BUNDLE_IDENTIFIER = com.raizlabs.BentoMapExample; 674 | PRODUCT_NAME = "$(TARGET_NAME)"; 675 | SWIFT_VERSION = 3.0; 676 | }; 677 | name = Debug; 678 | }; 679 | 723753471D2D9E75002A1BC2 /* Release */ = { 680 | isa = XCBuildConfiguration; 681 | buildSettings = { 682 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 683 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 684 | INFOPLIST_FILE = BentoMapExample/Assets/Info.plist; 685 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 686 | PRODUCT_BUNDLE_IDENTIFIER = com.raizlabs.BentoMapExample; 687 | PRODUCT_NAME = "$(TARGET_NAME)"; 688 | SWIFT_VERSION = 3.0; 689 | }; 690 | name = Release; 691 | }; 692 | /* End XCBuildConfiguration section */ 693 | 694 | /* Begin XCConfigurationList section */ 695 | 723752FE1D2D9D6D002A1BC2 /* Build configuration list for PBXProject "BentoMap" */ = { 696 | isa = XCConfigurationList; 697 | buildConfigurations = ( 698 | 723753161D2D9D6D002A1BC2 /* Debug */, 699 | 723753171D2D9D6D002A1BC2 /* Release */, 700 | ); 701 | defaultConfigurationIsVisible = 0; 702 | defaultConfigurationName = Release; 703 | }; 704 | 723753181D2D9D6D002A1BC2 /* Build configuration list for PBXNativeTarget "BentoMap" */ = { 705 | isa = XCConfigurationList; 706 | buildConfigurations = ( 707 | 723753191D2D9D6D002A1BC2 /* Debug */, 708 | 7237531A1D2D9D6D002A1BC2 /* Release */, 709 | ); 710 | defaultConfigurationIsVisible = 0; 711 | defaultConfigurationName = Release; 712 | }; 713 | 7237531B1D2D9D6D002A1BC2 /* Build configuration list for PBXNativeTarget "BentoMapTests" */ = { 714 | isa = XCConfigurationList; 715 | buildConfigurations = ( 716 | 7237531C1D2D9D6D002A1BC2 /* Debug */, 717 | 7237531D1D2D9D6D002A1BC2 /* Release */, 718 | ); 719 | defaultConfigurationIsVisible = 0; 720 | defaultConfigurationName = Release; 721 | }; 722 | 723753451D2D9E75002A1BC2 /* Build configuration list for PBXNativeTarget "BentoMapExample" */ = { 723 | isa = XCConfigurationList; 724 | buildConfigurations = ( 725 | 723753461D2D9E75002A1BC2 /* Debug */, 726 | 723753471D2D9E75002A1BC2 /* Release */, 727 | ); 728 | defaultConfigurationIsVisible = 0; 729 | defaultConfigurationName = Release; 730 | }; 731 | /* End XCConfigurationList section */ 732 | }; 733 | rootObject = 723752FB1D2D9D6D002A1BC2 /* Project object */; 734 | } 735 | --------------------------------------------------------------------------------