How to setRegion with google maps sdk for iOS? - 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;
}

Related

Google maps won't load, crashes the whole app

I still have not solved this one yet.
I've worked with google maps in a few projects, both in Objective C and Swift 2 and iOS 9. I install it using cocoa pods and it works just fine but now, in this app where I'm also using parse and a four square API (not sure if that has anything to do with it) the map view won't even load and the app crashes when I try to.
This is the error I get...
-[GMSMapView animateToCameraPosition:]: unrecognized selector sent to instance 0x7fe51cb3bfb0
It breaks on the line that sets the map view frame, I've tried adding the Obj-C linker flag too (which I've heard could be a possible solution) but that gives me 32 different errors itself.
Can anyone help with this? Thanks a million.
Can't say I have experience with Google Maps in Swift, but here's some Obj-C code to work off of. First make sure your GMSMapView delegate is set to self and that you are setting the constraints in your viewDidLoad. I ran this in various spots:
- (void)setBounds
{
CLLocationCoordinate2D coordinate = [self.currentLocation coordinate];
CGFloat coordinateDifference = 0.002;
CGFloat firstLatitude = coordinate.latitude;
firstLatitude += coordinateDifference;
CGFloat firstLongitude = coordinate.longitude;
firstLongitude += coordinateDifference;
CLLocationDegrees topLat = firstLatitude;
CLLocationDegrees topLon = firstLongitude;
CLLocationCoordinate2D northEastCoordinate = CLLocationCoordinate2DMake(topLat, topLon);
CGFloat secondLatitude = coordinate.latitude;
secondLatitude -= coordinateDifference;
CGFloat secondLongitude = coordinate.longitude;
secondLongitude -= coordinateDifference;
CLLocationDegrees botLat = secondLatitude;
CLLocationDegrees botLon = secondLongitude;
CLLocationCoordinate2D southWestCoordinate = CLLocationCoordinate2DMake(botLat, botLon);
self.bounds = [[GMSCoordinateBounds alloc] init];
self.bounds = [[GMSCoordinateBounds alloc] initWithCoordinate:northEastCoordinate coordinate:southWestCoordinate];
}
- (void)createMap
{
CLLocationCoordinate2D coordinate = [self.currentLocation coordinate];
GMSCameraPosition *camera = [GMSCameraPosition cameraWithLatitude:coordinate.latitude longitude:coordinate.longitude zoom:17];
CGFloat h = self.topLayoutGuide.length;
CGRect rect = CGRectMake(0, h, self.view.bounds.size.width, self.view.bounds.size.height - self.navigationController.navigationBar.frame.size.height - self.postSongButton.frame.size.height - 20);
self.mapView = [GMSMapView mapWithFrame:rect camera:camera];
[self.view insertSubview:self.mapView atIndex:0];
}

Update region to be around Annotations

