@lugg/maps 1.0.0-beta.17 → 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 (40) 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/GoogleMapProvider.h +10 -0
  14. package/ios/core/GoogleMapProvider.mm +47 -41
  15. package/ios/core/GoogleStaticMapProvider.h +21 -0
  16. package/ios/core/GoogleStaticMapProvider.mm +204 -0
  17. package/ios/core/MKPolylineAnimator.m +67 -43
  18. package/ios/core/MapProviderDelegate.h +6 -0
  19. package/ios/core/StaticMapProviderBase.h +69 -0
  20. package/ios/core/StaticMapProviderBase.mm +745 -0
  21. package/ios/extensions/MKMapView+Zoom.h +14 -0
  22. package/ios/extensions/MKMapView+Zoom.m +17 -15
  23. package/ios/extensions/UIView+Snapshot.h +16 -0
  24. package/ios/extensions/UIView+Snapshot.m +27 -0
  25. package/lib/module/MapView.js +4 -0
  26. package/lib/module/MapView.js.map +1 -1
  27. package/lib/module/MapView.web.js +8 -5
  28. package/lib/module/MapView.web.js.map +1 -1
  29. package/lib/module/fabric/LuggMapViewNativeComponent.ts +2 -0
  30. package/lib/typescript/src/MapView.d.ts.map +1 -1
  31. package/lib/typescript/src/MapView.types.d.ts +18 -0
  32. package/lib/typescript/src/MapView.types.d.ts.map +1 -1
  33. package/lib/typescript/src/MapView.web.d.ts.map +1 -1
  34. package/lib/typescript/src/fabric/LuggMapViewNativeComponent.d.ts +2 -0
  35. package/lib/typescript/src/fabric/LuggMapViewNativeComponent.d.ts.map +1 -1
  36. package/package.json +1 -1
  37. package/src/MapView.tsx +4 -0
  38. package/src/MapView.types.ts +18 -0
  39. package/src/MapView.web.tsx +14 -10
  40. package/src/fabric/LuggMapViewNativeComponent.ts +2 -0
