summaryrefslogtreecommitdiff
path: root/platform
diff options
context:
space:
mode:
authorJustin R. Miller <incanus@codesorcery.net>2015-02-09 18:01:40 -0800
committerJustin R. Miller <incanus@codesorcery.net>2015-02-09 18:01:40 -0800
commitd4d4cd44151423d374e373798f09d12157babc27 (patch)
treeede100edc58475b969b62fba8f3bcb219b29beda /platform
parent0ee6044ccf49bb32a25c57ce25af15bcd58ca77b (diff)
downloadqtlocation-mapboxgl-d4d4cd44151423d374e373798f09d12157babc27.tar.gz
move iOS code from gl-cocoa to this project
Diffstat (limited to 'platform')
-rw-r--r--platform/darwin/log_nslog.mm2
-rw-r--r--platform/darwin/string_nsstring.mm2
-rw-r--r--platform/ios/MGLMapView.mm1607
-rw-r--r--platform/ios/MGLStyleFunctionValue.m190
-rw-r--r--platform/ios/MGLTypes.m24
-rw-r--r--platform/ios/NSArray+MGLAdditions.m10
-rw-r--r--platform/ios/NSDictionary+MGLAdditions.m10
-rw-r--r--platform/ios/UIColor+MGLAdditions.m167
-rw-r--r--platform/ios/resources/Compass.pngbin0 -> 1736 bytes
-rw-r--r--platform/ios/resources/Compass@2x.pngbin0 -> 2376 bytes
-rw-r--r--platform/ios/resources/mapbox.pngbin0 -> 5947 bytes
-rw-r--r--platform/ios/resources/mapbox@2x.pngbin0 -> 10713 bytes
12 files changed, 2008 insertions, 4 deletions
diff --git a/platform/darwin/log_nslog.mm b/platform/darwin/log_nslog.mm
index ea5fddf0b9..a82d78a01f 100644
--- a/platform/darwin/log_nslog.mm
+++ b/platform/darwin/log_nslog.mm
@@ -10,7 +10,6 @@ void NSLogBackend::record(EventSeverity severity, Event event, const std::string
NSString *message =
[[NSString alloc] initWithBytes:msg.data() length:msg.size() encoding:NSUTF8StringEncoding];
NSLog(@"[%s] %s: %@", EventSeverityClass(severity).c_str(), EventClass(event).c_str(), message);
- [message release];
}
void NSLogBackend::record(EventSeverity severity, Event event, const char *format, ...) {
@@ -37,7 +36,6 @@ void NSLogBackend::record(EventSeverity severity, Event event, int64_t code,
[[NSString alloc] initWithBytes:msg.data() length:msg.size() encoding:NSUTF8StringEncoding];
NSLog(@"[%s] %s: (%lld) %@", EventSeverityClass(severity).c_str(), EventClass(event).c_str(),
code, message);
- [message release];
}
}
diff --git a/platform/darwin/string_nsstring.mm b/platform/darwin/string_nsstring.mm
index 86c2c07edd..9bf199afc0 100644
--- a/platform/darwin/string_nsstring.mm
+++ b/platform/darwin/string_nsstring.mm
@@ -13,7 +13,6 @@ std::string uppercase(const std::string &string) {
NSString *uppercase = [original uppercaseString];
const std::string result{[uppercase cStringUsingEncoding : NSUTF8StringEncoding],
[uppercase lengthOfBytesUsingEncoding:NSUTF8StringEncoding]};
- [original release];
return result;
}
@@ -25,7 +24,6 @@ std::string lowercase(const std::string &string) {
NSString *lowercase = [original lowercaseString];
const std::string result{[lowercase cStringUsingEncoding : NSUTF8StringEncoding],
[lowercase lengthOfBytesUsingEncoding:NSUTF8StringEncoding]};
- [original release];
return result;
}
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
diff --git a/platform/ios/MGLStyleFunctionValue.m b/platform/ios/MGLStyleFunctionValue.m
new file mode 100644
index 0000000000..a319114eef
--- /dev/null
+++ b/platform/ios/MGLStyleFunctionValue.m
@@ -0,0 +1,190 @@
+#import "MGLStyleFunctionValue.h"
+
+#import "MGLTypes.h"
+
+@interface MGLStyleFunctionValue ()
+
+@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;
+
+- (NSDictionary *)rawStyle;
+
+@end
+
+@implementation MGLStyleFunctionValue
+
+- (id)initWithFunctionType:(NSString *)functionType
+ stops:(NSDictionary *)stops
+ zBase:(CGFloat)zBase
+ val:(CGFloat)val
+ slope:(CGFloat)slope
+ min:(CGFloat)min
+ max:(CGFloat)max
+ minimumZoom:(CGFloat)minimumZoom
+ maximumZoom:(CGFloat)maximumZoom
+{
+ self = [super init];
+
+ if (self)
+ {
+ _functionType = functionType;
+ _stops = stops;
+ _zBase = zBase;
+ _val = val;
+ _slope = slope;
+ _min = min;
+ _max = max;
+ _minimumZoom = minimumZoom;
+ _maximumZoom = maximumZoom;
+ }
+
+ return self;
+}
+
+- (id)rawStyle
+{
+ if ([self.functionType isEqualToString:MGLStyleValueTypeFunctionMinimumZoom])
+ {
+ return @[ @"min", @(self.minimumZoom) ];
+ }
+ else if ([self.functionType isEqualToString:MGLStyleValueTypeFunctionMaximumZoom])
+ {
+ return @[ @"max", @(self.maximumZoom) ];
+ }
+ else if ([self.functionType isEqualToString:MGLStyleValueTypeFunctionLinear])
+ {
+ return @[ @"linear", @(self.zBase), @(self.val), @(self.slope), @(self.min), @(self.max) ];
+ }
+ else if ([self.functionType isEqualToString:MGLStyleValueTypeFunctionExponential])
+ {
+ return @[ @"exponential", @(self.zBase), @(self.val), @(self.slope), @(self.min), @(self.max) ];
+ }
+ else if ([self.functionType isEqualToString:MGLStyleValueTypeFunctionStops])
+ {
+ NSMutableArray *returnArray = [NSMutableArray array];
+
+ for (NSNumber *z in [self.stops allKeys])
+ {
+ [returnArray addObject:@{ @"z" : z, @"val" : self.stops[z] }];
+ }
+
+ [returnArray insertObject:@"stops" atIndex:0];
+
+ return returnArray;
+ }
+
+ return nil;
+}
+
++ (instancetype)stopsFunctionWithZoomLevelsAndValues:(NSNumber *)firstZoom, ...
+{
+ NSMutableArray *numbersArray = [NSMutableArray array];
+
+ va_list args;
+ va_start(args, firstZoom);
+
+ for (NSNumber *arg = firstZoom; arg != nil; arg = va_arg(args, NSNumber *))
+ {
+ [numbersArray addObject:arg];
+ }
+
+ va_end(args);
+
+ NSAssert([numbersArray count] % 2 == 0, @"invalid number of arguments");
+
+ NSMutableDictionary *stops = [NSMutableDictionary dictionary];
+
+ for (NSUInteger i = 0; i < [numbersArray count]; i = i + 2)
+ {
+ stops[numbersArray[i]] = stops[numbersArray[i + 1]];
+ }
+
+ return [[self alloc] initWithFunctionType:MGLStyleValueTypeFunctionStops
+ stops:stops
+ zBase:0
+ val:0
+ slope:0
+ min:0
+ max:0
+ minimumZoom:0
+ maximumZoom:0];
+}
+
++ (instancetype)linearFunctionWithBaseZoomLevel:(CGFloat)zBase
+ initialValue:(CGFloat)val
+ slope:(CGFloat)slope
+ minimumValue:(CGFloat)min
+ maximumValue:(CGFloat)max
+{
+ NSAssert(zBase >= 0 && zBase <= 18, @"invalid base zoom level");
+ NSAssert(min < max, @"minimum value must be less than maximum value");
+
+ return [[self alloc] initWithFunctionType:MGLStyleValueTypeFunctionLinear
+ stops:nil
+ zBase:zBase
+ val:val
+ slope:slope
+ min:min
+ max:max
+ minimumZoom:0
+ maximumZoom:0];
+}
+
++ (instancetype)exponentialFunctionWithBaseZoomLevel:(CGFloat)zBase
+ initialValue:(CGFloat)val
+ slope:(CGFloat)slope
+ minimumValue:(CGFloat)min
+ maximumValue:(CGFloat)max
+{
+ NSAssert(zBase >= 0 && zBase <= 18, @"invalid base zoom level");
+ NSAssert(min < max, @"minimum value must be less than maximum value");
+
+ return [[self alloc] initWithFunctionType:MGLStyleValueTypeFunctionExponential
+ stops:nil
+ zBase:zBase
+ val:val
+ slope:slope
+ min:min
+ max:max
+ minimumZoom:0
+ maximumZoom:0];
+}
+
++ (instancetype)minimumZoomLevelFunction:(CGFloat)minimumZoom
+{
+ NSAssert(minimumZoom >= 0 && minimumZoom <= 18, @"invalid minimum zoom value");
+
+ return [[self alloc] initWithFunctionType:MGLStyleValueTypeFunctionMinimumZoom
+ stops:nil
+ zBase:0
+ val:0
+ slope:0
+ min:0
+ max:0
+ minimumZoom:minimumZoom
+ maximumZoom:0];
+}
+
++ (instancetype)maximumZoomLevelFunction:(CGFloat)maximumZoom
+{
+ NSAssert(maximumZoom >= 0 && maximumZoom <= 18, @"invalid maximum zoom value");
+
+ return [[self alloc] initWithFunctionType:MGLStyleValueTypeFunctionMaximumZoom
+ stops:nil
+ zBase:0
+ val:0
+ slope:0
+ min:0
+ max:0
+ minimumZoom:0
+ maximumZoom:maximumZoom];
+}
+
+@end
diff --git a/platform/ios/MGLTypes.m b/platform/ios/MGLTypes.m
new file mode 100644
index 0000000000..2928bc2f4c
--- /dev/null
+++ b/platform/ios/MGLTypes.m
@@ -0,0 +1,24 @@
+#import "MGLTypes.h"
+
+NSString *const MGLStyleKeyGeneric = @"MGLStyleKeyGeneric";
+NSString *const MGLStyleKeyFill = @"MGLStyleKeyFill";
+NSString *const MGLStyleKeyLine = @"MGLStyleKeyLine";
+NSString *const MGLStyleKeyIcon = @"MGLStyleKeyIcon";
+NSString *const MGLStyleKeyText = @"MGLStyleKeyText";
+NSString *const MGLStyleKeyRaster = @"MGLStyleKeyRaster";
+NSString *const MGLStyleKeyComposite = @"MGLStyleKeyComposite";
+NSString *const MGLStyleKeyBackground = @"MGLStyleKeyBackground";
+
+NSString *const MGLStyleValueTypeBoolean = @"MGLStyleValueTypeBoolean";
+NSString *const MGLStyleValueTypeNumber = @"MGLStyleValueTypeNumber";
+NSString *const MGLStyleValueTypeNumberPair = @"MGLStyleValueTypeNumberPair";
+NSString *const MGLStyleValueTypeColor = @"MGLStyleValueTypeColor";
+NSString *const MGLStyleValueTypeString = @"MGLStyleValueTypeString";
+
+NSString *const MGLStyleValueFunctionAllowed = @"MGLStyleValueFunctionAllowed";
+
+NSString *const MGLStyleValueTypeFunctionMinimumZoom = @"MGLStyleValueTypeFunctionMinimumZoom";
+NSString *const MGLStyleValueTypeFunctionMaximumZoom = @"MGLStyleValueTypeFunctionMaximumZoom";
+NSString *const MGLStyleValueTypeFunctionLinear = @"MGLStyleValueTypeFunctionLinear";
+NSString *const MGLStyleValueTypeFunctionExponential = @"MGLStyleValueTypeFunctionExponential";
+NSString *const MGLStyleValueTypeFunctionStops = @"MGLStyleValueTypeFunctionStops";
diff --git a/platform/ios/NSArray+MGLAdditions.m b/platform/ios/NSArray+MGLAdditions.m
new file mode 100644
index 0000000000..2bac42ce0b
--- /dev/null
+++ b/platform/ios/NSArray+MGLAdditions.m
@@ -0,0 +1,10 @@
+#import "NSArray+MGLAdditions.h"
+
+@implementation NSArray (MGLAdditions)
+
+- (NSMutableArray *)deepMutableCopy
+{
+ return (NSMutableArray *)CFBridgingRelease(CFPropertyListCreateDeepCopy(kCFAllocatorDefault, (CFArrayRef)self, kCFPropertyListMutableContainersAndLeaves));
+}
+
+@end
diff --git a/platform/ios/NSDictionary+MGLAdditions.m b/platform/ios/NSDictionary+MGLAdditions.m
new file mode 100644
index 0000000000..3228d56533
--- /dev/null
+++ b/platform/ios/NSDictionary+MGLAdditions.m
@@ -0,0 +1,10 @@
+#import "NSDictionary+MGLAdditions.h"
+
+@implementation NSDictionary (MGLAdditions)
+
+- (NSMutableDictionary *)deepMutableCopy
+{
+ return (NSMutableDictionary *)CFBridgingRelease(CFPropertyListCreateDeepCopy(kCFAllocatorDefault, (CFDictionaryRef)self, kCFPropertyListMutableContainersAndLeaves));
+}
+
+@end
diff --git a/platform/ios/UIColor+MGLAdditions.m b/platform/ios/UIColor+MGLAdditions.m
new file mode 100644
index 0000000000..ae40735d15
--- /dev/null
+++ b/platform/ios/UIColor+MGLAdditions.m
@@ -0,0 +1,167 @@
+#import "UIColor+MGLAdditions.h"
+
+/* Portions based on Erica Sadun's uicolor-utilities
+ https://github.com/erica/uicolor-utilities */
+
+@interface UIColor (MGLAdditionsPrivate)
+
++ (UIColor *)colorWithRGBHex:(UInt32)hex;
+- (CGColorSpaceModel)colorSpaceModel;
+- (BOOL)canProvideRGBComponents;
+- (BOOL)red:(CGFloat *)red green:(CGFloat *)green blue:(CGFloat *)blue alpha:(CGFloat *)alpha;
+- (UInt32)rgbHex;
+
+@end
+
+@implementation UIColor (MGLAdditions)
+
++ (UIColor *)colorWithRGBAString:(NSString *)rgbaString
+{
+ UIColor *color;
+
+ NSString *numberString = [rgbaString stringByReplacingOccurrencesOfString:@"rgba("
+ withString:@""];
+ numberString = [numberString stringByReplacingOccurrencesOfString:@"rgb("
+ withString:@""];
+ numberString = [numberString stringByReplacingOccurrencesOfString:@")"
+ withString:@""];
+
+ NSArray *numbers = [numberString componentsSeparatedByString:@","];
+
+ if ([rgbaString hasPrefix:@"rgb("] && [numbers count] == 3)
+ {
+ color = [UIColor colorWithRed:[numbers[0] floatValue] / 255
+ green:[numbers[1] floatValue] / 255
+ blue:[numbers[2] floatValue] / 255
+ alpha:1.0];
+ }
+ else if ([rgbaString hasPrefix:@"rgba("] && [numbers count] == 4)
+ {
+ color = [UIColor colorWithRed:[numbers[0] floatValue] / 255
+ green:[numbers[1] floatValue] / 255
+ blue:[numbers[2] floatValue] / 255
+ alpha:[numbers[3] floatValue]];
+ }
+
+ return color;
+}
+
+- (NSString *)rgbaStringFromColor
+{
+ CGFloat r,g,b,a;
+
+ [self getRed:&r green:&g blue:&b alpha:&a];
+
+ r *= 255;
+ g *= 255;
+ b *= 255;
+ a *= 255;
+
+ return [NSString stringWithFormat:@"rgba(%lu,%lu,%lu,%lu)", (unsigned long)r, (unsigned long)g, (unsigned long)b, (unsigned long)a];
+}
+
++ (UIColor *)colorWithRGBHex:(UInt32)hex
+{
+ int r = (hex >> 16) & 0xFF;
+ int g = (hex >> 8) & 0xFF;
+ int b = (hex) & 0xFF;
+
+ return [UIColor colorWithRed:r / 255.0f
+ green:g / 255.0f
+ blue:b / 255.0f
+ alpha:1.0f];
+}
+
+- (CGColorSpaceModel)colorSpaceModel
+{
+ return CGColorSpaceGetModel(CGColorGetColorSpace(self.CGColor));
+}
+
+- (BOOL)canProvideRGBComponents
+{
+ switch (self.colorSpaceModel)
+ {
+ case kCGColorSpaceModelRGB:
+ case kCGColorSpaceModelMonochrome:
+ {
+ return YES;
+ }
+ default:
+ {
+ return NO;
+ }
+ }
+}
+
+- (BOOL)red:(CGFloat *)red green:(CGFloat *)green blue:(CGFloat *)blue alpha:(CGFloat *)alpha
+{
+ const CGFloat *components = CGColorGetComponents(self.CGColor);
+
+ CGFloat r,g,b,a;
+
+ switch (self.colorSpaceModel)
+ {
+ case kCGColorSpaceModelMonochrome:
+ {
+ r = g = b = components[0];
+ a = components[1];
+
+ break;
+ }
+ case kCGColorSpaceModelRGB:
+ {
+ r = components[0];
+ g = components[1];
+ b = components[2];
+ a = components[3];
+
+ break;
+ }
+ default:
+ {
+ return NO;
+ }
+ }
+
+ if (red) *red = r;
+ if (green) *green = g;
+ if (blue) *blue = b;
+ if (alpha) *alpha = a;
+
+ return YES;
+}
+
+- (UInt32)rgbHex
+{
+ NSAssert(self.canProvideRGBComponents, @"Must be a RGB color to use rgbHex");
+
+ CGFloat r, g, b, a;
+
+ if ( ! [self red:&r green:&g blue:&b alpha:&a])
+ return 0;
+
+ r = fminf(fmaxf(r, 0.0f), 1.0f);
+ g = fminf(fmaxf(g, 0.0f), 1.0f);
+ b = fminf(fmaxf(b, 0.0f), 1.0f);
+
+ return (((int)roundf(r * 255)) << 16) | (((int)roundf(g * 255)) << 8) | (((int)roundf(b * 255)));
+}
+
+- (NSString *)hexStringFromColor
+{
+ return [NSString stringWithFormat:@"%0.6X", (unsigned int)(self.rgbHex)];
+}
+
++ (UIColor *)colorWithHexString:(NSString *)hexString
+{
+ NSScanner *scanner = [NSScanner scannerWithString:[hexString stringByReplacingOccurrencesOfString:@"#" withString:@""]];
+
+ unsigned hexNum;
+
+ if ( ! [scanner scanHexInt:&hexNum])
+ return nil;
+
+ return [UIColor colorWithRGBHex:hexNum];
+}
+
+@end
diff --git a/platform/ios/resources/Compass.png b/platform/ios/resources/Compass.png
new file mode 100644
index 0000000000..fd3afe6f68
--- /dev/null
+++ b/platform/ios/resources/Compass.png
Binary files differ
diff --git a/platform/ios/resources/Compass@2x.png b/platform/ios/resources/Compass@2x.png
new file mode 100644
index 0000000000..58e7e08d24
--- /dev/null
+++ b/platform/ios/resources/Compass@2x.png
Binary files differ
diff --git a/platform/ios/resources/mapbox.png b/platform/ios/resources/mapbox.png
new file mode 100644
index 0000000000..01b5596b67
--- /dev/null
+++ b/platform/ios/resources/mapbox.png
Binary files differ
diff --git a/platform/ios/resources/mapbox@2x.png b/platform/ios/resources/mapbox@2x.png
new file mode 100644
index 0000000000..ff4ef2558b
--- /dev/null
+++ b/platform/ios/resources/mapbox@2x.png
Binary files differ