@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.
- package/README.md +15 -100
- package/android/src/main/java/com/luggmaps/LuggCalloutView.kt +5 -0
- package/android/src/main/java/com/luggmaps/LuggMapView.kt +41 -0
- package/android/src/main/java/com/luggmaps/LuggMapViewManager.kt +14 -0
- package/android/src/main/java/com/luggmaps/LuggMapWrapperView.kt +27 -2
- package/android/src/main/java/com/luggmaps/LuggMarkerView.kt +7 -2
- package/android/src/main/java/com/luggmaps/LuggPolylineView.kt +2 -1
- package/android/src/main/java/com/luggmaps/core/GoogleMapProvider.kt +307 -41
- package/android/src/main/java/com/luggmaps/core/MapProviderDelegate.kt +1 -0
- package/android/src/main/java/com/luggmaps/core/PolylineAnimator.kt +61 -10
- package/ios/LuggMapView.mm +170 -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 +48 -31
- package/ios/core/AppleStaticMapProvider.h +14 -0
- package/ios/core/AppleStaticMapProvider.mm +159 -0
- package/ios/core/GMSPolylineAnimator.m +40 -15
- package/ios/core/GoogleMapProvider.h +10 -0
- package/ios/core/GoogleMapProvider.mm +56 -44
- package/ios/core/GoogleStaticMapProvider.h +21 -0
- package/ios/core/GoogleStaticMapProvider.mm +213 -0
- package/ios/core/MKPolylineAnimator.m +67 -43
- package/ios/core/MapProviderDelegate.h +7 -0
- package/ios/core/StaticMapProviderBase.h +75 -0
- package/ios/core/StaticMapProviderBase.mm +845 -0
- package/ios/events/CameraIdleEvent.h +4 -2
- package/ios/events/CameraMoveEvent.h +4 -2
- package/ios/events/CirclePressEvent.h +4 -2
- package/ios/events/GroundOverlayPressEvent.h +4 -2
- package/ios/events/LongPressEvent.h +4 -2
- package/ios/events/MarkerDragEvent.h +12 -6
- package/ios/events/MarkerPressEvent.h +4 -2
- package/ios/events/PolygonPressEvent.h +4 -2
- package/ios/events/PressEvent.h +4 -2
- package/ios/events/ReadyEvent.h +4 -2
- package/ios/events/TileOverlayPressEvent.h +4 -2
- package/ios/extensions/MKMapView+Zoom.h +20 -0
- package/ios/extensions/MKMapView+Zoom.m +28 -18
- package/ios/extensions/UIView+Snapshot.h +16 -0
- package/ios/extensions/UIView+Snapshot.m +27 -0
- package/lib/module/MapView.js +7 -0
- package/lib/module/MapView.js.map +1 -1
- package/lib/module/MapView.web.js +9 -4
- package/lib/module/MapView.web.js.map +1 -1
- package/lib/module/fabric/LuggMapViewNativeComponent.ts +3 -0
- package/lib/typescript/src/MapView.d.ts.map +1 -1
- package/lib/typescript/src/MapView.types.d.ts +24 -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 +3 -0
- package/lib/typescript/src/fabric/LuggMapViewNativeComponent.d.ts.map +1 -1
- package/package.json +19 -12
- package/src/MapView.tsx +7 -0
- package/src/MapView.types.ts +24 -0
- package/src/MapView.web.tsx +15 -9
- package/src/fabric/LuggMapViewNativeComponent.ts +3 -0
- package/plugin/build/withMapsAndroid.d.ts +0 -5
- package/plugin/build/withMapsAndroid.js +0 -15
- package/plugin/build/withMapsIOS.d.ts +0 -5
- package/plugin/build/withMapsIOS.js +0 -27
|
@@ -5,6 +5,7 @@ import android.content.ComponentCallbacks2
|
|
|
5
5
|
import android.content.Context
|
|
6
6
|
import android.content.res.Configuration
|
|
7
7
|
import android.view.View
|
|
8
|
+
import android.view.ViewTreeObserver
|
|
8
9
|
import android.widget.ImageView
|
|
9
10
|
import androidx.core.graphics.createBitmap
|
|
10
11
|
import androidx.core.view.ViewCompat
|
|
@@ -15,9 +16,8 @@ import com.google.android.gms.maps.GoogleMap
|
|
|
15
16
|
import com.google.android.gms.maps.GoogleMapOptions
|
|
16
17
|
import com.google.android.gms.maps.MapView
|
|
17
18
|
import com.google.android.gms.maps.OnMapReadyCallback
|
|
18
|
-
import com.google.android.gms.maps.model.AdvancedMarker
|
|
19
|
-
import com.google.android.gms.maps.model.AdvancedMarkerOptions
|
|
20
19
|
import com.google.android.gms.maps.model.BitmapDescriptorFactory
|
|
20
|
+
import com.google.android.gms.maps.model.CameraPosition
|
|
21
21
|
import com.google.android.gms.maps.model.Circle
|
|
22
22
|
import com.google.android.gms.maps.model.CircleOptions
|
|
23
23
|
import com.google.android.gms.maps.model.GroundOverlay
|
|
@@ -26,6 +26,7 @@ import com.google.android.gms.maps.model.LatLng
|
|
|
26
26
|
import com.google.android.gms.maps.model.LatLngBounds
|
|
27
27
|
import com.google.android.gms.maps.model.MapColorScheme
|
|
28
28
|
import com.google.android.gms.maps.model.Marker
|
|
29
|
+
import com.google.android.gms.maps.model.MarkerOptions
|
|
29
30
|
import com.google.android.gms.maps.model.Polygon
|
|
30
31
|
import com.google.android.gms.maps.model.PolygonOptions
|
|
31
32
|
import com.google.android.gms.maps.model.PolylineOptions
|
|
@@ -48,7 +49,10 @@ import com.luggmaps.LuggTileOverlayViewDelegate
|
|
|
48
49
|
import com.luggmaps.extensions.findViewByTag
|
|
49
50
|
import java.net.URL
|
|
50
51
|
import kotlin.math.atan
|
|
52
|
+
import kotlin.math.ln
|
|
53
|
+
import kotlin.math.log2
|
|
51
54
|
import kotlin.math.pow
|
|
55
|
+
import kotlin.math.sin
|
|
52
56
|
import kotlin.math.sinh
|
|
53
57
|
|
|
54
58
|
class GoogleMapProvider(private val context: Context) :
|
|
@@ -78,6 +82,10 @@ class GoogleMapProvider(private val context: Context) :
|
|
|
78
82
|
|
|
79
83
|
var mapId: String = DEMO_MAP_ID
|
|
80
84
|
|
|
85
|
+
// Renders a non-interactive static map — lite mode when the view fits
|
|
86
|
+
// the lite bitmap cap, a gesture-less full map otherwise. Creation-time only.
|
|
87
|
+
var staticMode: Boolean = false
|
|
88
|
+
|
|
81
89
|
private var wrapperView: LuggMapWrapperView? = null
|
|
82
90
|
private var currentNightMode: Int = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
|
|
83
91
|
private var mapView: MapView? = null
|
|
@@ -96,6 +104,7 @@ class GoogleMapProvider(private val context: Context) :
|
|
|
96
104
|
private val groundOverlayToViewMap = mutableMapOf<GroundOverlay, LuggGroundOverlayView>()
|
|
97
105
|
private val markerToViewMap = mutableMapOf<Marker, LuggMarkerView>()
|
|
98
106
|
private val liveMarkerViews = mutableSetOf<LuggMarkerView>()
|
|
107
|
+
private var staticProjectionReady = false
|
|
99
108
|
private var activeNonBubbledMarker: Marker? = null
|
|
100
109
|
private var tapLocation: LatLng? = null
|
|
101
110
|
|
|
@@ -109,6 +118,7 @@ class GoogleMapProvider(private val context: Context) :
|
|
|
109
118
|
private var scrollEnabled: Boolean = true
|
|
110
119
|
private var rotateEnabled: Boolean = true
|
|
111
120
|
private var pitchEnabled: Boolean = true
|
|
121
|
+
private var compassEnabled: Boolean = true
|
|
112
122
|
private var userLocationEnabled: Boolean = false
|
|
113
123
|
private var userLocationButtonEnabled: Boolean = false
|
|
114
124
|
|
|
@@ -126,6 +136,13 @@ class GoogleMapProvider(private val context: Context) :
|
|
|
126
136
|
private var insetAdjustment: String = "never"
|
|
127
137
|
private var systemInsets: EdgeInsets = EdgeInsets()
|
|
128
138
|
|
|
139
|
+
// The maps renderer adds and positions the watermark asynchronously; the
|
|
140
|
+
// layout listener re-evaluates the translation on every layout pass
|
|
141
|
+
private var watermarkLayoutListener: ViewTreeObserver.OnGlobalLayoutListener? = null
|
|
142
|
+
private var watermarkView: View? = null
|
|
143
|
+
private var watermarkTargetX = 0f
|
|
144
|
+
private var watermarkTargetY = 0f
|
|
145
|
+
|
|
129
146
|
// region MapProvider
|
|
130
147
|
|
|
131
148
|
override fun initializeMap(wrapperView: View, latitude: Double, longitude: Double, zoom: Float) {
|
|
@@ -136,21 +153,45 @@ class GoogleMapProvider(private val context: Context) :
|
|
|
136
153
|
initialZoom = zoom
|
|
137
154
|
|
|
138
155
|
val wrapper = wrapperView as LuggMapWrapperView
|
|
156
|
+
wrapper.relayoutChildOnRequest = staticMode
|
|
139
157
|
this.wrapperView = wrapper
|
|
140
158
|
|
|
141
159
|
context.applicationContext.registerComponentCallbacks(this)
|
|
142
160
|
|
|
143
|
-
|
|
161
|
+
// Static mode needs the view size to pick lite vs full map, so wait
|
|
162
|
+
// for the first layout if it hasn't happened yet
|
|
163
|
+
if (staticMode && (wrapper.width == 0 || wrapper.height == 0)) {
|
|
164
|
+
wrapper.onLayoutReady = { createMapView() }
|
|
165
|
+
} else {
|
|
166
|
+
createMapView()
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private fun createMapView() {
|
|
171
|
+
if (mapView != null) return
|
|
172
|
+
val wrapper = wrapperView ?: return
|
|
173
|
+
|
|
174
|
+
// Lite mode renders the map as a single bitmap capped at ~2048px per
|
|
175
|
+
// dimension, centered in the view — bigger views get letterboxed. Fall
|
|
176
|
+
// back to a full map (gestures disabled) when the view exceeds the cap
|
|
177
|
+
val useLite = staticMode && maxOf(wrapper.width, wrapper.height) <= LITE_MODE_MAX_SIZE
|
|
178
|
+
val options = GoogleMapOptions().liteMode(useLite)
|
|
179
|
+
// Lite mode doesn't support map IDs (cloud-based styling); setting one
|
|
180
|
+
// makes the static map render blank
|
|
181
|
+
if (!useLite) options.mapId(mapId)
|
|
144
182
|
mapView = MapView(context, options).also { view ->
|
|
145
183
|
view.onCreate(null)
|
|
146
184
|
view.onResume()
|
|
147
185
|
view.getMapAsync(this)
|
|
148
186
|
wrapper.addView(view)
|
|
149
187
|
}
|
|
188
|
+
wrapper.onLayoutReady = null
|
|
150
189
|
}
|
|
151
190
|
|
|
152
191
|
override fun destroy() {
|
|
192
|
+
wrapperView?.onLayoutReady = null
|
|
153
193
|
detachWindowInsetsListener()
|
|
194
|
+
detachWatermarkLayoutListener()
|
|
154
195
|
context.applicationContext.unregisterComponentCallbacks(this)
|
|
155
196
|
dismissNonBubbledCallout()
|
|
156
197
|
for (markerView in liveMarkerViews) {
|
|
@@ -194,7 +235,23 @@ class GoogleMapProvider(private val context: Context) :
|
|
|
194
235
|
googleMap = map
|
|
195
236
|
_isMapReady = true
|
|
196
237
|
|
|
197
|
-
|
|
238
|
+
if (staticMode) {
|
|
239
|
+
// Tapping the map otherwise shows an "open in Google Maps" toolbar
|
|
240
|
+
map.uiSettings.isMapToolbarEnabled = false
|
|
241
|
+
// The projection is only valid once the map has rendered, and no
|
|
242
|
+
// camera events fire afterwards to correct live marker positions
|
|
243
|
+
map.setOnMapLoadedCallback {
|
|
244
|
+
staticProjectionReady = true
|
|
245
|
+
positionLiveMarkers()
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Padding must be in place before the initial camera move: setPadding
|
|
250
|
+
// never repositions the current camera, but camera updates after it are
|
|
251
|
+
// relative to the padded center
|
|
252
|
+
applyEdgeInsets()
|
|
253
|
+
|
|
254
|
+
val position = if (staticMode) staticCameraTarget() else LatLng(initialLatitude, initialLongitude)
|
|
198
255
|
map.moveCamera(CameraUpdateFactory.newLatLngZoom(position, initialZoom))
|
|
199
256
|
|
|
200
257
|
map.setOnCameraMoveStartedListener(this)
|
|
@@ -217,7 +274,6 @@ class GoogleMapProvider(private val context: Context) :
|
|
|
217
274
|
|
|
218
275
|
applyUiSettings()
|
|
219
276
|
applyZoomLimits()
|
|
220
|
-
applyEdgeInsets()
|
|
221
277
|
applyInsetAdjustment()
|
|
222
278
|
applyTheme()
|
|
223
279
|
applyUserLocation()
|
|
@@ -441,24 +497,33 @@ class GoogleMapProvider(private val context: Context) :
|
|
|
441
497
|
|
|
442
498
|
override fun setZoomEnabled(enabled: Boolean) {
|
|
443
499
|
zoomEnabled = enabled
|
|
500
|
+
if (staticMode) return
|
|
444
501
|
googleMap?.uiSettings?.isZoomGesturesEnabled = enabled
|
|
445
502
|
}
|
|
446
503
|
|
|
447
504
|
override fun setScrollEnabled(enabled: Boolean) {
|
|
448
505
|
scrollEnabled = enabled
|
|
506
|
+
if (staticMode) return
|
|
449
507
|
googleMap?.uiSettings?.isScrollGesturesEnabled = enabled
|
|
450
508
|
}
|
|
451
509
|
|
|
452
510
|
override fun setRotateEnabled(enabled: Boolean) {
|
|
453
511
|
rotateEnabled = enabled
|
|
512
|
+
if (staticMode) return
|
|
454
513
|
googleMap?.uiSettings?.isRotateGesturesEnabled = enabled
|
|
455
514
|
}
|
|
456
515
|
|
|
457
516
|
override fun setPitchEnabled(enabled: Boolean) {
|
|
458
517
|
pitchEnabled = enabled
|
|
518
|
+
if (staticMode) return
|
|
459
519
|
googleMap?.uiSettings?.isTiltGesturesEnabled = enabled
|
|
460
520
|
}
|
|
461
521
|
|
|
522
|
+
override fun setCompassEnabled(enabled: Boolean) {
|
|
523
|
+
compassEnabled = enabled
|
|
524
|
+
googleMap?.uiSettings?.isCompassEnabled = enabled
|
|
525
|
+
}
|
|
526
|
+
|
|
462
527
|
@SuppressLint("MissingPermission")
|
|
463
528
|
override fun setUserLocationEnabled(enabled: Boolean) {
|
|
464
529
|
userLocationEnabled = enabled
|
|
@@ -505,6 +570,11 @@ class GoogleMapProvider(private val context: Context) :
|
|
|
505
570
|
}
|
|
506
571
|
|
|
507
572
|
override fun setEdgeInsets(edgeInsets: EdgeInsets) {
|
|
573
|
+
if (staticMode) {
|
|
574
|
+
setStaticEdgeInsets(edgeInsets)
|
|
575
|
+
return
|
|
576
|
+
}
|
|
577
|
+
|
|
508
578
|
val oldInsets = this.edgeInsets
|
|
509
579
|
this.edgeInsets = edgeInsets
|
|
510
580
|
applyEdgeInsets()
|
|
@@ -518,6 +588,11 @@ class GoogleMapProvider(private val context: Context) :
|
|
|
518
588
|
}
|
|
519
589
|
|
|
520
590
|
override fun setEdgeInsets(edgeInsets: EdgeInsets, duration: Int) {
|
|
591
|
+
if (staticMode) {
|
|
592
|
+
setStaticEdgeInsets(edgeInsets)
|
|
593
|
+
return
|
|
594
|
+
}
|
|
595
|
+
|
|
521
596
|
val map = googleMap
|
|
522
597
|
val oldInsets = this.edgeInsets
|
|
523
598
|
this.edgeInsets = edgeInsets
|
|
@@ -599,6 +674,10 @@ class GoogleMapProvider(private val context: Context) :
|
|
|
599
674
|
syncPolylineView(polylineView)
|
|
600
675
|
}
|
|
601
676
|
|
|
677
|
+
override fun polylineViewDidDrop(polylineView: LuggPolylineView) {
|
|
678
|
+
teardownPolyline(polylineView)
|
|
679
|
+
}
|
|
680
|
+
|
|
602
681
|
// endregion
|
|
603
682
|
|
|
604
683
|
// region PolygonViewDelegate
|
|
@@ -692,12 +771,12 @@ class GoogleMapProvider(private val context: Context) :
|
|
|
692
771
|
val map = googleMap ?: return
|
|
693
772
|
|
|
694
773
|
val position = LatLng(markerView.latitude, markerView.longitude)
|
|
695
|
-
val options =
|
|
774
|
+
val options = MarkerOptions()
|
|
696
775
|
.position(position)
|
|
697
776
|
.title(markerView.title)
|
|
698
777
|
.snippet(markerView.description)
|
|
699
778
|
|
|
700
|
-
val marker = map.addMarker(options)
|
|
779
|
+
val marker = map.addMarker(options) ?: return
|
|
701
780
|
marker.setAnchor(markerView.anchorX, markerView.anchorY)
|
|
702
781
|
marker.zIndex = markerView.zIndex
|
|
703
782
|
marker.rotation = markerView.rotate
|
|
@@ -715,9 +794,8 @@ class GoogleMapProvider(private val context: Context) :
|
|
|
715
794
|
}
|
|
716
795
|
}
|
|
717
796
|
|
|
718
|
-
//
|
|
719
|
-
//
|
|
720
|
-
// marker uses a transparent bitmap matching the content size so taps still trigger onMarkerClick.
|
|
797
|
+
// Live marker: content view is added to the wrapper and positioned via screen projection.
|
|
798
|
+
// The underlying marker uses a transparent bitmap matching the content size so taps still trigger onMarkerClick.
|
|
721
799
|
private fun showLiveMarker(markerView: LuggMarkerView) {
|
|
722
800
|
val wrapper = wrapperView ?: return
|
|
723
801
|
|
|
@@ -728,6 +806,10 @@ class GoogleMapProvider(private val context: Context) :
|
|
|
728
806
|
|
|
729
807
|
val contentView = markerView.contentView
|
|
730
808
|
contentView.pointerEvents = com.facebook.react.uimanager.PointerEvents.NONE
|
|
809
|
+
// Until the lite map's projection is valid, hide the marker instead of
|
|
810
|
+
// flashing it at a wrong position; onMapLoaded positions and shows it
|
|
811
|
+
contentView.visibility =
|
|
812
|
+
if (staticMode && !staticProjectionReady) View.INVISIBLE else View.VISIBLE
|
|
731
813
|
(contentView.parent as? android.view.ViewGroup)?.removeView(contentView)
|
|
732
814
|
wrapper.addView(contentView)
|
|
733
815
|
liveMarkerViews.add(markerView)
|
|
@@ -759,10 +841,12 @@ class GoogleMapProvider(private val context: Context) :
|
|
|
759
841
|
|
|
760
842
|
private fun positionLiveMarker(markerView: LuggMarkerView) {
|
|
761
843
|
val map = googleMap ?: return
|
|
844
|
+
if (staticMode && !staticProjectionReady) return
|
|
762
845
|
val contentView = markerView.contentView
|
|
763
846
|
val point = map.projection.toScreenLocation(LatLng(markerView.latitude, markerView.longitude))
|
|
764
847
|
contentView.translationX = point.x - contentView.width * markerView.anchorX
|
|
765
848
|
contentView.translationY = point.y - contentView.height * markerView.anchorY
|
|
849
|
+
contentView.visibility = View.VISIBLE
|
|
766
850
|
}
|
|
767
851
|
|
|
768
852
|
// endregion
|
|
@@ -775,8 +859,12 @@ class GoogleMapProvider(private val context: Context) :
|
|
|
775
859
|
}
|
|
776
860
|
|
|
777
861
|
override fun removePolylineView(polylineView: LuggPolylineView) {
|
|
778
|
-
|
|
779
|
-
|
|
862
|
+
teardownPolyline(polylineView)
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
private fun teardownPolyline(polylineView: LuggPolylineView) {
|
|
866
|
+
pendingPolylineViews.remove(polylineView)
|
|
867
|
+
polylineAnimators.remove(polylineView)?.destroy()
|
|
780
868
|
polylineView.polyline?.remove()
|
|
781
869
|
polylineView.polyline = null
|
|
782
870
|
}
|
|
@@ -792,15 +880,13 @@ class GoogleMapProvider(private val context: Context) :
|
|
|
792
880
|
return
|
|
793
881
|
}
|
|
794
882
|
|
|
795
|
-
polylineView.polyline?.width = polylineView.strokeWidth.dpToPx()
|
|
796
|
-
polylineView.polyline?.zIndex = polylineView.zIndex
|
|
797
|
-
|
|
798
883
|
polylineAnimators[polylineView]?.apply {
|
|
799
884
|
coordinates = polylineView.coordinates
|
|
800
885
|
strokeColors = polylineView.strokeColors
|
|
801
886
|
strokeWidth = polylineView.strokeWidth.dpToPx()
|
|
887
|
+
zIndex = polylineView.zIndex
|
|
802
888
|
animatedOptions = polylineView.animatedOptions
|
|
803
|
-
animated = polylineView.animated
|
|
889
|
+
animated = polylineView.animated && !staticMode
|
|
804
890
|
update()
|
|
805
891
|
}
|
|
806
892
|
}
|
|
@@ -814,6 +900,8 @@ class GoogleMapProvider(private val context: Context) :
|
|
|
814
900
|
private fun addPolylineViewToMap(polylineView: LuggPolylineView) {
|
|
815
901
|
val map = googleMap ?: return
|
|
816
902
|
|
|
903
|
+
polylineAnimators.remove(polylineView)?.destroy()
|
|
904
|
+
|
|
817
905
|
val options = PolylineOptions()
|
|
818
906
|
.width(polylineView.strokeWidth.dpToPx())
|
|
819
907
|
.zIndex(polylineView.zIndex)
|
|
@@ -826,8 +914,10 @@ class GoogleMapProvider(private val context: Context) :
|
|
|
826
914
|
coordinates = polylineView.coordinates
|
|
827
915
|
strokeColors = polylineView.strokeColors
|
|
828
916
|
strokeWidth = polylineView.strokeWidth.dpToPx()
|
|
917
|
+
zIndex = polylineView.zIndex
|
|
829
918
|
animatedOptions = polylineView.animatedOptions
|
|
830
|
-
|
|
919
|
+
// Static maps render once; show the full polyline instead of animating
|
|
920
|
+
animated = polylineView.animated && !staticMode
|
|
831
921
|
update()
|
|
832
922
|
}
|
|
833
923
|
|
|
@@ -1126,10 +1216,25 @@ class GoogleMapProvider(private val context: Context) :
|
|
|
1126
1216
|
// region Commands
|
|
1127
1217
|
|
|
1128
1218
|
override fun moveCamera(latitude: Double, longitude: Double, zoom: Double, duration: Int) {
|
|
1219
|
+
if (staticMode) {
|
|
1220
|
+
// Static maps don't animate; re-center at the final camera
|
|
1221
|
+
initialLatitude = latitude
|
|
1222
|
+
initialLongitude = longitude
|
|
1223
|
+
if (zoom > 0) initialZoom = zoom.toFloat()
|
|
1224
|
+
val map = googleMap ?: return
|
|
1225
|
+
map.moveCamera(CameraUpdateFactory.newLatLngZoom(staticCameraTarget(), initialZoom))
|
|
1226
|
+
positionLiveMarkers()
|
|
1227
|
+
return
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1129
1230
|
val map = googleMap ?: return
|
|
1130
1231
|
val position = LatLng(latitude, longitude)
|
|
1131
1232
|
val targetZoom = if (zoom > 0) zoom.toFloat() else map.cameraPosition.zoom
|
|
1132
|
-
val
|
|
1233
|
+
val cameraPosition = CameraPosition.Builder(map.cameraPosition)
|
|
1234
|
+
.target(position)
|
|
1235
|
+
.zoom(targetZoom)
|
|
1236
|
+
.build()
|
|
1237
|
+
val cameraUpdate = CameraUpdateFactory.newCameraPosition(cameraPosition)
|
|
1133
1238
|
when {
|
|
1134
1239
|
duration < 0 -> map.animateCamera(cameraUpdate)
|
|
1135
1240
|
duration > 0 -> map.animateCamera(cameraUpdate, duration, null)
|
|
@@ -1151,15 +1256,20 @@ class GoogleMapProvider(private val context: Context) :
|
|
|
1151
1256
|
val latLongs = coordinates.filterIsInstance<LatLng>()
|
|
1152
1257
|
if (latLongs.isEmpty()) return
|
|
1153
1258
|
|
|
1154
|
-
val boundsBuilder = LatLngBounds.Builder()
|
|
1155
|
-
latLongs.forEach { boundsBuilder.include(it) }
|
|
1156
|
-
val bounds = boundsBuilder.build()
|
|
1157
|
-
|
|
1158
1259
|
val top = edgeInsetsTop.toFloat().dpToPx().toInt()
|
|
1159
1260
|
val left = edgeInsetsLeft.toFloat().dpToPx().toInt()
|
|
1160
1261
|
val bottom = edgeInsetsBottom.toFloat().dpToPx().toInt()
|
|
1161
1262
|
val right = edgeInsetsRight.toFloat().dpToPx().toInt()
|
|
1162
1263
|
|
|
1264
|
+
if (staticMode) {
|
|
1265
|
+
fitStaticCoordinates(latLongs, top, left, bottom, right)
|
|
1266
|
+
return
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
val boundsBuilder = LatLngBounds.Builder()
|
|
1270
|
+
latLongs.forEach { boundsBuilder.include(it) }
|
|
1271
|
+
val bounds = boundsBuilder.build()
|
|
1272
|
+
|
|
1163
1273
|
val combined = combinedEdgeInsets()
|
|
1164
1274
|
map.setPadding(
|
|
1165
1275
|
combined.left + left,
|
|
@@ -1185,10 +1295,17 @@ class GoogleMapProvider(private val context: Context) :
|
|
|
1185
1295
|
|
|
1186
1296
|
private fun applyUiSettings() {
|
|
1187
1297
|
googleMap?.uiSettings?.apply {
|
|
1298
|
+
if (staticMode) {
|
|
1299
|
+
// The non-lite fallback is a full map; keep it behaving like a
|
|
1300
|
+
// static image (lite mode ignores gestures anyway)
|
|
1301
|
+
setAllGesturesEnabled(false)
|
|
1302
|
+
return
|
|
1303
|
+
}
|
|
1188
1304
|
isZoomGesturesEnabled = zoomEnabled
|
|
1189
1305
|
isScrollGesturesEnabled = scrollEnabled
|
|
1190
1306
|
isRotateGesturesEnabled = rotateEnabled
|
|
1191
1307
|
isTiltGesturesEnabled = pitchEnabled
|
|
1308
|
+
isCompassEnabled = compassEnabled
|
|
1192
1309
|
isMyLocationButtonEnabled = userLocationButtonEnabled
|
|
1193
1310
|
}
|
|
1194
1311
|
}
|
|
@@ -1224,33 +1341,178 @@ class GoogleMapProvider(private val context: Context) :
|
|
|
1224
1341
|
|
|
1225
1342
|
private fun applyEdgeInsets(duration: Int = 0) {
|
|
1226
1343
|
val combined = combinedEdgeInsets()
|
|
1227
|
-
|
|
1344
|
+
// Lite mode misrenders padding: the static image is drawn only within
|
|
1345
|
+
// the padded viewport instead of the full view. The camera target shift
|
|
1346
|
+
// (staticCameraTarget) compensates for insets instead.
|
|
1347
|
+
if (!staticMode) {
|
|
1348
|
+
googleMap?.setPadding(combined.left, combined.top, combined.right, combined.bottom)
|
|
1349
|
+
}
|
|
1228
1350
|
applyWatermarkTranslation(combined, duration)
|
|
1229
1351
|
}
|
|
1230
1352
|
|
|
1353
|
+
// Insets are applied by shifting the camera target so the coordinate
|
|
1354
|
+
// centers in the inset viewport like a live map would (see
|
|
1355
|
+
// applyEdgeInsets for why padding can't be used)
|
|
1356
|
+
private fun setStaticEdgeInsets(edgeInsets: EdgeInsets) {
|
|
1357
|
+
val oldInsets = this.edgeInsets
|
|
1358
|
+
this.edgeInsets = edgeInsets
|
|
1359
|
+
applyEdgeInsets()
|
|
1360
|
+
|
|
1361
|
+
val map = googleMap
|
|
1362
|
+
if (map != null && oldInsets != edgeInsets) {
|
|
1363
|
+
map.moveCamera(CameraUpdateFactory.newLatLngZoom(staticCameraTarget(), initialZoom))
|
|
1364
|
+
positionLiveMarkers()
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
private fun staticCameraTarget(): LatLng {
|
|
1369
|
+
val offsetX = (edgeInsets.left - edgeInsets.right) / 2.0
|
|
1370
|
+
val offsetY = (edgeInsets.top - edgeInsets.bottom) / 2.0
|
|
1371
|
+
if (offsetX == 0.0 && offsetY == 0.0) return LatLng(initialLatitude, initialLongitude)
|
|
1372
|
+
|
|
1373
|
+
val worldSize = mercatorWorldSize(initialZoom)
|
|
1374
|
+
return latLngFromMercator(
|
|
1375
|
+
mercatorX(initialLongitude) - offsetX / worldSize,
|
|
1376
|
+
mercatorY(initialLatitude) - offsetY / worldSize
|
|
1377
|
+
)
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
// A static camera can't fit with map padding (lite mode misrenders it);
|
|
1381
|
+
// compute the fitted camera in mercator space instead, shifting the
|
|
1382
|
+
// center for asymmetric padding like edge insets
|
|
1383
|
+
private fun fitStaticCoordinates(
|
|
1384
|
+
coordinates: List<LatLng>,
|
|
1385
|
+
top: Int,
|
|
1386
|
+
left: Int,
|
|
1387
|
+
bottom: Int,
|
|
1388
|
+
right: Int
|
|
1389
|
+
) {
|
|
1390
|
+
val map = googleMap ?: return
|
|
1391
|
+
val wrapper = wrapperView ?: return
|
|
1392
|
+
if (wrapper.width == 0 || wrapper.height == 0) return
|
|
1393
|
+
|
|
1394
|
+
var minX = Double.MAX_VALUE
|
|
1395
|
+
var minY = Double.MAX_VALUE
|
|
1396
|
+
var maxX = -Double.MAX_VALUE
|
|
1397
|
+
var maxY = -Double.MAX_VALUE
|
|
1398
|
+
for (coordinate in coordinates) {
|
|
1399
|
+
val x = mercatorX(coordinate.longitude)
|
|
1400
|
+
val y = mercatorY(coordinate.latitude)
|
|
1401
|
+
minX = minOf(minX, x)
|
|
1402
|
+
maxX = maxOf(maxX, x)
|
|
1403
|
+
minY = minOf(minY, y)
|
|
1404
|
+
maxY = maxOf(maxY, y)
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
val insets = combinedEdgeInsets()
|
|
1408
|
+
val availWidth = (wrapper.width - insets.left - insets.right - left - right).coerceAtLeast(1)
|
|
1409
|
+
val availHeight = (wrapper.height - insets.top - insets.bottom - top - bottom).coerceAtLeast(1)
|
|
1410
|
+
|
|
1411
|
+
// World size in px at which the bounds fit the padded viewport;
|
|
1412
|
+
// infinite for coincident coordinates - keep the current zoom then
|
|
1413
|
+
val fitWorldSize = minOf(availWidth / (maxX - minX), availHeight / (maxY - minY))
|
|
1414
|
+
if (fitWorldSize.isFinite()) {
|
|
1415
|
+
initialZoom = log2(fitWorldSize / 256f.dpToPx())
|
|
1416
|
+
.toFloat()
|
|
1417
|
+
.coerceIn(map.minZoomLevel, map.maxZoomLevel)
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
val worldSize = mercatorWorldSize(initialZoom)
|
|
1421
|
+
val center = latLngFromMercator(
|
|
1422
|
+
(minX + maxX) / 2.0 - (left - right) / 2.0 / worldSize,
|
|
1423
|
+
(minY + maxY) / 2.0 - (top - bottom) / 2.0 / worldSize
|
|
1424
|
+
)
|
|
1425
|
+
initialLatitude = center.latitude
|
|
1426
|
+
initialLongitude = center.longitude
|
|
1427
|
+
|
|
1428
|
+
map.moveCamera(CameraUpdateFactory.newLatLngZoom(staticCameraTarget(), initialZoom))
|
|
1429
|
+
positionLiveMarkers()
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
// Web mercator world space in [0, 1]
|
|
1433
|
+
private fun mercatorX(longitude: Double): Double = (longitude + 180.0) / 360.0
|
|
1434
|
+
|
|
1435
|
+
private fun mercatorY(latitude: Double): Double {
|
|
1436
|
+
val sinLat = sin(Math.toRadians(latitude))
|
|
1437
|
+
return 0.5 - ln((1.0 + sinLat) / (1.0 - sinLat)) / (4.0 * Math.PI)
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
private fun latLngFromMercator(x: Double, y: Double): LatLng =
|
|
1441
|
+
LatLng(Math.toDegrees(atan(sinh(Math.PI * (1.0 - 2.0 * y)))), x * 360.0 - 180.0)
|
|
1442
|
+
|
|
1443
|
+
// World size in px at the given zoom
|
|
1444
|
+
private fun mercatorWorldSize(zoom: Float): Double = 256f.dpToPx().toDouble() * 2.0.pow(zoom.toDouble())
|
|
1445
|
+
|
|
1446
|
+
// Whether the renderer repositions the watermark for setPadding varies by
|
|
1447
|
+
// renderer version and by whether the watermark existed when padding was
|
|
1448
|
+
// set (and lite mode never applies padding). Measure where layout actually
|
|
1449
|
+
// placed it and translate only for the remainder.
|
|
1231
1450
|
private fun applyWatermarkTranslation(insets: EdgeInsets, duration: Int = 0) {
|
|
1232
1451
|
val view = mapView ?: return
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1452
|
+
attachWatermarkLayoutListener()
|
|
1453
|
+
|
|
1454
|
+
if (view.height == 0) return
|
|
1455
|
+
val watermark = view.findViewByTag("GoogleWatermark") ?: return
|
|
1456
|
+
|
|
1457
|
+
// Layout position relative to the map view, excluding translation
|
|
1458
|
+
var left = 0
|
|
1459
|
+
var top = 0
|
|
1460
|
+
var current: View? = watermark
|
|
1461
|
+
while (current != null && current !== view) {
|
|
1462
|
+
left += current.left
|
|
1463
|
+
top += current.top
|
|
1464
|
+
current = current.parent as? View
|
|
1465
|
+
}
|
|
1466
|
+
if (current == null) return
|
|
1467
|
+
|
|
1468
|
+
val bottomGap = view.height - (top + watermark.height)
|
|
1469
|
+
val targetY = if (bottomGap >= insets.bottom) 0f else -insets.bottom.toFloat()
|
|
1470
|
+
val targetX = if (left >= insets.left) 0f else insets.left.toFloat()
|
|
1471
|
+
|
|
1472
|
+
// Skip when already applied (or animating) toward this target so layout
|
|
1473
|
+
// passes don't snap an in-flight animation
|
|
1474
|
+
if (watermark === watermarkView && targetX == watermarkTargetX && targetY == watermarkTargetY) {
|
|
1475
|
+
return
|
|
1476
|
+
}
|
|
1477
|
+
watermarkView = watermark
|
|
1478
|
+
watermarkTargetX = targetX
|
|
1479
|
+
watermarkTargetY = targetY
|
|
1480
|
+
|
|
1481
|
+
if (duration > 0) {
|
|
1482
|
+
watermark.animate()
|
|
1483
|
+
.translationY(targetY)
|
|
1484
|
+
.translationX(targetX)
|
|
1485
|
+
.setDuration(duration.toLong())
|
|
1486
|
+
.start()
|
|
1487
|
+
} else if (duration < 0) {
|
|
1488
|
+
watermark.animate()
|
|
1489
|
+
.translationY(targetY)
|
|
1490
|
+
.translationX(targetX)
|
|
1491
|
+
.start()
|
|
1492
|
+
} else {
|
|
1493
|
+
watermark.translationY = targetY
|
|
1494
|
+
watermark.translationX = targetX
|
|
1251
1495
|
}
|
|
1252
1496
|
}
|
|
1253
1497
|
|
|
1498
|
+
private fun attachWatermarkLayoutListener() {
|
|
1499
|
+
if (watermarkLayoutListener != null) return
|
|
1500
|
+
val view = mapView ?: return
|
|
1501
|
+
|
|
1502
|
+
val listener = ViewTreeObserver.OnGlobalLayoutListener {
|
|
1503
|
+
applyWatermarkTranslation(combinedEdgeInsets())
|
|
1504
|
+
}
|
|
1505
|
+
watermarkLayoutListener = listener
|
|
1506
|
+
view.viewTreeObserver.addOnGlobalLayoutListener(listener)
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
private fun detachWatermarkLayoutListener() {
|
|
1510
|
+
watermarkView = null
|
|
1511
|
+
val listener = watermarkLayoutListener ?: return
|
|
1512
|
+
watermarkLayoutListener = null
|
|
1513
|
+
mapView?.viewTreeObserver?.takeIf { it.isAlive }?.removeOnGlobalLayoutListener(listener)
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1254
1516
|
private fun attachWindowInsetsListener() {
|
|
1255
1517
|
val view = mapView ?: return
|
|
1256
1518
|
ViewCompat.setOnApplyWindowInsetsListener(view) { _, windowInsets ->
|
|
@@ -1304,5 +1566,9 @@ class GoogleMapProvider(private val context: Context) :
|
|
|
1304
1566
|
|
|
1305
1567
|
companion object {
|
|
1306
1568
|
const val DEMO_MAP_ID = "DEMO_MAP_ID"
|
|
1569
|
+
|
|
1570
|
+
// Undocumented cap on the lite mode bitmap (2048 minus a 2px border,
|
|
1571
|
+
// measured empirically)
|
|
1572
|
+
private const val LITE_MODE_MAX_SIZE = 2046
|
|
1307
1573
|
}
|
|
1308
1574
|
}
|
|
@@ -30,6 +30,7 @@ interface MapProvider {
|
|
|
30
30
|
fun setScrollEnabled(enabled: Boolean)
|
|
31
31
|
fun setRotateEnabled(enabled: Boolean)
|
|
32
32
|
fun setPitchEnabled(enabled: Boolean)
|
|
33
|
+
fun setCompassEnabled(enabled: Boolean)
|
|
33
34
|
fun setUserLocationEnabled(enabled: Boolean)
|
|
34
35
|
fun setUserLocationButtonEnabled(enabled: Boolean)
|
|
35
36
|
fun setMapType(value: String)
|