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

Feature/sort by distance #382

Merged
merged 4 commits into from
Jun 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.github.swent.echo.di

import com.github.swent.echo.connectivity.GPSService
import com.github.swent.echo.connectivity.SimpleGPSService
import dagger.Module
import dagger.Provides
import dagger.hilt.components.SingletonComponent
import dagger.hilt.testing.TestInstallIn
import javax.inject.Singleton

@Module
@TestInstallIn(
components = [SingletonComponent::class],
replaces = [GPSServiceModule::class],
)
object FakeGPSServiceModule {
@Singleton
@Provides
fun provideGPSService(): GPSService {
return SimpleGPSService()
}
}
2 changes: 2 additions & 0 deletions app/src/main/java/com/github/swent/echo/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import androidx.lifecycle.ViewModelProvider
import com.github.swent.echo.authentication.AuthenticationService
import com.github.swent.echo.compose.components.ConnectivityStatus
import com.github.swent.echo.compose.navigation.AppNavigationHost
import com.github.swent.echo.connectivity.GPSService
import com.github.swent.echo.connectivity.NetworkService
import com.github.swent.echo.data.repository.Repository
import com.github.swent.echo.ui.theme.EchoTheme
Expand All @@ -29,6 +30,7 @@ class MainActivity : ComponentActivity() {
@Inject lateinit var authenticationService: AuthenticationService
@Inject lateinit var repository: Repository
@Inject lateinit var networkService: NetworkService
@Inject lateinit var gpsService: GPSService

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,8 @@ private fun Content(
val selectedAssociation by homeScreenViewModel.selectedAssociation.collectAsState()

val mapDrawerViewModel: MapDrawerViewModel = hiltViewModel()
// user location
val userLocation by homeScreenViewModel.userLocationStateFlow.collectAsState()

Box(modifier = Modifier.padding(paddingValues)) {
// Display the list view or the map view
Expand All @@ -162,6 +164,7 @@ private fun Content(
isOnline,
homeScreenViewModel::refreshEvents,
userId = homeScreenViewModel.userId,
userLocation = userLocation,
modify = { navActions.navigateTo(Routes.EDIT_EVENT.build(it.eventId)) },
viewOnMap = {
mapDrawerViewModel.setSavedCameraPosition(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,11 @@ import androidx.compose.ui.unit.dp
import com.github.swent.echo.R
import com.github.swent.echo.data.model.Event
import com.github.swent.echo.data.model.Tag
import com.mapbox.mapboxsdk.geometry.LatLng
import java.time.format.DateTimeFormatter
import java.util.Locale

const val KM_TO_M = 1000.0

/**
* Composable to display a list of events.
Expand All @@ -49,8 +53,8 @@ import java.time.format.DateTimeFormatter
* not be displayed.
* @param modify The callback to modify the event.
* @param onTagPressed The callback to handle tag presses.
* @param distances The list of distances to the events. If null, the distance will not be
* displayed.
* @param userLocation The user's current location, if any. If null, the distance from events to the
* user will not be displayed.
* @param userId The user ID. If null, none of the events can be modified.
*/
@Composable
Expand All @@ -61,7 +65,7 @@ fun ListDrawer(
viewOnMap: ((Event) -> Unit)? = null,
modify: ((Event) -> Unit) = {},
onTagPressed: (Tag) -> Unit = {},
distances: List<Double>? = null,
userLocation: LatLng? = null,
userId: String? = null,
) {
val selectedEvent = remember { mutableStateOf("") }
Expand All @@ -73,22 +77,24 @@ fun ListDrawer(
.testTag("list_drawer")
) {
// Iterate over the list of events and display them
items(eventsList.size) { index ->
val event = eventsList[index]
val canModifyEvent = event.creator.userId == userId
eventsList.forEach { event ->
item {
val canModifyEvent = event.creator.userId == userId
val spaceBetweenItems = 12.dp

EventListItem(
event = event,
selectedEvent = selectedEvent,
isOnline = isOnline,
refreshEvents = refreshEvents,
viewOnMap = viewOnMap,
modify = modify,
onTagPressed = onTagPressed,
canModifyEvent = canModifyEvent,
distance = distances?.get(index),
)
Spacer(modifier = Modifier.height(12.dp))
EventListItem(
event = event,
selectedEvent = selectedEvent,
isOnline = isOnline,
refreshEvents = refreshEvents,
viewOnMap = viewOnMap,
modify = modify,
onTagPressed = onTagPressed,
canModifyEvent = canModifyEvent,
distance = userLocation?.let { event.location.toLatLng().distanceTo(it) },
)
Spacer(modifier = Modifier.height(spaceBetweenItems))
}
}
}
}
Expand Down Expand Up @@ -124,7 +130,15 @@ fun EventListItem(
// Format the association name to be displayed
val association = event.organizer?.name?.let { "$it • " } ?: ""
// Format the distance to be displayed
val dist = distance?.let { "${it}km • " } ?: ""
val dist =
distance
?.let {
val fmt = { format: String, value: Double ->
String.format(Locale.getDefault(), format, value)
}
if (it < KM_TO_M) fmt("%.0fm", it) else fmt("%.1fkm", it / KM_TO_M)
}
?.let { "$it • " } ?: ""

// Layout constants
val spaceBetweenTagChips = 6.dp
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.github.swent.echo.connectivity

import com.github.swent.echo.compose.map.MAP_CENTER
import com.mapbox.mapboxsdk.geometry.LatLng
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow

class SimpleGPSService : GPSService {
private var _location = MutableStateFlow<LatLng?>(null)

override fun currentUserLocation(): LatLng? = _location.value

override val userLocation: StateFlow<LatLng?> = _location.asStateFlow()

override fun refreshUserLocation() {
_location.compareAndSet(null, MAP_CENTER.toLatLng())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ object GPSServiceModule {
@Singleton
@Provides
fun provideGPSService(application: Application): GPSService {
val context = application.applicationContext
return GPSServiceImpl(context)
return GPSServiceImpl(application.applicationContext)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import com.github.swent.echo.R
import com.github.swent.echo.authentication.AuthenticationService
import com.github.swent.echo.compose.components.searchmenu.FiltersContainer
import com.github.swent.echo.compose.components.searchmenu.floatToDate
import com.github.swent.echo.connectivity.GPSService
import com.github.swent.echo.connectivity.NetworkService
import com.github.swent.echo.data.model.Event
import com.github.swent.echo.data.model.Tag
Expand Down Expand Up @@ -39,11 +40,11 @@ enum class MapOrListMode {
}

// Enum class for the different states of the sort by filter
enum class SortBy(val stringKey: Int) {
DATE_ASC(R.string.filters_container_sort_by_date_asc),
DATE_DESC(R.string.filters_container_sort_by_date_desc),
DISTANCE_ASC(R.string.filters_container_sort_by_distance_asc),
DISTANCE_DESC(R.string.filters_container_sort_by_distance_desc),
enum class SortBy(val stringKey: Int, val descending: Boolean) {
DATE_ASC(R.string.filters_container_sort_by_date_asc, false),
DATE_DESC(R.string.filters_container_sort_by_date_desc, true),
DISTANCE_ASC(R.string.filters_container_sort_by_distance_asc, false),
DISTANCE_DESC(R.string.filters_container_sort_by_distance_desc, true),
}

// Threshold for the status of an event to be considered full or pending
Expand All @@ -61,7 +62,8 @@ class HomeScreenViewModel
constructor(
private val repository: Repository,
private val authenticationService: AuthenticationService,
private val networkService: NetworkService
private val networkService: NetworkService,
private val gpsService: GPSService,
) : ViewModel() {
// Get the current user id
val userId = authenticationService.getCurrentUserID()
Expand All @@ -87,22 +89,6 @@ constructor(
// Flow to observe if the user can modify the event
private val _canUserModifyEvent = MutableStateFlow(false)
val canUserModifyEvent = _canUserModifyEvent.asStateFlow()
// Flow to observe the filters container
private val _filtersContainer =
MutableStateFlow(
FiltersContainer(
searchEntry = "",
epflChecked = false,
sectionChecked = false,
classChecked = false,
pendingChecked = false,
confirmedChecked = false,
fullChecked = false,
from = 0f,
to = 14f,
sortBy = SortBy.DATE_ASC
)
)
private val defaultFiltersContainer =
FiltersContainer(
searchEntry = "",
Expand All @@ -116,6 +102,8 @@ constructor(
to = 14f,
sortBy = SortBy.DATE_ASC
)
// Flow to observe the filters container
private val _filtersContainer = MutableStateFlow(defaultFiltersContainer)
val filtersContainer = _filtersContainer.asStateFlow()
// Flow to observe the profile name
private val _profileName = MutableStateFlow("")
Expand Down Expand Up @@ -480,21 +468,25 @@ constructor(
event.organizer?.name ==
_followedAssociations.value[_selectedAssociation.value]
}
.sortedBy { event ->
event.startDate
// when we can sort by distance, update this
/*when (_filtersContainer.value.sortBy) {
SortBy.DATE_ASC -> event.startDate
SortBy.DATE_DESC -> event.startDate
else ->
}*/
// Always sort by date, and sort by distance afterwards only if the user
// requests a sort by distance
.sortedBy { it.startDate }
.let {
userLocationStateFlow.value?.let { location ->
when (_filtersContainer.value.sortBy) {
SortBy.DISTANCE_ASC,
SortBy.DISTANCE_DESC ->
it.sortedBy { event ->
event.location.toLatLng().distanceTo(location)
}
else -> it
}
} ?: it
}
.toList()

// reverse the list if the sort by is descending
if (
_filtersContainer.value.sortBy == SortBy.DATE_DESC
) { // when we can filter by distance, update this too
if (_filtersContainer.value.sortBy.descending) {
_displayEventList.value = _displayEventList.value.reversed()
}
}
Expand Down Expand Up @@ -540,6 +532,7 @@ constructor(

/** Switch between map and list mode. */
fun switchMode() {
refreshLocation()
_mode.value =
if (_mode.value == MapOrListMode.MAP) MapOrListMode.LIST else MapOrListMode.MAP
}
Expand All @@ -562,4 +555,10 @@ constructor(
filterEvents()
}
}

val userLocationStateFlow = gpsService.userLocation

private fun refreshLocation() {
gpsService.refreshUserLocation()
}
}
22 changes: 22 additions & 0 deletions app/src/test/java/com/github/swent/echo/di/FakeGPSServiceModule.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.github.swent.echo.di

import com.github.swent.echo.connectivity.GPSService
import com.github.swent.echo.connectivity.SimpleGPSService
import dagger.Module
import dagger.Provides
import dagger.hilt.components.SingletonComponent
import dagger.hilt.testing.TestInstallIn
import javax.inject.Singleton

@Module
@TestInstallIn(
components = [SingletonComponent::class],
replaces = [GPSServiceModule::class],
)
object FakeGPSServiceModule {
@Singleton
@Provides
fun provideGPSService(): GPSService {
return SimpleGPSService()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.github.swent.echo.viewmodels
import android.graphics.BitmapFactory
import com.github.swent.echo.compose.map.MAP_CENTER
import com.github.swent.echo.connectivity.NetworkService
import com.github.swent.echo.connectivity.SimpleGPSService
import com.github.swent.echo.data.model.AssociationHeader
import com.github.swent.echo.data.model.Event
import com.github.swent.echo.data.model.EventCreator
Expand Down Expand Up @@ -34,6 +35,7 @@ class HomeScreenViewModelTest {

private val fakeAuthenticationService = FakeAuthenticationService()
private val mockedRepository = mockk<Repository>(relaxed = true)
private val fakeGPSService = SimpleGPSService()
private lateinit var homeScreenViewModel: HomeScreenViewModel
private val scheduler = TestCoroutineScheduler()
private val eventList =
Expand Down Expand Up @@ -84,7 +86,8 @@ class HomeScreenViewModelTest {
HomeScreenViewModel(
mockedRepository,
fakeAuthenticationService,
mockedNetworkService
mockedNetworkService,
fakeGPSService,
)
}
scheduler.runCurrent()
Expand Down