diff --git a/androidshared/src/main/java/org/odk/collect/androidshared/utils/ColorUtils.kt b/androidshared/src/main/java/org/odk/collect/androidshared/utils/ColorUtils.kt new file mode 100644 index 00000000000..c3cf1bd3c95 --- /dev/null +++ b/androidshared/src/main/java/org/odk/collect/androidshared/utils/ColorUtils.kt @@ -0,0 +1,27 @@ +package org.odk.collect.androidshared.utils + +import android.graphics.Color +import androidx.annotation.ColorInt + +@ColorInt +fun String.toColorInt() = try { + var sanitizedColor = if (this.startsWith("#")) { + this + } else { + "#$this" + } + + if (sanitizedColor.length == 4) { + sanitizedColor = shorthandToLonghandHexColor(sanitizedColor) + } + + Color.parseColor(sanitizedColor) +} catch (e: IllegalArgumentException) { + null +} + +private fun shorthandToLonghandHexColor(shorthandColor: String): String { + return shorthandColor.substring(1).fold("#") { accum, char -> + "$accum$char$char" + } +} diff --git a/androidshared/src/test/java/org/odk/collect/androidshared/utils/ColorUtilsTest.kt b/androidshared/src/test/java/org/odk/collect/androidshared/utils/ColorUtilsTest.kt new file mode 100644 index 00000000000..52c83e58885 --- /dev/null +++ b/androidshared/src/test/java/org/odk/collect/androidshared/utils/ColorUtilsTest.kt @@ -0,0 +1,45 @@ +package org.odk.collect.androidshared.utils + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.equalTo +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class ColorUtilsTest { + @Test + fun `return null when color is empty`() { + assertThat("".toColorInt(), equalTo(null)) + } + + @Test + fun `return null when color is blank`() { + assertThat(" ".toColorInt(), equalTo(null)) + } + + @Test + fun `return null when color is invalid`() { + assertThat("qwerty".toColorInt(), equalTo(null)) + } + + @Test + fun `return color int for valid hex color with # prefix`() { + assertThat("#aaccee".toColorInt(), equalTo(-5583634)) + } + + @Test + fun `return color int for valid hex color without # prefix`() { + assertThat("aaccee".toColorInt(), equalTo(-5583634)) + } + + @Test + fun `return color int for valid shorthand hex color with # prefix`() { + assertThat("#ace".toColorInt(), equalTo(-5583634)) + } + + @Test + fun `return color int for valid shorthand hex color without # prefix`() { + assertThat("ace".toColorInt(), equalTo(-5583634)) + } +} diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/support/FakeClickableMapFragment.kt b/collect_app/src/androidTest/java/org/odk/collect/android/support/FakeClickableMapFragment.kt index 31df3597381..f2a7e6f5428 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/support/FakeClickableMapFragment.kt +++ b/collect_app/src/androidTest/java/org/odk/collect/android/support/FakeClickableMapFragment.kt @@ -3,8 +3,10 @@ package org.odk.collect.android.support import android.os.Handler import android.os.Looper import androidx.fragment.app.Fragment +import org.odk.collect.maps.LineDescription import org.odk.collect.maps.MapFragment import org.odk.collect.maps.MapPoint +import org.odk.collect.maps.PolygonDescription import org.odk.collect.maps.markers.MarkerDescription import org.odk.collect.maps.markers.MarkerIconDescription @@ -57,16 +59,12 @@ class FakeClickableMapFragment : Fragment(), MapFragment { return MapPoint(0.0, 0.0) } - override fun addPolyLine( - points: MutableIterable, - closed: Boolean, - draggable: Boolean - ): Int { + override fun addPolyLine(lineDescription: LineDescription): Int { return -1 } - override fun addPolygon(points: MutableIterable): Int { - return addPolyLine(points, closed = true, draggable = false) + override fun addPolygon(polygonDescription: PolygonDescription): Int { + return -1 } override fun appendPointToPolyLine(featureId: Int, point: MapPoint) {} diff --git a/collect_app/src/main/java/org/odk/collect/android/formmanagement/formmap/FormMapViewModel.kt b/collect_app/src/main/java/org/odk/collect/android/formmanagement/formmap/FormMapViewModel.kt index 057270a6901..720f070dd20 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formmanagement/formmap/FormMapViewModel.kt +++ b/collect_app/src/main/java/org/odk/collect/android/formmanagement/formmap/FormMapViewModel.kt @@ -115,12 +115,12 @@ class FormMapViewModel( ) val info = "$instanceLastStatusChangeDate\n${dateFormat.format(instance.deletedDate)}" - MappableSelectItem( + MappableSelectItem.MappableSelectPoint( instance.dbId, - listOf(MapPoint(latitude, longitude)), - getDrawableIdForStatus(instance.status, false), - getDrawableIdForStatus(instance.status, true), instance.displayName, + point = MapPoint(latitude, longitude), + smallIcon = getDrawableIdForStatus(instance.status, false), + largeIcon = getDrawableIdForStatus(instance.status, true), info = info ) } else if (!instance.canEditWhenComplete() && listOf( @@ -130,12 +130,12 @@ class FormMapViewModel( ).contains(instance.status) ) { val info = "$instanceLastStatusChangeDate\n${resources.getString(org.odk.collect.strings.R.string.cannot_edit_completed_form)}" - MappableSelectItem( + MappableSelectItem.MappableSelectPoint( instance.dbId, - listOf(MapPoint(latitude, longitude)), - getDrawableIdForStatus(instance.status, false), - getDrawableIdForStatus(instance.status, true), instance.displayName, + point = MapPoint(latitude, longitude), + smallIcon = getDrawableIdForStatus(instance.status, false), + largeIcon = getDrawableIdForStatus(instance.status, true), info = info ) } else { @@ -146,12 +146,12 @@ class FormMapViewModel( createViewAction() } - MappableSelectItem( + MappableSelectItem.MappableSelectPoint( instance.dbId, - listOf(MapPoint(latitude, longitude)), - getDrawableIdForStatus(instance.status, false), - getDrawableIdForStatus(instance.status, true), instance.displayName, + point = MapPoint(latitude, longitude), + smallIcon = getDrawableIdForStatus(instance.status, false), + largeIcon = getDrawableIdForStatus(instance.status, true), info = instanceLastStatusChangeDate, action = action, status = instanceStatusToMappableSelectionItemStatus(instance) diff --git a/collect_app/src/main/java/org/odk/collect/android/widgets/items/SelectOneFromMapDialogFragment.kt b/collect_app/src/main/java/org/odk/collect/android/widgets/items/SelectOneFromMapDialogFragment.kt index beab6944ae2..08b8a5e9466 100644 --- a/collect_app/src/main/java/org/odk/collect/android/widgets/items/SelectOneFromMapDialogFragment.kt +++ b/collect_app/src/main/java/org/odk/collect/android/widgets/items/SelectOneFromMapDialogFragment.kt @@ -136,7 +136,7 @@ internal class SelectChoicesMapData( prompt: FormEntryPrompt ): List { return selectChoices.foldIndexed(emptyList()) { index, list, selectChoice -> - val geometry = selectChoice.getChild("geometry") + val geometry = selectChoice.getChild(GEOMETRY) if (geometry != null) { try { @@ -153,26 +153,57 @@ internal class SelectChoicesMapData( IconifiedText(null, "${it.first}: ${it.second}") } - val markerColor = - selectChoice.additionalChildren.firstOrNull { it.first == "marker-color" }?.second - val markerSymbol = - selectChoice.additionalChildren.firstOrNull { it.first == "marker-symbol" }?.second - - list + MappableSelectItem( - index.toLong(), - points, - if (markerSymbol == null) org.odk.collect.icons.R.drawable.ic_map_marker_with_hole_small else org.odk.collect.icons.R.drawable.ic_map_marker_small, - if (markerSymbol == null) org.odk.collect.icons.R.drawable.ic_map_marker_with_hole_big else org.odk.collect.icons.R.drawable.ic_map_marker_big, - prompt.getSelectChoiceText(selectChoice), - properties, - selectChoice.index == selectedIndex, - markerColor, - markerSymbol, - action = IconifiedText( - org.odk.collect.icons.R.drawable.ic_save, - resources.getString(org.odk.collect.strings.R.string.select_item) + if (points.size == 1) { + val markerColor = + getPropertyValue(selectChoice, MARKER_COLOR) + val markerSymbol = + getPropertyValue(selectChoice, MARKER_SYMBOL) + + list + MappableSelectItem.MappableSelectPoint( + index.toLong(), + prompt.getSelectChoiceText(selectChoice), + properties, + selectChoice.index == selectedIndex, + point = points[0], + smallIcon = if (markerSymbol == null) org.odk.collect.icons.R.drawable.ic_map_marker_with_hole_small else org.odk.collect.icons.R.drawable.ic_map_marker_small, + largeIcon = if (markerSymbol == null) org.odk.collect.icons.R.drawable.ic_map_marker_with_hole_big else org.odk.collect.icons.R.drawable.ic_map_marker_big, + color = markerColor, + symbol = markerSymbol, + action = IconifiedText( + org.odk.collect.icons.R.drawable.ic_save, + resources.getString(org.odk.collect.strings.R.string.select_item) + ) ) - ) + } else if (points.first() != points.last()) { + list + MappableSelectItem.MappableSelectLine( + index.toLong(), + prompt.getSelectChoiceText(selectChoice), + properties, + selectChoice.index == selectedIndex, + points = points, + action = IconifiedText( + org.odk.collect.icons.R.drawable.ic_save, + resources.getString(org.odk.collect.strings.R.string.select_item) + ), + strokeWidth = getPropertyValue(selectChoice, STROKE_WIDTH), + strokeColor = getPropertyValue(selectChoice, STROKE) + ) + } else { + list + MappableSelectItem.MappableSelectPolygon( + index.toLong(), + prompt.getSelectChoiceText(selectChoice), + properties, + selectChoice.index == selectedIndex, + points = points, + action = IconifiedText( + org.odk.collect.icons.R.drawable.ic_save, + resources.getString(org.odk.collect.strings.R.string.select_item) + ), + strokeWidth = getPropertyValue(selectChoice, STROKE_WIDTH), + strokeColor = getPropertyValue(selectChoice, STROKE), + fillColor = getPropertyValue(selectChoice, FILL) + ) + } } else { list } @@ -188,6 +219,10 @@ internal class SelectChoicesMapData( } } + private fun getPropertyValue(selectChoice: SelectChoice, propertyName: String): String? { + return selectChoice.additionalChildren.firstOrNull { it.first == propertyName }?.second + } + override fun isLoading(): NonNullLiveData { return isLoading } @@ -207,4 +242,13 @@ internal class SelectChoicesMapData( override fun getMappableItems(): LiveData?> { return items } + + companion object PropertyNames { + const val GEOMETRY = "geometry" + const val MARKER_COLOR = "marker-color" + const val MARKER_SYMBOL = "marker-symbol" + const val STROKE = "stroke" + const val STROKE_WIDTH = "stroke-width" + const val FILL = "fill" + } } diff --git a/collect_app/src/test/java/org/odk/collect/android/formmanagement/formmap/FormMapViewModelTest.kt b/collect_app/src/test/java/org/odk/collect/android/formmanagement/formmap/FormMapViewModelTest.kt index d5fee4d392c..12324d344fb 100644 --- a/collect_app/src/test/java/org/odk/collect/android/formmanagement/formmap/FormMapViewModelTest.kt +++ b/collect_app/src/test/java/org/odk/collect/android/formmanagement/formmap/FormMapViewModelTest.kt @@ -106,12 +106,12 @@ class FormMapViewModelTest { val viewModel = createAndLoadViewModel(form) assertThat(viewModel.getMappableItems().value!!.size, equalTo(1)) - val expectedItem = MappableSelectItem( + val expectedItem = MappableSelectItem.MappableSelectPoint( instanceWithPoint.dbId, - listOf(MapPoint(2.0, 1.0)), - R.drawable.ic_room_form_state_incomplete_24dp, - R.drawable.ic_room_form_state_incomplete_48dp, instanceWithPoint.displayName, + point = MapPoint(2.0, 1.0), + smallIcon = R.drawable.ic_room_form_state_incomplete_24dp, + largeIcon = R.drawable.ic_room_form_state_incomplete_48dp, action = IconifiedText( R.drawable.ic_edit, application.getString(org.odk.collect.strings.R.string.edit_data) @@ -145,12 +145,12 @@ class FormMapViewModelTest { ) val viewModel = createAndLoadViewModel(form) - val expectedItem = MappableSelectItem( + val expectedItem = MappableSelectItem.MappableSelectPoint( instance.dbId, - listOf(MapPoint(2.0, 1.0)), - R.drawable.ic_room_form_state_incomplete_24dp, - R.drawable.ic_room_form_state_incomplete_48dp, instance.displayName, + point = MapPoint(2.0, 1.0), + smallIcon = R.drawable.ic_room_form_state_incomplete_24dp, + largeIcon = R.drawable.ic_room_form_state_incomplete_48dp, action = IconifiedText( R.drawable.ic_edit, application.getString(org.odk.collect.strings.R.string.edit_data) @@ -184,12 +184,12 @@ class FormMapViewModelTest { ) val viewModel = createAndLoadViewModel(form) - val expectedItem = MappableSelectItem( + val expectedItem = MappableSelectItem.MappableSelectPoint( instance.dbId, - listOf(MapPoint(2.0, 1.0)), - R.drawable.ic_room_form_state_incomplete_24dp, - R.drawable.ic_room_form_state_incomplete_48dp, instance.displayName, + point = MapPoint(2.0, 1.0), + smallIcon = R.drawable.ic_room_form_state_incomplete_24dp, + largeIcon = R.drawable.ic_room_form_state_incomplete_48dp, action = IconifiedText( R.drawable.ic_edit, application.getString(org.odk.collect.strings.R.string.edit_data) @@ -223,12 +223,12 @@ class FormMapViewModelTest { ) val viewModel = createAndLoadViewModel(form) - val expectedItem = MappableSelectItem( + val expectedItem = MappableSelectItem.MappableSelectPoint( instance.dbId, - listOf(MapPoint(2.0, 1.0)), - R.drawable.ic_room_form_state_complete_24dp, - R.drawable.ic_room_form_state_complete_48dp, instance.displayName, + point = MapPoint(2.0, 1.0), + smallIcon = R.drawable.ic_room_form_state_complete_24dp, + largeIcon = R.drawable.ic_room_form_state_complete_48dp, action = IconifiedText( R.drawable.ic_visibility, application.getString(org.odk.collect.strings.R.string.view_data) @@ -263,12 +263,12 @@ class FormMapViewModelTest { settingsProvider.getProtectedSettings().save(ProtectedProjectKeys.KEY_EDIT_SAVED, false) val viewModel = createAndLoadViewModel(form) - val expectedItem = MappableSelectItem( + val expectedItem = MappableSelectItem.MappableSelectPoint( instance.dbId, - listOf(MapPoint(2.0, 1.0)), - R.drawable.ic_room_form_state_complete_24dp, - R.drawable.ic_room_form_state_complete_48dp, instance.displayName, + point = MapPoint(2.0, 1.0), + smallIcon = R.drawable.ic_room_form_state_complete_24dp, + largeIcon = R.drawable.ic_room_form_state_complete_48dp, action = IconifiedText( R.drawable.ic_visibility, application.getString(org.odk.collect.strings.R.string.view_data) @@ -301,12 +301,12 @@ class FormMapViewModelTest { ) val viewModel = createAndLoadViewModel(form) - val expectedItem = MappableSelectItem( + val expectedItem = MappableSelectItem.MappableSelectPoint( instance.dbId, - listOf(MapPoint(2.0, 1.0)), - R.drawable.ic_room_form_state_incomplete_24dp, - R.drawable.ic_room_form_state_incomplete_48dp, instance.displayName, + point = MapPoint(2.0, 1.0), + smallIcon = R.drawable.ic_room_form_state_incomplete_24dp, + largeIcon = R.drawable.ic_room_form_state_incomplete_48dp, info = formatDate( org.odk.collect.strings.R.string.saved_on_date_at_time, instance.lastStatusChangeDate @@ -339,12 +339,12 @@ class FormMapViewModelTest { ) val viewModel = createAndLoadViewModel(form) - val expectedItem = MappableSelectItem( + val expectedItem = MappableSelectItem.MappableSelectPoint( instance.dbId, - listOf(MapPoint(2.0, 1.0)), - R.drawable.ic_room_form_state_submitted_24dp, - R.drawable.ic_room_form_state_submitted_48dp, instance.displayName, + point = MapPoint(2.0, 1.0), + smallIcon = R.drawable.ic_room_form_state_submitted_24dp, + largeIcon = R.drawable.ic_room_form_state_submitted_48dp, action = IconifiedText( R.drawable.ic_visibility, application.getString(org.odk.collect.strings.R.string.view_data) @@ -377,12 +377,12 @@ class FormMapViewModelTest { ) val viewModel = createAndLoadViewModel(form) - val expectedItem = MappableSelectItem( + val expectedItem = MappableSelectItem.MappableSelectPoint( instance.dbId, - listOf(MapPoint(2.0, 1.0)), - R.drawable.ic_room_form_state_submission_failed_24dp, - R.drawable.ic_room_form_state_submission_failed_48dp, instance.displayName, + point = MapPoint(2.0, 1.0), + smallIcon = R.drawable.ic_room_form_state_submission_failed_24dp, + largeIcon = R.drawable.ic_room_form_state_submission_failed_48dp, action = IconifiedText( R.drawable.ic_visibility, application.getString(org.odk.collect.strings.R.string.view_data) diff --git a/collect_app/src/test/java/org/odk/collect/android/widgets/items/SelectChoicesMapDataTest.kt b/collect_app/src/test/java/org/odk/collect/android/widgets/items/SelectChoicesMapDataTest.kt index 96359ba0f0d..50292fa3f9e 100644 --- a/collect_app/src/test/java/org/odk/collect/android/widgets/items/SelectChoicesMapDataTest.kt +++ b/collect_app/src/test/java/org/odk/collect/android/widgets/items/SelectChoicesMapDataTest.kt @@ -16,6 +16,7 @@ import org.odk.collect.android.widgets.support.FormElementFixtures.selectChoice import org.odk.collect.android.widgets.support.FormElementFixtures.treeElement import org.odk.collect.androidtest.getOrAwaitValue import org.odk.collect.geo.selection.IconifiedText +import org.odk.collect.geo.selection.MappableSelectItem import org.odk.collect.maps.MapPoint import org.odk.collect.testshared.FakeScheduler @@ -33,7 +34,7 @@ class SelectChoicesMapDataTest { selectChoice( value = "a", item = treeElement( - children = listOf(treeElement("geometry", "12.0 -1.0 3 4; 12.1 -1.0 3 4")) + children = listOf(treeElement(SelectChoicesMapData.GEOMETRY, "12.0 -1.0 3 4; 12.1 -1.0 3 4")) ) ) ) @@ -50,7 +51,7 @@ class SelectChoicesMapDataTest { val mappableItems = data.getMappableItems().getOrAwaitValue()!! assertThat(mappableItems.size, equalTo(1)) - val points = mappableItems[0].points + val points = (mappableItems[0] as MappableSelectItem.MappableSelectLine).points assertThat( points, equalTo(listOf(MapPoint(12.0, -1.0, 3.0, 4.0), MapPoint(12.1, -1.0, 3.0, 4.0))) @@ -63,7 +64,7 @@ class SelectChoicesMapDataTest { selectChoice( value = "a", item = treeElement( - children = listOf(treeElement("geometry", "12.0 -1.0 3 4; 12.1 -1.0 3 4; 12.0 -1.0 3 4")) + children = listOf(treeElement(SelectChoicesMapData.GEOMETRY, "12.0 -1.0 3 4; 12.1 -1.0 3 4; 12.0 -1.0 3 4")) ) ) ) @@ -80,7 +81,7 @@ class SelectChoicesMapDataTest { val mappableItems = data.getMappableItems().getOrAwaitValue()!! assertThat(mappableItems.size, equalTo(1)) - val points = mappableItems[0].points + val points = (mappableItems[0] as MappableSelectItem.MappableSelectPolygon).points assertThat( points, equalTo(listOf(MapPoint(12.0, -1.0, 3.0, 4.0), MapPoint(12.1, -1.0, 3.0, 4.0), MapPoint(12.0, -1.0, 3.0, 4.0))) @@ -92,7 +93,7 @@ class SelectChoicesMapDataTest { val choices = listOf( selectChoice( value = "a", - item = treeElement(children = listOf(treeElement("geometry", "12.0 -1.0 305 0"))) + item = treeElement(children = listOf(treeElement(SelectChoicesMapData.GEOMETRY, "12.0 -1.0 305 0"))) ), selectChoice( value = "b", @@ -119,7 +120,7 @@ class SelectChoicesMapDataTest { value = "a", item = treeElement( children = listOf( - treeElement("geometry", "12.0 -1.0 305 0"), + treeElement(SelectChoicesMapData.GEOMETRY, "12.0 -1.0 305 0"), treeElement("property", "blah") ) ) @@ -164,7 +165,7 @@ class SelectChoicesMapDataTest { value = "a", item = treeElement( children = listOf( - treeElement("geometry", "0 170.00 0 0") + treeElement(SelectChoicesMapData.GEOMETRY, "0 170.00 0 0") ) ) ), @@ -173,7 +174,7 @@ class SelectChoicesMapDataTest { value = "b", item = treeElement( children = listOf( - treeElement("geometry", "blah") + treeElement(SelectChoicesMapData.GEOMETRY, "blah") ) ) ), @@ -182,7 +183,7 @@ class SelectChoicesMapDataTest { value = "c", item = treeElement( children = listOf( - treeElement("geometry", "0 180.1 0 0") + treeElement(SelectChoicesMapData.GEOMETRY, "0 180.1 0 0") ) ) ), @@ -191,7 +192,7 @@ class SelectChoicesMapDataTest { value = "c", item = treeElement( children = listOf( - treeElement("geometry", "0 180 0 0; 0 180.1 0 0") + treeElement(SelectChoicesMapData.GEOMETRY, "0 180 0 0; 0 180.1 0 0") ) ) ) @@ -218,9 +219,9 @@ class SelectChoicesMapDataTest { value = "a", item = treeElement( children = listOf( - treeElement("geometry", "12.0 -1.0 305 0"), - treeElement("marker-symbol", "A"), - treeElement("marker-color", "#ffffff") + treeElement(SelectChoicesMapData.GEOMETRY, "12.0 -1.0 305 0"), + treeElement(SelectChoicesMapData.MARKER_SYMBOL, "A"), + treeElement(SelectChoicesMapData.MARKER_COLOR, "#ffffff") ) ) ) @@ -233,11 +234,75 @@ class SelectChoicesMapDataTest { .build() val data = loadDataForPrompt(prompt) - val item = data.getMappableItems().getOrAwaitValue()!![0] + val item = data.getMappableItems().getOrAwaitValue()!![0] as MappableSelectItem.MappableSelectPoint assertThat(item.symbol, equalTo("A")) assertThat(item.color, equalTo("#ffffff")) } + /** + * Attributes names come from properties defined at + * https://github.com/mapbox/simplestyle-spec/tree/master/1.1.0. + */ + @Test + fun `line stroke color is pulled from simple style attributes`() { + val choices = listOf( + selectChoice( + value = "a", + item = treeElement( + children = listOf( + treeElement(SelectChoicesMapData.GEOMETRY, "12.0 -1.0 3 4; 12.1 -1.0 3 4"), + treeElement(SelectChoicesMapData.STROKE_WIDTH, "10"), + treeElement(SelectChoicesMapData.STROKE, "#ffffff") + ) + ) + ) + ) + + val prompt = MockFormEntryPromptBuilder() + .withLongText("Which is your favourite place?") + .withSelectChoices(choices) + .withSelectChoiceText(mapOf(choices[0] to "A")) + .build() + + val data = loadDataForPrompt(prompt) + val item = data.getMappableItems().getOrAwaitValue()!![0] as MappableSelectItem.MappableSelectLine + assertThat(item.strokeWidth, equalTo("10")) + assertThat(item.strokeColor, equalTo("#ffffff")) + } + + /** + * Attributes names come from properties defined at + * https://github.com/mapbox/simplestyle-spec/tree/master/1.1.0. + */ + @Test + fun `polygon stroke width, stroke color and fill color are pulled from simple style attributes`() { + val choices = listOf( + selectChoice( + value = "a", + item = treeElement( + children = listOf( + treeElement(SelectChoicesMapData.GEOMETRY, "12.0 -1.0 3 4; 12.1 -1.0 3 4; 12.0 -1.0 3 4"), + treeElement(SelectChoicesMapData.STROKE_WIDTH, "10"), + treeElement(SelectChoicesMapData.STROKE, "#000000"), + treeElement(SelectChoicesMapData.FILL, "#ffffff") + ) + ) + ) + ) + + val prompt = MockFormEntryPromptBuilder() + .withLongText("Which is your favourite place?") + .withSelectChoices(choices) + .withSelectChoiceText(mapOf(choices[0] to "A")) + .build() + + val data = loadDataForPrompt(prompt) + val item = data.getMappableItems().getOrAwaitValue()!![0] as MappableSelectItem.MappableSelectPolygon + assertThat(item.strokeWidth, equalTo("10")) + assertThat(item.strokeColor, equalTo("#000000")) + assertThat(item.fillColor, equalTo("#ffffff")) + } + @Test fun `uses different icon if marker-symbol is defined`() { val choices = listOf( @@ -245,8 +310,8 @@ class SelectChoicesMapDataTest { value = "a", item = treeElement( children = listOf( - treeElement("geometry", "12.0 -1.0 305 0"), - treeElement("marker-symbol", "A") + treeElement(SelectChoicesMapData.GEOMETRY, "12.0 -1.0 305 0"), + treeElement(SelectChoicesMapData.MARKER_SYMBOL, "A") ) ) ) @@ -259,7 +324,7 @@ class SelectChoicesMapDataTest { .build() val data = loadDataForPrompt(prompt) - val item = data.getMappableItems().getOrAwaitValue()!![0] + val item = data.getMappableItems().getOrAwaitValue()!![0] as MappableSelectItem.MappableSelectPoint assertThat(item.smallIcon, equalTo(org.odk.collect.icons.R.drawable.ic_map_marker_small)) assertThat(item.largeIcon, equalTo(org.odk.collect.icons.R.drawable.ic_map_marker_big)) } diff --git a/collect_app/src/test/java/org/odk/collect/android/widgets/items/SelectOneFromMapDialogFragmentTest.kt b/collect_app/src/test/java/org/odk/collect/android/widgets/items/SelectOneFromMapDialogFragmentTest.kt index a3952bb56a0..d2059728313 100644 --- a/collect_app/src/test/java/org/odk/collect/android/widgets/items/SelectOneFromMapDialogFragmentTest.kt +++ b/collect_app/src/test/java/org/odk/collect/android/widgets/items/SelectOneFromMapDialogFragmentTest.kt @@ -53,11 +53,11 @@ class SelectOneFromMapDialogFragmentTest { private val selectChoices = listOf( selectChoice( value = "a", - item = treeElement(children = listOf(treeElement("geometry", "12.0 -1.0 305 0"))) + item = treeElement(children = listOf(treeElement(SelectChoicesMapData.GEOMETRY, "12.0 -1.0 305 0"))) ), selectChoice( value = "b", - item = treeElement(children = listOf(treeElement("geometry", "13.0 -1.0 305 0"))) + item = treeElement(children = listOf(treeElement(SelectChoicesMapData.GEOMETRY, "13.0 -1.0 305 0"))) ) ) @@ -170,43 +170,39 @@ class SelectOneFromMapDialogFragmentTest { assertThat(data.getMapTitle().value, equalTo(prompt.longText)) assertThat(data.getItemCount().value, equalTo(prompt.selectChoices.size)) - val firstFeatureGeometry = selectChoices[0].getChild("geometry")!!.split(" ") - val secondFeatureGeometry = selectChoices[1].getChild("geometry")!!.split(" ") + val firstFeatureGeometry = selectChoices[0].getChild(SelectChoicesMapData.GEOMETRY)!!.split(" ") + val secondFeatureGeometry = selectChoices[1].getChild(SelectChoicesMapData.GEOMETRY)!!.split(" ") assertThat( data.getMappableItems().value, equalTo( listOf( - MappableSelectItem( + MappableSelectItem.MappableSelectPoint( 0, - listOf( - MapPoint( - firstFeatureGeometry[0].toDouble(), - firstFeatureGeometry[1].toDouble(), - firstFeatureGeometry[2].toDouble(), - firstFeatureGeometry[3].toDouble() - ) - ), - org.odk.collect.icons.R.drawable.ic_map_marker_with_hole_small, - org.odk.collect.icons.R.drawable.ic_map_marker_with_hole_big, "A", + point = MapPoint( + firstFeatureGeometry[0].toDouble(), + firstFeatureGeometry[1].toDouble(), + firstFeatureGeometry[2].toDouble(), + firstFeatureGeometry[3].toDouble() + ), + smallIcon = org.odk.collect.icons.R.drawable.ic_map_marker_with_hole_small, + largeIcon = org.odk.collect.icons.R.drawable.ic_map_marker_with_hole_big, action = IconifiedText( org.odk.collect.icons.R.drawable.ic_save, application.getString(org.odk.collect.strings.R.string.select_item) ) ), - MappableSelectItem( + MappableSelectItem.MappableSelectPoint( 1, - listOf( - MapPoint( - secondFeatureGeometry[0].toDouble(), - secondFeatureGeometry[1].toDouble(), - secondFeatureGeometry[2].toDouble(), - secondFeatureGeometry[3].toDouble() - ) - ), - org.odk.collect.icons.R.drawable.ic_map_marker_with_hole_small, - org.odk.collect.icons.R.drawable.ic_map_marker_with_hole_big, "B", + point = MapPoint( + secondFeatureGeometry[0].toDouble(), + secondFeatureGeometry[1].toDouble(), + secondFeatureGeometry[2].toDouble(), + secondFeatureGeometry[3].toDouble() + ), + smallIcon = org.odk.collect.icons.R.drawable.ic_map_marker_with_hole_small, + largeIcon = org.odk.collect.icons.R.drawable.ic_map_marker_with_hole_big, action = IconifiedText( org.odk.collect.icons.R.drawable.ic_save, application.getString(org.odk.collect.strings.R.string.select_item) diff --git a/collect_app/src/test/java/org/odk/collect/android/widgets/support/NoOpMapFragment.kt b/collect_app/src/test/java/org/odk/collect/android/widgets/support/NoOpMapFragment.kt index a09636c0e6b..479ab65d6a2 100644 --- a/collect_app/src/test/java/org/odk/collect/android/widgets/support/NoOpMapFragment.kt +++ b/collect_app/src/test/java/org/odk/collect/android/widgets/support/NoOpMapFragment.kt @@ -1,8 +1,10 @@ package org.odk.collect.android.widgets.support import androidx.fragment.app.Fragment +import org.odk.collect.maps.LineDescription import org.odk.collect.maps.MapFragment import org.odk.collect.maps.MapPoint +import org.odk.collect.maps.PolygonDescription import org.odk.collect.maps.markers.MarkerDescription import org.odk.collect.maps.markers.MarkerIconDescription @@ -53,15 +55,11 @@ class NoOpMapFragment : Fragment(), MapFragment { TODO("Not yet implemented") } - override fun addPolyLine( - points: MutableIterable, - closed: Boolean, - draggable: Boolean - ): Int { + override fun addPolyLine(lineDescription: LineDescription): Int { TODO("Not yet implemented") } - override fun addPolygon(points: MutableIterable): Int { + override fun addPolygon(polygonDescription: PolygonDescription): Int { TODO("Not yet implemented") } diff --git a/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyActivity.java b/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyActivity.java index 374151c9746..306edbd0c66 100644 --- a/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyActivity.java +++ b/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyActivity.java @@ -43,6 +43,8 @@ import org.odk.collect.geo.ReferenceLayerSettingsNavigator; import org.odk.collect.location.Location; import org.odk.collect.location.tracker.LocationTracker; +import org.odk.collect.maps.LineDescription; +import org.odk.collect.maps.MapConsts; import org.odk.collect.maps.MapFragment; import org.odk.collect.maps.MapFragmentFactory; import org.odk.collect.maps.MapPoint; @@ -274,7 +276,7 @@ public void initMap(MapFragment newMapFragment) { if (restoredPoints != null) { points = restoredPoints; } - featureId = map.addPolyLine(points, outputMode == OutputMode.GEOSHAPE, true); + featureId = map.addPolyLine(new LineDescription(points, String.valueOf(MapConsts.DEFAULT_STROKE_WIDTH), null, true, outputMode == OutputMode.GEOSHAPE)); if (inputActive && !intentReadOnly) { startInput(); @@ -442,7 +444,7 @@ private void removeLastPoint() { private void clear() { map.clearFeatures(); - featureId = map.addPolyLine(new ArrayList<>(), outputMode == OutputMode.GEOSHAPE, true); + featureId = map.addPolyLine(new LineDescription(new ArrayList<>(), String.valueOf(MapConsts.DEFAULT_STROKE_WIDTH), null, true, outputMode == OutputMode.GEOSHAPE)); inputActive = false; updateUi(); } diff --git a/geo/src/main/java/org/odk/collect/geo/selection/MappableSelectItem.kt b/geo/src/main/java/org/odk/collect/geo/selection/MappableSelectItem.kt index 891b498f661..743ddb36f6d 100644 --- a/geo/src/main/java/org/odk/collect/geo/selection/MappableSelectItem.kt +++ b/geo/src/main/java/org/odk/collect/geo/selection/MappableSelectItem.kt @@ -2,20 +2,57 @@ package org.odk.collect.geo.selection import org.odk.collect.maps.MapPoint -data class MappableSelectItem( - val id: Long, - val points: List, - val smallIcon: Int, - val largeIcon: Int, - val name: String, - val properties: List = emptyList(), - val selected: Boolean = false, - val color: String? = null, - val symbol: String? = null, - val info: String? = null, - val action: IconifiedText? = null, - val status: Status? = null -) +sealed class MappableSelectItem { + abstract val id: Long + abstract val name: String + abstract val properties: List + abstract val selected: Boolean + abstract val info: String? + abstract val action: IconifiedText? + abstract val status: Status? + + data class MappableSelectPoint( + override val id: Long, + override val name: String, + override val properties: List = emptyList(), + override val selected: Boolean = false, + override val info: String? = null, + override val action: IconifiedText? = null, + override val status: Status? = null, + val point: MapPoint, + val smallIcon: Int, + val largeIcon: Int, + val color: String? = null, + val symbol: String? = null + ) : MappableSelectItem() + + data class MappableSelectLine( + override val id: Long, + override val name: String, + override val properties: List = emptyList(), + override val selected: Boolean = false, + override val info: String? = null, + override val action: IconifiedText? = null, + override val status: Status? = null, + val points: List, + val strokeWidth: String? = null, + val strokeColor: String? = null + ) : MappableSelectItem() + + data class MappableSelectPolygon( + override val id: Long, + override val name: String, + override val properties: List = emptyList(), + override val selected: Boolean = false, + override val info: String? = null, + override val action: IconifiedText? = null, + override val status: Status? = null, + val points: List, + val strokeWidth: String? = null, + val strokeColor: String? = null, + val fillColor: String? = null + ) : MappableSelectItem() +} data class IconifiedText(val icon: Int?, val text: String) diff --git a/geo/src/main/java/org/odk/collect/geo/selection/SelectionMapFragment.kt b/geo/src/main/java/org/odk/collect/geo/selection/SelectionMapFragment.kt index 64322ede7cd..e3a64c1a133 100644 --- a/geo/src/main/java/org/odk/collect/geo/selection/SelectionMapFragment.kt +++ b/geo/src/main/java/org/odk/collect/geo/selection/SelectionMapFragment.kt @@ -24,9 +24,11 @@ import org.odk.collect.androidshared.ui.multiclicksafe.setMultiClickSafeOnClickL import org.odk.collect.geo.GeoDependencyComponentProvider import org.odk.collect.geo.ReferenceLayerSettingsNavigator import org.odk.collect.geo.databinding.SelectionMapLayoutBinding +import org.odk.collect.maps.LineDescription import org.odk.collect.maps.MapFragment import org.odk.collect.maps.MapFragmentFactory import org.odk.collect.maps.MapPoint +import org.odk.collect.maps.PolygonDescription import org.odk.collect.maps.markers.MarkerDescription import org.odk.collect.maps.markers.MarkerIconDescription import org.odk.collect.material.BottomSheetBehavior @@ -237,7 +239,9 @@ class SelectionMapFragment( val selectedItem = selectedItemViewModel.getSelectedItem() if (newState == STATE_HIDDEN && selectedItem != null) { selectedItemViewModel.setSelectedItem(null) - resetIcon(selectedItem) + if (selectedItem is MappableSelectItem.MappableSelectPoint) { + resetIcon(selectedItem) + } closeSummarySheet.isEnabled = false } else { @@ -269,7 +273,7 @@ class SelectionMapFragment( val selectedItem = selectedItemViewModel.getSelectedItem() if (item != null) { - if (selectedItem != null && selectedItem.id != item.id) { + if (selectedItem != null && selectedItem.id != item.id && selectedItem is MappableSelectItem.MappableSelectPoint) { resetIcon(selectedItem) } @@ -281,23 +285,25 @@ class SelectionMapFragment( } ) } else { - if (item.points.size > 1) { - map.zoomToBoundingBox(item.points, 0.8, true) - } else { - val point = item.points[0] + when (item) { + is MappableSelectItem.MappableSelectLine -> map.zoomToBoundingBox(item.points, 0.8, true) + is MappableSelectItem.MappableSelectPolygon -> map.zoomToBoundingBox(item.points, 0.8, true) + is MappableSelectItem.MappableSelectPoint -> { + val point = item.point + + if (maintainZoom) { + map.zoomToPoint(MapPoint(point.latitude, point.longitude), map.zoom, true) + } else { + map.zoomToPoint(MapPoint(point.latitude, point.longitude), true) + } - if (maintainZoom) { - map.zoomToPoint(MapPoint(point.latitude, point.longitude), map.zoom, true) - } else { - map.zoomToPoint(MapPoint(point.latitude, point.longitude), true) + map.setMarkerIcon( + featureId, + MarkerIconDescription(item.largeIcon, item.color, item.symbol) + ) } } - map.setMarkerIcon( - featureId, - MarkerIconDescription(item.largeIcon, item.color, item.symbol) - ) - summarySheet.setItem(item) summarySheetBehavior.state = STATE_COLLAPSED @@ -349,7 +355,7 @@ class SelectionMapFragment( } } - private fun resetIcon(selectedItem: MappableSelectItem) { + private fun resetIcon(selectedItem: MappableSelectItem.MappableSelectPoint) { val featureId = featureIdsByItemId[selectedItem.id] if (featureId != null) { map.setMarkerIcon( @@ -367,14 +373,13 @@ class SelectionMapFragment( map.clearFeatures() itemsByFeatureId.clear() - val singlePoints = items.filter { it.points.size == 1 } - val polys = items.filter { it.points.size != 1 } + val singlePoints = items.filterIsInstance() + val lines = items.filterIsInstance() + val polygons = items.filterIsInstance() val markerDescriptions = singlePoints.map { - val point = it.points[0] - MarkerDescription( - MapPoint(point.latitude, point.longitude), + MapPoint(it.point.latitude, it.point.longitude), false, MapFragment.BOTTOM, MarkerIconDescription(it.smallIcon, it.color, it.symbol) @@ -382,18 +387,21 @@ class SelectionMapFragment( } val pointIds = map.addMarkers(markerDescriptions) - val polyIds = polys.fold(listOf()) { ids, item -> - if (item.points.first() == item.points.last()) { - ids + map.addPolygon(item.points) - } else { - ids + map.addPolyLine(item.points, false, false) - } + val lineIds = lines.fold(listOf()) { ids, item -> + ids + map.addPolyLine(LineDescription(item.points, item.strokeWidth, item.strokeColor)) + } + val polygonIds = polygons.fold(listOf()) { ids, item -> + ids + map.addPolygon(PolygonDescription(item.points, item.strokeWidth, item.strokeColor, item.fillColor)) } - (singlePoints + polys).zip(pointIds + polyIds).forEach { (item, featureId) -> + (singlePoints + lines + polygons).zip(pointIds + lineIds + polygonIds).forEach { (item, featureId) -> itemsByFeatureId[featureId] = item featureIdsByItemId[item.id] = featureId - points.addAll(item.points) + when (item) { + is MappableSelectItem.MappableSelectPoint -> points.add(item.point) + is MappableSelectItem.MappableSelectLine -> points.addAll(item.points) + is MappableSelectItem.MappableSelectPolygon -> points.addAll(item.points) + } } featureCount = items.size diff --git a/geo/src/main/java/org/odk/collect/geo/selection/SelectionSummarySheet.kt b/geo/src/main/java/org/odk/collect/geo/selection/SelectionSummarySheet.kt index b9b2690a096..80fc815a3fb 100644 --- a/geo/src/main/java/org/odk/collect/geo/selection/SelectionSummarySheet.kt +++ b/geo/src/main/java/org/odk/collect/geo/selection/SelectionSummarySheet.kt @@ -73,10 +73,10 @@ internal class SelectionSummarySheet(context: Context, attrs: AttributeSet?) : } item.action?.let { - binding.action.text = item.action.text + binding.action.text = it.text - if (item.action.icon != null) { - binding.action.icon = ContextCompat.getDrawable(context, item.action.icon) + if (it.icon != null) { + binding.action.icon = ContextCompat.getDrawable(context, it.icon) } binding.action.visibility = View.VISIBLE diff --git a/geo/src/test/java/org/odk/collect/geo/geopoly/GeoPolyActivityTest.java b/geo/src/test/java/org/odk/collect/geo/geopoly/GeoPolyActivityTest.java index 5701f0ff721..6db730f2d0c 100644 --- a/geo/src/test/java/org/odk/collect/geo/geopoly/GeoPolyActivityTest.java +++ b/geo/src/test/java/org/odk/collect/geo/geopoly/GeoPolyActivityTest.java @@ -51,6 +51,7 @@ import org.odk.collect.geo.support.FakeMapFragment; import org.odk.collect.geo.support.RobolectricApplication; import org.odk.collect.location.tracker.LocationTracker; +import org.odk.collect.maps.LineDescription; import org.odk.collect.maps.MapFragmentFactory; import org.odk.collect.maps.MapPoint; import org.robolectric.shadows.ShadowApplication; @@ -138,9 +139,9 @@ public void whenPolygonExtraPresent_showsPoly() { mapFragment.ready(); - List> polys = mapFragment.getPolyLines(); + List polys = mapFragment.getPolyLines(); assertThat(polys.size(), equalTo(1)); - assertThat(polys.get(0), equalTo(polygon)); + assertThat(polys.get(0).getPoints(), equalTo(polygon)); } @Test @@ -157,13 +158,13 @@ public void whenPolygonExtraPresent_andOutputModeIsShape_showsClosedPoly() { mapFragment.ready(); - List> polys = mapFragment.getPolyLines(); + List polys = mapFragment.getPolyLines(); assertThat(polys.size(), equalTo(1)); ArrayList expectedPolygon = new ArrayList<>(); expectedPolygon.add(new MapPoint(1.0, 2.0, 3, 4)); expectedPolygon.add(new MapPoint(2.0, 3.0, 3, 4)); - assertThat(polys.get(0), equalTo(expectedPolygon)); + assertThat(polys.get(0).getPoints(), equalTo(expectedPolygon)); assertThat(mapFragment.isPolyClosed(0), equalTo(true)); } @@ -178,9 +179,9 @@ public void whenPolygonExtraPresent_andPolyIsEmpty_andOutputModeIsShape_doesNotS mapFragment.ready(); - List> polys = mapFragment.getPolyLines(); + List polys = mapFragment.getPolyLines(); assertThat(polys.size(), equalTo(1)); - assertThat(polys.get(0).isEmpty(), equalTo(true)); + assertThat(polys.get(0).getPoints().isEmpty(), equalTo(true)); } @Test @@ -231,7 +232,7 @@ public void recordingPointManually_whenPointIsADuplicateOfTheLastPoint_skipsPoin mapFragment.setLocation(new MapPoint(1, 1)); onView(withId(R.id.record_button)).perform(click()); onView(withId(R.id.record_button)).perform(click()); - assertThat(mapFragment.getPolyLines().get(0).size(), equalTo(1)); + assertThat(mapFragment.getPolyLines().get(0).getPoints().size(), equalTo(1)); } @Test @@ -243,7 +244,7 @@ public void placingPoint_whenPointIsADuplicateOfTheLastPoint_skipsPoint() { mapFragment.click(new MapPoint(1, 1)); mapFragment.click(new MapPoint(1, 1)); - assertThat(mapFragment.getPolyLines().get(0).size(), equalTo(1)); + assertThat(mapFragment.getPolyLines().get(0).getPoints().size(), equalTo(1)); } private void startInput(int mode) { diff --git a/geo/src/test/java/org/odk/collect/geo/selection/SelectionMapFragmentTest.kt b/geo/src/test/java/org/odk/collect/geo/selection/SelectionMapFragmentTest.kt index 962dcefb9ab..b01686c0914 100644 --- a/geo/src/test/java/org/odk/collect/geo/selection/SelectionMapFragmentTest.kt +++ b/geo/src/test/java/org/odk/collect/geo/selection/SelectionMapFragmentTest.kt @@ -128,8 +128,8 @@ class SelectionMapFragmentTest { @Test fun `updates markers when items update`() { val items: List = listOf( - Fixtures.actionMappableSelectItem().copy(id = 0, points = listOf(MapPoint(40.0, 0.0))), - Fixtures.actionMappableSelectItem().copy(id = 1, points = listOf(MapPoint(41.0, 0.0))) + Fixtures.actionMappableSelectPoint().copy(id = 0, point = MapPoint(40.0, 0.0)), + Fixtures.actionMappableSelectPoint().copy(id = 1, point = MapPoint(41.0, 0.0)) ) val itemsLiveData = MutableLiveData(items) whenever(data.getMappableItems()).thenReturn(itemsLiveData) @@ -137,7 +137,7 @@ class SelectionMapFragmentTest { launcherRule.launchInContainer(SelectionMapFragment::class.java) map.ready() - assertThat(map.getMarkers(), equalTo(itemsLiveData.value?.map { it.toMapPoint() })) + assertThat(map.getMarkers(), equalTo(itemsLiveData.value?.map { (it as MappableSelectItem.MappableSelectPoint).toMapPoint() })) itemsLiveData.value = emptyList() assertThat(map.getMarkers(), equalTo(emptyList())) @@ -146,8 +146,8 @@ class SelectionMapFragmentTest { @Test fun `updates item count when items update`() { val items: List = listOf( - Fixtures.actionMappableSelectItem().copy(id = 0), - Fixtures.actionMappableSelectItem().copy(id = 1) + Fixtures.actionMappableSelectPoint().copy(id = 0), + Fixtures.actionMappableSelectPoint().copy(id = 1) ) val itemsLiveData = MutableLiveData(items) @@ -167,12 +167,14 @@ class SelectionMapFragmentTest { @Test fun `shows polyline when item has multiple points`() { val items: List = listOf( - Fixtures.actionMappableSelectItem().copy( + Fixtures.actionMappableSelectLine().copy( id = 0, points = listOf( MapPoint(40.0, 0.0), MapPoint(41.0, 0.0) - ) + ), + strokeWidth = "10", + strokeColor = "#ffffff" ) ) @@ -181,9 +183,9 @@ class SelectionMapFragmentTest { launcherRule.launchInContainer(SelectionMapFragment::class.java) map.ready() - - assertThat(map.getPolyLines(), equalTo(itemsLiveData.value?.map { it.points })) - assertThat(map.isPolyDraggable(0), equalTo(false)) + assertThat(map.getPolyLines()[0].points, equalTo(itemsLiveData.value?.map { (it as MappableSelectItem.MappableSelectLine).points }?.first())) + assertThat(map.getPolyLines()[0].getStrokeWidth(), equalTo(10f)) + assertThat(map.getPolyLines()[0].getStrokeColor(), equalTo(-1)) onView(withText(application.getString(org.odk.collect.strings.R.string.select_item_count, "Things", 0, 1))) .check(matches(isDisplayed())) } @@ -191,13 +193,16 @@ class SelectionMapFragmentTest { @Test fun `shows polygon when item has multiple closed points`() { val items: List = listOf( - Fixtures.actionMappableSelectItem().copy( + Fixtures.actionMappableSelectPolygon().copy( id = 0, points = listOf( MapPoint(40.0, 0.0), MapPoint(41.0, 0.0), MapPoint(40.0, 0.0) - ) + ), + strokeWidth = "10", + strokeColor = "#aaccee", + fillColor = "#ffffff" ) ) @@ -207,7 +212,10 @@ class SelectionMapFragmentTest { launcherRule.launchInContainer(SelectionMapFragment::class.java) map.ready() - assertThat(map.getPolygons(), equalTo(itemsLiveData.value?.map { it.points })) + assertThat(map.getPolygons()[0].points, equalTo(itemsLiveData.value?.map { (it as MappableSelectItem.MappableSelectPolygon).points }?.first())) + assertThat(map.getPolygons()[0].getStrokeWidth(), equalTo(10f)) + assertThat(map.getPolygons()[0].getStrokeColor(), equalTo(-5583634)) + assertThat(map.getPolygons()[0].getFillColor(), equalTo(1157627903)) onView(withText(application.getString(org.odk.collect.strings.R.string.select_item_count, "Things", 0, 1))) .check(matches(isDisplayed())) } @@ -215,8 +223,8 @@ class SelectionMapFragmentTest { @Test fun `zooms to fit all items`() { val items = listOf( - Fixtures.actionMappableSelectItem().copy(id = 0, points = listOf(MapPoint(40.0, 0.0))), - Fixtures.actionMappableSelectItem().copy(id = 1, points = listOf(MapPoint(41.0, 0.0))) + Fixtures.actionMappableSelectPoint().copy(id = 0, point = MapPoint(40.0, 0.0)), + Fixtures.actionMappableSelectPoint().copy(id = 1, point = MapPoint(41.0, 0.0)) ) whenever(data.getMappableItems()).thenReturn(MutableLiveData(items)) @@ -232,7 +240,7 @@ class SelectionMapFragmentTest { fun `zooms to fit all points in for item with multiple points`() { val points = listOf(MapPoint(40.0, 0.0), MapPoint(41.0, 0.0)) val items: List = listOf( - Fixtures.actionMappableSelectItem().copy(id = 0, points = points) + Fixtures.actionMappableSelectLine().copy(id = 0, points = points) ) whenever(data.getMappableItems()).thenReturn(MutableLiveData(items)) @@ -246,7 +254,7 @@ class SelectionMapFragmentTest { @Test fun `does not zoom to fit all items again when they change`() { val originalItems = - listOf(Fixtures.actionMappableSelectItem().copy(id = 0, points = listOf(MapPoint(40.0, 0.0)))) + listOf(Fixtures.actionMappableSelectPoint().copy(id = 0, point = MapPoint(40.0, 0.0))) val itemsLiveData: MutableLiveData?> = MutableLiveData(originalItems) whenever(data.getMappableItems()).thenReturn(itemsLiveData) @@ -255,7 +263,7 @@ class SelectionMapFragmentTest { map.ready() itemsLiveData.value = - listOf(Fixtures.actionMappableSelectItem().copy(id = 0, points = listOf(MapPoint(52.0, 0.0)))) + listOf(Fixtures.actionMappableSelectPoint().copy(id = 0, point = MapPoint(52.0, 0.0))) val points = originalItems.map { it.toMapPoint() } assertThat(map.getZoomBoundingBox(), equalTo(Pair(points, 0.8))) @@ -263,7 +271,7 @@ class SelectionMapFragmentTest { @Test fun `does not zoom to fit all items if map already has center`() { - val items = listOf(Fixtures.actionMappableSelectItem().copy(id = 0, points = listOf(MapPoint(40.0, 0.0)))) + val items = listOf(Fixtures.actionMappableSelectPoint().copy(id = 0, point = MapPoint(40.0, 0.0))) val itemsLiveData: MutableLiveData?> = MutableLiveData(items) whenever(data.getMappableItems()).thenReturn(itemsLiveData) @@ -278,8 +286,8 @@ class SelectionMapFragmentTest { @Test fun `zooms to current location when zoomToFitItems is false`() { val items = listOf( - Fixtures.actionMappableSelectItem().copy(id = 0, points = listOf(MapPoint(40.0, 0.0))), - Fixtures.actionMappableSelectItem().copy(id = 1, points = listOf(MapPoint(41.0, 0.0))) + Fixtures.actionMappableSelectPoint().copy(id = 0, point = MapPoint(40.0, 0.0)), + Fixtures.actionMappableSelectPoint().copy(id = 1, point = MapPoint(41.0, 0.0)) ) whenever(data.getMappableItems()).thenReturn(MutableLiveData(items)) @@ -335,7 +343,7 @@ class SelectionMapFragmentTest { @Test fun `does not zoom to current location when items change`() { val originalItems = - listOf(Fixtures.actionMappableSelectItem().copy(id = 0, points = listOf(MapPoint(40.0, 0.0)))) + listOf(Fixtures.actionMappableSelectPoint().copy(id = 0, point = MapPoint(40.0, 0.0))) val itemsLiveData: MutableLiveData?> = MutableLiveData(originalItems) whenever(data.getMappableItems()).thenReturn(itemsLiveData) @@ -345,7 +353,7 @@ class SelectionMapFragmentTest { map.setLocation(MapPoint(67.0, 48.0)) itemsLiveData.value = - listOf(Fixtures.actionMappableSelectItem().copy(id = 0, points = listOf(MapPoint(52.0, 0.0)))) + listOf(Fixtures.actionMappableSelectPoint().copy(id = 0, point = MapPoint(52.0, 0.0))) val points = originalItems.map { it.toMapPoint() } assertThat(map.getZoomBoundingBox(), equalTo(Pair(points, 0.8))) @@ -366,8 +374,8 @@ class SelectionMapFragmentTest { @Test fun `clicking zoom to fit button zooms to fit all items`() { val items = listOf( - Fixtures.actionMappableSelectItem().copy(id = 0, points = listOf(MapPoint(40.0, 0.0))), - Fixtures.actionMappableSelectItem().copy(id = 1, points = listOf(MapPoint(41.0, 0.0))) + Fixtures.actionMappableSelectPoint().copy(id = 0, point = MapPoint(40.0, 0.0)), + Fixtures.actionMappableSelectPoint().copy(id = 1, point = MapPoint(41.0, 0.0)) ) whenever(data.getMappableItems()).thenReturn(MutableLiveData(items)) @@ -384,7 +392,7 @@ class SelectionMapFragmentTest { fun `clicking zoom to fit button zooms to fit all polys`() { val points = listOf(MapPoint(40.0, 0.0), MapPoint(41.0, 0.0)) val items: List = listOf( - Fixtures.actionMappableSelectItem().copy(id = 0, points = points) + Fixtures.actionMappableSelectLine().copy(id = 0, points = points) ) whenever(data.getMappableItems()).thenReturn(MutableLiveData(items)) @@ -410,8 +418,8 @@ class SelectionMapFragmentTest { @Test fun `clicking on item centers on that item with current zoom level`() { val items = listOf( - Fixtures.actionMappableSelectItem().copy(id = 0, points = listOf(MapPoint(40.0, 0.0))), - Fixtures.actionMappableSelectItem().copy(id = 1, points = listOf(MapPoint(41.0, 0.0))) + Fixtures.actionMappableSelectPoint().copy(id = 0, point = MapPoint(40.0, 0.0)), + Fixtures.actionMappableSelectPoint().copy(id = 1, point = MapPoint(41.0, 0.0)) ) whenever(data.getMappableItems()).thenReturn(MutableLiveData(items)) @@ -429,8 +437,8 @@ class SelectionMapFragmentTest { fun `clicking on item with multiple points zooms to fit all item points`() { val itemPoints = listOf(MapPoint(40.0, 0.0), MapPoint(41.0, 0.0)) val items = listOf( - Fixtures.actionMappableSelectItem().copy(id = 0, points = listOf(MapPoint(40.0, 0.0))), - Fixtures.actionMappableSelectItem().copy(id = 1, points = itemPoints) + Fixtures.actionMappableSelectLine().copy(id = 0, points = listOf(MapPoint(40.0, 0.0))), + Fixtures.actionMappableSelectLine().copy(id = 1, points = itemPoints) ) whenever(data.getMappableItems()).thenReturn(MutableLiveData(items)) @@ -448,29 +456,29 @@ class SelectionMapFragmentTest { @Test fun `clicking on item always selects correct item`() { val items = listOf( - Fixtures.actionMappableSelectItem().copy(id = 0, points = listOf(MapPoint(40.0, 0.0), MapPoint(41.0, 0.0))), - Fixtures.actionMappableSelectItem().copy(id = 1, points = listOf(MapPoint(45.0, 0.0))) + Fixtures.actionMappableSelectLine().copy(id = 0, points = listOf(MapPoint(40.0, 0.0), MapPoint(41.0, 0.0))), + Fixtures.actionMappableSelectPoint().copy(id = 1, point = MapPoint(45.0, 0.0)) ) whenever(data.getMappableItems()).thenReturn(MutableLiveData(items)) launcherRule.launchInContainer(SelectionMapFragment::class.java) map.ready() - map.clickOnFeatureId(map.getFeatureId(items[1].points)) - assertThat(map.center, equalTo(items[1].points[0])) + map.clickOnFeatureId(map.getFeatureId(listOf((items[1] as MappableSelectItem.MappableSelectPoint).point))) + assertThat(map.center, equalTo((items[1] as MappableSelectItem.MappableSelectPoint).point)) } @Test fun `clicking on item switches item marker to large icon`() { val items = listOf( - Fixtures.actionMappableSelectItem().copy( + Fixtures.actionMappableSelectPoint().copy( id = 0, smallIcon = android.R.drawable.ic_lock_idle_charging, largeIcon = android.R.drawable.ic_lock_idle_alarm, symbol = "A", color = "#ffffff" ), - Fixtures.actionMappableSelectItem().copy( + Fixtures.actionMappableSelectPoint().copy( id = 1, smallIcon = android.R.drawable.ic_lock_idle_charging, largeIcon = android.R.drawable.ic_lock_idle_alarm, @@ -499,14 +507,14 @@ class SelectionMapFragmentTest { @Test fun `clicking on item when another has been tapped switches the first one back to its small icon`() { val items = listOf( - Fixtures.actionMappableSelectItem().copy( + Fixtures.actionMappableSelectPoint().copy( id = 0, smallIcon = android.R.drawable.ic_lock_idle_charging, largeIcon = android.R.drawable.ic_lock_idle_alarm, symbol = "A", color = "#ffffff" ), - Fixtures.actionMappableSelectItem().copy( + Fixtures.actionMappableSelectPoint().copy( id = 1, smallIcon = android.R.drawable.ic_lock_idle_charging, largeIcon = android.R.drawable.ic_lock_idle_alarm, @@ -536,8 +544,8 @@ class SelectionMapFragmentTest { @Test fun `clicking on item sets item on summary sheet`() { val items = listOf( - Fixtures.actionMappableSelectItem().copy(id = 0, name = "Blah1"), - Fixtures.actionMappableSelectItem().copy(id = 1, name = "Blah2") + Fixtures.actionMappableSelectPoint().copy(id = 0, name = "Blah1"), + Fixtures.actionMappableSelectPoint().copy(id = 1, name = "Blah2") ) whenever(data.getMappableItems()).thenReturn(MutableLiveData(items)) @@ -552,8 +560,8 @@ class SelectionMapFragmentTest { @Test fun `clicking on item returns item ID as result when skipSummary is true`() { val items = listOf( - Fixtures.actionMappableSelectItem().copy(id = 0), - Fixtures.actionMappableSelectItem().copy(id = 1) + Fixtures.actionMappableSelectPoint().copy(id = 0), + Fixtures.actionMappableSelectPoint().copy(id = 1) ) whenever(data.getMappableItems()).thenReturn(MutableLiveData(items)) @@ -589,7 +597,7 @@ class SelectionMapFragmentTest { @Test fun `clicking map with an item selected deselects it`() { - val item = Fixtures.actionMappableSelectItem().copy(id = 0, name = "Blah1") + val item = Fixtures.actionMappableSelectPoint().copy(id = 0, name = "Blah1") whenever(data.getMappableItems()).thenReturn(MutableLiveData(listOf(item))) launcherRule.launchInContainer(SelectionMapFragment::class.java) @@ -605,7 +613,7 @@ class SelectionMapFragmentTest { @Test fun `pressing back with an item selected deselects it`() { - val item = Fixtures.actionMappableSelectItem() + val item = Fixtures.actionMappableSelectPoint() .copy(id = 0, name = "Blah1", symbol = "A", color = "#ffffff") whenever(data.getMappableItems()).thenReturn(MutableLiveData(listOf(item))) @@ -627,7 +635,7 @@ class SelectionMapFragmentTest { @Test fun `pressing back after deselecting item disables back callbacks`() { - val item = Fixtures.actionMappableSelectItem().copy(id = 0, name = "Blah1") + val item = Fixtures.actionMappableSelectPoint().copy(id = 0, name = "Blah1") whenever(data.getMappableItems()).thenReturn(MutableLiveData(listOf(item))) val scenario = launcherRule.launchInContainer(SelectionMapFragment::class.java) @@ -642,7 +650,7 @@ class SelectionMapFragmentTest { @Test fun `recreating after deselecting item has no item selected`() { - val items = listOf(Fixtures.actionMappableSelectItem().copy(id = 0, name = "Point1")) + val items = listOf(Fixtures.actionMappableSelectPoint().copy(id = 0, name = "Point1")) whenever(data.getMappableItems()).thenReturn(MutableLiveData(items)) val scenario = launcherRule.launchInContainer(SelectionMapFragment::class.java) @@ -663,7 +671,7 @@ class SelectionMapFragmentTest { @Test fun `clicking action hides summary sheet`() { val items = listOf( - Fixtures.actionMappableSelectItem().copy( + Fixtures.actionMappableSelectPoint().copy( id = 0, name = "Item", action = IconifiedText(null, "Action") @@ -682,8 +690,8 @@ class SelectionMapFragmentTest { @Test fun `centers on already selected item`() { val items = listOf( - Fixtures.actionMappableSelectItem().copy(id = 0, points = listOf(MapPoint(40.0, 0.0))), - Fixtures.actionMappableSelectItem().copy(id = 1, points = listOf(MapPoint(40.1, 0.0)), selected = true) + Fixtures.actionMappableSelectPoint().copy(id = 0, point = MapPoint(40.0, 0.0)), + Fixtures.actionMappableSelectPoint().copy(id = 1, point = MapPoint(40.1, 0.0), selected = true) ) whenever(data.getMappableItems()).thenReturn(MutableLiveData(items)) @@ -697,8 +705,8 @@ class SelectionMapFragmentTest { @Test fun `does not move when location changes when centered on already selected item`() { val items = listOf( - Fixtures.actionMappableSelectItem().copy(id = 0, points = listOf(MapPoint(40.0, 0.0))), - Fixtures.actionMappableSelectItem().copy(id = 1, points = listOf(MapPoint(41.0, 0.0)), selected = true) + Fixtures.actionMappableSelectPoint().copy(id = 0, point = MapPoint(40.0, 0.0)), + Fixtures.actionMappableSelectPoint().copy(id = 1, point = MapPoint(41.0, 0.0), selected = true) ) whenever(data.getMappableItems()).thenReturn(MutableLiveData(items)) @@ -739,8 +747,8 @@ class SelectionMapFragmentTest { @Test fun `recreating maintains selection`() { val items = listOf( - Fixtures.actionMappableSelectItem().copy(id = 0, points = listOf(MapPoint(40.0, 0.0)), name = "Point1"), - Fixtures.actionMappableSelectItem().copy(id = 1, points = listOf(MapPoint(41.0, 0.0)), name = "Point2") + Fixtures.actionMappableSelectPoint().copy(id = 0, point = MapPoint(40.0, 0.0), name = "Point1"), + Fixtures.actionMappableSelectPoint().copy(id = 1, point = MapPoint(41.0, 0.0), name = "Point2") ) whenever(data.getMappableItems()).thenReturn(MutableLiveData(items)) @@ -759,9 +767,9 @@ class SelectionMapFragmentTest { @Test fun `recreating with initial selection maintains new selection`() { val items = listOf( - Fixtures.actionMappableSelectItem() - .copy(id = 0, points = listOf(MapPoint(40.0, 0.0)), name = "Point1", selected = true), - Fixtures.actionMappableSelectItem().copy(id = 1, points = listOf(MapPoint(41.0, 0.0)), name = "Point2") + Fixtures.actionMappableSelectPoint() + .copy(id = 0, point = MapPoint(40.0, 0.0), name = "Point1", selected = true), + Fixtures.actionMappableSelectPoint().copy(id = 1, point = MapPoint(41.0, 0.0), name = "Point2") ) whenever(data.getMappableItems()).thenReturn(MutableLiveData(items)) @@ -807,8 +815,8 @@ class SelectionMapFragmentTest { @Test fun `opening the map with already selected item when skipSummary is true does not close the map`() { val items = listOf( - Fixtures.actionMappableSelectItem().copy(id = 0, points = listOf(MapPoint(40.0, 0.0))), - Fixtures.actionMappableSelectItem().copy(id = 1, points = listOf(MapPoint(41.0, 0.0)), selected = true) + Fixtures.actionMappableSelectPoint().copy(id = 0, point = MapPoint(40.0, 0.0)), + Fixtures.actionMappableSelectPoint().copy(id = 1, point = MapPoint(41.0, 0.0), selected = true) ) whenever(data.getMappableItems()).thenReturn(MutableLiveData(items)) @@ -841,8 +849,8 @@ class SelectionMapFragmentTest { @Test fun `recreating the map with already selected item when skipSummary is true does not close the map`() { val items = listOf( - Fixtures.actionMappableSelectItem().copy(id = 0, points = listOf(MapPoint(40.0, 0.0))), - Fixtures.actionMappableSelectItem().copy(id = 1, points = listOf(MapPoint(41.0, 0.0)), selected = true) + Fixtures.actionMappableSelectPoint().copy(id = 0, point = MapPoint(40.0, 0.0)), + Fixtures.actionMappableSelectPoint().copy(id = 1, point = MapPoint(41.0, 0.0), selected = true) ) whenever(data.getMappableItems()).thenReturn(MutableLiveData(items)) @@ -873,7 +881,7 @@ class SelectionMapFragmentTest { assertThat(actualResult, equalTo(null)) } - private fun MappableSelectItem.toMapPoint(): MapPoint { - return MapPoint(this.points[0].latitude, this.points[0].longitude) + private fun MappableSelectItem.MappableSelectPoint.toMapPoint(): MapPoint { + return MapPoint(this.point.latitude, this.point.longitude) } } diff --git a/geo/src/test/java/org/odk/collect/geo/selection/SelectionSummarySheetTest.kt b/geo/src/test/java/org/odk/collect/geo/selection/SelectionSummarySheetTest.kt index 501ba60eacd..f098d005304 100644 --- a/geo/src/test/java/org/odk/collect/geo/selection/SelectionSummarySheetTest.kt +++ b/geo/src/test/java/org/odk/collect/geo/selection/SelectionSummarySheetTest.kt @@ -23,7 +23,7 @@ class SelectionSummarySheetTest { fun `setItem shows an error chip when the status is ERRORS`() { val selectionSummarySheet = SelectionSummarySheet(application) selectionSummarySheet.setItem( - Fixtures.actionMappableSelectItem().copy( + Fixtures.actionMappableSelectPoint().copy( status = Status.ERRORS ) ) @@ -36,7 +36,7 @@ class SelectionSummarySheetTest { fun `setItem shows a no-error chip when the status is NO_ERRORS`() { val selectionSummarySheet = SelectionSummarySheet(application) selectionSummarySheet.setItem( - Fixtures.actionMappableSelectItem().copy( + Fixtures.actionMappableSelectPoint().copy( status = Status.NO_ERRORS ) ) @@ -48,7 +48,7 @@ class SelectionSummarySheetTest { @Test fun `setItem does not show a chip if the status is null`() { val selectionSummarySheet = SelectionSummarySheet(application) - selectionSummarySheet.setItem(Fixtures.actionMappableSelectItem()) + selectionSummarySheet.setItem(Fixtures.actionMappableSelectPoint()) assertThat(selectionSummarySheet.binding.statusChip.visibility, equalTo(View.GONE)) } @@ -57,7 +57,7 @@ class SelectionSummarySheetTest { fun `setItem shows name`() { val selectionSummarySheet = SelectionSummarySheet(application) selectionSummarySheet.setItem( - Fixtures.actionMappableSelectItem().copy( + Fixtures.actionMappableSelectPoint().copy( name = "Cosmic Dread" ) ) @@ -69,7 +69,7 @@ class SelectionSummarySheetTest { fun `setItem shows properties`() { val selectionSummarySheet = SelectionSummarySheet(application) selectionSummarySheet.setItem( - Fixtures.actionMappableSelectItem().copy( + Fixtures.actionMappableSelectPoint().copy( properties = listOf( IconifiedText( android.R.drawable.ic_btn_speak_now, @@ -100,7 +100,7 @@ class SelectionSummarySheetTest { fun `properties without icons have hidden icon view`() { val selectionSummarySheet = SelectionSummarySheet(application) selectionSummarySheet.setItem( - Fixtures.actionMappableSelectItem().copy( + Fixtures.actionMappableSelectPoint().copy( properties = listOf( IconifiedText( null, @@ -119,7 +119,7 @@ class SelectionSummarySheetTest { fun `properties reset between items`() { val selectionSummarySheet = SelectionSummarySheet(application) selectionSummarySheet.setItem( - Fixtures.actionMappableSelectItem().copy( + Fixtures.actionMappableSelectPoint().copy( properties = listOf( IconifiedText( android.R.drawable.ic_btn_speak_now, @@ -130,7 +130,7 @@ class SelectionSummarySheetTest { ) selectionSummarySheet.setItem( - Fixtures.actionMappableSelectItem().copy( + Fixtures.actionMappableSelectPoint().copy( properties = listOf( IconifiedText( android.R.drawable.ic_dialog_info, @@ -153,7 +153,7 @@ class SelectionSummarySheetTest { fun `setItem shows info and hides action when it is non-null`() { val selectionSummarySheet = SelectionSummarySheet(application) selectionSummarySheet.setItem( - Fixtures.infoMappableSelectItem().copy( + Fixtures.infoMappableSelectPoint().copy( info = "Don't even bother looking" ) ) @@ -167,7 +167,7 @@ class SelectionSummarySheetTest { fun `setItem shows action and hides info when it is non-null`() { val selectionSummarySheet = SelectionSummarySheet(application) selectionSummarySheet.setItem( - Fixtures.actionMappableSelectItem().copy( + Fixtures.actionMappableSelectPoint().copy( action = IconifiedText( android.R.drawable.ic_btn_speak_now, "Come on in" diff --git a/geo/src/test/java/org/odk/collect/geo/support/FakeMapFragment.kt b/geo/src/test/java/org/odk/collect/geo/support/FakeMapFragment.kt index 094c33b130b..914c982b342 100644 --- a/geo/src/test/java/org/odk/collect/geo/support/FakeMapFragment.kt +++ b/geo/src/test/java/org/odk/collect/geo/support/FakeMapFragment.kt @@ -1,11 +1,13 @@ package org.odk.collect.geo.support import androidx.fragment.app.Fragment +import org.odk.collect.maps.LineDescription import org.odk.collect.maps.MapFragment import org.odk.collect.maps.MapFragment.FeatureListener import org.odk.collect.maps.MapFragment.PointListener import org.odk.collect.maps.MapFragment.ReadyListener import org.odk.collect.maps.MapPoint +import org.odk.collect.maps.PolygonDescription import org.odk.collect.maps.markers.MarkerDescription import org.odk.collect.maps.markers.MarkerIconDescription import kotlin.random.Random @@ -24,10 +26,8 @@ class FakeMapFragment : Fragment(), MapFragment { private var featureClickListener: FeatureListener? = null private val markers = mutableMapOf() private val markerIcons = mutableMapOf() - private val polyLines = mutableMapOf>() - private val polyClosed: MutableList = ArrayList() - private val polyDraggable: MutableList = ArrayList() - private val polygons = mutableMapOf>() + private val polyLines = mutableMapOf() + private val polygons = mutableMapOf() private var hasCenter = false private val featureIds = mutableListOf() @@ -107,40 +107,33 @@ class FakeMapFragment : Fragment(), MapFragment { return markers[featureId]!! } - override fun addPolyLine( - points: Iterable, - closed: Boolean, - draggable: Boolean - ): Int { + override fun addPolyLine(lineDescription: LineDescription): Int { val featureId = generateFeatureId() - polyLines[featureId] = points.toList() - polyClosed.add(closed) - polyDraggable.add(draggable) - + polyLines[featureId] = lineDescription featureIds.add(featureId) return featureId } - override fun addPolygon(points: MutableIterable): Int { + override fun addPolygon(polygonDescription: PolygonDescription): Int { val featureId = generateFeatureId() - polygons[featureId] = points.toList() + polygons[featureId] = polygonDescription featureIds.add(featureId) return featureId } override fun appendPointToPolyLine(featureId: Int, point: MapPoint) { val poly = polyLines[featureId]!! - polyLines[featureId] = poly + point + polyLines[featureId] = poly.copy(points = poly.points + point) } override fun removePolyLineLastPoint(featureId: Int) { val poly = polyLines[featureId]!! - polyLines[featureId] = poly.dropLast(1) + polyLines[featureId] = poly.copy(points = poly.points.dropLast(1)) } override fun getPolyLinePoints(featureId: Int): List { - return polyLines[featureId]!! + return polyLines[featureId]!!.points } override fun clearFeatures() { @@ -223,16 +216,16 @@ class FakeMapFragment : Fragment(), MapFragment { return zoomBoundingBox } - fun getPolyLines(): List> { + fun getPolyLines(): List { return polyLines.values.toList() } fun isPolyClosed(index: Int): Boolean { - return polyClosed[index] + return polyLines[featureIds[index]]!!.closed } fun isPolyDraggable(index: Int): Boolean { - return polyDraggable[index] + return polyLines[featureIds[index]]!!.draggable } fun getFeatureId(points: List): Int { @@ -242,7 +235,7 @@ class FakeMapFragment : Fragment(), MapFragment { }!!.key } else { polyLines.entries.find { - it.value == points + it.value.points == points }!!.key } } @@ -256,7 +249,7 @@ class FakeMapFragment : Fragment(), MapFragment { return featureId } - fun getPolygons(): List> { + fun getPolygons(): List { return polygons.values.toList() } diff --git a/geo/src/test/java/org/odk/collect/geo/support/Fixtures.kt b/geo/src/test/java/org/odk/collect/geo/support/Fixtures.kt index 2ba9fbb8e7e..50feb36efa4 100644 --- a/geo/src/test/java/org/odk/collect/geo/support/Fixtures.kt +++ b/geo/src/test/java/org/odk/collect/geo/support/Fixtures.kt @@ -6,27 +6,47 @@ import org.odk.collect.geo.selection.MappableSelectItem import org.odk.collect.maps.MapPoint object Fixtures { - fun actionMappableSelectItem(): MappableSelectItem { - return MappableSelectItem( + fun actionMappableSelectPoint(): MappableSelectItem.MappableSelectPoint { + return MappableSelectItem.MappableSelectPoint( 0, - listOf(MapPoint(0.0, 0.0)), - R.drawable.ic_lock_power_off, - R.drawable.ic_lock_idle_charging, "0", listOf(IconifiedText(R.drawable.ic_lock_idle_charging, "An item")), + point = MapPoint(0.0, 0.0), + smallIcon = R.drawable.ic_lock_power_off, + largeIcon = R.drawable.ic_lock_idle_charging, action = IconifiedText(R.drawable.ic_delete, "Action") ) } - fun infoMappableSelectItem(): MappableSelectItem { - return MappableSelectItem( + fun infoMappableSelectPoint(): MappableSelectItem.MappableSelectPoint { + return MappableSelectItem.MappableSelectPoint( 0, - listOf(MapPoint(0.0, 0.0)), - R.drawable.ic_lock_power_off, - R.drawable.ic_lock_idle_charging, "0", listOf(IconifiedText(R.drawable.ic_lock_idle_charging, "An item")), + point = MapPoint(0.0, 0.0), + smallIcon = R.drawable.ic_lock_power_off, + largeIcon = R.drawable.ic_lock_idle_charging, info = "Info" ) } + + fun actionMappableSelectLine(): MappableSelectItem.MappableSelectLine { + return MappableSelectItem.MappableSelectLine( + 0, + "0", + listOf(IconifiedText(R.drawable.ic_lock_idle_charging, "An item")), + points = listOf(MapPoint(0.0, 0.0), MapPoint(1.0, 1.0)), + action = IconifiedText(R.drawable.ic_delete, "Action") + ) + } + + fun actionMappableSelectPolygon(): MappableSelectItem.MappableSelectPolygon { + return MappableSelectItem.MappableSelectPolygon( + 0, + "0", + listOf(IconifiedText(R.drawable.ic_lock_idle_charging, "An item")), + points = listOf(MapPoint(0.0, 0.0), MapPoint(1.0, 1.0)), + action = IconifiedText(R.drawable.ic_delete, "Action") + ) + } } diff --git a/google-maps/src/main/java/org/odk/collect/googlemaps/GoogleMapFragment.java b/google-maps/src/main/java/org/odk/collect/googlemaps/GoogleMapFragment.java index d72eed16798..a078a5a431c 100644 --- a/google-maps/src/main/java/org/odk/collect/googlemaps/GoogleMapFragment.java +++ b/google-maps/src/main/java/org/odk/collect/googlemaps/GoogleMapFragment.java @@ -14,9 +14,6 @@ package org.odk.collect.googlemaps; -import static org.odk.collect.maps.MapConsts.POLYGON_FILL_COLOR_OPACITY; -import static org.odk.collect.maps.MapConsts.POLYLINE_STROKE_WIDTH; - import android.annotation.SuppressLint; import android.content.Context; import android.location.Location; @@ -25,7 +22,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.core.graphics.ColorUtils; import com.google.android.gms.location.LocationListener; import com.google.android.gms.maps.CameraUpdate; @@ -51,10 +47,12 @@ import org.odk.collect.androidshared.ui.ToastUtils; import org.odk.collect.googlemaps.GoogleMapConfigurator.GoogleMapTypeOption; import org.odk.collect.location.LocationClient; +import org.odk.collect.maps.LineDescription; import org.odk.collect.maps.MapConfigurator; import org.odk.collect.maps.MapFragment; import org.odk.collect.maps.MapFragmentDelegate; import org.odk.collect.maps.MapPoint; +import org.odk.collect.maps.PolygonDescription; import org.odk.collect.maps.layers.MapFragmentReferenceLayerUtils; import org.odk.collect.maps.layers.ReferenceLayerRepository; import org.odk.collect.maps.markers.MarkerDescription; @@ -306,20 +304,20 @@ public List addMarkers(List markers) { return feature instanceof MarkerFeature ? ((MarkerFeature) feature).getPoint() : null; } - @Override public int addPolyLine(@NonNull Iterable points, boolean closed, boolean draggable) { + @Override public int addPolyLine(LineDescription lineDescription) { int featureId = nextFeatureId++; - if (draggable) { - features.put(featureId, new DynamicPolyLineFeature(getActivity(), points, closed, map)); + if (lineDescription.getDraggable()) { + features.put(featureId, new DynamicPolyLineFeature(getActivity(), lineDescription, map)); } else { - features.put(featureId, new StaticPolyLineFeature(getActivity(), points, closed, map)); + features.put(featureId, new StaticPolyLineFeature(lineDescription, map)); } return featureId; } @Override - public int addPolygon(@NonNull Iterable points) { + public int addPolygon(PolygonDescription polygonDescription) { int featureId = nextFeatureId++; - features.put(featureId, new StaticPolygonFeature(map, points, requireContext().getResources().getColor(org.odk.collect.icons.R.color.mapLineColor))); + features.put(featureId, new StaticPolygonFeature(map, polygonDescription)); return featureId; } @@ -778,22 +776,22 @@ private static class StaticPolyLineFeature implements MapFeature { private Polyline polyline; - StaticPolyLineFeature(Context context, Iterable points, boolean closedPolygon, GoogleMap map) { + StaticPolyLineFeature(LineDescription lineDescription, GoogleMap map) { if (map == null) { // during Robolectric tests, map will be null return; } - List latLngs = StreamSupport.stream(points.spliterator(), false).map(mapPoint -> new LatLng(mapPoint.latitude, mapPoint.longitude)).collect(Collectors.toList()); - if (closedPolygon && !latLngs.isEmpty()) { + List latLngs = StreamSupport.stream(lineDescription.getPoints().spliterator(), false).map(mapPoint -> new LatLng(mapPoint.latitude, mapPoint.longitude)).collect(Collectors.toList()); + if (lineDescription.getClosed() && !latLngs.isEmpty()) { latLngs.add(latLngs.get(0)); } if (latLngs.isEmpty()) { clearPolyline(); } else if (polyline == null) { polyline = map.addPolyline(new PolylineOptions() - .color(context.getResources().getColor(org.odk.collect.icons.R.color.mapLineColor)) + .color(lineDescription.getStrokeColor()) .zIndex(1) - .width(POLYLINE_STROKE_WIDTH) + .width(lineDescription.getStrokeWidth()) .addAll(latLngs) .clickable(true) ); @@ -840,19 +838,19 @@ private static class DynamicPolyLineFeature implements MapFeature { private final Context context; private final GoogleMap map; private final List markers = new ArrayList<>(); - private final boolean closedPolygon; + private final LineDescription lineDescription; private Polyline polyline; - DynamicPolyLineFeature(Context context, Iterable points, boolean closedPolygon, GoogleMap map) { + DynamicPolyLineFeature(Context context, LineDescription lineDescription, GoogleMap map) { this.context = context; + this.lineDescription = lineDescription; this.map = map; - this.closedPolygon = closedPolygon; if (map == null) { // during Robolectric tests, map will be null return; } - for (MapPoint point : points) { + for (MapPoint point : lineDescription.getPoints()) { markers.add(createMarker(context, new MarkerDescription(point, true, CENTER, new MarkerIconDescription(org.odk.collect.icons.R.drawable.ic_map_point)), map)); } @@ -880,16 +878,16 @@ public void update() { for (Marker marker : markers) { latLngs.add(marker.getPosition()); } - if (closedPolygon && !latLngs.isEmpty()) { + if (lineDescription.getClosed() && !latLngs.isEmpty()) { latLngs.add(latLngs.get(0)); } if (markers.isEmpty()) { clearPolyline(); } else if (polyline == null) { polyline = map.addPolyline(new PolylineOptions() - .color(context.getResources().getColor(org.odk.collect.icons.R.color.mapLineColor)) + .color(lineDescription.getStrokeColor()) .zIndex(1) - .width(POLYLINE_STROKE_WIDTH) + .width(lineDescription.getStrokeWidth()) .addAll(latLngs) .clickable(true) ); @@ -943,12 +941,12 @@ private void clearPolyline() { private static class StaticPolygonFeature implements MapFeature { private Polygon polygon; - StaticPolygonFeature(GoogleMap map, Iterable points, int strokeLineColor) { + StaticPolygonFeature(GoogleMap map, PolygonDescription polygonDescription) { polygon = map.addPolygon(new PolygonOptions() - .addAll(StreamSupport.stream(points.spliterator(), false).map(mapPoint -> new LatLng(mapPoint.latitude, mapPoint.longitude)).collect(Collectors.toList())) - .strokeColor(strokeLineColor) - .strokeWidth(POLYLINE_STROKE_WIDTH) - .fillColor(ColorUtils.setAlphaComponent(strokeLineColor, POLYGON_FILL_COLOR_OPACITY)) + .addAll(StreamSupport.stream(polygonDescription.getPoints().spliterator(), false).map(mapPoint -> new LatLng(mapPoint.latitude, mapPoint.longitude)).collect(Collectors.toList())) + .strokeColor(polygonDescription.getStrokeColor()) + .strokeWidth(polygonDescription.getStrokeWidth()) + .fillColor(polygonDescription.getFillColor()) .clickable(true) ); } diff --git a/icons/src/main/res/drawable/ic_map_point.xml b/icons/src/main/res/drawable/ic_map_point.xml index b133ad2c9ca..f74cc68c255 100644 --- a/icons/src/main/res/drawable/ic_map_point.xml +++ b/icons/src/main/res/drawable/ic_map_point.xml @@ -3,6 +3,6 @@ android:height="36dp" android:viewportWidth="36" android:viewportHeight="36"> - + diff --git a/icons/src/main/res/values/map_colors.xml b/icons/src/main/res/values/map_colors.xml deleted file mode 100644 index f8a25398e7c..00000000000 --- a/icons/src/main/res/values/map_colors.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - #ffff0000 - diff --git a/mapbox/src/main/java/org/odk/collect/mapbox/DynamicPolyLineFeature.kt b/mapbox/src/main/java/org/odk/collect/mapbox/DynamicPolyLineFeature.kt index 1416c320db7..1b9228c27ec 100644 --- a/mapbox/src/main/java/org/odk/collect/mapbox/DynamicPolyLineFeature.kt +++ b/mapbox/src/main/java/org/odk/collect/mapbox/DynamicPolyLineFeature.kt @@ -2,7 +2,6 @@ package org.odk.collect.mapbox import android.content.Context import com.mapbox.geojson.Point -import com.mapbox.maps.extension.style.utils.ColorUtils import com.mapbox.maps.plugin.annotation.generated.OnPointAnnotationClickListener import com.mapbox.maps.plugin.annotation.generated.OnPointAnnotationDragListener import com.mapbox.maps.plugin.annotation.generated.PointAnnotation @@ -10,7 +9,7 @@ import com.mapbox.maps.plugin.annotation.generated.PointAnnotationManager import com.mapbox.maps.plugin.annotation.generated.PolylineAnnotation import com.mapbox.maps.plugin.annotation.generated.PolylineAnnotationManager import com.mapbox.maps.plugin.annotation.generated.PolylineAnnotationOptions -import org.odk.collect.maps.MapConsts.MAPBOX_POLYLINE_STROKE_WIDTH +import org.odk.collect.maps.LineDescription import org.odk.collect.maps.MapFragment import org.odk.collect.maps.MapPoint @@ -22,8 +21,7 @@ internal class DynamicPolyLineFeature( private val featureId: Int, private val featureClickListener: MapFragment.FeatureListener?, private val featureDragEndListener: MapFragment.FeatureListener?, - private val closedPolygon: Boolean, - initMapPoints: Iterable + private val lineDescription: LineDescription ) : MapFeature { val mapPoints = mutableListOf() private val pointAnnotations = mutableListOf() @@ -32,7 +30,7 @@ internal class DynamicPolyLineFeature( private var polylineAnnotation: PolylineAnnotation? = null init { - initMapPoints.forEach { + lineDescription.points.forEach { mapPoints.add(it) pointAnnotations.add( MapUtils.createPointAnnotation( @@ -108,7 +106,7 @@ internal class DynamicPolyLineFeature( } .toMutableList() .also { - if (closedPolygon && it.isNotEmpty()) { + if (lineDescription.closed && it.isNotEmpty()) { it.add(it.first()) } } @@ -121,8 +119,8 @@ internal class DynamicPolyLineFeature( polylineAnnotation = polylineAnnotationManager.create( PolylineAnnotationOptions() .withPoints(points) - .withLineColor(ColorUtils.colorToRgbaString(context.resources.getColor(org.odk.collect.icons.R.color.mapLineColor))) - .withLineWidth(MAPBOX_POLYLINE_STROKE_WIDTH.toDouble()) + .withLineColor(lineDescription.getStrokeColor()) + .withLineWidth(MapUtils.convertStrokeWidth(lineDescription)) ).also { polylineAnnotationManager.update(it) } diff --git a/mapbox/src/main/java/org/odk/collect/mapbox/MapUtils.kt b/mapbox/src/main/java/org/odk/collect/mapbox/MapUtils.kt index 06bf7b4b296..b2576797512 100644 --- a/mapbox/src/main/java/org/odk/collect/mapbox/MapUtils.kt +++ b/mapbox/src/main/java/org/odk/collect/mapbox/MapUtils.kt @@ -6,6 +6,7 @@ import com.mapbox.maps.extension.style.layers.properties.generated.IconAnchor import com.mapbox.maps.plugin.annotation.generated.PointAnnotation import com.mapbox.maps.plugin.annotation.generated.PointAnnotationManager import com.mapbox.maps.plugin.annotation.generated.PointAnnotationOptions +import org.odk.collect.maps.LineDescription import org.odk.collect.maps.MapFragment import org.odk.collect.maps.MapPoint import org.odk.collect.maps.markers.MarkerDescription @@ -65,4 +66,10 @@ object MapUtils { // deviation fields are no longer meaningful; reset them to zero. return MapPoint(pointAnnotation.point.latitude(), pointAnnotation.point.longitude(), 0.0, 0.0) } + + // To ensure consistent stroke width across map platforms like Mapbox, Google, and OSM, + // the value for Mapbox needs to be divided by 3. + fun convertStrokeWidth(lineDescription: LineDescription): Double { + return (lineDescription.getStrokeWidth() / 3).toDouble() + } } diff --git a/mapbox/src/main/java/org/odk/collect/mapbox/MapboxMapFragment.kt b/mapbox/src/main/java/org/odk/collect/mapbox/MapboxMapFragment.kt index 7fcde3857ea..c644812a2e7 100644 --- a/mapbox/src/main/java/org/odk/collect/mapbox/MapboxMapFragment.kt +++ b/mapbox/src/main/java/org/odk/collect/mapbox/MapboxMapFragment.kt @@ -50,6 +50,7 @@ import kotlinx.coroutines.launch import org.odk.collect.androidshared.utils.ScreenUtils import org.odk.collect.location.LocationClient import org.odk.collect.location.LocationClient.LocationClientListener +import org.odk.collect.maps.LineDescription import org.odk.collect.maps.MapFragment import org.odk.collect.maps.MapFragment.ErrorListener import org.odk.collect.maps.MapFragment.FeatureListener @@ -57,6 +58,7 @@ import org.odk.collect.maps.MapFragment.PointListener import org.odk.collect.maps.MapFragment.ReadyListener import org.odk.collect.maps.MapFragmentDelegate import org.odk.collect.maps.MapPoint +import org.odk.collect.maps.PolygonDescription import org.odk.collect.maps.layers.MapFragmentReferenceLayerUtils.getReferenceLayerFile import org.odk.collect.maps.layers.MbtilesFile import org.odk.collect.maps.layers.ReferenceLayerRepository @@ -336,9 +338,9 @@ class MapboxMapFragment : } } - override fun addPolyLine(points: MutableIterable, closed: Boolean, draggable: Boolean): Int { + override fun addPolyLine(lineDescription: LineDescription): Int { val featureId = nextFeatureId++ - if (draggable) { + if (lineDescription.draggable) { features[featureId] = DynamicPolyLineFeature( requireContext(), pointAnnotationManager, @@ -346,28 +348,24 @@ class MapboxMapFragment : featureId, featureClickListener, featureDragEndListener, - closed, - points + lineDescription ) } else { features[featureId] = StaticPolyLineFeature( - requireContext(), polylineAnnotationManager, featureId, featureClickListener, - closed, - points + lineDescription ) } return featureId } - override fun addPolygon(points: MutableIterable): Int { + override fun addPolygon(polygonDescription: PolygonDescription): Int { val featureId = nextFeatureId++ features[featureId] = StaticPolygonFeature( - requireContext(), mapView.annotations.createPolygonAnnotationManager(), - points, + polygonDescription, featureClickListener, featureId ) diff --git a/mapbox/src/main/java/org/odk/collect/mapbox/StaticPolyLineFeature.kt b/mapbox/src/main/java/org/odk/collect/mapbox/StaticPolyLineFeature.kt index e55fec08808..58f7696883d 100644 --- a/mapbox/src/main/java/org/odk/collect/mapbox/StaticPolyLineFeature.kt +++ b/mapbox/src/main/java/org/odk/collect/mapbox/StaticPolyLineFeature.kt @@ -1,29 +1,25 @@ package org.odk.collect.mapbox -import android.content.Context import com.mapbox.geojson.Point -import com.mapbox.maps.extension.style.utils.ColorUtils import com.mapbox.maps.plugin.annotation.generated.PolylineAnnotation import com.mapbox.maps.plugin.annotation.generated.PolylineAnnotationManager import com.mapbox.maps.plugin.annotation.generated.PolylineAnnotationOptions -import org.odk.collect.maps.MapConsts.MAPBOX_POLYLINE_STROKE_WIDTH +import org.odk.collect.maps.LineDescription import org.odk.collect.maps.MapFragment import org.odk.collect.maps.MapPoint /** A polyline that can not be manipulated by dragging Symbols at its vertices. */ internal class StaticPolyLineFeature( - context: Context, private val polylineAnnotationManager: PolylineAnnotationManager, private val featureId: Int, private val featureClickListener: MapFragment.FeatureListener?, - private val closedPolygon: Boolean, - initMapPoints: Iterable + private val lineDescription: LineDescription ) : MapFeature { private val mapPoints = mutableListOf() private var polylineAnnotation: PolylineAnnotation? = null init { - initMapPoints.forEach { + lineDescription.points.forEach { mapPoints.add(it) } @@ -33,7 +29,7 @@ internal class StaticPolyLineFeature( } .toMutableList() .also { - if (closedPolygon && it.isNotEmpty()) { + if (lineDescription.closed && it.isNotEmpty()) { it.add(it.first()) } } @@ -46,8 +42,8 @@ internal class StaticPolyLineFeature( polylineAnnotation = polylineAnnotationManager.create( PolylineAnnotationOptions() .withPoints(points) - .withLineColor(ColorUtils.colorToRgbaString(context.resources.getColor(org.odk.collect.icons.R.color.mapLineColor))) - .withLineWidth(MAPBOX_POLYLINE_STROKE_WIDTH.toDouble()) + .withLineColor(lineDescription.getStrokeColor()) + .withLineWidth(MapUtils.convertStrokeWidth(lineDescription)) ).also { polylineAnnotationManager.update(it) } diff --git a/mapbox/src/main/java/org/odk/collect/mapbox/StaticPolygonFeature.kt b/mapbox/src/main/java/org/odk/collect/mapbox/StaticPolygonFeature.kt index f9eae49aa72..d70846baf16 100644 --- a/mapbox/src/main/java/org/odk/collect/mapbox/StaticPolygonFeature.kt +++ b/mapbox/src/main/java/org/odk/collect/mapbox/StaticPolygonFeature.kt @@ -1,34 +1,25 @@ package org.odk.collect.mapbox -import android.content.Context -import androidx.core.graphics.ColorUtils import com.mapbox.geojson.Point import com.mapbox.maps.plugin.annotation.generated.OnPolygonAnnotationClickListener import com.mapbox.maps.plugin.annotation.generated.PolygonAnnotation import com.mapbox.maps.plugin.annotation.generated.PolygonAnnotationManager import com.mapbox.maps.plugin.annotation.generated.PolygonAnnotationOptions -import org.odk.collect.maps.MapConsts.POLYGON_FILL_COLOR_OPACITY import org.odk.collect.maps.MapFragment -import org.odk.collect.maps.MapPoint +import org.odk.collect.maps.PolygonDescription class StaticPolygonFeature( - context: Context, private val polygonAnnotationManager: PolygonAnnotationManager, - points: Iterable, + polygonDescription: PolygonDescription, featureClickListener: MapFragment.FeatureListener?, featureId: Int ) : MapFeature { private val polygonAnnotation: PolygonAnnotation = polygonAnnotationManager.create( PolygonAnnotationOptions() - .withPoints(listOf(points.map { Point.fromLngLat(it.longitude, it.latitude) })) - .withFillOutlineColor(context.resources.getColor(org.odk.collect.icons.R.color.mapLineColor)) - .withFillColor( - ColorUtils.setAlphaComponent( - context.resources.getColor(org.odk.collect.icons.R.color.mapLineColor), - POLYGON_FILL_COLOR_OPACITY - ) - ) + .withPoints(listOf(polygonDescription.points.map { Point.fromLngLat(it.longitude, it.latitude) })) + .withFillOutlineColor(polygonDescription.getStrokeColor()) + .withFillColor(polygonDescription.getFillColor()) ) private val polygonClickListener = diff --git a/maps/build.gradle.kts b/maps/build.gradle.kts index cd272ca60b2..728b2d3a13c 100644 --- a/maps/build.gradle.kts +++ b/maps/build.gradle.kts @@ -49,6 +49,8 @@ dependencies { coreLibraryDesugaring(Dependencies.desugar) implementation(project(":shared")) + implementation(project(":androidshared")) + implementation(project(":icons")) implementation(Dependencies.kotlin_stdlib) implementation(Dependencies.androidx_fragment_ktx) implementation(Dependencies.androidx_preference_ktx) diff --git a/maps/src/main/java/org/odk/collect/maps/LineDescription.kt b/maps/src/main/java/org/odk/collect/maps/LineDescription.kt new file mode 100644 index 00000000000..b422648db32 --- /dev/null +++ b/maps/src/main/java/org/odk/collect/maps/LineDescription.kt @@ -0,0 +1,30 @@ +package org.odk.collect.maps + +import org.odk.collect.androidshared.utils.toColorInt + +data class LineDescription( + val points: List = emptyList(), + private val strokeWidth: String? = null, + private val strokeColor: String? = null, + val draggable: Boolean = false, + val closed: Boolean = false +) { + fun getStrokeWidth(): Float { + return try { + strokeWidth?.toFloat()?.let { + if (it >= 0) { + it + } else { + MapConsts.DEFAULT_STROKE_WIDTH + } + } ?: MapConsts.DEFAULT_STROKE_WIDTH + } catch (e: NumberFormatException) { + MapConsts.DEFAULT_STROKE_WIDTH + } + } + + fun getStrokeColor(): Int { + val customColor = strokeColor?.toColorInt() + return customColor ?: MapConsts.DEFAULT_STROKE_COLOR + } +} diff --git a/maps/src/main/java/org/odk/collect/maps/MapConsts.kt b/maps/src/main/java/org/odk/collect/maps/MapConsts.kt index 0fdd2855bcb..a95e71b0aad 100644 --- a/maps/src/main/java/org/odk/collect/maps/MapConsts.kt +++ b/maps/src/main/java/org/odk/collect/maps/MapConsts.kt @@ -1,7 +1,7 @@ package org.odk.collect.maps object MapConsts { - const val POLYLINE_STROKE_WIDTH = 8 - const val MAPBOX_POLYLINE_STROKE_WIDTH = 4 - const val POLYGON_FILL_COLOR_OPACITY = 68 + const val DEFAULT_STROKE_COLOR = -65536 // color-int representation of #ffff0000 + const val DEFAULT_STROKE_WIDTH = 8f + const val DEFAULT_FILL_COLOR_OPACITY = 68 } diff --git a/maps/src/main/java/org/odk/collect/maps/MapFragment.java b/maps/src/main/java/org/odk/collect/maps/MapFragment.java index ebe00240167..5c8cacb6f21 100644 --- a/maps/src/main/java/org/odk/collect/maps/MapFragment.java +++ b/maps/src/main/java/org/odk/collect/maps/MapFragment.java @@ -118,13 +118,13 @@ public interface MapFragment { * The vertices will have handles that can be dragged by the user. * Returns a positive integer, the featureId for the newly added shape. */ - int addPolyLine(@NonNull Iterable points, boolean closed, boolean draggable); + int addPolyLine(LineDescription lineDescription); /** * Adds a polygon to the map with given sequence of vertices. * Returns a positive integer, * the featureId for the newly added shape. */ - int addPolygon(@NonNull Iterable points); + int addPolygon(PolygonDescription polygonDescription); /** Appends a vertex to the polyline or polygon specified by featureId. */ void appendPointToPolyLine(int featureId, @NonNull MapPoint point); diff --git a/maps/src/main/java/org/odk/collect/maps/PolygonDescription.kt b/maps/src/main/java/org/odk/collect/maps/PolygonDescription.kt new file mode 100644 index 00000000000..f97442bb661 --- /dev/null +++ b/maps/src/main/java/org/odk/collect/maps/PolygonDescription.kt @@ -0,0 +1,45 @@ +package org.odk.collect.maps + +import androidx.core.graphics.ColorUtils +import org.odk.collect.androidshared.utils.toColorInt + +data class PolygonDescription( + val points: List = emptyList(), + private val strokeWidth: String? = null, + private val strokeColor: String? = null, + private val fillColor: String? = null +) { + fun getStrokeWidth(): Float { + return try { + strokeWidth?.toFloat()?.let { + if (it >= 0) { + it + } else { + MapConsts.DEFAULT_STROKE_WIDTH + } + } ?: MapConsts.DEFAULT_STROKE_WIDTH + } catch (e: NumberFormatException) { + MapConsts.DEFAULT_STROKE_WIDTH + } + } + + fun getStrokeColor(): Int { + val customColor = strokeColor?.toColorInt() + return customColor ?: MapConsts.DEFAULT_STROKE_COLOR + } + + fun getFillColor(): Int { + val customColor = fillColor?.toColorInt()?.let { + ColorUtils.setAlphaComponent( + it, + MapConsts.DEFAULT_FILL_COLOR_OPACITY + ) + } + + return customColor + ?: ColorUtils.setAlphaComponent( + MapConsts.DEFAULT_STROKE_COLOR, + MapConsts.DEFAULT_FILL_COLOR_OPACITY + ) + } +} diff --git a/maps/src/main/java/org/odk/collect/maps/markers/MarkerIconDescription.kt b/maps/src/main/java/org/odk/collect/maps/markers/MarkerIconDescription.kt index 85e693d3394..3424367b8ea 100644 --- a/maps/src/main/java/org/odk/collect/maps/markers/MarkerIconDescription.kt +++ b/maps/src/main/java/org/odk/collect/maps/markers/MarkerIconDescription.kt @@ -1,6 +1,6 @@ package org.odk.collect.maps.markers -import android.graphics.Color +import org.odk.collect.androidshared.utils.toColorInt import org.odk.collect.shared.strings.StringUtils import java.util.Locale @@ -9,23 +9,7 @@ class MarkerIconDescription @JvmOverloads constructor( private val color: String? = null, private val symbol: String? = null ) { - fun getColor(): Int? = try { - color?.let { - var sanitizedColor = if (color.startsWith("#")) { - color - } else { - "#$color" - } - - if (sanitizedColor.length == 4) { - sanitizedColor = shorthandToLonghandHexColor(sanitizedColor) - } - - Color.parseColor(sanitizedColor) - } - } catch (e: Throwable) { - null - } + fun getColor(): Int? = color?.toColorInt() fun getSymbol(): String? = symbol?.let { if (it.isBlank()) { @@ -34,13 +18,4 @@ class MarkerIconDescription @JvmOverloads constructor( StringUtils.firstCharacterOrEmoji(it).uppercase(Locale.US) } } - - private fun shorthandToLonghandHexColor(shorthandColor: String): String { - var longHandColor = "" - shorthandColor.substring(1).map { - longHandColor += it.toString() + it.toString() - } - - return "#$longHandColor" - } } diff --git a/maps/src/test/java/org/odk/collect/maps/LineDescriptionTest.kt b/maps/src/test/java/org/odk/collect/maps/LineDescriptionTest.kt new file mode 100644 index 00000000000..3ce21facdf6 --- /dev/null +++ b/maps/src/test/java/org/odk/collect/maps/LineDescriptionTest.kt @@ -0,0 +1,58 @@ +package org.odk.collect.maps + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.hamcrest.CoreMatchers.equalTo +import org.hamcrest.MatcherAssert.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class LineDescriptionTest { + @Test + fun `getStrokeWidth returns the default value when the passed one is null`() { + val lineDescription = LineDescription(strokeWidth = null) + assertThat(lineDescription.getStrokeWidth(), equalTo(MapConsts.DEFAULT_STROKE_WIDTH)) + } + + @Test + fun `getStrokeWidth returns the default value when the passed one is invalid`() { + val lineDescription = LineDescription(strokeWidth = "blah") + assertThat(lineDescription.getStrokeWidth(), equalTo(MapConsts.DEFAULT_STROKE_WIDTH)) + } + + @Test + fun `getStrokeWidth returns the default value when the passed one is not greater than or equal to zero`() { + val lineDescription = LineDescription(strokeWidth = "-1") + assertThat(lineDescription.getStrokeWidth(), equalTo(MapConsts.DEFAULT_STROKE_WIDTH)) + } + + @Test + fun `getStrokeWidth returns custom value when the passed one is a valid int number`() { + val lineDescription = LineDescription(strokeWidth = "10") + assertThat(lineDescription.getStrokeWidth(), equalTo(10f)) + } + + @Test + fun `getStrokeWidth returns custom value when the passed one is a valid float number`() { + val lineDescription = LineDescription(strokeWidth = "10.5") + assertThat(lineDescription.getStrokeWidth(), equalTo(10.5f)) + } + + @Test + fun `getStrokeColor returns the default color when the passed one is null`() { + val lineDescription = LineDescription(strokeColor = null) + assertThat(lineDescription.getStrokeColor(), equalTo(MapConsts.DEFAULT_STROKE_COLOR)) + } + + @Test + fun `getStrokeColor returns the default color when the passed one is invalid`() { + val lineDescription = LineDescription(strokeColor = "blah") + assertThat(lineDescription.getStrokeColor(), equalTo(MapConsts.DEFAULT_STROKE_COLOR)) + } + + @Test + fun `getStrokeColor returns custom color when it is valid`() { + val lineDescription = LineDescription(strokeColor = "#aaccee") + assertThat(lineDescription.getStrokeColor(), equalTo(-5583634)) + } +} diff --git a/maps/src/test/java/org/odk/collect/maps/MarkerIconDescriptionTest.kt b/maps/src/test/java/org/odk/collect/maps/MarkerIconDescriptionTest.kt index f70b64cf154..94ba04b17a2 100644 --- a/maps/src/test/java/org/odk/collect/maps/MarkerIconDescriptionTest.kt +++ b/maps/src/test/java/org/odk/collect/maps/MarkerIconDescriptionTest.kt @@ -10,49 +10,12 @@ import org.odk.collect.maps.markers.MarkerIconDescription @RunWith(AndroidJUnit4::class) class MarkerIconDescriptionTest { - @Test fun `return null when color is null`() { val markerIconDescription = MarkerIconDescription(0, null) assertThat(markerIconDescription.getColor(), `is`(nullValue())) } - @Test - fun `return null when color is empty`() { - val markerIconDescription = MarkerIconDescription(0, "") - assertThat(markerIconDescription.getColor(), `is`(nullValue())) - } - - @Test - fun `return null when color is invalid`() { - val markerIconDescription = MarkerIconDescription(0, "qwerty") - assertThat(markerIconDescription.getColor(), `is`(nullValue())) - } - - @Test - fun `return color int for valid hex color with # prefix`() { - val markerIconDescription = MarkerIconDescription(0, "#aaccee") - assertThat(markerIconDescription.getColor(), `is`(-5583634)) - } - - @Test - fun `return color int for valid hex color without # prefix`() { - val markerIconDescription = MarkerIconDescription(0, "aaccee") - assertThat(markerIconDescription.getColor(), `is`(-5583634)) - } - - @Test - fun `return color int for valid shorthand hex color with # prefix`() { - val markerIconDescription = MarkerIconDescription(0, "#ace") - assertThat(markerIconDescription.getColor(), `is`(-5583634)) - } - - @Test - fun `return color int for valid shorthand hex color without # prefix`() { - val markerIconDescription = MarkerIconDescription(0, "ace") - assertThat(markerIconDescription.getColor(), `is`(-5583634)) - } - @Test fun `return null when symbol is null`() { val markerIconDescription = MarkerIconDescription(0, symbol = null) diff --git a/maps/src/test/java/org/odk/collect/maps/PolygonDescriptionTest.kt b/maps/src/test/java/org/odk/collect/maps/PolygonDescriptionTest.kt new file mode 100644 index 00000000000..52ea8cd1f86 --- /dev/null +++ b/maps/src/test/java/org/odk/collect/maps/PolygonDescriptionTest.kt @@ -0,0 +1,76 @@ +package org.odk.collect.maps + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.hamcrest.CoreMatchers.equalTo +import org.hamcrest.MatcherAssert.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class PolygonDescriptionTest { + @Test + fun `getStrokeWidth returns the default value when the passed one is null`() { + val polygonDescription = PolygonDescription(strokeWidth = null) + assertThat(polygonDescription.getStrokeWidth(), equalTo(MapConsts.DEFAULT_STROKE_WIDTH)) + } + + @Test + fun `getStrokeWidth returns the default value when the passed one is invalid`() { + val polygonDescription = PolygonDescription(strokeWidth = "blah") + assertThat(polygonDescription.getStrokeWidth(), equalTo(MapConsts.DEFAULT_STROKE_WIDTH)) + } + + @Test + fun `getStrokeWidth returns the default value when the passed one is not greater than or equal to zero`() { + val polygonDescription = PolygonDescription(strokeWidth = "-1") + assertThat(polygonDescription.getStrokeWidth(), equalTo(MapConsts.DEFAULT_STROKE_WIDTH)) + } + + @Test + fun `getStrokeWidth returns custom value when the passed one is a valid int number`() { + val polygonDescription = PolygonDescription(strokeWidth = "10") + assertThat(polygonDescription.getStrokeWidth(), equalTo(10f)) + } + + @Test + fun `getStrokeWidth returns custom value when the passed one is a valid float number`() { + val polygonDescription = PolygonDescription(strokeWidth = "10.5") + assertThat(polygonDescription.getStrokeWidth(), equalTo(10.5f)) + } + + @Test + fun `getStrokeColor returns the default color when the passed one is null`() { + val polygonDescription = PolygonDescription(strokeColor = null) + assertThat(polygonDescription.getStrokeColor(), equalTo(MapConsts.DEFAULT_STROKE_COLOR)) + } + + @Test + fun `getStrokeColor returns the default color when the passed one is invalid`() { + val polygonDescription = PolygonDescription(strokeColor = "blah") + assertThat(polygonDescription.getStrokeColor(), equalTo(MapConsts.DEFAULT_STROKE_COLOR)) + } + + @Test + fun `getStrokeColor returns custom color when it is valid`() { + val polygonDescription = PolygonDescription(strokeColor = "#aaccee") + assertThat(polygonDescription.getStrokeColor(), equalTo(-5583634)) + } + + @Test + fun `getFillColor returns the default color when the passed one is null`() { + val polygonDescription = PolygonDescription(fillColor = null) + assertThat(polygonDescription.getFillColor(), equalTo(1157562368)) + } + + @Test + fun `getFillColor returns the default color when the passed one is invalid`() { + val polygonDescription = PolygonDescription(fillColor = "blah") + assertThat(polygonDescription.getFillColor(), equalTo(1157562368)) + } + + @Test + fun `getFillColor returns custom color when it is valid`() { + val polygonDescription = PolygonDescription(fillColor = "#aaccee") + assertThat(polygonDescription.getFillColor(), equalTo(1152044270)) + } +} diff --git a/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java b/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java index 2cc434885b6..b830dbbdee7 100644 --- a/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java +++ b/osmdroid/src/main/java/org/odk/collect/osmdroid/OsmDroidMapFragment.java @@ -16,9 +16,6 @@ import static androidx.core.graphics.drawable.DrawableKt.toBitmap; -import static org.odk.collect.maps.MapConsts.POLYGON_FILL_COLOR_OPACITY; -import static org.odk.collect.maps.MapConsts.POLYLINE_STROKE_WIDTH; - import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -38,17 +35,18 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; -import androidx.core.graphics.ColorUtils; import androidx.fragment.app.Fragment; import com.google.android.gms.location.LocationListener; import org.odk.collect.androidshared.system.ContextUtils; import org.odk.collect.location.LocationClient; +import org.odk.collect.maps.LineDescription; import org.odk.collect.maps.MapConfigurator; import org.odk.collect.maps.MapFragment; import org.odk.collect.maps.MapFragmentDelegate; import org.odk.collect.maps.MapPoint; +import org.odk.collect.maps.PolygonDescription; import org.odk.collect.maps.layers.MapFragmentReferenceLayerUtils; import org.odk.collect.maps.layers.ReferenceLayerRepository; import org.odk.collect.maps.markers.MarkerDescription; @@ -333,20 +331,20 @@ MapPoint getMarkerPoint(int featureId) { } @Override - public int addPolyLine(@NonNull Iterable points, boolean closed, boolean draggable) { + public int addPolyLine(LineDescription lineDescription) { int featureId = nextFeatureId++; - if (draggable) { - features.put(featureId, new DynamicPolyLineFeature(map, points, closed)); + if (lineDescription.getDraggable()) { + features.put(featureId, new DynamicPolyLineFeature(map, lineDescription)); } else { - features.put(featureId, new StaticPolyLineFeature(map, points, closed)); + features.put(featureId, new StaticPolyLineFeature(map, lineDescription)); } return featureId; } @Override - public int addPolygon(@NonNull Iterable points) { + public int addPolygon(PolygonDescription polygonDescription) { int featureId = nextFeatureId++; - features.put(featureId, new StaticPolygonFeature(map, points)); + features.put(featureId, new StaticPolygonFeature(map, polygonDescription)); return featureId; } @@ -801,11 +799,11 @@ private class StaticPolyLineFeature implements MapFeature { final Polyline polyline; final boolean closedPolygon; - StaticPolyLineFeature(MapView map, Iterable points, boolean closedPolygon) { + StaticPolyLineFeature(MapView map, LineDescription lineDescription) { this.map = map; - this.closedPolygon = closedPolygon; + this.closedPolygon = lineDescription.getClosed(); polyline = new Polyline(); - polyline.setColor(map.getContext().getResources().getColor(org.odk.collect.icons.R.color.mapLineColor)); + polyline.setColor(lineDescription.getStrokeColor()); polyline.setOnClickListener((clickedPolyline, mapView, eventPos) -> { int featureId = findFeature(clickedPolyline); if (featureClickListener != null && featureId != -1) { @@ -815,10 +813,10 @@ private class StaticPolyLineFeature implements MapFeature { return false; }); Paint paint = polyline.getPaint(); - paint.setStrokeWidth(POLYLINE_STROKE_WIDTH); + paint.setStrokeWidth(lineDescription.getStrokeWidth()); map.getOverlays().add(polyline); - List geoPoints = StreamSupport.stream(points.spliterator(), false).map(mapPoint -> new GeoPoint(mapPoint.latitude, mapPoint.longitude, mapPoint.altitude)).collect(Collectors.toList()); + List geoPoints = StreamSupport.stream(lineDescription.getPoints().spliterator(), false).map(mapPoint -> new GeoPoint(mapPoint.latitude, mapPoint.longitude, mapPoint.altitude)).collect(Collectors.toList()); if (closedPolygon && !geoPoints.isEmpty()) { geoPoints.add(geoPoints.get(0)); } @@ -860,11 +858,11 @@ private class DynamicPolyLineFeature implements MapFeature { final Polyline polyline; final boolean closedPolygon; - DynamicPolyLineFeature(MapView map, Iterable points, boolean closedPolygon) { + DynamicPolyLineFeature(MapView map, LineDescription lineDescription) { this.map = map; - this.closedPolygon = closedPolygon; + this.closedPolygon = lineDescription.getClosed(); polyline = new Polyline(); - polyline.setColor(map.getContext().getResources().getColor(org.odk.collect.icons.R.color.mapLineColor)); + polyline.setColor(lineDescription.getStrokeColor()); polyline.setOnClickListener((clickedPolyline, mapView, eventPos) -> { int featureId = findFeature(clickedPolyline); if (featureClickListener != null && featureId != -1) { @@ -874,9 +872,9 @@ private class DynamicPolyLineFeature implements MapFeature { return false; }); Paint paint = polyline.getPaint(); - paint.setStrokeWidth(POLYLINE_STROKE_WIDTH); + paint.setStrokeWidth(lineDescription.getStrokeWidth()); map.getOverlays().add(polyline); - for (MapPoint point : points) { + for (MapPoint point : lineDescription.getPoints()) { markers.add(createMarker(map, new MarkerDescription(point, true, CENTER, new MarkerIconDescription(org.odk.collect.icons.R.drawable.ic_map_point)))); } update(); @@ -946,15 +944,14 @@ private class StaticPolygonFeature implements MapFeature { private final MapView map; private final Polygon polygon = new Polygon(); - StaticPolygonFeature(MapView map, Iterable points) { + StaticPolygonFeature(MapView map, PolygonDescription polygonDescription) { this.map = map; map.getOverlays().add(polygon); - int strokeColor = map.getContext().getResources().getColor(org.odk.collect.icons.R.color.mapLineColor); - polygon.getOutlinePaint().setColor(strokeColor); - polygon.setStrokeWidth(POLYLINE_STROKE_WIDTH); - polygon.getFillPaint().setColor(ColorUtils.setAlphaComponent(strokeColor, POLYGON_FILL_COLOR_OPACITY)); - polygon.setPoints(StreamSupport.stream(points.spliterator(), false).map(point -> new GeoPoint(point.latitude, point.longitude)).collect(Collectors.toList())); + polygon.getOutlinePaint().setColor(polygonDescription.getStrokeColor()); + polygon.setStrokeWidth(polygonDescription.getStrokeWidth()); + polygon.getFillPaint().setColor(polygonDescription.getFillColor()); + polygon.setPoints(StreamSupport.stream(polygonDescription.getPoints().spliterator(), false).map(point -> new GeoPoint(point.latitude, point.longitude)).collect(Collectors.toList())); polygon.setOnClickListener((polygon, mapView, eventPos) -> { int featureId = findFeature(polygon); if (featureClickListener != null && featureId != -1) {