Skip to content

Commit

Permalink
Animate camera to LatLngBounds (#258)
Browse files Browse the repository at this point in the history
Co-authored-by: Sargun Vohra <[email protected]>
  • Loading branch information
michalgwo and sargunv authored Jan 27, 2025
1 parent 71f0cef commit d795983
Show file tree
Hide file tree
Showing 11 changed files with 178 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ public external class Map public constructor(options: MapOptions) {

public fun easeTo(options: EaseToOptions)

public fun fitBounds(bounds: LngLatBounds, options: FitBoundsOptions?)

public fun flyTo(options: FlyToOptions)

public fun addControl(control: IControl, position: String)
Expand Down Expand Up @@ -383,6 +385,15 @@ public sealed external interface EaseToOptions : CameraOptions {
public var easing: (t: Double) -> Double?
}

/**
* [FitBoundsOptions](https://maplibre.org/maplibre-gl-js/docs/API/type-aliases/FitBoundsOptions/)
*/
public sealed external interface FitBoundsOptions : FlyToOptions {
public var linear: Boolean?
public var maxZoom: Double?
public var offset: Point?
}

/** [FlyToOptions](https://maplibre.org/maplibre-gl-js/docs/API/type-aliases/FlyToOptions/) */
public sealed external interface FlyToOptions : CameraOptions {
public var curve: Double?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,36 @@ public fun PaddingOptions(
right?.let { this.right = it }
}

public fun FitBoundsOptions(
linear: Boolean? = null,
maxZoom: Double? = null,
offset: Point? = null,
center: LngLat? = null,
zoom: Double? = null,
bearing: Double? = null,
pitch: Double? = null,
speed: Double? = null,
curve: Double? = null,
maxDuration: Double? = null,
minZoom: Double? = null,
padding: PaddingOptions? = null,
screenSpeed: Double? = null,
): FitBoundsOptions = jso {
linear?.let { this.linear = it }
maxZoom?.let { this.maxZoom = it }
offset?.let { this.offset = it }
center?.let { this.center = it }
zoom?.let { this.zoom = it }
bearing?.let { this.bearing = it }
pitch?.let { this.pitch = it }
speed?.let { this.speed = it }
curve?.let { this.curve = it }
maxDuration?.let { this.maxDuration = it }
minZoom?.let { this.minZoom = it }
padding?.let { this.padding = it }
screenSpeed?.let { this.screenSpeed = it }
}

public fun FlyToOptions(
center: LngLat? = null,
zoom: Double? = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import dev.sargunv.maplibrecompose.core.util.correctedAndroidUri
import dev.sargunv.maplibrecompose.core.util.toBoundingBox
import dev.sargunv.maplibrecompose.core.util.toGravity
import dev.sargunv.maplibrecompose.core.util.toLatLng
import dev.sargunv.maplibrecompose.core.util.toLatLngBounds
import dev.sargunv.maplibrecompose.core.util.toMLNExpression
import dev.sargunv.maplibrecompose.core.util.toOffset
import dev.sargunv.maplibrecompose.core.util.toPointF
Expand All @@ -24,6 +25,7 @@ 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 kotlin.coroutines.Continuation
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
import kotlin.time.Duration
Expand Down Expand Up @@ -292,18 +294,45 @@ internal class AndroidMap(
map.moveCamera(CameraUpdateFactory.newCameraPosition(cameraPosition.toMLNCameraPosition()))
}

private class CancelableCoroutineCallback(private val cont: Continuation<Unit>) :
MLNMap.CancelableCallback {
override fun onCancel() = cont.resume(Unit)

override fun onFinish() = cont.resume(Unit)
}

override suspend fun animateCameraPosition(finalPosition: CameraPosition, duration: Duration) =
suspendCoroutine { cont ->
map.animateCamera(
CameraUpdateFactory.newCameraPosition(finalPosition.toMLNCameraPosition()),
duration.toInt(DurationUnit.MILLISECONDS),
object : MLNMap.CancelableCallback {
override fun onFinish() = cont.resume(Unit)
CancelableCoroutineCallback(cont),
)
}

override fun onCancel() = cont.resume(Unit)
},
override suspend fun animateCameraPosition(
boundingBox: BoundingBox,
bearing: Double,
tilt: Double,
padding: PaddingValues,
duration: Duration,
) = suspendCoroutine { cont ->
with(density) {
map.animateCamera(
CameraUpdateFactory.newLatLngBounds(
bounds = boundingBox.toLatLngBounds(),
bearing = bearing,
tilt = tilt,
paddingLeft = padding.calculateLeftPadding(layoutDir).roundToPx(),
paddingTop = padding.calculateTopPadding().roundToPx(),
paddingRight = padding.calculateRightPadding(layoutDir).roundToPx(),
paddingBottom = padding.calculateBottomPadding().roundToPx(),
),
duration.toInt(DurationUnit.MILLISECONDS),
CancelableCoroutineCallback(cont),
)
}
}

override fun positionFromScreenLocation(offset: DpOffset): Position =
map.projection.fromScreenLocation(offset.toPointF(density)).toPosition()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,14 @@ internal fun Position.toLatLng(): LatLng = LatLng(latitude = latitude, longitude
internal fun LatLngBounds.toBoundingBox(): BoundingBox =
BoundingBox(northeast = northEast.toPosition(), southwest = southWest.toPosition())

internal fun BoundingBox.toLatLngBounds(): LatLngBounds =
LatLngBounds.from(
latNorth = northeast.latitude,
lonEast = northeast.longitude,
latSouth = southwest.latitude,
lonWest = southwest.longitude,
)

internal fun CompiledExpression<*>.toMLNExpression(): MLNExpression? =
if (this == NullLiteral) null else MLNExpression.Converter.convert(normalizeJsonLike(false))

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package dev.sargunv.maplibrecompose.compose

import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable
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
Expand Down Expand Up @@ -75,6 +77,27 @@ public class CameraState internal constructor(firstPosition: CameraPosition) {
map.animateCameraPosition(finalPosition, duration)
}

/**
* Animates the camera towards the specified [boundingBox] in the given [duration] time with the
* specified [bearing], [tilt], and [padding].
*
* @param boundingBox The bounds to animate the camera to.
* @param bearing The bearing to set during the animation. Defaults to 0.0.
* @param tilt The tilt to set during the animation. Defaults to 0.0.
* @param padding The padding to apply during the animation. Defaults to no padding.
* @param duration The duration of the animation. Defaults to 300 ms. Has no effect on JS.
*/
public suspend fun animateTo(
boundingBox: BoundingBox,
bearing: Double = 0.0,
tilt: Double = 0.0,
padding: PaddingValues = PaddingValues(0.dp),
duration: Duration = 300.milliseconds,
) {
val map = map ?: mapAttachSignal.receive()
map.animateCameraPosition(boundingBox, bearing, tilt, padding, duration)
}

private fun requireMap(): StandardMaplibreMap {
check(map != null) {
"Map requested before it was initialized; try calling awaitInitialization() first"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package dev.sargunv.maplibrecompose.core

import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.DpRect
import dev.sargunv.maplibrecompose.expressions.ast.CompiledExpression
Expand All @@ -12,6 +13,14 @@ import kotlin.time.Duration
internal interface MaplibreMap {
suspend fun animateCameraPosition(finalPosition: CameraPosition, duration: Duration)

suspend fun animateCameraPosition(
boundingBox: BoundingBox,
bearing: Double,
tilt: Double,
padding: PaddingValues,
duration: Duration,
)

suspend fun asyncSetStyleUri(styleUri: String)

suspend fun asyncSetDebugEnabled(enabled: Boolean)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package dev.sargunv.maplibrecompose.core

import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.DpRect
import androidx.compose.ui.unit.LayoutDirection
Expand Down Expand Up @@ -115,6 +116,14 @@ internal class WebviewMap(private val bridge: WebviewBridge) : MaplibreMap {

override suspend fun animateCameraPosition(finalPosition: CameraPosition, duration: Duration) {}

override suspend fun animateCameraPosition(
boundingBox: BoundingBox,
bearing: Double,
tilt: Double,
padding: PaddingValues,
duration: Duration,
) {}

override suspend fun asyncGetPosFromScreenLocation(offset: DpOffset): Position {
return Position(0.0, 0.0)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import dev.sargunv.maplibrecompose.core.util.toCGRect
import dev.sargunv.maplibrecompose.core.util.toCLLocationCoordinate2D
import dev.sargunv.maplibrecompose.core.util.toDpOffset
import dev.sargunv.maplibrecompose.core.util.toFeature
import dev.sargunv.maplibrecompose.core.util.toMLNCoordinateBounds
import dev.sargunv.maplibrecompose.core.util.toMLNOrnamentPosition
import dev.sargunv.maplibrecompose.core.util.toNSPredicate
import dev.sargunv.maplibrecompose.core.util.toPosition
Expand Down Expand Up @@ -433,13 +434,34 @@ internal class IosMap(
override suspend fun animateCameraPosition(finalPosition: CameraPosition, duration: Duration) =
suspendCoroutine { cont ->
mapView.flyToCamera(
finalPosition.toMLNMapCamera(),
camera = finalPosition.toMLNMapCamera(),
withDuration = duration.toDouble(DurationUnit.SECONDS),
edgePadding = finalPosition.padding.toEdgeInsets(),
completionHandler = { cont.resume(Unit) },
)
}

override suspend fun animateCameraPosition(
boundingBox: BoundingBox,
bearing: Double,
tilt: Double,
padding: PaddingValues,
duration: Duration,
) {
suspendCoroutine { cont ->
mapView.flyToCamera(
camera =
mapView.cameraThatFitsCoordinateBounds(boundingBox.toMLNCoordinateBounds()).apply {
heading = bearing
pitch = tilt
},
withDuration = duration.toDouble(DurationUnit.SECONDS),
edgePadding = padding.toEdgeInsets(),
completionHandler = { cont.resume(Unit) },
)
}
}

override fun positionFromScreenLocation(offset: DpOffset): Position =
mapView.convertPoint(point = offset.toCGPoint(), toCoordinateFromView = null).toPosition()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import cocoapods.MapLibre.MLNCoordinateBounds
import cocoapods.MapLibre.MLNCoordinateBoundsMake
import cocoapods.MapLibre.MLNFeatureProtocol
import cocoapods.MapLibre.MLNOrnamentPosition
import cocoapods.MapLibre.MLNOrnamentPositionBottomLeft
Expand Down Expand Up @@ -111,6 +112,12 @@ internal fun CValue<MLNCoordinateBounds>.toBoundingBox(): BoundingBox = useConte
BoundingBox(northeast = ne.toPosition(), southwest = sw.toPosition())
}

internal fun BoundingBox.toMLNCoordinateBounds(): CValue<MLNCoordinateBounds> =
MLNCoordinateBoundsMake(
ne = northeast.toCLLocationCoordinate2D(),
sw = southwest.toCLLocationCoordinate2D(),
)

internal fun GeoJson.toMLNShape(): MLNShape {
return MLNShape.shapeWithData(
data = json().encodeToByteArray().toNSData(),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package dev.sargunv.maplibrecompose.core

import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.DpRect
Expand All @@ -8,6 +9,7 @@ import co.touchlab.kermit.Logger
import dev.sargunv.maplibrecompose.core.util.toBoundingBox
import dev.sargunv.maplibrecompose.core.util.toControlPosition
import dev.sargunv.maplibrecompose.core.util.toDpOffset
import dev.sargunv.maplibrecompose.core.util.toLatLngBounds
import dev.sargunv.maplibrecompose.core.util.toLngLat
import dev.sargunv.maplibrecompose.core.util.toPaddingOptions
import dev.sargunv.maplibrecompose.core.util.toPaddingValuesAbsolute
Expand All @@ -17,6 +19,7 @@ import dev.sargunv.maplibrecompose.expressions.ast.CompiledExpression
import dev.sargunv.maplibrecompose.expressions.value.BooleanValue
import dev.sargunv.maplibrejs.AttributionControl
import dev.sargunv.maplibrejs.EaseToOptions
import dev.sargunv.maplibrejs.FitBoundsOptions
import dev.sargunv.maplibrejs.JumpToOptions
import dev.sargunv.maplibrejs.LngLat
import dev.sargunv.maplibrejs.LogoControl
Expand Down Expand Up @@ -253,6 +256,25 @@ internal class JsMap(
)
}

override suspend fun animateCameraPosition(
boundingBox: BoundingBox,
bearing: Double,
tilt: Double,
padding: PaddingValues,
duration: Duration,
) {
impl.fitBounds(
bounds = boundingBox.toLatLngBounds(),
options =
FitBoundsOptions(
linear = true,
bearing = bearing,
pitch = tilt,
padding = padding.toPaddingOptions(layoutDir),
),
)
}

override fun positionFromScreenLocation(offset: DpOffset): Position {
return impl.unproject(offset.toPoint()).toPosition()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,6 @@ internal fun PaddingValues.toPaddingOptions(layoutDir: LayoutDirection) =

internal fun LngLatBounds.toBoundingBox() =
BoundingBox(south = getSouth(), west = getWest(), north = getNorth(), east = getEast())

internal fun BoundingBox.toLatLngBounds() =
LngLatBounds(sw = southwest.toLngLat(), ne = northeast.toLngLat())

0 comments on commit d795983

Please sign in to comment.