@lugg/maps 1.0.0 → 1.1.1

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/LuggMaps.podspec +14 -1
  2. package/README.md +10 -0
  3. package/android/src/main/java/com/luggmaps/LuggMapView.kt +15 -2
  4. package/android/src/main/java/com/luggmaps/LuggMapViewManager.kt +4 -0
  5. package/android/src/main/java/com/luggmaps/LuggMapWrapperView.kt +9 -0
  6. package/android/src/main/java/com/luggmaps/core/GoogleMapProvider.kt +25 -1
  7. package/android/src/main/java/com/luggmaps/core/MapProviderDelegate.kt +5 -0
  8. package/ios/LuggMapView.mm +81 -8
  9. package/ios/core/AppleMapProvider.mm +15 -16
  10. package/ios/core/AppleStaticMapProvider.mm +74 -0
  11. package/ios/core/GoogleMapProvider.mm +11 -10
  12. package/ios/core/GoogleStaticMapProvider.mm +20 -16
  13. package/ios/core/MapEdgeInsets.h +18 -0
  14. package/ios/core/MapProviderDelegate.h +4 -0
  15. package/ios/core/StaticMapProviderBase.h +5 -0
  16. package/ios/core/StaticMapProviderBase.mm +8 -6
  17. package/lib/module/MapView.js +5 -0
  18. package/lib/module/MapView.js.map +1 -1
  19. package/lib/module/MapView.web.js +20 -6
  20. package/lib/module/MapView.web.js.map +1 -1
  21. package/lib/module/fabric/LuggMapViewNativeComponent.ts +7 -1
  22. package/lib/typescript/plugin/src/index.d.ts +5 -0
  23. package/lib/typescript/plugin/src/index.d.ts.map +1 -1
  24. package/lib/typescript/plugin/src/withLuggMapsIOS.d.ts +1 -0
  25. package/lib/typescript/plugin/src/withLuggMapsIOS.d.ts.map +1 -1
  26. package/lib/typescript/src/MapView.d.ts +1 -0
  27. package/lib/typescript/src/MapView.d.ts.map +1 -1
  28. package/lib/typescript/src/MapView.types.d.ts +9 -1
  29. package/lib/typescript/src/MapView.types.d.ts.map +1 -1
  30. package/lib/typescript/src/MapView.web.d.ts.map +1 -1
  31. package/lib/typescript/src/fabric/LuggMapViewNativeComponent.d.ts +1 -0
  32. package/lib/typescript/src/fabric/LuggMapViewNativeComponent.d.ts.map +1 -1
  33. package/package.json +6 -3
  34. package/plugin/build/index.d.ts +5 -0
  35. package/plugin/build/index.js +5 -2
  36. package/plugin/build/withLuggMapsIOS.d.ts +1 -0
  37. package/plugin/build/withLuggMapsIOS.js +26 -4
  38. package/src/MapView.tsx +7 -0
  39. package/src/MapView.types.ts +9 -1
  40. package/src/MapView.web.tsx +24 -5
  41. package/src/fabric/LuggMapViewNativeComponent.ts +7 -1
package/LuggMaps.podspec CHANGED
@@ -16,7 +16,20 @@ Pod::Spec.new do |s|
16
16
  s.source_files = "ios/**/*.{h,m,mm,swift,cpp}"
17
17
  s.private_header_files = "ios/**/*.h"
18
18
 
19
- s.dependency "GoogleMaps"
19
+ # `$LuggMapsGoogleEnabled = false` in the Podfile drops the Google Maps SDK (Apple Maps only)
20
+ google_enabled = defined?($LuggMapsGoogleEnabled) ? $LuggMapsGoogleEnabled : true
21
+ s.pod_target_xcconfig = {
22
+ "GCC_PREPROCESSOR_DEFINITIONS" => "$(inherited) LUGG_GOOGLE_MAPS_ENABLED=#{google_enabled ? 1 : 0}"
23
+ }
24
+ if google_enabled
25
+ s.dependency "GoogleMaps"
26
+ else
27
+ unless defined?($LuggMapsGoogleDisabledLogged)
28
+ Pod::UI.puts "[LuggMaps] Google Maps SDK is disabled, Apple Maps only.".yellow
29
+ $LuggMapsGoogleDisabledLogged = true
30
+ end
31
+ s.exclude_files = "ios/core/Google*", "ios/core/GMS*"
32
+ end
20
33
  s.frameworks = "MapKit"
21
34
 
22
35
  install_modules_dependencies(s)
