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

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 +676 -36
  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,41 @@
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
6
8
  import android.widget.ImageView
7
9
  import androidx.core.graphics.createBitmap
10
+ import androidx.core.view.ViewCompat
11
+ import androidx.core.view.WindowInsetsCompat
8
12
  import com.facebook.react.uimanager.PixelUtil.dpToPx
9
13
  import com.google.android.gms.maps.CameraUpdateFactory
10
14
  import com.google.android.gms.maps.GoogleMap
11
15
  import com.google.android.gms.maps.GoogleMapOptions
12
16
  import com.google.android.gms.maps.MapView
13
17
  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
18
  import com.google.android.gms.maps.model.BitmapDescriptorFactory
19
+ import com.google.android.gms.maps.model.CameraPosition
20
+ import com.google.android.gms.maps.model.Circle
21
+ import com.google.android.gms.maps.model.CircleOptions
22
+ import com.google.android.gms.maps.model.GroundOverlay
23
+ import com.google.android.gms.maps.model.GroundOverlayOptions
17
24
  import com.google.android.gms.maps.model.LatLng
25
+ import com.google.android.gms.maps.model.LatLngBounds
18
26
  import com.google.android.gms.maps.model.MapColorScheme
19
27
  import com.google.android.gms.maps.model.Marker
28
+ import com.google.android.gms.maps.model.MarkerOptions
20
29
  import com.google.android.gms.maps.model.Polygon
21
30
  import com.google.android.gms.maps.model.PolygonOptions
22
31
  import com.google.android.gms.maps.model.PolylineOptions
32
+ import com.google.android.gms.maps.model.TileOverlayOptions
33
+ import com.google.android.gms.maps.model.UrlTileProvider
23
34
  import com.luggmaps.LuggCalloutView
35
+ import com.luggmaps.LuggCircleView
36
+ import com.luggmaps.LuggCircleViewDelegate
37
+ import com.luggmaps.LuggGroundOverlayView
38
+ import com.luggmaps.LuggGroundOverlayViewDelegate
24
39
  import com.luggmaps.LuggMapWrapperView
25
40
  import com.luggmaps.LuggMarkerView
26
41
  import com.luggmaps.LuggMarkerViewDelegate
@@ -28,6 +43,16 @@ import com.luggmaps.LuggPolygonView
28
43
  import com.luggmaps.LuggPolygonViewDelegate
29
44
  import com.luggmaps.LuggPolylineView
30
45
  import com.luggmaps.LuggPolylineViewDelegate
46
+ import com.luggmaps.LuggTileOverlayView
47
+ import com.luggmaps.LuggTileOverlayViewDelegate
48
+ import com.luggmaps.extensions.findViewByTag
49
+ import java.net.URL
50
+ import kotlin.math.atan
51
+ import kotlin.math.ln
52
+ import kotlin.math.log2
53
+ import kotlin.math.pow
54
+ import kotlin.math.sin
55
+ import kotlin.math.sinh
31
56
 
32
57
  class GoogleMapProvider(private val context: Context) :
33
58
  MapProvider,
@@ -35,22 +60,33 @@ class GoogleMapProvider(private val context: Context) :
35
60
  LuggMarkerViewDelegate,
36
61
  LuggPolylineViewDelegate,
37
62
  LuggPolygonViewDelegate,
63
+ LuggCircleViewDelegate,
64
+ LuggGroundOverlayViewDelegate,
65
+ LuggTileOverlayViewDelegate,
38
66
  GoogleMap.OnCameraMoveStartedListener,
39
67
  GoogleMap.OnCameraMoveListener,
40
68
  GoogleMap.OnCameraIdleListener,
41
69
  GoogleMap.OnMapClickListener,
42
70
  GoogleMap.OnMapLongClickListener,
43
71
  GoogleMap.OnPolygonClickListener,
72
+ GoogleMap.OnCircleClickListener,
73
+ GoogleMap.OnGroundOverlayClickListener,
44
74
  GoogleMap.OnMarkerClickListener,
45
75
  GoogleMap.OnMarkerDragListener,
