From 5410b61ae32b9d645b789ecbbd79e54a037fbbfc Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 12 May 2022 09:27:01 +0200 Subject: [PATCH] Show user pins with correct zoom when map is first opened --- .../map/GetCurrentUserLiveLocationUseCase.kt | 49 ----------- .../map/GetListOfUserLiveLocationUseCase.kt | 66 ++++++++++++++ .../live/map/LocationLiveMapViewFragment.kt | 87 ++++++++++++++++++- .../live/map/LocationLiveMapViewModel.kt | 4 +- .../live/map/LocationLiveMapViewState.kt | 2 + 5 files changed, 155 insertions(+), 53 deletions(-) delete mode 100644 vector/src/main/java/im/vector/app/features/location/live/map/GetCurrentUserLiveLocationUseCase.kt create mode 100644 vector/src/main/java/im/vector/app/features/location/live/map/GetListOfUserLiveLocationUseCase.kt diff --git a/vector/src/main/java/im/vector/app/features/location/live/map/GetCurrentUserLiveLocationUseCase.kt b/vector/src/main/java/im/vector/app/features/location/live/map/GetCurrentUserLiveLocationUseCase.kt deleted file mode 100644 index 3f12fb9181..0000000000 --- a/vector/src/main/java/im/vector/app/features/location/live/map/GetCurrentUserLiveLocationUseCase.kt +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (c) 2022 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package im.vector.app.features.location.live.map - -import im.vector.app.features.location.LocationData -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow -import javax.inject.Inject - -class GetCurrentUserLiveLocationUseCase @Inject constructor() { - - // TODO add unit tests - fun execute(): Flow> { - // TODO get room and call SDK to get the correct flow - return flow { - val user1 = UserLiveLocationViewState( - userId = "user1", - locationData = LocationData( - latitude = 48.863447, - longitude = 2.328608, - uncertainty = null - ) - ) - val user2 = UserLiveLocationViewState( - userId = "user2", - locationData = LocationData( - latitude = 48.843816, - longitude = 2.359235, - uncertainty = null - ) - ) - emit(listOf(user1, user2)) - } - } -} diff --git a/vector/src/main/java/im/vector/app/features/location/live/map/GetListOfUserLiveLocationUseCase.kt b/vector/src/main/java/im/vector/app/features/location/live/map/GetListOfUserLiveLocationUseCase.kt new file mode 100644 index 0000000000..2505ba11a4 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/location/live/map/GetListOfUserLiveLocationUseCase.kt @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.location.live.map + +import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider +import im.vector.app.features.location.LocationData +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.channels.trySendBlocking +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import org.matrix.android.sdk.api.session.Session +import javax.inject.Inject + +class GetListOfUserLiveLocationUseCase @Inject constructor( + private val session: Session, + private val locationPinProvider: LocationPinProvider +) { + + // TODO add unit tests + fun execute(): Flow> { + // TODO get room and call SDK to get the correct flow of locations + + return callbackFlow { + val myUserId = session.myUserId + + locationPinProvider.create(myUserId) { pinDrawable -> + val user1 = UserLiveLocationViewState( + userId = session.myUserId, + pinDrawable = pinDrawable, + locationData = LocationData( + latitude = 48.863447, + longitude = 2.328608, + uncertainty = null + ) + ) + val user2 = UserLiveLocationViewState( + userId = session.myUserId, + pinDrawable = pinDrawable, + locationData = LocationData( + latitude = 48.843816, + longitude = 2.359235, + uncertainty = null + ) + ) + val userLocations = listOf(user1, user2) + trySendBlocking(userLocations) + channel.close() + } + awaitClose() + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewFragment.kt b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewFragment.kt index 32b87727d8..8ba4cdb5d2 100644 --- a/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewFragment.kt +++ b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewFragment.kt @@ -16,20 +16,34 @@ package im.vector.app.features.location.live.map +import android.graphics.drawable.Drawable import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.lifecycle.lifecycleScope import com.airbnb.mvrx.args +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState +import com.mapbox.mapboxsdk.camera.CameraPosition +import com.mapbox.mapboxsdk.constants.MapboxConstants +import com.mapbox.mapboxsdk.geometry.LatLng +import com.mapbox.mapboxsdk.geometry.LatLngBounds +import com.mapbox.mapboxsdk.maps.MapView +import com.mapbox.mapboxsdk.maps.MapboxMap import com.mapbox.mapboxsdk.maps.MapboxMapOptions +import com.mapbox.mapboxsdk.maps.Style import com.mapbox.mapboxsdk.maps.SupportMapFragment import dagger.hilt.android.AndroidEntryPoint +import com.mapbox.mapboxsdk.plugins.annotation.SymbolManager +import com.mapbox.mapboxsdk.plugins.annotation.SymbolOptions +import com.mapbox.mapboxsdk.style.layers.Property import im.vector.app.R import im.vector.app.core.extensions.addChildFragment import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.FragmentSimpleContainerBinding import im.vector.app.features.location.UrlMapProvider +import java.lang.ref.WeakReference import javax.inject.Inject /** @@ -43,6 +57,14 @@ class LocationLiveMapViewFragment : VectorBaseFragment? = null + private var symbolManager: SymbolManager? = null + private var mapStyle: Style? = null + private val pendingLiveLocations = mutableListOf() + private var isMapFirstUpdate = true + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentSimpleContainerBinding { return FragmentSimpleContainerBinding.inflate(layoutInflater, container, false) } @@ -55,9 +77,70 @@ class LocationLiveMapViewFragment : VectorBaseFragment + mapFragment.getMapAsync { mapboxMap -> lifecycleScope.launchWhenCreated { - mapBoxMap.setStyle(urlMapProvider.getMapUrl()) + mapboxMap.setStyle(urlMapProvider.getMapUrl()) { style -> + mapStyle = style + this@LocationLiveMapViewFragment.mapboxMap = WeakReference(mapboxMap) + symbolManager = SymbolManager(mapFragment.view as MapView, mapboxMap, style) + pendingLiveLocations + .takeUnless { it.isEmpty() } + ?.let { updateMap(it) } + } + } + } + } + + override fun invalidate() = withState(viewModel) { viewState -> + updateMap(viewState.userLocations) + } + + private fun updateMap(userLiveLocations: List) { + symbolManager?.let { + it.deleteAll() + + val latLngBoundsBuilder = LatLngBounds.Builder() + userLiveLocations.forEach { userLocation -> + addUserPinToMapStyle(userLocation.userId, userLocation.pinDrawable) + val symbolOptions = buildSymbolOptions(userLocation) + it.create(symbolOptions) + + if (isMapFirstUpdate) { + latLngBoundsBuilder.include(LatLng(userLocation.locationData.latitude, userLocation.locationData.longitude)) + } + } + + if (isMapFirstUpdate) { + isMapFirstUpdate = false + zoomToViewAllUsers(latLngBoundsBuilder.build()) + } + } ?: run { + pendingLiveLocations.clear() + pendingLiveLocations.addAll(userLiveLocations) + } + } + + private fun addUserPinToMapStyle(userId: String, userPinDrawable: Drawable) { + mapStyle?.let { style -> + if (style.getImage(userId) == null) { + style.addImage(userId, userPinDrawable) + } + } + } + + private fun buildSymbolOptions(userLiveLocation: UserLiveLocationViewState) = + SymbolOptions() + .withLatLng(LatLng(userLiveLocation.locationData.latitude, userLiveLocation.locationData.longitude)) + .withIconImage(userLiveLocation.userId) + .withIconAnchor(Property.ICON_ANCHOR_BOTTOM) + + private fun zoomToViewAllUsers(latLngBounds: LatLngBounds) { + mapboxMap?.get()?.let { mapboxMap -> + mapboxMap.getCameraForLatLngBounds(latLngBounds)?.let { cameraPosition -> + // update the zoom a little to avoid having pins exactly at the edges of the map + mapboxMap.cameraPosition = CameraPosition.Builder(cameraPosition) + .zoom((cameraPosition.zoom - 1).coerceAtLeast(MapboxConstants.MINIMUM_ZOOM.toDouble())) + .build() } } } diff --git a/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewModel.kt b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewModel.kt index c33e708d6b..bd834b4672 100644 --- a/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewModel.kt @@ -29,7 +29,7 @@ import kotlinx.coroutines.flow.onEach // TODO add unit tests class LocationLiveMapViewModel @AssistedInject constructor( @Assisted private val initialState: LocationLiveMapViewState, - getCurrentUserLiveLocationUseCase: GetCurrentUserLiveLocationUseCase + getListOfUserLiveLocationUseCase: GetListOfUserLiveLocationUseCase ) : VectorViewModel(initialState) { @AssistedFactory @@ -40,7 +40,7 @@ class LocationLiveMapViewModel @AssistedInject constructor( companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() init { - getCurrentUserLiveLocationUseCase.execute() + getListOfUserLiveLocationUseCase.execute() .onEach { setState { copy(userLocations = it) } } .launchIn(viewModelScope) } diff --git a/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewState.kt b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewState.kt index d6e58b2486..ca3be8e6ca 100644 --- a/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewState.kt +++ b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewState.kt @@ -16,6 +16,7 @@ package im.vector.app.features.location.live.map +import android.graphics.drawable.Drawable import com.airbnb.mvrx.MavericksState import im.vector.app.features.location.LocationData @@ -30,5 +31,6 @@ data class LocationLiveMapViewState( data class UserLiveLocationViewState( val userId: String, + val pinDrawable: Drawable, val locationData: LocationData )