2019-11-09 23:14:22 +04:00

443 lines
13 KiB
Plaintext

//
// 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 <AsyncDisplayKit/ASMapNode.h>
#if TARGET_OS_IOS && AS_USE_MAPKIT
#import <tgmath.h>
#import <AsyncDisplayKit/ASDisplayNode+Subclasses.h>
#import <AsyncDisplayKit/ASDisplayNodeExtras.h>
#import <AsyncDisplayKit/ASGraphicsContext.h>
#import <AsyncDisplayKit/ASInsetLayoutSpec.h>
#import "Private/ASInternalHelpers.h"
#import <AsyncDisplayKit/ASLayout.h>
#import <AsyncDisplayKit/ASThread.h>
@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<MKMapViewDelegate>)mapDelegate
{
return ASLockedSelf(_mapDelegate);
}
- (void)setMapDelegate:(id<MKMapViewDelegate>)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<MKAnnotation> 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<id<MKAnnotation>> *)annotations
{
if([annotations count] == 0)
return MKCoordinateRegionForMapRect(MKMapRectWorld);
CLLocationCoordinate2D topLeftCoord = CLLocationCoordinate2DMake(-90, 180);
CLLocationCoordinate2D bottomRightCoord = CLLocationCoordinate2DMake(90, -180);
for (id<MKAnnotation> 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