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

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.
@@ -16,6 +16,10 @@ class LuggMapWrapperView(context: ThemedReactContext) : ReactViewGroup(context)
16
16
  // don't need this - their GL surface renders continuously once sized.
17
17
  var relayoutChildOnRequest: Boolean = false
18
18
 
19
+ // Invoked on layout once the view has a non-zero size, so the map
20
+ // provider can create a map sized to the view
21
+ var onLayoutReady: (() -> Unit)? = null
22
+
19
23
  private val measureAndLayoutChild = Runnable { layoutChild(width, height) }
20
24
 
21
25
  override fun dispatchTouchEvent(event: MotionEvent): Boolean {
@@ -39,6 +43,9 @@ class LuggMapWrapperView(context: ThemedReactContext) : ReactViewGroup(context)
39
43
  ) {
40
44
  super.onLayout(changed, left, top, right, bottom)
41
45
  layoutChild(right - left, bottom - top)
46
+ if (right - left > 0 && bottom - top > 0) {
47
+ onLayoutReady?.invoke()
48
+ }
42
49
  }
43
50
 
44
51
  private fun layoutChild(w: Int, h: Int) {
@@ -48,7 +48,9 @@ import com.luggmaps.LuggTileOverlayViewDelegate
48
48
  import com.luggmaps.extensions.findViewByTag
49
49
  import java.net.URL
50
50
  import kotlin.math.atan
51
+ import kotlin.math.ln
51
52
  import kotlin.math.pow
53
+ import kotlin.math.sin
52
54
  import kotlin.math.sinh
53
55
 
54
56
  class GoogleMapProvider(private val context: Context) :
@@ -78,7 +80,8 @@ class GoogleMapProvider(private val context: Context) :
78
80
 
79
81
  var mapId: String = DEMO_MAP_ID
80
82
 
81
- // Renders the map in lite mode (static bitmap). Creation-time only.
83
+ // Renders a non-interactive static map — lite mode when the view fits
84
+ // the lite bitmap cap, a gesture-less full map otherwise. Creation-time only.
82
85
  var staticMode: Boolean = false
83
86
 
84
87
  private var wrapperView: LuggMapWrapperView? = null
@@ -99,7 +102,7 @@ class GoogleMapProvider(private val context: Context) :
99
102
  private val groundOverlayToViewMap = mutableMapOf<GroundOverlay, LuggGroundOverlayView>()
100
103
  private val markerToViewMap = mutableMapOf<Marker, LuggMarkerView>()
101
104
  private val liveMarkerViews = mutableSetOf<LuggMarkerView>()
102
- private var liteProjectionReady = false
105
+ private var staticProjectionReady = false
103
106
  private var activeNonBubbledMarker: Marker? = null
104
107
  private var tapLocation: LatLng? = null
105
108
 
@@ -146,19 +149,38 @@ class GoogleMapProvider(private val context: Context) :
146
149
 
147
150
  context.applicationContext.registerComponentCallbacks(this)
148
151
 
152
+ // Static mode needs the view size to pick lite vs full map, so wait
153
+ // for the first layout if it hasn't happened yet
154
+ if (staticMode && (wrapper.width == 0 || wrapper.height == 0)) {
155
+ wrapper.onLayoutReady = { createMapView() }
156
+ } else {
157
+ createMapView()
158
+ }
159
+ }
160
+
161
+ private fun createMapView() {
162
+ if (mapView != null) return
163
+ val wrapper = wrapperView ?: return
164
+
165
+ // Lite mode renders the map as a single bitmap capped at ~2048px per
166
+ // dimension, centered in the view — bigger views get letterboxed. Fall
167
+ // back to a full map (gestures disabled) when the view exceeds the cap
168
+ val useLite = staticMode && maxOf(wrapper.width, wrapper.height) <= LITE_MODE_MAX_SIZE
169
+ val options = GoogleMapOptions().liteMode(useLite)
149
170
  // Lite mode doesn't support map IDs (cloud-based styling); setting one
150
171
  // makes the static map render blank
151
- val options = GoogleMapOptions().liteMode(staticMode)
152
- if (!staticMode) options.mapId(mapId)
172
+ if (!useLite) options.mapId(mapId)
153
173
  mapView = MapView(context, options).also { view ->
154
174
  view.onCreate(null)
155
175
  view.onResume()
156
176
  view.getMapAsync(this)
157
177
  wrapper.addView(view)
158
178
  }
179
+ wrapper.onLayoutReady = null
159
180
  }
160
181
 
