@lugg/maps 1.0.0-beta.9 → 1.0.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 (62) 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 +41 -0
  4. package/android/src/main/java/com/luggmaps/LuggMapViewManager.kt +14 -0
  5. package/android/src/main/java/com/luggmaps/LuggMapWrapperView.kt +27 -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 +307 -41
  9. package/android/src/main/java/com/luggmaps/core/MapProviderDelegate.kt +1 -0
  10. package/android/src/main/java/com/luggmaps/core/PolylineAnimator.kt +61 -10
  11. package/ios/LuggMapView.mm +170 -2
  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 +48 -31
  17. package/ios/core/AppleStaticMapProvider.h +14 -0
  18. package/ios/core/AppleStaticMapProvider.mm +159 -0
  19. package/ios/core/GMSPolylineAnimator.m +40 -15
  20. package/ios/core/GoogleMapProvider.h +10 -0
  21. package/ios/core/GoogleMapProvider.mm +56 -44
  22. package/ios/core/GoogleStaticMapProvider.h +21 -0
  23. package/ios/core/GoogleStaticMapProvider.mm +213 -0
  24. package/ios/core/MKPolylineAnimator.m +67 -43
  25. package/ios/core/MapProviderDelegate.h +7 -0
  26. package/ios/core/StaticMapProviderBase.h +75 -0
  27. package/ios/core/StaticMapProviderBase.mm +845 -0
  28. package/ios/events/CameraIdleEvent.h +4 -2
  29. package/ios/events/CameraMoveEvent.h +4 -2
  30. package/ios/events/CirclePressEvent.h +4 -2
  31. package/ios/events/GroundOverlayPressEvent.h +4 -2
  32. package/ios/events/LongPressEvent.h +4 -2
  33. package/ios/events/MarkerDragEvent.h +12 -6
  34. package/ios/events/MarkerPressEvent.h +4 -2
  35. package/ios/events/PolygonPressEvent.h +4 -2
  36. package/ios/events/PressEvent.h +4 -2
  37. package/ios/events/ReadyEvent.h +4 -2
  38. package/ios/events/TileOverlayPressEvent.h +4 -2
  39. package/ios/extensions/MKMapView+Zoom.h +20 -0
  40. package/ios/extensions/MKMapView+Zoom.m +28 -18
  41. package/ios/extensions/UIView+Snapshot.h +16 -0
  42. package/ios/extensions/UIView+Snapshot.m +27 -0
  43. package/lib/module/MapView.js +7 -0
  44. package/lib/module/MapView.js.map +1 -1
  45. package/lib/module/MapView.web.js +9 -4
  46. package/lib/module/MapView.web.js.map +1 -1
  47. package/lib/module/fabric/LuggMapViewNativeComponent.ts +3 -0
  48. package/lib/typescript/src/MapView.d.ts.map +1 -1
  49. package/lib/typescript/src/MapView.types.d.ts +24 -0
  50. package/lib/typescript/src/MapView.types.d.ts.map +1 -1
  51. package/lib/typescript/src/MapView.web.d.ts.map +1 -1
  52. package/lib/typescript/src/fabric/LuggMapViewNativeComponent.d.ts +3 -0
  53. package/lib/typescript/src/fabric/LuggMapViewNativeComponent.d.ts.map +1 -1
  54. package/package.json +19 -12
  55. package/src/MapView.tsx +7 -0
  56. package/src/MapView.types.ts +24 -0
  57. package/src/MapView.web.tsx +15 -9
  58. package/src/fabric/LuggMapViewNativeComponent.ts +3 -0
  59. package/plugin/build/withMapsAndroid.d.ts +0 -5
  60. package/plugin/build/withMapsAndroid.js +0 -15
  61. package/plugin/build/withMapsIOS.d.ts +0 -5
  62. package/plugin/build/withMapsIOS.js +0 -27