package/README.md CHANGED
@@ -40,6 +40,16 @@ import { MapView, Marker } from '@lugg/maps';
40
40
 
41
41
  Google Maps needs an API key on every platform. See [Installation](https://maps.lodev09.com/installation) for Expo, iOS, Android, and web setup.
42
42
 
43
+ ## AI Skills
44
+
45
+ Skills give your AI coding agent working knowledge of `@lugg/maps` - setup, camera control, markers and callouts, static maps, advanced patterns, and platform limitations - so it generates correct code without you explaining the library each time.
46
+
47
+ ```sh
48
+ npx skills add lugg/maps
49
+ ```
50
+
51
+ This installs the **Maps Usage** skill into your project. The source lives in [`skills/maps-usage`](skills/maps-usage).
52
+
43
53
  ## Contributing
44
54
 
45
55
  - [Development workflow](CONTRIBUTING.md#development-workflow)
@@ -131,7 +131,7 @@ class LuggMapView(private val reactContext: ThemedReactContext) :
131
131
 
132
132
  // region Provider Initialization
133
133
 
134
- private fun initializeProvider() {
134
+ private fun initializeProvider(latitude: Double = initialLatitude, longitude: Double = initialLongitude, zoom: Float = initialZoom) {
135
135
  if (provider != null || mapWrapperView == null) return
136
136
 
137
137
  if (staticMode) {
@@ -146,7 +146,7 @@ class LuggMapView(private val reactContext: ThemedReactContext) :
146
146
 
147
147
  applyProps()
148
148
 
149
- google.initializeMap(mapWrapperView!!, initialLatitude, initialLongitude, initialZoom)
149
+ google.initializeMap(mapWrapperView!!, latitude, longitude, zoom)
150
150
 
151
151
  // Flush children mounted before provider was created
152
152
  for (i in 0 until childCount) {
@@ -371,6 +371,19 @@ class LuggMapView(private val reactContext: ThemedReactContext) :
371
371
  provider?.fitCoordinates(coordinates, edgeInsetsTop, edgeInsetsLeft, edgeInsetsBottom, edgeInsetsRight, duration)
372
372
  }
373
373
 
374
+ fun reload() {
375
+ val current = provider ?: return
376
+ val latitude = current.cameraLatitude
377
+ val longitude = current.cameraLongitude
378
+ val zoom = current.cameraZoom
379
+
380
+ current.destroy()
381
+ provider = null
382
+ if (isAttachedToWindow) {
383
+ initializeProvider(latitude, longitude, zoom)
384
+ }
385
+ }
386
+
374
387
  // endregion
375
388
 
376
389
  companion object {
@@ -273,6 +273,10 @@ class LuggMapViewManager :
273
273
  )
274
274
  }
275
275
 
276
+ override fun reload(view: LuggMapView) {
277
+ view.reload()
278
+ }
279
+
276
280
  companion object {
277
281
  const val NAME = "LuggMapView"
278
282
  }
@@ -2,6 +2,7 @@ package com.luggmaps
2
2
 
3
3
  import android.annotation.SuppressLint
4
4
  import android.view.MotionEvent
5
+ import android.view.View
5
6
  import com.facebook.react.uimanager.ThemedReactContext
6
7
  import com.facebook.react.views.view.ReactViewGroup
7
8
 
@@ -34,6 +35,14 @@ class LuggMapWrapperView(context: ThemedReactContext) : ReactViewGroup(context)
34
35
  }
35
36
  }
36
37
 
38
+ override fun onViewAdded(child: View) {
39
+ super.onViewAdded(child)
40
+ // A replacement map needs sizing even when Yoga's layout is unchanged.
41
+ if (indexOfChild(child) == 0) {
42
+ layoutChild(width, height)
43
+ }
44
+ }
45
+
37
46
  override fun onLayout(
38
47
  changed: Boolean,
39
48
  left: Int,
@@ -128,6 +128,7 @@ class GoogleMapProvider(private val context: Context) :
128
128
 
129
129
  // Theme
130
130
  private var theme: String = "system"
131
+ private var mapType: String = "standard"
131
132
 
132
133
  // Edge Insets
133
134
  private var edgeInsets: EdgeInsets = EdgeInsets()
@@ -183,7 +184,7 @@ class GoogleMapProvider(private val context: Context) :
183
184
  view.onCreate(null)
184
185
  view.onResume()
185
186
  view.getMapAsync(this)
186
- wrapper.addView(view)
187
+ wrapper.addView(view, 0)
187
188
  }
188
189
  wrapper.onLayoutReady = null
189
190
  }
@@ -211,6 +212,9 @@ class GoogleMapProvider(private val context: Context) :
211
212
  groundOverlayToViewMap.clear()
212
213
  markerToViewMap.clear()
213
214
  wrapperView?.touchEventHandler = null
215
+ // The wrapper outlives the provider on reload; don't leave the dead map
216
+ // view in it
217
+ mapView?.let { wrapperView?.removeView(it) }
214
218
  wrapperView = null
215
219
  googleMap?.setOnCameraMoveStartedListener(null)
216
220
  googleMap?.setOnCameraMoveListener(null)
@@ -276,6 +280,7 @@ class GoogleMapProvider(private val context: Context) :
276
280
  applyZoomLimits()
277
281
  applyInsetAdjustment()
278
282
  applyTheme()
283
+ setMapType(mapType)
279
284
  applyUserLocation()
280
285
  processPendingMarkers()
281
286
  processPendingPolylines()
@@ -542,6 +547,7 @@ class GoogleMapProvider(private val context: Context) :
542
547
  }
