From d765fa8314168d54f604386a50ce9b3ff90bbc0b Mon Sep 17 00:00:00 2001 From: nicoti8m Date: Fri, 20 Feb 2026 11:25:54 +0100 Subject: [PATCH 1/7] Add vehicle submodes for operational filters --- .../tir/ModeAndModeOfOperationFilterDto.kt | 52 ++++++++++++++++++- .../remote/trip/RemoteTripDataSourceImpl.kt | 14 ++++- .../model/ModeAndModeOfOperationFilter.kt | 14 ++++- .../ojp/domain/model/OptimisationMethod.kt | 19 +++++++ .../ojp/domain/model/RailSubmode.kt | 27 ++++++++++ 5 files changed, 123 insertions(+), 3 deletions(-) create mode 100644 sdk/src/main/java/ch/opentransportdata/ojp/domain/model/OptimisationMethod.kt create mode 100644 sdk/src/main/java/ch/opentransportdata/ojp/domain/model/RailSubmode.kt diff --git a/sdk/src/main/java/ch/opentransportdata/ojp/data/dto/request/tir/ModeAndModeOfOperationFilterDto.kt b/sdk/src/main/java/ch/opentransportdata/ojp/data/dto/request/tir/ModeAndModeOfOperationFilterDto.kt index 4bd424d..f5b4a7e 100644 --- a/sdk/src/main/java/ch/opentransportdata/ojp/data/dto/request/tir/ModeAndModeOfOperationFilterDto.kt +++ b/sdk/src/main/java/ch/opentransportdata/ojp/data/dto/request/tir/ModeAndModeOfOperationFilterDto.kt @@ -1,6 +1,8 @@ package ch.opentransportdata.ojp.data.dto.request.tir import ch.opentransportdata.ojp.data.dto.OJP_NAME_SPACE +import ch.opentransportdata.ojp.data.dto.SIRI_NAME_SPACE +import ch.opentransportdata.ojp.data.dto.SIRI_PREFIX import ch.opentransportdata.ojp.domain.model.PtMode import kotlinx.serialization.Serializable import nl.adaptivity.xmlutil.serialization.XmlElement @@ -18,5 +20,53 @@ internal data class ModeAndModeOfOperationFilterDto( @XmlElement(true) @XmlSerialName("Exclude", OJP_NAME_SPACE, "") - val exclude: Boolean? = null + val exclude: Boolean? = null, + + @XmlElement(true) + @XmlSerialName("RailSubmode", SIRI_NAME_SPACE, SIRI_PREFIX) + val railSubmode: String? = null, + + @XmlElement(true) + @XmlSerialName("BusSubmode", SIRI_NAME_SPACE, SIRI_PREFIX) + val busSubmode: String? = null, + + @XmlElement(true) + @XmlSerialName("CoachSubmode", SIRI_NAME_SPACE, SIRI_PREFIX) + val coachSubmode: String? = null, + + @XmlElement(true) + @XmlSerialName("MetroSubmode", SIRI_NAME_SPACE, SIRI_PREFIX) + val metroSubmode: String? = null, + + @XmlElement(true) + @XmlSerialName("TramSubmode", SIRI_NAME_SPACE, SIRI_PREFIX) + val tramSubmode: String? = null, + + @XmlElement(true) + @XmlSerialName("TrolleyBusSubmode", SIRI_NAME_SPACE, SIRI_PREFIX) + val trolleyBusSubmode: String? = null, + + @XmlElement(true) + @XmlSerialName("TelecabinSubmode", SIRI_NAME_SPACE, SIRI_PREFIX) + val telecabinSubmode: String? = null, + + @XmlElement(true) + @XmlSerialName("FunicularSubmode", SIRI_NAME_SPACE, SIRI_PREFIX) + val funicularSubmode: String? = null, + + @XmlElement(true) + @XmlSerialName("WaterSubmode", SIRI_NAME_SPACE, SIRI_PREFIX) + val waterSubmode: String? = null, + + @XmlElement(true) + @XmlSerialName("AirSubmode", SIRI_NAME_SPACE, SIRI_PREFIX) + val airSubmode: String? = null, + + @XmlElement(true) + @XmlSerialName("TaxiSubmode", SIRI_NAME_SPACE, SIRI_PREFIX) + val taxiSubmode: String? = null, + + @XmlElement(true) + @XmlSerialName("SelfDriveSubmode", SIRI_NAME_SPACE, SIRI_PREFIX) + val selfDriveSubmode: String? = null ) diff --git a/sdk/src/main/java/ch/opentransportdata/ojp/data/remote/trip/RemoteTripDataSourceImpl.kt b/sdk/src/main/java/ch/opentransportdata/ojp/data/remote/trip/RemoteTripDataSourceImpl.kt index 6593b41..39b1c8f 100644 --- a/sdk/src/main/java/ch/opentransportdata/ojp/data/remote/trip/RemoteTripDataSourceImpl.kt +++ b/sdk/src/main/java/ch/opentransportdata/ojp/data/remote/trip/RemoteTripDataSourceImpl.kt @@ -129,7 +129,19 @@ internal class RemoteTripDataSourceImpl( modeAndModeOfOperationFilter = this.modeAndModeOfOperationFilter?.map { filter -> ModeAndModeOfOperationFilterDto( ptMode = filter.ptMode, - exclude = filter.exclude + exclude = filter.exclude, + railSubmode = filter.railSubmode, + busSubmode = filter.busSubmode, + coachSubmode = filter.coachSubmode, + metroSubmode = filter.metroSubmode, + tramSubmode = filter.tramSubmode, + trolleyBusSubmode = filter.trolleyBusSubmode, + telecabinSubmode = filter.trolleyBusSubmode, + funicularSubmode = filter.funicularSubmode, + waterSubmode = filter.waterSubmode, + airSubmode = filter.airSubmode, + taxiSubmode = filter.taxiSubmode, + selfDriveSubmode = filter.selfDriveSubmode ) }, ) diff --git a/sdk/src/main/java/ch/opentransportdata/ojp/domain/model/ModeAndModeOfOperationFilter.kt b/sdk/src/main/java/ch/opentransportdata/ojp/domain/model/ModeAndModeOfOperationFilter.kt index 8734447..eeb8091 100644 --- a/sdk/src/main/java/ch/opentransportdata/ojp/domain/model/ModeAndModeOfOperationFilter.kt +++ b/sdk/src/main/java/ch/opentransportdata/ojp/domain/model/ModeAndModeOfOperationFilter.kt @@ -9,5 +9,17 @@ import kotlinx.parcelize.Parcelize @Parcelize data class ModeAndModeOfOperationFilter( val ptMode: List, - val exclude: Boolean + val exclude: Boolean, + val railSubmode: String? = null, + val busSubmode: String? = null, + val coachSubmode: String? = null, + val metroSubmode: String? = null, + val tramSubmode: String? = null, + val trolleyBusSubmode: String? = null, + val telecabinSubmode: String? = null, + val funicularSubmode: String? = null, + val waterSubmode: String? = null, + val airSubmode: String? = null, + val taxiSubmode: String? = null, + val selfDriveSubmode: String? = null ) : Parcelable \ No newline at end of file diff --git a/sdk/src/main/java/ch/opentransportdata/ojp/domain/model/OptimisationMethod.kt b/sdk/src/main/java/ch/opentransportdata/ojp/domain/model/OptimisationMethod.kt new file mode 100644 index 0000000..325af17 --- /dev/null +++ b/sdk/src/main/java/ch/opentransportdata/ojp/domain/model/OptimisationMethod.kt @@ -0,0 +1,19 @@ +package ch.opentransportdata.ojp.domain.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Created by Nico Brandenberger on 19.02.2026 + */ +@Serializable +enum class OptimisationMethod() { + @SerialName("minChanges") + MIN_CHANGES +} + +fun OptimisationMethod.serializedName(): String = + this::class.java.getField(this.name) + .getAnnotation(SerialName::class.java) + ?.value + ?: this.name \ No newline at end of file diff --git a/sdk/src/main/java/ch/opentransportdata/ojp/domain/model/RailSubmode.kt b/sdk/src/main/java/ch/opentransportdata/ojp/domain/model/RailSubmode.kt new file mode 100644 index 0000000..a0fb475 --- /dev/null +++ b/sdk/src/main/java/ch/opentransportdata/ojp/domain/model/RailSubmode.kt @@ -0,0 +1,27 @@ +package ch.opentransportdata.ojp.domain.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Created by Nico Brandenberger on 19.02.2026 + */ +@Serializable +enum class RailSubmode() { + @SerialName("international") //Trains ICE, TGV, EC, RJX, NJ, EN + INTERNATIONAL, + @SerialName("highSpeedRail") //Trains IC + HIGH_SPEED_RAIL, + @SerialName("interregionalRail") //Trains IR, IRN, IRE + INTERREGIONAL_RAIL, + @SerialName("railShuttle") // Trains ATZ, PE + RAIL_SHUTTLE, + @SerialName("local") //Trains S, SN, RB, RE, + LOCAL, +} + +fun RailSubmode.serializedName(): String = + this::class.java.getField(this.name) + .getAnnotation(SerialName::class.java) + ?.value + ?: this.name \ No newline at end of file From 7002b064a2f33531e60d6050674fb0f21b3da5ab Mon Sep 17 00:00:00 2001 From: nicoti8m Date: Fri, 20 Feb 2026 11:29:59 +0100 Subject: [PATCH 2/7] Extend TripParams to support walk speed, transfer limit, bike transport and optimisation method --- .../ojp/data/dto/request/tir/TripParamsDto.kt | 18 +++++++++++++++++- .../remote/trip/RemoteTripDataSourceImpl.kt | 4 ++++ .../ojp/domain/model/TripParams.kt | 6 +++++- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/sdk/src/main/java/ch/opentransportdata/ojp/data/dto/request/tir/TripParamsDto.kt b/sdk/src/main/java/ch/opentransportdata/ojp/data/dto/request/tir/TripParamsDto.kt index e72a053..9e8fda7 100644 --- a/sdk/src/main/java/ch/opentransportdata/ojp/data/dto/request/tir/TripParamsDto.kt +++ b/sdk/src/main/java/ch/opentransportdata/ojp/data/dto/request/tir/TripParamsDto.kt @@ -51,5 +51,21 @@ internal data class TripParamsDto( @XmlElement(true) @XmlSerialName("ModeAndModeOfOperationFilter", OJP_NAME_SPACE, "") - val modeAndModeOfOperationFilter: List? = null + val modeAndModeOfOperationFilter: List? = null, + + @XmlElement(true) + @XmlSerialName("WalkSpeed", OJP_NAME_SPACE, "") + val walkSpeed: Int?, + + @XmlElement(true) + @XmlSerialName("TransferLimit", OJP_NAME_SPACE, "") + val transferLimit: Int?, + + @XmlElement(true) + @XmlSerialName("OptimisationMethod", OJP_NAME_SPACE, "") + val optimisationMethod: String?, + + @XmlElement(true) + @XmlSerialName("BikeTransport", OJP_NAME_SPACE, "") + val bikeTransport: Boolean?, ) \ No newline at end of file diff --git a/sdk/src/main/java/ch/opentransportdata/ojp/data/remote/trip/RemoteTripDataSourceImpl.kt b/sdk/src/main/java/ch/opentransportdata/ojp/data/remote/trip/RemoteTripDataSourceImpl.kt index 39b1c8f..675e03d 100644 --- a/sdk/src/main/java/ch/opentransportdata/ojp/data/remote/trip/RemoteTripDataSourceImpl.kt +++ b/sdk/src/main/java/ch/opentransportdata/ojp/data/remote/trip/RemoteTripDataSourceImpl.kt @@ -144,6 +144,10 @@ internal class RemoteTripDataSourceImpl( selfDriveSubmode = filter.selfDriveSubmode ) }, + walkSpeed = this.walkSpeed, + transferLimit = this.transferLimit, + optimisationMethod = this.optimisationMethod, + bikeTransport = this.bikeTransport, ) } diff --git a/sdk/src/main/java/ch/opentransportdata/ojp/domain/model/TripParams.kt b/sdk/src/main/java/ch/opentransportdata/ojp/domain/model/TripParams.kt index f0525fa..e0801f5 100644 --- a/sdk/src/main/java/ch/opentransportdata/ojp/domain/model/TripParams.kt +++ b/sdk/src/main/java/ch/opentransportdata/ojp/domain/model/TripParams.kt @@ -21,5 +21,9 @@ data class TripParams( val includeIntermediateStops: Boolean = false, val includeAllRestrictedLines: Boolean = false, val useRealtimeData: RealtimeData? = null, - val modeAndModeOfOperationFilter: List? + val modeAndModeOfOperationFilter: List?, + val walkSpeed: Int?, + val transferLimit: Int?, + val optimisationMethod: String?, + val bikeTransport: Boolean?, ) : Parcelable \ No newline at end of file From 06f81927cc1444586bb6eb7e5c85c9f4fada0de7 Mon Sep 17 00:00:00 2001 From: nicoti8m Date: Fri, 20 Feb 2026 11:33:05 +0100 Subject: [PATCH 3/7] Add new individual transport option for trip requests --- .../java/ch/opentransportdata/ojp/OjpSdk.kt | 10 +++-- .../tir/IndividualTransportOptionDto.kt | 37 +++++++++++++++++++ .../tir/ItModeAndModeOfOperationDto.kt | 21 +++++++++++ .../data/dto/request/tir/TripRequestDto.kt | 6 ++- .../data/remote/trip/RemoteTripDataSource.kt | 4 +- .../remote/trip/RemoteTripDataSourceImpl.kt | 7 +++- .../ojp/data/repository/OjpRepositoryImpl.kt | 7 +++- .../ojp/domain/repository/OjpRepository.kt | 4 +- .../ojp/domain/usecase/RequestTrips.kt | 12 ++++-- .../ojp/domain/usecase/UpdateTrip.kt | 3 ++ 10 files changed, 98 insertions(+), 13 deletions(-) create mode 100644 sdk/src/main/java/ch/opentransportdata/ojp/data/dto/request/tir/IndividualTransportOptionDto.kt create mode 100644 sdk/src/main/java/ch/opentransportdata/ojp/data/dto/request/tir/ItModeAndModeOfOperationDto.kt diff --git a/sdk/src/main/java/ch/opentransportdata/ojp/OjpSdk.kt b/sdk/src/main/java/ch/opentransportdata/ojp/OjpSdk.kt index 1a49ccc..9ad0df0 100644 --- a/sdk/src/main/java/ch/opentransportdata/ojp/OjpSdk.kt +++ b/sdk/src/main/java/ch/opentransportdata/ojp/OjpSdk.kt @@ -1,5 +1,6 @@ package ch.opentransportdata.ojp +import ch.opentransportdata.ojp.data.dto.request.tir.IndividualTransportOptionDto import ch.opentransportdata.ojp.data.dto.request.tir.PlaceReferenceDto import ch.opentransportdata.ojp.data.dto.response.PlaceResultDto import ch.opentransportdata.ojp.data.dto.response.delivery.TripDeliveryDto @@ -101,7 +102,8 @@ class OjpSdk( via: PlaceReferenceDto? = null, time: LocalDateTime, isSearchForDepartureTime: Boolean = true, - params: TripParams? + params: TripParams?, + individualTransportOption: IndividualTransportOptionDto? = null ): Result { OjpKoinContext.koinApp.koin.get().reset() return OjpKoinContext.koinApp.koin.get().invoke( @@ -111,7 +113,8 @@ class OjpSdk( via = via, time = time, isSearchForDepartureTime = isSearchForDepartureTime, - params = params + params = params, + individualTransportOption = individualTransportOption ) } @@ -156,8 +159,9 @@ class OjpSdk( via: PlaceReferenceDto?, params: TripParams?, trip: TripDto, + individualTransportOption: IndividualTransportOptionDto? ): Result { - return OjpKoinContext.koinApp.koin.get().invoke(languageCode, origin, destination, via, params, trip) + return OjpKoinContext.koinApp.koin.get().invoke(languageCode, origin, destination, via, params, trip, individualTransportOption) } /** diff --git a/sdk/src/main/java/ch/opentransportdata/ojp/data/dto/request/tir/IndividualTransportOptionDto.kt b/sdk/src/main/java/ch/opentransportdata/ojp/data/dto/request/tir/IndividualTransportOptionDto.kt new file mode 100644 index 0000000..6ddd1e5 --- /dev/null +++ b/sdk/src/main/java/ch/opentransportdata/ojp/data/dto/request/tir/IndividualTransportOptionDto.kt @@ -0,0 +1,37 @@ +package ch.opentransportdata.ojp.data.dto.request.tir + +import ch.opentransportdata.ojp.data.dto.OJP_NAME_SPACE +import ch.opentransportdata.ojp.data.dto.converter.DurationSerializer +import kotlinx.serialization.Serializable +import nl.adaptivity.xmlutil.serialization.XmlElement +import nl.adaptivity.xmlutil.serialization.XmlSerialName +import java.time.Duration + +/** + * Created by Nico Brandenberger on 20.02.2026 + */ +@Serializable +@XmlSerialName("IndividualTransportOption", OJP_NAME_SPACE, "") +data class IndividualTransportOptionDto( + @XmlElement(true) + @XmlSerialName("ItModeAndModeOfOperation", OJP_NAME_SPACE, "") + val itModeAndModeOfOperation: ItModeAndModeOfOperationDto? = null, + + @XmlElement(true) + @XmlSerialName("MinDistance", OJP_NAME_SPACE, "") + val minDistance: Int? = null, + + @XmlElement(true) + @XmlSerialName("MaxDistance", OJP_NAME_SPACE, "") + val maxDistance: Int? = null, + + @XmlElement(true) + @XmlSerialName("MinDuration", OJP_NAME_SPACE, "") + @Serializable(with = DurationSerializer::class) + val minDuration: Duration? = null, + + @XmlElement(true) + @XmlSerialName("MaxDuration", OJP_NAME_SPACE, "") + @Serializable(with = DurationSerializer::class) + val maxDuration: Duration? = null, +) diff --git a/sdk/src/main/java/ch/opentransportdata/ojp/data/dto/request/tir/ItModeAndModeOfOperationDto.kt b/sdk/src/main/java/ch/opentransportdata/ojp/data/dto/request/tir/ItModeAndModeOfOperationDto.kt new file mode 100644 index 0000000..7bcd8f6 --- /dev/null +++ b/sdk/src/main/java/ch/opentransportdata/ojp/data/dto/request/tir/ItModeAndModeOfOperationDto.kt @@ -0,0 +1,21 @@ +package ch.opentransportdata.ojp.data.dto.request.tir + +import ch.opentransportdata.ojp.data.dto.OJP_NAME_SPACE +import kotlinx.serialization.Serializable +import nl.adaptivity.xmlutil.serialization.XmlElement +import nl.adaptivity.xmlutil.serialization.XmlSerialName + +/** + * Created by Nico Brandenberger on 20.02.2026 + */ +@Serializable +@XmlSerialName("ItModeAndModeOfOperation", OJP_NAME_SPACE, "") +data class ItModeAndModeOfOperationDto( + @XmlElement(true) + @XmlSerialName("PersonalMode", OJP_NAME_SPACE, "") + val personalMode: String?, + + @XmlElement(true) + @XmlSerialName("PersonalModeOfOperation", OJP_NAME_SPACE, "") + val personalModeOfOperation: String?, +) diff --git a/sdk/src/main/java/ch/opentransportdata/ojp/data/dto/request/tir/TripRequestDto.kt b/sdk/src/main/java/ch/opentransportdata/ojp/data/dto/request/tir/TripRequestDto.kt index ed787f7..ff25704 100644 --- a/sdk/src/main/java/ch/opentransportdata/ojp/data/dto/request/tir/TripRequestDto.kt +++ b/sdk/src/main/java/ch/opentransportdata/ojp/data/dto/request/tir/TripRequestDto.kt @@ -34,5 +34,9 @@ internal data class TripRequestDto( @XmlElement(true) @XmlSerialName("Params", OJP_NAME_SPACE, "") - val params: TripParamsDto? = null + val params: TripParamsDto? = null, + + @XmlElement(true) + @XmlSerialName("IndividualTransportOption", OJP_NAME_SPACE, "") + val individualTransportOption: IndividualTransportOptionDto?, ) \ No newline at end of file diff --git a/sdk/src/main/java/ch/opentransportdata/ojp/data/remote/trip/RemoteTripDataSource.kt b/sdk/src/main/java/ch/opentransportdata/ojp/data/remote/trip/RemoteTripDataSource.kt index 0e2768b..d7cc9dc 100644 --- a/sdk/src/main/java/ch/opentransportdata/ojp/data/remote/trip/RemoteTripDataSource.kt +++ b/sdk/src/main/java/ch/opentransportdata/ojp/data/remote/trip/RemoteTripDataSource.kt @@ -1,6 +1,7 @@ package ch.opentransportdata.ojp.data.remote.trip import ch.opentransportdata.ojp.data.dto.OjpDto +import ch.opentransportdata.ojp.data.dto.request.tir.IndividualTransportOptionDto import ch.opentransportdata.ojp.data.dto.request.tir.PlaceReferenceDto import ch.opentransportdata.ojp.data.dto.response.tir.TripResultDto import ch.opentransportdata.ojp.domain.model.LanguageCode @@ -19,7 +20,8 @@ internal interface RemoteTripDataSource { via: PlaceReferenceDto? = null, time: LocalDateTime, isSearchForDepartureTime: Boolean, - params: TripParams? + params: TripParams?, + individualTransportOption: IndividualTransportOptionDto? ): OjpDto suspend fun requestTripRefinement( diff --git a/sdk/src/main/java/ch/opentransportdata/ojp/data/remote/trip/RemoteTripDataSourceImpl.kt b/sdk/src/main/java/ch/opentransportdata/ojp/data/remote/trip/RemoteTripDataSourceImpl.kt index 675e03d..5e3e94d 100644 --- a/sdk/src/main/java/ch/opentransportdata/ojp/data/remote/trip/RemoteTripDataSourceImpl.kt +++ b/sdk/src/main/java/ch/opentransportdata/ojp/data/remote/trip/RemoteTripDataSourceImpl.kt @@ -4,6 +4,7 @@ import ch.opentransportdata.ojp.data.dto.OjpDto import ch.opentransportdata.ojp.data.dto.request.OjpRequestDto import ch.opentransportdata.ojp.data.dto.request.ServiceRequestContextDto import ch.opentransportdata.ojp.data.dto.request.ServiceRequestDto +import ch.opentransportdata.ojp.data.dto.request.tir.IndividualTransportOptionDto import ch.opentransportdata.ojp.data.dto.request.tir.ModeAndModeOfOperationFilterDto import ch.opentransportdata.ojp.data.dto.request.tir.PlaceContextDto import ch.opentransportdata.ojp.data.dto.request.tir.PlaceReferenceDto @@ -43,7 +44,8 @@ internal class RemoteTripDataSourceImpl( via: PlaceReferenceDto?, time: LocalDateTime, isSearchForDepartureTime: Boolean, - params: TripParams? + params: TripParams?, + individualTransportOption: IndividualTransportOptionDto? ): OjpDto = withContext(Dispatchers.IO) { val requestTime = LocalDateTime.now() val originPlace = PlaceContextDto( @@ -68,7 +70,8 @@ internal class RemoteTripDataSourceImpl( origin = originPlace, destination = destinationPlace, via = vias ?: emptyList(), - params = params?.mapToBackendParams() + params = params?.mapToBackendParams(), + individualTransportOption = individualTransportOption ) ) diff --git a/sdk/src/main/java/ch/opentransportdata/ojp/data/repository/OjpRepositoryImpl.kt b/sdk/src/main/java/ch/opentransportdata/ojp/data/repository/OjpRepositoryImpl.kt index 26e5c12..3ec2b55 100644 --- a/sdk/src/main/java/ch/opentransportdata/ojp/data/repository/OjpRepositoryImpl.kt +++ b/sdk/src/main/java/ch/opentransportdata/ojp/data/repository/OjpRepositoryImpl.kt @@ -1,5 +1,6 @@ package ch.opentransportdata.ojp.data.repository +import ch.opentransportdata.ojp.data.dto.request.tir.IndividualTransportOptionDto import ch.opentransportdata.ojp.data.dto.request.tir.PlaceReferenceDto import ch.opentransportdata.ojp.data.dto.response.PlaceResultDto import ch.opentransportdata.ojp.data.dto.response.delivery.LocationInformationDeliveryDto @@ -73,7 +74,8 @@ internal class OjpRepositoryImpl( via: PlaceReferenceDto?, time: LocalDateTime, isSearchForDepartureTime: Boolean, - params: TripParams? + params: TripParams?, + individualTransportOption: IndividualTransportOptionDto? ): Result { return try { val response = tripDataSource.requestTrips( @@ -83,7 +85,8 @@ internal class OjpRepositoryImpl( via = via, time = time, isSearchForDepartureTime = isSearchForDepartureTime, - params = params + params = params, + individualTransportOption = individualTransportOption ) val delivery = response.ojpResponse?.serviceDelivery?.ojpDelivery as? TripDeliveryDto if (delivery != null) Result.Success(delivery) else Result.Error(OjpError.Unknown(Exception("Trip delivery is null"))) diff --git a/sdk/src/main/java/ch/opentransportdata/ojp/domain/repository/OjpRepository.kt b/sdk/src/main/java/ch/opentransportdata/ojp/domain/repository/OjpRepository.kt index 13032c1..2f81179 100644 --- a/sdk/src/main/java/ch/opentransportdata/ojp/domain/repository/OjpRepository.kt +++ b/sdk/src/main/java/ch/opentransportdata/ojp/domain/repository/OjpRepository.kt @@ -1,5 +1,6 @@ package ch.opentransportdata.ojp.domain.repository +import ch.opentransportdata.ojp.data.dto.request.tir.IndividualTransportOptionDto import ch.opentransportdata.ojp.data.dto.request.tir.PlaceReferenceDto import ch.opentransportdata.ojp.data.dto.response.PlaceResultDto import ch.opentransportdata.ojp.data.dto.response.delivery.TripDeliveryDto @@ -34,7 +35,8 @@ internal interface OjpRepository { via: PlaceReferenceDto? = null, time: LocalDateTime, isSearchForDepartureTime: Boolean, - params: TripParams? + params: TripParams?, + individualTransportOption: IndividualTransportOptionDto?, ): Result suspend fun requestMockTrips( diff --git a/sdk/src/main/java/ch/opentransportdata/ojp/domain/usecase/RequestTrips.kt b/sdk/src/main/java/ch/opentransportdata/ojp/domain/usecase/RequestTrips.kt index 2da18ba..2672fed 100644 --- a/sdk/src/main/java/ch/opentransportdata/ojp/domain/usecase/RequestTrips.kt +++ b/sdk/src/main/java/ch/opentransportdata/ojp/domain/usecase/RequestTrips.kt @@ -1,5 +1,6 @@ package ch.opentransportdata.ojp.domain.usecase +import ch.opentransportdata.ojp.data.dto.request.tir.IndividualTransportOptionDto import ch.opentransportdata.ojp.data.dto.request.tir.PlaceReferenceDto import ch.opentransportdata.ojp.data.dto.response.delivery.TripDeliveryDto import ch.opentransportdata.ojp.data.dto.response.tir.TripResultDto @@ -30,7 +31,8 @@ internal class RequestTrips( via: PlaceReferenceDto? = null, time: LocalDateTime, isSearchForDepartureTime: Boolean, - params: TripParams? + params: TripParams?, + individualTransportOption: IndividualTransportOptionDto? ): Result { // do not return or overwrite state, if user canceled the request (long running task or something) if (!coroutineContext.isActive) return Result.Error(OjpError.RequestCancelled(CancellationException())) @@ -53,7 +55,8 @@ internal class RequestTrips( via = via, time = time, isSearchForDepartureTime = isSearchForDepartureTime, - params = params + params = params, + individualTransportOption = individualTransportOption ) ) { is Result.Success -> { @@ -81,6 +84,7 @@ internal class RequestTrips( numberOfResultsAfter = null, numberOfResults = null ), + individualTransportOption = state.individualTransportOption!! ) } @@ -99,6 +103,7 @@ internal class RequestTrips( numberOfResultsAfter = numberOfResultsAfter, numberOfResults = null ), + individualTransportOption = state.individualTransportOption!! ) } @@ -140,6 +145,7 @@ internal class RequestTrips( val params: TripParams? = null, val minDateTime: LocalDateTime? = null, val maxDateTime: LocalDateTime? = null, - val existingHashes: MutableList = mutableListOf() + val existingHashes: MutableList = mutableListOf(), + val individualTransportOption: IndividualTransportOptionDto? = null ) } \ No newline at end of file diff --git a/sdk/src/main/java/ch/opentransportdata/ojp/domain/usecase/UpdateTrip.kt b/sdk/src/main/java/ch/opentransportdata/ojp/domain/usecase/UpdateTrip.kt index e52343c..018e73e 100644 --- a/sdk/src/main/java/ch/opentransportdata/ojp/domain/usecase/UpdateTrip.kt +++ b/sdk/src/main/java/ch/opentransportdata/ojp/domain/usecase/UpdateTrip.kt @@ -1,5 +1,6 @@ package ch.opentransportdata.ojp.domain.usecase +import ch.opentransportdata.ojp.data.dto.request.tir.IndividualTransportOptionDto import ch.opentransportdata.ojp.data.dto.request.tir.PlaceReferenceDto import ch.opentransportdata.ojp.data.dto.response.delivery.TripDeliveryDto import ch.opentransportdata.ojp.data.dto.response.tir.TripResultDto @@ -24,6 +25,7 @@ internal class UpdateTrip( via: PlaceReferenceDto?, params: TripParams?, trip: TripDto, + individualTransportOption: IndividualTransportOptionDto? ): Result { return when (val response = ojpRepository.requestTrips( @@ -34,6 +36,7 @@ internal class UpdateTrip( time = trip.startTime.minusMinutes(5), isSearchForDepartureTime = true, params = params, + individualTransportOption = individualTransportOption )) { is Result.Success -> { var updatedTrip: TripDto? From 3be460eb667336554b68b40ba7fbc744f07afc14 Mon Sep 17 00:00:00 2001 From: nicoti8m Date: Fri, 20 Feb 2026 11:40:09 +0100 Subject: [PATCH 4/7] Add filter options to the sample app --- .../opentransportdata/domain/VehicleOption.kt | 10 + .../components/TripResultHeader.kt | 34 ++- .../tir/detail/TripDetailScreen.kt | 16 +- .../presentation/tir/filter/FilterScreen.kt | 193 +++++++++++++++ .../tir/result/TripResultScreen.kt | 74 +++++- .../tir/result/TripResultViewModel.kt | 224 +++++++++++++++--- app/src/main/res/drawable/ic_settings.xml | 10 + 7 files changed, 513 insertions(+), 48 deletions(-) create mode 100644 app/src/main/java/ch/opentransportdata/domain/VehicleOption.kt create mode 100644 app/src/main/java/ch/opentransportdata/presentation/tir/filter/FilterScreen.kt create mode 100644 app/src/main/res/drawable/ic_settings.xml diff --git a/app/src/main/java/ch/opentransportdata/domain/VehicleOption.kt b/app/src/main/java/ch/opentransportdata/domain/VehicleOption.kt new file mode 100644 index 0000000..2bcbc89 --- /dev/null +++ b/app/src/main/java/ch/opentransportdata/domain/VehicleOption.kt @@ -0,0 +1,10 @@ +package ch.opentransportdata.domain + +/** + * Created by Nico Brandenberger on 18.02.2026 + */ + +data class VehicleOption( + val vehicleType: String, + val isSelected: Boolean, +) diff --git a/app/src/main/java/ch/opentransportdata/presentation/components/TripResultHeader.kt b/app/src/main/java/ch/opentransportdata/presentation/components/TripResultHeader.kt index 9d22759..43d5015 100644 --- a/app/src/main/java/ch/opentransportdata/presentation/components/TripResultHeader.kt +++ b/app/src/main/java/ch/opentransportdata/presentation/components/TripResultHeader.kt @@ -2,16 +2,24 @@ package ch.opentransportdata.presentation.components import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.SwapCalls -import androidx.compose.material3.* +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedIconButton +import androidx.compose.material3.Surface +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.unit.dp +import ch.opentransportdata.R import ch.opentransportdata.presentation.theme.OJPAndroidSDKTheme /** @@ -22,7 +30,8 @@ fun TripResultHeader( modifier: Modifier = Modifier, originName: String, destinationName: String, - swapSearch: () -> Unit + swapSearch: () -> Unit, + onSettingsClick: () -> Unit, ) { Surface( @@ -44,13 +53,25 @@ fun TripResultHeader( style = MaterialTheme.typography.titleLarge ) } - OutlinedIconButton( + Row( modifier = Modifier .align(Alignment.CenterEnd) .padding(end = 8.dp), - onClick = swapSearch ) { - Icon(imageVector = Icons.Default.SwapCalls, contentDescription = null) + OutlinedIconButton( + modifier = Modifier, + onClick = onSettingsClick, + ) { + Icon( + painter = painterResource(id = R.drawable.ic_settings), + contentDescription = "Filter Option", + ) + } + OutlinedIconButton( + onClick = swapSearch + ) { + Icon(imageVector = Icons.Default.SwapCalls, contentDescription = null) + } } } } @@ -63,7 +84,8 @@ private fun TripResultHeaderPreview() { TripResultHeader( originName = "Bern, Eigerplatz", destinationName = "Basel SBB", - swapSearch = {} + swapSearch = {}, + onSettingsClick = {} ) } } \ No newline at end of file diff --git a/app/src/main/java/ch/opentransportdata/presentation/tir/detail/TripDetailScreen.kt b/app/src/main/java/ch/opentransportdata/presentation/tir/detail/TripDetailScreen.kt index b919896..fac5c3c 100644 --- a/app/src/main/java/ch/opentransportdata/presentation/tir/detail/TripDetailScreen.kt +++ b/app/src/main/java/ch/opentransportdata/presentation/tir/detail/TripDetailScreen.kt @@ -62,6 +62,7 @@ import ch.opentransportdata.presentation.utils.toFormattedString import java.time.Duration import java.time.LocalDateTime import java.time.format.DateTimeFormatter +import kotlin.math.roundToInt /** * Created by Michael Ruppen on 12.07.2024 @@ -75,7 +76,8 @@ fun TripDetailScreen( requestTripUpdate: (TripDto) -> Unit, refineTrip: (String) -> Unit, showMapText: String, - showMap: (String, Boolean) -> Unit + showMap: (String, Boolean) -> Unit, + walkingSpeed: Int, ) { val scrollState = rememberScrollState() val timedLegs = trip.legs.mapNotNull { it.legType as? TimedLegDto } @@ -148,7 +150,7 @@ fun TripDetailScreen( when (val legType = leg.legType) { is TransferLegDto -> { isZoomed = true - TransferLeg(modifier = Modifier.padding(all = 16.dp), leg = legType) + TransferLeg(modifier = Modifier.padding(all = 16.dp),walkingSpeed = walkingSpeed, leg = legType) } is ContinuousLegDto -> { isZoomed = true @@ -468,6 +470,7 @@ private fun ContinuousLeg( @Composable private fun TransferLeg( modifier: Modifier = Modifier, + walkingSpeed: Int, leg: TransferLegDto ) { Row( @@ -486,6 +489,12 @@ private fun TransferLeg( style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurface ) + Text( + modifier = Modifier.padding(start = 4.dp), + text = "Duration ${(leg.duration.toMinutes() / (walkingSpeed / 100.0)).roundToInt()} (min)", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface + ) } } @@ -567,7 +576,8 @@ private fun TripDetailScreenPreview() { requestTripUpdate = {}, refineTrip = {}, showMapText = "Show way on map", - showMap = {_ , _ ->} + showMap = {_ , _ ->}, + walkingSpeed = 100 ) } } \ No newline at end of file diff --git a/app/src/main/java/ch/opentransportdata/presentation/tir/filter/FilterScreen.kt b/app/src/main/java/ch/opentransportdata/presentation/tir/filter/FilterScreen.kt new file mode 100644 index 0000000..4e21ed1 --- /dev/null +++ b/app/src/main/java/ch/opentransportdata/presentation/tir/filter/FilterScreen.kt @@ -0,0 +1,193 @@ +package ch.opentransportdata.presentation.tir.filter + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ListItem +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import ch.opentransportdata.domain.VehicleOption + +@Composable +fun FilterScreen( + currentWalkingSpeed: Int, + onSelectWalkingSpeed: (Int) -> Unit, + isDirectConnection: Boolean, + onCheckDirectConnection: () -> Unit, + isFewerTransfers: Boolean, + onCheckFewerTransfers: () -> Unit, + vehicleOptions: List, + onToggleVehicle: (String) -> Unit, + vehicleSubOptions: List, + onToggleSubVehicle: (String) -> Unit, + minDistance: String, + onMinDistanceChange: (String) -> Unit, + maxDistance: String, + onMaxDistanceChange: (String) -> Unit, + minDuration: String, + onMinDurationChange: (String) -> Unit, + maxDuration: String, + onMaxDurationChange: (String) -> Unit, + isBikeTransport: Boolean, + onCheckBikeTransport: () -> Unit, +) { + val selectedWalkingSpeed = remember { mutableIntStateOf(currentWalkingSpeed) } + val walkingSpeedOptions = listOf(50, 75, 100, 150, 200, 400) + + Column( + modifier = Modifier + .verticalScroll(rememberScrollState()) + .padding(horizontal = 12.dp) + ) { + Text(text = "Deviation from average walking speed in percent. (100% == average)", textAlign = TextAlign.Center) + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) { + walkingSpeedOptions.forEach { walkingSpeed -> + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + RadioButton( + modifier = Modifier.size(40.dp), + selected = selectedWalkingSpeed.intValue == walkingSpeed, + onClick = { + onSelectWalkingSpeed(walkingSpeed) + selectedWalkingSpeed.value = walkingSpeed + }, + ) + Text("${walkingSpeed}%") + } + } + } + OptionItem( + text = "Only direct connections", + isSelected = isDirectConnection, + onClick = { + onCheckDirectConnection() + } + ) + OptionItem( + text = "Fewer transfers", + enabled = !isDirectConnection, + isSelected = isFewerTransfers && !isDirectConnection, + onClick = { + onCheckFewerTransfers() + }, + ) + OptionItem( + text = "Bike Transport", + isSelected = isBikeTransport, + onClick = { + onCheckBikeTransport() + } + ) + Text("Select your travel modes") + vehicleOptions.forEach { option -> + OptionItem( + text = option.vehicleType, + isSelected = option.isSelected, + onClick = { onToggleVehicle(option.vehicleType) } + ) + } + Text("Select your rail sub mode") + vehicleSubOptions.forEach { option -> + OptionItem( + text = option.vehicleType, + isSelected = if (vehicleOptions.first().isSelected) option.isSelected else false, + enabled = vehicleOptions.first().isSelected, + onClick = { onToggleSubVehicle(option.vehicleType) } + ) + } + Text("Select your distance") + Column { + DistanceItem( + text = "Min Distance (meter)", + distance = minDistance, + onValueChange = { onMinDistanceChange(it) } + ) + DistanceItem( + text = "Max Distance (meter)", + distance = maxDistance, + onValueChange = { onMaxDistanceChange(it) } + ) + } + Text("Select your duration") + Column { + DistanceItem( + text = "Min Duration (min)", + distance = minDuration, + onValueChange = { onMinDurationChange(it) } + ) + DistanceItem( + text = "Max Duration (min)", + distance = maxDuration, + onValueChange = { onMaxDurationChange(it) } + ) + } + } +} + +@Composable +private fun OptionItem( + text: String, + isSelected: Boolean, + enabled: Boolean = true, + onClick: () -> Unit, +) { + ListItem( + headlineContent = { + Text( + text = text, + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.onSurface + ) + }, + trailingContent = { + Switch( + checked = isSelected, + onCheckedChange = { onClick() }, + enabled = enabled + ) + } + ) +} + +@Composable +private fun DistanceItem( + text: String, + distance: String, + onValueChange: (String) -> Unit, +) { + ListItem( + headlineContent = { + Text( + text = text, + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.onSurface + ) + }, + trailingContent = { + TextField( + modifier = Modifier.fillMaxWidth(0.5f), + value = distance, + onValueChange = { onValueChange(it) }, + keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number) + ) + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/ch/opentransportdata/presentation/tir/result/TripResultScreen.kt b/app/src/main/java/ch/opentransportdata/presentation/tir/result/TripResultScreen.kt index d107b7d..d62857f 100644 --- a/app/src/main/java/ch/opentransportdata/presentation/tir/result/TripResultScreen.kt +++ b/app/src/main/java/ch/opentransportdata/presentation/tir/result/TripResultScreen.kt @@ -11,8 +11,32 @@ import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.BottomSheetDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemDefaults +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -27,6 +51,7 @@ import ch.opentransportdata.presentation.components.TripResultHeader import ch.opentransportdata.presentation.lir.name import ch.opentransportdata.presentation.navigation.TripMap import ch.opentransportdata.presentation.tir.detail.TripDetailScreen +import ch.opentransportdata.presentation.tir.filter.FilterScreen import ch.opentransportdata.presentation.utils.FileReader import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -51,6 +76,10 @@ fun TripResultScreen( var selectedTrip by remember { mutableStateOf(null) } var selectedAction by remember { mutableStateOf(null) } val showMapText = if (viewModel.state.collectAsState().value.mapData.isEmpty()) "Refine Trip first" else "Show way on map" + var selectFilters = remember { mutableStateOf(false) } + val filterBottomSheet = rememberModalBottomSheetState(skipPartiallyExpanded = true) + + if (selectedTrip != null) { ModalBottomSheet( @@ -80,7 +109,45 @@ fun TripResultScreen( ) ) } + }, + walkingSpeed = viewModel.state.collectAsState().value.walkingSpeed + ) + } + } + if (selectFilters.value) { + ModalBottomSheet( + onDismissRequest = { + coroutineScope.launch { + selectFilters.value = false + viewModel.requestTrips() } + }, + sheetState = filterBottomSheet, + dragHandle = { BottomSheetDefaults.DragHandle() }, + ) { + FilterScreen( + viewModel.state.collectAsState().value.walkingSpeed, + onSelectWalkingSpeed = { + viewModel.updateWalkingSpeed(it) + }, + isDirectConnection = state.value.isDirectConnection, + onCheckDirectConnection = { viewModel.setDirectConnection() }, + isFewerTransfers = state.value.isFewerTransfers, + onCheckFewerTransfers = { viewModel.setFewerTransfers() }, + vehicleOptions = viewModel.state.collectAsState().value.vehicleOptions, + onToggleVehicle = { viewModel.toggleVehicle(it) }, + vehicleSubOptions = viewModel.state.collectAsState().value.vehicleSubOptions, + onToggleSubVehicle = { viewModel.toggleSubVehicle(it) }, + minDuration = viewModel.state.collectAsState().value.minDuration?.toString() ?: "", + maxDuration = viewModel.state.collectAsState().value.maxDuration?.toString() ?: "", + minDistance = viewModel.state.collectAsState().value.minDistance?.toString() ?: "", + maxDistance = viewModel.state.collectAsState().value.maxDistance?.toString() ?: "", + onMinDistanceChange = { viewModel.setMinDistance(it.toIntOrNull()) }, + onMaxDistanceChange = { viewModel.setMaxDistance(it.toIntOrNull()) }, + onMaxDurationChange = { viewModel.setMaxDuration(it.toLongOrNull()) }, + onMinDurationChange = { viewModel.setMinDuration(it.toLongOrNull()) }, + isBikeTransport = viewModel.state.collectAsState().value.isBikeTransport, + onCheckBikeTransport = { viewModel.setBikeTransport() } ) } } @@ -137,7 +204,8 @@ fun TripResultScreen( modifier = Modifier.padding(horizontal = 8.dp), originName = viewModel.origin?.place?.placeType?.name() ?: "-", destinationName = viewModel.destination?.place?.placeType?.name() ?: "-", - swapSearch = { viewModel.swapSearch() } + swapSearch = { viewModel.swapSearch() }, + onSettingsClick = { selectFilters.value = true } ) HorizontalDivider(modifier = Modifier.padding(top = 16.dp)) diff --git a/app/src/main/java/ch/opentransportdata/presentation/tir/result/TripResultViewModel.kt b/app/src/main/java/ch/opentransportdata/presentation/tir/result/TripResultViewModel.kt index de235e8..e530ca5 100644 --- a/app/src/main/java/ch/opentransportdata/presentation/tir/result/TripResultViewModel.kt +++ b/app/src/main/java/ch/opentransportdata/presentation/tir/result/TripResultViewModel.kt @@ -5,6 +5,10 @@ import androidx.compose.runtime.Immutable import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import ch.opentransportdata.domain.MapLibreData +import ch.opentransportdata.domain.VehicleOption +import ch.opentransportdata.ojp.data.dto.request.tir.IndividualTransportOptionDto +import ch.opentransportdata.ojp.data.dto.request.tir.ItModeAndModeOfOperationDto import ch.opentransportdata.ojp.data.dto.request.tir.PlaceReferenceDto import ch.opentransportdata.ojp.data.dto.response.PlaceResultDto import ch.opentransportdata.ojp.data.dto.response.delivery.TripDeliveryDto @@ -14,19 +18,22 @@ import ch.opentransportdata.ojp.data.dto.response.tir.leg.TimedLegDto import ch.opentransportdata.ojp.data.dto.response.tir.leg.TransferLegDto import ch.opentransportdata.ojp.data.dto.response.tir.trips.TripDto import ch.opentransportdata.ojp.domain.model.LanguageCode -import ch.opentransportdata.domain.MapLibreData import ch.opentransportdata.ojp.domain.model.ModeAndModeOfOperationFilter +import ch.opentransportdata.ojp.domain.model.OptimisationMethod import ch.opentransportdata.ojp.domain.model.PtMode +import ch.opentransportdata.ojp.domain.model.RailSubmode import ch.opentransportdata.ojp.domain.model.RealtimeData import ch.opentransportdata.ojp.domain.model.Result import ch.opentransportdata.ojp.domain.model.TripParams import ch.opentransportdata.ojp.domain.model.TripRefineParam +import ch.opentransportdata.ojp.domain.model.serializedName import ch.opentransportdata.presentation.MainActivity import ch.opentransportdata.presentation.utils.toOjpLanguageCode import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import java.io.InputStream +import java.time.Duration import java.time.LocalDateTime import java.util.Locale import java.util.UUID @@ -38,7 +45,29 @@ class TripResultViewModel( private val savedStateHandle: SavedStateHandle ) : ViewModel() { - val state = MutableStateFlow(UiState()) + private val initialVehicleOptions = listOf( + "Train", "Bus", "Tram", "Metro", "Boat", "Telecabin" + ).map { vehicleType -> + VehicleOption(vehicleType = vehicleType, isSelected = true) + } + + private val initialVehicleSubOptions = listOf( + RailSubmode.INTERNATIONAL.serializedName(), + RailSubmode.HIGH_SPEED_RAIL.serializedName(), + RailSubmode.INTERREGIONAL_RAIL.serializedName(), + RailSubmode.RAIL_SHUTTLE.serializedName(), + RailSubmode.LOCAL.serializedName() + ).map { vehicleType -> + VehicleOption(vehicleType = vehicleType, isSelected = false) + } + + + val state = MutableStateFlow( + UiState( + vehicleOptions = initialVehicleOptions, + vehicleSubOptions = initialVehicleSubOptions + ) + ) val origin: PlaceResultDto? get() = savedStateHandle["origin"] @@ -164,13 +193,22 @@ class TripResultViewModel( includeIntermediateStops = true, includeAllRestrictedLines = true, modeAndModeOfOperationFilter = null, - useRealtimeData = RealtimeData.EXPLANATORY + useRealtimeData = RealtimeData.EXPLANATORY, + walkSpeed = state.value.walkingSpeed, + transferLimit = if (state.value.isDirectConnection) 0 else null, + optimisationMethod = if (state.value.isFewerTransfers) OptimisationMethod.MIN_CHANGES.serializedName() else null, + bikeTransport = state.value.isBikeTransport, ), trip = trip, + individualTransportOption = IndividualTransportOptionDto( + minDistance = state.value.minDistance, + maxDistance = state.value.maxDistance, + minDuration = state.value.minDuration?.let { Duration.ofMinutes(it) }, + maxDuration = state.value.maxDuration?.let { Duration.ofMinutes(it) }, + ) )) { is Result.Success -> { Log.d("TripResultViewModel", "Trip update successful") -// state.update { it.copy(tripDelivery = response.data.copy(tripResults = response.data.tripResults)) } } is Result.Error -> { @@ -185,7 +223,7 @@ class TripResultViewModel( state.update { it.copy(events = it.events.filterNot { event -> event.id == id }) } } - private fun requestTrips() { + fun requestTrips() { if (origin == null || destination == null) { Log.e("TripResultViewModel", "Origin or destination are null") return @@ -210,20 +248,48 @@ class TripResultViewModel( position = it.place?.position ) } - //example if you want only water trips when both stations are water - val modeFilter = - if (origin!!.place?.mode?.any { it.ptMode == PtMode.WATER } == true && destination!!.place?.mode?.any { it.ptMode == PtMode.WATER } == true) { - listOf(PtMode.WATER) - } else emptyList() - - val modeAndModeOfOperationFilter = if (modeFilter.isNotEmpty()) { - listOf( - ModeAndModeOfOperationFilter( - ptMode = modeFilter, - exclude = false - ) - ) - } else emptyList() + + val selectedPtModes: List = buildList { + if (state.value.vehicleOptions.first().isSelected) { + add(PtMode.RAIL) + add(PtMode.SUBURBAN_RAIL) + add(PtMode.URBAN_RAIL) + } + if (state.value.vehicleOptions[1].isSelected) add(PtMode.BUS) + if (state.value.vehicleOptions[2].isSelected) add(PtMode.TRAM) + if (state.value.vehicleOptions[3].isSelected) add(PtMode.METRO) + if (state.value.vehicleOptions[4].isSelected) add(PtMode.WATER) + if (state.value.vehicleOptions.last().isSelected) add(PtMode.TELECABIN) + + if (isEmpty()) add(PtMode.ALL) + } + + val selectedPtSubModes: MutableList = + if (PtMode.RAIL in selectedPtModes) { + state.value.vehicleSubOptions + .filter { it.isSelected } + .map { vehicleOption -> + ModeAndModeOfOperationFilter( + exclude = false, + ptMode = emptyList(), + railSubmode = vehicleOption.vehicleType + ) + } + .toMutableList() + } else { + mutableListOf() + } + + val railModes = setOf(PtMode.RAIL, PtMode.SUBURBAN_RAIL, PtMode.URBAN_RAIL) + + val ptModesForMainFilter = + if (selectedPtSubModes.isNotEmpty()) selectedPtModes.filterNot { it in railModes } + else selectedPtModes + + val modeAndModeOfOperationFilter: List = buildList { + add(ModeAndModeOfOperationFilter(ptMode = ptModesForMainFilter, exclude = false)) + addAll(selectedPtSubModes) + } val response = MainActivity.ojpSdk.requestTrips( languageCode = Locale.getDefault().language.toOjpLanguageCode(), @@ -236,7 +302,21 @@ class TripResultViewModel( includeIntermediateStops = true, includeAllRestrictedLines = true, modeAndModeOfOperationFilter = modeAndModeOfOperationFilter, - useRealtimeData = RealtimeData.EXPLANATORY + useRealtimeData = RealtimeData.EXPLANATORY, + walkSpeed = state.value.walkingSpeed, + transferLimit = if (state.value.isDirectConnection) 0 else null, + optimisationMethod = if (state.value.isFewerTransfers) OptimisationMethod.MIN_CHANGES.serializedName() else null, + bikeTransport = state.value.isBikeTransport, + ), + individualTransportOption = IndividualTransportOptionDto( + itModeAndModeOfOperation = ItModeAndModeOfOperationDto( + personalMode = "foot", + personalModeOfOperation = "own" + ), + minDistance = state.value.minDistance, + maxDistance = state.value.maxDistance, + minDuration = state.value.minDuration?.let { Duration.ofMinutes(it) }, + maxDuration = state.value.maxDuration?.let { Duration.ofMinutes(it) }, ) ) when (response) { @@ -284,32 +364,42 @@ class TripResultViewModel( val mapData = mutableListOf() response.data.tripResults?.forEach { result -> result.trip?.legs?.forEach { leg -> - when(leg.legType) { + when (leg.legType) { is ContinuousLegDto -> { - val positions = leg.continuousLeg?.legTrack?.trackSection?.first()?.linkProjection?.positions + val positions = + leg.continuousLeg?.legTrack?.trackSection?.first()?.linkProjection?.positions if (!positions.isNullOrEmpty()) { - mapData.add(MapLibreData( - id = leg.id, - positions = positions - )) + mapData.add( + MapLibreData( + id = leg.id, + positions = positions + ) + ) } } + is TransferLegDto -> { - val positions = leg.transferLeg?.pathGuidance?.pathGuidanceSection?.first()?.trackSection?.first()?.linkProjection?.positions + val positions = + leg.transferLeg?.pathGuidance?.pathGuidanceSection?.first()?.trackSection?.first()?.linkProjection?.positions if (!positions.isNullOrEmpty()) { - mapData.add(MapLibreData( - id = leg.id, - positions = positions - )) + mapData.add( + MapLibreData( + id = leg.id, + positions = positions + ) + ) } } + is TimedLegDto -> { val positions = leg.timedLeg?.legTrack?.trackSection?.first()?.linkProjection?.positions if (!positions.isNullOrEmpty()) { - mapData.add(MapLibreData( - id = leg.id, - positions = positions - )) + mapData.add( + MapLibreData( + id = leg.id, + positions = positions + ) + ) } } } @@ -333,6 +423,58 @@ class TripResultViewModel( state.update { it.copy(mapData = emptyList()) } } + fun updateWalkingSpeed(walkingSpeed: Int) { + state.update { it.copy(walkingSpeed = walkingSpeed) } + } + + fun setDirectConnection() { + state.update { it.copy(isDirectConnection = !state.value.isDirectConnection) } + } + + fun setFewerTransfers() { + state.update { it.copy(isFewerTransfers = !state.value.isFewerTransfers) } + } + + fun toggleVehicle(vehicleType: String) { + state.update { current -> + val updated = current.vehicleOptions.map { option -> + if (option.vehicleType == vehicleType) option.copy(isSelected = !option.isSelected) else option + } + current.copy(vehicleOptions = updated) + } + } + + fun toggleSubVehicle(vehicleType: String) { + state.update { current -> + val updated = current.vehicleSubOptions.map { option -> + if (option.vehicleType == vehicleType) option.copy(isSelected = !option.isSelected) else option + } + current.copy(vehicleSubOptions = updated) + } + } + + fun setMinDistance(minDistance: Int?) { + state.update { it.copy(minDistance = minDistance) } + } + + fun setMaxDistance(maxDistance: Int?) { + state.update { it.copy(maxDistance = maxDistance) } + } + + fun setMinDuration(minDuration: Long?) { + state.update { it.copy(minDuration = minDuration) } + + } + + fun setMaxDuration(maxDuration: Long?) { + state.update { it.copy(maxDuration = maxDuration) } + } + + fun setBikeTransport() { + state.update { it.copy(isBikeTransport = !state.value.isBikeTransport) } + } + + sealed class Event(val id: Long = UUID.randomUUID().mostSignificantBits) { data class ShowSnackBar(val message: String) : Event() data class ScrollToFirstTripItem(val offset: Int) : Event() @@ -345,6 +487,16 @@ class TripResultViewModel( val isLoadingPrevious: Boolean = false, val isLoading: Boolean = false, val isLoadingNext: Boolean = false, - val mapData: List = emptyList() + val mapData: List = emptyList(), + val walkingSpeed: Int = 100, + val isDirectConnection: Boolean = false, + val isFewerTransfers: Boolean = false, + val isBikeTransport: Boolean = false, + val vehicleOptions: List = emptyList(), + val vehicleSubOptions: List = emptyList(), + val minDuration: Long? = null, + val maxDuration: Long? = null, + val minDistance: Int? = null, + val maxDistance: Int? = null, ) } \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_settings.xml b/app/src/main/res/drawable/ic_settings.xml new file mode 100644 index 0000000..5a8beca --- /dev/null +++ b/app/src/main/res/drawable/ic_settings.xml @@ -0,0 +1,10 @@ + + + From f88393ccbf1bd9e7be03e722ced4a8685ca7e7fe Mon Sep 17 00:00:00 2001 From: nicoti8m Date: Fri, 20 Feb 2026 14:15:42 +0100 Subject: [PATCH 5/7] Refactor the individual transport option into place ref --- .../ojp/data/dto/request/tir/PlaceContextDto.kt | 6 +++++- .../ojp/data/dto/request/tir/TripRequestDto.kt | 4 ---- .../ojp/data/remote/trip/RemoteTripDataSourceImpl.kt | 3 ++- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/sdk/src/main/java/ch/opentransportdata/ojp/data/dto/request/tir/PlaceContextDto.kt b/sdk/src/main/java/ch/opentransportdata/ojp/data/dto/request/tir/PlaceContextDto.kt index 1c6a19b..2ff125e 100644 --- a/sdk/src/main/java/ch/opentransportdata/ojp/data/dto/request/tir/PlaceContextDto.kt +++ b/sdk/src/main/java/ch/opentransportdata/ojp/data/dto/request/tir/PlaceContextDto.kt @@ -19,5 +19,9 @@ internal data class PlaceContextDto( @XmlElement(true) @XmlSerialName("DepArrTime", OJP_NAME_SPACE, "") @Contextual - val departureArrivalTime: LocalDateTime? = null + val departureArrivalTime: LocalDateTime? = null, + + @XmlElement(true) + @XmlSerialName("IndividualTransportOption", OJP_NAME_SPACE, "") + val individualTransportOption: IndividualTransportOptionDto?, ) diff --git a/sdk/src/main/java/ch/opentransportdata/ojp/data/dto/request/tir/TripRequestDto.kt b/sdk/src/main/java/ch/opentransportdata/ojp/data/dto/request/tir/TripRequestDto.kt index ff25704..c3d603a 100644 --- a/sdk/src/main/java/ch/opentransportdata/ojp/data/dto/request/tir/TripRequestDto.kt +++ b/sdk/src/main/java/ch/opentransportdata/ojp/data/dto/request/tir/TripRequestDto.kt @@ -35,8 +35,4 @@ internal data class TripRequestDto( @XmlElement(true) @XmlSerialName("Params", OJP_NAME_SPACE, "") val params: TripParamsDto? = null, - - @XmlElement(true) - @XmlSerialName("IndividualTransportOption", OJP_NAME_SPACE, "") - val individualTransportOption: IndividualTransportOptionDto?, ) \ No newline at end of file diff --git a/sdk/src/main/java/ch/opentransportdata/ojp/data/remote/trip/RemoteTripDataSourceImpl.kt b/sdk/src/main/java/ch/opentransportdata/ojp/data/remote/trip/RemoteTripDataSourceImpl.kt index 5e3e94d..b91adf5 100644 --- a/sdk/src/main/java/ch/opentransportdata/ojp/data/remote/trip/RemoteTripDataSourceImpl.kt +++ b/sdk/src/main/java/ch/opentransportdata/ojp/data/remote/trip/RemoteTripDataSourceImpl.kt @@ -50,11 +50,13 @@ internal class RemoteTripDataSourceImpl( val requestTime = LocalDateTime.now() val originPlace = PlaceContextDto( placeReference = origin, + individualTransportOption = individualTransportOption, departureArrivalTime = if (isSearchForDepartureTime) time else null ) val destinationPlace = PlaceContextDto( placeReference = destination, + individualTransportOption = individualTransportOption, departureArrivalTime = if (isSearchForDepartureTime) null else time ) @@ -71,7 +73,6 @@ internal class RemoteTripDataSourceImpl( destination = destinationPlace, via = vias ?: emptyList(), params = params?.mapToBackendParams(), - individualTransportOption = individualTransportOption ) ) From 32881113630cda8d2499641a8870a048a1f1afb6 Mon Sep 17 00:00:00 2001 From: deka91 Date: Fri, 20 Feb 2026 16:04:44 +0100 Subject: [PATCH 6/7] Fix unit test --- .../ojp/domain/usecase/TripRefinementTest.kt | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/sdk/src/test/java/ch/opentransportdata/ojp/domain/usecase/TripRefinementTest.kt b/sdk/src/test/java/ch/opentransportdata/ojp/domain/usecase/TripRefinementTest.kt index a058596..4eb6062 100644 --- a/sdk/src/test/java/ch/opentransportdata/ojp/domain/usecase/TripRefinementTest.kt +++ b/sdk/src/test/java/ch/opentransportdata/ojp/domain/usecase/TripRefinementTest.kt @@ -19,10 +19,12 @@ import ch.opentransportdata.ojp.data.dto.response.tir.TripResultDto import ch.opentransportdata.ojp.data.dto.response.tir.minimalTripResult import ch.opentransportdata.ojp.domain.model.FareClass import ch.opentransportdata.ojp.domain.model.LanguageCode +import ch.opentransportdata.ojp.domain.model.OptimisationMethod import ch.opentransportdata.ojp.domain.model.RealtimeData import ch.opentransportdata.ojp.domain.model.Result import ch.opentransportdata.ojp.domain.model.TripParams import ch.opentransportdata.ojp.domain.model.TripRefineParam +import ch.opentransportdata.ojp.domain.model.serializedName import kotlinx.coroutines.test.runTest import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString @@ -136,7 +138,11 @@ class TripRefinementXmlUtilTest { includeIntermediateStops = true, includeAllRestrictedLines = true, modeAndModeOfOperationFilter = null, - useRealtimeData = RealtimeData.FULL + useRealtimeData = RealtimeData.FULL, + walkSpeed = 100, + transferLimit = 0, + optimisationMethod = OptimisationMethod.MIN_CHANGES.serializedName(), + bikeTransport = false ) val tripRequest = ojpSdk.requestTrips( @@ -152,7 +158,7 @@ class TripRefinementXmlUtilTest { when (tripRequest) { is Result.Success -> { if (!tripRequest.data.tripResults.isNullOrEmpty()) { - tripResult = tripRequest.data.tripResults!!.first() + tripResult = tripRequest.data.tripResults.first() } } From cc15e2e7373d2853b2488c6642ca58dadf57db04 Mon Sep 17 00:00:00 2001 From: deka91 Date: Mon, 23 Feb 2026 11:22:52 +0100 Subject: [PATCH 7/7] Fix NullPointerException in loadPrevious and loadNext --- .../ch/opentransportdata/ojp/domain/usecase/RequestTrips.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/src/main/java/ch/opentransportdata/ojp/domain/usecase/RequestTrips.kt b/sdk/src/main/java/ch/opentransportdata/ojp/domain/usecase/RequestTrips.kt index 2672fed..6d73afc 100644 --- a/sdk/src/main/java/ch/opentransportdata/ojp/domain/usecase/RequestTrips.kt +++ b/sdk/src/main/java/ch/opentransportdata/ojp/domain/usecase/RequestTrips.kt @@ -84,7 +84,7 @@ internal class RequestTrips( numberOfResultsAfter = null, numberOfResults = null ), - individualTransportOption = state.individualTransportOption!! + individualTransportOption = state.individualTransportOption ) } @@ -103,7 +103,7 @@ internal class RequestTrips( numberOfResultsAfter = numberOfResultsAfter, numberOfResults = null ), - individualTransportOption = state.individualTransportOption!! + individualTransportOption = state.individualTransportOption ) }