diff options
Diffstat (limited to 'platform/ios/src')
-rw-r--r-- | platform/ios/src/MGLMapView+Impl.h | 2 | ||||
-rw-r--r-- | platform/ios/src/MGLMapView+Impl.mm | 2 | ||||
-rw-r--r-- | platform/ios/src/MGLMapView.h | 24 | ||||
-rw-r--r-- | platform/ios/src/MGLMapView.mm | 389 | ||||
-rw-r--r-- | platform/ios/src/MGLMapView_Private.h | 2 | ||||
-rw-r--r-- | platform/ios/src/MGLMapboxEvents.h | 3 | ||||
-rw-r--r-- | platform/ios/src/MGLMapboxEvents.m | 66 | ||||
-rw-r--r-- | platform/ios/src/MGLScaleBar.mm | 15 |
8 files changed, 379 insertions, 124 deletions
diff --git a/platform/ios/src/MGLMapView+Impl.h b/platform/ios/src/MGLMapView+Impl.h index 0a62b7da82..66dc408274 100644 --- a/platform/ios/src/MGLMapView+Impl.h +++ b/platform/ios/src/MGLMapView+Impl.h @@ -62,7 +62,7 @@ public: void onDidFinishLoadingMap() override; void onDidFailLoadingMap(mbgl::MapLoadError mapError, const std::string& what) override; void onWillStartRenderingFrame() override; - void onDidFinishRenderingFrame(mbgl::MapObserver::RenderMode) override; + void onDidFinishRenderingFrame(mbgl::MapObserver::RenderMode, bool) override; void onWillStartRenderingMap() override; void onDidFinishRenderingMap(mbgl::MapObserver::RenderMode) override; void onDidFinishLoadingStyle() override; diff --git a/platform/ios/src/MGLMapView+Impl.mm b/platform/ios/src/MGLMapView+Impl.mm index 1bccfa662f..76c9c0f9ba 100644 --- a/platform/ios/src/MGLMapView+Impl.mm +++ b/platform/ios/src/MGLMapView+Impl.mm @@ -68,7 +68,7 @@ void MGLMapViewImpl::onWillStartRenderingFrame() { [mapView mapViewWillStartRenderingFrame]; } -void MGLMapViewImpl::onDidFinishRenderingFrame(mbgl::MapObserver::RenderMode mode) { +void MGLMapViewImpl::onDidFinishRenderingFrame(mbgl::MapObserver::RenderMode mode, bool) { bool fullyRendered = mode == mbgl::MapObserver::RenderMode::Full; [mapView mapViewDidFinishRenderingFrameFullyRendered:fullyRendered]; } diff --git a/platform/ios/src/MGLMapView.h b/platform/ios/src/MGLMapView.h index e6f66e39ae..6d9b30e467 100644 --- a/platform/ios/src/MGLMapView.h +++ b/platform/ios/src/MGLMapView.h @@ -748,7 +748,8 @@ MGL_EXPORT Changes the center coordinate of the map and optionally animates the change. Changing the center coordinate centers the map on the new coordinate without - changing the current zoom level. + changing the current zoom level. For animated changes, wait until the map view has + finished loading before calling this method. @param coordinate The new center coordinate for the map. @param animated Specify `YES` if you want the map view to scroll to the new @@ -762,7 +763,8 @@ MGL_EXPORT /** Changes the center coordinate and zoom level of the map and optionally animates - the change. + the change. For animated changes, wait until the map view has + finished loading before calling this method. @param centerCoordinate The new center coordinate for the map. @param zoomLevel The new zoom level for the map. @@ -777,7 +779,8 @@ MGL_EXPORT /** Changes the center coordinate, zoom level, and direction of the map and - optionally animates the change. + optionally animates the change. For animated changes, wait until the map view has + finished loading before calling this method. @param centerCoordinate The new center coordinate for the map. @param zoomLevel The new zoom level for the map. @@ -794,7 +797,8 @@ MGL_EXPORT /** Changes the center coordinate, zoom level, and direction of the map, calling a - completion handler at the end of an optional animation. + completion handler at the end of an optional animation. For animated changes, + wait until the map view has finished loading before calling this method. @param centerCoordinate The new center coordinate for the map. @param zoomLevel The new zoom level for the map. @@ -1065,7 +1069,8 @@ MGL_EXPORT /** Moves the viewpoint to a different location with respect to the map with an - optional transition animation. + optional transition animation. For animated changes, wait until the map view has + finished loading before calling this method. @param camera The new viewpoint. @param animated Specify `YES` if you want the map view to animate the change to @@ -1081,7 +1086,8 @@ MGL_EXPORT /** Moves the viewpoint to a different location with respect to the map with an - optional transition duration and timing function. + optional transition duration and timing function. For animated changes, wait + until the map view has finished loading before calling this method. @param camera The new viewpoint. @param duration The amount of time, measured in seconds, that the transition @@ -1100,7 +1106,8 @@ MGL_EXPORT /** Moves the viewpoint to a different location with respect to the map with an - optional transition duration and timing function. + optional transition duration and timing function. For animated changes, wait + until the map view has finished loading before calling this method. @param camera The new viewpoint. @param duration The amount of time, measured in seconds, that the transition @@ -1116,7 +1123,8 @@ MGL_EXPORT /** Moves the viewpoint to a different location with respect to the map with an optional transition duration and timing function, and optionally some additional - padding on each side. + padding on each side. For animated changes, wait until the map view has + finished loading before calling this method. @param camera The new viewpoint. @param duration The amount of time, measured in seconds, that the transition diff --git a/platform/ios/src/MGLMapView.mm b/platform/ios/src/MGLMapView.mm index d77f94d8ba..2eafea1dd9 100644 --- a/platform/ios/src/MGLMapView.mm +++ b/platform/ios/src/MGLMapView.mm @@ -128,6 +128,9 @@ const CLLocationDirection MGLToleranceForSnappingToNorth = 7; /// Distance threshold to stop the camera while animating. const CLLocationDistance MGLDistanceThresholdForCameraPause = 500; +/// Rotation threshold while a pinch gesture is occurring. +static NSString * const MGLRotationThresholdWhileZoomingKey = @"MGLRotationThresholdWhileZooming"; + /// Reuse identifier and file name of the default point annotation image. static NSString * const MGLDefaultStyleMarkerSymbolName = @"default_marker"; @@ -154,6 +157,9 @@ static const NSUInteger MGLPresentsWithTransactionAnnotationCount = 0; /// An indication that the requested annotation was not found or is nonexistent. enum { MGLAnnotationTagNotFound = UINT32_MAX }; +/// The threshold used to consider when a tilt gesture should start. +const CLLocationDegrees MGLHorizontalTiltToleranceDegrees = 45.0; + /// Mapping from an annotation tag to metadata about that annotation, including /// the annotation itself. typedef std::unordered_map<MGLAnnotationTag, MGLAnnotationContext> MGLAnnotationTagContextMap; @@ -240,6 +246,10 @@ public: @property (nonatomic) CGFloat quickZoomStart; @property (nonatomic, getter=isDormant) BOOL dormant; @property (nonatomic, readonly, getter=isRotationAllowed) BOOL rotationAllowed; +@property (nonatomic) CGFloat rotationThresholdWhileZooming; +@property (nonatomic) CGFloat rotationBeforeThresholdMet; +@property (nonatomic) BOOL isZooming; +@property (nonatomic) BOOL isRotating; @property (nonatomic) BOOL shouldTriggerHapticFeedbackForCompass; @property (nonatomic) MGLMapViewProxyAccessibilityElement *mapViewProxyAccessibilityElement; @property (nonatomic) MGLAnnotationContainerView *annotationContainerView; @@ -247,6 +257,7 @@ public: @property (nonatomic) NSMutableDictionary<NSString *, NSMutableArray<MGLAnnotationView *> *> *annotationViewReuseQueueByIdentifier; @property (nonatomic, readonly) BOOL enablePresentsWithTransaction; @property (nonatomic) UIImage *lastSnapshotImage; +@property (nonatomic) NSMutableArray *pendingCompletionBlocks; /// Experimental rendering performance measurement. @property (nonatomic) BOOL experimental_enableFrameRateMeasurement; @@ -260,6 +271,9 @@ public: @property (nonatomic) MGLMapDebugMaskOptions residualDebugMask; @property (nonatomic, copy) NSURL *residualStyleURL; +/// Tilt gesture recognizer helper +@property (nonatomic, assign) CGPoint dragGestureMiddlePoint; + - (mbgl::Map &)mbglMap; @end @@ -457,7 +471,7 @@ public: // setup mbgl map MGLRendererConfiguration *config = [MGLRendererConfiguration currentConfiguration]; - auto renderer = std::make_unique<mbgl::Renderer>(_mbglView->getRendererBackend(), config.scaleFactor, config.cacheDir, config.localFontFamilyName); + auto renderer = std::make_unique<mbgl::Renderer>(_mbglView->getRendererBackend(), config.scaleFactor, config.localFontFamilyName); BOOL enableCrossSourceCollisions = !config.perSourceCollisions; _rendererFrontend = std::make_unique<MGLRenderFrontend>(std::move(renderer), self, _mbglView->getRendererBackend()); @@ -612,6 +626,11 @@ public: [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didEnterBackground:) name:UIApplicationDidEnterBackgroundNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(willEnterForeground:) name:UIApplicationWillEnterForegroundNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didBecomeActive:) name:UIApplicationDidBecomeActiveNotification object:nil]; + + // Pending completion blocks are called *after* annotation views have been updated + // in updateFromDisplayLink. + _pendingCompletionBlocks = [NSMutableArray array]; + // As of 3.7.5, we intentionally do not listen for `UIApplicationWillResignActiveNotification` or call `pauseRendering:` in response to it, as doing // so causes a loop when asking for location permission. See: https://github.com/mapbox/mapbox-gl-native/issues/11225 @@ -1033,6 +1052,34 @@ public: return CGPointMake(CGRectGetMidX(contentFrame), CGRectGetMidY(contentFrame)); } +#pragma mark - Pending completion blocks + +- (void)processPendingBlocks +{ + NSArray *blocks = self.pendingCompletionBlocks; + self.pendingCompletionBlocks = [NSMutableArray array]; + + for (dispatch_block_t block in blocks) + { + block(); + } +} + +- (BOOL)scheduleTransitionCompletion:(dispatch_block_t)block +{ + // Only add a block if the display link (that calls processPendingBlocks) is + // running, otherwise fall back to calling immediately. + if (_displayLink && !_displayLink.isPaused) + { + [self willChangeValueForKey:@"pendingCompletionBlocks"]; + [self.pendingCompletionBlocks addObject:block]; + [self didChangeValueForKey:@"pendingCompletionBlocks"]; + return YES; + } + + return NO; +} + #pragma mark - Life Cycle - - (void)updateFromDisplayLink:(CADisplayLink *)displayLink @@ -1057,7 +1104,7 @@ public: return; } - if (_needsDisplayRefresh) + if (_needsDisplayRefresh || (self.pendingCompletionBlocks.count > 0)) { _needsDisplayRefresh = NO; @@ -1066,6 +1113,13 @@ public: [self updateAnnotationViews]; [self updateCalloutView]; + // Call any pending completion blocks. This is primarily to ensure + // that annotations are in the expected position after core rendering + // and map update. + // + // TODO: Consider using this same mechanism for delegate callbacks. + [self processPendingBlocks]; + _mbglView->display(); } @@ -1132,6 +1186,7 @@ public: { [_displayLink invalidate]; _displayLink = nil; + [self processPendingBlocks]; } } @@ -1415,6 +1470,7 @@ public: [MGLMapboxEvents flush]; _displayLink.paused = YES; + [self processPendingBlocks]; if ( ! self.glSnapshotView) { @@ -1467,6 +1523,11 @@ public: { super.hidden = hidden; _displayLink.paused = hidden; + + if (hidden) + { + [self processPendingBlocks]; + } } - (void)tintColorDidChange @@ -1622,6 +1683,9 @@ public: { self.scale = powf(2, [self zoomLevel]); + if (abs(pinch.velocity) > abs(self.rotate.velocity)) { + self.isZooming = YES; + } [self notifyGestureDidBegin]; } else if (pinch.state == UIGestureRecognizerStateChanged) @@ -1697,6 +1761,7 @@ public: } } + self.isZooming = NO; [self notifyGestureDidEndWithDrift:drift]; [self unrotateIfNeededForGesture]; } @@ -1709,6 +1774,106 @@ public: { if ( ! self.isRotateEnabled) return; + if ([[NSUserDefaults standardUserDefaults] objectForKey:MGLRotationThresholdWhileZoomingKey]) { + [self handleRotateGestureRecognizerWithThreshold:rotate]; + } else { + [self cancelTransitions]; + + CGPoint centerPoint = [self anchorPointForGesture:rotate]; + MGLMapCamera *oldCamera = self.camera; + + self.cameraChangeReasonBitmask |= MGLCameraChangeReasonGestureRotate; + + if (rotate.state == UIGestureRecognizerStateBegan) + { + self.angle = MGLRadiansFromDegrees(*self.mbglMap.getCameraOptions().bearing) * -1; + + self.isRotating = YES; + if (self.userTrackingMode != MGLUserTrackingModeNone) + { + self.userTrackingMode = MGLUserTrackingModeFollow; + } + + self.shouldTriggerHapticFeedbackForCompass = NO; + [self notifyGestureDidBegin]; + } + if (rotate.state == UIGestureRecognizerStateChanged) + { + CGFloat newDegrees = MGLDegreesFromRadians(self.angle + rotate.rotation) * -1; + + // constrain to +/-30 degrees when merely rotating like Apple does + // + if ( ! self.isRotationAllowed && std::abs(self.pinch.scale) < 10) + { + newDegrees = fminf(newDegrees, 30); + newDegrees = fmaxf(newDegrees, -30); + } + + MGLMapCamera *toCamera = [self cameraByRotatingToDirection:newDegrees aroundAnchorPoint:centerPoint]; + + if ([self _shouldChangeFromCamera:oldCamera toCamera:toCamera]) + { + self.mbglMap.jumpTo(mbgl::CameraOptions() + .withBearing(newDegrees) + .withAnchor(mbgl::ScreenCoordinate { centerPoint.x, centerPoint.y})); + } + + [self cameraIsChanging]; + + // Trigger a light haptic feedback event when the user rotates to due north. + if (@available(iOS 10.0, *)) + { + if (self.isHapticFeedbackEnabled && fabs(newDegrees) <= 1 && self.shouldTriggerHapticFeedbackForCompass) + { + UIImpactFeedbackGenerator *hapticFeedback = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleLight]; + [hapticFeedback impactOccurred]; + + self.shouldTriggerHapticFeedbackForCompass = NO; + } + else if (fabs(newDegrees) > 1) + { + self.shouldTriggerHapticFeedbackForCompass = YES; + } + } + } + else if ((rotate.state == UIGestureRecognizerStateEnded || rotate.state == UIGestureRecognizerStateCancelled)) + { + CGFloat velocity = rotate.velocity; + CGFloat decelerationRate = self.decelerationRate; + if (decelerationRate != MGLMapViewDecelerationRateImmediate && fabs(velocity) > 3) + { + CGFloat radians = self.angle + rotate.rotation; + CGFloat newRadians = radians + velocity * decelerationRate * 0.1; + CGFloat newDegrees = MGLDegreesFromRadians(newRadians) * -1; + + MGLMapCamera *toCamera = [self cameraByRotatingToDirection:newDegrees aroundAnchorPoint:centerPoint]; + + if ([self _shouldChangeFromCamera:oldCamera toCamera:toCamera]) + { + self.mbglMap.easeTo(mbgl::CameraOptions() + .withBearing(newDegrees) + .withAnchor(mbgl::ScreenCoordinate { centerPoint.x, centerPoint.y }), + MGLDurationFromTimeInterval(decelerationRate)); + + [self notifyGestureDidEndWithDrift:YES]; + __weak MGLMapView *weakSelf = self; + + [self animateWithDelay:decelerationRate animations:^ + { + [weakSelf unrotateIfNeededForGesture]; + }]; + } + } + else + { + [self notifyGestureDidEndWithDrift:NO]; + [self unrotateIfNeededForGesture]; + } + } + } +} + +- (void)handleRotateGestureRecognizerWithThreshold:(UIRotationGestureRecognizer *)rotate { [self cancelTransitions]; CGPoint centerPoint = [self anchorPointForGesture:rotate]; @@ -1716,20 +1881,29 @@ public: self.cameraChangeReasonBitmask |= MGLCameraChangeReasonGestureRotate; - if (rotate.state == UIGestureRecognizerStateBegan) + _rotationThresholdWhileZooming = [[[NSUserDefaults standardUserDefaults] objectForKey:MGLRotationThresholdWhileZoomingKey] floatValue]; + + // Check whether a zoom triggered by a pinch gesture is occurring and if the rotation threshold has been met. + if (MGLDegreesFromRadians(self.rotationBeforeThresholdMet) < self.rotationThresholdWhileZooming && self.isZooming && !self.isRotating) { + self.rotationBeforeThresholdMet += fabs(rotate.rotation); + rotate.rotation = 0; + return; + } + + if (rotate.state == UIGestureRecognizerStateBegan || ! self.isRotating) { self.angle = MGLRadiansFromDegrees(*self.mbglMap.getCameraOptions().bearing) * -1; + self.isRotating = YES; if (self.userTrackingMode != MGLUserTrackingModeNone) { self.userTrackingMode = MGLUserTrackingModeFollow; } self.shouldTriggerHapticFeedbackForCompass = NO; - [self notifyGestureDidBegin]; } - else if (rotate.state == UIGestureRecognizerStateChanged) + if (rotate.state == UIGestureRecognizerStateChanged) { CGFloat newDegrees = MGLDegreesFromRadians(self.angle + rotate.rotation) * -1; @@ -1740,14 +1914,14 @@ public: newDegrees = fminf(newDegrees, 30); newDegrees = fmaxf(newDegrees, -30); } - + MGLMapCamera *toCamera = [self cameraByRotatingToDirection:newDegrees aroundAnchorPoint:centerPoint]; if ([self _shouldChangeFromCamera:oldCamera toCamera:toCamera]) { - self.mbglMap.jumpTo(mbgl::CameraOptions() - .withBearing(newDegrees) - .withAnchor(mbgl::ScreenCoordinate { centerPoint.x, centerPoint.y})); + self.mbglMap.jumpTo(mbgl::CameraOptions() + .withBearing(newDegrees) + .withAnchor(mbgl::ScreenCoordinate { centerPoint.x, centerPoint.y})); } [self cameraIsChanging]; @@ -1768,8 +1942,12 @@ public: } } } - else if (rotate.state == UIGestureRecognizerStateEnded || rotate.state == UIGestureRecognizerStateCancelled) + else if ((rotate.state == UIGestureRecognizerStateEnded || rotate.state == UIGestureRecognizerStateCancelled)) { + self.rotationBeforeThresholdMet = 0; + if (! self.isRotating) { return; } + self.isRotating = NO; + CGFloat velocity = rotate.velocity; CGFloat decelerationRate = self.decelerationRate; if (decelerationRate != MGLMapViewDecelerationRateImmediate && fabs(velocity) > 3) @@ -1783,14 +1961,13 @@ public: if ([self _shouldChangeFromCamera:oldCamera toCamera:toCamera]) { self.mbglMap.easeTo(mbgl::CameraOptions() - .withBearing(newDegrees) - .withAnchor(mbgl::ScreenCoordinate { centerPoint.x, centerPoint.y }), + .withBearing(newDegrees) + .withAnchor(mbgl::ScreenCoordinate { centerPoint.x, centerPoint.y }), MGLDurationFromTimeInterval(decelerationRate)); [self notifyGestureDidEndWithDrift:YES]; - __weak MGLMapView *weakSelf = self; - + [self animateWithDelay:decelerationRate animations:^ { [weakSelf unrotateIfNeededForGesture]; @@ -2016,6 +2193,14 @@ public: if (twoFingerDrag.state == UIGestureRecognizerStateBegan) { + CGPoint midPoint = [twoFingerDrag translationInView:twoFingerDrag.view]; + // In the following if and for the first execution middlePoint + // will be equal to dragGestureMiddlePoint and the resulting + // gestureSlopeAngle will be 0º causing a small delay, + // initializing dragGestureMiddlePoint with the current midPoint + // but substracting one point from 'y' forces an initial 90º angle + // making the gesture avoid the delay + self.dragGestureMiddlePoint = CGPointMake(midPoint.x, midPoint.y-1); initialPitch = *self.mbglMap.getCameraOptions().pitch; [self notifyGestureDidBegin]; } @@ -2027,30 +2212,45 @@ public: twoFingerDrag.state = UIGestureRecognizerStateEnded; return; } - - CGFloat gestureDistance = CGPoint([twoFingerDrag translationInView:twoFingerDrag.view]).y; - CGFloat slowdown = 2.0; - - CGFloat pitchNew = initialPitch - (gestureDistance / slowdown); - - CGPoint centerPoint = [self anchorPointForGesture:twoFingerDrag]; - - MGLMapCamera *oldCamera = self.camera; - MGLMapCamera *toCamera = [self cameraByTiltingToPitch:pitchNew]; - - if ([self _shouldChangeFromCamera:oldCamera toCamera:toCamera]) - { - self.mbglMap.jumpTo(mbgl::CameraOptions() + + CGPoint leftTouchPoint = [twoFingerDrag locationOfTouch:0 inView:twoFingerDrag.view]; + CGPoint rightTouchPoint = [twoFingerDrag locationOfTouch:1 inView:twoFingerDrag.view]; + CLLocationDegrees fingerSlopeAngle = [self angleBetweenPoints:leftTouchPoint endPoint:rightTouchPoint]; + + CGPoint middlePoint = [twoFingerDrag translationInView:twoFingerDrag.view]; + + CLLocationDegrees gestureSlopeAngle = [self angleBetweenPoints:self.dragGestureMiddlePoint endPoint:middlePoint]; + self.dragGestureMiddlePoint = middlePoint; + if (fabs(fingerSlopeAngle) < MGLHorizontalTiltToleranceDegrees && fabs(gestureSlopeAngle) > 60.0 ) { + + CGFloat gestureDistance = middlePoint.y; + CGFloat slowdown = 2.0; + + CGFloat pitchNew = initialPitch - (gestureDistance / slowdown); + + CGPoint centerPoint = [self anchorPointForGesture:twoFingerDrag]; + + MGLMapCamera *oldCamera = self.camera; + MGLMapCamera *toCamera = [self cameraByTiltingToPitch:pitchNew]; + + if ([self _shouldChangeFromCamera:oldCamera toCamera:toCamera]) + { + self.mbglMap.jumpTo(mbgl::CameraOptions() .withPitch(pitchNew) .withAnchor(mbgl::ScreenCoordinate { centerPoint.x, centerPoint.y })); + } + + [self cameraIsChanging]; + } - [self cameraIsChanging]; + } else if (twoFingerDrag.state == UIGestureRecognizerStateEnded || twoFingerDrag.state == UIGestureRecognizerStateCancelled) { [self notifyGestureDidEndWithDrift:NO]; [self unrotateIfNeededForGesture]; + self.dragGestureMiddlePoint = CGPointZero; } } @@ -2173,23 +2373,17 @@ public: - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer { - if ([gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]]) + if (gestureRecognizer == _twoFingerDrag) { UIPanGestureRecognizer *panGesture = (UIPanGestureRecognizer *)gestureRecognizer; if (panGesture.minimumNumberOfTouches == 2) { - CGPoint west = [panGesture locationOfTouch:0 inView:panGesture.view]; - CGPoint east = [panGesture locationOfTouch:1 inView:panGesture.view]; - - if (west.x > east.x) { - CGPoint swap = west; - west = east; - east = swap; - } + CGPoint leftTouchPoint = [panGesture locationOfTouch:0 inView:panGesture.view]; + CGPoint rightTouchPoint = [panGesture locationOfTouch:1 inView:panGesture.view]; - CLLocationDegrees horizontalToleranceDegrees = 60.0; - if ([self angleBetweenPoints:west east:east] > horizontalToleranceDegrees) { + CLLocationDegrees degrees = [self angleBetweenPoints:leftTouchPoint endPoint:rightTouchPoint]; + if (fabs(degrees) > MGLHorizontalTiltToleranceDegrees) { return NO; } } @@ -2211,18 +2405,24 @@ public: - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer { NSArray *validSimultaneousGestures = @[ self.pan, self.pinch, self.rotate ]; - return ([validSimultaneousGestures containsObject:gestureRecognizer] && [validSimultaneousGestures containsObject:otherGestureRecognizer]); } -- (CLLocationDegrees)angleBetweenPoints:(CGPoint)west east:(CGPoint)east +- (CLLocationDegrees)angleBetweenPoints:(CGPoint)originPoint endPoint:(CGPoint)endPoint { - CGFloat slope = (west.y - east.y) / (west.x - east.x); + if (originPoint.x > endPoint.x) { + CGPoint swap = originPoint; + originPoint = endPoint; + endPoint = swap; + } + + CGFloat x = (endPoint.x - originPoint.x); + CGFloat y = (endPoint.y - originPoint.y); - CGFloat angle = atan(fabs(slope)); - CLLocationDegrees degrees = MGLDegreesFromRadians(angle); + CGFloat angleInRadians = atan2(y, x); + CLLocationDegrees angleInDegrees = MGLDegreesFromRadians(angleInRadians); - return degrees; + return angleInDegrees; } #pragma mark - Attribution - @@ -2302,7 +2502,7 @@ public: NSString *message; NSString *participateTitle; NSString *declineTitle; - if ([[NSUserDefaults standardUserDefaults] boolForKey:@"MGLMapboxMetricsEnabled"]) + if ([[NSUserDefaults standardUserDefaults] boolForKey:MGLMapboxMetricsEnabledKey]) { message = NSLocalizedStringWithDefaultValue(@"TELEMETRY_ENABLED_MSG", nil, nil, @"You are helping to make OpenStreetMap and Mapbox maps better by contributing anonymous usage data.", @"Telemetry prompt message"); participateTitle = NSLocalizedStringWithDefaultValue(@"TELEMETRY_ENABLED_ON", nil, nil, @"Keep Participating", @"Telemetry prompt button"); @@ -2330,14 +2530,14 @@ public: UIAlertAction *declineAction = [UIAlertAction actionWithTitle:declineTitle style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { - [[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"MGLMapboxMetricsEnabled"]; + [[NSUserDefaults standardUserDefaults] setBool:NO forKey:MGLMapboxMetricsEnabledKey]; }]; [alertController addAction:declineAction]; UIAlertAction *participateAction = [UIAlertAction actionWithTitle:participateTitle style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) { - [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"MGLMapboxMetricsEnabled"]; + [[NSUserDefaults standardUserDefaults] setBool:YES forKey:MGLMapboxMetricsEnabledKey]; }]; [alertController addAction:participateAction]; @@ -3188,26 +3388,35 @@ public: animationOptions.duration.emplace(MGLDurationFromTimeInterval(duration)); animationOptions.easing.emplace(MGLUnitBezierForMediaTimingFunction(function)); } + + dispatch_block_t pendingCompletion; + if (completion) { - animationOptions.transitionFinishFn = [completion]() { + __weak __typeof__(self) weakSelf = self; + + pendingCompletion = ^{ + if (![weakSelf scheduleTransitionCompletion:completion]) + { + completion(); + } + }; + + animationOptions.transitionFinishFn = [pendingCompletion]() { // Must run asynchronously after the transition is completely over. // Otherwise, a call to -setCenterCoordinate: within the completion // handler would reenter the completion handler’s caller. - dispatch_async(dispatch_get_main_queue(), ^{ - completion(); - }); + + dispatch_async(dispatch_get_main_queue(), pendingCompletion); }; } MGLMapCamera *camera = [self cameraForCameraOptions:cameraOptions]; if ([self.camera isEqualToMapCamera:camera] && UIEdgeInsetsEqualToEdgeInsets(_contentInset, insets)) { - if (completion) + if (pendingCompletion) { - [self animateWithDelay:duration animations:^{ - completion(); - }]; + [self animateWithDelay:duration animations:pendingCompletion]; } return; } @@ -3374,12 +3583,22 @@ public: animationOptions.duration.emplace(MGLDurationFromTimeInterval(duration)); animationOptions.easing.emplace(MGLUnitBezierForMediaTimingFunction(function)); } + + dispatch_block_t pendingCompletion; + if (completion) { - animationOptions.transitionFinishFn = [completion]() { - dispatch_async(dispatch_get_main_queue(), ^{ + __weak __typeof__(self) weakSelf = self; + + pendingCompletion = ^{ + if (![weakSelf scheduleTransitionCompletion:completion]) + { completion(); - }); + } + }; + + animationOptions.transitionFinishFn = [pendingCompletion]() { + dispatch_async(dispatch_get_main_queue(), pendingCompletion); }; } @@ -3389,11 +3608,9 @@ public: MGLMapCamera *camera = [self cameraForCameraOptions:cameraOptions]; if ([self.camera isEqualToMapCamera:camera]) { - if (completion) + if (pendingCompletion) { - [self animateWithDelay:duration animations:^{ - completion(); - }]; + [self animateWithDelay:duration animations:pendingCompletion]; } return; } @@ -3534,22 +3751,30 @@ public: animationOptions.duration.emplace(MGLDurationFromTimeInterval(duration)); animationOptions.easing.emplace(MGLUnitBezierForMediaTimingFunction(function)); } + + dispatch_block_t pendingCompletion; + if (completion) { - animationOptions.transitionFinishFn = [completion]() { - dispatch_async(dispatch_get_main_queue(), ^{ + __weak __typeof__(self) weakSelf = self; + + pendingCompletion = ^{ + if (![weakSelf scheduleTransitionCompletion:completion]) + { completion(); - }); + } + }; + + animationOptions.transitionFinishFn = [pendingCompletion]() { + dispatch_async(dispatch_get_main_queue(), pendingCompletion); }; } if ([self.camera isEqualToMapCamera:camera] && UIEdgeInsetsEqualToEdgeInsets(_contentInset, edgePadding)) { - if (completion) + if (pendingCompletion) { - [self animateWithDelay:duration animations:^{ - completion(); - }]; + [self animateWithDelay:duration animations:pendingCompletion]; } return; } @@ -3605,22 +3830,30 @@ public: animationOptions.minZoom = MGLZoomLevelForAltitude(peakAltitude, peakPitch, peakLatitude, self.frame.size); } + + dispatch_block_t pendingCompletion; + if (completion) { - animationOptions.transitionFinishFn = [completion]() { - dispatch_async(dispatch_get_main_queue(), ^{ + __weak __typeof__(self) weakSelf = self; + + pendingCompletion = ^{ + if (![weakSelf scheduleTransitionCompletion:completion]) + { completion(); - }); + } + }; + + animationOptions.transitionFinishFn = [pendingCompletion]() { + dispatch_async(dispatch_get_main_queue(), pendingCompletion); }; } if ([self.camera isEqualToMapCamera:camera] && UIEdgeInsetsEqualToEdgeInsets(_contentInset, insets)) { - if (completion) + if (pendingCompletion) { - [self animateWithDelay:duration animations:^{ - completion(); - }]; + [self animateWithDelay:duration animations:pendingCompletion]; } return; } diff --git a/platform/ios/src/MGLMapView_Private.h b/platform/ios/src/MGLMapView_Private.h index e53dc8519c..155527000f 100644 --- a/platform/ios/src/MGLMapView_Private.h +++ b/platform/ios/src/MGLMapView_Private.h @@ -52,8 +52,6 @@ FOUNDATION_EXTERN MGL_EXPORT MGLExceptionName const _Nonnull MGLUnderlyingMapUna /// Synchronously render a frame of the map. - (void)renderSync; -- (nonnull mbgl::Map *)mbglMap; - - (nonnull mbgl::Renderer *)renderer; /** Returns whether the map view is currently loading or processing any assets required to render the map */ diff --git a/platform/ios/src/MGLMapboxEvents.h b/platform/ios/src/MGLMapboxEvents.h index cb3132656f..a7d316cc06 100644 --- a/platform/ios/src/MGLMapboxEvents.h +++ b/platform/ios/src/MGLMapboxEvents.h @@ -3,6 +3,9 @@ NS_ASSUME_NONNULL_BEGIN +/// NSUserDefaults key that controls telemetry user opt-out status +FOUNDATION_EXTERN NSString * const MGLMapboxMetricsEnabledKey; + @interface MGLMapboxEvents : NSObject + (nullable instancetype)sharedInstance; diff --git a/platform/ios/src/MGLMapboxEvents.m b/platform/ios/src/MGLMapboxEvents.m index cc7390ac61..808c3a88bf 100644 --- a/platform/ios/src/MGLMapboxEvents.m +++ b/platform/ios/src/MGLMapboxEvents.m @@ -3,14 +3,16 @@ #import "NSBundle+MGLAdditions.h" #import "MGLAccountManager_Private.h" +// NSUserDefaults and Info.plist keys +NSString * const MGLMapboxMetricsEnabledKey = @"MGLMapboxMetricsEnabled"; +static NSString * const MGLMapboxMetricsDebugLoggingEnabledKey = @"MGLMapboxMetricsDebugLoggingEnabled"; +static NSString * const MGLMapboxMetricsEnabledSettingShownInAppKey = @"MGLMapboxMetricsEnabledSettingShownInApp"; +static NSString * const MGLTelemetryAccessTokenKey = @"MGLTelemetryAccessToken"; +static NSString * const MGLTelemetryBaseURLKey = @"MGLTelemetryBaseURL"; +static NSString * const MGLEventsProfileKey = @"MMEEventsProfile"; +static NSString * const MGLVariableGeofenceKey = @"VariableGeofence"; + static NSString * const MGLAPIClientUserAgentBase = @"mapbox-maps-ios"; -static NSString * const MGLMapboxAccountType = @"MGLMapboxAccountType"; -static NSString * const MGLMapboxMetricsEnabled = @"MGLMapboxMetricsEnabled"; -static NSString * const MGLMapboxMetricsDebugLoggingEnabled = @"MGLMapboxMetricsDebugLoggingEnabled"; -static NSString * const MGLTelemetryAccessToken = @"MGLTelemetryAccessToken"; -static NSString * const MGLTelemetryBaseURL = @"MGLTelemetryBaseURL"; -static NSString * const MGLEventsProfile = @"MMEEventsProfile"; -static NSString * const MGLVariableGeofence = @"VariableGeofence"; @interface MGLMapboxEvents () @@ -19,16 +21,16 @@ static NSString * const MGLVariableGeofence = @"VariableGeofence"; @property (nonatomic, copy) NSString *accessToken; @end -@implementation MGLMapboxEvents +@implementation MGLMapboxEvents + (void)initialize { if (self == [MGLMapboxEvents class]) { NSBundle *bundle = [NSBundle mainBundle]; - NSNumber *accountTypeNumber = [bundle objectForInfoDictionaryKey:MGLMapboxAccountType]; - [[NSUserDefaults standardUserDefaults] registerDefaults:@{MGLMapboxAccountType: accountTypeNumber ?: @0, - MGLMapboxMetricsEnabled: @YES, - MGLMapboxMetricsDebugLoggingEnabled: @NO}]; + NSNumber *accountTypeNumber = [bundle objectForInfoDictionaryKey:MGLMapboxAccountTypeKey]; + [[NSUserDefaults standardUserDefaults] registerDefaults:@{MGLMapboxAccountTypeKey: accountTypeNumber ?: @0, + MGLMapboxMetricsEnabledKey: @YES, + MGLMapboxMetricsDebugLoggingEnabledKey: @NO}]; } } @@ -46,9 +48,9 @@ static NSString * const MGLVariableGeofence = @"VariableGeofence"; self = [super init]; if (self) { _eventsManager = MMEEventsManager.sharedManager; - _eventsManager.debugLoggingEnabled = [[NSUserDefaults standardUserDefaults] boolForKey:MGLMapboxMetricsDebugLoggingEnabled]; - _eventsManager.accountType = [[NSUserDefaults standardUserDefaults] integerForKey:MGLMapboxAccountType]; - _eventsManager.metricsEnabled = [[NSUserDefaults standardUserDefaults] boolForKey:MGLMapboxMetricsEnabled]; + _eventsManager.debugLoggingEnabled = [[NSUserDefaults standardUserDefaults] boolForKey:MGLMapboxMetricsDebugLoggingEnabledKey]; + _eventsManager.accountType = [[NSUserDefaults standardUserDefaults] integerForKey:MGLMapboxAccountTypeKey]; + _eventsManager.metricsEnabled = [[NSUserDefaults standardUserDefaults] boolForKey:MGLMapboxMetricsEnabledKey]; // It is possible for the shared instance of this class to be created because of a call to // +[MGLAccountManager load] early on in the app lifecycle of the host application. @@ -57,11 +59,11 @@ static NSString * const MGLVariableGeofence = @"VariableGeofence"; // (once -[MMEEventsManager initializeWithAccessToken:userAgentBase:hostSDKVersion:] is called. // Normally, the telem access token and base URL are not set this way. However, overriding these values // with user defaults can be useful for testing with an alternative (test) backend system. - if ([[[[NSUserDefaults standardUserDefaults] dictionaryRepresentation] allKeys] containsObject:MGLTelemetryAccessToken]) { - self.accessToken = [[NSUserDefaults standardUserDefaults] objectForKey:MGLTelemetryAccessToken]; + if ([[[[NSUserDefaults standardUserDefaults] dictionaryRepresentation] allKeys] containsObject:MGLTelemetryAccessTokenKey]) { + self.accessToken = [[NSUserDefaults standardUserDefaults] objectForKey:MGLTelemetryAccessTokenKey]; } - if ([[[[NSUserDefaults standardUserDefaults] dictionaryRepresentation] allKeys] containsObject:MGLTelemetryBaseURL]) { - self.baseURL = [NSURL URLWithString:[[NSUserDefaults standardUserDefaults] objectForKey:MGLTelemetryBaseURL]]; + if ([[[[NSUserDefaults standardUserDefaults] dictionaryRepresentation] allKeys] containsObject:MGLTelemetryBaseURLKey]) { + self.baseURL = [NSURL URLWithString:[[NSUserDefaults standardUserDefaults] objectForKey:MGLTelemetryBaseURLKey]]; } [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(userDefaultsDidChange:) name:NSUserDefaultsDidChangeNotification object:nil]; @@ -81,22 +83,22 @@ static NSString * const MGLVariableGeofence = @"VariableGeofence"; } - (void)updateNonDisablingConfigurationValues { - self.eventsManager.debugLoggingEnabled = [[NSUserDefaults standardUserDefaults] boolForKey:@"MGLMapboxMetricsDebugLoggingEnabled"]; + self.eventsManager.debugLoggingEnabled = [[NSUserDefaults standardUserDefaults] boolForKey:MGLMapboxMetricsDebugLoggingEnabledKey]; - // It is possible for `MGLTelemetryAccessToken` to have been set yet `userDefaultsDidChange:` + // It is possible for the telemetry access token key to have been set yet `userDefaultsDidChange:` // is called before `setupWithAccessToken:` is called. // In that case, setting the access token here will have no effect. In practice, that's fine // because the access token value will be resolved when `setupWithAccessToken:` is called eventually - if ([[[[NSUserDefaults standardUserDefaults] dictionaryRepresentation] allKeys] containsObject:MGLTelemetryAccessToken]) { - self.eventsManager.accessToken = [[NSUserDefaults standardUserDefaults] objectForKey:MGLTelemetryAccessToken]; + if ([[[[NSUserDefaults standardUserDefaults] dictionaryRepresentation] allKeys] containsObject:MGLTelemetryAccessTokenKey]) { + self.eventsManager.accessToken = [[NSUserDefaults standardUserDefaults] objectForKey:MGLTelemetryAccessTokenKey]; } - // It is possible for `MGLTelemetryBaseURL` to have been set yet `userDefaultsDidChange:` + // It is possible for the telemetry base URL key to have been set yet `userDefaultsDidChange:` // is called before setupWithAccessToken: is called. // In that case, setting the base URL here will have no effect. In practice, that's fine // because the base URL value will be resolved when `setupWithAccessToken:` is called eventually - if ([[[[NSUserDefaults standardUserDefaults] dictionaryRepresentation] allKeys] containsObject:MGLTelemetryBaseURL]) { - NSURL *baseURL = [NSURL URLWithString:[[NSUserDefaults standardUserDefaults] objectForKey:MGLTelemetryBaseURL]]; + if ([[[[NSUserDefaults standardUserDefaults] dictionaryRepresentation] allKeys] containsObject:MGLTelemetryBaseURLKey]) { + NSURL *baseURL = [NSURL URLWithString:[[NSUserDefaults standardUserDefaults] objectForKey:MGLTelemetryBaseURLKey]]; self.eventsManager.baseURL = baseURL; } } @@ -109,8 +111,8 @@ static NSString * const MGLVariableGeofence = @"VariableGeofence"; if ([[notification object] respondsToSelector:@selector(objectForKey:)]) { NSUserDefaults *userDefaults = [notification object]; - NSInteger accountType = [userDefaults integerForKey:MGLMapboxAccountType]; - BOOL metricsEnabled = [userDefaults boolForKey:MGLMapboxMetricsEnabled]; + NSInteger accountType = [userDefaults integerForKey:MGLMapboxAccountTypeKey]; + BOOL metricsEnabled = [userDefaults boolForKey:MGLMapboxMetricsEnabledKey]; if (accountType != self.eventsManager.accountType || metricsEnabled != self.eventsManager.metricsEnabled) { self.eventsManager.accountType = accountType; @@ -124,7 +126,7 @@ static NSString * const MGLVariableGeofence = @"VariableGeofence"; + (void)setupWithAccessToken:(NSString *)accessToken { int64_t delayTime = 0; - if ([[[NSBundle mainBundle] objectForInfoDictionaryKey:MGLEventsProfile] isEqualToString:MGLVariableGeofence]) { + if ([[[NSBundle mainBundle] objectForInfoDictionaryKey:MGLEventsProfileKey] isEqualToString:MGLVariableGeofenceKey]) { delayTime = 10; } @@ -164,11 +166,11 @@ static NSString * const MGLVariableGeofence = @"VariableGeofence"; } + (void)ensureMetricsOptoutExists { - NSNumber *shownInAppNumber = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"MGLMapboxMetricsEnabledSettingShownInApp"]; + NSNumber *shownInAppNumber = [[NSBundle mainBundle] objectForInfoDictionaryKey:MGLMapboxMetricsEnabledSettingShownInAppKey]; BOOL metricsEnabledSettingShownInAppFlag = [shownInAppNumber boolValue]; if (!metricsEnabledSettingShownInAppFlag && - [[NSUserDefaults standardUserDefaults] integerForKey:MGLMapboxAccountType] == 0) { + [[NSUserDefaults standardUserDefaults] integerForKey:MGLMapboxAccountTypeKey] == 0) { // Opt-out is not configured in UI, so check for Settings.bundle id defaultEnabledValue; NSString *appSettingsBundle = [[NSBundle mainBundle] pathForResource:@"Settings" ofType:@"bundle"]; @@ -178,7 +180,7 @@ static NSString * const MGLVariableGeofence = @"VariableGeofence"; NSDictionary *settings = [NSDictionary dictionaryWithContentsOfFile:[appSettingsBundle stringByAppendingPathComponent:@"Root.plist"]]; NSArray *preferences = settings[@"PreferenceSpecifiers"]; for (NSDictionary *prefSpecification in preferences) { - if ([prefSpecification[@"Key"] isEqualToString:MGLMapboxMetricsEnabled]) { + if ([prefSpecification[@"Key"] isEqualToString:MGLMapboxMetricsEnabledKey]) { defaultEnabledValue = prefSpecification[@"DefaultValue"]; } } diff --git a/platform/ios/src/MGLScaleBar.mm b/platform/ios/src/MGLScaleBar.mm index 9590a99438..993852d8b9 100644 --- a/platform/ios/src/MGLScaleBar.mm +++ b/platform/ios/src/MGLScaleBar.mm @@ -84,7 +84,7 @@ static const MGLRow MGLImperialTable[] ={ @property (nonatomic) UIColor *secondaryColor; @property (nonatomic) CALayer *borderLayer; @property (nonatomic, assign) CGFloat borderWidth; -@property (nonatomic) NSCache* labelImageCache; +@property (nonatomic) NSMutableDictionary* labelImageCache; @property (nonatomic) MGLScaleBarLabel* prototypeLabel; @property (nonatomic) CGFloat lastLabelWidth; @@ -159,7 +159,7 @@ static const CGFloat MGLFeetPerMeter = 3.28084; _formatter = [[MGLDistanceFormatter alloc] init]; // Image labels are now images - _labelImageCache = [[NSCache alloc] init]; + _labelImageCache = [[NSMutableDictionary alloc] init]; _prototypeLabel = [[MGLScaleBarLabel alloc] init]; _prototypeLabel.font = [UIFont systemFontOfSize:8 weight:UIFontWeightMedium]; _prototypeLabel.clipsToBounds = NO; @@ -180,6 +180,17 @@ static const CGFloat MGLFeetPerMeter = 3.28084; // Zero is a special case (no formatting) [self addZeroLabel]; + + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(resetLabelImageCache) name:NSCurrentLocaleDidChangeNotification object:nil]; +} + +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +- (void)resetLabelImageCache { + self.labelImageCache = [[NSMutableDictionary alloc] init]; + [self addZeroLabel]; } #pragma mark - Dimensions |