@lugg/maps 1.0.0-beta.2 → 1.0.0-beta.21

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.
Files changed (206) hide show
  1. package/README.md +3 -0
  2. package/android/src/main/java/com/luggmaps/LuggCalloutView.kt +9 -2
  3. package/android/src/main/java/com/luggmaps/LuggCalloutViewManager.kt +4 -3
  4. package/android/src/main/java/com/luggmaps/LuggCircleView.kt +85 -0
  5. package/android/src/main/java/com/luggmaps/LuggCircleViewManager.kt +79 -0
  6. package/android/src/main/java/com/luggmaps/LuggGroundOverlayView.kt +81 -0
  7. package/android/src/main/java/com/luggmaps/LuggGroundOverlayViewManager.kt +82 -0
  8. package/android/src/main/java/com/luggmaps/LuggMapView.kt +90 -0
  9. package/android/src/main/java/com/luggmaps/LuggMapViewManager.kt +45 -0
  10. package/android/src/main/java/com/luggmaps/LuggMapWrapperView.kt +27 -2
  11. package/android/src/main/java/com/luggmaps/LuggMarkerView.kt +30 -2
  12. package/android/src/main/java/com/luggmaps/LuggMarkerViewManager.kt +13 -0
  13. package/android/src/main/java/com/luggmaps/LuggPackage.kt +4 -1
  14. package/android/src/main/java/com/luggmaps/LuggPolylineView.kt +3 -23
  15. package/android/src/main/java/com/luggmaps/LuggTileOverlayView.kt +92 -0
  16. package/android/src/main/java/com/luggmaps/LuggTileOverlayViewManager.kt +80 -0
  17. package/android/src/main/java/com/luggmaps/core/GoogleMapProvider.kt +737 -37
  18. package/android/src/main/java/com/luggmaps/core/MapProviderDelegate.kt +15 -0
  19. package/android/src/main/java/com/luggmaps/core/PolylineAnimator.kt +65 -11
  20. package/android/src/main/java/com/luggmaps/events/CirclePressEvent.kt +12 -0
  21. package/android/src/main/java/com/luggmaps/events/GroundOverlayPressEvent.kt +12 -0
  22. package/android/src/main/java/com/luggmaps/events/TileOverlayPressEvent.kt +12 -0
  23. package/android/src/main/java/com/luggmaps/extensions/ViewExtensions.kt +11 -0
  24. package/ios/LuggCalloutView.h +1 -1
  25. package/ios/LuggCalloutView.mm +21 -11
  26. package/ios/LuggCircleView.h +31 -0
  27. package/ios/LuggCircleView.mm +133 -0
  28. package/ios/LuggGroundOverlayView.h +30 -0
  29. package/ios/LuggGroundOverlayView.mm +125 -0
  30. package/ios/LuggMapView.mm +254 -4
  31. package/ios/LuggMapWrapperView.h +5 -0
  32. package/ios/LuggMapWrapperView.mm +18 -0
  33. package/ios/LuggMarkerView.h +3 -0
  34. package/ios/LuggMarkerView.mm +25 -0
  35. package/ios/LuggTileOverlayView.h +31 -0
  36. package/ios/LuggTileOverlayView.mm +130 -0
  37. package/ios/core/AppleMapProvider.h +8 -0
  38. package/ios/core/AppleMapProvider.mm +663 -29
  39. package/ios/core/AppleStaticMapProvider.h +14 -0
  40. package/ios/core/AppleStaticMapProvider.mm +159 -0
  41. package/ios/core/GMSPolylineAnimator.m +46 -18
  42. package/ios/core/GoogleMapProvider.h +10 -0
  43. package/ios/core/GoogleMapProvider.mm +430 -51
  44. package/ios/core/GoogleStaticMapProvider.h +21 -0
  45. package/ios/core/GoogleStaticMapProvider.mm +213 -0
  46. package/ios/core/MKPolylineAnimator.m +67 -43
  47. package/ios/core/MapProviderDelegate.h +27 -1
  48. package/ios/core/StaticMapProviderBase.h +75 -0
  49. package/ios/core/StaticMapProviderBase.mm +845 -0
  50. package/ios/events/CameraIdleEvent.h +4 -2
  51. package/ios/events/CameraMoveEvent.h +4 -2
  52. package/ios/events/CirclePressEvent.h +22 -0
  53. package/ios/events/GroundOverlayPressEvent.h +22 -0
  54. package/ios/events/LongPressEvent.h +4 -2
  55. package/ios/events/MarkerDragEvent.h +12 -6
  56. package/ios/events/MarkerPressEvent.h +4 -2
  57. package/ios/events/PolygonPressEvent.h +4 -2
  58. package/ios/events/PressEvent.h +4 -2
  59. package/ios/events/ReadyEvent.h +4 -2
  60. package/ios/events/TileOverlayPressEvent.h +22 -0
  61. package/ios/extensions/MKMapView+Zoom.h +20 -0
  62. package/ios/extensions/MKMapView+Zoom.m +28 -18
  63. package/ios/extensions/UIView+Snapshot.h +16 -0
  64. package/ios/extensions/UIView+Snapshot.m +27 -0
  65. package/lib/module/MapProvider.web.js +4 -1
  66. package/lib/module/MapProvider.web.js.map +1 -1
  67. package/lib/module/MapView.js +19 -1
  68. package/lib/module/MapView.js.map +1 -1
  69. package/lib/module/MapView.web.js +30 -22
  70. package/lib/module/MapView.web.js.map +1 -1
  71. package/lib/module/components/Circle.js +46 -0
  72. package/lib/module/components/Circle.js.map +1 -0
  73. package/lib/module/components/Circle.types.js +4 -0
  74. package/lib/module/components/Circle.types.js.map +1 -0
  75. package/lib/module/components/Circle.web.js +79 -0
  76. package/lib/module/components/Circle.web.js.map +1 -0
  77. package/lib/module/components/GeoJson.js +3 -3
  78. package/lib/module/components/GeoJson.js.map +1 -1
  79. package/lib/module/components/GroundOverlay.js +45 -0
  80. package/lib/module/components/GroundOverlay.js.map +1 -0
  81. package/lib/module/components/GroundOverlay.types.js +4 -0
  82. package/lib/module/components/GroundOverlay.types.js.map +1 -0
  83. package/lib/module/components/GroundOverlay.web.js +77 -0
  84. package/lib/module/components/GroundOverlay.web.js.map +1 -0
  85. package/lib/module/components/Marker.js +22 -4
  86. package/lib/module/components/Marker.js.map +1 -1
  87. package/lib/module/components/Marker.web.js +23 -10
  88. package/lib/module/components/Marker.web.js.map +1 -1
  89. package/lib/module/components/Polygon.web.js +3 -3
  90. package/lib/module/components/Polygon.web.js.map +1 -1
  91. package/lib/module/components/Polyline.web.js +6 -12
  92. package/lib/module/components/Polyline.web.js.map +1 -1
  93. package/lib/module/components/TileOverlay.js +44 -0
  94. package/lib/module/components/TileOverlay.js.map +1 -0
  95. package/lib/module/components/TileOverlay.types.js +4 -0
  96. package/lib/module/components/TileOverlay.types.js.map +1 -0
  97. package/lib/module/components/TileOverlay.web.js +69 -0
  98. package/lib/module/components/TileOverlay.web.js.map +1 -0
  99. package/lib/module/components/index.js +3 -0
  100. package/lib/module/components/index.js.map +1 -1
  101. package/lib/module/components/index.web.js +3 -0
  102. package/lib/module/components/index.web.js.map +1 -1
  103. package/lib/module/fabric/LuggCalloutViewNativeComponent.ts +1 -1
  104. package/lib/module/fabric/LuggCircleViewNativeComponent.ts +25 -0
  105. package/lib/module/fabric/LuggGroundOverlayViewNativeComponent.ts +29 -0
  106. package/lib/module/fabric/LuggMapViewNativeComponent.ts +16 -5
  107. package/lib/module/fabric/LuggMarkerViewNativeComponent.ts +14 -2
  108. package/lib/module/fabric/LuggTileOverlayViewNativeComponent.ts +30 -0
  109. package/lib/module/index.js +3 -0
  110. package/lib/module/index.js.map +1 -1
  111. package/lib/module/index.web.js +3 -0
  112. package/lib/module/index.web.js.map +1 -1
  113. package/lib/typescript/src/MapProvider.web.d.ts +3 -2
  114. package/lib/typescript/src/MapProvider.web.d.ts.map +1 -1
  115. package/lib/typescript/src/MapView.d.ts +1 -1
  116. package/lib/typescript/src/MapView.d.ts.map +1 -1
  117. package/lib/typescript/src/MapView.types.d.ts +60 -1
  118. package/lib/typescript/src/MapView.types.d.ts.map +1 -1
  119. package/lib/typescript/src/MapView.web.d.ts +1 -1
  120. package/lib/typescript/src/MapView.web.d.ts.map +1 -1
  121. package/lib/typescript/src/components/Circle.d.ts +10 -0
  122. package/lib/typescript/src/components/Circle.d.ts.map +1 -0
  123. package/lib/typescript/src/components/Circle.types.d.ts +33 -0
  124. package/lib/typescript/src/components/Circle.types.d.ts.map +1 -0
  125. package/lib/typescript/src/components/Circle.web.d.ts +3 -0
  126. package/lib/typescript/src/components/Circle.web.d.ts.map +1 -0
  127. package/lib/typescript/src/components/GeoJson.d.ts +2 -1
  128. package/lib/typescript/src/components/GeoJson.d.ts.map +1 -1
  129. package/lib/typescript/src/components/GroundOverlay.d.ts +10 -0
  130. package/lib/typescript/src/components/GroundOverlay.d.ts.map +1 -0
  131. package/lib/typescript/src/components/GroundOverlay.types.d.ts +35 -0
  132. package/lib/typescript/src/components/GroundOverlay.types.d.ts.map +1 -0
  133. package/lib/typescript/src/components/GroundOverlay.web.d.ts +3 -0
  134. package/lib/typescript/src/components/GroundOverlay.web.d.ts.map +1 -0
  135. package/lib/typescript/src/components/Marker.d.ts +6 -3
  136. package/lib/typescript/src/components/Marker.d.ts.map +1 -1
  137. package/lib/typescript/src/components/Marker.types.d.ts +18 -6
  138. package/lib/typescript/src/components/Marker.types.d.ts.map +1 -1
  139. package/lib/typescript/src/components/Marker.web.d.ts +3 -2
  140. package/lib/typescript/src/components/Marker.web.d.ts.map +1 -1
  141. package/lib/typescript/src/components/Polygon.web.d.ts +1 -1
  142. package/lib/typescript/src/components/Polygon.web.d.ts.map +1 -1
  143. package/lib/typescript/src/components/Polyline.web.d.ts +1 -1
  144. package/lib/typescript/src/components/Polyline.web.d.ts.map +1 -1
  145. package/lib/typescript/src/components/TileOverlay.d.ts +10 -0
  146. package/lib/typescript/src/components/TileOverlay.d.ts.map +1 -0
  147. package/lib/typescript/src/components/TileOverlay.types.d.ts +36 -0
  148. package/lib/typescript/src/components/TileOverlay.types.d.ts.map +1 -0
  149. package/lib/typescript/src/components/TileOverlay.web.d.ts +3 -0
  150. package/lib/typescript/src/components/TileOverlay.web.d.ts.map +1 -0
  151. package/lib/typescript/src/components/index.d.ts +7 -1
  152. package/lib/typescript/src/components/index.d.ts.map +1 -1
  153. package/lib/typescript/src/components/index.web.d.ts +6 -0
  154. package/lib/typescript/src/components/index.web.d.ts.map +1 -1
  155. package/lib/typescript/src/fabric/LuggCalloutViewNativeComponent.d.ts +1 -1
  156. package/lib/typescript/src/fabric/LuggCircleViewNativeComponent.d.ts +18 -0
  157. package/lib/typescript/src/fabric/LuggCircleViewNativeComponent.d.ts.map +1 -0
  158. package/lib/typescript/src/fabric/LuggGroundOverlayViewNativeComponent.d.ts +21 -0
  159. package/lib/typescript/src/fabric/LuggGroundOverlayViewNativeComponent.d.ts.map +1 -0
  160. package/lib/typescript/src/fabric/LuggMapViewNativeComponent.d.ts +13 -5
  161. package/lib/typescript/src/fabric/LuggMapViewNativeComponent.d.ts.map +1 -1
  162. package/lib/typescript/src/fabric/LuggMarkerViewNativeComponent.d.ts +8 -1
  163. package/lib/typescript/src/fabric/LuggMarkerViewNativeComponent.d.ts.map +1 -1
  164. package/lib/typescript/src/fabric/LuggTileOverlayViewNativeComponent.d.ts +21 -0
  165. package/lib/typescript/src/fabric/LuggTileOverlayViewNativeComponent.d.ts.map +1 -0
  166. package/lib/typescript/src/index.d.ts +9 -3
  167. package/lib/typescript/src/index.d.ts.map +1 -1
  168. package/lib/typescript/src/index.web.d.ts +7 -1
  169. package/lib/typescript/src/index.web.d.ts.map +1 -1
  170. package/lib/typescript/src/types.d.ts +0 -4
  171. package/lib/typescript/src/types.d.ts.map +1 -1
  172. package/package.json +17 -11
  173. package/src/MapProvider.web.tsx +5 -3
  174. package/src/MapView.tsx +19 -1
  175. package/src/MapView.types.ts +142 -1
  176. package/src/MapView.web.tsx +379 -340
  177. package/src/components/Circle.tsx +52 -0
  178. package/src/components/Circle.types.ts +33 -0
  179. package/src/components/Circle.web.tsx +95 -0
  180. package/src/components/GeoJson.tsx +3 -3
  181. package/src/components/GroundOverlay.tsx +47 -0
  182. package/src/components/GroundOverlay.types.ts +36 -0
  183. package/src/components/GroundOverlay.web.tsx +81 -0
  184. package/src/components/Marker.tsx +32 -6
  185. package/src/components/Marker.types.ts +19 -6
  186. package/src/components/Marker.web.tsx +196 -150
  187. package/src/components/Polygon.web.tsx +93 -89
  188. package/src/components/Polyline.web.tsx +235 -235
  189. package/src/components/TileOverlay.tsx +44 -0
  190. package/src/components/TileOverlay.types.ts +37 -0
  191. package/src/components/TileOverlay.web.tsx +97 -0
  192. package/src/components/index.ts +10 -0
  193. package/src/components/index.web.ts +9 -0
  194. package/src/fabric/LuggCalloutViewNativeComponent.ts +1 -1
  195. package/src/fabric/LuggCircleViewNativeComponent.ts +25 -0
  196. package/src/fabric/LuggGroundOverlayViewNativeComponent.ts +29 -0
  197. package/src/fabric/LuggMapViewNativeComponent.ts +16 -5
  198. package/src/fabric/LuggMarkerViewNativeComponent.ts +14 -2
  199. package/src/fabric/LuggTileOverlayViewNativeComponent.ts +30 -0
  200. package/src/index.ts +12 -1
  201. package/src/index.web.ts +15 -0
  202. package/src/types.ts +0 -5
  203. package/plugin/build/withMapsAndroid.d.ts +0 -5
  204. package/plugin/build/withMapsAndroid.js +0 -15
  205. package/plugin/build/withMapsIOS.d.ts +0 -5
  206. package/plugin/build/withMapsIOS.js +0 -27