543
548
 
544
549
  override fun setMapType(value: String) {
550
+ mapType = value
545
551
  googleMap?.mapType = when (value) {
546
552
  "satellite" -> GoogleMap.MAP_TYPE_SATELLITE
547
553
  "terrain" -> GoogleMap.MAP_TYPE_TERRAIN
@@ -1201,6 +1207,24 @@ class GoogleMapProvider(private val context: Context) :
1201
1207
 
1202
1208
  // endregion
1203
1209
 
1210
+ // region Camera
1211
+
1212
+ // A static map's camera is inset-shifted (staticCameraTarget), so report
1213
+ // the requested camera instead
1214
+ private val liveCameraPosition: CameraPosition?
1215
+ get() = if (staticMode) null else googleMap?.cameraPosition
1216
+
1217
+ override val cameraLatitude: Double
1218
+ get() = liveCameraPosition?.target?.latitude ?: initialLatitude
1219
+
1220
+ override val cameraLongitude: Double
1221
+ get() = liveCameraPosition?.target?.longitude ?: initialLongitude
1222
+
1223
+ override val cameraZoom: Float
1224
+ get() = liveCameraPosition?.zoom ?: initialZoom
1225
+
1226
+ // endregion
1227
+
1204
1228
  // region Lifecycle
1205
1229
 
1206
1230
  override fun pauseAnimations() {
@@ -58,6 +58,11 @@ interface MapProvider {
58
58
  fun addTileOverlayView(tileOverlayView: LuggTileOverlayView)
59
59
  fun removeTileOverlayView(tileOverlayView: LuggTileOverlayView)
60
60
 
61
+ // Current camera (the initial camera until the map is ready)
62
+ val cameraLatitude: Double
63
+ val cameraLongitude: Double
64
+ val cameraZoom: Float
65
+
61
66
  // Lifecycle
62
67
  fun pauseAnimations()
63
68
  fun resumeAnimations()
@@ -8,8 +8,10 @@
8
8
  #import "LuggTileOverlayView.h"
9
9
  #import "core/AppleMapProvider.h"
10
10
  #import "core/AppleStaticMapProvider.h"
11
+ #if LUGG_GOOGLE_MAPS_ENABLED
11
12
  #import "core/GoogleMapProvider.h"
12
13
  #import "core/GoogleStaticMapProvider.h"
14
+ #endif
13
15
  #import "core/MapProviderDelegate.h"
14
16
  #import "events/CameraIdleEvent.h"
15
17
  #import "events/CameraMoveEvent.h"
@@ -311,15 +313,25 @@ static NSCache<NSString *, UIImage *> *StaticSnapshotCache(void) {
311
313
  #pragma mark - Provider Initialization
312
314
 
313
315
  - (void)initializeProvider {
314
- if (_provider || !_mapWrapperView)
315
- return;
316
-
317
316
  const auto &viewProps =
318
317
  *std::static_pointer_cast<LuggMapViewProps const>(_props);
318
+ [self
319
+ initializeProviderWithCoordinate:CLLocationCoordinate2DMake(
320
+ viewProps.initialCoordinate.latitude,
321
+ viewProps.initialCoordinate
322
+ .longitude)
323
+ zoom:viewProps.initialZoom];
324
+ }
325
+
326
+ - (void)initializeProviderWithCoordinate:(CLLocationCoordinate2D)coordinate
327
+ zoom:(double)zoom {
328
+ if (_provider || !_mapWrapperView)
329
+ return;
319
330
 
320
331
  if (_providerType == LuggMapViewProvider::Apple) {
321
332
  _provider = _staticMode ? [[AppleStaticMapProvider alloc] init]
322
333
  : [[AppleMapProvider alloc] init];
334
+ #if LUGG_GOOGLE_MAPS_ENABLED
323
335
  } else if (_staticMode) {
324
336
  GoogleStaticMapProvider *google = [[GoogleStaticMapProvider alloc] init];
325
337
  google.mapId = _mapId;
@@ -329,17 +341,21 @@ static NSCache<NSString *, UIImage *> *StaticSnapshotCache(void) {
329
341
  google.mapId = _mapId;
330
342
  _provider = google;
331
343
  }
344
+ #else
345
+ } else {
346
+ NSLog(@"[LuggMaps] provider=\"google\" requested but the Google Maps SDK "
347
+ @"is excluded ($LuggMapsGoogleEnabled = false); using Apple Maps");
348
+ _provider = _staticMode ? [[AppleStaticMapProvider alloc] init]
349
+ : [[AppleMapProvider alloc] init];
350
+ }
351
+ #endif
332
352
 
333
353
  _provider.delegate = self;
334
354
  _provider.staticMode = _staticMode;
335
355
 
336
- CLLocationCoordinate2D coordinate =
337
- CLLocationCoordinate2DMake(viewProps.initialCoordinate.latitude,
338
- viewProps.initialCoordinate.longitude);
339
-
340
356
  [_provider initializeMapInView:_mapWrapperView
341
357
  initialCoordinate:coordinate
342
- initialZoom:viewProps.initialZoom];
358
+ initialZoom:zoom];
343
359
 
344
360
  // After initializeMapInView so the cache key reads the provider's camera;
345
361
  // the base render is async and picks up the cached image
@@ -582,6 +598,63 @@ static NSCache<NSString *, UIImage *> *StaticSnapshotCache(void) {
582
598
  duration:duration];
583
599
  }
584
600
 
601
+ // Loads the map again, e.g. to recover from missing tiles. A static map
602
+ // drops its cached snapshot and renders the base map again in place.
603
+ // Neither SDK can reload a live map's tiles, so the native map view is
604
+ // recreated at the current camera (coordinate and zoom; heading and pitch
605
+ // reset) with children re-added.
606
+ - (void)reload {
607
+ if (_staticMode) {
608
+ _staticSnapshotDone = NO;
609
+ NSString *cacheKey = [self staticSnapshotCacheKey];
610
+ if (cacheKey) {
611
+ [StaticSnapshotCache() removeObjectForKey:cacheKey];
612
+ }
613
+ if ([_provider isKindOfClass:[StaticMapProviderBase class]]) {
614
+ [(StaticMapProviderBase *)_provider rerenderBaseMap];
615
+ }
616
+ return;
617
+ }
618
+
619
+ if (!_provider)
620
+ return;
621
+
622
+ CLLocationCoordinate2D coordinate = _provider.coordinate;
623
+ double zoom = _provider.zoom;
624
+ // Children keep their native marker/overlay objects from the old map;
625
+ // detach them so the new provider creates fresh ones on the flush
626
+ [self removeChildrenFromProvider];
627
+ [_provider destroy];
628
+ _provider = nil;
629
+ _initialized = NO;
630
+ [self initializeProviderWithCoordinate:coordinate zoom:zoom];
631
+ if ([_provider isKindOfClass:[AppleMapProvider class]]) {
632
+ // The captured camera already includes the previous inset offset.
633
+ [_provider moveCamera:coordinate.latitude
634
+ longitude:coordinate.longitude
635
+ zoom:zoom
636
+ duration:0];
637
+ }
638
+ }
639
+
640
+ - (void)removeChildrenFromProvider {
641
+ for (UIView *subview in self.subviews) {
642
+ if ([subview isKindOfClass:[LuggMarkerView class]]) {
643
+ [_provider removeMarkerView:(LuggMarkerView *)subview];
644
+ } else if ([subview isKindOfClass:[LuggPolylineView class]]) {
645
+ [_provider removePolylineView:(LuggPolylineView *)subview];
646
+ } else if ([subview isKindOfClass:[LuggPolygonView class]]) {
647
+ [_provider removePolygonView:(LuggPolygonView *)subview];
648
+ } else if ([subview isKindOfClass:[LuggCircleView class]]) {
649
+ [_provider removeCircleView:(LuggCircleView *)subview];
650
+ } else if ([subview isKindOfClass:[LuggGroundOverlayView class]]) {
651
+ [_provider removeGroundOverlayView:(LuggGroundOverlayView *)subview];
652
+ } else if ([subview isKindOfClass:[LuggTileOverlayView class]]) {
653
+ [_provider removeTileOverlayView:(LuggTileOverlayView *)subview];
654
+ }
655
+ }
656
+ }
657
+
585
658
  - (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args {
586
659
  RCTLuggMapViewHandleCommand(self, commandName, args);
587
660
  }
@@ -15,6 +15,7 @@ using facebook::react::LuggMapViewTheme;
15
15
  #import "../extensions/MKMapView+Zoom.h"
16
16
  #import "LuggAnnotationView.h"
17
17
  #import "MKPolylineAnimator.h"
18
+ #import "MapEdgeInsets.h"
18
19
 
19
20
  @interface AppleMarkerAnnotation : NSObject <MKAnnotation>
20
21
  @property(nonatomic, assign) CLLocationCoordinate2D coordinate;
@@ -221,6 +222,14 @@ static double tileToLng(NSInteger x, NSInteger z) {
221
222
  _isMapReady = NO;
222
223
  }
223
224
 
225
+ - (CLLocationCoordinate2D)coordinate {
226
+ return _mapView.centerCoordinate;
227
+ }
228
+
229
+ - (double)zoom {
230
+ return _mapView.zoomLevel;
231
+ }
232
+
224
233
  - (void)destroyMapView {
225
234
  [self dismissNonBubbledCallout];
226
235
  [self stopEdgeInsetsAnimation];
@@ -457,13 +466,11 @@ MKMapType LuggMKMapTypeFromMapType(LuggMapViewMapType mapType) {
457
466
 
458
467
  - (void)setEdgeInsets:(UIEdgeInsets)edgeInsets
459
468
  oldEdgeInsets:(UIEdgeInsets)oldEdgeInsets {
460
- CGFloat oldOffsetX = (oldEdgeInsets.left - oldEdgeInsets.right) / 2.0;
461
- CGFloat oldOffsetY = (oldEdgeInsets.top - oldEdgeInsets.bottom) / 2.0;
462
- CGFloat newOffsetX = (edgeInsets.left - edgeInsets.right) / 2.0;
463
- CGFloat newOffsetY = (edgeInsets.top - edgeInsets.bottom) / 2.0;
469
+ CGPoint oldOffset = LuggMapInsetOffset(oldEdgeInsets);
470
+ CGPoint newOffset = LuggMapInsetOffset(edgeInsets);
464
471
 
465
- CGFloat deltaX = newOffsetX - oldOffsetX;
466
- CGFloat deltaY = newOffsetY - oldOffsetY;
472
+ CGFloat deltaX = newOffset.x - oldOffset.x;
473
+ CGFloat deltaY = newOffset.y - oldOffset.y;
467
474
 
468
475
  _mapView.layoutMargins = edgeInsets;
469
476
  _edgeInsetsCurrent = edgeInsets;
@@ -506,16 +513,8 @@ MKMapType LuggMKMapTypeFromMapType(LuggMapViewMapType mapType) {
506
513
  CFTimeInterval elapsed = CACurrentMediaTime() - _edgeInsetsAnimationStart;
507
514
  CGFloat progress = MIN(elapsed / _edgeInsetsAnimationDuration, 1.0);
508
515
 
509
- // Ease out cubic
510
- CGFloat t = 1.0 - (1.0 - progress) * (1.0 - progress) * (1.0 - progress);
511
-
512
- UIEdgeInsets current = UIEdgeInsetsMake(
513
- _edgeInsetsFrom.top + (_edgeInsetsTo.top - _edgeInsetsFrom.top) * t,
514
- _edgeInsetsFrom.left + (_edgeInsetsTo.left - _edgeInsetsFrom.left) * t,
515
- _edgeInsetsFrom.bottom +
516
- (_edgeInsetsTo.bottom - _edgeInsetsFrom.bottom) * t,
517
- _edgeInsetsFrom.right +
518
- (_edgeInsetsTo.right - _edgeInsetsFrom.right) * t);
516
+ UIEdgeInsets current =
517
+ LuggMapInsetsAtProgress(_edgeInsetsFrom, _edgeInsetsTo, progress);
519
518
 
520
519
  [self setEdgeInsets:current oldEdgeInsets:_edgeInsetsCurrent];
521
520
 
@@ -4,6 +4,7 @@ using facebook::react::LuggMapViewPoiFilterMode;
4
4
  using facebook::react::LuggMapViewTheme;
5
5
 
6
6
  #import "../extensions/MKMapView+Zoom.h"
7
+ #import "../extensions/UIView+Snapshot.h"
7
8
  #import "AppleMapProvider.h"
8
9
  #import "LuggAnnotationView.h"
9
10
 
@@ -42,8 +43,13 @@ static MKMapRect LuggStaticFittedMapRect(CLLocationCoordinate2D center,
42
43
  return rect;
43
44
  }
44
45
 
46
+ @interface AppleStaticMapProvider () <MKMapViewDelegate>
47
+ @end
48
+
45
49
  @implementation AppleStaticMapProvider {
46
50
  MKMapSnapshotter *_snapshotter;
51
+ MKMapView *_warmupMapView;
52
+ BOOL _snapshotReady;
47
53
 
48
54
  BOOL _poiEnabled;
49
55
  LuggMapViewPoiFilterMode _poiFilterMode;
@@ -106,6 +112,12 @@ static MKMapRect LuggStaticFittedMapRect(CLLocationCoordinate2D center,
106
112
  }
107
113
 
108
114
  - (void)renderBaseMap {
115
+ // MKMapSnapshotter bakes attribution into the image without inset control.
116
+ if (!UIEdgeInsetsEqualToEdgeInsets(self.edgeInsets, UIEdgeInsetsZero)) {
117
+ [self renderInsetBaseMap];
118
+ return;
119
+ }
120
+
109
121
  if (_snapshotter)
110
122
  return;
111
123
 
@@ -140,6 +152,68 @@ static MKMapRect LuggStaticFittedMapRect(CLLocationCoordinate2D center,
140
152
  - (void)cancelBaseRender {
141
153
  [_snapshotter cancel];
142
154
  _snapshotter = nil;
155
+ _warmupMapView.delegate = nil;
156
+ [_warmupMapView removeFromSuperview];
157
+ _warmupMapView = nil;
158
+ _snapshotReady = NO;
159
+ }
160
+
161
+ #pragma mark - Inset snapshots
162
+
163
+ - (void)renderInsetBaseMap {
164
+ if (_warmupMapView) {
165
+ if (!_snapshotReady || !_warmupMapView.window)
166
+ return;
167
+
168
+ UIImageView *imageView = [_warmupMapView lugg_snapshotImageView];
169
+ if (!imageView)
170
+ return;
171
+
172
+ [self displayBaseImage:imageView.image fromCache:NO];
173
+ [self cancelBaseRender];
174
+ return;
175
+ }
176
+
177
+ MKMapView *mapView =
178
+ [[MKMapView alloc] initWithFrame:self.wrapperView.bounds];
179
+ mapView.userInteractionEnabled = NO;
180
+ mapView.showsCompass = NO;
181
+ mapView.insetsLayoutMarginsFromSafeArea = NO;
182
+ mapView.layoutMargins = self.edgeInsets;
183
+ mapView.mapType = LuggMKMapTypeFromMapType(self.mapType);
184
+ mapView.pointOfInterestFilter = LuggPointOfInterestFilter(
185
+ _poiEnabled, _poiFilterMode, _poiFilterCategories);
186
+ mapView.overrideUserInterfaceStyle =
187
+ [self snapshotTraitCollection].userInterfaceStyle;
188
+ [mapView setVisibleMapRect:self.mapRect
189
+ edgePadding:UIEdgeInsetsZero
190
+ animated:NO];
191
+ _warmupMapView = mapView;
192
+ mapView.delegate = self;
193
+ [self.wrapperView insertSubview:mapView atIndex:0];
194
+ }
195
+
196
+ - (void)mapViewWillStartRenderingMap:(MKMapView *)mapView {
197
+ _snapshotReady = NO;
198
+ }
199
+
200
+ - (void)mapViewDidFinishRenderingMap:(MKMapView *)mapView
201
+ fullyRendered:(BOOL)fullyRendered {
202
+ _snapshotReady = fullyRendered;
203
+ [self revealOverlays];
204
+ if (!fullyRendered)
205
+ return;
206
+
207
+ __weak AppleStaticMapProvider *weakSelf = self;
208
+ __weak MKMapView *weakMapView = mapView;
209
+ [[NSRunLoop mainRunLoop]
210
+ performInModes:@[ NSDefaultRunLoopMode ]
211
+ block:^{
212
+ AppleStaticMapProvider *strongSelf = weakSelf;
213
+ if (strongSelf && weakMapView &&
214
+ strongSelf->_warmupMapView == weakMapView)
215
+ [strongSelf renderInsetBaseMap];
216
+ }];
143
217
  }
144
218
 
145
219
  #pragma mark - Props
@@ -13,6 +13,7 @@ using facebook::react::LuggMapViewTheme;
13
13
  #import "../LuggPolylineView.h"
14
14
  #import "../LuggTileOverlayView.h"
15
15
  #import "GMSPolylineAnimator.h"
16
+ #import "MapEdgeInsets.h"
16
17
  #import "PolylineAnimatorBase.h"
17
18
 
18
19
  static NSString *const kDemoMapId = @"DEMO_MAP_ID";
@@ -183,6 +184,14 @@ LuggInterfaceStyleFromTheme(facebook::react::LuggMapViewTheme theme) {
183
184
  _mapView = nil;
184
185
  }
185
186
 
187
+ - (CLLocationCoordinate2D)coordinate {
188
+ return _mapView.camera.target;
189
+ }
190
+
191
+ - (double)zoom {
192
+ return _mapView.camera.zoom;
193
+ }
194
+
186
195
  #pragma mark - Props
187
196
 
188
197
  - (void)setZoomEnabled:(BOOL)enabled {
@@ -288,16 +297,8 @@ LuggInterfaceStyleFromTheme(facebook::react::LuggMapViewTheme theme) {
288
297
  CFTimeInterval elapsed = CACurrentMediaTime() - _edgeInsetsAnimationStart;
289
298
  CGFloat progress = MIN(elapsed / _edgeInsetsAnimationDuration, 1.0);
290
299
 
291
- // Ease out cubic
292
- CGFloat t = 1.0 - (1.0 - progress) * (1.0 - progress) * (1.0 - progress);
293
-
294
- UIEdgeInsets current = UIEdgeInsetsMake(
295
- _edgeInsetsFrom.top + (_edgeInsetsTo.top - _edgeInsetsFrom.top) * t,
296
- _edgeInsetsFrom.left + (_edgeInsetsTo.left - _edgeInsetsFrom.left) * t,
297
- _edgeInsetsFrom.bottom +
298
- (_edgeInsetsTo.bottom - _edgeInsetsFrom.bottom) * t,
299
- _edgeInsetsFrom.right +
300
- (_edgeInsetsTo.right - _edgeInsetsFrom.right) * t);
300
+ UIEdgeInsets current =
301
+ LuggMapInsetsAtProgress(_edgeInsetsFrom, _edgeInsetsTo, progress);
301
302
 
302
303
  _mapView.padding = current;
303
304
 
@@ -49,7 +49,7 @@ static void EnqueueStaticMapView(NSString *mapId, GMSMapView *mapView) {
49
49
 
50
50
  @implementation GoogleStaticMapProvider {
51
51
  GMSMapView *_warmupMapView;
52
- BOOL _tilesRendered;
52
+ BOOL _snapshotReady;
53
53
  BOOL _swapScheduled;
54
54
  }
55
55
 
@@ -106,7 +106,7 @@ static void EnqueueStaticMapView(NSString *mapId, GMSMapView *mapView) {
106
106
  if (!wrapperView)
107
107
  return;
108
108
 
109
- _tilesRendered = NO;
109
+ _snapshotReady = NO;
110
110
 
111
111
  GMSMapView *mapView = DequeueStaticMapView([self poolKey]);
112
112
  if (mapView) {
@@ -152,12 +152,12 @@ static void EnqueueStaticMapView(NSString *mapId, GMSMapView *mapView) {
152
152
  _warmupMapView = nil;
153
153
  }
154
154
 
155
- // Once tiles are fully rendered, swap the live map with its image and
156
- // release the map view. The image render and map teardown are expensive,
157
- // so they run outside scroll tracking; the live map keeps displaying (and
158
- // loading tiles) until then.
155
+ // Once the map is stable, swap the live map with its image and release the
156
+ // map view. The image render and map teardown are expensive, so they run
157
+ // outside scroll tracking; the live map keeps displaying (and loading
158
+ // tiles) until then.
159
159
  - (void)scheduleSwap {
160
- if (_swapScheduled || !_warmupMapView || !_tilesRendered ||
160
+ if (_swapScheduled || !_warmupMapView || !_snapshotReady ||
161
161
  !_warmupMapView.window)
162
162
  return;
163
163
 
@@ -176,9 +176,9 @@ static void EnqueueStaticMapView(NSString *mapId, GMSMapView *mapView) {
176
176
 
177
177
  - (void)performSwap {
178
178
  // Tiles can invalidate (or the view leave the window) between scheduling
179
- // and this runloop pass; the next tile callback or resumeAnimations
179
+ // and this runloop pass; the next snapshotReady or resumeAnimations
180
180
  // retries
181
- if (!_warmupMapView || !_tilesRendered || !_warmupMapView.window)
181
+ if (!_warmupMapView || !_snapshotReady || !_warmupMapView.window)
182
182
  return;
183
183
 
184
184
  UIImageView *imageView = [_warmupMapView lugg_snapshotImageView];
@@ -192,20 +192,24 @@ static void EnqueueStaticMapView(NSString *mapId, GMSMapView *mapView) {
192
192
  #pragma mark - GMSMapViewDelegate
193
193
 
194
194
  - (void)mapViewDidStartTileRendering:(GMSMapView *)mapView {
195
- _tilesRendered = NO;
195
+ _snapshotReady = NO;
196
196
  }
197
197
 
198
+ // Fires for tiles that failed permanently too, so it can't gate the swap:
199
+ // capturing here cached blank tiles under the static key with no way to
200
+ // recover. The visible warmup map has rendered what it can; show the
201
+ // overlays now instead of waiting for the swap, which is deferred while
202
+ // scrolling
198
203
  - (void)mapViewDidFinishTileRendering:(GMSMapView *)mapView {
199
- _tilesRendered = YES;
200
- // The visible warmup map is fully rendered; show the overlays now
201
- // instead of waiting for the swap, which is deferred while scrolling
202
204
  [self revealOverlays];
203
- [self scheduleSwap];
204
205
  }
205
206
 
207
+ // Stable per the SDK: tiles loaded, labels and overlays rendered. The only
208
+ // signal that the render is complete, so the only one that captures and
209
+ // caches. Without it (e.g. offline) the live map stays until the view
210
+ // leaves the window, and a fresh warmup runs on return.
206
211
  - (void)mapViewSnapshotReady:(GMSMapView *)mapView {
207
- // Stable per the SDK: tiles loaded, labels and overlays rendered
208
- _tilesRendered = YES;
212
+ _snapshotReady = YES;
209
213
  [self revealOverlays];
210
214
  [self scheduleSwap];
211
215
  }
@@ -0,0 +1,18 @@
1
+ #pragma once
2
+
3
+ #import <UIKit/UIKit.h>
4
+
5
+ static inline CGPoint LuggMapInsetOffset(UIEdgeInsets insets) {
6
+ return CGPointMake((insets.left - insets.right) / 2.0,
7
+ (insets.top - insets.bottom) / 2.0);
8
+ }
9
+
10
+ static inline UIEdgeInsets
11
+ LuggMapInsetsAtProgress(UIEdgeInsets from, UIEdgeInsets to, CGFloat progress) {
12
+ // Ease out cubic
13
+ CGFloat t = 1.0 - (1.0 - progress) * (1.0 - progress) * (1.0 - progress);
14
+ return UIEdgeInsetsMake(from.top + (to.top - from.top) * t,
15
+ from.left + (to.left - from.left) * t,
16
+ from.bottom + (to.bottom - from.bottom) * t,
17
+ from.right + (to.right - from.right) * t);
18
+ }
@@ -89,6 +89,10 @@ NS_ASSUME_NONNULL_BEGIN
89
89
  - (void)removeTileOverlayView:(LuggTileOverlayView *)tileOverlayView;
90
90
  - (void)syncTileOverlayView:(LuggTileOverlayView *)tileOverlayView;
91
91
 
92
+ // Current camera (the initial camera until the map is ready)
93
+ @property(nonatomic, readonly) CLLocationCoordinate2D coordinate;
94
+ @property(nonatomic, readonly) double zoom;
95
+
92
96
  // Lifecycle
93
97
  - (void)pauseAnimations;
94
98
  - (void)resumeAnimations;
@@ -42,6 +42,11 @@ CGPoint LuggStaticPointForCoordinate(MKMapRect mapRect, CGSize size,
42
42
  /// delegate callbacks and marks the base render done.
43
43
  - (void)displayBaseImage:(UIImage *)image fromCache:(BOOL)fromCache;
44
44
 
45
+ /// Discards any cached, displayed or in-flight base render and renders
46
+ /// again at the current camera. The displayed image stays until the new
47
+ /// one replaces it.
48
+ - (void)rerenderBaseMap;
49
+
45
50
  /// Shows the marker and shape overlays (idempotent). Called automatically
46
51
  /// when the base image displays; subclasses whose base map is already
47
52
  /// visible earlier (e.g. a live warmup map) can call it sooner.