5
votes

EDIT: I believe my issue is that this code works for integer zoom levels, but I would like it to work for float zoom levels.

I have an iOS app in which the user can switch between a RouteMe-based map and a MapKit-based map.

When they switch sources, I would like to be able to show the exact same area in one as in the other. However, I can't figure out how to make them match because RouteMe and MapKit use different data structures to describe the map bounds.

Here is some code that gets it to be somewhat close, but it's not exact. This code comes from: http://troybrant.net/blog/2010/01/set-the-zoom-level-of-an-mkmapview/

I'm not sure whether this code should be fixed, or possibly I am overlooking a much easier solution. The code executes starting with the last method listed:

#define MERCATOR_OFFSET 268435456
#define MERCATOR_RADIUS 85445659.44705395

#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;
}


- (MKCoordinateSpan)coordinateSpanWithMapView:(MKMapView *)mapView
                             centerCoordinate:(CLLocationCoordinate2D)centerCoordinate
                                 andZoomLevel:(NSInteger)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;
}


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

  // 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];
}
1
One thing I've noticed about MKMapView is that if you set the region and then read it back immediately, you get back different values. In our app, I set the value which triggers a delegate callback and then I read the new value and use it to determine where to put our overlays. Not sure if this helps. :-(EricS

1 Answers

4
votes

Unfortunately this is a limitation of the Google Maps API, which only accepts integer values when setting the map's zoom level: Apple's MapKit code is calling the underlying Google Maps APIs when you set a MKMapView's displayed area, and the result – no matter which MapKit method you use to set the area – is a map that's zoomed out to the nearest integer zoom level.

Troy Brant's code takes you full circle, and puts a layer above the MapKit APIs that allows you to set the zoom level directly… but ultimately you don't have precise control over the area displayed by an MKMapView, unless the zoom level of your desired map happens to be an integer.

Several variations on this question have appeared on Stack Overflow (e.g., MKMapView setRegion "snaps" to predefined zoom levels? and MKMapView show incorrectly saved region), but so far no one has come up with a programmatic way to make a map with a non-integer zoom level, and I suspect it'd take cooperation between Google and Apple to ever make it happen.