diff --git a/iosApp/iosApp/Pages/Map/HomeMapView.swift b/iosApp/iosApp/Pages/Map/HomeMapView.swift index 5a765454f..929f2d757 100644 --- a/iosApp/iosApp/Pages/Map/HomeMapView.swift +++ b/iosApp/iosApp/Pages/Map/HomeMapView.swift @@ -34,8 +34,6 @@ struct HomeMapView: View { @State var vehiclesRepository: IVehiclesRepository @State var vehiclesData: [Vehicle]? - @State var upcomingRoutePatterns: Set = .init() - @StateObject var locationDataManager: LocationDataManager @Binding var sheetHeight: CGFloat diff --git a/iosApp/iosApp/Pages/Map/HomeMapViewHandlerExtension.swift b/iosApp/iosApp/Pages/Map/HomeMapViewHandlerExtension.swift index c10821913..9856ed69d 100644 --- a/iosApp/iosApp/Pages/Map/HomeMapViewHandlerExtension.swift +++ b/iosApp/iosApp/Pages/Map/HomeMapViewHandlerExtension.swift @@ -101,7 +101,9 @@ extension HomeMapView { leaveVehiclesChannel() vehiclesRepository.connect(routeId: routeId, directionId: directionId) { outcome in if case let .ok(result) = onEnum(of: outcome) { - vehiclesData = Array(result.data.vehicles.values) + if let departures = nearbyVM.departures { + vehiclesData = Array(departures.filterVehiclesByUpcoming(vehicles: result.data).values) + } } } } diff --git a/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/map/RouteFeaturesBuilder.kt b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/map/RouteFeaturesBuilder.kt index 21316db51..bbd96c602 100644 --- a/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/map/RouteFeaturesBuilder.kt +++ b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/map/RouteFeaturesBuilder.kt @@ -241,11 +241,7 @@ object RouteFeaturesBuilder { if (targetRouteData.isNotEmpty()) { return departures?.let { - val upcomingRoutePatternIds = - departures.routes - .flatMap { it.allUpcomingTrips() } - .mapNotNull { it.trip.routePatternId } - val targetRoutePatternIds = upcomingRoutePatternIds.toSet() + val targetRoutePatternIds = departures.upcomingPatternIds targetRouteData.map { routeData -> val filteredShapes = routeData.segmentedShapes.filter { diff --git a/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/model/StopDetailsDepartures.kt b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/model/StopDetailsDepartures.kt index 2245d08b9..463a33c21 100644 --- a/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/model/StopDetailsDepartures.kt +++ b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/model/StopDetailsDepartures.kt @@ -5,6 +5,7 @@ import com.mbta.tid.mbta_app.model.response.GlobalResponse import com.mbta.tid.mbta_app.model.response.NearbyResponse import com.mbta.tid.mbta_app.model.response.PredictionsStreamDataResponse import com.mbta.tid.mbta_app.model.response.ScheduleResponse +import com.mbta.tid.mbta_app.model.response.VehiclesStreamDataResponse import kotlinx.datetime.Instant data class StopDetailsDepartures(val routes: List) { @@ -44,6 +45,15 @@ data class StopDetailsDepartures(val routes: List) { } ) + val allUpcomingTrips = routes.flatMap { it.allUpcomingTrips() } + + val upcomingPatternIds = allUpcomingTrips.mapNotNull { it.trip.routePatternId }.toSet() + + fun filterVehiclesByUpcoming(vehicles: VehiclesStreamDataResponse): Map { + val routeIds = allUpcomingTrips.map { it.trip.routeId }.toSet() + return vehicles.vehicles.filter { routeIds.contains(it.value.routeId) } + } + fun autoFilter(): StopDetailsFilter? { if (routes.size != 1) { return null diff --git a/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/phoenix/VehiclesOnRouteChannel.kt b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/phoenix/VehiclesOnRouteChannel.kt index abd9a128c..b3fb6f875 100644 --- a/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/phoenix/VehiclesOnRouteChannel.kt +++ b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/phoenix/VehiclesOnRouteChannel.kt @@ -8,6 +8,9 @@ object VehiclesOnRouteChannel { val newDataEvent = "stream_data" + fun topic(routeIds: List, directionId: Int) = + "vehicles:routes:${routeIds.joinToString(",")}:${directionId}" + fun joinPayload(routeId: String, directionId: Int): Map { return mapOf("route_id" to routeId, "direction_id" to directionId) } diff --git a/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/repositories/VehiclesRepository.kt b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/repositories/VehiclesRepository.kt index c3ac30fd0..67ee2efc5 100644 --- a/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/repositories/VehiclesRepository.kt +++ b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/repositories/VehiclesRepository.kt @@ -2,6 +2,7 @@ package com.mbta.tid.mbta_app.repositories import com.mbta.tid.mbta_app.model.SocketError import com.mbta.tid.mbta_app.model.Vehicle +import com.mbta.tid.mbta_app.model.greenRoutes import com.mbta.tid.mbta_app.model.response.ApiResult import com.mbta.tid.mbta_app.model.response.VehiclesStreamDataResponse import com.mbta.tid.mbta_app.network.PhoenixChannel @@ -29,8 +30,16 @@ class VehiclesRepository(private val socket: PhoenixSocket) : IVehiclesRepositor directionId: Int, onReceive: (ApiResult) -> Unit ) { - val joinPayload = VehiclesOnRouteChannel.joinPayload(routeId, directionId) - channel = socket.getChannel(VehiclesOnRouteChannel.topic, joinPayload) + val topic = + VehiclesOnRouteChannel.topic( + if (routeId == "line-Green") { + greenRoutes.toList() + } else { + listOf(routeId) + }, + directionId + ) + channel = socket.getChannel(topic, emptyMap()) channel?.onEvent(VehiclesOnRouteChannel.newDataEvent) { message -> handleNewDataMessage(message, onReceive) diff --git a/shared/src/commonTest/kotlin/com/mbta/tid/mbta_app/model/StopDetailsDeparturesTest.kt b/shared/src/commonTest/kotlin/com/mbta/tid/mbta_app/model/StopDetailsDeparturesTest.kt index 0392f08ae..177180476 100644 --- a/shared/src/commonTest/kotlin/com/mbta/tid/mbta_app/model/StopDetailsDeparturesTest.kt +++ b/shared/src/commonTest/kotlin/com/mbta/tid/mbta_app/model/StopDetailsDeparturesTest.kt @@ -4,6 +4,7 @@ import com.mbta.tid.mbta_app.model.response.AlertsStreamDataResponse import com.mbta.tid.mbta_app.model.response.GlobalResponse import com.mbta.tid.mbta_app.model.response.PredictionsStreamDataResponse import com.mbta.tid.mbta_app.model.response.ScheduleResponse +import com.mbta.tid.mbta_app.model.response.VehiclesStreamDataResponse import kotlin.test.Test import kotlin.test.assertEquals import kotlin.time.Duration.Companion.hours @@ -228,6 +229,30 @@ class StopDetailsDeparturesTest { val directionWest = Direction("West", "Kenmore & West", 0) val directionEast = Direction("East", "Park St & North", 1) + val departures = + StopDetailsDepartures( + stop, + GlobalResponse( + objects, + mapOf( + stop.id to + listOf( + routePatternB1.id, + routePatternB2.id, + routePatternC1.id, + routePatternC2.id, + routePatternE1.id, + routePatternE2.id + ) + ) + ), + ScheduleResponse(objects), + PredictionsStreamDataResponse(objects), + null, + setOf(), + filterAtTime = time, + ) + assertEquals( StopDetailsDepartures( listOf( @@ -269,6 +294,115 @@ class StopDetailsDeparturesTest { ) ) ), + departures + ) + + assertEquals( + setOf( + routePatternB1.id, + routePatternC1.id, + routePatternE1.id, + routePatternB2.id, + routePatternC2.id, + routePatternE2.id + ), + departures.upcomingPatternIds + ) + } + + @Test + fun `StopDetailsDepartures filters vehicles by relevant routes`() { + val objects = ObjectCollectionBuilder() + + val stop = objects.stop() + objects.line { id = "line-Green" } + val routeB = + objects.route { + id = "B" + sortOrder = 1 + lineId = "line-Green" + directionNames = listOf("West", "East") + directionDestinations = listOf("Kenmore & West", "Park St & North") + } + val routePatternB = + objects.routePattern(routeB) { + representativeTrip { headsign = "B" } + directionId = 0 + typicality = RoutePattern.Typicality.Typical + } + val tripB = objects.trip(routePatternB) + + val routeC = + objects.route { + id = "C" + sortOrder = 2 + lineId = "line-Green" + directionNames = listOf("West", "East") + directionDestinations = listOf("Kenmore & West", "Park St & North") + } + val routePatternC = + objects.routePattern(routeC) { + representativeTrip { headsign = "C" } + directionId = 0 + typicality = RoutePattern.Typicality.Typical + } + val tripC = objects.trip(routePatternC) + + val routeD = + objects.route { + id = "D" + sortOrder = 3 + lineId = "line-Green" + directionNames = listOf("West", "East") + directionDestinations = listOf("Riverside", "Park St & North") + } + + val routeE = + objects.route { + id = "E" + sortOrder = 3 + lineId = "line-Green" + directionNames = listOf("West", "East") + directionDestinations = listOf("Heath Street", "Park St & North") + } + val routePatternE = + objects.routePattern(routeE) { + representativeTrip { headsign = "Heath Street" } + directionId = 0 + typicality = RoutePattern.Typicality.Typical + id = "test-hs" + } + val tripE = objects.trip(routePatternE) + + val time = Instant.parse("2024-03-18T10:41:13-04:00") + + val schedB = + objects.schedule { + trip = tripB + stopId = stop.id + stopSequence = 90 + departureTime = time + 1.minutes + } + val schedC = + objects.schedule { + trip = tripC + stopId = stop.id + stopSequence = 90 + departureTime = time + 2.minutes + } + val schedE = + objects.schedule { + trip = tripE + stopId = stop.id + stopSequence = 90 + departureTime = time + 3.minutes + } + + objects.prediction(schedB) { departureTime = time + 1.5.minutes } + objects.prediction(schedC) { departureTime = time + 2.3.minutes } + objects.prediction(schedE) { departureTime = time + 2.3.minutes } + + val departures = StopDetailsDepartures( stop, GlobalResponse( @@ -276,12 +410,9 @@ class StopDetailsDeparturesTest { mapOf( stop.id to listOf( - routePatternB1.id, - routePatternB2.id, - routePatternC1.id, - routePatternC2.id, - routePatternE1.id, - routePatternE2.id + routePatternB.id, + routePatternC.id, + routePatternE.id, ) ) ), @@ -291,6 +422,42 @@ class StopDetailsDeparturesTest { setOf(), filterAtTime = time, ) + val vehicleB = + objects.vehicle { + routeId = routeB.id + currentStatus = Vehicle.CurrentStatus.InTransitTo + } + val vehicleC = + objects.vehicle { + routeId = routeC.id + currentStatus = Vehicle.CurrentStatus.InTransitTo + } + val vehicleD = + objects.vehicle { + routeId = routeD.id + currentStatus = Vehicle.CurrentStatus.InTransitTo + } + val vehicleE = + objects.vehicle { + routeId = routeE.id + currentStatus = Vehicle.CurrentStatus.InTransitTo + } + val vehicleResponse = + VehiclesStreamDataResponse( + mapOf( + vehicleB.id to vehicleB, + vehicleC.id to vehicleC, + vehicleD.id to vehicleD, + vehicleE.id to vehicleE, + ) + ) + assertEquals( + mapOf( + vehicleB.id to vehicleB, + vehicleC.id to vehicleC, + vehicleE.id to vehicleE, + ), + departures.filterVehiclesByUpcoming(vehicleResponse) ) } diff --git a/shared/src/commonTest/kotlin/com/mbta/tid/mbta_app/phoenix/VehiclesOnRouteChannelTest.kt b/shared/src/commonTest/kotlin/com/mbta/tid/mbta_app/phoenix/VehiclesOnRouteChannelTest.kt index bb757a700..ea514e29a 100644 --- a/shared/src/commonTest/kotlin/com/mbta/tid/mbta_app/phoenix/VehiclesOnRouteChannelTest.kt +++ b/shared/src/commonTest/kotlin/com/mbta/tid/mbta_app/phoenix/VehiclesOnRouteChannelTest.kt @@ -59,4 +59,13 @@ class VehiclesOnRouteChannelTest { parsed ) } + + @Test + fun testTopicInterpolation() { + val topic1 = VehiclesOnRouteChannel.topic(listOf("Red"), 0) + val topic2 = VehiclesOnRouteChannel.topic(listOf("Green-B", "Green-C", "Green-D"), 1) + + assertEquals("vehicles:routes:Red:0", topic1) + assertEquals("vehicles:routes:Green-B,Green-C,Green-D:1", topic2) + } } diff --git a/shared/src/commonTest/kotlin/com/mbta/tid/mbta_app/repositories/VehiclesRepositoryTest.kt b/shared/src/commonTest/kotlin/com/mbta/tid/mbta_app/repositories/VehiclesRepositoryTest.kt index f2fe61572..e32d089fa 100644 --- a/shared/src/commonTest/kotlin/com/mbta/tid/mbta_app/repositories/VehiclesRepositoryTest.kt +++ b/shared/src/commonTest/kotlin/com/mbta/tid/mbta_app/repositories/VehiclesRepositoryTest.kt @@ -9,7 +9,7 @@ import com.mbta.tid.mbta_app.network.PhoenixMessage import com.mbta.tid.mbta_app.network.PhoenixPush import com.mbta.tid.mbta_app.network.PhoenixPushStatus import com.mbta.tid.mbta_app.network.PhoenixSocket -import com.mbta.tid.mbta_app.phoenix.PredictionsForStopsChannel +import com.mbta.tid.mbta_app.phoenix.VehiclesOnRouteChannel import dev.mokkery.MockMode import dev.mokkery.answering.returns import dev.mokkery.every @@ -44,7 +44,7 @@ class VehiclesRepositoryTest : KoinTest { val vehiclesRepo = VehiclesRepository(socket) every { socket.getChannel(any(), any()) } returns mock(MockMode.autofill) vehiclesRepo.channel = - socket.getChannel(topic = PredictionsForStopsChannel.topic, params = emptyMap()) + socket.getChannel(topic = VehiclesOnRouteChannel.topic, params = emptyMap()) assertNotNull(vehiclesRepo.channel) vehiclesRepo.disconnect()