Skip to content

fix(widgets): preserve fractional scale bar distances - #1045

Merged
dkhawk merged 9 commits into
googlemaps:mainfrom
premsinghsodha7:fix/scale-bar-distance-precision
Oct 30, 2026
Merged

dkhawk merged 9 commits into
googlemaps:mainfrom
premsinghsodha7:fix/scale-bar-distance-precision

Conversation

@premsinghsodha7

@premsinghsodha7 premsinghsodha7 commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Refs #233.

ScaleBar truncates kilometre and mile labels: 1,900 m becomes 1 km, and 3,200 m becomes 1 mi. Preserve up to one decimal place below 10 km/mi, and round larger distances to whole units to keep labels compact at the default 65.dp width. Exactly 1,000 m uses 1 km; sub-mile feet round to whole feet.

This corrects the displayed labels. Sizing the bar to round lengths remains a follow-up under #233, so that issue should stay open. Formatting uses pure Kotlin math and strings, without Java formatting or locale mutation. No new library dependencies or public API changes. Production drawing and projection calculation are unchanged.

Controlled-projection rendering tests run with Robolectric on API 29 and 35. Device rendering tests use a real, unbound CameraPositionState, avoiding the MockK Android agent reflection crash on API 29 (PictureInPictureUiState is unavailable there). Both test files use Material 3 themes and perform no bitmap capture, file writes, or locale mutation. Host-test resources and the Compose test activity are configured in maps-app; a placeholder key fallback supports manifest merging while configured app keys retain Secrets-plugin precedence.

Validation:

  • Full CI build and unit coverage command passed: 57 unit tests, including all six widget tests and eight rendering cases across API 29/35.
  • The CI instrumentation command, createDebugCoverageReport :maps-app:createLibraryCoverageReports, passed on an Android 10/API 29 emulator, including the two key-free device rendering tests. Live-key cases use the existing assumptions.
  • Lint, spotlessCheck, git diff --check, debug/release APK assembly, and validateDebugScreenshotTest passed. Lint reports no findings in either new rendering-test file.
  • Live-map tests retain the corrected horizontal projection coordinates and pure Kotlin expected-label formatting. Live-map assertions remain unverified without a Maps API key.

An extra API 29 demo-smoke run hits an AdvancedMarkersActivity / BitmapDescriptorFactory.fromPinConfig failure, also reproduced on unchanged commit 58ea02c. The demo-smoke CI workflow runs on API 36.

@dkhawk dkhawk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you so much for this contribution, @premsinghsodha7! We really appreciate the care you put into fixing the scale bar truncation issue (#233), adding unit and rendering tests, and catching the swapped x/y screen coordinate bug (Point(0, widthInPixels) -> Point(widthInPixels, 0)) in ScaleBarTests.kt!

We are actively working on making android-maps-compose Kotlin Multiplatform (KMP) friendly, so our main feedback is around avoiding java.* imports (java.text.NumberFormat, java.util.Locale, java.io.File) in favor of pure Kotlin stdlib / kotlin.math, along with a few small layout and test cleanups.

I've left a few inline comments with verified code suggestions below. Thanks again for helping improve the library, and please let us know if you have any questions!

(Note: This review and its code suggestions were prepared with the assistance of AI and verified locally against the test suite.)

Comment on lines 181 to 182
// Here, we determine the appropriate units (meters/kilometers and feet/miles)
// based on the calculated distance in meters.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Now that the unit selection logic has been extracted into formatImperialDistance and formatMetricDistance, this comment (// Here, we determine the appropriate units...) is slightly orphaned right above the ScaleText calls. Feel free to either remove it or move it into KDoc on the new helper functions!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the orphaned unit-selection comment. The formatting helpers now own that logic.

Comment on lines +204 to 222
} 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)

/**

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we are working toward making this codebase Kotlin Multiplatform (KMP) friendly, we'd love to avoid introducing java.text.NumberFormat (and java.* imports in general) into :maps-compose-widgets, which currently has zero java.* imports.

Also, when zoomed far out (e.g., continental/world zoom levels where the bar spans hundreds or thousands of kilometers/miles), formatting with 2 decimal places produces long labels like "3456.78 km" (10 characters) that can wrap or clip inside the default 65.dp width. For distances >= 10 km/mi, rounding to a whole number keeps the label compact and readable.

We can handle both cleanly in pure Kotlin using kotlin.math.roundToInt with zero JVM dependencies:

import kotlin.math.roundToInt

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.roundToInt()} ft"
    }
}

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')}"
    }
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This now uses pure Kotlin math and strings, with no java.* imports in the widget module. Values of 10 km/mi and above round to whole units; smaller values use one decimal after the precision follow-up.

Comment on lines +15 to +69
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))
}
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for adding unit tests for maps-compose-widgets!

Following up on the KMP comment in ScaleBar.kt, switching formatScaleDistance to pure Kotlin math means we can remove import java.util.Locale (and @Before / @After Locale.setDefault(...) mutation) here as well, and add a test case for larger distances (>= 10 km/mi):

package com.google.maps.android.compose.widgets

import org.junit.Assert.assertEquals
import org.junit.Test

internal class ScaleBarDistanceTest {
    @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 largeDistancesRoundToWholeUnits() {
        assertEquals("10 km", formatMetricDistance(10_400))
        assertEquals("12 km", formatMetricDistance(11_600))
        assertEquals("10 mi", formatImperialDistance(16_100))
    }

    @Test
    fun smallDistancesKeepWholeMetersAndFeet() {
        assertEquals("0 m", formatMetricDistance(0))
        assertEquals("0 ft", formatImperialDistance(0))
        assertEquals("100 m", formatMetricDistance(100))
        assertEquals("328 ft", formatImperialDistance(100))
    }
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the Locale setup and teardown, and added boundary, long-distance, rounding and trailing-zero cases. All six widget unit tests pass.

Comment on lines +56 to +95
assertFractionalLabels(dark = true)
}

private fun assertFractionalLabels(dark: Boolean) {
val originalLocale = Locale.getDefault()
Locale.setDefault(Locale.US)
try {
val projection = mockk<Projection>()
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<Point>().x == 0) LatLng(0.0, 0.0) else LatLng(0.0, 0.02)
}
val camera = mockk<CameraPositionState>()
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)
}
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Calling captureToImage() and writing PNGs to getExternalFilesDir(null) was a great way to visually verify the rendering during development! For the checked-in test suite, could we remove the bitmap capture and file write (along with the java.io.File and java.util.Locale imports) so the test stays fast, self-contained, and avoids unasserted disk I/O in CI?

    private fun assertFractionalLabels(dark: Boolean) {
        val projection = mockk<Projection>()
        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<Point>().x == 0) LatLng(0.0, 0.0) else LatLng(0.0, 0.02)
        }
        val camera = mockk<CameraPositionState>()
        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()
    }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed bitmap capture, file writes and locale mutation. In ee57e4c I moved the controlled-projection cases to Robolectric on API 29/35 because MockK’s Android agent crashes during API 29 activity cleanup. All eight cases pass. The device rendering checks now use a real, unbound camera state and pass on API 29.

Comment on lines +113 to +145
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"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome catch fixing Point(0, widthInPixels) to Point(widthInPixels, 0) here!

Similar to ScaleBar.kt, let's avoid import java.text.NumberFormat here as well so we stay free of new java.text.* imports:

                val upperLeftLatLng = proj.fromScreenLocation(Point(0, 0))
                val upperRightLatLng = proj.fromScreenLocation(Point(widthInPixels, 0))
                val canvasWidthMeters = upperLeftLatLng.sphericalDistance(upperRightLatLng)
                val horizontalLineWidthMeters = (canvasWidthMeters * 8 / 9).toInt()

                metricText = if (horizontalLineWidthMeters >= METERS_IN_KILOMETER) {
                    "${formatTestDistance(horizontalLineWidthMeters / METERS_IN_KILOMETER)} km"
                } else {
                    "$horizontalLineWidthMeters m"
                }

                val feet = horizontalLineWidthMeters.toDouble().toFeet()
                imperialText = if (feet >= FEET_IN_MILE) {
                    "${formatTestDistance(feet.toMiles())} mi"
                } else {
                    "${feet.roundToInt()} ft"
                }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kept the Point(widthInPixels, 0) correction and replaced the Java formatting with pure Kotlin expected labels. The live-map assertions still use the existing API-key assumption.

@premsinghsodha7
premsinghsodha7 marked this pull request as ready for review October 23, 2026 14:41
@premsinghsodha7

Copy link
Copy Markdown
Contributor Author

@dkhawk Addressed all five comments in 064e3b5:

  • Removed the orphaned unit-selection comment.
  • Replaced NumberFormat with pure Kotlin formatting; distances >= 10 km/mi round to whole units, and feet round as suggested.
  • Removed locale imports/mutation and added boundary, rounding, and long-distance unit coverage.
  • Removed bitmap capture, PNG writes, and File/Locale imports from the rendering tests. Added continental-distance coverage on light and dark backgrounds.
  • Updated live-map expected labels to pure Kotlin while retaining the horizontal projection-coordinate fix.

Validation: 6 widget unit tests and 4 API-35 emulator rendering tests pass; widget lint has zero issues, and app lint, instrumentation APK assembly, spotlessCheck, and git diff --check complete successfully. Live-map integration still needs a Maps API key.

Ready for another review. Thank you!

@bubenheimer

Copy link
Copy Markdown
Contributor

I find this PR very helpful for addressing the factual problem with the scale bar error, so it definitely is a valuable improvement.

I would not say that it completely fixes the issue, which also is about UX and usability. From my perspective a decent map has a scale bar showing lengths of 1 mile or 1 km, or 100ft, 100m, etc. Not 1.76 miles, because I as a human have trouble estimating distances based on fractional units. An optimal solution would size the scale bar to some of these more comprehensible lengths.

As far as reducing the error goes, I would not round to more than one decimal. I don't think 2 decimals are beneficial to human map readers.

@kikoso

kikoso commented Oct 28, 2026

Copy link
Copy Markdown
Collaborator

I would not say that it completely fixes the issue, which also is about UX and usability.

Hi @bubenheimer ! Agreed: this PR fixes the wrong numbers, but sizing the bar to round lengths (1 km, 500 m, 1 mi, 100 ft) like Google Maps does is the real fix for #233, so I would keep the issue open for that after this one lands. We are also adding a Units API to android-maps-utils (googlemaps/android-maps-utils#1832) that should make that follow-up easier.

As far as reducing the error goes, I would not round to more than one decimal.

That makes sense, I have suggested the same in my review.

@kikoso kikoso left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the contribution, @premsinghsodha7 , this is a nice fix for the wrong labels! Minor comment about the precision below. Also, since the variable-length bar from #233 is still pending, could we change Fixes #233 to Refs #233 in the description, so the issue stays open? Happy to approve after that.

Comment on lines +214 to +226
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')}"
}
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As @bubenheimer mentioned, two decimals are a bit too much for someone reading a map ("1.76 mi"). What about rounding to one decimal, which also matches what we are doing in the utils Units API? The tests would need an update (e.g. 1.01 km becomes 1 km, 1.24 mi becomes 1.2 mi).

Suggested change
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')}"
}
}
private fun formatScaleDistance(distance: Double): String {
if (distance >= 10.0) {
return distance.roundToInt().toString()
}
val scaled = (distance * 10.0).roundToInt()
return if (scaled % 10 == 0) "${scaled / 10}" else "${scaled / 10}.${scaled % 10}"
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied the one-decimal suggestion and updated the tests: 1.01 km becomes 1 km, and 1.24 mi becomes 1.2 mi. The description now uses Refs #233 so the variable-length bar work stays open.

@premsinghsodha7

Copy link
Copy Markdown
Contributor Author

Thanks @kikoso and @bubenheimer. I've reduced the precision to one decimal below 10 km/mi and updated the unit and rendering tests in ba81967. 1.01 km and 1.24 mi now read 1 km and 1.2 mi.

I've also changed Fixes #233 to Refs #233. Agreed that sizing the bar to round distances is the fuller UX fix, so #233 stays open for that follow-up. All six unit tests and four Android 15 emulator rendering tests pass.

@premsinghsodha7

Copy link
Copy Markdown
Contributor Author

@dkhawk Fixed the API 29 MockK crash and the MaterialTheme lint warning in ee57e4c. The 57 unit tests, full API 29 instrumentation/coverage command, build, lint, Spotless and screenshot validation passed locally. I’ve replied in each review thread and merged the latest main.

The current head is adb78fd. GitHub is waiting for maintainer approval before it can run the new workflows. Could you approve those runs?

dkhawk added a commit to googlemaps/android-maps-utils that referenced this pull request Oct 30, 2026
…esholds for ScaleBar

- Add `trimTrailingZeros: Boolean = false` and `NumberFormatSymbols.COMPACT` (`groupingSize = 0`, `trimTrailingZeros = true`) for compact widgets like `ScaleBar`
- Add `ImperialUnitsConverter(milesThreshold = ...)` / `ImperialUnitsConverter.withMilesThreshold(...)` and `MetricUnitsConverter(kilometersThreshold = ...)` / `MetricUnitsConverter.withKilometersThreshold(...)`
- Add `ScaleBar` distance formatting tests matching `googlemaps/android-maps-compose#1045` (`ScaleBarDistanceTest` and `ScaleBarRenderingTests`)
- Update `library.api` and documentation examples
dkhawk added a commit to googlemaps/android-maps-utils that referenced this pull request Nov 3, 2026
…ing (#1832)

* feat: add Units API for distance and elevation conversion and formatting

* fix(demo): read LocalLocale.current.platformLocale in UnitsControlPanel

* feat(units): expose clean Java conversion methods and fix UseKtx lint

* docs(skill): document Units API with Kotlin and Java examples in SKILL.md

* feat(units): address PR #1832 review feedback and support hybrid GB units

- Default `groupingSize` to `0` in `NumberFormatSymbols.fromLocale` when `!decimalFormat.isGroupingUsed`
- Fix threshold boundary rounding in `MetricUnitsConverter` (`999.6 m` -> `"1.0 km"`) and `ImperialUnitsConverter` (`1,319.6 ft` -> `"0.3 mi"`)
- Replace `translatable="false"` numeric templates with 4 translatable `%1$s` unit templates (`amu_in_feet`, `amu_in_miles`, `amu_in_meters`, `amu_in_kilometers`), add 24 localized `values-<locale>/strings.xml` translations, and unify `Locale` and `Resources` formatting onto a single `NumberFormatSymbols` code path
- Add `@get:JvmName("getMilesThreshold")` and `@get:JvmName("getKilometersThreshold")` for Java interop
- Remove short `Number.m`, `.km`, `.ft`, and `.mi` extensions and rename `Meters.to*` properties to `inMeters`, `inKilometers`, `inFeet`, and `inMiles`
- Default `"GB"` (`Locale.UK`) out-of-the-box to Imperial road distances (`ft`/`mi`) + Metric elevations (`m`), and add `withDistanceUnits`, `withElevationUnits`, `withDistanceConverter`, `withElevationConverter`, `withDistanceFormatter`, `withElevationFormatter`, and `UnitsConverter(...)` / `UnitsConverter.create(...)` overloads
- Update `README.md`, `llm-integration-prompt.md`, `.gemini/skills/android-maps-utils/SKILL.md`, and unit tests (`UnitsTest`, `UnitsJavaTest`, `UnitsDemoTest`)

* feat(units): add compact formatting symbols and configurable unit thresholds for ScaleBar

- Add `trimTrailingZeros: Boolean = false` and `NumberFormatSymbols.COMPACT` (`groupingSize = 0`, `trimTrailingZeros = true`) for compact widgets like `ScaleBar`
- Add `ImperialUnitsConverter(milesThreshold = ...)` / `ImperialUnitsConverter.withMilesThreshold(...)` and `MetricUnitsConverter(kilometersThreshold = ...)` / `MetricUnitsConverter.withKilometersThreshold(...)`
- Add `ScaleBar` distance formatting tests matching `googlemaps/android-maps-compose#1045` (`ScaleBarDistanceTest` and `ScaleBarRenderingTests`)
- Update `library.api` and documentation examples

* feat(units): improve Java API ergonomics and expand Java test coverage

* fix(units): align localized unit strings with Navigation SDK and CLDR across all 24 locales
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants