// // ASMapNode.mm // Texture // // Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. // Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. // Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 // #import #if TARGET_OS_IOS && AS_USE_MAPKIT #import #import #import #import #import #import "Private/ASInternalHelpers.h" #import #import @interface ASMapNode() { MKMapSnapshotter *_snapshotter; BOOL _snapshotAfterLayout; NSArray *_annotations; } @end @implementation ASMapNode @synthesize needsMapReloadOnBoundsChange = _needsMapReloadOnBoundsChange; @synthesize mapDelegate = _mapDelegate; @synthesize options = _options; @synthesize liveMap = _liveMap; @synthesize showAnnotationsOptions = _showAnnotationsOptions; #pragma mark - Lifecycle - (instancetype)init { if (!(self = [super init])) { return nil; } self.backgroundColor = ASDisplayNodeDefaultPlaceholderColor(); self.clipsToBounds = YES; self.userInteractionEnabled = YES; _needsMapReloadOnBoundsChange = YES; _liveMap = NO; _annotations = @[]; _showAnnotationsOptions = ASMapNodeShowAnnotationsOptionsIgnored; return self; } - (void)didLoad { [super didLoad]; if (self.isLiveMap) { [self addLiveMap]; } } - (void)dealloc { [self destroySnapshotter]; } - (void)setLayerBacked:(BOOL)layerBacked { ASDisplayNodeAssert(!self.isLiveMap, @"ASMapNode can not be layer backed whilst .liveMap = YES, set .liveMap = NO to use layer backing."); [super setLayerBacked:layerBacked]; } - (void)didEnterPreloadState { [super didEnterPreloadState]; ASPerformBlockOnMainThread(^{ if (self.isLiveMap) { [self addLiveMap]; } else { [self takeSnapshot]; } }); } - (void)didExitPreloadState { [super didExitPreloadState]; ASPerformBlockOnMainThread(^{ if (self.isLiveMap) { [self removeLiveMap]; } }); } #pragma mark - Settings - (BOOL)isLiveMap { ASLockScopeSelf(); return _liveMap; } - (void)setLiveMap:(BOOL)liveMap { ASDisplayNodeAssert(!self.isLayerBacked, @"ASMapNode can not use the interactive map feature whilst .isLayerBacked = YES, set .layerBacked = NO to use the interactive map feature."); ASLockScopeSelf(); if (liveMap == _liveMap) { return; } _liveMap = liveMap; if (self.nodeLoaded) { liveMap ? [self addLiveMap] : [self removeLiveMap]; } } - (BOOL)needsMapReloadOnBoundsChange { ASLockScopeSelf(); return _needsMapReloadOnBoundsChange; } - (void)setNeedsMapReloadOnBoundsChange:(BOOL)needsMapReloadOnBoundsChange { ASLockScopeSelf(); _needsMapReloadOnBoundsChange = needsMapReloadOnBoundsChange; } - (MKMapSnapshotOptions *)options { ASLockScopeSelf(); if (!_options) { _options = [[MKMapSnapshotOptions alloc] init]; _options.region = MKCoordinateRegionForMapRect(MKMapRectWorld); CGSize calculatedSize = self.calculatedSize; if (!CGSizeEqualToSize(calculatedSize, CGSizeZero)) { _options.size = calculatedSize; } } return _options; } - (void)setOptions:(MKMapSnapshotOptions *)options { ASLockScopeSelf(); if (!_options || ![options isEqual:_options]) { _options = options; if (self.isLiveMap) { [self applySnapshotOptions]; } else if (_snapshotter) { [self destroySnapshotter]; [self takeSnapshot]; } } } - (MKCoordinateRegion)region { return self.options.region; } - (void)setRegion:(MKCoordinateRegion)region { MKMapSnapshotOptions * options = [self.options copy]; options.region = region; self.options = options; } - (id)mapDelegate { return ASLockedSelf(_mapDelegate); } - (void)setMapDelegate:(id)mapDelegate { ASLockScopeSelf(); _mapDelegate = mapDelegate; if (_mapView) { ASDisplayNodeAssertMainThread(); _mapView.delegate = mapDelegate; } } #pragma mark - Snapshotter - (void)takeSnapshot { // If our size is zero, we want to avoid calling a default sized snapshot. Set _snapshotAfterLayout to YES // so if layout changes in the future, we'll try snapshotting again. ASLayout *layout = self.calculatedLayout; if (layout == nil || CGSizeEqualToSize(CGSizeZero, layout.size)) { _snapshotAfterLayout = YES; return; } _snapshotAfterLayout = NO; if (!_snapshotter) { [self setUpSnapshotter]; } if (_snapshotter.isLoading) { return; } __weak __typeof__(self) weakSelf = self; [_snapshotter startWithQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) completionHandler:^(MKMapSnapshot *snapshot, NSError *error) { __typeof__(self) strongSelf = weakSelf; if (!strongSelf) { return; } if (!error) { UIImage *image = snapshot.image; NSArray *annotations = strongSelf.annotations; if (annotations.count > 0) { // Only create a graphics context if we have annotations to draw. // The MKMapSnapshotter is currently not capable of rendering annotations automatically. CGRect finalImageRect = CGRectMake(0, 0, image.size.width, image.size.height); ASGraphicsBeginImageContextWithOptions(image.size, YES, image.scale); [image drawAtPoint:CGPointZero]; UIImage *pinImage; CGPoint pinCenterOffset = CGPointZero; // Get a standard annotation view pin if there is no custom annotation block. if (!strongSelf.imageForStaticMapAnnotationBlock) { pinImage = [strongSelf.class defaultPinImageWithCenterOffset:&pinCenterOffset]; } for (id annotation in annotations) { if (strongSelf.imageForStaticMapAnnotationBlock) { // Get custom annotation image from custom annotation block. pinImage = strongSelf.imageForStaticMapAnnotationBlock(annotation, &pinCenterOffset); if (!pinImage) { // just for case block returned nil, which can happen pinImage = [strongSelf.class defaultPinImageWithCenterOffset:&pinCenterOffset]; } } CGPoint point = [snapshot pointForCoordinate:annotation.coordinate]; if (CGRectContainsPoint(finalImageRect, point)) { CGSize pinSize = pinImage.size; point.x -= pinSize.width / 2.0; point.y -= pinSize.height / 2.0; point.x += pinCenterOffset.x; point.y += pinCenterOffset.y; [pinImage drawAtPoint:point]; } } image = ASGraphicsGetImageAndEndCurrentContext(); } strongSelf.image = image; } }]; } + (UIImage *)defaultPinImageWithCenterOffset:(CGPoint *)centerOffset NS_RETURNS_RETAINED { static MKAnnotationView *pin; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ pin = [[MKPinAnnotationView alloc] initWithAnnotation:nil reuseIdentifier:@""]; }); *centerOffset = pin.centerOffset; return pin.image; } - (void)setUpSnapshotter { _snapshotter = [[MKMapSnapshotter alloc] initWithOptions:self.options]; } - (void)destroySnapshotter { [_snapshotter cancel]; _snapshotter = nil; } - (void)applySnapshotOptions { MKMapSnapshotOptions *options = self.options; [_mapView setCamera:options.camera animated:YES]; [_mapView setRegion:options.region animated:YES]; [_mapView setMapType:options.mapType]; _mapView.showsBuildings = options.showsBuildings; _mapView.showsPointsOfInterest = options.showsPointsOfInterest; } #pragma mark - Actions - (void)addLiveMap { ASDisplayNodeAssertMainThread(); if (!_mapView) { __weak ASMapNode *weakSelf = self; _mapView = [[MKMapView alloc] initWithFrame:CGRectZero]; _mapView.delegate = weakSelf.mapDelegate; [weakSelf applySnapshotOptions]; [_mapView addAnnotations:_annotations]; [weakSelf setNeedsLayout]; [weakSelf.view addSubview:_mapView]; ASMapNodeShowAnnotationsOptions showAnnotationsOptions = self.showAnnotationsOptions; if (showAnnotationsOptions & ASMapNodeShowAnnotationsOptionsZoomed) { BOOL const animated = showAnnotationsOptions & ASMapNodeShowAnnotationsOptionsAnimated; [_mapView showAnnotations:_mapView.annotations animated:animated]; } } } - (void)removeLiveMap { [_mapView removeFromSuperview]; _mapView = nil; } - (NSArray *)annotations { ASLockScopeSelf(); return _annotations; } - (void)setAnnotations:(NSArray *)annotations { annotations = [annotations copy] ? : @[]; ASLockScopeSelf(); _annotations = annotations; ASMapNodeShowAnnotationsOptions showAnnotationsOptions = self.showAnnotationsOptions; if (self.isLiveMap) { [_mapView removeAnnotations:_mapView.annotations]; [_mapView addAnnotations:annotations]; if (showAnnotationsOptions & ASMapNodeShowAnnotationsOptionsZoomed) { BOOL const animated = showAnnotationsOptions & ASMapNodeShowAnnotationsOptionsAnimated; [_mapView showAnnotations:_mapView.annotations animated:animated]; } } else { if (showAnnotationsOptions & ASMapNodeShowAnnotationsOptionsZoomed) { self.region = [self regionToFitAnnotations:annotations]; } else { [self takeSnapshot]; } } } - (MKCoordinateRegion)regionToFitAnnotations:(NSArray> *)annotations { if([annotations count] == 0) return MKCoordinateRegionForMapRect(MKMapRectWorld); CLLocationCoordinate2D topLeftCoord = CLLocationCoordinate2DMake(-90, 180); CLLocationCoordinate2D bottomRightCoord = CLLocationCoordinate2DMake(90, -180); for (id annotation in annotations) { topLeftCoord = CLLocationCoordinate2DMake(std::fmax(topLeftCoord.latitude, annotation.coordinate.latitude), std::fmin(topLeftCoord.longitude, annotation.coordinate.longitude)); bottomRightCoord = CLLocationCoordinate2DMake(std::fmin(bottomRightCoord.latitude, annotation.coordinate.latitude), std::fmax(bottomRightCoord.longitude, annotation.coordinate.longitude)); } MKCoordinateRegion region = MKCoordinateRegionMake(CLLocationCoordinate2DMake(topLeftCoord.latitude - (topLeftCoord.latitude - bottomRightCoord.latitude) * 0.5, topLeftCoord.longitude + (bottomRightCoord.longitude - topLeftCoord.longitude) * 0.5), MKCoordinateSpanMake(std::fabs(topLeftCoord.latitude - bottomRightCoord.latitude) * 2, std::fabs(bottomRightCoord.longitude - topLeftCoord.longitude) * 2)); return region; } -(ASMapNodeShowAnnotationsOptions)showAnnotationsOptions { return ASLockedSelf(_showAnnotationsOptions); } -(void)setShowAnnotationsOptions:(ASMapNodeShowAnnotationsOptions)showAnnotationsOptions { ASLockScopeSelf(); _showAnnotationsOptions = showAnnotationsOptions; } #pragma mark - Layout - (void)setSnapshotSizeWithReloadIfNeeded:(CGSize)snapshotSize { if (snapshotSize.height > 0 && snapshotSize.width > 0 && !CGSizeEqualToSize(self.options.size, snapshotSize)) { _options.size = snapshotSize; if (_snapshotter) { [self destroySnapshotter]; [self takeSnapshot]; } } } - (CGSize)calculateSizeThatFits:(CGSize)constrainedSize { // FIXME: Need a better way to allow maps to take up the right amount of space in a layout (sizeRange, etc) // These fallbacks protect against inheriting a constrainedSize that contains a CGFLOAT_MAX value. if (!ASIsCGSizeValidForLayout(constrainedSize)) { //ASDisplayNodeAssert(NO, @"Invalid width or height in ASMapNode"); constrainedSize = CGSizeZero; } [self setSnapshotSizeWithReloadIfNeeded:constrainedSize]; return constrainedSize; } - (void)calculatedLayoutDidChange { [super calculatedLayoutDidChange]; if (_snapshotAfterLayout) { [self takeSnapshot]; } } // -layout isn't usually needed over -layoutSpecThatFits, but this way we can avoid a needless node wrapper for MKMapView. - (void)layout { [super layout]; if (self.isLiveMap) { _mapView.frame = CGRectMake(0.0f, 0.0f, self.calculatedSize.width, self.calculatedSize.height); } else { // If our bounds.size is different from our current snapshot size, then let's request a new image from MKMapSnapshotter. if (_needsMapReloadOnBoundsChange) { [self setSnapshotSizeWithReloadIfNeeded:self.bounds.size]; // FIXME: Adding a check for Preload here seems to cause intermittent map load failures, but shouldn't. // if (ASInterfaceStateIncludesPreload(self.interfaceState)) { } } } - (BOOL)supportsLayerBacking { return NO; } @end #endif // TARGET_OS_IOS && AS_USE_MAPKIT