Skip to content

Commit

Permalink
UI: Add transport mode filtering to trip planner (#609)
Browse files Browse the repository at this point in the history
### 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.
  • Loading branch information
ksharma-xyz authored Feb 12, 2025
1 parent c34d0d3 commit 17e7801
Show file tree
Hide file tree
Showing 7 changed files with 72 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class FakeTripPlanningService : TripPlanningService {
depArr: DepArr,
date: String?,
time: String?,
excludeProductClassSet: Set<Int>,
): TripResponse {
return if (isSuccess) FakeTripResponseBuilder.buildTripResponse()
else throw IllegalStateException("Failed to fetch trip")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -23,6 +24,7 @@ class RealTripPlanningService(
depArr: DepArr,
date: String?,
time: String?,
excludeProductClassSet: Set<Int>,
): TripResponse = withContext(ioDispatcher) {

httpClient.get("$NSW_TRANSPORT_BASE_URL/v1/tp/trip") {
Expand All @@ -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<Int>,
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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Int>,
): TripResponse

suspend fun stopFinder(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Int>) : TimeTableUiEvent
}
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")
},
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -105,6 +106,7 @@ class TimeTableViewModel(
val expandedJourneyId: StateFlow<String?> = _expandedJourneyId

private var tripInfo: Trip? = null
private val unselectedModes: MutableSet<Int> = mutableSetOf() // all are selected by default

@VisibleForTesting
var dateTimeSelectionItem: DateTimeSelectionItem? = null
Expand Down Expand Up @@ -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<Int>) {
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<Int>): Boolean {
log("hasModeSelectionChanged - OLD: ${this.unselectedModes} NEW: $unselectedModes")
return this.unselectedModes != unselectedModes
}

private fun onDateTimeSelectionChanged(item: DateTimeSelectionItem?) {
Expand Down Expand Up @@ -279,7 +296,8 @@ class TimeTableViewModel(
JourneyTimeOptions.LEAVE -> DepArr.DEP
JourneyTimeOptions.ARRIVE -> DepArr.ARR
else -> DepArr.DEP
}
},
excludeProductClassSet = unselectedModes,
)
Result.success(tripResponse)
}.getOrElse { error ->
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 17e7801

Please sign in to comment.