Google Maps API v2: LatLngBounds from CameraPosition
Asked Answered
R

2

44

Is there a simple way to get the LatLngBounds of the visible map from a CameraPosition with Android Google Maps API v2 so that I can use the OnCameraChangeListener to go fetch new data for the markers.

mMap.setOnCameraChangeListener(new OnCameraChangeListener() {
            @Override
            public void onCameraChange(CameraPosition position) {
                LatLngBounds bounds = ?;
                fetchNewData(bounds);
            }
        });
Renunciation answered 18/12, 2012 at 20:48 Comment(0)
G
42

Update since August 2016

Summary the correct answer now for this problem is to use the new onCameraIdle, instead of OnCameraChangeListener, which is now deprecated. Read bellow how.

Now you can listen to "dragEnd"-like event, and even other events on the newest version of Google Maps for Android.

As shown in the docs, you can avoid the problem of multiple (aka "several") calls of the OnCameraChangeListener by using the new listeners. For example, you are now able to check what is the reason behind the camera move, which is the ideal to couple with a fetchData problem as requested. The following code is mostly directly taken from the docs. One more thing, it is necessary to use Google Play Services 9.4.

public class MyCameraActivity extends FragmentActivity implements
        OnCameraMoveStartedListener,
        OnCameraMoveListener,
        OnCameraMoveCanceledListener,
        OnCameraIdleListener,
        OnMapReadyCallback {

    private GoogleMap mMap;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_my_camera);

        SupportMapFragment mapFragment =
            (SupportMapFragment) getSupportFragmentManager()
                    .findFragmentById(R.id.map);
        mapFragment.getMapAsync(this);
    }

    @Override
    public void onMapReady(GoogleMap map) {
        mMap = map;

        mMap.setOnCameraIdleListener(this);
        mMap.setOnCameraMoveStartedListener(this);
        mMap.setOnCameraMoveListener(this);
        mMap.setOnCameraMoveCanceledListener(this);

        // Show Sydney on the map.
        mMap.moveCamera(CameraUpdateFactory
                .newLatLngZoom(new LatLng(-33.87365, 151.20689), 10));
    }

    @Override
    public void onCameraMoveStarted(int reason) {

        if (reason == OnCameraMoveStartedListener.REASON_GESTURE) {
            Toast.makeText(this, "The user gestured on the map.",
                           Toast.LENGTH_SHORT).show();
        } else if (reason == OnCameraMoveStartedListener
                                .REASON_API_ANIMATION) {
            Toast.makeText(this, "The user tapped something on the map.",
                           Toast.LENGTH_SHORT).show();
        } else if (reason == OnCameraMoveStartedListener
                                .REASON_DEVELOPER_ANIMATION) {
            Toast.makeText(this, "The app moved the camera.",
                           Toast.LENGTH_SHORT).show();
        }
    }

    @Override
    public void onCameraMove() {
        Toast.makeText(this, "The camera is moving.",
                       Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onCameraMoveCanceled() {
        Toast.makeText(this, "Camera movement canceled.",
                       Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onCameraIdle() {
        Toast.makeText(this, "The camera has stopped moving. Fetch the data from the server!", Toast.LENGTH_SHORT).show();
        LatLngBounds bounds = mMap.getProjection().getVisibleRegion().latLngBounds;
        fetchData(bounds)
    }
}

Workaround for a efficient solution before August 2016

As the question is properly answered, I would like to add on that on a likely to be next issue.

The problem arises when using OnCameraChangeListener to fetch data from the server due to the frequency in which this method is triggered.

There is an issue reported on how crazily frequent this method is trigged when doing a simple map sliding, thus in the example of the question, it would trigger fetchData multiple sequential times for very little camera changes, even for no camera changes, yes, it happens that the camera bounds have not changed, but the method gets triggered.

This could impact on the server side performance and would waste a lot of devices' resources by fetching data sequentially tens of times from the server.

You can find in that link workarounds for this problem, but there is not yet a official way to do it, e.g., using a desirable dragEnd, or cameraChangeEnd callbacks.

One example bellow, based on the ones from there, is how I avoid the aforementioned problem by playing with the time interval of the calls and discarding the calls with the same boundaries.

// Keep the current camera bounds
private LatLngBounds currentCameraBounds;

new GoogleMap.OnCameraChangeListener() {
    private static int CAMERA_MOVE_REACT_THRESHOLD_MS = 500;
    private long lastCallMs = Long.MIN_VALUE;

    @Override
    public void onCameraChange(CameraPosition cameraPosition) {
      LatLngBounds bounds = map.getProjection().getVisibleRegion().latLngBounds;
      // Check whether the camera changes report the same boundaries (?!), yes, it happens
      if (currentCameraBounds.northeast.latitude == bounds.northeast.latitude
         && currentCameraBounds.northeast.longitude == bounds.northeast.longitude
         && currentCameraBounds.southwest.latitude == bounds.southwest.latitude
         && currentCameraBounds.southwest.longitude == bounds.southwest.longitude) {
         return;
       }

      final long snap = System.currentTimeMillis();
      if (lastCallMs + CAMERA_MOVE_REACT_THRESHOLD_MS > snap) {
        lastCallMs = snap;
        return;
      }

      fetchData(bounds);

      lastCallMs = snap;
      currentCameraBounds = bounds;

}
Geochronology answered 2/12, 2015 at 14:40 Comment(8)
Very good! That probably saved me several hours of work understanding and then solving this problem. Thanks!Unroot
You should also check for null for the field currentCameraBounds.Fosterfosterage
Problem with onCameraIdle approach is that its triggered too many times also, at startup I see it's triggered twice, what could be a best strategy here? is this listener triggered on OnCreate, OnStart and OnResume?Celindaceline
@ImRickJames, have you tried to keep the "reason" globally, and checking, at the onCameraIdle, if the reason is still the one (REASON_GESTURE)? If you are moving your camera automatically to users' position at startup, it will trigger, but with REASON_DEVELOPER_ANIMATION.Geochronology
onCameraIdle is getting called automatically though I didn't even touch the map can anybody help meCodicodices
@Codicodices You could store the "reason" during onCameraMoveStarted and discard the call on onCameraIdle if it was not REASON_GESTURE. This in case you want to react to idle events initiated as a user gesture.Geochronology
@Geochronology thank you I solved my problem I was doing a rookie mistake by calling CameraUpdate location = CameraUpdateFactory.newLatLngZoom( latlng, 12); mMap.animateCamera(location); inside onCameraIdle that's make it recursive.Codicodices
since LatLngBounds implements equals, you can replace the lengthy equals comparison with the method latLng.equals(currentCameraBounds)Teplica
R
82

You can't get the LatLngBounds from the CameraPosition, but you can get them easily from the GoogleMap.

private GoogleMap mMap;

mMap.setOnCameraChangeListener(new OnCameraChangeListener() {
            @Override
            public void onCameraChange(CameraPosition position) {
                LatLngBounds bounds = mMap.getProjection().getVisibleRegion().latLngBounds;
                fetchData(bounds);
            }
        });
Renunciation answered 18/12, 2012 at 21:50 Comment(5)
I wonder why they set it up like that. The visible region shouldn't have anything to do with the map projection.Seta
@Seta Wait, what? The map projection is precisely what defines the 4 coordinates that define the visible regionChauvinism
I guess what i'm saying is that they shouldn't be coupled like that. Visible region is simply bounds no matter what projection it's in. It shouldn't matter if the projection is EPSG:4326 or EPSG:900913, the view is the same regardless if it's in lat/lon coordinates or meters x/y. So one should be able to do mMap.getProjection() or mMap.getVisibleRegion() independent of each other.Seta
@h4lc0n: The way OpenLayers does it makes more sense. The map object allows you to make those two calls independent of each other. map.getExtent(); map.getProjection();Seta
In this context, "projection" relates to the world-to-screen projection (rather than map projection), and would take into account camera rotation and tilt.Meingolda
G
42

Update since August 2016

Summary the correct answer now for this problem is to use the new onCameraIdle, instead of OnCameraChangeListener, which is now deprecated. Read bellow how.

Now you can listen to "dragEnd"-like event, and even other events on the newest version of Google Maps for Android.

As shown in the docs, you can avoid the problem of multiple (aka "several") calls of the OnCameraChangeListener by using the new listeners. For example, you are now able to check what is the reason behind the camera move, which is the ideal to couple with a fetchData problem as requested. The following code is mostly directly taken from the docs. One more thing, it is necessary to use Google Play Services 9.4.

public class MyCameraActivity extends FragmentActivity implements
        OnCameraMoveStartedListener,
        OnCameraMoveListener,
        OnCameraMoveCanceledListener,
        OnCameraIdleListener,
        OnMapReadyCallback {

    private GoogleMap mMap;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_my_camera);

        SupportMapFragment mapFragment =
            (SupportMapFragment) getSupportFragmentManager()
                    .findFragmentById(R.id.map);
        mapFragment.getMapAsync(this);
    }

    @Override
    public void onMapReady(GoogleMap map) {
        mMap = map;

        mMap.setOnCameraIdleListener(this);
        mMap.setOnCameraMoveStartedListener(this);
        mMap.setOnCameraMoveListener(this);
        mMap.setOnCameraMoveCanceledListener(this);

        // Show Sydney on the map.
        mMap.moveCamera(CameraUpdateFactory
                .newLatLngZoom(new LatLng(-33.87365, 151.20689), 10));
    }

    @Override
    public void onCameraMoveStarted(int reason) {

        if (reason == OnCameraMoveStartedListener.REASON_GESTURE) {
            Toast.makeText(this, "The user gestured on the map.",
                           Toast.LENGTH_SHORT).show();
        } else if (reason == OnCameraMoveStartedListener
                                .REASON_API_ANIMATION) {
            Toast.makeText(this, "The user tapped something on the map.",
                           Toast.LENGTH_SHORT).show();
        } else if (reason == OnCameraMoveStartedListener
                                .REASON_DEVELOPER_ANIMATION) {
            Toast.makeText(this, "The app moved the camera.",
                           Toast.LENGTH_SHORT).show();
        }
    }

    @Override
    public void onCameraMove() {
        Toast.makeText(this, "The camera is moving.",
                       Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onCameraMoveCanceled() {
        Toast.makeText(this, "Camera movement canceled.",
                       Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onCameraIdle() {
        Toast.makeText(this, "The camera has stopped moving. Fetch the data from the server!", Toast.LENGTH_SHORT).show();
        LatLngBounds bounds = mMap.getProjection().getVisibleRegion().latLngBounds;
        fetchData(bounds)
    }
}

Workaround for a efficient solution before August 2016

As the question is properly answered, I would like to add on that on a likely to be next issue.

The problem arises when using OnCameraChangeListener to fetch data from the server due to the frequency in which this method is triggered.

There is an issue reported on how crazily frequent this method is trigged when doing a simple map sliding, thus in the example of the question, it would trigger fetchData multiple sequential times for very little camera changes, even for no camera changes, yes, it happens that the camera bounds have not changed, but the method gets triggered.

This could impact on the server side performance and would waste a lot of devices' resources by fetching data sequentially tens of times from the server.

You can find in that link workarounds for this problem, but there is not yet a official way to do it, e.g., using a desirable dragEnd, or cameraChangeEnd callbacks.

One example bellow, based on the ones from there, is how I avoid the aforementioned problem by playing with the time interval of the calls and discarding the calls with the same boundaries.

// Keep the current camera bounds
private LatLngBounds currentCameraBounds;

new GoogleMap.OnCameraChangeListener() {
    private static int CAMERA_MOVE_REACT_THRESHOLD_MS = 500;
    private long lastCallMs = Long.MIN_VALUE;

    @Override
    public void onCameraChange(CameraPosition cameraPosition) {
      LatLngBounds bounds = map.getProjection().getVisibleRegion().latLngBounds;
      // Check whether the camera changes report the same boundaries (?!), yes, it happens
      if (currentCameraBounds.northeast.latitude == bounds.northeast.latitude
         && currentCameraBounds.northeast.longitude == bounds.northeast.longitude
         && currentCameraBounds.southwest.latitude == bounds.southwest.latitude
         && currentCameraBounds.southwest.longitude == bounds.southwest.longitude) {
         return;
       }

      final long snap = System.currentTimeMillis();
      if (lastCallMs + CAMERA_MOVE_REACT_THRESHOLD_MS > snap) {
        lastCallMs = snap;
        return;
      }

      fetchData(bounds);

      lastCallMs = snap;
      currentCameraBounds = bounds;

}
Geochronology answered 2/12, 2015 at 14:40 Comment(8)
Very good! That probably saved me several hours of work understanding and then solving this problem. Thanks!Unroot
You should also check for null for the field currentCameraBounds.Fosterfosterage
Problem with onCameraIdle approach is that its triggered too many times also, at startup I see it's triggered twice, what could be a best strategy here? is this listener triggered on OnCreate, OnStart and OnResume?Celindaceline
@ImRickJames, have you tried to keep the "reason" globally, and checking, at the onCameraIdle, if the reason is still the one (REASON_GESTURE)? If you are moving your camera automatically to users' position at startup, it will trigger, but with REASON_DEVELOPER_ANIMATION.Geochronology
onCameraIdle is getting called automatically though I didn't even touch the map can anybody help meCodicodices
@Codicodices You could store the "reason" during onCameraMoveStarted and discard the call on onCameraIdle if it was not REASON_GESTURE. This in case you want to react to idle events initiated as a user gesture.Geochronology
@Geochronology thank you I solved my problem I was doing a rookie mistake by calling CameraUpdate location = CameraUpdateFactory.newLatLngZoom( latlng, 12); mMap.animateCamera(location); inside onCameraIdle that's make it recursive.Codicodices
since LatLngBounds implements equals, you can replace the lengthy equals comparison with the method latLng.equals(currentCameraBounds)Teplica

© 2022 - 2024 — McMap. All rights reserved.