ASMapNode.mm 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430
  1. //
  2. // ASMapNode.mm
  3. // AsyncDisplayKit
  4. //
  5. // Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
  6. // This source code is licensed under the BSD-style license found in the
  7. // LICENSE file in the root directory of this source tree. An additional grant
  8. // of patent rights can be found in the PATENTS file in the same directory.
  9. //
  10. #if TARGET_OS_IOS
  11. #import "ASMapNode.h"
  12. #import <tgmath.h>
  13. #import "ASDisplayNode+FrameworkSubclasses.h"
  14. #import "ASDisplayNodeExtras.h"
  15. #import "ASInsetLayoutSpec.h"
  16. #import "ASInternalHelpers.h"
  17. #import "ASLayout.h"
  18. @interface ASMapNode()
  19. {
  20. MKMapSnapshotter *_snapshotter;
  21. BOOL _snapshotAfterLayout;
  22. NSArray *_annotations;
  23. }
  24. @end
  25. @implementation ASMapNode
  26. @synthesize needsMapReloadOnBoundsChange = _needsMapReloadOnBoundsChange;
  27. @synthesize mapDelegate = _mapDelegate;
  28. @synthesize options = _options;
  29. @synthesize liveMap = _liveMap;
  30. @synthesize showAnnotationsOptions = _showAnnotationsOptions;
  31. #pragma mark - Lifecycle
  32. - (instancetype)init
  33. {
  34. if (!(self = [super init])) {
  35. return nil;
  36. }
  37. self.backgroundColor = ASDisplayNodeDefaultPlaceholderColor();
  38. self.clipsToBounds = YES;
  39. self.userInteractionEnabled = YES;
  40. _needsMapReloadOnBoundsChange = YES;
  41. _liveMap = NO;
  42. _annotations = @[];
  43. _showAnnotationsOptions = ASMapNodeShowAnnotationsOptionsIgnored;
  44. return self;
  45. }
  46. - (void)didLoad
  47. {
  48. [super didLoad];
  49. if (self.isLiveMap) {
  50. [self addLiveMap];
  51. }
  52. }
  53. - (void)dealloc
  54. {
  55. [self destroySnapshotter];
  56. }
  57. - (void)setLayerBacked:(BOOL)layerBacked
  58. {
  59. ASDisplayNodeAssert(!self.isLiveMap, @"ASMapNode can not be layer backed whilst .liveMap = YES, set .liveMap = NO to use layer backing.");
  60. [super setLayerBacked:layerBacked];
  61. }
  62. - (void)didEnterPreloadState
  63. {
  64. [super didEnterPreloadState];
  65. ASPerformBlockOnMainThread(^{
  66. if (self.isLiveMap) {
  67. [self addLiveMap];
  68. } else {
  69. [self takeSnapshot];
  70. }
  71. });
  72. }
  73. - (void)didExitPreloadState
  74. {
  75. [super didExitPreloadState];
  76. ASPerformBlockOnMainThread(^{
  77. if (self.isLiveMap) {
  78. [self removeLiveMap];
  79. }
  80. });
  81. }
  82. #pragma mark - Settings
  83. - (BOOL)isLiveMap
  84. {
  85. ASDN::MutexLocker l(__instanceLock__);
  86. return _liveMap;
  87. }
  88. - (void)setLiveMap:(BOOL)liveMap
  89. {
  90. ASDisplayNodeAssert(!self.isLayerBacked, @"ASMapNode can not use the interactive map feature whilst .isLayerBacked = YES, set .layerBacked = NO to use the interactive map feature.");
  91. ASDN::MutexLocker l(__instanceLock__);
  92. if (liveMap == _liveMap) {
  93. return;
  94. }
  95. _liveMap = liveMap;
  96. if (self.nodeLoaded) {
  97. liveMap ? [self addLiveMap] : [self removeLiveMap];
  98. }
  99. }
  100. - (BOOL)needsMapReloadOnBoundsChange
  101. {
  102. ASDN::MutexLocker l(__instanceLock__);
  103. return _needsMapReloadOnBoundsChange;
  104. }
  105. - (void)setNeedsMapReloadOnBoundsChange:(BOOL)needsMapReloadOnBoundsChange
  106. {
  107. ASDN::MutexLocker l(__instanceLock__);
  108. _needsMapReloadOnBoundsChange = needsMapReloadOnBoundsChange;
  109. }
  110. - (MKMapSnapshotOptions *)options
  111. {
  112. ASDN::MutexLocker l(__instanceLock__);
  113. if (!_options) {
  114. _options = [[MKMapSnapshotOptions alloc] init];
  115. _options.region = MKCoordinateRegionForMapRect(MKMapRectWorld);
  116. CGSize calculatedSize = self.calculatedSize;
  117. if (!CGSizeEqualToSize(calculatedSize, CGSizeZero)) {
  118. _options.size = calculatedSize;
  119. }
  120. }
  121. return _options;
  122. }
  123. - (void)setOptions:(MKMapSnapshotOptions *)options
  124. {
  125. ASDN::MutexLocker l(__instanceLock__);
  126. if (!_options || ![options isEqual:_options]) {
  127. _options = options;
  128. if (self.isLiveMap) {
  129. [self applySnapshotOptions];
  130. } else if (_snapshotter) {
  131. [self destroySnapshotter];
  132. [self takeSnapshot];
  133. }
  134. }
  135. }
  136. - (MKCoordinateRegion)region
  137. {
  138. return self.options.region;
  139. }
  140. - (void)setRegion:(MKCoordinateRegion)region
  141. {
  142. MKMapSnapshotOptions * options = [self.options copy];
  143. options.region = region;
  144. self.options = options;
  145. }
  146. - (void)setMapDelegate:(id<MKMapViewDelegate>)mapDelegate {
  147. _mapDelegate = mapDelegate;
  148. if (_mapView) {
  149. _mapView.delegate = mapDelegate;
  150. }
  151. }
  152. #pragma mark - Snapshotter
  153. - (void)takeSnapshot
  154. {
  155. // If our size is zero, we want to avoid calling a default sized snapshot. Set _snapshotAfterLayout to YES
  156. // so if layout changes in the future, we'll try snapshotting again.
  157. ASLayout *layout = self.calculatedLayout;
  158. if (layout == nil || CGSizeEqualToSize(CGSizeZero, layout.size)) {
  159. _snapshotAfterLayout = YES;
  160. return;
  161. }
  162. _snapshotAfterLayout = NO;
  163. if (!_snapshotter) {
  164. [self setUpSnapshotter];
  165. }
  166. if (_snapshotter.isLoading) {
  167. return;
  168. }
  169. __weak __typeof__(self) weakSelf = self;
  170. [_snapshotter startWithQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
  171. completionHandler:^(MKMapSnapshot *snapshot, NSError *error) {
  172. __typeof__(self) strongSelf = weakSelf;
  173. if (!strongSelf) {
  174. return;
  175. }
  176. if (!error) {
  177. UIImage *image = snapshot.image;
  178. NSArray *annotations = strongSelf.annotations;
  179. if (annotations.count > 0) {
  180. // Only create a graphics context if we have annotations to draw.
  181. // The MKMapSnapshotter is currently not capable of rendering annotations automatically.
  182. CGRect finalImageRect = CGRectMake(0, 0, image.size.width, image.size.height);
  183. UIGraphicsBeginImageContextWithOptions(image.size, YES, image.scale);
  184. [image drawAtPoint:CGPointZero];
  185. UIImage *pinImage;
  186. CGPoint pinCenterOffset = CGPointZero;
  187. // Get a standard annotation view pin if there is no custom annotation block.
  188. if (!strongSelf.imageForStaticMapAnnotationBlock) {
  189. pinImage = [strongSelf.class defaultPinImageWithCenterOffset:&pinCenterOffset];
  190. }
  191. for (id<MKAnnotation> annotation in annotations) {
  192. if (strongSelf.imageForStaticMapAnnotationBlock) {
  193. // Get custom annotation image from custom annotation block.
  194. pinImage = strongSelf.imageForStaticMapAnnotationBlock(annotation, &pinCenterOffset);
  195. if (!pinImage) {
  196. // just for case block returned nil, which can happen
  197. pinImage = [strongSelf.class defaultPinImageWithCenterOffset:&pinCenterOffset];
  198. }
  199. }
  200. CGPoint point = [snapshot pointForCoordinate:annotation.coordinate];
  201. if (CGRectContainsPoint(finalImageRect, point)) {
  202. CGSize pinSize = pinImage.size;
  203. point.x -= pinSize.width / 2.0;
  204. point.y -= pinSize.height / 2.0;
  205. point.x += pinCenterOffset.x;
  206. point.y += pinCenterOffset.y;
  207. [pinImage drawAtPoint:point];
  208. }
  209. }
  210. image = UIGraphicsGetImageFromCurrentImageContext();
  211. UIGraphicsEndImageContext();
  212. }
  213. strongSelf.image = image;
  214. }
  215. }];
  216. }
  217. + (UIImage *)defaultPinImageWithCenterOffset:(CGPoint *)centerOffset
  218. {
  219. static MKAnnotationView *pin;
  220. static dispatch_once_t onceToken;
  221. dispatch_once(&onceToken, ^{
  222. pin = [[MKPinAnnotationView alloc] initWithAnnotation:nil reuseIdentifier:@""];
  223. });
  224. *centerOffset = pin.centerOffset;
  225. return pin.image;
  226. }
  227. - (void)setUpSnapshotter
  228. {
  229. _snapshotter = [[MKMapSnapshotter alloc] initWithOptions:self.options];
  230. }
  231. - (void)destroySnapshotter
  232. {
  233. [_snapshotter cancel];
  234. _snapshotter = nil;
  235. }
  236. - (void)applySnapshotOptions
  237. {
  238. MKMapSnapshotOptions *options = self.options;
  239. [_mapView setCamera:options.camera animated:YES];
  240. [_mapView setRegion:options.region animated:YES];
  241. [_mapView setMapType:options.mapType];
  242. _mapView.showsBuildings = options.showsBuildings;
  243. _mapView.showsPointsOfInterest = options.showsPointsOfInterest;
  244. }
  245. #pragma mark - Actions
  246. - (void)addLiveMap
  247. {
  248. ASDisplayNodeAssertMainThread();
  249. if (!_mapView) {
  250. __weak ASMapNode *weakSelf = self;
  251. _mapView = [[MKMapView alloc] initWithFrame:CGRectZero];
  252. _mapView.delegate = weakSelf.mapDelegate;
  253. [weakSelf applySnapshotOptions];
  254. [_mapView addAnnotations:_annotations];
  255. [weakSelf setNeedsLayout];
  256. [weakSelf.view addSubview:_mapView];
  257. ASMapNodeShowAnnotationsOptions showAnnotationsOptions = self.showAnnotationsOptions;
  258. if (showAnnotationsOptions & ASMapNodeShowAnnotationsOptionsZoomed) {
  259. BOOL const animated = showAnnotationsOptions & ASMapNodeShowAnnotationsOptionsAnimated;
  260. [_mapView showAnnotations:_mapView.annotations animated:animated];
  261. }
  262. }
  263. }
  264. - (void)removeLiveMap
  265. {
  266. [_mapView removeFromSuperview];
  267. _mapView = nil;
  268. }
  269. - (NSArray *)annotations
  270. {
  271. ASDN::MutexLocker l(__instanceLock__);
  272. return _annotations;
  273. }
  274. - (void)setAnnotations:(NSArray *)annotations
  275. {
  276. annotations = [annotations copy] ? : @[];
  277. ASDN::MutexLocker l(__instanceLock__);
  278. _annotations = annotations;
  279. ASMapNodeShowAnnotationsOptions showAnnotationsOptions = self.showAnnotationsOptions;
  280. if (self.isLiveMap) {
  281. [_mapView removeAnnotations:_mapView.annotations];
  282. [_mapView addAnnotations:annotations];
  283. if (showAnnotationsOptions & ASMapNodeShowAnnotationsOptionsZoomed) {
  284. BOOL const animated = showAnnotationsOptions & ASMapNodeShowAnnotationsOptionsAnimated;
  285. [_mapView showAnnotations:_mapView.annotations animated:animated];
  286. }
  287. } else {
  288. if (showAnnotationsOptions & ASMapNodeShowAnnotationsOptionsZoomed) {
  289. self.region = [self regionToFitAnnotations:annotations];
  290. }
  291. else {
  292. [self takeSnapshot];
  293. }
  294. }
  295. }
  296. -(MKCoordinateRegion)regionToFitAnnotations:(NSArray<id<MKAnnotation>> *)annotations
  297. {
  298. if([annotations count] == 0)
  299. return MKCoordinateRegionForMapRect(MKMapRectWorld);
  300. CLLocationCoordinate2D topLeftCoord = CLLocationCoordinate2DMake(-90, 180);
  301. CLLocationCoordinate2D bottomRightCoord = CLLocationCoordinate2DMake(90, -180);
  302. for (id<MKAnnotation> annotation in annotations) {
  303. topLeftCoord = CLLocationCoordinate2DMake(std::fmax(topLeftCoord.latitude, annotation.coordinate.latitude),
  304. std::fmin(topLeftCoord.longitude, annotation.coordinate.longitude));
  305. bottomRightCoord = CLLocationCoordinate2DMake(std::fmin(bottomRightCoord.latitude, annotation.coordinate.latitude),
  306. std::fmax(bottomRightCoord.longitude, annotation.coordinate.longitude));
  307. }
  308. MKCoordinateRegion region = MKCoordinateRegionMake(CLLocationCoordinate2DMake(topLeftCoord.latitude - (topLeftCoord.latitude - bottomRightCoord.latitude) * 0.5,
  309. topLeftCoord.longitude + (bottomRightCoord.longitude - topLeftCoord.longitude) * 0.5),
  310. MKCoordinateSpanMake(std::fabs(topLeftCoord.latitude - bottomRightCoord.latitude) * 2,
  311. std::fabs(bottomRightCoord.longitude - topLeftCoord.longitude) * 2));
  312. return region;
  313. }
  314. -(ASMapNodeShowAnnotationsOptions)showAnnotationsOptions {
  315. ASDN::MutexLocker l(__instanceLock__);
  316. return _showAnnotationsOptions;
  317. }
  318. -(void)setShowAnnotationsOptions:(ASMapNodeShowAnnotationsOptions)showAnnotationsOptions {
  319. ASDN::MutexLocker l(__instanceLock__);
  320. _showAnnotationsOptions = showAnnotationsOptions;
  321. }
  322. #pragma mark - Layout
  323. - (void)setSnapshotSizeWithReloadIfNeeded:(CGSize)snapshotSize
  324. {
  325. if (snapshotSize.height > 0 && snapshotSize.width > 0 && !CGSizeEqualToSize(self.options.size, snapshotSize)) {
  326. _options.size = snapshotSize;
  327. if (_snapshotter) {
  328. [self destroySnapshotter];
  329. [self takeSnapshot];
  330. }
  331. }
  332. }
  333. - (CGSize)calculateSizeThatFits:(CGSize)constrainedSize
  334. {
  335. // FIXME: Need a better way to allow maps to take up the right amount of space in a layout (sizeRange, etc)
  336. // These fallbacks protect against inheriting a constrainedSize that contains a CGFLOAT_MAX value.
  337. if (!ASIsCGSizeValidForLayout(constrainedSize)) {
  338. //ASDisplayNodeAssert(NO, @"Invalid width or height in ASMapNode");
  339. constrainedSize = CGSizeZero;
  340. }
  341. [self setSnapshotSizeWithReloadIfNeeded:constrainedSize];
  342. return constrainedSize;
  343. }
  344. - (void)calculatedLayoutDidChange
  345. {
  346. [super calculatedLayoutDidChange];
  347. if (_snapshotAfterLayout) {
  348. [self takeSnapshot];
  349. }
  350. }
  351. // -layout isn't usually needed over -layoutSpecThatFits, but this way we can avoid a needless node wrapper for MKMapView.
  352. - (void)layout
  353. {
  354. [super layout];
  355. if (self.isLiveMap) {
  356. _mapView.frame = CGRectMake(0.0f, 0.0f, self.calculatedSize.width, self.calculatedSize.height);
  357. } else {
  358. // If our bounds.size is different from our current snapshot size, then let's request a new image from MKMapSnapshotter.
  359. if (_needsMapReloadOnBoundsChange) {
  360. [self setSnapshotSizeWithReloadIfNeeded:self.bounds.size];
  361. // FIXME: Adding a check for Preload here seems to cause intermittent map load failures, but shouldn't.
  362. // if (ASInterfaceStateIncludesPreload(self.interfaceState)) {
  363. }
  364. }
  365. }
  366. @end
  367. #endif