@lugg/maps 1.0.0-beta.16 → 1.0.0-beta.18

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 (41) hide show
  1. package/android/src/main/java/com/luggmaps/LuggMapView.kt +33 -0
  2. package/android/src/main/java/com/luggmaps/LuggMapViewManager.kt +9 -0
  3. package/android/src/main/java/com/luggmaps/LuggMapWrapperView.kt +20 -2
  4. package/android/src/main/java/com/luggmaps/core/GoogleMapProvider.kt +29 -3
  5. package/ios/LuggMapView.mm +152 -2
  6. package/ios/LuggMapWrapperView.h +5 -0
  7. package/ios/LuggMapWrapperView.mm +18 -0
  8. package/ios/LuggMarkerView.mm +2 -0
  9. package/ios/core/AppleMapProvider.h +8 -0
  10. package/ios/core/AppleMapProvider.mm +41 -28
  11. package/ios/core/AppleStaticMapProvider.h +14 -0
  12. package/ios/core/AppleStaticMapProvider.mm +146 -0
  13. package/ios/core/GMSPolylineAnimator.m +40 -15
  14. package/ios/core/GoogleMapProvider.h +10 -0
  15. package/ios/core/GoogleMapProvider.mm +47 -41
  16. package/ios/core/GoogleStaticMapProvider.h +21 -0
  17. package/ios/core/GoogleStaticMapProvider.mm +204 -0
  18. package/ios/core/MKPolylineAnimator.m +67 -43
  19. package/ios/core/MapProviderDelegate.h +6 -0
  20. package/ios/core/StaticMapProviderBase.h +69 -0
  21. package/ios/core/StaticMapProviderBase.mm +745 -0
  22. package/ios/extensions/MKMapView+Zoom.h +14 -0
  23. package/ios/extensions/MKMapView+Zoom.m +17 -15
  24. package/ios/extensions/UIView+Snapshot.h +16 -0
  25. package/ios/extensions/UIView+Snapshot.m +27 -0
  26. package/lib/module/MapView.js +4 -0
  27. package/lib/module/MapView.js.map +1 -1
  28. package/lib/module/MapView.web.js +8 -5
  29. package/lib/module/MapView.web.js.map +1 -1
  30. package/lib/module/fabric/LuggMapViewNativeComponent.ts +2 -0
  31. package/lib/typescript/src/MapView.d.ts.map +1 -1
  32. package/lib/typescript/src/MapView.types.d.ts +18 -0
  33. package/lib/typescript/src/MapView.types.d.ts.map +1 -1
  34. package/lib/typescript/src/MapView.web.d.ts.map +1 -1
  35. package/lib/typescript/src/fabric/LuggMapViewNativeComponent.d.ts +2 -0
  36. package/lib/typescript/src/fabric/LuggMapViewNativeComponent.d.ts.map +1 -1
  37. package/package.json +1 -1
  38. package/src/MapView.tsx +4 -0
  39. package/src/MapView.types.ts +18 -0
  40. package/src/MapView.web.tsx +14 -10
  41. package/src/fabric/LuggMapViewNativeComponent.ts +2 -0
@@ -1,6 +1,7 @@
1
1
  package com.luggmaps
2
2
 
3
3
  import android.annotation.SuppressLint
4
+ import android.content.res.Configuration
4
5
  import android.view.View
5
6
  import android.view.View.VISIBLE
6
7
  import com.facebook.react.uimanager.ThemedReactContext
@@ -65,6 +66,7 @@ class LuggMapView(private val reactContext: ThemedReactContext) :
65
66
  private var rotateEnabled: Boolean = true
66
67
  private var pitchEnabled: Boolean = true
67
68
  private var compassEnabled: Boolean = true
69
+ private var staticMode: Boolean = false
68
70
  private var userLocationEnabled: Boolean = false
69
71
  private var userLocationButtonEnabled: Boolean = false
70
72
  private var poiEnabled: Boolean = true
@@ -132,8 +134,13 @@ class LuggMapView(private val reactContext: ThemedReactContext) :
132
134
  private fun initializeProvider() {
133
135
  if (provider != null || mapWrapperView == null) return
134
136
 
137
+ if (staticMode) {
138
+ applyStaticPlaceholder()
139
+ }
140
+
135
141
  val google = GoogleMapProvider(context)
136
142
  google.mapId = mapId
143
+ google.staticMode = staticMode
137
144
  google.delegate = this
138
145
  provider = google
139
146
 
@@ -154,6 +161,23 @@ class LuggMapView(private val reactContext: ThemedReactContext) :
154
161
  }