@@ -1,26 +1,42 @@
1
1
  package com.luggmaps.core
2
2
 
3
3
  import android.annotation.SuppressLint
4
+ import android.content.ComponentCallbacks2
4
5
  import android.content.Context
6
+ import android.content.res.Configuration
5
7
  import android.view.View
8
+ import android.view.ViewTreeObserver
6
9
  import android.widget.ImageView
7
10
  import androidx.core.graphics.createBitmap
11
+ import androidx.core.view.ViewCompat
12
+ import androidx.core.view.WindowInsetsCompat
8
13
  import com.facebook.react.uimanager.PixelUtil.dpToPx
9
14
  import com.google.android.gms.maps.CameraUpdateFactory
10
15
  import com.google.android.gms.maps.GoogleMap
11
16
  import com.google.android.gms.maps.GoogleMapOptions
12
17
  import com.google.android.gms.maps.MapView
13
18
  import com.google.android.gms.maps.OnMapReadyCallback
14
- import com.google.android.gms.maps.model.AdvancedMarker
15
- import com.google.android.gms.maps.model.AdvancedMarkerOptions
16
19
  import com.google.android.gms.maps.model.BitmapDescriptorFactory
20
+ import com.google.android.gms.maps.model.CameraPosition
21
+ import com.google.android.gms.maps.model.Circle
22
+ import com.google.android.gms.maps.model.CircleOptions
23
+ import com.google.android.gms.maps.model.GroundOverlay
24
+ import com.google.android.gms.maps.model.GroundOverlayOptions
17
25
  import com.google.android.gms.maps.model.LatLng