46
- GoogleMap.InfoWindowAdapter {
76
+ GoogleMap.InfoWindowAdapter,
77
+ ComponentCallbacks2 {
47
78
 
48
79
  override var delegate: MapProviderDelegate? = null
49
80
  override val isMapReady: Boolean get() = _isMapReady
50
81
 
51
82
  var mapId: String = DEMO_MAP_ID
52
83
 
84
+ // Renders a non-interactive static map — lite mode when the view fits
85
+ // the lite bitmap cap, a gesture-less full map otherwise. Creation-time only.
86
+ var staticMode: Boolean = false
87
+
53
88
  private var wrapperView: LuggMapWrapperView? = null
89
+ private var currentNightMode: Int = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
54
90
  private var mapView: MapView? = null
55
91
  private var googleMap: GoogleMap? = null
56
92
  private var _isMapReady = false
@@ -58,10 +94,16 @@ class GoogleMapProvider(private val context: Context) :
58
94
  private val pendingMarkerViews = mutableSetOf<LuggMarkerView>()
59
95
  private val pendingPolylineViews = mutableSetOf<LuggPolylineView>()
60
96
  private val pendingPolygonViews = mutableSetOf<LuggPolygonView>()
97
+ private val pendingCircleViews = mutableSetOf<LuggCircleView>()
98
+ private val pendingGroundOverlayViews = mutableSetOf<LuggGroundOverlayView>()
99
+ private val pendingTileOverlayViews = mutableSetOf<LuggTileOverlayView>()
61
100
  private val polylineAnimators = mutableMapOf<LuggPolylineView, PolylineAnimator>()
62
101
  private val polygonToViewMap = mutableMapOf<Polygon, LuggPolygonView>()
102
+ private val circleToViewMap = mutableMapOf<Circle, LuggCircleView>()
103
+ private val groundOverlayToViewMap = mutableMapOf<GroundOverlay, LuggGroundOverlayView>()
63
104
  private val markerToViewMap = mutableMapOf<Marker, LuggMarkerView>()
64
105
  private val liveMarkerViews = mutableSetOf<LuggMarkerView>()
106
+ private var staticProjectionReady = false
65
107
  private var activeNonBubbledMarker: Marker? = null
66
108
  private var tapLocation: LatLng? = null
67
109
 
@@ -75,6 +117,7 @@ class GoogleMapProvider(private val context: Context) :
75
117
  private var scrollEnabled: Boolean = true
76
118
  private var rotateEnabled: Boolean = true
77
119
  private var pitchEnabled: Boolean = true
120
+ private var compassEnabled: Boolean = true
78
121
  private var userLocationEnabled: Boolean = false
79
122
  private var userLocationButtonEnabled: Boolean = false
80
123
 
@@ -88,6 +131,10 @@ class GoogleMapProvider(private val context: Context) :
88
131
  // Edge Insets
89
132
  private var edgeInsets: EdgeInsets = EdgeInsets()
90
133
 
134
+ // Inset adjustment
135
+ private var insetAdjustment: String = "never"
136
+ private var systemInsets: EdgeInsets = EdgeInsets()
137
+
91
138
  // region MapProvider
92
139
 
93
140
  override fun initializeMap(wrapperView: View, latitude: Double, longitude: Double, zoom: Float) {
@@ -98,18 +145,45 @@ class GoogleMapProvider(private val context: Context) :
98
145
  initialZoom = zoom
99
146
 
100
147
  val wrapper = wrapperView as LuggMapWrapperView
148
+ wrapper.relayoutChildOnRequest = staticMode
101
149
  this.wrapperView = wrapper
102
150
 
103
- val options = GoogleMapOptions().mapId(mapId)
151
+ context.applicationContext.registerComponentCallbacks(this)
152
+
153
+ // Static mode needs the view size to pick lite vs full map, so wait
154
+ // for the first layout if it hasn't happened yet
155
+ if (staticMode && (wrapper.width == 0 || wrapper.height == 0)) {
156
+ wrapper.onLayoutReady = { createMapView() }
157
+ } else {
158
+ createMapView()
159
+ }
160
+ }
161
+
162
+ private fun createMapView() {
163
+ if (mapView != null) return
164
+ val wrapper = wrapperView ?: return
165
+
166
+ // Lite mode renders the map as a single bitmap capped at ~2048px per
167
+ // dimension, centered in the view — bigger views get letterboxed. Fall
168
+ // back to a full map (gestures disabled) when the view exceeds the cap
169
+ val useLite = staticMode && maxOf(wrapper.width, wrapper.height) <= LITE_MODE_MAX_SIZE
170
+ val options = GoogleMapOptions().liteMode(useLite)
171
+ // Lite mode doesn't support map IDs (cloud-based styling); setting one
172
+ // makes the static map render blank
173
+ if (!useLite) options.mapId(mapId)
104
174
  mapView = MapView(context, options).also { view ->
105
175
  view.onCreate(null)
106
176
  view.onResume()
107
177
  view.getMapAsync(this)
108
178
  wrapper.addView(view)
109
179
  }
180
+ wrapper.onLayoutReady = null
110
181
  }
111
182
 
112
183
  override fun destroy() {
184
+ wrapperView?.onLayoutReady = null
185
+ detachWindowInsetsListener()
186
+ context.applicationContext.unregisterComponentCallbacks(this)
113
187
  dismissNonBubbledCallout()
114
188
  for (markerView in liveMarkerViews) {
115
189
  markerView.onUpdate = null
@@ -118,9 +192,14 @@ class GoogleMapProvider(private val context: Context) :
118
192
  pendingMarkerViews.clear()
119
193
  pendingPolylineViews.clear()
120
194
  pendingPolygonViews.clear()
195
+ pendingCircleViews.clear()
196
+ pendingGroundOverlayViews.clear()
197
+ pendingTileOverlayViews.clear()
121
198
  polylineAnimators.values.forEach { it.destroy() }
122
199
  polylineAnimators.clear()
123
200
  polygonToViewMap.clear()
201
+ circleToViewMap.clear()
202
+ groundOverlayToViewMap.clear()
124
203
  markerToViewMap.clear()
125
204
  wrapperView?.touchEventHandler = null
126
205
  wrapperView = null
@@ -130,6 +209,8 @@ class GoogleMapProvider(private val context: Context) :
130
209
  googleMap?.setOnMapClickListener(null)
131
210
  googleMap?.setOnMapLongClickListener(null)
132
211
  googleMap?.setOnPolygonClickListener(null)
212
+ googleMap?.setOnCircleClickListener(null)
213
+ googleMap?.setOnGroundOverlayClickListener(null)
133
214
  googleMap?.setOnMarkerClickListener(null)
134
215
  googleMap?.setOnMarkerDragListener(null)
135
216
  googleMap?.setInfoWindowAdapter(null)
@@ -145,7 +226,18 @@ class GoogleMapProvider(private val context: Context) :
145
226
  googleMap = map
146
227
  _isMapReady = true
147
228
 
148
- val position = LatLng(initialLatitude, initialLongitude)
229
+ if (staticMode) {
230
+ // Tapping the map otherwise shows an "open in Google Maps" toolbar
231
+ map.uiSettings.isMapToolbarEnabled = false
232
+ // The projection is only valid once the map has rendered, and no
233
+ // camera events fire afterwards to correct live marker positions
234
+ map.setOnMapLoadedCallback {
235
+ staticProjectionReady = true
236
+ positionLiveMarkers()
237
+ }
238
+ }
239
+
240
+ val position = if (staticMode) staticCameraTarget() else LatLng(initialLatitude, initialLongitude)
149
241
  map.moveCamera(CameraUpdateFactory.newLatLngZoom(position, initialZoom))
150
242
 
151
243
  map.setOnCameraMoveStartedListener(this)
@@ -154,6 +246,8 @@ class GoogleMapProvider(private val context: Context) :
154
246
  map.setOnMapClickListener(this)
155
247
  map.setOnMapLongClickListener(this)
156
248
  map.setOnPolygonClickListener(this)
249
+ map.setOnCircleClickListener(this)
250
+ map.setOnGroundOverlayClickListener(this)
157
251
  map.setOnMarkerClickListener(this)
158
252
  map.setOnMarkerDragListener(this)
159
253
  map.setInfoWindowAdapter(this)
@@ -167,11 +261,15 @@ class GoogleMapProvider(private val context: Context) :
167
261
  applyUiSettings()
168
262
  applyZoomLimits()
169
263
  applyEdgeInsets()
264
+ applyInsetAdjustment()
170
265
  applyTheme()
171
266
  applyUserLocation()
172
267
  processPendingMarkers()
173
268
  processPendingPolylines()
174
269
  processPendingPolygons()
270
+ processPendingCircles()
271
+ processPendingGroundOverlays()
272
+ processPendingTileOverlays()
175
273
 
176
274
  delegate?.mapProviderDidReady()
177
275
  }
@@ -227,6 +325,22 @@ class GoogleMapProvider(private val context: Context) :
227
325
  }
228
326
  }
229
327
 
328
+ override fun onCircleClick(circle: Circle) {
329
+ val circleView = circleToViewMap[circle]
330
+ if (circleView?.tappable == true) {
331
+ circleView.emitPressEvent()
332
+ } else {
333
+ onMapClick(tapLocation ?: return)
334
+ }
335
+ }
336
+
337
+ override fun onGroundOverlayClick(groundOverlay: GroundOverlay) {
338
+ val view = groundOverlayToViewMap[groundOverlay]
339
+ if (view?.tappable == true) {
340
+ view.emitPressEvent()
341
+ }
342
+ }
343
+
230
344
  override fun onMarkerClick(marker: Marker): Boolean {
231
345
  dismissNonBubbledCallout()
232
346
 
@@ -236,10 +350,17 @@ class GoogleMapProvider(private val context: Context) :
236
350
 
237
351
  val calloutView = view.calloutView
238
352
  if (calloutView != null && !calloutView.bubbled && calloutView.hasCustomContent) {
239
- googleMap?.animateCamera(CameraUpdateFactory.newLatLng(marker.position))
353
+ if (view.centerOnPress) {
354
+ googleMap?.animateCamera(CameraUpdateFactory.newLatLng(marker.position))
355
+ }
240
356
  showNonBubbledCallout(marker, calloutView)
241
357
  return true
242
358
  }
359
+
360
+ if (!view.centerOnPress) {
361
+ marker.showInfoWindow()
362
+ return true
363
+ }
243
364
  }
244
365
  return false
245
366
  }
