@@ -49,6 +49,7 @@ import com.luggmaps.extensions.findViewByTag
4949import java.net.URL
5050import kotlin.math.atan
5151import kotlin.math.ln
52+ import kotlin.math.log2
5253import kotlin.math.pow
5354import kotlin.math.sin
5455import kotlin.math.sinh
@@ -1203,6 +1204,17 @@ class GoogleMapProvider(private val context: Context) :
12031204 // region Commands
12041205
12051206 override fun moveCamera (latitude : Double , longitude : Double , zoom : Double , duration : Int ) {
1207+ if (staticMode) {
1208+ // Static maps don't animate; re-center at the final camera
1209+ initialLatitude = latitude
1210+ initialLongitude = longitude
1211+ if (zoom > 0 ) initialZoom = zoom.toFloat()
1212+ val map = googleMap ? : return
1213+ map.moveCamera(CameraUpdateFactory .newLatLngZoom(staticCameraTarget(), initialZoom))
1214+ positionLiveMarkers()
1215+ return
1216+ }
1217+
12061218 val map = googleMap ? : return
12071219 val position = LatLng (latitude, longitude)
12081220 val targetZoom = if (zoom > 0 ) zoom.toFloat() else map.cameraPosition.zoom
@@ -1232,15 +1244,20 @@ class GoogleMapProvider(private val context: Context) :
12321244 val latLongs = coordinates.filterIsInstance<LatLng >()
12331245 if (latLongs.isEmpty()) return
12341246
1235- val boundsBuilder = LatLngBounds .Builder ()
1236- latLongs.forEach { boundsBuilder.include(it) }
1237- val bounds = boundsBuilder.build()
1238-
12391247 val top = edgeInsetsTop.toFloat().dpToPx().toInt()
12401248 val left = edgeInsetsLeft.toFloat().dpToPx().toInt()
12411249 val bottom = edgeInsetsBottom.toFloat().dpToPx().toInt()
12421250 val right = edgeInsetsRight.toFloat().dpToPx().toInt()
12431251
1252+ if (staticMode) {
1253+ fitStaticCoordinates(latLongs, top, left, bottom, right)
1254+ return
1255+ }
1256+
1257+ val boundsBuilder = LatLngBounds .Builder ()
1258+ latLongs.forEach { boundsBuilder.include(it) }
1259+ val bounds = boundsBuilder.build()
1260+
12441261 val combined = combinedEdgeInsets()
12451262 map.setPadding(
12461263 combined.left + left,
@@ -1341,14 +1358,79 @@ class GoogleMapProvider(private val context: Context) :
13411358 val offsetY = (edgeInsets.top - edgeInsets.bottom) / 2.0
13421359 if (offsetX == 0.0 && offsetY == 0.0 ) return LatLng (initialLatitude, initialLongitude)
13431360
1344- // Web mercator world size in px at the initial zoom
1345- val worldSize = 256f .dpToPx().toDouble() * 2.0 .pow(initialZoom.toDouble())
1346- val sinLat = sin(Math .toRadians(initialLatitude))
1347- val x = (initialLongitude + 180.0 ) / 360.0 - offsetX / worldSize
1348- val y = 0.5 - ln((1.0 + sinLat) / (1.0 - sinLat)) / (4.0 * Math .PI ) - offsetY / worldSize
1349- return LatLng (Math .toDegrees(atan(sinh(Math .PI * (1.0 - 2.0 * y)))), x * 360.0 - 180.0 )
1361+ val worldSize = mercatorWorldSize(initialZoom)
1362+ return latLngFromMercator(
1363+ mercatorX(initialLongitude) - offsetX / worldSize,
1364+ mercatorY(initialLatitude) - offsetY / worldSize
1365+ )
1366+ }
1367+
1368+ // A static camera can't fit with map padding (lite mode misrenders it);
1369+ // compute the fitted camera in mercator space instead, shifting the
1370+ // center for asymmetric padding like edge insets
1371+ private fun fitStaticCoordinates (
1372+ coordinates : List <LatLng >,
1373+ top : Int ,
1374+ left : Int ,
1375+ bottom : Int ,
1376+ right : Int
1377+ ) {
1378+ val map = googleMap ? : return
1379+ val wrapper = wrapperView ? : return
1380+ if (wrapper.width == 0 || wrapper.height == 0 ) return
1381+
1382+ var minX = Double .MAX_VALUE
1383+ var minY = Double .MAX_VALUE
1384+ var maxX = - Double .MAX_VALUE
1385+ var maxY = - Double .MAX_VALUE
1386+ for (coordinate in coordinates) {
1387+ val x = mercatorX(coordinate.longitude)
1388+ val y = mercatorY(coordinate.latitude)
1389+ minX = minOf(minX, x)
1390+ maxX = maxOf(maxX, x)
1391+ minY = minOf(minY, y)
1392+ maxY = maxOf(maxY, y)
1393+ }
1394+
1395+ val insets = combinedEdgeInsets()
1396+ val availWidth = (wrapper.width - insets.left - insets.right - left - right).coerceAtLeast(1 )
1397+ val availHeight = (wrapper.height - insets.top - insets.bottom - top - bottom).coerceAtLeast(1 )
1398+
1399+ // World size in px at which the bounds fit the padded viewport;
1400+ // infinite for coincident coordinates - keep the current zoom then
1401+ val fitWorldSize = minOf(availWidth / (maxX - minX), availHeight / (maxY - minY))
1402+ if (fitWorldSize.isFinite()) {
1403+ initialZoom = log2(fitWorldSize / 256f .dpToPx())
1404+ .toFloat()
1405+ .coerceIn(map.minZoomLevel, map.maxZoomLevel)
1406+ }
1407+
1408+ val worldSize = mercatorWorldSize(initialZoom)
1409+ val center = latLngFromMercator(
1410+ (minX + maxX) / 2.0 - (left - right) / 2.0 / worldSize,
1411+ (minY + maxY) / 2.0 - (top - bottom) / 2.0 / worldSize
1412+ )
1413+ initialLatitude = center.latitude
1414+ initialLongitude = center.longitude
1415+
1416+ map.moveCamera(CameraUpdateFactory .newLatLngZoom(staticCameraTarget(), initialZoom))
1417+ positionLiveMarkers()
13501418 }
13511419
1420+ // Web mercator world space in [0, 1]
1421+ private fun mercatorX (longitude : Double ): Double = (longitude + 180.0 ) / 360.0
1422+
1423+ private fun mercatorY (latitude : Double ): Double {
1424+ val sinLat = sin(Math .toRadians(latitude))
1425+ return 0.5 - ln((1.0 + sinLat) / (1.0 - sinLat)) / (4.0 * Math .PI )
1426+ }
1427+
1428+ private fun latLngFromMercator (x : Double , y : Double ): LatLng =
1429+ LatLng (Math .toDegrees(atan(sinh(Math .PI * (1.0 - 2.0 * y)))), x * 360.0 - 180.0 )
1430+
1431+ // World size in px at the given zoom
1432+ private fun mercatorWorldSize (zoom : Float ): Double = 256f .dpToPx().toDouble() * 2.0 .pow(zoom.toDouble())
1433+
13521434 private fun applyWatermarkTranslation (insets : EdgeInsets , duration : Int = 0) {
13531435 val view = mapView ? : return
13541436 view.findViewByTag(" GoogleWatermark" )?.let { watermark ->
0 commit comments