Convert GMSVisibleRegion to CLRegion or MKCoordinateRegion - ios

I am using the GoogleMaps SDK and currently I am trying to convert a GMSVisibleRegion to a CLRegion.
GMSVisibleRegion is defined as:
typedef struct {
CLLocationCoordinate2D nearLeft;
CLLocationCoordinate2D nearRight;
CLLocationCoordinate2D farLeft;
CLLocationCoordinate2D farRight;
} GMSVisibleRegion;
What is the fastest way to do so?
Unfortunately it is difficult to understand what the developer meant with the naming "near" and "far". I think this comment can also be useful:
/**
* Returns the region (four location coordinates) that is visible according to
* the projection.
*
* The visible region can be non-rectangular. The result is undefined if the
* projection includes points that do not map to anywhere on the map (e.g.,
* camera sees outer space).
*/
- (GMSVisibleRegion)visibleRegion;
Thanks a lot!
EDIT:
Ok my first step was to create a MKCoordinateRegion of a GMSVisibleRegion.
I propose the following code to transform a a GMSVisibleRegion to a MKCoordinateRegion. Any objections.
+ (MKCoordinateRegion)regionForCenter:(CLLocationCoordinate2D)center andGMSVisibleRegion:(GMSVisibleRegion)visibleRegion
{
CLLocationDegrees latitudeDelta = visibleRegion.farLeft.latitude - visibleRegion.nearLeft.latitude;
CLLocationDegrees longitudeDelta = visibleRegion.farRight.longitude - visibleRegion.farLeft.longitude;
MKCoordinateSpan span = MKCoordinateSpanMake(latitudeDelta, longitudeDelta);
return MKCoordinateRegionMake(center, span);
}

My guess is that 'near' is for the corners of the view at the bottom of the screen, and 'far' is for the corners at the top of the screen. This is because if you've tilted the view, then the bottom corners are nearest to the camera, and the top corners are furthest from the camera.
One way to turn this into a CLRegion might be to use the camera's target as the centre, and then calculate the radius from the maximum distance to the four corners. This might not be the tightest fitting circle over the region, but since a circle can't fit the quadrilateral of the view anyway, it may be close enough.
Here's a helper function to calculate the distance in metres between two CLLocationCoordinate values:
double getDistanceMetresBetweenLocationCoordinates(
CLLocationCoordinate2D coord1,
CLLocationCoordinate2D coord2)
{
CLLocation* location1 =
[[CLLocation alloc]
initWithLatitude: coord1.latitude
longitude: coord1.longitude];
CLLocation* location2 =
[[CLLocation alloc]
initWithLatitude: coord2.latitude
longitude: coord2.longitude];
return [location1 distanceFromLocation: location2];
}
Then the CLRegion can be calculated like this:
GMSMapView* mapView = ...;
...
CLLocationCoordinate2D centre = mapView.camera.target;
GMSVisibleRegion* visibleRegion = mapView.projection.visibleRegion;
double nearLeftDistanceMetres =
getDistanceMetresBetweenLocationCoordinates(centre, visibleRegion.nearLeft);
double nearRightDistanceMetres =
getDistanceMetresBetweenLocationCoordinates(centre, visibleRegion.nearRight);
double farLeftDistanceMetres =
getDistanceMetresBetweenLocationCoordinates(centre, visibleRegion.farLeft);
double farRightDistanceMetres =
getDistanceMetresBetweenLocationCoordinates(centre, visibleRegion.farRight);
double radiusMetres =
MAX(nearLeftDistanceMetres,
MAX(nearRightDistanceMetres,
MAX(farLeftDistanceMetres, farRightDistanceMetres)));
CLRegion region = [[CLRegion alloc]
initCircularRegionWithCenter: centre radius: radius identifier: #"id"];
UPDATE:
Regarding your update for MKCoordinateRegion, your example code may not work. If the map has been rotated 90 degrees, then farLeft and nearLeft will have the same latitude, and farRight and farLeft will have the same longitude, and so your latitude and longitude deltas would be zero.
You would need to loop over all four of the farLeft, farRight, nearLeft, nearRight, calculate the min and max of the latitude and longitude of each, and then calculate the delta from that.
The Google Maps SDK for iOS includes a helper class which already does some of this for you - GMSCoordinateBounds. It can be initialized with a GMSVisibleRegion:
GMSMapView* mapView = ...;
....
GMSVisibleRegion visibleRegion = mapView.projection.visibleRegion;
GMSCoordinateBounds bounds =
[[GMSCoordinateBounds alloc] initWithRegion: visibleRegion];
The GMSCoordinateBounds then has northEast and southWest properties which define the bounds. So you could calculate the deltas as follows:
CLLocationDegrees latitudeDelta =
bounds.northEast.latitude - bounds.southWest.latitude;
CLLocationDegrees longitudeDelta =
bounds.northEast.longitude - bounds.southWest.longitude;
You could also calculate the centre from the bounds, and therefore the MKCoordinateRegion:
CLLocationCoordinate2D centre = CLLocationCoordinate2DMake(
(bounds.southWest.latitude + bounds.northEast.latitude) / 2,
(bounds.southWest.longitude + bounds.northEast.longitude) / 2);
MKCoordinateSpan span = MKCoordinateSpanMake(latitudeDelta, longitudeDelta);
return MKCoordinateRegionMake(centre, span);

Addendum for purists
If you want to be absolutely rigorous there's a correction you need to make around the international dateline. It would be a waste of effort in most apps but this problem has been causing me major grief as of late so I thought to throw it into the community hat
Building on Druce's update (afraid I can't make comments)...
GMSMapView* mapView = ...;
....
GMSVisibleRegion visibleRegion = mapView.projection.visibleRegion;
GMSCoordinateBounds bounds =
[[GMSCoordinateBounds alloc] initWithRegion: visibleRegion];
Latitude doesn't need anything doing to it
CLLocationDegrees latitudeDelta =
bounds.northEast.latitude - bounds.southWest.latitude;
The deal runs that a region that spans the international dateline might have a southWest corner in Japan (+140 longitude) and its northEast corner in Alaska (-150 longitude). Adding up and dividing by two gives a point around the wrong side of the globe.
The special case where northEast.longitude is less than southWest.longitude needs handling
CLLocationCoordinate2D centre;
CLLocationDegrees longitudeDelta;
if(bounds.northEast.longitude >= bounds.southWest.longitude) {
//Standard case
centre = CLLocationCoordinate2DMake(
(bounds.southWest.latitude + bounds.northEast.latitude) / 2,
(bounds.southWest.longitude + bounds.northEast.longitude) / 2);
longitudeDelta = bounds.northEast.longitude - bounds.southWest.longitude;
} else {
//Region spans the international dateline
centre = CLLocationCoordinate2DMake(
(bounds.southWest.latitude + bounds.northEast.latitude) / 2,
(bounds.southWest.longitude + bounds.northEast.longitude + 360) / 2);
longitudeDelta = bounds.northEast.longitude + 360
- bounds.southWest.longitude;
}
MKCoordinateSpan span = MKCoordinateSpanMake(latitudeDelta, longitudeDelta);
return MKCoordinateRegionMake(centre, span);