@@ -353,8 +474,8 @@ class GoogleMapProvider(private val context: Context) :
353
474
  val map = googleMap ?: return
354
475
 
355
476
  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
477
+ contentView.translationX = point.x - contentView.width / 2f + calloutView.offsetX
478
+ contentView.translationY = point.y - markerView.scaledContentHeight * markerView.anchorY - contentView.height + calloutView.offsetY
358
479
  }
359
480
 
360
481
  // endregion
@@ -363,24 +484,33 @@ class GoogleMapProvider(private val context: Context) :
363
484
 
364
485
  override fun setZoomEnabled(enabled: Boolean) {
365
486
  zoomEnabled = enabled
487
+ if (staticMode) return
366
488
  googleMap?.uiSettings?.isZoomGesturesEnabled = enabled
367
489
  }
368
490
 
369
491
  override fun setScrollEnabled(enabled: Boolean) {
370
492
  scrollEnabled = enabled
493
+ if (staticMode) return
371
494
  googleMap?.uiSettings?.isScrollGesturesEnabled = enabled
372
495
  }
373
496
 
374
497
  override fun setRotateEnabled(enabled: Boolean) {
375
498
  rotateEnabled = enabled
499
+ if (staticMode) return
376
500
  googleMap?.uiSettings?.isRotateGesturesEnabled = enabled
377
501
  }
378
502
 
379
503
  override fun setPitchEnabled(enabled: Boolean) {
380
504
  pitchEnabled = enabled
505
+ if (staticMode) return
381
506
  googleMap?.uiSettings?.isTiltGesturesEnabled = enabled
382
507
  }
383
508
 
509
+ override fun setCompassEnabled(enabled: Boolean) {
510
+ compassEnabled = enabled
511
+ googleMap?.uiSettings?.isCompassEnabled = enabled
512
+ }
513
+
384
514
  @SuppressLint("MissingPermission")
385
515
  override fun setUserLocationEnabled(enabled: Boolean) {
386
516
  userLocationEnabled = enabled
@@ -398,6 +528,15 @@ class GoogleMapProvider(private val context: Context) :
398
528
  googleMap?.uiSettings?.isMyLocationButtonEnabled = enabled
399
529
  }
400
530
 
531
+ override fun setMapType(value: String) {
532
+ googleMap?.mapType = when (value) {
533
+ "satellite" -> GoogleMap.MAP_TYPE_SATELLITE
534
+ "terrain" -> GoogleMap.MAP_TYPE_TERRAIN
535
+ "hybrid" -> GoogleMap.MAP_TYPE_HYBRID
536
+ else -> GoogleMap.MAP_TYPE_NORMAL
537
+ }
538
+ }
539
+
401
540
  override fun setTheme(value: String) {
402
541
  theme = value
403
542
  applyTheme()
@@ -418,6 +557,11 @@ class GoogleMapProvider(private val context: Context) :
418
557
  }
419
558
 
420
559
  override fun setEdgeInsets(edgeInsets: EdgeInsets) {
560
+ if (staticMode) {
561
+ setStaticEdgeInsets(edgeInsets)
562
+ return
563
+ }
564
+
421
565
  val oldInsets = this.edgeInsets
422
566
  this.edgeInsets = edgeInsets
423
567
  applyEdgeInsets()
@@ -431,13 +575,18 @@ class GoogleMapProvider(private val context: Context) :
431
575
  }
432
576
 
433
577
  override fun setEdgeInsets(edgeInsets: EdgeInsets, duration: Int) {
578
+ if (staticMode) {
579
+ setStaticEdgeInsets(edgeInsets)
580
+ return
581
+ }
582
+
434
583
  val map = googleMap
435
584
  val oldInsets = this.edgeInsets
436
585
  this.edgeInsets = edgeInsets
437
586
 
438
587
  if (map != null && oldInsets != edgeInsets) {
439
588
  val cameraUpdate = CameraUpdateFactory.newCameraPosition(map.cameraPosition)
440
- applyEdgeInsets()
589
+ applyEdgeInsets(duration)
441
590
  when {
442
591
  duration < 0 -> map.animateCamera(cameraUpdate)
443
592
  duration > 0 -> map.animateCamera(cameraUpdate, duration, null)
@@ -448,6 +597,17 @@ class GoogleMapProvider(private val context: Context) :
448
597
  }
449
598
  }
450
599
 
600
+ override fun setInsetAdjustment(value: String) {
601
+ insetAdjustment = value
602
+ applyInsetAdjustment()
603
+ }
604
+
605
+ override fun setPoiEnabled(enabled: Boolean) {}
606
+
607
+ override fun setPoiFilterMode(mode: String) {}
608
+
609
+ override fun setPoiFilterCategories(categories: List<String>) {}
610
+
451
611
  // endregion
452
612
 
453
613
  // region MarkerViewDelegate
@@ -469,6 +629,30 @@ class GoogleMapProvider(private val context: Context) :
469
629
  syncMarkerView(markerView)
470
630
  }