155
162
  }
156
163
 
164
+ // Shown while the map loads, until the map covers it
165
+ private fun applyStaticPlaceholder() {
166
+ // A user-provided background acts as the placeholder
167
+ if (background != null) return
168
+
169
+ val isDark = when (theme) {
170
+ "dark" -> true
171
+
172
+ "light" -> false
173
+
174
+ else ->
175
+ (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) ==
176
+ Configuration.UI_MODE_NIGHT_YES
177
+ }
178
+ mapWrapperView?.setBackgroundColor(if (isDark) COLOR_PLACEHOLDER_DARK else COLOR_PLACEHOLDER_LIGHT)
179
+ }
180
+
157
181
  // endregion
158
182
 
159
183
  // region MapProviderDelegate
@@ -251,6 +275,10 @@ class LuggMapView(private val reactContext: ThemedReactContext) :
251
275
  provider?.setCompassEnabled(enabled)
252
276
  }
253
277
 
278
+ fun setStaticMode(enabled: Boolean) {
279
+ staticMode = enabled
280
+ }
281
+
254
282
  fun setUserLocationEnabled(enabled: Boolean) {
255
283
  if (userLocationEnabled == enabled) return
256
284
  userLocationEnabled = enabled
@@ -344,4 +372,9 @@ class LuggMapView(private val reactContext: ThemedReactContext) :
344
372
  }
345
373
 
346
374
  // endregion
375
+
376
+ companion object {
377
+ private const val COLOR_PLACEHOLDER_LIGHT = 0xFFF2F2F7.toInt()
378
+ private const val COLOR_PLACEHOLDER_DARK = 0xFF2C2C2E.toInt()
379
+ }
347
380
  }
@@ -137,6 +137,15 @@ class LuggMapViewManager :
137
137
  view.setCompassEnabled(value)
138
138
  }
139
139
 
140
+ @ReactProp(name = "staticMode", defaultBoolean = false)
141
+ override fun setStaticMode(view: LuggMapView, value: Boolean) {
142
+ view.setStaticMode(value)
143
+ }
144
+
145
+ // iOS only - lite mode doesn't need snapshot caching
146
+ @ReactProp(name = "staticKey")
147
+ override fun setStaticKey(view: LuggMapView, value: String?) = Unit
148
+
140
149
  @ReactProp(name = "userLocationEnabled", defaultBoolean = false)
