Warning: join(): Invalid arguments passed in /home/troyb/troybrant.net/blog/wp-content/themes/hybrid-hacked/hybrid-hacked/library/functions/breadcrumbs.php on line 79

Set the Zoom Level of an MKMapView

If you have ever built a web application using the Google Maps API, you are likely intimately familiar with this line of code:

map.setCenter(new google.maps.LatLng(37.4419, -122.1419), 13);

The setCenter JavaScript method takes in the center coordinate and a zoom level. The zoom level, as you might expect, determines how far the map should zoom in. The zoom level ranges from 0 (all the way zoomed out) to some upper value (all the way zoomed in). The max zoom level for a particular area depends on the location (for instance, you can’t zoom in too far on North Korea) and the map type (default, satellite, hybrid, terrain, etc). Typically, the max zoom level for an area is somewhere between 15 – 21.

Unfortunately, MapKit on the iPhone does not include a way to set the zoom level. Instead, the zoom level is set implicitly by defining the MKCoordinateRegion of the map’s viewport. When initializing the region, you specify the amount of distance the map displays in the horizontal and vertical directions. The zoom level is set implicitly based on these distance values.

Instead of dealing with this region business, I wrote a category that adds support for setting the zoom level of an MKMapView explicitly. In this post, I’ll give you code you can drop into your own projects and start using immediately. My next post will detail exactly how it works.

The Code

Instead of force-feeding you everything I learned while working on this mini-project, I’ll give you the goods up front. The code below defines a category on MKMapView that gives you the ability to set the zoom level for your map:

// MKMapView+ZoomLevel.h

#import <MapKit/MapKit.h>

@interface MKMapView (ZoomLevel)

- (void)setCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate
    zoomLevel:(NSUInteger)zoomLevel
    animated:(BOOL)animated;

@end

// MKMapView+ZoomLevel.m

#import "MKMapView+ZoomLevel.h"

#define MERCATOR_OFFSET 268435456
#define MERCATOR_RADIUS 85445659.44705395

@implementation MKMapView (ZoomLevel)

#pragma mark -
#pragma mark Map conversion methods

- (double)longitudeToPixelSpaceX:(double)longitude
{
    return round(MERCATOR_OFFSET + MERCATOR_RADIUS * longitude * M_PI / 180.0);
}

- (double)latitudeToPixelSpaceY:(double)latitude
{
    return round(MERCATOR_OFFSET - MERCATOR_RADIUS * logf((1 + sinf(latitude * M_PI / 180.0)) / (1 - sinf(latitude * M_PI / 180.0))) / 2.0);
}

- (double)pixelSpaceXToLongitude:(double)pixelX
{
    return ((round(pixelX) - MERCATOR_OFFSET) / MERCATOR_RADIUS) * 180.0 / M_PI;
}

- (double)pixelSpaceYToLatitude:(double)pixelY
{
    return (M_PI / 2.0 - 2.0 * atan(exp((round(pixelY) - MERCATOR_OFFSET) / MERCATOR_RADIUS))) * 180.0 / M_PI;
}

#pragma mark -
#pragma mark Helper methods