471
631
 
632
+ override fun showCalloutForMarkerView(markerView: LuggMarkerView) {
633
+ val marker = markerView.marker ?: return
634
+ val calloutView = markerView.calloutView
635
+
636
+ if (calloutView != null && calloutView.hasCustomContent) {
637
+ dismissNonBubbledCallout()
638
+ if (calloutView.bubbled) {
639
+ marker.showInfoWindow()
640
+ } else {
641
+ showNonBubbledCallout(marker, calloutView)
642
+ }
643
+ return
644
+ }
645
+
646
+ if (!markerView.title.isNullOrEmpty()) {
647
+ marker.showInfoWindow()
648
+ }
649
+ }
650
+
651
+ override fun hideCalloutForMarkerView(markerView: LuggMarkerView) {
652
+ dismissNonBubbledCallout()
653
+ markerView.marker?.hideInfoWindow()
654
+ }
655
+
472
656
  // endregion
473
657
 
474
658
  // region PolylineViewDelegate
@@ -477,6 +661,10 @@ class GoogleMapProvider(private val context: Context) :
477
661
  syncPolylineView(polylineView)
478
662
  }
479
663
 
664
+ override fun polylineViewDidDrop(polylineView: LuggPolylineView) {
665
+ teardownPolyline(polylineView)
666
+ }
667
+
480
668
  // endregion
481
669
 
482
670
  // region PolygonViewDelegate
@@ -487,6 +675,30 @@ class GoogleMapProvider(private val context: Context) :
487
675
 
488
676
  // endregion
489
677
 
678
+ // region CircleViewDelegate
679
+
680
+ override fun circleViewDidUpdate(circleView: LuggCircleView) {
681
+ syncCircleView(circleView)
682
+ }
683
+
684
+ // endregion
685
+
686
+ // region GroundOverlayViewDelegate
687
+
688
+ override fun groundOverlayViewDidUpdate(groundOverlayView: LuggGroundOverlayView) {
689
+ syncGroundOverlayView(groundOverlayView)
690
+ }
691
+
692
+ // endregion
693
+
694
+ // region TileOverlayViewDelegate
695
+
696
+ override fun tileOverlayViewDidUpdate(tileOverlayView: LuggTileOverlayView) {
697
+ syncTileOverlayView(tileOverlayView)
698
+ }
699
+
700
+ // endregion
701
+
490
702
  // region Marker Management
491
703
 
492
704
  override fun addMarkerView(markerView: LuggMarkerView) {
@@ -546,12 +758,12 @@ class GoogleMapProvider(private val context: Context) :
546
758
  val map = googleMap ?: return
547
759
 
548
760
  val position = LatLng(markerView.latitude, markerView.longitude)
549
- val options = AdvancedMarkerOptions()
761
+ val options = MarkerOptions()
550
762
  .position(position)
551
763
  .title(markerView.title)
552
764
  .snippet(markerView.description)
553
765
 
554
- val marker = map.addMarker(options) as AdvancedMarker
766
+ val marker = map.addMarker(options) ?: return
555
767
  marker.setAnchor(markerView.anchorX, markerView.anchorY)
556
768
  marker.zIndex = markerView.zIndex
557
769
  marker.rotation = markerView.rotate
@@ -569,9 +781,8 @@ class GoogleMapProvider(private val context: Context) :
569
781
  }
570
782
  }
571
783
 
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.
784
+ // Live marker: content view is added to the wrapper and positioned via screen projection.
785
+ // The underlying marker uses a transparent bitmap matching the content size so taps still trigger onMarkerClick.
575
786
  private fun showLiveMarker(markerView: LuggMarkerView) {
576
787
  val wrapper = wrapperView ?: return
577
788
 
@@ -582,6 +793,10 @@ class GoogleMapProvider(private val context: Context) :
582
793
 
583
794
  val contentView = markerView.contentView
584
795
  contentView.pointerEvents = com.facebook.react.uimanager.PointerEvents.NONE
796
+ // Until the lite map's projection is valid, hide the marker instead of
797
+ // flashing it at a wrong position; onMapLoaded positions and shows it
798
+ contentView.visibility =
799
+ if (staticMode && !staticProjectionReady) View.INVISIBLE else View.VISIBLE
585
800
  (contentView.parent as? android.view.ViewGroup)?.removeView(contentView)
586
801
  wrapper.addView(contentView)
587
802
  liveMarkerViews.add(markerView)
@@ -613,10 +828,12 @@ class GoogleMapProvider(private val context: Context) :
613
828
 
614
829
  private fun positionLiveMarker(markerView: LuggMarkerView) {
615
830
  val map = googleMap ?: return
831
+ if (staticMode && !staticProjectionReady) return
616
832
  val contentView = markerView.contentView
617
833
  val point = map.projection.toScreenLocation(LatLng(markerView.latitude, markerView.longitude))
618
834
  contentView.translationX = point.x - contentView.width * markerView.anchorX
619
835
  contentView.translationY = point.y - contentView.height * markerView.anchorY
836
+ contentView.visibility = View.VISIBLE
620
837
  }
621
838
 
622
839
  // endregion
@@ -629,8 +846,12 @@ class GoogleMapProvider(private val context: Context) :
629
846
  }
630
847
 
631
848
  override fun removePolylineView(polylineView: LuggPolylineView) {
632
- polylineAnimators[polylineView]?.destroy()
633
- polylineAnimators.remove(polylineView)
849
+ teardownPolyline(polylineView)
850
+ }
851
+
852
+ private fun teardownPolyline(polylineView: LuggPolylineView) {
853
+ pendingPolylineViews.remove(polylineView)
854
+ polylineAnimators.remove(polylineView)?.destroy()
634
855
  polylineView.polyline?.remove()
635
856
  polylineView.polyline = null
636
857
  }
@@ -646,15 +867,13 @@ class GoogleMapProvider(private val context: Context) :
646
867
  return
647
868
  }
648
869
 