I have a mapView with some annotations and I'd like to center the map around the annotations. I have the following code:
- (void)updateRegion {
self.needUpdateRegion = NO;
CGRect boundingRect;
BOOL started = NO;
for (id <MKAnnotation> annotation in self.mapView.annotations){
CGRect annotationRect = CGRectMake(annotation.coordinate.longitude, annotation.coordinate.latitude, 0, 0);
if (!started) {
started = YES;
boundingRect = annotationRect;
} else {
boundingRect = CGRectUnion(boundingRect, annotationRect);
}
} if (started) {
boundingRect = CGRectInset(boundingRect, -0.2, -0.2);
if ((boundingRect.size.width >20) && (boundingRect.size.height >20)) {
MKCoordinateRegion region;
region.center.latitude = boundingRect.origin.x + boundingRect.size.width /2;
region.center.longitude = boundingRect.origin.y + boundingRect.size.height / 2;
region.span.latitudeDelta = boundingRect.size.width;
region.span.longitudeDelta = boundingRect.size.height;
[self.mapView setRegion:region animated:YES];
}
}
}
it gets executed in viewDidAppear to make the "sliding effect":
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
if (self.needUpdateRegion) [self updateRegion];
}
When I run the app it doesn't do anything and just shows the US.
The annotations are shown (in Europe).
Assuming that updateRegion gets called in the first place (make sure needUpdateRegion is initialized to YES), there are two main problems with the updateRegion method:
It only calls setRegion if the resulting bounding map rect's width and height are 20. Since you are doing the calculations using latitude and longitude degrees, this means setRegion will only get called if the resulting bounding map rect is more than 20 degrees latitude/longitude wide/high. It's not clear if this is what you intended.
The region properties are being set backwards. In the calculation of the bounding map rect, the x values are set to the longitude and the y values are set to the latitude. But when setting region.center.latitude, it is using boundingRect.origin.x instead of boundingRect.origin.y. This applies to the other properties as well so the code there should be:
region.center.longitude = boundingRect.origin.x + boundingRect.size.width /2;
region.center.latitude = boundingRect.origin.y + boundingRect.size.height / 2;
region.span.longitudeDelta = boundingRect.size.width;
region.span.latitudeDelta = boundingRect.size.height;
Note that iOS 7 provides a new convenient method showAnnotations:animated: to automatically show the annotations so you don't have to calculate the region yourself.
So in updateRegion you could do the following:
- (void)updateRegion {
self.needUpdateRegion = NO;
//if showAnnotations:animated: is available, use it...
if ([mapView respondsToSelector:#selector(showAnnotations:animated:)])
{
[self.mapView showAnnotations:mapView.annotations animated:YES];
return;
}
//calculate region manually...
}

MKMapRectMake how to zoom out after setup

I use MKMapRectMake to mark north east and south west to display a region. Here's how I do that:
routeRect = MKMapRectMake(southWestPoint.x, southWestPoint.y, northEastPoint.x - southWestPoint.x, northEastPoint.y - southWestPoint.y);
[self.mapView setVisibleMapRect:routeRect];
After I set up this display region, how can I zoom out the map a little? What is the best way to do this?
UPDATE
This is code that I use to get rect for setVisibleMapRect function:
for(Path* p in ar)
{
self.routeLine = nil;
self.routeLineView = nil;
// while we create the route points, we will also be calculating the bounding box of our route
// so we can easily zoom in on it.
MKMapPoint northEastPoint;
MKMapPoint southWestPoint;
// create a c array of points.
MKMapPoint* pointArr = malloc(sizeof(CLLocationCoordinate2D) * ar.count);
for(int idx = 0; idx < ar.count; idx++)
{
Path *m_p = [ar objectAtIndex:idx];
[NSCharacterSet characterSetWithCharactersInString:#","]];
CLLocationDegrees latitude = m_p.Latitude;
CLLocationDegrees longitude = m_p.Longitude;
// create our coordinate and add it to the correct spot in the array
CLLocationCoordinate2D coordinate = CLLocationCoordinate2DMake(latitude, longitude);
MKMapPoint point = MKMapPointForCoordinate(coordinate);
// adjust the bounding box
// if it is the first point, just use them, since we have nothing to compare to yet.
if (idx == 0) {
northEastPoint = point;
southWestPoint = point;
}
else
{
if (point.x > northEastPoint.x)
northEastPoint.x = point.x;
if(point.y > northEastPoint.y)
northEastPoint.y = point.y;
if (point.x < southWestPoint.x)
southWestPoint.x = point.x;
if (point.y < southWestPoint.y)
southWestPoint.y = point.y;
}
pointArr[idx] = point;
}
// create the polyline based on the array of points.
self.routeLine = [MKPolyline polylineWithPoints:pointArr count:ar.count];
_routeRect = MKMapRectMake(southWestPoint.x, southWestPoint.y, northEastPoint.x - southWestPoint.x, northEastPoint.y - southWestPoint.y);
// clear the memory allocated earlier for the points
free(pointArr);
[self.mapView removeOverlays: self.mapView.overlays];
// add the overlay to the map
if (nil != self.routeLine) {
[self.mapView addOverlay:self.routeLine];
}
// zoom in on the route.
[self zoomInOnRoute];
}
Try this: (Edit)
MKCoordinateRegion region;
MKCoordinateSpan span;
span.latitudeDelta = 0.01;
span.longitudeDelta = 0.01;
CLLocationCoordinate2D zoomLocation = newLocation.coordinate;
region.center = zoomLocation;
region.span = span;
region = [mapViewObject regionThatFits:region];
[mapViewObject setRegion:region animated:NO];
You can use this custom function to center the Map around two points
- (void)centerMapAroundSourceAndDestination
{
MKMapRect rect = MKMapRectNull;
MKMapPoint sourcePoint = MKMapPointForCoordinate(southWestPoint);
rect = MKMapRectUnion(rect, MKMapRectMake(sourcePoint.x, sourcePoint.y, 0, 0));
MKMapPoint destinationPoint = MKMapPointForCoordinate(_northEastPoint);
rect= MKMapRectUnion(rect, MKMapRectMake(destinationPoint.x, destinationPoint.y, 0, 0));
MKCoordinateRegion region = MKCoordinateRegionForMapRect(rect);
[_mapView setRegion:region animated:YES];
}
So in that case you need to find Centroid of a polygon and then pass that centroid values to this method so it would zoom to center of polygon i.e Centroid.
- (void)zoomMapView:(MKMapView *)mapview withLatitude:(Float32 )latitude andLongitude:(Float32 )longitude {
MKCoordinateRegion region;
region.span.latitudeDelta =0.005; //Change values to zoom. lower the value to zoom in and vice-versa
region.span.longitudeDelta = 0.009;//Change values to zoom. lower the value to zoom in and vice-versa
CLLocationCoordinate2D location;
location.latitude = latitude; // Add your Current Latitude here.
location.longitude = longitude; // Add your Current Longitude here.
region.center = location;
[mapview setRegion:region];
}
To use this method you need to pass three thing mapView, latitude and longitude i.e Position where to zoom.
how can I zoom out the map a little?
Unfortunatley MkMapView setRegion behaves so strange that this does not work on iPhone. (ios 6.1.3)
It works on iPad (ios 6.1.3)
setRegion and setVisibleMapRect
both changes the zoom factor only by steps of two. So you cannot programmatically zoom out by e.g 10%. Although Apple maps are vector based, they still snap the next higher zoom level that (would) fit the map tile pixels 1:1. Maybe to be compatible to map satellite display mode, which uses prerendered bitmaps.
Although bot methods should only correct the aspect ratio if you provided one of the lat/lon spans not perfectly, they additonally snap to said zoom levels.
Try it out: display map, then on button press: get the current region, make the longitude span 10% bigger (factor 1.1), set the region, then read it back, you will see on iphone simu and on iphone4 device the longitude span is now double the size, instead of factor 1.1.
Till today there exists no good solution.
Shame on you Apple.

Convert GMSVisibleRegion to CLRegion or MKCoordinateRegion

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)
}
}
}

