Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve selecting shapes/traces #5463

Merged
merged 9 commits into from
Mar 8, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,15 @@ class SelectionMapFragment(
@Inject
lateinit var permissionsChecker: PermissionsChecker

private val selectedFeatureViewModel by viewModels<SelectedFeatureViewModel>()
private val selectedItemViewModel by viewModels<SelectedItemViewModel>()

private lateinit var map: MapFragment
private lateinit var summarySheetBehavior: BottomSheetBehavior<*>
private lateinit var summarySheet: SelectionSummarySheet
private lateinit var bottomSheetCallback: BottomSheetCallback

private val itemsByFeatureId: MutableMap<Int, MappableSelectItem> = mutableMapOf()
private val featureIdsByItemId: MutableMap<Long, Int> = mutableMapOf()
Copy link
Member Author

Choose a reason for hiding this comment

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

It maybe feels excessive to have two Map indexes here, but otherwise we'd need to do linear searches on several click events, and we build these maps in a loop we're already performing.


/**
* Points to be mapped. Note: kept separately from [.itemsByFeatureId] so we can
Expand Down Expand Up @@ -234,10 +235,10 @@ class SelectionMapFragment(

bottomSheetCallback = object : BottomSheetCallback() {
override fun onStateChanged(onStateChangedbottomSheet: View, newState: Int) {
val selectedFeatureId = selectedFeatureViewModel.getSelectedFeatureId()
if (newState == STATE_HIDDEN && selectedFeatureId != null) {
selectedFeatureViewModel.setSelectedFeatureId(null)
resetIcon(selectedFeatureId)
val selectedItem = selectedItemViewModel.getSelectedItem()
if (newState == STATE_HIDDEN && selectedItem != null) {
selectedItemViewModel.setSelectedItem(null)
resetIcon(selectedItem)

closeSummarySheet.isEnabled = false
} else {
Expand Down Expand Up @@ -265,20 +266,25 @@ class SelectionMapFragment(
}

private fun onFeatureClicked(featureId: Int, maintainZoom: Boolean = true) {
val selectedFeatureId = selectedFeatureViewModel.getSelectedFeatureId()
if (selectedFeatureId != null && selectedFeatureId != featureId) {
resetIcon(selectedFeatureId)
}

val item = itemsByFeatureId[featureId]
val selectedItem = selectedItemViewModel.getSelectedItem()

if (item != null) {
if (!skipSummary) {
val point = item.points[0]
if (selectedItem != null && selectedItem.id != item.id) {
resetIcon(selectedItem)
}

if (maintainZoom) {
map.zoomToPoint(MapPoint(point.latitude, point.longitude), map.zoom, true)
if (!skipSummary) {
if (item.points.size > 1) {
map.zoomToBoundingBox(item.points, 0.8, true)
Copy link
Member Author

Choose a reason for hiding this comment

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

We're doing a "zoom to fit" when selecting a shape/trace rather than maintaining the user zoom. I think that addresses some of the feedback around it being hard to see which item is selected (without having to deal with color/shape changes), but as @lognaturel has pointed out to me, it isn't great in situations where the user taps on the wrong item and then loses context.

} else {
map.zoomToPoint(MapPoint(point.latitude, point.longitude), true)
val point = item.points[0]

if (maintainZoom) {
map.zoomToPoint(MapPoint(point.latitude, point.longitude), map.zoom, true)
} else {
map.zoomToPoint(MapPoint(point.latitude, point.longitude), true)
}
}

map.setMarkerIcon(
Expand All @@ -298,7 +304,7 @@ class SelectionMapFragment(
}
)

selectedFeatureViewModel.setSelectedFeatureId(featureId)
selectedItemViewModel.setSelectedItem(item)
} else {
parentFragmentManager.setFragmentResult(
REQUEST_SELECT_ITEM,
Expand All @@ -323,10 +329,13 @@ class SelectionMapFragment(

val previouslySelectedItem =
itemsByFeatureId.filter { it.value.selected }.map { it.key }.firstOrNull()
val selectedFeatureId = selectedFeatureViewModel.getSelectedFeatureId()
val selectedItem = selectedItemViewModel.getSelectedItem()

if (selectedFeatureId != null) {
onFeatureClicked(selectedFeatureId)
if (selectedItem != null) {
val featureId = featureIdsByItemId[selectedItem.id]
if (featureId != null) {
onFeatureClicked(featureId)
}
} else if (previouslySelectedItem != null) {
onFeatureClicked(previouslySelectedItem, maintainZoom = false)
} else if (!map.hasCenter()) {
Expand All @@ -341,12 +350,14 @@ class SelectionMapFragment(
}
}

private fun resetIcon(selectedFeatureId: Int) {
val item = itemsByFeatureId[selectedFeatureId]!!
map.setMarkerIcon(
selectedFeatureId,
MarkerIconDescription(item.smallIcon, item.color, item.symbol)
)
private fun resetIcon(selectedItem: MappableSelectItem) {
val featureId = featureIdsByItemId[selectedItem.id]
if (featureId != null) {
map.setMarkerIcon(
featureId,
MarkerIconDescription(selectedItem.smallIcon, selectedItem.color, selectedItem.symbol)
)
}
}

/**
Expand Down Expand Up @@ -376,8 +387,9 @@ class SelectionMapFragment(
ids + map.addPoly(item.points, false, false)
}

items.zip(pointIds + traceIds).forEach { (item, featureId) ->
(singlePoints + traces).zip(pointIds + traceIds).forEach { (item, featureId) ->
itemsByFeatureId[featureId] = item
featureIdsByItemId[item.id] = featureId
points.addAll(item.points)
}

Expand All @@ -391,16 +403,16 @@ class SelectionMapFragment(
}
}

internal class SelectedFeatureViewModel : ViewModel() {
internal class SelectedItemViewModel : ViewModel() {

private var selectedFeatureId: Int? = null
private var selectedItem: MappableSelectItem? = null

fun getSelectedFeatureId(): Int? {
return selectedFeatureId
fun getSelectedItem(): MappableSelectItem? {
return selectedItem
}

fun setSelectedFeatureId(itemId: Int?) {
selectedFeatureId = itemId
fun setSelectedItem(item: MappableSelectItem?) {
selectedItem = item
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,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.getPolyPoints(0).size(), equalTo(1));
assertThat(mapFragment.getPolys().get(0).size(), equalTo(1));
}

@Test
Expand All @@ -243,7 +243,7 @@ public void placingPoint_whenPointIsADuplicateOfTheLastPoint_skipsPoint() {

mapFragment.click(new MapPoint(1, 1));
mapFragment.click(new MapPoint(1, 1));
assertThat(mapFragment.getPolyPoints(0).size(), equalTo(1));
assertThat(mapFragment.getPolys().get(0).size(), equalTo(1));
}

private void startInput(int mode) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ class SelectionMapFragmentTest {
}

@Test
fun `zooms to fit all points in polys`() {
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<MappableSelectItem> = listOf(
Fixtures.actionMappableSelectItem().copy(id = 0, points = points)
Expand Down Expand Up @@ -329,7 +329,7 @@ class SelectionMapFragmentTest {
}

@Test
fun `tapping current location button zooms to gps location`() {
fun `clicking current location button zooms to gps location`() {
launcherRule.launchInContainer(SelectionMapFragment::class.java)
map.ready()

Expand All @@ -341,7 +341,7 @@ class SelectionMapFragmentTest {
}

@Test
fun `tapping zoom to fit button zooms to fit all items`() {
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)))
Expand All @@ -358,7 +358,7 @@ class SelectionMapFragmentTest {
}

@Test
fun `tapping zoom to fit button zooms to fit all polys`() {
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<MappableSelectItem> = listOf(
Fixtures.actionMappableSelectItem().copy(id = 0, points = points)
Expand All @@ -373,7 +373,7 @@ class SelectionMapFragmentTest {
}

@Test
fun `tapping layers button navigates to layers settings`() {
fun `clicking layers button navigates to layers settings`() {
val scenario = launcherRule.launchInContainer(SelectionMapFragment::class.java)
map.ready()

Expand All @@ -385,7 +385,7 @@ class SelectionMapFragmentTest {
}

@Test
fun `tapping on item centers on that item with current zoom level`() {
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)))
Expand All @@ -403,7 +403,42 @@ class SelectionMapFragmentTest {
}

@Test
fun `tapping on item switches item marker to large icon`() {
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)
)
whenever(data.getMappableItems()).thenReturn(MutableLiveData(items))

launcherRule.launchInContainer(SelectionMapFragment::class.java)
map.ready()

map.clickOnFeature(1)
assertThat(map.getZoomBoundingBox(), equalTo(Pair(itemPoints, 0.8)))
}

/**
* This looks like a duplicated test, but it's easy to write an implementation that will work
* for everything else and break for interleaved points and traces.
*/
@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))),
)
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]))
}

