From 9ac262c8c91b614f39249dd607fcfbeebad060c1 Mon Sep 17 00:00:00 2001 From: andreia Date: Wed, 5 Aug 2026 19:33:42 +0200 Subject: [PATCH] introduce existingLoiFeatures flow to BaseMapViewModel and enable drop pin and draw area tasks to see existing features in the map --- .../android/ui/common/BaseMapViewModel.kt | 32 +++++++++++++++ .../tasks/point/DropPinTaskMapFragment.kt | 8 +++- .../tasks/polygon/DrawAreaTaskMapFragment.kt | 7 +++- .../android/ui/common/BaseMapViewModelTest.kt | 41 +++++++++++++++++++ 4 files changed, 85 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/groundplatform/android/ui/common/BaseMapViewModel.kt b/app/src/main/java/org/groundplatform/android/ui/common/BaseMapViewModel.kt index 0c70e2f430..05c1b1661b 100644 --- a/app/src/main/java/org/groundplatform/android/ui/common/BaseMapViewModel.kt +++ b/app/src/main/java/org/groundplatform/android/ui/common/BaseMapViewModel.kt @@ -37,8 +37,10 @@ import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update @@ -51,11 +53,13 @@ import org.groundplatform.android.system.PermissionsManager import org.groundplatform.android.system.SettingsManager import org.groundplatform.android.ui.components.MapFloatingActionButtonType import org.groundplatform.android.ui.map.CameraUpdateRequest +import org.groundplatform.android.ui.map.Feature import org.groundplatform.android.ui.map.NewCameraPositionViaBounds import org.groundplatform.android.ui.map.NewCameraPositionViaCoordinates import org.groundplatform.android.ui.map.NewCameraPositionViaCoordinatesAndZoomLevel import org.groundplatform.android.ui.map.gms.GmsExt.toBounds import org.groundplatform.android.ui.map.gms.toCoordinates +import org.groundplatform.android.ui.util.getDefaultColor import org.groundplatform.domain.model.Survey import org.groundplatform.domain.model.geometry.Coordinates import org.groundplatform.domain.model.imagery.TileSource @@ -67,6 +71,7 @@ import org.groundplatform.domain.repository.OfflineAreaRepositoryInterface import org.groundplatform.domain.repository.SurveyRepositoryInterface import timber.log.Timber +@OptIn(ExperimentalCoroutinesApi::class) open class BaseMapViewModel @Inject constructor( @@ -129,6 +134,33 @@ constructor( } .asLiveData() + /** + * Read-only LOI features for the active survey. Lazily initialized to avoid unnecessary database + * queries when not rendered. + */ + val existingLoiFeatures: Flow> by lazy { + surveyRepository.activeSurveyFlow + .flatMapLatest { survey -> + if (survey == null) flowOf(emptySet()) + else locationOfInterestRepository.getValidLois(survey) + } + .map { lois -> + lois + .map { + Feature( + id = it.id, + type = Feature.Type.LOCATION_OF_INTEREST, + geometry = it.geometry, + style = Feature.Style(it.job.getDefaultColor()), + clusterable = false, + ) + } + .toSet() + } + .onStart { emit(setOf()) } + .distinctUntilChanged() + } + /** Returns whether the user has granted fine location permission. */ fun hasLocationPermission() = permissionsManager.isGranted(Manifest.permission.ACCESS_FINE_LOCATION) diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/point/DropPinTaskMapFragment.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/point/DropPinTaskMapFragment.kt index ae24394dd2..41af4cde3f 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/point/DropPinTaskMapFragment.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/point/DropPinTaskMapFragment.kt @@ -18,6 +18,7 @@ package org.groundplatform.android.ui.datacollection.tasks.point import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine import org.groundplatform.android.ui.datacollection.tasks.AbstractTaskMapFragment import org.groundplatform.android.ui.datacollection.tasks.launchWhenTaskVisible import org.groundplatform.android.ui.map.Feature @@ -50,7 +51,12 @@ class DropPinTaskMapFragment @Inject constructor() : taskViewModel.updateCameraPosition(position) } - override fun renderFeatures(): Flow> = taskViewModel.features + override fun renderFeatures(): Flow> = + combine(getMapViewModel().existingLoiFeatures, taskViewModel.features) { + loiFeatures, + pinFeatures -> + loiFeatures + pinFeatures + } override fun setDefaultViewPort() { val feature = taskViewModel.features.value?.firstOrNull() ?: return diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/polygon/DrawAreaTaskMapFragment.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/polygon/DrawAreaTaskMapFragment.kt index 93c537b4aa..2c314eb3c0 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/polygon/DrawAreaTaskMapFragment.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/polygon/DrawAreaTaskMapFragment.kt @@ -20,6 +20,7 @@ import android.view.View import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import org.groundplatform.android.ui.datacollection.tasks.AbstractTaskMapFragment @@ -66,8 +67,10 @@ class DrawAreaTaskMapFragment @Inject constructor() : } override fun renderFeatures(): Flow> = - taskViewModel.draftArea.map { feature: Feature? -> - if (feature == null) setOf() else setOf(feature) + combine(getMapViewModel().existingLoiFeatures, taskViewModel.draftArea) { + loiFeatures, + draftArea: Feature? -> + loiFeatures + setOfNotNull(draftArea) } override fun onMapCameraMoved(position: CameraPosition) { diff --git a/app/src/test/java/org/groundplatform/android/ui/common/BaseMapViewModelTest.kt b/app/src/test/java/org/groundplatform/android/ui/common/BaseMapViewModelTest.kt index 7e70946700..4ea4b05c92 100644 --- a/app/src/test/java/org/groundplatform/android/ui/common/BaseMapViewModelTest.kt +++ b/app/src/test/java/org/groundplatform/android/ui/common/BaseMapViewModelTest.kt @@ -20,17 +20,26 @@ import com.google.android.gms.common.api.ApiException import com.google.android.gms.common.api.CommonStatusCodes import com.google.android.gms.common.api.Status import com.google.android.gms.location.LocationSettingsStatusCodes.SETTINGS_CHANGE_UNAVAILABLE +import com.google.common.truth.Truth.assertThat import dagger.hilt.android.testing.HiltAndroidTest import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest import org.groundplatform.android.BaseHiltTest +import org.groundplatform.android.FakeData.AREA_OF_INTEREST +import org.groundplatform.android.FakeData.JOB +import org.groundplatform.android.FakeData.LOCATION_OF_INTEREST +import org.groundplatform.android.FakeData.SURVEY import org.groundplatform.android.system.FINE_LOCATION_UPDATES_REQUEST import org.groundplatform.android.system.LocationManager import org.groundplatform.android.system.PermissionsManager import org.groundplatform.android.system.SettingsManager import org.groundplatform.android.ui.components.MapFloatingActionButtonType +import org.groundplatform.android.ui.map.Feature +import org.groundplatform.android.ui.util.getDefaultColor import org.groundplatform.domain.repository.LocationOfInterestRepositoryInterface import org.groundplatform.domain.repository.MapStateRepositoryInterface import org.groundplatform.domain.repository.OfflineAreaRepositoryInterface @@ -160,6 +169,38 @@ class BaseMapViewModelTest : BaseHiltTest() { verify(locationManager).disableLocationUpdates() } + @Test + fun `Should show existing features on the map`() = + runWithTestDispatcher { + setupMocks() + val areaOfInterest = AREA_OF_INTEREST.copy(id = "loi id 2") + whenever(surveyRepository.activeSurveyFlow).thenReturn(MutableStateFlow(SURVEY)) + whenever(locationOfInterestRepository.getValidLois(SURVEY)) + .thenReturn(flowOf(setOf(LOCATION_OF_INTEREST, areaOfInterest))) + + val features = viewModel.existingLoiFeatures.first { it.isNotEmpty() } + + assertThat(features) + .containsExactly( + Feature( + id = LOCATION_OF_INTEREST.id, + type = Feature.Type.LOCATION_OF_INTEREST, + geometry = LOCATION_OF_INTEREST.geometry, + style = Feature.Style(JOB.getDefaultColor()), + clusterable = false, + selected = false, + ), + Feature( + id = areaOfInterest.id, + type = Feature.Type.LOCATION_OF_INTEREST, + geometry = areaOfInterest.geometry, + style = Feature.Style(JOB.getDefaultColor()), + clusterable = false, + selected = false, + ), + ) + } + private fun setupMocks( isLocationLocked: Boolean = false, hasLocationPermissions: Boolean = true,