Show user pins with correct zoom when map is first opened

This commit is contained in:
Maxime NATUREL 2022-05-12 09:27:01 +02:00
parent d6029210d0
commit 5410b61ae3
5 changed files with 155 additions and 53 deletions

View file

@ -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<List<UserLiveLocationViewState>> {
// 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))
}
}
}

View file

@ -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<List<UserLiveLocationViewState>> {
// 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()
}
}
}

View file

@ -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<FragmentSimpleContainerBi
private val args: LocationLiveMapViewArgs by args()
private val viewModel: LocationLiveMapViewModel by fragmentViewModel()
private var mapboxMap: WeakReference<MapboxMap>? = null
private var symbolManager: SymbolManager? = null
private var mapStyle: Style? = null
private val pendingLiveLocations = mutableListOf<UserLiveLocationViewState>()
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<FragmentSimpleContainerBi
private fun setupMap() {
val mapFragment = getOrCreateSupportMapFragment()
mapFragment.getMapAsync { mapBoxMap ->
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<UserLiveLocationViewState>) {
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()
}
}
}

View file

@ -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<LocationLiveMapViewState, LocationLiveMapAction, LocationLiveMapViewEvents>(initialState) {
@AssistedFactory
@ -40,7 +40,7 @@ class LocationLiveMapViewModel @AssistedInject constructor(
companion object : MavericksViewModelFactory<LocationLiveMapViewModel, LocationLiveMapViewState> by hiltMavericksViewModelFactory()
init {
getCurrentUserLiveLocationUseCase.execute()
getListOfUserLiveLocationUseCase.execute()
.onEach { setState { copy(userLocations = it) } }
.launchIn(viewModelScope)
}

View file

@ -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
)