Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import com.google.accompanist.permissions.ExperimentalPermissionsApi
Expand All @@ -16,13 +17,16 @@ import com.what3words.components.compose.maps.models.W3WLocationSource
import com.what3words.components.compose.maps.models.W3WMapType
import com.what3words.components.compose.maps.providers.googlemap.W3WGoogleMap
import com.what3words.components.compose.maps.providers.mapbox.W3WMapBox
import com.what3words.components.compose.maps.state.W3WButtonsState
import com.what3words.components.compose.maps.state.W3WMapState
import com.what3words.components.compose.maps.state.camera.W3WCameraState
import com.what3words.core.types.common.W3WError
import com.what3words.core.types.geometry.W3WCoordinates
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

/**
* A composable function that displays a What3Words (W3W) map.
Expand Down Expand Up @@ -50,6 +54,8 @@ fun W3WMapComponent(
) {

val mapState by mapManager.mapState.collectAsState()
val buttonState by mapManager.buttonState.collectAsState()
val coroutineScope = rememberCoroutineScope { Dispatchers.IO }

W3WMapContent(
modifier = modifier,
Expand All @@ -58,6 +64,7 @@ fun W3WMapComponent(
mapProvider = mapManager.mapProvider,
content = content,
mapState = mapState,
buttonState = buttonState,
onMapTypeClicked = {
mapManager.setMapType(it)
mapManager.orientCamera()
Expand All @@ -72,7 +79,8 @@ fun W3WMapComponent(
fetchCurrentLocation(
locationSource = locationSource,
mapManager = mapManager,
onError = onError
onError = onError,
coroutineScope = coroutineScope
)
},
onError = onError
Expand All @@ -88,10 +96,12 @@ fun W3WMapComponent(
* @param layoutConfig [W3WMapDefaults.LayoutConfig] Configuration for the map's layout.
* @param mapConfig [W3WMapDefaults.MapConfig] Configuration for the map's appearance.
* @param mapState The [W3WMapState] object that holds the mapState of the map.
* @param buttonState The [W3WButtonsState] object that holds the buttonState of the map.
* @param mapProvider An instance of enum [MapProvider] to define map provide: GoogleMap, MapBox.
* @param content Optional composable content to be displayed on the map.
* @param onMapTypeClicked Callback invoked when the map type is clicked.
* @param onMapClicked Callback invoked when the map is clicked.
* @param onMyLocationClicked Callback invoked when the my location button is clicked.
* @param onCameraUpdated Callback invoked when the camera position is updated.
* @param onError Callback invoked when an error occurs.
*/
Expand All @@ -101,6 +111,7 @@ fun W3WMapComponent(
layoutConfig: W3WMapDefaults.LayoutConfig = W3WMapDefaults.defaultLayoutConfig(),
mapConfig: W3WMapDefaults.MapConfig = W3WMapDefaults.defaultMapConfig(),
mapState: W3WMapState,
buttonState: W3WButtonsState,
mapProvider: MapProvider,
content: (@Composable () -> Unit)? = null,
onMapTypeClicked: ((W3WMapType) -> Unit)? = null,
Expand All @@ -116,6 +127,7 @@ fun W3WMapComponent(
mapProvider = mapProvider,
content = content,
mapState = mapState,
buttonState = buttonState,
onMapClicked = {
onMapClicked?.invoke(it)
},
Expand All @@ -142,9 +154,11 @@ fun W3WMapComponent(
* @param layoutConfig [W3WMapDefaults.LayoutConfig] Configuration for the map's layout.
* @param mapConfig [W3WMapDefaults.MapConfig] Configuration for the map's appearance.
* @param mapState The [W3WMapState] object that holds the mapState of the map.
* @param buttonState The [W3WButtonsState] object that holds the buttonState of the map.
* @param mapProvider An instance of enum [MapProvider] to define map provide: GoogleMap, MapBox.
* @param content Optional composable content to be displayed on the map.
* @param onMapTypeClicked Callback invoked when the user clicks on the map type button.
* @param onMyLocationClicked Callback invoked when the user clicks on the my location button.
* @param onMapClicked Callback invoked when the user clicks on the map.
* @param onCameraUpdated Callback invoked when the camera position is updated.
* @param onError Callback invoked when an error occurs during map initialization or interaction.
Expand All @@ -155,6 +169,7 @@ internal fun W3WMapContent(
layoutConfig: W3WMapDefaults.LayoutConfig = W3WMapDefaults.defaultLayoutConfig(),
mapConfig: W3WMapDefaults.MapConfig = W3WMapDefaults.defaultMapConfig(),
mapState: W3WMapState,
buttonState: W3WButtonsState,
mapProvider: MapProvider,
content: (@Composable () -> Unit)? = null,
onMapTypeClicked: ((W3WMapType) -> Unit),
Expand Down Expand Up @@ -192,7 +207,11 @@ internal fun W3WMapContent(
.align(Alignment.BottomEnd)
.padding(layoutConfig.contentPadding),
onMyLocationClicked = onMyLocationClicked,
mapConfig = mapConfig,
onMapTypeClicked = onMapTypeClicked,
isLocationEnabled = mapState.isMyLocationEnabled,
accuracyDistance = buttonState.accuracyDistance,
isLocationActive = buttonState.isLocationActive,
)
}
}
Expand Down Expand Up @@ -312,21 +331,30 @@ internal fun W3WMapView(
private fun fetchCurrentLocation(
locationSource: W3WLocationSource?,
mapManager: W3WMapManager,
coroutineScope: CoroutineScope,
onError: ((W3WError) -> Unit)? = null
) {
locationSource?.let {
CoroutineScope(IO).launch {
coroutineScope.launch {
try {
val location = it.fetchLocation()
// Update camera state
mapManager.moveToPosition(
coordinates = W3WCoordinates(location.latitude, location.longitude),
animate = true
)
//TODO: Update button state
withContext(Main) {
mapManager.moveToPosition(
coordinates = W3WCoordinates(location.latitude, location.longitude),
animate = true
)
}

if (location.hasAccuracy()) {
mapManager.updateAccuracyDistance(location.accuracy)
}
} catch (e: Exception) {
onError?.invoke(W3WError("Location fetch failed: ${e.message}"))
}
it.isActive.collect { isActive ->
mapManager.updateIsLocationActive(isActive)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ object W3WMapDefaults {
data class MapConfig(
val darkModeCustomJsonStyle: String = DarkModeStyle.darkMode,
// Grid view
val gridLineConfig: GridLinesConfig
val gridLineConfig: GridLinesConfig,
// Buttons
val buttonConfig: ButtonConfig
)

data class GridLinesConfig(
Expand All @@ -32,6 +34,12 @@ object W3WMapDefaults {
val contentPadding: PaddingValues
)

data class ButtonConfig(
val isMapSwitchButtonEnabled: Boolean,
val isMyLocationButtonEnabled: Boolean,
val isRecallButtonEnabled: Boolean,
)

@Composable
fun defaultLayoutConfig(
contentPadding: PaddingValues = PaddingValues(bottom = 24.dp, end = 8.dp)
Expand All @@ -43,11 +51,13 @@ object W3WMapDefaults {

fun defaultMapConfig(
darkModeCustomJsonStyle: String = DarkModeStyle.darkMode,
gridLineConfig: GridLinesConfig = defaultGridLinesConfig()
gridLineConfig: GridLinesConfig = defaultGridLinesConfig(),
buttonConfig: ButtonConfig = defaultButtonConfig()
): MapConfig {
return MapConfig(
darkModeCustomJsonStyle = darkModeCustomJsonStyle,
gridLineConfig = gridLineConfig
gridLineConfig = gridLineConfig,
buttonConfig = buttonConfig
)
}

Expand All @@ -63,4 +73,16 @@ object W3WMapDefaults {
)
}

fun defaultButtonConfig(
isMapSwitchButtonEnabled: Boolean = true,
isMyLocationButtonEnable: Boolean = true,
isRecallButtonEnable: Boolean = false
): ButtonConfig {
return ButtonConfig(
isMapSwitchButtonEnabled = isMapSwitchButtonEnabled,
isMyLocationButtonEnabled = isMyLocationButtonEnable,
isRecallButtonEnabled = isRecallButtonEnable
)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import com.what3words.components.compose.maps.models.W3WMapType
import com.what3words.components.compose.maps.models.W3WMarker
import com.what3words.components.compose.maps.models.W3WMarkerColor
import com.what3words.components.compose.maps.models.W3WZoomOption
import com.what3words.components.compose.maps.state.W3WButtonsState
import com.what3words.components.compose.maps.state.W3WMapState
import com.what3words.components.compose.maps.state.addOrUpdateMarker
import com.what3words.components.compose.maps.state.camera.W3WCameraState
Expand Down Expand Up @@ -60,13 +61,17 @@ class W3WMapManager(
private val textDataSource: W3WTextDataSource,
private val dispatcher: CoroutineDispatcher = Dispatchers.IO,
val mapProvider: MapProvider,
mapState: W3WMapState = W3WMapState()
mapState: W3WMapState = W3WMapState(),
buttonState: W3WButtonsState = W3WButtonsState(),
) {
private var language: W3WRFC5646Language = W3WRFC5646Language.EN_GB

private val _mapState: MutableStateFlow<W3WMapState> = MutableStateFlow(mapState)
val mapState: StateFlow<W3WMapState> = _mapState.asStateFlow()

private val _buttonState: MutableStateFlow<W3WButtonsState> = MutableStateFlow(buttonState)
val buttonState: StateFlow<W3WButtonsState> = _buttonState.asStateFlow()

init {
if (mapProvider == MapProvider.MAPBOX) {
_mapState.update {
Expand Down Expand Up @@ -302,6 +307,20 @@ class W3WMapManager(
}
}

// Button state region

fun updateAccuracyDistance(distance: Float) {
_buttonState.update {
it.copy(accuracyDistance = distance)
}
}

fun updateIsLocationActive(isActive: Boolean) {
_buttonState.update {
it.copy(isLocationActive = isActive)
}
}

companion object {
val CAMERA_POSITION_DEFAULT =
W3WCameraPosition(W3WCoordinates(51.521251, -0.203586), 19f, 0f, 0f)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,23 @@
package com.what3words.components.compose.maps.buttons

import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.LocationSearching
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.OutlinedButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import com.what3words.components.compose.maps.W3WMapDefaults
import com.what3words.components.compose.maps.buttons.findmylocation.W3WFindMyLocationButton
import com.what3words.components.compose.maps.buttons.mapswitch.W3WMapSwitchButton
import com.what3words.components.compose.maps.models.W3WMapType

@Composable
fun W3WMapButtons(
modifier: Modifier = Modifier,
mapConfig: W3WMapDefaults.MapConfig,
isLocationEnabled: Boolean,
isLocationActive: Boolean,
accuracyDistance: Float,
onMyLocationClicked: (() -> Unit),
onMapTypeClicked: ((W3WMapType) -> Unit),
) {
Expand All @@ -30,19 +26,21 @@ fun W3WMapButtons(
verticalArrangement = Arrangement.spacedBy(20.dp),
horizontalAlignment = Alignment.End
) {
OutlinedButton(
onClick = { onMyLocationClicked.invoke() },
modifier = Modifier.size(50.dp),
shape = CircleShape,
border = BorderStroke(1.dp, Color.Blue),
contentPadding = PaddingValues(0.dp), //avoid the little icon
colors = ButtonDefaults.outlinedButtonColors(contentColor = Color.Blue)
) {
Icon(Icons.Default.LocationSearching, contentDescription = "content description")
if (mapConfig.buttonConfig.isRecallButtonEnabled) {
//TODO: Add recall button
}

W3WMapSwitchButton {
onMapTypeClicked(it)
if (mapConfig.buttonConfig.isMyLocationButtonEnabled) {
W3WFindMyLocationButton(
accuracyDistance = accuracyDistance.toInt(),
isLocationEnabled = isLocationEnabled,
isLocationActive = isLocationActive,
onMyLocationClicked = onMyLocationClicked
)
}
if (mapConfig.buttonConfig.isMapSwitchButtonEnabled){
W3WMapSwitchButton {
onMapTypeClicked(it)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.what3words.components.compose.maps.buttons

import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.animation.core.tween
import androidx.compose.animation.expandHorizontally
import androidx.compose.animation.shrinkHorizontally
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp

object W3WMapButtonsDefault {

data class LocationButtonConfig(
val enterAnimation: EnterTransition,
val exitAnimation: ExitTransition,
val locationButtonSize: Dp,
val locationIconSize: Dp,
val accuracyIndicatorSize: Dp,
val accuracyTextStyle: TextStyle,
)

@Composable
fun defaultLocationButtonConfig(
enterAnimation: EnterTransition = expandHorizontally(
animationSpec = tween(durationMillis = 1000),
expandFrom = Alignment.Start
),
exitAnimation: ExitTransition = shrinkHorizontally(
animationSpec = tween(durationMillis = 1000),
shrinkTowards = Alignment.Start
),
locationButtonSize: Dp = 50.dp,
locationIconSize: Dp = 30.dp,
accuracyIndicatorSize: Dp = 20.dp,
accuracyTextStyle: TextStyle = MaterialTheme.typography.labelSmall
): LocationButtonConfig {
return LocationButtonConfig(
enterAnimation = enterAnimation,
exitAnimation = exitAnimation,
locationButtonSize = locationButtonSize,
accuracyIndicatorSize = accuracyIndicatorSize,
locationIconSize = locationIconSize,
accuracyTextStyle = accuracyTextStyle
)
}
}
Loading