649
- polylineView.polyline?.width = polylineView.strokeWidth.dpToPx()
650
- polylineView.polyline?.zIndex = polylineView.zIndex
651
-
652
870
  polylineAnimators[polylineView]?.apply {
653
871
  coordinates = polylineView.coordinates
654
872
  strokeColors = polylineView.strokeColors
655
873
  strokeWidth = polylineView.strokeWidth.dpToPx()
874
+ zIndex = polylineView.zIndex
656
875
  animatedOptions = polylineView.animatedOptions
657
- animated = polylineView.animated
876
+ animated = polylineView.animated && !staticMode
658
877
  update()
659
878
  }
660
879
  }
@@ -668,6 +887,8 @@ class GoogleMapProvider(private val context: Context) :
668
887
  private fun addPolylineViewToMap(polylineView: LuggPolylineView) {
669
888
  val map = googleMap ?: return
670
889
 
890
+ polylineAnimators.remove(polylineView)?.destroy()
891
+
671
892
  val options = PolylineOptions()
672
893
  .width(polylineView.strokeWidth.dpToPx())
673
894
  .zIndex(polylineView.zIndex)
@@ -680,8 +901,10 @@ class GoogleMapProvider(private val context: Context) :
680
901
  coordinates = polylineView.coordinates
681
902
  strokeColors = polylineView.strokeColors
682
903
  strokeWidth = polylineView.strokeWidth.dpToPx()
904
+ zIndex = polylineView.zIndex
683
905
  animatedOptions = polylineView.animatedOptions
684
- animated = polylineView.animated
906
+ // Static maps render once; show the full polyline instead of animating
907
+ animated = polylineView.animated && !staticMode
685
908
  update()
686
909
  }
687
910
 
@@ -753,6 +976,218 @@ class GoogleMapProvider(private val context: Context) :
753
976
 
754
977
  // endregion
755
978
 
