diff --git a/android/src/main/java/com/rnmapbox/rnmbx/components/camera/CameraStop.kt b/android/src/main/java/com/rnmapbox/rnmbx/components/camera/CameraStop.kt index 5e6a23385..7e1078bda 100644 --- a/android/src/main/java/com/rnmapbox/rnmbx/components/camera/CameraStop.kt +++ b/android/src/main/java/com/rnmapbox/rnmbx/components/camera/CameraStop.kt @@ -130,7 +130,7 @@ class CameraStop { // cameraForCoordinateBounds insets the bounds within the map's current padding, so omitted edges must add nothing. val boundsPadding = intArrayOf(mPaddingLeft ?: 0, mPaddingTop ?: 0, mPaddingRight ?: 0, mPaddingBottom ?: 0) val boundsCamera = map.cameraForCoordinateBounds( - mBounds!!.toBounds(), + mBounds!!.toCameraBounds(), convert(clippedPadding(boundsPadding, mapView)), bearing, tilt diff --git a/android/src/main/java/com/rnmapbox/rnmbx/utils/LatLngBounds.kt b/android/src/main/java/com/rnmapbox/rnmbx/utils/LatLngBounds.kt index 08f612b51..dca85a2fb 100644 --- a/android/src/main/java/com/rnmapbox/rnmbx/utils/LatLngBounds.kt +++ b/android/src/main/java/com/rnmapbox/rnmbx/utils/LatLngBounds.kt @@ -2,7 +2,6 @@ package com.rnmapbox.rnmbx.utils import com.mapbox.geojson.Point import com.mapbox.maps.CoordinateBounds -import com.rnmapbox.rnmbx.utils.LatLngBounds class LatLngBounds internal constructor( var latNorth: Double, @@ -27,9 +26,24 @@ class LatLngBounds internal constructor( ) } + fun toCameraBounds(): CoordinateBounds { + if (latNorth != latSouth) { + return toBounds() + } + + // Mapbox returns zoom 0 when fitting zero-height bounds (#4050). + // Expand only the fitting bounds, preserving the center and exact camera constraints. + val latitudePadding = 0.000001 + return CoordinateBounds( + Point.fromLngLat(lonWest, (latSouth - latitudePadding).coerceAtLeast(-90.0)), + Point.fromLngLat(lonEast, (latNorth + latitudePadding).coerceAtMost(90.0)), + false + ) + } + companion object { fun from(bbox: Double, bbox1: Double, bbox2: Double, bbox3: Double): LatLngBounds { return LatLngBounds(bbox, bbox1, bbox2, bbox3) } } -} \ No newline at end of file +} diff --git a/android/src/test/kotlin/com/rnmapbox/rnmbx/utils/LatLngBoundsTest.kt b/android/src/test/kotlin/com/rnmapbox/rnmbx/utils/LatLngBoundsTest.kt new file mode 100644 index 000000000..970f098cb --- /dev/null +++ b/android/src/test/kotlin/com/rnmapbox/rnmbx/utils/LatLngBoundsTest.kt @@ -0,0 +1,42 @@ +package com.rnmapbox.rnmbx.utils + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test + +class LatLngBoundsTest { + @Test + fun `horizontal camera bounds enclose the original line without shifting its center`() { + val bounds = LatLngBounds.from(37.0, -121.2, 37.0, -122.1) + val cameraBounds = bounds.toCameraBounds() + + assertTrue(cameraBounds.southwest.latitude() < 37.0) + assertTrue(cameraBounds.northeast.latitude() > 37.0) + assertEquals(37.0, (cameraBounds.southwest.latitude() + cameraBounds.northeast.latitude()) / 2, 1e-12) + assertEquals(-122.1, cameraBounds.southwest.longitude()) + assertEquals(-121.2, cameraBounds.northeast.longitude()) + + // Camera constraints must still use the exact bounds after a fit. + assertEquals(37.0, bounds.toBounds().southwest.latitude()) + assertEquals(37.0, bounds.toBounds().northeast.latitude()) + } + + @Test + fun `vertical and very thin nonzero bounds retain their exact coordinates`() { + for (bounds in listOf( + LatLngBounds.from(37.1, -121.1, 37.0, -121.1), + LatLngBounds.from(37.00000001, -121.2, 37.0, -122.1) + )) { + assertEquals(bounds.toBounds(), bounds.toCameraBounds()) + } + } + + @Test + fun `expanding a horizontal fit never exceeds valid latitudes`() { + for (latitude in listOf(-90.0, 90.0)) { + val cameraBounds = LatLngBounds.from(latitude, 1.0, latitude, -1.0).toCameraBounds() + assertTrue(cameraBounds.southwest.latitude() >= -90.0) + assertTrue(cameraBounds.northeast.latitude() <= 90.0) + assertTrue(cameraBounds.southwest.latitude() < cameraBounds.northeast.latitude()) + } + } +}