1// 2// AIRGoogleMapMarker.m 3// AirMaps 4// 5// Created by Gil Birman on 9/2/16. 6// 7 8#ifdef HAVE_GOOGLE_MAPS 9 10#import "AIRGoogleMapMarker.h" 11#import <GoogleMaps/GoogleMaps.h> 12#import <React/RCTImageLoaderProtocol.h> 13#import <React/RCTUtils.h> 14#import "AIRGMSMarker.h" 15#import "AIRGoogleMapCallout.h" 16#import "AIRDummyView.h" 17 18CGRect unionRect(CGRect a, CGRect b) { 19 return CGRectMake( 20 MIN(a.origin.x, b.origin.x), 21 MIN(a.origin.y, b.origin.y), 22 MAX(a.size.width, b.size.width), 23 MAX(a.size.height, b.size.height)); 24} 25 26@interface AIRGoogleMapMarker () 27- (id)eventFromMarker:(AIRGMSMarker*)marker; 28@end 29 30@implementation AIRGoogleMapMarker { 31 RCTImageLoaderCancellationBlock _reloadImageCancellationBlock; 32 __weak UIImageView *_iconImageView; 33 UIView *_iconView; 34} 35 36- (instancetype)init 37{ 38 if ((self = [super init])) { 39 _realMarker = [[AIRGMSMarker alloc] init]; 40 _realMarker.fakeMarker = self; 41 _realMarker.tracksViewChanges = true; 42 _realMarker.tracksInfoWindowChanges = false; 43 } 44 return self; 45} 46 47- (void)layoutSubviews { 48 float width = 0; 49 float height = 0; 50 51 for (UIView *v in [_iconView subviews]) { 52 53 float fw = v.frame.origin.x + v.frame.size.width; 54 float fh = v.frame.origin.y + v.frame.size.height; 55 56 width = MAX(fw, width); 57 height = MAX(fh, height); 58 } 59 60 [_iconView setFrame:CGRectMake(0, 0, width, height)]; 61} 62 63- (id)eventFromMarker:(AIRGMSMarker*)marker { 64 65 CLLocationCoordinate2D coordinate = marker.position; 66 CGPoint position = [self.realMarker.map.projection pointForCoordinate:coordinate]; 67 68 return @{ 69 @"id": marker.identifier ?: @"unknown", 70 @"position": @{ 71 @"x": @(position.x), 72 @"y": @(position.y), 73 }, 74 @"coordinate": @{ 75 @"latitude": @(coordinate.latitude), 76 @"longitude": @(coordinate.longitude), 77 } 78 }; 79} 80 81- (void)iconViewInsertSubview:(UIView*)subview atIndex:(NSInteger)atIndex { 82 if (!_realMarker.iconView) { 83 _iconView = [[UIView alloc] init]; 84 _realMarker.iconView = _iconView; 85 } 86 [_iconView insertSubview:subview atIndex:atIndex]; 87} 88 89- (void)insertReactSubview:(id<RCTComponent>)subview atIndex:(NSInteger)atIndex { 90 if ([subview isKindOfClass:[AIRGoogleMapCallout class]]) { 91 self.calloutView = (AIRGoogleMapCallout *)subview; 92 } else { // a child view of the marker 93 [self iconViewInsertSubview:(UIView*)subview atIndex:atIndex+1]; 94 } 95 AIRDummyView *dummySubview = [[AIRDummyView alloc] initWithView:(UIView *)subview]; 96 [super insertReactSubview:(UIView*)dummySubview atIndex:atIndex]; 97} 98 99- (void)removeReactSubview:(id<RCTComponent>)dummySubview { 100 UIView* subview = ((AIRDummyView*)dummySubview).view; 101 102 if ([subview isKindOfClass:[AIRGoogleMapCallout class]]) { 103 self.calloutView = nil; 104 } else { 105 [(UIView*)subview removeFromSuperview]; 106 } 107 [super removeReactSubview:(UIView*)dummySubview]; 108} 109 110- (void)showCalloutView { 111 [_realMarker.map setSelectedMarker:_realMarker]; 112} 113 114- (void)hideCalloutView { 115 [_realMarker.map setSelectedMarker:Nil]; 116} 117 118- (void)redraw { 119 if (!_realMarker.iconView) return; 120 121 BOOL oldValue = _realMarker.tracksViewChanges; 122 123 if (oldValue == YES) 124 { 125 // Immediate refresh, like right now. Not waiting for next frame. 126 UIView *view = _realMarker.iconView; 127 _realMarker.iconView = nil; 128 _realMarker.iconView = view; 129 } 130 else 131 { 132 // Refresh according to docs 133 _realMarker.tracksViewChanges = YES; 134 _realMarker.tracksViewChanges = NO; 135 } 136} 137 138- (UIView *)markerInfoContents { 139 if (self.calloutView && !self.calloutView.tooltip) { 140 return self.calloutView; 141 } 142 return nil; 143} 144 145- (UIView *)markerInfoWindow { 146 if (self.calloutView && self.calloutView.tooltip) { 147 return self.calloutView; 148 } 149 return nil; 150} 151 152- (void)didTapInfoWindowOfMarker:(AIRGMSMarker *)marker point:(CGPoint)point frame:(CGRect)frame { 153 if (self.calloutView && self.calloutView.onPress) { 154 //todo: why not 'callout-press' ? 155 id event = @{ 156 @"action": @"marker-overlay-press", 157 @"id": self.identifier ?: @"unknown", 158 @"point": @{ 159 @"x": @(point.x), 160 @"y": @(point.y), 161 }, 162 @"frame": @{ 163 @"x": @(frame.origin.x), 164 @"y": @(frame.origin.y), 165 @"width": @(frame.size.width), 166 @"height": @(frame.size.height), 167 } 168 }; 169 self.calloutView.onPress(event); 170 } 171} 172 173- (void)didTapInfoWindowOfMarker:(AIRGMSMarker *)marker { 174 [self didTapInfoWindowOfMarker:marker point:CGPointMake(-1, -1) frame:CGRectZero]; 175} 176 177- (void)didTapInfoWindowOfMarker:(AIRGMSMarker *)marker subview:(AIRGoogleMapCalloutSubview*)subview point:(CGPoint)point frame:(CGRect)frame { 178 if (subview && subview.onPress) { 179 //todo: why not 'callout-inside-press' ? 180 id event = @{ 181 @"action": @"marker-inside-overlay-press", 182 @"id": self.identifier ?: @"unknown", 183 @"point": @{ 184 @"x": @(point.x), 185 @"y": @(point.y), 186 }, 187 @"frame": @{ 188 @"x": @(frame.origin.x), 189 @"y": @(frame.origin.y), 190 @"width": @(frame.size.width), 191 @"height": @(frame.size.height), 192 } 193 }; 194 subview.onPress(event); 195 } else { 196 [self didTapInfoWindowOfMarker:marker point:point frame:frame]; 197 } 198} 199 200- (void)didBeginDraggingMarker:(AIRGMSMarker *)marker { 201 if (!self.onDragStart) return; 202 self.onDragStart([self eventFromMarker:marker]); 203} 204 205- (void)didEndDraggingMarker:(AIRGMSMarker *)marker { 206 if (!self.onDragEnd) return; 207 self.onDragEnd([self eventFromMarker:marker]); 208} 209 210- (void)didDragMarker:(AIRGMSMarker *)marker { 211 if (!self.onDrag) return; 212 self.onDrag([self eventFromMarker:marker]); 213} 214 215- (void)setCoordinate:(CLLocationCoordinate2D)coordinate { 216 _realMarker.position = coordinate; 217} 218 219- (CLLocationCoordinate2D)coordinate { 220 return _realMarker.position; 221} 222 223- (void)setRotation:(CLLocationDegrees)rotation { 224 _realMarker.rotation = rotation; 225} 226 227- (CLLocationDegrees)rotation { 228 return _realMarker.rotation; 229} 230 231- (void)setIdentifier:(NSString *)identifier { 232 _realMarker.identifier = identifier; 233} 234 235- (NSString *)identifier { 236 return _realMarker.identifier; 237} 238 239- (void)setOnPress:(RCTBubblingEventBlock)onPress { 240 _realMarker.onPress = onPress; 241} 242 243- (RCTBubblingEventBlock)onPress { 244 return _realMarker.onPress; 245} 246 247- (void)setOpacity:(double)opacity 248{ 249 _realMarker.opacity = opacity; 250} 251 252- (void)setImageSrc:(NSString *)imageSrc 253{ 254 _imageSrc = imageSrc; 255 256 if (_reloadImageCancellationBlock) { 257 _reloadImageCancellationBlock(); 258 _reloadImageCancellationBlock = nil; 259 } 260 261 if (!_imageSrc) { 262 if (_iconImageView) [_iconImageView removeFromSuperview]; 263 return; 264 } 265 266 if (!_iconImageView) { 267 // prevent glitch with marker (cf. https://github.com/react-native-maps/react-native-maps/issues/738) 268 UIImageView *empyImageView = [[UIImageView alloc] init]; 269 _iconImageView = empyImageView; 270 [self iconViewInsertSubview:_iconImageView atIndex:0]; 271 } 272 273 _reloadImageCancellationBlock = [[_bridge moduleForName:@"ImageLoader"] loadImageWithURLRequest:[RCTConvert NSURLRequest:_imageSrc] 274 size:self.bounds.size 275 scale:RCTScreenScale() 276 clipped:YES 277 resizeMode:RCTResizeModeCenter 278 progressBlock:nil 279 partialLoadBlock:nil 280 completionBlock:^(NSError *error, UIImage *image) { 281 if (error) { 282 // TODO(lmr): do something with the error? 283 NSLog(@"%@", error); 284 } 285 dispatch_async(dispatch_get_main_queue(), ^{ 286 287 // TODO(gil): This way allows different image sizes 288 if (self->_iconImageView) [self->_iconImageView removeFromSuperview]; 289 290 // ... but this way is more efficient? 291// if (_iconImageView) { 292// [_iconImageView setImage:image]; 293// return; 294// } 295 296 UIImageView *imageView = [[UIImageView alloc] initWithImage:image]; 297 298 // TODO: w,h or pixel density could be a prop. 299 float density = 1; 300 float w = image.size.width/density; 301 float h = image.size.height/density; 302 CGRect bounds = CGRectMake(0, 0, w, h); 303 304 imageView.contentMode = UIViewContentModeScaleAspectFit; 305 [imageView setFrame:bounds]; 306 307 // NOTE: sizeToFit doesn't work instead. Not sure why. 308 // TODO: Doing it this way is not ideal because it causes things to reshuffle 309 // when the image loads IF the image is larger than the UIView. 310 // Shouldn't required images have size info automatically via RN? 311 CGRect selfBounds = unionRect(bounds, self.bounds); 312 [self setFrame:selfBounds]; 313 314 self->_iconImageView = imageView; 315 [self iconViewInsertSubview:imageView atIndex:0]; 316 }); 317 }]; 318} 319 320- (void)setIconSrc:(NSString *)iconSrc 321{ 322 _iconSrc = iconSrc; 323 324 if (_reloadImageCancellationBlock) { 325 _reloadImageCancellationBlock(); 326 _reloadImageCancellationBlock = nil; 327 } 328 329 if (!_realMarker.icon) { 330 // prevent glitch with marker (cf. https://github.com/react-native-maps/react-native-maps/issues/3657) 331 UIImage *emptyImage = [[UIImage alloc] init]; 332 _realMarker.icon = emptyImage; 333 } 334 335 _reloadImageCancellationBlock = 336 [[_bridge moduleForName:@"ImageLoader"] loadImageWithURLRequest:[RCTConvert NSURLRequest:_iconSrc] 337 size:self.bounds.size 338 scale:RCTScreenScale() 339 clipped:YES 340 resizeMode:RCTResizeModeCenter 341 progressBlock:nil 342 partialLoadBlock:nil 343 completionBlock:^(NSError *error, UIImage *image) { 344 if (error) { 345 // TODO(lmr): do something with the error? 346 NSLog(@"%@", error); 347 } 348 dispatch_async(dispatch_get_main_queue(), ^{ 349 self->_realMarker.icon = image; 350 }); 351 }]; 352} 353 354- (void)setTitle:(NSString *)title { 355 _realMarker.title = [title copy]; 356} 357 358- (NSString *)title { 359 return _realMarker.title; 360} 361 362- (void)setSubtitle:(NSString *)subtitle { 363 _realMarker.snippet = subtitle; 364} 365 366- (NSString *)subtitle { 367 return _realMarker.snippet; 368} 369 370- (void)setPinColor:(UIColor *)pinColor { 371 _pinColor = pinColor; 372 _realMarker.icon = [GMSMarker markerImageWithColor:pinColor]; 373} 374 375- (void)setAnchor:(CGPoint)anchor { 376 _anchor = anchor; 377 _realMarker.groundAnchor = anchor; 378} 379 380- (void)setCalloutAnchor:(CGPoint)calloutAnchor { 381 _calloutAnchor = calloutAnchor; 382 _realMarker.infoWindowAnchor = calloutAnchor; 383} 384 385 386- (void)setZIndex:(NSInteger)zIndex 387{ 388 _zIndex = zIndex; 389 _realMarker.zIndex = (int)zIndex; 390} 391 392- (void)setDraggable:(BOOL)draggable { 393 _realMarker.draggable = draggable; 394} 395 396- (BOOL)draggable { 397 return _realMarker.draggable; 398} 399 400- (void)setTappable:(BOOL)tappable { 401 _realMarker.tappable = tappable; 402} 403 404- (BOOL)tappable { 405 return _realMarker.tappable; 406} 407 408- (void)setFlat:(BOOL)flat { 409 _realMarker.flat = flat; 410} 411 412- (BOOL)flat { 413 return _realMarker.flat; 414} 415 416- (void)setTracksViewChanges:(BOOL)tracksViewChanges { 417 _realMarker.tracksViewChanges = tracksViewChanges; 418} 419 420- (BOOL)tracksViewChanges { 421 return _realMarker.tracksViewChanges; 422} 423 424- (void)setTracksInfoWindowChanges:(BOOL)tracksInfoWindowChanges { 425 _realMarker.tracksInfoWindowChanges = tracksInfoWindowChanges; 426} 427 428- (BOOL)tracksInfoWindowChanges { 429 return _realMarker.tracksInfoWindowChanges; 430} 431 432@end 433 434#endif 435