21

Can anyone confirm that setRegion "snaps" to predefined zoom levels and whether or not this behavior is as designed (although undocumented) or a known bug? Specifically, it appears that setRegion snaps to the same zoom levels that correspond to the zoom levels used when the user double-taps the map.

I'm trying to restore a previously saved region but this behavior makes it impossible if the saved region was set via a pinch zoom and not a double-tap zoom.

A big clue to me that things are broken on the mapkit side is what occurs if I call regionThatFits on the map's current region. It should return the same region (since it obviously fits the map's frame) but it returns the region that corresponds to the next higher predefined zoom level instead.

setVisibleMapRect behaves similarly.

Any further insight or information would be appreciated.

I found these related posts but neither included a solution or definitive confirmation that this is in fact a mapkit bug:

MKMapView setRegion: odd behavior?

MKMapView show incorrectly saved region

EDIT:

Here is an example that demonstrates the problem. All values are valid for my map view's aspect ratio:

MKCoordinateRegion initialRegion;
initialRegion.center.latitude = 47.700200f;
initialRegion.center.longitude = -122.367109f;
initialRegion.span.latitudeDelta = 0.065189f;
initialRegion.span.longitudeDelta = 0.067318f;
[map setRegion:initialRegion animated:NO];
NSLog(@"DEBUG initialRegion:  %f  %f  %f  %f", initialRegion.center.latitude, initialRegion.center.longitude, initialRegion.span.latitudeDelta, initialRegion.span.longitudeDelta);
NSLog(@"DEBUG map.region:  %f  %f  %f  %f", map.region.center.latitude, map.region.center.longitude, map.region.span.latitudeDelta, map.region.span.longitudeDelta);

OUTPUT:

DEBUG initialRegion:  47.700199  -122.367111  0.065189  0.067318
DEBUG map.region:  47.700289  -122.367096  0.106287  0.109863

Note the discrepancy in the latitude/longitude delta values. The map's values are almost double what I requested. The larger values correspond to one of the zoom levels used when the user double-taps the map.

Community
  • 1
  • 1
charshep
  • 416
  • 4
  • 14

7 Answers7

7

Yes, it snaps to discrete levels. I've done quite a bit of experimentation, and it seems to like multiples of 2.68220906e-6 degrees of longitude per pixel.

So if your map fills the whole width of the screen, the first level spans .0008583 degrees, then the next level up you can get is twice that, .001717, and then the next one is twice that, .003433, and so on. I'm not sure why they chose to normalize by longitude, it means that fixes zoom levels vary depending on what part of the world you are looking at.

I've also spent a lot of time trying to understand the significance of that number .68220906e-6 degrees. It comes out to about 30cm at the equator, which kind of makes sense since the high resolution photos used by Google Maps have a 30cm resolution, but I would have expected them to use latitude instead of longitude to establish the zoom levels. That way, at maximum zoom, you always the native resolution of the satellite images, but who knows, they probably have some smart-people reason for making it work like that.

In my application I need to display a certain range of latitude. I'm gonna work on some code to try to zoom the map as close as possible to that. If anyone is interested, contact me.

pseudopeach
  • 1,475
  • 14
  • 29
1

I found a solution.

If the received snapped zoom level, is, lets say a factor of 1.2 bigger than the desired one: use this algorithm to correct:
Asumption: you want to set the map view to exactly show "longitudinalMeters" from left to right
1) Calculate the correction scale:
Calculate the relation between longitudinal span you received, to that one you have got.

    MKCoordinateRegion region = MKCoordinateRegionMakeWithDistance(center, 0, longitudinalMeters);

    MKCoordinateRegion regionFits = [mapView regionThatFits: region];
    double correctionFactor = regionFits.span.longitudeDelta / region.span.longitudeDelta;

2) Create the transformation and apply it to the map

   CGAffineTransform mapTransform = CGAffineTransformMakeScale(correctionScale, correctionScale);       
   CGAffineTransform pinTransform = CGAffineTransformInvert(mapTransform);
   [mapView setTransform:mapTransform];

3) Apply the inverse transformation to the Map pins, to keep them at original size

 [mapView setTransform:mapTransform];
 for (id<MKAnnotation> annotation in self.mapView.annotations)
 {
     [[self.mapView viewForAnnotation:annotation] setTransform:pinTransform];
 }