For anyone looking for boilerplate code based on all the answers and corrections provided so far, here's region implemented as a category on GMSMapView:
//
// GMSMapViewExtensions.h
//
#import <Foundation/Foundation.h>
#import <MapKit/MapKit.h>
#import <GoogleMaps/GoogleMaps.h>
#interface GMSMapView (GMSMapViewExtensions)
#end
and
//
// GMSMapViewExtensions.m
//
#import "GMSMapViewExtensions.h"
#implementation GMSMapView (GMSMapViewExtensions)
- (MKCoordinateRegion) region {
GMSVisibleRegion visibleRegion = self.projection.visibleRegion;
GMSCoordinateBounds * bounds = [[GMSCoordinateBounds alloc] initWithRegion: visibleRegion];
CLLocationDegrees latitudeDelta = bounds.northEast.latitude - bounds.southWest.latitude;
CLLocationCoordinate2D centre;
CLLocationDegrees longitudeDelta;
if (bounds.northEast.longitude >= bounds.southWest.longitude) {
// Standard case
centre = CLLocationCoordinate2DMake(
(bounds.southWest.latitude + bounds.northEast.latitude) / 2,
(bounds.southWest.longitude + bounds.northEast.longitude) / 2);
longitudeDelta = bounds.northEast.longitude - bounds.southWest.longitude;
} else {
// Region spans the international dateline
centre = CLLocationCoordinate2DMake(
(bounds.southWest.latitude + bounds.northEast.latitude) / 2,
(bounds.southWest.longitude + bounds.northEast.longitude + 360) / 2);
longitudeDelta = bounds.northEast.longitude + 360 - bounds.southWest.longitude;
}
MKCoordinateSpan span = MKCoordinateSpanMake(latitudeDelta, longitudeDelta);
return MKCoordinateRegionMake(centre, span);
}
- (MKMapRect)visibleMapRect {
MKCoordinateRegion region = [self region];
MKMapPoint a = MKMapPointForCoordinate(CLLocationCoordinate2DMake(
region.center.latitude + region.span.latitudeDelta / 2,
region.center.longitude - region.span.longitudeDelta / 2));
MKMapPoint b = MKMapPointForCoordinate(CLLocationCoordinate2DMake(
region.center.latitude - region.span.latitudeDelta / 2,
region.center.longitude + region.span.longitudeDelta / 2));
return MKMapRectMake(MIN(a.x, b.x), MIN(a.y, b.y), ABS(a.x - b.x), ABS(a.y - b.y));
}
#end
Usage example:
GMSMapView * mapView = .... // init code
MKCoordinateRegion mapRegion = mapView.region;

Based on answer of #Saxon Druce, this is a quick extension on setting and getting region using MKCoordinateRegion on GMSMapView
extension GMSMapView {
var region : MKCoordinateRegion {
get {
let position = self.camera
let visibleRegion = self.projection.visibleRegion()
let bounds = GMSCoordinateBounds(region: visibleRegion)
let latitudeDelta = bounds.northEast.latitude - bounds.southWest.latitude
let longitudeDelta = bounds.northEast.longitude - bounds.southWest.longitude
let center = CLLocationCoordinate2DMake(
(bounds.southWest.latitude + bounds.northEast.latitude) / 2,
(bounds.southWest.longitude + bounds.northEast.longitude) / 2)
let span = MKCoordinateSpanMake(latitudeDelta, longitudeDelta)
return MKCoordinateRegionMake(center, span)
}
set {
let northEast = CLLocationCoordinate2DMake(newValue.center.latitude - newValue.span.latitudeDelta/2, newValue.center.longitude - newValue.span.longitudeDelta/2)
let southWest = CLLocationCoordinate2DMake(newValue.center.latitude + newValue.span.latitudeDelta/2, newValue.center.longitude + newValue.span.longitudeDelta/2)
let bounds = GMSCoordinateBounds(coordinate: northEast, coordinate: southWest)
let update = GMSCameraUpdate.fitBounds(bounds, withPadding: 0)
self.moveCamera(update)
}
}
}

Related

Trying to get the span size in meters for an iOS MKCoordinateSpan