- (MKCoordinateSpan)coordinateSpanWithMapView:(MKMapView *)mapView
    centerCoordinate:(CLLocationCoordinate2D)centerCoordinate
    andZoomLevel:(NSUInteger)zoomLevel
{
    // convert center coordiate to pixel space
    double centerPixelX = [self longitudeToPixelSpaceX:centerCoordinate.longitude];
    double centerPixelY = [self latitudeToPixelSpaceY:centerCoordinate.latitude];
    
    // determine the scale value from the zoom level
    NSInteger zoomExponent = 20 - zoomLevel;
    double zoomScale = pow(2, zoomExponent);
    
    // scale the map’s size in pixel space
    CGSize mapSizeInPixels = mapView.bounds.size;
    double scaledMapWidth = mapSizeInPixels.width * zoomScale;
    double scaledMapHeight = mapSizeInPixels.height * zoomScale;
    
    // figure out the position of the top-left pixel
    double topLeftPixelX = centerPixelX - (scaledMapWidth / 2);
    double topLeftPixelY = centerPixelY - (scaledMapHeight / 2);
    
    // find delta between left and right longitudes
    CLLocationDegrees minLng = [self pixelSpaceXToLongitude:topLeftPixelX];
    CLLocationDegrees maxLng = [self pixelSpaceXToLongitude:topLeftPixelX + scaledMapWidth];
    CLLocationDegrees longitudeDelta = maxLng - minLng;
    
    // find delta between top and bottom latitudes
    CLLocationDegrees minLat = [self pixelSpaceYToLatitude:topLeftPixelY];
    CLLocationDegrees maxLat = [self pixelSpaceYToLatitude:topLeftPixelY + scaledMapHeight];
    CLLocationDegrees latitudeDelta = -1 * (maxLat - minLat);
    
    // create and return the lat/lng span
    MKCoordinateSpan span = MKCoordinateSpanMake(latitudeDelta, longitudeDelta);
    return span;
}

#pragma mark -
#pragma mark Public methods

- (void)setCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate
    zoomLevel:(NSUInteger)zoomLevel
    animated:(BOOL)animated
{
    // clamp large numbers to 28
    zoomLevel = MIN(zoomLevel, 28);
    
    // use the zoom level to compute the region
    MKCoordinateSpan span = [self coordinateSpanWithMapView:self centerCoordinate:centerCoordinate andZoomLevel:zoomLevel];
    MKCoordinateRegion region = MKCoordinateRegionMake(centerCoordinate, span);
    
    // set the region like normal
    [self setRegion:region animated:animated];
}

@end

If you’re wondering why this works, check out my next post where I describe in gruesome detail the math behind the code.

On the other hand, if you don’t really care how it works, just that it does work, copy and paste away, my friend.

Test the Code

To test the category, assuming you have a view controller with an MKMapView instance, you can use the following code:

// ZoomLevelTestViewController.m

#import "MKMapView+ZoomLevel.h"

#define GEORGIA_TECH_LATITUDE 33.777328
#define GEORGIA_TECH_LONGITUDE -84.397348

#define ZOOM_LEVEL 14

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    
    CLLocationCoordinate2D centerCoord = { GEORGIA_TECH_LATITUDE, GEORGIA_TECH_LONGITUDE };
    [mapView setCenterCoordinate:centerCoord zoomLevel:ZOOM_LEVEL animated:NO];
}

And, viola! Your map should zoom in to where you can see the Georgia Tech campus.

Results

To verify that the zoom level is set correctly, I wrote a simple web-based Google Maps application to make sure the web and native zoom levels matched. Both apps were centered at {33.777328, -84.397348} (Georgia Tech). In the images below, the iPhone on the left is running the native app and the iPhone on the right is running the web app:

Zoom Level 2: Native app on the left. Web app on the right.

Zoom Level 10: Native app on the left. Web app on the right.

Zoom Level 18: Native app on the left. Web app on the right.

As you can see, they match.

That’s a Wrap

By using the MKMapView+ZoomLevel category, you won’t have to bother setting the region at all. If you are like me and have no intuition for how to set the map’s region, then hopefully this code will give you a bit more control in setting your map’s zoom level.

Next time, I’ll go over exactly why the code above works. But, for now, enjoy the freedom to set zoom levels!

