MKMapView Zoom and Region - iphone

I'm familiar with using Google Maps Javascript API. Recently I started using MapKit framework for an iphone project, but I'm having a hard time to figure out zooming and setting a region on map.
In Google Maps API I used to use integer zoom levels like 8, 9, 10 along with straightforward function setZoom(). The only equivalent method I can see in the MapKit framework is setRegion:animated. As I understand, I need to set a region's span's latitude and longitude "delta" values to specify zoom level. But I really don't have an idea what these values represent(I read the documentation).
When I use a MKMapView delegate and trace the span values in regionDidChange delegate method results don't seem to correlate each other. It's ok when I zoom out and see the span delta values are increasing as specified in documentation. But suddenly I drag the map without zooming and delta values become 0.0.
Can somebody please explain what is the reference point to these span and delta? Or is there any algorithm to convert an integer zoom level(like 9) to these delta values?
As a bonus question is there any way to specify a minimum-maximum zoom level on a MKMapView :)
Thanks

First of all, MKMapView does not use/have a predefined set of zoom levels like Google Maps does.
Instead, the visible area of a MKMapView is described using MKCoordinateRegion, which consists of two values:
center (the center point of the region), and
span (the size of the visible area around center).
The center point should be obvious (it's the center point of the region.)
However, span (which is a MKCoordinateSpan) consists of:
latitudeDelta (the vertical distance represented by the region), and
longitudeDelta (the horizontal distance represented by the region).
A brief example. Here's a toy MKCoordinateRegion:
center:
latitude: 0
longitude: 0
span:
latitudeDelta: 8
longitudeDelta: 6
The region could be described using its min and max coordinates as follows:
min coordinate (lower left-hand point):
latitude: -4
longitude: -3
max coordinate (upper right-hand point):
latitude: 4
longitude: 3
So, you can specify zoom levels around a center point by using an appropriately sized MKCoordinateSpan. As an approximation of Google's numeric zoom levels, you could reverse engineer the span sizes that Google uses for a given zoom level and create a span, accordingly. (Google describes their view regions in the same way that MKMapView does, as a center + span, so you can pull these values out of Google Maps.)
As for restricting the region, you may play w/ this delegate method:
mapView:regionWillChangeAnimated
e.g. by resizing the region back into your allowed zoom levels. (Kind of like how table views will let you scroll past the edge, but will then rubber band back into place.) However, your mileage may vary, since I haven't used it for this purpose.
btw, there are definite fixes/improvements in OS 3.1 to aspects of MapKit that were giving me trouble in 3.0.

If you prefer using explicit zoom levels instead of defining an MKCoordinateSpan, I wrote a category that adds support for specifying the zoom level of an MKMapView. The code can be found here.

The span is in degrees of latitude and longitude. There is a method for constructing MKCoordinateRegion structs that takes distance, instead. It may be that you are using MKCoordinateRegionMakeWithDistance to specify the span, and then when you check it in regionDidChange, you're seeing at the lat/long span, which is how it is stored in an MKCoordinateRegion struct.
As far as I know, the integer zoom levels are not available or useful at all when working with MKMapKit. I personally prefer using the span figures, its more flexible.
You cannot specify max and min zoom, and I don't know of a way to hack it in. MKMapKit is actually pretty weak right now, I'm pretty disappointed by the lack of features.

A quick comparison of zoom levels for a location using maps.google.com by inspecting the link querystring shows that the dx and dy span values increase by a factor of 2:
(0.005334, 0.011834) starting span
(0.010668, 0.023668) dx: x2, dy: x2
(0.021335, 0.047337) dx: x2, dy: x2
(0.042671, 0.094671) dx: x2, dy: x2
...

Brant's category on MKMapView works well. However, it appears that it has not been updated to support newer devices with retina screens when calculating mapSizeInPixels.
It can be fixed by replacing this line:
CGSize mapSizeInPixels = mapView.bounds.size;
With this line:
CGSize mapSizeInPixels = CGSizeMake(mapView.bounds.size.width * [UIScreen mainScreen].scale, mapView.bounds.size.height * [UIScreen mainScreen].scale);

Related

Relative coordinate calculation in custom map

I'm currently working on a mapping app for iPhone. I've created some custom maps of various sizes, but I've run into an issue:
I would like to implement the ability for users' locations to be checked automatically, but since Im not using a MapView this is much more dificult. (see below)
given the different coordinate systems, I would like to receive a geolocation (green dot) and translate it into a pixel location on a custom map.
Ive got the geolocations for the 4 corners, but the rect is askew. Ive calculated the angle of rotation, but Im just generally confused.
note: the size of the maps arent big enough for the spherical nature of the earth to come into calculation.
Any help is appreciated!
To convert a geolocation to point you need to first understand the mapping. assuming you are using Mercator.
x = R*long
y = R*(1+sin(lat))/cos(lat)
where lat and long are in radians.R is radius of earth. the scale of the image would be from 0 to R*PI
so to get it within view.frame.size you may have to divide by a scale factor.
for difference between points.
x2-x1 = R* (long2-long1)
y2-y1 = R* ( (1+sin(lat2))/cos(lat2) - (1+sin(lat1))/cos(lat1) )

Why points above 85N or below -85S are not shown on the iPhone map?

I need to represent on the map (iPhone) some points such as (88, 60) or (90, 55), but the custom annotations representing these points get deallocated. I also noticed that these points are not actually displayed on the google map, they are somehow above the visible map. This happens for any point that is above 85 deg. N latitude or -85 deg. S latitude.
I know it's a really old post, but just to answer your question.
Most (allmost all) commercial maps are displayed in the mercator projection. This is what you see in mapKit or on google maps. This means that the latitude and longitude lines run horizontal and vertical.
If you would change this to for instance polar project (world from the top) it would become way to difficult to calculate the postion of objects because the lat and long lines converge rapidly...
So it's just for ease of use....

Longitude, Latitude to XY coordinate conversion

I want to now how to convert longitude, latitude to its equivalent xy coordinate components in iPhone programming. (I am using only CoreLocation programming, and want to show a point on iPhone screen without any map).
thanks
Well the exact conversion depends on exactly which part of the Earth you want to show, and the stretching along longitude varies according to latitude, at least in Mercator.
That being said, even if you don't want to display an actual MapKit map, it would probably be easiest to create an MKMapView and keep it to one side. If you set the area you want to display appropriately on that (by setting the region property), you can use convertCoordinate:toPointToView: to map from longitude and latitude to a 2d screen location.
Note that MKMapView adjusts the region you set so as to make sense for the viewport its been given (eg, if you gave it a region that was a short fat rectangle, but the view it had was a tall thin rectangle, it'd pick the smallest region that covers the entire short fat rectangle but is the shape of a tall thin rectangle), so don't get confused if you specify a region with the top left being a particular geolocation, but then that geolocation isn't at the exact top left of the view.

MKMapView setRegion "snaps" to predefined zoom levels?

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

strange behavior of iphone MKMapKit

Whenever I set the region property of the MKMapKit it doubles the span.
For example:
MKCoordinateRegion currentRegion = map.region; // assume the current center of region is around New-York and the span is 2 by 2
map.region = map.region; // assign the region to itself
MKCoordinateRegion newRegion = map.region;
// the span of newRegion is different (almost twice than currentRegion)
what's going on here?
Are you setting the region.center for the MapView too ?
Look at the region property API description of MKMapView:
"Changing only the center coordinate of the region can still cause the span to change implicitly. This is due to the fact that the distances represented by a span change at different latitudes and longitudes and the map view may need to adjust the span to account for the new location. If you want to change the center coordinate without changing the zoom level, use the centerCoordinate instead."
My reading is that this means that your region span settings can be overridden by calculated values based on the center coordinate when you change region.center.
I also get it both on the simulator and the device... iphone SDK 3.0, xcode 3.2.
also simulator for SDK 3.1 does it... don't have a device with 3.1 on it right now to try it.
How could they not find this bug?? What could be more basic than loading and saving your region??
Plenty of other bugs in mapkit too! :-/ it's pretty weak!
BTW if you call regionThatFits you get almost the same region. When you try to set the region, it actually sets something close to (but not always exactly) [mapview regionThatFits:yourregion]