@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.
- package/android/src/main/java/com/luggmaps/LuggMapWrapperView.kt +7 -0
- package/android/src/main/java/com/luggmaps/core/GoogleMapProvider.kt +90 -11
- package/ios/LuggMapView.mm +8 -7
- package/ios/core/GoogleStaticMapProvider.mm +3 -0
- package/ios/core/StaticMapProviderBase.h +1 -0
- package/ios/core/StaticMapProviderBase.mm +32 -0
- package/package.json +1 -1
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
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 && !
|
|
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 && !
|
|
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
|
-
|
|
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
|
}
|
package/ios/LuggMapView.mm
CHANGED
|
@@ -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
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|