@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
@@ -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;
@@ -188,14 +311,28 @@ using namespace luggmaps::events;
188
311
  #pragma mark - Provider Initialization
189
312
 
190
313
  - (void)initializeProvider {
191
- if (_provider || !_mapWrapperView)
192
- return;
193
-
194
314
  const auto &viewProps =
195
315
  *std::static_pointer_cast<LuggMapViewProps const>(_props);
316
+ [self
317
+ initializeProviderWithCoordinate:CLLocationCoordinate2DMake(
318
+ viewProps.initialCoordinate.latitude,
319
+ viewProps.initialCoordinate
320
+ .longitude)
321
+ zoom:viewProps.initialZoom];
322
+ }
323
+
324
+ - (void)initializeProviderWithCoordinate:(CLLocationCoordinate2D)coordinate
325
+ zoom:(double)zoom {
326
+ if (_provider || !_mapWrapperView)
327
+ return;
196
328
 
197
329
  if (_providerType == LuggMapViewProvider::Apple) {
198
- _provider = [[AppleMapProvider alloc] init];
330
+ _provider = _staticMode ? [[AppleStaticMapProvider alloc] init]
331
+ : [[AppleMapProvider alloc] init];
332
+ } else if (_staticMode) {
333
+ GoogleStaticMapProvider *google = [[GoogleStaticMapProvider alloc] init];
334
+ google.mapId = _mapId;
335
+ _provider = google;
199
336
  } else {
200
337
  GoogleMapProvider *google = [[GoogleMapProvider alloc] init];
201
338
  google.mapId = _mapId;
@@ -203,14 +340,21 @@ using namespace luggmaps::events;
203
340
  }
204
341
 
205
342
  _provider.delegate = self;
206
-
207
- CLLocationCoordinate2D coordinate =
208
- CLLocationCoordinate2DMake(viewProps.initialCoordinate.latitude,
209
- viewProps.initialCoordinate.longitude);
343
+ _provider.staticMode = _staticMode;
210
344
 
211
345
  [_provider initializeMapInView:_mapWrapperView
212
346
  initialCoordinate:coordinate
213
- initialZoom:viewProps.initialZoom];
347
+ initialZoom:zoom];
348
+
349
+ // After initializeMapInView so the cache key reads the provider's camera;
350
+ // the base render is async and picks up the cached image
351
+ if (_staticMode) {
352
+ NSString *cacheKey = [self staticSnapshotCacheKey];
353
+ if (cacheKey) {
354
+ ((StaticMapProviderBase *)_provider).cachedBaseImage =
355
+ [StaticSnapshotCache() objectForKey:cacheKey];
356
+ }
357
+ }
214
358
 
215
359
  // Apply cached props after map view is created
216
360
  [self applyProps];
@@ -242,6 +386,19 @@ using namespace luggmaps::events;
242
386
  ReadyEvent::emit<LuggMapViewEventEmitter>(_eventEmitter);
243
387
  }
244
388
 
389
+ - (void)mapProviderDidFinishStaticSnapshot {
390
+ _staticSnapshotDone = YES;
391
+ }
392
+
393
+ - (void)mapProviderDidCaptureStaticImage:(UIImage *)image {
394
+ NSString *key = [self staticSnapshotCacheKey];
395
+ if (key) {
396
+ NSUInteger cost = (NSUInteger)(image.size.width * image.scale *
397
+ image.size.height * image.scale * 4);
398
+ [StaticSnapshotCache() setObject:image forKey:key cost:cost];
399
+ }
400
+ }
401
+
245
402
  - (void)mapProviderDidMoveCamera:(double)latitude
246
403
  longitude:(double)longitude
247
404
  zoom:(double)zoom
@@ -281,6 +438,7 @@ using namespace luggmaps::events;
281
438
  [_provider setScrollEnabled:_scrollEnabled];
282
439
  [_provider setRotateEnabled:_rotateEnabled];
283
440
  [_provider setPitchEnabled:_pitchEnabled];
441
+ [_provider setCompassEnabled:_compassEnabled];
284
442
  [_provider setUserLocationEnabled:_userLocationEnabled];
285
443
  [_provider setMapType:_mapType];
286
444
  [_provider setTheme:_theme];
@@ -300,6 +458,12 @@ using namespace luggmaps::events;
300
458
  *std::static_pointer_cast<LuggMapViewProps const>(_props);
301
459
 
302
460
  _providerType = newViewProps.provider;
461
+ _staticMode = newViewProps.staticMode;
462
+
463
+ NSString *newStaticKey =
464
+ [NSString stringWithUTF8String:newViewProps.staticKey.c_str()];
465
+ BOOL staticKeyChanged = ![newStaticKey isEqualToString:_staticKey];
466
+ _staticKey = newStaticKey;
303
467
 
