diff --git a/app/src/main/java/org/groundplatform/android/ui/common/AbstractMapContainerFragment.kt b/app/src/main/java/org/groundplatform/android/ui/common/AbstractMapContainerFragment.kt index c1bd79647a..6a37130db0 100644 --- a/app/src/main/java/org/groundplatform/android/ui/common/AbstractMapContainerFragment.kt +++ b/app/src/main/java/org/groundplatform/android/ui/common/AbstractMapContainerFragment.kt @@ -33,8 +33,6 @@ 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.util.createComposeView -import org.groundplatform.domain.model.geometry.Coordinates -import org.groundplatform.domain.model.map.Bounds import org.groundplatform.ui.map.MapConfig import timber.log.Timber @@ -79,7 +77,7 @@ abstract class AbstractMapContainerFragment : AbstractFragment() { launchWhenStarted { map.startDragEvents.collect { viewModel.onMapDragged() } } launchWhenStarted { viewModel.locationLock.collect { onLocationLockStateChange(it, map) } } launchWhenStarted { - viewModel.getCameraUpdateRequests().collect { onCameraUpdateRequest(it, map) } + viewModel.getCameraUpdateRequests().collect { applyCameraUpdateRequest(it) } } applyMapConfig(map) @@ -153,17 +151,8 @@ abstract class AbstractMapContainerFragment : AbstractFragment() { Toast.makeText(context, messageId, Toast.LENGTH_LONG).show() } - /** Moves the camera to the given bounds. */ - fun moveToBounds(bounds: Bounds, padding: Int, shouldAnimate: Boolean) { - onCameraUpdateRequest(NewCameraPositionViaBounds(bounds, padding, shouldAnimate), map) - } - - /** Moves the camera to a given position. */ - fun moveToPosition(coordinates: Coordinates) { - onCameraUpdateRequest(NewCameraPositionViaCoordinates(coordinates, shouldAnimate = true), map) - } - - private fun onCameraUpdateRequest(request: CameraUpdateRequest, map: MapFragment) { + /** Moves the camera as described by the given [CameraUpdateRequest]. */ + fun applyCameraUpdateRequest(request: CameraUpdateRequest) { Timber.v("Update camera: $request") when (request) { is NewCameraPositionViaCoordinates -> { diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/AbstractTaskMapFragment.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/AbstractTaskMapFragment.kt index 113024c519..c9e670e907 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/AbstractTaskMapFragment.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/AbstractTaskMapFragment.kt @@ -30,6 +30,7 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle +import androidx.lifecycle.withStarted import java.math.RoundingMode import java.text.DecimalFormat import kotlinx.coroutines.flow.Flow @@ -153,8 +154,7 @@ abstract class AbstractTaskMapFragment : getMapViewModel().getCurrentCameraPosition().collect { onMapCameraMoved(it) } } launchWhenStarted { renderFeatures().collect { map.setFeatures(it) } } - // Allow the fragment to restore map viewport to previously drawn feature. - launchWhenStarted { setDefaultViewPort() } + lifecycleScope.launch { withStarted { setDefaultViewPort() } } } /** Must be overridden by subclasses. */ 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 8b8652a8c8..143c4b60a1 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 @@ -22,6 +22,7 @@ import kotlinx.coroutines.flow.combine import org.groundplatform.android.ui.datacollection.tasks.AbstractTaskMapFragment import org.groundplatform.android.ui.map.Feature import org.groundplatform.android.ui.map.MapFragment +import org.groundplatform.android.ui.map.NewCameraPositionViaCoordinates import org.groundplatform.domain.model.map.CameraPosition @AndroidEntryPoint @@ -58,6 +59,6 @@ class DropPinTaskMapFragment @Inject constructor() : override fun setDefaultViewPort() { val feature = taskViewModel.features.value?.firstOrNull() ?: return val coordinates = feature.geometry.center() - moveToPosition(coordinates) + applyCameraUpdateRequest(NewCameraPositionViaCoordinates(coordinates, shouldAnimate = true)) } } 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 4d06be419b..9ed996b39a 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 @@ -24,7 +24,7 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map import org.groundplatform.android.ui.datacollection.tasks.AbstractTaskMapFragment import org.groundplatform.android.ui.map.Feature -import org.groundplatform.domain.model.map.Bounds +import org.groundplatform.android.ui.map.NewCameraPositionViaCoordinates import org.groundplatform.domain.model.map.CameraPosition @AndroidEntryPoint @@ -51,15 +51,15 @@ class DrawAreaTaskMapFragment @Inject constructor() : } launchWhenStarted { - taskViewModel.cameraMoveEvents.collect { coordinates -> moveToPosition(coordinates) } + taskViewModel.cameraMoveEvents.collect { coordinates -> + applyCameraUpdateRequest(NewCameraPositionViaCoordinates(coordinates, shouldAnimate = true)) + } } } override fun setDefaultViewPort() { - val feature = taskViewModel.draftArea.value - val geometry = feature?.geometry ?: return - val bounds = Bounds.fromGeometry(geometry) ?: return - moveToBounds(bounds, padding = 200, shouldAnimate = false) + val request = taskViewModel.getDefaultViewPort() ?: return + applyCameraUpdateRequest(request) } override fun renderFeatures(): Flow> = diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/polygon/DrawAreaTaskViewModel.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/polygon/DrawAreaTaskViewModel.kt index 77036a56a7..73e8a2c638 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/polygon/DrawAreaTaskViewModel.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/polygon/DrawAreaTaskViewModel.kt @@ -39,7 +39,10 @@ import org.groundplatform.android.ui.datacollection.components.ButtonActionState import org.groundplatform.android.ui.datacollection.tasks.AbstractMapTaskViewModel import org.groundplatform.android.ui.datacollection.tasks.DataCollectionEvent import org.groundplatform.android.ui.datacollection.tasks.TaskPositionInterface +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.util.LocaleAwareMeasureFormatter import org.groundplatform.android.ui.util.VibrationHelper import org.groundplatform.android.ui.util.getDefaultColor @@ -50,6 +53,7 @@ import org.groundplatform.domain.model.geometry.LineString import org.groundplatform.domain.model.geometry.LinearRing import org.groundplatform.domain.model.geometry.Polygon import org.groundplatform.domain.model.job.Job +import org.groundplatform.domain.model.map.Bounds import org.groundplatform.domain.model.settings.MeasurementUnits import org.groundplatform.domain.model.submission.DrawAreaTaskData import org.groundplatform.domain.model.submission.DrawAreaTaskIncompleteData @@ -124,6 +128,19 @@ internal constructor( .stateIn(viewModelScope, WhileSubscribed(5_000), emptyList()) } + /** Restores the viewport to the area drawn so far or null if nothing has been drawn yet. */ + fun getDefaultViewPort(): CameraUpdateRequest? { + val vertices = session.vertices.distinct() + return when (vertices.size) { + 0 -> null + 1 -> NewCameraPositionViaCoordinates(vertices.first()) + else -> + Bounds.fromCoordinates(vertices)?.let { + NewCameraPositionViaBounds(it, padding = 200, shouldAnimate = false) + } + } + } + override fun initialize( job: Job, task: Task, diff --git a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/polygon/DrawAreaTaskViewModelTest.kt b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/polygon/DrawAreaTaskViewModelTest.kt index 4894091c58..a5c070154c 100644 --- a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/polygon/DrawAreaTaskViewModelTest.kt +++ b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/polygon/DrawAreaTaskViewModelTest.kt @@ -42,12 +42,15 @@ import org.groundplatform.android.ui.datacollection.components.ButtonAction import org.groundplatform.android.ui.datacollection.tasks.TaskPositionInterface import org.groundplatform.android.ui.datacollection.tasks.polygon.PolygonDrawingSession.Companion.DISTANCE_THRESHOLD_DP import org.groundplatform.android.ui.map.Feature +import org.groundplatform.android.ui.map.NewCameraPositionViaBounds +import org.groundplatform.android.ui.map.NewCameraPositionViaCoordinates import org.groundplatform.domain.model.geometry.Coordinates import org.groundplatform.domain.model.geometry.LineString import org.groundplatform.domain.model.geometry.LinearRing import org.groundplatform.domain.model.geometry.Polygon import org.groundplatform.domain.model.job.Job import org.groundplatform.domain.model.job.Style +import org.groundplatform.domain.model.map.Bounds import org.groundplatform.domain.model.settings.MeasurementUnits import org.groundplatform.domain.model.submission.DrawAreaTaskData import org.groundplatform.domain.model.submission.DrawAreaTaskIncompleteData @@ -105,6 +108,47 @@ class DrawAreaTaskViewModelTest : BaseHiltTest() { assertThat(viewModel.isMarkedComplete()).isFalse() } + @Test + fun `getDefaultViewPort returns null when nothing has been drawn`() { + setupViewModel() + + assertThat(viewModel.getDefaultViewPort()).isNull() + } + + @Test + fun `getDefaultViewPort centers on the first vertex`() { + setupViewModel() + + updateLastVertexAndAdd(COORDINATE_1) + + assertThat(viewModel.getDefaultViewPort()) + .isEqualTo(NewCameraPositionViaCoordinates(COORDINATE_1)) + } + + @Test + fun `getDefaultViewPort centers on a restored draft of a single vertex`() { + setupViewModel(taskData = DrawAreaTaskIncompleteData(LineString(listOf(COORDINATE_1)))) + + assertThat(viewModel.getDefaultViewPort()) + .isEqualTo(NewCameraPositionViaCoordinates(COORDINATE_1)) + } + + @Test + fun `getDefaultViewPort fits the camera to a draft covering an area`() { + setupViewModel() + updateLastVertexAndAdd(COORDINATE_1) + + updateLastVertex(COORDINATE_2) + + assertThat(viewModel.getDefaultViewPort()) + .isEqualTo( + NewCameraPositionViaBounds( + Bounds.fromCoordinates(listOf(COORDINATE_1, COORDINATE_2))!!, + padding = 200, + ) + ) + } + @Test fun `Removing last vertex on complete polygon`() { setupViewModel()