@@ -0,0 +1,146 @@
1
+ #import "AppleStaticMapProvider.h"
2
+
3
+ using facebook::react::LuggMapViewPoiFilterMode;
4
+ using facebook::react::LuggMapViewTheme;
5
+
6
+ #import "../extensions/MKMapView+Zoom.h"
7
+ #import "AppleMapProvider.h"
8
+ #import "LuggAnnotationView.h"
9
+
10
+ // Standard MKMarkerAnnotationView balloon size; standalone (off-map) views
11
+ // don't size themselves
12
+ static const CGSize kDefaultMarkerSize = {28, 33};
13
+
14
+ // The rect the snapshot renders: the region for the requested zoom, expanded
15
+ // on the short axis to the view's aspect ratio - the same fit MKMapView
16
+ // applies in setRegion, so static and live maps frame alike. Passing this
17
+ // exact rect to MKMapSnapshotter makes the projection exact.
18
+ static MKMapRect LuggStaticFittedMapRect(CLLocationCoordinate2D center,
19
+ double zoom, CGSize size) {
20
+ MKCoordinateSpan span = LuggCoordinateSpanForZoomLevel(MIN(zoom, 28), center);
21
+ MKMapPoint topLeft = MKMapPointForCoordinate(CLLocationCoordinate2DMake(
22
+ MIN(center.latitude + span.latitudeDelta / 2, 90.0),
23
+ center.longitude - span.longitudeDelta / 2));
24
+ MKMapPoint bottomRight = MKMapPointForCoordinate(CLLocationCoordinate2DMake(
25
+ MAX(center.latitude - span.latitudeDelta / 2, -90.0),
26
+ center.longitude + span.longitudeDelta / 2));
27
+ MKMapRect rect =
28
+ MKMapRectMake(topLeft.x, topLeft.y, bottomRight.x - topLeft.x,
29
+ bottomRight.y - topLeft.y);
30
+
31
+ double viewAspect = size.width / size.height;
32
+ double rectAspect = rect.size.width / rect.size.height;
33
+ if (rectAspect > viewAspect) {
34
+ double height = rect.size.width / viewAspect;
35
+ rect.origin.y -= (height - rect.size.height) / 2;
36
+ rect.size.height = height;
37
+ } else {
38
+ double width = rect.size.height * viewAspect;
39
+ rect.origin.x -= (width - rect.size.width) / 2;
40
+ rect.size.width = width;
41
+ }
42
+ return rect;
43
+ }
44
+
45
+ @implementation AppleStaticMapProvider {
46
+ MKMapSnapshotter *_snapshotter;
47
+
48
+ BOOL _poiEnabled;
49
+ LuggMapViewPoiFilterMode _poiFilterMode;
50
+ NSArray<NSString *> *_poiFilterCategories;
51
+ }
52
+
53
+ - (instancetype)init {
54
+ if (self = [super init]) {
55
+ _poiEnabled = YES;
56
+ _poiFilterMode = LuggMapViewPoiFilterMode::Including;
57
+ _poiFilterCategories = @[];
58
+ }
59
+ return self;
60
+ }
61
+
62
+ #pragma mark - StaticMapProviderBase
63
+
64
+ - (MKMapRect)mapRectForSize:(CGSize)size {
65
+ return LuggStaticFittedMapRect(self.coordinate, self.zoom, size);
66
+ }
67
+
68
+ - (UIView *)newDefaultMarkerOverlayView {
69
+ MKAnnotationView *pin =
70
+ [[LuggMarkerAnnotationView alloc] initWithAnnotation:nil
71
+ reuseIdentifier:nil];
72
+ pin.bounds =
73
+ CGRectMake(0, 0, kDefaultMarkerSize.width, kDefaultMarkerSize.height);
74
+ return pin;
75
+ }
76
+
77
+ - (UITraitCollection *)snapshotTraitCollection {
78
+ UITraitCollection *base = self.wrapperView.traitCollection;
79
+ UIUserInterfaceStyle style;
80
+ switch (self.theme) {
81
+ case LuggMapViewTheme::Dark:
82
+ style = UIUserInterfaceStyleDark;
83
+ break;
84
+ case LuggMapViewTheme::Light:
85
+ style = UIUserInterfaceStyleLight;
86
+ break;
87
+ default:
88
+ return base;
89
+ }
90
+ return [UITraitCollection traitCollectionWithTraitsFromCollections:@[
91
+ base, [UITraitCollection traitCollectionWithUserInterfaceStyle:style]
92
+ ]];
93
+ }
94
+
95
+ - (void)renderBaseMap {
96
+ if (_snapshotter)
97
+ return;
98
+
99
+ MKMapSnapshotOptions *options = [[MKMapSnapshotOptions alloc] init];
100
+ options.mapRect = self.mapRect;
101
+ options.size = self.projectedSize;
102
+ options.mapType = LuggMKMapTypeFromMapType(self.mapType);
103
+ options.pointOfInterestFilter = LuggPointOfInterestFilter(
104
+ _poiEnabled, _poiFilterMode, _poiFilterCategories);
105
+ options.traitCollection = [self snapshotTraitCollection];
106
+
107
+ MKMapSnapshotter *snapshotter =
108
+ [[MKMapSnapshotter alloc] initWithOptions:options];
109
+ _snapshotter = snapshotter;
110
+
111
+ __weak AppleStaticMapProvider *weakSelf = self;
112
+ [snapshotter startWithQueue:dispatch_get_main_queue()
113
+ completionHandler:^(MKMapSnapshot *snapshot, NSError *error) {
114
+ AppleStaticMapProvider *strongSelf = weakSelf;
115
+ if (!strongSelf || strongSelf->_snapshotter != snapshotter)
116
+ return;
117
+ strongSelf->_snapshotter = nil;
118
+ if (error || !snapshot) {
119
+ // Keep the placeholder; resumeAnimations retries (e.g. back
120
+ // online)
121
+ return;
122
+ }
123
+ [strongSelf displayBaseImage:snapshot.image fromCache:NO];
124
+ }];
125
+ }
126
+
127
+ - (void)cancelBaseRender {
128
+ [_snapshotter cancel];
129
+ _snapshotter = nil;
130
+ }
131
+
132
+ #pragma mark - Props
133
+
134
+ - (void)setPoiEnabled:(BOOL)enabled {
135
+ _poiEnabled = enabled;
136
+ }
137
+
138
+ - (void)setPoiFilterMode:(LuggMapViewPoiFilterMode)mode {
139
+ _poiFilterMode = mode;
140
+ }
141
+
142
+ - (void)setPoiFilterCategories:(NSArray<NSString *> *)categories {
143
+ _poiFilterCategories = categories;
144
+ }
145
+
146
+ @end
@@ -3,6 +3,16 @@
3
3
 
