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