979
+ // region Circle Management
980
+
981
+ override fun addCircleView(circleView: LuggCircleView) {
982
+ circleView.delegate = this
983
+ syncCircleView(circleView)
984
+ }
985
+
986
+ override fun removeCircleView(circleView: LuggCircleView) {
987
+ circleView.circle?.let { circleToViewMap.remove(it) }
988
+ circleView.circle?.remove()
989
+ circleView.circle = null
990
+ }
991
+
992
+ private fun syncCircleView(circleView: LuggCircleView) {
993
+ if (googleMap == null) {
994
+ pendingCircleViews.add(circleView)
995
+ return
996
+ }
997
+
998
+ if (circleView.circle == null) {
999
+ addCircleViewToMap(circleView)
1000
+ return
1001
+ }
1002
+
1003
+ circleView.circle?.apply {
1004
+ center = circleView.center
1005
+ radius = circleView.radius
1006
+ fillColor = circleView.fillColor
1007
+ strokeColor = circleView.strokeColor
1008
+ strokeWidth = circleView.strokeWidth.dpToPx()
1009
+ zIndex = circleView.zIndex
1010
+ isClickable = true
1011
+ }
1012
+ }
1013
+
1014
+ private fun processPendingCircles() {
1015
+ if (googleMap == null) return
1016
+ pendingCircleViews.forEach { addCircleViewToMap(it) }
1017
+ pendingCircleViews.clear()
1018
+ }
1019
+
1020
+ private fun addCircleViewToMap(circleView: LuggCircleView) {
1021
+ val map = googleMap ?: return
1022
+
1023
+ val options = CircleOptions()
1024
+ .center(circleView.center)
1025
+ .radius(circleView.radius)
1026
+ .fillColor(circleView.fillColor)
1027
+ .strokeColor(circleView.strokeColor)
1028
+ .strokeWidth(circleView.strokeWidth.dpToPx())
1029
+ .zIndex(circleView.zIndex)
1030
+ .clickable(true)
1031
+
1032
+ val circle = map.addCircle(options)
1033
+ circleView.circle = circle
1034
+ circleToViewMap[circle] = circleView
1035
+ }
1036
+
1037
+ // endregion
1038
+
1039
+ // region Ground Overlay Management
1040
+
1041
+ override fun addGroundOverlayView(groundOverlayView: LuggGroundOverlayView) {
1042
+ groundOverlayView.delegate = this
1043
+ syncGroundOverlayView(groundOverlayView)
1044
+ }
1045
+
1046
+ override fun removeGroundOverlayView(groundOverlayView: LuggGroundOverlayView) {
1047
+ groundOverlayView.delegate = null
1048
+ groundOverlayView.groundOverlay?.let { groundOverlayToViewMap.remove(it) }
1049
+ groundOverlayView.groundOverlay?.remove()
1050
+ groundOverlayView.groundOverlay = null
1051
+ }
1052
+
1053
+ private fun syncGroundOverlayView(groundOverlayView: LuggGroundOverlayView) {
1054
+ if (googleMap == null) {
1055
+ pendingGroundOverlayViews.add(groundOverlayView)
1056
+ return
1057
+ }
1058
+
1059
+ val imageUri = groundOverlayView.imageUri
1060
+ if (imageUri.isEmpty()) return
1061
+
1062
+ // Remove old overlay
1063
+ groundOverlayView.groundOverlay?.let { groundOverlayToViewMap.remove(it) }
1064
+ groundOverlayView.groundOverlay?.remove()
1065
+ groundOverlayView.groundOverlay = null
1066
+
1067
+ // Load image async and add overlay
1068
+ Thread {
1069
+ try {
1070
+ val connection = URL(imageUri).openConnection() as java.net.HttpURLConnection
1071
+ connection.instanceFollowRedirects = true
1072
+ connection.connect()
1073
+ val bitmap = android.graphics.BitmapFactory.decodeStream(connection.inputStream)
1074
+ connection.disconnect()
1075
+ if (bitmap != null) {
1076
+ mapView?.post {
1077
+ addGroundOverlayToMap(groundOverlayView, bitmap)
1078
+ }
1079
+ }
1080
+ } catch (_: Exception) {}
1081
+ }.start()
1082
+ }
1083
+
1084
+ private fun addGroundOverlayToMap(groundOverlayView: LuggGroundOverlayView, bitmap: android.graphics.Bitmap) {
1085
+ val map = googleMap ?: return
1086
+
1087
+ val bounds = LatLngBounds(groundOverlayView.southwest, groundOverlayView.northeast)
1088
+ val options = GroundOverlayOptions()
1089
+ .image(BitmapDescriptorFactory.fromBitmap(bitmap))
1090
+ .positionFromBounds(bounds)
1091
+ .transparency(1f - groundOverlayView.overlayOpacity)
1092
+ .bearing(groundOverlayView.bearing)
1093
+ .zIndex(groundOverlayView.zIndex)
1094
+ .clickable(groundOverlayView.tappable)
1095
+
1096
+ val overlay = map.addGroundOverlay(options)
1097
+ if (overlay != null) {
1098
+ groundOverlayView.groundOverlay = overlay
1099
+ groundOverlayToViewMap[overlay] = groundOverlayView
1100
+ }
1101
+ }
1102
+
1103
+ private fun processPendingGroundOverlays() {
1104
+ if (googleMap == null) return
1105
+ pendingGroundOverlayViews.forEach { syncGroundOverlayView(it) }
1106
+ pendingGroundOverlayViews.clear()
1107
+ }
1108
+
1109
+ // endregion
1110
+
1111
+ // region Tile Overlay Management
1112
+
1113
+ override fun addTileOverlayView(tileOverlayView: LuggTileOverlayView) {
1114
+ tileOverlayView.delegate = this
1115
+ syncTileOverlayView(tileOverlayView)
1116
+ }
1117
+
1118
+ override fun removeTileOverlayView(tileOverlayView: LuggTileOverlayView) {
1119
+ tileOverlayView.delegate = null
1120
+ tileOverlayView.tileOverlay?.remove()
1121
+ tileOverlayView.tileOverlay = null
1122
+ }
1123
+
1124
+ private fun syncTileOverlayView(tileOverlayView: LuggTileOverlayView) {
1125
+ if (googleMap == null) {
1126
+ pendingTileOverlayViews.add(tileOverlayView)
1127
+ return
1128
+ }
1129
+
1130
+ val urlTemplate = tileOverlayView.urlTemplate
1131
+ if (urlTemplate.isEmpty()) return
1132
+
1133
+ // Remove old overlay
1134
+ tileOverlayView.tileOverlay?.remove()
1135
+ tileOverlayView.tileOverlay = null
1136
+
1137
+ val tileSize = tileOverlayView.tileSize
1138
+ val hasBounds = tileOverlayView.hasBounds
1139
+ val swLat = tileOverlayView.boundsSwLat
1140
+ val swLng = tileOverlayView.boundsSwLng
1141
+ val neLat = tileOverlayView.boundsNeLat
1142
+ val neLng = tileOverlayView.boundsNeLng
1143
+
1144
+ val tileProvider = object : UrlTileProvider(tileSize, tileSize) {
1145
+ override fun getTileUrl(x: Int, y: Int, zoom: Int): URL? {
1146
+ if (hasBounds) {
1147
+ val n = 2.0.pow(zoom.toDouble())
1148
+ val tileSWLat = Math.toDegrees(atan(sinh(Math.PI * (1 - 2.0 * (y + 1) / n))))
1149
+ val tileNELat = Math.toDegrees(atan(sinh(Math.PI * (1 - 2.0 * y / n))))
1150
+ val tileSWLng = x / n * 360.0 - 180.0
1151
+ val tileNELng = (x + 1) / n * 360.0 - 180.0
1152
+
1153
+ if (tileNELat < swLat ||
1154
+ tileSWLat > neLat ||
1155
+ tileNELng < swLng ||
1156
+ tileSWLng > neLng
1157
+ ) {
1158
+ return null
1159
+ }
1160
+ }
1161
+
1162
+ val url = urlTemplate
1163
+ .replace("{x}", x.toString())
1164
+ .replace("{y}", y.toString())
1165
+ .replace("{z}", zoom.toString())
1166
+ return try {
1167
+ URL(url)
1168
+ } catch (_: Exception) {
1169
+ null
1170
+ }
1171
+ }
1172
+ }
1173
+
1174
+ val options = TileOverlayOptions()
1175
+ .tileProvider(tileProvider)
1176
+ .transparency(1f - tileOverlayView.overlayOpacity)
1177
+ .zIndex(tileOverlayView.zIndex)
1178
+
1179
+ val overlay = googleMap?.addTileOverlay(options)
1180
+ tileOverlayView.tileOverlay = overlay
1181
+ }
1182
+
1183
+ private fun processPendingTileOverlays() {
1184
+ if (googleMap == null) return
1185
+ pendingTileOverlayViews.forEach { syncTileOverlayView(it) }
1186
+ pendingTileOverlayViews.clear()
1187
+ }
1188
+
1189
+ // endregion
1190
+
756
1191
  // region Lifecycle
757
1192
 