@Test
fun `clicking on item switches item marker to large icon`() {
val items = listOf(
Fixtures.actionMappableSelectItem().copy(
id = 0,
Expand Down Expand Up @@ -439,7 +474,7 @@ class SelectionMapFragmentTest {
}

@Test
fun `tapping on item when another has been tapped switches the first one back to its small icon`() {
fun `clicking on item when another has been tapped switches the first one back to its small icon`() {
val items = listOf(
Fixtures.actionMappableSelectItem().copy(
id = 0,
Expand Down Expand Up @@ -476,7 +511,7 @@ class SelectionMapFragmentTest {
}

@Test
fun `tapping on item sets item on summary sheet`() {
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"),
Expand All @@ -492,7 +527,7 @@ class SelectionMapFragmentTest {
}

@Test
fun `tapping on item returns item ID as result when skipSummary is true`() {
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),
Expand Down Expand Up @@ -603,7 +638,7 @@ class SelectionMapFragmentTest {
}

@Test
fun `tapping action hides summary sheet`() {
fun `clicking action hides summary sheet`() {
val items = listOf(
Fixtures.actionMappableSelectItem().copy(
id = 0,
Expand Down Expand Up @@ -674,8 +709,8 @@ class SelectionMapFragmentTest {
launcherRule.launchInContainer(SelectionMapFragment::class.java)
map.ready()

map.clickOnFeature(-1)
map.clickOnFeature(-2) // First click is fine but second could use the ID and crash
map.clickOnFeatureId(-1)
map.clickOnFeatureId(-2) // First click is fine but second could use the ID and crash
}

@Test
Expand Down
Loading