26
+ import com.google.android.gms.maps.model.LatLngBounds
18
27
  import com.google.android.gms.maps.model.MapColorScheme
19
28
  import com.google.android.gms.maps.model.Marker
29
+ import com.google.android.gms.maps.model.MarkerOptions
20
30
  import com.google.android.gms.maps.model.Polygon
21
31
  import com.google.android.gms.maps.model.PolygonOptions
22
32
  import com.google.android.gms.maps.model.PolylineOptions
33
+ import com.google.android.gms.maps.model.TileOverlayOptions
34
+ import com.google.android.gms.maps.model.UrlTileProvider
23
35
  import com.luggmaps.LuggCalloutView
36
+ import com.luggmaps.LuggCircleView
37
+ import com.luggmaps.LuggCircleViewDelegate
38
+ import com.luggmaps.LuggGroundOverlayView
39
+ import com.luggmaps.LuggGroundOverlayViewDelegate
24
40
  import com.luggmaps.LuggMapWrapperView
25
41
  import com.luggmaps.LuggMarkerView
26
42
  import com.luggmaps.LuggMarkerViewDelegate
@@ -28,6 +44,16 @@ import com.luggmaps.LuggPolygonView
28
44
  import com.luggmaps.LuggPolygonViewDelegate
29
45
  import com.luggmaps.LuggPolylineView
30
46
  import com.luggmaps.LuggPolylineViewDelegate
47
+ import com.luggmaps.LuggTileOverlayView
48
+ import com.luggmaps.LuggTileOverlayViewDelegate
49
+ import com.luggmaps.extensions.findViewByTag
50
+ import java.net.URL
51
+ import kotlin.math.atan
52
+ import kotlin.math.ln
53
+ import kotlin.math.log2
54
+ import kotlin.math.pow
55
+ import kotlin.math.sin
56
+ import kotlin.math.sinh
31
57
 
32
58
  class GoogleMapProvider(private val context: Context) :
33
59
  MapProvider,
@@ -35,22 +61,33 @@ class GoogleMapProvider(private val context: Context) :
35
61
  LuggMarkerViewDelegate,
36
62
  LuggPolylineViewDelegate,
37
63
  LuggPolygonViewDelegate,
64
+ LuggCircleViewDelegate,
65
+ LuggGroundOverlayViewDelegate,
66
+ LuggTileOverlayViewDelegate,
38
67
  GoogleMap.OnCameraMoveStartedListener,
39
68
  GoogleMap.OnCameraMoveListener,
40
69
  GoogleMap.OnCameraIdleListener,
41
70
  GoogleMap.OnMapClickListener,
42
71
  GoogleMap.OnMapLongClickListener,
43
72
  GoogleMap.OnPolygonClickListener,
73
+ GoogleMap.OnCircleClickListener,
74
+ GoogleMap.OnGroundOverlayClickListener,
44
75
  GoogleMap.OnMarkerClickListener,
45
76
  GoogleMap.OnMarkerDragListener,
