title: Keeping a MKMapView within a region
body: <p>I'm trying to keep a specific region of the earth on screen at all times. I've looked around and found code that kinda works and sorta works but none that actually works... so I'm written up my own code and it's working better than what I've found except for one off behavior.</p>
  
  <p>Dropping this method into a MKMapViewController will keep a region on the display and within a specific zoom range. If the region/zoom combination is <em>smaller</em> than the display, that is it is completely visible on the screen, then this works as expected.</p>
  
  <p>If the region/zoom combination is <em>larger</em> than the screen things sorta work. I detect the out of bound and I move the map but it seems as if my offsets are flipped? I get in the cituation where the map "bounces" back and forth or up and down.</p>
  
  <p>I can't seem to figure out what causes the bouncing or how to stop it.</p>
  
  <pre><code>- (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated
  {
      // Keep map in the zoom range we want
      int zoom = self.map.zoomLevel;
      if (self.map.zoomLevelFine &gt; (float)self.maxZoom &amp;&amp; self.maxZoom &gt; 0)
      {
          zoom = self.maxZoom;
          [self.map setCenterCoordinate:self.map.centerCoordinate zoomLevel:self.maxZoom animated:YES];
      }
      else if (self.map.zoomLevelFine &lt; (float)self.minZoom &amp;&amp; self.maxZoom &gt; 0)
      {
          zoom = self.minZoom;
          [self.map setCenterCoordinate:self.map.centerCoordinate zoomLevel:self.minZoom animated:YES];
      }
  
      // Figure out the current on screen boundry
      MKCoordinateSpan span = self.map.region.span;
      double north = self.map.centerCoordinate.latitude + (span.latitudeDelta / 2);
      double south = self.map.centerCoordinate.latitude - (span.latitudeDelta / 2);
      double east = self.map.centerCoordinate.longitude + (span.longitudeDelta / 2);
      double west = self.map.centerCoordinate.longitude - (span.longitudeDelta / 2);
  
      // get the offsets
      float offsetLong = 0.0;
      float offsetLat = 0.0;
      if (self.topLeftLong &lt; west &amp;&amp; self.btmRightLong &lt; east)
      {
          offsetLong = west - self.topLeftLong;
      }
      else if (self.topLeftLong &gt; west &amp;&amp; self.btmRightLong &gt; east)
      {
          offsetLong = east - self.btmRightLong;
      }
  
      if (self.topLeftLat &gt; north &amp;&amp; self.btmRightLat &gt; south)
      {
          offsetLat = north - self.topLeftLat;
      }
      else if (self.topLeftLat &lt; north &amp;&amp; self.btmRightLat &lt; south)
      {
          offsetLat = south - self.btmRightLat;
      }
  
      // Keep the map on screen
      if (offsetLat != 0.0 || offsetLong != 0.0)
      {
          [self.map setCenterCoordinate:CLLocationCoordinate2DMake(self.map.centerCoordinate.latitude - offsetLat,
                                                                   self.map.centerCoordinate.longitude - offsetLong)
                              zoomLevel:zoom animated:YES];
      }
  
      // Debug
      NSLog(@"Center: %f, %f, Zoom: %f", self.map.centerCoordinate.latitude, self.map.centerCoordinate.longitude, self.map.zoomLevelFine);
      NSLog(@"A north: %f west: %f south: %f east: %f", north, west, south, east);
      NSLog(@"B north: %f west: %f south: %f east: %f", self.topLeftLat, self.topLeftLong, self.btmRightLat, self.btmRightLong);
      NSLog(@"offset %f, %f", offsetLong, offsetLat);
  }
  </code></pre>
  
  <p>Adding this snippet into the init method will setup the map to show the island of Oahu on the screen.</p>
  
  <pre><code>// Get some basic setings
  self.topLeftLat = 21.714852;
  self.topLeftLong = -158.286896;
  self.btmRightLat = 21.245862;
  self.btmRightLong = -157.646942;
  self.minZoom = 9.0;
  self.maxZoom = 13.0;
  </code></pre>
  
  <p>The method above also uses a category to help manipulating the map.</p>
  
  <p>MKMapView+Extra.h</p>
  
  <pre><code>#import &lt;Foundation/Foundation.h&gt;
  #import &lt;MapKit/MapKit.h&gt;
  
  @interface MKMapView (Extra)
  
  
  #pragma mark -
  #pragma mark Zoom methods
  - (void)setCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate
                    zoomLevel:(NSUInteger)zoomLevel
                     animated:(BOOL)animated;
  - (int) zoomLevel;
  - (float) zoomLevelFine;
  
  #pragma mark -
  #pragma mark Region methods
  - (void)setRegionTopLeft:(CLLocationCoordinate2D)topLeft
               bottomRight:(CLLocationCoordinate2D)bottomRight
                  animated:(BOOL)animated;
  - (CLLocationCoordinate2D)centerPointFromRegionTopLeft:(CLLocationCoordinate2D)topLeft
                                             bottomRight:(CLLocationCoordinate2D)bottomRight;
  - (MKCoordinateSpan)spanFromRegionTopLeft:(CLLocationCoordinate2D)topLeft
                                bottomRight:(CLLocationCoordinate2D)bottomRight;
  @end
  </code></pre>
  
  <p>MKMapView+Extra.m</p>
  
  <pre><code>#import "MKMapView+Extra.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 zoom methods
  
  - (void)setCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate
                    zoomLevel:(NSUInteger)zoomLevel
                     animated:(BOOL)animated
  {
      NSLog(@"setCenterCoordinate: zoomLevel:%d animated:", zoomLevel);
      // clamp large numbers to 28
      zoomLevel = MIN(zoomLevel-1, 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];
  }
  
  - (int) zoomLevel
  {
      return 21 - round(log2(self.region.span.longitudeDelta * MERCATOR_RADIUS * M_PI / (180.0 * self.bounds.size.width)));
  }
  
  - (float) zoomLevelFine
  {
      return 21.0 - log2(self.region.span.longitudeDelta * MERCATOR_RADIUS * M_PI / (180.0 * self.bounds.size.width));
  }
  
  #pragma mark -
  #pragma mark Public region methods
  - (void)setRegionTopLeft:(CLLocationCoordinate2D)topLeft
               bottomRight:(CLLocationCoordinate2D)bottomRight
                  animated:(BOOL)animated
  {
      CLLocationCoordinate2D center = [self centerPointFromRegionTopLeft:topLeft bottomRight:bottomRight];
      MKCoordinateSpan span = [self spanFromRegionTopLeft:topLeft bottomRight:bottomRight];
      MKCoordinateRegion region = MKCoordinateRegionMake(center, span);
  
      // set the region like normal
      [self setRegion:region animated:animated];
  }
  
  - (CLLocationCoordinate2D)centerPointFromRegionTopLeft:(CLLocationCoordinate2D)topLeft
                                             bottomRight:(CLLocationCoordinate2D)bottomRight
  {
      CLLocationCoordinate2D centerPoint;
  
      centerPoint.latitude = ((topLeft.latitude + bottomRight.latitude) / 2);
      centerPoint.longitude = ((topLeft.longitude + bottomRight.longitude) / 2);
  
      return centerPoint;
  }
  
  - (MKCoordinateSpan)spanFromRegionTopLeft:(CLLocationCoordinate2D)topLeft
                                bottomRight:(CLLocationCoordinate2D)bottomRight
  {
      MKCoordinateSpan span;
  
      span.latitudeDelta = fabs(bottomRight.latitude - topLeft.latitude);
      span.longitudeDelta = fabs(bottomRight.longitude - topLeft.longitude);
  
      return span;
  }
  
  @end
  </code></pre>