summaryrefslogtreecommitdiff
path: root/platform/ios/MGLMapView.mm
diff options
context:
space:
mode:
Diffstat (limited to 'platform/ios/MGLMapView.mm')
-rw-r--r--platform/ios/MGLMapView.mm1607
1 files changed, 1607 insertions, 0 deletions
diff --git a/platform/ios/MGLMapView.mm b/platform/ios/MGLMapView.mm
new file mode 100644
index 0000000000..dde15567a2
--- /dev/null
+++ b/platform/ios/MGLMapView.mm
@@ -0,0 +1,1607 @@
+#import "MGLMapView.h"
+
+#import <mbgl/platform/darwin/log_nslog.hpp>
+#import <mbgl/platform/gl.hpp>
+
+#import <GLKit/GLKit.h>
+#import <OpenGLES/EAGL.h>
+
+#include <mbgl/mbgl.hpp>
+#include <mbgl/platform/platform.hpp>
+#include <mbgl/platform/darwin/reachability.h>
+#include <mbgl/storage/default_file_source.hpp>
+#include <mbgl/storage/default/sqlite_cache.hpp>
+#include <mbgl/storage/network_status.hpp>
+
+#import "MGLTypes.h"
+#import "MGLStyleFunctionValue.h"
+
+#import "UIColor+MGLAdditions.h"
+#import "NSArray+MGLAdditions.h"
+#import "NSDictionary+MGLAdditions.h"
+
+
+// Returns the path to the default cache database on this system.
+const std::string &defaultCacheDatabase() {
+ static const std::string path = []() -> std::string {
+ NSArray *paths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES);
+ if ([paths count] == 0) {
+ // Disable the cache if we don't have a location to write.
+ return "";
+ }
+
+ NSString *libraryDirectory = [paths objectAtIndex:0];
+ return [[libraryDirectory stringByAppendingPathComponent:@"cache.db"] UTF8String];
+ }();
+ return path;
+}
+
+
+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;
+
+#pragma mark - Private -
+
+@interface MGLMapView () <UIGestureRecognizerDelegate, GLKViewDelegate>
+
+@property (nonatomic) EAGLContext *context;
+@property (nonatomic) GLKView *glView;
+@property (nonatomic) NSOperationQueue *regionChangeDelegateQueue;
+@property (nonatomic) UIImageView *compass;
+@property (nonatomic) UIImageView *logoBug;
+@property (nonatomic) UIButton *attributionButton;
+@property (nonatomic) UIPanGestureRecognizer *pan;
+@property (nonatomic) UIPinchGestureRecognizer *pinch;
+@property (nonatomic) UIRotationGestureRecognizer *rotate;
+@property (nonatomic) UILongPressGestureRecognizer *quickZoom;
+@property (nonatomic, readonly) NSDictionary *allowedStyleTypes;
+@property (nonatomic) CGPoint centerPoint;
+@property (nonatomic) CGFloat scale;
+@property (nonatomic) CGFloat angle;
+@property (nonatomic) CGFloat quickZoomStart;
+@property (nonatomic, getter=isAnimatingGesture) BOOL animatingGesture;
+@property (nonatomic, readonly, getter=isRotationAllowed) BOOL rotationAllowed;
+
+@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
+
+#pragma mark - Setup -
+
+@dynamic debugActive;
+
+class MBGLView;
+
+std::chrono::steady_clock::duration secondsAsDuration(float duration)
+{
+ return std::chrono::duration_cast<std::chrono::steady_clock::duration>(std::chrono::duration<float, std::chrono::seconds::period>(duration));
+}
+
+mbgl::Map *mbglMap = nullptr;
+MBGLView *mbglView = nullptr;
+mbgl::SQLiteCache *mbglFileCache = nullptr;
+mbgl::DefaultFileSource *mbglFileSource = nullptr;
+
+- (instancetype)initWithFrame:(CGRect)frame styleJSON:(NSString *)styleJSON accessToken:(NSString *)accessToken
+{
+ self = [super initWithFrame:frame];
+
+ if (self && [self commonInit])
+ {
+ if (accessToken) [self setAccessToken:accessToken];
+
+ if (styleJSON || accessToken)
+ {
+ // If style is set directly, pass it on. If not, if we have an access
+ // token, we can pass nil and use the default style.
+ //
+ [self setStyleJSON:styleJSON];
+ }
+ }
+
+ return self;
+}
+
+- (instancetype)initWithFrame:(CGRect)frame accessToken:(NSString *)accessToken
+{
+ return [self initWithFrame:frame styleJSON:nil accessToken:accessToken];
+}
+
+- (instancetype)initWithCoder:(NSCoder *)decoder
+{
+ self = [super initWithCoder:decoder];
+
+ if (self && [self commonInit])
+ {
+ return self;
+ }
+
+ return nil;
+}
+
+- (void)setAccessToken:(NSString *)accessToken
+{
+ if (accessToken)
+ {
+ mbglMap->setAccessToken((std::string)[accessToken cStringUsingEncoding:[NSString defaultCStringEncoding]]);
+ }
+}
+
+- (void)setStyleJSON:(NSString *)styleJSON
+{
+ if ( ! styleJSON)
+ {
+ [self useBundledStyleNamed:@"bright-v6"];
+ }
+ else
+ {
+ if ([@(mbglMap->getStyleJSON().c_str()) length]) mbglMap->stop();
+ mbglMap->setStyleJSON((std::string)[styleJSON cStringUsingEncoding:[NSString defaultCStringEncoding]]);
+ mbglMap->start();
+ }
+}
+
+- (void)setStyleURL:(NSString *)filePathURL
+{
+ if ([@(mbglMap->getStyleJSON().c_str()) length]) mbglMap->stop();
+ mbglMap->setStyleURL(std::string("asset://") + [filePathURL UTF8String]);
+ mbglMap->start();
+}
+
+- (BOOL)commonInit
+{
+ // set logging backend
+ //
+ mbgl::Log::Set<mbgl::NSLogBackend>();
+
+ // create context
+ //
+ _context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
+
+ if ( ! _context)
+ {
+ mbgl::Log::Error(mbgl::Event::Setup, "Failed to create OpenGL ES context");
+
+ return NO;
+ }
+
+ // setup accessibility
+ //
+ self.accessibilityLabel = @"Map";
+
+ // create GL view
+ //
+ _glView = [[GLKView alloc] initWithFrame:self.bounds context:_context];
+ _glView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
+ _glView.enableSetNeedsDisplay = NO;
+ _glView.drawableStencilFormat = GLKViewDrawableStencilFormat8;
+ _glView.drawableDepthFormat = GLKViewDrawableDepthFormat16;
+ _glView.delegate = self;
+ [_glView bindDrawable];
+ [self addSubview:_glView];
+
+
+ // load extensions
+ //
+ const std::string extensions = (char *)glGetString(GL_EXTENSIONS);
+ {
+ using namespace mbgl;
+
+ if (extensions.find("GL_OES_vertex_array_object") != std::string::npos) {
+ gl::BindVertexArray = glBindVertexArrayOES;
+ gl::DeleteVertexArrays = glDeleteVertexArraysOES;
+ gl::GenVertexArrays = glGenVertexArraysOES;
+ gl::IsVertexArray = glIsVertexArrayOES;
+ }
+ }
+
+ // setup mbgl map
+ //
+ mbglView = new MBGLView(self);
+ mbglFileCache = new mbgl::SQLiteCache(defaultCacheDatabase());
+ mbglFileSource = new mbgl::DefaultFileSource(mbglFileCache);
+ mbglMap = new mbgl::Map(*mbglView, *mbglFileSource);
+ mbglMap->resize(self.bounds.size.width, self.bounds.size.height, _glView.contentScaleFactor, _glView.drawableWidth, _glView.drawableHeight);
+
+ // Notify map object when network reachability status changes.
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(reachabilityChanged:)
+ name:kReachabilityChangedNotification
+ object:nil];
+
+ Reachability* reachability = [Reachability reachabilityForInternetConnection];
+ [reachability startNotifier];
+
+ // setup logo bug
+ //
+ _logoBug = [[UIImageView alloc] initWithImage:[MGLMapView resourceImageNamed:@"mapbox.png"]];
+ _logoBug.accessibilityLabel = @"Mapbox logo";
+ _logoBug.frame = CGRectMake(8, self.bounds.size.height - _logoBug.bounds.size.height - 4, _logoBug.bounds.size.width, _logoBug.bounds.size.height);
+ _logoBug.translatesAutoresizingMaskIntoConstraints = NO;
+ [self addSubview:_logoBug];
+
+ // setup attribution
+ //
+ _attributionButton = [UIButton buttonWithType:UIButtonTypeInfoLight];
+ _attributionButton.accessibilityLabel = @"Attribution info";
+ [_attributionButton addTarget:self action:@selector(showAttribution:) forControlEvents:UIControlEventTouchUpInside];
+ _attributionButton.frame = CGRectMake(self.bounds.size.width - _attributionButton.bounds.size.width - 8, self.bounds.size.height - _attributionButton.bounds.size.height - 8, _attributionButton.bounds.size.width, _attributionButton.bounds.size.height);
+ _attributionButton.translatesAutoresizingMaskIntoConstraints = NO;
+ [self addSubview:_attributionButton];
+
+ // setup compass
+ //
+ _compass = [[UIImageView alloc] initWithImage:[MGLMapView resourceImageNamed:@"Compass.png"]];
+ _compass.accessibilityLabel = @"Compass";
+ UIImage *compassImage = [MGLMapView resourceImageNamed:@"Compass.png"];
+ _compass.frame = CGRectMake(0, 0, compassImage.size.width, compassImage.size.height);
+ _compass.alpha = 0;
+ UIView *container = [[UIView alloc] initWithFrame:CGRectMake(self.bounds.size.width - compassImage.size.width - 5, 5, compassImage.size.width, compassImage.size.height)];
+ [container addSubview:_compass];
+ container.translatesAutoresizingMaskIntoConstraints = NO;
+ [container addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleCompassTapGesture:)]];
+ [self addSubview:container];
+
+ self.viewControllerForLayoutGuides = nil;
+
+ // setup interaction
+ //
+ _pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePanGesture:)];
+ _pan.delegate = self;
+ [self addGestureRecognizer:_pan];
+ _scrollEnabled = YES;
+
+ _pinch = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(handlePinchGesture:)];
+ _pinch.delegate = self;
+ [self addGestureRecognizer:_pinch];
+ _zoomEnabled = YES;
+
+ _rotate = [[UIRotationGestureRecognizer alloc] initWithTarget:self action:@selector(handleRotateGesture:)];
+ _rotate.delegate = self;
+ [self addGestureRecognizer:_rotate];
+ _rotateEnabled = YES;
+
+ UITapGestureRecognizer *doubleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleDoubleTapGesture:)];
+ doubleTap.numberOfTapsRequired = 2;
+ [self addGestureRecognizer:doubleTap];
+
+ UITapGestureRecognizer *twoFingerTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTwoFingerTapGesture:)];
+ twoFingerTap.numberOfTouchesRequired = 2;
+ [twoFingerTap requireGestureRecognizerToFail:_pinch];
+ [twoFingerTap requireGestureRecognizerToFail:_rotate];
+ [self addGestureRecognizer:twoFingerTap];
+
+ if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone)
+ {
+ _quickZoom = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleQuickZoomGesture:)];
+ _quickZoom.numberOfTapsRequired = 1;
+ _quickZoom.minimumPressDuration = 0.25;
+ [self addGestureRecognizer:_quickZoom];
+ }
+
+ // observe app activity
+ //
+ [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(appDidBackground:) name:UIApplicationDidEnterBackgroundNotification object:nil];
+ [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(appWillForeground:) name:UIApplicationWillEnterForegroundNotification object:nil];
+
+ // set initial position
+ //
+ mbglMap->setLonLatZoom(0, 0, mbglMap->getMinZoom());
+
+ // setup change delegate queue
+ //
+ _regionChangeDelegateQueue = [NSOperationQueue new];
+ _regionChangeDelegateQueue.maxConcurrentOperationCount = 1;
+
+ return YES;
+}
+
+-(void)reachabilityChanged:(NSNotification*)notification
+{
+ Reachability *reachability = [notification object];
+ if ([reachability isReachable]) {
+ mbgl::NetworkStatus::Reachable();
+ }
+}
+
+- (void)dealloc
+{
+ [_regionChangeDelegateQueue cancelAllOperations];
+
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+
+ if (mbglMap)
+ {
+ delete mbglMap;
+ mbglMap = nullptr;
+ }
+
+ if (mbglFileSource)
+ {
+ delete mbglFileSource;
+ mbglFileSource = nullptr;
+ }
+
+ if (mbglView)
+ {
+ delete mbglView;
+ mbglView = nullptr;
+ }
+
+ if ([[EAGLContext currentContext] isEqual:_context])
+ {
+ [EAGLContext setCurrentContext:nil];
+ }
+}
+
+#pragma mark - Layout -
+
+- (void)setFrame:(CGRect)frame
+{
+ [super setFrame:frame];
+
+ [self setNeedsLayout];
+}
+
+- (void)setBounds:(CGRect)bounds
+{
+ [super setBounds:bounds];
+
+ [self setNeedsLayout];
+}
+
++ (BOOL)requiresConstraintBasedLayout
+{
+ return YES;
+}
+
+- (void)didMoveToSuperview
+{
+ [self.compass.superview removeConstraints:self.compass.superview.constraints];
+ [self.logoBug removeConstraints:self.logoBug.constraints];
+ [self.attributionButton removeConstraints:self.attributionButton.constraints];
+
+ [self setNeedsUpdateConstraints];
+}
+
+- (void)setViewControllerForLayoutGuides:(UIViewController *)viewController
+{
+ _viewControllerForLayoutGuides = viewController;
+
+ [self.compass.superview removeConstraints:self.compass.superview.constraints];
+ [self.logoBug removeConstraints:self.logoBug.constraints];
+ [self.attributionButton removeConstraints:self.attributionButton.constraints];
+
+ [self setNeedsUpdateConstraints];
+}
+
+- (void)updateConstraints
+{
+ // If we have a view controller reference, use its layout guides for our various top & bottom
+ // views so they don't underlap navigation or tool bars. If we don't have a reference, apply
+ // constraints against ourself to maintain (albeit less ideal) placement of the subviews.
+ //
+ NSString *topGuideFormatString = (self.viewControllerForLayoutGuides ? @"[topLayoutGuide]" : @"|");
+ NSString *bottomGuideFormatString = (self.viewControllerForLayoutGuides ? @"[bottomLayoutGuide]" : @"|");
+
+ id topGuideViewsObject = (self.viewControllerForLayoutGuides ? (id)self.viewControllerForLayoutGuides.topLayoutGuide : (id)@"");
+ id bottomGuideViewsObject = (self.viewControllerForLayoutGuides ? (id)self.viewControllerForLayoutGuides.bottomLayoutGuide : (id)@"");
+
+ UIView *constraintParentView = (self.viewControllerForLayoutGuides.view ? self.viewControllerForLayoutGuides.view : self);
+
+ // compass
+ //
+ UIView *compassContainer = self.compass.superview;
+
+ [constraintParentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:[NSString stringWithFormat:@"V:%@-topSpacing-[container]", topGuideFormatString]
+ options:0
+ metrics:@{ @"topSpacing" : @(5) }
+ views:@{ @"topLayoutGuide" : topGuideViewsObject,
+ @"container" : compassContainer }]];
+
+ [constraintParentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:[container]-rightSpacing-|"
+ options:0
+ metrics:@{ @"rightSpacing" : @(5) }
+ views:@{ @"container" : compassContainer }]];
+
+ [compassContainer addConstraint:[NSLayoutConstraint constraintWithItem:compassContainer
+ attribute:NSLayoutAttributeWidth
+ relatedBy:NSLayoutRelationEqual
+ toItem:nil
+ attribute:NSLayoutAttributeNotAnAttribute
+ multiplier:1
+ constant:self.compass.image.size.width]];
+
+ [compassContainer addConstraint:[NSLayoutConstraint constraintWithItem:compassContainer
+ attribute:NSLayoutAttributeHeight
+ relatedBy:NSLayoutRelationEqual
+ toItem:nil
+ attribute:NSLayoutAttributeNotAnAttribute
+ multiplier:1
+ constant:self.compass.image.size.height]];
+
+ // logo bug
+ //
+ [constraintParentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:[NSString stringWithFormat:@"V:[logoBug]-bottomSpacing-%@", bottomGuideFormatString]
+ options:0
+ metrics:@{ @"bottomSpacing" : @(4) }
+ views:@{ @"logoBug" : self.logoBug,
+ @"bottomLayoutGuide" : bottomGuideViewsObject }]];
+
+ [constraintParentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-leftSpacing-[logoBug]"
+ options:0
+ metrics:@{ @"leftSpacing" : @(8) }
+ views:@{ @"logoBug" : self.logoBug }]];
+
+ // attribution button
+ //
+ [constraintParentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:[NSString stringWithFormat:@"V:[attributionButton]-bottomSpacing-%@", bottomGuideFormatString]
+ options:0
+ metrics:@{ @"bottomSpacing" : @(8) }
+ views:@{ @"attributionButton" : self.attributionButton,
+ @"bottomLayoutGuide" : bottomGuideViewsObject }]];
+
+ [constraintParentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:[attributionButton]-rightSpacing-|"
+ options:0
+ metrics:@{ @"rightSpacing" : @(8) }
+ views:@{ @"attributionButton" : self.attributionButton }]];
+
+ [super updateConstraints];
+}
+
+- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect
+{
+ mbglMap->resize(rect.size.width, rect.size.height, view.contentScaleFactor, view.drawableWidth, view.drawableHeight);
+}
+
+- (void)layoutSubviews
+{
+ mbglMap->update();
+
+ [super layoutSubviews];
+}
+
+#pragma mark - Conversions -
+
++ (CGFloat)degreesToRadians:(CGFloat)degrees
+{
+ return degrees * M_PI / 180;
+}
+
++ (CGFloat)radiansToDegrees:(CGFloat)radians
+{
+ return radians * 180 / M_PI;
+}
+
+#pragma mark - Life Cycle -
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wunused-parameter"
+
+- (void)appDidBackground:(NSNotification *)notification
+{
+ mbglMap->stop();
+
+ [self.glView deleteDrawable];
+}
+
+- (void)appWillForeground:(NSNotification *)notification
+{
+ [self.glView bindDrawable];
+
+ mbglMap->start();
+}
+
+#pragma mark - Gestures -
+
+- (void)handleCompassTapGesture:(id)sender
+{
+ [self resetNorthAnimated:YES];
+}
+
+#pragma clang diagnostic pop
+
+- (void)handlePanGesture:(UIPanGestureRecognizer *)pan
+{
+ if ( ! self.isScrollEnabled) return;
+
+ mbglMap->cancelTransitions();
+
+ if (pan.state == UIGestureRecognizerStateBegan)
+ {
+ self.centerPoint = CGPointMake(0, 0);
+ }
+ else if (pan.state == UIGestureRecognizerStateChanged)
+ {
+ CGPoint delta = CGPointMake([pan translationInView:pan.view].x - self.centerPoint.x,
+ [pan translationInView:pan.view].y - self.centerPoint.y);
+
+ mbglMap->moveBy(delta.x, delta.y);
+
+ self.centerPoint = CGPointMake(self.centerPoint.x + delta.x, self.centerPoint.y + delta.y);
+ }
+ else if (pan.state == UIGestureRecognizerStateEnded || pan.state == UIGestureRecognizerStateCancelled)
+ {
+ CGPoint velocity = [pan velocityInView:pan.view];
+ CGFloat duration = 0;
+
+ if ( ! CGPointEqualToPoint(velocity, CGPointZero))
+ {
+ CGFloat ease = 0.25;
+
+ velocity.x = velocity.x * ease;
+ velocity.y = velocity.y * ease;
+
+ CGFloat speed = sqrt(velocity.x * velocity.x + velocity.y * velocity.y);
+ CGFloat deceleration = 2500;
+ duration = speed / (deceleration * ease);
+ }
+
+ CGPoint offset = CGPointMake(velocity.x * duration / 2, velocity.y * duration / 2);
+
+ mbglMap->moveBy(offset.x, offset.y, secondsAsDuration(duration));
+
+ if (duration)
+ {
+ self.animatingGesture = YES;
+
+ __weak MGLMapView *weakSelf = self;
+
+ [self animateWithDelay:duration animations:^
+ {
+ weakSelf.animatingGesture = NO;
+
+ [weakSelf notifyMapChange:@(mbgl::MapChangeRegionDidChangeAnimated)];
+ }];
+ }
+ }
+}
+
+- (void)handlePinchGesture:(UIPinchGestureRecognizer *)pinch
+{
+ if ( ! self.isZoomEnabled) return;
+
+ if (mbglMap->getZoom() <= mbglMap->getMinZoom() && pinch.scale < 1) return;
+
+ mbglMap->cancelTransitions();
+
+ if (pinch.state == UIGestureRecognizerStateBegan)
+ {
+ mbglMap->startScaling();
+
+ self.scale = mbglMap->getScale();
+ }
+ else if (pinch.state == UIGestureRecognizerStateChanged)
+ {
+ CGFloat newScale = self.scale * pinch.scale;
+
+ if (log2(newScale) < mbglMap->getMinZoom()) return;
+
+ double scale = mbglMap->getScale();
+
+ mbglMap->scaleBy(newScale / scale, [pinch locationInView:pinch.view].x, [pinch locationInView:pinch.view].y);
+ }
+ else if (pinch.state == UIGestureRecognizerStateEnded || pinch.state == UIGestureRecognizerStateCancelled)
+ {
+ mbglMap->stopScaling();
+
+ [self unrotateIfNeededAnimated:YES];
+
+ [self notifyMapChange:@(mbgl::MapChangeRegionDidChangeAnimated)];
+ }
+}
+
+- (void)handleRotateGesture:(UIRotationGestureRecognizer *)rotate
+{
+ if ( ! self.isRotateEnabled) return;
+
+ mbglMap->cancelTransitions();
+
+ if (rotate.state == UIGestureRecognizerStateBegan)
+ {
+ mbglMap->startRotating();
+
+ self.angle = [MGLMapView degreesToRadians:mbglMap->getBearing()] * -1;
+ }
+ else if (rotate.state == UIGestureRecognizerStateChanged)
+ {
+ CGFloat newDegrees = [MGLMapView radiansToDegrees:(self.angle + rotate.rotation)] * -1;
+
+ // constrain to +/-30 degrees when merely rotating like Apple does
+ //
+ if ( ! self.isRotationAllowed && fabsf(self.pinch.scale) < 10)
+ {
+ newDegrees = fminf(newDegrees, 30);
+ newDegrees = fmaxf(newDegrees, -30);
+ }
+
+ mbglMap->setBearing(newDegrees,
+ [rotate locationInView:rotate.view].x,
+ [rotate locationInView:rotate.view].y);
+ }
+ else if (rotate.state == UIGestureRecognizerStateEnded || rotate.state == UIGestureRecognizerStateCancelled)
+ {
+ mbglMap->stopRotating();
+
+ [self unrotateIfNeededAnimated:YES];
+
+ [self notifyMapChange:@(mbgl::MapChangeRegionDidChangeAnimated)];
+ }
+}
+
+- (void)handleDoubleTapGesture:(UITapGestureRecognizer *)doubleTap
+{
+ if ( ! self.isZoomEnabled) return;
+
+ mbglMap->cancelTransitions();
+
+ if (doubleTap.state == UIGestureRecognizerStateEnded)
+ {
+ mbglMap->scaleBy(2, [doubleTap locationInView:doubleTap.view].x, [doubleTap locationInView:doubleTap.view].y, secondsAsDuration(MGLAnimationDuration));
+
+ self.animatingGesture = YES;
+
+ __weak MGLMapView *weakSelf = self;
+
+ [self animateWithDelay:MGLAnimationDuration animations:^
+ {
+ weakSelf.animatingGesture = NO;
+
+ [weakSelf unrotateIfNeededAnimated:YES];
+
+ [weakSelf notifyMapChange:@(mbgl::MapChangeRegionDidChangeAnimated)];
+ }];
+ }
+}
+
+- (void)handleTwoFingerTapGesture:(UITapGestureRecognizer *)twoFingerTap
+{
+ if ( ! self.isZoomEnabled) return;
+
+ if (mbglMap->getZoom() == mbglMap->getMinZoom()) return;
+
+ mbglMap->cancelTransitions();
+
+ if (twoFingerTap.state == UIGestureRecognizerStateEnded)
+ {
+ mbglMap->scaleBy(0.5, [twoFingerTap locationInView:twoFingerTap.view].x, [twoFingerTap locationInView:twoFingerTap.view].y, secondsAsDuration(MGLAnimationDuration));
+
+ self.animatingGesture = YES;
+
+ __weak MGLMapView *weakSelf = self;
+
+ [self animateWithDelay:MGLAnimationDuration animations:^
+ {
+ weakSelf.animatingGesture = NO;
+
+ [weakSelf unrotateIfNeededAnimated:YES];
+
+ [weakSelf notifyMapChange:@(mbgl::MapChangeRegionDidChangeAnimated)];
+ }];
+ }
+}
+
+- (void)handleQuickZoomGesture:(UILongPressGestureRecognizer *)quickZoom
+{
+ if ( ! self.isZoomEnabled) return;
+
+ mbglMap->cancelTransitions();
+
+ if (quickZoom.state == UIGestureRecognizerStateBegan)
+ {
+ self.scale = mbglMap->getScale();
+
+ self.quickZoomStart = [quickZoom locationInView:quickZoom.view].y;
+ }
+ else if (quickZoom.state == UIGestureRecognizerStateChanged)
+ {
+ CGFloat distance = self.quickZoomStart - [quickZoom locationInView:quickZoom.view].y;
+
+ CGFloat newZoom = log2f(self.scale) + (distance / 100);
+
+ if (newZoom < mbglMap->getMinZoom()) return;
+
+ mbglMap->scaleBy(powf(2, newZoom) / mbglMap->getScale(), self.bounds.size.width / 2, self.bounds.size.height / 2);
+ }
+ else if (quickZoom.state == UIGestureRecognizerStateEnded || quickZoom.state == UIGestureRecognizerStateCancelled)
+ {
+ [self unrotateIfNeededAnimated:YES];
+
+ [self notifyMapChange:@(mbgl::MapChangeRegionDidChangeAnimated)];
+ }
+}
+
+- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
+{
+ NSArray *validSimultaneousGestures = @[ self.pan, self.pinch, self.rotate ];
+
+ return ([validSimultaneousGestures containsObject:gestureRecognizer] && [validSimultaneousGestures containsObject:otherGestureRecognizer]);
+}
+
+#pragma mark - Settings -
+
+- (void)tintColorDidChange
+{
+ for (UIView *subview in self.subviews)
+ {
+ if ([subview respondsToSelector:@selector(setTintColor:)])
+ {
+ subview.tintColor = self.tintColor;
+ }
+ }
+}
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wunused-parameter"
+
+- (void)showAttribution:(id)sender
+{
+ [[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"https://www.mapbox.com/about/maps/"]];
+}
+
+#pragma clang diagnostic pop
+
+- (void)setDebugActive:(BOOL)debugActive
+{
+ mbglMap->setDebug(debugActive);
+}
+
+- (BOOL)isDebugActive
+{
+ return mbglMap->getDebug();
+}
+
+- (void)resetNorth
+{
+ [self resetNorthAnimated:YES];
+}
+
+- (void)resetNorthAnimated:(BOOL)animated
+{
+ CGFloat duration = (animated ? MGLAnimationDuration : 0);
+
+ mbglMap->setBearing(0, secondsAsDuration(duration));
+
+ [UIView animateWithDuration:duration
+ animations:^
+ {
+ self.compass.transform = CGAffineTransformIdentity;
+ }
+ completion:^(BOOL finished)
+ {
+ if (finished)
+ {
+ [UIView animateWithDuration:MGLAnimationDuration
+ animations:^
+ {
+ self.compass.alpha = 0;
+ }];
+ }
+ }];
+}
+
+- (void)resetPosition
+{
+ mbglMap->resetPosition();
+}
+
+- (void)toggleDebug
+{
+ mbglMap->toggleDebug();
+}
+
+- (void)setCenterCoordinate:(CLLocationCoordinate2D)coordinate animated:(BOOL)animated
+{
+ CGFloat duration = (animated ? MGLAnimationDuration : 0);
+
+ mbglMap->setLonLat(coordinate.longitude, coordinate.latitude, secondsAsDuration(duration));
+}
+
+- (void)setCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate
+{
+ [self setCenterCoordinate:centerCoordinate animated:NO];
+}
+
+- (CLLocationCoordinate2D)centerCoordinate
+{
+ double lon, lat;
+ mbglMap->getLonLat(lon, lat);
+
+ return CLLocationCoordinate2DMake(lat, lon);
+}
+
+- (void)setCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate zoomLevel:(double)zoomLevel animated:(BOOL)animated
+{
+ CGFloat duration = (animated ? MGLAnimationDuration : 0);
+
+ mbglMap->setLonLatZoom(centerCoordinate.longitude, centerCoordinate.latitude, zoomLevel, secondsAsDuration(duration));
+
+ [self unrotateIfNeededAnimated:animated];
+}
+
+- (double)zoomLevel
+{
+ return mbglMap->getZoom();
+}
+
+- (void)setZoomLevel:(double)zoomLevel animated:(BOOL)animated
+{
+ CGFloat duration = (animated ? MGLAnimationDuration : 0);
+
+ mbglMap->setZoom(zoomLevel, secondsAsDuration(duration));
+
+ [self unrotateIfNeededAnimated:animated];
+}
+
+- (void)setZoomLevel:(double)zoomLevel
+{
+ [self setZoomLevel:zoomLevel animated:NO];
+}
+
+- (CLLocationDirection)direction
+{
+ double direction = mbglMap->getBearing() * -1;
+
+ while (direction > 360) direction -= 360;
+ while (direction < 0) direction += 360;
+
+ return direction;
+}
+
+- (void)setDirection:(CLLocationDirection)direction animated:(BOOL)animated
+{
+ if ( ! animated && ! self.rotationAllowed) return;
+
+ CGFloat duration = (animated ? MGLAnimationDuration : 0);
+
+ mbglMap->setBearing(direction * -1, secondsAsDuration(duration));
+}
+
+- (void)setDirection:(CLLocationDirection)direction
+{
+ [self setDirection:direction animated:NO];
+}
+
+#pragma mark - Styling -
+
+- (NSDictionary *)getRawStyle
+{
+ const std::string styleJSON = mbglMap->getStyleJSON();
+
+ return [NSJSONSerialization JSONObjectWithData:[@(styleJSON.c_str()) dataUsingEncoding:[NSString defaultCStringEncoding]] 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]]];
+}
+
+- (NSArray *)bundledStyleNames
+{
+ NSString *stylesPath = [[MGLMapView resourceBundlePath] stringByAppendingString:@"/styles"];
+
+ NSArray *styleNames = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:stylesPath error:nil];
+
+ return styleNames;
+}
+
+- (void)useBundledStyleNamed:(NSString *)styleName
+{
+ [self setStyleURL:[NSString stringWithFormat:@"styles/%@.json", styleName]];
+}
+
+- (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];
+
+ const std::vector<std::string> &appliedClasses = mbglMap->getClasses();
+
+ for (auto class_it = appliedClasses.begin(); class_it != appliedClasses.end(); class_it++)
+ {
+ [returnArray addObject:@(class_it->c_str())];
+ }
+
+ return returnArray;
+}
+
+- (void)setAppliedStyleClasses:(NSArray *)appliedClasses
+{
+ [self setAppliedStyleClasses:appliedClasses transitionDuration:0];
+}
+
+- (void)setAppliedStyleClasses:(NSArray *)appliedClasses transitionDuration:(NSTimeInterval)transitionDuration
+{
+ std::vector<std::string> newAppliedClasses;
+
+ for (NSString *appliedClass in appliedClasses)
+ {
+ newAppliedClasses.insert(newAppliedClasses.end(), [appliedClass cStringUsingEncoding:[NSString defaultCStringEncoding]]);
+ }
+
+ mbglMap->setDefaultTransitionDuration(secondsAsDuration(transitionDuration));
+ mbglMap->setClasses(newAppliedClasses);
+}
+
+- (NSString *)getKeyTypeForLayer:(NSString *)layerName
+{
+ NSDictionary *style = [self getRawStyle];
+
+ NSString *bucketType;
+
+ if ([layerName isEqualToString:@"background"])
+ {
+ bucketType = @"background";
+ }
+ else
+ {
+ for (NSDictionary *layer in style[@"structure"])
+ {
+ if ([layer[@"name"] isEqualToString:layerName])
+ {
+ bucketType = style[@"buckets"][layer[@"bucket"]][@"type"];
+ break;
+ }
+ }
+ }
+
+ 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 keyType;
+}
+
+- (NSDictionary *)getStyleDescriptionForLayer:(NSString *)layerName inClass:(NSString *)className
+{
+ NSDictionary *style = [self getRawStyle];
+
+ if ( ! [[style valueForKeyPath:@"classes.name"] containsObject:className])
+ {
+ [NSException raise:@"invalid class name"
+ format:@"class name %@ unknown",
+ className];
+ }
+
+ NSUInteger classNumber = [[style valueForKeyPath:@"classes.name"] indexOfObject:className];
+
+ if ( ! [[style[@"classes"][classNumber][@"layers"] allKeys] containsObject:layerName])
+ {
+ // layer specified in structure, but not styled
+ //
+ return nil;
+ }
+
+ NSDictionary *layerStyle = style[@"classes"][classNumber][@"layers"][layerName];
+
+ NSMutableDictionary *styleDescription = [NSMutableDictionary dictionary];
+
+ for (NSString *keyName in [layerStyle allKeys])
+ {
+ id value = layerStyle[keyName];
+
+ while ([[style[@"constants"] allKeys] containsObject:value])
+ {
+ value = style[@"constants"][value];
+ }
+
+ if ([[self.allowedStyleTypes[MGLStyleKeyGeneric] allKeys] containsObject:keyName])
+ {
+ [styleDescription setValue:[self typedPropertyForKeyName:keyName
+ ofType:MGLStyleKeyGeneric
+ withValue:value]
+ forKey:keyName];
+ }
+
+ NSString *keyType = [self getKeyTypeForLayer:layerName];
+
+ if ([[self.allowedStyleTypes[keyType] allKeys] containsObject:keyName])
+ {
+ [styleDescription setValue:[self typedPropertyForKeyName:keyName
+ ofType:keyType
+ withValue:value]
+ forKey:keyName];
+ }
+ }
+
+ return styleDescription;
+}
+
+- (NSDictionary *)typedPropertyForKeyName:(NSString *)keyName ofType:(NSString *)keyType withValue:(id)value
+{
+ if ( ! [[self.allowedStyleTypes[keyType] allKeys] containsObject:keyName])
+ {
+ [NSException raise:@"invalid property name"
+ format:@"property name %@ unknown",
+ keyName];
+ }
+
+ NSArray *typeInfo = self.allowedStyleTypes[keyType][keyName];
+
+ if ([value isKindOfClass:[NSArray class]] && ! [typeInfo containsObject:MGLStyleValueTypeColor])
+ {
+ if ([typeInfo containsObject:MGLStyleValueFunctionAllowed])
+ {
+ if ([[(NSArray *)value firstObject] isKindOfClass:[NSString class]])
+ {
+ NSString *functionType;
+
+ 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;
+ }
+
+ if (functionType)
+ {
+ return @{ @"type" : functionType,
+ @"value" : value };
+ }
+ }
+ }
+ else if ([typeInfo containsObject:MGLStyleValueTypeNumberPair])
+ {
+ return @{ @"type" : MGLStyleValueTypeNumberPair,
+ @"value" : value };
+ }
+ }
+ 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"
+
+ color = [[UIColor class] performSelector:NSSelectorFromString([NSString stringWithFormat:@"%@Color", [(NSString *)value lowercaseString]])];
+
+#pragma clang diagnostic pop
+ }
+
+ return @{ @"type" : MGLStyleValueTypeColor,
+ @"value" : color };
+ }
+
+ return nil;
+}
+
+- (void)setStyleDescription:(NSDictionary *)styleDescription forLayer:(NSString *)layerName inClass:(NSString *)className
+{
+#pragma unused(className)
+
+ NSMutableDictionary *convertedStyle = [NSMutableDictionary dictionary];
+
+ for (NSString *key in [styleDescription allKeys])
+ {
+ NSArray *styleParameters = nil;
+
+ if ([[self.allowedStyleTypes[MGLStyleKeyGeneric] allKeys] containsObject:key])
+ {
+ styleParameters = self.allowedStyleTypes[MGLStyleKeyGeneric][key];
+ }
+ else
+ {
+ NSString *keyType = [self getKeyTypeForLayer:layerName];
+
+ if ([[self.allowedStyleTypes[keyType] allKeys] containsObject:key])
+ {
+ styleParameters = self.allowedStyleTypes[keyType][key];
+ }
+ }
+
+ if (styleParameters)
+ {
+ if ([styleDescription[key][@"value"] isKindOfClass:[MGLStyleFunctionValue class]])
+ {
+ convertedStyle[key] = [(MGLStyleFunctionValue *)styleDescription[key][@"value"] rawStyle];
+ }
+ else if ([styleParameters containsObject:styleDescription[key][@"type"]])
+ {
+ NSString *valueType = styleDescription[key][@"type"];
+
+ 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"];
+ }
+ }
+ }
+ else
+ {
+ [NSException raise:@"invalid style description format"
+ format:@"unable to parse key '%@'",
+ key];
+ }
+ }
+
+// 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;
+}
+
+#pragma mark - Utility -
+
+- (void)animateWithDelay:(NSTimeInterval)delay animations:(void (^)(void))animations
+{
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), dispatch_get_main_queue(), animations);
+}
+
+- (BOOL)isRotationAllowed
+{
+ return (self.zoomLevel > 3);
+}
+
+// correct rotations to north as needed
+//
+- (void)unrotateIfNeededAnimated:(BOOL)animated
+{
+ // don't worry about it in the midst of pinch or rotate gestures
+ //
+ if (self.pinch.state == UIGestureRecognizerStateChanged || self.rotate.state == UIGestureRecognizerStateChanged) return;
+
+ // but otherwise, do
+ //
+ if (self.direction != 0 && ! self.isRotationAllowed)
+ {
+ if (animated)
+ {
+ self.animatingGesture = YES;
+
+ self.userInteractionEnabled = NO;
+
+ __weak MGLMapView *weakSelf = self;
+
+ [self animateWithDelay:0.1 animations:^
+ {
+ [weakSelf resetNorthAnimated:YES];
+
+ [self animateWithDelay:MGLAnimationDuration animations:^
+ {
+ weakSelf.userInteractionEnabled = YES;
+
+ self.animatingGesture = NO;
+ }];
+
+ }];
+ }
+ else
+ {
+ [self resetNorthAnimated:NO];
+ }
+ }
+}
+
+- (void)unsuspendRegionChangeDelegateQueue
+{
+ @synchronized (self.regionChangeDelegateQueue)
+ {
+ [self.regionChangeDelegateQueue setSuspended:NO];
+ }
+}
+
+- (void)notifyMapChange:(NSNumber *)change
+{
+ switch ([change unsignedIntegerValue])
+ {
+ case mbgl::MapChangeRegionWillChange:
+ case mbgl::MapChangeRegionWillChangeAnimated:
+ {
+ BOOL animated = ([change unsignedIntegerValue] == mbgl::MapChangeRegionWillChangeAnimated);
+
+ @synchronized (self.regionChangeDelegateQueue)
+ {
+ if ([self.regionChangeDelegateQueue operationCount] == 0)
+ {
+ if ([self.delegate respondsToSelector:@selector(mapView:regionWillChangeAnimated:)])
+ {
+ [self.delegate mapView:self regionWillChangeAnimated:animated];
+ }
+ }
+
+ [self.regionChangeDelegateQueue setSuspended:YES];
+
+ if ([self.regionChangeDelegateQueue operationCount] == 0)
+ {
+ [self.regionChangeDelegateQueue addOperationWithBlock:^
+ {
+ dispatch_async(dispatch_get_main_queue(), ^
+ {
+ if ([self.delegate respondsToSelector:@selector(mapView:regionDidChangeAnimated:)])
+ {
+ [self.delegate mapView:self regionDidChangeAnimated:animated];
+ }
+ });
+ }];
+ }
+ }
+ break;
+ }
+ case mbgl::MapChangeRegionDidChange:
+ case mbgl::MapChangeRegionDidChangeAnimated:
+ {
+ [self updateCompass];
+
+ if (self.pan.state == UIGestureRecognizerStateChanged ||
+ self.pinch.state == UIGestureRecognizerStateChanged ||
+ self.rotate.state == UIGestureRecognizerStateChanged ||
+ self.quickZoom.state == UIGestureRecognizerStateChanged) return;
+
+ if (self.isAnimatingGesture) return;
+
+ [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(unsuspendRegionChangeDelegateQueue) object:nil];
+ [self performSelector:@selector(unsuspendRegionChangeDelegateQueue) withObject:nil afterDelay:0];
+
+ break;
+ }
+ case mbgl::MapChangeWillStartLoadingMap:
+ {
+ if ([self.delegate respondsToSelector:@selector(mapViewWillStartLoadingMap:)])
+ {
+ [self.delegate mapViewWillStartLoadingMap:self];
+ }
+ break;
+ }
+ case mbgl::MapChangeDidFinishLoadingMap:
+ {
+ if ([self.delegate respondsToSelector:@selector(mapViewDidFinishLoadingMap:)])
+ {
+ [self.delegate mapViewDidFinishLoadingMap:self];
+ }
+ break;
+ }
+ case mbgl::MapChangeDidFailLoadingMap:
+ {
+ if ([self.delegate respondsToSelector:@selector(mapViewDidFailLoadingMap:withError::)])
+ {
+ [self.delegate mapViewDidFailLoadingMap:self withError:nil];
+ }
+ break;
+ }
+ case mbgl::MapChangeWillStartRenderingMap:
+ {
+ if ([self.delegate respondsToSelector:@selector(mapViewWillStartRenderingMap:)])
+ {
+ [self.delegate mapViewWillStartRenderingMap:self];
+ }
+ break;
+ }
+ case mbgl::MapChangeDidFinishRenderingMap:
+ {
+ if ([self.delegate respondsToSelector:@selector(mapViewDidFinishRenderingMap:fullyRendered:)])
+ {
+ [self.delegate mapViewDidFinishRenderingMap:self fullyRendered:NO];
+ }
+ break;
+ }
+ case mbgl::MapChangeDidFinishRenderingMapFullyRendered:
+ {
+ if ([self.delegate respondsToSelector:@selector(mapViewDidFinishRenderingMap:fullyRendered:)])
+ {
+ [self.delegate mapViewDidFinishRenderingMap:self fullyRendered:YES];
+ }
+ break;
+ }
+ }
+}
+
+- (void)updateCompass
+{
+ double degrees = mbglMap->getBearing() * -1;
+ while (degrees >= 360) degrees -= 360;
+ while (degrees < 0) degrees += 360;
+
+ self.compass.transform = CGAffineTransformMakeRotation([MGLMapView degreesToRadians:degrees]);
+
+ if (mbglMap->getBearing() && self.compass.alpha < 1)
+ {
+ [UIView animateWithDuration:MGLAnimationDuration
+ delay:0
+ options:UIViewAnimationOptionBeginFromCurrentState
+ animations:^
+ {
+ self.compass.alpha = 1;
+ }
+ completion:nil];
+ }
+}
+
++ (UIImage *)resourceImageNamed:(NSString *)imageName
+{
+ if ( ! [[imageName pathExtension] length])
+ {
+ imageName = [imageName stringByAppendingString:@".png"];
+ }
+
+ return [UIImage imageWithContentsOfFile:[MGLMapView pathForBundleResourceNamed:imageName ofType:nil inDirectory:@""]];
+}
+
++ (NSString *)pathForBundleResourceNamed:(NSString *)name ofType:(NSString *)extension inDirectory:(NSString *)directory
+{
+ NSString *path = [[NSBundle bundleWithPath:[MGLMapView resourceBundlePath]] pathForResource:name ofType:extension inDirectory:directory];
+
+ NSAssert(path, @"Resource not found in application.");
+
+ return path;
+}
+
++ (NSString *)resourceBundlePath
+{
+ NSString *resourceBundlePath = [[NSBundle bundleForClass:[MGLMapView class]] pathForResource:@"MapboxGL" ofType:@"bundle"];
+
+ if ( ! resourceBundlePath) resourceBundlePath = [[NSBundle mainBundle] bundlePath];
+
+ return resourceBundlePath;
+}
+
+- (void)swap
+{
+ if (mbglMap->needsSwap())
+ {
+ [self.glView display];
+ mbglMap->swapped();
+ }
+}
+
+class MBGLView : public mbgl::View
+{
+ public:
+ MBGLView(MGLMapView *nativeView_) : nativeView(nativeView_) {}
+ virtual ~MBGLView() {}
+
+
+ void notify()
+ {
+ // no-op
+ }
+
+ void notifyMapChange(mbgl::MapChange change, std::chrono::steady_clock::duration delay = std::chrono::steady_clock::duration::zero())
+ {
+ if (delay != std::chrono::steady_clock::duration::zero())
+ {
+ __weak MGLMapView *weakNativeView = nativeView;
+
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, std::chrono::duration_cast<std::chrono::nanoseconds>(delay).count()), dispatch_get_main_queue(), ^
+ {
+ [weakNativeView performSelector:@selector(notifyMapChange:)
+ withObject:@(change)
+ afterDelay:0];
+ });
+ }
+ else
+ {
+ dispatch_async(dispatch_get_main_queue(), ^
+ {
+ [nativeView performSelector:@selector(notifyMapChange:)
+ withObject:@(change)
+ afterDelay:0];
+ });
+ }
+ }
+
+ void activate()
+ {
+ [EAGLContext setCurrentContext:nativeView.context];
+ }
+
+ void deactivate()
+ {
+ [EAGLContext setCurrentContext:nil];
+ }
+
+ void swap()
+ {
+ [nativeView performSelectorOnMainThread:@selector(swap)
+ withObject:nil
+ waitUntilDone:NO];
+ }
+
+ private:
+ MGLMapView *nativeView = nullptr;
+};
+
+@end