From aa0e509c6bdc12705efbf7919c216af5c407731b Mon Sep 17 00:00:00 2001 From: Prem Singh Date: Sun, 18 Oct 2026 16:57:45 +0400 Subject: [PATCH 1/4] fix(widgets): preserve fractional scale bar distances --- .../maps/android/compose/widgets/ScaleBar.kt | 41 ++++++----- .../compose/widgets/ScaleBarDistanceTest.kt | 69 +++++++++++++++++++ 2 files changed, 92 insertions(+), 18 deletions(-) create mode 100644 maps-compose-widgets/src/test/java/com/google/maps/android/compose/widgets/ScaleBarDistanceTest.kt diff --git a/maps-compose-widgets/src/main/java/com/google/maps/android/compose/widgets/ScaleBar.kt b/maps-compose-widgets/src/main/java/com/google/maps/android/compose/widgets/ScaleBar.kt index f1071bdb..3914ba97 100644 --- a/maps-compose-widgets/src/main/java/com/google/maps/android/compose/widgets/ScaleBar.kt +++ b/maps-compose-widgets/src/main/java/com/google/maps/android/compose/widgets/ScaleBar.kt @@ -51,6 +51,7 @@ import com.google.android.gms.maps.Projection import com.google.maps.android.compose.CameraPositionState import com.google.maps.android.sphericalDistance import kotlinx.coroutines.delay +import java.text.NumberFormat internal fun calculateDistance(projection: Projection, width: Dp, density: Density): Int { val widthInPixels = with(density) { @@ -180,40 +181,44 @@ public fun ScaleBar( // Here, we determine the appropriate units (meters/kilometers and feet/miles) // based on the calculated distance in meters. - var metricUnits = "m" - var metricDistance = horizontalLineWidthMeters - if (horizontalLineWidthMeters > METERS_IN_KILOMETER) { - // Switch from meters to kilometers as unit - metricUnits = "km" - metricDistance /= METERS_IN_KILOMETER.toInt() - } - - var imperialUnits = "ft" - var imperialDistance = horizontalLineWidthMeters.toDouble().toFeet() - if (imperialDistance > FEET_IN_MILE) { - // Switch from ft to miles as unit - imperialUnits = "mi" - imperialDistance = imperialDistance.toMiles() - } - // We display the calculated distances in two Text composables, one for imperial // and one for metric units. ScaleText( modifier = Modifier.align(End), textColor = textColor, shadowColor = shadowColor, - text = "${imperialDistance.toInt()} $imperialUnits", + text = formatImperialDistance(horizontalLineWidthMeters), ) ScaleText( modifier = Modifier.align(End), textColor = textColor, shadowColor = shadowColor, - text = "$metricDistance $metricUnits", + text = formatMetricDistance(horizontalLineWidthMeters), ) } } } +internal fun formatMetricDistance(meters: Int): String = if (meters >= METERS_IN_KILOMETER) { + "${formatScaleDistance(meters / METERS_IN_KILOMETER)} km" +} else { + "$meters m" +} + +internal fun formatImperialDistance(meters: Int): String { + val feet = meters.toDouble().toFeet() + return if (feet >= FEET_IN_MILE) { + "${formatScaleDistance(feet.toMiles())} mi" + } else { + "${feet.toInt()} ft" + } +} + +private fun formatScaleDistance(distance: Double): String = NumberFormat.getNumberInstance().apply { + maximumFractionDigits = 2 + isGroupingUsed = false +}.format(distance) + /** * An animated scale bar that appears when the zoom level of the map changes, and then disappears * after [visibilityDurationMillis]. This composable wraps [ScaleBar] with visibility animations. diff --git a/maps-compose-widgets/src/test/java/com/google/maps/android/compose/widgets/ScaleBarDistanceTest.kt b/maps-compose-widgets/src/test/java/com/google/maps/android/compose/widgets/ScaleBarDistanceTest.kt new file mode 100644 index 00000000..bdbc02d6 --- /dev/null +++ b/maps-compose-widgets/src/test/java/com/google/maps/android/compose/widgets/ScaleBarDistanceTest.kt @@ -0,0 +1,69 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.maps.android.compose.widgets + +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import java.util.Locale + +internal class ScaleBarDistanceTest { + private val originalLocale = Locale.getDefault() + + @Before + fun setUp() { + Locale.setDefault(Locale.US) + } + + @After + fun tearDown() { + Locale.setDefault(originalLocale) + } + + @Test + fun metricDistancePreservesFractionalKilometers() { + assertEquals("1.9 km", formatMetricDistance(1900)) + assertEquals("1.01 km", formatMetricDistance(1010)) + } + + @Test + fun imperialDistancePreservesFractionalMiles() { + assertEquals("1.24 mi", formatImperialDistance(2000)) + assertEquals("1.99 mi", formatImperialDistance(3200)) + } + + @Test + fun metricDistanceUsesKilometersAtUnitBoundary() { + assertEquals("999 m", formatMetricDistance(999)) + assertEquals("1 km", formatMetricDistance(1000)) + assertEquals("2 km", formatMetricDistance(2000)) + } + + @Test + fun smallDistancesKeepWholeMetersAndFeet() { + assertEquals("0 m", formatMetricDistance(0)) + assertEquals("0 ft", formatImperialDistance(0)) + assertEquals("100 m", formatMetricDistance(100)) + assertEquals("328 ft", formatImperialDistance(100)) + } + + @Test + fun fractionalDistancesUseLocaleDecimalSeparator() { + Locale.setDefault(Locale.GERMANY) + assertEquals("1,9 km", formatMetricDistance(1900)) + assertEquals("1,24 mi", formatImperialDistance(2000)) + } +} From ddeefca75be1b61c95656889a98b54a1442ad73b Mon Sep 17 00:00:00 2001 From: Prem Singh Date: Sun, 18 Oct 2026 22:12:26 +0400 Subject: [PATCH 2/4] test(widgets): verify fractional labels on an Android emulator --- .../android/compose/ScaleBarRenderingTests.kt | 96 +++++++++++++++++++ .../maps/android/compose/ScaleBarTests.kt | 44 +++++++-- 2 files changed, 132 insertions(+), 8 deletions(-) create mode 100644 maps-app/src/androidTest/java/com/google/maps/android/compose/ScaleBarRenderingTests.kt diff --git a/maps-app/src/androidTest/java/com/google/maps/android/compose/ScaleBarRenderingTests.kt b/maps-app/src/androidTest/java/com/google/maps/android/compose/ScaleBarRenderingTests.kt new file mode 100644 index 00000000..d80c977c --- /dev/null +++ b/maps-app/src/androidTest/java/com/google/maps/android/compose/ScaleBarRenderingTests.kt @@ -0,0 +1,96 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.maps.android.compose + +import android.graphics.Bitmap +import android.graphics.Point +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.material.MaterialTheme +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.asAndroidBitmap +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.captureToImage +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.unit.dp +import androidx.test.platform.app.InstrumentationRegistry +import com.google.android.gms.maps.Projection +import com.google.android.gms.maps.model.CameraPosition +import com.google.android.gms.maps.model.LatLng +import com.google.maps.android.compose.widgets.ScaleBar +import io.mockk.every +import io.mockk.mockk +import org.junit.Rule +import org.junit.Test +import java.io.File +import java.util.Locale + +class ScaleBarRenderingTests { + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun fractionalLabelsRenderOnLightBackground() { + assertFractionalLabels(dark = false) + } + + @Test + fun fractionalLabelsRenderOnDarkBackground() { + assertFractionalLabels(dark = true) + } + + private fun assertFractionalLabels(dark: Boolean) { + val originalLocale = Locale.getDefault() + Locale.setDefault(Locale.US) + try { + val projection = mockk() + every { projection.fromScreenLocation(any()) } answers { + // At the equator this spans about 2,224 m, or 1,976 m for the 8/9-width bar. + if (firstArg().x == 0) LatLng(0.0, 0.0) else LatLng(0.0, 0.02) + } + val camera = mockk() + every { camera.position } returns CameraPosition.fromLatLngZoom(LatLng(0.0, 0.0), 12f) + every { camera.projection } returns projection + val background = if (dark) Color.Black else Color.White + val foreground = if (dark) Color.White else Color.Black + composeTestRule.setContent { + MaterialTheme { + Box(Modifier.testTag("scale-card").background(background).padding(16.dp)) { + ScaleBar( + cameraPositionState = camera, + textColor = foreground, + lineColor = foreground, + shadowColor = background, + ) + } + } + } + composeTestRule.onNodeWithText("1.98 km").assertIsDisplayed() + composeTestRule.onNodeWithText("1.23 mi").assertIsDisplayed() + + val context = InstrumentationRegistry.getInstrumentation().targetContext + val image = composeTestRule.onNodeWithTag("scale-card").captureToImage().asAndroidBitmap() + val output = File(context.getExternalFilesDir(null), "scale-bar-${if (dark) "dark" else "light"}.png") + output.outputStream().use { image.compress(Bitmap.CompressFormat.PNG, 100, it) } + } finally { + Locale.setDefault(originalLocale) + } + } +} diff --git a/maps-app/src/androidTest/java/com/google/maps/android/compose/ScaleBarTests.kt b/maps-app/src/androidTest/java/com/google/maps/android/compose/ScaleBarTests.kt index 99de1243..f0271285 100644 --- a/maps-app/src/androidTest/java/com/google/maps/android/compose/ScaleBarTests.kt +++ b/maps-app/src/androidTest/java/com/google/maps/android/compose/ScaleBarTests.kt @@ -29,6 +29,7 @@ import org.junit.Assert.assertTrue import org.junit.Assert.fail import org.junit.Rule import org.junit.Test +import java.text.NumberFormat import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit @@ -75,7 +76,20 @@ class ScaleBarTests { @Test fun testScaleBarInitialState() { - val initialZoom = 15f + assertScaleBarLabels(initialZoom = 15f) + } + + @Test + fun testScaleBarFractionalKilometers() { + assertScaleBarLabels(initialZoom = 12f) + } + + @Test + fun testScaleBarFractionalMiles() { + assertScaleBarLabels(initialZoom = 10f) + } + + private fun assertScaleBarLabels(initialZoom: Float) { val initialPosition = LatLng(37.7749, -122.4194) // San Francisco initScaleBar(initialZoom, initialPosition) @@ -96,25 +110,39 @@ class ScaleBarTests { } val upperLeftLatLng = proj.fromScreenLocation(Point(0, 0)) - val upperRightLatLng = proj.fromScreenLocation(Point(0, widthInPixels)) + val upperRightLatLng = proj.fromScreenLocation(Point(widthInPixels, 0)) val canvasWidthMeters = upperLeftLatLng.sphericalDistance(upperRightLatLng) val horizontalLineWidthMeters = (canvasWidthMeters * 8 / 9).toInt() var metricUnits = "m" - var metricDistance = horizontalLineWidthMeters - if (horizontalLineWidthMeters > METERS_IN_KILOMETER) { + var metricDistance = horizontalLineWidthMeters.toDouble() + if (horizontalLineWidthMeters >= METERS_IN_KILOMETER) { metricUnits = "km" - metricDistance /= METERS_IN_KILOMETER.toInt() + metricDistance /= METERS_IN_KILOMETER } var imperialUnits = "ft" var imperialDistance = horizontalLineWidthMeters.toDouble().toFeet() - if (imperialDistance > FEET_IN_MILE) { + if (imperialDistance >= FEET_IN_MILE) { imperialUnits = "mi" imperialDistance = imperialDistance.toMiles() } - imperialText = "${imperialDistance.toInt()} $imperialUnits" - metricText = "$metricDistance $metricUnits" + val numberFormat = NumberFormat.getNumberInstance().apply { + maximumFractionDigits = 2 + isGroupingUsed = false + } + val imperialValue = if (imperialUnits == "ft") { + imperialDistance.toInt().toString() + } else { + numberFormat.format(imperialDistance) + } + val metricValue = if (metricUnits == "m") { + metricDistance.toInt().toString() + } else { + numberFormat.format(metricDistance) + } + imperialText = "$imperialValue $imperialUnits" + metricText = "$metricValue $metricUnits" } ?: fail("Projection should not be null") } From 064e3b53d5ecd374d4dbf5a6514ab95aff07f547 Mon Sep 17 00:00:00 2001 From: Prem Singh Date: Fri, 23 Oct 2026 02:49:53 +0400 Subject: [PATCH 3/4] fix(widgets): address scale bar review feedback --- .../android/compose/ScaleBarRenderingTests.kt | 73 ++++++++----------- .../maps/android/compose/ScaleBarTests.kt | 46 +++++------- .../maps/android/compose/widgets/ScaleBar.kt | 24 +++--- .../compose/widgets/ScaleBarDistanceTest.kt | 33 ++++----- 4 files changed, 81 insertions(+), 95 deletions(-) diff --git a/maps-app/src/androidTest/java/com/google/maps/android/compose/ScaleBarRenderingTests.kt b/maps-app/src/androidTest/java/com/google/maps/android/compose/ScaleBarRenderingTests.kt index d80c977c..47a7e2e8 100644 --- a/maps-app/src/androidTest/java/com/google/maps/android/compose/ScaleBarRenderingTests.kt +++ b/maps-app/src/androidTest/java/com/google/maps/android/compose/ScaleBarRenderingTests.kt @@ -14,7 +14,6 @@ package com.google.maps.android.compose -import android.graphics.Bitmap import android.graphics.Point import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box @@ -22,15 +21,10 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material.MaterialTheme import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.asAndroidBitmap -import androidx.compose.ui.platform.testTag import androidx.compose.ui.test.assertIsDisplayed -import androidx.compose.ui.test.captureToImage import androidx.compose.ui.test.junit4.createComposeRule -import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.unit.dp -import androidx.test.platform.app.InstrumentationRegistry import com.google.android.gms.maps.Projection import com.google.android.gms.maps.model.CameraPosition import com.google.android.gms.maps.model.LatLng @@ -39,8 +33,6 @@ import io.mockk.every import io.mockk.mockk import org.junit.Rule import org.junit.Test -import java.io.File -import java.util.Locale class ScaleBarRenderingTests { @get:Rule @@ -56,41 +48,40 @@ class ScaleBarRenderingTests { assertFractionalLabels(dark = true) } - private fun assertFractionalLabels(dark: Boolean) { - val originalLocale = Locale.getDefault() - Locale.setDefault(Locale.US) - try { - val projection = mockk() - every { projection.fromScreenLocation(any()) } answers { - // At the equator this spans about 2,224 m, or 1,976 m for the 8/9-width bar. - if (firstArg().x == 0) LatLng(0.0, 0.0) else LatLng(0.0, 0.02) - } - val camera = mockk() - every { camera.position } returns CameraPosition.fromLatLngZoom(LatLng(0.0, 0.0), 12f) - every { camera.projection } returns projection - val background = if (dark) Color.Black else Color.White - val foreground = if (dark) Color.White else Color.Black - composeTestRule.setContent { - MaterialTheme { - Box(Modifier.testTag("scale-card").background(background).padding(16.dp)) { - ScaleBar( - cameraPositionState = camera, - textColor = foreground, - lineColor = foreground, - shadowColor = background, - ) - } + @Test + fun largeLabelsRenderOnLightBackground() { + assertFractionalLabels(dark = false, large = true) + } + + @Test + fun largeLabelsRenderOnDarkBackground() { + assertFractionalLabels(dark = true, large = true) + } + + private fun assertFractionalLabels(dark: Boolean, large: Boolean = false) { + val projection = mockk() + every { projection.fromScreenLocation(any()) } answers { + // At the equator 0.02 degrees spans about 2,224 m, or 1,976 m for the 8/9-width bar. + if (firstArg().x == 0) LatLng(0.0, 0.0) else LatLng(0.0, if (large) 20.0 else 0.02) + } + val camera = mockk() + every { camera.position } returns CameraPosition.fromLatLngZoom(LatLng(0.0, 0.0), 12f) + every { camera.projection } returns projection + val background = if (dark) Color.Black else Color.White + val foreground = if (dark) Color.White else Color.Black + composeTestRule.setContent { + MaterialTheme { + Box(Modifier.background(background).padding(16.dp)) { + ScaleBar( + cameraPositionState = camera, + textColor = foreground, + lineColor = foreground, + shadowColor = background, + ) } } - composeTestRule.onNodeWithText("1.98 km").assertIsDisplayed() - composeTestRule.onNodeWithText("1.23 mi").assertIsDisplayed() - - val context = InstrumentationRegistry.getInstrumentation().targetContext - val image = composeTestRule.onNodeWithTag("scale-card").captureToImage().asAndroidBitmap() - val output = File(context.getExternalFilesDir(null), "scale-bar-${if (dark) "dark" else "light"}.png") - output.outputStream().use { image.compress(Bitmap.CompressFormat.PNG, 100, it) } - } finally { - Locale.setDefault(originalLocale) } + composeTestRule.onNodeWithText(if (large) "1977 km" else "1.98 km").assertIsDisplayed() + composeTestRule.onNodeWithText(if (large) "1228 mi" else "1.23 mi").assertIsDisplayed() } } diff --git a/maps-app/src/androidTest/java/com/google/maps/android/compose/ScaleBarTests.kt b/maps-app/src/androidTest/java/com/google/maps/android/compose/ScaleBarTests.kt index f0271285..60524393 100644 --- a/maps-app/src/androidTest/java/com/google/maps/android/compose/ScaleBarTests.kt +++ b/maps-app/src/androidTest/java/com/google/maps/android/compose/ScaleBarTests.kt @@ -29,9 +29,9 @@ import org.junit.Assert.assertTrue import org.junit.Assert.fail import org.junit.Rule import org.junit.Test -import java.text.NumberFormat import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit +import kotlin.math.roundToInt // These constants are used for converting between metric and imperial units // to ensure the scale bar displays distances correctly in both systems. @@ -114,35 +114,17 @@ class ScaleBarTests { val canvasWidthMeters = upperLeftLatLng.sphericalDistance(upperRightLatLng) val horizontalLineWidthMeters = (canvasWidthMeters * 8 / 9).toInt() - var metricUnits = "m" - var metricDistance = horizontalLineWidthMeters.toDouble() - if (horizontalLineWidthMeters >= METERS_IN_KILOMETER) { - metricUnits = "km" - metricDistance /= METERS_IN_KILOMETER - } - - var imperialUnits = "ft" - var imperialDistance = horizontalLineWidthMeters.toDouble().toFeet() - if (imperialDistance >= FEET_IN_MILE) { - imperialUnits = "mi" - imperialDistance = imperialDistance.toMiles() - } - val numberFormat = NumberFormat.getNumberInstance().apply { - maximumFractionDigits = 2 - isGroupingUsed = false - } - val imperialValue = if (imperialUnits == "ft") { - imperialDistance.toInt().toString() + metricText = if (horizontalLineWidthMeters >= METERS_IN_KILOMETER) { + "${formatTestDistance(horizontalLineWidthMeters / METERS_IN_KILOMETER)} km" } else { - numberFormat.format(imperialDistance) + "$horizontalLineWidthMeters m" } - val metricValue = if (metricUnits == "m") { - metricDistance.toInt().toString() + val feet = horizontalLineWidthMeters.toDouble().toFeet() + imperialText = if (feet >= FEET_IN_MILE) { + "${formatTestDistance(feet.toMiles())} mi" } else { - numberFormat.format(metricDistance) + "${feet.roundToInt()} ft" } - imperialText = "$imperialValue $imperialUnits" - metricText = "$metricValue $metricUnits" } ?: fail("Projection should not be null") } @@ -158,3 +140,15 @@ class ScaleBarTests { internal fun Double.toFeet(): Double = this * CENTIMETERS_IN_METER / CENTIMETERS_IN_INCH / INCHES_IN_FOOT internal fun Double.toMiles(): Double = this / FEET_IN_MILE + +private fun formatTestDistance(distance: Double): String { + if (distance >= 10.0) return distance.roundToInt().toString() + val scaled = (distance * 100.0).roundToInt() + val whole = scaled / 100 + val remainder = scaled % 100 + return when { + remainder == 0 -> "$whole" + remainder % 10 == 0 -> "$whole.${remainder / 10}" + else -> "$whole.${remainder.toString().padStart(2, '0')}" + } +} diff --git a/maps-compose-widgets/src/main/java/com/google/maps/android/compose/widgets/ScaleBar.kt b/maps-compose-widgets/src/main/java/com/google/maps/android/compose/widgets/ScaleBar.kt index 3914ba97..bc67eb5c 100644 --- a/maps-compose-widgets/src/main/java/com/google/maps/android/compose/widgets/ScaleBar.kt +++ b/maps-compose-widgets/src/main/java/com/google/maps/android/compose/widgets/ScaleBar.kt @@ -51,7 +51,7 @@ import com.google.android.gms.maps.Projection import com.google.maps.android.compose.CameraPositionState import com.google.maps.android.sphericalDistance import kotlinx.coroutines.delay -import java.text.NumberFormat +import kotlin.math.roundToInt internal fun calculateDistance(projection: Projection, width: Dp, density: Density): Int { val widthInPixels = with(density) { @@ -178,9 +178,6 @@ public fun ScaleBar( modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.SpaceAround, ) { - // Here, we determine the appropriate units (meters/kilometers and feet/miles) - // based on the calculated distance in meters. - // We display the calculated distances in two Text composables, one for imperial // and one for metric units. ScaleText( @@ -210,14 +207,23 @@ internal fun formatImperialDistance(meters: Int): String { return if (feet >= FEET_IN_MILE) { "${formatScaleDistance(feet.toMiles())} mi" } else { - "${feet.toInt()} ft" + "${feet.roundToInt()} ft" } } -private fun formatScaleDistance(distance: Double): String = NumberFormat.getNumberInstance().apply { - maximumFractionDigits = 2 - isGroupingUsed = false -}.format(distance) +private fun formatScaleDistance(distance: Double): String { + if (distance >= 10.0) { + return distance.roundToInt().toString() + } + val scaled = (distance * 100.0).roundToInt() + val whole = scaled / 100 + val remainder = scaled % 100 + return when { + remainder == 0 -> "$whole" + remainder % 10 == 0 -> "$whole.${remainder / 10}" + else -> "$whole.${remainder.toString().padStart(2, '0')}" + } +} /** * An animated scale bar that appears when the zoom level of the map changes, and then disappears diff --git a/maps-compose-widgets/src/test/java/com/google/maps/android/compose/widgets/ScaleBarDistanceTest.kt b/maps-compose-widgets/src/test/java/com/google/maps/android/compose/widgets/ScaleBarDistanceTest.kt index bdbc02d6..75922cf4 100644 --- a/maps-compose-widgets/src/test/java/com/google/maps/android/compose/widgets/ScaleBarDistanceTest.kt +++ b/maps-compose-widgets/src/test/java/com/google/maps/android/compose/widgets/ScaleBarDistanceTest.kt @@ -14,25 +14,10 @@ package com.google.maps.android.compose.widgets -import org.junit.After import org.junit.Assert.assertEquals -import org.junit.Before import org.junit.Test -import java.util.Locale internal class ScaleBarDistanceTest { - private val originalLocale = Locale.getDefault() - - @Before - fun setUp() { - Locale.setDefault(Locale.US) - } - - @After - fun tearDown() { - Locale.setDefault(originalLocale) - } - @Test fun metricDistancePreservesFractionalKilometers() { assertEquals("1.9 km", formatMetricDistance(1900)) @@ -61,9 +46,19 @@ internal class ScaleBarDistanceTest { } @Test - fun fractionalDistancesUseLocaleDecimalSeparator() { - Locale.setDefault(Locale.GERMANY) - assertEquals("1,9 km", formatMetricDistance(1900)) - assertEquals("1,24 mi", formatImperialDistance(2000)) + fun largeDistancesRoundToWholeUnits() { + assertEquals("10 km", formatMetricDistance(10_400)) + assertEquals("12 km", formatMetricDistance(11_600)) + assertEquals("10 mi", formatImperialDistance(16_100)) + assertEquals("3457 km", formatMetricDistance(3_456_780)) + } + + @Test + fun fractionalDistancesTrimZerosAndRoundAtBoundaries() { + assertEquals("1.05 km", formatMetricDistance(1050)) + assertEquals("10 km", formatMetricDistance(9999)) + assertEquals("9.99 km", formatMetricDistance(9990)) + assertEquals("3 ft", formatImperialDistance(1)) + assertEquals("7 ft", formatImperialDistance(2)) } } From ba81967340e442b8195d878d4a4308f79a1c8ef0 Mon Sep 17 00:00:00 2001 From: Prem Singh Date: Wed, 28 Oct 2026 19:46:05 +0400 Subject: [PATCH 4/4] fix(widgets): limit scale bar labels to one decimal --- .../maps/android/compose/ScaleBarRenderingTests.kt | 4 ++-- .../google/maps/android/compose/ScaleBarTests.kt | 10 ++-------- .../google/maps/android/compose/widgets/ScaleBar.kt | 10 ++-------- .../android/compose/widgets/ScaleBarDistanceTest.kt | 13 ++++++++----- 4 files changed, 14 insertions(+), 23 deletions(-) diff --git a/maps-app/src/androidTest/java/com/google/maps/android/compose/ScaleBarRenderingTests.kt b/maps-app/src/androidTest/java/com/google/maps/android/compose/ScaleBarRenderingTests.kt index 47a7e2e8..7f2bb02b 100644 --- a/maps-app/src/androidTest/java/com/google/maps/android/compose/ScaleBarRenderingTests.kt +++ b/maps-app/src/androidTest/java/com/google/maps/android/compose/ScaleBarRenderingTests.kt @@ -81,7 +81,7 @@ class ScaleBarRenderingTests { } } } - composeTestRule.onNodeWithText(if (large) "1977 km" else "1.98 km").assertIsDisplayed() - composeTestRule.onNodeWithText(if (large) "1228 mi" else "1.23 mi").assertIsDisplayed() + composeTestRule.onNodeWithText(if (large) "1977 km" else "2 km").assertIsDisplayed() + composeTestRule.onNodeWithText(if (large) "1228 mi" else "1.2 mi").assertIsDisplayed() } } diff --git a/maps-app/src/androidTest/java/com/google/maps/android/compose/ScaleBarTests.kt b/maps-app/src/androidTest/java/com/google/maps/android/compose/ScaleBarTests.kt index 60524393..3be09312 100644 --- a/maps-app/src/androidTest/java/com/google/maps/android/compose/ScaleBarTests.kt +++ b/maps-app/src/androidTest/java/com/google/maps/android/compose/ScaleBarTests.kt @@ -143,12 +143,6 @@ internal fun Double.toMiles(): Double = this / FEET_IN_MILE private fun formatTestDistance(distance: Double): String { if (distance >= 10.0) return distance.roundToInt().toString() - val scaled = (distance * 100.0).roundToInt() - val whole = scaled / 100 - val remainder = scaled % 100 - return when { - remainder == 0 -> "$whole" - remainder % 10 == 0 -> "$whole.${remainder / 10}" - else -> "$whole.${remainder.toString().padStart(2, '0')}" - } + val scaled = (distance * 10.0).roundToInt() + return if (scaled % 10 == 0) "${scaled / 10}" else "${scaled / 10}.${scaled % 10}" } diff --git a/maps-compose-widgets/src/main/java/com/google/maps/android/compose/widgets/ScaleBar.kt b/maps-compose-widgets/src/main/java/com/google/maps/android/compose/widgets/ScaleBar.kt index bc67eb5c..428ecd2b 100644 --- a/maps-compose-widgets/src/main/java/com/google/maps/android/compose/widgets/ScaleBar.kt +++ b/maps-compose-widgets/src/main/java/com/google/maps/android/compose/widgets/ScaleBar.kt @@ -215,14 +215,8 @@ private fun formatScaleDistance(distance: Double): String { if (distance >= 10.0) { return distance.roundToInt().toString() } - val scaled = (distance * 100.0).roundToInt() - val whole = scaled / 100 - val remainder = scaled % 100 - return when { - remainder == 0 -> "$whole" - remainder % 10 == 0 -> "$whole.${remainder / 10}" - else -> "$whole.${remainder.toString().padStart(2, '0')}" - } + val scaled = (distance * 10.0).roundToInt() + return if (scaled % 10 == 0) "${scaled / 10}" else "${scaled / 10}.${scaled % 10}" } /** diff --git a/maps-compose-widgets/src/test/java/com/google/maps/android/compose/widgets/ScaleBarDistanceTest.kt b/maps-compose-widgets/src/test/java/com/google/maps/android/compose/widgets/ScaleBarDistanceTest.kt index 75922cf4..52b635e7 100644 --- a/maps-compose-widgets/src/test/java/com/google/maps/android/compose/widgets/ScaleBarDistanceTest.kt +++ b/maps-compose-widgets/src/test/java/com/google/maps/android/compose/widgets/ScaleBarDistanceTest.kt @@ -21,13 +21,13 @@ internal class ScaleBarDistanceTest { @Test fun metricDistancePreservesFractionalKilometers() { assertEquals("1.9 km", formatMetricDistance(1900)) - assertEquals("1.01 km", formatMetricDistance(1010)) + assertEquals("1 km", formatMetricDistance(1010)) } @Test fun imperialDistancePreservesFractionalMiles() { - assertEquals("1.24 mi", formatImperialDistance(2000)) - assertEquals("1.99 mi", formatImperialDistance(3200)) + assertEquals("1.2 mi", formatImperialDistance(2000)) + assertEquals("2 mi", formatImperialDistance(3200)) } @Test @@ -55,9 +55,12 @@ internal class ScaleBarDistanceTest { @Test fun fractionalDistancesTrimZerosAndRoundAtBoundaries() { - assertEquals("1.05 km", formatMetricDistance(1050)) + assertEquals("1 km", formatMetricDistance(1049)) + assertEquals("1.1 km", formatMetricDistance(1050)) + assertEquals("9.9 km", formatMetricDistance(9949)) + assertEquals("10 km", formatMetricDistance(9950)) assertEquals("10 km", formatMetricDistance(9999)) - assertEquals("9.99 km", formatMetricDistance(9990)) + assertEquals("10 km", formatMetricDistance(9990)) assertEquals("3 ft", formatImperialDistance(1)) assertEquals("7 ft", formatImperialDistance(2)) }