From d87c1f3388eff5d3d3153919c8f574ece910cb73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Lo=CC=81pez=20Man=CC=83as?= Date: Mon, 15 Sep 2025 21:35:04 +0200 Subject: [PATCH 1/6] fix: updating DisappearingScaleBar --- .../maps/android/compose/widgets/ScaleBar.kt | 45 +++++++------------ 1 file changed, 16 insertions(+), 29 deletions(-) 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 1bdc387e..b36e8608 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 @@ -72,29 +72,25 @@ public fun ScaleBar( lineColor: Color = DarkGray, shadowColor: Color = Color.White, ) { + var horizontalLineWidthMeters by remember { + mutableIntStateOf(0) + } + + LaunchedEffect(key1 = cameraPositionState.position) { + val upperLeftLatLng = cameraPositionState.projection?.fromScreenLocation(Point(0, 0)) ?: LatLng(0.0, 0.0) + val upperRightLatLng = cameraPositionState.projection?.fromScreenLocation(Point(0, width.value.toInt())) ?: LatLng(0.0, 0.0) + val canvasWidthMeters = upperLeftLatLng.sphericalDistance(upperRightLatLng) + val eightNinthsCanvasMeters = (canvasWidthMeters * 8 / 9).toInt() + + horizontalLineWidthMeters = eightNinthsCanvasMeters + } + Box( - modifier = modifier - .size(width = width, height = height) + modifier = modifier.size(width = width, height = height) ) { - var horizontalLineWidthMeters by remember { - mutableIntStateOf(0) - } - Canvas( modifier = Modifier.fillMaxSize(), onDraw = { - // Get width of canvas in meters - val upperLeftLatLng = - cameraPositionState.projection?.fromScreenLocation(Point(0, 0)) - ?: LatLng(0.0, 0.0) - val upperRightLatLng = - cameraPositionState.projection?.fromScreenLocation(Point(0, size.width.toInt())) - ?: LatLng(0.0, 0.0) - val canvasWidthMeters = upperLeftLatLng.sphericalDistance(upperRightLatLng) - val eightNinthsCanvasMeters = (canvasWidthMeters * 8 / 9).toInt() - - horizontalLineWidthMeters = eightNinthsCanvasMeters - val oneNinthWidth = size.width / 9 val midHeight = size.height / 2 val oneThirdHeight = size.height / 3 @@ -189,13 +185,6 @@ public fun ScaleBar( } } -/** - * 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. - * - * Implement your own observer on camera move events using [CameraPositionState] and pass it in - * as [cameraPositionState]. - */ @Composable public fun DisappearingScaleBar( modifier: Modifier = Modifier, @@ -212,12 +201,10 @@ public fun DisappearingScaleBar( val visible = remember { MutableTransitionState(true) } - - LaunchedEffect(key1 = cameraPositionState.position.zoom) { - // Show ScaleBar + // This effect controls visibility, not data updates + LaunchedEffect(key1 = cameraPositionState.position) { visible.targetState = true delay(visibilityDurationMillis.toLong()) - // Hide ScaleBar after timeout period visible.targetState = false } From 92c6de51bd7ef1ac7c45938e28d5ac7e521b9f9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Lo=CC=81pez=20Man=CC=83as?= Date: Mon, 15 Sep 2025 21:51:27 +0200 Subject: [PATCH 2/6] fix: added test --- .../maps/android/compose/ScaleBarTests.kt | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 maps-app/src/androidTest/java/com/google/maps/android/compose/ScaleBarTests.kt 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 new file mode 100644 index 00000000..0225b3f7 --- /dev/null +++ b/maps-app/src/androidTest/java/com/google/maps/android/compose/ScaleBarTests.kt @@ -0,0 +1,64 @@ +// Copyright 2023 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 androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithText +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 org.junit.Rule +import org.junit.Test +import java.util.concurrent.CountDownLatch + +class ScaleBarTests { + + @get:Rule + val composeTestRule = createComposeRule() + + private lateinit var cameraPositionState: CameraPositionState + + private fun initScaleBar(initialZoom: Float, initialPosition: LatLng) { + check(hasValidApiKey) { "Maps API key not specified" } + + cameraPositionState = CameraPositionState( + position = CameraPosition.fromLatLngZoom(initialPosition, initialZoom) + ) + + composeTestRule.setContent { + ScaleBar(cameraPositionState = cameraPositionState) + } + } + + @Test + fun testScaleBarInitialState() { + val initialZoom = 15f + val initialPosition = LatLng(37.7749, -122.4194) // San Francisco + initScaleBar(initialZoom, initialPosition) + + composeTestRule.waitForIdle() + + composeTestRule.onNodeWithText( + text = "ft", + substring = true, + ignoreCase = false + ).assertExists() + composeTestRule.onNodeWithText( + text = "m", + substring = true, + ignoreCase = false + ).assertExists() + } +} \ No newline at end of file From 60cf31e174edae4107bce92f81be093dc9aba4fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Lo=CC=81pez=20Man=CC=83as?= Date: Mon, 15 Sep 2025 21:52:58 +0200 Subject: [PATCH 3/6] fix: added test --- .../java/com/google/maps/android/compose/ScaleBarTests.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 0225b3f7..0f611d5a 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 @@ -1,4 +1,4 @@ -// Copyright 2023 Google LLC +// Copyright 2025 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. From df72c4c3e0658b89100e1c57b595d8152b50eba6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Lo=CC=81pez=20Man=CC=83as?= Date: Mon, 15 Sep 2025 21:53:43 +0200 Subject: [PATCH 4/6] fix: added test --- .../com/google/maps/android/compose/widgets/ScaleBar.kt | 7 +++++++ 1 file changed, 7 insertions(+) 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 b36e8608..cb23542a 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 @@ -185,6 +185,13 @@ public fun ScaleBar( } } +/** + * 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. + * + * Implement your own observer on camera move events using [CameraPositionState] and pass it in + * as [cameraPositionState]. + */ @Composable public fun DisappearingScaleBar( modifier: Modifier = Modifier, From 3f211be22b7468384a6e490cb19f1bb20283129a Mon Sep 17 00:00:00 2001 From: dkhawk <107309+dkhawk@users.noreply.github.com> Date: Tue, 16 Sep 2025 13:05:04 -0600 Subject: [PATCH 5/6] refactor(widgets): improve ScaleBar performance, tests, and documentation This commit enhances the `ScaleBar` composable with performance optimizations, more robust tests, and comprehensive documentation. - **Performance**: The `ScaleBar` now uses `remember` with the zoom level as a key, and `derivedStateOf` to ensure the scale is only recalculated when the map's zoom level changes, not on every pan. This significantly improves efficiency. - **Tests**: The `ScaleBarTests` are now more reliable. They use a null-safe `let` block to prevent potential crashes and calculate the expected scale text based on the map's projection for more accurate verification. - **Documentation**: - Added additional KDocs and inline comments to `ScaleBar` and `DisappearingScaleBar`, explaining complex calculations and advanced Compose concepts. --- .../maps/android/compose/ScaleBarTests.kt | 82 ++++++++++++-- .../maps/android/compose/widgets/ScaleBar.kt | 102 ++++++++++++++---- 2 files changed, 154 insertions(+), 30 deletions(-) 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 0f611d5a..e2619ada 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 @@ -14,14 +14,29 @@ package com.google.maps.android.compose +import android.graphics.Point +import androidx.compose.foundation.layout.Box import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.unit.dp 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 com.google.maps.android.ktx.utils.sphericalDistance +import org.junit.Assert.assertTrue +import org.junit.Assert.fail import org.junit.Rule import org.junit.Test import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit + +// These constants are used for converting between metric and imperial units +// to ensure the scale bar displays distances correctly in both systems. +private const val CENTIMETERS_IN_METER: Double = 100.0 +private const val METERS_IN_KILOMETER: Double = 1000.0 +private const val CENTIMETERS_IN_INCH: Double = 2.54 +private const val INCHES_IN_FOOT: Double = 12.0 +private const val FEET_IN_MILE: Double = 5280.0 class ScaleBarTests { @@ -33,13 +48,25 @@ class ScaleBarTests { private fun initScaleBar(initialZoom: Float, initialPosition: LatLng) { check(hasValidApiKey) { "Maps API key not specified" } + val countDownLatch = CountDownLatch(1) + cameraPositionState = CameraPositionState( position = CameraPosition.fromLatLngZoom(initialPosition, initialZoom) ) composeTestRule.setContent { - ScaleBar(cameraPositionState = cameraPositionState) + Box { + GoogleMap( + cameraPositionState = cameraPositionState, + onMapLoaded = { + countDownLatch.countDown() + } + ) + ScaleBar(cameraPositionState = cameraPositionState) + } } + val mapLoaded = countDownLatch.await(5, TimeUnit.SECONDS) + assertTrue(mapLoaded) } @Test @@ -50,15 +77,54 @@ class ScaleBarTests { composeTestRule.waitForIdle() + var imperialText = "" + var metricText = "" + + composeTestRule.runOnIdle { + // We use a `let` block to safely handle the projection, which can be null. + // If the projection is null, the test will fail explicitly, preventing + // any potential NullPointerExceptions and ensuring the test is robust. + val projection = cameraPositionState.projection + projection?.let { proj -> + val widthInDp = 65.dp + val widthInPixels = widthInDp.value.toInt() + + val upperLeftLatLng = proj.fromScreenLocation(Point(0, 0)) + val upperRightLatLng = proj.fromScreenLocation(Point(0, widthInPixels)) + val canvasWidthMeters = upperLeftLatLng.sphericalDistance(upperRightLatLng) + val horizontalLineWidthMeters = (canvasWidthMeters * 8 / 9).toInt() + + var metricUnits = "m" + var metricDistance = horizontalLineWidthMeters + if (horizontalLineWidthMeters > METERS_IN_KILOMETER) { + metricUnits = "km" + metricDistance /= METERS_IN_KILOMETER.toInt() + } + + var imperialUnits = "ft" + var imperialDistance = horizontalLineWidthMeters.toDouble().toFeet() + if (imperialDistance > FEET_IN_MILE) { + imperialUnits = "mi" + imperialDistance = imperialDistance.toMiles() + } + imperialText = "${imperialDistance.toInt()} $imperialUnits" + metricText = "$metricDistance $metricUnits" + } ?: fail("Projection should not be null") + } + composeTestRule.onNodeWithText( - text = "ft", - substring = true, - ignoreCase = false + text = imperialText, ).assertExists() composeTestRule.onNodeWithText( - text = "m", - substring = true, - ignoreCase = false + text = metricText, ).assertExists() } -} \ No newline at end of file +} + +internal fun Double.toFeet(): Double { + return this * CENTIMETERS_IN_METER / CENTIMETERS_IN_INCH / INCHES_IN_FOOT +} + +internal fun Double.toMiles(): Double { + return this / FEET_IN_MILE +} 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 cb23542a..02460cdc 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 @@ -31,10 +31,9 @@ import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment.Companion.End import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset @@ -46,7 +45,6 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.em import androidx.compose.ui.unit.sp -import com.google.android.gms.maps.model.LatLng import com.google.maps.android.compose.CameraPositionState import com.google.maps.android.ktx.utils.sphericalDistance import kotlinx.coroutines.delay @@ -59,8 +57,13 @@ private val defaultHeight: Dp = 50.dp * A scale bar composable that shows the current scale of the map in feet and meters when zoomed in * to the map, changing to miles and kilometers, respectively, when zooming out. * - * Implement your own observer on camera move events using [CameraPositionState] and pass it in - * as [cameraPositionState]. + * @param modifier Modifier to be applied to the composable. + * @param width The width of the composable. + * @param height The height of the composable. + * @param cameraPositionState The state of the camera position, used to calculate the scale. + * @param textColor The color of the text on the scale bar. + * @param lineColor The color of the lines on the scale bar. + * @param shadowColor The color of the shadow behind the text and lines. */ @Composable public fun ScaleBar( @@ -72,22 +75,45 @@ public fun ScaleBar( lineColor: Color = DarkGray, shadowColor: Color = Color.White, ) { - var horizontalLineWidthMeters by remember { - mutableIntStateOf(0) - } + // This is the core logic for calculating the scale of the map. + // + // `remember` with a key (`cameraPositionState.position.zoom`) is used for performance. + // It ensures that the calculation inside is only re-executed when the zoom level changes. + // This is important because we don't need to recalculate the scale every time the map pans, + // only when the zoom level changes. + // + // `derivedStateOf` is a Compose state function that creates a new state object that is + // derived from other state objects. The calculation inside `derivedStateOf` is only + // re-executed when one of the state objects it reads from changes. In this case, it's + // `cameraPositionState.projection`. This is another performance optimization that + // prevents unnecessary recalculations. + val horizontalLineWidthMeters by remember(cameraPositionState.position.zoom) { + derivedStateOf { + // The projection is used to convert between screen coordinates (pixels) and + // geographical coordinates (LatLng). It can be null if the map is not ready yet. + val projection = cameraPositionState.projection ?: return@derivedStateOf 0 - LaunchedEffect(key1 = cameraPositionState.position) { - val upperLeftLatLng = cameraPositionState.projection?.fromScreenLocation(Point(0, 0)) ?: LatLng(0.0, 0.0) - val upperRightLatLng = cameraPositionState.projection?.fromScreenLocation(Point(0, width.value.toInt())) ?: LatLng(0.0, 0.0) - val canvasWidthMeters = upperLeftLatLng.sphericalDistance(upperRightLatLng) - val eightNinthsCanvasMeters = (canvasWidthMeters * 8 / 9).toInt() + // We get the geographical coordinates of two points on the screen: the top-left + // corner (0, 0) and a point to the right of it, at the width of the scale bar. + val upperLeftLatLng = projection.fromScreenLocation(Point(0, 0)) + val upperRightLatLng = + projection.fromScreenLocation(Point(0, width.value.toInt())) - horizontalLineWidthMeters = eightNinthsCanvasMeters + // We then calculate the spherical distance between these two points in meters. + // This gives us the distance that the scale bar represents on the map. + val canvasWidthMeters = upperLeftLatLng.sphericalDistance(upperRightLatLng) + + // We take 8/9th of the canvas width to provide some padding on the right side + // of the scale bar. + (canvasWidthMeters * 8 / 9).toInt() + } } Box( modifier = modifier.size(width = width, height = height) ) { + // The Canvas composable is used for custom drawing. Here, we are drawing the + // lines of the scale bar. Canvas( modifier = Modifier.fillMaxSize(), onDraw = { @@ -98,7 +124,11 @@ public fun ScaleBar( val strokeWidth = 4f val shadowStrokeWidth = strokeWidth + 3 - // Middle horizontal line shadow (drawn under main lines) + // The shadows are drawn first, slightly offset from the main lines, to create + // a "drop shadow" effect. This makes the scale bar more readable on different + // map backgrounds. + + // Middle horizontal line shadow drawLine( color = shadowColor, start = Offset(oneNinthWidth, midHeight), @@ -106,7 +136,7 @@ public fun ScaleBar( strokeWidth = shadowStrokeWidth, cap = StrokeCap.Round ) - // Top vertical line shadow (drawn under main lines) + // Top vertical line shadow drawLine( color = shadowColor, start = Offset(oneNinthWidth, oneThirdHeight), @@ -114,7 +144,7 @@ public fun ScaleBar( strokeWidth = shadowStrokeWidth, cap = StrokeCap.Round ) - // Bottom vertical line shadow (drawn under main lines) + // Bottom vertical line shadow drawLine( color = shadowColor, start = Offset(oneNinthWidth, midHeight), @@ -123,6 +153,8 @@ public fun ScaleBar( cap = StrokeCap.Round ) + // These are the main lines of the scale bar. + // Middle horizontal line drawLine( color = lineColor, @@ -153,6 +185,9 @@ 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. + var metricUnits = "m" var metricDistance = horizontalLineWidthMeters if (horizontalLineWidthMeters > METERS_IN_KILOMETER) { @@ -169,6 +204,8 @@ public fun ScaleBar( 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, @@ -189,8 +226,16 @@ public fun ScaleBar( * 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. * - * Implement your own observer on camera move events using [CameraPositionState] and pass it in - * as [cameraPositionState]. + * @param modifier Modifier to be applied to the composable. + * @param width The width of the composable. + * @param height The height of the composable. + * @param cameraPositionState The state of the camera position, used to calculate the scale. + * @param textColor The color of the text on the scale bar. + * @param lineColor The color of the lines on the scale bar. + * @param shadowColor The color of the shadow behind the text and lines. + * @param visibilityDurationMillis The duration in milliseconds that the scale bar will be visible. + * @param enterTransition The animation to use when the scale bar appears. + * @param exitTransition The animation to use when the scale bar disappears. */ @Composable public fun DisappearingScaleBar( @@ -208,13 +253,24 @@ public fun DisappearingScaleBar( val visible = remember { MutableTransitionState(true) } - // This effect controls visibility, not data updates + + // `LaunchedEffect` is a coroutine-based effect that is launched when the composable + // enters the composition. The `key1` parameter is used to re-launch the effect + // whenever the value of the key changes. In this case, we are using + // `cameraPositionState.position` as the key, so the effect will be re-launched + // every time the camera position changes. + // + // The effect itself makes the scale bar visible, waits for the specified duration, + // and then makes it invisible again. This creates the "disappearing" effect. LaunchedEffect(key1 = cameraPositionState.position) { visible.targetState = true delay(visibilityDurationMillis.toLong()) visible.targetState = false } + // `AnimatedVisibility` is a composable that animates the appearance and disappearance + // of its content. We are using it here to wrap the `ScaleBar` and provide the + // fade-in and fade-out animations. AnimatedVisibility( visibleState = visible, modifier = modifier, @@ -257,7 +313,8 @@ private fun ScaleText( } /** - * Converts [this] value in meters to the corresponding value in feet + * Converts [this] value in meters to the corresponding value in feet. + * This is a utility function used for unit conversion. * @return [this] meters value converted to feet */ internal fun Double.toFeet(): Double { @@ -265,7 +322,8 @@ internal fun Double.toFeet(): Double { } /** - * Converts [this] value in feet to the corresponding value in miles + * Converts [this] value in feet to the corresponding value in miles. + * This is a utility function used for unit conversion. * @return [this] feet value converted to miles */ internal fun Double.toMiles(): Double { From 40fccad93d6037421188e7ce5a32f7da72016b2b Mon Sep 17 00:00:00 2001 From: dkhawk <107309+dkhawk@users.noreply.github.com> Date: Tue, 16 Sep 2025 13:12:50 -0600 Subject: [PATCH 6/6] refactor: Simplify comment in DisappearingScaleBar --- .../com/google/maps/android/compose/widgets/ScaleBar.kt | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) 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 02460cdc..afa46463 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 @@ -254,11 +254,7 @@ public fun DisappearingScaleBar( MutableTransitionState(true) } - // `LaunchedEffect` is a coroutine-based effect that is launched when the composable - // enters the composition. The `key1` parameter is used to re-launch the effect - // whenever the value of the key changes. In this case, we are using - // `cameraPositionState.position` as the key, so the effect will be re-launched - // every time the camera position changes. + // This effect is re-launched every time the camera position changes. // // The effect itself makes the scale bar visible, waits for the specified duration, // and then makes it invisible again. This creates the "disappearing" effect.