From 17e7801b31255d500ec5f9d6789f29205b395950 Mon Sep 17 00:00:00 2001 From: Karan Sharma <55722391+ksharma-xyz@users.noreply.github.com> Date: Thu, 13 Feb 2025 09:53:31 +1100 Subject: [PATCH] UI: Add transport mode filtering to trip planner (#609) ### TL;DR Added transport mode filtering functionality to the trip planner ### What changed? - Added support for excluding specific transport modes (train, bus, ferry, etc.) when planning trips - Implemented mode selection UI with toggle functionality - Added parameter handling for excluded transport modes in the API service - Updated the TimeTableViewModel to handle mode selection changes and trigger API requests ### How to test? 1. Navigate to the trip planner screen 2. Plan a journey between two locations 3. Use the transport mode filters to exclude specific modes (e.g., trains or buses) 4. Verify that the journey results update to exclude the selected transport modes 5. Toggle different combinations of transport modes and confirm the results update accordingly ### Why make this change? To provide users with more control over their journey planning by allowing them to exclude transport modes they don't want to use. This is particularly useful for users who have preferences for specific types of transport or want to avoid certain modes of travel. --- .../test/fakes/FakeTripPlanningService.kt | 1 + .../api/service/RealTripPlanningService.kt | 37 ++++++++++++++++++- .../api/service/TripPlanningService.kt | 7 ++++ .../ui/state/timetable/TimeTableUiEvent.kt | 2 +- .../ui/timetable/TimeTableDestination.kt | 4 +- .../planner/ui/timetable/TimeTableScreen.kt | 4 +- .../ui/timetable/TimeTableViewModel.kt | 25 +++++++++++-- 7 files changed, 72 insertions(+), 8 deletions(-) diff --git a/core/test/src/commonTest/kotlin/xyz/ksharma/core/test/fakes/FakeTripPlanningService.kt b/core/test/src/commonTest/kotlin/xyz/ksharma/core/test/fakes/FakeTripPlanningService.kt index f2a427a3..21a85b2a 100644 --- a/core/test/src/commonTest/kotlin/xyz/ksharma/core/test/fakes/FakeTripPlanningService.kt +++ b/core/test/src/commonTest/kotlin/xyz/ksharma/core/test/fakes/FakeTripPlanningService.kt @@ -16,6 +16,7 @@ class FakeTripPlanningService : TripPlanningService { depArr: DepArr, date: String?, time: String?, + excludeProductClassSet: Set, ): TripResponse { return if (isSuccess) FakeTripResponseBuilder.buildTripResponse() else throw IllegalStateException("Failed to fetch trip") diff --git a/feature/trip-planner/network/src/commonMain/kotlin/xyz/ksharma/krail/trip/planner/network/api/service/RealTripPlanningService.kt b/feature/trip-planner/network/src/commonMain/kotlin/xyz/ksharma/krail/trip/planner/network/api/service/RealTripPlanningService.kt index 19dcaa71..0ce394ee 100644 --- a/feature/trip-planner/network/src/commonMain/kotlin/xyz/ksharma/krail/trip/planner/network/api/service/RealTripPlanningService.kt +++ b/feature/trip-planner/network/src/commonMain/kotlin/xyz/ksharma/krail/trip/planner/network/api/service/RealTripPlanningService.kt @@ -3,14 +3,15 @@ package xyz.ksharma.krail.trip.planner.network.api.service import io.ktor.client.HttpClient import io.ktor.client.call.body import io.ktor.client.request.get +import io.ktor.http.ParametersBuilder import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.withContext -import xyz.ksharma.krail.core.di.DispatchersComponent import xyz.ksharma.krail.trip.planner.network.api.model.StopFinderResponse import xyz.ksharma.krail.trip.planner.network.api.model.StopType import xyz.ksharma.krail.trip.planner.network.api.model.TripResponse import xyz.ksharma.krail.trip.planner.network.api.service.stop_finder.StopFinderRequestParams import xyz.ksharma.krail.trip.planner.network.api.service.trip.TripRequestParams +import kotlin.math.log class RealTripPlanningService( private val httpClient: HttpClient, @@ -23,6 +24,7 @@ class RealTripPlanningService( depArr: DepArr, date: String?, time: String?, + excludeProductClassSet: Set, ): TripResponse = withContext(ioDispatcher) { httpClient.get("$NSW_TRANSPORT_BASE_URL/v1/tp/trip") { @@ -45,10 +47,43 @@ class RealTripPlanningService( parameters.append(TripRequestParams.cycleSpeed, "16") parameters.append(TripRequestParams.useElevationData, "1") parameters.append(TripRequestParams.outputFormat, "rapidJSON") + + addExcludedTransportModes( + excludeProductClassSet = excludeProductClassSet, + parameters = parameters, + ) } }.body() } + private inline fun addExcludedTransportModes( + excludeProductClassSet: Set, + parameters: ParametersBuilder, + ) { + println("Exclude - $excludeProductClassSet") + parameters.append(TripRequestParams.excludedMeans, "checkbox") + + if (excludeProductClassSet.contains(1)) { + parameters.append(TripRequestParams.exclMOT1, "1") + } + if (excludeProductClassSet.contains(2)) { + parameters.append(TripRequestParams.exclMOT2, "2") + } + if (excludeProductClassSet.contains(4)) { + parameters.append(TripRequestParams.exclMOT4, "4") + } + if (excludeProductClassSet.contains(5) || excludeProductClassSet.contains(11)) { + parameters.append(TripRequestParams.exclMOT5, "5") + parameters.append(TripRequestParams.exclMOT11, "11") + } + if (excludeProductClassSet.contains(7)) { + parameters.append(TripRequestParams.exclMOT7, "7") + } + if (excludeProductClassSet.contains(9)) { + parameters.append(TripRequestParams.exclMOT9, "9") + } + } + override suspend fun stopFinder( stopSearchQuery: String, stopType: StopType, diff --git a/feature/trip-planner/network/src/commonMain/kotlin/xyz/ksharma/krail/trip/planner/network/api/service/TripPlanningService.kt b/feature/trip-planner/network/src/commonMain/kotlin/xyz/ksharma/krail/trip/planner/network/api/service/TripPlanningService.kt index 7a1ad2fb..9399fc5b 100644 --- a/feature/trip-planner/network/src/commonMain/kotlin/xyz/ksharma/krail/trip/planner/network/api/service/TripPlanningService.kt +++ b/feature/trip-planner/network/src/commonMain/kotlin/xyz/ksharma/krail/trip/planner/network/api/service/TripPlanningService.kt @@ -27,6 +27,13 @@ interface TripPlanningService { * E.g. 0830 means 8:30am and 2030 means 8:30pm. */ time: String? = null, + + /** + * List of product classes to exclude from the response. + * Only those journeys will be returned which are different modes of transport to the excluded + * product classes. + */ + excludeProductClassSet: Set, ): TripResponse suspend fun stopFinder( diff --git a/feature/trip-planner/state/src/commonMain/kotlin/xyz/ksharma/krail/trip/planner/ui/state/timetable/TimeTableUiEvent.kt b/feature/trip-planner/state/src/commonMain/kotlin/xyz/ksharma/krail/trip/planner/ui/state/timetable/TimeTableUiEvent.kt index 77f2a0e0..f943887b 100644 --- a/feature/trip-planner/state/src/commonMain/kotlin/xyz/ksharma/krail/trip/planner/ui/state/timetable/TimeTableUiEvent.kt +++ b/feature/trip-planner/state/src/commonMain/kotlin/xyz/ksharma/krail/trip/planner/ui/state/timetable/TimeTableUiEvent.kt @@ -21,5 +21,5 @@ sealed interface TimeTableUiEvent { data class JourneyLegClicked(val expanded: Boolean) : TimeTableUiEvent - data class ModeSelectionChanged(val x: String) : TimeTableUiEvent + data class ModeSelectionChanged(val unselectedModes: Set) : TimeTableUiEvent } diff --git a/feature/trip-planner/ui/src/commonMain/kotlin/xyz/ksharma/krail/trip/planner/ui/timetable/TimeTableDestination.kt b/feature/trip-planner/ui/src/commonMain/kotlin/xyz/ksharma/krail/trip/planner/ui/timetable/TimeTableDestination.kt index 2e71d296..e7d5f1d6 100644 --- a/feature/trip-planner/ui/src/commonMain/kotlin/xyz/ksharma/krail/trip/planner/ui/timetable/TimeTableDestination.kt +++ b/feature/trip-planner/ui/src/commonMain/kotlin/xyz/ksharma/krail/trip/planner/ui/timetable/TimeTableDestination.kt @@ -81,8 +81,8 @@ internal fun NavGraphBuilder.timeTableDestination(navController: NavHostControll viewModel.onEvent(TimeTableUiEvent.JourneyLegClicked(journeyId)) }, onModeSelectionChanged = { unselectedModes -> - log(unselectedModes.toString()) - viewModel.onEvent(TimeTableUiEvent.ModeSelectionChanged("")) + log("onModeSelectionChanged Exclude :$unselectedModes") + viewModel.onEvent(TimeTableUiEvent.ModeSelectionChanged(unselectedModes)) } ) } diff --git a/feature/trip-planner/ui/src/commonMain/kotlin/xyz/ksharma/krail/trip/planner/ui/timetable/TimeTableScreen.kt b/feature/trip-planner/ui/src/commonMain/kotlin/xyz/ksharma/krail/trip/planner/ui/timetable/TimeTableScreen.kt index 949f7be1..b23ad2c1 100644 --- a/feature/trip-planner/ui/src/commonMain/kotlin/xyz/ksharma/krail/trip/planner/ui/timetable/TimeTableScreen.kt +++ b/feature/trip-planner/ui/src/commonMain/kotlin/xyz/ksharma/krail/trip/planner/ui/timetable/TimeTableScreen.kt @@ -47,6 +47,7 @@ import krail.feature.trip_planner.ui.generated.resources.ic_star import krail.feature.trip_planner.ui.generated.resources.ic_check import krail.feature.trip_planner.ui.generated.resources.ic_star_filled import org.jetbrains.compose.resources.painterResource +import xyz.ksharma.krail.core.log.log import xyz.ksharma.krail.taj.LocalThemeColor import xyz.ksharma.krail.taj.components.Button import xyz.ksharma.krail.taj.components.ButtonDefaults @@ -226,10 +227,11 @@ fun TimeTableScreen( onClick = { // Toggle / Set behavior if (unselectedModesProductClass.contains(it.productClass)) { - unselectedModesProductClass.remove(it.productClass) + unselectedModesProductClass.removeAll(listOf(it.productClass)) } else { unselectedModesProductClass.add(it.productClass) } + log("After operation Exclude - : $unselectedModesProductClass") }, ) } diff --git a/feature/trip-planner/ui/src/commonMain/kotlin/xyz/ksharma/krail/trip/planner/ui/timetable/TimeTableViewModel.kt b/feature/trip-planner/ui/src/commonMain/kotlin/xyz/ksharma/krail/trip/planner/ui/timetable/TimeTableViewModel.kt index 9d5a0805..5ed9b869 100644 --- a/feature/trip-planner/ui/src/commonMain/kotlin/xyz/ksharma/krail/trip/planner/ui/timetable/TimeTableViewModel.kt +++ b/feature/trip-planner/ui/src/commonMain/kotlin/xyz/ksharma/krail/trip/planner/ui/timetable/TimeTableViewModel.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow @@ -105,6 +106,7 @@ class TimeTableViewModel( val expandedJourneyId: StateFlow = _expandedJourneyId private var tripInfo: Trip? = null + private val unselectedModes: MutableSet = mutableSetOf() // all are selected by default @VisibleForTesting var dateTimeSelectionItem: DateTimeSelectionItem? = null @@ -149,12 +151,27 @@ class TimeTableViewModel( analytics.track(AnalyticsEvent.JourneyLegClickEvent(expanded = event.expanded)) } - is TimeTableUiEvent.ModeSelectionChanged -> onModeSelectionChanged() + is TimeTableUiEvent.ModeSelectionChanged -> onModeSelectionChanged(event.unselectedModes) } } - private fun onModeSelectionChanged() { + private fun onModeSelectionChanged(unselectedModes: Set) { + if (hasModeSelectionChanged(unselectedModes)) { + this.unselectedModes.clear() + this.unselectedModes.addAll(unselectedModes) + // call api + rateLimiter.triggerEvent() + updateUiState { copy(isLoading = true) } + } else { + // do nothing. + log("Mode selection not changed") + } + } + + private fun hasModeSelectionChanged(unselectedModes: Set): Boolean { + log("hasModeSelectionChanged - OLD: ${this.unselectedModes} NEW: $unselectedModes") + return this.unselectedModes != unselectedModes } private fun onDateTimeSelectionChanged(item: DateTimeSelectionItem?) { @@ -279,7 +296,8 @@ class TimeTableViewModel( JourneyTimeOptions.LEAVE -> DepArr.DEP JourneyTimeOptions.ARRIVE -> DepArr.ARR else -> DepArr.DEP - } + }, + excludeProductClassSet = unselectedModes, ) Result.success(tripResponse) }.getOrElse { error -> @@ -454,6 +472,7 @@ class TimeTableViewModel( companion object { private const val ANR_TIMEOUT = 5000L + @VisibleForTesting val REFRESH_TIME_TEXT_DURATION = 10.seconds private val AUTO_REFRESH_TIME_TABLE_DURATION = 30.seconds