Skip to content

Commit

Permalink
Merge branch 'master' of https://github.com/google/ground-android int…
Browse files Browse the repository at this point in the history
…o gino-m/1855/1

# Conflicts:
#	ground/src/main/java/com/google/android/ground/ui/offlineareas/selector/OfflineAreaSelectorViewModel.kt
#	ground/src/main/res/values/colors.xml
  • Loading branch information
gino-m committed Sep 11, 2023
2 parents b851f84 + 3fe2275 commit a0e142e
Show file tree
Hide file tree
Showing 29 changed files with 197 additions and 202 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -165,13 +165,9 @@ constructor(
/** Returns a flowable of all [LocationOfInterest] within the map bounds (viewport). */
fun getWithinBoundsOnceAndStream(
survey: Survey,
cameraBoundUpdates: Flowable<Bounds>
bounds: Bounds
): Flowable<List<LocationOfInterest>> =
cameraBoundUpdates
.switchMap { bounds ->
getLocationsOfInterestOnceAndStream(survey).map { lois ->
lois.filter { bounds.contains(it.geometry) }
}
}
getLocationsOfInterestOnceAndStream(survey)
.map { lois -> lois.filter { bounds.contains(it.geometry) } }
.distinctUntilChanged()
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,22 +75,30 @@ abstract class AbstractMapContainerFragment : AbstractFragment() {
}

private fun applyMapConfig(map: Map) {
val config = getMapConfig()
val viewModel = getMapViewModel()
val config = viewModel.mapConfig

// Map Type
// Map type
if (config.overrideMapType != null) {
map.mapType = config.overrideMapType
} else {
getMapViewModel().mapType.observe(viewLifecycleOwner) { map.mapType = it }
viewModel.mapType.observe(viewLifecycleOwner) { map.mapType = it }
}

// Tile overlays.
if (getMapConfig().showOfflineTileOverlays) {
getMapViewModel().offlineTileSources.observe(viewLifecycleOwner) {
if (config.showOfflineTileOverlays) {
viewModel.offlineTileSources.observe(viewLifecycleOwner) {
map.clearTileOverlays()
it.forEach(map::addTileOverlay)
}
}

// Map gestures
if (config.disableGestures) {
map.disableGestures()
} else {
map.enableGestures()
}
}

/** Opens a dialog for selecting a [MapType] for the basemap layer. */
Expand Down Expand Up @@ -141,10 +149,6 @@ abstract class AbstractMapContainerFragment : AbstractFragment() {
} else {
error("Must have either target or bounds set")
}

// Manually notify that the camera has moved as `map.cameraMovedEvents` only returns
// an event when the map is moved by the user (REASON_GESTURE).
onMapCameraMoved(newPosition)
}

/** Called when the map camera is moved by the user or due to current location/survey changes. */
Expand All @@ -153,17 +157,8 @@ abstract class AbstractMapContainerFragment : AbstractFragment() {
}

/** Called when the map is attached to the fragment. */
protected abstract fun onMapReady(map: Map)
protected open fun onMapReady(map: Map) {}

/** Provides an implementation of [BaseMapViewModel]. */
protected abstract fun getMapViewModel(): BaseMapViewModel

// TODO: Should this be moved to BaseMapViewModel?
/** Configuration to enable/disable base map features. */
protected open fun getMapConfig(): MapConfig = DEFAULT_MAP_CONFIG

companion object {
private val DEFAULT_MAP_CONFIG: MapConfig =
MapConfig(showOfflineTileOverlays = true, overrideMapType = null)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,10 @@ import com.google.android.ground.system.LocationManager
import com.google.android.ground.system.PermissionDeniedException
import com.google.android.ground.system.PermissionsManager
import com.google.android.ground.system.SettingsManager
import com.google.android.ground.ui.map.Bounds
import com.google.android.ground.ui.map.CameraPosition
import com.google.android.ground.ui.map.MapType
import com.google.android.ground.ui.map.gms.GmsExt.toBounds
import com.google.android.ground.ui.map.gms.toCoordinates
import io.reactivex.BackpressureStrategy
import io.reactivex.Flowable
import io.reactivex.subjects.PublishSubject
import io.reactivex.subjects.Subject
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
Expand Down Expand Up @@ -75,12 +70,6 @@ constructor(
) : AbstractViewModel() {

private val _cameraPosition = MutableStateFlow<CameraPosition?>(null)
private val cameraZoomSubject: @Hot Subject<Float> = PublishSubject.create()
val cameraZoomUpdates: Flowable<Float> = cameraZoomSubject.toFlowable(BackpressureStrategy.LATEST)

private val cameraBoundsSubject: @Hot Subject<Bounds> = PublishSubject.create()
val cameraBoundUpdates: Flowable<Bounds> =
cameraBoundsSubject.toFlowable(BackpressureStrategy.LATEST)

val locationLock: MutableStateFlow<Result<Boolean>> =
MutableStateFlow(Result.success(mapStateRepository.isLocationLockEnabled))
Expand Down Expand Up @@ -115,6 +104,16 @@ constructor(

val offlineTileSources: LiveData<List<TileSource>>

/** Configuration to enable/disable base map features. */
open val mapConfig: MapConfig = DEFAULT_MAP_CONFIG

/** Current camera position. */
val currentCameraPosition: CameraPosition? = _cameraPosition.value

/** Last camera position. */
var lastCameraPosition: CameraPosition? = null
private set

init {
mapType = mapStateRepository.mapTypeFlowable.toLiveData()
offlineTileSources =
Expand Down Expand Up @@ -230,14 +229,12 @@ constructor(
}

private fun updatePosition(cameraPosition: CameraPosition) {
lastCameraPosition = _cameraPosition.value
_cameraPosition.value = cameraPosition
}

/** Called when the map camera is moved. */
open fun onMapCameraMoved(newCameraPosition: CameraPosition) {
newCameraPosition.zoomLevel?.let { cameraZoomSubject.onNext(it) }
newCameraPosition.bounds?.let { cameraBoundsSubject.onNext(it) }
}
open fun onMapCameraMoved(newCameraPosition: CameraPosition) {}

companion object {
private val LOCATION_LOCK_ICON_TINT_ENABLED = R.color.md_theme_primary
Expand All @@ -246,5 +243,7 @@ constructor(
// TODO(#1789): Consider adding another icon for representing "GPS disabled" state.
private val LOCATION_LOCK_ICON_ENABLED = R.drawable.ic_gps_lock
private val LOCATION_LOCK_ICON_DISABLED = R.drawable.ic_gps_lock_not_fixed

private val DEFAULT_MAP_CONFIG: MapConfig = MapConfig(showOfflineTileOverlays = true)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,8 @@ package com.google.android.ground.ui.common
import com.google.android.ground.ui.map.MapType

/** Configuration to apply on the rendered base map. */
data class MapConfig(val showOfflineTileOverlays: Boolean, val overrideMapType: MapType?)
data class MapConfig(
val showOfflineTileOverlays: Boolean,
val overrideMapType: MapType? = null,
val disableGestures: Boolean = false
)
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ import com.google.android.ground.ui.home.locationofinterestdetails.SubmissionLis
import com.google.android.ground.ui.home.locationofinterestdetails.SubmissionListViewModel
import com.google.android.ground.ui.home.mapcontainer.HomeScreenMapContainerViewModel
import com.google.android.ground.ui.home.mapcontainer.MapTypeViewModel
import com.google.android.ground.ui.offlinebasemap.OfflineAreasViewModel
import com.google.android.ground.ui.offlinebasemap.selector.OfflineAreaSelectorViewModel
import com.google.android.ground.ui.offlinebasemap.viewer.OfflineAreaViewerViewModel
import com.google.android.ground.ui.offlineareas.OfflineAreasViewModel
import com.google.android.ground.ui.offlineareas.selector.OfflineAreaSelectorViewModel
import com.google.android.ground.ui.offlineareas.viewer.OfflineAreaViewerViewModel
import com.google.android.ground.ui.signin.SignInViewModel
import com.google.android.ground.ui.submissiondetails.SubmissionDetailsViewModel
import com.google.android.ground.ui.surveyselector.SurveySelectorViewModel
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.asFlow
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.PagerSnapHelper
Expand Down Expand Up @@ -87,8 +86,7 @@ class HomeScreenMapContainerFragment : Hilt_HomeScreenMapContainerFragment() {
}

lifecycleScope.launch {
mapContainerViewModel.loisWithinMapBoundsAtVisibleZoomLevel
.asFlow()
mapContainerViewModel.loisInViewport
.combine(mapContainerViewModel.suggestLoiJobs) { lois, jobs ->
val loiCards = lois.map { MapCardUiData.LoiCardUiData(it) }
val jobCards = jobs.map { MapCardUiData.SuggestLoiCardUiData(it) }
Expand Down Expand Up @@ -176,7 +174,7 @@ class HomeScreenMapContainerFragment : Hilt_HomeScreenMapContainerFragment() {
override fun onMapReady(map: Map) {
// Observe events emitted by the ViewModel.
viewLifecycleOwner.lifecycleScope.launch {
mapContainerViewModel.mapLocationOfInterestFeatures.collect { map.renderFeatures(it) }
mapContainerViewModel.mapLoiFeatures.collect { map.renderFeatures(it) }
}

homeScreenViewModel.bottomSheetState.observe(this) { state: BottomSheetState ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,11 @@
*/
package com.google.android.ground.ui.home.mapcontainer

import androidx.lifecycle.LiveData
import androidx.lifecycle.toLiveData
import androidx.lifecycle.viewModelScope
import com.google.android.ground.Config.CLUSTERING_ZOOM_THRESHOLD
import com.google.android.ground.Config.ZOOM_LEVEL_THRESHOLD
import com.google.android.ground.coroutines.IoDispatcher
import com.google.android.ground.model.Survey
import com.google.android.ground.model.geometry.Point
import com.google.android.ground.model.job.Job
import com.google.android.ground.model.locationofinterest.LocationOfInterest
Expand All @@ -37,20 +36,17 @@ import com.google.android.ground.ui.common.BaseMapViewModel
import com.google.android.ground.ui.common.SharedViewModel
import com.google.android.ground.ui.map.CameraPosition
import com.google.android.ground.ui.map.Feature
import io.reactivex.Flowable
import io.reactivex.Observable
import io.reactivex.subjects.PublishSubject
import io.reactivex.subjects.Subject
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.transform
import kotlinx.coroutines.launch
import kotlinx.coroutines.reactive.asFlow
import timber.log.Timber

Expand Down Expand Up @@ -78,72 +74,87 @@ internal constructor(
ioDispatcher
) {

val mapLocationOfInterestFeatures: StateFlow<Set<Feature>>
private var _survey: Survey? = null

private var lastCameraPosition: CameraPosition? = null
private val _mapLoiFeatures: MutableStateFlow<Set<Feature>> = MutableStateFlow(setOf())
val mapLoiFeatures: StateFlow<Set<Feature>> =
_mapLoiFeatures.stateIn(viewModelScope, SharingStarted.Lazily, setOf())

/* UI Clicks */
private val zoomThresholdCrossed: @Hot Subject<Nil> = PublishSubject.create()

/**
* List of [LocationOfInterest] for the active survey that are present within the map bounds and
* List of [LocationOfInterest] for the active survey that are present within the viewport and
* zoom level is clustering threshold or higher.
*/
val loisWithinMapBoundsAtVisibleZoomLevel: LiveData<List<LocationOfInterest>>
private val _loisInViewport: MutableStateFlow<List<LocationOfInterest>> =
MutableStateFlow(listOf())
val loisInViewport: StateFlow<List<LocationOfInterest>> =
_loisInViewport.stateIn(viewModelScope, SharingStarted.Lazily, listOf())

val suggestLoiJobs: Flow<List<Job>>
private val _suggestLoiJobs: MutableStateFlow<List<Job>> = MutableStateFlow(listOf())
val suggestLoiJobs: StateFlow<List<Job>> =
_suggestLoiJobs.stateIn(viewModelScope, SharingStarted.Lazily, listOf())

init {
// THIS SHOULD NOT BE CALLED ON CONFIG CHANGE
// TODO: Clear location of interest markers when survey is deactivated.
// TODO: Since we depend on survey stream from repo anyway, this transformation can be moved
// into the repository.

// LOIs that are persisted to the local and remote dbs.
mapLocationOfInterestFeatures =
surveyRepository.activeSurveyFlow
.transform { survey ->
if (survey == null) {
emit(setOf())
} else {
emitAll(locationOfInterestRepository.findLocationsOfInterestFeatures(survey))
}
}
.distinctUntilChanged()
.stateIn(viewModelScope, SharingStarted.Lazily, setOf())

loisWithinMapBoundsAtVisibleZoomLevel =
surveyRepository.activeSurveyFlowable
.switchMap { survey ->
cameraZoomUpdates.switchMap { zoomLevel ->
if (zoomLevel >= CLUSTERING_ZOOM_THRESHOLD && survey.isPresent)
locationOfInterestRepository.getWithinBoundsOnceAndStream(
survey.get(),
cameraBoundUpdates
)
else Flowable.just(listOf())
}
}
.toLiveData()

suggestLoiJobs =
surveyRepository.activeSurveyFlow
.combine(cameraZoomUpdates.asFlow()) { survey, zoomLevel ->
if (zoomLevel < CLUSTERING_ZOOM_THRESHOLD) {
listOf()
} else {
survey?.jobs?.filter { job -> job.suggestLoiTaskType != null }?.toList() ?: listOf()
}
}
.distinctUntilChanged()
viewModelScope.launch {
surveyRepository.activeSurveyFlow.collect {
_survey = it
refreshMapFeaturesAndCards(it)
}
}
}

private suspend fun refreshMapFeaturesAndCards(survey: Survey?) {
updateMapFeatures(survey)
updateLoisAndJobs(survey, currentCameraPosition)
}

private suspend fun updateLoisAndJobs(survey: Survey?, cameraPosition: CameraPosition?) {
updateMapLois(survey, cameraPosition)
updateSuggestLoiJobs(survey, cameraPosition)
}

private suspend fun updateMapFeatures(survey: Survey?) {
if (survey == null) {
_mapLoiFeatures.value = setOf()
} else {
// LOIs that are persisted to the local and remote dbs.
_mapLoiFeatures.emitAll(locationOfInterestRepository.findLocationsOfInterestFeatures(survey))
}
}

private suspend fun updateMapLois(survey: Survey?, cameraPosition: CameraPosition?) {
val bounds = cameraPosition?.bounds
if (bounds == null || survey == null || cameraPosition.isBelowClusteringZoomThreshold()) {
_loisInViewport.value = listOf()
} else {
_loisInViewport.emitAll(
locationOfInterestRepository.getWithinBoundsOnceAndStream(survey, bounds).asFlow()
)
}
}

private fun updateSuggestLoiJobs(survey: Survey?, cameraPosition: CameraPosition?) {
if (survey == null || cameraPosition.isBelowClusteringZoomThreshold()) {
_suggestLoiJobs.value = listOf()
} else {
_suggestLoiJobs.value = survey.jobs.filter { it.suggestLoiTaskType != null }.toList()
}
}

override fun onMapCameraMoved(newCameraPosition: CameraPosition) {
super.onMapCameraMoved(newCameraPosition)
Timber.d("Setting position to $newCameraPosition")
onZoomChange(lastCameraPosition?.zoomLevel, newCameraPosition.zoomLevel)
mapStateRepository.setCameraPosition(newCameraPosition)
lastCameraPosition = newCameraPosition

viewModelScope.launch { updateLoisAndJobs(_survey, newCameraPosition) }
}

private fun onZoomChange(oldZoomLevel: Float?, newZoomLevel: Float?) {
Expand All @@ -170,4 +181,7 @@ internal constructor(
}

fun getZoomThresholdCrossed(): Observable<Nil> = zoomThresholdCrossed

private fun CameraPosition?.isBelowClusteringZoomThreshold() =
this?.zoomLevel?.let { it < CLUSTERING_ZOOM_THRESHOLD } ?: true
}
Loading

0 comments on commit a0e142e

Please sign in to comment.