@lugg/maps 1.0.0-beta.9 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +15 -100
  2. package/android/src/main/java/com/luggmaps/LuggCalloutView.kt +5 -0
  3. package/android/src/main/java/com/luggmaps/LuggMapView.kt +41 -0
  4. package/android/src/main/java/com/luggmaps/LuggMapViewManager.kt +14 -0
  5. package/android/src/main/java/com/luggmaps/LuggMapWrapperView.kt +27 -2
  6. package/android/src/main/java/com/luggmaps/LuggMarkerView.kt +7 -2
  7. package/android/src/main/java/com/luggmaps/LuggPolylineView.kt +2 -1
  8. package/android/src/main/java/com/luggmaps/core/GoogleMapProvider.kt +307 -41
  9. package/android/src/main/java/com/luggmaps/core/MapProviderDelegate.kt +1 -0
  10. package/android/src/main/java/com/luggmaps/core/PolylineAnimator.kt +61 -10
  11. package/ios/LuggMapView.mm +170 -2
  12. package/ios/LuggMapWrapperView.h +5 -0
  13. package/ios/LuggMapWrapperView.mm +18 -0
  14. package/ios/LuggMarkerView.mm +2 -0
  15. package/ios/core/AppleMapProvider.h +8 -0
  16. package/ios/core/AppleMapProvider.mm +48 -31
  17. package/ios/core/AppleStaticMapProvider.h +14 -0
  18. package/ios/core/AppleStaticMapProvider.mm +159 -0
  19. package/ios/core/GMSPolylineAnimator.m +40 -15
  20. package/ios/core/GoogleMapProvider.h +10 -0
  21. package/ios/core/GoogleMapProvider.mm +56 -44
  22. package/ios/core/GoogleStaticMapProvider.h +21 -0
  23. package/ios/core/GoogleStaticMapProvider.mm +213 -0
  24. package/ios/core/MKPolylineAnimator.m +67 -43
  25. package/ios/core/MapProviderDelegate.h +7 -0
  26. package/ios/core/StaticMapProviderBase.h +75 -0
  27. package/ios/core/StaticMapProviderBase.mm +845 -0
  28. package/ios/events/CameraIdleEvent.h +4 -2
  29. package/ios/events/CameraMoveEvent.h +4 -2
  30. package/ios/events/CirclePressEvent.h +4 -2
  31. package/ios/events/GroundOverlayPressEvent.h +4 -2
  32. package/ios/events/LongPressEvent.h +4 -2
  33. package/ios/events/MarkerDragEvent.h +12 -6
  34. package/ios/events/MarkerPressEvent.h +4 -2
  35. package/ios/events/PolygonPressEvent.h +4 -2
  36. package/ios/events/PressEvent.h +4 -2
  37. package/ios/events/ReadyEvent.h +4 -2
  38. package/ios/events/TileOverlayPressEvent.h +4 -2
  39. package/ios/extensions/MKMapView+Zoom.h +20 -0
  40. package/ios/extensions/MKMapView+Zoom.m +28 -18
  41. package/ios/extensions/UIView+Snapshot.h +16 -0
  42. package/ios/extensions/UIView+Snapshot.m +27 -0
  43. package/lib/module/MapView.js +7 -0
  44. package/lib/module/MapView.js.map +1 -1
  45. package/lib/module/MapView.web.js +9 -4
  46. package/lib/module/MapView.web.js.map +1 -1
  47. package/lib/module/fabric/LuggMapViewNativeComponent.ts +3 -0
  48. package/lib/typescript/src/MapView.d.ts.map +1 -1
  49. package/lib/typescript/src/MapView.types.d.ts +24 -0
  50. package/lib/typescript/src/MapView.types.d.ts.map +1 -1
  51. package/lib/typescript/src/MapView.web.d.ts.map +1 -1
  52. package/lib/typescript/src/fabric/LuggMapViewNativeComponent.d.ts +3 -0
  53. package/lib/typescript/src/fabric/LuggMapViewNativeComponent.d.ts.map +1 -1
  54. package/package.json +19 -12
  55. package/src/MapView.tsx +7 -0
  56. package/src/MapView.types.ts +24 -0
  57. package/src/MapView.web.tsx +15 -9
  58. package/src/fabric/LuggMapViewNativeComponent.ts +3 -0
  59. package/plugin/build/withMapsAndroid.d.ts +0 -5
  60. package/plugin/build/withMapsAndroid.js +0 -15
  61. package/plugin/build/withMapsIOS.d.ts +0 -5
  62. package/plugin/build/withMapsIOS.js +0 -27