758
1193
  override fun pauseAnimations() {
@@ -768,10 +1203,25 @@ class GoogleMapProvider(private val context: Context) :
768
1203
  // region Commands
769
1204
 
770
1205
  override fun moveCamera(latitude: Double, longitude: Double, zoom: Double, duration: Int) {
1206
+ if (staticMode) {
1207
+ // Static maps don't animate; re-center at the final camera
1208
+ initialLatitude = latitude
1209
+ initialLongitude = longitude
1210
+ if (zoom > 0) initialZoom = zoom.toFloat()
1211
+ val map = googleMap ?: return
1212
+ map.moveCamera(CameraUpdateFactory.newLatLngZoom(staticCameraTarget(), initialZoom))
1213
+ positionLiveMarkers()
1214
+ return
1215
+ }
1216
+
771
1217
  val map = googleMap ?: return
772
1218
  val position = LatLng(latitude, longitude)
773
1219
  val targetZoom = if (zoom > 0) zoom.toFloat() else map.cameraPosition.zoom
774
- val cameraUpdate = CameraUpdateFactory.newLatLngZoom(position, targetZoom)
1220
+ val cameraPosition = CameraPosition.Builder(map.cameraPosition)
1221
+ .target(position)
1222
+ .zoom(targetZoom)
1223
+ .build()
1224
+ val cameraUpdate = CameraUpdateFactory.newCameraPosition(cameraPosition)
775
1225
  when {
776
1226
  duration < 0 -> map.animateCamera(cameraUpdate)
777
1227
  duration > 0 -> map.animateCamera(cameraUpdate, duration, null)
@@ -790,23 +1240,29 @@ class GoogleMapProvider(private val context: Context) :
790
1240
  val map = googleMap ?: return
791
1241
  if (coordinates.isEmpty()) return
792
1242
 
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()
1243
+ val latLongs = coordinates.filterIsInstance<LatLng>()
1244
+ if (latLongs.isEmpty()) return
799
1245
 
800
1246
  val top = edgeInsetsTop.toFloat().dpToPx().toInt()
801
1247
  val left = edgeInsetsLeft.toFloat().dpToPx().toInt()
802
1248
  val bottom = edgeInsetsBottom.toFloat().dpToPx().toInt()
803
1249
  val right = edgeInsetsRight.toFloat().dpToPx().toInt()
804
1250
 
1251
+ if (staticMode) {
1252
+ fitStaticCoordinates(latLongs, top, left, bottom, right)
1253
+ return
1254
+ }
1255
+
1256
+ val boundsBuilder = LatLngBounds.Builder()
1257
+ latLongs.forEach { boundsBuilder.include(it) }
1258
+ val bounds = boundsBuilder.build()
1259
+
1260
+ val combined = combinedEdgeInsets()
805
1261
  map.setPadding(
806
- edgeInsets.left + left,
807
- edgeInsets.top + top,
808
- edgeInsets.right + right,
809
- edgeInsets.bottom + bottom
1262
+ combined.left + left,
1263
+ combined.top + top,
1264
+ combined.right + right,
1265
+ combined.bottom + bottom
810
1266
  )
811
1267
 
812
1268
  val cameraUpdate = CameraUpdateFactory.newLatLngBounds(bounds, 0)
@@ -817,7 +1273,7 @@ class GoogleMapProvider(private val context: Context) :
817
1273
  else -> map.moveCamera(cameraUpdate)
818
1274
  }
819
1275
 
820
- map.setPadding(edgeInsets.left, edgeInsets.top, edgeInsets.right, edgeInsets.bottom)
1276
+ map.setPadding(combined.left, combined.top, combined.right, combined.bottom)
821
1277
  }
822
1278
 
823
1279
  // endregion
@@ -826,10 +1282,17 @@ class GoogleMapProvider(private val context: Context) :
826
1282
 
827
1283
  private fun applyUiSettings() {
828
1284
  googleMap?.uiSettings?.apply {
1285
+ if (staticMode) {
1286
+ // The non-lite fallback is a full map; keep it behaving like a
1287
+ // static image (lite mode ignores gestures anyway)
1288
+ setAllGesturesEnabled(false)
1289
+ return
1290
+ }
829
1291
  isZoomGesturesEnabled = zoomEnabled
830
1292
  isScrollGesturesEnabled = scrollEnabled
831
1293
  isRotateGesturesEnabled = rotateEnabled
832
1294
  isTiltGesturesEnabled = pitchEnabled
1295
+ isCompassEnabled = compassEnabled
833
1296
  isMyLocationButtonEnabled = userLocationButtonEnabled
834
1297
  }
835
1298
  }
@@ -841,17 +1304,190 @@ class GoogleMapProvider(private val context: Context) :
841
1304
  }
842
1305
  }
843
1306
 
