1/**
2 * Copyright (c) 2015-present, Facebook, Inc.
3 * All rights reserved.
4 *
5 * This source code is licensed under the BSD-style license found in the
6 * LICENSE file in the root directory of this source tree. An additional grant
7 * of patent rights can be found in the PATENTS file in the same directory.
8 */
9
10#import "AIRMap.h"
11
12#import <React/RCTEventDispatcher.h>
13#import <React/UIView+React.h>
14#import "AIRMapMarker.h"
15#import "AIRMapPolyline.h"
16#import "AIRMapPolygon.h"
17#import "AIRMapCircle.h"
18#import <QuartzCore/QuartzCore.h>
19#import "AIRMapUrlTile.h"
20#import "AIRMapWMSTile.h"
21#import "AIRMapLocalTile.h"
22#import "AIRMapOverlay.h"
23
24const NSTimeInterval AIRMapRegionChangeObserveInterval = 0.1;
25const CGFloat AIRMapZoomBoundBuffer = 0.01;
26const NSInteger AIRMapMaxZoomLevel = 20;
27
28
29@interface MKMapView (UIGestureRecognizer)
30
31// this tells the compiler that MKMapView actually implements this method
32- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch;
33
34@end
35
36@interface AIRMap ()
37
38@property (nonatomic, strong) UIActivityIndicatorView *activityIndicatorView;
39@property (nonatomic, assign) NSNumber *shouldZoomEnabled;
40@property (nonatomic, assign) NSNumber *shouldScrollEnabled;
41
42- (void)updateScrollEnabled;
43- (void)updateZoomEnabled;
44
45@end
46
47@implementation AIRMap
48{
49    UIView *_legalLabel;
50    BOOL _initialRegionSet;
51    BOOL _initialCameraSet;
52
53    // Array to manually track RN subviews
54    //
55    // AIRMap implicitly creates subviews that aren't regular RN children
56    // (SMCalloutView injects an overlay subview), which otherwise confuses RN
57    // during component re-renders:
58    // https://github.com/facebook/react-native/blob/v0.16.0/React/Modules/RCTUIManager.m#L657
59    //
60    // Implementation based on RCTTextField, another component with indirect children
61    // https://github.com/facebook/react-native/blob/v0.16.0/Libraries/Text/RCTTextField.m#L20
62    NSMutableArray<UIView *> *_reactSubviews;
63}
64
65- (instancetype)init
66{
67    if ((self = [super init])) {
68        _hasStartedRendering = NO;
69        _reactSubviews = [NSMutableArray new];
70
71        // Find Apple link label
72        for (UIView *subview in self.subviews) {
73            if ([NSStringFromClass(subview.class) isEqualToString:@"MKAttributionLabel"]) {
74                // This check is super hacky, but the whole premise of moving around
75                // Apple's internal subviews is super hacky
76                _legalLabel = subview;
77                break;
78            }
79        }
80
81        // 3rd-party callout view for MapKit that has more options than the built-in. It's painstakingly built to
82        // be identical to the built-in callout view (which has a private API)
83        self.calloutView = [SMCalloutView platformCalloutView];
84        self.calloutView.delegate = self;
85
86        self.minZoomLevel = 0;
87        self.maxZoomLevel = AIRMapMaxZoomLevel;
88        self.compassOffset = CGPointMake(0, 0);
89    }
90    return self;
91}
92
93-(void)addSubview:(UIView *)view {
94    if([view isKindOfClass:[AIRMapMarker class]]) {
95        [self addAnnotation:(id <MKAnnotation>)view];
96    } else {
97        [super addSubview:view];
98    }
99}
100
101#pragma clang diagnostic push
102#pragma clang diagnostic ignored "-Wobjc-missing-super-calls"
103- (void)insertReactSubview:(id<RCTComponent>)subview atIndex:(NSInteger)atIndex {
104    // Our desired API is to pass up markers/overlays as children to the mapview component.
105    // This is where we intercept them and do the appropriate underlying mapview action.
106    if ([subview isKindOfClass:[AIRMapMarker class]]) {
107        [self addAnnotation:(id <MKAnnotation>) subview];
108    } else if ([subview isKindOfClass:[AIRMapPolyline class]]) {
109        ((AIRMapPolyline *)subview).map = self;
110        [self addOverlay:(id<MKOverlay>)subview];
111    } else if ([subview isKindOfClass:[AIRMapPolygon class]]) {
112        ((AIRMapPolygon *)subview).map = self;
113        [self addOverlay:(id<MKOverlay>)subview];
114    } else if ([subview isKindOfClass:[AIRMapCircle class]]) {
115        ((AIRMapCircle *)subview).map = self;
116        [self addOverlay:(id<MKOverlay>)subview];
117    } else if ([subview isKindOfClass:[AIRMapUrlTile class]]) {
118        ((AIRMapUrlTile *)subview).map = self;
119        [self addOverlay:(id<MKOverlay>)subview];
120    }else if ([subview isKindOfClass:[AIRMapWMSTile class]]) {
121        ((AIRMapWMSTile *)subview).map = self;
122        [self addOverlay:(id<MKOverlay>)subview];
123    } else if ([subview isKindOfClass:[AIRMapLocalTile class]]) {
124        ((AIRMapLocalTile *)subview).map = self;
125        [self addOverlay:(id<MKOverlay>)subview];
126    } else if ([subview isKindOfClass:[AIRMapOverlay class]]) {
127        ((AIRMapOverlay *)subview).map = self;
128        [self addOverlay:(id<MKOverlay>)subview];
129    } else {
130        NSArray<id<RCTComponent>> *childSubviews = [subview reactSubviews];
131        for (int i = 0; i < childSubviews.count; i++) {
132          [self insertReactSubview:(UIView *)childSubviews[i] atIndex:atIndex];
133        }
134    }
135    [_reactSubviews insertObject:(UIView *)subview atIndex:(NSUInteger) atIndex];
136}
137#pragma clang diagnostic pop
138
139#pragma clang diagnostic push
140#pragma clang diagnostic ignored "-Wobjc-missing-super-calls"
141- (void)removeReactSubview:(id<RCTComponent>)subview {
142    // similarly, when the children are being removed we have to do the appropriate
143    // underlying mapview action here.
144    if ([subview isKindOfClass:[AIRMapMarker class]]) {
145        [self removeAnnotation:(id<MKAnnotation>)subview];
146    } else if ([subview isKindOfClass:[AIRMapPolyline class]]) {
147        [self removeOverlay:(id <MKOverlay>) subview];
148    } else if ([subview isKindOfClass:[AIRMapPolygon class]]) {
149        [self removeOverlay:(id <MKOverlay>) subview];
150    } else if ([subview isKindOfClass:[AIRMapCircle class]]) {
151        [self removeOverlay:(id <MKOverlay>) subview];
152    } else if ([subview isKindOfClass:[AIRMapUrlTile class]]) {
153        [self removeOverlay:(id <MKOverlay>) subview];
154    } else if ([subview isKindOfClass:[AIRMapWMSTile class]]) {
155        [self removeOverlay:(id <MKOverlay>) subview];
156    } else if ([subview isKindOfClass:[AIRMapLocalTile class]]) {
157        [self removeOverlay:(id <MKOverlay>) subview];
158    } else if ([subview isKindOfClass:[AIRMapOverlay class]]) {
159        [self removeOverlay:(id <MKOverlay>) subview];
160    } else {
161        NSArray<id<RCTComponent>> *childSubviews = [subview reactSubviews];
162        for (int i = 0; i < childSubviews.count; i++) {
163          [self removeReactSubview:(UIView *)childSubviews[i]];
164        }
165    }
166    [_reactSubviews removeObject:(UIView *)subview];
167}
168#pragma clang diagnostic pop
169
170#pragma clang diagnostic push
171#pragma clang diagnostic ignored "-Wobjc-missing-super-calls"
172- (NSArray<id<RCTComponent>> *)reactSubviews {
173  return _reactSubviews;
174}
175#pragma clang diagnostic pop
176
177#pragma mark Utils
178
179- (NSArray*) markers {
180    NSPredicate *filterMarkers = [NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) {
181        AIRMapMarker *marker = (AIRMapMarker *)evaluatedObject;
182        return [marker isKindOfClass:[AIRMapMarker class]];
183    }];
184    NSArray *filteredMarkers = [self.annotations filteredArrayUsingPredicate:filterMarkers];
185    return filteredMarkers;
186}
187
188- (AIRMapMarker*) markerForCallout:(AIRMapCallout*)callout {
189    AIRMapMarker* marker = nil;
190    NSArray* markers = [self markers];
191    for (AIRMapMarker* mrk in markers) {
192        if (mrk.calloutView == callout) {
193            marker = mrk;
194            break;
195        }
196    }
197    return marker;
198}
199
200- (CGRect) frameForMarker:(AIRMapMarker*) mrkAnn {
201    MKAnnotationView* mrkView = [self viewForAnnotation: mrkAnn];
202    CGRect mrkFrame = mrkView.frame;
203    return mrkFrame;
204}
205
206- (NSDictionary*) getMarkersFramesWithOnlyVisible:(BOOL)onlyVisible {
207    NSMutableDictionary* markersFrames = [NSMutableDictionary new];
208    for (AIRMapMarker* mrkAnn in self.markers) {
209        CGRect frame = [self frameForMarker:mrkAnn];
210        CGPoint point = [self convertCoordinate:mrkAnn.coordinate toPointToView:self];
211        NSDictionary* frameDict = @{
212                                    @"x": @(frame.origin.x),
213                                    @"y": @(frame.origin.y),
214                                    @"width": @(frame.size.width),
215                                    @"height": @(frame.size.height)
216                                    };
217        NSDictionary* pointDict = @{
218                                   @"x": @(point.x),
219                                   @"y": @(point.y)
220                                  };
221        NSString* k = mrkAnn.identifier;
222        BOOL isVisible = CGRectIntersectsRect(self.bounds, frame);
223        if (k != nil && (!onlyVisible || isVisible)) {
224            [markersFrames setObject:@{ @"frame": frameDict, @"point": pointDict } forKey:k];
225        }
226    }
227    return markersFrames;
228}
229
230- (AIRMapMarker*) markerAtPoint:(CGPoint)point {
231    AIRMapMarker* mrk = nil;
232    for (AIRMapMarker* mrkAnn in self.markers) {
233        CGRect frame = [self frameForMarker:mrkAnn];
234        if (CGRectContainsPoint(frame, point)) {
235            mrk = mrkAnn;
236            break;
237        }
238    }
239    return mrk;
240}
241
242#pragma mark Overrides for Callout behavior
243
244// override UIGestureRecognizer's delegate method so we can prevent MKMapView's recognizer from firing
245// when we interact with UIControl subclasses inside our callout view.
246- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
247    if ([touch.view isDescendantOfView:self.calloutView])
248        return NO;
249    else
250        return [super gestureRecognizer:gestureRecognizer shouldReceiveTouch:touch];
251}
252
253
254// Allow touches to be sent to our calloutview.
255// See this for some discussion of why we need to override this: https://github.com/nfarina/calloutview/pull/9
256- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
257
258    CGPoint touchPoint = [self.calloutView convertPoint:point fromView:self];
259    UIView *touchedView = [self.calloutView hitTest:touchPoint withEvent:event];
260
261    if (touchedView) {
262        UIWindow* win = [[[UIApplication sharedApplication] windows] firstObject];
263        AIRMapCalloutSubview* calloutSubview = nil;
264        AIRMapCallout* callout = nil;
265        AIRMapMarker* marker = nil;
266
267        UIView* tmp = touchedView;
268        while (tmp && tmp != win && tmp != self.calloutView) {
269            if ([tmp respondsToSelector:@selector(onPress)]) {
270                calloutSubview = (AIRMapCalloutSubview*) tmp;
271            }
272            if ([tmp isKindOfClass:[AIRMapCallout class]]) {
273                callout = (AIRMapCallout*) tmp;
274                break;
275            }
276            tmp = tmp.superview;
277        }
278
279        if (callout) {
280            marker = [self markerForCallout:callout];
281            if (marker) {
282                CGPoint touchPointReal = [marker.calloutView convertPoint:point fromView:self];
283                if (![callout isPointInside:touchPointReal]) {
284                    return [super hitTest:point withEvent:event];
285                }
286            }
287        }
288
289        return calloutSubview ? calloutSubview : touchedView;
290    }
291
292    return [super hitTest:point withEvent:event];
293}
294
295#pragma mark SMCalloutViewDelegate
296
297- (NSTimeInterval)calloutView:(SMCalloutView *)calloutView delayForRepositionWithSize:(CGSize)offset {
298
299    // When the callout is being asked to present in a way where it or its target will be partially offscreen, it asks us
300    // if we'd like to reposition our surface first so the callout is completely visible. Here we scroll the map into view,
301    // but it takes some math because we have to deal in lon/lat instead of the given offset in pixels.
302
303    CLLocationCoordinate2D coordinate = self.region.center;
304
305    // where's the center coordinate in terms of our view?
306    CGPoint center = [self convertCoordinate:coordinate toPointToView:self];
307
308    // move it by the requested offset
309    center.x -= offset.width;
310    center.y -= offset.height;
311
312    // and translate it back into map coordinates
313    coordinate = [self convertPoint:center toCoordinateFromView:self];
314
315    // move the map!
316    [self setCenterCoordinate:coordinate animated:YES];
317
318    // tell the callout to wait for a while while we scroll (we assume the scroll delay for MKMapView matches UIScrollView)
319    return kSMCalloutViewRepositionDelayForUIScrollView;
320}
321
322#pragma mark Accessors
323
324- (NSArray *)getMapBoundaries
325{
326    MKMapRect mapRect = self.visibleMapRect;
327
328    CLLocationCoordinate2D northEast = MKCoordinateForMapPoint(MKMapPointMake(MKMapRectGetMaxX(mapRect), mapRect.origin.y));
329    CLLocationCoordinate2D southWest = MKCoordinateForMapPoint(MKMapPointMake(mapRect.origin.x, MKMapRectGetMaxY(mapRect)));
330
331    return @[
332        @[
333            [NSNumber numberWithDouble:northEast.longitude],
334            [NSNumber numberWithDouble:northEast.latitude]
335        ],
336        @[
337            [NSNumber numberWithDouble:southWest.longitude],
338            [NSNumber numberWithDouble:southWest.latitude]
339        ]
340    ];
341}
342
343- (void)setShowsUserLocation:(BOOL)showsUserLocation
344{
345    if (self.showsUserLocation != showsUserLocation) {
346        super.showsUserLocation = showsUserLocation;
347    }
348}
349
350- (void)setUserInterfaceStyle:(NSString*)userInterfaceStyle
351{
352    if (@available(iOS 13.0, *)) {
353        if([userInterfaceStyle isEqualToString:@"light"]) {
354            self.overrideUserInterfaceStyle = UIUserInterfaceStyleLight;
355        } else if([userInterfaceStyle isEqualToString:@"dark"]) {
356            self.overrideUserInterfaceStyle = UIUserInterfaceStyleDark;
357        } else {
358            self.overrideUserInterfaceStyle = UIUserInterfaceStyleUnspecified;
359        }
360    } else {
361        NSLog(@"UserInterfaceStyle not supported below iOS 13");
362    }
363}
364
365- (void)setTintColor:(UIColor *)tintColor
366{
367    super.tintColor = tintColor;
368}
369
370- (void)setFollowsUserLocation:(BOOL)followsUserLocation
371{
372    _followUserLocation = followsUserLocation;
373}
374
375- (void)setUserLocationCalloutEnabled:(BOOL)calloutEnabled
376{
377    _userLocationCalloutEnabled = calloutEnabled;
378}
379
380- (void)setHandlePanDrag:(BOOL)handleMapDrag {
381    for (UIGestureRecognizer *recognizer in [self gestureRecognizers]) {
382        if ([recognizer isKindOfClass:[UIPanGestureRecognizer class]]) {
383            recognizer.enabled = handleMapDrag;
384            break;
385        }
386    }
387}
388
389- (void)setRegion:(MKCoordinateRegion)region animated:(BOOL)animated
390{
391    // If location is invalid, abort
392    if (!CLLocationCoordinate2DIsValid(region.center)) {
393        return;
394    }
395
396    // If new span values are nil, use old values instead
397    if (!region.span.latitudeDelta) {
398        region.span.latitudeDelta = self.region.span.latitudeDelta;
399    }
400    if (!region.span.longitudeDelta) {
401        region.span.longitudeDelta = self.region.span.longitudeDelta;
402    }
403
404    // Animate/move to new position
405    [super setRegion:region animated:animated];
406}
407
408- (void)setInitialRegion:(MKCoordinateRegion)initialRegion {
409    if (!_initialRegionSet) {
410        _initialRegionSet = YES;
411        [self setRegion:initialRegion animated:NO];
412    }
413}
414
415- (void)setCamera:(MKMapCamera*)camera animated:(BOOL)animated
416{
417    [super setCamera:camera animated:animated];
418}
419
420
421- (void)setInitialCamera:(MKMapCamera*)initialCamera {
422    if (!_initialCameraSet) {
423        _initialCameraSet = YES;
424        [self setCamera:initialCamera animated:NO];
425    }
426}
427
428- (void)setCacheEnabled:(BOOL)cacheEnabled {
429    _cacheEnabled = cacheEnabled;
430    if (self.cacheEnabled && self.cacheImageView.image == nil) {
431        self.loadingView.hidden = NO;
432        [self.activityIndicatorView startAnimating];
433    }
434    else {
435        if (_loadingView != nil) {
436            self.loadingView.hidden = YES;
437        }
438    }
439}
440
441- (void)setLoadingEnabled:(BOOL)loadingEnabled {
442    _loadingEnabled = loadingEnabled;
443    if (!self.hasShownInitialLoading) {
444        self.loadingView.hidden = !self.loadingEnabled;
445    }
446    else {
447        if (_loadingView != nil) {
448            self.loadingView.hidden = YES;
449        }
450    }
451}
452
453- (UIColor *)loadingBackgroundColor {
454    return self.loadingView.backgroundColor;
455}
456
457- (void)setLoadingBackgroundColor:(UIColor *)loadingBackgroundColor {
458    self.loadingView.backgroundColor = loadingBackgroundColor;
459}
460
461- (UIColor *)loadingIndicatorColor {
462    return self.activityIndicatorView.color;
463}
464
465- (void)setLoadingIndicatorColor:(UIColor *)loadingIndicatorColor {
466    self.activityIndicatorView.color = loadingIndicatorColor;
467}
468
469// Include properties of MKMapView which are only available on iOS 9+
470// and check if their selector is available before calling super method.
471
472- (void)setShowsCompass:(BOOL)showsCompass {
473    if ([MKMapView instancesRespondToSelector:@selector(setShowsCompass:)]) {
474        [super setShowsCompass:showsCompass];
475    }
476}
477
478- (BOOL)showsCompass {
479    if ([MKMapView instancesRespondToSelector:@selector(showsCompass)]) {
480        return [super showsCompass];
481    } else {
482        return NO;
483    }
484}
485
486- (void)setShowsScale:(BOOL)showsScale {
487    if ([MKMapView instancesRespondToSelector:@selector(setShowsScale:)]) {
488        [super setShowsScale:showsScale];
489    }
490}
491
492- (BOOL)showsScale {
493    if ([MKMapView instancesRespondToSelector:@selector(showsScale)]) {
494        return [super showsScale];
495    } else {
496        return NO;
497    }
498}
499
500- (void)setShowsTraffic:(BOOL)showsTraffic {
501    if ([MKMapView instancesRespondToSelector:@selector(setShowsTraffic:)]) {
502        [super setShowsTraffic:showsTraffic];
503    }
504}
505
506- (BOOL)showsTraffic {
507    if ([MKMapView instancesRespondToSelector:@selector(showsTraffic)]) {
508        return [super showsTraffic];
509    } else {
510        return NO;
511    }
512}
513
514- (void)setScrollEnabled:(BOOL)scrollEnabled {
515    self.shouldScrollEnabled = [NSNumber numberWithBool:scrollEnabled];
516    [self updateScrollEnabled];
517}
518
519- (void)updateScrollEnabled {
520    if (self.cacheEnabled) {
521        [super setScrollEnabled:NO];
522    }
523    else if (self.shouldScrollEnabled != nil) {
524        [super setScrollEnabled:[self.shouldScrollEnabled boolValue]];
525    }
526}
527
528- (void)setZoomEnabled:(BOOL)zoomEnabled {
529    self.shouldZoomEnabled = [NSNumber numberWithBool:zoomEnabled];
530    [self updateZoomEnabled];
531}
532
533- (void)updateZoomEnabled {
534    if (self.cacheEnabled) {
535        [super setZoomEnabled: NO];
536    }
537    else if (self.shouldZoomEnabled != nil) {
538        [super setZoomEnabled:[self.shouldZoomEnabled boolValue]];
539    }
540}
541
542- (void)cacheViewIfNeeded {
543    // https://github.com/react-native-maps/react-native-maps/issues/3100
544    // Do nothing if app is not active
545    if ([[UIApplication sharedApplication] applicationState] != UIApplicationStateActive) {
546        return;
547    }
548    if (self.hasShownInitialLoading) {
549        if (!self.cacheEnabled) {
550            if (_cacheImageView != nil) {
551                self.cacheImageView.hidden = YES;
552                self.cacheImageView.image = nil;
553            }
554        }
555        else {
556            self.cacheImageView.image = nil;
557            self.cacheImageView.hidden = YES;
558
559            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.01 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
560                self.cacheImageView.image = nil;
561                self.cacheImageView.hidden = YES;
562                UIGraphicsBeginImageContextWithOptions(self.bounds.size, self.opaque, 0.0);
563                [self.layer renderInContext:UIGraphicsGetCurrentContext()];
564                UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
565                UIGraphicsEndImageContext();
566
567                self.cacheImageView.image = image;
568                self.cacheImageView.hidden = NO;
569            });
570        }
571
572        [self updateScrollEnabled];
573        [self updateZoomEnabled];
574        [self updateLegalLabelInsets];
575    }
576}
577
578- (void)updateLegalLabelInsets {
579    if (_legalLabel) {
580        dispatch_async(dispatch_get_main_queue(), ^{
581            CGRect frame = self->_legalLabel.frame;
582            if (self->_legalLabelInsets.left) {
583                frame.origin.x = self->_legalLabelInsets.left;
584            } else if (self->_legalLabelInsets.right) {
585                frame.origin.x = self.frame.size.width - self->_legalLabelInsets.right - frame.size.width;
586            }
587            if (self->_legalLabelInsets.top) {
588                frame.origin.y = self->_legalLabelInsets.top;
589            } else if (self->_legalLabelInsets.bottom) {
590                frame.origin.y = self.frame.size.height - self->_legalLabelInsets.bottom - frame.size.height;
591            }
592            self->_legalLabel.frame = frame;
593        });
594    }
595}
596
597
598- (void)setLegalLabelInsets:(UIEdgeInsets)legalLabelInsets {
599  _legalLabelInsets = legalLabelInsets;
600  [self updateLegalLabelInsets];
601}
602
603- (void)setMapPadding:(UIEdgeInsets)mapPadding {
604  self.layoutMargins = mapPadding;
605}
606
607- (UIEdgeInsets)mapPadding {
608  return self.layoutMargins;
609}
610
611- (void)beginLoading {
612    if ((!self.hasShownInitialLoading && self.loadingEnabled) || (self.cacheEnabled && self.cacheImageView.image == nil)) {
613        self.loadingView.hidden = NO;
614        [self.activityIndicatorView startAnimating];
615    }
616    else {
617        if (_loadingView != nil) {
618            self.loadingView.hidden = YES;
619        }
620    }
621}
622
623- (void)finishLoading {
624    self.hasShownInitialLoading = YES;
625    if (_loadingView != nil) {
626        self.loadingView.hidden = YES;
627    }
628    [self cacheViewIfNeeded];
629}
630
631- (UIActivityIndicatorView *)activityIndicatorView {
632    if (_activityIndicatorView == nil) {
633        _activityIndicatorView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
634        _activityIndicatorView.center = self.loadingView.center;
635        _activityIndicatorView.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin | UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin;
636        _activityIndicatorView.color = [UIColor colorWithRed:96.f/255.f green:96.f/255.f blue:96.f/255.f alpha:1.f]; // defaults to #606060
637    }
638    [self.loadingView addSubview:_activityIndicatorView];
639    return _activityIndicatorView;
640}
641
642- (UIView *)loadingView {
643    if (_loadingView == nil) {
644        _loadingView = [[UIView alloc] initWithFrame:self.bounds];
645        _loadingView.autoresizingMask = UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight;
646        _loadingView.backgroundColor = [UIColor whiteColor]; // defaults to #FFFFFF
647        [self addSubview:_loadingView];
648        _loadingView.hidden = NO;
649    }
650    return _loadingView;
651}
652
653- (UIImageView *)cacheImageView {
654    if (_cacheImageView == nil) {
655        _cacheImageView = [[UIImageView alloc] initWithFrame:self.bounds];
656        _cacheImageView.contentMode = UIViewContentModeCenter;
657        _cacheImageView.autoresizingMask = UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight;
658        [self addSubview:self.cacheImageView];
659        _cacheImageView.hidden = YES;
660    }
661    return _cacheImageView;
662}
663
664- (void)layoutSubviews {
665    [super layoutSubviews];
666    [self cacheViewIfNeeded];
667    NSUInteger index = [[self subviews] indexOfObjectPassingTest:^BOOL(__kindof UIView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
668        NSString *str = NSStringFromClass([obj class]);
669        return [str containsString:@"MKCompassView"];
670    }];
671    if (index != NSNotFound) {
672        UIView* compassButton;
673        compassButton = [self.subviews objectAtIndex:index];
674        compassButton.frame = CGRectMake(compassButton.frame.origin.x + _compassOffset.x, compassButton.frame.origin.y + _compassOffset.y, compassButton.frame.size.width, compassButton.frame.size.height);
675    }
676}
677
678@end
679