@@ -16,18 +16,43 @@ class PolylineAnimator {
16
16
  companion object {
17
17
  private const val MAX_ANIMATION_SPANS = 16
18
18
  private const val MAX_GRADIENT_SPANS = 512
19
+
20
+ // Cap polyline updates to ~60fps; every update crosses Binder into the
21
+ // maps renderer, and 120Hz displays double that churn (CONSUMER-APP-44Y)
22
+ private const val MIN_UPDATE_INTERVAL_NANOS = 16_000_000L
19
23
  }
20
24
 
21
25
  var polyline: Polyline? = null
22
26
  var coordinates: List<LatLng> = emptyList()
23
27
  set(value) {
28
+ // Structural equality: Fabric delivers a new list instance on every
29
+ // commit, so identity checks never match and the full route would be
30
+ // re-parceled across Binder even when unchanged
31
+ if (field == value) return
24
32
  field = value
33
+ dirty = true
25
34
  if (animated && animator != null) {
26
35
  computeCumulativeDistances()
27
36
  }
28
37
  }
29
38
  var strokeColors: List<Int> = listOf(Color.BLACK)
39
+ set(value) {
40
+ if (field == value) return
41
+ field = value
42
+ dirty = true
43
+ }
30
44
  var strokeWidth: Float = 1f
45
+ set(value) {
46
+ if (field == value) return
47
+ field = value
48
+ polyline?.width = value
49
+ }
50
+ var zIndex: Float = 0f
51
+ set(value) {
52
+ if (field == value) return
53
+ field = value
54
+ polyline?.zIndex = value
55
+ }
31
56
  var animatedOptions: AnimatedOptions = AnimatedOptions()
32
57
  set(value) {
33
58
  if (field == value) return
@@ -51,8 +76,11 @@ class PolylineAnimator {
51
76
 
52
77
  private var animator: ValueAnimator? = null
53
78
  private var animationProgress: Float = 0f
79
+ private var lastUpdateNanos: Long = 0L
80
+ private var lastAppliedColor: Int? = null
54
81
  private var cumulativeDistances: FloatArray = floatArrayOf()
55
82
  private var totalLength: Float = 0f
83
+ private var dirty: Boolean = true
56
84
 
57
85
  // Reusable collections to avoid per-frame allocations
58
86
  private val reusablePoints = ArrayList<LatLng>()
@@ -73,16 +101,21 @@ class PolylineAnimator {
73
101
 
74
102
  fun update() {
75
103
  if (animated) return
104
+ if (!dirty) return
76
105
 
77
106
  val poly = polyline ?: return
78
107
  if (coordinates.size < 2) return
79
108
 
109
+ dirty = false
80
110
  poly.points = coordinates
81
111
 
82
112
  if (strokeColors.size > 1) {
83
- poly.setSpans(createGradientSpans())
113
+ applyGradientSpans(poly)
114
+ lastAppliedColor = null
84
115
  } else {
85
- poly.color = strokeColors.firstOrNull() ?: Color.BLACK
116
+ val color = strokeColors.firstOrNull() ?: Color.BLACK
117
+ poly.color = color
118
+ lastAppliedColor = color
86
119
  }
87
120
  }
88
121
 
@@ -101,6 +134,9 @@ class PolylineAnimator {
101
134
  interpolator = LinearInterpolator()
102
135
  addUpdateListener { animation ->
103
136
  animationProgress = animation.animatedValue as Float
137
+ val now = System.nanoTime()
138
+ if (now - lastUpdateNanos < MIN_UPDATE_INTERVAL_NANOS) return@addUpdateListener
139
+ lastUpdateNanos = now
104
140
  updateAnimatedPolyline()
105
141
  }
106
142
  start()
@@ -172,6 +208,7 @@ class PolylineAnimator {
172
208
  private fun stopAnimation() {
173
209
  animator?.cancel()
174
210
  animator = null
211
+ lastUpdateNanos = 0L
175
212
  }
176
213
 
177
214
  fun pause() {
@@ -210,8 +247,10 @@ class PolylineAnimator {
210
247
  }
211
248
 
212
249
  if (headDist <= tailDist) {
213
- poly.points = listOf(coordinates.firstOrNull() ?: LatLng(0.0, 0.0))
250
+ val point = coordinates.firstOrNull() ?: LatLng(0.0, 0.0)
214
251
  poly.color = strokeColors.firstOrNull() ?: Color.BLACK
252
+ lastAppliedColor = null
253
+ poly.points = listOf(point, point)
215
254
  return
216
255
  }
217
256
 
@@ -235,11 +274,13 @@ class PolylineAnimator {
235
274
 
236
275
  if (reusablePoints.size < 2) return
237
276
 
238
- poly.points = ArrayList(reusablePoints)
239
-
240
- if (strokeColors.size <= 1) {
277
+ if (strokeColors.size > 1) {
278
+ // Clear spans before setting points to prevent IndexOutOfBoundsException
279
+ // when the new points list is shorter than what existing spans reference
241
280
  poly.color = strokeColors.firstOrNull() ?: Color.BLACK
242
- } else {
281
+ lastAppliedColor = null
282
+ poly.points = reusablePoints
283
+
243
284
  val segmentCount = reusablePoints.size - 1
244
285
  val spanCount = min(segmentCount, MAX_ANIMATION_SPANS)
245
286
  val segmentsPerSpan = segmentCount.toDouble() / spanCount
@@ -251,18 +292,28 @@ class PolylineAnimator {
251
292
  reusableSpans.add(StyleSpan(StrokeStyle.colorBuilder(color).build(), segmentsPerSpan))
252
293
  }
253
294
  poly.setSpans(reusableSpans)
295
+ } else {
296
+ val color = strokeColors.firstOrNull() ?: Color.BLACK
297
+ if (lastAppliedColor != color) {
298
+ poly.color = color
299
+ lastAppliedColor = color
300
+ }
301
+ poly.points = reusablePoints
254
302
  }
255
303
  }
256
304
 
257
- private fun createGradientSpans(): List<StyleSpan> {
305
+ private fun applyGradientSpans(poly: Polyline) {
258
306
  val segmentCount = coordinates.size - 1
259
307
  val spanCount = min(segmentCount, MAX_GRADIENT_SPANS)
260
308
  val segmentsPerSpan = segmentCount.toDouble() / spanCount
261
- return (0 until spanCount).map { i ->
309
+
310
+ reusableSpans.clear()
311
+ for (i in 0 until spanCount) {
262
312
  val position = (i + 0.5f) / spanCount
263
313
  val color = colorAtGradientPosition(position)
264
- StyleSpan(StrokeStyle.colorBuilder(color).build(), segmentsPerSpan)
314
+ reusableSpans.add(StyleSpan(StrokeStyle.colorBuilder(color).build(), segmentsPerSpan))
265
315
  }
316
+ poly.setSpans(reusableSpans)
266
317
  }
267
318
 
268
319
  private fun colorAtGradientPosition(position: Float): Int {
@@ -7,7 +7,9 @@
7
7
  #import "LuggPolylineView.h"
8
8
  #import "LuggTileOverlayView.h"
9
9
  #import "core/AppleMapProvider.h"
10
+ #import "core/AppleStaticMapProvider.h"
10
11
  #import "core/GoogleMapProvider.h"
12
+ #import "core/GoogleStaticMapProvider.h"
11
13
  #import "core/MapProviderDelegate.h"
12
14
  #import "events/CameraIdleEvent.h"
13
15
  #import "events/CameraMoveEvent.h"
@@ -25,6 +27,19 @@
25
27
  using namespace facebook::react;
26
28
  using namespace luggmaps::events;
27
29
 
30
+ // Static snapshots cached across view recycling, keyed by the user-provided
31
+ // staticKey plus the map settings that affect appearance.
32
+ static NSCache<NSString *, UIImage *> *StaticSnapshotCache(void) {
33
+ static NSCache<NSString *, UIImage *> *cache;
34
+ static dispatch_once_t onceToken;
35
+ dispatch_once(&onceToken, ^{
36
+ cache = [[NSCache alloc] init];
37
+ cache.countLimit = 100;
38
+ cache.totalCostLimit = 64 * 1024 * 1024; // bytes
39
+ });
40
+ return cache;
41
+ }
42
+
28
43
  @interface LuggMapView () <RCTLuggMapViewViewProtocol, MapProviderDelegate>
29
44
  @end
30
45
 
@@ -39,6 +54,10 @@ using namespace luggmaps::events;
39
54
  BOOL _scrollEnabled;
40
55
  BOOL _rotateEnabled;
41
56
  BOOL _pitchEnabled;
57
+ BOOL _compassEnabled;
58
+ BOOL _staticMode;
59
+ NSString *_staticKey;
60
+ BOOL _staticSnapshotDone;
42
61
  BOOL _userLocationEnabled;
43
62
  LuggMapViewMapType _mapType;
44
63
  LuggMapViewTheme _theme;
@@ -68,6 +87,9 @@ using namespace luggmaps::events;
68
87
  _scrollEnabled = YES;
69
88
  _rotateEnabled = YES;
70
89
  _pitchEnabled = YES;
90
+ _compassEnabled = YES;
91
+ _staticMode = NO;
92
+ _staticKey = @"";
71
93
  _userLocationEnabled = NO;
72
94
  _poiEnabled = NO;
73
95
  _poiFilterMode = LuggMapViewPoiFilterMode::Including;
@@ -168,17 +190,118 @@ using namespace luggmaps::events;
168
190
  [super didMoveToWindow];
169
191
  if (self.window) {
170
192
  if (!_provider && _mapWrapperView) {
171
- [self initializeProvider];
193
+ if (_staticMode) {
194
+ [self startStaticContent];
195
+ } else {
196
+ [self initializeProvider];
197
+ }
172
198
  }
173
199
  [_provider resumeAnimations];
174
200
  } else {
201
+ // A static map mid-warmup may hold an expensive live map view. Destroy
202
+ // it off-window and re-render on return instead of letting paused maps
203
+ // pile up across screen switches and all resume at once
204
+ if (_staticMode && _provider && !_staticSnapshotDone) {
205
+ [_provider destroy];
206
+ _provider = nil;
207
+ _initialized = NO;
208
+ }
175
209
  [_provider pauseAnimations];
176
210
  }
177
211
  }
178
212
 
213
+ // Shown while the map warms up, until the live map or snapshot covers it
214
+ - (void)applyStaticPlaceholderBackground {
215
+ const auto &viewProps =
216
+ *std::static_pointer_cast<LuggMapViewProps const>(_props);
217
+ if (viewProps.backgroundColor) {
218
+ // A user-provided background acts as the placeholder
219
+ return;
220
+ }
221
+
222
+ switch (_theme) {
223
+ case LuggMapViewTheme::Dark:
224
+ _mapWrapperView.overrideUserInterfaceStyle = UIUserInterfaceStyleDark;
225
+ break;
226
+ case LuggMapViewTheme::Light:
227
+ _mapWrapperView.overrideUserInterfaceStyle = UIUserInterfaceStyleLight;
228
+ break;
229
+ default:
230
+ _mapWrapperView.overrideUserInterfaceStyle =
231
+ UIUserInterfaceStyleUnspecified;
232
+ break;
233
+ }
234
+ _mapWrapperView.backgroundColor = UIColor.secondarySystemBackgroundColor;
235
+ }
236
+
237
+ - (nullable NSString *)staticSnapshotCacheKey {
238
+ if (_staticKey.length == 0 || !_mapWrapperView)
239
+ return nil;
240
+
241
+ CGSize size = _mapWrapperView.bounds.size;
242
+ if (size.width <= 0 || size.height <= 0)
243
+ return nil;
244
+
245
+ // 'system' theme snapshots depend on the current appearance
246
+ NSInteger style = _theme == LuggMapViewTheme::System
247
+ ? (NSInteger)self.traitCollection.userInterfaceStyle
248
+ : -(NSInteger)_theme;
249
+
250
+ // Camera is part of the key so a reused staticKey with a different
251
+ // location/zoom can't serve a stale snapshot. The provider's camera (when
252
+ // available) reflects imperative camera changes, so a re-rendered
253
+ // snapshot is keyed on what it actually shows.
254
+ const auto &viewProps =
255
+ *std::static_pointer_cast<LuggMapViewProps const>(_props);
256
+ double latitude = viewProps.initialCoordinate.latitude;
257
+ double longitude = viewProps.initialCoordinate.longitude;
258
+ double zoom = viewProps.initialZoom;
259
+ if ([_provider isKindOfClass:[StaticMapProviderBase class]]) {
260
+ StaticMapProviderBase *staticProvider = (StaticMapProviderBase *)_provider;
261
+ latitude = staticProvider.coordinate.latitude;
262
+ longitude = staticProvider.coordinate.longitude;
263
+ zoom = staticProvider.zoom;
264
+ }
265
+
266
+ return [NSString
267
+ stringWithFormat:@"%@|%d|%@|%d|%ld|%.0fx%.0f|%.6f,%.6f,%.2f|%@",
268
+ _staticKey, (int)_providerType, _mapId, (int)_mapType,
269
+ (long)style, size.width, size.height, latitude,
270
+ longitude, zoom, NSStringFromUIEdgeInsets(_edgeInsets)];
271
+ }
272
+
273
+ - (void)startStaticContent {
274
+ [self applyStaticPlaceholderBackground];
275
+
276
+ // Both static providers render fully async: the placeholder shows until
277
+ // the snapshotter (Apple) or a brief live-map warmup (Google) delivers
278
+ // the base map, which reveals together with the marker/shape overlays
279
+ [self initializeProvider];
280
+ }
281
+
282
+ // staticKey identifies the map's content; when it changes (e.g. a recycled
283
+ // list row bound to a new item), any live warmup or displayed snapshot is
284
+ // stale and the static lifecycle restarts. Without this, an in-flight
285
+ // warmup caches the old item's map under the new key.
286
+ - (void)resetStaticContent {
287
+ _staticSnapshotDone = NO;
288
+
289
+ if (_provider) {
290
+ [_provider destroy];
291
+ _provider = nil;
292
+ _initialized = NO;
293
+ }
294
+
295
+ if (self.window) {
296
+ [self startStaticContent];
297
+ }
298
+ }
299
+
179
300
  - (void)prepareForRecycle {
180
301
  [super prepareForRecycle];
181
302
 
303
+ _staticSnapshotDone = NO;
304
+
182
305
  [_provider destroy];
183
306
  _provider = nil;
184
307
  _mapWrapperView = nil;
@@ -195,7 +318,12 @@ using namespace luggmaps::events;
195
318
  *std::static_pointer_cast<LuggMapViewProps const>(_props);
196
319
 
197
320
  if (_providerType == LuggMapViewProvider::Apple) {
198
- _provider = [[AppleMapProvider alloc] init];
321
+ _provider = _staticMode ? [[AppleStaticMapProvider alloc] init]
322
+ : [[AppleMapProvider alloc] init];
323
+ } else if (_staticMode) {
324
+ GoogleStaticMapProvider *google = [[GoogleStaticMapProvider alloc] init];
325
+ google.mapId = _mapId;
326
+ _provider = google;
199
327
  } else {
200
328
  GoogleMapProvider *google = [[GoogleMapProvider alloc] init];
201
329
  google.mapId = _mapId;
@@ -203,6 +331,7 @@ using namespace luggmaps::events;
203
331
  }
204
332
 
205
333
  _provider.delegate = self;
334
+ _provider.staticMode = _staticMode;
206
335
 
207
336
  CLLocationCoordinate2D coordinate =
208
337
  CLLocationCoordinate2DMake(viewProps.initialCoordinate.latitude,
@@ -212,6 +341,16 @@ using namespace luggmaps::events;
212
341
  initialCoordinate:coordinate
213
342
  initialZoom:viewProps.initialZoom];
214
343
 
344
+ // After initializeMapInView so the cache key reads the provider's camera;
345
+ // the base render is async and picks up the cached image
346
+ if (_staticMode) {
347
+ NSString *cacheKey = [self staticSnapshotCacheKey];
348
+ if (cacheKey) {
349
+ ((StaticMapProviderBase *)_provider).cachedBaseImage =
350
+ [StaticSnapshotCache() objectForKey:cacheKey];
351
+ }
352
+ }
353
+
215
354
  // Apply cached props after map view is created
216
355
  [self applyProps];
217
356
  [_provider setEdgeInsets:_edgeInsets oldEdgeInsets:UIEdgeInsetsZero];
@@ -242,6 +381,19 @@ using namespace luggmaps::events;
242
381
  ReadyEvent::emit<LuggMapViewEventEmitter>(_eventEmitter);
243
382
  }
244
383
 
384
+ - (void)mapProviderDidFinishStaticSnapshot {
385
+ _staticSnapshotDone = YES;
386
+ }
387
+
388
+ - (void)mapProviderDidCaptureStaticImage:(UIImage *)image {
389
+ NSString *key = [self staticSnapshotCacheKey];
390
+ if (key) {
391
+ NSUInteger cost = (NSUInteger)(image.size.width * image.scale *
392
+ image.size.height * image.scale * 4);
393
+ [StaticSnapshotCache() setObject:image forKey:key cost:cost];
394
+ }
395
+ }
396
+
245
397
  - (void)mapProviderDidMoveCamera:(double)latitude
246
398
  longitude:(double)longitude
247
399
  zoom:(double)zoom
@@ -281,6 +433,7 @@ using namespace luggmaps::events;
281
433
  [_provider setScrollEnabled:_scrollEnabled];
282
434
  [_provider setRotateEnabled:_rotateEnabled];
283
435
  [_provider setPitchEnabled:_pitchEnabled];
436
+ [_provider setCompassEnabled:_compassEnabled];
284
437
  [_provider setUserLocationEnabled:_userLocationEnabled];
285
438
  [_provider setMapType:_mapType];
286
439
  [_provider setTheme:_theme];
@@ -300,6 +453,12 @@ using namespace luggmaps::events;
300
453
  *std::static_pointer_cast<LuggMapViewProps const>(_props);
301
454
 
302
455
  _providerType = newViewProps.provider;
456
+ _staticMode = newViewProps.staticMode;
457
+
458
+ NSString *newStaticKey =
459
+ [NSString stringWithUTF8String:newViewProps.staticKey.c_str()];
460
+ BOOL staticKeyChanged = ![newStaticKey isEqualToString:_staticKey];
461
+ _staticKey = newStaticKey;
303
462
 
304
463
  NSString *newMapId =
305
464
  [NSString stringWithUTF8String:newViewProps.mapId.c_str()];
@@ -323,6 +482,10 @@ using namespace luggmaps::events;
323
482
  _pitchEnabled = newViewProps.pitchEnabled;
324
483
  [_provider setPitchEnabled:_pitchEnabled];
325
484
  }
485
+ if (newViewProps.compassEnabled != prevViewProps.compassEnabled) {
486
+ _compassEnabled = newViewProps.compassEnabled;
487
+ [_provider setCompassEnabled:_compassEnabled];
488
+ }
326
489
  if (newViewProps.userLocationEnabled != prevViewProps.userLocationEnabled) {
327
490
  _userLocationEnabled = newViewProps.userLocationEnabled;
328
491
  [_provider setUserLocationEnabled:_userLocationEnabled];
@@ -374,6 +537,11 @@ using namespace luggmaps::events;
374
537
  }
375
538
 
376
539
  [super updateProps:props oldProps:oldProps];
540
+
541
+ // After super so the snapshot cache key sees the new camera props
542
+ if (staticKeyChanged && _staticMode && _mapWrapperView) {
543
+ [self resetStaticContent];
544
+ }
377
545
  }
378
546
 
379
547
  #pragma mark - Commands
@@ -4,6 +4,11 @@
4
4
  NS_ASSUME_NONNULL_BEGIN
5
5
 
6
6
  @interface LuggMapWrapperView : RCTViewComponentView
7
+
8
+ /// Called after layout; lets the static snapshotter start (or restart)
9
+ /// once the wrapper has a real size, since a snapshot can't autoresize
10
+ @property(nonatomic, copy, nullable) void (^layoutHandler)(void);
11
+
7
12
  @end
8
13
 
9
14
  NS_ASSUME_NONNULL_END
@@ -29,6 +29,24 @@ using namespace facebook::react;
29
29
  return self;
30
30
  }
31
31
 
32
+ - (void)layoutSubviews {
33
+ [super layoutSubviews];
34
+ if (self.layoutHandler) {
35
+ self.layoutHandler();
36
+ }
37
+ }
38
+
39
+ - (void)prepareForRecycle {
40
+ [super prepareForRecycle];
41
+
42
+ // Static placeholder styling is applied outside of props
43
+ // (LuggMapView applyStaticPlaceholderBackground) and would otherwise
44
+ // leak into the next map that reuses this recycled view
45
+ self.backgroundColor = nil;
46
+ self.overrideUserInterfaceStyle = UIUserInterfaceStyleUnspecified;
47
+ self.layoutHandler = nil;
48
+ }
49
+
32
50
  Class<RCTComponentViewProtocol> LuggMapWrapperViewCls(void) {
33
51
  return LuggMapWrapperView.class;
34
52
  }
@@ -287,6 +287,8 @@ using namespace luggmaps::events;
287
287
  _iconView.layer.anchorPoint = CGPointMake(0.5, 0.5);
288
288
  _iconView.frame = CGRectMake(0, 0, _iconView.bounds.size.width,
289
289
  _iconView.bounds.size.height);
290
+ // Static providers hide the icon view until positioned
291
+ _iconView.hidden = NO;
290
292
  }
291
293
 
292
294
  - (void)showCallout {
@@ -3,6 +3,14 @@
3
3
 
4
4
  NS_ASSUME_NONNULL_BEGIN
5
5
 
6
+ /// POI filter for the given props, shared by the live map view and the
7
+ /// static snapshotter. Returns nil for "show everything".
8
+ MKPointOfInterestFilter *_Nullable LuggPointOfInterestFilter(
9
+ BOOL poiEnabled, facebook::react::LuggMapViewPoiFilterMode filterMode,
10
+ NSArray<NSString *> *filterCategories);
11
+
12
+ MKMapType LuggMKMapTypeFromMapType(facebook::react::LuggMapViewMapType mapType);
13
+
6
14
  @interface LuggAppleMapViewContent : MKMapView
7
15
  @end
8
16
 
@@ -145,6 +145,7 @@ static double tileToLng(NSInteger x, NSInteger z) {
145
145
  }
146
146
 
147
147
  @synthesize delegate = _delegate;
148
+ @synthesize staticMode = _staticMode;
148
149
 
149
150
  - (instancetype)init {
150
151
  if (self = [super init]) {
@@ -182,6 +183,10 @@ static double tileToLng(NSInteger x, NSInteger z) {
182
183
  _mapView.delegate = self;
183
184
  _mapView.insetsLayoutMarginsFromSafeArea = NO;
184
185
 
186
+ if (_staticMode) {
187
+ _mapView.userInteractionEnabled = NO;
188
+ }
189
+
185
190
  [self applyZoomRange];
186
191
 
187
192
  _tapGesture =
@@ -212,6 +217,11 @@ static double tileToLng(NSInteger x, NSInteger z) {
212
217
  }
213
218
 
214
219
  - (void)destroy {
220
+ [self destroyMapView];
221
+ _isMapReady = NO;
222
+ }
223
+
224
+ - (void)destroyMapView {
215
225
  [self dismissNonBubbledCallout];
216
226
  [self stopEdgeInsetsAnimation];
217
227
  if (_tapGesture) {
@@ -222,9 +232,9 @@ static double tileToLng(NSInteger x, NSInteger z) {
222
232
  [_mapView removeGestureRecognizer:_longPressGesture];
223
233
  _longPressGesture = nil;
224
234
  }
235
+ _mapView.delegate = nil;
225
236
  [_mapView removeFromSuperview];
226
237
  _mapView = nil;
227
- _isMapReady = NO;
228
238
  }
229
239
 
230
240
  #pragma mark - Props
@@ -245,6 +255,10 @@ static double tileToLng(NSInteger x, NSInteger z) {
245
255
  _mapView.pitchEnabled = enabled;
246
256
  }
247
257
 
258
+ - (void)setCompassEnabled:(BOOL)enabled {
259
+ _mapView.showsCompass = enabled;
260
+ }
261
+
248
262
  - (void)setUserLocationEnabled:(BOOL)enabled {
249
263
  _mapView.showsUserLocation = enabled;
250
264
  }
@@ -338,33 +352,36 @@ static MKPointOfInterestCategory poiCategoryFromString(NSString *string) {
338
352
  return map[string];
339
353
  }
340
354
 
341
- - (void)applyPoiFilter {
342
- if (!_poiEnabled) {
343
- _mapView.pointOfInterestFilter =
344
- MKPointOfInterestFilter.filterExcludingAllCategories;
345
- return;
355
+ MKPointOfInterestFilter *_Nullable LuggPointOfInterestFilter(
356
+ BOOL poiEnabled, LuggMapViewPoiFilterMode filterMode,
357
+ NSArray<NSString *> *filterCategories) {
358
+ if (!poiEnabled) {
359
+ return MKPointOfInterestFilter.filterExcludingAllCategories;
346
360
  }
347
- if (_poiFilterCategories.count > 0) {
361
+ if (filterCategories.count > 0) {
348
362
  NSMutableArray<MKPointOfInterestCategory> *categories =
349
363
  [NSMutableArray array];
350
- for (NSString *name in _poiFilterCategories) {
364
+ for (NSString *name in filterCategories) {
351
365
  MKPointOfInterestCategory category = poiCategoryFromString(name);
352
366
  if (category) {
353
367
  [categories addObject:category];
354
368
  }
355
369
  }
356
370
  if (categories.count > 0) {
357
- if (_poiFilterMode == LuggMapViewPoiFilterMode::Excluding) {
358
- _mapView.pointOfInterestFilter = [[MKPointOfInterestFilter alloc]
371
+ if (filterMode == LuggMapViewPoiFilterMode::Excluding) {
372
+ return [[MKPointOfInterestFilter alloc]
359
373
  initExcludingCategories:categories];
360
- } else {
361
- _mapView.pointOfInterestFilter = [[MKPointOfInterestFilter alloc]
362
- initIncludingCategories:categories];
363
374
  }
364
- return;
375
+ return
376
+ [[MKPointOfInterestFilter alloc] initIncludingCategories:categories];
365
377
  }
366
378
  }
367
- _mapView.pointOfInterestFilter = nil;
379
+ return nil;
380
+ }
381
+
382
+ - (void)applyPoiFilter {
383
+ _mapView.pointOfInterestFilter = LuggPointOfInterestFilter(
384
+ _poiEnabled, _poiFilterMode, _poiFilterCategories);
368
385
  }
369
386
 
370
387
  - (void)setInsetAdjustment:(LuggMapViewInsetAdjustment)insetAdjustment {
@@ -395,26 +412,25 @@ static MKPointOfInterestCategory poiCategoryFromString(NSString *string) {
395
412
  [self applyPoiFilter];
396
413
  }
397
414
 
398
- - (void)setMapType:(LuggMapViewMapType)mapType {
415
+ MKMapType LuggMKMapTypeFromMapType(LuggMapViewMapType mapType) {
399
416
  switch (mapType) {
400
417
  case LuggMapViewMapType::Satellite:
401
- _mapView.mapType = MKMapTypeSatellite;
402
- break;
418
+ return MKMapTypeSatellite;
403
419
  case LuggMapViewMapType::Terrain:
404
- _mapView.mapType = MKMapTypeStandard;
405
- break;
420
+ return MKMapTypeStandard;
406
421
  case LuggMapViewMapType::Hybrid:
407
- _mapView.mapType = MKMapTypeHybrid;
408
- break;
422
+ return MKMapTypeHybrid;
409
423
  case LuggMapViewMapType::MutedStandard:
410
- _mapView.mapType = MKMapTypeMutedStandard;
411
- break;
424
+ return MKMapTypeMutedStandard;
412
425
  default:
413
- _mapView.mapType = MKMapTypeStandard;
414
- break;
426
+ return MKMapTypeStandard;
415
427
  }
416
428
  }
417
429
 
430
+ - (void)setMapType:(LuggMapViewMapType)mapType {
431
+ _mapView.mapType = LuggMKMapTypeFromMapType(mapType);
432
+ }
433
+
418
434
  - (void)setTheme:(LuggMapViewTheme)theme {
419
435
  switch (theme) {
420
436
  case LuggMapViewTheme::Dark:
@@ -832,7 +848,8 @@ static MKPointOfInterestCategory poiCategoryFromString(NSString *string) {
832
848
  renderer.strokeColors = colors;
833
849
  }
834
850
  renderer.animatedOptions = polylineView.animatedOptions;
835
- renderer.animated = polylineView.animated;
851
+ // Static maps snapshot an arbitrary frame; render the full polyline
852
+ renderer.animated = polylineView.animated && !_staticMode;
836
853
  polylineView.renderer = renderer;
837
854
  return renderer;
838
855
  }
@@ -1361,7 +1378,7 @@ static MKPointOfInterestCategory poiCategoryFromString(NSString *string) {
1361
1378
  renderer.strokeColors =
1362
1379
  polylineView.strokeColors.count > 1 ? polylineView.strokeColors : nil;
1363
1380
  renderer.animatedOptions = polylineView.animatedOptions;
1364
- renderer.animated = polylineView.animated;
1381
+ renderer.animated = polylineView.animated && !_staticMode;
1365
1382
  return;
1366
1383
  }
1367
1384
 
@@ -1774,11 +1791,11 @@ static MKPointOfInterestCategory poiCategoryFromString(NSString *string) {
1774
1791
  } else if (duration > 0) {
1775
1792
  CLLocationCoordinate2D center =
1776
1793
  CLLocationCoordinate2DMake(latitude, longitude);
1777
- MKCoordinateRegion region = [_mapView regionForCenterCoordinate:center
1778
- zoomLevel:targetZoom];
1794
+ MKMapCamera *camera = [_mapView cameraForCenterCoordinate:center
1795
+ zoomLevel:targetZoom];
1779
1796
  [UIView animateWithDuration:duration / 1000.0
1780
1797
  animations:^{
1781
- [self->_mapView setRegion:region animated:NO];
1798
+ [self->_mapView setCamera:camera animated:NO];
1782
1799
  }];
1783
1800
  } else {
1784
1801
  [_mapView
@@ -0,0 +1,14 @@
1
+ #import "StaticMapProviderBase.h"
2
+
3
+ NS_ASSUME_NONNULL_BEGIN
4
+
5
+ /**
6
+ * Static Apple map rendered with MKMapSnapshotter. The base map renders
7
+ * fully asynchronously (no live MKMapView is ever created), so static maps
8
+ * keep loading while the user scrolls without blocking the main thread.
9
+ */
10
+ @interface AppleStaticMapProvider : StaticMapProviderBase
11
+
12
+ @end
13
+
14
+ NS_ASSUME_NONNULL_END