diff options
Diffstat (limited to 'platform/ios/MGLMapView.mm')
-rw-r--r-- | platform/ios/MGLMapView.mm | 1399 |
1 files changed, 800 insertions, 599 deletions
diff --git a/platform/ios/MGLMapView.mm b/platform/ios/MGLMapView.mm index ce5997e0a9..4a55ada173 100644 --- a/platform/ios/MGLMapView.mm +++ b/platform/ios/MGLMapView.mm @@ -15,16 +15,15 @@ #include <mbgl/util/geo.hpp> #import "MGLTypes.h" -#import "MGLStyleFunctionValue.h" #import "MGLAnnotation.h" +#import "MGLUserLocationAnnotationView.h" +#import "MGLUserLocation_Private.h" -#import "UIColor+MGLAdditions.h" -#import "NSArray+MGLAdditions.h" -#import "NSDictionary+MGLAdditions.h" +#import "SMCalloutView.h" -#import <algorithm> #import "MGLMapboxEvents.h" -#import "MGLMetricsLocationManager.h" + +#import <algorithm> // Returns the path to the default cache database on this system. const std::string &defaultCacheDatabase() { @@ -47,24 +46,14 @@ NSString *const MGLDefaultStyleName = @"Emerald"; NSString *const MGLStyleVersion = @"v7"; NSString *const MGLDefaultStyleMarkerSymbolName = @"default_marker"; -extern NSString *const MGLStyleKeyGeneric; -extern NSString *const MGLStyleKeyFill; -extern NSString *const MGLStyleKeyLine; -extern NSString *const MGLStyleKeyIcon; -extern NSString *const MGLStyleKeyText; -extern NSString *const MGLStyleKeyRaster; -extern NSString *const MGLStyleKeyComposite; -extern NSString *const MGLStyleKeyBackground; - -extern NSString *const MGLStyleValueFunctionAllowed; - NSTimeInterval const MGLAnimationDuration = 0.3; +const CGSize MGLAnnotationUpdateViewportOutset = {150, 150}; NSString *const MGLAnnotationIDKey = @"MGLAnnotationIDKey"; #pragma mark - Private - -@interface MGLMapView () <UIGestureRecognizerDelegate, GLKViewDelegate> +@interface MGLMapView () <UIGestureRecognizerDelegate, GLKViewDelegate, CLLocationManagerDelegate> @property (nonatomic) EAGLContext *context; @property (nonatomic) GLKView *glView; @@ -77,10 +66,12 @@ NSString *const MGLAnnotationIDKey = @"MGLAnnotationIDKey"; @property (nonatomic) UIRotationGestureRecognizer *rotate; @property (nonatomic) UILongPressGestureRecognizer *quickZoom; @property (nonatomic) NSMutableArray *bundledStyleNames; -@property (nonatomic) NSMapTable *annotationsStore; +@property (nonatomic) NSMapTable *annotationIDsByAnnotation; @property (nonatomic) std::vector<uint32_t> annotationsNearbyLastTap; @property (nonatomic, weak) id <MGLAnnotation> selectedAnnotation; -@property (nonatomic, readonly) NSDictionary *allowedStyleTypes; +@property (nonatomic) SMCalloutView *selectedAnnotationCalloutView; +@property (nonatomic) MGLUserLocationAnnotationView *userLocationAnnotationView; +@property (nonatomic) CLLocationManager *locationManager; @property (nonatomic) CGPoint centerPoint; @property (nonatomic) CGFloat scale; @property (nonatomic) CGFloat angle; @@ -90,26 +81,8 @@ NSString *const MGLAnnotationIDKey = @"MGLAnnotationIDKey"; @end -@interface MGLStyleFunctionValue (MGLMapViewFriend) - -@property (nonatomic) NSString *functionType; -@property (nonatomic) NSDictionary *stops; -@property (nonatomic) CGFloat zBase; -@property (nonatomic) CGFloat val; -@property (nonatomic) CGFloat slope; -@property (nonatomic) CGFloat min; -@property (nonatomic) CGFloat max; -@property (nonatomic) CGFloat minimumZoom; -@property (nonatomic) CGFloat maximumZoom; - -- (id)rawStyle; - -@end - @implementation MGLMapView -@synthesize bundledStyleNames=_bundledStyleNames; - #pragma mark - Setup & Teardown - @dynamic debugActive; @@ -159,6 +132,19 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr; return self; } +- (instancetype)initWithFrame:(CGRect)frame accessToken:(NSString *)accessToken styleURL:(NSURL *)styleURL +{ + self = [super initWithFrame:frame]; + + if (self && [self commonInit]) + { + if (accessToken) [self setAccessToken:accessToken]; + if (styleURL) [self setStyleURL:styleURL]; + } + + return self; +} + - (instancetype)initWithFrame:(CGRect)frame accessToken:(NSString *)accessToken { return [self initWithFrame:frame accessToken:accessToken styleJSON:nil]; @@ -180,8 +166,8 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr; { if (accessToken) { - mbglMap->setAccessToken((std::string)[accessToken cStringUsingEncoding:[NSString defaultCStringEncoding]]); - [[MGLMapboxEvents sharedManager] setToken:accessToken]; + mbglMap->setAccessToken((std::string)[accessToken UTF8String]); + [MGLMapboxEvents setToken:accessToken]; } } @@ -195,13 +181,22 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr; } else { - mbglMap->setStyleJSON((std::string)[styleJSON cStringUsingEncoding:[NSString defaultCStringEncoding]]); + mbglMap->setStyleJSON((std::string)[styleJSON UTF8String]); } } -- (void)setStyleURL:(NSString *)filePathURL +- (void)setStyleURL:(NSURL *)styleURL { - mbglMap->setStyleURL(std::string("asset://") + [filePathURL UTF8String]); + std::string styleURLString([[styleURL absoluteString] UTF8String]); + + if ( ! [styleURL scheme]) + { + mbglMap->setStyleURL(std::string("asset://") + styleURLString); + } + else + { + mbglMap->setStyleURL(styleURLString); + } } - (BOOL)commonInit @@ -221,16 +216,11 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr; // self.accessibilityLabel = @"Map"; - // setup Metrics - MGLMapboxEvents *events = [MGLMapboxEvents sharedManager]; + // metrics: initial setup NSString *appName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleName"]; NSString *appVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"]; - if (appName != nil) { - events.appName = appName; - } - if (appVersion != nil) { - events.appVersion = appVersion; - } + if (appName != nil) [MGLMapboxEvents setAppName:appName]; + if (appVersion != nil) [MGLMapboxEvents setAppVersion:appVersion]; // create GL view // @@ -244,7 +234,7 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr; } _glView.delegate = self; [_glView bindDrawable]; - [self addSubview:_glView]; + [self insertSubview:_glView atIndex:0]; _glView.contentMode = UIViewContentModeCenter; [self setBackgroundColor:[UIColor clearColor]]; @@ -291,8 +281,8 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr; // setup annotations // - _annotationsStore = [NSMapTable mapTableWithKeyOptions:NSMapTableStrongMemory valueOptions:NSMapTableStrongMemory]; - std::string defaultSymbolName([MGLDefaultStyleMarkerSymbolName cStringUsingEncoding:[NSString defaultCStringEncoding]]); + _annotationIDsByAnnotation = [NSMapTable mapTableWithKeyOptions:NSMapTableStrongMemory valueOptions:NSMapTableStrongMemory]; + std::string defaultSymbolName([MGLDefaultStyleMarkerSymbolName UTF8String]); mbglMap->setDefaultPointAnnotationSymbol(defaultSymbolName); // setup logo bug @@ -371,9 +361,6 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(appDidBackground:) name:UIApplicationDidEnterBackgroundNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(appWillForeground:) name:UIApplicationWillEnterForegroundNotification object:nil]; - // Setup MBLocationManager for metrics - [MGLMetricsLocationManager sharedManager]; - // set initial position // mbglMap->setLatLngZoom(mbgl::LatLng(0, 0), mbglMap->getMinZoom()); @@ -386,28 +373,17 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr; // start the main loop mbglMap->start(); - - // Fire map.load on a background thread - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - - NSMutableDictionary *evt = [[NSMutableDictionary alloc] init]; - [evt setValue:[[NSNumber alloc] initWithDouble:mbglMap->getLatLng().latitude] forKey:@"lat"]; - [evt setValue:[[NSNumber alloc] initWithDouble:mbglMap->getLatLng().longitude] forKey:@"lng"]; - [evt setValue:[[NSNumber alloc] initWithDouble:mbglMap->getZoom()] forKey:@"zoom"]; - [[MGLMapboxEvents sharedManager] pushEvent:@"map.load" withAttributes:evt]; - - [evt setValue:[[NSNumber alloc] initWithBool:[[UIApplication sharedApplication] isRegisteredForRemoteNotifications]] forKey:@"enabled.push"]; - - NSString *email = @"Unknown"; - Class MFMailComposeViewController = NSClassFromString(@"MFMailComposeViewController"); - if (MFMailComposeViewController) { - SEL canSendMail = NSSelectorFromString(@"canSendMail"); - BOOL sendMail = ((BOOL (*)(id, SEL))[MFMailComposeViewController methodForSelector:canSendMail])(MFMailComposeViewController, canSendMail); - email = [NSString stringWithFormat:@"%i", sendMail]; - } - [evt setValue:email forKey:@"enabled.email"]; - }); - + // metrics: map load event + const mbgl::LatLng latLng = mbglMap->getLatLng(); + const double zoom = mbglMap->getZoom(); + + [MGLMapboxEvents pushEvent:MGLEventTypeMapLoad withAttributes:@{ + MGLEventKeyLatitude: @(latLng.latitude), + MGLEventKeyLongitude: @(latLng.longitude), + MGLEventKeyZoomLevel: @(zoom), + MGLEventKeyPushEnabled: @([MGLMapboxEvents checkPushEnabled]) + }]; + return YES; } @@ -449,6 +425,13 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr; } } +- (void)setDelegate:(id<MGLMapViewDelegate>)delegate +{ + if (_delegate == delegate) return; + + _delegate = delegate; +} + #pragma mark - Layout - - (void)setFrame:(CGRect)frame @@ -585,8 +568,7 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr; - (void)appDidBackground:(NSNotification *)notification { - // Flush Any Events Still In Queue - [[MGLMapboxEvents sharedManager] flush]; + [MGLMapboxEvents flush]; mbglMap->stop(); @@ -602,13 +584,14 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr; - (void)tintColorDidChange { - for (UIView *subview in self.subviews) - { - if ([subview respondsToSelector:@selector(setTintColor:)]) - { - subview.tintColor = self.tintColor; - } - } + for (UIView *subview in self.subviews) [self updateTintColorForView:subview]; +} + +- (void)updateTintColorForView:(UIView *)view +{ + if ([view respondsToSelector:@selector(setTintColor:)]) view.tintColor = self.tintColor; + + for (UIView *subview in view.subviews) [self updateTintColorForView:subview]; } #pragma mark - Gestures - @@ -616,21 +599,25 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr; - (void)handleCompassTapGesture:(id)sender { [self resetNorthAnimated:YES]; + + if (self.userTrackingMode == MGLUserTrackingModeFollowWithHeading) self.userTrackingMode = MGLUserTrackingModeFollow; } #pragma clang diagnostic pop - (void)handlePanGesture:(UIPanGestureRecognizer *)pan { - [self trackGestureEvent:@"Pan" forRecognizer:pan]; - if ( ! self.isScrollEnabled) return; mbglMap->cancelTransitions(); if (pan.state == UIGestureRecognizerStateBegan) { + [self trackGestureEvent:MGLEventGesturePanStart forRecognizer:pan]; + self.centerPoint = CGPointMake(0, 0); + + self.userTrackingMode = MGLUserTrackingModeNone; } else if (pan.state == UIGestureRecognizerStateChanged) { @@ -640,6 +627,8 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr; mbglMap->moveBy(delta.x, delta.y); self.centerPoint = CGPointMake(self.centerPoint.x + delta.x, self.centerPoint.y + delta.y); + + [self notifyMapChange:@(mbgl::MapChangeRegionDidChangeAnimated)]; } else if (pan.state == UIGestureRecognizerStateEnded || pan.state == UIGestureRecognizerStateCancelled) { @@ -675,23 +664,26 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr; [weakSelf notifyMapChange:@(mbgl::MapChangeRegionDidChangeAnimated)]; }]; } - - // Send Map Drag End Event - CGPoint ptInView = CGPointMake([pan locationInView:pan.view].x, [pan locationInView:pan.view].y); - CLLocationCoordinate2D coord = [self convertPoint:ptInView toCoordinateFromView:pan.view]; - NSMutableDictionary *dict = [[NSMutableDictionary alloc] init]; - [dict setValue:[[NSNumber alloc] initWithDouble:coord.latitude] forKey:@"lat"]; - [dict setValue:[[NSNumber alloc] initWithDouble:coord.longitude] forKey:@"lng"]; - [dict setValue:[[NSNumber alloc] initWithDouble:[self zoomLevel]] forKey:@"zoom"]; - - [[MGLMapboxEvents sharedManager] pushEvent:@"map.dragend" withAttributes:dict]; + else + { + [self notifyMapChange:@(mbgl::MapChangeRegionDidChange)]; + } + + // metrics: pan end + CGPoint pointInView = CGPointMake([pan locationInView:pan.view].x, [pan locationInView:pan.view].y); + CLLocationCoordinate2D panCoordinate = [self convertPoint:pointInView toCoordinateFromView:pan.view]; + double zoom = [self zoomLevel]; + + [MGLMapboxEvents pushEvent:MGLEventTypeMapDragEnd withAttributes:@{ + MGLEventKeyLatitude: @(panCoordinate.latitude), + MGLEventKeyLongitude: @(panCoordinate.longitude), + MGLEventKeyZoomLevel: @(zoom) + }]; } } - (void)handlePinchGesture:(UIPinchGestureRecognizer *)pinch { - [self trackGestureEvent:@"Pinch" forRecognizer:pinch]; - if ( ! self.isZoomEnabled) return; if (mbglMap->getZoom() <= mbglMap->getMinZoom() && pinch.scale < 1) return; @@ -700,9 +692,13 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr; if (pinch.state == UIGestureRecognizerStateBegan) { + [self trackGestureEvent:MGLEventGesturePinchStart forRecognizer:pinch]; + mbglMap->startScaling(); self.scale = mbglMap->getScale(); + + self.userTrackingMode = MGLUserTrackingModeNone; } else if (pinch.state == UIGestureRecognizerStateChanged) { @@ -720,23 +716,25 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr; [self unrotateIfNeededAnimated:YES]; - [self notifyMapChange:@(mbgl::MapChangeRegionDidChangeAnimated)]; + [self notifyMapChange:@(mbgl::MapChangeRegionDidChange)]; } } - (void)handleRotateGesture:(UIRotationGestureRecognizer *)rotate { - [self trackGestureEvent:@"Rotation" forRecognizer:rotate]; - if ( ! self.isRotateEnabled) return; mbglMap->cancelTransitions(); if (rotate.state == UIGestureRecognizerStateBegan) { + [self trackGestureEvent:MGLEventGestureRotateStart forRecognizer:rotate]; + mbglMap->startRotating(); self.angle = [MGLMapView degreesToRadians:mbglMap->getBearing()] * -1; + + self.userTrackingMode = MGLUserTrackingModeNone; } else if (rotate.state == UIGestureRecognizerStateChanged) { @@ -760,135 +758,169 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr; [self unrotateIfNeededAnimated:YES]; - [self notifyMapChange:@(mbgl::MapChangeRegionDidChangeAnimated)]; + [self notifyMapChange:@(mbgl::MapChangeRegionDidChange)]; } } - (void)handleSingleTapGesture:(UITapGestureRecognizer *)singleTap { - [self trackGestureEvent:@"SingleTap" forRecognizer:singleTap]; - - CGPoint tapPoint = [singleTap locationInView:self]; - - // tolerances based on touch size & typical marker aspect ratio - CGFloat toleranceWidth = 50; - CGFloat toleranceHeight = 75; + if (singleTap.state == UIGestureRecognizerStateEnded) + { + [self trackGestureEvent:MGLEventGestureSingleTap forRecognizer:singleTap]; + + CGPoint tapPoint = [singleTap locationInView:self]; + + if (self.userLocationVisible && ! [self.selectedAnnotation isEqual:self.userLocation]) + { + CGRect userLocationRect = CGRectMake(tapPoint.x - 15, tapPoint.y - 15, 30, 30); + + if (CGRectContainsPoint(userLocationRect, [self convertCoordinate:self.userLocation.coordinate toPointToView:self])) + { + [self selectAnnotation:self.userLocation animated:YES]; + return; + } + } - // setup a recognition area weighted 2/3 of the way above the point to account for average marker imagery - CGRect tapRect = CGRectMake(tapPoint.x - toleranceWidth / 2, tapPoint.y - 2 * toleranceHeight / 3, toleranceWidth, toleranceHeight); - CGPoint tapRectLowerLeft = CGPointMake(tapRect.origin.x, tapRect.origin.y + tapRect.size.height); - CGPoint tapRectUpperLeft = CGPointMake(tapRect.origin.x, tapRect.origin.y); - CGPoint tapRectUpperRight = CGPointMake(tapRect.origin.x + tapRect.size.width, tapRect.origin.y); - CGPoint tapRectLowerRight = CGPointMake(tapRect.origin.x + tapRect.size.width, tapRect.origin.y + tapRect.size.height); + // tolerances based on touch size & typical marker aspect ratio + CGFloat toleranceWidth = 40; + CGFloat toleranceHeight = 60; - // figure out what that means in coordinate space - CLLocationCoordinate2D coordinate; - mbgl::LatLngBounds tapBounds; + // setup a recognition area weighted 2/3 of the way above the point to account for average marker imagery + CGRect tapRect = CGRectMake(tapPoint.x - toleranceWidth / 2, tapPoint.y - 2 * toleranceHeight / 3, toleranceWidth, toleranceHeight); + CGPoint tapRectLowerLeft = CGPointMake(tapRect.origin.x, tapRect.origin.y + tapRect.size.height); + CGPoint tapRectUpperLeft = CGPointMake(tapRect.origin.x, tapRect.origin.y); + CGPoint tapRectUpperRight = CGPointMake(tapRect.origin.x + tapRect.size.width, tapRect.origin.y); + CGPoint tapRectLowerRight = CGPointMake(tapRect.origin.x + tapRect.size.width, tapRect.origin.y + tapRect.size.height); - coordinate = [self convertPoint:tapRectLowerLeft toCoordinateFromView:self]; - tapBounds.extend(mbgl::LatLng(coordinate.latitude, coordinate.longitude)); + // figure out what that means in coordinate space + CLLocationCoordinate2D coordinate; + mbgl::LatLngBounds tapBounds; - coordinate = [self convertPoint:tapRectUpperLeft toCoordinateFromView:self]; - tapBounds.extend(mbgl::LatLng(coordinate.latitude, coordinate.longitude)); + coordinate = [self convertPoint:tapRectLowerLeft toCoordinateFromView:self]; + tapBounds.extend(coordinateToLatLng(coordinate)); - coordinate = [self convertPoint:tapRectUpperRight toCoordinateFromView:self]; - tapBounds.extend(mbgl::LatLng(coordinate.latitude, coordinate.longitude)); + coordinate = [self convertPoint:tapRectUpperLeft toCoordinateFromView:self]; + tapBounds.extend(coordinateToLatLng(coordinate)); - coordinate = [self convertPoint:tapRectLowerRight toCoordinateFromView:self]; - tapBounds.extend(mbgl::LatLng(coordinate.latitude, coordinate.longitude)); + coordinate = [self convertPoint:tapRectUpperRight toCoordinateFromView:self]; + tapBounds.extend(coordinateToLatLng(coordinate)); - // query for nearby annotations - std::vector<uint32_t> nearbyAnnotations = mbglMap->getAnnotationsInBounds(tapBounds); + coordinate = [self convertPoint:tapRectLowerRight toCoordinateFromView:self]; + tapBounds.extend(coordinateToLatLng(coordinate)); - int32_t newSelectedAnnotationID = -1; + // query for nearby annotations + std::vector<uint32_t> nearbyAnnotations = mbglMap->getAnnotationsInBounds(tapBounds); - if (nearbyAnnotations.size()) - { - // there is at least one nearby annotation; select one - // - // first, sort for comparison and iteration - std::sort(nearbyAnnotations.begin(), nearbyAnnotations.end()); + int32_t newSelectedAnnotationID = -1; - if (nearbyAnnotations == self.annotationsNearbyLastTap) + if (nearbyAnnotations.size()) { - // the selection candidates haven't changed; cycle through them - if (self.selectedAnnotation && - [[[self.annotationsStore objectForKey:self.selectedAnnotation] - objectForKey:MGLAnnotationIDKey] unsignedIntValue] == self.annotationsNearbyLastTap.back()) - { - // the selected annotation is the last in the set; cycle back to the first - // note: this could be the selected annotation if only one in set - newSelectedAnnotationID = self.annotationsNearbyLastTap.front(); - } - else if (self.selectedAnnotation) + // there is at least one nearby annotation; select one + // + // first, sort for comparison and iteration + std::sort(nearbyAnnotations.begin(), nearbyAnnotations.end()); + + if (nearbyAnnotations == self.annotationsNearbyLastTap) { - // otherwise increment the selection through the candidates - uint32_t currentID = [[[self.annotationsStore objectForKey:self.selectedAnnotation] objectForKey:MGLAnnotationIDKey] unsignedIntValue]; - auto result = std::find(self.annotationsNearbyLastTap.begin(), self.annotationsNearbyLastTap.end(), currentID); - auto distance = std::distance(self.annotationsNearbyLastTap.begin(), result); - newSelectedAnnotationID = self.annotationsNearbyLastTap[distance + 1]; + // the selection candidates haven't changed; cycle through them + if (self.selectedAnnotation && + [[[self.annotationIDsByAnnotation objectForKey:self.selectedAnnotation] + objectForKey:MGLAnnotationIDKey] unsignedIntValue] == self.annotationsNearbyLastTap.back()) + { + // the selected annotation is the last in the set; cycle back to the first + // note: this could be the selected annotation if only one in set + newSelectedAnnotationID = self.annotationsNearbyLastTap.front(); + } + else if (self.selectedAnnotation) + { + // otherwise increment the selection through the candidates + uint32_t currentID = [[[self.annotationIDsByAnnotation objectForKey:self.selectedAnnotation] objectForKey:MGLAnnotationIDKey] unsignedIntValue]; + auto result = std::find(self.annotationsNearbyLastTap.begin(), self.annotationsNearbyLastTap.end(), currentID); + auto distance = std::distance(self.annotationsNearbyLastTap.begin(), result); + newSelectedAnnotationID = self.annotationsNearbyLastTap[distance + 1]; + } + else + { + // no current selection; select the first one + newSelectedAnnotationID = self.annotationsNearbyLastTap.front(); + } } else { - // no current selection; select the first one + // start tracking a new set of nearby annotations + self.annotationsNearbyLastTap = nearbyAnnotations; + + // select the first one newSelectedAnnotationID = self.annotationsNearbyLastTap.front(); } } else { - // start tracking a new set of nearby annotations - self.annotationsNearbyLastTap = nearbyAnnotations; - - // select the first one - newSelectedAnnotationID = self.annotationsNearbyLastTap.front(); + // there are no nearby annotations; deselect if necessary + newSelectedAnnotationID = -1; } - } - else - { - // there are no nearby annotations; deselect if necessary - newSelectedAnnotationID = -1; - } - if (newSelectedAnnotationID >= 0) - { - // find & select model object for selection - NSEnumerator *enumerator = self.annotationsStore.keyEnumerator; - - while (id <MGLAnnotation> annotation = enumerator.nextObject) + if (newSelectedAnnotationID >= 0) { - if ([[[self.annotationsStore objectForKey:annotation] objectForKey:MGLAnnotationIDKey] integerValue] == newSelectedAnnotationID) + // find & select model object for selection + NSEnumerator *enumerator = self.annotationIDsByAnnotation.keyEnumerator; + + while (id <MGLAnnotation> annotation = enumerator.nextObject) { - // only change selection status if not the currently selected annotation - if ( ! [annotation isEqual:self.selectedAnnotation]) + if ([[[self.annotationIDsByAnnotation objectForKey:annotation] objectForKey:MGLAnnotationIDKey] integerValue] == newSelectedAnnotationID) { - [self selectAnnotation:annotation animated:YES]; - } + // only change selection status if not the currently selected annotation + if ( ! [annotation isEqual:self.selectedAnnotation]) + { + [self selectAnnotation:annotation animated:YES]; + } - // either way, we should stop enumerating - break; + // either way, we should stop enumerating + break; + } } } + else + { + // deselect any selected annotation + if (self.selectedAnnotation) [self deselectAnnotation:self.selectedAnnotation animated:YES]; + } } - else - { - // deselect any selected annotation - if (self.selectedAnnotation) [self deselectAnnotation:self.selectedAnnotation animated:YES]; - } - - NSLog(@"%i (%@)", newSelectedAnnotationID, self.selectedAnnotation.title); } - (void)handleDoubleTapGesture:(UITapGestureRecognizer *)doubleTap { - [self trackGestureEvent:@"DoubleTap" forRecognizer:doubleTap]; - if ( ! self.isZoomEnabled) return; mbglMap->cancelTransitions(); - if (doubleTap.state == UIGestureRecognizerStateEnded) + if (doubleTap.state == UIGestureRecognizerStateBegan) + { + [self trackGestureEvent:MGLEventGestureDoubleTap forRecognizer:doubleTap]; + } + else if (doubleTap.state == UIGestureRecognizerStateEnded) { - mbglMap->scaleBy(2, [doubleTap locationInView:doubleTap.view].x, [doubleTap locationInView:doubleTap.view].y, secondsAsDuration(MGLAnimationDuration)); + CGPoint doubleTapPoint = [doubleTap locationInView:doubleTap.view]; + + CGPoint zoomInPoint; + + if (self.userTrackingMode != MGLUserTrackingModeNone) + { + CGPoint userPoint = [self convertCoordinate:self.userLocation.coordinate toPointToView:self]; + CGRect userLocationRect = CGRectMake(userPoint.x - 40, userPoint.y - 40, 80, 80); + if (CGRectContainsPoint(userLocationRect, doubleTapPoint)) + { + zoomInPoint = userPoint; + } + } + else + { + self.userTrackingMode = MGLUserTrackingModeNone; + + zoomInPoint = doubleTapPoint; + } + + mbglMap->scaleBy(2, zoomInPoint.x, zoomInPoint.y, secondsAsDuration(MGLAnimationDuration)); self.animatingGesture = YES; @@ -907,17 +939,32 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr; - (void)handleTwoFingerTapGesture:(UITapGestureRecognizer *)twoFingerTap { - [self trackGestureEvent:@"TwoFingerTap" forRecognizer:twoFingerTap]; - if ( ! self.isZoomEnabled) return; if (mbglMap->getZoom() == mbglMap->getMinZoom()) return; mbglMap->cancelTransitions(); - if (twoFingerTap.state == UIGestureRecognizerStateEnded) + if (twoFingerTap.state == UIGestureRecognizerStateBegan) + { + [self trackGestureEvent:MGLEventGestureTwoFingerSingleTap forRecognizer:twoFingerTap]; + } + else if (twoFingerTap.state == UIGestureRecognizerStateEnded) { - mbglMap->scaleBy(0.5, [twoFingerTap locationInView:twoFingerTap.view].x, [twoFingerTap locationInView:twoFingerTap.view].y, secondsAsDuration(MGLAnimationDuration)); + CGPoint zoomOutPoint; + + if (self.userTrackingMode != MGLUserTrackingModeNone) + { + zoomOutPoint = self.center; + } + else + { + self.userTrackingMode = MGLUserTrackingModeNone; + + zoomOutPoint = CGPointMake([twoFingerTap locationInView:twoFingerTap.view].x, [twoFingerTap locationInView:twoFingerTap.view].y); + } + + mbglMap->scaleBy(0.5, zoomOutPoint.x, zoomOutPoint.y, secondsAsDuration(MGLAnimationDuration)); self.animatingGesture = YES; @@ -936,17 +983,19 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr; - (void)handleQuickZoomGesture:(UILongPressGestureRecognizer *)quickZoom { - [self trackGestureEvent:@"QuickZoom" forRecognizer:quickZoom]; - if ( ! self.isZoomEnabled) return; mbglMap->cancelTransitions(); if (quickZoom.state == UIGestureRecognizerStateBegan) { + [self trackGestureEvent:MGLEventGestureQuickZoom forRecognizer:quickZoom]; + self.scale = mbglMap->getScale(); self.quickZoomStart = [quickZoom locationInView:quickZoom.view].y; + + self.userTrackingMode = MGLUserTrackingModeNone; } else if (quickZoom.state == UIGestureRecognizerStateChanged) { @@ -962,7 +1011,16 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr; { [self unrotateIfNeededAnimated:YES]; - [self notifyMapChange:@(mbgl::MapChangeRegionDidChangeAnimated)]; + [self notifyMapChange:@(mbgl::MapChangeRegionDidChange)]; + } +} + +- (void)handleCalloutAccessoryTapGesture:(UITapGestureRecognizer *)tap +{ + if ([self.delegate respondsToSelector:@selector(mapView:annotation:calloutAccessoryControlTapped:)]) + { + [self.delegate mapView:self annotation:self.selectedAnnotation + calloutAccessoryControlTapped:(UIControl *)tap.view]; } } @@ -973,18 +1031,18 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr; return ([validSimultaneousGestures containsObject:gestureRecognizer] && [validSimultaneousGestures containsObject:otherGestureRecognizer]); } -- (void) trackGestureEvent:(NSString *) gesture forRecognizer:(UIGestureRecognizer *) recognizer +- (void)trackGestureEvent:(NSString *)gestureID forRecognizer:(UIGestureRecognizer *)recognizer { - // Send Map Zoom Event - CGPoint ptInView = CGPointMake([recognizer locationInView:recognizer.view].x, [recognizer locationInView:recognizer.view].y); - CLLocationCoordinate2D coord = [self convertPoint:ptInView toCoordinateFromView:recognizer.view]; - NSMutableDictionary *dict = [[NSMutableDictionary alloc] init]; - [dict setValue:[[NSNumber alloc] initWithDouble:coord.latitude] forKey:@"lat"]; - [dict setValue:[[NSNumber alloc] initWithDouble:coord.longitude] forKey:@"lng"]; - [dict setValue:[[NSNumber alloc] initWithDouble:[self zoomLevel]] forKey:@"zoom"]; - [dict setValue:gesture forKey:@"gesture"]; - - [[MGLMapboxEvents sharedManager] pushEvent:@"map.click" withAttributes:dict]; + CGPoint pointInView = CGPointMake([recognizer locationInView:recognizer.view].x, [recognizer locationInView:recognizer.view].y); + CLLocationCoordinate2D gestureCoordinate = [self convertPoint:pointInView toCoordinateFromView:recognizer.view]; + double zoom = [self zoomLevel]; + + [MGLMapboxEvents pushEvent:MGLEventTypeMapTap withAttributes:@{ + MGLEventKeyLatitude: @(gestureCoordinate.latitude), + MGLEventKeyLongitude: @(gestureCoordinate.longitude), + MGLEventKeyZoomLevel: @(zoom), + MGLEventKeyGestureID: gestureID + }]; } #pragma mark - Properties - @@ -1016,6 +1074,8 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr; - (void)resetNorthAnimated:(BOOL)animated { + self.userTrackingMode = MGLUserTrackingModeNone; + CGFloat duration = (animated ? MGLAnimationDuration : 0); mbglMap->setBearing(0, secondsAsDuration(duration)); @@ -1029,6 +1089,8 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr; { if (finished) { + [self notifyMapChange:@(animated ? mbgl::MapChangeRegionDidChangeAnimated : mbgl::MapChangeRegionDidChange)]; + [UIView animateWithDuration:MGLAnimationDuration animations:^ { @@ -1041,6 +1103,8 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr; - (void)resetPosition { mbglMap->resetPosition(); + + [self notifyMapChange:@(mbgl::MapChangeRegionDidChange)]; } - (void)toggleDebug @@ -1050,11 +1114,20 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr; #pragma mark - Geography - +- (void)setCenterCoordinate:(CLLocationCoordinate2D)coordinate animated:(BOOL)animated preservingTracking:(BOOL)tracking +{ + self.userTrackingMode = (tracking ? self.userTrackingMode : MGLUserTrackingModeNone); + + [self setCenterCoordinate:coordinate animated:animated]; +} + - (void)setCenterCoordinate:(CLLocationCoordinate2D)coordinate animated:(BOOL)animated { CGFloat duration = (animated ? MGLAnimationDuration : 0); mbglMap->setLatLng(coordinateToLatLng(coordinate), secondsAsDuration(duration)); + + [self notifyMapChange:@(animated ? mbgl::MapChangeRegionDidChangeAnimated : mbgl::MapChangeRegionDidChange)]; } - (void)setCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate @@ -1069,11 +1142,15 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr; - (void)setCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate zoomLevel:(double)zoomLevel animated:(BOOL)animated { + self.userTrackingMode = MGLUserTrackingModeNone; + CGFloat duration = (animated ? MGLAnimationDuration : 0); mbglMap->setLatLngZoom(coordinateToLatLng(centerCoordinate), zoomLevel, secondsAsDuration(duration)); [self unrotateIfNeededAnimated:animated]; + + [self notifyMapChange:@(animated ? mbgl::MapChangeRegionDidChangeAnimated : mbgl::MapChangeRegionDidChange)]; } - (double)zoomLevel @@ -1083,11 +1160,15 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr; - (void)setZoomLevel:(double)zoomLevel animated:(BOOL)animated { + self.userTrackingMode = MGLUserTrackingModeNone; + CGFloat duration = (animated ? MGLAnimationDuration : 0); mbglMap->setZoom(zoomLevel, secondsAsDuration(duration)); [self unrotateIfNeededAnimated:animated]; + + [self notifyMapChange:@(animated ? mbgl::MapChangeRegionDidChangeAnimated : mbgl::MapChangeRegionDidChange)]; } - (void)setZoomLevel:(double)zoomLevel @@ -1095,6 +1176,22 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr; [self setZoomLevel:zoomLevel animated:NO]; } +- (void)zoomToSouthWestCoordinate:(CLLocationCoordinate2D)southWestCoordinate northEastCoordinate:(CLLocationCoordinate2D)northEastCoordinate animated:(BOOL)animated +{ + // NOTE: does not disrupt tracking mode + + CLLocationCoordinate2D center = CLLocationCoordinate2DMake((northEastCoordinate.latitude + southWestCoordinate.latitude) / 2, (northEastCoordinate.longitude + southWestCoordinate.longitude) / 2); + + CGFloat scale = mbglMap->getScale(); + CGFloat scaleX = mbglMap->getState().getWidth() / (northEastCoordinate.longitude - southWestCoordinate.longitude); + CGFloat scaleY = mbglMap->getState().getHeight() / (northEastCoordinate.latitude - southWestCoordinate.latitude); + CGFloat minZoom = mbglMap->getMinZoom(); + CGFloat maxZoom = mbglMap->getMaxZoom(); + CGFloat zoomLevel = MAX(MIN(log(scale * MIN(scaleX, scaleY)) / log(2), maxZoom), minZoom); + + [self setCenterCoordinate:center zoomLevel:zoomLevel animated:animated]; +} + - (CLLocationDirection)direction { double direction = mbglMap->getBearing() * -1; @@ -1109,9 +1206,13 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr; { if ( ! animated && ! self.rotationAllowed) return; + self.userTrackingMode = MGLUserTrackingModeNone; + CGFloat duration = (animated ? MGLAnimationDuration : 0); mbglMap->setBearing(direction * -1, secondsAsDuration(duration)); + + [self notifyMapChange:@(animated ? mbgl::MapChangeRegionDidChangeAnimated : mbgl::MapChangeRegionDidChange)]; } - (void)setDirection:(CLLocationDirection)direction @@ -1132,7 +1233,7 @@ mbgl::DefaultFileSource *mbglFileSource = nullptr; - (CGPoint)convertCoordinate:(CLLocationCoordinate2D)coordinate toPointToView:(UIView *)view { - mbgl::vec2<double> pixel = mbglMap->pixelForLatLng(mbgl::LatLng(coordinate.latitude, coordinate.longitude)); + mbgl::vec2<double> pixel = mbglMap->pixelForLatLng(coordinateToLatLng(coordinate)); // flip y coordinate for iOS view origin in top left // @@ -1178,14 +1279,14 @@ CLLocationCoordinate2D latLngToCoordinate(mbgl::LatLng latLng) { const std::string styleJSON = mbglMap->getStyleJSON(); - return [NSJSONSerialization JSONObjectWithData:[@(styleJSON.c_str()) dataUsingEncoding:[NSString defaultCStringEncoding]] options:0 error:nil]; + return [NSJSONSerialization JSONObjectWithData:[@(styleJSON.c_str()) dataUsingEncoding:NSUTF8StringEncoding] options:0 error:nil]; } - (void)setRawStyle:(NSDictionary *)style { NSData *data = [NSJSONSerialization dataWithJSONObject:style options:0 error:nil]; - [self setStyleJSON:[[NSString alloc] initWithData:data encoding:[NSString defaultCStringEncoding]]]; + [self setStyleJSON:[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]]; } - (NSArray *)bundledStyleNames @@ -1219,53 +1320,12 @@ CLLocationCoordinate2D latLngToCoordinate(mbgl::LatLng latLng) if (isHybrid) { styleName = [@"satellite-" stringByAppendingString:[styleName substringFromIndex:[hybridStylePrefix length]]]; } - [self setStyleURL:[NSString stringWithFormat:@"styles/%@.json", styleName]]; + [self setStyleURL:[NSURL URLWithString:[NSString stringWithFormat:@"styles/%@.json", styleName]]]; if (isHybrid) { [self setStyleClasses:@[@"contours", @"labels"]]; } } -- (NSArray *)getStyleOrderedLayerNames -{ - return [[self getRawStyle] valueForKeyPath:@"layers.id"]; -} - -- (void)setStyleOrderedLayerNames:(NSArray *)orderedLayerNames -{ - NSMutableDictionary *style = [[self getRawStyle] deepMutableCopy]; - NSArray *oldLayers = style[@"layers"]; - NSMutableArray *newLayers = [NSMutableArray array]; - - if ([orderedLayerNames count] != [[oldLayers valueForKeyPath:@"id"] count]) - { - [NSException raise:@"invalid layer count" - format:@"new layer count (%lu) should equal existing layer count (%lu)", - (unsigned long)[orderedLayerNames count], - (unsigned long)[[oldLayers valueForKeyPath:@"id"] count]]; - } - else - { - for (NSString *newLayerName in orderedLayerNames) - { - if ( ! [[oldLayers valueForKeyPath:@"id"] containsObject:newLayerName]) - { - [NSException raise:@"invalid layer name" - format:@"layer name %@ unknown", - newLayerName]; - } - else - { - NSDictionary *newLayer = [oldLayers objectAtIndex:[[oldLayers valueForKeyPath:@"id"] indexOfObject:newLayerName]]; - [newLayers addObject:newLayer]; - } - } - } - - [style setValue:newLayers forKey:@"layers"]; - - [self setRawStyle:style]; -} - - (NSArray *)getAppliedStyleClasses { NSMutableArray *returnArray = [NSMutableArray array]; @@ -1291,480 +1351,579 @@ CLLocationCoordinate2D latLngToCoordinate(mbgl::LatLng latLng) for (NSString *appliedClass in appliedClasses) { - newAppliedClasses.insert(newAppliedClasses.end(), [appliedClass cStringUsingEncoding:[NSString defaultCStringEncoding]]); + newAppliedClasses.insert(newAppliedClasses.end(), [appliedClass UTF8String]); } mbglMap->setDefaultTransitionDuration(secondsAsDuration(transitionDuration)); mbglMap->setClasses(newAppliedClasses); } -- (NSString *)getKeyTypeForLayer:(NSString *)layerName +#pragma mark - Annotations - + +- (NSArray *)annotations { - NSDictionary *style = [self getRawStyle]; + if ([_annotationIDsByAnnotation count]) + { + NSMutableArray *result = [NSMutableArray array]; - NSString *bucketType; + NSEnumerator *keyEnumerator = [_annotationIDsByAnnotation keyEnumerator]; + id <MGLAnnotation> annotation; - if ([layerName isEqualToString:@"background"]) - { - bucketType = @"background"; - } - else - { - for (NSDictionary *layer in style[@"structure"]) + while (annotation = [keyEnumerator nextObject]) { - if ([layer[@"name"] isEqualToString:layerName]) - { - bucketType = style[@"buckets"][layer[@"bucket"]][@"type"]; - break; - } + [result addObject:annotation]; } - } - NSString *keyType; - - if ([bucketType isEqualToString:@"fill"]) - { - keyType = MGLStyleKeyFill; - } - else if ([bucketType isEqualToString:@"line"]) - { - keyType = MGLStyleKeyLine; - } - else if ([bucketType isEqualToString:@"point"]) - { - keyType = MGLStyleKeyIcon; - } - else if ([bucketType isEqualToString:@"text"]) - { - keyType = MGLStyleKeyText; - } - else if ([bucketType isEqualToString:@"raster"]) - { - keyType = MGLStyleKeyRaster; - } - else if ([bucketType isEqualToString:@"composite"]) - { - keyType = MGLStyleKeyComposite; - } - else if ([bucketType isEqualToString:@"background"]) - { - keyType = MGLStyleKeyBackground; - } - else - { - [NSException raise:@"invalid bucket type" - format:@"bucket type %@ unknown", - bucketType]; + return [NSArray arrayWithArray:result]; } - return keyType; + return nil; } -- (NSDictionary *)getStyleDescriptionForLayer:(NSString *)layerName inClass:(NSString *)className +- (void)addAnnotation:(id <MGLAnnotation>)annotation { - NSDictionary *style = [self getRawStyle]; + if ( ! annotation) return; - if ( ! [[style valueForKeyPath:@"classes.name"] containsObject:className]) - { - [NSException raise:@"invalid class name" - format:@"class name %@ unknown", - className]; - } + // The core bulk add API is efficient with respect to indexing and + // screen refreshes, thus we should defer to it even for individual adds. + // + [self addAnnotations:@[ annotation ]]; +} - NSUInteger classNumber = [[style valueForKeyPath:@"classes.name"] indexOfObject:className]; +- (void)addAnnotations:(NSArray *)annotations +{ + if ( ! annotations) return; - if ( ! [[style[@"classes"][classNumber][@"layers"] allKeys] containsObject:layerName]) - { - // layer specified in structure, but not styled - // - return nil; - } + std::vector<mbgl::LatLng> latLngs; + latLngs.reserve(annotations.count); - NSDictionary *layerStyle = style[@"classes"][classNumber][@"layers"][layerName]; + std::vector<std::string> symbols; + symbols.reserve(annotations.count); - NSMutableDictionary *styleDescription = [NSMutableDictionary dictionary]; + BOOL delegateImplementsSymbolLookup = [self.delegate respondsToSelector:@selector(mapView:symbolNameForAnnotation:)]; - for (NSString *keyName in [layerStyle allKeys]) + for (id <MGLAnnotation> annotation in annotations) { - id value = layerStyle[keyName]; + assert([annotation conformsToProtocol:@protocol(MGLAnnotation)]); - while ([[style[@"constants"] allKeys] containsObject:value]) - { - value = style[@"constants"][value]; - } + latLngs.push_back(coordinateToLatLng(annotation.coordinate)); - if ([[self.allowedStyleTypes[MGLStyleKeyGeneric] allKeys] containsObject:keyName]) + NSString *symbolName = nil; + + if (delegateImplementsSymbolLookup) { - [styleDescription setValue:[self typedPropertyForKeyName:keyName - ofType:MGLStyleKeyGeneric - withValue:value] - forKey:keyName]; + symbolName = [self.delegate mapView:self symbolNameForAnnotation:annotation]; } - NSString *keyType = [self getKeyTypeForLayer:layerName]; + symbols.push_back((symbolName ? [symbolName UTF8String] : "")); + } + + std::vector<uint32_t> annotationIDs = mbglMap->addPointAnnotations(latLngs, symbols); - if ([[self.allowedStyleTypes[keyType] allKeys] containsObject:keyName]) - { - [styleDescription setValue:[self typedPropertyForKeyName:keyName - ofType:keyType - withValue:value] - forKey:keyName]; - } + for (size_t i = 0; i < annotationIDs.size(); ++i) + { + [self.annotationIDsByAnnotation setObject:@{ MGLAnnotationIDKey : @(annotationIDs[i]) } + forKey:annotations[i]]; } +} + +- (void)removeAnnotation:(id <MGLAnnotation>)annotation +{ + if ( ! annotation) return; - return styleDescription; + // The core bulk deletion API is efficient with respect to indexing + // and screen refreshes, thus we should defer to it even for + // individual deletes. + // + [self removeAnnotations:@[ annotation ]]; } -- (NSDictionary *)typedPropertyForKeyName:(NSString *)keyName ofType:(NSString *)keyType withValue:(id)value +- (void)removeAnnotations:(NSArray *)annotations { - if ( ! [[self.allowedStyleTypes[keyType] allKeys] containsObject:keyName]) - { - [NSException raise:@"invalid property name" - format:@"property name %@ unknown", - keyName]; - } + if ( ! annotations) return; - NSArray *typeInfo = self.allowedStyleTypes[keyType][keyName]; + std::vector<uint32_t> annotationIDsToRemove; + annotationIDsToRemove.reserve(annotations.count); - if ([value isKindOfClass:[NSArray class]] && ! [typeInfo containsObject:MGLStyleValueTypeColor]) + for (id <MGLAnnotation> annotation in annotations) { - if ([typeInfo containsObject:MGLStyleValueFunctionAllowed]) - { - if ([[(NSArray *)value firstObject] isKindOfClass:[NSString class]]) - { - NSString *functionType; + assert([annotation conformsToProtocol:@protocol(MGLAnnotation)]); - if ([[(NSArray *)value firstObject] isEqualToString:@"linear"]) - { - functionType = MGLStyleValueTypeFunctionLinear; - } - else if ([[(NSArray *)value firstObject] isEqualToString:@"stops"]) - { - functionType = MGLStyleValueTypeFunctionStops; - } - else if ([[(NSArray *)value firstObject] isEqualToString:@"exponential"]) - { - functionType = MGLStyleValueTypeFunctionExponential; - } - else if ([[(NSArray *)value firstObject] isEqualToString:@"min"]) - { - functionType = MGLStyleValueTypeFunctionMinimumZoom; - } - else if ([[(NSArray *)value firstObject] isEqualToString:@"max"]) - { - functionType = MGLStyleValueTypeFunctionMaximumZoom; - } + annotationIDsToRemove.push_back([[[self.annotationIDsByAnnotation objectForKey:annotation] + objectForKey:MGLAnnotationIDKey] unsignedIntValue]); + [self.annotationIDsByAnnotation removeObjectForKey:annotation]; - if (functionType) - { - return @{ @"type" : functionType, - @"value" : value }; - } - } - } - else if ([typeInfo containsObject:MGLStyleValueTypeNumberPair]) + if (annotation == self.selectedAnnotation) { - return @{ @"type" : MGLStyleValueTypeNumberPair, - @"value" : value }; + [self deselectAnnotation:annotation animated:NO]; } } - else if ([typeInfo containsObject:MGLStyleValueTypeNumber]) - { - return @{ @"type" : MGLStyleValueTypeNumber, - @"value" : value }; - } - else if ([typeInfo containsObject:MGLStyleValueTypeBoolean]) - { - return @{ @"type" : MGLStyleValueTypeBoolean, - @"value" : @([(NSString *)value boolValue]) }; - } - else if ([typeInfo containsObject:MGLStyleValueTypeString]) - { - return @{ @"type" : MGLStyleValueTypeString, - @"value" : value }; - } - else if ([typeInfo containsObject:MGLStyleValueTypeColor]) - { - UIColor *color; - if ([(NSString *)value hasPrefix:@"#"]) - { - color = [UIColor colorWithHexString:value]; - } - else if ([(NSString *)value hasPrefix:@"rgb"]) - { - color = [UIColor colorWithRGBAString:value]; - } - else if ([(NSString *)value hasPrefix:@"hsl"]) - { - [NSException raise:@"invalid color format" - format:@"HSL color format not yet supported natively"]; - } - else if ([value isKindOfClass:[NSArray class]] && [(NSArray *)value count] == 4) - { - color = [UIColor colorWithRed:[value[0] floatValue] - green:[value[1] floatValue] - blue:[value[2] floatValue] - alpha:[value[3] floatValue]]; - } - else if ([[UIColor class] respondsToSelector:NSSelectorFromString([NSString stringWithFormat:@"%@Color", [(NSString *)value lowercaseString]])]) - { -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Warc-performSelector-leaks" + mbglMap->removeAnnotations(annotationIDsToRemove); +} + +- (NSArray *)selectedAnnotations +{ + return (self.selectedAnnotation ? @[ self.selectedAnnotation ] : @[]); +} - color = [[UIColor class] performSelector:NSSelectorFromString([NSString stringWithFormat:@"%@Color", [(NSString *)value lowercaseString]])]; +- (void)setSelectedAnnotations:(NSArray *)selectedAnnotations +{ + if ( ! selectedAnnotations.count) return; -#pragma clang diagnostic pop - } + id <MGLAnnotation> firstAnnotation = selectedAnnotations[0]; - return @{ @"type" : MGLStyleValueTypeColor, - @"value" : color }; - } + assert([firstAnnotation conformsToProtocol:@protocol(MGLAnnotation)]); - return nil; + if ( ! [self viewportBounds].contains(coordinateToLatLng(firstAnnotation.coordinate))) return; + + [self selectAnnotation:firstAnnotation animated:NO]; } -- (void)setStyleDescription:(NSDictionary *)styleDescription forLayer:(NSString *)layerName inClass:(NSString *)className +- (void)selectAnnotation:(id <MGLAnnotation>)annotation animated:(BOOL)animated { -#pragma unused(className) + if ( ! annotation) return; + + if ( ! [self viewportBounds].contains(coordinateToLatLng(annotation.coordinate))) return; + + if (annotation == self.selectedAnnotation) return; + + self.userTrackingMode = MGLUserTrackingModeNone; - NSMutableDictionary *convertedStyle = [NSMutableDictionary dictionary]; + [self deselectAnnotation:self.selectedAnnotation animated:NO]; - for (NSString *key in [styleDescription allKeys]) + self.selectedAnnotation = annotation; + + if (annotation.title && [self.delegate respondsToSelector:@selector(mapView:annotationCanShowCallout:)] && + [self.delegate mapView:self annotationCanShowCallout:annotation]) { - NSArray *styleParameters = nil; + // build the callout + self.selectedAnnotationCalloutView = [self calloutViewForAnnotation:annotation]; + + CGRect calloutBounds; - if ([[self.allowedStyleTypes[MGLStyleKeyGeneric] allKeys] containsObject:key]) + if ([annotation isEqual:self.userLocation]) { - styleParameters = self.allowedStyleTypes[MGLStyleKeyGeneric][key]; + CGPoint calloutAnchorPoint = [self convertCoordinate:annotation.coordinate toPointToView:self]; + calloutBounds = CGRectMake(calloutAnchorPoint.x - 1, calloutAnchorPoint.y - 13, 0, 0); } else { - NSString *keyType = [self getKeyTypeForLayer:layerName]; - - if ([[self.allowedStyleTypes[keyType] allKeys] containsObject:key]) + // determine symbol in use for point + NSString *symbol = MGLDefaultStyleMarkerSymbolName; + if ([self.delegate respondsToSelector:@selector(mapView:symbolNameForAnnotation:)]) { - styleParameters = self.allowedStyleTypes[keyType][key]; + symbol = [self.delegate mapView:self symbolNameForAnnotation:annotation]; } + std::string symbolName([symbol UTF8String]); + + // determine anchor point based on symbol + CGPoint calloutAnchorPoint = [self convertCoordinate:annotation.coordinate toPointToView:self]; + double y = mbglMap->getTopOffsetPixelsForAnnotationSymbol(symbolName); + calloutBounds = CGRectMake(calloutAnchorPoint.x - 1, calloutAnchorPoint.y + y, 0, 0); } - if (styleParameters) + // consult delegate for left and/or right accessory views + if ([self.delegate respondsToSelector:@selector(mapView:leftCalloutAccessoryViewForAnnotation:)]) { - if ([styleDescription[key][@"value"] isKindOfClass:[MGLStyleFunctionValue class]]) - { - convertedStyle[key] = [(MGLStyleFunctionValue *)styleDescription[key][@"value"] rawStyle]; - } - else if ([styleParameters containsObject:styleDescription[key][@"type"]]) + self.selectedAnnotationCalloutView.leftAccessoryView = + [self.delegate mapView:self leftCalloutAccessoryViewForAnnotation:annotation]; + + if ([self.selectedAnnotationCalloutView.leftAccessoryView isKindOfClass:[UIControl class]]) { - NSString *valueType = styleDescription[key][@"type"]; + UITapGestureRecognizer *calloutAccessoryTap = [[UITapGestureRecognizer alloc] initWithTarget:self + action:@selector(handleCalloutAccessoryTapGesture:)]; - if ([valueType isEqualToString:MGLStyleValueTypeColor]) - { - convertedStyle[key] = [@"#" stringByAppendingString:[(UIColor *)styleDescription[key][@"value"] hexStringFromColor]]; - } - else - { - // the rest (bool/number/pair/string) are already JSON-convertible types - // - convertedStyle[key] = styleDescription[key][@"value"]; - } + [self.selectedAnnotationCalloutView.leftAccessoryView addGestureRecognizer:calloutAccessoryTap]; } } - else + + if ([self.delegate respondsToSelector:@selector(mapView:rightCalloutAccessoryViewForAnnotation:)]) { - [NSException raise:@"invalid style description format" - format:@"unable to parse key '%@'", - key]; + self.selectedAnnotationCalloutView.rightAccessoryView = + [self.delegate mapView:self rightCalloutAccessoryViewForAnnotation:annotation]; + + if ([self.selectedAnnotationCalloutView.rightAccessoryView isKindOfClass:[UIControl class]]) + { + UITapGestureRecognizer *calloutAccessoryTap = [[UITapGestureRecognizer alloc] initWithTarget:self + action:@selector(handleCalloutAccessoryTapGesture:)]; + + [self.selectedAnnotationCalloutView.rightAccessoryView addGestureRecognizer:calloutAccessoryTap]; + } } + + // present popup + [self.selectedAnnotationCalloutView presentCalloutFromRect:calloutBounds + inView:self.glView + constrainedToView:self.glView + animated:animated]; } -// NSMutableDictionary *style = [[self getRawStyle] deepMutableCopy]; -// -// NSUInteger classIndex = [[[self getAllStyleClasses] valueForKey:@"name"] indexOfObject:className]; -// -// style[@"classes"][classIndex][@"layers"][layerName] = convertedStyle; -// -// [self setRawStyle:style]; -} - -- (NSDictionary *)allowedStyleTypes -{ - static NSDictionary *MGLStyleAllowedTypes = @{ - MGLStyleKeyGeneric : @{ - @"enabled" : @[ MGLStyleValueTypeBoolean, MGLStyleValueFunctionAllowed ], - @"translate" : @[ MGLStyleValueTypeNumberPair, MGLStyleValueFunctionAllowed ], - @"translate-anchor" : @[ MGLStyleValueTypeString, MGLStyleValueFunctionAllowed ], - @"opacity" : @[ MGLStyleValueTypeNumber, MGLStyleValueFunctionAllowed ], - @"prerender" : @[ MGLStyleValueTypeBoolean ], - @"prerender-buffer" : MGLStyleValueTypeNumber, - @"prerender-size" : @[ MGLStyleValueTypeNumber ], - @"prerender-blur" : @[ MGLStyleValueTypeNumber ] }, - MGLStyleKeyFill : @{ - @"color" : @[ MGLStyleValueTypeColor ], - @"stroke" : @[ MGLStyleValueTypeColor ], - @"antialias" : @[ MGLStyleValueTypeBoolean ], - @"image" : @[ MGLStyleValueTypeString ] }, - MGLStyleKeyLine : @{ - @"color" : @[ MGLStyleValueTypeColor ], - @"width" : @[ MGLStyleValueTypeNumber, MGLStyleValueFunctionAllowed ], - @"dasharray" : @[ MGLStyleValueTypeNumberPair, MGLStyleValueFunctionAllowed ] }, - MGLStyleKeyIcon : @{ - @"color" : @[ MGLStyleValueTypeColor ], - @"image" : @[ MGLStyleValueTypeString ], - @"size" : @[ MGLStyleValueTypeNumber, MGLStyleValueFunctionAllowed ], - @"radius" : @[ MGLStyleValueTypeNumber, MGLStyleValueFunctionAllowed], - @"blur" : @[ MGLStyleValueTypeNumber, MGLStyleValueFunctionAllowed ] }, - MGLStyleKeyText : @{ - @"color" : @[ MGLStyleValueTypeColor ], - @"stroke" : @[ MGLStyleValueTypeColor ], - @"strokeWidth" : @[ MGLStyleValueTypeNumber, MGLStyleValueFunctionAllowed ], - @"strokeBlur" : @[ MGLStyleValueTypeNumber, MGLStyleValueFunctionAllowed ], - @"size" : @[ MGLStyleValueTypeNumber, MGLStyleValueFunctionAllowed ], - @"rotate" : @[ MGLStyleValueTypeNumber, MGLStyleValueFunctionAllowed ], - @"alwaysVisible" : @[ MGLStyleValueTypeBoolean ] }, - MGLStyleKeyRaster : @{}, - MGLStyleKeyComposite : @{}, - MGLStyleKeyBackground : @{ - @"color" : @[ MGLStyleValueTypeColor ] } - }; - - return MGLStyleAllowedTypes; + // notify delegate + if ([self.delegate respondsToSelector:@selector(mapView:didSelectAnnotation:)]) + { + [self.delegate mapView:self didSelectAnnotation:annotation]; + } } -#pragma mark - Annotations - +- (SMCalloutView *)calloutViewForAnnotation:(id <MGLAnnotation>)annotation +{ + SMCalloutView *calloutView = [SMCalloutView platformCalloutView]; -- (NSArray *)annotations + if ([annotation respondsToSelector:@selector(title)]) calloutView.title = annotation.title; + if ([annotation respondsToSelector:@selector(subtitle)]) calloutView.subtitle = annotation.subtitle; + + calloutView.tintColor = self.tintColor; + + return calloutView; +} + +- (void)deselectAnnotation:(id <MGLAnnotation>)annotation animated:(BOOL)animated { - if ([_annotationsStore count]) + if ( ! annotation) return; + + if ([self.selectedAnnotation isEqual:annotation]) { - NSMutableArray *result = [NSMutableArray array]; + // dismiss popup + [self.selectedAnnotationCalloutView dismissCalloutAnimated:animated]; - NSEnumerator *keyEnumerator = [_annotationsStore keyEnumerator]; - id <MGLAnnotation> annotation; + // clean up + self.selectedAnnotationCalloutView = nil; + self.selectedAnnotation = nil; + } - while (annotation = [keyEnumerator nextObject]) + // notify delegate + if ([self.delegate respondsToSelector:@selector(mapView:didDeselectAnnotation:)]) + { + [self.delegate mapView:self didDeselectAnnotation:annotation]; + } +} + +#pragma mark - User Location - + +- (void)setShowsUserLocation:(BOOL)showsUserLocation +{ + if (showsUserLocation == _showsUserLocation) return; + + _showsUserLocation = showsUserLocation; + + if (showsUserLocation) + { + if ([self.delegate respondsToSelector:@selector(mapViewWillStartLocatingUser:)]) { - [result addObject:annotation]; + [self.delegate mapViewWillStartLocatingUser:self]; + } + + self.userLocationAnnotationView = [[MGLUserLocationAnnotationView alloc] initInMapView:self]; + + self.locationManager = [CLLocationManager new]; + +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 80000 + // enable iOS 8+ location authorization API + // + if ([CLLocationManager instancesRespondToSelector:@selector(requestWhenInUseAuthorization)]) + { + BOOL hasLocationDescription = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"NSLocationWhenInUseUsageDescription"] || + [[NSBundle mainBundle] objectForInfoDictionaryKey:@"NSLocationAlwaysUsageDescription"]; + NSAssert(hasLocationDescription, + @"For iOS 8 and above, your app must have a value for NSLocationWhenInUseUsageDescription or NSLocationAlwaysUsageDescription in its Info.plist"); + [self.locationManager requestWhenInUseAuthorization]; } +#endif + + self.locationManager.headingFilter = 5.0; + self.locationManager.delegate = self; + [self.locationManager startUpdatingLocation]; + } + else + { + [self.locationManager stopUpdatingLocation]; + [self.locationManager stopUpdatingHeading]; + self.locationManager.delegate = nil; + self.locationManager = nil; - return [NSArray arrayWithArray:result]; + if ([self.delegate respondsToSelector:@selector(mapViewDidStopLocatingUser:)]) + { + [self.delegate mapViewDidStopLocatingUser:self]; + } + + [self setUserTrackingMode:MGLUserTrackingModeNone animated:YES]; + + [self.userLocationAnnotationView removeFromSuperview]; + self.userLocationAnnotationView = nil; } +} - return nil; +- (void)setUserLocationAnnotationView:(MGLUserLocationAnnotationView *)newAnnotationView +{ + if ( ! [newAnnotationView isEqual:_userLocationAnnotationView]) + { + _userLocationAnnotationView = newAnnotationView; + } } -- (void)addAnnotation:(id <MGLAnnotation>)annotation ++ (NSSet *)keyPathsForValuesAffectingUserLocation { - if ( ! annotation) return; + return [NSSet setWithObject:@"userLocationAnnotationView"]; +} - // The core bulk add API is efficient with respect to indexing and - // screen refreshes, thus we should defer to it even for individual adds. - // - [self addAnnotations:@[ annotation ]]; +- (MGLUserLocation *)userLocation +{ + return self.userLocationAnnotationView.annotation; } -- (void)addAnnotations:(NSArray *)annotations +- (BOOL)isUserLocationVisible { - if ( ! annotations) return; + if (self.userLocationAnnotationView) + { + CGPoint locationPoint = [self convertCoordinate:self.userLocation.coordinate toPointToView:self]; - std::vector<mbgl::LatLng> latLngs; - latLngs.reserve(annotations.count); + CGRect locationRect = CGRectMake(locationPoint.x - self.userLocation.location.horizontalAccuracy, + locationPoint.y - self.userLocation.location.horizontalAccuracy, + self.userLocation.location.horizontalAccuracy * 2, + self.userLocation.location.horizontalAccuracy * 2); - std::vector<std::string> symbols; - symbols.reserve(annotations.count); + return CGRectIntersectsRect([self bounds], locationRect); + } - BOOL delegateImplementsSymbolLookup = [self.delegate respondsToSelector:@selector(mapView:symbolNameForAnnotation:)]; + return NO; +} - for (id <MGLAnnotation> annotation in annotations) +- (void)setUserTrackingMode:(MGLUserTrackingMode)mode +{ + [self setUserTrackingMode:mode animated:YES]; +} + +- (void)setUserTrackingMode:(MGLUserTrackingMode)mode animated:(BOOL)animated +{ + if (mode == _userTrackingMode) return; + + if (mode == MGLUserTrackingModeFollowWithHeading && ! CLLocationCoordinate2DIsValid(self.userLocation.coordinate)) { - assert([annotation conformsToProtocol:@protocol(MGLAnnotation)]); + mode = MGLUserTrackingModeNone; + } - latLngs.push_back(coordinateToLatLng(annotation.coordinate)); + _userTrackingMode = mode; - NSString *symbolName = nil; + switch (_userTrackingMode) + { + case MGLUserTrackingModeNone: + default: + { + [self.locationManager stopUpdatingHeading]; - if (delegateImplementsSymbolLookup) + break; + } + case MGLUserTrackingModeFollow: { - symbolName = [self.delegate mapView:self symbolNameForAnnotation:annotation]; + self.showsUserLocation = YES; + + [self.locationManager stopUpdatingHeading]; + + if (self.userLocationAnnotationView) + { + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wdeprecated-declarations" + [self locationManager:self.locationManager didUpdateToLocation:self.userLocation.location fromLocation:self.userLocation.location]; + #pragma clang diagnostic pop + } + + break; } + case MGLUserTrackingModeFollowWithHeading: + { + self.showsUserLocation = YES; - symbols.push_back((symbolName ? [symbolName cStringUsingEncoding:[NSString defaultCStringEncoding]] : "")); - } + if (self.zoomLevel < 3) [self setZoomLevel:3 animated:YES]; - std::vector<uint32_t> annotationIDs = mbglMap->addPointAnnotations(latLngs, symbols); + if (self.userLocationAnnotationView) + { + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wdeprecated-declarations" + [self locationManager:self.locationManager didUpdateToLocation:self.userLocation.location fromLocation:self.userLocation.location]; + #pragma clang diagnostic pop + } - for (size_t i = 0; i < annotationIDs.size(); ++i) + [self updateHeadingForDeviceOrientation]; + + [self.locationManager startUpdatingHeading]; + + break; + } + } + + if ([self.delegate respondsToSelector:@selector(mapView:didChangeUserTrackingMode:animated:)]) { - [self.annotationsStore setObject:@{ MGLAnnotationIDKey : @(annotationIDs[i]) } - forKey:annotations[i]]; + [self.delegate mapView:self didChangeUserTrackingMode:_userTrackingMode animated:animated]; } } -- (void)removeAnnotation:(id <MGLAnnotation>)annotation +- (void)locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation { - if ( ! annotation) return; + (void)manager; - // The core bulk deletion API is efficient with respect to indexing - // and screen refreshes, thus we should defer to it even for - // individual deletes. - // - [self removeAnnotations:@[ annotation ]]; -} + if ( ! _showsUserLocation || ! newLocation || ! CLLocationCoordinate2DIsValid(newLocation.coordinate)) return; -- (void)removeAnnotations:(NSArray *)annotations -{ - if ( ! annotations) return; + if ([newLocation distanceFromLocation:oldLocation] || ! oldLocation) + { + self.userLocation.location = newLocation; - std::vector<uint32_t> annotationIDsToRemove; - annotationIDsToRemove.reserve(annotations.count); + // deselect user if applicable since we don't do callout tracking yet + if ([self.selectedAnnotation isEqual:self.userLocation]) [self deselectAnnotation:self.userLocation animated:NO]; - for (id <MGLAnnotation> annotation in annotations) + if ([self.delegate respondsToSelector:@selector(mapView:didUpdateUserLocation:)]) + { + [self.delegate mapView:self didUpdateUserLocation:self.userLocation]; + } + } + + if (self.userTrackingMode != MGLUserTrackingModeNone) { - assert([annotation conformsToProtocol:@protocol(MGLAnnotation)]); + // center on user location unless we're already centered there (or very close) + // + CGPoint mapCenterPoint = [self convertPoint:self.center fromView:self.superview]; + CGPoint userLocationPoint = [self convertCoordinate:self.userLocation.coordinate toPointToView:self]; - annotationIDsToRemove.push_back([[[self.annotationsStore objectForKey:annotation] objectForKey:MGLAnnotationIDKey] unsignedIntValue]); - [self.annotationsStore removeObjectForKey:annotation]; + if (std::abs(userLocationPoint.x - mapCenterPoint.x) > 1.0 || std::abs(userLocationPoint.y - mapCenterPoint.y) > 1.0) + { + if (round(self.zoomLevel) >= 10) + { + // at sufficient detail, just re-center the map; don't zoom + // + [self setCenterCoordinate:self.userLocation.location.coordinate animated:YES preservingTracking:YES]; + } + else + { + // otherwise re-center and zoom in to near accuracy confidence + // + float delta = (newLocation.horizontalAccuracy / 110000) * 1.2; // approx. meter per degree latitude, plus some margin + + CLLocationCoordinate2D desiredSouthWest = CLLocationCoordinate2DMake(newLocation.coordinate.latitude - delta, + newLocation.coordinate.longitude - delta); + + CLLocationCoordinate2D desiredNorthEast = CLLocationCoordinate2DMake(newLocation.coordinate.latitude + delta, + newLocation.coordinate.longitude + delta); + + CGFloat pixelRadius = fminf(self.bounds.size.width, self.bounds.size.height) / 2; + + CLLocationCoordinate2D actualSouthWest = [self convertPoint:CGPointMake(userLocationPoint.x - pixelRadius, + userLocationPoint.y - pixelRadius) + toCoordinateFromView:self]; + + CLLocationCoordinate2D actualNorthEast = [self convertPoint:CGPointMake(userLocationPoint.x + pixelRadius, + userLocationPoint.y + pixelRadius) + toCoordinateFromView:self]; + + if (desiredNorthEast.latitude != actualNorthEast.latitude || + desiredNorthEast.longitude != actualNorthEast.longitude || + desiredSouthWest.latitude != actualSouthWest.latitude || + desiredSouthWest.longitude != actualSouthWest.longitude) + { + // assumes we won't disrupt tracking mode + [self zoomToSouthWestCoordinate:desiredSouthWest northEastCoordinate:desiredNorthEast animated:YES]; + } + } + } } - mbglMap->removeAnnotations(annotationIDsToRemove); + self.userLocationAnnotationView.haloLayer.hidden = ! CLLocationCoordinate2DIsValid(self.userLocation.coordinate) || + newLocation.horizontalAccuracy > 10; + + [self updateUserLocationAnnotationView]; } -- (NSArray *)selectedAnnotations +- (BOOL)locationManagerShouldDisplayHeadingCalibration:(CLLocationManager *)manager { - return (self.selectedAnnotation ? @[ self.selectedAnnotation ] : @[]); + (void)manager; + + if (self.displayHeadingCalibration) [self.locationManager performSelector:@selector(dismissHeadingCalibrationDisplay) + withObject:nil + afterDelay:10.0]; + + return self.displayHeadingCalibration; } -- (void)setSelectedAnnotations:(NSArray *)selectedAnnotations +- (void)locationManager:(CLLocationManager *)manager didUpdateHeading:(CLHeading *)newHeading { - if ( ! selectedAnnotations.count) return; + (void)manager; - id <MGLAnnotation> firstAnnotation = selectedAnnotations[0]; + if ( ! _showsUserLocation || self.pan.state == UIGestureRecognizerStateBegan || newHeading.headingAccuracy < 0) return; - assert([firstAnnotation conformsToProtocol:@protocol(MGLAnnotation)]); + self.userLocation.heading = newHeading; - if ( ! [self viewportBounds].contains(coordinateToLatLng(firstAnnotation.coordinate))) return; + if ([self.delegate respondsToSelector:@selector(mapView:didUpdateUserLocation:)]) + { + [self.delegate mapView:self didUpdateUserLocation:self.userLocation]; - self.selectedAnnotation = firstAnnotation; -} + if ( ! _showsUserLocation) return; + } -- (void)selectAnnotation:(id <MGLAnnotation>)annotation animated:(BOOL)animated -{ - (void)animated; + CLLocationDirection headingDirection = (newHeading.trueHeading > 0 ? newHeading.trueHeading : newHeading.magneticHeading); - if ( ! annotation) return; + if (headingDirection > 0 && self.userTrackingMode == MGLUserTrackingModeFollowWithHeading) + { + mbglMap->setBearing(headingDirection, secondsAsDuration(MGLAnimationDuration)); + } +} - if ( ! [self viewportBounds].contains(coordinateToLatLng(annotation.coordinate))) return; +- (void)locationManager:(CLLocationManager *)manager didChangeAuthorizationStatus:(CLAuthorizationStatus)status +{ + (void)manager; - self.selectedAnnotation = annotation; + if (status == kCLAuthorizationStatusDenied || status == kCLAuthorizationStatusRestricted) + { + self.userTrackingMode = MGLUserTrackingModeNone; + self.showsUserLocation = NO; + } } -- (void)deselectAnnotation:(id <MGLAnnotation>)annotation animated:(BOOL)animated +- (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error { - (void)animated; + (void)manager; - if ( ! annotation) return; + if ([error code] == kCLErrorDenied) + { + self.userTrackingMode = MGLUserTrackingModeNone; + self.showsUserLocation = NO; - if ([self.selectedAnnotation isEqual:annotation]) self.selectedAnnotation = nil; + if ([self.delegate respondsToSelector:@selector(mapView:didFailToLocateUserWithError:)]) + { + [self.delegate mapView:self didFailToLocateUserWithError:error]; + } + } +} + +- (void)updateHeadingForDeviceOrientation +{ + if (self.locationManager) + { + // note that right/left device and interface orientations are opposites (see UIApplication.h) + // + switch ([[UIApplication sharedApplication] statusBarOrientation]) + { + case (UIInterfaceOrientationLandscapeLeft): + { + self.locationManager.headingOrientation = CLDeviceOrientationLandscapeRight; + break; + } + case (UIInterfaceOrientationLandscapeRight): + { + self.locationManager.headingOrientation = CLDeviceOrientationLandscapeLeft; + break; + } + case (UIInterfaceOrientationPortraitUpsideDown): + { + self.locationManager.headingOrientation = CLDeviceOrientationPortraitUpsideDown; + break; + } + case (UIInterfaceOrientationPortrait): + default: + { + self.locationManager.headingOrientation = CLDeviceOrientationPortrait; + break; + } + } + } } #pragma mark - Utility - @@ -1844,6 +2003,11 @@ CLLocationCoordinate2D latLngToCoordinate(mbgl::LatLng latLng) case mbgl::MapChangeRegionWillChange: case mbgl::MapChangeRegionWillChangeAnimated: { + [self updateUserLocationAnnotationView]; + [self updateCompass]; + + [self deselectAnnotation:self.selectedAnnotation animated:NO]; + BOOL animated = ([change unsignedIntegerValue] == mbgl::MapChangeRegionWillChangeAnimated); @synchronized (self.regionChangeDelegateQueue) @@ -1874,9 +2038,20 @@ CLLocationCoordinate2D latLngToCoordinate(mbgl::LatLng latLng) } break; } + case mbgl::MapChangeRegionIsChanging: + { + [self updateUserLocationAnnotationView]; + [self updateCompass]; + + if ([self.delegate respondsToSelector:@selector(mapViewRegionIsChanging:)]) + { + [self.delegate mapViewRegionIsChanging:self]; + } + } case mbgl::MapChangeRegionDidChange: case mbgl::MapChangeRegionDidChangeAnimated: { + [self updateUserLocationAnnotationView]; [self updateCompass]; if (self.pan.state == UIGestureRecognizerStateChanged || @@ -1942,6 +2117,32 @@ CLLocationCoordinate2D latLngToCoordinate(mbgl::LatLng latLng) } } +- (void)updateUserLocationAnnotationView +{ + if ( ! CLLocationCoordinate2DIsValid(self.userLocation.coordinate)) { + self.userLocationAnnotationView.layer.hidden = YES; + return; + } + + if ( ! self.userLocationAnnotationView.superview) [self.glView addSubview:self.userLocationAnnotationView]; + + CGPoint userPoint = [self convertCoordinate:self.userLocation.coordinate toPointToView:self]; + + if (CGRectContainsPoint(CGRectInset(self.bounds, -MGLAnnotationUpdateViewportOutset.width, + -MGLAnnotationUpdateViewportOutset.height), userPoint)) + { + self.userLocationAnnotationView.center = userPoint; + + self.userLocationAnnotationView.layer.hidden = NO; + + [self.userLocationAnnotationView setupLayers]; + } + else + { + self.userLocationAnnotationView.layer.hidden = YES; + } +} + - (void)updateCompass { double degrees = mbglMap->getBearing() * -1; @@ -1993,8 +2194,11 @@ CLLocationCoordinate2D latLngToCoordinate(mbgl::LatLng latLng) - (void)invalidate { - // This is run in the main/UI thread. + assert([[NSThread currentThread] isMainThread]); + [self.glView setNeedsDisplay]; + + [self notifyMapChange:@(mbgl::MapChangeRegionIsChanging)]; } class MBGLView : public mbgl::View @@ -2022,12 +2226,9 @@ class MBGLView : public mbgl::View } else { - dispatch_async(dispatch_get_main_queue(), ^ - { - [nativeView performSelector:@selector(notifyMapChange:) - withObject:@(change) - afterDelay:0]; - }); + assert([[NSThread currentThread] isMainThread]); + + [nativeView notifyMapChange:@(change)]; } } |