We knew that the Earth was not flat long before 1492. Early navigators observed the way ships would dip out of view over the horizon many centuries before the Age of Discovery.
For many iOS developers, though, a flat MKMapView
was a necessary conceit until recently.
What changed? The discovery of MKGeodesicPolyline
, which is the subject of this week's article.
MKGeodesicPolyline
was introduced to the Map Kit framework in iOS 7. As its name implies, it creates a geodesic—essentially a straight line over a curved surface.
On the surface of a sphereoblate spheroidgeoid, the shortest distance between two points on appears as an arc on a flat projection. Over large distances, this takes a pronounced, circular shape.
A MKGeodesicPolyline
is created with an array of of 2 MKMapPoint
s or CLLocationCoordinate2D
s:
Creating an MKGeodesicPolyline
CLLocation*LAX=[[CLLocationalloc]initWithLatitude:33.9424955longitude:-118.4080684];CLLocation*JFK=[[CLLocationalloc]initWithLatitude:40.6397511longitude:-73.7789256];CLLocationCoordinate2Dcoordinates[2]={LAX.coordinate,JFK.coordinate};MKGeodesicPolyline*geodesicPolyline=[MKGeodesicPolylinepolylineWithCoordinates:coordinatescount:2];[mapViewaddOverlay:geodesicPolyline];
Although the overlay looks like a smooth curve, it is actually comprised of thousands of tiny line segments (true to its MKPolyline
lineage):
NSLog(@"%d",geodesicPolyline.pointsCount)// 3984
Like any object conforming to the <MKOverlay>
protocol, an MKGeodesicPolyline
instance is displayed by adding to an MKMapView
with -addOverlay:
and implementing mapView:rendererForOverlay:
:
Rendering MKGeodesicPolyline
on an MKMapView
#pragma mark - MKMapViewDelegate-(MKOverlayRenderer*)mapView:(MKMapView*)mapViewrendererForOverlay:(id<MKOverlay>)overlay{if(![overlayisKindOfClass:[MKPolylineclass]]){returnnil;}MKPolylineRenderer*renderer=[[MKPolylineRendereralloc]initWithPolyline:(MKPolyline*)overlay];renderer.lineWidth=3.0f;renderer.strokeColor=[UIColorblueColor];renderer.alpha=0.5;returnrenderer;}
For comparison, here's the same geodesic overlaid with a route created from
MKDirections
:
As the crow flies, it's 3,983 km.
As the wolf runs, it's 4,559 km—nearly 15% longer.
...and that's just distance; taking into account average travel speed, the total time is ~5 hours by air and 40+ hours by land.
Animating an MKAnnotationView
on a MKGeodesicPolyline
Since geodesics make reasonable approximations for flight paths, a common use case would be to animate the trajectory of a flight over time.
To do this, we'll make properties for our map view and geodesic polyline between LAX and JFK, and add new properties for the planeAnnotation
and planeAnnotationPosition
(the index of the current map point for the polyline):
@interfaceMapViewController()<MKMapViewDelegate>@propertyMKMapView*mapView;@propertyMKGeodesicPolyline*flightpathPolyline;@propertyMKPointAnnotation*planeAnnotation;@propertyNSUIntegerplaneAnnotationPosition;@end
Next, right below the initialization of our map view and polyline, we create an MKPointAnnotation
for our plane:
self.planeAnnotation=[[MKPointAnnotationalloc]init];self.planeAnnotation.title=NSLocalizedString(@"Plane",nil);[self.mapViewaddAnnotation:self.planeAnnotation];[selfupdatePlanePosition];
That call to updatePlanePosition
in the last line ticks the animation and updates the position of the plane:
-(void)updatePlanePosition{staticNSUIntegerconststep=5;if(self.planeAnnotationPosition+step>self.flightpathPolyline.pointCount){return;}self.planeAnnotationPosition+=step;MKMapPointnextMapPoint=self.flightpathPolyline.points[self.planeAnnotationPosition];self.planeAnnotation.coordinate=MKCoordinateForMapPoint(nextMapPoint);[selfperformSelector:@selector(updatePlanePosition)withObject:nilafterDelay:0.03];}
We'll perform this method roughly 30 times a second, until the plane has arrived at its final destination.
Finally, we implement mapView:viewForAnnotation:
to have the annotation render on the map view:
-(MKAnnotationView*)mapView:(MKMapView*)mapViewviewForAnnotation:(id<MKAnnotation>)annotation{staticNSString*PinIdentifier=@"Pin";MKAnnotationView*annotationView=[mapViewdequeueReusableAnnotationViewWithIdentifier:PinIdentifier];if(!annotationView){annotationView=[[MKAnnotationViewalloc]initWithAnnotation:annotationreuseIdentifier:PinIdentifier];};annotationView.image=[UIImageimageNamed:@"plane"];returnannotationView;}
Hmm.... close but no SkyMall Personalized Cigar Case Flask.
Let's update the rotation of the plane as it moves across its flightpath.
Rotating an MKAnnotationView
along a Path
To calculate the plane's direction, we'll take the slope from the previous and next points:
MKMapPointpreviousMapPoint=self.flightpathPolyline.points[self.planeAnnotationPosition];self.planeAnnotationPosition+=step;MKMapPointnextMapPoint=self.flightpathPolyline.points[self.planeAnnotationPosition];self.planeDirection=XXDirectionBetweenPoints(previousMapPoint,nextMapPoint);self.planeAnnotation.coordinate=MKCoordinateForMapPoint(nextMapPoint);
XXDirectionBetweenPoints
is a function that returns a CLLocationDirection
(0 – 360 degrees, where North = 0) given two MKMapPoint
s.
We calculate from
MKMapPoint
s rather than converted coordinates, because we're interested in the slope of the line on the flat projection.
staticCLLocationDirectionXXDirectionBetweenPoints(MKMapPointsourcePoint,MKMapPointdestinationPoint){doublex=destinationPoint.x-sourcePoint.x;doubley=destinationPoint.y-sourcePoint.y;returnfmod(XXRadiansToDegrees(atan2(y,x)),360.0f)+90.0f;}
That convenience function XXRadiansToDegrees
(and its partner, XXDegreesToRadians
) are simply:
staticinlinedoubleXXRadiansToDegrees(doubleradians){returnradians*180.0f/M_PI;}staticinlinedoubleXXDegreesToRadians(doubledegrees){returndegrees*M_PI/180.0f;}
That direction is stored in a new property, @property CLLocationDirection planeDirection;
, calculated from self.planeDirection = CLDirectionBetweenPoints(currentMapPoint, nextMapPoint);
in updatePlanePosition
(ideally renamed to updatePlanePositionAndDirection
with this addition). To make the annotation rotate, we apply a transform
on annotationView
:
annotationView.transform=CGAffineTransformRotate(self.mapView.transform,XXDegreesToRadians(self.planeDirection));
Ah much better! At last, we have mastered the skies with a fancy visualization, worthy of any travel-related app.
Perhaps more than any other system framework, MapKit has managed to get incrementally better, little by little with every iOS release [1][2]. For anyone with a touch-and-go relationship to the framework, returning after a few releases is a delightful experience of discovery and rediscovery.
I look forward to seeing what lies on the horizon with iOS 8 and beyond.