summaryrefslogtreecommitdiff
path: root/platform
diff options
context:
space:
mode:
authorJesse Bounds <jesse@rebounds.net>2016-06-27 13:54:58 -0700
committerJesse Bounds <jesse@rebounds.net>2016-06-27 14:15:10 -0700
commit1fa1819de1ffadc74c07972b7195cfdaea663507 (patch)
treec29cbec1e727a245629cfeb85233ad9c79c6c2fb /platform
parent8956179ce06bca099fca765ce6172980dfe7a998 (diff)
parent2d9ffcbadbfa0d6f2ecf16e0a33457a17017f583 (diff)
downloadqtlocation-mapboxgl-1fa1819de1ffadc74c07972b7195cfdaea663507.tar.gz
[ios] Merge branch 'release-ios-v3.3.0'
This merges v3.3.0 at beta 2 stage. Conflicts related to the os x -> macOS rename were corrected manually.
Diffstat (limited to 'platform')
-rw-r--r--platform/ios/Mapbox-iOS-SDK-symbols.podspec2
-rw-r--r--platform/ios/Mapbox-iOS-SDK.podspec2
-rw-r--r--platform/ios/app/MBXAnnotationView.m36
-rw-r--r--platform/ios/app/MBXViewController.m14
-rw-r--r--platform/ios/ios.xcodeproj/project.pbxproj4
-rw-r--r--platform/ios/src/MGLAnnotationView.h56
-rw-r--r--platform/ios/src/MGLAnnotationView.mm151
-rw-r--r--platform/ios/src/MGLAnnotationView_Private.h5
-rw-r--r--platform/ios/src/MGLMapView.h9
-rw-r--r--platform/ios/src/MGLMapView.mm152
-rw-r--r--platform/ios/src/MGLMapViewDelegate.h33
-rw-r--r--platform/ios/src/MGLMapboxEvents.m24
-rw-r--r--platform/ios/test/MGLAnnotationViewTests.m62
13 files changed, 502 insertions, 48 deletions
diff --git a/platform/ios/Mapbox-iOS-SDK-symbols.podspec b/platform/ios/Mapbox-iOS-SDK-symbols.podspec
index 3642d8b7de..022902a3fd 100644
--- a/platform/ios/Mapbox-iOS-SDK-symbols.podspec
+++ b/platform/ios/Mapbox-iOS-SDK-symbols.podspec
@@ -1,7 +1,7 @@
Pod::Spec.new do |m|
m.name = 'Mapbox-iOS-SDK'
- m.version = '3.3.0-beta.1-symbols'
+ m.version = '3.3.0-beta.2-symbols'
m.summary = 'Open source vector map solution for iOS with full styling capabilities.'
m.description = 'Open source, OpenGL-based vector map solution for iOS with full styling capabilities and Cocoa Touch APIs.'
diff --git a/platform/ios/Mapbox-iOS-SDK.podspec b/platform/ios/Mapbox-iOS-SDK.podspec
index da9213dba2..df9020671b 100644
--- a/platform/ios/Mapbox-iOS-SDK.podspec
+++ b/platform/ios/Mapbox-iOS-SDK.podspec
@@ -1,7 +1,7 @@
Pod::Spec.new do |m|
m.name = 'Mapbox-iOS-SDK'
- m.version = '3.3.0-beta.1'
+ m.version = '3.3.0-beta.2'
m.summary = 'Open source vector map solution for iOS with full styling capabilities.'
m.description = 'Open source, OpenGL-based vector map solution for iOS with full styling capabilities and Cocoa Touch APIs.'
diff --git a/platform/ios/app/MBXAnnotationView.m b/platform/ios/app/MBXAnnotationView.m
index 890881a316..c181211431 100644
--- a/platform/ios/app/MBXAnnotationView.m
+++ b/platform/ios/app/MBXAnnotationView.m
@@ -25,4 +25,40 @@
}
}
+- (void)setSelected:(BOOL)selected animated:(BOOL)animated
+{
+ [super setSelected:selected animated:animated];
+
+ self.layer.borderColor = selected ? [UIColor blackColor].CGColor : [UIColor whiteColor].CGColor;
+ self.layer.borderWidth = selected ? 2.0 : 0;
+}
+
+- (void)setDragState:(MGLAnnotationViewDragState)dragState animated:(BOOL)animated
+{
+ [super setDragState:dragState animated:NO];
+
+ switch (dragState) {
+ case MGLAnnotationViewDragStateNone:
+ break;
+ case MGLAnnotationViewDragStateStarting: {
+ [UIView animateWithDuration:.4 delay:0 usingSpringWithDamping:.4 initialSpringVelocity:.5 options:UIViewAnimationOptionCurveLinear animations:^{
+ self.transform = CGAffineTransformScale(CGAffineTransformIdentity, 2, 2);
+ } completion:nil];
+ break;
+ }
+ case MGLAnnotationViewDragStateDragging:
+ break;
+ case MGLAnnotationViewDragStateCanceling:
+ break;
+ case MGLAnnotationViewDragStateEnding: {
+ [UIView animateWithDuration:.4 delay:0 usingSpringWithDamping:.4 initialSpringVelocity:.5 options:UIViewAnimationOptionCurveLinear animations:^{
+ self.transform = CGAffineTransformScale(CGAffineTransformIdentity, 1, 1);
+ } completion:nil];
+ break;
+ }
+ }
+
+}
+
+
@end
diff --git a/platform/ios/app/MBXViewController.m b/platform/ios/app/MBXViewController.m
index 761644b29f..cd5694d835 100644
--- a/platform/ios/app/MBXViewController.m
+++ b/platform/ios/app/MBXViewController.m
@@ -411,6 +411,7 @@ static NSString * const MBXViewControllerAnnotationViewReuseIdentifer = @"MBXVie
{
if (longPress.state == UIGestureRecognizerStateBegan)
{
+ /*
CGPoint point = [longPress locationInView:longPress.view];
NSArray *features = [self.mapView visibleFeaturesAtPoint:point];
NSString *title;
@@ -427,6 +428,7 @@ static NSString * const MBXViewControllerAnnotationViewReuseIdentifer = @"MBXVie
pin.subtitle = [[[MGLCoordinateFormatter alloc] init] stringFromCoordinate:pin.coordinate];
// Calling `addAnnotation:` on mapView is not required since `selectAnnotation:animated` has the side effect of adding the annotation if required
[self.mapView selectAnnotation:pin animated:YES];
+ */
}
}
@@ -590,6 +592,12 @@ static NSString * const MBXViewControllerAnnotationViewReuseIdentifer = @"MBXVie
// uncomment to flatten the annotation view against the map when the map is tilted
// this currently causes severe performance issues when more than 2k annotations are visible
// annotationView.flat = YES;
+
+ // uncomment to make the annotation view draggable
+ // also note that having two long press gesture recognizers on overlapping views (`self.view` & `annotationView`) will cause weird behaviour
+ // comment out the pin dropping functionality in the handleLongPress: method in this class to make draggable annotation views play nice
+ annotationView.draggable = YES;
+
// uncomment to force annotation view to maintain a constant size when the map is tilted
// by default, annotation views will shrink and grow as the move towards and away from the
@@ -603,6 +611,12 @@ static NSString * const MBXViewControllerAnnotationViewReuseIdentifer = @"MBXVie
return annotationView;
}
+- (void)mapView:(MGLMapView *)mapView didDragAnnotationView:(nonnull MGLAnnotationView *)annotationView toCoordinate:(CLLocationCoordinate2D)coordinate
+{
+ MGLPointAnnotation *annotation = (MGLPointAnnotation *)annotationView.annotation;
+ annotation.coordinate = coordinate;
+}
+
- (BOOL)mapView:(__unused MGLMapView *)mapView annotationCanShowCallout:(__unused id <MGLAnnotation>)annotation
{
return YES;
diff --git a/platform/ios/ios.xcodeproj/project.pbxproj b/platform/ios/ios.xcodeproj/project.pbxproj
index cb1bd70199..ec0957a4f5 100644
--- a/platform/ios/ios.xcodeproj/project.pbxproj
+++ b/platform/ios/ios.xcodeproj/project.pbxproj
@@ -7,6 +7,7 @@
objects = {
/* Begin PBXBuildFile section */
+ 353D23961D0B0DFE002BE09D /* MGLAnnotationViewTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 353D23951D0B0DFE002BE09D /* MGLAnnotationViewTests.m */; };
4018B1C71CDC287F00F666AF /* MGLAnnotationView.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4018B1C41CDC277F00F666AF /* MGLAnnotationView.mm */; };
4018B1C81CDC287F00F666AF /* MGLAnnotationView.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4018B1C41CDC277F00F666AF /* MGLAnnotationView.mm */; };
4018B1C91CDC288A00F666AF /* MGLAnnotationView_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 4018B1C31CDC277F00F666AF /* MGLAnnotationView_Private.h */; };
@@ -325,6 +326,7 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
+ 353D23951D0B0DFE002BE09D /* MGLAnnotationViewTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MGLAnnotationViewTests.m; sourceTree = "<group>"; };
4018B1C31CDC277F00F666AF /* MGLAnnotationView_Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGLAnnotationView_Private.h; sourceTree = "<group>"; };
4018B1C41CDC277F00F666AF /* MGLAnnotationView.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = MGLAnnotationView.mm; sourceTree = "<group>"; };
4018B1C51CDC277F00F666AF /* MGLAnnotationView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGLAnnotationView.h; sourceTree = "<group>"; };
@@ -653,6 +655,7 @@
DA2E885D1CC0382C00F24E7B /* MGLOfflinePackTests.m */,
DA2E885E1CC0382C00F24E7B /* MGLOfflineRegionTests.m */,
DA2E885F1CC0382C00F24E7B /* MGLOfflineStorageTests.m */,
+ 353D23951D0B0DFE002BE09D /* MGLAnnotationViewTests.m */,
DA2E88601CC0382C00F24E7B /* MGLStyleTests.mm */,
DA2E88551CC036F400F24E7B /* Info.plist */,
);
@@ -1351,6 +1354,7 @@
DA35A2C51CCA9F8300E826B2 /* MGLClockDirectionFormatterTests.m in Sources */,
DA2E88621CC0382C00F24E7B /* MGLOfflinePackTests.m in Sources */,
DA35A2AA1CCA058D00E826B2 /* MGLCoordinateFormatterTests.m in Sources */,
+ 353D23961D0B0DFE002BE09D /* MGLAnnotationViewTests.m in Sources */,
DA0CD5901CF56F6A00A5F5A5 /* MGLFeatureTests.mm in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
diff --git a/platform/ios/src/MGLAnnotationView.h b/platform/ios/src/MGLAnnotationView.h
index 5b8091e7b4..18e4985884 100644
--- a/platform/ios/src/MGLAnnotationView.h
+++ b/platform/ios/src/MGLAnnotationView.h
@@ -4,6 +4,16 @@
NS_ASSUME_NONNULL_BEGIN
+@protocol MGLAnnotation;
+
+typedef NS_ENUM(NSUInteger, MGLAnnotationViewDragState) {
+ MGLAnnotationViewDragStateNone = 0, // View is sitting on the map.
+ MGLAnnotationViewDragStateStarting, // View is beginning to drag.
+ MGLAnnotationViewDragStateDragging, // View is being dragged.
+ MGLAnnotationViewDragStateCanceling, // View dragging was cancelled and will be returned to its starting positon.
+ MGLAnnotationViewDragStateEnding // View was dragged.
+};
+
/** The MGLAnnotationView class is responsible for representing point-based annotation markers as a view. Annotation views represent an annotation object, which is an object that corresponds to the MGLAnnotation protocol. When an annotation’s coordinate point is visible on the map view, the map view delegate is asked to provide a corresponding annotation view. If an annotation view is created with a reuse identifier, the map view may recycle the view when it goes offscreen. */
@interface MGLAnnotationView : UIView
@@ -16,6 +26,13 @@ NS_ASSUME_NONNULL_BEGIN
- (instancetype)initWithReuseIdentifier:(nullable NSString *)reuseIdentifier;
/**
+ This property will be set to the associated annotation when the view is visible.
+
+ When the view is queued and waiting to be reused, the value will be set to nil.
+ */
+@property (nonatomic, readonly, nullable) id <MGLAnnotation> annotation;
+
+/**
The string that identifies that this annotation view is reusable. (read-only)
You specify the reuse identifier when you create the view. You use the identifier later to retrieve an annotation view that was
@@ -41,6 +58,45 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, assign, getter=isFlat) BOOL flat;
/**
+ Defaults to NO and becomes YES when the view is tapped on.
+
+ Selecting another view will first deselect the currently selected view.
+ This property should not be changed directly.
+ */
+@property (nonatomic, assign, getter=isSelected) BOOL selected;
+
+/**
+ Subclasses may override this method in order to customize appearance.
+ This method should not be called directly.
+ */
+- (void)setSelected:(BOOL)selected animated:(BOOL)animated;
+
+/*
+ This property defaults to YES. Setting it to NO will cause the annotation view to ignore all touch events.
+ Subclasses may use this property to customize the appearance.
+ */
+@property (nonatomic, assign, getter=isEnabled) BOOL enabled;
+
+/**
+ Setting this property to YES will make the view draggable. Long-press followed by a pan gesture will start to move the
+ view around the map. `-mapView:didDragAnnotationView:toCoordinate:` will be called when a view is dropped.
+ */
+@property (nonatomic, assign, getter=isDraggable) BOOL draggable;
+
+/**
+ All states are handled automatically when `draggable` is set to YES.
+ Custom animations can be achieved by overriding setDragState:animated:
+ */
+@property (nonatomic, readonly) MGLAnnotationViewDragState dragState;
+
+/**
+ Called when the `dragState` changes.
+
+ Implementer may override this method in order to customize animations in subclasses.
+ */
+- (void)setDragState:(MGLAnnotationViewDragState)dragState animated:(BOOL)animated NS_REQUIRES_SUPER;
+
+/**
Setting this property to YES will cause the annotation view to shrink as it approaches the horizon and grow as it moves away from the
horizon when the associated map view is tilted. Conversely, setting this property to NO will ensure that the annotation view maintains
a constant size even when the map view is tilted. To maintain consistency with annotation representations that are not backed by an
diff --git a/platform/ios/src/MGLAnnotationView.mm b/platform/ios/src/MGLAnnotationView.mm
index 31657dbf4e..e086e3bde5 100644
--- a/platform/ios/src/MGLAnnotationView.mm
+++ b/platform/ios/src/MGLAnnotationView.mm
@@ -1,15 +1,19 @@
#import "MGLAnnotationView.h"
#import "MGLAnnotationView_Private.h"
+#import "MGLAnnotation.h"
#import "MGLMapView_Internal.h"
#import "NSBundle+MGLAdditions.h"
#include <mbgl/util/constants.hpp>
-@interface MGLAnnotationView ()
+@interface MGLAnnotationView () <UIGestureRecognizerDelegate>
-@property (nonatomic) id<MGLAnnotation> annotation;
@property (nonatomic, readwrite, nullable) NSString *reuseIdentifier;
+@property (nonatomic, readwrite, nullable) id <MGLAnnotation> annotation;
+@property (nonatomic, weak) UIPanGestureRecognizer *panGestureRecognizer;
+@property (nonatomic, weak) UILongPressGestureRecognizer *longPressRecognizer;
+@property (nonatomic, weak) MGLMapView *mapView;
@end
@@ -22,6 +26,7 @@
{
_reuseIdentifier = [reuseIdentifier copy];
_scalesWithViewingDistance = YES;
+ _enabled = YES;
}
return self;
}
@@ -37,6 +42,18 @@
self.center = self.center;
}
+- (void)setSelected:(BOOL)selected
+{
+ [self setSelected:selected animated:NO];
+}
+
+- (void)setSelected:(BOOL)selected animated:(BOOL)animated
+{
+ [self willChangeValueForKey:@"selected"];
+ _selected = selected;
+ [self didChangeValueForKey:@"selected"];
+}
+
- (void)setCenter:(CGPoint)center
{
[self setCenter:center pitch:0];
@@ -49,6 +66,11 @@
[super setCenter:center];
+ // Omit applying a new transformation while the view is being dragged.
+ if (self.dragState == MGLAnnotationViewDragStateDragging) {
+ return;
+ }
+
if (self.flat)
{
[self updatePitch:pitch];
@@ -95,6 +117,129 @@
}
}
+#pragma mark - Draggable
+
+- (void)setDraggable:(BOOL)draggable
+{
+ [self willChangeValueForKey:@"draggable"];
+ _draggable = draggable;
+ [self didChangeValueForKey:@"draggable"];
+
+ if (draggable)
+ {
+ [self enableDrag];
+ }
+ else
+ {
+ [self disableDrag];
+ }
+}
+
+- (void)enableDrag
+{
+ if (!_longPressRecognizer)
+ {
+ UILongPressGestureRecognizer *recognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPress:)];
+ recognizer.delegate = self;
+ [self addGestureRecognizer:recognizer];
+ _longPressRecognizer = recognizer;
+ }
+
+ if (!_panGestureRecognizer)
+ {
+ UIPanGestureRecognizer *recognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePan:)];
+ recognizer.delegate = self;
+ [self addGestureRecognizer:recognizer];
+ _panGestureRecognizer = recognizer;
+ }
+}
+
+- (void)disableDrag
+{
+ [self removeGestureRecognizer:_longPressRecognizer];
+ [self removeGestureRecognizer:_panGestureRecognizer];
+}
+
+- (void)handleLongPress:(UILongPressGestureRecognizer *)sender
+{
+ switch (sender.state) {
+ case UIGestureRecognizerStateBegan:
+ self.dragState = MGLAnnotationViewDragStateStarting;
+ break;
+ case UIGestureRecognizerStateChanged:
+ self.dragState = MGLAnnotationViewDragStateDragging;
+ break;
+ case UIGestureRecognizerStateCancelled:
+ self.dragState = MGLAnnotationViewDragStateCanceling;
+ break;
+ case UIGestureRecognizerStateEnded:
+ self.dragState = MGLAnnotationViewDragStateEnding;
+ break;
+ case UIGestureRecognizerStateFailed:
+ self.dragState = MGLAnnotationViewDragStateNone;
+ break;
+ case UIGestureRecognizerStatePossible:
+ break;
+ }
+}
+
+- (void)handlePan:(UIPanGestureRecognizer *)sender
+{
+ CGPoint center = [sender locationInView:sender.view.superview];
+ [self setCenter:center pitch:self.mapView.camera.pitch];
+
+ if (sender.state == UIGestureRecognizerStateEnded) {
+ self.dragState = MGLAnnotationViewDragStateNone;
+ }
+}
+
+- (void)setDragState:(MGLAnnotationViewDragState)dragState
+{
+ [self setDragState:dragState animated:YES];
+}
+
+- (void)setDragState:(MGLAnnotationViewDragState)dragState animated:(BOOL)animated
+{
+ [self willChangeValueForKey:@"dragState"];
+ _dragState = dragState;
+ [self didChangeValueForKey:@"dragState"];
+
+ if (dragState == MGLAnnotationViewDragStateStarting)
+ {
+ [self.superview bringSubviewToFront:self];
+ }
+
+ if (dragState == MGLAnnotationViewDragStateEnding)
+ {
+ if ([self.mapView.delegate respondsToSelector:@selector(mapView:didDragAnnotationView:toCoordinate:)])
+ {
+ CGPoint offsetAdjustedCenter = self.center;
+ offsetAdjustedCenter.x -= self.centerOffset.dx;
+ offsetAdjustedCenter.y -= self.centerOffset.dy;
+
+ CLLocationCoordinate2D coordinate = [self.mapView convertPoint:offsetAdjustedCenter toCoordinateFromView:self.mapView];
+ [self.mapView.delegate mapView:self.mapView didDragAnnotationView:self toCoordinate:coordinate];
+ }
+ }
+}
+
+- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
+{
+ BOOL isDragging = self.dragState == MGLAnnotationViewDragStateDragging;
+
+ if ([gestureRecognizer isKindOfClass:UIPanGestureRecognizer.class] && !(isDragging))
+ {
+ return NO;
+ }
+
+ return YES;
+}
+
+- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
+{
+ return YES;
+}
+
- (id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event
{
// Allow mbgl to drive animation of this view’s bounds.
@@ -144,4 +289,4 @@
[self.superview accessibilityDecrement];
}
-@end \ No newline at end of file
+@end
diff --git a/platform/ios/src/MGLAnnotationView_Private.h b/platform/ios/src/MGLAnnotationView_Private.h
index c5a65487a2..8f4f4fc17a 100644
--- a/platform/ios/src/MGLAnnotationView_Private.h
+++ b/platform/ios/src/MGLAnnotationView_Private.h
@@ -3,10 +3,13 @@
NS_ASSUME_NONNULL_BEGIN
+@class MGLMapView;
+
@interface MGLAnnotationView (Private)
-@property (nonatomic) id<MGLAnnotation> annotation;
@property (nonatomic, readwrite, nullable) NSString *reuseIdentifier;
+@property (nonatomic, readwrite, nullable) id <MGLAnnotation> annotation;
+@property (nonatomic, weak) MGLMapView *mapView;
- (void)setCenter:(CGPoint)center pitch:(CGFloat)pitch;
diff --git a/platform/ios/src/MGLMapView.h b/platform/ios/src/MGLMapView.h
index 24ddd13817..3f649e45d2 100644
--- a/platform/ios/src/MGLMapView.h
+++ b/platform/ios/src/MGLMapView.h
@@ -953,6 +953,15 @@ IB_DESIGNABLE
- (void)removeAnnotations:(NS_ARRAY_OF(id <MGLAnnotation>) *)annotations;
/**
+ Returns an `MGLAnnotationView` if the given annotation is currently associated
+ with a view, otherwise nil.
+
+ @param annotation The annotation associated with the view.
+ Annotation must conform to the `MGLAnnotation` protocol.
+ */
+- (nullable MGLAnnotationView *)viewForAnnotation:(id <MGLAnnotation>)annotation;
+
+/**
Returns a reusable annotation image object associated with its identifier.
For performance reasons, you should generally reuse `MGLAnnotationImage`
diff --git a/platform/ios/src/MGLMapView.mm b/platform/ios/src/MGLMapView.mm
index cde877407b..a036e6d812 100644
--- a/platform/ios/src/MGLMapView.mm
+++ b/platform/ios/src/MGLMapView.mm
@@ -88,6 +88,10 @@ const CLLocationDirection MGLToleranceForSnappingToNorth = 7;
/// Reuse identifier and file name of the default point annotation image.
static NSString * const MGLDefaultStyleMarkerSymbolName = @"default_marker";
+/// Reuse identifier and file name of the invisible point annotation image used
+/// by annotations that are visually backed by MGLAnnotationView objects
+static NSString * const MGLInvisibleStyleMarkerSymbolName = @"invisible_marker";
+
/// Prefix that denotes a sprite installed by MGLMapView, to avoid collisions
/// with style-defined sprites.
NSString *const MGLAnnotationSpritePrefix = @"com.mapbox.sprites.";
@@ -255,8 +259,9 @@ public:
NS_MUTABLE_DICTIONARY_OF(NSString *, NS_MUTABLE_ARRAY_OF(MGLAnnotationView *) *) *_annotationViewReuseQueueByIdentifier;
BOOL _userLocationAnnotationIsSelected;
- /// Size of the rectangle formed by unioning the maximum slop area around every annotation image.
- CGSize _unionedAnnotationImageSize;
+ /// Size of the rectangle formed by unioning the maximum slop area around every annotation image and annotation image view.
+ CGSize _unionedAnnotationRepresentationSize;
+ CGSize _largestAnnotationViewSize;
std::vector<MGLAnnotationTag> _annotationsNearbyLastTap;
CGPoint _initialImplicitCalloutViewOffset;
NSDate *_userLocationAnimationCompletionDate;
@@ -282,8 +287,6 @@ public:
BOOL _delegateHasLineWidthsForShapeAnnotations;
MGLCompassDirectionFormatter *_accessibilityCompassFormatter;
-
- CGSize _largestAnnotationViewSize;
}
#pragma mark - Setup & Teardown -
@@ -1413,13 +1416,6 @@ mbgl::Duration MGLDurationInSeconds(NSTimeInterval duration)
}
return;
}
-
- MGLAnnotationView *hitAnnotationView = [self annotationViewAtPoint:tapPoint];
- if (hitAnnotationView)
- {
- [self selectAnnotation:hitAnnotationView.annotation animated:YES];
- return;
- }
MGLAnnotationTag hitAnnotationTag = [self annotationTagAtPoint:tapPoint persistingResults:YES];
if (hitAnnotationTag != MGLAnnotationTagNotFound)
@@ -2866,8 +2862,17 @@ mbgl::Duration MGLDurationInSeconds(NSTimeInterval duration)
if (annotationView)
{
annotationViewsForAnnotation[annotationValue] = annotationView;
+ annotationView.annotation = annotation;
annotationView.center = [self convertCoordinate:annotation.coordinate toPointToView:self];
[newAnnotationViews addObject:annotationView];
+
+ MGLAnnotationImage *annotationImage = self.invisibleAnnotationImage;
+ symbolName = annotationImage.styleIconIdentifier;
+ annotationImagesForAnnotation[annotationValue] = annotationImage;
+ if ( ! self.annotationImagesByIdentifier[annotationImage.reuseIdentifier])
+ {
+ [self installAnnotationImage:annotationImage];
+ }
}
}
@@ -2904,22 +2909,21 @@ mbgl::Duration MGLDurationInSeconds(NSTimeInterval duration)
MGLAnnotationTag annotationTag = _mbglMap->addAnnotation(mbgl::SymbolAnnotation {
MGLPointFromLocationCoordinate2D(annotation.coordinate),
- symbolName.UTF8String ?: ""
+ symbolName.UTF8String
});
MGLAnnotationContext context;
context.annotation = annotation;
MGLAnnotationImage *annotationImage = annotationImagesForAnnotation[annotationValue];
+ context.imageReuseIdentifier = annotationImage.reuseIdentifier;
- if (annotationImage) {
- context.imageReuseIdentifier = annotationImage.reuseIdentifier;
- }
if (annotationView) {
context.annotationView = annotationView;
context.viewReuseIdentifier = annotationView.reuseIdentifier;
}
_annotationContextsByAnnotationTag[annotationTag] = context;
+
if ([annotation isKindOfClass:[NSObject class]]) {
NSAssert(![annotation isKindOfClass:[MGLMultiPoint class]], @"Point annotation should not be MGLMultiPoint.");
[(NSObject *)annotation addObserver:self forKeyPath:@"coordinate" options:0 context:(void *)(NSUInteger)annotationTag];
@@ -2969,6 +2973,23 @@ mbgl::Duration MGLDurationInSeconds(NSTimeInterval duration)
return annotationImage;
}
+- (MGLAnnotationImage *)invisibleAnnotationImage
+{
+ MGLAnnotationImage *annotationImage = [self dequeueReusableAnnotationImageWithIdentifier:MGLInvisibleStyleMarkerSymbolName];
+
+ if (!annotationImage)
+ {
+ UIGraphicsBeginImageContext(CGSizeMake(1, 1));
+ UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
+ UIGraphicsEndImageContext();
+ annotationImage = [MGLAnnotationImage annotationImageWithImage:image
+ reuseIdentifier:MGLInvisibleStyleMarkerSymbolName];
+ annotationImage.styleIconIdentifier = [MGLAnnotationSpritePrefix stringByAppendingString:annotationImage.reuseIdentifier];
+ }
+
+ return annotationImage;
+}
+
- (MGLAnnotationView *)annotationViewForAnnotation:(id<MGLAnnotation>)annotation
{
MGLAnnotationView *annotationView = [self.delegate mapView:self viewForAnnotation:annotation];
@@ -2977,12 +2998,26 @@ mbgl::Duration MGLDurationInSeconds(NSTimeInterval duration)
{
annotationView.annotation = annotation;
CGRect bounds = UIEdgeInsetsInsetRect({ CGPointZero, annotationView.frame.size }, annotationView.alignmentRectInsets);
- _largestAnnotationViewSize = CGSizeMake(bounds.size.width / 2.0, bounds.size.height / 2.0);
+
+ _largestAnnotationViewSize = CGSizeMake(MAX(_largestAnnotationViewSize.width, CGRectGetWidth(bounds)),
+ MAX(_largestAnnotationViewSize.height, CGRectGetHeight(bounds)));
+
+ _unionedAnnotationRepresentationSize = CGSizeMake(MAX(_unionedAnnotationRepresentationSize.width, _largestAnnotationViewSize.width),
+ MAX(_unionedAnnotationRepresentationSize.height, _largestAnnotationViewSize.height));
}
return annotationView;
}
+- (nullable MGLAnnotationView *)viewForAnnotation:(id<MGLAnnotation>)annotation
+{
+ MGLAnnotationTag annotationTag = [self annotationTagForAnnotation:annotation];
+ if (annotationTag == MGLAnnotationTagNotFound) return nil;
+
+ MGLAnnotationContext &annotationContext = _annotationContextsByAnnotationTag.at(annotationTag);
+ return annotationContext.annotationView;
+}
+
- (double)alphaForShapeAnnotation:(MGLShape *)annotation
{
if (_delegateHasAlphasForShapeAnnotations)
@@ -3050,8 +3085,8 @@ mbgl::Duration MGLDurationInSeconds(NSTimeInterval duration)
// within this image. Union this slop area with any existing slop areas.
CGRect bounds = UIEdgeInsetsInsetRect({ CGPointZero, annotationImage.image.size },
annotationImage.image.alignmentRectInsets);
- _unionedAnnotationImageSize = CGSizeMake(MAX(_unionedAnnotationImageSize.width, bounds.size.width),
- MAX(_unionedAnnotationImageSize.height, bounds.size.height));
+ _unionedAnnotationRepresentationSize = CGSizeMake(MAX(_unionedAnnotationRepresentationSize.width, bounds.size.width),
+ MAX(_unionedAnnotationRepresentationSize.height, bounds.size.height));
}
- (void)removeAnnotation:(id <MGLAnnotation>)annotation
@@ -3083,6 +3118,7 @@ mbgl::Duration MGLDurationInSeconds(NSTimeInterval duration)
MGLAnnotationContext &annotationContext = _annotationContextsByAnnotationTag.at(annotationTag);
MGLAnnotationView *annotationView = annotationContext.annotationView;
+ annotationView.annotation = nil;
[annotationView removeFromSuperview];
if (annotationTag == _selectedAnnotationTag)
@@ -3188,10 +3224,10 @@ mbgl::Duration MGLDurationInSeconds(NSTimeInterval duration)
{
// Look for any annotation near the tap. An annotation is “near” if the
// distance between its center and the tap is less than the maximum height
- // or width of an installed annotation image.
+ // or width of an installed annotation image or annotation view.
CGRect queryRect = CGRectInset({ point, CGSizeZero },
- -_unionedAnnotationImageSize.width,
- -_unionedAnnotationImageSize.height);
+ -_unionedAnnotationRepresentationSize.width,
+ -_unionedAnnotationRepresentationSize.height);
queryRect = CGRectInset(queryRect, -MGLAnnotationImagePaddingForHitTest,
-MGLAnnotationImagePaddingForHitTest);
std::vector<MGLAnnotationTag> nearbyAnnotations = [self annotationTagsInRect:queryRect];
@@ -3203,10 +3239,7 @@ mbgl::Duration MGLDurationInSeconds(NSTimeInterval duration)
-MGLAnnotationImagePaddingForHitTest,
-MGLAnnotationImagePaddingForHitTest);
- MGLAnnotationImage *fallbackAnnotationImage = [self dequeueReusableAnnotationImageWithIdentifier:MGLDefaultStyleMarkerSymbolName];
- UIImage *fallbackImage = fallbackAnnotationImage.image;
-
- // Filter out any annotation whose image is unselectable or for which
+ // Filter out any annotation whose image or view is unselectable or for which
// hit testing fails.
auto end = std::remove_if(nearbyAnnotations.begin(), nearbyAnnotations.end(),
[&](const MGLAnnotationTag annotationTag)
@@ -3215,17 +3248,36 @@ mbgl::Duration MGLDurationInSeconds(NSTimeInterval duration)
NSAssert(annotation, @"Unknown annotation found nearby tap");
MGLAnnotationContext annotationContext = _annotationContextsByAnnotationTag[annotationTag];
+ CGRect annotationRect;
- MGLAnnotationImage *annotationImage = [self imageOfAnnotationWithTag:annotationTag];
- if ( ! annotationImage.enabled)
+ MGLAnnotationView *annotationView = annotationContext.annotationView;
+ if (annotationView)
+ {
+ if ( ! annotationView.enabled)
+ {
+ return true;
+ }
+
+ CGPoint calloutAnchorPoint = [self convertCoordinate:annotation.coordinate toPointToView:self];
+ CGRect frame = CGRectInset({ calloutAnchorPoint, CGSizeZero }, -CGRectGetWidth(annotationView.frame) / 2, -CGRectGetHeight(annotationView.frame) / 2);
+ annotationRect = UIEdgeInsetsInsetRect(frame, annotationView.alignmentRectInsets);
+ }
+ else
{
- return true;
+ MGLAnnotationImage *annotationImage = [self imageOfAnnotationWithTag:annotationTag];
+ if ( ! annotationImage.enabled)
+ {
+ return true;
+ }
+
+ MGLAnnotationImage *fallbackAnnotationImage = [self dequeueReusableAnnotationImageWithIdentifier:MGLDefaultStyleMarkerSymbolName];
+ UIImage *fallbackImage = fallbackAnnotationImage.image;
+
+ annotationRect = [self frameOfImage:annotationImage.image ?: fallbackImage centeredAtCoordinate:annotation.coordinate];
}
// Filter out the annotation if the fattened finger didn’t land
// within the image’s alignment rect.
- CGRect annotationRect = [self frameOfImage:annotationImage.image ?: fallbackImage centeredAtCoordinate:annotation.coordinate];
-
return !!!CGRectIntersectsRect(annotationRect, hitRect);
});
@@ -3382,17 +3434,22 @@ mbgl::Duration MGLDurationInSeconds(NSTimeInterval duration)
// By default attempt to use the GL annotation image frame as the positioning rect.
CGRect positioningRect = [self positioningRectForCalloutForAnnotationWithTag:annotationTag];
+ MGLAnnotationView *annotationView = nil;
+
if (annotation != self.userLocation)
{
MGLAnnotationContext &annotationContext = _annotationContextsByAnnotationTag.at(annotationTag);
- MGLAnnotationView *annotationView = annotationContext.annotationView;
- if (annotationView)
+ annotationView = annotationContext.annotationView;
+
+ if (annotationView && annotationView.enabled)
{
// Annotations represented by views use the view frame as the positioning rect.
positioningRect = annotationView.frame;
[annotationView.superview bringSubviewToFront:annotationView];
+
+ annotationView.selected = YES;
}
}
@@ -3474,6 +3531,11 @@ mbgl::Duration MGLDurationInSeconds(NSTimeInterval duration)
{
[self.delegate mapView:self didSelectAnnotation:annotation];
}
+
+ if (annotationView && [self.delegate respondsToSelector:@selector(mapView:didSelectAnnotationView:)])
+ {
+ [self.delegate mapView:self didSelectAnnotationView:annotationView];
+ }
}
- (MGLCompactCalloutView *)calloutViewForAnnotation:(id <MGLAnnotation>)annotation
@@ -3546,6 +3608,17 @@ mbgl::Duration MGLDurationInSeconds(NSTimeInterval duration)
{
// dismiss popup
[self.calloutViewForSelectedAnnotation dismissCalloutAnimated:animated];
+
+ // deselect annotation view
+ MGLAnnotationView *annotationView = nil;
+ MGLAnnotationTag annotationTag = [self annotationTagForAnnotation:annotation];
+
+ if (annotationTag != MGLAnnotationTagNotFound)
+ {
+ MGLAnnotationContext &annotationContext = _annotationContextsByAnnotationTag.at(annotationTag);
+ annotationView = annotationContext.annotationView;
+ annotationView.selected = NO;
+ }
// clean up
self.calloutViewForSelectedAnnotation = nil;
@@ -3556,6 +3629,11 @@ mbgl::Duration MGLDurationInSeconds(NSTimeInterval duration)
{
[self.delegate mapView:self didDeselectAnnotation:annotation];
}
+
+ if (annotationView && [self.delegate respondsToSelector:@selector(mapView:didDeselectAnnotationView:)])
+ {
+ [self.delegate mapView:self didDeselectAnnotationView:annotationView];
+ }
}
}
@@ -4446,11 +4524,11 @@ mbgl::Duration MGLDurationInSeconds(NSTimeInterval duration)
case mbgl::MapChangeDidFinishRenderingFrame:
case mbgl::MapChangeDidFinishRenderingFrameFullyRendered:
{
+ [self updateAnnotationViews];
if ([self.delegate respondsToSelector:@selector(mapViewDidFinishRenderingFrame:fullyRendered:)])
{
[self.delegate mapViewDidFinishRenderingFrame:self fullyRendered:(change == mbgl::MapChangeDidFinishRenderingFrameFullyRendered)];
}
- [self updateAnnotationViews];
break;
}
}
@@ -4472,7 +4550,9 @@ mbgl::Duration MGLDurationInSeconds(NSTimeInterval duration)
for (auto &pair : _annotationContextsByAnnotationTag)
{
- CGRect viewPort = CGRectInset(self.bounds, -_largestAnnotationViewSize.width - MGLAnnotationUpdateViewportOutset.width, -_largestAnnotationViewSize.height - MGLAnnotationUpdateViewportOutset.width);
+ CGRect viewPort = CGRectInset(self.bounds,
+ -_largestAnnotationViewSize.width / 2.0 - MGLAnnotationUpdateViewportOutset.width / 2.0,
+ -_largestAnnotationViewSize.height / 2.0 - MGLAnnotationUpdateViewportOutset.width);
MGLAnnotationContext &annotationContext = pair.second;
MGLAnnotationView *annotationView = annotationContext.annotationView;
@@ -4490,7 +4570,7 @@ mbgl::Duration MGLDurationInSeconds(NSTimeInterval duration)
CGPoint center = [self convertCoordinate:annotationContext.annotation.coordinate toPointToView:self];
[annotationView setCenter:center pitch:self.camera.pitch];
-
+ annotationView.mapView = self;
annotationContext.annotationView = annotationView;
}
}
@@ -4514,6 +4594,8 @@ mbgl::Duration MGLDurationInSeconds(NSTimeInterval duration)
if (!annotationView) return;
+ annotationView.annotation = nil;
+
if (annotationContext.viewReuseIdentifier)
{
NSMutableArray *annotationViewReuseQueue = [self annotationViewReuseQueueForIdentifier:annotationContext.viewReuseIdentifier];
diff --git a/platform/ios/src/MGLMapViewDelegate.h b/platform/ios/src/MGLMapViewDelegate.h
index 39eb43d4ca..173b40f93f 100644
--- a/platform/ios/src/MGLMapViewDelegate.h
+++ b/platform/ios/src/MGLMapViewDelegate.h
@@ -279,6 +279,39 @@ NS_ASSUME_NONNULL_BEGIN
*/
- (void)mapView:(MGLMapView *)mapView didDeselectAnnotation:(id <MGLAnnotation>)annotation;
+
+/**
+ Tells the delegate that one of its annotation views was selected.
+
+ You can use this method to track changes in the selection state of annotation views.
+
+ @param mapView The map view containing the annotation.
+ @param annotationView The annotation view that was selected.
+ */
+- (void)mapView:(MGLMapView *)mapView didSelectAnnotationView:(MGLAnnotationView *)annotationView;
+
+/**
+ Tells the delegate that one of its annotation views was deselected.
+
+ You can use this method to track changes in the selection state of annotation views.
+
+ @param mapView The map view containing the annotation.
+ @param annotationView The annotation view that was deselected.
+ */
+- (void)mapView:(MGLMapView *)mapView didDeselectAnnotationView:(MGLAnnotationView *)annotationView;
+
+/**
+ Tells the delegate that one if its annotation views was dragged to a new coordinate.
+
+ In order to make the new location persistent, you have to update the `coordinate` property of the associated annotation.
+
+ @param mapView The map view containing the annotation view.
+ @param annotationView The annotation view that was dragged.
+ @param coordinate The coordinate that the annotation view was dropped on.
+
+ */
+- (void)mapView:(MGLMapView *)mapView didDragAnnotationView:(MGLAnnotationView *)annotationView toCoordinate:(CLLocationCoordinate2D)coordinate;
+
@end
NS_ASSUME_NONNULL_END
diff --git a/platform/ios/src/MGLMapboxEvents.m b/platform/ios/src/MGLMapboxEvents.m
index 5ddf4e2b57..53b917f701 100644
--- a/platform/ios/src/MGLMapboxEvents.m
+++ b/platform/ios/src/MGLMapboxEvents.m
@@ -147,11 +147,17 @@ const NSTimeInterval MGLFlushInterval = 180;
#if TARGET_OS_SIMULATOR
return NO;
#else
+ BOOL isLowPowerModeEnabled = NO;
+ if ([NSProcessInfo instancesRespondToSelector:@selector(isLowPowerModeEnabled)]) {
+ isLowPowerModeEnabled = [[NSProcessInfo processInfo] isLowPowerModeEnabled];
+ }
return ([[NSUserDefaults standardUserDefaults] boolForKey:@"MGLMapboxMetricsEnabled"] &&
- [[NSUserDefaults standardUserDefaults] integerForKey:@"MGLMapboxAccountType"] == 0);
+ [[NSUserDefaults standardUserDefaults] integerForKey:@"MGLMapboxAccountType"] == 0 &&
+ !isLowPowerModeEnabled);
#endif
}
+
- (BOOL)debugLoggingEnabled {
return (self.canEnableDebugLogging &&
[[NSUserDefaults standardUserDefaults] boolForKey:@"MGLMapboxMetricsDebugLoggingEnabled"]);
@@ -203,6 +209,11 @@ const NSTimeInterval MGLFlushInterval = 180;
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(pauseOrResumeMetricsCollectionIfRequired) name:UIApplicationDidEnterBackgroundNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(pauseOrResumeMetricsCollectionIfRequired) name:UIApplicationWillEnterForegroundNotification object:nil];
+
+ // Watch for Low Power Mode change events
+ if (&NSProcessInfoPowerStateDidChangeNotification != NULL) {
+ [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(pauseOrResumeMetricsCollectionIfRequired) name:NSProcessInfoPowerStateDidChangeNotification object:nil];
+ }
}
return self;
}
@@ -253,12 +264,11 @@ const NSTimeInterval MGLFlushInterval = 180;
return;
}
- // Toggle pause based on current pause state and current settings state
- // Practically, a pause only occurs because of a change to an NSUserDefaultsDidChangeNotification
- BOOL enabledInSettings = [[self class] isEnabled];
- if (self.paused && enabledInSettings) {
+ // Toggle pause based on current pause state, user opt-out state, and low-power state.
+ BOOL enabled = [[self class] isEnabled];
+ if (self.paused && enabled) {
[self resumeMetricsCollection];
- } else if (!self.paused && !enabledInSettings) {
+ } else if (!self.paused && !enabled) {
[self pauseMetricsCollection];
}
}
@@ -281,7 +291,7 @@ const NSTimeInterval MGLFlushInterval = 180;
if (!self.paused || ![[self class] isEnabled]) {
return;
}
-
+
self.paused = NO;
self.data = [[MGLMapboxEventsData alloc] init];
diff --git a/platform/ios/test/MGLAnnotationViewTests.m b/platform/ios/test/MGLAnnotationViewTests.m
new file mode 100644
index 0000000000..541c43b5a1
--- /dev/null
+++ b/platform/ios/test/MGLAnnotationViewTests.m
@@ -0,0 +1,62 @@
+#import <Mapbox/Mapbox.h>
+#import <XCTest/XCTest.h>
+
+static NSString * const MGLTestAnnotationReuseIdentifer = @"MGLTestAnnotationReuseIdentifer";
+
+@interface MGLTestAnnotation : NSObject <MGLAnnotation>
+@property (nonatomic, assign) CLLocationCoordinate2D coordinate;
+@end
+
+@implementation MGLTestAnnotation
+@end
+
+@interface MGLAnnotationViewTests : XCTestCase <MGLMapViewDelegate>
+@property (nonatomic) XCTestExpectation *expectation;
+@property (nonatomic) MGLMapView *mapView;
+@property (nonatomic, weak) MGLAnnotationView *annotationView;
+@end
+
+@implementation MGLAnnotationViewTests
+
+- (void)setUp
+{
+ [super setUp];
+ _mapView = [[MGLMapView alloc] initWithFrame:CGRectZero];
+ _mapView.delegate = self;
+}
+
+- (void)testAnnotationView
+{
+ _expectation = [self expectationWithDescription:@"annotation property"];
+
+ MGLTestAnnotation *annotation = [[MGLTestAnnotation alloc] init];
+ [_mapView addAnnotation:annotation];
+
+ [self waitForExpectationsWithTimeout:1 handler:nil];
+
+ XCTAssert(_mapView.annotations.count == 1, @"number of annotations should be 1");
+ XCTAssertNotNil(_annotationView.annotation, @"annotation property should not be nil");
+
+ [_mapView removeAnnotation:_annotationView.annotation];
+
+ XCTAssert(_mapView.annotations.count == 0, @"number of annotations should be 0");
+ XCTAssertNil(_annotationView.annotation, @"annotation property should be nil");
+}
+
+- (MGLAnnotationView *)mapView:(MGLMapView *)mapView viewForAnnotation:(id<MGLAnnotation>)annotation
+{
+ MGLAnnotationView *annotationView = [mapView dequeueReusableAnnotationViewWithIdentifier:MGLTestAnnotationReuseIdentifer];
+
+ if (!annotationView)
+ {
+ annotationView = [[MGLAnnotationView alloc] initWithReuseIdentifier:MGLTestAnnotationReuseIdentifer];
+ }
+
+ _annotationView = annotationView;
+
+ [_expectation fulfill];
+
+ return annotationView;
+}
+
+@end