Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
9556c93
Fix isse 3541, reset hasSelfIntersection to false in updateVertices a…
hassan-nsubuga Mar 11, 2026
5cb0e85
Add tests to DrawAreaTaskViewModelTest to convert resetting of hasSel…
hassan-nsubuga Mar 12, 2026
520a374
Bump com.google.maps.android:android-maps-utils from 4.1.0 to 4.1.1 (…
dependabot[bot] Mar 12, 2026
951c359
fix codecov failing dependabot CI (#3612)
andreia-ferreira Mar 13, 2026
6f8ee64
Migrate home screen drawer to Compose (#3554)
gino-m Mar 17, 2026
a2ed615
Refactor DrawAreaTaskViewModel, change showSelfIntersectionDialog an…
hassan-nsubuga Mar 19, 2026
8bc5523
Refactor DrawAreaTaskViewModelTest to add more tests to cover the cha…
hassan-nsubuga Mar 19, 2026
7f56a0f
Merge branch 'master' into bugfix/3541
hassan-nsubuga Mar 19, 2026
daa7cd4
Remove `imageUrl` binding adapter from `BindingAdapters` (#3620)
shobhitagarwal1612 Mar 19, 2026
4aeda4f
Fix firebase emulator failing CI (#3613)
andreia-ferreira Mar 19, 2026
7975a9e
Migrate TaskView to compose (#3617)
shobhitagarwal1612 Mar 19, 2026
750b1d6
Refactor HomeDrawer to use Material 3 typography (#3623)
shobhitagarwal1612 Mar 19, 2026
3744ab3
Bump kotlinVersion from 2.3.10 to 2.3.20 (#3616)
dependabot[bot] Mar 20, 2026
5b53d01
Bump androidx.compose:compose-bom from 2026.02.01 to 2026.03.00 (#3609)
dependabot[bot] Mar 20, 2026
62380f9
fix delete loi called on every started lifecycle event (#3618)
andreia-ferreira Mar 20, 2026
7a14f2f
Bump gradle-wrapper from 9.4.0 to 9.4.1 (#3626)
dependabot[bot] Mar 23, 2026
69dbdc8
Refactor DrawAreaTaskViewModel, change showSelfIntersectionDialog an…
hassan-nsubuga Mar 19, 2026
838c8b2
Refactor, rename method to showSelfIntersectionDialog, make it priva…
hassan-nsubuga Mar 19, 2026
258a133
Refactor, add tearDown method to reset dispatcher and prevent possibl…
hassan-nsubuga Mar 19, 2026
8ef5d61
Collect showSelfIntersectionDialog using collectAsStateWithLifecycle,…
hassan-nsubuga Mar 23, 2026
af69b35
Format code in DrawAreaTaskFragment
hassan-nsubuga Mar 23, 2026
fb2f968
Merge branch 'master' into bugfix/3541
hassan-nsubuga Mar 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,13 @@ class PhotoTaskViewModel @Inject constructor(private val userMediaRepository: Us

val showPermissionDeniedDialog = mutableStateOf(false)

val uri: Flow<Uri> = taskTaskData.map { taskData ->
if (taskData is PhotoTaskData && taskData.isNotNullOrEmpty()) {
userMediaRepository.getDownloadUrl(taskData.remoteFilename)
} else {
Uri.EMPTY
val uri: Flow<Uri> =
taskTaskData.map { taskData ->
if (taskData is PhotoTaskData && taskData.isNotNullOrEmpty()) {
userMediaRepository.getDownloadUrl(taskData.remoteFilename)
} else {
Uri.EMPTY
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ import android.view.LayoutInflater
import android.widget.Toast
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.os.bundleOf
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
Expand Down Expand Up @@ -53,7 +53,8 @@ class DrawAreaTaskFragment @Inject constructor() : AbstractTaskFragment<DrawArea

@Composable
override fun TaskBody() {
var showSelfIntersectionDialog by viewModel.showSelfIntersectionDialog
val showSelfIntersectionDialog by
viewModel.showSelfIntersectionDialog.collectAsStateWithLifecycle()

AndroidView(
factory = { context ->
Expand Down Expand Up @@ -86,7 +87,8 @@ class DrawAreaTaskFragment @Inject constructor() : AbstractTaskFragment<DrawArea
description = R.string.polygon_vertex_add_dialog_message,
confirmButtonText = R.string.polygon_vertex_add_dialog_positive_button,
dismissButtonText = null,
onConfirmClicked = { showSelfIntersectionDialog = false },
onConfirmClicked = { viewModel.dismissSelfIntersectionDialog() },
onDismiss = { viewModel.dismissSelfIntersectionDialog() },
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,24 +140,28 @@ internal constructor(
private val _isTooClose = MutableStateFlow(false)
val isTooClose: StateFlow<Boolean> = _isTooClose.asStateFlow()

val showSelfIntersectionDialog = mutableStateOf(false)
private val _showSelfIntersectionDialog = MutableStateFlow(false)
val showSelfIntersectionDialog: StateFlow<Boolean> = _showSelfIntersectionDialog.asStateFlow()

var hasSelfIntersection: Boolean = false
private set
private val _hasSelfIntersection = MutableStateFlow(false)
val hasSelfIntersection: StateFlow<Boolean> = _hasSelfIntersection.asStateFlow()

private lateinit var featureStyle: Feature.Style
lateinit var measurementUnits: MeasurementUnits

override val taskActionButtonStates: StateFlow<List<ButtonActionState>> by lazy {
combine(taskTaskData, merge(draftArea, draftUpdates)) { taskData, currentFeature ->
combine(taskTaskData, merge(draftArea, draftUpdates), hasSelfIntersection) {
taskData,
currentFeature,
intersected ->
val isClosed = (currentFeature?.geometry as? LineString)?.isClosed() ?: false
listOfNotNull(
getPreviousButton(),
getSkipButton(taskData),
getUndoButton(taskData, true),
getRedoButton(taskData),
getAddPointButton(isClosed, isTooClose.value),
getCompleteButton(isClosed, isMarkedComplete.value, hasSelfIntersection),
getCompleteButton(isClosed, isMarkedComplete.value, intersected),
getNextButton(taskData).takeIf { isMarkedComplete() },
)
}
Expand Down Expand Up @@ -202,8 +206,13 @@ internal constructor(

@VisibleForTesting fun getLastVertex() = vertices.lastOrNull()

private fun onSelfIntersectionDetected() {
showSelfIntersectionDialog.value = true
private fun showSelfIntersectionDialog() {
_showSelfIntersectionDialog.value = true
}

fun dismissSelfIntersectionDialog() {
_showSelfIntersectionDialog.value = false
resetHasSelfIntersection()
}

/**
Expand Down Expand Up @@ -319,12 +328,12 @@ internal constructor(
}

private fun checkVertexIntersection(): Boolean {
hasSelfIntersection = isSelfIntersecting(vertices)
if (hasSelfIntersection) {
vertices = vertices.dropLast(1)
onSelfIntersectionDetected()
_hasSelfIntersection.value = isSelfIntersecting(vertices)
if (_hasSelfIntersection.value) {
updateVertices(vertices.dropLast(1))
showSelfIntersectionDialog()
}
return hasSelfIntersection
return _hasSelfIntersection.value
}

private fun validatePolygonCompletion(): Boolean {
Expand All @@ -339,9 +348,9 @@ internal constructor(
vertices
}

hasSelfIntersection = isSelfIntersecting(ring)
if (hasSelfIntersection) {
onSelfIntersectionDetected()
_hasSelfIntersection.value = isSelfIntersecting(ring)
if (_hasSelfIntersection.value) {
showSelfIntersectionDialog()
return false
}
return true
Expand All @@ -352,6 +361,10 @@ internal constructor(
refreshMap()
}

fun resetHasSelfIntersection() {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is only used inside the VM so it can be private

_hasSelfIntersection.value = false
}

@VisibleForTesting
fun completePolygon() {
check(LineString(vertices).isClosed()) { "Polygon is not complete" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,17 @@ import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import org.groundplatform.android.BaseHiltTest
import org.groundplatform.android.data.local.LocalValueStore
import org.groundplatform.android.model.job.Job
Expand All @@ -53,6 +58,7 @@ 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.junit.After
import org.junit.Assert.assertThrows
import org.junit.Test
import org.junit.runner.RunWith
Expand All @@ -72,6 +78,7 @@ class DrawAreaTaskViewModelTest : BaseHiltTest() {

override fun setUp() {
super.setUp()
Dispatchers.setMain(UnconfinedTestDispatcher())
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when overriding the main test dispatcher, it should be reset otherwise it may leak into other tests. I suggest adding the following to the test file to prevent it:

  @After
  fun tearDown() {
    Dispatchers.resetMain()
  }

mergedFeatureFlow = merge(viewModel.draftArea.filterNotNull(), viewModel.draftUpdates)

mergedFeatureLiveData = mergedFeatureFlow.asLiveData()
Expand Down Expand Up @@ -620,6 +627,95 @@ class DrawAreaTaskViewModelTest : BaseHiltTest() {
assertThat(viewModel.isMarkedComplete()).isTrue()
}

@Test
fun `checkVertexIntersection sets hasSelfIntersection and showSelfIntersectionDialog to true`() =
runWithTestDispatcher {
setupViewModel()

// Create a path: (0,0) -> (10,10) -> (0,10)
updateLastVertexAndAdd(COORDINATE_1)
updateLastVertexAndAdd(COORDINATE_2)
updateLastVertexAndAdd(COORDINATE_6)

// Move cursor to a point that crosses the first segment: (10,0)
updateLastVertex(COORDINATE_5)

// Trigger ADD_POINT which calls checkVertexIntersection
viewModel.onButtonClick(ButtonAction.ADD_POINT)
advanceUntilIdle()

assertThat(viewModel.hasSelfIntersection.value).isTrue()
assertThat(viewModel.showSelfIntersectionDialog.value).isTrue()
// offending vertex should be dropped
assertGeometry(4, isLineString = true)
}

@Test
fun `dismissSelfIntersectionDialog resets both flag and dialog visibility`() =
runWithTestDispatcher {
setupViewModel()

// Force an intersection state
updateLastVertexAndAdd(COORDINATE_1)
updateLastVertexAndAdd(COORDINATE_2)
updateLastVertexAndAdd(COORDINATE_6)
updateLastVertex(COORDINATE_5)
viewModel.onButtonClick(ButtonAction.ADD_POINT)
advanceUntilIdle()

viewModel.dismissSelfIntersectionDialog()
assertThat(viewModel.hasSelfIntersection.value).isFalse()
assertThat(viewModel.showSelfIntersectionDialog.value).isFalse()
}

@Test
fun `taskActionButtonStates re-enables COMPLETE button after intersection is dismissed`() =
runTest(UnconfinedTestDispatcher()) { // Use Unconfined to trigger emissions immediately
setupViewModel()

viewModel.taskActionButtonStates.test {
// 1. Initial State: Create a closed non-intersecting square
updateLastVertexAndAdd(COORDINATE_1)
updateLastVertexAndAdd(COORDINATE_2)
updateLastVertexAndAdd(COORDINATE_3)
// Set state to be near first vertex to enable COMPLETE
updateLastVertex(COORDINATE_1, isNearFirstVertex = true)

// 2. Trigger Intersection Logic
// Clear state (Simulated)
while (viewModel.getLastVertex() != null) {
viewModel.removeLastVertex()
}

updateLastVertexAndAdd(COORDINATE_1)
updateLastVertexAndAdd(COORDINATE_2)
updateLastVertexAndAdd(COORDINATE_6)
updateLastVertexAndAdd(COORDINATE_5)
updateLastVertex(COORDINATE_1, isNearFirstVertex = true)

// Trigger validation
viewModel.onButtonClick(ButtonAction.COMPLETE)

// 3. Assert: Button should eventually become disabled due to intersection
// We look for the emission where COMPLETE is disabled
expectMostRecentItem().let { latestStates ->
val completeAction = latestStates.find { it.action == ButtonAction.COMPLETE }
assertThat(completeAction?.isEnabled).isFalse()
}

// 4. Act: Dismiss and Verify
viewModel.dismissSelfIntersectionDialog()

// 5. Assert: Button is enabled again
expectMostRecentItem().let { latestStates ->
val completeAction = latestStates.find { it.action == ButtonAction.COMPLETE }
assertThat(completeAction?.isEnabled).isTrue()
}

cancelAndIgnoreRemainingEvents()
}
}

private fun assertGeometry(
expectedVerticesCount: Int,
isLineString: Boolean = false,
Expand Down Expand Up @@ -679,6 +775,8 @@ class DrawAreaTaskViewModelTest : BaseHiltTest() {
private val COORDINATE_2 = Coordinates(10.0, 10.0)
private val COORDINATE_3 = Coordinates(20.0, 20.0)
private val COORDINATE_4 = Coordinates(30.0, 30.0)
private val COORDINATE_5 = Coordinates(10.0, 0.0)
private val COORDINATE_6 = Coordinates(0.0, 10.0)

private val TASK =
Task(
Expand All @@ -690,4 +788,9 @@ class DrawAreaTaskViewModelTest : BaseHiltTest() {
)
private val JOB = Job("job", Style("#112233"))
}

@After
fun tearDown() {
Dispatchers.resetMain()
}
}
Loading