@@ -0,0 +1,159 @@
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
+ // Inverse of LuggStaticFittedMapRect: the pre-fit rect is square in map
78
+ // points with side latitudeDelta * worldHeight / (360 * cos(lat)), and the
79
+ // aspect fit scales it to the view's short side
80
+ - (double)zoomForMapPointsPerPoint:(double)mapPointsPerPoint {
81
+ CGSize size = self.projectedSize;
82
+ double side = mapPointsPerPoint * MIN(size.width, size.height);
83
+ double latitudeDelta = side * 360.0 *
84
+ cos(self.coordinate.latitude * M_PI / 180.0) /
85
+ MKMapSizeWorld.height;
86
+ double zoom = 0.5 + log2(360.0 / latitudeDelta);
87
+ return MAX(MIN(zoom, 28.0), 0.0);
88
+ }
89
+
90
+ - (UITraitCollection *)snapshotTraitCollection {
91
+ UITraitCollection *base = self.wrapperView.traitCollection;
92
+ UIUserInterfaceStyle style;
93
+ switch (self.theme) {
94
+ case LuggMapViewTheme::Dark:
95
+ style = UIUserInterfaceStyleDark;
96
+ break;
97
+ case LuggMapViewTheme::Light:
98
+ style = UIUserInterfaceStyleLight;
99
+ break;
100
+ default:
101
+ return base;
102
+ }
103
+ return [UITraitCollection traitCollectionWithTraitsFromCollections:@[
104
+ base, [UITraitCollection traitCollectionWithUserInterfaceStyle:style]
105
+ ]];
106
+ }
107
+
108
+ - (void)renderBaseMap {
109
+ if (_snapshotter)
110
+ return;
111
+
112
+ MKMapSnapshotOptions *options = [[MKMapSnapshotOptions alloc] init];
113
+ options.mapRect = self.mapRect;
114
+ options.size = self.projectedSize;
115
+ options.mapType = LuggMKMapTypeFromMapType(self.mapType);
116
+ options.pointOfInterestFilter = LuggPointOfInterestFilter(
117
+ _poiEnabled, _poiFilterMode, _poiFilterCategories);
118
+ options.traitCollection = [self snapshotTraitCollection];
119
+
120
+ MKMapSnapshotter *snapshotter =
121
+ [[MKMapSnapshotter alloc] initWithOptions:options];
122
+ _snapshotter = snapshotter;
123
+
124
+ __weak AppleStaticMapProvider *weakSelf = self;
125
+ [snapshotter startWithQueue:dispatch_get_main_queue()
126
+ completionHandler:^(MKMapSnapshot *snapshot, NSError *error) {
127
+ AppleStaticMapProvider *strongSelf = weakSelf;
128
+ if (!strongSelf || strongSelf->_snapshotter != snapshotter)
129
+ return;
130
+ strongSelf->_snapshotter = nil;
131
+ if (error || !snapshot) {
132
+ // Keep the placeholder; resumeAnimations retries (e.g. back
133
+ // online)
134
+ return;
135
+ }
136
+ [strongSelf displayBaseImage:snapshot.image fromCache:NO];
137
+ }];
138
+ }
139
+
140
+ - (void)cancelBaseRender {
141
+ [_snapshotter cancel];
142
+ _snapshotter = nil;
143
+ }
144
+
145
+ #pragma mark - Props
146
+
147
+ - (void)setPoiEnabled:(BOOL)enabled {
148
+ _poiEnabled = enabled;
149
+ }
150
+
151
+ - (void)setPoiFilterMode:(LuggMapViewPoiFilterMode)mode {
152
+ _poiFilterMode = mode;
153
+ }
154
+
155
+ - (void)setPoiFilterCategories:(NSArray<NSString *> *)categories {
156
+ _poiFilterCategories = categories;
157
+ }
158
+
159
+ @end
@@ -148,13 +148,25 @@ static const NSUInteger kMaxGradientSpans = 512;
148
148
  }
