diff --git a/libraries/maplibre-compose/build.gradle.kts b/libraries/maplibre-compose/build.gradle.kts deleted file mode 100644 index 5552aae37f..0000000000 --- a/libraries/maplibre-compose/build.gradle.kts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright (c) 2025 Element Creations Ltd. - * Copyright 2022-2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -plugins { - id("io.element.android-compose-library") - id("kotlin-parcelize") -} - -android { - namespace = "io.element.android.libraries.maplibre.compose" - - kotlin { - compilerOptions { - explicitApi() - } - } -} - -dependencies { - api(libs.maplibre) - api(libs.maplibre.ktx) - api(libs.maplibre.annotation) -} diff --git a/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/CameraMode.kt b/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/CameraMode.kt deleted file mode 100644 index 8ef02f5bbc..0000000000 --- a/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/CameraMode.kt +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (c) 2025 Element Creations Ltd. - * Copyright 2023-2025 New Vector Ltd. - * Copyright 2021 Google LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.libraries.maplibre.compose - -import androidx.compose.runtime.Immutable -import org.maplibre.android.location.modes.CameraMode as InternalCameraMode - -@Immutable -public enum class CameraMode { - NONE, - NONE_COMPASS, - NONE_GPS, - TRACKING, - TRACKING_COMPASS, - TRACKING_GPS, - TRACKING_GPS_NORTH; - - @InternalCameraMode.Mode - internal fun toInternal(): Int = when (this) { - NONE -> InternalCameraMode.NONE - NONE_COMPASS -> InternalCameraMode.NONE_COMPASS - NONE_GPS -> InternalCameraMode.NONE_GPS - TRACKING -> InternalCameraMode.TRACKING - TRACKING_COMPASS -> InternalCameraMode.TRACKING_COMPASS - TRACKING_GPS -> InternalCameraMode.TRACKING_GPS - TRACKING_GPS_NORTH -> InternalCameraMode.TRACKING_GPS_NORTH - } - - internal companion object { - fun fromInternal(@InternalCameraMode.Mode mode: Int): CameraMode = when (mode) { - InternalCameraMode.NONE -> NONE - InternalCameraMode.NONE_COMPASS -> NONE_COMPASS - InternalCameraMode.NONE_GPS -> NONE_GPS - InternalCameraMode.TRACKING -> TRACKING - InternalCameraMode.TRACKING_COMPASS -> TRACKING_COMPASS - InternalCameraMode.TRACKING_GPS -> TRACKING_GPS - InternalCameraMode.TRACKING_GPS_NORTH -> TRACKING_GPS_NORTH - else -> error("Unknown camera mode: $mode") - } - } -} diff --git a/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/CameraMoveStartedReason.kt b/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/CameraMoveStartedReason.kt deleted file mode 100644 index 2683de1655..0000000000 --- a/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/CameraMoveStartedReason.kt +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (c) 2025 Element Creations Ltd. - * Copyright 2023-2025 New Vector Ltd. - * Copyright 2021 Google LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.libraries.maplibre.compose - -import androidx.compose.runtime.Immutable -import org.maplibre.android.maps.MapLibreMap.OnCameraMoveStartedListener.REASON_API_ANIMATION -import org.maplibre.android.maps.MapLibreMap.OnCameraMoveStartedListener.REASON_API_GESTURE -import org.maplibre.android.maps.MapLibreMap.OnCameraMoveStartedListener.REASON_DEVELOPER_ANIMATION - -/** - * Enumerates the different reasons why the map camera started to move. - * - * Based on enum values from https://docs.maptiler.com/maplibre-gl-native-android/org.maplibre.android.maps/#oncameramovestartedlistener. - * - * [NO_MOVEMENT_YET] is used as the initial state before any map movement has been observed. - * - * [UNKNOWN] is used to represent when an unsupported integer value is provided to [fromInt] - this - * may be a new constant value from the Maps SDK that isn't supported by maps-compose yet, in which - * case this library should be updated to include a new enum value for that constant. - */ -@Immutable -public enum class CameraMoveStartedReason(public val value: Int) { - UNKNOWN(-2), - NO_MOVEMENT_YET(-1), - GESTURE(REASON_API_GESTURE), - API_ANIMATION(REASON_API_ANIMATION), - DEVELOPER_ANIMATION(REASON_DEVELOPER_ANIMATION); - - public companion object { - /** - * Converts from the Maps SDK [org.maplibre.android.maps.MapLibreMap.OnCameraMoveStartedListener] - * constants to [CameraMoveStartedReason], or returns [UNKNOWN] if there is no such - * [CameraMoveStartedReason] for the given [value]. - * - * See https://docs.maptiler.com/maplibre-gl-native-android/org.maplibre.android.maps/#oncameramovestartedlistener. - */ - public fun fromInt(value: Int): CameraMoveStartedReason { - return values().firstOrNull { it.value == value } ?: return UNKNOWN - } - } -} diff --git a/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/CameraPositionState.kt b/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/CameraPositionState.kt deleted file mode 100644 index 1999526718..0000000000 --- a/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/CameraPositionState.kt +++ /dev/null @@ -1,179 +0,0 @@ -/* - * Copyright (c) 2025 Element Creations Ltd. - * Copyright 2023-2025 New Vector Ltd. - * Copyright 2021 Google LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.libraries.maplibre.compose - -import android.location.Location -import android.os.Parcelable -import androidx.compose.runtime.Composable -import androidx.compose.runtime.ReadOnlyComposable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.Saver -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.runtime.staticCompositionLocalOf -import kotlinx.parcelize.Parcelize -import org.maplibre.android.camera.CameraPosition -import org.maplibre.android.camera.CameraUpdateFactory -import org.maplibre.android.maps.MapLibreMap -import org.maplibre.android.maps.Projection - -/** - * Create and [rememberSaveable] a [CameraPositionState] using [CameraPositionState.Saver]. - * [init] will be called when the [CameraPositionState] is first created to configure its - * initial state. - */ -@Composable -public inline fun rememberCameraPositionState( - crossinline init: CameraPositionState.() -> Unit = {} -): CameraPositionState = rememberSaveable(saver = CameraPositionState.Saver) { - CameraPositionState().apply(init) -} - -/** - * A state object that can be hoisted to control and observe the map's camera state. - * A [CameraPositionState] may only be used by a single [MapLibreMap] composable at a time - * as it reflects instance state for a single view of a map. - * - * @param position the initial camera position - * @param cameraMode the initial camera mode - */ -public class CameraPositionState( - position: CameraPosition = CameraPosition.Builder().build(), - cameraMode: CameraMode = CameraMode.NONE, -) { - /** - * Whether the camera is currently moving or not. This includes any kind of movement: - * panning, zooming, or rotation. - */ - public var isMoving: Boolean by mutableStateOf(false) - internal set - - /** - * The reason for the start of the most recent camera moment, or - * [CameraMoveStartedReason.NO_MOVEMENT_YET] if the camera hasn't moved yet or - * [CameraMoveStartedReason.UNKNOWN] if an unknown constant is received from the Maps SDK. - */ - public var cameraMoveStartedReason: CameraMoveStartedReason by mutableStateOf( - CameraMoveStartedReason.NO_MOVEMENT_YET - ) - internal set - - /** - * Returns the current [Projection] to be used for converting between screen - * coordinates and lat/lng. - */ - public val projection: Projection? - get() = map?.projection - - /** - * Local source of truth for the current camera position. - * While [map] is non-null this reflects the current position of [map] as it changes. - * While [map] is null it reflects the last known map position, or the last value set by - * explicitly setting [position]. - */ - internal var rawPosition by mutableStateOf(position) - - /** - * Current position of the camera on the map. - */ - public var position: CameraPosition - get() = rawPosition - set(value) { - synchronized(lock) { - val map = map - if (map == null) { - rawPosition = value - } else { - map.moveCamera(CameraUpdateFactory.newCameraPosition(value)) - } - } - } - - /** - * Local source of truth for the current camera mode. - * While [map] is non-null this reflects the current camera mode as it changes. - * While [map] is null it reflects the last known camera mode, or the last value set by - * explicitly setting [cameraMode]. - */ - internal var rawCameraMode by mutableStateOf(cameraMode) - - /** - * Current tracking mode of the camera. - */ - public var cameraMode: CameraMode - get() = rawCameraMode - set(value) { - synchronized(lock) { - val map = map - if (map == null) { - rawCameraMode = value - } else { - map.locationComponent.cameraMode = value.toInternal() - } - } - } - - /** - * The user's last available location. - */ - public var location: Location? by mutableStateOf(null) - internal set - - // Used to perform side effects thread-safely. - // Guards all mutable properties that are not `by mutableStateOf`. - private val lock = Unit - - // The map currently associated with this CameraPositionState. - // Guarded by `lock`. - private var map: MapLibreMap? by mutableStateOf(null) - - // The current map is set and cleared by side effect. - // There can be only one associated at a time. - internal fun setMap(map: MapLibreMap?) { - synchronized(lock) { - if (this.map == null && map == null) return - if (this.map != null && map != null) { - error("CameraPositionState may only be associated with one MapLibreMap at a time") - } - this.map = map - if (map == null) { - isMoving = false - } else { - map.moveCamera(CameraUpdateFactory.newCameraPosition(position)) - map.locationComponent.cameraMode = cameraMode.toInternal() - } - } - } - - public companion object { - /** - * The default saver implementation for [CameraPositionState]. - */ - public val Saver: Saver = Saver( - save = { SaveableCameraPositionData(it.position, it.cameraMode.toInternal()) }, - restore = { CameraPositionState(it.position, CameraMode.fromInternal(it.cameraMode)) } - ) - } -} - -/** Provides the [CameraPositionState] used by the map. */ -internal val LocalCameraPositionState = staticCompositionLocalOf { CameraPositionState() } - -/** The current [CameraPositionState] used by the map. */ -public val currentCameraPositionState: CameraPositionState - @[MapLibreMapComposable ReadOnlyComposable Composable] - get() = LocalCameraPositionState.current - -@Parcelize -public data class SaveableCameraPositionData( - val position: CameraPosition, - val cameraMode: Int -) : Parcelable diff --git a/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/IconAnchor.kt b/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/IconAnchor.kt deleted file mode 100644 index e46dcdcf65..0000000000 --- a/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/IconAnchor.kt +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (c) 2025 Element Creations Ltd. - * Copyright 2023-2025 New Vector Ltd. - * Copyright 2021 Google LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.libraries.maplibre.compose - -import androidx.compose.runtime.Immutable -import org.maplibre.android.style.layers.Property - -@Immutable -public enum class IconAnchor { - CENTER, - LEFT, - RIGHT, - TOP, - BOTTOM, - TOP_LEFT, - TOP_RIGHT, - BOTTOM_LEFT, - BOTTOM_RIGHT; - - @Property.ICON_ANCHOR - internal fun toInternal(): String = when (this) { - CENTER -> Property.ICON_ANCHOR_CENTER - LEFT -> Property.ICON_ANCHOR_LEFT - RIGHT -> Property.ICON_ANCHOR_RIGHT - TOP -> Property.ICON_ANCHOR_TOP - BOTTOM -> Property.ICON_ANCHOR_BOTTOM - TOP_LEFT -> Property.ICON_ANCHOR_TOP_LEFT - TOP_RIGHT -> Property.ICON_ANCHOR_TOP_RIGHT - BOTTOM_LEFT -> Property.ICON_ANCHOR_BOTTOM_LEFT - BOTTOM_RIGHT -> Property.ICON_ANCHOR_BOTTOM_RIGHT - } -} diff --git a/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/MapApplier.kt b/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/MapApplier.kt deleted file mode 100644 index f8fd64c537..0000000000 --- a/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/MapApplier.kt +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright (c) 2025 Element Creations Ltd. - * Copyright 2023-2025 New Vector Ltd. - * Copyright 2021 Google LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.libraries.maplibre.compose - -import androidx.compose.runtime.AbstractApplier -import org.maplibre.android.maps.MapLibreMap -import org.maplibre.android.maps.Style -import org.maplibre.android.plugins.annotation.SymbolManager - -internal interface MapNode { - fun onAttached() {} - fun onRemoved() {} - fun onCleared() {} -} - -private object MapNodeRoot : MapNode - -internal class MapApplier( - val map: MapLibreMap, - val style: Style, - val symbolManager: SymbolManager, -) : AbstractApplier(MapNodeRoot) { - private val decorations = mutableListOf() - - override fun onClear() { - symbolManager.deleteAll() - decorations.forEach { it.onCleared() } - decorations.clear() - } - - override fun insertBottomUp(index: Int, instance: MapNode) { - decorations.add(index, instance) - instance.onAttached() - } - - override fun insertTopDown(index: Int, instance: MapNode) { - // insertBottomUp is preferred - } - - override fun move(from: Int, to: Int, count: Int) { - decorations.move(from, to, count) - } - - override fun remove(index: Int, count: Int) { - repeat(count) { - decorations[index + it].onRemoved() - } - decorations.remove(index, count) - } -} diff --git a/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/MapLibreMap.kt b/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/MapLibreMap.kt deleted file mode 100644 index 62c29fbd04..0000000000 --- a/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/MapLibreMap.kt +++ /dev/null @@ -1,247 +0,0 @@ -/* - * Copyright (c) 2025 Element Creations Ltd. - * Copyright 2023-2025 New Vector Ltd. - * Copyright 2021 Google LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.libraries.maplibre.compose - -import android.content.ComponentCallbacks2 -import android.content.Context -import android.content.res.Configuration -import android.os.Bundle -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Composition -import androidx.compose.runtime.CompositionContext -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCompositionContext -import androidx.compose.runtime.rememberUpdatedState -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalInspectionMode -import androidx.compose.ui.viewinterop.AndroidView -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleEventObserver -import androidx.lifecycle.compose.LocalLifecycleOwner -import kotlinx.collections.immutable.ImmutableMap -import kotlinx.collections.immutable.persistentMapOf -import kotlinx.coroutines.awaitCancellation -import org.maplibre.android.MapLibre -import org.maplibre.android.maps.MapLibreMap -import org.maplibre.android.maps.MapView -import org.maplibre.android.maps.Style -import org.maplibre.android.plugins.annotation.SymbolManager -import kotlin.coroutines.resume -import kotlin.coroutines.suspendCoroutine - -/** - * A compose container for a MapLibre [MapView]. - * - * Heavily inspired by https://github.com/googlemaps/android-maps-compose - * - * @param styleUri a URI where to asynchronously fetch a style for the map - * @param modifier Modifier to be applied to the MapLibreMap - * @param images images added to the map's style to be later used with [Symbol] - * @param cameraPositionState the [CameraPositionState] to be used to control or observe the map's - * camera state - * @param uiSettings the [MapUiSettings] to be used for UI-specific settings on the map - * @param symbolManagerSettings the [MapSymbolManagerSettings] to be used for symbol manager settings - * @param locationSettings the [MapLocationSettings] to be used for location settings - * @param content the content of the map - */ -@Composable -public fun MapLibreMap( - styleUri: String, - modifier: Modifier = Modifier, - images: ImmutableMap = persistentMapOf(), - cameraPositionState: CameraPositionState = rememberCameraPositionState(), - uiSettings: MapUiSettings = DefaultMapUiSettings, - symbolManagerSettings: MapSymbolManagerSettings = DefaultMapSymbolManagerSettings, - locationSettings: MapLocationSettings = DefaultMapLocationSettings, - content: (@Composable @MapLibreMapComposable () -> Unit)? = null, -) { - // When in preview, early return a Box with the received modifier preserving layout - if (LocalInspectionMode.current) { - @Suppress("ModifierReused") // False positive, the modifier is not reused due to the early return. - Box( - modifier = modifier.background(Color.DarkGray) - ) { - Text("[Map]", modifier = Modifier.align(Alignment.Center)) - } - return - } - - val context = LocalContext.current - val mapView = remember { - MapLibre.getInstance(context) - MapView(context) - } - - @Suppress("ModifierReused") - AndroidView(modifier = modifier, factory = { mapView }) - MapLifecycle(mapView) - - // rememberUpdatedState and friends are used here to make these values observable to - // the subcomposition without providing a new content function each recomposition - val currentCameraPositionState by rememberUpdatedState(cameraPositionState) - val currentUiSettings by rememberUpdatedState(uiSettings) - val currentMapLocationSettings by rememberUpdatedState(locationSettings) - val currentSymbolManagerSettings by rememberUpdatedState(symbolManagerSettings) - - val parentComposition = rememberCompositionContext() - val currentContent by rememberUpdatedState(content) - - LaunchedEffect(styleUri, images) { - disposingComposition { - parentComposition.newComposition( - context = context, - mapView = mapView, - styleUri = styleUri, - images = images, - ) { - MapUpdater( - cameraPositionState = currentCameraPositionState, - uiSettings = currentUiSettings, - locationSettings = currentMapLocationSettings, - symbolManagerSettings = currentSymbolManagerSettings, - ) - CompositionLocalProvider( - LocalCameraPositionState provides cameraPositionState, - ) { - currentContent?.invoke() - } - } - } - } -} - -private suspend inline fun disposingComposition(factory: () -> Composition) { - val composition = factory() - try { - awaitCancellation() - } finally { - composition.dispose() - } -} - -private suspend inline fun CompositionContext.newComposition( - context: Context, - mapView: MapView, - styleUri: String, - images: ImmutableMap, - noinline content: @Composable () -> Unit -): Composition { - val map = mapView.awaitMap() - val style = map.awaitStyle(context, styleUri, images) - val symbolManager = SymbolManager(mapView, map, style) - return Composition( - MapApplier(map, style, symbolManager), - this - ).apply { - setContent(content) - } -} - -private suspend inline fun MapView.awaitMap(): MapLibreMap = suspendCoroutine { continuation -> - getMapAsync { map -> - continuation.resume(map) - } -} - -private suspend inline fun MapLibreMap.awaitStyle( - context: Context, - styleUri: String, - images: ImmutableMap, -): Style = suspendCoroutine { continuation -> - setStyle( - Style.Builder().apply { - fromUri(styleUri) - images.forEach { (id, drawableRes) -> - withImage(id, checkNotNull(context.getDrawable(drawableRes)) { - "Drawable resource $drawableRes with id $id not found" - }) - } - } - ) { style -> - continuation.resume(style) - } -} - -/** - * Registers lifecycle observers to the local [MapView]. - */ -@Composable -private fun MapLifecycle(mapView: MapView) { - val context = LocalContext.current - val lifecycle = LocalLifecycleOwner.current.lifecycle - val previousState = remember { mutableStateOf(Lifecycle.Event.ON_CREATE) } - DisposableEffect(context, lifecycle, mapView) { - val mapLifecycleObserver = mapView.lifecycleObserver(previousState) - val callbacks = mapView.componentCallbacks() - - lifecycle.addObserver(mapLifecycleObserver) - context.registerComponentCallbacks(callbacks) - - onDispose { - lifecycle.removeObserver(mapLifecycleObserver) - context.unregisterComponentCallbacks(callbacks) - } - } - DisposableEffect(mapView) { - onDispose { - mapView.onDestroy() - mapView.removeAllViews() - } - } -} - -private fun MapView.lifecycleObserver(previousState: MutableState): LifecycleEventObserver = - LifecycleEventObserver { _, event -> - event.targetState - when (event) { - Lifecycle.Event.ON_CREATE -> { - // Skip calling mapView.onCreate if the lifecycle did not go through onDestroy - in - // this case the MapLibreMap composable also doesn't leave the composition. So, - // recreating the map does not restore state properly which must be avoided. - if (previousState.value != Lifecycle.Event.ON_STOP) { - this.onCreate(Bundle()) - } - } - Lifecycle.Event.ON_START -> this.onStart() - Lifecycle.Event.ON_RESUME -> this.onResume() - Lifecycle.Event.ON_PAUSE -> this.onPause() - Lifecycle.Event.ON_STOP -> this.onStop() - Lifecycle.Event.ON_DESTROY -> { - // handled in onDispose - } - Lifecycle.Event.ON_ANY -> error("ON_ANY should never be used") - } - previousState.value = event - } - -private fun MapView.componentCallbacks(): ComponentCallbacks2 = - object : ComponentCallbacks2 { - override fun onConfigurationChanged(config: Configuration) = Unit - - @Suppress("OVERRIDE_DEPRECATION") - override fun onLowMemory() = Unit - - override fun onTrimMemory(level: Int) { - // We call the `MapView.onLowMemory` method for any memory trim level - this@componentCallbacks.onLowMemory() - } - } diff --git a/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/MapLibreMapComposable.kt b/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/MapLibreMapComposable.kt deleted file mode 100644 index c819dee711..0000000000 --- a/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/MapLibreMapComposable.kt +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (c) 2025 Element Creations Ltd. - * Copyright 2023-2025 New Vector Ltd. - * Copyright 2021 Google LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.libraries.maplibre.compose - -import androidx.compose.runtime.ComposableTargetMarker - -/** - * An annotation that can be used to mark a composable function as being expected to be use in a - * composable function that is also marked or inferred to be marked as a [MapLibreMapComposable]. - * - * This will produce build warnings when [MapLibreMapComposable] composable functions are used outside - * of a [MapLibreMapComposable] content lambda, and vice versa. - */ -@Retention(AnnotationRetention.BINARY) -@ComposableTargetMarker(description = "MapLibre Map Composable") -@Target( - AnnotationTarget.FILE, - AnnotationTarget.FUNCTION, - AnnotationTarget.PROPERTY_GETTER, - AnnotationTarget.TYPE, - AnnotationTarget.TYPE_PARAMETER, -) -public annotation class MapLibreMapComposable diff --git a/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/MapLocationSettings.kt b/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/MapLocationSettings.kt deleted file mode 100644 index 7fb777aeba..0000000000 --- a/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/MapLocationSettings.kt +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright (c) 2025 Element Creations Ltd. - * Copyright 2023-2025 New Vector Ltd. - * Copyright 2021 Google LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.libraries.maplibre.compose - -import androidx.compose.ui.graphics.Color - -internal val DefaultMapLocationSettings = MapLocationSettings() - -/** - * Data class for UI-related settings on the map. - * - * Note: Should not be a data class if in need of maintaining binary compatibility - * on future changes. See: https://jakewharton.com/public-api-challenges-in-kotlin/ - */ -public data class MapLocationSettings( - public val locationEnabled: Boolean = false, - public val backgroundTintColor: Color = Color.Unspecified, - public val foregroundTintColor: Color = Color.Unspecified, - public val backgroundStaleTintColor: Color = Color.Unspecified, - public val foregroundStaleTintColor: Color = Color.Unspecified, - public val accuracyColor: Color = Color.Unspecified, - public val pulseEnabled: Boolean = false, - public val pulseColor: Color = Color.Unspecified -) diff --git a/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/MapSymbolManagerSettings.kt b/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/MapSymbolManagerSettings.kt deleted file mode 100644 index 93c7b2118b..0000000000 --- a/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/MapSymbolManagerSettings.kt +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright (c) 2025 Element Creations Ltd. - * Copyright 2023-2025 New Vector Ltd. - * Copyright 2021 Google LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.libraries.maplibre.compose - -internal val DefaultMapSymbolManagerSettings = MapSymbolManagerSettings() - -/** - * Data class for UI-related settings on the map. - * - * Note: Should not be a data class if in need of maintaining binary compatibility - * on future changes. See: https://jakewharton.com/public-api-challenges-in-kotlin/ - */ -public data class MapSymbolManagerSettings( - public val iconAllowOverlap: Boolean = false, -) diff --git a/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/MapUiSettings.kt b/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/MapUiSettings.kt deleted file mode 100644 index edee9b4dc5..0000000000 --- a/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/MapUiSettings.kt +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (c) 2025 Element Creations Ltd. - * Copyright 2023-2025 New Vector Ltd. - * Copyright 2021 Google LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.libraries.maplibre.compose - -import android.view.Gravity -import androidx.compose.ui.graphics.Color - -internal val DefaultMapUiSettings = MapUiSettings() - -/** - * Data class for UI-related settings on the map. - * - * Note: Should not be a data class if in need of maintaining binary compatibility - * on future changes. See: https://jakewharton.com/public-api-challenges-in-kotlin/ - */ -public data class MapUiSettings( - public val compassEnabled: Boolean = true, - public val rotationGesturesEnabled: Boolean = true, - public val scrollGesturesEnabled: Boolean = true, - public val tiltGesturesEnabled: Boolean = true, - public val zoomGesturesEnabled: Boolean = true, - public val logoGravity: Int = Gravity.BOTTOM, - public val attributionGravity: Int = Gravity.BOTTOM, - public val attributionTintColor: Color = Color.Unspecified, -) diff --git a/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/MapUpdater.kt b/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/MapUpdater.kt deleted file mode 100644 index a07a596fe3..0000000000 --- a/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/MapUpdater.kt +++ /dev/null @@ -1,153 +0,0 @@ -/* - * Copyright (c) 2025 Element Creations Ltd. - * Copyright 2023-2025 New Vector Ltd. - * Copyright 2021 Google LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ -@file:Suppress("MatchingDeclarationName") - -package io.element.android.libraries.maplibre.compose - -import android.annotation.SuppressLint -import android.content.Context -import androidx.compose.runtime.Composable -import androidx.compose.runtime.ComposeNode -import androidx.compose.runtime.currentComposer -import androidx.compose.ui.graphics.toArgb -import androidx.compose.ui.platform.LocalContext -import org.maplibre.android.location.LocationComponentActivationOptions -import org.maplibre.android.location.LocationComponentOptions -import org.maplibre.android.location.OnCameraTrackingChangedListener -import org.maplibre.android.location.engine.LocationEngineRequest -import org.maplibre.android.maps.MapLibreMap -import org.maplibre.android.maps.Style - -private const val LOCATION_REQUEST_INTERVAL = 750L - -internal class MapPropertiesNode( - val map: MapLibreMap, - style: Style, - context: Context, - cameraPositionState: CameraPositionState, - locationSettings: MapLocationSettings, -) : MapNode { - init { - map.locationComponent.activateLocationComponent( - LocationComponentActivationOptions.Builder(context, style) - .locationComponentOptions( - LocationComponentOptions.builder(context) - .backgroundTintColor(locationSettings.backgroundTintColor.toArgb()) - .foregroundTintColor(locationSettings.foregroundTintColor.toArgb()) - .backgroundStaleTintColor(locationSettings.backgroundStaleTintColor.toArgb()) - .foregroundStaleTintColor(locationSettings.foregroundStaleTintColor.toArgb()) - .accuracyColor(locationSettings.accuracyColor.toArgb()) - .pulseEnabled(locationSettings.pulseEnabled) - .pulseColor(locationSettings.pulseColor.toArgb()) - .build() - ) - .locationEngineRequest( - LocationEngineRequest.Builder(LOCATION_REQUEST_INTERVAL) - .setPriority(LocationEngineRequest.PRIORITY_HIGH_ACCURACY) - .setFastestInterval(LOCATION_REQUEST_INTERVAL) - .build() - ) - .build() - ) - cameraPositionState.setMap(map) - } - - var cameraPositionState = cameraPositionState - set(value) { - if (value == field) return - field.setMap(null) - field = value - value.setMap(map) - } - - override fun onAttached() { - map.addOnCameraIdleListener { - cameraPositionState.isMoving = false - // addOnCameraIdleListener is only invoked when the camera position - // is changed via .animate(). To handle updating state when .move() - // is used, it's necessary to set the camera's position here as well - cameraPositionState.rawPosition = map.cameraPosition - // Updating user location on every camera move due to lack of a better location updates API. - cameraPositionState.location = map.locationComponent.lastKnownLocation - } - map.addOnCameraMoveCancelListener { - cameraPositionState.isMoving = false - } - map.addOnCameraMoveStartedListener { - cameraPositionState.cameraMoveStartedReason = CameraMoveStartedReason.fromInt(it) - cameraPositionState.isMoving = true - } - map.addOnCameraMoveListener { - cameraPositionState.rawPosition = map.cameraPosition - // Updating user location on every camera move due to lack of a better location updates API. - cameraPositionState.location = map.locationComponent.lastKnownLocation - } - map.locationComponent.addOnCameraTrackingChangedListener(object : OnCameraTrackingChangedListener { - override fun onCameraTrackingDismissed() {} - - override fun onCameraTrackingChanged(currentMode: Int) { - cameraPositionState.rawCameraMode = CameraMode.fromInternal(currentMode) - } - }) - } - - override fun onRemoved() { - cameraPositionState.setMap(null) - } - - override fun onCleared() { - cameraPositionState.setMap(null) - } -} - -/** - * Used to keep the primary map properties up to date. This should never leave the map composition. - */ -@SuppressLint("MissingPermission") -@Suppress("NOTHING_TO_INLINE") -@Composable -internal inline fun MapUpdater( - cameraPositionState: CameraPositionState, - locationSettings: MapLocationSettings, - uiSettings: MapUiSettings, - symbolManagerSettings: MapSymbolManagerSettings, -) { - val mapApplier = currentComposer.applier as MapApplier - val map = mapApplier.map - val style = mapApplier.style - val symbolManager = mapApplier.symbolManager - val context = LocalContext.current - ComposeNode( - factory = { - MapPropertiesNode( - map = map, - style = style, - context = context, - cameraPositionState = cameraPositionState, - locationSettings = locationSettings, - ) - }, - update = { - set(locationSettings.locationEnabled) { map.locationComponent.isLocationComponentEnabled = it } - - set(uiSettings.compassEnabled) { map.uiSettings.isCompassEnabled = it } - set(uiSettings.rotationGesturesEnabled) { map.uiSettings.isRotateGesturesEnabled = it } - set(uiSettings.scrollGesturesEnabled) { map.uiSettings.isScrollGesturesEnabled = it } - set(uiSettings.tiltGesturesEnabled) { map.uiSettings.isTiltGesturesEnabled = it } - set(uiSettings.zoomGesturesEnabled) { map.uiSettings.isZoomGesturesEnabled = it } - set(uiSettings.logoGravity) { map.uiSettings.logoGravity = it } - set(uiSettings.attributionGravity) { map.uiSettings.attributionGravity = it } - set(uiSettings.attributionTintColor) { map.uiSettings.setAttributionTintColor(it.toArgb()) } - - set(symbolManagerSettings.iconAllowOverlap) { symbolManager.iconAllowOverlap = it } - - update(cameraPositionState) { this.cameraPositionState = it } - } - ) -} diff --git a/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/Symbol.kt b/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/Symbol.kt deleted file mode 100644 index e6a5c3f632..0000000000 --- a/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/Symbol.kt +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright (c) 2025 Element Creations Ltd. - * Copyright 2023-2025 New Vector Ltd. - * Copyright 2021 Google LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.libraries.maplibre.compose - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.ComposeNode -import androidx.compose.runtime.currentComposer -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.Saver -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import org.maplibre.android.geometry.LatLng -import org.maplibre.android.plugins.annotation.Symbol -import org.maplibre.android.plugins.annotation.SymbolManager -import org.maplibre.android.plugins.annotation.SymbolOptions - -internal class SymbolNode( - val symbolManager: SymbolManager, - val symbol: Symbol, -) : MapNode { - override fun onRemoved() { - symbolManager.delete(symbol) - } - - override fun onCleared() { - symbolManager.delete(symbol) - } -} - -/** - * A state object that can be hoisted to control and observe the symbol state. - * - * @param position the initial symbol position - */ -public class SymbolState( - position: LatLng -) { - /** - * Current position of the symbol. - */ - public var position: LatLng by mutableStateOf(position) - - public companion object { - /** - * The default saver implementation for [SymbolState]. - */ - public val Saver: Saver = Saver( - save = { it.position }, - restore = { SymbolState(it) } - ) - } -} - -@Composable -public fun rememberSymbolState( - position: LatLng = LatLng(0.0, 0.0) -): SymbolState = rememberSaveable(saver = SymbolState.Saver) { - SymbolState(position) -} - -/** - * A composable for a symbol on the map. - * - * @param iconId an id of an image from the current [Style] - * @param state the [SymbolState] to be used to control or observe the symbol - * state such as its position and info window - * @param iconAnchor the anchor for the symbol image - */ -@Composable -@MapLibreMapComposable -public fun Symbol( - iconId: String, - state: SymbolState = rememberSymbolState(), - iconAnchor: IconAnchor? = null, -) { - val mapApplier = currentComposer.applier as MapApplier - val symbolManager = mapApplier.symbolManager - ComposeNode( - factory = { - SymbolNode( - symbolManager = symbolManager, - symbol = symbolManager.create( - SymbolOptions().apply { - withLatLng(state.position) - withIconImage(iconId) - iconAnchor?.let { withIconAnchor(it.toInternal()) } - } - ), - ) - }, - update = { - update(state.position) { - this.symbol.latLng = it - symbolManager.update(this.symbol) - } - update(iconId) { - this.symbol.iconImage = it - symbolManager.update(this.symbol) - } - update(iconAnchor) { - this.symbol.iconAnchor = it?.toInternal() - symbolManager.update(this.symbol) - } - } - ) -}