Question

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);
}
Was it helpful?

Solution

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

OTHER TIPS

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)
        }
    }
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top