73 Responses to “Set the Zoom Level of an MKMapView”

  1. works great!!!

    just one minor issue when I try to copy the code, it seems that the “-” (minus) sign inside is replaced by unicode inside the HTML.

    also, for your reference, I wrote a simple function to derive the zoom level from the current map :P

    - (int) getZoomLevel
    {
    return 21 – round(log2(mapView.region.span.longitudeDelta * MERCATOR_RADIUS * M_PI / (180.0 * mapView.bounds.size.width)));
    }

  2. Thanks for the heads-up on the minus sign problem. I’ve updated the HTML so the code is now officially copy-and-pastable.

    Also, thanks for sharing the getZoomLevel method! We’ll make this category fully functional, yet.

  3. Sweet post on the whole, swear I noticed a grammar mistake but I may be wrong it’s late :D

  4. Good post… I’m always on the lookout for good blogs.

  5. Hi, great work, but I can’t get it to work. I’m trying to use it in a UITabBar app with one mkmapview but it doesn’t recognize the new method, can you tell me how you initialize the mapview object? I’m trying to do it directly from the interface builder and using de view on the viewdidappear method.

    Thanks, and keep the good job! ;)

  6. My fault, it works perfectly. Thanks for this! :D

  7. Thanks!!

  8. wow! nice!!!

  9. :-) cool!

  10. Very nice solution! Helped me a lot in my project!!

  11. Amazing! This is very helpful for me. Thank you so much!

  12. It’s great! Thanks!!!
    I have a question about how to get current zoom level when I enlarge the mapView

  13. I see!哈哈

  14. Thank you for the getZoomLevel method. I was trying to reverse the algorithm using the longitudeDelta and my head started to hurt. @_@ It’s been awhile sense I’ve done math like that. Anyways great tutorial! Great algorithm! Awesome blog. Defiantly been favorited!

  15. Hi,
    This is really interesting and am exactly looking for this. Does any one have working code with these zoom level functionality. I tried to create mine but not working as expected. can some one please post working code it will be really helpful.
    Thank you

  16. cool :-)

  17. Hi Troy,

    Thanks for this really useful code. I was have problems creating valid MKCoordinateSpans for coordinates around the poles and this code helped me out. Note that even Apple’s MKCoordinateRegionMakeWithDistance suffers from this problem, which can be really annoying when it throws an exception when you set the region on an MKMapView.

    However, you still need to adjust the latitudeToPixelSpaceY call as follows to avoid infinite values:

    + (double)latitudeToPixelSpaceY:(double)latitude
    {
    if (latitude == 90.0) {
    return 0;
    } else if (latitude == -90.0) {
    return 256;
    } else {
    return round(MERCATOR_OFFSET – MERCATOR_RADIUS * logf((1 + sinf(latitude * M_PI / 180.0)) / (1 – sinf(latitude * M_PI / 180.0))) / 2.0);
    }
    }

    Cheers,

    Adam

  18. Further to my previous comment, there was more to change than just adjusting the calc to avoid infinite values — MKMapView cannot display tiles that cross the pole (as these would involve wrapping the map from top to bottom, something that a Mercator projection just cannot do).

    Consequently, I adjusted the code as shown below. I also created a unit test if that’s of interest to anyone…

    Cheers,

    Adam

    - (double)latitudeToPixelSpaceY:(double)latitude
    {
    if (latitude == 90.0) {
    return 0;
    } else if (latitude == -90.0) {
    return MERCATOR_OFFSET * 2;
    } else {
    return round(MERCATOR_OFFSET – MERCATOR_RADIUS * logf((1 + sinf(latitude * M_PI / 180.0)) / (1 – sinf(latitude * M_PI / 180.0))) / 2.0);
    }
    }

    - (MKCoordinateRegion)coordinateRegionWithMapView:(MKMapView *)mapView
    centerCoordinate:(CLLocationCoordinate2D)centerCoordinate
    andZoomLevel:(NSUInteger)zoomLevel
    {
    // clamp lat/long values to appropriate ranges
    centerCoordinate.latitude = MIN(MAX(-90.0, centerCoordinate.latitude), 90.0);
    centerCoordinate.longitude = fmod(centerCoordinate.longitude, 180.0);

    // convert center coordiate to pixel space
    double centerPixelX = [MKMapView longitudeToPixelSpaceX:centerCoordinate.longitude];
    double centerPixelY = [MKMapView latitudeToPixelSpaceY:centerCoordinate.latitude];

    // determine the scale value from the zoom level
    NSInteger zoomExponent = 20 – zoomLevel;
    double zoomScale = pow(2, zoomExponent);

    // scale the map’s size in pixel space
    CGSize mapSizeInPixels = mapView.bounds.size;
    double scaledMapWidth = mapSizeInPixels.width * zoomScale;
    double scaledMapHeight = mapSizeInPixels.height * zoomScale;

    // figure out the position of the left pixel
    double topLeftPixelX = centerPixelX – (scaledMapWidth / 2);

    // find delta between left and right longitudes
    CLLocationDegrees minLng = [MKMapView pixelSpaceXToLongitude:topLeftPixelX];
    CLLocationDegrees maxLng = [MKMapView pixelSpaceXToLongitude:topLeftPixelX + scaledMapWidth];
    CLLocationDegrees longitudeDelta = maxLng – minLng;

    // if we’re at a pole then calculate the distance from the pole towards the equator
    // as MKMapView doesn’t like drawing boxes over the poles
    double topPixelY = centerPixelY – (scaledMapHeight / 2);
    double bottomPixelY = centerPixelY + (scaledMapHeight / 2);
    BOOL adjustedCenterPoint = NO;
    if (topPixelY MERCATOR_OFFSET * 2) {
    topPixelY = centerPixelY – scaledMapHeight;
    bottomPixelY = MERCATOR_OFFSET * 2;
    adjustedCenterPoint = YES;
    }

    // find delta between top and bottom latitudes
    CLLocationDegrees minLat = [MKMapView pixelSpaceYToLatitude:topPixelY];
    CLLocationDegrees maxLat = [MKMapView pixelSpaceYToLatitude:bottomPixelY];
    CLLocationDegrees latitudeDelta = -1 * (maxLat – minLat);

    // create and return the lat/lng span
    MKCoordinateSpan span = MKCoordinateSpanMake(latitudeDelta, longitudeDelta);
    MKCoordinateRegion region = MKCoordinateRegionMake(centerCoordinate, span);
    // once again, MKMapView doesn’t like drawing boxes over the poles
    // so adjust the center coordinate to the center of the resulting region
    if (adjustedCenterPoint) {
    region.center.latitude = [MKMapView pixelSpaceYToLatitude:((bottomPixelY + topPixelY) / 2.0)];
    }

    return region;
    }

  19. Thanks a lot Troy, works like a charm !!!

    Cheers,
    Nicolas

  20. Troy,

    I’ve had an attempt at doing some code to work out the inverse function, given a span, work out the zoom level; this might be useful for people who want to change annotations at different zoom levels when the region changes (e.g. clustering). Warning: this code’s not been thoroughly tested!

    Here’s the code:

    - (NSUInteger) zoomLevelWithMapView: (MKMapView*) mapView {
    MKCoordinateRegion region = self.region;

    double centerPixelX = [self longitudeToPixelSpaceX: region.center.longitude];
    double topLeftPixelX = [self longitudeToPixelSpaceX: region.center.longitude - region.span.longitudeDelta / 2];

    double scaledMapWidth = (centerPixelX – topLeftPixelX) * 2;
    CGSize mapSizeInPixels = mapView.bounds.size;
    double zoomScale = scaledMapWidth / mapSizeInPixels.width;
    double zoomExponent = log(zoomScale) / log(2);
    double zoomLevel = 20 – zoomExponent;

    return zoomLevel;
    }

  21. My hero… This is amazing code and my poor feeble brain thanks you for putting this out there.

  22. Any ideas how we can change the center region outside viewDidAppear method ?

  23. Is there a way to set the max zoom level, lets say to level 14. Have the map only zoom to level 14 or snap back like a rubber band when panning?

  24. WoooW, great tutorial.

    I have used it in my app “Strangeness from Earth”, i will insert in the next update.
    Now my app use 3 methods for manage the zoom level:
    multitouch gestures, UIButton and UISlider.
    I use take the Span from zoomLevel and i use the inverse function of user Dave to take the zoomLevel from actual span (i need to update the position of UISlider)

    But, i have a question…
    How can I know the exact Maximum Level of Zoom for my actual center coordinate???

    Thank you.

  25. I personally haven’t found a way to get the max zoom region or max zoom level.

  26. Troy, I’ve published your sources as a publit GitHub repository (https://github.com/vhbit/MKMapViewZoom). Let me know if you wish it to be removed.

  27. Thanks, this works great :-)

  28. Hi, Troy,
    Great Tutorial…..

    Thanks to you man…
    But I have one query about it… that zoom Level defines what….?? is it in miles….?? I mean if i declare zoom level = 15 then it shows map from 15 miles…. means vertical distance…??

    Thanks in advance…

  29. One day I’ll get my act together and post all this code on in a proper repo, but until then, thanks for the assist.

  30. Thanks so much. This really helped me with the app I’m building Troy

  31. Nice job! Works as advertised and i don’t have to understand any of the math behind the scenes :

  32. Wow, thanks for this! I was struggling trying to do this myself with the setRegion: method and trying to set the latitudeDelta and longitudeDelta and wasn’t getting anywhere.

    This worked on the first try!

  33. Merged zoomLevel changes and Adam Cohen-Rose code here https://github.com/jdp-global/MKMapViewZoom

  34. I enjoyed your post very much.
    However I wonder how much room there is for approximation: my app is based on Google Static Maps, it will use zoomlevels 13-18 and it will always be used with geo-location in central-european countries. I’ve been looking for a simple table providing me with the relation between pixelDelta and latLongDelta given zoomLevel, but I couldn’t find it. My intention is to implement your routines once to calculate those relations and feed them to my app, publishing a blog page in the process.
    What is your take with regards to the magnitude of error that this process will entail? Thanks in advance, and again thanks a lot for publishing this page

  35. It is so powerful ! Thank you !

    Where is an example to download !?

    When I use this ,could it pass the AppStore!?

  36. AWESOME! SO AWESOME! Thanks Thanks Thanks!!!

    This code rocks my socks off. Also props to Walty for the getZoomLevel as well.

  37. Awsome code but somehow I am receiving following error “implicit-conversion-shortens-64-bit-to-32-bit” at code “return round(MERCATOR_OFFSET – MERCATOR_RADIUS * logf((1 + sinf(latitude * M_PI / 180.0)) / (1 – sinf(latitude * M_PI / 180.0))) / 2.0)”…any idea why ?

    Thanks,

  38. I found it much simpler to set the visibleRect than to set the region of an MKMapView (and more accurate results too, when using a specific zoomScale, as opposed to a zoomLevel.

    - (MKMapRect)mapRectWithCentreCoordinate:(CLLocationCoordinate2D)centreCoordinate zoomScale:(double)zoomScale
    {
    MKMapPoint centrePoint = MKMapPointForCoordinate(centreCoordinate);
    double scaledWidth = self.bounds.size.width / zoomScale;
    double scaledHeight = self.bounds.size.height / zoomScale;

    return MKMapRectMake(centrePoint.x – (scaledWidth / 2),
    centrePoint.y – (scaledHeight / 2),
    scaledWidth,
    scaledHeight);
    }

    - (void)setCentreCoordinate:(CLLocationCoordinate2D)centreCoordinate zoomScale:(double)zoomScale animated:(BOOL)animated
    {
    [self setVisibleMapRect:[self mapRectWithCentreCoordinate:centreCoordinate zoomScale:zoomScale] animated:animated];
    }

    and then if you want it to work for zoomLevel, as well as for zoomScale, you can reuse the same code simply by adding the following (untested):

    - (void)setCentreCoordinate:(CLLocationCoordinate2D)centreCoordinate zoomLevel:(NSUInteger)zoomLevel animated:(BOOL)animated
    {
    // clamp large numbers to 28
    zoomLevel = MIN(zoomLevel, 28);

    [self setCentreCoordinate:centreCoordinate zoomScale:[self zoomScaleForZoomLevel:zoomLevel] animated:animated];
    }

  39. Hi Troy,

    I’m not sure if I missed it, but there doesn’t seem to be a clear set of distribution terms with your code here. Your blog has a general copyright, but your language makes this code sound like it’s free for anyone to use. Is your work here Public Domain? Released under a specific Open Source License?

    Thanks

  40. Voila, not viola :P

    10x for the nice code ;)

  41. I believe there is quite a significant error in the original code (and all variations). The line

    NSInteger zoomExponent = 20 – zoomLevel;

    should actually be:

    NSInteger zoomExponent = 21 – zoomLevel;

    I combined the setCenterCoordinate:zoomLevel:animated: code with the getZoomLevel function (see walty’s post)and created / connected two buttons (Zoom+ and Zoom-). It’s fairly simple code:

    [mapView setCenterCoordinate:mapView.centerCoordinate zoomLevel:mapZoomLevel+1 ...];

    and

    [mapView setCenterCoordinate:mapView.centerCoordinate zoomLevel:mapZoomLevel-1 ...];

    The result was zoom buttons which jump up 2 increments (Zoom+) or 0 (Zoom -).

    The value 268435456 is actually quite significant: it is the number of pixels (horizontal or vertical) at zoom level 20. Tiles are always 256 x 256 pixels. The value 268435456 == 256 * 2 ^20; in other words, at zoom level 20, there are 1048576 (or 2 ^ 20) tiles, each with 256 (2 ^ 8) pixels.

    I’m very confident in the zoomLevel calculations; I base-lined the equation by measuring MapKit tiling at its minimum magnification. At minimum zoom, in a full-screen iPad view, MapKit shows 16 tiles, which correlates to zoom level 2.

    If you try using the erroneous code, and setting your zoom level to 2 (which should show 16 tiles), you will actually have a map at zoom level 3 (containing 64 files).

    Thanks for a great writeup – it’s been hard validating all of the related map math, and this (and your other mapping explanation) saved me a bunch of time in doing so.

  42. Thanks! Works like a charm!

  43. This is great… but I am having a small issue. Once I do the initial zoom I want it to stay wherever the user puts it – not recenter and rezoom.

    How do I make it do this just once?

  44. For retina devices, I suggest using:

    CGSize mapSizeInPoints = self.bounds.size;
    CGSize mapSizeInPixels = CGSizeMake(mapSizeInPoints.width * [UIScreen mainScreen].scale, mapSizeInPoints.height * [UIScreen mainScreen].scale);

  45. I have some problem with new maps.
    When i increase the zoom, sometimes doesn’t work and the center of map go a little on the right.
    I have tried to understand the reason, but nothing.

    Some idea?

  46. The problem is in
    - (void) zoomLevelWithMapView: (MKMapView*) mapView {
    …..
    zoomLevel = 20 – zoomExponent;
    …..
    }

    Returns ever the same value, and map doesn’t zoom.

    Can depends from size of UIView map?
    How i can solve it?

    Thanks, Byteros.

  47. excellents article..love it lolzzzzzz

  48. Supper boss realy its rock
    thanks

  49. Very interesting article. In fact, there is an even much shorter implementation of the method:

    - (void)setCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate
    zoomLevel:(NSUInteger)zoomLevel
    animated:(BOOL)animated
    {
    [mapView setRegion:MKCoordinateRegionMake(centerCoordinate, MKCoordinateSpanMake(0, 360/pow(2, zoomLevel)*self.width/256)) animated:YES];
    }

    That’s it

  50. The capacities of 8, 12, roofing baltimore maryland 16, and
    20 quarts ensure you always have the right size pot for stews, soups, chili, and more.
    Hoffritz 4 Piece Nesting Aluminum Stockpot Set with Lids – This
    Hoffritz 4 Piece Nesting Aluminum Stockpot Set is perfect
    for preparing big family dinners, holiday meals, and
    large cookouts. The capacities of 8, 12, 16, and 20 quarts ensure you always have the right size pot for stews,
    soups, chili, and more.

Leave a Reply