-
Notifications
You must be signed in to change notification settings - Fork 141
Fix issue 3541, Can't complete polygon after getting an interception error popup #3606
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
9556c93
5cb0e85
520a374
951c359
6f8ee64
a2ed615
8bc5523
7f56a0f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -139,25 +139,30 @@ internal constructor( | |
| private val _isTooClose = MutableStateFlow(false) | ||
| val isTooClose: StateFlow<Boolean> = _isTooClose.asStateFlow() | ||
|
|
||
| private val _showSelfIntersectionDialog = MutableSharedFlow<Unit>() | ||
| val showSelfIntersectionDialog = _showSelfIntersectionDialog.asSharedFlow() | ||
| 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() | ||
| // var hasSelfIntersection: Boolean = false | ||
| // private set | ||
|
|
||
| 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() }, | ||
| ) | ||
| } | ||
|
|
@@ -203,7 +208,12 @@ internal constructor( | |
| @VisibleForTesting fun getLastVertex() = vertices.lastOrNull() | ||
|
|
||
| private fun onSelfIntersectionDetected() { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion: not related to this PR, so feel free to resolve this commen, but maybe a clearer name for this method would be |
||
| viewModelScope.launch { _showSelfIntersectionDialog.emit(Unit) } | ||
| _showSelfIntersectionDialog.value = true | ||
| } | ||
|
|
||
| fun dismissSelfIntersectionDialog() { | ||
| _showSelfIntersectionDialog.value = false | ||
| resetHasSelfIntersection() | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -319,12 +329,12 @@ internal constructor( | |
| } | ||
|
|
||
| private fun checkVertexIntersection(): Boolean { | ||
| hasSelfIntersection = isSelfIntersecting(vertices) | ||
| if (hasSelfIntersection) { | ||
| vertices = vertices.dropLast(1) | ||
| _hasSelfIntersection.value = isSelfIntersecting(vertices) | ||
| if (_hasSelfIntersection.value) { | ||
| updateVertices(vertices.dropLast(1)) | ||
| onSelfIntersectionDetected() | ||
| } | ||
| return hasSelfIntersection | ||
| return _hasSelfIntersection.value | ||
| } | ||
|
|
||
| private fun validatePolygonCompletion(): Boolean { | ||
|
|
@@ -339,8 +349,8 @@ internal constructor( | |
| vertices | ||
| } | ||
|
|
||
| hasSelfIntersection = isSelfIntersecting(ring) | ||
| if (hasSelfIntersection) { | ||
| _hasSelfIntersection.value = isSelfIntersecting(ring) | ||
| if (_hasSelfIntersection.value) { | ||
| onSelfIntersectionDetected() | ||
| return false | ||
| } | ||
|
|
@@ -352,6 +362,10 @@ internal constructor( | |
| refreshMap() | ||
| } | ||
|
|
||
| fun resetHasSelfIntersection() { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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" } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -29,12 +29,16 @@ 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.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 | ||
|
|
@@ -72,6 +76,7 @@ class DrawAreaTaskViewModelTest : BaseHiltTest() { | |
|
|
||
| override fun setUp() { | ||
| super.setUp() | ||
| Dispatchers.setMain(UnconfinedTestDispatcher()) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: |
||
| mergedFeatureFlow = merge(viewModel.draftArea.filterNotNull(), viewModel.draftUpdates) | ||
|
|
||
| mergedFeatureLiveData = mergedFeatureFlow.asLiveData() | ||
|
|
@@ -620,6 +625,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, | ||
|
|
@@ -679,6 +773,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( | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
unused code can be removed