4
4
  NS_ASSUME_NONNULL_BEGIN
5
5
 
6
+ /// Resolves a mapId prop value ("" or "DEMO_MAP_ID" -> demo map ID),
7
+ /// shared by the live and static Google providers.
8
+ GMSMapID *LuggGMSMapIDFromString(NSString *_Nullable mapId);
9
+
10
+ GMSMapViewType
11
+ LuggGMSMapTypeFromMapType(facebook::react::LuggMapViewMapType mapType);
12
+
13
+ UIUserInterfaceStyle
14
+ LuggInterfaceStyleFromTheme(facebook::react::LuggMapViewTheme theme);
15
+
6
16
  @interface GoogleMapProvider : NSObject <MapProvider, GMSMapViewDelegate>
7
17
 
8
18
  @property(nonatomic, copy, nullable) NSString *mapId;
@@ -17,6 +17,39 @@ using facebook::react::LuggMapViewTheme;
17
17
 
18
18
  static NSString *const kDemoMapId = @"DEMO_MAP_ID";
19
19
 
20
+ GMSMapID *LuggGMSMapIDFromString(NSString *_Nullable mapId) {
21
+ if (mapId.length == 0 || [mapId isEqualToString:kDemoMapId]) {
22
+ return [GMSMapID demoMapID];
23
+ }
24
+ return [GMSMapID mapIDWithIdentifier:mapId];
25
+ }
26
+
27
+ GMSMapViewType
28
+ LuggGMSMapTypeFromMapType(facebook::react::LuggMapViewMapType mapType) {
29
+ switch (mapType) {
30
+ case LuggMapViewMapType::Satellite:
31
+ return kGMSTypeSatellite;
32
+ case LuggMapViewMapType::Terrain:
33
+ return kGMSTypeTerrain;
34
+ case LuggMapViewMapType::Hybrid:
35
+ return kGMSTypeHybrid;
36
+ default:
37
+ return kGMSTypeNormal;
38
+ }
39
+ }
40
+
41
+ UIUserInterfaceStyle
42
+ LuggInterfaceStyleFromTheme(facebook::react::LuggMapViewTheme theme) {
43
+ switch (theme) {
44
+ case LuggMapViewTheme::Dark:
45
+ return UIUserInterfaceStyleDark;
46
+ case LuggMapViewTheme::Light:
47
+ return UIUserInterfaceStyleLight;
48
+ default:
49
+ return UIUserInterfaceStyleUnspecified;
50
+ }
51
+ }
52
+
20
53
  @interface GoogleMapProvider () <
21
54
  LuggMarkerViewDelegate, LuggCalloutViewDelegate, LuggPolylineViewDelegate,
22
55
  LuggPolygonViewDelegate, LuggCircleViewDelegate,
@@ -53,6 +86,7 @@ static NSString *const kDemoMapId = @"DEMO_MAP_ID";
53
86
  }
54
87
 
55
88
  @synthesize delegate = _delegate;
89
+ @synthesize staticMode = _staticMode;
56
90
 
