@lugg/maps 1.0.0-beta.17 → 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/LuggMapView.kt +33 -0
- package/android/src/main/java/com/luggmaps/LuggMapViewManager.kt +9 -0
- package/android/src/main/java/com/luggmaps/LuggMapWrapperView.kt +27 -2
- package/android/src/main/java/com/luggmaps/core/GoogleMapProvider.kt +110 -5
- package/ios/LuggMapView.mm +153 -2
- package/ios/LuggMapWrapperView.h +5 -0
- package/ios/LuggMapWrapperView.mm +18 -0
- package/ios/LuggMarkerView.mm +2 -0
- package/ios/core/AppleMapProvider.h +8 -0
- package/ios/core/AppleMapProvider.mm +41 -28
- package/ios/core/AppleStaticMapProvider.h +14 -0
- package/ios/core/AppleStaticMapProvider.mm +146 -0
- package/ios/core/GoogleMapProvider.h +10 -0
- package/ios/core/GoogleMapProvider.mm +47 -41
- package/ios/core/GoogleStaticMapProvider.h +21 -0
- package/ios/core/GoogleStaticMapProvider.mm +207 -0
- package/ios/core/MKPolylineAnimator.m +67 -43
- package/ios/core/MapProviderDelegate.h +6 -0
- package/ios/core/StaticMapProviderBase.h +70 -0
- package/ios/core/StaticMapProviderBase.mm +777 -0
- package/ios/extensions/MKMapView+Zoom.h +14 -0
- package/ios/extensions/MKMapView+Zoom.m +17 -15
- package/ios/extensions/UIView+Snapshot.h +16 -0
- package/ios/extensions/UIView+Snapshot.m +27 -0
- package/lib/module/MapView.js +4 -0
- package/lib/module/MapView.js.map +1 -1
- package/lib/module/MapView.web.js +8 -5
- package/lib/module/MapView.web.js.map +1 -1
- package/lib/module/fabric/LuggMapViewNativeComponent.ts +2 -0
- package/lib/typescript/src/MapView.d.ts.map +1 -1
- package/lib/typescript/src/MapView.types.d.ts +18 -0
- package/lib/typescript/src/MapView.types.d.ts.map +1 -1
- package/lib/typescript/src/MapView.web.d.ts.map +1 -1
- package/lib/typescript/src/fabric/LuggMapViewNativeComponent.d.ts +2 -0
- package/lib/typescript/src/fabric/LuggMapViewNativeComponent.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/MapView.tsx +4 -0
- package/src/MapView.types.ts +18 -0
- package/src/MapView.web.tsx +14 -10
- package/src/fabric/LuggMapViewNativeComponent.ts +2 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
package com.luggmaps
|
|
2
2
|
|
|
3
3
|
import android.annotation.SuppressLint
|
|
4
|
+
import android.content.res.Configuration
|
|
4
5
|
import android.view.View
|
|
5
6
|
import android.view.View.VISIBLE
|
|
6
7
|
import com.facebook.react.uimanager.ThemedReactContext
|
|
@@ -65,6 +66,7 @@ class LuggMapView(private val reactContext: ThemedReactContext) :
|
|
|
65
66
|
private var rotateEnabled: Boolean = true
|
|
66
67
|
private var pitchEnabled: Boolean = true
|
|
67
68
|
private var compassEnabled: Boolean = true
|
|
69
|
+
private var staticMode: Boolean = false
|
|
68
70
|
private var userLocationEnabled: Boolean = false
|
|
69
71
|
private var userLocationButtonEnabled: Boolean = false
|
|
70
72
|
private var poiEnabled: Boolean = true
|
|
@@ -132,8 +134,13 @@ class LuggMapView(private val reactContext: ThemedReactContext) :
|
|
|
132
134
|
private fun initializeProvider() {
|
|
133
135
|
if (provider != null || mapWrapperView == null) return
|
|
134
136
|
|
|
137
|
+
if (staticMode) {
|
|
138
|
+
applyStaticPlaceholder()
|
|
139
|
+
}
|
|
140
|
+
|
|
135
141
|
val google = GoogleMapProvider(context)
|
|
136
142
|
google.mapId = mapId
|
|
143
|
+
google.staticMode = staticMode
|
|
137
144
|
google.delegate = this
|
|
138
145
|
provider = google
|
|
139
146
|
|
|
@@ -154,6 +161,23 @@ class LuggMapView(private val reactContext: ThemedReactContext) :
|
|
|
154
161
|
}
|
|
155
162
|
}
|
|
156
163
|
|
|
164
|
+
// Shown while the map loads, until the map covers it
|
|
165
|
+
private fun applyStaticPlaceholder() {
|
|
166
|
+
// A user-provided background acts as the placeholder
|
|
167
|
+
if (background != null) return
|
|
168
|
+
|
|
169
|
+
val isDark = when (theme) {
|
|
170
|
+
"dark" -> true
|
|
171
|
+
|
|
172
|
+
"light" -> false
|
|
173
|
+
|
|
174
|
+
else ->
|
|
175
|
+
(context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) ==
|
|
176
|
+
Configuration.UI_MODE_NIGHT_YES
|
|
177
|
+
}
|
|
178
|
+
mapWrapperView?.setBackgroundColor(if (isDark) COLOR_PLACEHOLDER_DARK else COLOR_PLACEHOLDER_LIGHT)
|
|
179
|
+
}
|
|
180
|
+
|
|
157
181
|
// endregion
|
|
158
182
|
|
|
159
183
|
// region MapProviderDelegate
|
|
@@ -251,6 +275,10 @@ class LuggMapView(private val reactContext: ThemedReactContext) :
|
|
|
251
275
|
provider?.setCompassEnabled(enabled)
|
|
252
276
|
}
|
|
253
277
|
|
|
278
|
+
fun setStaticMode(enabled: Boolean) {
|
|
279
|
+
staticMode = enabled
|
|
280
|
+
}
|
|
281
|
+
|
|
254
282
|
fun setUserLocationEnabled(enabled: Boolean) {
|
|
255
283
|
if (userLocationEnabled == enabled) return
|
|
256
284
|
userLocationEnabled = enabled
|
|
@@ -344,4 +372,9 @@ class LuggMapView(private val reactContext: ThemedReactContext) :
|
|
|
344
372
|
}
|
|
345
373
|
|
|
346
374
|
// endregion
|
|
375
|
+
|
|
376
|
+
companion object {
|
|
377
|
+
private const val COLOR_PLACEHOLDER_LIGHT = 0xFFF2F2F7.toInt()
|
|
378
|
+
private const val COLOR_PLACEHOLDER_DARK = 0xFF2C2C2E.toInt()
|
|
379
|
+
}
|
|
347
380
|
}
|
|
@@ -137,6 +137,15 @@ class LuggMapViewManager :
|
|
|
137
137
|
view.setCompassEnabled(value)
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
@ReactProp(name = "staticMode", defaultBoolean = false)
|
|
141
|
+
override fun setStaticMode(view: LuggMapView, value: Boolean) {
|
|
142
|
+
view.setStaticMode(value)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// iOS only - lite mode doesn't need snapshot caching
|
|
146
|
+
@ReactProp(name = "staticKey")
|
|
147
|
+
override fun setStaticKey(view: LuggMapView, value: String?) = Unit
|
|
148
|
+
|
|
140
149
|
@ReactProp(name = "userLocationEnabled", defaultBoolean = false)
|
|
141
150
|
override fun setUserLocationEnabled(view: LuggMapView, value: Boolean) {
|
|
142
151
|
view.setUserLocationEnabled(value)
|
|
@@ -10,11 +10,30 @@ class LuggMapWrapperView(context: ThemedReactContext) : ReactViewGroup(context)
|
|
|
10
10
|
|
|
11
11
|
var touchEventHandler: ((MotionEvent) -> Unit)? = null
|
|
12
12
|
|
|
13
|
+
// React Native never runs an Android layout pass on Yoga-managed views, so
|
|
14
|
+
// a child's requestLayout (e.g. a lite mode MapView displaying its bitmap
|
|
15
|
+
// once loaded) would otherwise never result in a measure/layout. Live maps
|
|
16
|
+
// don't need this - their GL surface renders continuously once sized.
|
|
17
|
+
var relayoutChildOnRequest: Boolean = false
|
|
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
|
+
|
|
23
|
+
private val measureAndLayoutChild = Runnable { layoutChild(width, height) }
|
|
24
|
+
|
|
13
25
|
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
|
|
14
26
|
touchEventHandler?.invoke(event)
|
|
15
27
|
return super.dispatchTouchEvent(event)
|
|
16
28
|
}
|
|
17
29
|
|
|
30
|
+
override fun requestLayout() {
|
|
31
|
+
super.requestLayout()
|
|
32
|
+
if (relayoutChildOnRequest) {
|
|
33
|
+
post(measureAndLayoutChild)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
18
37
|
override fun onLayout(
|
|
19
38
|
changed: Boolean,
|
|
20
39
|
left: Int,
|
|
@@ -23,8 +42,14 @@ class LuggMapWrapperView(context: ThemedReactContext) : ReactViewGroup(context)
|
|
|
23
42
|
bottom: Int
|
|
24
43
|
) {
|
|
25
44
|
super.onLayout(changed, left, top, right, bottom)
|
|
26
|
-
|
|
27
|
-
|
|
45
|
+
layoutChild(right - left, bottom - top)
|
|
46
|
+
if (right - left > 0 && bottom - top > 0) {
|
|
47
|
+
onLayoutReady?.invoke()
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private fun layoutChild(w: Int, h: Int) {
|
|
52
|
+
if (w <= 0 || h <= 0) return
|
|
28
53
|
getChildAt(0)?.let {
|
|
29
54
|
it.measure(
|
|
30
55
|
MeasureSpec.makeMeasureSpec(w, MeasureSpec.EXACTLY),
|
|
@@ -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,6 +80,10 @@ class GoogleMapProvider(private val context: Context) :
|
|
|
78
80
|
|
|
79
81
|
var mapId: String = DEMO_MAP_ID
|
|
80
82
|
|
|
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.
|
|
85
|
+
var staticMode: Boolean = false
|
|
86
|
+
|
|
81
87
|
private var wrapperView: LuggMapWrapperView? = null
|
|
82
88
|
private var currentNightMode: Int = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
|
|
83
89
|
private var mapView: MapView? = null
|
|
@@ -96,6 +102,7 @@ class GoogleMapProvider(private val context: Context) :
|
|
|
96
102
|
private val groundOverlayToViewMap = mutableMapOf<GroundOverlay, LuggGroundOverlayView>()
|
|
97
103
|
private val markerToViewMap = mutableMapOf<Marker, LuggMarkerView>()
|
|
98
104
|
private val liveMarkerViews = mutableSetOf<LuggMarkerView>()
|
|
105
|
+
private var staticProjectionReady = false
|
|
99
106
|
private var activeNonBubbledMarker: Marker? = null
|
|
100
107
|
private var tapLocation: LatLng? = null
|
|
101
108
|
|
|
@@ -137,20 +144,43 @@ class GoogleMapProvider(private val context: Context) :
|
|
|
137
144
|
initialZoom = zoom
|
|
138
145
|
|
|
139
146
|
val wrapper = wrapperView as LuggMapWrapperView
|
|
147
|
+
wrapper.relayoutChildOnRequest = staticMode
|
|
140
148
|
this.wrapperView = wrapper
|
|
141
149
|
|
|
142
150
|
context.applicationContext.registerComponentCallbacks(this)
|
|
143
151
|
|
|
144
|
-
|
|
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)
|
|
170
|
+
// Lite mode doesn't support map IDs (cloud-based styling); setting one
|
|
171
|
+
// makes the static map render blank
|
|
172
|
+
if (!useLite) options.mapId(mapId)
|
|
145
173
|
mapView = MapView(context, options).also { view ->
|
|
146
174
|
view.onCreate(null)
|
|
147
175
|
view.onResume()
|
|
148
176
|
view.getMapAsync(this)
|
|
149
177
|
wrapper.addView(view)
|
|
150
178
|
}
|
|
179
|
+
wrapper.onLayoutReady = null
|
|
151
180
|
}
|
|
152
181
|
|
|
153
182
|
override fun destroy() {
|
|
183
|
+
wrapperView?.onLayoutReady = null
|
|
154
184
|
detachWindowInsetsListener()
|
|
155
185
|
context.applicationContext.unregisterComponentCallbacks(this)
|
|
156
186
|
dismissNonBubbledCallout()
|
|
@@ -195,7 +225,18 @@ class GoogleMapProvider(private val context: Context) :
|
|
|
195
225
|
googleMap = map
|
|
196
226
|
_isMapReady = true
|
|
197
227
|
|
|
198
|
-
|
|
228
|
+
if (staticMode) {
|
|
229
|
+
// Tapping the map otherwise shows an "open in Google Maps" toolbar
|
|
230
|
+
map.uiSettings.isMapToolbarEnabled = false
|
|
231
|
+
// The projection is only valid once the map has rendered, and no
|
|
232
|
+
// camera events fire afterwards to correct live marker positions
|
|
233
|
+
map.setOnMapLoadedCallback {
|
|
234
|
+
staticProjectionReady = true
|
|
235
|
+
positionLiveMarkers()
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
val position = if (staticMode) staticCameraTarget() else LatLng(initialLatitude, initialLongitude)
|
|
199
240
|
map.moveCamera(CameraUpdateFactory.newLatLngZoom(position, initialZoom))
|
|
200
241
|
|
|
201
242
|
map.setOnCameraMoveStartedListener(this)
|
|
@@ -442,21 +483,25 @@ class GoogleMapProvider(private val context: Context) :
|
|
|
442
483
|
|
|
443
484
|
override fun setZoomEnabled(enabled: Boolean) {
|
|
444
485
|
zoomEnabled = enabled
|
|
486
|
+
if (staticMode) return
|
|
445
487
|
googleMap?.uiSettings?.isZoomGesturesEnabled = enabled
|
|
446
488
|
}
|
|
447
489
|
|
|
448
490
|
override fun setScrollEnabled(enabled: Boolean) {
|
|
449
491
|
scrollEnabled = enabled
|
|
492
|
+
if (staticMode) return
|
|
450
493
|
googleMap?.uiSettings?.isScrollGesturesEnabled = enabled
|
|
451
494
|
}
|
|
452
495
|
|
|
453
496
|
override fun setRotateEnabled(enabled: Boolean) {
|
|
454
497
|
rotateEnabled = enabled
|
|
498
|
+
if (staticMode) return
|
|
455
499
|
googleMap?.uiSettings?.isRotateGesturesEnabled = enabled
|
|
456
500
|
}
|
|
457
501
|
|
|
458
502
|
override fun setPitchEnabled(enabled: Boolean) {
|
|
459
503
|
pitchEnabled = enabled
|
|
504
|
+
if (staticMode) return
|
|
460
505
|
googleMap?.uiSettings?.isTiltGesturesEnabled = enabled
|
|
461
506
|
}
|
|
462
507
|
|
|
@@ -511,6 +556,11 @@ class GoogleMapProvider(private val context: Context) :
|
|
|
511
556
|
}
|
|
512
557
|
|
|
513
558
|
override fun setEdgeInsets(edgeInsets: EdgeInsets) {
|
|
559
|
+
if (staticMode) {
|
|
560
|
+
setStaticEdgeInsets(edgeInsets)
|
|
561
|
+
return
|
|
562
|
+
}
|
|
563
|
+
|
|
514
564
|
val oldInsets = this.edgeInsets
|
|
515
565
|
this.edgeInsets = edgeInsets
|
|
516
566
|
applyEdgeInsets()
|
|
@@ -524,6 +574,11 @@ class GoogleMapProvider(private val context: Context) :
|
|
|
524
574
|
}
|
|
525
575
|
|
|
526
576
|
override fun setEdgeInsets(edgeInsets: EdgeInsets, duration: Int) {
|
|
577
|
+
if (staticMode) {
|
|
578
|
+
setStaticEdgeInsets(edgeInsets)
|
|
579
|
+
return
|
|
580
|
+
}
|
|
581
|
+
|
|
527
582
|
val map = googleMap
|
|
528
583
|
val oldInsets = this.edgeInsets
|
|
529
584
|
this.edgeInsets = edgeInsets
|
|
@@ -737,6 +792,10 @@ class GoogleMapProvider(private val context: Context) :
|
|
|
737
792
|
|
|
738
793
|
val contentView = markerView.contentView
|
|
739
794
|
contentView.pointerEvents = com.facebook.react.uimanager.PointerEvents.NONE
|
|
795
|
+
// Until the lite map's projection is valid, hide the marker instead of
|
|
796
|
+
// flashing it at a wrong position; onMapLoaded positions and shows it
|
|
797
|
+
contentView.visibility =
|
|
798
|
+
if (staticMode && !staticProjectionReady) View.INVISIBLE else View.VISIBLE
|
|
740
799
|
(contentView.parent as? android.view.ViewGroup)?.removeView(contentView)
|
|
741
800
|
wrapper.addView(contentView)
|
|
742
801
|
liveMarkerViews.add(markerView)
|
|
@@ -768,10 +827,12 @@ class GoogleMapProvider(private val context: Context) :
|
|
|
768
827
|
|
|
769
828
|
private fun positionLiveMarker(markerView: LuggMarkerView) {
|
|
770
829
|
val map = googleMap ?: return
|
|
830
|
+
if (staticMode && !staticProjectionReady) return
|
|
771
831
|
val contentView = markerView.contentView
|
|
772
832
|
val point = map.projection.toScreenLocation(LatLng(markerView.latitude, markerView.longitude))
|
|
773
833
|
contentView.translationX = point.x - contentView.width * markerView.anchorX
|
|
774
834
|
contentView.translationY = point.y - contentView.height * markerView.anchorY
|
|
835
|
+
contentView.visibility = View.VISIBLE
|
|
775
836
|
}
|
|
776
837
|
|
|
777
838
|
// endregion
|
|
@@ -813,7 +874,7 @@ class GoogleMapProvider(private val context: Context) :
|
|
|
813
874
|
strokeColors = polylineView.strokeColors
|
|
814
875
|
strokeWidth = polylineView.strokeWidth.dpToPx()
|
|
815
876
|
animatedOptions = polylineView.animatedOptions
|
|
816
|
-
animated = polylineView.animated
|
|
877
|
+
animated = polylineView.animated && !staticMode
|
|
817
878
|
update()
|
|
818
879
|
}
|
|
819
880
|
}
|
|
@@ -842,7 +903,8 @@ class GoogleMapProvider(private val context: Context) :
|
|
|
842
903
|
strokeColors = polylineView.strokeColors
|
|
843
904
|
strokeWidth = polylineView.strokeWidth.dpToPx()
|
|
844
905
|
animatedOptions = polylineView.animatedOptions
|
|
845
|
-
|
|
906
|
+
// Static maps render once; show the full polyline instead of animating
|
|
907
|
+
animated = polylineView.animated && !staticMode
|
|
846
908
|
update()
|
|
847
909
|
}
|
|
848
910
|
|
|
@@ -1204,6 +1266,12 @@ class GoogleMapProvider(private val context: Context) :
|
|
|
1204
1266
|
|
|
1205
1267
|
private fun applyUiSettings() {
|
|
1206
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
|
+
}
|
|
1207
1275
|
isZoomGesturesEnabled = zoomEnabled
|
|
1208
1276
|
isScrollGesturesEnabled = scrollEnabled
|
|
1209
1277
|
isRotateGesturesEnabled = rotateEnabled
|
|
@@ -1244,10 +1312,43 @@ class GoogleMapProvider(private val context: Context) :
|
|
|
1244
1312
|
|
|
1245
1313
|
private fun applyEdgeInsets(duration: Int = 0) {
|
|
1246
1314
|
val combined = combinedEdgeInsets()
|
|
1247
|
-
|
|
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
|
+
}
|
|
1248
1321
|
applyWatermarkTranslation(combined, duration)
|
|
1249
1322
|
}
|
|
1250
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
|
+
|
|
1251
1352
|
private fun applyWatermarkTranslation(insets: EdgeInsets, duration: Int = 0) {
|
|
1252
1353
|
val view = mapView ?: return
|
|
1253
1354
|
view.findViewByTag("GoogleWatermark")?.let { watermark ->
|
|
@@ -1324,5 +1425,9 @@ class GoogleMapProvider(private val context: Context) :
|
|
|
1324
1425
|
|
|
1325
1426
|
companion object {
|
|
1326
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
|
|
1327
1432
|
}
|
|
1328
1433
|
}
|
package/ios/LuggMapView.mm
CHANGED
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
#import "LuggPolylineView.h"
|
|
8
8
|
#import "LuggTileOverlayView.h"
|
|
9
9
|
#import "core/AppleMapProvider.h"
|
|
10
|
+
#import "core/AppleStaticMapProvider.h"
|
|
10
11
|
#import "core/GoogleMapProvider.h"
|
|
12
|
+
#import "core/GoogleStaticMapProvider.h"
|
|
11
13
|
#import "core/MapProviderDelegate.h"
|
|
12
14
|
#import "events/CameraIdleEvent.h"
|
|
13
15
|
#import "events/CameraMoveEvent.h"
|
|
@@ -25,6 +27,19 @@
|
|
|
25
27
|
using namespace facebook::react;
|
|
26
28
|
using namespace luggmaps::events;
|
|
27
29
|
|
|
30
|
+
// Static snapshots cached across view recycling, keyed by the user-provided
|
|
31
|
+
// staticKey plus the map settings that affect appearance.
|
|
32
|
+
static NSCache<NSString *, UIImage *> *StaticSnapshotCache(void) {
|
|
33
|
+
static NSCache<NSString *, UIImage *> *cache;
|
|
34
|
+
static dispatch_once_t onceToken;
|
|
35
|
+
dispatch_once(&onceToken, ^{
|
|
36
|
+
cache = [[NSCache alloc] init];
|
|
37
|
+
cache.countLimit = 100;
|
|
38
|
+
cache.totalCostLimit = 64 * 1024 * 1024; // bytes
|
|
39
|
+
});
|
|
40
|
+
return cache;
|
|
41
|
+
}
|
|
42
|
+
|
|
28
43
|
@interface LuggMapView () <RCTLuggMapViewViewProtocol, MapProviderDelegate>
|
|
29
44
|
@end
|
|
30
45
|
|
|
@@ -40,6 +55,9 @@ using namespace luggmaps::events;
|
|
|
40
55
|
BOOL _rotateEnabled;
|
|
41
56
|
BOOL _pitchEnabled;
|
|
42
57
|
BOOL _compassEnabled;
|
|
58
|
+
BOOL _staticMode;
|
|
59
|
+
NSString *_staticKey;
|
|
60
|
+
BOOL _staticSnapshotDone;
|
|
43
61
|
BOOL _userLocationEnabled;
|
|
44
62
|
LuggMapViewMapType _mapType;
|
|
45
63
|
LuggMapViewTheme _theme;
|
|
@@ -70,6 +88,8 @@ using namespace luggmaps::events;
|
|
|
70
88
|
_rotateEnabled = YES;
|
|
71
89
|
_pitchEnabled = YES;
|
|
72
90
|
_compassEnabled = YES;
|
|
91
|
+
_staticMode = NO;
|
|
92
|
+
_staticKey = @"";
|
|
73
93
|
_userLocationEnabled = NO;
|
|
74
94
|
_poiEnabled = NO;
|
|
75
95
|
_poiFilterMode = LuggMapViewPoiFilterMode::Including;
|
|
@@ -170,17 +190,110 @@ using namespace luggmaps::events;
|
|
|
170
190
|
[super didMoveToWindow];
|
|
171
191
|
if (self.window) {
|
|
172
192
|
if (!_provider && _mapWrapperView) {
|
|
173
|
-
|
|
193
|
+
if (_staticMode) {
|
|
194
|
+
[self startStaticContent];
|
|
195
|
+
} else {
|
|
196
|
+
[self initializeProvider];
|
|
197
|
+
}
|
|
174
198
|
}
|
|
175
199
|
[_provider resumeAnimations];
|
|
176
200
|
} else {
|
|
201
|
+
// A static map mid-warmup may hold an expensive live map view. Destroy
|
|
202
|
+
// it off-window and re-render on return instead of letting paused maps
|
|
203
|
+
// pile up across screen switches and all resume at once
|
|
204
|
+
if (_staticMode && _provider && !_staticSnapshotDone) {
|
|
205
|
+
[_provider destroy];
|
|
206
|
+
_provider = nil;
|
|
207
|
+
_initialized = NO;
|
|
208
|
+
}
|
|
177
209
|
[_provider pauseAnimations];
|
|
178
210
|
}
|
|
179
211
|
}
|
|
180
212
|
|
|
213
|
+
// Shown while the map warms up, until the live map or snapshot covers it
|
|
214
|
+
- (void)applyStaticPlaceholderBackground {
|
|
215
|
+
const auto &viewProps =
|
|
216
|
+
*std::static_pointer_cast<LuggMapViewProps const>(_props);
|
|
217
|
+
if (viewProps.backgroundColor) {
|
|
218
|
+
// A user-provided background acts as the placeholder
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
switch (_theme) {
|
|
223
|
+
case LuggMapViewTheme::Dark:
|
|
224
|
+
_mapWrapperView.overrideUserInterfaceStyle = UIUserInterfaceStyleDark;
|
|
225
|
+
break;
|
|
226
|
+
case LuggMapViewTheme::Light:
|
|
227
|
+
_mapWrapperView.overrideUserInterfaceStyle = UIUserInterfaceStyleLight;
|
|
228
|
+
break;
|
|
229
|
+
default:
|
|
230
|
+
_mapWrapperView.overrideUserInterfaceStyle =
|
|
231
|
+
UIUserInterfaceStyleUnspecified;
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
_mapWrapperView.backgroundColor = UIColor.secondarySystemBackgroundColor;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
- (nullable NSString *)staticSnapshotCacheKey {
|
|
238
|
+
if (_staticKey.length == 0 || !_mapWrapperView)
|
|
239
|
+
return nil;
|
|
240
|
+
|
|
241
|
+
CGSize size = _mapWrapperView.bounds.size;
|
|
242
|
+
if (size.width <= 0 || size.height <= 0)
|
|
243
|
+
return nil;
|
|
244
|
+
|
|
245
|
+
// 'system' theme snapshots depend on the current appearance
|
|
246
|
+
NSInteger style = _theme == LuggMapViewTheme::System
|
|
247
|
+
? (NSInteger)self.traitCollection.userInterfaceStyle
|
|
248
|
+
: -(NSInteger)_theme;
|
|
249
|
+
|
|
250
|
+
// Camera is part of the key so a reused staticKey with a different
|
|
251
|
+
// location/zoom can't serve a stale snapshot
|
|
252
|
+
const auto &viewProps =
|
|
253
|
+
*std::static_pointer_cast<LuggMapViewProps const>(_props);
|
|
254
|
+
|
|
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)];
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
- (void)startStaticContent {
|
|
266
|
+
[self applyStaticPlaceholderBackground];
|
|
267
|
+
|
|
268
|
+
// Both static providers render fully async: the placeholder shows until
|
|
269
|
+
// the snapshotter (Apple) or a brief live-map warmup (Google) delivers
|
|
270
|
+
// the base map, which reveals together with the marker/shape overlays
|
|
271
|
+
[self initializeProvider];
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// staticKey identifies the map's content; when it changes (e.g. a recycled
|
|
275
|
+
// list row bound to a new item), any live warmup or displayed snapshot is
|
|
276
|
+
// stale and the static lifecycle restarts. Without this, an in-flight
|
|
277
|
+
// warmup caches the old item's map under the new key.
|
|
278
|
+
- (void)resetStaticContent {
|
|
279
|
+
_staticSnapshotDone = NO;
|
|
280
|
+
|
|
281
|
+
if (_provider) {
|
|
282
|
+
[_provider destroy];
|
|
283
|
+
_provider = nil;
|
|
284
|
+
_initialized = NO;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (self.window) {
|
|
288
|
+
[self startStaticContent];
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
181
292
|
- (void)prepareForRecycle {
|
|
182
293
|
[super prepareForRecycle];
|
|
183
294
|
|
|
295
|
+
_staticSnapshotDone = NO;
|
|
296
|
+
|
|
184
297
|
[_provider destroy];
|
|
185
298
|
_provider = nil;
|
|
186
299
|
_mapWrapperView = nil;
|
|
@@ -197,14 +310,28 @@ using namespace luggmaps::events;
|
|
|
197
310
|
*std::static_pointer_cast<LuggMapViewProps const>(_props);
|
|
198
311
|
|
|
199
312
|
if (_providerType == LuggMapViewProvider::Apple) {
|
|
200
|
-
_provider = [[
|
|
313
|
+
_provider = _staticMode ? [[AppleStaticMapProvider alloc] init]
|
|
314
|
+
: [[AppleMapProvider alloc] init];
|
|
315
|
+
} else if (_staticMode) {
|
|
316
|
+
GoogleStaticMapProvider *google = [[GoogleStaticMapProvider alloc] init];
|
|
317
|
+
google.mapId = _mapId;
|
|
318
|
+
_provider = google;
|
|
201
319
|
} else {
|
|
202
320
|
GoogleMapProvider *google = [[GoogleMapProvider alloc] init];
|
|
203
321
|
google.mapId = _mapId;
|
|
204
322
|
_provider = google;
|
|
205
323
|
}
|
|
206
324
|
|
|
325
|
+
if (_staticMode) {
|
|
326
|
+
NSString *cacheKey = [self staticSnapshotCacheKey];
|
|
327
|
+
if (cacheKey) {
|
|
328
|
+
((StaticMapProviderBase *)_provider).cachedBaseImage =
|
|
329
|
+
[StaticSnapshotCache() objectForKey:cacheKey];
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
207
333
|
_provider.delegate = self;
|
|
334
|
+
_provider.staticMode = _staticMode;
|
|
208
335
|
|
|
209
336
|
CLLocationCoordinate2D coordinate =
|
|
210
337
|
CLLocationCoordinate2DMake(viewProps.initialCoordinate.latitude,
|
|
@@ -244,6 +371,19 @@ using namespace luggmaps::events;
|
|
|
244
371
|
ReadyEvent::emit<LuggMapViewEventEmitter>(_eventEmitter);
|
|
245
372
|
}
|
|
246
373
|
|
|
374
|
+
- (void)mapProviderDidFinishStaticSnapshot {
|
|
375
|
+
_staticSnapshotDone = YES;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
- (void)mapProviderDidCaptureStaticImage:(UIImage *)image {
|
|
379
|
+
NSString *key = [self staticSnapshotCacheKey];
|
|
380
|
+
if (key) {
|
|
381
|
+
NSUInteger cost = (NSUInteger)(image.size.width * image.scale *
|
|
382
|
+
image.size.height * image.scale * 4);
|
|
383
|
+
[StaticSnapshotCache() setObject:image forKey:key cost:cost];
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
247
387
|
- (void)mapProviderDidMoveCamera:(double)latitude
|
|
248
388
|
longitude:(double)longitude
|
|
249
389
|
zoom:(double)zoom
|
|
@@ -303,6 +443,12 @@ using namespace luggmaps::events;
|
|
|
303
443
|
*std::static_pointer_cast<LuggMapViewProps const>(_props);
|
|
304
444
|
|
|
305
445
|
_providerType = newViewProps.provider;
|
|
446
|
+
_staticMode = newViewProps.staticMode;
|
|
447
|
+
|
|
448
|
+
NSString *newStaticKey =
|
|
449
|
+
[NSString stringWithUTF8String:newViewProps.staticKey.c_str()];
|
|
450
|
+
BOOL staticKeyChanged = ![newStaticKey isEqualToString:_staticKey];
|
|
451
|
+
_staticKey = newStaticKey;
|
|
306
452
|
|
|
307
453
|
NSString *newMapId =
|
|
308
454
|
[NSString stringWithUTF8String:newViewProps.mapId.c_str()];
|
|
@@ -381,6 +527,11 @@ using namespace luggmaps::events;
|
|
|
381
527
|
}
|
|
382
528
|
|
|
383
529
|
[super updateProps:props oldProps:oldProps];
|
|
530
|
+
|
|
531
|
+
// After super so the snapshot cache key sees the new camera props
|
|
532
|
+
if (staticKeyChanged && _staticMode && _mapWrapperView) {
|
|
533
|
+
[self resetStaticContent];
|
|
534
|
+
}
|
|
384
535
|
}
|
|
385
536
|
|
|
386
537
|
#pragma mark - Commands
|
package/ios/LuggMapWrapperView.h
CHANGED
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
NS_ASSUME_NONNULL_BEGIN
|
|
5
5
|
|
|
6
6
|
@interface LuggMapWrapperView : RCTViewComponentView
|
|
7
|
+
|
|
8
|
+
/// Called after layout; lets the static snapshotter start (or restart)
|
|
9
|
+
/// once the wrapper has a real size, since a snapshot can't autoresize
|
|
10
|
+
@property(nonatomic, copy, nullable) void (^layoutHandler)(void);
|
|
11
|
+
|
|
7
12
|
@end
|
|
8
13
|
|
|
9
14
|
NS_ASSUME_NONNULL_END
|
|
@@ -29,6 +29,24 @@ using namespace facebook::react;
|
|
|
29
29
|
return self;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
- (void)layoutSubviews {
|
|
33
|
+
[super layoutSubviews];
|
|
34
|
+
if (self.layoutHandler) {
|
|
35
|
+
self.layoutHandler();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
- (void)prepareForRecycle {
|
|
40
|
+
[super prepareForRecycle];
|
|
41
|
+
|
|
42
|
+
// Static placeholder styling is applied outside of props
|
|
43
|
+
// (LuggMapView applyStaticPlaceholderBackground) and would otherwise
|
|
44
|
+
// leak into the next map that reuses this recycled view
|
|
45
|
+
self.backgroundColor = nil;
|
|
46
|
+
self.overrideUserInterfaceStyle = UIUserInterfaceStyleUnspecified;
|
|
47
|
+
self.layoutHandler = nil;
|
|
48
|
+
}
|
|
49
|
+
|
|
32
50
|
Class<RCTComponentViewProtocol> LuggMapWrapperViewCls(void) {
|
|
33
51
|
return LuggMapWrapperView.class;
|
|
34
52
|
}
|
package/ios/LuggMarkerView.mm
CHANGED
|
@@ -287,6 +287,8 @@ using namespace luggmaps::events;
|
|
|
287
287
|
_iconView.layer.anchorPoint = CGPointMake(0.5, 0.5);
|
|
288
288
|
_iconView.frame = CGRectMake(0, 0, _iconView.bounds.size.width,
|
|
289
289
|
_iconView.bounds.size.height);
|
|
290
|
+
// Static providers hide the icon view until positioned
|
|
291
|
+
_iconView.hidden = NO;
|
|
290
292
|
}
|
|
291
293
|
|
|
292
294
|
- (void)showCallout {
|
|
@@ -3,6 +3,14 @@
|
|
|
3
3
|
|
|
4
4
|
NS_ASSUME_NONNULL_BEGIN
|
|
5
5
|
|
|
6
|
+
/// POI filter for the given props, shared by the live map view and the
|
|
7
|
+
/// static snapshotter. Returns nil for "show everything".
|
|
8
|
+
MKPointOfInterestFilter *_Nullable LuggPointOfInterestFilter(
|
|
9
|
+
BOOL poiEnabled, facebook::react::LuggMapViewPoiFilterMode filterMode,
|
|
10
|
+
NSArray<NSString *> *filterCategories);
|
|
11
|
+
|
|
12
|
+
MKMapType LuggMKMapTypeFromMapType(facebook::react::LuggMapViewMapType mapType);
|
|
13
|
+
|
|
6
14
|
@interface LuggAppleMapViewContent : MKMapView
|
|
7
15
|
@end
|
|
8
16
|
|