diff options
author | Fabian Guerra <fabian.guerra@mapbox.com> | 2018-04-23 10:44:02 -0400 |
---|---|---|
committer | Fabian Guerra <fabian.guerra@mapbox.com> | 2018-04-23 10:44:02 -0400 |
commit | e08b6fe87f5824ab05a4cc67d9a76af5bb5ddd3b (patch) | |
tree | 886e10260bfa044f62943186ec837b9ccd02934c /platform/ios/src/MGLMapView.mm | |
parent | 2bb785dad2489d04db179fa9cf65514640db0a96 (diff) | |
parent | a45670cfb5752866b9c8130024a313944684c2db (diff) | |
download | qtlocation-mapboxgl-upstream/fabian-merge-v4.0.0.tar.gz |
Merge branch 'release-boba' into masterupstream/fabian-merge-v4.0.0
# Conflicts:
# circle.yml
# include/mbgl/style/expression/let.hpp
# platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/LatLngBounds.java
# platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapGestureDetector.java
# platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapKeyListener.java
# platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapView.java
# platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapboxMap.java
# platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/Projection.java
# platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/Transform.java
# platform/android/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/geometry/LatLngBoundsTest.java
# platform/android/MapboxGLAndroidSDKTestApp/src/main/AndroidManifest.xml
# platform/android/gradle/dependencies.gradle
# platform/android/src/example_custom_layer.cpp
# platform/android/src/geojson/point.cpp
# platform/darwin/src/NSPredicate+MGLAdditions.mm
# platform/darwin/test/MGLExpressionTests.mm
# platform/ios/Mapbox-iOS-SDK-nightly-dynamic.podspec
# platform/ios/Mapbox-iOS-SDK-symbols.podspec
# platform/ios/Mapbox-iOS-SDK.podspec
# platform/ios/app/MBXViewController.m
# src/mbgl/renderer/layers/render_custom_layer.cpp
# src/mbgl/style/conversion/filter.cpp
# src/mbgl/style/expression/interpolate.cpp
# src/mbgl/style/expression/value.cpp
# test/style/filter.test.cpp
Diffstat (limited to 'platform/ios/src/MGLMapView.mm')
-rw-r--r-- | platform/ios/src/MGLMapView.mm | 393 |
1 files changed, 195 insertions, 198 deletions
diff --git a/platform/ios/src/MGLMapView.mm b/platform/ios/src/MGLMapView.mm index 6be6a71f66..b7d0974872 100644 --- a/platform/ios/src/MGLMapView.mm +++ b/platform/ios/src/MGLMapView.mm @@ -41,7 +41,7 @@ #import "MGLGeometry_Private.h" #import "MGLMultiPoint_Private.h" #import "MGLOfflineStorage_Private.h" -#import "MGLVectorSource_Private.h" +#import "MGLVectorTileSource_Private.h" #import "MGLFoundation_Private.h" #import "MGLRendererFrontend.h" #import "MGLRendererConfiguration.h" @@ -66,6 +66,7 @@ #import "MGLStyle_Private.h" #import "MGLStyleLayer_Private.h" #import "MGLMapboxEvents.h" +#import "MMEConstants.h" #import "MGLSDKUpdateChecker.h" #import "MGLCompactCalloutView.h" #import "MGLAnnotationContainerView.h" @@ -143,6 +144,9 @@ const CGFloat MGLAnnotationImagePaddingForCallout = 1; const CGSize MGLAnnotationAccessibilityElementMinimumSize = CGSizeMake(10, 10); +/// Padding to edge of view that an offscreen annotation must have when being brought onscreen (by being selected) +const UIEdgeInsets MGLMapViewOffscreenAnnotationPadding = UIEdgeInsetsMake(-20.0f, -20.0f, -20.0f, -20.0f); + /// An indication that the requested annotation was not found or is nonexistent. enum { MGLAnnotationTagNotFound = UINT32_MAX }; @@ -227,6 +231,7 @@ public: @property (nonatomic) CGFloat quickZoomStart; @property (nonatomic, getter=isDormant) BOOL dormant; @property (nonatomic, readonly, getter=isRotationAllowed) BOOL rotationAllowed; +@property (nonatomic) BOOL shouldTriggerHapticFeedbackForCompass; @property (nonatomic) MGLMapViewProxyAccessibilityElement *mapViewProxyAccessibilityElement; @property (nonatomic) MGLAnnotationContainerView *annotationContainerView; @property (nonatomic) MGLUserLocation *userLocation; @@ -244,8 +249,6 @@ public: BOOL _opaque; - NS_MUTABLE_ARRAY_OF(NSURL *) *_bundledStyleURLs; - MGLAnnotationTagContextMap _annotationContextsByAnnotationTag; MGLAnnotationObjectTagMap _annotationTagsByAnnotation; @@ -270,7 +273,7 @@ public: CADisplayLink *_displayLink; BOOL _needsDisplayRefresh; - NSUInteger _changeDelimiterSuppressionDepth; + NSInteger _changeDelimiterSuppressionDepth; /// Center coordinate of the pinch gesture on the previous iteration of the gesture. CLLocationCoordinate2D _previousPinchCenterCoordinate; @@ -524,6 +527,8 @@ public: [_twoFingerTap requireGestureRecognizerToFail:_twoFingerDrag]; [self addGestureRecognizer:_twoFingerTap]; + _hapticFeedbackEnabled = YES; + _decelerationRate = MGLMapViewDecelerationRateNormal; _quickZoom = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleQuickZoomGesture:)]; @@ -568,7 +573,8 @@ public: _targetCoordinate = kCLLocationCoordinate2DInvalid; if ([UIApplication sharedApplication].applicationState != UIApplicationStateBackground) { - [MGLMapboxEvents pushEvent:MGLEventTypeMapLoad withAttributes:@{}]; + [MGLMapboxEvents pushTurnstileEvent]; + [MGLMapboxEvents pushEvent:MMEEventTypeMapLoad withAttributes:@{}]; } } @@ -970,8 +976,8 @@ public: - (void)layoutSubviews { // Calling this here instead of in the scale bar itself because if this is done in the - // scale bar instance, it triggers a call to this this `layoutSubviews` method that - // calls `_mbglMap->setSize()` just below that triggers rendering update which triggers + // scale bar instance, it triggers a call to this `layoutSubviews` method that calls + // `_mbglMap->setSize()` just below that triggers rendering update which triggers // another scale bar update which causes a rendering update loop and a major performace // degradation. The only time the scale bar's intrinsic content size _must_ invalidated // is here as a reaction to this object's view dimension changes. @@ -1220,7 +1226,8 @@ public: [self validateLocationServices]; - [MGLMapboxEvents pushEvent:MGLEventTypeMapLoad withAttributes:@{}]; + [MGLMapboxEvents pushTurnstileEvent]; + [MGLMapboxEvents pushEvent:MMEEventTypeMapLoad withAttributes:@{}]; } } @@ -1332,7 +1339,7 @@ public: if (pan.state == UIGestureRecognizerStateBegan) { - [self trackGestureEvent:MGLEventGesturePanStart forRecognizer:pan]; + [self trackGestureEvent:MMEEventGesturePanStart forRecognizer:pan]; self.userTrackingMode = MGLUserTrackingModeNone; @@ -1380,10 +1387,10 @@ public: CLLocationCoordinate2D panCoordinate = [self convertPoint:pointInView toCoordinateFromView:pan.view]; int zoom = round([self zoomLevel]); - [MGLMapboxEvents pushEvent:MGLEventTypeMapDragEnd withAttributes:@{ - MGLEventKeyLatitude: @(panCoordinate.latitude), - MGLEventKeyLongitude: @(panCoordinate.longitude), - MGLEventKeyZoomLevel: @(zoom) + [MGLMapboxEvents pushEvent:MMEEventTypeMapDragEnd withAttributes:@{ + MMEEventKeyLatitude: @(panCoordinate.latitude), + MMEEventKeyLongitude: @(panCoordinate.longitude), + MMEEventKeyZoomLevel: @(zoom) }]; } @@ -1402,7 +1409,7 @@ public: if (pinch.state == UIGestureRecognizerStateBegan) { - [self trackGestureEvent:MGLEventGesturePinchStart forRecognizer:pinch]; + [self trackGestureEvent:MMEEventGesturePinchStart forRecognizer:pinch]; self.scale = powf(2, _mbglMap->getZoom()); @@ -1503,7 +1510,7 @@ public: if (rotate.state == UIGestureRecognizerStateBegan) { - [self trackGestureEvent:MGLEventGestureRotateStart forRecognizer:rotate]; + [self trackGestureEvent:MMEEventGestureRotateStart forRecognizer:rotate]; self.angle = MGLRadiansFromDegrees(_mbglMap->getBearing()) * -1; @@ -1512,6 +1519,8 @@ public: self.userTrackingMode = MGLUserTrackingModeFollow; } + self.shouldTriggerHapticFeedbackForCompass = NO; + [self notifyGestureDidBegin]; } else if (rotate.state == UIGestureRecognizerStateChanged) @@ -1534,6 +1543,22 @@ public: } [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) { @@ -1575,7 +1600,7 @@ public: { return; } - [self trackGestureEvent:MGLEventGestureSingleTap forRecognizer:singleTap]; + [self trackGestureEvent:MMEEventGestureSingleTap forRecognizer:singleTap]; if (self.mapViewProxyAccessibilityElement.accessibilityElementIsFocused) { @@ -1601,7 +1626,7 @@ public: { CGPoint calloutPoint = [singleTap locationInView:self]; CGRect positionRect = [self positioningRectForAnnotation:annotation defaultCalloutPoint:calloutPoint]; - [self selectAnnotation:annotation animated:YES calloutPositioningRect:positionRect]; + [self selectAnnotation:annotation moveOnscreen:YES animateSelection:YES calloutPositioningRect:positionRect]; } else if (self.selectedAnnotation) { @@ -1697,7 +1722,7 @@ public: if ([self _shouldChangeFromCamera:oldCamera toCamera:toCamera]) { - [self trackGestureEvent:MGLEventGestureDoubleTap forRecognizer:doubleTap]; + [self trackGestureEvent:MMEEventGestureDoubleTap forRecognizer:doubleTap]; mbgl::ScreenCoordinate center(gesturePoint.x, gesturePoint.y); _mbglMap->setZoom(newZoom, center, MGLDurationFromTimeInterval(MGLAnimationDuration)); @@ -1728,7 +1753,7 @@ public: if (twoFingerTap.state == UIGestureRecognizerStateBegan) { - [self trackGestureEvent:MGLEventGestureTwoFingerSingleTap forRecognizer:twoFingerTap]; + [self trackGestureEvent:MMEEventGestureTwoFingerSingleTap forRecognizer:twoFingerTap]; [self notifyGestureDidBegin]; } @@ -1767,7 +1792,7 @@ public: if (quickZoom.state == UIGestureRecognizerStateBegan) { - [self trackGestureEvent:MGLEventGestureQuickZoom forRecognizer:quickZoom]; + [self trackGestureEvent:MMEEventGestureQuickZoom forRecognizer:quickZoom]; self.scale = powf(2, _mbglMap->getZoom()); @@ -1812,7 +1837,7 @@ public: if (twoFingerDrag.state == UIGestureRecognizerStateBegan) { - [self trackGestureEvent:MGLEventGesturePitchStart forRecognizer:twoFingerDrag]; + [self trackGestureEvent:MMEEventGesturePitchStart forRecognizer:twoFingerDrag]; [self notifyGestureDidBegin]; } @@ -2017,11 +2042,11 @@ public: CLLocationCoordinate2D gestureCoordinate = [self convertPoint:pointInView toCoordinateFromView:recognizer.view]; int zoom = round([self zoomLevel]); - [MGLMapboxEvents pushEvent:MGLEventTypeMapTap withAttributes:@{ - MGLEventKeyLatitude: @(gestureCoordinate.latitude), - MGLEventKeyLongitude: @(gestureCoordinate.longitude), - MGLEventKeyZoomLevel: @(zoom), - MGLEventKeyGestureID: gestureID + [MGLMapboxEvents pushEvent:MMEEventTypeMapTap withAttributes:@{ + MMEEventKeyLatitude: @(gestureCoordinate.latitude), + MMEEventKeyLongitude: @(gestureCoordinate.longitude), + MMEEventKeyZoomLevel: @(zoom), + MMEEventKeyGestureID: gestureID }]; } @@ -2293,16 +2318,6 @@ public: MGLMapDebugCollisionBoxesMask) : 0; } -- (BOOL)isDebugActive -{ - return self.debugMask; -} - -- (void)toggleDebug -{ - self.debugActive = !self.debugActive; -} - - (void)resetNorth { [self resetNorthAnimated:YES]; @@ -2325,11 +2340,6 @@ public: heading:heading]; } -- (void)emptyMemoryCache -{ - _rendererFrontend->reduceMemoryUse(); -} - - (void)setZoomEnabled:(BOOL)zoomEnabled { _zoomEnabled = zoomEnabled; @@ -2357,6 +2367,17 @@ public: self.twoFingerDrag.enabled = pitchEnabled; } +- (void)setShowsScale:(BOOL)showsScale +{ + _showsScale = showsScale; + self.scaleBar.hidden = !showsScale; + + if (showsScale) + { + [self updateScaleBar]; + } +} + #pragma mark - Accessibility - - (NSString *)accessibilityValue @@ -3439,36 +3460,24 @@ public: /// bounding box. - (mbgl::LatLngBounds)convertRect:(CGRect)rect toLatLngBoundsFromView:(nullable UIView *)view { - mbgl::LatLngBounds bounds = mbgl::LatLngBounds::empty(); - bounds.extend([self convertPoint:rect.origin toLatLngFromView:view]); - bounds.extend([self convertPoint:{ CGRectGetMaxX(rect), CGRectGetMinY(rect) } toLatLngFromView:view]); - bounds.extend([self convertPoint:{ CGRectGetMaxX(rect), CGRectGetMaxY(rect) } toLatLngFromView:view]); - bounds.extend([self convertPoint:{ CGRectGetMinX(rect), CGRectGetMaxY(rect) } toLatLngFromView:view]); - - // The world is wrapping if a point just outside the bounds is also within - // the rect. - mbgl::LatLng outsideLatLng; - if (bounds.west() > -180) - { - outsideLatLng = { - (bounds.south() + bounds.north()) / 2, - bounds.west() - 1, - }; - } - else if (bounds.east() < 180) - { - outsideLatLng = { - (bounds.south() + bounds.north()) / 2, - bounds.east() + 1, - }; - } - - // If the world is wrapping, extend the bounds to cover all longitudes. - if (CGRectContainsPoint(rect, [self convertLatLng:outsideLatLng toPointToView:view])) - { - bounds.extend(mbgl::LatLng(bounds.south(), -180)); - bounds.extend(mbgl::LatLng(bounds.south(), 180)); - } + auto bounds = mbgl::LatLngBounds::empty(); + auto topLeft = [self convertPoint:{ CGRectGetMinX(rect), CGRectGetMinY(rect) } toLatLngFromView:view]; + auto topRight = [self convertPoint:{ CGRectGetMaxX(rect), CGRectGetMinY(rect) } toLatLngFromView:view]; + auto bottomRight = [self convertPoint:{ CGRectGetMaxX(rect), CGRectGetMaxY(rect) } toLatLngFromView:view]; + auto bottomLeft = [self convertPoint:{ CGRectGetMinX(rect), CGRectGetMaxY(rect) } toLatLngFromView:view]; + + // If the bounds straddles the antimeridian, unwrap it so that one side + // extends beyond ±180° longitude. + auto center = [self convertPoint:{ CGRectGetMidX(rect), CGRectGetMidY(rect) } toLatLngFromView:view]; + topLeft.unwrapForShortestPath(center); + topRight.unwrapForShortestPath(center); + bottomRight.unwrapForShortestPath(center); + bottomLeft.unwrapForShortestPath(center); + + bounds.extend(topLeft); + bounds.extend(topRight); + bounds.extend(bottomRight); + bounds.extend(bottomLeft); return bounds; } @@ -3478,11 +3487,6 @@ public: return mbgl::Projection::getMetersPerPixelAtLatitude(latitude, self.zoomLevel); } -- (CLLocationDistance)metersPerPixelAtLatitude:(CLLocationDegrees)latitude -{ - return [self metersPerPointAtLatitude:latitude]; -} - #pragma mark - Camera Change Reason - - (void)resetCameraChangeReason @@ -3490,70 +3494,6 @@ public: self.cameraChangeReasonBitmask = MGLCameraChangeReasonNone; } -#pragma mark - Styling - - -- (NS_ARRAY_OF(NSURL *) *)bundledStyleURLs -{ - if ( ! _bundledStyleURLs) - { - _bundledStyleURLs = [NSMutableArray array]; - for (NSUInteger i = 0; i < mbgl::util::default_styles::numOrderedStyles; i++) - { - NSURL *styleURL = [NSURL URLWithString:@(mbgl::util::default_styles::orderedStyles[i].url)]; - [_bundledStyleURLs addObject:styleURL]; - } - } - - return [NSArray arrayWithArray:_bundledStyleURLs]; -} - -- (nullable NSString *)styleID -{ - [NSException raise:@"Method unavailable" format: - @"%s has been replaced by -[MGLMapView styleURL].", - __PRETTY_FUNCTION__]; - return nil; -} - -- (void)setStyleID:(nullable NSString *)styleID -{ - [NSException raise:@"Method unavailable" format: - @"%s has been replaced by -[MGLMapView setStyleURL:].\n\n" - @"If you previously set this style ID in a storyboard inspectable, select the MGLMapView in Interface Builder and delete the “styleID” entry from the User Defined Runtime Attributes section of the Identity inspector. " - @"Then go to the Attributes inspector and enter “mapbox://styles/%@” into the “Style URL” field.", - __PRETTY_FUNCTION__, styleID]; -} - -- (NS_ARRAY_OF(NSString *) *)styleClasses -{ - return [self.style styleClasses]; -} - -- (void)setStyleClasses:(NS_ARRAY_OF(NSString *) *)appliedClasses -{ - [self setStyleClasses:appliedClasses transitionDuration:0]; -} - -- (void)setStyleClasses:(NS_ARRAY_OF(NSString *) *)appliedClasses transitionDuration:(NSTimeInterval)transitionDuration -{ - [self.style setStyleClasses:appliedClasses transitionDuration:transitionDuration]; -} - -- (BOOL)hasStyleClass:(NSString *)styleClass -{ - return [self.style hasStyleClass:styleClass]; -} - -- (void)addStyleClass:(NSString *)styleClass -{ - [self.style addStyleClass:styleClass]; -} - -- (void)removeStyleClass:(NSString *)styleClass -{ - [self.style removeStyleClass:styleClass]; -} - #pragma mark - Annotations - - (nullable NS_ARRAY_OF(id <MGLAnnotation>) *)annotations @@ -4174,51 +4114,48 @@ public: MGLAnnotationTag hitAnnotationTag = MGLAnnotationTagNotFound; if (nearbyAnnotations.size()) { - // The annotation tags need to be stable in order to compare them with - // the remembered tags. - std::sort(nearbyAnnotations.begin(), nearbyAnnotations.end()); - + // The first selection in the cycle should be the one nearest to the + // tap. Also the annotation tags need to be stable in order to compare them with + // the remembered tags _annotationsNearbyLastTap. + CLLocationCoordinate2D currentCoordinate = [self convertPoint:point toCoordinateFromView:self]; + std::sort(nearbyAnnotations.begin(), nearbyAnnotations.end(), [&](const MGLAnnotationTag tagA, const MGLAnnotationTag tagB) { + CLLocationCoordinate2D coordinateA = [[self annotationWithTag:tagA] coordinate]; + CLLocationCoordinate2D coordinateB = [[self annotationWithTag:tagB] coordinate]; + CLLocationDegrees deltaA = hypot(coordinateA.latitude - currentCoordinate.latitude, + coordinateA.longitude - currentCoordinate.longitude); + CLLocationDegrees deltaB = hypot(coordinateB.latitude - currentCoordinate.latitude, + coordinateB.longitude - currentCoordinate.longitude); + return deltaA < deltaB; + }); + if (nearbyAnnotations == _annotationsNearbyLastTap) { - // The first selection in the cycle should be the one nearest to the - // tap. - CLLocationCoordinate2D currentCoordinate = [self convertPoint:point toCoordinateFromView:self]; - std::sort(nearbyAnnotations.begin(), nearbyAnnotations.end(), [&](const MGLAnnotationTag tagA, const MGLAnnotationTag tagB) { - CLLocationCoordinate2D coordinateA = [[self annotationWithTag:tagA] coordinate]; - CLLocationCoordinate2D coordinateB = [[self annotationWithTag:tagB] coordinate]; - CLLocationDegrees deltaA = hypot(coordinateA.latitude - currentCoordinate.latitude, - coordinateA.longitude - currentCoordinate.longitude); - CLLocationDegrees deltaB = hypot(coordinateB.latitude - currentCoordinate.latitude, - coordinateB.longitude - currentCoordinate.longitude); - return deltaA < deltaB; - }); - // The last time we persisted a set of annotations, we had the same // set of annotations as we do now. Cycle through them. if (_selectedAnnotationTag == MGLAnnotationTagNotFound - || _selectedAnnotationTag == _annotationsNearbyLastTap.back()) + || _selectedAnnotationTag == nearbyAnnotations.back()) { // Either no annotation is selected or the last annotation in // the set was selected. Wrap around to the first annotation in // the set. - hitAnnotationTag = _annotationsNearbyLastTap.front(); + hitAnnotationTag = nearbyAnnotations.front(); } else { - auto result = std::find(_annotationsNearbyLastTap.begin(), - _annotationsNearbyLastTap.end(), + auto result = std::find(nearbyAnnotations.begin(), + nearbyAnnotations.end(), _selectedAnnotationTag); - if (result == _annotationsNearbyLastTap.end()) + if (result == nearbyAnnotations.end()) { // An annotation from this set hasn’t been selected before. // Select the first (nearest) one. - hitAnnotationTag = _annotationsNearbyLastTap.front(); + hitAnnotationTag = nearbyAnnotations.front(); } else { // Step to the next annotation in the set. - auto distance = std::distance(_annotationsNearbyLastTap.begin(), result); - hitAnnotationTag = _annotationsNearbyLastTap[distance + 1]; + auto distance = std::distance(nearbyAnnotations.begin(), result); + hitAnnotationTag = nearbyAnnotations[distance + 1]; } } } @@ -4230,7 +4167,7 @@ public: { _annotationsNearbyLastTap = nearbyAnnotations; } - + // Choose the first nearby annotation. if (nearbyAnnotations.size()) { @@ -4259,6 +4196,12 @@ public: }); } + +- (BOOL)isBringingAnnotationOnscreenSupportedForAnnotation:(id<MGLAnnotation>)annotation animated:(BOOL)animated { + // Consider delegating + return animated && [annotation isKindOfClass:[MGLPointAnnotation class]]; +} + - (id <MGLAnnotation>)selectedAnnotation { if (_userLocationAnnotationIsSelected) @@ -4299,20 +4242,16 @@ public: if ([firstAnnotation isKindOfClass:[MGLMultiPoint class]]) return; - // Select the annotation if it’s visible. - if (MGLCoordinateInCoordinateBounds(firstAnnotation.coordinate, self.visibleCoordinateBounds)) - { - [self selectAnnotation:firstAnnotation animated:NO]; - } + [self selectAnnotation:firstAnnotation animated:YES]; } - (void)selectAnnotation:(id <MGLAnnotation>)annotation animated:(BOOL)animated { CGRect positioningRect = [self positioningRectForAnnotation:annotation defaultCalloutPoint:CGPointZero]; - [self selectAnnotation:annotation animated:animated calloutPositioningRect:positioningRect]; + [self selectAnnotation:annotation moveOnscreen:animated animateSelection:YES calloutPositioningRect:positioningRect]; } -- (void)selectAnnotation:(id <MGLAnnotation>)annotation animated:(BOOL)animated calloutPositioningRect:(CGRect)calloutPositioningRect +- (void)selectAnnotation:(id <MGLAnnotation>)annotation moveOnscreen:(BOOL)moveOnscreen animateSelection:(BOOL)animateSelection calloutPositioningRect:(CGRect)calloutPositioningRect { if ( ! annotation) return; @@ -4336,22 +4275,34 @@ public: MGLAnnotationContext &annotationContext = _annotationContextsByAnnotationTag.at(annotationTag); annotationView = annotationContext.annotationView; if (annotationView && annotationView.enabled) { - // Annotations represented by views use the view frame as the positioning rect. - calloutPositioningRect = annotationView.frame; - [annotationView.superview bringSubviewToFront:annotationView]; - [annotationView setSelected:YES animated:animated]; + // Annotations represented by views use the view frame as the positioning rect. + calloutPositioningRect = annotationView.frame; + [annotationView.superview bringSubviewToFront:annotationView]; + + [annotationView setSelected:YES animated:animateSelection]; } } self.selectedAnnotation = annotation; + // Determine if we're allowed to move this offscreen annotation on screen, even though we've asked it to + if (moveOnscreen) { + moveOnscreen = [self isBringingAnnotationOnscreenSupportedForAnnotation:annotation animated:animateSelection]; + } + + CGRect expandedPositioningRect = UIEdgeInsetsInsetRect(calloutPositioningRect, MGLMapViewOffscreenAnnotationPadding); + + // Used for callout positioning, and moving offscreen annotations onscreen. + CGRect constrainedRect = UIEdgeInsetsInsetRect(self.bounds, self.contentInset); + + UIView <MGLCalloutView> *calloutView = nil; + if ([annotation respondsToSelector:@selector(title)] && annotation.title && [self.delegate respondsToSelector:@selector(mapView:annotationCanShowCallout:)] && [self.delegate mapView:self annotationCanShowCallout:annotation]) { // build the callout - UIView <MGLCalloutView> *calloutView; if ([self.delegate respondsToSelector:@selector(mapView:calloutViewForAnnotation:)]) { id providedCalloutView = [self.delegate mapView:self calloutViewForAnnotation:annotation]; @@ -4409,13 +4360,51 @@ public: // set annotation delegate to handle taps on the callout view calloutView.delegate = self; - // present popup - [calloutView presentCalloutFromRect:calloutPositioningRect - inView:self.glView - constrainedToView:self.glView - animated:animated]; + // If the callout view provides inset (outset) information, we can use it to expand our positioning + // rect, which we then use to help move the annotation on-screen if want need to. + if (moveOnscreen && [calloutView respondsToSelector:@selector(marginInsetsHintForPresentationFromRect:)]) { + UIEdgeInsets margins = [calloutView marginInsetsHintForPresentationFromRect:calloutPositioningRect]; + expandedPositioningRect = UIEdgeInsetsInsetRect(expandedPositioningRect, margins); + } + } + + if (moveOnscreen) + { + moveOnscreen = NO; + + // Need to consider the content insets. + CGRect bounds = UIEdgeInsetsInsetRect(self.bounds, self.contentInset); + + // Any one of these cases should trigger a move onscreen + if (CGRectGetMinX(calloutPositioningRect) < CGRectGetMinX(bounds)) + { + constrainedRect.origin.x = expandedPositioningRect.origin.x; + moveOnscreen = YES; + } + else if (CGRectGetMaxX(calloutPositioningRect) > CGRectGetMaxX(bounds)) + { + constrainedRect.origin.x = CGRectGetMaxX(expandedPositioningRect) - constrainedRect.size.width; + moveOnscreen = YES; + } + + if (CGRectGetMinY(calloutPositioningRect) < CGRectGetMinY(bounds)) + { + constrainedRect.origin.y = expandedPositioningRect.origin.y; + moveOnscreen = YES; + } + else if (CGRectGetMaxY(calloutPositioningRect) > CGRectGetMaxY(bounds)) + { + constrainedRect.origin.y = CGRectGetMaxY(expandedPositioningRect) - constrainedRect.size.height; + moveOnscreen = YES; + } } + // Remember, calloutView can be nil here. + [calloutView presentCalloutFromRect:calloutPositioningRect + inView:self.glView + constrainedToRect:constrainedRect + animated:animateSelection]; + // notify delegate if ([self.delegate respondsToSelector:@selector(mapView:didSelectAnnotation:)]) { @@ -4426,6 +4415,13 @@ public: { [self.delegate mapView:self didSelectAnnotationView:annotationView]; } + + if (moveOnscreen) + { + CGPoint center = CGPointMake(CGRectGetMidX(constrainedRect), CGRectGetMidY(constrainedRect)); + CLLocationCoordinate2D centerCoord = [self convertPoint:center toCoordinateFromView:self]; + [self setCenterCoordinate:centerCoord animated:animateSelection]; + } } - (MGLCompactCalloutView *)calloutViewForAnnotation:(id <MGLAnnotation>)annotation @@ -4616,6 +4612,7 @@ public: animated:animated]; } + #pragma mark Annotation Image Delegate - (void)annotationImageNeedsRedisplay:(MGLAnnotationImage *)annotationImage @@ -4625,11 +4622,6 @@ public: NSString *fallbackReuseIdentifier = MGLDefaultStyleMarkerSymbolName; NSString *fallbackIconIdentifier = [MGLAnnotationSpritePrefix stringByAppendingString:fallbackReuseIdentifier]; - // Remove the old icon from the style. - if ( ! [iconIdentifier isEqualToString:fallbackIconIdentifier]) { - _mbglMap->removeAnnotationImage(iconIdentifier.UTF8String); - } - if (annotationImage.image) { // Add the new icon to the style. @@ -4746,12 +4738,8 @@ public: userLocationAnnotationView = (MGLUserLocationAnnotationView *)[self.delegate mapView:self viewForAnnotation:self.userLocation]; if (userLocationAnnotationView && ! [userLocationAnnotationView isKindOfClass:MGLUserLocationAnnotationView.class]) { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - NSLog(@"Ignoring user location annotation view with type %@. User location annotation view must be a kind of MGLUserLocationAnnotationView. This warning is only shown once and will become an error in a future version.", NSStringFromClass(userLocationAnnotationView.class)); - }); - - userLocationAnnotationView = nil; + [NSException raise:@"MGLUserLocationAnnotationTypeException" + format:@"User location annotation view must be a kind of MGLUserLocationAnnotationView. %@", userLocationAnnotationView.debugDescription]; } } @@ -5455,10 +5443,7 @@ public: } [self updateCompass]; - - if (!self.scaleBar.hidden) { - [(MGLScaleBar *)self.scaleBar setMetersPerPoint:[self metersPerPointAtLatitude:self.centerCoordinate.latitude]]; - } + [self updateScaleBar]; if ([self.delegate respondsToSelector:@selector(mapView:regionIsChangingWithReason:)]) { @@ -5476,6 +5461,7 @@ public: } [self updateCompass]; + [self updateScaleBar]; if ( ! [self isSuppressingChangeDelimiters]) { @@ -5920,6 +5906,17 @@ public: } } +- (void)updateScaleBar +{ + // Use the `hidden` property (instead of `self.showsScale`) so that we don't + // break developers who still rely on the <4.0.0 approach of directly + // setting this property. + if ( ! self.scaleBar.hidden) + { + [(MGLScaleBar *)self.scaleBar setMetersPerPoint:[self metersPerPointAtLatitude:self.centerCoordinate.latitude]]; + } +} + + (UIImage *)resourceImageNamed:(NSString *)imageName { UIImage *image = [UIImage imageNamed:imageName |