57
91
  - (instancetype)init {
58
92
  if (self = [super init]) {
@@ -89,13 +123,6 @@ static NSString *const kDemoMapId = @"DEMO_MAP_ID";
89
123
  if (_mapView)
90
124
  return;
91
125
 
92
- GMSMapID *gmsMapId;
93
- if ([_mapId isEqualToString:kDemoMapId] || _mapId.length == 0) {
94
- gmsMapId = [GMSMapID demoMapID];
95
- } else {
96
- gmsMapId = [GMSMapID mapIDWithIdentifier:_mapId];
97
- }
98
-
99
126
  GMSCameraPosition *camera =
100
127
  [GMSCameraPosition cameraWithLatitude:coordinate.latitude
101
128
  longitude:coordinate.longitude
@@ -104,7 +131,7 @@ static NSString *const kDemoMapId = @"DEMO_MAP_ID";
104
131
  GMSMapViewOptions *options = [[GMSMapViewOptions alloc] init];
105
132
  options.frame = wrapperView.bounds;
106
133
  options.camera = camera;
107
- options.mapID = gmsMapId;
134
+ options.mapID = LuggGMSMapIDFromString(_mapId);
108
135
 
109
136
  _mapView = [[GMSMapView alloc] initWithOptions:options];
110
137
  _mapView.autoresizingMask =
@@ -142,10 +169,18 @@ static NSString *const kDemoMapId = @"DEMO_MAP_ID";
142
169
  [_circleToViewMap removeAllObjects];
143
170
  [_markerToViewMap removeAllObjects];
144
171
  [_groundOverlayToViewMap removeAllObjects];
145
- [_mapView clear];
172
+ [self destroyMapView];
173
+ _isMapReady = NO;
174
+ }
175
+
176
+ - (void)destroyMapView {
177
+ if (!_mapView)
178
+ return;
179
+
180
+ _mapView.delegate = nil;
146
181
  [_mapView removeFromSuperview];
182
+ [_mapView clear];
147
183
  _mapView = nil;
148
- _isMapReady = NO;
149
184
  }
150
185
 
151
186
  #pragma mark - Props
@@ -191,23 +226,7 @@ static NSString *const kDemoMapId = @"DEMO_MAP_ID";
191
226
  }
192
227
 
193
228
  - (void)setMapType:(LuggMapViewMapType)mapType {
194
- if (!_mapView)
195
- return;
196
-
197
- switch (mapType) {
198
- case LuggMapViewMapType::Satellite:
199
- _mapView.mapType = kGMSTypeSatellite;
200
- break;
201
- case LuggMapViewMapType::Terrain:
202
- _mapView.mapType = kGMSTypeTerrain;
203
- break;
204
- case LuggMapViewMapType::Hybrid:
205
- _mapView.mapType = kGMSTypeHybrid;
206
- break;
207
- default:
208
- _mapView.mapType = kGMSTypeNormal;
209
- break;
210
- }
229
+ _mapView.mapType = LuggGMSMapTypeFromMapType(mapType);
211
230
  }
212
231
 
213
232
  - (void)setTheme:(LuggMapViewTheme)theme {
@@ -216,20 +235,7 @@ static NSString *const kDemoMapId = @"DEMO_MAP_ID";
216
235
  }
217
236
 
218
237
  - (void)applyTheme {
219
- if (!_mapView)
220
- return;
221
-
222
- switch (_theme) {
223
- case LuggMapViewTheme::Dark:
224
- _mapView.overrideUserInterfaceStyle = UIUserInterfaceStyleDark;
225
- break;
226
- case LuggMapViewTheme::Light:
227
- _mapView.overrideUserInterfaceStyle = UIUserInterfaceStyleLight;
228
- break;
229
- default:
230
- _mapView.overrideUserInterfaceStyle = UIUserInterfaceStyleUnspecified;
231
- break;
232
- }
238
+ _mapView.overrideUserInterfaceStyle = LuggInterfaceStyleFromTheme(_theme);
233
239
  }
234
240
 
235
241
  - (void)setMinZoom:(double)minZoom {
@@ -0,0 +1,21 @@
1
+ #import "StaticMapProviderBase.h"
2
+ #import <GoogleMaps/GoogleMaps.h>
3
+
4
+ NS_ASSUME_NONNULL_BEGIN
5
+
6
+ /**
7
+ * Static Google map. The Google Maps SDK has no async snapshotter, so the
8
+ * base map briefly warms up on a live, tiles-only GMSMapView inserted below
9
+ * the overlays, then is swapped with a rendered image and the map view is
10
+ * pooled for reuse. Markers and shapes are overlay views (shared with
11
+ * Apple static maps) that reveal with the base map, so the warmup map
12
+ * never renders its own markers. Warmup cost scales with how many static
13
+ * maps mount at once - bound that with list props (windowSize etc.).
14
+ */
15
+ @interface GoogleStaticMapProvider : StaticMapProviderBase <GMSMapViewDelegate>
16
+
17
+ @property(nonatomic, copy, nullable) NSString *mapId;
18
+
19
+ @end
20
+
21
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,204 @@
1
+ #import "GoogleStaticMapProvider.h"
2
+
3
+ #import "../extensions/UIView+Snapshot.h"
4
+ #import "GoogleMapProvider.h"
5
+
6
+ // Static maps churn through GMSMapViews as list rows warm up, and both
7
+ // creating one and tearing one down are expensive main-thread operations
8
+ // (renderer setup/teardown). Detached map views are pooled and reused so a
9
+ // warmup usually only costs a camera move plus tile load. Keyed by map ID,
10
+ // which is fixed at GMSMapView creation.
11
+ static const NSUInteger kStaticMapViewPoolCapacity = 6;
12
+
13
+ static NSMutableDictionary<NSString *, NSMutableArray<GMSMapView *> *> *
14
+ StaticMapViewPool(void) {
15
+ static NSMutableDictionary<NSString *, NSMutableArray<GMSMapView *> *> *pool;
16
+ static dispatch_once_t onceToken;
17
+ dispatch_once(&onceToken, ^{
18
+ pool = [NSMutableDictionary dictionary];
19
+ [[NSNotificationCenter defaultCenter]
20
+ addObserverForName:UIApplicationDidReceiveMemoryWarningNotification
21
+ object:nil
22
+ queue:[NSOperationQueue mainQueue]
23
+ usingBlock:^(NSNotification *notification) {
24
+ [pool removeAllObjects];
25
+ }];
26
+ });
27
+ return pool;
28
+ }
29
+
30
+ static GMSMapView *_Nullable DequeueStaticMapView(NSString *mapId) {
31
+ NSMutableArray<GMSMapView *> *views = StaticMapViewPool()[mapId];
32
+ GMSMapView *mapView = views.lastObject;
33
+ [views removeLastObject];
34
+ return mapView;
35
+ }
36
+
37
+ static void EnqueueStaticMapView(NSString *mapId, GMSMapView *mapView) {
38
+ NSMutableDictionary<NSString *, NSMutableArray<GMSMapView *> *> *pool =
39
+ StaticMapViewPool();
40
+ NSMutableArray<GMSMapView *> *views = pool[mapId];
41
+ if (!views) {
42
+ views = [NSMutableArray array];
43
+ pool[mapId] = views;
44
+ }
45
+ if (views.count >= kStaticMapViewPoolCapacity)
46
+ return;
47
+ [views addObject:mapView];
48
+ }
49
+
50
+ @implementation GoogleStaticMapProvider {
51
+ GMSMapView *_warmupMapView;
52
+ BOOL _tilesRendered;
53
+ BOOL _swapScheduled;
54
+ }
55
+
56
+ #pragma mark - StaticMapProviderBase
57
+
58
+ - (MKMapRect)mapRectForSize:(CGSize)size {
59
+ // GMSMapView shows the world 256 * 2^zoom points wide, centered on the
60
+ // camera target - the same web mercator MKMapPoint uses, so overlays
61
+ // project onto the warmed-up tiles exactly
62
+ double zoom =
63
+ MAX(MIN(self.zoom, (double)kGMSMaxZoomLevel), (double)kGMSMinZoomLevel);
64
+ double mapPointsPerPoint = MKMapSizeWorld.width / (256.0 * pow(2.0, zoom));
65
+ double width = size.width * mapPointsPerPoint;
66
+ double height = size.height * mapPointsPerPoint;
67
+ MKMapPoint center = MKMapPointForCoordinate(self.coordinate);
68
+ return MKMapRectMake(center.x - width / 2, center.y - height / 2, width,
69
+ height);
70
+ }
71
+
72
+ - (UIView *)newDefaultMarkerOverlayView {
73
+ return
74
+ [[UIImageView alloc] initWithImage:[GMSMarker markerImageWithColor:nil]];
75
+ }
76
+
77
+ - (void)renderBaseMap {
78
+ if (_warmupMapView) {
79
+ // Retry a swap that couldn't run (e.g. view was off-window)
80
+ [self scheduleSwap];
81
+ return;
82
+ }
83
+ [self startWarmup];
84
+ }
85
+
86
+ - (void)cancelBaseRender {
87
+ [self destroyWarmupMapView];
88
+ }
89
+
90
+ #pragma mark - Warmup
91
+
92
+ // "" resolves to the demo map ID (see LuggGMSMapIDFromString), so both
93
+ // share a pool bucket
94
+ - (NSString *)poolKey {
95
+ return self.mapId.length > 0 ? self.mapId : @"DEMO_MAP_ID";
96
+ }
97
+
98
+ - (void)startWarmup {
99
+ UIView *wrapperView = self.wrapperView;
100
+ if (!wrapperView)
101
+ return;
102
+
103
+ _tilesRendered = NO;
104
+
105
+ GMSMapView *mapView = DequeueStaticMapView([self poolKey]);
106
+ if (mapView) {
107
+ mapView.frame = wrapperView.bounds;
108
+ [mapView moveCamera:[GMSCameraUpdate setTarget:self.coordinate
109
+ zoom:(float)self.zoom]];
110
+ } else {
111
+ GMSCameraPosition *camera =
112
+ [GMSCameraPosition cameraWithLatitude:self.coordinate.latitude
113
+ longitude:self.coordinate.longitude
114
+ zoom:(float)self.zoom];
115
+
116
+ GMSMapViewOptions *options = [[GMSMapViewOptions alloc] init];
117
+ options.frame = wrapperView.bounds;
118
+ options.camera = camera;
119
+ options.mapID = LuggGMSMapIDFromString(self.mapId);
120
+
121
+ mapView = [[GMSMapView alloc] initWithOptions:options];
122
+ }
123
+
124
+ mapView.autoresizingMask =
125
+ UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
126
+ mapView.delegate = self;
127
+ mapView.userInteractionEnabled = NO;
128
+ mapView.preferredFrameRate = kGMSFrameRateConservative;
129
+ mapView.paddingAdjustmentBehavior = kGMSMapViewPaddingAdjustmentBehaviorNever;
130
+ mapView.mapType = LuggGMSMapTypeFromMapType(self.mapType);
131
+ mapView.overrideUserInterfaceStyle = LuggInterfaceStyleFromTheme(self.theme);
132
+
133
+ _warmupMapView = mapView;
134
+ [wrapperView insertSubview:mapView atIndex:0];
135
+ }
136
+
137
+ - (void)destroyWarmupMapView {
138
+ if (!_warmupMapView)
139
+ return;
140
+ _warmupMapView.delegate = nil;
141
+ [_warmupMapView removeFromSuperview];
142
+ EnqueueStaticMapView([self poolKey], _warmupMapView);
143
+ _warmupMapView = nil;
144
+ }
145
+
146
+ // Once tiles are fully rendered, swap the live map with its image and
147
+ // release the map view. The image render and map teardown are expensive,
148
+ // so they run outside scroll tracking; the live map keeps displaying (and
149
+ // loading tiles) until then.
150
+ - (void)scheduleSwap {
151
+ if (_swapScheduled || !_warmupMapView || !_tilesRendered ||
152
+ !_warmupMapView.window)
153
+ return;
154
+
155
+ _swapScheduled = YES;
156
+ __weak GoogleStaticMapProvider *weakSelf = self;
157
+ [[NSRunLoop mainRunLoop] performInModes:@[ NSDefaultRunLoopMode ]
158
+ block:^{
159
+ GoogleStaticMapProvider *strongSelf =
160
+ weakSelf;
161
+ if (!strongSelf)
162
+ return;
163
+ strongSelf->_swapScheduled = NO;
164
+ [strongSelf performSwap];
165
+ }];
166
+ }
167
+
168
+ - (void)performSwap {
169
+ // Tiles can invalidate (or the view leave the window) between scheduling
170
+ // and this runloop pass; the next tile callback or resumeAnimations
171
+ // retries
172
+ if (!_warmupMapView || !_tilesRendered || !_warmupMapView.window)
173
+ return;
174
+
175
+ UIImageView *imageView = [_warmupMapView lugg_snapshotImageView];
176
+ if (!imageView)
177
+ return;
178
+
179
+ [self displayBaseImage:imageView.image fromCache:NO];
180
+ [self destroyWarmupMapView];
181
+ }
182
+
183
+ #pragma mark - GMSMapViewDelegate
184
+
185
+ - (void)mapViewDidStartTileRendering:(GMSMapView *)mapView {
186
+ _tilesRendered = NO;
187
+ }
188
+
189
+ - (void)mapViewDidFinishTileRendering:(GMSMapView *)mapView {
190
+ _tilesRendered = YES;
191
+ // The visible warmup map is fully rendered; show the overlays now
192
+ // instead of waiting for the swap, which is deferred while scrolling
193
+ [self revealOverlays];
194
+ [self scheduleSwap];
195
+ }
196
+
197
+ - (void)mapViewSnapshotReady:(GMSMapView *)mapView {
198
+ // Stable per the SDK: tiles loaded, labels and overlays rendered
199
+ _tilesRendered = YES;
200
+ [self revealOverlays];
201
+ [self scheduleSwap];
202
+ }
203
+
204
+ @end