Search code examples
iosswiftgeometrymapkitmapkitannotation

How to draw circle overlay on MapKit that surrounds several annotations/coordinates?


I have an array [CLLocationCoordinate2D], I would like to draw a circle on MKMapView surrounding all these coordinates.

I have managed to draw a circle around a single CLLocationCoordinate2D as such:

let coordinate = CLLocationCoordinate2D(latitude: 53, longitude: 27)
self.mapView.add(MKCircle(center: coordinate, radius: 100))

extension MapViewController: MKMapViewDelegate {
    func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
        guard overlay is MKCircle else { return MKOverlayRenderer() }

        let circle = MKCircleRenderer(overlay: overlay)
        circle.strokeColor = UIColor.red
        circle.fillColor = UIColor(red: 255, green: 0, blue: 0, alpha: 0.1)
        circle.lineWidth = 1
        return circle
    }
}

How can I draw a circle that surrounds/encompasses all coordinates?, like below:

enter image description here


Solution

  • I came up with MKCoordinateRegion initializer, which provides the region of the coordinates, the extension has a computed property to provide the radius of the region.

    extension MKCoordinateRegion {
        
        init?(from coordinates: [CLLocationCoordinate2D]) {
            guard coordinates.count > 1 else { return nil }
            
            let a = MKCoordinateRegion.region(coordinates, fix: { $0 }, fix2: { $0 })
            let b = MKCoordinateRegion.region(coordinates, fix: MKCoordinateRegion.fixMeridianNegativeLongitude, fix2: MKCoordinateRegion.fixMeridian180thLongitude)
    
            if let a, let b, let result = [a, b].min(by: { $0.span.longitudeDelta < $1.span.longitudeDelta }) {
                self = result
            } else if let result = a ?? b {
                self = result
            } else {
                return nil
            }
        }
        
        var radius: CLLocationDistance {
            let furthest = CLLocation(latitude: self.center.latitude + (span.latitudeDelta / 2),
                                      longitude: center.longitude + (span.longitudeDelta / 2))
            return CLLocation(latitude: center.latitude, longitude: center.longitude).distance(from: furthest)
        }
        
        // MARK: - Private
        private static func region(_ coordinates: [CLLocationCoordinate2D],
                                   fix: (CLLocationCoordinate2D) -> CLLocationCoordinate2D,
                                   fix2: (CLLocationCoordinate2D) -> CLLocationCoordinate2D) -> MKCoordinateRegion? {
            let t = coordinates.map(fix)
            let min = CLLocationCoordinate2D(latitude: t.min { $0.latitude < $1.latitude }!.latitude,
                                             longitude: t.min { $0.longitude < $1.longitude }!.longitude)
            let max = CLLocationCoordinate2D(latitude: t.max { $0.latitude < $1.latitude }!.latitude,
                                             longitude: t.max { $0.longitude < $1.longitude }!.longitude)
            
            // find span
            let span = MKCoordinateSpanMake(max.latitude - min.latitude, max.longitude - min.longitude)
            
            // find center
            let center = CLLocationCoordinate2D(latitude: max.latitude - span.latitudeDelta / 2,
                                                longitude: max.longitude - span.longitudeDelta / 2)
            
            return MKCoordinateRegion(center: fix2(center), span: span)
        }
        
        private static func fixMeridianNegativeLongitude(coordinate: CLLocationCoordinate2D) -> CLLocationCoordinate2D {
            guard (coordinate.longitude < 0) else { return coordinate }
            
            let fixedLng = 360 + coordinate.longitude
            return CLLocationCoordinate2D(latitude: coordinate.latitude, longitude: fixedLng)
        }
        
        private static func fixMeridian180thLongitude(coordinate: CLLocationCoordinate2D) -> CLLocationCoordinate2D {
            guard (coordinate.longitude > 180) else { return coordinate }
            
            let fixedLng = -360 + coordinate.longitude
            return CLLocationCoordinate2D(latitude: coordinate.latitude, longitude: fixedLng)
        }
        
    }
    

    Usage:

    let coordinates: [CLLocationCoordinate2D] = self.mapView.annotations.map{ $0.coordinate }
    if let region = MKCoordinateRegion(from: coordinates) {
        self.mapView.add(MKCircle(center: region.center, radius: region.radius))
    }
    

    Result is exactly what I want, with ability to handle coordinates crossing 180th meridian:

    enter image description here