@lugg/maps 1.0.0-beta.8 → 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 +3 -23
  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 +65 -11
  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 +46 -18
  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 +7 -4
  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 +7 -4
  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 +7 -4
  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
@@ -15,18 +15,44 @@ import kotlin.math.min
15
15
  class PolylineAnimator {
16
16
  companion object {
17
17
  private const val MAX_ANIMATION_SPANS = 16
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
18
23
  }
19
24
 
20
25
  var polyline: Polyline? = null
21
26
  var coordinates: List<LatLng> = emptyList()
22
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
23
32
  field = value
33
+ dirty = true
24
34
  if (animated && animator != null) {
25
35
  computeCumulativeDistances()
26
36
  }
27
37
  }
28
38
  var strokeColors: List<Int> = listOf(Color.BLACK)
39
+ set(value) {
40
+ if (field == value) return
41
+ field = value
42
+ dirty = true
43
+ }
29
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
+ }
30
56
  var animatedOptions: AnimatedOptions = AnimatedOptions()
31
57
  set(value) {
32
58
  if (field == value) return
@@ -50,8 +76,11 @@ class PolylineAnimator {
50
76
 
51
77
  private var animator: ValueAnimator? = null
52
78
  private var animationProgress: Float = 0f
79
+ private var lastUpdateNanos: Long = 0L
80
+ private var lastAppliedColor: Int? = null
53
81
  private var cumulativeDistances: FloatArray = floatArrayOf()
54
82
  private var totalLength: Float = 0f
83
+ private var dirty: Boolean = true
55
84
 
56
85
  // Reusable collections to avoid per-frame allocations
57
86
  private val reusablePoints = ArrayList<LatLng>()
@@ -72,16 +101,21 @@ class PolylineAnimator {
72
101
 
73
102
  fun update() {
74
103
  if (animated) return
104
+ if (!dirty) return
75
105
 
76
106
  val poly = polyline ?: return
77
107
  if (coordinates.size < 2) return
78
108
 
109
+ dirty = false
79
110
  poly.points = coordinates
80
111
 
81
112
  if (strokeColors.size > 1) {
82
- poly.setSpans(createGradientSpans())
113
+ applyGradientSpans(poly)
114
+ lastAppliedColor = null
83
115
  } else {
84
- poly.color = strokeColors.firstOrNull() ?: Color.BLACK
116
+ val color = strokeColors.firstOrNull() ?: Color.BLACK
117
+ poly.color = color
118
+ lastAppliedColor = color
85
119
  }
86
120
  }
87
121
 
@@ -100,6 +134,9 @@ class PolylineAnimator {
100
134
  interpolator = LinearInterpolator()
101
135
  addUpdateListener { animation ->
102
136
  animationProgress = animation.animatedValue as Float
137
+ val now = System.nanoTime()
138
+ if (now - lastUpdateNanos < MIN_UPDATE_INTERVAL_NANOS) return@addUpdateListener
139
+ lastUpdateNanos = now
103
140
  updateAnimatedPolyline()
104
141
  }
105
142
  start()
@@ -171,6 +208,7 @@ class PolylineAnimator {
171
208
  private fun stopAnimation() {
172
209
  animator?.cancel()
173
210
  animator = null
211
+ lastUpdateNanos = 0L
174
212
  }
175
213
 
176
214
  fun pause() {
@@ -209,8 +247,10 @@ class PolylineAnimator {
209
247
  }
210
248
 
211
249
  if (headDist <= tailDist) {
212
- poly.points = listOf(coordinates.firstOrNull() ?: LatLng(0.0, 0.0))
250
+ val point = coordinates.firstOrNull() ?: LatLng(0.0, 0.0)
213
251
  poly.color = strokeColors.firstOrNull() ?: Color.BLACK
252
+ lastAppliedColor = null
253
+ poly.points = listOf(point, point)
214
254
  return
215
255
  }
216
256
 
@@ -234,11 +274,13 @@ class PolylineAnimator {
234
274
 
235
275
  if (reusablePoints.size < 2) return
236
276
 
237
- poly.points = ArrayList(reusablePoints)
238
-
239
- 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
240
280
  poly.color = strokeColors.firstOrNull() ?: Color.BLACK
241
- } else {
281
+ lastAppliedColor = null
282
+ poly.points = reusablePoints
283
+
242
284
  val segmentCount = reusablePoints.size - 1
243
285
  val spanCount = min(segmentCount, MAX_ANIMATION_SPANS)
244
286
  val segmentsPerSpan = segmentCount.toDouble() / spanCount
@@ -250,16 +292,28 @@ class PolylineAnimator {
250
292
  reusableSpans.add(StyleSpan(StrokeStyle.colorBuilder(color).build(), segmentsPerSpan))
251
293
  }
252
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
253
302
  }
254
303
  }
255
304
 
256
- private fun createGradientSpans(): List<StyleSpan> {
305
+ private fun applyGradientSpans(poly: Polyline) {
257
306
  val segmentCount = coordinates.size - 1
258
- return (0 until segmentCount).map { i ->
259
- val position = i.toFloat() / segmentCount
307
+ val spanCount = min(segmentCount, MAX_GRADIENT_SPANS)
308
+ val segmentsPerSpan = segmentCount.toDouble() / spanCount
309
+
310
+ reusableSpans.clear()
311
+ for (i in 0 until spanCount) {
312
+ val position = (i + 0.5f) / spanCount
260
313
  val color = colorAtGradientPosition(position)
261
- StyleSpan(StrokeStyle.colorBuilder(color).build())
314
+ reusableSpans.add(StyleSpan(StrokeStyle.colorBuilder(color).build(), segmentsPerSpan))
262
315
  }
316
+ poly.setSpans(reusableSpans)
263
317
  }
264
318
 
265
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