Drawing Great Circle overlay lines on an MKMapView

I'm trying to draw a Great Circle line between two lat/lon points on an MKMapView. This is a line that would appear rounded (a 'straight' line on a globe) and is best visualized here. In fact this very odd WordPress site seems to begin to describe exactly how to do this, but it ends abruptly after the first few steps.
Reading in Apple's documentation I see
In iOS 4.0 and later, you can also use projected map coordinates instead of regions to specify some values. When you project the curved surface of the globe onto a flat surface, you get a two-dimensional version of a map where longitude lines appear to be parallel. Locations and distances on this map are specified using the MKMapPoint, MKMapSize, and MKMapRect data types. You can use these data types to specify the map’s visible region and when specifying the location of overlays.
How I would apply this to a Great Circle overlay I'm not sure. Can anyone help?
I've implemented this for drawing a great circle route for aircraft going between two airports using MKPolyline.
+ (void)createGreatCircleMKPolylineFromPoint:(CLLocationCoordinate2D)point1
toPoint:(CLLocationCoordinate2D)point2
forMapView:(MKMapView*)mapView
{
double lat1 = point1.latitude;
double lon1 = point1.longitude;
double lat2 = point2.latitude;
double lon2 = point2.longitude;
lat1 = lat1 * (PI/180);
lon1 = lon1 * (PI/180);
lat2 = lat2 * (PI/180);
lon2 = lon2 * (PI/180);
double d = 2 * asin( sqrt(pow(( sin( (lat1-lat2)/2) ), 2) + cos(lat1) * cos(lat2) * pow(( sin( (lon1-lon2)/2) ), 2)));
int numsegs = 100;
CLLocationCoordinate2D *coords = malloc(sizeof(CLLocationCoordinate2D) * numsegs);
double f = 0.0;
for(int i=1; i<=numsegs; i++)
{
f += 1.0 / (float)numsegs;
double A=sin((1-f)*d)/sin(d);
double B=sin(f*d)/sin(d);
double x = A*cos(lat1) * cos(lon1) + B * cos(lat2) * cos(lon2);
double y = A*cos(lat1) * sin(lon1) + B * cos(lat2) * sin(lon2);
double z = A*sin(lat1) + B*sin(lat2);
double latr=atan2(z, sqrt(pow(x, 2) + pow(y, 2) ));
double lonr=atan2(y, x);
double lat = latr * (180/PI);
double lon = lonr * (180/PI);
// NSLog(#"lat: %f lon: %f", lat, lon);
CLLocationCoordinate2D loc = CLLocationCoordinate2DMake(lat, lon);
coords[i - 1] = loc;
}
//check for circling west to east. If the plane is crossing 180, we need
//to draw two lines or else the polyline connects the dots and draws a straight
//line all the way across the map.
CLLocationCoordinate2D prevCoord;
BOOL twoLines = NO;
int numsegs2 = 0;
CLLocationCoordinate2D *coords2;
for(int i=0; i<numsegs; i++)
{
CLLocationCoordinate2D coord = coords[i];
if(prevCoord.longitude < -170 && prevCoord.longitude > -180 && prevCoord.longitude < 0
&& coord.longitude > 170 && coord.longitude < 180 && coord.longitude > 0)
{
twoLines = YES;
coords2 = malloc(sizeof(CLLocationCoordinate2D) * (numsegs - i));
numsegs2 = numsegs - i;
for(int j=0; j<numsegs2; j++)
{
coords2[j] = coords[i + j];
}
break;
}
prevCoord = coord;
}
//remove any previously added overlays
[mapView removeOverlays:mapView.overlays];
if(twoLines)
{
MKPolyline *polyline = [MKPolyline polylineWithCoordinates:coords count:numsegs - numsegs2];
free(coords);
[mapView addOverlay:polyline];
MKPolyline *polyline2 = [MKPolyline polylineWithCoordinates:coords2 count:numsegs2];
free(coords2);
[mapView addOverlay:polyline2];
}
else
{
MKPolyline *polyline = [MKPolyline polylineWithCoordinates:coords count:numsegs];
free(coords);
[mapView addOverlay:polyline];
}
}
You've now created the overlay(s), now you just need to provide an MKOverlayView in mapView:viewForOverlay.
- (MKOverlayView*)mapView:(MKMapView *)mapView viewForOverlay:(id<MKOverlay>)overlay
{
MKPolyline *polyline = (MKPolyline*)overlay;
MKPolylineView *view = [[[MKPolylineView alloc] initWithPolyline:polyline] autorelease];
//choose your line params here
view.lineWidth = 2;
view.fillColor = [UIColor blueColor];
return view;
}
Hope this helps.
Screenshot http://s1-03.twitpicproxy.com/photos/large/489178500.png
It is late in the game, but worth mentioning MKGeodesicPolyline, new since iOS 7.0, which 'traces the shortest path along the surface of the Earth'.
With this it becomes simple to create and add an MKPolylineOverlay of type Geodesic Polyline.
points = [CLLocationCoordinate2DMake(27.123, 85.765),
CLLocationCoordinate2DMake(41.444, 106.987)]
geodesic = MKGeodesicPolyline(coordinates: points, count: 2)
mapView.add(geodesic)
remember to include the renderer and give the mapView a delegate:
//MARK: - MKMapView Delegate Method
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
if overlay is MKGeodesicPolyline {
let polylineRenderer = MKPolylineRenderer(overlay: overlay)
polylineRenderer.strokeColor = UIColor.white
polylineRenderer.lineWidth = 2.5
return polylineRenderer
}
}
This can be accomplish creating a subclass of the MKOverlayPathView class. You need to override the (void)createPath method, and basically you could use a UIBezierPath to create the arc, or create an arc as path directly, which is possible but I haven't done it yet.
Once you define the path on the method, you need to set the path property of the class with the newly created path. That way, the rendering of the path is going to be done automatically.
Hope this helps.
The numsegs parameter should be changeable according to the map zoom level and the distance of the two points. The lat/lon coordinate of the two points can be transformed into pixel coordinate. Thus the numsegs parameter can be viewed as a function of pixel differences.

Resources