46
- GoogleMap.InfoWindowAdapter {
77
+ GoogleMap.InfoWindowAdapter,
78
+ ComponentCallbacks2 {
47
79
 
48
80
  override var delegate: MapProviderDelegate? = null
49
81
  override val isMapReady: Boolean get() = _isMapReady
50
82
 
51
83
  var mapId: String = DEMO_MAP_ID
52
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
+
53
89
  private var wrapperView: LuggMapWrapperView? = null
90
+ private var currentNightMode: Int = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
54
91
  private var mapView: MapView? = null
55
92
  private var googleMap: GoogleMap? = null
56
93
  private var _isMapReady = false
@@ -58,10 +95,16 @@ class GoogleMapProvider(private val context: Context) :
58
95
  private val pendingMarkerViews = mutableSetOf<LuggMarkerView>()
59
96
  private val pendingPolylineViews = mutableSetOf<LuggPolylineView>()
60
97
  private val pendingPolygonViews = mutableSetOf<LuggPolygonView>()
98
+ private val pendingCircleViews = mutableSetOf<LuggCircleView>()
99
+ private val pendingGroundOverlayViews = mutableSetOf<LuggGroundOverlayView>()
100
+ private val pendingTileOverlayViews = mutableSetOf<LuggTileOverlayView>()
61
101
  private val polylineAnimators = mutableMapOf<LuggPolylineView, PolylineAnimator>()
62
102
  private val polygonToViewMap = mutableMapOf<Polygon, LuggPolygonView>()
103
+ private val circleToViewMap = mutableMapOf<Circle, LuggCircleView>()
104
+ private val groundOverlayToViewMap = mutableMapOf<GroundOverlay, LuggGroundOverlayView>()
63
105
  private val markerToViewMap = mutableMapOf<Marker, LuggMarkerView>()
64
106
  private val liveMarkerViews = mutableSetOf<LuggMarkerView>()
107
+ private var staticProjectionReady = false
65
108
  private var activeNonBubbledMarker: Marker? = null
66
109
  private var tapLocation: LatLng? = null
67
110
 
@@ -75,6 +118,7 @@ class GoogleMapProvider(private val context: Context) :
75
118
  private var scrollEnabled: Boolean = true
76
119
  private var rotateEnabled: Boolean = true
77
120
  private var pitchEnabled: Boolean = true
121
+ private var compassEnabled: Boolean = true
78
122
  private var userLocationEnabled: Boolean = false
79
123
  private var userLocationButtonEnabled: Boolean = false
80
124
 
@@ -88,6 +132,17 @@ class GoogleMapProvider(private val context: Context) :
88
132
  // Edge Insets
89
133
  private var edgeInsets: EdgeInsets = EdgeInsets()
90
134
 
135
+ // Inset adjustment
136
+ private var insetAdjustment: String = "never"
137
+ private var systemInsets: EdgeInsets = EdgeInsets()
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
+
91
146
  // region MapProvider
92
147
 
93
148
  override fun initializeMap(wrapperView: View, latitude: Double, longitude: Double, zoom: Float) {
@@ -98,18 +153,46 @@ class GoogleMapProvider(private val context: Context) :
98
153
  initialZoom = zoom
99
154
 
100
155
  val wrapper = wrapperView as LuggMapWrapperView
156
+ wrapper.relayoutChildOnRequest = staticMode
101
157
  this.wrapperView = wrapper
102
158
 
103
- val options = GoogleMapOptions().mapId(mapId)
159
+ context.applicationContext.registerComponentCallbacks(this)
160
+
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)
104
182
  mapView = MapView(context, options).also { view ->
105
183
  view.onCreate(null)
106
184
  view.onResume()
107
185
  view.getMapAsync(this)
108
186
  wrapper.addView(view)
109
187
  }
188
+ wrapper.onLayoutReady = null
110
189
  }
111
190
 
112
191
  override fun destroy() {
192
+ wrapperView?.onLayoutReady = null
193
+ detachWindowInsetsListener()
194
+ detachWatermarkLayoutListener()
195
+ context.applicationContext.unregisterComponentCallbacks(this)
113
196
  dismissNonBubbledCallout()
114
197
  for (markerView in liveMarkerViews) {
115
198
  markerView.onUpdate = null
@@ -118,9 +201,14 @@ class GoogleMapProvider(private val context: Context) :
118
201
  pendingMarkerViews.clear()
119
202
  pendingPolylineViews.clear()
120
203
  pendingPolygonViews.clear()
204
+ pendingCircleViews.clear()
205
+ pendingGroundOverlayViews.clear()
206
+ pendingTileOverlayViews.clear()
121
207
  polylineAnimators.values.forEach { it.destroy() }
122
208
  polylineAnimators.clear()
123
209
  polygonToViewMap.clear()
210
+ circleToViewMap.clear()
211
+ groundOverlayToViewMap.clear()
124
212
  markerToViewMap.clear()
125
213
  wrapperView?.touchEventHandler = null
126
214
  wrapperView = null
@@ -130,6 +218,8 @@ class GoogleMapProvider(private val context: Context) :
130
218
  googleMap?.setOnMapClickListener(null)
131
219
  googleMap?.setOnMapLongClickListener(null)
132
220
  googleMap?.setOnPolygonClickListener(null)
221
+ googleMap?.setOnCircleClickListener(null)
222
+ googleMap?.setOnGroundOverlayClickListener(null)
133
223
  googleMap?.setOnMarkerClickListener(null)
134
224
  googleMap?.setOnMarkerDragListener(null)
135
225
  googleMap?.setInfoWindowAdapter(null)
@@ -145,7 +235,23 @@ class GoogleMapProvider(private val context: Context) :
145
235
  googleMap = map
146
236
  _isMapReady = true
147
237
 
148
- val position = LatLng(initialLatitude, initialLongitude)
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)
149
255
  map.moveCamera(CameraUpdateFactory.newLatLngZoom(position, initialZoom))
150
256
 
151
257
  map.setOnCameraMoveStartedListener(this)
@@ -154,6 +260,8 @@ class GoogleMapProvider(private val context: Context) :
154
260
  map.setOnMapClickListener(this)
155
261
  map.setOnMapLongClickListener(this)
156
262
  map.setOnPolygonClickListener(this)
263
+ map.setOnCircleClickListener(this)
264
+ map.setOnGroundOverlayClickListener(this)
157
265
  map.setOnMarkerClickListener(this)
158
266
  map.setOnMarkerDragListener(this)
159
267
  map.setInfoWindowAdapter(this)
@@ -166,12 +274,15 @@ class GoogleMapProvider(private val context: Context) :
166
274
 
167
275
  applyUiSettings()
168
276
  applyZoomLimits()
169
- applyEdgeInsets()
277
+ applyInsetAdjustment()
170
278
  applyTheme()
171
279
  applyUserLocation()
172
280
  processPendingMarkers()
173
281
  processPendingPolylines()
174
282
  processPendingPolygons()
283
+ processPendingCircles()
284
+ processPendingGroundOverlays()
285
+ processPendingTileOverlays()
175
286
 
176
287
  delegate?.mapProviderDidReady()
177
288
  }
@@ -227,6 +338,22 @@ class GoogleMapProvider(private val context: Context) :
227
338
  }
228
339
  }
229
340
 
341
+ override fun onCircleClick(circle: Circle) {
342
+ val circleView = circleToViewMap[circle]
343
+ if (circleView?.tappable == true) {
344
+ circleView.emitPressEvent()
345
+ } else {
346
+ onMapClick(tapLocation ?: return)
347
+ }
348
+ }
349
+
350
+ override fun onGroundOverlayClick(groundOverlay: GroundOverlay) {
351
+ val view = groundOverlayToViewMap[groundOverlay]
352
+ if (view?.tappable == true) {
353
+ view.emitPressEvent()
354
+ }
355
+ }
356
+
230
357
  override fun onMarkerClick(marker: Marker): Boolean {
231
358
  dismissNonBubbledCallout()
232
359
 
@@ -236,10 +363,17 @@ class GoogleMapProvider(private val context: Context) :
236
363
 
237
364
  val calloutView = view.calloutView
238
365
  if (calloutView != null && !calloutView.bubbled && calloutView.hasCustomContent) {
239
- googleMap?.animateCamera(CameraUpdateFactory.newLatLng(marker.position))
366
+ if (view.centerOnPress) {
367
+ googleMap?.animateCamera(CameraUpdateFactory.newLatLng(marker.position))
368
+ }
240
369
  showNonBubbledCallout(marker, calloutView)
241
370
  return true
242
371
  }
372
+
373
+ if (!view.centerOnPress) {
374
+ marker.showInfoWindow()
375
+ return true
376
+ }
243
377
  }
244
378
  return false
