GT2/GT2-Android/node_modules/react-native-maps/lib/ios/AirMaps/AIRMap.m

493 lines
17 KiB
Objective-C

/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import "AIRMap.h"
#import <React/RCTEventDispatcher.h>
#import <React/UIView+React.h>
#import "AIRMapMarker.h"
#import "AIRMapPolyline.h"
#import "AIRMapPolygon.h"
#import "AIRMapCircle.h"
#import <QuartzCore/QuartzCore.h>
#import "AIRMapUrlTile.h"
#import "AIRMapLocalTile.h"
const CLLocationDegrees AIRMapDefaultSpan = 0.005;
const NSTimeInterval AIRMapRegionChangeObserveInterval = 0.1;
const CGFloat AIRMapZoomBoundBuffer = 0.01;
const NSInteger AIRMapMaxZoomLevel = 20;
@interface MKMapView (UIGestureRecognizer)
// this tells the compiler that MKMapView actually implements this method
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch;
@end
@interface AIRMap ()
@property (nonatomic, strong) UIActivityIndicatorView *activityIndicatorView;
@property (nonatomic, assign) NSNumber *shouldZoomEnabled;
@property (nonatomic, assign) NSNumber *shouldScrollEnabled;
- (void)updateScrollEnabled;
- (void)updateZoomEnabled;
@end
@implementation AIRMap
{
UIView *_legalLabel;
CLLocationManager *_locationManager;
BOOL _initialRegionSet;
// Array to manually track RN subviews
//
// AIRMap implicitly creates subviews that aren't regular RN children
// (SMCalloutView injects an overlay subview), which otherwise confuses RN
// during component re-renders:
// https://github.com/facebook/react-native/blob/v0.16.0/React/Modules/RCTUIManager.m#L657
//
// Implementation based on RCTTextField, another component with indirect children
// https://github.com/facebook/react-native/blob/v0.16.0/Libraries/Text/RCTTextField.m#L20
NSMutableArray<UIView *> *_reactSubviews;
}
- (instancetype)init
{
if ((self = [super init])) {
_hasStartedRendering = NO;
_reactSubviews = [NSMutableArray new];
// Find Apple link label
for (UIView *subview in self.subviews) {
if ([NSStringFromClass(subview.class) isEqualToString:@"MKAttributionLabel"]) {
// This check is super hacky, but the whole premise of moving around
// Apple's internal subviews is super hacky
_legalLabel = subview;
break;
}
}
// 3rd-party callout view for MapKit that has more options than the built-in. It's painstakingly built to
// be identical to the built-in callout view (which has a private API)
self.calloutView = [SMCalloutView platformCalloutView];
self.calloutView.delegate = self;
self.minZoomLevel = 0;
self.maxZoomLevel = AIRMapMaxZoomLevel;
}
return self;
}
- (void)dealloc
{
[_regionChangeObserveTimer invalidate];
}
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-missing-super-calls"
- (void)insertReactSubview:(id<RCTComponent>)subview atIndex:(NSInteger)atIndex {
// Our desired API is to pass up markers/overlays as children to the mapview component.
// This is where we intercept them and do the appropriate underlying mapview action.
if ([subview isKindOfClass:[AIRMapMarker class]]) {
[self addAnnotation:(id <MKAnnotation>) subview];
} else if ([subview isKindOfClass:[AIRMapPolyline class]]) {
((AIRMapPolyline *)subview).map = self;
[self addOverlay:(id<MKOverlay>)subview];
} else if ([subview isKindOfClass:[AIRMapPolygon class]]) {
((AIRMapPolygon *)subview).map = self;
[self addOverlay:(id<MKOverlay>)subview];
} else if ([subview isKindOfClass:[AIRMapCircle class]]) {
[self addOverlay:(id<MKOverlay>)subview];
} else if ([subview isKindOfClass:[AIRMapUrlTile class]]) {
((AIRMapUrlTile *)subview).map = self;
[self addOverlay:(id<MKOverlay>)subview];
} else if ([subview isKindOfClass:[AIRMapLocalTile class]]) {
((AIRMapLocalTile *)subview).map = self;
[self addOverlay:(id<MKOverlay>)subview];
} else {
NSArray<id<RCTComponent>> *childSubviews = [subview reactSubviews];
for (int i = 0; i < childSubviews.count; i++) {
[self insertReactSubview:(UIView *)childSubviews[i] atIndex:atIndex];
}
}
[_reactSubviews insertObject:(UIView *)subview atIndex:(NSUInteger) atIndex];
}
#pragma clang diagnostic pop
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-missing-super-calls"
- (void)removeReactSubview:(id<RCTComponent>)subview {
// similarly, when the children are being removed we have to do the appropriate
// underlying mapview action here.
if ([subview isKindOfClass:[AIRMapMarker class]]) {
[self removeAnnotation:(id<MKAnnotation>)subview];
} else if ([subview isKindOfClass:[AIRMapPolyline class]]) {
[self removeOverlay:(id <MKOverlay>) subview];
} else if ([subview isKindOfClass:[AIRMapPolygon class]]) {
[self removeOverlay:(id <MKOverlay>) subview];
} else if ([subview isKindOfClass:[AIRMapCircle class]]) {
[self removeOverlay:(id <MKOverlay>) subview];
} else if ([subview isKindOfClass:[AIRMapUrlTile class]]) {
[self removeOverlay:(id <MKOverlay>) subview];
} else if ([subview isKindOfClass:[AIRMapLocalTile class]]) {
[self removeOverlay:(id <MKOverlay>) subview];
} else {
NSArray<id<RCTComponent>> *childSubviews = [subview reactSubviews];
for (int i = 0; i < childSubviews.count; i++) {
[self removeReactSubview:(UIView *)childSubviews[i]];
}
}
[_reactSubviews removeObject:(UIView *)subview];
}
#pragma clang diagnostic pop
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-missing-super-calls"
- (NSArray<id<RCTComponent>> *)reactSubviews {
return _reactSubviews;
}
#pragma clang diagnostic pop
#pragma mark Overrides for Callout behavior
// override UIGestureRecognizer's delegate method so we can prevent MKMapView's recognizer from firing
// when we interact with UIControl subclasses inside our callout view.
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
if ([touch.view isDescendantOfView:self.calloutView])
return NO;
else
return [super gestureRecognizer:gestureRecognizer shouldReceiveTouch:touch];
}
// Allow touches to be sent to our calloutview.
// See this for some discussion of why we need to override this: https://github.com/nfarina/calloutview/pull/9
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
UIView *calloutMaybe = [self.calloutView hitTest:[self.calloutView convertPoint:point fromView:self] withEvent:event];
if (calloutMaybe) return calloutMaybe;
return [super hitTest:point withEvent:event];
}
#pragma mark SMCalloutViewDelegate
- (NSTimeInterval)calloutView:(SMCalloutView *)calloutView delayForRepositionWithSize:(CGSize)offset {
// When the callout is being asked to present in a way where it or its target will be partially offscreen, it asks us
// if we'd like to reposition our surface first so the callout is completely visible. Here we scroll the map into view,
// but it takes some math because we have to deal in lon/lat instead of the given offset in pixels.
CLLocationCoordinate2D coordinate = self.region.center;
// where's the center coordinate in terms of our view?
CGPoint center = [self convertCoordinate:coordinate toPointToView:self];
// move it by the requested offset
center.x -= offset.width;
center.y -= offset.height;
// and translate it back into map coordinates
coordinate = [self convertPoint:center toCoordinateFromView:self];
// move the map!
[self setCenterCoordinate:coordinate animated:YES];
// tell the callout to wait for a while while we scroll (we assume the scroll delay for MKMapView matches UIScrollView)
return kSMCalloutViewRepositionDelayForUIScrollView;
}
#pragma mark Accessors
- (void)setShowsUserLocation:(BOOL)showsUserLocation
{
if (self.showsUserLocation != showsUserLocation) {
if (showsUserLocation && !_locationManager) {
_locationManager = [CLLocationManager new];
if ([_locationManager respondsToSelector:@selector(requestWhenInUseAuthorization)]) {
[_locationManager requestWhenInUseAuthorization];
}
}
super.showsUserLocation = showsUserLocation;
}
}
- (void)setFollowsUserLocation:(BOOL)followsUserLocation
{
_followUserLocation = followsUserLocation;
}
- (void)setHandlePanDrag:(BOOL)handleMapDrag {
for (UIGestureRecognizer *recognizer in [self gestureRecognizers]) {
if ([recognizer isKindOfClass:[UIPanGestureRecognizer class]]) {
recognizer.enabled = handleMapDrag;
break;
}
}
}
- (void)setRegion:(MKCoordinateRegion)region animated:(BOOL)animated
{
// If location is invalid, abort
if (!CLLocationCoordinate2DIsValid(region.center)) {
return;
}
// If new span values are nil, use old values instead
if (!region.span.latitudeDelta) {
region.span.latitudeDelta = self.region.span.latitudeDelta;
}
if (!region.span.longitudeDelta) {
region.span.longitudeDelta = self.region.span.longitudeDelta;
}
// Animate/move to new position
[super setRegion:region animated:animated];
}
- (void)setInitialRegion:(MKCoordinateRegion)initialRegion {
if (!_initialRegionSet) {
_initialRegionSet = YES;
[self setRegion:initialRegion animated:NO];
}
}
- (void)setCacheEnabled:(BOOL)cacheEnabled {
_cacheEnabled = cacheEnabled;
if (self.cacheEnabled && self.cacheImageView.image == nil) {
self.loadingView.hidden = NO;
[self.activityIndicatorView startAnimating];
}
else {
if (_loadingView != nil) {
self.loadingView.hidden = YES;
}
}
}
- (void)setLoadingEnabled:(BOOL)loadingEnabled {
_loadingEnabled = loadingEnabled;
if (!self.hasShownInitialLoading) {
self.loadingView.hidden = !self.loadingEnabled;
}
else {
if (_loadingView != nil) {
self.loadingView.hidden = YES;
}
}
}
- (UIColor *)loadingBackgroundColor {
return self.loadingView.backgroundColor;
}
- (void)setLoadingBackgroundColor:(UIColor *)loadingBackgroundColor {
self.loadingView.backgroundColor = loadingBackgroundColor;
}
- (UIColor *)loadingIndicatorColor {
return self.activityIndicatorView.color;
}
- (void)setLoadingIndicatorColor:(UIColor *)loadingIndicatorColor {
self.activityIndicatorView.color = loadingIndicatorColor;
}
// Include properties of MKMapView which are only available on iOS 9+
// and check if their selector is available before calling super method.
- (void)setShowsCompass:(BOOL)showsCompass {
if ([MKMapView instancesRespondToSelector:@selector(setShowsCompass:)]) {
[super setShowsCompass:showsCompass];
}
}
- (BOOL)showsCompass {
if ([MKMapView instancesRespondToSelector:@selector(showsCompass)]) {
return [super showsCompass];
} else {
return NO;
}
}
- (void)setShowsScale:(BOOL)showsScale {
if ([MKMapView instancesRespondToSelector:@selector(setShowsScale:)]) {
[super setShowsScale:showsScale];
}
}
- (BOOL)showsScale {
if ([MKMapView instancesRespondToSelector:@selector(showsScale)]) {
return [super showsScale];
} else {
return NO;
}
}
- (void)setShowsTraffic:(BOOL)showsTraffic {
if ([MKMapView instancesRespondToSelector:@selector(setShowsTraffic:)]) {
[super setShowsTraffic:showsTraffic];
}
}
- (BOOL)showsTraffic {
if ([MKMapView instancesRespondToSelector:@selector(showsTraffic)]) {
return [super showsTraffic];
} else {
return NO;
}
}
- (void)setScrollEnabled:(BOOL)scrollEnabled {
self.shouldScrollEnabled = [NSNumber numberWithBool:scrollEnabled];
[self updateScrollEnabled];
}
- (void)updateScrollEnabled {
if (self.cacheEnabled) {
[super setScrollEnabled:NO];
}
else if (self.shouldScrollEnabled != nil) {
[super setScrollEnabled:[self.shouldScrollEnabled boolValue]];
}
}
- (void)setZoomEnabled:(BOOL)zoomEnabled {
self.shouldZoomEnabled = [NSNumber numberWithBool:zoomEnabled];
[self updateZoomEnabled];
}
- (void)updateZoomEnabled {
if (self.cacheEnabled) {
[super setZoomEnabled: NO];
}
else if (self.shouldZoomEnabled != nil) {
[super setZoomEnabled:[self.shouldZoomEnabled boolValue]];
}
}
- (void)cacheViewIfNeeded {
if (self.hasShownInitialLoading) {
if (!self.cacheEnabled) {
if (_cacheImageView != nil) {
self.cacheImageView.hidden = YES;
self.cacheImageView.image = nil;
}
}
else {
self.cacheImageView.image = nil;
self.cacheImageView.hidden = YES;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.01 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
self.cacheImageView.image = nil;
self.cacheImageView.hidden = YES;
UIGraphicsBeginImageContextWithOptions(self.bounds.size, self.opaque, 0.0);
[self.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
self.cacheImageView.image = image;
self.cacheImageView.hidden = NO;
});
}
[self updateScrollEnabled];
[self updateZoomEnabled];
[self updateLegalLabelInsets];
}
}
- (void)updateLegalLabelInsets {
if (_legalLabel) {
dispatch_async(dispatch_get_main_queue(), ^{
CGRect frame = _legalLabel.frame;
if (_legalLabelInsets.left) {
frame.origin.x = _legalLabelInsets.left;
} else if (_legalLabelInsets.right) {
frame.origin.x = self.frame.size.width - _legalLabelInsets.right - frame.size.width;
}
if (_legalLabelInsets.top) {
frame.origin.y = _legalLabelInsets.top;
} else if (_legalLabelInsets.bottom) {
frame.origin.y = self.frame.size.height - _legalLabelInsets.bottom - frame.size.height;
}
_legalLabel.frame = frame;
});
}
}
- (void)setLegalLabelInsets:(UIEdgeInsets)legalLabelInsets {
_legalLabelInsets = legalLabelInsets;
[self updateLegalLabelInsets];
}
- (void)beginLoading {
if ((!self.hasShownInitialLoading && self.loadingEnabled) || (self.cacheEnabled && self.cacheImageView.image == nil)) {
self.loadingView.hidden = NO;
[self.activityIndicatorView startAnimating];
}
else {
if (_loadingView != nil) {
self.loadingView.hidden = YES;
}
}
}
- (void)finishLoading {
self.hasShownInitialLoading = YES;
if (_loadingView != nil) {
self.loadingView.hidden = YES;
}
[self cacheViewIfNeeded];
}
- (UIActivityIndicatorView *)activityIndicatorView {
if (_activityIndicatorView == nil) {
_activityIndicatorView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
_activityIndicatorView.center = self.loadingView.center;
_activityIndicatorView.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin | UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin;
_activityIndicatorView.color = [UIColor colorWithRed:96.f/255.f green:96.f/255.f blue:96.f/255.f alpha:1.f]; // defaults to #606060
}
[self.loadingView addSubview:_activityIndicatorView];
return _activityIndicatorView;
}
- (UIView *)loadingView {
if (_loadingView == nil) {
_loadingView = [[UIView alloc] initWithFrame:self.bounds];
_loadingView.autoresizingMask = UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight;
_loadingView.backgroundColor = [UIColor whiteColor]; // defaults to #FFFFFF
[self addSubview:_loadingView];
_loadingView.hidden = NO;
}
return _loadingView;
}
- (UIImageView *)cacheImageView {
if (_cacheImageView == nil) {
_cacheImageView = [[UIImageView alloc] initWithFrame:self.bounds];
_cacheImageView.contentMode = UIViewContentModeCenter;
_cacheImageView.autoresizingMask = UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight;
[self addSubview:self.cacheImageView];
_cacheImageView.hidden = YES;
}
return _cacheImageView;
}
- (void)layoutSubviews {
[super layoutSubviews];
[self cacheViewIfNeeded];
}
@end