844
- private fun applyEdgeInsets() {
845
- googleMap?.setPadding(edgeInsets.left, edgeInsets.top, edgeInsets.right, edgeInsets.bottom)
1307
+ private fun applyInsetAdjustment() {
1308
+ if (insetAdjustment == "automatic") {
1309
+ attachWindowInsetsListener()
1310
+ } else {
1311
+ detachWindowInsetsListener()
1312
+ systemInsets = EdgeInsets()
1313
+ }
1314
+ applyEdgeInsets()
1315
+ }
1316
+
1317
+ private fun combinedEdgeInsets(): EdgeInsets =
1318
+ if (insetAdjustment == "automatic") {
1319
+ EdgeInsets(
1320
+ edgeInsets.top + systemInsets.top,
1321
+ edgeInsets.left + systemInsets.left,
1322
+ edgeInsets.bottom + systemInsets.bottom,
1323
+ edgeInsets.right + systemInsets.right
1324
+ )
1325
+ } else {
1326
+ edgeInsets
1327
+ }
1328
+
1329
+ private fun applyEdgeInsets(duration: Int = 0) {
1330
+ val combined = combinedEdgeInsets()
1331
+ // Lite mode misrenders padding: the static image is drawn only within
1332
+ // the padded viewport instead of the full view. The camera target shift
1333
+ // (staticCameraTarget) compensates for insets instead.
1334
+ if (!staticMode) {
1335
+ googleMap?.setPadding(combined.left, combined.top, combined.right, combined.bottom)
1336
+ }
1337
+ applyWatermarkTranslation(combined, duration)
1338
+ }
1339
+
1340
+ // Insets are applied by shifting the camera target so the coordinate
1341
+ // centers in the inset viewport like a live map would (see
1342
+ // applyEdgeInsets for why padding can't be used)
1343
+ private fun setStaticEdgeInsets(edgeInsets: EdgeInsets) {
1344
+ val oldInsets = this.edgeInsets
1345
+ this.edgeInsets = edgeInsets
1346
+ applyEdgeInsets()
1347
+
1348
+ val map = googleMap
1349
+ if (map != null && oldInsets != edgeInsets) {
1350
+ map.moveCamera(CameraUpdateFactory.newLatLngZoom(staticCameraTarget(), initialZoom))
1351
+ positionLiveMarkers()
1352
+ }
846
1353
  }
847
1354
 
1355
+ private fun staticCameraTarget(): LatLng {
1356
+ val offsetX = (edgeInsets.left - edgeInsets.right) / 2.0
1357
+ val offsetY = (edgeInsets.top - edgeInsets.bottom) / 2.0
1358
+ if (offsetX == 0.0 && offsetY == 0.0) return LatLng(initialLatitude, initialLongitude)
1359
+
1360
+ val worldSize = mercatorWorldSize(initialZoom)
1361
+ return latLngFromMercator(
1362
+ mercatorX(initialLongitude) - offsetX / worldSize,
1363
+ mercatorY(initialLatitude) - offsetY / worldSize
1364
+ )
1365
+ }
1366
+
1367
+ // A static camera can't fit with map padding (lite mode misrenders it);
1368
+ // compute the fitted camera in mercator space instead, shifting the
1369
+ // center for asymmetric padding like edge insets
1370
+ private fun fitStaticCoordinates(
1371
+ coordinates: List<LatLng>,
1372
+ top: Int,
1373
+ left: Int,
1374
+ bottom: Int,
1375
+ right: Int
1376
+ ) {
1377
+ val map = googleMap ?: return
1378
+ val wrapper = wrapperView ?: return
1379
+ if (wrapper.width == 0 || wrapper.height == 0) return
1380
+
1381
+ var minX = Double.MAX_VALUE
1382
+ var minY = Double.MAX_VALUE
1383
+ var maxX = -Double.MAX_VALUE
1384
+ var maxY = -Double.MAX_VALUE
1385
+ for (coordinate in coordinates) {
1386
+ val x = mercatorX(coordinate.longitude)
1387
+ val y = mercatorY(coordinate.latitude)
1388
+ minX = minOf(minX, x)
1389
+ maxX = maxOf(maxX, x)
1390
+ minY = minOf(minY, y)
1391
+ maxY = maxOf(maxY, y)
1392
+ }
1393
+
1394
+ val insets = combinedEdgeInsets()
1395
+ val availWidth = (wrapper.width - insets.left - insets.right - left - right).coerceAtLeast(1)
1396
+ val availHeight = (wrapper.height - insets.top - insets.bottom - top - bottom).coerceAtLeast(1)
1397
+
1398
+ // World size in px at which the bounds fit the padded viewport;
1399
+ // infinite for coincident coordinates - keep the current zoom then
1400
+ val fitWorldSize = minOf(availWidth / (maxX - minX), availHeight / (maxY - minY))
1401
+ if (fitWorldSize.isFinite()) {
1402
+ initialZoom = log2(fitWorldSize / 256f.dpToPx())
1403
+ .toFloat()
1404
+ .coerceIn(map.minZoomLevel, map.maxZoomLevel)
1405
+ }
1406
+
1407
+ val worldSize = mercatorWorldSize(initialZoom)
1408
+ val center = latLngFromMercator(
1409
+ (minX + maxX) / 2.0 - (left - right) / 2.0 / worldSize,
1410
+ (minY + maxY) / 2.0 - (top - bottom) / 2.0 / worldSize
1411
+ )
1412
+ initialLatitude = center.latitude
1413
+ initialLongitude = center.longitude
1414
+
1415
+ map.moveCamera(CameraUpdateFactory.newLatLngZoom(staticCameraTarget(), initialZoom))
1416
+ positionLiveMarkers()
1417
+ }
1418
+
1419
+ // Web mercator world space in [0, 1]
1420
+ private fun mercatorX(longitude: Double): Double = (longitude + 180.0) / 360.0
1421
+
1422
+ private fun mercatorY(latitude: Double): Double {
1423
+ val sinLat = sin(Math.toRadians(latitude))
1424
+ return 0.5 - ln((1.0 + sinLat) / (1.0 - sinLat)) / (4.0 * Math.PI)
1425
+ }
1426
+
1427
+ private fun latLngFromMercator(x: Double, y: Double): LatLng =
1428
+ LatLng(Math.toDegrees(atan(sinh(Math.PI * (1.0 - 2.0 * y)))), x * 360.0 - 180.0)
1429
+
1430
+ // World size in px at the given zoom
1431
+ private fun mercatorWorldSize(zoom: Float): Double = 256f.dpToPx().toDouble() * 2.0.pow(zoom.toDouble())
1432
+
1433
+ private fun applyWatermarkTranslation(insets: EdgeInsets, duration: Int = 0) {
1434
+ val view = mapView ?: return
1435
+ view.findViewByTag("GoogleWatermark")?.let { watermark ->
1436
+ val targetY = -insets.bottom.toFloat()
1437
+ val targetX = insets.left.toFloat()
1438
+ if (duration > 0) {
1439
+ watermark.animate()
1440
+ .translationY(targetY)
1441
+ .translationX(targetX)
1442
+ .setDuration(duration.toLong())
1443
+ .start()
1444
+ } else if (duration < 0) {
1445
+ watermark.animate()
1446
+ .translationY(targetY)
1447
+ .translationX(targetX)
1448
+ .start()
1449
+ } else {
1450
+ watermark.translationY = targetY
1451
+ watermark.translationX = targetX
1452
+ }
1453
+ }
1454
+ }
1455
+
1456
+ private fun attachWindowInsetsListener() {
1457
+ val view = mapView ?: return
1458
+ ViewCompat.setOnApplyWindowInsetsListener(view) { _, windowInsets ->
1459
+ val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
1460
+ systemInsets = EdgeInsets(insets.top, insets.left, insets.bottom, insets.right)
1461
+ applyEdgeInsets()
1462
+ windowInsets
1463
+ }
1464
+ ViewCompat.requestApplyInsets(view)
1465
+ }
1466
+
1467
+ private fun detachWindowInsetsListener() {
1468
+ mapView?.let { ViewCompat.setOnApplyWindowInsetsListener(it, null) }
1469
+ }
1470
+
1471
+ override fun onConfigurationChanged(newConfig: Configuration) {
1472
+ val newNightMode = newConfig.uiMode and Configuration.UI_MODE_NIGHT_MASK
1473
+ if (newNightMode != currentNightMode) {
1474
+ currentNightMode = newNightMode
1475
+ applyTheme()
1476
+ }
1477
+ }
1478
+
1479
+ @Deprecated("Deprecated in Java")
1480
+ override fun onLowMemory() {}
1481
+
1482
+ override fun onTrimMemory(level: Int) {}
1483
+
848
1484
  private fun applyTheme() {
849
1485
  val colorScheme = when (theme) {
850
1486
  "dark" -> MapColorScheme.DARK
851
1487
  "light" -> MapColorScheme.LIGHT
852
1488
  else -> MapColorScheme.FOLLOW_SYSTEM
853
1489
  }
854
- googleMap?.setMapColorScheme(colorScheme)
1490
+ googleMap?.mapColorScheme = colorScheme
855
1491
  }
856
1492
 
857
1493
  @SuppressLint("MissingPermission")
@@ -870,5 +1506,9 @@ class GoogleMapProvider(private val context: Context) :
870
1506
 
871
1507
  companion object {
872
1508
  const val DEMO_MAP_ID = "DEMO_MAP_ID"
1509
+
1510
+ // Undocumented cap on the lite mode bitmap (2048 minus a 2px border,
1511
+ // measured empirically)
1512
+ private const val LITE_MODE_MAX_SIZE = 2046
873
1513
  }
874
1514
  }