Skip to content
This repository was archived by the owner on Aug 8, 2023. It is now read-only.

[ios] shouldChangeFromCamera doesn't allow for constraining the camera to specific bounds #9368

Closed
epau opened this issue Jun 26, 2017 · 13 comments
Labels
archived Archived because of inactivity feature iOS Mapbox Maps SDK for iOS

Comments

@epau
Copy link

epau commented Jun 26, 2017

Platform: iOS
Mapbox SDK version: 3.5

I talked to @1ec5 about this a little bit at WWDC '17, my apologies for posting this so late!!

The Problem

I want to constrain the viewport such as to not allow scrolling past some MGLCoordinateBounds (ie ne = (10,10) and sw = (-10,-10)).

I'm currently trying to implement this using the following delegate method:

- (BOOL)mapView:(MGLMapView *)mapView shouldChangeFromCamera:(MGLMapCamera *)oldCamera toCamera:(MGLMapCamera *)newCamera

This falls short however because I cannot force the camera to move to the bounds, I can only tell it to not move at all (simplified example: bounds = 10, old = 0, new = 15. I'd want to move to 10 but that is not currently possible)

Modifying properties on the newCamera instance has no effect since it it's properties aren't used after the delegate call (from MGLMapView.mm handlePanGesture)

CGPoint delta = [pan translationInView:pan.view];

MGLMapCamera *toCamera = [self cameraByPanningWithTranslation:delta panGesture:pan];
        
if (![self.delegate respondsToSelector:@selector(mapView:shouldChangeFromCamera:toCamera:)] ||
        [self.delegate mapView:self shouldChangeFromCamera:oldCamera toCamera:toCamera])
{
        _mbglMap->moveBy({ delta.x, delta.y });
        [pan setTranslation:CGPointZero inView:pan.view];
}

Potential Solution

It would be great to have modifications to newCamera in the delegate method affect the final result or to have some property/delegate method to restrict the viewport.

@boundsj boundsj added iOS Mapbox Maps SDK for iOS feature labels Jun 26, 2017
@1ec5
Copy link
Contributor

1ec5 commented Jun 26, 2017

Modifying properties on the newCamera instance has no effect since it it's properties aren't used after the delegate call

MGLMapCamera is a value class. Merely modifying its properties won’t cause a map view to change: #8930. So you’d need to set the map view’s camera property to the modified camera in order to apply a change to any of that camera’s properties. Then return NO so to preempt the call to moveBy().

@epau
Copy link
Author

epau commented Jun 26, 2017

Setting the map view's camera property in shouldChangeFromCamera causes some pretty glitchy/jumpy behavior while the user is panning or zooming.

- (BOOL)mapView:(MGLMapView *)mapView shouldChangeFromCamera:(MGLMapCamera *)oldCamera toCamera:(MGLMapCamera *)newCamera {
    if (MGLCoordinateInCoordinateBounds(newCamera.centerCoordinate, self.mapManager.mapBounds)) {
        return YES;
    }
    
    MGLMapCamera *camera = newCamera.copy;
    CLLocationDegrees lat = camera.centerCoordinate.latitude;
    CLLocationDegrees lon = camera.centerCoordinate.longitude;
    
    if (camera.centerCoordinate.latitude < self.mapManager.mapBounds.sw.latitude) {
        lat = self.mapManager.mapBounds.sw.latitude;
    } else if (camera.centerCoordinate.latitude > self.mapManager.mapBounds.ne.latitude) {
        lat = self.mapManager.mapBounds.ne.latitude;
    }
    
    if (camera.centerCoordinate.longitude < self.mapManager.mapBounds.sw.longitude) {
        lon = self.mapManager.mapBounds.sw.longitude;
    } else if (camera.centerCoordinate.longitude > self.mapManager.mapBounds.ne.longitude) {
        lon = self.mapManager.mapBounds.ne.longitude;
    }
    
    camera.centerCoordinate = CLLocationCoordinate2DMake(lat, lon);
    [mapView setCamera:camera animated:NO];
    return NO;
}

@1ec5
Copy link
Contributor

1ec5 commented Jun 27, 2017

I wonder if the glitchy/jumpy behavior is due to #9344.

@epau
Copy link
Author

epau commented Jun 28, 2017

Will test with ios-v3.6.0-rc.1 and report back this week

@epau
Copy link
Author

epau commented Jul 10, 2017

@1ec5 still seeing the same issues with 3.6

Would it be possible for MapView to have a MGLCoordinateInCoordinateBounds property that if set, the MapView would bound all viewport changes to?

@1ec5
Copy link
Contributor

1ec5 commented Jul 10, 2017

still seeing the same issues with 3.6

Could you post a screen recording showing the behavior you’re seeing? I tried the code above on both iOS and macOS (which has a slightly different implementation that might be more robust), but it didn’t look jumpy to me, so I’m probably missing something.

Would it be possible for MapView to have a MGLCoordinateInCoordinateBounds property that if set, the MapView would bound all viewport changes to?

Given the existing delegate method, it would be unfortunate to expose a redundant property like the one proposed in #2341. However, I realize that the current workflow is tedious for the simple case of restricting to a particular bounding box. (#8499 would improve the ergonomics around this delegate method.) Hopefully we can make this API work as intended and improve the ergonomics so it feels as natural as a simple property would be.

@epau
Copy link
Author

epau commented Jul 31, 2017

Sorry for the long update, but i think we can close this issue.

I am able to get what i want by clamping the MGLMapView to some bounds within the mapViewRegionIsChanging method. Thanks for looking into this and helping me get to this point.

@erikvdwal
Copy link

erikvdwal commented Jul 31, 2017

@epau Would you mind sharing your solution? I tried a similar approach, but it resulted in some undesired zooming behavior when hitting the outer bounds. Perhaps my approach was too naive, though. I've put up an example project.

At this point, my guess is that the setVisibleCoordinateBounds(animated:) method slightly alters the bounds it's been given, which results in the calculated difference not being accurate, which in turn causes this behavior. I'm interested to see how your solution is different and if it maybe works around this issue.

@epau
Copy link
Author

epau commented Jul 31, 2017

@erikvdwal my solution looks almost identical to yours. Here's my clamping logc fwiw.

Could you explain the "undesired zooming behavior"?

@erikvdwal
Copy link

erikvdwal commented Jul 31, 2017

@epau Thanks!

You should be able to reproduce the behavior I was describing if you run that example project. Basically, If you aren't zoomed out entirely and you pan towards - and hit - one of the edges of the bounding box, the map will start zooming out until you hit the minimum zoom level.

Edit: here's a gif

@epau
Copy link
Author

epau commented Sep 6, 2018

We're still struggling with a solution here.

Is there a way we can properly set a bounding box to restrict the camera to?
Using the delegate simply doesn't cut it since the camera doesn't change where as we want it to move to bounds but not go past it.

@1ec5
Copy link
Contributor

1ec5 commented Sep 7, 2018

Would you mind sharing your solution? I tried a similar approach, but it resulted in some undesired zooming behavior when hitting the outer bounds. Perhaps my approach was too naive, though. I've put up an example project.

@erikvdwal, that project uses mapViewRegionIsChanging(_:), whereas the suggestion above is to use mapView(_:shouldChangeFrom:to:). Calling setVisibleCoordinateBounds(_:) from within mapViewRegionIsChanging(_:) is bound to start a feedback loop that’s only broken by hitting the minimum zoom level. This call to MGLCoordinateBoundsEqualToCoordinateBounds() attempts to stop the feedback loop as well, but visibleCoordinateBounds does differ from adjusted because the distance between two latitudes is not uniform. This difference corresponds to one screen distance when applied to the northeast coordinate and another screen distance when applied to the southwest coordinate.

I would recommend an implementation of mapView(_:shouldChangeFrom:to:) that steers clear of the visibleCoordinateBounds property. I haven’t tried the code below, but hopefully it serves as a useful starting point:

let clampedCamera = newCamera
// Lots of assumptions; the rest is left as an exercise to the reader.
if oldCamera.altitude == newCamera.altitude && oldCamera.heading == newCamera.heading && newCamera.heading = 0 && oldCamera.pitch == newCamera.pitch && newCamera.pitch == 0 {
    // If panning upward beyond the maximum coordinate bounds…
    if newCamera.ne.latitude > maximumCoordinateBounds.ne.latitude {
        // Where would the northernmost allowed coordinate be on screen before changing the camera?
        let clampedTopRight = mapView.convert(maximumCoordinateBounds.ne, toPointTo: mapView)
        // The map view is the same size regardless of latitude, so offset the top-right point to find the corresponding center point.
        let clampedCenter = CGPoint(x: clampedTopRight.x, y: clampedTopRight.y + mapView.bounds.midY)
        // What coordinate would that center point correspond to before changing the camera?
        clampedCamera.center = mapView.convert(clampedCenter, toCoordinateFrom: mapView)
    }
}
// …

Clamping the visible coordinate bounds on a rotated or tilted map would be a particular challenge. There may be an opportunity for Turf to provide helper methods to facilitate these calculations. Not sure if any Turf.js methods can be ported over for that.

@stale stale bot added the archived Archived because of inactivity label Mar 6, 2019
@stale
Copy link

stale bot commented Mar 6, 2019

This issue has been automatically detected as stale because it has not had recent activity and will be archived. Thank you for your contributions.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
archived Archived because of inactivity feature iOS Mapbox Maps SDK for iOS
Projects
None yet
Development

No branches or pull requests

4 participants