diff --git a/ground/src/main/java/com/google/android/ground/repository/OfflineAreaRepository.kt b/ground/src/main/java/com/google/android/ground/repository/OfflineAreaRepository.kt index 8464af6a02..a00a12c8ad 100644 --- a/ground/src/main/java/com/google/android/ground/repository/OfflineAreaRepository.kt +++ b/ground/src/main/java/com/google/android/ground/repository/OfflineAreaRepository.kt @@ -17,6 +17,7 @@ package com.google.android.ground.repository import com.google.android.ground.Config import com.google.android.ground.model.imagery.OfflineArea +import com.google.android.ground.model.imagery.TileSource import com.google.android.ground.persistence.local.stores.LocalOfflineAreaStore import com.google.android.ground.persistence.uuid.OfflineUuidGenerator import com.google.android.ground.rx.annotations.Cold @@ -94,9 +95,23 @@ constructor( } // TODO(#1730): Generate local tiles path based on source base path. - fun getLocalTileSourcePath(): String = File(fileUtil.filesDir.path, "tiles").path + private fun getLocalTileSourcePath(): String = File(fileUtil.filesDir.path, "tiles").path - fun getOfflineAreaBoundsFlow(): Flow> = + fun getOfflineTileSources(): Flow> = + surveyRepository.activeSurveyFlow.mapNotNull { + it?.tileSources?.mapNotNull(this::toLocalTileSource) ?: listOf() + } + // TODO(#1790): Maybe create a new data class object which is not of type TileSource. + + private fun toLocalTileSource(tileSource: TileSource): TileSource? { + if (tileSource.type != TileSource.Type.MOG_COLLECTION) return null + return TileSource( + "file://${getLocalTileSourcePath()}/{z}/{x}/{y}.jpg", + TileSource.Type.TILED_WEB_MAP + ) + } + + private fun getOfflineAreaBoundsFlow(): Flow> = localOfflineAreaStore.getOfflineAreasFlow().transform { list -> list.map { it.bounds } } /** diff --git a/ground/src/main/java/com/google/android/ground/ui/common/AbstractMapContainerFragment.kt b/ground/src/main/java/com/google/android/ground/ui/common/AbstractMapContainerFragment.kt index 63c7d490c2..952149750c 100644 --- a/ground/src/main/java/com/google/android/ground/ui/common/AbstractMapContainerFragment.kt +++ b/ground/src/main/java/com/google/android/ground/ui/common/AbstractMapContainerFragment.kt @@ -15,7 +15,6 @@ */ package com.google.android.ground.ui.common -import android.annotation.SuppressLint import android.os.Bundle import android.view.View import android.widget.Toast @@ -46,6 +45,8 @@ abstract class AbstractMapContainerFragment : AbstractFragment() { } private fun onMapAttached(map: Map) { + val viewModel = getMapViewModel() + // Removes all markers, overlays, polylines and polygons from the map. map.clear() @@ -56,41 +57,28 @@ abstract class AbstractMapContainerFragment : AbstractFragment() { map.startDragEvents .onBackpressureLatest() .`as`(RxAutoDispose.disposeOnDestroy(this)) - .subscribe { getMapViewModel().onMapDragged() } + .subscribe { viewModel.onMapDragged() } + lifecycleScope.launch { viewModel.locationLock.collect { onLocationLockStateChange(it, map) } } + viewModel.mapType.observe(viewLifecycleOwner) { map.mapType = it } lifecycleScope.launch { - getMapViewModel().locationLock.collect { onLocationLockStateChange(it, map) } - } - getMapViewModel().mapType.observe(viewLifecycleOwner) { map.mapType = it } - lifecycleScope.launch { - getMapViewModel().getCameraUpdates().collect { onCameraUpdateRequest(it, map) } + viewModel.getCameraUpdates().collect { onCameraUpdateRequest(it, map) } } - // Enable map controls - getMapViewModel().setLocationLockEnabled(true) + // Enable map controls. + viewModel.setLocationLockEnabled(true) - // Offline imagery - if (getMapConfig().showTileOverlays) { - lifecycleScope.launch { - getMapViewModel().offlineImageryEnabled.collect { enabled -> - if (enabled) addTileOverlays() else map.clearTileOverlays() - } + // Tile overlays. + if (getMapConfig().showOfflineTileOverlays) { + viewModel.offlineTileSources.observe(viewLifecycleOwner) { + map.clearTileOverlays() + it.forEach(map::addTileOverlay) } } onMapReady(map) } - @SuppressLint("FragmentLiveDataObserve") - private fun addTileOverlays() { - // TODO(#1756): Clear tile overlays on change to stop accumulating them on map. - - // TODO(#1782): Changing the owner to `viewLifecycleOwner` in observe() causes a crash in task - // fragment and converting live data to flow results in clear tiles not working. Figure out a - // better way to fix the IDE warning. - getMapViewModel().tileOverlays.observe(this) { it.forEach(map::addTileOverlay) } - } - /** Opens a dialog for selecting a [MapType] for the basemap layer. */ fun showMapTypeSelectorDialog() { val types = map.supportedMapTypes.toTypedArray() @@ -160,6 +148,6 @@ abstract class AbstractMapContainerFragment : AbstractFragment() { protected open fun getMapConfig(): MapConfig = DEFAULT_MAP_CONFIG companion object { - private val DEFAULT_MAP_CONFIG: MapConfig = MapConfig(showTileOverlays = true) + private val DEFAULT_MAP_CONFIG: MapConfig = MapConfig(showOfflineTileOverlays = true) } } diff --git a/ground/src/main/java/com/google/android/ground/ui/common/BaseMapViewModel.kt b/ground/src/main/java/com/google/android/ground/ui/common/BaseMapViewModel.kt index a426fe5444..c616abcacd 100644 --- a/ground/src/main/java/com/google/android/ground/ui/common/BaseMapViewModel.kt +++ b/ground/src/main/java/com/google/android/ground/ui/common/BaseMapViewModel.kt @@ -55,7 +55,6 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.transform import kotlinx.coroutines.flow.withIndex @@ -114,29 +113,22 @@ constructor( } .stateIn(viewModelScope, SharingStarted.Lazily, null) - val tileOverlays: LiveData> - val offlineImageryEnabled: Flow = mapStateRepository.offlineImageryFlow + val offlineTileSources: LiveData> init { mapType = mapStateRepository.mapTypeFlowable.toLiveData() - tileOverlays = - surveyRepository.activeSurveyFlow - .mapNotNull { it?.tileSources?.mapNotNull(this::toLocalTileSource) ?: listOf() } + offlineTileSources = + offlineAreaRepository + .getOfflineTileSources() + .combine(mapStateRepository.offlineImageryFlow) { offlineSources, enabled -> + if (enabled) offlineSources else listOf() + } .asLiveData() viewModelScope.launch(ioDispatcher) { updateCameraPositionOnLocationChange() } viewModelScope.launch(ioDispatcher) { updateCameraPositionOnSurveyChange() } } - // TODO(#1790): Maybe create a new data class object which is not of type TileSource. - private fun toLocalTileSource(tileSource: TileSource): TileSource? { - if (tileSource.type != TileSource.Type.MOG_COLLECTION) return null - return TileSource( - "file://${offlineAreaRepository.getLocalTileSourcePath()}/{z}/{x}/{y}.jpg", - TileSource.Type.TILED_WEB_MAP - ) - } - private suspend fun toggleLocationLock() { if (locationLock.value.getOrDefault(false)) { disableLocationLock() diff --git a/ground/src/main/java/com/google/android/ground/ui/common/MapConfig.kt b/ground/src/main/java/com/google/android/ground/ui/common/MapConfig.kt index 2a8f6793a6..8ca340135f 100644 --- a/ground/src/main/java/com/google/android/ground/ui/common/MapConfig.kt +++ b/ground/src/main/java/com/google/android/ground/ui/common/MapConfig.kt @@ -16,4 +16,4 @@ package com.google.android.ground.ui.common /** Configuration to apply on the rendered base map. */ -data class MapConfig(val showTileOverlays: Boolean) +data class MapConfig(val showOfflineTileOverlays: Boolean) diff --git a/ground/src/main/java/com/google/android/ground/ui/offlinebasemap/selector/OfflineAreaSelectorFragment.kt b/ground/src/main/java/com/google/android/ground/ui/offlinebasemap/selector/OfflineAreaSelectorFragment.kt index 5c24d64f18..d270b84b07 100644 --- a/ground/src/main/java/com/google/android/ground/ui/offlinebasemap/selector/OfflineAreaSelectorFragment.kt +++ b/ground/src/main/java/com/google/android/ground/ui/offlinebasemap/selector/OfflineAreaSelectorFragment.kt @@ -63,5 +63,6 @@ class OfflineAreaSelectorFragment : Hilt_OfflineAreaSelectorFragment() { override fun getMapViewModel(): BaseMapViewModel = viewModel - override fun getMapConfig(): MapConfig = super.getMapConfig().copy(showTileOverlays = false) + override fun getMapConfig(): MapConfig = + super.getMapConfig().copy(showOfflineTileOverlays = false) } diff --git a/ground/src/main/java/com/google/android/ground/ui/offlinebasemap/selector/OfflineAreaSelectorViewModel.kt b/ground/src/main/java/com/google/android/ground/ui/offlinebasemap/selector/OfflineAreaSelectorViewModel.kt index df60e77cc0..7ad80d5051 100644 --- a/ground/src/main/java/com/google/android/ground/ui/offlinebasemap/selector/OfflineAreaSelectorViewModel.kt +++ b/ground/src/main/java/com/google/android/ground/ui/offlinebasemap/selector/OfflineAreaSelectorViewModel.kt @@ -70,7 +70,7 @@ internal constructor( ioDispatcher ) { - val tileSources: List + val remoteTileSources: List private var viewport: Bounds? = null val isDownloadProgressVisible = MutableLiveData(false) val downloadProgressMax = MutableLiveData(0) @@ -80,7 +80,7 @@ internal constructor( val downloadButtonEnabled = MutableLiveData(false) init { - tileSources = surveyRepository.activeSurvey!!.tileSources + remoteTileSources = surveyRepository.activeSurvey!!.tileSources } fun onDownloadClick() { @@ -109,7 +109,7 @@ internal constructor( fun onMapReady(map: Map) { map.mapType = MapType.TERRAIN - tileSources.forEach { map.addTileOverlay(it) } + remoteTileSources.forEach { map.addTileOverlay(it) } disposeOnClear(cameraBoundUpdates.subscribe { viewport = it }) } diff --git a/ground/src/main/java/com/google/android/ground/ui/offlinebasemap/viewer/OfflineAreaViewerFragment.kt b/ground/src/main/java/com/google/android/ground/ui/offlinebasemap/viewer/OfflineAreaViewerFragment.kt index 9b2bd23a76..34b5772388 100644 --- a/ground/src/main/java/com/google/android/ground/ui/offlinebasemap/viewer/OfflineAreaViewerFragment.kt +++ b/ground/src/main/java/com/google/android/ground/ui/offlinebasemap/viewer/OfflineAreaViewerFragment.kt @@ -62,7 +62,8 @@ class OfflineAreaViewerFragment @Inject constructor() : Hilt_OfflineAreaViewerFr override fun getMapViewModel(): BaseMapViewModel = viewModel - override fun getMapConfig(): MapConfig = super.getMapConfig().copy(showTileOverlays = false) + override fun getMapConfig(): MapConfig = + super.getMapConfig().copy(showOfflineTileOverlays = false) private fun panMap(offlineArea: OfflineArea) { map.viewport = offlineArea.bounds