/* Copyright (c) 2014-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. */ #if TARGET_OS_IOS #import "ASMapNode.h" #import #import #import #import #import @interface ASMapNode() { ASDN::RecursiveMutex _propertyLock; MKMapSnapshotter *_snapshotter; NSArray *_annotations; CLLocationCoordinate2D _centerCoordinateOfMap; } @end @implementation ASMapNode @synthesize needsMapReloadOnBoundsChange = _needsMapReloadOnBoundsChange; @synthesize mapDelegate = _mapDelegate; @synthesize options = _options; @synthesize liveMap = _liveMap; #pragma mark - Lifecycle - (instancetype)init { if (!(self = [super init])) { return nil; } self.backgroundColor = ASDisplayNodeDefaultPlaceholderColor(); self.clipsToBounds = YES; _needsMapReloadOnBoundsChange = YES; _liveMap = NO; _centerCoordinateOfMap = kCLLocationCoordinate2DInvalid; return self; } - (void)didLoad { [super didLoad]; if (self.isLiveMap) { self.userInteractionEnabled = YES; [self addLiveMap]; } } - (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)fetchData { [super fetchData]; if (self.isLiveMap) { [self addLiveMap]; } else { [self takeSnapshot]; } } - (void)clearContents { [super clearContents]; if (self.isLiveMap) { [self removeLiveMap]; } } #pragma mark - Settings - (BOOL)isLiveMap { ASDN::MutexLocker l(_propertyLock); 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."); ASDN::MutexLocker l(_propertyLock); if (liveMap == _liveMap) { return; } _liveMap = liveMap; if (self.nodeLoaded) { liveMap ? [self addLiveMap] : [self removeLiveMap]; } } - (BOOL)needsMapReloadOnBoundsChange { ASDN::MutexLocker l(_propertyLock); return _needsMapReloadOnBoundsChange; } - (void)setNeedsMapReloadOnBoundsChange:(BOOL)needsMapReloadOnBoundsChange { ASDN::MutexLocker l(_propertyLock); _needsMapReloadOnBoundsChange = needsMapReloadOnBoundsChange; } - (MKMapSnapshotOptions *)options { ASDN::MutexLocker l(_propertyLock); 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 { ASDN::MutexLocker l(_propertyLock); _options = options; if (self.isLiveMap) { [self applySnapshotOptions]; } else { [self resetSnapshotter]; [self takeSnapshot]; } } - (MKCoordinateRegion)region { return self.options.region; } - (void)setRegion:(MKCoordinateRegion)region { self.options.region = region; } #pragma mark - Snapshotter - (void)takeSnapshot { if (!_snapshotter) { [self setUpSnapshotter]; } [_snapshotter cancel]; [_snapshotter startWithCompletionHandler:^(MKMapSnapshot *snapshot, NSError *error) { if (!error) { UIImage *image = snapshot.image; CGRect finalImageRect = CGRectMake(0, 0, image.size.width, image.size.height); UIGraphicsBeginImageContextWithOptions(image.size, YES, image.scale); [image drawAtPoint:CGPointMake(0, 0)]; if (_annotations.count > 0 ) { // Get a standard annotation view pin. Future implementations should use a custom annotation image property. MKAnnotationView *pin = [[MKPinAnnotationView alloc] initWithAnnotation:nil reuseIdentifier:@""]; UIImage *pinImage = pin.image; for (idannotation in _annotations) { CGPoint point = [snapshot pointForCoordinate:annotation.coordinate]; if (CGRectContainsPoint(finalImageRect, point)) { CGPoint pinCenterOffset = pin.centerOffset; point.x -= pin.bounds.size.width / 2.0; point.y -= pin.bounds.size.height / 2.0; point.x += pinCenterOffset.x; point.y += pinCenterOffset.y; [pinImage drawAtPoint:point]; } } } UIImage *finalImage = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); self.image = finalImage; } }]; } - (void)setUpSnapshotter { ASDisplayNodeAssert(!CGSizeEqualToSize(CGSizeZero, self.calculatedSize), @"self.calculatedSize can not be zero. Make sure that you are setting a preferredFrameSize or wrapping ASMapNode in a ASRatioLayoutSpec or similar."); _snapshotter = [[MKMapSnapshotter alloc] initWithOptions:self.options]; } - (void)resetSnapshotter { // FIXME: The semantics of this method / name would suggest that we cancel + destroy the snapshotter, // but not that we create a new one. We should probably only create the new one in -takeSnapshot or something. [_snapshotter cancel]; _snapshotter = [[MKMapSnapshotter alloc] initWithOptions:self.options]; } - (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]; if (CLLocationCoordinate2DIsValid(_centerCoordinateOfMap)) { [_mapView setCenterCoordinate:_centerCoordinateOfMap]; } } } - (void)removeLiveMap { // FIXME: With MKCoordinateRegion, isn't the center coordinate fully specified? Do we need this? _centerCoordinateOfMap = _mapView.centerCoordinate; [_mapView removeFromSuperview]; _mapView = nil; } - (void)setAnnotations:(NSArray *)annotations { ASDN::MutexLocker l(_propertyLock); _annotations = [annotations copy]; if (self.isLiveMap) { [_mapView removeAnnotations:_mapView.annotations]; [_mapView addAnnotations:annotations]; } else { [self takeSnapshot]; } } #pragma mark - Layout - (void)setSnapshotSizeIfNeeded:(CGSize)snapshotSize { if (!CGSizeEqualToSize(self.options.size, snapshotSize)) { _options.size = snapshotSize; [self resetSnapshotter]; } } - (CGSize)calculateSizeThatFits:(CGSize)constrainedSize { CGSize size = self.preferredFrameSize; if (CGSizeEqualToSize(size, CGSizeZero)) { size = constrainedSize; } [self setSnapshotSizeIfNeeded:size]; return constrainedSize; } // Layout isn't usually needed in the box model, but since we are making use of MKMapView this is preferred. - (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 setSnapshotSizeIfNeeded:self.bounds.size]; // FIXME: Adding a check for FetchData here seems to cause intermittent map load failures, but shouldn't. // if (ASInterfaceStateIncludesFetchData(self.interfaceState)) { [self takeSnapshot]; } } } @end #endif