Last active
September 21, 2024 10:17
-
-
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.
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 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) | |
} | |
} |
This helped me out with making my first iOS project! I'll share your github on my blog! thank you again :)
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
Brilliant.
Saved me figuring it out from this article. http://janmatuschek.de/LatitudeLongitudeBoundingCoordinates
Thanks 👍
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
Thanks for this!