Skip to content

Commit

Permalink
create MapScale quantity with arithmetic to/from Length and Dp
Browse files Browse the repository at this point in the history
  • Loading branch information
sargunv committed Jan 8, 2025
1 parent cb846dc commit 1e0f74c
Show file tree
Hide file tree
Showing 13 changed files with 101 additions and 43 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ fun DemoMapControls(
if (Platform.supportsBlending) {
Box(modifier = modifier.fillMaxSize().padding(8.dp)) {
DisappearingScaleBar(
lengthPerDp = cameraState.lengthPerDpAtTarget,
scale = cameraState.scaleAtTarget,
zoom = cameraState.position.zoom,
modifier = Modifier.align(Alignment.TopStart),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ object CameraStateDemo : Demo {

Row(modifier = Modifier.safeDrawingPadding().wrapContentSize(Alignment.Center)) {
val pos = cameraState.position
val scale = cameraState.lengthPerDpAtTarget
val scale = cameraState.scaleAtTarget

Cell("Latitude", pos.target.latitude.format(3), Modifier.weight(1.4f))
Cell("Longitude", pos.target.longitude.format(3), Modifier.weight(1.4f))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ fun Material3() {
)

Box(modifier = Modifier.fillMaxSize().padding(8.dp)) {
ScaleBar(cameraState.lengthPerDpAtTarget, modifier = Modifier.align(Alignment.TopStart))
ScaleBar(cameraState.scaleAtTarget, modifier = Modifier.align(Alignment.TopStart))
CompassButton(cameraState, modifier = Modifier.align(Alignment.TopEnd))
AttributionButton(styleState, modifier = Modifier.align(Alignment.BottomEnd))
}
Expand All @@ -50,7 +50,7 @@ fun Material3() {

Box(modifier = Modifier.fillMaxSize().padding(8.dp)) {
DisappearingScaleBar(
lengthPerDp = cameraState.lengthPerDpAtTarget,
scale = cameraState.scaleAtTarget,
zoom = cameraState.position.zoom,
modifier = Modifier.align(Alignment.TopStart),
) // (1)!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import dev.sargunv.maplibrecompose.core.util.MapScale
import dev.sargunv.maplibrecompose.material3.defaultScaleBarMeasures
import io.github.kevincianfarini.alchemist.type.Length
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.delay
Expand All @@ -25,9 +25,8 @@ import kotlinx.coroutines.delay
* An animated scale bar that appears when the [zoom] level of the map changes, and then disappears
* after [visibilityDuration]. This composable wraps [ScaleBar] with visibility animations.
*
* @param lengthPerDp the real world distance in one device independent pixel (dp), i.e. the scale.
* See
* [CameraState.lengthPerDpAtTarget][dev.sargunv.maplibrecompose.compose.CameraState.lengthPerDpAtTarget]
* @param scale the ratio of real world distance [Length] to [Dp], i.e. the scale. See
* [CameraState.scaleAtTarget][dev.sargunv.maplibrecompose.compose.CameraState.scaleAtTarget]
* @param zoom zoom level of the map
* @param modifier the [Modifier] to be applied to this layout node
* @param measures which measures to show on the scale bar. If `null`, measures will be selected
Expand All @@ -43,7 +42,7 @@ import kotlinx.coroutines.delay
*/
@Composable
public fun DisappearingScaleBar(
lengthPerDp: Length,
scale: MapScale,
zoom: Double,
modifier: Modifier = Modifier,
measures: ScaleBarMeasures = defaultScaleBarMeasures(),
Expand Down Expand Up @@ -72,7 +71,7 @@ public fun DisappearingScaleBar(
exit = exitTransition,
) {
ScaleBar(
lengthPerDp = lengthPerDp,
scale = scale,
measures = measures,
haloColor = haloColor,
color = color,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.toSize
import dev.sargunv.maplibrecompose.core.util.MapScale
import dev.sargunv.maplibrecompose.core.util.div
import dev.sargunv.maplibrecompose.material3.defaultScaleBarMeasures
import dev.sargunv.maplibrecompose.material3.drawPathsWithHalo
import dev.sargunv.maplibrecompose.material3.drawTextWithHalo
Expand All @@ -38,9 +40,8 @@ public data class ScaleBarMeasures(
* A scale bar composable that shows the current scale of the map in feet, yards, or meters when
* zoomed in to the map, changing to miles and kilometers when zooming out.
*
* @param lengthPerDp the real world distance in one device independent pixel (dp), i.e. the scale.
* See
* [CameraState.lengthPerDpAtTarget][dev.sargunv.maplibrecompose.compose.CameraState.lengthPerDpAtTarget]
* @param scale the ratio of real world distance [Length] to [Dp], i.e. the scale. See
* [CameraState.scaleAtTarget][dev.sargunv.maplibrecompose.compose.CameraState.scaleAtTarget]
* @param modifier the [Modifier] to be applied to this layout node
* @param measures which measures to show on the scale bar. If `null`, measures will be selected
* based on the system settings or otherwise the user's locale.
Expand All @@ -52,7 +53,7 @@ public data class ScaleBarMeasures(
*/
@Composable
public fun ScaleBar(
lengthPerDp: Length,
scale: MapScale,
modifier: Modifier = Modifier,
measures: ScaleBarMeasures = defaultScaleBarMeasures(),
haloColor: Color = MaterialTheme.colorScheme.surface,
Expand All @@ -61,6 +62,7 @@ public fun ScaleBar(
alignment: Alignment.Horizontal = Alignment.Start,
) {
val textMeasurer = rememberTextMeasurer()

// longest possible text
val maxTextSizePx =
remember(textMeasurer, textStyle) { textMeasurer.measure("5000 km", textStyle).size }
Expand All @@ -87,8 +89,8 @@ public fun ScaleBar(
// scale bar start/end should not overlap horizontally with canvas bounds
val maxBarLength = maxWidth - fullStrokeWidth

val params1 = scaleBarParameters(measures.primary, lengthPerDp, maxBarLength)
val params2 = measures.secondary?.let { scaleBarParameters(it, lengthPerDp, maxBarLength) }
val params1 = scaleBarParameters(measures.primary, scale, maxBarLength)
val params2 = measures.secondary?.let { scaleBarParameters(it, scale, maxBarLength) }

Canvas(modifier.fillMaxSize()) {
val fullStrokeWidthPx = fullStrokeWidth.toPx()
Expand Down Expand Up @@ -187,12 +189,12 @@ private data class ScaleBarParams(val barWidth: Dp, val text: String)
@Composable
private fun scaleBarParameters(
measure: ScaleBarMeasure,
lengthPerDp: Length,
scale: MapScale,
maxBarLength: Dp,
): ScaleBarParams {
val max = lengthPerDp * maxBarLength.value.toDouble()
val max = scale * maxBarLength
val stop = findStop(max, measure.stops)
return ScaleBarParams(barWidth = (stop / lengthPerDp).dp, text = measure.getText(stop))
return ScaleBarParams(barWidth = (stop / scale), text = measure.getText(stop))
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.coerceAtLeast
import androidx.compose.ui.unit.dp
import co.touchlab.kermit.Logger
import dev.sargunv.maplibrecompose.core.util.MapScale
import dev.sargunv.maplibrecompose.core.util.correctedAndroidUri
import dev.sargunv.maplibrecompose.core.util.div
import dev.sargunv.maplibrecompose.core.util.toBoundingBox
import dev.sargunv.maplibrecompose.core.util.toGravity
import dev.sargunv.maplibrecompose.core.util.toLatLng
Expand All @@ -25,7 +27,6 @@ import io.github.dellisd.spatialk.geojson.BoundingBox
import io.github.dellisd.spatialk.geojson.Feature
import io.github.dellisd.spatialk.geojson.Position
import io.github.kevincianfarini.alchemist.scalar.toLength
import io.github.kevincianfarini.alchemist.type.Length
import io.github.kevincianfarini.alchemist.unit.LengthUnit.International.Meter
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
Expand Down Expand Up @@ -338,9 +339,9 @@ internal class AndroidMap(
.map { Feature.fromJson(it.toJson()) }
}

override fun lengthPerDpAtLatitude(latitude: Double): Length {
override fun scaleAtLatitude(latitude: Double): MapScale {
// https://github.com/kevincianfarini/alchemist/issues/54
return map.projection.getMetersPerPixelAtLatitude(latitude).toLength(Meter)
return map.projection.getMetersPerPixelAtLatitude(latitude).toLength(Meter) / 1.dp
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.DpRect
import androidx.compose.ui.unit.dp
import dev.sargunv.maplibrecompose.core.CameraMoveReason
import dev.sargunv.maplibrecompose.core.CameraPosition
import dev.sargunv.maplibrecompose.core.MaplibreMap
import dev.sargunv.maplibrecompose.core.StandardMaplibreMap
import dev.sargunv.maplibrecompose.core.VisibleRegion
import dev.sargunv.maplibrecompose.core.util.MapScale
import dev.sargunv.maplibrecompose.core.util.div
import dev.sargunv.maplibrecompose.expressions.ExpressionContext
import dev.sargunv.maplibrecompose.expressions.ast.Expression
import dev.sargunv.maplibrecompose.expressions.dsl.const
Expand All @@ -18,7 +21,6 @@ import io.github.dellisd.spatialk.geojson.BoundingBox
import io.github.dellisd.spatialk.geojson.Feature
import io.github.dellisd.spatialk.geojson.Position
import io.github.kevincianfarini.alchemist.scalar.toLength
import io.github.kevincianfarini.alchemist.type.Length
import io.github.kevincianfarini.alchemist.unit.LengthUnit.International.Meter
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
Expand Down Expand Up @@ -46,7 +48,7 @@ public class CameraState internal constructor(firstPosition: CameraPosition) {
internal val positionState = mutableStateOf(firstPosition)
internal val moveReasonState = mutableStateOf(CameraMoveReason.NONE)
// https://github.com/kevincianfarini/alchemist/issues/54
internal val lengthPerDpAtTargetState = mutableStateOf(0.toLength(Meter))
internal val scaleAtTargetState = mutableStateOf(0.toLength(Meter) / 1.dp)

/** how the camera is oriented towards the map */
// if the map is not yet initialized, we store the value to apply it later
Expand All @@ -62,8 +64,8 @@ public class CameraState internal constructor(firstPosition: CameraPosition) {
get() = moveReasonState.value

/** real world distance per dp at the target position */
public val lengthPerDpAtTarget: Length
get() = lengthPerDpAtTargetState.value
public val scaleAtTarget: MapScale
get() = scaleAtTargetState.value

/** suspends until the map has been initialized */
public suspend fun awaitInitialized() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,8 @@ public fun MaplibreMap(
map as StandardMaplibreMap
styleState.attach(style)
rememberedStyle = style
cameraState.lengthPerDpAtTargetState.value =
map.lengthPerDpAtLatitude(map.getCameraPosition().target.latitude)
cameraState.scaleAtTargetState.value =
map.scaleAtLatitude(map.getCameraPosition().target.latitude)
}

override fun onCameraMoveStarted(map: MaplibreMap, reason: CameraMoveReason) {
Expand All @@ -126,8 +126,8 @@ public fun MaplibreMap(
override fun onCameraMoved(map: MaplibreMap) {
map as StandardMaplibreMap
cameraState.positionState.value = map.getCameraPosition()
cameraState.lengthPerDpAtTargetState.value =
map.lengthPerDpAtLatitude(map.getCameraPosition().target.latitude)
cameraState.scaleAtTargetState.value =
map.scaleAtLatitude(map.getCameraPosition().target.latitude)
}

override fun onCameraMoveEnded(map: MaplibreMap) {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ package dev.sargunv.maplibrecompose.core

import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.DpRect
import dev.sargunv.maplibrecompose.core.util.MapScale
import dev.sargunv.maplibrecompose.expressions.ast.CompiledExpression
import dev.sargunv.maplibrecompose.expressions.value.BooleanValue
import io.github.dellisd.spatialk.geojson.BoundingBox
import io.github.dellisd.spatialk.geojson.Feature
import io.github.dellisd.spatialk.geojson.Position
import io.github.kevincianfarini.alchemist.type.Length
import kotlin.time.Duration

internal interface MaplibreMap {
Expand Down Expand Up @@ -55,7 +55,7 @@ internal interface MaplibreMap {
predicate: CompiledExpression<BooleanValue>? = null,
): List<Feature>

suspend fun asyncLengthPerDpAtLatitude(latitude: Double): Length
suspend fun asyncScaleAtLatitude(latitude: Double): MapScale

interface Callbacks {
fun onStyleChanged(map: MaplibreMap, style: Style?)
Expand Down Expand Up @@ -121,8 +121,7 @@ internal interface StandardMaplibreMap : MaplibreMap {
predicate: CompiledExpression<BooleanValue>?,
): List<Feature> = queryRenderedFeatures(rect, layerIds, predicate)

override suspend fun asyncLengthPerDpAtLatitude(latitude: Double): Length =
lengthPerDpAtLatitude(latitude)
override suspend fun asyncScaleAtLatitude(latitude: Double): MapScale = scaleAtLatitude(latitude)

fun setStyleUri(styleUri: String)

Expand Down Expand Up @@ -166,5 +165,5 @@ internal interface StandardMaplibreMap : MaplibreMap {
predicate: CompiledExpression<BooleanValue>? = null,
): List<Feature>

fun lengthPerDpAtLatitude(latitude: Double): Length
fun scaleAtLatitude(latitude: Double): MapScale
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package dev.sargunv.maplibrecompose.core.util

import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import io.github.kevincianfarini.alchemist.scalar.toLength
import io.github.kevincianfarini.alchemist.type.Length
import io.github.kevincianfarini.alchemist.unit.LengthUnit.International.Nanometer
import kotlin.jvm.JvmInline
import kotlin.math.roundToLong

public inline operator fun Length.div(dp: Dp): MapScale =
MapScale((this.toLong(Nanometer) / dp.value).roundToLong().toLength(Nanometer))

public inline operator fun Length.div(scale: MapScale): Dp = (this / scale.lengthPerDp).dp

public inline operator fun Dp.times(scale: MapScale): Length = scale * this

public inline operator fun Number.times(scale: MapScale): MapScale = scale * this

/**
* Represents a map scale: a ratio of real-world distance ([Length]) to device-independent pixels
* ([Dp]).
*/
@JvmInline
public value class MapScale
@PublishedApi
internal constructor(@PublishedApi internal val lengthPerDp: Length) : Comparable<MapScale> {

public operator fun times(dp: Dp): Length =
(lengthPerDp.toLong(Nanometer) * dp.value).roundToLong().toLength(Nanometer)

public operator fun div(other: MapScale): Double {
return lengthPerDp / other.lengthPerDp
}

public operator fun div(other: Number): MapScale =
MapScale((lengthPerDp.toLong(Nanometer) / other.toDouble()).roundToLong().toLength(Nanometer))

public operator fun unaryMinus(): MapScale = MapScale(-lengthPerDp)

public operator fun times(other: Number): MapScale =
MapScale((lengthPerDp.toLong(Nanometer) * other.toDouble()).roundToLong().toLength(Nanometer))

override fun toString(): String {
return "$lengthPerDp/dp"
}

override fun compareTo(other: MapScale): Int = lengthPerDp.compareTo(other.lengthPerDp)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ package dev.sargunv.maplibrecompose.core
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.DpRect
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import dev.sargunv.maplibrecompose.core.util.MapScale
import dev.sargunv.maplibrecompose.core.util.div
import dev.sargunv.maplibrecompose.expressions.ast.CompiledExpression
import dev.sargunv.maplibrecompose.expressions.value.BooleanValue
import io.github.dellisd.spatialk.geojson.BoundingBox
import io.github.dellisd.spatialk.geojson.Feature
import io.github.dellisd.spatialk.geojson.Position
import io.github.kevincianfarini.alchemist.scalar.toLength
import io.github.kevincianfarini.alchemist.type.Length
import io.github.kevincianfarini.alchemist.unit.LengthUnit.International.Meter
import kotlin.time.Duration

Expand Down Expand Up @@ -142,8 +144,8 @@ internal class WebviewMap(private val bridge: WebviewBridge) : MaplibreMap {
return emptyList()
}

override suspend fun asyncLengthPerDpAtLatitude(latitude: Double): Length {
override suspend fun asyncScaleAtLatitude(latitude: Double): MapScale {
// https://github.com/kevincianfarini/alchemist/issues/54
return 0.0.toLength(Meter)
return 0.0.toLength(Meter) / 1.dp
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import cocoapods.MapLibre.MLNOrnamentPositionTopRight
import cocoapods.MapLibre.MLNStyle
import cocoapods.MapLibre.MLNZoomLevelForAltitude
import cocoapods.MapLibre.allowsTilting
import dev.sargunv.maplibrecompose.core.util.div
import dev.sargunv.maplibrecompose.core.util.toBoundingBox
import dev.sargunv.maplibrecompose.core.util.toCGPoint
import dev.sargunv.maplibrecompose.core.util.toCGRect
Expand Down Expand Up @@ -475,6 +476,6 @@ internal class IosMap(
)
.map { (it as MLNFeatureProtocol).toFeature() }

override fun lengthPerDpAtLatitude(latitude: Double) =
mapView.metersPerPointAtLatitude(latitude).meters
override fun scaleAtLatitude(latitude: Double) =
mapView.metersPerPointAtLatitude(latitude).meters / 1.dp
}
Loading

0 comments on commit 1e0f74c

Please sign in to comment.