141
150
  override fun setUserLocationEnabled(view: LuggMapView, value: Boolean) {
142
151
  view.setUserLocationEnabled(value)
@@ -10,11 +10,26 @@ class LuggMapWrapperView(context: ThemedReactContext) : ReactViewGroup(context)
10
10
 
11
11
  var touchEventHandler: ((MotionEvent) -> Unit)? = null
12
12
 
13
+ // React Native never runs an Android layout pass on Yoga-managed views, so
14
+ // a child's requestLayout (e.g. a lite mode MapView displaying its bitmap
15
+ // once loaded) would otherwise never result in a measure/layout. Live maps
16
+ // don't need this - their GL surface renders continuously once sized.
17
+ var relayoutChildOnRequest: Boolean = false
18
+
19
+ private val measureAndLayoutChild = Runnable { layoutChild(width, height) }
20
+
13
21
  override fun dispatchTouchEvent(event: MotionEvent): Boolean {
14
22
  touchEventHandler?.invoke(event)
15
23
  return super.dispatchTouchEvent(event)
16
24
  }
17
25
 
26
+ override fun requestLayout() {
27
+ super.requestLayout()
28
+ if (relayoutChildOnRequest) {
29
+ post(measureAndLayoutChild)
30
+ }
31
+ }
32
+
18
33
  override fun onLayout(
19
34
  changed: Boolean,
20
35
  left: Int,
@@ -23,8 +38,11 @@ class LuggMapWrapperView(context: ThemedReactContext) : ReactViewGroup(context)
23
38
  bottom: Int
24
39
  ) {
25
40
  super.onLayout(changed, left, top, right, bottom)
26
- val w = right - left
27
- val h = bottom - top
41
+ layoutChild(right - left, bottom - top)
42
+ }
43
+
44
+ private fun layoutChild(w: Int, h: Int) {
45
+ if (w <= 0 || h <= 0) return
28
46
  getChildAt(0)?.let {
29
47
  it.measure(
30
48
  MeasureSpec.makeMeasureSpec(w, MeasureSpec.EXACTLY),
@@ -78,6 +78,9 @@ class GoogleMapProvider(private val context: Context) :
78
78
 
79
79
  var mapId: String = DEMO_MAP_ID
80
80
 
81
+ // Renders the map in lite mode (static bitmap). Creation-time only.
82
+ var staticMode: Boolean = false
83
+
81
84
  private var wrapperView: LuggMapWrapperView? = null
82
85
  private var currentNightMode: Int = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
83
86
  private var mapView: MapView? = null
@@ -96,6 +99,7 @@ class GoogleMapProvider(private val context: Context) :
96
99
  private val groundOverlayToViewMap = mutableMapOf<GroundOverlay, LuggGroundOverlayView>()
97
100
  private val markerToViewMap = mutableMapOf<Marker, LuggMarkerView>()
98
101
  private val liveMarkerViews = mutableSetOf<LuggMarkerView>()
102
+ private var liteProjectionReady = false
99
103
  private var activeNonBubbledMarker: Marker? = null
100
104
  private var tapLocation: LatLng? = null
101
105
 
@@ -137,11 +141,15 @@ class GoogleMapProvider(private val context: Context) :
137
141
  initialZoom = zoom
138
142
 
139
143
  val wrapper = wrapperView as LuggMapWrapperView
144
+ wrapper.relayoutChildOnRequest = staticMode
140
145
  this.wrapperView = wrapper
141
146
 
142
147
  context.applicationContext.registerComponentCallbacks(this)
143
148
 
144
- val options = GoogleMapOptions().mapId(mapId)
149
+ // Lite mode doesn't support map IDs (cloud-based styling); setting one
150
+ // makes the static map render blank
151
+ val options = GoogleMapOptions().liteMode(staticMode)
152
+ if (!staticMode) options.mapId(mapId)
145
153
  mapView = MapView(context, options).also { view ->
146
154
  view.onCreate(null)
147
155
  view.onResume()
@@ -195,6 +203,17 @@ class GoogleMapProvider(private val context: Context) :
195
203
  googleMap = map
196
204
  _isMapReady = true
197
205
 
206
+ if (staticMode) {
207
+ // Lite mode shows an "open in Google Maps" toolbar on tap by default
208
+ map.uiSettings.isMapToolbarEnabled = false
209
+ // The projection is only valid once the lite map has rendered, and no
210
+ // camera events fire afterwards to correct live marker positions
211
+ map.setOnMapLoadedCallback {
212
+ liteProjectionReady = true
213
+ positionLiveMarkers()
214
+ }
215
+ }
216
+
198
217
  val position = LatLng(initialLatitude, initialLongitude)
199
218
  map.moveCamera(CameraUpdateFactory.newLatLngZoom(position, initialZoom))
200
219
 
@@ -737,6 +756,10 @@ class GoogleMapProvider(private val context: Context) :
737
756
 
738
757
  val contentView = markerView.contentView
739
758
  contentView.pointerEvents = com.facebook.react.uimanager.PointerEvents.NONE
759
+ // Until the lite map's projection is valid, hide the marker instead of
760
+ // flashing it at a wrong position; onMapLoaded positions and shows it
761
+ contentView.visibility =
762
+ if (staticMode && !liteProjectionReady) View.INVISIBLE else View.VISIBLE
740
763
  (contentView.parent as? android.view.ViewGroup)?.removeView(contentView)
741
764
  wrapper.addView(contentView)
742
765
  liveMarkerViews.add(markerView)
@@ -768,10 +791,12 @@ class GoogleMapProvider(private val context: Context) :
768
791
 
769
792
  private fun positionLiveMarker(markerView: LuggMarkerView) {
770
793
  val map = googleMap ?: return
794
+ if (staticMode && !liteProjectionReady) return
771
795
  val contentView = markerView.contentView
772
796
  val point = map.projection.toScreenLocation(LatLng(markerView.latitude, markerView.longitude))
773
797
  contentView.translationX = point.x - contentView.width * markerView.anchorX
774
798
  contentView.translationY = point.y - contentView.height * markerView.anchorY
799
+ contentView.visibility = View.VISIBLE
775
800
  }
776
801
 
777
802
  // endregion
@@ -813,7 +838,7 @@ class GoogleMapProvider(private val context: Context) :
813
838
  strokeColors = polylineView.strokeColors
814
839
  strokeWidth = polylineView.strokeWidth.dpToPx()
815
840
  animatedOptions = polylineView.animatedOptions
816
- animated = polylineView.animated
841
+ animated = polylineView.animated && !staticMode
817
842
  update()
818
843
  }
819
844
  }
@@ -842,7 +867,8 @@ class GoogleMapProvider(private val context: Context) :
842
867
  strokeColors = polylineView.strokeColors
843
868
  strokeWidth = polylineView.strokeWidth.dpToPx()
844
869
  animatedOptions = polylineView.animatedOptions
845
- animated = polylineView.animated
870
+ // Static maps render once; show the full polyline instead of animating
871
+ animated = polylineView.animated && !staticMode
846
872
  update()
847
873
  }
848
874
 
@@ -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
 
@@ -40,6 +55,9 @@ using namespace luggmaps::events;
40
55
  BOOL _rotateEnabled;
41
56
  BOOL _pitchEnabled;
42
57
  BOOL _compassEnabled;
58
+ BOOL _staticMode;
59
+ NSString *_staticKey;
60
+ BOOL _staticSnapshotDone;
43
61
  BOOL _userLocationEnabled;
44
62
  LuggMapViewMapType _mapType;
45
63
  LuggMapViewTheme _theme;
@@ -70,6 +88,8 @@ using namespace luggmaps::events;
70
88
  _rotateEnabled = YES;
71
89
  _pitchEnabled = YES;
72
90
  _compassEnabled = YES;
91
+ _staticMode = NO;
92
+ _staticKey = @"";
73
93
  _userLocationEnabled = NO;
74
94
  _poiEnabled = NO;
75
95
  _poiFilterMode = LuggMapViewPoiFilterMode::Including;
@@ -170,17 +190,109 @@ using namespace luggmaps::events;
170
190
  [super didMoveToWindow];
171
191
  if (self.window) {
172
192
  if (!_provider && _mapWrapperView) {
173
- [self initializeProvider];
193
+ if (_staticMode) {
194
+ [self startStaticContent];
195
+ } else {
196
+ [self initializeProvider];
197
+ }
174
198
  }
175
199
  [_provider resumeAnimations];
176
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
+ }
177
209
  [_provider pauseAnimations];
178
210
  }
179
211
  }
180
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
252
+ const auto &viewProps =
253
+ *std::static_pointer_cast<LuggMapViewProps const>(_props);
254
+
255
+ return [NSString stringWithFormat:@"%@|%d|%@|%d|%ld|%.0fx%.0f|%.6f,%.6f,%.2f",
256
+ _staticKey, (int)_providerType, _mapId,
257
+ (int)_mapType, (long)style, size.width,
258
+ size.height,
259
+ viewProps.initialCoordinate.latitude,
260
+ viewProps.initialCoordinate.longitude,
261
+ viewProps.initialZoom];
262
+ }
263
+
264
+ - (void)startStaticContent {
265
+ [self applyStaticPlaceholderBackground];
266
+
267
+ // Both static providers render fully async: the placeholder shows until
268
+ // the snapshotter (Apple) or a brief live-map warmup (Google) delivers
269
+ // the base map, which reveals together with the marker/shape overlays
270
+ [self initializeProvider];
271
+ }
272
+
273
+ // staticKey identifies the map's content; when it changes (e.g. a recycled
274
+ // list row bound to a new item), any live warmup or displayed snapshot is
275
+ // stale and the static lifecycle restarts. Without this, an in-flight
276
+ // warmup caches the old item's map under the new key.
277
+ - (void)resetStaticContent {
278
+ _staticSnapshotDone = NO;
279
+
280
+ if (_provider) {
281
+ [_provider destroy];
282
+ _provider = nil;
283
+ _initialized = NO;
284
+ }
285
+
286
+ if (self.window) {
287
+ [self startStaticContent];
288
+ }
289
+ }
290
+
181
291
  - (void)prepareForRecycle {
182
292
  [super prepareForRecycle];
183
293
 
294
+ _staticSnapshotDone = NO;
295
+
184
296
  [_provider destroy];
185
297
  _provider = nil;
186
298
  _mapWrapperView = nil;
@@ -197,14 +309,28 @@ using namespace luggmaps::events;
197
309
  *std::static_pointer_cast<LuggMapViewProps const>(_props);
198
310
 
199
311
  if (_providerType == LuggMapViewProvider::Apple) {
200
- _provider = [[AppleMapProvider alloc] init];
312
+ _provider = _staticMode ? [[AppleStaticMapProvider alloc] init]
313
+ : [[AppleMapProvider alloc] init];
314
+ } else if (_staticMode) {
315
+ GoogleStaticMapProvider *google = [[GoogleStaticMapProvider alloc] init];
316
+ google.mapId = _mapId;
317
+ _provider = google;
201
318
  } else {
202
319
  GoogleMapProvider *google = [[GoogleMapProvider alloc] init];
203
320
  google.mapId = _mapId;
204
321
  _provider = google;
205
322
  }
206
323
 
324
+ if (_staticMode) {
325
+ NSString *cacheKey = [self staticSnapshotCacheKey];
326
+ if (cacheKey) {
327
+ ((StaticMapProviderBase *)_provider).cachedBaseImage =
328
+ [StaticSnapshotCache() objectForKey:cacheKey];
329
+ }
330
+ }
331
+
207
332
  _provider.delegate = self;
333
+ _provider.staticMode = _staticMode;
208
334
 
209
335
  CLLocationCoordinate2D coordinate =
210
336
  CLLocationCoordinate2DMake(viewProps.initialCoordinate.latitude,
@@ -244,6 +370,19 @@ using namespace luggmaps::events;
244
370
  ReadyEvent::emit<LuggMapViewEventEmitter>(_eventEmitter);
245
371
  }
246
372
 
373
+ - (void)mapProviderDidFinishStaticSnapshot {
374
+ _staticSnapshotDone = YES;
375
+ }
376
+
377
+ - (void)mapProviderDidCaptureStaticImage:(UIImage *)image {
378
+ NSString *key = [self staticSnapshotCacheKey];
379
+ if (key) {
380
+ NSUInteger cost = (NSUInteger)(image.size.width * image.scale *
381
+ image.size.height * image.scale * 4);
382
+ [StaticSnapshotCache() setObject:image forKey:key cost:cost];
383
+ }
384
+ }
385
+
247
386
  - (void)mapProviderDidMoveCamera:(double)latitude
248
387
  longitude:(double)longitude
249
388
  zoom:(double)zoom
@@ -303,6 +442,12 @@ using namespace luggmaps::events;
303
442
  *std::static_pointer_cast<LuggMapViewProps const>(_props);
304
443
 
305
444
  _providerType = newViewProps.provider;
445
+ _staticMode = newViewProps.staticMode;
446
+
447
+ NSString *newStaticKey =
448
+ [NSString stringWithUTF8String:newViewProps.staticKey.c_str()];
449
+ BOOL staticKeyChanged = ![newStaticKey isEqualToString:_staticKey];
450
+ _staticKey = newStaticKey;
306
451
 
307
452
  NSString *newMapId =
308
453
  [NSString stringWithUTF8String:newViewProps.mapId.c_str()];
@@ -381,6 +526,11 @@ using namespace luggmaps::events;
381
526
  }
