How does MapKit’s projected plane behave: map points, metres at different latitudes, and why a rect across the date line is wrong?
Predict first
What do the geometry values show?
import Foundation
import MapKit
let pune = CLLocationCoordinate2D(latitude: 18.5204, longitude: 73.8567)
let mumbai = CLLocationCoordinate2D(latitude: 19.0760, longitude: 72.8777)
let world = MKMapRect.world
print("world:", Int(world.size.width), "x", Int(world.size.height), "map points")
let equator = MKMapPointsPerMeterAtLatitude(0), north = MKMapPointsPerMeterAtLatitude(60)
print("points per metre at 60N is", (north / equator * 100).rounded() / 100, "times the equator")
print("map-point distance:", Int(MKMapPoint(pune).distance(to: MKMapPoint(mumbai))), "m; great circle:", Int(CLLocation(latitude: pune.latitude, longitude: pune.longitude).distance(from: CLLocation(latitude: mumbai.latitude, longitude: mumbai.longitude))), "m")
let r = MKCoordinateRegion(center: pune, latitudinalMeters: 1000, longitudinalMeters: 1000)
print("1 km square span:", (r.span.latitudeDelta * 1e4).rounded() / 1e4, "lat deg x", (r.span.longitudeDelta * 1e4).rounded() / 1e4, "lon deg")
let polar = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 70, longitude: 20), latitudinalMeters: 1000, longitudinalMeters: 1000)
print("same square at 70N:", (polar.span.latitudeDelta * 1e4).rounded() / 1e4, "x", (polar.span.longitudeDelta * 1e4).rounded() / 1e4)
let fiji = [CLLocationCoordinate2D(latitude: -17, longitude: 179.9), CLLocationCoordinate2D(latitude: -17, longitude: -179.9)]
var naive = MKMapRect.null
for c in fiji { naive = naive.union(MKMapRect(origin: MKMapPoint(c), size: MKMapSize(width: 0, height: 0))) }
print("rect around two points 0.2 degrees apart across the date line covers", (naive.size.width / world.size.width * 100).rounded() / 100, "of the world; span", (MKCoordinateRegion(naive).span.longitudeDelta * 10).rounded() / 10, "degrees")
- Points per metre grows with latitude, the span widens at 70N, and the date-line rect covers about the whole world
- Spans are identical at every latitude
- The date-line rect is tiny
Mental model. A flat wall map stretches the arctic, so a centimetre there means fewer kilometres, and a trip across its left-right seam looks like crossing the whole map.
MapKit works on a Mercator-like plane of 268,435,456 × 268,435,456 map points covering the world. One metre is a different number of points at each latitude (at 60° north about 1.99 times the equator), so a region built from metres has a longitude span that grows towards the poles: a 1 km square is 0.009° × 0.0095° at Pune but 0.009° × 0.0262° at 70° north. Distance between two map points (120,161 m Pune to Mumbai) is only an approximation of the great-circle distance CLLocation.distance(from:) gives (120,138 m), so use Core Location for real distances and the map plane for drawing and hit tests.
The plane has an edge: unioning rects for points at 179.9° and −179.9° produces a rect that spans almost the entire world (about 359.8°), not the 0.2° that separates them. For antimeridian-crossing data, normalise longitudes (add 360 to the negative ones), compute the bounds there, and convert back, or use several rects. The values were computed on macOS with MapKit’s geometry functions; rendering was not run.
The projection scales with latitude and has a seam at ±180°.
Common traps
- Zooming to the union of points across ±180°. You show the whole world.
- Using map-point distance for billing or safety. Use Core Location.
They might follow up with
- How do I fit a region around coordinates? Build a rect from the points and call
setVisibleMapRectwith padding.
Related questions
- 17.2.2 How do you show places with MapKit: annotations, overlays, regions and clustering?
- 17.2.1 How does Core Location work: authorization levels, accuracy, significant-change and visits, and geofencing?
← How do you geofence 500 stores when an app may monitor only 20…