Size an MKMapView to Fit its Annotations in iOS Without Futzing with Coordinate Systems
March 2, 2012 13 Comments
An oddity of MKMapView is that it always defaults to showing the western hemisphere of the Earth. That’s almost certainly not what you want. Instead, you have probably set an array of id <MKAnnotation> objects which you want to appear as pins on the map. If the pins were evenly distributed over the USA, then great. Otherwise, the MapView is not at all zoomed the way you want. It baffles me a map view doesn’t just zoom itself by default when you set annotations, but it doesn’t.
One approach might be to loop over all of your annotations and figure out what the max and min x and y values are across them all. Then get the center point of the map view and do some math to convert the coordinates and scale the map view.
Alternatively, MapKit has API that is separate from the map view itself for manipulating rectangles and regions which can do the job automatically.
The main problem is that id <MKAnnotation> objects contain a CoreLocation coordinate – CLLocationCoordinate2D – which represents a GPS coordinate while MapView has an MKCoordinateRegion which has a CLLocationCoordinate2D defining its center but uses an MKCoordinateRegion span struct to define the zoom level of the map view in terms of degrees of arc.
MapKit provides a mix of object-oriented API and C functions to convert an array of CLLocationCoordinate2D structs from id <MKAnnotation> objects into an MKCoordinateRegion suitable for setting the region in a MKMapView. These transformations are straightforward and don’t require fiddling with converting between coordinate systems.
Here is the code in a map view controller
#define MINIMUM_ZOOM_ARC 0.014 //approximately 1 miles (1 degree of arc ~= 69 miles) #define ANNOTATION_REGION_PAD_FACTOR 1.15 #define MAX_DEGREES_ARC 360 //size the mapView region to fit its annotations - (void)zoomMapViewToFitAnnotations:(MKMapView *)mapView animated:(BOOL)animated { NSArray *annotations = mapView.annotations; int count = [mapView.annotations count]; if ( count == 0) { return; } //bail if no annotations //convert NSArray of id <MKAnnotation> into an MKCoordinateRegion that can be used to set the map size //can't use NSArray with MKMapPoint because MKMapPoint is not an id MKMapPoint points[count]; //C array of MKMapPoint struct for( int i=0; i<count; i++ ) //load points C array by converting coordinates to points { CLLocationCoordinate2D coordinate = [(id <MKAnnotation>)[annotations objectAtIndex:i] coordinate]; points[i] = MKMapPointForCoordinate(coordinate); } //create MKMapRect from array of MKMapPoint MKMapRect mapRect = [[MKPolygon polygonWithPoints:points count:count] boundingMapRect]; //convert MKCoordinateRegion from MKMapRect MKCoordinateRegion region = MKCoordinateRegionForMapRect(mapRect); //add padding so pins aren't scrunched on the edges region.span.latitudeDelta *= ANNOTATION_REGION_PAD_FACTOR; region.span.longitudeDelta *= ANNOTATION_REGION_PAD_FACTOR; //but padding can't be bigger than the world if( region.span.latitudeDelta > MAX_DEGREES_ARC ) { region.span.latitudeDelta = MAX_DEGREES_ARC; } if( region.span.longitudeDelta > MAX_DEGREES_ARC ){ region.span.longitudeDelta = MAX_DEGREES_ARC; } //and don't zoom in stupid-close on small samples if( region.span.latitudeDelta < MINIMUM_ZOOM_ARC ) { region.span.latitudeDelta = MINIMUM_ZOOM_ARC; } if( region.span.longitudeDelta < MINIMUM_ZOOM_ARC ) { region.span.longitudeDelta = MINIMUM_ZOOM_ARC; } //and if there is a sample of 1 we want the max zoom-in instead of max zoom-out if( count == 1 ) { region.span.latitudeDelta = MINIMUM_ZOOM_ARC; region.span.longitudeDelta = MINIMUM_ZOOM_ARC; } [mapView setRegion:region animated:animated]; } - (void)viewWillAppear:(BOOL)animated { [self zoomMapViewToFitAnnotations:self.mapView animated:animated]; //or maybe you would do the call above in the code path that sets the annotations array }
Thank you Brian, this was very helpful and nicely encapsulated as well.
Hi Brian, have you noticed that when there are two annotations, one located too far to the East and another located too far to the West, the map view does not zoom out enough to show them both on screen?
Thanks a lot. Clean code 🙂
was very very useful code. thanks so much!
Thanks for the Tip! Here is a quick translation to MonoTouch,…
#region FitAnnotations
// https://brianreiter.org/tag/iphone/
const double MINIMUM_ZOOM_ARC = 0.014; //approximately 1 miles (1 degree of arc ~= 69 miles)
const double ANNOTATION_REGION_PAD_FACTOR = 1.15;
const double MAX_DEGREES_ARC = 360.0;
//size the mapView region to fit its annotations
void zoomMapViewToFitAnnotations(MonoTouch.MapKit.MKMapView mapView, bool animated)
{
NSObject[] annotations = mapView.Annotations;
int count = mapView.Annotations.GetLength(0);
if ( count == 0) { return; } //bail if no annotations
//convert NSArray of id into an MKCoordinateRegion that can be used to set the map size
//can’t use NSArray with MKMapPoint because MKMapPoint is not an id
MKMapPoint[] points = new MKMapPoint[count]; //C array of MKMapPoint struct
for( int i=0; i<count; i++ ) //load points C array by converting coordinates to points
{
//// [(id )[annotations objectAtIndex:i] coordinate];
MonoTouch.CoreLocation.CLLocationCoordinate2D coordinate = ((MonoTouch.CoreLocation.CLPlacemark)annotations[i]).Location.Coordinate;
points[i] = MonoTouch.MapKit.MKMapPoint.FromCoordinate(coordinate);
}
//create MKMapRect from array of MKMapPoint
//MKMapRect mapRect = [[MKPolygon polygonWithPoints:points count:count] boundingMapRect];
MKMapRect mapRect = MKPolygon.FromPoints(points).BoundingMapRect;
//convert MKCoordinateRegion from MKMapRect
MKCoordinateRegion region = MKCoordinateRegion.FromMapRect(mapRect);
//add padding so pins aren’t scrunched on the edges
region.Span.LatitudeDelta *= ANNOTATION_REGION_PAD_FACTOR;
region.Span.LongitudeDelta *= ANNOTATION_REGION_PAD_FACTOR;
//but padding can’t be bigger than the world
if( region.Span.LatitudeDelta > MAX_DEGREES_ARC ) { region.Span.LatitudeDelta = MAX_DEGREES_ARC; }
if( region.Span.LongitudeDelta > MAX_DEGREES_ARC ){ region.Span.LongitudeDelta = MAX_DEGREES_ARC; }
//and don’t zoom in stupid-close on small samples
if( region.Span.LatitudeDelta < MINIMUM_ZOOM_ARC ) { region.Span.LatitudeDelta = MINIMUM_ZOOM_ARC; }
if( region.Span.LongitudeDelta < MINIMUM_ZOOM_ARC ) { region.Span.LongitudeDelta = MINIMUM_ZOOM_ARC; }
//and if there is a sample of 1 we want the max zoom-in instead of max zoom-out
if( count == 1 )
{
region.Span.LatitudeDelta = MINIMUM_ZOOM_ARC;
region.Span.LongitudeDelta = MINIMUM_ZOOM_ARC;
}
mapView.SetRegion(region, animated);
}
public override void ViewWillAppear (bool animated)
{
base.ViewWillAppear (animated);
zoomMapViewToFitAnnotations(MyMapView, animated);
//or maybe you would do the call above in the code path that sets the annotations array
}
#endregion
Pingback: Sizing MKMapView to Fit All Annotations
Do we need to use a C array of MKMapPoint struct, or could we instead use a C array of CLLocationCoordinate2D, as that is also a struct? As follows:
CLLocationCoordinate2D coords[count]; //C array of CLLocationCoordinate2D struct
for( int i=0; i<count; i++ ) { //load coordinates C array
coords[i] = [(id )[annotations objectAtIndex:i] coordinate];
}
//create MKMapRect from array of CLLocationCoordinate2D
MKMapRect mapRect = [[MKPolygon polygonWithCoordinates:coords count:count] boundingMapRect];
Thank you for this very helpful post! I compiled a list of some other top tutorials on ways to add custom annotations to the MapView Control. Hope other developers find this useful too 🙂 http://www.verious.com/board/Giancarlo-Leonio/map-annotation-for-ios/
Thanks for this very helpful post. I compiled a list of top tutorials on ways to add custom annotations to the MapView control. Hope other developers find it useful too 🙂
http://www.verious.com/board/Giancarlo-Leonio/map-annotation-for-ios/
very helpful code.. thanks a lot
This is really amazing and perfect solution for me. Thanks a lot 🙂
Swift version:
///size the mapView region to fit its annotations
// From: https://brianreiter.org/2012/03/02/size-an-mkmapview-to-fit-its-annotations-in-ios-without-futzing-with-coordinate-systems/
func zoomMapViewToFitAnnotations(mapView: MKMapView, animated: Bool) {
let MINIMUM_ZOOM_ARC = 0.014 //approximately 1 miles (1 degree of arc ~= 69 miles)
let ANNOTATION_REGION_PAD_FACTOR = 1.15
let MAX_DEGREES_ARC: CLLocationDegrees = 360
var annotations = mapView.annotations
let count = mapView.annotations.count
if (count == 0) { return } //bail if no annotations
//convert NSArray of id into an MKCoordinateRegion that can be used to set the map size
var points = [MKMapPoint]() //C array of MKMapPoint struct
for annotation in annotations {
let coordinate = (annotation as MKAnnotation).coordinate
points.append(MKMapPointForCoordinate(coordinate))
}
//create MKMapRect from array of MKMapPoint
let polygon = MKPolygon(points: UnsafeMutablePointer(points), count:count)
let mapRect = polygon.boundingMapRect
//convert MKCoordinateRegion from MKMapRect
var region = MKCoordinateRegionForMapRect(mapRect)
//add padding so pins aren’t scrunched on the edges
region.span.latitudeDelta *= ANNOTATION_REGION_PAD_FACTOR
region.span.longitudeDelta *= ANNOTATION_REGION_PAD_FACTOR
//but padding can’t be bigger than the world
if (region.span.latitudeDelta > MAX_DEGREES_ARC) { region.span.latitudeDelta = MAX_DEGREES_ARC }
if (region.span.longitudeDelta > MAX_DEGREES_ARC) { region.span.longitudeDelta = MAX_DEGREES_ARC }
//and don’t zoom in stupid-close on small samples
if (region.span.latitudeDelta < MINIMUM_ZOOM_ARC) { region.span.latitudeDelta = MINIMUM_ZOOM_ARC }
if (region.span.longitudeDelta < MINIMUM_ZOOM_ARC) { region.span.longitudeDelta = MINIMUM_ZOOM_ARC }
//and if there is a sample of 1 we want the max zoom-in instead of max zoom-out
if (count == 1) {
region.span.latitudeDelta = MINIMUM_ZOOM_ARC
region.span.longitudeDelta = MINIMUM_ZOOM_ARC
}
mapView.setRegion(region, animated: animated)
}
Very helpfull, thanks a lot!