diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/ui/LocationPinMarkers.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/ui/LocationPinMarkers.kt index 7fce6ab281..762a719cf4 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/ui/LocationPinMarkers.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/ui/LocationPinMarkers.kt @@ -36,6 +36,8 @@ import org.maplibre.spatialk.geojson.Point import org.maplibre.spatialk.geojson.Position import org.maplibre.spatialk.geojson.toJson +private const val LOCATION_MARKER_ID = "LOCATION_MARKER_ID" + /** * Data class representing a marker on the map. * @@ -81,7 +83,7 @@ fun LocationPinMarkers( id = JsonPrimitive(marker.id), geometry = Point(Position(marker.location.lon, marker.location.lat)), properties = mapOf( - "id" to JsonPrimitive(marker.id), + LOCATION_MARKER_ID to JsonPrimitive(marker.id), ) ) } @@ -153,7 +155,7 @@ private fun LocationPinMarkerLayer( SymbolLayer( id = "pin-marker-${marker.id}", source = source, - filter = !feature.has("point_count") and (feature["id"].asString() eq const(marker.id)), + filter = !feature.has("point_count") and (feature[LOCATION_MARKER_ID].asString() eq const(marker.id)), iconImage = image(imageBitmap), iconAnchor = const(SymbolAnchor.Bottom), iconAllowOverlap = const(true), diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationPresenter.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationPresenter.kt index f402b8fac9..9bbfe35b83 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationPresenter.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationPresenter.kt @@ -24,8 +24,13 @@ import io.element.android.features.location.impl.common.actions.LocationActions import io.element.android.features.location.impl.common.permissions.PermissionsEvents import io.element.android.features.location.impl.common.permissions.PermissionsPresenter import io.element.android.features.location.impl.common.permissions.PermissionsState +import io.element.android.features.location.impl.common.ui.LocationMarkerData import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.meta.BuildMeta +import io.element.android.libraries.designsystem.components.PinVariant +import io.element.android.libraries.designsystem.components.avatar.AvatarData +import io.element.android.libraries.designsystem.components.avatar.AvatarSize +import io.element.android.libraries.matrix.api.room.location.AssetType @AssistedInject class ShowLocationPresenter( @@ -88,9 +93,38 @@ class ShowLocationPresenter( } } + val markers = remember(mode) { + when (mode) { + is ShowLocationMode.Static -> { + val pinVariant = if (mode.assetType == AssetType.PIN) { + PinVariant.PinnedLocation + } else { + PinVariant.UserLocation( + avatarData = AvatarData( + id = mode.senderId.value, + name = mode.senderName, + url = mode.senderAvatarUrl, + size = AvatarSize.UserListItem, + ), + isLive = false, + ) + } + listOf( + LocationMarkerData( + id = mode.senderId.value, + location = mode.location, + variant = pinVariant, + ) + ) + } + ShowLocationMode.Live -> emptyList() + } + } + return ShowLocationState( permissionDialog = permissionDialog, mode = mode, + markers = markers, hasLocationPermission = permissionsState.isAnyGranted, isTrackMyLocation = isTrackMyLocation, appName = appName, diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationState.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationState.kt index 4eefa34053..ec29cd7c54 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationState.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationState.kt @@ -9,10 +9,12 @@ package io.element.android.features.location.impl.show import io.element.android.features.location.api.ShowLocationMode +import io.element.android.features.location.impl.common.ui.LocationMarkerData data class ShowLocationState( val permissionDialog: Dialog, val mode: ShowLocationMode, + val markers: List, val hasLocationPermission: Boolean, val isTrackMyLocation: Boolean, val appName: String, diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationStateProvider.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationStateProvider.kt index 4941d8984d..944645fa1f 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationStateProvider.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationStateProvider.kt @@ -11,6 +11,10 @@ package io.element.android.features.location.impl.show import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.location.api.Location import io.element.android.features.location.api.ShowLocationMode +import io.element.android.features.location.impl.common.ui.LocationMarkerData +import io.element.android.libraries.designsystem.components.PinVariant +import io.element.android.libraries.designsystem.components.avatar.AvatarData +import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.location.AssetType @@ -50,18 +54,44 @@ class ShowLocationStateProvider : PreviewParameterProvider { fun aShowLocationState( permissionDialog: ShowLocationState.Dialog = ShowLocationState.Dialog.None, mode: ShowLocationMode = aStaticLocationMode(), + markers: List? = null, hasLocationPermission: Boolean = false, isTrackMyLocation: Boolean = false, appName: String = APP_NAME, eventSink: (ShowLocationEvents) -> Unit = {}, -) = ShowLocationState( - permissionDialog = permissionDialog, - mode = mode, - hasLocationPermission = hasLocationPermission, - isTrackMyLocation = isTrackMyLocation, - appName = appName, - eventSink = eventSink, -) +): ShowLocationState { + val effectiveMarkers = markers ?: when (mode) { + is ShowLocationMode.Static -> listOf( + LocationMarkerData( + id = mode.senderId.value, + location = mode.location, + variant = if (mode.assetType == AssetType.PIN) { + PinVariant.PinnedLocation + } else { + PinVariant.UserLocation( + avatarData = AvatarData( + id = mode.senderId.value, + name = mode.senderName, + url = mode.senderAvatarUrl, + size = AvatarSize.UserListItem, + ), + isLive = true, + ) + } + ) + ) + ShowLocationMode.Live -> emptyList() + } + return ShowLocationState( + permissionDialog = permissionDialog, + mode = mode, + markers = effectiveMarkers, + hasLocationPermission = hasLocationPermission, + isTrackMyLocation = isTrackMyLocation, + appName = appName, + eventSink = eventSink, + ) +} fun aStaticLocationMode( location: Location = Location(1.23, 2.34, 4f), diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationView.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationView.kt index d1a3537d7a..8839dca52f 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationView.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationView.kt @@ -15,33 +15,26 @@ import androidx.compose.material3.rememberBottomSheetScaffoldState import androidx.compose.material3.rememberStandardBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.compound.tokens.generated.CompoundIcons -import io.element.android.features.location.api.Location import io.element.android.features.location.api.ShowLocationMode import io.element.android.features.location.impl.common.MapDefaults import io.element.android.features.location.impl.common.PermissionDeniedDialog import io.element.android.features.location.impl.common.PermissionRationaleDialog import io.element.android.features.location.impl.common.ui.LocationFloatingActionButton -import io.element.android.features.location.impl.common.ui.LocationMarkerData import io.element.android.features.location.impl.common.ui.LocationPinMarkers import io.element.android.features.location.impl.common.ui.MapBottomSheetScaffold import io.element.android.features.location.impl.common.ui.UserLocationPuck -import io.element.android.libraries.designsystem.components.PinVariant -import io.element.android.libraries.designsystem.components.avatar.AvatarData -import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.IconButton import io.element.android.libraries.designsystem.theme.components.TopAppBar -import io.element.android.libraries.matrix.api.room.location.AssetType import io.element.android.libraries.ui.strings.CommonStrings import org.maplibre.compose.camera.CameraMoveReason import org.maplibre.compose.camera.CameraPosition @@ -51,10 +44,6 @@ import org.maplibre.compose.location.rememberDefaultLocationProvider import org.maplibre.compose.location.rememberNullLocationProvider import org.maplibre.compose.location.rememberUserLocationState import org.maplibre.spatialk.geojson.Position -import kotlin.math.cos -import kotlin.math.sin -import kotlin.math.sqrt -import kotlin.random.Random import kotlin.time.Duration.Companion.minutes @OptIn(ExperimentalMaterial3Api::class) @@ -105,7 +94,7 @@ fun ShowLocationView( } val scaffoldState = rememberBottomSheetScaffoldState( - bottomSheetState = rememberStandardBottomSheetState(skipHiddenState = false, initialValue = SheetValue.Hidden) + bottomSheetState = rememberStandardBottomSheetState(initialValue = SheetValue.PartiallyExpanded) ) MapBottomSheetScaffold( scaffoldState = scaffoldState, @@ -137,69 +126,7 @@ fun ShowLocationView( locationState = userLocationState, trackUserLocation = state.isTrackMyLocation ) - when (val mode = state.mode) { - is ShowLocationMode.Static -> { - val pinVariant = if (mode.assetType == AssetType.PIN) { - PinVariant.PinnedLocation - } else { - PinVariant.UserLocation( - avatarData = AvatarData(mode.senderId.value, mode.senderName, mode.senderAvatarUrl, AvatarSize.UserListItem), - isLive = true - ) - } - // Generate test markers around the original location - val testMarkers = remember { - buildList { - // Add the original marker - add( - LocationMarkerData( - id = "original", - location = mode.location, - variant = pinVariant - ) - ) - // Generate 10 random points within 50 meters - val radiusInMeters = 50.0 - val metersPerDegreeLat = 111_320.0 - val metersPerDegreeLon = 111_320.0 * cos(Math.toRadians(mode.location.lat)) - val variants = listOf( - PinVariant.StaleLocation, - PinVariant.UserLocation(AvatarData("@alice", "Alice", null, AvatarSize.TimelineSender), isLive = true), - PinVariant.UserLocation(AvatarData("@bob", "Bob", null, AvatarSize.TimelineSender), isLive = true), - PinVariant.UserLocation(AvatarData("@cassy", "Cassy", null, AvatarSize.TimelineSender), isLive = true), - PinVariant.UserLocation(AvatarData("@daisy", "Daisy", null, AvatarSize.TimelineSender), isLive = true), - PinVariant.UserLocation(AvatarData("@en", "G", null, AvatarSize.TimelineSender), isLive = true), - PinVariant.UserLocation(AvatarData("@f", "H", null, AvatarSize.TimelineSender), isLive = true), - PinVariant.UserLocation(AvatarData("@g", "I", null, AvatarSize.TimelineSender), isLive = true), - PinVariant.UserLocation(AvatarData("@h", "J", null, AvatarSize.TimelineSender), isLive = true), - PinVariant.UserLocation(AvatarData("@i", "K", null, AvatarSize.TimelineSender), isLive = true), - ) - repeat(10) { index -> - // Random point in a circle using sqrt for uniform distribution - val angle = Random.nextDouble() * 2 * Math.PI - val distance = sqrt(Random.nextDouble()) * radiusInMeters - val latOffset = (distance * cos(angle)) / metersPerDegreeLat - val lonOffset = (distance * sin(angle)) / metersPerDegreeLon - add( - LocationMarkerData( - id = "test_$index", - location = Location( - lat = mode.location.lat + latOffset, - lon = mode.location.lon + lonOffset - ), - variant = variants[index % (variants.size-1)] - ) - ) - } - } - } - LocationPinMarkers(testMarkers) - } - ShowLocationMode.Live -> { - // TODO: Show pins for all active live location sharers - } - } - + LocationPinMarkers(state.markers) }, overlayContent = { LocationFloatingActionButton(