149
149
 
150
150
  GMSMutablePath *path = [GMSMutablePath path];
151
- for (CLLocation *location in self.coordinates) {
152
- [path addCoordinate:location.coordinate];
151
+ CLLocationCoordinate2D lastCoord = self.coordinates.firstObject.coordinate;
152
+ [path addCoordinate:lastCoord];
153
+ for (NSUInteger i = 1; i < self.coordinates.count; i++) {
154
+ CLLocationCoordinate2D coord = self.coordinates[i].coordinate;
155
+ if (coord.latitude == lastCoord.latitude && coord.longitude == lastCoord.longitude) {
156
+ continue;
157
+ }
158
+ [path addCoordinate:coord];
159
+ lastCoord = coord;
153
160
  }
161
+
162
+ // Clear stale spans before swapping the path so Google Maps never rebuilds
163
+ // span models against a path with fewer segments (GMSModelStyleBuilder
164
+ // couldNotInstantiate -> EXC_BAD_ACCESS). Reassign spans once the new path is set.
165
+ _polyline.spans = nil;
154
166
  _polyline.path = path;
155
167
 
156
- if (self.strokeColors.count > 1) {
157
- _polyline.spans = [self createGradientSpans];
168
+ if (path.count > 1 && self.strokeColors.count > 1) {
169
+ _polyline.spans = [self createGradientSpansForSegmentCount:path.count - 1];
158
170
  } else {
159
171
  _polyline.strokeColor = self.strokeColors.firstObject ?: [UIColor blackColor];
160
172
  }
@@ -222,35 +234,44 @@ static const NSUInteger kMaxGradientSpans = 512;
222
234
  }
223
235
 
224
236
  if (headDist <= tailDist) {
237
+ _polyline.spans = nil;
225
238
  _polyline.path = [GMSMutablePath path];
226
239
  return;
227
240
  }
228
241
 
229
- CGFloat visibleLength = headDist - tailDist;
230
242
  NSUInteger startIndex = [self indexForDistance:tailDist];
231
243
  NSUInteger endIndex = [self indexForDistance:headDist];
232
244
 
233
245
  GMSMutablePath *path = [GMSMutablePath path];
234
- NSMutableArray<GMSStyleSpan *> *spans = [NSMutableArray array];
235
246
 
236
- CLLocationCoordinate2D startCoord = [self coordinateAtDistance:tailDist];
237
- [path addCoordinate:startCoord];
247
+ CLLocationCoordinate2D lastCoord = [self coordinateAtDistance:tailDist];
248
+ [path addCoordinate:lastCoord];
238
249
 
239
250
  for (NSUInteger i = startIndex + 1; i <= endIndex; i++) {
240
- [path addCoordinate:self.coordinates[i].coordinate];
251
+ CLLocationCoordinate2D coord = self.coordinates[i].coordinate;
252
+ if (coord.latitude == lastCoord.latitude && coord.longitude == lastCoord.longitude) {
253
+ continue;
254
+ }
255
+ [path addCoordinate:coord];
256
+ lastCoord = coord;
241
257
  }
242
258
 
243
259
  CLLocationCoordinate2D endCoord = [self coordinateAtDistance:headDist];
244
- CLLocationCoordinate2D lastAdded =
245
- (endIndex < self.coordinates.count) ? self.coordinates[endIndex].coordinate : endCoord;
246
- if (endCoord.latitude != lastAdded.latitude || endCoord.longitude != lastAdded.longitude) {
260
+ if (endCoord.latitude != lastCoord.latitude || endCoord.longitude != lastCoord.longitude) {
247
261
  [path addCoordinate:endCoord];
248
262
  }
249
263
 
250
264
  NSUInteger pathCount = path.count;
251
- NSUInteger segmentCount = pathCount - 1;
265
+ NSUInteger segmentCount = (pathCount > 1) ? pathCount - 1 : 0;
266
+
267
+ if (segmentCount == 0) {
268
+ _polyline.spans = nil;
269
+ _polyline.path = path;
270
+ return;
271
+ }
252
272
 
253
273
  if (self.strokeColors.count <= 1) {
274
+ _polyline.spans = nil;
254
275
  _polyline.path = path;
255
276
  _polyline.strokeColor = self.strokeColors.firstObject ?: [UIColor blackColor];
256
277
  return;
@@ -259,6 +280,7 @@ static const NSUInteger kMaxGradientSpans = 512;
259
280
  NSUInteger spanCount = MIN(segmentCount, kMaxAnimationSpans);
260
281
  double segmentsPerSpan = (double)segmentCount / spanCount;
261
282
 
283
+ NSMutableArray<GMSStyleSpan *> *spans = [NSMutableArray array];
262
284
  for (NSUInteger i = 0; i < spanCount; i++) {
263
285
  CGFloat gradientPos = ((CGFloat)i + 0.5) / spanCount;
264
286
  UIColor *color = [self colorAtGradientPosition:gradientPos];
@@ -266,13 +288,16 @@ static const NSUInteger kMaxGradientSpans = 512;
266
288
  [spans addObject:[GMSStyleSpan spanWithStyle:style segments:segmentsPerSpan]];
267
289
  }
268
290
 
291
+ // Clear stale spans before swapping the path so Google Maps never rebuilds
292
+ // span models against a path with fewer segments (GMSModelStyleBuilder
293
+ // couldNotInstantiate -> EXC_BAD_ACCESS). Reassign spans once the new path is set.
294
+ _polyline.spans = nil;
269
295
  _polyline.path = path;
270
296
  _polyline.spans = spans;
271
297
  }
272
298
 
273
- - (NSArray<GMSStyleSpan *> *)createGradientSpans {
299
+ - (NSArray<GMSStyleSpan *> *)createGradientSpansForSegmentCount:(NSUInteger)segmentCount {
274
300
  NSMutableArray<GMSStyleSpan *> *spans = [NSMutableArray array];
275
- NSUInteger segmentCount = self.coordinates.count - 1;
276
301
  NSUInteger spanCount = MIN(segmentCount, kMaxGradientSpans);
277
302
  double segmentsPerSpan = (double)segmentCount / spanCount;
278
303
 
@@ -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
@@ -166,6 +201,10 @@ static NSString *const kDemoMapId = @"DEMO_MAP_ID";
166
201
  _mapView.settings.tiltGestures = enabled;
167
202
  }
168
203
 
204
+ - (void)setCompassEnabled:(BOOL)enabled {
205
+ _mapView.settings.compassButton = enabled;
206
+ }
207
+
169
208
  - (void)setUserLocationEnabled:(BOOL)enabled {
170
209
  _mapView.myLocationEnabled = enabled;
171
210
  }
@@ -187,23 +226,7 @@ static NSString *const kDemoMapId = @"DEMO_MAP_ID";
187
226
  }
188
227
 
189
228
  - (void)setMapType:(LuggMapViewMapType)mapType {
190
- if (!_mapView)
191
- return;
192
-
193
- switch (mapType) {
194
- case LuggMapViewMapType::Satellite:
195
- _mapView.mapType = kGMSTypeSatellite;
196
- break;
197
- case LuggMapViewMapType::Terrain:
198
- _mapView.mapType = kGMSTypeTerrain;
199
- break;
200
- case LuggMapViewMapType::Hybrid:
201
- _mapView.mapType = kGMSTypeHybrid;
202
- break;
203
- default:
204
- _mapView.mapType = kGMSTypeNormal;
205
- break;
206
- }
229
+ _mapView.mapType = LuggGMSMapTypeFromMapType(mapType);
207
230
  }
208
231
 
209
232
  - (void)setTheme:(LuggMapViewTheme)theme {
@@ -212,20 +235,7 @@ static NSString *const kDemoMapId = @"DEMO_MAP_ID";
212
235
  }
213
236
 
214
237
  - (void)applyTheme {
215
- if (!_mapView)
216
- return;
217
-
218
- switch (_theme) {
219
- case LuggMapViewTheme::Dark:
220
- _mapView.overrideUserInterfaceStyle = UIUserInterfaceStyleDark;
221
- break;
222
- case LuggMapViewTheme::Light:
223
- _mapView.overrideUserInterfaceStyle = UIUserInterfaceStyleLight;
224
- break;
225
- default:
226
- _mapView.overrideUserInterfaceStyle = UIUserInterfaceStyleUnspecified;
227
- break;
228
- }
238
+ _mapView.overrideUserInterfaceStyle = LuggInterfaceStyleFromTheme(_theme);
229
239
  }
230
240
 
231
241
  - (void)setMinZoom:(double)minZoom {
@@ -1125,9 +1135,11 @@ static NSString *const kDemoMapId = @"DEMO_MAP_ID";
1125
1135
  return;
1126
1136
 
1127
1137
  float targetZoom = zoom > 0 ? (float)zoom : _mapView.camera.zoom;
1128
- GMSCameraPosition *camera = [GMSCameraPosition cameraWithLatitude:latitude
1129
- longitude:longitude
1130
- zoom:targetZoom];
1138
+ GMSCameraPosition *camera = [GMSCameraPosition
1139
+ cameraWithTarget:CLLocationCoordinate2DMake(latitude, longitude)
1140
+ zoom:targetZoom
1141
+ bearing:_mapView.camera.bearing
1142
+ viewingAngle:_mapView.camera.viewingAngle];
1131
1143
  if (duration < 0) {
1132
1144
  [_mapView animateToCameraPosition:camera];
1133
1145
  } else if (duration > 0) {
@@ -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,213 @@
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
+ // Inverse of mapRectForSize
78
+ - (double)zoomForMapPointsPerPoint:(double)mapPointsPerPoint {
79
+ double zoom = log2(MKMapSizeWorld.width / (256.0 * mapPointsPerPoint));
80
+ return MAX(MIN(zoom, (double)kGMSMaxZoomLevel), (double)kGMSMinZoomLevel);
81
+ }
82
+
83
+ - (void)renderBaseMap {
84
+ if (_warmupMapView) {
85
+ // Retry a swap that couldn't run (e.g. view was off-window)
86
+ [self scheduleSwap];
87
+ return;
88
+ }
89
+ [self startWarmup];
90
+ }
91
+
92
+ - (void)cancelBaseRender {
93
+ [self destroyWarmupMapView];
94
+ }
95
+
96
+ #pragma mark - Warmup
97
+
98
+ // "" resolves to the demo map ID (see LuggGMSMapIDFromString), so both
99
+ // share a pool bucket
100
+ - (NSString *)poolKey {
101
+ return self.mapId.length > 0 ? self.mapId : @"DEMO_MAP_ID";
102
+ }
103
+
104
+ - (void)startWarmup {
105
+ UIView *wrapperView = self.wrapperView;
106
+ if (!wrapperView)
107
+ return;
108
+
109
+ _tilesRendered = NO;
110
+
111
+ GMSMapView *mapView = DequeueStaticMapView([self poolKey]);
112
+ if (mapView) {
113
+ mapView.frame = wrapperView.bounds;
114
+ [mapView moveCamera:[GMSCameraUpdate setTarget:self.coordinate
115
+ zoom:(float)self.zoom]];
116
+ } else {
117
+ GMSCameraPosition *camera =
118
+ [GMSCameraPosition cameraWithLatitude:self.coordinate.latitude
119
+ longitude:self.coordinate.longitude
120
+ zoom:(float)self.zoom];
121
+
122
+ GMSMapViewOptions *options = [[GMSMapViewOptions alloc] init];
123
+ options.frame = wrapperView.bounds;
124
+ options.camera = camera;
125
+ options.mapID = LuggGMSMapIDFromString(self.mapId);
126
+
127
+ mapView = [[GMSMapView alloc] initWithOptions:options];
128
+ }
129
+
130
+ mapView.autoresizingMask =
131
+ UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
132
+ mapView.delegate = self;
133
+ mapView.userInteractionEnabled = NO;
134
+ mapView.preferredFrameRate = kGMSFrameRateConservative;
135
+ mapView.paddingAdjustmentBehavior = kGMSMapViewPaddingAdjustmentBehaviorNever;
136
+ // Frames the camera like the inset-shifted projection (and clears stale
137
+ // padding on pooled views)
138
+ mapView.padding = self.edgeInsets;
139
+ mapView.mapType = LuggGMSMapTypeFromMapType(self.mapType);
140
+ mapView.overrideUserInterfaceStyle = LuggInterfaceStyleFromTheme(self.theme);
141
+
142
+ _warmupMapView = mapView;
143
+ [wrapperView insertSubview:mapView atIndex:0];
144
+ }
145
+
146
+ - (void)destroyWarmupMapView {
147
+ if (!_warmupMapView)
148
+ return;
149
+ _warmupMapView.delegate = nil;
150
+ [_warmupMapView removeFromSuperview];
151
+ EnqueueStaticMapView([self poolKey], _warmupMapView);
152
+ _warmupMapView = nil;
153
+ }
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.
159
+ - (void)scheduleSwap {
160
+ if (_swapScheduled || !_warmupMapView || !_tilesRendered ||
161
+ !_warmupMapView.window)
162
+ return;
163
+
164
+ _swapScheduled = YES;
165
+ __weak GoogleStaticMapProvider *weakSelf = self;
166
+ [[NSRunLoop mainRunLoop] performInModes:@[ NSDefaultRunLoopMode ]
167
+ block:^{
168
+ GoogleStaticMapProvider *strongSelf =
169
+ weakSelf;
170
+ if (!strongSelf)
171
+ return;
172
+ strongSelf->_swapScheduled = NO;
173
+ [strongSelf performSwap];
174
+ }];
175
+ }
176
+
177
+ - (void)performSwap {
178
+ // Tiles can invalidate (or the view leave the window) between scheduling
179
+ // and this runloop pass; the next tile callback or resumeAnimations
180
+ // retries
181
+ if (!_warmupMapView || !_tilesRendered || !_warmupMapView.window)
182
+ return;
183
+
184
+ UIImageView *imageView = [_warmupMapView lugg_snapshotImageView];
185
+ if (!imageView)
186
+ return;
187
+
188
+ [self displayBaseImage:imageView.image fromCache:NO];
189
+ [self destroyWarmupMapView];
190
+ }
191
+
192
+ #pragma mark - GMSMapViewDelegate
193
+
194
+ - (void)mapViewDidStartTileRendering:(GMSMapView *)mapView {
195
+ _tilesRendered = NO;
196
+ }
197
+
198
+ - (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
+ [self revealOverlays];
203
+ [self scheduleSwap];
204
+ }
205
+
206
+ - (void)mapViewSnapshotReady:(GMSMapView *)mapView {
207
+ // Stable per the SDK: tiles loaded, labels and overlays rendered
208
+ _tilesRendered = YES;
209
+ [self revealOverlays];
210
+ [self scheduleSwap];
211
+ }
212
+
213
+ @end