@lugg/maps 1.0.0-beta.9 → 1.1.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 +56 -2
- package/android/src/main/java/com/luggmaps/LuggMapViewManager.kt +18 -0
- package/android/src/main/java/com/luggmaps/LuggMapWrapperView.kt +36 -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 +332 -42
- package/android/src/main/java/com/luggmaps/core/MapProviderDelegate.kt +6 -0
- package/android/src/main/java/com/luggmaps/core/PolylineAnimator.kt +61 -10
- package/ios/LuggMapView.mm +240 -10
- 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 +63 -47
- package/ios/core/AppleStaticMapProvider.h +14 -0
- package/ios/core/AppleStaticMapProvider.mm +233 -0
- package/ios/core/GMSPolylineAnimator.m +40 -15
- package/ios/core/GoogleMapProvider.h +10 -0
- package/ios/core/GoogleMapProvider.mm +67 -54
- package/ios/core/GoogleStaticMapProvider.h +21 -0
- package/ios/core/GoogleStaticMapProvider.mm +217 -0
- package/ios/core/MKPolylineAnimator.m +67 -43
- package/ios/core/MapEdgeInsets.h +18 -0
- package/ios/core/MapProviderDelegate.h +11 -0
- package/ios/core/StaticMapProviderBase.h +80 -0
- package/ios/core/StaticMapProviderBase.mm +847 -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 +12 -0
- package/lib/module/MapView.js.map +1 -1
- package/lib/module/MapView.web.js +29 -10
- package/lib/module/MapView.web.js.map +1 -1
- package/lib/module/fabric/LuggMapViewNativeComponent.ts +10 -1
- package/lib/typescript/src/MapView.d.ts +1 -0
- package/lib/typescript/src/MapView.d.ts.map +1 -1
- package/lib/typescript/src/MapView.types.d.ts +32 -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 +4 -0
- package/lib/typescript/src/fabric/LuggMapViewNativeComponent.d.ts.map +1 -1
- package/package.json +24 -14
- package/src/MapView.tsx +14 -0
- package/src/MapView.types.ts +32 -0
- package/src/MapView.web.tsx +39 -14
- package/src/fabric/LuggMapViewNativeComponent.ts +10 -1
- 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
|
|
|
@@ -118,6 +128,7 @@ class GoogleMapProvider(private val context: Context) :
|
|
|
118
128
|
|
|
119
129
|
// Theme
|
|
120
130
|
private var theme: String = "system"
|
|
131
|
+
private var mapType: String = "standard"
|
|
121
132
|
|
|
122
133
|
// Edge Insets
|
|
123
134
|
private var edgeInsets: EdgeInsets = EdgeInsets()
|
|
@@ -126,6 +137,13 @@ class GoogleMapProvider(private val context: Context) :
|
|
|
126
137
|
private var insetAdjustment: String = "never"
|
|
127
138
|
private var systemInsets: EdgeInsets = EdgeInsets()
|
|
128
139
|
|
|
140
|
+
// The maps renderer adds and positions the watermark asynchronously; the
|
|
141
|
+
// layout listener re-evaluates the translation on every layout pass
|
|
142
|
+
private var watermarkLayoutListener: ViewTreeObserver.OnGlobalLayoutListener? = null
|
|
143
|
+
private var watermarkView: View? = null
|
|
144
|
+
private var watermarkTargetX = 0f
|
|
145
|
+
private var watermarkTargetY = 0f
|
|
146
|
+
|
|
129
147
|
// region MapProvider
|
|
130
148
|
|
|
131
149
|
override fun initializeMap(wrapperView: View, latitude: Double, longitude: Double, zoom: Float) {
|
|
@@ -136,21 +154,45 @@ class GoogleMapProvider(private val context: Context) :
|
|
|
136
154
|
initialZoom = zoom
|
|
137
155
|
|
|
138
156
|
val wrapper = wrapperView as LuggMapWrapperView
|
|
157
|
+
wrapper.relayoutChildOnRequest = staticMode
|
|
139
158
|
this.wrapperView = wrapper
|
|
140
159
|
|
|
141
160
|
context.applicationContext.registerComponentCallbacks(this)
|
|
142
161
|
|
|
143
|
-
|
|
162
|
+
// Static mode needs the view size to pick lite vs full map, so wait
|
|
163
|
+
// for the first layout if it hasn't happened yet
|
|
164
|
+
if (staticMode && (wrapper.width == 0 || wrapper.height == 0)) {
|
|
165
|
+
wrapper.onLayoutReady = { createMapView() }
|
|
166
|
+
} else {
|
|
167
|
+
createMapView()
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private fun createMapView() {
|
|
172
|
+
if (mapView != null) return
|
|
173
|
+
val wrapper = wrapperView ?: return
|
|
174
|
+
|
|
175
|
+
// Lite mode renders the map as a single bitmap capped at ~2048px per
|
|
176
|
+
// dimension, centered in the view — bigger views get letterboxed. Fall
|
|
177
|
+
// back to a full map (gestures disabled) when the view exceeds the cap
|
|
178
|
+
val useLite = staticMode && maxOf(wrapper.width, wrapper.height) <= LITE_MODE_MAX_SIZE
|
|
179
|
+
val options = GoogleMapOptions().liteMode(useLite)
|
|
180
|
+
// Lite mode doesn't support map IDs (cloud-based styling); setting one
|
|
181
|
+
// makes the static map render blank
|
|
182
|
+
if (!useLite) options.mapId(mapId)
|
|
144
183
|
mapView = MapView(context, options).also { view ->
|
|
145
184
|
view.onCreate(null)
|
|
146
185
|
view.onResume()
|
|
147
186
|
view.getMapAsync(this)
|
|
148
|
-
wrapper.addView(view)
|
|
187
|
+
wrapper.addView(view, 0)
|
|
149
188
|
}
|
|
189
|
+
wrapper.onLayoutReady = null
|
|
150
190
|
}
|
|
151
191
|
|
|
152
192
|
override fun destroy() {
|
|
193
|
+
wrapperView?.onLayoutReady = null
|
|
153
194
|
detachWindowInsetsListener()
|
|
195
|
+
detachWatermarkLayoutListener()
|
|
154
196
|
context.applicationContext.unregisterComponentCallbacks(this)
|
|
155
197
|
dismissNonBubbledCallout()
|
|
156
198
|
for (markerView in liveMarkerViews) {
|
|
@@ -170,6 +212,9 @@ class GoogleMapProvider(private val context: Context) :
|
|
|
170
212
|
groundOverlayToViewMap.clear()
|
|
171
213
|
markerToViewMap.clear()
|
|
172
214
|
wrapperView?.touchEventHandler = null
|
|
215
|
+
// The wrapper outlives the provider on reload; don't leave the dead map
|
|
216
|
+
// view in it
|
|
217
|
+
mapView?.let { wrapperView?.removeView(it) }
|
|
173
218
|
wrapperView = null
|
|
174
219
|
googleMap?.setOnCameraMoveStartedListener(null)
|
|
175
220
|
googleMap?.setOnCameraMoveListener(null)
|
|
@@ -194,7 +239,23 @@ class GoogleMapProvider(private val context: Context) :
|
|
|
194
239
|
googleMap = map
|
|
195
240
|
_isMapReady = true
|
|
196
241
|
|
|
197
|
-
|
|
242
|
+
if (staticMode) {
|
|
243
|
+
// Tapping the map otherwise shows an "open in Google Maps" toolbar
|
|
244
|
+
map.uiSettings.isMapToolbarEnabled = false
|
|
245
|
+
// The projection is only valid once the map has rendered, and no
|
|
246
|
+
// camera events fire afterwards to correct live marker positions
|
|
247
|
+
map.setOnMapLoadedCallback {
|
|
248
|
+
staticProjectionReady = true
|
|
249
|
+
positionLiveMarkers()
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Padding must be in place before the initial camera move: setPadding
|
|
254
|
+
// never repositions the current camera, but camera updates after it are
|
|
255
|
+
// relative to the padded center
|
|
256
|
+
applyEdgeInsets()
|
|
257
|
+
|
|
258
|
+
val position = if (staticMode) staticCameraTarget() else LatLng(initialLatitude, initialLongitude)
|
|
198
259
|
map.moveCamera(CameraUpdateFactory.newLatLngZoom(position, initialZoom))
|
|
199
260
|
|
|
200
261
|
map.setOnCameraMoveStartedListener(this)
|
|
@@ -217,9 +278,9 @@ class GoogleMapProvider(private val context: Context) :
|
|
|
217
278
|
|
|
218
279
|
applyUiSettings()
|
|
219
280
|
applyZoomLimits()
|
|
220
|
-
applyEdgeInsets()
|
|
221
281
|
applyInsetAdjustment()
|
|
222
282
|
applyTheme()
|
|
283
|
+
setMapType(mapType)
|
|
223
284
|
applyUserLocation()
|
|
224
285
|
processPendingMarkers()
|
|
225
286
|
processPendingPolylines()
|
|
@@ -441,24 +502,33 @@ class GoogleMapProvider(private val context: Context) :
|
|
|
441
502
|
|
|
442
503
|
override fun setZoomEnabled(enabled: Boolean) {
|
|
443
504
|
zoomEnabled = enabled
|
|
505
|
+
if (staticMode) return
|
|
444
506
|
googleMap?.uiSettings?.isZoomGesturesEnabled = enabled
|
|
445
507
|
}
|
|
446
508
|
|
|
447
509
|
override fun setScrollEnabled(enabled: Boolean) {
|
|
448
510
|
scrollEnabled = enabled
|
|
511
|
+
if (staticMode) return
|
|
449
512
|
googleMap?.uiSettings?.isScrollGesturesEnabled = enabled
|
|
450
513
|
}
|
|
451
514
|
|
|
452
515
|
override fun setRotateEnabled(enabled: Boolean) {
|
|
453
516
|
rotateEnabled = enabled
|
|
517
|
+
if (staticMode) return
|
|
454
518
|
googleMap?.uiSettings?.isRotateGesturesEnabled = enabled
|
|
455
519
|
}
|
|
456
520
|
|
|
457
521
|
override fun setPitchEnabled(enabled: Boolean) {
|
|
458
522
|
pitchEnabled = enabled
|
|
523
|
+
if (staticMode) return
|
|
459
524
|
googleMap?.uiSettings?.isTiltGesturesEnabled = enabled
|
|
460
525
|
}
|
|
461
526
|
|
|
527
|
+
override fun setCompassEnabled(enabled: Boolean) {
|
|
528
|
+
compassEnabled = enabled
|
|
529
|
+
googleMap?.uiSettings?.isCompassEnabled = enabled
|
|
530
|
+
}
|
|
531
|
+
|
|
462
532
|
@SuppressLint("MissingPermission")
|
|
463
533
|
override fun setUserLocationEnabled(enabled: Boolean) {
|
|
464
534
|
userLocationEnabled = enabled
|
|
@@ -477,6 +547,7 @@ class GoogleMapProvider(private val context: Context) :
|
|
|
477
547
|
}
|
|
478
548
|
|
|
479
549
|
override fun setMapType(value: String) {
|
|
550
|
+
mapType = value
|
|
480
551
|
googleMap?.mapType = when (value) {
|
|
481
552
|
"satellite" -> GoogleMap.MAP_TYPE_SATELLITE
|
|
482
553
|
"terrain" -> GoogleMap.MAP_TYPE_TERRAIN
|
|
@@ -505,6 +576,11 @@ class GoogleMapProvider(private val context: Context) :
|
|
|
505
576
|
}
|
|
506
577
|
|
|
507
578
|
override fun setEdgeInsets(edgeInsets: EdgeInsets) {
|
|
579
|
+
if (staticMode) {
|
|
580
|
+
setStaticEdgeInsets(edgeInsets)
|
|
581
|
+
return
|
|
582
|
+
}
|
|
583
|
+
|
|
508
584
|
val oldInsets = this.edgeInsets
|
|
509
585
|
this.edgeInsets = edgeInsets
|
|
510
586
|
applyEdgeInsets()
|
|
@@ -518,6 +594,11 @@ class GoogleMapProvider(private val context: Context) :
|
|
|
518
594
|
}
|
|
519
595
|
|
|
520
596
|
override fun setEdgeInsets(edgeInsets: EdgeInsets, duration: Int) {
|
|
597
|
+
if (staticMode) {
|
|
598
|
+
setStaticEdgeInsets(edgeInsets)
|
|
599
|
+
return
|
|
600
|
+
}
|
|
601
|
+
|
|
521
602
|
val map = googleMap
|
|
522
603
|
val oldInsets = this.edgeInsets
|
|
523
604
|
this.edgeInsets = edgeInsets
|
|
@@ -599,6 +680,10 @@ class GoogleMapProvider(private val context: Context) :
|
|
|
599
680
|
syncPolylineView(polylineView)
|
|
600
681
|
}
|
|
601
682
|
|
|
683
|
+
override fun polylineViewDidDrop(polylineView: LuggPolylineView) {
|
|
684
|
+
teardownPolyline(polylineView)
|
|
685
|
+
}
|
|
686
|
+
|
|
602
687
|
// endregion
|
|
603
688
|
|
|
604
689
|
// region PolygonViewDelegate
|
|
@@ -692,12 +777,12 @@ class GoogleMapProvider(private val context: Context) :
|
|
|
692
777
|
val map = googleMap ?: return
|
|
693
778
|
|
|
694
779
|
val position = LatLng(markerView.latitude, markerView.longitude)
|
|
695
|
-
val options =
|
|
780
|
+
val options = MarkerOptions()
|
|
696
781
|
.position(position)
|
|
697
782
|
.title(markerView.title)
|
|
698
783
|
.snippet(markerView.description)
|
|
699
784
|
|
|
700
|
-
val marker = map.addMarker(options)
|
|
785
|
+
val marker = map.addMarker(options) ?: return
|
|
701
786
|
marker.setAnchor(markerView.anchorX, markerView.anchorY)
|
|
702
787
|
marker.zIndex = markerView.zIndex
|
|
703
788
|
marker.rotation = markerView.rotate
|
|
@@ -715,9 +800,8 @@ class GoogleMapProvider(private val context: Context) :
|
|
|
715
800
|
}
|
|
716
801
|
}
|
|
717
802
|
|
|
718
|
-
//
|
|
719
|
-
//
|
|
720
|
-
// marker uses a transparent bitmap matching the content size so taps still trigger onMarkerClick.
|
|
803
|
+
// Live marker: content view is added to the wrapper and positioned via screen projection.
|
|
804
|
+
// The underlying marker uses a transparent bitmap matching the content size so taps still trigger onMarkerClick.
|
|
721
805
|
private fun showLiveMarker(markerView: LuggMarkerView) {
|
|
722
806
|
val wrapper = wrapperView ?: return
|
|
723
807
|
|
|
@@ -728,6 +812,10 @@ class GoogleMapProvider(private val context: Context) :
|
|
|
728
812
|
|
|
729
813
|
val contentView = markerView.contentView
|
|
730
814
|
contentView.pointerEvents = com.facebook.react.uimanager.PointerEvents.NONE
|
|
815
|
+
// Until the lite map's projection is valid, hide the marker instead of
|
|
816
|
+
// flashing it at a wrong position; onMapLoaded positions and shows it
|
|
817
|
+
contentView.visibility =
|
|
818
|
+
if (staticMode && !staticProjectionReady) View.INVISIBLE else View.VISIBLE
|
|
731
819
|
(contentView.parent as? android.view.ViewGroup)?.removeView(contentView)
|
|
732
820
|
wrapper.addView(contentView)
|
|
733
821
|
liveMarkerViews.add(markerView)
|
|
@@ -759,10 +847,12 @@ class GoogleMapProvider(private val context: Context) :
|
|
|
759
847
|
|
|
760
848
|
private fun positionLiveMarker(markerView: LuggMarkerView) {
|
|
761
849
|
val map = googleMap ?: return
|
|
850
|
+
if (staticMode && !staticProjectionReady) return
|
|
762
851
|
val contentView = markerView.contentView
|
|
763
852
|
val point = map.projection.toScreenLocation(LatLng(markerView.latitude, markerView.longitude))
|
|
764
853
|
contentView.translationX = point.x - contentView.width * markerView.anchorX
|
|
765
854
|
contentView.translationY = point.y - contentView.height * markerView.anchorY
|
|
855
|
+
contentView.visibility = View.VISIBLE
|
|
766
856
|
}
|
|
767
857
|
|
|
768
858
|
// endregion
|
|
@@ -775,8 +865,12 @@ class GoogleMapProvider(private val context: Context) :
|
|
|
775
865
|
}
|
|
776
866
|
|
|
777
867
|
override fun removePolylineView(polylineView: LuggPolylineView) {
|
|
778
|
-
|
|
779
|
-
|
|
868
|
+
teardownPolyline(polylineView)
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
private fun teardownPolyline(polylineView: LuggPolylineView) {
|
|
872
|
+
pendingPolylineViews.remove(polylineView)
|
|
873
|
+
polylineAnimators.remove(polylineView)?.destroy()
|
|
780
874
|
polylineView.polyline?.remove()
|
|
781
875
|
polylineView.polyline = null
|
|
782
876
|
}
|
|
@@ -792,15 +886,13 @@ class GoogleMapProvider(private val context: Context) :
|
|
|
792
886
|
return
|
|
793
887
|
}
|
|
794
888
|
|
|
795
|
-
polylineView.polyline?.width = polylineView.strokeWidth.dpToPx()
|
|
796
|
-
polylineView.polyline?.zIndex = polylineView.zIndex
|
|
797
|
-
|
|
798
889
|
polylineAnimators[polylineView]?.apply {
|
|
799
890
|
coordinates = polylineView.coordinates
|
|
800
891
|
strokeColors = polylineView.strokeColors
|
|
801
892
|
strokeWidth = polylineView.strokeWidth.dpToPx()
|
|
893
|
+
zIndex = polylineView.zIndex
|
|
802
894
|
animatedOptions = polylineView.animatedOptions
|
|
803
|
-
animated = polylineView.animated
|
|
895
|
+
animated = polylineView.animated && !staticMode
|
|
804
896
|
update()
|
|
805
897
|
}
|
|
806
898
|
}
|
|
@@ -814,6 +906,8 @@ class GoogleMapProvider(private val context: Context) :
|
|
|
814
906
|
private fun addPolylineViewToMap(polylineView: LuggPolylineView) {
|
|
815
907
|
val map = googleMap ?: return
|
|
816
908
|
|
|
909
|
+
polylineAnimators.remove(polylineView)?.destroy()
|
|
910
|
+
|
|
817
911
|
val options = PolylineOptions()
|
|
818
912
|
.width(polylineView.strokeWidth.dpToPx())
|
|
819
913
|
.zIndex(polylineView.zIndex)
|
|
@@ -826,8 +920,10 @@ class GoogleMapProvider(private val context: Context) :
|
|
|
826
920
|
coordinates = polylineView.coordinates
|
|
827
921
|
strokeColors = polylineView.strokeColors
|
|
828
922
|
strokeWidth = polylineView.strokeWidth.dpToPx()
|
|
923
|
+
zIndex = polylineView.zIndex
|
|
829
924
|
animatedOptions = polylineView.animatedOptions
|
|
830
|
-
|
|
925
|
+
// Static maps render once; show the full polyline instead of animating
|
|
926
|
+
animated = polylineView.animated && !staticMode
|
|
831
927
|
update()
|
|
832
928
|
}
|
|
833
929
|
|
|
@@ -1111,6 +1207,24 @@ class GoogleMapProvider(private val context: Context) :
|
|
|
1111
1207
|
|
|
1112
1208
|
// endregion
|
|
1113
1209
|
|
|
1210
|
+
// region Camera
|
|
1211
|
+
|
|
1212
|
+
// A static map's camera is inset-shifted (staticCameraTarget), so report
|
|
1213
|
+
// the requested camera instead
|
|
1214
|
+
private val liveCameraPosition: CameraPosition?
|
|
1215
|
+
get() = if (staticMode) null else googleMap?.cameraPosition
|
|
1216
|
+
|
|
1217
|
+
override val cameraLatitude: Double
|
|
1218
|
+
get() = liveCameraPosition?.target?.latitude ?: initialLatitude
|
|
1219
|
+
|
|
1220
|
+
override val cameraLongitude: Double
|
|
1221
|
+
get() = liveCameraPosition?.target?.longitude ?: initialLongitude
|
|
1222
|
+
|
|
1223
|
+
override val cameraZoom: Float
|
|
1224
|
+
get() = liveCameraPosition?.zoom ?: initialZoom
|
|
1225
|
+
|
|
1226
|
+
// endregion
|
|
1227
|
+
|
|
1114
1228
|
// region Lifecycle
|
|
1115
1229
|
|
|
1116
1230
|
override fun pauseAnimations() {
|
|
@@ -1126,10 +1240,25 @@ class GoogleMapProvider(private val context: Context) :
|
|
|
1126
1240
|
// region Commands
|
|
1127
1241
|
|
|
1128
1242
|
override fun moveCamera(latitude: Double, longitude: Double, zoom: Double, duration: Int) {
|
|
1243
|
+
if (staticMode) {
|
|
1244
|
+
// Static maps don't animate; re-center at the final camera
|
|
1245
|
+
initialLatitude = latitude
|
|
1246
|
+
initialLongitude = longitude
|
|
1247
|
+
if (zoom > 0) initialZoom = zoom.toFloat()
|
|
1248
|
+
val map = googleMap ?: return
|
|
1249
|
+
map.moveCamera(CameraUpdateFactory.newLatLngZoom(staticCameraTarget(), initialZoom))
|
|
1250
|
+
positionLiveMarkers()
|
|
1251
|
+
return
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1129
1254
|
val map = googleMap ?: return
|
|
1130
1255
|
val position = LatLng(latitude, longitude)
|
|
1131
1256
|
val targetZoom = if (zoom > 0) zoom.toFloat() else map.cameraPosition.zoom
|
|
1132
|
-
val
|
|
1257
|
+
val cameraPosition = CameraPosition.Builder(map.cameraPosition)
|
|
1258
|
+
.target(position)
|
|
1259
|
+
.zoom(targetZoom)
|
|
1260
|
+
.build()
|
|
1261
|
+
val cameraUpdate = CameraUpdateFactory.newCameraPosition(cameraPosition)
|
|
1133
1262
|
when {
|
|
1134
1263
|
duration < 0 -> map.animateCamera(cameraUpdate)
|
|
1135
1264
|
duration > 0 -> map.animateCamera(cameraUpdate, duration, null)
|
|
@@ -1151,15 +1280,20 @@ class GoogleMapProvider(private val context: Context) :
|
|
|
1151
1280
|
val latLongs = coordinates.filterIsInstance<LatLng>()
|
|
1152
1281
|
if (latLongs.isEmpty()) return
|
|
1153
1282
|
|
|
1154
|
-
val boundsBuilder = LatLngBounds.Builder()
|
|
1155
|
-
latLongs.forEach { boundsBuilder.include(it) }
|
|
1156
|
-
val bounds = boundsBuilder.build()
|
|
1157
|
-
|
|
1158
1283
|
val top = edgeInsetsTop.toFloat().dpToPx().toInt()
|
|
1159
1284
|
val left = edgeInsetsLeft.toFloat().dpToPx().toInt()
|
|
1160
1285
|
val bottom = edgeInsetsBottom.toFloat().dpToPx().toInt()
|
|
1161
1286
|
val right = edgeInsetsRight.toFloat().dpToPx().toInt()
|
|
1162
1287
|
|
|
1288
|
+
if (staticMode) {
|
|
1289
|
+
fitStaticCoordinates(latLongs, top, left, bottom, right)
|
|
1290
|
+
return
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
val boundsBuilder = LatLngBounds.Builder()
|
|
1294
|
+
latLongs.forEach { boundsBuilder.include(it) }
|
|
1295
|
+
val bounds = boundsBuilder.build()
|
|
1296
|
+
|
|
1163
1297
|
val combined = combinedEdgeInsets()
|
|
1164
1298
|
map.setPadding(
|
|
1165
1299
|
combined.left + left,
|
|
@@ -1185,10 +1319,17 @@ class GoogleMapProvider(private val context: Context) :
|
|
|
1185
1319
|
|
|
1186
1320
|
private fun applyUiSettings() {
|
|
1187
1321
|
googleMap?.uiSettings?.apply {
|
|
1322
|
+
if (staticMode) {
|
|
1323
|
+
// The non-lite fallback is a full map; keep it behaving like a
|
|
1324
|
+
// static image (lite mode ignores gestures anyway)
|
|
1325
|
+
setAllGesturesEnabled(false)
|
|
1326
|
+
return
|
|
1327
|
+
}
|
|
1188
1328
|
isZoomGesturesEnabled = zoomEnabled
|
|
1189
1329
|
isScrollGesturesEnabled = scrollEnabled
|
|
1190
1330
|
isRotateGesturesEnabled = rotateEnabled
|
|
1191
1331
|
isTiltGesturesEnabled = pitchEnabled
|
|
1332
|
+
isCompassEnabled = compassEnabled
|
|
1192
1333
|
isMyLocationButtonEnabled = userLocationButtonEnabled
|
|
1193
1334
|
}
|
|
1194
1335
|
}
|
|
@@ -1224,31 +1365,176 @@ class GoogleMapProvider(private val context: Context) :
|
|
|
1224
1365
|
|
|
1225
1366
|
private fun applyEdgeInsets(duration: Int = 0) {
|
|
1226
1367
|
val combined = combinedEdgeInsets()
|
|
1227
|
-
|
|
1368
|
+
// Lite mode misrenders padding: the static image is drawn only within
|
|
1369
|
+
// the padded viewport instead of the full view. The camera target shift
|
|
1370
|
+
// (staticCameraTarget) compensates for insets instead.
|
|
1371
|
+
if (!staticMode) {
|
|
1372
|
+
googleMap?.setPadding(combined.left, combined.top, combined.right, combined.bottom)
|
|
1373
|
+
}
|
|
1228
1374
|
applyWatermarkTranslation(combined, duration)
|
|
1229
1375
|
}
|
|
1230
1376
|
|
|
1377
|
+
// Insets are applied by shifting the camera target so the coordinate
|
|
1378
|
+
// centers in the inset viewport like a live map would (see
|
|
1379
|
+
// applyEdgeInsets for why padding can't be used)
|
|
1380
|
+
private fun setStaticEdgeInsets(edgeInsets: EdgeInsets) {
|
|
1381
|
+
val oldInsets = this.edgeInsets
|
|
1382
|
+
this.edgeInsets = edgeInsets
|
|
1383
|
+
applyEdgeInsets()
|
|
1384
|
+
|
|
1385
|
+
val map = googleMap
|
|
1386
|
+
if (map != null && oldInsets != edgeInsets) {
|
|
1387
|
+
map.moveCamera(CameraUpdateFactory.newLatLngZoom(staticCameraTarget(), initialZoom))
|
|
1388
|
+
positionLiveMarkers()
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
private fun staticCameraTarget(): LatLng {
|
|
1393
|
+
val offsetX = (edgeInsets.left - edgeInsets.right) / 2.0
|
|
1394
|
+
val offsetY = (edgeInsets.top - edgeInsets.bottom) / 2.0
|
|
1395
|
+
if (offsetX == 0.0 && offsetY == 0.0) return LatLng(initialLatitude, initialLongitude)
|
|
1396
|
+
|
|
1397
|
+
val worldSize = mercatorWorldSize(initialZoom)
|
|
1398
|
+
return latLngFromMercator(
|
|
1399
|
+
mercatorX(initialLongitude) - offsetX / worldSize,
|
|
1400
|
+
mercatorY(initialLatitude) - offsetY / worldSize
|
|
1401
|
+
)
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
// A static camera can't fit with map padding (lite mode misrenders it);
|
|
1405
|
+
// compute the fitted camera in mercator space instead, shifting the
|
|
1406
|
+
// center for asymmetric padding like edge insets
|
|
1407
|
+
private fun fitStaticCoordinates(
|
|
1408
|
+
coordinates: List<LatLng>,
|
|
1409
|
+
top: Int,
|
|
1410
|
+
left: Int,
|
|
1411
|
+
bottom: Int,
|
|
1412
|
+
right: Int
|
|
1413
|
+
) {
|
|
1414
|
+
val map = googleMap ?: return
|
|
1415
|
+
val wrapper = wrapperView ?: return
|
|
1416
|
+
if (wrapper.width == 0 || wrapper.height == 0) return
|
|
1417
|
+
|
|
1418
|
+
var minX = Double.MAX_VALUE
|
|
1419
|
+
var minY = Double.MAX_VALUE
|
|
1420
|
+
var maxX = -Double.MAX_VALUE
|
|
1421
|
+
var maxY = -Double.MAX_VALUE
|
|
1422
|
+
for (coordinate in coordinates) {
|
|
1423
|
+
val x = mercatorX(coordinate.longitude)
|
|
1424
|
+
val y = mercatorY(coordinate.latitude)
|
|
1425
|
+
minX = minOf(minX, x)
|
|
1426
|
+
maxX = maxOf(maxX, x)
|
|
1427
|
+
minY = minOf(minY, y)
|
|
1428
|
+
maxY = maxOf(maxY, y)
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
val insets = combinedEdgeInsets()
|
|
1432
|
+
val availWidth = (wrapper.width - insets.left - insets.right - left - right).coerceAtLeast(1)
|
|
1433
|
+
val availHeight = (wrapper.height - insets.top - insets.bottom - top - bottom).coerceAtLeast(1)
|
|
1434
|
+
|
|
1435
|
+
// World size in px at which the bounds fit the padded viewport;
|
|
1436
|
+
// infinite for coincident coordinates - keep the current zoom then
|
|
1437
|
+
val fitWorldSize = minOf(availWidth / (maxX - minX), availHeight / (maxY - minY))
|
|
1438
|
+
if (fitWorldSize.isFinite()) {
|
|
1439
|
+
initialZoom = log2(fitWorldSize / 256f.dpToPx())
|
|
1440
|
+
.toFloat()
|
|
1441
|
+
.coerceIn(map.minZoomLevel, map.maxZoomLevel)
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
val worldSize = mercatorWorldSize(initialZoom)
|
|
1445
|
+
val center = latLngFromMercator(
|
|
1446
|
+
(minX + maxX) / 2.0 - (left - right) / 2.0 / worldSize,
|
|
1447
|
+
(minY + maxY) / 2.0 - (top - bottom) / 2.0 / worldSize
|
|
1448
|
+
)
|
|
1449
|
+
initialLatitude = center.latitude
|
|
1450
|
+
initialLongitude = center.longitude
|
|
1451
|
+
|
|
1452
|
+
map.moveCamera(CameraUpdateFactory.newLatLngZoom(staticCameraTarget(), initialZoom))
|
|
1453
|
+
positionLiveMarkers()
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
// Web mercator world space in [0, 1]
|
|
1457
|
+
private fun mercatorX(longitude: Double): Double = (longitude + 180.0) / 360.0
|
|
1458
|
+
|
|
1459
|
+
private fun mercatorY(latitude: Double): Double {
|
|
1460
|
+
val sinLat = sin(Math.toRadians(latitude))
|
|
1461
|
+
return 0.5 - ln((1.0 + sinLat) / (1.0 - sinLat)) / (4.0 * Math.PI)
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
private fun latLngFromMercator(x: Double, y: Double): LatLng =
|
|
1465
|
+
LatLng(Math.toDegrees(atan(sinh(Math.PI * (1.0 - 2.0 * y)))), x * 360.0 - 180.0)
|
|
1466
|
+
|
|
1467
|
+
// World size in px at the given zoom
|
|
1468
|
+
private fun mercatorWorldSize(zoom: Float): Double = 256f.dpToPx().toDouble() * 2.0.pow(zoom.toDouble())
|
|
1469
|
+
|
|
1470
|
+
// Whether the renderer repositions the watermark for setPadding varies by
|
|
1471
|
+
// renderer version and by whether the watermark existed when padding was
|
|
1472
|
+
// set (and lite mode never applies padding). Measure where layout actually
|
|
1473
|
+
// placed it and translate only for the remainder.
|
|
1231
1474
|
private fun applyWatermarkTranslation(insets: EdgeInsets, duration: Int = 0) {
|
|
1232
1475
|
val view = mapView ?: return
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1476
|
+
attachWatermarkLayoutListener()
|
|
1477
|
+
|
|
1478
|
+
if (view.height == 0) return
|
|
1479
|
+
val watermark = view.findViewByTag("GoogleWatermark") ?: return
|
|
1480
|
+
|
|
1481
|
+
// Layout position relative to the map view, excluding translation
|
|
1482
|
+
var left = 0
|
|
1483
|
+
var top = 0
|
|
1484
|
+
var current: View? = watermark
|
|
1485
|
+
while (current != null && current !== view) {
|
|
1486
|
+
left += current.left
|
|
1487
|
+
top += current.top
|
|
1488
|
+
current = current.parent as? View
|
|
1489
|
+
}
|
|
1490
|
+
if (current == null) return
|
|
1491
|
+
|
|
1492
|
+
val bottomGap = view.height - (top + watermark.height)
|
|
1493
|
+
val targetY = if (bottomGap >= insets.bottom) 0f else -insets.bottom.toFloat()
|
|
1494
|
+
val targetX = if (left >= insets.left) 0f else insets.left.toFloat()
|
|
1495
|
+
|
|
1496
|
+
// Skip when already applied (or animating) toward this target so layout
|
|
1497
|
+
// passes don't snap an in-flight animation
|
|
1498
|
+
if (watermark === watermarkView && targetX == watermarkTargetX && targetY == watermarkTargetY) {
|
|
1499
|
+
return
|
|
1500
|
+
}
|
|
1501
|
+
watermarkView = watermark
|
|
1502
|
+
watermarkTargetX = targetX
|
|
1503
|
+
watermarkTargetY = targetY
|
|
1504
|
+
|
|
1505
|
+
if (duration > 0) {
|
|
1506
|
+
watermark.animate()
|
|
1507
|
+
.translationY(targetY)
|
|
1508
|
+
.translationX(targetX)
|
|
1509
|
+
.setDuration(duration.toLong())
|
|
1510
|
+
.start()
|
|
1511
|
+
} else if (duration < 0) {
|
|
1512
|
+
watermark.animate()
|
|
1513
|
+
.translationY(targetY)
|
|
1514
|
+
.translationX(targetX)
|
|
1515
|
+
.start()
|
|
1516
|
+
} else {
|
|
1517
|
+
watermark.translationY = targetY
|
|
1518
|
+
watermark.translationX = targetX
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
private fun attachWatermarkLayoutListener() {
|
|
1523
|
+
if (watermarkLayoutListener != null) return
|
|
1524
|
+
val view = mapView ?: return
|
|
1525
|
+
|
|
1526
|
+
val listener = ViewTreeObserver.OnGlobalLayoutListener {
|
|
1527
|
+
applyWatermarkTranslation(combinedEdgeInsets())
|
|
1251
1528
|
}
|
|
1529
|
+
watermarkLayoutListener = listener
|
|
1530
|
+
view.viewTreeObserver.addOnGlobalLayoutListener(listener)
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
private fun detachWatermarkLayoutListener() {
|
|
1534
|
+
watermarkView = null
|
|
1535
|
+
val listener = watermarkLayoutListener ?: return
|
|
1536
|
+
watermarkLayoutListener = null
|
|
1537
|
+
mapView?.viewTreeObserver?.takeIf { it.isAlive }?.removeOnGlobalLayoutListener(listener)
|
|
1252
1538
|
}
|
|
1253
1539
|
|
|
1254
1540
|
private fun attachWindowInsetsListener() {
|
|
@@ -1304,5 +1590,9 @@ class GoogleMapProvider(private val context: Context) :
|
|
|
1304
1590
|
|
|
1305
1591
|
companion object {
|
|
1306
1592
|
const val DEMO_MAP_ID = "DEMO_MAP_ID"
|
|
1593
|
+
|
|
1594
|
+
// Undocumented cap on the lite mode bitmap (2048 minus a 2px border,
|
|
1595
|
+
// measured empirically)
|
|
1596
|
+
private const val LITE_MODE_MAX_SIZE = 2046
|
|
1307
1597
|
}
|
|
1308
1598
|
}
|
|
@@ -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)
|
|
@@ -57,6 +58,11 @@ interface MapProvider {
|
|
|
57
58
|
fun addTileOverlayView(tileOverlayView: LuggTileOverlayView)
|
|
58
59
|
fun removeTileOverlayView(tileOverlayView: LuggTileOverlayView)
|
|
59
60
|
|
|
61
|
+
// Current camera (the initial camera until the map is ready)
|
|
62
|
+
val cameraLatitude: Double
|
|
63
|
+
val cameraLongitude: Double
|
|
64
|
+
val cameraZoom: Float
|
|
65
|
+
|
|
60
66
|
// Lifecycle
|
|
61
67
|
fun pauseAnimations()
|
|
62
68
|
fun resumeAnimations()
|