From 272dc3f6a91ee1f0734c6642d610366f4396ec93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguy=E1=BB=85n?= Date: Thu, 9 Feb 2017 13:49:04 -0800 Subject: Delegate method to restrict movement (#5584) * [ios, macos] Added delegate method to restrict movement Added a way for the delegate to restrict where the user can move within the map using gestures. Fixes #2457. * [ios] Added support to restrict movement in pinch/rotate gestures * [ios] Added support to restrict movement in double tap/quick zoom/two finger drag gestures * [ios] fixed camera reset before two finger drag gesture is complete * [ios] fixed camera comparison in double tap gestures * [ios] Changelog update * [macos] Changelog updated * [ios, macos] Changelog cleanup * [ios, macos] Added documentation to clarify performance impact * [ios] clarified variable name * [ios] blocking gestures implementation changed to a predictive approach * [ios] gesture delegate methods refactoring * [ios] Removed duplicated methods, improved code readability * [ios] code refactoring to clarify the conditions to execute a gesture --- platform/ios/CHANGELOG.md | 1 + platform/ios/src/MGLMapView.mm | 244 +++++++++++++++++++++++++------- platform/ios/src/MGLMapViewDelegate.h | 22 +++ platform/macos/CHANGELOG.md | 1 + platform/macos/src/MGLMapView.mm | 32 +++++ platform/macos/src/MGLMapViewDelegate.h | 22 +++ 6 files changed, 270 insertions(+), 52 deletions(-) diff --git a/platform/ios/CHANGELOG.md b/platform/ios/CHANGELOG.md index 53a0538506..5b4a81c6e3 100644 --- a/platform/ios/CHANGELOG.md +++ b/platform/ios/CHANGELOG.md @@ -37,6 +37,7 @@ Mapbox welcomes participation and contributions from everyone. Please read [CONT * Fixed an issue where translucent, non-view-backed point annotations along tile boundaries would be drawn darker than expected. ([#6832](https://github.com/mapbox/mapbox-gl-native/pull/6832)) * Fixed flickering that occurred when panning past the antimeridian. ([#7574](https://github.com/mapbox/mapbox-gl-native/pull/7574)) * Added a `MGLDistanceFormatter` class for formatting geographic distances. ([#7888](https://github.com/mapbox/mapbox-gl-native/pull/7888)) +* Added a method to MGLMapViewDelegate, `-mapView:shouldChangeFromCamera:toCamera:`, that you can implement to restrict which parts the user can navigate to using gestures. ([#5584](https://github.com/mapbox/mapbox-gl-native/pull/5584)) ## 3.4.1 - January 25, 2017 diff --git a/platform/ios/src/MGLMapView.mm b/platform/ios/src/MGLMapView.mm index 0c438d5606..cc86ed1e53 100644 --- a/platform/ios/src/MGLMapView.mm +++ b/platform/ios/src/MGLMapView.mm @@ -1205,6 +1205,8 @@ public: _mbglMap->cancelTransitions(); + MGLMapCamera *oldCamera = self.camera; + if (pan.state == UIGestureRecognizerStateBegan) { [self trackGestureEvent:MGLEventGesturePanStart forRecognizer:pan]; @@ -1216,8 +1218,15 @@ public: else if (pan.state == UIGestureRecognizerStateChanged) { CGPoint delta = [pan translationInView:pan.view]; - _mbglMap->moveBy({ delta.x, delta.y }); - [pan setTranslation:CGPointZero inView: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]; + } [self notifyMapChange:mbgl::MapChangeRegionIsChanging]; } @@ -1234,7 +1243,13 @@ public: if (drift) { CGPoint offset = CGPointMake(velocity.x * self.decelerationRate / 4, velocity.y * self.decelerationRate / 4); - _mbglMap->moveBy({ offset.x, offset.y }, MGLDurationInSecondsFromTimeInterval(self.decelerationRate)); + MGLMapCamera *toCamera = [self cameraByPanningWithTranslation:offset panGesture:pan]; + + if (![self.delegate respondsToSelector:@selector(mapView:shouldChangeFromCamera:toCamera:)] || + [self.delegate mapView:self shouldChangeFromCamera:oldCamera toCamera:toCamera]) + { + _mbglMap->moveBy({ offset.x, offset.y }, MGLDurationInSecondsFromTimeInterval(self.decelerationRate)); + } } [self notifyGestureDidEndWithDrift:drift]; @@ -1250,6 +1265,7 @@ public: MGLEventKeyZoomLevel: @(zoom) }]; } + } - (void)handlePinchGesture:(UIPinchGestureRecognizer *)pinch @@ -1261,6 +1277,7 @@ public: _mbglMap->cancelTransitions(); CGPoint centerPoint = [self anchorPointForGesture:pinch]; + MGLMapCamera *oldCamera = self.camera; if (pinch.state == UIGestureRecognizerStateBegan) { @@ -1273,11 +1290,17 @@ public: else if (pinch.state == UIGestureRecognizerStateChanged) { CGFloat newScale = self.scale * pinch.scale; - - if (log2(newScale) < _mbglMap->getMinZoom()) return; - - _mbglMap->setScale(newScale, mbgl::ScreenCoordinate { centerPoint.x, centerPoint.y }); - + double zoom = log2(newScale); + if (zoom < _mbglMap->getMinZoom()) return; + + // Calculates the final camera zoom, has no effect within current map camera. + MGLMapCamera *toCamera = [self cameraByZoomingToZoomLevel:zoom aroundAnchorPoint:centerPoint]; + + if (![self.delegate respondsToSelector:@selector(mapView:shouldChangeFromCamera:toCamera:)] || + [self.delegate mapView:self shouldChangeFromCamera:oldCamera toCamera:toCamera]) + { + _mbglMap->setScale(newScale, mbgl::ScreenCoordinate { centerPoint.x, centerPoint.y }); + } // The gesture recognizer only reports the gesture’s current center // point, so use the previous center point to anchor the transition. // If the number of touches has changed, the remembered center point is @@ -1288,7 +1311,6 @@ public: _mbglMap->setLatLng(MGLLatLngFromLocationCoordinate2D(centerCoordinate), mbgl::ScreenCoordinate { centerPoint.x, centerPoint.y }); } - [self notifyMapChange:mbgl::MapChangeRegionIsChanging]; } else if (pinch.state == UIGestureRecognizerStateEnded || pinch.state == UIGestureRecognizerStateCancelled) @@ -1321,14 +1343,25 @@ public: { velocity = 0; } - - if (velocity && duration) + + BOOL drift = velocity && duration; + + // Calculates the final camera zoom, this has no effect within current map camera. + double zoom = log2(newScale); + MGLMapCamera *toCamera = [self cameraByZoomingToZoomLevel:zoom aroundAnchorPoint:centerPoint]; + + if ([self.delegate respondsToSelector:@selector(mapView:shouldChangeFromCamera:toCamera:)] + && ![self.delegate mapView:self shouldChangeFromCamera:oldCamera toCamera:toCamera]) { - _mbglMap->setScale(newScale, mbgl::ScreenCoordinate { centerPoint.x, centerPoint.y }, MGLDurationInSecondsFromTimeInterval(duration)); + drift = NO; + } else { + if (drift) + { + _mbglMap->setScale(newScale, mbgl::ScreenCoordinate { centerPoint.x, centerPoint.y }, MGLDurationInSecondsFromTimeInterval(duration)); + } } - [self notifyGestureDidEndWithDrift:velocity && duration]; - + [self notifyGestureDidEndWithDrift:drift]; [self unrotateIfNeededForGesture]; } @@ -1343,7 +1376,8 @@ public: _mbglMap->cancelTransitions(); CGPoint centerPoint = [self anchorPointForGesture:rotate]; - + MGLMapCamera *oldCamera = self.camera; + if (rotate.state == UIGestureRecognizerStateBegan) { [self trackGestureEvent:MGLEventGestureRotateStart forRecognizer:rotate]; @@ -1368,10 +1402,17 @@ public: newDegrees = fminf(newDegrees, 30); newDegrees = fmaxf(newDegrees, -30); } - - _mbglMap->setBearing(newDegrees, mbgl::ScreenCoordinate { centerPoint.x, centerPoint.y }); - + + MGLMapCamera *toCamera = [self cameraByRotatingToDirection:newDegrees aroundAnchorPoint:centerPoint]; + + if (![self.delegate respondsToSelector:@selector(mapView:shouldChangeFromCamera:toCamera:)] || + [self.delegate mapView:self shouldChangeFromCamera:oldCamera toCamera:toCamera]) + { + _mbglMap->setBearing(newDegrees, mbgl::ScreenCoordinate { centerPoint.x, centerPoint.y }); + } + [self notifyMapChange:mbgl::MapChangeRegionIsChanging]; + } else if (rotate.state == UIGestureRecognizerStateEnded || rotate.state == UIGestureRecognizerStateCancelled) { @@ -1383,16 +1424,22 @@ public: CGFloat newRadians = radians + velocity * decelerationRate * 0.1; CGFloat newDegrees = MGLDegreesFromRadians(newRadians) * -1; - _mbglMap->setBearing(newDegrees, mbgl::ScreenCoordinate { centerPoint.x, centerPoint.y }, MGLDurationInSecondsFromTimeInterval(decelerationRate)); - - [self notifyGestureDidEndWithDrift:YES]; - - __weak MGLMapView *weakSelf = self; - - [self animateWithDelay:decelerationRate animations:^ - { - [weakSelf unrotateIfNeededForGesture]; - }]; + MGLMapCamera *toCamera = [self cameraByRotatingToDirection:newDegrees aroundAnchorPoint:centerPoint]; + + if (![self.delegate respondsToSelector:@selector(mapView:shouldChangeFromCamera:toCamera:)] || + [self.delegate mapView:self shouldChangeFromCamera:oldCamera toCamera:toCamera]) + { + _mbglMap->setBearing(newDegrees, mbgl::ScreenCoordinate { centerPoint.x, centerPoint.y }, MGLDurationInSecondsFromTimeInterval(decelerationRate)); + + [self notifyGestureDidEndWithDrift:YES]; + + __weak MGLMapView *weakSelf = self; + + [self animateWithDelay:decelerationRate animations:^ + { + [weakSelf unrotateIfNeededForGesture]; + }]; + } } else { @@ -1508,18 +1555,27 @@ public: if (doubleTap.state == UIGestureRecognizerStateEnded) { - [self trackGestureEvent:MGLEventGestureDoubleTap forRecognizer:doubleTap]; + MGLMapCamera *oldCamera = self.camera; + CGPoint gesturePoint = [self anchorPointForGesture:doubleTap]; - - mbgl::ScreenCoordinate center(gesturePoint.x, gesturePoint.y); - _mbglMap->scaleBy(2, center, MGLDurationInSecondsFromTimeInterval(MGLAnimationDuration)); - - __weak MGLMapView *weakSelf = self; - - [self animateWithDelay:MGLAnimationDuration animations:^ + + MGLMapCamera *toCamera = [self cameraByZoomingToZoomLevel:self.zoomLevel + 1.0 aroundAnchorPoint:gesturePoint]; + + if (![self.delegate respondsToSelector:@selector(mapView:shouldChangeFromCamera:toCamera:)] || + [self.delegate mapView:self shouldChangeFromCamera:oldCamera toCamera:toCamera]) { - [weakSelf unrotateIfNeededForGesture]; - }]; + [self trackGestureEvent:MGLEventGestureDoubleTap forRecognizer:doubleTap]; + + mbgl::ScreenCoordinate center(gesturePoint.x, gesturePoint.y); + _mbglMap->scaleBy(2, center, MGLDurationInSecondsFromTimeInterval(MGLAnimationDuration)); + + __weak MGLMapView *weakSelf = self; + + [self animateWithDelay:MGLAnimationDuration animations:^ + { + [weakSelf unrotateIfNeededForGesture]; + }]; + } } } @@ -1537,17 +1593,26 @@ public: } else if (twoFingerTap.state == UIGestureRecognizerStateEnded) { - CGPoint gesturePoint = [self anchorPointForGesture:twoFingerTap]; + MGLMapCamera *oldCamera = self.camera; - mbgl::ScreenCoordinate center(gesturePoint.x, gesturePoint.y); - _mbglMap->scaleBy(0.5, center, MGLDurationInSecondsFromTimeInterval(MGLAnimationDuration)); - - __weak MGLMapView *weakSelf = self; - - [self animateWithDelay:MGLAnimationDuration animations:^ + double zoom = self.zoomLevel; + CGPoint gesturePoint = [self anchorPointForGesture:twoFingerTap]; + + MGLMapCamera *toCamera = [self cameraByZoomingToZoomLevel:zoom - 1.0 aroundAnchorPoint:gesturePoint]; + + if (![self.delegate respondsToSelector:@selector(mapView:shouldChangeFromCamera:toCamera:)] || + [self.delegate mapView:self shouldChangeFromCamera:oldCamera toCamera:toCamera]) { - [weakSelf unrotateIfNeededForGesture]; - }]; + mbgl::ScreenCoordinate center(gesturePoint.x, gesturePoint.y); + _mbglMap->scaleBy(0.5, center, MGLDurationInSecondsFromTimeInterval(MGLAnimationDuration)); + + __weak MGLMapView *weakSelf = self; + + [self animateWithDelay:MGLAnimationDuration animations:^ + { + [weakSelf unrotateIfNeededForGesture]; + }]; + } } } @@ -1556,7 +1621,7 @@ public: if ( ! self.isZoomEnabled) return; _mbglMap->cancelTransitions(); - + if (quickZoom.state == UIGestureRecognizerStateBegan) { [self trackGestureEvent:MGLEventGestureQuickZoom forRecognizer:quickZoom]; @@ -1576,9 +1641,21 @@ public: if (newZoom < _mbglMap->getMinZoom()) return; CGPoint centerPoint = [self anchorPointForGesture:quickZoom]; - - _mbglMap->scaleBy(powf(2, newZoom) / _mbglMap->getScale(), - mbgl::ScreenCoordinate { centerPoint.x, centerPoint.y }); + + MGLMapCamera *oldCamera = self.camera; + + double zoom = self.zoomLevel; + double scale = powf(2, newZoom) / _mbglMap->getScale(); + + double estimatedZoom = zoom * scale; + + MGLMapCamera *toCamera = [self cameraByZoomingToZoomLevel:estimatedZoom aroundAnchorPoint:centerPoint]; + + if (![self.delegate respondsToSelector:@selector(mapView:shouldChangeFromCamera:toCamera:)] || + [self.delegate mapView:self shouldChangeFromCamera:oldCamera toCamera:toCamera]) + { + _mbglMap->scaleBy(scale, mbgl::ScreenCoordinate { centerPoint.x, centerPoint.y }); + } [self notifyMapChange:mbgl::MapChangeRegionIsChanging]; } @@ -1594,6 +1671,7 @@ public: if ( ! self.isPitchEnabled) return; _mbglMap->cancelTransitions(); + MGLMapCamera *oldCamera = self.camera; if (twoFingerDrag.state == UIGestureRecognizerStateBegan) { @@ -1610,7 +1688,13 @@ public: CGPoint centerPoint = [self anchorPointForGesture:twoFingerDrag]; - _mbglMap->setPitch(pitchNew, mbgl::ScreenCoordinate { centerPoint.x, centerPoint.y }); + MGLMapCamera *toCamera = [self cameraByTiltingToPitch:pitchNew]; + + if (![self.delegate respondsToSelector:@selector(mapView:shouldChangeFromCamera:toCamera:)] || + [self.delegate mapView:self shouldChangeFromCamera:oldCamera toCamera:toCamera]) + { + _mbglMap->setPitch(pitchNew, mbgl::ScreenCoordinate { centerPoint.x, centerPoint.y }); + } [self notifyMapChange:mbgl::MapChangeRegionIsChanging]; } @@ -1619,6 +1703,62 @@ public: [self notifyGestureDidEndWithDrift:NO]; [self unrotateIfNeededForGesture]; } + +} + +- (MGLMapCamera *)cameraByPanningWithTranslation:(CGPoint)endPoint panGesture:(UIPanGestureRecognizer *)pan +{ + MGLMapCamera *panCamera = [self.camera copy]; + + CGPoint centerPoint = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds)); + CGPoint endCameraPoint = CGPointMake(centerPoint.x - endPoint.x, centerPoint.y - endPoint.y); + CLLocationCoordinate2D panCoordinate = [self convertPoint:endCameraPoint toCoordinateFromView:pan.view]; + + panCamera.centerCoordinate = panCoordinate; + + return panCamera; +} + +- (MGLMapCamera *)cameraByZoomingToZoomLevel:(double)zoom aroundAnchorPoint:(CGPoint)anchorPoint +{ + mbgl::EdgeInsets padding = MGLEdgeInsetsFromNSEdgeInsets(self.contentInset); + mbgl::CameraOptions currentCameraOptions = _mbglMap->getCameraOptions(padding); + MGLMapCamera *camera; + + mbgl::ScreenCoordinate anchor = mbgl::ScreenCoordinate { anchorPoint.x, anchorPoint.y }; + currentCameraOptions.zoom = mbgl::util::clamp(zoom, self.minimumZoomLevel, self.maximumZoomLevel); + currentCameraOptions.anchor = anchor; + camera = [self cameraForCameraOptions:currentCameraOptions]; + + return camera; +} + +- (MGLMapCamera *)cameraByRotatingToDirection:(CLLocationDirection)degrees aroundAnchorPoint:(CGPoint)anchorPoint +{ + mbgl::EdgeInsets padding = MGLEdgeInsetsFromNSEdgeInsets(self.contentInset); + mbgl::CameraOptions currentCameraOptions = _mbglMap->getCameraOptions(padding); + + MGLMapCamera *camera; + + mbgl::ScreenCoordinate anchor = mbgl::ScreenCoordinate { anchorPoint.x, anchorPoint.y }; + currentCameraOptions.angle = degrees * mbgl::util::DEG2RAD; + currentCameraOptions.anchor = anchor; + camera = [self cameraForCameraOptions:currentCameraOptions]; + + return camera; +} + +- (MGLMapCamera *)cameraByTiltingToPitch:(CGFloat)pitch +{ + mbgl::EdgeInsets padding = MGLEdgeInsetsFromNSEdgeInsets(self.contentInset); + mbgl::CameraOptions currentCameraOptions = _mbglMap->getCameraOptions(padding); + + MGLMapCamera *camera; + + currentCameraOptions.pitch = pitch * mbgl::util::DEG2RAD; + camera = [self cameraForCameraOptions:currentCameraOptions]; + + return camera; } - (CGPoint)anchorPointForGesture:(UIGestureRecognizer *)gesture { diff --git a/platform/ios/src/MGLMapViewDelegate.h b/platform/ios/src/MGLMapViewDelegate.h index 8a57e15f7a..12320a63a5 100644 --- a/platform/ios/src/MGLMapViewDelegate.h +++ b/platform/ios/src/MGLMapViewDelegate.h @@ -59,6 +59,28 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)mapView:(MGLMapView *)mapView regionDidChangeAnimated:(BOOL)animated; +/** + Asks the delegate whether the map view should be allowed to change from the + existing camera to the new camera in response to a user gesture. + + This method is called as soon as the user gesture is recognized. It is not + called in response to a programmatic camera change, such as by setting the + `centerCoordinate` property or calling `-flyToCamera:completionHandler:`. + + This method is called many times during gesturing, so you should avoid performing + complex or performance-intensive tasks in your implementation. + + @param mapView The map view that the user is manipulating. + @param oldCamera The camera representing the viewpoint at the moment the + gesture is recognized. If this method returns `NO`, the map view’s camera + continues to be this camera. + @param newCamera The expected camera after the gesture completes. If this + method returns `YES`, this camera becomes the map view’s camera. + @return A Boolean value indicating whether the map view should stay at + `oldCamera` or change to `newCamera`. + */ +- (BOOL)mapView:(MGLMapView *)mapView shouldChangeFromCamera:(MGLMapCamera *)oldCamera toCamera:(MGLMapCamera *)newCamera; + #pragma mark Loading the Map /** diff --git a/platform/macos/CHANGELOG.md b/platform/macos/CHANGELOG.md index eb06c36e36..dc2c93a64a 100644 --- a/platform/macos/CHANGELOG.md +++ b/platform/macos/CHANGELOG.md @@ -34,6 +34,7 @@ * Fixed an issue where translucent point annotations along tile boundaries would be drawn darker than expected. ([#6832](https://github.com/mapbox/mapbox-gl-native/pull/6832)) * Fixed flickering that occurred when panning past the antimeridian. ([#7574](https://github.com/mapbox/mapbox-gl-native/pull/7574)) +* Added a method to MGLMapViewDelegate, `-mapView:shouldChangeFromCamera:toCamera:`, that you can implement to restrict which parts the user can navigate to using gestures. ([#5584](https://github.com/mapbox/mapbox-gl-native/pull/5584)) * Added a `MGLDistanceFormatter` class for formatting geographic distances. ([#7888](https://github.com/mapbox/mapbox-gl-native/pull/7888)) ## 0.3.1 diff --git a/platform/macos/src/MGLMapView.mm b/platform/macos/src/MGLMapView.mm index d687a19aed..3d5f71feb1 100644 --- a/platform/macos/src/MGLMapView.mm +++ b/platform/macos/src/MGLMapView.mm @@ -995,8 +995,13 @@ public: - (void)offsetCenterCoordinateBy:(NSPoint)delta animated:(BOOL)animated { [self willChangeValueForKey:@"centerCoordinate"]; _mbglMap->cancelTransitions(); + MGLMapCamera *oldCamera = self.camera; _mbglMap->moveBy({ delta.x, delta.y }, MGLDurationInSecondsFromTimeInterval(animated ? MGLAnimationDuration : 0)); + if ([self.delegate respondsToSelector:@selector(mapView:shouldChangeFromCamera:toCamera:)] + && ![self.delegate mapView:self shouldChangeFromCamera:oldCamera toCamera:self.camera]) { + self.camera = oldCamera; + } [self didChangeValueForKey:@"centerCoordinate"]; } @@ -1043,8 +1048,13 @@ public: - (void)scaleBy:(double)scaleFactor atPoint:(NSPoint)point animated:(BOOL)animated { [self willChangeValueForKey:@"centerCoordinate"]; [self willChangeValueForKey:@"zoomLevel"]; + MGLMapCamera *oldCamera = self.camera; mbgl::ScreenCoordinate center(point.x, self.bounds.size.height - point.y); _mbglMap->scaleBy(scaleFactor, center, MGLDurationInSecondsFromTimeInterval(animated ? MGLAnimationDuration : 0)); + if ([self.delegate respondsToSelector:@selector(mapView:shouldChangeFromCamera:toCamera:)] + && ![self.delegate mapView:self shouldChangeFromCamera:oldCamera toCamera:self.camera]) { + self.camera = oldCamera; + } [self didChangeValueForKey:@"zoomLevel"]; [self didChangeValueForKey:@"centerCoordinate"]; } @@ -1379,15 +1389,25 @@ public: _directionAtBeginningOfGesture = self.direction; _pitchAtBeginningOfGesture = _mbglMap->getPitch(); } else if (gestureRecognizer.state == NSGestureRecognizerStateChanged) { + MGLMapCamera *oldCamera = self.camera; + BOOL didChangeCamera = NO; mbgl::ScreenCoordinate center(startPoint.x, self.bounds.size.height - startPoint.y); if (self.rotateEnabled) { CLLocationDirection newDirection = _directionAtBeginningOfGesture - delta.x / 10; [self willChangeValueForKey:@"direction"]; _mbglMap->setBearing(newDirection, center); + didChangeCamera = YES; [self didChangeValueForKey:@"direction"]; } if (self.pitchEnabled) { _mbglMap->setPitch(_pitchAtBeginningOfGesture + delta.y / 5, center); + didChangeCamera = YES; + } + + if (didChangeCamera + && [self.delegate respondsToSelector:@selector(mapView:shouldChangeFromCamera:toCamera:)] + && ![self.delegate mapView:self shouldChangeFromCamera:oldCamera toCamera:self.camera]) { + self.camera = oldCamera; } } } else if (self.scrollEnabled) { @@ -1427,7 +1447,12 @@ public: if (gestureRecognizer.magnification > -1) { [self willChangeValueForKey:@"zoomLevel"]; [self willChangeValueForKey:@"centerCoordinate"]; + MGLMapCamera *oldCamera = self.camera; _mbglMap->setScale(_scaleAtBeginningOfGesture * (1 + gestureRecognizer.magnification), center); + if ([self.delegate respondsToSelector:@selector(mapView:shouldChangeFromCamera:toCamera:)] + && ![self.delegate mapView:self shouldChangeFromCamera:oldCamera toCamera:self.camera]) { + self.camera = oldCamera; + } [self didChangeValueForKey:@"centerCoordinate"]; [self didChangeValueForKey:@"zoomLevel"]; } @@ -1502,9 +1527,16 @@ public: _mbglMap->setGestureInProgress(true); _directionAtBeginningOfGesture = self.direction; } else if (gestureRecognizer.state == NSGestureRecognizerStateChanged) { + MGLMapCamera *oldCamera = self.camera; + NSPoint rotationPoint = [gestureRecognizer locationInView:self]; mbgl::ScreenCoordinate center(rotationPoint.x, self.bounds.size.height - rotationPoint.y); _mbglMap->setBearing(_directionAtBeginningOfGesture + gestureRecognizer.rotationInDegrees, center); + + if ([self.delegate respondsToSelector:@selector(mapView:shouldChangeFromCamera:toCamera:)] + && ![self.delegate mapView:self shouldChangeFromCamera:oldCamera toCamera:self.camera]) { + self.camera = oldCamera; + } } else if (gestureRecognizer.state == NSGestureRecognizerStateEnded || gestureRecognizer.state == NSGestureRecognizerStateCancelled) { _mbglMap->setGestureInProgress(false); diff --git a/platform/macos/src/MGLMapViewDelegate.h b/platform/macos/src/MGLMapViewDelegate.h index 534e28e3a8..08a9f7eff4 100644 --- a/platform/macos/src/MGLMapViewDelegate.h +++ b/platform/macos/src/MGLMapViewDelegate.h @@ -61,6 +61,28 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)mapView:(MGLMapView *)mapView cameraDidChangeAnimated:(BOOL)animated; +/** + Asks the delegate whether the map view should be allowed to change from the + existing camera to the new camera in response to a user gesture. + + This method is called as soon as the user gesture is recognized. It is not + called in response to a programmatic camera change, such as by setting the + `centerCoordinate` property or calling `-flyToCamera:completionHandler:`. + + This method is called many times during gesturing, so you should avoid performing + complex or performance-intensive tasks in your implementation. + + @param mapView The map view that the user is manipulating. + @param oldCamera The camera representing the viewpoint at the moment the + gesture is recognized. If this method returns `NO`, the map view’s camera + continues to be this camera. + @param newCamera The expected camera after the gesture completes. If this + method returns `YES`, this camera becomes the map view’s camera. + @return A Boolean value indicating whether the map view should stay at + `oldCamera` or change to `newCamera`. + */ +- (BOOL)mapView:(MGLMapView *)mapView shouldChangeFromCamera:(MGLMapCamera *)oldCamera toCamera:(MGLMapCamera *)newCamera; + #pragma mark Loading the Map /** -- cgit v1.2.1