Size an MKMapView to Fit its Annotations in iOS Without Futzing with Coordinate Systems

map viewAn 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
}
Advertisements

12 Responses to Size an MKMapView to Fit its Annotations in iOS Without Futzing with Coordinate Systems

  1. Dimity says:

    Thank you Brian, this was very helpful and nicely encapsulated as well.

  2. Konstantin Shagin says:

    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?

  3. Martin says:

    Thanks a lot. Clean code 🙂

  4. Johann says:

    was very very useful code. thanks so much!

  5. dlonghurst says:

    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

  6. Pingback: Sizing MKMapView to Fit All Annotations

  7. Miek says:

    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];

  8. 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/

  9. gleonio says:

    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/

  10. mc says:

    very helpful code.. thanks a lot

  11. RAVI KB says:

    This is really amazing and perfect solution for me. Thanks a lot 🙂

  12. Chris Morse says:

    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)
    }

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: