@lugg/maps 1.0.0-beta.9 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +15 -100
  2. package/android/src/main/java/com/luggmaps/LuggCalloutView.kt +5 -0
  3. package/android/src/main/java/com/luggmaps/LuggMapView.kt +56 -2
  4. package/android/src/main/java/com/luggmaps/LuggMapViewManager.kt +18 -0
  5. package/android/src/main/java/com/luggmaps/LuggMapWrapperView.kt +36 -2
  6. package/android/src/main/java/com/luggmaps/LuggMarkerView.kt +7 -2
  7. package/android/src/main/java/com/luggmaps/LuggPolylineView.kt +2 -1
  8. package/android/src/main/java/com/luggmaps/core/GoogleMapProvider.kt +332 -42
  9. package/android/src/main/java/com/luggmaps/core/MapProviderDelegate.kt +6 -0
  10. package/android/src/main/java/com/luggmaps/core/PolylineAnimator.kt +61 -10
  11. package/ios/LuggMapView.mm +240 -10
  12. package/ios/LuggMapWrapperView.h +5 -0
  13. package/ios/LuggMapWrapperView.mm +18 -0
  14. package/ios/LuggMarkerView.mm +2 -0
  15. package/ios/core/AppleMapProvider.h +8 -0
  16. package/ios/core/AppleMapProvider.mm +63 -47
  17. package/ios/core/AppleStaticMapProvider.h +14 -0
  18. package/ios/core/AppleStaticMapProvider.mm +233 -0
  19. package/ios/core/GMSPolylineAnimator.m +40 -15
  20. package/ios/core/GoogleMapProvider.h +10 -0
  21. package/ios/core/GoogleMapProvider.mm +67 -54
  22. package/ios/core/GoogleStaticMapProvider.h +21 -0
  23. package/ios/core/GoogleStaticMapProvider.mm +217 -0
  24. package/ios/core/MKPolylineAnimator.m +67 -43
  25. package/ios/core/MapEdgeInsets.h +18 -0
  26. package/ios/core/MapProviderDelegate.h +11 -0
  27. package/ios/core/StaticMapProviderBase.h +80 -0
  28. package/ios/core/StaticMapProviderBase.mm +847 -0
  29. package/ios/events/CameraIdleEvent.h +4 -2
  30. package/ios/events/CameraMoveEvent.h +4 -2
  31. package/ios/events/CirclePressEvent.h +4 -2
  32. package/ios/events/GroundOverlayPressEvent.h +4 -2
  33. package/ios/events/LongPressEvent.h +4 -2
  34. package/ios/events/MarkerDragEvent.h +12 -6
  35. package/ios/events/MarkerPressEvent.h +4 -2
  36. package/ios/events/PolygonPressEvent.h +4 -2
  37. package/ios/events/PressEvent.h +4 -2
  38. package/ios/events/ReadyEvent.h +4 -2
  39. package/ios/events/TileOverlayPressEvent.h +4 -2
  40. package/ios/extensions/MKMapView+Zoom.h +20 -0
  41. package/ios/extensions/MKMapView+Zoom.m +28 -18
  42. package/ios/extensions/UIView+Snapshot.h +16 -0
  43. package/ios/extensions/UIView+Snapshot.m +27 -0
  44. package/lib/module/MapView.js +12 -0
  45. package/lib/module/MapView.js.map +1 -1
  46. package/lib/module/MapView.web.js +29 -10
  47. package/lib/module/MapView.web.js.map +1 -1
  48. package/lib/module/fabric/LuggMapViewNativeComponent.ts +10 -1
  49. package/lib/typescript/src/MapView.d.ts +1 -0
  50. package/lib/typescript/src/MapView.d.ts.map +1 -1
  51. package/lib/typescript/src/MapView.types.d.ts +32 -0
  52. package/lib/typescript/src/MapView.types.d.ts.map +1 -1
  53. package/lib/typescript/src/MapView.web.d.ts.map +1 -1
  54. package/lib/typescript/src/fabric/LuggMapViewNativeComponent.d.ts +4 -0
  55. package/lib/typescript/src/fabric/LuggMapViewNativeComponent.d.ts.map +1 -1
  56. package/package.json +24 -14
  57. package/src/MapView.tsx +14 -0
  58. package/src/MapView.types.ts +32 -0
  59. package/src/MapView.web.tsx +39 -14
  60. package/src/fabric/LuggMapViewNativeComponent.ts +10 -1
  61. package/plugin/build/withMapsAndroid.d.ts +0 -5
  62. package/plugin/build/withMapsAndroid.js +0 -15
  63. package/plugin/build/withMapsIOS.d.ts +0 -5
  64. package/plugin/build/withMapsIOS.js +0 -27
@@ -0,0 +1,847 @@
1
+ #import "StaticMapProviderBase.h"
2
+
3
+ using facebook::react::LuggMapViewInsetAdjustment;
4
+ using facebook::react::LuggMapViewMapType;
5
+ using facebook::react::LuggMapViewPoiFilterMode;
6
+ using facebook::react::LuggMapViewTheme;
7
+
8
+ #import "../LuggCircleView.h"
9
+ #import "../LuggGroundOverlayView.h"
10
+ #import "../LuggMapWrapperView.h"
11
+ #import "../LuggMarkerView.h"
12
+ #import "../LuggPolygonView.h"
13
+ #import "../LuggPolylineView.h"
14
+ #import "MapEdgeInsets.h"
15
+
16
+ #pragma mark - Projection
17
+
18
+ CGPoint LuggStaticPointForCoordinate(MKMapRect mapRect, CGSize size,
19
+ CLLocationCoordinate2D coordinate) {
20
+ MKMapPoint point = MKMapPointForCoordinate(coordinate);
21
+ return CGPointMake(
22
+ (point.x - mapRect.origin.x) / mapRect.size.width * size.width,
23
+ (point.y - mapRect.origin.y) / mapRect.size.height * size.height);
24
+ }
25
+
26
+ #pragma mark - Shape overlay
27
+
28
+ // Draws polylines, polygons, circles and ground overlays over the base
29
+ // map with CoreGraphics; the base render is map tiles only
30
+ @interface LuggStaticShapeOverlayView : UIView
31
+ @property(nonatomic, assign) MKMapRect mapRect;
32
+ @property(nonatomic, assign) BOOL projectionReady;
33
+ @property(nonatomic, strong) NSMutableArray<LuggPolylineView *> *polylineViews;
34
+ @property(nonatomic, strong) NSMutableArray<LuggPolygonView *> *polygonViews;
35
+ @property(nonatomic, strong) NSMutableArray<LuggCircleView *> *circleViews;
36
+ @property(nonatomic, strong)
37
+ NSMutableArray<LuggGroundOverlayView *> *groundOverlayViews;
38
+ @property(nonatomic, strong)
39
+ NSMapTable<LuggGroundOverlayView *, UIImage *> *groundOverlayImages;
40
+ @end
41
+
42
+ @implementation LuggStaticShapeOverlayView
43
+
44
+ - (instancetype)initWithFrame:(CGRect)frame {
45
+ if (self = [super initWithFrame:frame]) {
46
+ self.opaque = NO;
47
+ self.backgroundColor = UIColor.clearColor;
48
+ self.userInteractionEnabled = NO;
49
+ self.contentMode = UIViewContentModeRedraw;
50
+ _polylineViews = [NSMutableArray array];
51
+ _polygonViews = [NSMutableArray array];
52
+ _circleViews = [NSMutableArray array];
53
+ _groundOverlayViews = [NSMutableArray array];
54
+ _groundOverlayImages = [NSMapTable weakToStrongObjectsMapTable];
55
+ }
56
+ return self;
57
+ }
58
+
59
+ static NSInteger shapeZIndex(UIView *shape) {
60
+ if ([shape isKindOfClass:[LuggPolylineView class]])
61
+ return ((LuggPolylineView *)shape).zIndex;
62
+ if ([shape isKindOfClass:[LuggPolygonView class]])
63
+ return ((LuggPolygonView *)shape).zIndex;
64
+ if ([shape isKindOfClass:[LuggCircleView class]])
65
+ return ((LuggCircleView *)shape).zIndex;
66
+ if ([shape isKindOfClass:[LuggGroundOverlayView class]])
67
+ return ((LuggGroundOverlayView *)shape).zIndex;
68
+ return 0;
69
+ }
70
+
71
+ - (CGPoint)pointForCoordinate:(CLLocationCoordinate2D)coordinate {
72
+ return LuggStaticPointForCoordinate(_mapRect, self.bounds.size, coordinate);
73
+ }
74
+
75
+ - (void)addPath:(CGContextRef)context
76
+ ring:(NSArray<CLLocation *> *)coordinates {
77
+ for (NSUInteger i = 0; i < coordinates.count; i++) {
78
+ CGPoint point = [self pointForCoordinate:coordinates[i].coordinate];
79
+ if (i == 0) {
80
+ CGContextMoveToPoint(context, point.x, point.y);
81
+ } else {
82
+ CGContextAddLineToPoint(context, point.x, point.y);
83
+ }
84
+ }
85
+ }
86
+
87
+ - (void)drawPolyline:(LuggPolylineView *)polylineView
88
+ inContext:(CGContextRef)context {
89
+ NSArray<CLLocation *> *coordinates = polylineView.coordinates;
90
+ UIColor *color = polylineView.strokeColors.firstObject;
91
+ if (coordinates.count < 2 || !color || polylineView.strokeWidth <= 0)
92
+ return;
93
+
94
+ CGContextSaveGState(context);
95
+ [self addPath:context ring:coordinates];
96
+ CGContextSetLineWidth(context, polylineView.strokeWidth);
97
+ CGContextSetLineCap(context, kCGLineCapRound);
98
+ CGContextSetLineJoin(context, kCGLineJoinRound);
99
+ CGContextSetStrokeColorWithColor(context, color.CGColor);
100
+ CGContextStrokePath(context);
101
+ CGContextRestoreGState(context);
102
+ }
103
+
104
+ - (void)drawPolygon:(LuggPolygonView *)polygonView
105
+ inContext:(CGContextRef)context {
106
+ NSArray<CLLocation *> *coordinates = polygonView.coordinates;
107
+ if (coordinates.count < 3)
108
+ return;
109
+
110
+ CGContextSaveGState(context);
111
+
112
+ if (polygonView.fillColor) {
113
+ [self addPath:context ring:coordinates];
114
+ CGContextClosePath(context);
115
+ for (NSArray<CLLocation *> *hole in polygonView.holes) {
116
+ [self addPath:context ring:hole];
117
+ CGContextClosePath(context);
118
+ }
119
+ CGContextSetFillColorWithColor(context, polygonView.fillColor.CGColor);
120
+ CGContextEOFillPath(context);
121
+ }
122
+
123
+ if (polygonView.strokeColor && polygonView.strokeWidth > 0) {
124
+ [self addPath:context ring:coordinates];
125
+ CGContextClosePath(context);
126
+ for (NSArray<CLLocation *> *hole in polygonView.holes) {
127
+ [self addPath:context ring:hole];
128
+ CGContextClosePath(context);
129
+ }
130
+ CGContextSetLineWidth(context, polygonView.strokeWidth);
131
+ CGContextSetLineJoin(context, kCGLineJoinRound);
132
+ CGContextSetStrokeColorWithColor(context, polygonView.strokeColor.CGColor);
133
+ CGContextStrokePath(context);
134
+ }
135
+
136
+ CGContextRestoreGState(context);
137
+ }
138
+
139
+ - (void)drawCircle:(LuggCircleView *)circleView
140
+ inContext:(CGContextRef)context {
141
+ if (circleView.radius <= 0)
142
+ return;
143
+
144
+ CGPoint center = [self pointForCoordinate:circleView.center];
145
+ double pointsPerMapPoint = self.bounds.size.width / _mapRect.size.width;
146
+ double radius = circleView.radius *
147
+ MKMapPointsPerMeterAtLatitude(circleView.center.latitude) *
148
+ pointsPerMapPoint;
149
+ CGRect ellipse =
150
+ CGRectMake(center.x - radius, center.y - radius, radius * 2, radius * 2);
151
+
152
+ CGContextSaveGState(context);
153
+ if (circleView.fillColor) {
154
+ CGContextSetFillColorWithColor(context, circleView.fillColor.CGColor);
155
+ CGContextFillEllipseInRect(context, ellipse);
156
+ }
157
+ if (circleView.strokeColor && circleView.strokeWidth > 0) {
158
+ CGContextSetLineWidth(context, circleView.strokeWidth);
159
+ CGContextSetStrokeColorWithColor(context, circleView.strokeColor.CGColor);
160
+ CGContextStrokeEllipseInRect(context, ellipse);
161
+ }
162
+ CGContextRestoreGState(context);
163
+ }
164
+
165
+ - (void)drawGroundOverlay:(LuggGroundOverlayView *)groundOverlayView {
166
+ UIImage *image = [_groundOverlayImages objectForKey:groundOverlayView];
167
+ if (!image)
168
+ return;
169
+
170
+ CGPoint topLeft =
171
+ [self pointForCoordinate:CLLocationCoordinate2DMake(
172
+ groundOverlayView.northeast.latitude,
173
+ groundOverlayView.southwest.longitude)];
174
+ CGPoint bottomRight =
175
+ [self pointForCoordinate:CLLocationCoordinate2DMake(
176
+ groundOverlayView.southwest.latitude,
177
+ groundOverlayView.northeast.longitude)];
178
+ CGRect rect = CGRectMake(topLeft.x, topLeft.y, bottomRight.x - topLeft.x,
179
+ bottomRight.y - topLeft.y);
180
+ [image drawInRect:rect
181
+ blendMode:kCGBlendModeNormal
182
+ alpha:groundOverlayView.opacity];
183
+ }
184
+
185
+ - (void)drawRect:(CGRect)rect {
186
+ if (!_projectionReady)
187
+ return;
188
+
189
+ CGContextRef context = UIGraphicsGetCurrentContext();
190
+ if (!context)
191
+ return;
192
+
193
+ NSMutableArray<UIView *> *shapes = [NSMutableArray array];
194
+ [shapes addObjectsFromArray:_groundOverlayViews];
195
+ [shapes addObjectsFromArray:_polygonViews];
196
+ [shapes addObjectsFromArray:_circleViews];
197
+ [shapes addObjectsFromArray:_polylineViews];
198
+ NSArray<UIView *> *ordered =
199
+ [shapes sortedArrayWithOptions:NSSortStable
200
+ usingComparator:^NSComparisonResult(UIView *a, UIView *b) {
201
+ NSInteger za = shapeZIndex(a), zb = shapeZIndex(b);
202
+ if (za < zb)
203
+ return NSOrderedAscending;
204
+ if (za > zb)
205
+ return NSOrderedDescending;
206
+ return NSOrderedSame;
207
+ }];
208
+
209
+ for (UIView *shape in ordered) {
210
+ if ([shape isKindOfClass:[LuggGroundOverlayView class]]) {
211
+ [self drawGroundOverlay:(LuggGroundOverlayView *)shape];
212
+ } else if ([shape isKindOfClass:[LuggPolygonView class]]) {
213
+ [self drawPolygon:(LuggPolygonView *)shape inContext:context];
214
+ } else if ([shape isKindOfClass:[LuggCircleView class]]) {
215
+ [self drawCircle:(LuggCircleView *)shape inContext:context];
216
+ } else if ([shape isKindOfClass:[LuggPolylineView class]]) {
217
+ [self drawPolyline:(LuggPolylineView *)shape inContext:context];
218
+ }
219
+ }
220
+ }
221
+
222
+ @end
223
+
224
+ #pragma mark - Provider base
225
+
226
+ @interface StaticMapProviderBase () <
227
+ LuggMarkerViewDelegate, LuggPolylineViewDelegate, LuggPolygonViewDelegate,
228
+ LuggCircleViewDelegate, LuggGroundOverlayViewDelegate>
229
+ @end
230
+
231
+ @implementation StaticMapProviderBase {
232
+ BOOL _isMapReady;
233
+ BOOL _baseRenderDone;
234
+ BOOL _overlaysRevealed;
235
+
236
+ UIImageView *_baseImageView;
237
+ LuggStaticShapeOverlayView *_shapeOverlayView;
238
+ NSMutableArray<LuggMarkerView *> *_markerViews;
239
+ NSMapTable<LuggMarkerView *, UIView *> *_defaultMarkerOverlays;
240
+ }
241
+
242
+ @synthesize delegate = _delegate;
243
+ @synthesize staticMode = _staticMode;
244
+
245
+ - (instancetype)init {
246
+ if (self = [super init]) {
247
+ _markerViews = [NSMutableArray array];
248
+ _defaultMarkerOverlays = [NSMapTable weakToStrongObjectsMapTable];
249
+ _mapType = LuggMapViewMapType::Standard;
250
+ _theme = LuggMapViewTheme::System;
251
+ }
252
+ return self;
253
+ }
254
+
255
+ - (BOOL)isMapReady {
256
+ return _isMapReady;
257
+ }
258
+
259
+ #pragma mark - MapProvider
260
+
261
+ - (void)initializeMapInView:(UIView *)wrapperView
262
+ initialCoordinate:(CLLocationCoordinate2D)coordinate
263
+ initialZoom:(double)zoom {
264
+ if (_wrapperView)
265
+ return;
266
+
267
+ _wrapperView = wrapperView;
268
+ _coordinate = coordinate;
269
+ _zoom = zoom;
270
+
271
+ _shapeOverlayView =
272
+ [[LuggStaticShapeOverlayView alloc] initWithFrame:wrapperView.bounds];
273
+ _shapeOverlayView.autoresizingMask =
274
+ UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
275
+ // Overlays stay hidden over the placeholder and reveal with the base map
276
+ _shapeOverlayView.hidden = YES;
277
+ [wrapperView addSubview:_shapeOverlayView];
278
+
279
+ [self updateProjection];
280
+
281
+ __weak StaticMapProviderBase *weakSelf = self;
282
+ if ([wrapperView isKindOfClass:[LuggMapWrapperView class]]) {
283
+ ((LuggMapWrapperView *)wrapperView).layoutHandler = ^{
284
+ [weakSelf wrapperDidLayout];
285
+ };
286
+ }
287
+
288
+ _isMapReady = YES;
289
+
290
+ // Start after the current mount pass so map settings props applied right
291
+ // after this call shape the base render
292
+ dispatch_async(dispatch_get_main_queue(), ^{
293
+ [weakSelf renderBaseMapIfNeeded];
294
+ });
295
+ }
296
+
297
+ - (void)destroy {
298
+ [self cancelBaseRender];
299
+
300
+ if ([_wrapperView isKindOfClass:[LuggMapWrapperView class]]) {
301
+ ((LuggMapWrapperView *)_wrapperView).layoutHandler = nil;
302
+ }
303
+
304
+ for (LuggMarkerView *markerView in _markerViews) {
305
+ markerView.delegate = nil;
306
+ UIView *iconView = markerView.iconView;
307
+ if (iconView.superview == _wrapperView) {
308
+ [iconView removeFromSuperview];
309
+ }
310
+ [markerView resetIconViewTransform];
311
+ }
312
+ [_markerViews removeAllObjects];
313
+ for (UIView *pin in _defaultMarkerOverlays.objectEnumerator) {
314
+ [pin removeFromSuperview];
315
+ }
316
+ [_defaultMarkerOverlays removeAllObjects];
317
+
318
+ for (LuggPolylineView *view in _shapeOverlayView.polylineViews) {
319
+ view.delegate = nil;
320
+ }
321
+ for (LuggPolygonView *view in _shapeOverlayView.polygonViews) {
322
+ view.delegate = nil;
323
+ }
324
+ for (LuggCircleView *view in _shapeOverlayView.circleViews) {
325
+ view.delegate = nil;
326
+ }
327
+ for (LuggGroundOverlayView *view in _shapeOverlayView.groundOverlayViews) {
328
+ view.delegate = nil;
329
+ }
330
+ [_shapeOverlayView removeFromSuperview];
331
+ _shapeOverlayView = nil;
332
+
333
+ [_baseImageView removeFromSuperview];
334
+ _baseImageView = nil;
335
+
336
+ _baseRenderDone = NO;
337
+ _overlaysRevealed = NO;
338
+ _projectionReady = NO;
339
+ _wrapperView = nil;
340
+ _isMapReady = NO;
341
+ }
342
+
343
+ #pragma mark - Base render
344
+
345
+ - (void)updateProjection {
346
+ CGSize size = _wrapperView.bounds.size;
347
+ if (size.width <= 0 || size.height <= 0)
348
+ return;
349
+
350
+ _projectedSize = size;
351
+ _mapRect = [self mapRectForSize:size];
352
+
353
+ // Edge insets shift the visible center like a live map: the coordinate
354
+ // lands at the center of the inset viewport instead of the view center
355
+ CGPoint offset = LuggMapInsetOffset(_edgeInsets);
356
+ _mapRect.origin.x -= offset.x * _mapRect.size.width / size.width;
357
+ _mapRect.origin.y -= offset.y * _mapRect.size.height / size.height;
358
+
359
+ _projectionReady = YES;
360
+
361
+ _shapeOverlayView.mapRect = _mapRect;
362
+ _shapeOverlayView.projectionReady = YES;
363
+ [_shapeOverlayView setNeedsDisplay];
364
+
365
+ for (LuggMarkerView *markerView in _markerViews) {
366
+ [self positionMarkerOverlay:markerView];
367
+ }
368
+ }
369
+
370
+ // A static map can't autoresize like a live map; when the wrapper gets its
371
+ // first real size (or resizes later) recompute the projection and
372
+ // (re)render the base map at the new size
373
+ - (void)wrapperDidLayout {
374
+ CGSize size = _wrapperView.bounds.size;
375
+ if (size.width <= 0 || size.height <= 0 ||
376
+ CGSizeEqualToSize(size, _projectedSize))
377
+ return;
378
+
379
+ [self updateProjection];
380
+
381
+ // Any in-flight or completed base render targets the old size
382
+ [self cancelBaseRender];
383
+ _baseRenderDone = NO;
384
+ self.cachedBaseImage = nil;
385
+
386
+ [self renderBaseMapIfNeeded];
387
+ }
388
+
389
+ // Camera commands invalidate any cached, displayed or in-flight base
390
+ // render and re-render at the new camera
391
+ - (void)rerenderBaseMap {
392
+ [self cancelBaseRender];
393
+ _baseRenderDone = NO;
394
+ self.cachedBaseImage = nil;
395
+
396
+ // Without a layout yet, the first layout renders with the new camera
397
+ if (!_projectionReady)
398
+ return;
399
+
400
+ [self updateProjection];
401
+
402
+ __weak StaticMapProviderBase *weakSelf = self;
403
+ dispatch_async(dispatch_get_main_queue(), ^{
404
+ [weakSelf renderBaseMapIfNeeded];
405
+ });
406
+ }
407
+
408
+ - (void)renderBaseMapIfNeeded {
409
+ if (_baseRenderDone || !_projectionReady || !_wrapperView)
410
+ return;
411
+
412
+ if (self.cachedBaseImage) {
413
+ [self displayBaseImage:self.cachedBaseImage fromCache:YES];
414
+ return;
415
+ }
416
+
417
+ [self renderBaseMap];
418
+ }
419
+
420
+ - (void)displayBaseImage:(UIImage *)image fromCache:(BOOL)fromCache {
421
+ if (!_baseImageView) {
422
+ _baseImageView = [[UIImageView alloc] initWithFrame:_wrapperView.bounds];
423
+ _baseImageView.autoresizingMask =
424
+ UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
425
+ [_wrapperView insertSubview:_baseImageView atIndex:0];
426
+ }
427
+ _baseImageView.image = image;
428
+ _baseRenderDone = YES;
429
+ [self revealOverlays];
430
+
431
+ if (!fromCache) {
432
+ [_delegate mapProviderDidCaptureStaticImage:image];
433
+ }
434
+ [_delegate mapProviderDidFinishStaticSnapshot];
435
+ [_delegate mapProviderDidReady];
436
+ }
437
+
438
+ - (void)revealOverlays {
439
+ if (_overlaysRevealed)
440
+ return;
441
+ _overlaysRevealed = YES;
442
+ _shapeOverlayView.hidden = NO;
443
+ for (LuggMarkerView *markerView in _markerViews) {
444
+ [self positionMarkerOverlay:markerView];
445
+ }
446
+ }
447
+
448
+ #pragma mark - Subclass hooks
449
+
450
+ - (MKMapRect)mapRectForSize:(CGSize)size {
451
+ return MKMapRectNull;
452
+ }
453
+
454
+ - (UIView *)newDefaultMarkerOverlayView {
455
+ return [[UIView alloc] init];
456
+ }
457
+
458
+ - (double)zoomForMapPointsPerPoint:(double)mapPointsPerPoint {
459
+ return _zoom;
460
+ }
461
+
462
+ - (void)renderBaseMap {
463
+ }
464
+
465
+ - (void)cancelBaseRender {
466
+ }
467
+
468
+ #pragma mark - Props
469
+
470
+ - (void)setMapType:(LuggMapViewMapType)mapType {
471
+ _mapType = mapType;
472
+ }
473
+
474
+ - (void)setTheme:(LuggMapViewTheme)theme {
475
+ _theme = theme;
476
+ }
477
+
478
+ // Interaction and camera props don't apply to a static map
479
+ - (void)setInsetAdjustment:(LuggMapViewInsetAdjustment)insetAdjustment {
480
+ }
481
+ - (void)setZoomEnabled:(BOOL)enabled {
482
+ }
483
+ - (void)setScrollEnabled:(BOOL)enabled {
484
+ }
485
+ - (void)setRotateEnabled:(BOOL)enabled {
486
+ }
487
+ - (void)setPitchEnabled:(BOOL)enabled {
488
+ }
489
+ - (void)setCompassEnabled:(BOOL)enabled {
490
+ }
491
+ - (void)setUserLocationEnabled:(BOOL)enabled {
492
+ }
493
+ - (void)setMinZoom:(double)minZoom {
494
+ }
495
+ - (void)setMaxZoom:(double)maxZoom {
496
+ }
497
+ - (void)setPoiEnabled:(BOOL)enabled {
498
+ }
499
+ - (void)setPoiFilterMode:(LuggMapViewPoiFilterMode)mode {
500
+ }
501
+ - (void)setPoiFilterCategories:(NSArray<NSString *> *)categories {
502
+ }
503
+ - (void)setEdgeInsets:(UIEdgeInsets)edgeInsets
504
+ oldEdgeInsets:(UIEdgeInsets)oldEdgeInsets {
505
+ if (UIEdgeInsetsEqualToEdgeInsets(_edgeInsets, edgeInsets))
506
+ return;
507
+ _edgeInsets = edgeInsets;
508
+
509
+ if (!_projectionReady)
510
+ return;
511
+
512
+ [self updateProjection];
513
+
514
+ // Any in-flight or completed base render targets the old projection. The
515
+ // initial prop set lands before the first render, so the cached image
516
+ // (keyed on insets) stays valid.
517
+ [self cancelBaseRender];
518
+ if (_baseRenderDone) {
519
+ _baseRenderDone = NO;
520
+ self.cachedBaseImage = nil;
521
+ }
522
+ __weak StaticMapProviderBase *weakSelf = self;
523
+ dispatch_async(dispatch_get_main_queue(), ^{
524
+ [weakSelf renderBaseMapIfNeeded];
525
+ });
526
+ }
527
+
528
+ // Static maps don't animate; apply the final insets directly
529
+ - (void)setEdgeInsets:(UIEdgeInsets)edgeInsets
530
+ oldEdgeInsets:(UIEdgeInsets)oldEdgeInsets
531
+ duration:(double)duration {
532
+ [self setEdgeInsets:edgeInsets oldEdgeInsets:oldEdgeInsets];
533
+ }
534
+
535
+ #pragma mark - Markers
536
+
537
+ // Markers are live views over the base map (like Android lite mode's live
538
+ // markers), so async content such as remote images shows up when it loads
539
+
540
+ - (void)addMarkerView:(LuggMarkerView *)markerView {
541
+ markerView.delegate = self;
542
+ if (![_markerViews containsObject:markerView]) {
543
+ [_markerViews addObject:markerView];
544
+ }
545
+ [self syncMarkerView:markerView];
546
+ }
547
+
548
+ - (void)removeMarkerView:(LuggMarkerView *)markerView {
549
+ markerView.delegate = nil;
550
+ [_markerViews removeObject:markerView];
551
+
552
+ UIView *pin = [_defaultMarkerOverlays objectForKey:markerView];
553
+ if (pin) {
554
+ [pin removeFromSuperview];
555
+ [_defaultMarkerOverlays removeObjectForKey:markerView];
556
+ }
557
+
558
+ UIView *iconView = markerView.iconView;
559
+ if (iconView.superview == _wrapperView) {
560
+ [iconView removeFromSuperview];
561
+ }
562
+ [markerView resetIconViewTransform];
563
+ }
564
+
565
+ - (void)syncMarkerView:(LuggMarkerView *)markerView {
566
+ if (!_wrapperView)
567
+ return;
568
+
569
+ UIView *pin = [_defaultMarkerOverlays objectForKey:markerView];
570
+
571
+ if (markerView.hasCustomView) {
572
+ if (pin) {
573
+ [pin removeFromSuperview];
574
+ [_defaultMarkerOverlays removeObjectForKey:markerView];
575
+ }
576
+ UIView *iconView = markerView.iconView;
577
+ if (iconView.superview != _wrapperView) {
578
+ [iconView removeFromSuperview];
579
+ iconView.userInteractionEnabled = NO;
580
+ // Hidden until the base map displays; positionMarkerOverlay shows it
581
+ iconView.hidden = YES;
582
+ [_wrapperView addSubview:iconView];
583
+ }
584
+ } else {
585
+ UIView *iconView = markerView.iconView;
586
+ if (iconView.superview == _wrapperView) {
587
+ [iconView removeFromSuperview];
588
+ [markerView resetIconViewTransform];
589
+ }
590
+ if (!pin) {
591
+ pin = [self newDefaultMarkerOverlayView];
592
+ pin.userInteractionEnabled = NO;
593
+ pin.hidden = YES;
594
+ [_wrapperView addSubview:pin];
595
+ [_defaultMarkerOverlays setObject:pin forKey:markerView];
596
+ }
597
+ }
598
+
599
+ [self positionMarkerOverlay:markerView];
600
+ }
601
+
602
+ - (void)positionMarkerOverlay:(LuggMarkerView *)markerView {
603
+ if (!_projectionReady)
604
+ return;
605
+
606
+ CGPoint point = LuggStaticPointForCoordinate(_mapRect, _projectedSize,
607
+ markerView.coordinate);
608
+
609
+ if (markerView.hasCustomView) {
610
+ UIView *iconView = markerView.iconView;
611
+ CGSize size = iconView.bounds.size;
612
+ if (size.width <= 0 || size.height <= 0) {
613
+ // Not laid out yet; markerViewDidLayout re-runs this
614
+ return;
615
+ }
616
+ CGFloat scale = markerView.scale;
617
+ CGFloat radians = markerView.rotate * M_PI / 180.0;
618
+ iconView.layer.anchorPoint = markerView.anchor;
619
+ iconView.transform =
620
+ CGAffineTransformConcat(CGAffineTransformMakeScale(scale, scale),
621
+ CGAffineTransformMakeRotation(radians));
622
+ // center positions the layer's anchorPoint, so the marker's anchor
623
+ // lands exactly on the projected coordinate
624
+ iconView.center = point;
625
+ iconView.layer.zPosition = markerView.zIndex;
626
+ iconView.hidden = !_overlaysRevealed;
627
+ } else {
628
+ UIView *pin = [_defaultMarkerOverlays objectForKey:markerView];
629
+ pin.layer.anchorPoint = CGPointMake(0.5, 1.0);
630
+ pin.center = point;
631
+ pin.layer.zPosition = markerView.zIndex;
632
+ pin.hidden = !_overlaysRevealed;
633
+ }
634
+ }
635
+
636
+ #pragma mark - LuggMarkerViewDelegate
637
+
638
+ - (void)markerViewDidLayout:(LuggMarkerView *)markerView {
639
+ [self syncMarkerView:markerView];
640
+ }
641
+
642
+ - (void)markerViewDidUpdate:(LuggMarkerView *)markerView {
643
+ [self syncMarkerView:markerView];
644
+ }
645
+
646
+ #pragma mark - Shapes
647
+
648
+ - (void)addPolylineView:(LuggPolylineView *)polylineView {
649
+ polylineView.delegate = self;
650
+ if (![_shapeOverlayView.polylineViews containsObject:polylineView]) {
651
+ [_shapeOverlayView.polylineViews addObject:polylineView];
652
+ }
653
+ [_shapeOverlayView setNeedsDisplay];
654
+ }
655
+
656
+ - (void)removePolylineView:(LuggPolylineView *)polylineView {
657
+ polylineView.delegate = nil;
658
+ [_shapeOverlayView.polylineViews removeObject:polylineView];
659
+ [_shapeOverlayView setNeedsDisplay];
660
+ }
661
+
662
+ - (void)syncPolylineView:(LuggPolylineView *)polylineView {
663
+ [_shapeOverlayView setNeedsDisplay];
664
+ }
665
+
666
+ - (void)polylineViewDidUpdate:(LuggPolylineView *)polylineView {
667
+ [_shapeOverlayView setNeedsDisplay];
668
+ }
669
+
670
+ - (void)addPolygonView:(LuggPolygonView *)polygonView {
671
+ polygonView.delegate = self;
672
+ if (![_shapeOverlayView.polygonViews containsObject:polygonView]) {
673
+ [_shapeOverlayView.polygonViews addObject:polygonView];
674
+ }
675
+ [_shapeOverlayView setNeedsDisplay];
676
+ }
677
+
678
+ - (void)removePolygonView:(LuggPolygonView *)polygonView {
679
+ polygonView.delegate = nil;
680
+ [_shapeOverlayView.polygonViews removeObject:polygonView];
681
+ [_shapeOverlayView setNeedsDisplay];
682
+ }
683
+
684
+ - (void)syncPolygonView:(LuggPolygonView *)polygonView {
685
+ [_shapeOverlayView setNeedsDisplay];
686
+ }
687
+
688
+ - (void)polygonViewDidUpdate:(LuggPolygonView *)polygonView {
689
+ [_shapeOverlayView setNeedsDisplay];
690
+ }
691
+
692
+ - (void)addCircleView:(LuggCircleView *)circleView {
693
+ circleView.delegate = self;
694
+ if (![_shapeOverlayView.circleViews containsObject:circleView]) {
695
+ [_shapeOverlayView.circleViews addObject:circleView];
696
+ }
697
+ [_shapeOverlayView setNeedsDisplay];
698
+ }
699
+
700
+ - (void)removeCircleView:(LuggCircleView *)circleView {
701
+ circleView.delegate = nil;
702
+ [_shapeOverlayView.circleViews removeObject:circleView];
703
+ [_shapeOverlayView setNeedsDisplay];
704
+ }
705
+
706
+ - (void)syncCircleView:(LuggCircleView *)circleView {
707
+ [_shapeOverlayView setNeedsDisplay];
708
+ }
709
+
710
+ - (void)circleViewDidUpdate:(LuggCircleView *)circleView {
711
+ [_shapeOverlayView setNeedsDisplay];
712
+ }
713
+
714
+ - (void)addGroundOverlayView:(LuggGroundOverlayView *)groundOverlayView {
715
+ groundOverlayView.delegate = self;
716
+ if (![_shapeOverlayView.groundOverlayViews
717
+ containsObject:groundOverlayView]) {
718
+ [_shapeOverlayView.groundOverlayViews addObject:groundOverlayView];
719
+ }
720
+ [self loadGroundOverlayImage:groundOverlayView];
721
+ }
722
+
723
+ - (void)removeGroundOverlayView:(LuggGroundOverlayView *)groundOverlayView {
724
+ groundOverlayView.delegate = nil;
725
+ [_shapeOverlayView.groundOverlayViews removeObject:groundOverlayView];
726
+ [_shapeOverlayView.groundOverlayImages removeObjectForKey:groundOverlayView];
727
+ [_shapeOverlayView setNeedsDisplay];
728
+ }
729
+
730
+ - (void)syncGroundOverlayView:(LuggGroundOverlayView *)groundOverlayView {
731
+ [self loadGroundOverlayImage:groundOverlayView];
732
+ }
733
+
734
+ - (void)groundOverlayViewDidUpdate:(LuggGroundOverlayView *)groundOverlayView {
735
+ [self loadGroundOverlayImage:groundOverlayView];
736
+ }
737
+
738
+ - (void)loadGroundOverlayImage:(LuggGroundOverlayView *)groundOverlayView {
739
+ NSURL *url = [NSURL URLWithString:groundOverlayView.imageUri];
740
+ if (!url)
741
+ return;
742
+
743
+ __weak StaticMapProviderBase *weakSelf = self;
744
+ __weak LuggGroundOverlayView *weakOverlayView = groundOverlayView;
745
+ dispatch_async(
746
+ dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
747
+ NSData *data = [NSData dataWithContentsOfURL:url];
748
+ UIImage *image = data ? [UIImage imageWithData:data] : nil;
749
+ dispatch_async(dispatch_get_main_queue(), ^{
750
+ StaticMapProviderBase *strongSelf = weakSelf;
751
+ LuggGroundOverlayView *overlayView = weakOverlayView;
752
+ if (!strongSelf || !overlayView || !image)
753
+ return;
754
+ LuggStaticShapeOverlayView *shapeView = strongSelf->_shapeOverlayView;
755
+ if (![shapeView.groundOverlayViews containsObject:overlayView])
756
+ return;
757
+ [shapeView.groundOverlayImages setObject:image forKey:overlayView];
758
+ [shapeView setNeedsDisplay];
759
+ });
760
+ });
761
+ }
762
+
763
+ // Tile overlays need a live map to fetch and compose tiles; unsupported on
764
+ // static maps
765
+ - (void)addTileOverlayView:(LuggTileOverlayView *)tileOverlayView {
766
+ }
767
+ - (void)removeTileOverlayView:(LuggTileOverlayView *)tileOverlayView {
768
+ }
769
+ - (void)syncTileOverlayView:(LuggTileOverlayView *)tileOverlayView {
770
+ }
771
+
772
+ #pragma mark - Lifecycle
773
+
774
+ - (void)pauseAnimations {
775
+ }
776
+
777
+ - (void)resumeAnimations {
778
+ // Retry a failed or cancelled base render (e.g. device back online)
779
+ [self renderBaseMapIfNeeded];
780
+ }
781
+
782
+ #pragma mark - Commands
783
+
784
+ // Static maps don't animate; commands re-render once at the final camera
785
+
786
+ - (void)moveCamera:(double)latitude
787
+ longitude:(double)longitude
788
+ zoom:(double)zoom
789
+ duration:(double)duration {
790
+ _coordinate = CLLocationCoordinate2DMake(latitude, longitude);
791
+ if (zoom > 0) {
792
+ _zoom = zoom;
793
+ }
794
+ [self rerenderBaseMap];
795
+ }
796
+
797
+ - (void)fitCoordinates:(NSArray *)coordinates
798
+ edgeInsetsTop:(double)edgeInsetsTop
799
+ edgeInsetsLeft:(double)edgeInsetsLeft
800
+ edgeInsetsBottom:(double)edgeInsetsBottom
801
+ edgeInsetsRight:(double)edgeInsetsRight
802
+ duration:(double)duration {
803
+ if (coordinates.count == 0 || !_projectionReady)
804
+ return;
805
+
806
+ MKMapRect boundsRect = MKMapRectNull;
807
+ for (NSDictionary *coordinate in coordinates) {
808
+ MKMapPoint point = MKMapPointForCoordinate(
809
+ CLLocationCoordinate2DMake([coordinate[@"latitude"] doubleValue],
810
+ [coordinate[@"longitude"] doubleValue]));
811
+ boundsRect =
812
+ MKMapRectUnion(boundsRect, MKMapRectMake(point.x, point.y, 0, 0));
813
+ }
814
+
815
+ double availWidth =
816
+ MAX(_projectedSize.width - _edgeInsets.left - _edgeInsets.right -
817
+ edgeInsetsLeft - edgeInsetsRight,
818
+ 1.0);
819
+ double availHeight =
820
+ MAX(_projectedSize.height - _edgeInsets.top - _edgeInsets.bottom -
821
+ edgeInsetsTop - edgeInsetsBottom,
822
+ 1.0);
823
+
824
+ // Map points per view point at which the bounds fit the padded viewport
825
+ double scale = MAX(boundsRect.size.width / availWidth,
826
+ boundsRect.size.height / availHeight);
827
+ MKMapPoint center = MKMapPointMake(MKMapRectGetMidX(boundsRect),
828
+ MKMapRectGetMidY(boundsRect));
829
+ if (scale > 0) {
830
+ _coordinate = MKCoordinateForMapPoint(center);
831
+ _zoom = [self zoomForMapPointsPerPoint:scale];
832
+ } else {
833
+ // Coincident coordinates; keep the current zoom
834
+ scale = _mapRect.size.width / _projectedSize.width;
835
+ }
836
+
837
+ // Asymmetric padding shifts the visible center, like edge insets
838
+ CGPoint offset = LuggMapInsetOffset(UIEdgeInsetsMake(
839
+ edgeInsetsTop, edgeInsetsLeft, edgeInsetsBottom, edgeInsetsRight));
840
+ center.x -= offset.x * scale;
841
+ center.y -= offset.y * scale;
842
+ _coordinate = MKCoordinateForMapPoint(center);
843
+
844
+ [self rerenderBaseMap];
845
+ }
846
+
847
+ @end