161
182
  override fun destroy() {
183
+ wrapperView?.onLayoutReady = null
162
184
  detachWindowInsetsListener()
163
185
  context.applicationContext.unregisterComponentCallbacks(this)
164
186
  dismissNonBubbledCallout()
@@ -204,17 +226,17 @@ class GoogleMapProvider(private val context: Context) :
204
226
  _isMapReady = true
205
227
 
206
228
  if (staticMode) {
207
- // Lite mode shows an "open in Google Maps" toolbar on tap by default
229
+ // Tapping the map otherwise shows an "open in Google Maps" toolbar
208
230
  map.uiSettings.isMapToolbarEnabled = false
209
- // The projection is only valid once the lite map has rendered, and no
231
+ // The projection is only valid once the map has rendered, and no
210
232
  // camera events fire afterwards to correct live marker positions
211
233
  map.setOnMapLoadedCallback {
212
- liteProjectionReady = true
234
+ staticProjectionReady = true
213
235
  positionLiveMarkers()
214
236
  }
215
237
  }
216
238
 
217
- val position = LatLng(initialLatitude, initialLongitude)
239
+ val position = if (staticMode) staticCameraTarget() else LatLng(initialLatitude, initialLongitude)
218
240
  map.moveCamera(CameraUpdateFactory.newLatLngZoom(position, initialZoom))
219
241
 
220
242
  map.setOnCameraMoveStartedListener(this)
@@ -461,21 +483,25 @@ class GoogleMapProvider(private val context: Context) :
461
483
 
462
484
  override fun setZoomEnabled(enabled: Boolean) {
463
485
  zoomEnabled = enabled
486
+ if (staticMode) return
464
487
  googleMap?.uiSettings?.isZoomGesturesEnabled = enabled
465
488
  }
466
489
 
467
490
  override fun setScrollEnabled(enabled: Boolean) {
468
491
  scrollEnabled = enabled
492
+ if (staticMode) return
469
493
  googleMap?.uiSettings?.isScrollGesturesEnabled = enabled
470
494
  }
471
495
 
472
496
  override fun setRotateEnabled(enabled: Boolean) {
473
497
  rotateEnabled = enabled
498
+ if (staticMode) return
474
499
  googleMap?.uiSettings?.isRotateGesturesEnabled = enabled
475
500
  }
476
501
 
477
502
  override fun setPitchEnabled(enabled: Boolean) {
478
503
  pitchEnabled = enabled
504
+ if (staticMode) return
479
505
  googleMap?.uiSettings?.isTiltGesturesEnabled = enabled
480
506
  }
481
507
 
@@ -530,6 +556,11 @@ class GoogleMapProvider(private val context: Context) :
530
556
  }
531
557
 
532
558
  override fun setEdgeInsets(edgeInsets: EdgeInsets) {
559
+ if (staticMode) {
560
+ setStaticEdgeInsets(edgeInsets)
561
+ return
562
+ }
563
+
533
564
  val oldInsets = this.edgeInsets
534
565
  this.edgeInsets = edgeInsets
535
566
  applyEdgeInsets()
@@ -543,6 +574,11 @@ class GoogleMapProvider(private val context: Context) :
543
574
  }
544
575
 
