Files
MapLibreParticleLab/platform/ios/MGLMapView.mm
2015-02-23 13:50:04 -08:00

1646 lines
55 KiB
Plaintext

#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>
#include <mbgl/util/geo.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 & Teardown -
@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;
if ([UIScreen instancesRespondToSelector:@selector(nativeScale)]) {
_glView.contentScaleFactor = [[UIScreen mainScreen] nativeScale];
}
_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;
}
if (extensions.find("GL_OES_packed_depth_stencil") != std::string::npos) {
gl::isPackedDepthStencilSupported = YES;
}
if (extensions.find("GL_OES_depth24") != std::string::npos) {
gl::isDepth24Supported = YES;
}
}
// 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->setLatLngZoom(mbgl::LatLng(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 - 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();
}
- (void)tintColorDidChange
{
for (UIView *subview in self.subviews)
{
if ([subview respondsToSelector:@selector(setTintColor:)])
{
subview.tintColor = self.tintColor;
}
}
}
#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 && std::abs(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 - Properties -
#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();
}
#pragma mark - Geography -
- (void)setCenterCoordinate:(CLLocationCoordinate2D)coordinate animated:(BOOL)animated
{
CGFloat duration = (animated ? MGLAnimationDuration : 0);
mbglMap->setLatLng(mbgl::LatLng(coordinate.latitude, coordinate.longitude), secondsAsDuration(duration));
}
- (void)setCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate
{
[self setCenterCoordinate:centerCoordinate animated:NO];
}
- (CLLocationCoordinate2D)centerCoordinate
{
mbgl::LatLng latLng = mbglMap->getLatLng();
return CLLocationCoordinate2DMake(latLng.latitude, latLng.longitude);
}
- (void)setCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate zoomLevel:(double)zoomLevel animated:(BOOL)animated
{
CGFloat duration = (animated ? MGLAnimationDuration : 0);
mbglMap->setLatLngZoom(mbgl::LatLng(centerCoordinate.latitude, centerCoordinate.longitude), 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];
}
- (CLLocationCoordinate2D)convertPoint:(CGPoint)point toCoordinateFromView:(UIView *)view
{
CGPoint convertedPoint = [self convertPoint:point fromView:view];
// flip y coordinate for iOS view origin top left
//
convertedPoint.y = self.bounds.size.height - convertedPoint.y;
mbgl::LatLng latLng = mbglMap->latLngForPixel(mbgl::vec2<double>(convertedPoint.x, convertedPoint.y));
return CLLocationCoordinate2DMake(latLng.latitude, latLng.longitude);
}
- (CGPoint)convertCoordinate:(CLLocationCoordinate2D)coordinate toPointToView:(UIView *)view
{
mbgl::vec2<double> pixel = mbglMap->pixelForLatLng(mbgl::LatLng(coordinate.latitude, coordinate.longitude));
// flip y coordinate for iOS view origin in top left
//
pixel.y = self.bounds.size.height - pixel.y;
return [self convertPoint:CGPointMake(pixel.x, pixel.y) toView:view];
}
- (CLLocationDistance)metersPerPixelAtLatitude:(CLLocationDegrees)latitude
{
return mbglMap->getMetersPerPixelAtLatitude(latitude, self.zoomLevel);
}
#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 -
+ (CGFloat)degreesToRadians:(CGFloat)degrees
{
return degrees * M_PI / 180;
}
+ (CGFloat)radiansToDegrees:(CGFloat)radians
{
return radians * 180 / M_PI;
}
- (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())
{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, std::chrono::duration_cast<std::chrono::nanoseconds>(delay).count()), dispatch_get_main_queue(), ^
{
[nativeView 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:
__weak MGLMapView *nativeView = nullptr;
};
@end