AlexWien
  • 28,470
  • 6
  • 53
  • 83
  • The problem with this is that the entire map view is scaled so (a) the user's current location blue dot get's scaled, and (b) the Google logo can disappears and according to the license agreement this needs to be visible. I'm also desperately looking for a way to programmatically set the zoom level to avoid this "snapping". – pho0 Feb 05 '12 at 03:03
  • Yes, I have the same Problem with th eblue dot, it sometimes scales, most times not. You might draw your own user location. Further the Google logo can be placed manually. there are posts here regarding rotating of the map view, where the google logo must added explicitly. Further I think that my solution is the only one that works, the map view has simply not that feature to set the zoom scale exactly. – AlexWien Feb 09 '12 at 12:18
  • I've worked very very much on this problem. Before iOS 6, the solution given works. Beginning with iOS 6, the annotations' affine transform are reset once in a while, seemingly randomly. I ended up having to KVO observe the affine transform of each and every annotations added to the map, then reset the affine transform to the wanted one each time it is modified. – Frizlab Apr 05 '13 at 08:21
  • Also, I've asked Apple directly, and there is *no way* to set the wanted region exactly with the current API Apple gives us (iOS 6). – Frizlab Apr 05 '13 at 08:28
  • @Fritzlab: Thanks for the KVO info, i thought the same, to intercep the set Transform. But for iOS6 it seems that evrything works fine out of the box, I can set the zoom level exactly and i dont need the transform approach decribed above. The correctionFactor above is nearly 1: 0,99996 e.g, So for me since ios6 it seems to be solved. (However not yet tested enough) (ios6 uses vector maps, so Apple is not bound the googles pre rendered jps) – AlexWien Apr 05 '13 at 12:40
  • @Fritlab: How did you solve in ios5 the blue dot and other scaling problems? – AlexWien Apr 05 '13 at 12:41
  • Two comments above i quoted that i have solved it on ios6, but this is valid only for iPad. I cannot get it to work on iPhone4, only with my trick (transform) – AlexWien Aug 10 '13 at 21:41
  • @Fritzlab i created an question with bounty worth 250: http://stackoverflow.com/questions/18166176/mkmapview-setregion-under-ios6-snaps-to-grid-like-ios5-no-precise-setregion-pos – AlexWien Sep 16 '13 at 14:16
  • @Fritzlab on ios6 for me, it works out of the box only on iPad, does it work for you on iphone, too? – AlexWien Sep 16 '13 at 14:18
0

MKCoordinateRegion region;

region.center.latitude = latitude;
region.center.longitude = longitude;
region.span.latitudeDelta = 5.0;
region.span.longitudeDelta = 5.0;
[mapView setRegion:region animated:YES];
Julian F. Weinert
  • 7,474
  • 7
  • 59
  • 107
Dhaval Parmar
  • 293
  • 3
  • 5
0

This is an old question, but I recently investigated Google maps in detail, and can share some insight. I don't know whether this is also valid for the current Apple maps.

The reason that the resolution snaps to predefined zoomlevels is because the original maps fetched from Google's servers are drawn with those zoomlevels. The size of the features on those maps are drawn with a certain resolution in mind. For example, the width (in pixels) of a road on those maps is always the same. On higher resolution maps, more secundary roads are drawn, but their width is always the same. The resolution snaps to predefined levels to make sure those features are always depicted with the same size. That is, it is not a bug but a feature.

Those predefined resolutions vary with latitude because of the Mercator projection of the maps. Mercator projection is easy to work with because latitude lines are depicted straight and horizontal and longitude lines are straight and vertical. But with Mercator projection the top of the map has a slightly higher resolution than the bottom (on the Northern hemisphere). That has consequences for fitting maps together at the northern and sourthern edges.

That is, when you start on the equator and drive north, then the resolution of the Mercator maps you drive over will gradually increase. The longitude lines remain vertical, and therefore the longitude spans remains the same. But the resolution increases, and therefore the latitude span decreases. Still, on all those maps the roads have the same width in pixels, and texts are depicted in the same font size, etc.

Google uses a Mercator projection where the equator circumference is 256 pixels at zoomlevel 0. Each next zoomlevel doubles that amount. That is, at zoomlevel 1, the equator is 512 pixels long, at zoomlevel 2, the equator is 1024 pixels long, etc. The model for the earth they use is a FAI globe with a radius of exactly 6371 km, or circumference of 40030 km. Therefore, resolution for zoomLevel 0 at the equator is 156.37 km/pixel, at zoomlevel 1 it is 78.19 km/pixel, etc. Those resolutions then vary with the cosinus of the latitude anywhere else on the earth.

fishinear
  • 6,101
  • 3
  • 36
  • 84
  • That would make sense if the fixed zoom levels were pegged to latitude, since each degree of latitude is about 68 miles, anywhere on earth. It wasn't like that in MKMapkit though, it was pegged to longitude, which makes no sense and was almost certainly a bug. Having your map snap to fixed longitude spans makes it so you can never have a consistent zoom level. – pseudopeach Jan 08 '13 at 15:40
  • @pseudopeach I am not sure what you mean with "zoom levels pegged to latitude". The zoomlevels are what they are. The resolution that belongs to a fixed zoomlevel varies with latitude, because the resolution of the Mercator projection varies with latitude. Each degree of latitude is constant on the globe (about 1 nautical mile per minute), but it is not on a Mercator projection. On a Mercator projection, a degree of longitude is constant. – fishinear Jan 08 '13 at 19:47
  • What I mean is that at the equator, the max resolution of the map would be 30 cm/px and further away from the equator it was 50 cm/px. The max zoom level (and all other levels) would always span the same longitude range, meaning it was effectively "zooming out" as you strayed from the equator. Conversely, if they had tied the discrete zoom levels to specific latitude spans, the resolution in terms of cm per pixel would have been constant everywhere. (I'm sure that's what they meant to do, but someone made a little typo in the code) – pseudopeach Jan 26 '13 at 03:26
  • @pseudopeach The discrete zoom levels are tied to the native resolution of the maps. As I tried to explain, in a Mercator projection, the north end of a map has higher resolution (say: 30 px/km) than the south end (say: 25 px/km). The map north of that therefore must have 30 px/km at the south end, otherwise you will see discontinuities at the edge where the two maps meet. That second map will then necessarily have 35 px/km at its north edge, etcetera. It is definitely NOT a type in the code. Read up on Mercator projection, and you will understand. – fishinear Jan 26 '13 at 11:04