545
576
  override fun setEdgeInsets(edgeInsets: EdgeInsets, duration: Int) {
577
+ if (staticMode) {
578
+ setStaticEdgeInsets(edgeInsets)
579
+ return
580
+ }
581
+
546
582
  val map = googleMap
547
583
  val oldInsets = this.edgeInsets
548
584
  this.edgeInsets = edgeInsets
@@ -759,7 +795,7 @@ class GoogleMapProvider(private val context: Context) :
759
795
  // Until the lite map's projection is valid, hide the marker instead of
760
796
  // flashing it at a wrong position; onMapLoaded positions and shows it
761
797
  contentView.visibility =
762
- if (staticMode && !liteProjectionReady) View.INVISIBLE else View.VISIBLE
798
+ if (staticMode && !staticProjectionReady) View.INVISIBLE else View.VISIBLE
763
799
  (contentView.parent as? android.view.ViewGroup)?.removeView(contentView)
764
800
  wrapper.addView(contentView)
765
801
  liveMarkerViews.add(markerView)
@@ -791,7 +827,7 @@ class GoogleMapProvider(private val context: Context) :
791
827
 
792
828
  private fun positionLiveMarker(markerView: LuggMarkerView) {
793
829
  val map = googleMap ?: return
794
- if (staticMode && !liteProjectionReady) return
830
+ if (staticMode && !staticProjectionReady) return
795
831
  val contentView = markerView.contentView
796
832
  val point = map.projection.toScreenLocation(LatLng(markerView.latitude, markerView.longitude))
797
833
  contentView.translationX = point.x - contentView.width * markerView.anchorX
@@ -1230,6 +1266,12 @@ class GoogleMapProvider(private val context: Context) :
1230
1266
 
1231
1267
  private fun applyUiSettings() {
1232
1268
  googleMap?.uiSettings?.apply {
1269
+ if (staticMode) {
1270
+ // The non-lite fallback is a full map; keep it behaving like a
1271
+ // static image (lite mode ignores gestures anyway)
1272
+ setAllGesturesEnabled(false)
1273
+ return
1274
+ }
1233
1275
  isZoomGesturesEnabled = zoomEnabled
1234
1276
  isScrollGesturesEnabled = scrollEnabled
1235
1277
  isRotateGesturesEnabled = rotateEnabled
@@ -1270,10 +1312,43 @@ class GoogleMapProvider(private val context: Context) :
1270
1312
 
1271
1313
  private fun applyEdgeInsets(duration: Int = 0) {
1272
1314
  val combined = combinedEdgeInsets()
1273
- googleMap?.setPadding(combined.left, combined.top, combined.right, combined.bottom)
1315
+ // Lite mode misrenders padding: the static image is drawn only within
1316
+ // the padded viewport instead of the full view. The camera target shift
1317
+ // (staticCameraTarget) compensates for insets instead.
1318
+ if (!staticMode) {
1319
+ googleMap?.setPadding(combined.left, combined.top, combined.right, combined.bottom)
1320
+ }
1274
1321
  applyWatermarkTranslation(combined, duration)
1275
1322
  }
1276
1323
 
1324
+ // Insets are applied by shifting the camera target so the coordinate
1325
+ // centers in the inset viewport like a live map would (see
1326
+ // applyEdgeInsets for why padding can't be used)
1327
+ private fun setStaticEdgeInsets(edgeInsets: EdgeInsets) {
1328
+ val oldInsets = this.edgeInsets
1329
+ this.edgeInsets = edgeInsets
1330
+ applyEdgeInsets()
1331
+
1332
+ val map = googleMap
1333
+ if (map != null && oldInsets != edgeInsets) {
1334
+ map.moveCamera(CameraUpdateFactory.newLatLngZoom(staticCameraTarget(), initialZoom))
1335
+ positionLiveMarkers()
1336
+ }
1337
+ }
1338
+
1339
+ private fun staticCameraTarget(): LatLng {
1340
+ val offsetX = (edgeInsets.left - edgeInsets.right) / 2.0
1341
+ val offsetY = (edgeInsets.top - edgeInsets.bottom) / 2.0
1342
+ if (offsetX == 0.0 && offsetY == 0.0) return LatLng(initialLatitude, initialLongitude)
1343
+
1344
+ // Web mercator world size in px at the initial zoom
1345
+ val worldSize = 256f.dpToPx().toDouble() * 2.0.pow(initialZoom.toDouble())
1346
+ val sinLat = sin(Math.toRadians(initialLatitude))
1347
+ val x = (initialLongitude + 180.0) / 360.0 - offsetX / worldSize
1348
+ val y = 0.5 - ln((1.0 + sinLat) / (1.0 - sinLat)) / (4.0 * Math.PI) - offsetY / worldSize
1349
+ return LatLng(Math.toDegrees(atan(sinh(Math.PI * (1.0 - 2.0 * y)))), x * 360.0 - 180.0)
1350
+ }
1351
+
1277
1352
  private fun applyWatermarkTranslation(insets: EdgeInsets, duration: Int = 0) {
1278
1353
  val view = mapView ?: return
1279
1354
  view.findViewByTag("GoogleWatermark")?.let { watermark ->
@@ -1350,5 +1425,9 @@ class GoogleMapProvider(private val context: Context) :
1350
1425
 
1351
1426
  companion object {
1352
1427
  const val DEMO_MAP_ID = "DEMO_MAP_ID"
1428
+
1429
+ // Undocumented cap on the lite mode bitmap (2048 minus a 2px border,
1430
+ // measured empirically)
1431
+ private const val LITE_MODE_MAX_SIZE = 2046
1353
1432
  }
1354
1433
  }
@@ -252,13 +252,14 @@ static NSCache<NSString *, UIImage *> *StaticSnapshotCache(void) {
252
252
  const auto &viewProps =
253
253
  *std::static_pointer_cast<LuggMapViewProps const>(_props);
254
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];
255
+ return [NSString
256
+ stringWithFormat:@"%@|%d|%@|%d|%ld|%.0fx%.0f|%.6f,%.6f,%.2f|%@",
257
+ _staticKey, (int)_providerType, _mapId, (int)_mapType,
258
+ (long)style, size.width, size.height,
259
+ viewProps.initialCoordinate.latitude,
260
+ viewProps.initialCoordinate.longitude,
261
+ viewProps.initialZoom,
262
+ NSStringFromUIEdgeInsets(_edgeInsets)];
262
263
  }
263
264
 
264
265
  - (void)startStaticContent {
@@ -127,6 +127,9 @@ static void EnqueueStaticMapView(NSString *mapId, GMSMapView *mapView) {
127
127
  mapView.userInteractionEnabled = NO;
128
128
  mapView.preferredFrameRate = kGMSFrameRateConservative;
129
129
  mapView.paddingAdjustmentBehavior = kGMSMapViewPaddingAdjustmentBehaviorNever;
130
+ // Frames the camera like the inset-shifted projection (and clears stale
131
+ // padding on pooled views)
132
+ mapView.padding = self.edgeInsets;
130
133
  mapView.mapType = LuggGMSMapTypeFromMapType(self.mapType);
131
134
  mapView.overrideUserInterfaceStyle = LuggInterfaceStyleFromTheme(self.theme);
132
135
 
@@ -33,6 +33,7 @@ CGPoint LuggStaticPointForCoordinate(MKMapRect mapRect, CGSize size,
33
33
  @property(nonatomic, readonly) double zoom;
34
34
  @property(nonatomic, readonly) MKMapRect mapRect;
35
35
  @property(nonatomic, readonly) CGSize projectedSize;
36
+ @property(nonatomic, readonly) UIEdgeInsets edgeInsets;
36
37
  @property(nonatomic, readonly) BOOL projectionReady;
37
38
  @property(nonatomic, readonly) facebook::react::LuggMapViewMapType mapType;
38
39
  @property(nonatomic, readonly) facebook::react::LuggMapViewTheme theme;
@@ -348,6 +348,14 @@ static NSInteger shapeZIndex(UIView *shape) {
348
348
 
349
349
  _projectedSize = size;
350
350
  _mapRect = [self mapRectForSize:size];
351
+
352
+ // Edge insets shift the visible center like a live map: the coordinate
353
+ // lands at the center of the inset viewport instead of the view center
354
+ CGFloat offsetX = (_edgeInsets.left - _edgeInsets.right) / 2.0;
355
+ CGFloat offsetY = (_edgeInsets.top - _edgeInsets.bottom) / 2.0;
356
+ _mapRect.origin.x -= offsetX * _mapRect.size.width / size.width;
357
+ _mapRect.origin.y -= offsetY * _mapRect.size.height / size.height;
358
+
351
359
  _projectionReady = YES;
352
360
 
353
361
  _shapeOverlayView.mapRect = _mapRect;
@@ -471,10 +479,34 @@ static NSInteger shapeZIndex(UIView *shape) {
471
479
  }
472
480
  - (void)setEdgeInsets:(UIEdgeInsets)edgeInsets
473
481
  oldEdgeInsets:(UIEdgeInsets)oldEdgeInsets {
482
+ if (UIEdgeInsetsEqualToEdgeInsets(_edgeInsets, edgeInsets))
483
+ return;
484
+ _edgeInsets = edgeInsets;
485
+
486
+ if (!_projectionReady)
487
+ return;
488
+
489
+ [self updateProjection];
490
+
491
+ // Any in-flight or completed base render targets the old projection. The
492
+ // initial prop set lands before the first render, so the cached image
493
+ // (keyed on insets) stays valid.
494
+ [self cancelBaseRender];
495
+ if (_baseRenderDone) {
496
+ _baseRenderDone = NO;
497
+ self.cachedBaseImage = nil;
498
+ }
499
+ __weak StaticMapProviderBase *weakSelf = self;
500
+ dispatch_async(dispatch_get_main_queue(), ^{
501
+ [weakSelf renderBaseMapIfNeeded];
502
+ });
474
503
  }
504
+
505
+ // Static maps don't animate; apply the final insets directly
475
506
  - (void)setEdgeInsets:(UIEdgeInsets)edgeInsets
476
507
  oldEdgeInsets:(UIEdgeInsets)oldEdgeInsets
477
508
  duration:(double)duration {
509
+ [self setEdgeInsets:edgeInsets oldEdgeInsets:oldEdgeInsets];
478
510
  }
479
511
 
480
512
  #pragma mark - Markers
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lugg/maps",
3
- "version": "1.0.0-beta.18",
3
+ "version": "1.0.0-beta.19",
4
4
  "description": "Universal maps for React Native.",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",