summaryrefslogtreecommitdiff
path: root/platform/ios/app/MBXViewController.mm
diff options
context:
space:
mode:
Diffstat (limited to 'platform/ios/app/MBXViewController.mm')
-rw-r--r--platform/ios/app/MBXViewController.mm669
1 files changed, 669 insertions, 0 deletions
diff --git a/platform/ios/app/MBXViewController.mm b/platform/ios/app/MBXViewController.mm
new file mode 100644
index 0000000000..af4e425841
--- /dev/null
+++ b/platform/ios/app/MBXViewController.mm
@@ -0,0 +1,669 @@
+#import "MBXViewController.h"
+#import "MBXCustomCalloutView.h"
+
+#import <Mapbox/Mapbox.h>
+#import "../../../include/mbgl/util/default_styles.hpp"
+
+#import <CoreLocation/CoreLocation.h>
+#import <OpenGLES/ES2/gl.h>
+
+static UIColor *const kTintColor = [UIColor colorWithRed:0.120 green:0.550 blue:0.670 alpha:1.000];
+static NSString * const kCustomCalloutTitle = @"Custom Callout";
+
+static const CLLocationCoordinate2D WorldTourDestinations[] = {
+ { 38.9131982, -77.0325453144239 },
+ { 37.7757368, -122.4135302 },
+ { 12.9810816, 77.6368034 },
+ { -13.15589555, -74.2178961777998 },
+};
+
+@interface MBXViewController () <UIActionSheetDelegate, MGLMapViewDelegate>
+
+@property (nonatomic) MGLMapView *mapView;
+@property (nonatomic) NSUInteger styleIndex;
+
+@end
+
+@implementation MBXViewController
+{
+ BOOL _isTouringWorld;
+ BOOL _isShowingCustomStyleLayer;
+}
+
+#pragma mark - Setup
+
++ (void)initialize
+{
+ if (self == [MBXViewController class])
+ {
+ [[NSUserDefaults standardUserDefaults] registerDefaults:@{
+ @"MBXUserTrackingMode": @(MGLUserTrackingModeNone),
+ @"MBXShowsUserLocation": @NO,
+ @"MBXDebug": @NO,
+ }];
+ }
+}
+
+- (id)init
+{
+ self = [super init];
+
+ if (self)
+ {
+ [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(saveState:) name:UIApplicationDidEnterBackgroundNotification object:nil];
+ [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(restoreState:) name:UIApplicationWillEnterForegroundNotification object:nil];
+ [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(saveState:) name:UIApplicationWillTerminateNotification object:nil];
+ }
+
+ return self;
+}
+
+- (void)viewDidLoad
+{
+ [super viewDidLoad];
+
+ self.mapView = [[MGLMapView alloc] initWithFrame:self.view.bounds];
+ self.mapView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
+ self.mapView.delegate = self;
+ [self.view addSubview:self.mapView];
+
+ self.view.tintColor = kTintColor;
+ self.navigationController.navigationBar.tintColor = kTintColor;
+ self.mapView.tintColor = kTintColor;
+
+ self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:@"settings.png"]
+ style:UIBarButtonItemStylePlain
+ target:self
+ action:@selector(showSettings)];
+
+ self.styleIndex = 0;
+
+ UIButton *titleButton = [UIButton buttonWithType:UIButtonTypeCustom];
+ [titleButton setFrame:CGRectMake(0, 0, 150, 40)];
+ [titleButton setTitle:@(mbgl::util::default_styles::orderedStyles[self.styleIndex].name) forState:UIControlStateNormal];
+ [titleButton setTitleColor:kTintColor forState:UIControlStateNormal];
+ [titleButton addTarget:self action:@selector(cycleStyles) forControlEvents:UIControlEventTouchUpInside];
+ self.navigationItem.titleView = titleButton;
+
+ self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:@"TrackingLocationOffMask.png"]
+ style:UIBarButtonItemStylePlain
+ target:self
+ action:@selector(locateUser)];
+
+ [self.mapView addGestureRecognizer:[[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPress:)]];
+
+ [self restoreState:nil];
+}
+
+- (void)saveState:(__unused NSNotification *)notification
+{
+ if (self.mapView)
+ {
+ NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
+ NSData *archivedCamera = [NSKeyedArchiver archivedDataWithRootObject:self.mapView.camera];
+ [defaults setObject:archivedCamera forKey:@"MBXCamera"];
+ [defaults setInteger:self.mapView.userTrackingMode forKey:@"MBXUserTrackingMode"];
+ [defaults setBool:self.mapView.showsUserLocation forKey:@"MBXShowsUserLocation"];
+ [defaults setInteger:self.mapView.debugMask forKey:@"MBXDebugMask"];
+ [defaults synchronize];
+ }
+}
+
+- (void)restoreState:(__unused NSNotification *)notification
+{
+ if (self.mapView) {
+ NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
+ NSData *archivedCamera = [defaults objectForKey:@"MBXCamera"];
+ MGLMapCamera *camera = archivedCamera ? [NSKeyedUnarchiver unarchiveObjectWithData:archivedCamera] : nil;
+ if (camera)
+ {
+ self.mapView.camera = camera;
+ }
+ NSInteger uncheckedTrackingMode = [defaults integerForKey:@"MBXUserTrackingMode"];
+ if (uncheckedTrackingMode >= 0 &&
+ (NSUInteger)uncheckedTrackingMode >= MGLUserTrackingModeNone &&
+ (NSUInteger)uncheckedTrackingMode <= MGLUserTrackingModeFollowWithCourse)
+ {
+ self.mapView.userTrackingMode = (MGLUserTrackingMode)uncheckedTrackingMode;
+ }
+ self.mapView.showsUserLocation = [defaults boolForKey:@"MBXShowsUserLocation"];
+ NSInteger uncheckedDebugMask = [defaults integerForKey:@"MBXDebugMask"];
+ if (uncheckedDebugMask >= 0)
+ {
+ self.mapView.debugMask = (MGLMapDebugMaskOptions)uncheckedDebugMask;
+ }
+ }
+}
+
+- (NSUInteger)supportedInterfaceOrientations
+{
+ return UIInterfaceOrientationMaskAll;
+}
+
+#pragma mark - Actions
+
+- (void)showSettings
+{
+ MGLMapDebugMaskOptions debugMask = self.mapView.debugMask;
+ UIActionSheet *sheet = [[UIActionSheet alloc] initWithTitle:@"Map Settings"
+ delegate:self
+ cancelButtonTitle:@"Cancel"
+ destructiveButtonTitle:nil
+ otherButtonTitles:@"Reset Position",
+ ((debugMask & MGLMapDebugTileBoundariesMask)
+ ? @"Hide Tile Boundaries"
+ : @"Show Tile Boundaries"),
+ ((debugMask & MGLMapDebugTileInfoMask)
+ ? @"Hide Tile Info"
+ : @"Show Tile Info"),
+ ((debugMask & MGLMapDebugTimestampsMask)
+ ? @"Hide Tile Timestamps"
+ : @"Show Tile Timestamps"),
+ ((debugMask & MGLMapDebugCollisionBoxesMask)
+ ? @"Hide Collision Boxes"
+ : @"Show Collision Boxes"),
+ @"Empty Memory",
+ @"Add 100 Points",
+ @"Add 1,000 Points",
+ @"Add 10,000 Points",
+ @"Add Test Shapes",
+ @"Start World Tour",
+ @"Add Custom Callout Point",
+ @"Remove Annotations",
+ (_isShowingCustomStyleLayer
+ ? @"Hide Custom Style Layer"
+ : @"Show Custom Style Layer"),
+ @"Print Telemetry Logfile",
+ @"Delete Telemetry Logfile",
+ nil];
+
+ [sheet showFromBarButtonItem:self.navigationItem.leftBarButtonItem animated:YES];
+}
+
+- (void)actionSheet:(UIActionSheet *)actionSheet didDismissWithButtonIndex:(NSInteger)buttonIndex
+{
+ if (buttonIndex == actionSheet.firstOtherButtonIndex)
+ {
+ [self.mapView resetPosition];
+ }
+ else if (buttonIndex == actionSheet.firstOtherButtonIndex + 1)
+ {
+ self.mapView.debugMask ^= MGLMapDebugTileBoundariesMask;
+ }
+ else if (buttonIndex == actionSheet.firstOtherButtonIndex + 2)
+ {
+ self.mapView.debugMask ^= MGLMapDebugTileInfoMask;
+ }
+ else if (buttonIndex == actionSheet.firstOtherButtonIndex + 3)
+ {
+ self.mapView.debugMask ^= MGLMapDebugTimestampsMask;
+ }
+ else if (buttonIndex == actionSheet.firstOtherButtonIndex + 4)
+ {
+ self.mapView.debugMask ^= MGLMapDebugCollisionBoxesMask;
+ }
+ else if (buttonIndex == actionSheet.firstOtherButtonIndex + 5)
+ {
+ [self.mapView emptyMemoryCache];
+ }
+ else if (buttonIndex == actionSheet.firstOtherButtonIndex + 6)
+ {
+ [self parseFeaturesAddingCount:100];
+ }
+ else if (buttonIndex == actionSheet.firstOtherButtonIndex + 7)
+ {
+ [self parseFeaturesAddingCount:1000];
+ }
+ else if (buttonIndex == actionSheet.firstOtherButtonIndex + 8)
+ {
+ [self parseFeaturesAddingCount:10000];
+ }
+ else if (buttonIndex == actionSheet.firstOtherButtonIndex + 9)
+ {
+ // PNW triangle
+ //
+ CLLocationCoordinate2D triangleCoordinates[3] =
+ {
+ CLLocationCoordinate2DMake(44, -122),
+ CLLocationCoordinate2DMake(46, -122),
+ CLLocationCoordinate2DMake(46, -121)
+ };
+
+ MGLPolygon *triangle = [MGLPolygon polygonWithCoordinates:triangleCoordinates count:3];
+
+ [self.mapView addAnnotation:triangle];
+
+ // Orcas Island hike
+ //
+ NSDictionary *hike = [NSJSONSerialization JSONObjectWithData:
+ [NSData dataWithContentsOfFile:
+ [[NSBundle mainBundle] pathForResource:@"polyline" ofType:@"geojson"]]
+ options:0
+ error:nil];
+
+ NSArray *hikeCoordinatePairs = hike[@"features"][0][@"geometry"][@"coordinates"];
+
+ CLLocationCoordinate2D *polylineCoordinates = (CLLocationCoordinate2D *)malloc([hikeCoordinatePairs count] * sizeof(CLLocationCoordinate2D));
+
+ for (NSUInteger i = 0; i < [hikeCoordinatePairs count]; i++)
+ {
+ polylineCoordinates[i] = CLLocationCoordinate2DMake([hikeCoordinatePairs[i][1] doubleValue], [hikeCoordinatePairs[i][0] doubleValue]);
+ }
+
+ MGLPolyline *polyline = [MGLPolyline polylineWithCoordinates:polylineCoordinates
+ count:[hikeCoordinatePairs count]];
+
+ [self.mapView addAnnotation:polyline];
+
+ free(polylineCoordinates);
+
+ // PA/NJ/DE polys
+ //
+ NSDictionary *threestates = [NSJSONSerialization JSONObjectWithData:
+ [NSData dataWithContentsOfFile:
+ [[NSBundle mainBundle] pathForResource:@"threestates" ofType:@"geojson"]]
+ options:0
+ error:nil];
+
+ for (NSDictionary *feature in threestates[@"features"])
+ {
+ NSArray *stateCoordinatePairs = feature[@"geometry"][@"coordinates"];
+
+ while ([stateCoordinatePairs count] == 1) stateCoordinatePairs = stateCoordinatePairs[0];
+
+ CLLocationCoordinate2D *polygonCoordinates = (CLLocationCoordinate2D *)malloc([stateCoordinatePairs count] * sizeof(CLLocationCoordinate2D));
+
+ for (NSUInteger i = 0; i < [stateCoordinatePairs count]; i++)
+ {
+ polygonCoordinates[i] = CLLocationCoordinate2DMake([stateCoordinatePairs[i][1] doubleValue], [stateCoordinatePairs[i][0] doubleValue]);
+ }
+
+ MGLPolygon *polygon = [MGLPolygon polygonWithCoordinates:polygonCoordinates count:[stateCoordinatePairs count]];
+
+ [self.mapView addAnnotation:polygon];
+
+ free(polygonCoordinates);
+ }
+ }
+ else if (buttonIndex == actionSheet.firstOtherButtonIndex + 10)
+ {
+ [self startWorldTour:actionSheet];
+ }
+ else if (buttonIndex == actionSheet.firstOtherButtonIndex + 11)
+ {
+ [self presentAnnotationWithCustomCallout];
+ }
+ else if (buttonIndex == actionSheet.firstOtherButtonIndex + 12)
+ {
+ [self.mapView removeAnnotations:self.mapView.annotations];
+ }
+ else if (buttonIndex == actionSheet.firstOtherButtonIndex + 13)
+ {
+ if (_isShowingCustomStyleLayer)
+ {
+ [self removeCustomStyleLayer];
+ }
+ else
+ {
+ [self insertCustomStyleLayer];
+ }
+ }
+ else if (buttonIndex == actionSheet.firstOtherButtonIndex + 14)
+ {
+ NSString *fileContents = [NSString stringWithContentsOfFile:[self telemetryDebugLogfilePath] encoding:NSUTF8StringEncoding error:nil];
+ NSLog(@"%@", fileContents);
+ }
+ else if (buttonIndex == actionSheet.firstOtherButtonIndex + 15)
+ {
+ NSString *filePath = [self telemetryDebugLogfilePath];
+ if ([[NSFileManager defaultManager] isDeletableFileAtPath:filePath]) {
+ NSError *error;
+ BOOL success = [[NSFileManager defaultManager] removeItemAtPath:filePath error:&error];
+ if (success) {
+ NSLog(@"Deleted telemetry log.");
+ } else {
+ NSLog(@"Error deleting telemetry log: %@", error.localizedDescription);
+ }
+ }
+ }
+}
+
+- (void)parseFeaturesAddingCount:(NSUInteger)featuresCount
+{
+ [self.mapView removeAnnotations:self.mapView.annotations];
+
+ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^
+ {
+ NSData *featuresData = [NSData dataWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"points" ofType:@"geojson"]];
+
+ id features = [NSJSONSerialization JSONObjectWithData:featuresData
+ options:0
+ error:nil];
+
+ if ([features isKindOfClass:[NSDictionary class]])
+ {
+ NSMutableArray *annotations = [NSMutableArray array];
+
+ for (NSDictionary *feature in features[@"features"])
+ {
+ CLLocationCoordinate2D coordinate = CLLocationCoordinate2DMake([feature[@"geometry"][@"coordinates"][1] doubleValue],
+ [feature[@"geometry"][@"coordinates"][0] doubleValue]);
+ NSString *title = feature[@"properties"][@"NAME"];
+
+ MGLPointAnnotation *annotation = [MGLPointAnnotation new];
+ annotation.coordinate = coordinate;
+ annotation.title = title;
+
+ [annotations addObject:annotation];
+
+ if (annotations.count == featuresCount) break;
+ }
+
+ dispatch_async(dispatch_get_main_queue(), ^
+ {
+ [self.mapView addAnnotations:annotations];
+ [self.mapView showAnnotations:annotations animated:YES];
+ });
+ }
+ });
+}
+
+- (void)insertCustomStyleLayer
+{
+ _isShowingCustomStyleLayer = YES;
+
+ static const GLchar *vertexShaderSource = "attribute vec2 a_pos; void main() { gl_Position = vec4(a_pos, 0, 1); }";
+ static const GLchar *fragmentShaderSource = "void main() { gl_FragColor = vec4(0, 1, 0, 1); }";
+
+ __block GLuint program = 0;
+ __block GLuint vertexShader = 0;
+ __block GLuint fragmentShader = 0;
+ __block GLuint buffer = 0;
+ __block GLuint a_pos = 0;
+ [self.mapView insertCustomStyleLayerWithIdentifier:@"mbx-custom" preparationHandler:^{
+ program = glCreateProgram();
+ vertexShader = glCreateShader(GL_VERTEX_SHADER);
+ fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
+
+ glShaderSource(vertexShader, 1, &vertexShaderSource, nullptr);
+ glCompileShader(vertexShader);
+ glAttachShader(program, vertexShader);
+ glShaderSource(fragmentShader, 1, &fragmentShaderSource, nullptr);
+ glCompileShader(fragmentShader);
+ glAttachShader(program, fragmentShader);
+ glLinkProgram(program);
+ a_pos = glGetAttribLocation(program, "a_pos");
+
+ GLfloat background[] = { -1,-1, 1,-1, -1,1, 1,1 };
+ glGenBuffers(1, &buffer);
+ glBindBuffer(GL_ARRAY_BUFFER, buffer);
+ glBufferData(GL_ARRAY_BUFFER, 8 * sizeof(GLfloat), background, GL_STATIC_DRAW);
+ } drawingHandler:^(__unused CGSize size,
+ __unused CLLocationCoordinate2D centerCoordinate,
+ __unused double zoomLevel,
+ __unused CLLocationDirection direction,
+ __unused CGFloat pitch,
+ __unused CGFloat perspectiveSkew) {
+ glUseProgram(program);
+ glBindBuffer(GL_ARRAY_BUFFER, buffer);
+ glEnableVertexAttribArray(a_pos);
+ glVertexAttribPointer(a_pos, 2, GL_FLOAT, GL_FALSE, 0, NULL);
+ glDisable(GL_STENCIL_TEST);
+ glDisable(GL_DEPTH_TEST);
+ glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
+ } completionHandler:^{
+ if (program) {
+ glDeleteBuffers(1, &buffer);
+ glDetachShader(program, vertexShader);
+ glDetachShader(program, fragmentShader);
+ glDeleteShader(vertexShader);
+ glDeleteShader(fragmentShader);
+ glDeleteProgram(program);
+ }
+ } belowStyleLayerWithIdentifier:@"housenum-label"];
+}
+
+- (void)removeCustomStyleLayer
+{
+ _isShowingCustomStyleLayer = NO;
+ [self.mapView removeCustomStyleLayerWithIdentifier:@"mbx-custom"];
+}
+
+- (void)presentAnnotationWithCustomCallout
+{
+ [self.mapView removeAnnotations:self.mapView.annotations];
+
+ MGLPointAnnotation *annotation = [MGLPointAnnotation new];
+ annotation.coordinate = CLLocationCoordinate2DMake(48.8533940, 2.3775439);
+ annotation.title = kCustomCalloutTitle;
+
+ [self.mapView addAnnotation:annotation];
+ [self.mapView showAnnotations:@[annotation] animated:YES];
+}
+
+- (void)handleLongPress:(UILongPressGestureRecognizer *)longPress
+{
+ if (longPress.state == UIGestureRecognizerStateBegan)
+ {
+ MGLPointAnnotation *point = [MGLPointAnnotation new];
+ point.coordinate = [self.mapView convertPoint:[longPress locationInView:longPress.view]
+ toCoordinateFromView:self.mapView];
+ point.title = @"Dropped Marker";
+ point.subtitle = [NSString stringWithFormat:@"lat: %.3f, lon: %.3f", point.coordinate.latitude, point.coordinate.longitude];
+ [self.mapView addAnnotation:point];
+ [self.mapView selectAnnotation:point animated:YES];
+ }
+}
+
+- (void)cycleStyles
+{
+ UIButton *titleButton = (UIButton *)self.navigationItem.titleView;
+
+ self.styleIndex = (self.styleIndex + 1) % mbgl::util::default_styles::numOrderedStyles;
+
+ self.mapView.styleURL = [NSURL URLWithString:@(mbgl::util::default_styles::orderedStyles[self.styleIndex].url)];
+
+ [titleButton setTitle:@(mbgl::util::default_styles::orderedStyles[self.styleIndex].name) forState:UIControlStateNormal];
+}
+
+- (void)locateUser
+{
+ MGLUserTrackingMode nextMode;
+ switch (self.mapView.userTrackingMode) {
+ case MGLUserTrackingModeNone:
+ nextMode = MGLUserTrackingModeFollow;
+ break;
+ case MGLUserTrackingModeFollow:
+ nextMode = MGLUserTrackingModeFollowWithHeading;
+ break;
+ case MGLUserTrackingModeFollowWithHeading:
+ nextMode = MGLUserTrackingModeFollowWithCourse;
+ break;
+ case MGLUserTrackingModeFollowWithCourse:
+ nextMode = MGLUserTrackingModeNone;
+ break;
+ }
+ self.mapView.userTrackingMode = nextMode;
+}
+
+- (IBAction)startWorldTour:(__unused id)sender
+{
+ _isTouringWorld = YES;
+
+ [self.mapView removeAnnotations:self.mapView.annotations];
+ NSUInteger numberOfAnnotations = sizeof(WorldTourDestinations) / sizeof(WorldTourDestinations[0]);
+ NSMutableArray *annotations = [NSMutableArray arrayWithCapacity:numberOfAnnotations];
+ for (NSUInteger i = 0; i < numberOfAnnotations; i++)
+ {
+ MGLPointAnnotation *annotation = [[MGLPointAnnotation alloc] init];
+ annotation.coordinate = WorldTourDestinations[i];
+ [annotations addObject:annotation];
+ }
+ [self.mapView addAnnotations:annotations];
+ [self continueWorldTourWithRemainingAnnotations:annotations];
+}
+
+- (void)continueWorldTourWithRemainingAnnotations:(NS_MUTABLE_ARRAY_OF(MGLPointAnnotation *) *)annotations
+{
+ MGLPointAnnotation *nextAnnotation = annotations.firstObject;
+ if (!nextAnnotation || !_isTouringWorld)
+ {
+ _isTouringWorld = NO;
+ return;
+ }
+
+ [annotations removeObjectAtIndex:0];
+ MGLMapCamera *camera = [MGLMapCamera cameraLookingAtCenterCoordinate:nextAnnotation.coordinate
+ fromDistance:10
+ pitch:arc4random_uniform(60)
+ heading:arc4random_uniform(360)];
+ __weak MBXViewController *weakSelf = self;
+ [self.mapView flyToCamera:camera completionHandler:^{
+ MBXViewController *strongSelf = weakSelf;
+ [strongSelf performSelector:@selector(continueWorldTourWithRemainingAnnotations:)
+ withObject:annotations
+ afterDelay:2];
+ }];
+}
+
+- (NSString *)telemetryDebugLogfilePath
+{
+ NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
+ [dateFormatter setDateFormat:@"yyyy'-'MM'-'dd"];
+ [dateFormatter setTimeZone:[NSTimeZone systemTimeZone]];
+ NSString *filePath = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject] stringByAppendingPathComponent:[NSString stringWithFormat:@"telemetry_log-%@.json", [dateFormatter stringFromDate:[NSDate date]]]];
+
+ return filePath;
+}
+
+#pragma mark - Destruction
+
+- (void)dealloc
+{
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+
+ [self saveState:nil];
+}
+
+#pragma mark - MGLMapViewDelegate
+
+- (MGLAnnotationImage *)mapView:(MGLMapView * __nonnull)mapView imageForAnnotation:(id <MGLAnnotation> __nonnull)annotation
+{
+ if ([annotation.title isEqualToString:@"Dropped Marker"]
+ || [annotation.title isEqualToString:kCustomCalloutTitle])
+ {
+ return nil; // use default marker
+ }
+
+ NSString *title = [(MGLPointAnnotation *)annotation title];
+ if (!title.length) return nil;
+ NSString *lastTwoCharacters = [title substringFromIndex:title.length - 2];
+
+ UIColor *color;
+
+ // make every tenth annotation blue
+ if ([lastTwoCharacters hasSuffix:@"0"]) {
+ color = [UIColor blueColor];
+ } else {
+ color = [UIColor redColor];
+ }
+
+ MGLAnnotationImage *image = [mapView dequeueReusableAnnotationImageWithIdentifier:lastTwoCharacters];
+
+ if ( ! image)
+ {
+ CGRect rect = CGRectMake(0, 0, 20, 15);
+
+ UIGraphicsBeginImageContextWithOptions(rect.size, NO, [[UIScreen mainScreen] scale]);
+
+ CGContextRef ctx = UIGraphicsGetCurrentContext();
+
+ CGContextSetFillColorWithColor(ctx, [[color colorWithAlphaComponent:0.75] CGColor]);
+ CGContextFillRect(ctx, rect);
+
+ CGContextSetStrokeColorWithColor(ctx, [[UIColor blackColor] CGColor]);
+ CGContextStrokeRectWithWidth(ctx, rect, 2);
+
+ NSAttributedString *drawString = [[NSAttributedString alloc] initWithString:lastTwoCharacters attributes:@{
+ NSFontAttributeName: [UIFont fontWithName:@"Arial-BoldMT" size:12],
+ NSForegroundColorAttributeName: [UIColor whiteColor] }];
+ CGSize stringSize = drawString.size;
+ CGRect stringRect = CGRectMake((rect.size.width - stringSize.width) / 2,
+ (rect.size.height - stringSize.height) / 2,
+ stringSize.width,
+ stringSize.height);
+ [drawString drawInRect:stringRect];
+
+ image = [MGLAnnotationImage annotationImageWithImage:UIGraphicsGetImageFromCurrentImageContext() reuseIdentifier:lastTwoCharacters];
+
+ // don't allow touches on blue annotations
+ if ([color isEqual:[UIColor blueColor]]) image.enabled = NO;
+
+ UIGraphicsEndImageContext();
+ }
+
+ return image;
+}
+
+- (BOOL)mapView:(__unused MGLMapView *)mapView annotationCanShowCallout:(__unused id <MGLAnnotation>)annotation
+{
+ return YES;
+}
+
+- (CGFloat)mapView:(__unused MGLMapView *)mapView alphaForShapeAnnotation:(MGLShape *)annotation
+{
+ return ([annotation isKindOfClass:[MGLPolygon class]] ? 0.5 : 1.0);
+}
+
+- (UIColor *)mapView:(__unused MGLMapView *)mapView strokeColorForShapeAnnotation:(MGLShape *)annotation
+{
+ return ([annotation isKindOfClass:[MGLPolyline class]] ? [UIColor purpleColor] : [UIColor blackColor]);
+}
+
+- (UIColor *)mapView:(__unused MGLMapView *)mapView fillColorForPolygonAnnotation:(__unused MGLPolygon *)annotation
+{
+ return (annotation.pointCount > 3 ? [UIColor greenColor] : [UIColor redColor]);
+}
+
+- (void)mapView:(__unused MGLMapView *)mapView didChangeUserTrackingMode:(MGLUserTrackingMode)mode animated:(__unused BOOL)animated
+{
+ UIImage *newButtonImage;
+ NSString *newButtonTitle;
+
+ switch (mode) {
+ case MGLUserTrackingModeNone:
+ newButtonImage = [UIImage imageNamed:@"TrackingLocationOffMask.png"];
+ break;
+
+ case MGLUserTrackingModeFollow:
+ newButtonImage = [UIImage imageNamed:@"TrackingLocationMask.png"];
+ break;
+
+ case MGLUserTrackingModeFollowWithHeading:
+ newButtonImage = [UIImage imageNamed:@"TrackingHeadingMask.png"];
+ break;
+ case MGLUserTrackingModeFollowWithCourse:
+ newButtonImage = nil;
+ newButtonTitle = @"Course";
+ break;
+ }
+
+ self.navigationItem.rightBarButtonItem.title = newButtonTitle;
+ [UIView animateWithDuration:0.25 animations:^{
+ self.navigationItem.rightBarButtonItem.image = newButtonImage;
+ }];
+}
+
+- (UIView<MGLCalloutView> *)mapView:(__unused MGLMapView *)mapView calloutViewForAnnotation:(id<MGLAnnotation>)annotation
+{
+ if ([annotation respondsToSelector:@selector(title)]
+ && [annotation.title isEqualToString:kCustomCalloutTitle])
+ {
+ MBXCustomCalloutView *calloutView = [[MBXCustomCalloutView alloc] init];
+ calloutView.representedObject = annotation;
+ return calloutView;
+ }
+ return nil;
+}
+
+@end