Skip to content

Instantly share code, notes, and snippets.

@jmcd
Last active February 22, 2018 07:10
Show Gist options
  • Save jmcd/3ef75827728722740fa5 to your computer and use it in GitHub Desktop.
Save jmcd/3ef75827728722740fa5 to your computer and use it in GitHub Desktop.
MKMapView contested annotation location handling in Swift
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
}
@karthisiva
Copy link

I am working on objective-c.is there source code for objective-c?
i dont understand in your blog

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment