Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,4 @@ android-application = { id = "com.android.application", version.ref = "agp" }
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
screenshot = { id = "com.android.compose.screenshot", version.ref = "screenshot"}
screenshot = { id = "com.android.compose.screenshot", version.ref = "screenshot"}
10 changes: 10 additions & 0 deletions maps-compose-widgets/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,19 @@ android {
sarifOutput = layout.buildDirectory.file("reports/lint-results.sarif").get().asFile
}

packaging {
resources {
excludes += "META-INF/LICENSE.md"
excludes += "META-INF/LICENSE-notice.md"
}
}

namespace = "com.google.maps.android.compose.widgets"
compileSdk = 36

defaultConfig {
minSdk = 21
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}

compileOptions {
Expand Down Expand Up @@ -55,4 +63,6 @@ dependencies {
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.test.espresso)
androidTestImplementation(libs.androidx.test.junit.ktx)
androidTestImplementation(libs.mockk)
androidTestImplementation(libs.truth)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.google.maps.android.compose.widgets


import android.graphics.Point
import io.mockk.every
import io.mockk.mockk
import org.junit.Test
import com.google.common.truth.Truth.assertThat
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.android.gms.maps.Projection
import com.google.android.gms.maps.model.LatLng
import com.google.maps.android.ktx.utils.sphericalDistance
import org.junit.runner.RunWith


@RunWith(AndroidJUnit4::class)
public class ScaleBarUnitTest {

@Test
public fun testScaleBarCalculation() {
val projection = mockk<Projection>(relaxed = true)
val density = Density(1f, 1f)
val width = 100.dp

val startPoint = Point(0, 0)
val endPoint = Point(width.value.toInt(), 0)

val startLatLng = LatLng(0.0, 0.0)
val endLatLng = LatLng(0.0, 0.001)

every { projection.fromScreenLocation(startPoint) } returns startLatLng
every { projection.fromScreenLocation(endPoint) } returns endLatLng

val expectedDistance = startLatLng.sphericalDistance(endLatLng)
val expectedResult = (expectedDistance * 8 / 9).toInt()

val result = calculateDistance(projection, width, density)

assertThat(result).isEqualTo(expectedResult)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,36 @@ import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shadow
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Density
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.Projection
import com.google.maps.android.compose.CameraPositionState
import com.google.maps.android.ktx.utils.sphericalDistance
import kotlinx.coroutines.delay

internal fun calculateDistance(
projection: Projection,
width: Dp,
density: Density
): Int {
val widthInPixels = with(density) {
width.toPx().toInt()
}

val upperLeftLatLng = projection.fromScreenLocation(Point(0, 0))
val upperRightLatLng =
projection.fromScreenLocation(Point(widthInPixels, 0))

val canvasWidthMeters = upperLeftLatLng.sphericalDistance(upperRightLatLng)

return (canvasWidthMeters * 8 / 9).toInt()
}

public val DarkGray: Color = Color(0xFF3a3c3b)
private val defaultWidth: Dp = 65.dp
private val defaultHeight: Dp = 50.dp
Expand All @@ -75,37 +96,12 @@ public fun ScaleBar(
lineColor: Color = DarkGray,
shadowColor: Color = Color.White,
) {
// 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 density = LocalDensity.current
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

// 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()))

// 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()
cameraPositionState.projection?.let {
calculateDistance(it, width, density)
} ?: 0
}
}

Expand Down