245
379
  }
@@ -353,8 +487,8 @@ class GoogleMapProvider(private val context: Context) :
353
487
  val map = googleMap ?: return
354
488
 
355
489
  val point = map.projection.toScreenLocation(marker.position)
356
- contentView.translationX = point.x - contentView.width * calloutView.anchorX
357
- contentView.translationY = point.y - contentView.height * calloutView.anchorY
490
+ contentView.translationX = point.x - contentView.width / 2f + calloutView.offsetX
491
+ contentView.translationY = point.y - markerView.scaledContentHeight * markerView.anchorY - contentView.height + calloutView.offsetY
358
492
  }
359
493
 
360
494
  // endregion
@@ -363,24 +497,33 @@ class GoogleMapProvider(private val context: Context) :
363
497
 
364
498
  override fun setZoomEnabled(enabled: Boolean) {
365
499
  zoomEnabled = enabled
500
+ if (staticMode) return
366
501
  googleMap?.uiSettings?.isZoomGesturesEnabled = enabled
367
502
  }
368
503
 
369
504
  override fun setScrollEnabled(enabled: Boolean) {
370
505
  scrollEnabled = enabled
506
+ if (staticMode) return
371
507
  googleMap?.uiSettings?.isScrollGesturesEnabled = enabled
372
508
  }
373
509
 
374
510
  override fun setRotateEnabled(enabled: Boolean) {
375
511
  rotateEnabled = enabled
512
+ if (staticMode) return
376
513
  googleMap?.uiSettings?.isRotateGesturesEnabled = enabled
377
514
  }
378
515
 
379
516
  override fun setPitchEnabled(enabled: Boolean) {
380
517
  pitchEnabled = enabled
518
+ if (staticMode) return
381
519
  googleMap?.uiSettings?.isTiltGesturesEnabled = enabled
382
520
  }
383
521
 
522
+ override fun setCompassEnabled(enabled: Boolean) {
523
+ compassEnabled = enabled
524
+ googleMap?.uiSettings?.isCompassEnabled = enabled
525
+ }
526
+
384
527
  @SuppressLint("MissingPermission")
385
528
  override fun setUserLocationEnabled(enabled: Boolean) {
386
529
  userLocationEnabled = enabled
@@ -398,6 +541,15 @@ class GoogleMapProvider(private val context: Context) :
398
541
  googleMap?.uiSettings?.isMyLocationButtonEnabled = enabled
399
542
  }
400
543
 
544
+ override fun setMapType(value: String) {
545
+ googleMap?.mapType = when (value) {
546
+ "satellite" -> GoogleMap.MAP_TYPE_SATELLITE
547
+ "terrain" -> GoogleMap.MAP_TYPE_TERRAIN
548
+ "hybrid" -> GoogleMap.MAP_TYPE_HYBRID
549
+ else -> GoogleMap.MAP_TYPE_NORMAL
550
+ }
551
+ }
552
+
401
553
  override fun setTheme(value: String) {
402
554
  theme = value
403
555
  applyTheme()
@@ -418,6 +570,11 @@ class GoogleMapProvider(private val context: Context) :
418
570
  }
419
571
 
420
572
  override fun setEdgeInsets(edgeInsets: EdgeInsets) {
573
+ if (staticMode) {
574
+ setStaticEdgeInsets(edgeInsets)
575
+ return
576
+ }
577
+
421
578
  val oldInsets = this.edgeInsets
422
579
  this.edgeInsets = edgeInsets
423
580
  applyEdgeInsets()
@@ -431,13 +588,18 @@ class GoogleMapProvider(private val context: Context) :
431
588
  }
432
589
 
433
590
  override fun setEdgeInsets(edgeInsets: EdgeInsets, duration: Int) {
591
+ if (staticMode) {
592
+ setStaticEdgeInsets(edgeInsets)
593
+ return
594
+ }
595
+
434
596
  val map = googleMap
435
597
  val oldInsets = this.edgeInsets
436
598
  this.edgeInsets = edgeInsets
437
599
 
438
600
  if (map != null && oldInsets != edgeInsets) {
439
601
  val cameraUpdate = CameraUpdateFactory.newCameraPosition(map.cameraPosition)
440
- applyEdgeInsets()
602
+ applyEdgeInsets(duration)
441
603
  when {
442
604
  duration < 0 -> map.animateCamera(cameraUpdate)
443
605
  duration > 0 -> map.animateCamera(cameraUpdate, duration, null)
@@ -448,6 +610,17 @@ class GoogleMapProvider(private val context: Context) :
448
610
  }
449
611
  }
450
612
 
613
+ override fun setInsetAdjustment(value: String) {
614
+ insetAdjustment = value
615
+ applyInsetAdjustment()
616
+ }
617
+
618
+ override fun setPoiEnabled(enabled: Boolean) {}
619
+
620
+ override fun setPoiFilterMode(mode: String) {}
621
+
622
+ override fun setPoiFilterCategories(categories: List<String>) {}
623
+
451
624
  // endregion
452
625
 
453
626
  // region MarkerViewDelegate
@@ -469,6 +642,30 @@ class GoogleMapProvider(private val context: Context) :
469
642
  syncMarkerView(markerView)
470
643
  }
471
644
 
645
+ override fun showCalloutForMarkerView(markerView: LuggMarkerView) {
646
+ val marker = markerView.marker ?: return
647
+ val calloutView = markerView.calloutView
648
+
649
+ if (calloutView != null && calloutView.hasCustomContent) {
650
+ dismissNonBubbledCallout()
651
+ if (calloutView.bubbled) {
652
+ marker.showInfoWindow()
653
+ } else {
654
+ showNonBubbledCallout(marker, calloutView)
655
+ }
656
+ return
657
+ }
658
+
659
+ if (!markerView.title.isNullOrEmpty()) {
660
+ marker.showInfoWindow()
661
+ }
662
+ }
663
+
664
+ override fun hideCalloutForMarkerView(markerView: LuggMarkerView) {
665
+ dismissNonBubbledCallout()
666
+ markerView.marker?.hideInfoWindow()
667
+ }
668
+
472
669
  // endregion
473
670
 
474
671
  // region PolylineViewDelegate
@@ -477,6 +674,10 @@ class GoogleMapProvider(private val context: Context) :
477
674
  syncPolylineView(polylineView)
478
675
  }
479
676
 
677
+ override fun polylineViewDidDrop(polylineView: LuggPolylineView) {
678
+ teardownPolyline(polylineView)
679
+ }
680
+
480
681
  // endregion
481
682
 
482
683
  // region PolygonViewDelegate
@@ -487,6 +688,30 @@ class GoogleMapProvider(private val context: Context) :
487
688
 
488
689
  // endregion
489
690
 
691
+ // region CircleViewDelegate
692
+
693
+ override fun circleViewDidUpdate(circleView: LuggCircleView) {
694
+ syncCircleView(circleView)
695
+ }
696
+
697
+ // endregion
698
+
699
+ // region GroundOverlayViewDelegate
700
+
701
+ override fun groundOverlayViewDidUpdate(groundOverlayView: LuggGroundOverlayView) {
702
+ syncGroundOverlayView(groundOverlayView)
703
+ }
704
+
705
+ // endregion
706
+
707
+ // region TileOverlayViewDelegate
708
+
709
+ override fun tileOverlayViewDidUpdate(tileOverlayView: LuggTileOverlayView) {
710
+ syncTileOverlayView(tileOverlayView)
711
+ }
712
+
713
+ // endregion
714
+
490
715
  // region Marker Management
491
716
 
492
717
  override fun addMarkerView(markerView: LuggMarkerView) {
@@ -546,12 +771,12 @@ class GoogleMapProvider(private val context: Context) :
546
771
  val map = googleMap ?: return
547
772
 
548
773
  val position = LatLng(markerView.latitude, markerView.longitude)
549
- val options = AdvancedMarkerOptions()
774
+ val options = MarkerOptions()
550
775
  .position(position)
551
776
  .title(markerView.title)
552
777
  .snippet(markerView.description)
553
778
 
554
- val marker = map.addMarker(options) as AdvancedMarker
779
+ val marker = map.addMarker(options) ?: return
555
780
  marker.setAnchor(markerView.anchorX, markerView.anchorY)
556
781
  marker.zIndex = markerView.zIndex
557
782
  marker.rotation = markerView.rotate
@@ -569,9 +794,8 @@ class GoogleMapProvider(private val context: Context) :
569
794
  }
570
795
  }
571
796
 
572
- // Workaround: AdvancedMarker.iconView is buggy on Android, so we manually add the custom
573
- // content view to the wrapper and position it via screen projection instead. The underlying
574
- // 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.
575
799
  private fun showLiveMarker(markerView: LuggMarkerView) {
576
800
  val wrapper = wrapperView ?: return
577
801
 
@@ -582,6 +806,10 @@ class GoogleMapProvider(private val context: Context) :
582
806
 
583
807
  val contentView = markerView.contentView
584
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
585
813
  (contentView.parent as? android.view.ViewGroup)?.removeView(contentView)
586
814
  wrapper.addView(contentView)
587
815
  liveMarkerViews.add(markerView)
@@ -613,10 +841,12 @@ class GoogleMapProvider(private val context: Context) :
613
841
 
614
842
  private fun positionLiveMarker(markerView: LuggMarkerView) {
615
843
  val map = googleMap ?: return
844
+ if (staticMode && !staticProjectionReady) return
616
845
  val contentView = markerView.contentView
617
846
  val point = map.projection.toScreenLocation(LatLng(markerView.latitude, markerView.longitude))
618
847
  contentView.translationX = point.x - contentView.width * markerView.anchorX
619
848
  contentView.translationY = point.y - contentView.height * markerView.anchorY
849
+ contentView.visibility = View.VISIBLE
620
850
  }
621
851
 
622
852
  // endregion
@@ -629,8 +859,12 @@ class GoogleMapProvider(private val context: Context) :
629
859
  }
630
860
 
631
861
  override fun removePolylineView(polylineView: LuggPolylineView) {
632
- polylineAnimators[polylineView]?.destroy()
633
- polylineAnimators.remove(polylineView)
862
+ teardownPolyline(polylineView)
863
+ }
864
+
865
+ private fun teardownPolyline(polylineView: LuggPolylineView) {
866
+ pendingPolylineViews.remove(polylineView)
867
+ polylineAnimators.remove(polylineView)?.destroy()
634
868
  polylineView.polyline?.remove()
635
869
  polylineView.polyline = null
636
870
  }
@@ -646,15 +880,13 @@ class GoogleMapProvider(private val context: Context) :
646
880
  return
647
881
  }
648
882
 
649
- polylineView.polyline?.width = polylineView.strokeWidth.dpToPx()
650
- polylineView.polyline?.zIndex = polylineView.zIndex
651
-
652
883
  polylineAnimators[polylineView]?.apply {
653
884
  coordinates = polylineView.coordinates
654
885
  strokeColors = polylineView.strokeColors
655
886
  strokeWidth = polylineView.strokeWidth.dpToPx()
887
+ zIndex = polylineView.zIndex
656
888
  animatedOptions = polylineView.animatedOptions
657
- animated = polylineView.animated
889
+ animated = polylineView.animated && !staticMode
658
890
  update()
659
891
  }
660
892
  }
@@ -668,6 +900,8 @@ class GoogleMapProvider(private val context: Context) :
668
900
  private fun addPolylineViewToMap(polylineView: LuggPolylineView) {
669
901
  val map = googleMap ?: return
670
902
 
903
+ polylineAnimators.remove(polylineView)?.destroy()
904
+
671
905
  val options = PolylineOptions()
672
906
  .width(polylineView.strokeWidth.dpToPx())
673
907
  .zIndex(polylineView.zIndex)
@@ -680,8 +914,10 @@ class GoogleMapProvider(private val context: Context) :
680
914
  coordinates = polylineView.coordinates
681
915
  strokeColors = polylineView.strokeColors
682
916
  strokeWidth = polylineView.strokeWidth.dpToPx()
917
+ zIndex = polylineView.zIndex
683
918
  animatedOptions = polylineView.animatedOptions
684
- animated = polylineView.animated
919
+ // Static maps render once; show the full polyline instead of animating
920
+ animated = polylineView.animated && !staticMode
685
921
  update()
686
922
  }
687
923
 
@@ -753,6 +989,218 @@ class GoogleMapProvider(private val context: Context) :
753
989
 
754
990
  // endregion
755
991
 
