Skip to content

Instantly share code, notes, and snippets.

@dionc
Last active September 21, 2024 10:17
Show Gist options
  • Save dionc/46f7e7ee9db7dbd7bddec56bd5418ca6 to your computer and use it in GitHub Desktop.
Save dionc/46f7e7ee9db7dbd7bddec56bd5418ca6 to your computer and use it in GitHub Desktop.
Create an MKCoordinateRegion from an array of coordinates. Safely handles coordinates that cross the 180th meridian.
import MapKit
extension MKCoordinateRegion {
init?(coordinates: [CLLocationCoordinate2D]) {
// first create a region centered around the prime meridian
let primeRegion = MKCoordinateRegion.region(for: coordinates, transform: { $0 }, inverseTransform: { $0 })
// next create a region centered around the 180th meridian
let transformedRegion = MKCoordinateRegion.region(for: coordinates, transform: MKCoordinateRegion.transform, inverseTransform: MKCoordinateRegion.inverseTransform)
// return the region that has the smallest longitude delta
if let a = primeRegion,
let b = transformedRegion,
let min = [a, b].min(by: { $0.span.longitudeDelta < $1.span.longitudeDelta }) {
self = min
}
else if let a = primeRegion {
self = a
}
else if let b = transformedRegion {
self = b
}
else {
return nil
}
}
// Latitude -180...180 -> 0...360
private static func transform(c: CLLocationCoordinate2D) -> CLLocationCoordinate2D {
if c.longitude < 0 { return CLLocationCoordinate2DMake(c.latitude, 360 + c.longitude) }
return c
}
// Latitude 0...360 -> -180...180
private static func inverseTransform(c: CLLocationCoordinate2D) -> CLLocationCoordinate2D {
if c.longitude > 180 { return CLLocationCoordinate2DMake(c.latitude, -360 + c.longitude) }
return c
}
private typealias Transform = (CLLocationCoordinate2D) -> (CLLocationCoordinate2D)
private static func region(for coordinates: [CLLocationCoordinate2D], transform: Transform, inverseTransform: Transform) -> MKCoordinateRegion? {
// handle empty array
guard !coordinates.isEmpty else { return nil }
// handle single coordinate
guard coordinates.count > 1 else {
return MKCoordinateRegion(center: coordinates[0], span: MKCoordinateSpanMake(1, 1))
}
let transformed = coordinates.map(transform)
// find the span
let minLat = transformed.min { $0.latitude < $1.latitude }!.latitude
let maxLat = transformed.max { $0.latitude < $1.latitude }!.latitude
let minLon = transformed.min { $0.longitude < $1.longitude }!.longitude
let maxLon = transformed.max { $0.longitude < $1.longitude }!.longitude
let span = MKCoordinateSpanMake(maxLat - minLat, maxLon - minLon)
// find the center of the span
let center = inverseTransform(CLLocationCoordinate2DMake((maxLat - span.latitudeDelta / 2), maxLon - span.longitudeDelta / 2))
return MKCoordinateRegionMake(center, span)
}
}
@AdrianBinDC
Copy link

Thank you for posting this. If you're looking for something that gets regions from continents and major oceans, I threw this together.

https://gist.github.com/AdrianBinDC/19d283f31cdd04a467ff3adad5c12413

@nicodioso
Copy link

Thanks for this!

@AnnaBaeTofuMom
Copy link

This helped me out with making my first iOS project! I'll share your github on my blog! thank you again :)

@0xifarouk
Copy link

This is amazing, but how can I add some padding for the edges? in other words I need the zoom level to be larger so pins are not on the edges of the screen. @dionc

@NineLostSouls
Copy link

Brilliant.

Saved me figuring it out from this article. http://janmatuschek.de/LatitudeLongitudeBoundingCoordinates

Thanks 👍

@podratz
Copy link

podratz commented Sep 21, 2024

Made an adjustment to accommodate for a padding factor:

import MapKit

extension MKCoordinateRegion {

    init?(coordinates: [CLLocationCoordinate2D], paddingFactor: Double = 0.2) {

        // first create a region centered around the prime meridian
        let primeRegion = MKCoordinateRegion.region(for: coordinates, transform: { $0 }, inverseTransform: { $0 })

        // next create a region centered around the 180th meridian
        let transformedRegion = MKCoordinateRegion.region(for: coordinates, transform: MKCoordinateRegion.transform, inverseTransform: MKCoordinateRegion.inverseTransform)

        // return the region that has the smallest longitude delta and apply padding
        if let a = primeRegion,
           let b = transformedRegion,
           let minRegion = [a, b].min(by: { $0.span.longitudeDelta < $1.span.longitudeDelta })
        {
            self = minRegion.withPadding(paddingFactor)
        } else if let a = primeRegion {
            self = a.withPadding(paddingFactor)
        } else if let b = transformedRegion {
            self = b.withPadding(paddingFactor)
        } else {
            return nil
        }
    }

    // Apply padding to a region by increasing the span deltas
    func withPadding(_ factor: Double) -> MKCoordinateRegion {
        let latitudeDeltaWithPadding = span.latitudeDelta * (1 + factor)
        let longitudeDeltaWithPadding = span.longitudeDelta * (1 + factor)
        let spanWithPadding = MKCoordinateSpan(latitudeDelta: latitudeDeltaWithPadding, longitudeDelta: longitudeDeltaWithPadding)
        return MKCoordinateRegion(center: center, span: spanWithPadding)
    }

    // Latitude -180...180 -> 0...360
    private static func transform(c: CLLocationCoordinate2D) -> CLLocationCoordinate2D {
        if c.longitude < 0 { return CLLocationCoordinate2DMake(c.latitude, 360 + c.longitude) }
        return c
    }

    // Latitude 0...360 -> -180...180
    private static func inverseTransform(c: CLLocationCoordinate2D) -> CLLocationCoordinate2D {
        if c.longitude > 180 { return CLLocationCoordinate2DMake(c.latitude, -360 + c.longitude) }
        return c
    }

    private typealias Transform = (CLLocationCoordinate2D) -> (CLLocationCoordinate2D)

    private static func region(for coordinates: [CLLocationCoordinate2D], transform: Transform, inverseTransform: Transform) -> MKCoordinateRegion? {

        // handle empty array
        guard !coordinates.isEmpty else { return nil }

        // handle single coordinate
        guard coordinates.count > 1 else {
            return MKCoordinateRegion(center: coordinates[0], span: MKCoordinateSpan(latitudeDelta: 1, longitudeDelta: 1))
        }

        let transformed = coordinates.map(transform)

        // find the span
        let minLat = transformed.min { $0.latitude < $1.latitude }!.latitude
        let maxLat = transformed.max { $0.latitude < $1.latitude }!.latitude
        let minLon = transformed.min { $0.longitude < $1.longitude }!.longitude
        let maxLon = transformed.max { $0.longitude < $1.longitude }!.longitude
        let span = MKCoordinateSpan(latitudeDelta: maxLat - minLat, longitudeDelta: maxLon - minLon)

        // find the center of the span
        let center = inverseTransform(CLLocationCoordinate2DMake(maxLat - span.latitudeDelta / 2, maxLon - span.longitudeDelta / 2))

        return MKCoordinateRegion(center: center, span: span)
    }
}

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