304
468
  NSString *newMapId =
305
469
  [NSString stringWithUTF8String:newViewProps.mapId.c_str()];
@@ -323,6 +487,10 @@ using namespace luggmaps::events;
323
487
  _pitchEnabled = newViewProps.pitchEnabled;
324
488
  [_provider setPitchEnabled:_pitchEnabled];
325
489
  }
490
+ if (newViewProps.compassEnabled != prevViewProps.compassEnabled) {
491
+ _compassEnabled = newViewProps.compassEnabled;
492
+ [_provider setCompassEnabled:_compassEnabled];
493
+ }
326
494
  if (newViewProps.userLocationEnabled != prevViewProps.userLocationEnabled) {
327
495
  _userLocationEnabled = newViewProps.userLocationEnabled;
328
496
  [_provider setUserLocationEnabled:_userLocationEnabled];
@@ -374,6 +542,11 @@ using namespace luggmaps::events;
374
542
  }
375
543
 
376
544
  [super updateProps:props oldProps:oldProps];
545
+
546
+ // After super so the snapshot cache key sees the new camera props
547
+ if (staticKeyChanged && _staticMode && _mapWrapperView) {
548
+ [self resetStaticContent];
549
+ }
377
550
  }
378
551
 
379
552
  #pragma mark - Commands
@@ -414,6 +587,63 @@ using namespace luggmaps::events;
414
587
  duration:duration];
415
588
  }
416
589
 
590
+ // Loads the map again, e.g. to recover from missing tiles. A static map
591
+ // drops its cached snapshot and renders the base map again in place.
592
+ // Neither SDK can reload a live map's tiles, so the native map view is
593
+ // recreated at the current camera (coordinate and zoom; heading and pitch
594
+ // reset) with children re-added.
595
+ - (void)reload {
596
+ if (_staticMode) {
597
+ _staticSnapshotDone = NO;
598
+ NSString *cacheKey = [self staticSnapshotCacheKey];
599
+ if (cacheKey) {
600
+ [StaticSnapshotCache() removeObjectForKey:cacheKey];
601
+ }
602
+ if ([_provider isKindOfClass:[StaticMapProviderBase class]]) {
603
+ [(StaticMapProviderBase *)_provider rerenderBaseMap];
604
+ }
605
+ return;
606
+ }
607
+
608
+ if (!_provider)
609
+ return;
610
+
611
+ CLLocationCoordinate2D coordinate = _provider.coordinate;
612
+ double zoom = _provider.zoom;
613
+ // Children keep their native marker/overlay objects from the old map;
614
+ // detach them so the new provider creates fresh ones on the flush
615
+ [self removeChildrenFromProvider];
616
+ [_provider destroy];
617
+ _provider = nil;
618
+ _initialized = NO;
619
+ [self initializeProviderWithCoordinate:coordinate zoom:zoom];
620
+ if (_providerType == LuggMapViewProvider::Apple) {
621
+ // The captured camera already includes the previous inset offset.
622
+ [_provider moveCamera:coordinate.latitude
623
+ longitude:coordinate.longitude
624
+ zoom:zoom
625
+ duration:0];
626
+ }
627
+ }
628
+
629
+ - (void)removeChildrenFromProvider {
630
+ for (UIView *subview in self.subviews) {
631
+ if ([subview isKindOfClass:[LuggMarkerView class]]) {
632
+ [_provider removeMarkerView:(LuggMarkerView *)subview];
633
+ } else if ([subview isKindOfClass:[LuggPolylineView class]]) {
634
+ [_provider removePolylineView:(LuggPolylineView *)subview];
635
+ } else if ([subview isKindOfClass:[LuggPolygonView class]]) {
636
+ [_provider removePolygonView:(LuggPolygonView *)subview];
637
+ } else if ([subview isKindOfClass:[LuggCircleView class]]) {
638
+ [_provider removeCircleView:(LuggCircleView *)subview];
639
+ } else if ([subview isKindOfClass:[LuggGroundOverlayView class]]) {
640
+ [_provider removeGroundOverlayView:(LuggGroundOverlayView *)subview];
641
+ } else if ([subview isKindOfClass:[LuggTileOverlayView class]]) {
642
+ [_provider removeTileOverlayView:(LuggTileOverlayView *)subview];
643
+ }
644
+ }
645
+ }
646
+
417
647
  - (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args {
418
648
  RCTLuggMapViewHandleCommand(self, commandName, args);
419
649
  }
@@ -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