0

The weird behavior seems to be due to the fact that while one requests a particular region or view size, the actual API call to google is invoked with a center point and a zoom level. E.G.:

map.setCenter(new google.maps.LatLng(234.3453, 454.2345), 42);

Now it would be possible for Apple to request the appropriate zoom level and then adjust the sizing of the view to accommodate the actual region request, but it seems they fail to do so. I am drawing bus routes on a map, and one of my routes barely triggers a larger zoom level and thus scales too small (under-zooms) and looks ugly and smashed.

@pseudopeach, Please update me on the progress of your attempts to work around this issue. If one could detect the boundaries of a zoom level, the region request could then be deliberately underscaled to avoid the under-zoom. Since you are onto this I would be interested in seeing your code before I have to make an attempt at it myself.

There is an interesting category that the author of the blog Backspace Prolog has written to enable the direct manipulation of the Google Maps API by emulating their setCenter(centerPoint,ZoomLevel) call signature. You can find it here. I haven't spent the time yet, but the math can probably be reverse engineered to yield a means of calculating the zoom level for a given Region or MapRect. Depending on how far it is within the zoom level's range - i.e. how far it is over the threshold that triggers the lower zoom level - it could decide whether to go to the lower level or keep to higher one by under-requesting.

This is clearly a behavioral bug that needs to be fixed so that MKMapView can be used in a more refined manner.

Curtis
  • 101,612
  • 66
  • 270
  • 352
wjodon
  • 9
  • 2
-1

If you set up the MapView in InterfaceBuilder, make sure you don't do this:

_mapView = [[MKMapView alloc] init];

As soon as I removed this init line, my map view suddenly began responding properly to all the updates I sent it. I suspect that what happens is that if you do the alloc init, it's actually creating another view that's not being shown anywhere. The one you see on the screen is the one initialized by your nib. But if you alloc init a new one, then that's something somewhere else and it's not going to do anything.

CommaToast
  • 11,370
  • 7
  • 54
  • 69
-1

I restore the region with no problem and with no variance as you describe. It is really impossible to tell what is specifically wrong in your case without some code to look at but here's what works for me:

Save both the center and span values somewhere. When you are restoring them specifically set both the center and span.

Restoring should look like this:

MKCoordinateRegion initialRegion;
initialRegion.center.latitude = Value you've stored
initialRegion.center.longitude = Value you've stored 
initialRegion.span.latitudeDelta = Value you've stored
initialRegion.span.longitudeDelta = Value you've stored
[self.mapView setRegion:initialRegion animated:NO];

Also remember that this method is available in 4.0: `mapRectThatFits:edgePadding: MapRectThatFits helpfully adds a reasonable border to ensure that say a map annotation on the edge is not obscured and the the rect that you're attempting to display is fully visible. If you want to control the border use the call that gives you access to set edgePadding as well.

Nick
  • 8,483
  • 10
  • 46
  • 65
  • Thanks. I've updated my original post with a specific example borrowed from your sample that demonstrates the problem. – charshep Aug 31 '10 at 22:57
  • Thanks for the code details. Couple of followup questions. Where did those floats come from? Are you reading them out of the property from the map when you are pinch/spread zooming? What are the dimensions of the map view? This is interesting because I read the values out when my view is unloading and save them to UserSettings. I have no problems restoring my view accurately. – Nick Sep 01 '10 at 01:15
  • Yes, I pulled those from my map's region property after a pinch zoom. Dimensions are 460 x 320, running in the iPhone 4 simulator. – charshep Sep 01 '10 at 01:25
  • 1
    I'm really sorry. I feel like an idiot because I'm seeing this behavior too it seems to be varied depending on zoom levels and positioning I didn't test adequately with pinch zooming. I wonder if working through the new mapRectThatFits: edgePadding: would work. I'll experiment a bit and circle back. Apologies again for my mix up. – Nick Sep 01 '10 at 02:14
  • 2
    Don't apologize! It's surprisingly easy to miss for how obvious it sounds. It took me a while to notice as well. I'm just relieved you're able to confirm the problem. Thanks again for looking into it. One side note. You'll notice it's not an issue if you restore a region that was set by double-tapping rather than pinch zooming; further evidence that setRegion is "snapping" the region its given. This is why it took me a while to notice. It's much easier to double-tap zoom in the simulator than to pinch zoom so I was always tap-zooming. – charshep Sep 01 '10 at 02:25
  • Just curious if you've tried `setVisibleMapRect:edgePadding:animated:`, because for me the edge insets appear to be getting ignored. Does it honor the edge insets for you? – Daniel Dickison Sep 07 '10 at 14:40