992
+ // region Circle Management
993
+
994
+ override fun addCircleView(circleView: LuggCircleView) {
995
+ circleView.delegate = this
996
+ syncCircleView(circleView)
997
+ }
998
+
999
+ override fun removeCircleView(circleView: LuggCircleView) {
1000
+ circleView.circle?.let { circleToViewMap.remove(it) }
1001
+ circleView.circle?.remove()
1002
+ circleView.circle = null
1003
+ }
1004
+
1005
+ private fun syncCircleView(circleView: LuggCircleView) {
1006
+ if (googleMap == null) {
1007
+ pendingCircleViews.add(circleView)
1008
+ return
1009
+ }
1010
+
1011
+ if (circleView.circle == null) {
1012
+ addCircleViewToMap(circleView)
1013
+ return
1014
+ }
1015
+
1016
+ circleView.circle?.apply {
1017
+ center = circleView.center
1018
+ radius = circleView.radius
1019
+ fillColor = circleView.fillColor
1020
+ strokeColor = circleView.strokeColor
1021
+ strokeWidth = circleView.strokeWidth.dpToPx()
1022
+ zIndex = circleView.zIndex
1023
+ isClickable = true
1024
+ }
1025
+ }
1026
+
1027
+ private fun processPendingCircles() {
1028
+ if (googleMap == null) return
1029
+ pendingCircleViews.forEach { addCircleViewToMap(it) }
1030
+ pendingCircleViews.clear()
1031
+ }
1032
+
1033
+ private fun addCircleViewToMap(circleView: LuggCircleView) {
1034
+ val map = googleMap ?: return
1035
+
1036
+ val options = CircleOptions()
1037
+ .center(circleView.center)
1038
+ .radius(circleView.radius)
1039
+ .fillColor(circleView.fillColor)
1040
+ .strokeColor(circleView.strokeColor)
1041
+ .strokeWidth(circleView.strokeWidth.dpToPx())
1042
+ .zIndex(circleView.zIndex)
1043
+ .clickable(true)
1044
+
1045
+ val circle = map.addCircle(options)
1046
+ circleView.circle = circle
1047
+ circleToViewMap[circle] = circleView
1048
+ }
1049
+
1050
+ // endregion
1051
+
1052
+ // region Ground Overlay Management
1053
+
1054
+ override fun addGroundOverlayView(groundOverlayView: LuggGroundOverlayView) {
1055
+ groundOverlayView.delegate = this
1056
+ syncGroundOverlayView(groundOverlayView)
1057
+ }
1058
+
1059
+ override fun removeGroundOverlayView(groundOverlayView: LuggGroundOverlayView) {
1060
+ groundOverlayView.delegate = null
1061
+ groundOverlayView.groundOverlay?.let { groundOverlayToViewMap.remove(it) }
1062
+ groundOverlayView.groundOverlay?.remove()
1063
+ groundOverlayView.groundOverlay = null
1064
+ }
1065
+
1066
+ private fun syncGroundOverlayView(groundOverlayView: LuggGroundOverlayView) {
1067
+ if (googleMap == null) {
1068
+ pendingGroundOverlayViews.add(groundOverlayView)
1069
+ return
1070
+ }
1071
+
1072
+ val imageUri = groundOverlayView.imageUri
1073
+ if (imageUri.isEmpty()) return
1074
+
1075
+ // Remove old overlay
1076
+ groundOverlayView.groundOverlay?.let { groundOverlayToViewMap.remove(it) }
1077
+ groundOverlayView.groundOverlay?.remove()
1078
+ groundOverlayView.groundOverlay = null
1079
+
1080
+ // Load image async and add overlay
1081
+ Thread {
1082
+ try {
1083
+ val connection = URL(imageUri).openConnection() as java.net.HttpURLConnection
1084
+ connection.instanceFollowRedirects = true
1085
+ connection.connect()
1086
+ val bitmap = android.graphics.BitmapFactory.decodeStream(connection.inputStream)
1087
+ connection.disconnect()
1088
+ if (bitmap != null) {
1089
+ mapView?.post {
1090
+ addGroundOverlayToMap(groundOverlayView, bitmap)
1091
+ }
1092
+ }
1093
+ } catch (_: Exception) {}
1094
+ }.start()
1095
+ }
1096
+
1097
+ private fun addGroundOverlayToMap(groundOverlayView: LuggGroundOverlayView, bitmap: android.graphics.Bitmap) {
1098
+ val map = googleMap ?: return
1099
+
1100
+ val bounds = LatLngBounds(groundOverlayView.southwest, groundOverlayView.northeast)
1101
+ val options = GroundOverlayOptions()
1102
+ .image(BitmapDescriptorFactory.fromBitmap(bitmap))
1103
+ .positionFromBounds(bounds)
1104
+ .transparency(1f - groundOverlayView.overlayOpacity)
1105
+ .bearing(groundOverlayView.bearing)
1106
+ .zIndex(groundOverlayView.zIndex)
1107
+ .clickable(groundOverlayView.tappable)
1108
+
1109
+ val overlay = map.addGroundOverlay(options)
1110
+ if (overlay != null) {
1111
+ groundOverlayView.groundOverlay = overlay
1112
+ groundOverlayToViewMap[overlay] = groundOverlayView
1113
+ }
1114
+ }
1115
+
1116
+ private fun processPendingGroundOverlays() {
1117
+ if (googleMap == null) return
1118
+ pendingGroundOverlayViews.forEach { syncGroundOverlayView(it) }
1119
+ pendingGroundOverlayViews.clear()
1120
+ }
1121
+
1122
+ // endregion
1123
+
1124
+ // region Tile Overlay Management
1125
+
1126
+ override fun addTileOverlayView(tileOverlayView: LuggTileOverlayView) {
1127
+ tileOverlayView.delegate = this
1128
+ syncTileOverlayView(tileOverlayView)
1129
+ }
1130
+
1131
+ override fun removeTileOverlayView(tileOverlayView: LuggTileOverlayView) {
1132
+ tileOverlayView.delegate = null
1133
+ tileOverlayView.tileOverlay?.remove()
1134
+ tileOverlayView.tileOverlay = null
1135
+ }
1136
+
1137
+ private fun syncTileOverlayView(tileOverlayView: LuggTileOverlayView) {
1138
+ if (googleMap == null) {
1139
+ pendingTileOverlayViews.add(tileOverlayView)
1140
+ return
1141
+ }
1142
+
1143
+ val urlTemplate = tileOverlayView.urlTemplate
1144
+ if (urlTemplate.isEmpty()) return
1145
+
1146
+ // Remove old overlay
1147
+ tileOverlayView.tileOverlay?.remove()
1148
+ tileOverlayView.tileOverlay = null
1149
+
1150
+ val tileSize = tileOverlayView.tileSize
1151
+ val hasBounds = tileOverlayView.hasBounds
1152
+ val swLat = tileOverlayView.boundsSwLat
1153
+ val swLng = tileOverlayView.boundsSwLng
1154
+ val neLat = tileOverlayView.boundsNeLat
1155
+ val neLng = tileOverlayView.boundsNeLng
1156
+
1157
+ val tileProvider = object : UrlTileProvider(tileSize, tileSize) {
1158
+ override fun getTileUrl(x: Int, y: Int, zoom: Int): URL? {
1159
+ if (hasBounds) {
1160
+ val n = 2.0.pow(zoom.toDouble())
1161
+ val tileSWLat = Math.toDegrees(atan(sinh(Math.PI * (1 - 2.0 * (y + 1) / n))))
1162
+ val tileNELat = Math.toDegrees(atan(sinh(Math.PI * (1 - 2.0 * y / n))))
1163
+ val tileSWLng = x / n * 360.0 - 180.0
1164
+ val tileNELng = (x + 1) / n * 360.0 - 180.0
1165
+
1166
+ if (tileNELat < swLat ||
1167
+ tileSWLat > neLat ||
1168
+ tileNELng < swLng ||
1169
+ tileSWLng > neLng
1170
+ ) {
1171
+ return null
1172
+ }
1173
+ }
1174
+
1175
+ val url = urlTemplate
1176
+ .replace("{x}", x.toString())
1177
+ .replace("{y}", y.toString())
1178
+ .replace("{z}", zoom.toString())
1179
+ return try {
1180
+ URL(url)
1181
+ } catch (_: Exception) {
1182
+ null
1183
+ }
1184
+ }
1185
+ }
1186
+
1187
+ val options = TileOverlayOptions()
1188
+ .tileProvider(tileProvider)
1189
+ .transparency(1f - tileOverlayView.overlayOpacity)
1190
+ .zIndex(tileOverlayView.zIndex)
1191
+
1192
+ val overlay = googleMap?.addTileOverlay(options)
1193
+ tileOverlayView.tileOverlay = overlay
1194
+ }
1195
+
1196
+ private fun processPendingTileOverlays() {
1197
+ if (googleMap == null) return
1198
+ pendingTileOverlayViews.forEach { syncTileOverlayView(it) }
1199
+ pendingTileOverlayViews.clear()
1200
+ }
1201
+
1202
+ // endregion
1203
+
756
1204
  // region Lifecycle
