Last active
February 22, 2018 07:10
-
-
Save jmcd/3ef75827728722740fa5 to your computer and use it in GitHub Desktop.
MKMapView contested annotation location handling in Swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import UIKit | |
import MapKit | |
/** | |
* Define behaviour of app through its lifetime | |
*/ | |
@UIApplicationMain | |
class AppDelegate: UIResponder, UIApplicationDelegate { | |
var window: UIWindow? | |
// Called when the app starts | |
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { | |
// create a demo view controller and set it to have 5 shops, all contesting a single coordinate | |
let vc = ShopViewController() | |
vc.shops = (1...5).map { | |
Shop(name: "Shop \($0)", latitude: 55.9520600, longitude: -3.1964800) | |
} | |
// set up the window, and set our demo view controller as the root | |
let w = UIWindow(frame: UIScreen.mainScreen().bounds) | |
w.rootViewController = vc | |
w.makeKeyAndVisible() | |
self.window = w | |
return true | |
} | |
} | |
/** | |
* Model of a shop | |
*/ | |
struct Shop { | |
let name: String | |
let latitude: Double | |
let longitude: Double | |
} | |
/** | |
* View controller for shops, just holds a map view | |
*/ | |
class ShopViewController: UIViewController { | |
var shops: [Shop] = [] | |
override func viewDidLoad() { | |
// add a mapview | |
let mv = MKMapView() | |
view.addSubview(mv) | |
// add layout constraints maximizing the mapview in the parent view | |
mv.translatesAutoresizingMaskIntoConstraints = false | |
for a in [{ (v: UIView) in v.topAnchor }, { $0.bottomAnchor }, { $0.leftAnchor }, { $0.rightAnchor }] { | |
a(mv).constraintEqualToAnchor(a(view)).active = true | |
} | |
// construct and add annotations for our model | |
for shop in shops { | |
let p = constructAnnotationForTitle(shop.name, coordinate: CLLocationCoordinate2D(latitude: shop.latitude, longitude: shop.longitude)) | |
mv.addAnnotation(p) | |
} | |
// at this point we may have annotations contesting a location | |
// construct new annotations | |
let newAnnotations = ContestedAnnotationTool.annotationsByDistributingAnnotations(mv.annotations) { (oldAnnotation:MKAnnotation, newCoordinate:CLLocationCoordinate2D) in | |
self.constructAnnotationForTitle(oldAnnotation.title!, coordinate: newCoordinate) | |
} | |
// replace annotations | |
mv.removeAnnotations(mv.annotations) | |
mv.addAnnotations(newAnnotations) | |
// zoom to annotations | |
mv.showAnnotations(mv.annotations, animated: true) | |
} | |
// Constructs an MKAnnotation, in this demo just a point | |
private func constructAnnotationForTitle(title: String?, coordinate: CLLocationCoordinate2D) -> MKAnnotation { | |
let p = MKPointAnnotation() | |
p.coordinate = coordinate | |
p.title = title | |
return p | |
} | |
} | |
/** | |
* Tool to construct new annotations | |
*/ | |
public struct ContestedAnnotationTool { | |
private static let radiusOfEarth = Double(6378100) | |
public typealias annotationRelocator = ((oldAnnotation:MKAnnotation, newCoordinate:CLLocationCoordinate2D) -> (MKAnnotation)) | |
public static func annotationsByDistributingAnnotations(annotations: [MKAnnotation], constructNewAnnotationWithClosure ctor: annotationRelocator) -> [MKAnnotation] { | |
// 1. group the annotations by coordinate | |
let coordinateToAnnotations = groupAnnotationsByCoordinate(annotations) | |
// 2. go through the groups and redistribute | |
var newAnnotations = [MKAnnotation]() | |
for (_, annotationsAtCoordinate) in coordinateToAnnotations { | |
let newAnnotationsAtCoordinate = ContestedAnnotationTool.annotationsByDistributingAnnotationsContestingACoordinate(annotationsAtCoordinate, constructNewAnnotationWithClosure: ctor) | |
newAnnotations.appendContentsOf(newAnnotationsAtCoordinate) | |
} | |
return newAnnotations | |
} | |
private static func groupAnnotationsByCoordinate(annotations: [MKAnnotation]) -> [CLLocationCoordinate2D: [MKAnnotation]] { | |
var coordinateToAnnotations = [CLLocationCoordinate2D: [MKAnnotation]]() | |
for annotation in annotations { | |
let coordinate = annotation.coordinate | |
let annotationsAtCoordinate = coordinateToAnnotations[coordinate] ?? [MKAnnotation]() | |
coordinateToAnnotations[coordinate] = annotationsAtCoordinate + [annotation] | |
} | |
return coordinateToAnnotations | |
} | |
private static func annotationsByDistributingAnnotationsContestingACoordinate(annotations: [MKAnnotation], constructNewAnnotationWithClosure ctor: annotationRelocator) -> [MKAnnotation] { | |
var newAnnotations = [MKAnnotation]() | |
let contestedCoordinates = annotations.map{ $0.coordinate } | |
let newCoordinates = coordinatesByDistributingCoordinates(contestedCoordinates) | |
for (i, annotation) in annotations.enumerate() { | |
let newCoordinate = newCoordinates[i] | |
let newAnnotation = ctor(oldAnnotation: annotation, newCoordinate: newCoordinate) | |
newAnnotations.append(newAnnotation) | |
} | |
return newAnnotations | |
} | |
private static func coordinatesByDistributingCoordinates(coordinates: [CLLocationCoordinate2D]) -> [CLLocationCoordinate2D] { | |
if coordinates.count == 1 { | |
return coordinates | |
} | |
var result = [CLLocationCoordinate2D]() | |
let distanceFromContestedLocation: Double = 3.0 * Double(coordinates.count) / 2.0 | |
let radiansBetweenAnnotations = (M_PI * 2) / Double(coordinates.count) | |
for (i, coordinate) in coordinates.enumerate() { | |
let bearing = radiansBetweenAnnotations * Double(i) | |
let newCoordinate = calculateCoordinateFromCoordinate(coordinate, onBearingInRadians: bearing, atDistanceInMetres: distanceFromContestedLocation) | |
result.append(newCoordinate) | |
} | |
return result | |
} | |
private static func calculateCoordinateFromCoordinate(coordinate: CLLocationCoordinate2D, onBearingInRadians bearing: Double, atDistanceInMetres distance: Double) -> CLLocationCoordinate2D { | |
let coordinateLatitudeInRadians = coordinate.latitude * M_PI / 180; | |
let coordinateLongitudeInRadians = coordinate.longitude * M_PI / 180; | |
let distanceComparedToEarth = distance / radiusOfEarth; | |
let resultLatitudeInRadians = asin(sin(coordinateLatitudeInRadians) * cos(distanceComparedToEarth) + cos(coordinateLatitudeInRadians) * sin(distanceComparedToEarth) * cos(bearing)); | |
let resultLongitudeInRadians = coordinateLongitudeInRadians + atan2(sin(bearing) * sin(distanceComparedToEarth) * cos(coordinateLatitudeInRadians), cos(distanceComparedToEarth) - sin(coordinateLatitudeInRadians) * sin(resultLatitudeInRadians)); | |
let latitude = resultLatitudeInRadians * 180 / M_PI; | |
let longitude = resultLongitudeInRadians * 180 / M_PI; | |
return CLLocationCoordinate2D(latitude: latitude, longitude: longitude) | |
} | |
} | |
// To use CLLocationCoordinate2D as a key in a dictionary, it needs to comply with the Hashable protocol | |
extension CLLocationCoordinate2D: Hashable { | |
public var hashValue: Int { | |
get { | |
return (latitude.hashValue&*397) &+ longitude.hashValue; | |
} | |
} | |
} | |
// To be Hashable, you need to be Equatable too | |
public func ==(lhs: CLLocationCoordinate2D, rhs: CLLocationCoordinate2D) -> Bool { | |
return lhs.latitude == rhs.latitude && lhs.longitude == rhs.longitude | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I am working on objective-c.is there source code for objective-c?
i dont understand in your blog