382
527
 
383
528
  [super updateProps:props oldProps:oldProps];
529
+
530
+ // After super so the snapshot cache key sees the new camera props
531
+ if (staticKeyChanged && _staticMode && _mapWrapperView) {
532
+ [self resetStaticContent];
533
+ }
384
534
  }
385
535
 
386
536
  #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
@@ -342,33 +352,36 @@ static MKPointOfInterestCategory poiCategoryFromString(NSString *string) {
342
352
  return map[string];
343
353
  }
344
354
 
345
- - (void)applyPoiFilter {
346
- if (!_poiEnabled) {
347
- _mapView.pointOfInterestFilter =
348
- MKPointOfInterestFilter.filterExcludingAllCategories;
349
- return;
355
+ MKPointOfInterestFilter *_Nullable LuggPointOfInterestFilter(
356
+ BOOL poiEnabled, LuggMapViewPoiFilterMode filterMode,
357
+ NSArray<NSString *> *filterCategories) {
358
+ if (!poiEnabled) {
359
+ return MKPointOfInterestFilter.filterExcludingAllCategories;
350
360
  }
351
- if (_poiFilterCategories.count > 0) {
361
+ if (filterCategories.count > 0) {
352
362
  NSMutableArray<MKPointOfInterestCategory> *categories =
353
363
  [NSMutableArray array];
354
- for (NSString *name in _poiFilterCategories) {
364
+ for (NSString *name in filterCategories) {
355
365
  MKPointOfInterestCategory category = poiCategoryFromString(name);
356
366
  if (category) {
357
367
  [categories addObject:category];
358
368
  }
359
369
  }
360
370
  if (categories.count > 0) {
361
- if (_poiFilterMode == LuggMapViewPoiFilterMode::Excluding) {
362
- _mapView.pointOfInterestFilter = [[MKPointOfInterestFilter alloc]
371
+ if (filterMode == LuggMapViewPoiFilterMode::Excluding) {
372
+ return [[MKPointOfInterestFilter alloc]
363
373
  initExcludingCategories:categories];
364
- } else {
365
- _mapView.pointOfInterestFilter = [[MKPointOfInterestFilter alloc]
366
- initIncludingCategories:categories];
367
374
  }
368
- return;
375
+ return
376
+ [[MKPointOfInterestFilter alloc] initIncludingCategories:categories];
369
377
  }
370
378
  }
371
- _mapView.pointOfInterestFilter = nil;
379
+ return nil;
380
+ }
381
+
382
+ - (void)applyPoiFilter {
383
+ _mapView.pointOfInterestFilter = LuggPointOfInterestFilter(
384
+ _poiEnabled, _poiFilterMode, _poiFilterCategories);
372
385
  }
373
386
 
374
387
  - (void)setInsetAdjustment:(LuggMapViewInsetAdjustment)insetAdjustment {
@@ -399,26 +412,25 @@ static MKPointOfInterestCategory poiCategoryFromString(NSString *string) {
399
412
  [self applyPoiFilter];
400
413
  }
401
414
 
402
- - (void)setMapType:(LuggMapViewMapType)mapType {
415
+ MKMapType LuggMKMapTypeFromMapType(LuggMapViewMapType mapType) {
403
416
  switch (mapType) {
404
417
  case LuggMapViewMapType::Satellite:
405
- _mapView.mapType = MKMapTypeSatellite;
406
- break;
418
+ return MKMapTypeSatellite;
407
419
  case LuggMapViewMapType::Terrain:
408
- _mapView.mapType = MKMapTypeStandard;
409
- break;
420
+ return MKMapTypeStandard;
410
421
  case LuggMapViewMapType::Hybrid:
411
- _mapView.mapType = MKMapTypeHybrid;
412
- break;
422
+ return MKMapTypeHybrid;
413
423
  case LuggMapViewMapType::MutedStandard:
414
- _mapView.mapType = MKMapTypeMutedStandard;
415
- break;
424
+ return MKMapTypeMutedStandard;
416
425
  default:
417
- _mapView.mapType = MKMapTypeStandard;
418
- break;
426
+ return MKMapTypeStandard;
419
427
  }
420
428
  }
421
429
 
430
+ - (void)setMapType:(LuggMapViewMapType)mapType {
431
+ _mapView.mapType = LuggMKMapTypeFromMapType(mapType);
432
+ }
433
+
422
434
  - (void)setTheme:(LuggMapViewTheme)theme {
423
435
  switch (theme) {
424
436
  case LuggMapViewTheme::Dark:
@@ -836,7 +848,8 @@ static MKPointOfInterestCategory poiCategoryFromString(NSString *string) {
836
848
  renderer.strokeColors = colors;
837
849
  }
838
850
  renderer.animatedOptions = polylineView.animatedOptions;
839
- renderer.animated = polylineView.animated;
851
+ // Static maps snapshot an arbitrary frame; render the full polyline
852
+ renderer.animated = polylineView.animated && !_staticMode;
840
853
  polylineView.renderer = renderer;
841
854
  return renderer;
842
855
  }
@@ -1365,7 +1378,7 @@ static MKPointOfInterestCategory poiCategoryFromString(NSString *string) {
1365
1378
  renderer.strokeColors =
1366
1379
  polylineView.strokeColors.count > 1 ? polylineView.strokeColors : nil;
1367
1380
  renderer.animatedOptions = polylineView.animatedOptions;
1368
- renderer.animated = polylineView.animated;
1381
+ renderer.animated = polylineView.animated && !_staticMode;
1369
1382
  return;
1370
1383
  }
1371
1384
 
@@ -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