757
1205
 
758
1206
  override fun pauseAnimations() {
@@ -768,10 +1216,25 @@ class GoogleMapProvider(private val context: Context) :
768
1216
  // region Commands
769
1217
 
770
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
+
771
1230
  val map = googleMap ?: return
772
1231
  val position = LatLng(latitude, longitude)
773
1232
  val targetZoom = if (zoom > 0) zoom.toFloat() else map.cameraPosition.zoom
774
- val cameraUpdate = CameraUpdateFactory.newLatLngZoom(position, targetZoom)
1233
+ val cameraPosition = CameraPosition.Builder(map.cameraPosition)
1234
+ .target(position)
1235
+ .zoom(targetZoom)
1236
+ .build()
1237
+ val cameraUpdate = CameraUpdateFactory.newCameraPosition(cameraPosition)
775
1238
  when {
776
1239
  duration < 0 -> map.animateCamera(cameraUpdate)
777
1240
  duration > 0 -> map.animateCamera(cameraUpdate, duration, null)
@@ -790,23 +1253,29 @@ class GoogleMapProvider(private val context: Context) :
790
1253
  val map = googleMap ?: return
791
1254
  if (coordinates.isEmpty()) return
792
1255
 
793
- val latLngs = coordinates.filterIsInstance<LatLng>()
794
- if (latLngs.isEmpty()) return
795
-
796
- val boundsBuilder = com.google.android.gms.maps.model.LatLngBounds.Builder()
797
- latLngs.forEach { boundsBuilder.include(it) }
798
- val bounds = boundsBuilder.build()
1256
+ val latLongs = coordinates.filterIsInstance<LatLng>()
1257
+ if (latLongs.isEmpty()) return
799
1258
 
800
1259
  val top = edgeInsetsTop.toFloat().dpToPx().toInt()
801
1260
  val left = edgeInsetsLeft.toFloat().dpToPx().toInt()
802
1261
  val bottom = edgeInsetsBottom.toFloat().dpToPx().toInt()
803
1262
  val right = edgeInsetsRight.toFloat().dpToPx().toInt()
804
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
+
1273
+ val combined = combinedEdgeInsets()
805
1274
  map.setPadding(
806
- edgeInsets.left + left,
807
- edgeInsets.top + top,
808
- edgeInsets.right + right,
809
- edgeInsets.bottom + bottom
1275
+ combined.left + left,
1276
+ combined.top + top,
1277
+ combined.right + right,
1278
+ combined.bottom + bottom
810
1279
  )
811
1280
 
812
1281
  val cameraUpdate = CameraUpdateFactory.newLatLngBounds(bounds, 0)
@@ -817,7 +1286,7 @@ class GoogleMapProvider(private val context: Context) :
817
1286
  else -> map.moveCamera(cameraUpdate)
818
1287
  }
819
1288
 
820
- map.setPadding(edgeInsets.left, edgeInsets.top, edgeInsets.right, edgeInsets.bottom)
1289
+ map.setPadding(combined.left, combined.top, combined.right, combined.bottom)
821
1290
  }
822
1291
 
823
1292
  // endregion
@@ -826,10 +1295,17 @@ class GoogleMapProvider(private val context: Context) :
826
1295
 
827
1296
  private fun applyUiSettings() {
828
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
+ }
829
1304
  isZoomGesturesEnabled = zoomEnabled
830
1305
  isScrollGesturesEnabled = scrollEnabled
831
1306
  isRotateGesturesEnabled = rotateEnabled
832
1307
  isTiltGesturesEnabled = pitchEnabled
1308
+ isCompassEnabled = compassEnabled
833
1309
  isMyLocationButtonEnabled = userLocationButtonEnabled
834
1310
  }
835
1311
  }
@@ -841,17 +1317,237 @@ class GoogleMapProvider(private val context: Context) :
841
1317
  }
842
1318
  }
843
1319
 
844
- private fun applyEdgeInsets() {
845
- googleMap?.setPadding(edgeInsets.left, edgeInsets.top, edgeInsets.right, edgeInsets.bottom)
1320
+ private fun applyInsetAdjustment() {
1321
+ if (insetAdjustment == "automatic") {
1322
+ attachWindowInsetsListener()
1323
+ } else {
1324
+ detachWindowInsetsListener()
1325
+ systemInsets = EdgeInsets()
1326
+ }
1327
+ applyEdgeInsets()
1328
+ }
1329
+
1330
+ private fun combinedEdgeInsets(): EdgeInsets =
1331
+ if (insetAdjustment == "automatic") {
1332
+ EdgeInsets(
1333
+ edgeInsets.top + systemInsets.top,
1334
+ edgeInsets.left + systemInsets.left,
1335
+ edgeInsets.bottom + systemInsets.bottom,
1336
+ edgeInsets.right + systemInsets.right
1337
+ )
1338
+ } else {
1339
+ edgeInsets
1340
+ }
1341
+
1342
+ private fun applyEdgeInsets(duration: Int = 0) {
1343
+ val combined = combinedEdgeInsets()
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
+ }
1350
+ applyWatermarkTranslation(combined, duration)
1351
+ }
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.
1450
+ private fun applyWatermarkTranslation(insets: EdgeInsets, duration: Int = 0) {
1451
+ val view = mapView ?: return
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
1495
+ }
1496
+ }
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
+
1516
+ private fun attachWindowInsetsListener() {
1517
+ val view = mapView ?: return
1518
+ ViewCompat.setOnApplyWindowInsetsListener(view) { _, windowInsets ->
1519
+ val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
1520
+ systemInsets = EdgeInsets(insets.top, insets.left, insets.bottom, insets.right)
1521
+ applyEdgeInsets()
1522
+ windowInsets
1523
+ }
1524
+ ViewCompat.requestApplyInsets(view)
1525
+ }
1526
+
1527
+ private fun detachWindowInsetsListener() {
1528
+ mapView?.let { ViewCompat.setOnApplyWindowInsetsListener(it, null) }
1529
+ }
1530
+
1531
+ override fun onConfigurationChanged(newConfig: Configuration) {
1532
+ val newNightMode = newConfig.uiMode and Configuration.UI_MODE_NIGHT_MASK
1533
+ if (newNightMode != currentNightMode) {
1534
+ currentNightMode = newNightMode
1535
+ applyTheme()
1536
+ }
846
1537
  }
847
1538
 
1539
+ @Deprecated("Deprecated in Java")
1540
+ override fun onLowMemory() {}
1541
+
1542
+ override fun onTrimMemory(level: Int) {}
1543
+
848
1544
  private fun applyTheme() {
849
1545
  val colorScheme = when (theme) {
850
1546
  "dark" -> MapColorScheme.DARK
851
1547
  "light" -> MapColorScheme.LIGHT
852
1548
  else -> MapColorScheme.FOLLOW_SYSTEM
853
1549
  }
854
- googleMap?.setMapColorScheme(colorScheme)
1550
+ googleMap?.mapColorScheme = colorScheme
855
1551
  }
856
1552
 
857
1553
  @SuppressLint("MissingPermission")
@@ -870,5 +1566,9 @@ class GoogleMapProvider(private val context: Context) :
870
1566
 
871
1567
  companion object {
872
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
873
1573
  }
874
1574
  }