When I need to make an MKCoordinateRegion, I do the following:
var region = MKCoordinateRegion
.FromDistance(coordinate, RegionSizeInMeters, RegionSizeInMeters);
very simple - works perfectly.
Now I wish to store the value of the current region span. When i look at the region.Span value, it’s an MKCoordinateSpan which has two properties:
public double LatitudeDelta;
public double LongitudeDelta;
How can I convert the LatitudeDelta value into a latitudinalMeters please? (So then I can recreate my region (later on) using the above method...
As I can see you already have the region of the map. It doesn't only contain the lat & long deltas but also the center point of the region. You can calculate the distances in meters as illustrated in the picture:
1: Get the region span (how big the region is in lat/long degrees)
MKCoordinateSpan span = region.span;
2: Get the region center (lat/long coordinates)
CLLocationCoordinate2D center = region.center;
3: Create two locations (loc1 & loc2, north - south) based on the center location and calculate the distance inbetween (in meters)
//get latitude in meters
CLLocation *loc1 = [[CLLocation alloc] initWithLatitude:(center.latitude - span.latitudeDelta * 0.5) longitude:center.longitude];
CLLocation *loc2 = [[CLLocation alloc] initWithLatitude:(center.latitude + span.latitudeDelta * 0.5) longitude:center.longitude];
int metersLatitude = [loc1 distanceFromLocation:loc2];
4: Create two locations (loc3 & loc4, west - east) based on the center location and calculate the distance inbetween (in meters)
//get longitude in meters
CLLocation *loc3 = [[CLLocation alloc] initWithLatitude:center.latitude longitude:(center.longitude - span.longitudeDelta * 0.5)];
CLLocation *loc4 = [[CLLocation alloc] initWithLatitude:center.latitude longitude:(center.longitude + span.longitudeDelta * 0.5)];
int metersLongitude = [loc3 distanceFromLocation:loc4];
Swift implementation for Hanne's solution:
let span = mapView.region.span
let center = mapView.region.center
let loc1 = CLLocation(latitude: center.latitude - span.latitudeDelta * 0.5, longitude: center.longitude)
let loc2 = CLLocation(latitude: center.latitude + span.latitudeDelta * 0.5, longitude: center.longitude)
let loc3 = CLLocation(latitude: center.latitude, longitude: center.longitude - span.longitudeDelta * 0.5)
let loc4 = CLLocation(latitude: center.latitude, longitude: center.longitude + span.longitudeDelta * 0.5)
let metersInLatitude = loc1.distanceFromLocation(loc2)
let metersInLongitude = loc3.distanceFromLocation(loc4)
EDIT:
For Swift 5 distanceFromLocation(_:) has been renamed to distance(from:) meaning that the two last lines now read
let metersInLatitude = loc1.distance(from: loc2)
let metersInLongitude = loc3.distance(from: loc4)
Based on the above, here's a useful extension I use in my mapping projects:
extension MKCoordinateRegion {
public var radius: CLLocationDistance {
let loc1 = CLLocation(latitude: center.latitude - span.latitudeDelta * 0.5, longitude: center.longitude)
let loc2 = CLLocation(latitude: center.latitude + span.latitudeDelta * 0.5, longitude: center.longitude)
let loc3 = CLLocation(latitude: center.latitude, longitude: center.longitude - span.longitudeDelta * 0.5)
let loc4 = CLLocation(latitude: center.latitude, longitude: center.longitude + span.longitudeDelta * 0.5)
let metersInLatitude = loc1.distance(from: loc2)
let metersInLongitude = loc3.distance(from: loc4)
let radius = max(metersInLatitude, metersInLongitude) / 2.0
return radius
}
}

One unit test affects another

I'm defining two functions:
In mkgeometry_additions.h
#import <Foundation/Foundation.h>
#import <MapKit/MapKit.h>
MKMapRect MKMapRectForCoordinateRegion(MKCoordinateRegion region);
MKCoordinateRegion MKCoordinateRegionForSouthWestAndNorthEast(CLLocationCoordinate2D sw, CLLocationCoordinate2D ne);
In mkgeometry_additions.m (.m, even though they're c functions)
MKMapRect MKMapRectForCoordinateRegion(MKCoordinateRegion region)
{
CLLocationCoordinate2D center = region.center;
MKCoordinateSpan span = region.span;
CLLocationCoordinate2D topLeft =
CLLocationCoordinate2DMake(center.latitude - span.latitudeDelta / 2.0,
center.longitude - span.longitudeDelta / 2.0);
CLLocationCoordinate2D bottomRight =
CLLocationCoordinate2DMake(center.latitude - span.latitudeDelta / 2.0,
center.longitude - span.longitudeDelta / 2.0);
MKMapPoint mapPointTopLeft = MKMapPointForCoordinate(topLeft);
MKMapPoint mapPointBottomRight = MKMapPointForCoordinate(bottomRight);
double width = mapPointTopLeft.x - mapPointBottomRight.x;
double height = mapPointTopLeft.y - mapPointBottomRight.y;
return MKMapRectMake(mapPointTopLeft.x, mapPointTopLeft.y, width, height);
}
MKCoordinateRegion MKCoordinateRegionForSouthWestAndNorthEast(CLLocationCoordinate2D sw, CLLocationCoordinate2D ne)
{
MKCoordinateSpan span =
MKCoordinateSpanMake(ne.latitude - sw.latitude, ne.longitude - sw.longitude);
CLLocationCoordinate2D topLeft = CLLocationCoordinate2DMake(ne.latitude, sw.longitude);
CLLocationCoordinate2D center = CLLocationCoordinate2DMake(topLeft.latitude + span.latitudeDelta * 0.5, topLeft.longitude + span.longitudeDelta * 0.5);
return MKCoordinateRegionMake(center, span);
}
That's pretty much the entirety of the two files.
In my unit test with XCTests:
- (void)testMKMapRectForCoordinateRegionAfterReverseConversion
{
CLLocationCoordinate2D fremont = CLLocationCoordinate2DMake(37.54827, -121.98857);
MKCoordinateRegion region = MKCoordinateRegionMake(fremont, MKCoordinateSpanMake(0.05, 0.05));
MKMapRect mapRect = MKMapRectForCoordinateRegion(region);
MKCoordinateRegion derivedRegion = MKCoordinateRegionForMapRect(mapRect);
double accuracy = 0.0001;
XCTAssertEqualWithAccuracy(region.center.latitude, derivedRegion.center.latitude,
accuracy, #"Latitude is equal");
XCTAssertEqualWithAccuracy(region.center.longitude, derivedRegion.center.longitude,
accuracy, #"Latitude is equal");
XCTAssertEqualWithAccuracy(region.span.latitudeDelta, derivedRegion.span.latitudeDelta,
accuracy, #"Latitude delta is equal");
XCTAssertEqualWithAccuracy(region.span.longitudeDelta, derivedRegion.span.longitudeDelta,
accuracy, #"Latitude delta is equal");
}
- (void)testMKCoordinateRegionForSouthWestAndNorthEast
{
CLLocationCoordinate2D taipeiWanHua = CLLocationCoordinate2DMake(25.02946, 121.49652);
CLLocationCoordinate2D taipeiSongShan = CLLocationCoordinate2DMake(25.06640, 121.56166);
/* When the following line is called, the first test fails */
MKCoordinateRegionForSouthWestAndNorthEast(taipeiWanHua, taipeiSongShan);
/* I have the rest of the lines in this test commented out */
}
Now, I have empty setup and teardown for this test class. When I run the test. The first test fails if the third line of the second test is in the test. A closer look shows that when it fails, it's that the values are still similar but falling outside of the given accuracy:
If I commented that third line of the second test out, the first test would pass. Why is that?
The problem is not with unit testing - for me it fails even if I comment out entirely the second test - it is with the MKMapRectForCoordinateRegion implementation.
I think the correct implementation is:
MKMapRect MKMapRectForCoordinateRegion(MKCoordinateRegion region)
{
CLLocationCoordinate2D center = region.center;
MKCoordinateSpan span = region.span;
CLLocationCoordinate2D topLeft =
CLLocationCoordinate2DMake(center.latitude + span.latitudeDelta / 2.0,
center.longitude - span.longitudeDelta / 2.0);
CLLocationCoordinate2D bottomRight =
CLLocationCoordinate2DMake(center.latitude - span.latitudeDelta / 2.0,
center.longitude + span.longitudeDelta / 2.0);
MKMapPoint mapPointTopLeft = MKMapPointForCoordinate(topLeft);
MKMapPoint mapPointBottomRight = MKMapPointForCoordinate(bottomRight);
double width = mapPointBottomRight.x - mapPointTopLeft.x;
double height = mapPointBottomRight.y - mapPointTopLeft.y;
MKMapRect ret = MKMapRectMake(mapPointTopLeft.x, mapPointTopLeft.y, width, height);
return ret;
}
check out this: Mercator Projection

How to setRegion with google maps sdk for iOS?

How to setRegion with google maps sdk for iOS?
I want set zoom for location and radius of markers.
UPDATE:
The original answer below is obsolete as of version 1.2 of the SDK - you can now use the fitBounds: method of the GMSCameraUpdate class:
https://developers.google.com/maps/documentation/ios/reference/interface_g_m_s_camera_update
Original answer:
The MKMapPoint type in MapKit defines a 2D projection of a map. Although the actual values of the projection are meant to be opaque, they turn out to be equivalent to pixels at zoom level 20. This can be used to convert lat/lon values to pixels, and therefore a scale, and therefore a zoom level.
Start by defining two locations which specify the bounds of the region you want to display. These could be opposite corners of the bounding box, or just two locations, for example:
CLLocationCoordinate2D location1 =
CLLocationCoordinate2DMake(-33.8683, 151.2086); // Sydney
CLLocationCoordinate2D location2 =
CLLocationCoordinate2DMake(-31.9554, 115.8585); // Perth
If you have more than two points that you want to include, you could calculate the bounds of them yourself. This can also be done using GMSCoordinateBounds, for example:
GMSCoordinateBounds* bounds =
[[GMSCoordinateBounds alloc]
initWithCoordinate: CLLocationCoordinate2DMake(-33.8683, 151.2086) // Sydney
andCoordinate: CLLocationCoordinate2DMake(-31.9554, 115.8585)]; // Perth
bounds = [bounds including:
CLLocationCoordinate2DMake(-12.4667, 130.8333)]; // Darwin
CLLocationCoordinate2D location1 = bounds.southWest;
CLLocationCoordinate2D location2 = bounds.northEast;
Next, you need to get the size of the map view in points. You could use this:
float mapViewWidth = _mapView.frame.size.width;
float mapViewHeight = _mapView.frame.size.height;
But that will only work if you've already created the map view. Also, if you're using the sample code in the getting started guide, the frame is set to CGRectZero, as the actual size will be set later to fill the screen. In these cases if you're creating a full-screen map then you might want something like this:
float mapViewWidth = [UIScreen mainScreen].applicationFrame.size.width;
float mapViewHeight = [UIScreen mainScreen].applicationFrame.size.height;
Otherwise, use the size which you're creating your map view with.
Now you have the info necessary to calculate the camera position:
MKMapPoint point1 = MKMapPointForCoordinate(location1);
MKMapPoint point2 = MKMapPointForCoordinate(location2);
MKMapPoint centrePoint = MKMapPointMake(
(point1.x + point2.x) / 2,
(point1.y + point2.y) / 2);
CLLocationCoordinate2D centreLocation = MKCoordinateForMapPoint(centrePoint);
double mapScaleWidth = mapViewWidth / fabs(point2.x - point1.x);
double mapScaleHeight = mapViewHeight / fabs(point2.y - point1.y);
double mapScale = MIN(mapScaleWidth, mapScaleHeight);
double zoomLevel = 20 + log2(mapScale);
GMSCameraPosition *camera = [GMSCameraPosition
cameraWithLatitude: centreLocation.latitude
longitude: centreLocation.longitude
zoom: zoomLevel];
You can then initialize the map view with this camera, or set the map view to this camera.
For this code to compile, you will need to add the MapKit framework to your project, and then also import it:
#import <MapKit/MapKit.h>
Note that this code doesn't handle wrap-around if your coordinates span across the date line. For example if you tried using this code with Tokyo and Hawaii, instead of displaying an area of the Pacific, it will try to display almost the entire world. In portrait mode it's not possible to zoom out far enough to see Hawaii on the left and Tokyo on the right, and so the map ends up centred on Africa with neither location visible. You could modify the above code to handle the wrap-around at the date line if you wanted to.
UPDATE
All issues were fixed in the latest version of Google maps (1.5). Standard method [mapView_ animateWithCameraUpdate:[GMSCameraUpdate fitBounds:bounds]]; can noow be used instead of the code below
ORIGINAL ANSWER
[GMSCameraUpdate fitBounds] does not give accurate results in my version of the SDK (1.2.0). I am using the code below instead of it. The formulae are taken from the Mercator Projection. The world is latitudonally bounded at 85 degrees as per Google Documentation.
#import <stdlib.h>
-(void) animateBoundsNorth:(CGFloat)north West:(CGFloat)west South:(CGFloat)south East:(CGFloat)east Padding:(int)padding {
CGFloat northRad = north * M_PI / 180.0;
CGFloat northProj = logf(tanf(M_PI_4 + northRad/2.0));
CGFloat southRad = south * M_PI / 180.0;
CGFloat southProj = logf(tanf(M_PI_4 + southRad/2.0));
CGFloat topRad = 85.0 * M_PI / 180.0;
CGFloat topProj = logf(tanf(M_PI_4 + topRad/2.0));
CGFloat zoomLat = log2f((mapView_.bounds.size.height - padding * 2) * 2 * topProj /(northProj - southProj)) - 8;
CGFloat zoomLon = log2f((mapView_.bounds.size.width - padding * 2) * 360/(east - west)) - 8;
GMSCameraUpdate *update = [GMSCameraUpdate setTarget:CLLocationCoordinate2DMake((north+south)/2.0, (west+east)/2.0) zoom:MIN(zoomLat, zoomLon)];
[mapView_ animateWithCameraUpdate:update];
}
Saxun Druce's answer is really good. But in addition, if you want to calculate a radius from any location you can do that with the following code:
CLLocationCoordinate2D center = CLLocationCoordinate2DMake([currentLocationLat doubleValue],[currentLocationLong doubleValue]);
float radius = 25*1000; //radius in meters (25km)
MKCoordinateRegion region = MKCoordinateRegionMakeWithDistance(center, radius*2, radius*2);
CLLocationCoordinate2D northEast = CLLocationCoordinate2DMake(region.center.latitude - region.span.latitudeDelta/2, region.center.longitude - region.span.longitudeDelta/2);
CLLocationCoordinate2D southWest = CLLocationCoordinate2DMake(region.center.latitude + region.span.latitudeDelta/2, region.center.longitude + region.span.longitudeDelta/2);
GMSCoordinateBounds* bounds = [[GMSCoordinateBounds alloc]
initWithCoordinate: northEast
andCoordinate: southWest];
If you have latitude and longitude of 'far-left' and 'near-right' corners of the google map ,you can display the data in swift using below code
var region:GMSVisibleRegion = GMSVisibleRegion()
region.nearLeft = CLLocationCoordinate2DMake(nearleflat, nearleftlong)
region.farRight = CLLocationCoordinate2DMake(fareastlat,fareastlong)
let bounds = GMSCoordinateBounds(coordinate: region.nearLeft,coordinate: region.farRight)
let camera = googleMapView.cameraForBounds(bounds, insets:UIEdgeInsetsZero)
googleMapView.camera = camera;
This link may also be helpful for related things.
I currently using this method.
self.markers is a dictionary with markers stored by a location ID, self.currentLocation is a CLLocation2D, and self.mapView is a GMSMapView.
The maths here is a check on whether to match the sizes on the width or the height, and then a calculation of the zoom based on the fact that x1 / pow(2, zoom1) = x2 / pow(2, zoom2)", leading to zoom2 = log2(x2 * pow(2, self.mapView.camera.zoom) / x1).
- (void)fitMarkers
{
if (2 > self.markers.count)
{
[self.mapView animateToCameraPosition:[GMSCameraPosition cameraWithTarget:self.currentLocation.coordinate zoom:kZoom]];
return;
}
NSArray* markers = self.markers.allValues;
GMSCoordinateBounds* markerBounds = [[GMSCoordinateBounds alloc] initWithCoordinate:((id<GMSMarker>)markers[0]).position andCoordinate:((id<GMSMarker>)markers[1]).position];
for (id<GMSMarker> marker in markers)
{
markerBounds = [markerBounds including:marker.position];
}
// get marker bounds in points
CGPoint markerBoundsTopLeft = [self.mapView.projection pointForCoordinate:CLLocationCoordinate2DMake(markerBounds.northEast.latitude, markerBounds.southWest.longitude)];
CGPoint markerBoundsBottomRight = [self.mapView.projection pointForCoordinate:CLLocationCoordinate2DMake(markerBounds.southWest.latitude, markerBounds.northEast.longitude)];
// get user location in points
CGPoint currentLocation = [self.mapView.projection pointForCoordinate:self.currentLocation.coordinate];
CGPoint markerBoundsCurrentLocationMaxDelta = CGPointMake(MAX(fabs(currentLocation.x - markerBoundsTopLeft.x), fabs(currentLocation.x - markerBoundsBottomRight.x)), MAX(fabs(currentLocation.y - markerBoundsTopLeft.y), fabs(currentLocation.y - markerBoundsBottomRight.y)));
// the marker bounds centered on self.currentLocation
CGSize centeredMarkerBoundsSize = CGSizeMake(2.0 * markerBoundsCurrentLocationMaxDelta.x, 2.0 * markerBoundsCurrentLocationMaxDelta.y);
// inset the view bounds to fit markers
CGSize insetViewBoundsSize = CGSizeMake(self.mapView.bounds.size.width - kMarkerSize / 2.0 - kMarkerMargin, self.mapView.bounds.size.height - kMarkerSize / 2.0 - kMarkerSize);
CGFloat x1;
CGFloat x2;
// decide which axis to calculate the zoom level with by comparing the width/height ratios
if (centeredMarkerBoundsSize.width / centeredMarkerBoundsSize.height > insetViewBoundsSize.width / insetViewBoundsSize.height)
{
x1 = centeredMarkerBoundsSize.width;
x2 = insetViewBoundsSize.width;
}
else
{
x1 = centeredMarkerBoundsSize.height;
x2 = insetViewBoundsSize.height;
}
CGFloat zoom = log2(x2 * pow(2, self.mapView.camera.zoom) / x1);
GMSCameraPosition* camera = [GMSCameraPosition cameraWithTarget:self.currentLocation.coordinate zoom:zoom];
[self.mapView animateToCameraPosition:camera];
}
As per new release of GoogleMaps iOS sdk 1.9.2,
We can set up using Camera's position, zoom level, viewingAngle.
GMSCameraPosition* camera = [GMSCameraPosition cameraWithLatitude:28.6100
longitude:77.2300
zoom:14.0
bearing:0
viewingAngle:0.00];
self.mapView = [GMSMapView mapWithFrame:CGRectMake(0, 45, self.view.frame.size.width, self.view.frame.size.height - 45) camera:camera];
mapView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
mapView.delegate = self;
mapView.myLocationEnabled = YES;
mapView.mapType = kGMSTypeTerrain;
mapView.settings.compassButton = YES;
mapView.settings.myLocationButton = YES;
[self.mapView setMinZoom:10 maxZoom:18];
GMSMarker* marker = [[GMSMarker alloc] init];
marker.position = CLLocationCoordinate2DMake(28.6100, 77.2300);
marker.title = #"New Delhi";
marker.snippet = #"Capital Of India";
marker.map = self.mapView;
marker.appearAnimation = kGMSMarkerAnimationPop;
marker.icon = [GMSMarker markerImageWithColor:[UIColor grayColor]];
[self.view addSubview:self.mapView];
For Further reference see this PDF document.
you can also set minimum and maximum Zoom level as per your need:
[self.mapView setMinZoom:10 maxZoom:30];
Hope this solves the problem.
I searched through the header files of the framework and only found the interface that could be used for the following code which could be a start. The problem here is that i can not find any imports of GMSCoordinateBounds in one of the other headers so i can not find a way to display this region in GMSMapView.
GMSVisibleRegion region;
region.farLeft = CLLocationCoordinate2DMake(farLeftLatitude, farLeftlongitude);
region.farRight = CLLocationCoordinate2DMake(farRightlatitude, farRightlongitude);
GMSCoordinateBounds *bounds = [[GMSCoordinateBounds alloc] initWithRegion:region];
As of June 2014, this answer is the simplest way to iterate over a given array of markers and then set bounds accordingly.
// I have found this method that worked for me
func setMapZoomToRadius(lat:Double, lng:Double, var mile:Double)
{
let center = CLLocationCoordinate2DMake(lat, lng)
let radius: Double = (mile ) * 621.371
let region = MKCoordinateRegionMakeWithDistance(center, radius * 2.0, radius * 2.0)
let northEast = CLLocationCoordinate2DMake(region.center.latitude - region.span.latitudeDelta, region.center.longitude - region.span.longitudeDelta)
let southWest = CLLocationCoordinate2DMake(region.center.latitude + region.span.latitudeDelta, region.center.longitude + region.span.longitudeDelta)
print("\(region.center.longitude) \(region.span.longitudeDelta)")
let bounds = GMSCoordinateBounds(coordinate: southWest, coordinate: northEast)
let camera = googleMapView.cameraForBounds(bounds, insets:UIEdgeInsetsZero)
googleMapView.camera = camera;
}

Zoom MKMapView to best fit distance zoom level based on list of locations

I have a center location I want my MKMapView to zoom to and also a list of locations (latitude/longitude). I would like to set the zoom level (distance) based on this list of locations so that as many of these locations are visible on the MKMapView at the same time, but with a minimum zoom level (or max distance) so that my MKMapView does not zoom out to much.
So lets say I have a list of 10 locations but one of those locations is so far away from my center location that if I show that one location on the MKMapView it will be zoomed out to much, how do I calculate the distance parameters for MKCoordinateRegionMakeWithDistance?
I hope I explained my problem well enough, I have difficulties explaining it I think.
Thank you
Søren
I got it working with the following solution.
I use the Great-circle distance formula to calculate the distance between my center point and all the found lat/long points, and if this distance is greater than my minimum distance and my maximum distance and larger than my "last found distance" I set this distance to be my zoom distance. It works like a charm and was actually quite simple.
Here is my distance calculation code:
-(double)distanceBetweenLocations:(CLLocationCoordinate2D)c1 :(CLLocationCoordinate2D)c2 {
int r = 6371 * 1000;//meters
double dLat = (c2.latitude - c1.latitude) * M_PI / 180;
double dLon = (c2.longitude - c1.longitude) * M_PI / 180;
double lat1 = c1.latitude * M_PI / 180;
double lat2 = c2.latitude * M_PI / 180;
double a = sin(dLat / 2) * sin(dLat / 2) + sin(dLon / 2) * sin(dLon / 2) * cos(lat1) * cos(lat2);
double c = 2 * atan2(sqrt(a), sqrt(1 - a));
double d = r * c;
return d;
}
And here is my code that zooms to my center point with the calculated distance:
-(void)zoomToLocation {
double MAX_DISTANCE = 10000.0;
double MIN_DISTANCE = 600.0;
double zoomDistance = MIN_DISTANCE;
CLLocationCoordinate2D center;
center.latitude = self.searchLocationLatitude;
center.longitude = self.searchLocationLongitude;
BOOL treaterVisible = NO;
for (Treater *treater in treaters) {
CLLocationCoordinate2D c2;
c2.latitude = treater.latitude;
c2.longitude = treater.longitude;
double distance = [self distanceBetweenLocations:center :c2];
if(distance > zoomDistance && distance < MAX_DISTANCE) {
zoomDistance = distance;
treaterVisible = YES;
}
}
if(!treaterVisible) {
zoomDistance = MAX_DISTANCE;
}
CLLocationCoordinate2D location;
location.latitude = self.searchLocationLatitude;
location.longitude = self.searchLocationLongitude;
MKCoordinateRegion region = MKCoordinateRegionMakeWithDistance(location, zoomDistance, zoomDistance);
MKCoordinateRegion adjustedRegion = [self.mapView regionThatFits:region];
[self.mapView setRegion:adjustedRegion];
}
Just if anybody should need something similar.
Best regards
Søren
Edit didAddAnnotationViews method. I hope this is what you are looking for,
-(void) mapView:(MKMapView *)mapView didAddAnnotationViews:(NSArray *)views
{
MKMapRect zoomRect = MKMapRectNull;
for (id <MKAnnotation> annotation in mapView.annotations)
{
MKMapPoint annotationPoint = MKMapPointForCoordinate(annotation.coordinate);
MKMapRect pointRect = MKMapRectMake(annotationPoint.x, annotationPoint.y, 0.1, 0.1);
if (MKMapRectIsNull(zoomRect)) {
zoomRect = pointRect;
} else {
zoomRect = MKMapRectUnion(zoomRect, pointRect);
}
}
[mapView setVisibleMapRect:zoomRect animated:YES];
}
DDAnnotation is custom class for display pin(PlaceMark)
-(void)zoomToFitMapAnnotations:(MKMapView*)mapView
{
if([mapView.annotations count] == 0)
return;
CLLocationCoordinate2D topLeftCoord;
topLeftCoord.latitude = -90;
topLeftCoord.longitude = 180;
CLLocationCoordinate2D bottomRightCoord;
bottomRightCoord.latitude = 90;
bottomRightCoord.longitude = -180;
for(DDAnnotation* annotation in mapView.annotations)
{
topLeftCoord.longitude = fmin(topLeftCoord.longitude, annotation.coordinate.longitude);
topLeftCoord.latitude = fmax(topLeftCoord.latitude, annotation.coordinate.latitude);
bottomRightCoord.longitude = fmax(bottomRightCoord.longitude, annotation.coordinate.longitude);
bottomRightCoord.latitude = fmin(bottomRightCoord.latitude, annotation.coordinate.latitude);
}
NSLog(#"A%f, B%f, C%f, D%f,", topLeftCoord.latitude, topLeftCoord.longitude, bottomRightCoord.latitude, bottomRightCoord.longitude);
MKCoordinateRegion region;
region.center.latitude = topLeftCoord.latitude - (topLeftCoord.latitude - bottomRightCoord.latitude) * 0.5;
region.center.longitude = topLeftCoord.longitude + (bottomRightCoord.longitude - topLeftCoord.longitude) * 0.5;
region.span.latitudeDelta = fabs(topLeftCoord.latitude - bottomRightCoord.latitude) * 1.1;
region.span.longitudeDelta = fabs(bottomRightCoord.longitude - topLeftCoord.longitude) * 1.1;
region = [mapView regionThatFits:region];
[mapView setRegion:region animated:YES];
}
Some days ago i have read a solution to your problem in a book, I think it is Preparata Shamos, Computational Geometry.
Unfortunatley the solution is komplex: it is the question: find the best/max subset of points that are visible withing a given radius.
if your point count is not to high, less than ten thousands, i would do it as follows
0) add all point to current selected subset.
1) iterate through all points insubset, calcualte gravity point, calc all distances to gravity point, does it fit the max zoom level desired distance? yes the ready.
2) No? the remove the point from subset, with highest distance, and do step 1 again.
Do that till all points are within the desired distance from the current subset center.
The gravity point is that point in the subset with average x (or longitude) and average y ( or latitude) coordinate.
(If you accept, please dont upvote)
For this we need to find the minimum and maximum latitude and longitude, after that we need make the region with average sum of these latitude and longitude. then assign the region to the mapview and fit the region on it. thats it.

What's the best way to zoom out and fit all annotations in MapKit

Quick background of what I have going on. UIMapView loads and shows a single annotation (Never more than one). On the menu bar, there is a Locate Me button, on tap the userLocation is found and displayed as a second annotation. I then I have MapView zoom out to fit both those annotations in range but I am unable to find a suitable way of doing so. Depending on where the first annotation is placed in relation to the user location, sometimes it doesn't zoom out enough.
For example, if the annotation is North West of the User, it zooms out fine. But if the annotation is South East of the User, it only zooms out enough that they are just getting cut off and you have to manually zoom out a bit more. Here's the code I am using, variation of some others I have found on SO.
CLLocation *whereIAm = mapView.userLocation.location;
float lat = whereIAm.coordinate.latitude;
float lon = whereIAm.coordinate.longitude;
CLLocationCoordinate2D southWest = {[currentLatitude floatValue], [currentLongitude floatValue]};
CLLocationCoordinate2D northEast = southWest;
southWest.latitude = MIN(southWest.latitude, lat);
southWest.longitude = MIN(southWest.longitude, lon);
northEast.latitude = MAX(northEast.latitude, lat);
northEast.longitude = MAX(northEast.longitude, lon);
CLLocation *locSouthWest = [[CLLocation alloc] initWithLatitude:southWest.latitude longitude:southWest.longitude];
CLLocation *locNorthEast = [[CLLocation alloc] initWithLatitude:northEast.latitude longitude:northEast.longitude];
CLLocationDistance meters = [locSouthWest distanceFromLocation:locNorthEast];
MKCoordinateRegion region;
region.center.latitude = (southWest.latitude + northEast.latitude) / 2.0;
region.center.longitude = (southWest.longitude + northEast.longitude) / 2.0;
region.span.latitudeDelta = meters / 111319.5
region.span.longitudeDelta = 7.0;
MKCoordinateRegion savedRegion = [mapView regionThatFits:region];
[mapView setRegion:savedRegion animated:YES];
[locSouthWest release];
[locNorthEast release];
Is there a better way built into MapKit to just zoom out so that both annotations have, lets say half an inch between them at the outer frame? I don't really care if the user has to zoom in after, I just want it to zoom out properly.
-(void)zoomToFitMapAnnotations:(MKMapView*)mapView
{
if([mapView.annotations count] == 0)
return;
CLLocationCoordinate2D topLeftCoord;
topLeftCoord.latitude = -90;
topLeftCoord.longitude = 180;
CLLocationCoordinate2D bottomRightCoord;
bottomRightCoord.latitude = 90;
bottomRightCoord.longitude = -180;
for(MapAnnotation* annotation in mapView.annotations)
{
topLeftCoord.longitude = fmin(topLeftCoord.longitude, annotation.coordinate.longitude);
topLeftCoord.latitude = fmax(topLeftCoord.latitude, annotation.coordinate.latitude);
bottomRightCoord.longitude = fmax(bottomRightCoord.longitude, annotation.coordinate.longitude);
bottomRightCoord.latitude = fmin(bottomRightCoord.latitude, annotation.coordinate.latitude);
}
MKCoordinateRegion region;
region.center.latitude = topLeftCoord.latitude - (topLeftCoord.latitude - bottomRightCoord.latitude) * 0.5;
region.center.longitude = topLeftCoord.longitude + (bottomRightCoord.longitude - topLeftCoord.longitude) * 0.5;
region.span.latitudeDelta = fabs(topLeftCoord.latitude - bottomRightCoord.latitude) * 1.1; // Add a little extra space on the sides
region.span.longitudeDelta = fabs(bottomRightCoord.longitude - topLeftCoord.longitude) * 1.1; // Add a little extra space on the sides
region = [mapView regionThatFits:region];
[mapView setRegion:region animated:YES];
}
Taken from: http://codisllc.com/blog/zoom-mkmapview-to-fit-annotations/
(Use it all the time.)
In iOS7 there's a method that does just that. Just call:
[yourMapView showAnnotations:yourAnnotationsArray animated:YES];
Those who do the monotouch c# coding
BasicMapAnnotation is inherit class from MKAnnotation
private void GetRegion(MKMapView mapView)
{
var userWasVisible = mapView.ShowsUserLocation;
mapView.ShowsUserLocation = false; // ignoring the blue blip
// start with the widest possible viewport
var tl = new CLLocationCoordinate2D(-90, 180); // top left
var br = new CLLocationCoordinate2D(90, -180); // bottom right
foreach (var an in mapView.Annotations)
{
// narrow the viewport bit-by-bit
CLLocationCoordinate2D coordinate = ((BasicMapAnnotation) an).Coordinate;
tl.Longitude = Math.Min(tl.Longitude, coordinate.Longitude);
tl.Latitude = Math.Max(tl.Latitude, coordinate.Latitude);
br.Longitude = Math.Max(br.Longitude, coordinate.Longitude);
br.Latitude = Math.Min(br.Latitude, coordinate.Latitude);
}
var center = new CLLocationCoordinate2D
{
// divide the range by two to get the center
Latitude = tl.Latitude - (tl.Latitude - br.Latitude)*0.5,Longitude = tl.Longitude + (br.Longitude - tl.Longitude)*0.5
};
var span = new MKCoordinateSpan
{
// calculate the span, with 20% margin so pins aren’t on the edge
LatitudeDelta = Math.Abs(tl.Latitude - br.Latitude)*1.2,LongitudeDelta = Math.Abs(br.Longitude - tl.Longitude)*1.2
};
var region = new MKCoordinateRegion {Center = center, Span = span};
region = mapView.RegionThatFits(region); // adjusts zoom level too
mapView.SetRegion(region, true); // animated transition
mapView.ShowsUserLocation =
userWasVisible;
}
You can use this code to show all annotations
MKMapRect zoomRect = MKMapRectNull;
for (id <MKAnnotation> annotation in mapView.annotations)
{
MKMapPoint annotationPoint = MKMapPointForCoordinate(annotation.coordinate);
MKMapRect pointRect = MKMapRectMake(annotationPoint.x, annotationPoint.y, 0.1, 0.1);
zoomRect = MKMapRectUnion(zoomRect, pointRect);
}
[mapView setVisibleMapRect:zoomRect animated:YES];
if you want to include user location just replace two lines below with the first line of above code
MKMapPoint annotationPoint = MKMapPointForCoordinate(mapView.userLocation.coordinate);
MKMapRect zoomRect = MKMapRectMake(annotationPoint.x, annotationPoint.y, 0.1, 0.1);

Resources